1. 视觉导航任务为什么选PPO而非DDPG或A2C
很多人一听到"游戏导航视觉"这种组合,第一反应都是"拿强化学习去跑就完事了"。但等你真把环境搭好、输入是图像、动作是连续控制的时候,才会发现算法选型这一步就够卡半天。
我这个实验的场景是一个自建的简化驾驶导航环境,模拟车在网格地图上从起点开到目标点,能看见的只有前方摄像机视角的像素画面。没有地图、没有GPS、没有全局信息,只能靠视觉判断自己该往哪走、该给多少油门。动作输出如果只做离散方向键,车跑起来一卡一顿;如果全部用连续控制,转向幅度很难稳定。这也是为什么最终我敲定了"PPO+CNN+双动作空间"的组合。
先说为什么是PPO。
PPO几乎是我见过最"稳"的策略梯度算法。尤其在视觉输入这类高维状态场景里,DDPG那一套(确定性策略+异策略回放)对超参数极其敏感,回放缓冲区大小、软更新系数、目标网络延迟,稍微配合不好,策略就废给你看。A2C虽然稳定,但样本利用率偏低,在图像采样成本高的环境里,你每轮都要重新采集几百上千帧画面,训练速度被拖得很慢。PPO属于"带有策略更新的安全护栏"那种类型,它用裁剪过的目标函数限制每次参数更新的幅度,不会让一次大梯度把辛辛苦苦学出来的策略整个冲垮。对项目排期紧、没那么多时间天天调参的人来说,这几乎是唯一的正解。
CNN则是把"画面"变成"特征"的基础工具。像素级输入不能直接丢进全连接网络,84x84x3的输入展开就是两万多个数,算起来慢不说,全连接层还学不到图像平移不变性。CNN通过卷积核识别边缘、色块、轮廓,再往下组合出"路在哪""墙在哪""目标方向在哪"这样的高层次信息,本质上跟人开车先看路再看线的认知过程是同一套逻辑。
我想用一个生活类比:一个人第一次进一座陌生商场,要走去某个出口。他一边用眼睛接收眼前的画面(视觉信号),一边尝试迈步往不同方向走(动作决策),走错了就回头,走对了就记住。CNN就是把"看到的东西"翻译成"我大概在什么位置、该往哪去"的脑回路,PPO则是负责"这次方向试对了、下次要多往这个方向拐一点"的学习规则。
所以这套组合解决的核心问题其实就一句话:在只靠画面反馈、没有全局信息的条件下,训练一个智能体逐步学会"看画面→判断方向→输出动作"的完整导航策略。它适合的游戏形态是车、飞船、迷宫寻路这类需要边走边看的场景,不适合棋类那种状态全透明的任务。
2. 双动作空间的设计逻辑:转向离散、油门连续
游戏导航任务里,动作空间的设计是决定训练效率的关键分水岭。最开始我试过把动作合并成"方向 + 速度"的一维连续输出——车学会"慢慢走"挺快,但转弯永远磨磨蹭蹭,因为策略网络总想输出中庸值。也试过完全离散化,比如9个方向组合,结果是动作太粗糙,经常从路肩擦过去。最后我采用了"双动作空间"方式:转向用离散、油门用连续。
这个拆分逻辑不复杂:转向本身是分类问题——左转、直行、右转,顶多加个"大幅左转、大幅右转"就是五类,离散动作空间天然适合表达"选哪个方向"这种决策意图;而油门是程度问题——给多少动力合适,平滑连续地调节比从五个挡位里选更自然。相当于把"方向盘的把向"和"右脚的力度"拆成了两种学习任务,各用各的输出头,各学各的损失信号,互不干扰。
具体网络输出头是这样设计的:
# 策略网络两个输出头示意 class PolicyNet(nn.Module): def __init__(self, n_discrete=3, continuous_dim=1): super().__init__() # CNN 特征提取部分 self.conv = nn.Sequential( nn.Conv2d(4, 16, kernel_size=8, stride=4), nn.ReLU(), nn.Conv2d(16, 32, kernel_size=4, stride=2), nn.ReLU(), nn.Conv2d(32, 32, kernel_size=3, stride=1), nn.ReLU(), ) self.fc = nn.Sequential( nn.Linear(32 * 7 * 7, 512), nn.ReLU(), ) # 离散动作头:输出方向(左/中/右) self.discrete_head = nn.Linear(512, n_discrete) # 连续动作头:输出油门力度(映射到 [-1, 1]) self.continuous_head = nn.Linear(512, continuous_dim) def forward(self, x): feat = self.fc(self.conv(x).flatten(1)) logits = self.discrete_head(feat) value = torch.tanh(self.continuous_head(feat)) * 0.8 return logits, value这里有个细节值得展开说:连续动作输出头用tanh * 0.8而不是暴力tanh直接输出。我踩过坑——如果不加缩放系数,tanh 输出很容易长时间饱和在 ±1 附近,梯度回传接近消失,油门变成了"要么全踩、要么全刹"的开关。乘以 0.8 把输出域收窄到 [-0.8, 0.8],既保留了连续调节能力,又给策略网络留有深入探索的空间。
双动作空间的采样逻辑要分别处理:离散部分从Categorical分布采样转向动作,连续部分按正态分布采样油门值,两者联合成一个复合动作送入环境。PPO 计算对数概率时,要把离散动作负对数似然和连续动作负对数似然相加。这个联合概率计算在实现时特别容易漏,漏了后果是策略梯度里只有一部分动作空间在更新,另一头永远学不动。
从网络结构角度看,两个输出头共享底层 CNN 特征提取层,把"看路"这件事只学一次,比单独拆两个网络省下来大量参数量和训练时间。从决策逻辑角度看,转向和油门天然有主从关系——油门多给的同时往往伴随转向幅度小,急转弯的时候自动松油门,这种关联模式让共享特征加双头的结构很容易学到。我实测下来,双头设计比两个独立网络结构在 120 轮内的收敛速度快了大约 30%。
3. 120轮训练配置与运行记录:奖励曲线从负到正的完整轨迹
训练前我先把所有超参数固定好,全程 120 轮(每轮采样 2048 步)只记录、不大改。这里把配置列出来,方便你直接复现或者作对比基准:
| 配置项 | 参数值 | 说明 |
|---|---|---|
| 输入帧 | 84x84 灰度,4帧堆叠 | 堆叠帧保留运动速度信息,单车帧分不清静止还是移动 |
| 奖励设计 | 到达目标 +50;接近目标 +0.1/帧;碰撞 -10;偏航 -0.01 | 稀疏大奖励 + 稠密小引导 |
| PPO clip 范围 | 0.2 | 标准值 |
| GAE 参数 | gamma=0.99,lam=0.95 | 平衡即时与长期回报 |
| 学习率 | 3e-4,每 40 轮衰减 0.5 | |
| mini-batch | 64 | |
| K epoch | 10 | 每次更新遍历整个 batch 10 次 |
| 熵系数 | 0.01(第 60 轮后设为 0.005) | 前期鼓励探索,后期固化策略 |
第 1 到 20 轮,平均奖励基本在 -5 到 0 之间打转。车频繁撞墙,偶尔进到死角出不来,策略基本是"随机乱走 + 偶尔运气好没撞"。但熵系数 0.01 保证了动作采样足够随机,所以这段时间的价值不是拿奖励,而是让环境回放数据覆盖尽可能多的状态空间。
第 21 到 40 轮是非常明显的"学会转向"阶段。奖励曲线开始爬到 5-10 附近,观察行为发现,车已经能识别路口的左右分支,在接近墙之前会主动偏转。这个阶段油门输出基本都在 0.2-0.4 之间,车开得慢但方向基本对。很有意思的是,我抽查了 CNN 中间层的特征可视化,发现卷积核已经学会了"亮色区域是路面、暗色区域是墙"这种基本纹理区分。
第 41 到 60 轮是质量跃升期,平均奖励跳到 28 左右。车表现出两个典型行为:一是在长直道上油门明显加大,二是不会再一头闷进死路,遇到封闭死角会在入口处刹停然后转向退出。连续动作空间在这里开始真正发挥作用——如果全程只有离散动作,这种"减速避让"行为很难学出来。
第 61 到 80 轮进入瓶颈震荡区。平均奖励卡在 25-30 不上不下,甚至偶尔冲到 40 又跌回 15。我一度以为策略崩了,后来查了日志发现是某几条采样轨迹里出现了极端情况:车连续几次差一点到目标点然后碰撞,PG 估计的 advantage 波动增大,策略更新方向摇摆。这也是 PPO 这种 on-policy 算法的老毛病,样本方差一大,曲线就会抖。
第 81 到 100 轮我开始降学习率并按计划衰减熵系数。曲线逐步稳定在 35-42 之间,特征是"转向 + 油门"的组合战术开始出现。比如 U 型弯道,车会先减速的同时打满方向,出弯后立刻全油门。这种人类驾驶员很自然的操作,策略是在第 90 轮左右才稳定复现的。
第 101 到 120 轮,平均奖励稳定在 38-45,成功到达率约 53%。量化看,平均每轮耗时缩短到初始随机策略的约三分之一。
整个过程最值得记录的其实是"行为演化顺序":先学转向,再学起步油门,再学减速过弯,最后学组合操作。这个顺序对判断训练是否正常很有参考价值。如果你发现你的智能体一开始就在大油门乱冲,那大概率是奖励设计和动作空间的耦合出了偏差,而不是算法不够好。
4. 训练中遇到的三个"卡脖子"问题与完整排查链路
120 轮跑下来,说一帆风顺是假的,中途遇到了好几个能把人逼疯的问题。这里挑三个最有代表性的,每个都给出完整症状、根因定位过程和修复方案,你直接对照踩坑。
4.1 连续动作输出长时间饱和:油门变成了开关
现象出现在第 30 轮左右,查看采样数据发现连续动作头输出的值 90% 时间都等于 ±1 附近。车要么全油门冲墙,要么完全刹停,几乎没有中间态。一开始我怀疑是奖励设计的问题,给油门加了个 0.1 的惩罚项,跑了几轮没改善,反而把平均奖励拉低了。
后来我查了行为日志。连续输出饱和的根因不是奖励,而是网络层初始化和激活函数配合不当。连续头的输入特征是共享 CNN 的 512 维向量,因为初始化权重方差偏大,特征向量的范数波动剧烈,经过 tanh 后很容易被推到饱和区。饱和区梯度几乎为零,策略更新无法修正这个头,形成死循环。
修复方案有三步:第一步,连续头的初始化改用nn.init.orthogonal_(weight, gain=0.01),大幅缩小初始输出方差;第二步,给输出乘上 0.8 缩放(前面代码里有);第三步,在连续动作的损失函数里加入很小的output penalty(0.001 倍的点乘自己),防止数值漂移。改完之后,第 35 轮开始输出分布明显呈钟形,中间态占比恢复到正常水平。
4.2 画面亮度变化导致CNN特征失效
到第 70 轮时,环境里有一段"隧道"区域,进入后画面整体变暗。车在几轮内频繁在这段区域撞墙,但出了隧道又恢复良好。这两个极端状态让 CNN 的卷积核很为难——它的特征是从环境整体亮度分布中学出来的,暗画面下的边缘和纹理信息被压缩,特征分布和正常画面差异巨大。
定位方法是直接可视化网络中间特征。我把某次进入隧道前后的特征图打印出来对比,发现隧道内特征响应强度整体低了一个量级,策略网络等于"半瞎"状态。
既然不能改环境,只能增强输入鲁棒性。我做了三处修改:一是输入灰度化后做归一化时,把像素除以全局标准差而非固定 255,让暗区也能有可区分的数值分布;二是在训练前 60 轮加入随机亮度扰动(亮度乘 0.8~1.2),让 CNN 提前见过"变暗的路况";三是把隧道内试跑轨迹单独回放,额外预填充了 5000 步经验数据,让策略网络有足够样本适应暗环境。三管齐下后,隧道内碰撞率下降约 70%。
4.3 策略更新幅度失控导致的老策略覆灭
这是经典问题。第 88 轮时平均奖励冲到 42,但第 92 轮突然回落到 18——车的行为仿佛一夜回到解放前。查 PPO 更新日志发现,某次更新时clip比例(被裁剪的参数比例)从正常的 8% 飙升到 45%,说明策略一次更新幅度太大,直接跨过了稳定区间。
原因有两层:一是那一轮采样的数据里恰好包含了几条高奖励的轨迹,advantage 方差被拉大,策略对这批数据过拟合;二是学习率虽然在第 80 轮衰减过一次,但衰减幅度不足,导致迈的步子还是太大。
修复思路是"限制更新步伐"。我把 clip 范围从 0.2 临时放宽到 0.3(避免缓冲),学习率从 1.5e-4 进一步降到 1e-4,同时增加一个kl_divergence正则项,每次更新后检查新旧策略的 KL 散度,超过 0.02 就提前终止本轮更新。这套保险机制加上后,从第 93 轮起更新比例明显回落,曲线重新爬回到 40 以上。
4.4 小表格:三个问题速查
| 症状 | 根因 | 修复方案 | 核心指标变化 |
|---|---|---|---|
| 连续动作饱和为开关 | 初始化方差 + tanh 饱和 | 正交初始化、输出缩放、输出惩罚 | 中间态输出占比 10% → 65% |
| 隧道内大量撞墙 | CNN 特征对亮度敏感 | 亮度扰动增强、标准差归一化、经验回填 | 隧道碰撞率 -70% |
| 策略突然退化 | 单轮高奖励轨迹过拟合 + 学习率偏大 | 放宽 clip、加 KL 正则、降学习率 | clip 比例 45% → 12% |
这套排查逻辑本身值得单独说一句:卡住时不要先怀疑算法实现了 bug,先看数据分布。输出饱和看统计、特征失效看可视化、策略退化看 KL 和 clip 比例,每类问题对应一个诊断锚点,比盲目改超参数高效太多。
5. 120轮后的效果评估:这套方案的边界与可迁移性
训练结束后,我用固定随机种子跑了 30 次完整测试,每次从随机起点发车。统计结果是:成功到达率 53%,平均碰撞次数 1.8 次,平均耗时约为随机策略的 31%。成功率不算惊艳,但在纯视觉无全局地图的条件下,这个结果已经能证明"看画面学导航"的逻辑在当前场景里是通顺的。
我还做了一组对比实验:只用离散动作空间(转向3档 + 固定油门)跑同样 120 轮,成功率只有 39%;只用连续动作空间(方向连续 + 油门连续),成功率 44%。双动作空间在这两者之间取得了明显的平衡收益。这个对比说明了最初设计的一个判断——混合动作空间比纯离散或纯连续更适合导航类任务,因为它在"决策意图清晰表达"和"控制精细度"之间取了最优。
从可视化特征的角度复盘,CNN 学到的特征有一个很有意思的规律:在策略已收敛的区域,卷积核会在道路边缘和障碍物边界处产生强响应,而对大面积的平坦路面基本无响应。这说明策略真正依赖的是"边界信息"而不是"路面纹理"。这一点可以迁移到真实机器人导航任务——与其喂高清纹理图,不如喂语义分割图,让 CNN 专注学边界和距离,效率和泛化都会大幅提升。
这个项目的边界我直说:假设你换一个画面风格完全不同的游戏(比如卡通风换写实风),这套模型基本要重新训练,因为 CNN 的低层特征被当前画风固化了。但网络结构、双头设计、PPO 配置全部可以原样迁移,换的只是训练数据。如果你想要更好的迁移能力,方向是领域自适应或者预训练视觉encoder,这也是我下一步准备尝试的方向。
另一点值得说的是"训练进度监测"的方法论。我能在问题发生时快速定位,很大程度上归功于从第 1 轮起就完整记录了每个动作头的输出分布、clip比例、KL散度、CNN各层激活均值这几项指标。很多跑 RL 的人只看 reward curve,但 reward 是众多内部机制的总和信号,必须同时盯住若干内部健康度指标才能判断"奖励低了是因为探索还是因为真的崩溃"。这算是我在这个项目里总结的核心经验。
如果你也要做类似项目,我建议你在训练脚本里留下一个"续训日志"出口:每 10 轮自动保存一次完整的网络参数、优化器状态、熵系数和 noise scale。120 轮后我发现最有价值的不是最终权重,而是第 60 轮保存的那个中间权重——用它做初始化继续训练的收敛速度,比从头跑要快 25%。这种训练资产管理和网络结构、超参数一样重要,但几乎在各种教程里都很少被提。