LSTM实现多变量输入多步预测(直接多输出)时间序列预测(PyTorch版)

前言

  • 👑 最近很多订阅了🔥《深度学习100例》🔥的用户私信咨询基于深度学习实现时间序列的相关问题,为了能更清晰的说明,所以建立了本专栏专门记录基于深度学习的时间序列预测方法,帮助广大零基础用户达到轻松入门。

  • 👑 本专栏适用人群:🚨🚨🚨深度学习初学者刚刚接触时间序列的用户群体,专栏将具体讲解如何快速搭建深度学习模型用自己的数据集实现时间序列预测,快速让新手小白能够对基于深度学习方法进行时间序列预测有个基本的框架认识

  • 👑 本专栏整理了《深度学习时间序列预测案例》,内包含了各种不同的基于深度学习模型的时间序列预测方法,例如LSTM、GRU、CNN(一维卷积、二维卷积)、LSTM-CNN、BiLSTM、Self-Attention、LSTM-Attention、Transformer等经典模型,💥💥💥包含项目原理以及源码,每一个项目实例都附带有完整的代码+数据集

正在更新中~ ✨

🚨 我的项目环境:

  • 平台:Windows10
  • 语言环境:python3.7
  • 编译器:PyCharm
  • PyTorch版本:1.11.0

💥 项目专栏:【深度学习时间序列预测案例】零基础入门经典深度学习时间序列预测项目实战(附代码+数据集+原理介绍)

一、基于PyTorch搭建LSTM模型实现多变量输入多步预测(直接多输出)风速时间序列预测

本篇文章开始我们将讲解如何使用PyTorch实现多步预测,对于多步预测主要是有五种策略,分别是 直接多输出预测递归多步预测(单步滚动预测) 直接多步预测(多模型单步预测)直接递归混合预测(多模型滚动预测)Seq2Seq多步预测,对于这五种不同的策略分别有不同的优缺点,本篇开始将实现这五种不同的多步预测策略,让新手朋友们能够对这五种策略有个初步的认知。

关于这五种策略的详细对比说明可以参考本专栏的这篇文章 时间序列多步预测经典方法总结 ,如果对于多步预测不了解的朋友可以先去看这篇文章有个大概了解之后,再回来观看本篇文章,可能看的更清晰。

接下来本篇文章我们将采用另外一种经典的循环神经网络——长短期记忆神经网络 LSTM 来对我们的时序数据建模处理,本篇将💎 讲解如何基于多变量进行时序预测,本篇是使用多个特征来对未来多个状态进行时序预测的,详细介绍项目的每个实现部分以及细节处理,帮助新手小白快速建立起如何处理时序数据的框架

二、多步预测(直接多输出)

为了方便讲解不同的多步预测策略,这里假设我们的时间序列数据为【t1,t2,t3,t4,t5】,这是我们已知的数据,我们需要预测未来两天的状态【t6,t7】。

对于这个策略是比较好理解与实现的,就是训练一个模型,只不过需要在模型最终的线性层设置为两个输出神经元即可。

正常我们的单输出预测,预测未来一天的模型最终的输出层为 nn.Linear(hidden_size,1),对于直接多步预测我们修改输出层为 nn.Linear(hidden_size,2) 即可,最后一层的两个神经元分别预测 t6t7

在这里插入图片描述

定义的模型结构状态为:【t1,t2,t3,t4,t5】 🔜 【t6,t7】

对于这种策略优点就是预测 t6t7 是独立的,不会造成误差累计,因为两个预测状态会同时通过线性层进行预测,t7 的预测状态不会依赖 t6,那么缺点也很显然,就是这两个状态独立了,但是现实是因为这是时序问题,t7 的状态会受 t6 的状态所影响,如果分别独立预测,t7 的预测状态会受影响,造成信息的损失。

三、配置类

下面是本项目需要使用的参数以及相关变量,为了方便我们将所有参数封装到一个类中,也可以使用 argparse 参数解析方式。

为了说明数据各个阶段的维度变化,特此定义了如下变量大小,小伙伴需要记住下面变量的值一遍理解下文说明各个阶段的维度大小。

