本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:k-means算法是一种无监督机器学习方法,用于数据聚类。它将样本点分配到K个类别中,最小化每个样本点到其类别中心点的距离平方和。算法包括初始化质心、迭代分配与更新质心以及满足终止条件停止迭代。本文深入分析了C++中k-means算法的实现,包括其关键步骤的代码结构和优化策略。我们使用鸢尾花数据集作为测试案例,演示了算法的应用。
k-means   源代码(C++实现)

1. k-means算法概述

k-means算法是数据科学中使用最为广泛的聚类算法之一。它通过迭代计算,将数据集分成指定数量的k个聚类,每个聚类由一个质心表示。算法的核心目标是最小化各数据点到其对应质心的距离之和。本章将对k-means算法的基本原理和应用场景进行概述,为后续深入分析其工作流程、性能优化和代码实现打下基础。

2. 质心的初始化方法

2.1 随机选择质心

2.1.1 随机选择的理论基础

在K-means算法中,随机选择初始质心是最简单、最直接的方法,但也是最依赖于初始选择质量的方法。理论上,随机选择质心的基本假设是,数据点是均匀分布的。实际应用中,此方法存在一定的缺陷,因为初始质心位置的随机性可能会导致算法收敛到局部最优解而非全局最优解。尽管如此,随机选择初始质心的简单易行使得其在实际中仍有广泛的应用,特别是当数据集规模较小时。

2.1.2 随机选择的具体实现

随机选择质心的实现可以通过编程语言中的随机函数来完成。下面是一个简化的示例代码,演示了如何使用Python语言随机选择数据集中的K个点作为初始质心。

import numpy as np

def initialize_centroids(data, num_centroids):
    """随机选择初始质心"""
    centroids = np.zeros((num_centroids, data.shape[1]))
    for idx in range(num_centroids):
        centroid_idx = np.random.choice(range(data.shape[0]))
        centroids[idx] = data[centroid_idx]
    return centroids

在上述代码中, data 是输入的数据集, num_centroids 是需要选择的质心数量。该函数首先创建一个同样维度的零矩阵 centroids ,然后通过 np.random.choice 函数随机选择 data 中的一个索引,并将其对应的点设置为初始质心。这个过程重复 num_centroids 次来获得所需的初始质心。

2.2 最佳初始质心的选择

2.2.1 K-means++算法原理

K-means++算法是一种改进的质心初始化方法,其目标是通过智能选择初始质心来提高算法的收敛速度和最终聚类结果的质量。K-means++算法选择初始质心的策略是,首先随机选择一个数据点作为第一个质心,然后根据数据点与已选择质心的最短距离来概率性地选择下一个质心。距离越远,被选为下一个质心的概率就越大,这样可以增加质心之间的距离,从而提高聚类的质量。

2.2.2 K-means++算法的实现步骤

K-means++算法的具体实现步骤如下:

  1. 从数据集D中随机选择一个点作为第一个质心。
  2. 对于数据集中的每个点x,计算其与最近质心的距离D(x)。
  3. 选择一个新的数据点作为质心,该点的选择概率与D(x)的平方成正比。
  4. 重复步骤2和3,直到选出K个质心。
  5. 使用选出的质心作为初始质心开始执行标准的K-means算法。

以下是K-means++算法初始化质心的Python代码示例:

def initialize_centroids_kmeans_plus_plus(data, num_centroids):
    """K-means++方法选择初始质心"""
    centroids = [data[np.random.choice(range(data.shape[0]))]]
    for _ in range(1, num_centroids):
        dist_sq = np.array([min([np.inner(c-x, c-x) for c in centroids]) for x in data])
        probs = dist_sq/dist_sq.sum()
        cumulative_probs = probs.cumsum()
        r = np.random.rand()
        for j, p in enumerate(cumulative_probs):
            if r < p:
                i = j
                break
        centroids.append(data[i])
    return np.array(centroids)

