使用LSTM模型进行时序数据预测

1. LSTM模型简介

        LSTM,全程长短期记忆网络(Long Short-Term Memory),是在循环神经网络基础上发展起来的,主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。LSTM神经网络能够处理长时间序列数据,具有长时记忆能力,因此成为解决时间序列数据预测问题的有效方法。其他关于LSTM的发展过程和相关原理就不多赘述了,本文重点是结合实例介绍如何使用LSTM进行预测。

2. 研究目标

        使用临近若干时刻的气象观测数据,预测未来3个时刻的能见度。

3. 准备工作

        3.1 环境准备

        python 3.x环境,必要库:pytorch、numpy、pandas、scikit-learn

        3.2 数据准备

        全国气象站逐小时观测数据,要素包括:温度、露点温度、相对湿度、饱和水汽压差、能见度。每个时刻的观测数据为一个csv文件,每个文件中的行代表不同的站点观测值,列代表不同的要素属性,如下所示:

        数值为999999表示缺测,这是我们要清洗掉的数据。

4. 数据预处理

        4.1 读取数据

        首先从文件中读取数据:

import pandas as pd
import os
import numpy as np

indir = r'./filepath'    #保存所有数据文件的文件夹
file_list = os.listdir(indir)    #获得所有数据文件的文件名
feature_vars = ['TEM', 'DPT', 'RHU', 'VAP', 'VIS']
label_vars = 'VIS'

dataset = []
for f in range(len(file_list)):
    df = pd.read_csv(os.path.join(indir, f), na_values=[999999, 999107], usecols=['Station_Id_C', 'Lat', 'Lon', 'TEM', 'DPT', 'RHU', 'VAP', 'VIS'])
    df = df.dropna(axis=0, how='any').query('Lat>17 & Lat<29 & Lon>108 & Lon<123')    #剔除所有存在空值的行,并提取目标空间范围内的数据
    dataset.append(df.loc[:, feature_vars].values)
dataset = np.array(dataset, dtype=np.float32)    #(times, stations, features)

        上步将所有数据都读进了dataset三维数组中,三个维度分别是时间、站点、要素。dropna函数用于剔除所有存在空值的行,query函数用于筛选位于指定范围内的站点。接下来需要根据要预测的时刻(pred_step)和用于预测的时刻(time_step)将数据构造为样本集。

        4.2 创建样本集

        (1)什么是样本?

        准确理解样本的含义对于接下来的工作非常重要!

        样本就是一组数据,它包括两部分内容:特征数据和标签数据。标签数据很简单,就是你的预测目标,而特征数据则是用来计算标签数据的数据。举个例子:

        假设我想要用当前时刻的温度和气压去预测风速,那么温度和气压组成的数据就是特征数据,风速则是标签数据,特征数据和标签数据又组成一个样本。如果我有100个时刻的温度、气压、风速数据,那么就能形成100个样本,这便是样本集。如果只有单站点数据,那么样本数量把往往和时间长度有关;如果是多站点数据,那么时间长度与站点数量都会影响最终的样本总数。

        (2)构造样本集

        上面对于样本的例子是最简单的一种,现在来个复杂点的:我们需要用最近5个时刻的温度和气压数据,去预测未来3个时刻的风速数据。

        显然,现在我们有了三个“时间”,分别是用于预测的时间(time_step)、预测的目标时间(pred_step)、样本数据的时间(times)。为了便于理解,不妨假设原始观测资料共有times=100个连续时刻:

如上图所示,在时间序列上从左向右滑动,每次滑动都能生成一个样本,显然时序总长为100,time_step=5,pred_step=3时,总共能够生成93个样本,每个样本包含:

特征数据:time_step*features,即5个时刻*2个要素

标签数据:pred_step*labels,即3个时刻*1个要素

经过这一步处理后,原来的时间长度缩短了(times - time_step - pred_step + 1),新时间维的长度就是这个站点所能够生成的样本数,如果总共有N个站点,那么样本总数就是N*(times - time_step - pred_step + 1)。代码如下:

time_step = 5
pred_step = 3
samples = []
X = []
Y = []

for i in range(dataset.shape[0] - time_step - pred_step + 1):
    X.append(dataset[i:i+time_step, :, :-1])    #(time_step, stations, features)
    Y.append(dataset[i+time_step:i+time_step+pred_step, :, -1])    #(pred_step, stations)
X = np.array(X).transpose(0, 2, 1, 3)    #(new_times, stations, time_step, features)
Y = np.array(Y).transpose(0, 2, 1)    #(new_times, stations, pred_step)

        注意,这里由于标签要素的值恰好在dataset的最后一列,所以直接切片,不然的话得根据位置调整。

        到了这一步,就完全可以把新的时间维度与站点维度合并成样本维度了:

