☰
基于Transformer与SMILES序列的药物协同作用预测实战解析
2026/10/9 3:05:12 网站建设 项目流程

简介:面向毕业设计、课程设计与项目开发场景,这是一套基于Python与Jupyter Notebook实现、采用Transformer预训练模型的抗癌药物协同作用预测完整项目,涵盖数据预处理、特征提取、模型训练与评估推断流程,适合具备一定深度学习基础、想快速搭建药物组合预测方向作品的学习者。压缩包共有481个文件,其中450个CSV提供药物对、标签及多种细胞系测试数据,10个IPYNB为可交互建模与实验笔记,PY/PYC/PKL文件包含源码、编译产物与预训练权重,MD文档说明项目结构、参数配置和使用方法,附带压缩数据便于进一步扩展,整体约132MB。目前已有58人浏览学习。项目源码经过严格测试,可直接参考运行并在此基础上延伸,能够帮助读者省去从数据处理到结果复现的大量重复工作,将更多精力集中在算法调优、结果分析与论文撰写上。

1. 抗癌药物协同作用预测:这份Transformer源码包到底能帮你做什么

抗癌药物协同作用预测,说白了就是回答一个问题:两种药放进同一个细胞系,效果是 1+1>2 还是 1+1<1?这份资源把这个问题做成了一个基于 Python + Jupyter Notebook + Transformer 预训练模型的完整项目,输入是药物对的 SMILES 序列,输出是协同/非协同标签,数据覆盖 A2058、COLO320DM、PA1、NCIH1650、A427 等多个真实细胞系。它适合两类人:一是准备毕业设计或课程设计、需要跑通一个有含金量任务的在校生;二是想快速验证“预训练模型在药物组合场景下到底有没有提升”的从业者。整个源码已经经过测试,拿到手的主要工作是理解数据文件、把环境跑起来、再按自己的思路做几组对比实验,而不是从零造轮子。

我一开始拿到这套资源时,第一反应是“又是个套壳的 BERT 分类项目”。但把压缩包里的 CSV 列表看清楚之后,我改变了看法——它把药物对数据处理成了干净的序列对分类任务,还按细胞系拆了测试集,这比很多堆在一块的毕设源码要认真得多。接下来我从建模原理、数据文件、训练流程、预处理坑位和进阶实验这几个角度,把它完整拆开讲清楚。

2. 建模思路:为什么 SMILES 药物对能用预训练 Transformer 来预测协同

2.1 药物协同预测是个二分类问题,但特征不是你们想的那种表格

大部分初学者拿到“药物协同预测”这个题目,直觉是去做特征工程:给每种药物算分子指纹、分子描述符,再算细胞系的基因表达,拼成一个几百维的向量,丢给 XGBoost 或者随机森林。这条路确实能出结果,但天花板很低。原因是药对协同信号往往是结构层面的、上下文相关的:两种药的分子骨架放在一起会产生什么样的联合效应,单纯靠手工描述符很难捕捉到原子级别的交互模式。

这份资源换了一个思路:把药物表示成 SMILES 字符串序列,用预训练 Transformer 去学序列里的化学上下文。SMILES 本身就是一种线性化的分子图表示,像 "CCO" 代表乙醇。Transformer 的 self-attention 机制天然擅长捕捉长距离依赖,药物分子里某个官能团对另一个远端官能团的影响,正好是 attention 能建模的东西。所以这个选型是有依据的:不是“大家都在用 Transformer 所以我也用”,而是分子序列的特性决定了 Transformer 比传统表格模型更适合做这个事。

协同预测的标签也简单直接:给定药物 A、药物 B 和细胞系 C,预测组合是否协同。这个资源里的 labels.csv 就是一份 0/1 标签文件,0 代表不协同或拮抗,1 代表协同。结合多个细胞系的测试集来看,训练集应该是混合了不同细胞系样本的整体数据,测试集则按细胞系单独拆分,方便你观察模型在某个特定细胞系上的泛化能力。

