循环神经网络RNN时间序列预测与MLP比较

1 序列数据

利用CNN构建图像识别模型,对每个样本的假设是独立同分布的,然而,大多数的数据并非如此。例如,文章中的单词是按顺序 写的,如果顺序被随机地重排,就很难理解文章原始的意思。同样,视频中的图像帧、对话中的音频信号以及网站上的浏览行为都是有顺序的(称之为序列数据)。因此,针对此类数据而设计特定模型,可能效果会更好。

简言之,如果说卷积神经网络可以有效地处理空间信息,那么本章的循环神经网络(recurrent neural network,RNN)则可以更好地处理序列信息。循环神经网络通过引入状态变量存储过去的信息和当前的输入,从而可以确定当前的输出。[1]

2 从MLP到RNN 【2】

MLP和CNN的输出只考虑与当前输入的关系,同一层之间不存在关系,所以同层不存在与前一时刻或者后一时刻的关系。

RNN关注隐层每个神经元在时间维度上的不断成长与进步,与MLP相比,没有添加新的神经元,但是沿着时间维度recurrent,建立时序上的关联。图中的隐层神经元数量并没有增加,而是表示隐层不同时刻的状态。隐层之间的关联可以是全连接的,也可以是其他的形式。隐层不同时刻之间的关联共享一个矩阵 W s W_s Ws (减少训练参数)。

MLP的隐藏层只与当前的输入 X X X有关系。写成数学公式就是: S = f ( W i n X + b ) (1) S=f(W_{in}X+b) \tag{1} S=f(WinX+b)(1)

但是,RNN的隐藏层不仅与当前输入 X X X有关系,还与前一时刻的隐层状态 S t − 1 S_{t-1} St−1有关系。写成数学公式就是: S t = f ( W i n X + W s S t − 1 + b ) (2) S_t=f(W_{in}X+W_sS_{t-1}+b) \tag{2} St=f(WinX+WsSt−1+b)(2)

不同时刻隐层之间的关系通过矩阵 W s W_s Ws进行关联。让神经网络有了某种记忆的能力。

3 输入输出的不同形式会有不同的应用

3.1 看图说话(1toN)

3.2 句子分类(Nto1)

3.3 词性标定(等长NtoN)

3.4 Seq2Seq/Encoder-Decoder

先将输入数据编码成一个上下文向量,然后通过它预测输出的序列。机器翻译、文本理解、对话系统

4 代码

突出RNN与MLP在数据的预处理、输入、训练、输出等各个阶段的处理有什么不同

4.1 时间序列预测

4.1.1 下载数据并观察

python 复制代码
#数据加载
import pandas_datareader as pdr
dji = pdr.DataReader('^DJI', 'stooq')
print(dji)

数据显示如下:

python 复制代码
                Open      High       Low     Close        Volume
Date                                                            
2024-08-22  40932.23  41026.64  40584.47  40712.78  3.047464e+08
2024-08-21  40881.03  40974.40  40738.43  40890.49  2.696454e+08
2024-08-20  40874.52  40909.38  40756.65  40834.97  2.947137e+08
2024-08-19  40670.83  40907.32  40670.83  40896.53  2.708081e+08
2024-08-16  40528.86  40726.03  40453.58  40659.76  3.087936e+08
             ...       ...       ...       ...           ...
2019-08-30  26476.39  26514.62  26295.59  26403.28  2.191756e+09
2019-08-29  26249.09  26408.84  26185.71  26362.25  2.086640e+08
2019-08-28  25712.99  26041.57  25637.43  26036.10  2.069843e+08
2019-08-27  26014.46  26054.02  25721.85  25777.90  2.635140e+08
2019-08-26  25826.05  25941.25  25716.39  25898.83  2.228613e+08

[1257 rows x 5 columns]

四列分别对应着开盘、最高点、最低点和收盘。将收盘这一列拿出来画曲线图:

python 复制代码
import matplotlib.pyplot as plt

fig1=plt.figure(num=1)
plt.plot(dji['Close'])
plt.show()

任务就是通过最近一段时间的收盘价格,预测明天的收盘价格。"最近一段时间"指的是多长时间这个可以自己定义,在程序中就是通过变量seq_len指定。

为了对比MLP和RNN,下面对MLP和RNN分别进行定义

4.1.2 定义模型:MLP和RNN

python 复制代码
###################################   MLP
class MLP(nn.Module):
    def __init__(self, input_size, output_size, num_hiddens):
        super().__init__()
        self.linear1 = nn.Linear(input_size, num_hiddens)
        self.linear3 = nn.Linear(num_hiddens, num_hiddens)
        self.linear2 = nn.Linear(num_hiddens, output_size)
    
    def forward(self, X):
        output = torch.relu(self.linear1(X))
        output = torch.relu(self.linear3(output))
        output = self.linear2(output)
        return output
        
