聊ML-Agents网上能找到的教程基本都是拿官方示例跑一遍——3D平衡小球、推箱子、训练僵尸看着挺热闹但真轮到自己做项目第一道坎就卡在“怎么创建一个自己的训练环境”。我一开始也被这道坎绊了挺久感觉示例代码都能跑一换到自己的任务就到处报错后来把官方文档里“创建新训练环境”这一节反复啃了几遍才彻底理顺了整个流程。这篇内容相当于把官方这套流程按实际操作顺序重写了一遍目标很简单让你从零搭出一个能跑训练的最小环境。包括场景怎么搭、Agent脚本怎么写、训练配置怎么调、模型怎么导回Unity中间穿插一些我自己踩过的坑。适合刚装好ML-Agents 1.9.1、准备脱离示例做自己任务的开发者参考Unity用的2018到2020的LTS版本都适用。1. 动手前先搞明白ML-Agents的“训练环境”到底由哪几块组成很多人一上来就急着写Agent脚本结果场景里缺组件、行为参数没配对训练起来各种莫名其妙。其实ML-Agents里的“训练环境”不是一个单一的东西而是三个模块的配合Unity场景、Agent脚本、Behavior配置。理解这三者的分工后面所有操作才能对号入座。1.1 训练环境的三大模块先给一个整体认知Unity场景是“身体”Agent脚本是“神经系统”Behavior Parameters是“大脑接口”。Unity场景负责物理模拟和状态反馈。地面、障碍物、可交互物体、Agent的本体全部放在场景里。ML-Agents训练的本质就是让Agent在物理环境中反复试错所以场景里的碰撞体、刚体、物理材质一定要认真设置任何物理异常都会直接影响训练效果。Agent脚本是你写的核心代码。它决定了Agent每回合怎么感知环境、怎么做动作、怎么获得奖励。一个Agent组件挂到角色对象上之后Unity引擎会在每个决策周期调用它的三个回调方法CollectObservations、OnActionReceived、OnEpisodeBegin。Behavior Parameters是配置项。它告诉系统Agent能看到多少维度的信息、能输出多少维度的动作、动作是连续的还是离散的、训练时用哪个模型。这个组件不写代码纯在Inspector面板里配置但它决定了Agent学习的输入输出边界。这三者的关系可以类比成一个人场景是周围的世界Agent是人的感官和肌肉而Behavior Parameters是神经系统的输入输出带宽。任何一环出了问题训练都会失败只是失败方式不同。1.2 版本搭配1.9.1的Unity包对应哪一版Python包ML-Agents最大的坑之一就是版本匹配。Unity里的包版本和Python侧的包版本是严格对应的装错直接连不上。ML-Agents 1.9.1这个Unity包版本对应的Python包是mlagents 0.28.x系列。Unity侧通过Package Manager安装的是com.unity.ml-agents 1.9.1Python侧通过pip安装的是mlagents0.28.0两者能正常通信。Unity版本方面2018.4 LTS、2019.4 LTS、2020.3 LTS这几个版本都验证过能正常工作。我个人建议优先用2020.3 LTS因为编辑器自带的组件兼容性更好而且Package Manager操作起来更顺。2018版本较老部分新特性的API接口会有细微差别比如传感器组件的命名和Inspector界面长得不太一样。Python环境建议用3.7或3.8配合PyTorch 1.6到1.10之间的版本。装完包之后可以执行一条命令验证版本是否正确pip show mlagents如果在Unity里点击Play后训练端完全没反应大概率就是上面的版本对错了。这个我后面专门写一节细说。2. 搭建场景从一个空白场景到一个能训练的Agent理解了组成之后就可以动手搭了。这里我以一个最简单的“小球追赶目标”任务为例这是官方文档里常用的一类示例任务适合用来跑通整个流程之后换成你自己的任务逻辑就行。2.1 场景物体的基本组成新建一个场景之后需要准备三样东西地面、目标物体、Agent本体。地面可以直接用自带Cube拉成扁平形状或者用3D Object-Plane。注意Plane的默认碰撞体是Mesh Collider有时会出问题我习惯用Cube缩成地面形状然后挂Box Collider稳定省心。给地面一个物理材质摩擦系数设低一点这样Agent在上面移动的时候不会因为摩擦力方向不一致导致抖动的奇怪行为。目标物体用一个小Cube或者Sphere挂上Collider方便后面做触发检测。Agent本体我用的是一个胶囊体或者Cube挂上Rigidbody然后锁定旋转避免训练过程中翻倒。这一步很关键——如果你的Agent在训练时动不动就翻车模型很难学到有用的策略。然后给Agent本体挂上三个组件Agent组件自己写的脚本继承自AgentBehavior Parameters组件Decision Requester组件有人说还缺传感器组件没错传感器可以挂在这个对象上也可以写在脚本里创建后面专门讲。2.2 Behavior Parameters的每个字段该怎么填Behavior Parameters是环境能不能跑起来的关键里面每个字段都有明确含义我的建议如下字段含义训练时的推荐设置Behavior Name行为标识起一个不易混淆的名字比如Runner_0Vector Observation / Space Type观察类型纯位置速度类任务用Vector Observation即可Vector Observation / Space Size观察维度等于你自己AddObservation的总数Vector Action / Space Type动作类型连续Continuous或离散Discrete选一Vector Action / Space Size动作维度连续动作填动作分量个数离散动作填分支数Model模型文件训练时留空Behavior Type模式训练时Default调试时可用Heuristic OnlyBehavior Name尤其要注意后面训练产出的模型文件、TensorBoard里的曲线标签都跟它绑定。如果你在同一个场景里放了多个AgentBehavior Name必须不同否则训练端会把它们的经验混在一起。Vector Action是另一个容易搞混的地方。连续动作的Space Size不是动作的组合数而是动作分量的个数。比如你可以控制水平转向和前后移动两个分量那么Space Size就等于2。而离散动作的Space Size表示有几个离散分支每个分支的“选项数量”是另外的一个参数——在1.9.1版本中离散动作的分支选项数通过Branch Sizes来设置。2.3 Decision Requester与决策频率Decision Requester的作用是告诉引擎每隔多少帧调用一次Agent的决策逻辑。它的核心参数是Decision Period默认是5。也就是说正常情况下Agent每5帧做一次动作决策而不是每帧都决策。这个设计是有道理的强化学习训练中动作频率太高会导致训练步数爆炸而且相邻帧之间的状态几乎没有变化信息冗余很大。5帧一次的默认值适合大多数物理控制任务。如果你的任务对响应速度要求高比如弹幕躲避可以改成1或2如果是策略性强的回合制任务可以改成10以上。有一点要注意Decision Requester组件决策频率与Behavior Parameters是配合工作的。Behavior Parameters决定了Agent怎么做决策用什么模型、动作维度多大Decision Requester决定什么时候做决策。两者缺一不可——没有Behavior Parameters没法做决策没有Decision Requester决策根本不会触发。3. Agent脚本三个必须实现的方法以及它们的坑组件齐了之后核心工作就落在Agent脚本上。一个完整的Agent子类至少要重写三个方法还有一个可以按需重写。这里我直接贴一个可用的最小脚本然后逐个方法讲清楚设计逻辑。using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Actuators; using Unity.MLAgents.Sensors; public class RunnerAgent : Agent { public Transform target; public float speed 2f; public override void OnEpisodeBegin() { // 随机重置Agent位置 transform.localPosition new Vector3( Random.Range(-4f, 4f), 0.5f, Random.Range(-4f, 4f)); // 随机重置目标位置 target.localPosition new Vector3( Random.Range(-4f, 4f), 0.5f, Random.Range(-4f, 4f)); } public override void CollectObservations(VectorSensor sensor) { Vector3 diff target.localPosition - transform.localPosition; sensor.AddObservation(diff.x / 8f); sensor.AddObservation(diff.z / 8f); sensor.AddObservation(Vector3.Distance(transform.localPosition, target.localPosition) / 8f); } public override void OnActionReceived(ActionBuffers actions) { float moveX actions.ContinuousActions[0]; float moveZ actions.ContinuousActions[1]; transform.localPosition new Vector3(moveX, 0f, moveZ) * speed * Time.deltaTime; if (Vector3.Distance(transform.localPosition, target.localPosition) 1.2f) { AddReward(1.0f); EndEpisode(); } AddReward(-0.001f); } }3.1 CollectObservations观察向量设计的黄金法则CollectObservations是Agent感知环境的入口参数VectorSensor是收集观察向量的容器你调用AddObservation加入的每一个值都会被拼接到观察向量里最终喂给神经网络。这个方法的写法直接影响训练效率我总结了几条黄金法则第一尽量使用相对信息。观察自己和目标的位置差而不是各自的绝对坐标。绝对坐标在场景固定时会让Agent记住“坐标A有奖励”一旦场景变化就失效。相对位置差让Agent学到的是“目标在我右边多远”这种通用概念。第二观察值必须归一化。神经网络对输入数值范围很敏感0到1之间的输入最稳定。上面的代码里把坐标差除以8是因为场景范围大概是正负4坐标差范围就在正负8之间归一化后基本落在正负1之间。如果不做归一化直接传入几十几百的数值训练初期梯度就容易爆炸。第三不要一股脑加太多无用的观察。很多新手喜欢把几十个状态全部丢进去觉得信息越全越好。实际上无关观察会稀释有效信号的权重模型更难收敛。这条小球追目标的任务里坐标差和目标距离已经足够没必要再加一大堆装饰性信息。3.2 OnActionReceived从动作到奖励的完整闭环OnActionReceived做的两件事执行动作给出奖励。动作的执行方式取决于你在Behavior Parameters里配置的动作类型。如果是连续动作actions.ContinuousActions是一个float数组每个元素对应一个连续控制量通常用于速度、力矩、位置偏移控制。如果是离散动作actions.DiscreteActions是int数组通常用于选择方向、跳跃、攻击等离散决策。奖励设计是这里最核心的部分也是强化学习项目中最容易翻车的点。上面这个简单任务我用了“到达目标得1分、每步扣0.001”的设定看起来很简单但背后是有考虑的稀疏奖励只在任务成功/失败时给奖励的问题是信号太稀疏尤其在复杂任务里Agent可能在几千步内都得不到任何正反馈训练效率极低。所以我在任务本身成功奖励之外加了微小的每步惩罚强制Agent学最短路径而不是原地打转或者到处闲逛。这个每步惩罚的数值要小如果设成0.1会跟任务成功奖励相差太小Agent可能选择呆在原地不动来减少惩罚。实践中还有一个常见问题EndEpisode调用之后没有及时给奖励。EndEpisode表示当前回合结束此后这个回合的累计奖励会被统计进平均奖励里。如果你在调用EndEpisode之前忘了AddReward那回合结束的奖励就是0前面所有探索都白干了。所以正确顺序是先AddReward再EndEpisode。3.3 OnEpisodeBegin随机初始状态是防作弊的关键OnEpisodeBegin做的事情很简单重置每个训练回合的初始状态。但就是这一步决定模型能不能泛化。如果你的场景里Agent和目标每次都从同一个位置出发Agent会记住一套固定的动作套路换一个初始位置就完全不会玩了。这就像学生背答案换了题目就懵。正确的做法是每一回合都随机化初始位置让Agent接触到尽可能多的状态分布。随机化的时候注意范围要合理别把Agent随机到墙里面也别让目标一开始就贴在Agent脸上。我个人的习惯是给随机半径留一点安全边界上一节代码里随机范围是正负4也就是整个场地可活动范围的八成左右保证基本不出现极端初始化情况。如果你的任务比较复杂比如躲避障碍或者多目标选择不妨把障碍物位置、目标数量也纳入随机化范围。总之让Agent每次开局面对的都是一道“新题”。4. 传感器选型让Agent“看世界”的方式决定训练上限前面说的Vector Observation是直接传入数值相当于直接把游戏状态给Agent。但很多任务里Agent并不知道全局状态只能靠“眼观六路、耳听八方”来感知这时候就要用传感器了。ML-Agents在1.9.1版本里最常用的是RayPerceptionSensor3D可选的还有CameraSensor和自写传感器。4.1 RayPerceptionSensor3D为什么推荐它RayPerceptionSensor3D是ML-Agents的经典传感器模拟了从Agent中心向周围发射多条射线检测射线碰到什么物体、距离多远。在Inspector面板里需要配置几个关键参数射线数量、Ray Start和Ray End决定了探测范围Detection on Which Layers指定检测哪些层。它输出的观察向量格式是固定拼接的每条射线的命中信息包括是否命中、命中距离、命中物体的类型等维度。我推荐首选射线传感器原因是它的输入信息非常稳定不容易受到光照变化影响而且计算开销小。相比之下相机传感器要看渲染画面环境中光影变了、遮挡变了都可能给模型带来无谓的扰动。对于大多数物理控制任务射线传感器的信息量已经足够支持Agent做决策。在场景里挂上RayPerceptionSensor3D之后它产生的观察会自动附加到Agent的观察向量末尾。也就是说Behavior Parameters里的Vector Observation Space Size只需填自己手动AddObservation的数量不用手动去算射线传感器的维度。如果你在Inspector里看到传感器的Gizmos线条可以直接在Scene视图里拖动视角查看射线覆盖范围。我第一次调这个参数的时候就是靠肉眼盯着每个方向的射线判断死角调试效率反而比看数值快。4.2 CameraSensor和自定义传感器什么时候用CameraSensor适用于需要视觉感知的任务比如自动驾驶识别车道线、机器人通过图像辨认物体。它的配置和Unity的摄像机组件绑定注意设置好分辨率分辨率越高训练成本越高。一般建议先用低分辨率比如64x64甚至32x32跑通流程再慢慢提高。如果你的任务逻辑复杂也可以实现ISensor接口写自定义传感器。比如你想让Agent知道前方某个扇区内的敌人密度既没有现成的射线传感器又不想传全局状态就自己写一个传感器来计算这个值。自定义传感器通常不写进场景组件而是在Agent的GetSensor方法里动态创建。但要注意每次训练入口的传感器数量、维度必须一致。如果传感器是动态生成的务必保证每回合创建的数量相同否则训练端在拼接观察向量时维度对不上直接就报错。4.3 观察向量汇总怎么算一个Agent的输入维度一个Agent的输入维度等于Behavior Parameters里Vector Observation Space Size加上所有附加传感器的维度总和。举一个实际的例子我这篇示例里的RunnerAgent脚本里手动添加了3个观察值x差、z差、距离没有附加任何传感器那Space Size就是3。如果再加一个RayPerceptionSensor3D总观察维度就是3加上射线传感器产出的维度。有个工具方法可以直接在运行时打印出来方便你核对public override void OnEnable() { ObservationWriter writer new ObservationWriter(); Debug.Log($Observation size: {GetObservationShape()}); }第一次跑之前建议确认一下观察维度。如果维度跟你设计的差了太多往往就是传感器重复挂载或者Space Size填错了。5. 训练配置与启动第一次跑通mlagents-learn环境搭完脚本写完传感器就有了接下来进入正式训练阶段。这一步涉及两部分写一个YAML训练配置文件然后用mlagents-learn命令把Unity和Python连接起来。5.1 训练配置文件的必填项ML-Agents的训练超参数放在一个YAML文件里默认文件名可以写成trainer_config.yaml。核心配置如下behaviors: Runner_0: trainer_type: ppo max_steps: 500000 time_horizon: 128 summary_freq: 10000 hyperparameters: batch_size: 128 buffer_size: 2048 learning_rate: 0.0003 learning_rate_schedule: linear network_settings: normalize: true hidden_units: 128 num_layers: 2 reward_signals: extrinsic: strength: 1.0 gamma: 0.99解释几个关键参数trainer_typeppo还是sac。PPO是稳定且适应性最广的新手首选SAC适合连续控制且对探索效率要求更高的任务。max_steps训练总步数。太小模型学不会太大浪费时间。我这个简单任务用50万步已经差不多了复杂任务可以调大到200万甚至更多。time_horizonAgent在截断序列前的最长步数影响策略估计的粒度和收敛稳定性128是官方推荐起步值。network_settings里的normalize设为true相当于对观察值做归一化即使脚本里没有手动归一化也帮你稳一手。gamma折扣因子调整Agent对远期奖励的重视程度0.99表示比较看重远期收益。5.2 启动训练一行命令的参数含义训练启动命令长这样mlagents-learn config/trainer_config.yaml --run-idrunner_first --train--run-id是本次训练的标识它在训练结果目录里创建子文件夹名字不要重复否则要么覆盖之前的训练记录要么启动时报错。--train表示以训练模式运行不加的话默认是推理模式会尝试加载模型。如果你需要从之前中断的训练继续可以加--resume参数后面跟之前的run-id。命令执行后Python端会显示“Waiting for Unity...”之类的内容这是正常的。接着回到Unity编辑器打开你的训练场景点击Play按钮两边就会开始通信。第一次跑的时候可以在TensorBoard里看训练曲线。启动TensorBoard的命令是tensorboard --logdir results浏览器打开本地端口看到环境的平均奖励曲线。平均奖励从负值或零值往上爬说明Agent在变聪明如果一直趴在原地不动就要回头检查环境和奖励设计了。5.3 多环境并行训练如果整个训练只在Unity编辑器里跑速度会很慢因为编辑器本身要承担大量渲染和物理计算。ML-Agents支持通过--num-envs参数拉起多个Unity环境并行训练每个环境里各有一个Agent同时探索训练速度可以提升很多。一个常规做法是把场景Build成可执行文件然后用mlagents-learn config/trainer_config.yaml --run-idrunner_first --train --num-envs4这个命令会自动启动4个并行的Unity进程。如果你的电脑性能一般先从2个试起。并行环境数量太多会导致物理模拟线程抢占反而让单个环境变得不稳定。6. 训练收尾模型导出、嵌入与验证训练结束后模型的真正考验才刚开始——能不能在Unity场景里独立运行。6.1 训练产出的文件在哪里训练完成后打开results目录你会看到以之前--run-id命名的文件夹里面包含若干个.onnx文件命名格式是 .onnx。例如我的Runner_0行为训练完后的模型文件就是Runner_0.onnx。还有对应的.onnx.json等配置文件记录了模型的结构和输入输出。这些文件不用管只要.onnx存在就可以接入Unity。如果训练中途崩溃了先确认有没有.onnx文件生成如果没有检查是否训练步数不足或者命令意外中断。这种情况通常只需要重新跑一次。6.2 在Unity里切换推理模式把模型接回Unity的步骤很简单回到Agent对象上的Behavior Parameters组件把之前留空的Model字段拖入刚才生成的.onnx文件然后把Behavior Type从Default改为Inference Only。这样Agent在执行时就会直接用模型输出动作不再通过训练端控制。改完之后直接点击Play。注意如果Behavior Type不改即使挂了模型Agent也不会真正用模型决策——它可能会忽略模型或者仍走训练通信CtrlP那样只是在编辑器里跑训练逻辑并不是真正的推理验证。如果你希望Agent在推理模式下仍然保留一些随机性可以保留Behavior Type为Heuristic Only但那必须配合写好的Heuristic方法否则Agent不会动。6.3 如果模型效果不理想怎么办第一次推理跑出来的Agent很可能不完美——有时速度慢、有时撞墙、有时卡在一个地方抖动。这种时候先别急着改代码按照顺序检查第一训练步数够不够。50万步对简单任务够了复杂任务可能远远不够。把max_steps调大到100万或200万重新训练观察TensorBoard里奖励曲线是否还在上升期。第二奖励设计的合理性。如果Agent一直原地转圈大概率是奖励函数没有引导它朝目标走或者每步惩罚太大。试着降低每步惩罚或者给靠近目标的距离差一个负反馈。第三推理时是否加回了传感器和碰撞体。有时候训练时场景里一切正常推理时不小心把传感器组件隐藏了或者把Rigidbody的约束改了Agent就直接“失明”了。按这个顺序排查大多数“模型像个傻子”的问题都能找到根源。7. 踩坑记录版本错位、通信失败、奖励不收敛最后这部分我把自己在创建训练环境过程中踩过的坑集中写出来这些都是官方文档里没有细说的地方但概率极高。7.1 Unity包和Python包版本错位训练端没反应这是我印象最深的一个坑。某次训练命令行正常输出Waiting for UnityUnity也点了Play结果两端完全没握手。查了很久才发现Python侧的mlagents装成了0.27.x跟Unity侧1.9.1对应不上。ML-Agents的Unity包和Python包版本必须一一对应差一个小版本都可能无法通信。装完包之后先pip show mlagents确认版本号符合搭配逻辑。如果版本不对直接用pip install mlagents0.28.0重新安装再重启训练端和Unity。7.2 缺少Decision RequesterAgent全程静止还有一种常见情况组件、脚本、行为配置都齐了但Agent在场景里一动不动。排查了一圈发现Agent对象上根本没挂Decision Requester组件。没有Decision RequesterAgent就没有触发决策的定时器自然的就永远不会调用OnActionReceived也不会输出动作。新手最容易忽略这个组件因为Agent脚本本身没有报错。验证Agent是否在正常决策一个很土的方法是在OnActionReceived里加一行Debug.Log看看训练时这个函数到底有没有被调用。如果完全没有日志输出第一反应就该检查决策节奏组件。7.3 训练很久不收敛的排查顺序训练半小时后TensorBoard上的平均奖励一直在0附近波动完全看不出上升趋势。这种情况我一般按这个顺序排查观察是否归一化。手动观察值如果达到几十甚至上百网络输入分布直接崩掉奖励信号被淹没。是否有及时的正向奖励反馈。如果任务是稀疏奖励Agent可能需要非常多的探索才能偶尔碰到一次成功耐心调大max_steps同时考虑加一些中间状态的塑形奖励。动作是否过大或过小。连续动作输出值是-1到1之间如果你的速度乘数设成100Agent一个决策帧就能飞出场景训练必然失败。根据物理规则先让动作幅度在一个稳定的范围内再开始训练。Episode长度是否合理。如果每个回合无限长Agent一直不撞墙不结束训练体验就会拖得很长很难拿到足够的样本。你可以考虑在超时步数后强制结束回合并给一个小的惩罚。按照这套顺序排查几乎每次都能找到问题所在。训练环境创建这件事说难也难说简单也简单——难在细节多简单在只要把数据结构理清楚、奖励信号设计合理剩下的就是耐心调参。我现在的习惯是每改一次观察或奖励先用小步数跑10分钟看看曲线走向确认有起色再放大训练量这样能省下大量试错时间。希望这篇梳理能让你少走一些弯路。