admin管理员组

文章数量:1627945

【想直接进入结果的请直接从右侧目录点击去看 解决方案如何评估时序模型的泛化能力

期待你提出宝贵的意见。

注1:本文仅仅展示思路和最基础的代码。欢迎提出您的宝贵意见。

注2:本文展示的可视化图形的数据来源均为网络来源,且经过脱敏。

要想得到好的模型效果,你要做的不只是拿数据套模型而已!套模型调参、修改模型之前你要的事情还有很多,这些事情几乎占用你整个项目的60%的时间!

更新信息

2021.09.06 16:24 更新解决方案:方案五:
2021.09.30 14:30 更新问题思考:Informer的预测功能的疑惑
2021.10.15 8:51 更新我的LSTM源码建模主要代码
2021.10.26 informer解读
2021-11 如何安装GPU版本的ML框架、而且是多版本共存的那种,两种方法
2022.10.25 更新源码链接

先上某个方案的结果

这里的预测结果只为证实可行性,这里的预测结果是不使用真实值作为输入去预测未来(不是测试集,是未来)的。

希望大家提出新的方案,一起学习进步。

问题探究

【真实数据,不是经过加工和改造的,也不是人为生成的】

my task:
根据历史数据进行时间序列建模,并且进行预测未来。

我的数据是time列 和 price列 的dataframe。从2007~2021.02.28的数据。

我做的事情是 预测未来的price,注意!不是测试集上的!因为对于时间序列来说,测试集上的预测效果并不能证明模型的好坏(因为很多硕士论文、期刊、博客等文章都是由真实值组成的数组作为输入的测试集),只能说明训练集的拟合好坏!【这里你可能会很疑惑,继续往下看】

任务类型:长时预测任务

我翻过很多博客文章,也翻过几篇和我的项目相关的论文(国内的)。

基本很多博客文章的预测都是针对测试数据集上进行预测的,但是其实测试集上的预测本质上还是一个拟合的过程,因为测试集是有真实数据构建滑动窗口作为input的(但真实预测未来的时候你是没有真实数据的,因为还没有发生。),当然,也有些很优秀的文章【比如这篇:LSTM进行客流预测】。

