我正在参加「启航计划」


前馈知识

之前在浅谈word embedding里浅浅的说了一下one-hot是怎样向词向量表明开展的,咱们能够回顾一下。接下来我补充一下,接说二者之间还有一个阶段,词的分布式表明。


词的分布式表明

理论

分布式表明的开展

英国言语学家John Rupert Firth 在1957 年的《A synopsis of linguistic theory》中说到

You shall know a word by the company it keeps.

便是说咱们人类能够经过上下文的意义来了解某一单词意义。

比方下边两个语句,人类看完之后就能直接知道两个杜鹃指的是哪个。

  • 树上有一只杜鹃在叫。
  • 雨后春笋开满了杜鹃。

一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题

所以John Rupert Firth提出咱们能够运用词的上下文分布进行词的表明

怎样才能用到上下文信息?

我喜爱你。我爱你。

前后都是。那机器就能够知道喜爱之间必定是有点联系的。

那你能够杠我一句:“那假如遇到 我恨你。 呢?”

假如只看这三个短语句必定机器是分不出这些词的情感极性的,这就涉及到NLP的其他任务上边去了。

对于上下文,还有不同的挑选方式。比方:

  • 在一个语句中挑选一个固定巨细的窗口作为其上下文。
  • 运用整个语句作为上下文。
  • 运用整个文档作为上下文。

不同的挑选方式会取得不同的表明,比方前两个能够取得词的部分性质,而最后一个方法取得的词表明更倾向于代表主题信息。

这样之后分布式表明相对于独热码的好处在于:

  • 运用独热码,意思附近词的词也是彻底不相干的表明,无法核算余弦类似度。
  • 运用分布式表明之后,由于上下文的缘故“喜爱”和“爱”能够取得附近的表明,之后能够经过余弦类似度核算词汇之间的距离。

举个比方

用书上一个比方讲一下怎么运用上下文表明信息。

  • 我 喜爱 天然 言语 处理。
  • 我 爱 深度 学习。
  • 我 喜爱 机器 学习。

在这个比方里面,咱们用一个语句作为上下文。

解析一下

  • 在第一个语句里上下文词汇有喜爱天然言语处理
  • 在第二个语句里上下文词汇有深度学习
  • 在第三个语句里上下文词汇有喜爱机器学习

搞成调集,然后看一下每一个词和其他词汇在同一个语句呈现的概率,就能够得到下表。下表是个对角线对称矩阵,咱们能够以为每一行或者每一列是一个词的表明。

我喜爱天然言语处理爱深度学习机器∘我0211111213喜爱2011100112天然1101100001言语1110100001处理1111000001爱1000001101深度1000010101学习2100011011机器1100000101。3211111210\begin{array}{ccccccccccc} \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 \end{array}

可是这样还存在一个问题,便是有些词天然会和其他词一同呈现的频率很高。比方“我”、“你”这类词,而实际上他们对词汇的意义表明影响并不大,可是经过一同呈现的次数这么表明,会导致不相干的词之间类似度进步。

举个核算的比方,比方我饿我能够

饿能够 没什么联系,可是由于的联系二者取得了同样的表明。这个比方中一个语句只要俩词,比较极端,加长语句之后,也会由于“我”这种词的天然特性而影响到其他词汇的表明。

我饿能够我011饿100能够100\begin{array}{cccc} \hline & \text { 我 } & \text { 饿 } & \text { 能够 }\\ \hline \text { 我 } & 0 & 1 & 1 \\ \text { 饿 } & 1 & 0 & 0 \\ \text { 能够 } & 1 & 0 & 0 \\ \hline \end{array}

要解决这个问题能够运用点互信息

点互信息

PMI⁡(A,B)=log⁡2P(A,B)P(A)P(B)\operatorname{PMI}(A, B)=\log _2 \frac{P(A, B)}{P(A) P(B)}

这个公式是将AB两个词一同呈现的频率除以A呈现的频率和B呈现的频率。

这样操作能够完成:假如一个词和很多其他词汇一同呈现,那就降低它的权重,反之进步它的权重。

