C++实现K-Means聚类算法
·
一、引言
K-Means聚类是机器学习中最经典的无监督学习算法之一,它以其简单性和高效性在数据挖掘、图像分割、客户分群等领域广泛应用。本文将详细介绍如何使用C++从零开始实现K-Means聚类算法。
K-Means算法原理:
K-Means算法的核心思想是将数据点划分为K个簇,使得每个数据点都属于离它最近的质心(簇中心)对应的簇,从而让簇内数据点尽可能相似,簇间数据点尽可能不同。
算法步骤:
- 初始化:随机选择K个数据点作为初始质心
- 分配阶段:将每个数据点分配到距离最近的质心所在的簇
- 更新阶段:重新计算每个簇的质心(该簇所有点的均值)
- 迭代:重复步骤2-3,直到质心不再变化或达到最大迭代次数
二、C++实现核心代码
2.1 数据结构设计
#include <vector>
#include <cmath>
#include <algorithm>
#include <random>
#include <limits>
class KMeans {
private:
int k; // 簇的数量
int maxIterations; // 最大迭代次数
double tolerance; // 收敛阈值
std::vector<std::vector<double>> centroids; // 质心坐标
public:
KMeans(int k, int maxIter = 100, double tol = 1e-4)
: k(k), maxIterations(maxIter), tolerance(tol) {}
// 计算两点之间的欧氏距离
double euclideanDistance(const std::vector<double>& a,
const std::vector<double>& b) {
double sum = 0.0;
for (size_t i = 0; i < a.size(); ++i) {
sum += std::pow(a[i] - b[i], 2);
}
return std::sqrt(sum);
}
2.2 核心算法实现
// 拟合数据并进行聚类
std::vector<int> fit(const std::vector<std::vector<double>>& data) {
if (data.empty()) return {};
int n = data.size();
int dim = data[0].size();
// 1. 初始化质心 - 随机选择K个数据点
initializeCentroids(data);
std::vector<int> labels(n, -1);
std::vector<std::vector<double>> oldCentroids;
int iteration = 0;
do {
oldCentroids = centroids;
// 2. 分配阶段:为每个数据点分配最近的簇
for (int i = 0; i < n; ++i) {
double minDist = std::numeric_limits<double>::max();
int bestCluster = -1;
// 寻找最近的质心
for (int j = 0; j < k; ++j) {
double dist = euclideanDistance(data[i], centroids[j]);
if (dist < minDist) {
minDist = dist;
bestCluster = j;
}
}
labels[i] = bestCluster;
}
// 3. 更新阶段:重新计算质心
updateCentroids(data, labels);
iteration++;
} while (!hasConverged(oldCentroids) && iteration < maxIterations);
return labels;
}
2.3 初始化质心
private:
// 随机初始化质心
void initializeCentroids(const std::vector<std::vector<double>>& data) {
centroids.clear();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, data.size() - 1);
// 随机选择K个不重复的数据点作为初始质心
std::vector<int> indices;
while (centroids.size() < k) {
int idx = dis(gen);
// 避免重复选择
if (std::find(indices.begin(), indices.end(), idx) == indices.end()) {
indices.push_back(idx);
centroids.push_back(data[idx]);
}
}
}
2.4 质心更新和收敛判断
// 更新质心坐标
void updateCentroids(const std::vector<std::vector<double>>& data,
const std::vector<int>& labels) {
std::vector<std::vector<double>> newCentroids(k,
std::vector<double>(data[0].size(), 0.0));
std::vector<int> clusterSizes(k, 0);
// 计算每个簇的数据点总和
for (size_t i = 0; i < data.size(); ++i) {
int cluster = labels[i];
for (size_t j = 0; j < data[i].size(); ++j) {
newCentroids[cluster][j] += data[i][j];
}
clusterSizes[cluster]++;
}
// 计算均值得到新质心
for (int i = 0; i < k; ++i) {
if (clusterSizes[i] > 0) {
for (size_t j = 0; j < newCentroids[i].size(); ++j) {
newCentroids[i][j] /= clusterSizes[i];
}
} else {
// 如果簇为空,保持原质心
newCentroids[i] = centroids[i];
}
}
centroids = newCentroids;
}
// 判断算法是否收敛
bool hasConverged(const std::vector<std::vector<double>>& oldCentroids) {
for (int i = 0; i < k; ++i) {
double dist = euclideanDistance(centroids[i], oldCentroids[i]);
if (dist > tolerance) {
return false;
}
}
return true;
}
2.5 使用示例
// 示例:使用K-Means对二维数据进行聚类
int main() {
// 创建一些示例数据
std::vector<std::vector<double>> data = {
{1.0, 1.0}, {1.5, 2.0}, {3.0, 4.0},
{5.0, 7.0}, {3.5, 5.0}, {4.5, 5.0},
{1.5, 1.0}, {2.0, 1.5}, {5.0, 6.0}
};
// 创建K-Means聚类器,设置K=3
KMeans kmeans(3);
// 执行聚类
std::vector<int> labels = kmeans.fit(data);
// 输出聚类结果
std::cout << "聚类结果:" << std::endl;
for (size_t i = 0; i < data.size(); ++i) {
std::cout << "数据点 (" << data[i][0] << ", " << data[i][1]
<< ") 属于簇 " << labels[i] << std::endl;
}
return 0;
}
三、注意事项
3.1 K值选择问题
K-Means需要预先指定簇的数量K,选择合适的K值至关重要:
- 肘部法则:绘制不同K值对应的误差平方和(SSE),选择拐点
- 轮廓系数:衡量簇内紧密度和簇间分离度
- 业务理解:基于实际应用场景确定K值
3.2 空簇处理
在更新质心时可能出现空簇,需要特殊处理:
- 重新初始化该簇的质心
- 选择距离当前质心最远的点作为新质心
- 选择SSE贡献最大的点作为新质心
3.3 局限性
- 对非球形簇效果不佳
- 对噪声和离群点敏感
- 初始质心选择影响最终结果
四、总结
通过这个C++实现,我们完整地展示了K-Means聚类算法的核心思想和实现细节。K-Means虽然简单,但在实际应用中需要注意数据预处理、参数选择和算法优化等问题。
关键要点总结:
- 核心思想:通过迭代优化质心位置来最小化簇内距离
- 实现重点:质心初始化、数据点分配、质心更新
五、面试手撕
考虑到面试手撕时可能只要写出核心代码体现思路就行,下面给出一个稍显粗糙的版本。
#include <vector>
#include <cmath>
#include <cstdlib>
#include <algorithm>
// 定义点的类型为double类型的向量(表示多维权重特征)
using Point = std::vector<double>;
/**
* 计算两点之间的欧氏距离平方
* (使用平方避免开方运算,不影响距离大小的相对比较,提升效率)
* @param a 第一个点
* @param b 第二个点
* @return 距离的平方值
*/
double distSq(const Point& a, const Point& b) {
double d = 0;
// 累加每个维度上的差值平方
for (size_t i = 0; i < a.size(); ++i) {
d += (a[i] - b[i]) * (a[i] - b[i]);
}
return d;
}
/**
* KMeans聚类核心函数
* @param data 输入数据集(每个元素是一个多维权重特征向量)
* @param k 目标聚类数量
* @param maxIter 最大迭代次数(防止无限循环)
* @return 每个样本对应的聚类标签(与输入数据顺序一致)
*/
std::vector<int> kmeans(const std::vector<Point>& data, int k, int maxIter = 50) {
int n = data.size(); // 样本总数
int dim = data[0].size(); // 每个样本的特征维度
std::vector<int> labels(n, 0); // 存储每个样本的聚类标签(初始都为0)
std::vector<Point> centroids(k); // 存储k个聚类中心(质心)
// 1. 初始化质心:从数据中随机选择k个点作为初始聚类中心
std::srand(123); // 固定随机种子,保证结果可复现(实际应用可去掉固定种子)
for (int i = 0; i < k; ++i) {
// 随机选择一个样本索引,将该样本作为初始质心
centroids[i] = data[rand() % n];
}
// 2. 迭代优化聚类中心(核心循环)
for (int iter = 0; iter < maxIter; ++iter) {
bool changed = false; // 标记本轮迭代中标签是否有变化(用于判断收敛)
// 3. 分配阶段:将每个样本分配到最近的质心对应的聚类
for (int i = 0; i < n; ++i) {
int bestCluster = 0; // 记录距离当前样本最近的聚类索引
// 计算样本到第一个质心的距离
double minDist = distSq(data[i], centroids[0]);
// 遍历其他质心,找到最近的聚类
for (int c = 1; c < k; ++c) {
double currentDist = distSq(data[i], centroids[c]);
if (currentDist < minDist) {
minDist = currentDist;
bestCluster = c; // 更新最近聚类索引
}
}
// 如果样本的聚类标签发生变化,更新标签并标记状态
if (labels[i] != bestCluster) {
labels[i] = bestCluster;
changed = true;
}
}
// 如果所有样本的标签都未变化,说明收敛,提前退出迭代
if (!changed) {
break;
}
// 4. 更新阶段:计算新的聚类中心(每个聚类的均值)
std::vector<Point> newCentroids(k, Point(dim, 0.0)); // 新质心(初始化为0)
std::vector<int> clusterCounts(k, 0); // 每个聚类的样本数量(初始化为0)
// 累加每个聚类中所有样本的特征值
for (int i = 0; i < n; ++i) {
int cluster = labels[i]; // 当前样本所属的聚类
// 累加该样本的每个维度特征到对应聚类的新质心
for (int d = 0; d < dim; ++d) {
newCentroids[cluster][d] += data[i][d];
}
clusterCounts[cluster]++; // 对应聚类的样本数+1
}
// 计算均值:将累加的特征值除以样本数,得到新质心
for (int c = 0; c < k; ++c) {
// 每个维度都取均值
for (int d = 0; d < dim; ++d) {
newCentroids[c][d] /= clusterCounts[c];
}
}
// 更新质心为新计算的质心
centroids = newCentroids;
}
// 返回最终的聚类标签
return labels;
}
更多推荐

所有评论(0)