共计 3571 个字符,预计需要花费 9 分钟才能阅读完成。
核心概念
K-Means 是一种无监督学习算法,通过迭代将数据划分到 K 个簇中。其核心思想是:

- 随机选择 K 个初始质心(簇中心)
- 计算每个数据点到质心的距离,分配到最近质心的簇
- 重新计算每个簇的质心(取簇内点的均值)
- 重复 2 - 3 步直到质心不再变化或达到最大迭代次数
适用场景包括客户分群、图像压缩、异常检测等。数学基础主要依赖欧式距离公式:
距离 = √(Σ(xi - yi)²)
痛点分析
初学者常遇到这些问题:
- 初始质心敏感:随机初始化可能导致不同结果
- 距离度量选择:高维数据时欧式距离可能失效
- 空簇问题:某些簇可能在迭代中失去所有成员
- 收敛判断:如何确定算法真正收敛而非卡在局部最优
技术实现
1. 数据预处理
// 标准化数据(Z-score 标准化)public static double[][] Normalize(double[][] data)
{var means = Enumerable.Range(0, data[0].Length)
.Select(i => data.Average(x => x[i])).ToArray();
var stdDevs = Enumerable.Range(0, data[0].Length)
.Select(i => Math.Sqrt(data.Average(x => Math.Pow(x[i] - means[i], 2)))).ToArray();
return data.Select(row => row
.Select((val, idx) => (val - means[idx]) / stdDevs[idx])
.ToArray()).ToArray();}
2. 核心算法实现
public class KMeans
{
// 欧式距离计算
public static double EuclideanDistance(double[] a, double[] b)
{if (a.Length != b.Length) throw new ArgumentException("Vectors must be same length");
double sum = 0.0;
for (int i = 0; i < a.Length; i++)
sum += Math.Pow(a[i] - b[i], 2);
return Math.Sqrt(sum);
}
// K-Means++ 初始化
public static double[][] InitializeCentroids(double[][] data, int k)
{var centroids = new List<double[]> {data[new Random().Next(data.Length)] };
for (int i = 1; i < k; i++)
{
var distances = data.Select(x =>
centroids.Min(c => EuclideanDistance(x, c))).ToArray();
centroids.Add(data[SampleWithWeights(distances)]);
}
return centroids.ToArray();}
// 主训练方法
public static (int[] labels, double[][] centroids) Fit(double[][] data, int k, int maxIter = 100)
{var centroids = InitializeCentroids(data, k);
int[] labels = new int[data.Length];
for (int iter = 0; iter < maxIter; iter++)
{
// 分配标签
Parallel.For(0, data.Length, i =>
{
double minDist = double.MaxValue;
for (int j = 0; j < k; j++)
{double dist = EuclideanDistance(data[i], centroids[j]);
if (dist < minDist)
{
minDist = dist;
labels[i] = j;
}
}
});
// 更新质心
var newCentroids = Enumerable.Range(0, k)
.Select(cluster =>
{var clusterPoints = data.Where((_, idx) => labels[idx] == cluster);
if (!clusterPoints.Any()) return centroids[cluster]; // 处理空簇
return Enumerable.Range(0, data[0].Length)
.Select(dim => clusterPoints.Average(x => x[dim]))
.ToArray();}).ToArray();
// 收敛检查
if (centroids.Zip(newCentroids, EuclideanDistance).All(d => d < 1e-6))
break;
centroids = newCentroids;
}
return (labels, centroids);
}
}
3. 评估模块
// 轮廓系数计算
public static double SilhouetteScore(double[][] data, int[] labels)
{return ParallelEnumerable.Range(0, data.Length).Select(i =>
{var cluster = labels[i];
// 计算 a(i):同簇内平均距离
double a = data.Where((_, idx) => labels[idx] == cluster && idx != i)
.Average(x => KMeans.EuclideanDistance(data[i], x));
// 计算 b(i):最近其他簇的平均距离
double b = Enumerable.Range(0, labels.Max() + 1)
.Where(c => c != cluster)
.Min(c => data.Where((_, idx) => labels[idx] == c)
.Average(x => KMeans.EuclideanDistance(data[i], x)));
return (b - a) / Math.Max(a, b);
}).Average();}
性能优化
- 并行计算 :使用
Parallel.For加速距离计算 - SIMD 优化:对欧式距离计算使用
System.Numerics - 提前终止:当质心移动小于阈值时提前退出循环
// SIMD 优化版距离计算(需要引用 System.Numerics)public static unsafe double EuclideanDistanceSIMD(double[] a, double[] b)
{
int vectorSize = Vector<double>.Count;
int i = 0;
double sum = 0.0;
fixed (double* ptrA = a, ptrB = b)
{for (; i <= a.Length - vectorSize; i += vectorSize)
{var va = new Vector<double>(ptrA + i);
var vb = new Vector<double>(ptrB + i);
var diff = va - vb;
sum += Vector.Dot(diff, diff);
}
}
// 处理剩余元素
for (; i < a.Length; i++)
sum += Math.Pow(a[i] - b[i], 2);
return Math.Sqrt(sum);
}
避坑指南
- 初始质心选择:
- 使用 K -Means++ 而非纯随机初始化
-
多次运行取最优结果
-
类别不平衡:
- 对少数类样本过采样
-
使用加权距离度量
-
高维问题:
- 先进行 PCA 降维
- 改用余弦相似度等更适合高维的距离度量
延伸思考
可将算法封装为可配置组件:
public class KMeansClusterer
{public int MaxIterations { get; set;} = 100;
public int K {get; set;}
public DistanceMetric DistanceMetric {get; set;} = DistanceMetric.Euclidean;
public ClusterResult Cluster(double[][] data)
{// 实现略...}
}
public enum DistanceMetric {Euclidean, Cosine, Manhattan}
与 Python 的 scikit-learn 相比,C# 实现:
– 在大量数据时性能更好(多线程优化空间大)
– 更适合集成到.NET 生产环境
– 但缺少丰富的可视化工具支持
总结
本文完整实现了 K -Means 算法的 C# 版本,包含:
– 核心算法流程
– 性能优化技巧
– 常见问题解决方案
可以直接将代码用于实际项目,建议先在小数据集测试再扩展到大规模数据。后续可考虑添加 GPU 加速支持以进一步提升性能。
正文完
