ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

TMAP化学空间可视化:从SMILES到交互式树状图实战

TMAP化学空间可视化:从SMILES到交互式树状图实战

做化学信息学的人,手里头难免会有那种要人命的大表:几万条SMILES,对不对?光看表格根本看不出这些分子在化学空间里是扎堆还是分散,这时候大多数人第一反应就是做个PCA或者t-SNE。可惜这两招放到分子集合上,效果经常很一般——要么速度卡得让人难受,要么图上的点糊成一团,你想看的局部邻接关系反而被破坏了。后来我开始用TMAP来做化学空间可视化作图,算是彻底换了思路。TMAP的目标非常明确:把大量带化学式信息的分子数据,画成一棵容易浏览的“化学空间树”,节点还能按分子量、活性、骨架类型等属性映射颜色。这篇文章适合两类人:一类是已经会用RDKit处理SMILES、想把TMAP用起来的同学;另一类是刚接触组合化学库或者虚拟筛选结果分析、需要快速上手可视化工具的新手。我会把TMAP的算法逻辑、数据准备、作图参数和常见坑一次讲清楚,照着走基本能出一张像样的图。

1. TMAP是什么:化学空间可视化的一把新钥匙

1.1 为什么我不用t-SNE来画化学空间

先聊一个很实际的问题:分子集合的二维降维,为什么传统方法不够用?t-SNE和UMAP在单细胞转录组这类连续数据上表现不错,但放到分子指纹这种高维稀疏向量上,问题就暴露了。第一是速度,几万条分子做t-SNE优化已经很煎熬,上百万条基本不可行;第二是随机性,换一个随机种子,图上的形状可能完全不一样,很难当作汇报材料;第三是空间解释性差,t-SNE投影出来的距离没有稳定的横纵坐标含义,你只能“看个大概”。

TMAP的思路和这些方法完全不同。它不追求把高维空间完整“摊平”到二维平面,而是先构造分子之间的近邻关系图,然后从图里抽出一棵最小生成树,再用分层布局把这棵树画出来。打个比方:地理地图是保距的,但地铁地图是保序的——站点之间的远近会被扭曲,但连通关系一目了然。TMAP就是化学空间里的“地铁图”,它更关心哪些结构连在一起,而不是它们在某个线性坐标上的绝对位置。

我自己用过一段时间之后,最大的感受是:t-SNE给的是“一团若隐若离的散点”,TMAP给的是“一棵有清晰主干和分支的树”。对于化学家来说,后者显然更好讲故事,也更好做结构阐释。

1.2 TMAP技术链路拆解:指纹、LSH、最小生成树与分层布局

TMAP能处理超大分子集合,核心在于它把问题拆成了几步,每一步都有明确的工程取舍。

第一步,把分子结构变成指纹。常用的是RDKit生成的Morgan指纹、MACCS keys或者RDKit fingerprint。指纹本质上是一个固定位长的位向量,分子越相似,位向量越接近。这一步非常标准,任何一个做分子描述符计算的人都很熟。

第二步,用LSH森林寻找近似最近邻。所谓LSH(局部敏感哈希),是通过哈希函数把相近的向量以较大概率分到同一个桶里,从而避免精确计算两两相似度的O(N^2)成本。在一个几万甚至百万级的数据集里,精确计算所有分子对的距离是完全不现实的,所以TMAP用LSH森林先找到每个节点最像的若干个邻居,形成一个稀疏的kNN图。

第三步,在kNN图上求最小生成树。一个分子就是一个节点,分子之间的相似度就是边的权重。最小生成树算法(比如经典的Kruskal算法)会从全图中挑出连接所有节点、且总权重尽量小的一组边。这一步其实是整棵树的骨架来源:相似的分子会被短边串在一起,结果就形成一条条“结构主题分支”。丢掉部分边当然会损失信息,但留下的主干恰恰是化学空间里最值得关注的局部关系。

第四步,用分层布局把树画出来。TMAP使用的不是传统树状图那种一层层往下的“家族树”,而是一种面向大型树形图的平面布局算法,能把树根放在中心、分支向外展开,或者根据配置生成其他布局样式。最终得到的是一张可以交互缩放、悬停查看信息的2D图。

把这几步串起来,你会发现TMAP天生就是为了“大规模+局部邻接结构”设计的。它不做全局坐标拟合,而是在图论框架里做信息压缩,所以计算效率和可解释性都比较稳。

1.3 TMAP能解决的三个典型问题

TMAP最常见的应用场景,我归纳下来有三个。

