Home

模型可视化方法

专为 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),我封装了一个通用的处理类。

功能亮点:

  1. 自动识别方法类型。
  2. 针对 t-SNE 做出了“禁止 Transform”的逻辑保护。
  3. 自动处理模型保存与加载。

新建文件 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 标签),它找的是“最能把不同类别的样本分开”的方向。
  • 场景: 如果你的任务是分类(比如:药物是否有毒性

5.MDS,随机森林筛选 / Pearson 相关 / LASSO