☰
基于Unity ML-Agents的自行车群体仿真与PPO训练实践
2026/9/29 18:13:51 网站建设 项目流程

简介:基于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填12,Continuous 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 = GetComponent<Rigidbody>(); } 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、Epsilon

release 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辆自行车的样本会同时进入buffer,batch太小会导致更新方向被某几辆车主导;epsilon是PPO裁剪阈值,控制单次更新能改多少,调小了稳定但收敛慢。network_settings里normalize设true很关键,多Agent的位置观测从几米到几十米分布差异大,不归一化会让前几层网络被大数值观测带偏。

3.3 训练启动与 TensorBoard 监控:曲线翻车前早发现

训练启动命令很简单:

mlagents-learn config/trainer_config.yaml --run-id=exp_bike_001 --train tensorboard --logdir results --port 6006

--run-id是本次实验名,换参数就换run-id,方便后期对比;加--train表示正式训练,不加则进入推理模式等待Unity连接。启动后Unity编辑器点Play,Python端会开始收集样本。

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-4,network_settings里normalize设true,buffer_size加到8192把群体样本混得更均匀。改完重新开run-id训练,不要加载旧checkpoint继续。

5.4 训练速度掉到个位数 FPS:决策频率和 Ray 数量

现象:20个自行车Agent之后,训练FPS跌到5以下,日志时间戳明显变慢。

原因:决策周期等于1意味着每个物理帧都做一次策略推理,20个Agent的推理次数暴增;加上12根射线各自做物理查询,CPU被抽干。

解决:把决策周期从1改到5,相当于每0.1秒决策一次,骑行动作完全可以接受;射线数量从12降到8,Max 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-id=eval_${ckpt}_${seed} \ --initialize-from=${ckpt} \ --port=5006 # 不加 --train,即为推理模式,不更新策略 done done

eval_config.yaml把max_steps设小,只做推理不训练,固定Unity端随机种子。跑完用第4.3节的脚本统计碰撞率和到达率,挑一个中位表现最好的checkpoint,导出.onnx后放进Unity里手动试骑。

验证时我会把评估场景里的障碍物随机挪几个位置再跑一遍,确认策略不是背板子记忆出来的。策略对于人来说是黑匣子不可怕,可怕的是你没给它留出验证出口——试骑时重点观察十字路口和窄路两处,自行车群体最常在这里露怯。如果Agent在路口犹豫,说明目标方向的Reward占比还不够;如果它在窄路贴着墙怕,说明射线长度不够、前瞻距离太短。一套能落地的自行车群体仿真,从来不是训完就结束,而是靠多轮“训练-评估-试骑”打磨出来的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询