【人工智能学习】【十一】循环神经网络进阶

Badia ·
更新时间:2024-05-17
· 508 次阅读

RNN的问题

RNN(Recurrent Neural Network,循环神经网络)主要应用在自然语言处理、机器翻译、情感分析、时序序列问题。这些的功能的共同特点是具有时序性。卷积神经网络是没有记忆性的(我对这句话的理解是神经元之间没有信息传递,各个WWW矩阵是独立计算的,当然不是说整个网络没有记忆,只是记忆是独立的),RNN通过神经元之间的信息传递保留了记忆(就是一个state变量,加变量是为了增加模型的非线性表达能力),但在长序列,即长时间步的问题上,梯度消失会让网络变得不可训练
在这里插入图片描述
Ht=f(XtWxh+Hh−1Whh+bh)H_t=f(X_tW_{xh}+H_{h-1}W_{hh}+b_h)Ht​=f(Xt​Wxh​+Hh−1​Whh​+bh​)
在【人工智能学习】【六】循环神经网络中介绍了RNN的结构,RNN需要按照时间序列进行展开可能导致梯度消失和梯度爆炸的问题【人工智能学习】【八】梯度消失与梯度爆炸,梯度爆炸我们可以dropout,做正则化来解决。
长短期记忆网络(LSTM,Long Short-Term Memory)(1997年)和后来出现的GRU模型(2014年),都解决了梯度消失和语义前后顺序的问题。这两者差不多,但是为啥又出现了了GRU,论文里说是它比LSTM好算。

LSTM

长短期记忆网络(LSTM,Long Short-Term Memory),RNN的变种。
在这里插入图片描述
上图对比RNN,似乎结构上差不多,但是里面多了很多东西。但是模型虽然复杂了,不要忘了诞生于1997年的LSTM是解决了RNN的问题:梯度消失和长序列记忆(说白了就是和前面离得太远了,梯度传过来已经接近于0了)。所以这些结构看上去应该是和梯度消失问题有关。
RNN的神经元节点上有两个输入:1、ttt时刻的输入XtX_tXt​;2、t−1t-1t−1时刻的隐含层节点传过来的状态Ht−1H_{t-1}Ht−1​。

状态C的引出

现在得出一个很直观的结论:既然当前节点无法记忆到很长时间序列之前的信息,那么我再开辟一条通道,用来传递之前的信息。即在RNN上加上一条传送带(adding a carry track),这条传送带上有之前神经元节点的记忆信息Ct−1C_{t-1}Ct−1​(C是carry track里的C),这里的Ct−1C_{t-1}Ct−1​尽管也是一个状态,好像和之前神经元节点的Ht−1H_{t-1}Ht−1​(state)没什么区别。但是RNN中的Ht−1H_{t-1}Ht−1​是每次都会进行下面的计算来更新,并传递到下一个神经元节点。
Ht=f(XtWxh+Hh−1Whh+bh)H_t=f(X_tW_{xh}+H_{h-1}W_{hh}+b_h)Ht​=f(Xt​Wxh​+Hh−1​Whh​+bh​)
这里的这个Ct−1C_{t-1}Ct−1​就不是这么计算的了,极端情况试想一下,Ct−1C_{t-1}Ct−1​永远只参与Ht−1H_{t-1}Ht−1​的计算,但是自己永远不被计算,初始的C0C_{0}C0​值会沿着这条carry track,从第一个节点传到最后一个节点,途中值是复制一份自己来计算Ht−1H_{t-1}Ht−1​的值。
CCC既然是传送各个神经元的状态,那么CtC_tCt​应该是要和下面这3个变量有关系:Ct−1C_{t-1}Ct−1​、XtX_{t}Xt​、Ht−1H_{t-1}Ht−1​。Ct−1C_{t-1}Ct−1​负责将上一层的信息传过来,XtX_{t}Xt​负责计算本时刻节点的状态,是否和上一层的Ht−1H_{t-1}Ht−1​有关。
所以,现在HtH_{t}Ht​的计算和3个变量有关了:Ht−1H_{t-1}Ht−1​、Ct−1C_{t-1}Ct−1​、XtX_{t}Xt​,加入到公式中
Ht=f(XtWxh+Hh−1Whh+Ct−1W+bh)H_t=f(X_tW_{xh}+H_{h-1}W_{hh}+C_{t-1}W+b_h)Ht​=f(Xt​Wxh​+Hh−1​Whh​+Ct−1​W+bh​)
这里先用Ct−1WC_{t-1}WCt−1​W暂时表示用Ct−1C_{t-1}Ct−1​对模型起到的作用。