在这个函数中, data 是输入的数据集, num_centroids 是需要选择的质心数量。 dist_sq 数组存储了每个点与最近质心的平方距离,这与K-means++算法的概率选择规则相对应。 probs cumulative_probs 分别计算了每个点的概率和累积概率。随机数 r 用于选择新的质心,选取方式是选择累积概率首次超过 r 的点。最终返回一个包含K个质心的NumPy数组。

2.3 质心初始化的性能影响

2.3.1 质心初始化对收敛速度的影响

质心的初始化方法对K-means算法的性能有显著影响。良好的初始化可以大幅减少算法达到收敛所需的迭代次数。随机初始化质心可能导致算法进行更多的迭代才能收敛,特别是当数据集较大时,因为随机选择的质心可能与数据集中的主要分群结构不够吻合。相比之下,K-means++初始化由于考虑了数据点之间的距离,通常可以更快速地收敛。

2.3.2 质心初始化对最终聚类结果的影响

除对收敛速度有影响外,质心初始化对最终聚类结果的准确性也有很大影响。随机初始化可能导致结果偏向于数据集中的某些密集区域,而忽略其他区域。K-means++算法因其初始质心间隔较大,使得最终聚类的分散度和稳定性更高,因此,聚类结果往往更加准确和稳定。这也意味着,使用K-means++算法比随机初始化在获得满意聚类结果方面更有优势,特别是在数据集有明显聚类结构时。

以上内容构成了对第二章”质心的初始化方法”的详细介绍。从随机选择质心的传统方法,到更先进高效的K-means++算法,本章介绍了质心初始化的不同策略以及它们对算法性能的具体影响。接下来的章节将深入探讨K-means算法的迭代分配与质心更新过程。

3. 迭代分配与质心更新

3.1 数据点分配过程

3.1.1 欧氏距离的计算方法

欧氏距离是一种用来衡量两个点之间直线距离的标准方法。它在k-means算法中的应用尤为广泛,因为聚类问题本质上是在寻找数据点与质心之间距离最短的划分。在多维空间中,欧氏距离的计算公式如下:

[ d(p, q) = \sqrt{(q_1 - p_1)^2 + (q_2 - p_2)^2 + \ldots + (q_n - p_n)^2} ]

其中,( p ) 和 ( q ) 是多维空间中的两个点,( q_i ) 和 ( p_i ) 分别是点 ( q ) 和点 ( p ) 在第 ( i ) 维的坐标值。

3.1.2 距离计算与点分配的关联

在k-means算法中,每个数据点需要被分配到最近的质心所代表的簇中。这个“最近”就是根据欧氏距离来判断的。算法的每一次迭代开始时,所有的数据点都会重新计算到每个质心的距离,并且根据最小距离原则分配到相应的簇。

在实现过程中,通常会涉及到一个数据点距离所有质心距离的计算。这个计算可以通过循环遍历所有质心并使用上述公式计算完成,也可以利用矩阵运算优化性能。

#include <vector>
#include <cmath>

struct Point {
    std::vector<double> coordinates;
};

double calculateEuclideanDistance(const Point& point1, const Point& point2) {
    double distance = 0.0;
    for (size_t i = 0; i < point1.coordinates.size(); ++i) {
        distance += (point1.coordinates[i] - point2.coordinates[i]) *
                    (point1.coordinates[i] - point2.coordinates[i]);
    }
    return std::sqrt(distance);
}

// 其中pointList是所有数据点的集合,centroids是当前质心的集合
void assignPointsToCentroids(const std::vector<Point>& pointList, const std::vector<Point>& centroids) {
    for (const auto& point : pointList) {
        double minDistance = std::numeric_limits<double>::max();
        size_t closestCentroidIndex = 0;
        for (const auto& centroid : centroids) {
            double distance = calculateEuclideanDistance(point, centroid);
            if (distance < minDistance) {
                minDistance = distance;
                closestCentroidIndex = &centroid - &centroids[0]; // 记录最近质心的索引
            }
        }
        // 分配逻辑,此处省略具体实现细节
    }
}

