☰
AI制药工程化拆解:从分子特征到骨架划分与实验闭环
2026/10/27 2:14:55 网站建设 项目流程

先说一句可能不太讨喜的话:把“AI生物公司”当作一个整体板块去看,是这件事里最容易出错的一步。这四家公司拿出来的模型架构可能完全不同,数据来源可能一个是公开数据库、一个是自建湿实验平台,商业模式可能一个是卖软件订阅、一个是自己推管线到临床。用一个标签去概括它们,就像把“用了数据库的 SaaS 公司”当成一个赛道一样,信息密度几乎为零。

标题里的那句“你投就完了”,我理解成一种情绪表达。这篇文章不打算顺着情绪的节奏往下走,也不会点名推荐任何具体公司或标的,更不构成任何形式的投资建议。我想做的是另一件事:把“AI + 生物医药”这件事拆成一条可以被工程师理解、可以被复现、可以被检验的技术链路,然后告诉你这条链路的哪一段真正难、哪一段是包装、哪一段是深坑。

之所以值得这样写,是因为我在看这类项目的时候,反复遇到同一个问题:技术叙事听起来都非常完整,从大模型到生成式设计到自动化实验室,词都认识,但落到“这个能力到底能不能被另一个团队在半年内复刻”这个具体问题上,大部分人答不上来。答不上来,判断就只能靠感觉,而靠感觉判断,通常就是被叙事带着走。

所以这篇文章的落点是工程视角,不是资本视角。读完之后,你应该能做到三件事:第一,看懂一份 AI 制药、AI 生物类技术材料里的水分在哪;第二,用 RDKit 加一个最简模型,亲手把“分子数据到性质预测”这条最小闭环跑通,从而对“AI 到底在这里做了什么”有一个手感;第三,拿到一份可以用来审视任何这类项目的技术检查清单。

1. 这类项目的技术门槛到底在哪里

先从最容易混淆的地方说起。很多宣传材料会把“我们用 AI 做药物发现”当作一个技术能力的描述,但这句话本身没有信息量。因为从技术上拆,它至少包含四种完全不同的工作,而这四种工作的难度、可复现性、价值密度差得非常远。

第一种是结构预测。给定一条氨基酸序列,预测蛋白质的三维结构。这一类工作在 AlphaFold2 之后基本被“基础设施化”了,开源实现、公开权重、在线服务都有,一个具备基本工程能力的团队可以在几周内把它跑起来。这意味着,结构预测本身已经很难构成技术壁垒,它更像今天的“调用一个成熟的编译器”,属于能力底座而不是差异化。

第二种是性质预测与虚拟筛选。给定一批分子,预测它们的活性、毒性、溶解度、透膜性等性质,用来在海量候选里做初筛。这是工业界用得最多、也最容易被低估的一类工作。它的难点不在模型,而在数据:真实的高质量标注数据往往来自企业内部积累,公开数据集普遍存在分布偏移和标注噪声。一个团队如果只有公开数据,模型指标可以做得很好看,一上真实项目就掉得很厉害,这是行业里非常常见的现象。

第三种是生成式分子设计。用扩散模型、自回归模型或者强化学习去“生成”满足多重约束的新分子。这一类的技术演示效果最好看,因为生成出来的结构图很有冲击力。但它也是水分最大的一块:生成得出来,不等于合成得出来;合成得出来,不等于有活性;有活性,不等于能成药。生成模型很容易陷入“优化奖励函数而非优化真实性质”的陷阱,也就是 reward hacking,模型学到了怎么骗过打分函数,而不是真的找到了好分子。

第四种是实验闭环。把预测结果送进湿实验,拿到实测数据,再回流训练模型。这一类工作最难被复刻,因为它要求团队同时具备干实验和湿实验能力,还要求两者之间有工程化的数据通道。它不性感,不出论文,但它决定了一家公司的模型能不能随时间变强。

