第4章 word2vec的高速化
假设词汇量有 100 万个,CBOW 模型的中间层神经元有 100 个。输入层和输出层存在 100 万个神经元,在如此多的神经元的情况下,中间的计算过程需要很长时间。
本章将重点放在 word2vec 的加速上,来改善 word2vec。将对简单的 word2vec 进行两点改进:
- 引入名为 Embedding 层的新层;
- 引入名为 Negative Sampling 的新损失函数。
4.1 word2vec的改进①:引入名为 Embedding 层的新层
第 1 个问题与输入层的 one-hot 表示有关。这是因为我们用 one-hot 表示来处理单词,随着词汇量的增加,one-hot 表示的向量大小也会增加。此外,还需要计算 one-hot 表示和权重矩阵 W i n W_{in} Win 的乘积,这也要花费大量的计算资源。
如上图,one-hot 表示在 MatMul 层中的作用无非是将权重矩阵 W i n W_{in} Win 的某个特定的行取出来。
我们创建一个从权重参数中抽取“单词 ID 对应行(向量)”的层,这里我们称之为 Embedding 层。Embedding 来自“词嵌入”(word embedding)这一术语。也就是说,在这个 Embedding 层存放词嵌入(分布式表示)。
在自然语言处理领域,单词的密集向量表示称为 词嵌入(word embedding)或者单词的分布式表示(distributed representation)。
实现 Embedding 层:
class Embedding:
def __init__(self, W):
self.params = [W]
self.grads = [np.zeros_like(W)]
self.idx = None
def forward(self, idx):
W, = self.params
self.idx = idx
out = W[idx]
return out
def backward(self, dout):
dW, = self.grads
dW[...] = 0
for i, word_id in enumerate(self.idx):
dW[word_id] += dout[i]
# 或者
# np.add.at(dW, self.idx, dout) # 将 dout 加到 dW 指定 self.idx 行上
return None
将 word2vec(CBOW 模型)的实现中的输入侧的 MatMul 层换成 Embedding 层。这样一来,既能减少内存使用量,又能避免不必要的计算。
4.2 word2vec的改进②:引入名为 Negative Sampling 的新损失函数
第 2 个问题是中间层之后的计算。首先,中间层和权重矩阵 W o u t W_{out} Wout 的乘积需要大量的计算。其次,随着词汇量的增加,Softmax 层的计算量也会增加。
- 从多分类到二分类
将之前采用 Softmax 函数计算所有单词正确的概率的问题转化为求某一个单词为正确的概率问题,即二分类问题,以减少计算量。
要使用神经网络解决二分类问题,需要使用 sigmoid 函数将得分转化为概率。为了求损失,我们使用交叉熵误差作为损失函数。
- 负采样
除了计算正确单词的概率外,我们还需要错误的单词的概率。当前的神经网络只是学习了正例 say,但是对 say 之外的负例一无所知。而我们真正要做的事情是,对于正例(say),使 Sigmoid 层的输出接近 1;对于负例(say 以外的单词),使 Sigmoid 层的输出接近 0。
负采样方法既可以求将正例作为目标词时的损失,同时也可以采样(选出)若干个负例,对这些负例求损失。然后,将这些数据(正例和采样出来的负例)的损失加起来,将其结果作为最终的损失。
- 负采样的采样方法
基于语料库中单词使用频率的采样方法会先计算语料库中各个单词的出现次数,并将其表示为“概率分布”,然后使用这个概率分布对单词进行采样。
import numpy as np
# 从0到9的数字中随机选择一个数字
np.random.choice(10) # 7
np.random.choice(10) # 2
# 从words列表中随机选择一个元素
words = ['you', 'say', 'goodbye', 'I', 'hello', '.']
np.random.choice(words) # 'goodbye'
# 有放回采样5次
np.random.choice(words, size=5) # array(['goodbye', '.', 'hello', 'goodbye', 'say'], dtype='<U7')
# 无放回采样5次
np.random.choice(words, size=5, replace=False) # array(['hello', '.', 'goodbye', 'I', 'you'], dtype='<U7')
# 基于概率分布进行采样
p = [0.5, 0.1, 0.05, 0.2, 0.05, 0.1]
np.random.choice(words, p=p) # 'you'
4.3 改进版CBOW模型的实现
# coding: utf-8
import sys
sys.path.append('..')
from common.np import * # import numpy as np
from common.layers import Embedding
from ch04.negative_sampling_layer import NegativeSamplingLoss
class CBOW:
def __init__(self, vocab_size, hidden_size, window_size, corpus):
V, H = vocab_size, hidden_size
# 初始化权重
W_in = 0.01 * np.random.randn(V, H).astype('f')
W_out = 0.01 * np.random.randn(V, H).astype('f')
# 生成层
self.in_layers = []
for i in range(2 * window_size):
layer = Embedding(W_in) # 使用Embedding层
self.in_layers.append(layer)
self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)
# 将所有的权重和梯度整理到列表中
layers = self.in_layers + [self.ns_loss]
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 = 0
for i, layer in enumerate(self.in_layers):
h += layer.forward(contexts[:, i])
h *= 1 / len(self.in_layers)
loss = self.ns_loss.forward(h, target)
return loss
def backward(self, dout=1):
dout = self.ns_loss.backward(dout)
dout *= 1 / len(self.in_layers)
for layer in self.in_layers:
layer.backward(dout)
return None
4.4 word2vec小结
word2vec 对自然语言处理领域产生了很大的影响,基于它获得的单词的分布式表示被应用在了各种自然语言处理任务中。另外,不仅限于自然语言处理,word2vec 的思想还被应用在了语音、图像和视频等领域中。
CBOW 模型的学习旨在找到使损失函数(确切地说,是整个语料库的损失函数之和)最小的权重参数。只要找到了这样的权重参数,CBOW 模型就可以更准确地从上下文预测目标词。像这样,CBOW 模型的学习目的是从上下文预测出目标词。为了达成这一目标,随着学习的推进,(作为副产品)获得了编码了单词含义信息的单词的分布式表示。
继续阅读:
《深度学习进阶:自然语言处理(第1章)》-读书笔记
《深度学习进阶:自然语言处理(第2章)》-读书笔记
《深度学习进阶:自然语言处理(第3章)》-读书笔记
《深度学习进阶:自然语言处理(第4章)》-读书笔记
《深度学习进阶:自然语言处理(第5章)》-读书笔记
《深度学习进阶:自然语言处理(第6章)》-读书笔记
《深度学习进阶:自然语言处理(第7章)》-读书笔记
《深度学习进阶:自然语言处理(第8章)》-读书笔记