ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RDKit 分子多样性挑选实战:rdSimDivPickers 模块的 MaxMin、Leader 与层次聚类算法全解析

RDKit 分子多样性挑选实战:rdSimDivPickers 模块的 MaxMin、Leader 与层次聚类算法全解析
  • 科学计算
  • 科研
  • 机器学习

【免费下载链接】rdkit

The official sources for the RDKit library

项目地址:https://gitcode.com/gh_mirrors/rd/rdkit
点击查看免费下载

导读

本文围绕 RDKit 的rdkit.SimDivFilters.rdSimDivPickers模块展开,它是 RDKit 官方提供的多样性/相似性挑选(diversity & similarity picking)C++ 扩展模块,适用于虚拟筛选、化合物库去冗余、代表性化合物子集选取等典型场景。读完本文,你将掌握MaxMinPicker、LeaderPicker、HierarchicalClusterPicker三大核心类的完整 API、参数语义与算法原理,并能在自己的化合物指纹数据上直接构造距离矩阵、运行挑选并校验结果。

模块定位:一个面向多样性挑选的 C++ 扩展

rdSimDivPickers是rdkit.SimDivFilters包下的核心扩展模块。在 Python 侧,rdkit/SimDivFilters/__init__.py通过from rdkit.SimDivFilters import rdSimDivPickers导入该扩展,并将其中全部成员重新导出(from rdkit.SimDivFilters.rdSimDivPickers import *);如果扩展不可用(例如编译时未启用 Python 包装),则置为None以保证包可以正常导入。

该模块的 Python 绑定入口定义在 Code/SimDivPickers/Wrap/rdSimDivPickers.cpp,其模块级 docstring 为 "Module containing the diversity and similarity pickers",内部依次包装三个类:

  • MaxMinPicker:基于 MaxMin 算法的多样性挑选器;
  • LeaderPicker:基于 Roger Sayle 提出的 Leader 算法(球排除法的类似物)的挑选器;
  • HierarchicalClusterPicker:基于层次聚类的多样性挑选器。

C++ 侧的实现位于 Code/SimDivPickers 目录:DistPicker.h定义了抽象基类与下三角距离矩阵的索引函数,MaxMinPicker.h、LeaderPicker.h、HierarchicalClusterPicker.h分别实现三种挑选策略,该目录的 CMakeLists.txt 将三个源文件编译为SimDivPickers库,并在启用包装时递归构建Wrap(Boost.Python)与nbWrap(nanobind)两套绑定。

三种挑选器都共享同一个抽象基类DistPicker(见 Code/SimDivPickers/DistPicker.h),其虚函数pick(distMat, poolSize, pickSize)约定:

  • distMat:一维下三角距离矩阵(double 数组),元素个数应为poolSize*(poolSize-1)/2;
  • poolSize:候选池大小;
  • pickSize:需要挑选的个数(不得超过poolSize);
  • 返回值为被选中的元素下标(RDKit::INT_VECT)。

矩阵下标通过getDistFromLTM换算:i==j返回 0;i>j时取distMat[i*(i-1)/2 + j]。

MaxMinPicker:最大化最小距离的贪心挑选

MaxMinPicker是使用频率最高的多样性挑选器,对应算法文献为 Ashton 等发表于Quant. Struct.-Act. Relat., 21 (2002), 598-604的 MaxMin 方法。其核心思想记录在 Code/SimDivPickers/MaxMinPicker.h 的类注释中:

  1. 用合适的种子化合物初始化子集,置x = 1;
  2. 对数据集中剩余的每个化合物,计算它与子集中每个化合物之间的距离(不相似度),并保留每个化合物对应的最小距离;
  3. 选取"最小距离最大"的那个化合物移入子集;
  4. x = x + 1,若x < k则回到第 2 步。

最终结果是"两两距离尽可能大"的k个化合物,这正是多样性子集选取的核心目标。

Pick:基于完整距离矩阵

Python 绑定定义在 Code/SimDivPickers/Wrap/MaxMinPicker.cpp,签名与参数如下:

