基于Transformer预训练的抗癌药物协同作用预测模型构建

📅 发布时间:2026/9/15 4:03:43
基于Transformer预训练的抗癌药物协同作用预测模型构建
简介基于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.csvSMILES 序列标签每条药物对对应的 SMILES 序列与协同标签labels_smiles_seq_class_tissue.csvSMILES 组织类型在标签基础上补充了组织来源可用于分层分析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, moder) 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列。参数说明moder表示只读打开压缩包不修改原始文件特征列的数量可以在读入后打印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:2iso5 版本应当接近 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 能保证每个字符独立映射但序列变长、注意力计算量上升BPEByte 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_len180): 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, dtypetorch.long)逻辑说明SMILES_CHARSET是手工维护的词表覆盖了有机分子最常见的原子、键型、环标记和芳香原子。Cl与Br用双字符匹配代码里通过检查当前字符的后一个字符是否构成完整元素符号来合并2是因为索引 0 是 pad token1 是 unknown token所以有效 token 从索引 2 开始序列首尾分别加[CLS]0与[SEP]2[CLS]位置输出的向量用作下游分类特征这和 BERT 的分类头保持一致。参数说明max_len180是截断阈值超过截断的环数字可能会导致 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], dtypetorch.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 中的动态 paddingdef collate_fn(batch): ids1 torch.nn.utils.rnn.pad_sequence( [b[ids1] for b in batch], batch_firstTrue, padding_value0) ids2 torch.nn.utils.rnn.pad_sequence( [b[ids2] for b in batch], batch_firstTrue, padding_value0) 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_value0对应词表里的 pad tokenTransformer 的 attention mask 会在后续把这些位置屏蔽掉确保 padding 不参与注意力计算。cell与label直接用stack堆叠保持第一个维度对齐 batch_size。参数说明batch_firstTrue让输出形状为(batch, seq_len)与大多数预训练模型的输入约定一致可以少写一次转置。训练时把batch_size设置在 16 到 32 之间即可。4. 基于预训练权重的 Transformer 分类模型参数冻结与再训练模型部分是这个项目的重头戏。所谓预训练模型在分子领域有两种理解一是直接复用 NLP 领域的 BERT 权重做迁移二是用分子语料预训练的 ChemBERTa 类模型。那个项目没有在数据包里直接附带 checkpoint所以实操上采用复用分词逻辑 反向微调的组合方案。4.1 模型结构选型与超参数表整个分类模型是双塔共享权重的 Transformer 编码器上面接细胞特征融合层和分类头。Transformer 编码器使用标准的 BERT-base 结构共享权重可以减少参数量让两个药物分子的编码规则一致。超参数推荐值说明encoder_layers6层数加深会提升表达力但小样本下容易过拟合hidden_size256嵌入维度预训练模型常用 768本项目可以降低到 256attention_heads8head 数与 hidden_size 保持可整除关系intermediate_size1024FFN 中间层维度一般取 4 * hidden_sizemax_seq_len180对应 tokenizer 的截断长度dropout0.1防止注意力矩阵过拟合weight_decay0.01AdamW 默认建议值warmup_ratio0.1前 10% steps 线性升温学习率batch_size32显存不足时降到 16epochs30配合 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_sizelen(SMILES_CHARSET) 2, hidden_size256, num_layers6, num_heads8, intermediate_size1024, max_len180 )逻辑说明在线环境下AutoModel会把预训练权重加载进内存但需要注意ChemBERTa 的词表与前面手工构建的SMILES_CHARSET不一致加载后不能直接用smiles_to_ids的 token id而应该改用AutoTokenizer自带的编码方法。离线环境下退回自建 Transformer此时词表才与前面代码对齐属于从零训练路线。参数说明如果加载了预训练模型建议把底层的 embedding 层和前 2 层 encoder 冻结只训练后 4 层和分类头。冻结可以用require_grad False实现这会显著降低显存占用和过拟合风险训练速度也能提 30% 左右。4.3 双分子编码与分类头实现两个药物分子的 SMILES 分别过编码器取[CLS]位置的输出作为分子向量然后拼上细胞系特征过 MLP 分类头dimport torch.nn as nn class DrugPairTransformer(nn.Module): def __init__(self, encoder, feature_dim, hidden_size256, num_classes2): 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_maskmask1).last_hidden_state[:, 0, :] out2 self.encoder(ids2, attention_maskmask2).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_classes2对应协同与拮抗两个类别如果是五分类任务数据里有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(), lr2e-5, weight_decay0.01) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepslen(train_loader) // 10, num_training_stepslen(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_decay0.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_size64, collate_fncollate_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, methodisotonic, cv3) calibrated_model.fit(val_features, val_labels) test_prob calibrated_model.predict_proba(test_features)[:, 1]逻辑说明Transformer 输出的 softmax 概率在样本外往往过度自信isotonic 回归校准可以用验证集上的真实分布把概率拉回来。校准之后0.8 的概率就基本对应 80% 的真实协同频率做筛选阈值时更有依据。cv3表示用三折交叉拟合校准器防过拟合。参数说明methodisotonic比sigmoid适合非线性偏差但需要更多验证样本验证集不足时换回sigmoid。校准器只接受二维特征如果直接用训练流水线需要先把融合层输出提取出来再喂入校准器。本文还有配套的精品资源点击获取