Word2Vec 的根本目标是为词汇表中的每个单词学习一个稠密的向量表示(即词向量) ,使得在语义或语法上相似的单词,其向量在空间中的距离也更近。
为了学习这些向量,模型基于一个简单的语言学假设:一个单词的含义可以由它周围频繁出现的单词(上下文)来定义 。
代码实现:Learning/Skip_Gram.py
at master · Striver98/Learning
数学原理
http://blog.csdn.net/itplus/article/details/37969519
https://spaces.ac.cn/usr/uploads/2017/04/146269300.pdf
简单来说,Word2Vec 就是 “两个训练方案+两个提速手段”,所以严格来讲,它有四个备选的模型。
两个训练方案分别是CBOW 和Skip-Gram ,如图所示
用通俗的语言来说,就是 “周围词叠加起来预测当前词”(P (w t |C o n t e x t )) 和 “当前词分别来预测周围词”(P (w o t h e r s |w t )) ,也就是条件概率建模问题了;
两个提速手段,分别是层次 Softmax 和负样本采样。层次 Softmax 是对 Softmax 的简化,直接将预测概率的效率从O (|V |) 降为O (l o g 2 |V |) ,但相对来说,精度会比原生的 Softmax 略差;负样本采样 则采用了相反的思路,它把原来的输入和输出联合起来当作输入,然后做一个二分类来打分,这样子我们可以看成是联合概率P (w t , C o n t e x t ) 和P (w t , w o t h e r s ) 的建模了,正样本就用语料出现过的,负样本就随机抽若干。
本文所使用的模型是 “Skip-Gram +
层次 Softmax” 的组合
层次 Softmax 核心思想:将复杂的多分类问题转化为一系列二分类问题,用一棵二叉树(通常是哈夫曼树)来组织词汇表,从而将计算复杂度从 O(V) 降低到 O(log₂V) 。
构建哈夫曼树 :根据词频构建一棵哈夫曼二叉树。词汇表中的每个单词都是树的一个叶子节点 。词频越高的单词,其路径越短。
取消输出向量 :模型不再需要为每个单词维护一个输出向量(即巨大的
W N × V ′
矩阵)。取而代之的是,为树上的每个非叶子节点(即内部节点)学习一个 N 维的向量 。内部节点的数量大约是 V-1 个,所以参数量几乎没有增加。
预测路径 :对于给定的中心词和上下文,模型的目标不再是直接计算每个单词的概率,而是从根节点到目标单词叶子节点所经过的路径 。路径上的每一步都是一个二分类问题(通常用逻辑回归 /Sigmoid 函数判断是向左走还是向右走)。
例如,要计算单词 w
的概率,就从根节点开始,在每一个内部节点,用该节点的向量和隐藏层向量计算一个概率,判断下一步的方向。最终,w
的概率就是路径上所有判断概率的连乘。
提取关键词
关键词的定义
关键词也好,摘要也好,我们希望能够尽可能快地获取文章的大意,如果一篇文章的关键词是 “深度学习”,我们就会知道,这篇文章不可能大谈特谈 “钢铁是怎么练成的”,也就是说,我们可以由关键词来猜到文本的大意,用数学来表示,那就是条件概率
p (s |w i )
这里的s 代表着一段文本,w i 是文本中的某个词,如果w i 是文本的关键词,那么应该使得上述概率最大。也就是说,我们只需要对句子中所有的词,算一遍上述概率,然后降序排列,就可以提取关键词了。说白了,关键词就是最能让我们猜到原文的词语 。
怎么估算这个概率?简单使用朴素贝叶斯假设就好,如果s 由 n 个词w 1 ,w 2 ,…,w n 组成,那么
这样,我们只需要估算词与词之间的转移概率p (w k |w i ) ,就可以得到条件概率p (s |w i ) 了,从而完成关键词的提取。
Word2Vec 的 Skip-Gram 模型就擅长于对p (w k |w i ) 的建模
Word2Vec 算概率
1 2 3 4 5 6 7 8 9 10 11 import numpy as npimport gensim model = gensim.models.word2vec.Word2Vec.load('word2vec_wx' )def predict_proba (oword, iword ): iword_vec = model[iword] oword = model.wv.vocab[oword] oword_l = model.syn1[oword.point].T dot = np.dot(iword_vec, oword_l) lprob = -sum (np.logaddexp(0 , -dot) + oword.code*dot) return lprob
我来逐行详细解释这段使用 Word2Vec Skip-Gram +
层次 Softmax 计算概率的代码:
1 2 3 import numpy as npimport gensim model = gensim.models.word2vec.Word2Vec.load('word2vec_wx' )
第 1-3 行:导入和模型加载 -
导入必要的库:numpy 用于数值计算,gensim 用于加载预训练的 Word2Vec 模型 -
加载名为’word2vec_wx’ 的预训练 Word2Vec 模型
1 def predict_proba (oword, iword ):
第 4 行:函数定义 -
定义函数predict_proba,接受两个参数: -
oword:输出词(目标上下文词) -
iword:输入词(中心词) - 函数目标是计算 P(oword |
iword),即给定中心词 iword 时,上下文词是 oword 的概率
1 iword_vec = model[iword]
第 5 行:获取中心词向量 -
model[iword]:获取中心词 iword 的词向量表示 -
在 Skip-Gram 中,这是输入权重矩阵中的向量,代表中心词的编码
1 oword = model.wv.vocab[oword]
第 6 行:获取输出词的词汇表信息 -
model.wv.vocab[oword]:获取输出词 oword 在词汇表中的详细信息
- 这个对象包含层次 Softmax 所需的 Huffman 编码信息: -
oword.point:从根节点到该词叶子节点的路径(节点索引序列) -
oword.code:路径上每一步的二进制编码(0= 左,1= 右)
1 oword_l = model.syn1[oword.point].T
第 7 行:获取路径上的节点向量 -
model.syn1:层次 Softmax 中所有非叶子节点的向量矩阵 -
oword.point:目标词在 Huffman 树中的路径节点索引 -
model.syn1[oword.point]:获取路径上所有非叶子节点的向量 -
.T:转置矩阵,使向量维度匹配后续的点积运算
1 dot = np.dot(iword_vec, oword_l)
第 8 行:计算点积 -
np.dot(iword_vec, oword_l):计算中心词向量与路径上每个非叶子节点向量的点积
- 结果是一个向量,包含路径上每一步的二分类得分
1 lprob = -sum (np.logaddexp(0 , -dot) + oword.code*dot)
第 9 行:计算对数概率
这是最复杂但最关键的一行,让我分解说明:
层次 Softmax 的概率原理:
在 Huffman 树的每个节点,使用逻辑回归进行二分类: -
向左走(编码 0)的概率:σ(θᵣᵀv_c) = 1/(1+exp(-θᵣᵀv_c)) -
向右走(编码 1)的概率:1 - σ(θᵣᵀv_c) = σ(-θᵣᵀv_c)
概率计算分解: 1.
np.logaddexp(0, -dot):计算 log(1 + exp(-dot)) -
这等价于计算-log(σ(dot)),因为: -log(σ(x)) = -log(1/(1+exp(-x))) =
log(1+exp(-x))
oword.code*dot:编码与点积的乘积
当编码为 0 时:这项为 0
当编码为 1 时:这项为 dot
组合后的表达式:
对于路径上的每个节点 r:
如果编码为 0:贡献为 -log(σ(dot_r))
如果编码为 1:贡献为 -log(σ(-dot_r)) = dot_r - log(1+exp(dot_r))
sum(...):对路径上所有节点的对数概率求和
因为总概率是路径上各节点概率的乘积,取对数后变为求和
-sum(...):取负号得到最终的对数似然
原始计算得到的是负对数概率,取负号得到正常的对数概率
数学等价性证明: - 当 code=0 时:-log(σ(dot)) =
log(1+exp(-dot)) - 当 code=1 时:-log(σ(-dot)) = dot + log(1+exp(-dot)) -
统一公式:log(1+exp(-dot)) + code*dot
第 10 行:返回结果 - 返回计算得到的对数概率 P(oword |
iword)
完整示例说明
假设: - 中心词 iword = “猫” - 目标词 oword =
“抓”(在 Huffman 树中路径长度为 3) - oword.code = [0, 1,
0](路径编码:左-右-左) - 点积结果 dot = [2.1, -1.3, 0.8]
计算过程:
1 2 3 4 5 6 节点1 (编码0 ):log (1 +exp (-2.1 )) + 0 *2.1 = log (1 +exp (-2.1 )) 节点2 (编码1 ):log (1 +exp (1.3 )) + 1 *(-1.3 ) = log (1 +exp (1.3 )) - 1.3 节点3 (编码0 ):log (1 +exp (-0.8 )) + 0 *0.8 = log (1 +exp (-0.8 )) 总和 = [log (1 +exp (-2.1 ))] + [log (1 +exp (1.3 )) - 1.3 ] + [log (1 +exp (-0.8 ))] lprob = -总和
实践
1 2 3 4 5 6 7 8 9 10 from collections import Counterdef keywords (s ): s = [w for w in s if w in model] ws = {w:sum ([predict_proba(u, w) for u in s]) for w in s} return Counter(ws).most_common()import pandas as pd import jieba s = u'太阳是一颗恒星' pd.Series(keywords(jieba.cut(s)))
不一样的 “相似”
相似度的定义
当用 Word2Vec 得到词向量后,一般我们会用余弦相似度来比较两个词的相似程度,定义为
有了这个相似度概念,我们既可以比较任意两个词之间的相似度,也可以找出跟给定词最相近的词语。这在 gensim 的 Word2Vec 中,由 most_similar 函数实现。
相似度怎么定义呢?答案是:看场景定义所需要的相似 。
事实上,Word2Vec 本质上来说,还是使用上下文的平均分布描述当前词(因为 Word2Vec 是不考虑词序的),而余弦值与向量模长没关系,因此它描述的是 “相对一致 ”。那么,余弦相似度大,事实上意味着这两个词经常跟同一批词搭配,或者更粗糙讲,那就是在同一句话中,两个词具有可替换性 。
比如,“广州” 最相近的词语是 “东莞”、“深圳”,那是因为很多场景下,直接将矩阵中的 “广州” 直接换成 “东莞”、“深圳”,这个句子还是合理的(是句子本身的合理,但这个句子不一定是事实,比如 “广州是广东的省会”,变成 “东莞是广东的省会”,这个句子是合理的,但是这个句子并非是事实)。
1 2 3 4 5 6 7 8 9 10 11 12 13 >>> s = u'广州' >>> pd.Series(model.most_similar(s))0 (东莞, 0.840889930725 )1 (深圳, 0.799216389656 )2 (佛山, 0.786817014217 )3 (惠州, 0.779960155487 )4 (珠海, 0.735232532024 )5 (厦门, 0.725090026855 )6 (武汉, 0.724122405052 )7 (汕头, 0.719602525234 )8 (增城, 0.713532149792 )9 (上海, 0.710560560226 )
另一种相似
前面已经说了,相似度的定义事实上要看场景的,余弦相似度只是其中之一。有时候我们会觉得 “东莞” 和 “广州” 压根就没联系,对于 “老广州” 来说,“白云山”、“白云机场”、“广州塔” 这些词才是跟 “广州” 最相似的,这种场景也是很常见的,比如做旅游的推荐,旅游来到广州后,自然是希望输入 “广州” 后,自动输出来 “白云山”、“白云机场”、“广州塔” 这些广州相关的词语,而不是输出 “东莞”、“深圳” 这些词语。
这种 “相似”,准确来说是 “相关”,应该怎么描述呢?答案是互信息 ,定义为
互信息越大,说明x ,y 两个词经常一起出现。
这样,在给定词 x 的情况下,我们就可以找出经常跟词x 一起出现的词,这个也完全可以由 Word2Vec 中的 Skip-Gram+Huffman
Softmax 模型来完成。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import numpy as npimport gensim model = gensim.models.word2vec.Word2Vec.load('word2vec_wx' )def predict_proba (oword, iword ): iword_vec = model[iword] oword = model.wv.vocab[oword] oword_l = model.syn1[oword.point].T dot = np.dot(iword_vec, oword_l) lprob = -sum (np.logaddexp(0 , -dot) + oword.code*dot) return lprobfrom collections import Counterdef relative_words (word ): r = {i:predict_proba(i, word)-np.log(j.count) for i,j in model.wv.vocab.iteritems()} return Counter(r).most_common()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 >>> pd.Series(relative_words(u'武汉' )[:25 ])0 (首义路, -17.170893633587788 )1 (82618656 , -17.231201044500523 )2 (2367009 , -17.287974766672306 )3 (云南警官学院, -17.509239610149237 )4 (2370699 , -17.556570768771998 )5 (邦威, -17.57391761673954 )6 (3860861 , -17.590552869469576 )7 (中国地质大学, -17.626356836976214 )8 (PLAYFUN, -17.67451952769016 )9 (4267366 , -17.680991448877684 )10 (洪山区, -17.708803456747557 )11 (2495890 , -17.75180380766531 )12 (≙, -17.80653344695773 )13 (CRBC, -17.808028264860624 )14 (88127209 , -17.854606799066715 )15 (陕西理工学院, -17.87299837843647 )16 (新闻广播, -17.922096056447952 )17 (贵州民族学院, -17.925434487853966 )18 (青菱乡, -17.950129885816672 )19 (华远地产, -17.96047848483534 )20 (王家湾, -17.96697624713915 )21 (浙江海洋学院, -17.967661025916062 )22 (8374 , -17.990002225873262 )23 (武汉, -18.006979965405414 )24 (北京西, -18.025565738369206 )
可以发现,得到的结果基本上都是跟武汉紧密相关的。当然,有时候我们稍微强调一下高频词,因此,可以考虑将互信息公式修改为
其中α 是一个略小于 1 的常数。如果取α = 0.9 ,那么有
1 2 3 4 from collections import Counterdef relative_words (word ): r = {i:predict_proba(i, word)-0.9 *np.log(j.count) for i,j in model.wv.vocab.iteritems()} return Counter(r).most_common()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 >>> pd.Series(relative_words(u'武汉' )[:25 ])0 (首义路, -16.578201030990748 )1 (82618656 , -16.736325055462704 )2 (武汉, -16.7690849910558 )3 (中国地质大学, -16.78506251997197 )4 (2367009 , -16.793098777634487 )5 (洪山区, -16.85748493974569 )6 (湖北, -16.91253013941413 )7 (云南警官学院, -16.988290994865096 )8 (邦威, -17.018234810569584 )9 (新闻广播, -17.052261316402134 )10 (2370699 , -17.074542612211495 )11 (武汉市, -17.076732606985267 )12 (PLAYFUN, -17.081295008945357 )13 (3860861 , -17.095676880431757 )14 (4267366 , -17.19896329231718 )15 (王家湾, -17.239389787084498 )16 (2495890 , -17.256927818627492 )17 (北京西, -17.26734281894156 )18 (沌口, -17.315363984341012 )19 (CRBC, -17.331810871380846 )20 (88127209 , -17.359730810028896 )21 (陕西理工学院, -17.38027300992075 )22 (≙, -17.381683922752796 )23 (长江大桥, -17.38558019076123 )24 (老河口, -17.391156917974474 )
相对来说,后面这个结果更加可读一点。
要说明的是:很遗憾,Huffman
Softmax 虽然在训练阶段加速计算,但在预测阶段,当需要遍历一遍词典时,事实上它比原生的 Softmax 还要慢,所以这并不是一个高效率的方案。
总结
根据前面两部分,我们可以看到,“相似” 一般有两种情景:1、经常跟同一批词语搭配出现;2、经常一起出现。这两种情景,我们都可以认为是词语之间的相似,适用于不同的需求。
比如,在做多义词的词义推断时,比如 star 是 “恒星” 还是 “明星”,就可以利用互信息。我们可以事先找到 star 意思为 “恒星” 的时候的语料,找出与 star 互信息比较大的的词语,这些词语可能有 sun、planet、earth,类似地,可以找到 star 为 “明星” 的时候的语料,找出与 star 互信息比较大的词语,这些词语可能有 entertainment、movie 等。到了新的语境,我们就可以根据上下文,来推断究竟是哪个词义。
总而言之,需要明确自己的需求,然后再考虑对应的方法。