Word2Vec知识总结

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就是“两个训练方案+两个提速手段”,所以严格来讲,它有四个备选的模型。

两个训练方案分别是CBOWSkip-Gram,如图所示

  • 用通俗的语言来说,就是“周围词叠加起来预测当前词”P(wt|Context))“当前词分别来预测周围词”P(wothers|wt)),也就是条件概率建模问题了;

  • 两个提速手段,分别是层次Softmax和负样本采样。层次Softmax是对Softmax的简化,直接将预测概率的效率从O(|V|)降为O(log2|V|),但相对来说,精度会比原生的Softmax略差;负样本采样则采用了相反的思路,它把原来的输入和输出联合起来当作输入,然后做一个二分类来打分,这样子我们可以看成是联合概率P(wt, Context)P(wt, wothers)的建模了,正样本就用语料出现过的,负样本就随机抽若干。

本文所使用的模型是“Skip-Gram + 层次Softmax”的组合

层次Softmax核心思想:将复杂的多分类问题转化为一系列二分类问题,用一棵二叉树(通常是哈夫曼树)来组织词汇表,从而将计算复杂度从O(V)降低到O(log₂V)

  1. 构建哈夫曼树:根据词频构建一棵哈夫曼二叉树。词汇表中的每个单词都是树的一个叶子节点。词频越高的单词,其路径越短。
  2. 取消输出向量:模型不再需要为每个单词维护一个输出向量(即巨大的 WN × V 矩阵)。取而代之的是,为树上的每个非叶子节点(即内部节点)学习一个N维的向量。内部节点的数量大约是V-1个,所以参数量几乎没有增加。
  3. 预测路径:对于给定的中心词和上下文,模型的目标不再是直接计算每个单词的概率,而是从根节点到目标单词叶子节点所经过的路径。路径上的每一步都是一个二分类问题(通常用逻辑回归/Sigmoid函数判断是向左走还是向右走)。
    • 例如,要计算单词 w 的概率,就从根节点开始,在每一个内部节点,用该节点的向量和隐藏层向量计算一个概率,判断下一步的方向。最终,w 的概率就是路径上所有判断概率的连乘。

提取关键词

关键词的定义

关键词也好,摘要也好,我们希望能够尽可能快地获取文章的大意,如果一篇文章的关键词是“深度学习”,我们就会知道,这篇文章不可能大谈特谈“钢铁是怎么练成的”,也就是说,我们可以由关键词来猜到文本的大意,用数学来表示,那就是条件概率 p(s|wi) 这里的s代表着一段文本,wi是文本中的某个词,如果wi是文本的关键词,那么应该使得上述概率最大。也就是说,我们只需要对句子中所有的词,算一遍上述概率,然后降序排列,就可以提取关键词了。说白了,关键词就是最能让我们猜到原文的词语

怎么估算这个概率?简单使用朴素贝叶斯假设就好,如果sn个词w1,w2,…,wn组成,那么 这样,我们只需要估算词与词之间的转移概率p(wk|wi),就可以得到条件概率p(s|wi)了,从而完成关键词的提取。

Word2VecSkip-Gram模型就擅长于对p(wk|wi)的建模

Word2Vec算概率

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import 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 np
import 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))

  1. oword.code*dot:编码与点积的乘积
    • 当编码为0时:这项为0
    • 当编码为1时:这项为dot
  2. 组合后的表达式:
    • 对于路径上的每个节点r:
      • 如果编码为0:贡献为 -log(σ(dot_r))
      • 如果编码为1:贡献为 -log(σ(-dot_r)) = dot_r - log(1+exp(dot_r))
  3. sum(...):对路径上所有节点的对数概率求和
    • 因为总概率是路径上各节点概率的乘积,取对数后变为求和
  4. -sum(...):取负号得到最终的对数似然
    • 原始计算得到的是负对数概率,取负号得到正常的对数概率

数学等价性证明: - 当code=0时:-log(σ(dot)) = log(1+exp(-dot)) - 当code=1时:-log(σ(-dot)) = dot + log(1+exp(-dot)) - 统一公式:log(1+exp(-dot)) + code*dot

1
return lprob

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 Counter
def 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得到词向量后,一般我们会用余弦相似度来比较两个词的相似程度,定义为 有了这个相似度概念,我们既可以比较任意两个词之间的相似度,也可以找出跟给定词最相近的词语。这在gensimWord2Vec中,由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 np
import 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

from collections import Counter
def 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 Counter
def 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等。到了新的语境,我们就可以根据上下文,来推断究竟是哪个词义。

总而言之,需要明确自己的需求,然后再考虑对应的方法。


Word2Vec知识总结
https://striver98.github.io/2025/09/24/Word2Vec知识总结/
作者
Wang Zhixuan
发布于
2025924
许可协议