140

DeepDTA 论文精读:序列模型为什么成为 DTA 早期基线

DeepDTA 用两条 CNN 建立了 DTA 预测的基本范式。这篇讲清它为什么成为长期基线,以及它揭示的方法学问题。

DeepDTA(Öztürk 等,Bioinformatics 2018)是深度学习做药物-靶点亲和力预测的开创性工作。它的架构极其简单——正是这个简单性让它成为长期的基线,也让后来的一个发现格外值得注意:很多更复杂的模型并没有真正超越它多少

架构:两条 CNN

# 药物侧:
#   SMILES 字符串 → 字符级 embedding → 1D CNN → 药物向量
#
# 蛋白侧:
#   氨基酸序列 → 字符级 embedding → 1D CNN → 蛋白向量
#
# 融合:
#   拼接 → 三层全连接 → 预测亲和力(回归)
#
# 就这么简单。没有注意力,没有图,没有预训练。
#
# 关键设计:
#   把 SMILES 与蛋白序列都当作「文本」处理
#   用同样的 CNN 架构提取局部模式
#
# 论文的贡献:
#   1) 证明了端到端深度学习在 DTA 上可行
#   2) 【把 DTA 从分类问题变成回归问题】
#      此前多数工作做「结合/不结合」的二分类,
#      而实际需要的是定量的亲和力
#   3) 建立了 Davis 与 KIBA 两个标准数据集的用法

为什么 SMILES 的 CNN 会有效

  • SMILES 中相邻的字符往往对应化学上相邻的原子:因此局部卷积能捕捉到片段级的模式(如芳环、酰胺、卤素);
  • 但也有明显的问题
    • 环闭合标记会把远距离的原子关联起来——c1ccccc1 中的两个 1 在字符串上相邻但语义上表示成环;
    • 同一分子有多种 SMILES 写法,模型对写法敏感(可用 SMILES 增强缓解);
    • 分支括号的嵌套结构,CNN 难以正确解析;
    • 没有显式的化学约束:模型不知道价键规则。
  • 这些问题正是后续工作转向图表示的动机(见 139《GraphDTA 论文精读》)。

它为什么成为长期基线

# 后续大量工作声称超越 DeepDTA,但:
#
# 1) 【提升幅度普遍不大】
#    Davis 数据集上 MSE 从 0.26 降到 0.22 之类
#    考虑到数据集规模与噪声,这个差距的意义存疑
#
# 2) 【很多提升可能来自超参数与训练技巧】
#    而非架构本身(见 131 的讨论)
#
# 3) 【平凡基线的表现被长期忽略】
#    只用「药物均值 + 靶点均值」的矩阵分解式基线
#    在随机划分下就能接近这些深度模型
#    → 说明大部分「性能」来自记住了药物/靶点的边际分布
#
# 4) 【cold split 下所有方法都大幅下降】
#    这才是真实的泛化能力
#
# 这个现象不限于 DTA ——
# 它是深度学习应用于小数据科学问题时的普遍情况:
#   简单基线 + 好的评测设置,
#   常常比复杂模型 + 宽松评测更有信息量

# 【阅读 DTA 论文的检查清单】:
#   □ 划分方式是什么?random / cold drug / cold protein
#   □ 有没有平凡基线对照?
#   □ 报告了多少个种子的结果?标准差多大?
#   □ 提升是否超过种子间的方差?
#   □ 有没有在独立数据上验证?
#   □ 有没有实验验证的案例?

Davis 与 KIBA 数据集

Davis KIBA
药物数 68 2111
靶点数 442(全是激酶 229
测量 Kd KIBA 分数(整合 Ki/Kd/IC50)
矩阵密度 完整(全部测过) 稀疏
主要问题 药物太少;只有激酶 KIBA 分数的物理含义模糊

Davis 只有 68 个药物——这意味着随机划分下,测试集中的每个药物几乎必然在训练集中出现过(配不同的靶点)。在这样的数据上比较模型的「泛化能力」,本身就站不住脚。

实现(作为对照基线)

import torch
import torch.nn as nn

class DeepDTA(nn.Module):
    def __init__(self, n_smi_chars=64, n_seq_chars=25,
                 embed_dim=128, n_filters=32, dropout=0.1):
        super().__init__()
        self.embed_smi = nn.Embedding(n_smi_chars + 1, embed_dim)
        self.cnn_smi = nn.Sequential(
            nn.Conv1d(embed_dim, n_filters, 4), nn.ReLU(),
            nn.Conv1d(n_filters, n_filters * 2, 6), nn.ReLU(),
            nn.Conv1d(n_filters * 2, n_filters * 3, 8), nn.ReLU(),
            nn.AdaptiveMaxPool1d(1),
        )
        self.embed_seq = nn.Embedding(n_seq_chars + 1, embed_dim)
        self.cnn_seq = nn.Sequential(
            nn.Conv1d(embed_dim, n_filters, 4), nn.ReLU(),
            nn.Conv1d(n_filters, n_filters * 2, 8), nn.ReLU(),
            nn.Conv1d(n_filters * 2, n_filters * 3, 12), nn.ReLU(),
            nn.AdaptiveMaxPool1d(1),
        )
        self.head = nn.Sequential(
            nn.Linear(n_filters * 6, 1024), nn.ReLU(), nn.Dropout(dropout),
            nn.Linear(1024, 1024), nn.ReLU(), nn.Dropout(dropout),
            nn.Linear(1024, 512), nn.ReLU(),
            nn.Linear(512, 1),
        )

    def forward(self, smi_idx, seq_idx):
        d = self.cnn_smi(self.embed_smi(smi_idx).permute(0, 2, 1)).squeeze(-1)
        p = self.cnn_seq(self.embed_seq(seq_idx).permute(0, 2, 1)).squeeze(-1)
        return self.head(torch.cat([d, p], dim=1))

# 用 TDC 加载数据并做正确的划分(见 133)
from tdc.multi_pred import DTI
data = DTI(name="DAVIS")
data.convert_to_log(form="binding")
split = data.get_split(method="cold_split", column_name="Drug", seed=42)
# 【用 cold_split 而非 random】

这条路线现在的位置

关键要点

  • 架构极简(两条 CNN),正是这个简单性让它成为难以撼动的基线
  • 它把 DTA 从二分类变成回归——这个转变比架构本身更重要;
  • Davis 只有 68 个药物,随机划分下比较「泛化能力」站不住脚;
  • 后续工作的提升幅度普遍不大,且平凡基线的对照长期缺失

延伸资源