K-Means 算法

1, 聚类

前面接触的算法, 都是 监督学习 , 即训练数据中自变量(特征)和因变量(结果)都是已知的, 用含有结果的训练集建立模型, 然后对未知结果的数据进行预测

聚类属于 无监督学习 , 训练数据中没有”已知结果的监督”. 聚类的目的, 就是通过已知样本数据的特征, 将数据划分为若干个类别, 每个类别成一个类簇, 使得同一个簇内的数据相似度越大, “物以类聚”, 不同簇之间的数据相似度越小, 聚类效果越好

聚类的样本相似度根据距离来度量

2, K-Means

即 K 均值算法, 是常见的聚类算法, 该算法将数据集分为 K 个簇, 每个簇使用簇内所有样本的均值来表示, 该均值称为”质心”

K-Means 算法的目标, 就是选择适合的质心, 使得每个簇内, 样本点距质心的距离尽可能的小, 从而保证簇内样本有较高相似度

算法实现步骤:

a, 从样本中选择 K 个点作为初始质心
b, 计算每个样本点到各个质心的距离, 将样本点划分到距离最近的质心所对应的簇中
c, 计算每个簇内所有样本的均值, 使用该均值作为新的质心
d, 重复 b 和 c, 重复一定次数质心一般会趋于稳定, 如果达到以下条件, 重复结束:
– 质心位置变化小于指定的阈值
– 达到最迭代环次数

对于算法的实现步骤, 我们有几个重要的疑问:

– 1.怎么评价质心是否达到了最佳位置?
– 2.初始质心随机选, 还是选在哪里?
– 3. K 值怎么定?

3, 算法优化目标

样本的相似度是根据距离来度量的, 一般使用簇内 误差平方和 (within-cluster SSE 簇惯性) 来作为优化算法的目标函数, 距离常用欧氏距离, 优化目标就是使 SSE 最小化:

$$S S E=\sum_{i=1}^{k} \sum_{j=1}^{m_{i}}\left(\left|x_{j}-\mu_{i}\right|^{2}\right)$$

k: 族的数量

$m_{i}$: 第 i 个簇含有的样本数量

${\mu}_{i}$: 第 i 个族的质心

$\left|x_{j}-\mu_{i}\right|$: 第 i 个族中,每个样本 $x_{j}$ 与质心 $\mu_{i}$ 的距离

同一个数据集, 相同的簇数, SSE 越小, 通常质心位置更佳, 算法模型更好

4, 初始质心的影响

初始质心可以随机选择, 但由于算法是通过迭代初始质心一步步实现, 初始质心的位置受随机性影响, 算法训练的最终结果也会受到影响

import numpy as np  
from sklearn.cluster import KMeans  
from sklearn.datasets import make_blobs  
import matplotlib.pyplot as plt  

plt.rcParams['font.family'] = 'Microsoft YaHei'   
plt.rcParams['font.size'] = 12   
plt.rcParams['axes.unicode_minus'] = False   

'''  
生成数据:  
n_samples: 样本数量  
n_features: 特征数  
centers: 聚类中心  
cluster_std: 簇的标准差, 可以统一指定, 也分别指定  
'''  
centers = [[1, 1], [5, 2], [2, 5]]  
x, y = make_blobs(n_samples=90,  
                  n_features=2,  
                  centers=centers,  
                  cluster_std=[2.2, 2.5, 2],  
                  random_state=0)  
# x 是特征, y 是类别标签  

# 绘制原始数据  
plt.figure(figsize=(12,8))  
plt.subplot(221)  
colors = np.array(['Coral', 'SeaGreen', 'RoyalBlue'])  
plt.scatter(x[:, 0], x[:, 1], c=colors[y], marker='.', label='原始数据')  
plt.title('原始数据')  

