跳转至

第 29 课 从固定窗口走到 RNN、LSTM 和 GRU

行情总是按时间到来。这一课从一段被窗口丢掉的历史开始,逐日追踪 RNN 的状态,再把 LSTM 的遗忘、写入和读出拆成可以手算的动作。

适合谁已经理解普通神经网络训练,希望读懂序列模型的读者
前置知识神经元、激活函数、损失、反向传播以及按时间验证
建议用时180 分钟,建议分两次完成
本课成果手推五天 RNN 记忆,算完一次 LSTM 门控,并能公平比较三种循环模型

被五日窗口挡在门外的财报

假设某公司六个交易日前公布财报,价格当天跳涨 8%,成交量也明显放大。随后五天的收益依次为 -1%、2%、1%、-1% 和 1%。研究者想用最近五日行情预测明天,但他制作的输入只有下面五个数字。

第 1 个位置  -1%
第 2 个位置  +2%
第 3 个位置  +1%
第 4 个位置  -1%
第 5 个位置  +1%

财报日已经落在窗口之外,模型完全看不见。把窗口扩大到六十日可以保留更多历史,却会出现新的困难。输入列从“前一日收益”一直排到“前六十日收益”,同一种形状出现在不同位置时,普通网络需要分别学习;窗口越长,输入维度越大,可用的完整样本反而越少,过时信息也会一起进入。

固定窗口本身没有错。许多可靠策略都使用明确的近二十日或近六十日统计。问题在于,有些研究任务更像阅读一篇按时间展开的故事。我们希望模型每天读入一页,把值得保留的内容压缩成一份状态,第二天继续带着这份状态阅读。循环神经网络正是为这种顺序处理设计的。

固定窗口、RNN、LSTM 与注意力模型保存历史信息的四种方式及能力比较

上图展示四种历史读取方式。固定窗口只保留预先选定的最近几格;RNN 每读一格就更新一枚状态;LSTM 为长期状态增加保留、写入和读出控制;注意力模型则允许当前位置直接比较较远的历史位置。前三种是本课重点,最后一种会在第 30 课展开。

RNN 比普通神经元多带了一份昨日状态

普通前馈网络处理今天的输入时,不会自动记得昨天内部算出了什么。RNN 在每天计算结束后留下一个隐藏状态,第二天把新输入和旧状态一起交给同一个计算单元。这里的“隐藏”表示它是模型内部的中间数;这里的“状态”表示它会沿时间继续传递。

先不看矩阵公式,我们设计一条可以手算的更新规则。每天的新状态等于旧状态的 60%,再加上当天收益。为了避免小数太长,表格把 1% 暂时记作数字 1。初始状态设为 0。

今天的新状态 = 0.6 × 昨天的状态 + 今天的收益

把五天逐步算完,会得到下面这张状态账本。

日期 今天的收益 昨天的状态 先保留旧状态的 60% 今天的新状态
第 1 天 -1 0.00 0.00 -1.00
第 2 天 +2 -1.00 -0.60 1.40
第 3 天 +1 1.40 0.84 1.84
第 4 天 -1 1.84 1.10 0.10
第 5 天 +1 0.10 0.06 1.06

第 5 天的状态 1.06 并非某一天收益的原样复制。它包含当天的 1,也包含前四天经过多次衰减后的影响。以第 2 天的上涨 2 为例,它经过第 3、4、5 天三次乘以 0.6,到最后仍留下 \(2\times0.6^3=0.432\) 的贡献。RNN 用一个固定长度的状态压缩历史,而没有保存每一天的完整原始记录。

真实模型会同时处理动量、成交量、波动和其他特征,隐藏状态也会从一个数扩展成一组数。更新比例不会固定写成 0.6,而是由训练数据学到的权重决定。为了让数值保持在可控范围,基础 RNN 常用双曲正切函数,英文写作 tanh。它会把很大的正负输入压到 -1 与 1 之间,同时保留正负方向。