MaxMinPicker().Pick(distMat, poolSize, pickSize, firstPicks=(), seed=-1)
参数类型默认值说明
distMatnumpy 一维数组(float64)必填下三角距离矩阵
poolSizeint必填候选池大小
pickSizeint必填挑选个数,必须小于poolSize(否则抛出ValueError)
firstPicks序列()预先指定的首批种子下标
seedint-1随机数种子;小于 0 时使用系统随机源播种

包装层要求distMat必须是 numpy 数组(PyArray_Check检查失败即抛ValueError),并通过PyArray_ContiguousFromObject将其转换为连续的一维 double 数组后才进入 C++ 的pick()。C++ 侧会校验:池为空、pickSize > poolSize或firstPicks中下标超出poolSize时均抛出ValueErrorException。未提供firstPicks时,第一个种子由boost::mt19937随机数生成器在[0, poolSize-1]内随机选取(Code/SimDivPickers/MaxMinPicker.h 中lazyPick的实现)。

LazyPick 与 LazyBitVectorPick:按需计算距离

对于距离矩阵过大、无法一次性驻留内存的场景,MaxMinPicker 提供"惰性"接口——由用户提供一个距离函数,算法按需调用:

  • LazyPick(distFunc, poolSize, pickSize, firstPicks=(), seed=-1, useCache=None):distFunc接收两个下标(i, j)并返回两点间距离。绑定 docstring 特别注明:实现内部会缓存距离值,客户端无需也不应自行缓存;useCache参数已被废弃并忽略(调用时会打印 warning)。
  • LazyBitVectorPick(objects, poolSize, pickSize, firstPicks=(), seed=-1, useCache=None):直接传入一个位向量(指纹)序列。包装层把序列抽取为std::vector<const ExplicitBitVect *>,使用 Code/SimDivPickers/Wrap/PickerHelpers.h 中的pyBVFunctor以 Tanimoto 距离(1 - TanimotoSimilarity)为距离函数执行同样的 MaxMin 算法。
  • LazyPickWithThreshold(distFunc, poolSize, pickSize, threshold, firstPicks=(), seed=-1)与LazyBitVectorPickWithThreshold(objects, poolSize, pickSize, threshold, firstPicks=(), seed=-1):额外接收一个threshold距离阈值,当算法算出的候选最小距离<= threshold时提前终止,返回(picks, threshold)元组——threshold会更新为实际达到的距离值。C++ 实现中threshold < 0表示"不提前终止"(见MaxMinPicker::lazyPick中if (maxOFmin <= threshold && threshold >= 0.0) break;)。

测试验证与使用要点

测试文件 Code/SimDivPickers/Wrap/testPickers.py 提供了大量可复现的实证:

  • test0MaxMin:对 1000 个二维随机点调用Pick(self.dMat, 1000, 80, (886, 112)),前两个选中的正是种子886、112;同时验证了Pick与LazyPick结果一致,以及非法的firstPicks(如(1012,)、(-1,))会抛出ValueError;
  • testBitVectorMaxMin:同一批位向量分别用LazyPick(自写 Tanimoto 距离函数)与LazyBitVectorPick在相同seed=42下得到完全一致的结果;
  • testBitVectorMaxMin3:用chembl_cyps.head.fps(位于 Code/SimDivPickers/Wrap/test_data)挑选 20 个指纹,固定seed=42可复现确定的下标序列;传入firstPicks=[374, 720, 690, 339, 875]后结果保持不变,说明种子序列被正确采纳;
  • testBitVectorMaxMin4:验证阈值接口——threshold=-1.0时挑选 20 个并返回实际阈值约0.8977;threshold=0.91时提前终止只返回 13 个,且返回阈值>= 0.91。

要点:seed决定随机种子的选取,固定seed可复现结果;不同seed通常得到不同子集(见testNonUniqueCrash中的断言)。LazyBitVectorPick与等价的自定义距离函数LazyPick在相同种子下结果一致,因此当数据是指纹序列时优先使用前者,代码更简洁且无需手写距离函数。

LeaderPicker:球排除式的快速多样性挑选

