LSTM入门案例(时间序列预测)| pytorch实现

需求

假如我有一个时间序列,例如是前113天的价格数据(训练集),然后我希望借此预测后30天的数据(测试集),实际上这143天的价格数据都已经有了。这里为了简单,每一天的数据只有一个价格维度(转化成矩阵形式就是1列),但实际上每一天的数据也可以是多维的特征,转化成矩阵就是多列。

预测思路

首先训练模型去预测下一天数据的能力,训练完后,我们使用历史数据预测第114天的数据,预测后,我们暂时将第114天的数据看做真实数据,放入历史数据中,再用它预测第115天的数据,依次类推,最终预测完后30天的数据。

本实现的关键点

1. 数据预处理:归一化很重要,可以加速训练过程并提高模型性能

2. 模型结构:LSTM + 全连接层的组合用于回归预测

3. 训练过程:使用MSE损失和Adam优化器

4. 预测方式:滚动预测,保持隐藏状态的连续性

一、LSTM神经元结构

 

二、定义LSTM模型

我们会使用torch.nn.LSTM()加载LSTM层。其LSTM的参数定义如下:

 注意:

  • 对于双向 LSTM,正向和反向分别是方向 0 和 1。当输出层拆分时,以 batch_first=False : output.view(seq_len, batch, num_directions, hidden_size) 为例。
  • 对于双向 LSTM,h_n 与输出的最后一个元素并不等价;前者包含最终的向前和向后隐藏状态,而后者包含最终的向前隐藏状态和初始的向后隐藏状态。
  • batch_first 参数对于非批处理的输入会被忽略。
  • proj_size 应该小于 hidden_size

 torch.nn.LSTM中的proj_size参数到底是什么意思?

 proj_size 是 PyTorch torch.nn.LSTM 中的一个参数,用于指定将 LSTM 输出的隐藏状态(hidden state)映射到指定维度。当 proj_size 大于 0 时,LSTM 会通过一个可学习的投影矩阵将隐藏状态投影到该维度,通常用于调整输出特征的数量或适配特定任务需求。

具体作用

在 LSTM 模型中,隐藏状态 h 的维度通常与输入特征维度一致(即 hidden_size)。通过引入 proj_size 参数,可以在 LSTM 输出后通过线性变换进一步压缩或扩展特征空间,例如:

  • 降维‌:当 proj_size 小于 hidden_size 时,通过投影矩阵将高维特征映射到低维空间,减少计算量或提取核心特征。
  • 升维‌:当 proj_size 大于 hidden_size 时,可增加输出特征的多样性,适用于需要更多特征的情况。 

注意事项

  • 该参数默认值为 0,即不进行投影操作。
  • 投影矩阵是可学习的,其维度由 proj_sizehidden_size 共同决定。

最重要的参数就前三个,其他参数都可以默认。把网络看成一个黑箱,我们在用是肯定是输入一个向量,然后网络处理后输出一个向量,所以我们必须要告诉网络输入的向量是多少维,输出的为多少维,因此前两个参数就决定了输入和输出向量的维度。当然,hidden_size只是指定从LSTM输出的向量的维度,并不是最后的维度,因为LSTM层之后可能还会接其他层,如全连接层(FC),因此hidden_size对应的维度也就是FC层的输入维度。

LSTM网络的输入和输出:

 

 输入:input,(h_0, c_0)

 输出:output,(h_n, c_n)

output:对于未批处理的输入,形状为 (L,D∗Hout) 的张量,当 batch_first=False 时为 (L,N,D∗Hout),当 batch_first=True 时为 (N,L,D∗Hout),包含来自 LSTM 最后层的输出特征 (h_t),对于每个 t。如果输入了 torch.nn.utils.rnn.PackedSequence,输出也将是一个打包序列。当 bidirectional=True 时,输出将包含序列中每个时间步的正向和反向隐藏状态的拼接。

 LSTM参数这部分详细学习可以跳转到:

https://zhuanlan.zhihu.com/p/510072883

LSTM — PyTorch 2.7 documentation

 LSTM的输入和输出为:output,(h_n,c_n) = lstm (x, [ht_1, ct_1])

其中x就是我们喂给网络的数据,它的shape要求如下:x:[seq_length, batch_size, input_size]

 图片地址:pytorch中LSTM参数详解(一张图帮你更好的理解每一个参数)_torch lstm参数图解-CSDN博客 

input(seq_len, batch_size, input_size)
参数有:
    seq_len:序列长度,在NLP中就是句子长度,一般都会用pad_sequence补齐长度
    batch_size:每次喂给网络的数据条数,在NLP中就是一次喂给网络多少个句子
    input_size:输入的特征维度,和前面定义网络结构的input_size参数一致

##

那么输入此LSTM的 input() == (seq_len, batch_size, input_size)

