先讲一个最直观的现象:刷论文列表的时候,如果标题里同时出现 In-Context Learning、Linear Dynamical Systems 和 Transformers 这三个词,基本能猜到这篇工作在干什么——它想把“上下文学习”从函数拟合推进到更复杂的系统辨识。我最近在 NIPS 2025 的稿件堆里留意到一篇标题包含 “In-Context Learning of Linear Dynamical Systems with Transformers: Approximation Bounds” 的工作,标题后面应该还跟着泛化或误差相关的字眼,但光看前半部分已经足够引起兴趣。它想解决的问题其实是:一个固定参数的 Transformer,能不能像人类看几组数据就总结规律那样,在推理时只凭上下文里给出的输入输出样本,现场学会一个线性动态系统的行为,然后对未来做预测?这个问题既有理论难度,又有实际价值。这篇博文我想从我的角度把这类工作拆开,聊它到底解决了什么、核心细节在哪儿、如果只想搭一个最小实验验证想法,应该从哪几个地方下手,以及复现时最容易踩的坑。
1. 这篇论文到底在解决什么问题
1.1 一句话版本:让 Transformer 在多步预测现场学会系统规律
如果你熟悉上下文学习,应该知道一个典型场景:给模型一段包含若干“输入-输出”示例的文本,然后让它回答新输入对应的输出。模型不做任何梯度更新,所有“学习”都发生在 Attention 对上下文的加权组合里。现有研究证明了 Transformer 能在上下文中解决线性回归、逻辑回归、甚至一些简单优化问题。
但线性动态系统是另一类更硬的对象。它不只意味着一个输入到输出的静态映射,而是存在隐藏状态,当前输出受历史状态影响,系统行为由状态转移矩阵决定。用生活化的类比来说:线性回归像是“看着几个菜谱就学会了一道菜的配方”,而线性动态系统更像是“只看几段行情,就要现场判断接下来行情怎么走”,因为市场状态是隐藏的、连续的、带噪声的。前者是一个点对点的映射问题,后者是一个序列滤波与预测问题,复杂度不在一个量级。
基于常见实践的直观判断,Transformer 之所以能处理这种任务,是因为因果自注意力天然具有“按时间顺序聚合历史”的结构。它可以在前向计算中隐式维护一个关于系统状态或参数的统计量,不断用新观测修正。论文标题里的 “Approximation Bounds” 给我们划定了清晰的研究目标:不只是说“能学会”,而是从理论上证明在有限层数、有限宽度、有限上下文长度的条件下,这个“现场学会”的误差边界到底是多少。
1.2 上下文学习要和传统系统辨识对应上
传统做法是离线系统辨识。拿一批观测序列,用预测误差法、子空间方法,或者直接跑卡尔曼滤波来做状态估计,估计出状态转移矩阵和观测矩阵,再部署预测。整个流程分为“收集数据、拟合模型、上线预测”三步。而上下文学习的做法是:不提前为每个系统单独拟合参数,而是把所有系统的共性规律压缩进 Transformer 的权重里。推理时,模型通过上下文样本识别当前是哪个系统,直接输出预测。
这两种路径各有利弊。传统系统辨识每一步都可解释,有现成的控制理论工具做稳定性分析,但更新模型参数需要重新优化或在线递推,在数据分布漂移时经常要重新拟合。Transformer 上下文学习则把自适应放到更靠前的位置,所有适配都发生在序列内部。它的弱点是理论解释还不像卡尔曼滤波那么完备,而且模型要见足够多的系统才能举一反三。
明白这个对照之后,标题里的 “Approximation Bounds” 就更有意义了。它本质上是在问:如果我想让 Transformer 模仿传统系统辨识流程,甚至达到接近卡尔曼滤波的效果,到底需要多少层、多少头、多少上下文长度?这个问题的答案直接指导实验设计和模型选型,而不是停留在“效果还行”的工程判断上。
1.3 为什么 “Approximation Bounds” 值得单独关注
我是做应用出身的人,以前看到理论文章里的 bound 常常跳过去。但这个方向我建议你别跳,因为它的结论能帮你省大量实验经费。假设一个 Transformer 模型要达到近似卡尔曼滤波的预测效果,理论上需要深度 L 和宽度 D 都随状态维度或上下文长度增长。如果事先知道这个依赖关系,你就能判断:一个 6 层的模型在 128 步上下文里大概率够用,还是明显不够用。
另外,这类论文通常会区分“表达能力”和“可学习性”两个层次。前者研究的是:是否存在一组注意力权重,让 Transformer 在这个任务上达到很小误差。后者研究的是:用梯度下降能不能学到那组权重。很多做实验的人会发现模型效果差,第一时间怀疑数据或调参,但有时候根本问题是网络容量不够,或者上下文长度不够,模型没有获得足够信息来辨识系统。Approximation Bound 就是那个“别瞎猜,先算容量”的参考。
所以我个人读这类稿子的习惯是:先把问题定义搞清楚,再去看边界定理的形式,最后才是实验部分。问题定义决定了你要预测什么、误差怎么衡量、上下文如何组织,这些搞清楚之后,复现的时候就不会跑偏。
2. 核心细节解读:Transformer 如何逼近线性动态系统估计
2.1 先把线性动态系统写成“人话”
很多刚接触这个方向的同学会被状态空间模型吓到,其实核心就两条公式。假设系统有一个隐藏状态向量 (\mathbf{x}_t),你能观测到的输出是 (\mathbf{y}_t),系统的演化满足:
[ \mathbf{x}_{t+1} = \mathbf{A}\mathbf{x}_t + \mathbf{B}\mathbf{u}_t + \mathbf{w}_t ]
[ \mathbf{y}_t = \mathbf{C}\mathbf{x}_t + \mathbf{v}_t ]
其中 (\mathbf{u}_t) 是控制输入,(\mathbf{w}_t) 和 (\mathbf{v}_t) 分别是过程噪声和观测噪声。预测任务就是给定历史观测序列和输入序列,估计当前隐藏状态,并预测未来的 (\mathbf{y})。
这类问题之所以难,难在隐藏状态不可直接观测。要想从观测中恢复状态,系统需要满足可观测性条件。用一个简单类比:你看不到房间里的温度计内部结构,但每隔一段时间能看到温度读数,如果你知道读数和真实温度的映射关系,就能反过来估计真实温度。线性动态系统的辨识就是同时估计“映射关系”和“隐状态演化规律”。
在上下文学习的设置里,模型并不知道 (\mathbf{A}) 和 (\mathbf{C}),它只看到一组轨迹。理想行为是先从示例轨迹中推断出系统参数,再对新轨迹做滤波和预测。这就和纯记忆不同,因为系统参数在推理时是不可见的,模型必须通过上下文完成一次隐式的系统辨识。
2.2 自注意力机制如何模拟状态估计过程
自注意力机制本身做的是加权求和。给定一串 token,每个 token 会通过 Query、Key、Value 三个矩阵和其他 token 交互。因果掩码保证了每个 token 只能看到它之前的 token,这就构成了一种“按时间顺序递推”的结构。你可以把注意力输出看作历史信息的加权平均,而不同的权重分配方式就对应不同的估计算法。
线性动态系统估计中最经典的工具是卡尔曼滤波。卡尔曼滤波每一轮做的事情无非是:根据当前状态估计和系统模型做一步预测,然后根据新观测计算残差和增益,再更新状态估计。这个“预测-更新”的过程是可以被 Transformer 中的层堆叠模拟出来的。一层或多层叠加可以维护一个隐式状态向量,每读入一个新的观测 token,就更新一次这个向量;最后用更新后的状态去做未来多步预测。
更具体一点,注意力头可以扮演不同的角色。某个头负责找与当前观测最相似的过去观测,从而计算残差;另一个头可能负责把当前隐状态估计与预测值拼接;MLP 块则负责执行非线性变换,比如把协方差矩阵的更新映射成可用的增益。多头机制相当于并行运行多个不同的估计算法,最后把它们的结果加权融合。
这也是为什么论文里要讨论“层数”和“宽度”。层数对应递推更新的轮数,宽度对应每个轮次能存储多少统计信息。如果状态维度是 d,那模型至少要能在隐状态里保存 d 维状态估计和 d×d 量级的协方差信息。仅从信息量角度推理,D 应该随 d 增长;仅从递推轮数角度推理,L 应该随上下文长度或迭代次数增长。这就是 Approximation Bound 给出的基本直觉:深度和宽度不能太小,否则表达不出卡尔曼滤波需要的中间量。
2.3 逼近界给出的理论直觉
这类论文里的界通常长成下面这样:模型输出与最优估计之间的误差,被一个关于上下文长度 T、层数 L、宽度 D、状态维度 d 的表达式控制。直观上,它们分别对应了三个资源:上下文长度是样本量,层数是迭代轮数,宽度是存储容量。
| 影响因素 | 直观对应 | 理论表现 |
|---|---|---|
| 上下文长度 T | 样本量越大,辨识越准 | 误差随 T 增加而下降 |
| 层数 L | 递推更新轮数越多,越接近理想算法 | 误差随 L 增加而下降 |
| 宽度 D | 可存储的统计量越多 | 至少需要包含状态和协方差量级 |
| 状态维度 d | 系统越复杂,需要的信息越多 | 下界随 d 增长 |
这个表格看着简单,但实际用处很大。比如你跑实验时发现加大上下文长度收益不大,可能不是样本不够,而是模型深度不够,无法完成足够的递推更新。你发现增加宽度明显提高了效果,说明模型缺少的是存储容量而不是迭代能力。这些判断在没有理论的时候只能靠反复试错,有了边界之后至少能明确排查方向。
另外,标题如果带了 “Generalization” 或类似词,通常还会回答另一个问题:在训练时见过的系统分布上学的经验,能不能泛化到没见过的系统。这对应的是元学习里的“跨任务泛化”问题。这个层面更实际,因为没人会在部署时保证新系统一定跟训练分布完全一致。
3. 实操思路:搭一个最小可复现实验
3.1 任务定义与数据生成
如果你想验证 Transformer 能不能上下文学习线性动态系统,我建议从最小的可控实验开始。第一步是定义系统分布。不要只固定一个系统,那样模型只需记忆曲线形态,不叫上下文学习。要随机生成一批系统,让模型每次面对一个新系统,必须看上下文才能知道它是什么。
生成随机线性动态系统时,最需要注意的就是稳定性。状态转移矩阵 (\mathbf{A}) 的特征值谱半径要小于 1,否则信号会发散,数值不稳定。常见做法是先生成一个随机矩阵,再做谱半径缩放。例如用numpy.random.randn(d, d)生成矩阵,然后计算特征值,取最大模,把矩阵除以这个模再乘一个 0.9 左右的系数,保证谱半径小于 1。
噪声尺度也要控制。过程噪声和观测噪声如果太小,问题会退化成确定性系统,模型只要靠记忆轨迹规律就行;如果太大,信号被噪声淹没,任何方法都难学。经验上噪声标准差设为信号标准差的 0.05 到 0.2 之间,是比较合理的起始范围。
3.2 构造 Prompt 与训练目标
上下文学习的核心是提示格式。一种直接的做法是:每个训练样本由同一个系统的多条短轨迹拼接而成,再加上最后一条要预测的查询轨迹。模型读取整个序列,看到前几条轨迹后,对最后一条轨迹逐点预测未来输出。具体格式可以设计成:
[ [y_{1,1}, y_{1,2}, ..., y_{1,T}, \text{sep}, y_{2,1}, ..., y_{2,T}, \text{sep}, y_{q,1}, ..., y_{q,T}, y_{q,T+1}] ]
其中 sep 是分隔符 token。训练目标是预测 y_{q,T+1} 以及查询轨迹内部每个下一步输出。如果用 teacher forcing,就允许模型看到查询轨迹历史的真实值;如果要模拟真实部署,建议在训练后期加入一部分自由运行(free-run)模式,让模型用自己的预测作为输入。
关于批量组织,有一个重要经验:一个 batch 中每个样本必须来自不同的随机系统。如果多个样本共享同一个系统,模型在训练时可能会偷懒,直接跨样本共享信息。这就破坏了上下文学习的基本设定。理想情况是每个样本一个系统,从分布里独立采样。
3.3 一个 PyTorch 风格的最小骨架
下面给一个简化但能说明结构的 PyTorch 风格骨架。它不是一个完整可调参的项目,但把关键模块列出来了,你可以在上面快速迭代。
import torch import torch.nn as nn class CausalSelfAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.qkv = nn.Linear(d_model, 3 * d_model) self.proj = nn.Linear(d_model, d_model) self.n_heads = n_heads self.d_head = d_model // n_heads def forward(self, x): B, T, D = x.shape qkv = self.qkv(x).reshape(B, T, self.n_heads, 3 * self.d_head) q, k, v = qkv.chunk(3, dim=-1) attn = torch.matmul(q, k.transpose(-1, -2)) / (self.d_head ** 0.5) mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool() attn = attn.masked_fill(mask, float("-inf")) attn = attn.softmax(dim=-1) out = torch.matmul(attn, v) out = out.reshape(B, T, D) return self.proj(out) class TransformerBlock(nn.Module): def __init__(self, d_model, n_heads, d_ff): super().__init__() self.attn = CausalSelfAttention(d_model, n_heads) self.norm1 = nn.LayerNorm(d_model) self.mlp = nn.Sequential( nn.Linear(d_model, d_ff), nn.GELU(), nn.Linear(d_ff, d_model) ) self.norm2 = nn.LayerNorm(d_model) def forward(self, x): x = x + self.attn(self.norm1(x)) x = x + self.mlp(self.norm2(x)) return x class MinimalICLTransformer(nn.Module): def __init__(self, d_input, d_model, n_heads, n_layers, d_ff): super().__init__() self.embed = nn.Linear(d_input + 1, d_model) self.pos = nn.Embedding(512, d_model) self.blocks = nn.ModuleList([ TransformerBlock(d_model, n_heads, d_ff) for _ in range(n_layers) ]) self.head = nn.Linear(d_model, d_input) def forward(self, y, t): B, T, _ = y.shape pos = self.pos(t) x = self.embed(torch.cat([y, t.unsqueeze(-1).float()], dim=-1)) x = x + pos for block in self.blocks: x = block(x) return self.head(x)这个骨架把时间索引直接作为额外输入,而不是只依赖位置编码。原因是线性动态系统对时间顺序极其敏感,明确的时间信息能帮助模型区分“第几步”和“系统时间”。做实验时可以把 d_model 先设成 64,层数设成 3 到 4 层,头数设成 4,先用一维状态跑通,再逐步加复杂度。
3.4 评估方法:别只看一步预测
训练完成后的评估比训练本身更容易出错。最常见的问题是只测一步预测误差,也就是给定真实历史预测下一时刻。这个指标太宽松,因为模型如果退化成简单的自回归模型,也能做到看起来不错。真正有意义的是多步滚动预测:只给定上下文前若干个真实观测,之后就让模型用自己的预测继续生成未来轨迹,不断对比真实值。
建议至少计算两个指标。第一个是标准化均方根误差,把误差除以真实轨迹的标准差,得到相对误差。第二个是“有效预测长度”,也就是预测误差超过某个阈值之前还能走多少步。后者更接近实际应用感受。比如一个模型可能一步误差很小,但三步之后就开始发散,这在控制系统中是完全不可接受的。
还可以设置一个 oracle 对照:如果系统参数已知,卡尔曼滤波可以达到理论最优。Transformer 的预测误差如果显著高于这个 oracle,说明还有提升空间;如果接近它,说明模型在上下文学习中已经学到了接近最优滤波器的行为。这个对照比单纯看绝对误差更有说服力。
4. 复现中的踩坑记录与排查技巧
4.1 数据生成里的稳定性问题
我在刚跑类似实验时踩过一个大坑:生成的系统里混入了一些谱半径刚好大于 1 的矩阵,结果训练到一半损失突然变成 NaN。排查到最后发现不是模型问题,而是某几条轨迹发散,数值溢出。建议生成数据时不仅要做谱半径缩放,还要在轨迹生成后检查最大幅值,把超过设定阈值的样本重新采样。
另一个细节是对齐观测尺度。如果不同系统生成的值范围差别很大,Attention 里的 softmax 很容易饱和,导致几乎所有注意力集中到一两个 token 上。模型退化成只看最近一步,学不到长期依赖。解决办法是在每个样本内部用上下文统计量做标准化,但不能用全量数据统计替代,否则推理时新样本会面临不一致的归一化尺度。
这里提醒一句:数据标准化最好放在模型输入之前,并且在评估时使用完全相同的统计量计算方式。如果训练用了全局均值和方差,推理时不保持一致,模型输出会明显漂移。这类 bug 很隐蔽,但造成的效果差异比模型结构还大。
4.2 上下文长度与示例数量的选择
很多人会把上下文长度设得很大,觉得信息越多越好。但实验上并不是这样。上下文过长时,位置编码可能覆盖不到训练长度,模型在推理时表现崩塌;上下文过短时,系统辨识信息不足,模型只能做到“抄近路”式的短期预测。
我的实验建议是从 T=32 或者 T=64 开始,先把模型跑稳,再逐渐增加到 128 甚至 256。如果训练时随机采样不同长度的上下文,模型对位置编码的依赖会更鲁棒,推理时遇到比训练更长的序列也不至于完全失效。给每条轨迹设置不同长度,相当于一种数据增强。
示例轨迹数量也很关键。对同一个系统,如果只给一条几十步的轨迹,模型很难区分观测噪声和系统动态。给两条或三条轨迹,辨识效果会有明显提升。从信息论角度看,示例越多,系统参数的后验越集中。但示例太多又会超过注意力能有效整合的范围,所以需要找到一个平衡点,这个最好通过小范围网格搜索确定。
4.3 评估指标的陷阱与修正
如果你发现训练损失降得很低,但滚动预测很快发散,不用慌,这几乎是常态。原因在于训练目标通常是每步用真实历史来预测下一步,即 teacher forcing。可是推理时模型看到的是自己的预测,误差会不断累积。训练和推理之间的一致性存在明显缺口。
解决思路有两个。一是训练时随机用一部分自己的预测替换真实输入,模拟推理时的误差传播;二是直接训练多步预测目标,让模型输出未来连续 k 步,然后和真实轨迹计算整体损失。第二种方法收敛更慢,但评测表现通常更稳。建议先用 teacher forcing 训练,再用少量轮次精调成多步一致模式,两者兼顾。
在做评估之前,一定要把“训练时见过的系统”和“没见过的系统”分开。上下文学习最有价值的地方就是对新系统的快速适应,如果只在训练分布内的系统上评测,你测的是泛化还是记忆很难说清。最好设置一组保留系统,它们的参数分布和训练时有细微偏移,用来模拟真实部署中的分布漂移。
4.4 与经典算法的对照基准
强烈建议在实验报告里放一个对照统一的表格,把不同方法在同一组保留系统上的效果列出来。下面是我通常会比较的几个选项:
| 方法 | 是否需要更新参数 | 可解释性 | 滚动预测精度 | 适用场景 |
|---|---|---|---|---|
| 卡尔曼滤波(已知系统) | 否 | 高 | 最优 | 理论下界 |
| 在线递推最小二乘 | 是 | 中 | 较好 | 需要在线自适应 |
| 离线状态空间辨识 | 是 | 高 | 好 | 数据充足 |
| Transformer 上下文学习 | 否 | 低 | 取决于训练分布 | 多系统快速切换 |
这个对照的意义不只是展示 Transformer 好坏,而是快速定位问题。如果 Transformer 连已知系统参数的卡尔曼滤波都差很远,大概率是容量不够;如果差得不远,说明架构表达力到位,问题出在跨系统泛化。这两类问题需要完全不同的优化方向,前者加宽度层数,后者加系统多样化。
5. 后续影响与我的个人判断
5.1 对时间序列基础模型的影响
如果这类工作做扎实了,最直接的影响是时间序列基础模型的玩法会变。现在很多时间序列预测模型还是先在一个大数据集上预训练,然后在目标数据上微调。微调意味着要保存模型副本、准备验证集、跑优化流程,成本并不低。上下文学习一旦成熟,预测模型可以在推理时直接利用目标序列的最近观测做现场适应,连梯度更新都省了。
这种模式对“多客户、多场景、模型统一”的部署非常友好。同一个模型服务很多不同的数据流,每条数据流有自己独特的动态规律;传统做法是为每条数据流维护一个微调副本,成本随数据流数量线性增长。上下文学习则把每个数据流的历史直接丢进上下文,模型自动适配,几乎零边际维护成本。我之前在某个时序项目里做过类似尝试,理论上很诱人,但当时缺少对动态系统的理论支撑,很多效果靠调参堆出来。现在有了近似界的分析,至少能指导上下文长度和模型容量怎么选。
5.2 对控制与自适应系统的影响
控制领域的自适应控制一直想解决一个问题:如何让控制器快速适应环境变化。传统自适应控制依赖实时参数辨识,对噪声和未建模动态很敏感。如果 Transformer 能够在上下文中完成系统辨识并直接输出预测或控制量,那它就可以扮演“在线辨识器+预测器”的角色,而且不更新权重,因此没有灾难性遗忘问题。
这里面有个需要注意的地方:控制任务比单纯预测多了一个闭环反馈环节,模型预测误差会通过控制器影响系统输入,再影响未来状态。所以上下文学习中证明的预测误差界不能直接迁移到闭环控制,需要有闭环稳定性分析。这一点我自己踩过模型预测控制的坑,开环预测看起来很好,一旦闭环就出现振荡。如果未来论文能在闭环设置下给出误差界或稳定性条件,那才是真正可落地的突破。
5.3 个人经验与扩展想法
如果让我给后来者划一条上手的路径,我会建议先跑一维状态系统,不要一上来就挑战高维状态。一维状态的好处是所有统计量退化成标量,协方差更新和增益计算都很好理解,你可以手动画出模型内部注意力权重来对照卡尔曼滤波。当一维系统跑通,再慢慢加维度、加控制输入、加部分可观测性。因为一旦状态维度升高,注意力需要维护的交叉协方差矩阵迅速膨胀,你会立刻看到“上下文长度够、层数不够”的瓶颈。
还有一个我觉得很有潜力的扩展方向:把线性动态系统的上下文学习推广到带未知时滞或部分非线性环节的系统。现实系统很少是纯线性,但很多非线性系统在工作点附近可以线性化。如果模型先通过上下文估计当前工作点,再切换到对应的线性动态预测,这可能成为轻量级在线系统辨识的新范式。个人经验是,这类方向做理论可能很难,但做工程原型非常有意思:先用小模型把数据管线跑通,再看理论研究给什么启示。
最后一个小建议:真遇到效果不佳时,不要急着换模型结构。先把数据生成分布、上下文长度、评估指标三个变量各自做一次简单敏感性分析。我见过太多人一上来就堆大模型,结果训练不稳定,最后连问题是出在容量还是数据都理不清。把控制变量先拆干净,再谈优化,这是这类任务里最实用的心法。