集體智慧編程——博客文章聚類-Python實現

本章中實現了層次聚類算法和K均值算法,用於博客的聚類。使用的特徵爲詞向量。即特定詞在博客文章中出現的次數。

  1. 讀入數據
    數據中行的第一個詞代表博客名,列的第一個詞代表單詞特徵。存儲的數字代表該詞在該博客中出現的次數。讀入該句子,用Python的list存儲。【【】,【】,【】….】用兩層鏈表結構來模擬矩陣。

  2. 層次聚類算法
    首先定義向量之間的相似度度量方法是皮爾森相關係數。該相關係數比歐氏距離更適合,因爲不同的博客長短不一,我們要探求的相似度是線性相關性,而非真實距離。
    數據結構:
    class bicluster的數據成員有
    – vec:代表該聚類的特徵向量
    – left:如果該節點不是葉子節點,則存儲其左孩子,否則爲None
    – right:如果該節點不是葉子節點,則存儲其右孩子,否則爲None
    – distance:表示合併左子樹和右子樹時,兩個特徵向量之間的距離。
    – id:用來標誌該節點是葉節點還是內部節點,如果是葉節點,則爲正數,如果不是葉節點,則爲負數。

  3. 打印層次聚類樹
    根據最後返回的一個根節點,可以遍歷其左右子樹打印該聚類樹。

  4. 層次聚類樹的缺點是耗時,時間主要消耗在相似度的計算上。

  5. 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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章