如何通过GridSearchCV获取KMeans的inertia_值与最优聚类数
问题背景
作为机器学习初学者,基于SKLearn内置Iris数据集开展KMeans聚类实验,待解决两个核心问题:
- 使用GridSearchCV调参时,如何获取KMeans的
inertia_值以及最优聚类数 - GridSearchCV是否适用于KMeans的聚类数调优场景
已验证可运行的手动实现方案
目前两种手动遍历的实现均可正常运行,可通过肘部法得到正确聚类数,但遍历效率偏低:
方法1:单模型直接遍历
X = data.data # 特征标准化 scaler = StandardScaler() scaled_X = scaler.fit_transform(X) inertias = [] for i in range(1,20): model = KMeans(n_clusters=i, random_state=42) model.fit(scaled_X) inertias.append(model.inertia_) inertias
方法2:Pipeline封装遍历
inertias = [] for i in range(1,20): pipe = Pipeline([ ('sc', StandardScaler()), ('kmeans', KMeans(n_clusters=i, random_state=42)) ], verbose=True) pipe.fit(scaled_X) inertias.append(pipe.named_steps['kmeans'].inertia_) inertias
以上两种方法绘制inertia折线图后,均可通过肘部法确定最优聚类数为3,与Iris数据集实际类别数一致。
GridSearchCV实现的异常现象
尝试用GridSearchCV复现上述逻辑时,返回结果完全不符合预期,实现代码如下:
pipe = Pipeline([ ('sc', StandardScaler()), ('kmeans', KMeans(random_state=42)) ], verbose=True) param_grid = { 'kmeans__n_clusters':range(1,20) } grid = GridSearchCV(pipe, param_grid=param_grid, verbose=3) grid.fit(scaled_X) # 返回结果异常 grid.best_params_ # {'kmeans__n_clusters': 19} grid.score(scaled_X) # -26.379283976769145
存在三个核心问题:
grid.best_params_返回的最优n_clusters为19,和肘部法结论完全矛盾grid.score()返回值和手动遍历得到的inertia变化规律不匹配- 无法直接通过类似
grid.inertia_的属性获取所有参数组合对应的inertia值,也无法直接得到肘部法对应的正确最优聚类数3
问题原因与解决方法
为什么GridSearchCV默认返回n_clusters=19
GridSearchCV的核心选型逻辑是交叉验证下评分最大化。KMeans默认的
score()方法返回值为负inertia(即score = -inertia),而inertia的固有特性是:聚类数越多,样本到所属簇心的距离和越小,inertia越低,对应的score就越高。在1-19的参数范围内,聚类数取19时score达到最大值,因此GridSearchCV默认返回19是逻辑必然,不是代码bug。
GridSearchCV是否适合调KMeans聚类数
不适合直接用默认配置调聚类数:
- 聚类数选择属于无监督场景下的偏差-方差权衡,不存在带标签的验证目标支撑交叉验证打分最大化的选型逻辑
- 肘部法、轮廓系数法等聚类数选型方法,都是基于聚类结果本身的分布特性判断,和GridSearchCV默认的预测精度导向的交叉验证逻辑不匹配
- GridSearchCV更适合调优KMeans的
init、n_init、max_iter这类不改变聚类数的超参数,硬套它来选聚类数属于舍近求远。
如何通过GridSearchCV获取所有聚类数对应的inertia值
如果一定要用GridSearchCV记录不同k对应的inertia,需要做两个配置修改:一是自定义评分器提取每个训练完成的模型的inertia,二是关闭交叉验证的数据集拆分(inertia是训练集上的统计值,拆分不同折计算的inertia没有可比性),参考代码如下:
import numpy as np # 定义字典存储每个k对应的inertia inertia_records = {} def get_inertia_score(estimator, X): k_val = estimator.named_steps['kmeans'].n_clusters inertia_val = estimator.named_steps['kmeans'].inertia_ inertia_records[k_val] = inertia_val # 返回负inertia符合GridSearchCV最大化评分的逻辑 return -inertia_val pipe = Pipeline([ ('sc', StandardScaler()), ('kmeans', KMeans(random_state=42)) ]) param_grid = { 'kmeans__n_clusters':range(1,20) } # cv配置为全量数据训练,不做折拆分 grid = GridSearchCV( pipe, param_grid=param_grid, scoring=get_inertia_score, cv=[(np.arange(len(scaled_X)), np.arange(len(scaled_X)))], verbose=3 ) grid.fit(scaled_X)
代码运行完成后,inertia_records字典中就存储了1-19每个聚类数对应的inertia值,和手动遍历得到的结果完全一致。
如何得到正确的最优聚类数
拿到所有聚类数对应的inertia后,依然需要通过肘部法、轮廓系数法等无监督聚类选型规则判断最优k,不能直接使用grid.best_params_的返回值——只要评分规则和inertia负相关,GridSearchCV永远会选参数范围内最大的聚类数,这是KMeans目标函数的固有特性,和工具本身无关。
内容的提问来源于stack exchange,提问作者Linus

