系列文章目录
第五章 1:Gated RNN(门控RNN)
第五章 2:梯度消失和LSTM
第五章 3:LSTM的实现
第五章 4:使用LSTM的语言模型
第五章 5:进一步改进RNNLM(以及总结)
文章目录
目录
系列文章目录
文章目录
前言
一、LSTM的代码实现
二、Time LSTM层的实现
前言
下面,我们来实现LSTM。这里将进行单步处理的类实现为LSTM类, 将整体处理T步的类实现为TimeLSTM类。现在我们先来整理一下LSTM中 进行的计算,如下所示:
一、LSTM的代码实现
首先LSTM要进行的计算有:
上述式子中,前四个式子(f, g, i, o)是仿射变换,这里的仿射变换是指xWx+hWh+b这样的式子。虽然通过4个式子可以分别进行仿射变换,但其实也可以整合为通过1个式子进行。如下图所示:
在上图中,4个权重(或偏置)被整合为了1个。如此,原本单独执行4次的仿射变换通过1次计算即可完成,可以加快计算速度。这是因为矩阵库计算“大矩阵”时通常会更快,而且通过将权重整合到一起管理,源代码也会更简洁。
假设Wx、Wh和b分别包含4个权重(或偏置),此时LSTM的计算图如下图所示。(如果看不懂,看看博主前几篇文章,你就会明白)
如上计算图所示,先一起执行4个仿射变换。然后,基于slice节点,取出4个结果。这个slice节点很简单,它将仿射变换的结果(矩阵)均等地分成4份,然后取出内容。在slice节点之后,数据流过激活函数(sigmoid 函数或tanh函数),进行上一节介绍的计算。
现在,参考上面计算图,我们来实现LSTM类。首先来看一下LSTM类的初始化代码:
from common.layers import *
from common.functions import softmax, sigmoid
"""
def sigmoid(x):return 1 / (1 + np.exp(-x))def softmax(x):if x.ndim == 2:x = x - x.max(axis=1, keepdims=True)x = np.exp(x)x /= x.sum(axis=1, keepdims=True)elif x.ndim == 1:x = x - np.max(x)x = np.exp(x) / np.sum(np.exp(x))return x"""class LSTM: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
初始化的参数有权重参数Wx、Wh和偏置b。如前所述,这些权重(或偏 置)整合了4个权重。把这些参数获得的权重参数设定给成员变量params, 并初始化形状与之对应的梯度。另外,成员变量cache保存正向传播的中间 结果,它们将在反向传播的计算中使用.
接下来实现正向传播的forward(x, h_prev, c_prev) 方法。它的参数接 收当前时刻的输入x、上一时刻的隐藏状态h_prev,以及上一时刻的记忆单元c_prev。
def forward(self, x, h_prev, c_prev):"""看着计算图一步步填进去,你会明白的!!,相信自己"""Wx, Wh, b = self.paramsN, H = h_prev.shapeA = np.dot(x, Wx) + np.dot(h_prev, Wh) + b# slicef = A[:, :H]g = A[:, H:2*H]i = A[:, 2*H:3*H]o = A[:, 3*H:]f = sigmoid(f) # sigmoid代码实现在LSTM定义最上面g = np.tanh(g) i = sigmoid(i)o = sigmoid(o)c_next = f * c_prev + g * ih_next = o * np.tanh(c_next)self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)return h_next, c_next
首先进行仿射变换。重复一下,此时的成员变量Wx、Wh和b保存的是4个权重,矩阵的形状将变为如下图所示的样子。
在上图式子中,批大小是N,输入数据的维数是D,记忆单元和隐藏状态的维数都是H。另外,计算结果A中保存了4个仿射变换的结果。因此,通过A[:, :H]、A[:, H:2*H] 这样的切片取出数据,并分配给之后的运算节点。 参考LSTM的数学式和计算图,剩余的实现应该不难
LSTM的反向传播可以通过将上面的计算图反方向传播而求得。基于前面介绍的知识,这并不困难。不过,因为slide节点是第一次见到,所以我们简要说明一下它的反向传播。slice 节点将矩阵分成了4份,因此它的反向传播需要整合4个梯度,如下图所示。
由上图可知,在slice节点的反向传播中,拼接4个矩阵。图中有4个梯度df、dg、di和do,将它们拼接成dA。如果通过NumPy进行,则可以使用np.hstack()。np.hstack() 在水平方向上将参数中给定的数组拼接起来(垂直方向上的拼接使用np.vstack())。因此,上述处理可以用下面1行代码完成。 dA = np.hstack((df, dg, di, do)) 以上就是对slice节点的反向传播的说明。
二、Time LSTM层的实现
现在我们继续TimeLSTM的实现。Time LSTM层是整体处理T个时序数据的层,由T个LSTM层构成,如下图所示。
如前所述,RNN中使用Truncated BPTT进行学习。Truncated BPTT以适当的长度截断反向传播的连接,但是需要维持正向传播的数据流。为此,如下图所示,将隐藏状态和记忆单元保存在成员变量中。这样一来,在调用下一个forward()函数时,就可以继承上一时刻的隐藏状态 (和记忆单元)。
我们已经实现了Time RNN层(在自然语言处理(13:RNN的实现)-CSDN博客),这里也以同样的方式实现Time LSTM层。TimeLSTM可以像下面这样实现:
class TimeLSTM: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 = Noneself.h, self.c = None, Noneself.dh = Noneself.stateful = statefuldef forward(self, xs):Wx, Wh, b = self.paramsN, T, D = xs.shapeH = Wh.shape[0]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')if not self.stateful or self.c is None:self.c = np.zeros((N, H), dtype='f')for t in range(T):layer = LSTM(*self.params) # 此处实现在上面哦self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)hs[:, t, :] = self.hself.layers.append(layer)return hsdef backward(self, dhs):Wx, Wh, b = self.paramsN, T, H = dhs.shapeD = Wx.shape[0]dxs = np.empty((N, T, D), dtype='f')dh, dc = 0, 0grads = [0, 0, 0]for t in reversed(range(T)):layer = self.layers[t]dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)dxs[:, t, :] = dxfor i, grad in enumerate(layer.grads):grads[i] += gradfor i, grad in enumerate(grads):self.grads[i][...] = gradself.dh = dhreturn dxsdef set_state(self, h, c=None):self.h, self.c = h, cdef reset_state(self):self.h, self.c = None, None
在LSTM中,除了隐藏状态h外,还使用记忆单元c。TimeLSTM类的实现和TimeRNN 类几乎一样。这里仍通过参数stateful指定是否维持状态。接下来,我们使用这个TimeLSTM创建语言模型。