LeaderPicker实现 Roger Sayle 提出的 Leader 算法(与球排除法 sphere exclusion 类似),包装代码见 Code/SimDivPickers/Wrap/LeaderPicker.cpp,算法实现位于 Code/SimDivPickers/LeaderPicker.h。

核心 API

LeaderPicker().LazyBitVectorPick(objects, poolSize, threshold, pickSize=0, firstPicks=(), numThreads=1) LeaderPicker().LazyPick(distFunc, poolSize, threshold, pickSize=0, firstPicks=(), numThreads=1)

与 MaxMin 最大的不同在于threshold是必填位置参数,且其含义是距离(即1 - similarity),而不是相似度。例如 Tanimoto 相似度阈值 0.8 对应 distance 阈值 0.2;LeaderPicker 文档字符串中给出的测试均使用threshold=0.8、0.9这类距离值。pickSize默认为 0,此时算法会一直挑选直到池子耗尽(等价于pickSize = poolSize,见LeaderPicker::lazyPick中if (!pickSize) pickSize = poolSize;)。numThreads参数当前被忽略(绑定 docstring 明确注明)。

算法流程(单线程实现):维护一个未选列表,每轮取出列表首个元素作为新 leader(种子),用距离函数将其与列表中所有剩余元素逐一比较,凡是距离<= threshold的元素全部从列表中剔除("排斥"进该 leader 的球内),直到列表为空或达到pickSize。由于最终任意两个被选元素之间的距离都严格大于threshold,结果天然满足多样性约束。

测试验证

Code/SimDivPickers/Wrap/testPickers.py 中testBitVectorLeader1提供了清晰的验证逻辑:

  • 对同一批chembl_cyps.head.fps指纹,threshold=0.8(距离)时选出 146 个;threshold=0.9时仅选出 14 个——阈值越大,每个 leader 的排斥半径越大,子集越小;
  • 测试随后遍历所有配对,断言任意两两之间的 Tanimoto 距离>= threshold,直接验证了算法的核心不变量;
  • pickSize=10时恰好返回 10 个,且同样满足两两距离约束。

testLazyLeader用简单函数i - j作为距离,验证LazyPick(func, 100, 20)返回[0, 21, 42, 63, 84]——即每个新 leader 会把距离 20 以内的元素全部剔除,间隔 21 的等差序列正是预期结果。

HierarchicalClusterPicker:先聚类、再取代表元

HierarchicalClusterPicker基于层次聚类做多样性挑选,头文件见 Code/SimDivPickers/HierarchicalClusterPicker.h,实现沿用Code/ML/Cluster下的 Murtagh 聚类代码。

聚类方法与构造参数

构造函数接收clusterMethod参数,Python 侧通过HierarchicalClusterPicker.ClusterMethod枚举暴露(绑定见 Code/SimDivPickers/Wrap/HierarchicalClusterPicker.cpp):

枚举值C++ 常量说明
ClusterMethod.WARDWARD = 1Ward 法(最小方差)
ClusterMethod.SLINKSLINK = 2单连接(最近邻)
ClusterMethod.CLINKCLINK = 3完全连接(最远邻)
ClusterMethod.UPGMAUPGMA = 4非加权平均连接
ClusterMethod.MCQUITTYMCQUITTY = 5McQuitty 法
ClusterMethod.GOWERGOWER = 6Gower 法
ClusterMethod.CENTROIDCENTROID = 7质心法

两个方法:Pick 与 Cluster

pkr = rdSimDivPickers.HierarchicalClusterPicker(rdSimDivPickers.ClusterMethod.WARD) pkr.Pick(distMat, poolSize, pickSize) # 返回代表元下标列表 pkr.Cluster(distMat, poolSize, pickSize) # 返回 clusters:每个簇为一个下标列表
  • Pick:先将整个池子按指定方法做层次聚类,当簇的数目合并到pickSize时停止;然后在每个簇内计算各元素到簇内其余元素距离的平方和,选取平方和最小(最接近质心)的元素作为该簇代表元。
  • Cluster:直接返回聚类结果,每个元素是一个下标列表,可用作后续分析。