第一个是超大规模化学空间的可视化。几百万条虚拟筛选结果,靠人工列表根本看不出分布,TMAP可以先把所有分子画出来,再通过颜色、搜索、缩放快速定位感兴趣的区域。

第二个是化学空间多样性评估。如果你想评估一个组合库到底覆盖了多少化学空间,看TMAP的树形分支和簇的分散程度就够了。骨架类型越丰富,树上的分支越明显;如果都是一种骨架,分支会非常单调。

第三个是结构-活性关系分析。把活性数值映射成节点颜色之后,相邻节点如果颜色差异很大,往往意味着“活性悬崖”——结构只改了一点点,活性却发生了剧烈变化。这类位置在药物优化中非常值得深入挖。

2. 环境与数据准备:先把SMILES和化学式伺候好

2.1 两条命令把环境搭好

TMAP本身是一个Python库,但依赖的RDKit需要单独安装。我比较推荐用conda创建独立环境,省得把平时的分析环境搞乱。实际操作用下面几行命令就行:

conda create -n chem python=3.9 -y conda activate chem conda install -c conda-forge rdkit tmap numpy pandas -y

这里有几个细节需要留意。第一,TMAP最好也走conda-forge渠道,因为在某些系统上用纯pip安装可能涉及本地编译问题,版本容易踩坑。第二,Python版本建议在3.8到3.10之间,太新的版本偶尔会碰到依赖库还没跟上轮子的情况。第三,如果只是临时做一次分析,也可以直接pip安装,但我还是建议用conda隔离,后面重装、换版本都方便。

环境装好之后,可以快速验证一下:

python -c "import tmap, rdkit; print('ok')"

能打印出ok,说明基础环境没问题。后面如果遇到奇怪的导入报错,多半是tmap和rdkit的版本不匹配,回退一下版本就行。

2.2 SMILES清洗:别让脏数据影响整张图

很多人拿到TMAP的第一版图特别难看,以为是参数问题,调了半天才发现是输入数据本身脏。SMILES清洗这一步看起来简单,其实决定了TMAP图的还原度。

我通常遵循这几条规则:第一,解析失败的直接去掉;第二,重复结构去重;第三,把盐、溶剂等不关心的片段从结构里剥掉;第四,只保留中性形式或统一一个特定的pH形态,否则酸碱形态混在一起会干扰指纹相似度。

处理代码可以写成下面这样:

from rdkit import Chem def clean_smiles(smi): mol = Chem.MolFromSmiles(smi) if mol is None: return None # 取最大片段,去掉盐和溶剂 frags = Chem.GetMolFrags(mol, asMols=True) main = max(frags, key=lambda m: m.GetNumAtoms()) return Chem.MolToSmiles(main)

为什么要取最大片段?因为很多数据库里的SMILES带着反离子,比如钠盐、盐酸盐,这些离子若保留下来,会让两个化学结构明明一样、只是盐型不同的分子在图上被分到不同位置。取最大片段是一种通用但不完美的策略,它假设你关注的是分子主体。如果你的体系里配体、金属和受体同等重要,那就要另外设计清洗逻辑了。

还有一个容易忽略的坑:去重。指纹虽然能区分差异,但如果有大量完全重复的SMILES,这些分子在图上会重叠在一起,个别簇的面积被人为放大。我会在清洗后用集合或者DataFrame的drop_duplicates处理一次,并记录数量变化,写进分析报告里。

2.3 指纹与化学式计算:从结构到向量的关键转换

TMAP要处理的是分子相似性,所以在做图之前,必须把SMILES转成指纹向量。指纹类型有很多,这里我只给出实际场景的选择建议。

最常用的是Morgan指纹,也就是ECFP家族的变体。RDKit里默认用下面这行生成:

from rdkit.Chem import AllChem from rdkit import Chem mol = Chem.MolFromSmiles("CC(=O)NC1=CC=C(C=C1)O") fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)

radius=2基本对应ECFP4,radius=3对应ECFP6。对普通分子集合,ECFP4和ECFP6差别不大;如果结构尺寸偏大,半径大一点的指纹可能区分度更高。nBits我一般用1024或2048,位长太短容易碰撞、位长太长会让后续LSH内存开销大。

同时,TMAP的结果经常要按性质着色,所以我会顺手把化学式和基础性质一起算出来:

from rdkit.Chem import rdMolDescriptors formula = rdMolDescriptors.CalcMolFormula(mol) mw = rdMolDescriptors.CalcExactMolWt(mol) logp, mr = rdMolDescriptors.CalcCrippenDescriptors(mol) tpsa = rdMolDescriptors.CalcTPSA(mol)