我的判断很明确:在这四类工作里,前三类的技术门槛正在快速下降,因为开源生态和基础模型的普及把它们变成了“可用工具”;真正难以复刻的是第四类,也就是数据回路能不能自动转起来。一家 AI 生物公司在技术上值不值得关注,核心不是它用了多大的模型,而是它有没有能力持续生产别人拿不到的、带实验验证的高质量数据。

这个判断会贯穿整篇文章,也是后面所有实操代码存在的理由:只有你亲手跑一遍从分子数据到模型评估的流程,你才会真切地感受到,为什么“数据划分方式”这种看起来微不足道的细节,能直接决定一个模型是真有用还是假有用。

2. AI 生物医药的四层技术栈与核心概念

要判断一个项目的技术含量,先要知道它站在哪一层。我把这条链路拆成四层,从上到下依次是表示层、模型层、实验层和工程层。大部分人只盯着模型层看,而真正拉开差距的往往是表示层和工程层。

2.1 表示层:分子和蛋白到底怎么变成数字

这是最基础也最容易被跳过的一层。计算机不认识“分子”,只认识张量,所以第一步永远是把化学结构编码成数字。常见的三种表示方式,各有各的脾气。

SMILES 字符串是最常见的一种,本质上是用一串字符描述分子的连接关系,比如水是O,乙醇是CCO。它的优点是极度紧凑、人类可读、几乎所有化学数据库都支持;缺点是同一个分子可以写出多种合法的 SMILES,如果不做标准化,同一个化合物在数据里会以多个身份出现,直接污染统计和训练集划分。

分子图把原子当节点、化学键当边,是最贴近化学本质的表示。图神经网络在这个表示上工作,能显式建模局部结构。缺点是构图成本高,而且对三维构象的信息利用有限。

描述符向量是把分子压成一组固定长度的数字,比如分子量、脂水分配系数、氢键供体数、拓扑极性表面积等。它的优点是可以直接喂给任何传统的机器学习模型,训练快、可解释性强、对数据量要求低;缺点是信息损失严重,同一个描述符向量可能对应完全不同的分子。

在真实项目里,这三种表示经常是混用的:用描述符跑快速基线,用分子图跑主力模型,用 SMILES 做生成式任务。判断一个团队是否真的懂行,一个很简单的信号是看它有没有认真处理 SMILES 标准化,以及有没有说明自己的数据划分策略。这两件事做得敷衍的,模型指标基本不可信。

2.2 模型层:四类模型解决四类问题

模型层可以粗略分成四类:结构预测模型、性质预测模型、生成模型、以及近两年兴起的生物基础模型。

结构预测以 AlphaFold 系列和 ESMFold 为代表,把序列映射到三维结构。性质预测以图神经网络和各类描述符加梯度提升树为主,是工业界的主力。生成模型包括扩散模型、变分自编码器、自回归语言模型,用于在化学空间里采样。生物基础模型则是把 Transformer 那一套搬到序列和表达谱上,做统一表征,然后再下游微调。

这里有个认知需要纠正:模型复杂度不等于解决能力。在分子性质预测这种任务上,一个调好参的梯度提升树经常能打败一个没调好的图神经网络,尤其是在标注数据只有几千条的时候。很多团队在数据量不足的情况下上大模型,结果只是在过拟合上花了更多算力。

2.3 实验层:干湿之间的那条缝

实验层是 AI 生物和纯软件 AI 最大的区别。模型的输出不是一份报告,而是一个需要被合成、被测试、被验证的物理假设。这一层涉及化合物合成、高通量筛选、细胞实验、动物实验,周期以周和月计,成本以万为单位。

这带来一个纯软件领域不存在的约束:反馈延迟极长。在推荐系统里,模型上线几小时就能拿到点击反馈;在这里,一轮实验可能要等六周。这意味着算法设计必须考虑样本效率,不能指望用海量试错来收敛。同时也意味着,自动化实验平台的价值被严重低估了——它缩短的不是某一次实验的时间,而是整个回路的周期。

2.4 工程层:最不性感但最致命