P(A,B)=A和B一同呈现的次数矩阵中一切元素的数量P(A)=A呈现的次数矩阵中一切的元素数量P(B)=B呈现的次数矩阵中一切的元素数量\begin{aligned} &P(A, B) =\frac{A和B一同呈现的次数}{矩阵中一切元素的数量} \\\\ &P(A) =\frac{A呈现的次数}{矩阵中一切的元素数量} \\\\ &P(B) =\frac{B呈现的次数}{矩阵中一切的元素数量} \end{aligned}

为了核算看图便利,把这个表格搬下来了。以喜爱为例,算一下。

一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题

  • 喜爱 一同呈现 = 2

  • 呈现次数 = [我我] + [我喜爱] + [我天然] + … + [我。] = 13

    • 其实便是地点行向量(或列向量)的和。
  • 喜爱呈现次数 = [我喜爱] + [喜爱喜爱] + … + [喜爱。] = 9

    • 其实便是地点行向量(或列向量)的和。
  • 一切元素数量 = 行向量和(或列向量和)再求和 = 69

PMI(我,喜爱)=log⁡2(2691369969)PMI(我,喜爱) = \log_2{\left(\frac{\frac{2}{69}}{\frac{13}{69} \times \frac{9}{69}}\right)}

所以简单来说某一元素的PMI能够用以下公式核算:

PMI⁡(A,B)=log⁡2(AB一同呈现的次数一切元素数量A呈现的次数一切元素数量B呈现次数一切元素数量)=log⁡2(AB一同呈现的次数一切元素数量A呈现的次数B呈现次数)\begin{aligned} \operatorname{PMI}(A, B) &=\log_2{\left(\frac{\frac{AB一同呈现的次数}{一切元素数量}}{\frac{A呈现的次数}{一切元素数量} \times \frac{B呈现次数}{一切元素数量}}\right)} \\\\ & =\log_2{\left(AB一同呈现的次数\times \frac{一切元素数量}{A呈现的次数\times B呈现次数 }\right)} \end{aligned}

当某个词与上下文之间共现次数较低时,可能会得到负的PMI值。考虑到这种情况下的PMI不太稳定(具有较大的方差),在实际运用中一般选用PPMI (Positive PMI)的形式

PPMI⁡=max⁡(PMI⁡,0)\operatorname{PPMI}=\max (\operatorname{PMI}, 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)

补充解析:

  1. 代码 公式最后是 PMI⁡(A,B)=log⁡2(AB一同呈现的次数一切元素数量A呈现的次数B呈现次数)\operatorname{PMI}(A, B) =\log_2{\left(AB一同呈现的次数\times \frac{一切元素数量}{A呈现的次数\times B呈现次数 }\right)}

    而实际上咱们在expected = np.outer(row_totals, col_totals) / total这一步中得到的是A呈现的次数B呈现次数一切元素数量\frac{A呈现的次数\times B呈现次数}{一切元素数量}

    小学知识 除以一个分数等于乘以它的倒数,所以这一步是M = M / expected

    也是这两行代码借助矩阵完成并行核算,不必for循环挨个元素算。

  2. 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)
    

    成果是:

    vec = [1 2 3]

    内积 = 14

    外积 = [[1 2 3]

    \quad\quad\quad[2 4 6]

    \quad\quad\quad[3 6 9]]

  3. with np.errstate(divide='ignore')

    由于咱们的矩阵中有0,由于log⁡(0)=−∞\log(0)=-\infty,所以核算log的时分会有一个警告divide by zero encountered in log。 这儿用with np.errstate(divide='ignore')包裹住M = np.log(M)便是让他疏忽这一步操作的警告。

代码二

补一个pytorch 版本的代码。

和上边没啥区别,便是nptorch即可。首要区别在于做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前后的成果

一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题

左面是M,右边是M_pmi。

用个比方核算一下类似度:

能够看到在PPMI之前 言语机器 的类似度为0.671,PPMI之后变为0.207。

运用PPMI明显降低了不相干词汇的类似度。

一文拿捏点互信息(PMI)解决词分布式表示稀疏性问题

代码

便是在上边代码一的后边加上下边这块代码即可:

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]))