金融时间序列预测lstm及informer实战汇总

一、LSTM

1、数据加载

本篇均使用kaggle自带数据集GOOG.csv(从2016年6月到2021年6月五年来每日的股票数据),环境使用kaggle在线notebook,这样自带各模块(比如pytorch),无须手动安装,可以直接import来使用。

google_data=pd.read_csv('/kaggle/input/google-stock-prediction/GOOG.csv', parse_dates=['date'])
google_data.head()

2、数据预处理

用30天数据预测后面1天的数据,只选用收盘价一个特征做预测,预处理中进行归一化,并划分训练集(80%)和测试集(20%)。

data = google_data[['close']]  # Using only closing prices
timesteps = 30  # Number of past days used for prediction
scaler = MinMaxScaler(feature_range=(0,1))
data_scaled = scaler.fit_transform(data.values) #归一化
train_size = int(len(data_scaled) * 0.8)
train_data = data_scaled[:train_size]
test_data = data_scaled[train_size - timesteps:]  
#为了使train和test连续,此处要从train_size - timesteps开始
def create_sequences(data, timesteps):
    X, y = [], []
    for i in range(timesteps, len(data)):
        X.append(data[i-timesteps:i, 0])
        y.append(data[i, 0])
    return np.array(X), np.array(y)
X_train, y_train = create_sequences(train_data, timesteps)
X_test, y_test = create_sequences(test_data, timesteps)

此时,X_train.shape为(976,30),y_test.shape为(252,)。

接着,需要将numpy array转换为张量,再变换为三维方便后续作为lstm模型的输入。

X_train, y_train = torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)
X_test, y_test = torch.tensor(X_test, dtype=torch.float32), torch.tensor(y_test, dtype=torch.float32)

# Reshape data for LSTM input
X_train = X_train.unsqueeze(-1)
X_test = X_test.unsqueeze(-1)
y_train = y_train.unsqueeze(-1)
y_test = y_test.unsqueeze(-1)

3、模型构建

构建lstm模型,本次是序列到标签(sequence-to-label) 的任务(用整个序列预测一个最终结果),则应该使用最后一个时间步的输出,代码中由out[:, -1, :] 体现。

class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers):
        super(LSTMModel, self).__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, 1)
    
    def forward(self, x):
        out, _ = self.lstm(x)
        out = self.fc(out[:, -1, :])  # Take the last timestep output
        return out

model1 = LSTMModel(input_size=1, hidden_size=50, num_layers=2)
model2 = LSTMModel(input_size=1, hidden_size=100, num_layers=3)

4、模型训练

先定义loss和optimizer,再定义训练函数,迭代50次。

# Loss and Optimizer
criterion = nn.MSELoss()
optimizer1 = optim.Adam(model1.parameters(), lr=0.001)
optimizer2 = optim.Adam(model2.parameters(), lr=0.001)

# Training loop with loss and MAE tracking
def train_model(model, optimizer, X_train, y_train, epochs=50, batch_size=32):
    dataset = TensorDataset(X_train, y_train)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    losses = []
    maes = []
    
    for epoch in range(epochs):
        for X_batch, y_batch in dataloader:
            #print(X_batch.shape,y_batch.shape)
            optimizer.zero_grad()
            y_pred = model(X_batch)
            loss = criterion(y_pred, y_batch)
            loss.backward()
            optimizer.step()
        print(X_batch.shape,y_batch.shape, y_pred.shape)
        losses.append(loss.item())
        maes.append(mean_absolute_error(y_batch.detach().numpy(), y_pred.detach().numpy()))
        print(f"Epoch {epoch+1}/{epochs}, Loss: {loss.item()}, MAE: {maes[-1]}")
    return losses, maes

# Train both models and capture loss & MAE history
losses1, maes1 = train_model(model1, optimizer1, X_train, y_train)
losses2, maes2 = train_model(model2, optimizer2, X_train, y_train)

通过TensorDataset和DataLoader,将train数据变为一个个batch大小,每次输入一个batch

本项目数据是滑动窗口采样得到的多个独立样本(从长序列中截取了多个不重叠的 30+1 窗口),且每个窗口内部的时间顺序是完整的,因此可以使用 shuffle=True 来打乱不同窗口的顺序

