第4章 节点特征工程:原子特征、化学键特征、基因表达特征、药物性质特征的编码与标准化
节点特征工程这个活儿,说白了就是决定图神经网络能“看到”什么信息。你想想看,如果喂给模型的特征是错的或者噪声大,后面GNN再厉害也白搭。在我做药物批次效应检测项目时,这个问题被放大了很多倍——不同批次的实验条件不一样,特征不统一,模型直接“傻眼”。
这一章我们就专门讲特征怎么编码、怎么标准化。我会把原子、化学键、基因表达、药物性质这四类特征全部捋一遍。
4.1 原子特征编码
原子是图里的节点。每个节点到底带哪些属性?我个人习惯分成三类:
- 离散类别型——元素种类、杂化类型、环归属
- 数值连续型——原子质量、电负性、部分电荷
- 位点索引型——原子在分子中的序号、是否属于特定官能团
离散类别型特征,我用One-hot编码。比如元素种类,常见的有C、N、O、S、F,那就搞一个5维的One-hot向量。但要注意,有些数据集中会出现稀有元素,比如Se或P。我建议把稀有元素归到“其他”类别里,不然特征维度会失控。
关键原则:所有批次的原子特征维度必须一致。如果批次A有Se原子而批次B没有,那“其他”类就是你的安全垫。
import numpy as np
# 元素种类映射
ELEM_MAP = {'C':0, 'N':1, 'O':2, 'S':3, 'F':4, 'OTHER':5}
def encode_element(elem):
idx = ELEM_MAP.get(elem, ELEM_MAP['OTHER'])
vec = np.zeros(6, dtype=np.float32)
vec[idx] = 1.0
return vec
# 连续型特征标准化
def normalize_atom_mass(mass, mean=12.01, std=6.0):
return (mass - mean) / std
4.2 化学键特征编码
化学键是图的边。这里有一个很容易被忽略的事:键的类型在批次之间可能存在差异。比如一个化合物在批次A被测为单键,批次B因为pH不同变成了双键——这种情况在药物研发里并不少见。
我也踩过这个坑。我建议大家编码化学键特征时,不光编码键的类型,还要加上键的置信度或者权重。
| 键类型 | One-hot维度 | 附加特征 |
|---|---|---|
| 单键 | [1,0,0,0] | 键长、是否可旋转 |
| 双键 | [0,1,0,0] | 键长、是否共轭 |
| 三键 | [0,0,1,0] | 键长、是否芳香族 |
| 芳香键 | [0,0,0,1] | 环大小、离域电子数 |
我的经验:化学键特征千万不能只用One-hot。加上键长和可旋转性之后,GNN对分子构象变化的鲁棒性明显提升。特别是在批次效应检测中,构象变化经常是批次差异的来源之一。
4.3 基因表达特征融合
基因表达数据是药物批次效应检测里最特殊的特征。它不是分子本身的属性,而是分子与生物系统相互作用的产物。也就是说,同一个药物在不同细胞系或者不同批次里,基因表达谱可能完全不同。
怎么把几十个甚至上万个基因的表达值融合到节点特征里?总不能每个原子都拼接一堆基因表达吧?
我常用的做法是这样:
- 先降维——用PCA或自编码器把高维表达数据压缩到16或32维。
- 然后拼接一个全局向量——将降维后的表达向量拼接到每个原子的特征末尾。
- 最后做逐原子归一化——保证每个原子受到的“基因信号”尺度一致。
from sklearn.decomposition import PCA
# 假设 gene_matrix 是 (num_samples, num_genes) 的表达矩阵
pca = PCA(n_components=16)
gene_latent = pca.fit_transform(gene_matrix)
# 拼接到每个原子特征上
def combine_features(atom_feat, gene_latent_vec):
# 广播:每个原子都加上相同的基因特征
atom_feat_aug = np.hstack([atom_feat,
np.tile(gene_latent_vec,
(atom_feat.shape[0], 1))])
return atom_feat_aug
注意:不同批次之间的表达谱差异可能很大。我建议在训练前对每个批次单独做一次标准化,然后再用批次的对照样本做校正。否则GNN可能会学到错误的关联。
4.4 药物性质特征标准化
药物性质特征包括logP、分子量、氢键供体/受体数、可旋转键数等。这些特征通常是全局的——即一个分子只有一个值。所以它们不能直接作为节点特征,得做一下处理。
我一般是这样操作的:
- 把全局性质特征复制到所有原子节点上。
- 或者,把全局性质拼到“虚拟节点”上,然后让GNN自己学怎么传播。
第二种方式更灵活,我在实际项目里也验证过,效果更好。但批次效应检测时,一定要注意:药物性质本身在批次之间可能会有测量误差。比如同一个化合物,不同实验室测出来的logP可能差0.3个单位。
标准化方法我推荐用Z-score,按批次来做:
# 按批次做Z-score标准化
def batch_normalize(properties, batch_ids):
unique_batches = np.unique(batch_ids)
normalized = np.zeros_like(properties, dtype=np.float32)
for batch in unique_batches:
mask = batch_ids == batch
batch_data = properties[mask]
mean = np.mean(batch_data, axis=0)
std = np.std(batch_data, axis=0) + 1e-8
normalized[mask] = (batch_data - mean) / std
return normalized
避坑指南:千万不要把所有批次的数据一起做标准化。我曾经这么干过,结果模型完全不收敛。因为不同批次的均值和方差都不一样,混在一起标准化后,真正的批次差异反而被抹掉了。最后,GNN检测不到的批次效应,等于白做。
4.5 特征拼接与最终特征向量
所有特征都编好码、标准化之后,最后一步就是把它们拼成一个完整的特征向量。我通常会按这个顺序拼接:
- [原子类别One-hot] —— 比如6维
- [原子数值特征] —— 比如质量、电荷、范德华半径,3维
- [基因表达latent] —— 16维
- [药物全局性质] —— 比如logP、MW、HBD、HBA,4维
最后得到的是一个6+3+16+4=29维的节点特征向量。这个向量就是GNN每个节点看到的输入。边特征的处理方法类似,只不过维度会少一些,通常8到12维就够了。
小技巧:特征维度不要超过64。维度过高会导致GNN过拟合,而且训练速度会变慢。如果特征本身就很多,先降维再拼接。
这就是节点特征工程的核心流程。说白了,你就是在做一件事:把异构的、多源的数据统一成一组数字,然后让GNN去学这些数字背后的规律。
特征工程做好了,后面GNN的训练和批次效应检测才能跑得顺。嗯,这里要注意:特征的质量比数量重要得多。多花时间清洗和标准化,远比你堆100维的噪声特征有效。