这阵子折腾强化学习又把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 mlagents0.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 GetComponentRigidbody(); } 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); }这里我特别强调“归一化”。很多人直接把位置坐标原样丢进去比如x12.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倍的力系数需要根据物体质量调整。一个实用建议如果小球质量是默认的110倍力已经能让它在地面上滑动得比较快如果加了阻尼或者质量变大这个系数要同步调。动作空间的维度必须和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采集数据时也可以配合ILImitation 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-idMoveToTarget-v1 --train这里--run-id是给这次训练记录起名字会作为results目录下的文件夹名。不加--train的话实际上是推理模式这会让你莫名觉得“怎么不更新模型”所以训练时千万别漏掉--train参数。我建议再加两个参数mlagents-learn config/ppo.yaml --run-idMoveToTarget-v1 --train --num-envs1 --force--num-envs1是只开一个Unity环境默认也是一显式写出来提醒自己。--force是允许覆盖核对已有run-id的结果。若想多环境并行加速可以设--num-envs4但同时也要在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-idMoveToTarget-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-envs1时记得用--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.05num_epoch降到2。另外time_horizon建议设成64到128之间太短会丢失长期收益信息太长会增大方差。6.4 想用大TimeScale提速有三件事要照做官方文档支持通过Time.timeScale把模拟速度调大比如设成20倍或50倍来加速训练。但实际执行时如果发现训练速度上不去或者行为变得异常可能问题出在下面三件事没做对。第一确保场景内没有高帧率动画或依赖Update的逻辑因为TimeScale影响的是Update和FixedUpdate的时间增量如果你在Update里写了跟训练强相关的逻辑会导致决策步数和物理步数错位。第二物理仿真建议保持50HzFixed Timestep0.02TimeScale再大也尽量不要去改物理步长否则碰撞检测会出问题。第三训练窗口可能会因为帧率不匹配跑得忽快忽慢有条件的话用-batchmode -nographics启动Unity能让训练速度更稳定。我个人在简单寻物任务上timeScale20配合低画质设置训练速度能从每秒几十步提升到每秒两百步以上而且收敛行为没有明显劣化。当然timeScale不是万能的如果任务涉及复杂物理交互比如机械臂、多关节控制ratio太大反而会让学习变得不稳定。6.5 训练完成后的模型导出与实用体会训练完成后得到的.onnx文件就是被训练出来的模型。把这个文件拖进Unity工程的Assets目录在Behavior Parameters组件上指定该模型文件同时把Behavior Type切换为“Inference Only”运行项目就能看到训练好的智能体在场景里做决策。这里有个细节模型文件名或路径不能包含中文和空格否则Unity加载时可能出问题。我做这个新环境流程走了好几次最大的感受是环境设计占七分算法调参占三分。官方文档给了很标准的流程模板但真正的好环境是在一次次的跑图和人工尝试中调出来的。拿奖励设计来说多调一个数量级、多试一种随机范围效果天壤之别。所以我特别建议在投入长时间训练前先用Heuristic手动确认环境再跑一个短步数比如50000步的“冒烟测试”看一眼曲线和智能体行为再决定是否加大训练规模。这一步看似麻烦但能帮你省掉大把无意义的等待时间。
阅读完成 · 觉得有帮助?