系列文章目录
第一章 1:同义词词典和基于计数方法语料库预处理
第一章 2:基于计数方法的分布式表示和假设,共现矩阵,向量相似度
第一章 3:基于计数方法的改进以及总结
第二章 1:word2vec
第二章 2:word2vec和CBOW模型的初步实现
第二章 3:CBOW模型的完整实现
第二章 4:CBOW模型的补充和skip-gram模型的理论
第三章 1:word2vec的高速化(CBOW的改进)
第三章 2:word2vec高速化(CBOW的二次改进)
第三章 3:改进版word2vec的学习以及总结
第四章 1:RNN(RNN的前置知识和引入)
第四章 2:RNN(RNN的正式介绍)
第四章 3:RNN的实现
第四章 4:处理时序数据的层的实现
第四章 5:RNNLM的学习和评价
前言
本次我们的目标是使用RNN实现语言模型。目前我们已经实现了 RNN层和整体处理时序数据的Time RNN层,本节将创建几个可以处理时序数据的新层。我们将基于RNN的语言模型称为RNNLM(RNN Language Model,RNN 语言模型)。现在,我们来完成RNNLM。
一、RNNLM的全貌图
首先,我们看一下RNNLM使用的网络。图5-25所示为最简单的 RNNLM的网络,其中左图显示了RNNLM的层结构,右图显示了在时间 轴上展开后的网络。
上图中的第1层是Embedding层,该层将单词ID转化为单词的分布式表示(单词向量)。然后,这个单词向量被输入到RNN层。RNN层向下一层(上方)输出隐藏状态,同时也向下一时刻的RNN层(右侧)输出隐藏状态。RNN层向上方输出的隐藏状态经过Affine层,传给Softmax层。 现在,我们仅考虑正向传播,向上图的神经网络传入具体的数据, 并观察输出结果。这里使用的句子还是我们熟悉的“you say goodbye and i say hello.”,此时 RNNLM 进行的处理如下图所示。
如上图所示,被输入的数据是单词ID列表。首先,我们关注第1个时刻。作为第1个单词,单词ID为0的you被输入。此时,查看Softmax层输出的概率分布,可知say的概率最高,这表明正确预测出了you后面出现的单词为say。当然,这样的正确预测只在有“好的”(学习顺利的)权重时才会发生。 接着,我们关注第2个单词say。此时,Softmax层的输出在goodbye处和hello处概率较高。确实,“you say goodby”和“you say hello”都是很自然的句子(顺便说一下,正确答案是goodbye)。这里需要注意的是, RNN层“记忆”了“you say”这一上下文。更准确地说,RNN将“you say”这一过去的信息保存为了简短的隐藏状态向量。RNN层的工作是将这个信息传送到上方的Affine层和下一时刻的RNN层。 像这样,RNNLM可以“记忆”目前为止输入的单词,并以此为基础预测接下来会出现的单词。RNN层通过从过去到现在继承并传递数据,使得编码和存储过去的信息成为可能
二、使用步骤
之前我们将整体处理时序数据的层实现为了Time RNN层,这里也同 样使用Time Embedding层、Time Affine层等来实现整体处理时序数据的层。一旦创建了这些Time 层,我们的目标神经网络就可以像下图这样实现。
(我们将整体处理含有T个时序数据的层称为“Time 层”。如果 可以实现这些层,通过像组装乐高积木一样组装它们,就可以完成 处理时序数据的网络。)
Time 层的实现很简单。比如,在Time Affine层的情况下,只需要像下图那样,准备T个Affine层分别处理各个时刻的数据即可。
Time Embedding层也一样,在正向传播时准备T个Embedding层, 由各个Embedding层处理各个时刻的数据。 关于Time Affine层和Time Embedding层没有什么特别难的内容, 我们就不再赘述了。需要注意的是,Time Affine层并不是单纯地使用T个Affine层,而是使用矩阵运算实现了高效的整体处理。感兴趣的读者可以参考如下源代码:
class TimeAffine:
def __init__(self, W, b):
self.params = [W, b]
self.grads = [np.zeros_like(W), np.zeros_like(b)]
self.x = None
def forward(self, x):
N, T, D = x.shape
W, b = self.params
rx = x.reshape(N*T, -1)
out = np.dot(rx, W) + b
self.x = x
return out.reshape(N, T, -1)
def backward(self, dout):
x = self.x
N, T, D = x.shape
W, b = self.params
dout = dout.reshape(N*T, -1)
rx = x.reshape(N*T, -1)
db = np.sum(dout, axis=0)
dW = np.dot(rx.T, dout)
dx = np.dot(dout, W.T)
dx = dx.reshape(*x.shape)
self.grads[0][...] = dW
self.grads[1][...] = db
return dx
接下来我们看一下时序版本的Softmax。 我们在Softmax中一并实现损失误差Cross Entropy Error层。这里, 按照下图所示的网络结构实现Time Softmax with Loss层。
上图中的x0、x1等数据表示从下方的层传来的得分(得分是正规化为概率之前的值),t0、t1等数据表示正确解标签。如该图所示,T个 Softmax with Loss 层各自算出损失,然后将它们加在一起取平均,将得到的值作为最终的损失。此处进行的计算可用下式表示
Softmax with Loss层计算mini-batch的平均损失。 具体而言,假设mini-batch有N笔数据,通过先求N笔数据的损失之和, 再除以N,可以得到单笔数据的平均损失。这里也一样,通过取时序数据的 平均,可以求得单笔数据的平均损失作为最终的输出。
差点忘了,代码如下(记得三连加关注哦):
class TimeSoftmaxWithLoss:
def __init__(self):
self.params, self.grads = [], []
self.cache = None
self.ignore_label = -1
def forward(self, xs, ts):
N, T, V = xs.shape
if ts.ndim == 3: # 在监督标签为one-hot向量的情况下
ts = ts.argmax(axis=2)
mask = (ts != self.ignore_label)
# 按批次大小和时序大小进行整理(reshape)
xs = xs.reshape(N * T, V)
ts = ts.reshape(N * T)
mask = mask.reshape(N * T)
ys = softmax(xs)
ls = np.log(ys[np.arange(N * T), ts])
ls *= mask # 与ignore_label相应的数据将损失设为0
loss = -np.sum(ls)
loss /= mask.sum()
self.cache = (ts, ys, mask, (N, T, V))
return loss
def backward(self, dout=1):
ts, ys, mask, (N, T, V) = self.cache
dx = ys
dx[np.arange(N * T), ts] -= 1
dx *= dout
dx /= mask.sum()
dx *= mask[:, np.newaxis] # 与ignore_label相应的数据将梯度设为0
dx = dx.reshape((N, T, V))
return dx