4. 数据标准化/归一化错误:尺度不一致导致模型不稳定
模型训练完,一看 loss 曲线,震荡得像心电图?验证集上表现时好时坏?
别急着调网络结构,很可能问题出在数据预处理阶段。标准化/归一化没做好,模型就像在悬崖边上开车——稍微有点坡度就翻车了。
说白了,ADMET 数据里各描述符的尺度差异太大了。分子量动辄几百,logP 只有个位数,极性表面积可能几十到几百,电荷相关特征又在 0 到 1 之间。如果你的模型(尤其是深度神经网络和 SVM)直接拿这些数据去训,那大尺度的特征会天然「主导」梯度更新,小尺度的特征直接被淹没。
我个人习惯:拿到数据第一件事,不是看分布,而是先检查每一列的数值范围。这个习惯帮我避免过至少两次大型返工。
4.1 标准化 vs 归一化:别搞混了
很多人把这两个词混着用,其实它们干的活完全不同。
| 方法 | 公式 | 输出范围 | 适用场景 |
|---|---|---|---|
| Z-score 标准化 | \( x' = \frac{x - \mu}{\sigma} \) | 理论上无界,均值为 0,标准差为 1 | 数据近似正态分布;算法假设特征服从高斯分布(如线性模型、SVM) |
| Min-Max 归一化 | \( x' = \frac{x - x_{min}}{x_{max} - x_{min}} \) | [0, 1] | 有明确上下界的特征;需要保持稀疏性(如神经网络输入层) |
| Robust 标准化 | \( x' = \frac{x - median}{IQR} \) | 受异常值影响小 | 数据存在离群点;ADMET 数据中常用于 LogD、pKa 等不易归一化的物理量 |
一个常见的坑:训练集和测试集分别做标准化。你想想看,测试集不应该「知道」训练集的均值和方差吗?分别做等于泄漏信息,模型在测试集上会异常好,但到新数据上立刻原形毕露。
4.2 分子描述符范围未对齐:看似小事,后果严重
ADMET 预测里,同一描述符在不同数据集里的取值范围可能完全不同。比如同一个指纹位点,在 Caco-2 渗透性数据和 hERG 抑制数据里的出现频率天差地别。
我在项目中遇到过这种情况:两个团队分别准备了训练集和外部验证集,logP 在训练集里是 -1 到 6,验证集里却到了 9。模型在验证集上一塌糊涂。排查了两天才发现——验证集里含了几个脂溶性极强的化合物,标准化参数根本没覆盖到。
为什么会这样?
- 化学空间偏移:新化合物与训练集的结构差异大,描述符自然对不上。
- 指纹位点定义不一致:不同版本的 Morgan 指纹半径或位点压缩逻辑不同。
- 缺失值处理不当:某个描述符在部分样本里填了 0 或 NaN,导致统计量偏掉。
4.3 代码实例:标准化流程的正确写法
下面是一个常用的 pipeline,注意 fit_transform 和 transform 的使用位置。
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
# 假设 X 是分子描述符矩阵
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 初始化标准化器
scaler = StandardScaler()
# 关键:只在训练集上 fit
X_train_scaled = scaler.fit_transform(X_train)
# 用训练集的均值和标准差转换测试集
X_test_scaled = scaler.transform(X_test)
# 验证一下
print(f"训练集均值: {X_train_scaled.mean(axis=0)[:5]}")
print(f"训练集标准差: {X_train_scaled.std(axis=0)[:5]}")
print(f"测试集均值(应接近 0): {X_test_scaled.mean(axis=0)[:5]}")
如果你看到测试集均值明显偏离 0(比如大于 0.1),那大概率是化学空间偏移了。这时候强行标准化只会掩盖问题,建议回头检查数据分布。
4.4 特征尺度的可视化诊断
别全靠眼睛看数字。画个箱线图,一秒就能发现异常。
import matplotlib.pyplot as plt
import pandas as pd
# 假设 raw_df 是原始描述符数据
df_plot = pd.DataFrame({
'MolWt': raw_df['MolWt'],
'LogP': raw_df['LogP'],
'TPSA': raw_df['TPSA'],
'HBA': raw_df['NumHAcceptors'],
'HBD': raw_df['NumHDonors']
})
df_plot.boxplot()
plt.xticks(rotation=45)
plt.title('Before Standardization: Feature Scale Disparity')
plt.show()
曾经有一次我在看这个图时,发现 LogP 那列的箱线图顶部飞出去了一个点——分子量 800 多,LogP 居然 12。这种离群点如果用 Min-Max 归一化,会把所有其他值的范围压缩到极窄的区域,模型根本学不到东西。改成 RobustScaler 就解决了一大半。
4.5 知识体系总览
下图的流程,是我自己项目里总结出来的决策路径。每次做预处理前,我都会过一遍这张图,节省了大量试错时间。
4.6 常见错误速查表
下面是我自己整理的「踩坑清单」,每次预处理跑不过就对着查一遍。
| 错误 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| 训练/测试集分别标准化 | 验证集指标异常好,外部泛化差 | 数据泄漏,测试集信息进入训练环节 | 只对训练集 fit,再用该 scaler transform 测试集 |
| Min-Max 后特征坍缩 | 大量样本的某特征值集中在 [0, 0.01] | 存在极端离群点拉高了 max,压缩了正常范围 | 改用 RobustScaler,或先剔除/缩尾处理离群点 |
| 对离散/二进制特征做标准化 | 模型权重异常大或 NaN | 二进制特征(如 0/1 指纹)标准化后失去稀疏性 | 二进制特征不要标准化,用 pass_through |
| 标准化参数固化后混入新数据 | 新批次数据预测效果断崖式下降 | 旧参数无法覆盖新数据的范围 | 收集数据后重新拟合 scaler,或用在线标准化策略 |
4.7 经验总结
标准化这件事,看着简单,但翻车的概率其实很高。我个人遇到过最好笑的一次是,有个团队把训练集的 logP 列手动除以了 10,然后模型收敛了,但结果解释时发现:预测的 logP 值比真实值大了 10 倍。找了半天才发现是预处理代码里写了一个硬编码的除法。
嗯,这事之后我养成了一个习惯:每次做完标准化,随手画一个特征分布对比图,训练集和测试集都画,一对比就什么都清楚了。
核心原则:
- 确定目标模型是否对尺度敏感(树模型不敏感,NN/SVM 敏感)
- 选择正确的标准化方法(正态用 Z-score,有离群用 Robust)
- 永远只从训练集计算转化参数
- 每次转换后做可视化验证