深度学习:手动实现PCA算法

125 阅读7分钟

任务:手动实现PCA算法并应用于手写数字数据集

一、简介

手写数字数据集(MNIST dataset)是深度学习领域中一个非常经典的数据集,包含了70,000张28×28像素的手写数字图像,每个图像有784个特征(每个像素点的灰度值),并且分为10个类别(0-9)。PCA可以用于降维,减少计算复杂度,同时还可以用于数据可视化。

二、步骤

1. 数据加载与预处理

· 使用sklearn.datasets加载MNIST数据集。

· 将数据集分为特征矩阵X和目标标签y。

· 对特征矩阵X进行归一化处理(将像素值缩放到[0, 1]区间)。

2. 手动实现PCA算法

· 编写一个函数manual_pca(X, n_components),手动实现PCA算法,将数据降维到指定的维度n_components。

· PCA算法步骤:

1. 对数据进行中心化(减去均值)。

2. 计算协方差矩阵。

3. 对协方差矩阵进行特征分解,获取特征值和特征向量。

4. 选择最大的n_components个特征值对应的特征向量,组成降维矩阵。

5. 使用降维矩阵对原始数据进行降维。

3. 降维与可视化

· 使用手动实现的PCA函数将MNIST数据集降维到2维。

· 使用matplotlib库绘制降维后的数据点,并根据目标标签y对不同类别的数据点使用不同的颜色进行区分。

· 添加合适的标题、坐标轴标签和图例,使图形清晰易懂。

4. 性能对比

· 使用sklearn.decomposition.PCA类对MNIST数据集进行降维到2维。

· 比较手动实现的PCA和sklearn的PCA在降维结果上的差异(可以通过可视化对比)。

· 分析两种方法的运行时间和内存占用情况。

5. 解释方差

· 打印手动实现的PCA模型的解释方差比率(explained_variance_ratio_),分析前两个主成分所占的方差比例。

三、代码分析 

3.1 数据加载函数

