自然语言处理(6:CBOW模型的完整实现)
系列文章目录
第一章 1:同义词词典和基于计数方法语料库预处理
第一章 2:基于计数方法的分布式表示和假设,共现矩阵,向量相似度
第一章 3:基于计数方法的改进以及总结
第二章 1:word2vec
第二章 2:word2vec和CBOW模型的初步实现
第二章 3:CBOW模型的完整实现
第二章 4:
文章目录
前言
在开始word2vec的学习之前,我们先来准备学习用的数据。这里我们仍以“You say goodbye and I say hello.”这个只有一句话的语料库为例进行说明
一、学习数据的准备
1.上下文和目标词
word2vec 中使用的神经网络的输入是上下文,它的正确解标签是被这些上下文包围在中间的单词,即目标词。也就是说,我们要做的事情是,当向神经网络输入上下文时,使目标词出现的概率高(为了达成这一目标而进行学习)。 下面我们就从语料库生成上下文和目标词,如下图所示。

在上图中,我们将语料库中的目标单词作为目标词,将其周围的单词作为上下文提取出来。我们对语料库中的所有单词都执行该操作(两端的单词除外),可以得到上图右侧的contexts(上下文)和target(目标词)。 contexts 的各行成为神经网络的输入(那基于计数的方法呢?),target的各行成为正确解标签(要预测出的单词)。另外,在各笔样本数据中,上下文有多个单词(这个例子中有两个),而目标词则只有一个,因此只有上下文写成了复数形式contexts。
现在,我们来实现从语料库生成上下文和目标词的函数。在此之前,我们先复习一下上一章的内容。首先,将语料库的文本转化成单词ID。这需要使用第1章第一节实现的preprocess()函数。(忘了或者没看的点这里,在文章的最后有该函数自然语言处理(1:同义词词典和基于计数方法语料库预处理)-CSDN博客)
from common.util import preprocess
# 我将其封装在了common中的util文件中
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: '.'}
然后,从单词ID列表corpus生成contexts和target。具体来说,如下图所示,实现一个当给定corpus时返回contexts和target的函数。

如上图所示,contexts 是二维数组。此时,contexts的第0维保存的是各个上下文数据。具体来说,contexts[0]保存的是第0个上下文,context[1] 保存的是第1个上下文……同样地,就目标词而言,target[0] 保存的是第0个目标词,target[1]保存的是第1个目标词…… 现在,我们来实现这个生成上下文和目标词的函数,这里将其称为create_contexts_target(corpus, window_size)(我还是封装在了common的util文件中了)
def create_contexts_target(corpus, window_size=1):
target = corpus[window_size:window_size + 1]
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)
# 不理解上述代码问DS(deepseek)哈
这个函数有两个参数:一个是单词ID列表(corpus);另一个是上下文的窗口大小(window_size)。另外,函数返回的是NumPy多维数组格式的上下文和目标词。现在,我们来实际使用一下这个函数,接着刚才的实现, 代码如下所示
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]
这样就从语料库生成了上下文和目标词,后面只需将它们赋给CBOW 模型即可。不过,因为这些上下文和目标词的元素还是单词ID,所以还需 要将它们转化为one-hot表示。
2.转化为one-hot表示(独热编码)
我们的最终实现总目标如下图:

如上图所示,上下文和目标词从单词ID转化为了one-hot表示。 这里需要注意各个多维数组的形状。在上面的例子中,使用单词ID时的 contexts 的形状是(6,2),将其转化为one-hot表示后,形状变为(6,2,7)。 这里提供了convert_one_hot() 函数以将单词ID转化为one-hot表示。 这个函数的实现内容很简单,代码依然存放在common/util.py中。该函数的参数是单词ID列表和词汇个数。并且我们再把到目前为止的数据预处理总结 一下,如下所示。
def convert_ont_hot(corpus, vocab_size):
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
from common.util import preprocess, create_contexts_target, convert_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)
至此,学习数据的准备就完成了,下面我们来讨论最重要的CBOW模 型的实现。
二、CBOW模型的实现
1.计算图
首先给出计算图

