第五章RNN
目 录
RNN
前面我们已经学习过word2vec的两种自然语言处理模型了,但是这两种模型由于设计最初的目的是获得单词的分布式表示,所以它并不能根据前文推测下文,即根据输入数据的先后关系进行预测下文的单词。由此我们本章将会学习RNN,它可以根据上文推测下文,下面就是这个算法的一个应用例子:
5.1 循环神经网络
RNN是Recurrent Neural Network的意思,翻译过来是循环神经网路,他的特点是能记录上一次的计算状态并将结果用于下一次的计算,由于在逻辑上像是一个循环所以叫做循环神经网络。
回到本章刚开始的那个问题,“Tom 在房间看电视,Mary 进了房间”据该语境(上下文),正确答案应该是 Mary 向 Tom(或者“him”)打招呼,但是这个问题对于CBOW来说很难解决,他最多可以判断出来是Mary或Tom但是无法判读到底是谁,这是由于他忽略的单词出现的顺序,因为这个方法在设计的时候就是将句子变成单词ID组成的列表,计算词之间共同出现的关系忽视掉了词出现的顺序。
实际上也有人考虑设计能考虑词出现时间顺序的CBOW但是由于这种方法用到了拼接方法,导致如果上下文特别多,那么拼接的上下文大小大不说,权重参数数量也多,所以不太好,所以就有人设计了RNN,如上图这种方法在计算时考虑了前面的结果,这样可以做到模型可以考虑单词出现的时序。
这种模型每次层的输出计算方法为如下公式,其中的t表示第t个出现的词,h称为隐藏状态(hidden state)或者隐藏向量(hidden state vector),他表示以前的状态:
h t = t a n h ( h t − 1 W h + x t W x + b ) \boldsymbol{h}_t=tanh(\boldsymbol{h}_{t-1}\boldsymbol{W}_h+\boldsymbol{x}_t\boldsymbol{W}_x+b) ht=tanh(ht−1Wh+xtWx+b)
5.2 Backpropagation Through Time(BPTT) 和 Truncated BPTT
有了前面的介绍大家自然也就大致猜到了这个模型的正向传播流程,但是这不是重点,众所周知反向传播才是我们进行神经网络学习总是需要进行关注的问题,这个RNN的方向传播流程就是将输入与传到下一层的隐藏状态进行求和然后再反向传播。
本节出现了Backpropagation Through Time(BPTT),它的意思就是基于时间的反向传播,为什么这么说呢,是因为他的反向传播是按照单词出现的顺序反向传播的。
但是这也有一个问题就是随着单词量的增大。BPTT 消耗的计算机资源也会成比例地增大。另外,反向传播的梯度也会变得不稳定,这里说的不稳定就是反向传播的越长,梯度越小,最后导致梯度小到无法进行学习。
由于上面说的这个问题,所以又提出了截断时的基于时间的反向传播(Truncated BPTT),这种方法是在正向传播正常进行的情况下,反响传播会按照固定的词量进行截断,从而避免刚刚提到的两点问题出现。
这种方法同样是支持批量学习的,只要设置偏移量即可:
5.3 RNN 层的实现
为了避免只见树木不见林,这里还是先给大家看一下我们要搭建一个什么样的网络:
这个网络将整个RNN层封装成了一个Time RNN类通过这个类来实现层中各个RNN之间的隐藏状态传递和反向传播,万丈高楼平地起,先来搭建RNN,有了前面介绍的公式和流程RNN的实现主要是实现公式:
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
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层就算完成了:
def backward(self, dh_next):
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(dt, Wh.T)
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类,在这个类中他将会负责每个截断快之间的隐藏状态传递和自身内部的反向传播,Time RNN 层将隐藏状态 h 保存在成员变量中,以在块之间继承隐藏状态:
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
正向传播的 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 的对应索引(时刻)中。
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
接下来开始考虑反向传播,需要将从上游(输出侧的层)传来的梯度记为 d h s \boldsymbol{dhs} dhs,将流向下游的梯度记为 d x s \boldsymbol{dxs} dxs。因为这里我们进行的是 Truncated BPTT,所以不需要流向这个块上一时刻的反向传播。不过,我们将流向上一时刻的隐藏状态的梯度存放在成员变量 d h dh dh 中。这是因为在第 7 章探讨 seq2seq(sequence-to-sequence,序列到序列)时会用到它(具体请参考第 7 章)。
def backward(self, dhs):
Wx, Wh, b = self.params
N, T, 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):
grads[i] += grad
for i, grad in enumerate(grads):
self.grads[i][...] = grad
self.dh = dh
return dxs
5.4 RNNLM的全貌图及实现
有了前面的基础下面就可以实现我们的模型了,先给出模型的全貌:
不过在计算最后一层输出的时候需要加上损失合成为Time Softmax with Loss 层计算图如下:
然后开始实现这个网络:
class SimpleRnnlm:
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
# 初始化权重
embed_W = (rn(V, D) / 100).astype('f')
rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f')
rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')
rnn_b = np.zeros(H).astype('f')
affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
affine_b = np.zeros(V).astype('f')
# 生成层
self.layers = [
TimeEmbedding(embed_W),
TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),
TimeAffine(affine_W, affine_b)
]
self.loss_layer = TimeSoftmaxWithLoss()
self.rnn_layer = self.layers[1]
# 将所有的权重和梯度整理到列表中
self.params, self.grads = [], []
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
def forward(self, xs, ts):
for layer in self.layers:
xs = layer.forward(xs)
loss = self.loss_layer.forward(xs, ts)
return loss
def backward(self, dout=1):
dout = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
return dout
def reset_state(self):
self.rnn_layer.reset_state()
如此网络的实现就完成了,再配合学习代码 ,整个网络就可以跑起来了:
# 设定超参数
batch_size = 10
wordvec_size = 100
hidden_size = 100 # RNN的隐藏状态向量的元素个数
time_size = 5 # Truncated BPTT的时间跨度大小
lr = 0.1
max_epoch = 100
# 读入训练数据(缩小了数据集)
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)
xs = corpus[:-1] # 输入
ts = corpus[1:] # 输出(监督标签)
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))
# 学习用的参数
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []
# 生成模型
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
# ❶ 计算读入mini-batch的各笔样本数据的开始位置
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]
for epoch in range(max_epoch):
for iter in range(max_iters):
# ❷ 获取mini-batch
batch_x = np.empty((batch_size, time_size), dtype='i')
batch_t = np.empty((batch_size, time_size), dtype='i')
for t in range(time_size):
for i, offset in enumerate(offsets):
batch_x[i, t] = xs[(offset + time_idx) % data_size]
batch_t[i, t] = ts[(offset + time_idx) % data_size]
time_idx += 1
# 计算梯度,更新参数
loss = model.forward(batch_x, batch_t)
model.backward()
optimizer.update(model.params, model.grads)
total_loss += loss
loss_count += 1
# ❸ 各个epoch的困惑度评价
ppl = np.exp(total_loss / loss_count)
print('| epoch %d | perplexity %.2f'
% (epoch+1, ppl))
ppl_list.append(float(ppl))
total_loss, loss_count = 0, 0
这里的最后是计算了困惑度,它越小越好:
可见随着学习的进行,网络的困惑度逐渐下降。到此我们本章就学完了。
总结
本章主要说明了RNN提出是为了解决时序数据的问题,在操作过程中,要将他们实现为Time RNN类这样避免计算占用大量内存和导致梯度消失,但是网络中还存在问题,下一章将会学习替代 RNN 的 LSTM 层或 GRU 层。
另外需要强调的是RNN是一种可以考虑以前状态的网络,同时在 Truncated BPTT 中,为了维持正向传播的连接,需要按顺序输入数据,本节我们实际上学习了语言模型,他将句子解释为概率,来判断通不通顺。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)