首先,先声明一下:在多元统计课堂上我们的老师就曾经证明过:股票、价格类的数据是无法预测的。(所以,建立模型预测的时候,不为准确,只为能够尽可能的逼近

环境:

  • tensorflow2.0
  • win10
  • python 3.7.10

我的步骤:

  • 读取数据
  • 判断是否需要差分运算【平稳性检验】,如果是,那么进行差分;如果不是,那么继续。
  • 判断是否白噪声数据【白噪声检验】,如果是直接终止,没有往下研究的必要了。
  • 划分训练集合测试集,验证集。【注意!时间序列的数据下,不同于普通回归、分类聚类、生成等场景的数据集划分策略,因为后面涉及滑动窗口的构建,所以我是根据时间的序列来进行划分的】
  • 分别对训练集、测试集、验证集进行数据标准化。【如果是在总数据集上进行数据标准化的话,会造成数据泄露,所以要分开进行。(为什么不用归一化?因为归一化会受到量纲的影响!为什么不用对数化等手段?因为对数没有负数会造成模型无法收敛)】
  • 接下来就是构建滑动窗口。【按照常理,我应该进行一些分组和规律的探索来确定这个windows_size,或者进行网络搜索来确定最优windows_size,我的电脑算力条件不允许我去进行这个复杂的过程。为了简便,我尝试windows_size为3,4,5,7,15,30,60】的情况。
  • 进行LSTM / linear / RNN / GRU / ARIMA / cnn+LSTM 的模型构建。并且已经进行学习率自动衰减、存储best model 等配置项的配置。
  • 训练集上的训练【评估良好】
  • 测试集上的预测效果良好。

问题来了,就在我预测未来的时候,有趣的事情发生了!

这是测试集上的评估:【有更好的情况,但已删了,先贴这个叭】

这是训练的结果:

当预测未来的时候:

奇妙的事情发生了,模型的预测结果的方差和均值在急剧变化,预测根本无法逼近真实的趋势、更不用说预测值准确与否,这是不正常的,这是一个误差累积的过程,只要第一个偏了后面也跟着偏了。但对于n多个神经单元的神经网络那只是小问题,这不是主要的原因,主要的原因是归咎于滑动窗口,如果我预测未来的时候,预测的值的趋势被滑动窗口里面的数据分布决定了。滑动窗口是一把双刃剑!危乎高哉!

【可想而知,但凡正常点都不会这样!而且真实数据也并不是这样的】

为了验证这个问题,我进行了更长时间的预测:

问题分析

首先,要说明的是我的预测过程是怎么样的:

  • 1.既然是序列预测,那么我预测未来的第一个值的input 就是所有训练集合中的从最后一个数往回数的最后一个窗口大小的数据。
  • 2.为了进行多步预测,我构建 了一个函数,进行数组的元素删除和添加、shape转换等。
  • 3.不断的把input数组的第一个元素剔除、并不断的把未来预测值加入input数组的最后一个元素中,从而实现滚动预测。【是滴,你没听错,这里用的不是测试集上的真实数据构建的滑动窗口作为input,这里用的是未来的预测值作为input的,因为未来是未知的、暂无真实数据,而我实现的是单步预测】
  • 4.通过不断的往后滚动input数组,实现滚动的单步预测。【不用多步预测的原因,因为多步预测的本质是建立在单步预测上面的,而且在使用预测值去添加到数组尾部然后去作为输入去预测的这种方法下,单步预测可以使用最近的历史数据去预测,可靠性相对强;而多步预测的话,利用的历史信息太远,没能根据最近的变化去预测下个时间步】

为了方便问题分析,我对预测过程进行了print。【这是归一化后的】



通过观察可以发现,在前面input的数组组内的数据分布不是呈现某一个单一趋势的时候,其预测并不是服从某个单一趋势,这是正常的。到了后面当滑动窗口组内趋势单一,那么其预测也就跟随这个预测延续下去了!这就是错的!不光是LSTM,别的模型也有。transformer系列的没有试过。

解决方案

  1. 方案一:尝试序列分解,单独给分解出的趋势序列进行建模。最后把分别建模的模型的预测结果相加或者相乘得到最终预测结果,相加还是相乘取决于分解算法。【在本数据集中的可行性还未验证】(在尝试当中,对噪声进行Boosting回归算法建模这一步比较难)
  2. 方案二:加入外生变量,即从时间列中衍生其它变量。(亲测无效)
  3. 另外:滑动窗口不要随便选择!要去notebook去看一下数据的周期大概是多少。【我也只是猜测,具体怎么选择我现在也很迷,搜索不到相关论文和资料,可以采取网格搜索/贝叶斯调参】
  4. 方案三:在每个窗口内部进行加权,即设置一个算法专门去训练窗口内部的权值变化 与 结果值之间的关系。(需要造轮子,比较复杂,需要很多时间精力)
  5. 方案四:开始进行transformer系列的尝试。
  6. 方案6:长序列预测模型 : informer,具体可以去看我这篇文章:informer的学习、阅读和使用
  7. 方案7:预测范围(将预测值转换为预测概率,根据置信度决定预测区间)

问题思考:Informer的预测功能的疑惑

关于informer的学习和使用:时间序列深度学习模型AAAI 2021最佳论文Informer的主要代码解读、项目运作、自定义数据集使用【提供包含注释和加工过的项目源码】
informer的预测功能的那一部分代码是下面这几个函数:


# 获取数据并进行处理,返回符合输入格式的数据
    def _get_data(self, flag):
        args = self.args

        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,
            'titick':Dataset_Custom,
            'custom':Dataset_Custom,
        }
        # 下面这个Data,此时是一个Dataset_Custom。
        Data = data_dict[self.args.data]
        timeenc = 0 if args.embed!='timeF' else 1

        # flag:设置任务类型
        # 根据flag设置训练设置和数据操作设置
        if flag == 'test':
            shuffle_flag = False; drop_last = True; batch_size = args.batch_size; freq=args.freq
        elif flag=='pred':
            # 如果是预测未来的任务
            shuffle_flag = False; drop_last = False; batch_size = 1; freq=args.detail_freq
            Data = Dataset_Pred
        else:
            shuffle_flag = True; drop_last = True; batch_size = args.batch_size; freq=args.freq
        # 使用Dataset_Custom进行读取数据集,并转换为数组
        data_set = Data(
            root_path=args.root_path,
            data_path=args.data_path,
            flag=flag,
            size=[args.seq_len, args.label_len, args.pred_len],
            features=args.features,
            target=args.target,
            inverse=args.inverse,
            timeenc=timeenc,
            freq=freq,
            scale=True,
            cols=args.cols
        )
        # print("data_set结果:",data_set)
        # d1 = iter(data_set)
        # d1 =next(d1)
        # print(len(d1),type(d1))
        # for i in  d1:
        #     print(i.shape)
            # print(i)
            # print("\n")
        """
        (96, 1)
        (72, 1)
        (96, 3)
        (72, 3)
        """
        """
        返回读取的数据且是一个iterable,可迭代对象。这个可迭代对象里面是4个数组,对应了
        """
        # sys.exit()
        print(flag, len(data_set))
        # 对data_set使用DataLoader
        data_loader = DataLoader(
            data_set,
            batch_size=batch_size,
            shuffle=shuffle_flag,
            num_workers=args.num_workers,
            drop_last=drop_last)
        """
        drop_last代表将不足一个batch_size的数据是否保留,即假如有4条数据,batch_size的值为3,将取出一个batch_size之后剩余的1条数据是否仍然作为训练数据。
        """
        # d2 = iter(data_loader)
        # d2 = next(d2)
        # print(len(d2),type(d2))
        # for i in  d2:
        #     print(i.shape)
        """
        torch.Size([32, 96, 1])
        torch.Size([32, 72, 1])
        torch.Size([32, 96, 3])
        torch.Size([32, 72, 3])
        """
        """
        DataLoader就是将数据data_set组装起来成input的格式,且是一个iterable,可迭代对象。这个输入格式是序列的输入格式,[批次大小batch_size,输入序列长度seq_len,特征(有多少列)数量]。
        其中,输入序列长度seq_len相当于是滑动窗口的大小。
        """

        return data_set, data_loader

