赞
踩
本章的主题仍是单词的分布式表示。在上一章中,我们使 用基于计数的方法得到了单词的分布式表示。本章我们将讨论该方法的替代 方法,即基于推理的方法。
顾名思义,基于推理的方法使用了推理机制。当然,这里的推理机制用 的是神经网络。本章,著名的 word2vec 将会登场。
本章的目标是实现一个简单的 word2vec。这个简单的 word2vec 会优 先考虑易理解性,从而牺牲一定的处理效率。因此,我们不会用它来处理大 规模数据集,但用它处理小数据集毫无问题。
用向量表示单词的研究最近正在如火如荼地展开,其中比较成功的方 法大致可以分为两种:一种是基于计数的方法;另一种是基于推理的方法。
基于计数的方法使用整个语料库的统计数据(共现矩阵和 PPMI 等), 通过一次处理(SVD 等)获得单词的分布式表示。而基于推理的方法使用 神经网络,通常在 mini-batch 数据上进行学习。这意味着神经网络一次只 需要看一部分学习数据(mini-batch),并反复更新权重。
基于计数的方法一次性处理全部学习数据;反之,基于推理的方法使用部分学习数据逐步学习。
基于推理的方法的主要操作是**“推理”。**
如图 3-5 所示,输入层由 7 个神经元表示,分别对应于 7 个单词(第 1 个神经元对应于 you,第 2 个神经元对应于 say)
现在事情变得很简单了。因为只要将单词表示为向量,这些向量就可以由构成神经网络的各种“层”来处理。比如,对于one-hot表示的某个单词, 使用全连接层对其进行变换的情况如图 3-6 所示。(省略了偏置)
我们看一下代码。这里的全连接层变换可以写成如下
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(W) print(h) #输出 [[-1.30982294 0.19485001 -0.49979452] [-1.77466539 -2.67810488 2.7992046 ] [-0.20747764 -0.68246166 0.7149981 ] [ 0.18558413 -0.61176428 2.25844791] [-0.70263837 0.63946127 -0.33276184] [-0.31945603 0.07161013 1.18615179] [ 2.01949978 -0.5961003 -1.01233551]] [[-1.30982294 0.19485001 -0.49979452]]
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
这里,仅为了提取权重的行向量而进行矩阵乘积计算好像不是很有效 率。关于这一点,我们将在 4.1 节进行改进。另外,上述代码的功能也可以 使用第 1 章中实现的 MatMul 层完成,
c = np.array([[1, 0, 0, 0, 0, 0, 0]])
W = np.random.randn(7, 3)
layer = MatMul(W)
h = layer.forward(c)
print(h)
# [[-0.70012195 0.25204755 -0.79774592]]
word2vec 一词最初用来指程序或者工具,但是随着该词的流行, 在某些语境下,也指神经网络的模型。正确地说,CBOW 模型 和 skip-gram 模型是 word2vec 中使用的两个神经网络。
CBOW 模型是根据上下文预测目标词的神经网络(“目标词”是指中间 的单词,它周围的单词是“上下文”)。通过训练这个 CBOW 模型,使其能 尽可能地进行正确的预测,我们可以获得单词的分布式表示。
CBOW 模型的输入是上下文。这个上下文用 [‘you’, ‘goodbye’] 这样 的单词列表表示。我们将其转换为 one-hot 表示,以便 CBOW 模型可以进 行处理。在此基础上,CBOW 模型的网络可以画成图 3-9 这样。
图3-9 是 CBOW 模型的网络。它有两个输入层,经过中间层到达输出 层。这里,从输入层到中间层的变换由相同的全连接层(权重为Win)完成, 从中间层到输出层神经元的变换由另一个全连接层(权重为 Wout)完成。
有时将得分经过 Softmax 层之后的神经元称为输出层。这里,我 们将输出得分的节点称为输出层。
不使用偏置的全连接层的处理由 MatMul 层的正向传播代理。这 个层在内部计算矩阵乘积。
# coding: utf-8 import sys sys.path.append('..') 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) # [[-0.33304998 3.19700011 1.75226542 1.36880744 1.68725368 2.38521564 0.81187955]]
以上就是 CBOW 模型的推理过程。这里我们见到的 CBOW 模型是没 有使用激活函数的简单的网络结构。除了多个输入层共享权重外,并没有什 么难点。
CBOW 模型的学习就是调整权重,以使预测准确。其结果是,权重 Win(确切地说是 Win 和 Wout 两者)学习到蕴含单词出现模式的向量。根据过去的实验,CBOW 模型(和 skip-gram 模型)得到的单词的分布式表示,特别是使用维基百科等大规模语料库学习到的单词的分布式表示,在单词的含义和语法上符合我们直觉的案例有很多.
CBOW模型只是学习语料库中单词的出现模式。如果语料库不一样, 学习到的单词的分布式表示也不一样。比如,只使用“体育”相关 的文章得到的单词的分布式表示,和只使用“音乐”相关的文章得 到的单词的分布式表示将有很大不同。
就 word2vec(特别是 skip-gram 模型)而言,最受欢迎的是方案 A。遵循这一思路,我们也使用 Win 作为单词的分布式表示
、在开始 word2vec 的学习之前,我们先来准备学习用的数据。这里我们 仍以“You say goodbye and I say hello.”这个只有一句话的语料库为例进 行说明。
word2vec 中使用的神经网络的输入是上下文,它的正确解标签是被这些上下文包围在中间的单词,即目标词。
def preprocess(text): text = text.lower() text = text.replace('.', ' .') words = text.split(' ') word_to_id = {} id_to_word = {} for word in words: if word not in word_to_id: new_id = len(word_to_id) word_to_id[word] = new_id id_to_word[new_id] = word corpus = np.array([word_to_id[w] for w in words]) return corpus, word_to_id, id_to_word
我们来实现从语料库生成上下文和目标词的函数。在此之前,我 们先复习一下上一章的内容。首先,将语料库的文本转化成单词 ID。
import sys
sys.path.append('..')
from common.util import preprocess
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)
# [0 1 2 3 4 1 5 6]
print(id_to_word)
# {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
def create_contexts_target(corpus, window_size=1): '''生成上下文和目标词 :param corpus: 语料库(单词ID列表) :param window_size: 窗口大小(当窗口大小为1时,左右各1个单词为上下文) :return: ''' target = corpus[window_size:-window_size] contexts = [] 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]) contexts.append(cs) return np.array(contexts), np.array(target)
接着刚才的实现, 代码如下所示。
contexts, target = create_contexts_target(corpus, window_size=1)
print(contexts)
# [[0 2]
# [1 3]
# [2 4]
# [3 1]
# [4 5]
# [1 6]]
print(target)
# [1 2 3 4 1 5]
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
text = 'You say goodbye and I say hello.' corpus, word_to_id, id_to_word = preprocess(text) contexts, target = create_contexts_target(corpus, window_size=1) vocab_size = len(word_to_id) target = convert_one_hot(target, vocab_size) contexts = convert_one_hot(contexts, vocab_size) contexts #输出contexts array([[[1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0]], [[0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0]], [[0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0]], [[0, 0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0]], [[0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1, 0]], [[0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1]]])
class SimpleCBOW: def __init__(self, vocab_size, hidden_size): V, H = vocab_size, hidden_size #词汇个数 vocab_size,中间层的神经元个数 hidden_size # 初始化权重,并用一些小的随机值初始化这两个权重,astype('f')初始化将使用 32 位的浮点数。 W_in = 0.01 * np.random.randn(V, H).astype('f') W_out = 0.01 * np.random.randn(H, V).astype('f') # 生成层 #生成两个输入侧的 MatMul 层、一个输出侧的 MatMul 层,以及一个 Softmax with Loss 层。 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 #现神经网络的正向传播 forward() 函数 def forward(self, contexts, target): h0 = self.in_layer0.forward(contexts[:, 0]) h1 = self.in_layer1.forward(contexts[:, 1]) h = (h0 + h1) * 0.5 score = self.out_layer.forward(h) loss = self.loss_layer.forward(score, target) return loss 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
这里,用来处理输入侧上下文的 MatMul 层的数量与上下文的单词数量相同(本例中是两个)。 另外,我们使用相同的权重来初始化 MatMul 层。
最后,将该神经网络中使用的权重参数和梯度分别保存在列表类型的成 员变量 params 和 grads 中。
这里,多个层共享相同的权重。因此,params列表中存在多个相 同的权重。但是,在 params列表中存在多个相同的权重的情况 下,Adam、Momentum 等优化器的运行会变得不符合预期(至 少就我们的代码而言)。为此,在 Trainer类的内部,在更新参数 时会进行简单的去重操作。关于这一点,这里省略说明,感兴趣 的读者可以参考 common/trainer.py的 remove_duplicate(params, grads)
这里,我们假定参数 contexts 是一个三维 NumPy 数组,即上一节图 3-18 的例子中 (6,2,7)的形状,其中第 0 维的元素个数是 mini-batch 的数量, 第 1 维的元素个数是上下文的窗口大小,第 2 维表示 one-hot 向量。此外, target 是 (6,7) 这样的二维形状。
至此,反向传播的实现就结束了。我们已经将各个权重参数的梯度保存在了成员变量 grads 中。因此,通过先调用 forward() 函数,再调 用 backward() 函数,grads 列表中的梯度被更新。下面,我们继续看一下 SimpleCBOW 类的学习
CBOW 模型的学习和一般的神经网络的学习完全相同。首先,给神 经网络准备好学习数据。然后,求梯度,并逐步更新权重参数。这里,我 们使用第 1 章介绍的 Trainer 类来执行学习过程,
import sys sys.path.append('..') # 为了引入父目录的文件而进行的设定 from common.trainer import Trainer from common.optimizer import Adam from 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 hello.' corpus, word_to_id, id_to_word = preprocess(text) vocab_size = len(word_to_id) contexts, target = create_contexts_target(corpus, window_size) target = convert_one_hot(target, vocab_size) contexts = convert_one_hot(contexts, vocab_size) model = SimpleCBOW(vocab_size, hidden_size) optimizer = Adam() trainer = Trainer(model, optimizer) trainer.fit(contexts, target, max_epoch, batch_size) trainer.plot() word_vecs = model.word_vecs #为输入侧的 MatMul 层的权重已经赋值给了成员变量 word_vecs for word_id, word in id_to_word.items(): print(word, word_vecs[word_id])
you [ 1.1550112 1.2552509 -1.1116056 1.1482503 -1.2046812]
say [-1.2141827 -1.2367412 1.2163384 -1.2366292 0.67279905]
goodbye [ 0.7116186 0.55987084 -0.8319744 0.749239 -0.8436555 ]
and [-0.94652086 -0.8535852 0.55927175 -0.6934891 2.0411916 ]
i [ 0.7177702 0.5699475 -0.8368816 0.7513028 -0.8419596]
hello [ 1.1411363 1.2600429 -1.105042 1.1401148 -1.2044929]
. [-1.1948084 -1.2921802 1.4119368 -1.345656 -1.5299033]
不过,遗憾的是,这里使用的小型语料库并没有给出很好的结果。当 然,主要原因是语料库太小了。如果换成更大、更实用的语料库,相信会获 得更好的结果。但是,这样在处理速度方面又会出现新的问题,这是因为当 前这个 CBOW 模型的实现在处理效率方面存在几个问题。下一章我们将改 进这个简单的 CBOW 模型,实现一个“真正的”CBOW 模型。
本书中将概率记为 P(·),比如事 件 A 发生的概率记为 P(A)。联合概率记为** P(A, B),表示事件 A 和事件 B 同时发生的概率。 后验概率记为 P(A|B)**,字面意思是“事件发生后的概率”。从另一个 角度来看,也可以解释为“在给定事件 B(的信息)时事件 A 发生的概率”。
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
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 class SoftmaxWithLoss: def __init__(self): self.params, self.grads = [], [] self.y = None # softmax的输出 self.t = None # 监督标签 def forward(self, x, t): self.t = t self.y = softmax(x) # 在监督标签为one-hot向量的情况下,转换为正确解标签的索引 if self.t.size == self.y.size: self.t = self.t.argmax(axis=1) loss = cross_entropy_error(self.y, self.t) return loss def backward(self, dout=1): batch_size = self.t.shape[0] dx = self.y.copy() dx[np.arange(batch_size), self.t] -= 1 dx *= dout dx = dx / batch_size return dx
此外,在 word2vec 之后,有研究人员提出了 GloVe 方法 [27]。GloVe 方法融合了基于推理的方法和基于计数的方法。该方法的思想是,将整个语 料库的统计数据的信息纳入损失函数,进行 mini-batch 学习(具体请参考 论文 [27])。据此,这两个方法论成功地被融合在了一起。
本章我们详细解释了 word2vec 的 CBOW 模型,并对其进行了实 现。CBOW 模型基本上是一个 2 层的神经网络,结构非常简单。我们使用 MatMul 层和 Softmax with Loss 层构建了 CBOW 模型,并用一个小规模 语料库确认了它的学习过程。遗憾的是,现阶段的 CBOW 模型在处理效率 上还存在一些问题。不过,在理解了本章的 CBOW 模型之后,离真正的 word2vec 也就一步之遥了。下一章,我们将改进 CBOW 模型。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。