halo 的技术博客

返回

你有一段很长的时间序列——几年 tick 级或日线价量。你想预测下一步。Transformer 一把梭:把所有历史塞进注意力,算一遍全对的权重。问题是:注意力矩阵是 O(N²),序列一长显存就爆;而且它每次都得「从头看一遍」全序列,没法像人一样边走边记。

RNN 相反:它有个状态 h_t,每来一个新点就更新一下,复杂度 O(N),还能边走边记。但 RNN 有老毛病——梯度消失,记不了太久;而且必须严格串行,没法并行训练,慢。

结论先放这:状态空间模型 SSM 走中间路线——它用「连续系统的离散化」把历史压成一个固定维度的状态向量 h_t,h_t 沿时间递推(线性复杂度、可并行训练),又天然携带长程记忆。Mamba(Gu & Dao, 2023)的关键创新是「选择性」:让离散化步长 Δ 由输入 x_t 决定,模型自己决定「这一步该认真记还是快速忘」。

在我们的长程依赖合成序列(目标 = 130 步前的事件回声,τ 远大于 120 的滚动窗口)上:带持久循环状态的选择性 SSM 预测 MSE=0.50,相对固定 120 窗口线性基线(2.93)降 82.9%、相对 AR(1)(1.34)降 62.7%。 诚实地讲:这个优势来自 SSM 的循环状态跨窗口延续、能看到窗口外的 τ 步回声,而固定窗口线性读出的感受野被窗口锁死;但如果你把 SSM 也切成固定窗口、用线性读出头,它一样赢不了同等信息的线性基线(数据处理的不等式,后文陷阱 1)。SSM 的 Δ 选择性、状态维度 N、训练稳定性、长程衰减,是四个必须正视的真实约束。但作为「线性复杂度 + 长程记忆 + 可并行」三者兼得的结构,它是目前最接近理想的那一个。

状态空间模型:把历史压缩成状态向量 h_t,沿时间递推携带长程记忆


1. 连续状态空间:从微分方程到递推#

经典 SSM 描述一个连续系统:输入 u(t) 通过状态 h(t) 产生输出 y(t)。

h'(t) = A·h(t) + B·u(t)
y(t)  = C·h(t)
plaintext
  • A(N×N):状态转移矩阵,决定记忆怎么衰减(对角线为负 → 指数遗忘)。
  • B(N×1):输入怎么注入状态。
  • C(1×N):状态怎么读出输出。
  • N:状态维度,固定大小,这就是「把无限历史压成有限向量」的关键。

关键一步是离散化:模型在离散时间步 t 上跑,要用零阶保持(ZOH)把连续系统变成递推式。给定步长 Δ_t:

Ā = exp(Δ_t · A)
B̄ = (Δ_t·A)⁻¹ (exp(Δ_t·A) − I) · Δ_t·B
h_t = Ā·h_{t-1} + B̄·x_t
y_t = C·h_t
plaintext

exp(Δ_t·A) 是矩阵指数(我们用 scipy.linalg.expm)。这样 h_t 就递归地携带了所有过去——它是历史的压缩,而不是重看一遍。


2. 从零实现:离散化 + 递归扫描#

注意 dt逐时间步的——这就是 Mamba 的入口。传统 SSM(如 S4)用固定 Δ;Mamba 让 Δ 由输入 x_t 经过一层线性投影得到,于是「每一步记得多细」随数据变化。


3. 卷积视角:SSM 等价于一个卷积#

SSM 的递推可以展开成卷积:y_t = Σ_{s≥0} k_s · x_{t-s},其中核 k_s = C·Ā^{s}·B̄。也就是说,SSM 既不神秘也不必须串行——它能写成卷积,因此训练时可并行(像 CNN 一样一次性算完);推理时又能递归(像 RNN 一样 O(1) 每步)。这是它比 Transformer 省、比 RNN 快的根本原因。

def ssm_kernel(A, B, C, dt, L):
    """展开成卷积核 k(h),h 越大=越久之前。"""
    N = A.shape[0]
    Abar, Bbar = discretize(A, B, dt)
    CB = C.ravel()
    k = np.zeros(L); h = np.zeros(N)
    for t in range(L):
        h = Abar[t] @ h + Bbar[t].ravel()
        k[t] = CB @ h
    return k
python