神经元节点的思考

如果神经元节点自己会思考,他会做什么事?
1、传过来的状态CCC我不仅自己用,我还会加上自己的状态把他继续往后传。(传话人设,这是基本人设)
2、传过来的状态CCC我不想用,那么我有遗忘的权利,我只想把我这个节点产生的状态CCC传递到未来。(称作霸道人设,这是霸道加持)

传话人设对应的是输入门,反映到数学上是WinputW_{input}Winput​,缩写成WiW_{i}Wi​,这个输入门指的是输入到CtC_tCt​中的状态,控制有多少XtX_tXt​的信息要被保存到状态CtC_tCt​上。
霸道人设对应的是遗忘门,反映到数学上是WforgetW_{forget}Wforget​,缩写成WfW_{f}Wf​,控制有多少Ct−1C_{t-1}Ct−1​要被丢弃。

考虑输入门是和XtX_tXt​、Ht−1H_{t-1}Ht−1​有关,那么
It=XtXxi+Ht−1Whi+biI_t=X_tX_{xi}+H_{t-1}W_{hi}+b_iIt​=Xt​Xxi​+Ht−1​Whi​+bi​
这个公式就是之前RNN的公式,这里的输入门还没有用到Ct−1C_{t-1}Ct−1​呢

考虑遗忘门是和XtX_tXt​、Ht−1H_{t-1}Ht−1​、Ct−1C_{t-1}Ct−1​有关。那么
Ft=XtWxf+Ht−1Whf+bfF_t=X_tW_{xf}+H_{t-1}W_{hf}+b_fFt​=Xt​Wxf​+Ht−1​Whf​+bf​

原来RNN的输出这里叫输出门,和XtX_tXt​、Ht−1H_{t-1}Ht−1​有关
Ot=XtWxo+Ht−1Who+boO_t=X_tW_{xo}+H_{t-1}W_{ho}+b_oOt​=Xt​Wxo​+Ht−1​Who​+bo​

遗忘门、输入门、输出门上都有sigmod函数,将值映射到[0,1][0,1][0,1]之间,等于0意味着全部丢弃,等于1意味着全部保留,这个结果就是通过门的信息量大小。所以上面三个式子的最终形态如下:
It=sigmod(XtXxi+Ht−1Whi+bi)I_t=sigmod(X_tX_{xi}+H_{t-1}W_{hi}+b_i)It​=sigmod(Xt​Xxi​+Ht−1​Whi​+bi​)
Ft=sigmod(XtWxf+Ht−1Whf+bf)F_t=sigmod(X_tW_{xf}+H_{t-1}W_{hf}+b_f)Ft​=sigmod(Xt​Wxf​+Ht−1​Whf​+bf​)
Ot=sigmod(XtWxo+Ht−1Who+bo)O_t=sigmod(X_tW_{xo}+H_{t-1}W_{ho}+b_o)Ot​=sigmod(Xt​Wxo​+Ht−1​Who​+bo​)

到这里还是没有完,还有一个候选记忆,它的计算是:
Ct~=tanh(XtWxc+Ht−1Whc+bc)\widetilde{C_t}=tanh(X_tW_{xc}+H_{t-1}W_{hc}+b_c)Ct​​=tanh(Xt​Wxc​+Ht−1​Whc​+bc​)
最终的长期记忆状态CtC_tCt​的计算公式:
Ct=Ft⋅Ct−1+It⋅Ct~C_t=F_t·C_{t-1}+I_t·\widetilde{C_t}Ct​=Ft​⋅Ct−1​+It​⋅Ct​​
候选记忆是将要记忆的XtX_tXt​和Ht−1H_{t-1}Ht−1​通过tanh激活函数放缩到[−1,1][-1,1][−1,1]之间,然后通过输入门训练的WiW_iWi​来决定哪些记忆将会被记忆。如果不通过tanh将XtX_tXt​和Ht−1H_{t-1}Ht−1​的计算结果放缩到[−1,1][-1,1][−1,1]之间,状态CtC_tCt​可能会爆炸了(没验证)。我觉得这样理解不是很好,但是这个东西感觉和输入门有点功能重复。

最后的输出计算公式:
Ht=Ot⋅tanh(Ct)H_t=O_t·tanh(C_t)Ht​=Ot​⋅tanh(Ct​)

代码

参数初始化