工程层包括数据治理、实验记录管理、任务编排、模型版本、可复现性、权限和审计。这些东西在宣传材料里几乎不会出现,但它们决定了一个团队能不能把一次成功的实验变成一百次成功的实验。

我见过太多“模型很强但工程很烂”的案例:训练脚本靠人肉改参数,数据集版本靠文件名区分,评估结果只存在某个人电脑里,换一个人就跑不出同样的数字。这种团队即使短期做出成果,也无法持续迭代。反过来,工程扎实的团队,哪怕模型朴素,只要回路能转,长期就会赢。

下面这张表把四层的关键差异列清楚,方便你对照一份技术材料去定位它讲的是哪一层:

层级典型产出复刻难度常见夸大点
表示层标准化分子库、特征矩阵中声称数据量巨大,但未说明去重与标准化
模型层预测模型、生成模型低到中只报随机划分指标,不报骨架划分指标
实验层实测活性、毒性、药代数据高把少量验证说成闭环
工程层数据管道、实验管理系统高几乎不提,但决定长期上限

3. 环境准备与前置条件

理论说完,接下来动手。这一节的目的不是让你成为一个药物化学家,而是让你亲手把“分子数据到模型评估”这条最小闭环跑通。跑通之后,你对前面那些概念的体感会完全不一样。

运行环境建议如下。操作系统用 Linux 或 macOS 都行,Windows 建议走 WSL2,因为 RDKit 在原生 Windows 上的安装体验时好时坏。Python 建议 3.10 或 3.11,这两个版本和主流科学计算库的兼容性最好。包管理统一用 conda 或 mamba,不要混用 pip 和 conda 装同一个库,这是新手最容易踩的坑之一。

核心依赖只有三个:RDKit 负责化学结构解析和特征计算,pandas 和 numpy 负责数据处理,scikit-learn 负责划分、建模和评估。如果想跑神经网络版本,再加 PyTorch。具体的版本号请以你本地实际解析到的为准,本文重点演示通用思路,不锁定具体版本。

把下面的文件保存为env/environment.yml:

# 文件路径:env/environment.yml name: ai-bio-lab channels: - conda-forge - pytorch dependencies: - python=3.10 - rdkit - numpy - pandas - scikit-learn - matplotlib - jupyterlab - pip - pip: - torch

创建并激活环境:

conda env create -f env/environment.yml conda activate ai-bio-lab python -c "import rdkit; print('rdkit', rdkit.__version__)"

最后一行是验证步骤,能打印出版本号就说明 RDKit 装好了。如果这里报ImportError,先检查是不是在错误的 conda 环境里执行,这是最常见的原因,而不是安装失败。

项目目录结构建议这样组织,后面所有代码都按这个路径来:

ai-bio-lab/ ├── env/ │ └── environment.yml ├── data/ │ ├── raw/molecules.csv │ └── processed/features.csv ├── scripts/ │ ├── featurize.py │ ├── split.py │ └── train.py └── outputs/ └── metrics.json

data/raw/molecules.csv需要两列:smiles和label。label是二分类标签,比如 1 表示有活性、0 表示无活性。数据可以从公开数据集里取,比如 ChEMBL、BindingDB 这类公开的化学与生物活性数据库,它们提供可下载的结构与活性记录。我这里不贴具体下载链接,因为不同任务的筛选条件差别很大,你需要按自己的任务去构造。本文的代码不依赖任何特定数据集,只要能凑出几百到几千条带标签的 SMILES 就能跑。

4. 核心流程拆解:一条最小可验证的闭环

在写代码之前,先把整个流程拆成六步,每一步都说清楚为什么需要它,以及做错了会出现什么后果。这六步是任何分子性质预测项目都绕不开的骨架。

第一步,数据来源与去重。原始数据里最常见的两个问题是重复和矛盾:同一个分子出现多次,或者同一个分子在不同来源下标签不一致。如果不处理,同一个分子可能同时出现在训练集和测试集里,模型看起来学得很好,实际上是在背答案。这一步的产出是一份去重后的、标签一致的候选集。