在我们的参数下,卷积核在回溯 130 步处仍有明显权重(-1.86,非零),说明状态确实能感知窗口外的历史——这正是它能在长程依赖上赢过固定窗口的关键。下图为核的衰减形状和状态的递推轨迹。

SSM 卷积核指数衰减 + 状态 h 沿时间递推携带历史


4. Mamba 的「选择性」:Δ 由输入决定#

普通 SSM 的 Δ 是常数,等于「对所有输入一视同仁地遗忘」。Mamba 让 Δ_t = softplus(W_Δ · x_t + b_Δ),于是:

  • 遇到「重要的、信息量大」的点(|x_t| 大)→ Δ 大 → Ā 更接近单位阵 → 认真记住
  • 遇到「噪声、冗余」的点 → Δ 小 → Ā 更接近零 → 快速遗忘

这就是「选择性」:模型对历史做内容相关的压缩,而不是无差别地遗忘。

def selective_delta(x, dt_min=0.05, dt_max=0.9):
    """演示用:Δ 随输入局部幅度变化(真实 Mamba 是学出来的线性投影+softplus)。"""
    loc = np.abs(x)
    return dt_min + (dt_max - dt_min) / (1.0 + np.exp(-(loc - 0.8) * 3.0))
python

为了直观演示「选择性 = 任务相关」,我们让同一个序列通过两个不同关注点的 Δ 函数:任务 A 随幅度上升、任务 B 与幅度反相关。下图左是原始序列,中右是两个任务各自学到的输入相关 Δ(t)——同一段数据,不同任务聚焦的区域不同。这正是选择性相对「固定 Δ」的本质区别。

Mamba 选择性:同一序列,两个任务学到不同的输入相关 Δ


5. 实测:长程依赖上,持久状态赢在「看得见窗口外」#

我们造一段长程依赖序列:目标 y_t 里有 0.6·evt_{t-130}——130 步前的事件回声,再加短周期和噪声。预测窗 H=12。

  • 固定窗口线性基线:用最近 120 点线性读出未来 12 步。但 120 < 130,窗口里根本看不到那次事件的回声,感受野被锁死。
  • AR(1):只用上一时刻值外推,更看不到长程。
  • 选择性 SSM(持久状态):维护一个跨窗口延续的循环状态 h,每个时刻读 y_t=C·h_t 再刷新 h。因为 h 里一直带着 130 步前的回声,预测头只用当前状态就能用上它。
# 训练一个从「状态」到「未来 H 步」的读出头(滑动构造 (h_t, y_{t+H}))
Hs, Ys = [], []
for t in range(W, len(y) - H):
    dt = selective_delta(y[:t + 1])
    _, hf = ssm_scan(y[:t + 1], A_ssm, B_ssm, C_ssm, dt)
    Hs.append(hf); Ys.append(y[t + 1:t + 1 + H])
Hs = np.array(Hs); Ys = np.array(Ys)
Ws, *_ = np.linalg.lstsq(Hs[:ntr], Ys[:ntr], rcond=None)
pred_ssm = Hs @ Ws
python

结果:

窗口线性基线 MSE (H=12): 2.9250
AR(1)            MSE:      1.3425
SSM 持久状态     MSE:      0.5004
SSM 相对窗口线性改进: 82.9%
SSM 相对 AR 改进:      62.7%
plaintext

SSM 的持久循环状态把 MSE 砍到 0.50,比锁死在 120 窗口的线性基线低 82.9%。 根因很干净:τ=130 的回声在 120 窗口外,固定窗口线性读出永远看不到它;而 SSM 的状态 h 跨窗口延续,把那次事件一直带到了预测时刻。下图是测试段一条样本的预测对比(SSM 紧贴真实)和各步平均误差曲线(SSM 全程最低)。

长程依赖预测:SSM 持久状态(MSE 0.50) 远优于窗口线性(2.93) 与 AR(1)(1.34)


6. 五个真实陷阱(必须说清)#

陷阱 1:线性读出下,SSM 赢不了同等信息的线性基线。 和 TimesNet 完全同构的「数据处理的不等式」:如果你把 SSM 也切成固定窗口、用线性读出头,那么 SSM 的输出是输入的线性函数,整体等价于对窗口的线性回归,不可能比「直接对原窗口线性回归」更优。我实测的对比其实有个关键不对称——SSM 用的是「跨窗口延续的持久状态」(感受野是整个历史),而线性基线用的是「固定 120 窗口」(感受野锁死)。这个不对称才是 SSM 赢的原因,不是 SSM 本身比线性强。 若把 SSM 也切成 120 窗口、丢弃窗口外状态,它立刻回到和线性基线同一水平。所以:SSM 的威力来自「持久状态 / 长程感受野」,不是来自「变换本身」。 要在短程、窗口内就能解决的任务上,SSM 没有结构性优势。

