做交通预测和路网仿真这行的老手都有个共识:真正卡住项目的往往不是模型不够深,而是数据不够用、也不敢用。一条路段装了检测器,坏点、断线、检修、施工,一年下来能用的完整记录可能就那几个月;跨城市、跨区域的数据想拿到手,流程比调模型长得多。这几年文本到交通态势生成这个方向被反复提起,本质就是想把"用一句话描述场景,直接产出一段可信的交通流量矩阵"这件事做出来,而扩散模型因为擅长建模复杂分布,成了这条路上最主流的骨架。ChatTraffic 这个框架讨论的正是这件事:给定"工作日早高峰、小雨、主干道中度拥堵"这样的自然语言描述,生成一批在数值分布、日周期形态、路网空间相关性上都站得住脚的交通流量数据。这篇内容适合三类人看:做时空数据建模想扩样本的、做生成模型想换领域练手的、以及被"合成数据到底靠不靠谱"这个问题困扰过的工程同学。我会把数据管线、条件注入、采样调参和踩坑排查完整拆一遍,能直接抄的部分我都写成可运行的形状。
1. 先把任务说清楚:一句话到一张流量矩阵,到底在生成什么
很多人第一次听这个题目,脑子里对标的是"文本生成图片",觉得换个数据模态就行。真动手就会发现,这个类比只对了一半,剩下的一半会把你的模型按在地上摩擦。所以在写第一行代码之前,必须把任务的形式化定义和它与图像生成的结构性差异掰扯清楚,否则后面所有的调参都是在错误的地基上盖楼。
1.1 交通数据的两个老毛病:不够用,也不敢用
先说"不够用"。交通流量是典型的高频采样、低有效样本数据。以 5 分钟为粒度,一条路段一天是 288 个点,听着不少,但一旦按"场景"切分——工作日/周末、晴天/雨雪、常态/事件——每个格子里的独立样本数会迅速塌缩到个位数。你要训一个带场景条件的预测模型,或者做极端场景下的鲁棒性测试,样本根本喂不饱。这是第一层。
第二层是"不敢用"。真实路网数据涉及出行行为,共享和二次分发有明确边界,跨机构合作往往只能拿到脱敏后的统计量。于是形成了一个很尴尬的局面:做算法的人手上只有少量公开数据,做数据的人手上有一堆但不能随便给。合成数据在这个缝里就有了价值——它不是替代真实数据,而是给真实数据做"增广"和"压力测试"。这里必须强调一句:合成数据的定位应该是补足长尾场景和扩充训练集,而不是宣称能完全替代真实采集。这个定位一旦搞错,后面评估指标的设计就会全歪。
提示:如果你的目标只是"把预测模型精度再刷 1 个点",合成数据的性价比通常不如直接做数据清洗和特征工程。它真正的战场是冷启动场景、极端天气/事件场景、以及需要大量样本做敏感性分析的仿真评估。
1.2 任务的形式化定义与张量形状
把话说得工程一点。设某路网有 N 个观测点(检测器或路段),时间粒度固定,一天 T 个时间步。一条样本就是一个矩阵X ∈ R^{N×T},比如 N=200、T=288,就是 200×288 的实数矩阵。文本条件是 c,比如"周三早高峰,中雨,城北主干道拥堵指数偏高,全天流量较平日下降约一成"。我们要学的是条件分布p(X | c),然后从中采样。
注意这里的关键词是"分布"。模型不是要输出唯一正确答案,而是要输出一批都合理、但细节各不相同的样本。这一点非常反直觉——做预测做久了的人第一反应是"给我一个最准的",但生成任务要的是"给我 100 个都不离谱的"。我第一次转过来的时候,习惯性地拿 RMSE 去衡量生成质量,结果模型被我调成了"输出全体样本均值",指标好看得要命,但生成出来的每一条流量曲线都长得一模一样,完全失去了增广的意义。这个坑后面第 6 节会详细讲。
实际工程里通常还会再做两件事:一是把 N 个点按空间邻接关系组织成图结构或者分区,二是按小时/按天做多尺度切分。因为一天的曲线和一周的曲线形态差异很大,单尺度建模容易顾此失彼。
1.3 和文本生图的三处结构性差异
这是我建议每个入场的人都先画在纸上的东西,因为它直接决定了你后面每一个模块的设计。
| 维度 | 文本生图 | 文本生交通矩阵 |
|---|---|---|
| 数值特性 | 像素归一化后近似有界连续,分布相对平滑 | 连续物理量,非负、有量纲、强双峰(早晚高峰)、长尾(事件) |
| 空间结构 | 规则二维网格,局部平移不变性成立 | 不规则图结构,邻接关系由路网决定,平移不变性不成立 |
| 条件粒度 | 文本与图像内容语义对齐相对细 | 一句文本只约束高层统计属性,一对一映射根本不存在 |
第一条差异决定了你的归一化和噪声调度不能照抄。图像那套把像素压到 [-1, 1] 的线性映射,在交通流量上会把深夜的低流量段压成一条几乎为零的直线,模型学不到夜间形态;而白天高峰段又挤在高位,梯度分布极不均匀。
第二条差异决定了骨干网络。图像 U-Net 靠卷积的平移不变性提取局部纹理,但路网上"相邻"是有明确物理含义的,两个检测器在矩阵里位置挨着不代表在路网上连通。所以时空骨干里必须显式注入图结构信息,或者用注意力让模型自己学邻接。
第三条差异最容易被低估:一句文本对应的不是一个确定的矩阵,而是一族矩阵。这就要求模型具备"语义可控、细节随机"的能力——文本说什么必须体现出来,文本没说的部分要敢于变化。这个平衡点的控制,后面会落在无分类器引导的尺度参数上。
2. 数据管线:把"早高峰下雨的环路"翻译成模型能吃的配对样本
扩散模型的效果上限,在数据管线这一步基本就定了。我见过太多人模型结构调了两周没起色,最后发现问题出在文本描述模板写得过于笼统,或者归一化把动态范围压死了。这一节按处理顺序讲,每一步都说明为什么这么做。
2.1 从原始线圈记录到规整张量
原始数据通常是长表:时间戳、检测器编号、流量、占有率、速度。要变成 N×T 的矩阵,大致四步。
第一步是选点与对齐。不是所有检测器都值得留。我的做法是先算每个点近一年的数据完整率,低于 85% 的直接剔除,剩下的再做时间轴对齐。这一步会损失一些覆盖度,但换来的是干净的张量,非常值。别舍不得,缺失率 60% 的点补出来的值本质上是你自己编的,会污染整个分布。
第二步是缺失填补。这里有个原则:填补方法要区分"短缺口"和"长缺口"。15 分钟以内的短缺口,用同一检测器前后时刻的线性插值就够,因为交通流量在短时内是连续的;超过 1 小时的长缺口,线性插值会造出一条假的平直线,应该用同类日(同星期、同天气)的同时段均值来填,并且打上掩码标记。
第三步是异常值处理。流量为负、单步跳变超过阈值(比如相邻两步变化超过历史 99 分位数的 3 倍)、连续 2 小时恒定不变(典型的检测器卡死),这三类要单独标记。注意不是简单删掉,而是标记后交给后续的掩码机制处理,因为异常本身也可能是信息。
第四步是组织成样本。按天切是最常见的,一天一条样本。如果要做周级建模,就按 7 天切,形状变成 N×7×288。实测下来按天切 + 把星期几作为附加条件,比直接按周切更好训,样本量也大得多。
import numpy as np def build_tensor(long_df, n_sensors, steps_per_day=288): # long_df: columns = [timestamp, sensor_id, flow] mat = long_df.pivot_table( index="timestamp", columns="sensor_id", values="flow" ).reindex(columns=sensor_ids) # 对齐到固定的点顺序 mat = mat.asfreq("5min") # 补齐时间轴,缺口变 NaN short_gap = mat.interpolate(limit=3, limit_direction="both") # 15min 内线性插值 long_fill = same_weekday_hour_mean(mat) # 长缺口用同类日均值 filled = short_gap.fillna(long_fill) mask = mat.notna().astype(np.float32) # 掩码带进模型,别丢 # 按天切片,丢掉不满一天的尾部 n_days = len(filled) // steps_per_day arr = filled.values[: n_days * steps_per_day].reshape(n_days, steps_per_day, n_sensors) arr = arr.transpose(0, 2, 1) # -> (n_days, N, T) return arr, mask.values.reshape(n_days, steps_per_day, n_sensors).transpose(0, 2, 1)这段代码里最容易被忽略的是mask。很多实现直接把 NaN 填完就扔了,结果模型在填补出来的假值上学到了假的平坦形态。把 mask 作为额外通道喂进去,损失函数只在真实观测位置回传,效果差别很明显。
2.2 文本侧怎么写才不是废话
这是整个管线里最"手工"的部分,也是最影响结果的部分。核心思路是属性解耦:把一句话拆成若干个可独立控制的维度,每个维度在文本里有明确、无歧义的表达,同时在数值上有可验证的对应关系。
我一般拆成这几维:
- 时间属性:工作日/周末、星期几、早高峰/平峰/晚高峰/夜间
- 气象属性:晴、阴、小/中/大雨、雪、雾
- 事件属性:无事件、施工占道、大型活动散场、临时管制
- 路网属性:主干道、次干道、快速路、城北/城南片区
- 强度属性:全天均值相对平日的变化幅度(上升/下降约一成等)
- 形态属性:拥堵指数等级、峰值出现时段
然后写模板。模板不是随便拼,要注意两点:一是同一语义要有多种说法(数据增广,避免模型死记句式),二是数值描述要留量化锚点。
模板示例(同一语义的三种说法): A. "工作日早高峰,小雨,城北主干道,全天流量较平日下降约 8%,峰值前移至 7:30 前后。" B. "周一早间有小雨,城北主干道通行效率偏低,整体流量小幅下滑,早高峰提前。" C. "雨天工作日的早高峰,城北主干道流量略低于平日,拥堵峰值来得更早一些。"这里有个实操心得:描述里只要出现"约"、"小幅"、"明显"这类模糊词,就一定要在生成样本后用回归器反推验证,确认模型真的按这个幅度走了。我在早期版本里发现,模型对"下降约一成"和"下降约三成"的响应几乎没差别——因为训练集里这种幅度描述太少,模型直接忽略了它。后来把每个幅度档位补到至少 200 条样本,才有明显区分。属性维度的样本均衡,比模型结构重要得多。
2.3 归一化:别把数值域压死
这一步我要重点说,因为它是最隐蔽的坑。直觉做法是全局 min-max 归一化到 [-1, 1],但交通流量的分布是严重右偏的:大量样本集中在低流量区间,高峰部分拉出长尾。全局 min-max 之后,90% 的样本挤在 [-1, 0.2] 这个狭窄区间,模型对这段的梯度信号极弱,生成的夜间和低峰段会非常糊。
我试过三种方案,实测结论如下:
| 方案 | 做法 | 优点 | 问题 |
|---|---|---|---|
| 全局 min-max | 全数据集统一缩放 | 简单,可逆 | 低流量段梯度弱,形态糊 |
| 分点位标准化 | 每个检测器单独 z-score | 兼顾各点量级差异 | 极小流量的点会被放大噪声 |
| 分位数变换 | 先 log1p 再按分位数映射到近似高斯 | 分布形态最均匀 | 可逆时要小心边界外推 |
最终我选的是第三种,具体流程是:先log1p压缩动态范围,再按全局分位数映射到标准正态,最后按点位做一次均值方差校正。这样夜间低流量段和高晚高峰段在数值域里都有足够的"呼吸空间",模型学形态明显更稳。反变换时要注意:落在训练集分位数范围之外的值会外推,需要做截断,否则会生成负流量。
注意:生成任务做完反变换后一定要做一次物理合法性检查——非负、不超过该点历史最大值的一个合理倍数。看似多余,但训练早期的模型真的会输出负值,如果直接拿去训练下游模型会污染结果。
2.4 切分与泄漏风险
时间序列数据的切分不能用随机切。正确做法是按时间块切:训练集取前若干个月,验证集取中间一段,测试集取最后一段,中间留出缓冲期(比如各留 7 天不用),避免相邻天的强相关造成泄漏。
这里有个专门针对生成任务的泄漏点容易被忽略:文本模板的泄漏。如果你把同一批模板句式全部放在训练集里,那验证集上的条件一致性指标必然虚高,因为模型只是记住了句式。我的做法是留出 10% 的模板变体,只在测试集出现,用它们来检验模型对未见句式的泛化。这个数字看起来小,但能真实反映泛化能力。
3. 条件扩散的骨架:U-Net 之外必须做的三个改造
结构设计这块,我的建议是"骨架借现成的,接口自己改"。扩散过程本身是通用的,没必要重新发明;真正需要定制的是噪声调度的参数、条件注入的位置、以及时空骨干能不能表达路网结构。下面按前向、条件、骨干、采样四块讲。
3.1 前向过程与噪声调度
前向过程就是逐步加高斯噪声,标准写法:给定 T 步,β_t 从 1e-4 线性增到 0.02,α_t = 1 - β_t,累积得到 ᾱ_t,则 x_t = √ᾱ_t · x_0 + √(1-ᾱ_t) · ε。T 取 1000 是图像领域的惯例,在交通矩阵上也是可用的起点。
但噪声调度不建议照抄线性。原因是我们的数据经过分位数变换后近似高斯,但尾部(极端高峰、事件日的尖峰)仍然比标准高斯更"重"。线性调度在前段加噪太慢,模型花了大量步数在几乎没有信息的区域;到后段又加得太快,细粒度形态来不及学。换成 cosine 调度后,训练损失下降更平滑,生成曲线的尖峰保留得也更好。
import torch, math def cosine_alphas_cumprod(T=1000, s=0.008): steps = torch.arange(T + 1, dtype=torch.float64) f = torch.cos(((steps / T) + s) / (1 + s) * math.pi / 2) ** 2 alphas_cumprod = f / f[0] return alphas_cumprod.clamp(1e-5, 0.9999)[1:] # 返回 ᾱ_1..ᾱ_T def q_sample(x0, t, noise, abar): a = abar[t].view(-1, 1, 1) # (B,1,1) 广播到 (B,N,T) return a.sqrt() * x0 + (1 - a).sqrt() * noise注意广播维度。交通张量是三维的(batch, 路段, 时间),很多从图像代码改过来的实现会在这里出现维度错位,症状是损失能降但生成结果在时间轴上是错乱的。我踩过一次,排查了大半天才发现是view(-1, 1, 1)写成了view(-1, 1)。
3.2 文本条件注入的三条路线对比
这是整个框架里最核心的设计选择。我给三条路线的实测对比:
| 注入方式 | 机制 | 表达力 | 训练稳定性 | 适合场景 |
|---|---|---|---|---|
| 拼接通路 | 把文本向量复制后与噪声图拼接成通道 | 弱,条件只能全局生效 | 高 | 快速验证、属性少 |
| 自适应归一化 | 用文本向量回归缩放与偏移系数 | 中等,控制整体统计量 | 很高 | 主要控制均值/峰值幅度 |
| 交叉注意力 | 文本 token 作为 key/value 参与注意力 | 强,能对齐局部片段 | 中等,需要足够数据 | 多属性、细粒度控制 |
我最终的方案是混合:主干用交叉注意力保证表达力,同时在每个残差块里加一层轻量的自适应归一化,把文本向量的全局信息直接注入。这个组合的理由很实在——交通文本里的"全天均值下降 8%"是一种全局统计约束,让交叉注意力去学这种全局量是浪费容量,而自适应归一化的仿射参数天然适合干这个;反过来,"早高峰提前"是位置相关的约束,就交给交叉注意力。分工明确之后,训练收敛快了不少。
文本编码器这块,用预训练语言模型做初始化比从头训效果好太多,因为交通领域的配对数据量远不够训一个可用的编码器。如果你实在没有条件用预训练模型,退而求其次的做法是把文本属性拆成 one-hot 向量再拼接,虽然表达能力差,但至少可控、可解释,适合先跑通链路。
3.3 时空骨干:为什么不能直接照搬图像 U-Net
图像 U-Net 的下采样是二维的,在交通矩阵上直接套用会出两个问题。第一,时间轴和路段轴被同等对待,但实际上时间轴的强周期性(日周期、周周期)和路段轴的空间相关性是两种完全不同的结构,混在一起下采样会丢周期信息。第二,纯卷积看不到路网拓扑。
我的改造思路是解耦:时间轴用一维因果/空洞卷积堆叠,专门捕捉多尺度周期(15 分钟级、小时级、天级);路段轴用图卷积或者带邻接掩码的注意力,专门捕捉空间传播。两路输出再融合。这样做的另一好处是显存友好——一维卷积在时间轴上的计算量比二维卷积小很多,200×288 的矩阵在单卡上能跑比较大的 batch。
如果路网邻接矩阵拿不到,退路是用数据驱动的方式学一个软邻接:对每个点算历史流量序列的皮尔逊相关,取 top-k 作为邻居。这个替代方案实测能用,但要小心相关不等于因果,某些点的相关性可能来自共同的下游瓶颈而非直接连通。
3.4 采样加速与步数权衡
训练用 1000 步,采样绝对不能 1000 步,太慢。DDIM 是标准做法,关键是步数选多少。
我的实测:50 步基本无损,20 步开始出现峰值削顶(早高峰的尖峰被磨圆),10 步以下日周期形态还保留着,但局部波动全糊成一片。所以如果你要拿生成数据去做下游的短时预测训练,建议至少用 30 步;如果只是做可视化或者粗粒度统计对比,20 步可以接受。
还有一个便宜的加速手段是把一天拆成若干段分别生成,因为日周期的主要形态差异集中在几个关键时段。但我不太推荐,因为段与段之间的衔接会出现不连续,需要在边界做平滑,反而更麻烦。
4. 训练现场:超参、显存与"看起来在收敛"的假象
理论讲完,进训练场。这一节的内容基本都是被现实教育出来的,文档里不会写。
4.1 关键超参的取值与理由
| 参数 | 推荐值 | 为什么 |
|---|---|---|
| 扩散步数 T | 1000 | 与图像实践一致,配合 cosine 调度足够 |
| 噪声调度 | cosine,s=0.008 | 数据尾部较重,线性调度前段浪费步数 |
| batch size | 32~64 | 受矩阵尺寸限制,太小则梯度噪声大 |
| 学习率 | 1e-4,配合 500 步 warmup | 条件分支起步梯度大,不 warmup 容易炸 |
| EMA 衰减 | 0.999 | 生成任务对权重噪声极敏感,EMA 几乎是必须的 |
| 无分类器引导 drop 概率 | 0.1 | 太低学不会无条件分支,太高浪费条件信号 |
| 采样步数 | 50(质量优先)/ 20(速度优先) | 见 3.4 的实测结论 |
表里最值得说的是 EMA。生成任务和判别任务不一样,模型在验证损失最低点附近的权重波动会直接体现在生成样本的形态上——同一句文本,隔两个 epoch 生成出来的曲线一个平滑一个毛刺。开 EMA 之后这个现象基本消失。代价是显存多一份模型副本,但值。
无分类器引导的 dropout 概率也容易被随手设成 0.2,导致 20% 的训练样本根本不看文本。交通数据的配对样本本来就珍贵,drop 到 0.1 就够了,实测条件一致性明显更好。
4.2 损失曲线骗人的几种情况
扩散模型的训练损失是噪声预测的 MSE,这个指标"看起来"很稳定——它从第一天开始就在一个不大的区间内波动,你很难从曲线上判断好坏。我遇到过三种典型的假收敛:
第一种,损失平稳但采样结果全是噪声。原因通常是 ᾱ 的累积计算有 off-by-one 或者 clamping 太激进,导致最末步的信噪比不对。排查方法很简单:拿一条真实样本,做一次完整的加噪-去噪,看能不能还原。如果连"重建"都失败,问题一定在前向过程,不在模型。
第二种,损失缓慢下降但生成样本单一。这是模式坍塌的前兆,损失下降是因为模型学会了预测"平均噪声",而平均噪声对应的就是平均样本。这时候要看的是生成样本之间的方差,而不是损失。
第三种,训练损失低但验证损失高,且验证集上生成的样本出现明显的"记忆痕迹"——某些曲线和训练集里的某条几乎一模一样。这是过拟合,尤其在数据量少于一万条样本时很容易发生。对策是加强条件 dropout、加数据增广(对文本模板做同义替换、对流量做小幅时间抖动)。
我现在的习惯是每 5 个 epoch 固定用三条文本各采 8 个样本画图出来看,比盯损失曲线靠谱得多。
4.3 显存吃紧时的四档取舍
不是所有人都有大卡。按性价比排:
- 降 batch,加梯度累积。最无损的方案,32 的 batch 用 4 步累积等效 128,只是训练慢。
- 混合精度 + 梯度检查点。省一半以上显存,代价是速度降 20% 左右。梯度检查点对扩散模型特别划算,因为它的计算图是重复结构,重算成本可控。
- 降模型宽度。把通道数砍一半,参数量降到 1/4,效果损失在 10% 以内,很多时候可以接受。
- 缩短时间轴。把 288 步降采样到 144 再上采样回来,这是最后手段,因为日周期的细节会丢,尤其是早晚高峰的陡峭上升沿。
最后这条我要提醒一句:降采样再上采样听起来无损,实际上早高峰那段 30 分钟内流量翻倍的陡坡,降采样后基本变成缓坡,模型学到的"峰值形态"就失真了。
5. 怎么判断生成得好不好:不能只看一张图
生成模型的评估是玄学重灾区。我的原则是四层验证,缺一层都不算数:分布级指标、条件一致性、下游任务、人的眼睛。下面逐层说。
5.1 分布级指标:先确认整体形状对不对
最直接的是按小时分桶,对比真实数据和生成数据的均值、标准差、分位数。这一步能快速暴露量级问题。指标上我用三个:
- Wasserstein 距离:衡量同一时刻流量分布的整体偏移,对量级敏感,很适合抓"生成值系统性偏低"这类问题。
- MMD(最大均值差异):用 RBF 核对整体分布做检验,对形态差异敏感。
- 按小时的 JS 散度:把流量离散成若干区间算分布散度,能看出哪个时段生成得最差。
实测经验:早高峰时段(7:00-9:00)和晚高峰时段(17:00-19:00)的指标通常最差,因为这两段的分布最尖、最窄,模型倾向于生成更平滑的版本。如果你的指标显示全时段都很好,反而要怀疑是不是评估时把真实和生成的数据混在一起了。
5.2 条件一致性:文本说的到底有没有体现
这层是最容易被忽略、但最关键的。做法是训一个轻量的属性回归器(输入一条流量曲线,输出峰值时段、全天均值相对变化、峰均比等几个标量),然后拿它去反推生成样本的属性,和输入文本里的描述比对。
比如文本说"峰值前移至 7:30 前后",就检查生成样本的峰值时刻落在 7:00-8:00 的比例。如果这个比例只有 30%,说明模型基本没听文本的。
这里有个技巧:回归器要用真实数据训练、在真实数据上验证过精度(我一般要求峰值时刻的 MAE 在 15 分钟以内),否则你是在用一个不准的尺子量东西。我最初图省事用一个很粗糙的回归器,结果条件一致性指标全乱,白折腾一周。
5.3 下游任务当裁判:Train-on-Synthetic 才是硬指标
前面两层都是"像不像",这一层是"有没有用"。做法是把生成数据当训练集,训一个标准的需求预测模型,然后在真实测试集上评估。同时用真实数据训一个同结构的模型做基线,对比差距。
如果合成数据训出来的模型精度能达到真实数据基线的 85% 以上,说明数据可用性不错;如果只有 50%,那合成数据基本只能用来做可视化演示。我自己的经验是,纯靠合成数据训练,精度一般在基线的 75%~90% 之间,混入 30% 真实数据后能追到 95% 以上。所以混合训练才是最务实的用法,别指望纯合成。
还有一个反向用法:用合成数据专门构造极端场景(暴雨、大型活动),看预测模型的性能衰减。这个用法对合成数据的"真实性"要求低一些,但对"极端性"要求高,需要专门做长尾采样。
5.4 画图:最朴素也最有效
最后这层说出来有点土,但真的管用。把真实样本和生成样本叠在一张图上,横轴时间、纵轴流量,按场景分组各画 20 条。人眼对"这条曲线看着不像真的"极其敏感,比任何指标都快。
我自己总结的几个视觉检查点:早高峰的上升沿够不够陡(真实数据通常在 40 分钟内完成翻倍);午间有没有那个标志性的小回落;晚高峰的峰值是不是比早高峰更宽更平;夜间 0-5 点是不是贴近基线的平滑段而不是随机抖动。这几条只要有一条不对,指标再好我也不会信。
6. 三个踩坑的完整排查链路
这一节按"现象—怀疑—验证—修复"的顺序写,希望你能顺着复现我的排查思路,而不是只拿走结论。
6.1 模式坍塌:所有文本都生成同一条平均曲线
现象:跑了大概 30 个 epoch 之后,用十条完全不同的文本各采 10 个样本,画出来发现所有曲线高度重合,都是那种"温和的双峰"——有早高峰有晚高峰,但峰值高度被磨平,上升沿很缓。
第一层怀疑:条件没生效。验证方法是把文本条件全部置空(走无条件分支)再采样,对比两种结果的差异。如果几乎一样,那确实是条件失效。这次的结果是"有差异但差异很小",说明条件是有用的,只是强度不够。这一层排除了硬件和代码 bug 的可能。
第二层怀疑:引导尺度太小。把无分类器引导的 scale 从 1.0 提到 3.0,曲线开始分化,但数值开始崩——出现了负流量和超过历史最大值两倍的异常峰值。说明单纯放大 scale 是在用数值崩坏换条件强度。
第三层,真正的原因:训练数据本身的多样性不够。我去统计了训练集里各属性组合的样本数,发现"雨天 + 事件"的组合只有不到 30 条,"雪天"整个类别不到 10 条。这几十条样本在几万条总量里被彻底淹没,模型学到的最优策略就是"不管文本说什么,输出一个最安全的平均值"。这是典型的数据不均衡导致的模式坍塌,靠调参救不回来。
修复:三件事同时做。一是重采样,对稀有属性组合做上采样,权重按类别频率的倒数算,但上限压到 5 倍以内避免过拟合;二是给损失函数加一个针对稀有类别的加权项,让它们在梯度里的占比提高;三是在推理时用分组的引导尺度——常见场景用小 scale(1.5),稀有场景用大 scale(2.5),兼顾稳定和强度。改完之后,稀有场景的样本间方差明显上来了。
6.2 文本被无视:引导尺度调不对的连锁反应
这是最经典的坑,我前后栽了两次。第一次是把 guidance scale 直接设成 7.5(图像生成的经验值),结果生成的流量矩阵数值全崩,到处是尖刺和负值。第二次是矫枉过正调到 0.5,文本完全不起作用。
原因在于,图像是无界连续的像素强度,而交通流量经过反变换后是受物理约束的数值。scale 过大时,条件梯度被放大,模型会为了"满足文本"而牺牲数值合理性。我的实测结论是:这类物理量回归型生成任务,scale 的合理区间是 1.5~3.0,比图像的常规值低一半以上。
具体调法:从 1.0 起,每次加 0.3,同时观察两个指标——条件一致性(越高越好)和数值合法率(生成样本中非负且不超过历史 1.5 倍的比例,应该接近 100%)。找那个"一致性还在涨但合法率开始掉"的临界点,然后退一格。我最后的取值是 2.0。
还有一个细节:引导是在噪声预测层面做的,公式是ε = ε_uncond + w * (ε_cond - ε_uncond)。如果训练时文本 dropout 的概率设得太高(比如 0.3),无条件分支学得比有条件分支还好,这个引导就没有意义了,因为 ε_uncond 本身信息量太大。这也是为什么我在 4.1 里强调 dropout 要压到 0.1。
6.3 周期错位与数值越界:两个看起来无关其实同源的问题
现象:生成的曲线,早高峰的位置会整体偏移,有时候偏到 6:00,有时候偏到 9:30;同时生成的序列末尾(22:00 之后)偶尔出现数值发散。
排查路径:先查数值越界。反变换做完之后,确实出现了一些超出训练集分位数范围的值,做截断后发散消失。但周期偏移还在。
接着查归一化。我把一条真实样本走一遍"变换→反变换",能完美还原,说明可逆性没问题。那就不是归一化的事。
然后查位置编码。我当时的模型在时间轴上用的是固定的正弦位置编码,而输入的时间步索引是按 batch 内相对位置生成的。问题就在这里——当我把一天切成不同起始时刻的滑窗做增广时,位置编码没有跟随滑窗偏移更新,模型学到的"第 50 个位置"对应的是不同的真实时刻,周期就错乱了。
修复:把位置编码从"序列内相对位置"改成"一天内的绝对时刻比例",并且把"距离当日峰值的相对时间"也作为一个显式特征输入。改完之后,峰值位置的偏差从平均 70 分钟降到了 12 分钟。
顺带说一句,数值越界和周期错位这两个问题看起来无关,其实同源:都来自"模型没有正确理解时间轴的真实含义"。前者是边界处理不严,后者是位置信息错误。这也印证了一件事——在时空数据上,把时间信息显式建模,比让模型自己学要可靠得多。
7. 能落地的地方,和不该吹的边界
最后聊聊实际价值。我自己看到比较实在的用法有三类:一是给预测模型做训练集增广,尤其是补足长尾场景,混合训练能实打实提升极端场景下的鲁棒性;二是给仿真和调度评估提供压力测试输入,需要的是大量"合理但不同"的场景,这正是生成模型的强项;三是数据脱敏场景下的替代交付,但这一条要非常慎重,必须经过充分的统计一致性验证,而且更适合交付统计特征而非原始矩阵。
不该吹的地方也得说清楚。第一,生成模型不会凭空创造规律,它只能重组训练分布里的模式,训练集里没有的场景(比如从没出现过的极端天气)它生成不出来,硬做就是编造。第二,长时预测上的价值有限,生成的是"一天"的形态,跨天的演化规律它学不到。第三,评估成本很高,前面第 5 节那四层验证都要做,否则很容易自我感觉良好。
我个人在实际操作中的体会是,这套东西的技术门槛不在扩散模型本身——那部分现成的实现太多了——而在数据管线的三件事:属性拆得够不够细、每个属性的样本够不够均衡、文本和数值的对应关系有没有被验证过。这三件事扎实了,模型用一个朴素版本的 U-Net 都能出效果;这三件事虚,你换再花哨的骨干也只是在拟合噪声。
另外再分享一个省时间的小技巧:先别急着训 1000 步的完整模型。用一个缩小版(T=200、通道砍半、只取 50 个路段)把"数据管线 → 条件注入 → 采样 → 反变换 → 评估"这条链路完整走通一遍,确认每一环的输出都能对得上,再上全量。我第一次做的时候直接上全量,训了两天才发现评估脚本里的时间轴对齐错了,白烧了 48 小时的卡。链路先通,规模后上,这个顺序别反。