3.2 质心的更新策略

3.2.1 质心更新的数学公式

在每次迭代分配完成后,新的簇内数据点将用于更新质心的位置。质心更新的公式可以表示为:

[ \text{新质心} = \frac{1}{N} \sum_{i=1}^{N} x_i ]

其中,( N ) 是属于当前簇 ( C ) 中的点的总数,( x_i ) 表示簇内每一个点的坐标。

3.2.2 更新策略对聚类结果的影响

质心的更新对于算法的最终结果至关重要。在每次迭代中,通过移动质心,使得簇内的点与质心之间的距离总和最小化。这个过程会一直进行,直到满足停止条件为止。更新策略的有效性直接关系到算法收敛速度和最终的聚类质量。

// 假设pointsInCluster是当前簇内所有点的集合
Point calculateNewCentroid(const std::vector<Point>& pointsInCluster) {
    size_t dimensions = pointsInCluster[0].coordinates.size();
    std::vector<double> newCentroidCoordinates(dimensions, 0.0);

    for (const auto& point : pointsInCluster) {
        for (size_t i = 0; i < dimensions; ++i) {
            newCentroidCoordinates[i] += point.coordinates[i];
        }
    }

    for (double& coord : newCentroidCoordinates) {
        coord /= pointsInCluster.size();
    }

    return {newCentroidCoordinates};
}

3.3 迭代停止的条件

3.3.1 最大迭代次数与收敛条件

在实际应用中,算法不可能无限迭代下去。通常会设置一个最大迭代次数(maxIter),一旦达到这个次数,算法就会停止。这个条件可以确保算法在合理的时间内给出结果,即使聚类结果可能不是最优的。

3.3.2 迭代停止的判断方法

除了最大迭代次数之外,另一个常见的停止条件是收敛条件。如果质心在连续几次迭代后的位置变化非常小,那么可以认为算法已经收敛。这种情况下的停止可以防止过度迭代。

int maxIterations = 100;
double centroidConvergenceThreshold = 1e-5;
int iteration = 0;
bool centroidsHaveConverged = false;

while (!centroidsHaveConverged && iteration < maxIterations) {
    // 分配和更新质心的步骤
    // ...

    // 检查收敛条件
    if (isConvergenceSatisfied(centroidsOld, centroidsNew, centroidConvergenceThreshold)) {
        centroidsHaveConverged = true;
    }

    centroidsOld = centroidsNew;
    ++iteration;
}

在这段伪代码中, isConvergenceSatisfied 函数用来判断是否达到了收敛条件, centroidsOld centroidsNew 分别表示迭代前后质心的位置。一旦达到满足停止迭代的条件,算法将停止。

4. 输入输出与数据处理

4.1 鸢尾花数据集介绍

4.1.1 数据集的来源和特征

鸢尾花(Iris)数据集是由著名的统计学家Ronald Fisher于1936年整理的一个用于分类问题的样本数据集。该数据集记录了三个鸢尾花品种(Setosa, Versicolour, 和 Virginica)各50个样本的花萼长度、花萼宽度、花瓣长度、花瓣宽度的测量数据。这些数据构成了一个四维空间的特征向量,因其简洁性和代表性,广泛被用作各种聚类算法的测试集。

4.1.2 数据集的预处理

在应用k-means算法之前,对鸢尾花数据集进行适当的预处理是必要的。预处理包括数据清洗、数据标准化以及数据转换等步骤。数据清洗旨在剔除异常值和缺失值,保证数据质量;数据标准化则是为了消除不同指标量纲的影响,通常使用Z-score标准化方法,计算公式为:

[ z = \frac{(x - \mu)}{\sigma} ]

其中 ( x ) 为原始数据值,( \mu ) 为均值,( \sigma ) 为标准差。

