word2vec的基本实现
word2vec
1.基于推理的方法和神经网络
用向量表示单词的研究最近正在如火如荼地展开,其中比较成功的方法大致可以分为两种:一种是基于计数的方法;另一种是基于推理的方法。
虽然两者在获得单词含义的方法上差别很大,但是两者的背景都是分布式假设。
1.1 基于计数的方法的问题
基于计数的方法根据一个单词周围的单词的出现频数来表示该单词。具体来说,先生成所有单词的共现矩阵,再对这个矩阵进行SVD,以获得密集向量(单词的分布式示)
但是在现实操作中,语料库处理的单词数量非常大。假如词汇量超过100万个,那么基于计数的方法就需要生成一个100w * 100w的庞大矩阵,对于如此庞大的矩阵生成SVD显然是不现实的
基于计数的方法使用整个语料库的统计数据(共现矩阵和PPMI等),
通过一次处理(SVD等)获得单词的分布式表示。
而基于推理的方法使用神经网络,通常在mini-batch上进行学习,这意味着神经网络一次只需要看一部份学习数据(mini-batch),并反复更新权重。这种学习机制上的差异如下图所示

基于计数的方法会一次性处理全部数据,但是基于推理的方法使用部分学习数据逐步学习。这意味着在面对词汇量很大的语料库时,对于SVD难以处理的计算,神经网络可以在部分数据上学习。
1.2 基于推理方法的概要
基于推理的方法的主要操作是“推理”。如图示,当给出周围的单词(上下文)时,预测“?”处会出现什么单词,这就是推理。

推理问题并学习规律,就是基于推理的方法的主要任务。通过反复求解这些推理问题,可以学习到单词的出现模式。

如图所示,基于推理的方法引入了某种模型,我们将神经网络用于此模型。这个模型接收上下文信息作为输入,并输出(可能出现的)各个单词的出现概率。在这样的框架中,使用语料库来学习模型,使之能做出正确的预测。另外,作为模型学习的产物,我们得到了单词的分布式表示。这就是基于推理的方法的全貌。
1.3 神经网络中的单词处理方法
神经网络无法直接处理单词,如果需要使用神经网络处理单词,需要先将单词转化成固定长度的向量。
对此一种方式是将单词转换为one-hot向量。在one-hot表示中只有一个元素是1,其他元素都是0
一个one-hot向量的例子
我们用“You say goodbye and I say hello.”这个一句话的语料库来说明。在这个语料库中,一共有7 个单词(“you“ “say” “goodbye” “and” “i” “hello” “.”)。此时,各个单词可以转化为图所示的one-hot 表示。

单词可以表示为文本、单词ID 和one-hot 表示。此时,要将单词转化为one-hot 表示,就需要准备元素个数与词汇个数相等的向量,并将单词ID 对应的元素设为1,其他元素设为0。像这样,只要将单词转化为固定长度的向量,神经网络的输入层的神经元个数就可以固定下来.

输入层由7个神经元表示,分别对应于7个单词(第1个神经元对于you,第2个神经元对应于say)
现在事情变得很简单了。因为只要将单词表示为向量,这些向量就可以由构成神经网络的各种“层”来处理。比如,对于one-hot 表示的某个单词,使用全连接层对其进行变换的情况如图所示。

全连接层通过箭头连接所有节点。这些箭头拥有权重(参数),它们和输入层神经元的加权和成为中间层的神经元。
这里的全连接层变换可以写成如下的python代码
import numpy as np
c = np.array([[1, 0, 0, 0, 0, 0, 0]]) # 输入
W = np.random.randn(7, 3) # 权重
h = np.dot(c, W) # 中间节点
print(h)
# [[-1.19224042 -1.48380426 -1.22824923]]
这段代码将单词ID 为0 的单词表示为了one-hot 表示,并用全连接层对其进行了变换。全连接层的计算通过矩阵乘积进行。这可以用NumPy的np.dot()来实现
2. 简单的word2vec
在上面的内容中了解了基于推理的方法和神经网络中单词的处理方法,现在将实现简单的word2vec
我们将神经网络嵌入到上一节提到的模型中。这里我们使用原版word2vec提出的continuous bag-of-words(CBOW)的模型作为神经网络
2.1 CBOW模型的推理
CBOW 模型是根据上下文预测目标词的神经网络
“目标词”是指中间的单词,它周围的单词是“上下文”
通过训练这个CBOW 模型,使其能尽可能地进行正确的预测,我们可以获得单词的分布式表示。
CBOW 模型的输入是上下文。这个上下文用['you', 'goodbye'] 这样的单词列表表示。我们将其转换为one-hot 表示,以便CBOW 模型可以进行处理。

