ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity ML-Agents实战:从零构建强化学习驱动的游戏AI

Unity ML-Agents实战:从零构建强化学习驱动的游戏AI 1. 项目概述当游戏角色开始“自学成才”如果你还在用一长串的if-else或者复杂的状态机来驱动你的NPC是时候换个思路了。想象一下你设计的敌人不再只是按照预设的路径巡逻而是会观察你的走位预判你的攻击甚至学会利用地形进行伏击你训练的宠物不再只会执行几个固定的指令而是能根据环境和你互动的历史发展出独特的“性格”和行为模式。这听起来像是遥远的未来不这就是 Unity ML-Agents 工具箱正在让开发者触手可及的现实。ML-Agents 本质上是一个桥梁它将游戏开发世界与机器学习特别是强化学习世界连接了起来。它把 Unity 编辑器变成了一个功能强大的“虚拟生物实验室”而你就是那个设定进化规则的设计师。你不再需要手把手地教角色每一个动作而是通过定义“奖励”和“惩罚”让角色在虚拟环境中通过数百万次的试错自己找到达成目标的最优策略。这个过程我们称之为“训练”。训练完成后你会得到一个神经网络模型一个.nn文件这就是角色的“大脑”。把这个大脑装回你的游戏一个能自主“思考”的智能体就诞生了。这个工具箱的价值远不止于让方块学会走路。它适用于任何需要复杂决策、适应性行为或玩家-like AI 的场景从 RTS 游戏中的单位微操到开放世界 NPC 的拟真反应再到解谜游戏中能自适应玩家策略的对手。它降低了机器学习的门槛让你能用熟悉的 Unity 工作流创造出真正拥有“学习能力”的游戏内容。2. 核心原理拆解奖励驱动的试错学习要玩转 ML-Agents你必须理解其核心——强化学习。你可以把它想象成训练一只小狗。小狗不知道“握手”是什么但你可以在它抬起爪子时给它一块零食正向奖励在其他时候不给或给予轻微惩罚。经过多次重复小狗就把“抬爪”和“获得零食”联系了起来。ML-Agents 中的智能体Agent就是这只“小狗”而你就是那个手握零食的训练师。2.1 强化学习的关键要素在这个框架里有几个核心概念构成了智能体与环境的交互循环智能体 (Agent)你需要训练的对象即游戏中的角色。它身上挂载着继承自Agent基类的脚本。环境 (Environment)你的 Unity 场景。它包含了地面、障碍物、目标等一切外部元素。状态/观察 (State/Observation)智能体感知环境的方式。在 ML-Agents 中你通过CollectObservations方法将环境信息如自身位置、目标位置、生命值、敌人距离等转化为一组数字向量输入给神经网络。这是智能体的“眼睛”和“耳朵”。动作 (Action)智能体可以做的事情。在OnActionReceived方法中你将神经网络输出的数字例如一个代表“向前移动力”的 -1 到 1 的值转化为实际的行为如给刚体施加一个力。动作可以是连续的如转向角度也可以是离散的如跳跃、下蹲、开枪。奖励 (Reward)驱动学习的核心。你需要在代码中 strategically 地调用AddReward()函数。奖励信号告诉智能体“刚才那一步做得好不好”。达成目标给一个大额正奖励10犯错给一个惩罚-1甚至为了鼓励效率每一步都可以给一个微小的负奖励-0.001以激励其尽快完成任务。策略 (Policy)即神经网络模型。它根据当前的“观察”决定采取哪个“动作”以最大化未来累积的奖励期望。训练的过程就是不断优化这个策略的过程。2.2 训练流程PPO 算法与课程学习ML-Agents 默认使用 PPO近端策略优化算法这是一种非常稳健的强化学习算法。你可以把它理解为一个既鼓励探索尝试新动作又防止“学坏”动作变得太随机的教练。在后台Unity 会启动一个 Python 进程运行训练算法而你的 Unity 编辑器则作为“环境模拟器”在运行。更强大的是课程学习 (Curriculum Learning)功能。就像教小孩先学爬再学走一样你可以把复杂任务分解成多个阶段。在配置文件中你可以定义当智能体在简单环境如无障碍中的成功率超过 90% 后自动切换到更难的环境如加入移动障碍。这能极大地解决稀疏奖励问题即智能体在成功前几乎得不到任何有效反馈导致它无从学起是训练复杂行为的利器。注意奖励函数的设计是整个项目成败的关键也是最容易“踩坑”的地方。一个设计不当的奖励函数会导致智能体学会各种意想不到的“作弊”行为。例如如果你的惩罚是“每存活一秒扣 0.1 分”而“到达终点加 100 分”智能体可能会发现与其冒险冲向终点不如找个角落躲起来“苟活”因为扣分速度很慢。这被称为“奖励黑客”。3. 环境搭建与项目初始化实战理论说再多不如动手搭一个。我们从零开始构建一个经典案例训练一个立方体 Agent 找到场景中随机位置的目标球。3.1 基础环境配置首先确保你有一个较新版本的 Unity Hub 和 Unity Editor推荐 2021 LTS 或更新版本。新建一个 3D 项目命名为MLAgents_Playground。安装 ML-Agents 包是第一步。打开Window - Package Manager。点击左上角的号选择Add package from git URL...。输入官方包地址com.unity.ml-agents。点击AddUnity 会自动下载并安装。这个过程可能会花费几分钟取决于你的网络。安装完成后强烈建议导入示例项目。在 Package Manager 中找到已安装的ML-Agents包在详情页切换到Samples选项卡点击Import按钮导入Getting Started示例。这里面包含了多个预制场景和脚本是极佳的学习参考能帮你省去大量搭建基础组件的时间。3.2 构建第一个训练场景现在让我们搭建一个最简单的训练场创建地面在 Hierarchy 窗口右键 -3D Object - Plane重命名为Ground缩放至合适大小如 Scale 设为 10, 1, 10。创建智能体右键 -3D Object - Cube重命名为Agent。将其 Y 轴位置设为 0.5使其刚好立在地面上。创建目标右键 -3D Object - Sphere重命名为Target。将其放置在离 Agent 一段距离的位置如 X5, Y0.5, Z5。添加物理与标签选中Agent点击Add Component添加Rigidbody刚体。为了简化初期训练可以暂时取消勾选Use Gravity避免它因摔倒而增加学习难度。确保Agent有Box Collider。选中Target确保其有Sphere Collider并勾选Is Trigger这样 Agent 可以穿过它并触发事件。在Target的 Tag 下拉列表中点击Add Tag...新建一个名为Target的标签并赋予给Target物体。可选创建训练边界复制几个Cube缩放成围墙摆放在Ground边缘防止 Agent 在训练初期跑出视野。为它们创建一个新 Tag如Wall。至此一个最基础的训练环境就搭建好了。接下来我们要赋予 Agent “灵魂”。4. 编写智能体脚本定义观察、动作与奖励在 Project 窗口右键 -Create - C# Script命名为MoveToTargetAgent。双击打开我们将逐步实现一个完整 Agent。4.1 脚本骨架与初始化using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; using Unity.MLAgents.Actuators; public class MoveToTargetAgent : Agent // 必须继承自 Agent 类 { // 关键引用 public Transform targetTransform; // 在 Inspector 中拖拽赋值 private Rigidbody agentRigidbody; // 初始化类似于 Start() public override void Initialize() { agentRigidbody GetComponentRigidbody(); // 确保刚体存在如果忘记添加这里可以自动添加 if (agentRigidbody null) { agentRigidbody gameObject.AddComponentRigidbody(); agentRigidbody.useGravity false; // 按需设置 } } }4.2 定义观察告诉大脑“世界是什么样子”CollectObservations方法负责收集环境信息。我们决定让 Agent 知道三件事自己在哪、目标在哪、自己跑得多快。public override void CollectObservations(VectorSensor sensor) { // 观察1: Agent自身的位置 (3个浮点数: x, y, z) sensor.AddObservation(transform.localPosition); // 观察2: 目标点的位置 (3个浮点数) sensor.AddObservation(targetTransform.localPosition); // 观察3: Agent当前的速度 (3个浮点数) sensor.AddObservation(agentRigidbody.velocity); // 总观察值数量 3 3 3 9 // 这个数字必须与 Behavior Parameters 组件中的 Vector Observation Space Size 严格一致 }为什么是这些观察值位置信息是导航的基础。速度信息则至关重要它让 Agent 能感知自己的运动状态是静止、加速还是减速没有速度信息Agent 很难学会平稳地启动和停止容易产生振荡。4.3 定义动作大脑决策后“该怎么做”OnActionReceived是执行指令的地方。我们假设大脑输出两个连续值分别控制 X 轴和 Z 轴的力。public override void OnActionReceived(ActionBuffers actions) { // 从大脑的输出缓冲区获取连续动作 float moveX actions.ContinuousActions[0]; // 范围通常在 [-1, 1] float moveZ actions.ContinuousActions[1]; // 将输出值转换为力。系数 5f 需要根据你的 Agent 质量和场景大小调整 Vector3 controlSignal new Vector3(moveX, 0, moveZ); Vector3 force controlSignal * 5f; // 使用 VelocityChange 力模式忽略质量使控制更直接 agentRigidbody.AddForce(force, ForceMode.VelocityChange); // 基于距离的奖励/惩罚设计 float distanceToTarget Vector3.Distance(transform.localPosition, targetTransform.localPosition); // 核心奖励1每一步的小惩罚鼓励快速完成任务时间惩罚 AddReward(-0.001f); // 核心奖励2越靠近目标给予的即时奖励越多稠密奖励解决稀疏性问题 // 计算一个基于距离的奖励距离越近奖励越高 float proximityReward 0.01f * (1f / (distanceToTarget 1f)); // 防止除零 AddReward(proximityReward); }这里引入了“稠密奖励”的概念。如果只在碰到目标时给一个大奖励稀疏奖励Agent 在探索初期几乎得不到任何正向反馈学习会非常缓慢甚至失败。我们通过proximityReward提供了一个连续的、与距离成反比的奖励信号像一块磁铁始终将 Agent 引向目标大大加速了学习过程。4.4 处理回合与碰撞事件// 每个训练回合(Episode)开始时调用用于重置状态 public override void OnEpisodeBegin() { // 随机重置Agent的位置增加训练的泛化能力 float randomX Random.Range(-4f, 4f); float randomZ Random.Range(-4f, 4f); transform.localPosition new Vector3(randomX, 0.5f, randomZ); // 重置物理状态 agentRigidbody.velocity Vector3.zero; agentRigidbody.angularVelocity Vector3.zero; transform.rotation Quaternion.identity; // 也可以随机重置目标的位置让学习更具挑战性 // targetTransform.localPosition new Vector3(Random.Range(-4f, 4f), 0.5f, Random.Range(-4f, 4f)); } // 碰撞检测用于判断成功或失败 private void OnTriggerEnter(Collider other) { if (other.CompareTag(Target)) { // 成功抵达目标给予巨额奖励 AddReward(10.0f); // 可选播放成功音效或粒子效果 // Debug.Log(Target Reached!); EndEpisode(); // 结束本轮触发 OnEpisodeBegin } else if (other.CompareTag(Wall)) { // 撞墙了给予惩罚并结束本轮 AddReward(-2.0f); EndEpisode(); } }4.5 实现手动控制Heuristic用于调试这是一个极其重要但常被忽略的步骤。它让你可以用键盘控制 Agent验证你的动作逻辑移动、转向是否正常同时也是调试奖励函数的绝佳工具。public override void Heuristic(in ActionBuffers actionsOut) { var continuousActions actionsOut.ContinuousActions; // 将键盘输入映射到动作输出 continuousActions[0] Input.GetAxis(Horizontal); // A/D 键对应 X 轴力 continuousActions[1] Input.GetAxis(Vertical); // W/S 键对应 Z 轴力 }5. 编辑器配置与训练启动脚本写好后回到 Unity 编辑器进行配置。5.1 组件挂载与参数设置将MoveToTargetAgent脚本拖到Agent立方体上。在 Inspector 中将场景中的Target球体拖拽到脚本的Target Transform字段。为Agent添加Behavior Parameters组件。这是连接 Agent 脚本与 ML-Agents 训练后端的关键。Behavior Name: 填写MoveToTarget。这个名字必须与后续配置文件中的行为名严格一致。Vector Observation Space Size: 填入9。这就是我们CollectObservations方法中AddObservation调用次数的总和。Actions Continuous Actions: 填入2。对应OnActionReceived中我们期望的两个连续动作值moveX, moveZ。Model: 暂时留空。训练完成后我们会把生成的.nn模型文件拖到这里。在Behavior Parameters组件下方确保Agent脚本的Use Heuristic在训练时是取消勾选的我们让算法控制但在手动测试时可以勾选。5.2 编写训练配置文件在项目根目录与Assets同级下创建一个新文件夹config。在里面创建一个文本文件重命名为move_to_target.yaml。用文本编辑器如 VSCode打开输入以下内容behaviors: MoveToTarget: # 必须与 Behavior Name 完全一致 trainer_type: ppo # 使用PPO算法 hyperparameters: batch_size: 64 # 每次参数更新使用的经验样本数 buffer_size: 1024 # 经验回放缓冲区大小 learning_rate: 3.0e-4 # 学习率太大不稳定太小学得慢 beta: 5.0e-4 # 熵系数鼓励探索 epsilon: 0.2 # PPO裁剪参数 lambd: 0.95 # GAE参数权衡偏差与方差 num_epoch: 3 # 每次更新时对数据进行几轮优化 learning_rate_schedule: linear # 学习率随训练步数线性衰减 network_settings: normalize: false # 是否归一化输入简单任务可关闭 hidden_units: 128 # 神经网络隐藏层神经元数量 num_layers: 2 # 神经网络隐藏层层数 reward_signals: extrinsic: gamma: 0.99 # 折扣因子越接近1表示越重视远期奖励 strength: 1.0 # 外部奖励的权重 max_steps: 500000 # 最大训练步数 time_horizon: 64 # 每个Agent收集多少步经验后进行一次更新 summary_freq: 10000 # 每多少步输出一次训练摘要到TensorBoard5.3 启动训练流程打开命令行终端Windows 的 CMD/PowerShellMac/Linux 的 Terminal。使用cd命令导航到你的 Unity 项目根目录。输入训练命令mlagents-learn config/move_to_target.yaml --run-idRun_01 --forcemlagents-learn: 训练命令。config/move_to_target.yaml: 你的配置文件路径。--run-idRun_01: 为本次训练运行指定一个名称用于区分不同实验。--force: 如果同名 run-id 已存在则覆盖。命令行会显示Start training by pressing the Play button in the Unity Editor.。切回 Unity 编辑器点击 Play 按钮。此时场景中可能会出现多个 Agent 的副本取决于配置它们会同时开始探索。观察命令行窗口你会看到奖励Cumulative Reward等指标在滚动。同时你可以直接在 Game 视图中观察 Agent 们从“无头苍蝇”到“精准导航”的进化过程。训练达到最大步数或你手动CtrlC停止后在项目根目录下的results/Run_01文件夹中会找到生成的MoveToTarget.nn模型文件。将此.nn文件拖入 Unity 项目的Assets文件夹。然后选中Agent在Behavior Parameters组件的Model字段中拖入这个模型文件。取消勾选Agent脚本上的Use Heuristic再次点击 Play。现在你的立方体将使用训练好的 AI 大脑自主导航到目标点6. 实战进阶从走到跑解锁复杂行为基础移动只是开始。ML-Agents 的真正威力在于构建复杂、智能的行为。6.1 添加视觉观察让 Agent “看见”世界除了向量观察ML-Agents 支持基于摄像头的视觉观察。这适用于需要识别形状、颜色或复杂空间关系的任务。给 Agent 添加一个子物体作为“眼睛”比如一个Camera。在 Agent 脚本中声明一个CameraSensorComponent类型的公共变量并在 Inspector 中赋值。在CollectObservations中你不再需要手动添加目标位置等神经网络会直接从摄像头画面中学习特征。但请注意视觉训练对算力要求高训练时间更长且需要更精细的奖励设计。6.2 实现离散动作与组合动作很多游戏行为是离散的比如“跳跃”、“开枪”、“切换武器”。ML-Agents 支持离散动作空间。// 在 Behavior Parameters 中设置 Discrete Actions 分支例如定义两个分支 // Branch 0: 3个动作 (0: 不动1: 左转2: 右转) // Branch 1: 2个动作 (0: 不跳1: 跳) public override void OnActionReceived(ActionBuffers actions) { // 处理离散动作 int turnAction actions.DiscreteActions[0]; // 取值 0, 1, 2 int jumpAction actions.DiscreteActions[1]; // 取值 0, 1 float turnStrength 0f; switch (turnAction) { case 1: turnStrength -1f; break; // 左转 case 2: turnStrength 1f; break; // 右转 } // 应用转向... if (jumpAction 1 IsGrounded()) { // 执行跳跃 agentRigidbody.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } // 同时也可以处理连续动作如移动 float moveZ actions.ContinuousActions[0]; // ... 应用移动 }6.3 多智能体与竞争/协作在同一个场景中放置多个具有相同Behavior Name的 Agent它们会共享同一个大脑进行训练。你可以设计奖励机制让它们竞争如对抗游戏或协作如共同搬运物体。竞争给击败对手的 Agent 正奖励给被击败的 Agent 负奖励。协作给共同完成任务的群体一个共享的大奖励。这里需要注意“信用分配”问题即如何将群体奖励合理地分配给个体。ML-Agents 提供了Group Reward等机制来辅助处理。6.4 利用课程学习攻克难关对于非常困难的任务直接训练可能无法收敛。课程学习允许你定义多个关卡逐步提升难度。behaviors: MoveToTarget: trainer_type: ppo # ... 其他超参数 curriculum: - name: EasyLevel # 第一阶段课程 completion_criteria: measure: reward # 以奖励为衡量标准 behavior: MoveToTarget threshold: 1.0 # 当平均奖励超过1.0时进入下一阶段 min_lesson_length: 1000 # 本阶段至少训练1000步 value: 0.0 # 这个参数会传递给环境用于控制难度 parameters: obstacle_count: 0 # 无障碍物 target_range: 5.0 # 目标出现范围较小 - name: HardLevel completion_criteria: measure: reward behavior: MoveToTarget threshold: 5.0 min_lesson_length: 5000 value: 1.0 parameters: obstacle_count: 5 # 增加5个障碍物 target_range: 10.0 # 目标出现范围变大在你的 Agent 脚本中可以通过Academy实例读取这些课程参数并动态调整环境如生成障碍物。7. 血泪踩坑指南与调优心法这部分是无数小时调试换来的经验能帮你避开最常见的陷阱。7.1 奖励函数设计艺术与科学的结合坑1奖励稀疏。Agent 在成功前得不到任何反馈。解决设计“塑形奖励”。例如在走迷宫时不仅给最终出口奖励还给每一步更靠近出口一个微小正奖励。坑2奖励冲突。例如同时奖励“收集金币”和“保持高速”Agent 可能会为了高速而绕过金币。解决仔细权衡不同奖励的权重strength或者设计更高级的奖励结构如分层奖励。坑3局部最优与奖励黑客。Agent 找到了一个刷分但不合你意的漏洞。例如在一个需要跳跃的平台游戏中如果“存活”有微小正奖励而“坠落”有大惩罚Agent 可能学会了一动不动。解决增加负奖励惩罚要非常小心。更好的办法是修改环境或观察空间堵上漏洞。实操心得大量使用Heuristic模式进行手动测试。你亲自操作 Agent感受当前的奖励设置是否合理。你觉得别扭或不合理的地方AI 学起来也一定困难。7.2 观察空间给大脑恰到好处的信息坑1信息不足。没给速度Agent 学不会平稳移动没给相对角度Agent 学不会有效转向。解决从第一性原理思考完成这个任务一个智能体最少需要知道哪些信息坑2信息冗余或噪声。提供了无关信息如天空盒颜色或包含大量噪声会干扰学习。解决保持观察空间简洁、干净、归一化。例如位置坐标可以相对于某个参考点或者进行缩放。坑3观察值尺度不一。位置坐标可能是几十个单位而速度值可能是个位数。这会导致神经网络难以处理。解决在脚本中对观察值进行归一化或者启用Behavior Parameters中的Normalize选项对于简单任务可能不需要。7.3 超参数调优没有银弹只有耐心配置文件中的超参数没有绝对的最优值。但有一些启发性原则batch_size和buffer_size通常保持默认比例buffer_size是batch_size的 10-20 倍。如果任务复杂可以适当增大。learning_rate最常见的调整参数。如果奖励曲线震荡剧烈上蹿下跳尝试调低如1.0e-4。如果学习速度太慢曲线几乎不动尝试调高如5.0e-4。使用linear衰减通常是好选择。beta熵系数鼓励探索。如果 Agent 过早地陷入单一行为模式可以适当增加beta值。hidden_units和num_layers对于简单任务如走到点128和2足够。对于复杂任务如视觉输入、多智能体可以尝试增大网络规模如256或512个单元3层。7.4 训练过程监控与调试使用 TensorBoardML-Agents 训练时会自动生成 TensorBoard 日志。在命令行中进入results文件夹下的具体运行目录运行tensorboard --logdir .然后在浏览器打开提示的地址。你可以看到奖励曲线、 episode 长度、熵值等关键指标的可视化图表。奖励曲线稳步上升是健康的如果剧烈波动或下降说明有问题。在 Unity 中实时观察训练时多看看 Game 视图。Agent 们是不是在“抽风”是不是卡在某个角落这些直观现象能给你最直接的调试线索。打印日志在 Agent 脚本的关键位置如获得大奖励或惩罚时使用Debug.Log可以帮助你理解 Agent 在何时因何故受到了奖励。7.5 环境与物理的坑物理不稳定训练中如果大量使用物理如刚体碰撞可能会导致模拟不一致影响学习。解决尽量简化碰撞体使用ForceMode.VelocityChange替代ForceMode.Force以获得更直接的控制或者考虑在训练时固定时间步长Time.fixedDeltaTime。Episode 长度失控如果 Agent 永远无法成功或失败EndEpisode()不被调用会导致一个 Episode 无限长。解决在Agent脚本的FixedUpdate中设置一个最大步数计数器超时则给予惩罚并EndEpisode()。训练一个强大的游戏 AI 并非一蹴而就它更像是一场与算法的对话。你通过奖励函数和观察空间提出“问题”算法通过试错给出“答案”。这个过程需要反复迭代、观察和调整。但当你在游戏中看到那个由你亲手“培育”出来的智能体做出超出你预设的精彩行为时那种成就感是无与伦比的。ML-Agents 打开了一扇新的大门门后是让游戏角色真正“活”过来的无限可能。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进