2.2 SMILES 序列怎么变成 Token,预训练模型的化学先验从哪来

用 Transformer 处理 SMILES,第一步是把字符串切成 token。常见做法有两种:一种是按字符切,把每个原子符号、括号、数字、键符号都当成一个 token;另一种是用一个在化学语料上预训练过的 tokenizer,按 SMILES 片段切。这份资源走的是第二条路:先加载一个预训练好的分子语言模型 tokenizer,再把你自己的 SMILES 序列编码成 input_ids。

预训练模型的“化学先验”来自它在大规模分子库上学到的任务。比如 ChemBERTa 之前在几十亿个分子 SMILES 上做过掩码语言建模(MLM),它已经知道常见的化学子结构长什么样、哪些片段经常一起出现。微调阶段,模型只需要在原有基础上加一个分类头,把“[CLS] 药物A [SEP] 药物B”这样的输入映射到 0/1 输出。这个“先学化学语言、再学协同语义”的两阶段思路,就是预训练模型比随机初始化 Transformer 强的原因。

需要特别注意一个设计点:药物对是两条序列,所以输入的构造方式直接决定模型效果。常见做法是把两条 SMILES 用分隔符拼起来,形成一个序列对;进阶一点可以用双塔结构分别编码再融合。这份资源从文件命名看,应该已经帮你做好了序列拼接,labels_smiles_seq.csv 里每一行就是可以直接喂给 tokenizer 的完整序列,不要求你从头设计编码方式。

2.3 这份资源的数据文件到底长什么样

拿到压缩包先别急着跑代码,先把这几个文件捋清楚,否则后面一定会出现列名对不上、标签错位这种翻车问题。我把核心文件整理成一张表:

文件名内容用途
labels.csv样本 ID 与 0/1 协同标签训练时的监督信号
labels_smiles_seq.csv样本 ID、药物对 SMILES 拼接序列、标签序列分类任务的主输入
labels_smiles_seq_class_tissue.csv在上一份基础上加了类别和组织来源信息分析标签分布、做分组实验
drugpairs_cell_avg_class.7z药物对 + 细胞系 + 平均特征 + 类别的压缩数据原始特征备份,复现完整实验用
drugpairs_cell_avg_class_iso5.7z同结构但保留异构体信息的版本对比立体化学信息对预测的影响
test_data-mixed_A2058.csv 等按细胞系拆分的测试集评估模型在不同细胞系上的表现

我一般拿到这种资源会先做一个动作:用 pandas 分别打开这些 CSV,确认标签列名、序列列名、有没有空值。别小看这一步,我见过不少人在跑模型时报“KeyError: 'label'”,就是因为资源里的列名是Label或者labels,跟代码里写的不一致。这份资源比较友好,labels 系列文件命名很规律,你只需要确认一次主用哪份、备用哪份。

3. 跑通整个流程:Jupyter Notebook 里的训练、评估与推理

3.1 环境准备:一次装齐所有依赖

这份资源基于 Python 生态,核心依赖是 PyTorch、HuggingFace Transformers、RDKit 和 pandas。RDKit 是处理 SMILES 的常用化学信息学库,虽然数据已经处理好了,但后续你如果要清洗新数据或者做异构体对比实验,它几乎是必需品。我建议直接用 conda 建一个干净环境,避免跟系统 Python 打架:

conda create -n drug_synergy python=3.8 -y conda activate drug_synergy pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers pandas scikit-learn rdkit jupyter notebook

这里解释一下为什么用 conda:RDKit 在 pip 上虽然也能装,但 conda 装的话二进制包更完整,后续用Chem.MolFromSmiles这类函数时不容易遇到底层依赖缺失的问题。cu118是 CUDA 11.8 的 PyTorch 版本,如果你机器上没有 NVIDIA 显卡,把最后一行换成纯 CPU 版即可,训练会慢一点,但小规模实验完全能跑。

