0%

Word2Vec 的原理与实现

由于 Word2Vec 易于理解和使用,而被认为是自然语言处理发展中最大的突破之一。Word2Vec 是是一种词嵌入技术,用于单词转化为词向量。它的核心思想是预测中心词与上下文的关系,其中有两种实现方式,分别是 Skip-gram 和 CBOW。前者通过中心词预测上下文,后者通过上下文预测中心词。无论使用哪种方法实现,Word2Vec 都是属于无监督学习。

1. Word2Vec 原理与过程

1.1 Skip-gram 模型

Skip-gram 的思想是通过中心词预测上下文

从图看出 skip-gram 是由三层结构组成。分别是输入,隐藏和输出层。其中 \(N\) 表示向量长度,\(V\) 表示 vocabulary size,\(C\) 为上下文的单词数量。

  • 输入层:把中心词 \(x_k\) 用 one-hot 向量化表示
  • 隐藏层:中心词乘以权重矩阵 \(W_{V \times N}\),由于中心词 \(x_k\) 是个 one-hot 向量(中心词索引位置为 1 其余为 0),所以相乘结果相当于提取 \(W_{V \times N}\) 中对应的那一行,也就是这个词的 embedding 表示。\(W_{V \times N}\) 就是平时说的 word embedding
  • 输出层:隐藏层结果再乘以权重矩阵 \(W'_{N \times V}\) 得到的向量,然后在经过 \(softmax\) 函数得到每个单词的概率。预测结果有 \(C\) 个,所以要乘 \(n\)\(W'_{N \times V}\), 输出有 \(C\) 个向量,每个向量长度为 \(V\)。(其实可以放在一起计算,输出结果成了输出概率最高的 \(C\) 个单词)

1.2 CBOW 模型

CBOW 的思想是通过上下文预测中心词

与 skip-gram 一样由三层结构组成,\(N\) 表示向量长度,\(V\) 表示 vocabulary size,\(C\) 为上下文的单词数量。

  • 输入层:把上下文单词 \(x_{1k}, x_{2k}, \cdots, x_{Ck}\) 转换为 one-hot 向量
  • 隐藏层:将上下文 one-hot 向量乘以权重矩阵 \(W_{V \times N}\) 得到 \(C\) 个向量,并将这 \(C\) 个向量求平均值等于隐藏层向量 \(h_i\)
  • 输出层:隐藏向量 \(h_i\) 乘以 \(W'_{N \times V}\) 得到 \(V\) 维向量,然后再经过 \(softmax\) 激活函数得到每个词的概率所组成的向量,也即是 \(y_i\)

2. negative sampling 和 hierarchical softmax 优化方法

由于无论使用 Skip-gram 还是使用 CBOW,每个词预测一次都需要计算一遍 softmax,而 softmax 每次的输出范围与词汇大小相同。所以导致计算 Word2Vec 时,运算速度过慢。因此可以用 hierarchical softmax 或者 negative sampling 方法提升训练速度。

2.1 negative sampling

2.1.1 negative sampling 是什么

negative sampling(负采样)本质是预测总体类别的一个子集。negative sampling 每次让一个训练样本仅仅更新一小部分的权重参数,从而降低梯度下降过程中的计算量。举个例子:如果词汇大小为 1 万时,当输入样本("fox", "quick")到神经网络时,“fox” 经过 one-hot 编码,在输出层我们期望对应 “quick” 单词的那个神经元结点输出 1,其余 9999 个都应该输出 0。在这里,这9999个我们期望输出为0的神经元结点所对应的单词我们为 negative word。negative sampling 的想法也很直接,将随机选择一小部分的 negative words,比如选 10个 negative words 来更新对应的权重参数。在论文中作者指出指出对于小规模数据集,建议选择 5~20 个 negative words,对于大规模数据集选择 2~5 个 negative words。

2.1.2 如何选择 negative words

我们使用“一元模型分布(unigram distribution)”来选择 “negative words”。要注意的一点是,一个单词被选作 negative sample 的概率跟它出现的频次有关,出现频次越高的单词越容易被选作 negative words。在 word2vec 的 C 语言实现中,你可以看到对于这个概率的实现公式。每个单词被选为 “negative words” 的概率计算公式与其出现的频次有关。代码中的公式实现如下: \[ \begin{aligned} P(w_i) = \frac{f(w_i)^{\frac{3}{4}}}{ {\textstyle \sum_{j=0}^{n}} (f(w_j)^{\frac{3}{4}})} \end{aligned} \] 每个单词被赋予一个权重,即 \(f(w_i)\) ,它代表着单词出现的频次。公式中的开 \(\frac{3}{4}\) 次方完全是基于经验的,论文中提到这个公式的效果要比其它公式更加出色。你可以在google的搜索栏中输入 “plot y = x^(3/4) and y = x”,然后看到下面这幅图,仔细观察 \(x\) 在 [0,1] 区间内时 \(y\) 的取值。函数 \(y=x^{\frac{3}{4}}\) 在区间 [0,1] 上有一小段弧形,取值在函数 \(y=x\) 之上。

负采样的 C 语言实现非常的有趣。unigram table 有一个包含了一亿个元素的数组,这个数组是由词汇表中每个单词的索引号填充的,并且这个数组中有重复,也就是说有些单词会出现多次。那么每个单词的索引在这个数组中出现的次数该如何决定呢,有公式,也就是说计算出的**负采样概率*1亿=单词在表中出现的次数**。有了这张表以后,每次去我们进行负采样时,只需要在0-1 亿范围内生成一个随机数,然后选择表中索引号为这个随机数的那个单词作为我们的 negative word 即可。一个单词的负采样概率越大,那么它在这个表中出现的次数就越多,它被选中的概率就越大。

2.2 hierarchical softmax

hierarchical softmax(层次 softmax)本质是把 \(N\) 分类问题变成 \(log(N)\) 次二分类(待完善)

Word2Vec 训练 tick

Windows size 设置:

  • 设置较大会提取更多的 topic 信息
  • 设置较小会更加关注词本身

一般设置 5,但是有些任务中 Windows size 为 2 的效果会更好,比如某些英语预料的短文本任务(Windows size 并非越大越好)

4. 代码实现

这里用 Pytorch 实现 Skip-gram 模型

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
27
class SkipGramModel(nn.Module):

def __init__(self, emb_size, emb_dimension):
super(SkipGramModel, self).__init__()
self.emb_size = emb_size
self.emb_dimension = emb_dimension
self.u_embeddings = nn.Embedding(emb_size, emb_dimension, sparse=True)
self.v_embeddings = nn.Embedding(emb_size, emb_dimension, sparse=True)

initrange = 1.0 / self.emb_dimension
init.uniform_(self.u_embeddings.weight.data, -initrange, initrange)
init.constant_(self.v_embeddings.weight.data, 0)

def forward(self, pos_u, pos_v, neg_v):
emb_u = self.u_embeddings(pos_u)
emb_v = self.v_embeddings(pos_v)
emb_neg_v = self.v_embeddings(neg_v)

score = torch.sum(torch.mul(emb_u, emb_v), dim=1)
score = torch.clamp(score, max=10, min=-10)
score = -F.logsigmoid(score)

neg_score = torch.bmm(emb_neg_v, emb_u.unsqueeze(2)).squeeze()
neg_score = torch.clamp(neg_score, max=10, min=-10)
neg_score = -torch.sum(F.logsigmoid(-neg_score), dim=1)

return torch.mean(score + neg_score)

参考