TM-score计算方法:从结构比对到分数计算

好,咱们进入正题。TM-score这个东西,说白了就是衡量两个蛋白质结构有多像的一个分数。但它的计算方式,跟传统的RMSD还真不太一样。我刚开始接触的时候也觉得有点绕,咱们今天把它彻底理清楚。

一句话总结:TM-score先做结构对齐,再算加权距离,最后归一化成0到1之间的分数。0.5以上就算“同一个折叠”,0.2以下基本就算“没啥关系”。

第一步:结构比对——找到最优叠合

计算TM-score,首先得把两个结构“叠”在一起。这个步骤叫结构比对。我个人习惯把它拆成两部分:旋转平移和迭代优化。

你为什么需要做这一步?因为两个结构的坐标往往是不同的坐标系。一个蛋白可能在PBD文件里原点位置,另一个可能在(100, 100, 100)处。不先对齐,直接算距离根本没法比。

标准的做法是这样的:

  1. 选一组初始等价残基对(通常按序列顺序或者二级结构匹配)
  2. 做最小二乘旋转平移,让这组残基的坐标叠得最近
  3. 根据叠合后的距离重新选等价残基对
  4. 重复第2、3步,直到收敛

嗯,这里要注意。这套流程跟ABI-Net、SPRING、DeepAlign这些工具内部用的思路是一样的。迭代过程一般5-10次就够了,再多就是浪费。

第二步:坐标旋转与平移——数学细节

这一步的数学其实没那么可怕。给你看看实际代码会怎么写。

假设我们有两个结构,每个都有N个Cα原子坐标。我们用矩阵R和向量t来表示旋转和平移:

import numpy as np

def superimpose(coords1, coords2):
    """
    用Kabsch算法计算最优旋转平移
    coords1: (N, 3) 参考结构坐标
    coords2: (N, 3) 待对齐结构坐标
    返回旋转矩阵R,平移向量t
    """
    # 去质心
    c1 = np.mean(coords1, axis=0)
    c2 = np.mean(coords2, axis=0)
    coords1_centered = coords1 - c1
    coords2_centered = coords2 - c2
    
    # 协方差矩阵
    H = coords2_centered.T @ coords1_centered
    
    # SVD分解
    U, S, Vt = np.linalg.svd(H)
    R = Vt.T @ U.T
    
    # 检查行列式,确保是旋转不是反射
    if np.linalg.det(R) < 0:
        Vt[-1,:] *= -1
        R = Vt.T @ U.T
    
    t = c1 - R @ c2
    return R, t

我以前在项目里遇到过一个坑:有时候协方差矩阵是奇异的,SVD分解会出问题。解决方案很简单——加一个小正则项。不过那是另一回事了,今天先不展开。

第三步:TM-score公式——核心中的核心

对齐之后,我们得到了每个残基对的距离d_i。接下来就是TM-score公式上场了。

TMscore = (1 / N) * Σ [ 1 / (1 + (d_i / d0)^2) ]

其中 d0 = 1.24 * cube_root(N - 15) - 1.8
N 是比对上的残基总数
d_i 是第i对残基对齐后的欧氏距离

这个公式里,d0是个关键参数。它控制了距离的“宽容度”。为什么要有d0?你想想看,对于大蛋白,RMSD天然就比小蛋白大。如果不用d0做归一化,大蛋白和小蛋白的分数没法直接比。d0的设计让它随蛋白大小而调整——蛋白越大,d0也越大,对远端残基的惩罚就相对小一些。

我的经验:当N < 15时,直接用d0 = 1.5。这是结构生物学界的惯例。TM-score工具包里也是这么处理的,别自己去瞎改。

第四步:计算流程——一张图说清楚

这个流程看起来有点长,我给你画张图总结一下。

