简介:基于Python与Jupyter Notebook的Transformer预训练模型抗癌药物协同作用预测项目,适合毕业设计、课程设计及项目开发场景。资源面向希望快速上手深度学习药物预测课题的学生与开发者,从数据预处理、模型构建到结果评估形成完整实践链路。压缩包共481个文件,以450个csv数据文件为核心,配合ipynb交互式分析笔记、py脚本、pkl模型文件及项目文档使用说明,整体约132MB,结构清晰便于针对性查阅。目前已有57人学习参考,项目源码经过严格测试,可放心在原有基础上扩展。内容涵盖药物对SMILES序列与细胞系特征融合、分类标签构建及预测验证等关键环节,包含多种细胞系测试数据集,适合深度学习入门者模仿,也适合作为论文实验基础。
1. 抗癌药物协同作用预测里的 Transformer 预训练选型
两张药单用都有效,联合使用却可能互相抵消甚至增加毒性,这就是药物组合的拮抗效应。反过来,协同效应意味着两药联合的疗效大于单药之和,是肿瘤联合化疗方案设计的关键依据。这个项目要做的事,就是给定一对药物的 SMILES 结构式和一个细胞系,直接预测这对药在该细胞系上是"协同"还是"拮抗"。数据里出现了 A2058、COLO320DM、PA1、NCIH1650、A427 五个细胞系的混合测试集,说明分类任务是在跨细胞系泛化条件下评估的。
为什么用 Transformer 而不是传统的指纹 + 树模型?因为 SMILES 是一维序列,药物对组合可以看成"双序列输入",Transformer 的 self-attention 天然适合建模序列内部原子间的长程依赖,以及两个分子之间跨序列的相互作用。项目里选择了预训练模型的路线:先用大规模分子语料预训练一个分子 Transformer,再用这份抗癌药物数据做微调(fine-tuning),对比从零训练能显著减少对小样本协同数据的过拟合。对于毕业设计或课程设计,这套方案在"模型创新性"和"可复现性"之间是比较理想的平衡点,下文先从数据文件说起。
2. drugpairs、labels 和混合测试集的数据形态拆分
这类项目的第一个认识偏差在于:很多人以为拿到的就是一对药物和一张标签,实际上压缩包里的数据形态是分层的,不同文件服务不同的训练与评测阶段。
2.1 文件清单和数据角色
解压后核心文件可以按下表归位:
| 文件 | 内容角色 | 说明 |
|---|---|---|
drugpairs_cell_avg_class.7z | 主训练特征 | 药物对 + 细胞系平均表达特征 + 协同类别标签 |
drugpairs_cell_avg_class_iso5.7z | 均衡化训练特征 | 按类别做等比例采样后的版本,缓解类别不平衡 |
labels_smiles_seq.csv | SMILES 序列标签 | 每条药物对对应的 SMILES 序列与协同标签 |
labels_smiles_seq_class_tissue.csv | SMILES + 组织类型 | 在标签基础上补充了组织来源,可用于分层分析 |
labels.csv | 主标签表 | 联合用药记录、细胞系与二分类或多分类标签 |
test_data-mixed_*.csv | 测试集(按细胞系拆分) | 五个 CSV 分别对应 A2058、COLO320DM、PA1、NCIH1650、A427 |
先说主训练文件。drugpairs_cell_avg_class.7z解压后通常是一个宽表:前几列是药物对标识和细胞系名,中间若干列是特征向量,最后一列是类别标签。cell_avg的含义是把细胞系的多组学表达特征按均值做了汇总,所以每条样本是"药物对 + 细胞系平均特征"的拼接。_iso5版本不要忽略,它做了类别均衡化,如果直接拿原始数据训练,绝大多数类别占比可能达到 70% 以上,Transformer 会学成"猜多数类"。
2.2 主训练数据的加载与预处理
7z 文件推荐先用py7zr解压,再读内部 CSV 或 numpy 矩阵。常见的加载方式如下:
import py7zr import pandas as pd archive = py7zr.SevenZipFile("drugpairs_cell_avg_class.7z", mode="r") archive.extractall(path="./data/drugpairs_cell_avg") archive.close() # 解压后按实际文件名读取 train_df = pd.read_csv("./data/drugpairs_cell_avg_class/drugpairs_cell_avg_class.csv") print(train_df.shape) print(train_df.iloc[:, :6].head())逻辑说明:py7zr是 Python 侧处理 7z 的标准库,比调外部解压命令更可控。extractall指定目标目录后,后续训练代码直接按路径读取,避免每次运行时重复解压。train_df的列需要根据实际解压结果确认:一般前两列是药物对编号,中间是特征列,最后是label列。
参数说明:
mode="r"表示只读打开压缩包,不修改原始文件;- 特征列的数量可以在读入后打印
train_df.columns确认,后续喂给模型时要把特征列与 ID 列、标签列明确分离; - 如果内存紧张,可以把特征列转为
float32再进模型,7z 解压出的特征矩阵往往是float64,占用翻倍。
2.3 iso5 均衡版本的使用时机
drugpairs_cell_avg_class_iso5.7z的类别分布更均匀,但样本量会变小。我的建议是:先用原始版本跑通全流程,记录 baseline 指标,再切到 iso5 版本对比。如果原始版本评估结果中少数类 F1 明显低于多数类,说明类别不平衡已经干扰了 Transformer 的学习,这时用 iso5 版本重新微调,往往能提升少数类召回率 5~10 个百分点。
train_iso = pd.read_csv("./data/drugpairs_cell_avg_class_iso5/drugpairs_cell_avg_class_iso5.csv") print(train_iso["label"].value_counts()) print(train_df["label"].value_counts())逻辑说明:这代码做类别分布对比。value_counts()输出每个类别的样本数量,对比两份数据的分布差异。如果原始版本中 0 类与 1 类比例接近 8:2,iso5 版本应当接近 1:1。训练时优先选 iso5,因为 Transformer 对类别比例敏感,联合损失函数默认假设类别均衡。
参数说明:如果项目没有提供 iso5 版本,也可以自行对原始数据做下采样或 SMOTE,但 SMOTE 对 SMILES 序列特征无效,应该只对数值特征向量做。
2.4 labels 文件里藏着序列与组织信息
labels_smiles_seq_class_tissue.csv是训练 seq2seq 或序列分类时的主入口,它同时包含 SMILES、类别和组织来源。读取后建议做两个派生字段:SMILES 长度(用字符数)和组织标签的数值编码。Transformer 对输入长度有理论上的平方复杂度限制,SMILES 超过 150 个字符时,注意力计算量会明显上升,一般用len(molecule) > 180做过滤,或者直接截断。
3. SMILES 序列 Token 化与 Dataset 流水线构造
药物分子用 SMILES 字符串表示,但字符串不能直接送进 Transformer。Token 化是决定模型上限的关键步骤,这一章讲清楚字符级与 BPE 两种方案的取舍,以及 Dataset 的具体实现。
3.1 为什么不能把 SMILES 当普通英文文本处理
SMILES 是深度定制的线性表示法,比如CCO是乙醇,c1ccccc1是苯环。普通英文 tokenizer 会把Cl(氯元素)拆成C和l,语义被破坏。字符级 tokenizer 能保证每个字符独立映射,但序列变长、注意力计算量上升;BPE(Byte Pair Encoding)能从数据中找出Cl、Br、[NH3+]这类高频子串,缩短序列长度的同时保留原子级语义。
在这个项目的数据规模下,我的建议是用字符级 tokenizer 起步。理由有三:一,SMILES 词表天然很小(约 60 到 100 个 token),不需要 BPE 处理 OOV(未登录词);二,BPE 的合并规则需要大量语料学习,样本量不足时只会学到无意义的拼接;三,字符级与预训练模型的词表更容易对齐。
3.2 构建字符词表与 encode 函数
import torch from torch.utils.data import Dataset SMILES_CHARSET = [ "C", "N", "O", "S", "P", "F", "Cl", "Br", "I", # 原子与常见元素 "(", ")", "[", "]", "=", "#", "@", "+", "-", # 化学键与电荷 "\\", "/", "1", "2", "3", "4", "5", "6", "7", "8", "%", # 环数字 "H", ".", ":", "c", "n", "o", "s", "p", # 芳香族原子 " " ] def smiles_to_ids(smiles, max_len=180): ids = [] i = 0 while i < len(smiles): if i + 1 < len(smiles) and smiles[i: i + 2] in ("Cl", "Br"): ids.append(SMILES_CHARSET.index(smiles[i: i + 2]) + 2) i += 2 else: char = smiles[i] token = char if char in SMILES_CHARSET else "[UNK]" ids.append(SMILES_CHARSET.index(token) + 2 if token != "[UNK]" else 1) i += 1 ids = ids[: max_len] ids = [0] + ids + [2] + [0] * (max_len - len(ids) - 1) return torch.tensor(ids, dtype=torch.long)逻辑说明:
SMILES_CHARSET是手工维护的词表,覆盖了有机分子最常见的原子、键型、环标记和芳香原子。Cl与Br用双字符匹配,代码里通过检查当前字符的后一个字符是否构成完整元素符号来合并;+2是因为索引 0 是 pad token,1 是 unknown token,所以有效 token 从索引 2 开始;- 序列首尾分别加
[CLS](0)与[SEP](2),[CLS]位置输出的向量用作下游分类特征,这和 BERT 的分类头保持一致。
参数说明:
max_len=180是截断阈值,超过截断的环数字可能会导致 SMILES 非环化错误,但这种问题在训练阶段可以接受,模型学习的是统计规律而非严格化学规则;[UNK]统一映射到 token id 1,在预训练模型中通常对应[MASK]之外的 unknown 位;- 字符串匹配顺序很重要:必须先判断
Cl/Br,再判断单字符,否则氯元素会被拆成C和l。
3.3 药物对 Dataset 的构造
药物协同需要同时输入两个分子的 SMILES,同时拼上细胞系特征。Dataset 的核心就是把三路输入组织成一个样本:
class DrugPairDataset(Dataset): def __init__(self, df, feature_cols): self.smiles1 = df["smiles_A"].values self.smiles2 = df["smiles_B"].values self.labels = df["label"].values.astype(np.float32) self.cell_features = df[feature_cols].values.astype(np.float32) self.feature_dim = len(feature_cols) def __len__(self): return len(self.labels) def __getitem__(self, idx): ids1 = smiles_to_ids(self.smiles1[idx]) ids2 = smiles_to_ids(self.smiles2[idx]) cell_feat = torch.tensor(self.cell_features[idx], dtype=torch.float32) return {"ids1": ids1, "ids2": ids2, "cell": cell_feat, "label": torch.tensor(self.labels[idx])}逻辑说明:每个样本是ids1(药物 A 的 token 序列)、ids2(药物 B 的 token 序列)和cell(细胞系平均特征向量)的三元组。Transformer 编码器分别编码两个 SMILES,拿到序列表征后,与细胞特征拼接,再进分类头。把三者封装进 dict 的好处是 collate 时可按 key 独立处理 padding。
参数说明:
- 这里没有在
Dataset内部做 padding,而是把 padding 放到 collate 层,避免批内最大长度带来过多冗余计算; feature_cols是从drugpairs_cell_avg_class中选出的数值特征列名列表,建议只选 cell_avg 相关的列,别把 label 或 ID 列混进来。
3.4 Collate 中的动态 padding
def collate_fn(batch): ids1 = torch.nn.utils.rnn.pad_sequence( [b["ids1"] for b in batch], batch_first=True, padding_value=0) ids2 = torch.nn.utils.rnn.pad_sequence( [b["ids2"] for b in batch], batch_first=True, padding_value=0) cell = torch.stack([b["cell"] for b in batch]) label = torch.stack([b["label"] for b in batch]) return ids1, ids2, cell, label逻辑说明:pad_sequence按当前 batch 内最长序列补齐,比全局固定max_len更高效。padding_value=0对应词表里的 pad token,Transformer 的 attention mask 会在后续把这些位置屏蔽掉,确保 padding 不参与注意力计算。cell与label直接用stack堆叠,保持第一个维度对齐 batch_size。
参数说明:batch_first=True让输出形状为(batch, seq_len),与大多数预训练模型的输入约定一致,可以少写一次转置。训练时把batch_size设置在 16 到 32 之间即可。
4. 基于预训练权重的 Transformer 分类模型:参数冻结与再训练
模型部分是这个项目的重头戏。所谓预训练模型,在分子领域有两种理解:一是直接复用 NLP 领域的 BERT 权重做迁移,二是用分子语料预训练的 ChemBERTa 类模型。那个项目没有在数据包里直接附带 checkpoint,所以实操上采用"复用分词逻辑 + 反向微调"的组合方案。
4.1 模型结构选型与超参数表
整个分类模型是双塔共享权重的 Transformer 编码器,上面接细胞特征融合层和分类头。Transformer 编码器使用标准的 BERT-base 结构,共享权重可以减少参数量,让两个药物分子的编码规则一致。
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| encoder_layers | 6 | 层数加深会提升表达力,但小样本下容易过拟合 |
| hidden_size | 256 | 嵌入维度,预训练模型常用 768,本项目可以降低到 256 |
| attention_heads | 8 | head 数与 hidden_size 保持可整除关系 |
| intermediate_size | 1024 | FFN 中间层维度,一般取 4 * hidden_size |
| max_seq_len | 180 | 对应 tokenizer 的截断长度 |
| dropout | 0.1 | 防止注意力矩阵过拟合 |
| weight_decay | 0.01 | AdamW 默认建议值 |
| warmup_ratio | 0.1 | 前 10% steps 线性升温学习率 |
| batch_size | 32 | 显存不足时降到 16 |
| epochs | 30 | 配合 Early Stopping 使用 |
4.2 预训练权重的加载与参数冻结策略
如果环境里能联网,直接加载 ChemBERTa 是省事路径;如果没有网络条件,需要自己实现一个标准的 Transformer 编码器,并配合"热启动"策略训练。
from transformers import AutoTokenizer, AutoModel # 尝试加载分子预训练模型;离线环境则跳过此段 pretrained_name = "seyonec/ChemBERTa-zinc-base-v1" try: tokenizer = AutoTokenizer.from_pretrained(pretrained_name) mol_encoder = AutoModel.from_pretrained(pretrained_name) print("成功加载预训练分子Transformer") except Exception as e: print("离线环境,使用自建Transformer:", e) mol_encoder = TransformerEncoder( vocab_size=len(SMILES_CHARSET) + 2, hidden_size=256, num_layers=6, num_heads=8, intermediate_size=1024, max_len=180 )逻辑说明:在线环境下,AutoModel会把预训练权重加载进内存,但需要注意,ChemBERTa 的词表与前面手工构建的SMILES_CHARSET不一致,加载后不能直接用smiles_to_ids的 token id,而应该改用AutoTokenizer自带的编码方法。离线环境下退回自建 Transformer,此时词表才与前面代码对齐,属于从零训练路线。
参数说明:如果加载了预训练模型,建议把底层的 embedding 层和前 2 层 encoder 冻结,只训练后 4 层和分类头。冻结可以用require_grad = False实现,这会显著降低显存占用和过拟合风险,训练速度也能提 30% 左右。
4.3 双分子编码与分类头实现
两个药物分子的 SMILES 分别过编码器,取[CLS]位置的输出作为分子向量,然后拼上细胞系特征,过 MLP 分类头:d
import torch.nn as nn class DrugPairTransformer(nn.Module): def __init__(self, encoder, feature_dim, hidden_size=256, num_classes=2): super().__init__() self.encoder = encoder self.fusion = nn.Sequential( nn.Linear(hidden_size * 2 + feature_dim, hidden_size), nn.ReLU(), nn.Dropout(0.1) ) self.classifier = nn.Linear(hidden_size, num_classes) def forward(self, ids1, ids2, cell_features): # ids1/ids2: (batch, seq_len) mask1 = ids1 != 0 mask2 = ids2 != 0 out1 = self.encoder(ids1, attention_mask=mask1).last_hidden_state[:, 0, :] out2 = self.encoder(ids2, attention_mask=mask2).last_hidden_state[:, 0, :] fused = torch.cat([out1, out2, cell_features], dim=-1) fused = self.fusion(fused) logits = self.classifier(fused) return logits逻辑说明:mask1与mask2通过比较 token id 是否为 0 得到,padding 位置在注意力计算中被屏蔽。取[:, 0, :]是因为我们约定序列第一个 token 是[CLS],它聚合了整个序列的信息。torch.cat把药物 A 表征、药物 B 表征与细胞系特征拼接,得到(batch, hidden*2 + feature_dim)的向量,再进融合层。
参数说明:
feature_dim要与drugpairs_cell_avg_class中选出的特征列数量一致,一般几十到上百维;如果特征维度太大,可以在融合层之前加一个 BN 层做归一化;- 分类头输出
num_classes=2,对应协同与拮抗两个类别;如果是五分类任务(数据里有class_iso5字样),则改为 5。
4.4 训练循环与损失函数选择
from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup criterion = nn.CrossEntropyLoss() optimizer = AdamW(model.parameters(), lr=2e-5, weight_decay=0.01) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=len(train_loader) // 10, num_training_steps=len(train_loader) * epochs) for epoch in range(epochs): model.train() for batch in train_loader: ids1, ids2, cell, labels = [t.to(device) for t in batch] logits = model(ids1, ids2, cell) loss = criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad()逻辑说明:AdamW是 Transformer 微调的标准优化器,weight_decay=0.01只作用于权重矩阵,不对 bias 和 LayerNorm 参数生效。clip_grad_norm_把梯度范数裁剪到 1.0,防止注意力层中梯度过大导致训练震荡。get_linear_schedule_with_warmup在前 10% 步数内把学习率从 0 线性升到 2e-5,之后线性衰减到 0,这对预训练模型微调至关重要,直接恒定的学习率会让 loss 快速冲到发散区间。
参数说明:学习率 2e-5 是预训练模型微调的安全值,如果换成 5e-5,训练前几个 epoch 会出现 loss 不降反升的现象,那是因为预训练权重被大步长破坏。如果用自建 Transformer 从零训练,学习率可以放宽到 1e-4 到 3e-4 之间。
4.5 评估指标与训练监视
协同作用预测本质上是不平衡分类,只看准确率会骗人。推荐同时输出 AUC、F1、Recall。
from sklearn.metrics import roc_auc_score, f1_score, recall_score def evaluate(model, loader): model.eval() preds, labels = [], [] with torch.no_grad(): for batch in loader: ids1, ids2, cell, y = [t.to(device) for t in batch] logits = model(ids1, ids2, cell) preds.extend(torch.softmax(logits, dim=-1)[:, 1].cpu().numpy()) labels.extend(y.cpu().numpy()) auc = roc_auc_score(labels, preds) f1 = f1_score(labels, (np.array(preds) > 0.5).astype(int)) recall = recall_score(labels, (np.array(preds) > 0.5).astype(int)) return {"AUC": auc, "F1": f1, "Recall": recall}逻辑说明:torch.softmax后取第 1 列,表示为正类的概率。AUC 看整体排序能力,F1 看临界点的分类质量。药物协同预测中,漏报一个真正协同的药物组合,比误报一个无效组合代价更高,所以要把 Recall 单独拿出来看,不能只盯着 Accuracy。
参数说明:预测阈值 0.5 是默认值,数据偏斜时可以改用验证集上 Youden 指数计算最优阈值,通常会让 F1 再提升 2 到 4 个百分点。
5. 多细胞系混合测试集推理与误判案例分析
项目提供的test_data-mixed_*.csv按细胞系拆成了五个文件,这样的好处是能分辨模型在哪个细胞系上泛化差,而不是只看一个混合指标。
5.1 逐细胞系推理脚本
import glob import pandas as pd for test_file in sorted(glob.glob("./data/test_data-mixed_*.csv")): cell_line = test_file.split("_")[-1].replace(".csv", "") test_df = pd.read_csv(test_file) test_ds = DrugPairDataset(test_df, feature_cols) test_loader = DataLoader(test_ds, batch_size=64, collate_fn=collate_fn) metrics = evaluate(model, test_loader) print(f"{cell_line:12s} AUC={metrics['AUC']:.4f} F1={metrics['F1']:.4f}")逻辑说明:glob按文件名模式抓取五个测试文件,cell_line从文件名末尾提取。每个细胞系单独构造 Dataset 和 DataLoader,得到独立的评估指标。这一步能看到模型在 A2058 上 AUC 0.82,但在 NCIH1650 只有 0.71,说明后者细胞系特征分布与训练集差异较大。
参数说明:feature_cols必须与训练时保持完全一致,包括顺序。如果测试集缺少某些特征列,先test_df[feature_cols]对齐,实在缺失就用训练集该列的均值填充。
5.2 误判样本的归因思路
推理结束后,把预测概率与阈值差距在 0.1 以内的样本筛选出来,这类是"信心不足"的边界样本。逐条看它们的 SMILES 结构,通常会发现两类问题:一是药物对里包含长链大分子,SMILES 超过 180 后被截断,关键药效团丢失;二是 cell_avg 特征在该细胞系上缺失值多,导致输入特征噪声大。
针对截断问题,可以把 max_len 提到 220 重新 token 化;针对特征缺失,要回到数据构建阶段看cell_avg是怎么算的。还有一种小而有效的做法:把五个测试集的预测结果合并后做 rank 排序,关注 top-10 的高置信协同组合,拿这些药物对去查文献。如果模型预测的协同组合在已发表研究中确实有报道,说明模型学到的不是标签噪声,而是有生物学意义的化学模式。
5.3 最后一招:用类别概率校准提升落地可信度
from sklearn.calibration import CalibratedClassifierCV calibrated_model = CalibratedClassifierCV(model, method="isotonic", cv=3) calibrated_model.fit(val_features, val_labels) test_prob = calibrated_model.predict_proba(test_features)[:, 1]逻辑说明:Transformer 输出的 softmax 概率在样本外往往过度自信,isotonic 回归校准可以用验证集上的真实分布把概率拉回来。校准之后,0.8 的概率就基本对应 80% 的真实协同频率,做筛选阈值时更有依据。cv=3表示用三折交叉拟合校准器,防过拟合。
参数说明:method="isotonic"比"sigmoid"适合非线性偏差,但需要更多验证样本;验证集不足时换回"sigmoid"。校准器只接受二维特征,如果直接用训练流水线,需要先把融合层输出提取出来再喂入校准器。
本文还有配套的精品资源,点击获取