Python数据分析案例70——基于神经网络的时间序列预测(滞后性的效果,预测中存在的问题)

背景

这篇文章可以说是基于 现代的一些神经网络的方法去做时间序列预测的一个介绍科普,也可以说是一个各种模型对比的案例,但也会谈一谈自己做了这么久关于神经网络的时间序列预测的论文,其中一些常见的模式及它们存在的问题以及效果,还有在实际生产中去运用究竟会有什么问题?

时间序列预测的几种模式

首先我们要知道一个滑动窗口 的概念,时间序列预测无非就是用之前的数据去预测未来的数据。与传统的机器学习的二维表格不同,时间序列最简单的模式就是单变量单步预测 。即我们要预测一个变量,我们就用这个变量前几期的数据去预测未来下一期的数据,例如我们的滑动窗口大小为10,也就是说我们用这个变量 'Xt-10到Xt' 的数据,去预测' Xt+1' 的取值(这里就不写latex公式了反正应该也挺通俗易懂的)。也就是说,在单变量单步预测模型中,你的一个X向量就是 'Xt-10到Xt' 的数据 ,y标量就是 ' Xt+1' 的取值。然后我们可以不停的滑动这个窗口,得到很多组Xy ,就可以去训练模型了。(注意这里的X 是三维数据,形状为(n,t,p), n为样本量,t是时间步长,也就是滑动窗口大小,p是特征数量,这里单变量就是1,y形状为(n,1)的向量 )

现代神经网络基本都是这样做的,所以这导致了一个问题,我们要预测' Xt+2' 就没有办法了,因为我们不知道' Xt+1' 的真实值。有的人说可以吧 'Xt+1' 的预测值放进去继续预测,确实,传统模型都是这样做的,但是这样势必又会导致另外一个问题,即误差的累计,因为 'Xt+1' 的预测值不是真实值,所以有误差,预测出来的'Xt+2'就是在有误差的数据上预测出来的,那效果肯定会更差,并且时间步长越久,误差越夸张。 根据经验,这种方法预测出来到三步,五步以后,基本都没法看,和真实值差距是天涯海角。(传统的ARIMA都是这样做的,所以基本上很多人用这个模型都会发现他们预测出来的数据就是一条直线..........)

所以这就衍生出了一个问题,那我们要做多步预测该怎么办?这个也是我们要谈到的神经网络的第二种预测模式,即单变量多步预测 。既然我们因为不知道'Xt+1'所以没办法预测'Xt+2',那我们干脆直接用 'Xt-10到Xt' 的数据,去预测出来 'Xt+1 到 Xt+5'(假设我们要预测5步),很多计量经济学和传统统计学的同学要炸毛了,因为在传统的统计学模式基本看不到这种y取值是多个的情况,在无数的传统统计学和计量的模型以及一些基础的机器学习模型,无论是分类还是回归问题,我们的y都是一个标量,都是一个具体的取值,一个数字,而不是一个向量。向量也能作为y? 在神经网络里面是可以的,毕竟深度学习连图片,连音频,连文本, 视频都可以作为y,一个向量作为y是很正常的事情,大部分同学做模型的时候连数据的形状都没弄清楚,其实这是一个很重要的认知,你一定要认清楚你的Xy是什么形状,你才能去用对应的模型。

回到我们的单变量多步预测模型上来,我们的X 还是之前的X ,形状为(n,t,p),但是我们的y却不再是一个向量,而是一个矩阵形状为(n,T) ,T为你要预测的时间的步长,多步神经网络有2种方法去训练,即直接要预测几步, 你的输出层就用几个神经元:

python 复制代码
outputs = Dense(5, activation='linear')(pooled_output)  # 输出形状: (n, 5)

pooled_output是上一层经过池化 或者是循环神经网络最后一刻的状态,有可能是经过展开的mlp,反正就是将输入的X从3维变为2维度之后的情况,输入的pooled_output是二维数据,这样输出的就是一个(n,5)的向量,即n个样本,每个样本都预测了5步。这种方法直观,我目前基本上做的多步预测都是用这个方法。

也可以使用另外一种方法训练:使用 TimeDistributed

python 复制代码
lstm_output = LSTM(16, return_sequences=True)(inputs)  # 输出形状: (n, 10, 16)
# 使用 TimeDistributed
outputs = TimeDistributed(Dense(1, activation='linear'))(lstm_output)  # 输出形状: (n, 10, 1)

