机器学习之 简单手写KMeans算法

2,972 阅读4分钟

算法简介

聚类是针对特定样本,根据他们特征的相似度或者距离,将其归并到若干个“类”或“簇”。

这篇文章简单介绍KMeans聚类算法的过程,并仿sklean,实现一个简化版的的KMeans算法。

算法过程图示

为了演示过程,通过图例来更直观的感受一下KMeans的计算过程。

首先这里有8个点。我们要把这8个点分为2类。

假设我们要分为2类。那么我们先假设2个中心点是 (1,1)(2,1)(1,1) (2,1)。如下图:

根据2个中心点的中垂线。把点分为2类,线左边的是一类,右边的是一类:

重新计算2类的中心点,如图:

根据2个中心点,再次把所有点分为2类:

再次计算中心点:

如此,发现中心点已经不再变化了,是不是很简单呢。

整个过程就不断的重复2件事情:

(1)根据中心点,计算所有点的分类

(2)计算每个类的新的中心点

直到收敛。

这篇文章只介绍通俗易懂的过程,和自己实现的代码,至于算法模型和算法步骤,大家可以自行阅读书籍,或者查看其他博客。推荐李航博士的《统计学习方法》。

自己实现KMeans算法

本文按照上述代码,仿sklean,实现了一个简化版的KMeans算法, 可保存为KMeans.py 并引用:

import numpy as np
from math import sqrt
from collections import Counter
from sklearn.metrics import accuracy_score
import random

class KMeans:
    def __init__(self, n_clusters=3, random_state=0):
        assert n_clusters >=1, " must be valid"
        self._n_clusters = n_clusters
        self._random_state = random_state
        self._X = None
        self._center = None
        self.cluster_centers_ = None
        
    def distance(self, M, N):
        return (np.sum((M - N) ** 2, axis = 1))** 0.5
    
    def _generate_labels(self, center, X):
        return np.array([np.argmin(self.distance(center, item)) for item in X])

    def _generate_centers(self, labels, X):
        return np.array([np.average(X[labels == i], axis=0) for i in np.arange(self._n_clusters)])

    def fit_predict(self, X):
        k = self._n_clusters
        
        # 设置随机数
        if self._random_state:
            random.seed(self._random_state)
        
        # 生成随机中心点的索引
        center_index = [random.randint(0, X.shape[0]) for i in np.arange(k)]
        
        center = X[center_index]

        # print('init center: ', center)

        n_iters = 1e3
        while n_iters > 0:
            
            
            # 记录上一个迭代的中心点坐标
            last_center = center

            # 根据上一批中心点,计算各个点所属的类
            labels = self._generate_labels(last_center, X)
            self.labels_ = labels

            # 新的中心点坐标
            center = self._generate_centers(labels, X)

            # print('n center: ', center)

            # 暴露给外头的参数
            # 中心点
            self.cluster_centers_ = center

            # 返回节点对应的分类 {0, 1, ..., n}
            

            # 如果新计算得到的中心点,和上一次计算得到的点相同,说明迭代已经稳定了。
            if (last_center == center).all():

                self.labels_ = self._generate_labels(center, X)
                break

            n_iters = n_iters - 1
        return self

我们来看看实际效果吧,这里我绘制了很密集的点阵。来观察分类是否有遗漏。

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


t1 = np.linspace(-1, 1.5, 100)
t2 = np.linspace(-1, 1.5, 100)

X = np.array([(x, y) for x in t1 for y in t2])

plt.figure(figsize=(10, 10))
clf = KMeans(n_clusters=6, random_state=None)
clf.fit_predict(X)

plt.scatter(X[:, 0], X[:, 1], c=clf.labels_)
center = clf.cluster_centers_
plt.scatter(center[:, 0], center[:, 1],marker="*",s=200)

实际效果如下:

注意:这里的色块其实是密集的点阵,而他们到各中心的分类也都是非常符合我们的预期的。

接下来,我们用该算法来实现以下鸢尾花数据集的分类吧。

首先,我们先导入数据集,看看实际点在图上的效果:

import numpy as np
import matplotlib.pyplot as plt
from KMeans import KMeans

from sklearn.datasets import load_iris
iris = load_iris()

X = iris.data
X = X[:, 2:]
y = iris.target

plt.figure(figsize=(12, 12))
plt.scatter(X[:, 0], X[:, 1])
plt.grid()
plt.xlim(0, 7)
plt.ylim(0, 7)
center = clf.cluster_centers_

样本分布如下:

接下来,我们调用自己写的KMeans算法,对样本进行分类,由于鸢尾花的数据集本来就是分为3类的,所以我们设置k=3。

import numpy as np
import matplotlib.pyplot as plt
from KMeans import KMeans

from sklearn.datasets import load_iris
iris = load_iris()

X = iris.data
X = X[:, 2:]
y = iris.target

clf = KMeans(n_clusters=3)
s = clf.fit_predict(X)
plt.figure(figsize=(12, 12))
plt.scatter(X[:, 0], X[:, 1], c=clf.labels_)
plt.grid()
plt.xlim(0, 7)
plt.ylim(0, 7)
center = clf.cluster_centers_
plt.scatter(center[:, 0], center[:, 1],marker="*",s=200)

结果如下:

非常符合预期。

总结

KMeans算法是迭代算法,不能保证全局最优,具体表现,和初始化的参数有关。初始中心点的选取,对结果会有一定影响。

k均值算法的k值需要预先指定,而在实际应用中,最有的类别数K是不确定的。解决这个问题的一个方法,就是尝试用不同的k值聚类,检查各个聚类的表现来推断最优的K值。一般来说,类别数变小时,平均直径会增加;类别数变大超过某个值后,平均直径变化不明显。这个值就是我们要找的k值。