# 定义绘制聚类结果的函数  
def plot_cluster(model, train, test=None):  
    global colors  # 使用上面的颜色  
    cc = model.cluster_centers_ # 获取质心  
    label = model.labels_ # 获取聚类结果的标签  
    # 绘制质心  
    plt.scatter(cc[:, 0], # 质心的 x 坐标  
                cc[:, 1], # 质心的 y 坐标  
                marker='*',  
                s=150,  
                c=colors)  
    # 绘制训练集  
    plt.scatter(train[:, 0], train[:, 1], marker='.', c=colors[label])  
    # 绘制测试集  
    if test is not None:  
        y_hat = model.predict(test)  
        plt.scatter(test[:, 0], test[:, 1], marker='+',  
                    s=150, c=colors[y_hat])          
    # 标题  
    plt.title(f'SSE:{model.inertia_:.1f} 迭代次数:{model.n_iter_}')  

# 测试集  
test = np.array([[6, 5]])      
# 绘制不同初始质心的聚类结果  
seed = [1, 10, 100]  
for i in range(2, 5):  
    plt.subplot(2, 2, i)  
    kmeans = KMeans(n_clusters=3, # 簇数  
                    init='random', # 初始化方式  
                    n_init=1, # 初始化质心组数  
                    random_state=seed[i-2])  
    kmeans.fit(x)  
    plot_cluster(kmeans, x)  
    # 测试结果  
    plot_cluster(kmeans, x, test)  

png

从上图可以看出受初始化质心的影响, 聚类效果(SSE) 与 收敛速度(迭代次数) 会不同, 也即是可能会收敛到局部最小, 而不是整体最优; 同时, 也可以看出 SSE 越小, 整体结果越优, 越接近原始数据

5, K-Means++ 优化

针对上述初始化质心造成的问题, 设置初始化多组质心可以得到缓解, 但通常限于聚类簇数较少的情况, 如果簇数较多, 可能就不会有效

于是有了 K-Means++, 选择初始化质心时, 不在随机选, 而是按下述步骤进行选择:

– 1, 从训练数据中随机选择一个样本点, 作为初始质心
– 2, 对任意一个非质心样本点 $x^{(i)}$, 计算 $x^{(i)}$ 与现有最近质心的距离 $D\left(x^{(i)}\right)$
– 3, 根据概率 $\frac{D\left(x^{(i)}\right)^{2}}{\sum_{j=1}^{m}D\left(x^{(j)}\right)^{2}}$ 最大, 来选择最远的一个样本点 $x^{(i)}$ 作为质心, m 为非质心样本点数量
– 4, 重复 2 和 3, 直到选择了 K 个质心为止

做了优化之后, 保证了初始质心不会集中, 而是分散在数据集中

下面试试 K-Means++ 的聚类效果:

kmeans = KMeans(n_clusters=3, init='k-means++', n_init=1)  
kmeans.fit(x)  
plot_cluster(kmeans, x)  

png

6, 确定 K 值

K 是超参数, 需要预先人为指定

有时需要按照建模的需求和目的来选择聚类的个数, 但是 K 值选择不当, 聚类效果可能不佳. 例如实际 3 类, K 选了 10, 或者 K 无限制, 取值和样本点个数一样, 最后每个点一个类, SEE 为 0, 但是聚类已经毫无意义

如果不是硬性要求 K 的取值, 怎么确定最佳的 K 值呢? 一个比较好的方法就是 肘部法则 :

SEE 需要越小越好, K 又不能取太大, 我们可以看看他们之间的关系:

# 设置列表储存 SSE  
sse = []  
# K 值从 1~9 变化  
scope = range(1, 10)  
for k in scope:  
    kmeans = KMeans(n_clusters=k)  
    kmeans.fit(x)  
    sse.append(kmeans.inertia_)  

plt.xticks(scope)  
plt.plot(scope, sse, marker='o')  
plt.show()  

png

从上图可以看出, K 增加, SSE 减小, 但当 K > 3 时, K 再增加, SSE 减小变得缓慢, 所以 K 选择 3, 实际情况也是 3

6, Mini Batch K-Means

K-Means 每次迭代都会使用所有数据参与运算, 当数据集较大时, 会比较耗时. Mini Batch K-Means (小批量 K-Means) 算法每次迭代使用小批量样本训练, 逐批次累计的方式进行计算, 从而大大减少计算时间. 效果上, 通常只是略差于 K-Means

Mini Batch K-Means 算法实现步骤:

a, 从数据集中随机选择部分数据, 使用 K-Means 算法在这部分数据上聚类, 获取质心
b, 再从数据集中随机选择部分数据, 分别分配给最近的质心
c, 每个簇根据现有的数据集更新质心
d, 重复 b 和 c, 直到质心变化小于指定阈值或达到最大迭代次数

下面比较一下两个算法:

import time  
import pandas as pd  
from sklearn.cluster import MiniBatchKMeans  
from sklearn.metrics.pairwise import pairwise_distances_argmin  

import warnings
warnings.filterwarnings('ignore')

# 生成数据  
centers = [[1, 1], [400, 100], [100, 400]]  
x, y = make_blobs(n_samples=8000, n_features=2, centers=centers,  
                  cluster_std=120, random_state=0)  

# 定义函数, 用于计算模型训练时间  
def elapsed_time(model, data):  
    start = time.time()  
    model.fit(data)  
    end = time.time()  
    return end - start  

n_clusters = len(centers)  
kmeans = KMeans(n_clusters=n_clusters)  
mbk = MiniBatchKMeans(n_clusters=n_clusters,  
                      batch_size=200, # 小批量的大小  
                     n_init=10 # 和 KMeans 统一为 10  
                     )  
kmeans_time = elapsed_time(kmeans, x)  
mbk_time = elapsed_time(mbk, x)  

print('K-Means耗时:', kmeans_time)  
print('Mini Batch K-Means耗时:', mbk_time)  

# 绘制聚类效果  
plt.figure(figsize=(12, 5))  
model = [kmeans, mbk]  
for i, m in enumerate(model, start=1):  
    plt.subplot(1, 2, i)  
    plot_cluster(m, x)  
K-Means耗时: 0.051812171936035156
Mini Batch K-Means耗时: 0.04886937141418457

png

可见, 聚类耗时 K-Means 更多, 如果数据量很大, 耗时会更明显, 而聚类效果基本一样. 但发现颜色对不上, 这是因为质心的随机性, 聚类之后质心虽然最终落在相同的位置, 但是顺序不一致, 从而聚类的结果标签不一致, 即使是同一个算法, 运行几次, 标签结果也会不一致

我们将相同簇用相同的颜色绘制:

plt.figure(figsize=(12, 5))  
# 定义列表, 用来保存两个模型预测结果  
y_hat_list = []  
for i, m in enumerate(model, start=1):  
    plt.subplot(1, 2, i)  
    y_hat = m.predict(x)  
    if m == mbk:  
        '''  
        因为输出的质心顺序就是训练结果标签的顺序  
        故可以按 mbk 训练的质心, 去找 kmeans 训练的相同簇的质心  

        pairwise_distances_argmin(x, y) 解释:  
        依次取出数组 X 中的元素 x,   
        计算找到数组 Y 中与 x 距离最近的元素 y 的索引,   
        返回索引构成的数组  
        '''  
        # 将两者相同簇的质心一一对应并按 mbk 质心的顺序封装成字典  
        ar = pairwise_distances_argmin(  
        mbk.cluster_centers_, kmeans.cluster_centers_)  
        dict_ = dict(enumerate(ar))  
        # 用 mbk 的训练结果标签 y_hat 就可以寻找到对应的 kmeans 的质心  
        y_hat = pd.Series(y_hat).map(dict_).values  
    # 将预测结果加入到列表中  
    y_hat_list.append(y_hat)  

    plt.scatter(x[:, 0], x[:, 1], c=colors[y_hat], marker='.')  

png

比较两个算法聚类结果的差异:

same = y_hat_list[0] == y_hat_list[1]  
diff = y_hat_list[0] != y_hat_list[1]  
plt.scatter(x[same, 0], x[same, 1], c='g', marker='.', label='预测相同')  
plt.scatter(x[diff, 0], x[diff, 1], c='r', marker='.', label='预测不同')  
plt.legend()  
print('相同数量:', x[same].shape[0])  
print('不同数量:', x[diff].shape[0])  
相同数量: 7967
不同数量: 33

png

两个算法聚类结果只有 33 个样本点不同