Word2Vec 与《Distributed Representations of Words and Phrases and their Compositionality》学习笔记
什么是Word2Vec
目录
- 单词的表示(word representation)
- 单词表示的主要特点
- 映射矩阵
- 跳克里夫特模型(Skip-gram)[1]
- 层次化结构(Hierarchical Softmax)
- 负采样方法
- 其他相关细节
词嵌入(word embedding)
换句话说,在词嵌入方法提出之前 人们通常会通过使用 $one\ hot\ encoding\ 技术来对词语进行编码 表现为每个单词都被映射到一个高维度的空间中 例如 阿里的商品类别信息可能就需要一个千万级别的向量空间 然而这样的编码方式会导致数据极度稀疏 从深度学习角度来看 这种稀疏特征难以高效处理 因此 将物体编码为一个低维稠密向量并输入神经网络是一个更为高效的操作 而词嵌入算法正是在这种背景下应运而生
词嵌入的特点
基于研究发现,在构建Word2Vec模型时,并非随机初始化模型参数而是通过大量数据学习获得参数值这一过程具有重要意义

如上图所示,在词嵌入模型中,我们选取了六个单词转化为四维空间中的向量表示。这些具体意义维度分别代表性别特征、王室背景、年龄特征以及饮食偏好四个不同的抽象概念。通过计算男性与女性各自所对应的二维空间坐标之差(即男性的隐喻性语义空间减去女性隐喻性语义空间),我们获得了一个差异性的极小结果;同样地,在该空间中计算king与queen两个向量相减得到的结果也呈现出极其微小的差异性现象。这种差异性极小的现象揭示了一种有趣的类比关系

在向量空间中两个向量是平行的。
嵌入矩阵
那么如何来表示词向量呢?常用的方法是嵌入矩阵。在词汇表中通常包含大约1万个不同的词汇,在这种情况下,每个词汇都对应一个独热编码向量,并通过一一对应的方式与词汇表中的每个词汇相关联。将这些独热编码向量转换为300维的词向量表示,则需要定义一个维度为1万乘以3百的嵌入矩阵W:因此,我们建立了一个维度为1万乘以3百的嵌入矩阵W:e =xW

在所给的神经网络架构中,输入层(Input layer)对应于one hot vector表示;而隐藏层(Hidden layer)则用于表示词向量。其中W_{V×N}被定义为嵌入矩阵。因此生成词向量的过程等价于通过神经网络架构来构建并优化损失函数;进而完成对嵌入矩阵的优化过程。
Skip-gram模型
*Skip-gram*是一种生成词向量表示的方法,在Word2Vec家族中具有重要地位。
假设训练数据是一个连续的文本序列\{w_1, w_2, ..., w_T\},
其中每一个位置上的词都能够影响其周围的其他词语(CBOW模型的核心思想),
或者每一个位置上的词都能够被周围的不同词语所影响(Skip-gram模型的设计理念)。如图所示,
输入一个词语及其周边几个词语的内容,
预测该词语是基于整个上下文的概率分布属于CBOW模型;
而输入一个词语本身并预测其周围词语的概率分布,则属于Skip-gram模型。

举个例子来说吧:假设有一个句子 "I" "want" "a" "glass" "of" "juice" "to" "go" "along" "with" "my" "cereal." 如果输入为 "a glass of" 和 "*****(token)to go along" 的时候 输出就是 CBOW 模型;而当输入为 "juice," 的时候 输出则是 "a glass of frankfurter with orange juice and the token to go along." 我们主要介绍的是 Skip-gram 模型。
那么为了生成模型的正样本实例,则选取一个长度为2c+1(即在目标词前后各选取c个候选词)的滑动窗口,并从左到右滑动这个窗口至整个输入序列末端。每当窗口移动一次时,则将该窗口内的所有候选词语作为一个正样本实例加入训练数据集。
当我们获得了足够的训练样本后就可以着手定义优化目标了。
因为每个中心词w_t决定了其后的每一个可能后续词(即t+j, j>0)的概率分布基于极大似然估计。
我们的优化目标设定为最大化所有样本条件概率乘积。

接下来需要解决的问题是如何确定P(w_{t+j}|w_{t})。对于一个多分类问题而言,采用Softmax函数作为基础模型是最直观且效率最高的选择。