CBOW的网络结构如上图所示,它有两个输入层,经过中间层到达输出层。从输入层到中间层的变换由相同的全连接层(权重为 \(W_{in}\) )完成,从中间层到输出层神经元的变换由另一个全连接层(权重为\(W_{out}\))完成。
我们再来观察一下CBOW的中间层。中间层的神经元是各个输入层经全连接层变换后得到的值的“平均”。就上面的例子而言,经全连接层变换后,第1个输入层转化为\(h_1\),第2个输入层转化为\(h_2\),那么中间层的神经元是\(\frac{1}{2}(h_1 + h_2)\)。
在上图的CBOW中的输出层有7个神经元。这里重要的是,这些神经元对应于各个单词。输出层的神经元是各个单词的得分,它的值越大,说明对应单词的出现概率就越高。得分是指在被解释为概率之前的值,对这些得分应用Softmax函数,就可以得到概率。
从输入层到中间层的变换由全连接层(权重是\(W_{in}\))完成。此时,全连接层的权重\(W_{in}\)是一个\(7 × 3\)的矩阵。这个权重就是我们要的单词的分布式表示,如图所示。

权重\(W_{in}\) 的各行保存着各个单词的分布式表示。通过反复学习,不断更新各个单词的分布式表示,以正确地从上下文预测出应当出现的单词。令人惊讶的是,如此获得的向量很好地对单词含义进行了编码。这就是word2vec的全貌。
中间层的神经元数量比输入层少这一点很重要。 中间层需要将预测单词所需的信息压缩保存,从而产生密集的向量表示。这时,中间层被写入了我们人类无法解读的代码,这相当于“编码”工作。而从中间层的信息获得期望结果的过程则称为“解码”。这一过程将被编码的信息复原为我们可以理解的式。
到目前为止,我们从神经元视角图示CBOW 模型。下面,我们从层视角图示CBOW 模型。

MatMul层就是全连接层,它的作用就是将输入的ont-hot向量和权重W进行点积
CBOW 模型一开始有两MatMul 层,这两个层的输出被加在一起。然后,对这个相加后得到的值乘以0.5 求平均,可以得到中间层的神经元。最后,将另一个MatMul 层应用于中间层的神经元,输出得分。
下面来实现CBOW的模型推理
MatMul的实现
class MatMul:
def __init__(self, W):
self.params = [W]
self.grads = [np.zeros_like(W)]
self.x = None
def forward(self, x):
W, = self.params
out = np.dot(x, W)
self.x = x
return out
def backward(self, dout):
W, = self.params
dx = np.dot(dout, W.T)
dW = np.dot(self.x.T, dout)
self.grads[0][...] = dW
return dx
CBOW的实现
import sys
sys.path.append('../domeCode/')
import numpy as np
from common.layers import MatMul
# 样本的上下文数据
c0 = np.array([[1, 0, 0, 0, 0, 0, 0]])
c1 = np.array([[0, 0, 1, 0, 0, 0, 0]])
# 权重的初始值
W_in = np.random.randn(7, 3)
W_out = np.random.randn(3, 7)
# 生成层
in_layer0 = MatMul(W_in)
in_layer1 = MatMul(W_in)
out_layer = MatMul(W_out)
# 正向传播
h0 = in_layer0.forward(c0)
h1 = in_layer1.forward(c1)
h = 0.5 * (h0 + h1)
s = out_layer.forward(h)
print(s)
这里,我们首先将必要的权重(\(W_{in}\) 和\(W_{out}\))初始化。然后,生成与上下文单词数量等量(这里是两个)的处理输入层的MatMul 层,输出侧仅生成一个MatMul层。需要注意的是,输入侧的MatMul 层共享重\(W_{in}\)。
之后,输入侧的MatMul 层(in_layer0 和in_layer1)调用forward()方法,计算中间数据,并通过输出侧的MatMul层(out_layer)计算各个单词的得分。
这里我们见到的CBOW 模型是没有使用激活函数的简单的网络结构。除了多个输入层共享权重外,并没有什么难点。
2.2 CBOW模型的学习
到目前为止,我们介绍的CBOW模型在输出层输出了各个单词的得分。通过对这些得分应用Softmax函数,可以获得概率。这个概率表示哪个单词会出现在给定的上下文(周围单词)中间。

