TL;DR
场景:用 NumPy/Pandas 手写 K-Means,对 Iris.txt 做 3 类聚类并输出质心与分簇结果
结论:实现可跑通,但需补齐”空簇处理 / 最大迭代 / 版本与数据类型约束 / 特征尺度”才能工程化稳定
产出:distEclud + randCent + kMeans 完整链路、结果表 result_set、常见错误定位与修复速查
Python实现
导入依赖
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# 解决坐标轴刻度负号乱码
plt.rcParams['axes.unicode_minus'] = False
# 解决中文乱码问题
plt.rcParams['font.sans-serif'] = ['Simhei']
导入数据集
此处使用鸢尾花数据集为例:
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
#导入数据集
iris = pd.read_csv("iris.txt",header = None)
iris.head()
iris.shape
编写距离计算函数
我们需要定义一个两个长度相等的数组之间欧式距离计算函数,在不直接应用计算结果,只比较距离远近的情况下,我们可以用距离平方和代替距离进行比较,化简开平方运算,从而减少函数计算量。此外需要说明的是,涉及到距离计算的,一定要注意量纲的统一。如果量纲不统一的话,模型极易偏向量纲大的那一方。
函数功能:计算两个数据集之间的欧式距离 输入:两个 array 数据集 返回:两个数据集之间的欧式距离(此处用距离平方和代替距离)
def distEclud(arrA, arrB):
d = arrA - arrB
dist = np.sum(np.power(d, 2), axis=1)
return dist
编写随机函数生成质心函数
在定义随机质心生成函数时,需要按照以下步骤进行操作:
- 数据范围计算:首先遍历数据集中的每一列(特征),计算该列的最小值(min)和最大值(max)
- 随机质心生成:根据用户指定的簇个数k,生成k个质心
- 参数说明:
- 输入参数:dataSet(包含标签的完整数据集),k(需要生成的质心数量)
- 输出参数:data_cent(生成的k个质心)
def randCent(dataSet, k):
# n为列数,假设dataSet是一个DataFrame
n = dataSet.shape[1]
# 获取每一列的最小值和最大值(仅使用前 n-1 列,最后一列是标签或类别)
data_min = dataSet.iloc[:, :n-1].min()
data_max = dataSet.iloc[:, :n-1].max()
# 在最小值和最大值之间生成 k 个随机中心点
data_cent = np.random.uniform(data_min, data_max, (k, n-1))
return data_cent
编写 K-Means 聚类函数
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
# 获取数据集的维度,m 是行数,n 是列数
m, n = dataSet.shape
# 初始化质心 centroids,生成 k 个随机质心
centroids = createCent(dataSet, k)
# 初始化 clusterAssment 矩阵
clusterAssment = np.zeros((m, 3))
clusterAssment[:, 0] = np.inf
clusterAssment[:, 1:3] = -1
# 将数据集和 clusterAssment 合并,形成 result_set
result_set = pd.concat([dataSet, pd.DataFrame(clusterAssment)], axis=1, ignore_index=True)
# 标记簇是否发生变化
clusterChanged = True
while clusterChanged:
clusterChanged = False
# 遍历每个样本点
for i in range(m):
# 计算当前数据点到所有质心的距离
dist = distMeas(dataSet.iloc[i, :n-1].values, centroids)
# 记录最小距离和对应质心的索引
result_set.iloc[i, n] = dist.min()
result_set.iloc[i, n+1] = np.where(dist == dist.min())[0][0]
# 检查当前簇分配与上次是否完全一致
clusterChanged = not (result_set.iloc[:, -1] == result_set.iloc[:, -2]).all()
# 如果簇分配发生变化,则更新质心
if clusterChanged:
cent_df = result_set.groupby(n+1).mean()
centroids = cent_df.iloc[:, :n-1].values
result_set.iloc[:, -1] = result_set.iloc[:, -2]
return centroids, result_set
错误速查
| 症状 | 根因 | 修复 |
|---|---|---|
| 读入 iris.txt 报错或为空 | 路径不对/文件不在工作目录 | 使用 os.getcwd()、pd.read_csv 报错栈;使用绝对路径或把数据放到工作目录 |
iris.shape 显示列数不符合预期 | 分隔符/编码问题导致整行进一列 | iris.head() 检查列是否被挤到一列;指定 sep=',' |
K-Means 迭代报 TypeError/could not convert string to float | 标签列是字符串,但参与了数值运算 | 检查 dataSet.dtypes;result_set.groupby(...).mean() 只对数值特征列计算 |
质心数量变少(k=3 变成 2)或出现 NaN | 空簇:某簇没有样本被分配 | 检查 result_set.iloc[:, n+1].value_counts();检测缺失簇并重置该簇质心 |
| 程序长时间不结束或”看似卡住” | 缺少 max_iter | 为 while 循环增加 max_iter 与容差阈值 |
| 每次运行结果差异大,难复现 | 未设置随机种子 | 在 randCent 前设置 np.random.seed(...) |
| 聚类结果偏向某一维度,分簇不合理 | 特征量纲不统一 | 对比各列范围 min/max;先做标准化/归一化 |