samples_x = X.reshape(-1, time_step, features)    #形状为(samples, time_step, features)
samples_y = Y.reshape(-1, pred_step)    #形状为(samples, pred_step)

        样本集创建完成,样本总数为(times - time_step - pred_step + 1) * stations。

        4.3 拆分样本集

        将样本集拆分成训练集、验证集、测试集三部分:

from sklearn.model_selection import train_test_split

train_x, tmp_x, train_y, tmp_y = train_test_split(data_x, data_y, train_size=0.6, random_state=42, shuffle=True)
val_x, test_x, val_y, test_y = train_test_split(tmp_x, tmp_y, train_size=0.5, random_state=42, shuffle=True)

        4.4 样本归一化

        不同要素的数值范围可能又很大差别,如果直接使用会导致它们对模型预测的影响度不同,因此需要先进行归一化。

from sklearn.preprocessing import MinMaxScaler

scale_x = MinMaxScaler()
train_x = scale_x.fit_transform(train_x.reshape(-1, len(feature_vars)).reshape(-1, time_step, len(feature_vars))
val_x = scale_x.transform(val_x.reshape(-1, len(feature_vars))).reshape(-1, time_step, len(feature_vars))
test_x = scale_x.transform(test_x.reshape(-1, len(feature_vars))).reshape(-1, time_step, len(feature_vars))

scale_y = MinMaxScaler()
train_y = scale_y.fit_transform(train_y)
val_y = scale_y.transform(val_y)
test_y = scale_y.transform(test_y)

        之所以不先归一化再拆分样本集,是为了避免归一化的过程中训练样本获取到了测试集的数据信息,影响测试效果。这里记得保留scale_y这个归一化器,后面在模型预测完成后可以用它对预测值逆归一化,就能使预测数值重新变得可解释。

5. 模型训练

        5.1 定义LSTM模型

        首先需要根据我们的数据和任务需求定义一个lstm模型,pytorch已经提供了很便捷的方法,我们直接调用即可:

import torch
import torch.nn as nn
import torch.optim as optim

class LSTMModel(nn.Module):
    '''
    long-short term model
    '''
    def __init__(self, input_size, hidden_size, output_size, num_layers=1, dropout_rate=0.2):
        super(LSTMModel, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.dropout_rate = dropout_rate
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.dropout = nn.Dropout(dropout_rate)
        self.fc1 = nn.Linear(hidden_size, output_size)
        self.relu = nn.ReLU()

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        out, _ = self.lstm(x, (h0, c0)) # LSTM层
        out = self.dropout(out[:, -1, :])    #非必需
        out = self.fc1(out) # 全连接层
        out = self.relu(out)    #非必需
        return out

上面的模型包含两个函数块,__init__函数为LSTMModel类的初始化函数,对模型的各项参数进行了定义。forward函数定义了数据在模型中的流动、传递和操作,就像流水线加工一样,先经过第一道函数处理,再传给第二个,以此类推......

数据在模型中的流动过程为:

(1)首先特征数据x被输入到模型中,先通过lstm层,这一层接受形如[batch_size, seq_len, input_size]的三维张量。其中batch_size为每个批次输入的样本数(后面会再解释相关概念);seq_len表示样本的序列长度,对应于time_step;input_size表示样本的特征数量,对应于len(feature_vars)。

lstm默认的输入维度为[seq_len, batch_size, input_size],但我们可以通过令batch_first=True将前两维交换位置。

值得注意的是,如果数据的time_step=1(即每个样本只有1个时刻的特征要素数据),或者len(feature_vars)=0(即每个样本只有1个特征要素),那也必须保持三维,只是将对应的维度长度设置为1,即输入的张量x变成[batch_size, 1, input_size]或[batch_size, seq_len, 1]。

lstm层输出为out=[batch_size, seq_len, hidden_size],显然x的最后一个维度被变成了hidden_size,其他维度长度没变化;但是再seq_len维度上,out储存了模型每个时间步的预测值,其中只有最后一个时间步,即out[:, -1, :]才是我们需要的最终预测值。

(2)lstm的输出被传入dropout层,这一步主要是为了随机丢弃一些神经元,避免模型过拟合,非必需。

(3)上步输出结果传入fc1全连接层,得到[batch_size, output_size],这里的output_size根据任务需求由自己定义。比如我需要预测1个要素的未来3个时刻数值,则定义output_size=3。

(4)上步结果通过relu函数,去除负值。这一步也非必需。

        5.2 模型训练

import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt

def set_loader(x, y, batch_size):
    '''
    输入要素数据和标签数据,构造数据加载器

    参数:
    -----
    x: 要素数据。数组
    y: 标签数据。数组
    '''
    
    tensor_x = torch.from_numpy(x)
    tensor_y = torch.from_numpy(y)
    loader = DataLoader(TensorDataset(tensor_x, tensor_y), batch_size=batch_size, shuffle=True)
    
    return loader

def lstm_train(model, epochs, train_loader, val_loader, learning_rate=0.01, plot_loss=False):
    '''
    LSTM模型训练
    '''
    # 定义损失函数和优化器
    loss_function = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)

    train_losses = []
    val_losses = []

    for epoch in range(epochs):
        model.train()
        train_loss = 0.0
        for x_batch, y_batch in train_loader:
            optimizer.zero_grad()   #每一次循环前,梯度清零
            outputs = model(x_batch)
            loss = loss_function(outputs, y_batch)
            loss.backward()         #反向传播
            optimizer.step()        #梯度下降
            train_loss += loss.item() * x_batch.size(0)
        train_loss /= len(train_loader.dataset)
        train_losses.append(train_loss)

        model.eval()
        val_loss = 0.0
        with torch.no_grad():
            for x, y in val_loader:
                outputs = model(x)
                loss = loss_function(outputs, y)
                val_loss += loss.item() * x.size(0)
        val_loss /= len(val_loader.dataset)
        val_losses.append(val_loss)

        print(f'Epoch [{epoch+1}/{epochs}], Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}')

    if plot_loss:
        #查看损失函数曲线
        fig = plt.figure(figsize=[8,6])
        ax = fig.add_subplot(111)
        ax.plot(train_losses, 'b', label='train_losses')
        ax.plot(val_losses, 'r', label='val_losses')
        ax.legend()
        ax.set_title(f'Epochs:{epochs}  learning_rate:{learning_rate}')
        plt.show()
    
    return model

hidden_size = 16
input_size = len(feature_vars)
output_size = pred_time
batch_size = 16
epochs = 500

train_loader = set_loader(train_x, train_y, batch_size)
val_loader = set_loader(val_x, val_y, batch_size)

model = LSTMModel(input_size, hidden_size, output_size, num_layers=1, dropout_rate=0.3)

model = lstm_train(model, epochs, train_loader, val_loader, learning_rate=0.001, plot_loss=False)

set_loader函数用于根据样本数据集创建数据加载器,主要做了两件事:(1)把数组转成torch.tensor格式,这是必要的;(2)根据设置好的batch_size,把样本分批次。

batch_size的意义在于每次向模型投入不同数量的样本用于训练,例如样本总数100,batch_size=10,则每次训练都要分10次投放样本。一般来说,batch_size习惯设置为2的幂次数,当然非必须,最小可以设置为1,即每个只投放1个样本。batch_size越大,模型训练越快,但是收敛也会比较慢。

lstm_train函数用于训练模型,需要输入的参数包括:

model:实例化的LSTMModel。

epochs:模型训练次数。

train_loader:训练样本集的数据加载器。

val_loader:验证样本集的数据加载器。

learning_rate:学习率,数值范围为0~1。又是一个需要反复尝试的参数。学习率过大,模型会无法收敛;学习率太小,模型则容易过拟合。

plot_loss:bool型。若为True,则模型训练完成后会弹出损失函数曲线图,用于辅助调参。

再具体看lstm_train函数内部:

loss_function = nn.MSELoss()为模型损失函数,用于衡量预测值和真实值的差距。

optimizer = optim.Adam(model.parameters(), lr=learning_rate)定义了一个优化器。

这两个都可以根据实际需求修改。

for epoch in range(epochs)开始进行多次模型训练:

for x_batch, y_batch in train_loader:

            optimizer.zero_grad()   #每一次循环前,梯度清零

            outputs = model(x_batch)

            loss = loss_function(outputs, y_batch)

            loss.backward()         #反向传播

            optimizer.step()        #梯度下降

            train_loss += loss.item() * x_batch.size(0)

梯度清零、反向传播、梯度下降是3个固定步骤,train_loss则是用于保存模型再训练集上预测的损失函数值,便于诊断模型性能。

后面在验证集上对模型执行了同样的操作,并记录了验证集损失函数值。

两个损失函数的变化可以反应模型的学习效果与泛化能力:

理想情况下,两条曲线应该是先快速下降,然后慢慢稳定下来。

如果曲线一直在下降,那么可能是训练次数不够,需要调高epochs。

如果train_loss下降/稳定,而val_loss却稳定/上升,那么说明模型过拟合了,可以简化一下网络结构或者增加dropout层。

        5.3 模型预测

        上步,我们进行了模型训练和验证,并完成了调优,接下来就可以在测试集上进行预测了。

model.eval()
with torch.no_grad():
    out = model(torch.from_numpy(test_x))    #模型预测
pred = scale_y.inverse_transform(out)    #结果逆归一化

        原数据的变化范围太大,预测结果在极大极小的表现较差,但是基本能够预测出变化趋势。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值