在上图的例子中,上下文是you 和goodbye,正确解标签(神经网络应该预测出的单词)是say。这时,如果网络具有“良好的权重”,那么在表示概率的神经元中,对应正确解的神经元的得分应该更高。
CBOW 模型的学习就是调整权重,以使预测准确。其结果是,权重\(W_{in}\)(确切地说是\(W_{in}\)和\(W_{out}\)两者)学习到蕴含单词出现模式的向量。
根据过去的实验,CBOW 模型(和skip-gram 模型)得到的单词的分布式表示,特别是使用维基百科等大规模语料库学习到的单词的分布式表示,在单词的含义和语法上符合我们直觉的案例有很多。
现在来考虑一下上述的神经网络的学习。CBOW的网络本质上是一个进行多类别的神经网络,所以对其进行学习只是使用Softmax函数和交叉熵误差。
首先,使用softmax函数将得分转换为概率,再求这些概率和监督标签之间的交叉熵误差,将其作为损失进行学习。学习过程如下所示

只需向上一节介绍的进行推理的CBOW 模型加上Softmax 层和 Cross Entropy Error 层,就可以得到损失。这就是CBOW模型计算损失的流程,对应于神经网络的正向传播。
2.3 word2vec的权重和分布式表示
如前所述,word2vec 中使用的网络有两个权重,分别是输入侧的全连接层的权重(\(W_{in}\))和输出侧的全连接层的权重(\(W_{out}\))。一般而言,输入侧的权重\(W_{in}\) 的每一行对应于各个单词的分布式表示。另外,输出侧的权重\(W_{out}\) 也同样保存了对单词含义进行了编码的向量。

如上图所示。输出侧的权重在列方向上保存了各个单词的分布式表示
那么,我们最终应该使用哪个权重作为单词的分布式表示呢?这里有三个选项。
- 只使用输入侧的权重
- 只使用输出侧的权重
- 同时使用两个权重
方案A和方案B只使用其中一个权重。而在采用方案C的情况下,根据如何组合这两个权重,存在多种方式,其中一个方式就是简单地将这两个权重相加。
就word2vec(特别是skip-gram 模型)而言,最受欢迎的是方案A。许多研究中也都仅使用输入侧的权重\(W_{in}\)作为最终的单词的分布式表示。遵循这一思路,我们也使用\(W_{in}\)作为单词的分布式表示。
3. 学习数据的准备
在开始word2vec的学习之前,先来准备学习需要的数据。这里仍以"You say goodbye and I say hello." 这个只有一句话的语料库为例进行说明
3.1 上下文和目标词
word2vec中使用的神经网络的输入是上下文,它的正确解标签是被这些上下文包围在中间的单词,即目标词。
也就是说,当向神经网络输入上下文时,时目标词出现的概率高,为了达成这一目标进而进行学习
现在,我们从语料库生成上下文和目标词

将语料库中的目标单词作为目标词,将其周围的单词作为上下文提取出来。我们对语料库中的所有单词都执行该操作(两端的单词除外),
图左侧的contexts(上下文)和target(目标词)。context的各行形成神经网络的输入,target各行成为正确解标签(要预测的单词)。另外,在各笔样本数据中,上下文有多个单词(这个例子中有两个),而目标词则只有一个,因此只有上下文写成了复数形式contexts。
现在来实现从语料库生成上下文和目标词的函数。首先,将语料库的文本转化成单词ID。然后从单词ID列表corpus生成contexts和target

