本文紧接上一篇理论文章《情感分析学习笔记(5)——PolarityRank算法原理讲解》,本文是代码实现。
参考的是两篇论文(Fernández-Gavilanes et al., 2016; Cruz et al., 2011)以及PageRank算法(Page et al., 1998)
由于找了一圈没有找到相关的代码,所以是我参考pagerank的实现过程以及论文自己手撸的代码,所以不一定保证效率与正确率,而且我测试也只用了positive的一条语句进行测试,没有测试过negative的和混合的,所以如果有问题,属实正常。
本文与SO-PMI算法一样,不会公开任何数据集,但是我会留下数据集的获取方式。
对了宝贝儿们,卑微小李的公众号【野指针小李】已开通,期待与你一起探讨学术哟~摸摸大!
代码我已上传至github,链接在这里:https://github.com/Balding-Lee/polarityrank/tree/master,如有需要,请自行提取
目录
1 准备工作
1.1 stanford coreNLP
我用的POS和NER的工具都是stanford coreNLP,这里不多阐述具体的安装配置方法,如有需要参考这篇文章。
这里帮大家排个坑,最新版的stanford coreNLP的中文版jar包下载下来名字是这样的:
如果就这样直接导入的话,会报错,具体报错的理由也很简单,就是你这个jar文件名字有问题。反正这是我编码5年多以来遇到的最离谱的一个错。要把jar包名字改为这样,就不会有问题了:
中间的年月日不重要,只需要是yyyy-MM-dd格式就行。
至于这个东西加载速度有点慢,我这里查了查相关资料,如果有需要的,参考这篇文章。不过我没用,也不知道效果怎么样。
1.2 数据集获取
数据集我一共使用了两个数据集,分别如下:
- 搜狗实验室的新闻数据,我使用的是迷你版的,链接如下:https://www.sogou.com/labs/resource/t.php
- 情感词汇表,我采用的清华大学整理的情感词汇表(带有情感极性的)。
1.3 POS标签
POS标签我是直接参考的stanford句法分析的标签,链接如下:https://blog.csdn.net/u011847043/article/details/79595225
这个POS标签我用来主要是提取出名词、动词、形容词,具体理由下面解释。
2 代码讲解
本代码没有用语料数据集,只用了一条语句进行测试,语句如下:
text = '阿龙在成都吃的炸鸡不仅香,而且价格便宜'
2.1 预处理
预处理环节我们需要处理的有以下2部分:
- 加载stanford coreNLP,并进行POS和NER;
- 清洗数据。
2.1.1 stanford coreNLP
def get_init_attr(text):
"""
获得初始属性, 包括:
1. 使用stanford coreNLP进行句法树分析, 得到的结果为str
2. 使用stanford coreNLP进行POS, 得到的结果为list
3. 使用stanford coreNLP进行NER, 得到的结果为list
:param text: 需要进行初始处理的文本
:return: 数据清洗后的结果, i.e. 清洗了命名实体后的所有名词、动词、形容词
"""
print("开始加载stanford coreNLP")
nlp = StanfordCoreNLP(r'D:\pycharm\workspace\polarityrank\stanford-corenlp-4.2.0', lang='zh')
# words_with_tags = nlp.parse(text) # 句法树分析
print("开始pos")
pos = nlp.pos_tag(text) # POS
print("pos结束")
print("开始ner")
ner = nlp.ner(text) # NER
print("ner结束")
# draw_tree(words_with_tags)
return clean_data(pos, ner)
虽然我吐槽了很多次这个stanford coreNLP加载速度极慢,但是不得不说一行代码出一串结果的感觉贼爽。
但是这一个代码是有问题的,主要出现在加载stanford coreNLP这里,如果你有多个数据,那么每次运行都会加载一次stanford coreNLP,我估计运行不了两三个数据你的java虚拟机就会崩盘,所以如果各位要跑大量数据集,一定要把这行代码改为静态加载的:
nlp = StanfordCoreNLP(r'D:\pycharm\workspace\polarityrank\stanford-corenlp-4.2.0', lang='zh')
将上面获得的POS结果绘制出来,结果如下:
至于为何需要NER,我们来看下面一步,清洗数据。
2.1.2 清洗数据
def clean_data(pos, ner):
"""
清洗数据, 步骤共2步:
1. 先对句子进行NER, 清洗掉所有命名实体, 因为命名实体不带有任何情感。
由于不清楚stanford coreNLP里面的标签, 所以只提取标签为'o'的词语
2. 提取出除了命名实体之外的所有名词、动词、形容词
:param pos: pos后的结果(list)
:param ner: ner后的结果(list)
:return nodes: (list)数据清洗后的结果, i.e. 清洗了命名实体后的所有名词、动词、形容词
"""
n_v_adj = pos_e.n_v_adj
nodes = [] # 存储句法图的词语
not_ne = [] # 存储非命名实体的词语
for n in ner:
if n[1] == 'O':
not_ne.append(n[0])
for p in pos:
if p[0] in not_ne and p[1] in n_v_adj:
nodes.append(p[0])
return nodes
首先根据 Fernández-Gavilanes et al. 老哥们的理论,一个句子中可能携带情感的词有:名词,动词,形容词。但是名词中有可能会有命名实体,这些就能带来更高的维度以及更低的准确率,比如“阿龙”的标签为Person,而我们也知道“阿龙”是不带有任何情感的。
但是由于我不知道NER有哪些标签,所以就直接清洗掉“O”以外的所有标签,同时设置一个列表,用于记录所有的名词、动词、形容词的POS标签:
n_v_adj = ['NN', 'NR', 'NT', 'MD', 'VV', 'JJ', 'JJR', 'JJS', 'VA']
清洗出来的词语有:
['吃', '炸鸡', '香', '价格', '便宜']
2.2 生成句法图
当有了句法树,并且提取出所有的名词、动词、形容词节点后,就可以开始生成句法图了。
2.2.1 句法图创建
还是Fernández-Gavilanes et al. 老哥们提出,句法图的创建方式为:
边的生成是依据节点之间的依赖关系,每个节点会与其所有的后代节点相连,但是为了使整棵树的左右都有边相连(因为情感传播不是针对于某棵子树),所以每个节点还会与自己的兄弟节点相连。作者考虑到系动词不带有任何情感极性,所以作者剔除了系动词,同时将系动词的孩子节点向上提一级。
反正这个我是敲代码敲不出来的,尤其是我一个连英语语法都不会的人,所以我就强行解释(狡辩),根据SO-PMI理论,只要共现的词语,那么必然有关联性,所以我直接把提取出来的节点构成了一个强连接图。
def create_network(nodes):
"""
创建强连接图(每条边都是双向连接)
:param nodes: 节点
:return dg: 有向图
"""
dg = nx.DiGraph()
for i in range(len(nodes)):
dg.add_nodes_from([(nodes[i], {'pos': i})]) # 创建节点
# 创建双向边
for i in nodes:
for j in nodes:
if i != j:
dg.add_edge(i, j)
# draw_network(dg)
get_words_sentiment(dg, nodes)
get_edges_weight(dg)
return dg
这里给每个节点赋值一个位置信息
i
i
i主要是方便后面处理,构成的图如下:
2.2.2 词语初始PR值确定
有了图后,我们需要对每个节点赋予初始的情感值 P R + , P R − PR^+,PR^- PR+,PR−,赋值语句如下:
def get_words_sentiment(dg, nodes):
"""
从清华大学情感词语数据集中获取节点的初始情感极性
:param dg: 有向图, 用于更新节点的PR值
:param nodes: 节点
"""
sentiment_words = [] # 存储情感词典数据的列表
node_attribute = {} # 存储节点属性的字典
with open('./data/sentiment_words/chinese_sentiment_words_with_polarity.txt', 'r') as f:
# print(f.read())
for line in f.readlines():
sentiment_words.append(line.strip('\n').split('\t'))
# 如果词语在情感词典中, 将情感值赋值给该词语, 否则情感值为0
for node in nodes:
for sentiment_word in sentiment_words:
if node == sentiment_word[0]:
if float(sentiment_word[1]) > 0:
# 词典中情感值 > 0, 则pr+为情感值, pr-为0
node_attribute[node] = [float(sentiment_word[1]), 0]
break
elif float(sentiment_word[1]) < 0:
# 词典中情感值 > 0, 则pr+为0, pr-为情感值
node_attribute[node] = [0, float(sentiment_word[1])]
break
# 词语不在情感词典中, 则pr+与pr-都为0
node_attribute[node] = [0, 0]
# 更新有向图节点
for node in node_attribute.keys():
dg.add_nodes_from([node], pr_plus=node_attribute[node][0], pr_minus=node_attribute[node][1])
我这里对情感值的判定条件是,如果 s > 0 s>0 s>0( s s s是每个词语的情感值),那么 P R + = s , P R − = 0 PR^+=s,PR^-=0 PR+=s,PR−=0,反之亦然,如果 s = 0 s=0 s=0,那么 P R + = 0 , P R − = 0 PR^+=0,PR^-=0 PR+=0,PR−=0。
所获得的的图的PR值为:
吃 {'pos': 0, 'pr_plus': 0, 'pr_minus': 0}
炸鸡 {'pos': 1, 'pr_plus': 0, 'pr_minus': 0}
香 {'pos': 2, 'pr_plus': 1.1513888888888888, 'pr_minus': 0}
价格 {'pos': 3, 'pr_plus': 0, 'pr_minus': 0}
便宜 {'pos': 4, 'pr_plus': 1.0333333333333332, 'pr_minus': 0}
2.2.3 词语之间边权判定
边权我并没有按照 Cruz et al. 等老哥的方法,分配 w i j + , w i j − w_{ij}^+,w_{ij}^- wij+,wij−(因为我代码写的是weight,所以我这里用 w w w表达权重,与上篇文章的 p p p等价)(主要是我不知道这群老哥怎么算出来的这两个值),我采用的是Fernández-Gavilanes et al. 老哥们的方法,每条边只有一个权重,即双向边来回的权重相同。
Fernández-Gavilanes et al. 老哥们没有在论文里面提出怎么算权重的,但是提到了一句记录两个词语共现的频率,然后我就懂了。
首先我获取搜狗新闻数据,这里没有用微博是考虑到如下两个原因:
- 微博十分稀疏(一条微博评论可能只有两三个词),这样会导致分母特别大,会造成干扰;
- 微博是非正式文本包含了许多的错别字、流行语、表情符号等,也会导致分母特别大,造成干扰。
def get_sogou_news():
"""
获取搜狗新闻语料
:return news: 新闻列表
"""
news = []
# 该文件是用gbk编码的, 但是文件中一些特殊字符超出了gbk编码范围, 所以采用gb18030,
# 为了避免无法编码字符, 添加errors='ignore'忽略
# 解决方案来源: https://blog.csdn.net/lqzdreamer/article/details/76549256
with open('./data/corpus/news_tensite_xml.smarty.dat', encoding='gb18030', errors='ignore') as f:
for line in f:
if re.match('<content>', line):
line = line.strip().lstrip('<content>').rstrip('</content>')
if line:
news.append(line)
return news
获得了新闻数据后,将两个词放入,计算共现频率。
def get_edges_weight(dg):
"""
获得每条边的权重
权重计算方法: n(word1, word2) / N
其中: n(word1, word2)为word1和word2在所有语料中共现的次数; N为语料的条数
:param dg: 有向图
"""
news = get_sogou_news()
nodes = list(dg.nodes())
N = len(news)
weights = []
temp_list = [] # 双向边只用计算一次权重
for node_1 in nodes:
temp_list.append(node_1)
# weight = 0
for node_2 in nodes:
if node_1 != node_2:
if node_2 not in temp_list:
# 如果节点不同, 且未曾出现, 则计算在语料中产生的频率
# 判断是否出现过, 主要是因为双向图, 权重是相同的,
# 且如果不处理, 后续会重复遍历语料, 减少运行速度
count = 0
for new in news:
# 判断是否共现
if re.search(node_1, new) and re.search(node_2, new):
count += 1
weight = count / N
else:
continue
else:
weight = 0
weights.append([node_1, node_2, weight])
# 虽然双向边的权重只用计算一次, 但是在更新图中的权重的时候要更新两条边
for weight in weights:
dg.add_edges_from([(weight[0], weight[1], {'weight': weight[2]})])
dg.add_edges_from([(weight[1], weight[0], {'weight': weight[2]})])
由于是双向图,且来回权重相同,所以我判断节点是否曾经出现过,这样主要是为了少遍历一半的新闻数据,减少时间浪费。
我也设定一个节点(词)不能与自己有边。
虽然权重只用计算一次,但是给边权赋值的时候要赋值两次哈。
这里的代码和之前的一样有问题,不可能每次都加载一遍新闻数据,所以如果要跑多条( ≥ 2 \geq2 ≥2)数据,这行代码也要改成静态加载:
news = get_sogou_news()
得到的边权为:
吃 炸鸡 {'weight': 0.006024096385542169}
吃 香 {'weight': 0.0}
吃 价格 {'weight': 0.006024096385542169}
吃 便宜 {'weight': 0.0}
吃 吃 {'weight': 0}
炸鸡 吃 {'weight': 0.006024096385542169}
炸鸡 香 {'weight': 0.0}
炸鸡 价格 {'weight': 0.0}
炸鸡 便宜 {'weight': 0.0}
炸鸡 炸鸡 {'weight': 0}
香 吃 {'weight': 0.0}
香 炸鸡 {'weight': 0.0}
香 价格 {'weight': 0.006024096385542169}
香 便宜 {'weight': 0.0}
香 香 {'weight': 0}
价格 吃 {'weight': 0.006024096385542169}
价格 炸鸡 {'weight': 0.0}
价格 香 {'weight': 0.006024096385542169}
价格 便宜 {'weight': 0.006024096385542169}
价格 价格 {'weight': 0}
便宜 吃 {'weight': 0.0}
便宜 炸鸡 {'weight': 0.0}
便宜 香 {'weight': 0.0}
便宜 价格 {'weight': 0.006024096385542169}
便宜 便宜 {'weight': 0}
这里全部为0.006024096385542169,并非是出错了,而是在数据集中所有的内容只出现过一次,导致了值都相同,如下图所示:
2.3 生成矩阵
矩阵生成就对应了上篇文章的所有公式推导过程。
2.3.1 生成PR向量
def create_pr_vec(dg):
"""
创建PR向量
:param dg: 有向图
:return pr_vec: PR+与PR-组合在一起的向量, ndarray
:return pr_plus: PR+的向量, 用于求0范数, ndarray
:return pr_minus: PR-的向量, 用于求0范数, ndarray
"""
pr_plus = []
pr_minus = []
nodes = dg.nodes()
for node in nodes:
pr_plus.append(dg.nodes[node]['pr_plus'])
pr_minus.append(dg.nodes[node]['pr_minus'])
temp_list = pr_plus + pr_minus # 合并两个列表
# 将list转为ndarray对象
pr_plus = np.array(pr_plus)
pr_minus = np.array(pr_minus)
pr_vec = np.array(temp_list)
return pr_vec, pr_plus, pr_minus
这里多传回了2个列表,pr_plus和pr_minus,即 P R + PR^+ PR+和 P R − PR^- PR−分别组成的列表,主要是方便后面计算 e e e。
pr_vec结果如下:
array([0. , 0. , 1.15138889, 0. , 1.03333333,
0. , 0. , 0. , 0. , 0. ])
2.3.2 生成e向量
这里考虑到向量 e ⃗ \vec e e的计算方法是: ∑ i = 1 n e i + = ∑ i = 1 n e i − = n \sum_{i=1}^n e_i^+ = \sum_{i=1}^n e_i^- = n ∑i=1nei+=∑i=1nei−=n,所以我在计算这个的时候,采用了归一化的方法处理:
def calc_e(sum_pr, pr, n):
"""
计算e的值
计算方法:
1. 获得PR+与PR-的向量
2. 判断向量的第0范数, 如果0范数为0, 则这n个e为0; 如果0范数不为0, 则归一化不为0的数,
再乘以n
归一化方法: e_i = \frac{pr_i}{\sum_{j=1}^n pr_j}
3. 将所获得的结果组成向量
:param sum_pr: 传入的PR向量的总和
:param pr: 传入的PR向量
:param n: PR值的个数
:return e: e向量
"""
if np.linalg.norm(pr, ord=0):
e = []
for i in pr:
e.append((i / sum_pr) * n)
e = np.array(e)
else:
e = np.zeros(n)
return e
def create_e_vec(pr_plus, pr_minus):
"""
创建e_i^+和e_i^-构成的向量
通过调用calc_e(sum_pr, pr)计算
e的要求: e(e+和e-组合的向量)的第一范数为2n
:param pr_plus: PR+构成的向量
:param pr_minus: PR-构成的向量
:return:
"""
n = pr_plus.shape[0] # 获取向量中元素的个数
sum_pr_plus = sum(pr_plus)
sum_pr_minus = sum(pr_minus)
e_plus = calc_e(sum_pr_plus, pr_plus, n)
e_minus = calc_e(sum_pr_minus, pr_minus, n)
e_vec = np.concatenate([e_plus, e_minus])
return e_vec
calc_e这个方法是我为了提高代码复用性加的。得到的结果如下:
array([0. , 0. , 2.63509218, 0. , 2.36490782,
0. , 0. , 0. , 0. , 0. ])
2.3.3 生成 f u T fu^T fuT矩阵
这个没啥好说的,就是矩阵乘法:
def create_fu_mat(e_vec):
"""
创建fu^T矩阵
计算方法:
f = e / m, m = 2n
u: 单位向量
:param e_vec: e+和e-构成的向量, 大小为2n×1
:return fu^T: 将e_vec复制了2n列向量构成的矩阵, 每一列的向量一模一样, 大小为2n×2n
"""
m = e_vec.shape[0]
f = (e_vec / m).reshape((m, 1))
u = np.ones((1, m))
return np.dot(f, u)
结果如下:
array([[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0.26350922, 0.26350922, 0.26350922, 0.26350922, 0.26350922,
0.26350922, 0.26350922, 0.26350922, 0.26350922, 0.26350922],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0.23649078, 0.23649078, 0.23649078, 0.23649078, 0.23649078,
0.23649078, 0.23649078, 0.23649078, 0.23649078, 0.23649078],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. ]])
2.3.4 生成邻接矩阵
def get_sum_weights(dg):
"""
获得邻接矩阵每一行的数据的和并乘2, i.e. 2 * \sum_{i=1}^n\sum_{j=1}^n w_{ij}
因为原论文中的公式是q_j^+ 加 q_j^-, 和为2
由于没有A+和A-之分, i.e. 没有q_j^+ 和q_j^- 之分
但是矩阵是合并了的, 为了保证矩阵第一范数为1, 所以需要乘2
:param dg: 有向图
:return sum_weights: dict, {node: sum_weight}, 每一行的总权重,
i.e. /sum_{k \in out(v_j)}|p_{jk}|
"""
sum_weights = {}
for node_1 in dg.nodes():
weight = 0
for node_2 in dg.nodes():
weight += dg.edges[node_1, node_2]['weight']
sum_weights[node_1] = weight * 2
return sum_weights
def create_adj_mat(dg):
"""
生成邻接矩阵
邻接矩阵权重计算方式: a_{ij} = w{ij} / w{j}
w{j}是j列的权重和, 但是由于邻接矩阵是上下对称的, 所以也是第j行的权重和
该过程实际上是一个归一化的过程
:param dg: 有向图
:return A_mat: 2n×2n大小的邻接矩阵, 由4个n×n的邻接矩阵拼接而成
"""
n = dg.number_of_nodes()
sum_weights = get_sum_weights(dg) # 获得每一行的权重总和
adj_list = []
for node_1 in dg.nodes():
sum_weight = sum_weights[node_1]
for node_2 in dg.nodes():
a = dg.edges[node_1, node_2]['weight'] / sum_weight
adj_list.append(a)
adj_mat = np.array(adj_list).reshape(n, n)
A_mat = np.vstack((adj_mat, adj_mat))
A_mat = np.hstack((A_mat, A_mat))
return A_mat.T
这个邻接矩阵是我觉得最复杂的部分,因为我没有用
w
+
,
w
−
w^+,w^-
w+,w−,我只有一个权重,所以我的矩阵
A
\bold A
A是如下构造:
A
=
[
A
A
A
A
]
\bold A=\left[ \begin{array}{ccc} \bold A & & \bold A \\ \bold A & & \bold A \end{array} \right]
A=[AAAA]
但是这样带来了一个问题,就是转置后的第一范数不为1,是大于1的,这是因为上面的 ∥ A ∥ 1 = 1 \left\| \bold A \right\|_1=1 ∥A∥1=1下面的也是等于1,再求和之后为2,那么就会导致最后的结果是发散的,而不是收敛的。那么问题我后来发现是出在这个公式上面:
a i j + = q i j + ∑ k = 1 n q k j + + ∑ k = 1 n q k j − a_{ij}^+=\frac{q_{ij}^+}{\sum_{k=1}^n q_{kj}^+ + \sum_{k=1}^n q_{kj}^-} aij+=∑k=1nqkj++∑k=1nqkj−qij+
我没有正负之分,所以分母就小了一倍,所以我用这行代码,把权重翻倍,使得最后能够保证 ∥ A ∥ 1 = 1 \left\| \bold A \right\|_1=1 ∥A∥1=1:
sum_weights[node_1] = weight * 2
接着第二个问题就是记得返回值的时候要转置,要转置,要转置!因为毕竟公式里面是 Q = P T Q=P^T Q=PT,我最开始搞成了这个邻接矩阵是对称的,可惜我后来才想起来不是对称的。
运行结果如下:
array([[0. , 0.5 , 0. , 0.16666667, 0. ,
0. , 0.5 , 0. , 0.16666667, 0. ],
[0.25 , 0. , 0. , 0. , 0. ,
0.25 , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0.16666667, 0. ,
0. , 0. , 0. , 0.16666667, 0. ],
[0.25 , 0. , 0.5 , 0. , 0.5 ,
0.25 , 0. , 0.5 , 0. , 0.5 ],
[0. , 0. , 0. , 0.16666667, 0. ,
0. , 0. , 0. , 0.16666667, 0. ],
[0. , 0.5 , 0. , 0.16666667, 0. ,
0. , 0.5 , 0. , 0.16666667, 0. ],
[0.25 , 0. , 0. , 0. , 0. ,
0.25 , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0.16666667, 0. ,
0. , 0. , 0. , 0.16666667, 0. ],
[0.25 , 0. , 0.5 , 0. , 0.5 ,
0.25 , 0. , 0.5 , 0. , 0.5 ],
[0. , 0. , 0. , 0.16666667, 0. ,
0. , 0. , 0. , 0.16666667, 0. ]])
2.4 PolarityRank
有了矩阵后,我们就可以开始计算了。
2.4.1 计算每个节点PR值
def calc_polarityrank(pr_vec, fu_mat, A_mat):
"""
polarityrank算法
计算方法:
pr_vec := B * pr_vec
B = ((1 - d)fu^T + dA)
f = e / m, u: 单位向量
使用论文中的计算方法, 可以减少一次矩阵与矩阵的乘法
收敛判断:
1. ||PR_{k+1} - PR_{k}|| <= 1e-10
2. 迭代200次
:param pr_vec: PR+和PR-构成的向量, 大小2n×1
:param fu_mat: 将e_vec复制了2n列向量构成的矩阵, 每一列的向量一模一样, 大小为2n×2n
:param A_mat: 由4个归一化权重构成的邻接矩阵拼接而成的大邻接矩阵, 用于矩阵运算, 大小为2n×2n
:return pr_vec: 迭代结束后的每个节点的PR值
"""
d = 0.85
epsilon = 1e-5
for i in range(200):
pr_new = (1 - d) * np.dot(fu_mat, pr_vec) + d * np.dot(A_mat, pr_vec)
print(i, np.linalg.norm(pr_new - pr_vec, ord=2))
if np.linalg.norm(pr_new - pr_vec, ord=2) <= epsilon:
pr_vec = pr_new
break
pr_vec = pr_new
print("运行结束!")
print("最终收敛的PR值:", pr_vec)
return pr_vec
由于之前证明过,在 n → ∞ n \rightarrow \infty n→∞时,最终会趋近于0,但是我们也没有那么多资源来跑到无穷对吧,所以我这里参考了Fernández-Gavilanes et al. 老哥们的方法,并自己设定了一个 ϵ = 1 0 − 5 \epsilon=10^{-5} ϵ=10−5,我的收敛条件给定的是:
- ∥ x ⃗ k + 1 − x ⃗ k ∥ ≤ ϵ \left\| \vec x_{k+1} - \vec x_k \right\| \le \epsilon ∥xk+1−xk∥≤ϵ;
- 迭代200次。
运行结果:
0 1.9422066348391442
1 1.372653777753172
2 0.999663462218954
...
110 1.0971141479105944e-05
111 1.0159208470470569e-05
112 9.387995084108366e-06
运行结束!
最终收敛的PR值: [3.32872517e-05 1.52967968e-05 3.31700068e-05 6.28000260e-05
3.17410413e-05 3.32872517e-05 1.52967968e-05 1.92333899e-05
6.28000260e-05 1.92333899e-05]
大家发现这个收敛的 P R PR PR值小的离谱对吧,但是当我们计算 S O SO SO之后大家会发现新大陆。
2.4.2 计算SO值
def calc_so(pr_vec, nodes):
"""
计算每个节点的情感极性
计算方法:
SO(n) = (PR+(n) - PR-(n)) / (PR+(n) + PR-(n))
:param pr_vec: 收敛后的PR值
:param nodes: 节点
"""
# so_dict = {}
n = len(nodes) # 记录节点数
i = 0
for node in nodes:
pr_plus = pr_vec[i]
pr_minus = pr_vec[i + n]
# so_dict[node] = (pr_plus - pr_minus) / (pr_plus + pr_minus)
print(node, " 的情感值为: ", (pr_plus - pr_minus) / (pr_plus + pr_minus))
i += 1
运行结果如下:
吃 的情感值为: 0.0
炸鸡 的情感值为: 0.0
香 的情感值为: 0.26594873331317725
价格 的情感值为: 0.0
便宜 的情感值为: 0.2453710838914052
最后得出一个结论,该你有情感,你就得有情感。
3 总结
本文配合上文《情感分析学习笔记(5)——PolarityRank算法原理讲解》,讲解了如何通过python代码实现PolarityRank算法,其总体思维导图如下:
4 参考
[1] Cruz F L , Vallejo C G , Enriquez F , et al. PolarityRank: Finding an equilibrium between followers and contraries in a network[J]. Information Processing & Management, 2012, 48(2):p.271-282.
[2] Fernandez-Gavilanes M , Alvarez-Lopez T , Juncal-Martinez J , et al. Unsupervised method for sentiment analysis in online texts[J]. Expert Systems with Applications, 2016, 58(Oct.):57-75.
[3]羽毛的笔.Stanford CoreNLP 入门指南[EB/OL].https://zhuanlan.zhihu.com/p/137226095,2020-5-5.
[4]闰土不用叉.利用nltk可视化stanford coreNLP构建的中文句法树[EB/OL].https://blog.csdn.net/xyz1584172808/article/details/81951846,2018-8-22.
[5]云水禅心_心一.Python中读取txt文本出现“ ‘gbk’ codec can’t decode byte 0xbf in position 2: illegal multibyte sequence”的解决办法[EB/OL].https://blog.csdn.net/lqzdreamer/article/details/76549256,2017-8-1.
[6]zy4321234zx.StanfordCoreNLP 运行缓慢(python)[EB/OL].https://blog.csdn.net/zy4321234zx/article/details/88913771,2019-3-30.
[7]张士超你到底把我家代码藏哪了.Stanford Parser 标签说明[EB/OL].https://blog.csdn.net/u011847043/article/details/79595225,2018-3-1.