我们将理论付诸实践。您已经学习了无监督学习、聚类以及 K-Means 算法的运作方式。现在,我们将逐步演示如何将 K-Means 应用于一个简单数据集,以观察其实际效果。目的是在不预先知道数据点“真实”标签的情况下,仅依据数据点自身的位置对其进行分组。在入门示例中,我们通常采用一种方法:生成模拟数据。这很有用,因为我们可以创建已知存在明显分组的数据,从而更容易通过视觉检查 K-Means 在发现这些分组方面是否做得合理。我们将使用 Python,并配合 Scikit-learn(用于 K-Means 算法)和 Plotly(用于可视化)等常用库。准备工具首先,请确保您已安装所需的库。如果您在 Google Colab 或 Anaconda 等环境中工作,这些库可能已预装。否则,您通常可以使用 pip 进行安装:pip install scikit-learn numpy plotly本示例中,我们需要 numpy 进行数值运算(特别是生成数据),sklearn.cluster 用于 KMeans 算法,以及 plotly.graph_objects 用于创建适合网页的交互式图表。生成和可视化简单数据我们来创建一些二维数据,这些数据明显分为三个组或“斑点”。Scikit-learn 提供了一个方便的 make_blobs 函数,正是为此目的而设。import numpy as np import plotly.graph_objects as go from sklearn.datasets import make_blobs # 生成包含3个明显聚类的模拟数据 X, _ = make_blobs(n_samples=150, # 总点数 centers=3, # 要生成的聚类数量 cluster_std=0.8,# 聚类的标准差(分散程度) random_state=42)# 用于结果复现 # 我们忽略第二个输出 (y),即真实标签 # X 现在是一个 NumPy 数组,包含 150 行和 2 列(我们的特征) # 让我们在聚类之前显示原始数据 fig_raw = go.Figure(data=[go.Scatter( x=X[:, 0], y=X[:, 1], mode='markers', marker=dict(color='#495057', size=7, opacity=0.8) # 原始数据使用灰色 )]) fig_raw.update_layout( title='模拟数据点(聚类前)', xaxis_title='特征 1', yaxis_title='特征 2', width=600, height=450, plot_bgcolor='#f8f9fa' # 浅色背景 ) # 显示图表(在笔记本/网页环境中) # fig_raw.show()在应用 K-Means 之前,查看您的数据总是一个好习惯。这是由上述代码生成的图表:{"layout": {"title": "模拟数据点(聚类前)", "xaxis": {"title": "特征 1"}, "yaxis": {"title": "特征 2"}, "width": 600, "height": 450, "plot_bgcolor": "#f8f9fa"}, "data": [{"type": "scatter", "x": [9.40, -3.89, -2.17, 0.63, 3.09, 8.67, -4.03, -1.82, 8.68, 8.57, 2.33, -3.50, -3.61, 2.29, 9.27, -1.22, -4.28, 0.01, -1.36, 2.33, 8.59, 10.03, -2.82, -3.24, 9.71], "y": [0.32, -2.38, 8.17, 7.79, 7.05, -0.55, -1.10, 8.97, 0.71, -0.09, 8.61, -1.62, -2.41, 6.90, -0.35, 8.15, -2.23, 7.39, 7.78, 7.33, 0.39, 0.19, 7.26, -2.19, -0.47], "mode": "markers", "marker": {"color": "#495057", "size": 7, "opacity": 0.8}}]}在二维空间中绘制的模拟数据点。我们可以通过视觉方式识别出三个潜在的分组。如您所见,这些点形成了三个分离得相当好的分组。对于这种简单的二维数据,我们的眼睛可以很轻松地完成这项聚类任务。我们来看看 K-Means 能否重现这种分组。应用 K-Means现在我们将使用 Scikit-learn 的 KMeans 实现。我们需要告诉算法要寻找多少个聚类 ($K$)。由于我们生成的数据有 3 个中心,我们将 $K$ 设置为 3。from sklearn.cluster import KMeans # 初始化 K-Means 算法 # n_clusters 是最重要的参数:聚类数量 (K) # n_init='auto' 使用智能默认值,用于多次运行算法 # 使用不同的质心种子以提高结果。 # random_state 确保初始化的可复现性。 kmeans = KMeans(n_clusters=3, n_init='auto', random_state=42) # 将算法拟合到数据 X # K-Means 在此进行迭代:将点分配给聚类并更新质心。 kmeans.fit(X) # 拟合后,模型包含以下结果: # 1. 每个数据点的聚类分配: cluster_labels = kmeans.labels_ # 2. 最终聚类中心(质心)的坐标: centroids = kmeans.cluster_centers_ # print("分配给每个点的聚类标签:", cluster_labels) # print("最终质心的坐标:\n", centroids)fit() 方法在我们的数据 X 上运行 K-Means 算法。该算法迭代地将每个点分配给最近的质心,然后根据分配的点重新计算质心位置,直到质心稳定或达到最大迭代次数。结果存储在 kmeans 对象中:kmeans.labels_:一个数组,第 $i$ 个元素表示分配给 X 中第 $i$ 个数据点的聚类索引(在此示例中为 0、1 或 2)。kmeans.cluster_centers_:一个二维数组,包含每个聚类质心的最终坐标。K-Means 结果可视化现在,我们来可视化相同的数据点,但这次根据 K-Means 分配的聚类标签进行着色。我们还将绘制算法找到的最终质心。# 定义聚类的颜色 - 使用建议的调色板 cluster_colors = ['#4263eb', '#12b886', '#fd7e14'] # 靛蓝色、青色、橙色 centroid_color = '#f03e3e' # 质心使用红色 # 创建图表 fig_clustered = go.Figure() # 添加数据点,按聚类标签着色 for i in range(3): # 遍历聚类 0, 1, 2 points_in_cluster = X[cluster_labels == i] fig_clustered.add_trace(go.Scatter( x=points_in_cluster[:, 0], y=points_in_cluster[:, 1], mode='markers', marker=dict(color=cluster_colors[i], size=7, opacity=0.8), name=f'聚类 {i}' )) # 添加质心 fig_clustered.add_trace(go.Scatter( x=centroids[:, 0], y=centroids[:, 1], mode='markers', marker=dict(color=centroid_color, size=14, symbol='x', line=dict(width=3)), name='质心' )) fig_clustered.update_layout( title=f'K-Means 聚类结果 (K=3)', xaxis_title='特征 1', yaxis_title='特征 2', width=600, height=450, plot_bgcolor='#f8f9fa', legend_title_text='图例' ) # 显示图表 # fig_clustered.show()这是结果图表:{"layout": {"title": "K-Means 聚类结果 (K=3)", "xaxis_title": "特征 1", "yaxis_title": "特征 2", "width": 600, "height": 450, "plot_bgcolor": "#f8f9fa", "legend_title_text": "图例"}, "data": [{"type": "scatter", "x": [-3.8944466, -4.0307817, -3.49879463, -3.60856438, -4.28364986, -2.82413277, -3.24349636, -3.86908011, -3.89519813, -3.61645308, -3.09745833, -3.09142537, -4.11892455, -3.47499921, -3.41549603, -3.61725122, -3.38299587, -4.25895783, -3.05677876, -2.99166233, -3.53489394, -3.18922301, -3.05746663, -2.97908147, -4.00345846, -4.43579128, -3.88682853, -4.23528217, -3.01684953, -4.08703417, -3.11201568, -3.09844608, -3.94129376, -3.2776345, -3.68351318, -3.20806829, -3.19362707, -2.67867522, -2.58810129, -3.59801299, -3.09005407, -2.80153218, -3.25816333], "y": [-2.38218416, -1.10321427, -1.61918697, -2.40769042, -2.2290819, 7.25888311, -2.1941847, -1.59177754, -1.60287089, -1.60081525, -1.56254636, -1.63656999, -1.46774876, -1.65414337, -2.44838893, -1.45309417, -2.42025743, -2.11475232, -2.07976469, -2.68137258, -0.8474635, -1.10928646, -2.00044627, -2.53353386, -1.6835102, -1.34812434, -1.60279546, -1.44304745, -2.57820237, -1.17392271, -1.49738819, -2.62645786, -2.30683014, -2.61503097, -1.04255853, -0.89907947, -1.37764128, 8.34059978, 7.76004224, -1.71469683, -1.28558574, 8.61409983, -1.90397304], "mode": "markers", "marker": {"color": "#4263eb", "size": 7, "opacity": 0.8}, "name": "聚类 0"}, {"type": "scatter", "x": [-2.17399939, 0.63447058, 3.0902331, -1.82419002, 2.32643218, 2.29491571, -1.21909073, 0.0112588, -1.36399585, 2.33003117, -2.27073282, 2.51370938, 2.93376674, -2.56506782, 1.85706746, 2.20446538, 0.94870871, -2.22214035, 2.9651487, 2.62396722, 2.68645927, 2.21014116, 1.62583857, 1.23435846, -2.7734411, 2.41259211, -2.34841319, -2.57907391, 2.89968801, -2.34927089, 1.68502645, 2.35518894, -1.91377378, 1.3525249, 2.72385102, 1.45896167, 1.98292497, 2.60171305, 2.06567807, -1.27533232, -1.8886289, 2.48017873, 2.62140818, 2.91030311, -2.1229853, 1.68181629, -1.68185409, 2.45147835, -2.57485385, 1.97969605, -1.39757857, 2.47611934, 2.85306097, 1.55067484, 1.56174678, 2.9917874, 1.32281877], "y": [8.16659979, 7.79143032, 7.05315316, 8.96916688, 8.60753479, 6.9011889, 8.14757385, 7.39361353, 7.77945849, 7.33166537, 9.12352141, 7.09137364, 7.36820713, 8.5053366, 6.57942344, 8.1834023, 7.23553302, 7.32431041, 8.33589429, 7.95017784, 7.99216678, 7.58311015, 7.73518309, 7.20993471, 8.14907471, 7.58580846, 9.09953083, 7.87177664, 6.61767197, 8.60353306, 6.82982971, 8.43919954, 9.18548535, 7.73797418, 7.14496464, 8.35835846, 7.07295833, 8.27544208, 6.61151908, 7.48901314, 8.62308333, 7.67681382, 8.38021859, 7.7720913, 8.52102629, 7.69048475, 7.98726332, 8.06523303, 7.74734064, 7.01837241, 7.89803359, 8.70073113, 7.76503948, 8.05351342, 7.22386065, 7.76976782, 7.17659323], "mode": "markers", "marker": {"color": "#12b886", "size": 7, "opacity": 0.8}, "name": "聚类 1"}, {"type": "scatter", "x": [9.40487926, 8.67028461, 8.68490888, 8.56820617, 9.26813307, 8.58921288, 10.02922413, 9.71135036, 8.06968037, 8.27200317, 7.91291223, 9.39663251, 8.18409341, 8.42895401, 9.28596881, 9.78631698, 8.35558648, 9.28499641, 8.76873352, 9.50358321, 8.64980199, 10.20133189, 8.89124388, 10.34603865, 8.94312171, 8.56998805, 8.08909871, 8.6176526, 9.8484214, 9.30611344, 8.98809746, 8.39494944, 10.13180203, 8.82484509, 9.69327317, 9.04357012, 7.98905271, 8.4383966, 9.07676821, 9.25474465, 9.32199849, 9.25977058, 8.20591249, 8.69809633, 10.2130389, 8.73647584, 9.72528665, 8.6998064, 8.99835309], "y": [0.32379694, -0.54950377, 0.71479233, -0.08938143, -0.34905046, 0.38512236, 0.18751794, -0.46837488, 0.88659018, 0.62688641, 0.7301589, -0.40308784, 0.92289687, -0.1006295, 0.20755535, 0.62850943, 0.13807953, 0.7287545, 0.86400827, 0.02840174, 1.35760308, -0.66630451, 0.8325716, -0.2716906, 0.14767768, -0.31182744, -0.30212011, 0.70151865, 0.10867936, -0.36474129, 0.21541716, 0.23526112, 0.07945874, -0.3848356, 0.34577225, 1.06134237, 1.45636823, 0.61506872, 0.38861725, -0.45738783, 1.38896482, -0.27467057, 0.75772998, 0.08395616, -0.24050672, 0.23092453, 0.82060666, 0.15193608, -0.28945617], "mode": "markers", "marker": {"color": "#fd7e14", "size": 7, "opacity": 0.8}, "name": "聚类 2"}, {"type": "scatter", "x": [-3.51986895, -0.01908098, 8.98844164], "y": [-1.70494843, 7.81773901, 0.20707911], "mode": "markers", "marker": {"color": "#f03e3e", "size": 14, "symbol": "x", "line": {"width": 3}}, "name": "质心"}]}相同的数据点,现在根据 K-Means 算法($K=3$)分配的聚类进行了着色。红色“x”标记表示聚类质心的最终位置。结果分析将 K-Means 结果图与原始数据的初始图进行比较。您会看到 K-Means 成功识别出了我们模拟数据中存在的三个明显分组。每种颜色代表算法发现的一个聚类,红色“x”标记显示了属于该聚类的所有点的中心(平均位置)。在这种简单情况下,当聚类分离良好且大致呈球形时,K-Means 表现非常出色。如果选择不同的 $K$ 值会怎样?还记得关于 $K$ 值选择的讨论吗?让我们简要思考一下,如果我们指示 K-Means 在此数据中寻找,比如 $K=2$ 个聚类,会发生什么。算法仍会运行,但它将被迫将三个可见的分组划分为仅两个聚类。通常,它可能会合并原始分组中的两个,或者将一个分组分散到两个结果聚类中,具体取决于初始质心的放置。同样,选择 $K=4$ 将迫使算法将一个或多个自然分组拆分成更小、可能意义较小的聚类。这项练习强调,尽管 K-Means 在数据划分方面很有效,但 $K$ 值的选择会显著影响结果及其解读。小结在本实践部分中,您已将 K-Means 算法应用于一个简单且直观可见的数据集。您了解了如何:使用 make_blobs 生成适合聚类实践的模拟数据。可视化原始数据以理解其结构。初始化并拟合 Scikit-learn 中的 KMeans 模型,指定所需的聚类数量 ($K$)。从拟合的模型中提取聚类分配 (labels_) 和质心位置 (cluster_centers_)。通过根据分配的聚类对数据点进行着色,并绘制最终质心来可视化结果。"这个动手示例显示了使用 K-Means 在无标签数据中寻找分组的核心过程。尽管数据通常更为复杂且维度更高,但基本步骤保持不变。您现在具备了理解 K-Means 工作原理以及如何使用常用工具实现它的实践基础。"