K_Means算法实战

k_means为k均值聚类,是一种聚类算法。
本文主要讲解:
1.如何自己用python编写k_means
2.实例演练

k_means算法流程:
1.对于数据集X选择要将数据集分为几类,设为k

2.初始化k个中心点(中心点的初始化方法有很多种比如:(1)随机选取样本X中每个特征最大值最小值之间的任意数据、(2)在样本中随机选取k个点,(3)人工根据经验选取k个中心点传入程序中,(4)对于比较大的样本可以选择几个子样本进行分类,根据子样本最终的中心点的均值作为原始样本的初始化的k个中心点,(5)k-means++ 该文中有介绍等等),本文的初始化方法为第一种方法。

3.计算X中每一个样本点对每一个中心点的距离,将距离该中心点最小距离的样本加入该中心点,(例如:有两个中心点center1与center2,X={x1,x2,x3…},如果x1距离center1的距离为2,距离center2的距离为5,那么就将x1归属于center1类。相应的x2距离center1的距离为28,距离center2的距离为1,那么x2就归属于center2)。本文所用的距离为欧氏距离。

4.将数据集X中的样本根据距离分别归类到k个中心点中后,计算每个中心点中的样本的质心(也就是该中心点样本的每个特征的平均值),将新算出来的质心代替先前的中心点。
例如:

# center1中的样本点为:
data_center1 = [[1,2,3]
				[4,5,6]
				[7,8,9]]
# 该样本的质心point为
point = [(1+4+7)/3, (2+5+8)/3, (3+6+9)/3]

5.根据第4步算出来的新的中心点,计算新的中心点与旧的中心点的欧氏距离;选取所有新的中心点与相对应旧的中心点欧氏距离最大的值,将最大值与所定的收敛条件比较(比如:最大值为max,收敛条件为e = 0.01,若max<e,则退出循环,否则继续3、4、5步的运算)

6.重复3、4、5步直到max<e为止。

python实现k_means:

class K_Means:
    """
    k 均值聚类
    """
    def __init__(self, X, k, e=0.01):
        self.X = np.array(X)
        self.k = k
        self.e = e

    def init_random(self):
        """
        随机初始化k个中心点
        :return:
        """
        # 计算X中的每一个特征的最大与最小值,使初始化的点在最大值与最小值之间
        # 注:将计算出的最大最小值分为self.k份,每一个初始化质心在每一份中随机选取或选取平均值
        self.X_max = []
        self.X_min = []
        for temp in range(self.X.shape[1]):
            self.X_max.append(max(self.X[:, temp]))
            self.X_min.append(min(self.X[:, temp]))

        # 将self.X_min与self.X_max分为self.k份
        split = (np.array(self.X_max) - np.array(self.X_min))/self.k

        # 随机初始化self.k个点
        init_point = []
        for temp in range(self.k):
            point = []
            for temp1 in range(self.X.shape[1]):
                 point.append(random.uniform(self.X_min[temp1] + temp*split[temp1], self.X_min[temp1] + (temp + 1)*split[temp1]))
            init_point.append(point)

        return init_point

    def EuchdeanDistance(self, a, b):
        """欧氏距离"""
        return np.sqrt(np.dot(a - b, a - b))

    def center_point(self, x):
        """计算一群样本中心点"""
        center = []
        x = np.array(x)
        point = []
        if x.ndim == 1:
            for temp1 in range(self.X.shape[1]):
                 point.append(random.uniform(self.X_min[temp1], self.X_max[temp1]))
            return point

        for temp in range(x.shape[1]):
            center.append(np.average(x[:, temp]))
        return center

    def sample_allocation(self, init_point):
        """
        将样本X中的点划分给,距离k个点中最近的点,更新中心点,直到满足要求
        :return:
        """
        # 定义一个previous_point用于存储上一个init_point,用于比较两个点的差值,用于终止程序
        previous_point = []
        for i in range(self.k):
            previous_point.append([0]*self.X.shape[1])

        # 定义一个字典,用于存储每个self.k中做具有的数据点的索引
        k_include = {}

        # max_loop 最大循环次数防止死循环
        max_loop = 500

        # loop 当前循环次数
        loop = 0

        # 循环终止条件,中心点差值小于0.01(previous_point与init_point差值的欧氏距离的最大值小于0.01,为了防止样本数据太小的影响可以用相对误差)
        e = self.e

        # 循环整个迭代过程,直到收敛为止
        while True:
            for temp in range(self.k):
                k_include[temp] = []

            # 计算每一个样本点到init_point每个点的距离,将距离self.k中最近的点加入到k_include中相应的k中
            for index, temp in enumerate(self.X):
                distance = []
                for i in init_point:
                    distance.append(self.EuchdeanDistance(temp, i))
                k_include[distance.index(min(distance))].append(index)

            # 计算每一个k_include样本中的中心点并更新到init_point中
            previous_point = init_point
            for temp in range(self.k):
                temp_value = []
                for i in k_include[temp]:
                    temp_value.append(self.X[i, :])
                center = self.center_point(temp_value)
                init_point[temp] = center

            # 计算init_point 与 previous_point 之间的差值(用欧式距离表示),存储在diff中
            diff = []
            for temp in range(self.k):
                diff.append(self.EuchdeanDistance(np.array(init_point)[temp, :], np.array(previous_point)[temp, :]))

            # 计算k_include中的元素长度,防止k_include有空列表(这只是防止陷入局部最优解,但有可能loop > max_loop还是有空列表,那么这就是选取的k只有问题)
            min_length = 1
            for temp in range(self.k):
                if min_length > np.array(k_include[temp]).shape[0]:
                    min_length = np.array(k_include[temp]).shape[0]

            # 如果min_length=0,从新初始化init_point
            if min_length == 0:
                init_point = self.init_random()

            loop += 1

            # 判断循环是否终止
            if (loop > max_loop) or ((max(diff) < e) and (min_length != 0)):
                break

        return k_include, init_point

    def main(self):
        """调用该函数运行kmeans"""
        init_point = self.init_random()
        k_include, init_point = self.sample_allocation(init_point)
        return k_include, init_point