可以看到,这里的滑动窗口的时间步长和预测的步长就得是一样的。输入的lstm_output还是三维进去,三维出来,然后直接直接转为二维y矩阵了。(y就一个特征所以可以直接reshape为(n,10))。所以这种方法不能灵活的控制你的输入滑动窗口的步长跟你要预测的时间步长,必须一样。至于精准度和上面的方法比起来,我也没试过哪一种好。

讲完了上面的单变量的单步和多步预测,下面另外两种模式也更好理解,也无非就是X 特征变多了,是多变量的单步跟多变量的多步预测

其实思路和训练代码上没有太多差异,X 形状还是三维,形状为(n,t,p), 这里的p就不是1了,是2以上。单步预测的y 还是(n,1),多步还是(n,T), 只是在构建我们的训练集和测试集的时候,需要注意一下这个多维度的时候,构建Xy去对数据运用切片索引的问题。

按道理来说,以前基于树模型的二维表格的机器学习都是变量越多越好,但是在如今的这个循环神经网络里面并不是特征p越多越好。时间序列很看你上一时刻的这些变量的情况,有的时间序列特征噪音会特别多,而且会突变(例如之前的可能取值都是1,后面突然一下变成100),会对我们要预测的变量造成严重的干扰,这个可能也得进行一定的选择。

还有一些别的模式,例如用'Xt-10到Xt' 去预测 'Xt+8' 或者是 'Xt+9', 这种美言自称是多步预测的模式,我就不多说了,这本质也是单步预测..............都是一个换汤不换药的概念。(反正在自己的论文里面咋吹都是合理的)

神经网络预测时间序列预测的一些问题

常见的时间序列预测的模式都讲完了,下面再来讲一下。这些模式会有哪些问题?

首先就是很多新手刚开始做出一个模型,预测值和真实值一对比就会有一个:滞后性的困惑

所谓滞后性的困惑就是如上图一样,我们的预测值看起来就像真实值往后挪了一个时间单位。可能我这个图有点密集,画的太小看不清楚,但整体而言,滞后性的困惑 就是我们预测出来的'Xt+1'的值好像就是上一时刻Xt的值,看起来我们的预测无非就是把真实值进行了滞后一期罢了。

很多人不懂为什么,我刚开始学的时候也有这个困惑,但是后来做多了也就没管了,反正大家都这样干,我也就这样干吧。但现在我大概明白了里面的这个原理,和大家讲一讲。

首先一些正常的时间序列,例如股票价格,空气质量,人体的血糖浓度......都是具有强烈的自相关的序列数据,这点是不可否认的,**即我们的'Xt+1'跟'Xt'肯定是具有高度的相关性系数,并且是线性相关。**就例如股价你无论再怎么变,你肯定也是昨天价格的±10%的区间,不可能离昨天的值差距太远太离谱。所以就造成了这种今天的价格跟昨天的价格是强烈的线性正相关的关系。

我们都知道随机过程中经典的醉汉问题,即一个醉汉如果在开始坐标为(0,0)的二维平面上开始进行随机游走,每1秒钟走一步,那么走了十几个小时或者是一天之后,我们在哪里找到这个醉汉的概率最大?

答案是原点,就是(0,0)的位置,因为是随机游走,在随机游走中,醉汉的每一步移动是独立的,且在每个方向上的移动是随机的。假设醉汉在二维平面上每秒钟移动一步,每一步在x轴和y轴上的移动是独立的,且在每个方向上(左、右、上、下)的概率相等。他的动向的分布肯定是符合正态的,符合均值为零的。

所以再回到我们的时间序列预测问题。我们要预测下一时间的Xt+1时间的取值,也就是去找可能概率最大的取值,其可能性在哪里呢?那么就在上 Xt 上,也就是Xt的取值是作为'Xt+1'的概率最大,所以模型们都很聪明------他们基本都学到了概率最大的情况,也就是:直接把昨天的值稍微修改那么一点点,作为今天的预测值就好了,这也是为什么我们用循环神经网络做这种单步预测的时候,发现这种有滞后性的困惑的问题。本质就是时间序列的跟上一时刻的强烈的自相关带来的概率化最优的问题。

当然实际上模型肯定不会直接用上一个的取值完全相同作为下一时间的预测值,因为有些时间序列的动向变化肯定也不会是完全的概率相同的随机过程。他们神经网络模型肯定还是会根据数据的一些模式,例如季节性,波动性,趋势性学到一点点的修改。但是这种修改到底是好还是坏呢?我们肯定是用一些误差指标来进行衡量。然后进行一个对比。

