jjzjj

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

51Ann 2023-03-28 原文

前馈知识

之前在word embedding里浅浅的说了一下one-hot是怎么向词向量表示发展的,大家可以回顾一下。接下来我补充一下,接说二者之间还有一个阶段,词的分布式表示。


词的分布式表示

理论

分布式表示的发展

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

You shall know a word by the company it keeps.

就是说我们人类可以通过上下文的含义来理解某一单词含义。

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

  • 树上有一只杜鹃在叫。
  • 漫山遍野开满了杜鹃。

所以John Rupert Firth提出我们可以使用词的上下文分布进行词的表示

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

我喜欢你。我爱你。

前后都是。那机器就可以知道喜欢之间肯定是有点关系的。

那你可以杠我一句:“那如果遇到 我恨你。 呢?”

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

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

  • 在一个句子中选择一个固定大小的窗口作为其上下文。
  • 使用整个句子作为上下文。
  • 使用整个文档作为上下文。
不同的选择方式会获得不同的表示,比如前两个可以获得词的局部性质,而最后一个方法获得的词表示更倾向于代表主题信息。

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

  • 使用独热码,意思相近词的词也是完全不相干的表示,无法计算余弦相似度。
  • 使用分布式表示之后,因为上下文的缘故“喜欢”和“爱”可以获得相近的表示,之后可以通过余弦相似度计算词汇之间的距离。

举个例子

用书上一个例子讲一下如何使用上下文表示信息。

  • 我 喜欢 自然 语言 处理。
  • 我 爱 深度 学习。
  • 我 喜欢 机器 学习。
在这个例子里边,我们用一个句子作为上下文。

解析一下

  • 在第一个句子里上下文词汇有喜欢自然语言处理
  • 在第二个句子里上下文词汇有深度学习
  • 在第三个句子里上下文词汇有喜欢机器学习
搞成集合,然后看一下每一个词和其他词汇在同一个句子出现的概率,就可以得到下表。下表是个对角线对称矩阵,我们可以认为每一行或者每一列是一个词的表示。

$$ \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} $$

但是这样还存在一个问题,就是有些词天然会和其他词一起出现的频率很高。比如“我”、“你”这类词,而实际上他们对词汇的含义表示影响并不大,但是通过共同出现的次数这么表示,会导致不相干的词之间相似度提高。

举个计算的例子,比如我饿我可以

饿可以 没什么关系,但是因为的关系二者获得了同样的表示。这个例子中一个句子只有俩词,比较极端,加长句子之后,也会因为“我”这种词的天然特性而影响到其他词汇的表示。

$$ \begin{array}{cccc} \hline & \text { 我 } & \text { 饿 } & \text { 可以 }\ \hline \text { 我 } & 0 & 1 & 1 \ \text { 饿 } & 1 & 0 & 0 \ \text { 可以 } & 1 & 0 & 0 \ \hline \end{array} $$

要解决这个问题可以使用点互信息

点互信息

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

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

这样操作可以实现:如果一个词和很多其他词汇共同出现,那就降低它的权重,反之提高它的权重。

$$ \begin{aligned} &P(A, B) =\frac{A和B一起出现的次数}{矩阵中所有元素的数量} \\ &P(A) =\frac{A出现的次数}{矩阵中所有的元素数量} \\ &P(B) =\frac{B出现的次数}{矩阵中所有的元素数量} \end{aligned} $$

为了计算看图方便,把这个表格搬下来了。以喜欢为例,算一下。

  • 喜欢 共同出现 = 2

  • 出现次数 = [我我] + [我喜欢] + [我自然] + ... + [我。] = 13

    • 其实就是所在行向量(或列向量)的和。
  • 喜欢出现次数 = [我喜欢] + [喜欢喜欢] + ... + [喜欢。] = 9

    • 其实就是所在行向量(或列向量)的和。
  • 所有元素数量 = 行向量和(或列向量和)再求和 = 69

$$ PMI(我,喜欢) = \log_2{\left(\frac{\frac{2}{69}}{\frac{13}{69} \times \frac{9}{69}}\right)} $$

所以简单来说某一元素的PMI可以用以下公式计算:

$$ \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)的形式

$$ \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. 代码 公式最后是 $\operatorname{PMI}(A, B) =\log_2{\left(AB共同出现的次数\times \frac{所有元素数量}{A出现的次数\times B出现次数 }\right)}$。

    而实际上我们在expected = np.outer(row_totals, col_totals) / total这一步中得到的是$\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)=-\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前后的结果

左边是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)解决词分布式表示稀疏性问题的更多相关文章

  1. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 分布式事务和队列,ruby,erlang,scala - 2

    我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和

  4. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  5. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  6. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  7. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

  8. ruby - what is - gets is a directory - 错误信息 - 2

    我遇到了这个奇怪的错误.../Users/gideon/Documents/ca_ruby/rubytactoe/lib/player.rb:13:in`gets':Isadirectory-spec(Errno::EISDIR)player_spec.rb:require_relative'../spec_helper'#theuniverseisvastandinfinite...itcontainsagame....butnoplayersdescribe"tictactoegame"docontext"theplayerclass"doit"musthaveahumanplay

  9. ruby - 如何更快地解决 project euler #21? - 2

    原始问题Letd(n)bedefinedasthesumofproperdivisorsofn(numberslessthannwhichdivideevenlyinton).Ifd(a)=bandd(b)=a,whereab,thenaandbareanamicablepairandeachofaandbarecalledamicablenumbers.Forexample,theproperdivisorsof220are1,2,4,5,10,11,20,22,44,55and110;therefored(220)=284.Theproperdivisorsof284are1,2,

  10. ruby - 尝试比较两个文本文件,并根据信息创建第三个 - 2

    我有两个文本文件,master.txt和926.txt。如果926.txt中有一行不在master.txt中,我想写入一个新文件notinbook.txt。我写了我能想到的最好的东西,但考虑到我是一个糟糕的/新手程序员,它失败了。这是我的东西g=File.new("notinbook.txt","w")File.open("926.txt","r")do|f|while(line=f.gets)x=line.chompifFile.open("master.txt","w")do|h|endwhile(line=h.gets)ifline.chomp!=xputslineendende

随机推荐