概述
Word2vec在NLP里占有非常重要的地位,是很多深度学习技术的基础,那它解决了什么问题?是什么?为什么能解决呢?下面详细地介绍各个部分。
Word2vec解决什么问题?
我们知道,在一般的机器学习中,数据分为两大类,一类是数值型,另一类是字符型数据:对于数值型数据,能够有效地处理,可以直接用于数据拟合,回归分析中;对于字符型数据,如常见的性别(男,女),省份等,我们会把他们one-hot编码,这样由原来的一列数据增加若干列数据,当我们的一列数据具有大量不同的值时,扩增的列也将大幅增加,这在机器学习中会极大影响模型的学习,降低学习的效果。
而在NLP中,我们的数据全部是字符型数据,one-hot编码根本不现实!当然,也可以这样做,我们得到每个词的维度将是词汇表的大小,至少数万级别。
为此,我们需要找到其他的方法,将每个词的表示降维,如果从one-hot的结果出发,我们利用PCA, SGD等方法也可以降维,但代价是巨大的,不仅消耗大量计算资源,而且可扩展性差,上述降维技术要求把所有的数据一次性输入,若我们增加一些数据,则需要重新训练。
Word2vec怎么解决的
泛化地说:word2vec得到每个词的词向量并不是一步完成的,首先为每个词初始化一个随机向量,每次训练一个滑动窗口内的数据(一般是5个词),将滑动窗口内某个(些)词作为训练数据,其他词作为预测数据完成一次训练,更新词的向量参数,这样经过多次迭代后,每个词的向量会达到稳定,完成词向量的学习。
具体地说:
Word2vec分为两个模型:CBOW(Continuous Bag-of-Words Model) 模型和 Skip-gram模型,它们的区别如图:
CBOW模型
首先介绍CBOW模型,上图给出了模型的结构,包括三层:输入层,投影层和输出层;每次的输入和输出都是在一个滑动窗口内。举例而言, w w w为一个词, C o n t e x t c ( w ) Context_c(w) Contextc(w)为词前后长度为c的所有词,这2c+1个词组成一个滑动窗口;
- 输入层:输入内容为中心词 w w w外的所有词,具体内容是把每个词转化为表示为one-hot编码,此时每个词的维度为词汇表的大小。
- 投影层:输入层为 2 c ⋅ V 2c \cdot V 2c⋅V的维度,输入到投影层有一个参数矩阵 V ⋅ m V\cdot m V⋅m(其中m为我们预设的词向量维度) ,经计算得到 2 c ⋅ m 2c \cdot m 2c⋅m的矩阵,投影层将 2 c 2c 2c个向量做求和累加,即 x w = ∑ i = 1 2 c v w x_w=\sum_{i=1}^{2c}v_w xw=∑i=12cvw
- 输出层:经过投影层后,我们得到 1 ∗ m 1*m 1∗m维的向量,这就是我们模型的输出,那怎么将他和真实的label(也即是 w w w)关联在一起,构造合适的损失函数进行学习?这里有三种思路:
传统做法
首先想到的是将得到的向量与新的矩阵M(维度为 V ∗ m V*m V∗m)计算相似度,得到 1 ∗ V 1*V 1∗V个值,然后进行softmax处理,得到 1 ⋅ m 1\cdot m 1⋅m维向量,将其与为真实词对应的向量 v m v_m vm比较设置目标函数,进行更新,即完成一次参数更新。
上述过程的简单代码表示即:
# 输入层-投影层
EMBEDDING_DIM = 5 # you can choose your own number
W1 = tf.Variable(tf.random_normal([vocab_size, EMBEDDING_DIM]))
b1 = tf.Variable(tf.random_normal([EMBEDDING_DIM])) #bias
hidden_representation = tf.add(tf.matmul(x,W1), b1)
# 输出层
prediction = tf.nn.softmax(tf.add( tf.matmul(hidden_representation, W1), b1))
# 设置目标函数
cross_entropy_loss = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(prediction), reduction_indices=[1]))
# 更新
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy_loss)
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
sess.run(train_step, feed_dict={x: x_train, y_label: y_train})
注意此时,我们得到两个矩阵,前面为input_embedding, 后面为output_embedding,训练后输入和输出的向量效果几乎一样,而把这两个矩阵设为同样的权重则效果更差。
Hierarchical Softmax
- 首先介绍Humman Tree
由名字可知,Huffman Tree是一种树结构,它的定义是:
给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,
称这样的二叉树为最优二叉 树,也称为霍夫曼树(Huffman Tree).
我们以每个词出现频率作为权重,构建Huffman Tree,具体过程如下[1]:
输入: 权值为 ( w 1 , w 2 , . . . , w n ) (w_1, w_2, ...,w_n) (w1,w2,...,wn)的n个节点
输出:一颗Huffman 树
- 将 ( w 1 , w 2 , . . . , w n ) (w_1, w_2, ...,w_n) (w1,w2,...,wn)看做是有n棵树的森林,每个树仅有一个节点。
2)在森林中选择根节点权值最小的两棵树进行合并,得到一个新的树,这两颗树分布作为新树的左右子树。新树的根节点权重为左右子树的根节点权重之和。
3)将之前的根节点权值最小的两棵树从森林删除,并把新树加入森林。- 重复步骤2)和3)直到森林里只有一棵树为止。
举个例子:
我们有(a,b,c,d,e,f)共6个节点,节点的权值分布是(20,4,8,6,16,3)。
首先是最小的b和f合并,得到的新树根节点权重是7.此时森林里5棵树,根节点权重分别是20,8,6,16,7。此时根节点权重最小的6,7合并,得到新子树,依次类推,最终得到下面的霍夫曼树。
- 为什么使用Huffman tree
由传统做法虽然可以求得词向量,但计算量很大,耗资源严重,为了解决上述问题,避免计算所有词的softmax概率,引入Huffman Tree;具体的做法是:
对于我们要预测的标签,也就是中间的词,先将其进行Huffman编码,实际上事先需要对所有的词进行Huffman编码,这里对于所有词汇组成的Huffman Tree,我们可以约定左子树编码为1,右子树为0,这样我们可能得到中心词 w w w的编码为 010 010 010
由真实值的编码和隐藏层的向量,怎么利用设置目标函数呢?
这里,我们采用二元逻辑回归的方法,即如果编码为1,那就是负类,沿左子树走;如果编码为0,那沿着右子树走,就是正类,举例,当为1时:
P
(
+
)
=
σ
(
x
w
T
θ
)
=
1
1
+
e
−
x
w
T
θ
P(+) = \sigma(x_w^T\theta) = \frac{1}{1+e^{-x_w^T\theta}}
P(+)=σ(xwTθ)=1+e−xwTθ1
具体而言,即:
P
(
d
j
w
∣
x
w
,
θ
j
−
1
w
)
=
{
σ
(
x
w
T
θ
j
−
1
w
)
d
j
w
=
0
1
−
σ
(
x
w
T
θ
j
−
1
w
)
d
j
w
=
1
P(d_j^w|x_w, \theta_{j-1}^w)= \begin{cases} \sigma(x_w^T\theta_{j-1}^w)& {d_j^w=0}\\ 1- \sigma(x_w^T\theta_{j-1}^w) & {d_j^w = 1} \end{cases}
P(djw∣xw,θj−1w)={σ(xwTθj−1w)1−σ(xwTθj−1w)djw=0djw=1
其中 x w x_w xw为隐层的词向量,而 θ \theta θ为需要求的模型参数。
具体举个例子,当我们要预测的词为 w 2 w_2 w2时,其编码为 110 110 110,那么我们需要最大整个路程的似然函数,即
∏ i = 1 3 P ( n ( w i ) , i ) = ( 1 − 1 1 + e − x w T θ 1 ) ( 1 − 1 1 + e − x w T θ 2 ) 1 1 + e − x w T θ 3 \prod_{i=1}^3P(n(w_i),i) = (1- \frac{1}{1+e^{-x_w^T\theta_1}})(1- \frac{1}{1+e^{-x_w^T\theta_2}})\frac{1}{1+e^{-x_w^T\theta_3}} i=1∏3P(n(wi),i)=(1−1+e−xwTθ11)(1−1+e−xwTθ21)1+e−xwTθ31
后续可以使用梯度下降法来更新参数;
Huffman tree的好处:
第一, 由于是二叉树,之前的计算量是
V
V
V,现在是
l
o
g
2
V
log_2V
log2V
第二,由于建树时,高频的词靠近树根,这样高频词更少时间找到。
具体算法流程:
输入:基于CBOW的语料训练样本,词向量的维度大小M,CBOW的上下文大小2c,步长η
输出:霍夫曼树的内部节点模型参数θ,所有的词向量w
- 基于语料训练样本建立霍夫曼树
- 随机初始化所有的模型参数θ,所有的词向量w
- 进行梯度上升迭代过程,对于训练集中的每一个样本(context(w),w)做如下处理:
a) e=0, 计算 x w = 1 2 c ∑ i = 1 2 c x i x_w= \frac{1}{2c}\sum\limits_{i=1}^{2c}x_i xw=2c1i=1∑2cxi
b) for j = 2 to l w l_w lw, 计算:
f = σ ( x w T θ j − 1 w ) f = \sigma(x_w^T\theta_{j-1}^w) f=σ(xwTθj−1w)
g = ( 1 − d j w − f ) η g = (1-d_j^w-f)\eta g=(1−djw−f)η
e = e + g θ j − 1 w e = e + g\theta_{j-1}^w e=e+gθj−1w
θ j − 1 w = θ j − 1 w + g x w \theta_{j-1}^w= \theta_{j-1}^w + gx_w θj−1w=θj−1w+gxw
c) 对于context(w)中的每一个词向量 x i x_i xi(共2c个)进行更新:
x i = x i + e x_i = x_i + e xi=xi+e
d) 如果梯度收敛,则结束梯度迭代,否则回到步骤3继续迭代。
Negative Sampling
利用Huffman tree能够节省很多计算,但也有一些问题,如果我们的目标词是一个生僻的词,那么就需要在Huffman中走很久;另一种更简单的方法即是 Negative sampling,中文名就是负采样。
传统做法中,在巨大的词汇表里,我们有一个明确的正例,其他均是负例,softmax计算时会用到正例以及所有的负例,负采样简单的可以认为是从其他所有的负例中选取若干负例,与正例一起完成一次训练;
那么怎样通过一个正例和n个负例设置目标函数呢?又怎样从所有负例中采样呢?
- 目标函数
根据逻辑斯特回归,模型的似然函数为:
∏ i = 0 n e g P ( c o n t e x t ( w 0 ) , w i ) = σ ( x w 0 T θ w 0 ) ∏ i = 1 n e g ( 1 − σ ( x w 0 T θ w i ) ) = ∏ i = 0 n e g σ ( x w 0 T θ w i ) y i ( 1 − σ ( x w 0 T θ w i ) ) 1 − y i \begin{aligned} \prod_{i=0}^{neg}P(context(w_0), w_i) & = \sigma(x_{w_0}^T\theta^{w_0})\prod_{i=1}^{neg}(1- \sigma(x_{w_0}^T\theta^{w_i})) \\ &= \prod_{i=0}^{neg} \sigma(x_{w_0}^T\theta^{w_i})^{y_i}(1- \sigma(x_{w_0}^T\theta^{w_i}))^{1-y_i} \end{aligned} i=0∏negP(context(w0),wi)=σ(xw0Tθw0)i=1∏neg(1−σ(xw0Tθwi))=i=0∏negσ(xw0Tθwi)yi(1−σ(xw0Tθwi))1−yi
对数似然函数为
L = ∑ i = 0 n e g y i l o g ( σ ( x w 0 T θ w i ) ) + ( 1 − y i ) l o g ( 1 − σ ( x w 0 T θ w i ) ) L = \sum\limits_{i=0}^{neg}y_i log(\sigma(x_{w_0}^T\theta^{w_i})) + (1-y_i) log(1- \sigma(x_{w_0}^T\theta^{w_i})) L=i=0∑negyilog(σ(xw0Tθwi))+(1−yi)log(1−σ(xw0Tθwi))
计算
θ
w
i
\theta^{w_i}
θwi的梯度:
∂
L
∂
θ
w
i
=
y
i
(
1
−
σ
(
x
w
0
T
θ
w
i
)
)
x
w
0
−
(
1
−
y
i
)
σ
(
x
w
0
T
θ
w
i
)
x
w
0
=
(
y
i
−
σ
(
x
w
0
T
θ
w
i
)
)
x
w
0
\begin{aligned} \frac{\partial L}{\partial \theta^{w_i} } &= y_i(1- \sigma(x_{w_0}^T\theta^{w_i}))x_{w_0}-(1-y_i)\sigma(x_{w_0}^T\theta^{w_i})x_{w_0} \\ & = (y_i -\sigma(x_{w_0}^T\theta^{w_i})) x_{w_0} \end{aligned}
∂θwi∂L=yi(1−σ(xw0Tθwi))xw0−(1−yi)σ(xw0Tθwi)xw0=(yi−σ(xw0Tθwi))xw0
计算
x
w
0
x_{w_0}
xw0的梯度:
∂
L
∂
x
w
0
=
∑
i
=
0
n
e
g
(
y
i
−
σ
(
x
w
0
T
θ
w
i
)
)
θ
w
i
\frac{\partial L}{\partial x^{w_0} } = \sum\limits_{i=0}^{neg}(y_i -\sigma(x_{w_0}^T\theta^{w_i}))\theta^{w_i}
∂xw0∂L=i=0∑neg(yi−σ(xw0Tθwi))θwi
- 负采样方法
采样方法并不复杂,希望高频词得到的概率大,低频词得到的概率更小;这里,我们将一个长度为1的线段分为V份,每份对应词汇表中的一个词。每个词的线段长度为
l
e
n
(
w
)
=
c
o
u
n
t
(
w
)
∑
u
∈
v
o
c
a
b
c
o
u
n
t
(
u
)
len(w) = \frac{count(w)}{\sum\limits_{u \in vocab} count(u)}
len(w)=u∈vocab∑count(u)count(w)
在Word2Vec中,分子和分母都去了3/4次幂:
l e n ( w ) = c o u n t ( w ) 3 / 4 ∑ u ∈ v o c a b c o u n t ( u ) 3 / 4 len(w) = \frac{count(w)^{3/4}}{\sum\limits_{u \in vocab} count(u)^{3/4}} len(w)=u∈vocab∑count(u)3/4count(w)3/4
在采样前,我们将这段长度为1的线段划分成M等份,这里M>>V(论文中 M = 1 0 8 M=10^8 M=108),这样可以保证每个词对应的线段都会划分成对应的小块。而M份中的每一份都会落在某一个词对应的线段上。在采样的时候,随机生成一个[1, M-1]的随机数m,然后看m对应到的线段所属的词就是我们的负例词,如果碰巧遇到目前词 w w w,跳过即可。
算法流程
输入:基于CBOW的语料训练样本,词向量的维度大小M,CBOW的上下文大小2c,步长η, 负采样的个数neg
输出:词汇表每个词对应的模型参数θ,所有的词向量 x w x_w xw
- 随机初始化所有的模型参数θ,所有的词向量w
- 对于每个训练样本 ( c o n t e x t ( w 0 ) , w 0 ) (context(w_0),w_0) (context(w0),w0),负采样出neg个负例中心词 w i , i = 1 , 2 , . . . n e g w_i,i=1,2,...neg wi,i=1,2,...neg
- 进行梯度上升迭代过程,对于训练集中的每一个样本 ( c o n t e x t ( w 0 ) , w 0 , w 1 , . . . w n e g ) (context(w_0), w_0,w_1,...w_{neg}) (context(w0),w0,w1,...wneg),做如下处理:
a. e=0, 计算 x w 0 = 1 2 c ∑ i = 1 2 c x i x_{w_0}= \frac{1}{2c}\sum\limits_{i=1}^{2c}x_i xw0=2c1i=1∑2cxi
b. for i= 0 to neg, 计算:
f = σ ( x w 0 T θ w i ) g = ( y i − f ) η e = e + g θ w i θ w i = θ w i + g x w 0 f = \sigma(x_{w_0}^T\theta^{w_i})\\ g = (y_i-f)\eta\\ e = e + g\theta^{w_i}\\ \theta^{w_i}= \theta^{w_i} + gx_{w_0}\\ f=σ(xw0Tθwi)g=(yi−f)ηe=e+gθwiθwi=θwi+gxw0
c.对于context(w)中的每一个词向量 x k x_k xk(共2c个)进行更新:
x k = x k + e x_k = x_k + e xk=xk+e
d. 如果梯度收敛,则结束梯度迭代,否则回到步骤3继续迭代。
Skip-gram 模型
除了结构上不同,即我们从滑动窗口中选取1个中心词作为输入,2c个词作为预测的label,其建模的方式与CBOW类似,同样有hierachical softmax和negative sampling。
这里直接谢谢Negative sampling的算法流程
输入:基于Skip-gram的语料训练样本,词向量的维度大小M,skip-gram的上下文大小2c,步长η, 负采样的个数neg
输出:词汇表每个词对应的模型参数θ,所有的词向量 x w x_w xw
- 随机初始化所有的模型参数θ,所有的词向量w
- 对于每个训练样本 ( c o n t e x t ( w 0 ) , w 0 ) (context(w_0),w_0) (context(w0),w0),负采样出neg个负例中心词 w i , i = 1 , 2 , . . . n e g w_i,i=1,2,...neg wi,i=1,2,...neg
- 进行梯度上升迭代过程,对于训练集中的每一个样本 ( c o n t e x t ( w 0 ) , w 0 , w 1 , . . . w n e g ) (context(w_0), w_0,w_1,...w_{neg}) (context(w0),w0,w1,...wneg),做如下处理:
a for i=1 to 2c:
i) e=0, 计算 x w 0 = 1 2 c ∑ i = 1 2 c x i x_{w_0}= \frac{1}{2c}\sum\limits_{i=1}^{2c}x_i xw0=2c1i=1∑2cxi
ii) for i= 0 to neg, 计算:
f = σ ( x w 0 T θ w i ) g = ( y i − f ) η e = e + g θ w i θ w i = θ w i + g x w 0 f = \sigma(x_{w_0}^T\theta^{w_i})\\ g = (y_i-f)\eta\\ e = e + g\theta^{w_i}\\ \theta^{w_i}= \theta^{w_i} + gx_{w_0}\\ f=σ(xw0Tθwi)g=(yi−f)ηe=e+gθwiθwi=θwi+gxw0
iii)词向量更新:
x k = x k + e x_k = x_k + e xk=xk+e
b. 如果梯度收敛,则结束梯度迭代,否则回到步骤3继续迭代。
区别
CBOW和Skip-gram均是词向量化的方法,他们的差异在哪呢?
CBOW对周围词的调整是统一的,求出的梯度会同样地作用到每个周围词向量中,并且其预测行为的次数和文本的词数几乎相等,复杂度为O(V);
Skip-gram 的预测次数要多于CBOW, 因为每个词作为中心词时,都要对周围的词预测一次,这样时间复杂度为O(KV),训练上比CBOW要久。
在使用skip-gram时,每个词都要收到周围的词的影响,每个词在作为中心词的时候,都要进行K次的预测、调整。当数据量较小时,或者词相对生僻时,这样的调整会使得词向量更加准确。
Ref:
- https://www.cnblogs.com/pinard/p/7160330.html
- https://blog.csdn.net/itplus/article/details/37969519
- https://blog.csdn.net/Mr_tyting/article/details/80091842
- https://zhuanlan.zhihu.com/p/37477611