注意两点(源码与测试均有印证):

  1. 距离矩阵会被算法修改:头文件注释明确指出 "this matrix WILL BE ALTERED during the picking"。Python 包装层因此在进入 C++ 前用PyArray_CopyFromObject拷贝一份,避免破坏用户传入的矩阵(见HierarchicalPicks与HierarchicalClusters中的注释与实现)。
  2. Pick要求pickSize < poolSize:包装层在HierarchicalPicks中显式检查并抛ValueError。

测试验证

Code/SimDivPickers/Wrap/testPickers.py 中的test1HierarchPick使用测试数据 Code/SimDivPickers/Wrap/test_data/points.csv(带标签的二维点集):Cluster(self.dMat, n, 2)划分出的两个簇内标签完全一致(聚类正确),而Pick返回(1, 30)作为两个簇的代表元。testIssue208与testInts则验证了对相同输入重复调用结果稳定,且整数距离矩阵也能正确处理。

纯 Python 层的补充接口:TopNOverallPicker 与 SpreadPicker

除 C++ 扩展外,rdkit.SimDivFilters包还提供纯 Python 实现的相似性挑选类,定义在 rdkit/SimDivFilters/SimilarityPickers.py,适用于"给定探针指纹、从库中挑选最相似化合物"的场景:

  • TopNOverallPicker(numToPick=10, probeFps=None, dataSet=None, simMetric=DataStructs.TanimotoSimilarity):对库中每个指纹,计算其与所有探针相似度的最大值作为该指纹得分,用 rdkit/DataStructs/TopNContainer.py 维护 Top-N 容器,最终返回得分最高的numToPick个(fp, score)对,结果按得分降序排列。
  • SpreadPicker(numToPick=10, probeFps=None, dataSet=None, simMetric=DataStructs.TanimotoSimilarity, expectPickles=True, onlyNames=False):为每个探针维护一个独立 Top-N 列表,再以"轮转取每行当前最优、跳过已被取走的元素"的方式挑选,使结果在多个探针间分散分布(这正是其名称含义)。expectPickles=False时允许dataSet为可转换为位向量的字符串;onlyNames=True时结果只保留_fieldsFromDb[0]名称。

两类 picker 均为惰性求值:构造时_picks为None,首次调用len(picker)或picker[i]才触发MakePicks()(见GenericPicker基类与 rdkit/SimDivFilters/UnitTestSimilarityPickers.py 的test_TopNOverallPicker、test_SpreadPicker对惰性行为的断言)。其 docstring 自带完整的 doctest 示例(连接RDTestDatabase的simple_mols1表、生成 RDKFingerprint、用COC与CC(=O)O作探针),可直接作为上手模板。

端到端实战:从指纹到多样性子集

综合以上 API,下面给出一个可直接运行的完整流程,涵盖"构建池子 → 计算距离 → 多样性与相似性挑选 → 结果校验":