其中W表示为字典的大小,并且v_{wi}与v_{w}^{'}均由一个神经网络生成(如图所示):

在上文中介绍嵌入矩阵时,我们展示了这张图。其中v_{wi}代表隐藏层向量h=(h_1, h_2, ..., h_N)中的第i个元素;而v_w'则表示该系数矩阵中对应第w列的参数。将隐藏层向量h与系数矩阵W'_{N*V}进行点积运算会得到一个长度为V的一维数组;接着应用Softmax函数会对这一结果进行归一化处理。具体来说,在计算损失函数时我们希望输出尽可能接近真实标签对应的概率值;因此,在反向传播过程中通过优化参数使得定义的损失函数值能够最大化进而训练出合适的嵌入表示。
- 这就是 Skip-gram 模型的核心。
- 该模型的主要缺陷在于其在词汇表规模较大的情况下会导致训练难度显著提升。
- 基于其定义可知,在计算过程中需要逐一评估每个单词与其特定词之间的点积。
- 这将导致计算复杂度显著上升。
分层Softmax (Hierarchical Softmax)
- 分层Softmax (Hierarchical Softmax)是对于之前Softmax的一种优化,用于解决在大规模数据上难以训练的问题。
- 分层Softmax使用二叉树表示输出层,其中W个单词作为其叶子节点,并且对于每个内部节点,显式地表示其子节点的相对概率。举个例子:I want a glass of juice to go along with my cereal. 我们现在想要知道p(glass|orange),根据建立的二叉树,我们已知走到叶子节点glass的路径,现在我们从根节点出发,根据每一个内部节点为glass分配的概率,一步步的走到glass对应的叶子节点,我们将所有的概率相乘就得到了p(glass|orange)。具体公式如下:

L(w)表示为w对应的叶子节点深度。定义n(w,j)为从root到单词w的路径上的第j个节点,则n(w,1)=\text{root}且n(w,L(w))=w. [[x]]等于1当且仅当x=\text{left};否则为-1。
层次化Softmax算法所采用的树状结构对模型性能具有重要意义。研究者Mnih与Hinton深入探讨了构建树状结构的方法及其对训练时间和模型精度的影响。在本研究中我们采用了霍夫曼编码树(基于二元哈夫曼编码)作为基础架构,在这种架构下通过将短码分配给高频词汇能够有效提升训练效率。此前研究表明,在语言建模任务中根据词汇频率组合单词是一种有效的加速技术。
负采样
在我们之前的章节中提到过,在计算Softmax时效率较低是Skip-Gram的一个不足之处;而负采样作为一种改进的学习方法,则能够有效解决这一问题。
算法思想:在负采样过程中不再采用Softmax这一概率分布模型,而是将其转化为一个二分类学习任务。具体来说,在处理两个词的关系时,我们需要判断其中一个词是否为另一个词的target(目标)词:如果是,则输出1;否则输出0。
首要任务是构建训练数据集。具体步骤如下:首先筛选出一组正样本作为基础;接着从候选集中排除这些正样本之外的内容中随机选取n组作为负样本;最后将这n+1组样本整合后作为训练数据输入到神经网络模型中进行学习。

第一行从文本中选取了一个正取样,Content是orange这个单词,Target是juice这个单词。对于orange我们同时需要取n个负取样,分别是后面的四组并标记为0,在负取样中Target是从字典中随机选取的。
此外还需要注意n的选取,在论文中指出,如果在小数据集上n选取5-20比较好,如果数据集规模变大,n的取值随之减小,在大数据集上n取2-5。
在本节中我们将探讨如何构建这一监督学习模型(Logistic回归模型)。首先定义输出即为:P(y=1|c,t)=\sigma (\theta _{t}^{T}e_{c})与使用Softmax分类器类似θ_t是由目标空间中的参数控制,并通过训练集优化获得的参数向量其中e_c表示来自词嵌入矩阵中的词向量。
- 最后看一下模型,如下图:

与上一节所述的过程完全相同,在本节中我们将着重探讨最后一个步骤的具体实现细节。具体而言,在构建模型的过程中我们会对每个词向量以及字典中的每一个词进行单独的一次二分类处理以识别其是否属于目标类别即判断该词是否是Content中的Target这一操作的好处在于它将一个ValueSize规模的多分类问题成功转化为ValueSize数量的一系列二分类问题从而显著降低了计算复杂度并提高了整体运行效率此外在每一次迭代过程中仅需更新涉及这k+1个单元的相关信息即可完成模型参数的优化这一设计不仅简化了算法流程还能有效提升训练效率
- 如何选取负样本?
- 根据文本中单词的词频取样,缺点是导致一些经常出现的词(比如:冠词、代词等)出现在负取样中偏多,导致模型经常做无谓的训练。
- 采用均匀分布的随机取样,缺点是没有代表性。
- 这是论文中提到的一个效果不错的取样方法(基于实践的经验吧),有如下公式:
P(\omega _{i})=\frac{f(\omega _{i})^{3/4}}{\sum _{j=1}^{n}f(\omega _{j})^{3/4}}
既然单纯用词频会导致某些我们不希望出现的词出现很多次,均匀分布又不具代表性,那么何不采取一种折中的办法,这个办法就是加权,用某个词词频的3/4次方比上所有词词频的3/4次方作为该词出现的频率。这个方法在实际应用中合情合理,效果不错。
其他细节
学习短语
在语言学中,在某些情况下词语会常常同时出现在一起的现象较为常见,在这种情况下我们就可以认为它们构成短语关系。那么具体该如何量化评估它们之间的关系呢?我们可以采用以下数学公式进行计算:

获取两组词向量后,在计算得到的得分超过预设的临界值时(即score > threshold),我们就判定这两个词向量是高度相关的。
在考虑到较长短语的影响下(即需要捕捉更长范围的关系),我们采用2到4个连续词语组成训练样本,并逐步调低临界值。
代码以及注释
'''
code by Tae Hwan Jung(Jeff Jung) @graykode
'''
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
import matplotlib.pyplot as plt
#定义一个多维张量torch
dtype = torch.FloatTensor
# 3 Words Sentence
sentences = [ "i like dog", "i like cat", "i like animal",
"dog cat animal", "apple cat dog like", "dog fish milk like",
"dog cat eyes like", "i like apple", "apple i hate",
"apple i movie book music like", "cat dog hate", "cat dog like"]
#.join的用法是将元素以指定字符连接生成新的字符串,用split进行分割
word_sequence = " ".join(sentences).split()
word_list = " ".join(sentences).split()
#利用set的性质将重复的元素消去,之后再重新转化成list
word_list = list(set(word_list))
#enumerate是一个枚举的关键词,i是键,w是值,这样就将所有单词按照自然数编成字典
word_dict = {w: i for i, w in enumerate(word_list)}
# Word2Vec Parameter
batch_size = 20 # batch size 一批训练20组sample
embedding_size = 2 # 词嵌入的维数
voc_size = len(word_list) #字典的大小
def random_batch(data, size):
random_inputs = []
random_labels = []
#随机选取sample
random_index = np.random.choice(range(len(data)), size, replace=False)
for i in random_index:
#构建输入对应的one hot-vec
random_inputs.append(np.eye(voc_size)[data[i][0]]) # target
random_labels.append(data[i][1]) # context word
return random_inputs, random_labels
# Make skip gram of one size window
#构建输入的samples
skip_grams = []
for i in range(1, len(word_sequence) - 1):
target = word_dict[word_sequence[i]]
context = [word_dict[word_sequence[i - 1]], word_dict[word_sequence[i + 1]]]
for w in context:
skip_grams.append([target, w])
# Model
class Word2Vec(nn.Module):
#模型初始化,固定格式记住
def __init__(self):
super(Word2Vec, self).__init__()
# W and WT is not Traspose relationship
#初始化从输入到隐藏层的嵌入矩阵,参数随机初始化在(-1,1]
self.W = nn.Parameter(-2 * torch.rand(voc_size, embedding_size) + 1).type(dtype) # voc_size > embedding_size Weight
#随机初始化从隐藏层到输出层的系数矩阵,参数随机初始化在(-1,1]
self.WT = nn.Parameter(-2 * torch.rand(embedding_size, voc_size) + 1).type(dtype) # embedding_size > voc_size Weight
#定义前向传播,记得self也要作为参数传入
def forward(self, X):
# X : [batch_size, voc_size]
hidden_layer = torch.matmul(X, self.W) # hidden_layer : [batch_size, embedding_size]
output_layer = torch.matmul(hidden_layer, self.WT) # output_layer : [batch_size, voc_size]
return output_layer
model = Word2Vec()
#定义损失函数和优化
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Training
for epoch in range(5000):
input_batch, target_batch = random_batch(skip_grams, batch_size)
#被定义为Varialbe类型的变量可以认为是一种常量,在pytorch反向传播过程中不对其求导
input_batch = Variable(torch.Tensor(input_batch))
target_batch = Variable(torch.LongTensor(target_batch))
optimizer.zero_grad()
output = model(input_batch)
# output : [batch_size, voc_size], target_batch : [batch_size] (LongTensor, not one-hot)
loss = criterion(output, target_batch)
if (epoch + 1)%1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()
for i, label in enumerate(word_list):
W, WT = model.parameters()
x,y = float(W[i][0]), float(W[i][1])
plt.scatter(x, y)
plt.annotate(label, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom')
plt.show()
