
搞机器人强化学习的人尤其是做导航、避障、多车调度这类任务的基本都经历过同一个阶段在真实小车上调算法调到怀疑人生。我现在的做法是先在一套叫 Microduck 的仿真强化学习流程里把算法验证完再往实体车上迁移。Microduck 这个名字看起来像“小鸭子”其实它对应的是 Duckietown 这种微缩自动驾驶平台但真正有价值的不是那辆模型车而是从仿真环境、传感器模拟、奖励函数到强化学习算法的一条完整链路。这篇文章就把这条链路拆开讲环境怎么搭、算法怎么接、训练时哪些坑必须绕开。适合刚接触强化学习、又急着跑仿真实验的工程师和学生也适合准备做 sim-to-real 迁移的团队。1. 为什么仿真强化学习流程要先搭好1.1 真实机器人试错成本太高仿真是第一个缓冲层真实小车跑强化学习大多数时候不是在调算法而是在调环境。电池电压掉一点策略行为就变了撞一次墙角要手动把车放回起点训练一晚上想攒经验数据结果发现车早就卡在桌椅腿旁边空转了。更麻烦的是强化学习天然需要探索而探索意味着要试错。在真实环境里试错轻则原地打滑重则电机过热、结构件断裂这些都是钱和时间。所以我在做这类项目时坚持先用仿真兜底。仿真给了一个低成本试错的缓冲层跑坏了直接 reset状态可以恢复物理参数可以任意改。Microduck 就是从这个需求出发的项目。它不追求照片级真实感重点是把动力学反馈、传感器噪声和任务逻辑做对。一个 episode 训练样本在仿真里按分钟计算在真车上可能要人工干预半天这个差距决定了强化学习项目能不能快速迭代。1.2 Microduck 的定位轻量、够用、可复现Microduck 不是一个新算法而是一套流程。它参考了 Duckietown 里“微缩城镇 自动驾驶小车”的思路但把场景极简化一辆差速驱动小车、一张栅格地图、若干动态障碍物外加一个多目标导航任务。我当初给自己定了三个原则分别是轻量、够用、可复现。轻量是指仿真器不能吃满显卡和内存。我自己默认用 PyBullet 做物理引擎只加载小车和地面模型一张地图几十 MB 足够训练时 CPU 占用也不会太高。够用是指环境必须能表达移动机器人决策的核心问题车能不能走、会不会撞、能不能到目标点。我加了碰撞检测、激光雷达、里程计和差速运动模型这些在真实小车里都是标配。可复现则是每次实验必须能固定随机种子保证不同算法之间比较结果是有意义的。没有这一步后面所有指标都是噪声。1.3 先想清楚要解决什么决策任务搭环境之前一定要先问自己一个问题你到底想让小车学会什么决策Microduck 一开始锁定的任务就是点目标导航给定起点、终点和静态地图小车需要学会避开障碍物到达目标。这个任务看起来简单但它覆盖了强化学习动作空间设计、奖励塑造成本、状态观测设计的大部分关键问题。选这个任务还有一个好处它容易扩展。把静态地图改成带动态障碍物的地图就是动态避障把一台车改成多台车就是多 AGV 协调把目标改成一条路径轨迹就能做车道跟踪。如果一开始就贪心想把所有场景都做进去环境代码会复杂到连调试都没法下手。先从最简单的版跑通再逐步叠加这个顺序我建议不要反过来。2. 环境搭建时最容易出错的几个细节2.1 环境依赖与目录结构Microduck 的仿真环境我采用 PyBullet 作为底层物理引擎。选它的原因很简单Python 接口干净安装起来不用处理一堆 ROS 中间件。如果你已经非常熟悉 Gazebo 和 ROS完全可以用 gym-gazebo 替换底层但只要保持了 gymnasium 风格接口上层算法代码就不需要改。一个比较典型的安装流程是这样的git clone https://github.com/your-project/microduck.git cd microduck conda create -n microduck python3.9 -y conda activate microduck pip install pybullet gymnasium numpy matplotlib stable-baselines3装完之后目录结构我建议按这样分microduck/ ├── envs/ │ ├── microduck_env.py │ └── map_loader.py ├── configs/ │ ├── default.yaml │ └── reward.yaml ├── agents/ │ ├── ppo_train.py │ └── evaluate.py └── scripts/ └── run_tensorboard.sh你可能觉得多建几个文件夹浪费几分钟但后面训练不同算法、对比不同 reward 配置时这套结构能帮你省下大量找文件的时间。我见过太多项目训练脚本和回报函数混在一个文件里最后改了一行 reward 又开始全量重训根本不知道改的是哪份代码。2.2 地图、传感器和动态障碍物怎么建模才合理地图方面我用一张 128x128 的二值 PNG 当输入白色像素是可通行区域黑色像素是障碍物边界。加载之后转成占据栅格小车初始位置和目标点都从地图参数里读取。你用灰度图甚至矢量图也可以只要在 map_loader 里统一转成矩阵就行。传感器方面我给虚拟小车配置了三个核心感知模块2D 激光雷达、里程计和碰撞检测器。激光雷达射线数量设 64 根探测距离 5 米。射线太多会让训练变慢太少则小车会漏掉侧面障碍物。里程计返回车体坐标系下的线速度和角速度。碰撞检测器返回布尔值和小车与障碍物的最近距离。这三个信息拼在一起就是强化学习算法的观测输入。还有一个容易被忽略的点每次 reset 时车的位置和朝向都要加一点随机扰动否则策略会记住“从同一个起点出发”这件事学不到泛化能力。动态障碍物也不是随便放的。我让障碍物沿固定路线移动但初始相位和移动速度在每次 reset 时随机化。这样做的好处是小车在训练初期能看到一个“会动的世界”但又不至于难度爆炸。如果一开始就让所有障碍物乱跑很多随机种子下小车根本活不过前几个 episodereward 曲线全是零你很难判断是算法问题还是环境问题。2.3 奖励函数不是拍脑袋是先定行为边界奖励函数是强化学习里最玄学的地方也是 Microduck 项目里我调得最久的部分。我的经验是不要一开始就设计一堆复杂规则先用“稀疏奖励 少量引导”跑通再逐步加东西。具体来说我用的奖励形式是这样的到达目标点10碰撞障碍物-10与上一步相比离目标点更近0.2 * distance_change每个 step 附加存活惩罚-0.01这个设计的关键在于引导项。distance_change 如果乘的系数太大小车会为了多拿引导奖励而贴着墙走乘得太小引导效果不明显训练初期小车到处乱窜。我最终把系数固定在 0.2配合碰撞惩罚 -10才得到比较稳定的行为。还有一个经常被人忽略的点一定要区分 episode 结束原因。在 gymnasium 里terminated 表示真实终止比如到达目标或碰撞truncated 表示超时或步数上限导致截断。强化学习算法在计算收益时对这两种结束的处理方式不同。我在 Microduck 里默认设置单 episode 步数上限为 300如果 300 步还没到目标就按 truncated 处理并把这一步的 value 用当前价值估计 bootstrap。很多新手在这里直接把 both 当成 done结果小车永远学不会长距离导航。3. 训练流程里的关键实现3.1 算法从 PPO 开始别一上来就 SACMicroduck 的算法接入我默认从 PPO近端策略优化开始。原因是 PPO 对超参数不那么敏感训练曲线相对平滑而且 2048 步一个 batch 的更新方式很适合在小规模环境上反复实验。如果你直接上 SAC可能样本效率更高但 reward scale、学习率、自动温度系数这些参数会引入一堆额外变量出了问题你很难定位是环境还是算法的问题。我把环境包装成 gymnasium 的 Env 类实现 reset、step、render 三个方法。动作空间是二维连续动作线速度[-0.5, 0.5]m/s角速度[-1.0, 1.0]rad/s。观测空间则拼接了激光雷达数据、当前速度、目标相对坐标和上一时刻动作。封装好之后接 Stable-Baselines3 的 PPO 只需要几行配置。如果你不想依赖现成库也可以自己写 PPO但前期我不建议重复造轮子先把端到端链路跑通更重要。3.2 训练主循环别把 terminated 和 truncated 混在一起训练主循环本身不复杂但数据流的细节会直接影响收敛效果。我常用的伪代码如下obs, _ env.reset() buffer RolloutBuffer(capacity2048) while step total_steps: action, _ policy(obs) obs_next, reward, terminated, truncated, info env.step(action) buffer.store(obs, action, reward, terminated, truncated) obs obs_next if buffer.full: policy.update(buffer) buffer.clear() if terminated or truncated: obs, _ env.reset()代码看起来简单核心问题在 buffer 里每一项要同时记录 terminated 和 truncated。PPO 计算 GAE 时只有 terminated 为 True目标价值才设为 0truncated 为 True 时应该用当前 critic 网络输出的价值估计继续往后推。如果你把两个标志合并成一个 done那么小车在到达 300 步上限被截断时会被误判为失败value target 被压到 0策略就会越来越不敢往外走训练最后变成原地踏步。另一个容易踩的坑是采样和更新频率。PPO 不是每个 step 都更新而是收集满一个 rollout buffer 再统一更新。我把 buffer 容量设成 2048也就是小车跑约 6 到 7 个 episode 收集一次数据。更新时采用 mini-batch 方式和 GAEGAE lambda 设 0.95。如果你每个 step 都更新不仅慢而且策略会被单条轨迹的噪声带偏。3.3 超参数和观测归一化超参数我建议先固定一组比较稳的 baseline而不是一上来就调参。Microduck 里我常用的初始配置如下表所示参数取值说明学习率3e-4对 PPO 来说比较安全batch_size2048采样缓冲区容量minibatch_size64每次梯度更新的样本数GAE lambda0.95优势估计平滑程度折扣因子0.99长距离目标依赖未来奖励clip 系数0.2PPO 的裁剪范围更新轮数10每个 buffer 更新次数控制频率10-15 Hz与真车保持一致这套参数跑 300 万步左右Microduck 里的智能车基本能从随机乱撞变成会找目标。如果完全不上涨问题多半不在超参数而在 reward 设计和状态归一化。观测归一化是我特别想强调的一点。速度、激光雷达距离、目标坐标这三类数据量纲完全不一样。激光雷达距离是 0 到 5 米速度是 -0.5 到 0.5目标坐标可能是几十像素级别。如果不做归一化PPO 的策略头会被量级大的维度带偏。我在 environments 里维护了一个 running mean 和 running std在训练过程中逐步更新推理时固定统计量。做完这一步reward 曲线稳定性提升非常明显。4. 调试实录常见问题与排查方法4.1 训练发散、NaN 和虚假碰撞训练过程中最让人头疼的就是 reward 突然变成 NaN。我遇到过的 NaN 基本来自三类reward 计算分母为 0、激光雷达某帧为空、策略输出的 action 超出仿真器允许范围。排查办法是在 env 里的 step 函数加上断言把所有 NaN 出现时的 obs、action、reward 打印出来再回放对应 frame。有一个具体例子我在计算 distance_change 时取的是“目标点当前距离减上一时刻距离”如果小车正好在某一步到了目标点目标距离变成 0此时再除以上一步距离就可能产生除零。把公式改成差值而不是比值问题就消失了。虚假碰撞也很常见。PyBullet 里如果碰撞体尺寸比视觉模型大小车明明离障碍物还有一段距离碰撞检测就被触发。我的处理是把碰撞体缩小 10%并且增加一个条件只有当小车速度低于 0.05m/s 且碰撞体持续接触障碍物时才判定为真实碰撞。这个缓冲逻辑看起来有些放宽碰撞标准但实际训练出来的策略更加稳定不会因为碰撞检测抖动而变得畏手畏脚。4.2 小车不走、原地转圈、只走直线这几个现象几乎每个做过 RL 导航的人都会遇到。我先说小车不走。如果每一步存活扣 -0.01而前进没有明显的正向反馈策略最优解就是站在原地不动因为这样扣分最少。解决办法很简单把存活惩罚去掉或者给“向目标靠近”一个更明显的正奖励让走动比不走更划算。原地转圈通常是观测里缺少目标方向信息。激光雷达只能感知局部障碍物小车并不知道目标点在自己前方还是后方它当然只能靠旋转碰运气。我把目标点在车体坐标系下的坐标归一化后拼进观测问题立刻改善。只走直线则说明碰撞惩罚权重不够小车觉得撞上也没关系因为离目标更近的引导奖励盖过了碰撞惩罚。我当时的做法是把碰撞惩罚从 -10 调到 -20并对连续靠近障碍物的行为追加一个小惩罚让小车学会留出安全距离。4.3 仿真到实物的迁移误差怎么缩小仿真练出来的策略直接搬到真车上成功率通常惨不忍睹。Microduck 在这方面做了三件事随机化动力学、动作低通滤波、限制最大速度。动力学随机化是在每次 reset 时给轮子的半径、摩擦系数增加 5% 到 10% 的随机扰动。这样训练出来的策略不会依赖精确的物理参数到了真车上能更快适应。动作低通滤波是给 policy 输出的动作加一个一阶惯性环节把突变的控制量平滑掉这在真车上能明显减少电机抖动。限制最大速度则更关键我把仿真中的最大速度设为真车的 60% 到 70%。前期我试过把仿真速度设成真车两倍来训练策略学会了一路全速冲真车上根本刹不住。后来把仿真速度限制在真车能力范围内迁移效果一下子好了很多。5. 从 Microduck 延伸出去的强化学习方向5.1 离线强化学习用沉淀数据训练 IQLMicroduck 运行一段时间后你会积累大量历史轨迹。这些数据如果直接扔掉非常可惜。离线强化学习可以只靠历史数据训练策略不需要和仿真环境实时交互。IQLImplicit Q-Learning是我试过比较稳的离线算法。它不依赖动作选择网络通过分位数回归避免分布外动作的价值估计过高。我做过对比用随机策略和 PPO 策略各跑 500 个 episode混合成一个离线数据集。IQL 在这个数据集上训练出来的策略其导航成功率不输于在线 PPO而且策略的方差更小。这说明只要数据覆盖足够广离线训练可以作为在线训练的替代或预热阶段。5.2 多 AGV 路径规划和交通协调Microduck 的场景稍加修改就能变成多 AGV 路径规划问题。把一张地图上放三到五辆虚拟小车每辆车有独立起点和目标点共享全局地图但只使用自己的局部观测。这个场景下单智能体强化学习很容易出现“抢路”行为两辆车同时挤在一个路口谁都不让谁。我的建议是不要急着上多智能体强化学习先试试分层方案全局路径规划用 RRT 或者 A* 算出一条参考路径再用 Microduck 里的单智能体 RL 做局部避障和路径跟踪。只有在多个 AGV 频繁交互的动态场景里才需要考虑集中训练、分布执行的多智能体框架。入门阶段把一个智能体的问题做好比追多智能体算法更稳妥。5.3 基于模型的强化学习与因果强化学习如果你希望减少与仿真环境的在线交互次数可以考虑基于模型的强化学习。它的核心是先学一个动力学模型用小车的当前状态和动作预测下一时刻状态再基于这个模型做规划或价值估计。Microduck 的仿真器足够轻量刚好适合做这种实验。但要注意动力学模型本身也会有误差误差累积后策略可能在仿真里表现不错换到真车立刻失效。因果强化学习是更前沿的方向。它把因果推断工具嵌入强化学习流程核心能力是识别变量之间的因果关系而不是简单的相关性。放到 Microduck 里一个最简单的因果干预是多了一个环境开关某个障碍物是否出现、地面摩擦系数是高是低训练时随机切换这个因素观察策略行为是否跟着改变。这样可以检验策略到底依赖了哪个真正的因果变量。这个方向更适合做研究型项目离工程落地还有段距离但如果想了解最新的强化学习趋势值得留个心眼。调试强化学习流程时我自己的习惯是先把仿真速度调到实时以上确保一天能看到足够多的轨迹。TensorBoard 里只盯三个指标平均回报、episode 长度、碰撞率。还有一个常被忽略的小技巧每次训练完用固定的 10 个随机种子各评估 20 次记录成功率和平均到达时间。这一步相当于给策略建立基准线下一次算法改了什么、效果是否变好一眼就能看明白。希望这套 Microduck 流程能帮你少踩几个坑快速跑出自己的第一个仿真强化学习 demo。