恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Transformer预训练模型的抗癌药物协同预测实践
首页
资讯中心
/
基于Transformer预训练模型的抗癌药物协同预测实践
基于Transformer预训练模型的抗癌药物协同预测实践
发布时间:2026/9/15 3:54:59
简介基于Python与Jupyter Notebook搭建的Transformer预训练模型抗癌药物协同作用预测项目面向毕业设计、课程设计及项目开发人员解决药物组合协同效应预测中的建模、训练与评估问题。资源内含完整源码、项目文档和使用说明源码经过严格测试可放心在其基础之上扩展使用。整个资源包共481个文件以450个CSV数据文件为主覆盖药物对细胞系平均分类、SMILES序列、组织类型标签等预处理数据同时包含10个ipynb交互式分析/训练脚本、7个py工具脚本、2个pkl模型文件及说明文档压缩包大小约132MB。内容结构清晰从数据清洗、特征构造到Transformer模型训练与预测均有对应代码和文档支撑便于理解整个流程。目前已有57人学习适合具备一定Python基础并希望快速上手Transformer在生物信息学中应用的开发者。1. 抗癌药物协同预测为什么需要 Transformer 预训练模型药敏棋盘实验每块板上几十个浓度组合一条条做体外筛选已经耗掉了大量实验预算而真正有效的协同组合始终是少数。传统做法把药物编成分子指纹再塞给随机森林或 LightGBM这类方案在药物对级别上很快触到天花板指纹编码的是子结构是否存在而不是两个分子如何共同扰动同一个通路。把药物分子当作一段用 SMILES 字符串书写的语言序列用 Transformer 预训练模型在大规模无标注分子库上先学会“化学语法”再让药物对共享一个语义空间预测协同与否就变成了一个二分类问题。这条路线适合正在做毕业设计或课程设计的人也适合实验室里想用小样本数据做筛选预判的团队。下文从数据表构造讲到模型微调再讲到如何把 Jupyter Notebook 里的实验代码整理成能交付的源码包。2. 用 SMILES 序列构造药物对数据集从原始表到可训练的 Pandas DataFrame2.1 药物对数据的标准字段设计预测模型的训练数据核心是一条“药物 A 药物 B 协同标签”的记录。无论数据来自公开药物组合数据库还是自己做的棋盘实验最终都需要先把表结构统一。字段越简单越好避免把实验条件过早耦合进模型输入。字段名类型示例作用pair_idstr1001药对唯一编号用于分组和追溯drug_a_idstrCID_123A 药标识drug_b_idstrCID_456B 药标识smiles_astrCC(O)Oc1ccccc1C(O)OA 药的 SMILES 序列smiles_bstrCN1CCC[CH]1c2cccnc2B 药的 SMILES 序列synergy_labelint11 为协同0 为非协同协同标签的定义在不同数据集里并不完全一致。常见标准是根据组合指数 CI 或者浓度响应曲线下面积分CI 小于 0.8 视为协同0.8 到 1.2 视为相加大于 1.2 视为拮抗。做二分类时典型做法是取 CI 0.8 作为正样本剩下非协同组合作为负样本。这里有个隐藏问题拮抗样本对模型同样有信息量但二分类场景下一般把相加和拮抗合并成 0否则样本量会被切碎。2.2 用 RDKit 规范化 SMILES清洗无效分子原始数据里的 SMILES 经常存在冗余写法或非法字符。同一分子可能写成多种等价形式直接喂给模型会让 Transformer 认为它们是不同分子。清洗用的标准工具是 RDKit其MolToSmiles能把分子重新规范成唯一序列。import pandas as pd from rdkit import Chem def canonical_smiles(smiles: str) - str: if not isinstance(smiles, str) or len(smiles) 0: return mol Chem.MolFromSmiles(smiles) if mol is None: return return Chem.MolToSmiles(mol, canonicalTrue, isomericSmilesTrue) df[smiles_a] df[smiles_a].map(canonical_smiles) df[smiles_b] df[smiles_b].map(canonical_smiles) df df[(df[smiles_a] ! ) (df[smiles_b] ! )]MolFromSmiles解析不成功会返回 None说明该 SMILES 字符串不合法或 RDKit 版本不支持该原子类型。清洗后要检查被删掉的行数一般数据集里无效样本不应超过 5%如果超过需要回到原始数据处理环节看是不是编码转换出了问题。isomericSmilesTrue保留手性信息对药物活性预测有意义代价是会稍微增加 token 序列长度。2.3 在 Jupyter Notebook 里做标签分布和序列长度分析数据清洗后先不急着建模在 Jupyter Notebook 里做一轮快速 EDA重点看两个东西标签是否均衡SMILES 长度分布长什么样。标签均衡度决定损失函数和评估指标长度分布决定 Transformer 的max_length参数。print(df[synergy_label].value_counts(normalizeTrue)) len_a df[smiles_a].str.len() len_b df[smiles_b].str.len() for name, col in [(drugA, len_a), (drugB, len_b)]: print(f{name}: median{col.median():.0f}, fp95{col.quantile(0.95):.0f}, max{col.max()}) df[token_len] len_a len_b df[token_len].hist(bins50)如果正样本占比低于 10%训练时会倾向把所有样本预测为负类。处理办法是先不删除负样本在训练时用加权采样或者pos_weight调整损失。SMILES 长度分位数用于设置max_length一般取 p95 再乘 1.2既能覆盖绝大多数分子又不会让序列过长导致显存浪费。Transformer 的注意力复杂度是序列长度的平方把长度从 256 压到 128训练速度能差到 3 倍以上。3. Transformer 预训练模型选型与分子表征抽取3.1 为什么不能从零训练 Transformer 而必须用预训练权重药物组合筛选数据通常只有几千到几万条有效标记样本。在这个规模上从零训练一个多层 Transformer极容易过拟合到训练集的分子写法上而且训练周期很长。预训练模型的价值在于它已经在大规模无标签分子序列上完成了类似“化学语言模型”的预训练学会了原子之间的常见组合模式。下游任务只需要在预训练基础上微调或者干脆把输出向量当作特征使用。面向 SMILES 序列的分子预训练模型已经有多个开源权重常见代表如 ChemBERTa 这类架构。它们的核心做法是在分子数据库上用掩码语言建模训练输入 SMILES输出每个位置的上下文向量。使用方式和普通 BERT 完全一致只是 tokenizer 不同。SMILES 的 tokenizer 必须区分大小写C是碳原子c是芳香碳含义完全不同加载时严禁设置do_lower_caseTrue否则化学语义直接错乱。3.2 用 HuggingFace 接口加载预训练 Transformer 编码器加载预训练模型时使用 HuggingFace 的AutoModel和AutoTokenizer不区分具体模型结构。占用显存的只是编码器部分分类头要自己设计。以下代码可以放在项目入口模块里避免每个 Notebook 重复加载。import torch from transformers import AutoModel, AutoTokenizer MODEL_NAME your_molecule_pretrained_weights MAX_LEN 128 BATCH_SIZE 32 tokenizer AutoTokenizer.from_pretrained(MODEL_NAME) encoder AutoModel.from_pretrained(MODEL_NAME) encoder.eval()MODEL_NAME是占位符按实际拉取的权重名替换。encoder.eval()这一步容易漏如果后面训练时既要抽取特征又要微调整个编码器需要分开处理如果作为特征抽取器使用必须切到 eval 模式否则 BatchNorm 和 Dropout 会引入随机性导致抽出来的表征不稳定。3.3 把分子批量编码成 CLS 向量并做离线缓存做协同预测的常见做法是让两个药物分子共享同一个编码器分别过一遍得到各自的表征。训练阶段每次 forward 都过编码器的做法在显存上很奢侈工程上更稳妥的方案是所有药物分子先做一次离线推理把输出向量缓存成文件训练时直接查表。药物分子总数通常只有几百到几千个离线推理只要几分钟。def encode_smiles_offline(smiles_list: list[str], save_path: str) - torch.Tensor: all_vecs [] for i in range(0, len(smiles_list), BATCH_SIZE): batch tokenizer( smiles_list[i:i BATCH_SIZE], paddingTrue, truncationTrue, max_lengthMAX_LEN, return_tensorspt, ) with torch.no_grad(): outputs encoder(**batch) cls_vec outputs.last_hidden_state[:, 0, :] all_vecs.append(cls_vec.cpu()) result torch.cat(all_vecs, dim0) torch.save(result, save_path) return result vec_a encode_smiles_offline(df[smiles_a].tolist(), data/drug_a_vec.pt) vec_b encode_smiles_offline(df[smiles_b].tolist(), data/drug_b_vec.pt)paddingTrue会把同一批内不同长度的序列补齐到一样长truncationTrue保证超过MAX_LEN的序列被截断。取last_hidden_state[:, 0, :]是为了获得 CLS token 的最终隐状态这个向量在预训练时聚合了整个序列的信息。缓存文件用torch.save存成 pt 格式比每轮训练重新提取能节省大量 GPU 计算时间。要注意 SMILES 序列的顺序不影响分子本身但影响字符位置编码所以训练和推理时要用同一种 SMILES 写法通常统一用 RDKit 规范化后的结果。4. 协同预测模型训练双塔特征融合与关键超参4.1 双塔 Transformer 编码器加 MLP 分类头模型中两个药物共享同一个预训练编码器这比各自微调一套独立参数更好药物 A 和药物 B 在语义空间里使用同一套坐标交互信息更容易被后续融合层捕捉。编码器之上接一个前馈分类头输入是两个 CLS 向量的拼接结果输出协同概率。class SynergyPredictor(torch.nn.Module): def __init__(self, hidden_size: int, dropout: float 0.1): super().__init__() self.encoder encoder self.classifier torch.nn.Sequential( torch.nn.Linear(2 * hidden_size, 256), torch.nn.ReLU(), torch.nn.Dropout(dropout), torch.nn.Linear(256, 1), ) def forward(self, input_a: dict, input_b: dict) - torch.Tensor: vec_a self.encoder(**input_a).last_hidden_state[:, 0, :] vec_b self.encoder(**input_b).last_hidden_state[:, 0, :] fused torch.cat([vec_a, vec_b], dim-1) return self.classifier(fused).squeeze(-1)hidden_size必须与预训练模型配置一致从encoder.config.hidden_size读取不要写死。拼接之后再接 256 维的中间层是兼顾表达力和训练稳定性的选择再大的宽度在几千条样本上容易过拟合。Dropout 放在全连接层之后能有效缓解小样本下的过拟合问题。如果实验发现训练损失下降但验证集 AUC 不涨优先调整这个 Dropout 值。4.2 选择 AdamW 优化器并调整微调学习率预训练模型微调时编码器和分类头的学习率建议分开设置编码器用较小学习率分类头用较大学习率。AdamW 是微调 Transformer 的默认优化器配合线性学习率预热避免训练初期梯度步长太大把预训练权重冲坏。参数推荐值说明base_lr2e-5 到 5e-5预训练编码器层学习率head_lr1e-4 到 3e-4新初始化的分类头学习率batch_size16 到 32显存不足时优先降到 16warmup_ratio0.1前 10% 步数线性升 lrweight_decay0.01AdamW 默认常用值max_grad_norm1.0梯度裁剪防 NaN编码器如果也用 1e-4 以上的学习率预训练学到的分子表示会在前几百步内被破坏后期很难恢复。分类头是随机初始化的需要相对大的学习率才能尽快收敛。梯度裁剪max_grad_norm1.0在长序列训练中几乎必须开启否则个别 drug pair 输入会产生异常大的梯度。4.3 完整训练循环与早停策略训练代码的骨架非常标准化需要注意的关键点有两个每个 batch 传入的是 tokenizer 处理后的字典不能直接传原始字符串验证集上使用 AUC 作为早停指标而不是准确率。协同预测里正负样本不均衡准确率几乎没有参考价值。optimizer torch.optim.AdamW([ {params: model.encoder.parameters(), lr: 2e-5}, {params: model.classifier.parameters(), lr: 2e-4}, ]) scheduler torch.optim.lr_scheduler.LinearLR( optimizer, start_factor0.1, total_iters5 ) criterion torch.nn.BCEWithLogitsLoss() best_auc 0.0 patience 3 no_improve 0 for epoch in range(10): model.train() for batch in train_loader: input_a {k: batch[input_a][k] for k in [input_ids, attention_mask]} input_b {k: batch[input_b][k] for k in [input_ids, attention_mask]} labels batch[label].float() logits model(input_a, input_b) loss criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad() # 验证逻辑省略输出验证集 AUC valid_auc evaluate(model, valid_loader) if valid_auc best_auc: best_auc valid_auc torch.save(model.state_dict(), checkpoints/best.pt) no_improve 0 else: no_improve 1 if no_improve patience: breakBCEWithLogitsLoss内部已经做了 sigmoid 计算模型输出层不用额外激活函数这比手动sigmoid加BCELoss在数值上更稳定。LinearLR的start_factor0.1表示前几步先以十分之一的学习率起步5 步后升到设定值。patience3意味着连续 3 个 epoch 验证集没有提升就停止这个策略能省下大量无效训练时间。5. 评估反直觉点随机划分会高估协同预测效果5.1 同一药物出现在训练集和验证集是隐性数据泄漏药物协同预测任务里有种数据泄漏比标签翻转更隐蔽随机划分数据时某种药物会同时出现在训练集和验证集的不同组合里。模型不一定在学习“这两个分子是否协同”而是在记住“这个药物本身的效果强”。推理阶段出现全新的药物时这类模型表现极其不稳定。解决方式是使用GroupKFold按药物对分组。分组的 key 不能是pair_id而应该是药物标识的组合A_B和B_A要归一化成同一个 group。from sklearn.model_selection import GroupKFold df[pair_group] df.apply( lambda row: _.join(sorted([row[drug_a_id], row[drug_b_id]])), axis1, ) gkf GroupKFold(n_splits5) for train_idx, valid_idx in gkf.split(df, groupsdf[pair_group]): train_df df.iloc[train_idx] valid_df df.iloc[valid_idx]这样划分后验证集里任意药物对都和训练集没有重合。常见的实践结果是随机划分的 AUC 比 GroupKFold 高 5 到 10 个百分点但前者是不可信的。如果你的项目评估结果和文献对不上先检查是不是划分方式不同。5.2 正样本稀少时 AUC-ROC 会掩盖问题要同时看 PR-AUC协同组合在一个大型筛选实验里往往只有 15% 甚至更低的比例。AUC-ROC 对类别不平衡不敏感导致即使正样本全预测错AUC 依然可能徘徊在 0.8 附近。PR-AUC 对假阳性更敏感在正样本稀疏时能更真实反映模型在筛选场景里的可用性。指标对正样本稀疏的敏感度结果解读AUC-ROC低0.85 看起来很好但实际找到的协同组合可能很少PR-AUC高0.3 不代表模型没用但能体现筛选成本的真实下降比例评估代码直接调用 sklearn 的接口同时输出两个指标并用 Bootstrap 采样给出 95% 置信区间防止因为验证集样本量小导致指标波动。from sklearn.metrics import roc_auc_score, average_precision_score import numpy as np def bootstrap_auc(y_true, y_score, n_iter1000, seed0): rng np.random.default_rng(seed) scores [] for _ in range(n_iter): idx rng.integers(0, len(y_true), sizelen(y_true)) if len(np.unique(y_true[idx])) 2: continue scores.append(roc_auc_score(y_true[idx], y_score[idx])) return np.percentile(scores, [2.5, 97.5]) print(fAUC-ROC: {roc_auc_score(y_true, y_score):.4f}) print(fPR-AUC: {average_precision_score(y_true, y_score):.4f}) print(fAUC 95% CI: {bootstrap_auc(y_true, y_score)})Bootstrap 时跳过那些采样后只剩一个类别的迭代这是处理小样本验证集的常用技巧。置信区间如果横跨 0.5说明验证集太小或模型不稳定这时候先别急着调参先检查分组泄漏和样本量。5.3 分级结果比单一阈值更有用协同预测的最后输出不应该只是一个 0 或 1按概率分数分成高、中、低三档对实验筛选更有参考价值。阈值可以按验证集的 PR 曲线选择优先保证某一段召回率再确定逻辑回归分类概率的切分点。保存模型时连同 tokenizer 配置一起打包避免推理环境重建时不匹配。6. 交付落地项目结构、使用说明和 Jupyter Notebook 常见故障6.1 把 Notebook 里的代码整理成 src 包毕业设计或项目交付最忌只交一个.ipynb文件。Notebook 适合做探索性分析和可视化但训练、数据清洗、推理逻辑必须独立成模块。一个可以完整复现的标准目录结构如下project/ ├── data/ │ ├── raw/ # 原始药物组合数据 │ └── processed/ # 清洗后的 CSV 与向量缓存 ├── notebooks/ │ └── 01_eda.ipynb # 探索性分析 ├── src/ │ ├── data_preprocess.py # 清洗与特征构建 │ ├── model.py # SynergyPredictor 定义 │ ├── train.py # 训练入口 │ └── inference.py # 推理脚本 ├── docs/ │ └── API.md ├── requirements.txt └── README.md训练脚本要支持命令行参数至少能通过--batch_size、--lr和--epochs覆盖 Noteboook 里的默认值。这样答辩或演示时直接用命令行跑一遍效果更可信。6.2 使用说明文档里必须写清的四部分README 不需要长篇大论但要保证新人拿到源码包后 30 分钟内能跑通。第一写明 Python 版本和依赖安装命令包括指定 PyTorch 和 Transformers 版本。第二说明数据格式直接放两行 CSV 示例。第三写清训练和推理命令。第四是常见错误表比如内存不足时怎么调 batch size。# 环境 pip install -r requirements.txt # 数据 准备 data/raw/drug_pairs.csv字段包含 drug_a_id, drug_b_id, smiles_a, smiles_b, synergy_label # 训练 python src/train.py --epochs 10 --batch_size 16 # 推理 python src/inference.py --input data/raw/example.csv --ckpt checkpoints/best.pt6.3 Jupyter Notebook 无法运行时的排查顺序Notebook 单元格点击执行后没有任何反应这是最常见也最打击人的问题。先看界面右上角内核状态显示一个实心圆说明内核正忙可能是训练循环死循环或内存耗尽空心圆说明 kernel 已经断连。断连时先尝试重启内核而不是重启整个 Notebook 服务往往问题出在某个导入库把内核搞挂了。Windows 环境还经常出现ImportError: DLL load failed while importing rpds这种报错。这个错误源于rpds-py这个 Rust 扩展的二进制依赖与当前环境不兼容常见于 conda 和 pip 混用时。处理顺序是先升级 pip再强制重装rpds-py如果还在报错用 conda 安装替代版本。python -m pip install --upgrade pip python -m pip install --force-reinstall rpds-py conda install -c conda-forge rpds-py先重装再切换 channel避免在没确认问题前把整个环境重建。所有排错操作完成后打开 Notebook 跑一遍最小冒烟测试载入模型、编码一个 SMILES、执行一次前向传播确认无误后再进入完整训练流程。本文还有配套的精品资源点击获取