系列文章目录
第一章 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的实现
1.RNN层的实现
2.Time RNN层的实现
前言
通过之前的探讨,我们已经看到了RNN的全貌。实际上,我们要实现的是一个在水平方向上延伸的神经网络。另外,考虑到基于Truncated BPTT的学习,只需要创建一个在水平方向上长度固定的网络序列即可,来吧,开始实现(迫不及待!!)
一、RNN的实现
如上图所示,目标神经网络接收长度为T的时序数据(T为任意值), 输出各个时刻的隐藏状态T个。这里,考虑到模块化,将图中在水平方向上延伸的神经网络实现为“一个层”,如下图所示。
如图5-17所示,将垂直方向上的输入和输出分别捆绑在一起,就可以 将水平排列的层视为一个层。换句话说,可以将(x0,x1,···,xT−1)捆绑为 xs 作为输入,将(h0,h1,···,hT−1)捆绑为hs作为输出。这里,我们将进 行Time RNN层中的单步处理的层称为“RNN层”,将一次处理T步的层称为“Time RNN层 ”。
我们接下来进行的实现的流程是:首先,实现进行RNN单步处理的RNN 类;然后,利用这个RNN类,完成一次进行T步处理的TimeRNN类。
1.RNN层的实现
现在,我们来实现进行RNN单步处理的RNN类。首先复习一下RNN 正向传播的数学式,如下式所示:
这里,我们将数据整理为mini-batch进行处理。因此,xt(和ht)在行方向上保存各样本数据。在矩阵计算中,矩阵的形状检查非常重要。这里,假设批大小是N,输入向量的维数是D,隐藏状态向量的维数是H,则矩阵的形状检查可以像下面这样进行:
如上图所示,通过矩阵的形状检查,可以确认它的实现是否正确, 至少可以确认它的计算是否成立。基于以上内容,现在我们给出RNN类的初始化方法和正向传播的forward()方法
class RNN:
def __init__(self, Wx, Wh, b):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.cache = None
# 中间数据cache
def forward(self, x, h_prev):
Wx, Wh, b = self.params
t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
h_next = np.tanh(t)
self.cache = (x, h_prev, h_next)
return h_next
RNN 的初始化方法接收两个权重参数和一个偏置参数。这里,将通过函数参数传进来的模型参数设置为列表类型的成员变量params。然后,以各个参数对应的形状初始化梯度,并保存在grads中。最后,使用None对反向传播时要用到的中间数据cache进行初始化。
正向传播的forward(x, h_prev) 方法接收两个参数:从下方输入的x和从左边输入的h_prev。剩下的就是按下式进行实现。
顺便说一下,这里 从前一个RNN层接收的输入是h_prev,当前时刻的RNN层的输出(=下一时刻的RNN层的输入)是h_next。
接下来,我们继续实现RNN的反向传播。在此之前,让我们通过下图的计算图再次确认一下RNN的正向传播:
RNN层的正向传播可由上图的计算图表示。这里进行的计算由矩阵乘积“MatMul”、加法“+”和“tanh”这3种运算构成。此外,因为偏置 b 的加法运算会触发广播操作,所以严格地讲,这里还应该加上Repeat节点。不过简单起见,这里省略了它。
剩下就是基于下图,按正向传播的反方向实现各个运算的反向传播:
下面实现RNN层的backward()
def backward(self, dh_next):
"""
这里看不懂的,可以看博主前几节的文章,或者问Deepseek
"""
Wx, Wh, b = self.params
x, h_prev, h_next = self.cache
dt = dh_next * (1 - h_next ** 2)
db = np.sum(dt, axis=0)
dWh = np.dot(h_prev.T, dt)
dh_prev = np.dot(x.T,, dt)
dWx = np.dot(x.T, dt)
dx = np.dot(dt, Wx.T)
self.grads[0][...] = dWx
self.grads[1][...] = dWh
self.grads[2][...] = db
return dx, dh_prev
以上就是RNN层的反向传播的实现。接下来,我们将实现Time RNN层
2.Time RNN层的实现
Time RNN 层由T个RNN层构成,如下所示:
由上图可知,Time RNN层是T个RNN层连接起来的网络。我们将这个网络实现为Time RNN层。这里,RNN层的隐藏状态h保存在成员变量中。如下图所示,在进行隐藏状态的“继承”时会用到它。
如上图所示,我们使用Time RNN层管理RNN层的隐藏状态。这样一来,使用Time RNN的人就不必考虑RNN层的隐藏状态的“继承工作”了。另外,我们可以用stateful这个参数来控制是否继承隐藏状态。
下面,我们来看一下Time RNN层的实现。
class TimeRNN:
def __init__(self, Wx, Wh, b, stateful=False):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx),np.zeros_like(Wh),np.zeros_like(b)]
self.layers = None
self.h, self.dh = None, None
self.stateful = stateful
def set_state(self, h):
self.h = h
def reset_state(self):
self.h = None
初始化方法的参数有权重、偏置和布尔型(True/False)的stateful。 一个成员变量layers在列表中保存多个RNN层,另一个成员变量,h保存调用forward() 方法时的最后一个RNN层的隐藏状态。另外,在调用 backward() 时,dh保存传给前一个块的隐藏状态的梯度(关于dh,我们会在 反向传播的实现中说明)。
考虑到TimeRNN类的扩展性,将设定Time RNN层的隐藏状态的 方法实现为set_state(h)。另外,将重设隐藏状态的方法实现为 reset_state()。
当 stateful 为 True 时,Time RNN 层“有状态”。这里说的“有状态”是指维 持Time RNN层的隐藏状态。也就是说,无论时序数据多长,Time RNN 层的正向传播都可以不中断地进行。而当stateful为False时,每次调用Time RNN 层的forward() 时,第一个RNN层的隐藏状态都会被初始化为 零矩阵(所有元素均为0的矩阵)。这是没有状态的模式,称为“无状态”。
(True就是这一层受上一层的影响,具有依赖性;
False就是相互独立,各个数据互不影响)
接着,我们来看一下正向传播的实现:
def forward(self, xs):
Wx, Wh, b = self.params
N, T, D = xs.shape
D, H = Wx.shape
self.layers = []
hs = np.empty((N, T, H), dtype='f')
if not self.stateful or self.h is None:
self.h = np.zeros((N, H), dtype='f')
for t in range(T):
layer = RNN(*self.params)
self.h = layer.forward(xs[:, t, :], self.h)
hs[:, t, :] = self.h
self.layers.append(layer)
return hs
正向传播的forward(xs)方法从下方获取输入xs,xs囊括了T个时序数 据。因此,如果批大小是N,输入向量的维数是D,则xs的形状为(N,T,D)。 在首次调用时(self.h为None时 ), RNN层的隐藏状态h由所有元素 均为0的矩阵初始化。另外,在成员变量stateful为False的情况下,h将 总是被重置为零矩阵。
在主体实现中,首先通过hs=np.empty((N, T, H), dtype='f') 为输出准 备一个“容器”。接着,在T次for循环中,生成RNN层,并将其添加到成员变量layers中。然后,计算RNN层各个时刻的隐藏状态,并存放在hs 的对应索引(时刻)中。
(如果调用Time RNN层的forward()方法,则成员变量h中将存放 最后一个RNN层的隐藏状态。在stateful为True的情况下,在下 一次调用forward()方法时,刚才的成员变量h将被继续使用。而在 stateful为False的情况下,成员变量h将被重置为零向量。)
接下来是Time RNN层的反向传播的实现。用计算图绘制这个反向传播:
在上图中,将从上游(输出侧的层)传来的梯度记为dhs,将流向 下游的梯度记为dxs。因为这里我们进行的是Truncated BPTT,所以不需要流向这个块上一时刻的反向传播。不过,我们将流向上一时刻的隐藏状态的梯度存放在成员变量dh中。这是因为在第7章探讨seq2seq(sequence to-sequence,序列到序列)时会用到它(具体请参考第7章 )。 以上就是Time RNN层的反向传播的全貌图。如果关注第t个RNN层, 则它的反向传播如下图所示。
从上方传来的梯度dht和从将来的层传来的梯度dhnext会传到第t个 RNN层。这里需要注意的是,RNN层的正向传播的输出有两个分叉。在正 向传播存在分叉的情况下,在反向传播时各梯度将被求和。因此,在反向传 播时,流向RNN层的是求和后的梯度。考虑到以上这些,反向传播的实现 如下所示。
def backward(self, dhs):
Wx, Wh, b = self.params
N, D, H = dhs.shape
D, H = Wx.shape
dxs = np.empty((N, T, D), dtype="f")
dh = 0
grads = [0, 0, 0]
for t in reversed(range(T)):
layer = self.layers[t]
dx, dh = layer.backward(dhs[:, t, :] + dh) # 求和后的梯度
dxs[:, t, :] = dx
for i, grad in enumerate(layer.grads):
grad[i] += grad
for i, grad in enumerate(grads):
self.grads[i][...] = grad
self.dh = dh
return dxs
这里,首先创建传给下游的梯度的“容器”(dxs)。接着,按与正向传 播相反的方向,调用RNN层的backward()方法,求得各个时刻的梯度dx, 并存放在dxs的对应索引处。另外,关于权重参数,需要求各个RNN层的 权重梯度的和,并通过“...”用最终结果覆盖成员变量self.grads。
以上就是对Time RNN层的实现的说明。