现在公式才容易阅读。当天输入记作 \(x_t\),昨日隐藏状态记作 \(h_{t-1}\),今日状态记作 \(h_t\)\(W_x\) 负责调整当天输入,\(W_h\) 负责调整旧状态,\(b\) 是偏置。

\[ h_t=\tanh(W_xx_t+W_hh_{t-1}+b) \]

同一组 \(W_x,W_h,b\) 会在每个日期重复使用。图中看起来有五个状态节点,实际可以理解为同一台机器连续工作五次。把循环计算画成从第 1 天到第 5 天的一条链,叫作按时间展开

flowchart LR
    accTitle: RNN 用同一套规则沿五天更新状态
    accDescr: 每一天的新输入与前一天隐藏状态共同产生今天隐藏状态,第五天状态再用于预测下一阶段标签
    x1["第 1 天输入 x1"] --> h1["状态 h1"]
    h1 --> h2["状态 h2"]
    x2["第 2 天输入 x2"] --> h2
    h2 --> h3["状态 h3"]
    x3["第 3 天输入 x3"] --> h3
    h3 --> h4["状态 h4"]
    x4["第 4 天输入 x4"] --> h4
    h4 --> h5["状态 h5"]
    x5["第 5 天输入 x5"] --> h5
    h5 --> y["预测未来标签"]

打开序列模型记忆实验

状态是怎样变成预测的

如果任务是用过去六十日预测下一周收益,网络通常取最后一天的隐藏状态,把它交给一个普通输出层。输出层可能给出连续收益、上涨概率或横截面分数。只在序列结束后输出一次的结构常被称为“多对一”,意思是多天输入对应一个答案。

另一些任务需要每天都输出。例如,用每分钟行情判断下一分钟市场状态,序列中的每个时间点都可能产生标签。这类结构常被称为“多对多”。名称描述的是输入与输出的数量关系,不代表哪一种更先进。选择方式取决于研究问题和标签时间。

无论使用哪一种输出,成交边界仍与前面课程相同。若第 \(t\) 日收盘后才能得到完整序列,预测最早只能在随后可执行的时点下单。把最后一个输入日的收盘价同时当成成交价,会让回测提前行动。序列模型增加了结构复杂度,却不会修改市场时间规则。

RNN 为何难以记住很久以前

刚才的教学规则每经过一天,就把旧影响乘以 0.6。第 1 天的一单位信息,十天后只剩 \(0.6^{10}\),约等于 0.006;三十天后几乎无法分辨。前向计算中的信息会衰减,训练时从最后一天传回最早一天的改进信号也会经历类似连乘。

第 28 课把这种改进信号称为梯度。RNN 的反向传播需要沿时间链逐步计算,所以又常叫作随时间反向传播。当许多步的变化率都小于 1,远处梯度经过连乘后越来越接近零。早期事件几乎收不到学习信号,模型很难知道应该怎样修改处理它的权重。这种现象叫作梯度消失

若连乘中的数经常大于 1,梯度可能快速膨胀。一个轻微误差传回几十步后变成巨大数值,参数更新会剧烈震荡,甚至出现无穷大或非数值。这叫作梯度爆炸。梯度裁剪可以给梯度设置上限,缓解爆炸问题,却不能恢复已经消失的长期信号。

基础 RNN 还有一项直观限制。新旧信息每天都被挤进同一组状态,没有专门的开关告诉模型哪些内容应长期保留,哪些噪声应立即丢弃。重大财报与普通日内波动可能竞争有限的状态空间。LSTM 的设计正是从这里出发。

LSTM 给长期记忆单独留出一条通道

LSTM 的中文常译为长短期记忆网络。它保留 RNN 的隐藏状态 \(h_t\),又增加一组叫作细胞状态的数 \(c_t\)。可以把细胞状态想成一条持续向后传递的笔记带。每天到来时,模型依次处理三个问题。旧笔记还要保留多少,今天的新信息要写入多少,当前预测需要读出多少。