class Config():
    data_path = '../data/wind_dataset.csv'
    timestep = 20  # 时间步长,就是利用多少时间窗口
    batch_size = 32  # 批次大小
    feature_size = 8  # 每个步长对应的特征数量,这里只使用1维,每天的风速
    hidden_size = 256  # 隐层大小
    output_size = 2  # 由于是多输出任务,最终输出层大小为2,预测未来2天风速
    num_layers = 2  # lstm的层数
    epochs = 10 # 迭代轮数
    best_loss = 0 # 记录损失
    learning_rate = 0.0003 # 学习率
    model_name = 'lstm' # 模型名称
    save_path = './{}.pth'.format(model_name) # 最优模型保存路径

config = Config()

四、时序数据集的制作

对于时序数据集的制作,在前面的文章已经说的很清楚,不清楚的小伙伴可以先去了解一下,所以这里不再赘述,LSTM实现时间序列预测(PyTorch版)

本数据集使用的是风速数据集,数据集介绍可以看这 深度学习时间序列预测项目案例数据集介绍 ,该数据集有8列,也就是每天会有8个特征,例如风速、温度、降水量等,但是本文仍处于入门阶段,所以不需要使用这么多变量同时预测,仅仅使用风速这一列特征进行时序预测,也就是单变量输入,本项目是使用如下代码获取时序数据的:

# 形成训练数据,例如12345789 123-4 234-5
def split_data(data, timestep, feature_size):
    dataX = []  # 保存X
    dataY = []  # 保存Y

    # 将整个窗口的数据保存到X中,将未来一天保存到Y中
    for index in range(len(data) - timestep):
        dataX.append(data[index: index + timestep])
        dataY.append(data[index + timestep][0])

    dataX = np.array(dataX)
    dataY = np.array(dataY)

    # 获取训练集大小
    train_size = int(np.round(0.8 * dataX.shape[0]))

    # 划分训练集、测试集
    x_train = dataX[: train_size, :].reshape(-1, timestep, feature_size)
    y_train = dataY[: train_size].reshape(-1, 1)

    x_test = dataX[train_size:, :].reshape(-1, timestep, feature_size)
    y_test = dataY[train_size:].reshape(-1, 1)

    return [x_train, y_train, x_test, y_test]

运行上述代码后我们可以看一下,获得的训练集维度:

x_train, y_train, x_test, y_test = split_data(data, config.timestep, config.feature_size)

print(x_train.shape)
print(y_train.shape)

>>>(4494, 20, 8)
>>>(4494, 1)

上面的训练集的维度为【4494,20,8】,这三个维度的意思分别代表【样本数,时间步,特征维度】,样本数就不用多说了吧,时间步就是利用过去多长时间来预测未来的数据,如果我们设置为30,那就是基于过去30天的样本数据来预测未来1天的数据,特征维度就是每天的特征维度,如果我们利用所有特征,那么特征维度应该为8(温度、风速、降水等)。

本项目就是利用过去20天的天气数据来预测未来一天的风速信息,也就是多变量输入单变量输出的问题,多变量输入就是利用多个特征信息,单变量输出就是预测单个变量,本项目是预测风速。

如果熟悉NLP,那么时间步可以看成是 seq_len ,就是序列长度,一句话有多少个词,而特征维度可以看成是 embedding_size ,也就是每个词的嵌入维度,如果不熟悉NLP也没关系,只要记住我们是基于过于 timestep 天数的数据进行预测,而每一天的特征又需要一个向量来表示,但是本文使用的是8个特征,所以 feature_size=8

小伙伴们应该了解输入数据的格式以及各个维度的意义,所以可以自己尝试自己的数据集以及尝试利用不同数量的特征以及时间步进行处理。

🚩🚩🚩至此,训练集的维度为【4494,20,8】,各个维度代表【样本数,时间步,特征维度】,英文变量表示【len(data),timestep,feature_size】。

五、数据归一化

数据的 归一化和标准化 是特征缩放(feature scaling)的方法,是数据预处理的关键步骤。不同评价指标往往具有不同的量纲和量纲单位,这样的情况会影响到数据分析的结果,为了消除指标之间的量纲影响,需要进行数据归一化/标准化处理,以解决数据指标之间的可比性。原始数据经过数据归一化/标准化处理后,各指标处于同一数量级,适合进行综合对比评价。

常见的归一化方式有:Min-MaxZ-ScoreL2范数归一化等等,对于本项目使用的是 MIn-Max 归一化,他会将所有的数据缩放到 0-1 区间,对于该操作可以手动实现,代码如下:

(df - df.min()) / (df.max() - df.min())

也可以使用 sklearn 中提供的 MinMaxScaler() 函数实现,代码如下:

scaler = MinMaxScaler()
data = scaler_model.fit_transform(np.array(df))

有兴趣的小伙伴可以多多尝试不同的归一化方式,可以参照这篇文章进行尝试 归一化 (Normalization)、标准化 (Standardization)和中心化/零均值化 (Zero-centered)

六、数据集加载器

上面已经处理好了数据,获得了模型的输入数据,但是对于PyTorch的输入需要是Tensor类型数据,所以此时需要将上面获得的numpy.array类型数据转成Tensor类型数据。

x_train, y_train, x_test, y_test = split_data(data, config.timestep, config.input_size)

# 将数据转为tensor
x_train_tensor = torch.from_numpy(x_train).to(torch.float32)
y_train_tensor = torch.from_numpy(y_train).to(torch.float32)
x_test_tensor = torch.from_numpy(x_test).to(torch.float32)
y_test_tensor = torch.from_numpy(y_test).to(torch.float32)

现在数据集已经全部转成了Tensor类型数据,此时已经可以直接喂入模型进行使用,但是我们常常使用数据加载器,数据集如果小直接使用Tensor也都OK,但是如果我们的数据集过大,如果不适用数据加载器直接加载到内存中会导致内存爆炸,所以要分批次一点点加载数据。

# 形成训练数据集
train_data = TensorDataset(x_train_tensor, y_train_tensor)
test_data = TensorDataset(x_test_tensor, y_test_tensor)

# 将数据加载成迭代器
train_loader = torch.utils.data.DataLoader(train_data,
                                           config.batch_size,
                                           False)

test_loader = torch.utils.data.DataLoader(test_data,
                                          config.batch_size,
                                          False)

🚩🚩🚩至此,训练集的维度为【32,1,1】,各个维度代表【批次大小,时间步,特征维度】,英文变量表示【batch_size,timestep,feature_size】。

七、搭建LSTM模型

本项目使用的模型是经典的长短期基于神经网络 LSTM,由于此专栏注重实战讲解,所以关于LSTM模型的原理词处就不赘述了,有兴趣的小伙伴可以去百度进行了解,这里你只需要知道LSTM是一个深度学习模型,能够处理时序数据,获取时间维度上的关联信息。

该图为PyTorch官方文档给出的LSTM模型解释:

在这里插入图片描述

为了能够使用LSTM搭建模型处理我们的时序数据,我们需要LSTM模型的输入和输出是什么,对于初学者了解这个很重要,它能够快速帮助我们在自己的数据集上调试运行。

该图为LSTM模型的参数:

在这里插入图片描述

  • input_size:每个时间点的特征维度,就是对应上面我们说的每天的特征维度是3还是1
  • hidden_size:GRU内部隐层的维度大小
  • num_layers:GRU的层数,默认为1
  • bias:是否在隐层中添加偏置bias,默认为True
  • batch_first:如果设置为True,GRU的输入第一个维度为批次大小,也就是【batch_size,seq_len,feature_size】,如果为False,则模型的输入Tensor的维度为【seq_len,batch_size,feature_size】,默认为False
  • dropout:是否采用dropout
  • bidirectional:是否采用双向GRU模型,默认单向为False
  • proj_size:LSTM网络的变体,即LSTMP,减少LSTM的参数和计算量,进行隐层进行压缩,默认为0

该图为LSTM模型的输入:

在这里插入图片描述

LSTM的模型输入Tensor的维度为【batch_size,seq_len,feature_size】,其实也可以是【seq_len,batch_size,feature_size】,但是我们常常将批次大小作为第一个维度传入,容易理解,本项目采用的输入维度为第一种方式,批次为先,对于LSTM的输入也就是我们项目数据的维度【4494,20,8】,其中 timestep=seq_len

此处有小伙伴会存在一个问题,模型的输入还有一个 h_0c_0 作为输入,如果了解LSTM原理的同学就可以知道这两个输入变量是干嘛的,就是模型初始的隐层状态和细胞状态,对于这个变量可传可不传,如果不传则默认为0,有兴趣了解这个参数到底传入还是不传入可以参考这篇文章 对LSTM中每个batch都初始化隐含层的理解 ,本项目的隐层状态是传入的,但是传入的参数是以0进行填充,和默认不传入一致,只是为了让小伙伴了解这个参数是怎么传入的。

对于 h_0 初始化权重维度应该是【D*num_layers,batch_size,hidden_size】,如果采用双向LSTM,则 D=2,否则为1,对于本项目使用的是单向LSTM,所以D=1,那么我们的 h_0 的维度就应该是【2,32,256】,2是我们定义了2层LSTM,32是批次大小,256是LSTM中间的隐层大小。

c_0 的定义维度和 h_0 一致,就不再赘述了。

# 初始化隐层状态
if hidden is None:
	h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
	c_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
else:
	h_0, c_0 = hidden

该图为LSTM模型的输出:

在这里插入图片描述

LSTM模型的输出有三个,相对于GRU多了一个细胞状态 c_n,一个输出的是模型的最终输出,也就是我们想要的输出,另外一个输出是模型的隐藏状态 h_n ,另外一个就是细胞状态 c_n,对于本项目来说我们并不需要细胞状态和隐藏状态。

对于这三个输出的维度一定要知道,首先是我们需要的输出 output,该输出的维度为【batch_size,seq_len,D * hidden_size】,此处的D就是我们是否采用双向LSTM,如果设置 bidirectional=True,则D=2,否则D=1,hidden_size 就是LSTM中间隐层的维度大小,对于本项目的 output 的维度应该为【32,8,256】。

对于h_nc_n的输出我们简答了解一下就好,因为我们不会对他进行处理。

h_nc_n 对于本项目的输出维度都为【2,32,256】,因为我们采用的是单向LSTM,所以D=1,隐层大小为256。

为了能够理解LSTM的输入和输出,举个例子说明:

model = nn.LSTM(input_size=3, hidden_size=10, num_layers=2, batch_first=True)

x = torch.randn(32, 5, 3)

output, (h_0, c_0) = model(x)

我们定义了输入向量,该向量的维度为【32,5,3】,分别代表【批次大小,时间片,特征大小】,用语言叙述就是32个样本,然后用5天的数据去未来1天的数据,每天的特征维度为3。

print(output.shape)
>>>torch.Size([32, 5, 10])

print(h_0.shape)
>>>torch.Size([2, 32, 10])

print(c_0.shape)
>>>torch.Size([2, 32, 10])

我们可以看到 output 的输出维度为【batch_size,seq_len,D * hidden_size】,由于我们的LSTM是单向的,所以D=1。

如果设置为双向:

model = nn.LSTM(input_size=3, hidden_size=10, num_layers=2, batch_first=True, bidirectional=True)

x = torch.randn(32, 5, 3)

output, (h_0, c_0) = model(x)

print(output.shape)
>>>torch.Size([32, 5, 20])

print(h_0.shape)
>>>torch.Size([4, 32, 10])

print(c_0.shape)
>>>torch.Size([4, 32, 10])

接下来就到了使用PyTorch搭建LSTM模型,代码如下:

# 定义LSTM网络
class LSTM(nn.Module):
    def __init__(self, feature_size, hidden_size, num_layers, output_size):
        super(LSTM, self).__init__()
        self.hidden_size = hidden_size  # 隐层大小
        self.num_layers = num_layers  # lstm层数
        # feature_size为特征维度,就是每个时间点对应的特征数量,这里为1
        self.lstm = nn.LSTM(feature_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden=None):
        batch_size = x.shape[0] # 获取批次大小
        
        # 初始化隐层状态
        if hidden is None:
            h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
            c_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
        else:
            h_0, c_0 = hidden
            
        # LSTM运算
        output, h_0 = self.lstm(x, (h_0, c_0))
        
        # 获取LSTM输出的维度信息
        batch_size, timestep, hidden_size = output.shape  
            
        # 将output变成 batch_size * timestep, hidden_dim
        output = output.reshape(-1, hidden_size)
        
        # 全连接层
        output = self.fc(output)  # 形状为batch_size * timestep, 1
        
        # 转换维度,用于输出
        output = output.reshape(timestep, batch_size, -1)
        
        # 我们只需要返回最后一个时间片的数据即可
        return output[-1]

本项目的模型采用的是LSTM+全连接层,有小伙伴可能有一个问题就是搭建模型的过程中有句代码就是 output = output.reshape(timestep, batch_size, -1),它的目的就是将我们的输出数据的第一个维度变成时间片。

如果我们设置 timestep=5,那么我们的 output 的输出就为【5,32,1】,作为模型输出我们只需要最后一个时间片的数据作为输出即可,因为GRU是处理时序数据的,最后一个时间片包含了前面所有时间片的信息(T1,T2…)。

感兴趣的小伙伴可以尝试不返回最后一个时间片的数据作为输出,可以将每个时间片的数据池化再进行输出(例如将每个时间片的数据取平均、取最大等操作),也就是将5个【32,1】维度的张量取平均再进行输出,不过这样会提高运算时间,至于模型效果会不会提高,需要自己在自己的数据集上面尝试,希望小伙伴初学时可以多多尝试。

接下来我们会讲解数据送入到模型中维度的变化:

我们的输入数据 x 的维度为【4494,20,8】,首先将它送入到了 lstm中进行运算 output, (h_0, c_0) = self.lstm(x, (h_0, c_0)),经过这句我们会获得三个LSTM对应的输出:

print(output.shape)
>>>torch.Size([32, 8, 256])

print(h_0.shape)
>>>torch.Size([2, 32, 256])

print(c_0.shape)
>>>torch.Size([2, 32, 256])

🚩🚩🚩至此,output 的维度为【32,8,256】,各个维度代表【批次大小,时间步,隐层维度】,英文变量表示【batch_size,timestep,hidden_size】,h_0 的维度为【2,32,256】,各个维度代表【双向 * LSTM层数,批次大小,隐层维度】,英文变量表示【D * num_layers,batch_size,hidden_size】,c_0 的维度为【2,32,256】,各个维度代表【双向 * LSTM层数,批次大小,隐层维度】,英文变量表示【D * num_layers,batch_size,hidden_size】。

然后我们把 output 进行了维度转换将其变成【batch_size * timestep,hidden_size】,对于本项目的维度就是【32*8,256】,然后将结果送入到全连接层,得到结果【batch_size * timestep,output_size】,本项目的维度就是【32 * 1,1】,对于本项目使用的timestep为1,所以结果是【32,1】,假设我们使用的timestep=30,那么上一步得到的结果就是【32 * 30,1】,为了得到最后一个时间片的输出,我们需要将其转成【timestep,batch_size,output_size】的形式,然后返回最后一个时间片 output[-1] ,也就是【32,1】。

其实本项目全连接层这么实现有点麻烦了,但是为了新手能够了解过程还是写的麻烦了一点,可以完全不用转换维度,直接将lstm的输出送入到全连接层,可以通过下面代码简单实现:

def forward(self, x, hidden=None):
	batch_size = x.shape[0] # 获取批次大小
	
	# 初始化隐层状态
	if hidden is None:
	    h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
	    c_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
	else:
	    h_0, c_0 = hidden
	    
	# LSTM运算
	output, (h_0, c_0) = self.lstm(x, (h_0, c_0))
	
	# 全连接层
	output = self.fc(output)  # 形状为batch_size, timestep, output_size【32,1,1】
        
    # 我们只需要返回最后一个时间片的数据即可
    return output[:, -1, :]

八、定义模型、损失函数、优化器

为了训练数据,首先定义LSTM模型,然后再定义对应的损失函数,由于我们这里是风速预测,显然是个回归问题,所以采用回归问题常用的 MESLoss(),如果可以的话,可以自定义损失函数,针对自己的项目需求定义对应的损失函数。

对于优化器来讲,使用的也是目前常用的 Adam 优化器,对于新手来讲也可以多多尝试其它的优化器,比如 SGDRMSprop等,对于优化器的选择,可以参考这篇文章 Pytorch 30种优化器总结

model = LSTM(config.feature_size, config.hidden_size, config.num_layers, config.output_size)  # 定义LSTM网络
loss_function = nn.MSELoss()  # 定义损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate)  # 定义优化器

在定义优化器的同时,我们需要将模型的参数传入,可以通过 model.parameters() 获得,如果模型中有些层不需要训练,我们可以将参数冻结,这时使用优化器训练模型时,该层参数就不会被修改,实现该目的可以使用以下函数:

# 冻结模型参数
for param in model.parameters():
    param.requires_grad = False

另外多说一点,有些复杂的模型需要定义多个优化器,也就是模型中不同的层或者参数需要使用不同的优化策略进行优化,例如本项目中我们的模型包含了LSTM层和全连接层,我们可以定义 optimizer_gru = torch.optim.Adam()optimizer_fc = torch.optim.RMSprop(),这时两个层可以使用不同的学习率或者权重衰减方式进行训练,但这是后话, 对于新手不需要搞这么复杂,用一个优化器训练模型即可,如果有能力可以自己尝试以下,使用多个优化器来训练自己的模型。

另外一点,如果我们需要进行权重衰减,可以在定义优化器的同时传入 weight_decay ,有兴趣的同学可以参考这篇文章进行了解 权重衰退

九、模型训练

下面代码是相对标准的模型训练框架,涵盖训练集和测试集的验证,该部分用户可以自己DIY设计,比如统计相关的指标信息(整体损失、epoch损失等)或者打印信息等,这些都可以按照自己的喜好进行调整。

对于模型验证部分,添不添加都OK,但是一般我们是会加上的,因为防止过拟合,让模型有更好的泛化性、鲁棒性,模型训练完成需要在测试集上进行验证,如果损失下降,则保留此轮训练的模型。

# 模型训练
for epoch in range(config.epochs):
    model.train()
    running_loss = 0
    train_bar = tqdm(train_loader)  # 形成进度条
    for data in train_bar:
        x_train, y_train = data  # 解包迭代器中的X和Y
        optimizer.zero_grad()
        y_train_pred = model(x_train)
        loss = loss_function(y_train_pred, y_train.reshape(-1, 1))
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                 config.epochs,
                                                                 loss)

    # 模型验证
    model.eval()
    test_loss = 0
    with torch.no_grad():
        test_bar = tqdm(test_loader)
        for data in test_bar:
            x_test, y_test = data
            y_test_pred = model(x_test)
            test_loss = loss_function(y_test_pred, y_test.reshape(-1, 1))

    if test_loss < config.best_loss:
        config.best_loss = test_loss
        torch.save(model.state_dict(), save_path)

print('Finished Training')

上述代码理解相对简单,就不多赘述了,有同学会好奇 train_bar = tqdm(train_loader) 这句代码是做什么的,对于模型训练来将不是必须的,他只是用来形成进度条的,帮助用户了解当前模型的训练进度,效果如下:

train epoch[1/10] loss:0.017: 100%|██████████████████████████████████████████████████| 141/141 [00:01<00:00, 83.10it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 360.01it/s]
train epoch[2/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:02<00:00, 62.06it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 404.49it/s]
train epoch[3/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:01<00:00, 80.92it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 276.92it/s]
train epoch[4/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:01<00:00, 84.16it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 375.53it/s]
train epoch[5/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:01<00:00, 77.95it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 399.98it/s]
train epoch[6/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:01<00:00, 81.74it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 404.50it/s]
train epoch[7/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:01<00:00, 72.54it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 370.81it/s]
train epoch[8/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:02<00:00, 68.67it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 237.74it/s]
train epoch[9/10] loss:0.014: 100%|██████████████████████████████████████████████████| 141/141 [00:02<00:00, 64.72it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 303.73it/s]
train epoch[10/10] loss:0.014: 100%|█████████████████████████████████████████████████| 141/141 [00:02<00:00, 66.28it/s]
100%|█████████████████████████████████████████████████████████████████████████████████| 36/36 [00:00<00:00, 292.70it/s]
Finished Training

对于模型损失的指标,用户可以手动保存,就是将每轮的损失值保存到列表中或者其它处理,也可以掉包来实现,在pytorch中有个 meter 库可以实现这个目的,有兴趣的同学可以参考这篇文章 pytorch中meter.ClassErrorMeter()使用方法

十、可视化结果

