Pytorch_ 循环神经网络 RNN

RNN 是 Recurrent Neural Networks 的缩写,即循环神经网络,它常用于解决序列问题。RNN 有记忆功能,除了当前输入,还把上下文环境作为预测的依据。它常用于语音识别、翻译等场景之中。

RNN 是序列模型的基础,尽管能够直接调用现成的 RNN 算法,但后续的复杂网络很多构建在 RNN 网络的基础之上,如 Attention 方法需要使用 RNN 的隐藏层数据。RNN 的原理并不复杂,但由于其中包括循环,很难用语言或者画图来描述,最好的方法是自己手动编写一个 RNN 网络。本篇将介绍 RNN 网络的原理及具体实现。

序列

在学习循环神经网络之前,先看看什么是序列。序列 sequence 简称 seq,是有先后顺序的一组数据。自然语言处理是最为典型的序列问题,比如将一句话翻译成另一句话时,其中某个词汇的含义不仅取决于它本身,还与它前后的多个单词相关。类似的,如果想预测电影的情节发展,不仅与当前的画面有关,还与当前的一系列前情有关。在使用序列模型预测的过程中,输入是序列,而输出是一个或多个预测值。

在使用深度学习模型解决序列问题时,最容易混淆的是,序列与序列中的元素。在不同的场景中,定义序列的方式不同,当分析单词的感情色彩时,一个单词是一个序列 seq;当分析句子感情色彩时,一个句子是一个 seq,其中的每个单词是序列中的元素;当分析文章感情色彩时,一篇文章是一个 seq。简单地说,seq 是最终使用模型时的输入数据,由一系列元素组成。

当分析句子的感情色彩时,以句为 seq,而句中包含的各个单词的含义,以及单词间的关系是具体分析的对象,此时,单词是序列中的元素,每一个单词又可有多维特征。从单词中提取特征的方法将在后面的自然语言处理中介绍。

循环神经网络

RNN 有很多种形式,单个输入单个输入;多个输入多个输出,单个输入多个输出等等。

举个最简单的例子:用模型预测一个四字短语的感情色彩,它的输入为四个元素 X={x1,x2,x3,x4},它的输出为单个值 Y={y1}。字的排列顺序至关重要,比如“从好变坏”和“从坏变好”,表达的意思完全相反。之所以输入输出的个数不需要一一对应,是因为中间的隐藏层,变向存储中间信息。

如果把模型设想成黑盒,如下图所示:

如果模型使用全连接网络,在每次迭代时,模型将计算各个元素 x1,x2...中各个特征 f1,f2...代入网络,求它们对结果 y 的贡献度。

RNN 网络则要复杂一些,在模型内部,它不是将序列中所有元素的特征一次性输入模型,而是每一次将序列中单个元素的特征输入模型,下图描述了 RNN 的数据处理过程,左图为分步展示,右图将所有时序步骤抽象成单一模块。

第一步:将第一个元素 x1 的特征 f1,f2...输入模型,模型根据输入计算出隐藏层 h。

第二步:将第二个元素 x2 的特征输入模型,模型根据输入和上一步产生的 h 再计算隐藏层 h,其它元素以此类推。

第三步:将最后一个元素 xn 的特征输入模型,模型根据输入和上一步产生的 h 计算隐藏层 h 和预测值 y。

隐藏层 h 可视为将序列中前面元素的特征和位置通过编码向前传递,从而对输出 y 发生作用,隐藏层的大小决定了模型携带信息量的多少。隐藏层也可以作为模型的输入从外部传入,以及作为模型的输出返回给外部调用。

代码分析

本例仍使用上篇中的航空乘客序列数据,分别用两种方法实现 RNN:自己编写程序实现 RNN 模型,以及调用 Pytorch 提供的 RNN 模型。前一种方法主要用于剖析原理,后一种用于展示常用的调用方法。

1.读取数据

首先导入头文件,读取乘客数据,做归一化处理,并将数据切分为测试集和训练集,与之前不同的是加入了 create_dataset 函数,用于生成序列数据,序列的输入部分,每个元素中包括两个特征:前一个月的乘客量 prev 和月份值 mon,这里的月份值并不是关键特征,主要用于在例程中展示如何使用多个特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

import torch
import torch.nn as nn
from torch.autograd import Variable

# 归一化
def feature_normalize(data):
mu = np.mean(data,axis=0)
std = np.std(data,axis=0)
return (data - mu)/std

# 数据转换
def create_dataset(df):
dataX = torch.from_numpy(np.array(df[['prev','mon']].astype('float32')).reshape(-1, 1, 2))
dataY = torch.from_numpy(np.array(df['y'].astype('float32')).reshape(-1, 1, 1))
return dataX, dataY

df = pd.read_csv('data/AirPassengers.csv')
df['y'] = feature_normalize(df['#Passengers'])
df['mon'] = feature_normalize(df['Month'].apply(lambda x: float(x[5:])))
df['prev'] = df['y'].shift() # 将前一个月的乘客量写入prev特征
df = df.dropna() # 去掉包含空值的行

TRAIN_PERCENT = 0.7
train_size = int(len(df) * TRAIN_PERCENT)
train = df[:train_size]

2.实现 RNN 模型

第一步:实现模型类,此例中的 RNN 模型除了全连接层,还生成了一个隐藏层,并在下一次前向传播时将隐藏层输出的数据与输入数据组合后再代入模型运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RNN1(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN1, self).__init__()
self.hidden_size = hidden_size
self.rnn = nn.Linear(input_size + hidden_size, hidden_size)
self.reg = nn.Linear(input_size + hidden_size, output_size)

