模型可视化方法
专为 AI4Science/ChemE 设计的降维指南:从原理到 Python 通用模块实现,彻底搞懂什么时候用 PCA,什么时候用 TruncatedSVD。
在 AI4ChemE(化学工程人工智能)的研究中,我们经常面临“维度灾难”。无论是 2048 位的 Morgan Fingerprints (ECFP),还是图神经网络生成的高维节点嵌入 (Node Embeddings),直接扔进全连接层往往会造成计算冗余或过拟合。
在构建深度学习模型前,合理的降维(Dimensionality Reduction) 是特征工程中不可或缺的一环。
本文将总结四种最主流的降维方法,并提供一个通用的 Python 模块,方便在任意项目中一键接入。
做 Input Feature 时:
化学指纹 / 稀疏数据 ———— TruncatedSVD
Dense Embedding / 连续数据 ———— PCA
追求非线性效果 ———— UMAP (或 AutoEncoder)
做 Visualization 时:
t-SNE (经典,效果好,慢)
UMAP (现代,快,保结构)
1. 核心方法论:怎么选?
在开始写代码之前,我们需要理清数学原理。对于化学数据,选择方法的逻辑如下:
线性降维派 (Linear)
- PCA (主成分分析):
- 原理: 寻找数据方差最大的方向进行投影。需对数据中心化 (Centering)。
- 适用: 稠密向量 (Dense Vectors),如预训练模型的 Embeddings。
- 忌讳: 稀疏数据 (Sparse Data)。因为 Center 操作会把 0 变成非 0,破坏稀疏性,导致内存爆炸。
- TruncatedSVD (截断奇异值分解):
- 原理: 类似于 PCA,但不进行中心化。
- 适用: 稀疏矩阵 (Sparse Matrix)。这是 ChemE 领域的首选,特别适合处理 One-Hot 编码或 ECFP 分子指纹。
流形学习派 (Manifold / Non-linear)
- t-SNE:
- 定位: 仅用于可视化。
- 缺陷: 它学习的是样本坐标而非映射函数。这意味着如果有新的测试集数据,t-SNE 无法直接将其转换到同一个低维空间。严禁用于模型训练的输入特征。
- UMAP:
- 定位: 可视化 + 特征提取。
- 优势: 比 t-SNE 快,保留更多全局结构,且支持 transform 新数据。可以用作非线性特征降维器。
实战示例
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.manifold import TSNE
from sklearn.datasets import make_classification, make_blobs
# 1. 模拟数据生成
# 模拟 Mordred (连续特征): 1075个样本, 50个特征
X_mordred, y_type = make_classification(n_samples=1075, n_features=50, n_informative=10, n_classes=3, random_state=42)
# 模拟产率 (与特征有一定相关性,但也有些噪声)
y_yield = X_mordred[:, 0] * 0.5 + X_mordred[:, 1] * 0.3 + np.random.normal(0, 1, 1075)
# 归一化产率到 0-100%
y_yield = (y_yield - y_yield.min()) / (y_yield.max() - y_yield.min()) * 100
# 模拟 Morgan 指纹 (稀疏二值特征): 1075个样本, 1024 bits
# 使用 make_blobs 模拟聚类结构,然后二值化模拟指纹
X_morgan_raw, _ = make_blobs(n_samples=1075, n_features=1024, centers=5, random_state=42)
X_morgan = (X_morgan_raw > 0).astype(int)
# 2. 降维可视化
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
plt.subplots_adjust(hspace=0.3)
# --- A. Mordred 特征分析 (推荐 PCA 或 t-SNE) ---
# A1. PCA on Mordred (看整体分布)
pca = PCA(n_components=2)
mordred_pca = pca.fit_transform(X_mordred)
sc1 = axes[0, 0].scatter(mordred_pca[:, 0], mordred_pca[:, 1], c=y_yield, cmap='viridis', alpha=0.7, s=15)
axes[0, 0].set_title('PCA on Mordred Features\n(Color by Yield)', fontsize=14)
axes[0, 0].set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%} var)')
axes[0, 0].set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%} var)')
plt.colorbar(sc1, ax=axes[0, 0], label='Yield (%)')
# A2. t-SNE on Mordred (看局部聚类)
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
mordred_tsne = tsne.fit_transform(X_mordred)
sc2 = axes[0, 1].scatter(mordred_tsne[:, 0], mordred_tsne[:, 1], c=y_type, cmap='tab10', alpha=0.7, s=15)
axes[0, 1].set_title('t-SNE on Mordred Features\n(Color by Reaction Type)', fontsize=14)
axes[0, 1].set_xlabel('t-SNE 1')
axes[0, 1].set_ylabel('t-SNE 2')
# create a legend strictly for the reaction types
handles, _ = sc2.legend_elements()
axes[0, 1].legend(handles, ['Type A', 'Type B', 'Type C'], title="Reaction Type")
# --- B. Morgan 指纹分析 (推荐 TruncatedSVD 或 UMAP/t-SNE) ---
# B1. TruncatedSVD on Morgan (稀疏数据的PCA)
svd = TruncatedSVD(n_components=2)
morgan_svd = svd.fit_transform(X_morgan)
sc3 = axes[1, 0].scatter(morgan_svd[:, 0], morgan_svd[:, 1], c=y_yield, cmap='viridis', alpha=0.7, s=15)
axes[1, 0].set_title('TruncatedSVD on Morgan Fingerprints\n(Color by Yield)', fontsize=14)
axes[1, 0].set_xlabel('Component 1')
axes[1, 0].set_ylabel('Component 2')
plt.colorbar(sc3, ax=axes[1, 0], label='Yield (%)')
# B2. t-SNE on Morgan (Jaccard距离往往更好,这里演示标准Euclidean)
tsne_morgan = TSNE(n_components=2, perplexity=30, init='pca', learning_rate='auto', random_state=42)
morgan_tsne_proj = tsne_morgan.fit_transform(X_morgan)
sc4 = axes[1, 1].scatter(morgan_tsne_proj[:, 0], morgan_tsne_proj[:, 1], c=y_type, cmap='tab10', alpha=0.7, s=15)
axes[1, 1].set_title('t-SNE on Morgan Fingerprints\n(Color by Reaction Type)', fontsize=14)
axes[1, 1].set_xlabel('t-SNE 1')
axes[1, 1].set_ylabel('t-SNE 2')
axes[1, 1].legend(handles, ['Type A', 'Type B', 'Type C'], title="Reaction Type")
plt.savefig('dim_reduction_analysis.png', dpi=300, bbox_inches='tight')