算法数据实战演练:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 解决画图中文字体乱码问题
plt.rcParams['font.sans-serif'] = ['SimHei']
# 解决画图负号(-)显示不出来问题
plt.rcParams['axes.unicode_minus'] = False

# 制作一个二维数据,该数据集有两个特征a,b,该数据集最好的结果是被分为5类
x = []
for i in range(5):
    for _ in range(20):
        a = np.random.normal(i, 0.2)
        b = np.random.normal(i + 5, 0.2)
        x.append([a, b])
        
# 将数据集x画出来
plt.scatter(np.array(x)[:, 0], np.array(x)[:, 1])
plt.xlabel("a")
plt.ylabel("b")
plt.show()
    

在这里插入图片描述

当k=5时的分类情况

k = 5
for _ in range(3):
    km = K_Means(x, k)
    k_include, init_point = km.main()
	
	# 将k_include中的x的标签的数据加入到D中
    D = {}
    for i in range(k):
        D[i] = []
        for j in k_include[i]:
            D[i].append(x[j])

    plt.figure()
    for i in range(k):
   		 # 防止D为空
        if np.array(D[i]).ndim == 2:
            plt.scatter(np.array(D[i])[:, 0], np.array(D[i])[:, 1])
    plt.show()

运行结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这是运行三次k=5状态下的运行结果,从图中可见三次分类结果有些许不同,最后一次结果有些误差绿色点与红色点分类效果不好,这是由于,初始参数是随机选取的所以可能在一些情况下分类效果不是很好。

由于k值是人工选取的,所以依赖于人工,下面将介绍一种k值选择方法:
该方法是通过,计算在k值确定的情况下,利用最终迭代所得的中心点以及每个中心点中的数据,计算每个中心点下的数据点到中心点的欧式距离并取平均值。将每个中心点所计算的平均值相加在平均得到“最终结果”。利用不同k下的“最终结果”画出曲线,可以看出,如果分类效果好那么“最终结果”越小。

代码如下:

x = []
for i in range(5):
    for _ in range(20):
        a = np.random.normal(i, 0.2)
        b = np.random.normal(i + 5, 0.2)
        x.append([a, b])


k = 2
# 存储“最终结果”
result = {}
# 存储不同的k值
result["k"] = []
# 存储不同k值下的“最终结果”
result["dis"] = []

for _ in range(12):
    km = K_Means(x, k)
    k_include, init_point = km.main()

    D = {}
    for i in range(k):
        D[i] = []
        for j in k_include[i]:
            D[i].append(x[j])

    # 计算最优k值,利用每个质心到该质心所对应的点的距离的平均值
    SUM = 0
    for i in range(k):
        sum = 0
        for j in D[i]:
            sum += np.sqrt(np.dot(np.array(j) - np.array(init_point[i]), np.array(j) - np.array(init_point[i])))
        if len(D[i]) != 0:
            SUM += sum/(len(D[i]))

    result["k"].append(k)
    result["dis"].append(SUM/k)

    k += 1

# 画出不同k值下的最终结果
plt.figure()
plt.title("最佳k值")
plt.plot(result["k"], result["dis"])
plt.show()

最终结果:
在这里插入图片描述
从上图中可以看出,在k=5往后k值的变化就很缓慢,所以k=5的时候最合适。原因如下:从极限的方面考虑,当k=1时此时只有一个中心点,所以此时距离(也就是y轴 )应该最大。当k的值与样本数目相同时,此时距离应该为0,。所以当k值越大时距离的变化趋势应该是逐渐减小。但k值并不是越大越好,这和实际需求有关。

上面的数据分布的较好,还有一些数据变化极小,如下所示
| | |

在这里插入图片描述

这类数据画散点图如下:
在这里插入图片描述
所有数据在图中可以显示为3个点,这时由于数据变化极小导致的,这些数据用上述自己编写的代码运行如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
虽然对应于不同的k值但最终的分类结果相同,都分为3类。如下代码所示,这是由于在程序运行期间k_include中有空列表,即使loop达到最大值max_loop,程序k_include中仍有空列表,所以为了防止死循环,程序运行结束。

# 计算k_include中的元素长度,防止k_include有空列表(这只是防止陷入局部最优解,但有可能loop > max_loop还是有空列表,那么这就是选取的k只有问题)
min_length = 1
for temp in range(self.k):
    if min_length > np.array(k_include[temp]).shape[0]:
        min_length = np.array(k_include[temp]).shape[0]

# 如果min_length=0,从新初始化init_point
if min_length == 0:
    init_point = self.init_random()

loop += 1

# 判断循环是否终止
if (loop > max_loop) or ((max(diff) < e) and (min_length != 0)):
    break

对于这类数据k值与距离变化如下:
在这里插入图片描述
虽然随着k值的增加,距离在减小,但在k=3时,距离几乎为0,所以最佳k=3

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值