
一、先坦白一件事MHAC 不是某篇论文的名字搜MHAC你找不到一个权威定义。它在工业界和博客里通常指Multi-Head Actor-Critic —— 多头 Actor-Critic是一类架构的统称不是某个特定模型。它的学术锚点是这篇真实存在的论文《Hybrid Actor-Critic Reinforcement Learning in Parameterized Action Space》Fan Zhou, Rui Su, Weinan Zhang, Yong Yu上海交通大学IJCAI 2019DOI: 10.24963/ijcai.2019/316这篇提出了HACHybrid Actor-Critic以及它在 PPO 上的实例H-PPO。核心思想一句话用一种多个平行子 actor 网络 一个共享 critic的架构把结构化动作空间拆成几个简单动作空间。再加上后来的Invalid Action Masking、Autoregressive Action HeadAlphaStar 用的那套等工程技巧就构成了今天大家口中的MHAC 多头动作空间。所以这篇文章讲的不是某个黑魔法而是一个被工业界反复验证、也确实有效的工程范式。二、为什么一个头不够用先看传统 RL 的两种极端类型例子问题纯离散动作上/下/左/右/开火/换弹想表达往这个方向以这个速度精确瞄准就没辙了纯连续动作[前进量, 左右量, yaw, pitch]想表达按下换弹键这种开关动作就很别扭而射击游戏里真正的动作长这样开火 瞄准方向 (yaw127.3°, pitch-4.1°) 换弹 没有参数 投掷 力度 (0.73) 抛物线角度 (42°) 移动 方向 (8 选 1) 是否冲刺这就是参数化动作空间Parameterized Action Space一个离散的选择后面跟一串连续的参数而且不同离散动作的参数个数还不一样。换弹没有参数投掷有两个瞄准有二十几个准星轨迹。把它拍平成一个连续向量会怎样有人会说那就搞一个[a_discrete_idx, p1, p2, ..., p24]的大向量让网络一起输出呗。会有三个致命问题无效组合爆炸。选择换弹却输出了一个瞄准参数——这个参数完全没有意义但梯度还是会去训练它纯纯浪费网络容量、污染学习信号。参数维度不匹配。投掷是 2 维参数瞄准是 24 维。硬塞进一个向量等于让网络同时学两种完全不同的东西。Critic 无法输入。这是 HAC 论文点出的关键如果用 Q(s,a)critic 的输入必须包含所有候选动作的参数因为你得比较它们。不同动作参数维度不同根本拼不成一个固定长度的输入。强行拼就会过参数化over-parameterization——大量无效输入让估计方差爆炸。三、MHAC 的答案切开来各管各的架构长什么样┌─────────────────────┐ 状态 s ────────►│ 共享编码器 (Backbone)│───► 特征向量 h (地图/血量/ │ CNN Transformer │ 队友位置...) └──────────┬──────────┘ │ ┌──────────┬─────────────┼─────────────┬──────────┐ ▼ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ │移动头 │ │动作头 │ │瞄准头 │ │目标头 │ │价值头 │ │离散(9) │ │离散(6) │ │连续(2) │ │离散(N) │ │V(s) │ └────────┘ └────────┘ └─────────┘ └────────┘ └────────┘ π_move π_act π_aim π_target Critic关键设计所有头共享同一个编码器。这是全部价值的来源编码器学到的这局现在是个什么局面对每一个头都有用样本效率大幅提升参数数量比每个动作单独训一个网络少得多每个头只需处理自己那点输出维度低、梯度干净。Critic 只吃状态不吃动作HAC 论文里一个很关键的取舍用 V(s) 而不是 Q(s,a) 作为 critic 的输出。理由前面说了不同离散动作的参数维度不同Q(s,a) 的输入没法统一。而 V(s) 只依赖状态完全回避了这个问题然后用它算优势函数Â_t r_t γV(s_{t1}) − V(s_t)这个优势值同时喂给所有头去更新它们的策略。一个 critic带多个 actor——这就是多头 Actor-Critic名字的由来。四、数学上怎么更新各头独立优势共享以 H-PPO 为例。假设有 K 个头第 k 个头有自己的策略 π_θk。损失函数是L Σ_k L_k^CLIP(θ_k) − c₁ · L^VF(θ_critic) c₂ · S[π_θk] ▲ ▲ ▲ 各头独立的 PPO 裁剪项 共享的 value 损失 各自的熵正则三个必须注意的细节① 每个头有独立的旧策略概率。不能复用同一个old_log_probratio_moveexp(logp_move_new-logp_move_old)# 各算各的ratio_actexp(logp_act_new-logp_act_old)② 联合动作的 log 概率是各头之和且各头条件独立logp_jointlogp_movelogp_actlogp_aim.sum(-1)logp_target③ 熵系数要分头调。离散头需要较大熵鼓励探索不同动作连续头尤其是瞄准熵系数要小——瞄准的探索空间是连续高维的熵给大了准星会疯狂乱飘训练根本收敛不了。实践值离散头ent_coef ≈ 0.01瞄准头≈ 0.001甚至 0。五、代码骨架classMHACActor(nn.Module):def__init__(self,obs_dim,n_move9,n_act6,n_target8):super().__init__()# ---- 共享躯干 ----self.backbonenn.Sequential(nn.Linear(obs_dim,512),nn.ReLU(),nn.Linear(512,256),nn.ReLU(),)# ---- 各分支头注意都是小网络 ----self.move_headnn.Linear(256,n_move)self.act_headnn.Linear(256,n_act)self.aim_headnn.Linear(256,4)# 输出 2 个正态分布的 mu log_stdself.target_headnn.Linear(256,n_target)self.criticnn.Linear(256,1)# 只有 V(s)defforward(self,obs,maskNone):hself.backbone(obs)logits_moveself.move_head(h)logits_actself.act_head(h)logits_tgtself.target_head(h)# ★★ 关键无效动作掩码在 softmax 之前把非法动作打成 -infifmaskisnotNone:logits_actlogits_act.masked_fill(mask[act].eq(0),-1e9)logits_tgtlogits_tgt.masked_fill(mask[tgt].eq(0),-1e9)logits_movelogits_move.masked_fill(mask[move].eq(0),-1e9)mu,log_stdself.aim_head(h).chunk(2,dim-1)log_stdlog_std.clamp(-5,0)# 必须夹住否则数值爆掉return(Categorical(logitslogits_move),Categorical(logitslogits_act),Normal(mu,log_std.exp()),Categorical(logitslogits_tgt),self.critic(h).squeeze(-1))动作掩码这一步是 MHAC 能不能训起来的生死线。不给掩码会怎样假设弹匣空了、周围没有敌人网络输出开火 → 环境返回无效 → 浪费一个 tick还可能吃负奖励网络输出瞄准 3 号敌人但 3 号敌人不在视野内、或者已经死了网络要从这些无效尝试里慢慢学会什么不能干这要消耗海量样本。而掩码是直接把非法选项的概率改成 0——这是纯粹的领域知识注入零成本、零副作用。必守纪律采样时打掩码算 log_prob 时必须用同一套掩码。很多人训练崩在这里——采样用了掩码、计算 PPO ratio 时忘了ratio 立刻算错整个裁剪机制失效。六、射击游戏实战案例背景一款 5v5 战术射击的 Bot 系统目标是让 AI 的战术水平接近人类中高段位。案例 1单兵动作空间怎么切头类型维度内容移动头离散98 方向 站定姿态头离散3站/蹲/趴动作头离散7开火 / 换弹 / 换武器 / 投掷 / 交互 / 冲刺 / 空操作瞄准头连续2yaw、pitch 的每 tick 增量目标头离散8可见敌人中选一个作为注意力目标五个设计决策每个都有坑① 瞄准输出的是增量不是绝对角度。用绝对角度直接输出 yaw127.3°会有一个严重问题网络要输出一个和上一帧几乎相同的绝对值才能保持稳定瞄准而正态分布的采样噪声会让准星每帧抖动。改成增量后网络的输出天然接近 0 均值、小方差稳定性好得多。yawdelta_yaw*MAX_TURN_PER_TICK# 增量式天然平滑② 瞄准头的 log_std 初值要压小。如果你把log_std初始化为 0std1在 ±180° 的角度空间里等于让准星每 tick 随机转 1 弧度——AI 永远打不中人。实践初始化log_std -3std ≈ 0.05让它先学会稳住熵随训练自动增长。③ 目标头的作用是注意力不是开火对象。开了目标头网络的注意力会聚焦到指定敌人的特征上类似 MOBA 里的 target attention 机制。这比让网络自己从一堆敌人特征里学该关注谁要高效得多。④ 目标头必须有掩码。视野外、已死、烟雾中的敌人必须屏蔽。否则会出现AI 隔墙对着不存在的目标开枪这种经典智障行为。⑤ 移动头不包含追人这类复合意图。复合意图交给网络从状态里自己学。头切得太细等于把领域知识硬编码进去反而限制了策略空间。这是调 MHAC 最常见的过拟合错误。案例 2动作掩码的完整依赖关系掩码不是独立的它们之间有依赖链defbuild_mask(state):mask{}# 弹药has_ammostate.ammo0can_reloadstate.ammostate.mag_sizeandstate.spare0has_nadestate.nades0# 姿态趴下时不能冲刺can_sprintstate.stance!PRONE mask[act]torch.tensor([has_ammo,# 开火can_reload,# 换弹state.has_secondary,# 换武器has_nade,# 投掷state.near_interactable,# 交互can_sprint,# 冲刺1,# 空操作永远是合法退路])# ★ 依赖掩码没弹药时瞄准谁都不该瞄准ifnothas_ammoandnotcan_reload:mask[tgt]torch.zeros_like(...)# 全屏蔽else:mask[tgt](enemy_visibleenemy_alivenot_in_smoke).float()# ★ 目标头全屏蔽时动作头也必须屏蔽开火ifmask[tgt].sum()0:mask[act][FIRE]0returnmask最后那条级联掩码很重要没有可见目标 → 不该开火。漏掉它AI 会养成对着空气开枪、浪费弹药的习惯而这在战术射击里是会直接输掉对枪的。工程纪律掩码逻辑要写成纯函数和游戏状态解耦方便单元测试。掩码 bug 是这类系统最难查的问题——它不报错只是让 AI 变笨而且你很难判断是策略没学好还是掩码写错了。案例 3多头 多任务价值头MHAC 里除了动作头还有一个常被忽略的用法在 critic 后面挂多个辅助价值头共享躯干、预测不同维度self.v_returnnn.Linear(256,1)# 主任务到本局结束的胜负self.v_healthnn.Linear(256,1)# 辅助3 秒后剩余血量self.v_ammonn.Linear(256,1)# 辅助3 秒后弹药self.v_distnn.Linear(256,1)# 辅助3 秒后与最近敌人距离为什么有用这叫辅助任务学习Auxiliary Task Learning。胜负信号在一局 3 分钟里只出现一次极其稀疏而3 秒后我还能剩多少血每时每刻都有信号。这些稠密的辅助梯度逼着共享躯干学到真正有用的局面表征主任务的收敛速度通常能提升 2~3 倍。TensorFlow 的 Agent57、以及很多 MOBA AI都用过类似的多头价值估计。这是多头最被低估的用法。案例 4分层 MHAC —— 战术头 执行头5v5 里最难的决策不是这一 tick 打哪里而是这 20 秒我们打 A 点还是 B 点。时间尺度不一样就不该用同一套频率输出。战术头每 2 秒决策一次离散 12 维打A/打B/转点/收缩/架枪/救队友... │ 作为意图向量注入下层 ▼ 执行头每 tick 决策就是案例 1 的那套这就是 HAC 论文里说的该架构可以扩展到更一般的、有层级结构的动作空间。工程收益战术层 2 秒决策一次计算量降到 1/120假设 60 tick可以放开用大模型执行层不再需要记住跨 20 秒的长期目标样本效率大幅提升战术层可以直接 RL 训练执行层用行为克隆BC从人类录像里预训练——因为怎么做瞄准、走位人类数据充足“做什么”战术才是 RL 该解的问题。腾讯在《王者荣耀》1v1 的绝悟工作AAAI 2020,Mastering Complex Control in MOBA Games with Deep Reinforcement Learning里就用了类似的分层 目标注意力 无效动作屏蔽的组合拳这是公开可查的、最接近的工业级参考。案例 5大厂都在做的同一件事团队系统动作空间设计DeepMindAlphaStar星际 2动作空间是层级/自回归的先选动作类型 → 再选参数谁/哪里/什么时候。约 10²⁶ 种组合不可行枚举必须分解OpenAIOpenAI FiveDota 2每 tick 拆成多个动作头主动作 延迟参数 目标选择并且动作频率受限4 tick 一次因为人类手速有上限DeepMindFTW夺旗刻意限制动作空间到人类可达的范围无瞬间 180° 转身、无像素级瞄准换取样本效率和像人的行为腾讯 AI Lab绝悟王者荣耀分层动作 目标注意力 无效动作屏蔽外加动作频率约束UnityML-Agents官方支持Hybrid Action Space一个策略同时输出 Discrete 分支和 Continuous 分支是入门 MHAC 最简单的起点DeepMindIMPALA多任务多头的经典骨架共享躯干 多头的标准范式来源注意 DeepMind 在 FTW 里的选择他们故意缩小动作空间。这是个反直觉但极其重要的工程判断——动作空间不是越大越好。给 AI 一个 300° 瞬时转向能力它能训出人类打不过的怪物但这不是你要的 AI而且它的行为会显得非人玩家体验很差。动作空间设计本身就是游戏设计。七、踩坑清单坑后果对策采样打掩码、算 ratio 忘打PPO 裁剪失效训练崩掩码作为策略输入的一部分一起保存瞄准头 log_std 初值太大准星乱飘永不收敛初值 -3按头单独调 entropy coef瞄准输出绝对角度每帧抖动输出每 tick 增量并做 clip各头共用同一个 old_log_probratio 算错每个头独立保存头切得太细把追人做一个头限制策略空间泛化差只切动作类型不切复合意图各头 entropy 系数相同连续头过度探索分头设置连续头小一个量级不同头用不同学习率但共享 Adam动量张量互相污染每个头独立 optimizer或分组参数掩码有 bug如忘了级联AI 变笨但不报错极难排查掩码写成纯函数 单元测试只用稀疏胜负奖励训多任务主任务收敛慢上辅助价值头本质是 reward shaping动作频率和人类不一致训出的 AI 手感非人玩家不爽参考 OpenAI Five主动限制决策频率八、资源清单原始论文Fan Zhou et al.,Hybrid Actor-Critic Reinforcement Learning in Parameterized Action Space,IJCAI 2019arXiv:1903.01344——HAC 与 H-PPO 的出处必读。动作空间设计AlphaStar 的Grandmaster level in StarCraft IINature 2019看它的自回归动作头怎么处理 10²⁶ 空间。无效动作屏蔽Invalid Action Masking: A Technique to Improve Policy LearningHuang Ontañón, 2020——掩码为什么有效的理论说明。工业级实践Mastering Complex Control in MOBA Games with Deep RL腾讯 AI Lab, AAAI 2020——目标注意力 动作屏蔽。辅助任务UNREALJaderberg et al.——多头辅助价值函数的经典来源。上手工具Unity ML-Agents官方支持 Hybrid Action Space代码可读性最好、Ray RLlibMultiDiscreteBox组合掩码支持完善。环境ViZDoomDoom 上的 FPS RL 环境学术标准、ML-Agents 的 PushBlock/Soccer、Dota 2 / 星际 2 的第三方环境。九、收尾把这篇压成三句话MHAC 不是某个模型是一类架构——它的学术锚点是 HAC / H-PPOIJCAI 2019核心是多个平行子 actor 一个共享 critic把结构化动作空间切开。它为什么必要射击游戏的动作天生是离散选择 连续参数拍平会爆炸共用一个头会互相干扰。切开是为了让每个头只学一件干净的事。它真正的价值不在架构本身而在那几个工程细节动作掩码、增量式瞄准、分头熵系数、辅助价值头、以及别把头切得太细。最后一句最重要动作空间的设计就是游戏 AI 的设计。DeepMind 在 FTW 里主动缩小动作空间OpenAI Five 主动限制决策频率——他们放弃了一部分理论上更强换来了训得动、行为像人、玩家愿意玩。这才是游戏 AI 和论文 AI 的根本差别。