import numpy from rdkit import Chem, DataStructs from rdkit.SimDivFilters import rdSimDivPickers as rdsimdiv from rdkit.SimDivFilters.SimilarityPickers import TopNOverallPicker # 1. 构建化合物池与指纹 smis = ["CCO", "CCOC", "CC(=O)O", "CCN", "C1CCCCC1", "c1ccccc1", "C#N", "CC(C)C"] mols = [Chem.MolFromSmiles(s) for s in smis] fps = [Chem.RDKFingerprint(m) for m in mols] # 2. 位向量版本:MaxMin 多样性挑选(固定种子可复现) picker = rdsimdiv.MaxMinPicker() picks = picker.LazyBitVectorPick(fps, len(fps), 4, seed=42) print("MaxMin picks:", picks) # 3. 阈值版本:同时返回终止时达到的距离 picks, threshold = picker.LazyBitVectorPickWithThreshold(fps, len(fps), 4, -1.0, seed=42) print("threshold reached:", threshold) # 4. Leader 算法:按距离阈值排斥,结果两两距离必大于阈值 lp = rdsimdiv.LeaderPicker() ids = lp.LazyBitVectorPick(fps, len(fps), 0.5) # 注意:0.5 是距离(1-相似度) for i in range(len(ids)): for j in range(i): d = 1.0 - DataStructs.TanimotoSimilarity(fps[ids[i]], fps[ids[j]]) assert d >= 0.5 print("Leader picks:", ids) # 5. 层次聚类挑选(构造欧氏距离矩阵可改用 rdMetricMatrixCalc) n = len(fps) dist_mat = numpy.zeros(n * (n - 1) // 2, "d") for i in range(n - 1): for j in range(i + 1, n): d = 1.0 - DataStructs.TanimotoSimilarity(fps[i], fps[j]) dist_mat[i * (i - 1) // 2 + j] = d # 注意仅填充下三角 hp = rdsimdiv.HierarchicalClusterPicker(rdsimdiv.ClusterMethod.WARD) print("Hierarchical representative picks:", hp.Pick(dist_mat, n, 2)) print("Hierarchical clusters:", hp.Cluster(dist_mat, n, 2)) # 6. 探针驱动的相似性挑选(纯 Python 层) probe = Chem.RDKFingerprint(Chem.MolFromSmiles("CCO")) top = TopNOverallPicker(numToPick=3, probeFps=[probe], dataSet=fps) for fp, score in top: print("top match, score =", score)

运行前提:需要已安装并正确编译的 RDKit(rdSimDivPickers为 C++ 扩展,需在构建时启用 Python 包装,参见 Code/SimDivPickers/CMakeLists.txt 中的RDK_BUILD_BOOST_PYTHON_WRAPPERS/RDK_BUILD_NANOBIND_WRAPPERS选项)。欧氏距离矩阵可借助rdkit.DataManip.Metric.rdMetricMatrixCalc的GetEuclideanDistMat直接生成(测试testPickers.py即采用此方式);若使用Pick系列方法,务必遵守下三角一维布局约定。

参数速查与常见陷阱

场景关键参数注意事项
MaxMinPickdistMat为下三角一维数组;pickSize < poolSizefirstPicks下标越界或为负会抛ValueError
MaxMinLazyPickdistFunc(i, j)返回距离内部缓存距离,客户端不要重复计算;useCache已废弃忽略
MaxMin 位向量接口objects为ExplicitBitVect序列与等价LazyPick结果一致;固定seed复现
MaxMin 阈值接口threshold为距离达到阈值提前终止,返回元组(picks, actual_threshold)
Leaderthreshold是距离不是相似度结果任意两两距离> threshold;pickSize=0表示选完
HierarchicalclusterMethod枚举 7 选 1Pick会修改矩阵(包装层自动拷贝);Pick要求pickSize < poolSize
TopNOverall / SpreadprobeFps、dataSet、simMetric惰性求值;SpreadPicker在探针间分散结果

最常见的两类错误:其一,把 Leader 的threshold当作相似度传入,导致排斥半径错误;其二,distMat布局不按下三角一维填充,或pickSize不小于poolSize,均会触发ValueError。对照上述速查表即可快速定位。

小结

rdSimDivPickers模块以统一的"下三角距离矩阵 + 距离函数"抽象承载了三种互补的挑选策略:MaxMin 以贪心方式最大化子集内部最小距离,适合精确的多样性优化;Leader 以距离阈值排斥邻近点,速度快、结果天然满足距离下限,适合大规模库快速去冗余;层次聚类则把挑选问题转化为"聚类 + 取代表元",并能顺带产出簇划分。配合纯 Python 层的TopNOverallPicker/SpreadPicker做探针驱动的相似性挑选,RDKit 的SimDivFilters家族可以覆盖化合物库设计的绝大多数挑选需求。想深入了解实现细节的读者,可继续阅读 Code/SimDivPickers/MaxMinPicker.h、Code/SimDivPickers/LeaderPicker.h、Code/SimDivPickers/HierarchicalClusterPicker.h 及对应的包装与测试文件。

  • 科学计算
  • 科研
  • 机器学习

【免费下载链接】rdkit

The official sources for the RDKit library

项目地址:https://gitcode.com/gh_mirrors/rd/rdkit
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进