这就引出了第二个问题,即 神经网络预测出来的都是类似滞后一期的数据。那我能不能构建一个基准模型------即MA(1)模型,直接用Xt 的值作为'Xt+1'的预测值,然后在整个样本上计算误差评价指标和神经网络预测出来的预测值进行一个对比,我们来看看效果到底行不行。

为什么叫MA(1)模型,MA大家都知道是移动平均模型,即用前n天的真实值平均一下作为每天的预测值。如果我们极端一点,取n等于1, 那么就是用前一天的平均值作为下一天的预测值,也就是说直接用今天的真实值作为下一天的预测值,我们就可以对比------我们花费了无数时间精力学习到构建出来的循环神经网络模型(整了一堆乱七八糟的卷积门控注意力机制transformer层)和我们最简单的MA(1)模型对比到底,到底能够'强多少'?

这就是今天这个案例的目的。


数据介绍

本次用的数据都是之前的案例常用的一些时间序列,我懒得找新的了,就主要是这5个:

每个数据都是两列,一列时间,一列是它的取值,当然它们的时间频率不一样,但是无所谓,神经网络也不管你是日度还是月度还是季度的,还是甚至是秒级的,反正都是一样训练。

本次下面演示的就用石油价格这个序列进行划分训练集和测试集,构建3维的数据张量,进行单变量单步预测。

对比如下的神经网络模型:

["Transformer", "CNN+BiLSTM", "BiGRU-Attention","BiLSTM-Attention", "BiGRU", "BiLSTM", "TCN", "GRU", "CNN", "LSTM","RNN","MLP", ]

在目前大量外行还在用lstm这种模型发论文的时候,我上面就随便这几个模型都可以写一篇普通期刊的论文,再缝合一点模态分解,优化算法或者损失函数都可以发SCI了。并且他们的构建很简单,我全部都统一化和模块化了。

当然,需要本次演示的数据和全部代码文件的同学还是可以参考:神经网络时间序列

代码实现

导入包,我们用keras框架,默认TensorFlow后端。3.0以上可以用pytorch作为后端,API接口类似。

python 复制代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt 
import seaborn as sns
import os
import time
from datetime import datetime
import random as rn
import scipy.special as sc_special
plt.rcParams ['font.sans-serif'] ='SimHei'               #显示中文
plt.rcParams ['axes.unicode_minus']=False               #显示负号

from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error,r2_score

import tensorflow as tf
import keras
import keras.backend as K
from keras.models import Model, Sequential
from keras.layers import Dense,Input, Dropout, Flatten,MaxPooling1D,Conv1D,SimpleRNN,LSTM,GRU,GlobalMaxPooling1D,Layer
from keras.layers import BatchNormalization,GlobalAveragePooling1D,MultiHeadAttention,AveragePooling1D,Bidirectional,LayerNormalization
from keras.callbacks import EarlyStopping

这个框架的代码做了太多次了,以前的文章也有非常多,所以说我也不过多的写解释了,下面就简单罗列一下这个过程。都是为了我们要展示一下最终的结果对比罢了。

读取数据

python 复制代码
data0=pd.read_excel('时间序列测试数据.xlsx',parse_dates=['date'],sheet_name=1).set_index('date').ffill()
data0.head()

sheet_name=1,因为我的石油价格是装在第二个sheet里面,所以我们用等于一,然后把data设置为时间索引。最后的.ffill 是为了防止数据的缺失值,就用前一个值进行填充。

展示数据折线图

python 复制代码
data0.plot(figsize=(12,3))

构建训练集和测试集

单变量单步模型,构建X和y的函数:

python 复制代码
def build_sequences(text, window_size=24):
    #text:list of capacity
    x, y = [],[]
    for i in range(len(text) - window_size):
        sequence = text[i:i+window_size]
        target = text[i+window_size]
        x.append(sequence)
        y.append(target)
    return np.array(x), np.array(y)
 
def get_traintest(data,train_ratio=0.8,window_size=24):
    train_size=int(len(data0)*train_ratio)
    train=data[:train_size]
    test=data[train_size-window_size:]
    X_train,y_train=build_sequences(train,window_size=window_size)
    X_test,y_test=build_sequences(test,window_size=window_size)
    return X_train,y_train,X_test,y_test

划分训练集和测试集,滑动窗口大小为64