这三个比例分别由遗忘门、输入门和输出门控制。门值落在 0 与 1 之间。接近 0 表示几乎关闭,接近 1 表示大部分通过。研究者不会把门值作为交易参数手工指定,模型会根据当天输入、上一隐藏状态和训练目标学习这些内部控制量。

flowchart LR
    accTitle: LSTM 一天之内的保留、写入和读出
    accDescr: 旧细胞状态先经过遗忘门保留一部分,当天候选记忆再经过输入门写入,新的细胞状态最后经过输出门形成隐藏状态
    oldc["旧细胞状态 c(t-1)<br/>昨天留下的长期笔记"] --> forget["遗忘门 f(t)<br/>决定保留比例"]
    x["当天输入 x(t)<br/>与旧隐藏状态"] --> forget
    x --> input["输入门 i(t)<br/>决定写入比例"]
    x --> candidate["候选记忆 c~(t)<br/>准备写入的内容"]
    forget --> newc["新细胞状态 c(t)<br/>保留项加写入项"]
    input --> newc
    candidate --> newc
    newc --> output["输出门 o(t)<br/>决定当前读出比例"]
    x --> output
    output --> h["隐藏状态 h(t)<br/>交给预测与下一天"]

第一道门决定旧笔记保留多少

假设昨日细胞状态为 0.80。今天出现普通波动后,遗忘门给出 0.90。旧记忆保留部分就是 \(0.80\times0.90=0.72\)。门值若为 0.10,旧记忆只留下 0.08。遗忘并不总是损失。市场制度、公司基本面和波动环境都会变化,持续保留过时关系同样会伤害预测。

遗忘门根据当天输入和昨日隐藏状态计算一个线性分数,再经过上一课见过的 Sigmoid 函数,把结果压到 0 与 1 之间。写成公式如下。

\[ f_t=\sigma(W_f[x_t,h_{t-1}]+b_f) \]

方括号表示把当天输入和旧隐藏状态放在一起。\(W_f\)\(b_f\) 是训练中学习的权重和偏置。\(f_t\) 通常是一组数,因此模型可以让某些记忆维度长期保留,同时让另一些维度迅速衰减。

第二道门决定今天写多少,以及写什么

写新笔记需要回答两个不同问题。输入门决定“允许写入多少”,候选记忆决定“准备写入什么”。假设候选记忆为 -0.50,输入门为 0.20,实际写入量就是 \(-0.50\times0.20=-0.10\)。负号可以表示对某个内部方向的削弱,并不自动等于做空信号。

输入门继续使用 Sigmoid 产生 0 到 1 的比例,候选记忆通常使用 tanh 产生 -1 到 1 的内容。

\[ i_t=\sigma(W_i[x_t,h_{t-1}]+b_i) \]
\[ \widetilde c_t=\tanh(W_c[x_t,h_{t-1}]+b_c) \]

新的细胞状态把“旧状态保留项”和“新候选写入项”相加。沿用刚才的数字,旧状态保留 0.72,新内容写入 -0.10,所以今天的细胞状态为 0.62。

\[ c_t=f_tc_{t-1}+i_t\widetilde c_t \]

这条加法通道是 LSTM 的关键。遗忘门长期接近 1、输入门保持较小时,旧状态可以跨越许多日期相对稳定地传递。梯度也能沿这条通道保留更多影响,因此 LSTM 比基础 RNN 更容易学习远距离关系。

第三道门决定今天读出多少

细胞状态保存的内容不必全部用于今天的预测。输出门决定当前读出比例。假设新细胞状态为 0.62,经过 tanh 后约为 0.55,输出门为 0.60,今日隐藏状态约为 \(0.60\times0.55=0.33\)。细胞状态 0.62 仍会继续传向下一天,隐藏状态 0.33 则参与今天输出和下一天各道门的计算。

\[ o_t=\sigma(W_o[x_t,h_{t-1}]+b_o) \]
\[ h_t=o_t\tanh(c_t) \]

