K-means聚类(一)

我们的问题是:怎么给一堆数据分类?

首先,每一类都叫一个簇(Cluster),如何才能算作是同一类呢?

我们有K-means聚类,DBSCAN(Density-Based Spatial Clustering of Application with Noise),hierarchical clustering等等这些聚类算法,这些算法区分同一类的方式都不同,比如DBSCAN,它是以一定的密度进行传播,将传播到的点都聚成一类,在某些场景,比如簇与簇之间有明显分隔的,DBSCAN显然要比k-means好。

下面是用k-means和DBSCAN划分一个笑脸的不同效果。
k-means算法

k-means算法

DBSCAN算法

DBSCAN算法

 
不过对于均匀分布的数据,指定簇个数的k-means就要优于依靠密度发展的DBSCAN了。

k-means算法

DBSCAN算法
  这些动画的演示你可在https://www.naftaliharris.com/blog/visualizing-k-means-clustering/上做到

使用pycharm手写k-means聚类算法

思路:

①随机生成1000个二维坐标的样本点(我们就先拿这1000个样本点做实验了)

②指定划分成5类(这个你可以自己指定)

③确定收敛阈值为0.2(同样可以变,小一点为好)

④从1000个样本点中随机选出5个样本点,作为5个簇的中心

⑤每个中心为一个Cluster

⑥遍历1000个点,分别计算它们到5个中心点之间的距离,对每个点,我们得到5个距离,选择最小的,加入那个簇。

⑦第一次循环结束,我们分配好了1000个点的归属,得到了5个Cluster,对每个Cluster,计算里面包含的所有的点的均值,

也就是所有的x加起来除以总个数,所有的y加起来除以总个数,得到一个均值点。

⑧算出均值点到原来的中心点之间的距离,可以记为shift。对5个Cluster都这么做,我们会得到5个shift,挑出里边最大的biggest_shift

⑨如果biggest_shift小于收敛阈值0.2,跳出loop,否则,回到⑥

请注意,每个循环的时候,我们都要更新中心点,这是在第⑧步做的。


代码:
kmeans_tools.py

# -*- coding: utf-8 -*-

import math
import random


class Cluster(object):
    """
        聚类
    """

    def __init__(self, samples):
        if len(samples) == 0:
            # 如果聚类中无样本点
            raise Exception("错误:一个空的聚类!")

        # 属于该聚类的样本点
        self.samples = samples

        # 该聚类中样本点的维度
        self.n_dim = samples[0].n_dim

        # 判断该聚类中所有样本点的维度是否相同
        for sample in samples:
            if sample.n_dim != self.n_dim:
                raise Exception("错误: 聚类中样本点的维度不一致!")

        # 设置初始化的聚类中心
        self.centroid = self.cal_centroid()

    def __repr__(self):
        """
            输出对象信息
        """
        return str(self.samples)

    def update(self, samples):
        """
            计算之前的聚类中心和更新后聚类中心的距离
        """

        old_centroid = self.centroid
        #这里lists[i]中的所有数据都赋给clusters[i]了
        self.samples = samples
        #这里把clusters[i]的centroid也更新了
        self.centroid = self.cal_centroid()
        shift = get_distance(old_centroid, self.centroid)
        return shift

    def cal_centroid(self):
        """
           对于一组样本点计算其中心点
        """
        #获取样本点的个数
        n_samples = len(self.samples)
        # 获取所有样本点的坐标(特征)
        coords = [sample.coords for sample in self.samples]
        # 将所有sample的横坐标合起来作为一个tuple,所有的纵坐标合起来作为一个tuple
        unzipped = zip(*coords)
        # 计算每个维度的均值
        centroid_coords = [math.fsum(d_list)/n_samples for d_list in unzipped]
        #将新得到的中心点封装成一个Sample
        return Sample(centroid_coords)


class Sample(object):
    """
        样本点类
    """
    def __init__(self, coords):
        self.coords = coords    # 样本点包含的坐标
        self.n_dim = len(coords)    # 样本点维度

    # 将coords转成str输出
    # 方便查看
    def __repr__(self):

        return str(self.coords)


def get_distance(a, b):

    #使用两点间的距离公式计算两个sample之间的距离
    if a.n_dim != b.n_dim:
        # 如果样本点维度不同
        raise Exception("错误: 样本点维度不同,无法计算距离!")

    acc_diff = 0.0
    for i in range(a.n_dim):
        square_diff = pow((a.coords[i]-b.coords[i]), 2)
        acc_diff += square_diff
    distance = math.sqrt(acc_diff)

    return distance


def gen_random_sample(n_dim, lower, upper):
    """
        生成随机样本
    """
    # [random.uniform(lower, upper) for _ in range(n_dim)]得到的是一个二维的点,我们将其传入Sample
    #得到一个Sample对象
    sample = Sample([random.uniform(lower, upper) for _ in range(n_dim)])
    return sample

