6.1 K-means聚类
K-means 是一种将输入数据划分成k个簇的简单的聚类算法。K-means反复提炼初 始评估的类中心,步骤如下:
(1) 以随机或猜测的方式初始化类中心
;
(2) 将每个数据点归并到离它距离最近的类中心所属的类
;
(3) 对所有属于该类的数据点求平均,将平均值作为新的类中心;
(4) 重复步骤(2)和步骤(3)直到收敛。
K-means 试图使类内总方差最小:
是输入数据,并且是矢量。该算法是启发式提炼算法,在很多情形下都适用,但 是并不能保证得到最优的结果。为了避免初始化类中心时没选取好类中心初值所造 成的影响,该算法通常会初始化不同的类中心进行多次运算,然后选择方差V最小 的结果。
6.1.1 SciPy聚类包
SciPy矢量量化包 scipy.cluster.vq 中有 K-means 的实现,下面是使用方法。
from scipy.cluster.vq import *
import numpy as np
import matplotlib.pyplot as plt
class1 = 1.5 * np.random.randn(100,2)
class2 = np.random.randn(100,2) + np.array([5,5])
features = np.vstack((class1,class2))
centroids , variance = kmeans(features,2)
code, distance = vq(features,centroids)
plt.figure()
ndx = np.where(code == 0)[0]
plt.plot(features[ndx,0],features[ndx,1],'*')
ndx = np.where(code == 1)[0]
plt.plot(features[ndx,0],features[ndx,1],'r.')
plt.plot(centroids[:,0],centroids[:,1],'go')
plt.axis('off')
plt.show()
绘制出的结果如图所示。
6.1.2 图像聚类
利用之前计算过的前40个主 成分进行投影,用投影系数作为每幅图像的向量描述符。用pickle模块载入模型文 件,在主成分上对图像进行投影,然后用下面的方法聚类:
import imtools
import pickle
from scipy.cluster.vq import *
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
# 获取selected-fontimages 文件下图像文件名,并保存在列表中
imlist = imtools.get_imlist('selected_fontimages/')
imnbr = len(imlist)
# 载入模型文件
with open('a_pca_modes.pkl','rb') as f:
immean = pickle.load(f)
V = pickle.load(f)
# 创建矩阵,存储所有拉成一组形式后的图像
immatrix = np.array([np.array(Image.open(im)).flatten() for im in imlist],'f')
# 投影到前40个主成分上
immean = immean.flatten()
projected = np.array([np.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 = np.where(code==k)[0]
plt.figure()
plt.gray()
for i in range(np.minimum(len(ind),40)):
plt.subplot(4,10,i+1)
plt.imshow(immatrix[ind[i]].reshape((25,25)))
plt.axis('off')
plt.show()
实验效果:
6.1.3 在主成分上可视化图像
为了便于观察上面是如何利用主成分进行聚类的,我们可以在一对主成分方向的坐 标上可视化这些图像。一种方法是将图像投影到两个主成分上,改变投影为:
projected = np.array([np.dot(V[[0,2]],immatrix[i]-immean) for i in range(imnbr)])
from PIL import Image,ImageDraw
import numpy as np
projected = np.array([np.dot(V[[0,2]],immatrix[i]-immean) for i in range(imnbr)])
# 高和宽
h,w = 1200,1200
# 创建一幅白色背景图
img = Image.new('RGB',(w,h),(255,255,255))
draw = ImageDraw.Draw(img)
# 绘制坐标轴
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 = abs(projected).max(0)
scaled = floor(np.array([ (p / scale) * (w/2-20,h/2-20) + (w/2,h/2) for p in projected]))
# 粘贴每幅图像的缩略图到白色背景图片
for i in range(imnbr):
nodeim = Image.open(imlist[i])
nodeim.thumbnail((25,25))
ns = nodeim.size
img.paste(nodeim,(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.save('pca_font.jpg')
实验效果:
我们用到了整数或floor向下取整除法运算符//,通过移去小数点后面的部 分,可以返回各个缩略图在白色背景中对应的整数坐标位置。
6.1.4 像素聚类
将图像区域或像素合并成有意义的部分称为图像分割,除了在 一些简单的图像上,单纯在像素水平上应用K-means得出的结果往往是毫无意义 的。要产生有意义的结果,往往需要更复杂的类模型而非平均像素色彩或空间一致 性。
下面的代码示例载入一幅图像,用一个步长为steps的方形网格在图像中滑动,每 滑一次对网格中图像区域像素求平均值,将其作为新生成的低分辨率图像对应位置 处的像素值,并用K-means进行聚类:
from scipy.cluster.vq import kmeans, vq
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
# 设置图像划分的步数
steps = 50
# 读取图像并转换为数组
im = np.array(Image.open('xiaozhou.jpg'))
# 计算每个划分区域的宽度和高度
dx = im.shape[0] // steps
dy = im.shape[1] // steps
# 计算每个区域的颜色特征
features = []
for x in range(steps):
for y in range(steps):
R = np.mean(im[x*dx:(x+1)*dx, y*dy:(y+1)*dy, 0])
G = np.mean(im[x*dx:(x+1)*dx, y*dy:(y+1)*dy, 1])
B = np.mean(im[x*dx:(x+1)*dx, y*dy:(y+1)*dy, 2])
features.append([R, G, B])
features = np.array(features, 'f') # 转换为数组
# 进行聚类
centroids, variance = kmeans(features, 3)
code, distance = vq(features, centroids)
# 创建聚类标记图像
codeim = code.reshape(steps, steps)
# 对于聚类图像的配色
color_map = centroids.astype(int)
new_image = np.zeros((steps, steps, 3), dtype=np.uint8)
for i in range(steps):
for j in range(steps):
new_image[i, j] = color_map[codeim[i, j]]
plt.figure()
plt.imshow(im)
plt.axis('off') # 不显示坐标轴
plt.show()
# 显示聚类结果图像
plt.figure(figsize=(8, 8))
plt.imshow(new_image)
plt.axis('off') # 不显示坐标轴
plt.show()
实验效果:
6.2 层次聚类
层次聚类(或凝聚式聚类)是另一种简单但有效的聚类算法,其思想是基于样本间 成对距离建立一个简相似性树。该算法首先将特征向量距离最近的两个样本归并为 一组,并在树中创建一个“平均”节点,将这两个距离最近的样本作为该“平均” 节点下的子节点;然后在剩下的包含任意平均节点的样本中寻找下一个最近的对, 重复进行前面的操作。在每一个节点处保存了两个子节点之间的距离。
让我们看看层次聚类算法怎样在代码中体现,创建文件hcluster.py,将下面代码添加进去。
from itertools import combinations
import numpy as np
class ClusterNode(object):
def __init__(self,vec,left,right,distance=0.0,count=1):
self.left = left
self.right = right
self.vec = vec
self.distance = distance
self.count = count # 只用于加权平均
def extract_clusters(self,dist):
""" 从层次聚类树中提取距离小于dist的子树簇群列表"""
if self.distance < dist:
return [self]
return self.left.extract_clusters(dist) + self.right.extract_clusters(dist)
def get_cluster_elements(self):
""" 在聚类子树中返回元素的id """
return self.left.get_cluster_elements() + self.right.get_cluster_elements()
def get_height(self):
""" 返回节点的高度,高度是各分支的和"""
return self.left.get_height() + self.right.get_height()
def get_depth(self):
""" 返回节点的深度,深度是每个子节点取最大再加上它的自身距离"""
return max(self.left.get_depth(), self.right.get_depth()) + self.distance
class ClusterLeafNode(object):
def __init__(self,vec,id):
self.vec = vec
self.id = id
def extract_clusters(self,dist):
return [self]
def get_cluster_elements(self):
return [self.id]
def get_height(self):
return 1
def get_depth(self):
return 0
def L2dist(v1,v2):
return np.sqrt(sum((v1 - v2)**2))
def L1dist(v1,v2):
return sum(abs(v1-v2))
def hcluster(features,distfcn=L2dist):
""" 用层次聚类对行特征进行聚类"""
#用于保存计算出的距离
distances = {}
#每行初始化为一个簇
node = [ClusterLeafNode(np.array(f),id=i) for i,f in enumerate(features)]
while len(node)>1:
closest = float('Inf')
#遍历每对,寻找最小距离
for ni,nj in combinations(node,2):
if (ni,nj) not in distances:
distances[ni,nj] = distfcn(ni.vec,nj.vec)
d = distances[ni,nj]
if d<closest:
closest = d
lowestpair = (ni,nj)
ni,nj = lowestpair
#对两个簇求平均
new_vec = (ni.vec + nj.vec) / 2.0
#创建新的节点
new_node = ClusterNode(new_vec,left=ni,right=nj,distance=closest)
node.remove(ni)
node.remove(nj)
node.append(new_node)
return node[0]
我们为树节点创建了两个类,即ClusterNode和ClusterLeafNode,这两个类将用于 创建聚类树,其中函数hcluster()用于创建树。首先创建一个包含叶节点的列表, 然后根据选择的距离度量方式将距离最近的对归并到一起,返回的终节点即为树的 根。对于一个行为特征向量的矩阵,运行hcluster()会创建和返回聚类树。
对于每个子树,计算其所有节点特征向量的平均值,作为新的特征向 量来表示该子树,并将每个子树视为一个对象。当然,还有其他将哪两个节点合并 在一起的方案,比如在两个子树中使用对象间距离最小的单向锁,及在两个子树中 用对象间距离最大的完全锁。选择不同的锁会生成不同类型的聚类树。
全连接的凝聚层次聚类的操作步骤:
1.获取样本
随机产生5个样本,每个样本包含3个特征(x,y,z)
import pandas as pd
import numpy as np
if __name__ == "__main__":
np.random.seed(1)
# 设置特征的名称
variables = ["x", "y", "z"]
# 设置编号
labels = ["s1", "s2", "s3", "s4", "s5"]
# 产生一个(5,3)的数组
data = np.random.random_sample([5, 3]) * 10
# 通过pandas将数组转换成一个DataFrame
df = pd.DataFrame(data, columns=variables, index=labels)
# 查看数据
print(df)
2.获取所有样本的距离矩阵
通过SciPy来计算距离矩阵,计算每个样本间两两的欧式距离,将矩阵矩阵用一个DataFrame进行保存,方便查看。
#获取距离矩阵
'''
pdist:计算两两样本间的欧式距离,返回的是一个一维数组
squareform:将数组转成一个对称矩阵
'''
dist_matrix =
pd.DataFrame(squareform(pdist(df,metric="euclidean")),columns=labels,index=labels)
print(dist_matrix)
3.获取全连接矩阵的关联矩阵
通过scipy的linkage函数,获取一个以全连接作为距离判定标准的关联矩阵(linkage matrix)
# 以全连接作为距离判断标准,获取一个关联矩阵
row_clusters = linkage(dist_matrix.values, method="complete", metric="euclidean")
# 将关联矩阵转换成为一个DataFrame
clusters = pd.DataFrame(row_clusters, columns=["label 1", "label 2", "distance", "sample size"],
index=["cluster %d" % (i + 1) for i in range(row_clusters.shape[0])])
print(clusters)
4.通过关联矩阵绘制树状图
使用scipy的dendrogram来绘制树状图
row_dendr = dendrogram(row_clusters,labels=labels)
plt.tight_layout()
plt.ylabel("欧式距离")
plt.show()
在实际图像处理中的应用:
# -*- coding: utf-8 -*-
import os
import hcluster
from matplotlib.pyplot import *
from numpy import *
from PIL import Image
# 创建图像列表
path = 'F:\\Anaconda\\chapter6\\picture\\data\\sunsets\\flickr-sunsets-small'
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')
实验效果图
6.3 谱聚类
谱聚类方法是一种有趣的聚类算法,与前面K-means和层次聚类方法截然不同。对于n个元素(如n幅图像),相似矩阵(或亲和矩阵,有时也称距离矩阵)是一个 n×n 的矩阵,矩阵每个元素表示两两之间的相似性分数。谱聚类是由相似性矩阵构 建谱矩阵而得名的。对该谱矩阵进行特征分解得到的特征向量可以用于降维,然后 聚类。
给定一个n×n的相似矩阵S,为相似性分数,我们可以创建一个矩阵,称为拉普拉斯矩阵
其中,I是单位矩阵,D是对角矩阵,对角线上的元素是S对应行元素之和,,
。拉普拉斯矩阵中的
为:
创建一 个矩阵,该矩阵的各列是由之前求出的k个特征向量构成,每一行可以看做一个新 的特征向量,长度为k。这些新的特征向量可以用诸如K-means方法进行聚类,生 成最终的聚类簇。本质上,谱聚类算法是将原始空间中的数据转换成更容易聚类的 新特征向量。
下面编写使用拉普拉斯矩阵的特征向量对字体图像进行谱聚类的代码:
# -*- coding: utf-8 -*-
from PCV.tools import imtools, pca
from PIL import Image, ImageDraw
from pylab import *
from scipy.cluster.vq import *
imlist = imtools.get_imlist('F:\\Anaconda\\chapter6\\picture\\data\\fontimages\\a_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()
实验效果图: