1 循环神经网络概述
在普通的神经网络中,信息是单项的,这种限制虽然使得网络变得更容易学习,但也在一定程度上减弱了神经网络模型的能力。特别是在很多现实任务中,网络的输出不仅和当前时刻的输入相关,也和过去一段时间内的输出相关。此外,普通网络难以处理时序数据,比如视频、语音、文本等,时间数据的长度一般不是固定的,而前馈神经网络要求输入和输出的位数都是固定的,不能任意改变。因此,当处理这一类和时序相关的问题时,就需要一种更强的模型。
循环神经网络(Recurrent Neural Network, RNN)是一类具有短期记忆能力的神经网络。在循环神经网络中,神经元不但可以接受其他神经元的信息,也可以接受自身的信息,形成具有环路的网络结构。换句话说:神经元的输出可以在下一个时间步直接作用到自身(作为输入)
通过简化图,可以看到RNN比传统神经网络多了一个循环圈,这个循环圈表示的是在下一个时间步(Time step)上会返回作为输入的一部分,把RNN在时间点上展开,得到的图形如下:
或者是:
在不同的时间步,RNN的输入都将与之前的时间状态有关,tn时刻网络的输出结果是该时刻的输入和所有历史共同作用的结果,这就达到了对使时间序列建模的目的。
2 RNN基本结构
-
one to one
图1: 固定长度的输入和输出(e.g.图像分类)
最基本的单层网络,输入是 x x x,经过变换 W x + b Wx+b Wx+b和激活函数 f f f得到输出 y y y。 -
one to n
图2:序列输出(e.g.图像转文字)
输入不是序列而输出为序列的情况,只在序列开始进行输入计算: 圆圈或方块表示的是向量。一个箭头就表示对该向量做一次变换。如上图中 h 0 h_0 h0和 x x x分别有一个箭头连接,就表示对 h 0 h_0 h0和 x x x各做了一次变换。
还有一种结构是把输入信息 X X X作为每个阶段的输入:
下图省略了一些X的圆圈,是一个等价表示:
这种 one-to-n 的结构可以处理的问题有:
(1) 从图像生成文字(image caption),此时输入的X就是图像的特征,而输出的y序列就是一段句子,就像看图说话等
(2) 从类别生成语音或音乐等 -
n to one
图3:数列输入(e.g.文本分类)
要处理的问题输入是一个序列,输出是一个单独的值而不是序列,应该怎样建模呢?实际上,我们只在最后一个h上进行输出变换就可以了:
这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。 -
n to n
图4:异步的序列输入和输出(e.g.文本翻译)。图5:同步的序列输入和输出(e.g.根据视频的每一帧来对视频进行分类)
最经典的RNN结构,输入、输出都是等长的序列数据。假设输入为 X = ( x 1 , x 2 , x 3 , x 4 ) X=(x_1, x_2, x_3, x_4) X=(x1,x2,x3,x4),每个 x i x_i xi是一个单词的词向量。
为了建模序列问题,RNN引入了隐状态 h h h(hidden state)的概念, h h h可以对序列形的数据提取特征,接着再转换为输出。先从 h 1 h_1 h1的计算开始看:
h 2 h_2 h2的计算和 h 1 h_1 h1类似。要注意的是,在计算时,每一步使用的参数 U 、 W 、 b U、W、b U、W、b都是一样的,也就是说每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记。
依次计算剩下来的(使用相同的参数 U 、 W 、 b U、W、b U、W、b):
这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。得到输出值的方法就是直接通过 h h h进行计算:
正如之前所说,一个箭头就表示对对应的向量做一次类似于 f ( V x + c ) f(Vx+c) f(Vx+c)的变换,这里的这个箭头就表示对 h 1 h_1 h1进行一次变换,得到输出 y 1 y_1 y1。
剩下的输出类似进行(使用和 y 1 y_1 y1同样的参数 V V V和 c c c):
这就是最经典的RNN结构,它的输入是 x 1 , x 2 , … . . x n x_1, x_2, …..x_n x1,x2,…..xn,输出为 y 1 , y 2 , … y n y_1, y_2, …y_n y1,y2,…yn,也就是说,输入和输出序列必须要是等长的。由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:
计算视频中每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。
输入为字符,输出为下一个字符的概率。这就是著名的Char RNN(详细介绍请参考:The Unreasonable Effectiveness of Recurrent Neural Networks,Char RNN可以用来生成文章,诗歌,甚至是代码,非常有意思)。
3 RNN训练过程
其实RNN存在着两种训练模式(mode):
- free-running mode
- teacher-forcing mode
free-running mode就是大家常见的那种训练网络的方式: 上一个state的输出作为下一个state的输入。
teacher-forcing mode是一种快速有效地训练循环神经网络模型的方法,该模型每次不使用上一个state的输出作为下一个state的输入,而是直接使用训练数据的标准答案(ground truth)的对应上一项作为下一个state的输入。所谓Teacher Forcing,就是在学习时跟着老师(ground truth)走!它是一种网络训练方法,对于开发用于机器翻译,文本摘要,图像字幕的深度学习语言模型以及许多其他应用程序至关重要。
4 RNN优缺点
①RNN优点
- 针对CNN中无法对时间序列上的变化进行建模的局限,为了适应对时序数据的处理,出现了RNN。在普通的全连接网络或者CNN中,每层神经元的信号只能向上一层传播,样本的处理在各个时刻独立(这种就是前馈神经网络)。而在RNN中,神经元的输出可以在下一个时间戳直接作用到自身。(t+1)时刻网络的最终结果O(t+1)是该时刻输入和所有历史共同作用的结果,这就达到了对时间序列建模的目的。
- 内部结构简单,计算需要的时间空间低,参数量少,对于短序列的任务表现很好。
②RNN缺点
- 在解决长序列之间的关联时,RNN表现很差,在进行反向传播时,由于过长的序列,导致梯度消失或梯度爆炸。
5 RNN代码实现
import torch
import datetime
import numpy as np
import torch.nn as nn
import torch.optim as optim
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['FangSong']
mpl.rcParams['axes.unicode_minus'] = False
###########################设置全局变量###################################
num_time_steps = 16 # 训练时时间窗的步长
input_size = 3 # 输入数据维度
hidden_size = 16 # 隐含层维度
output_size = 3 # 输出维度
num_layers = 1
lr=0.01
####################定义RNN类##############################################
class Net(nn.Module):
def __init__(self, input_size, hidden_size, num_layers):
super(Net, self).__init__()
self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
)
for p in self.rnn.parameters():
nn.init.normal_(p, mean=0.0, std=0.001)
self.linear = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden_prev):
out, hidden_prev = self.rnn(x, hidden_prev)
# [b, seq, h]
out = out.view(-1, hidden_size)
out = self.linear(out)#[seq,h] => [seq,3]
out = out.unsqueeze(dim=0) # => [1,seq,3]
return out, hidden_prev
####################初始化训练集#################################
def getdata():
x1 = np.linspace(1,10,30).reshape(30,1)
y1 = (np.zeros_like(x1)+2)+np.random.rand(30,1)*0.1
z1 = (np.zeros_like(x1)+2).reshape(30,1)
tr1 = np.concatenate((x1,y1,z1),axis=1)
# mm = MinMaxScaler()
# data = mm.fit_transform(tr1) #数据归一化
return tr1
#####################开始训练模型#################################
def tarin_RNN(data):
model = Net(input_size, hidden_size, num_layers)
print('model:\n',model)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr)
#初始化h
hidden_prev = torch.zeros(1, 1, hidden_size)
l = []
# 训练3000次
for iter in range(3000):
# loss = 0
start = np.random.randint(10, size=1)[0]
end = start + 15
x = torch.tensor(data[start:end]).float().view(1, num_time_steps - 1, 3)
# 在data里面随机选择15个点作为输入,预测第16
y = torch.tensor(data[start + 5:end + 5]).float().view(1, num_time_steps - 1, 3)
output, hidden_prev = model(x, hidden_prev)
hidden_prev = hidden_prev.detach()
loss = criterion(output, y)
model.zero_grad()
loss.backward()
optimizer.step()
if iter % 100 == 0:
print("Iteration: {} loss {}".format(iter, loss.item()))
l.append(loss.item())
##############################绘制损失函数#################################
plt.plot(l,'r')
plt.xlabel('训练次数')
plt.ylabel('loss')
plt.title('RNN损失函数下降曲线')
return hidden_prev,model
#############################预测#########################################
def RNN_pre(model,data,hidden_prev):
data_test = data[19:29]
data_test = torch.tensor(np.expand_dims(data_test, axis=0),dtype=torch.float32)
pred1,h1 = model(data_test,hidden_prev )
print('pred1.shape:',pred1.shape)
pred2,h2 = model(pred1,hidden_prev )
print('pred2.shape:',pred2.shape)
pred1 = pred1.detach().numpy().reshape(10,3)
pred2 = pred2.detach().numpy().reshape(10,3)
predictions = np.concatenate((pred1,pred2),axis=0)
# predictions= mm.inverse_transform(predictions)
print('predictions.shape:',predictions.shape)
#############################预测可视化########################################
fig = plt.figure(figsize=(9, 6))
ax = Axes3D(fig)
ax.scatter3D(data[:, 0],data[:, 1],data[:,2],c='red')
ax.scatter3D(predictions[:,0],predictions[:,1],predictions[:,2],c='y')
ax.set_xlabel('X')
ax.set_xlim(0, 8.5)
ax.set_ylabel('Y')
ax.set_ylim(0, 10)
ax.set_zlabel('Z')
ax.set_zlim(0, 4)
plt.title("RNN航迹预测")
plt.show()
def main():
data = getdata()
start = datetime.datetime.now()
hidden_pre, model = tarin_RNN(data)
end = datetime.datetime.now()
print('The training time: %s' % str(end - start))
plt.show()
RNN_pre(model, data, hidden_pre)
if __name__ == '__main__':
main()
6 RNN变体
6.1 Encoder-Decoder
Encoder-Decoder 框架,Encoder-Decoder 不是一个具体的模型,是一种框架。在不同的NLP任务中,Encoder框架及Decoder框架均是由多个单独的特征提取器堆叠而成,比如说我们之前提到的LSTM结构或CNN结构。由最初的one-hot向量通过Encoder框架,我们将得到一个矩阵(或是一个向量),这就可以看作其对输入序列的一个编码。而对于Decoder结构就比较灵活了,我们可以根据任务的不同,对我们得到的“特征”矩阵或“特征”向量进行解码,输出为我们任务需要的输出结果。因此,对于不同的任务,如果我们堆叠的特征抽取器能够提取到更好的特征,那么理论上来说,在所有的NLP任务中我们都能够得到更好的表现。
Encoder-Decoder结构是 n-to-m,输入、输出为不等长的序列,也叫Seq2Seq,是RNN的一个重要变种。原始的n-to-n的RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。为此,Encoder-Decoder结构先将输入数据编码成一个上下文语义向量 c c c:
语义向量 c c c可以有多种表达方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c,还可以对最后的隐状态做一个变换得到c,也可以对所有的隐状态做变换。
拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。Decoder的RNN可以与Encoder的一样,也可以不一样。具体做法就是将c当做之前的初始状态 h 0 h_0 h0输入到Decoder中:
还有一种做法是将c当做每一步的输入:
Encoder-Decoder 应用
由于这种Encoder-Decoder结构不限制输入和输出的序列长度,因此应用的范围非常广泛,比如:
- 机器翻译:Encoder-Decoder的最经典应用,事实上这结构就是在机器翻译领域最先提出的。
- 文本摘要:输入是一段文本序列,输出是这段文本序列的摘要序列。
- 阅读理解:将输入的文章和问题分别编码,再对其进行解码得到问题的答案。
- 语音识别:输入是语音信号序列,输出是文字序列。
Encoder:将 input序列 →转成→ 固定长度的向量
Decoder:将 固定长度的向量 →转成→ output序列
Encoder 与 Decoder 可以彼此独立使用,实际上经常一起使用
因为最早出现的机器翻译领域,最早广泛使用的转码模型是RNN。其实模型可以是 CNN /RNN /BiRNN /LSTM /GRU /…
Encoder-Decoder 缺点
最大的局限性:编码和解码之间的唯一联系是固定长度的语义向量c
编码要把整个序列的信息压缩进一个固定长度的语义向量c,语义向量c无法完全表达整个序列的信息。先输入的内容携带的信息,会被后输入的信息稀释掉,或者被覆盖掉。输入序列越长,这样的现象越严重,这样使得在Decoder解码时一开始就没有获得足够的输入序列信息,解码效果会打折扣。因此,为了弥补基础的 Encoder-Decoder 的局限性,提出了attention机制。
6.2 Attention-Mechanism
注意力机制(attention mechanism)是对基础Encoder-Decoder的改良。Attention机制通过在每个时间输入不同的c来解决问题,下图是带有Attention机制的Decoder:
每一个c会自动去选取与当前所要输出的 y y y最合适的上下文信息。具体来说,我们用 a i j a_{ij} aij衡量Encoder中第 j j j阶段的 h j h_j hj和解码时第 i i i阶段的相关性,最终Decoder中第 i i i阶段的输入的上下文信息 c i c_i ci就来自于所有 h j h_j hj 对 a i j a_{ij} aij 的加权和。以机器翻译为例(将中文翻译成英文):
输入的序列是“我爱中国”,因此,Encoder中的 h 1 、 h 2 、 h 3 、 h 4 h_1、h_2、h_3、h_4 h1、h2、h3、h4就可以分别看做是“我”、“爱”、“中”、“国”所代表的信息。在翻译成英语时,第一个上下文 c 1 c_1 c1应该和 “我” 这个字最相关,因此对应的 a 11 a_{11} a11 就比较大,而相应的 a 12 、 a 13 、 a 14 a_{12}、a_{13}、a_{14} a12、a13、a14 就比较小。 c 2 c_2 c2应该和“爱”最相关,因此对应的 a 22 a_{22} a22 就比较大。最后的 c 3 c_3 c3和 h 3 、 h 4 h_3、h_4 h3、h4最相关,因此 a 33 、 a 34 a_{33}、a_{34} a33、a34 的值就比较大。