引言
主成分分析(Principal Component Analysis, PCA)是最经典的无监督降维算法之一,由Karl Pearson于1901年提出。PCA通过线性变换将高维数据投影到低维空间,在保留数据最大方差的同时减少特征维度。本文将用Java完整实现PCA算法,深入讲解协方差矩阵计算、特征分解过程,并通过可视化展示降维效果。
算法原理
核心思想
PCA的目标是找到一组正交基(主成分),使得数据在该基上的投影方差最大。第一主成分方向是数据方差最大的方向,第二主成分方向是与第一主成分正交且方差次大的方向,依此类推。
数学推导
步骤1:数据标准化
对原始数据进行中心化处理:
$$\tilde{x}_i = x_i – \bar{x}$$
其中 $\bar{x}$ 为样本均值。
步骤2:计算协方差矩阵
$$C = \frac{1}{n-1} \sum_{i=1}^{n} \tilde{x}_i \tilde{x}_i^T = \frac{1}{n-1} X^T X$$
其中 $X$ 为中心化后的数据矩阵。
步骤3:特征分解
对协方差矩阵进行特征分解:
$$C v_i = \lambda_i v_i$$
其中 $\lambda_i$ 为特征值,$v_i$ 为对应的特征向量。
步骤4:选择主成分
按特征值从大到小排序,选择前 $k$ 个特征向量构成投影矩阵 $W = [v_1, v_2, …, v_k]$。
步骤5:数据投影
$$Y = X W$$
方差保留率
前 $k$ 个主成分保留的方差比例为:
$$\text{保留率} = \frac{\sum_{i=1}^{k} \lambda_i}{\sum_{i=1}^{d} \lambda_i}$$
Java实现
项目结构
src/
├── PCA.java // PCA核心类
├── MatrixUtils.java // 矩阵运算工具类
├── DataPoint.java // 数据点类
├── PCADemo.java // 演示入口
└── DatasetGenerator.java // 数据集生成器
矩阵运算工具类
/**
* 矩阵运算工具类
* 提供PCA所需的矩阵基本运算:乘法、转置、特征分解等
*/
public class MatrixUtils {
/**
* 矩阵乘法 C = A × B
*/
public static double[][] multiply(double[][] A, double[][] B) {
int rowsA = A.length;
int colsA = A[0].length;
int colsB = B[0].length;
double[][] C = new double[rowsA][colsB];
for (int i = 0; i < rowsA; i++) {
for (int j = 0; j < colsB; j++) {
double sum = 0;
for (int k = 0; k < colsA; k++) {
sum += A[i][k] * B[k][j];
}
C[i][j] = sum;
}
}
return C;
}
/**
* 矩阵转置
*/
public static double[][] transpose(double[][] A) {
int rows = A.length;
int cols = A[0].length;
double[][] T = new double[cols][rows];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
T[j][i] = A[i][j];
}
}
return T;
}
/**
* 向量点积
*/
public static double dotProduct(double[] a, double[] b) {
double sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
/**
* 向量归一化
*/
public static double[] normalize(double[] v) {
double norm = Math.sqrt(dotProduct(v, v));
double[] result = new double[v.length];
for (int i = 0; i < v.length; i++) {
result[i] = v[i] / norm;
}
return result;
}
/**
* 矩阵与向量乘法
*/
public static double[] multiplyMatrixVector(double[][] A, double[] v) {
int rows = A.length;
double[] result = new double[rows];
for (int i = 0; i < rows; i++) {
result[i] = dotProduct(A[i], v);
}
return result;
}
/**
* 打印矩阵
*/
public static void printMatrix(double[][] A, String name) {
System.out.println(name + ":");
for (double[] row : A) {
System.out.print(" ");
for (double val : row) {
System.out.printf("%10.4f ", val);
}
System.out.println();
}
}
/**
* 打印向量
*/
public static void printVector(double[] v, String name) {
System.out.print(name + ": [");
for (int i = 0; i < v.length; i++) {
System.out.printf("%.4f", v[i]);
if (i < v.length - 1) System.out.print(", ");
}
System.out.println("]");
}
}
PCA核心类
import java.util.Arrays;
import java.util.Comparator;
/**
* 主成分分析(PCA)算法实现
* 支持数据标准化、协方差矩阵计算、特征分解和降维投影
*/
public class PCA {
private double[] mean; // 各维度均值
private double[] std; // 各维度标准差
private double[][] components; // 主成分(特征向量)
private double[] explainedVariance; // 各主成分解释的方差(特征值)
private double[] explainedVarianceRatio; // 方差贡献率
private int nComponents; // 保留的主成分数
private int originalDim; // 原始维度
private boolean standardize; // 是否标准化
public PCA(int nComponents, boolean standardize) {
this.nComponents = nComponents;
this.standardize = standardize;
}
/**
* 拟合数据并执行PCA
* @param data 输入数据矩阵,每行一个样本
*/
public void fit(double[][] data) {
int nSamples = data.length;
originalDim = data[0].length;
System.out.println("=== PCA 拟合开始 ===");
System.out.println("样本数: " + nSamples);
System.out.println("原始维度: " + originalDim);
System.out.println("目标维度: " + nComponents);
System.out.println("标准化: " + standardize);
// 步骤1:计算均值
mean = new double[originalDim];
for (double[] sample : data) {
for (int j = 0; j < originalDim; j++) {
mean[j] += sample[j];
}
}
for (int j = 0; j < originalDim; j++) {
mean[j] /= nSamples;
}
MatrixUtils.printVector(mean, "均值向量");
// 步骤2:中心化和标准化
double[][] centered = new double[nSamples][originalDim];
std = new double[originalDim];
if (standardize) {
// 计算标准差
for (double[] sample : data) {
for (int j = 0; j < originalDim; j++) {
std[j] += Math.pow(sample[j] - mean[j], 2);
}
}
for (int j = 0; j < originalDim; j++) {
std[j] = Math.sqrt(std[j] / (nSamples - 1));
}
MatrixUtils.printVector(std, "标准差向量");
}
for (int i = 0; i < nSamples; i++) {
for (int j = 0; j < originalDim; j++) {
centered[i][j] = data[i][j] - mean[j];
if (standardize && std[j] > 1e-10) {
centered[i][j] /= std[j];
}
}
}
// 步骤3:计算协方差矩阵
double[][] covMatrix = computeCovarianceMatrix(centered);
MatrixUtils.printMatrix(covMatrix, "协方差矩阵");
// 步骤4:特征分解(使用幂迭代法)
EigenResult eigenResult = powerIteration(covMatrix, nComponents);
components = eigenResult.eigenvectors;
explainedVariance = eigenResult.eigenvalues;
// 计算方差贡献率
double totalVariance = 0;
for (double v : explainedVariance) {
totalVariance += v;
}
explainedVarianceRatio = new double[explainedVariance.length];
for (int i = 0; i < explainedVariance.length; i++) {
explainedVarianceRatio[i] = explainedVariance[i] / totalVariance;
}
// 输出结果
System.out.println("\n特征值与方差贡献率:");
for (int i = 0; i < explainedVariance.length; i++) {
System.out.printf(" 主成分 %d: λ=%.4f, 贡献率=%.2f%%%n",
i + 1, explainedVariance[i], explainedVarianceRatio[i] * 100);
}
double cumulativeRatio = 0;
for (int i = 0; i < nComponents; i++) {
cumulativeRatio += explainedVarianceRatio[i];
}
System.out.printf("\n前 %d 个主成分累计方差保留率: %.2f%%%n",
nComponents, cumulativeRatio * 100);
}
/**
* 计算协方差矩阵
*/
private double[][] computeCovarianceMatrix(double[][] centered) {
int n = centered.length;
int d = centered[0].length;
double[][] cov = new double[d][d];
for (int i = 0; i < d; i++) {
for (int j = i; j < d; j++) {
double sum = 0;
for (int k = 0; k < n; k++) {
sum += centered[k][i] * centered[k][j];
}
cov[i][j] = sum / (n - 1);
cov[j][i] = cov[i][j];
}
}
return cov;
}
/**
* 使用幂迭代法计算前k个最大特征值和特征向量
*/
private EigenResult powerIteration(double[][] matrix, int k) {
int n = matrix.length;
double[][] eigenvectors = new double[k][n];
double[] eigenvalues = new double[k];
// 复制矩阵用于后续deflation
double[][] workingMatrix = new double[n][n];
for (int i = 0; i < n; i++) {
System.arraycopy(matrix[i], 0, workingMatrix[i], 0, n);
}
for (int comp = 0; comp < k; comp++) {
// 随机初始化特征向量
double[] vector = new double[n];
for (int i = 0; i < n; i++) {
vector[i] = Math.random() - 0.5;
}
vector = MatrixUtils.normalize(vector);
// 幂迭代
double eigenvalue = 0;
for (int iter = 0; iter < 1000; iter++) {
double[] newVector = MatrixUtils.multiplyMatrixVector(workingMatrix, vector);
double newEigenvalue = MatrixUtils.dotProduct(vector, newVector);
newVector = MatrixUtils.normalize(newVector);
// 检查收敛
if (Math.abs(newEigenvalue - eigenvalue) < 1e-10) {
break;
}
vector = newVector;
eigenvalue = newEigenvalue;
}
eigenvectors[comp] = vector;
eigenvalues[comp] = eigenvalue;
// Deflation: 移除当前特征向量的影响
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
workingMatrix[i][j] -= eigenvalue * vector[i] * vector[j];
}
}
}
return new EigenResult(eigenvectors, eigenvalues);
}
/**
* 将数据投影到主成分空间
*/
public double[][] transform(double[][] data) {
int nSamples = data.length;
double[][] projected = new double[nSamples][nComponents];
for (int i = 0; i < nSamples; i++) {
// 中心化和标准化
double[] standardized = new double[originalDim];
for (int j = 0; j < originalDim; j++) {
standardized[j] = data[i][j] - mean[j];
if (standardize && std[j] > 1e-10) {
standardized[j] /= std[j];
}
}
// 投影到主成分
for (int k = 0; k < nComponents; k++) {
projected[i][k] = MatrixUtils.dotProduct(standardized, components[k]);
}
}
return projected;
}
/**
* 拟合并转换数据
*/
public double[][] fitTransform(double[][] data) {
fit(data);
return transform(data);
}
/**
* 从降维数据重构原始维度数据
*/
public double[][] inverseTransform(double[][] projected) {
int nSamples = projected.length;
double[][] reconstructed = new double[nSamples][originalDim];
for (int i = 0; i < nSamples; i++) {
for (int j = 0; j < originalDim; j++) {
double sum = mean[j];
for (int k = 0; k < nComponents; k++) {
sum += projected[i][k] * components[k][j];
if (standardize && std[j] > 1e-10) {
// 反标准化需要在投影累加后进行
// 这里简化处理
}
}
reconstructed[i][j] = sum;
}
}
return reconstructed;
}
public double[][] getComponents() {
return components;
}
public double[] getExplainedVarianceRatio() {
return explainedVarianceRatio;
}
/**
* 内部类:存储特征分解结果
*/
private static class EigenResult {
double[][] eigenvectors;
double[] eigenvalues;
EigenResult(double[][] eigenvectors, double[] eigenvalues) {
this.eigenvectors = eigenvectors;
this.eigenvalues = eigenvalues;
}
}
}
数据集生成器
import java.util.Random;
/**
* 生成用于PCA演示的数据集
*/
public class DatasetGenerator {
private Random random;
public DatasetGenerator(long seed) {
this.random = new Random(seed);
}
/**
* 生成具有强相关性的三维数据(实际分布接近二维平面)
*/
public double[][] generateCorrelated3D(int n) {
double[][] data = new double[n][3];
for (int i = 0; i < n; i++) {
double x = random.nextGaussian() * 3;
double y = x * 0.8 + random.nextGaussian() * 0.5;
double z = x * 0.6 + y * 0.3 + random.nextGaussian() * 0.3;
data[i][0] = x;
data[i][1] = y;
data[i][2] = z;
}
return data;
}
/**
* 生成高维数据(10维,但主要方差集中在前3维)
*/
public double[][] generateHighDimData(int n, int dim) {
double[][] data = new double[n][dim];
for (int i = 0; i < n; i++) {
// 前3维携带主要信息
double v1 = random.nextGaussian() * 5;
double v2 = random.nextGaussian() * 3;
double v3 = random.nextGaussian() * 2;
data[i][0] = v1;
data[i][1] = v2;
data[i][2] = v3;
// 其余维度为噪声
for (int j = 3; j < dim; j++) {
data[i][j] = random.nextGaussian() * 0.5;
}
}
return data;
}
}
演示入口
/**
* PCA算法演示
*/
public class PCADemo {
public static void main(String[] args) {
demo3DTo2D();
System.out.println("\n" + "=".repeat(60) + "\n");
demoHighDimReduction();
}
/**
* 演示1:三维数据降维到二维
*/
private static void demo3DTo2D() {
System.out.println("=== 演示1:三维数据降维到二维 ===\n");
DatasetGenerator generator = new DatasetGenerator(42);
double[][] data3D = generator.generateCorrelated3D(100);
System.out.println("原始三维数据样本(前5条):");
for (int i = 0; i < Math.min(5, data3D.length); i++) {
System.out.printf(" 样本%d: [%.2f, %.2f, %.2f]%n",
i + 1, data3D[i][0], data3D[i][1], data3D[i][2]);
}
PCA pca = new PCA(2, true);
double[][] data2D = pca.fitTransform(data3D);
System.out.println("\n降维后二维数据样本(前5条):");
for (int i = 0; i < Math.min(5, data2D.length); i++) {
System.out.printf(" 样本%d: [%.4f, %.4f]%n",
i + 1, data2D[i][0], data2D[i][1]);
}
// 打印主成分方向
double[][] components = pca.getComponents();
System.out.println("\n主成分方向:");
for (int i = 0; i < components.length; i++) {
System.out.printf(" PC%d: [", i + 1);
for (int j = 0; j < components[i].length; j++) {
System.out.printf("%.4f", components[i][j]);
if (j < components[i].length - 1) System.out.print(", ");
}
System.out.println("]");
}
}
/**
* 演示2:十维数据降维
*/
private static void demoHighDimReduction() {
System.out.println("=== 演示2:十维数据降维分析 ===\n");
DatasetGenerator generator = new DatasetGenerator(123);
double[][] data10D = generator.generateHighDimData(200, 10);
System.out.println("原始维度: 10");
System.out.println("样本数: 200");
// 尝试不同的降维维度
for (int k : new int[]{2, 3, 5}) {
PCA pca = new PCA(k, true);
pca.fit(data10D);
double[] ratio = pca.getExplainedVarianceRatio();
double cumulative = 0;
for (double r : ratio) {
cumulative += r;
}
System.out.printf("\n降维到 %d 维: 累计方差保留率 = %.2f%%%n", k, cumulative * 100);
}
}
}
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 协方差矩阵计算 | $O(n \cdot d^2)$ | $n$为样本数,$d$为原始维度 |
| 特征分解(幂迭代) | $O(k \cdot d^2 \cdot T)$ | $k$为主成分数,$T$为迭代次数 |
| 数据投影 | $O(n \cdot d \cdot k)$ | 将数据投影到k维空间 |
| 空间复杂度 | $O(d^2 + n \cdot d)$ | 存储协方差矩阵和主成分 |
算法应用
图像压缩
PCA可用于图像压缩:将图像分块后,对每个块进行PCA降维,只保留主成分系数,实现有损压缩。
人脸识别
Eigenfaces方法使用PCA提取人脸图像的主要特征,将高维像素空间降维到特征脸空间,实现高效的人脸识别。
数据可视化
将高维数据降维到2D或3D,便于可视化分析和模式发现。
总结
PCA通过协方差矩阵特征分解找到数据方差最大的方向,实现无监督降维。本文的Java实现完整展示了从数据标准化、协方差计算到幂迭代特征分解的全过程。PCA不仅是降维利器,更是理解数据内在结构的重要工具,为后续的流形学习、自编码器等深度降维方法奠定了基础。