从MMoE到PLE:读懂多任务学习架构的渐进式演化

发布时间:2026/7/25 16:14:06
从MMoE到PLE:读懂多任务学习架构的渐进式演化 从MMoE到PLE读懂多任务学习架构的渐进式演化在推荐系统、广告点击率预测、自然语言处理等领域多任务学习Multi-Task LearningMTL已成为提升模型泛化能力与业务指标的关键技术。从早期的Shared-Bottom模型到谷歌提出的MMoEMulti-gate Mixture-of-Experts再到腾讯提出的PLEProgressive Layered Extraction多任务学习架构经历了一场从“简单共享”到“精细分离”的渐进式演化。本文将从实战角度出发通过代码示例和架构解析带你读懂这一演进路径。## 痛点出发为什么需要多任务学习在真实业务中我们往往需要同时预测多个目标。例如在短视频推荐场景中既要预测用户点击率CTR又要预测用户观看时长Stay Time。传统的单任务模型为每个目标独立建模不仅浪费计算资源还会忽略任务间的关联信息。Shared-Bottom模型是最早的MTL方案多个任务共享底层网络顶层为每个任务单独设置输出层。但这种方式存在一个严重问题当任务之间相关性较弱甚至冲突时例如“点击”和“购买”可能具有不同模式共享表示会限制模型的学习能力导致“负迁移”。## MMoE门控机制下的专家混合### 核心思想MMoEMulti-gate Mixture-of-Experts通过引入多个“专家网络”和“门控网络”让每个任务能够自适应地选择最相关的专家组合。其关键改进在于-专家网络多个并行的前馈网络每个专家负责学习不同的数据模式。-门控网络每个任务拥有一个独立的门控输出对专家的加权组合权重。### 代码实现PyTorch版MMoE下面是一个完整的MMoE实现示例包含数据生成、模型定义和训练循环。pythonimport torchimport torch.nn as nnimport torch.optim as optimimport numpy as np# ---------- MMoE模型定义 ----------class MMoE(nn.Module): def __init__(self, input_dim, num_experts4, experts_hidden64, tasks_outputs[1, 1]): Args: input_dim: 输入特征维度 num_experts: 专家数量 experts_hidden: 每个专家隐藏层大小 tasks_outputs: 每个任务输出维度列表例如[1,1]表示两个二分类任务 super(MMoE, self).__init__() self.num_experts num_experts self.num_tasks len(tasks_outputs) # 定义专家网络每个专家是一个两层的MLP self.experts nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, experts_hidden), nn.ReLU(), nn.Linear(experts_hidden, experts_hidden) ) for _ in range(num_experts) ]) # 定义门控网络每个任务对应一个门控输出权重维度等于专家数量 self.gates nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, num_experts), nn.Softmax(dim1) # 对每个样本的专家权重做归一化 ) for _ in range(self.num_tasks) ]) # 定义任务塔每个任务独立的输出层 self.towers nn.ModuleList([ nn.Sequential( nn.Linear(experts_hidden, 16), nn.ReLU(), nn.Linear(16, tasks_outputs[i]) ) for i in range(self.num_tasks) ]) def forward(self, x): # Step1: 计算所有专家的输出 [batch, num_experts, experts_hidden] expert_outputs [expert(x) for expert in self.experts] # 每个元素形状 [batch, experts_hidden] expert_outputs torch.stack(expert_outputs, dim1) # [batch, num_experts, experts_hidden] # Step2: 为每个任务计算门控权重 task_outputs [] for i in range(self.num_tasks): gate_weights self.gates[i](x) # [batch, num_experts] # 加权求和权重与专家输出做点积 weighted_output torch.bmm(gate_weights.unsqueeze(1), expert_outputs) # [batch, 1, experts_hidden] weighted_output weighted_output.squeeze(1) # [batch, experts_hidden] # Step3: 通过任务塔输出 task_out self.towers[i](weighted_output) task_outputs.append(task_out) return task_outputs # 返回列表每个元素为对应任务的输出# ---------- 数据生成与训练 ----------# 生成模拟数据1000个样本10维特征两个二分类任务np.random.seed(42)X np.random.randn(1000, 10).astype(np.float32)y1 (X[:, 0] X[:, 1] 0).astype(np.float32) # 任务1基于前两维特征y2 (X[:, 2] * X[:, 3] 0).astype(np.float32) # 任务2基于中间两维特征# 转换为TensorX_tensor torch.from_numpy(X)y1_tensor torch.from_numpy(y1).view(-1, 1)y2_tensor torch.from_numpy(y2).view(-1, 1)# 初始化模型、损失函数和优化器model MMoE(input_dim10, num_experts4, experts_hidden64, tasks_outputs[1, 1])criterion nn.BCEWithLogitsLoss() # 二分类交叉熵optimizer optim.Adam(model.parameters(), lr0.001)# 训练循环for epoch in range(50): optimizer.zero_grad() out1, out2 model(X_tensor) loss1 criterion(out1, y1_tensor) loss2 criterion(out2, y2_tensor) total_loss loss1 loss2 # 多任务损失加权求和 total_loss.backward() optimizer.step() if epoch % 10 0: print(fEpoch {epoch}, Loss: {total_loss.item():.4f})print(MMoE训练完成)代码解析- 专家网络通过nn.ModuleList实现并行专家每个专家学习不同特征模式。- 门控网络使用Softmax输出权重让任务能“挑选”专家。- 训练时两个任务的损失直接相加实际项目中可引入动态权重如Uncertainty Weighting。## 从MMoE到PLE解决“跷跷板”问题### MMoE的局限性尽管MMoE比Shared-Bottom灵活但在实际大规模推荐系统中它仍面临一个关键问题任务间的“跷跷板”现象。即优化任务A时任务B的指标会下降。原因在于MMoE的专家是被所有任务共享的当某个任务主导了梯度更新时会迫使专家偏向该任务损害其他任务。### PLE的核心创新PLEProgressive Layered Extraction提出了任务专属网络和共享网络的分离机制1.多层提取网络将网络分为多个“提取层”每个层包含共享专家和任务专属专家。2.渐进式分离低层网络保留更多共享信息高层网络逐渐分离出任务专属特征。3.门控网络升级每个任务的门控可以同时访问共享专家和专属专家。### 代码实现简化版PLE下面实现一个两层的PLE模型包含共享专家和任务专属专家。pythonimport torchimport torch.nn as nnimport torch.optim as optim# ---------- PLE模型定义简化版2个任务2层提取 ----------class PLELayer(nn.Module): def __init__(self, input_dim, num_shared2, num_specific2, experts_hidden64): Args: input_dim: 输入维度 num_shared: 共享专家数量 num_specific: 每个任务的专属专家数量 experts_hidden: 专家输出维度 super(PLELayer, self).__init__() self.num_shared num_shared self.num_specific num_specific # 共享专家 self.shared_experts nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, experts_hidden), nn.ReLU() ) for _ in range(num_shared) ]) # 任务1专属专家 self.specific_experts_1 nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, experts_hidden), nn.ReLU() ) for _ in range(num_specific) ]) # 任务2专属专家 self.specific_experts_2 nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, experts_hidden), nn.ReLU() ) for _ in range(num_specific) ]) # 门控网络每个任务一个输入为当前层输入输出权重覆盖所有专家 total_experts num_shared 2 * num_specific # 共享两个专属 self.gate1 nn.Sequential( nn.Linear(input_dim, total_experts), nn.Softmax(dim1) ) self.gate2 nn.Sequential( nn.Linear(input_dim, total_experts), nn.Softmax(dim1) ) def forward(self, x): # 计算所有专家的输出 shared_outs [expert(x) for expert in self.shared_experts] spec1_outs [expert(x) for expert in self.specific_experts_1] spec2_outs [expert(x) for expert in self.specific_experts_2] # 拼接所有专家输出[batch, total_experts, experts_hidden] all_experts torch.stack(shared_outs spec1_outs spec2_outs, dim1) # 任务1加权 w1 self.gate1(x).unsqueeze(1) # [batch, 1, total_experts] out1 torch.bmm(w1, all_experts).squeeze(1) # [batch, experts_hidden] # 任务2加权 w2 self.gate2(x).unsqueeze(1) out2 torch.bmm(w2, all_experts).squeeze(1) return out1, out2 # 返回每个任务的中间表示class PLE(nn.Module): def __init__(self, input_dim, num_layers2, num_shared2, num_specific2, experts_hidden64, tasks_outputs[1,1]): super(PLE, self).__init__() self.layers nn.ModuleList([ PLELayer(input_dim if i0 else experts_hidden, num_shared, num_specific, experts_hidden) for i in range(num_layers) ]) # 最终任务塔 self.towers nn.ModuleList([ nn.Sequential( nn.Linear(experts_hidden, 16), nn.ReLU(), nn.Linear(16, tasks_outputs[i]) ) for i in range(2) ]) def forward(self, x): h1, h2 x, x # 每个任务的初始表示 for layer in self.layers: h1, h2 layer(h1) # 注意这里简化了实际PLE中任务专属表示只传入对应任务 out1 self.towers[0](h1) out2 self.towers[1](h2) return out1, out2# ---------- 训练PLE模型使用相同数据 ----------model_ple PLE(input_dim10, num_layers2, num_shared2, num_specific2, experts_hidden64)optimizer_ple optim.Adam(model_ple.parameters(), lr0.001)for epoch in range(50): optimizer_ple.zero_grad() out1, out2 model_ple(X_tensor) loss1 criterion(out1, y1_tensor) loss2 criterion(out2, y2_tensor) total_loss loss1 loss2 total_loss.backward() optimizer_ple.step() if epoch % 10 0: print(fPLE Epoch {epoch}, Loss: {total_loss.item():.4f})print(PLE训练完成)关键改进- 每个PLE层同时包含共享专家和任务专属专家门控网络可以访问所有专家。- 通过多层堆叠低层学习通用特征高层逐渐分离任务特有模式。- 实际论文中还引入了“渐进式分离”的损失项强制专属专家只对特定任务贡献。## 架构演进对比总结| 架构 | 共享方式 | 门控机制 | 解决的核心问题 ||------|----------|----------|----------------|| Shared-Bottom | 完全共享 | 无 | 无法处理任务冲突 || MMoE | 专家共享 | 每任务独立门控 | 任务选择性利用专家 || PLE | 共享专属分离 | 多层门控 | 消除负迁移与跷跷板效应 |## 实战建议与总结从MMoE到PLE的演进本质上是**从“完全共享”到“精细分离”**的转变。在实际项目中选择哪种架构需要根据任务相关性和数据规模来决定-任务高度相关如CTR与CVRMMoE通常就足够甚至Shared-Bottom也能表现良好。-任务中度相关但存在冲突建议使用MMoE任务权重调整如动态损失加权。-任务相关性低且冲突严重如点击率与转化率PLE的分离机制能显著提升稳定性和最终指标。总结多任务学习架构的演化反映了深度学习从“一刀切”到“个性化”的思维转变。MMoE通过门控机制让任务“学会选择”PLE则通过分层分离让网络“自觉分工”。理解这些架构背后的设计哲学比单纯背诵公式更重要——当你面对真实业务中的多目标优化问题时才能设计出最适合自己场景的“定制版MTL”。

相关新闻

最新新闻

日新闻

周新闻

月新闻