陷阱 2:Δ 选择性是演示近似。 我们 selective_delta 用「幅度 → sigmoid」做演示,真实 Mamba 是学出来的线性投影 + softplus,Δ 是端到端从任务里涌现的。演示函数只为了说明「选择性 = 输入相关」,不能当真模型用。真要做,得上 PyTorch + 因果卷积扫 Δ。

陷阱 3:状态维度 N 是硬上限。 记忆被压进 N 维向量,N 太小记不下复杂历史,N 太大计算量和显存又回来。金融多资产、长历史场景里 N 的选取是实打实的 trade-off,不是越大越好(过拟合 + 训练不稳)。

陷阱 4:训练稳定性。 expm、矩阵求逆、Δ 的极端值都会让训练抖动。真实实现用 A = exp(A_log) 参数化(保证稳定)、Δ = softplus 保证正、并配合归一化。我们的 numpy 演示固定了参数,没暴露这层。

陷阱 5:长程记忆会衰减。 A 的对角线为负 → Ā 是收缩矩阵 → 越久之前的影响按指数衰减。τ 太大(超过 exp(ΔA) 的有效记忆长度)时,回声也会被忘掉。我们的 τ=130 还能被感知,是因为 N 维状态 + 适中 Δ 给了够长的有效记忆;换成 τ=500 可能就失效了。


7. 它和 RNN / Transformer / TimesNet 怎么选#

结构复杂度长程记忆可并行选择性
RNN/LSTMO(N)有但梯度消失否(串行)隐式
TransformerO(N²)完美(全看)注意力即选择性
SSM (S4)O(N)有,固定 Δ是(卷积视角)
MambaO(N)Δ 输入相关
  • Transformer:数据够、序列不太长、要最强拟合 → 用注意力。
  • TimesNet:周期结构主导、想让模型「看得见周期」→ 折叠成 2D。
  • Mamba/SSM:序列极长、要线性复杂度 + 长程记忆 + 可并行 → 状态空间。

一句话:Mamba 是「把 RNN 的记忆、Transformer 的选择性、CNN 的并行」揉到一起的尝试,代价是状态维度有限、训练更娇气。


8. 落地路径#

  1. 长序列特征提取:用预训练 Mamba 当 backbone 处理 tick / 分钟级长序列,吐出状态给下游模型。
  2. 在线推断省显存:生产环境用 SSM 的递归形式,每步 O(1) 状态更新,不重算全序列。
  3. 多资产联合:状态维度 N 同时编码多标的动态,比逐标的 RNN 共享记忆。
  4. 真要做端到端:上官方 Mamba 的 selective_scan(CUDA 实现),别用 numpy 递归(慢且不稳)。

9. 结论#

状态空间模型的精髓一句话:把无限长的历史,压成一个 N 维状态向量 h_t,让它沿时间递推。 它既不像 Transformer 那样每次从头看一遍全序列(省复杂度),也不像 RNN 那样记不住、训不动(可并行、长程记忆)。Mamba 再加的「选择性」——让 Δ 由输入决定——把「模型自己决定记什么、忘什么」变成了可微的操作。

我们纯 numpy+scipy 从零实现证明:在 τ=130 的长程依赖上,带持久循环状态的选择性 SSM 把预测 MSE 压到 0.50,比锁死窗口的线性基线低 82.9%——因为循环状态带着窗口外的回声。但这个优势来自「持久状态 / 长程感受野」,不是 SSM 变换本身比线性强;切成固定窗口、线性读出,它立刻回到基线水平(陷阱 1)。Δ 选择性、状态维度 N、训练稳定性、长程衰减,是四个必须正视的约束。

历史不用每次重看,压缩成一个状态就好——前提是,你让它选择性地压缩。

状态空间模型 Mamba 金融时序:把「历史」压缩成一个状态向量,选择性地记住该记的
https://blog.halo26812.eu.org/blog/mamba-state-space
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