这些值会成为后面“颜色映射”的数据来源:分子式可以拿来做离散类别着色,分子量和TPSA可以做连续色标。我建议在建图之前就把这些信息整理成表格,每一行对应一个分子,后面渲染时直接按同一索引映射即可。

2.4 指纹横向对比:在TMAP中怎么选

不同的指纹对化学空间的形状影响非常大。我之前做过一组对比实验,用同一批约五千个分子分别跑RDKit指纹、Morgan指纹和MACCS keys,结果树形分支的粒度明显不同。下表是我个人经验里的选择参考:

指纹类型默认位长特点在TMAP中更适用的场景
RDKit fingerprint2048基于子结构片段,全局信息较均衡多样骨架分析、查找全局相似性
Morgan fingerprint / ECFP1024-2048围绕原子中心扩展,局部环境敏感官能团局部改动导致的活性差异、SAR分析
MACCS keys166官能团分类清晰,位宽小快速粗筛、可解释性要求高的场景

如果在TMAP上只打算用一种,我会默认用Morgan指纹。原因很简单:它在绝大多数化学数据集上的结构相似性区分度稳定,位向量稀疏度合适,和LSH森林的近邻搜索配合得也比较顺畅。MACCS keys虽然计算快,但166位的信息量有限,容易把差异很大的分子判成很相似。

3. 核心实操:构建第一张TMAP化学空间图

3.1 一张图看懂完整流程与代码骨架

TMAP的代码流程可以浓缩成这样几条:解析SMILES、计算指纹、构建LSH森林、生成kNN图、计算最小生成树、布局渲染。下面这段代码是核心逻辑的示意写法。不同版本TMAP的API偶尔会有拼写层面的小变化,我在代码中保留了最通用的结构,实际运行前请你对照本机版本的官方示例微调一下函数名。

import numpy as np import tmap from rdkit import Chem from rdkit.Chem import AllChem smiles_list = ["CCO", "CCN", "c1ccccc1", "c1ccccc1O", "CC(=O)O", "CC(=O)N", "C1CCCCC1"] mols = [Chem.MolFromSmiles(s) for s in smiles_list] mols = [m for m in mols if m is not None] fingerprints = [AllChem.GetMorganFingerprintAsBitVect(m, radius=2, nBits=2048) for m in mols] # 注意:TMAP不同版本对输入格式要求不同, # 有的版本直接接收RDKit位向量,有的需要转成数组。 # 下面的LSHForest对象是TMAP内置的近邻图结构。 lsh_forest = tmap.LSHForest() for fp in fingerprints: lsh_forest.add_point(fp) lsh_forest.index() # 生成图结构。get_graph的参数通常用于控制邻居数和阈值。 graph = lsh_forest.get_graph() mapper = tmap.TMAP() mapper.compute_mst(graph) layout = mapper.get_layout()

这段代码最重要的不是每一行是否一字不差,而是它告诉你顺序:指纹先算好,然后丢进LSH森林,再从森林里取出图,最后计算最小生成树并布局。如果你在某个版本里找不到get_graph或者compute_mst,多半是官方换成了更面向对象的封装,搜索一下你本地版本自带的示例文件就能对上。

我自己习惯把所有分子的指纹存成一个list,属性表存成一个DataFrame,这样后面对应“哪个节点是哪条SMILES”非常简单。把属性列表和指纹列表的索引对齐,是你后面做颜色映射时最需要小心的一件事。

3.2 TMAP的5个关键参数怎么调

参数如果不调,默认值虽然能跑起来,但大概率不是你想要的效果。我按踩坑经验整理出五个影响最大的参数。

第一个是邻居数k,它决定每个分子要和多少个最近邻相连。k太小,图会碎成很多小岛;k太大,树会失去局部细节,分支全部混在一起。几千到几万条分子,我一般从8到15之间起步尝试。

第二个是LSH森林的树数量,我习惯叫它tree_num。这个值越大,近似近邻的召回率越高,但耗时也线性增加。一百万条分子内,几十棵树通常就够了,不需要贪多。

第三个是相似度阈值,低于这个阈值的边会被直接舍弃。这个参数和k配合使用:一个控制边的数量上限,一个控制边的质量下限。阈值过低会把大量无关结构强行连起来;阈值过高又会把簇切得太碎。

第四个是布局相关的节点间距或分辨率,它影响图上的视觉密度。分子多了如果不拉大画布,整个树会挤成一团,颜色变化也看不清。

