本章中實現了層次聚類算法和K均值算法,用於博客的聚類。使用的特徵爲詞向量。即特定詞在博客文章中出現的次數。
讀入數據
數據中行的第一個詞代表博客名,列的第一個詞代表單詞特徵。存儲的數字代表該詞在該博客中出現的次數。讀入該句子,用Python的list存儲。【【】,【】,【】….】用兩層鏈表結構來模擬矩陣。層次聚類算法
首先定義向量之間的相似度度量方法是皮爾森相關係數。該相關係數比歐氏距離更適合,因爲不同的博客長短不一,我們要探求的相似度是線性相關性,而非真實距離。
數據結構:
class bicluster的數據成員有
– vec:代表該聚類的特徵向量
– left:如果該節點不是葉子節點,則存儲其左孩子,否則爲None
– right:如果該節點不是葉子節點,則存儲其右孩子,否則爲None
– distance:表示合併左子樹和右子樹時,兩個特徵向量之間的距離。
– id:用來標誌該節點是葉節點還是內部節點,如果是葉節點,則爲正數,如果不是葉節點,則爲負數。打印層次聚類樹
根據最後返回的一個根節點,可以遍歷其左右子樹打印該聚類樹。層次聚類樹的缺點是耗時,時間主要消耗在相似度的計算上。
K-均值聚類
K-均值聚類需要人爲指定K值。首先隨機確定K個聚類中心的位置,隨後將每個點分配到與其距離最近的聚類中心,重新確定每個中心的位置,將中心移動到該類別的平均值出。反覆進行這一過程,直到聚類結果不再改變爲止。
詳細代碼及註釋如下:
# -*- coding: utf-8 -*-
__author__ = 'Bai Chenjia'
from PIL import Image, ImageDraw
from math import *
import random
def readfile(filename):
"""
該函數從本目錄下的 blogdata 中讀取數據
數據第一行從第二個字符串開始是列標題,列標題代表單詞;
數據從第二行開始是數據,其中每行開始第一個單詞是博客名稱,從後面開始對應詞在該博客中出現的次數,即次向量
注意:表示詞出現的個數的行號和列號分別和博客名和單詞名一一對應
:return: rownames行名(博客名), colname列名(單詞名),data每一個元素是一個list,代表本博客的詞向量
"""
fp = open(filename, 'r')
lines = [line for line in fp.readlines()]
colname = lines[0].strip().split('\t')[1:] # 列名
rownames = []
data = []
for line in lines[1:]:
rownames.append(line.strip().split('\t')[0])
data.append([float(vec) for vec in line.strip().split('\t')[1:]])
return rownames, colname, data
def pearson(v1, v2):
"""
求詞向量v1和詞向量v2的皮爾森相關係數,兩個詞向量大小相同。相關係數越大,返回值越小
:param v1: 第一個詞向量
:param v2:
:return:
"""
# 求和
sum1 = sum(v1)
sum2 = sum(v2)
# 求平方和
sum1Sq = sum([pow(w1, 2) for w1 in v1])
sum2Sq = sum([pow(w2, 2) for w2 in v2])
# 求乘積之和
pSum = sum([v1[i] * v2[i] for i in range(len(v1))])
num = pSum - (sum1 * sum2 / len(v1))
den = sqrt((sum1Sq - pow(sum1, 2) / len(v1)) * (sum2Sq - pow(sum2, 2)/len(v1)))
if den == 0:
return 0
else:
return 1.0 - num/den
class bicluster:
"""
存儲每個簇,其屬性包括
left:多個簇聚成的簇中最小的簇標號 right:多個簇聚成的簇中最大的簇標號
vec:詞向量 id:簇標號,原始簇標號爲正,聚集後的簇標號爲負 distance:當前合併的簇之間的距離
"""
def __init__(self, vec, left=None, right=None, distance=0.0, iid=None):
self.left = left
self.right = right
self.vec = vec
self.id = iid
self.distance = distance
def hcluster(rows, cal_distance=pearson):
"""
層次聚類算法聚類,兩個簇聚成一個簇之後,其詞向量用原來兩個簇的詞向量取平均值表示
:param rows: 詞向量集合,每一個元素代表一個詞向量
:return: 返回最終的簇
"""
distances = {}
currentclustid = -1
clust = [bicluster(rows[i], iid=i) for i in range(len(rows))]
while len(clust) > 1:
# 尋找距離最小的簇對
lowespair = (0, 1)
clostest = pearson(clust[0].vec, clust[1].vec)
for i in range(len(clust)):
for j in range(i+1, len(clust)):
if (clust[i], clust[j]) not in distances:
distances[(clust[i].id, clust[j].id)] = cal_distance(clust[i].vec, clust[j].vec)
d = distances[(clust[i].id, clust[j].id)] # 所合併的兩個節點的距離,即合併誤差
if d < clostest:
clostest = d
lowespair = (i, j)
# 將找到的距離最小的簇對合併爲新簇,新簇的vec爲原來兩個簇vec的平均值
mergevec = [(clust[lowespair[0]].vec[k] + clust[lowespair[1]].vec[k]) / 2 for k in range(len(clust[0].vec))]
newcluster = bicluster(mergevec, left=clust[lowespair[0]], right=clust[lowespair[1]], distance=clostest, iid=currentclustid)
currentclustid -= 1
print "本次合併的兩個簇的標號分別是", clust[lowespair[0]].id, clust[lowespair[1]].id,
print "生成的簇標號是:", currentclustid+1,
print "當前簇的個數是", len(clust)
# 刪除原來的兩個簇,添加新簇
# 注意此處必須先刪除 lowespair[1] 再刪除 lowespair[0]. 因爲 lowespair[1]的序號大於lowespair[0]
# 而刪除會導致數組個數減少,故如果先刪除序號在前的元素,會比其序號大的元素全部前移.
del clust[lowespair[1]]
del clust[lowespair[0]]
clust.append(newcluster)
return clust[0]
def printclust(clust, label=None, n=0):
"""
根據 hcluster 函數的輸出,遞歸遍歷樹,輸出層次聚類樹的結構
根據 clust 中每個元素存儲的 left 和 right 信息可以知道合併時其左右子樹,遞歸遍歷則可以輸出所有子樹
:param clust: 層次遍歷最後輸出的一個簇
:param label: 在本例中代表博客名,即聚類的對象
:param n: 在本例中代表樹的層數
:return: 輸出結構,無返回值
"""
for i in range(n): # n代表當前遍歷的層數,層數越多,前面的空格越多
print " ",
if clust.id < 0:
# 負數標記代表這是一個分支
print '-'
else:
# 正數標記代表這是一個葉節點
if label == None:
print clust.id
else:
print label[clust.id]
if clust.left != None:
printclust(clust.left, label=label, n=n+1)
if clust.right!= None:
printclust(clust.right, label=label, n=n+1)
"""
------------------------------------------------------------------
以下幾個函數利用PIL包繪製層次聚類的樹形結構
-----???此處較難設計----
"""
def getheight(clust):
"""
返回聚類樹的總體高度,即圖形的整體高度,所有分支的高度之和。本樹的高度爲 99
遞歸計算。如果該節點是葉子節點,則該節點高度爲1,否則高度爲該節點左右子樹高度之和
:param clust: clust是hcluster函數返回的層次聚類的最後一層
:return: 返回層次聚類樹的總體高度
"""
if clust.left == None and clust.right == None:
return 1
return getheight(clust.left) + getheight(clust.right)
def getdepth(clust):
"""
返回聚類樹的總體寬度,即聚類樹的層數。
一個節點的誤差深度等於其下屬的每個分支的最大可能誤差 + 自身的誤差。根節點的誤差爲0
:param clust: 根節點
:return:返回樹的總體寬度(深度)
"""
if clust.left == None and clust.right == None:
return 1
return max(getdepth(clust.left), getdepth(clust.right)) + clust.distance
def drawdendrogram(clust, labels, jpeg='clusters.jpg'):
"""
該函數調用 getheight, getdepth, drawnode 函數最終繪製出層次聚類樹
具體做法:1.首先繪製根節點和根節點的水平線 2.繪製分支節點,首先獲取左子樹和右子樹深度,再繪製到分支節點的垂直線和繪製兩條水平線
:param clust: 層次聚類結果
:param labels: 博客名
:param jpeg: 結果保存的圖像名
:return: 生成圖像保存在本地
"""
h = getheight(clust)*20
w = 1200 # 固定寬度爲1200像素
depth = getdepth(clust)
# 寬度方向的縮放因子
scaling = float(w-150)/depth
# 創建圖像,白色背景
img = Image.new('RGB', (w, h), (255, 255, 255))
draw = ImageDraw.Draw(img)
# 繪製根節點的水平線,即在高速爲 h/2 的地方繪製長度爲10個像素的水平線
draw.line((0, h/2, 10, h/2), fill=(255, 0, 0))
# 調用 diawnode 函數繪製節點
drawnode(draw, clust, 10, (h/2), scaling, labels)
# 保存圖像
img.save(jpeg, 'JPEG')
def drawnode(draw, clust, x, y, scaling, labels):
"""
??? 遞歸,繪製指定節點 clust 及其分支節點的垂直線和水平線
:param draw: 繪圖對象
:param clust: 聚類
:param x: 水平方向繪製
:param y: 垂直方向繪製
:param scaling: 縮放因子
:param labels: 博客名
:return:
"""
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))
"""
繪製層次聚類樹部分結束-------------太強大了!!! -------------------------------------------
"""
def rotatematrix(data):
"""
列聚類,將data矩陣作轉置,返回轉置後的矩陣 newdata。
newdata可以使用前面寫的 hcluster(newdata)函數 和 drawdendrogram(newclust, labels=words, jpeg='newclusters.jpg')函數聚類
"""
newdata = []
for i in range(len(data[0])): # 循環列
line = []
# 內層循環可用列表生成式 line = [data[j][i] for j in range(len(data))] 替代
for j in range(len(data)): # 循環行
line.append(data[j][i])
newdata.append(line)
return newdata
def kcluster(rows, distances=pearson, k=4):
"""
K均值聚類,針對博客名,單詞作爲向量進行聚類,k代表簇的個數
"""
# 求每行的最大值和最小值
ranges = [(min([row[i] for row in rows]), max([row[i] for row in rows]))
for i in range(len(rows[0]))]
# 隨機創建k箇中心點
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
for t in range(100): # 最多循環100次
print '循環:%d', t
#k個簇首先都初始化爲空
bestmatches = [[] for i in range(k)]
# 循環每一行,從k箇中心中查找與之最近的中心
for j in range(len(rows)):
row = rows[j]
bestmatch = 0
for i in range(k):
d = distances(clusters[i], row)
if d < distances(clusters[bestmatch], row):
bestmatch = i
bestmatches[bestmatch].append(j) # 在簇bestmatch中加入元素j
# 如果結果與上一次結果相同則結束
if bestmatches == lastmatches:
break
lastmatches = bestmatches
# 重新計算簇中心
for i in range(k):
avgs = [0.0] * len(rows[0]) # 置成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
def tanimoto(v1, v2):
"""
希望擁有兩件物品的人在物品方面互有疊加的情況下進行度量
Tanimoto係數度量代表交集與並集的比例,返回一個介於0和1之間的值,相似度越高,返回值越小
"""
c1, c2, shr = 0, 0, 0
for i in range(len(v1)):
if v1[i] != 0:
c1 += 1
if v2[i] != 0:
c2 += 1
if v1[i] != 0 and v2[i] != 0:
shr += 1
return 1.0 - (float(shr)/(c1+c2-shr))
def scaledown(data, distance=pearson, rate=0.001):
"""
用二維圖形展示二維空間中向量的位置關係
首先初始化各點,以各頂點間的目標距離作爲優化目標,計算誤差,根據誤差計算梯度
根據梯度移動各頂點,直到誤差滿足要求或達到最大迭代次數爲止
"""
n = len(data)
# 存儲兩兩點之間的目標距離
realdist = [[distance(data[i], data[j]) for j in range(n)]
for i in range(0, n)]
# 隨機初始化節點在二維空間中的起始位置
loc = [[random.random(), random.random()] for i in range(n)]
# 存儲投影到二維平面後兩兩之間的實際距離
fakedist = [[0.0 for j in range(n)] for i in range(n)]
lasterror = None # 非常小的數
for m in range(0, 1000):
print m,
#尋找投影后的位置
for i in range(n):
for j in range(n):
# 計算平面上兩向量之間的歐式距離
fakedist[i][j] = sqrt(sum([pow(loc[i][x] - loc[j][x], 2)
for x in range(len(loc[i]))]))
#移動節點,記錄四個方向的梯度
grad = [[0.0, 0.0] for i in range(n)]
totalerror = 0
# 當前需要移動的節點是k節點
for k in range(n):
# 循環遍歷其餘節點計算誤差
for j in range(n):
if j == k:
continue
errorterm = (fakedist[j][k] - realdist[j][k]) / realdist[j][k]
grad[k][0] += ((loc[k][0] - loc[j][0]) / fakedist[j][k]) * errorterm
grad[k][1] += ((loc[k][1] - loc[j][1]) / fakedist[j][k]) * errorterm
totalerror += abs(errorterm)
print lasterror, totalerror, grad[0][:], grad[1][:]
# 比較誤差較上一次增大還是減小
if lasterror and lasterror < totalerror:
break
lasterror = totalerror
# 根據rate參數與grad值相乘的結果,移動每一個節點
for k in range(n):
loc[k][0] -= rate * grad[k][0]
loc[k][1] -= rate * grad[k][1]
return loc
def draw2d(data, labels, jpeg='mds2d.jpg'):
"""
使用PIL生成一幅圖,根據新的座標值,在圖上標出所有數據項的位置及其對應的標籤
"""
img = Image.new('RGB', (2000, 2000), (255, 255, 255))
draw = ImageDraw.Draw(img)
for i in range(len(data)):
x = (data[i][0] + 0.5) * 1000
y = (data[i][1] + 0.5) * 1000
draw.text((x, y), labels[i], (0, 0, 0))
img.save(jpeg, 'JPEG')
if __name__ == '__main__':
# 讀取文件,返回博客名列表,單詞名列表和數據
blogname, words, data = readfile(filename="blogdata.txt")
"""
-----------------------------------------------------
行聚類,針對博客名,以單詞出現頻次組成的詞向量爲特徵進行聚類
----------------------------------------------------
"""
#層次聚類,返回最終得到的聚類樹的最上層(只有一個類別)
#clust = hcluster(data, cal_distance=pearson)
#不使用圖像包,簡單繪製層次聚類樹
#printclust(clust, label=blogname)
# 根據聚類返回值 clust 獲取層次聚類樹的高度
#height = getheight(clust)
#print "行聚類樹的高度是:", height
# 根據聚類返回值 clust 獲取層次層次聚類樹的深度(總體誤差)
#depth = getdepth(clust)
#print "行聚類樹的深度是:", depth
# 繪製層次聚類樹
#drawdendrogram(clust, labels=blogname, jpeg="Cluster_BlogData//clusters.jpg")
#print "行聚類層次聚類樹繪製完畢!"
"""
---------------------------------------------------------
列聚類,對單詞進行聚類,處理時只需要將 data 進行轉置,按照之前編寫的聚類函數進行聚類
轉置後的data矩陣行元素代表單詞,列元素代表博客名,特徵向量轉爲詞出現在一系列博客中的次數
由於單詞的數量多於博客的數量,因而運行時間更長
"""
#newdata = rotatematrix(data) # 反轉顏色
#newclust = hcluster(newdata, cal_distance=pearson) # 列聚類
#drawdendrogram(newclust, labels=words, jpeg='newclusters.jpg')
"""
------------------------------------------------------
K均值聚類
"""
#kclust = kcluster(data, k = 10)
#print "簇0中元素是:", [blogname[r] for r in kclust[0]][:]
"""
-------------------------------------------------------
二維數據可視化
"""
coords = scaledown(data)
draw2d(coords, blogname)