在我们的LSTM时间序列预测任务中:
seq_len:时间序列的长度,在这里使用前113天的价格数据进行训练,则 seq_len == 113。
batch_size:同一批次中要处理的样本数,它决定了每次输入网络的数据批次大小,直接影响训练效率和内存占用。(这里的样本好像指的是序列的个数,即一个seq就是一个样本
input_size:输入数据的特征维度,在这里只有价格一个维度,则 input_size == 1。

##

如果是自然语言处理 (NLP) ,那么:
seq_len:将对应句子的长度
batch_size:同个批次中输入的句子数量
input_size: 句子中用来表示每个单词(中文分词)的矢量维度

##

请注意,虽然通常情况下input张量的第一个维度是批次大小batch size,但是PyTorch建议我们输入循环网络的时候张量的第一个维度是序列长度seq_len,而第二个维度才是批次大小batch_size。(其实就是LSTM参数中batch_fist==False)

为了进行时间序列预测,我们在LSTM后面接上两层全连接层(1层亦可),同时改变最终输出张量的维度,我们只需要预测价格这一个值,因此最终的out_dim 为1。在LSTM后方的全连接层也可以看做是一个回归操作 regression。

在LSTM后面接上两层全连接层,为何是两层: 理论上足够宽,并且至少存在一层具有任何一种“挤压”性质的激活函数的两层全连接层就能拟合任何连续函数。最先提出这个理论证明的是 Barron et al., 1993,使用了UAT (Universal Approximation Theorem),指出了可以在compact domain拟合任意多项式函数。”
实际上对于过于复杂的连续函数,这个「足够宽」不容易满足。并且拟合训练数据并让神经网络具备足够的泛化性的前提是:良好的训练方法(比如批次训练数据满足 独立同分布 (i.i.d.),良好的损失函数,满足Lipschitz连续 etc.)

代码示例:

reg = nn.Sequential(
    nn.Linear(mid_dim, mid_dim),   # 全连接层,将LSTM输出映射到mid_dim维度
    nn.Tanh(),  # 激活函数,将输出映射到[-1, 1]之间
    nn.Linear(mid_dim, out_dim),   # 全连接层,将LSTM输出映射到out_dim维度
)  # regression回归

##

linear_input = output_of_LSTM  # LSTM的输出作为回归层的输入
[seq_len, batch_size, mid_dim]= linear_input.shape
linear_input = linear_input.view(seq_len * batch_size, mid_dim)  # 将输出重塑为2D
output = reg(linear_input) # 通过回归层
output = output.view(seq_len, batch_size, out_dim) # 重塑回原始形状

 看不懂上面代码示例的话,请跳转到API文档:

Linear — PyTorch 2.7 documentation

Tanh — PyTorch 2.7 documentation

Tensor Views — PyTorch 2.7 documentation

 定义LSTM模型的完整代码如下:

class RegLSTM(nn.Module):def __init__(self, inp_dim, out_dim, mid_dim, mid_layers):"""初始化LSTM回归模型参数:inp_dim: 输入维度out_dim: 输出维度mid_dim: LSTM隐藏层维度mid_layers: LSTM层数"""super(RegLSTM, self).__init__()# LSTM层,输入维度为inp_dim,隐藏状态维度为mid_dim,层数为mid_layersself.rnn = nn.LSTM(inp_dim, mid_dim, mid_layers)  # 回归层,将LSTM输出映射到预测值self.reg = nn.Sequential(nn.Linear(mid_dim, mid_dim), # 全连接层,将LSTM输出映射到mid_dim维度nn.Tanh(), # 激活函数,将输出映射到[-1, 1]之间nn.Linear(mid_dim, out_dim), # 全连接层,将LSTM输出映射到out_dim维度)  def forward(self, x):"""前向传播参数:self.rnnx: 输入数据,形状为 [seq_len, batch_size, inp_dim]返回:输出预测,形状为 [seq_len, batch_size, out_dim]"""# 获取LSTM输出,y形状为 [seq_len, batch_size, mid_dim]y = self.rnn(x)[0]  # y, (h, c) = self.rnn(x)     # 获取输出的形状seq_len, batch_size, hid_dim = y.shape# 将输出重塑为2D,便于全连接层处理y = y.view(-1, hid_dim)                           # 通过回归层y = self.reg(y)                                   # 重塑回原始形状y = y.view(seq_len, batch_size, -1)               return ydef output_y_hc(self, x, hc):"""带隐藏状态的前向传播,用于预测时保持状态连续性参数:x: 输入数据hc: 上一步的隐藏状态和单元状态元组 (h, c)返回:y: 输出预测hc: 更新后的隐藏状态和单元状态"""# 传递隐藏状态和单元状态y, hc = self.rnn(x, hc)# 与forward相同的处理步骤seq_len, batch_size, hid_dim = y.size() # 获取输出y的形状y = y.view(-1, hid_dim) # 将输出y重塑为2D,便于全连接层处理y = self.reg(y) # 通过回归层y = y.view(seq_len, batch_size, -1) # 重塑回原始形状return y, hc# 示例:LSTM的输入输出维度
print("LSTM输入格式: [seq_len, batch_size, feature_dim]")
print("示例: 5个时间步,3个样本,每个样本10个特征 -> [5, 3, 10]")

forward方法中LSTM层的输入与输出(带批处理、batch_first=False、proj_size=0)

一、输入格式:input,(h0, C0)

1、input为(seq_len,batch_size,input_size)格式的tensor,seq_len即为time_step

2、h0为(num_layers * num_directions, batch_size, hidden_size)格式的tensor,隐藏状态的初始状态。(num_directions指lstm单向还是双向;如果设置了投影维度则应为proj_size)

3、C0为(num_layers * num_directions, batch_size, hidden_size)格式的tensor,细胞初始状态

二、输出格式:output,(ht,Ct)

1、output为(seq_len, batch_size, num_directions*hidden_size)格式的tensor,包含输出特征h_t(源于LSTM每个t的最后一层)

2、ht为(num_layers * num_directions, batch_size, hidden_size)格式的tensor,

3、Ct为(num_layers * num_directions, batch_size, hidden_size)格式的tensor,

###

 代码示例(以NLP为例):

rnn = nn.LSTM(10, 20, 2) # 一个单词向量长度为10,隐藏层节点数为20,LSTM有2层
input = torch.randn(5, 3, 10) # 输入数据由3个句子组成,每个句子由5个单词组成,单词向量长度为10
h0 = torch.randn(2, 3, 20) # 2:LSTM层数*方向  3:batch  20: 隐藏层节点数
c0 = torch.randn(2, 3, 20) # 同上
output, (hn, cn) = rnn(input, (h0, c0))

print(output.shape, hn.shape, cn.shape) 

>>> torch.Size([5, 3, 20])   torch.Size([2, 3, 20])   torch.Size([2, 3, 20])

 总结一下:

在LSTM内部,有h和c,可以理解为hidden和cell。模型中定义了两个函数forward()和output_y_hc,可以理解为forward()函数在训练后预测时,会扔掉h和c,每次预测都用同一个h和c(可能是训练时最后一次的h和c,可能是随机的),output_y_hc()会一直返回h和c,从而下一次预测可以把h和c在带进去,一直用最新的h和c。

模型构造函数接受四个参数:inp_dim, out_dim, mid_dim, mid_layers,其中inp_dim, mid_dim, mid_layers是nn.LSTM()构造时传入的3个参数,输入维度是inp_dim,在这里是1,输出维度是mid_dim,这里可以自己定义。后面再跟两个全连接层,第一个全连接层是mid_dim to mid_dim,第二个全连接层是mid_dim to out_dim,也就是说,模型最后的输出维度是out_dim,在本问题中,我们希望预测的是每天的价格,所以out_dim也是1。

REGLSTM这个类里面定义的成员函数output_y_hc,有什么作用?

我们需要保存LSTM的隐藏状态(hidden state),用于恢复序列中断后的计算。举例子,我有完整的序列 seq12345:

  • 我输入seq12345 到LSTM后,我能得到6,即seq123456。
  • 我也可以先输入 seq123 以及默认的隐藏状态hc,得到4和新的hc,然后我把seq234和新的hc输入,可以得到5和新的hc,我接着把 seq345 以及 hc一起输入到LSTM,我也能得到6,即seq123456。

(hc 指 h和c,是两个张量,本文开头的LSTM结构图注明了何为 h与c)

    三、数据预处理

    3.1 数据预处理函数

    在模型训练之前,需要对训练集的train_x和train_y都要进行归一化。

    train_x, train_x_minmax = minmaxscaler(train_x)   #返回归一化后的数据、最大和最小值

    train_y, train_y_minmax = minmaxscaler(train_y)

    前后过程:

    划分数据集 --> 对训练集的x和y进行归一化 --> 模型训练 --> 保存模型 

    #  #  #  # 

    在模型预测过程,需要对测试集的test_x进行归一化,对predict_y进行反归一化。

    test_x = preminmaxscaler(test_x, train_x_minmax[0], train_x_minmax[1])  #返回归一化后的值

    predict_y = unminmaxscaler(predict_y, train_x_minmax[0], train_y_minmax[1])

    前后过程:

    使用定长的timestep序列数据作为输入来预测下一个数据点--> 对测试集的test_x进行归一化 --> 获取最后一个时间步的预测值test_y[-1].item()  -->  对预测值predict_y进行反归一化  --> 将预测结果加入数据集当作真实数据并进行下一步数据点的预测

    数据先预处理一下。若直接对全部数据进行归一化处理是不正确的,归一化的时候不应该把测试用的数据也包括进去。对于训练集的x和y我们分别都归一化处理。之后在预测的时候,对于输入的x,我们要用训练集x的最大和最小值进行归一化处理,对于预测得到的y,我们要用训练集y的最大和最小值进行反归一化。所以我们要保存着训练集中x和y的最大值与最小值。

    为了提高神经网络的训练效果,我们需要对数据进行归一化处理。下面定义三个函数:

    1. `minmaxscaler`: 将数据归一化到[0,1]区间

    2. `preminmaxscaler`: 使用已知的最小值最大值对新数据进行归一化

    3. `unminmaxscaler`: 将归一化的数据反归一化回原始范围

    def minmaxscaler(data):"""将数据归一化到[0,1]区间参数:data: 输入数据返回:scaled_data: 归一化后的数据(min_val, max_val): 用于反归一化的最小值和最大值"""min_val = np.min(data)max_val = np.max(data)scaled_data = (data - min_val) / (max_val - min_val)return scaled_data, (min_val, max_val)def preminmaxscaler(data, min_val, max_val):"""使用已知的最小值和最大值对数据进行归一化参数:data: 输入数据min_val: 最小值max_val: 最大值返回:scaled_data: 归一化后的数据"""scaled_data = (data - min_val) / (max_val - min_val)return scaled_datadef unminmaxscaler(data, min_val, max_val):"""将归一化的数据反归一化参数:data: 归一化的数据min_val: 原始数据的最小值max_val: 原始数据的最大值返回:原始数据"""return data * (max_val - min_val) + min_val# 测试归一化函数
    test_data = np.array([1, 5, 10, 15, 20])
    normalized_data, (min_val, max_val) = minmaxscaler(test_data)
    print("原始数据:", test_data)
    print("归一化数据:", normalized_data)
    print("反归一化数据:", unminmaxscaler(normalized_data, min_val, max_val))
    

     输出结果:

    preminmaxscaler是在预测的时候,我们用训练集的最大最小值去做归一化。
    unminmaxscaler就是反归一化。

    3.2 准备数据集

    • 经过尝试,LSTM对输入的时间序列长度似乎没有要求,也就是说我可以输入100天的历史数据进行训练,我也可以输入50天的历史数据进行训练。之后在训练完进行预测的时候,我也可以输入任意天数的历史数据预测未来的数据。
    • 由于数据较少,我们只设置1个batch,也就是一次就把所有训练数据输入进去,然后迭代多个epoch进行训练。
    • 我们使用113天的历史数据训练模型,预测后30天的数据。
    # 加载数据 - 时间序列数据(144天的价格历史数据)bchain = np.array([112., 118., 132., 129., 121., 135., 148., 148., 136., 119., 104.,118., 115., 126., 141., 135., 125., 149., 170., 170., 158., 133.,114., 140., 145., 150., 178., 163., 172., 178., 199., 199., 184.,162., 146., 166., 171., 180., 193., 181., 183., 218., 230., 242.,209., 191., 172., 194., 196., 196., 236., 235., 229., 243., 264.,272., 237., 211., 180., 201., 204., 188., 235., 227., 234., 264.,302., 293., 259., 229., 203., 229., 242., 233., 267., 269., 270.,315., 364., 347., 312., 274., 237., 278., 284., 277., 317., 313.,318., 374., 413., 405., 355., 306., 271., 306., 315., 301., 356.,348., 355., 422., 465., 467., 404., 347., 305., 336., 340., 318.,362., 348., 363., 435., 491., 505., 404., 359., 310., 337., 360.,342., 406., 396., 420., 472., 548., 559., 463., 407., 362., 405.,417., 391., 419., 461., 472., 535., 622., 606., 508., 461., 390.,432.], dtype=np.float32)bchain = bchain[:, np.newaxis]  # 转为列向量,形状变为(144, 1)# 查看数据形状print("数据形状:", bchain.shape)print("前10个数据点:", bchain[:10].flatten())# 绘制原始数据plt.figure(figsize=(12, 6))plt.plot(bchain, 'b-')plt.title('原始时间序列数据')plt.xlabel('时间步')plt.ylabel('值')plt.grid(True)plt.show()

     输出结果:

    # 设置模型参数
    inp_dim = 1      # 输入维度
    out_dim = 1      # 输出维度
    mid_dim = 8      # LSTM隐藏层维度
    mid_layers = 1   # LSTM层数# 准备输入和目标数据
    # data_x是从第一个到倒数第二个数据点,data_y是从第二个到最后一个数据点
    # 这里我们预测下一个时间步的值
    data_x = bchain[:-1, :]  # 形状: (143, 1)
    data_y = bchain[+1:, :]  # 形状: (143, 1)# 查看输入和输出的形状
    print("输入数据形状 data_x:", data_x.shape)
    print("输出数据形状 data_y:", data_y.shape)# 划分训练集(前113个数据点)
    train_size = 113
    train_x = data_x[:train_size, :]  # 形状: (113, 1)
    train_y = data_y[:train_size, :]  # 形状: (113, 1)
    test_x = data_x[train_size:, :]   # 形状: (30, 1)
    test_y = data_y[train_size:, :]   # 形状: (30, 1)print("训练集 train_x:", train_x.shape)
    print("训练集 train_y:", train_y.shape)
    print("测试集 test_x:", test_x.shape)
    print("测试集 test_y:", test_y.shape)# 数据归一化处理
    train_x, train_x_minmax = minmaxscaler(train_x)
    train_y, train_y_minmax = minmaxscaler(train_y)# 可视化数据集划分
    plt.figure(figsize=(12, 6))
    plt.plot(range(len(data_x)), data_x, 'b-', label='原始数据')
    plt.axvline(x=train_size, color='r', linestyle='--', label='训练集/测试集分割点')
    plt.title('数据集划分')
    plt.xlabel('时间步')
    plt.ylabel('值')
    plt.legend()
    plt.grid(True)
    plt.show()

      输出结果:

    方法1:只输入一条历史序列进行训练:

    最简单的训练模式,我们把113天的历史数据一次性输入到模型中进行训练。113天的历史序列长这样:[112., 118., 132., 129. …… 362., 348., 363.]

    那这就是输入模型的x。那么输入模型的y是什么样呢?由于我们希望的是预测后一天的数据,所以我们每次都取后一天的数据,同样构成一个113天的序列,序列长这样:[118., 132., 129., 121. …… 348., 363., 435.]

    这就是输入模型的y。可以看到y就是x后移了1天。这里我认为,如果我们想预测后两天你的数据,那么我们的y就可以是x后移2天。

    我们构造好了输入数据的x和y,现在要把它们整理成模型希望的数据格式。LSTM希望的输入数据是3维,[seq_len, batch_size, inp_dim]:

    seq_len是时间步,也就是每个序列的长度。
    batch_size是序列个数,也就是我们希望同时处理多少个序列。
    inp_dim是输入数据的特征维度,也就是对于每个时间序列,每一天的数据维数。

    对于本问题,我们输入的是一个113天的历史序列,因此batch_size是1。每一天都只有一个价格数据,因此inp_dim也是1。而seq_len就是113。
    对于y,y也是一个113天的序列,维度是1,数据格式也是[113, 1, 1]。只不过对应的seq_len具体的值往后移了一位。

    # 设置设备(GPU或CPU)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"使用设备: {device}")# 第一种操作,直接把batch_x batch_y这一个序列扔进去# 准备训练数据,添加batch维度
    # LSTM输入格式: [seq_len, batch_size, feature_dim]
    batch_x = train_x[:, np.newaxis, :]  # 形状: [113, 1, 1],表示113个时间步,1个样本,每个样本1个特征
    batch_y = train_y[:, np.newaxis, :]  # 形状: [113, 1, 1]
    batch_x = torch.tensor(batch_x, dtype=torch.float32, device=device)
    batch_y = torch.tensor(batch_y, dtype=torch.float32, device=device)print("训练输入 batch_x 形状:", batch_x.shape)
    print("训练目标 batch_y 形状:", batch_y.shape)

     输出:

     注意:了解np.newaxis的作用、用法

    【Numpy】基础学习:一文了解np.newaxis的作用、用法-CSDN博客

    【Python】np.newaxis()函数用法详解_np.newaxis函数-CSDN博客 Constants — NumPy v1.24 Manual【Python】np.newaxis()函数用法详解_np.newaxis函数-CSDN博客

    方法2:输入多条短的历史序列进行训练:

    我们也可以将使用类似于滑动窗口的方法,从原始数据里选取多段相同长度的序列,作为一条条的历史序列x,当然也要搭配y序列(就是把x序列右移一步)。
    我们选定历史序列长度为40,一共选了25个序列,代码如下:

    # 设置设备(GPU或CPU)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"使用设备: {device}")# 将训练数据转换为张量
    train_x_tensor = torch.tensor(train_x, dtype=torch.float32, device=device)
    train_y_tensor = torch.tensor(train_y, dtype=torch.float32, device=device)# 使用滑动窗口构造多条序列,窗口长度为40,步长为3
    window_len = 40
    batch_x = []  # 存储输入序列
    batch_y = []  # 存储目标序列for end in range(len(train_x_tensor), window_len, -3):# 添加一段历史序列到batch_xbatch_x.append(train_x_tensor[end-window_len:end])# 添加对应的目标序列到batch_ybatch_y.append(train_y_tensor[end-window_len:end])# 检查构造的序列数量
    print(f"构造的序列数量: {len(batch_x)}")# 使用pad_sequence将数据整理成LSTM希望的格式
    # 将多条序列整理成 [seq_len, batch_size, feature_dim] 格式
    from torch.nn.utils.rnn import pad_sequence
    batch_x = pad_sequence(batch_x)  # 形状变为 [40, batch_size, 1]
    batch_y = pad_sequence(batch_y)  # 形状变为 [40, batch_size, 1]print(f"batch_x的形状: {batch_x.shape}")  # [40, num_sequences, 1]
    print(f"batch_y的形状: {batch_y.shape}")  # [40, num_sequences, 1]

     输出:

     

    注意:因为pytorch要求timestep必须定长,基本上网上搜到的其他所有pytorch的lstm入门教程都是定长的timestep,如果遇到的案例中使用的是不定长的timestep,需借助pad_sequence成定长的timestep来训练。(但是我整个实验流程都是用的定长的timestep,可忽略这一点

    如果同一批次里面训练序列长度不统一,直接在末尾补0的操作不优雅,我们需要借助torch 自带的工具 pad_sequence的协助,放入pad_sequence 的序列必须从长到短放置,随着反向传播的进行,PyTorch 会逐步忽略完成梯度计算的短序列。具体解释请看PyTorch官网。

     注意:了解torch.pad_sequence的作用、用法

    pad_sequence_padsequence-CSDN博客

    pytorch中的pad_sequence、pack_padded_sequence和pad_packed_sequence函数_pytorch pad-CSDN博客

    「构建用于训练的序列」

    要避免输入相同起始裁剪位点的序列用于训练。只有序列的起始裁剪位点都不一样,在RNN内才不会重复训练,这一点很重要。

    batch_var_x.append(var_x[j:])  # 不同的起始裁剪位点,正确的裁剪方法
    batch_var_x.append(var_x[:j])  # 相同的起始裁剪位点,完全错误的裁剪方法

    我们通过pad_sequence将数据整理成LSTM希望的格式。
    比如我们本来有3条历史序列,分别是[1, 2, 3,4][ 5, 6,7, 8][9,10,11,12],但是我们将它们整理成的格式为:

    原本是:               整理成:
    [ [1,  2,   3,   4],                [ [[1],  [5],  [9]],
      [5,  6,   7,   8],                  [[2],  [6],  [10]],
      [9, 10, 11, 12] ]                [[3],  [7],  [11]],

                                              [[4],  [8],  [12]] ]

    这样,每一列是一个序列,一共有3个历史序列。每一行是一个时间步,这样整理数据,模型就能一行一行的处理,从而同时处理3个序列。
    对于训练用的x和y,我们都整理成一样的格式。只不过在一般的情境中,x的维度要高一点,比如每一天(也就是一个时间步),一共有n个特征数据表示,也就是说x的维度是n,也就是说在定义LSTM的时候,input_size是n。假如我们有m个序列,每个序列有z个时间步,最后的x要整理成[z, m, n]。

    四、模型训练

    4.1 初始化模型、损失函数和优化器

    有了训练用的x和y,我们就可以将其输入到模型进行训练。在模型训练之前需要初始化模型,并设置损失函数和优化器。(保存模型结构图这一步骤不是必要的,只是为了方便可视化)

    # 初始化模型
    model = RegLSTM(inp_dim, out_dim, mid_dim, mid_layers).to(device)
    print(model)# 保存模型结构图
    try:from graphviz import Digraphdef save_model_structure(model, inp_dim, mid_dim, mid_layers, out_dim, filename='model_structure'):dot = Digraph(comment='LSTM时间序列预测模型')# 添加输入节点dot.node('input', f'Input\n[seq_len, batch_size, {inp_dim}]', shape='box')# 添加LSTM节点dot.node('lstm', f'LSTM\nLayers: {mid_layers}\nHidden dim: {mid_dim}', shape='box')# 添加回归网络节点dot.node('reg1', f'Linear\n{mid_dim} -> {mid_dim}', shape='box')dot.node('tanh', 'Tanh', shape='box')dot.node('reg2', f'Linear\n{mid_dim} -> {out_dim}', shape='box')# 添加输出节点dot.node('output', f'Output\n[seq_len, batch_size, {out_dim}]', shape='box')# 添加边dot.edge('input', 'lstm')dot.edge('lstm', 'reg1')dot.edge('reg1', 'tanh')dot.edge('tanh', 'reg2')dot.edge('reg2', 'output')# 保存为图像dot.render(filename, format='png', cleanup=True)print(f'模型结构图已保存为: {filename}.png')save_model_structure(model, inp_dim, mid_dim, mid_layers, out_dim, './model_structure_tutorial')# 显示模型结构图from IPython.display import Imagedisplay(Image(filename='./model_structure_tutorial.png'))except ImportError:print("无法保存模型结构图。请安装graphviz库: pip install graphviz")print("并确保已安装Graphviz软件: https://graphviz.org/download/")# 设置损失函数和优化器
    loss_fn = nn.MSELoss()  # 均方误差损失函数
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)  # Adam优化器

    输出结果:

     下面的草稿是分析整个模型的结构是怎么来的:

    4.2 训练模型

    有了训练用的x和y,我们就可以将其输入到模型进行训练。我们将训练800个epoch,每10个epoch打印一次损失值,代码如下:

    # 训练模型
    print("训练开始...")
    losses = []  # 记录损失值,用于绘图epochs = 801 
    for e in range(epochs):# 前向传播out = model(batch_x) # 将输入数据batch_x传递给模型,得到预测输出out# 计算损失loss = loss_fn(out, batch_y) # 计算预测输出out与真实输出batch_y之间的损失losses.append(loss.item()) # 将损失值添加到losses列表中# 反向传播和优化optimizer.zero_grad() # 清空梯度loss.backward() # 计算梯度optimizer.step() # 更新参数# 每10个epoch打印一次损失if e % 10 == 0:print('Epoch: {:4}, Loss: {:.5f}'.format(e, loss.item()))# 保存模型
    torch.save(model.state_dict(), './net.pth')
    print("模型已保存至: './net.pth'")# 绘制损失曲线
    plt.figure(figsize=(10, 5))
    plt.plot(losses)
    plt.title('训练损失曲线')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.yscale('log')  # 使用对数尺度更容易观察损失下降
    plt.grid(True)
    plt.show()
    

     输出:

    4.3 模型预测

    预测的时候,我们还是要输入一个序列x,得到一个输出序列y。由于在训练时输出序列是输入序列右移一步,因此对于得到的y,其最后一个值就是我们预测的下一天的数据。
    对于输入的序列x,序列长度任意,我在尝试的时候发现序列长度长一点和短一点(甚至序列长度是1),预测的效果好像没有差别,这可能证明LSTM的预测效果并不好。我也不太清楚。

    现在使用训练好的模型对测试集进行预测:

    # 模型预测
    print("开始预测...")
    new_data_x = data_x.copy()
    new_data_x[train_size:] = 0  # 清除训练集之后的数据,用于存放预测结果test_len = 40  # 使用前40个数据点来预测下一个eval_size = 1  # 评估批量大小
    # 初始化LSTM隐藏状态和单元状态为零
    zero_ten = torch.zeros((mid_layers, eval_size, mid_dim), dtype=torch.float32, device=device)# 循环预测训练集之后的数据
    for i in range(train_size, len(new_data_x)):# 获取前test_len个数据点作为输入test_x = new_data_x[i-test_len:i, np.newaxis, :]# 归一化test_x = preminmaxscaler(test_x, train_x_minmax[0], train_x_minmax[1])batch_test_x = torch.tensor(test_x, dtype=torch.float32, device=device)# 如果是第一个预测点,使用初始隐藏状态# 否则,使用上一次预测的隐藏状态继续预测if i == train_size:test_y, hc = model.output_y_hc(batch_test_x, (zero_ten, zero_ten))else:# 仅使用最近的两个时间步来更新状态test_y, hc = model.output_y_hc(batch_test_x[-2:], hc)# 获取完整的预测结果test_y = model(batch_test_x)# 获取最后一个时间步的预测值predict_y = test_y[-1].item()# 反归一化predict_y = unminmaxscaler(predict_y, train_x_minmax[0], train_y_minmax[1])# 保存预测结果new_data_x[i] = predict_yprint(f"预测时间步 {i}, 预测值: {predict_y:.2f}, 真实值: {data_x[i, 0]:.2f}")# 计算测试集的均方误差
    test_predictions = new_data_x[train_size:]
    test_actual = data_x[train_size:]
    mse = np.mean((test_predictions - test_actual) ** 2)
    print(f"测试集均方误差 (MSE): {mse:.2f}")
    
    • new_data_x中,前113天是历史数据,后30天是我们要预测的,因此其值都设置为0。
    • 我们每次输入40天的数据,并希望预测得到下一天,这样依次将114天、115天直到最后一天的数据预测出来。
    • test_x是我们每次输入的40天的历史序列,将其整理成[40, 1, 1]的格式,并进行归一化,然后输入模型。
    • 得到的test_y也是一个40天的序列,最后一个值就是我们预测的下一天的值。使用反归一化将其还原,就是预测的下一天的值。我们将其添加到new_data_x的相应位置中。
    • hc就是模型的隐状态,这样不断返回模型隐状态,再输入到模型中,应该是效果会比较好。这个我不太清楚。

    4.4 可视化预测效果对比

    # 可视化结果
    plt.figure(figsize=(12, 6))
    plt.plot(new_data_x, 'r', label='预测值')
    plt.plot(data_x, 'b', label='真实值', alpha=0.3)
    plt.axvline(x=train_size, color='g', linestyle='--', label='训练/测试分界线')
    plt.legend(loc='best')
    plt.title('LSTM时间序列预测结果')
    plt.xlabel('时间步')
    plt.ylabel('值')
    plt.grid(True)
    plt.savefig('prediction_result.png')
    plt.show()
    print("预测结果已保存至: 'prediction_result.png'")# 放大查看测试集部分
    plt.figure(figsize=(12, 6))
    plt.plot(range(train_size, len(data_x)), new_data_x[train_size:], 'r-o', label='预测值')
    plt.plot(range(train_size, len(data_x)), data_x[train_size:], 'b-o', label='真实值')
    plt.title('测试集预测结果对比')
    plt.xlabel('时间步')
    plt.ylabel('值')
    plt.legend(loc='best')
    plt.grid(True)
    plt.show()
    

     数据结果:

    进一步改进方向

    1. 调整网络结构(层数、隐藏单元数)

    2. 尝试不同的窗口大小

    3. 添加更多特征

    4. 使用更复杂的损失函数

    5. 应用正则化技术防止过拟合

    参考文章:

    使用LSTM进行简单时间序列预测(入门全流程,包括如何整理输入数据)_lstm 时间序列-CSDN博客
    https://zhuanlan.zhihu.com/p/94757947
    https://zhuanlan.zhihu.com/p/94757947

    LSTM详解-CSDN博客

    https://zhuanlan.zhihu.com/p/36455374

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
    如若转载,请注明出处:http://www.pswp.cn/pingmian/88990.shtml
    繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88990.shtml

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    WPS、Word加载项开发流程(免费最简版本)

    文章目录1 加载项对比2 WPS 加载项2.1 本地开发2.1.1 准备开发环境2.1.2 新建 WPS 加载项项目2.1.3 运行项目2.2 在线部署2.2.1 编译项目2.2.2 部署项目2.2.3 生成分发文件2.2.4 部署分发文件2.3 安装加载项2.4 取消发布3 Word 加载项3.1 本地开发3.1.1 准备开发环境3.1.2 新建…

    Flink SQL 性能优化实战

    最近我们组在大规模上线Flink SQL作业。首先,在进行跑批量初始化完历史数据后,剩下的就是消费Kafka历史数据进行追数了。但是发现某些作业的追数过程十分缓慢,要运行一晚上甚至三四天才能追上最新数据。由于是实时数仓指标计算上线初期&#…

    HTML 树结构(DOM)深入讲解教程

    一、HTML 树结构的核心概念 1.1 DOM(文档对象模型)的定义 DOM(Document Object Model)是 W3C 制定的标准接口,允许程序或脚本(如 JavaScript)动态访问和更新 HTML/XML 文档的内容、结构和样式。…

    用鼠标点击终端窗口的时候出现:0;61;50M0;61;50M0;62;50M0

    在做aws webrtc viewer拉流压测的过程中,我本地打开了多个终端,用于连接EC2实例: 一个终端用于启动 ‘并发master脚本’、监控master端的cpu、mem;一个终端用于监控master端的带宽情况;一个终端用于监控viewer端的cpu、…

    C++-linux 5.gdb调试工具

    GDB调试工具 在C/C开发中,程序运行时的错误往往比编译错误更难定位。GDB(GNU Debugger)是Linux环境下最强大的程序调试工具,能够帮助开发者追踪程序执行流程、查看变量状态、定位内存错误等。本章将从基础到进阶,全面讲…

    Update~Read PLC for Chart ~ Log By Shift To be... Alarm AI Machine Learning

    上图~ 持续迭代 1、增加报警弹窗,具体到哪个值,双边规格具体是多少 2、实时显示当前值的统计特征,Max Min AVG ... import tkinter as tk from tkinter import simpledialog import time import threading import queue import logging from datetime import datet…

    es的自定义词典和停用词

    在 Elasticsearch 中,自定义词典是优化分词效果的核心手段,尤其适用于中文或专业领域的文本处理。以下是关于 ES 自定义词典的完整指南: 为什么需要自定义词典? 默认分词不足: ES 自带的分词器(如 Standard…

    微算法科技技术突破:用于前馈神经网络的量子算法技术助力神经网络变革

    随着量子计算和机器学习的迅猛发展,企业界正逐步迈向融合这两大领域的新时代。在这一背景下,微算法科技(NASDAQ:MLGO)成功研发出一套用于前馈神经网络的量子算法,突破了传统神经网络在训练和评估中的性能瓶颈。这一创新…

    一文读懂循环神经网络(RNN)—语言模型+读取长序列数据(2)

    目录 读取长序列数据 为什么需要 “读取长序列数据”? 读取长序列数据的核心方法 1. 滑动窗口(Sliding Window) 2. 分段截取(Segmentation) 3. 滚动生成(Rolling Generation) 4. 关键信息…

    Oracle Virtualbox 虚拟机配置静态IP

    Oracle Virtualbox 虚拟机配置静态IP VirtualBox的网卡,默认都是第一个不能自定义,后续新建的可以自定义。 新建NAT网卡、host主机模式网卡 依次点击:管理->工具->网络管理器新建host主机模式网卡 这个网卡的网段自定义,创建…

    Linux RAID1 创建与配置实战指南(mdadm)

    Linux RAID1 创建与配置实战指南(mdadm)一、RAID1 核心价值与实战目标RAID1(磁盘镜像) 通过数据冗余提供高可靠性:当单块硬盘损坏时,数据不丢失支持快速阵列重建读写性能略低于单盘(镜像写入开销…

    MySQL数据库----函数

    目录函数1,字符串函数2,数值函数3,日期函数4,流程函数函数 1,字符串函数 MySQL中内置了很多字符串函数 2,数值函数 3,日期函数 4,流程函数

    1.2 vue2(组合式API)的语法结构以及外部暴露

    vue2 vue3中可以写vue2的语法&#xff0c;vue2的结构像一个花盆里的根&#xff08;根组件App.vue&#xff09;&#xff0c;根上可以插上不同的枝杈和花朵&#xff08;组件&#xff09;。 组件的结构&#xff1a; // 这里写逻辑行为 <script lang"ts"> export d…

    Swift 解 LeetCode 324:一步步实现摆动排序 II,掌握数组重排的节奏感

    文章目录摘要描述题解答案题解代码&#xff08;Swift&#xff09;题解代码分析步骤一&#xff1a;排序数组步骤二&#xff1a;左右指针分段步骤三&#xff1a;按位置交错插入示例测试及结果示例 1示例 2示例 3&#xff08;边界情况&#xff09;时间复杂度分析空间复杂度分析总结…

    使用SQLMAP的文章管理系统CMS的sql注入渗透测试

    SQLMAP注入演示&#xff1a;抓包拿到Cookie:召唤sqlmap&#xff1a;sqlmap -u "http://192.168.1.99:8085/show.php?id34" --cookie "pma_langzh_CN; kbqug_admin_username2621-PL_LxhFjyVe43ZuQvht6MI5q0ZcpRVV5FI0pzQ6XR8; kbqug_siteid2621-PL_LxhFjyVe4yA5…

    I3C通信协议核心详解

    一、物理层与电气特性双线结构 SCL&#xff08;串行时钟线&#xff09;&#xff1a;主设备控制&#xff0c;支持 推挽&#xff08;Push-Pull&#xff09;输出&#xff08;高速模式&#xff09;和 开漏&#xff08;Open-Drain&#xff09;&#xff08;兼容I2C模式&#xff09;。…

    Docker搭建Redis哨兵集群

    Redis提供了哨兵机制实现主从集群下的故障转移&#xff0c;其中包含了对主从服务的检测、自动故障恢复和通知。 1.环境 centos7、redis6.2.4、MobaXterm 目的&#xff1a; 搭建redis的主从同步哨兵集群&#xff08;一主一从三哨兵&#xff09; 2.步骤 1.主从集群的搭建 主从…

    暑假Python基础整理 --异常处理及程序调试

    异常概念 在程序运行过程中&#xff0c;经常会遇到各种各样的错误&#xff0c;这些错误统称为“异常”。如下表是Python常见的异常与描述&#xff1a; 异常描述NameError尝试访问一个未声明的变量引发错误IndexError索引超出序列范围引发错误IndentationError缩进错误ValueErr…

    k8s-高级调度(二)

    目录 Taint(污点)与Toleration(容忍) Taint&#xff08;污点&#xff09;&#xff1a;节点的排斥标记 Toleration&#xff08;容忍&#xff09;&#xff1a;Pod的适配声明 与节点亲和性的对比 警戒(cordon)和转移(drain) Cordon&#xff1a;节点隔离&#xff08;阻止新 Po…

    基于OpenCV的深度学习人脸识别系统开发全攻略(DNN+FaceNet核心技术选型)

    核心技术选型表 技术组件版本/型号用途OpenCV DNN4.5.5人脸检测FaceNet (facenet-pytorch)0.5.0人脸特征提取MiniConda最新版Python环境管理PyTorch1.8.0FaceNet运行基础OpenVINO2021.4模型加速(可选)SSD Caffe模型res10_300x300高精度人脸检测 一、环境准备与项目搭建 1.1 M…