contexts是二维数组。此时,contexts 的第0维保存的是各个上下文数据。具体来说,contexts[0] 保存的是第0个上下文,context[1] 保存的是第1 个上下文……同样地,就目标词而言target[0]保存的是第0 个目标词,target[1]保存的是第1 个目标词……
现在,来实现生成上下文和目标词的函数
def create_context_target(corpus, window_size=1):
# 去头去尾
target = corpus[window_size:-window_size]
context = []
for idx in range(window_size, len(corpus)-window_size):
cs = []
for t in range(-window_size, window_size + 1):
if t == 0:
continue
cs.append(corpus[idx + t])
context.append(cs)
return np.array(context), np.array(target)
这样就从语料库生成了上下文和目标词,后面只需将它们赋给CBOW模型即可。不过,因为这些上下文和目标词的元素还是单词ID,所以还需要将它们转化为one-hot 表示。
3.2 转换为one-hot表示
我们将上下文和目标词转化为one-hot表示

上下文和目标词从单词ID 转化为了one-hot 表示。这里需要注意各个多维数组的形状
def convert_one_hot(corpus, vocab_size):
'''转换为one-hot表示
:param corpus: 单词ID列表(一维或二维的NumPy数组)
:param vocab_size: 词汇个数
:return: one-hot表示(二维或三维的NumPy数组)
'''
N = corpus.shape[0]
if corpus.ndim == 1:
one_hot = np.zeros((N, vocab_size), dtype=np.int32)
for idx, word_id in enumerate(corpus):
one_hot[idx, word_id] = 1
elif corpus.ndim == 2:
C = corpus.shape[1]
one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
for idx_0, word_ids in enumerate(corpus):
for idx_1, word_id in enumerate(word_ids):
one_hot[idx_0, idx_1, word_id] = 1
return one_hot
4. CBOW模型的实现

现在,我们来实现CBOW模型。这里要实现的神经网络如图所示
我们将图中的神经网络实现为SimpleCBOW类(下一章将实现对其进行了改进的CBOW 类)。
先实现SimpleCBOW的初始化
class SimpleCBOW:
def __init__(self, vocab_size, hidden_size):
V,H = vocab_size, hidden_size
# 初始化权重
W_in = 0.01 * np.random.randn(V,H).astype('f')
W_out = 0.01 * np.random.randn(H,V).astype('f')
# 生成层
self.in_layer0 = MatMul(W_in)
self.in_layer1 = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer = SoftmaxWithLoss()
# 将所有权重和梯度整理到列表中
layers = [self.in_layer0, self.in_layer1, self.out_layer]
self.params, self.grads =[],[]
for layer in layers:
self.params += layer.params
self.grads += layer.grads
# 将单词的分布式表示设置成为成员变量
self.word_vecs = W_in
这里,初始化方法的参数包括词汇个数vocab_size和中间层的神经元个数hidden_size。关于权重的初始化,首先我们生成两个权重(W_in 和W_out),并用一些小的随机值初始化这两个权重。此外,我们指定NumPy数组的数据类型为astype('f'),这样一来,初始化将使用32位的浮点数。
接着,我们创建必要的层。首先,生成两个输入侧的MatMul层、一个输出侧的MatMul层,以及一个Softmax with Loss层。这里,用来处理输入侧上下文的MatMul层的数量与上下文的单词数量相同(本例中是两个)。另外,我们使用相同的权重来初始化MatMul层。
最后,将该神经网络中使用的权重参数和梯度分别保存在列表类型的成员变量params和grads中。
这里,多个层共享相同的权重。因此,params列表中存在多个相同的权重。但是,在params列表中存在多个相同的权重的情况下,Adam、Momentum 等优化器的运行会变得不符合预期,在更新参数时会进行简单的去重操作。
接下来实现神经网络的正向传播
def forward(self, context, target):
h0 = self.in_layer0.forward(context[:,0])
h1 = self.in_layer1.forward(context[:,1])
h = (h0 + h1) * 0.5
score = self.out_layer.forward(h)
loss = self.loss_layer.forward(score, target)
return loss
我们假定参数contexts 是一个三维NumPy数组,即上一节的例子中(6,2,7) 的形状,其中第0维的元素个数是mini-batch的数量,第1维的元素个数是上下文的窗口大小,第2 维表示one-hot 向量。 此外,target 是(6,7) 这样的二维形状。
finally,来整一下反向传播backward()
计算图如下所示