Sample类做样本点,样本点提供给Cluster做簇,gen_random_sample方法生成随机点,get_distance方法计算两点间的距离
 
main.py

# -*- coding: utf-8 -*-

import random
from kmeans_tools import Cluster, get_distance, gen_random_sample
import matplotlib.pyplot as plt
from matplotlib import colors as mcolors


def kmeans(samples, k, cutoff):

    # 随机选k个样本点作为初始聚类中心
    #random.sample返回的是samples中任选的5个sample,以它们作为初始的重心
    init_samples = random.sample(samples, k)

    # 创建k个聚类,聚类的中心分别为随机初始的样本点
    #最初,clusters是有5个Cluster的list
    #每个Cluster的centroid都是init_sample中的值,因为只有一个中心点嘛,它的中心自然就是自己了
    #这也可以看成是初始化Cluster
    clusters = [Cluster([sample]) for sample in init_samples]


    # 迭代循环直到聚类划分稳定

    #记录循环次数
    n_loop = 0
    while True:
        # 初始化一组空列表用于存储每个聚类内的样本点
        #注意:我们现在是要把所有的sample分开来存在这以下的5个空的list中
        #但是,我们最终要得到的是分好类的clusters,并非分好的lists,所以还会有将lists中的值转移到clusters中的步骤
        lists = [[] for _ in clusters]

        # 开始迭代
        n_loop += 1
        # 遍历样本集中的每个样本
        for sample in samples:
            # 计算样本点sample和第一个聚类中心的距离
            smallest_distance = get_distance(sample, clusters[0].centroid)
            # 初始化属于聚类 0
            cluster_index = 0

            # 计算和其他聚类中心的距离
            #分别算出sample和另外四个cluster的centroid的距离
            for i in range(k - 1):
                # 计算样本点sample和聚类中心的距离
                distance = get_distance(sample, clusters[i+1].centroid)
                # 如果存在更小的距离,更新距离
                if distance < smallest_distance:
                    smallest_distance = distance
                    cluster_index = i + 1

            # 找到最近的聚类中心,更新所属聚类
            lists[cluster_index].append(sample)

        #运行至此,1000个样本点根据距离最小的分类方案分别存在了5个list当中
        # 初始化最大移动距离
        biggest_shift = 0.0

        # 计算本次迭代中,聚类中心移动的距离
        for i in range(k):
            #注意:这个update方法不仅计算出了原来的聚类中心和新的聚类中心的距离
            #而且,他把list中的值更新到cluster当中了,并且每个cluster的centroid也在这一步更新了
            shift = clusters[i].update(lists[i])
            # 记录最大移动距离
            biggest_shift = max(biggest_shift, shift)

        # 如果聚类中心移动的距离小于收敛阈值,即:聚类稳定
        if biggest_shift < cutoff:
            print("第{}次迭代后,聚类稳定。".format(n_loop))
            break
    # 返回聚类结果
    return clusters


def run_main():
    """
        主函数
    """
    # 样本个数
    n_samples = 1000

    # 维度
    n_feat = 2

    # 生成的点的范围,也就是0到200
    lower = 0
    upper = 200

    # 聚类个数
    n_cluster = 5

    # 生成随机样本
    samples = [gen_random_sample(n_feat, lower, upper) for _ in range(n_samples)]

    # 收敛阈值
    cutoff = 0.2

    #将1000个随机sample,聚类个数,和收敛阈值传入kmeans方法,
    #返回的是经过无监督学习后自动分好类的5个Cluster
    #此函数是理解kmeans的关键
    clusters = kmeans(samples, n_cluster, cutoff)

    # 输出结果
    for i, c in enumerate(clusters):
        for sample in c.samples:
            print('聚类--{},样本点--{}'.format(i, sample))

    # ---------------------算法结束,以下是画图-----------------------------------------------------------------

    # 可视化结果
    plt.subplot()
    color_names = list(mcolors.cnames)
    # 将5个Cluster以scatter的方式显示
    for i, c in enumerate(clusters):
        x = []
        y = []

        # 这里i是循环次数,是固定的,所以从color_names中取出的颜色也是固定的,这个你可以所以改动
        color = [color_names[i]] * len(c.samples)
        for sample in c.samples:
            x.append(sample.coords[0])
            y.append(sample.coords[1])
        plt.scatter(x, y, c=color)
    plt.show()

if __name__ == '__main__':
    run_main()

用面向对象的思想写kmeans确实麻烦,倒是用pandas简单点


结果:



我在这里只是随机运行了三次,其实改变的只是样本点不同罢了,结果还是很满意的。

你当然可以改变k以及收敛阈值的值,观察结果的改变。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值