第五个是随机种子。LSH森林和后续布局都有随机性,不固定种子的话,同一份数据两次运行可能得到两张差异很大的图。固定种子的具体写法要看版本,通常在初始化TMAP对象或LSH森林时有一个seed参数。

这里有个很实用的调参方式:先用随机抽样的五百到一千条分子快速跑一遍,观察树形是否出现清晰分支。不看整体美观,只看分支结构,确定好k和阈值的量级后,再放大到全量数据。直接拿全量数据一遍遍调参,不仅慢,而且容易在局部参数空间里绕圈子。

3.3 颜色映射:把分子式、分子量和活性画到树枝上

TMAP最有价值的一个功能就是把数值或分类属性“涂”到节点上。图的结构告诉你哪些分子像,颜色告诉你这些分子在整个空间里怎么分布。分子量就是一种很直观的连续属性:如果树上有一个簇全是深色,说明这片区域集中了分子量很大的结构;如果颜色沿着分支渐次变化,说明结构改动和分子量增长是平滑关联的。

连续属性的映射逻辑很直接,准备一个和节点一一对应的浮点列表,传给可视化组件即可。离散属性则是准备字符串或整数列表,比如把分子式、骨架类型、通过或失败这样的标签当成类别处理。我自己的经验是:连续属性用渐变色谱,离散属性用对比色,而且离散类别的数量最好不超过十类,否则图会变成彩色马赛克,反而失去可读性。

还需要注意缺失值的处理。颜色列表里一旦混入NaN或者空字符串,有些版本会直接报错,有些版本会把整个颜色标尺打乱。我在建图前都会先检查一遍属性列,把缺失值用中位数填充或者单独分成“未知”类。

3.4 输出与保存:从交互式HTML到静图

TMAP默认会生成一个可在浏览器里打开的交互式页面。你可以在页面上拖动、缩放、悬停节点查看对应的分子信息,还能按子结构搜索位置。对日常汇报来说,交互式页面已经够用了;但如果要放进PPT或者论文,我通常直接截图,或者用无头模式输出更高质量的图片。

一个实用建议:在最终汇报前,把你要展示的某些节点做上额外标记。很多结构相似的分子在树上位置很近,如果不标记,读者根本不知道哪个节点对应哪个化合物。标记方式一般是给节点增加标签字段,标签用编号或者简短备注,避免把整个SMILES直接糊在图上,否则字体一多整张图就开始乱了。

4. 进阶场景:三个可以直接套用的样例

4.1 样例一:虚拟筛选结果的可视化

先说我跑得最多的一个场景:虚拟筛选。假设你在某次对接计算里筛出一个打分靠前的化合物列表,背景是几万个库存结构。你当然可以直接看排序,但你更想知道这些命中结构在化学空间里是否聚集。

做法是把背景库全部画成灰色,把命中结构的节点标成另一个颜色。如果命中列表密集落在树的某几个分支里,说明这些分子的骨架和官能团高度相似;如果命中点星星点点分布在各个大簇,说明筛选找到了多样化的结构。后面这种结果,在组合设计里往往更有价值。

这个样例不需要任何额外预处理,只是把颜色映射的“离散类别”设成“背景库/命中”两个值。我的经验是,百分之八十的虚拟筛选项目,看完这张图基本就能判断这一轮结果值不值得做下一轮实验。

4.2 样例二:活性悬崖的定位

活性悬崖是一个让做药化的朋友又爱又恨的现象:两个分子结构高度相似,活性却差了百倍。TMAP天然适合发现这类位置。

你需要把活性值或打分值作为连续属性映射到颜色上。在树状图上,绝大多数分支的颜色应该是平滑渐变的,说明结构和活性的变化是连续的。但偶尔会出现两个相邻节点,结构上只差一个取代基,颜色却从深红跳到深蓝。这个局部突变点就是典型的活性悬崖候选。

遇到这种位置,我会用交互页面把这两个节点单独拎出来,并排比较它们的SMILES,看看差异到底发生在哪个位点上。如果你手里有多个系列的化合物,这种操作能很快找到结构优化的关键位点。活性悬崖分析听起来高大上,其实在TMAP上就是“找相邻节点的颜色突变”,工作量不复杂,效果却非常好。

4.3 样例三:不同批次数据集的叠加对比

另一个常见需求是比较两个批次的结构空间是否一致。比如你合成了第一批五十个化合物,后来又合成了第二批五十个,你想知道第二批是补充了第一批没覆盖的新空间,还是只是在老空间里打转。

