目录
一、基于PyTorch搭建TCN(时间卷积网络)模型实现风速时间序列预测
一、基于PyTorch搭建TCN(时间卷积网络)模型实现风速时间序列预测
TCN(Temporal Convolutional Network)
是一种基于卷积神经网络的时间序列模型。它通过一系列的一维卷积层对输入序列进行特征提取,然后将提取到的特征输入到一个全连接层中进行预测。
TCN的主要特点是可以处理变长的时间序列数据,同时具有比传统循环神经网络(RNN
)更快的训练速度和更好的性能。这是因为 TCN
使用了一维卷积层来提取特征,而卷积层是一种高效的操作,可以利用 GPU
加速训练过程。
在 TCN
模型中,一维卷积层的核大小通常比较小,比如 3或5
,这是因为这样的卷积核可以在不丢失太多信息的情况下捕捉到序列中的局部模式。此外,TCN
还使用了一种叫做 dilated convolution
的技术来增加卷积层的感受野,这可以使卷积层捕捉到更长的序列依赖关系。
在 TCN
模型中,通常会使用一种叫做残差连接(Residual Connection
)的技术来缓解梯度消失问题。残差连接可以使网络更深,同时也可以使梯度更好地传播,从而提高模型的性能。
总之,TCN是一种非常有前途的时间序列模型,已经在多个领域得到了广泛应用,比如语音识别、自然语言处理、视频分析等。
本项目依旧是使用过去20天的数据来预测未来1天的数据,且每天的特征我们是用到了所有变量 ,也就是多变量预测。
二、配置类
下面是本项目需要使用的参数以及相关变量,为了方便我们将所有参数封装到一个类中,也可以使用 argparse
参数解析方式。
为了说明数据各个阶段的维度变化,特此定义了如下变量大小,小伙伴需要记住下面变量的值一遍理解下文说明各个阶段的维度大小。
class Config():
data_path = '../data/wind_dataset.csv'
timestep = 20 # 时间步长,就是利用多少时间窗口
batch_size = 32 # 批次大小
feature_size = 8 # 每个步长对应的特征数量,这里只使用1维,每天的风速
num_channels = [32, 64, 128, 256] # 卷积通道数
kernel_size = 3 # 卷积核大小
dropout = 0.2 # 丢弃率
output_size = 1 # 由于是单输出任务,最终输出层大小为1,预测未来1天风速
epochs = 10 # 迭代轮数
best_loss = 0 # 记录损失
learning_rate = 0.0003 # 学习率
model_name = 'tcn' # 模型名称
save_path = './{}.pth'.format(model_name) # 最优模型保存路径
config = Config()
三、时序数据集的制作
略
四、数据归一化
略
五、数据集加载器
略
六、搭建TCN(时间卷积网络)
本项目使用的是 TCN
时间卷积网络,这个模块PyTorch中没有集成,所以我们需要利用现有的卷积模型来自己复现,对于TCN来说其实我们需要解决的三个问题就是:因果卷积、膨胀卷积、残差连接
。
因果卷积(Causal Convolution
)是TCN中常用的一种卷积方式。它是一种限制卷积核只能看到当前时刻及之前的数据的卷积方式,因此被称为因果卷积。
在时间序列数据中,当前时刻的数据是由过去的数据决定的,因此在训练模型时,我们只能利用当前时刻之前的数据来预测当前时刻的值。因此,使用因果卷积可以更好地模拟真实的数据流动,从而更好地进行预测。
在TCN中,因果卷积通常通过在卷积层中添加 padding
实现。具体地,我们在序列的前面添加了一些 0
,使得卷积核只能看到当前时刻及之前的数据,而看不到当前时刻之后的数据。这样就可以模拟真实的数据流动,从而提高模型的性能。
需要注意的是,因果卷积的计算量较大,因为需要在卷积层中添加 padding
。因此,当我们构建TCN模型时,需要考虑计算量和模型性能之间的平衡。
膨胀卷积(Dilated Convolution
)是TCN中的一种卷积方式,它可以增加卷积层的感受野,从而更好地捕捉时间序列数据中的长期依赖关系。
膨胀卷积的基本思想是在卷积核中添加一些间隔,使得卷积核可以跳过一些元素进行卷积,从而增加卷积层的感受野。在TCN中,通常使用一种叫做指数膨胀(Exponential Dilation
)的方式来生成膨胀卷积核,即通过指数级别增加间隔来生成卷积核。例如,一个膨胀系数为2的卷积核可以看作是一个间隔为 2
的普通卷积核和一个间隔为1的普通卷积核的加权和。
膨胀卷积可以在不增加模型参数和计算量的情况下增加卷积层的感受野,从而更好地捕捉时间序列数据中的长期依赖关系。在TCN中,通常会使用多个膨胀系数不同的卷积层来构建模型,从而更好地捕捉不同时间尺度的特征。
需要注意的是,膨胀卷积也会增加卷积层的参数量和计算量,因此需要在计算量和模型性能之间进行平衡。同时,在使用膨胀卷积时,我们还需要注意卷积核的大小和膨胀系数的选择,以获得最佳的性能。
定义TCN模型结构代码如下:
# 定义TCN模型
class TCN(nn.Module):
def __init__(self, input_size, output_size, num_channels, kernel_size, dropout):
super(TCN, self).__init__()
self.input_size = input_size
self.output_size = output_size
self.num_channels = num_channels
self.kernel_size = kernel_size
self.dropout = dropout
# 定义卷积层和dropout层
self.layers = []
self.layers.append(nn.Conv1d(input_size, num_channels[0], kernel_size))
for i in range(1, len(num_channels)):
self.layers.append(nn.Conv1d(num_channels[i-1], num_channels[i], kernel_size))
self.dropout = nn.Dropout(dropout)
# 定义全连接层
self.fc = nn.Linear(num_channels[-1], output_size)
def forward(self, x):
# 将数据维度从 (batch_size, seq_len, input_size) 变为 (batch_size, input_size, seq_len)
x = x.permute(0, 2, 1)
# 通过卷积层和dropout层进行特征提取
for layer in self.layers:
x = layer(x)
x = nn.functional.relu(x)
x = self.dropout(x)
# 将卷积层的输出求平均并输入全连接层得到最终输出
x = x.mean(dim=2)
x = self.fc(x)
return x
TCN主要分为以下几个部分:
- 多个卷积层:TCN模型中通常会使用多个卷积层来捕捉不同时间尺度的特征,每个卷积层都是一组因果卷积和膨胀卷积的组合。
- 因果卷积:因果卷积保证了模型不会利用未来的信息来预测当前的输出,从而更好地捕捉时间序列数据中的依赖关系。
- 膨胀卷积:膨胀卷积增加了卷积层的感受野,从而更好地捕捉时间序列数据中的长期依赖关系。
- 残差连接:残差连接可以减缓梯度消失的问题,从而更好地训练深层神经网络。
- 池化层:池化层可以减小特征图的大小,从而降低计算量。
注:这里为了新手容易理解没有加入残差结构,感兴趣的小伙伴可以自己将该结构嵌入在里面,其实很简单就是把经过卷积层、池化层、激活层的输出与输入相加,但是在相加之前要将原始输入通过一个一维卷积层,使得这两部分的形状一样。
之前看过作者源码的小伙伴可能会问:上面实现的TCN与原作者给出的不太一样,这是因为我们在学习时更应该注重TCN提出的思想(因果卷积和膨胀卷积),所以在复现时我们只是把他的核心思想保留,一些小细节需要自己去完善。
七、定义模型、损失函数、优化器
略
八、模型训练
略
九、可视化结果
为了查看模型的训练效果,我们采用可视化的方式来对比真实值和预测值的差距,对于绘图,我们采用了经典的可视化库 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()
,由于我们的数据集在训练之前进行了归一化,所以在绘制曲线时需要将预测结果进行反归一化,恢复到原来的量纲区间。
下面两图为训练集和测试集的效果图,发现预测值为一条直线,所以效果不是很好,有可能是过拟合了,可以调整一下参数或者使用 Early Stopping
来保留最好的模型,提前停止训练。
注意:这里也提到一点如果发现效果不好,也不要灰心,这个有多方面因素导致,例如模型天生弊病(LSTM不适合处理图像数据)、学习率这类超参数(尝试调整超参数)、训练偏移(梯度问题,多训练几次解决这个问题)、数据集脏(模型难以拟合)以及模型小(数据集涵盖信息多,而模型小参数量小,不足以支撑学习这个大的数据集,尝试模型堆叠,增加参数)的问题,所以如果效果不好,不要立刻否定模型,要多找找原因,如果其它原因都尝试了,再去调整模型的问题,尝试改进模型结构。
训练集效果:
测试集效果:
完整源码
注意🚨🚨🚨:由于是针对于新手小白入门的系列专栏,所以代码并没有采用开发大型项目的方式,而是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维,每天的风速
num_channels = [32, 64, 128, 256] # 卷积通道数
kernel_size = 3 # 卷积核大小
dropout = 0.2 # 丢弃率
output_size = 1 # 由于是单输出任务,最终输出层大小为1,预测未来1天风速
epochs = 10 # 迭代轮数
best_loss = 0 # 记录损失
learning_rate = 0.0003 # 学习率
model_name = 'tcn' # 模型名称
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 12-3456789
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]
# 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)
# 定义TCN模型
class TCN(nn.Module):
def __init__(self, input_size, output_size, num_channels, kernel_size, dropout):
super(TCN, self).__init__()
self.input_size = input_size
self.output_size = output_size
self.num_channels = num_channels
self.kernel_size = kernel_size
self.dropout = dropout
# 定义卷积层和dropout层
self.layers = []
self.layers.append(nn.Conv1d(input_size, num_channels[0], kernel_size))
for i in range(1, len(num_channels)):
self.layers.append(nn.Conv1d(num_channels[i-1], num_channels[i], kernel_size))
self.dropout = nn.Dropout(dropout)
# 定义全连接层
self.fc = nn.Linear(num_channels[-1], output_size)
def forward(self, x):
# 将数据维度从 (batch_size, seq_len, input_size) 变为 (batch_size, input_size, seq_len)
x = x.permute(0, 2, 1)
# 通过卷积层和dropout层进行特征提取
for layer in self.layers:
x = layer(x)
x = nn.functional.relu(x)
x = self.dropout(x)
# 将卷积层的输出求平均并输入全连接层得到最终输出
x = x.mean(dim=2)
x = self.fc(x)
return x
model = TCN(input_size=config.feature_size, output_size=config.output_size, num_channels=config.num_channels, kernel_size=config.kernel_size, dropout=config.dropout) # 定义TCN网络
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]).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()