第二步,结构标准化。把 SMILES 解析成分子对象,去掉盐、去掉溶剂、保留最大片段,再重新生成规范 SMILES。这一步做不好的直接后果是:同一个化合物在你的数据里以三种形态存在,特征计算出来三个不同的值,模型的输入噪声凭空增加。很多人跳过这一步,然后在模型效果不好时去调超参数,方向完全错了。

第三步,特征化。把标准化后的分子转换成模型能吃的数字。用描述符做基线是最稳妥的起点,因为它的计算确定性强、可解释、不依赖大量数据。

第四步,数据划分。这一步是整个流程里最容易被做错、也最能暴露问题的地方。随机划分看起来没有问题,但同一类骨架的分子会被打散到训练集和测试集,模型只要学会“这个骨架大概是什么活性”就能拿到高分,而它对全新的化学骨架可能一无所知。更诚实的做法是按骨架划分,把同一骨架的分子全部放进同一个集合,模拟真实场景下“遇到没见过的化学类型”。

第五步,训练与评估。二分类任务不要只看准确率,因为数据通常极度不平衡,负样本远多于正样本。应该看 ROC-AUC、PR-AUC,以及在虚拟筛选场景下更有意义的富集因子。

第六步,上线与实验验证。模型给出的高分分子需要被送进湿实验。这一步的前提是模型输出要带不确定性估计,否则你无法决定先测哪一批。前 20 个分子测出来全是假阳性,意味着这一轮实验预算白烧了。

这六步里,第二、四、六步是最容易出问题的。第一步和第三步属于工程熟练度问题,多做几次就顺手了;第二步和第四步属于方法论问题,做错了模型指标会虚高,而且虚高得很自然,你很难察觉;第六步属于闭环问题,没有它,前面五步只是一个离线练习。

5. 完整示例代码实现

接下来写代码。三个脚本,分别负责特征化、划分、训练。每个都能单独运行,也都能串起来跑。

5.1 分子标准化与特征化

# 文件路径:scripts/featurize.py import pandas as pd from rdkit import Chem from rdkit.Chem import Descriptors, rdMolDescriptors from rdkit.Chem.SaltRemover import SaltRemover # 统一的特征列顺序,训练和推理必须完全一致 FEATURE_COLUMNS = [ "mol_wt", "logp", "h_donors", "h_acceptors", "tpsa", "rotatable_bonds", "aromatic_rings", "heavy_atoms", ] _salt_remover = SaltRemover() def standardize(smiles: str): """把原始 SMILES 标准化成规范 SMILES,失败返回 None。""" if not isinstance(smiles, str) or not smiles.strip(): return None mol = Chem.MolFromSmiles(smiles) if mol is None: return None # 去掉盐和溶剂,只保留主体结构 mol = _salt_remover.StripMol(mol, dontRemoveEverything=True) if mol is None or mol.GetNumAtoms() == 0: return None # 重新输出规范 SMILES,保证同一分子只有一种写法 return Chem.MolToSmiles(mol, canonical=True) def compute_features(canonical_smiles: str): """计算一组稳定的分子描述符。""" mol = Chem.MolFromSmiles(canonical_smiles) if mol is None: return None return { "mol_wt": Descriptors.MolWt(mol), "logp": Descriptors.MolLogP(mol), "h_donors": Descriptors.NumHDonors(mol), "h_acceptors": Descriptors.NumHAcceptors(mol), "tpsa": Descriptors.TPSA(mol), "rotatable_bonds": Descriptors.NumRotatableBonds(mol), "aromatic_rings": rdMolDescriptors.CalcNumAromaticRings(mol), "heavy_atoms": mol.GetNumHeavyAtoms(), } def build_dataset(raw_csv: str, out_csv: str): df = pd.read_csv(raw_csv) assert {"smiles", "label"}.issubset(df.columns), "原始数据必须包含 smiles 和 label 两列" df["canonical_smiles"] = df["smiles"].map(standardize) before = len(df) df = df.dropna(subset=["canonical_smiles"]) print(f"SMILES 解析失败丢弃:{before - len(df)} 条") # 同一个规范 SMILES 只保留一条,标签取众数,冲突时丢弃 grouped = df.groupby("canonical_smiles")["label"].agg(lambda s: s.mode().iloc[0] if len(set(s)) == 1 else None) grouped = grouped.dropna() print(f"去重后剩余:{len(grouped)} 条") rows = [] for smi, label in grouped.items(): feats = compute_features(smi) if feats is None: continue feats["canonical_smiles"] = smi feats["label"] = int(label) rows.append(feats) out = pd.DataFrame(rows) out = out[["canonical_smiles", "label"] + FEATURE_COLUMNS] out.to_csv(out_csv, index=False) print(f"特征化完成,写入 {out_csv},共 {len(out)} 行") return out if __name__ == "__main__": build_dataset("data/raw/molecules.csv", "data/processed/features.csv")