把这一天的动作合在一张数字卡里,可以看到 LSTM 没有神秘步骤。它仍由乘法、加法和激活函数组成,只是把信息流拆得更细。

动作 输入数值 计算 结果
保留旧记忆 旧状态 0.80,遗忘门 0.90 0.80 × 0.90 0.72
准备新内容 候选记忆 -0.50 暂不写入 -0.50
写入新内容 输入门 0.20 -0.50 × 0.20 -0.10
更新细胞状态 保留项 0.72,写入项 -0.10 0.72 - 0.10 0.62
形成隐藏状态 tanh(0.62) 约 0.55,输出门 0.60 0.55 × 0.60 约 0.33

LSTM 通过遗忘门、输入门和输出门选择记忆

这张图把细胞状态画成贯穿时间的主通道。遗忘门控制旧信息的保留,输入门和候选记忆共同完成写入,输出门只控制当前向外暴露多少。图中的门是计算比例的函数,并非有人在模型内部手动开关。

LSTM 仍然会忘记,也仍然会过拟合

门控改善了长期训练,却没有保证模型一定保留研究者认为重要的事件。如果训练标签与早期事件关系微弱,模型可能学会关闭相应通道;如果样本中存在泄漏,门控反而能更有效地保存错误答案。序列太长时,计算成本和参数选择也会增加。

门值有时可以画成热力图,观察哪些日期出现较高保留或写入比例。这种图只描述模型内部的信息流。若要把某一维命名为“趋势记忆”或“风险情绪”,还要用删除特征、替换日期、跨窗口复现等独立实验支持。仅凭颜色变化编出市场故事,很容易把解释建立在偶然相关上。

GRU 如何把三道门简化成两道

GRU 的中文常译为门控循环单元。它把 LSTM 的细胞状态和隐藏状态合并成一组状态,主要使用更新门与重置门。更新门决定旧状态保留多少、新候选接收多少;重置门决定生成候选状态时参考多少过去。

flowchart LR
    accTitle: GRU 用两道门更新一组状态
    accDescr: 重置门控制生成候选状态时参考多少旧信息,更新门再在旧状态和新候选之间分配比例,得到今天的新状态
    old["旧状态 h(t-1)"] --> reset["重置门<br/>候选生成时参考多少过去"]
    input["当天输入 x(t)"] --> candidate["候选状态"]
    reset --> candidate
    old --> update["更新门<br/>分配新旧比例"]
    candidate --> update
    update --> new["新状态 h(t)"]

可以把更新门想成一个混合旋钮。若它更偏向旧状态,记忆会保持得久;若它更偏向新候选,模型会快速响应当天信息。重置门较小时,候选状态更少参考过去,适合在环境突变时重新开始组织信息。

GRU 的参数通常少于相近规模的 LSTM,训练速度可能更快,在数据有限时也更容易控制。LSTM 把长期状态与当前输出分开,提供更细的读写控制。两者没有脱离任务的固定胜负。公平比较必须使用相同输入、标签、时间切分、训练预算、随机种子清单和交易规则。

把 RNN、LSTM 与 GRU 放回同一张桌子

模型 状态怎样传递 主要优点 主要限制 合适的起点
基础 RNN 一组隐藏状态反复更新 结构简单,容易看清循环逻辑 长距离梯度容易消失或爆炸 短序列与教学基线
LSTM 隐藏状态加细胞状态,并用三道门控制 长期信息通道更稳定,读写分工细 参数较多,训练和解释成本更高 长短信息并存且样本较充足
GRU 合并状态,并用更新门和重置门控制 参数较少,训练通常较轻 记忆控制粒度比 LSTM 简化 数据有限时的门控基线

“基线”在这里仍然重要。固定窗口加线性模型、固定窗口加树模型、基础 RNN 都可以成为参照。如果 LSTM 只比它们多消耗十倍计算,却没有在滚动测试中提供稳定增量,就没有必要因为名称更复杂而保留。

