文章目录
1改进点
1.1多层化
加深神经网络的层数往往能够学习更复杂的模式;因此这里增加了LSTM层的数量;如下图所示:
- 第一个 LSTM 层的隐藏状态是第二个LSTM层的输入;
- 关于叠加几个层,这是超参数调优的范畴了;需要根据要解决的问题的复杂程度、能给到的训练数据的规模来确定;
- 在 PTB 数据集上学习语言模型的情况下,当 LSTM 的层数为 2 ~ 4 时,可以获得比较好的结果
1.2 dropout
详细可以再看一下:https://1drv.ms/b/s!AvF6gzVaw0cNjqYc_hrdnGUeqyCXWg?e=hn5AoB;
1.2.1具体概念
通过加深层,可以创建表现力更强的模型,但是这样的模型往往会发生过拟合;过拟合即模型泛化能力差;
RNN 比常规的前馈神经网络更容易发生过拟合,因此 RNN 的过拟合对策非常重要;
如何缓解过拟合:
- 优先考虑:一是增加训练数据;二是降低模型的复杂度
- 还有正则化的方法;即给予惩罚;
- dropout这种,在训练时随机忽略层的一部分(比如 50%)神经元,也可以被视为一种正则化;
dropout的具体结构
Dropout 随机选择一部分神经元,然后忽略它们,停止向前传递信号;具体而言,训练时,每传递一次数据,就会随机选择要删除的神经元。 然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出, 要乘上训练时的删除比例后再输出。
下图是概念图:
dropout的代码如下(直接看了书上的结果,没有自己做一遍):
- forward函数中,是一个判断;训练时需要按照设置的比例忽略一些神经元,忽略的方法就是将x中某些元素值设置为0;测试时传递所有的神经元信号,但是都要乘上删除比例;
- 正向传播时没有传递信号的神经元,反向传播时信号就停在那里;换句话说,这些神经元
dout*self.mask
的值为0,即这些神经元的梯度不会影响后续的反向传播;
下图是书上的一个实验:
- 加了dropout之后,训练集和测试集上的表现更相近,说明缓解了过拟合;
1.2.2应该插入到LSTM模型的哪里
不能在水平方向上插入
水平方向上是这样插入的:
由于每次dropout都有神经元不进行前向计算,这存在信息丢失;
并且我们会设置stateful参数为true,即下一次Time LSTM前向计算时会继承上一次Time LSTM的最后一个LSTM层的隐藏状态;这样一来,这个信息丢失会随着时间的延续而不断累积;
信息丢失多了,那就是说有用的信息少了,噪声多了;
所以不建议在水平方向上加入dropout
因此可以在垂直方向上插入;如下图所示;这样水平方向上不存在信息丢失,因此至少输入的数据的信息都学到了;
但是也有方法来实现水平方向上的dropout(正则化)
“变分 Dropout”(variational dropout):同一层的 Dropout 使用相同的 mask。这里所说的 mask 是指决定是否传递数据的随机布尔值;这样每一层信息丢失的都是那一部分;那么在其它层的dropout信息丢失的又都是另外一部分,这样互相找补找补就会好很多;
1.3权重共享
通过前面了解LSTM的结构,我们可以感受到,LSTM的参数量是RNN的好几倍;加上多层化的话,参数量更多;因此可以适当减少一下参数量,在能够保证模型效果的前提下;而且参数量减少了还能抑制过拟合;见论文;
权重共享前提是得两个层的权重确实是一样的;像本书中这里说的权重共享,是将Embedding 层和 Softmax 前的 Affine 层共享权重;这两个部分用的权重维度是一样的;
- 具体而言,Embedding层的权重维度为
(V,H)
;Affine 层的权重维度为(H,V)
; - 因此只需将 Embedding 层权重的转置设置为 Affine 层的权重即可;
- 具体而言,Embedding层的权重维度为
2改进之后的LSTMLM的代码实现
本书学习过程中的代码已全部搬运至gitee:https://gitee.com/gaoyan1518/basic-nlp;
代码位于:
LSTM_model/better_LSTMLM.py
;
首先需要构建一下Time Dropout层;由于Dropout层只是单纯的给某些神经元遮蔽掉,多个时刻也是一样的处理,时刻与时刻之间的Dropout层是独立的;所以不需要像Time LSTM层那样在forward里面循环T次;
另外这里在实现的时候,是在训练阶段就进行了缩放,所以测试阶段直接就返回了
x
,代码如下;如果训练阶段没有进行缩放,则测试阶段就要对x
乘上删除比例;1.0 - self.dropout_ratio
是删除比例;【可以看这个博客】class TimeDropout: def __init__(self, dropout_ratio=0.5): self.params, self.grads = [], [] self.dropout_ratio = dropout_ratio self.mask = None self.train_flg = True def forward(self, xs): if self.train_flg: flg = cupy.random.rand(*xs.shape) > self.dropout_ratio scale = 1 / (1.0 - self.dropout_ratio) # 删除比例的倒数;删除的越多,剩余神经元的权重就越小 self.mask = flg.astype(cupy.float32) * scale return xs * self.mask else: return xs def backward(self, dout): return dout * self.mask
2.1初始化
与之前的LSTMLM相比,这里就是多层化了、权重共享了、加入了dropout,其他的都一样;代码如下:
class BetterRnnlm: def __init__(self, vocab_size=10000, wordvec_size=650, hidden_size=650, dropout_ratio=0.5): V, D, H = vocab_size, wordvec_size, hidden_size rn = np.random.randn embed_W = (rn(V, D) / 100).astype('f') # 与affine_w权重共享 lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f') # 同样是一个整合之后的权重矩阵 lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f') lstm_b1 = np.zeros(4 * H).astype('f') lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f') lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f') lstm_b2 = np.zeros(4 * H).astype('f') affine_b = np.zeros(V).astype('f') if GPU: embed_W = to_gpu(embed_W) lstm_Wx1 = to_gpu(lstm_Wx1) lstm_Wx2 = to_gpu(lstm_Wx2) lstm_Wh1 = to_gpu(lstm_Wh1) lstm_Wh2 = to_gpu(lstm_Wh2) lstm_b1 = to_gpu(lstm_b1) lstm_b2 = to_gpu(lstm_b2) affine_b = to_gpu(affine_b) self.layers = [ TimeEmbedding(embed_W), TimeDropout(dropout_ratio), TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True), TimeDropout(dropout_ratio), TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True), TimeDropout(dropout_ratio), TimeAffine(embed_W.T, affine_b) # weight tying!! ] self.loss_layer = TimeSoftmaxWithLoss() self.lstm_layers = [self.layers[2], self.layers[4]] self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]] self.params, self.grads = [], [] for layer in self.layers: self.params += layer.params self.grads += layer.grads
2.2前向计算
和之前一样,只不过这里需要先把dropout层设置为训练模式;如下图所示:
代码如下:
def predict(self, xs, train_flg=False): ''' @param xs: (N, T);输入的数据; @param train_flg: 是否是训练阶段的标记;用于dropout层的计算方式选择; @return xs: (N,T,V);Time Affine层之后的输出,即得分;''' for layer in self.drop_layers: # 先将所有的dropout层都设置为训练模式 layer.train_flg = train_flg for layer in self.layers: xs = layer.forward(xs) return xs def forward(self, xs, ts, train_flg=True): ''' @param xs: (N, T);输入的数据; @param ts: (N, T);监督数据;(N,T)时不是独热编码形式; @param train_flg: 是否是训练阶段的标记;用于dropout层的计算方式选择; @return loss: 损失值;''' score = self.predict(xs, train_flg) loss = self.loss_layer.forward(score, ts) return loss
2.3反向传播
反向传播和LSTMLM一样;另外,重设状态的函数稍有区别;再就是还有加载和保存参数的函数;如下图所示;
3相应的学习代码的实现
代码位于:
LSTM_model/train_better_LSTMLM.py
;
由于这里将Embedding层和Affine层进行权重共享,因此在
RNN_model/rnnTrainer.py
的RnnlmTrainer.fit
函数中,需要在梯度计算之后整合相同的权重对应的梯度值;即加入了remove_duplicate
函数;如下图所示;在学习过程中,每学习一个epoch,就在验证集上计算困惑度,如果困惑度变大了,则降低学习率,让模型训练更加稳定;
所以这里还需要读取验证集数据;与读取测试集数据一样;同样考虑了GPU的运行;
我们只需要在调用
RnnlmTrainer.fit
函数时指定max_epoch=1
就可以复用RnnlmTrainer.fit
函数的同时,满足此处每个epoch都进行验证的需求;验证前通过model.reset_state()重置模型的状态(即Time LSTM层的初始h和c置为空);验证完开启下一轮训练前也重置一次;
- 回忆改进前LSTMLM的学习过程,我们好像没有在每个epoch结束后重置状态;这里我自己试了一下在每个epoch开始前都重置初始的状态,结果和没有重置差不多;
- 那这里重置的意义在于,因为要看在验证集上的表现,因此来自训练集训练好的状态当然不能要,否则就收到训练集影响了;
通过调用
ppl = eval_perplexity(model, corpus_val)
在测试集上走一遍前向计算的流程,获取模型在验证集上的困惑度值;读取数据的地方与RnnlmTrainer.fit
函数中的有所不同,但本质上是一样的;eval_perplexity
函数代码如下:def eval_perplexity(model, corpus, batch_size=10, time_size=35): print('evaluating perplexity ...') corpus_size = len(corpus) total_loss, loss_cnt = 0, 0 max_iters = (corpus_size - 1) // (batch_size * time_size) jump = (corpus_size - 1) // batch_size for iters in range(max_iters): xs = np.zeros((batch_size, time_size), dtype=np.int32) ts = np.zeros((batch_size, time_size), dtype=np.int32) time_offset = iters * time_size # 每过一个iters就跳过每个句子的time_size个单词 offsets = [time_offset + (i * jump) for i in range(batch_size)] for t in range(time_size): for i, offset in enumerate(offsets): # 这里xs和ts是都从corpus中获取; xs[i, t] = corpus[(offset + t) % corpus_size] ts[i, t] = corpus[(offset + t + 1) % corpus_size] try: xs = to_gpu(xs) ts = to_gpu(ts) loss = model.forward(xs, ts, train_flg=False) # 将dropout层设置为测试模式 except TypeError: xs = to_gpu(xs) ts = to_gpu(ts) loss = model.forward(xs, ts) total_loss += loss # 实现了一个进度条 sys.stdout.write('\r%d / %d' % (iters, max_iters)) sys.stdout.flush() print('') ppl = np.exp(total_loss / max_iters) # 返回验证集上的困惑度 return ppl
从生成模型开始的学习代码如下:
# 生成模型 model = BetterRnnlm(vocab_size, wordvec_size, hidden_size) optimizer = SGD(lr) trainer = RnnlmTrainer(model, optimizer) best_ppl = float('inf') for epoch in range(max_epoch): # 每次的fit都只训练一个epoch,所以传入的max_epoch=1 trainer.fit(xs, ts, max_epoch=1, batch_size=batch_size, time_size=time_size, max_grad=max_grad) model.reset_state() ppl = eval_perplexity(model, corpus_val) # 在验证集上进行前向计算然后评价困惑度 print('valid perplexity: ', ppl) if best_ppl > ppl: # 此时这一个epoch的模型效果更好 best_ppl = ppl model.save_params() else: # 否则困惑度更大,模型效果变差,则调整学习率 lr /= 4.0 optimizer.lr = lr model.reset_state() # 验证完了之后下个epoch训练前还要重置一下h和c print('-' * 50) # 基于验证数据进行评价 model.reset_state() ppl_test = eval_perplexity(model, corpus_test) print('test perplexity: ', ppl_test)
训练过程和结果如下:
由于堆叠了Time LSTM层,网络的参数量大幅增加,且相关超参数也增加了;可以看到显卡占用比原来增加了:
每次训练完都会进行验证集上的困惑度的计算:
下图是训练结果;训练集上最终的困惑度降低到了三四十;验证集上困惑度为79;测试集上的困惑度为76;如下图所示:
4总结
- 本章我们用LSTM来代替RNN,从而缓解梯度消失的问题,能够控制梯度的流动;
- 本节咱们加深了神经网络的层数,为了防止过拟合,加入了dropout层;另外,对于能够权重共享的,我们进行权重共享;实现了效果的提升;
- 书上还有一部分是其他的一些研究,这里就不说了;