整个项目是我借鉴了论文的内容一行代码一行代码敲的(除了.scel转为.txt),没有查阅github上面的开源项目等(就是说我自己封装了一个SO-PMI算法),说不定有漏洞或者思考不清晰的地方,如果发现了请麻烦指正。当然在进行计算的时候,我是采用的循环,并没有将数据矩阵化或者向量化(主要是还不清楚怎样矩阵化或者向量化),在运行效率上确实很差。
这是我在3400条数据集上跑出来的效果,效果很烂,主要数据集太拉胯了。
整个项目的代码除了数据集已开源到了github上,我会在文章后放上链接。
该项目算是复现了少许论文《基于情感词典的中文微博情感分析与话题倾向性判定研究》中SO-PMI那部分的代码,只不过公式更贴近于原始公式。
本文将从项目的目的,SO-PMI算法,数据集获取,代码讲解四部分说明。
目录
1 项目目的
通过已有的情感基准词判定网络用语的情感极性,包括积极的、消极的,中性的。
整个项目没有做性能度量,不过可以通过人工标注实际数据的真实情感极性来衡量准确率、召回率、F1度量等。
2 SO-PMI算法
SO-PMI(情感倾向点互信息算法,Semantic Orientation Pointwise Mutual Information),SO-PMI算法由两部分组成:SO-PMI和PMI。
算法整体思想很简单,判断需要判断的词语 P ( w o r d ) P(word) P(word)与基准词 P ( b a s e ) P(base) P(base)同时出现的概率,如果与积极(positive)的词同时出现的概率更高,那么就判断为积极的词语,如果与消极(negative)的词同时出现的概率更高,那么就判断为消极的词语,如果与积极和消极的概率相同,那么就判断为中性的词语。
下面分别介绍PMI和SO-PMI:
2.1 PMI
PMI(Pointwise Mutual Information,点互信息算法),用于判断某个词与基准词出现的概率。公式如下:
P
M
I
=
log
2
(
P
(
w
o
r
d
1
,
w
o
r
d
2
)
P
(
w
o
r
d
1
)
P
(
w
o
r
d
2
)
)
PMI=\log_2(\frac{P(word1, word2)}{P(word1)P(word2)})
PMI=log2(P(word1)P(word2)P(word1,word2))
P ( w o r d 1 , w o r d 2 ) P(word1, word2) P(word1,word2)为联合概率,即 w o r d 1 word1 word1和 w o r d 2 word2 word2同时出现在语料中的概率,如果两者独立,那么 P ( w o r d 1 , w o r d 2 ) = P ( w o r d 1 ) P ( w o r d 2 ) P(word1, word2) = P(word1)P(word2) P(word1,word2)=P(word1)P(word2),即整个分数为1,那么 P M I = 0 PMI=0 PMI=0。
我查了很多资料并没有说明这个 log 2 \log2 log2的含义,但是我估计是和log的曲线有关,不会影响到单调性,同时不会让1这个点附近的值变化过大这个原因。
PMI最终结果的分析如下:
- P M I > 0 PMI>0 PMI>0,两个词语相关,值越大关联性越强;
- P M I = 0 PMI=0 PMI=0,两个词语独立;
- P M I < 0 PMI<0 PMI<0,两个词语不相关。
2.2 SO-PMI
SO-PMI(Semantic Orientation Pointwise Mutual Information,情感倾向点互信息算法),算法的思想是,判断陌生的词与基准词的关联程度,与积极基准词关联程度大,那么为积极的,与消极基准词关联程度大,那么为消极的,如果与积极和消极的概率相同(与积极和消极的词语都独立),那么就判断为中性的词语。
算法的公式如下:
S
O
−
P
M
I
(
w
o
r
d
)
=
∑
i
=
1
n
u
m
(
p
o
s
)
P
M
I
(
w
o
r
d
,
p
o
s
i
)
−
∑
i
=
1
n
u
m
(
n
e
g
)
P
M
I
(
w
o
r
d
,
n
e
g
i
)
SO-PMI(word)=\sum_{i=1}^{num(pos)}PMI(word, pos_i)-\sum_{i=1}^{num(neg)}PMI(word,neg_i)
SO−PMI(word)=i=1∑num(pos)PMI(word,posi)−i=1∑num(neg)PMI(word,negi)
这里 n u m ( p o s ) num(pos) num(pos)指的是积极基准词的总数,同理 n u m ( n e g ) num(neg) num(neg)指的是消极基准词的总数。
公式带来的结果如下:
- S O − P M I > 0 SO-PMI>0 SO−PMI>0,词语判定为积极词语;
- S O − P M I = 0 SO-PMI=0 SO−PMI=0,词语判定为中性词语;
- S O − P M I < 0 SO-PMI<0 SO−PMI<0,词语判定为消极词语;
3. 数据集获取
本项目的数据集并没有开源,主要是大部分数据是我自己整理的(当然现在也就才3400+数据)。
项目数据集为4个,其中有2个来源于同一篇论文,如下:
- 微博语料,我是用的微博开放API,效率贼低,用过的都知道,API链接如下:微博开放平台,python3调用微博API的方法如下:全网最详:python3调用新浪微博API接口获取数据。后来和师兄聊了聊,我打算自己重新封装个用selenium爬取的方法;
- 2020年网络用语数据,我使用的是搜狗网络流行语,人工清洗了45510条数据后标注出的网络用语:网络流行新词【官方推荐】;
- 情感基准词,我是直接使用的论文《基于HowNet和PMI的词语情感极性计算》整理出的基准词。
嗯,担心后面有人找我要数据集,我这里就先说明一下,数据集我这边收集好了,等以后我会挂到CSDN的资源下载里面,至于多久我也不清楚。
4 代码讲解
这一部分我主要讲一下代码里面我觉得比较关键和后续可以考虑改进的部分,想要详细代码的,可以访问我开源到github上的代码,当然,我自认为就在这份代码上改进的部分还有很多,所以欢迎大家多多讨论,github地址:nlp_so-pmi。
4.1 .scel转.txt
这一部分的代码我是借鉴的,这是原博客地址,这不过我在这个的基础上更改了一点,主要是在getWordPy这个函数里面,我改为了:
def getWordPy(data):
pos = 0
ret = ''
flag = 1
while pos < len(data):
index = struct.unpack('H', bytes([data[pos], data[pos + 1]]))[0]
if index <= max_index:
ret += GPy_Table[index]
pos += 2
else:
flag = 0
break
return ret, flag
我这里多返回了一个flag标志位,主要是因为这个代码输出出来一共拼音组合我记得好像就412个,但是在刚刚那个数据集上有的数据这个索引会达到34000几(从第一个错误开始,之后的数据索引值都是这个),直接报错,而且又是Unicode编码格式,我又不会查看,所以我这里直接设置这么一个标志位,当索引值达到第一个错误值的时候跳出循环并保存数据。最后还是获得了45510条数据,反正我人工标注还是连续标注了几个小时的,眼睛都看来眼泪止不住。
跳出循环也很简单,就是判断flag值:
if flag == 0:
break
4.2 so-pmi
这里我一共封装了3个方法,分别是:
- get_probability(word_list, weibo_list),用于计算单独概率 P ( w o r d ) P(word) P(word);
- get_joint_probability(buzzwords, base_word_list, weibo_list),用于计算联合概率 P ( w o r d , b a s e w o r d ) P(word, baseword) P(word,baseword);
- get_so_pmi(p_buzzwords, p_base_positive, p_base_negative, jp_buzzword_pos, jp_buzzword_neg),用于计算 s o − p m i so-pmi so−pmi值。
论文《基于情感词典的中文微博情感分析与话题倾向性判定研究》中,采用频率并非概率(其实我觉得好像概率也没办法表达,挺佩服作者的,能够把概率变为频率都扯几段话出来),所以我虽然这里面代码的注释都写的是概率,但是大家要注意,这里其实是频率,并非概率。
4.2.1 计算单独概率
def get_probability(word_list, weibo_list):
"""
获得概率, P(word)
:param word_list: 词语列表(包括流行词语, 正向情感基准词, 负向情感基准词)
:param weibo_list: 微博列表
:return p_word_dict: 词语与概率构成的词典, {word: probability}
"""
p_word_dict = {}
length = len(weibo_list)
for word in word_list:
count = 0
for weibo in weibo_list:
if re.search(word, weibo):
count += 1
p_word_dict[word] = count / length
return p_word_dict
为了提高代码复用性,我这里就直接封装了这么一个方法,通过调用3次来获取3种频率 P ( 流 行 语 ) P(流行语) P(流行语), P ( 积 极 基 准 词 ) P(积极基准词) P(积极基准词), P ( 消 极 基 准 词 ) P(消极基准词) P(消极基准词)。
这里主要就是用正则判断这个词语是否出现在微博中,我这里没用分词工具和循环判断,是因为分词工具会把这些网络流行语切成一个又一个单独的字,那么就根本没法用,而循环的话,不就等于我自己又封装了个正则表达式么,所以我就直接用正则了。
返回的部分数据如下:
{'甄开心': 0.0, ...}
4.2.2 计算联合概率
def get_joint_probability(buzzwords, base_word_list, weibo_list):
"""
获得联合概率, P(buzzword, baseword)
:param buzzwords: 流行语列表
:param base_word_list: 基准词列表
:param weibo_list: 微博列表
:return jp_word: 联合概率词典, {buzzword: {baseword: probability}}
"""
jp_word = {}
length = len(weibo_list)
for buzzword in buzzwords:
jp_word[buzzword] = {}
for base_word in base_word_list:
count = 0
for weibo in weibo_list:
if re.search(buzzword, weibo) and re.search(base_word, weibo):
count += 1
jp_word[buzzword][base_word] = count / length
return jp_word
这里联合概率一样,可以同样计算 P ( w o r d , p o s ) P(word, pos) P(word,pos)和 P ( w o r d , n e g ) P(word, neg) P(word,neg),不过我是用的两个正则来判断的,就是在这个微博里面同时发现了 w o r d word word和 b a s e w o r d baseword baseword才增加一次 c o u n t count count。
返回的部分数据如下:
{'甄开心': {'美丽': 0.0, ...}, ...}
4.3.3 计算so-pmi
def get_so_pmi(p_buzzwords, p_base_positive, p_base_negative, jp_buzzword_pos, jp_buzzword_neg):
"""
计算so-pmi值
:param p_buzzwords: 流行词出现概率的字典
:param p_base_positive: 正向基准词出现概率的字典
:param p_base_negative: 负向基准词出现概率的字典
:param jp_buzzword_pos: 流行语与正向基准词的联合概率字典
:param jp_buzzword_neg: 流行语与负向基准词的联合概率字典
:return so_pmi: so-pmi值, {buzzword: so-pmi}
"""
# {buzzword: so-pmi}
so_pmi = {}
for buzzword in p_buzzwords.keys():
pos_pmi = 0
neg_pmi = 0
# 加1平滑
for pos in p_base_positive.keys():
pos_pmi += log((jp_buzzword_pos[buzzword][pos] + 1) /
(p_base_positive[pos] * p_buzzwords[buzzword] + 1), 2)
for neg in p_base_negative.keys():
neg_pmi += log((jp_buzzword_neg[buzzword][neg] + 1) /
(p_base_negative[neg] * p_buzzwords[buzzword] + 1), 2)
so_pmi[buzzword] = pos_pmi - neg_pmi
return so_pmi
这里我是分开计算的 log 2 \log_2 log2的值然后相减,不清楚把这个转为 l o g log log内相除会不会效率高点,不过感觉好像也没那么重要,毕竟我主要考虑的是能否实现,并非算法效率。
这里最主要的是加1平滑,在分子分母上都选择了+1,主要理由如下:
- 当数据集够大的时候,分子分母+1其实对整体结果影响不会太大;
- 分子+1,是考虑到如果联合概率为0,那么 log 2 P ( w o r d , b a s e w o r d ) \log_2P(word, baseword) log2P(word,baseword)会直接报错;
- 分母+1,是考虑到如果分母为0,那么会直接报错。
返回的数据格式如下:
{'甄开心': 0.0, ...}
4.3 代码运行结果
由于数据集太小了(3400+),所以运行的结果不尽如人意,许多判断错误的情况,部分结果截图如下:
那么我们看到,就光上面的截图,“嗨皮”,“甄开心”,“真德秀”,“真不戳”都是分类错误了的,我们接下来分析下错误原因。
4.4 错误原因分析
-
根据论文《基于情感词典的中文微博情感分析与话题倾向性判定研究》中的内容,设定了
n u m ( p o s ) = n u m ( n e g ) num(pos)=num(neg) num(pos)=num(neg)
这里 n u m ( p o s ) num(pos) num(pos)是积极基准词独立出现在的微博中的数量, n u m ( n e g ) num(neg) num(neg)同理,这一部分就是作者论文中推导的内容,这段推导挺简单的,大家有兴趣可以拿论文来推导下,这一部分我简单说明一下,就是如果 n u m ( p o s ) ! = n u m ( n e g ) num(pos)!=num(neg) num(pos)!=num(neg),那么这个常数项会影响到最终情感极性的判定,为了避免这样的错误产生,所以作者人为设定这两个值相等。但是我并没有设置这样的情况,所以可能这方面也造成了偏差。 -
数据集太小了,我们可以从上面返回的结果来看,许多词语出现的概率都为0,而且论文《基于情感词典的中文微博情感分析与话题倾向性判定研究》中的判断语料是45万+微博数据,我的3400+,都不在同一个数量级上面了对吧……
-
在微博语料环境中,许多人发的内容估计只有短短一句话,比如“就这?”,“针不戳”之类的,那么这种的分析我估计不光是我,程序也是一头的黑人问号,所以导致了分类错误的情况。
5 参考
[1]陈佳慧. 基于情感词典的中文微博情感分析与话题倾向性判定研究[D].西南大学,2019.
[2]冯跃. 面向微博的情感倾向性研究[D].吉林大学,2018.
[3]寒江共雪.Python读取scel文件[EB/OL].https://blog.csdn.net/cqdiy/article/details/82840027,2018-9-25.
[4]今夜无风.点互信息算法(PMI)[EB/OL].https://www.cnblogs.com/demo-deng/p/13085188.html,2020-6-10.
[5]owolf.全网最详:python3调用新浪微博API接口获取数据[EB/OL].https://www.jianshu.com/p/7c68f3ca73ed,2018-10-21.