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,这里的月份值并不是关键特征,主要用于在例程中展示如何使用多个特征。

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模型除了全连接层,还生成了一个隐藏层,并在下一次前向传播时将隐藏层输出的数据与输入数据组合后再代入模型运算。

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循环将序列中的每个元素代入模型,并将模型输出的隐藏层和下一个元素一起送入下一次迭代。

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()

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

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的输出转换成输出目标的维度。

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循环。

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转换。

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

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个元素的训练集上拟合得很好,但在测试集效果较差,可能存在过拟合。

posted on 2019-12-06 17:33  xieyan0811  阅读(99)  评论(0编辑  收藏  举报