##############################     DRNN
class DRNN(nn.Module):
    def __init__(self, input_size, output_size, hidden_size, num_layers):
        super(DRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True) 
        # batch_first 为 True时output的tensor为(batch_size,seq_len,hidden_size),否则为(seq_len,batch_size,hidden_size)
        self.linear = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        # 初始化隐藏状态和细胞状态
        state = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
        # 计算输出和最终隐藏状态
        output, _ = self.rnn(x, state)
        output = self.linear(output)
        return output

通过模型定义可以看出以下几点:

(1)RNN不需要定义序列长度 :RNN用于处理序列数据,但是在模型定义中并没有定义序列长度是多少。这一点和MLP不需要定义batch size是一样的,batch size序列长度 都是等数据注入模型的时候才能决定。

(2)nn.RNN前向传播输入除了当前时刻输入(x)还需要上一时刻隐藏层的状态(state)作为输入 ,而nn.Linear不需要上一时刻隐藏层的状态(state)作为输入

(3)因为2,所以RNN需要有一个最初始的隐藏曾状态(state) ,这个state的形状是(隐藏层个数,batch size,hidden_size)

(4)nn.RNN的输出也是两个,一个是feature,一个是隐藏层状态。feature的形状要看在定义的时候batch_first的设置。如果batch_first=True,那么feature的形状为(batch_size,seq_len,hidden_size),否则为(seq_len,batch_size,hidden_size)。隐藏层状态的形状就是(隐藏层个数,batch size,hidden_size)

4.1.3 对数据进行预处理

python 复制代码
import torch
from torch.utils.data import DataLoader, TensorDataset

num = len(dji)                           # 总数据量
x = torch.tensor(dji['Close'].to_list()[::-1])  # 股价列表,把顺序倒过来,因为原来的时间顺序是从大到小的

x = (x - torch.mean(x)) / torch.std(x)  #对数据进行归一化

这里有个需要注意的点,x把数据从dataframe里拿出来的时候,倒转了顺序,因为dataframe里的时间顺序是从大到小的

4.1.4 为训练做准备

因为要做MLP与RNN的对比,所以要准备两组数据,因为这两个模型对输入输出的要求不太一样

准备RNN的train_loader
python 复制代码
seq_len = 16                               # 预测序列长度
batch_size = 32                            # 设置批大小

X_feature = torch.zeros((num - seq_len, seq_len))      # 构建特征矩阵,num-seq_len行,seq_len列,初始值均为0
Y_label = torch.zeros((num - seq_len, seq_len))        # 构建标签矩阵,形状同特征矩阵

for i in range(seq_len):
    X_feature[:, i] = x[i: num - seq_len + i]    # 为特征矩阵赋值
    Y_label[:, i] = x[i+1: num - seq_len + i + 1]    # 为标签矩阵赋值

train_loader = DataLoader(TensorDataset(
    X_feature[:num-seq_len].unsqueeze(2), Y_label[:num-seq_len]),
    batch_size=batch_size, shuffle=True)  # 构建数据加载器,训练各种RNN
准备MLP的train_loader
python 复制代码
y_label = x[seq_len:].reshape((-1, 1))           # 真实结果列表,用于MLP

train_loader_mlp = DataLoader(TensorDataset(X_feature[:num-seq_len], 
    y_label[:num-seq_len]), batch_size=batch_size, shuffle=True)  # 构建数据加载器,训练mlp
定义超参数
python 复制代码
# 定义超参数
input_size = 1
output_size = 1
num_hiddens = 64
n_layers = 2
lr = 0.001
建立模型
python 复制代码
# 建立模型
model = DRNN(input_size, output_size, num_hiddens, n_layers)
mlp = MLP(seq_len, output_size, num_hiddens)

criterion = nn.MSELoss(reduction='none')

trainer = torch.optim.Adam(model.parameters(), lr)
trainer_mlp = torch.optim.Adam(mlp.parameters(), lr)

4.1.5 开始训练

python 复制代码
# 训练轮次
num_epochs = 20
mlp_loss_history = []
rnn_loss_history = []
#######################  MLP训练
for epoch in tqdm(range(num_epochs)):
    # print('##########################epoch:',epoch)
    # 批量训练
    for X, y in train_loader_mlp:
        # print('%%%%%%%%%%%')
        # print('X.shape:',X.shape)#####X.shape: torch.Size([32, 16])
        # print('y.shape:',y.shape)######y.shape: torch.Size([32, 1])
        trainer_mlp.zero_grad()
        y_pred = mlp(X)
        # print('y_pred.shape:',y_pred.shape)######y_pred.shape: torch.Size([32, 1])
        loss = criterion(y_pred, y)
        # print('loss:',loss)

        loss.sum().backward()
        trainer_mlp.step()
    # 输出损失
    with torch.no_grad():
        total_loss = 0
        for X, y in train_loader_mlp:
            y_pred = mlp(X)
            loss = criterion(y_pred, y)
            total_loss += loss.sum()/loss.numel()
        avg_loss = total_loss / len(train_loader)
        print(f'Epoch {epoch+1}: Validation loss = {avg_loss:.4f}')
        # loss_history.append(avg_loss.detach().numpy())
        mlp_loss_history.append(avg_loss)

