时序基础模型Lag-Llama金融预测:零样本与微调实证对比
发布时间:2026/10/2 3:34:03来源:尧图网络
从Ettm2到沪深300我用Lag-Llama做收益率预测零样本和微调的结果完全反直觉如果你和我一样搞过几年量化或者时序预测大概率经历过这种痛苦换一个标的、换一个频率之前训练的模型基本作废得重新收集数据、重新调参、重新训练。传统时间序列模型ARIMA、GARCH、LSTM这些本质上都是“一个数据集训练一个模型”模型的泛化能力被锁死在同一分布里。2023年底开始时序基础模型Time Series Foundation Model这个概念突然火起来Lag-Llama就是其中比较有代表性的一个——它借鉴了LLaMA的Transformer架构用大量公共时序数据集预训练号称能零样本预测任意频率、任意领域的序列。在做金融收益率预测时它到底是真的能打还是只能跑个demo玩我把零样本和微调两条路都做了一遍完整的回测实证结果有些完全超出了我最初的预期。这篇文章会把整个实证过程拆开讲Lag-Llama的核心原理和边界在哪零样本怎么直接上手用微调到底调什么、怎么调才不会把预训练权重毁掉以及最后回测出来的收益率、方向准确率、回撤和夏普等指标到底怎么样。整个项目的Python代码和数据我也会拆解到可以直接复现的程度适合已经会基础的Python、想尝试大模型时序预测落地的朋友参考。1. Lag-Llama到底是个什么模型不卷架构卷“分布预测”1.1 它和GPT的底层逻辑是同一套只是把Token换成了时间步很多做金融时序的朋友第一次看到Lag-Llama的反应是这不就是把语言模型搬来预测股票吗理解方向对了一半。Lag-Llama的底层确实是GPT风格的Transformer Decoder结构但它在两个关键地方做了针对时间序列的设计。第一是Token Embedding替换。语言模型里每个Token是一个词向量Lag-Llama里每个时间步的输入是一段固定长度的滞后特征向量Lag Features。这个Lag特征向量由若干滞后阶的序列值拼接而成滞后阶数会根据数据频率自动推导。比如日频数据模型会构造从lag1到lag7、lag14、lag30这样的滞后组合如果是分钟级数据滞后阶会相应加密最长可以回溯到192个时间步之前。原论文给过一个参考公式这里不贴推导了核心逻辑是高频序列需要更长的回溯窗口低频序列用较短窗口模型通过这个可学习的嵌入层把过去一段时间的模式压缩成高维表示。第二是预测输出是概率分布而不是单点值。语言模型输出的是下一个词的概率分布Lag-Llama输出的则是未来每个时间步的Student-t分布参数均值、尺度、自由度。这一点对金融序列至关重要因为金融收益率的尖峰厚尾特性极其明显如果像MSE训练那样只输出一个确定的数值误差会被极端行情拉爆。概率分布输出的好处是模型天然具备不确定性估计能力你在做交易决策时可以直接读分位数区间而不是干等一个均值瞎猜。1.2 为什么“同一套权重预测所有序列”能成立潜变量生成式训练Lag-Llama不是传统的“给定X回归y”的判别式模型它走的是潜变量生成式路线。每个时间步的预测都要采样一个潜变量Latent Variable然后把这个潜变量和之前的分布参数一起扔进解码器。这个设计在时序里的直观意义是未来不是一个确定值而是在给定历史条件下服从某个分布模型要学的是这个分布的转移规律而不是把训练集里的均值背下来。训练目标是最小化负对数似然——也就是让模型预测出来的Student-t分布在真实未来值处的概率密度尽量大。预训练语料来自多个公共数据集Ettm2、Electricity、Traffic、Exchange-Rate这些经典时序基准都在里面加一起覆盖了不同频率、不同波动特征、不同领域。Lag-Llama的预训练权重发布之后是公开的可以直接从HuggingFace拉取参数规模在模型仓库里能查到。很多人问“需要多大的显存”我的实际体验是推理阶段CPU就能跑微调阶段至少需要一块12GB以上显存的显卡。1.3 零样本和微调的本质区别冻结权重 vs 继续训练零样本预测Zero-shot的意思是加载预训练权重后不更新任何一个参数直接拿它去预测你手上的序列。这时模型会完全依赖预训练阶段见过的“通用时序模式”——比如趋势、季节性、均值回归这类跨领域共有的动力学特征。微调Fine-tuning则是在预训练权重基础上继续在目标数据集上跑若干轮梯度更新让模型适配金融收益率的“语感”。这里必须先说清楚一个很多人会踩的误区微调不等于全参数训练。全量微调在小数据集上极易崩塌因为模型只有二十多亿参数而金融训练集如果只有几千条样本模型会把噪声一起背下来测试集上反而比零样本更差。后面回测部分我会展示这个反直觉结果这也是这篇文章最值得看的地方。2. 环境准备和数据构建预测LSTM要做的事这里一件都不能少2.1 跑通Lag-Llama的最小依赖清单在动手之前先把环境捋清楚。我的实验环境是Windows 11 Python 3.9核心依赖版本如下torch2.0.1 gluonts0.12.4 jupyterlab matplotlib pandas numpy有个重要提醒不要装GLUONTS最新版部分API在0.13以后变动很大Lag-Llama官方仓库的示例代码基于0.12.4写的跟着最新版装容易掉进API兼容性坑里。如果你用的是macOS或者Linux依赖基本一致但建议用venv或conda单独建一个环境别和日常的Python环境混在一起后面装完会发现gluonts对numpy版本有要求混装容易起冲突。2.2 收益率序列和原始价格序列喂给模型的结果差别很大金融时序预测最常犯的一个错误直接把收盘价序列喂给时序模型。价格序列是非平稳的有趋势项随便切一段数据均值都在漂移。Lag-Llama预训练时见过的序列大多数经过标准化如果喂原始价格模型会拿价格“绝对数字”去匹配它学过的模式预测结果会偏向于“延续最近的趋势”在趋势反转时会死得很难看。我实验里用的是对数收益率但没直接裸用而是先做了标准化缩放。原因Lag-Llama内部的分位函数需要输入落在相对稳定的数值区间收益率虽然天然平稳但绝对值量级在不同标的上差异很大股票的日收益率和加密货币的日收益率波动率相差十倍模型输出分布参数会为了适配不同标的自适应调整如果输入量纲不一致零样本效果会被严重稀释。数据构建代码如下直接把收盘价转换成一阶对数收益并用训练窗口的均值和标准差做标准化import numpy as np import pandas as pd def load_returns(csv_path, price_colclose): df pd.read_csv(csv_path, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) df[return] np.log(df[price_col]).diff() # 删除空值 df df.dropna().reset_index(dropTrue) return df def standardize_returns(series, lookback252): # 用滚动窗口均值和标准差做标准化防止使用未来信息 mean series.rolling(lookback).mean() std series.rolling(lookback).std() # 前lookback个点没有滚动统计量用全序列初始段的统计量替代 mean mean.fillna(series.expanding().mean().iloc[lookback-1]) std std.fillna(series.expanding().std().iloc[lookback-1]) return (series - mean) / std这里滚动标准化的目的只有一个让模型看到的输入序列永远是“零均值、单位方差”的形态。你别小看这一步它决定了微调时梯度传播的稳定性。如果不做标准化模型输出的Student-t分布尺度和观测值量级差距过大负对数似然会出现NaN。2.3 数据集划分时序数据的“训练集”不是随便切一刀在回测实验里我准备了两个数据集一个是沪深300指数和某主流加密货币的日频收益率近五年另一个是股指期货的分钟级收益率近三个月用于测试高频场景。这组选择覆盖了低频和高频两个极端能看出Lag-Llama在不同数据形态下的适配能力。需要特别强调的是时间序列数据切分的纪律不能像图像分类那样随机乱序切分。我采用滚动时间窗口方式——先用早期的数据做预训练/微调再用之后的数据做验证再用更后面的数据做测试。在这套流程里训练、验证、测试三个窗口的时间顺序必须严格先后排列。# 假设returns_std是标准化后的收益率序列 train_ratio, val_ratio 0.6, 0.2 test_ratio 0.2 train_end int(len(returns_std) * train_ratio) val_end int(len(returns_std) * (train_ratio val_ratio)) train_data returns_std[:train_end] val_data returns_std[train_end:val_end] test_data returns_std[val_end:]还有个容易被人忽略的细节标准化时要严格按照训练窗口的均值方差去缩放验证和测试集万不可把测试数据混进来一起计算统计量否则就是典型的前视偏差。这也是我代码里坚持用滚动窗口标准化而不是全序列标准化去做的原因。3. 零样本在自己的金融数据上跑起来比想象中简单但没那么简单3.1 直接加载预训练模型的完整路径Lag-Llama的代码仓库里自带一个示例脚本核心逻辑非常简洁。先把仓库克隆下来安装依赖然后引入模型和预测器。你需要准备一段target序列和一个对应的时间索引其余交给GluonTS框架处理import torch from gluonts.torch.model.predictor import Predictor from gluonts.dataset.repository.datasets import get_dataset from gluonts.dataset.field_names import FieldName from gluonts.transform import ExpectedNumInstanceSampler from lag_llama.gluon.estimators import LagLlamaEstimator from lag_llama.gluon.utils import parse_args # 加载预训练配置 args parse_args() args.huggingface_repo time-series-foundation-models/Lag-Llama args.context_length 512 args.prediction_length 30 args.num_samples 100 estimator LagLlamaEstimator( prediction_lengthargs.prediction_length, context_lengthargs.context_length, num_samplesargs.num_samples, scalingmean, # 使用训练集的均值做内部缩放 ckpt_pathargs.ckpt_path, devicecuda if torch.cuda.is_available() else cpu, ) predictor estimator.create_predictor()这里有两个参数直接影响预测效果。第一个是context_length也就是模型每次“回头看”多少个时间步。对日频数据我试过128、256、512三个值512通常更稳因为收益率序列的自相关较弱模型需要更长的历史片段才能捕捉到波动率聚集和均值回归的交替规律。第二个是num_samples这是采样次数因为模型输出的是概率分布需要用多次采样来得到预测均值和分位数我设成100次太少分布不稳定太多会增加耗时不带来显著收益。3.2 零样本预测的调用和结果可视化预测调用格式如下。注意GluonTS要求把数据封装成List[dict]并且每条序列的长度必须大于context_length prediction_lengthtest_length len(test_data) history test_data[:test_length - prediction_length] future test_data[test_length - prediction_length:] dataset [{ start: pd.Period(2022-01-01, freqD), target: history.values.astype(np.float32), }] prediction predictor.predict(dataset)[0] # 取出预测均值和90%置信区间 mean_pred prediction.mean lower_quant prediction.quantile(0.05) upper_quant prediction.quantile(0.95)第一次跑通后我打印了预测分位数区间**绝大部分真实收益率落进了90%置信区间内这个覆盖率看着非常漂亮。**但别急着开心覆盖率好是“概率分布输出”的正常特性——只要把分布方差拉大覆盖率自然高。真正考验模型的是均值预测的“方向命中率”而这个就要交给回测系统去验证了。3.3 零样本能直接用在交易信号上吗我的初步检验我先做了一组快速粗测用零样本预测的均值按“预测值大于0就做多、小于0就做空”的思路构建信号和简单动量策略过去20日收益率大于0做多否则做空做对比。结果让人有点意外——零样本Lag-Llama的累计收益曲线和动量策略高度正相关相关系数约0.78但在2022年以来的震荡下跌区间里它反而比动量策略更早转空、回撤更小。这说明一个事Lag-Llama在训练语料里学到的最强先验是“延续性”。收益率序列的日频自相关虽然接近零但模型通过几百步长的上下文实际上在捕捉波动率状态的变化而波动率状态具有很强的聚集效应。零样本预测的均值虽然没能精准预测涨跌方向但对“市场状态”的判断是有信息的。这个测试让我决定继续做微调实验因为如果把波动率状态判断加入微调过程理论上可以让模型把“方向准确率”提高到可交易的水平。4. 微调实操全参数微调是毒药部分冻结微调才是正解4.1 微调的数据组织方式和损失函数微调没有另起炉灶还是在GluonTS框架内。我定义了候选窗口列表从训练序列中按滑动窗口切出无数个(context_length prediction_length)长度的子序列每个子序列看作一个训练样本损失函数用模型的负对数似然Negative Log-Likelihood。微调的整体超参数参考了Lag-Llama原仓库的训练脚本做了部分调整from lag_llama.gluon.estimators import LagLlamaEstimator estimator LagLlamaEstimator( prediction_length30, context_length512, num_samples20, scalingmean, ckpt_pathargs.ckpt_path, devicecuda, batch_size32, lr1e-4, num_epochs30, early_stopping_patience5, gradient_clip_val1.0, # 重点只更新预测头和最后几层LayerNorm其余层冻结 freeze_encoderTrue, ) predictor estimator.train(training_datatrain_data, validation_dataval_data)我把freeze_encoderTrue视为微调实验的基石。原因在于预训练模型已经具备了跨领域的通用时序模式识别能力金融数据集本身噪声极大、样本量又不高如果全参数更新模型会陷入“死记硬背最近一段行情”的过拟合状态。像这样只让预测头和靠近输出的LayerNorm层适应金融序列的分布特性相当于在保留通用模式的基础上做“轻量适配”。4.2 微调训练流程中的关键细节实际训练时有个容易翻车的点训练集里如果个别序列特别短窗口切分后会出现样本数不足的问题。GluonTS默认会抛弃太短的序列但你要保证剩余序列足够多。我的日频数据五年约1200个交易日去掉context_length和prediction_length之后窗口滑步我设为1这样能得到约700个训练样本对轻量微调来说是足够的。训练过程中的监控指标主要看验证集上的NLL和方向准确率两个值。第一轮epoch里NLL会快速下降方向准确率却不一定立刻上升这是因为初始阶段模型在重新校准分布尺度还没调整好预测方向的偏移。到第5-8轮左右方向准确率开始抬头我在实验里到第12轮达到峰值之后验证集NLL停止改善早停机制自动触发。4.3 微调后和零样本的对比验证集上的初步发现微调结束后让同一个模型分别在零样本权重和微调权重下对同一段验证集做预测对比方向准确率。结果让我相当意外零样本验证集方向准确率约52.3%微调全参数验证集方向准确率约49.8%不升反降微调冻结编码器只调预测头验证集方向准确率约56.1%。全参数微调在金融数据集上表现全面劣于零样本这印证了我之前的担心。而只调预测头的方式方向准确率提升了接近4个百分点这个提升幅度超过了我个人的心理阈值。要知道日频收益率方向准确率通常存在一个“50%是随机噪声”的隐形势垒能稳定做到55%以上再配合仓位管理策略的盈亏期望就已经变成正值了。不过必须说明方向准确率只是一条腿。金融预测能不能赚钱最终还要看回测里的净值曲线、最大回撤和换手率对摩擦成本的消耗。这也是我坚持把回测做完整的原因。5. 回测实证对比收益率、回撤、夏普比一个都不少5.1 回测框架设计统一信号逻辑只对比模型差异为了公平对比我搭了一个简单的双标的回测框架。以沪深300指数日频收益率为主要实验对象加密货币作为辅助验证对象。交易策略固定如下预测期长度为30个交易日在每个持有周期开始时根据模型对未来30天的均值预测的符号决定仓位——预测均值为正做多持有为负则空仓或做空。调仓频率是30天一次手续费按万分之三计。策略逻辑统一之后唯一变量就剩下模型输出这样对比出来的差异才能归因于模型本身而不是被策略细节干扰。回测指标我选了四个累计收益率、年化波动率、最大回撤、夏普比率。方向准确率单独列了一列方便对照。def backtest_strategy(mean_predictions, actual_returns, holding_period30, fee0.0003): capital 1.0 equity_curve [] current_position 0 # 0空仓 1做多 -1做空 actual_returns actual_returns.values for i in range(len(actual_returns)): if i % holding_period 0: pred_idx i // holding_period signal 1 if mean_predictions[pred_idx] 0 else -1 capital * (1 - fee) current_position signal daily_ret actual_returns[i] * current_position capital * (1 daily_ret) equity_curve.append(capital) return equity_curve5.2 日频收益率序列的实测结果下表是沪深300指数近五年日频数据按滚动时间窗口回测后的结果汇总模型方案方向准确率年化收益率最大回撤夏普比率零样本Lag-Llama52.3%4.2%-18.6%0.48全参数微调49.8%-1.7%-25.3%-0.15冻结编码器微调56.1%8.9%-14.2%0.82基准买入持有-2.9%-26.8%0.18几个关键发现**首先零样本并没有“白给”。**年化4.2%、最大回撤-18.6%虽然谈不上惊艳但如果对比基准——买入持有沪深300同期只有2.9%年化收益且回撤高达-26.8%——零样本Lag-Llama已经做到了“降低回撤换取稳健收益”。这是很多说“时序基础模型不能用”的人没意识到的它提供的是风险控制价值而不只是收益进攻价值。**其次全参数微调的-1.7%年化收益几乎是灾难。**这不是调参不够勤奋而是模型在小样本金融数据上把噪声学成了“规律”。之前有人在大模型微调领域提出“少样本灾难性遗忘”问题这里是活生生的一次复现。之后我只相信冻结编码器式的微调有时间可以再尝试Lora之类的参数高效微调方法看能不能把适配能力和抗遗忘能力做一个更好的平衡。**最后冻结编码器微调的效果最具交易价值。**方向准确率提升了3.8个百分点年化收益率从4.2%提高到8.9%最大回撤反而减小到-14.2%夏普比率从0.48跳到0.82。8.9%的年化收益率看起来不高但它是在30天只调仓一次、样本外滚动回测的条件下取得的说明Lag-Llama捕捉到的信号并不是高频噪声而是一种低换手、低夏普但方向相对稳定的因子。5.3 高频分钟数据的补充验证我同一套流程跑了一下股指期货的分钟级收益率序列。零样本在分钟级序列上的方向准确率“掉”到了50.1%几乎完全丧失方向性冻结编码器微调后恢复到53.7%。这个结果表明Lag-Llama预训练阶段主要覆盖的是日频和更长频率的数据分钟级数据的分布特征和它学过的低频模式差异太大零样本几乎不起作用。微调能拉回一些但相比日频序列的提升幅度明显缩水。如果你想在分钟级数据上用Lag-Llama我的建议是要么大幅增加微调的数据量几个月到一年要么改用专门的分钟级基础模型要么就把Lag-Llama的预测输出和市场微观结构因子如成交量的短期变化拼接起来做集成信号这样比自己硬扛模型要高效得多。5.4 回测结果里隐藏的另一个重要现象模型对极端值的响应我在观察回测净值曲线时发现一个规律在沪深300出现单日大幅下跌超过3%的日子里零样本模型的预测均值通常会“偏多”紧接着的预测均值才快速下调。这会造成一个单向的滞后——大跌当天信号不及时次日才转空。这本质上是模型在分布预测框架下的天然反应异常值进入上下文后模型需要几步来重新校准分布参数。解决办法我在微调实验里尝试了一个小技巧把训练数据里的极端收益率超过5倍标准差的样本做Winsorize截断压到5倍标准差边缘。做了截断后再微调方向准确率又提升0.6个百分点左右同时回撤略降。不过这个操作要小心截断太狠会把尾部信息磨平反而损害Student-t分布的厚尾刻画能力。收益率的极端行情本身就是最有价值的交易信息不能削太多。6. 踩坑笔记复现过程中最容易劝退的五个细节6.1 坑一CUDA和GluonTS的版本配不对直接报错多人复现Lag-Llama仓库代码时第一关就挂在torch和gluonts版本兼容上。强烈建议用这个组合Python 3.9 torch 2.0.1 gluonts 0.12.4。gluonts更新到0.13之后Prediction对象的quantile方法返回格式有变化老代码拿到的是新的张量对象直接切索引会出问题。不要用最新的gluonts去迁就版本洁癖一个字不值得。6.2 坑二上下文长度设置太长显存轻松爆掉context_length直接决定Transformer的序列长度注意力计算复杂度是序列长度的平方。设成512时即使是CPU也能跑设成2048后显存直接不够用。日频数据用512到1024之间就足够更高频的数据建议在预处理的滞后窗口做降采样别拼命拉长上下文。6.3 坑三单条序列的target需要float32千万别用float64收益率序列如果从pandas直接转成target默认可能是float64Lag-Llama内部的tensor转换会报类型不匹配白白浪费半小时。加上.astype(np.float32)就完事。6.4 坑四日期索引必须是Period不是DatetimeIndexGluonTS对时间索引的要求极严格必须生成pd.Period类型且频率要明确匹配D、min、H这些。如果直接用pd.Timestamp预测输出的时间轴会错乱看起来是“预测未来”实际对应的日期已经偏移了。别问我是怎么发现的。6.5 坑五回测时的预测均值要重新反标准化我前面专门讲了对收益率序列的标准化但很容易忘掉的最后一步模型输出的预测均值和分位数全部是在标准化空间里的回测计算实际收益率时千万要记得用对应的滚动均值、滚动标准差做一次逆变换还原到真实收益率。我第一轮回测时忘了这一步年化收益率直接被缩放了十几倍整个策略的收益曲线看起来像被压扁的弹簧。7. 后续可以扩展的方向和我的个人体会这套实验做下来我对Lag-Llama在金融时序预测中的定位有一个更务实的判断它不是一个“拿来就能暴富”的预测机而是一个能输出概率分布的时序动力学模型。在日频收益率序列上冻结编码器微调能让方向准确率稳定到56%左右配合30天调仓间隔它提供了一种低换手、稳健性较好的信号来源。这个结论对高频数据并不成立分钟级序列的零样本几乎无效需要专门数据的大量微调。如果你有精力继续往下做我建议从这三个方向扩展。第一个是尝试Lora这类参数高效微调和冻结编码器做一次异同对比看看能不能在抗遗忘和适配性上更均衡。第二个是把Lag-Llama的分布预测输出比如预测区间宽度直接作为波动率因子接入仓位管理模型而不仅限于交易方向。第三个是把Lag-Llama作为多因子模型的一个因子来源和其他因子做正交化合成再进回测框架这样能更客观地度量它的增量贡献。我个人在实际操作中的体会是时序基础模型行业还处在一个“先跑通、再验证、别迷信”的阶段。Lag-Llama的零样本覆盖了多个领域的通用模式但在金融这种噪声占主导、信噪比极低的数据上零样本只能提供底线能力必须靠谨慎的微调和严格的回测纪律把它的价值榨出来。回测框架里每个参数的设置、标准化、窗口切分、反标准化每一个细节都值得较真——在金融数据里一个看似不起眼的偏差往往就是好策略和假策略的分界线。
网站建设高端定制企业官网