|
|
分享一个纯 C++ 实现的 K-Means 聚类程序。它不依赖第三方库,结构完整、逻辑清晰,编译后就能直接跑,适合拿来学习 C++ 算法实现,也可以作为后续扩展的基础模板。
它是什么
这个程序实现的是经典的 K-Means 聚类算法,主要用来把一组二维数据点自动划分成若干个簇。
简单来说,它可以完成这些工作:
● 从文本文件中加载二维数据;
● 随机初始化聚类中心;
● 反复把数据点分配到最近的聚类中心;
● 更新每个簇的中心位置;
● 根据惯性变化判断是否收敛;
● 把最终聚类结果保存到输出文件中。
它比较适合用来学习 C++ 文件读写、算法流程、工程结构,也适合做课程练习、算法实验,或者在此基础上继续加可视化、多线程、参数解析等功能。
适合什么人
● 想练习 C++ 基础算法实现的人;
● 想了解 K-Means 聚类流程的人;
● 想做一个可以编译运行的 C++ 小项目的人;
● 想在此基础上继续扩展功能的人。
使用方法
1. 准备数据文件
程序默认读取当前目录下的 data.txt。
文件格式很简单:每行两个数字,用空格分开,分别表示一个点的 x 和 y 坐标。
例如:
2.1 2.3
7.8 8.1
5.0 1.2
不需要表头,也不需要逗号分隔,只要每行两个浮点数即可。
2. 编译程序
使用支持 C++11 及以上的编译器即可。
编译命令示例:
g++ -o kmeans kmeans.cpp -std=c++11
如果你使用的是其他编译器,只要支持 C++11 标准,一般也可以正常编译。
3. 运行程序
编译完成后,直接运行生成的可执行文件:
./kmeans
程序运行后会输出当前迭代信息,包括迭代次数和惯性值。惯性值越小,说明数据点离各自簇中心的距离总和越小,聚类效果通常越稳定。
4. 查看结果
运行结束后,程序会生成一个 result.txt 文件。
输出文件每行包含三个值:
x y cluster_id
其中:
● x:数据点的横坐标;
● y:数据点的纵坐标;
● cluster_id:该点所属的簇编号,从 0 开始。
你可以用这个结果文件做后续分析、绘图,或者导入到其他工具里查看聚类分布。
默认参数说明
程序内置了几个默认参数:
● 输入文件:data.txt
● 输出文件:result.txt
● 聚类数量:3
● 最大迭代次数:100
● 收敛阈值:0.0001
如果想修改这些参数,可以直接在源码中调整对应变量,也可以后续自己扩展成命令行参数形式。
注意事项
● 数据文件必须存在,且至少包含两列有效数字;
● 聚类数量不能超过数据点数量;
● 由于初始中心是随机选择的,多次运行结果可能略有差异;
● 如果数据分布本身不适合 K-Means,聚类效果可能不理想;
● 当前版本只支持二维数据,如需更高维度可以继续扩展。
后续可以怎么玩
这个版本适合作为基础模板。后面可以继续扩展:
● 支持命令行参数;
● 支持多维数据;
● 加入肘部法则自动选择 K 值;
● 支持 CSV 文件读取;
● 输出可视化数据;
● 加入多线程加速;
● 封装成更完整的 C++ 小库。
如果大家对这类 C++ 算法小项目感兴趣,后面可以继续分享更多版本。
代码:- // kmeans.cpp
- #include <iostream>
- #include <fstream>
- #include <sstream>
- #include <vector>
- #include <cmath>
- #include <random>
- #include <algorithm>
- #include <limits>
- #include <iomanip>
- struct Point {
- double x, y;
- int cluster;
- Point(double x = 0, double y = 0) : x(x), y(y), cluster(-1) {}
- };
- double euclidean_distance(const Point& a, const Point& b) {
- return std::sqrt((a.x - b.x) * (a.x - b.x) +
- (a.y - b.y) * (a.y - b.y));
- }
- std::vector<Point> load_data(const std::string& filename) {
- std::vector<Point> data;
- std::ifstream file(filename);
- if (!file.is_open()) {
- std::cerr << "无法打开文件: " << filename << std::endl;
- return data;
- }
- std::string line;
- while (std::getline(file, line)) {
- std::istringstream iss(line);
- double x, y;
- if (iss >> x >> y) {
- data.emplace_back(x, y);
- }
- }
- return data;
- }
- std::vector<Point> initialize_centroids(const std::vector<Point>& data, int k) {
- std::vector<Point> centroids;
- std::random_device rd;
- std::mt19937 gen(rd());
- std::uniform_int_distribution<size_t> dist(0, data.size() - 1);
- for (int i = 0; i < k; ++i) {
- centroids.push_back(data[dist(gen)]);
- }
- return centroids;
- }
- void assign_clusters(std::vector<Point>& data, const std::vector<Point>& centroids) {
- for (auto& point : data) {
- double min_dist = std::numeric_limits<double>::max();
- int best_cluster = 0;
- for (size_t i = 0; i < centroids.size(); ++i) {
- double dist = euclidean_distance(point, centroids[i]);
- if (dist < min_dist) {
- min_dist = dist;
- best_cluster = static_cast<int>(i);
- }
- }
- point.cluster = best_cluster;
- }
- }
- std::vector<Point> update_centroids(const std::vector<Point>& data, int k) {
- std::vector<Point> new_centroids(k);
- std::vector<int> counts(k, 0);
- for (const auto& point : data) {
- new_centroids[point.cluster].x += point.x;
- new_centroids[point.cluster].y += point.y;
- counts[point.cluster]++;
- }
- for (int i = 0; i < k; ++i) {
- if (counts[i] > 0) {
- new_centroids[i].x /= counts[i];
- new_centroids[i].y /= counts[i];
- }
- }
- return new_centroids;
- }
- double compute_inertia(const std::vector<Point>& data, const std::vector<Point>& centroids) {
- double inertia = 0.0;
- for (const auto& point : data) {
- inertia += euclidean_distance(point, centroids[point.cluster]);
- }
- return inertia;
- }
- void kmeans(std::vector<Point>& data, int k, int max_iters, double tolerance) {
- auto centroids = initialize_centroids(data, k);
- double prev_inertia = std::numeric_limits<double>::max();
- for (int iter = 0; iter < max_iters; ++iter) {
- assign_clusters(data, centroids);
- auto new_centroids = update_centroids(data, k);
- double inertia = compute_inertia(data, new_centroids);
- std::cout << "迭代 " << iter + 1
- << " | 惯性: " << std::fixed << std::setprecision(4) << inertia << std::endl;
- if (std::abs(prev_inertia - inertia) < tolerance) {
- std::cout << "收敛于迭代 " << iter + 1 << std::endl;
- break;
- }
- centroids = new_centroids;
- prev_inertia = inertia;
- }
- }
- void save_results(const std::vector<Point>& data, const std::string& filename) {
- std::ofstream file(filename);
- for (const auto& point : data) {
- file << point.x << " " << point.y << " " << point.cluster << std::endl;
- }
- }
- int main() {
- std::string input_file = "data.txt";
- std::string output_file = "result.txt";
- int k = 3;
- int max_iters = 100;
- double tolerance = 1e-4;
- auto data = load_data(input_file);
- if (data.empty()) {
- std::cerr << "数据为空" << std::endl;
- return 1;
- }
- std::cout << "加载了 " << data.size() << " 个数据点" << std::endl;
- kmeans(data, k, max_iters, tolerance);
- save_results(data, output_file);
- std::cout << "结果已保存到 " << output_file << std::endl;
- return 0;
- }
复制代码 |
|