所谓聚类,就是将相似的事物聚集在一 起,而将不相似的事物划分到不同的类别的过程,是数据分析之中十分重要的一种手段。比如古典生物学之中,人们通过物种的形貌特征将其分门别类,可以说就是 一种朴素的人工聚类。如此,我们就可以将世界上纷繁复杂的信息,简化为少数方便人们理解的类别,可以说是人类认知这个世界的最基本方式之一。
在数据分析的术语之中,聚类和分类是两种技术。分类是指我们已经知道了事物的类别,需要从样品中学习分类的规则,是一种有指导学习;而聚类则是由我们来给定简单的规则,从而得到分类,是一种无指导学习。两者可以说是相反的过程。
定义:k-means算法中的k代表类簇个数,means代表类簇内数据对象的均值(这种均值是一种对类簇中心的描述),因此,k-means算法又称为k-均值算法。k-means算法是一种基于划分的聚类算法,以距离作为数据对象间相似性度量的标准,即数据对象间的距离越小,则它们的相似性越高,则它们越有可能在同一个类簇。
K-Means算法原理:
对给定的样本集,事先确定聚类簇数K,让簇内的样本尽可能紧密分布在一起,使簇间的距离尽可能大。该算法试图使集群数据分为n组独立数据样本,使n组集群间的方差相等,数学描述为最小化惯性或集群内的平方和。K-Means作为无监督的聚类算法,实现较简单,聚类效果好,因此被广泛使用。
算法步骤:
输入:样本集D,簇的数目k,最大迭代次数N;
输出:簇划分(k个簇,使平方误差最小);
以随机或猜测的方式初始化类中心ui,i=1……k;(注:采用随机的方法实现简单,但是簇的质量往往比较差,所以有好几种关于中心选取的解决方案,比如先使用层次聚类进行聚类,从层次聚类中提取K个簇,并用这些簇的质心作为初始质心。也有通过使类内总方差最小的方式,选择方差最小的类中心。)
将每个数据点归并到离它距离最近的类中心所属的类ci;
(注:常用的距离度量方法包括:欧几里得距离和余弦相似度。两者都是评定个体间差异的大小的。欧几里得距离度量会受指标不同单位刻度的影响,所以一般需要先进行标准化。余弦相似度倾向给出更优解。)
对所有属于该类的数据点求平均,将平均值作为新的类中心;
重复步骤(2)和步骤(3)直到收敛。
K-means试图使类内总方差最小:
V = ∑ i k ∑ x j ∈ c i ( x j − μ i ) 2 V= \sum_{i}^{k}\sum_{x_{j}\in c_{i}}^{}(x_{j}-\mu _{i})^{2} V=i∑kxj∈ci∑(xj−μi)2
其中, x j xj xj是输入数据,并且是矢量。该算法是启发式提炼算法,在很多情形下都适用,但是不能保证得到最优的结果。为了避免初始化类中心时没选取好类中心初值所造成的影响,该算法通常会初始化不同的类中心进行多次运算,然后选择方差 V V V最小的结果。
K-means算法的缺陷是: 必须预先设定聚类数k,如果选择不恰当则会导致聚类出来的结果很差。当样本集规模大时,收敛速度会变慢;对孤立点数据敏感,少量噪声就会对平均值造成较大影响;k的取值十分关键,对不同数据集,k选择没有参考性,需要大量实验。
K-means算法优点是: 容易实现,可以并行计算,并且对于很多别的问题不需要任何调整就能够直接使用;聚类效果较优;算法的可解释度比较强;主要需要调参的参数仅仅是簇数k。
K-means算法的关键: 在于初始中心的选择和距离公式。
算法复杂度:
时间复杂度:O(tKmn),其中,t为迭代次数,K为簇的数目,m为记录数,n为维数。
空间复杂度:O((m+K)n),其中,K为簇的数目,m为记录数,n为维数。
K-means算法很容易实现,可以使用Scipy矢量量化包scipy.clusterr.vq中有K-means的实现。
#导入scipy中K-means的相关工具
from scipy.cluster.vq import *
#randn是NumPy中的一个函数
from numpy import *
from pylab import *
#生成简单的二维数据:生成两类二维正态分布数据
class1 = 1.5 * randn(100,2)
class2 = randn(100,2) + array([5,5])
features = vstack((class1,class2))
#用 k=2 对这些数据进行聚类:
centroids,variance = kmeans(features,2)
"""
由于 SciPy 中实现的 K-means 会计算若干次(默认为 20 次),并为我们选择方差最
小的结果,所以这里返回的方差并不是我们真正需要的。
"""
#用 SciPy 包中的矢量量化函数对每个数据点进行归类:通过得到的 code ,我们可以检查是否有归类错误
code,distance = vq(features,centroids)
#可视化结果:画出这些数据点及最终的聚类中心:函数 where() 给出每个类的索引
figure()
ndx = where(code==0)[0]
plot(features[ndx,0],features[ndx,1],'*')
ndx = where(code==1)[0]
plot(features[ndx,0],features[ndx,1],'r.')
plot(centroids[:,0],centroids[:,1],'go')
axis('off')
show()
处理效果:
分析:
原数据聚完类后的结果如图所示,绿色圆点表示聚类中心,预测出的类分别标记为蓝色星号和红色点。
文件selectedfontimeages.zip包含66幅来自该字体数据集fontinages的图像(为了便于说明这些聚类簇,选择这些图像做简单概述)。利用之前计算过的前40个主成分进行投影,用投影系数作为每幅图像的向量描述符。用pickle模块载入模型文件,在主成分上对图像进行投影,然后用下面的方法聚类:
# -*- coding: utf-8 -*-
from PCV.tools import imtools
import pickle
from scipy import *
from pylab import *
from PIL import Image
from scipy.cluster.vq import *
from PCV.tools import pca
# Uses sparse pca codepath.
imlist = imtools.get_imlist('D:\Python Computer Vision\_selected_thumbs\_selected_thumbs')
# 获取图像列表和他们的尺寸
im = array(Image.open(imlist[0])) # open one image to get the size
m, n = im.shape[:2] # get the size of the images
imnbr = len(imlist) # get the number of images
print
"The number of images is %d" % imnbr
# Create matrix to store all flattened images
immatrix = array([array(Image.open(imname)).flatten() for imname in imlist], 'f')
# PCA降维
V, S, immean = pca.pca(immatrix)
# 保存均值和主成分
f = open('D:\Python Computer Vision\A_s', 'wb')
pickle.dump(immean, f)
pickle.dump(V, f)
f.close()
# 获取 selected-fontimages 文件下图像文件名,并保存在列表中
imlist = imtools.get_imlist('D:\Python Computer Vision\_selected_thumbs\_selected_thumbs')
imnbr = len(imlist)
# 载入模型文件
with open('D:\Python Computer Vision\A_s', 'rb') as f:
immean = pickle.load(f)
V = pickle.load(f)
# 创建矩阵,存储所有拉成一组形式后的图像
immatrix = array([array(Image.open(im)).flatten() for im in imlist], 'f')
# 投影到前 40 个主成分上
immean = immean.flatten()
projected = array([dot(V[:40], immatrix[i] - immean) for i in range(imnbr)])
# 进行 k-means 聚类
projected = whiten(projected)
centroids, distortion = kmeans(projected, 4)
code, distance = vq(projected, centroids)
# 绘制聚类簇
for k in range(4):
ind = where(code == k)[0]
figure()
gray()
for i in range(minimum(len(ind), 40)):
subplot(4, 10, i + 1)
imshow(immatrix[ind[i]].reshape((25, 25)))
axis('off')
show()
处理效果:
分析:
上面的code变量中包含的是每幅图像属于哪个簇。这里设定聚类数k=4,同时用Scipy的whiten()函数对数据“白化”处理,并进行归一化,使每个特征具有单位方差,得到4类。可以改变其中的参数,比如主成分数目和k,观察聚类结果有何改变。将每个簇显示在一个独立图像窗口中,且在该图形窗口中最多可以显示40幅图像。用pylab的subplot()函数来设定网格数。
主成分分析(PCA)
用途: 降维中最常用的一种手段
目标: 提取最有价值的信息(基于方差,找最大的方差方向,因为越大的方差方向,就会使数据降维后越分的开,数据分的开,就能更好的进行分类任务。)
概念: 主成分分析(Principal Component Analysis,PCA), 是一种统计方法。通过正交变换将一组可能存在相关性的变量转换为一组线性不相关的变量,转换后的这组变量叫主成分。它已经应用于人脸识别和图像压缩领域中,并且是高维数据计算模型的常用技术。 简单说是把高维数据将成低维数据,比如100000x100000的矩阵降成100000x100的。
为了便于观察上面是如何利用主成分进行聚类的,我们可以在一对主成分方向的坐标上可视化这些图像。一种方法是将图像投影到两个主成分上,改变投影为:
projected = array([dot(V[[0,2]],immatrix[i]-immean) for i in range(imnbr)])
相应的坐标(在这里 V[[0,2]] 分别是第一个和第三个主成分)。当然,也可以将其投影到所有成分上,之后挑选出需要的列。
用PIL中的ImageDraw模块进行可视化。用下面的脚本可以生成如图所示的效果:
# -*- coding: utf-8 -*-
from PCV.tools import imtools, pca
from PIL import Image, ImageDraw
from pylab import *
from PCV.clustering import hcluster
imlist = imtools.get_imlist('D:\Python Computer Vision\_selected_thumbs\_selected_thumbs')
imnbr = len(imlist)
# Load images, run PCA.
immatrix = array([array(Image.open(im)).flatten() for im in imlist], 'f')
V, S, immean = pca.pca(immatrix)
# Project on 2 PCs.
projected = array([dot(V[[0, 1]], immatrix[i] - immean) for i in range(imnbr)])
#projected = array([dot(V[[1, 2]], immatrix[i] - immean) for i in range(imnbr)])
# height and width
h, w = 1200, 1200
# create a new image with a white background
img = Image.new('RGB', (w, h), (255, 255, 255))
draw = ImageDraw.Draw(img)
# draw axis
draw.line((0, h/2, w, h/2), fill=(255, 0, 0))
draw.line((w/2, 0, w/2, h), fill=(255, 0, 0))
# scale coordinates to fit
scale = abs(projected).max(0)
scaled = floor(array([(p/scale) * (w/2 - 20, h/2 - 20) + (w/2, h/2)
for p in projected])).astype(int)
# paste thumbnail of each image
for i in range(imnbr):
nodeim = Image.open(imlist[i])
nodeim.thumbnail((25, 25))
ns = nodeim.size
box = (scaled[i][0] - ns[0] // 2, scaled[i][1] - ns[1] // 2,
scaled[i][0] + ns[0] // 2 + 1, scaled[i][1] + ns[1] // 2 + 1)
img.paste(nodeim, box)
#tree = hcluster.hcluster(projected)
#hcluster.draw_dendrogram(tree,imlist,filename='fonts.png')
figure()
imshow(img)
axis('off')
img.save('pca_font.png')
show()
分析:
这里用到了整数或 floor 向下取整除法运算符 //,通过移去小数点后面的部 分,可以返回各个缩略图在白色背景中对应的整数坐标位置。可以很清楚地看到,二维投影后相似的字体图像距离较近。
将图像区域或像素合并成有意义的部分称为图像分割。单纯在像素水平上应用 K-means可以用于一些简单图像的图像分割,但是对于复杂图像得出的结果往往是毫无意义的。要产生有意义的结果,往往需要更复杂的类模型而非平均像素色彩或空间一致性。
下面在RGB三通道的像素值上运用K-means进行聚类:
注意:
把书上的改为:dx = im.shape[0]//steps dy = im.shape[1]//steps 。如果只想得到整数的结果,丢弃分数部分,可以使用运算符 //,// 得到的是整除的结果。使用list[n]访问list元素时,必须保证n是个整数!
from scipy.cluster.vq import *
from scipy import *
from pylab import *
from PIL import Image,ImageFont
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
def clusterpixels(infile, k, steps):
im = array(Image.open(infile))
dx = im.shape[0] // steps
dy = im.shape[1] // steps
# compute color features for each region
features = []
for x in range(steps):
for y in range(steps):
R = mean(im[x * dx:(x + 1) * dx, y * dy:(y + 1) * dy, 0])
G = mean(im[x * dx:(x + 1) * dx, y * dy:(y + 1) * dy, 1])
B = mean(im[x * dx:(x + 1) * dx, y * dy:(y + 1) * dy, 2])
features.append([R, G, B])
features = array(features, 'f') # make into array
# 聚类, k是聚类数目
centroids, variance = kmeans(features, k)
code, distance = vq(features, centroids)
# create image with cluster labels
codeim = code.reshape(steps, steps)
# codeim = imresize(codeim, im.shape[:2], 'nearest')
return codeim
k = 3
infile_empire = 'D:\Data\JM.jpg'
im_empire = array(Image.open(infile_empire))
infile_boy_on_hill = 'D:\Data\pkq.jpg'
im_boy_on_hill = array(Image.open(infile_boy_on_hill))
steps = (50, 100) # image is divided in steps*steps region
print (steps[0], steps[-1])
# 显示原图empire.jpg
figure()
subplot(231)
title(u'原图', fontproperties=font)
axis('off')
imshow(im_empire)
# 用50*50的块对empire.jpg的像素进行聚类
codeim = clusterpixels(infile_empire, k, steps[0])
subplot(232)
title(u'k=3,steps=50', fontproperties=font)
# ax1.set_title('Image')
axis('off')
imshow(codeim)
# 用100*100的块对empire.jpg的像素进行聚类
codeim = clusterpixels(infile_empire, k, steps[-1])
ax1 = subplot(233)
title(u'k=3,steps=100', fontproperties=font)
# ax1.set_title('Image')
axis('off')
imshow(codeim)
# 显示原图empire.jpg
subplot(234)
title(u'原图', fontproperties=font)
axis('off')
imshow(im_boy_on_hill)
# 用50*50的窗口对empire.jpg的像素进行聚类
codeim = clusterpixels(infile_boy_on_hill, k, steps[0])
subplot(235)
title(u'k=3,steps=50', fontproperties=font)
# ax1.set_title('Image')
axis('off')
imshow(codeim)
# 用100*100的窗口对empire.jpg的像素进行聚类
codeim = clusterpixels(infile_boy_on_hill, k, steps[-1])
subplot(236)
title(u'k=3,steps=100', fontproperties=font)
axis('off')
imshow(codeim)
show()
处理效果:
分析:
K-means的输入是一个有steps×steps行的数组,数组的每一行有3列,各列分别为区域块R、G、B三个通道的像素平均值。为可视化最后的结果,用skimage.transform中的resize()函数在原图像坐标中显示这幅图像。上图显示了用50×50和100×100窗口对两幅相对比较简单的示例图像进行像素聚类后的结果。注意,K-means标签的次序是任意的。窗口对图像进行了下采样,但结果仍然是有噪声的。如果图像某些区域有空间一致性,则较容易将它们分开,如图中的皮卡丘。
层次聚类,是一种很直观的算法。顾名思义就是要一层一层地进行聚类,可以从下而上地把小的cluster合并聚集,也可以从上而下地将大的cluster进行分割。似乎一般用得比较多的是从下而上地聚集,因此这里我就只介绍这一种。
所谓从下而上地合并cluster,具体而言,就是每次找到距离最短的两个cluster,然后进行合并成一个大的cluster,直到全部合并为一个cluster。整个过程就是建立一个树结构,类似于下图。
那么,如何判断两个cluster之间的距离呢?一开始每个数据点独自作为一个类,它们的距离就是这两个点之间的距离。而对于包含不止一个数据点的 cluster,就可以选择多种方法了。最常用的,就是average-linkage,即计算两个cluster各自数据点的两两距离的平均值。类似的 还有single-linkage/complete-linkage,选择两个cluster中距离最短/最长的一对数据点的距离作为类的距离。个人经 验complete-linkage基本没用,single-linkage通过关注局域连接,可以得到一些形状奇特的cluster,但是因为太过极 端,所以效果也不是太好。
层次聚类较大的优点,就是它一次性地得到了整个聚类的过程,只要得到了上面那样的聚类树,想要分多少个cluster都可以直接根据树结构来得到结果,改变 cluster数目不需要再次计算数据点的归属。层次聚类的缺点是计算量比较大,因为要每次都要计算多个cluster内所有数据点的两两距离。另外,由 于层次聚类使用的是贪心算法,得到的显然只是局域最优,不一定就是全局最优,这可以通过加入随机效应解决,这就是另外的问题了。
层次法(Hierarchicalmethods) 先计算样本之间的距离。每次将距离最近的点合并到同一个类。然后,再计算类与类之间的距离,将距离最近的类合并为一个大类。不停的合并,直到合成了一个类。其中类与类的距离的计算方法有:最短距离法,最长距离法,中间距离法,类平均法等。比如最短距离法,将类与类的距离定义为类与类之间样本的最短距离。
层次聚类算法根据层次分解的顺序分为:自下底向上和自上向下,即凝聚的层次聚类算法和分裂的层次聚类算法(agglomerative和divisive),也可以理解为自下而上法(bottom-up)和自上而下法(top-down)。分裂层次聚类采用的就是"自顶而下"的思想,先将所有的样本都看作是同一个簇,然后通过迭代将簇划分为更小的簇,直到每个簇中只有一个样本为止。凝聚层次聚类采用的是"自底向上"的思想,先将每一个样本都看成是一个不同的簇,通过重复将最近的一对簇进行合并,直到最后所有的样本都属于同一个簇为止。
在凝聚层次聚类中,判定簇间距离的两个标准方法就是单连接(single linkage)和全连接(complete linkage)。单连接,是计算每一对簇中最相似两个样本的距离,并合并距离最近的两个样本所属簇。全连接,通过比较找到分布于两个簇中最不相似的样本(距离最远),从而来完成簇的合并。还可以通过平均连接(average linkage)。使用平均连接时,合并两个簇所有成员间平均距离最小的两个簇。
层次聚类有若干优点,例如,利用树结构可以可视化数据间的关系,并显示这些簇是如何关联的。在树中,一个好的特征向量可以给出一个很好的分离结果。另外一个优点是,对于给定的不同的阈值,可以直接利用原来的树,而不需要重新计算。不足之处在于,实际需要的聚类簇,需要选择一个合适的阈值。
示例:
# -*- coding: utf-8 -*-
import os
from PCV.clustering import hcluster
from matplotlib.pyplot import *
from numpy import *
from PIL import Image
# 创建图像列表
path = 'D:\Python Computer Vision\Star-skY'
imlist = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.jpg')]
# 提取特征向量,每个颜色通道量化成 8 个小区间
features = zeros([len(imlist), 512])
for i, f in enumerate(imlist):
im = array(Image.open(f))
# 多维直方图
h, edges = histogramdd(im.reshape(-1, 3), 8, normed=True, range=[(0, 255), (0, 255), (0, 255)])
features[i] = h.flatten()
tree = hcluster.hcluster(features)
# 设置一些(任意的)阈值以可视化聚类簇
clusters = tree.extract_clusters(0.23 * tree.distance)
# 绘制聚类簇中元素超过 3 个的那些图像
for c in clusters:
elements = c.get_cluster_elements()
nbr_elements = len(elements)
if nbr_elements > 3:
figure()
for p in range(minimum(nbr_elements, 20)):
subplot(4, 5, p + 1)
im = array(Image.open(imlist[elements[p]]))
imshow(im)
axis('off')
show()
hcluster.draw_dendrogram(tree, imlist, filename='sunset.pdf')
说明:
可见相近的图像具有相似的颜色分布。用100幅星空进行层次聚类,将RGB空间的512个小区间直方图作为每幅图像的特征向量。树中挨得近的图像具有相似的颜色分布。树状图的高和子部分由距离决定,这些都需要调整,以适应所选择的图像分辨率。随着坐标向下传递到下一级,会递归绘制出这些节点,上述代码用20×20像素绘制叶节点的缩略图,使用 get_height() 和 get_depth() 这两个辅助函数可以获得树的高和宽。
聚类算法的一个有趣的类型叫做谱聚类。谱聚类相对K均值和层次聚类来说有不同的方法。
对于n个元素的相似度矩阵(或者叫affinity matrix, 有时也叫距离矩阵)是一个有着成对相似度分数的n*n矩阵。谱聚类的这个名称是从相似度矩阵构造的矩阵的谱的使用得来。这个矩阵的特征向量被用来降维,然后再聚类。
谱聚类方法的其中一个优势是唯一的输入就是这个矩阵,并且可以被你可以想到的任何相似度度量构造出来。像K均值和层次聚类这样的方法计算特征向量的平均值,这个限制了特征(或者是描述符)对向量(为了能够计算平均值)。有了谱方法,不再需要任何类型的特征向量,只有“距离”或者“相似度”。
这里描述它是如何实现的。给定一个有着相似度分数 s i j sij sij的 n ∗ n n*n n∗n的相似度矩阵S,创建一个矩阵,叫做拉普拉斯矩阵
L = I − D − 1 / 2 S D − 1 / 2 L=I−D^{−1/2}SD^{−1/2} L=I−D−1/2SD−1/2
其中, I I I是单位矩阵, D D D是对角矩阵,对角线上的元素是 S S S对应行元素之和, D = d i a g ( d i ) D=diag(di) D=diag(di), d i = ∑ j s i j d i =∑jsij di=∑jsij 。拉普拉斯矩阵中的 D − 1 / 2 D^{−1/2} D−1/2 为:
为了简介表示,使用较小的值并且要求 s i j ⩾ 0 sij⩾0 sij⩾0。
通过计算L的特征向量和使用对应于k个最大的特征值的k个特征向量创建一个特征向量的集合(记住我们可能会没有任何东西开始)。使用k个特征向量作为列创建一个矩阵,行将被作为新的特征向量(长度为k)。这些新的特征向量可以使用类似k均值被聚类产生最终的簇。本质上,算法做的事情是把原始的数据转换成可以被更加容易聚类的新的特征向量(在一些情况下,使用在一开始不能用的聚类算法)。
对理论足够了解之后,再看看应用到实际案例中的代码。 使用在k均值中案例中使用的字体图像:
from PCV.tools import imtools, pca
from PIL import Image, ImageDraw
from pylab import *
from scipy.cluster.vq import *
imlist = imtools.get_imlist('D:\Python Computer Vision\_selected_thumbs\_selected_thumbs')
imnbr = len(imlist)
# Load images, run PCA.
immatrix = array([array(Image.open(im)).flatten() for im in imlist], 'f')
V, S, immean = pca.pca(immatrix)
# Project on 2 PCs.
projected = array([dot(V[[0, 1]], immatrix[i] - immean) for i in range(imnbr)])
n = len(projected)
# 计算距离矩阵
S = array([[sqrt(sum((projected[i] - projected[j]) ** 2))
for i in range(n)] for j in range(n)], 'f')
# 创建拉普拉斯矩阵
rowsum = sum(S, axis=0)
D = diag(1 / sqrt(rowsum))
I = identity(n)
L = I - dot(D, dot(S, D))
# 计算矩阵 L 的特征向量
U, sigma, V = linalg.svd(L)
k = 5
# 从矩阵 L 的前k个特征向量(eigenvector)中创建特征向量(feature vector) # 叠加特征向量作为数组的列
features = array(V[:k]).T
# k-means 聚类
features = whiten(features)
centroids, distortion = kmeans(features, k)
code, distance = vq(features, centroids)
# 绘制聚类簇
for c in range(k):
ind = where(code == c)[0]
figure()
gray()
for i in range(minimum(len(ind), 39)):
im = Image.open(imlist[ind[i]])
subplot(4, 10, i + 1)
imshow(array(im))
axis('equal')
axis('off')
show()
处理效果:
说明:
在上面的实验中,用两两间的欧式距离创建矩阵S,并对k个特征向量用常规的K-means进行聚类。绘制出这些聚类簇。观察到,上面分别显示出了五类,根据不同的特征向量,将相同的类聚集起来,形成这些聚类图像。