def get_params(): def _one(shape): ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32) return torch.nn.Parameter(ts, requires_grad=True) def _three(): return (_one((num_inputs, num_hiddens)), _one((num_hiddens, num_hiddens)), torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True)) W_xi, W_hi, b_i = _three() # 输入门参数 W_xf, W_hf, b_f = _three() # 遗忘门参数 W_xo, W_ho, b_o = _three() # 输出门参数 W_xc, W_hc, b_c = _three() # 候选记忆细胞参数 # 输出层参数 W_hq = _one((num_hiddens, num_outputs)) b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True) return nn.ParameterList([W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q]) def init_lstm_state(batch_size, num_hiddens, device): return (torch.zeros((batch_size, num_hiddens), device=device), torch.zeros((batch_size, num_hiddens), device=device))

模型定义

def lstm(inputs, state, params): [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params (H, C) = state outputs = [] for X in inputs: I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i) F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f) O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o) C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c) C = F * C + I * C_tilda H = O * C.tanh() Y = torch.matmul(H, W_hq) + b_q outputs.append(Y) return outputs, (H, C)

模型训练

num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2 pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开'] d2l.train_and_predict_rnn(lstm, get_params, init_lstm_state, num_hiddens, vocab_size, device, corpus_indices, idx_to_char, char_to_idx, False, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes) torch简介实现 num_hiddens=256 num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2 pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开'] lr = 1e-2 # 注意调整学习率 lstm_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens) model = d2l.RNNModel(lstm_layer, vocab_size) d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device, corpus_indices, idx_to_char, char_to_idx, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes) GRU

门控循环单元(GRU),RNN的变种,LSTM的变种。GRU 保持了 LSTM 的效果同时又使结构更加简单。GRU 只剩下两个门,即更新门和重置门。在LSTM中,我们是引出了一个carry track,也就是状态CCC,这个在LSTM是单独一条计算链。跟着GRU思考,**Ct−1C_{t-1}Ct−1​和RNN中的Ht−1H_{t-1}Ht−1​是不是可以合并呢?**GRU就是这样设计的。

在这里插入图片描述

计算重置门和更新门

重置门(reset gate):决定有多少信息被忽略。
更新门(update gate):决定有多少前一个神经元传来的信息被用到这个神经元的计算。
来看重置门和更新门的计算
在这里插入图片描述
显然
Rt=sigmod(XtWxr+Ht−1Whr+br)R_t=sigmod(X_tW_{xr}+H_{t-1}W_{hr}+b_r)Rt​=sigmod(Xt​Wxr​+Ht−1​Whr​+br​)
Zt=sigmod(XtWxz+Ht−1Whz+bz)Z_t=sigmod(X_tW_{xz}+H_{t-1}W_{hz}+b_z)Zt​=sigmod(Xt​Wxz​+Ht−1​Whz​+bz​)
RtR_tRt​ 和更新ZtZ_tZt​ 的值域都为[0, 1],公式很简洁

计算候选隐藏状态

在这里插入图片描述
Ht~=tanh(XtWxh+(Rt⋅Ht−1)Whh+bh)\widetilde{H_t}=tanh(X_tW_{xh}+(R_t·H_{t-1})W_{hh}+b_h)Ht​​=tanh(Xt​Wxh​+(Rt​⋅Ht−1​)Whh​+bh​)

输出

在这里插入图片描述
按照上图的计算方式,最后输出的HtH_tHt​为:
Ht=Zt⋅Ht−1+(1−Zt)⋅Ht~H_t=Z_t·H_{t-1}+(1-Z_t)·\widetilde{H_t}Ht​=Zt​⋅Ht−1​+(1−Zt​)⋅Ht​​

代码

载入数据集

import numpy as np import torch from torch import nn, optim import torch.nn.functional as F import os import sys import d2l_jay9460 as d2l device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') (corpus_indices, char_to_idx, idx_to_char, vocab_size) = d2l.load_data_jay_lyrics()

初始化

num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size print('will use', device) def get_params(): def _one(shape): ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32) #正态分布 return torch.nn.Parameter(ts, requires_grad=True) def _three(): return (_one((num_inputs, num_hiddens)), _one((num_hiddens, num_hiddens)), torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True)) W_xz, W_hz, b_z = _three() # 更新门参数 W_xr, W_hr, b_r = _three() # 重置门参数 W_xh, W_hh, b_h = _three() # 候选隐藏状态参数 # 输出层参数 W_hq = _one((num_hiddens, num_outputs)) b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True) return nn.ParameterList([W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]) def init_gru_state(batch_size, num_hiddens, device): #隐藏状态初始化 return (torch.zeros((batch_size, num_hiddens), device=device), )

模型定义

