资讯动态

Unity ML-Agents强化学习:自行车智能体仿真训练与避障实战

发布时间:2026/10/26 1:02:30 来源:尧图企业网站定制
简介面向智能交通与强化学习研究者的Unity ML-Agents自行车智能体仿真项目基于release_15版本开发通过强化学习训练自行车智能体在复杂城市交通环境中实现自主导航、避障及群体协同行驶适用于算法验证、毕业设计与智能交通课题研究。资源共566个文件压缩包约34.12MB涵盖Unity场景与预制体asset/prefab、C#控制脚本cs、训练完成的神经网络模型onnx/pt、训练日志及材质贴图mat/png/jpg等目录结构完整便于二次开发与实验复现。已有71人浏览学习。附赠说明文档与额外训练场景可帮助用户快速搭建仿真环境、理解强化学习奖励设置与群体行为模拟思路内置的EasyRoads3D地形与多种路口动态资源可用于搭建不同复杂度的交通测试场景是探索自行车智能化决策问题的实用参考。1. 当一辆自行车要在车流里自己找路为什么必须用强化学习仿真把一辆自行车扔进晚高峰的城市路口它要同时处理侧后方来车、右转不打灯的出租车、突然窜出的外卖骑手还有自己在两轮着地时的物理平衡。这种连续决策问题用状态机写规则几乎无解——你根本枚举不完所有冲突组合。真实路测又太危险一次碰撞的成本抵得上几千次仿真。于是训练一个能在复杂交通场景里自主导航和避障的自行车智能体就成了强化学习 仿真平台最典型的落地场景之一。Unity ML-Agents release 15 这个版本把训练环境和算法库的接口切得比较干净适合从零搭一套自己的仿真系统。本文就从环境搭建、观测设计、奖励塑造到训练排错把整条路走一遍。2. 搭建 ML-Agents release 15 训练环境从 Unity 项目到能“看路”的自行车智能体2.1 版本对齐是第一个玄学问题ML-Agents 最容易被忽略的点是 Unity 侧插件包和 Python 侧的 trainer 版本必须严格匹配。release 15 这个命名是 Unity Package Manager 里的版本号它对应的 Python 端大约在 0.29.x 到 0.30.0 之间。版本不匹配最常见的翻车现场是Unity 编辑器能正常 Play但mlagents-learn一启动就报communicator连接失败或者训练过程中step数完全不动。我一般会先建一个干净的 Python 虚拟环境再把 Python 包固定到和插件包一致的版本范围内python -m venv venv_mlagents source venv_mlagents/bin/activate pip install mlagents0.30.0装完后先跑一条最简单的命令验证通信链路。在 Unity 编辑器里打开一个只有地面和 Agent 的测试场景保持 Play 状态然后执行mlagents-learn --run-idsmoke_test。如果能看到[INFO] Connected to Unity environment和控制台里持续滚动 step 数说明通信正常。这一步花五分钟省掉后面排查环境问题的半天时间。Unity 项目侧要注意编辑器版本。release 15 年代的主流 Unity 版本是 2019.4 到 2021.3 这条线太新的 Unity 6 打包出来可能会遇到序列化格式差异建议直接用 LTS 版本。把com.unity.ml-agents插件包通过 Package Manager 以 tarball 方式安装后确认一下菜单栏出现了ML-Agents Settings选项如果没有多半是包没正确加载。2.2 自行车的物理模型与速度获取自行车在 Unity 里建模有两种思路一是严格还原两轮车动力学给前后轮分别添加 WheelCollider二是简化成可转向的刚体小车。训练导航和避障策略时我建议先用刚体简化模型——WheelCollider 本身调参就很痛苦而强化学习训练初期会频繁摔倒、翻车过于细腻的物理模型会导致环境 reset 逻辑变得很复杂。等策略在简化模型上收敛之后再替换成高保真模型做迁移验证。刚体自行车的核心部件就是Rigidbody 一个可视化的车身外加一个胶囊碰撞体包住车身。控制动作是两个连续量转向角和驱动力。这里有个热词频率很高的细节unity 物体速度怎么获取。新手最容易写错的是用transform.forward * speed去手动记录速度但这样完全绕过了物理引擎训练出来的策略在换到真实物理参数时直接失效。using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; public class BikeAgent : Agent { private Rigidbody rb; private float currentSteerAngle; private float maxSteerAngle 30f; public override void Initialize() { rb GetComponentRigidbody(); } public override void CollectObservations(VectorSensor sensor) { // 使用刚体的真实速度向量而不是自己累计的虚拟速度 Vector3 localVelocity transform.InverseTransformDirection(rb.velocity); // 归一化到[-1,1]避免绝对值过大导致训练不稳定 sensor.AddObservation(Mathf.Clamp(localVelocity.x / 10f, -1f, 1f)); sensor.AddObservation(Mathf.Clamp(localVelocity.z / 10f, -1f, 1f)); sensor.AddObservation(currentSteerAngle / maxSteerAngle); // 目标相对位置与距离 Vector3 toTarget target.position - transform.position; sensor.AddObservation(toTarget.normalized); sensor.AddObservation(Mathf.Clamp(toTarget.magnitude / 40f, 0f, 1f)); } }代码里我把localVelocity拆成 x 和 z 两个分量而不是只给智能体一个标量速度。原因在于自行车在转向时侧向速度分量能反映是否在侧滑或偏离预期方向这比单纯知道“我有多快”信息量大得多。距离除以 40 是把观测缩放到合理范围ML-Agents 训练时观测绝对值太大神经网络很容易饱和loss 会变得像噪声一样乱跳。物理参数上自行车的Rigidbody质量设 20 kg质心centerOfMass稍微靠下并后移一点约(0, -0.2, 0)否则转弯的时候车身会感觉像在冰面上打转。drag和angular drag我都设了 0.5 和 2这样智能体松油门之后会有自然减速的滑行感转弯时也不至于转出离心力夸张的急弯。做完这些先手动用 W A S D 操作跑一下确认手感接近真实自行车再开始训练。2.3 观测空间射线之外还要有“场”只靠速度目标的观测智能体不可能学会避障。视觉传感器有两种RaycastPerceptionSensor和CameraSensor。训练阶段我用射线传感器因为它能直接输出压缩后的距离信息训练收敛速度明显快于图像输入部署阶段再换成相机视觉做端到端推理也不迟。射线传感器在 ML-Agents 里是 Scalar 类型观测的一部分需要在 Agent 的 Inspector 面板里手动配置射线数量。我的配置是前方向扇区 5 条射线覆盖夹角 120 度每条射线的检测长度 20 米检测层只勾选Obstacle和Vehicle。每条射线会输出命中距离归一化值和是否命中 bool智能体相当于长了一排“触须”去感知障碍物。决策频率方面Decision Frequency设为 10。也就是每 10 次FixedUpdate才会让智能体输出一次动作。自行车智能体不需要像乒乓球那样高频反应10 Hz 的决策频率已经足够而且能显著减少训练时 CPU 的开销。如果你强行用 1每帧决策训练速度会肉眼可见地慢一大截。3. 奖励函数设计与 PPO 训练配置让自行车智能体学会“怕车”和“到站”3.1 把“会骑车”拆成可量化的奖励项奖励函数是整个训练里最像黑匣子、却又最值得花时间的地方。如果你只给“到达目标 1碰撞 -1”绝大多数情况下智能体学到的策略是原地不动——因为不动至少不会被扣分。这就是稀疏奖励的经典尴尬。我一般在OnActionReceived里叠加三个层次的奖励距离差分、碰撞惩罚、时间惩罚。public override void OnActionReceived(float[] vectorAction) { // 将连续动作映射到实际控制量 float steerCommand Mathf.Clamp(vectorAction[0], -1f, 1f); float throttleCommand Mathf.Clamp(vectorAction[1], -1f, 1f); currentSteerAngle steerCommand * maxSteerAngle; // 用局部坐标控制前轮转角用AddTorque驱动力用AddForce rb.AddForceAtPosition(transform.forward * throttleCommand * driveForce, transform.position - Vector3.up * 0.3f); // 距离差分奖励比上次更接近目标就给正分 float distanceNow Vector3.Distance(transform.position, target.position); float deltaDistance lastDistance - distanceNow; AddReward(deltaDistance * 0.5f); lastDistance distanceNow; // 时间惩罚防止原地磨蹭 AddReward(-0.001f); } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag(Obstacle) || collision.gameObject.CompareTag(Vehicle)) { AddReward(-0.5f); // 碰撞惩罚但不立即结束 } }碰撞给负奖励但不结束当前回合这是我试了很多次后保留的做法。原因是真实骑行中碰一下并不意味着死亡如果立刻EndEpisode智能体很容易学会“绕着所有障碍物走超大圈”这种保守策略目标明明是直线距离 20 米的点它却画一个半径 50 米的弧线绕过去。让它带着碰撞惩罚继续开反而会诱导它学会碰撞后修正方向、尽快脱离冲突区域。到达目标的奖励放在距离小于阈值时给大分并结束回合。阈值 1.5 米奖励 1。这里不要给太大否则和距离差分的尺度差距太大梯度会被单一事件主导。3.2 PPO 超参数别直接复制别人项目的配置ML-Agents 内置训练器是 PPOrelease 15 对应的经典默认配置可以用但自行车这种连续控制任务我把几个核心参数做了调整。参数默认值我的取值调整原因batch_size10242048自行车控制策略方差大大 batch 让梯度估计更稳定buffer_size1024040960更大经验池覆盖更多状态分布learning_rate3e-43e-4保持默认过高会导致奖励震荡beta1e-35e-4降低熵正则避免智能体一直随机试探epsilon0.20.2连续动作下 0.2 是稳妥值lambd0.950.99让价值估计看到更远的奖励num_epoch35提高每次数据的使用效率max_steps5e72e6单次训练控制在两百万步内不够再续训这些参数我习惯写到 yaml 配置里而不是每次命令行传。ML-Agents 的 yaml 配置格式里要注意缩进和 trainer 类型声明behaviors: BikeBehavior: trainer_type: ppo hyperparameters: batch_size: 2048 buffer_size: 40960 learning_rate: 3.0e-4 beta: 5.0e-4 epsilon: 0.2 lambd: 0.99 num_epoch: 5 network_settings: normalize: true hidden_units: 512 num_layers: 3 vis_encode_type: simple reward_signals: extrinsic: gamma: 0.99 strength: 1.0 max_steps: 2000000normalize: true是我特别建议打开的一项。它会在训练时对观测做 RunningNormalization相当于自动帮你在观测量纲不统一的情况下降燥。自行车智能体的观测里速度归一化到 10 m/s距离却可能大到 100显式归一化做了第一遍网络内部再跑一遍 RunningNorm训练稳定性会好很多。hidden_units: 512配三层全连接对连续动作策略来说够用且不会过拟合环境噪声。3.3 启动训练与监控配置写好后启动命令如下mlagents-learn config/ppo_bike.yaml --run-idbike_v1 --train跑起来之后在 Unity 编辑器里点 Play让训练场景保持运行。训练中最重要的是盯 TensorBoard 里的几个曲线Environment/Episode Reward、Policy/Loss和Policy/Entropy。tensorboard --logdir results --port 6006我个人的判断框架是前 50 万步时Episode Reward应该震荡上行Entropy缓慢下降Loss不要出现指数爆炸。如果Entropy快速跌到接近 0 但奖励还在低位说明策略提前收敛到了某个局部最优比如原地画圈这时候要回溯奖励函数如果Loss曲线像心电图一样疯狂跳动先检查观测是不是有NaN再降低learning_rate到 1e-4 重训。4. 复杂交通场景与自行车群体行为光是“能避障”远不够真实4.1 动态障碍物让车流自己跑起来静态障碍物训练出来的策略换一个障碍物位置就失灵。复杂交通场景的核心是动态性和随机性。我写了一个极简的交通车脚本挂在路面上持续生成的车身上。它不需要 ML-Agents 参与纯粹是环境动态实体。using UnityEngine; public class TrafficCar : MonoBehaviour { private Rigidbody rb; private float speed; void OnEnable() { speed Random.Range(4f, 8f); rb GetComponentRigidbody(); // 不统一用AddForce而是直接给初速度让车匀速移动更容易复现碰撞场景 rb.velocity transform.forward * speed; } void Update() { if (transform.position.z 120f || transform.position.z -30f) { gameObject.SetActive(false); // 回收避免场景无限膨胀 } } }这个脚本故意没有用固定路线每辆车在OnEnable时随机速度方向由车身的 prefab 旋转角决定。场景里同时在跑的车辆数控制在 2040 辆太多会影响训练速度太少则无法形成车流密度。生成器每 5 秒检查一次如果车辆数低于阈值就实例化新车。注意车辆之间的碰撞不要用 Unity 默认物理否则车流会互相撞停导致智能体面前堆一堵“车墙”。给车流加碰撞体但不加Rigidbody的模拟物或者把它们放在忽略相互碰撞的 layer 上都可以。4.2 自行车群体不是多几个 Agent 就叫群体行为标题里提到“自行车群体行”这个在仿真系统里是指同一个场景里同时出现多辆由 ML-Agents 训练的自行车智能体它们共享同一套行为策略但各自独立决策形成车群效果。实现方式绝不是复制粘贴几个 Agent 实例那么简单有几个结构性问题要注意。在 ML-Agents 里多个同类型 Agent 挂同一个 Behavior 名称训练时是共享网络权重的。也就是说智能体 A 学到的策略智能体 B 天然就会用。你要做的是让每个智能体有独立的target引用和独立的Rigidbody不要让它们抢同一个目标点。我在场景里用了一个GroupManager脚本在每回合开始时给每组智能体分配不同的起始点和终点using System.Collections.Generic; using UnityEngine; public class GroupManager : MonoBehaviour { public ListBikeAgent agents; public Transform[] startPoints; public Transform[] endPoints; public void ResetGroup() { for (int i 0; i agents.Count; i) { agents[i].SetRoute(startPoints[i % startPoints.Length].position, endPoints[i % endPoints.Length].position); } } }SetRoute内部会调用 Agent 里的OnEpisodeBegin逻辑。这里最容易忽略的一点是多智能体互相之间的碰撞。如果自行车之间也挂相同层并参与碰撞检测群体一多奖励曲线会被频繁的互相碰撞搞得很乱。我的处理方式是专门设置一个Rider层智能体之间碰撞时给一个很小的惩罚-0.1但车辆和行人层碰撞惩罚则维持 -0.5。这样训练出来的智能体既不会贴脸挤成一团又不会因为队友而变得过度保守。4.3 场景随机化与 curriculum进阶但值得做的一步场景随机化做得好不好直接决定策略能不能泛化。我所谓的随机化不只是障碍物位置随机还包括起点和终点每次都在一组差异较大的区域里挑选交通车流量密度每回合从低到高三档随机天气可选时灯光角度也随机。这样训练出来的策略才会对“新路线”不再感到陌生。如果发现直接上全随机复杂度太高训练一直不收敛就用 curriculum。ML-Agents release 15 里可以在 yaml 配置文件里声明课程难度。我用课程的方式是前 30 万步只有 10% 概率出现车辆且车速上限按 0.8 倍缩放手感30 万步之后逐步增大车流密度和车速。这个方式确实比一个固定高难度场景收敛速度快很多而且最终得到的策略在最高难度下表现反而更好。5. 自行车智能体训练的 5 个避坑记录现象、根因与处方5.1 奖励曲线纹丝不动动作像随机抖动现象训练了 30 万步TensorBoard 里Episode Reward始终在 -0.3 附近波动没有上升趋势。原因最常见的根源是观测根本没有进入网络。我遇到过把CollectObservations里sensor.AddObservation写错变量名导致观测全是 0也有过因为传感器配置里DetectionOnly勾了错误层射线一直打不到目标层智能体永远“看不到”障碍物。解决先暂停训练在 Unity 编辑器里选中智能体开启Monitor面板查看射线可视化。确认射线端点确实落在有碰撞体的物体上。再检查CollectObservations的总维度是否和 Inspector 里配置的Vector Observation数量一致不一致时训练器会直接报错但有时候会以静默方式忽略最终表现为奖励不动。5.2rb.velocity返回零或数值诡异现象智能体明明在移动但观测里的速度分量一直是 0训练完全失真。原因最常见是用了transform.Translate或直接改transform.position移动自行车。这样绕过了物理引擎Rigidbody.velocity一直为零而且碰撞检测也会变得极其不稳定。解决保持所有移动基于rb.AddForce、rb.AddTorque或直接赋值rb.velocity。检查Rigidbody的interpolation设为Interpolate并在FixedUpdate里读取而非Update。如果确认移动没问题还是 0检查你的代码是否在Initialize之前访问了rb——ML-Agents 的 Agent 初始化时机比 Unity 默认的Awake晚空引用会静默吞掉异常后面的观测采集自然全盘出错。5.3 智能体疯狂画圈或原地漂移现象训练过程中奖励连续下降观察智能体发现它总是往一个方向打满方向盘画圆圈直到回合结束。原因这是连续动作控制里典型的“动作饱和 奖励错位”。自行车智能体的转向动作steerCommand被直接映射到maxSteerAngle 30但实际物理反馈是力矩而非角度导致智能体输出一个 1 就永远转到底。另一个因素是距离差分奖励在转向转向角过大时失去参考意义因为距离变化率太小。解决先把动作映射改成非线性函数比如currentSteerAngle Mathf.Pow(steerCommand, 3) * maxSteerAngle让智能体在转向小角度时有更高分辨率。同时在观测里增加“当前转向角”这一项让它能看到自己的转向状态避免在饱和区盲打。最后把距离差分奖励从 0.5 降到 0.2减少奖励噪声对转向决策的干扰。5.4 把Time.timeScale调大加速训练结果穿模翻车现象为了加快训练速度把Time.timeScale调到了 10结果智能体频繁穿进车辆模型物理表现面目全非。原因Time.timeScale影响的是整个物理引擎的迭代频率时间步长变大的同时固定物理更新时间Fixed Timestep没有同步缩小刚体之间的穿透检测就失效了。解决Time.timeScale 5是安全上限同时把Physics.defaultMaxDepenetrationVelocity调大到 10再适当增加Physics.solverIterationCount。如果你仍然需要 10 倍速就手动在 Project Settings 里把Fixed Timestep从 0.02 改到 0.005。注意训练完成后做正式评估时一定把Time.timeScale恢复为 1否则评估数据不可信。5.5 训练到 150 万步后进入平台期现象奖励在某个水平稳定但离完成任务还差得远再怎么训也涨不上去。原因通常是奖励塑形不足以提供更进一步的梯度信号。比如只给了碰撞惩罚和到达奖励智能体学会了“慢速绕行”但绕行路线是随机的没有逼近最优路径。解决我一般会在这个阶段加一项“行驶方向对齐奖励”——如果自行车的行进方向和目标方向夹角小于 30 度每步给一个小的正奖励夹角大则不给。这个奖励项能有效打破平台期引导智能体从“躲避”转向“向目标前进”。float targetAngle Vector3.Angle(transform.forward, toTarget.normalized); if (targetAngle 30f rb.velocity.magnitude 0.5f) { AddReward(0.01f); // 很小的持续正向激励 }这属于 reward shaping 的“引导项”虽然单价很低但每步都在给积累效应足以把策略拉开。6. 验证与部署把训练好的策略从仿真搬进 Unity 运行时训练收敛后别急着感叹终于跑通了。先做一次正式的评估用mlagents-learn的评估模式让策略在不同随机种子下跑固定数量的回合统计平均到达率、平均碰撞次数、平均耗时三个指标。这三个指标必须同时看——到达率 90% 但平均碰撞 3 次说明策略是“冲过去”不是真避障到达率 60% 但零碰撞说明策略过于保守。评估通过后把训练出的.onnx文件导入 Unity。在 Agent 的 Behavior Parameters 里把 Model 从None切换为你的.onnx再把Inference Device选为 CPU 或 GPU。这一步的意义是让智能体不再依赖 Python 侧训练器完全在 Unity 内部实时推理这样你才能把它嵌入到演示场景、数字孪生项目或者后续的真实系统里。从仿真到真实环境最后要坦诚面对那条“玄学”鸿沟。仿真里学会靠右行驶的策略在真实两轮车上可能需要加一层 PID 做速度平滑仿真里能精确感知 20 米内障碍物真实传感器却有噪声。我的习惯是仿真里刻意给观测加微小高斯噪声——比如速度分量加 ±0.05 m/s 的抖动距离观测加 ±0.2 米抖动。这样训练出来的策略抗噪能力强很多真机迁移时翻车概率明显降低。另外两轮车真车的平衡控制是独立子系统不建议把仿真里学到的力矩直接下发常见做法是让仿真策略输出“目标转向角”再由真车控制器跟踪这个角。我踩过最大的一个坑是训练到最后才发现自己 30 万步里一直在用错误的观测维度相当于让一个盲人学了 30 万次开车。后来我把观测设计文档和训练参数写在项目 README 里每次重训前先花十分钟核对观测和奖励再启动训练器。希望你也能在一开始就把这条记录写进自己的工程习惯少走这段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