2. 多任务学习基础:概念、共享表示与任务特定层、与单任务学习对比
多任务学习(Multi-Task Learning, MTL)这个词,说白了就是让一个模型同时学会做几件事。举个例子,你要同时预测一个化合物的 水溶性、毒性 和 口服吸收率。以前咱们是三个模型各干各的,现在呢,一个模型搞定三个任务。
听起来像是“压榨”模型?其实不是。我刚开始接触多任务学习时也觉得有点反直觉——多个任务共享一个模型,会不会“互相拖后腿”?后来在 ADMET 预测项目里踩过坑又爬出来,才明白这件事的妙处。
2.1 多任务学习的核心动机
为什么我们在 ADMET 预测里要用多任务学习?原因有三条:
- 数据少,但任务多:单个 ADMET 属性(比如心脏毒性)的实验数据常常只有几百条。但在多任务框架下,模型可以同时从水溶性、代谢稳定性等任务中“偷学”有用的特征。
- 属性之间有关联:比如亲脂性(logP)会影响渗透性和毒性。这在化学领域是常识。多任务学习可以利用这些共享信息。
- 训练和部署更省事:一个模型加载一次,就能输出所有 ADMET 预测值。我在实际管线里部署过六个单任务模型,那感觉就像同时追六个球——所以后来我坚决改成了多任务模型。
核心好处:多任务学习的本质是一种 归纳迁移(Inductive Transfer)。它通过共享信息来提升每个任务的泛化能力。尤其是在数据稀缺的 ADMET 场景,这一点非常关键。
2.2 共享表示层:模型的“通用大脑”
多任务学习最直观的结构,就是前半部分大家一起用,后半部分各干各的。
共享表示层(Shared Representation)就是一个“通用特征提取器”。无论任务是预测 hERG 毒性还是 Caco-2 渗透性,模型先通过它把分子图或者指纹转化成一组公共特征向量。
我习惯把这一层比喻成“读分子的基础能力”——比如判断原子类型、识别官能团、提取分子拓扑结构。这些能力对所有 ADMET 任务都有用。
实用建议:共享层不宜太浅,也不宜太深。层数太少提取不到有效特征;层数太多,模型容易在多个任务之间摇摆不定(我在一个 8 任务模型里试过 6 层共享全连接,结果收敛极慢)。一般来说,2~3 层 GCN 或 Transformer 编码器是个不错的起点。
# 共享编码器示例(PyTorch 伪代码)
class SharedEncoder(nn.Module):
def __init__(self, input_dim, hidden_dim=128):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
return x # 输出共享特征向量
2.3 任务特定层:各司其职的“专科医生”
共享层之后,每个任务都会接上自己的 任务特定层(Task-Specific Layers)。打个比方,共享层是内科医生的通用诊断能力,而任务特定层就是心脏科和神经科的专科知识。
实际上,这种结构非常灵活。你可以为每个任务分配一个独立的单层线性分类器,也可以为复杂度高的任务分配两层 MLP。我在项目中遇到过这样一个情况:药物-药物相互作用(DDI)预测需要更复杂的决策边界,而 logD(分布系数)只需要一个简单的回归头。那就分别给它们不同的任务特定层深度。
避坑提醒:我曾经在一个多任务模型里,把 8 个任务的任务头都设成了完全相同的结构。结果水溶性任务很快就过拟合了,而代谢稳定性任务还在欠拟合。解决办法很简单——为每个任务设置独立的 正则化强度 或 退出率。
# 任务特定层示例
class TaskHead(nn.Module):
def __init__(self, shared_dim, output_dim, n_layers=1):
super().__init__()
layers = [nn.Linear(shared_dim, 64) if n_layers > 1 else nn.Linear(shared_dim, output_dim)]
if n_layers > 1:
layers.append(nn.ReLU())
layers.append(nn.Linear(64, output_dim))
self.head = nn.Sequential(*layers)
def forward(self, shared_feat):
return self.head(shared_feat)
2.4 与单任务学习的对比:一张表看清区别
单任务学习(Single-Task Learning, STL)就像请三个独立的专家。多任务学习(MTL)则像培养一个全能型选手。它们的核心区别,我用一张表整理一下。
| 维度 | 单任务学习 (STL) | 多任务学习 (MTL) |
|---|---|---|
| 模型数量 | 每项属性一个独立模型 | 一个模型处理所有属性 |
| 训练数据利用率 | 只能使用单任务标签 | 可利用所有任务的标签信息 |
| 表示泛化性 | 特征提取容易瞄准单个任务,导致过拟合 | 共享表示受到多任务约束,泛化性更强 |
| 部署开销 | 需加载 N 个模型,内存大、推理慢 | 一次加载,一次前向传播,效率高 |
| 数据不平衡风险 | 数据少的任务训练效果差 | 多任务可互相补充,但可能仍有偏置 |
| 调参难度 | 各模型独立调参,粒度细 | 需要平衡各任务的损失权重(稍有不慎就翻车) |
你看这张表,STL 在灵活性上占优,但 MTL 在效率和泛化性上明显胜出。我个人在 ADMET 项目里经历过这样一个对比:用两个单任务模型分别预测口服吸收率和代谢半衰期,结果在交叉验证中,单任务模型对某些边缘化合物(比如 logP 在 2 到 3 之间的)判断很不稳定。把它们组合成多任务模型后,预测方差降低了将近 18%。这就是共享表示带来的好处。
实战经验:当你发现某个 ADMET 属性(比如血浆蛋白结合率)数据量特别少,不要急着上生成模型做数据增强。先试试看能不能把这个任务和另外一个数据量充足的属性(比如 logP)做成一个多任务模型。在很多情况下,这比数据增强来得更自然。
2.5 关键权衡:什么情况下该放弃 MTL?
多任务学习不是万能的。我遇到过几个典型翻车案例:
- 任务之间对抗:比如预测水溶性和 lgP(脂溶性)。这两个属性在化学上高度负相关。如果强制共享表示,可能导致模型在两者之间“左右为难”。这种情况我建议使用硬参数共享的变体——软共享(Soft Sharing),让每个任务保留一部分私有表示。
- 数据规模相差悬殊:比如一个任务有 10 万条数据,另一个只有 200 条。多任务模型会倾向于优化大任务。我的做法是对小任务使用更高的学习率或任务权重。
- 任务输出单位不一致:分类和回归混在一起时,损失函数要谨慎设计。嗯,具体怎么设计,咱们后面章节会详细讲。
2.6 本章知识结构图
为了让你对多任务学习基础有一个整体的视觉印象,我画了下面这张图。它涵盖了“多任务动机”、“共享表示+任务特定层”和“与单任务对比”三个核心模块,以及它们之间的关系。
这张图你可以保存下来。它包含了我们刚才讲的所有关键组件:顶部是使用多任务的三个动机,中间是共享表示,下面分叉为多个任务特定层。最底部的提示框总结了单任务与多任务对比的核心结论。
记住一点:多任务学习的精髓不在于“一个模型做多件事”,而在于“通过多件事学会更本质的表示”。后面我们讲偏置、损失函数和训练策略时,还会反复回到这个图来。