2. 通用降维模块代码 (UniversalReducer)
为了在科研项目中保持代码整洁,并防止数据泄露 (Data Leakage),我封装了一个通用的处理类。
功能亮点:
- 自动识别方法类型。
- 针对 t-SNE 做出了“禁止 Transform”的逻辑保护。
- 自动处理模型保存与加载。
新建文件 dim_reduction.py:
import numpy as np
import joblib
import os
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
# 尝试导入 UMAP,如果未安装则跳过
try:
import umap
except ImportError:
umap = None
class UniversalReducer(BaseEstimator, TransformerMixin):
"""
通用降维器 v2.0 for AI4Science
集成逻辑判断:自动识别稀疏数据,自动警告不可用于 Inference 的方法。
"""
def __init__(self, method='pca', n_components=2, sparse_input=False, save_dir=None, **kwargs):
"""
Args:
method: 'pca', 'svd', 'lda', 'umap', 'tsne'
n_components: 目标维度
sparse_input: 输入是否为稀疏矩阵 (针对化学指纹建议开启)
save_dir: 模型保存路径
"""
self.method = method.lower()
self.n_components = n_components
self.sparse_input = sparse_input
self.save_dir = save_dir
self.kwargs = kwargs
self.model = None
# 验证方法是否适合作为模型输入
if self.method == 'tsne':
print("【Warning】: t-SNE 不具备参数化映射能力,严禁用于模型训练输入,仅供可视化使用!")
def _get_model(self):
"""工厂模式生成模型"""
if self.method == 'pca':
# 保护机制:如果是稀疏数据,强制切换到 SVD
if self.sparse_input:
print(">> Warning: PCA does not support sparse input efficiently. Auto-switching to TruncatedSVD.")
return TruncatedSVD(n_components=self.n_components, **self.kwargs)
return PCA(n_components=self.n_components, **self.kwargs)
elif self.method == 'svd':
return TruncatedSVD(n_components=self.n_components, **self.kwargs)
elif self.method == 'lda':
return LDA(n_components=self.n_components, **self.kwargs)
elif self.method == 'umap':
if umap is None: raise ImportError("Please install umap-learn via pip")
return umap.UMAP(n_components=self.n_components, **self.kwargs)
elif self.method == 'tsne':
from sklearn.manifold import TSNE
return TSNE(n_components=self.n_components, **self.kwargs)
else:
raise ValueError(f"Unknown method: {self.method}")
def fit(self, X, y=None):
"""仅在训练集上调用"""
self.model = self._get_model()
if self.method == 'tsne':
print(">> t-SNE skipping fit (it uses fit_transform directly).")
return self
# LDA 需要标签 y
if self.method == 'lda':
if y is None: raise ValueError("LDA requires labels (y) for fitting.")
self.model.fit(X, y)
else:
self.model.fit(X)
# 自动保存模型(便于 Inference 阶段加载)
if self.save_dir and self.method != 'tsne':
os.makedirs(self.save_dir, exist_ok=True)
joblib.dump(self.model, os.path.join(self.save_dir, f'{self.method}_model.pkl'))
print(f">> Model saved to {self.save_dir}")
return self
def transform(self, X):
"""将数据转换到低维空间"""
if self.method == 'tsne':
raise NotImplementedError("Sklearn t-SNE does not support transform on new data.")
if self.model is None:
raise RuntimeError("Model must be fitted before transform.")
return self.model.transform(X)
def fit_transform(self, X, y=None):
"""快捷调用"""
if self.method == 'tsne':
self.model = self._get_model()
return self.model.fit_transform(X)
self.fit(X, y)
return self.transform(X)3.自编码器 (Autoencoder)/变分自编码器 (VAE)
- 原理: 训练一个神经网络 $Input \to Encoder \to Bottleneck \to Decoder \to Output$。中间的 Bottleneck 就是降维后的特征。
- 亮点: 它是非线性的,且可以处理极其复杂的映射。在化学中,训练一个 VAE 将分子压缩成潜向量(Latent Vector),是分子生成和性质预测的顶刊主流做法(如 CDDD 模型)。
- 分类: 属于非线性、参数化方法
4.LDA (Linear Discriminant Analysis, 线性判别分析)
- 原理: PCA 是无监督的(不看 Y 标签),只找方差最大的方向。LDA 是有监督的(看 Y 标签),它找的是“最能把不同类别的样本分开”的方向。
- 场景: 如果你的任务是分类(比如:药物是否有毒性