第三章主要讲述了利用分级聚类,K均值聚类来发现群组的过程。聚类是寻找紧密相关的事人或者观点,并将其可视化的方法,通过数据聚类可以将相似度很高的项目聚集在一起,属于一种无监督学习,聚类在机器学习中的应用十分广泛。比如可以通过聚类来发现数据的分布特征,通过聚类可以寻找相似用户等等。本章主要通过一个对博客进行聚类的来说明聚类的过程,并在聚类的基础上将数据进行了可视化。
1.单词向量
对博客进行聚类属于文本分析的过程,但是在聚类的过程中实际参与计算的通常都是数字,所以这里我们需要对原始博客进行一个处理,将文本向量化,对于每一个在博客中出现的单词,用出现的次数作为一个特征,所有的单词放在一起组成一个特征向量,当然,预先可以选择特定的单词作为统计标准,不要求每一个在文章中出现的单词都要参与到向量化的过程中。就像我们在第二章中对喜好的数字化一样。下图是一个例子说明:
对于第一篇文档,可以用向量(0,3,3,0,...)来进行表示,在本实验中,采用的数据集也正是这种形式。
2.分级聚类
分级聚类是连续的将最为相似的 群组两两进行合并,来构造出一个群组的层级结构,其中的每一个群组都是从单一元素开始的,在本实验中,这个单一的元素就是每一篇博客,计算每两篇博客之间的相似度,将最为相似的两篇博客进行合并,这个过程一直持续下去,知道最终剩下一个群组为止。整个过程如下图所示:
在本例中,最开始的时候ABCDE是一个不同的群组,第一次将AB进行聚类,之后再对C合并。。
对博客进行聚类,我们首先需要将数据集读入到内存中,数据集的格式如下:
第一行表示统计文本特性时指定的单词,第一列表示博客名称,利用一个列表将所有的数据读入到内存。
def readfile(filename):
lines = [line for line in open(filename, encoding='utf-8')]
# 第一行是列标题
colnames = lines[0].strip().split('\t')[1:]
rownames = []
data = []
for line in lines[1:]:
p = line.strip().split('\t')
# 每一行的第一行是行名
rownames.append(p[0])
# 剩余的部分是该行对应的数据
data.append([float(x) for x in p[1:]])
return rownames, colnames, data
将文本数据数字化之后读入到列表之后,为了聚类,首先要衡量两个文本之间的相似度,在这里也就是两个向量之间的相似性,利用皮尔逊相关系数来衡量相似度。
计算相似性
# 利用皮尔逊相关系数来计算紧密度,传入的参数v1和v2分别为两个向量
def person(v1, v2):
# 计算各自的和
sum1 = sum(v1)
sum2 = sum(v2)
# 计算平方和
sqsum1 = sum([pow(x, 2) for x in v1])
sqsum2 = sum([pow(x, 2) for x in v2])
# 计算二者乘积的和
psum = sum([v1[i] * v2[i] for i in range(len(v1))])
# 计算皮尔逊相关系数
num = psum - sum1 * sum2 / len(v1)
den = sqrt((sqsum1 - pow(sum1, 2) / len(v1)) * (sqsum2 - pow(sum2, 2) / len(v1)))
if den == 0:
return 0
return 1.0 - num / den
为了最后将数据可视化成一个层次结构,我们利用面向对象的思想,将每一个节点都看作是一个对象,每一个对象有代表自身的一个向量,有一个左分支和右分支和一个id,定义节点类如下:
# 为每一个节点建立一个类
class bicluster:
def __init__(self, vec, left=None, right=None, distance=0.0, id=None):
self.left = left
self.right = right
self.distance = distance
self.id = id
self.vec = vec
分级聚类算法以一组对应原始数据项的数据聚类开始,每一次循环计算每一对之间的相似度 ,选择相似度最高的一组作为要聚类的对象,聚类之后,两个项目会被合并为一个项目,并计算二者的平均值作为新的项目所对应的向量。这个过程会一直持续下去,知道最后形成一个群组为止。
# 进行分级聚类的过程
# rows: 所用的行数据
# distance: 计算距离函数
def hcluster(rows, distance=person):
distances = {} # 计算每一对的距离结果
currentclusterid = -1
# 最起初的聚类就是rows中的每一行的数据,每一行为一类
clust = [bicluster(rows[i], id=i) for i in range(len(rows))]
while (len(clust) > 1):
lowestpair = (0, 1) # 假定距离最小的id为0和1
closest = distance(clust[0].vec, clust[1].vec)
# 遍历每一组节点,找到距离最近的一组节点
for i in range(len(clust)):
for j in range(i + 1, len(clust)):
# 利用distances字典来保存两个节点之间的距离值
if (clust[i].id, clust[j].id) not in distances:
distances[(clust[i].id, clust[j].id)] = distance(clust[i].vec, clust[j].vec)
d = distances[(clust[i].id, clust[j].id)]
if d < closest:
closest = d
lowestpair = (i, j)
# 计算两个聚类结果之间的平均值
mergevec = [(clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i]) / 2.0 for i in range(len(clust[0].vec))]
# 建立新的聚类
newcluster = bicluster(mergevec,
left=clust[lowestpair[0]],
right=clust[lowestpair[1]],
distance=closest,
id=currentclusterid)
# 不在原始集合中的聚类,其id设置为负数
currentclusterid -= 1
del clust[lowestpair[1]]
del clust[lowestpair[0]]
clust.append(newcluster)
# 返回最后的聚类结果
return clust[0]
最后返回的clust[0]就是最终的聚类结果,可以递归搜索由该函数最终返回的聚类,可以重建所有的聚类及节点。
为了可视化聚类结果,我们采用树状图的形式来展现最终的结果。
# 求取每一个节点的高度,如果是叶子节点,那么高度就是1,如果不是叶子节点,那么高度就是所有的分支高度之和
def getheight(clust):
if clust.left == None and clust.right == None:
return 1
else:
return getheight(clust.left) + getheight(clust.right)
# 求取每个节点的误差深度
def getdepth(clust):
if clust.left == None and clust.right == None:
return 0;
else:
return max(getdepth(clust.left), getdepth(clust.right)) + clust.distance
def drawdendrogram(clust, labels, jpeg='clusters.jpg'):
h = getheight(clust) * 20
w = 1200
depth = getdepth(clust)
scaling = float(w - 150) / depth
img = Image.new('RGB', (w, h), (255, 255, 255))
draw = ImageDraw.Draw(img)
draw.line((0, h / 2, 10, h / 2), fill=(255, 0, 0))
drawnode(draw, clust, 10, (h / 2), scaling, labels)
img.save(jpeg, 'JPEG')
def drawnode(draw, clust, x, y, scaling, labels):
if clust.id < 0:
h1 = getheight(clust.left) * 20
h2 = getheight(clust.right) * 20
top = y - (h1 + h2) / 2
bottom = y + (h1 + h2) / 2
ll = clust.distance * scaling
draw.line((x, top + h1 / 2, x, bottom - h2 / 2), fill=(255, 0, 0))
draw.line((x, top + h1 / 2, x + ll, top + h1 / 2), fill=(255, 0, 0))
draw.line((x, bottom - h2 / 2, x + ll, bottom - h2 / 2), fill=(255, 0, 0))
drawnode(draw, clust.left, x + ll, top + h1 / 2, scaling, labels)
drawnode(draw, clust.right, x + ll, bottom - h2 / 2, scaling, labels)
else:
draw.text((x + 5, y - 7), labels[clust.id], (0, 0, 0))
运行程序,最后得到的结果如下所示():
其实分级聚类的思想和简单,就是不断的发现两个最为相似的项目,不但得进行合二为一的过程,知道最后剩余一个。
3.列聚类
注意到,我们在上面对博客文档进行聚类的过程中,每一次聚类合并的两篇文章,具体到程序中,也就是对博客单词矩阵的每一行进行的聚类,如果现在对每一列进行聚类会产生什么呢,每一列代表的意义就是某一个单词在所有的文章中出现次数,这样每次在聚类过程中衡量的就是两个单词之间的相似性,和啤酒尿布的例子是一一样的道理,比如说在某一篇文章中出现了machine这个单词,那么在这篇文章中很大的可能性也会出现learning这个词,最后衡量的结果就是machine和learning这两个单词在所有的文章中出现的次数所构成的向量是很相似的。要实现这个过程,只需要将代表矩阵的列表做一下调整即可。
#对数据进行转置操作,对列进行聚类
def rotatematrix(data):
newdata=[]
for i in range(len (data[0])):
line=[data[j][i] for j in range(len(data))]
newdata.append(line)
return newdata
最后也可以将聚类的结果进行可视化操作,这里就不再展示了。
4.K均值聚类
层级聚类在最后的聚类结束时,将所有的项目聚类成一个结果。不同与层级聚类,K均值聚类在一开始的时候就指定了最终需要聚类的个数,也就是k的值,起初,随机的指定k个聚类中心,在每一次的迭代中,计算在k个聚类中心和每一个样本之间的相似度,将每一个样本根据相似度的大小指派给每一个聚类中心,一次迭代结束,重新计算聚类中心新的聚类中心的计算采取的是平均法则。
K均值的初始聚类中心对聚类过程的影响很大,如果初始化的聚类中心选择的比较好的话,那么可以很快聚类结束,否则可能需要更多的时间。
#K均值聚类
def kcluster(rows,distance=person,k=4):
#确定每一行的最大值和最小值
ranges = [(min([row[i] for row in rows]), max([row[i] for row in rows])) for i in range(len(rows[0]))]
#随机的选择K个初始中心,clusters是一个元素为列表的列表
#内层列表中的数值是一个每一行最小值到最大值之间的数字,与row[i]具有一样的维数
clusters=[[random.random()*(ranges[i][1]-ranges[i][0])+ranges[i][0]
for i in range(len(rows[0]))]
for j in range(k)]
lastmatches=None
#设置循环次数为100次
for t in range(100):
#bestmatches中保存的是每一个聚类心点所包含的数据行下标
#形如这样的格式bestmatches[i]表示第i类中的样本下标[[1,5,34,....],[3,8,9,....]]
bestmatches=[ [] for i in range(k)]
#为每一行分派到距离最近的中心点
for j in range(len(rows)):
row=rows[j]
bestmatch=0
#计算每一个数据点与中心点之间的距离
for i in range(k):
d=distance(clusters[i],row)
if d<distance(clusters[bestmatch],row):
bestmatch=i
bestmatches[bestmatch].append(j)
#如果下一次的聚类结果和上一次的一样的话,那么直接结束
if bestmatch==lastmatches:break
lastmatches=bestmatches
#把中心点平均化,对每一个类计算向量平均值
for i in range(k):
avgs=[0.0]*len(rows[0])
#如果某一个分类中有数据的
if len(bestmatches[i])>0:
for rowid in bestmatches[i]:
for m in range(len(rows[rowid])):
avgs[m]+=rows[rowid][m]
for j in range(len(avgs)):
avgs[j]/=len(bestmatches[i])
clusters[i]=avgs
return bestmatches
输出的一个可能的聚类结果: