Select Language

AI社区

AI技术百科

机器学习之聚类算法

一、引言

聚类算法是无监督学习,只需要数据,而不需要标记结果,通过学习训练,用于发现共同的群体。本文将介绍几种常见的聚类算法,包括K-means、层次聚类和GMM高斯混合模型等。

1、聚类算法是什么

给定n个训练样本(未标记),如:{X1,X2,...,Xn},同时给定聚类的个数K。

目标:把比较“接近”的样本放到一个簇类(cluster)里,总共得到K个簇类(cluster)。

 

2、如何衡量样本距离

下面是几个最常用的距离计算公式:

(1)欧氏距离:

(2)曼哈顿距离

(3)核函数映射后距离

 

3、如何评价聚类好坏

类间距高,类内距低,通俗地说就是“抱团紧不紧,异族远不远”。

 

4、应用场景

(1)图片检索:图片内容相似度

(2)图片分割:图片像素相似度

(3)网页聚类:文本内容相似度

(4)社交网络聚类:(被)关注人群、兴趣

(5)电商用户聚类:点击/加车/购买商品、行为序列等

 

二、K-means算法

K-means算法是使用最普遍,最重要的聚类算法之一。

已知观测集(x_{1},x_{2},...,x_{n}),其中每个观测都是一个D-维实向量,k-平均聚类要把这n个观测划分到k个集合中(k≤n),使得组内平方和(WCSS within-cluster sum of squares)最小。换句话说,它的目标是找到使得下式满足的聚类S_{i}

1、算法原理

迭代收敛定义:

(1)聚类中心不在有变化

(2)每个样本到对应聚类中心的距离之和不再有很大变化

损失函数:

假定为K个聚类中心,用表示是否属于聚类K,则损失函数定义如下:

最小化损失函数的过程是一个NP问题。上面的迭代,是收敛到局部最低点的过程。

 

2、优缺点

K-means算法优点:

(1)计算复杂度低,为O(Nmq),其中N是数据总量,m是类别(即k),q是迭代次数

(2)原理简单,实现容易,容易解释

(3)聚类效果不错

 

K-means算法缺点:

(1)对异常值(噪声)敏感,可以通过一些调整(如中心值不直接取均值,而是找均值最近的样本点代替)

(2)需要提前确定K值(提前确定多少类)

(3)分类结果依赖于分类中心的初始化(可以通过进行多次K-means取最优来解决)

(4)属于“硬聚类”,每个样本只能有一个类别。其他的一些聚类方法(GMM或者模糊K-means允许“软聚类”)

(5)对于团状的数据点集区分度好,对于带状(环绕)等非凸形状不太好。(用谱聚类或做特征映射)

 

3、改进算法

如:K-means++等

 

4、动手用python实现K-means算法

样本数据:

1.658985    4.285136
-3.453687   3.424321
4.838138    -1.151539
-5.379713   -3.362104
0.972564    2.924086
-3.567919   1.531611
0.450614    -3.302219
-3.487105   -1.724432
2.668759    1.594842
-3.156485   3.191137
3.165506    -3.999838
-2.786837   -3.099354
4.208187    2.984927
-2.123337   2.943366
0.704199    -0.479481
-0.392370   -3.963704
2.831667    1.574018
-0.790153   3.343144
2.943496    -3.357075
-3.195883   -2.283926
2.336445    2.875106
-1.786345   2.554248
2.190101    -1.906020
-3.403367   -2.778288
1.778124    3.880832
-1.688346   2.230267
2.592976    -2.054368
-4.007257   -3.207066
2.257734    3.387564
-2.679011   0.785119
0.939512    -4.023563
-3.674424   -2.261084
2.046259    2.735279
-3.189470   1.780269
4.372646    -0.822248
-2.579316   -3.497576
1.889034    5.190400
-0.798747   2.185588
2.836520    -2.658556
-3.837877   -3.253815
2.096701    3.886007
-2.709034   2.923887
3.367037    -3.184789
-2.121479   -4.232586
2.329546    3.179764
-3.284816   3.273099
3.091414    -3.815232
-3.762093   -2.432191
3.542056    2.778832
-1.736822   4.241041
2.127073    -2.983680
-4.323818   -3.938116
3.792121    5.135768
-4.786473   3.358547
2.624081    -3.260715
-4.009299   -2.978115
2.493525    1.963710
-2.513661   2.642162
1.864375    -3.176309
-3.171184   -3.572452
2.894220    2.489128
-2.562539   2.884438
3.491078    -3.947487
-2.565729   -2.012114
3.332948    3.983102
-1.616805   3.573188
2.280615    -2.559444
-2.651229   -3.103198
2.321395    3.154987
-1.685703   2.939697
3.031012    -3.620252
-4.599622   -2.185829
4.196223    1.126677
-2.133863   3.093686
4.668892    -2.562705
-2.793241   -2.149706
2.884105    3.043438
-2.967647   2.848696
4.479332    -1.764772
-4.905566   -2.911070

其数据分布如下,可以看到,数据大概可以分成四类。

下面是通过K-means算法聚类后的分布图:

完整代码:k-means.py

# coding: utf-8
import random

import numpy as np
import matplotlib.pyplot as plt


def show_fig():
    data = load_data()
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(data[:, 0], data[:, 1])
    plt.show()


def cal_distance(node1, node2):
    """
    计算两个向量之间的欧式距离
    :param node1:
    :param node2:
    :return:
    """
    return np.sqrt(np.sum(np.square(node1 - node2)))


def load_data():
    """
    加载数据
    :return:
    """
    data = np.loadtxt("dataSet.csv")
    return data


def init_k_node(data, k):
    """
    随机初始化k个数据返回
    :param data:
    :param k:
    :return:
    """
    data = list(data)
    return random.sample(data, k)


def get_clusters(data, k_centroids):
    """
    计算各个节点所属的簇类
    :param data:
    :param k_centroids:
    :return:
    """
    cluster_dict = dict()
    k = len(k_centroids)
    for node in data:
        # 计算节点node与k个质心的距离,选取距离最近的,并将节点加入相应簇类中
        cluster_idx = -1
        min_dis = float("inf")
        for idx in range(k):
            centroid = k_centroids[idx]
            distance = cal_distance(node, centroid)
            if distance < min_dis:
                min_dis = distance
                cluster_idx = idx

        if cluster_idx not in cluster_dict.keys():
            cluster_dict[cluster_idx] = []
        cluster_dict[cluster_idx].append(node) # 加入对应类别中
    return cluster_dict


def get_centroids(cluster_dict):
    """
    重新计算k个质心
    :param cluster_dict:
    :return:
    """
    new_k_centroids = []
    for cluster_idx in cluster_dict.keys():
        new_centroid = np.mean(cluster_dict[cluster_idx], axis=0)
        new_k_centroids.append(new_centroid)
    return new_k_centroids


def get_variance(centroids, cluster_dict):
    """
    计算各个簇集合的均方误差
    将簇类中各个节点与质心的距离累加求和
    :param centroids:
    :param cluster_dict:
    :return:
    """
    sum = 0.0
    for cluster_idx in cluster_dict.keys():
        centroid = centroids[cluster_idx]
        distance = 0.0
        for node in cluster_dict[cluster_idx]:
            distance += cal_distance(node, centroid)
        sum += distance
    return sum


def show_cluster(centroids, cluster_dict):
    """
    展示聚类结果
    :param centroids:
    :param cluster_dict:
    :return:
    """
    color_mark = ['or', 'ob', 'og', 'ok', 'oy', 'ow']
    centroid_mark = ['dr', 'db', 'dg', 'dk', 'dy', 'dw']

    for key in cluster_dict.keys():
        plt.plot(centroids[key][0], centroids[key][1], centroid_mark[key], markersize=12) # 质心点
        for node in cluster_dict[key]:
            plt.plot(node[0], node[1], color_mark[key])
    plt.show()


def main():
    data = load_data()
    centroids = init_k_node(data, 4)
    cluster_dict = get_clusters(data, centroids)
    new_var = get_variance(centroids, cluster_dict)
    old_var = 1

    # 当两次聚类的误差小于某个值时,说明质心基本稳定
    while abs(new_var - old_var) >= 0.00001:
        centroids = get_centroids(cluster_dict)
        cluster_dict = get_clusters(data, centroids)
        old_var = new_var
        new_var = get_variance(centroids, cluster_dict)
        show_cluster(centroids, cluster_dict)

if __name__ == '__main__':
    show_fig()
    main()

 

三、层次聚类

K-means一个最大的限制是,需要实现知道K值,即知道多少个分类。那么有没有不需要确定K值就可以分类的聚类算法呢?层次聚类就是这种算法。

层次聚类分为凝聚式层次聚类和分裂式层次聚类。

凝聚式层次聚类,就是在初始阶段将每一个点都视为一个簇,之后每一次合并两个最接近的簇。

分裂式层次聚类,就是在初始阶段将所有的点视为一个簇,之后每次分裂出一个簇,直到最后剩下单个点的簇为止。

1、簇之间的距离定义

下面讲解凝聚式聚类。凝聚式聚类簇之间的距离定义有如下三种。

cluster R和cluster S之间的距离计算有以下几种:

(1)最小连接距离法(Single linkage)

取两个簇中距离最近的两个样本的距离作为这两个簇的距离。

(2)最大连接距离发(Complete linkage)

取两个簇中距离最远的两个点的距离作为这两个簇的距离。

(3)平均连接距离法(Average linkage)

把两个簇中的点的两两的距离全部放在一起求均值,取得的结果也相对合适一点。

图 2. 层次聚类计算准则

2、层次聚类算法原理

(1)初始化,将每个样本都视为一个簇

(2)计算各个簇之间的距离

(3)寻找最近的两个簇,并将他们合并为一类

(4)重复步骤(2)和步骤(3),知道所有样本归为一类。

3、层次聚类算法过程举例

如下图是5个点的样本分布图:

初始化,将每个样本视为一个簇,其欧式距离原始举证如下:


P1P2P3P4P5
P100.811.321.941.82
P20.8101.562.161.77
P31.321.5600.630.71
P41.942.160.6300.71
P51.821.770.710.710

按照算法的流程,我们取距离最近的两个簇P3和P4并合并为{P3,P4},根据最小连接距离法更新距离矩阵:


P1P2{P3,P4}P5
P100.811.321.82
P20.8101.561.77
{P3,P4}1.321.5600.71
P51.821.770.710

接着找出距离最近的两个簇{P3,P4}和P5,合并为{P3,P4,P5},根据最小连接距离法更新距离矩阵:


P1P2{P3,P4,P5}
P100.811.32
P20.8101.56
{P3,P4,P5}1.321.560

接着找出距离最近的两个簇P1和P2,合并为{P1,P2},根据最小连接距离法更新距离矩阵:


{P1,P2}{P3,P4,P5}
{P1,P2}01.32
{P3,P4,P5}1.320

最终合并剩下的两个簇,得到如下图的聚类结果:

4、用python实现层次聚类算法

#-*- coding:utf-8 -*-
import math
import pylab as pl


def dist(node1, node2):
    """
    计算欧几里得距离,node1,node2分别为两个元组
    :param node1:
    :param node2:
    :return:
    """
    return math.sqrt(math.pow(node1[0]-node2[0], 2)+math.pow(node1[1]-node2[1], 2))


def dist_min(cluster_x, cluster_y):
    """
    Single linkage
    又叫做 nearest-neighbor ,就是取两个类中距离最近的两个样本的距离作为这两个集合的距离。
    :param cluster_x:
    :param cluster_y:
    :return:
    """
    return min(dist(node1, node2) for node1 in cluster_x for node2 in cluster_y)


def dist_max(cluster_x, cluster_y):
    """
    Complete linkage
    这个则完全是 Single linkage 的反面极端,取两个集合中距离最远的两个点的距离作为两个集合的距离。
    :param cluster_x:
    :param cluster_y:
    :return:
    """
    return max(dist(node1, node2) for node1 in cluster_x for node2 in cluster_y)


def dist_avg(cluster_x, cluster_y):
    """
    Average linkage
    这种方法就是把两个集合中的点两两的距离全部放在一起求均值,相对也能得到合适一点的结果。
    :param cluster_x:
    :param cluster_y:
    :return:
    """
    return sum(dist(node1, node2) for node1 in cluster_x for node2 in cluster_y)/(len(cluster_x)*len(cluster_y))


def find_min(distance_matrix):
    """
    找出距离最近的两个簇下标
    :param distance_matrix:
    :return:
    """
    min = 1000
    x = 0; y = 0
    for i in range(len(distance_matrix)):
        for j in range(len(distance_matrix[i])):
            if i != j and distance_matrix[i][j] < min:
                min = distance_matrix[i][j];x = i; y = j
    return (x, y, min)


def AGNES(dataset, distance_method, k):
    """
    聚类算法模型
    :param dataset: 数据集
    :param distance_method: 计算簇类聚类的方法
    :param k: 目标簇类数目
    :return:
    """
    print len(dataset)
    # 初始化簇类集合和距离矩阵
    cluster_set = []
    distance_matrix = []
    for node in dataset:
        cluster_set.append([node])
    print 'original cluster set:'
    print cluster_set
    for cluster_x in cluster_set:
        distance_list = []
        for cluster_y in cluster_set:
            distance_list.append(distance_method(cluster_x, cluster_y))
        distance_matrix.append(distance_list)
    print 'original distance matrix:'
    print len(distance_matrix), len(distance_matrix[0]), distance_matrix
    q = len(dataset)
    # 合并更新
    while q > k:
        id_x, id_y, min_distance = find_min(distance_matrix)
        cluster_set[id_x].extend(cluster_set[id_y])
        cluster_set.remove(cluster_set[id_y])
        distance_matrix = []

        for cluster_x in cluster_set:
            distance_list = []
            for cluster_y in cluster_set:
                distance_list.append(distance_method(cluster_x, cluster_y))
            distance_matrix.append(distance_list)
        q -= 1
    return cluster_set


def draw(cluster_set):
    """
    画图
    :param cluster_set:
    :return:
    """
    color_list = ['r', 'y', 'g', 'b', 'c', 'k', 'm']

    for cluster_idx, cluster in enumerate(cluster_set):
        coo_x = []  # x坐标列表
        coo_y = []  # y坐标列表

        for node in cluster:
            coo_x.append(node[0])
            coo_y.append(node[1])
        pl.scatter(coo_x, coo_y, marker='x', color=color_list[cluster_idx % len(color_list)], label=cluster_idx)

    pl.legend(loc='upper right')
    pl.show()


# 数据集:每三个是一组分别是西瓜的编号,密度,含糖量
data = """
1,0.697,0.46,2,0.774,0.376,3,0.634,0.264,4,0.608,0.318,5,0.556,0.215,
6,0.403,0.237,7,0.481,0.149,8,0.437,0.211,9,0.666,0.091,10,0.243,0.267,
11,0.245,0.057,12,0.343,0.099,13,0.639,0.161,14,0.657,0.198,15,0.36,0.37,
16,0.593,0.042,17,0.719,0.103,18,0.359,0.188,19,0.339,0.241,20,0.282,0.257,
21,0.748,0.232,22,0.714,0.346,23,0.483,0.312,24,0.478,0.437,25,0.525,0.369,
26,0.751,0.489,27,0.532,0.472,28,0.473,0.376,29,0.725,0.445,30,0.446,0.459"""

# 数据处理 dataset是30个样本(密度,含糖量)的列表
a = data.split(',')
dataset = [(float(a[i]), float(a[i+1])) for i in range(1, len(a)-1, 3)]

cluster_set = AGNES(dataset, dist_avg, 3)
print 'final cluster set:'
print cluster_set
draw(cluster_set)

最终效果:

四、GMM高斯混合模型和EM算法

EM算法是一种迭代算法,1977年由Dempster等人总结提出,用于含有隐变量的概率模型参数的极大似然估计,或极大后验概率估计。

EM算法的每次迭代由两部组成:E步,求期望;M步,求极大。所以这一算法称为期望极大算法,简称EM算法。

4.1 高斯混合模型

定义:高斯混合模型是指具有如下形式的概率分布模型:

其中,是系数,大于等于0,是高斯分布密度,

称为第k个分模型。

4.2 高斯混合模型参数估计的EM算法

输入:假设观测数据,由高斯混合模型生成,即:

其中

输出:利用EM算法估计高斯混合模型的参数θ。

(1)取参数的初始值开始迭代

(2)E步:依据当前模型参数,计算分模型k对观测数据的响应度

(3)M步:计算新一轮的模型参数

(4)重复(2)步和(3)步,直到收敛。

4.3 python实现

生成样本数据:

import numpy as np
import matplotlib.pyplot as plt

cov1 = np.mat("0.3 0;0 0.1")
cov2 = np.mat("0.2 0;0 0.3")
mu1 = np.array([0, 1])
mu2 = np.array([2, 1])

sample = np.zeros((100, 2))
sample[:30, :] = np.random.multivariate_normal(mean=mu1, cov=cov1, size=30)
sample[30:, :] = np.random.multivariate_normal(mean=mu2, cov=cov2, size=70)
np.savetxt("sample.data", sample)

plt.plot(sample[:30, 0], sample[:30, 1], "bo")
plt.plot(sample[30:, 0], sample[30:, 1], "rs")
plt.show()

原始数据分布图

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

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import multivariate_normal

######################################################
# 第 k 个模型的高斯分布密度函数
# 每 i 行表示第 i 个样本在各模型中的出现概率
# 返回一维列表
######################################################
def phi(Y, mu_k, cov_k):
    norm = multivariate_normal(mean=mu_k, cov=cov_k)
    return norm.pdf(Y)


######################################################
# E 步:计算每个模型对样本的响应度
# Y 为样本矩阵,每个样本一行,只有一个特征时为列向量
# mu 为均值多维数组,每行表示一个样本各个特征的均值
# cov 为协方差矩阵的数组
# alpha 为模型响应度数组
######################################################
def getExpectation(Y, mu, cov, alpha):
    # 样本数
    N = Y.shape[0]
    # 模型数
    K = alpha.shape[0]

    # 为避免使用单个高斯模型或样本,导致返回结果的类型不一致
    # 因此要求样本数和模型个数必须大于1
    assert N > 1, "There must be more than one sample!"
    assert K > 1, "There must be more than one gaussian model!"

    # 响应度矩阵,行对应样本,列对应响应度
    gamma = np.mat(np.zeros((N, K)))

    # 计算各模型中所有样本出现的概率,行对应样本,列对应模型
    prob = np.zeros((N, K))
    for k in range(K):
        prob[:, k] = phi(Y, mu[k], cov[k])
    prob = np.mat(prob)

    # 计算每个模型对每个样本的响应度
    for k in range(K):
        gamma[:, k] = alpha[k] * prob[:, k]
    for i in range(N):
        gamma[i, :] /= np.sum(gamma[i, :])
    return gamma


