
简介本资源为基于Unity ML-Agents release_15开发的自行车机器人智能躲避同伴项目工程面向人工智能、强化学习与游戏开发方向的学生及开发者可用于毕业设计、课程设计、大作业、工程实训及学科竞赛等场景。压缩包共564个文件约34.08MB包含C#脚本、Unity场景与预制体、ONNX与PT模型文件、TensorBoard训练日志、材质贴图及说明文档等覆盖从训练配置到推理部署的完整链路。目前已有43人学习关注。项目代码经过测试运行功能完整可复现读者可据此复刻出相同效果的智能体也可在现有工程基础上修改奖励函数、观测空间或场景布局扩展出新的避障与协同行为设计报告与训练日志亦可作为撰写论文与实验分析的参考素材。1. 自行车机器人躲避同伴这个毕设题目到底在做什么一辆只有两个轮子的自行车机器人在 Unity 场景里自己保持平衡、自己找路还要实时躲开另一个同样在移动的同伴机器人——这就是这个题目的核心。它属于强化学习 物理仿真的交叉方向用 Unity ML-Agents release_15 作为训练框架把自行车机器人的平衡控制、目标导航和动态避障塞进同一个决策网络里。适合做毕设、课设、实训或竞赛的同学也适合想入门具身智能Embodied AI但不想从零搭仿真环境的工程师。很多人第一反应是「自行车机器人平衡都难还要躲同伴这不是自找麻烦吗」。恰恰相反平衡和避障放在一起训练反而比分开做更容易收敛——因为躲避动作本身会带来姿态扰动模型被迫学会在动态中保持稳定泛化能力比单一任务强得多。这个方案能解决三个具体问题一是用 ML-Agents 的 PPO 算法训练连续控制策略二是用 Unity 物理引擎模拟真实的自行车动力学三是用课程学习Curriculum Learning让躲避难度逐步升级。读完你能自己搭出一个可训练、可复现、可调参的完整工程。2. 环境搭建与 ML-Agents release_15 的版本对齐2.1 为什么 release_15 不能随便换版本ML-Agents 的版本迭代非常快release_15 对应的是 Unity 2021 LTS 之后的稳定分支它的 Python 包mlagents和 Unity 包com.unity.ml-agents必须严格对应。我见过太多人用 release_20 的 Python 包去连 release_15 的 Unity 工程结果UnityEnvironment初始化直接抛The Unity environment took too long to respond查半天以为是端口问题其实是通信协议不匹配。常见做法是Unity 端通过 Package Manager 安装com.unity.ml-agents2.0.0release_15 对应的版本Python 端用pip install mlagents0.28.0。这两个版本号是绑定的不要看网上随便一个教程就照抄。如果你用的是 Unity Hub 安装的编辑器注意安装时勾选 Linux Build Support即使你在 Windows 上训练ML-Agents 的 headless 模式也需要它来构建可执行文件。注意release_15 的mlagents-learn命令默认使用 5004 端口如果被占用会静默失败训练日志里只显示Couldnt connect to trainer不会告诉你端口冲突。2.2 创建工程与导入 ML-Agents 包第一步用 Unity Hub 创建一个 3D 工程命名BikeBot_Dodge。打开后进入 Window Package Manager点击左上角加号选择 Add package from git URL输入# 这是 release_15 对应的 Unity 包地址不要改成 main 分支 https://github.com/Unity-Technologies/ml-agents.git?pathcom.unity.ml-agents#release_15等包导入完成后在 Project 窗口里应该能看到Packages/com.unity.ml-agents。接着创建 Python 虚拟环境这一步是为了隔离依赖避免和你系统里的其他 Python 包打架# 建议用 Python 3.8 或 3.9release_15 对 3.10 支持不完善 python -m venv mlagents_env # Windows 激活 mlagents_env\Scripts\activate # macOS/Linux 激活 source mlagents_env/bin/activate # 安装对应版本注意 torch 要指定 1.8.1 左右 pip install mlagents0.28.0 pip install torch1.8.1安装完成后用mlagents-learn --help验证如果能看到--run-id、--train等参数说明说明环境通了。这里有个血泪经验如果你之前装过其他版本的 mlagents一定要先pip uninstall mlagents mlagents-envs再重装否则残留的gym依赖会导致import mlagents时报cannot import name Space。2.3 自行车机器人的物理配置与碰撞体设置自行车机器人的核心是 Rigidbody 两个 WheelCollider。前轮负责转向后轮负责驱动车架用 CapsuleCollider 近似。在 Unity 里新建一个空物体命名BikeBot添加 RigidbodyMass 设为 10Drag 设为 0.05Angular Drag 设为 0.1。然后建两个子物体FrontWheel和RearWheel各挂 WheelCollider。关键参数在 WheelCollider 上前轮的Steer Angle通过脚本控制范围 -30 到 30 度后轮的Motor Torque控制驱动力范围 -200 到 200。轮胎的Forward Friction和Sideways Friction用默认的曲线就行但Extremum Slip要调到 0.4 左右否则自行车在低速时容易侧滑翻车。// BikeController.cs 核心片段 using UnityEngine; public class BikeController : MonoBehaviour { public WheelCollider frontWheel; public WheelCollider rearWheel; public Transform frontMesh; public Transform rearMesh; [Range(-30f, 30f)] public float steerInput; [Range(-200f, 200f)] public float driveInput; void FixedUpdate() { // 转向只作用在前轮 frontWheel.steerAngle steerInput; // 驱动力只作用在后轮 rearWheel.motorTorque driveInput; // 同步轮子模型的位置和旋转否则视觉上轮子不动 UpdateWheelMesh(frontWheel, frontMesh); UpdateWheelMesh(rearWheel, rearMesh); } void UpdateWheelMesh(WheelCollider col, Transform mesh) { Vector3 pos; Quaternion rot; col.GetWorldPose(out pos, out rot); mesh.position pos; mesh.rotation rot; } }这段代码的逻辑是FixedUpdate里每物理帧更新一次转向和驱动力steerInput和driveInput后续会被 ML-Agents 的 Agent 脚本覆盖。参数说明steerAngle超过 30 度会导致前轮与车架碰撞体穿插motorTorque超过 200 会让后轮空转打滑。UpdateWheelMesh是必须的否则 WheelCollider 在动但视觉模型不动调试时你会以为物理没生效。3. Agent 脚本与观测空间设计让机器人「看见」同伴3.1 观测向量里该放什么、不该放什么Agent 的观测空间决定了模型能学到什么。自行车机器人的观测分三块自身状态、目标信息、同伴信息。自身状态包括车架的世界坐标3 维、四元数旋转4 维、前轮转速1 维、后轮转速1 维、当前速度向量3 维共 12 维。目标信息是目标点的相对位置3 维。同伴信息是同伴机器人的相对位置3 维和相对速度3 维共 6 维。总计 21 维连续观测。不要放的东西绝对时间、帧率、场景里无关物体的坐标。我试过把场景里所有障碍物的坐标都塞进去结果观测维度飙到 80 多训练 50 万步还在原地打转。观测不是越多越好冗余信息会让 PPO 的价值网络难以收敛。// BikeAgent.cs 观测收集部分 public override void CollectObservations(VectorSensor sensor) { // 自身状态 12 维 sensor.AddObservation(transform.position); // 3 sensor.AddObservation(transform.rotation); // 4 sensor.AddObservation(frontWheel.rpm / 1000f); // 1归一化 sensor.AddObservation(rearWheel.rpm / 1000f); // 1 sensor.AddObservation(rb.velocity / 20f); // 3归一化 // 目标信息 3 维 Vector3 toTarget (target.position - transform.position) / 50f; sensor.AddObservation(toTarget); // 同伴信息 6 维 Vector3 toPeer (peer.position - transform.position) / 50f; Vector3 peerVel peerRb.velocity / 20f; sensor.AddObservation(toPeer); sensor.AddObservation(peerVel); }归一化是必须的。rpm除以 1000、速度除以 20、距离除以 50都是让输入落在 -1 到 1 附近。如果不归一化位置坐标可能是几百速度可能是几十神经网络的第一层权重会被大数值主导小数值的特征直接被淹没。这是新手最容易翻车的地方训练日志里Policy Loss一直不降八成是观测没归一化。3.2 动作空间连续控制还是离散控制自行车机器人的动作空间用连续型Continuous两个维度转向 [-1, 1] 映射到 [-30°, 30°]驱动力 [-1, 1] 映射到 [-200, 200]。不要用离散型因为离散动作会把转向切成几个固定角度自行车在避障时需要平滑的微调离散动作会导致「一顿一顿」的抖动物理引擎处理这种抖动时容易让车翻掉。在 Agent 脚本里这样写// 在 Initialize() 里设置 public override void Initialize() { // 连续动作2 个维度 // 第一个维度控制转向第二个控制驱动力 // 注意release_15 里连续动作默认范围是 [-1, 1] }然后在OnActionReceived里接收public override void OnActionReceived(ActionBuffers actions) { float steer actions.ContinuousActions[0]; float drive actions.ContinuousActions[1]; // 映射到物理参数范围 bikeController.steerInput steer * 30f; bikeController.driveInput drive * 200f; // 每步给微小惩罚鼓励尽快完成任务 AddReward(-0.001f); }参数说明steer * 30f里的 30 是最大转向角drive * 200f里的 200 是最大驱动力。这两个值要和 WheelCollider 的物理参数匹配改了一个另一个也要改。AddReward(-0.001f)是时间惩罚让模型学会「尽快到达目标」不加这个惩罚模型会学会在原地转圈拖时间。3.3 奖励函数设计躲避同伴的奖励怎么给奖励函数是这个题目的灵魂。我见过有人只给「到达目标 1撞到同伴 -1」结果模型学到的是「慢慢挪不撞就行」训练 100 万步还在起点附近晃。正确的做法是分层奖励奖励项触发条件数值说明到达目标距离目标 2m1.0回合结束撞到同伴与同伴碰撞-1.0回合结束靠近目标每步距离减少0.01连续奖励远离同伴距离同伴 3m 时-0.02/步惩罚危险接近保持平衡车架倾角 15°0.005/步鼓励稳定时间惩罚每步-0.001防止拖延public override void OnActionReceived(ActionBuffers actions) { // ... 动作映射代码 ... // 靠近目标的连续奖励 float distToTarget Vector3.Distance(transform.position, target.position); if (distToTarget lastDistToTarget) AddReward(0.01f); lastDistToTarget distToTarget; // 远离同伴的惩罚 float distToPeer Vector3.Distance(transform.position, peer.position); if (distToPeer 3f) AddReward(-0.02f); // 保持平衡的奖励 float tiltAngle Vector3.Angle(transform.up, Vector3.up); if (tiltAngle 15f) AddReward(0.005f); // 到达目标 if (distToTarget 2f) { AddReward(1.0f); EndEpisode(); } // 撞到同伴 if (distToPeer 0.8f) { AddReward(-1.0f); EndEpisode(); } }这段代码的关键在于「连续奖励」和「稀疏奖励」的配比。到达目标 1 是稀疏奖励靠近目标 0.01 是连续奖励后者让模型在早期就能获得梯度信号。distToPeer 3f的惩罚范围要调太小了模型学不会提前避让太大了模型会不敢靠近目标。我一般从 3m 开始试如果模型太怂就降到 2m如果总是撞就升到 4m。4. 课程学习与训练配置让躲避难度逐步升级4.1 课程学习的三个难度阶段课程学习Curriculum Learning是 ML-Agents 里最实用的功能之一。自行车躲避同伴这个任务如果一上来就让同伴高速乱窜模型根本学不会。我一般分三个阶段第一阶段同伴静止不动自行车只需要学会平衡和到达目标。这个阶段大概 20 万步就能稳定。 第二阶段同伴沿固定路径低速移动速度 2 m/s自行车需要绕开。这个阶段 30 万步左右。 第三阶段同伴随机游走速度 5 m/s并且会主动朝自行车方向移动。这个阶段 50 万步以上。在 Unity 里通过Academy或Agent的OnEpisodeBegin来切换难度但更规范的做法是用 ML-Agents 的Curriculum配置文件。在config/目录下新建bike_curriculum.yamlBigWall: measure: progress thresholds: [0.3, 0.6, 0.9] min_lesson_length: 100 parameters: peer_speed: [0.0, 2.0, 5.0] peer_random: [0.0, 0.3, 1.0]然后在 Agent 脚本里读取这些参数// 在 Initialize() 里 peerSpeed curriculumConfig[peer_speed]; peerRandom curriculumConfig[peer_random];参数说明measure: progress表示用「到达目标的成功率」作为难度提升的指标thresholds是三个阶段的阈值min_lesson_length是每个阶段最少跑多少个回合才允许切换。peer_speed和peer_random是自定义参数分别控制同伴速度和随机程度。4.2 PPO 超参数怎么调学习率、批次大小、网络结构ML-Agents 的 PPO 默认参数对自行车任务来说偏保守我一般会改这几个# config/bike_trainer.yaml behaviors: BikeAgent: trainer_type: ppo hyperparameters: batch_size: 2048 # 默认 1024自行车任务观测维度高加大批次 buffer_size: 20480 # 默认 10240经验池加大 learning_rate: 3.0e-4 # 默认 3.0e-4保持 beta: 5.0e-3 # 熵系数默认 5.0e-3鼓励探索 epsilon: 0.2 # PPO 裁剪系数默认 0.2 lambd: 0.95 # GAE 系数默认 0.95 num_epoch: 3 # 默认 3不要超过 5否则过拟合 network_settings: normalize: true # 必须开观测归一化 hidden_units: 256 # 默认 128自行车任务用 256 num_layers: 2 # 默认 2 max_steps: 2.0e6 # 总训练步数 200 万 time_horizon: 64 # 默认 64 summary_freq: 10000 # 每 1 万步输出一次日志batch_size和buffer_size的关系是buffer_size必须是batch_size的整数倍且buffer_size要大于time_horizon。我见过有人把batch_size设成 4096 但buffer_size还是 10240结果训练直接报Buffer size must be larger than batch size。hidden_units从 128 提到 256 是因为 21 维观测 2 维动作的映射关系比简单任务复杂128 个神经元不够用训练曲线会卡在某个奖励值上不去。4.3 启动训练与 TensorBoard 监控配置写好后在命令行启动训练# 先激活虚拟环境 mlagents_env\Scripts\activate # 启动训练--run-id 是这次训练的名字 mlagents-learn config/bike_trainer.yaml --run-idbike_dodge_v1 --time-scale20--time-scale20是把 Unity 仿真速度调到 20 倍训练时不用实时看画面。启动后按 Play 按钮Unity 会开始跑仿真命令行会输出类似[INFO] BikeAgent. Step: 10000. Mean Reward: 0.23. Std of Reward: 0.45. [INFO] BikeAgent. Step: 20000. Mean Reward: 0.31. Std of Reward: 0.52.同时开一个终端跑 TensorBoardtensorboard --logdir results在浏览器打开localhost:6006重点看三个曲线Environment/Cumulative Reward应该稳步上升Policy/Learning Rate应该平稳Policy/Entropy应该缓慢下降。如果Cumulative Reward在 0 附近震荡超过 10 万步检查奖励函数是不是太稀疏如果Entropy掉到 0.1 以下说明模型不探索了把beta调大。5. 避坑与排查训练不收敛、机器人翻车、同伴穿模5.1 训练 50 万步奖励不涨观测里混入了未归一化的值现象TensorBoard 里Cumulative Reward一直在 -0.5 到 0.5 之间震荡Policy Loss不降。原因观测向量里某个维度的数值范围是几百甚至几千神经网络第一层的权重被这个大数值主导其他小数值特征被淹没。最常见的是transform.position没除以场景尺度或者rb.velocity没除以最大速度。解决在CollectObservations里逐项检查每个AddObservation的值都应该在 -5 到 5 之间。位置除以场景对角线长度速度除以最大设计速度角度用四元数本身就是 -1 到 1。如果懒得逐项算在 Agent 的Initialize里开normalize: trueML-Agents 会自动做 running mean/std 归一化但手动归一化效果更稳定。5.2 自行车在训练初期频繁翻车WheelCollider 参数太激进现象回合刚开始 1 秒内自行车就侧翻EndEpisode被频繁调用模型学不到任何有效策略。原因WheelCollider 的Sideways Friction曲线默认Extremum Slip是 0.2对自行车这种窄轮胎来说太小稍微一转向就侧滑。另外Rigidbody的Center of Mass默认在几何中心自行车重心应该偏低。解决把Sideways Friction的Extremum Slip调到 0.4Extremum Value调到 1.0。在Start()里手动设置重心void Start() { rb GetComponentRigidbody(); // 重心降低到车架底部上方 0.2m rb.centerOfMass new Vector3(0, -0.2f, 0); }重心降低后自行车在 15 度以内的倾斜都能自动回正训练初期的翻车率会大幅下降。5.3 同伴机器人穿模碰撞检测被物理引擎跳过现象自行车和同伴明明重叠了但碰撞事件没触发OnCollisionEnter不执行奖励也没扣。原因Unity 的碰撞检测分 Discrete 和 Continuous 两种。Discrete 模式下如果两个物体速度很快物理引擎在两帧之间会「跳过」碰撞。同伴速度 5 m/s 时每帧移动 0.1m按 50Hz 物理帧率算如果碰撞体厚度小于 0.1m就会穿模。解决把自行车和同伴的Rigidbody的Collision Detection都设为Continuous Dynamic并且把Interpolate设为Interpolate。另外碰撞体不要用 MeshCollider用 CapsuleCollider 或 BoxColliderMeshCollider 在 Continuous 模式下性能开销大且容易出 bug。5.4 课程学习卡在第二阶段阈值设置不合理现象peer_speed从 0 升到 2.0 后奖励突然从 0.8 掉到 0.2然后一直不涨课程也不切到第三阶段。原因thresholds设得太高第二阶段要求 60% 成功率才切第三阶段但同伴速度 2 m/s 时模型成功率只有 40%永远达不到阈值。另外min_lesson_length设得太小模型还没适应新难度就被判定为「失败」。解决把thresholds改成[0.2, 0.5, 0.8]min_lesson_length改成 200。如果还是卡住在第二阶段多跑 20 万步或者把peer_speed的中间值从 2.0 降到 1.5让难度曲线更平滑。5.5 TensorBoard 曲线正常但实际测试时机器人原地转圈现象训练时Cumulative Reward涨到 0.9但用mlagents-learn --resume加载模型测试时自行车在原地转圈不前进。原因训练时用了--time-scale20物理帧率被拉高模型学到的策略依赖高帧率下的微小时间差。测试时time-scale1物理帧率恢复正常策略失效。另外Time.fixedDeltaTime在训练和测试时不一致也会导致这个问题。解决训练时--time-scale不要超过 10并且把Time.fixedDeltaTime固定为 0.0250Hz。在 Agent 的Initialize里加一行Time.fixedDeltaTime 0.02f;确保训练和测试的物理步长一致。如果已经训练完了用--inference模式测试不要用--resume。6. 模型导出与 ONNX 推理把训练好的策略塞进 Unity训练完成后results/bike_dodge_v1/BikeAgent.onnx就是导出的模型文件。ML-Agents 会自动把.nn文件转成.onnx但 release_15 的转换有时候会失败报ONNX export failed。我一般手动转# 先找到 .nn 文件 ls results/bike_dodge_v1/BikeAgent/BikeAgent.nn # 用 mlagents 自带的转换工具 mlagents-convert results/bike_dodge_v1/BikeAgent/BikeAgent.nn转换成功后在 Unity 里把BikeAgent.onnx拖到 Agent 脚本的Model字段上把Behavior Type设为Inference Only。这时候按 Play自行车就会用训练好的策略自己跑不再需要 Python 端。但这里有个坑ONNX 推理的数值精度和 Python 端不完全一致有时候模型在 Python 里表现很好导出后自行车会轻微抖动。解决方法是把Decision Period从默认的 5 改成 3让模型每 3 个物理帧决策一次给物理引擎更多时间平滑。另外Inference Device选CPU还是GPU也有讲究CPU 推理延迟低但吞吐小GPU 吞吐大但延迟高。自行车任务只有一个 Agent用 CPU 就够了。// 在 Agent 脚本的 Inspector 里设置 // Behavior Parameters: // Behavior Name: BikeAgent // Vector Observation Space Size: 21 // Continuous Actions: 2 // Model: BikeAgent.onnx // Inference Device: CPU // Decision Period: 3最后说一个我自己的习惯每次改完奖励函数或观测空间先跑 5 万步看Cumulative Reward有没有上升趋势没有就立刻停不要硬跑 200 万步。我最多的一次跑了 300 万步才发现观测里有个维度忘了归一化白白浪费了两天。训练日志里的Mean Reward比 TensorBoard 曲线更敏感命令行输出连续 10 次不涨就该回头查配置了。希望帮到你。本文还有配套的精品资源点击获取