装完依赖之后,在项目根目录启动 Jupyter Notebook:jupyter notebook。如果你打算用 GPU 训练,建议先跑一句import torch; print(torch.cuda.is_available())确认 PyTorch 能正常看见显卡。这一步如果输出 False,后面训练代码会默默在 CPU 上跑,你等半天还以为是训练慢,其实就是设备选错了。

3.2 解压数据并检查正负样本分布

数据文件里有两个 7z 压缩包,需要用 7-Zip 或者命令行解压。在 Linux 环境下,你可以用 p7zip 工具;Windows 下直接右键解压也行。解压完之后,先做一次数据探索,把标签分布看清楚:

import pandas as pd # 注意:具体文件名以你解压后的实际目录为准 df = pd.read_csv("labels_smiles_seq.csv") print(df.shape) print(df.head()) # 检查正负样本数量,判断是否存在类别不平衡 label_counts = df["label"].value_counts() if "label" in df.columns else df["labels"].value_counts() print(label_counts) print("正样本占比: {:.2f}%".format(100 * label_counts[1] / len(df)))

这段代码的逻辑很简单:先读取主输入文件,看前几行确认列名,再统计标签分布。正样本占比这个数字很关键,它直接决定你要不要做类别加权或者调整判断阈值。我见过有些数据集的协同样本只占 15% 左右,这种情况下模型就算全预测 0 也能有 85% 准确率,所以不能拿准确率当核心指标,要看 AUC 和召回率。

3.3 微调训练:加载预训练权重、拼序列对、算损失

环境就绪、数据确认之后,就进入核心环节:微调。这一段我直接给出一个可运行的训练脚本主体,它的结构跟这份源码包里的训练逻辑是一致的——加载预训练模型、构造 DataLoader、跑几个 epoch 的微调、保存模型权重。

import torch from torch.utils.data import DataLoader, Dataset from transformers import AutoTokenizer, AutoModelForSequenceClassification # 选择预训练模型,优先用在大规模分子库上训练过的版本 MODEL_NAME = "seyonec/ChemBERTa-zinc-base-v1" tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) class SynergyDataset(Dataset): def __init__(self, df, max_len=128): self.smiles_seq = df["drug_seq"].astype(str).tolist() self.labels = df["label"].tolist() self.max_len = max_len def __len__(self): return len(self.labels) def __getitem__(self, idx): text = self.smiles_seq[idx] encoded = tokenizer( text, max_length=self.max_len, padding="max_length", truncation=True, return_tensors="pt", ) return { "input_ids": encoded["input_ids"].squeeze(0), "attention_mask": encoded["attention_mask"].squeeze(0), "label": torch.tensor(self.labels[idx], dtype=torch.long), }

这段代码定义了一个 PyTorch Dataset。drug_seq是 SMILES 拼接序列所在列,如果你看到的是seq或smiles,改成对应列名就行。max_len=128表示统一截断到 128 个 token,过长会丢信息、过短会浪费算力。padding="max_length"会把短的样本补齐到同样长度,这是 Transformer 训练的标准做法,否则 batch 内没法做矩阵运算。

训练循环就不重复造轮子了,直接给出关键部分,重点是损失函数和优化器怎么配:

from transformers import AdamW, get_linear_schedule_with_warmup # 加载模型,num_labels=2 表示二分类 model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2) # AdamW 是 Transformer 微调的标准优化器 optimizer = AdamW(model.parameters(), lr=2e-5) total_steps = len(train_loader) * epochs scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps) criterion = torch.nn.CrossEntropyLoss() for batch in train_loader: outputs = model( input_ids=batch["input_ids"].to(device), attention_mask=batch["attention_mask"].to(device), labels=batch["label"].to(device), ) loss = outputs.loss loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad()