python 复制代码
train_ratio=0.8     #训练集比例
window_size=64      #滑动窗口大小,即循环神经网络的时间步长
X_train,y_train,X_test,y_test=get_traintest(np.array(data0).reshape(-1,),window_size=window_size,train_ratio=train_ratio)
print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)

可以看到上面数据X还是2维的,下面归一化

python 复制代码
#归一化
scaler = MinMaxScaler() 
scaler = scaler.fit(X_train)
X_train=scaler.transform(X_train)
X_test=scaler.transform(X_test)

y_train_orage=y_train.copy()
y_scaler = MinMaxScaler() 
y_scaler = y_scaler.fit(y_train.reshape(-1,1))
y_train=y_scaler.transform(y_train.reshape(-1,1))

转为3维

python 复制代码
X_train=X_train.reshape(X_train.shape[0],X_train.shape[1],1)
X_test=X_test.reshape(X_test.shape[0],X_test.shape[1],1)
y_test=y_test.reshape(-1,1)   ; test_size=y_test.shape[0]
print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)

画图展示:

python 复制代码
plt.figure(figsize=(10,5),dpi=256)
plt.plot(data0.index[:-test_size],data0.iloc[:-test_size],label='Train',color='#FA9905')
plt.plot(data0.index[-test_size:],data0.iloc[-(test_size):],label='Test',color='#FB8498',linestyle='dashed')
plt.legend()
plt.ylabel('Predict Series',fontsize=16)
plt.xlabel('Time',fontsize=16)
plt.show()

定义评价指标

回归问题,总是用这四个指标:mae,rmse,mape,R2

定义随机数种子和计算评价指标的函数,当然我这里就没要R2了,用的mse,是一样的,要用R2就改一下下面的函数就行了。

python 复制代码
def set_my_seed():
    os.environ['PYTHONHASHSEED'] = '0'
    np.random.seed(1)
    rn.seed(12345)
    tf.random.set_seed(123)
    
def evaluation(y_test, y_predict):
    mae = mean_absolute_error(y_test, y_predict)
    mse = mean_squared_error(y_test, y_predict)
    rmse = np.sqrt(mean_squared_error(y_test, y_predict))
    mape=(abs(y_predict -y_test)/ y_test).mean()
    #r_2=r2_score(y_test, y_predict)
    return mse, rmse, mae, mape 

构建ma1模型:

python 复制代码
### 基准预测情况
result = pd.DataFrame()
result['t'] = pd.Series(data0.iloc[:,0])
# 生成第1列到第10列,每一列是t+1到t+10滑动窗口的值
for i in range(1, 6):
    result[f't-{i}'] = result['t'].shift(i)
result=result.dropna()

for t in result.columns[1:]:
    score=list(evaluation(result['t'], result[t]))
    s=[round(i,3) for i in score]
    print(f'{t}的预测效果为:RMSE:{s[0]},MAE:{s[1]},MAPE:{s[2]},R2:{s[3]}')

可以看到MA1模型的RMSE:1.791,MAE:1.338,MAPE:0.768,R2:0.013,很低。

构建模型

下面构建我们的神经网络模型,由于要用transformer层,所以我们需要自定义很多东西:

python 复制代码
class AttentionLayer(Layer):    #自定义注意力层
    def __init__(self, **kwargs):
        super(AttentionLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        self.W = self.add_weight(name='attention_weight',
                                 shape=(input_shape[-1], input_shape[-1]),
                                 initializer='random_normal',
                                 trainable=True)
        self.b = self.add_weight(name='attention_bias',
                                 shape=(input_shape[1], input_shape[-1]),
                                 initializer='zeros',
                                 trainable=True)
        super(AttentionLayer, self).build(input_shape)

    def call(self, x):
        # Applying a simpler attention mechanism
        e = K.tanh(K.dot(x, self.W) + self.b)
        a = K.softmax(e, axis=1)
        output = x * a
        return output

    def compute_output_shape(self, input_shape):
        return input_shape

#from __future__ import print_function
from keras import backend as K
from keras.layers import Layer
from tensorflow.keras import layers
from tensorflow import keras
    
class PositionEncoding(Layer):
    def __init__(self, model_dim, **kwargs):
        self._model_dim = model_dim
        super(PositionEncoding, self).__init__(**kwargs)

    def call(self, inputs):
        seq_length = inputs.shape[1]
        position_encodings = np.zeros((seq_length, self._model_dim))
        for pos in range(seq_length):
            for i in range(self._model_dim):
                position_encodings[pos, i] = pos / np.power(10000, (i-i%2) / self._model_dim)
        position_encodings[:, 0::2] = np.sin(position_encodings[:, 0::2]) # 2i
        position_encodings[:, 1::2] = np.cos(position_encodings[:, 1::2]) # 2i+1
        position_encodings = K.cast(position_encodings, 'float32')
        return position_encodings

    def compute_output_shape(self, input_shape):
        return input_shape
class Add(Layer):
    def __init__(self, **kwargs):
        super(Add, self).__init__(**kwargs)

    def call(self, inputs):
        input_a, input_b = inputs
        return input_a + input_b

    def compute_output_shape(self, input_shape):
        return input_shape[0]

class TransformerEncoder(layers.Layer):
    def __init__(self, embed_dim, dense_dim, num_heads, **kwargs):
        super().__init__(**kwargs)
        self.embed_dim = embed_dim
        self.dense_dim = dense_dim
        self.num_heads = num_heads
        self.attention = layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)
        self.dense_proj = keras.Sequential([layers.Dense(dense_dim, activation="relu"),
             layers.Dense(embed_dim),] )
        self.layernorm_1 = layers.LayerNormalization()
        self.layernorm_2 = layers.LayerNormalization()

    def call(self, inputs, mask=None):
        if mask is not None:
            mask = mask[:, tf.newaxis, :]
        attention_output = self.attention(inputs, inputs, attention_mask=mask)
        proj_input = self.layernorm_1(inputs + attention_output)
        proj_output = self.dense_proj(proj_input)
        return self.layernorm_2(proj_input + proj_output)

    def get_config(self):
        config = super().get_config()
        config.update({
            "embed_dim": self.embed_dim,
            "num_heads": self.num_heads,
            "dense_dim": self.dense_dim,
        })
        return config

构建模型函数:

python 复制代码
def build_model(X_train,mode='LSTM',hidden_dim=[64,32]):
    set_my_seed()
    if mode=='MLP':
        model = Sequential()
        model.add(Flatten())
        model.add(Dense(hidden_dim[0],activation='relu',input_shape=(X_train.shape[-2],X_train.shape[-1])))
        model.add(Dense(hidden_dim[1],activation='relu'))
        #model.add(Dense(16,activation='relu'))
        model.add(Dense(1))
        
    elif mode=='RNN':
        model = Sequential()
        model.add(SimpleRNN(hidden_dim[0],return_sequences=True, input_shape=(X_train.shape[-2],X_train.shape[-1])))
        model.add(Dropout(0.2))
        model.add(SimpleRNN(hidden_dim[1])) 
        model.add(Dropout(0.2))
        model.add(Dense(1))
         
    elif mode=='CNN':
        model = Sequential()
        model.add(Conv1D(hidden_dim[0],X_train.shape[-2]-2,activation='relu',input_shape=(X_train.shape[-2],X_train.shape[-1])))
        model.add(GlobalMaxPooling1D())
        model.add(Dense(hidden_dim[1],activation='relu'))
        model.add(Dense(1))
    
    elif mode=='LSTM':
        model = Sequential()
        model.add(LSTM(hidden_dim[0],return_sequences=True, input_shape=(X_train.shape[-2],X_train.shape[-1])))
        model.add(Dropout(0.2))
        model.add(LSTM(hidden_dim[1]))
        model.add(Dropout(0.2))
        model.add(Dense(1))

    elif mode=='GRU':
        model = Sequential()
        model.add(GRU(hidden_dim[0],return_sequences=True, input_shape=(X_train.shape[-2],X_train.shape[-1])))
        model.add(Dropout(0.2))
        model.add(GRU(hidden_dim[1]))
        model.add(Dropout(0.2))
        model.add(Dense(1))

    elif mode=='BiLSTM':
        model = Sequential()
        model.add(Bidirectional(LSTM(hidden_dim[0],return_sequences=True, input_shape=(X_train.shape[-2],X_train.shape[-1]))))
        model.add(Dropout(0.2))
        model.add(Bidirectional(LSTM(hidden_dim[1])))
        model.add(Dropout(0.2))
        model.add(Dense(1))

    elif mode=='BiGRU':
        model = Sequential()
        model.add(Bidirectional(GRU(hidden_dim[0],return_sequences=True, input_shape=(X_train.shape[-2],X_train.shape[-1]))))
        model.add(Dropout(0.2))
        model.add(Bidirectional(GRU(hidden_dim[1])))
        model.add(Dropout(0.2))
        model.add(Dense(1))

    elif mode == 'BiGRU-Attention':
        model = Sequential()
        model.add(GRU(hidden_dim[0], return_sequences=True, input_shape=(X_train.shape[-2], X_train.shape[-1])))
        model.add(AttentionLayer())
        # Adding normalization and dropout for better training stability and performance
        model.add(LayerNormalization())
        #model.add(Dropout(0.1))
        model.add(GRU(hidden_dim[1]))
        model.add(Dense(1))
        
    elif mode == 'BiLSTM-Attention':
        model = Sequential()
        model.add(Bidirectional(LSTM(hidden_dim[0], return_sequences=True), input_shape=(X_train.shape[-2], X_train.shape[-1])))
        model.add(AttentionLayer())
        model.add(LayerNormalization())
        model.add(Dropout(0.2))
        model.add(Bidirectional(LSTM(hidden_dim[1])))
        #model.add(Flatten())
        model.add(Dense(hidden_dim[1],activation='relu'))
        model.add(Dense(1))
        
    elif mode=='CNN+BiLSTM': 
        model = Sequential()
        model.add(Conv1D(filters=hidden_dim[0], kernel_size=3, padding="same",activation="relu"))
        model.add(MaxPooling1D(pool_size=2))
        model.add(Bidirectional(LSTM(hidden_dim[1])))
        model.add(Dense(1))
        
    elif mode == 'TCN':
        model = Sequential()
        for dilation_rate in [1, 2]:
            model.add(Conv1D(filters=hidden_dim[0], kernel_size=2, 
                             dilation_rate=dilation_rate, padding='causal', 
                             activation='relu', input_shape=(X_train.shape[-2], X_train.shape[-1])))
        model.add(Flatten())
        model.add(Dense(1))
        
    elif mode=='Transformer':
        model = Sequential()
        inputs = Input(shape=[X_train.shape[-2],X_train.shape[-1]], name="inputs")
        encodings = PositionEncoding(32)(inputs)
        encodings = Add()([inputs, encodings])
        x = TransformerEncoder(32,  hidden_dim[1], 2)(encodings)        #嵌入维度,全连接层神经元数,多头数
        x = GlobalAveragePooling1D()(x)
        #x = Dropout(0.2)(x)
        #x = Dense(32, activation='relu')(x)
        outputs = Dense(1)(x)
        model = Model(inputs=[inputs], outputs=outputs)

    else:
        raise ValueError("Unsupported mode: " + mode)
        
    model.compile(optimizer='Adam', loss='mse' ,metrics=[tf.keras.metrics.RootMeanSquaredError(),"mape","mae"])
    return model

