168

可解释性 AI:解释要能帮助化学决策,而不是只画热图

可解释性要能帮助化学决策,而不是画一张好看的热图。这篇讲清各类方法的可靠性与验证要求。

分子模型的「可解释性」常常停留在画一张原子高亮图的层面。但一张好看的热图,如果不能改变化学家的下一步行动,就没有创造价值。真正有用的解释必须可验证、可行动。

常见方法与可靠性

方法 原理 可靠性
特征重要性(树模型) 分裂增益 较可靠但粒度粗
SHAP 合作博弈论的归因 理论基础好;计算贵
LIME 局部线性近似 不稳定
注意力权重 模型内部权重 不可靠——不是因果证据
梯度类(Grad-CAM 等) 输出对输入的梯度 有噪声
遮蔽/消融 删掉某部分看预测变化 直接,较可信
匹配分子对(MMP) 数据驱动的结构-活性关系 最可靠——基于真实实验

为什么注意力不是解释

# 三个理由:
#
# 1) 【注意力不唯一】
#    可以构造出很不同的注意力分布,
#    给出几乎相同的预测
#    → 那么哪一个是「正确的解释」?
#
# 2) 【注意力不等于因果】
#    模型「看了」某个位置,
#    不代表这个位置在化学上重要
#    → 可能只是计算路径的中间产物
#
# 3) 【缺乏验证】
#    多数论文只展示 2~3 个「看起来合理」的案例
#    → 【人类在任何热图中都能找到模式】
#    → 没有统计验证的解释不构成证据
#
# 【正确的验证方式】:
#   把解释与【已知的真实信息】比较:
#     - 共晶结构中的实际相互作用(见 108)
#     - MMP 分析得到的取代基贡献
#     - 已知的代谢位点
#     - 化学家标注的药效团
#   计算重合度,并与随机基线比较
#
# 【消融验证】:
#   把模型认为重要的部分改掉,
#   预测是否显著变化?
#   → 【这是最直接的检验】
#   → 而且这个检验很容易做,却常常被省略

SHAP:相对可靠的选择

import shap
import numpy as np
from rdkit import Chem
from rdkit.Chem import rdFingerprintGenerator, Draw

# 对树模型,TreeSHAP 是精确且快速的
gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)

explainer = shap.TreeExplainer(lgbm_model)
shap_values = explainer.shap_values(X_test)

# 全局重要性
shap.summary_plot(shap_values, X_test, max_display=20)

# ---- 把 SHAP 值映射回分子原子(关键步骤)----
def atom_contributions(smiles, model, explainer, radius=2, n_bits=2048):
    """把指纹位的 SHAP 值归到原子上"""
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    bit_info = {}
    gen_local = rdFingerprintGenerator.GetMorganGenerator(
        radius=radius, fpSize=n_bits)
    ao = rdFingerprintGenerator.AdditionalOutput()
    ao.AllocateBitInfoMap()
    fp = gen_local.GetFingerprint(mol, additionalOutput=ao)
    bit_info = ao.GetBitInfoMap()

    sv = explainer.shap_values(np.array(fp).reshape(1, -1))[0]

    contrib = np.zeros(mol.GetNumAtoms())
    for bit, envs in bit_info.items():
        if sv[bit] == 0:
            continue
        for center_atom, rad in envs:
            # 该位对应的子结构包含哪些原子
            if rad == 0:
                atoms = [center_atom]
            else:
                env = Chem.FindAtomEnvironmentOfRadiusN(mol, rad, center_atom)
                atoms = set()
                for b in env:
                    bond = mol.GetBondWithIdx(b)
                    atoms.add(bond.GetBeginAtomIdx())
                    atoms.add(bond.GetEndAtomIdx())
                atoms = list(atoms) or [center_atom]
            for a in atoms:
                contrib[a] += sv[bit] / len(atoms)
    return contrib

# 可视化
contrib = atom_contributions("CC(=O)Nc1ccc(O)cc1", model, explainer)
mol = Chem.MolFromSmiles("CC(=O)Nc1ccc(O)cc1")
Draw.SimilarityMaps.GetSimilarityMapFromWeights(mol, list(contrib))

# 【解读注意】:
#   1) SHAP 值是【相对于基线(平均预测)】的贡献
#   2) 一个指纹位可能对应多个原子环境 —— 归因有任意性
#   3) 【相关不等于因果】:SHAP 反映的是模型的行为,
#      不是化学的真理
#   4) 【必须与 MMP 分析交叉验证】

匹配分子对:最可靠的「解释」

# MMP(Matched Molecular Pairs):
#   找出只差一个取代基的分子对,
#   统计这个变换对性质的影响
#
# 【为什么最可靠】:
#   它基于【真实的实验数据】,
#   而非模型的内部状态
#   → 这不是「解释模型」,而是「解释数据」
#   → 但对化学决策而言,后者才是真正需要的

from rdkit import Chem
from collections import defaultdict
import numpy as np

# 用 mmpdb 或 RDKit 的 rdMMPA 做片段化
from rdkit.Chem import rdMMPA

def find_mmps(df, smiles_col="smiles", value_col="pIC50"):
    """找出匹配分子对并统计变换的影响"""
    frags = defaultdict(list)
    for _, row in df.iterrows():
        mol = Chem.MolFromSmiles(row[smiles_col])
        if mol is None:
            continue
        for core, chains in rdMMPA.FragmentMol(mol, maxCuts=1):
            if core:
                frags[Chem.MolToSmiles(core)].append(
                    (Chem.MolToSmiles(chains), row[value_col]))

    transforms = defaultdict(list)
    for core, items in frags.items():
        for i in range(len(items)):
            for j in range(i + 1, len(items)):
                a, va = items[i]
                b, vb = items[j]
                if a != b:
                    transforms[(a, b)].append(vb - va)
                    transforms[(b, a)].append(va - vb)

    results = []
    for (a, b), deltas in transforms.items():
        if len(deltas) >= 5:      # 【需要足够的样本才有统计意义】
            results.append({
                "from": a, "to": b, "n": len(deltas),
                "median_delta": float(np.median(deltas)),
                "std": float(np.std(deltas)),
            })
    return sorted(results, key=lambda r: -abs(r["median_delta"]))

# 【输出对化学家直接可用】:
#   「把甲基换成三氟甲基,在这个系列中
#     中位数提升 0.6 个 log 单位(n=12)」
#   → 这是可以直接指导下一步合成的信息

什么样的解释才有价值

标准 说明
可行动 能指出「改哪里、怎么改」
可验证 能设计实验检验这个解释
与已知知识一致或有理由不一致 矛盾时应能给出说法
有统计支撑 不是精选的个案
粒度合适 到官能团/取代基级别,而非单个指纹位
诚实标注不确定性 解释本身也有不确定性

一个实用的检验:把解释拿给化学家看,问「这会改变你下一步做什么吗?」如果答案是「不会」,那么这个解释没有创造价值——不管它的图画得多好看。

关键要点

  • 注意力权重不是因果证据——注意力不唯一,且多数论文只展示精选案例;
  • 消融验证最直接:改掉模型认为重要的部分,预测是否显著变化;
  • 匹配分子对最可靠——它解释的是数据而非模型,且输出直接可指导合成;
  • 检验标准:「这会改变化学家下一步做什么吗?」答案是否定的,解释就没有价值。

延伸资源