2. simpleCBOW的实现
我们将上图中的神经网络实现为SimpleCBOW类(下一次将实现对其进行了改进的CBOW类)。首先,让我们看一下SimpleCBOW类的初始化方法
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss
# MatMul和SoftmaxWithLoss大家可以搜一下啊如何实现的,问DS(deepseek)
# 当然我在这里还是会继续给出,大家如果要在这里实现SoftmaxWithLoss,那么去掉上面的导入
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)
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
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中。
3.SimpleCBOW的正反向传播
接下来,我们首先来实现神经网络的正向传播forward()函数。这个函数接收参数contexts和target,并返回损失(loss)
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
这里,我们假定参数contexts是一个三维NumPy数组,即往上翻‘转化为one-hot表示’(独热编码)这一小节的图的例子中(6,2,7)的形状,其中第0维的元素个数是mini-batch的数量, 第1维的元素个数是上下文的窗口大小,第2维表示one-hot向量。此外, target 是 (6,7) 这样的二维形状。 最后,我们实现反向传播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模型的实例化
CBOW模型的学习和一般的神经网络的学习完全相同。首先,给神 经网络准备好学习数据。然后,求梯度,并逐步更新权重参数。这里,我 们使用第1章介绍的Trainer类来执行学习过程,学习的源代码如下所示
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
"""
import numpy as np
import time
import matplotlib.pyplot as plt
class Trainer:
def __init__(self, model, optimizer):
self.model = model
self.optimizer = optimizer
self.loss_list = []
self.eval_interval = None
self.current_epoch = 0
def fit(self, x, t, max_epoch=10, batch_size=32, max_grad=None, eval_interval=20):
data_size = len(x)
max_iters = data_size // batch_size
self.eval_interval = eval_interval
model, optimizer = self.model, self.optimizer
total_loss = 0
loss_count = 0
start_time = time.time()
for epoch in range(max_epoch):
# 打乱
idx = numpy.random.permutation(numpy.arange(data_size))
x = x[idx]
t = t[idx]
for iters in range(max_iters):
batch_x = x[iters*batch_size:(iters+1)*batch_size]
batch_t = t[iters*batch_size:(iters+1)*batch_size]
# 计算梯度,更新参数
loss = model.forward(batch_x, batch_t)
model.backward()
params, grads = remove_duplicate(model.params, model.grads) # 将共享的权重整合为1个
if max_grad is not None:
clip_grads(grads, max_grad)
optimizer.update(params, grads)
total_loss += loss
loss_count += 1
# 评价
if (eval_interval is not None) and (iters % eval_interval) == 0:
avg_loss = total_loss / loss_count
elapsed_time = time.time() - start_time
print('| epoch %d | iter %d / %d | time %d[s] | loss %.2f'
% (self.current_epoch + 1, iters + 1, max_iters, elapsed_time, avg_loss))
self.loss_list.append(float(avg_loss))
total_loss, loss_count = 0, 0
self.current_epoch += 1
def plot(self, ylim=None):
x = numpy.arange(len(self.loss_list))
if ylim is not None:
plt.ylim(*ylim)
plt.plot(x, self.loss_list, label='train')
plt.xlabel('iterations (x' + str(self.eval_interval) + ')')
plt.ylabel('loss')
plt.show()
"""
# --------------------------------------------------
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = [], []
for param in params:
self.m.append(np.zeros_like(param))
self.v.append(np.zeros_like(param))
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
for i in range(len(params)):
self.m[i] += (1 - self.beta1) * (grads[i] - self.m[i])
self.v[i] += (1 - self.beta2) * (grads[i]**2 - self.v[i])
params[i] -= lr_t * self.m[i] / (np.sqrt(self.v[i]) + 1e-7)
# -------------------------------------------
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()
运行上面的代码,结果如下

如上图所示,通过不断学习,损失在减小,看起来学习进行得一切正常。我们来看一下学习结束后的权重参数。这里,我们取出输入侧的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的分布式表示。实际运行一下,你看看结果会是什么(自己验证)
我们终于将单词表示为了密集向量!这就是单词的分布式表示。我们有 理由相信,这样的分布式表示能够很好地捕获单词含义。 不过,遗憾的是,这里使用的小型语料库并没有给出很好的结果。当 然,主要原因是语料库太小了。如果换成更大、更实用的语料库,相信会获 得更好的结果。但是,这样在处理速度方面又会出现新的问题,这是因为当 前这个CBOW模型的实现在处理效率方面存在几个问题。下一次我们将改进这个简单的CBOW模型,实现一个“真正的”CBOW模型。
总结
本次主要讲了CBOW的简单实现过程,当然难免有不少遗漏之处,希望看到这里的大佬们点点关注,一键三连哦!
更多推荐
所有评论(0)