序列数据在程序里长什么样

普通表格通常有“样本数”和“特征数”两个方向。序列模型还要知道每个样本内部有多少个时间步,因此输入常写成三个维度,顺序是 [样本数, 时间长度, 每日特征数]

假设共有 2,000 个样本,每个样本包含过去 60 日,每日记录收益、成交量变化和波动三项特征,输入形状就是 [2000, 60, 3]。第一维告诉模型有多少段历史,第二维告诉它每段有多少天,第三维告诉它每天观察多少项数值。

建立这些窗口会损失一部分样本。资产至少要拥有 60 日有效历史,才能形成第一个输入;上市时间不足、长期停牌或字段缺失都会减少可用数量。相邻窗口还会共享 59 天行情,彼此高度相似。若随机把它们分到训练集和测试集,两边会出现近乎重复的序列,测试结果因此虚高。序列任务更需要按照时间边界切分,并结合标签跨度设置隔离区。

第一段序列模型代码怎样读

下面的示例完成一个“六十日输入对应一个连续标签”的任务。X_train 的形状应为“训练样本数、60、每日特征数”,y_train 每项对应同一行序列之后的未来标签。X_validy_valid 来自更晚时期。运行后,每个验证样本应得到一个预测。

from tensorflow.keras import Sequential
from tensorflow.keras.layers import LSTM, Dense
from tensorflow.keras.callbacks import EarlyStopping

model = Sequential([
    LSTM(16, input_shape=(60, X_train.shape[2])),
    Dense(1)
])

model.compile(optimizer="adam", loss="mse")
stop = EarlyStopping(
    monitor="val_loss",
    patience=5,
    restore_best_weights=True
)

model.fit(
    X_train,
    y_train,
    validation_data=(X_valid, y_valid),
    epochs=100,
    batch_size=64,
    callbacks=[stop],
    verbose=0
)
prediction = model.predict(X_valid)

LSTM(16, ...) 表示隐藏状态和细胞状态各有 16 个内部维度。input_shape 只描述一个样本内部的 60 天和每日特征数,不把样本数量写进去。Dense(1) 把最后状态转换成一个连续预测。均方误差、早停和批次的含义与第 28 课相同。

读代码时还要寻找没有写在模型结构里的选择。六十天为何合适,缺失日期如何处理,跨资产序列怎样对齐,标准化参数从哪里估计,标签从哪个可成交时点开始,验证边界是否隔离了重叠标签。网络只会接收准备好的数组,这些数组怎样形成往往比 16 个还是 32 个单元更影响可信度。

若把 LSTM 换成 GRU,代码改动很小,研究结论却不能只比较一次运行。两者的参数量不同,随机初始化也会带来波动。应预先设定多个容量和随机种子,在同一滚动验证框架下报告平均、范围和失败次数,并把固定窗口基线一起保留。

序列长度为什么不能越长越好

使用过去二十日、六十日还是二百五十日,代表不同研究假设。短窗口更关注近期状态,样本数量较多,计算也轻;长窗口可以覆盖慢变化,却会纳入更多过时关系,减少拥有完整历史的资产,并增加训练难度。

序列长度应先从机制提出一个有限候选范围。例如,研究月度趋势可以比较二十日与六十日,研究年度基本面变化才可能考虑更长跨度。随后只在训练与验证窗口选择,测试集保持封存。如果先看测试结果,再挑出“恰好最好”的六十七天,窗口长度就成为过度拟合的一部分。

还要区分交易频率与记忆长度。每天更新模型不代表只能看一天历史;使用一年历史也不代表持仓一年。输入跨度、预测跨度、调仓频率和持有期限是四个不同决定,必须在研究说明中分别写清。

容易弄错的地方