最简单的做法是把两批数据合在一起跑一张TMAP,用颜色区分批次。如果两批分子混在同一个树簇里,说明它们结构空间高度重合;如果第二批集中在树的远端分支,说明它确实拓展了化学空间。

有一个细节需要注意:合在一起跑图时,树的整体骨架是所有数据共同决定的。如果两个批次差异实在太大,可能某些分支就是两个批次各自独有的,从视觉上很容易看出来。这种“合并跑一张图”的方式比单独跑两张图再对比要可靠得多,因为单独跑两张图的坐标轴不统一,根本没法严格比较。

5. 常见问题与避坑技巧

5.1 指纹和输入类型报错

我见过最多的报错都出在指纹输入格式上。各地版本对“能不能直接接收RDKit的ExplicitBitVect”态度不一样。有的版本内部会自动转换,有的版本只认numpy数组或稀疏数组。遇到TypeError或者维度不匹配,解决思路不是查几十页issue,而是直接把指纹统一转成数组再接进去。常见的转换方法是用numpy.frombuffer解析位串,或者先转成list再构造数组。

这个预处理步骤虽然看起来多写了几行,但能让你的代码在不同版本之间迁移时少很多痛苦。特别是如果你的环境是团队共用,不同人的环境里版本不一致,统一输入格式能省下一大堆沟通成本。

5.2 布局挤成一团或全部塌缩

如果出图后整棵树挤成一团,分不开层,我的第一个排查方向不是布局,而是数据。检查是不是有大量重复或近似重复分子。如果有几百个几乎一样的结构,它们会形成一个超重节点,把整棵树拉得失衡。先去重,看树形是否恢复。

如果数据没问题,再检查k和阈值。k太大或阈值太低都会导致图里塞进太多长边,MST阶段就很难抽出清晰主干。我的习惯是先只调k,因为阈值对簇的粒度影响更剧烈,容易一下子切过头。k和阈值的配对需要一点耐心,但判断标准很简单:树的主干清晰,分支可辨,不在某个地方堆成毛线团。

5.3 大数据集算不动怎么办

TMAP虽然是为大规模数据设计的,但也不会无限快。几十万条分子跑起来,主要瓶颈在LSH森林建索引和后续图计算。我的建议是,先做一次聚类或者均匀抽样,在子集上调节参数。等参数确定了,再决定是否跑全量。

另外,指纹位长也是成本来源。把Morgan指纹从2048位降到1024位,速度提升明显,而相似性区分度损失通常不大。如果你的数据量已经达到百万级,还可以考虑先按骨架做聚类,再把每个簇的代表分子拿来做TMAP,这样树形图依然能反映整个空间,但计算压力小了很多。

5.4 颜色映射不生效或颜色标尺混乱

颜色映射失效,九成九是属性列表和节点列表的顺序没对上。我遇到过的情况包括:清洗SMILES时删了一部分分子,但属性列表没有同步删除;或者排序之后忘了重新索引。记住一个原则:从SMILES清洗到指纹计算到属性绑定,始终用同一个索引体系。

另一个容易出问题的地方是连续属性里混入缺失值。别再问为什么标尺颜色一团乱,先检查有没有NaN。把缺失值填掉,或者归一化之前对属性列做一次dropna处理,颜色马上恢复正常。

5.5 复现性:把随机种子管好

TMAP里的LSH森林带随机性,所以两次跑图可能得到不同的树。如果你要把这张图用在正式汇报里,一定要固定随机种子,否则第二天重新跑一遍,图上某个分支位置变了,讲PPT的时候容易被动。

我通常在文件开头固定一个seed变量,并且把输入SMILES文件、指纹参数、k、阈值、种子值全部记录到一个配置文本里。这样不管过多久,只要拿着这份配置重跑,图就能复现。别小看这一步,我做过的项目里,有多次需要旧图和新图并排对比,种子没固定导致只能重做,非常浪费时间。

说回我自己的习惯:不管原始数据有多少条,我都会先抽一两千条分子跑一遍TMAP,把k、阈值这些参数调到结构看得顺眼,再放全量。这个习惯救了我很多次,因为小样本上发现的问题,往往是大样本里最容易被忽视的坑。最后再分享一个我常用的小技巧:颜色属性别只盯着分子量,换成分数、价格、批次号或者活性值再多看一遍。很多时候,结构一致的结论,换一个视角就会有完全不同的解读。TMAP最值得喜欢的地方,就是它能把这些隐藏的局部关系从数据里一点点显性化出来。

RELATED READING

延伸阅读

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