在做数据挖掘项目时,拿到数据,首先要分析问题和观察数据,我在“观察数据”这一阶段主要是看看有没有直观的数据规律以及异常点,对于那些特征维度较低的数据,比如仅仅2维,我选择画散点图观察,高纬度数据可以用t-SNE降维后再画图。除了可视化,聚类算法也是找到异常点的常用手段之一。

这篇博客介绍一种最基本的聚类算法:K-Means。

算法描述与EM

聚类算法的作用是将数据集自动划分为多个不同的集合。 K-Means算法是典型的无监督算法,对于数据集\(D\), 我们设定参数k(k个集合),她就能自动地学习出k个类别,简单又神奇。那么K-Means是如何学习到这k个集合呢?专业一点说就是,K-Means的目标函数是啥?如何优化她的目标函数?先回答第一个问题,下面就是K-Means的目标函数:

物理意义:我们希望找到的k个集合,每个集合内的点都距离此集合内平均值点最近(每个集合中的点都紧密团结在一起)。

知道了目标函数,下一步就要考虑如何求解,最常见的求解方法是Lloyd算法,也就是使用EM算法,注意,Lloyd算法得到的结果是局部极小值而不是全局最小值哦。

EM算法求解步骤很简单,总共分三步:

  • 将所有数据点都分配到某个集合 -> 计算每个点到集合平均值点的欧氏距离
  • 计算每个集合内点的平均值 -> 计算每个特征的平均值
  • 重复上面两步直到局部收敛 -> 前后两次迭代的集合结果相同

上面的三步概括起来就是 E->M->E->M->………..

import numpy as np
from collections import defaultdict
from random import uniform

class KMeans(object):
    """
    kmeans 聚类算算
    """
    def __init__(self):
        pass

    def cluster(self, X, k, initializer="random"):
        self.X = X
        self.k = k
        self.centers = defaultdict(list)
        self.assignment = np.zeros(self.X.shape[0]) #记录每个样本属于哪个中心点
        if initializer == "random":
            self._initialize() #初始化得到k个点
        elif initializer == "kmeans++":
            self._carefulSeed()
        else:
            raise ValueError('initializer error!')
        self._assign() #将数据集中每个样本分配给最近的中心点
        self.next_centers = None
        while self.centers != self.next_centers:
            self.next_centers = self.centers
            self._getcenters()
            self._assign()
        return self.assignment

    def _carefulSeed(self):
        """
        kmeans++
        1、从输入的数据点集合(要求有k个聚类)中随机选择一个点作为第一个聚类中心
        2、对于数据集中的每一个点x,计算它与最近聚类中心(指已选择的聚类中心)的距离D(x)
        3、选择一个新的数据点作为新的聚类中心,选择的原则是:D(x)较大的点,被选取作为聚类中心的概率较大
        4、重复2和3直到k个聚类中心被选出来
        """
        from random import randint
        index = randint(0, self.X.shape[0]-1)
        self.centers[0] = self.X[index]
        indexs = {index:1} #记录中心点是X中的第几个点(index), 以避免选择相同的点作为类中心
        for i in range(self.k)[1:]:



    def _initialize(self):
        """
        初始化,即初始化k个中心点和每个样本属于哪个中心点
        这k个样本点是随机产生的
        """
        feature_min_max = defaultdict(list) #保存每个特征值的最小值和最大值
        feature_dimensions = self.X.shape[1]
        get_min_max = lambda x, y: (x,y) if x<y else (y,x)
        for i in xrange(feature_dimensions):
            i_min, i_max = get_min_max(self.X[0][i], self.X[1][i])
            for j in range(self.X.shape[0])[2👎2]:
                tmp_min, tmp_max = get_min_max(self.X[j][i], self.X[j+1][i])#self.X[j+1][i], self.X[j][i] if self.X[j][i] > self.X[j+1][i] else self.X[j][i], self.X[j+1][i]
                if tmp_min < i_min:
                    i_min = tmp_min
                if tmp_max > i_max:
                    i_max = tmp_max
            tmp_min, tmp_max = get_min_max(self.X[-1][i], self.X[-2][i])#self.X[-1][i], self.X[-2][i] if self.X[-2][i] > self.X[-1][i] else self.X[-2][i], self.X[-1][i]
            i_min = tmp_min if tmp_min < i_min else i_min
            i_max = tmp_max if tmp_max > i_max else i_max
            feature_min_max[i] = [i_min, i_max]
        for i in xrange(self.k):
            this_k = []
            for j in xrange(feature_dimensions):
                value = uniform(feature_min_max[j][0], feature_min_max[j][1])
                this_k.append(value)
            self.centers[i] = this_k

    def _distance(self, point1, point2):
        """
        计算点point1 和 point2 之间的欧氏距离
        """
        dd = 0
        for i in xrange(len(point1)):
            dd += (point1[i] - point2[i]) ** 2
        return np.sqrt(dd)

    def _assign(self):

        feature_dimensions = self.X.shape[1]

        for i in xrange(self.X.shape[0]):
            min_distance = float("inf")
            current_assignment = self.k
            for j in xrange(self.k):
                tmp_d = self._distance(self.X[i], self.centers[j])
                if tmp_d < min_distance:
                    min_distance = tmp_d
                    current_assignment = j
            self.assignment[i] = current_assignment

    def _getcenters(self):
        """
        计算每个中心点的平均值,作为新的中心点
        """
        cluster_numbers = defaultdict(int) #记录每个分类的样本数目
        cluster_sum = np.zeros((self.k, len(self.X[0]))) #记录每个分类的所有样本相加之和
        for index, center in enumerate(self.assignment):
            cluster_numbers[center] = cluster_numbers.get(center, 0) + 1
            cluster_sum[center] += self.X[index]
        for i in xrange(self.k):
            self.centers[i] = list(cluster_sum[i]/float(cluster_numbers[i]))

调包篇

还是以调用sklearn为例,sklearn.cluster.KMeans对应于k-means算法,此外还有sklearn.cluster.MiniBatchKMeans, 顾名思义,就是使用批梯度下降算法优化目标函数,收敛速度会比KMeans快一点,但是结果可能要稍差。

KMeans

MiniBatchKMeans