神经网络的反向传播在与正向传播相反的方向上传播梯度。这个反向传播从1出发,并将其传向Softmax with Loss层。然后,将Softmax with Loss层的反向传播的输出ds传到输出侧的MatMul层。
之后就是+和×运算的反向传播。×的反向播将正向传播时的输入值“交换”后乘以梯度。+的反向传播则将梯度“原样”传播。我们按照图来实现反向传播
def backward(self, dout = 1):
ds = self.loss_layer.backward(dout)
da = self.out_layer.backward(ds)
da *= 0.5
self.in_layer1.backward(da)
self.in_layer0.backward(da)
return None
至此,反向传播的实现就结束了。我们已经将各个权重参数的梯度保存在了成员变量grads 中。因此, 通过先调用forward()函数, 再调用backward()函数,grads列表中的梯度被更新。
下面,我们继续看一下SimpleCBOW类的学习。
学习的实现
CBOW 模型的学习和一般的神经网络的学习完全相同。首先,给神经网络准备好学习数据。然后,求梯度,并逐步更新权重参数。
import sys,os
sys.path.append('..')
from common.trainer import Trainer
from common.optimizer import Adam
from domeCode.ch03.simple_cbow import SimpleCBOW
from common.util import preprocess, create_contexts_target, convert_one_hot
window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000
text = 'You say goodbye and I say hi'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
context, target = create_contexts_target(corpus, window_size)
target = convert_one_hot(target, vocab_size)
context = convert_one_hot(context, vocab_size)
model = SimpleCBOW(vocab_size, hidden_size)
optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(context, target, max_epoch, batch_size)
trainer.plot()

通过不断学习,损失在减小,看起来学习进行得一切正常。我们来看一下学习结束后的权重参数。这里,我们取出输入侧MatMul 层的权重,实际确认一下它的内容。因为输入侧的MatMul 层的权重已经赋值给了成员变量word_vecs,所以接着上面的代码,我们追加下面的代码。
word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
print(word, word_vecs[word_id])
这里,使用word_vecs 这个变量保存权重。word_vecs 的各行保存了对应的单词ID 的分布式表示。实际运行一下,可以得到下述结果。
you [-0.9031807 -1.0374491 -1.4682057 -1.3216232 0.93127245]
say [ 1.2172916 1.2620505 -0.07845993 0.07709391 -1.2389531 ]
goodbye [-1.0834033 -0.8826921 -0.33428606 -0.5720131 1.0488235 ]
and [ 1.0244362 1.0160093 -1.6284224 -1.6400533 -1.0564581]
i [-1.0642933 -0.9162385 -0.31357735 -0.5730831 1.041875 ]
hello [-0.9018145 -1.035476 -1.4629668 -1.3058501 0.9280102]
. [ 1.0985303 1.1642815 1.4365371 1.3974973 -1.0714306]
至此我们得到了单词密集向量表示,这就是单词的分布式表示。不过,遗憾的是,这里使用的小型语料库并没有给出很好的结果。当然,主要原因是语料库太小了。如果换成更大、更实用的语料库,相信会获得更好的结果。但是,这样在处理速度方面又会出现新的问题,这是因为当前这个CBOW 模型的实现在处理效率方面存在几个问题。
下一次我们将改进这个简单的CBOW 模型,实现一个“真正的”CBOW 模型。
5. word2vec的补充说明
这里我们从概率的角度来看一下CBOW模型
5.1 CBOW模型和概率
联合概率记为\(P(A,B)\),表示A,B事件同时发生的概率
后验概率记为\(P(A|B)\),字面意思是“事件发生后的概率”。从另一个角度来看,也可以解释为“在给定事件B(的信息)时事件A 发生的概率”
现在来康康用概率的表示方法,也就是数学方法来描述CBOW模型。
CBOW 模型进行的处理是,当给定某个上下文时,输出目标词的概率。这里,我们使用包含单词\(w_1, w_2, · · ·, w_T\)的语料库。如图所示,对第 个单词,考虑窗口大小为1 的上下文。

