
简介本资源是一套面向机器学习初学者与数据挖掘实践者的Python版KMeans聚类算法完整实现聚焦于无监督学习中的核心聚类任务适用于课程设计、算法复现及科研预研等场景。压缩包共246个文件总大小35.02MB涵盖141个CSV格式的多源时序分解数据集如EEMD、CEEMDAN系列、43张PNG可视化图表含聚类过程迭代图、簇中心演化与轮廓系数分析、16个结构清晰的Python脚本含数据标准化、肘部法则计算、KMeans主循环及评估模块另有辅助配置与版本控制文件。已有60人学习下载资源目录按功能分层组织从原始数据→预处理→建模→评估→可视化形成闭环流程所有代码可直接运行配套图表直观呈现不同K值下的聚类效果差异为理解算法原理与调参逻辑提供了扎实的实践支撑。1. 项目概述从理论到实践的KMeans之旅如果你正在学习机器学习或者手头有一堆数据想看看能不能分个类那KMeans聚类算法几乎是你绕不开的第一个实战点。它不像那些需要你提前打好标签的分类算法KMeans属于“无监督学习”意思就是你只管把数据扔给它它自己就能摸索着把相似的数据点归到一堆。这个“一堆”在算法里就叫“簇”。我最早接触它是在处理用户行为数据的时候面对几万条没有任何分类标记的点击流记录KMeans帮我快速勾勒出了几个典型的用户群体画像为后续的精准运营省下了大量人工标注的成本。今天我就把手写Python实现KMeans源码的整个过程连同如何准备和处理数据集的经验一次性讲透。无论你是刚学完Python语法想找个项目练手还是需要在工作中快速应用聚类分析这篇内容都能让你不仅“跑通代码”更能“吃透原理”避开我当年踩过的那些坑。简单来说这个项目要干两件事一是用纯Python不直接调sklearn的KMeans类从头实现KMeans算法的核心逻辑二是使用真实或模拟的数据集来验证我们写的算法对不对。通过这个实践你会彻底明白“质心”是怎么移动的“距离”是怎么计算的以及为什么KMeans有时会给出不太理想的结果。我们会用最流行的numpy来处理数据计算用matplotlib来可视化聚类过程整个代码结构清晰每一步都有对应的原理讲解。你会发现看似神秘的机器学习算法拆解开来都是扎实的数学和编程基本功。2. KMeans核心原理与我们的实现思路在动手写代码之前我们必须把KMeans的“大脑”搞清楚。很多教程直接上公式容易让人懵。我用一个最生活的例子来比喻假设你是一个快递站老板手下有几个快递员质心负责一片居民区数据点。你的目标是划分送货区域让每个快递员负责的住户尽可能离他的站点近同时各个区域住户数量均衡。KMeans做的就是类似的事它通过迭代来优化这个划分过程可以概括为四个步骤初始化随机在居民区里选几个点作为快递站的初始位置初始质心。分配阶段计算每一个住户数据点到所有快递站质心的距离然后把这个住户分配给离他最近的那个快递员。更新阶段所有住户分配完毕后每个快递员根据自己负责的所有住户的位置重新计算一个新的站点位置通常是这些住户位置的平均值也就是“均值点”并搬过去。迭代重复第2步和第3步直到快递站的位置不再发生显著变化或者说每个住户所属的快递员不再改变算法就收敛了。这里有几个关键点决定了我们代码怎么写距离度量我们怎么定义“远近”最常用的是欧几里得距离也就是我们中学学的两点间直线距离。在代码里这就是numpy向量运算的优势所在。质心初始化随机选虽然简单但可能导致结果不好或收敛慢。我们实现时会采用更常用的“K-Means”初始化方法它能让初始质心彼此远离效果更好更稳定。这是第一个要动手实现的重要改进。停止条件通常有两种一是质心的移动距离小于一个很小的阈值比如1e-4二是迭代次数达到了预设的最大值。我们的代码里会把这两个都加上。基于这个思路我们的代码骨架就出来了一个KMeans类包含初始化方法、一个拟合方法包含分配和更新的循环、以及一个预测方法。我们会把计算距离、分配簇、更新质心这些核心操作都拆成独立的函数这样代码既好读也方便调试。注意自己实现算法最大的好处是“可控”。你用sklearn的黑盒子如果聚类效果不好可能很难定位问题。而自己写的代码你可以在每一轮迭代后打印质心位置、每个簇的样本数甚至画图看看问题出在哪一目了然。3. 手把手实现KMeans核心源码接下来我们进入实战环节。我会分模块讲解代码并解释每一行背后的意图。建议你打开编辑器跟着敲一遍。3.1 环境准备与工具导入首先确保你的Python环境里安装了必要的科学计算库。打开终端用pip安装pip install numpy matplotlib scikit-learnnumpy核心用于高效的数组和矩阵运算所有距离计算、均值计算都靠它。matplotlib用于可视化我们可以把聚类过程动态地画出来非常直观。scikit-learn我们不用它的KMeans但会用它来生成模拟数据集以及最后和我们自己实现的算法结果做对比验证这是检验我们代码正确性的好方法。然后在Python文件的开头导入这些库import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs # 用于生成模拟聚类数据 from sklearn.metrics import pairwise_distances_argmin # 用于高效计算最近质心我们后面会自己实现类似功能 import random3.2 KMeans类骨架与初始化我们定义一个KMeans类。__init__方法用于接收算法的主要参数。class MyKMeans: def __init__(self, n_clusters3, max_iter300, tol1e-4, initk-means, random_stateNone): 初始化KMeans参数。 :param n_clusters: 要形成的簇数也是质心数量。必须事先指定这是KMeans的主要缺点之一。 :param max_iter: 最大迭代次数防止不收敛时无限循环。 :param tol: 容忍度当质心移动距离小于此值时认为已收敛停止迭代。 :param init: 质心初始化方法random为随机选择k-means为改进方法。 :param random_state: 随机种子固定后可使每次运行结果一致便于调试。 self.n_clusters n_clusters self.max_iter max_iter self.tol tol self.init init self.random_state random_state self.centroids None # 质心坐标形状为 (n_clusters, n_features) self.labels None # 每个样本点所属的簇标签 self.inertia_ None # 所有样本到其所属质心的距离平方和用于衡量聚类效果越小越好这里inertia_惯性是聚类效果的一个内部指标就是所有样本点到其所属质心距离的平方和。算法优化的目标其实就是最小化这个值。3.3 核心方法一质心初始化K-Means随机初始化质心可能导致糟糕的局部最优解。K-Means的思路是让初始质心尽可能彼此远离。第一个质心从数据点中随机选取。对于每个数据点计算它到已选质心的最短距离即离已选质心中最近的那个的距离。将这些最短距离的平方作为概率权重随机选择下一个质心。距离越远的点被选为下一个质心的概率越大。重复步骤2和3直到选够K个质心。def _initialize_centroids(self, X): 使用K-Means方法初始化质心。 n_samples, n_features X.shape centroids np.zeros((self.n_clusters, n_features)) # 设置随机种子以保证可复现性 if self.random_state is not None: np.random.seed(self.random_state) # 1. 随机选择第一个质心 first_idx np.random.randint(n_samples) centroids[0] X[first_idx] # 2. 选择剩余的 K-1 个质心 for i in range(1, self.n_clusters): # 计算每个样本点到已有质心的最小距离平方 distances np.array([min([np.linalg.norm(x - c) ** 2 for c in centroids[:i]]) for x in X]) # 将距离平方转换为概率距离越远概率越大 probabilities distances / distances.sum() # 根据概率分布随机选择一个样本点作为下一个质心 next_idx np.random.choice(n_samples, pprobabilities) centroids[i] X[next_idx] return centroids这个初始化方法比纯随机多了一些计算但能显著提升最终聚类效果和收敛速度是实践中几乎必用的技巧。3.4 核心方法二分配样本点到最近质心这一步需要计算每个样本点到所有质心的距离并找到最近的质心索引。我们使用欧氏距离。为了提高效率我们利用numpy的广播机制进行向量化计算避免写低效的Python循环。def _assign_clusters(self, X, centroids): 将每个样本点分配到最近的质心。 :return: labels - 每个样本点所属簇的索引数组。 # 计算样本点与所有质心的距离矩阵形状为 (n_samples, n_clusters) # np.linalg.norm用于计算范数axis1指定按行计算即每个样本点与一个质心的距离 # 这里利用广播X[:, np.newaxis, :] 形状 (n_samples, 1, n_features) # centroids[np.newaxis, :, :] 形状 (1, n_clusters, n_features) # 相减后形状 (n_samples, n_clusters, n_features)再求范数得到距离 distances np.linalg.norm(X[:, np.newaxis, :] - centroids[np.newaxis, :, :], axis2) # 找到每个样本点距离最小的那个质心的索引 labels np.argmin(distances, axis1) return labels向量化运算是numpy的精华它底层用C实现比用Python写for循环快成百上千倍。理解这里的广播机制对写好科学计算代码至关重要。3.5 核心方法三更新质心位置根据当前分配结果重新计算每个簇的质心即该簇所有样本点的均值。def _update_centroids(self, X, labels): 根据当前样本点的分配重新计算每个簇的质心。 :return: new_centroids - 新的质心坐标。 new_centroids np.zeros((self.n_clusters, X.shape[1])) for i in range(self.n_clusters): # 找出所有属于簇i的样本点 cluster_points X[labels i] # 防止空簇出现如果某个簇没有点则保持原质心不变或重新初始化 if len(cluster_points) 0: new_centroids[i] cluster_points.mean(axis0) else: # 如果出现空簇一种处理方法是将其质心设置为一个随机数据点 new_centroids[i] X[np.random.randint(X.shape[0])] return new_centroids这里有一个重要的边界情况处理空簇。在迭代过程中有可能某个质心分配不到任何样本点。如果不处理计算均值时会出错。我们的策略是如果遇到空簇就随机选择一个数据点作为该簇的新质心。还有其他策略比如将空簇的质心设置为离它最远的那个样本点或者直接忽略它但这样簇数就减少了。3.6 核心方法四拟合主循环现在我们把初始化、分配、更新串起来形成完整的训练流程。def fit(self, X): 在数据X上拟合KMeans模型。 :param X: 训练数据形状为 (n_samples, n_features)。 :return: self - 返回实例自身符合scikit-learn API风格。 n_samples, n_features X.shape # 1. 初始化质心 if self.init k-means: self.centroids self._initialize_centroids(X) else: # random random_indices np.random.choice(n_samples, self.n_clusters, replaceFalse) self.centroids X[random_indices] # 迭代优化 for iteration in range(self.max_iter): # 2. 分配簇 self.labels self._assign_clusters(X, self.centroids) # 3. 更新质心 new_centroids self._update_centroids(X, self.labels) # 4. 检查收敛条件质心移动的平均距离是否小于容忍度 centroid_shift np.linalg.norm(new_centroids - self.centroids, axis1).mean() if centroid_shift self.tol: print(f迭代在第 {iteration 1} 轮收敛。) break self.centroids new_centroids else: # 如果for循环正常结束未break说明达到了最大迭代次数 print(f达到最大迭代次数 {self.max_iter}可能未完全收敛。) # 计算最终的惯性距离平方和 self.inertia_ 0 for i in range(self.n_clusters): cluster_points X[self.labels i] if len(cluster_points) 0: distances np.linalg.norm(cluster_points - self.centroids[i], axis1) self.inertia_ np.sum(distances ** 2) return self def predict(self, X): 预测新数据点所属的簇。 # 本质上就是分配步骤 return self._assign_clusters(X, self.centroids)fit方法是核心它控制着迭代的节奏。我们添加了详细的打印信息方便观察收敛过程。predict方法很简单就是对新的数据点用训练好的质心去分配簇标签。实操心得在调试自己实现的算法时我强烈建议在fit循环里加入可视化代码把每一轮迭代的质心和数据点分布画出来。这能让你直观地看到质心是如何“追逐”数据点并最终稳定的对理解算法有巨大帮助。这也是使用matplotlib动态绘图的好机会。4. 数据集准备、实验与结果可视化算法写好了我们需要数据来测试它。这里介绍两种常用的数据来源模拟数据和真实数据。4.1 使用模拟数据集进行验证scikit-learn的make_blobs函数可以生成适合聚类算法的数据集它会产生几个高斯分布的“斑点”。# 生成模拟数据 def generate_sample_data(n_samples300, centers4, random_state42): 生成用于聚类的模拟数据。 :param n_samples: 总样本数。 :param centers: 簇的中心数量即真实的K。 :param random_state: 随机种子。 :return: 特征数据X以及真实标签仅用于评估算法本身不知道。 X, y_true make_blobs(n_samplesn_samples, centerscenters, cluster_std0.8, # 每个簇的标准差控制簇的紧密程度 random_staterandom_state) return X, y_true # 生成数据 X, y_true generate_sample_data(n_samples300, centers4) # 可视化原始数据 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.scatter(X[:, 0], X[:, 1], cy_true, s30, cmapviridis, edgecolorsk) plt.title(原始数据真实标签) plt.xlabel(特征 1) plt.ylabel(特征 2)这段代码生成了300个样本点它们实际上来自4个不同的分布centers4。我们用颜色y_true区分它们这是“上帝视角”的真实分类。我们的KMeans算法在训练时是看不到y_true的。4.2 运行自实现KMeans并可视化过程为了更生动我们修改一下fit方法让它能记录每一轮迭代的质心位置然后我们画一个动态过程图。这里为了简化我们写一个专门用于演示的拟合函数。def fit_and_plot(X, n_clusters4, max_iter10): 运行KMeans并绘制每次迭代的结果。 km MyKMeans(n_clustersn_clusters, max_itermax_iter, initk-means, random_state42) # 手动执行迭代并记录 centroids_history [] labels_history [] # 初始化 centroids km._initialize_centroids(X) if km.init k-means else X[np.random.choice(X.shape[0], n_clusters, replaceFalse)] centroids_history.append(centroids.copy()) for it in range(max_iter): # 分配 labels km._assign_clusters(X, centroids) labels_history.append(labels.copy()) # 更新 new_centroids km._update_centroids(X, labels) centroids_history.append(new_centroids.copy()) # 检查收敛简化版 if np.linalg.norm(new_centroids - centroids) 1e-4: print(f提前收敛于迭代 {it1}) break centroids new_centroids # 绘制迭代过程 fig, axes plt.subplots(2, (max_iter1)//2, figsize(15, 8)) axes axes.flatten() for i in range(len(centroids_history)-1): ax axes[i] ax.scatter(X[:, 0], X[:, 1], clabels_history[i], s30, cmapviridis, alpha0.6) ax.scatter(centroids_history[i][:, 0], centroids_history[i][:, 1], cred, s200, markerX, labelf质心-迭代{i}) # 用线连接新旧质心显示移动轨迹 for j in range(n_clusters): ax.plot([centroids_history[i][j, 0], centroids_history[i1][j, 0]], [centroids_history[i][j, 1], centroids_history[i1][j, 1]], r--, linewidth1) ax.set_title(f迭代 {i1}) ax.legend() for j in range(i1, len(axes)): axes[j].axis(off) plt.tight_layout() plt.show() return km # 运行并绘图 km_model fit_and_plot(X, n_clusters4, max_iter6)运行这段代码你会看到一系列子图展示了质心红色大叉如何一步步移动到簇的中心以及数据点的颜色所属簇如何随之变化。这个可视化是理解KMeans动态过程的无价之宝。4.3 与Scikit-learn官方实现对比为了验证我们代码的正确性最好的方法是用同样的数据和参数运行sklearn的KMeans对比结果。from sklearn.cluster import KMeans as SKLearnKMeans # 使用我们自实现的模型 my_km MyKMeans(n_clusters4, random_state42).fit(X) my_labels my_km.labels my_centroids my_km.centroids # 使用sklearn的模型 sk_km SKLearnKMeans(n_clusters4, initk-means, random_state42, n_initauto).fit(X) sk_labels sk_km.labels_ sk_centroids sk_km.cluster_centers_ # 比较质心位置 print(自实现质心\n, my_centroids) print(\nSklearn质心\n, sk_centroids) print(\n质心间平均差异, np.linalg.norm(my_centroids - sk_centroids, axis1).mean()) # 比较惯性距离平方和 print(\n自实现惯性, my_km.inertia_) print(Sklearn惯性, sk_km.inertia_) # 可视化对比结果 fig, axes plt.subplots(1, 3, figsize(15, 4)) # 真实分布 axes[0].scatter(X[:, 0], X[:, 1], cy_true, s30, cmapviridis, edgecolorsk) axes[0].set_title(真实分布) axes[0].set_xlabel(特征 1) axes[0].set_ylabel(特征 2) # 自实现结果 axes[1].scatter(X[:, 0], X[:, 1], cmy_labels, s30, cmapviridis, edgecolorsk) axes[1].scatter(my_centroids[:, 0], my_centroids[:, 1], cred, s200, markerX) axes[1].set_title(自实现 KMeans) axes[1].set_xlabel(特征 1) # Sklearn结果 axes[2].scatter(X[:, 0], X[:, 1], csk_labels, s30, cmapviridis, edgecolorsk) axes[2].scatter(sk_centroids[:, 0], sk_centroids[:, 1], cred, s200, markerX) axes[2].set_title(Sklearn KMeans) axes[2].set_xlabel(特征 1) plt.tight_layout() plt.show()如果我们的实现正确两个模型的质心位置应该非常接近惯性值也相差无几。由于KMeans对初始质心敏感即使都用K-Means随机性也不同结果可能在小数点后几位有细微差别但聚类的大格局应该是一致的。通过这个对比你能极大增强对自己代码的信心。4.4 尝试真实数据集鸢尾花数据集模拟数据太完美我们试试经典的真实数据集——鸢尾花数据集。它包含150个样本4个特征花萼和花瓣的长宽3个类别山鸢尾、变色鸢尾、维吉尼亚鸢尾。我们只用前两个特征以便可视化。from sklearn.datasets import load_iris # 加载数据 iris load_iris() X_iris iris.data[:, :2] # 只取前两个特征用于二维可视化 y_iris_true iris.target # 运行自实现KMeans假设我们不知道是3类先尝试分为3类 km_iris MyKMeans(n_clusters3, random_state42).fit(X_iris) labels_iris km_iris.labels # 可视化 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.scatter(X_iris[:, 0], X_iris[:, 1], cy_iris_true, s50, cmapviridis) plt.title(鸢尾花真实类别) plt.xlabel(花萼长度 (cm)) plt.ylabel(花萼宽度 (cm)) plt.subplot(1, 2, 2) plt.scatter(X_iris[:, 0], X_iris[:, 1], clabels_iris, s50, cmapviridis) plt.scatter(km_iris.centroids[:, 0], km_iris.centroids[:, 1], cred, s200, markerX, label质心) plt.title(KMeans聚类结果 (K3)) plt.xlabel(花萼长度 (cm)) plt.legend() plt.show()你会发现仅用两个特征KMeans的结果与真实类别已经有较高的吻合度但也存在一些错分的点。这引出了KMeans的两个关键问题1K值需要预先指定我们这里知道是3类但现实中往往不知道2特征选择和尺度对结果影响很大如果我们用了全部4个特征或者特征之间的量纲差异巨大结果可能不同。5. 深入探讨K值选择、局限性与优化自己实现了一遍之后你对KMeans的优缺点会有更深刻的认识。这里分享几个必须知道的进阶话题。5.1 如何确定最佳的K值KMeans最大的痛点就是需要人工指定K簇数。有几种常用的方法肘部法则绘制不同K值对应的惯性inertia_曲线。惯性会随着K增大而减小但减小的幅度会变缓。那个拐点像手肘一样对应的K值通常是一个好的选择。轮廓系数它结合了簇内的凝聚度和簇间的分离度。轮廓系数越接近1说明聚类效果越好。可以计算不同K值下的平均轮廓系数取最大的那个K。我们可以用自实现的模型来计算肘部法则曲线def plot_elbow_method(X, max_k10): 绘制肘部法则图以帮助选择K值。 inertias [] K_range range(1, max_k1) for k in K_range: km MyKMeans(n_clustersk, random_state42) km.fit(X) inertias.append(km.inertia_) plt.figure(figsize(8, 5)) plt.plot(K_range, inertias, bo-) plt.xlabel(簇的数量 K) plt.ylabel(惯性 (Inertia)) plt.title(肘部法则 (Elbow Method)) plt.xticks(K_range) plt.grid(True) plt.show() # 对模拟数据使用肘部法则 plot_elbow_method(X, max_k10)对于之前生成的4个簇的模拟数据你会在K4附近看到一个明显的拐点。而对于更复杂的数据拐点可能不明显这就需要结合轮廓系数和业务理解来综合判断。5.2 KMeans的主要局限性通过实践你应该能感受到KMeans的“脾气”需要预先指定K这是最常被诟病的一点。对初始值敏感虽然K-Means改善了但多次运行结果仍可能略有不同。一个实践技巧是多次运行比如10次选择惯性最小的那次结果作为最终模型。对异常值敏感质心是均值异常值会把它“拉偏”。假设簇是凸形且大小相近KMeans使用欧氏距离它倾向于发现球状或凸形的簇。对于流形、环形或不规则形状的簇效果会很差。特征尺度影响大如果特征A的范围是0-100特征B的范围是0-1那么距离计算会被特征A主导。因此在应用KMeans前对特征进行标准化如Z-score标准化是必不可少的预处理步骤。5.3 针对局限性的优化实践针对上述问题有一些常见的应对策略数据预处理from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) # 标准化使每个特征均值为0方差为1 km.fit(X_scaled) # 在标准化后的数据上拟合多次初始化在我们的MyKMeans类中可以很容易地添加一个n_init参数在fit方法中运行多次保留最佳结果。class MyKMeansEnhanced(MyKMeans): def __init__(self, n_clusters3, max_iter300, tol1e-4, initk-means, n_init10, random_stateNone): super().__init__(n_clusters, max_iter, tol, init, random_state) self.n_init n_init # 初始化次数 def fit(self, X): best_inertia float(inf) best_centroids None best_labels None for init_idx in range(self.n_init): # 调用父类的fit但每次用不同的随机种子 current_seed self.random_state init_idx if self.random_state is not None else None super().__init__(self.n_clusters, self.max_iter, self.tol, self.init, current_seed) super().fit(X) if self.inertia_ best_inertia: best_inertia self.inertia_ best_centroids self.centroids.copy() best_labels self.labels.copy() self.centroids best_centroids self.labels best_labels self.inertia_ best_inertia return self处理非凸簇如果数据形状复杂可能需要考虑其他算法如DBSCAN基于密度或谱聚类。6. 常见问题与调试技巧实录在自己实现和使用的过程中我遇到过不少坑。这里总结一下希望能帮你节省时间。6.1 空簇问题及其处理在_update_centroids方法中我们已经提到了空簇。除了随机重置另一种策略是选择距离当前所有质心最远的那个数据点作为新质心这有助于探索新的区域。你可以根据实际情况选择策略。在调试时如果发现某个簇的样本数突然变为0可以打印出当前迭代信息来检查。6.2 算法不收敛或震荡有时你会发现质心在两个位置之间来回跳无法稳定。这通常是因为数据中有重复点或非常近的点导致质心计算时在两个相同位置来回切换。检查数据去重。容忍度tol设置得太小而迭代次数max_iter不够。可以适当增大tol比如到1e-3或增加max_iter。K值设置不合理远大于或小于真实簇数。用肘部法则重新评估K。一个实用的调试方法是在fit循环中打印每一轮迭代的质心移动距离centroid_shift和惯性inertia_观察它们的变化趋势。正常情况下两者都应该单调递减并趋于平缓。6.3 结果不一致问题即使固定了random_state你的结果也可能和sklearn的结果在标签编号上不一致。比如你的算法把A簇标为0B簇标为1而sklearn可能把A簇标为2B簇标为0。这通常是正常的因为KMeans的簇标签本身没有语义只是索引。你需要关注的是聚类本身的划分是否一致。可以通过构建混淆矩阵需要知道真实标签或计算调整互信息等指标来评估这些指标与标签编号无关。6.4 性能优化技巧我们实现的版本为了清晰在有些地方用了Python循环如更新质心时遍历每个簇。对于大数据集这可能会成为瓶颈。完全向量化的更新方式更高效但代码稍复杂。一个折中的优化是在_update_centroids中可以使用numpy的bincount和np.add.at等高级操作来避免显式循环。不过对于学习和中等规模数据我们当前的实现已经足够清晰和高效。最后把完整的MyKMeans类、数据生成、实验对比和可视化代码整合到一个脚本里就是一个非常扎实的KMeans聚类算法实践项目。它不仅是一段可运行的代码更是一个理解无监督学习核心思想的窗口。当你下次需要做客户分群、图像颜色量化、文档主题发现时你会感谢自己曾经亲手实现过这个经典的算法。本文还有配套的精品资源点击获取