#######################  DRNN训练

for epoch in tqdm(range(num_epochs)):
    # 批量训练
    for X, Y in train_loader:
        trainer.zero_grad()
        y_pred = model(X)
        ##X.shape: torch.Size([32, 16,1]),其中1是input_size;
        #Y.shape: torch.Size([32, 16])
        ###y_pred.shape: torch.Size([32, 16, 1]),其中1是output_size
        loss = criterion(y_pred.squeeze(), Y.squeeze())
        loss.sum().backward()
        trainer.step()
    # 输出损失
    with torch.no_grad():
        total_loss = 0
        for X, Y in train_loader:
            y_pred = model(X)
            loss = criterion(y_pred.squeeze(), Y.squeeze())
            total_loss += loss.sum()/loss.numel()
        avg_loss = total_loss / len(train_loader)
        print(f'Epoch {epoch+1}: Validation loss = {avg_loss:.4f}')
        rnn_loss_history.append(avg_loss)
    
# 绘制损失曲线图
# import matplotlib.pyplot as plt
# plt.plot(loss_history, label='loss')
fig2=plt.figure(num=2)
plt.plot(mlp_loss_history, label='MLP_loss')
plt.plot(rnn_loss_history, label='RNN_loss')
plt.legend()
plt.show()

损失曲线图如图所示:

可以看到RNN的损失更低一些。

需要注意几点:

(1)MLP预测时间序列的输入输出形式与rnn不同 ,mlp如下:

RNN如下:

MLP的input_size就是序列长度,输出size是1;RNN的输入size与输出size都是1

(2)注入MLP的数据与注入RNN的数据的形状不同 :注入MLP的数据形状是(batch_size, input_size=seq_len),注入RNN的数据形状是(batch_size, seq_len, input_size=1)

(3)MLP输出形状和RNN输出形状不同 :MLP输出形状:(batch_size, output_size)RNN输出形状(batch_size, seq_len, output_size=1)。这个时候,我们就可以看出MLP与RNN的数据的区别了,MLP少了一个seq_len的维度,而这个维度是在数据侧决定的,不是在模型侧

(4)RNN是可以输出隐藏层的状态的,只是这里没有输出。(RNN的输入需要X和state,输出是Y和state,state看需要,可以不输出

所以为了能够正确的训练和预测,需要对数据的形状进行整理,需要用到unsqueeze()和squeeze(),这两个函数的原理请参考【3】

另外MSEloss也需要注意,reduction='none'就是每个元素进行差和平方,保留数据形状,不加和不平均。可参考【4】

4.1.6 预测

python 复制代码
fig3=plt.figure(num=3)
rnn_preds = model(X_feature.unsqueeze(2))
mlp_preds = mlp(X_feature)
rnn_preds.squeeze()
time = torch.arange(1, num+1, dtype= torch.float32)  # 时间轴

plt.plot(time[:num-seq_len], x[seq_len:num], label='dji')
# plt.plot(time[:num-seq_len], preds.detach().numpy(), label='preds')
plt.plot(time[:num-seq_len], mlp_preds.detach().numpy(), label='mlp_preds')
plt.plot(time[:num-seq_len], rnn_preds[:,seq_len-1].detach(), label='RNN_preds')######只取了序列最后节点的输出
plt.legend()
plt.show()

这里需要注意的是,预测的时候,RNN的预测值只是取了最后时刻节点的输出。

4.2 文本

参考:

【1】动手学深度学习

【2】【循环神经网络】5分钟搞懂RNN,3D动画深入浅出

【*】PyTorch nn.MSELoss() 均方误差损失函数详解和要点提醒

【3】unsqueeze()和squeeze()

相关推荐
Point__Nemo11 分钟前
深度学习Day-33:Semi-Supervised GAN理论与实战
人工智能·深度学习
魔力之心16 分钟前
人工智能与机器学习原理精解【20】
人工智能·机器学习
SEU-WYL3 小时前
基于深度学习的因果发现算法
人工智能·深度学习·算法
Tunny_yyy4 小时前
李宏毅2023机器学习HW15-Few-shot Classification
人工智能·机器学习
Francek Chen4 小时前
【机器学习-监督学习】朴素贝叶斯
人工智能·机器学习·分类·数据挖掘·scikit-learn·朴素贝叶斯·naive bayes
jndingxin6 小时前
OpenCV运动分析和目标跟踪(1)累积操作函数accumulate()的使用
人工智能·opencv·目标跟踪
文艺倾年6 小时前
【大模型专栏—进阶篇】语言模型创新大总结——“后起之秀”
人工智能·pytorch·语言模型·自然语言处理·大模型
DA树聚6 小时前
大语言模型之ICL(上下文学习) - In-Context Learning Creates Task Vectors
人工智能·学习·程序人生·ai·语言模型·自然语言处理·easyui
一支王同学6 小时前
论文解读《LaMP: When Large Language Models Meet Personalization》
人工智能·语言模型·自然语言处理
水花花花花花6 小时前
卷积神经网络
人工智能·神经网络·cnn