简介:这份资源面向计算机、自动化与游戏开发方向的学生及开发者,提供一套基于Unity ML-Agents release_15开发的自行车机器人智能躲避同伴的完整工程,可用于毕业设计、课程设计、大作业、工程实训及学科竞赛等场景,帮助读者快速搭建强化学习训练环境并复现智能体行为。压缩包共564个文件,约34.08MB,包含C#脚本、Unity场景与预制体、材质与地形资源、ONNX与PT模型文件、TensorBoard训练日志、HTML文档及工程配置等,覆盖从训练到推理的完整链路。目前已有43人学习关注。资源内代码经过测试运行,功能正常,可复现复刻,说明文件与设计报告可供借鉴,也支持在此基础上扩展新功能;训练日志与模型文件便于分析智能体学习过程,Unity工程结构清晰,适合作为强化学习入门与项目立项的参考模板。
1. 自行车机器人躲同伴:为什么值得用 Unity ML-Agents release_15 跑一遍
一辆自行车机器人在场景里绕圈,前方突然出现另一个同类,它得在零点几秒内决定往左偏还是往右偏,还不能撞上去。这个场景听起来像游戏 AI,但它背后是一套完整的强化学习训练链路:环境搭建、观测设计、奖励塑形、并行采样、策略导出。用 Unity ML-Agents release_15 做这件事,最大的好处是你不需要从零写物理仿真器,Unity 的刚体、碰撞、关节系统直接可用,训练完的策略还能直接挂回场景里的 Agent 脚本上跑推理。
这个方向适合三类人:做毕设或课设、需要一套能演示、能写论文、能答辩的完整项目;做机器人仿真预研、想快速验证避障策略可行性的工程师;以及想入门强化学习但不想一上来就啃 MuJoCo 和 Isaac Gym 的人。自行车机器人的动力学比轮式底盘复杂,但比足式机器人简单,是一个很好的中间难度靶子。躲同伴这个任务又天然带多智能体属性,能顺带把 Self-Play 和课程学习一起练了。
2. 环境搭建与 release_15 的版本选择:从 Unity Hub 到 mlagents Python 包
2.1 为什么锁定 release_15 而不是随手装最新版
ML-Agents 的版本兼容性是这条链路里最容易翻车的地方。release_15 对应的是 Unity 2021.3 LTS 到 Unity 2022.3 LTS 之间的编辑器版本,Python 包版本号是 0.28 到 0.30 这个区间。如果你用 Unity 6 打开旧工程,Agent 脚本里的Academy引用会直接报错,因为 Academy 在 release_15 之后被拆成了Agent和TrainingAnalytics两套东西。我一般会先确认三件事:Unity 编辑器版本、com.unity.ml-agents包版本、Python 端mlagents版本,三者必须对齐。
常见做法是在 Unity Hub 里装一个 2022.3 LTS 的编辑器专门跑这个项目,不要和日常开发用的 Unity 6 混在一起。Unity 6 虽然也能装 ML-Agents 包,但 release_15 的示例场景和 Prefab 结构在新版本里会有序列化丢失,尤其是Behavior Parameters组件里的Vector Observation字段,升级后经常变成空数组。
2.2 用 Unity Hub 建工程并导入 ML-Agents 包
第一步是建一个 3D 工程,然后通过 Package Manager 从 Git URL 或本地 tarball 导入 ML-Agents。release_15 的包在 Unity 的 Package Manager 里搜ML Agents能直接找到,但默认给的是最新版,需要手动展开See other versions选 2.0.0 之前的版本。更稳的做法是直接改manifest.json。
{ "dependencies": { "com.unity.ml-agents": "2.0.1", "com.unity.ml-agents.extensions": "0.1.0-preview", "com.unity.barracuda": "2.3.0" } }这段manifest.json片段里,com.unity.ml-agents是核心包,提供 Agent、Behavior Parameters、Decision Requester 这些组件;com.unity.ml-agents.extensions提供一些传感器和辅助工具,比如Match3Sensor和GridSensor,做自行车机器人的视觉观测时会用到;com.unity.barracuda是推理引擎,训练完的.onnx模型靠它跑。版本号不要随意往上跳,2.0.1 是 release_15 生态里比较稳的一个点。
导入完成后,在Project Settings里确认Scripting Define Symbols没有多余的宏定义。有些教程会让你加ML_AGENTS_DEBUG,这个宏在 release_15 里会打开大量日志,训练时拖慢采样速度,非调试阶段建议去掉。
2.3 Python 端环境隔离与 mlagents 安装
Python 端我强烈建议用 conda 建独立环境,不要和系统 Python 混。release_15 对应的mlagents版本是 0.28.0,它依赖的torch版本是 1.8 到 1.11 之间,numpy不能超过 1.23。如果你直接pip install mlagents,装到的是最新版,和 Unity 端的 release_15 通信协议对不上,表现是训练启动后 Unity 端一直显示Waiting for trainer,Python 端报UnityEnvironment version mismatch。
conda create -n mlagents15 python=3.8.10 conda activate mlagents15 pip install mlagents==0.28.0 pip install torch==1.11.0 --index-url https://download.pytorch.org/whl/cu113第一行建环境,Python 3.8.10 是 release_15 官方测试过的版本;第二行激活;第三行装 mlagents 0.28.0,它会自动拉取匹配的mlagents-envs和gym依赖;第四行单独装 torch,指定 CUDA 11.3 的 wheel,如果你没有 NVIDIA 显卡,把cu113换成cpu。装完后用mlagents-learn --help验证,能打印出参数列表就说明环境通了。
注意:不要用 Python 3.10 及以上,
mlagents 0.28.0里的collections.Mapping引用在 3.10 被移除了,会直接 ImportError。
3. 自行车机器人的物理建模与观测设计:从刚体到 Ray Perception
3.1 用 Rigidbody + Configurable Joint 搭一辆能平衡的两轮车
自行车机器人的核心难点是它静态不稳定,必须靠运动维持平衡。在 Unity 里搭这辆车,我一般用三层结构:一个Rigidbody作为车身主体,两个SphereCollider作为前后轮,轮子通过Configurable Joint和车身连接,限制除了绕轴旋转以外的自由度。车身重心要压低,Rigidbody的centerOfMass设到轮轴连线下方 0.1 到 0.2 米,这样初始状态不会立刻倒。
public class BikeBody : MonoBehaviour { public Rigidbody bodyRb; public Rigidbody frontWheelRb; public Rigidbody rearWheelRb; public float motorTorque = 50f; public float steerTorque = 10f; void FixedUpdate() { // 后轮驱动,模拟踩踏 rearWheelRb.AddTorque(transform.forward * motorTorque * Input.GetAxis("Vertical")); // 前轮转向,模拟车把 frontWheelRb.AddTorque(transform.up * steerTorque * Input.GetAxis("Horizontal")); // 车身施加一个微小的扶正力矩,防止完全倒下后无法恢复 Vector3 balanceTorque = Vector3.Cross(transform.up, Vector3.up) * 20f; bodyRb.AddTorque(balanceTorque); } }这段代码里,motorTorque控制后轮驱动力,steerTorque控制前轮转向力矩,balanceTorque是一个辅助扶正力矩,让车在接近倒下时有一个恢复趋势。参数不是拍脑袋定的:motorTorque50 对应车身质量 10kg 时能产生约 5m/s² 的加速度,steerTorque10 对应转向响应时间约 0.3 秒,这两个值需要根据你的车身质量等比调整。balanceTorque的 20 是经验值,太大会让车像陀螺一样自稳,太小则训练初期频繁倒地,采样效率极低。
3.2 观测空间设计:哪些信息喂给策略网络
观测设计直接决定训练能不能收敛。自行车机器人躲同伴,我一般给三类观测:自身状态、同伴状态、环境边界。自身状态包括车身倾角、速度、角速度、前轮转角;同伴状态包括相对位置、相对速度;环境边界用几个射线检测。全部拼成一个Vector Observation,维度控制在 30 到 50 之间。
public override void CollectObservations(VectorSensor sensor) { // 自身状态:8 维 sensor.AddObservation(transform.localRotation.eulerAngles / 180f); sensor.AddObservation(bodyRb.velocity / 10f); sensor.AddObservation(bodyRb.angularVelocity / 10f); sensor.AddObservation(frontWheelRb.transform.localEulerAngles.y / 180f); // 同伴状态:6 维,取最近的两个同伴 foreach (var mate in nearbyMates) { Vector3 relativePos = transform.InverseTransformPoint(mate.transform.position); sensor.AddObservation(relativePos / 10f); sensor.AddObservation(mate.GetComponent<Rigidbody>().velocity / 10f); } // 环境射线:12 维,前后左右各 3 条 foreach (var dir in rayDirections) { sensor.AddObservation(Physics.Raycast(transform.position, dir, out RaycastHit hit, 5f) ? hit.distance / 5f : 1f); } }AddObservation里的除法是归一化,把物理量压到 -1 到 1 之间,这对神经网络训练至关重要。不做归一化的话,速度值可能到几十,而角度值只有零点几,网络会把大部分注意力放在速度上,角度变化被淹没。同伴只取最近两个,是因为观测维度不能无限膨胀,超过 50 维后训练速度明显下降,而且远处同伴对当前决策影响很小。射线检测的 5 米范围是根据自行车典型速度 5m/s 和决策频率 10Hz 估算的,一个决策周期内移动 0.5 米,5 米足够提前发现障碍。
3.3 奖励函数:稀疏奖励为什么会让自行车机器人摆烂
奖励设计是强化学习里最玄学的部分。躲同伴这个任务,如果只给稀疏奖励——撞了给 -1,没撞给 0——训练初期几乎学不到东西,因为随机策略撞车概率很高,负奖励密集,智能体会倾向于原地不动来避免撞车,这就是典型的摆烂。我一般用分层奖励:生存奖励 + 接近惩罚 + 朝向奖励。
public override void OnActionReceived(ActionBuffers actions) { // 生存奖励:每步 +0.001,鼓励存活 AddReward(0.001f); // 接近惩罚:与同伴距离小于 2 米时线性扣分 foreach (var mate in nearbyMates) { float dist = Vector3.Distance(transform.position, mate.transform.position); if (dist < 2f) { AddReward(-0.01f * (2f - dist)); } } // 朝向奖励:车头方向与目标方向夹角越小,奖励越高 float alignment = Vector3.Dot(transform.forward, targetDirection.normalized); AddReward(0.002f * alignment); // 撞车终止 if (hasCollided) { AddReward(-1f); EndEpisode(); } }生存奖励每步 0.001,一个 episode 按 1000 步算,总共 1 分,撞车扣 1 分,这样智能体有动力活满全程。接近惩罚用线性函数而不是阶跃,是为了给智能体一个渐变信号,距离越近扣得越多,它才能学会提前避让。朝向奖励让车保持前进方向,防止它学会原地转圈来躲避。这三个奖励的权重需要调:生存奖励太大,智能体会绕圈跑;接近惩罚太大,智能体会不敢靠近任何东西,包括目标点。
提示:奖励权重不要一次性定死,先用 TensorBoard 看
Environment/Cumulative Reward曲线,如果曲线在 0 附近震荡不上升,说明正负奖励抵消了,需要调整比例。
4. 训练配置与多智能体 Self-Play:让自行车学会躲另一个自行车
4.1 用 YAML 配置文件控制 PPO 超参数
ML-Agents 的训练行为由 YAML 配置文件驱动。release_15 的默认配置在config/ppo/目录下,但默认参数是给简单任务调的,自行车机器人需要改几个关键项。我一般复制一份BikeAgent.yaml,重点调batch_size、buffer_size、learning_rate和time_horizon。
behaviors: BikeAgent: trainer_type: ppo hyperparameters: batch_size: 2048 buffer_size: 20480 learning_rate: 3.0e-4 beta: 5.0e-3 epsilon: 0.2 lambd: 0.95 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: true hidden_units: 256 num_layers: 2 vis_encode_type: simple reward_signals: extrinsic: gamma: 0.99 strength: 1.0 max_steps: 5.0e6 time_horizon: 128 summary_freq: 20000 keep_checkpoints: 5 checkpoint_interval: 500000batch_size2048 和buffer_size20480 的比例是 1:10,这是 PPO 的常见经验比例,buffer 太小会导致梯度估计方差大,太大则更新慢。learning_rate3e-4 是 Adam 优化器的常用起点,配合linear衰减,训练后期自动降低学习率,避免在最优解附近震荡。time_horizon128 表示每 128 步做一次 GAE 计算,自行车机器人的决策周期是 0.1 秒,128 步对应 12.8 秒,足够覆盖一次完整的避让动作。hidden_units256 和num_layers2 是中等复杂度网络,观测维度 50 左右时够用,再大容易过拟合。
4.2 多智能体 Self-Play 的 Group 配置
躲同伴这个任务,如果同伴是固定策略,智能体很快就能找到漏洞,比如一直往一个方向绕。用 Self-Play 让两个智能体互相训练,策略会越来越鲁棒。release_15 的 Self-Play 通过self_play字段配置,需要定义play_against_latest_model_ratio和save_steps。
behaviors: BikeAgent: trainer_type: ppo self_play: save_steps: 100000 team_change: 200000 swap_steps: 50000 window: 10 play_against_latest_model_ratio: 0.5 initial_elo: 1200.0save_steps100000 表示每 10 万步存一个历史模型快照,team_change200000 表示每 20 万步交换一次训练队伍,swap_steps50000 表示每 5 万步交换一次对手模型。play_against_latest_model_ratio0.5 表示 50% 的概率对战最新模型,50% 对战历史模型,这个比例防止策略退化。window10 表示 Elo 评分窗口大小,initial_elo1200 是初始分。Self-Play 的训练时间比单智能体长 2 到 3 倍,但最终策略的鲁棒性明显更好。
4.3 启动训练与 TensorBoard 监控
训练启动命令很简单,但路径和参数要对。在 Python 端激活环境后,进入包含 YAML 配置的目录,执行mlagents-learn。
mlagents-learn config/BikeAgent.yaml --run-id=bike_dodge_01 --time-scale=20 --num-envs=4--run-id是这次训练的唯一标识,结果存在results/bike_dodge_01/下;--time-scale=20把 Unity 仿真速度提到 20 倍,采样效率直接翻 20 倍,但不要超过 20,否则物理模拟会失真;--num-envs=4启动 4 个并行环境,需要你在 Unity 场景里复制 4 份 Agent,或者用UnityEnvironment的num_envs参数。启动后 Unity 编辑器里点 Play,Python 端会打印Listening on port 5004,然后开始训练。
TensorBoard 用tensorboard --logdir results启动,重点看三条曲线:Environment/Cumulative Reward应该稳步上升,Policy/Learning Rate应该线性下降,Policy/Entropy应该缓慢下降。如果 Cumulative Reward 上升后突然暴跌,通常是 Self-Play 的对手策略突变导致的,可以调大window或降低team_change频率。
5. 避坑与排查:训练不收敛、模型导出失败、推理延迟高
5.1 训练启动后 Unity 端一直 Waiting for trainer
现象:Python 端显示Listening on port 5004,Unity 端 Console 打印Waiting for trainer to connect,但一直不开始。
原因:端口被占用,或者 Unity 端Behavior Parameters里的Behavior Name和 YAML 里的behaviors键名不一致。release_15 对大小写敏感,BikeAgent和bikeagent会被当成两个不同的行为。
解决:先用netstat -ano | findstr 5004查端口占用,杀掉占用进程或换端口--port=5005。然后检查 Unity 场景里每个 Agent 的Behavior Parameters组件,Behavior Name必须和 YAML 里的键名完全一致,包括大小写。
5.2 训练几万步后 Cumulative Reward 卡在某个值不涨
现象:TensorBoard 里Environment/Cumulative Reward在前 5 万步上升,之后一直平在 0.3 左右,偶尔波动但不突破。
原因:奖励函数设计有局部最优,智能体学会了某种“安全但低效”的策略,比如一直绕大圈躲避,虽然不撞车但也不前进。或者观测空间里缺少关键信息,比如没有把同伴的相对速度喂进去,智能体只能看到位置,无法预判。
解决:先检查观测维度是否覆盖了位置和速度,如果只有位置,加上相对速度。然后在奖励函数里加一个前进距离奖励,每步AddReward(0.0005f * forwardSpeed),鼓励智能体在安全的前提下保持前进。如果还是卡住,把time_horizon从 128 降到 64,让 GAE 更关注短期回报,打破局部最优。
5.3 导出的 ONNX 模型在 Unity 里推理结果和训练时不一致
现象:训练时智能体表现很好,导出.onnx后用Behavior Type: Inference Only跑,智能体动作僵硬,甚至原地转圈。
原因:导出时没有勾选Behavior Parameters里的Model字段,或者导出后没有把Behavior Type从Default改成Inference Only。另一个常见原因是训练时用了normalize: true,但推理时没有把归一化参数一起导出,导致输入分布不匹配。
解决:在 Unity 编辑器里选中 Agent,把训练生成的.onnx文件拖到Behavior Parameters的Model槽位,然后把Behavior Type设为Inference Only。如果还是不对,检查normalize设置,release_15 的 ONNX 导出会自动包含归一化层,但如果你手动改过Vector Observation的顺序,归一化参数会对不上,需要重新训练导出。
5.4 并行环境数量开太多导致物理模拟失真
现象:--num-envs=8时训练速度确实快,但智能体学到的策略在单环境推理时表现很差,经常撞车。
原因:Unity 的物理引擎在多个环境并行时,如果Time.fixedDeltaTime没有同步调整,每个环境的物理步进会互相干扰。release_15 的并行环境默认共享同一个物理场景,刚体数量翻倍后碰撞检测精度下降。
解决:把--num-envs控制在 4 以内,或者在每个环境里用独立的PhysicsScene。更简单的做法是调小Time.fixedDeltaTime,从默认的 0.02 降到 0.01,物理精度翻倍,但采样速度会减半,需要权衡。我一般用 4 个环境加 0.01 的 fixedDeltaTime,训练 500 万步大约需要 6 到 8 小时,具体看显卡。
6. 从训练到落地:策略导出、课程学习与一个我常用的验证技巧
训练完的模型导出成.onnx后,文件大小通常在 1 到 3 MB 之间,取决于hidden_units和num_layers。导出路径在results/bike_dodge_01/BikeAgent.onnx,把它拖进 Unity 的Assets目录,然后挂到 Agent 的Behavior Parameters上。这里有一个我踩过的坑:如果你在训练时用了self_play,导出的模型是对战历史模型的策略,直接拿去和固定策略的同伴对战,表现会偏保守,因为它习惯了对手也在进化。解决办法是在 Self-Play 训练后期,把play_against_latest_model_ratio调到 0.9,让智能体更多面对最新对手,导出前的策略会更激进。
课程学习是另一个提升落地效果的手段。自行车机器人躲同伴,可以从简单场景开始:一个同伴、直线路径、低速。然后逐步增加同伴数量、加入弯道、提高速度。release_15 的课程学习通过Curriculum文件夹下的 JSON 配置,每个课程阶段对应不同的Environment Parameters。
{ "BikeAgent": [ { "name": "Lesson1_Easy", "completion_criteria": { "behavior": "BikeAgent", "signal_smoothing": true, "min_lesson_length": 100, "threshold": 0.5 }, "value": 1 }, { "name": "Lesson2_Medium", "completion_criteria": { "behavior": "BikeAgent", "signal_smoothing": true, "min_lesson_length": 100, "threshold": 0.7 }, "value": 2 } ] }value字段对应你在 Agent 脚本里读的EnvironmentParameters,比如value=1时同伴速度 2m/s,value=2时 4m/s。threshold是完成当前课程的平均奖励阈值,min_lesson_length是至少跑多少个 episode 才判断。课程学习能让训练初期收敛快很多,但配置不当会导致智能体卡在简单课程里,因为threshold设太低,它觉得简单场景已经够好了。我一般把threshold设在当前课程理论最大奖励的 70% 左右。
验证策略好坏,我常用一个笨办法:把训练好的模型挂到 Agent 上,手动控制一个同伴去追它,看它能撑多久不撞。这个测试比看 TensorBoard 曲线直观得多,因为曲线是平均表现,手动追能暴露策略的边界情况,比如急转弯时会不会失控、被逼到墙角时会不会卡死。如果手动追 30 秒内撞了三次以上,说明策略还不够鲁棒,需要回去调奖励函数或者加更多样的训练场景。
最后说一个习惯:每次训练前,我会把 YAML 配置和 Unity 场景的Behavior Parameters截图存到一个experiments文件夹里,命名带上日期和 run-id。强化学习的训练结果对超参数太敏感,同一个配置换个随机种子可能结果差很多,没有记录的话,两周后你根本想不起来当时改了什么。这个习惯帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取