这段代码有三个地方值得单独说明。第一,standardize里做了盐去除和规范 SMILES 重写,这是保证同一分子只有一个身份的关键。第二,去重逻辑里对标签冲突做了处理:同一个分子如果既有正样本又有负样本的记录,说明标注本身有问题,直接丢弃比强行选一个更安全。第三,FEATURE_COLUMNS定义成常量,是为了保证训练和推理的特征顺序严格一致,这是一个很小的工程细节,但在生产环境里,特征列顺序错位是最难排查的故障之一。

运行方式:

python scripts/featurize.py

5.2 骨架划分:让评估指标说真话

# 文件路径:scripts/split.py import random from collections import defaultdict from rdkit import Chem from rdkit.Chem.Scaffolds import MurckoScaffold def scaffold_split(smiles_list, frac_train=0.8, frac_valid=0.1, seed=0): """ 按 Bemis-Murcko 骨架划分数据集。 同一骨架的分子整体进入同一个子集,避免训练集与测试集共享骨架。 """ groups = defaultdict(list) for idx, smi in enumerate(smiles_list): mol = Chem.MolFromSmiles(smi) if mol is None: continue scaffold = MurckoScaffold.MurckoScaffoldSmiles(mol=mol, includeChirality=False) groups[scaffold].append(idx) # 大骨架优先成组分配,保证各子集规模相对均衡 group_list = sorted(groups.values(), key=len, reverse=True) rng = random.Random(seed) rng.shuffle(group_list) n_total = len(smiles_list) n_train = int(n_total * frac_train) n_valid = int(n_total * frac_valid) train_idx, valid_idx, test_idx = [], [], [] for group in group_list: if len(train_idx) + len(group) <= n_train: train_idx.extend(group) elif len(valid_idx) + len(group) <= n_valid: valid_idx.extend(group) else: test_idx.extend(group) return sorted(train_idx), sorted(valid_idx), sorted(test_idx)

这段代码的核心是MurckoScaffold。它把一个分子拆成“环系统加连接子”的骨架部分,去掉所有侧链。同一骨架的分子在化学空间里往往性质相近,如果它们被随机打散到训练集和测试集,模型只要记住骨架和活性的关联就能拿到高分,但这个模型对全新骨架的泛化能力完全没有被检验。

所以你会在实践中看到一个非常有诊断价值的现象:同一个模型,随机划分下的 ROC-AUC 可能比骨架划分高出一大截。这个差距本身就是信息,差距越大,说明模型对骨架的依赖越强,对真实场景的迁移能力越弱。如果你看到一份材料只报告随机划分的指标,那么它的实际能力大概率被高估了。

5.3 训练与评估

