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 本章知识结构图

为了让你对多任务学习基础有一个整体的视觉印象,我画了下面这张图。它涵盖了“多任务动机”、“共享表示+任务特定层”和“与单任务对比”三个核心模块,以及它们之间的关系。

多任务学习(MTL)核心知识框架 数据稀缺,多任务互补 共享标签信息 任务间内在关联 利用化学知识迁移 部署效率高 一个模型搞定一切 共享表示层 通用特征提取(GCN/Transformer编码器) 任务头A (水溶性) 任务头B (hERG毒性) 任务头C (口服吸收) 与单任务学习对比关键 优势:泛化性、效率、数据利用 → 劣势:任务干扰、调参复杂、损失平衡

这张图你可以保存下来。它包含了我们刚才讲的所有关键组件:顶部是使用多任务的三个动机,中间是共享表示,下面分叉为多个任务特定层。最底部的提示框总结了单任务与多任务对比的核心结论。

记住一点:多任务学习的精髓不在于“一个模型做多件事”,而在于“通过多件事学会更本质的表示”。后面我们讲偏置、损失函数和训练策略时,还会反复回到这个图来。