一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题相关的知识,希望对你有一定的参考价值。
前馈知识
之前在word embedding里浅浅的说了一下one-hot是怎么向词向量表示发展的,大家可以回顾一下。接下来我补充一下,接说二者之间还有一个阶段,词的分布式表示。
词的分布式表示
理论
分布式表示的发展
英国语言学家John Rupert Firth 在1957 年的《A synopsis of linguistic theory》中提到
就是说我们人类可以通过上下文的含义来理解某一单词含义。
比如下边两个句子,人类看完之后就能直接知道两个杜鹃指的是哪个。
- 树上有一只杜鹃在叫。
- 漫山遍野开满了杜鹃。
所以John Rupert Firth提出我们可以使用词的上下文分布进行词的表示。
怎么才能用到上下文信息?
我喜欢你。 和 我爱你。
前后都是我,你。那机器就可以知道喜欢和爱之间肯定是有点关系的。
那你可以杠我一句:“那如果遇到 我恨你。 呢?”
如果只看这三个短句子肯定机器是分不出这些词的情感极性的,这就涉及到NLP的其他任务上边去了。
对于上下文,还有不同的选择方式。比如:
- 在一个句子中选择一个固定大小的窗口作为其上下文。
- 使用整个句子作为上下文。
- 使用整个文档作为上下文。
不同的选择方式会获得不同的表示,比如前两个可以获得词的局部性质,而最后一个方法获得的词表示更倾向于代表主题信息。
这样之后分布式表示相对于独热码的好处在于:
- 使用独热码,意思相近词的词也是完全不相干的表示,无法计算余弦相似度。
- 使用分布式表示之后,因为上下文的缘故“喜欢”和“爱”可以获得相近的表示,之后可以通过余弦相似度计算词汇之间的距离。
举个例子
用书上一个例子讲一下如何使用上下文表示信息。
- 我 喜欢 自然 语言 处理。
- 我 爱 深度 学习。
- 我 喜欢 机器 学习。
在这个例子里边,我们用一个句子作为上下文。
解析一下我:
- 在第一个句子里上下文词汇有喜欢,自然,语言,处理
- 在第二个句子里上下文词汇有爱,深度,学习
- 在第三个句子里上下文词汇有喜欢,机器,学习
搞成集合,然后看一下每一个词和其他词汇在同一个句子出现的概率,就可以得到下表。下表是个对角线对称矩阵,我们可以认为每一行或者每一列是一个词的表示。
$$ \\beginarrayccccccccccc \\hline & \\text 我 & \\text 喜欢 & \\text 自然 & \\text 语言 & \\text 处理 & \\text 爱 & \\text 深度 & \\text 学习 & \\text 机器 & \\circ \\ \\hline \\text 我 & 0 & 2 & 1 & 1 & 1 & 1 & 1 & 2 & 1 & 3 \\ \\text 喜欢 & 2 & 0 & 1 & 1 & 1 & 0 & 0 & 1 & 1 & 2 \\ \\text 自然 & 1 & 1 & 0 & 1 & 1 & 0 & 0 & 0 & 0 & 1 \\ \\text 语言 & 1 & 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 1 \\ \\text 处理 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 1 \\ \\text 爱 & 1 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 0 & 1 \\ \\text 深度 & 1 & 0 & 0 & 0 & 0 & 1 & 0 & 1 & 0 & 1 \\ \\text 学习 & 2 & 1 & 0 & 0 & 0 & 1 & 1 & 0 & 1 & 1 \\ \\text 机器 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 \\ \\text 。 & 3 & 2 & 1 & 1 & 1 & 1 & 1 & 2 & 1 & 0 \\ \\hline \\endarray $$
但是这样还存在一个问题,就是有些词天然会和其他词一起出现的频率很高。比如“我”、“你”这类词,而实际上他们对词汇的含义表示影响并不大,但是通过共同出现的次数这么表示,会导致不相干的词之间相似度提高。
举个计算的例子,比如我饿,我可以。
饿 和 可以 没什么关系,但是因为我的关系二者获得了同样的表示。这个例子中一个句子只有俩词,比较极端,加长句子之后,也会因为“我”这种词的天然特性而影响到其他词汇的表示。
$$ \\beginarraycccc \\hline & \\text 我 & \\text 饿 & \\text 可以 \\ \\hline \\text 我 & 0 & 1 & 1 \\ \\text 饿 & 1 & 0 & 0 \\ \\text 可以 & 1 & 0 & 0 \\ \\hline \\endarray $$
要解决这个问题可以使用点互信息。
点互信息
$$ \\operatornamePMI(A, B)=\\log _2 \\fracP(A, B)P(A) P(B) $$
这个公式是将AB两个词共同出现的频率除以A出现的频率和B出现的频率。
这样操作可以实现:如果一个词和很多其他词汇共同出现,那就降低它的权重,反之提高它的权重。
$$ \\beginaligned &P(A, B) =\\fracA和B一起出现的次数矩阵中所有元素的数量 \\\\ &P(A) =\\fracA出现的次数矩阵中所有的元素数量 \\\\ &P(B) =\\fracB出现的次数矩阵中所有的元素数量 \\endaligned $$
为了计算看图方便,把这个表格搬下来了。以我和喜欢为例,算一下。
-
我和 喜欢 共同出现 = 2
-
我出现次数 = [我我] + [我喜欢] + [我自然] + ... + [我。] = 13
- 其实就是所在行向量(或列向量)的和。
-
喜欢出现次数 = [我喜欢] + [喜欢喜欢] + ... + [喜欢。] = 9
- 其实就是所在行向量(或列向量)的和。
-
所有元素数量 = 行向量和(或列向量和)再求和 = 69
$$ PMI(我,喜欢) = \\log_2\\left(\\frac\\frac269\\frac1369 \\times \\frac969\\right) $$
所以简单来说某一元素的PMI可以用以下公式计算:
$$ \\beginaligned \\operatornamePMI(A, B) &=\\log_2\\left(\\frac\\fracAB共同出现的次数所有元素数量\\fracA出现的次数所有元素数量 \\times \\fracB出现次数所有元素数量\\right) \\\\ & =\\log_2\\left(AB共同出现的次数\\times \\frac所有元素数量A出现的次数\\times B出现次数 \\right) \\endaligned $$
当某个词与上下文之间共现次数较低时,可能会得到负的PMI值。考虑到这种情况下的PMI不太稳定(具有较大的方差),在实际应用中通常采用PPMI (Positive PMI)的形式:
$$ \\operatornamePPMI=\\max (\\operatornamePMI, 0) $$
代码实现
用代码实现一下。使用矩阵计算的话我们就不用挨个元素这么算了。直接使用矩阵并行计算即可。代码如下:
代码一
代码一是用numpy写的。代码二是用pytorch写的,除了框架不一样别的都完全一样,按需选择。
import numpy as np
M = np.array([[0, 2, 1, 1, 1, 1, 1, 2, 1, 3],
[2, 0, 1, 1, 1, 0, 0, 1, 1, 2],
[1, 1, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1, 0, 1],
[2, 1, 0, 0, 0, 1, 1, 0, 1, 1],
[1, 1, 0, 0, 0, 0, 0, 1, 0, 1],
[3, 2, 1, 1, 1, 1, 1, 2, 1, 0]])
np.set_printoptions(3)
def pmi(M, positive=True):
# 计算出每个词出现的次数,得到一个向量,每个值都是一个词出现的次数
single = M.sum(axis=0)
# 计算元素出现的总次数
total = single.sum()
# 这样计算得到的是 A次数*B次数/总次数
expected = np.outer(single,single) / total
# 这一步看代码后边的解析
M = M / expected
# 计算log2
with np.errstate(divide=ignore):
M = np.log(M)
# 将M中的负无穷设置为0
M[np.isinf(M)] = 0.0
#PPMI 将M中的负数设置为0
if positive:
M[M < 0] = 0.0
return M
M_pmi = pmi(M)
print(M_pmi)
补充解析:
-
代码 公式最后是 $\\operatornamePMI(A, B) =\\log_2\\left(AB共同出现的次数\\times \\frac所有元素数量A出现的次数\\times B出现次数 \\right)$。
而实际上我们在
expected = np.outer(row_totals, col_totals) / total
这一步中得到的是$\\fracA出现的次数\\times B出现次数所有元素数量$ 。小学知识 除以一个分数等于乘以它的倒数,所以这一步是
M = M / expected
。也是这两行代码借助矩阵实现并行计算,不用for循环挨个元素算。
-
np.outer
是计算两个向量的外积。给你两个向量
a = [a0, a1, ..., aM]
和b = [b0, b1, ..., bN]
内积计算是一个数,等于
a0*b0 + a1*b1 + ... + aN*bN
外积是一个矩阵:
[[a0*b0 a0*b1 ... a0*bN ]
[a1*b0 ...
[ ...
[aM*b0 .......... aM*bN ]]
比如
vec = np.array([1,2,3]) inn = np.vdot(vec,vec) out = np.outer(vec,vec) print(vec = , vec) print(内积 = ,inn) print(外积 = ,out)
结果是:
-
with np.errstate(divide=ignore)
因为我们的矩阵中有0,因为$\\log(0)=-\\infty$,所以计算log的时候会有一个警告
divide by zero encountered in log
。 这里用with np.errstate(divide=ignore)
包裹住M = np.log(M)
就是让他忽略这一步操作的警告。
代码二
补一个pytorch 版本的代码。
和上边没啥区别,就是np
改torch
即可。主要区别在于做log计算那里。
pytorch中不会有这个log(0)的警告,pytorch 中也没有errstate方法。
import torch
M = torch.Tensor([[0, 2, 1, 1, 1, 1, 1, 2, 1, 3],
[2, 0, 1, 1, 1, 0, 0, 1, 1, 2],
[1, 1, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1, 0, 1],
[2, 1, 0, 0, 0, 1, 1, 0, 1, 1],
[1, 1, 0, 0, 0, 0, 0, 1, 0, 1],
[3, 2, 1, 1, 1, 1, 1, 2, 1, 0]])
torch.set_printoptions(3)
def pmi(M, positive=True):
single = M.sum(axis=0)
total = single.sum()
expected = torch.outer(single, single) / total
M = M / expected
# pytorch中不会有这个log(0)的警告,pytorch 中也没有errstate方法
M = torch.log(M)
M[torch.isinf(M)] = 0.0
if positive:
M[M < 0] = 0.0
return M
M_pmi = pmi(M)
print(M_pmi)
代码三
这段代码是书上写的,我觉得写的让人比较困惑,不多做解释,看看能看懂的。
import numpy as np
M = np.array([[0, 2, 1, 1, 1, 1, 1, 2, 1, 3],
[2, 0, 1, 1, 1, 0, 0, 1, 1, 2],
[1, 1, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1, 0, 1],
[2, 1, 0, 0, 0, 1, 1, 0, 1, 1],
[1, 1, 0, 0, 0, 0, 0, 1, 0, 1],
[3, 2, 1, 1, 1, 1, 1, 2, 1, 0]])
np.set_printoptions(3)
def pmi(M, positive=True):
# 因为是对称矩阵,其实这俩的值完全是一样的。
col_totals = M.sum(axis=0)
row_totals = M.sum(axis=1)
# 计算元素出现的总次数
total = col_totals.sum()
# 这样计算得到的是 A次数*B次数/总次数
expected = np.outer(row_totals, col_totals) / total
# 实现并行计算,不用for挨个元素算了
M = M / expected
# 计算log2
with np.errstate(divide=ignore):
M = np.log(M)
M[np.isinf(M)] = 0.0
if positive:
M[M < 0] = 0.0
return M
M_pmi = pmi(M)
print(M_pmi)
看看使用PPMI前后的结果
左边是M,右边是M_pmi。
用个例子计算一下相似度:
可以看到在PPMI之前 语言 和 机器 的相似度为0.671,PPMI之后变为0.207。
使用PPMI明显降低了不相干词汇的相似度。
代码
就是在上边代码一的后边加上下边这块代码即可:
def cos(a,b):
f1 = np.vdot(a,b)
f2 = np.vdot(a,a)**(1/2)
f3 = np.vdot(b,b)**(1/2)
return f1/(f2*f3)
print(\\nPPMI前:)
print(语言 = , M[3])
print(机器 = , M[8])
print(余弦相似度 = , cos(M[3], M[8]))
print(\\nPPMI后:)
print(语言 = , M_pmi[3])
print(机器 = , M_pmi[8])
print(余弦相似度 = , cos(M_pmi[3], M_pmi[8]))
以上是关于一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题的主要内容,如果未能解决你的问题,请参考以下文章
逐点互信息PMI(Pointwise mutual information)
逐点互信息PMI(Pointwise mutual information)5发