# 文件路径:scripts/train.py import json import sys import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import average_precision_score, roc_auc_score from sklearn.preprocessing import StandardScaler sys.path.append("scripts") from featurize import FEATURE_COLUMNS from split import scaffold_split def enrichment_factor(y_true, y_score, top_frac=0.1): """富集因子:前 top_frac 比例里命中的正样本占比,相对随机抽样的倍数。""" n = len(y_true) k = max(1, int(n * top_frac)) order = np.argsort(-y_score)[:k] hits = y_true[order].sum() baseline = y_true.sum() / n if baseline == 0: return 0.0 return float((hits / k) / baseline) def main(): df = pd.read_csv("data/processed/features.csv") smiles = df["canonical_smiles"].tolist() X = df[FEATURE_COLUMNS].values y = df["label"].values tr, va, te = scaffold_split(smiles, frac_train=0.8, frac_valid=0.1, seed=42) print(f"划分结果 训练/验证/测试 = {len(tr)}/{len(va)}/{len(te)}") scaler = StandardScaler().fit(X[tr]) X_tr, X_va, X_te = scaler.transform(X[tr]), scaler.transform(X[va]), scaler.transform(X[te]) clf = RandomForestClassifier( n_estimators=500, max_depth=None, min_samples_leaf=2, class_weight="balanced", n_jobs=-1, random_state=42, ) clf.fit(X_tr, y[tr]) metrics = {} for name, Xs, ys in (("valid", X_va, y[va]), ("test", X_te, y[te])): prob = clf.predict_proba(Xs)[:, 1] metrics[name] = { "roc_auc": float(roc_auc_score(ys, prob)) if len(set(ys)) > 1 else None, "pr_auc": float(average_precision_score(ys, prob)) if len(set(ys)) > 1 else None, "ef_top10": enrichment_factor(ys, prob, 0.1), "n_samples": int(len(ys)), "n_positive": int(ys.sum()), } with open("outputs/metrics.json", "w", encoding="utf-8") as f: json.dump(metrics, f, ensure_ascii=False, indent=2) print(json.dumps(metrics, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()

这里有几个决策需要解释。第一,StandardScaler只在训练集上 fit,然后应用到验证集和测试集。如果对整个数据集 fit,测试集的统计信息就泄露进训练过程了,这属于典型的数据泄露,会让指标虚高。第二,用了class_weight="balanced",因为活性数据通常是严重不平衡的,正样本可能只占几个百分点,不加权模型会倾向于全部预测为负。第三,除了 AUC,还计算了富集因子,因为在虚拟筛选的真实使用场景里,你只会去合成和测试排名最靠前的那一小批分子,富集因子比整体 AUC 更贴近实际收益。

运行:

mkdir -p outputs && python scripts/train.py

6. 运行结果与效果验证

跑完之后,outputs/metrics.json会输出类似下面的结构。注意,这里的关键不是具体数字,而是你要理解每个数字的含义和它的合理区间:

{ "valid": { "roc_auc": 0.78, "pr_auc": 0.41, "ef_top10": 3.2, "n_samples": 120, "n_positive": 15 }, "test": { "roc_auc": 0.74, "pr_auc": 0.35, "ef_top10": 2.7, "n_samples": 260, "n_positive": 31 } }

怎么判断这个结果算成功?先看三个层面。

第一层,流程是否跑通。终端没有异常退出,metrics.json正常生成,划分后的三个子集样本数加起来等于总样本数,这说明代码逻辑是对的。这一步的验证意义在于排除低级错误,而不是评估模型好坏。

第二层,指标之间的关系是否合理。PR-AUC 在类别不平衡时通常明显低于 ROC-AUC,如果你看到 PR-AUC 反而比 ROC-AUC 高很多,先怀疑标签是不是搞反了,或者正负样本比例计算有问题。富集因子如果接近 1,说明模型排序能力基本等同于随机,这时候不要急着调参,先回去看特征和标签。

第三层,训练集与测试集的差距。如果测试集 AUC 和验证集 AUC 相差很大,常见原因是划分时骨架冲突没有完全消除,或者样本量太小导致波动。如果两者都还不错,但富集因子偏低,说明模型的排序能力集中在中间段,对头部的区分度不够,这在应用上比整体 AUC 低更麻烦。

