
做化学信息学的人手里头难免会有那种要人命的大表几万条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 python3.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, asMolsTrue) main max(frags, keylambda 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)NC1CCC(CC1)O) fp AllChem.GetMorganFingerprintAsBitVect(mol, radius2, nBits2048)radius2基本对应ECFP4radius3对应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, radius2, nBits2048) 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最值得喜欢的地方就是它能把这些隐藏的局部关系从数据里一点点显性化出来。