直接说结论:做过多目标任务的人,不管眼下在搞推荐、广告还是内容分发,碰到“多个业务目标打架”这种问题,基本都绕不开MMOE(Multi-gate Mixture-of-Experts,多门控专家混合)这条建模思路。MMOE从原理上把“共享表示”和“任务特化”这对矛盾拆开处理,用多个底层专家网络搭配每个任务独立门控的方式,让模型自己学会在不同任务之间分配网络容量。这篇文章不聊Paper复现,只讲怎么把它想清楚、怎么在自己的项目里真正落地,适合正在做多目标排序、CTR/CVR联合建模、或者准备从单任务切多任务的算法同学参考。我会把建模动机、数学拆解、工程实现和踩坑记录都过一遍,保证你看完能直接评估这个方案适不适合自己的场景。
1. 建模动机:MMOE到底在解决什么问题
1.1 单任务模型堆多了之后,麻烦就来了
先假设一个最简单的业务场景。某内容平台要做推荐排序,线上一个模型要同时预测用户会不会点击(CTR)、会不会看完(完播率)、会不会点赞(互动率)。三个目标都重要,但早期做法很直白——每个目标单独训练一个模型,各用各的特征和样本,上线之后把三个分数简单加权或者相乘,凑成一个最终排序分。
这个方案一开始能跑,但随着特征体系变大、业务节奏变快,问题会越来越明显。最直接的麻烦是特征重复计算。三套模型共享大量用户行为特征和物品特征,但每一套都要单独做特征工程、单独训练、单独上线,离线实验和线上调试的成本直接翻三倍。更麻烦的是线上推理,三个模型各算一遍,为了控制耗时还得把特征抽取逻辑并行化,工程复杂度一下就上去了。
真正让多目标联合建模成为刚需的,是数据稀疏和信息互补。比如新用户或者冷门内容,点击样本可能有一批,但点赞、收藏这类深层行为的样本少得可怜。单目标模型在这种稀疏目标上很难学好,因为特征维度太高、有效样本太少,模型很容易过拟合到少数几个强特征上。但如果把这些任务放在一起学,浅层反馈(点击、曝光时长)和深层反馈(点赞、关注)之间存在共享的底层模式,比如用户对内容风格的基本偏好,这种共享信息可以让深层任务借到浅层任务的数据力量。这是多任务学习最本源的收益,MMOE的一切设计也都是围绕这个收益展开的。
1.2 硬参数共享和单门控MoE的局限,是理解MMOE的钥匙
多任务建模不是只有MMOE一个选择,但理解它为什么出现,需要先看另外两个更基础的方案。
硬参数共享(Hard Parameter Sharing)是早期最常见的结构:底层一个大网络共享特征表示,顶层分叉成多个任务塔。这个方案直观、参数量小,对相似度高、正相关强的任务效果不错。但它的隐含假设是“所有任务可以用同一套特征表示来支撑”,一旦任务之间相关性弱甚至负相关,共享层的优化方向就会互相拉扯,出现经典的跷跷板现象——A任务涨了,B任务就跌。比如电商场景里,点击率和购买转化率大概率正相关,但点击率和“退货率”“投诉率”这类目标之间就可能存在冲突,硬共享很容易顾此失彼。
单门控MoE是在共享层和任务塔之间加一层“专家混合”,底层有多个专家网络(一般是多个MLP),一个门控网络根据输入样本计算各专家的权重,加权求和后送给所有任务塔。它比硬共享灵活,因为模型可以根据不同样本选择不同专家组合,相当于按样本自适应地划分共享空间。但问题在于:所有任务共用一个门控,它学到的权重分布是对全部任务的折中。某公司内部模拟项目X的测试里,两个任务在特征分布上明显不同,单门控会倾向输出比较平滑的权重,让每个任务都拿不到自己最想要的那部分专家,结果是两个任务的表现都不算最优。
MMOE的改进点就在“Multi-gate”上——每个任务拥有独立的门控网络。这意味着任务A可以选择专家1和专家2的组合,任务B可以选专家2和专家3的组合,不同任务对专家网络的利用方式完全由自己决定。从直觉上看,这既保留了多任务共享带来的数据增强,又给每个任务留了足够大的个性化空间。Google原论文里的实验数据也印证了这一点,在合成数据和公开数据集上,MMOE的AUC和相关性建模能力通常优于硬共享和单门控MoE,尤其是在任务相关性波动时不那么脆。
MMOE结构上最值得注意的点:它不是简单地把“共享底层”替换成“多个专家”,而是引入了“门控按任务拆开”这个核心设计。多门控才是这个名字的灵魂,专家混合只是提供多样化的候选空间。
2. 核心细节解析:MMOE的结构、公式与关键参数
2.1 输入、专家、门控、任务塔四个层次各司其职
把MMOE的模型结构从上到下拆开看,其实就是四层:
输入层:所有任务共享同一份特征输入,包括用户特征、物品特征、上下文特征以及交叉特征。这里有一个容易被忽视的细节:MMOE本身不负责特征交互,它是在你已经完成了特征工程之后、模型主干阶段发挥作用。所以输入层一般就是一个Embedding拼接层,把稀疏ID类特征转成稠密向量,再拼上连续特征,送进专家网络。
专家层(Expert Layer):底部有k个专家网络,通常每个专家是结构相同但参数独立的MLP或残差网络。这些专家就像一组“专业顾问”,理论上每个专家能学会某种特征组合模式。MMOE不要你预先定义专家分别干什么,它们的专长是在训练过程中自动分化出来的。需要说明一点:专家不是越多越好,它跟模型的容量和数据的复杂度有关,具体后面会讲。
门控层(Gate Layer):每个任务t对应一个独立的门控网络g^t,输入同样是原始特征x,输出是一个k维向量,经过Softmax归一化得到各专家的权重。门控网络本身一般就是一个小的MLP加Softmax,参数量不大,但它是整个MMOE里最微妙的部分,因为“每个任务如何选择专家”这件事完全由它决定。
任务塔层(Task Tower):每个任务t有自己的塔网络h^t,输入是门控加权组合后的专家输出,经过几层全连接,最后输出任务预测值。这一层的结构基本和大家熟悉的单任务模型塔一致,可以接sigmoid做二分类,也可以接softmax做多分类。
举个电商场景的例子会更直观。假设专家1学到的是“价格敏感型用户”的行为模式,专家2学到的是“内容偏好型用户”的模式。点击率任务的门控可能会对价格敏感用户加大专家1的权重,购买转化任务的门控则可能更均衡地使用两个专家,因为它需要同时考虑价格和内容吸引力。这种样本级、任务级的差异化选择,就是多门控的巧妙之处。
2.2 数学表达与理解角度:MoE加权、门控概率与任务共享
形式化地看,MMOE的计算过程可以分为三个关键公式,理解这三个公式基本就理解了论文的核心。
设输入特征为x,专家网络的集合表示为 f_i(x),其中下标i表示第i个专家,i从1到k。对任务t,门控网络先计算原始logits:
gate_t(x) = W_g^t · x + b_g^t
这个gate_t(x)是一个k维向量,然后在专家维度上做Softmax归一化,得到每个专家的概率权重:
g_i^t(x) = softmax(gate_t(x))_i
最后,任务t的塔网络输入就是这个权重加权的专家输出之和:
y^t = h^t( Σ_{i=1}^{k} g_i^t(x) · f_i(x) )
注意这里的加权方式:先对每个专家的输出向量 f_i(x) 做标量权重 g_i^t(x) 的加权,然后再累加,而不是把专家输出在向量维度上concat。加权求和的做法有两个好处:一是输出维度与单个专家一致,不会因为堆专家数量导致后续塔网络输入维度过大;二是在梯度反传时,每个专家都会收到所有任务的梯度信号,只是权重系数不同,这让专家网络能够充分利用多任务的数据收益。
从优化的角度看,门控可以理解为一个“软选择器”。非要说它的规律的话,在训练初期,不同任务的门控会迅速分化,各自偏向对自己有用的专家;训练后期,如果某些专家对所有任务都没太多贡献,门控会给它接近零的权重,相当于模型自动完成了专家淘汰。另外,因为每个任务有独立的门控参数,所以即使两个任务的梯度冲突比较严重,门控也可以选择错开使用不同的专家集合,在一定程度上缓解梯度冲突带来的优化困难。
2.3 效果来源:MMOE为什么能缓解任务冲突
很多搞了两年以上多任务建模的同行都会问:MMOE到底是在数学上保证了什么,还是一个结构巧妙的“经验设计”?说实话,它更像后者,但它的经验设计是有明确统计学依据的。
第一,专家共享带来了样本效率的提升。k个专家虽然不是严格意义上为不同任务分开的,但任务A的样本可以提升专家网络的表示质量,而任务B的门控又可以复用这套表示,这在小样本任务上尤其受益。
第二,多门控引入了一种隐式的任务相关性建模能力。当两个任务高度相关时,它们的门控会倾向于输出相似的权重分布,专家使用方式趋同;当任务负相关时,它们会分化到不同的专家子集。也就是说,模型不需要预先告诉你任务之间有多相关,门控在训练中会自动“测量”出这种关系,并用权重分布表达出来。这也是论文题目里“Modeling Task Relationships”的含义。
第三,从梯度流的角度看,多门控本质上给每个任务开了一条“专用通道”。任务t的梯度对专家i的贡献是 g_i^t(x) · ∇f_i,如果任务A和任务B对同一个专家的梯度方向严重冲突,由于各自门控不同,至少模型有机会选择把冲突负荷分散到不同专家上,而不是像硬共享那样被迫在同一个共享层里折中。
3. 实操过程:从零搭一个MMOE模型的完整流程
3.1 需求场景评估与方案选型
动手写代码之前,先问三个问题,建议想清楚再开始:
一是任务之间是否存在共享信息空间。比如判断“点击”和“购买”是否共享了“用户对物品感兴趣”这一底层信号,如果完全没有共享,MMOE帮不了你,并联模型或独立模型更合适。
二是任务之间相关性是高还是低,是正相关还是负相关。高相关、正相关场景可以优先考虑硬共享或MMOE;负相关场景MMOE通常好于硬共享,但仍可能有上限,如果试下来冲突依旧严重,可能要考虑PLE这类带专家分区的升级方案。
三是样本量是否支撑得起专家网络。MMOE比单任务多了一组专家和门控参数,参数量大约增加2到3倍(取决于k和专家宽度),如果训练数据只有几万条,可能直接过拟合。
从实际项目经验来看,MMOE最适用的典型场景是:任务数2到6个、任务之间有共享信息但相关性不同、单任务模型单独训练已经出现跷跷板现象、特征体系相对统一。
3.2 一个可落地的MMOE建模块(PyTorch实现)
下面这个实现是某跨平台系统实践中整理出来的简化版,去掉了业务细节,保留了完整的结构。核心是一个MMOEBlock类,它接收底层特征输入,输出每个任务的logits。
import torch import torch.nn as nn import torch.nn.functional as F class Expert(nn.Module): def __init__(self, input_dim, expert_dim, hidden_dims=[128, 128], dropout=0.1): super(Expert, self).__init__() layers = [] prev_dim = input_dim for hdim in hidden_dims: layers.append(nn.Linear(prev_dim, hdim)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout)) prev_dim = hdim self.mlp = nn.Sequential(*layers) self.out_proj = nn.Linear(prev_dim, expert_dim) def forward(self, x): return self.out_proj(self.mlp(x)) class Gate(nn.Module): def __init__(self, input_dim, num_experts, hidden_dim=None, use_softmax=True): super(Gate, self).__init__() hidden_dim = hidden_dim or (input_dim // 2) self.net = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_experts) ) self.use_softmax = use_softmax def forward(self, x): logits = self.net(x) if self.use_softmax: return F.softmax(logits, dim=-1) return logits class MMOEBlock(nn.Module): def __init__(self, input_dim, expert_dim, num_experts, num_tasks, expert_hidden_dims=[128, 128], gate_hidden_dim=None, dropout=0.1): super(MMOEEBlock.__name__, self).__init__() self.num_experts = num_experts self.num_tasks = num_tasks self.experts = nn.ModuleList([ Expert(input_dim, expert_dim, expert_hidden_dims, dropout) for _ in range(num_experts) ]) self.gates = nn.ModuleList([ Gate(input_dim, num_experts, gate_hidden_dim) for _ in range(num_tasks) ]) self.towers = nn.ModuleList([ nn.Sequential( nn.Linear(expert_dim, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, 1) ) for _ in range(num_tasks) ]) def forward(self, x): # 专家输出: [batch, num_experts, expert_dim] expert_outputs = torch.stack([e(x) for e in self.experts], dim=1) outputs = [] for t in range(self.num_tasks): gate_weights = self.gates[t](x) # [batch, num_experts] # 加权求和: [batch, expert_dim] weighted = torch.einsum('be,bed->ed', gate_weights, expert_outputs) task_out = self.towers[t](weighted) outputs.append(task_out) return outputs几个关键的实现细节要特别留意:
- 专家网络的输出维度expert_dim决定了后续塔的输入维度,一般设在64到256之间,不建议太大。所有专家的输出维度必须保持一致,否则无法做加权求和。
- 门控网络隐藏层的维度不需要很大,一般可以是输入维度的一半或者更小,过大的门控不仅增加参数量,还容易过拟合。
- 上面对任务塔的输出都是单值logit,训练时按各自任务搭配BCELoss或CrossEntropyLoss。
- 专家和门控的初始权重推荐用Xavier或Kaiming初始化,如果默认初始化太激进,会导致某一两个专家在训练初期被所有门控选中,后面很难翻转。
3.3 数据组织、损失函数与训练调参要点
数据组织上,多任务学习最核心的训练样本组织方式是“样本共享”。一条样本如果同时有点击标签和购买标签,就让这条样本同时参与两个任务的loss计算。但要注意处理缺失标签:比如一条样本只有曝光没有点击行为,这时候点击标签可以置0,但购买标签往往是缺失的,不能直接置0(这会造成假负样本)。常见做法是用一个task-specific mask,只在某个任务标签有效时把该任务的loss计入总loss。
损失函数方面,直接对所有任务的loss做简单求和通常是次优的。因为不同任务的loss量纲不同,点击率任务的loss可能在0.3到0.6之间,而某些回归任务如观看时长损失可能在几十上百,简单求和会让大loss任务主导模型训练。我实测下来比较好用的是不确定性加权(Uncertainty Weighting),公式即常见的形式:Loss = 1/(2σ_t²) · L_t + log σ_t,把每个任务的噪声方差作为一个可学习参数,自动平衡各任务的权重。也有团队喜欢用GradNorm,在训练过程中按梯度范数动态调整任务权重,效果也不错,但实现复杂度高一些且不太稳定,我本人更推荐uncertainty weighting做首选。
训练调参有以下几条实操建议:
- 学习率:多任务模型的训练稳定性比单任务敏感,学习率建议从3e-4起步,比单任务常用的1e-3要低。
- Batch size:由于多任务共享专家,batch size过小会导致门控梯度方差大,门控权重频繁震荡,建议batch size至少256。
- 专家数量:2到4个专家在大多数场景下够用。业内偶尔有超过8个专家的做法,但除非任务数量已经超过5个且专家分化需求明显,否则专家数量增加带来的收益会明显边际递减。
- 优化器:AdamW稳定且收敛好,建议直接采用,权重衰减设1e-6到1e-5之间,不要设太大,因为这可能导致专家网络被过度约束而难以分化。
4. 工程细节解析:从离线实验到上线推理
MMOE模型离线训练好之后,真正落地还要解决两个核心问题:离线评估怎么做才靠谱,线上推理如何控制成本。
离线评估是多任务模型的一个大坑。如果用各任务单独的AUC来评估,你会发现即使MMOE训练正常,有些任务的离线AUC可能比对应的单任务模型还低一点点。这是因为多任务模型的共享表示牺牲了部分单任务特长,换取整体的样本效率和泛化性。更合理的评估方式是综合看所有任务AUC的均值,同时观察任务的相对提升。推荐做法:
一是计算所有任务AUC的均值,再看每个任务相对单任务模型的变化幅度。如果所有任务都持平或微涨,说明共享有效且没有引入太多冲突;如果部分涨部分跌,可以看整体加权业务指标。
二是用GAUC(Group AUC)评估排序质量,按用户维度做分组,避免样本间非独立性问题干扰结论。
三是看门控权重的分布特征。经验上,一个健康的MMOE模型,每个任务的门控输出权重分布在训练后应该是有差异的,而且不应该是某个专家在所有任务上权重都很低。如果出现某个专家对所有任务的权重都接近0,说明专家存在冗余或者模型容量过大,要适当减少专家数量。
线上推理的成本问题也不能忽视。MMOE的专家网络是共享计算,输入特征只需要过一遍专家层,然后每个任务过一遍自己的门控和塔网络。计算量主要增加在门控网络本身——因为每个任务都要单独算一次门控的MLP,然后做k次加权求和。k不大时这个增加可以忽略,但如果任务数t较大、专家数k也较大,门控层更多,要做一次线性变换加Softmax,会累计可观的计算开销。实际工程上如果任务数超过5个且每个门控都有隐层,可以考虑把门控隐层去掉,直接输入线性层得到logits,推理速度会快很多,在多数场景下精度损失不大。
另一个上线前必须处理的问题:多任务产出得分如何进行线上融合。MMOE输出的是多个任务的预测分,不是最终排序分。融合方式有加权乘积、加权对数、或者套一层LTR(Learning to Rank)模型去学融合权重。我见过不少团队在这步翻车:离线评估时用的是MMOE输出分数,线上融合时却随便试了几个固定权重组合,导致离线有提升但线上没提升甚至负向。正确做法是定义好业务指标(比如有效播放时长、GMV等),再在MMOE输出的多个分数之上做小规模搜索或简单超参调优,找到最优融合策略。这步优化空间往往不亚于模型结构本身的收益。
5. 复盘与进阶:MMOE的边界、升级方向和我的经验
先说一个比较现实的结论:MMOE不是万能药。它的边界条件主要在于无法彻底解决任务之间无法共享的部分带来的冲突。如果两个任务的输入特征空间明显不同,或者目标本身在业务上就是矛盾的,多门控只是缓解了冲突,并没有消除它。当测试集上发现任务A涨了、任务B一直跌,把专家数调到8个、门控隐层加到两层也救不回来时,基本不是调参问题,而是结构问题。
这时可以考虑两个升级方向。一个是PLE(渐进式分层抽取),它对专家做分区,一部分专家仅被特定任务使用,一部分专家被所有任务共享,相当于把“任务专属专家”和“共享专家”显式分开,更彻底地隔离任务冲突,同时对任务相关性建模也更精细。另一个方向是引入序列模型或Transformer结构替代底层MLP作为专家网络,适合输入本身带有强序列特征的场景,但参数量和训练成本会明显上升,需要谨慎平衡。
再分享两个我自己踩过的坑。第一次做MMOE时,测试集上两个任务AUC都涨了,但线上效果反而下滑。排查下来发现是融合权重有问题——离线评估的时候我把两个任务的输出单独评估,忽视了业务方实际是把它们按某个比例相加后用于排序的。后来我把评估整个调整成先融合再算排序指标,才真正对齐了线上和离线。另一次是在一个负相关较强的任务对上强行上MMOE,调了各类超参数和损失权重,最终AUC还是比两个单独的模型低了,后来换思路做PLE才解决问题。这个经历让我学会了一件事:先做充分的冲突分析,再决定要不要上MMOE,不要盲目追求结构先进。
MMOE这套建模思路真正落地时,训练指标只是一个中间结果,能不能给业务带来可量化的提升,才是一个多任务模型从“算法项目”变成“业务项目”的分水岭。如果你们团队正准备在多目标场景里引入MMOE,我的建议是:先小规模实验,对比单任务模型,把每个任务的提升和损耗都量化出来,再决定是否全面铺开——这个过程本身比任何爽文式技术结论都更有参考价值。希望这篇整理能帮你少踩几个坑。