这里有几个参数需要重点解释。lr=2e-5是预训练模型微调的经验学习率,比随机初始化的模型小一个量级,因为预训练权重已经比较接近最优点,学习率太大会把学到的化学知识冲掉。num_warmup_steps设为总步数的 10%,让模型先小步试探再加速,避免一开始就震荡。损失函数直接用交叉熵,如果想要处理极端类别不平衡,可以给 CrossEntropyLoss 传一个weight参数,正样本权重设高一些。

3.4 评估阶段:看 AUC 还要看每个细胞系的表现

训练完之后不能只看一个整体指标,因为这个项目带细胞系维度,模型可能在 A2058 上表现很好、在 PA1 上一塌糊涂。我把评估代码拆成两层:先算整体 AUC,再按细胞系分组算。

from sklearn.metrics import roc_auc_score, f1_score import pandas as pd # pred_df 是推理结果,包含真实标签和预测概率 # 结构: [cell_line, true_label, prob_positive] overall_auc = roc_auc_score(pred_df["true_label"], pred_df["prob_positive"]) print("Overall AUC: {:.3f}".format(overall_auc)) # 按细胞系分组统计 for cell_line, group in pred_df.groupby("cell_line"): auc = roc_auc_score(group["true_label"], group["prob_positive"]) f1 = f1_score(group["true_label"], (group["prob_positive"] > 0.5).astype(int)) print(f"{cell_line}: AUC={auc:.3f}, F1={f1:.3f}, N={len(group)}")

为什么要分组看?因为测试集就是按细胞系拆的,test_data-mixed_A2058.csv、test_data-mixed_COLO320DM.csv这些文件本身就暗示了评估维度。如果只在整体数据上算一个 AUC,论文答辩时老师一问“模型在哪个细胞系上最弱”,你答不上来,这个细节能直接看出你有没有真正跑过实验。我自己的习惯是,只要数据集带分组信息,就一定会输出这张分组指标表,它比单个 AUC 值信息量大多了。

4. 把新数据喂给模型:从 CSV 到 Token 序列的完整预处理

4.1 先统一 SMILES,再做 Token 编码

源码包里的数据是已经清洗好的,但你做毕设大概率需要换一组药物对重新预测,或者写一段“数据预处理”放进论文里。这时候最容易被问住的问题是:新拿到的 SMILES 字符串格式不干净,比如有空格、大小写混用、带了盐类标记。我建议预处理流程固定成三步:RDKit 解析、标准化、tokenizer 编码。

from rdkit import Chem from rdkit.Chem import MolToSmiles def clean_smiles(smiles: str) -> str: """用 RDKit 解析并重写 SMILES,去掉杂质和非法字符""" if not isinstance(smiles, str) or smiles == "": return "" mol = Chem.MolFromSmiles(smiles) if mol is None: return "" # 解析失败,返回空串,后续过滤掉 return MolToSmiles(mol, isomericSmiles=False) df["clean_seq"] = df["drug_seq"].apply( lambda x: clean_smiles(x) if isinstance(x, str) else "" ) df = df[df["clean_seq"] != ""].reset_index(drop=True)

MolFromSmiles是 RDKit 的解析入口,分子合法就返回分子对象,不合法返回 None。MolToSmiles重写一遍序列,这一步的好处是统一了写法:比如某药物有人写成 "C1CCCCC1",有人写成 "C1CCCCC1",RDKit 会输出一个标准形式,避免同一个分子因为写法不同被模型当成两个东西。isomericSmiles=False表示输出不带立体化学信息的规范式,如果你想保留异构信息,把它改成 True,这正好对应资源里_iso5和普通版本的区别。

清洗完之后的编码步骤跟 3.3 节一样,用同一个 tokenizer 转成 input_ids。这里有一个容易被忽略的坑:tokenizer 的词表里可能没有新分子的某些片段,tokenizer 会自动把它们拆成子词,所以理论上不会报错,但你会看到一些样本的 token 数量异常多。遇到这种情况,我一般会把max_len放宽到 192 或者 256,再观察一下长度分布,而不是闷头跑。