如果运行失败,第一步应该看什么?我的建议顺序是:先看data/raw/molecules.csv的行数和列名是否正确,再看特征化脚本打印的“解析失败”和“去重后剩余”这两行日志——如果解析失败数占了一半以上,说明 SMILES 格式有系统性问题,比如整列被 Excel 当成日期或者编码错乱;最后才去看模型部分。绝大多数“模型不收敛”的问题,源头都在数据。

还有一点必须提醒:上面这段 JSON 里的数字只是示意结构,不是任何真实数据集的结果。你跑出来的数字可能更高也可能更低,这取决于你的数据集大小、任务难度和正负比例。小样本下的指标波动非常大,几百条数据算出来的 AUC 差异,很多时候在统计上不显著。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
ImportError: No module named 'rdkit'conda 环境未激活,或装到了 base 环境which python查看解释器路径重新conda activate ai-bio-lab,再装一次
特征化时大量 SMILES 解析失败原始数据编码错误、包含非 SMILES 字段、含不可见字符打印前 20 条失败样本人工看清洗列、去除 BOM 和空格、统一编码为 UTF-8
去重后样本数骤降原始数据本身重复率高,或标准化过强对比去重前后行数比例检查标准化逻辑,确认没有误删主体结构
训练集 AUC 极高但测试集很差训练测试存在同一骨架或同一分子的泄漏用骨架划分重跑对比改用骨架划分,或按时间、来源切分
模型全部预测为负类正负样本严重不平衡且未加权查看预测概率分布加class_weight,或做负样本下采样
富集因子接近 1特征无区分度,或标签噪声大检查单特征与标签的相关性补充特征、清洗标签、检查任务定义是否合理
不同机器跑出的指标不一致随机种子未固定、依赖版本差异对比随机种子和依赖版本固定 seed,锁定环境文件
内存溢出一次性加载全量数据或生成过多描述符观察内存曲线分批处理,或减少特征维度

这张表里的每一条,背后都是真实项目里反复出现的问题。其中“训练集 AUC 极高但测试集很差”这一条最值得警惕,因为它不会报错,不会崩溃,只会给你一个看起来很美的数字。在没有做骨架划分的前提下,你在分子数据上得到的任何高分都应该先打个问号。

8. 最佳实践与工程建议

跑通最小闭环只是起点。如果要让这套东西在团队里长期可用,有几条经验值得写下来。

数据版本要和管理代码一样严格。分子数据集的特点是会持续追加和修订,今天跑出的模型和一个月后跑出的模型,如果数据集变了却没有记录,你根本无法复现。建议每个数据集打上版本号和生成时间,特征文件里记录来源数据的哈希值。这一条听起来笨,但它是所有可复现性的基础。

标准化逻辑必须单点维护。训练时的标准化函数和线上推理时的标准化函数必须是同一份代码,不能一个用 Python 一个用 Java 各写一遍。我见过因为两边对盐的处理不一致,导致线上输入的特征分布和训练时完全错位的案例,表现是模型线上效果断崖式下跌,排查了两周才定位到。

评估指标要和业务目标对齐。如果这套模型的用途是从一百万分子里挑出五十个送去合成,那么整体 AUC 的意义有限,真正要看的是头部富集和命中成本。反过来,如果用途是对接一个更大的排序系统,那么概率校准就比排序能力更重要。指标选错,模型优化方向就会错。

必须输出不确定性。在湿实验成本极高的场景下,模型只给一个分数是不够的,你需要知道这个分数有多可信。随机森林可以用树间方差估计不确定性,梯度提升可以用分位数回归,深度模型可以用蒙特卡洛 dropout 或者集成。没有不确定性,你无法决定实验预算怎么分配。

划分策略要写进文档。骨架划分、时间划分、来源划分,对应的是三种不同的泛化假设。选哪种取决于你上线后遇到的分布变化是什么形态。这件事必须显式说明,不能只在代码里默默实现。

