特征工程基础:分子描述符、指纹、图特征

各位同学好。到了特征工程这块,我估计很多人都觉得「不就是把分子变成数字么」。嗯,话是没错,但怎么变得有信息量、变得让模型能听懂,这里面的门道可不少。

我在做ADMET预测项目早期,踩过一个坑:直接把几百个分子描述符一股脑扔进随机森林里,结果模型准确率惨不忍睹。后来复盘才发现——那些描述符里有一半是高度相关的冗余信息。所以说白了,特征工程不只是「找特征」,更是「挑特征」。

分子描述符:从0维到4维

什么叫分子描述符?就是把分子结构用数字量化的结果。我个人习惯把它们按维度分个类:

维度描述符例子典型应用
0D分子量、原子数、键数基础物性估算
1DLogP、氢键供体/受体数Lipinski规则检查
2D拓扑极性表面积(TPSA)、分子柔性吸收与透膜性预测
3D分子体积、偶极矩、静电势结合能、代谢位点预测
4D构象系综描述符柔性分子活性预测

我记得有次做CYP450代谢预测,数据集的分子都比较大,2D描述符在LogD和TPSA上差距明显,但换到3D描述符后,模型性能反而下降了——因为3D描述符对构象采样太敏感。所以,不要盲目迷信高维描述符。

核心观念:描述符的维度越高,计算代价越大,但信息增益却可能递减。实战中我建议从2D描述符开始,效果不好再往3D扩展。

分子指纹:稀疏编码的威力

你想想看,描述符是连续实数,那能不能用0/1的位图来表示分子特征呢?这就是分子指纹的出发点。

常见的指纹类型:

  • MACCS密钥指纹:166个预定义的分子子结构键。说白了就是一问一答:「这个分子里有苯环吗?有羰基吗?」每个答案对应一个二进制位。
  • Morgan(ECFP)指纹:基于原子环境的圆形指纹。半径越大,包含的微环境信息越多。ECFP4是我用得最多的,适合预测水溶性、透过性这类物理化学性质。
  • RDKit拓扑指纹:基于分子图的路径指纹。从分子图中枚举所有路径生成指纹位。
  • AtomPair指纹:编码原子对之间的距离和类型。在活性预测中表现出色。

实操中,我一般这样选:

from rdkit import Chem
from rdkit.Chem import MACCSkeys, AllChem

mol = Chem.MolFromSmiles('c1ccccc1C(=O)O')

# MACCS密钥指纹
maccs = MACCSkeys.GenMACCSKeys(mol)

# ECFP4(摩根指纹,半径2)
ecfp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)

# RDKit拓扑指纹
from rdkit.Chem.rdmolops import RDKFingerprint
topo = RDKFingerprint(mol, minPath=1, maxPath=7)
个人技巧:做ADMET分类任务时,我常用ECFP4 + 2048位。位太少(比如512)会丢失区分度,位太多(比如4096)则容易过拟合。这是个经验取值,但也值得你根据数据集规模微调。

图特征:让分子结构可视

到了图特征这块,思路其实更自然——把原子看作节点,把化学键看作边。一个分子就是一个图。

为什么要用图特征?因为我发现有些ADMET性质,比如肝脏代谢预测,跟某些局部结构紧密相关。指纹虽然能表示局部,但图神经网络能从图结构中学到更复杂的模式。

典型图特征包括:

  • 节点特征:原子类型、杂化态、形式电荷、氢原子数、部分电荷(Gasteiger/Mulliken)
  • 边特征:键型(单/双/三键/芳香键)、共轭性、是否成环
  • 全局特征:分子级描述符、指纹信息、图直径、拉普拉斯特征

具体怎么构建?上代码:

import torch
from torch_geometric.data import Data

def mol_to_graph(mol):
    atoms = mol.GetAtoms()
    node_features = []
    for atom in atoms:
        # 原子特征:原子类型one-hot + 杂化态 + 部分电荷
        feat = [
            atom.GetAtomicNum(),
            atom.GetHybridization(),
            atom.GetFormalCharge(),
            atom.GetTotalNumHs()
        ]
        node_features.append(feat)

    edges = []
    edge_features = []
    for bond in mol.GetBonds():
        i = bond.GetBeginAtomIdx()
        j = bond.GetEndAtomIdx()
        edges.append([i, j])
        edges.append([j, i])
        # 边特征:键型 + 是否芳香
        btype = str(bond.GetBondType())
        is_aromatic = 1 if bond.GetIsAromatic() else 0
        edge_features.append([btype, is_aromatic])
        edge_features.append([btype, is_aromatic])

    x = torch.tensor(node_features, dtype=torch.float)
    edge_index = torch.tensor(edges, dtype=torch.long).t().contiguous()
    edge_attr = torch.tensor(edge_features, dtype=torch.float)

    return Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
曾经踩过的坑:我在用图数据训练GCN时,发现模型时不崩时不崩。后来排查发现——相邻原子索引是双向对称的,但有的原子没有邻居时需要特殊标记。所以构建图时,一定别忘了添加自环,不然信息传递会断层。

特征选择的实战策略

光有特征还不够,你得知道留哪些、删哪些。我的套路是三步走:

  1. 方差筛选:去掉方差接近0的描述符。比如说,一个数据集中90%的分子都有同一个亚结构,那这个特征对区分不同ADMET性质基本没贡献。
  2. 相关性分析:分子量、原子数、TPSA之间往往高度相关。我会计算Spearman相关系数,保留那些与目标变量相关性最高且特征间相关性低的组合。
  3. 模型初筛:用LightGBM或随机森林的feature importance做一次排序,只保留Top-N特征。

光说不够,直接看代码怎么跑:

# 使用方差阈值过滤
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.01)
X_selected = selector.fit_transform(X_descriptors)

# 相关性分析
import pandas as pd
corr_matrix = pd.DataFrame(X_selected).corr().abs()
upper_tri = corr_matrix.where(
    np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)
)
high_corr = [column for column in upper_tri.columns
             if any(upper_tri[column] > 0.85)]
实战印象:做hERG毒性预测时,我发现分子的部分电荷分布比分子量、LogP都重要。筛选后只保留65个特征,模型性能反而提升了6%。所以别老想着堆特征,有时候做减法更管用。
特征工程核心流程 输入:分子结构 分子描述符 分子指纹 图特征 0D / 1D / 2D / 3D / 4D MACCS / ECFP / AtomPair 节点 / 边 / 全局特征 特征选择 ADMET预测模型 关键策略 • 方差筛选去除常量特征 • 相关性分析防冗余 • 模型初筛保留Top-N
一句话总结:描述符给的是数字,指纹给的是模式,图特征给的是结构。三者不是替代关系,而是互补关系。我在实际项目中经常把ECFP指纹和分子描述符合并成一个特征矩阵,然后让图神经网络额外引入拓扑信息——这样效果往往最好。