训练数据共有976个,每32个作为一个batch后,最后会剩余16个,因此dataloader的for循环结束后,最后一个X_batch.shape为(16,30,1)。

5、测试集上进行模型预测

def predict(model, X):
    with torch.no_grad():
        return model(X).numpy()

y_pred1 = predict(model1, X_test)
y_pred2 = predict(model2, X_test)

测试集不再分batch进行,y_pred1.shape为(252,1)和y_test.shape一致

画图展示之前,要进行反归一化,代码如下:

y_test_actual = scaler.inverse_transform(y_test.numpy())
y_pred1_actual = scaler.inverse_transform(y_pred1)
y_pred2_actual = scaler.inverse_transform(y_pred2)

6、画图看效果

plt.figure(figsize=(12,6))
plt.plot(y_test_actual, label='Actual Prices')
plt.plot(y_pred1_actual, label='Predicted Model 1')
plt.plot(y_pred2_actual, label='Predicted Model 2')
plt.legend()
plt.title('Google Stock Price Prediction')
plt.xlabel('Time')
plt.ylabel('Stock Price')
plt.show()

注:

(1)若要预测未来多个值(10天),可以递归法进行预测,效果好于训练时直接用30天数据预测10天数据

每次预测后面一个值,然后剔除最老的一个值,在末尾加上新的预测值

# Forecast future cases
# Generate predictions for the next 60 days based on the last 30 days in the dataset
X_next = data_scaled[-timesteps:]
X_next = X_next.reshape(1, timesteps, 1)
X_next=torch.tensor(X_next,dtype=torch.float32)
predictions = []

for _ in range(10):  # Predict for the next 10 days
    pred = predict(model2, X_next)
    predictions.append(pred[0][0])
    # Append the new prediction to X_test and remove the oldest value
    X_next = np.append(X_next[:, 1:, :], np.expand_dims([[pred[0][0]]], axis=2), axis=1)
    X_next=torch.tensor(X_next,dtype=torch.float32)

# Inverse scale predictions to get actual values
predicted_cases = scaler.inverse_transform(np.array(predictions).reshape(-1, 1))

然后画出测试集以及未来的预测结果来看效果

# Plot actual vs. predicted cases
plt.figure(figsize=(12, 6))
plt.plot(google_data['date'][-252:], y_test_actual, label="Actual Cases")
plt.plot(google_data['date'][-252:], y_pred2_actual, label='Predicted Model 2')
plt.plot(pd.date_range(google_data['date'].iloc[-1] + pd.Timedelta(days=1), periods=10), predicted_cases, color="red", label="LSTM Predicted Cases")
plt.xlabel("Date")
plt.ylabel("Stock Price")
plt.title("Google Stock Price Prediction")
plt.legend()
plt.show()

(2)本项目使用纯numpy函数的方式手动创建滑动窗口,生成数据集,对于大型项目,也可以用继承dataset类的方法来生成lstm数据

import torch
from torch.utils.data import Dataset, DataLoader

class LSTMDataset(Dataset):
    def __init__(self, data, window_size):
        self.data=data
        self.window_size=window_size
        self.n_samples=len(data)-window_size

    def __len__(self):
        return self.n_samples

    def __getitem__(self, index):
        X=self.data[index:index+self.window_size]
        y=self.data[index+self.window_size]
        return torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)

(3)如果需要多步预测任务,可以尝试seq2seq的lstm,Seq2Seq LSTM通过上下文向量在编码器和解码器之间传递信息,可以更好地捕捉长期依赖关系