######################################################
# M 步:迭代模型参数
# Y 为样本矩阵,gamma 为响应度矩阵
######################################################
def maximize(Y, gamma):
    # 样本数和特征数
    N, D = Y.shape
    # 模型数
    K = gamma.shape[1]

    #初始化参数值
    mu = np.zeros((K, D))
    cov = []
    alpha = np.zeros(K)

    # 更新每个模型的参数
    for k in range(K):
        # 第 k 个模型对所有样本的响应度之和
        Nk = np.sum(gamma[:, k])
        # 更新 mu
        # 对每个特征求均值
        for d in range(D):
            mu[k, d] = np.sum(np.multiply(gamma[:, k], Y[:, d])) / Nk
        # 更新 cov
        cov_k = np.mat(np.zeros((D, D)))
        for i in range(N):
            cov_k += gamma[i, k] * (Y[i] - mu[k]).T * (Y[i] - mu[k]) / Nk
        cov.append(cov_k)
        # 更新 alpha
        alpha[k] = Nk / N
    cov = np.array(cov)
    return mu, cov, alpha


######################################################
# 数据预处理
# 将所有数据都缩放到 0 和 1 之间
######################################################
def scale_data(Y):
    # 对每一维特征分别进行缩放
    for i in range(Y.shape[1]):
        max_ = Y[:, i].max()
        min_ = Y[:, i].min()
        Y[:, i] = (Y[:, i] - min_) / (max_ - min_)
    print("Data scaled.")
    return Y


######################################################
# 初始化模型参数
# shape 是表示样本规模的二元组,(样本数, 特征数)
# K 表示模型个数
######################################################
def init_params(shape, K):
    N, D = shape
    mu = np.random.rand(K, D) # 生成K个,D维的随机样本数据,并且随机样本位于[0,1)中
    cov = np.array([np.eye(D)] * K) # 生成包含K个DxD对角矩阵的矩阵
    alpha = np.array([1.0 / K] * K) # 生成K个值为1/K的矩阵
    # print "Parameters initialized."
    # print "mu:", mu
    # print "cov:", cov
    # print "alpha:", alpha
    return mu, cov, alpha


######################################################
# 高斯混合模型 EM 算法
# 给定样本矩阵 Y,计算模型参数
# K 为模型个数
# times 为迭代次数
######################################################
def GMM_EM(Y, K, times):
    Y = scale_data(Y)
    mu, cov, alpha = init_params(Y.shape, K)
    for i in range(times):
        gamma = getExpectation(Y, mu, cov, alpha)
        mu, cov, alpha = maximize(Y, gamma)
    print("{sep} Result {sep}".format(sep="-" * 20))
    print "mu:", mu
    print "cov:", cov
    print "alpha:", alpha
    return mu, cov, alpha

# 载入数据
Y = np.loadtxt("gmm.data")
matY = np.matrix(Y, copy=True)

# 模型个数,即聚类的类别个数
K = 2

# 计算 GMM 模型参数
mu, cov, alpha = GMM_EM(matY, K, 100)

# 根据 GMM 模型,对样本数据进行聚类,一个模型对应一个类别
N = Y.shape[0]
# 求当前模型参数下,各模型对样本的响应度矩阵
gamma = getExpectation(matY, mu, cov, alpha)
# 对每个样本,求响应度最大的模型下标,作为其类别标识

category = gamma.argmax(axis=1).flatten().tolist()[0]
# 将每个样本放入对应类别的列表中
class1 = np.array([Y[i] for i in range(N) if category[i] == 0])
class2 = np.array([Y[i] for i in range(N) if category[i] == 1])

# 绘制聚类结果
plt.plot(class1[:, 0], class1[:, 1], 'rs', label="class1")
plt.plot(class2[:, 0], class2[:, 1], 'bo', label="class2")
plt.legend(loc="best")
plt.title("GMM Clustering By EM Algorithm")
plt.show()

聚类结果分布图

对比原数据的分布图和聚类的分布图,可以看到效果还是非常好的。


我要发帖
聚类算法
2021-05-12 17:06:10加入圈子
  • 11

    条内容
聚类分析又称群分析,它是研究(样品或指标)分类问题的一种统计分析方法,同时也是数据挖掘的一个重要算法。
聚类(Cluster)分析是由若干模式(Pattern)组成的,通常,模式是一个度量(Measurement)的向量,或者是多维空间中的一个点。
聚类分析以相似性为基础,在一个聚类中的模式之间比不在同一聚类中的模式之间具有更多的相似性。