定义一些画图展示用的函数

python 复制代码
def plot_loss(hist,imfname=''):
    plt.subplots(1,4,figsize=(16,2))
    for i,key in enumerate(hist.history.keys()):
        n=int(str('14')+str(i+1))
        plt.subplot(n)
        plt.plot(hist.history[key], 'k', label=f'Training {key}')
        plt.title(f'{imfname} Training {key}')
        plt.xlabel('Epochs')
        plt.ylabel(key)
        plt.legend()
    plt.tight_layout()
    plt.show()
def plot_fit(y_test, y_pred):
    plt.figure(figsize=(4,2))
    plt.plot(y_test, color="red", label="actual")
    plt.plot(y_pred, color="blue", label="predict")
    plt.title(f"拟合值和真实值对比")
    plt.xlabel("Time")
    plt.ylabel('power')
    plt.legend()
    plt.show()

定义训练函数,初始化两个数据框,用于储存我们的误差评价指标和预测值,我们会在序列函数里面进行模型的训练,预测,误差评价指标的计算,以及储存。

python 复制代码
df_eval_all=pd.DataFrame(columns=['MSE','RMSE','MAE','MAPE'])
df_preds_all=pd.DataFrame()
def train_fuc(mode='LSTM',batch_size=32,epochs=50,hidden_dim=[32,16],verbose=0,show_loss=True,show_fit=True):
    #构建模型
    s = time.time()
    set_my_seed()
    model=build_model(X_train=X_train,mode=mode,hidden_dim=hidden_dim)
    earlystop = EarlyStopping(monitor='loss', min_delta=0, patience=5)
    hist=model.fit(X_train, y_train,batch_size=batch_size,epochs=epochs,callbacks=[earlystop],verbose=verbose)
    if show_loss:
        plot_loss(hist)
            
    #预测
    y_pred = model.predict(X_test)
    y_pred = y_scaler.inverse_transform(y_pred)
    #print(f'真实y的形状:{y_test.shape},预测y的形状:{y_pred.shape}')
    if show_fit:
        plot_fit(y_test, y_pred)
    e=time.time()
    print(f"运行时间为{round(e-s,3)}")
    df_preds_all[mode]=y_pred.reshape(-1,)
        
    s=list(evaluation(y_test, y_pred))
    df_eval_all.loc[f'{mode}',:]=s
    s=[round(i,3) for i in s]
    print(f'{mode}的预测效果为:MSE:{s[0]},RMSE:{s[1]},MAE:{s[2]},MAPE:{s[3]}')
    print("=======================================运行结束==========================================")
    return s[0]