# 定义Seq2Seq模型
class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers):
        super(Encoder, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        
    def forward(self, x):
        # x shape: (batch_size, seq_len, input_size)
        outputs, (hidden, cell) = self.lstm(x)
        return hidden, cell

class Decoder(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(Decoder, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)
        
    def forward(self, x, hidden, cell):
        # x shape: (batch_size, 1, input_size)
        output, (hidden, cell) = self.lstm(x, (hidden, cell))
        prediction = self.fc(output)
        return prediction, hidden, cell

class Seq2Seq(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size, output_window):
        super(Seq2Seq, self).__init__()
        self.encoder = Encoder(input_size, hidden_size, num_layers)
        self.decoder = Decoder(input_size, hidden_size, num_layers, output_size)
        self.output_window = output_window
        self.output_size = output_size
        
    def forward(self, src):
        # src shape: (batch_size, input_seq_len, input_size)
        batch_size = src.shape[0]
        
        # 编码器处理输入序列
        hidden, cell = self.encoder(src)
        
        # 准备解码器的第一个输入(使用输入序列的最后一个元素)
        decoder_input = src[:, -1:, :]
        
        # 初始化输出张量
        outputs = torch.zeros(batch_size, self.output_window, self.output_size)
        
        # 解码器逐步生成输出序列
        for t in range(self.output_window):
            decoder_output, hidden, cell = self.decoder(decoder_input, hidden, cell)
            outputs[:, t:t+1, :] = decoder_output
            # 使用当前预测作为下一个输入(teacher forcing可以在训练时使用)
            decoder_input = decoder_output
            
        return outputs

二、Informer

本项目采用修改Informer2020原论文的方式,把informer应用到金融股票时间序列预测上,在kaggle在线notebook运行,只需要upload从github上下载的原论文代码包,然后只需要修改参数适配自己的数据和模型即可。

1、导入必要的库和代码包

import argparse 
import os
import torch
import sys
sys.path.append("/kaggle/input/informer2020/pytorch/default/1/Informer2020-main")
from exp.exp_informer import Exp_Informer as Exp #执行训练流程的板块

2、导入数据集(先从官方数据集入手,跑通代码)

import pandas as pd
df_ETT_h = pd.read_csv(r'/kaggle/input/informer-data/ETT/ETTh1.csv')
df_ETT_h

3、添加参数

此处要修改模型保存路径,kaggle notebook中指定的是/kaggle/working/

parser = argparse.ArgumentParser(description='[Informer] Long Sequences Forecasting')
#这里有三个模型可以选择,默认是Informer模型
parser.add_argument('--model', type=str, required=True, default='informer',help='model of experiment, options: [informer, informerstack, informerlight(TBD)]')
#训练出来的模型保存路径
parser.add_argument('--checkpoints', type=str, default='/kaggle/working/', help='location of model checkpoints')

下面这几个数据相关的参数基本都需要修改:

#想要用官方定义的方法还是你自己的数据集进行定义数据加载器,如果是自己的数据集就输入custom
parser.add_argument('--data', type=str, required=True, default='ETTh1', help='data')
#数据集文件的路径,不要到具体的文件,到目录级别即可
parser.add_argument('--root_path', type=str, default='/kaggle/input/informer-data/ETT/', help='root path of the data file')
#数据集文件的名称
parser.add_argument('--data_path', type=str, default='ETTh1.csv', help='data file')    
#特征有三个选项M,MS,S。分别是多元预测多元,多元预测单元,单元预测单元
parser.add_argument('--features', type=str, default='M', help='forecasting task, options:[M, S, MS]; M:multivariate predict multivariate, S:univariate predict univariate, MS:multivariate predict univariate')
#数据集中想要预测那一列数据名称,指定标签
parser.add_argument('--target', type=str, default='OT', help='target feature in S or MS task')
#时间的间隔,数据集每一条数据之间的时间间隔,默认为小时,可以自己设定秒,年,月,日,周等
parser.add_argument('--freq', type=str, default='h', help='freq for time features encoding, options:[s:secondly, t:minutely, h:hourly, d:daily, b:business days, w:weekly, m:monthly], you can also use more detailed freq like 15min or 3h')

根据下述字典决定data参数取值,如果是自己的数据集要改成default='custom'

data_dict = {

            'ETTh1':Dataset_ETT_hour,

            'ETTh2':Dataset_ETT_hour,

            'ETTm1':Dataset_ETT_minute,

            'ETTm2':Dataset_ETT_minute,

            'WTH':Dataset_Custom,

            'ECL':Dataset_Custom,

            'Solar':Dataset_Custom,

            'custom':Dataset_Custom,

        }

以下为与模型相关的参数:

#用过去的多少条数据来预测未来的数据,也就是encoder输入序列的长度
parser.add_argument('--seq_len', type=int, default=96, help='input sequence length of Informer encoder')
# Decoder中输入的没有掩码部分序列长度
parser.add_argument('--label_len', type=int, default=48, help='start token length of Informer decoder')
#预测未来多少个时间点的数据,Decoder输入中用0掩码的序列长度
parser.add_argument('--pred_len', type=int, default=24, help='prediction sequence length')
# Informer decoder input: concat[start token series(label_len), zero padding series(pred_len)]
#输入数据的特征数量,要减去时间的那一列,encoder和decoder是一样的
parser.add_argument('--enc_in', type=int, default=7, help='encoder input size')
parser.add_argument('--dec_in', type=int, default=7, help='decoder input size')
#输出数据的维度
#如果features填写的是M那么和上面就一样,是数据列数,如果填写的MS那么这里要输入1因为你的输出只有一列数据。
parser.add_argument('--c_out', type=int, default=7, help='output size')
#编码器中使用的注意力类型,默认为"prob"论文的主要改进点,提出的注意力机制
parser.add_argument('--attn', type=str, default='prob', help='attention used in encoder, options:[prob, full]')
#设置注意力机制中的d_model,默认值为512。可以根据需要调整该参数的数值来改变模型的维度
parser.add_argument('--d_model', type=int, default=512, help='dimension of model')
#设置模型中的注意力头数,默认值为8
parser.add_argument('--n_heads', type=int, default=8, help='num of heads')
#设置编码器的层数,默认为2层
parser.add_argument('--e_layers', type=int, default=2, help='num of encoder layers')
#设置解码器的层数,默认为1层
parser.add_argument('--d_layers', type=int, default=1, help='num of decoder layers')
#设置堆叠编码器的层数
parser.add_argument('--s_layers', type=str, default='3,2,1', help='num of stack encoder layers')
#模型中全连接网络(FCN)的维度,默认值为2048
parser.add_argument('--d_ff', type=int, default=2048, help='dimension of fcn')
#是否在编码器中使用蒸馏操作,默认为True也是论文中比较重要的一个改进
parser.add_argument('--distil', action='store_false', help='whether to use distilling in encoder, using this argument means not using distilling', default=True)
#激活函数,在不同的位置使用了不同的激活函数,我们在代码中看具体激活函数的使用
parser.add_argument('--activation', type=str, default='gelu',help='activation')

以下为gpu相关参数设置,如果用kaggle可以复制下面的代码,不会报错:

#是否使用GPU训练,根据自身来选择
parser.add_argument('--use_gpu', type=bool, default=torch.cuda.is_available(), help='auto-detect GPU')
#GPU的编号
parser.add_argument('--gpu', type=int, default=0, help='gpu')
#是否使用多个GPU训练。
parser.add_argument('--use_multi_gpu', action='store_false', help='disable multi-GPU', default=False)
parser.add_argument('--devices', type=str, default='0',help='single GPU')

以下为一般不需要改动的参数:

#是否进行预测
parser.add_argument('--do_predict', action='store_true', help='whether to predict unseen future data')
#自注意力中的因子,默认值为5
parser.add_argument('--factor', type=int, default=5, help='probsparse attn factor')
#填充类型,默认值为0,如果不够数据就填写0
parser.add_argument('--padding', type=int, default=0, help='padding type')
#丢弃的概率,防止过拟合
parser.add_argument('--dropout', type=float, default=0.05, help='dropout')
#时间特征的编码方式,默认为"timeF"
parser.add_argument('--embed', type=str, default='timeF', help='time features encoding, options:[timeF, fixed, learned]')
#是否在编码器中输出注意力,默认为False
parser.add_argument('--output_attention', action='store_true', help='whether to output attention in ecoder')
#在生成式解码器中是否使用混合注意力,默认为True
parser.add_argument('--mix', action='store_false', help='use mix attention in generative decoder', default=True)
#从数据文件中选择特定的列作为输入特征,不常用
parser.add_argument('--cols', type=str, nargs='+', help='certain cols from the data files as the input features')
#线程数量。windows最好设置成0避免报线程错误,linux系统可随便设置
parser.add_argument('--num_workers', type=int, default=0, help='data loader num workers')
#实验运行的次数,默认为2
parser.add_argument('--itr', type=int, default=2, help='experiments times')
#训练的次数
parser.add_argument('--train_epochs', type=int, default=6, help='train epochs')
#一次往模型内输入多少数据
parser.add_argument('--batch_size', type=int, default=32, help='batch size of train input data')
#早停机制,如果损失多少个epochs没有改变就停止训练
parser.add_argument('--patience', type=int, default=3, help='early stopping patience')
#学习率
parser.add_argument('--learning_rate', type=float, default=0.0001, help='optimizer learning rate')
#实验描述,默认为"test"
parser.add_argument('--des', type=str, default='test',help='exp description')
#损失函数,默认为"mse"
parser.add_argument('--loss', type=str, default='mse',help='loss function')
#学习率的调整方式,默认为"type1"
parser.add_argument('--lradj', type=str, default='type1',help='adjust learning rate')
#混合精度训练
parser.add_argument('--use_amp', action='store_true', help='use automatic mixed precision training', default=False)
#是否将归一化后的数据转换为原始值
parser.add_argument('--inverse', action='store_true', help='inverse output data', default=False)

4、赋予参数具体值

args = parser.parse_args(args=["--model", "informer", 
                               "--data", "ETTh1",
                               "--attn", 'prob',
                               "--freq", "h"])
args.s_layers = [int(s_l) for s_l in args.s_layers.replace(' ','').split(',')]
args.detail_freq = args.freq
args.freq = args.freq[-1:]

print('Args in experiment:')
print(args)

5、实例化、训练

# 设置settings
setting = '{}_{}_ft{}_sl{}_ll{}_pl{}_dm{}_nh{}_el{}_dl{}_df{}_at{}_fc{}_eb{}_dt{}_{}'.format(args.model
                                                                                             , args.data, args.features
                                                                                             , args.seq_len, args.label_len
                                                                                             , args.pred_len
                                                                                             , args.d_model, args.n_heads
                                                                                             , args.e_layers
                                                                                             , args.d_layers, args.d_ff, args.attn
                                                                                             , args.factor, args.embed
                                                                                             , args.distil, args.des)

# set experiments实例化上面的实验
exp = Exp(args)

# train
print('>>>>>>>start training : {}>>>>>>>>>>>>>>>>>>>>>>>>>>'.format(setting))
exp.train(setting)

# test
print('>>>>>>>testing : {}<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<'.format(setting))
exp.test(setting)

torch.cuda.empty_cache()

6、预测

exp.predict(setting, True)

打印看一下prediction的形状

# the prediction will be saved in ./results/{setting}/real_prediction.npy
import numpy as np

prediction = np.load('./results/'+setting+'/real_prediction.npy')

print(prediction.shape)

取第一个批次所有时间步的最后一个特征值,画图展示

import matplotlib.pyplot as plt

plt.figure()
plt.plot(prediction[0,:,-1])
plt.show()

输出测试集的预测结果,输出形状

preds = np.load('./results/'+setting+'/pred.npy')
trues = np.load('./results/'+setting+'/true.npy')

# [samples, pred_len, dimensions]
preds.shape, trues.shape

画图展示测试集的预测结果和真实取值的差异

plt.figure(figsize=(20,5))

plt.plot(trues[:,0,-1].reshape(-1), label='GroundTruth')
plt.plot(preds[:,0,-1].reshape(-1), label='Prediction')
plt.legend()
plt.show()

注:

(1)如果要换成自己的数据(csv的二维时间序列数据)

首先确保csv中第一列为时间,后面均为特征(包含需预测的特征)

然后更改args参数,主要涉及的更改如下:

args = parser.parse_args(args=["--model", "informer", 
                               "--data", "custom",
                               "--root_path", "./data/",
                               "--data_path", "WTH.csv",
                               "--features", "MS",
                               "--target", "WetBulbCelsius",
                               "--data_path", "WTH.csv",
                               "--seq_len", '20',
                               "--label_len", '10',
                               "--pred_len", '5',
                               "--enc_in", '12',
                               "--dec_in", '12',
                               "--c_out", '1',
                               "--freq", "h",
                               "--attn", 'prob'
                               ])

本篇项目代码展示就到这里啦,全部代码可以跑通,其他时间序列的方法和改进正在研究中,欢迎提问和补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值