RNN实战入门:基于GRU的股票价格预测全流程解析
很多人问我学RNN循环神经网络的第一个实战项目为什么推荐股票预测而不是图像描述、文本生成这类更“典型”的任务。我的回答是股票预测的数据结构足够简单、特征足够直观、容错空间也足够大而且它天然就是一个时间序列问题正好打在RNN最擅长的点上。你不需要先搞懂词向量、注意力机制这些前置知识只需要拿着历史价格数据和一张图表就能把“循环”这两个字理解得明明白白。这篇文章我把我做RNN入门股票预测的全过程包括数据预处理、模型搭建、训练时踩的坑、评估时的误区完整拆开来讲代码也贴在下面希望给准备入门的读者一条能直接照着走的路。严格来说这篇文章的定位不是教你“怎么炒股赚钱”而是通过一个可感知、可复现的预测案例把RNN、GRU这些循环网络的核心机制弄透。适合的读者是对深度学习有一点点基础、但还没真正跑过时间序列项目的人也适合那些已经跑过图像分类但想跨到序列建模领域的朋友。1. 项目定位与基础认知为什么拿股票预测练手RNN1.1 这个项目到底适合谁需要什么样的基础入门RNN,最容易卡住的地方不是代码而是“不知道拿到什么数据、预测什么目标、怎么衡量好坏”。股票预测这个选题恰好把这三件事都变得非常具体数据是每天的收盘价目标是根据过去若干天的走势预测未来一天的价格好坏用误差大小来判断。基础方面我建议有三样东西就够了一是Python的基本语法和NumPy、Pandas的简单操作二是PyTorch或者TensorFlow中张量、Dataset、DataLoader这些基本概念三是最基础的神经网络直觉知道反向传播大概在做什么不需要会手推公式。如果前面这些还没摸过建议先花两天跑一个线性回归和全连接网的回归例子再回来做这个项目会顺畅很多。1.2 股票预测的“预测”到底指什么先破除几个误解很多初学者以为“RNN股票预测”就是模型输入历史价格输出未来一周每一天的价格曲线。实际入门项目通常做的是单步预测给定最近N天的价格预测明天的价格。如果要做多步预测一般是把预测出来的值再拼回输入序列这种方式误差会累积属于进阶内容。还有一种常见误解是“预测准确就等于赚钱”。先不说交易成本、滑点这些实际问题单看模型输出它拟合的是价格数值可实际交易中更关心的是涨跌方向和波动幅度。同一套模型在回归指标上表现不错在方向准确率上可能只有50%出头后者才是更有业务意义的衡量标准。这个我后面会专门讲怎么评估。1.3 为什么不推荐一上来就学LSTM和Attention现在网上讲RNN的教程十个里有九个直接上LSTM甚至一上来就上Transformer。我的看法是作为入门案例最好先用普通RNN或GRU。LSTM引入了三个门和一个记忆细胞结构比普通RNN复杂不少新手单独理解每个门就要花不少时间。GRU是LSTM的简化版只有两个门参数更少理解起来直观得多而且在很多中等规模的时间序列任务上表现并不比LSTM差。等到GRU跑通了再去比较LSTM、双向RNN、Attention这些变体的差异你会发现自己一下子就能看懂它们的改进动机。直接看LSTM很容易被那些“门”绕晕反而忽略了循环网络最核心的“隐状态传递”思想。2. 数据准备金融序列预处理里的隐藏天坑2.1 数据来源与特征选择别贪多从单特征开始我用的是公开的行情演示数据集包含某只股票每天的开盘价、最高价、最低价、收盘价和成交量。正式写代码前我先做了特征选择。初学者最容易犯的错误是“我有很多列那就全用上”但特征一多模型复杂度跟着涨入门的调试难度也会变大。所以我第一版只用了收盘价这一列。理由是收盘价是每天行情的最终结算价信息密度高、噪声相对小RNN学起来最容易。什么时候开始加入最高价、最低价、成交量这些信息等你把单特征版本跑通、能把loss曲线和预测图都看懂之后再逐步加特征对比效果变化。这叫“每次只改一个变量”是最基本的实验方法论。2.2 滑动窗口机制与超参数直觉window20怎么来的把时间序列转成监督学习的格式是这类项目里最绕的一步。核心思路是用一个固定长度的滑动窗口设window为N就用第1到第N天的收盘价预测第N1天的收盘价然后把窗口往后移一天变成第2到第N1天预测第N2天以此类推。这样就把一个序列“切”成了一批样本输入是N个数字输出是1个数字。window的选择直接影响模型看到的时间跨度。我第一版选的是20直觉参考是大约一个月的交易日数量希望模型能捕捉到一个月内的趋势惯性。这个值如果太小比如1到3天模型只能看到极短期的波动学不到趋势如果太大比如120天样本数量会急剧减少而且过长序列里的弱相关反而会稀释有效信息还容易把模型训难。我实测下来对日线收盘价20到40之间是个不错的起步区间。2.3 数据标准化为什么选MinMaxScaler而不是StandardScaler标准化这步看着简单其实有个细节很多教程没讲清楚。RNN的内部激活函数是tanh函数它对外部输入很敏感如果输入数据是几千块的价格tanh很容易饱和梯度直接消失模型怎么训都训不动。所以必须把价格压缩到一个可控的范围内。我用的是MinMaxScaler把数据线性映射到0到1之间。为什么不用StandardScalerStandardScaler是把数据变成均值0、方差1虽然是无线值域但对股票价格这种有明显的上下界相对一段时间且分布比较均匀的序列MinMax保持原始相对距离更直观值域也严格可控。实际测试中两者在这个任务上都能用但MinMax在“看到预测结果后反缩放回去”时更好理解因为最大最小值是固定的。重点提示标准化只在训练集上计算最大值和最小值然后用这套参数去缩放验证集和测试集。绝对不能在整个数据集上先统一fit再train/test切片那样会把未来数据的信息泄漏到训练过程里导致验证效果虚高后面一上真实场景就崩。2.4 训练集/验证集/测试集切分防泄漏是最容易被忽视的点时间序列的切分和图像分类完全不同。图像分类可以随机打乱数据分成三份因为每张图片是独立的时间序列不行今天的价格和昨天的价格强相关随机打乱会把序列的时序关系完全破坏模型等于在“偷看未来”。我的切分方式是严格按照时间顺序从历史数据中取前70%作为训练集接下来15%作为验证集最后15%作为测试集。只做一次滚动切分不用K折交叉验证时间序列的K折有专门做法新手没必要一上来碰。这样训练集、验证集、测试集在时间上一段挨着一段验证集能体现“未来数据”上模型的真实表现。2.5 代码实现数据准备部分的参考写法下面这段是把原始收盘价转成监督学习样本的核心代码我用的是PyTorch风格import numpy as np import torch from torch.utils.data import Dataset def create_sequences(data, window): xs, ys [], [] for i in range(len(data) - window - 1): x data[i: i window] y data[i window] xs.append(x) ys.append(y) return np.array(xs, dtypenp.float32), np.array(ys, dtypenp.float32) class StockDataset(Dataset): def __init__(self, features, targets): self.features torch.from_numpy(features) self.targets torch.from_numpy(targets) def __len__(self): return len(self.targets) def __getitem__(self, idx): return self.features[idx], self.targets[idx]注意我在create_sequences中用了一个非常直接的双重循环这个写法在小规模数据集上完全够用。如果数据量大可以改用滑动窗口索引或者stride技巧但入门阶段首要任务是“看得懂”性能优化可以后面再说。3. 模型原理拆解RNN、GRU与股票序列的底层关联3.1 为什么全连接网络和普通回归在这个任务上效果差你可以先想想如果不用RNN怎么预测明天的股价“今天涨了明天继续涨”这种简单规则算一种但不能捕捉更复杂的模式。用全连接网络也能做把过去20天的收盘价拼成一个长度为20的向量喂进去输出一个价格。但全连接网络的问题是它对每个位置的“权重”是固定的相当于认为“三天前的价格”和“十五天前的价格”分别按固定系数影响明天价格它没有“逐步加工信息”的过程也没有时间顺序的建模能力。RNN不一样的地方在于它是一步步读入序列的。先看第一天的价格更新一个内部记忆状态再看第二天参考已有记忆再次更新……每次读取都会把新信息与旧记忆融合。这样模型隐式地学会了“顺序”和“记忆”而不是把所有位置简单相加。对股票这种连续累积趋势的序列这种建模方式天然更匹配。3.2 循环网络的记忆机制隐藏状态是模型的核心所有RNN家族模型的核心都是一个叫隐藏状态hidden state的向量记作h。可以把它理解为一个“浓缩记忆的笔记本”每一步读完输入后模型会更新这个笔记本下一步的预测既依赖当前输入也依赖笔记本里已经积累的信息。用生活化类比你读一句话判断作者情绪不是在每个词上都重新开始而是带着前面所有词的语气印象继续读。RNN做的事就是这个。简单RNN在每个时间步做这样一件事h_t tanh(W_ih * x_t b_ih W_hh * h_{t-1} b_hh)x_t是当前时间的价格h_{t-1}是上一步的记忆两者加权求和再经过tanh激活成为新的隐藏状态h_t。最后一个时间步的h_t再经过一个全连接层映射成最终预测值。模型训练要学的就是W_ih、W_hh这些权重矩阵。3.3 梯度消失与GRU的门控思想为什么普通RNN不好训普通RNN有个著名问题当序列比较长时梯度在反向传播过程中经过很多时间步连乘会指数级衰减到接近0导致模型学不到较早时间步的信息。这就是梯度消失。序列越长这个问题越严重。GRU的改进思路是加入门控机制——更新门update gate和重置门reset gate。更新门决定“旧记忆保留多少、新信息融入多少”重置门决定“过去的记忆用什么方式参与当前计算”。这两个门都是可学习的本质上是两组带sigmoid激活的权重。门控机制让梯度可以更直接地“穿过时间”流动缓解了梯度消失问题。这里我建议你用一个非常简单的方式理解GRU它其实就是给普通RNN装了一个“阀门”阀门学会在合适的时机让旧记忆影响当前计算、又在新信息重要时大胆覆盖旧记忆。股票走势里昨天的影响显然比一年前大GRU可以通过学习更新门的取值来控制这种权重关系而不需要我们手工指定。3.4 模型结构设计隐藏层维度和dropout的选择我第一版模型的结构非常朴素第一层是GRU隐藏单元数设为64输出每个时间步的隐藏状态第二层是Dropout层概率0.2第三层是线性层把最后一步的隐藏状态从64维映射到1维价格值。隐藏维度64是我习惯的起步大小。太小了模型容量不足学不到足够复杂的走势模式太大了参数量多训练慢还容易过拟合。64对单特征日线数据来说是够用的。Dropout是防过拟合用的它对随机一半的连接进行抑制相当于强迫模型不要死记硬背某条路径而是在多次训练中学会更泛化的特征。我用的代码可以参考这个import torch.nn as nn class GRUModel(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers1, dropout0.2): super().__init__() self.gru nn.GRU( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.regressor nn.Sequential( nn.Dropout(pdropout), nn.Linear(hidden_size, 1) ) def forward(self, x): out, _ self.gru(x) # out: (batch, seq_len, hidden_size) last_out out[:, -1, :] # 取最后一步的隐藏状态 return self.regressor(last_out)注意我在GRU里设置了batch_firstTrue所以输入形状是(batch_size, window, 1)。window就是前面说的201代表特征维度。取最后一步的隐藏状态是这类单步回归任务的常见做法因为我们要预测的是“基于过去全部信息”的明天价格最后一步的隐状态已经汇总了整段的记忆。4. 完整代码实战从数据加载到模型推预测4.1 环境准备与依赖我用的是PyTorch 2.x配置很轻量pip install torch pandas numpy scikit-learn matplotlibscikit-learn在这里只用来做MinMaxScaler和计算评估指标matplotlib用来画预测图。如果你有显卡代码会自动用GPU没有显卡也不影响这个数据量CPU跑起来也就几分钟。4.2 完整训练代码一个最小可复现版本下面是完整流程我把它压缩在一个脚本里方便读者直接复制运行import torch import torch.optim as optim import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt from torch.utils.data import DataLoader def prepare_data(df_close, window20): scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(df_close.values.reshape(-1, 1)) xs, ys [], [] for i in range(len(scaled) - window - 1): xs.append(scaled[i: i window]) ys.append(scaled[i window]) return np.array(xs), np.array(ys), scaler def train_model(model, train_loader, valid_loader, epochs50, lr0.001): criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lrlr) for epoch in range(epochs): model.train() train_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch.unsqueeze(-1)) y_batch y_batch.unsqueeze(-1) loss criterion(pred, y_batch) loss.backward() optimizer.step() train_loss loss.item() * x_batch.size(0) train_loss / len(train_loader.dataset) model.eval() valid_loss 0.0 with torch.no_grad(): for x_batch, y_batch in valid_loader: pred model(x_batch.unsqueeze(-1)) loss criterion(pred, y_batch.unsqueeze(-1)) valid_loss loss.item() * x_batch.size(0) valid_loss / len(valid_loader.dataset) if (epoch 1) % 10 0: print(fEpoch {epoch1:03d} | Train Loss: {train_loss:.5f} | Val Loss: {valid_loss:.5f}) # 主流程示例 df pd.read_csv(your_stock_data.csv) # 请替换为实际数据文件 close df[Close] X, y, scaler prepare_data(close, window20) # 按时间顺序切分 split1 int(len(X) * 0.7) split2 int(len(X) * 0.85) X_train, y_train X[:split1], y[:split1] X_valid, y_valid X[split1:split2], y[split1:split2] X_test, y_test X[split2:], y[split2:] train_ds StockDataset(X_train, y_train) valid_ds StockDataset(X_valid, y_valid) test_ds StockDataset(X_test, y_test) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) valid_loader DataLoader(valid_ds, batch_size64, shuffleFalse) test_loader DataLoader(test_ds, batch_size64, shuffleFalse) model GRUModel(input_size1, hidden_size64, num_layers1, dropout0.2) train_model(model, train_loader, valid_loader, epochs50, lr0.001) torch.save(model.state_dict(), gru_stock_model.pth)几个关键点解释一下在训练DataLoader里我设置了shuffleTrue但验证和测试集shuffleFalse。训练时打乱顺序是为了每次batch尽量多样加速收敛验证测试时不打乱是为了和真实时间顺序保持一致方便后面画图和计算指标。MSE均方误差是回归任务最常用的loss缺点是受异常值影响大但作为入门足够。如果后续发现极端价格把loss带偏可以试Huber Loss。学习率0.001是Adam优化器的常用起点如果发现loss震荡不降可以调低到0.0005如果收敛太慢可以尝试0.003。4.3 模型推理与逆标准化预测结果怎么还原成真实价格模型输出的预测值是0到1之间的标准化数值要跟真实股价比较一定要做逆变换。否则就像拿华氏度读数和摄氏度读数作对比数值上根本对不上。def evaluate(model, loader, scaler, devicecpu): model.eval() preds, trues [], [] with torch.no_grad(): for x_batch, y_batch in loader: x_batch x_batch.unsqueeze(-1) pred model(x_batch) preds.append(pred.numpy()) trues.append(y_batch.numpy()) preds np.concatenate(preds).reshape(-1, 1) trues np.concatenate(trues).reshape(-1, 1) preds_real scaler.inverse_transform(preds) trues_real scaler.inverse_transform(trues) return preds_real.flatten(), trues_real.flatten() preds_test, trues_test evaluate(model, test_loader, scaler)画出预测曲线最简单的方式就是这样plt.figure(figsize(12, 6)) plt.plot(trues_test, labelTrue) plt.plot(preds_test, labelPred) plt.legend() plt.title(Stock Price Prediction with GRU) plt.show()如果图像上预测线在形状上基本跟随真实曲线、只是有点滞后或者幅度偏小说明模型学到了趋势如果预测线几乎是一条平线那多半是模型欠拟合或者数据泄漏问题后面排查章节我会专门讲。4.4 训练过程中需要盯紧的指标训练阶段我习惯每个epoch都记录train loss和valid loss并且每10个epoch打印一次。什么时候算正常通常train loss和valid loss都持续下降最后稳定在一个小范围内波动。什么时候出问题了valid loss降着降着开始回升train loss还在降说明开始过拟合了valid loss一开始就比train loss高很多首先要怀疑数据泄漏或者window设置不合理。我一直强调要把验证集当成“模拟未来”。你可以在训练过程中多次用验证集做预测看趋势方向有没有明显偏移。这个反馈比单纯看loss数字直观得多。模型训练不是“一键跑完看结果”而是频繁检查中间输出、随时调整的过程。5. 训练阶段常见的三个大坑与我的排查路径下面这部分是我自己踩过、也指导别人踩过的三个高频坑我按“遇到问题时的现象、排查思路、最终找到的原因”来写而不是直接把答案贴在开头。希望读的人能记住排查链路本身下次遇到类似问题更快定位。5.1 第一个坑模型输出逼近均值、loss死不下降现象非常典型训练了几十个epochloss下降非常缓慢预测输出变成一条接近股票均价的直线。很多人会怀疑是不是学习率太小。我当时的排查路径是“先看训练集拟合能力再谈泛化能力”。于是我把模型在训练集上做了一次预测发现train loss也很高。这说明问题不在于验证集而是模型本身的拟合能力就不足。然后我检查了数据预处理。发现我在第一步做MinMaxScaler时是在整个数据集上fit的之后再切分训练集和验证集。这意味着验证集和测试集的最大最小值已经“泄露”给了训练过程。虽然这个泄漏不像标签泄漏那样致命但它会让模型间接获得整个数据范围的宏观信息导致对验证集的表现异常自信而实际的训练梯度分配却被扭曲了。修正方法就是把scaler.fit放在序列切分之前只对训练集fit然后用训练集的scaler去transform验证集和测试集。代码也简单train_size int(len(scaled_data) * 0.7) scaler.fit(scaled_data[:train_size]) scaled_train scaler.transform(scaled_data[:train_size]) scaled_valid scaler.transform(scaled_data[train_size:])注意这里跟前面prepare_data里先切分再归一化的说法一致。我把这个问题放在前面已经提前提醒过但如果真忘了loss死活不降就能给你一个有力的信号。5.2 第二个坑验证集loss在降但方向准确率很差有一个阶段我的loss已经降到很低了预测曲线形状也对但统计每天的“预测涨跌方向是否和真实涨跌方向一致”准确率只有52%左右跟抛硬币差不多。这让我很困惑。我后来意识到问题出在时间步的粒度上。日线级别的价格变动中有很多“当天收盘相对昨天收盘”的微小波动是纯噪声。模型学的是整体趋势如果今天只涨了0.1%模型预测值可能差距在极小量级上方向就随机翻转了。换句话说方向准确率差不一定代表模型坏了而更可能说明“用日线收盘价做单步方向预测”这个任务本身就接近噪声上限。改进方向有两个一是把预测目标从“明天收盘价”改成“未来5天方向”或“未来5天累计收益率”平滑掉日内噪声二是把训练目标从回归改成分类——预测概率式的“涨/跌”——这会增加一点建模复杂度但指标含义更贴近业务。5.3 第三个坑同样的代码跑两次结果波动很大深度学习模型本身有随机性比如参数初始化和batch顺序。同样的代码两次训练结果在测试集上的误差相差5%到10%很正常。有些人就慌了觉得模型不稳定。我的处理方式是第一固定全局随机种子torch.manual_seed、numpy.random.seed都设好保证实验结果可复现。第二把训练之后的结果做多次重复实验取均值和方差。在汇报预测能力时用多次实验的均值而不是单次结果。第三把dropout关掉后再做一次纯预测测试排出dropout导致的推理波动注意PyTorch里模型.eval()已经会自动关闭dropout但如果在数据增强里有随机成分需要一并处理。这是深度学习项目里非常核心的习惯区分“模型本身的随机性”和“模型真正的能力”。如果你用GPU训练还需要注意一些框架内部的随机源例如PyTorch在CUDA卷积里会有非确定性算法可以用torch.use_deterministic_algorithms(True)来尝试强制确定性但部分情况下会降低性能。入门阶段用CPU跑就完全够用还省去了这层麻烦。5.4 我的排查工具与结论排查阶段我用的工具其实很简单一张loss曲线图、一张预测对照图、一个方向准确率数值。这三个指标配合起来比任何复杂框架都有用如果train loss高检查模型结构、学习率、输入是否标准化如果train loss低但valid loss高先查数据泄漏再查过拟合增加dropout或降低hidden_size如果预测曲线形状对但方向准确率低考虑任务本身噪声上限考虑换目标如果两次结果差很多固定随机种子多次实验取均值这个排查思路不仅适用于RNN股票预测也可以迁移到其他时间序列回归任务上。6. 效果评估与结果解读别拿一条拟合线当结论6.1 回归指标与方向准确率两者必须一起看我习惯同时看三个指标MAE平均绝对误差预测值和真实值的绝对差平均大小直观告诉你在股价这个量纲下平均错多少RMSE均方根误差对大误差更敏感能反映有没有极端离谱的预测方向准确率Direction Accuracy比较预测曲线相邻两点的变化方向是否和真实曲线相邻两点的变化方向一致。这三个指标的组合能帮你判断一个模型的真实能力。只看MAE和RMSE模型完全可以学成一个“滞后一天”的预测器预测曲线比真实曲线晚一天数值误差看起来不大实际却没有任何操作价值。方向准确率能把这个“滞后但误差小”的假象揭穿。6.2 预测曲线应该怎么读画图的时候我建议你把测试集的数据分成几段不要一整条曲线拉到两三年的跨度。为什么因为一整条曲线上的“看起来很像”可能是视觉错觉局部的先后偏差会被整体趋势淹没。我通常的做法是在测试集里随机挑三个不相邻的连续区间段每个区间显示50个时间点分别画预测和真实曲线。这样能看清模型在上升段、下跌段、震荡段的响应差异。通常你会发现模型在震荡段表现很差这恰好说明日线价格在无趋势时期不易预测这是序列规律本身决定的不是你代码写错了。6.3 业务意义上的“能不能交易”要冷静看待很多入门者跑出不错的效果后第一反应是“我是不是找到圣杯了”。我的建议是冷静。即便是完美的预测器在真实交易中还必须考虑交易成本、买卖价差、资金占用成本、滑点以及模型更新频率。这些因素叠加起来足以吞掉大部分预测误差优势。更重要的是股票市场是复杂的自适应系统过去数据中学到的规律可能随时失效。这也是为什么我反复强调这个项目的目标是理解RNN不是构建交易系统。你在评估模型时应该把重心放在“RNN是否学到了序列依赖”“GRU相对简单RNN是否更稳定”这些模型行为上而不是“这条策略能不能赚钱”。6.4 项目后续扩展从单步到多步从单特征到多特征把基础版本跑通后值得做的扩展按难度排列大概是这样多特征输入把Open、High、Low、Volume甚至技术指标一并作为输入看特征增多后模型是否更稳定。需要注意的是不同特征之间的尺度差异很大必须统一做标准化。多步预测把目标从明天的价格改成未来5天的价格需要调整输出层的结构和loss设计。常见做法有两种seq2seq式的一次生成多步或者递归式地连续把预测值作为输入。分类任务把预测目标从回归改为“明天涨跌”的二分类用交叉熵loss这样可以更直接地评估方向准确率。回测框架把模型嵌入到一个简单的交易回测逻辑里加入手续费和滑点假设对比“模型信号”和“简单买入持有”策略的表现。这些扩展每做完一步你都会对时间序列建模有更深的理解。但我的建议是务必一步一步来每次只改一个变量并把改动前后的指标记录在一个表格里。否则多个因素同时变化你会完全不知道是哪个操作导致效果变化。7. 写在最后我做这个项目的几个体会这个项目做完一遍我对RNN的理解从“能背出公式”变成了“知道它为什么适合序列预测”的层面。最直观的感受是RNN模型的隐状态虽然是个抽象向量但通过调整输入序列长度、比较不同窗口下的预测结果你能真切地“看见”它在用多长的一段历史信息来做判断。这种直观感受是读十篇文章都换不来的。另外一个体会是处理时间序列和图像完全不同你没法随机打乱数据、也没法使用常规的交叉验证必须时刻注意顺序和泄漏问题。参加比赛和做工程化项目时这种严格的时间分段意识往往是决定模型成败的核心因素。最后分享一个小技巧训练出第一个基本可用的模型之后可以故意把窗口调大一些比如从20改成60再看结果。你会发现模型通常不会因此大幅变好反而可能更差。这个实验能很直观地告诉你“并非历史数据越多越好对日线价格来说一段适度的近期记忆更关键”。这也是理解RNN乃至所有序列模型“记忆长度”到底意味着什么的绝佳方式。如果你正在入门RNN我强烈建议你按本文的方式真正跑一遍这个项目把所有踩坑环节都亲身体验一次。数据和代码本身不难难的是理解每一步选择背后的原因。等你独立调通并且能解释清楚每一步为什么这样设计时RNN这块敲门砖你就算是真正拿下了。