引言
聚类分析是机器学习中最基础的无监督学习任务之一。传统的K-means聚类基于距离度量,在处理球形分布数据时表现良好,但对于非凸形状、嵌套结构或噪声较大的数据集往往力不从心。
谱聚类(Spectral Clustering)是一种基于图论的聚类方法,它将数据点视为图的顶点,通过构建相似度图并分析其拉普拉斯矩阵的特征向量,将数据映射到低维空间后再进行聚类。这种方法能够发现任意形状的簇结构,在图像分割、社交网络分析和文本聚类等领域表现出色。本文将用Java完整实现谱聚类算法,包含相似度图构建、拉普拉斯矩阵计算、特征分解和K-means后处理。
算法原理
核心思想
谱聚类的核心思想是将聚类问题转化为图划分问题:
1. 构建相似度图:将数据点连接成图,边的权重表示点之间的相似度
2. 计算拉普拉斯矩阵:L = D – W,其中D为度矩阵,W为权重矩阵
3. 特征分解:计算拉普拉斯矩阵的前k个最小特征向量
4. 低维映射与聚类:将数据映射到特征向量构成的低维空间,使用K-means聚类
拉普拉斯矩阵类型
未归一化拉普拉斯矩阵:L = D – W,适用于大多数场景
对称归一化拉普拉斯矩阵:L_sym = I – D^(-1/2)WD^(-1/2),对度分布不均的数据更稳健
随机游走拉普拉斯矩阵:L_rw = I – D^(-1)W,与随机游走视角等价
本文实现对称归一化版本,兼顾理论性质和实际效果。
相似度度量
常用高斯核函数(RBF核)计算相似度:
w(i,j) = exp(-||x_i - x_j||^2 / (2 * sigma^2))
sigma参数控制邻域范围,通常设置为数据点间距离的中位数。
Java实现
项目结构
src/
├── SpectralClustering.java // 主算法类
├── Matrix.java // 矩阵运算工具
├── KMeans.java // K-means聚类
└── Main.java // 测试入口
Matrix.java – 矩阵运算
/**
* 矩阵运算工具类
* 提供基础的矩阵操作:加减乘、转置、逆、特征分解等
*/
public class Matrix {
private double[][] data;
private int rows;
private int cols;
public Matrix(int rows, int cols) {
this.rows = rows;
this.cols = cols;
this.data = new double[rows][cols];
}
public Matrix(double[][] data) {
this.rows = data.length;
this.cols = data[0].length;
this.data = new double[rows][cols];
for (int i = 0; i < rows; i++) {
System.arraycopy(data[i], 0, this.data[i], 0, cols);
}
}
public double get(int i, int j) { return data[i][j]; }
public void set(int i, int j, double val) { data[i][j] = val; }
public int getRows() { return rows; }
public int getCols() { return cols; }
/**
* 矩阵乘法
*/
public Matrix multiply(Matrix other) {
Matrix result = new Matrix(rows, other.cols);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < other.cols; j++) {
double sum = 0;
for (int k = 0; k < cols; k++) {
sum += data[i][k] * other.data[k][j];
}
result.set(i, j, sum);
}
}
return result;
}
/**
* 矩阵转置
*/
public Matrix transpose() {
Matrix result = new Matrix(cols, rows);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result.set(j, i, data[i][j]);
}
}
return result;
}
/**
* 对称矩阵的特征分解(使用Jacobi迭代法)
* 返回特征值和特征向量矩阵
*/
public EigenResult eigenDecomposition() {
int n = rows;
double[][] a = new double[n][n];
double[][] v = new double[n][n];
for (int i = 0; i < n; i++) {
System.arraycopy(data[i], 0, a[i], 0, n);
v[i][i] = 1.0;
}
// Jacobi迭代
for (int iter = 0; iter < 100; iter++) {
// 找最大非对角元
int p = 0, q = 1;
double max = Math.abs(a[0][1]);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (Math.abs(a[i][j]) > max) {
max = Math.abs(a[i][j]);
p = i; q = j;
}
}
}
if (max < 1e-10) break;
// 计算旋转角度
double theta = (a[q][q] - a[p][p]) / (2 * a[p][q]);
double t = Math.signum(theta) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
double c = 1 / Math.sqrt(t * t + 1);
double s = t * c;
// 更新矩阵
double app = a[p][p], aqq = a[q][q], apq = a[p][q];
a[p][p] = c * c * app - 2 * s * c * apq + s * s * aqq;
a[q][q] = s * s * app + 2 * s * c * apq + c * c * aqq;
a[p][q] = a[q][p] = 0;
for (int i = 0; i < n; i++) {
if (i != p && i != q) {
double aip = a[i][p], aiq = a[i][q];
a[i][p] = a[p][i] = c * aip - s * aiq;
a[i][q] = a[q][i] = s * aip + c * aiq;
}
}
for (int i = 0; i < n; i++) {
double vip = v[i][p], viq = v[i][q];
v[i][p] = c * vip - s * viq;
v[i][q] = s * vip + c * viq;
}
}
double[] eigenvalues = new double[n];
for (int i = 0; i < n; i++) {
eigenvalues[i] = a[i][i];
}
return new EigenResult(eigenvalues, new Matrix(v));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
sb.append(String.format("%.4f ", data[i][j]));
}
sb.append("\n");
}
return sb.toString();
}
public static class EigenResult {
double[] eigenvalues;
Matrix eigenvectors;
public EigenResult(double[] eigenvalues, Matrix eigenvectors) {
this.eigenvalues = eigenvalues;
this.eigenvectors = eigenvectors;
}
}
}
SpectralClustering.java – 谱聚类核心
import java.util.*;
/**
* 谱聚类算法实现
* 基于对称归一化拉普拉斯矩阵的特征分解
*/
public class SpectralClustering {
private int k; // 聚类数
private double sigma; // RBF核参数
private int[] labels; // 聚类结果
public SpectralClustering(int k, double sigma) {
this.k = k;
this.sigma = sigma;
}
/**
* 执行谱聚类
* @param data 数据矩阵,每行一个样本
* @return 聚类标签
*/
public int[] fit(double[][] data) {
int n = data.length;
// 1. 构建相似度矩阵(RBF核)
Matrix W = buildAffinityMatrix(data);
// 2. 计算度矩阵D
Matrix D = new Matrix(n, n);
for (int i = 0; i < n; i++) {
double sum = 0;
for (int j = 0; j < n; j++) {
sum += W.get(i, j);
}
D.set(i, i, sum);
}
// 3. 计算对称归一化拉普拉斯矩阵 L_sym = I - D^(-1/2) * W * D^(-1/2)
Matrix D_inv_sqrt = new Matrix(n, n);
for (int i = 0; i < n; i++) {
double val = D.get(i, i);
D_inv_sqrt.set(i, i, val > 0 ? 1.0 / Math.sqrt(val) : 0);
}
Matrix L = computeLaplacian(D_inv_sqrt, W);
// 4. 特征分解
Matrix.EigenResult eigenResult = L.eigenDecomposition();
// 5. 选取前k个最小特征向量
Matrix eigenvectors = eigenResult.eigenvectors;
double[] eigenvalues = eigenResult.eigenvalues;
// 按特征值排序,取最小的k个
Integer[] indices = new Integer[n];
for (int i = 0; i < n; i++) indices[i] = i;
Arrays.sort(indices, Comparator.comparingDouble(i -> eigenvalues[i]));
// 6. 构建低维嵌入矩阵 U (n x k)
Matrix U = new Matrix(n, k);
for (int i = 0; i < n; i++) {
for (int j = 0; j < k; j++) {
U.set(i, j, eigenvectors.get(i, indices[j]));
}
}
// 7. 行归一化(重要:确保特征向量在球面上)
for (int i = 0; i < n; i++) {
double norm = 0;
for (int j = 0; j < k; j++) {
norm += U.get(i, j) * U.get(i, j);
}
norm = Math.sqrt(norm);
if (norm > 0) {
for (int j = 0; j < k; j++) {
U.set(i, j, U.get(i, j) / norm);
}
}
}
// 8. 在嵌入空间执行K-means
labels = kmeans(U);
return labels;
}
/**
* 构建RBF相似度矩阵
*/
private Matrix buildAffinityMatrix(double[][] data) {
int n = data.length;
Matrix W = new Matrix(n, n);
double twoSigmaSq = 2 * sigma * sigma;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
double dist = euclideanDistance(data[i], data[j]);
double sim = Math.exp(-dist * dist / twoSigmaSq);
W.set(i, j, sim);
W.set(j, i, sim);
}
}
return W;
}
/**
* 计算拉普拉斯矩阵 L_sym = I - D^(-1/2) * W * D^(-1/2)
*/
private Matrix computeLaplacian(Matrix D_inv_sqrt, Matrix W) {
int n = W.getRows();
Matrix temp = D_inv_sqrt.multiply(W).multiply(D_inv_sqrt);
Matrix L = new Matrix(n, n);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
L.set(i, j, (i == j ? 1.0 : 0.0) - temp.get(i, j));
}
}
return L;
}
/**
* 欧氏距离
*/
private double euclideanDistance(double[] a, double[] b) {
double sum = 0;
for (int i = 0; i < a.length; i++) {
double diff = a[i] - b[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
/**
* K-means聚类(在嵌入空间)
*/
private int[] kmeans(Matrix U) {
int n = U.getRows();
int dim = U.getCols();
labels = new int[n];
Random random = new Random(42);
// 随机初始化中心点
double[][] centers = new double[k][dim];
Set<Integer> selected = new HashSet<>();
for (int i = 0; i < k; i++) {
int idx;
do { idx = random.nextInt(n); } while (selected.contains(idx));
selected.add(idx);
for (int j = 0; j < dim; j++) {
centers[i][j] = U.get(idx, j);
}
}
// 迭代优化
for (int iter = 0; iter < 100; iter++) {
boolean changed = false;
// 分配标签
for (int i = 0; i < n; i++) {
int bestCluster = 0;
double minDist = Double.MAX_VALUE;
for (int c = 0; c < k; c++) {
double dist = 0;
for (int j = 0; j < dim; j++) {
double diff = U.get(i, j) - centers[c][j];
dist += diff * diff;
}
if (dist < minDist) {
minDist = dist;
bestCluster = c;
}
}
if (labels[i] != bestCluster) {
labels[i] = bestCluster;
changed = true;
}
}
if (!changed) break;
// 更新中心点
double[][] newCenters = new double[k][dim];
int[] counts = new int[k];
for (int i = 0; i < n; i++) {
int c = labels[i];
counts[c]++;
for (int j = 0; j < dim; j++) {
newCenters[c][j] += U.get(i, j);
}
}
for (int c = 0; c < k; c++) {
if (counts[c] > 0) {
for (int j = 0; j < dim; j++) {
centers[c][j] = newCenters[c][j] / counts[c];
}
}
}
}
return labels;
}
}
Main.java – 测试
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// 构造两组同心圆数据(非凸结构,K-means难以处理)
int n = 200;
double[][] data = new double[n][2];
// 内圆
for (int i = 0; i < n / 2; i++) {
double angle = 2 * Math.PI * i / (n / 2);
data[i][0] = Math.cos(angle) + randomNoise(0.05);
data[i][1] = Math.sin(angle) + randomNoise(0.05);
}
// 外圆
for (int i = n / 2; i < n; i++) {
double angle = 2 * Math.PI * (i - n / 2) / (n / 2);
data[i][0] = 2.5 * Math.cos(angle) + randomNoise(0.05);
data[i][1] = 2.5 * Math.sin(angle) + randomNoise(0.05);
}
// 谱聚类
SpectralClustering sc = new SpectralClustering(2, 0.5);
int[] labels = sc.fit(data);
// 统计结果
int[] counts = new int[2];
for (int label : labels) counts[label]++;
System.out.println("聚类结果分布: " + Arrays.toString(counts));
// 验证内圆和外圆是否被正确分开
int innerCluster = labels[0];
int innerCorrect = 0;
for (int i = 0; i < n / 2; i++) {
if (labels[i] == innerCluster) innerCorrect++;
}
System.out.println("内圆正确率: " + (100.0 * innerCorrect / (n / 2)) + "%");
}
private static double randomNoise(double scale) {
return (Math.random() - 0.5) * 2 * scale;
}
}
运行结果
聚类结果分布: [100, 100]
内圆正确率: 100.0%
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 构建相似度矩阵 | O(n²d) | n为样本数,d为维度;每对样本计算距离 |
| 特征分解 | O(n³) | Jacobi迭代法,可用Lanczos加速到O(kn²) |
| K-means后处理 | O(knit) | k为簇数,ni为迭代次数,t为嵌入维度 |
对于大规模数据,建议使用k近邻图(k-NN graph)稀疏化相似度矩阵,将特征分解复杂度降至O(kn²)。
参数调优
- sigma(RBF核参数):可使用k-dist图法或距离中位数自动估计
- k(簇数):可通过特征值间隙(eigengap heuristic)自动确定
- 图构造方式:k-NN图比全连接图更适合大规模数据
应用场景
- 图像分割:将像素视为数据点,基于颜色和位置相似度聚类
- 社交网络分析:发现社区结构,识别紧密联系的用户群体
- 文本聚类:基于语义相似度对文档进行非线性聚类
- 生物信息学:基因表达数据分析,发现共表达基因模块
总结
谱聚类通过图拉普拉斯矩阵的特征分解,将非线性聚类问题转化为在低维嵌入空间中的线性聚类问题。本文提供的Java实现完整覆盖了从相似度图构建到K-means后处理的完整流程,并支持同心圆等非凸数据结构的正确聚类。读者可根据实际数据特点选择不同的拉普拉斯矩阵变体和图构造策略,以获得最佳聚类效果。