K均值聚类(K_means)基础理论
K_means聚类是一种简单且广泛使用的聚类算法,它旨在将数据集中的样本划分为k
个不同的聚类,其中k
是事先指定的聚类数量,该算法的核心思想是迭代地优化聚类中心,以最小化每个样本与其所属聚类中心之间的距离之和。这个算法的优点是简单易懂、计算效率高,适用于大规模数据集,但是嘞,它需要事先指定聚类数量k,同时这个算法对初始聚类中心的选择敏感,还可能陷入局部最优解,所以在实际使用之前还是需要先考虑使用场景。
计算公式
-
聚类中心的定义: 对于第 i i i个聚类中心 μ i \mu_i μi,它是一个 d d d维向量,其中 d d d是数据点的维度。在K_means聚类算法中,聚类中心用于表示每个聚类的中心位置,是聚类内所有点的均值。
-
数据点与聚类中心的距离: 数据点与聚类中心的距离使用欧几里得距离公式计算,即:
d ( x , μ i ) = ∑ j = 1 d ( x j − μ i j ) 2 d(x, \mu_i) = \sqrt{\sum_{j=1}^{d} (x_j - \mu_{ij})^2} d(x,μi)=j=1∑d(xj−μij)2
这里:
x x x是一个 d d d维的数据点,
μ i \mu_i μi是第 i i i个聚类中心,
x j x_j xj是数据点 x x x的第 j j j个维度,
μ i j \mu_{ij} μij是第 i i i个聚类中心的第 j j j个维度。 -
聚类分配: 对于数据点 x x x,其所属的聚类 c c c是通过计算 x x x与所有聚类中心的距离,并选择距离最小的聚类中心所对应的聚类。具体地,聚类分配满足:
c = arg min i d ( x , μ i ) c = \arg\min_{i} d(x, \mu_i) c=argimind(x,μi)
即 c c c是使得 d ( x , μ i ) d(x, \mu_i) d(x,μi)最小的 i i i的值。 -
聚类中心的更新: 在每次迭代中,聚类中心会根据当前聚类内的数据点进行更新。对于第 i i i个聚类,其新的聚类中心 μ i ′ \mu_i' μi′是该聚类内所有数据点的均值,即:
μ i ′ = 1 ∣ C i ∣ ∑ x ∈ C i x \mu_i' = \frac{1}{|C_i|} \sum_{x \in C_i} x μi′=∣Ci∣1x∈Ci∑x
其中:
C i C_i Ci表示第 i i i个聚类内的所有数据点,
∣ C i ∣ |C_i| ∣Ci∣表示第 i i i个聚类内的数据点数量。 -
算法终止条件: K_Means算法在两种情况下会终止:一是当聚类中心在迭代过程中不再发生变化时,即新的聚类中心与旧的聚类中心相同;二是当达到预定的迭代次数时。在这两种情况下,算法认为已经找到了稳定的聚类结构,并会停止迭代。
选择训练所使用的数据集
iris数据集,全称为安德森鸢尾花卉数据集(Anderson's Iris dataset)
,是R语言中一个非常经典且广泛使用的数据集,该数据集最初由迷糊老师(Teacher MiHu)收集,用于统计分类和聚类分析。iris数据集包含了150个样本,每个样本代表了不同种类的鸢尾花(Iris)的四个测量值:萼片长度(Sepal.Length)、萼片宽度(Sepal.Width)、花瓣长度(Petal.Length)和花瓣宽度(Petal.Width),以及对应的鸢尾花种类(Species),共有三种:山鸢尾(Setosa)、变色鸢尾(Versicolor)和维吉尼亚鸢尾(Virginica)。
由于iris数据集包含三个不同种类的鸢尾花,且这些种类在特征空间中存在明显的区分,因此非常适合用于练习k_means聚类算法。在实际应用中,k_means聚类算法需要事先指定聚类数k,而在iris数据集中,由于已知存在三个种类,因此可以设定k=3来进行聚类,从而验证聚类效果。
iris数据集的内容如下:
C语言实现
定义一个结构体iris
,包含不同种类的鸢尾花(Iris)的四个测量值:萼片长度(sepal_length
)、萼片宽度(sepal_width
)、花瓣长度(petal_length
)和花瓣宽度(petal_width
)和 簇( cluster
)
typedef struct {
double sepal_length;
double sepal_width;
double petal_length;
double petal_width;
int cluster;
} Iris;
定义一个名为calculate_distance
的函数,这个函数接收两个指向 Iris 结构体的指针作为参数,并返回这两个实例之间的欧几里得距离。
double calculate_distance(const Iris *a, const Iris *b) {
return sqrt(pow(a->sepal_length - b->sepal_length, 2) +
pow(a->sepal_width - b->sepal_width, 2) +
pow(a->petal_length - b->petal_length, 2) +
pow(a->petal_width - b->petal_width, 2));
}
随后定义一个名为assign_to_clusters
的函数,将数据集data
中的每个数据点分配到最近的聚类中心(centroid)
,代码中的centroids
是一个指向iris结构体数组的指针,该数组包含了当前的聚类中心位置data_size
是数据点的总数k
是聚类中心
的数量。
void assign_to_clusters(Iris *data, Iris *centroids, int data_size, int k) {
for (int i = 0; i < data_size; i++) {
double min_distance = INFINITY;
int closest_cluster = 0;
for (int j = 0; j < k; j++) {
double distance = calculate_distance(&data[i], ¢roids[j]);
if (distance < min_distance) {
min_distance = distance;
closest_cluster = j;
}
}
data[i].cluster = closest_cluster;
}
}
这段代码定义一个名为update_centroids
的函数,其目的是更新K-means聚类算法中的聚类中心位置。
void update_centroids(Iris *data, Iris *centroids, int data_size, int k) {
int *cluster_counts = (int *)malloc(k * sizeof(int));
memset(cluster_counts, 0, k * sizeof(int));
for (int i = 0; i < k; i++) {
centroids[i].sepal_length = 0;
centroids[i].sepal_width = 0;
centroids[i].petal_length = 0;
centroids[i].petal_width = 0;
}
for (int i = 0; i < data_size; i++) {
int cluster = data[i].cluster;
centroids[cluster].sepal_length += data[i].sepal_length;
centroids[cluster].sepal_width += data[i].sepal_width;
centroids[cluster].petal_length += data[i].petal_length;
centroids[cluster].petal_width += data[i].petal_width;
cluster_counts[cluster]++;
}
for (int i = 0; i < k; i++) {
if (cluster_counts[i] > 0) {
centroids[i].sepal_length /= cluster_counts[i];
centroids[i].sepal_width /= cluster_counts[i];
centroids[i].petal_length /= cluster_counts[i];
centroids[i].petal_width /= cluster_counts[i];
}
}
free(cluster_counts);
}
定义一个k_means
函数初始化聚类中心。通过随机选择数据集中的点作为初始聚类中心,这里使用了srand(time(NULL))
来初始化随机数生成器,但通常建议在程序开始时只调用一次srand
,而不是在每次调用k_means
时都调用。
迭代过程:
- 使用一个
do-while
循环来重复执行以下步骤,直到达到最大迭代次数或聚类中心不再发生变化。 - 使用
memcpy
复制当前聚类中心到old_centroids
,以便后续比较。 - 调用
assign_to_clusters
函数,将每个数据点分配到最近的聚类中心。 - 调用
update_centroids
函数,根据当前分配更新聚类中心的位置。 - 通过比较
old_centroids和centroids
检查聚类中心是否发生了变化。 - 释放
old_centroids
占用的内存。
void k_means(Iris *data, Iris *centroids, double *cluster_ss, int data_size, int k, int max_iterations) {
Iris *old_centroids = (Iris *)malloc(k * sizeof(Iris));
int *cluster_counts = (int *)malloc(k * sizeof(int));
srand(time(NULL));
for (int i = 0; i < k; i++) {
int index = rand() % data_size;
// 确保选择不同的质心
while (i > 0 && calculate_distance(¢roids[i], &data[index]) == 0.0) {
index = rand() % data_size;
}
centroids[i] = data[index];
}
int iteration = 0;
do {
memcpy(old_centroids, centroids, k * sizeof(Iris));
assign_to_clusters(data, centroids, cluster_counts, cluster_ss, data_size, k);
update_centroids(data, centroids, cluster_counts, data_size, k);
iteration++;
} while (iteration < max_iterations &&
(memcmp(old_centroids, centroids, k * sizeof(Iris)) != 0));
free(old_centroids);
free(cluster_counts);
}
定义一个calculate_global_mean
函数计算给定数据集的全局平均值(即所有特征的平均值)。
函数的主要步骤如下:
- 初始化一个Iris结构体变量
global_mean
,其所有特征值都设置为0。 - 遍历数据集,累加每个数据点的特征值到
global_mean
中。 - 将累加后的特征值除以数据点的数量,计算出平均值。
- 返回计算得到的全局平均值。
Iris calculate_global_mean(Iris *data, int data_size) {
Iris global_mean = {0};
for (int i = 0; i < data_size; i++) {
global_mean.sepal_length += data[i].sepal_length;
global_mean.sepal_width += data[i].sepal_width;
global_mean.petal_length += data[i].petal_length;
global_mean.petal_width += data[i].petal_width;
}
global_mean.sepal_length /= data_size;
global_mean.sepal_width /= data_size;
global_mean.petal_length /= data_size;
global_mean.petal_width /= data_size;
return global_mean;
}
定义函数print_cluster_means
用于打印聚类中心的均值
void print_cluster_means(Iris *centroids, int k) {
printf("集群均值:\n");
for (int i = 0; i < k; i++) {
printf("集群 %d: Sepal Length: %.2f, Sepal Width: %.2f, Petal Length: %.2f, Petal Width: %.2f\n",
i, centroids[i].sepal_length, centroids[i].sepal_width, centroids[i].petal_length, centroids[i].petal_width);
}
}
定义函数calculate_within_cluster_ss
计算所有聚类内部的平方和(Within-Cluster Sum of Squares, WCSS)
double calculate_within_cluster_ss(Iris *data, Iris *centroids, int data_size, int k) {
double within_ss = 0.0;
for (int i = 0; i < data_size; i++) {
within_ss += calculate_distance(&data[i], ¢roids[data[i].cluster]) * calculate_distance(&data[i], ¢roids[data[i].cluster]);
}
return within_ss;
}
定义calculate_total_ss
函数计算数据集的总平方和(Total Sum of Squares, TSS)
double calculate_total_ss(Iris *data, Iris global_mean, int data_size) {
double total_ss = 0.0;
for (int i = 0; i < data_size; i++) {
total_ss += calculate_distance(&data[i], &global_mean) * calculate_distance(&data[i], &global_mean);
}
return total_ss;
}
定义函数calculate_between_cluster_ss
函数计算聚类间的平方和(Between-Cluster Sum of Squares, BSS)
double calculate_between_cluster_ss(Iris *centroids, Iris global_mean, int k) {
double between_ss = 0.0;
for (int i = 0; i < k; i++) {
between_ss += calculate_distance(¢roids[i], &global_mean) * calculate_distance(¢roids[i], &global_mean);
}
return between_ss;
}
加入main
主函数的完整版代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
typedef struct {
double sepal_length;
double sepal_width;
double petal_length;
double petal_width;
int cluster;
} Iris;
double calculate_distance(const Iris *a, const Iris *b) {
return sqrt(pow(a->sepal_length - b->sepal_length, 2) +
pow(a->sepal_width - b->sepal_width, 2) +
pow(a->petal_length - b->petal_length, 2) +
pow(a->petal_width - b->petal_width, 2));
}
void assign_to_clusters(Iris *data, Iris *centroids, int *cluster_counts, double *cluster_ss, int data_size, int k) {
memset(cluster_counts, 0, k * sizeof(int));
memset(cluster_ss, 0, k * sizeof(double));
for (int i = 0; i < data_size; i++) {
double min_distance = INFINITY;
int closest_cluster = -1;
for (int j = 0; j < k; j++) {
double distance = calculate_distance(&data[i], ¢roids[j]);
if (distance < min_distance) {
min_distance = distance;
closest_cluster = j;
}
}
data[i].cluster = closest_cluster;
cluster_ss[closest_cluster] += min_distance * min_distance; // 累加每个集群的平方和
cluster_counts[closest_cluster]++;
}
}
void update_centroids(Iris *data, Iris *centroids, int *cluster_counts, int data_size, int k) {
for (int i = 0; i < k; i++) {
centroids[i].sepal_length = 0;
centroids[i].sepal_width = 0;
centroids[i].petal_length = 0;
centroids[i].petal_width = 0;
}
for (int i = 0; i < data_size; i++) {
int cluster = data[i].cluster;
centroids[cluster].sepal_length += data[i].sepal_length;
centroids[cluster].sepal_width += data[i].sepal_width;
centroids[cluster].petal_length += data[i].petal_length;
centroids[cluster].petal_width += data[i].petal_width;
}
for (int i = 0; i < k; i++) {
if (cluster_counts[i] > 0) {
centroids[i].sepal_length /= cluster_counts[i];
centroids[i].sepal_width /= cluster_counts[i];
centroids[i].petal_length /= cluster_counts[i];
centroids[i].petal_width /= cluster_counts[i];
}
}
}
void k_means(Iris *data, Iris *centroids, double *cluster_ss, int data_size, int k, int max_iterations) {
Iris *old_centroids = (Iris *)malloc(k * sizeof(Iris));
int *cluster_counts = (int *)malloc(k * sizeof(int));
srand(time(NULL));
for (int i = 0; i < k; i++) {
int index = rand() % data_size;
// 确保选择不同的质心
while (i > 0 && calculate_distance(¢roids[i], &data[index]) == 0.0) {
index = rand() % data_size;
}
centroids[i] = data[index];
}
int iteration = 0;
do {
memcpy(old_centroids, centroids, k * sizeof(Iris));
assign_to_clusters(data, centroids, cluster_counts, cluster_ss, data_size, k);
update_centroids(data, centroids, cluster_counts, data_size, k);
iteration++;
} while (iteration < max_iterations &&
(memcmp(old_centroids, centroids, k * sizeof(Iris)) != 0));
free(old_centroids);
free(cluster_counts);
}
Iris calculate_global_mean(Iris *data, int data_size) {
Iris global_mean = {0};
for (int i = 0; i < data_size; i++) {
global_mean.sepal_length += data[i].sepal_length;
global_mean.sepal_width += data[i].sepal_width;
global_mean.petal_length += data[i].petal_length;
global_mean.petal_width += data[i].petal_width;
}
global_mean.sepal_length /= data_size;
global_mean.sepal_width /= data_size;
global_mean.petal_length /= data_size;
global_mean.petal_width /= data_size;
return global_mean;
}
void read_csv(char *filename, Iris *data, int data_size) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
fprintf(stderr, "无法打开文件 '%s'\n", filename);
exit(EXIT_FAILURE);
}
// 声明行缓冲区
char line[256];
// 跳过标题行
if (fgets(line, sizeof(line), file) == NULL) {
fprintf(stderr, "错误:无法读取标题行。\n");
fclose(file);
exit(EXIT_FAILURE);
}
int i = 0;
while (fgets(line, sizeof(line), file) != NULL && i < data_size) {
if (sscanf(line, "%lf,%lf,%lf,%lf",
&data[i].sepal_length, &data[i].sepal_width,
&data[i].petal_length, &data[i].petal_width) == 4) {
i++;
} else {
fprintf(stderr, "读取行时出错: %s", line);
break;
}
}
if (feof(file)) {
} else {
perror("读取文件时出错");
}
fclose(file);
}
void print_cluster_means(Iris *centroids, int k) {
printf("簇均值:\n");
for (int i = 0; i < k; i++) {
printf("簇 %d: Sepal Length: %.2f, Sepal Width: %.2f, Petal Length: %.2f, Petal Width: %.2f\n",
i, centroids[i].sepal_length, centroids[i].sepal_width, centroids[i].petal_length, centroids[i].petal_width);
}
}
double calculate_within_cluster_ss(Iris *data, Iris *centroids, int data_size, int k) {
double within_ss = 0.0;
for (int i = 0; i < data_size; i++) {
within_ss += calculate_distance(&data[i], ¢roids[data[i].cluster]) * calculate_distance(&data[i], ¢roids[data[i].cluster]);
}
return within_ss;
}
double calculate_total_ss(Iris *data, Iris global_mean, int data_size) {
double total_ss = 0.0;
for (int i = 0; i < data_size; i++) {
total_ss += calculate_distance(&data[i], &global_mean) * calculate_distance(&data[i], &global_mean);
}
return total_ss;
}
double calculate_between_cluster_ss(Iris *centroids, Iris global_mean, int k) {
double between_ss = 0.0;
for (int i = 0; i < k; i++) {
between_ss += calculate_distance(¢roids[i], &global_mean) * calculate_distance(¢roids[i], &global_mean);
}
return between_ss;
}
int main() {
int data_size = 150;
Iris *data = (Iris *)malloc(data_size * sizeof(Iris));
read_csv("iris_dataset.csv", data, data_size);
int k = 3;
int max_iterations = 100;
srand(time(NULL));
Iris *centroids = (Iris *)malloc(k * sizeof(Iris));
double cluster_ss[k];
k_means(data, centroids, cluster_ss, data_size, k, max_iterations);
Iris global_mean = calculate_global_mean(data, data_size);
// 输出聚类大小
int *cluster_sizes = (int *)malloc(k * sizeof(int));
memset(cluster_sizes, 0, k * sizeof(int));
for (int i = 0; i < data_size; i++) {
cluster_sizes[data[i].cluster]++;
}
printf("\nK-means聚类有 %d 簇:", k);
for (int i = 0; i < k; i++) {
printf("%d, ", cluster_sizes[i]);
}
printf("\b\b\n");
// 输出聚类中心
print_cluster_means(centroids, k);
// 输出聚类向量
printf("\n聚类向量:\n");
for (int i = 0; i < data_size; i++) {
printf(" %d", data[i].cluster);
if ((i + 1) % 10 == 0) {
printf("\n");
}
}
// 输出within SS 和 between SS 比率
double within_ss = calculate_within_cluster_ss(data, centroids, data_size, k);
double total_ss = calculate_total_ss(data, global_mean, data_size);
double between_ss = calculate_between_cluster_ss(centroids, global_mean, k);
printf("\n聚类内各聚类平方和:\n");
for (int i = 0; i < k; i++) {
printf("[%d] %.3f\n", i + 1, cluster_ss[i]);
}
if (total_ss == 0.0) {
printf("(between_SS / total_SS = undefined)\n");
} else {
printf("(between_SS / total_SS = %.1f %%)\n", (between_ss / total_ss) * 100);
}
free(data);
free(centroids);
free(cluster_sizes);
return 0;
}
代码运行后输出:
K-means聚类有 3 簇:62, 38, 50,
簇均值:
簇 0: Sepal Length: 5.90, Sepal Width: 2.75, Petal Length: 4.39, Petal Width: 1.43
簇 1: Sepal Length: 6.85, Sepal Width: 3.07, Petal Length: 5.74, Petal Width: 2.07
簇 2: Sepal Length: 5.01, Sepal Width: 3.43, Petal Length: 1.46, Petal Width: 0.25
聚类向量:
2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2
0 0 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
1 0 1 1 1 1 0 1 1 1
1 1 1 0 0 1 1 1 1 0
1 0 1 0 1 1 0 0 1 1
1 1 1 0 1 1 1 1 0 1
1 1 0 1 1 1 0 1 1 0
聚类内各聚类平方和:
[1] 39.821
[2] 23.879
[3] 15.151
(between_SS / total_SS = 2.0 %)
R语言和Python实现
相较于C语言来说,R语言实现就非常简洁,在设置好随机种子set.seed()
后,直接使用内置的kmeans
函数就可以轻松实现:
data(iris)
X <- iris[, 1:4]
set.seed(123)
kmeans_value <- kmeans(X, centers = 3)
print(kmeans_value)
Python实现需要提前安装好scikit-learn
库和pandas
库:
import pandas as pd
from sklearn.cluster import KMeans
data = pd.read_csv('iris_dataset.csv')
X = data.iloc[:, :-1].values
kmeans = KMeans(n_clusters=3, random_state=0).fit(X)
labels = kmeans.labels_
centroids = kmeans.cluster_centers_
print(centroids)