# 预测未来
    def predict(self, setting, load=False):
        # 从_get_data获取数据
        pred_data, pred_loader = self._get_data(flag='pred')

        # sys.exit()
        # 加载模型
        if load:
            path = os.path.join(self.args.checkpoints, setting)
            best_model_path = path+'/'+'checkpoint.pth'
            self.model.load_state_dict(torch.load(best_model_path))
        # 清楚缓存
        self.model.eval()
        preds = []
        
        for i, (batch_x,batch_y,batch_x_mark,batch_y_mark) in enumerate(pred_loader):
            print(batch_x.shape,batch_y.shape,batch_x_mark.shape,batch_y_mark.shape)
            # torch.Size([1, 96, 1]) torch.Size([1, 48, 1]) torch.Size([1, 96, 3]) torch.Size([1, 72, 3])
            """
            [1, 96, 1]是输入的一个批次的X数据,可以认为是滑动窗口为96的X。
            [1, 48, 1]是输入的一个批次的Y数据,可以认为是滑动窗口为96的X的标签数据,48是inform解码器的开始令牌长度label_len,多步预测的展现。
            
            [1, 96, 3]是输入的X数据的Q、K、V向量的数组。
            [1, 72, 3]是输入的Y数据的Q、K、V向量的数组,其中,72=48+24,48是label_len,24是预测序列长度pred_len,也就是说24是被预测的,这里是作为已知输入的。
            """
            pred, true = self._process_one_batch(pred_data, batch_x, batch_y, batch_x_mark, batch_y_mark)
            preds.append(pred.detach().cpu().numpy())

        # print(true)
        preds = np.array(preds)
        preds = preds.reshape(-1, preds.shape[-2], preds.shape[-1])
        
        # result save
        folder_path = './results/' + setting +'/'
        if not os.path.exists(folder_path):
            os.makedirs(folder_path)

        print("本次预测:",preds)
        np.save(folder_path+'real_prediction.npy', preds)
        return

    # 对一个batch进行的编码解码操作,就是训练模型
    def _process_one_batch(self, dataset_object, batch_x, batch_y, batch_x_mark, batch_y_mark):
        batch_x = batch_x.float().to(self.device)
        batch_y = batch_y.float()

        batch_x_mark = batch_x_mark.float().to(self.device)
        batch_y_mark = batch_y_mark.float().to(self.device)

        # decoder input
        if self.args.padding==0:
            # 返回一个形状为为size,size是一个list,代表了数组的shape,类型为torch.dtype,里面的每一个值都是0的tensor
            dec_inp = torch.zeros([batch_y.shape[0], self.args.pred_len, batch_y.shape[-1]]).float()
        elif self.args.padding==1:
            dec_inp = torch.ones([batch_y.shape[0], self.args.pred_len, batch_y.shape[-1]]).float()
        # 在给定维度上对输入的张量序列seq 进行连接操作。
        """
        outputs = torch.cat(inputs, dim=0) → Tensor
        
        inputs : 待连接的张量序列,可以是任意相同Tensor类型的python 序列,可以是列表或者元组。
        dim : 选择的扩维, 必须在0到len(inputs[0])之间,沿着此维连接张量序列。
        """
        dec_inp = torch.cat([batch_y[:,:self.args.label_len,:], dec_inp], dim=1).float().to(self.device)
        # encoder - decoder(编码器-解码器)
        # 假如使用自动混合精度训练
        if self.args.use_amp:
            # pytorch 使用autocast半精度进行加速训练
            with torch.cuda.amp.autocast():
                # 假如在编码器中输出注意力
                if self.args.output_attention:
                    outputs = self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)[0]
                else:
                    outputs = self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)
        # 假如不使用自动混合精度训练
        else:
            if self.args.output_attention:
                outputs = self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)[0]
            else:
                outputs = self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)
        # 逆标准化输出数据
        if self.args.inverse:
            outputs = dataset_object.inverse_transform(outputs)
        f_dim = -1 if self.args.features=='MS' else 0
        # 下面未知
        batch_y = batch_y[:,-self.args.pred_len:,f_dim:].to(self.device)
        return outputs, batch_y