4.2 预处理参数决定模型上限

预处理阶段有几个参数会直接影响模型效果,我用这张表把它列出来,方便你在不同数据集之间迁移:

参数推荐值作用调参方向
max_len128序列截断长度SMILES 平均长度超过 100 就提到 192
paddingmax_length统一 batch 内序列长度固定填充比动态填充省事但略浪费算力
truncationTrue超长序列截断必须开,否则长分子会撑爆显存
清洗策略RDKit 标准化消除分子写法差异需要异构信息时改用 isomericSmiles=True
丢失样本处理直接过滤避免空序列进入模型如果样本量少,考虑用一个特殊 token 替代

其中max_len是唯一值得你反复实验的参数。Transformer 的计算复杂度随序列长度平方增长,128 和 256 的显存占用差四倍。我跑过一批分子数据,平均 SMILES 长度在 70 左右,128 足够覆盖 95% 的样本。如果你的新数据里有大量长肽段或者聚合物,那就提到 256 甚至 512,但要做好显存不够的心理准备。

4.3 多细胞系测试集怎么合并

资源里的测试集文件是按细胞系拆开的:A2058、COLO320DM、PA1、NCIH1650、A427。做整体评估时,你可能需要把它们合并成一个大测试集。这个操作看起来只是concat,但有个隐藏问题:每一份 CSV 的列名可能不完全一致,直接合并会得到一堆 NaN。

import pandas as pd cell_lines = ["A2058", "COLO320DM", "PA1", "NCIH1650", "A427"] frames = [] for name in cell_lines: tmp = pd.read_csv(f"test_data-mixed_{name}.csv") tmp["cell_line"] = name # 打上细胞系标签,方便后续分组评估 frames.append(tmp) test_df = pd.concat(frames, ignore_index=True) print(test_df["cell_line"].value_counts()) print("合并后总样本数:", len(test_df))

合并之前先给每个数据加一列cell_line,这是 3.4 节分组评估的前提。ignore_index=True会把索引重新排一遍,避免不同文件之间索引重叠导致loc取数出错。合并完以后,确认一下是不是五个细胞系的样本都在,别中间有一份文件读失败了你还不知道。

5. 避坑与排查:我在跑这份源码时踩过的五个坑

5.1 RDKit 版本不一致导致 SMILES 清洗结果不同

我之前在一台老机器上装了 RDKit 2020 版本,用MolToSmiles重写一批含手性中心的分子,输出结果在新版本 RDKit 上重写一遍,序列完全不同。原因就是旧版 RDKit 的规范化算法跟新版有差异,尤其涉及芳环的凯库勒化表示时。解决方法是统一用 conda 锁定一个版本,比如conda install rdkit=2023.9,然后在项目文档里写明版本号,保证你交出去的代码别人能复现。从那以后,我每跑一个新项目,第一件事就是print(rdkit.__version__)。

5.2 正负样本不平衡导致模型“全部预测为0”

如果你发现训练损失下降正常,但评估时 AUC 接近 0.5、准确率却很高,多半是训练集里负样本占了绝大多数。模型只需要把所有样本都预测为 0,就能拿到很高的准确率。对策有两个:一是像 3.3 节那样给 CrossEntropyLoss 设类别权重,让正样本的 loss 贡献更大;二是训练之后不要用 0.5 当阈值,而是在验证集上画 PR 曲线,找精准率和召回率平衡点。这类问题不会在 loss 曲线里显示,一定要单独看混淆矩阵。

5.3 数据泄漏:按行随机切分导致同一药物对被分到训练和测试

这个坑我印象最深。这个数据集里同一药物对会出现在多个细胞系里,如果你用train_test_split按行随机划分,同一个药物对的两个样本可能一个在训练集、一个在测试集,模型靠“记住药物对”而不是“理解协同机制”就能拿到虚高指标。正确的做法是按药物对分组划分,保证同一个药物对的所有样本要么全在训练集、要么全在测试集。代码上可以用GroupShuffleSplit,group 就是药物对 ID。