初始化参数

python 复制代码
window_size=64
batch_size=32
epochs=50
hidden_dim=[32,16]

verbose=0
show_fit=True
show_loss=True
mode='LSTM'  #MLP,GRU

模型训练

构建MLP模型

python 复制代码
train_fuc(mode='MLP',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)

上面4个小图分别是训练时候的不同损失的下降情况可以看到基本上40轮以后肯定都收敛了。下面是真实值和预测值对比,然后会打印这些误差指标以及运行时间。

构建RNN模型:

python 复制代码
train_fuc(mode='RNN',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)

就不展示那么多了,下面我们把所有的模型都一起训练,然后所有的误差指标都会储存起来,我们后面一起查看就可以了。

python 复制代码
train_fuc(mode='CNN',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='TCN',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='GRU',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='LSTM',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='BiGRU',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='BiLSTM',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='CNN+BiLSTM',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='BiLSTM-Attention',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='BiGRU-Attention',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)
train_fuc(mode='Transformer',batch_size=batch_size,epochs=epochs,hidden_dim=hidden_dim)

有的同学可能惊讶于我不同模型就修改一个mode参数就行了???其他全自动???

我知道大部分同学写代码都是东拼西凑,而且没有很好的编码风格,也不规范,所以说会花费大量的时间在于人的手工调整以及修改中。优雅的写法当然是只修改一个参数就可以把所有重复的过程都再进行一遍,这就是函数的封装性和简洁性的妙用,重复的代码绝对不会重写,重复的工作绝对不会重做。

因为我们写文章,无非都是训练不同的模型预测结果进行评价对比,这都是重复的工作,只是说模型不一样罢了。所有的过程都是重复性的过程,自然就要用代码去消灭这些重复性的工作,就得把他们封装的特别好,简洁和易用。

查看评价指标

我们直接按照MSE排个序:

python 复制代码
df_eval_all.loc['MA1',:]=evaluation(result['t'], result['t-1'])
df_eval_all.sort_values('MSE',ascending=True).style.bar(color='pink')

可以看到MA1模型误差最低,不直观的话,可以进行可视化:

python 复制代码
bar_width = 0.4
colors=['c', 'orange','g', 'tomato','b', 'm', 'y', 'lime', 'k','orange','pink','grey','tan']
fig, ax = plt.subplots(2,2,figsize=(8,6),dpi=128)
for i,col in enumerate(df_eval_all.columns):
    n=int(str('22')+str(i+1))
    plt.subplot(n)
    df_col=df_eval_all[col]
    m =np.arange(len(df_col))
    plt.bar(x=m,height=df_col.to_numpy(),width=bar_width,color=colors)#
    
    #plt.xlabel('Methods',fontsize=12)
    names=df_col.index
    plt.xticks(range(len(df_col)),names,fontsize=10)
    plt.xticks(rotation=90)
    plt.ylabel(col,fontsize=14)
    
plt.tight_layout()
#plt.savefig('柱状图.jpg',dpi=512)
plt.show()

什么结果不用多说了吧,虽然transformer是所有神经网络里面表现效果最好的模型,mse最小,也符合常理。但是,所有的神经网络都没有MA1模型的误差低。也就是说,我们费尽千辛万苦,各种复杂的结构层算法构建出来的神经网络居然都不如直接用Xt 的值作为'Xt+1'的预测值这种最简单的MA1模型!!!!

是不是颠覆三观了,也就是说,这么多充斥在学术界和研究界的用神经网络去做时间序列预测的模型的水论文研究,基本上都是无用功。(当然,更高级的模型我没试过不知道,不乱说)

