
简介基于Unity ML-Agents release 15的智能自行车机器人仿真系统面向智能交通、强化学习与机器人仿真研究者用于训练自行车智能体在复杂城市交通中实现自主导航、避障并与群体协同行进提升城市骑行效率与安全性。包内共566个文件核心包括C#脚本、Prefab预制体、Asset场景资产、ONNX及PT模型文件同时包含TensorBoard训练日志、材质贴图、Html说明文档等压缩包大小34.12MB目录结构完整可直接导入Unity项目复现实验。目前已有71人学习使用。压缩包不仅提供可运行的仿真场景和预训练模型还配有训练记录、场景地形文件与说明文档可帮助读者掌握ML-Agents强化学习流程理解自行车群体行为建模并开展参数调优适合作为智能交通与自主导航研究的实验基础也能为后续二次开发提供完整起点。1. 为什么自行车群体仿真比单车更难把 release 15 放在正确的位置城市交通仿真里自行车是最难写规则的那一类实体。机动车可以套跟车模型行人可以套社会力模型但自行车既有机动性又有随意性——超车、让行、并线、闯黄灯边缘本质上都是骑车人依据当前视野和速度做出的独立决策。基于Unity_ML-Agents_release_15开发的智能自行车机器人仿真系统把这种决策交给强化学习每辆自行车是一个Agent用PPO在Unity场景里反复试错训练出自主导航和避障策略再把多个Agent同时放进路网模拟真实交通中的自行车群体行为。对做智能交通仿真和机器人避障研究的人来说这套系统的价值很直接不用手工标定几十条跟车、超车和让行的规则让群体行为自己涌现出来。难点也随之而来——单辆自行车学会避障不难难的是十几辆车同时出现在交叉路口时它们彼此之间能不能形成自然、不傻的互动。这篇文章沿着场景搭建、PPO训练、群体验证、常见翻车四条线走一遍。2. 搭场景和 Agent用 Unity ML-Agents 15 立起一辆能自己骑的自行车这一步的目标是把“一个能训练的自行车Agent”立在场景里而不是直接开始调超参。我会先解释为什么release 15适合做群体仿真再给出最小可复现的场景配置和C#骨架最后谈传感器选型。做完这一章你在Unity编辑器里应该能骑着一辆用方向键控制的自行车在路网上走。2.1 为什么选 release 15稳定、案例多、适合群体仿真第一个问题往往是ML-Agents都出到二十几个release了为什么偏要锁在15我选它的判断标准不是“新”而是“训练管线少折腾”。release 15的RayPerceptionSensor3D和Behavior Parameters配置方式稳定网上的踩坑案例和论坛讨论几乎覆盖了所有常见问题升级到新版后API变更集中在命名空间、包依赖和个别传感器组件上对做交通仿真的人来说迁移成本不小收益却很有限。另一个对比对象是ROS小车自主导航仿真。ROS那套全局路径规划加局部代价地图的框架很成熟但它更适合单个轮式机器人自行车群体里几十辆车同时决策、彼此避让规则型规划器很难覆盖所有交互情况。强化学习的优势是反应式避障和群体行为涌现所以把Unity、ML-Agents和强化学习放在同一个标题下是顺理成章的选型。2.2 最小场景与 C# 骨架CollectObservations 和 OnActionReceived场景结构从简一个Plane当地面放一段双车道Cube拼出来车道尽头放一枚圆柱体作为Target再扔几个立方体当作静态障碍物。自行车的模型可以先用Capsule代替挂上Rigidbody和Behavior Parameters组件Behavior Name填“Bike”Vector Observation Size填12Continuous Actions填2再挂一个RayPerceptionSensorComponent3D。组件挂好之后写BikeAgent.cs。下面是一个能直接跑通的最小版本using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; using Unity.MLAgents.Actuators; public class BikeAgent : Agent { public Transform target; // 目标点编辑器里拖入 private Rigidbody rb; private float maxSpeed 6f; // 骑行上限约 21.6 km/h private float steerRange 0.5f; // 转向上限弧度 public override void Initialize() { rb GetComponentRigidbody(); } public override void CollectObservations(VectorSensor sensor) { // 自身位置 3 维 sensor.AddObservation(transform.localPosition); // 目标方向单位向量 3 维 sensor.AddObservation((target.localPosition - transform.localPosition).normalized); // 自身速度 3 维 sensor.AddObservation(rb.velocity); // 自身朝向 3 维 sensor.AddObservation(rb.transform.forward); } public override void OnActionReceived(ActionBuffers actions) { float steer Mathf.Clamp(actions.ContinuousActions[0], -1f, 1f); float throttle Mathf.Clamp(actions.ContinuousActions[1], 0f, 1f); // 转向作用在 yaw 轴速度沿 forward 方向推进 rb.MoveRotation(rb.rotation * Quaternion.Euler(0f, steer * steerRange * Mathf.Rad2Deg, 0f)); rb.MovePosition(rb.position rb.transform.forward * (throttle * maxSpeed * Time.fixedDeltaTime)); } public override void Heuristic(in ActionBuffers actionsOut) { // 编辑器里手动试车用训练时不需要 var cont actionsOut.ContinuousActions; cont[0] Input.GetAxis(Horizontal); cont[1] Mathf.Clamp01(Input.GetAxis(Vertical)); } }这份代码的逻辑是12维向量观测由“位置3 目标方向3 速度3 朝向3”组成射线传感器是单独组件不占向量观测维度。动作空间2维分别是转向和油门连续值输出后再做一次Clamp把网络输出约束到物理上合理的区间。MoveRotation和MovePosition是物理驱动的移动方式保证碰撞体和刚体保持一致不会出现模型穿墙但碰撞体在后面的情况。参数上maxSpeed取6m/s接近城市自行车巡航速度steerRange取0.5弧度约等于28.6度每决策步——太小转不过弯太大容易原地绕圈。RayPerceptionSensorComponent3D的射线数量、角度和长度见下一节。注意Rigidbody的Constraints必须冻结X和Z轴旋转否则自行车起步就侧翻。这是自行车仿真最先踩的坑。2.3 射线感知参数表群体仿真里传感器怎么配射线传感器是群体仿真里性价比最高的感知方案。它的计算量小每根射线只做一次物理查询而且观测确定性好——同一状态下射线命中结果完全一致训练更容易收敛。视觉传感器的信息量更大、泛化潜力更强但训练时要跑CNN网络20个Agent同时采集视觉观测训练速度会掉一个数量级不建议作为第一版方案。参数建议值说明Ray Number12覆盖前方扇形检测区Max Ray Distance25 m城市骑行看25米内足够Ray Angles120°前方60度左右对称扇形Stacked Raycasts1自行车高度差不大不需要多堆叠Ignore Triggertrue避免把触发器误判为障碍物射线数量每增加1根每个Agent每决策步就多一次物理查询20个Agent就是20倍开销。在群体仿真里优先压射线数量和决策频率不要急着上视觉传感器。如果你以后想做视觉输入的对照组release 15也支持Camera Sensor但请先跑通当前这套射线版本再做消融对比。3. 训练导航与避障策略Reward 设计与 PPO 三组核心参数Agent动起来只完成了三分之一。真正决定仿真系统能不能用的是Reward设计和PPO超参这两块。这一章给出可复用的Reward分项、一组默认的PPO配置以及TensorBoard里该盯哪些曲线。3.1 Reward 设计距离引导、避障惩罚与行为代价训练自行车不能只给“到达2”这种稀疏奖励否则Agent在50万步里基本靠瞎撞。常见做法是距离势函数引导再加碰撞惩罚和时间惩罚。下面是三组Reward叠加的写法public class BikeAgent : Agent { private float previousDistance; public override void OnEpisodeBegin() { previousDistance Vector3.Distance(transform.localPosition, target.localPosition); } public override void OnActionReceived(ActionBuffers actions) { // 动作执行部分见 2.2这里省略 float nowDistance Vector3.Distance(transform.localPosition, target.localPosition); // 距离缩短给正奖励拉远给负奖励 AddReward((previousDistance - nowDistance) * 0.5f); previousDistance nowDistance; // 每步时间惩罚防止原地磨时间 AddReward(-0.001f); if (nowDistance 1.5f) { AddReward(2f); // 到达目标 EndEpisode(); } } private void OnCollisionEnter(Collision collision) { if (collision.collider.CompareTag(Obstacle) || collision.collider.CompareTag(Bike)) { AddReward(-1f); // 撞障碍物或撞其他自行车 EndEpisode(); } } }这套Reward的本质是Potential-based reward shaping用距离差而不是当前距离绝对值做奖励不会改变最优策略但能让Agent更快找到目标方向。0.5是距离奖励系数我一般从0.2起步如果发现Agent绕圈刷距离奖励就调大到0.5以上。注意障碍物和自行车都要打Tag避免碰撞检测漏掉同行者。3.2 PPO 三组必调参数学习率、Batch Size、Epsilonrelease 15的默认训练器配置放在trainer_config.yaml里。下面是针对自行车群体场景的一组稳定起点behaviors: Bike: trainer_type: ppo hyperparameters: learning_rate: 3.0e-4 learning_rate_schedule: linear batch_size: 128 buffer_size: 4096 beta: 5.0e-3 epsilon: 0.2 lambd: 0.95 num_epoch: 3 network_settings: normalize: true hidden_units: 256 num_layers: 2 max_steps: 5.0e6 time_horizon: 64 summary_freq: 10000参数常见范围什么时候改learning_rate1e-4 ~ 3e-4训练震荡时往下降batch_size128 ~ 256多Agent样本混在一起时加大epsilon0.2Policy Loss冲高时降到0.15三个参数里learning_rate最重要它控制每一步策略更新的步长batch_size在多Agent场景下要适当加大因为20辆自行车的样本会同时进入bufferbatch太小会导致更新方向被某几辆车主导epsilon是PPO裁剪阈值控制单次更新能改多少调小了稳定但收敛慢。network_settings里normalize设true很关键多Agent的位置观测从几米到几十米分布差异大不归一化会让前几层网络被大数值观测带偏。3.3 训练启动与 TensorBoard 监控曲线翻车前早发现训练启动命令很简单mlagents-learn config/trainer_config.yaml --run-idexp_bike_001 --train tensorboard --logdir results --port 6006--run-id是本次实验名换参数就换run-id方便后期对比加--train表示正式训练不加则进入推理模式等待Unity连接。启动后Unity编辑器点PlayPython端会开始收集样本。TensorBoard里我一般盯三条线Environment Reward、Episode Length、Value Loss。前20万步Reward来回震荡是正常的关键是不要直线向下Episode Length到50万步以后应该明显下降说明Agent在更短的时间步内到达目标Value Loss稳步下降属正常突然冲高基本就是学习率过大或normalize没开。注意训练中途想验证手感可以随时导出当前checkpoint的.onnx在Unity的Behavior Model里切到推理模式手动试骑不需要停训练。4. 把单车变成群体随机化、Curriculum 与三项验证指标单辆自行车学会避障不难难的是让10辆、20辆自行车同时在路上表现得不傻。这一章讲群体行为的涌现条件、难度递进方式和验证指标把“多智能体强化学习”在工程里的落地形态说清楚。4.1 共享策略下的个体异构观测群体行为的真相ML-Agents并不是每个Agent独立训练一套策略而是同一Behavior Name下的所有Agent共享同一个网络。训练时它们各自采样梯度汇合后更新同一个策略。所谓群体行为其实来自个体观测的不同——每辆自行车的位置、速度、目标方向和射线命中的邻居都不一样共享策略必须学会对“我此刻看到的局面”做出恰当反应。这是从强化学习到多智能体强化学习的中间形态。真正的多智能体强化学习要考虑对手建模和通信机制但工程上绝大多数自行车群体仿真用“共享策略异构观测”就能跑出自然行为投入产出比最高。想让群体行为涌现三个条件缺一不可每辆自行车的目标点互相不同、射线传感器把其他自行车当障碍物检测、初始位置和朝向随机化。4.2 难度递进用 Curriculum 让群体从稀疏走向密集直接从20辆车加密集障碍物开始训练收敛速度会慢到让人怀疑人生。常见做法是Curriculum Learning先让Agent在稀疏环境里学会基本骑行再逐步增加车辆数和障碍密度。Bike: measure: progress thresholds: [0.3, 0.5, 0.7] min_lesson_length: 200000 signal_smoothing: true parameters: bike_count: values: [5, 10, 15, 20] obstacle_density: values: [0.05, 0.10, 0.15, 0.20]measure有两种可选progress表示按归一化训练进度切换难度reward表示按平均Reward阈值切换。thresholds从0.3开始意思是训练进度达到30%才把bike_count从5升到10避免环境过早变难。min_lesson_length保证每个难度等级至少训20万步再切换防止在环境变化瞬间训练还没稳定就跳级。启动训练时加--curriculum参数指定这个文件。注意修改curriculum后要开新的run-id重新训练直接加载旧checkpoint继续会导致难度曲线错乱。4.3 验证指标碰撞率、到达率、平均速度怎么算群体行为做没做出来不能只看Reward。要固定一个评价场景跑30个episode统计碰撞率、到达率和平均速度。下面这段Python脚本读取Unity端导出的每回合日志import json from statistics import mean rows [] with open(bike_episode_log.jsonl) as f: for line in f: rows.append(json.loads(line)) total len(rows) collisions sum(1 for r in rows if r[collision]) arrived sum(1 for r in rows if r[arrived]) speeds [r[avg_speed_kmh] for r in rows if r[arrived]] print(f总episode数: {total}) print(f碰撞率: {collisions / total:.2%}) print(f到达率: {arrived / total:.2%}) if speeds: print(f到达样本平均速度: {mean(speeds):.2f} km/h)这份日志由Unity侧在每个Episode结束时写出字段至少包括collision、arrived、avg_speed_kmh。计算平均速度时只看到达样本避免把撞车停在半路的低速混进去。速度太低说明Agent在“龟速保命”速度高但碰撞率高说明策略在蛮干——这两个指标要一起看。对比两个checkpoint时必须用同一批随机种子跑同一场景否则结果不可比。5. 训自行车群体的五个常见翻车现场与排查路径release 15的大多数问题不是算法问题是组件配置和训练管线问题。这一章挑五个我踩过的坑按现象、原因、解决三部分写。5.1 智能体原地打转或贴墙蹭距离奖励尺度不对现象训练几十万步Agent在墙角反复横跳Episode Length不见下降。原因Reward给得太稀疏或者距离奖励系数太小“转向”这个动作的收益不明显另一种可能是steer范围过小动作根本转不过去。解决把距离奖励系数调到0.5用(previousDist - nowDist)这种距离差而不是(1 / nowDist)这种非线性项把steerRange从0.3弧度提到0.5弧度每个Episode开始时把目标点做少量随机偏移打破对称位置导致的零梯度区。5.2 多Agent训练后碰撞率高射线感知把同类当背景现象单Agent避障指标很好群体训练后车与车频繁擦碰。原因RayPerception的层级过滤把Bike层级排除掉了射线物理查询默认忽略该层或者自行车的Collider设成了isTrigger射线查询对Trigger默认不生效。解决在RayPerceptionSensorComponent3D的Detectable Tags里同时加入“Obstacle”和“Bike”自行车Collider不要只挂Trigger用物理碰撞体。碰撞检测统一走OnCollisionEnter不要混用Trigger和Collider判断逻辑。5.3 训练到一半 Value Loss 爆炸Reward 横跳现象前30万步正常之后Value Loss指数级冲高Reward开始上下剧烈摆动。原因learning_rate维持3e-4偏大配合linear schedule时后段更新步长仍然太大另一个是normalize没开位置观测几十米和速度观测几米每秒量级不一致网络训练不稳定。解决learning_rate降到1e-4network_settings里normalize设truebuffer_size加到8192把群体样本混得更均匀。改完重新开run-id训练不要加载旧checkpoint继续。5.4 训练速度掉到个位数 FPS决策频率和 Ray 数量现象20个自行车Agent之后训练FPS跌到5以下日志时间戳明显变慢。原因决策周期等于1意味着每个物理帧都做一次策略推理20个Agent的推理次数暴增加上12根射线各自做物理查询CPU被抽干。解决把决策周期从1改到5相当于每0.1秒决策一次骑行动作完全可以接受射线数量从12降到8Max Ray Distance从50米压到25米。先单进程跑到20FPS以上再考虑并行并行训练是另一个话题。5.5 换个环境训练不了Unity 包与 Python 版本错位现象升级Unity编辑器或换了台电脑后mlagents-learn一连接就报Communication Error退出。原因release 15的Unity包与Python包通过protobuf通信版本不匹配时消息定义或端口约定对不上。解决以Unity Package Manager里com.unity.ml-agents对应版本的发布说明为准用pip安装配套的mlagents包或者干脆锁住编辑器版本不动把这套项目当作固定依赖。别急着升新包交通仿真项目的稳定性比新功能值钱。6. 最后一步用清单式评估替代“看 Reward 猜效果”训练收尾阶段我一般不做“一次训到位”的期待。自行车骑行本身有很强的物理性Reward和超参的配合很大程度上是手感问题属于玄学范畴。我习惯先跑30万步看Reward曲线的走势能向上就续向下立刻降learning_rate重开每50万步存一个checkpoint训练失败也有后悔药可吃。固定场景评估用下面这段脚本批量跑三个种子分别对比不同run-id的checkpoint# 评估两个 checkpoint 在固定场景的表现 for ckpt in exp_bike_001 exp_bike_002; do for seed in 1 2 3; do mlagents-learn config/eval_config.yaml \ --run-ideval_${ckpt}_${seed} \ --initialize-from${ckpt} \ --port5006 # 不加 --train即为推理模式不更新策略 done doneeval_config.yaml把max_steps设小只做推理不训练固定Unity端随机种子。跑完用第4.3节的脚本统计碰撞率和到达率挑一个中位表现最好的checkpoint导出.onnx后放进Unity里手动试骑。验证时我会把评估场景里的障碍物随机挪几个位置再跑一遍确认策略不是背板子记忆出来的。策略对于人来说是黑匣子不可怕可怕的是你没给它留出验证出口——试骑时重点观察十字路口和窄路两处自行车群体最常在这里露怯。如果Agent在路口犹豫说明目标方向的Reward占比还不够如果它在窄路贴着墙怕说明射线长度不够、前瞻距离太短。一套能落地的自行车群体仿真从来不是训完就结束而是靠多轮“训练-评估-试骑”打磨出来的。希望帮到你。本文还有配套的精品资源点击获取