def load_mnist(path, kind='train'):

    """从idx文件加载MNIST数据"""

    raw_path = os.path.join(path, "MNIST""raw")

    if not os.path.exists(raw_path):

        raise FileNotFoundError(f"未找到raw目录:{raw_path}")

    

    # 解析标签文件(.idx1格式)

    labels_path = os.path.join(raw_path, f"{kind}-labels-idx1-ubyte")

    with open(labels_path, 'rb'as f:

        magic, n = struct.unpack('>II', f.read(8))  # 前4字节为魔数,后4字节为样本数

        labels = np.fromfile(f, dtype=np.uint8)     # 读取标签数据

    

    # 解析图像文件(.idx3格式)

    images_path = os.path.join(raw_path, f"{kind}-images-idx3-ubyte")

    with open(images_path, 'rb'as f:

        magic, num, rows, cols = struct.unpack('>IIII', f.read(16))

        images = np.fromfile(f, dtype=np.uint8).reshape(num, rows*cols)  # 展平为784维特征

    

    return images, labels

3.2 数据预处理

def load_and_preprocess_data(data_root):

    """加载数据并归一化"""

    print(f"正在从 {data_root} 加载MNIST数据...")

    X_train, y_train = load_mnist(data_root, kind='train')

    X_test, y_test = load_mnist(data_root, kind='t10k')

    

    print(f"训练集大小: {X_train.shape[0]}样本,特征维度: {X_train.shape[1]}")

    print(f"测试集大小: {X_test.shape[0]}样本")

    

    # 归一化到[0, 1]区间

    scaler = MinMaxScaler()

    X_train_scaled = scaler.fit_transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    

    return X_train_scaled, X_test_scaled, y_train, y_test

 

3.3 手动实现 PCA(核心步骤)

def manual_pca(X, n_components):

    """手动实现PCA算法"""

    # 1. 数据中心化

    X_mean = np.mean(X, axis=0)

    X_centered = X - X_mean

    

    # 2. 计算协方差矩阵(样本数多时有优化空间)

    cov_matrix = np.cov(X_centered, rowvar=False)  # rowvar=False表示样本为行,特征为列

    

    # 3. 特征值分解(对称矩阵用eigh效率更高)

    eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)

    

    # 4. 按特征值降序排序

    idx = np.argsort(eigenvalues)[::-1]

    eigenvalues = eigenvalues[idx]

    eigenvectors = eigenvectors[:, idx]

    

    # 5. 选择主成分并降维

    top_eigenvectors = eigenvectors[:, :n_components]  # 前k个特征向量

    X_reduced = np.dot(X_centered, top_eigenvectors)   # 投影到低维空间

    

    # 计算解释方差比率

    total_variance = np.sum(eigenvalues)

    explained_variance_ratio = eigenvalues[:n_components] / total_variance

    

    return X_reduced, explained_variance_ratio

 

 

3.4 主函数逻辑

def main():

    # 1. 数据加载与预处理

    X_train, X_test, y_train, y_test = load_and_preprocess_data(data_root)

    

    # 2. 选取部分样本加速实验(MNIST样本量较大)

    sample_size5000

    X_sample = X_train[:sample_size]

    y_sample = y_train[:sample_size]

    print(f"\n使用 {sample_size} 个样本进行分析")

    

    # 3. 手动PCA实验

    manual_time, manual_mem, X_manual, ev_manual = run_pca(manual_pca, X_sample, 2)

    

    # 4. sklearn PCA实验

    sklearn_time, sklearn_mem, X_sklearn, ev_sklearn = run_pca(sklearn_pca, X_sample, 2)

    

    # 5. 结果可视化与对比

    plot_comparison(X_manual, X_sklearn, y_sample)

    plot_explained_variance(ev_manual, ev_sklearn)

3.5 性能统计函数

def run_pca(pca_func, X, n_components):

    """运行PCA并统计时间和内存"""

    start_time = time.time()

    mem_before = psutil.Process(os.getpid()).memory_info().rss / 1024**2  # MB

    

    X_reduced, ev_ratio = pca_func(X, n_components)

    

    mem_after = psutil.Process(os.getpid()).memory_info().rss / 1024**2

    run_time = time.time() - start_time

    mem_usage = mem_after - mem_before

    

    print(f"运行时间: {run_time:.4f}秒 | 内存占用: {mem_usage:.2f} MB")

    print(f"解释方差比率: {ev_ratio.round(4)}")

    print(f"前两主成分方差和: {np.sum(ev_ratio):.4f}\n")

    

    return run_time, mem_usage, X_reduced, ev_ratio

四、完整代码

import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.decomposition import PCA
import time
import psutil
import os
from struct import unpack

# 设置中文显示
plt.rcParams["font.family"] = ["Microsoft YaHei", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示问题

def load_mnist(path, kind='train'):
    """
    从指定路径的raw子目录加载MNIST数据(idx格式)
    
    参数:
    path: 数据集根目录(包含raw子目录)
    kind: 'train'或't10k',分别表示训练集和测试集
    
    返回:
    images: 图像数据,形状为(n_samples, 784)
    labels: 标签数据,形状为(n_samples,)
    """
    raw_path = os.path.join(path, "MNIST", "raw")  # 指向raw子目录
    if not os.path.exists(raw_path):
        raise FileNotFoundError(f"未找到raw目录:{raw_path}")
    
    labels_path = os.path.join(raw_path, f"{kind}-labels-idx1-ubyte")
    images_path = os.path.join(raw_path, f"{kind}-images-idx3-ubyte")
    
    # 读取标签文件
    with open(labels_path, 'rb') as lbpath:
        magic, n = unpack('>II', lbpath.read(8))  # 解析文件头
        labels = np.fromfile(lbpath, dtype=np.uint8)  # 读取标签数据
    
    # 读取图像文件
    with open(images_path, 'rb') as imgpath:
        magic, num, rows, cols = unpack('>IIII', imgpath.read(16))  # 解析文件头
        images = np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784)  # 展平为784维特征
    
    return images, labels

def load_and_preprocess_data(data_root):
    """加载并预处理MNIST数据集"""
    print(f"正在从 {data_root} 加载MNIST数据...")
    X_train, y_train = load_mnist(data_root, kind='train')
    X_test, y_test = load_mnist(data_root, kind='t10k')
    
    print(f"训练集样本数: {X_train.shape[0]}, 特征维度: {X_train.shape[1]}")
    print(f"测试集样本数: {X_test.shape[0]}")
    
    # 数据归一化(缩放到[0, 1])
    scaler = MinMaxScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)
    
    return X_train_scaled, X_test_scaled, y_train, y_test

def manual_pca(X, n_components):
    """手动实现PCA算法(同前)"""
    X_mean = np.mean(X, axis=0)
    X_centered = X - X_mean
    cov_matrix = np.cov(X_centered, rowvar=False)
    eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
    idx = np.argsort(eigenvalues)[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]
    top_eigenvectors = eigenvectors[:, :n_components]
    X_reduced = np.dot(X_centered, top_eigenvectors)
    total_variance = np.sum(eigenvalues)
    explained_variance_ratio = eigenvalues[:n_components] / total_variance
    return X_reduced, explained_variance_ratio