安全与合规不能事后补。化学与生物数据涉及实验室安全、数据授权和使用边界。所有涉及实际合成与测试的环节,都必须在有资质、有审批、有安全规程的环境下进行,遵循最小权限原则,实验方案需经专业审查。数据和模型的对外发布也需要经过合规评估。这部分不是技术问题,但它是技术方案能不能落地的前提。

实验闭环要有明确的失败处理机制。预测被实验证伪是常态,不是事故。关键是要把证伪结果结构化地记录下来,回流到训练集里作为负样本。如果证伪结果只是散落在实验记录的备注里,那这个回路实际上没有闭合。

9. 用工程视角审视这类项目的检查清单

回到最初的话题。既然这篇文章不推荐任何标的,那它至少要给你一个可以反复使用的工具。下面这份清单,是我看任何 AI 生物、AI 制药类项目时会挨个问的问题,你也可以拿去对照。

第一问,数据从哪来,能不能自己持续生产。如果数据全部来自公开数据库,那么所有竞争对手和你看到的是同一批数据,模型差异只来自调参,这种优势非常脆弱。如果数据来自自建实验平台,那么要追问产能:一个月能做多少个化合物的实测,数据回流周期多长,历史积累有多少条带实测标签的记录。

第二问,模型是自研还是调用。调用公开模型不丢人,工业界大量使用现成工具是合理的。但你要区分“用工具解决问题”和“把工具当卖点”。区分方法很简单:让对方讲一个模型失败的具体案例,以及他们是怎么定位和修复的。真做过的人会讲出很细的细节,没做过的人只能讲架构图。

第三问,评估是怎么做的。这是最能暴露水平的一问。只报随机划分指标的,基本可以判定为对外叙事;能说清楚骨架划分、时间划分、以及为什么选这种划分的,才是在认真做这件事。再进一步,问他们的模型在多少比例的新骨架上是失效的,这个问题会直接筛掉大部分人。

第四问,闭环里最慢的一环是什么。任何做这类项目的人都应该能立刻回答这个问题。如果答案是“合成”,那他们应该有合成能力或者稳定的合作方;如果答案是“数据标注”,那他们应该有标准化的实验记录体系;如果答不上来,说明闭环还没真正建立。

第五问,工程化程度。问三个具体问题:训练脚本能不能一键复现,数据集有没有版本管理,模型上线后怎么监控效果漂移。这三个问题的答案,基本就决定了这个团队是“研究型”还是“可交付型”。

第六问,团队构成是否匹配。这类项目必须同时有计算和实验两类人,而且这两类人需要有共同的沟通语言。如果一个团队全是算法背景,那实验层要么缺位要么外包,闭环很难真正形成;如果全是实验背景、计算只是外包,那模型迭代速度会非常慢。

这六个问题不需要高深的专业知识,但它们能把“叙事”和“能力”分开。我的经验是,越是对技术有把握的团队,回答这些问题时越会主动讲限制和失败;越是靠叙事驱动的项目,回答越倾向于用更大的词。

回到那条最小闭环。你现在手上有了一份能跑的代码,有了一个能看的指标,有了一套能问的问题。下次再看到“AI 重新定义药物发现”这样的表述时,你不必立刻判断它真假,你只需要在心里把它拆成四层,然后问一句:这条回路,上一次真正转起来是什么时候,转一圈要多久。这个问题的答案,比任何一份商业计划书里的市场规模数字都更能说明问题。

如果你想把这条链路继续往下做,下一步值得深入的方向有三个:一是把描述符换成分子图,对比一下同样的数据下泛化能力的差异;二是引入不确定性估计,做一个主动学习的模拟实验,看看用同样的实验预算能多找到多少正样本;三是把骨架划分的粒度从 Bemis-Murcko 换成更贴近真实化学空间的相似性聚类划分。这三个方向都不需要额外的实验资源,但在你自己的数据集上跑一遍,对“AI 在这些任务上到底能做到什么程度”的理解会完全不同。代码建议保留下来,换数据集时直接用,比重新写一遍划算。

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

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

立即咨询