1. 多任务学习入门:从单任务到多任务的跃迁
第一次接触多任务学习(MTL)时,我正被公司要求同时优化推荐系统的点击率和停留时长两个指标。当时傻乎乎地训练了两个独立模型,结果线上部署时发现资源消耗翻倍,两个模型的预测结果还经常打架。直到同事扔给我一篇MTL论文,才恍然大悟:原来一个模型可以同时搞定多个任务!
多任务学习的本质就像让一个学生同时学习数学和语文。传统单任务学习是培养"偏科生",而MTL要培养"全能型选手"。在实际工业场景中,这种"全能模型"的优势非常明显:
- 部署成本低:一个模型服务替代多个独立模型
- 资源共享:底层特征表示可以在任务间共享
- 泛化更强:任务间的相关性起到正则化作用
但MTL最让人头疼的就是损失加权问题。就像老师要给不同科目分配课时一样,我们需要决定每个任务在总损失中的比重。早期我试过最简单的加权平均法:
# 手工加权示例 total_loss = 0.3 * loss1 + 0.7 * loss2这种粗暴方法很快就让我栽了跟头——权重稍微变化0.1,线上指标就能波动5%。后来才发现,优秀的MTL实现需要更精细的加权策略,这正是本文要重点探讨的内容。
2. 手工加权:简单但危险的起点
2.1 基础加权方法
手工加权就像给多个任务分配固定比例的资源。假设我们要同时优化分类准确率和回归误差:
def manual_weighted_loss(loss1, loss2): return alpha * loss1 + (1-alpha) * loss2这里的alpha就是需要人工调整的超参数。我在电商搜索业务中实践时,发现这种方法的痛点非常明显:
- 敏感度过高:当alpha从0.4调整到0.5时,AUC可能提升2%,但MAE会恶化15%
- 任务量纲差异:分类loss通常在0-1之间,而回归loss可能达到几十
- 动态适应性差:不同训练阶段任务难度会变化
2.2 改进方案:标准化加权
后来我采用了一种改进方案——先对各个loss进行标准化处理:
# 对loss进行标准化 normalized_loss1 = loss1 / loss1.detach() normalized_loss2 = loss2 / loss2.detach() total_loss = w1 * normalized_loss1 + w2 * normalized_loss2这种方法确实缓解了量纲问题,但依然需要大量实验来确定最佳权重。在推荐系统场景下,我们通常要跑数十组AB测试才能找到相对合理的权重组合。
提示:手工加权适合任务间关系稳定且量级相近的场景,比如同时预测用户年龄和性别。对于差异大的任务,建议考虑动态加权方法。
3. 动态加权平均(DWA):让任务平衡学习
3.1 DWA算法原理
Dynamic Weight Averaging的核心思想很直观——根据任务的学习速度动态调整权重。就像老师会根据学生各科进步速度调整教学重点:
- 计算各任务loss在相邻epoch的变化率
- 对变化率进行softmax归一化
- 用归一化结果作为当前epoch的权重
具体实现可以参考这个PyTorch示例:
class DWA(nn.Module): def __init__(self, num_tasks, temp=2.0): super().__init__() self.temp = temp self.register_buffer('prev_loss', torch.zeros(num_tasks)) def forward(self, losses): if self.prev_loss.sum() == 0: # 第一轮平均加权 return torch.softmax(torch.ones_like(losses), dim=0) loss_ratio = losses / self.prev_loss weights = torch.softmax(loss_ratio / self.temp, dim=0) self.prev_loss = losses.detach() return weights3.2 实战经验与调参技巧
在商品多属性预测任务中,DWA表现出色但需要注意:
- 温度系数temp:控制权重分布平滑度,通常1.0-3.0之间
- 初始阶段稳定:前几个epoch建议使用固定权重
- 异常值处理:单个epoch的剧烈波动需要平滑处理
实测发现,对于价格预测+销量预测的双任务场景,DWA相比手工加权能使模型收敛速度提升30%,最终指标也更加均衡。
4. 不确定性加权:更科学的自适应方法
4.1 不确定性理论基础
不确定性加权方法源自论文《Multi-task learning using uncertainty to weigh losses》,它将任务权重与不确定性建模相结合。这里需要区分两种不确定性:
- 认知不确定性:数据不足导致,可通过增加数据缓解
- 偶然不确定性:数据本身噪声导致,与数据量无关
该方法主要针对第二种不确定性中的同方差情况。推导后的loss函数形式非常优雅:
L = \sum_i \frac{1}{2\sigma_i^2}L_i + \log\sigma_i其中σ是任务相关的不确定性参数,会被自动学习。
4.2 代码实现详解
以下是完整的AutomaticWeightedLoss实现,我在原基础上添加了数值稳定处理:
class RobustAutomaticWeightedLoss(nn.Module): def __init__(self, num_tasks, eps=1e-6): super().__init__() self.params = nn.Parameter(torch.ones(num_tasks)) self.eps = eps def forward(self, *losses): total_loss = 0 for i, loss in enumerate(losses): sigma = torch.clamp(self.params[i], min=self.eps) total_loss += 0.5/(sigma**2)*loss + torch.log(1 + sigma**2) return total_loss使用时需要特别注意优化器配置:
model = MultiTaskModel() awl = RobustAutomaticWeightedLoss(2) # 关键:awl参数需要单独配置优化器 optimizer = torch.optim.Adam([ {'params': model.parameters()}, {'params': awl.parameters(), 'weight_decay': 0} # 禁止权重衰减 ], lr=1e-3)4.3 工业场景应用案例
在视频推荐系统中,我们同时优化:
- 点击率预测(分类任务)
- 观看时长预测(回归任务)
使用不确定性加权后,模型自动学习到两个任务的σ值分别为0.8和1.2,这与我们手动分析的任务噪声水平一致。最终线上AB测试显示,相比DWA方法,不确定性加权使时长预测的MAE降低了12%,而点击率保持稳定。
5. 进阶技巧与避坑指南
5.1 多任务架构设计
除了损失加权,网络结构设计同样重要。分享几个实用技巧:
- 硬参数共享:底层共享,顶层独立(适合相关任务)
- 软参数共享:各任务有独立参数但保持相似(适合差异任务)
- 任务门控:学习任务特定的特征组合方式
# 门控共享示例 class TaskGate(nn.Module): def __init__(self, input_dim, num_tasks): super().__init__() self.gates = nn.ModuleList([ nn.Linear(input_dim, input_dim) for _ in range(num_tasks) ]) def forward(self, x, task_id): return x * torch.sigmoid(self.gates[task_id](x))5.2 常见问题排查
踩过无数坑后,我总结出MTL调试checklist:
- 梯度检查:各任务梯度量级是否均衡
- 权重监控:动态权重的变化曲线是否合理
- 表征分析:共享层的特征是否被某些任务主导
- 资源分配:显存占用是否超出预期
遇到问题时,可以先用简单的加权方法建立baseline,再逐步引入复杂方法。记住:不是所有场景都需要花哨的加权算法,有时候简单的加权平均配合好的网络结构就能取得不错的效果。