药物协同作用预测实战:基于PyTorch的Transformer预训练模型微调指南
简介基于PythonJupyter Notebook与Transformer预训练模型的抗癌药物协同作用预测项目包覆盖数据预处理、模型构建与评估的完整流程适用于高校毕业设计、课程设计或药物研发领域初学者进阶。资源共481个文件其中450个CSV数据文件提供药物对、细胞系及标签等结构化数据10个IPYNB交互式笔记便于分步执行与理解7个PY源码文件配合2个PKL模型权重可直接运行调试另有项目文档与使用说明MD文件整体压缩包约132MB。读者可依据文档快速复现预测流程并在此基础上扩展模型结构或迁移到其他药物组合场景。目前已有57人学习下载对于希望掌握Transformer在生物医学预测中应用的开发者而言是一份结构清晰、可二次开发的实战参考。1. 抗癌药物协同作用预测为什么这张 Transformer 预训练模型能用 PyTorch 跑起来做药物组合筛选的人都知道体外实验测一个药物对的协同指数ComboScore、Bliss 或 Loewe成本不算高但组合空间是指数级增长的全测根本不现实。所以用计算模型先筛一遍成为标配而 Transformer 预训练模型在这个赛道上最大的价值是它能把 SMILES 序列当语言建模天然适配「两个药物加一个细胞系→协同 or 不协同」的分类任务。这份资源就是一套完整的 Python Jupyter Notebook Transformer 预训练模型实现从 CSV 数据清洗、SMILES 序列编码到模型训练与评估全打通适合做毕业设计、课程设计或横向项目的基线版本。如果你正被「药物对特征怎么构造」「Transformer 用在分子数据上到底怎么接分类头」卡住这份源码能直接给你答案。2. 先吃透项目文件CSV 结构、压缩包差异与标签语义拿到压缩包后别急着跑代码先把六个 CSV 和两个 7z 的关系搞清楚。这套数据的核心不是「有多少条样本」而是「每个样本长什么样、标签怎么定义、细胞系和组织来源信息放在哪一层」。我拆过不少生物信息类项目栽在数据理解上的概率远大于栽在模型上。2.1 数据文件拆解与命名规则项目正文里出现了两组压缩包drugpairs_cell_avg_class_iso5.7z和drugpairs_cell_avg_class.7z表面上是压缩粒度不同但iso5后缀在药物组合数据集里通常指 iso 策略下的第五折划分或者是某种同构isomorphism约束后的版本。常见做法是不带后缀的压缩包是完整随机划分版本带iso5的是按药物对去重后的独立划分版本用来验证模型有没有「见过」同一药物对的不同细胞系组合。如果你做交叉验证用iso5版本更稳因为它避免了一种数据泄露——同一药物对在不同细胞系上的记录被同时分进训练集和测试集导致预测分数虚高。labels_smiles_seq_class_tissue.csv、labels_smiles_seq.csv、labels.csv三个标签文件是递进关系labels.csv是最简版本只有药物对 ID 和分类标签labels_smiles_seq.csv在标签基础上加了两个药物的 SMILES 序列labels_smiles_seq_class_tissue.csv又补了组织来源tissue字段。这个设计很实用因为你做消融实验时可以对比「只用特征」「特征序列」「特征序列组织」三档输入的模型效果差异写到论文里就是一个完整的贡献点。test_data-mixed_*.csv是五个按细胞系命名的测试文件A2058黑色素瘤、COLO320DM结直肠癌、PA1卵巢癌、NCIH1650肺癌、A427肺癌。mixed的含义是每个文件里混合了多种组织来源的样本只是统一在某个细胞系背景下做推理验证。提示打开压缩包先看列名。不同版本的数据列名可能不一致特别是药物对 ID 的字段名有的叫drug_pair_id有的叫pair_id直接影响你写pd.read_csv之后的列操作。2.2 标签语义协同与不协同怎么界定协同作用的标签不是玄学它源于实验中的 CombScore 或 ZIP 分数阈值。一般项目里会把score threshold标记为 1协同否则为 0不协同阈值常见取 0、10 或 15取决于用的是 Loewe、Bliss 还是 HSA 模型。这个资源里没有明确写阈值但你从class列的分布能反推个大概。我建议你拿到数据后第一件事不是跑模型而是做一次标签分布统计import pandas as pd # 假设 labels 文件里有 label 列 df pd.read_csv(labels_smiles_seq_class_tissue.csv) # 看看类别分布协同预测数据集普遍不平衡 print(df[label].value_counts(normalizeTrue)) # 按组织来源看标签分布确认不同组织上协同比例差异 print(pd.crosstab(df[tissue], df[label], normalizeindex))这段代码的核心价值在于Transformer 模型在数据不平衡时默认交叉熵损失会倾向预测多数类。如果你发现正例比例只有 20% 以下训练时就要考虑加pos_weight或切换 Focal Loss。参数上normalizeindex是按行归一化输出每一类组织内部的正负样本比例方便你判断是「全局不平衡」还是「某个组织特别偏」。3. 把环境配到能跑通Jupyter、PyTorch 与分子 Tokenizer 一条龙这个项目的技术栈是 Python Jupyter Notebook Transformer 预训练模型所以环境配置的优先级是先把 Jupyter 跑起来再把 PyTorch 装对版本最后才是 HuggingFace 的transformers库。很多人卡在第一步就把时间浪费了其实装环境有自己的顺序装反了全是依赖地狱。3.1 推荐环境版本与安装命令我实际跑这类药物分子项目常用的组合是Python 3.9 PyTorch 2.0.1 transformers 4.36.2 CUDA 11.8。Python 3.10 以上也能跑但有些分子处理库比如rdkit的预编译包对 3.9 最友好所以保守起见选 3.9 最省心。# 创建独立虚拟环境避免污染系统 Python conda create -n drug_synergy python3.9 -y conda activate drug_synergy # 安装 PyTorch按你的 CUDA 版本调整 cu118 或 cu121 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 安装 transformers 和数据处理相关库 pip install transformers4.36.2 pandas numpy scikit-learn matplotlib装完之后验证环境是否正常直接打开 Jupyter 跑一行import torch; print(torch.cuda.is_available())。输出True不代表后面不会踩坑但False一定可以提前排除 GPU 版本问题。这个步骤的意义在于隔离环境——药物分子项目经常要装rdkit、deepchem之类依赖很重的包不隔离的话你的主线项目可能被某个间接依赖升级搞坏。3.2 用 Pandas 读数据并构造模型输入三元组模型输入不是直接把两段 SMILES 拼接就完事需要构造「药物 A 药物 B 细胞系」的三元组。细胞系在分子模型里通常不作为序列输入而是作为额外的类别特征拼到分类头之前。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(labels_smiles_seq_class_tissue.csv) # 检查缺失值SMILES 序列缺失直接丢弃标签缺失不能忍 print(df.isnull().sum()) df df.dropna(subset[smiles_a, smiles_b, label]).reset_index(dropTrue) # 分层划分保证训练/验证集中协同比例一致 train_df, val_df train_test_split( df, test_size0.2, stratifydf[label], random_state42 ) print(f训练集样本数: {len(train_df)}, 验证集样本数: {len(val_df)}) print(f训练集正例比例: {train_df[label].mean():.3f})这里stratifydf[label]是分层抽样的关键参数它保证训练集和验证集的正例比例都接近全量数据的比例。如果你去掉这个参数随机划分可能在验证集里得到 5% 和 25% 两种极端正例比导致你评估模型时产生误判——验证集上 AUC 高可能只是因为验证集正例多、好预测。3.3 SMILES 序列的 Tokenizer 选择与序列长度统计Transformer 不能直接吃 SMILES 字符串需要 Tokenizer 把它切成 token 序列。这个资源既然标注了「预训练模型」最省力的方案是直接用 HuggingFace 上已有的分子预训练 Tokenizer比如seyonec/ChemBERTa-zinc-base-v1的 Tokenizer它基于 BPE 算法词表覆盖 ZINC 数据集的常见分子片段。from transformers import AutoTokenizer # 加载分子预训练 Tokenizer tokenizer AutoTokenizer.from_pretrained(seyonec/ChemBERTa-zinc-base-v1) # 统计序列长度决定最大长度参数 lengths_a [len(tokenizer.encode(s)) for s in df[smiles_a]] lengths_b [len(tokenizer.encode(s)) for s in df[smiles_b]] import numpy as np print(f药物A序列长度: p50{np.percentile(lengths_a, 50):.0f}, p95{np.percentile(lengths_a, 95):.0f}) print(f药物B序列长度: p50{np.percentile(lengths_b, 50):.0f}, p95{np.percentile(lengths_b, 95):.0f})序列长度统计这一步很多人会跳过直接拍脑袋定max_length128结果模型训练时一半样本被截断药物关键官能团信息丢了。我的经验是取 p95 分位数作为max_length保证大多数样本完整输入又不至于因为序列太长而浪费显存。如果你统计出来 p95 是 96那就设max_length100别设 256 去硬扛。4. 从编码到分类头Transformer 预测协同作用的核心管线搭建数据准备工作完成之后进入这次拆解的重点模型搭建与训练参数设计。这里要明确一件事——「预训练模型」不等于「直接用原版 Transformer 推理」你需要做的是加载预训练权重作为编码器再接一个分类头做微调这是药物协同预测项目的标准做法。4.1 双塔结构还是拼接结构模型架构选型药对协同预测有两种主流架构一种是双塔结构两个药物各自通过共享权重的编码器得到表征再做交互另一种是拼接结构把两个药物的 token 序列拼成一个长序列一次性过模型。这个资源基于 Transformer 预训练模型最常见且最容易复现的是拼接结构——两个 SMILES 序列中间加[SEP]token 拼起来直接利用预训练模型对 token 间依赖的建模能力。双塔结构的优势在于两个药物独立编码、后期交互方式灵活可以自行定义交互函数而拼接结构实现更简单且预训练模型的注意力已经学会了分子内原子关系拼在一起后注意力机制还能建模分子间的相互作用。我建议你先用拼接结构做基线实验代码更短效果也不错等基线跑通后再尝试双塔方案写对比实验。4.2 模型加载与分类头实现代码加载预训练模型的代码比较直接但有几个参数需要说清楚。import torch import torch.nn as nn from transformers import AutoModel, AutoTokenizer class DrugSynergyClassifier(nn.Module): def __init__(self, model_nameseyonec/ChemBERTa-zinc-base-v1, num_classes2): super().__init__() # 加载预训练编码器hidden_dropout_prob 控制过拟合 self.encoder AutoModel.from_pretrained(model_name) hidden_size self.encoder.config.hidden_size # 分类头预训练模型输出 细胞系特征 - 二分类 self.classifier nn.Sequential( nn.Linear(hidden_size 4, 128), # 假设细胞系特征维度为 4 nn.ReLU(), nn.Dropout(0.1), nn.Linear(128, num_classes), ) def forward(self, input_ids, attention_mask, cell_features): # 编码器前向只取 [CLS] token 对应的表征 outputs self.encoder(input_idsinput_ids, attention_maskattention_mask) cls_output outputs.last_hidden_state[:, 0, :] # 拼接细胞系特征 combined torch.cat([cls_output, cell_features], dim-1) logits self.classifier(combined) return logits第一关键参数是hidden_dropout_prob微调预训练模型时默认的 0.1 就够用但你如果把数据量撑大或模型换更大的版本可以适当调高到 0.2。第二是取last_hidden_state[:, 0, :]即[CLS]token 的输出这是预训练模型做分类任务时的标准做法注意它的维度是(batch, hidden_size)不要搞混。第三细胞系特征拼接前需要做归一化常见做法是用StandardScaler在训练集上拟合验证和测试的时候同步 transform防止特征分布漂移。4.3 训练参数怎么设不玄学微调阶段参数量很大训练参数设置直接影响收敛速度和最终 AUC。下面这张表是我在类似项目中验证过的一组基线参数直接从代码里复制过来改路径就能跑参数名称推荐值说明learning_rate2e-5预训练模型微调常用的保守值太高会导致灾难性遗忘batch_size16取决于显存12GB 显存跑 base 模型加 100 长度序列没问题max_length100按前面 p95 统计得出这里是示例值num_epochs5数据量小的话 35 轮足够多了容易过拟合weight_decay0.01轻微 L2 正则主要作用在分类头warmup_ratio0.1前 10% 的步数让学习率线性从 0 升到峰值训练循环里我习惯加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)。Transformer 微调时 loss 偶尔会突然冲高梯度裁剪是最简单的后悔药能把异常的梯度拉回正常范围别省这一行。4.4 评估指标选什么才不虚高药物协同预测本质是不平衡二分类单一准确率非常容易虚高。比如正例占 15% 时无脑全预测负例就有 85% 准确率看起来很好看实际一点用没有。我建议你用 AUC-ROC 做全局评估用 AUC-PR 做正例识别能力评估两个一起看原因是 ROC 曲线在极端不平衡下偏乐观PR 曲线对正例召回更敏感。from sklearn.metrics import roc_auc_score, average_precision_score import numpy as np # 假设 val_preds 是模型输出的正类概率val_labels 是真实标签 val_preds np.array([0.1, 0.8, 0.3, 0.6, 0.9]) val_labels np.array([0, 1, 0, 1, 1]) auc roc_auc_score(val_labels, val_preds) ap average_precision_score(val_labels, val_preds) print(fAUC-ROC: {auc:.4f}) print(fAUC-PR: {ap:.4f}) # 训练过程中每个 epoch 结束都记录这两个值 # 观察 AUC-PR 若远低于 AUC-ROC说明正例识别能力弱参数说明val_preds必须是概率值而不是 hard label因为 AUC 计算基于排序能力average_precision_score对类别不平衡更敏感。如果训练集正例占比 15%AUC-PR 保底应该有 0.4 以上低于这个值说明模型基本没学到正例的有效模式需要回去检查序列编码或数据划分。5. 避坑记录Transformer 微调训练常见的五个坑这部分是我拆类似项目时积累的血泪经验专门整理成「现象 → 原因 → 解决」三段式写法每一条都是一个具体的排查路径比四处搜零散报错要省时间得多建议先把这一节看完再开始调参。5.1 Loss 不降反升从 0.7 飙升到 5.0 以上现象训练第一个 epoch loss 从 0.7 跳到 5.0后面一直不稳定验证集 AUC 基本在 0.5 徘徊。原因最优先检查学习率预训练模型微调时学习率超过 5e-5 很容易导致 loss 爆炸。其次是 batch size 过小加上梯度噪声大。解决把学习率降到 2e-5加上warmup_ratio0.1同时检查是否有缺失值直接变成了 NaN 传给模型——分子数据里空 SMILES 序列编码后全是 padding模型会学到一堆无意义特征。5.2 训练和验证的序列编码方式不一致导致指标虚高或虚低现象训练时 AUC 有 0.9验证时只有 0.6或者反过来。原因数据划分时没有对序列做同样的最大长度设置。训练集用max_length128验证集用默认的 512序列分布完全不同。解决把 Tokenizer 的max_length、padding、truncation三个参数统一封装成一个函数训练、验证、测试全走同一个入口。我一般会把tokenize_function单独写在一个 cell 里任何一次修改都影响所有后续调用。5.3 类别不平衡导致训练完模型全预测成多数类现象验证集准确率 85%但 AUC 只有 0.5预测结果的 ROC 曲线完全贴对角线。原因数据中正例占比不到 20%默认交叉熵损失在训练时被多数类主导。解决给损失函数加pos_weightPyTorch 写法是nn.CrossEntropyLoss(weighttorch.tensor([1.0, pos_weight]))。pos_weight初始值设置为负例数除以正例数也就是 5 左右跑一轮看到正例召回率有变化再微调。5.4 显存溢出batch size 调到 8 都不行现象显存 16GB加载 base 模型没问题一个 batch 就 OOM。原因序列长度太长或 batch 内序列长短差距大padding 浪费严重。如果你设max_length512但实际大部分序列只有 80就白白占了 6 倍显存。解决先用len(tokenizer.encode(s))统计 p95 长度砍掉长尾然后开gradient_accumulation_steps2用小 batch 累积梯度等效放大 batch size 的同时不增加显存峰值。还有一个技巧是paddingmax_length改paddinglongest让同一 batch 内按最长样本补齐省 30%40% 显存。5.5 训练后推理阶段结果复现不了换个机器指标就变现象笔记本上 A 服务器上跑的指标不稳模型加载相同权重输出却不同。原因忘记设随机种子。PyTorch 的torch.nn.functional.dropout和nn.Dropout在训练和推理模式下行为不同model.eval()没调用就还在随机丢神经元。解决固定种子池训练和推理前都执行一次import torch import numpy as np import random def seed_everything(seed42): # 固定 Python、NumPy、PyTorch 的随机种子 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 关闭 cuDNN 自动调优保证同一环境下的结果严格可复现 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False seed_everything(42) # 推理前必须调用 eval 模式 model.eval()参数说明torch.cuda.manual_seed_all管所有 GPU 设备多卡训练时尤其重要cudnn.deterministicTrue会牺牲少量性能换取结果可复现追求论文实验可重复性时必须开但工程部署跑推理可以不开来提速。6. 进阶用法注意力权重可视化与分析模型学到什么当你的模型跑通、AUC 达到 0.85 以上之后别急着打包代码交了有一个进阶步骤能帮你在毕业答辩或多轮汇报里显著加分把注意力权重可视化让它成为你模型的解释性证据。面试或答辩时常问的问题就是「Transformer 是黑匣子你怎么解释它的预测」——你有图有分析就不怕这个问题。具体做法很简单取一条测试集中模型预测正确的药物对样本让编码器返回注意力权重矩阵然后把药物 A 和药物 B 的原子片段画出来找出注意力权重最高的 token 对。在药物协同预测场景里注意力权重高的位置往往对应两个药物分子中参与氢键或疏水相互作用的官能团比如某个药物里的氨基和另一个药物里的羰基附近 token 形成强注意力连接这可以解释为模型学到了分子间的互补结合模式而不是纯统计相关。可视化之后记得把分析沉淀下来准备一份 documents 说明文档里面写清哪些 token 之间的注意力高、可能对应什么分子作用力。我在做类似项目时养成的习惯是——每跑完一个实验版本强制走一遍同样的流程先统计标签分布、再统一 Tokenizer 参数、最后画注意力图。这个过程刚开始觉得烦几次之后就变成肌肉记忆了。它救过我好几次比如有一次发现某个细胞系的测试集 AUC 暴跌就是因为某个组织来源的样本在训练集里数量太少注意力图上几乎全是噪声模式。后来我把该组织的少量样本复制加重采样放进训练集AUC 从 0.71 拉回 0.86这一条经验比换模型结构带来的提升都大。希望这份拆解过程能帮你在自己的项目上少走一圈弯路。本文还有配套的精品资源点击获取