def gru(inputs, state, params): W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params H, = state outputs = [] for X in inputs: Z = torch.sigmoid(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z) R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r) H_tilda = torch.tanh(torch.matmul(X, W_xh) + R * torch.matmul(H, W_hh) + b_h) H = Z * H + (1 - Z) * H_tilda Y = torch.matmul(H, W_hq) + b_q outputs.append(Y) return outputs, (H,)

模型训练

num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2 pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开'] d2l.train_and_predict_rnn(gru, get_params, init_gru_state, num_hiddens, vocab_size, device, corpus_indices, idx_to_char, char_to_idx, False, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes) torch简洁实现 num_hiddens=256 num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2 pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开'] lr = 1e-2 # 注意调整学习率 gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens) model = d2l.RNNModel(gru_layer, vocab_size).to(device) d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device, corpus_indices, idx_to_char, char_to_idx, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes) 深度循环神经网络

在这里插入图片描述
深度循环神经网络属于一种模型层次结构上的改进,考虑的是模型复杂度(模型的表现力)
Ht(1)=f(XtWxh(1)+Ht−1(1)Whh(1)+bh(1))H^{(1)}_t=f(X_tW_{xh}^{(1)}+H_{t-1}^{(1)}W_{hh}^{(1)}+b_h^{(1)})Ht(1)​=f(Xt​Wxh(1)​+Ht−1(1)​Whh(1)​+bh(1)​)
Ht(l)=f(XtWxh(l)+Ht−1(l)Whh(l)+bh(l))H^{(l)}_t=f(X_tW_{xh}^{(l)}+H_{t-1}^{(l)}W_{hh}^{(l)}+b_h^{(l)})Ht(l)​=f(Xt​Wxh(l)​+Ht−1(l)​Whh(l)​+bh(l)​)
深度循环神经网络的结构,RNN、LSTM、GRU,都可以扩展,就是把之前隐含层的输出(不是装填)当做输入,按序列输入到另一个RNN中、LSTM、GRU中,这个可以看做一层,多个层连接到一起就构成了深度循环神经网络。
所以这只是一个结构上的变化,代码实现

num_hiddens=256 num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2 pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开'] lr = 1e-2 # 注意调整学习率 gru_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens,num_layers=2) model = d2l.RNNModel(gru_layer, vocab_size).to(device) d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device, corpus_indices, idx_to_char, char_to_idx, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes)

num_layers=2意味着有2层。这里可以自己选择有多少层。不是层数越多越好,要根据模型。层数越多模型越复杂,对数据集要求就越多。要看实际情况。

双向循环神经网络(BRNN)

在这里插入图片描述
双向循环神经网络属于一种模型层次结构上的改进,BRNN考虑的是输入序列的反向序列对模型的影响。比如单靠前面一句话,没有办法得到准确结论的情况。
模型分两步计算:
1、正向计算,和普通RNN一样。
2、反向计算,逆向的计算和普通RNN一样。

Ht→=f(XtWxhf+Ht−1Whhf+bhf)\overrightarrow{H_t}=f(X_tW_{xh}^{f}+H_{t-1}W_{hh}^f+b_h^{f})Ht​​=f(Xt​Wxhf​+Ht−1​Whhf​+bhf​)
Ht←=f(XtWxhb+Ht−1Whhb+bhb)\overleftarrow{H_t}=f(X_tW_{xh}^{b}+H_{t-1}W_{hh}^b+b_h^{b})Ht​​=f(Xt​Wxhb​+Ht−1​Whhb​+bhb​)
向左的箭头是正向,向右的是反向。
现在得到两个H输出了。普通RNN的输出是下面这样:
Y=HtWhy+byY=H_tW_{hy}+b_yY=Ht​Why​+by​
双向的有两个HHH,做一个数学上的拼接
Ht=(Ht→,Ht←)H_t=(\overrightarrow{H_t},\overleftarrow{H_t})Ht​=(Ht​​,Ht​​)
然后
Y=HtWhy+byY=H_tW_{hy}+b_yY=Ht​Why​+by​

代码

torch封装的很好了,通过一个bidirectional=True开关来打开双向循环就好。

num_hiddens=128 num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e-2, 1e-2 pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开'] lr = 1e-2 # 注意调整学习率 gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens,bidirectional=True) model = d2l.RNNModel(gru_layer, vocab_size).to(device) d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device, corpus_indices, idx_to_char, char_to_idx, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes)
作者:番茄发烧了



循环 学习 神经网络 人工智能 循环神经网络

需要 登录 后方可回复, 如果你还没有账号请 注册新账号