这阵子折腾强化学习,又把ML-Agents 1.9.1这套东西翻了出来。官方文档里“Creating a New Environment”这一章,看着篇幅不长,真上手才发现里面有大量“文档没写但你一定会遇到”的细节。我自己在Unity 2018和2020两个版本下都跑过一套完整流程,这篇就把从头创建一个新训练环境这件事掰开揉碎讲清楚:从Unity端搭场景、挂组件、写Agent脚本,到训练端配YAML、启动mlagents-learn、看TensorBoard曲线,再到训练失败之后怎么排查。不管你是刚接触ML-Agents的初学者,还是已经跑通官方示例、想换成自己任务场景的老手,这篇应该都能帮你省下不少试错时间。
1. 训练环境到底是个什么东西
1.1 环境、智能体、行为参数三件套
ML-Agents里的“训练环境”和普通Unity游戏场景最大的区别在于,它不是一个只给玩家玩的关卡,而是一个能被强化学习算法反复交互的模拟器。官方文档把环境拆成几个核心部分:物理场景、智能体(Agent)、行为参数(Behavior Parameters)以及(可选的)决策请求器(Decision Requester)。
可以这样理解:物理场景就是“考试场地”,智能体是被考核的学生,行为参数规定了这场考试考什么科目、能交什么样的答案,而决策请求器决定了学生多久被提问一次。训练过程中,Agent通过传感器观察场景状态(CollectObservations),决策器输出动作(OnActionReceived),环境根据动作改变状态并给出奖励(AddReward),当达到终止条件后整段episode结束(EndEpisode),然后重新开始下一轮。这套“观察-决策-奖励-重置”的循环,就是整个ML-Agents训练环境的核心运转方式。
1.2 这套文档对应什么版本,能解决什么问题
这里必须先说清楚版本,不然后面全是坑。这篇文章对应的是ML-Agents 1.9.1,配合Unity 2018到2020的版本使用。ML-Agents从1.0开始API变化很大,很多网上老教程用的还是0.x的写法,比如CollectObservations还是接收List 这种早就不推荐了。如果你用的是1.9.1,网上搜到的大部分旧教程会把你带偏,所以认准版本来读。
这一章文档解决的实际问题是:你不想再跑官方自带的示例环境和预训练模型,而是想把自己项目里的某个场景变成训练环境。比如让一个角色学会走到目标点、让一个机械臂学会抓取、让一个NPC学会躲避障碍。文档给出的完整路径是:先准备好Unity场景和物理组件,然后给智能体挂上ML-Agents组件,再写一个继承自Agent类的C#脚本,最后用Python端配置训练器并开始训练。下面我们就按这个路径一步步来。
2. 动手前的准备:版本选择与项目初始化
2.1 Unity版本与包安装方式
官方明确ML-Agents 1.9.1支持Unity 2018.4以上的版本,我实测下来Unity 2019.4 LTS和2020.3 LTS最省心,2018.4也能跑但有些新特性用不了。建议直接用2020.3或2019.4,别贪新版本,ML-Agents对Unity新版本的适配往往有滞后。
安装方式我推荐用Package Manager通过Git URL导入,因为这样版本最干净,升级降级都方便。操作路径是:打开Unity项目,进入Window -> Package Manager,点击左上角“+”,选择“Add package from git URL...”,输入:
com.unity.ml-agents如果要锁定1.9.1,可以用带tag的地址。安装完成后,Unity会自动拉取依赖。注意:这里有个经常踩的坑,如果你之前从Asset Store导入过老版ML-Agents,先彻底删除老的文件夹再装新的,否则会出现两个命名空间同时存在,编译直接报错。
安装完确认一下:菜单栏应该出现ML-Agents相关的选项,Project窗口里Packages下能看到com.unity.ml-agents。同时建议把示例项目也装上(官方叫“ML-Agents Examples”或单独下载的示例包),后面调试脚本时可以对照着看。
2.2 Python训练端准备:虚拟环境与包安装
Unity端只是“环境模拟器”,真正做强化学习训练的是Python端。ML-Agents 1.9.1对应的Python包版本是mlagents 0.28.0,这里版本对应非常重要:Unity包和Python包的版本必须匹配,否则握手阶段就会报错。
我建议务必用虚拟环境,不要直接装到系统Python里,因为ML-Agents对依赖版本很挑剔,后续你还可能要装TensorFlow或者其他深度学习库,很容易搞乱。按下面这套操作:
python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install mlagents==0.28.0安装完成后,验证一下:
mlagents-learn --help如果能正常打印帮助信息,说明Python端就绪了。这里有个小检查点:mlagents-learn命令在1.9.1这个版本属于新的Python包命令行入口,老教程里的“mlagents-learn config/trainer_config.yaml --train”依然有效,但内部参数和输出格式有变化,别直接用老配置文件,后面我会给出适配1.9.1的新写法。
2.3 Unity项目设置:四件必须做的事
环境跑不起来,很多时候不是脚本问题,而是Unity项目本身的设置问题。我总结出四个必须做的事,缺一个都可能让你在训练时一头雾水:
第一,Player Settings里的API兼容级别要设为.NET 4.x。路径是Edit -> Project Settings -> Player -> Other Settings -> Api Compatibility Level,选.NET Standard 2.0或.NET 4.x(推荐后者)。如果保持默认的“.NET Standard 2.0”也不是完全不行,但部分老API会受限,设成4.x最稳。
第二,开启后台运行。Edit -> Project Settings -> Player -> Resolution and Presentation下,勾选“Run In Background”。否则训练时Unity窗口一旦失去焦点,窗口会暂停,训练直接卡死。
第三,Quality设置降到底。Edit -> Project Settings -> Quality,把所有级别的Quality都设成最低(或者至少保证训练场景用的那个Quality Level是最低档)。光照、阴影、后处理这些对训练没有任何帮助,只会白白占用GPU资源,拖慢训练步进。
第四,检查Time Manager里的Fixed Timestep。物理仿真和Agent的决策步进都依赖FixedUpdate,默认0.02s没问题,但如果你手动改了某个数值,后面训练出来的效果可能会“发飘”。建议保持默认,后续如果追求训练速度,用timeScale提速而不是去改Fixed Timestep。
3. 从零搭建一个训练场景
3.1 场景里的基础物体与平面设置
拿最简单的“小球学习走到目标点”来说。一个完整的训练场景至少需要三类物体:训练场地(通常是带Collider的地面)、目标物体(需要被收集或接近的物体)、智能体(需要学习控制的物体)。其他高级传感器、障碍物、多智能体交互,都是在这个基础框架上扩展的。
先在地面上创建一个小球作为Agent,给它添加Rigidbody组件。这里有一个关键点:Rigidbody的Interpolate可以选插值,但Collision Detection如果不是高速运动场景建议保持默认。另外务必不要勾选Is Kinematic,否则物理力和速度设置都不生效。再创建一个Cube作为目标点,调成明显的颜色,方便训练时肉眼观察。
地面建议放一个足够大的Plane,但要注意:Plane默认的MeshCollider对物理性能不太友好,如果是大场景可以考虑用BoxCollider自己拼地面。这一步看似基础,但直接影响训练速度和碰撞检测的准确性。
3.2 给智能体挂上ML-Agents组件
接下来在Agent小球的GameObject上挂上三个关键组件:Agent脚本(我们自定义的C#脚本)、Behavior Parameters组件、Decision Requester组件。这三者的关系我简单说:Behavior Parameters定义了智能体的“行为名片”,包括行为名、观察空间大小、动作空间类型;Agent脚本是具体逻辑;Decision Requester告诉环境每隔多少个FixedUpdate向决策模块发一次请求。
很多新手只加Behavior Parameters不加Decision Requester,结果训练时候智能体完全不动。不要忘记。
Behavior Parameters组件的关键字段这样配:
- Behavior Name:给这个行为起个唯一的名字,比如
MoveToTarget,这个名字必须和训练配置文件YAML里的行为名保持一致,否则训练器找不到对应的智能体。 - Vector Observation -> Space Size:这个数字是所有观察向量长度的总和。比如我下面的脚本会添加3个三维向量(位置、目标位置、速度),加起来就是9。
- Actions -> Continuous Actions:连续动作维度,这里我设2,代表X和Z两个方向的力。
- Actions -> Discrete Branches:这一行暂时留空,等用离散动作时再填。
- Behavior Type:默认值是Default。训练时保持Default即可,推理时改成Inference Only。
3.3 决策请求器与最大步数
Decision Requester组件上有一个Decision Period参数,官方默认给到5,意思是每5次FixedUpdate请求一次决策。如果你的任务需要更精细的控制,可以调成1(每物理帧都决策),但决策频率越高训练越慢。我实测像小球移动这类任务,Decision Period设5就很合适,智能体动作看起来不会太“神经质”。
Agent脚本上还有一个Max Step参数:每个episode的最大步数。这个参数非常重要。如果设成0,表示不限制,除非脚本里显式调用EndEpisode,否则这个episode永远不会结束。对训练来说,一个永远不结束的episode意味着奖励永远无法收敛。建议新环境先设一个保守值,比如1000,后续根据训练情况调整。达到Max Step后Episode会自动重置,但不会自动给奖励,你需要在脚本里根据业务逻辑给奖励或者给一个-1的惩罚,否则Max Step触发的重置就变成无奖励空转了。
4. 核心脚本的四段式写法
4.1 继承Agent类的脚本骨架
ML-Agents 1.9.1的C# API已经非常规整,一个标准的Agent脚本包含四个核心方法:Initialize(初始化)、OnEpisodeBegin(每个episode开始时重置)、CollectObservations(收集观察)、OnActionReceived(接收动作并执行)、Heuristic(手动控制用)。这里我要把“OnEpisodeBegin”和“Initialize”分清楚:Initialize是智能体创建时执行一次,OnEpisodeBegin是每次episode开始时执行一次。
新手最容易犯的错误是把随机生成位置的代码放在Start或Initialize里,结果智能体永远只在同一个位置起跑。正确的做法是把每一次episode的初始状态变更都放在OnEpisodeBegin里。下面是一个可供直接参考的完整脚本框架:
using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; using Unity.MLAgents.Actuators; public class MoveToTargetAgent : Agent { public Transform target; private Rigidbody rb; public override void Initialize() { rb = GetComponent<Rigidbody>(); } public override void OnEpisodeBegin() { // 每轮开始重置速度,随机位置 rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; transform.localPosition = new Vector3(Random.Range(-4f, 4f), 1f, Random.Range(-4f, 4f)); target.localPosition = new Vector3(Random.Range(-4f, 4f), 1f, Random.Range(-4f, 4f)); } }4.2 观察是怎么收集的
CollectObservations方法负责把当前环境状态传给决策模块。1.9.1版本传入参数是VectorSensor,你需要把需要模型感知的所有数值都AddObservation进去。
public override void CollectObservations(VectorSensor sensor) { sensor.AddObservation(transform.localPosition / 5f); sensor.AddObservation(target.localPosition / 5f); sensor.AddObservation(rb.velocity / 5f); }这里我特别强调“归一化”。很多人直接把位置坐标原样丢进去,比如x=12.3这种大数值,然后发现训练怎么都不收敛。原因是神经网络的输入需要在一个相对小的范围内,最好在[-1, 1]区间。官方文档虽然没有强制要求,但实际项目中这点直接决定训练效率。我这个示例场景边界范围是5,所以除以5把它映射到[-1, 1]附近。你根据自己的场景边界来定这个缩放系数。
观察维度要和Behavior Parameters里的Space Size严格一致。我在上面加了3个三维向量,所以Space Size是9。如果你加了小数、角度值、传感器读数,记得重新数一遍。这里的一致性错误最常见,Unity端跑起来也不一定报错,但训练端会出现观察维度错误甚至直接崩溃,后面排查章节我再细说。
4.3 动作如何映射到环境
OnActionReceived是智能体收到决策结果后的执行函数。1.9.1的ActionBuffers结构体同时包含了连续动作和离散动作,比以前分开写的API清晰很多。
public override void OnActionReceived(ActionBuffers actions) { float moveX = actions.ContinuousActions[0]; float moveZ = actions.ContinuousActions[1]; rb.AddForce(new Vector3(moveX, 0f, moveZ) * 10f); }连续动作的数值范围默认是[-1, 1]区间(由训练器网络输出层决定),你拿到之后可以根据需要做缩放。这里我给10倍的力系数,需要根据物体质量调整。一个实用建议:如果小球质量是默认的1,10倍力已经能让它在地面上滑动得比较快;如果加了阻尼或者质量变大,这个系数要同步调。
动作空间的维度必须和Behavior Parameters里配置的Continuous Actions数量保持一致,不然会在训练时出现“action shape mismatch”。我自己遇到过一个情况:Behavior Parameters里配了3个连续动作,脚本里只读前2个,第三个永远是默认0,结果模型学出来的效果怎么都不对,后来排查半天才发现是这里的粗细问题。
4.4 奖励设计的门道
奖励是整个强化学习环境设计里最关键的环节。我这里先给一个简单但有效的奖励结构:到达目标给正奖励,掉落或超出范围给负奖励并结束,每步加一个微小惩罚让智能体“想尽快完成任务”。
float distance = Vector3.Distance(transform.localPosition, target.localPosition); if (distance < 1.2f) { AddReward(2.0f); EndEpisode(); } else if (transform.localPosition.y < 0f || distance > 20f) { AddReward(-1.0f); EndEpisode(); } else { AddReward(-0.001f); }这里注意,AddReward的数值并不是越大越好。官方文档建议reward保持在合理的数值范围,通常1-10的正奖励配合中小量的步长惩罚就够用,不要去搞几千几万的奖励,那会让梯度爆炸。另外,EndEpisode之前必须AddReward,否则这轮的经验没有终止奖励信号,训练器很难学到“到达目标”这个行为。
关于“稀疏奖励”和“稠密奖励”:上面的设计属于半稠密奖励,因为每步都有惩罚,目标则有大的正奖励。如果只有到达目标才有+2,其他步都是0,那种就是稀疏奖励,训练耗时会长很多。实际做新环境时建议先给简单任务用稠密奖励,等模型能基本完成任务后再逐步降低奖励密度,提升泛化能力。
4.5 启发式方法带来的调试便利
除了自动训练,ML-Agents还支持用手动控制来验证环境和调试奖励参数。这个功能在1.9.1里通过Heuristic方法实现:你可以用键盘输入去控制智能体,直观感受环境是否合理。
public override void Heuristic(float[] actionsOut) { actionsOut[0] = Input.GetAxis("Horizontal"); actionsOut[1] = Input.GetAxis("Vertical"); }写完之后,把Behavior Parameters里的Behavior Type临时切换为“Heuristic Only”,然后在Unity里点Play,你就能用键盘方向键控制小球移动。这一步很重要:在开始训练之前,先手动玩一玩,确认目标位置、碰撞、结束条件都符合预期,奖励设置也没有明显逻辑矛盾。我始终坚信一个原则:如果人手动操作都无法稳定完成任务的环境,让模型去学只会更痛苦。
还有一个隐藏用途:当你使用Heuristic采集数据时,也可以配合IL(Imitation Learning)做模仿学习,先手动录几段完美操作,再用官方示例里的行为克隆来训练初始模型,能显著缩短强化学习的探索时间。
5. 训练器配置与启动训练
5.1 训练器配置文件里的关键参数
Python端的训练靠一个YAML配置文件驱动。ML-Agents 1.9.1的配置文件结构是以behaviors为根节点,每个行为名对应一段配置。我这里给出一个针对小球寻目标任务的PPO配置:
behaviors: MoveToTarget: trainer_type: ppo hyperparameters: batch_size: 128 buffer_size: 2048 learning_rate: 0.0003 beta: 0.01 epsilon: 0.2 lambd: 0.95 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: true hidden_units: 128 num_layers: 2 max_steps: 500000 time_horizon: 64 summary_freq: 5000几个关键参数背后的逻辑我说一下。batch_size是每次梯度更新用多少样本,太小容易震荡,太大训练慢,我这里的128对中小任务够用。buffer_size是经验缓冲池大小,至少要是batch_size的10倍以上,2048是比较常见的起步值。learning_rate决定更新步长,0.0003是PPO的常见选择,太大会崩塌,太小半天不出效果。beta是熵正则系数,控制探索程度,太大模型会一直“乱摸”,太小会过早收敛到某个次优策略。epsilon是PPO裁剪系数,0.2是默认值,不用动。
network_settings里的normalize: true会自动对网络输入做归一化,这里即使脚本里已经手动做了缩放,也建议打开,它能更细腻地处理特征尺度。hidden_units和num_layers控制网络容量,像这种简单任务1282足够,别一上来就用5123,训练慢还容易过拟合。
max_steps是总训练步数上限,500000对这个任务比较合适,你可以根据收敛情况调整。summary_freq是每隔多少步记录一次训练统计信息,默认5000太小会频繁写盘,我常用5000还行。
5.2 启动训练的完整命令
配置好YAML文件后,把它放在项目根目录或专门的config目录下。启动训练的命令是:
mlagents-learn config/ppo.yaml --run-id=MoveToTarget-v1 --train这里--run-id是给这次训练记录起名字,会作为results目录下的文件夹名。不加--train的话实际上是推理模式,这会让你莫名觉得“怎么不更新模型”,所以训练时千万别漏掉--train参数。我建议再加两个参数:
mlagents-learn config/ppo.yaml --run-id=MoveToTarget-v1 --train --num-envs=1 --force--num-envs=1是只开一个Unity环境(默认也是一,显式写出来提醒自己)。--force是允许覆盖核对已有run-id的结果。若想多环境并行加速,可以设--num-envs=4,但同时也要在Unity里配置好编译好的可执行文件(player),否则这个参数无效。
命令执行后,终端会显示类似“Waiting for Unity to connect...”的提示,然后切到Unity编辑器里面,确认场景已经打开,直接点击Play按钮。Unity端会自动通过内部通信机制连接到Python端,之后训练就真正开始了。
5.3 训练过程的检查点与恢复
训练不是一次到位就成功的。ML-Agents会定期保存模型检查点,默认在results目录下的MoveToTarget-v1文件夹里。真正的模型文件是.onnx格式的,训练结束后会生成。
如果你想中断训练改天继续,用同样的--run-id并加--resume参数:
mlagents-learn config/ppo.yaml --run-id=MoveToTarget-v1 --resume我个人的习惯是:每天训练前先跑一次短时间的训练(比如50000步)看曲线趋势,趋势OK再停掉,用--resume接着长跑。这样既能控制在服务器上的时间,也能及时发现问题。
另外一个实用技巧:把训练配置、环境脚本、场景文件都放进一个带版号的项目目录里保存好,因为ML-Agents的YAML配置和Unity包版本绑定太紧,过几个月你可能就忘了当时是怎么配置的。我吃过的亏就是训练出来一个好模型,结果换机器后配置怎么也对不上,最后只能重新调很久。
5.4 TensorBoard监控与判断收敛
训练启动后,另一个终端里运行:
tensorboard --logdir results然后在浏览器打开http://localhost:6006,就能看到训练曲线。重点关注两个指标:累积奖励(Cumulative Reward)和策略熵(Policy Entropy)。累积奖励曲线应该整体向上爬升,然后进入平台期;熵值应该一开始相对较高(在探索),然后逐渐下降,但不要降到接近0,否则模型过早停止探索。
我判断一个环境训练成功的标准有三个:一是累积奖励稳定在一个高点不再大幅波动;二是肉眼观察Unity场景中智能体能从多个随机起点都稳定完成任务;三是用验证模式跑几次不是每次都撞车。只要这三点都满足,就可以停了。
6. 训练不收敛?排查清单与避坑技巧
6.1 环境连接类问题
最典型的场景是:运行mlagents-learn后终端一直显示等待连接,Unity端按下Play却毫无反应。第一反应检查Unity包和Python包版本是否匹配,1.9.1对应mlagents 0.28.0,差一个小版本有时候也会握手失败。第二检查防火墙,ML-Agents默认走本地端口5005,某些安全软件会拦截本地回环端口。第三看Unity控制台有没有报错,如果有异常堆栈,先解决脚本编译问题再说。
如果用了多环境并行,--num-envs>1时记得用--port参数给每个环境指定不同端口,否则环境会抢同一个端口导致连接错乱。这里我吃过亏:开两个环境训练时忘记分配端口,Unity其中一个能连上,另一个一直离线,训练效率不增反降。
6.2 维度与API不匹配
维度错误是训练阶段最常见的技术问题。表现是训练开始时NN端报错,说Observation或Action的shape和模型不匹配。解决思路:检查Behavior Parameters里Vector Observation的Space Size和脚本里AddObservation的数量是否相等;检查Continuous Actions的数量和脚本里ContinuousActions[索引]的使用情况是否一致;如果你配了Discrete Branches,注意DiscreteActions[0]对应的是第一个分支,而不是把所有离散动作数加起来。
另一个容易弄混的API问题是老教程里的heuristic()方法名。1.9.1里官方签名是Heuristic(float[] actionsOut),如果你还沿用旧版不带参数的方法,编译会直接报错。看到编译错误先别急着搜,优先看API差异。
还有一个小细节:Behavior Name不能有空格和特殊字符,且Unity端和YAML里的名字必须完全一致。因为名字对不上时训练器可能不会立刻报错,而是静默忽略掉那个行为,导致奖励曲线完全不更新。我排查这类问题时,习惯打印一下训练器初始化时识别到的behavior名字。
6.3 训练不收敛的常见原因与调参建议
如果奖励曲线长时间不动,或者升到某个值就上不去了,不能只怪算法,先检查环境本身的合理性。我的排查顺序是:
第一,手动玩一遍(用Heuristic Only),如果手动都很难达成目标,说明任务难度本身太高,要么缩小随机范围,要么做课程学习(Curriculum)把初始位置从近到远逐渐拉开。
第二,看奖励是否存在“gaming”漏洞。比如你只设置了到达目标+2,每步-0.001,智能体可能学会在原地打转不积累惩罚,或反复进出某个区域刷奖励。这类问题需要重新设计奖励,例如在进入目标区域时标记已获得过奖励,并在EndEpisode时检查。
第三,检查物理参数是否合理。Rigidbody的力系数、阻尼、场景碰撞体的大小,都会显著影响任务可控性。如果力气太大,物体会到处弹跳,训练极不稳定;力气太小,又走不到目标点。这小参数值得花一天去手工调。
第四,调训练超参。最常用的组合是:learning_rate降到0.0001左右,beta从0.01调到0.005或者0.05,num_epoch降到2。另外time_horizon建议设成64到128之间,太短会丢失长期收益信息,太长会增大方差。
6.4 想用大TimeScale提速?有三件事要照做
官方文档支持通过Time.timeScale把模拟速度调大,比如设成20倍或50倍来加速训练。但实际执行时如果发现训练速度上不去,或者行为变得异常,可能问题出在下面三件事没做对。
第一,确保场景内没有高帧率动画或依赖Update的逻辑,因为TimeScale影响的是Update和FixedUpdate的时间增量,如果你在Update里写了跟训练强相关的逻辑,会导致决策步数和物理步数错位。第二,物理仿真建议保持50Hz(Fixed Timestep=0.02),TimeScale再大也尽量不要去改物理步长,否则碰撞检测会出问题。第三,训练窗口可能会因为帧率不匹配跑得忽快忽慢,有条件的话用-batchmode -nographics启动Unity,能让训练速度更稳定。
我个人在简单寻物任务上,timeScale=20配合低画质设置,训练速度能从每秒几十步提升到每秒两百步以上,而且收敛行为没有明显劣化。当然,timeScale不是万能的,如果任务涉及复杂物理交互(比如机械臂、多关节控制),ratio太大反而会让学习变得不稳定。
6.5 训练完成后的模型导出与实用体会
训练完成后,得到的.onnx文件就是被训练出来的模型。把这个文件拖进Unity工程的Assets目录,在Behavior Parameters组件上指定该模型文件,同时把Behavior Type切换为“Inference Only”,运行项目就能看到训练好的智能体在场景里做决策。这里有个细节:模型文件名或路径不能包含中文和空格,否则Unity加载时可能出问题。
我做这个新环境流程走了好几次,最大的感受是:环境设计占七分,算法调参占三分。官方文档给了很标准的流程模板,但真正的好环境是在一次次的跑图和人工尝试中调出来的。拿奖励设计来说,多调一个数量级、多试一种随机范围,效果天壤之别。所以我特别建议,在投入长时间训练前,先用Heuristic手动确认环境,再跑一个短步数(比如50000步)的“冒烟测试”,看一眼曲线和智能体行为再决定是否加大训练规模。这一步看似麻烦,但能帮你省掉大把无意义的等待时间。