接下来通过数据标准化处理后的数据可以用于后续的聚类分析。

4.2 输入输出数据处理

4.2.1 数据输入的格式和方法

在实现k-means算法时,数据输入的方式需保证算法能有效地访问到每个样本的数据点。在C++中,可以通过数组或向量来存储数据,每个数据点作为一个向量存储在更大的向量中。也可以将数据文件读入到二维数组或二维向量中。代码示例如下:

#include <fstream>
#include <vector>
#include <iostream>

int main() {
    std::string filename = "iris.data";
    std::ifstream file(filename);
    std::vector<std::vector<double>> data;
    std::string line;

    while (getline(file, line)) {
        std::istringstream is_line(line);
        std::vector<double> sample;
        double value;
        while (is_line >> value) {
            sample.push_back(value);
        }
        data.push_back(sample);
    }

    file.close();

    // 此处data变量包含了鸢尾花数据集的数据点
    // 可以进一步处理或直接用于k-means算法
}

4.2.2 数据输出的格式和信息展示

数据输出应该清晰地展示聚类结果,包括每个数据点所属的类别、聚类中心以及聚类后的一些统计信息。以下是输出格式的一种可能实现,使用C++的输出流操作:

#include <iostream>
#include <vector>
#include <iomanip>

void printClusterResults(const std::vector<std::vector<double>>& data, const std::vector<std::vector<double>>& centroids, const std::vector<int>& assignments) {
    for (size_t i = 0; i < data.size(); ++i) {
        std::cout << "Sample " << i + 1 << ": [ ";
        for (size_t j = 0; j < data[i].size(); ++j) {
            std::cout << std::fixed << std::setprecision(2) << data[i][j] << " ";
        }
        std::cout << "] --> Cluster " << assignments[i] << std::endl;
    }

    std::cout << "Centroids:" << std::endl;
    for (size_t i = 0; i < centroids.size(); ++i) {
        std::cout << "Cluster " << i + 1 << ": [ ";
        for (size_t j = 0; j < centroids[i].size(); ++j) {
            std::cout << std::fixed << std::setprecision(2) << centroids[i][j] << " ";
        }
        std::cout << "]" << std::endl;
    }
}

通过上述函数,可以输出数据点所属的聚类编号以及计算得到的聚类中心点坐标。输出时,采用了 std::fixed std::setprecision 来确保输出数字的格式整齐,便于阅读和分析。

5. k-means算法的C++实现与优化

在探索了k-means算法的原理和一些关键步骤之后,我们将进入实际的编程实现阶段。C++作为一个性能强大、灵活的编程语言,在实现算法上有着先天的优势。本章将深入剖析k-means算法的C++实现,并探讨一些常见的优化策略。

5.1 C++代码结构分析

5.1.1 主要类和函数的介绍

在C++中实现k-means算法,我们通常需要定义几个关键的类和函数。基础的类结构可能包括:

  • Point 类:用于表示数据点,包含坐标属性和相关操作。
  • Cluster 类:表示一个聚类,包含质心和属于该聚类的所有数据点。
  • KMeans 类:负责算法的主要逻辑,包含初始化、迭代和优化等方法。

函数则主要包括:

  • initializeCentroids() : 初始化质心。
  • assignPointsToClusters() : 根据质心将数据点分配到最近的聚类。
  • updateCentroids() : 更新聚类的质心。
  • run() fit() :开始算法的主要流程。

5.1.2 代码逻辑结构解析

逻辑上,k-means算法的核心步骤可以分解为以下流程:

  1. 初始化质心。
  2. 对每个数据点,将其分配给最近的质心所代表的聚类。
  3. 更新每个聚类的质心位置。
  4. 重复步骤2和3,直到满足停止条件。

这样的流程使得算法具有很高的模块化,便于在C++中以面向对象的方式实现。

5.2 距离计算函数实现

5.2.1 欧氏距离计算函数编写