为了查看模型的训练效果,我们采用可视化的方式来对比真实值和预测值的差距,对于绘图,我们采用了经典的可视化库 matplotlib ,可视化代码如下:

# 绘制结果
plot_size = 200 # 绘制前200个样本
plt.figure(figsize=(12, 8))
plt.plot(scaler.inverse_transform((model(x_train_tensor).detach().numpy()[: plot_size]).reshape(-1, 1)), "b")
plt.plot(scaler.inverse_transform(y_train_tensor.detach().numpy().reshape(-1, 1)[: plot_size]), "r")
plt.legend()
plt.show()

y_test_pred = model(x_test_tensor)
plt.figure(figsize=(12, 8))
plt.plot(scaler.inverse_transform(y_test_pred.detach().numpy()[: plot_size]), "b")
plt.plot(scaler.inverse_transform(y_test_tensor.detach().numpy().reshape(-1, 1)[: plot_size]), "r")
plt.legend()
plt.show()

解释下上述代码,首先定义了 plot_size ,这个变量是用来绘制样本数的,因为我们的数据集中存在几千个样本,如果全部绘制,会导致曲线过于拥挤,为了更好的观察拟合效果,所以只绘制其中一小部分。

还有一处需要说明的是 scaler.inverse_transform() ,由于我们的数据集在训练之前进行了归一化,所以在绘制曲线时需要将预测结果进行反归一化,恢复到原来的量纲区间。

训练集效果:

在这里插入图片描述

测试集效果:

在这里插入图片描述

完整源码

注意🚨🚨🚨:由于是针对于新手小白入门的系列专栏,所以代码并没有采用开发大型项目的方式,而是python单文件实现,这样能够帮助新人一键复制调试运行,不需要理解复杂的项目构造,另外一点就是由于是帮助新人理解时间序列预测基本过程,所以源码仅包含了时间序列预测的基本框架结构,有些地方实现略有简陋,有能力的小伙伴可以根据自己的能力在此基础上进行修改,例如尝试更深层次的模型结构,尝试更多的参数,以及进行分文件编写(模型训练、模型测试、定义模型、绘制图像)达到项目开发流程。

再说明一点,本专栏的模型结构相对简单,相对于原论文中代码只是一个阉割版,这只是为了帮助新手小白有个初步的了解,明白每个模块是干嘛的,以及他们的输入输出到底是说明,每个维度代表的意义,所以没有采用复杂实现方式,尽可能的保留核心部分。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import tushare as ts
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from torch.utils.data import TensorDataset
from tqdm import tqdm

class Config():
    data_path = '../data/wind_dataset.csv'
    timestep = 20  # 时间步长,就是利用多少时间窗口
    batch_size = 32  # 批次大小
    feature_size = 8  # 每个步长对应的特征数量,这里只使用1维,每天的风速
    hidden_size = 256  # 隐层大小
    output_size = 2  # 由于是多输出任务,最终输出层大小为2,预测未来2天风速
    num_layers = 2  # lstm的层数
    epochs = 10 # 迭代轮数
    best_loss = 0 # 记录损失
    learning_rate = 0.0003 # 学习率
    model_name = 'lstm' # 模型名称
    save_path = './{}.pth'.format(model_name) # 最优模型保存路径

config = Config()

# 1.加载时间序列数据
df = pd.read_csv(config.data_path, index_col = 0)


# 2.将数据进行标准化
scaler = MinMaxScaler()
scaler_model = MinMaxScaler()
data = scaler_model.fit_transform(np.array(df))
scaler.fit_transform(np.array(df['WIND']).reshape(-1, 1))

# 形成训练数据,例如12345789 12345-67 23456-78
def split_data(data, timestep, feature_size, output_size):
    dataX = []  # 保存X
    dataY = []  # 保存Y

    # 将整个窗口的数据保存到X中,将未来一天保存到Y中
    for index in range(len(data) - timestep - 1):
        dataX.append(data[index: index + timestep])
        dataY.append(data[index + timestep: index + timestep + output_size][:, 0].tolist())
        

    dataX = np.array(dataX)
    dataY = np.array(dataY)
    
    # 获取训练集大小
    train_size = int(np.round(0.8 * dataX.shape[0]))

    # 划分训练集、测试集
    x_train = dataX[: train_size, :].reshape(-1, timestep, feature_size)
    y_train = dataY[: train_size].reshape(-1, output_size)
    
    x_test = dataX[train_size:, :].reshape(-1, timestep, feature_size)
    y_test = dataY[train_size:].reshape(-1, output_size)

    return [x_train, y_train, x_test, y_test]