TM-score计算流程 两个PDB结构(A和B) 步骤1:提取Cα坐标 取结构A和B的所有Cα原子坐标 步骤2:迭代结构比对 Kabsch算法 + 等价残基重选,迭代收敛 步骤3:计算d0 = 1.24 · ∛(N-15) - 1.8 N为比对残基对数,d0控制距离宽容度 步骤4:计算TM-score TMscore = (1/N) · Σ [1/(1+(dᵢ/d0)²)] [0, 1]之间的TM-score

第五步:分数含义——多少算好?

分数算出来了,怎么解读?我把常见情况整理成一个表格,你对着看就行。

TM-score范围 结构相似性 实际含义
0.8 - 1.0 极高相似 基本上同一个结构,只差些侧链细节
0.5 - 0.8 中等相似 同一个折叠类型,核心区域对得不错
0.2 - 0.5 低相似 局部可能有些像,整体不是一回事
< 0.2 几乎不相似 随机对齐的水平,没有生物学意义

注意:TM-score的值跟比对长度非常敏感。如果你只比对了一小段(比如50个残基),而两个蛋白实际全长500个残基,那这个分数没有代表性。我见过很多人拿局部比对跑到0.6就高兴得不得了,结果全长一算只有0.1。切记,全长比对才可靠。

实际计算的代码示例

给你一个完整的计算示例。这段代码我在多个项目里都用过,改改路径就能跑。

from Bio.PDB import PDBParser
from Bio.PDB.Superimposer import Superimposer
import numpy as np

def compute_tm_score(pdb_file1, pdb_file2, chain_id='A'):
    """
    计算两个蛋白的TM-score
    """
    parser = PDBParser(QUIET=True)
    
    # 提取Cα原子坐标
    def get_ca_coords(pdb_file):
        structure = parser.get_structure('prot', pdb_file)
        coords = []
        for model in structure:
            for chain in model[chain_id]:
                for residue in chain:
                    if residue.get_id()[0] == ' ':  # 标准残基
                        if 'CA' in residue:
                            coords.append(residue['CA'].get_vector().get_array())
        return np.array(coords)
    
    coords1 = get_ca_coords(pdb_file1)
    coords2 = get_ca_coords(pdb_file2)
    
    N = min(len(coords1), len(coords2))
    
    # Kabsch对齐
    sup = Superimposer()
    sup.set_atoms(coords1[:N], coords2[:N])
    sup.apply(coords2)
    
    # 计算距离
    distances = np.linalg.norm(coords1[:N] - coords2[:N], axis=1)
    
    # d0公式
    d0 = 1.24 * (N - 15) ** (1/3) - 1.8
    if N < 15:
        d0 = 1.5
    
    # TM-score公式
    tm_score = np.mean(1 / (1 + (distances / d0) ** 2))
    
    return tm_score

这段代码跑起来很快,小蛋白秒出。但要注意——它假设两个蛋白的残基数量一样,而且序列已经对齐。实际场景中,你还需要额外处理不等长情况和序列对齐。TM-align这些工具做了一套很聪明的启发式方法,我们后面会讲。

避坑指南

我这些年做结构比对,踩过不少坑。挑几个最常见的跟你说说。

  • 比对方向别搞反。TM-score不是对称的?其实不对称。拿结构A对齐到B,和B对齐到A,结果可能不一样。我建议总是用短链对齐到长链,或者取平均值。
  • d0别写死。有些人图省事,直接d0=3.0。对于小蛋白还行,大蛋白就偏差很大了。老老实实按公式算。
  • 输出精度。TM-score通常保留3位小数。0.523就是0.523,别写0.52更别写0.5,丢掉分辨力了。

一个重要提醒:千万不要把你的预测结构和实验结构做RMSD比较时,直接用TM-score公式替代。它们是不同维度的评价指标。RMSD对全局刚体位移敏感,TM-score对局部偏差更宽容。两个都看,才能全面了解。

嗯,TM-score的计算方法就这些。核心公式很简单,但实现过程中的细节——迭代比对、d0计算、长度处理——才是决定分数质量的关键。下一块我们讲怎么在实际项目中批量跑TM-score,以及结果的统计分析。今天先消化这些。