Word2Vec
1. 背景知识
在NLP任务中,很多时候我们处理文本的细粒度的是词语,所以我们需要将词语转换成向量的形式以进行各式各样的计算。
最初也是最简单的一种词向量表达方式是 One-hot 编码词语,例如:
text: I love you
Vocab: { I: 0, love: 1, you: 2}
Wordvector: {I:[1,0,0], love: [0,1,0], you: [0,0,1]}
可以看到这种表达方式很直观,但是有缺陷:
- 词向量的维度 = vocab中词语数量。后者在实际中往往是几万甚至几十几百万,因此会有维度爆炸
- 只能单纯的表示词语在vocab的索引,无法表示词语之间的内在联系
有许多研究者就词向量的获取方式作了改进,我们今天讲的word2vec便是其中一种。wordvec是google团队在2013年发布的一个训练获取词向量的工具包,包含了两种训练训练词向量的模型(skip-gram, COBW)和训练优化工具(negative samping, hierarchy softmax)
2. CBOW, Skip-Gram介绍
2.1 以单个词语为输入的情况
从最简单的情况开始讲起,假设输入的文本只有一个单词。我们设 V = vocab size,N = hidden size。Input layer是一个 (V*1) 大小的embedding layer : { x 1 , x 2 , . . . , x V x_1,x_2,...,x_V x1,x2,...,xV}。 x i x_i xi为1代表该词语在输入文本中出现。以单个词语为输入时,意味着{ x 1 , x 2 , . . . , x V x_1,x_2,...,x_V x1,x2,...,xV}只有一个1值。
W V × N W_{V\times N} WV×N是input 和 hidden之间的权重矩阵,每一行代表一个与input word相关的N维向量,我们用 v w v_w vw表示。对于单个词语的输入,假设 x k = 1 x_k=1 xk=1,我们有:
h = W T x = W ( k , . ) T : = v w I T h = W^Tx = W^T_{(k,.)}:= v^T_{w_I} h=WTx=W(k,.)T:=vwIT
注意到 x k = 1 x_k=1 xk=1,所以h其实就是W的第k行, v w I v_{w_I} vwI便是输入词 w I w_I wI的向量表示。
通过hidden layer到output layer中的权重矩阵 W ′ = w i j ′ W'={w'_{ij}} W′=wij′,我们可以计算出词汇表中每个词语的得分:
u j = v w j ′ T h u_j = {v'_{w_j}}^Th uj=vwj′Th
v w j ′ v'_{w_j} vwj′是 W ’ W’ W’的第j列,然后我们用softmax得到所有词语的后验概率:
y j y_j yj便是对应output layer的第j个词。
我们注意到,对于输入词 w I w_I wI,他在W和W’中有两种向量形式: v w I , v w I ′ v_{w_I}, v'_{w_I} vwI,vwI′,接下来我们把分别称为词语 w I w_I wI输入向量(input vector)和输出向量(output vector)。
更新输出向量:
我们要最大化 P ( w o ∣ w i ) P(w_o|w_i) P(wo∣wi):
E
=
−
l
o
g
p
(
w
O
∣
w
I
)
E = -log\ p(w_O|w_I)
E=−log p(wO∣wI)便是我们的损失函数,
j
∗
j^*
j∗是实际输出词语的index。然后反向传播更新输出向量:
从式子中可以看出,更新一次,我们要检查vocab中所有词的概率。
更新输入向量:
E对输入向量W求导,得到一个V*N的矩阵,因为只有一个输入词,所以该矩阵只有一行是非0的,值为 E H T EH^T EHT,然后便可以对W进行更新:
注意,只有 v w I v_{w_I} vwI那一行得到了更新。
讲过多次训练,最终,输入词的词向量维 w j = h = W T x j w_j=h = W^Tx_j wj=h=WTxj
2.2 CBOW
直观来看,CBOW实际上是以上下文的方式来预测输出的词,我们需要设置一个上下文词语数量skip_window来决定窗口值C, 比如
Text: I love you three thousands.
Window size: skip_window = 2,C=2*skip_window+1=5
假设我们的输入词是 {I, love, three, thousands}, 我们的预测就应该是{you}。参数与2.1中的参数设置基本相同,hidden layer的值为:
C为窗口值5,损失函数也随即成了:
输出输入向量更新为:
注意,对于所有的输入词,输入向量 W W W都是同一个矩阵。
2.3 Skip-Gram
直观来看,skip-gram实际上利用单个词预测上下文。我们设置skip_window和num_skip两个参数,前者代表上文或者下文的个数,后者代表从窗口中选取输出词的数量。同样对于文本"I love you three thousand":
设 skip_window = 1, num_skip=3, 意味着我们的窗口值(包括输入词)为span = 2*1+1 = 5, 输出词个数为3,即:
Input: “you” output=[“I love you”, “love you three”, “you three thousand”)]
损失函数为:
其中为输出层所有词的错误和。
输出向量更新公式:
输入向量更新公式:
3. 优化计算效率的两种方法
我们注意到,输入向量的学习是很轻松的,但是输出向量的更新却很繁重。对于每个训练示例(training instance),我们都需要遍历一遍vocab中的所有词语,计算每个词语的概率,损失,然后更新相对应输出向量,这对于动辄几万几十万的vocab来说计算成本巨大,所以有一个优化的思路是减少每次训练是需要更新的输出向量。
3.1 Hierarchical Softmax
Hierarchical Softmax是一个利用霍夫曼树来表示vocab中所有词来达到优化softmax计算的方法。首先,将原先模型的输出向量 W ′ W' W′矩阵替代成如下的霍夫曼树:
白色的叶结点代表输出层的词,共V个; 黑色结点为内部节点,共V-1个。图中显示从隐藏层到输出层 w 2 w_2 w2的路径 P 2 P_2 P2,路径长度 L ( w 2 ) = 4 L(w_2)=4 L(w2)=4, n ( w 2 , j ) : j = 1 , 2 , 3 n(w_2,j):j=1,2,3 n(w2,j):j=1,2,3代表该路径上的第i个内部节点。
原先用输出向量表示输出词 w 2 w_2 w2,现在变成了用 P 2 P_2 P2路径上每个节点表示。树中每一个内部节点都有一个输出向量 v n ( w , j ) ′ v'_{n(w,j)} vn(w,j)′,所以相应的,输出词的概率也变成了:
c h ( n ( w , j ) ) ch(n(w,j)) ch(n(w,j))表示节点 n ( w , j ) n(w,j) n(w,j)的左节点,
直观的理解就是预测值 w w w为实际值 w O w_O wO的概率是在 P 2 P_2 P2的每一个节点都取左节点的概率。
更新的式子为:
其中
就是路径上每个词语的内部节点的预测error。我们根据词频构建霍夫曼树,出现频率越高的词拥有越短的路径(内部节点越少),所需要更新的
v
j
′
v'_j
vj′也越少。因此,我们的更新参数时的计算复杂度从原来的O(V)减少到了O(logV)。当然,我们的总参数量并没有显著减少(
N
×
V
→
(
V
−
1
)
×
N
N \times V \to (V-1)\times N
N×V→(V−1)×N)。
3.2 Negative Sampling
Negative Sampling的优化思路简单暴力,我们称输出词为正样本,非输出词为负样本,我们基于经验选择特定的概率分布作为样本的噪音分布 P n ( w ) P_n(w) Pn(w),基于 P n ( w ) P_n(w) Pn(w)选择K个负样本:
更新方程:
所以,每次更新,我们只需要更新正样本和K个负样本就好了,显著地减少了计算时间。
参考资料:
Rong, Xin. “word2vec parameter learning explained.” arXiv preprint arXiv:1411.2738 (2014).