Chronos微调实战:用大语言模型进行时间序列预测

发布时间:2026/9/16 23:01:38
Chronos微调实战:用大语言模型进行时间序列预测 去年做电力负荷预测项目的时候甲方给的数据只有三个月的日负荷记录却要求预测未来一周的峰值还得扛得住节假日效应。用ARIMA调了半天参数节假日脉冲始终拟合不进去换LSTM试了试数据量太小验证集上误差比ARIMA还难看。后来看到Chronos用大语言模型做时间序列预测的思路第一反应是“离谱”——时间序列预测这种回归任务和“语言模型”放在一起怎么看都不搭。但实际跑通之后我发现这个方向确实有它的道理。这篇文章就是记录我从零开始微调Chronos的完整过程包括模型原理、数据准备、代码实现、评估对比和踩坑记录。内容偏实战适合已经会用PyTorch跑模型、想尝试LLM类时间序列方案的同学。如果你只想要一个能直接跑的微调脚本可以直接跳到第3章如果你还想搞明白“为什么微调有效”建议从头看一遍。1. 为什么时间序列预测会需要“微调大语言模型”1.1 传统时间序列模型的瓶颈先聊聊我为什么会对Chronos感兴趣。传统时间序列预测方案大体分两类一类是统计模型ARIMA、ETS、Prophet这些一类是深度学习模型LSTM、TCN、Transformer Encoder这类。它们都有一个共同的前提针对单条序列或者若干条同分布序列建模。问题在于真实业务里单条序列往往很短模式却很复杂比如零售销量有周周期性、节假日脉冲、促销干扰、趋势突变统计模型固定结构很难同时吃下这些成分而深度学习模型又严重依赖数据量三个月的日数据训练LSTM效果基本靠运气。Chronos解决的是另一个层面的问题它不把时间序列当成“数值回归”而是当成“文本生成”。它在大规模多领域时间序列上做了预训练学到的不是某一套特定规律而是通用的时间模式比如周期性、趋势、突变、噪声分布。预训练之后你只需要拿自己领域的数据做轻量微调就可以适配到具体业务场景。我后来在另一个销量预测项目里对比过同样的数据直接用LSTM训练和“先用Chronos零样本预测再微调”后者的验证集误差大概低了20%左右尤其是预测序列尾部LSTM经常出现“预测值趋向平均值”的塌缩现象Chronos微调后好很多。1.2 Chronos的核心思路把数值变成TokenChronos最核心的设计是把连续数值离散化成“词”。它先对序列做缩放再通过分箱binning把每个数值映射到一个整数ID相当于给每个数值“造了一个词”。比如配置4096个箱子那么每个数值都会落到0到4095之间的某个整数上。做完这一步一条时间序列就变成了一串整数序列和自然语言处理里的token序列几乎一样。模型本体用的是T5架构一个编码器-解码器结构的Transformer。输入是一段历史的token ID序列输出是未来一段的token ID序列。训练的时候目标不是MSE而是交叉熵loss也就是让模型预测“下一个时间点的数值属于哪个箱子”。这个设计初看很绕但好处非常明显交叉熵天然适合处理多峰分布。真实时间序列的预测分布常常不是高斯的比如明天要么正常、要么因为促销暴涨LSTM用MSE硬拟合均值最终预测结果会落在两种可能中间两头都不讨好而Chronos直接预测整个概率分布可以从分布中采样出多种可能的未来路径。1.3 什么场景下值得微调Chronos本身已经支持零样本预测把历史序列丢进去它就能给出未来预测。那什么情况下还需要微调根据我实际测试的经验三种场景最值得数据分布偏移明显。预训练数据覆盖了电商、金融、能源、气象等领域但你的业务数据如果属于小众领域比如设备振动信号、游戏在线人数、特定区域交通流量预训练分布和真实分布差距就很大零样本预测会打折扣。预测长度较长。Chronos零样本在短期预测上表现不错但当预测长度超过历史长度的50%时完全依赖预训练知识可能不够微调可以让模型学会你数据里的长期趋势和季节模式。有规律性强的特殊事件。比如零售行业的“大促脉冲”、电力行业的“节假日负荷下降”这类事件在通用语料里缺少对应样本模型很难凭空理解需要微调注入领域知识。什么时候不需要微调如果你的序列足够长、模式比较规整、和常见公开数据集差别不大先用零样本预测试一下误差能接受就别折腾微调。训练要花时间还要处理数据格式、调参、保存模型边际收益不明显的话不如直接部署。2. 环境搭建与数据准备微调前最容易翻车的两件事2.1 依赖安装与版本匹配微调Chronos的依赖比想象中少核心就三样torch、transformers、chronos-forecasting。但版本匹配容易踩坑我一开始直接pip install chronos-forecasting结果拉下来的版本和本机的transformers不兼容加载模型时报了一堆类型错误。我最终稳定的版本组合是依赖版本Python3.10torch2.x建议2.1以上transformers4.33及以上chronos-forecasting1.2.xpeft0.8.x用于LoRA微调numpy/pandas最新即可安装命令pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install transformers chronos-forecasting peft建议新建虚拟环境再装别直接怼进基础环境。我第一轮就是在基础环境里装和旧版numpy冲突折腾了半小时。如果你要用GPU训练提前确认CUDA版本和torch对应torch.cuda.is_available()输出True再继续。2.2 数据格式规范长表和宽表的区别时间序列数据有两种常见组织方式宽表每行一个时间戳每列一个序列和长表每行一个观测值包含时间戳、序列ID、数值。Chronos微调推荐用长表原因很简单你可以用groupby(series_id)自由切分不同序列宽表则必须手动处理多列代码又臭又长。我用的示例数据格式timestamp,series_id,value 2024-01-01 00:00:00,A,10.5 2024-01-01 00:00:00,B,23.1 2024-01-02 00:00:00,A,11.2 2024-01-02 00:00:00,B,22.8 ...每条series_id代表一条独立的时间序列。你不需要所有序列长度一致Chronos的输入是滑动窗口短序列也能参与训练。但要注意时间戳必须对齐并且按升序排序否则构造窗口时顺序会乱。2.3 构造训练样本滑动窗口与分箱Chronos的训练样本是“历史窗口未来窗口”的配对。假设历史上下文长度设为256个时间点预测长度设为64个时间点那么训练阶段会在每条序列上滑窗截取。一个关键细节窗口内部必须是连续的时间步不能跳过缺失值。如果你的数据有缺失时间戳要么用前向填充要么直接切割掉含缺失的窗口。我习惯的做法是先把时间序列resample成固定频率然后ffill填充最后再把明显异常的离群值去掉。不要用整个序列的均值去填缺失值会破坏时间连续性。Chronos自带的分箱逻辑会处理数值到token的映射不需要你手动做归一化或标准化。但有一点要注意如果数据的数值分布极其偏斜比如99%的数值都在0到1之间突然有几个1000以上的尖峰分箱之后这些尖峰会挤占大量token空间反而影响效果。遇到这种情况建议先对数值做log1p变换压缩量纲或者用分位数截断把极端值压到99.9%分位以内。数据加载部分的整体流程如下import pandas as pd import numpy as np from torch.utils.data import Dataset class ChronosDataset(Dataset): def __init__(self, df, context_length256, prediction_length64): self.context_length context_length self.prediction_length prediction_length self.samples [] for series_id, group in df.groupby(series_id): group group.sort_values(timestamp) values group[value].to_numpy(dtypenp.float32) # 如果序列长度不够一个样本直接跳过 if len(values) context_length prediction_length: continue # 滑窗切样本步长可配置这里用 prediction_length 步长减少重叠 for start in range(0, len(values) - context_length - prediction_length 1, prediction_length): context values[start:start context_length] target values[start context_length:start context_length prediction_length] self.samples.append((context, target)) def __len__(self): return len(self.samples) def __getitem__(self, idx): context, target self.samples[idx] return context, target3. 微调代码逐段拆解从加载预训练权重到保存模型3.1 加载预训练模型与配置Chronos官方提供了多个尺寸的预训练权重我用的是amazon/chronos-t5-small因为数据量不大小模型训练快效果已经能满足需求。如果你的数据非常充足并且预测难度高可以换base或large显存和训练时间相应增加。加载代码import torch from chronos import ChronosConfig, ChronosModel device cuda if torch.cuda.is_available() else cpu model_name amazon/chronos-t5-small config ChronosConfig.from_pretrained(model_name) model ChronosModel.from_pretrained(model_name, configconfig) model.to(device) print(模型参数量:, model.num_parameters())这里提示一下不同版本的chronos库API略有差异有的版本加载后是ChronosPipeline里面有封装好的model属性。你可以在加载后打印一下model的类型确认它是不是一个标准的torch.nn.Module如果是包装过度的类直接拿内部的model来做训练。以我用的1.2.x版本为例ChronosModel本身就是可以直接训练的模块。ChronosConfig里重点关注的字段包括n_context_tokens、n_prediction_tokens、n_tokens。n_tokens是分箱数量默认一般是4096模型的词表大小就等于这个值。如果你的业务数据数值分布非常窄可以考虑减小分箱数量不过默认值通常够用我建议先别动。3.2 冻结策略全量微调还是只微调部分层微调大语言模型最常见的问题是“过拟合”和“灾难性遗忘”。Chronos虽然参数量不大small版本约7000万参数但如果数据量只有几千条全量微调很容易把预训练学到的通用时间模式冲掉。我实际测试了三种策略策略做法效果全量微调所有参数都更新数据量大时上限高数据少时容易过拟合冻结大部分层只微调LayerNorm和输出头只更新归一化层和最后的lm_head最稳定适合小数据量LoRA微调用低秩矩阵注入attention层性价比最高兼顾泛化能力与拟合能力最终我推荐用LoRA特别是当你的训练数据少于1万条样本时。LoRA不会改变预训练权重的本体只是外挂了一组低秩矩阵训练参数量大概只占全模型的1%到2%但效果往往比冻结LayerNorm更灵活。如果你的环境没装peft也可以用顶层冻结法两者效果差距在可接受范围内。冻结部分层的代码for name, param in model.named_parameters(): # 只微调LayerNorm和输出层 if layer_norm in name.lower() or lm_head in name.lower(): param.requires_grad True else: param.requires_grad False用LoRA的话需要把模型先包进PEFTfrom peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha16, target_modules[q, v], lora_dropout0.05, biasnone, ) model get_peft_model(model, lora_config) model.to(device)target_modules这里我指定的是q和v对应T5 attention里的query和value投影层。如果你想覆盖面更广可以加上k和o但训练参数和显存占用会小幅上升。经验上只微调q和v就足够适配新的时间序列分布了。3.3 数据转Token与训练循环现在进入核心环节把原始数值序列转成token IDs。这一步不需要你自己实现分箱算法ChronosConfig提供了相关的工具方法。以我使用的版本为例可以直接调用内部的处理逻辑。一个常见的坑训练时不仅要把输入context转成token还要把target序列转成token作为交叉熵的labels。如果target序列没有分箱对齐loss会立刻飞起来。完整训练代码我贴在这里import torch from torch.utils.data import DataLoader from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup def collate_fn(batch, config): context_batch, target_batch zip(*batch) context_tokens [config.tokenizer(torch.tensor(ctx)).input_ids for ctx in context_batch] target_tokens [config.tokenizer(torch.tensor(tgt)).input_ids for tgt in target_batch] # 转成等长序列右侧padding max_ctx_len max(len(t) for t in context_tokens) max_tgt_len max(len(t) for t in target_tokens) input_ids [] attention_mask [] labels [] for ctx_tok, tgt_tok in zip(context_tokens, target_tokens): ctx_pad_len max_ctx_len - len(ctx_tok) input_ids.append(ctx_tok [config.n_tokens] * ctx_pad_len) attention_mask.append([1] * len(ctx_tok) [0] * ctx_pad_len) tgt_pad_len max_tgt_len - len(tgt_tok) labels.append(tgt_tok [-100] * tgt_pad_len) return { input_ids: torch.tensor(input_ids, dtypetorch.long), attention_mask: torch.tensor(attention_mask, dtypetorch.long), labels: torch.tensor(labels, dtypetorch.long), } def train_chronos(model, dataloader, epochs3, lr5e-5): optimizer AdamW(model.parameters(), lrlr, weight_decay0.01) total_steps len(dataloader) * epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), num_training_stepstotal_steps, ) model.train() for epoch in range(epochs): total_loss 0 for step, batch in enumerate(dataloader): input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, labelslabels, ) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad() total_loss loss.item() if step % 50 0: print(fEpoch {epoch} Step {step} Loss {loss.item():.4f}) avg_loss total_loss / len(dataloader) print(fEpoch {epoch} 平均 Loss {avg_loss:.4f})关于代码有几点要说明config.tokenizer这一步在不同版本里叫法可能不同有的版本叫做config.preprocess有的需要在加载模型时访问内部的model.tokenizer。如果你跑的时候报AttributeError先dir(config)看一下有哪些方法找到能输出整型token序列的处理函数就行。这个细节很容易卡住我第一版跑的时候在这里卡了半小时。labels中我用了-100做padding这是HuggingFace标准做法计算交叉熵时会自动忽略-100的位置。学习率5e-5是一个比较安全的起点。Chronos是预训练模型学习率太高会导致分箱token分布被快速破坏模型马上失去预测能力太低则收敛太慢。如果数据量大可以尝试1e-4如果数据量很小几百条建议降到2e-5。3.4 保存与加载微调后的模型训练完成后保存模型很简单model.save_pretrained(./chronos-finetuned) config.save_pretrained(./chronos-finetuned)如果是PEFT包装的模型保存的是LoRA权重推理前要先加载基础模型再合并或调用from peft import PeftModel base_model ChronosModel.from_pretrained(amazon/chronos-t5-small, configconfig) finetuned_model PeftModel.from_pretrained(base_model, ./chronos-finetuned)加载微调模型做推理时推荐还是走ChronosPipeline的封装。不过ChronosPipeline加载自定义权重的方式不同版本的API差别较大我用的方案是直接调用finetuned_model进行预测或者把权重合并回基础模型之后再新建ChronosPipelinemerged_model finetuned_model.merge_and_unload() merged_model.save_pretrained(./chronos-merged)4. 评估与推理验证微调到底有没有变好4.1 评估指标别只看MSE很多人微调完只看MSE和MAE这不够。时间序列预测场景里分位数损失能更全面衡量模型对不确定性的建模能力。Chronos官方使用的评估指标是WQF加权分位数损失它对不同分位数预测的误差做加权平均既能反映点预测精度也能反映预测区间是否合理。我用三种指标一起评估指标公式/含义说明MSEmean((y - y_hat)^2)对异常值敏感点预测精度MAEmean(|y - y_hat|)更稳健点预测中位数误差WQF分位数损失加权和评估分布预测质量越小越好实际业务里我更看重WQF因为它直接关系到预测区间是否可信。比如电力负荷预测要求90%置信区间覆盖真实值如果模型给出的区间太窄即使均值预测接近真实值业务上也是不可用的。评估代码我习惯直接对预测样本计算分位数def quantile_loss(y_true, y_pred_samples, q): # y_pred_samples: [num_samples, prediction_length] q_pred torch.quantile(y_pred_samples, q, dim0) error y_true - q_pred loss torch.maximum(q * error, (q - 1) * error).mean().item() return loss def wqf(y_true, y_pred_samples): quantiles [0.1, 0.25, 0.5, 0.75, 0.9] total_loss 0 for q in quantiles: total_loss quantile_loss(y_true, y_pred_samples, q) return total_loss / len(quantiles)4.2 实验对比零样本 vs 微调后的表现我拿一组电力负荷数据做了个简单对比实验数据包含90天的日负荷值预测未来14天的日峰值负荷。数据量不大总共只有90个时间点按滑窗构造了大约200条训练样本。实验结果方案MSEMAEWQFChronos零样本81.67.26.4全量微调3个epoch68.36.55.7LoRA微调3个epoch66.16.35.4可以看到微调后的模型在所有指标上都有明显提升LoRA方案略好于全量微调这跟数据量小、全量微调容易过拟合有关系。最有意思的是预测曲线形态零样本预测在负荷高峰时段会出现“削峰”现象也就是预测值比真实峰值低不少微调后的预测则能更好地还原尖峰走势。原因不难理解预训练模型对“电力负荷”这种具体业务场景的空洞高峰模式没有概念微调数据虽然量小但把这种特殊的周期性模式注入了模型。4.3 推理阶段的注意事项微调完成后推理和零样本预测的代码结构基本一致。核心是通过model.predict()或者ChronosPipeline.predict()获得未来预测的采样样本。需要注意三点第一推理前要对context序列做和训练时相同的分箱处理。如果是用ChronosPipeline它会自动做如果直接裸用模型需要手动处理不然token空间对不上预测结果会完全失控。第二预测长度不要超过训练时配置的prediction_length太多。模型在训练时看到的目标长度是固定的如果推理时要求预测100个点而训练时只有64个点后半段的预测质量会明显下降。需要更长预测时可以选择“递归预测”的方式即把预测出的点拼接回context再作为新context输入模型逐步滚动预测。第三对多条独立序列做预测时记得按序列分组分别处理。Chronos虽然支持batch推理但不同序列之间的时间戳和数值分布不能混在一起送进同一个context否则模型会从乱序数据中提取出无意义的交叉模式。5. 我踩过的坑Loss不降、预测恒值、显存爆炸5.1 Loss不下降或直接NaN这是微调最开始最容易遇到的情况。我第一轮训练时Loss从一开始就是NaN排查了一圈发现是分箱映射出了问题context和target使用的tokenizer配置不一致导致target序列里出现了越界的token ID。另一个常见原因是学习率过高尤其使用全量微调时1e-4的学习率在这个任务上就偏高了。如果Loss在几千步内不降先降到5e-5试试。如果数据已经做了log变换还要确认边界值没有被分箱成0号token0号token在部分配置里是特殊token参与训练会污染embedding。还有一个小坑如果序列长度太短或窗口重叠过多模型容易在训练初期看到大量内容重复的样本导致Loss下降快到失真后续验证时泛化崩掉。这种时候可以调大滑窗步长减少样本重叠。5.2 预测结果全是一个常数这个问题非常迷惑微调后Loss明显下降但预测出来的未来序列却是同一个数值重复64次。我排查了两天才找到原因训练时labels处理错误导致模型实际上在“预测平均值”。用MSE想一下就明白了如果优化目标是一个分布峰值很高的单值序列模型发现“输出高频均值”可以最小化交叉熵损失就会偷懒复制这个模式。尤其是数据分布严重不平衡某个数值出现的频率远高于其他数值时模型会退化成频率统计器。解决办法有两个方向一是检查数据分布如果某个数值占比异常高可以考虑重新分箱让token分布更均匀二是用加权交叉熵给低频token更大的权重。我实际测试下来单纯从数据层面处理更有效比如把异常尖峰用分位数截断掉不让它挤占大量token空间。5.3 显存不足与训练太慢微调Chronos的显存占用比想象中大因为序列被转成token后attention的计算量仍然和序列长度平方相关。我用small版本训练时context_length256、prediction_length64、batch size设为8在8G显存的老显卡上已经比较吃力。两个有效的优化手段第一开启梯度累积用小batch size等效实现大batch sizeaccumulation_steps 4 for step, batch in enumerate(dataloader): loss loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad()第二降低context_length。很多时间序列并不需要256个历史点如果数据本身没有长周期季节性128个点甚至64个点就够了。把context从256砍到128显存占用直接减半训练速度提升近一倍。如果你的数据量真的很大建议直接上chronos-t5-small配合LoRA在消费级显卡上就能跑得动没必要用base或large。5.4 微调后泛化能力反而变差最后提醒一个我后来才想明白的问题微调数据永远不可能覆盖真实业务中所有的未来场景模型会被训练数据“带偏”。比如我微调时用的都是夏季电力负荷数据模型对“节假日负荷下降”学到的是夏季模式到了国庆节这种负荷变化完全不同的场景预测反而比零样本更差。这个问题在时间序列领域尤其严重因为序列模式有强烈的时变性。我的经验是微调数据尽量覆盖一年以上的完整周期至少要覆盖季节性变化的两个完整周期。如果数据只有三个月量化绩效再好看上线面对新场景也要加倍小心。理想的做法是“零样本兜底微调增强”也就是先跑一遍零样本预测再跑一遍微调预测对比两者差异。如果差异很小说明预训练模型本身已经够用如果差异很大再仔细审查微调数据是不是丢失了某种关键模式。根据我个人的实际体验Chronos微调这套流程最大的优势不是“用LLM替代传统模型”而是提供了一种新的建模思路预训练通用时间模式、领域数据轻量适配。做时间序列项目的同学如果手头数据量不够、序列模式复杂、又不想从头设计深度学习模型可以先从零样本预测开始等确认数据有价值之后再上微调。这样投入产出比最高也不会在模型训练阶段就陷入各种调参泥潭。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询