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,导致统计量偏掉。
我的建议:做完标准化后,随手打印一下转换后数据的均值和方差。如果均值不是 0 或方差不是 1,说明哪里出错了。这个小检查花不了 10 秒钟,但能挡住一大半问题。

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 知识体系总览

下图的流程,是我自己项目里总结出来的决策路径。每次做预处理前,我都会过一遍这张图,节省了大量试错时间。

标准化/归一化决策流程 原始描述符数据 检查特征尺度范围 数据近似正态分布 Z-score 标准化 存在离群点/偏态分布 Robust 标准化 统一尺度描述符矩阵 建议画箱线图 做可视化验证

4.6 常见错误速查表

下面是我自己整理的「踩坑清单」,每次预处理跑不过就对着查一遍。

错误 现象 原因 解决方法
训练/测试集分别标准化 验证集指标异常好,外部泛化差 数据泄漏,测试集信息进入训练环节 只对训练集 fit,再用该 scaler transform 测试集
Min-Max 后特征坍缩 大量样本的某特征值集中在 [0, 0.01] 存在极端离群点拉高了 max,压缩了正常范围 改用 RobustScaler,或先剔除/缩尾处理离群点
对离散/二进制特征做标准化 模型权重异常大或 NaN 二进制特征(如 0/1 指纹)标准化后失去稀疏性 二进制特征不要标准化,用 pass_through
标准化参数固化后混入新数据 新批次数据预测效果断崖式下降 旧参数无法覆盖新数据的范围 收集数据后重新拟合 scaler,或用在线标准化策略

4.7 经验总结

标准化这件事,看着简单,但翻车的概率其实很高。我个人遇到过最好笑的一次是,有个团队把训练集的 logP 列手动除以了 10,然后模型收敛了,但结果解释时发现:预测的 logP 值比真实值大了 10 倍。找了半天才发现是预处理代码里写了一个硬编码的除法。

嗯,这事之后我养成了一个习惯:每次做完标准化,随手画一个特征分布对比图,训练集和测试集都画,一对比就什么都清楚了。

核心原则:

  1. 确定目标模型是否对尺度敏感(树模型不敏感,NN/SVM 敏感)
  2. 选择正确的标准化方法(正态用 Z-score,有离群用 Robust)
  3. 永远只从训练集计算转化参数
  4. 每次转换后做可视化验证