在k-means算法中,计算点与质心之间的距离是关键操作。欧氏距离是最常用的度量方式,其计算公式为:

[ d(p, q) = \sqrt{\sum_{i=1}^{n}(q_i - p_i)^2} ]

其中 ( p ) 和 ( q ) 是两个点的坐标。

C++实现如下:

#include <cmath>
#include <vector>

double euclideanDistance(const std::vector<double>& pointA, const std::vector<double>& pointB) {
    double sum = 0.0;
    for (size_t i = 0; i < pointA.size(); ++i) {
        sum += (pointA[i] - pointB[i]) * (pointA[i] - pointB[i]);
    }
    return std::sqrt(sum);
}

5.2.2 其他距离度量的实现

除了欧氏距离,还有曼哈顿距离、切比雪夫距离等其他距离度量方法。每种方法都有其适用的场景和优点,可根据具体问题来选择。

double manhattanDistance(const std::vector<double>& pointA, const std::vector<double>& pointB) {
    double sum = 0.0;
    for (size_t i = 0; i < pointA.size(); ++i) {
        sum += std::abs(pointA[i] - pointB[i]);
    }
    return sum;
}

double chebyshevDistance(const std::vector<double>& pointA, const std::vector<double>& pointB) {
    double maxDiff = 0.0;
    for (size_t i = 0; i < pointA.size(); ++i) {
        double diff = std::abs(pointA[i] - pointB[i]);
        if (diff > maxDiff) {
            maxDiff = diff;
        }
    }
    return maxDiff;
}

5.3 k-means.cpp主函数实现

5.3.1 主函数流程介绍

主函数 int main() 负责算法的启动和流程控制。通常包含以下步骤:

  1. 读取或生成数据点。
  2. 初始化质心。
  3. 进行迭代,直到满足停止条件。
  4. 输出聚类结果。

5.3.2 主函数中的关键代码解读

int main() {
    // 数据点读取或生成
    std::vector<Point> dataPoints;
    // 质心初始化
    std::vector<Point> centroids = initializeCentroids(dataPoints);
    // 迭代过程
    while (!convergenceCriteriaMet) {
        // 数据点分配
        std::vector<Cluster> clusters = assignPointsToClusters(dataPoints, centroids);
        // 更新质心
        centroids = updateCentroids(clusters);
    }
    // 输出结果
    printClusters(clusters);
    return 0;
}

5.4 k-means算法优化策略

5.4.1 常见的k-means优化方法

  • 并行化 :利用多线程或分布式计算来加速计算。
  • K-means++ :初始化质心以减少迭代次数。
  • 早期停止 :当质心移动量低于某个阈值时提前停止迭代。
  • 使用BFR或ELKI等高效算法 :这些算法在处理大数据集时更有效率。

5.4.2 优化策略的实际应用分析

例如,使用K-means++算法来初始化质心,可以显著提高聚类的质量并减少所需迭代次数。代码实现上,K-means++的初始质心选择过程可以是:

std::vector<Point> kMeansPlusPlusInitialization(const std::vector<Point>& dataPoints, int k) {
    std::vector<Point> centroids;
    // 具体实现...
}

通过应用这些优化策略,算法的性能可以得到显著提升,特别是在处理大规模数据集时。

在实现和优化k-means算法的过程中,我们必须不断地平衡算法的性能和精确度,确保得到实用且高效的聚类结果。在下一章节,我们将详细探讨如何将这些理论和实现应用到实际问题中。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:k-means算法是一种无监督机器学习方法,用于数据聚类。它将样本点分配到K个类别中,最小化每个样本点到其类别中心点的距离平方和。算法包括初始化质心、迭代分配与更新质心以及满足终止条件停止迭代。本文深入分析了C++中k-means算法的实现,包括其关键步骤的代码结构和优化策略。我们使用鸢尾花数据集作为测试案例,演示了算法的应用。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