从代码中可以发现,貌似还是从现有数据集里面去做预测的(这里不敢确定啊,所以说是疑惑,因为我不太懂torch!)。【已经分析证实:informer没有这种问题

欢迎大佬在下面评论。

如何去评估时间序列任务模型的泛化能力呢

既然由于时间序列任务本质上是一个窗口化任务(自回归过程)的过程,那么肯定不能像普通机器学习任务的 使用测试集的前x-1个真值作为输入去预测第x个那样进行评估。

应该这样:

  • 从整个数据集中删除最后时间段10%的数据,剩下的数据分为训练集验证集即可训练模型,那10%数据作为评估集(也可以叫测试集,我为了区分非时序任务模型所以才叫的评估集),然后训练完模型后,对这10%的数据进行预测,预测的时候不能把这10%数据的真实值进行设置窗口和预测,这10%数据只能作为true_value 和 预测值 进行一个评价指标的计算。也就是预测这10%的数据是使用预测值去 预测 值。【这里的10%自己决定是多少,还有尽量要从数据集后面取,这样滑动窗口的第一个input 比较好取】

附上我的一些源码主要内容:【期待你提出宝贵的建议】

1.安装模块化去写你的项目/模型 , 这样后期改动的时候方便改动:
【还是比较喜欢函数式编程,有时候不太喜欢类编程】

  1. 滑动窗口
def sequence_engineering_for_train(df_train,arg):
    """
    :param df_train: Dataframe
    :param standard_sign:
    :param windows_size:
    :return:
    """
    df_train.set_index('time',inplace=True)
    array_train_old = df_train.values
    ans = array_train_old[:, 1:]
    scaler = StandardScaler()
    array_train = (array_train_old[:,:1])
    if arg.standard_sign == True:
        scaler.fit(array_train_old[:,:1])
        array_train = scaler.transform(array_train_old[:,:1])
    array_train = np.hstack((array_train, ans))
    features_set = []
    labels = []
    time_list = []
    df_train.reset_index(drop=False, inplace=True)
    # 定义滑动窗口
    for i in range(arg.windows_size,len(df_train),arg.interval+1):
        if i+arg.interval >= len(df_train):
            break
        features_set.append(array_train[i - arg.windows_size:i, :])
        labels.append(array_train_old[i+arg.interval, 0])
        time_list.append(df_train["time"][i+arg.interval])
    features_set, labels = np.array(features_set), np.array(labels)
    labels = np.reshape(labels,[labels.shape[0],1])
    print("sequence train data is prepared")
    return features_set,labels,scaler,time_list