但是没什么人来指出这个问题,听说最近国外在顶会上有人针对这种神经网络预测长时间序列提出了一系列的问题,但是他们的聚焦点还是在于这些误差评价指标不适用于时间序列预测中,并没有意识到神经网络用于时间序列目前的这个构建方法是存在问题的。

好在我们不用那么悲观,虽然没意义,但是90%的论文谁不是为了水论文呢,谁又真的拿去实际生产模型中去部署调用呢?更重要的是所谓的专家,学者,老师,导师,审稿人都对这些一窍不通,也没人发现这其中的问题。

当然,我为什么选着这个油价数据,是因为他是最具有代表性的。我测试了在其他数据上的这些模型的对比表现,我发现ma1模型并不总是最好的,但是它总是能够获得一个中等偏上的水平,也就是说他总能够打败60%以上的神经网络模型,如果你的数据有的还挺适合神经网络的,那就还可以用神经网络做一些有价值的工作吧。

但是一般来说,频率很高的这种强自相关的数据,ma1效果都挺好的。

最后再画一个不同模型预测出来的序列结果的对比图,这也是水文章里面常用的:


总结

神经网络做时间序列预测主要有,单变量单步预测,单变量多步预测,多变量单步预测,多变量多步预测,

本次演示的是最简单的单变量单步预测,对比了10种神经网络模型。["Transformer", "CNN+BiLSTM", "BiGRU-Attention","BiLSTM-Attention", "BiGRU", "BiLSTM", "TCN", "GRU", "CNN", "LSTM","RNN","MLP", ],然后发现所有的神经网络都没有MA1模型的误差低。也就是说,我们费尽千辛万苦,各种复杂的结构层算法构建出来的神经网络居然都不如直接用Xt 的值作为'Xt+1'的预测值这种最简单的MA1模型。

但是也不用太悲观,在有的数据上表现ma1不一定最好,并且大部分专家,审稿人,导师都不懂,国内也没有人指出这个问题,所以目前水论文还是可以随便放心的用。并且知道这个东西没啥意义就好,要是真的以为自己做了个模型能够产生多少价值跟收益,那可太天真了。


各种模态分解优化算法,损失函数缝合不同的神经网络预测时间序列的模型在往期文章中都有:

Python数据分析案例24------基于深度学习的锂电池寿命预测_锂离子电池寿命预测

Python数据分析案例25------海上风力发电预测(多变量循环神经网络)

Python数据分析案例41------基于CNN-BiLSTM的沪深300收盘价预测

Python数据分析案例42------基于Attention-BiGRU的时间序列数据预测

Python数据分析案例44------基于模态分解和深度学习的电负荷量预测(VMD+BiGRU+注意力)

Python数据分析案例50------基于EEMD-LSTM的石油价格预测

Python数据分析案例52------基于SSA-LSTM的风速预测(麻雀优化)

怎么水论文里面也写的非常清楚。代码都是类似的,框架高度封装,换个数据就能用,不需要怎么修改。

随便组合缝合都能发SCI,毕业真的太容易了有木有。

创作不易,看官觉得写得还不错的话点个关注和赞吧,本人会持续更新python数据分析领域的代码文章~(需要定制类似的代码可私信)

相关推荐
AIGC大时代16 分钟前
数据分析如何正确使用ChatGPT进行辅助?
大数据·人工智能·深度学习·chatgpt·数据挖掘·数据分析·aigc
Tester_孙大壮36 分钟前
第13章:Python TDD完善货币加法运算(二)
开发语言·python
蹦蹦跳跳真可爱5891 小时前
Python----Python高级(面向对象:对象,类,属性,方法)
开发语言·python
mnwl12_02 小时前
python轻量级框架-flask
开发语言·python·flask
张小特2 小时前
flask项目中使用schedule定时任务案例
后端·python·flask
gf13211112 小时前
python_在钉钉群@人员发送消息
android·python·钉钉
B站计算机毕业设计超人3 小时前
计算机毕业设计PySpark+Hadoop+Hive机票预测 飞机票航班数据分析可视化大屏 航班预测系统 机票爬虫 飞机票推荐系统 大数据毕业设计
大数据·hadoop·爬虫·python·spark·课程设计·数据可视化
xianfianpan3 小时前
史上最简单open-webui安装方式!!!
python·深度学习·神经网络·ai
deephub4 小时前
Python时间序列分析:使用TSFresh进行自动化特征提取
python·机器学习·时间序列·特征提取
brilliantgby4 小时前
蓝桥杯3527阶乘的和 | 组合数学
python·蓝桥杯