def plot_pca_results(X_reduced, y, title, ax=None):
    """可视化降维结果(同前)"""
    if ax is None:
        fig, ax = plt.subplots(figsize=(10, 8))
    y_int = y.astype(int)
    colors = plt.cm.tab10.colors
    for digit in range(10):
        mask = (y_int == digit)
        ax.scatter(X_reduced[mask, 0], X_reduced[mask, 1], 
                   label=str(digit), alpha=0.6, s=10, c=[colors[digit]])
    ax.set_title(title, fontsize=14)
    ax.set_xlabel("第一主成分", fontsize=12)
    ax.set_ylabel("第二主成分", fontsize=12)
    ax.legend(title="数字标签", loc='best', frameon=True, markerscale=2)
    ax.grid(True, linestyle='--', alpha=0.7)
    return ax

def get_memory_usage():
    """获取内存占用(同前)"""
    process = psutil.Process(os.getpid())
    return process.memory_info().rss / 1024 / 1024  # MB

def main():
    # 你的数据集根目录(包含MNIST/raw子目录)
    data_root = r""
    
    # 加载数据并预处理
    X_train, X_test, y_train, y_test = load_and_preprocess_data(data_root)
    
    # 取部分样本加速计算(MNIST训练集共60000样本,取5000测试)
    sample_size = 5000
    X_sample = X_train[:sample_size]
    y_sample = y_train[:sample_size]
    print(f"\n使用 {sample_size} 个训练样本进行分析")
    
    # ---------------------- 手动PCA ----------------------
    print("\n---------- 手动实现PCA ----------")
    start_time = time.time()
    mem_before = get_memory_usage()
    X_reduced_manual, explained_var_manual = manual_pca(X_sample, n_components=2)
    manual_time = time.time() - start_time
    manual_mem = get_memory_usage() - mem_before
    print(f"运行时间: {manual_time:.4f} 秒 | 内存占用: {manual_mem:.2f} MB")
    print(f"解释方差比率: {explained_var_manual.round(4)}")
    print(f"前两主成分方差和: {np.sum(explained_var_manual):.4f}")
    
    # ---------------------- sklearn PCA ----------------------
    print("\n---------- sklearn实现PCA ----------")
    start_time = time.time()
    mem_before = get_memory_usage()
    pca_sklearn = PCA(n_components=2)
    X_reduced_sklearn = pca_sklearn.fit_transform(X_sample)
    sklearn_time = time.time() - start_time
    sklearn_mem = get_memory_usage() - mem_before
    print(f"运行时间: {sklearn_time:.4f} 秒 | 内存占用: {sklearn_mem:.2f} MB")
    print(f"解释方差比率: {pca_sklearn.explained_variance_ratio_.round(4)}")
    print(f"前两主成分方差和: {np.sum(pca_sklearn.explained_variance_ratio_):.4f}")
    
    # ---------------------- 可视化对比 ----------------------
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 8))
    plot_pca_results(X_reduced_manual, y_sample, "手动PCA降维结果", ax1)
    plot_pca_results(X_reduced_sklearn, y_sample, "sklearn PCA降维结果", ax2)
    plt.suptitle("PCA降维效果对比", fontsize=16, y=1.02)
    plt.tight_layout()
    plt.savefig("pca_comparison.png", dpi=300)
    plt.show()
    
    # ---------------------- 解释方差对比图 ----------------------
    plt.figure(figsize=(10, 6))
    bars = plt.bar(
        ["手动PC1", "手动PC2", "sklearn PC1", "sklearn PC2"],
        [explained_var_manual[0], explained_var_manual[1],
         pca_sklearn.explained_variance_ratio_[0], pca_sklearn.explained_variance_ratio_[1]],
        color=["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78"],
        width=0.4
    )
    for bar in bars:
        height = bar.get_height()
        plt.text(bar.get_x() + bar.get_width()/2, height, f"{height:.4f}", ha="center", va="bottom")
    plt.title("主成分解释方差比率", fontsize=14)
    plt.ylabel("解释方差比例", fontsize=12)
    plt.grid(axis="y", linestyle="--", alpha=0.7)
    plt.tight_layout()
    plt.savefig("explained_variance.png", dpi=300)
    plt.show()

if __name__ == "__main__":
    main()

五、结果

image.png

image.png

image.png