用数学式来表示当给定上下文\(w_{t−1}\) 和\(w_{t+1}\) 时目标词为\(w_t\)的概率。使用后验概率,有式
上式表示在\(w_{t-1}\)和\(w_{t+1}\)发生后,\(w_t\)发生的概率
也可以解释为当给定\(w_{t-1}\)和\(w_{t+1}\)时,\(w_t\)发生的概率
也就是说,CBOW 模型可以建模为式(5.1)
这里, 使用式(5.1) 可以简洁地表CBOW 模型的损失函数。把交叉熵误差函数套用在这里,交叉熵误差函数表示为:
其中\(y_k\)表示第\(k\)个事件发生的概率,\(t_k\)是监督标签,是one-hot向量的元素。当\(w_t\)之外的事件发生时,对应的one-hot 向量的元素均为0。考虑到这一点,可以推导出下式:
CBOW 模型的损失函数只是对式(5.1) 的概率取log,并加上负号。这称为负对数似然(negative log likelihood)。式(5.2) 是一笔样本数据的损失函数。如果将其扩展到整个语料库,则损失函数可以写为:
CBOW 模型学习的任务就是让式(5.3) 表示的损失函数尽可能地小。那时的权重参数就是我们想要的单词的分布式表示。这里,我们只考虑了窗口大小为1 的情况,不过其他的窗口大小(或者窗口大小为m的一般情况)也很容易用数学式表示。
5.2 skip-gram模型
word2vec 有两个模型:一个是我们已经讨论过的CBOW模型;另一个是被称为skip-gram 的模型。skip-gram 是反转了CBOW 模型处理的上下文和目标词的模型。
举例来说,两者要解决的问题如图

CBOW 模型从上下文的多个单词预测中间的单词(目标词),而skip-gram 模型则从中间的单词(目标词)预测周围的多个单词(上下文)。此时,skip-gram 模型的网络结构如图