容易出现的理解 为什么不够可靠 应该怎样处理
隐藏状态就是模型对市场的理解 它只是为降低损失形成的内部数字表示 用替换输入、跨期复现和消融实验谨慎解释
窗口越长,信息越完整 长窗口减少样本,也会带入过时内容 从机制给出有限候选,在验证期选择
LSTM 一定胜过 GRU 参数更多只表示可表达关系更多 在相同数据、预算和种子下比较
随机切分重叠窗口 相邻样本共享大部分日期,测试会看到近似副本 按时间切分,并处理标签跨度重叠
门值高就代表看多信号 门值只控制内部维度的信息流 不把单个门值直接转成交易方向
用测试集决定序列长度和早停 测试答案参与了模型选择 所有选择停留在训练与验证阶段
只与另一个深度模型比较 两者可能一起输给简单方案 始终保留固定窗口线性与树模型基线

一分钟自测

旧细胞状态为 0.6,遗忘门为 0.8,候选记忆为 -0.4,输入门为 0.25。新细胞状态是多少?若输出门很小,细胞状态是否会立刻消失?

查看答案 保留项为 `0.6 × 0.8 = 0.48`,写入项为 `-0.4 × 0.25 = -0.10`,新细胞状态为 0.38。输出门只控制当前隐藏状态读出多少,细胞状态仍可继续传向下一天,不会因为输出门小就立刻消失。

本课术语

词语 先这样理解
序列 按时间或其他顺序排列的一组数据
隐藏状态 循环模型传给下一时间步的压缩信息
RNN 重复使用同一计算单元逐步处理序列的网络
按时间展开 把同一循环单元画成多个连续日期的表示方法
梯度消失 远距离学习信号经过连乘后越来越小
细胞状态 LSTM 中用于较稳定传递长期信息的通道
门控 用 0 到 1 的比例调节信息保留、写入或读出
LSTM 使用细胞状态和三类门控的循环网络
GRU 用更新门与重置门简化状态控制的循环网络
多对一 多个时间步输入对应一个最终输出的任务结构

课后练习

  1. 状态跟踪。 使用“新状态等于 0.5 乘以旧状态再加当天收益”,初始状态为 0,两天收益依次为 2 和 -1。第二天状态是多少?

    提示与答案第一天状态为 2。第二天代入以后得到 `0.5 × 2 - 1 = 0`。

  2. 长期衰减。 某项影响每天保留 80%。请计算五天后还剩原来的多少,并解释这对基础 RNN 意味着什么。

    提示与答案`0.8` 的五次方约为 0.328,也就是约 32.8%。时间继续拉长后影响会越来越小,早期事件可能难以收到足够训练信号。

  3. 门控计算。 旧状态为 0.7,遗忘门为 0.9,候选记忆为 0.4,输入门为 0.5。请算出新细胞状态。

    提示与答案旧信息保留 `0.7 × 0.9 = 0.63`,新信息写入 `0.4 × 0.5 = 0.20`,相加得到 0.83。

  4. 动手实验。 在序列记忆实验中放入一个较早的重大事件,逐步把它移得更远,比较固定窗口、简化 RNN 和带高遗忘门的 LSTM。

    提示与答案事件离开固定窗口后会完全不可见;简化 RNN 的影响会逐步衰减;LSTM 在遗忘门长期接近 1 时可保留更久。实验结论只解释信息通路,不能直接证明真实市场预测更好。

  5. 研究设计。 你要比较固定窗口树模型、RNN、LSTM 和 GRU。请设计一套公平方案,并说明至少六项必须固定或报告的条件。

    评价要点应包括相同数据版本、特征、标签、时间切分、成交规则和成本;控制相近参数量或训练预算;预先固定多个随机种子;只在验证期选择序列长度与早停;报告平均、范围、失败次数和不同市场阶段结果。

离开本页前
固定窗口会把更早行情挡在输入之外,RNN 用逐日更新的状态把过去带向今天,长链条中的信息与梯度却会逐步变弱。LSTM 用保留、写入和读出控制长期状态,GRU 再把状态和门作了简化。把五日数字从头算一遍,比只记住三个缩写更能说明这些模型各自改了哪里。