def sequence_engineering_for_test(train_df,test_df,arg,scaler):
    """
    对测试集构造 特征数据,包含了训练集中的最后一个 windows_size 的数据。
    :param train_df: 训练集,Dataframe
    :param test_df: 测试集,Dataframe
    :param windows_size: 滑动窗口大小,需要与训练集的滑动窗口大小一致。
    :return:
    """
    train_df.set_index('time', inplace=True)
    test_df.set_index('time', inplace=True)
    # 获取训练集的最后一个窗口
    test_new_array_old = pd.concat((train_df[-arg.windows_size:], test_df), axis=0).values
    ans = test_new_array_old[:, 1:]
    test_new_array = test_new_array_old[:, :1]
    if arg.standard_sign == True:
        scaler = scaler.fit(test_new_array_old[:,:1])
        test_new_array = scaler.transform(test_new_array_old[:, :1])
    test_new_array = np.hstack((test_new_array, ans))
    test_features = []
    test_labels = []
    time_list = []
    test_df.reset_index(drop=False,inplace=True)
    for i in range(arg.windows_size, len(test_df),arg.interval+1):
        if i+arg.interval >= len(test_df):
            break
        test_features.append(test_new_array[i - arg.windows_size:i, :])
        test_labels.append([test_new_array_old[i+arg.interval, 0]])
        time_list.append(test_df["time"][i + arg.interval])
    test_features = np.array(test_features)
    test_labels = np.array(test_labels)
    print("sequence test data is prepared")
    return test_features,test_labels,scaler,time_list
  1. 模型训练主要内容