skip-gram 模型的输入层只有一个,输出层的数量则与上下文的单词个数相等。因此,首先要分别求出各个输出层的损失(通过Softmax with Loss 层等),然后将它们加起来作为最后的损失。
现在,我们使用概率的表示方法来表示skip-gram 模型。我们来考虑根据中间单词(目标词)wt 预测上下文wt−1 和wt+1 的情况。此时,skipgram可以建模为式(5.4):
式(5.4) 表示“当给定\(w_t\) 时,\(w_{t−1}\) 和\(w_{t+1}\) 同时发生的概率。这里,在skip-gram 模型中,假定上下文的单词之间没有相关性(正确地说是假定
“条件独立”),将式(5.4) 如下进行分解:
通过将式(5.5) 代入交叉熵误差函数,可以推导出skip-gram 模型的损失函数:
这里利用了对数的性质\(\log xy = \log x + \log y\)
skip-gram模型的损失函数先分别求出各个上下文对应的损失,然后将它们加在一起。式(5.6) 是一笔样本数据的损失函数。如果扩展到整个语料库,则skip-gram 模型的损失函数可以表示为式(5.7):
比较式(5.7) 和CBOW 模型的式(5.3),差异是非常明显的。因为skip-gram模型的预测次数和上下文单词数量一样多,所以它的损失函数需要求各个上下文单词对应的损失的总和。而CBOW模型只需要求目标词的损失。
那么,我们应该使用CBOW 模型和skip-gram 模型中的哪一个呢?答案应该是skip-gram 模型。这是因为,从单词的分布式表示的准确度来看,在大多数情况下,skip-grm 模型的结果更好。特别是随着语料库规模的增大,在低频词和类推问题的性能方面,skip-gram 模型往往会有更好的表现。此外,就学习速度而言,CBOW 模型比skip-gram 模型要快。这是因为skip-gram 模型需要根据上下文数量计算相应个数的损失,计算成本变大。
理解了CBOW 模型的实现,在实现skip-gram 模型时应该就不存在什么难点了。因此,这里给出简单的skip-gram 模型的实现,代码如下:
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss
class SimpleSkipGram:
def __init__(self, vocab_size, hidden_size):
V, H = vocab_size, hidden_size
# 初始化权重
W_in = 0.01 * np.random.randn(V, H).astype('f')
W_out = 0.01 * np.random.randn(H, V).astype('f')
# 生成层
self.in_layer = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer1 = SoftmaxWithLoss()
self.loss_layer2 = SoftmaxWithLoss()
# 将所有的权重和梯度整理到列表中
layers = [self.in_layer, self.out_layer]
self.params, self.grads = [], []
for layer in layers:
self.params += layer.params
self.grads += layer.grads
# 将单词的分布式表示设置为成员变量
self.word_vecs = W_in
def forward(self, contexts, target):
h = self.in_layer.forward(target)
s = self.out_layer.forward(h)
l1 = self.loss_layer1.forward(s, contexts[:, 0])
l2 = self.loss_layer2.forward(s, contexts[:, 1])
loss = l1 + l2
return loss
def backward(self, dout=1):
dl1 = self.loss_layer1.backward(dout)
dl2 = self.loss_layer2.backward(dout)
ds = dl1 + dl2
dh = self.out_layer.backward(ds)
self.in_layer.backward(dh)
return None
5.3 基于计数和基于推理
到目前为止,我们已经了解了基于计数的方法和基于推理的方法(特别是word2vec)。两种方法在学习机制上存在显著差异:基于计数的方法通过对整个语料库的统计数据进行一次学习来获得单词的分布式表示,而基于推理的方法则反复观察语料库的一部分数据进行学习(mini-batch 学习)。这里,我们就其他方面来对比一下这两种方法。
首先,考虑需要向词汇表添加新词并更新单词的分布式表示的场景
此时基于计数的场景需要从头开始计算,就算是想稍微更改单词的分布式表示,就需要重新进行计算共现矩阵,ppmi矩阵进行SVD等一系列操作。
相反在使用基于推理的方法时候允许参数的增量学习。具体来说,可以将之前学习到的权重作为下一次学习的初始值,在不损失之前学习到的经验的情况下,高效地更新单词的分布式表示。在这方面,基于推理的方法(word2vec)具有优势。
其次,两种方法得到的单词的分布式表示的性质和准确度有些差异呢。就分布式表示的性质而言,基于计数的方法主要是编码单词的相似性,而word2vec(特别是skip-gram 模型)除了单词的相似性以外,还能理解更复杂的单词之间的模式。关于这一点,word2vec 因能解开\(king −man + woman = queen\)这样的类推问题而知名
这里有一个常见的误解,那就是基于推理的方法在准确度方面优于基于计数的方法。实际上,有研究表明,就单词相似性的定量评价而言,基于推理的方法和基于计数的方法难分上下。
另外一个重要的事实是,基于推理的方法和基于计数的方法存在关联性,这两个方法论(在某些条件下)是“相通”的。
此外,在word2vec 之后,有研究人员提出了GloVe 方法。GloVe方法融合了基于推理的方法和基于计数的方法。该方法的思想是,将整个语料库的统计数据的信息纳入损失函数,进行mini-batch 学习(具体请参考论文GloVel)。据此,这两个方法论成功地被融合在了一起。
6.小结
这次我们详细解释了word2vec的CBOW模型,并对其进行了实现。CBOW模型基本上是一个2 层的神经网络,结构非常简单。我们使用MatMul 层和Softmax with Loss 层构建了CBOW 模型,并用一个小规模语料库确认了它的学习过程。 遗憾的是,现阶段的CBOW 模型在处理效率上还存在一些问题。不过,在理解了本章的CBOW 模型之后,离真正的word2vec 也就一步之遥了。下一次,我们将改进CBOW 模型。

浙公网安备 33010602011771号