尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

HoME多任务学习架构:层级多门专家如何解决MMoE的任务干扰

HoME多任务学习架构:层级多门专家如何解决MMoE的任务干扰 我先抛一个做多任务建模时遇到过的真实场景推荐系统里同时要预测点击率、完播率和有效播放时长最初用共享底层加三个任务塔离线AUC勉强能看上线之后点击率指标还凑合完播率却被拉得很难看。后来换到MMoE整体收益明显但仔细观察又发现三个门控网络学出来的行为几乎一样等于白做了一个专家路由。后面读到一篇论文标题是“HoMEHierarchy of Multi-Gate Experts for Multi-Task Learning”核心思路正好戳中这个痛点用层级化多门专家结构去处理多任务学习里的共享信息和任务特有信息的纠缠问题。下面我把这篇论文拆开讲清楚它的架构逻辑、复现要点、训练过程中最容易翻车的几个坑以及工程落地时该怎么做取舍适合正在做多任务建模、对MMoE体系有一定了解的算法工程师和数据挖掘从业者。1. 多任务学习为什么会“任务打架”——MMoE的边界在哪里多任务学习听起来很美好一个模型同时做几件事参数共享数据利用率高。但真正上线过的人都知道多任务建模最让人头疼的不是模型太复杂而是任务之间会互相“打架”。为了理解HoME要解决什么问题得先把MMoE这套体系的边界画清楚。1.1 Shared-Bottom与任务塔用最朴素的方式共享参数大部分团队做多目标优化的起点都是Shared-Bottom架构一个共享底层网络抽取通用特征上面接若干任务塔每个塔负责一个目标。这个结构简单到可以十分钟写好跑起来但它有一个很本质的问题共享底层学出来的表征是一个“折中解”。举个推荐场景的例子。点击率预测需要捕捉用户“会不会好奇点进来”的特征完播率预测需要捕捉“用户能不能看完”的特征。好奇和看完虽然相关但信号完全不同。共享底层在拟合这两个任务时梯度方向经常不一致最后学出来的表征往往是两个目标互相妥协的产物我见过不少项目在这里调了很久的塔容量和loss权重效果还是不稳定。Shared-Bottom不是不能用它适合任务相关性高、数据量不大的场景。但任务一多、目标差异一大底层表征就会被拉成四不像。1.2 MMoE给了每个任务一条动态选择专家的路MMoEMulti-gate Mixture-of-Experts是2018年KDD上提的方案思路很直接把共享底层拆成若干专家网络每个任务塔前挂一个独立的门控网络门控对专家输出做加权求和再喂给任务塔。用公式表达就是[ y_k h_k\left(\sum_{i1}^{n} g_k(x)_i \cdot f_i(x)\right) ]其中 (f_i) 是第 (i) 个专家(g_k) 是第 (k) 个任务的门控输出一个在专家上的softmax权重分布(h_k) 是任务塔。这比Shared-Bottom聪明的地方在于每个任务有自己独立的门控理论上可以“选择”更有利于自己目标的那部分专家。比如点击率任务可能主要用专家1、专家3完播率任务重点用专家2、专家5大家各取所需不必再强行共享一个折中表征。MMoE实现也不难专家模块就是几个MLP门控网络就是一层线性加softmax。很多团队把线上模型从Shared-Bottom升级到MMoE之后多任务指标都有实打实的提升这也是它成为多任务学习事实baseline的原因。1.3 MMoE没解决的三个问题但MMoE并不是终点它身上有三块短板恰好是HoME这篇论文想重点补的。第一个问题是专家不分层。所有专家都处于同一个抽象层级输入直接进专家输出直接被门控组合。可多任务学习里的知识本身是有层级性的有些特征是通用的“低级模式”比如用户历史行为里的时间衰减规律有些特征是任务相关的“高级语义”比如“点击意愿”和“完播意愿”各自的细粒度表达。扁平专家结构没有办法表达这种抽象度差异所有知识被塞在同一层专家里。第二个问题是门控只依赖原始输入 (x)。MMoE的门控网络输入是原始特征它做路由决策时看不到上一层专家已经提炼过的信息。相当于点菜时只看菜单不尝菜决策依据非常有限。在任务差异大的情况下门控学出来的路由策略会趋同最后变成“每个任务都用同一批专家”退化效果和Shared-Bottom差不多。第三个问题是专家数量上去之后门控容易坍缩。softmax门控在训练后期往往退化成近似one-hot全部权重压在同一个专家上其余专家收不到梯度整个模型退化成一个普通MLPMMoE的“专家混合”设计等于白搭。这三个问题合在一起指向一个明确方向如果专家本身可以分层每层门控又能感知更高阶的信息任务之间的共享和特化是不是能切得更干净HoME做的就是这件事。2. HoME架构拆解层级专家、门控路径与信息精炼HoME的全称是Hierarchy of Multi-Gate Experts直译过来就是“多门专家的层级结构”。核心思想可以概括成一句话把专家组织成多个层级每一层都配备专用的门控网络输入特征从通用层开始逐层被提炼、筛选最终在不同任务塔之前形成任务特化的表征。2.1 从“扁平专家”到“专家流水线”理解HoME最快的方式是把MMoE的专家想象成一群工人他们水平差不多都直接做最终装配门控就是个工头分配任务。而HoME把工人分成了几个车间第一个车间做原材料粗加工提炼通用信息第二个车间把粗加工的结果再细化第三个车间直接对接任务需求输出高度特化的特征。数据从流水线上流过去每个车间都有独立的门控来决定哪些加工路径最合适。放到模型结构上每一层都有自己的专家集合和门控集合。低层专家负责提取跨任务共享的通用模式比如统计特征交互、序列行为建模这类基础信息高层专家则逐步向任务语义靠拢输出离目标任务更近的特征。输入 (x) 经过第一层门控加权组合专家输出得到第一层表征 (z_1)(z_1) 再作为下一层的输入经过第二层门控和专家加工得到 (z_2)。经过若干层这一步一步的精炼最终表征才进入各任务塔。这里有个和MMoE很关键的区别MMoE的专家输入都是 (x)输出直接拼给任务塔HoME中每一层专家吃的是上一层已经“加工过”的信息抽象程度逐层递增。低层抓通用模式高层抓任务模式分工明确了任务打架的空间自然就小。2.2 门控网络在层级结构里到底学什么在HoME里门控不再只是“挑几个专家”这么简单。每一层的门控输入通常是原始特征 (x) 与上一层输出的拼接而不是只吃 (x)。这样做的好处是门控在做路由决策时能看到当前已经提炼到哪一步了从而动态决定下一步该往通用方向走还是往任务特化方向走。单层的计算可以写成[ z_l \sum_{i1}^{n} g_{k,l}\left(x, z_{l-1}\right)i \cdot f{l,i}\left(z_{l-1}\right) ](f_{l,i}) 是第 (l) 层的第 (i) 个专家(g_{k,l}) 是第 (k) 个任务在第 (l) 层的门控。注意这里的门控下标带 (k)意味着不同任务在每一层都有自己的路由策略。我读这篇论文时的理解是HoME的门控实际上在同时做三件事选专家、调抽象度、动态路由。选专家好理解就是softmax加权调抽象度是层级结构自然带来的能力——低层门控更倾向于均匀使用专家以保留信息高层门控更倾向于集中选择适合当前任务的专家这种倾向不是手工设计的而是任务梯度训练出来的动态路由则体现在门控输入同时包含原始特征和中间表征特征变化时路由策略也跟着变。这种设计在业务上的价值很直观用户刚打开App时点击率和完播率都依赖用户长期兴趣特征低层门控会倾向通用专家随着行为序列变长、实时兴趣信号增强完播率门控会越来越偏好那些捕捉“沉浸度”的高层专家两个任务开始“分道扬镳”。2.3 与PLE、CGC这些同期结构的横向对比说HoME之前得先提同期另外两个常见方案避免混淆。PLEProgressive Layered Extraction也是做分层多任务建模的CGCCustomized Gate Control可以理解为PLE的一个单层简化版本。我把这几个结构放在一起对比一下结构专家组织方式门控机制主要特点MMoE扁平专家不分组每个任务一个门控只吃原始输入实现简单效果稳定但专家无分工CGC任务专属专家共享专家每个任务一个门控吃固定拼接输入显式切分共享与专属知识结构简单PLE分层专家每层含共享组和任务专属组每层每个任务一个门控显式建模渐进式信息提取参数多训练慢HoME多层级专家不显式分组每层每个任务一个门控输入含上层输出靠层级路由自动分离共享与特化信息结构更灵活PLE和CGC走的是“按任务分组”的路线把专家显式分成共享专家组和任务专属专家组通过定制门控控制信息流向。这种方式思路清晰但存在一个隐藏问题任务相关性差异较大时任务专属专家和共享专家之间的边界是手工画的不一定贴合真实数据分布。HoME走的是另一条路线不显式分组完全靠层级结构和门控去学。低层专家自然倾向共享高层专家自然倾向特化分工是通过端到端训练“长”出来的而不是人肉指定的。这个差异听起来微妙但在特征空间比较复杂、任务关系不是简单“共享/专属”二分法的业务场景里HoME的灵活性明显更占优势。3. 从论文到代码一个可复现的HoME实现思路论文读完肯定要动手验证。这一章我按自己复现时的思路把HoME的核心实现拆出来讲代码风格尽量保持精简方便你在此基础上改业务结构。3.1 先用PyTorch把核心模块搭出来HoME可以拆成三个基础组件专家模块、门控模块、层级堆叠模块。专家模块没什么特别的就是一个带激活的MLP负责把输入特征映射到隐空间。门控模块稍微讲究一点结构上是一个输入拼接后过MLP加softmax的模块我建议把温度参数也放在里面方便后面做坍缩控制。import torch import torch.nn as nn import torch.nn.functional as F class Expert(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout0.1): super().__init__() self.mlp nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_dim, out_dim), ) def forward(self, x): return self.mlp(x) class Gate(nn.Module): def __init__(self, in_dim, num_experts, hidden_dim64, temperature1.0): super().__init__() self.temperature temperature self.mlp nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_experts), ) def forward(self, x): logits self.mlp(x) / self.temperature return F.softmax(logits, dim-1)层级堆叠模块是核心负责把多层专家和门控串起来。我这里做了一个细节设计第一层门控只吃原始输入 (x)从第二层开始门控输入拼接原始特征和上一层输出 (z_{l-1})。这样门控能感知当前信息提炼程度。class HoMEStack(nn.Module): def __init__(self, in_dim, hidden_dim, num_levels, num_experts): super().__init__() self.num_levels num_levels self.experts nn.ModuleList() self.gates nn.ModuleList() for l in range(num_levels): expert_in_dim hidden_dim if l 0 else in_dim self.experts.append(nn.ModuleList([ Expert(expert_in_dim, hidden_dim, hidden_dim) for _ in range(num_experts) ])) gate_in_dim in_dim hidden_dim if l 0 else in_dim self.gates.append(Gate(gate_in_dim, num_experts)) def forward(self, x): z x for l in range(self.num_levels): expert_out torch.stack([e(z) for e in self.experts[l]], dim1) gate_input torch.cat([x, z], dim-1) if l 0 else x weights self.gates[l](gate_input).unsqueeze(1) z torch.bmm(weights, expert_out).squeeze(1) return z最后把HoMEStack放进一个完整的多任务模型里每个任务塔接一个自己的输出头即可。需要注意多个任务的塔建议用一层或两层MLP就够了因为HoMEStack已经做了大量特征精炼任务塔太深反而容易过拟合。3.2 层级深度、专家数量与维度配置纸上谈兵没用参数怎么设还得看数据。我按自己复现和业务测试的经验给出一套默认参考配置你可以当成起点不用太纠结最优值超参数建议范围说明层级数2~3层3层以上收益衰减明显训练时间翻倍每层专家数8~16个数据量小就用8样本足够大可以用16专家隐层维度256~512和原始特征维度有关特征维度高则相应放大门控隐层维度64~128门控不需要太大重点是路由能力门控初始温度1.0训练中可以逐步降到0.1~0.5Dropout0.1~0.3专家输出加dropout防止过拟合层级数是最值得细调的参数。我的经验是两层结构往往性价比最高第一层保留通用信息第二层做任务特化。三层结构适合任务数量多、目标形态差异大、数据规模也够大的场景比如同时做点击率、播放时长、点赞率、关注率预测这类情况。专家数量也值得多说一句。专家不是越多越好因为每多一个专家参数和显存开销都线性上涨而且专家数量超过一定值后门控坍缩概率明显变大。原因也不难理解路由网络要从几十个候选中稳定选出合适的几个需要更强的训练信号小样本下很容易选错。3.3 在公开数据集上快速验证方案复现HoME别一上来就接业务数据先用公开数据集做对照实验最稳妥。我常用的验证集合是Ali-CCP阿里巴巴的点击转化数据集天然适合做多任务实验主任务是点击率预测辅助任务是转化率预测样本量在千万级别特征偏用户行为统计结构化的特征处理起来也快。另一个选择是Criteo虽然它是纯点击率CTR数据集但可以考虑人工构造辅助任务比如“点击后是否快速产生后续行为”不过构造出来的任务相关性比较难控制不如Ali-CCP自然。实验设计上至少要比三个模型MMoE作为baselinePLE作为同期对比HoME作为待验证模型。统一用相同特征、相同数据切分训练步数和loss权重保持一致。我复现时遇到过一个问题三个模型用同一套超参数跑PLE和HoME因为结构更深训练速度明显变慢需要多跑几十个epoch才能收敛。对比离线AUC时记得看每个任务的单独AUC不要只看加权平均。如果HoME相比MMoE在至少一个任务上有稳定提升、且其他任务不掉点基本可以说明层级结构在当前数据上有效。要是两个方案表现打平我的建议是优先选MMoE毕竟参数少、调参成本低、上线风险小。HoME不是万能药后面我会细说它真正适用的条件。4. 训练过程里的常见大坑门控坍缩、loss失衡与共享知识判定复现HoME的过程中真正耗时间的不是写模型而是处理训练时的各种意外状况。下面这三个问题我做多任务建模时几乎每次都会遇到值得专门列一章讲清楚。4.1 门控坍缩softmax退化成近似one-hot现象非常典型训练到中途门控网络输出的权重分布开始往某个专家身上集中最后变成近似one-hot权重和1相差无几。结果就是“专家混合”名存实亡整个模型退化成普通MLPHoME的层级设计完全失去意义。为什么会出现坍缩因为softmax门控在训练中很容易形成“赢家通吃”的局面某个专家在早期偶然被选中它收到的梯度足够多学得比别的专家快后续门控越发倾向于选它形成正反馈。尤其在任务梯度和方向比较一致时各个任务的门控会被同时推向同一个专家整体坍缩更严重。我常用的几个解法按有效性排序温度调节。训练早期温度设为1.0甚至更高让softmax的分布更平滑等模型稳定之后再逐步降温到0.5左右。温度越大门控分布越均匀每个专家都能收到梯度。门控初始化偏向均匀分布。把门控最后的线性层bias初始化为0logits就接近均匀而不是随机给某个专家偏高初始化值。熵正则。在训练loss上对门控输出分布加一个熵惩罚项鼓励门控保持一定的不确定性。这个办法效果直接但注意正则系数不能太大否则门控失去选择性。给专家反向传播做梯度裁剪或dropout。降低专家之间的“军备竞赛”速度也能间接抑制坍缩。我的实操习惯是温度调节加熵正则一起做先保证训练初期所有专家都在更新再逐步放开让门控自己做选择。4.2 多任务loss尺度不一致梯度容易被大loss任务带跑HoME和多任务学习的通病一样不同任务的loss尺度可能差出几十倍。点击率用二分类交叉熵loss通常在0.3~0.7之间完播率如果做成回归用MSEloss轻松到个位数甚至两位数。合并训练时大loss任务会主导回传梯度小loss任务对应的门控和专家几乎学不到东西。处理尺度不一致业内常用的有三类方案Uncertainty weighting在任务塔后面加一个可学习的噪声参数用最大似然准则动态调整各任务权重。这是很经典的方案优点是实现简单效果稳定。GradNorm根据各任务梯度范数动态分配loss权重直接调控梯度尺度。效果上限更高但调参麻烦对学习率敏感。最朴素但最实用的一招把回归任务转换成排序学习任务统一用二分类交叉熵。比如播放时长不直接预测数值而是预测“时长是否超过某分位数”这样所有任务的loss尺度天然一致。我自己的做法是先试uncertainty weighting如果任务之间仍然是跷跷板再考虑把回归loss换成交叉熵。霍ME结构越深任务梯度交互就越复杂loss尺度不统一的影响会被放大这条一定要重视。4.3 如何判定共享知识真的被学到了模型AUC涨了不代表层级结构真的在起作用。很多情况下指标提升纯粹是因为参数变多了。判断HoME有没有学到真正意义上的层级共享知识我一般看三个信号。第一个信号是不同任务门控权重的分布差异。理想状态下低层门控在不同任务之间差异不大甚至接近均匀分布说明底层在共享通用信息高层门控的任务间差异应该明显拉大每个任务有自己偏好的专家子集。如果高层门控分布和低层长得一模一样说明层级特化没有学出来模型只是在堆参数。可以在训练过程中周期打印或可视化每层门控的权重分布来做这个判断。第二个信号是消融实验。把高层专家替换成低层专家的复制品或者直接抹掉某一层观察各任务指标变化幅度。如果去掉最高层后任务特有指标掉得厉害但通用指标几乎不受影响说明特化层真的在起作用。第三个信号是中间层表征的t-SNE。把不同任务的样本分别过HoMEStack各层查看输出表征的聚类情况。低层表征不同任务样本应该混杂在一起高层表征应该逐步出现任务聚类分离的迹象这能直观验证“逐层特化”是否符合预期。我在项目里还有一个更实用的土办法把专家输出直接可视化看看不同专家是否接收了不同类型的输入样本。如果一个专家在低层接收各种样本另一个专家在高层只接收完播率高的样本那说明专家确实形成了分工。这个验证方法不用额外写复杂代码只需要给中间层输出打log就行。5. 工程视角的取舍HoME适合什么样的业务场景模型效果好不等于可以无脑上线。HoME的结构比MMoE深参数和计算量都上去了工程落地前需要想清楚它到底值不值得。5.1 三个判断维度数据量、任务数、任务相关性从我的实践来看HoME是否比MMoE更值得选主要看三点数据量是本最要紧。HoME的参数量是MMoE的数倍如果没有足够多的训练样本层级结构学不出来就会过拟合。我的经验阈值是千万级样本起步百亿级特征更稳数据量小于这个规模时我建议先老老实实跑MMoE。任务数决定收益幅度。两个任务时HoME相对MMoE的收益往往有限三个以上任务、且目标形态差异较大时层级结构才有足够的发挥空间。任务越多任务间的关系越复杂显式分层带来的收益就越明显。任务相关性决定方向。任务完全独立硬要做多任务共享没有意义任务高度一致叠这么多层纯属浪费。HoME最占优势的区域是任务有共享信息、但又不完全一致的重叠地带比如“点击率完播率时长”这种组合既有交叉信号又有各自特有信号。5.2 线上推理成本控制与结构简化HoME的推理开销主要来自专家层级。一次推理要过“层级数×每层专家数”个MLP这比MMoE贵得多。线上延迟敏感的团队要注意几个可落地的优化手段。第一个是限制低层专家规模。低层专家学的是通用信息网络宽度可以适当缩小比如隐层维度从512降到256参数量直接少一半。高层专家因为是任务特化保留稍大容量就够了但也不用所有层一样大。第二个是门控结果缓存。如果线上特征短期变化不大可以对部分门控做近似缓存减少重复计算。注意门控输入含有原始特征缓存时要评估特征新鲜度的影响不能无脑缓存。第三个是蒸馏落地。用训练好的HoME模型作为teacher蒸馏一个结构更小的student模型上线这个方案在很多团队验证过效果不错。相比直接对HoME做剪枝蒸馏的工程风险更小可维护性也更好。如果线上基础设施比较薄弱我更推荐CGC或PLE它们结构直观、推理成本低、调参空间也明确。HoME更适合有充足数据和在线推理优化能力的团队去尝试。5.3 个人实践体会与复现建议最后写点我个人的经验。第一次复现HoME时我犯过一个很低级的错误门控的输入拼接顺序在离线训练和线上推理时不统一导致离线指标和线上效果差了很大一截排查了大半天。后来我把特征拼接顺序写进配置文件所有环节强制复用同一份配置这个问题才彻底解决。这种一眼看过去不是事儿的细节在结构比较复杂的模型里会被放大提前规避比事后排查划算得多。训练层面我的建议是先跑MMoE做基线把特征、loss权重、学习率都调到相对理想的状态再在这个基础上替换HoMEStack。这样能避免“模型结构变复杂了、特征却还在拖后腿”的混淆收益确认之后再对层级数和专家数做一次小规模网格搜索。别指望一次训练就能看到显著提升HoME的收益往往要在多任务指标的整体曲线上观察单看某一个任务AUC容易产生误判。我目前在业务线上最终保留的是两层HoME配置底层8个专家、隐层256维上层12个专家、隐层256维门控温度从1.0逐步降到0.3配合uncertainty weighting合并三个任务的loss。整体比MMoE在完播率和时长两个任务上稳定涨点点击率基本持平线上推理增加了大约1.6倍的耗时通过把低层专家替换成低秩结构压回了可接受范围。LoME这个方向其实还有很多可玩的空间比如把门控输入换成目标任务的embedding、或者把专家的激活函数换成更复杂的表达形式。有兴趣的同学可以在我的实现基础上继续扩展但核心一句话HoME的价值不在于结构花哨而在于它对“共享和特化”这件事做了更精细的切分。如果你的业务数据能支撑起这个复杂度值得一试如果撑不起别硬上结构简单一点可能反而跑得更稳。我最近在不少分享里看到有人问“为什么我复现HoME效果不如MMoE”大部分时候先检查自己的数据量再检查门控是否坍缩这两个问题排查完结论往往会清晰很多。
返回列表