# 3.获取训练数据   x_train: 170000,30,1   y_train:170000,7,1
x_train, y_train, x_test, y_test = split_data(data, config.timestep, config.feature_size)

# 4.将数据转为tensor
x_train_tensor = torch.from_numpy(x_train).to(torch.float32)
y_train_tensor = torch.from_numpy(y_train).to(torch.float32)
x_test_tensor = torch.from_numpy(x_test).to(torch.float32)
y_test_tensor = torch.from_numpy(y_test).to(torch.float32)

# 5.形成训练数据集
train_data = TensorDataset(x_train_tensor, y_train_tensor)
test_data = TensorDataset(x_test_tensor, y_test_tensor)

# 6.将数据加载成迭代器
train_loader = torch.utils.data.DataLoader(train_data,
                                           config.batch_size,
                                           False)

test_loader = torch.utils.data.DataLoader(test_data,
                                          config.batch_size,
                                          False)

# 7.定义LSTM网络
class LSTM(nn.Module):
    def __init__(self, feature_size, hidden_size, num_layers, output_size):
        super(LSTM, self).__init__()
        self.hidden_size = hidden_size  # 隐层大小
        self.num_layers = num_layers  # lstm层数
        # feature_size为特征维度,就是每个时间点对应的特征数量,这里为1
        self.lstm = nn.LSTM(feature_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden=None):
        batch_size = x.shape[0] # 获取批次大小
        
        # 初始化隐层状态
        if hidden is None:
            h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
            c_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
        else:
            h_0, c_0 = hidden
            
        # LSTM运算
        output, (h_0, c_0) = self.lstm(x, (h_0, c_0))
        
        # 全连接层
        output = self.fc(output)  # 形状为batch_size * timestep, 1
        
        # 我们只需要返回最后一个时间片的数据即可
        return output[:, -1, :]

model = LSTM(config.feature_size, config.hidden_size, config.num_layers, config.output_size)  # 定义LSTM网络
loss_function = nn.MSELoss()  # 定义损失函数
optimizer = torch.optim.AdamW(model.parameters(), lr=config.learning_rate)  # 定义优化器

# 8.模型训练
for epoch in range(config.epochs):
    model.train()
    running_loss = 0
    train_bar = tqdm(train_loader)  # 形成进度条
    for data in train_bar:
        x_train, y_train = data  # 解包迭代器中的X和Y
        optimizer.zero_grad()
        y_train_pred = model(x_train)
        loss = loss_function(y_train_pred, y_train.reshape(-1, 1))
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                 config.epochs,
                                                                 loss)

    # 模型验证
    model.eval()
    test_loss = 0
    with torch.no_grad():
        test_bar = tqdm(test_loader)
        for data in test_bar:
            x_test, y_test = data
            y_test_pred = model(x_test)
            test_loss = loss_function(y_test_pred, y_test.reshape(-1, 1))

    if test_loss < config.best_loss:
        config.best_loss = test_loss
        torch.save(model.state_dict(), save_path)

print('Finished Training')

# 9.绘制结果
plot_size = 200
plt.figure(figsize=(12, 8))
plt.plot(scaler.inverse_transform((model(x_train_tensor).detach().numpy()[: plot_size, 0]).reshape(-1, 1)), "b")
plt.plot(scaler.inverse_transform(y_train_tensor.detach().numpy().reshape(-1, 1)[: plot_size]), "r")
plt.legend()
plt.show()

y_test_pred = model(x_test_tensor)
plt.figure(figsize=(12, 8))
plt.plot(scaler.inverse_transform(y_test_pred.detach().numpy()[: plot_size, 0].reshape(-1, 1)), "b")
plt.plot(scaler.inverse_transform(y_test_tensor.detach().numpy().reshape(-1, 1)[: plot_size]), "r")
plt.legend()
plt.show()

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(2)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2023年2月25日 下午5:35
下一篇 2023年2月25日 下午5:36

相关推荐