def forward(self, input, hidden):
combined = torch.cat((input, hidden), 1) # combined: [1,(128+6)]
hidden = self.rnn(combined)
output = self.reg(combined)
return output, hidden

def initHidden(self): # 初始化隐藏层
return Variable(torch.zeros(1, self.hidden_size))

第二步,训练模型,使用全部数据训练 500 次,在每次训练时,内部 for 循环将序列中的每个元素代入模型,并将模型输出的隐藏层和下一个元素一起送入下一次迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FEATURE_SIZE = 2
OUTPUT_SIZE = 1
model = RNN1(FEATURE_SIZE, 128, OUTPUT_SIZE)
loss_func = nn.MSELoss() # 损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 优化器
train_x, train_y = create_dataset(train)
all_x, all_y = create_dataset(df)

for e in range(500):
h_state = model.initHidden()
for i, (x,y) in enumerate(zip(train_x, train_y)):
prediction, _h_state = model(x, h_state) # 返回隐藏层
h_state = _h_state.data # 隐藏层的参数部分
loss = loss_func(prediction, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

第三步:预测和作图,预测的过程与训练一样,把全部数据拆分成元素代入模型,并将每一次预测结果存储在数组中,并作图显示。

1
2
3
4
5
6
7
8
9
10
11
12
model = model.eval()
arr_pred = []
h_state = model.initHidden()
for x,y in zip(all_x, all_y):
prediction, _h_state = model(x, h_state) # 返回隐藏层
h_state = _h_state.data
arr_pred.append(prediction.detach().numpy().flatten()[0])
plt.plot(all_y.numpy().flatten(), 'y', label='real')
plt.plot(arr_pred, label='prediction', alpha=0.5)
plt.legend(loc='best')
plt.grid()
plt.show()

需要注意的是,在训练和预测过程中,每一次开始输入新序列之前,都重置了隐藏层,这是由于隐藏层的内容只与当前序列相关,序列之间并无连续性。

程序输出结果如下图所示:

经过 500 次迭代,使用 RNN 的效果明显优于上一篇中使用全连接网络的拟合效果,还可以通过调整超参数以及选择不同特征,进一步优化。

3.调用现有 RNN 模型

使用 Pytorch 提供的 RNN 模型,torch.nn.RNN 类可直接使用,是循环网络最常用的解决方案。RNN,LSTM,GRU 等循环网络都实现在同一源码文件 torch/nn/modules/rnn.py 中。

第一步:创建模型,模型包含两部分,第一部分是 Pytorch 提供的 RNN 层,第二部分是一个全连接层,用于将 RNN 的输出转换成输出目标的维度。

1
2
3
4
5
6
7
8
9
class RNN2(nn.Module):
def __init__(self, input_size, hidden_size, output_size=1, num_layers=2):
super(RNN2, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, num_layers)
self.reg = nn.Linear(hidden_size, output_size)

def forward(self, x):
x, _ = self.rnn(x) # 未在不同序列中传递hidden_state
return self.reg(x)

Pytorch 的 RNN 前向传播允许将隐藏层数据 h 作为参数传入模型,并将模型产生的 h 和 y 作为函数返回值。形如:pred, h_state = model(x, h_state)

什么情况下需要接收隐藏层的状态 h_state,并转入下一次迭代呢?当处理单个 seq 时,h 在内部前向传递;当序列与序列之间也存在前后依赖关系时,可以接收 h_state 并传入下一步迭代。另外,当模型比较复杂如 LSTM 模型包含众多参数,传递会增加模型的复杂度,使训练过程变慢。本例未将隐藏层转到模型外部,这是由于模型内部实现了对整个序列的处理,而非处理单个元素,而每次代入的序列之间又没有连续性。

第二步:训练模型,与上例中把序列中的元素逐个代入模型不同,本例一次性把整个序列代入了模型,因此,只有一个 for 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FEATURE_SIZE = 2
OUTPUT_SIZE = 1
model = RNN2(FEATURE_SIZE, 64, OUTPUT_SIZE)
loss_func = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
train_x, train_y = create_dataset(train)
all_x, all_y = create_dataset(df) # 全部数据

for e in range(500):
var_x = Variable(train_x)
var_y = Variable(train_y)
out = model(var_x) # var_x: [100, 1, 2]
loss = loss_func(out, var_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

if (e+1)%100==0:
print('Epoch:{}, Loss:{:.5f}'.format(e+1, loss.item()))

Pythorch 支持批量处理,前向传递时输入数据格式是 [seq_len, batch_size, input_dim),本例中输入数据的维度是 [100, 1, 2],input_dim 是每个元素的特征数,batch_size 是训练的序列个数,seq_len 是序列的长度,这里使用 70% 作为训练数据,seq_len 为 100。如果数据维度的顺序与要求不一致,一般使用 transpose 转换。

第三步:预测和作图,将全部数据作为序列代入模型,并用预测值作图。

1
2
3
4
5
6
7
model = model.eval()
pred_y = model(Variable(all_x)) # 预测
plt.plot(all_y.view(-1).data.numpy(), 'y', label='real')
plt.plot(pred_y.view(-1).data.numpy(), label='prediction', alpha=0.5)
plt.legend(loc='best')
plt.grid()
plt.show()

程序输出结果如下图所示:

可以看到,经过 500 次迭代,在前 100 个元素的训练集上拟合得很好,但在测试集效果较差,可能存在过拟合。