def train_model(time_list, features_set, labels, arg):
    global now_time
    """
    :param features_set: 训练数据集
    :param labels: 训练的标签
    :param batch_size: 每批大小
    :param epochs: 训练轮数
    :return:
    """
    # print("loss:",type(loss),loss)
    model = Sequential()
    #CNN+LSTM
    # model.add(tf.keras.layers.Conv1D(filters=20, kernel_size=4, strides=2, padding="valid",input_shape=(features_set.shape[1], features_set.shape[2])))
    # model.add(LSTM(units=arg.units1, return_sequences=True, kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    # model.add(Dropout(arg.dropout))
    # model.add(LSTM(units=arg.units2, kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    # model.add(Dense(units=arg.units_last))


    # LSTM
    model.add(LSTM(units=arg.units1, return_sequences=True, input_shape=(features_set.shape[1], features_set.shape[2]),kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    model.add(Dropout(arg.dropout))
    model.add(LSTM(units=arg.units2,kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    model.add(Dense(units=arg.units_last))

    # RNN
    # model.add(SimpleRNN(arg.units1, return_sequences=True, input_shape=(features_set.shape[1], features_set.shape[2]),kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    # model.add(Dropout(arg.dropout))
    # model.add(SimpleRNN(units=arg.units2,kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    # model.add(Dense(units=arg.units_last))

    #线性回归:
    # model.add(Flatten(input_shape=[features_set.shape[1], features_set.shape[2]]))
    # model.add(Dense(units=arg.units1,kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    # model.add(Dropout(arg.dropout))
    # model.add(Dense(units=arg.units2, kernel_regularizer=tf.keras.regularizers.l2(arg.l2)))
    # model.add(Dense(units=arg.units_last))

    model_name = "{0}_{1}_{2}_{3}ep_{4}ws_{5}bs_{6}_{7}.h5".format(arg.them, arg.model_sign, now_time, arg.epochs,
                                                                   arg.windows_size, arg.batch_size, arg.optimizer, arg.mask_choose)
    # 指数衰减学习率
    exponential_decay = tf.keras.optimizers.schedules.ExponentialDecay(initial_learning_rate=arg.learning_rate,
                                                                       decay_steps=arg.decay_steps, decay_rate=arg.decay_rate)
    if not os.path.exists(arg.log_path):
        os.makedirs(arg.log_path)
    if not os.path.exists(os.path.join(arg.model_path,arg.them)):
        os.makedirs(os.path.join(arg.model_path,arg.them))
    callbacks = [
        # 当验证集上的损失“val_loss”连续n个训练回合(epoch)都没有变化,则提前结束训练
        tf.keras.callbacks.EarlyStopping(monitor='loss', min_delta=arg.min_delta, patience=arg.patience, mode='auto'),
        # 使用TensorBoard保存训练的记录,保存到“./logs”目录中
        tf.keras.callbacks.TensorBoard(log_dir=arg.log_path, histogram_freq=2, write_images=True, update_freq='epoch',
                                       profile_batch=5),
        ModelCheckpoint(filepath=os.path.join(arg.model_path,arg.them,model_name), monitor='val_loss', mode='auto', verbose=1,
                        save_best_only=True)
    ]
    if arg.beta_jude == False:
        model.compile(optimizer=tf.keras.optimizers.Adam(exponential_decay), loss=arg.loss, metrics=arg.metrics)
    else:
        model.compile(optimizer=tf.keras.optimizers.Adam(exponential_decay,beta_1=arg.beta_1, beta_2=arg.beta_2), loss=arg.loss, metrics=arg.metrics)

    history = model.fit(features_set, labels, validation_split=arg.val_size, callbacks=callbacks, epochs=arg.epochs, verbose=2,batch_size=arg.batch_size)
    model_cofig = model.get_config()

    predict_train = model.predict(features_set)
    print(predict_train.shape)
    print(labels.shape)
    # sys.exit()
    # 计算r方分数
    r2_value = r2_score(labels, predict_train)
    # 计算解释方差
    exs_value = explained_variance_score(labels, predict_train)
    evaluation_dict = dict()
    chart_train(time_list, labels.tolist(), predict_train.tolist(), now_time,arg)
    evaluation_dict['model type'] = arg.model_sign
    evaluation_dict['model name'] = model_name
    evaluation_dict['data them'] = arg.them
    evaluation_dict["train mode"] = arg.mask_choose
    evaluation_dict['train time'] = now_time
    evaluation_dict['windows_size'] = arg.windows_size
    evaluation_dict["epoch"] = arg.epochs
    evaluation_dict["dropout"] = arg.dropout
    evaluation_dict["l1"] = arg.l1
    evaluation_dict["l2"] = arg.l2
    evaluation_dict["epochs"] = arg.epochs
    evaluation_dict["batch size"] = arg.batch_size
    evaluation_dict['optimizer'] = arg.optimizer
    evaluation_dict['r2 score on train'] = r2_value
    evaluation_dict['explained variance score on train'] = exs_value
    evaluation_dict['avg huber loss on train'] = sum(model.history.history['loss']) / len(model.history.history['loss'])
    evaluation_dict['min huber loss on train'] = min(model.history.history['loss'])
    evaluation_dict['max huber loss on train'] = max(model.history.history['loss'])
    evaluation_dict["avg huber val_loss on validation"] = sum(model.history.history['val_loss']) / len(model.history.history['val_loss'])
    evaluation_dict['min huber val_loss on validation'] = min(model.history.history['val_loss'])
    evaluation_dict['max huber val_loss on validation'] = max(model.history.history['val_loss'])
    print("模型名字:\t",model_name)
    print("R²:",r2_value)
    chart_loss(list(range(len(model.history.history['loss']))), model.history.history['loss'],  model.history.history['val_loss'],now_time,arg)
    return model,evaluation_dict,model_cofig

看过此篇文章的大佬/同学 评论 一起讨论一下解决方法QAQ。

期待你提出宝贵的意见。

英语里有一句谚语:预测是很困难的,特别是当它涉及未来的时候(It is difficult to make predictions, especially about the future)。了解一下预测未来的难度

推荐阅读:

  • https://blog.csdn/weixin_44607126/article/details/89086035?spm=1001.2014.3001.5501
  • https://blog.csdn/weixin_44607126/article/details/89392204

本文标签: 双刃剑序列窗口时间LSTM