思路以及代码都来源于下面两篇文章:
一个不知死活的胖子:Python做文本情感分析之情感极性分析
Ran Fengzheng 的博客:基于情感词典的文本情感极性分析相关代码
基于情感词典的情感分析应该是最简单的情感分析方法了,大致说一下使用情感词典进行情感分析的思路:
对文档分词,找出文档中的情感词、否定词以及程度副词,然后判断每个情感词之前是否有否定词及程度副词,将它之前的否定词和程度副词划分为一个组,如果有否定词将情感词的情感权值乘以-1,如果有程度副词就乘以程度副词的程度值,最后所有组的得分加起来,大于0的归于正向,小于0的归于负向。
准备:
1.BosonNLP情感词典
既然是基于情感词典的分析,当然需要一份包含所有情感词的词典,网上已有现成的,直接下载即可。
https://bosonnlp.com/dev/resource
从下载的文件里,随便粘了几个正向的情感词,词后面的数字表示的是情感词的情感分值,一般正向的都是正数,负向的是负数:
丰富多彩 1.87317228434 神采飞扬 1.87321290817 细微 1.87336937803 178.00 1.87338705728 不辞辛劳 1.87338705728 保加利亚 1.87338705728
注:由于BosonNLP是基于微博、新闻、论坛等数据来源构建的情感词典,因此拿来对其他类别的文本进行分析效果可能不好
也有一种将所有情感词的情感分值设为1的方法来计算,想要详细了解可参考此文章:
2.否定词词典
文本情感分类(一):传统模型中提供了一个情感极性词典的下载包,包中带了一个否定词的txt。
不大 不丁点儿 不甚 不怎么 聊 没怎么 不可以 怎么不 几乎不 从来不 从不 不用 不曾 不该 不必 不会 不好 不能 很少 极少 没有 不是 难以 放下 扼杀 终止 停止 放弃 反对 缺乏 缺少 不 甭 勿 别 未 反 没 否 木有 非 无 请勿 无须 并非 毫无 决不 休想 永不 不要 未尝 未曾 毋 莫 从未 从未有过 尚未 一无 并未 尚无 从没 绝非 远非 切莫 绝不 毫不 禁止 忌 拒绝 杜绝 弗3.程度副词词典
程度副词如:非常、很、特别...等词
原博中提供了《知网》情感分析用词语集(beta版)的下载链接,该词典中包含了程度副词已经对应的程度值,但是下载下来之后发现只有程度副词,并没有对应的程度值。
从程度级别词语.txt中选取了一部分程度副词,可以看到只有程度词,没有程度值,这个时候就自己看情况赋一个值好了:
中文程度级别词语 219 1. “极其|extreme / 最|most” 69 百分之百 倍加 备至 不得了 不堪 不可开交 不亦乐乎 不折不扣 彻头彻尾 充分 到头 地地道道 非常 极 极度 极端 极其 极为 截然 尽 惊人地
改完之后的格式如下,程度副词和程度值用逗号分割,程度值可以自己定义:
百分之百,2 倍加,2 备至,2 不得了,2 不堪,2 不可开交,2 不亦乐乎,2 不折不扣,2 彻头彻尾,2 .....
4.停用词词典
数据堂的下载本地总是打不开,因此原博中提供的数据堂的中文停用词下载也是没下载下来,然后使用了snownlp源码中的停用词词典,但是后来发现有些情感词被当做停用词了
数据堂停用词下载:http://www.datatang.com/data/43894
snownlp源码:https://github.com/isnowfy/snownlp (停用词在snownlp/normal文件夹下 stopwords.txt)
5.分词工具
由于使用python,选择了jieba分词
数据和工具都准备好了,现在可以开始情感分析了~
来一个简单的句子:我今天很高兴也非常开心
(1)分词,去除停用词
我、今天、也被当作停用词去掉,剩下很、高兴、非常、开心
def seg_word(sentence): """使用jieba对文档分词""" seg_list = jieba.cut(sentence) seg_result = [] for w in seg_list: seg_result.append(w) # 读取停用词文件 stopwords = set() fr = codecs.open('stopwords.txt', 'r', 'utf-8') for word in fr: stopwords.add(word.strip()) fr.close() # 去除停用词 return list(filter(lambda x: x not in stopwords, seg_result))(2)将分词结果转为字典,key为单词,value为单词在分词结果中的索引,后来想到一个问题,如果把单词作为key的话假如一个情感词在文中出现了多次,那么应该是只记录了这个词最后一次出现的位置,其他的被覆盖掉了。
将上一步得到的分词结果转为字典:
{'很': 0, '高兴': 1, '非常': 2, '开心': 3}
def list_to_dict(word_list): """将分词后的列表转为字典,key为单词,value为单词在列表中的索引,索引相当于词语在文档中出现的位置""" data = {} for x in range(0, len(word_list)): data[word_list[x]] = x return data
(3)对分词结果分类,找出情感词、否定词和程度副词
情感词sen_word(高兴和开心,key为单词的索引,value为情感权值):
{1: '1.48950851679', 3: '2.61234173173'}
程度副词degree_word(很和非常,key为索引,value为程度值)
{0: '1.75', 2: '2'}
否定词not_word,由于没有出现否定词,所以否定词为空:
{}
def classify_words(word_dict): """词语分类,找出情感词、否定词、程度副词""" # 读取情感字典文件 sen_file = open('BosonNLP_sentiment_score.txt', 'r+', encoding='utf-8') # 获取字典文件内容 sen_list = sen_file.readlines() # 创建情感字典 sen_dict = defaultdict() # 读取字典文件每一行内容,将其转换为字典对象,key为情感词,value为对应的分值 for s in sen_list: # 每一行内容根据空格分割,索引0是情感词,索引1是情感分值(情感词典文件中有一行是空行,因此执行的时候会报错,注意处理一下空行,这里没有处理) sen_dict[s.split(' ')[0]] = s.split(' ')[1] # 读取否定词文件 not_word_file = open('notDic.txt', 'r+', encoding='utf-8') # 由于否定词只有词,没有分值,使用list即可 not_word_list = not_word_file.readlines() # 读取程度副词文件 degree_file = open('degree.txt', 'r+', encoding='utf-8') degree_list = degree_file.readlines() degree_dic = defaultdict() # 程度副词与情感词处理方式一样,转为程度副词字典对象,key为程度副词,value为对应的程度值 for d in degree_list: degree_dic[d.split(',')[0]] = d.split(',')[1] # 分类结果,词语的index作为key,词语的分值作为value,否定词分值设为-1 sen_word = dict() not_word = dict() degree_word = dict()(4)计算得分
首先设置初始权重W为1,从第一个情感词开始,用权重W*该情感词的情感值作为得分(用score记录),然后判断与下一个情感词之间是否有程度副词及否定词,如果有否定词将W*-1,如果有程度副词,W*程度副词的程度值,此时的W作为遍历下一个情感词的权重值,循环直到遍历完所有的情感词,每次遍历过程中的得分score加起来的总和就是这篇文档的情感得分。
def socre_sentiment(sen_word, not_word, degree_word, seg_result): """计算得分""" # 权重初始化为1 W = 1 score = 0 # 情感词下标初始化 sentiment_index = -1 # 情感词的位置下标集合 sentiment_index_list = list(sen_word.keys()) # 遍历分词结果(遍历分词结果是为了定位两个情感词之间的程度副词和否定词) for i in range(0, len(seg_result)): # 如果是情感词(根据下标是否在情感词分类结果中判断) if i in sen_word.keys(): # 权重*情感词得分 score += W * float(sen_word[i]) # 情感词下标加1,获取下一个情感词的位置 sentiment_index += 1 if sentiment_index < len(sentiment_index_list) - 1: # 判断当前的情感词与下一个情感词之间是否有程度副词或否定词 for j in range(sentiment_index_list[sentiment_index], sentiment_index_list[sentiment_index + 1]): # 更新权重,如果有否定词,取反 if j in not_word.keys(): W *= -1 elif j in degree_word.keys(): # 更新权重,如果有程度副词,分值乘以程度副词的程度分值 W *= float(degree_word[j]) # 定位到下一个情感词 if sentiment_index < len(sentiment_index_list) - 1: i = sentiment_index_list[sentiment_index + 1] return score
W=1
score=0
第一个情感词是高兴,高兴的情感权值为1.48950851679,score=W*情感权值=1*1.48950851679=1.48950851679
高兴和下一个情感词开心之间出现了程度副词非常,程度值为2,因此W=W*2=1*2=2,然后获取下一个情感词
下一个情感词是开心,此时W=2,score=score+2*2.61234173173=1.48950851679+2*2.61234173173=6.71419198025
遍历结束
这里也发现两个问题:
(1)第一个情感词之前出现的程度副词和否定词被忽略了
(2)在判断两个情感词之间出现否定词以及程度副词时,W没有被初始化为1,这样W就被累乘了
有兴趣的可以修改一下~
完整代码:
from collections import defaultdict import os import re import jieba import codecs def seg_word(sentence): """使用jieba对文档分词""" seg_list = jieba.cut(sentence) seg_result = [] for w in seg_list: seg_result.append(w) # 读取停用词文件 stopwords = set() fr = codecs.open('stopwords.txt', 'r', 'utf-8') for word in fr: stopwords.add(word.strip()) fr.close() # 去除停用词 return list(filter(lambda x: x not in stopwords, seg_result)) def classify_words(word_dict): """词语分类,找出情感词、否定词、程度副词""" # 读取情感字典文件 sen_file = open('BosonNLP_sentiment_score.txt', 'r+', encoding='utf-8') # 获取字典文件内容 sen_list = sen_file.readlines() # 创建情感字典 sen_dict = defaultdict() # 读取字典文件每一行内容,将其转换为字典对象,key为情感词,value为对应的分值 for s in sen_list: # 每一行内容根据空格分割,索引0是情感词,索引01是情感分值 sen_dict[s.split(' ')[0]] = s.split(' ')[1] # 读取否定词文件 not_word_file = open('notDic.txt', 'r+', encoding='utf-8') # 由于否定词只有词,没有分值,使用list即可 not_word_list = not_word_file.readlines() # 读取程度副词文件 degree_file = open('degree.txt', 'r+', encoding='utf-8') degree_list = degree_file.readlines() degree_dic = defaultdict() # 程度副词与情感词处理方式一样,转为程度副词字典对象,key为程度副词,value为对应的程度值 for d in degree_list: degree_dic[d.split(',')[0]] = d.split(',')[1] # 分类结果,词语的index作为key,词语的分值作为value,否定词分值设为-1 sen_word = dict() not_word = dict() degree_word = dict() # 分类 for word in word_dict.keys(): if word in sen_dict.keys() and word not in not_word_list and word not in degree_dic.keys(): # 找出分词结果中在情感字典中的词 sen_word[word_dict[word]] = sen_dict[word] elif word in not_word_list and word not in degree_dic.keys(): # 分词结果中在否定词列表中的词 not_word[word_dict[word]] = -1 elif word in degree_dic.keys(): # 分词结果中在程度副词中的词 degree_word[word_dict[word]] = degree_dic[word] sen_file.close() degree_file.close() not_word_file.close() # 将分类结果返回 return sen_word, not_word, degree_word def list_to_dict(word_list): """将分词后的列表转为字典,key为单词,value为单词在列表中的索引,索引相当于词语在文档中出现的位置""" data = {} for x in range(0, len(word_list)): data[word_list[x]] = x return data def get_init_weight(sen_word, not_word, degree_word): # 权重初始化为1 W = 1 # 将情感字典的key转为list sen_word_index_list = list(sen_word.keys()) if len(sen_word_index_list) == 0: return W # 获取第一个情感词的下标,遍历从0到此位置之间的所有词,找出程度词和否定词 for i in range(0, sen_word_index_list[0]): if i in not_word.keys(): W *= -1 elif i in degree_word.keys(): # 更新权重,如果有程度副词,分值乘以程度副词的程度分值 W *= float(degree_word[i]) return W def socre_sentiment(sen_word, not_word, degree_word, seg_result): """计算得分""" # 权重初始化为1 W = 1 score = 0 # 情感词下标初始化 sentiment_index = -1 # 情感词的位置下标集合 sentiment_index_list = list(sen_word.keys()) # 遍历分词结果(遍历分词结果是为了定位两个情感词之间的程度副词和否定词) for i in range(0, len(seg_result)): # 如果是情感词(根据下标是否在情感词分类结果中判断) if i in sen_word.keys(): # 权重*情感词得分 score += W * float(sen_word[i]) # 情感词下标加1,获取下一个情感词的位置 sentiment_index += 1 if sentiment_index < len(sentiment_index_list) - 1: # 判断当前的情感词与下一个情感词之间是否有程度副词或否定词 for j in range(sentiment_index_list[sentiment_index], sentiment_index_list[sentiment_index + 1]): # 更新权重,如果有否定词,取反 if j in not_word.keys(): W *= -1 elif j in degree_word.keys(): # 更新权重,如果有程度副词,分值乘以程度副词的程度分值 W *= float(degree_word[j]) # 定位到下一个情感词 if sentiment_index < len(sentiment_index_list) - 1: i = sentiment_index_list[sentiment_index + 1] return score # 计算得分 def setiment_score(sententce): # 1.对文档分词 seg_list = seg_word(sententce) # 2.将分词结果列表转为dic,然后找出情感词、否定词、程度副词 sen_word, not_word, degree_word = classify_words(list_to_dict(seg_list)) # 3.计算得分 score = socre_sentiment(sen_word, not_word, degree_word, seg_list) return score # 测试 print(setiment_score("我今天很高兴也非常开心"))