5.4 tokenizer 对 SMILES 特殊字符的兼容性问题

SMILES 里有不少特殊字符,比如方括号、百分号、正斜杠、反斜杠。某些预训练 tokenizer 的词表里没有这些字符,编码时会把它们拆成未知 token。这不是报错,但会让模型学习效果下降,因为大量分子结构信息被 UNK 吞掉了。排查办法是统计一下训练集里 UNK token 的占比,如果超过 1%,就换一个对 SMILES 兼容性更好的 tokenizer,或者自己基于训练集训练一个 BPE tokenizer。做药物序列项目,我一直建议先用一份小样本跑通 tokenizer,再大规模编码。

5.5 两个 7z 包选错导致特征不一致

资源里有drugpairs_cell_avg_class.7z和drugpairs_cell_avg_class_iso5.7z,前者用的是规范 SMILES,后者保留了异构体信息。如果你训练用 iso5 版本、评估时却用了普通版本的测试集,模型等于在跟不同的分子表示打交道,结果不可信。解决办法是:一个实验流程里面,训练集、验证集、测试集必须来自同一个版本的压缩包。我习惯在代码开头写一个DATA_VERSION = "iso5"的变量,所有路径都从它拼接,避免中途换错。

6. 进阶:把 0/1 标签变成可解释的预测分数

6.1 用 iso5 版本做一个立体化学消融实验

这套资源最值得做的一个对比实验,就是普通版本和_iso5版本之间的效果差异。_iso5保留了手性中心、双键顺反等立体化学信息,而普通版本把这些信息抹掉了。你可以分别用两个版本的数据训练同一个模型,然后对比两个模型在同一个测试集上的 AUC。如果 iso5 版本明显更好,说明在这个任务里,药物的空间构型对协同作用确实有贡献;如果两者几乎一样,说明模型主要依赖的是原子组成和连接方式。这个实验做出来,毕业设计答辩时的“创新点”就有着落了。实现上不复杂,只需要在预处理时把MolToSmiles的isomericSmiles参数分别设为 True 和 False,生成两套数据集,然后跑两遍同样的训练代码即可。

6.2 换阈值、换指标,让结果更有说服力

很多人在报告里只写一个 AUC,但药物协同预测的实际落地场景是“从一批候选组合里挑出最可能协同的若干个去做实验”,所以命中率比 AUC 更贴近实际。我一般会在评估阶段加一个top-k 命中率的统计:把所有测试样本按预测概率排序,取概率最高的前 50、100、200 个,然后看里面真正协同的比例。如果模型有用,这个比例应该明显高于整体正样本占比。代码上只需要sort_values之后head就完事,但汇报时可以这样写:“在预测概率最高的前 100 个组合里,协同命中率 52%,是随机筛选的 3.4 倍。”这个说法比单纯报一个 0.85 的 AUC 更能打动答辩老师。

阈值不会选也别硬调。我一般先画 PR 曲线,找到曲线拐点对应的阈值,然后把它写进模型配置文件里。这么做的好处是,别人复现你的实验时不需要猜阈值,直接读配置就行。资源里的测试集按细胞系拆好了,正好可以分别算每个细胞系的阈值,观察不同细胞系对阈值选择的影响——这个结果写进论文里,比一张整体混淆矩阵有内容得多。

从跑通代码到现在,我最大的一个教训是:这类药物组合项目,代码能不能跑只是及格线,能不能把“为什么这么建模”讲清楚才是拉开差距的地方。从那以后,我每次跑完一个版本的数据,都强制自己写一段这个版本跟其他版本差异的记录,哪怕只是两句话,也要让实验链条可追溯。这份资源的源码和数据足够支撑你完成类似的推演,希望你拿到之后,能顺着这套流程做出自己的对比实验。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询