智能交通强化学习泛化难题:从场景指纹到通用策略的重构实践
上个月复盘一套智能路口信号控制项目时,统计到一件让人很尴尬的事:在A路口训练收敛的强化学习模型,平移到一条相隔不到两公里、车道布局几乎一样、流量只差15%的B路口后,平均延误不降反升了12%。团队成员开玩笑说“这模型可能只认A路口的沥青颜色”。玩笑归玩笑,这个尴尬的结果正好戳中了强化学习在交通场景里最疼的一块软肋——泛化能力。圈子里默认RL学的是“控制规律”,但真实项目里模型经常是在死记一套场景指纹,遇到相似但不完全相同的交通问题,决策就崩掉了。
标题问“连相似的交通问题都失灵,研究者怎么办”,我特别能理解这个疑问,因为过去半年我就在这个泥潭里反复挣扎。这篇文章不打算灌鸡汤,而是把我做失效复现、根因排查、重构状态表示和训练方案的全过程整理出来,包括实验数据、踩过的坑、最后验证泛化能力用的评估协议,给同样被困在“一个路口只能训一个模型”状态里的同行一个可以照做的参考。
1. 相似路口的“近迁移测试”:一次让我拍桌子的实验
1.1 实验是怎么设计的
先说清楚当时那个让我“破防”的实验设计。两个仿真路口,A路口和B路口,控制周期相同,进口道都是四相位,车道数也基本一致。唯一的显著区别是B路口的中片区多了一条右转专用道,同时全天总体流量比A路口低约15%。我当时抱着“这么轻微的变化应该没问题吧”的心态,把训练好的一套PPO权重直接零样本加载进B路口环境里测试,不重启训练,不做任何适配。
结果是这样的:
| 指标 | A路口训练收敛值 | B路口零样本迁移值 | 相对变化 |
|---|---|---|---|
| 平均排队长度(辆) | 18.6 | 27.4 | +47.3% |
| 平均停车延误(秒/车) | 42.3 | 47.4 | +12.1% |
| 路口总吞吐量(辆/小时) | 1862 | 1705 | -8.4% |
| 归一化回合奖励 | 0.87 | 0.71 | -18.4% |
最离谱的是,这还不是“完全没见过的新场景”。两个路口在拓扑上几乎同构,交通流量差异也不算大,按常识判断,模型应该有很高的“近迁移”成功率。可数据告诉我们,策略不仅在B路口没有保持住最优性,连最基础的“别把排队搞得更长”这条底线都没守住。当时我第一反应是奖励函数计算有问题,检查了三遍之后,才不得不承认模型真的“认生了”。
1.2 “数据不够再训一训”的直觉陷阱
遇到这种迁移失败,团队里最常见的提议就是:是不是数据量不够?多采集一些B路口的样本,在原有预训练权重上继续微调。这个思路听起来合理,但实际做出来又给了我一记闷棍。
我在A路口预训练权重基础上,加入B路口数据继续做微调训练,训练两万个环境步后,B路口的平均延误确实降下来了,从47.4秒回到44.9秒左右,但紧接着A路口的平均延误却从42.3秒涨到了46.7秒。也就是说,增量数据把网络参数拉向了B场景,同时又覆盖掉了原先已经学好的A场景行为。灾难性遗忘在连续微调里表现得非常干脆。
这个结果让我意识到,问题根本不在“数据量”。即便我们把两个路口的数据全部混在一起训练,也只是得到一个在两个场景之间“和稀泥”的策略,勉强让A和B都不太好也不太差。真正缺的不是样本,而是模型结构里没有把“场景特有信息”和“通用决策信息”分开。当所有的场景线索都纠缠在一组网络权重里,迁移本质上是失败的。
2. 场景漂移为什么能骗过强化学习模型:三层根因
2.1 状态接口把“几何指纹”当成决策依据
先看当时喂给策略网络的状态向量。那套模型用的是很常见的结构:车道级排队长度、当前信号相位、相位剩余时间、各进口道通过车辆数。这些特征听起来没问题,但它们天然把路口几何结构渗进了数值里。
A路口和B路口车道数相同,但因为有右转专用道,B路口在右转方向上的排队分布和A路口完全不同。神经网络在提取特征时,不会去区分“这个差异对决策有没有用”,它只会发现这些特征能高度辨识当前场景,于是把车道特有分布当成决策依据一起学进去了。于是模型学到的不是“看到某方向排队接近饱和就延长绿灯”,而是“当右转专用道队列特征长成某个固定形状时,延长绿灯大概率有好处”。换一个路口,几何特征分布变了,策略自然失效。
用更直白的话说:模型把“这个路口长什么样”和“这个路口该怎么控制”两件事揉在了一起。我们以为它学会了控制规律,实际上它在背路口指纹。
2.2 奖励尺度的不统一:同一个数字在不同路口含义完全不同
第二个根因出现在奖励函数上。那套模型用绝对排队长度作为主奖励项,目标是最小化每个周期内各进口道的总排队车辆数。在A路口,排队30辆基本就等于严重拥堵,因为进口道容量就这么大;但在B路口,由于通行能力不同,排队30辆可能只是晚高峰的常态水平。
这会导致什么后果?同一个奖励权重在A路口能形成“拥堵就往疏”的正确梯度,到B路口却变成了“无论怎么压都有30辆排队,奖励怎么都上不去”。模型在B路口里看到的奖励地形被拉平了,梯度信号弱,什么都不做反而是局部最优。就算迁移后继续训练,策略也会往一个奇怪的方向偏——不是学交通控制,而是在学“怎么让排队数勉强小于那个绝对阈值”。
奖励数值的绝对尺度不归一,是一切迁移失败里最隐蔽的坑。因为单场景训练时你根本看不出问题,A路口的奖励曲线很漂亮,你只会以为模型学得不错。
2.3 经验池分布固化:策略拟合的是“场景转移概率”而非“交通流规律”
第三层根因要往强化学习的训练机制里挖。单场景训练时,经验回放池里的样本分布完全由该场景的交通流模式决定。比如A路口早高峰流量大、排队饱和时间长,经验池里绝大多数样本都是高占用率状态;B路口流量低15%,中等占用率状态占大头。
策略网络在优化时,本质上是在拟合“给定状态s,采取动作a后,在某个转移概率下能拿到多少奖励”的条件期望。它拟合的对象,是A路口的转移函数和奖励分布,而不是交通流本身的一般规律。到了B路口,流量分布变了,模型大概率会碰到训练时很少见的状态组合。这些状态处于分布外,策略输出的动作本质上是在做外推,自然不可信。
这就好比一个人在畅通马路上练熟了过马路,你把他丢到车速相近但车距分布完全不同的环岛里,他的判断模型会被瞬间打乱。不是因为他不会过马路,而是他学到的“经验池”里全是双向平交路口的样本,没有环岛的先验。
3. 从状态接口和奖励函数下手,切断“记场景”的路径
3.1 策略无关状态表示:无量纲、几何无关、供需导向
既然根因之一是状态向量携带了大量几何指纹信息,最直接的应对就是重做状态接口。我的做法是:把原始状态全部替换成无量纲的比值型特征,让策略只看到“供需关系”和“压力等级”,不需要知道路口具体长什么样。
改造后的状态向量大概长这样:
- 各路口的饱和度:当前到达流量除以该进口道的理论通行能力,数值范围0到1.2;
- 排队占用率:当前排队车辆数除以该进口道最大可容纳排队车辆数,纯比例;
- 延误相对变化率:当前平均延误减去自由流延误,再除以自由流延误,反映延误恶化程度;
- 相位进度:当前相位已执行时长除以该相位最大可选时长;
- 上一个周期内各方向的绿灯利用率:实际通过车数除以最大理论通过车数。
构造出来的状态向量的关键特点是:任何一个路口都能计算这些值,而计算所用的分母本身已经蕴含了场景特性。饱和度高意味着这条路已经承载不住了,饱和度低则意味着还有余量。策略不用再去记住“这个路口是几车道”,它只需要知道“现在这个路口的压力有多大”。
我直接用一个简单的Python片段来说明状态构造逻辑,实际工程中这只是状态编码器一小部分:
def build_scene_free_state(lane_volume, lane_capacity, queue_len, max_queue, delay, free_flow_delay): saturation = lane_volume / max(lane_capacity, 1e-6) occupancy = queue_len / max(max_queue, 1e-6) delay_ratio = (delay - free_flow_delay) / max(free_flow_delay, 1e-6) return np.array([saturation, occupancy, delay_ratio], dtype=np.float32)最直观的变化是,这个状态向量里的所有数字在A路口和B路口都有了近似相同的语义。策略不再需要从“车道数差异”里反向推导交通压力,压力已经直接在输入端摆好了。
3.2 奖励函数改成“比例化标尺”
状态接口换掉之后,奖励函数也必须同步改造,不能继续用绝对排队数或者绝对延误做奖励项。我最后用的奖励公式是三个无量纲项的加权组合:
R = -α · |S_current − S_target| − β · (delay / free_flow_delay) − γ · switch_penalty
这里S_current是当前路口的综合饱和度,S_target是目标饱和度(一般设为0.8到0.9之间的理想区间);delay / free_flow_delay是延误放大倍数,反映的是“比自由流慢了多少倍”,而不是具体的秒数;switch_penalty用来惩罚过于频繁的相位切换。
这样设计的核心好处是:奖励函数各项在不同场景下拥有同一个“标尺”。A路口晚高峰排队30辆可能意味着饱和度接近1.0,B路口排队30辆可能饱和度只有0.6,奖励函数会自动给出完全不同的评价。模型学到的优化目标是“把压力拉回目标区间”,而不是“把某个绝对数字压到最小”。这是整个泛化改造里性价比最高的一处调整,改完之后就算不换模型结构,B路口的零样本迁移奖励也回升了不少。
3.3 动作空间:从“指定绿灯时长”改成“延长决策”
第三步改造是动作空间。旧版本的动作输出的是“下一个相位的绿灯绝对时长”,这意味着网络必须学会不同路口绿灯时长约束的绝对值差异:A路口最大绿灯60秒,B路口最大绿灯45秒,输入输出映射完全不同。这个设计天然让动作层面也绑定了场景。
我把动作设计改成了“保持/结束当前相位,以及是否延长一个固定决策步长”。模型只输出两个动作:继续延长当前相位,或者切换到下一个相位。具体延长多少秒由环境内部的约束参数处理。
这样改动之后,动作的语义在任意路口都是一样的——“我现在觉得当前相位还需要再等等”,还是一个二分类决策。模型不需要在输出层去拟合不同路口的绝对时长差异,环境约束又保证了动作不会超过该路口的最大绿灯限制。从实际效果看,这个改造虽然简单,却让策略在结构变化的路口之间迁移时少了一个很大自由度的外推。
4. “多世界训练”的组合拳:域随机化、课程学习与场景条件化策略
4.1 域随机化必须随机“结构变体”,不能只随机参数
光改状态、奖励和动作还不够,训练数据分布还是来自单一路口,模型依然会被单一场景的经验池锁住。要真正逼出泛化能力,必须让训练过程本身就覆盖多个“世界”。我用的第一招是域随机化,即训练时不断随机环境的配置。
不少文章提域随机化时只随机数值参数,比如车流量乘个0.7到1.3的系数,驾驶员激进程度随机取值。这样做的效果有限,因为模型仍然知道“路口一定是四相位、一定没有右转专用道”这个隐性前提。我的做法是在参数随机之外,加上了结构性变体的随机化,像车道数量组合、是否存在右转专用道、相位配置是四相位还是三相位、信号周期范围,都作为可随机项加入训练环境。
初始域随机化的配置表大概长这样:
| 随机维度 | 随机范围 |
|---|---|
| 各进口道流量倍率 | 0.6x ~ 1.4x |
| 跟车模型的安全距离参数 | 0.8 ~ 1.5 |
| 驾驶员激进程度 | 随机采样三档 |
| 车道数组合 | 双向二车道/双向三车道/双向四车道 |
| 相位结构 | 三相位、四相位、带右转专用道四相位 |
| 最大绿灯时长 | 30秒 ~ 75秒 |
这样训练时,模型每换一个episode就身处一个新“世界”,同一套策略权重必须同时在各种结构变体上都表现得不错,才能获得稳定奖励。模型就失去了“背单一路口指纹”的机会。
4.2 课程学习:别让多任务一拥而上
不过把所有随机变体直接同时丢进去,前期训练会非常容易出现梯度震荡。原因很简单:不同难度场景的回报差异太大,低难度场景已经把奖励推得很高,高难度场景还在原地挣扎,优化方向互相拉扯。
我用了课程学习来做平滑过渡。训练阶段分三级:
第一阶段用最简单的场景组合:低流量、双向二车道、四相位固定结构,先把策略学到能稳定处理基础压力。第二阶段加入中等复杂度的结构变体,比如双向三车道、带右转道、流量倍率放宽到0.8到1.2。第三阶段才打开全部随机域。
关于“难度”的定义,我在代码里按一个综合分数排序:拥堵状态出现频率、相位数量、结构变体差异度。每一级训练中,环境采样遵循难度越高采样概率越小的原则,防止模型被极端难的高维结构变体直接打懵。课程学习在日志里看起来会牺牲一点前期的单场景峰值分数,但它换来的是中后期更平滑的收敛和终局阶段更好的泛化。
4.3 场景条件化策略:把“场景ID”明明白白喂给网络
第三个组合拳是给策略网络增加一个“场景描述向量”作为额外输入。训练时,我们把每个环境的静态结构配置编码成一个向量,例如车道数、相位数、是否存在右转专用道、当前流量倍率,拼在状态向量后面一起送进网络。
这样做的好处很直接:网络可以学会一种条件化的映射——看到场景描述向量为“四相位无右转道”,就走A策略分支;看到“三相位有右转道”,就走B策略分支。训练目标是学习条件分布π(a|s, c)而不是把所有场景的平均解压到一个点。这样一来,不同场景之间的参数冲突大幅降低,域随机化训练也不容易顾此失彼。
实现上就是把状态拼接一下:
obs = np.concatenate([scene_free_state, scene_embedding], axis=-1) action_logits = policy_net(obs)需要注意的是,加了这个条件输入之后,初期训练曲线看起来反而变慢了,因为网络需要额外学习“不同场景描述对应不同策略”这个映射关系。我一开始差点把它关掉,后来坚持训练到中期才看到效果。这个现象后面还会再讲。
5. 验证泛化能力:评估协议、防骗清单与实测数据解读
5.1 构造“零样本测试矩阵”
改造做完之后,验证阶段是最不能糊弄的。很多人说模型泛化了,但拿出来的证据永远是一张测试集奖励曲线图。我的建议是直接做一个结构化的“零样本测试矩阵”,把测试环境按难度分成三档,每档测五个以上不同配置:
| 测试类型 | 具体配置示例 | 零样本要求 |
|---|---|---|
| 参数扰动类 | A路口/流量±15%/周期长度变化 | 不动权重,直接评估 |
| 结构微变类 | 三相位改四相位/增加右转专用道/车道数增减 | 不动权重,直接评估 |
| 极端模式类 | 行人过街比例提高/公交优先触发/事故占道 | 不动权重,直接评估 |
报告的指标不直接报绝对数值,而是报“相对基线规则(比如固定最大绿信比规则)的性能提升比例”。用相对基线的变化能消掉场景本身难易差异,防止你拿一个本身就很简单的场景当“泛化成功”的证据。
5.2 三个容易被“表面上好结果”骗过去的时刻
第一次做泛化评估的人很容易被好结果骗到,我自己就上过三次当。
第一次是在单一路口上反复调超参,把A路口的奖励曲线调到近乎完美,自我感觉良好。后来换了测试矩阵才发现这种“完美”完全是和A场景绑定的,连参数扰动类的测试都过不了。
第二次是只看训练总奖励曲线,不看评估时的状态分布。后来我把评估过程中的状态做了分布外检测,发现训练曲线漂亮只是因为经验池里塞满了同一场景样本,评估时模型遇到一堆训练中没见过的状态,动作完全在瞎猜。
第三次是对状态加了极小的高斯噪声做灵敏度测试,结果策略立刻崩溃。这说明模型记住的不是“状态规律”而是“精确的数值模板”,稍微一点扰动就暴露了。这个测试做起来成本极低,强烈建议每个做RL交通控制的人都加上。
5.3 我现在的评估流程与实测数据解读
经过以上几个版本的改造,我现在做RL信号控制模型评估时会走一套固定流程:
- 先跑单场景收敛性实验,确认策略在原场景没有明显性能倒退;
- 再跑参数扰动类零样本测试矩阵,验证对流量、周期等数值漂移的鲁棒性;
- 然后跑结构微变类测试,观察增加右转专用道这类结构变化时的性能保持率;
- 最后随机挑两个在训练中未出现过的极端场景做压力测试。
这套流程跑完之后,再回头量化改进前后的效果。状态与奖励改造加上域随机化加场景条件化之后,我手里项目的零样本迁移成功率从最初的约四成提升到了七成上下,结构微变类场景的平均延误提升幅度从最初比基线规则还差,变成了比基线规则领先约15%到20%。参数扰动类的场景表现更稳定,奖励波动幅度比改造前缩小了接近一半。
这个结果离“一个模型搞定所有路口”还很远,但至少它证明了“相似交通问题失灵”不一定是强化学学习的死穴,很多时候是我们把状态表示、奖励函数和训练数据分布设计成了“单场景定制款”,模型没有机会学到可泛化的决策逻辑。
最后分享一个自己踩过的坑:做场景条件化策略时,因为加了场景嵌入后早期训练曲线明显变慢,我一度想把它回退掉。后来坚持多训练了几十万步,模型才逐渐学会“看清场景再决策”,曲线一下子反超了不加条件时的版本。强化学习项目的迭代最怕的不是模型能力不够,而是评估标准不统一导致你永远在单场景里自嗨。先把状态里的“场景指纹”拆掉、奖励换成一个通用标尺、训练时让模型见过足够多的“世界”,这是目前我验证过最靠谱的破局顺序。