// spectralcluster.cpp
// MATLAB spectralcluster 的 C++ 独立实现(无第三方依赖)
// 编译: g++ -std=c++11 -O2 spectralcluster.cpp -o spectralcluster
// 运行: ./spectralcluster
#include <vector>
#include <cmath>
#include <algorithm>
#include <random>
#include <iostream>
#include <limits>
#include <string>
#include <numeric>
#include <stdexcept>
#include <utility>
#include <cstdint>
// ============================================================
// 轻量稠密矩阵(行主序)
// ============================================================
struct Matrix {
int rows = 0, cols = 0;
std::vector<double> d;
Matrix() = default;
Matrix(int r, int c, double v = 0.0)
: rows(r), cols(c), d((size_t)r * c, v) {}
double& operator()(int i, int j) { return d[(size_t)i * cols + j]; }
const double& operator()(int i, int j) const { return d[(size_t)i * cols + j]; }
static Matrix identity(int n) {
Matrix m(n, n, 0.0);
for (int i = 0; i < n; ++i) m(i, i) = 1.0;
return m;
}
};
// ============================================================
// 对称矩阵 Jacobi 特征分解
// 输入 A(对称 n×n),输出 evals 升序,evecs 的列为对应特征向量
// ============================================================
static void jacobi_eigen(const Matrix& A,
std::vector<double>& evals,
Matrix& evecs) {
const int n = A.rows;
Matrix a = A;
Matrix v = Matrix::identity(n);
const double eps = 1e-12;
const int max_sweeps = 200;
for (int sweep = 0; sweep < max_sweeps; ++sweep) {
double off = 0.0;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
off += a(i, j) * a(i, j);
if (off < eps) break;
for (int p = 0; p < n - 1; ++p) {
for (int q = p + 1; q < n; ++q) {
double apq = a(p, q);
if (std::abs(apq) < 1e-15) continue;
double theta = (a(q, q) - a(p, p)) / (2.0 * apq);
double t = (theta >= 0.0 ? 1.0 : -1.0) /
(std::abs(theta) + std::sqrt(theta * theta + 1.0));
double c = 1.0 / std::sqrt(t * t + 1.0);
double s = t * c;
double tau = s / (1.0 + c);
double h = t * apq;
a(p, p) -= h;
a(q, q) += h;
a(p, q) = 0.0;
a(q, p) = 0.0;
for (int j = 0; j < n; ++j) {
if (j == p || j == q) continue;
double gj = a(j, p);
double hj = a(j, q);
a(j, p) = gj - s * (hj + gj * tau);
a(p, j) = a(j, p);
a(j, q) = hj + s * (gj - hj * tau);
a(q, j) = a(j, q);
}
for (int i = 0; i < n; ++i) {
double g = v(i, p);
double hh = v(i, q);
v(i, p) = g - s * (hh + g * tau);
v(i, q) = hh + s * (g - hh * tau);
}
}
}
}
std::vector<std::pair<double, int>> pairs(n);
for (int i = 0; i < n; ++i) pairs[i] = {a(i, i), i};
std::sort(pairs.begin(), pairs.end());
evals.resize(n);
evecs = Matrix(n, n);
for (int j = 0; j < n; ++j) {
evals[j] = pairs[j].first;
int src = pairs[j].second;
for (int i = 0; i < n; ++i)
evecs(i, j) = v(i, src);
}
}
// ============================================================
// SpectralCluster
// ============================================================
class SpectralCluster {
public:
struct Params {
std::string similarity_graph = "knn"; // "knn" | "radius"
int num_neighbors = 0; // 0 表示自动
double radius = -1.0; // <0 表示自动
double kernel_scale = -1.0; // <0 表示自动
std::string laplacian_norm = "rw"; // "rw" | "sym" | "none"
int max_iter = 300;
int n_init = 10;
unsigned seed = 42;
};
explicit SpectralCluster(const Params& p);
std::vector<int> fit_predict(const Matrix& X, int k);
private:
Params params_;
std::mt19937 rng_;
Matrix build_similarity_matrix(const Matrix& X);
Matrix knn_similarity(const Matrix& X, int k);
Matrix radius_similarity(const Matrix& X, double radius);
double estimate_kernel_scale(const Matrix& X, int k);
Matrix pairwise_distance(const Matrix& X);
std::vector<int> kmeans(const Matrix& V, int k);
std::vector<int> kmeans_single(const Matrix& V, int k);
// ---- 跨平台可复现采样工具(不依赖具体标准库分布实现)----
double uniform01() {
uint64_t r = (uint64_t)rng_();
return r / (double)0xFFFFFFFFULL; // mt19937 输出范围 [0, 2^32-1]
}
int uniform_int(int n) {
int idx = (int)(uniform01() * n);
return (idx >= n) ? n - 1 : idx;
}
int weighted_sample(const std::vector<double>& w) {
double total = 0.0;
for (double x : w) total += x;
if (total <= 0.0) return 0;
double r = uniform01() * total;
double cum = 0.0;
for (size_t i = 0; i < w.size(); ++i) {
cum += w[i];
if (r < cum) return (int)i;
}
return (int)w.size() - 1;
}
};
// 构造函数类外定义(C++11 兼容:避免默认实参在类定义期求值 Params{})
SpectralCluster::SpectralCluster(const Params& p) : params_(p) {
rng_.seed(params_.seed);
}
// ============================================================
// fit_predict:主流程
// ============================================================
std::vector<int> SpectralCluster::fit_predict(const Matrix& X, int k) {
if (X.rows < k || X.cols == 0)
throw std::invalid_argument("X rows must be >= k and cols > 0");
// 1) 相似度矩阵
Matrix S = build_similarity_matrix(X);
const int n = S.rows;
// 2) 度向量及 D^{-1/2}
std::vector<double> deg(n, 0.0);
for (int i = 0; i < n; ++i) {
double s = 0.0;
for (int j = 0; j < n; ++j) s += S(i, j);
deg[i] = s;
}
std::vector<double> inv_sqrt_d(n, 0.0);
for (int i = 0; i < n; ++i)
if (deg[i] > 1e-15) inv_sqrt_d[i] = 1.0 / std::sqrt(deg[i]);
// 3) 构建对称拉普拉斯矩阵 L_s
// none: L = D - S(对称)
// sym : L_s = I - D^{-1/2} S D^{-1/2}
// rw : 用 L_s 求谱,再变换回 L_rw 的特征向量
Matrix Ls(n, n, 0.0);
if (params_.laplacian_norm == "none") {
for (int i = 0; i < n; ++i) {
Ls(i, i) = deg[i];
for (int j = 0; j < n; ++j)
Ls(i, j) -= S(i, j);
}
} else {
for (int i = 0; i < n; ++i) {
Ls(i, i) = 1.0;
for (int j = 0; j < n; ++j)
Ls(i, j) -= inv_sqrt_d[i] * S(i, j) * inv_sqrt_d[j];
}
}
// 4) 特征分解(升序),取最小的 k 个特征向量
std::vector<double> evals;
Matrix evecs;
jacobi_eigen(Ls, evals, evecs);
// 5) 构造谱嵌入 V
Matrix V(n, k, 0.0);
if (params_.laplacian_norm == "rw") {
// L_rw = D^{-1} L = I - D^{-1} S 与 L_s 相似:V = D^{-1/2} U
for (int i = 0; i < n; ++i)
for (int j = 0; j < k; ++j)
V(i, j) = inv_sqrt_d[i] * evecs(i, j);
} else {
for (int i = 0; i < n; ++i)
for (int j = 0; j < k; ++j)
V(i, j) = evecs(i, j);
}
// 6) sym 情况:对 V 的每一行做单位化(Ng-Jordan-Weiss)
if (params_.laplacian_norm == "sym") {
for (int i = 0; i < n; ++i) {
double norm = 0.0;
for (int j = 0; j < k; ++j) norm += V(i, j) * V(i, j);
norm = std::sqrt(norm);
if (norm > 1e-15)
for (int j = 0; j < k; ++j) V(i, j) /= norm;
}
}
// 7) K-means
return kmeans(V, k);
}
// ============================================================
// 相似度图构建
// ============================================================
Matrix SpectralCluster::build_similarity_matrix(const Matrix& X) {
if (params_.similarity_graph == "radius") {
double r = params_.radius;
if (r <= 0) {
Matrix D = pairwise_distance(X);
int n = X.rows;
double sum = 0.0;
for (int i = 0; i < n; ++i) {
double best = std::numeric_limits<double>::max();
for (int j = 0; j < n; ++j)
if (i != j && D(i, j) < best) best = D(i, j);
sum += best;
}
r = (n > 0) ? sum / n : 1.0;
}
return radius_similarity(X, r);
} else {
int nn = params_.num_neighbors;
if (nn <= 0)
nn = std::max(1, (int)std::ceil(std::log2((double)X.rows)));
return knn_similarity(X, nn);
}
}
Matrix SpectralCluster::pairwise_distance(const Matrix& X) {
int n = X.rows, d = X.cols;
Matrix D(n, n, 0.0);
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
double s = 0.0;
for (int t = 0; t < d; ++t) {
double diff = X(i, t) - X(j, t);
s += diff * diff;
}
double dist = std::sqrt(s);
D(i, j) = dist;
D(j, i) = dist;
}
}
return D;
}
double SpectralCluster::estimate_kernel_scale(const Matrix& X, int k) {
Matrix D = pairwise_distance(X);
int n = X.rows;
std::vector<double> knn_dists(n, 1.0);
for (int i = 0; i < n; ++i) {
std::vector<double> row;
row.reserve(n - 1);
for (int j = 0; j < n; ++j) if (i != j) row.push_back(D(i, j));
int kk = std::min(k, (int)row.size());
if (kk <= 0) continue;
std::nth_element(row.begin(), row.begin() + kk - 1, row.end());
knn_dists[i] = row[kk - 1];
}
int mid = n / 2;
std::nth_element(knn_dists.begin(), knn_dists.begin() + mid, knn_dists.end());
double med = knn_dists[mid];
return med > 1e-15 ? med : 1.0;
}
Matrix SpectralCluster::knn_similarity(const Matrix& X, int k) {
int n = X.rows;
Matrix D = pairwise_distance(X);
Matrix S(n, n, 0.0);
double sigma = params_.kernel_scale;
if (sigma <= 0) sigma = estimate_kernel_scale(X, k);
std::vector<std::vector<int>> neighbors(n);
for (int i = 0; i < n; ++i) {
std::vector<std::pair<double, int>> di;
di.reserve(n - 1);
for (int j = 0; j < n; ++j)
if (i != j) di.push_back({D(i, j), j});
int kk = std::min(k, n - 1);
std::partial_sort(di.begin(), di.begin() + kk, di.end());
for (int t = 0; t < kk; ++t)
neighbors[i].push_back(di[t].second);
}
for (int i = 0; i < n; ++i) {
for (int j : neighbors[i]) {
double z = D(i, j) / sigma;
double sim = std::exp(-z * z);
if (sim > S(i, j)) {
S(i, j) = sim;
S(j, i) = sim;
}
}
}
return S;
}
Matrix SpectralCluster::radius_similarity(const Matrix& X, double radius) {
int n = X.rows;
Matrix D = pairwise_distance(X);
Matrix S(n, n, 0.0);
double sigma = params_.kernel_scale;
if (sigma <= 0) sigma = radius;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
if (D(i, j) <= radius) {
double z = D(i, j) / sigma;
double sim = std::exp(-z * z);
S(i, j) = sim;
S(j, i) = sim;
}
return S;
}
// ============================================================
// K-means(多次初始化 + k-means++)
// ============================================================
std::vector<int> SpectralCluster::kmeans(const Matrix& V, int k) {
std::vector<int> best_labels;
double best_inertia = std::numeric_limits<double>::max();
int n = V.rows, d = V.cols;
for (int run = 0; run < params_.n_init; ++run) {
std::vector<int> labels = kmeans_single(V, k);
Matrix centroids(k, d, 0.0);
std::vector<int> counts(k, 0);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < d; ++j)
centroids(labels[i], j) += V(i, j);
counts[labels[i]]++;
}
for (int c = 0; c < k; ++c)
if (counts[c] > 0)
for (int j = 0; j < d; ++j)
centroids(c, j) /= counts[c];
double inertia = 0.0;
for (int i = 0; i < n; ++i) {
double s = 0.0;
for (int j = 0; j < d; ++j) {
double diff = V(i, j) - centroids(labels[i], j);
s += diff * diff;
}
inertia += s;
}
if (inertia < best_inertia) {
best_inertia = inertia;
best_labels = labels;
}
}
return best_labels;
}
std::vector<int> SpectralCluster::kmeans_single(const Matrix& V, int k) {
int n = V.rows, d = V.cols;
// k-means++ 初始化
std::vector<int> centers;
centers.reserve(k);
centers.push_back(uniform_int(n));
std::vector<double> min_dist2(n, std::numeric_limits<double>::max());
for (int c = 1; c < k; ++c) {
int last = centers.back();
for (int i = 0; i < n; ++i) {
double s = 0.0;
for (int j = 0; j < d; ++j) {
double diff = V(i, j) - V(last, j);
s += diff * diff;
}
if (s < min_dist2[i]) min_dist2[i] = s;
}
centers.push_back(weighted_sample(min_dist2));
}
Matrix centroids(k, d, 0.0);
for (int c = 0; c < k; ++c)
for (int j = 0; j < d; ++j)
centroids(c, j) = V(centers[c], j);
std::vector<int> labels(n, -1);
for (int iter = 0; iter < params_.max_iter; ++iter) {
bool changed = false;
for (int i = 0; i < n; ++i) {
double best_d = std::numeric_limits<double>::max();
int best_c = 0;
for (int c = 0; c < k; ++c) {
double s = 0.0;
for (int j = 0; j < d; ++j) {
double diff = V(i, j) - centroids(c, j);
s += diff * diff;
}
if (s < best_d) { best_d = s; best_c = c; }
}
if (labels[i] != best_c) { labels[i] = best_c; changed = true; }
}
if (!changed && iter > 0) break;
Matrix new_c(k, d, 0.0);
std::vector<int> counts(k, 0);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < d; ++j)
new_c(labels[i], j) += V(i, j);
counts[labels[i]]++;
}
for (int c = 0; c < k; ++c)
if (counts[c] > 0)
for (int j = 0; j < d; ++j)
new_c(c, j) /= counts[c];
centroids = new_c;
}
return labels;
}
// ============================================================
// main:三簇高斯数据演示
// 使用自实现的确定性 PRNG 生成数据,避免依赖标准库分布实现
// ============================================================
int main() {
const int n = 150;
Matrix X(n, 2, 0.0);
// 确定性 PRNG(LCG + Box-Muller)
uint64_t state = 12345ULL;
auto next_uniform = [&]() -> double {
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
return (double)((state >> 33) & 0x7FFFFFFFULL) / (double)0x7FFFFFFFULL;
};
auto next_gauss = [&]() -> double {
double u1 = next_uniform();
double u2 = next_uniform();
if (u1 < 1e-12) u1 = 1e-12;
return std::sqrt(-2.0 * std::log(u1)) *
std::cos(2.0 * 3.14159265358979323846 * u2);
};
for (int i = 0; i < 50; ++i) {
X(i, 0) = 2.0 + 0.5 * next_gauss();
X(i, 1) = 2.0 + 0.5 * next_gauss();
}
for (int i = 50; i < 100; ++i) {
X(i, 0) = -2.0 + 0.5 * next_gauss();
X(i, 1) = 2.0 + 0.5 * next_gauss();
}
for (int i = 100; i < 150; ++i) {
X(i, 0) = 0.0 + 0.5 * next_gauss();
X(i, 1) = -2.0 + 0.5 * next_gauss();
}
SpectralCluster::Params params;
params.similarity_graph = "knn";
params.laplacian_norm = "rw"; // Shi-Malik 归一化(MATLAB 默认)
params.n_init = 10;
SpectralCluster sc(params);
std::vector<int> labels = sc.fit_predict(X, 3);
// 输出 1:前 10 个样本的标签
std::cout << "前 10 个样本的标签: ";
for (int i = 0; i < 10; ++i)
std::cout << labels[i] << (i + 1 < 10 ? " " : "");
std::cout << "\n";
// 输出 2:每簇样本数
std::vector<int> cnt(3, 0);
for (int l : labels) cnt[l]++;
std::cout << "簇 0: " << cnt[0]
<< ", 簇 1: " << cnt[1]
<< ", 簇 2: " << cnt[2] << "\n";
return 0;
}