機器學習實戰之集體智慧編程學習筆記(2):聚類

聚類的作用

通過聚類,我們可以跟蹤統計消費者信息,發現具有相似消費習慣的羣體,並據此開發相應的產品或者市場策略

監督學習與無監督學習

監督學習

利用樣本輸入和期望輸出來學習如何預測的技術

  • 神經網絡
  • 決策樹
  • 向量支持機
  • 貝葉斯過濾

無監督學習

無監督學習不是利用樣本進行訓練,而是要在一組數據中找尋某種結構

  • 聚類
  • 非負矩陣因式分解
  • 自組織映射

數據源

由於本文主要講述聚類,所以對數據來源不做記錄,此處提供本文數據源下載地址

讀取數據:

# 讀取博客的統計數據
def readFile():
    with open('blogdata.txt', 'r')as f:
        lines = [line for line in f]
        # 第一行數據是列名稱,去掉第一個blog字樣
        colNames = lines[0].replace('\n', '').split('\t')[1:]
        # 每一行的第一個數據是行名稱
        rowNames = []
        # data數據不包含每一行的第一列
        data = []
        for i in range(1, len(lines)):
            # 去除換行符合空格
            l = lines[i].replace('\n', '').split('\t')
            # 行名是第一個數據
            rowNames.append(l[0])
            data.append([float(x) for x in l[1:]])

        return colNames, rowNames, data

聚類分類

  • 分級聚類
  • k-均值聚類
  • 二位空間聚類

分級聚類

通過連續的把最相近的羣組合併爲新的羣組來構造一個全新的羣組,每一個羣組都是從單一元素開始的,其過程如下圖:

分級聚類
在上圖中,每次都把最近的兩個元素放在一起,合成一個新的元素,然後求得這個元素裏兩個元素的平均值作爲新的值,再重複上面過程直到最後只剩下一個元素就可以了,而所有的元素內容都會保存在我們的數據中
對於數據的緊密度,我們依然採用上一篇提到過的皮爾遜係數:

# 比較數據d1和d2的相似度
def pearson(d1, d2):
    # 求和
    sum1 = sum(d1)
    sum2 = sum(d2)
    # 平方和
    sumSq1 = sum([pow(v, 2) for v in d1])
    sumSq2 = sum([pow(v, 2) for v in d2])

    # 乘積之和
    pSum = sum([d1[i] * d2[i] for i in range(len(d1))])

    num = pSum - (sum1 * sum2 / len(d1))
    den = math.sqrt(((sumSq1 - pow(sum1, 2) / len(d1)) * (sumSq2 - pow(sum2, 2) / len(d2))))

    if den == 0: return 0

    # num/den得到皮爾遜係數,這個數字越大表示兩個數據集相似度越高
    # 用1-num/den得到的結果表示兩個數據集之間的距離,相似度越高距離越近
    return 1 - num / den

新建一個類作爲聚類的載體:

# 用來保存聚合數據的類
# vec 保存聚合數據
# left 是聚合數據的左節點
# right 是聚合數據的右節點
# id 可以用來判斷數據是原始數據還是聚合數據,如果是原始數據還可以根據id獲取對應的行名稱
# distance中保存原始數據的距離
class bicluster:
    def __init__(self, vec, left=None, right=None, id=None, distance=None):
        self.vec = vec
        self.left = left
        self.right = right
        self.id = id
        self.distance = distance

有了上面的載體,我們只需要循環把兩個最相近的組聚合,然後重複這個過程,就能得到一個包含了所有數據的最終聚類

# 分級聚類,將數據聚合成一個bicluster對象
def hcluster(data, distance=pearson):
    distances = {}
    currentclustId = -1
    # 原始的聚類就是所有數據的集合
    clust = [bicluster(data[i], id=i) for i in range(len(data))]
    # 大循環
    while len(clust) > 1:

        # 默認0/1是每次大循環開始時最近的數據
        # lowestpair保存最近的一組數據,closest保存他們的距離
        lowestpair = (0, 1)
        closest = distance(clust[0].vec, clust[1].vec)
        # 兩次循環保證所有數據可以比較
        for i in range(len(clust)):
            for j in range(len(clust)):
                # 不跟自己比
                if i == j: continue
                # 如果當前數據沒有計算過才計算,不直接用i,j是因爲聚合之後i,j就不跟原始的數據對應了
                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:
                    # 在這個大循環結束之前,i/j組合還可以代表最近的組
                    lowestpair = (i, j)
                    closest = d

        # 獲取當前最近組的所有項的平均值
        mergevec = [(clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i]) / 2 for i in range(len(data[0]))]
        # 構造新的組,這個組中包含了子數據的所有信息
        newclust = bicluster(mergevec, left=clust[lowestpair[0]], right=clust[lowestpair[1]], id=currentclustId,
                             distance=closest)

        # 清除原始數據組,加入新數據
        currentclustId -= 1
        del clust[lowestpair[1]]
        del clust[lowestpair[0]]
        clust.append(newclust)
    print(clust[0])
    return clust[0]

通過循環執行,得到了唯一的聚類,我們可以來嘗試一下使用:

colNames, rowNames, data = readFile()
hcluster(data)

這個過程會消耗一定的時間,結果會打印出來一個bicluster的對象

但是這樣一個對象並不能讓我們直觀的感受到各個數據之間的關係,所以我們需要想辦法使聚類的結構關係可視化,此處引入一個很好用的Python圖像處理庫PythonImagingLibrary,簡稱PIL,如果對這個庫不瞭解的可以在網上學習一下簡單的使用,此處不做過多介紹

要繪製圖片,我們需要知道各個元素的高度和圖片的寬度,由於線條的長度會根據原始數據的誤差進行調整,所以我們還需要計算出總得誤差並據此生成一個誤差因子:

# 獲取聚類的高度
def getHeight(bicluster):
    # 是原始數據,高度爲1
    if bicluster.left is None and bicluster.right is None:
        return 1
    # 非原始數據,高度是兩個子數據高度之和
    else:
        return getHeight(bicluster.left) + getHeight(bicluster.right)

#獲取聚類的誤差
def getDepth(bicluster):
    # 原始數據誤差爲0
    if bicluster.left is None and bicluster.right is None:
        return 0
    # 聚合數據取誤差較大者
    else:
        return max(getDepth(bicluster.left),        getDepth(bicluster.right)) + bicluster.distance

這樣我們就可以開始繪圖了:

# 繪製圖片
def drawDendrogram(bicluster, labels, jpge='clusters.jpeg'):
    # 設置寬高數據
    h = getHeight(bicluster) * 20
    w = 1200
    depath = getDepth(bicluster)
    # 寬度固定,所有留一點額外的空間
    scaling = float((w - 150) / depath)

    image = Image.new('RGB', (w, h), (255, 255, 255))
    draw = ImageDraw.Draw(image)

    draw.line((0, h / 2, 10, h / 2), (255, 0, 0))

    print('draw start...')
    drawNode(bicluster, draw, 10, h / 2, scaling, labels)
    image.save(jpge, 'JPEG')

#遞歸繪製細節
def drawNode(bicluster, draw, x, y, scaling, labels):
    # 原始數據,顯示文字即可
    if bicluster.left is None and bicluster.right is None:
        draw.text((x + 5, y - 7), labels[bicluster.id], (0, 0, 0))
    # 聚合數據,根據聚合兩個元素的距離來畫
    else:
        h1 = getHeight(bicluster.left) * 20
        h2 = getHeight(bicluster.right) * 20
        # 留出兩個子元素高度的空隙
        top = y - (h1 + h2) / 2
        bottom = y + (h1 + h2) / 2
        # 畫出豎直的線,高度是兩個子元素高度的一半
        draw.line((x, top + h1 / 2, x, bottom - h2 / 2), fill=(255, 0, 0))

        # 畫出水平的線,寬度是縮放係數X距離
        ll = scaling * bicluster.distance
        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(bicluster.left, draw, x + ll, top + h1 / 2, scaling, labels)
        drawNode(bicluster.right, draw, x + ll, bottom - h2 / 2, scaling, labels)

完成上面的代碼之後就可以把我們得到的分級聚類結果顯示出來了,還不趕緊試試

有時候我們不僅想獲得行元素的聚類,也想看一下列元素的聚類結果,對應我們這次操作的數據來看,行元素的聚類可以讓我們查看博客之間的相似性,但是對列元素也就是詞組的相似性分析有時候對我們也很有意義,所以我們可以對數據做一下矩陣轉換,再對變換過的矩陣做相同的操作,得到我們想要的結果:

def translateXY(data):
    result = []
    # 獲取列數,用此進行循環,每個新的組包含了原來的一列元素
    for x in range(len(data[0])):
        # x代表第x列,y代表第y行,這樣把每一列的元素都取出來形成新的數組
        newrow = [data[y][x] for y in range(len(data))]
        # 這樣添加的數據,他的列數和他的索引是相同的
        result.append(newrow)

    return result

一起調用一下看看:

colNames, rowNames, data = readFile()
drawDendrogram(hcluster(data), rowNames)
drawDendrogram(hcluster(translateXY(data)), colNames, jpge='trans_clusters.jpeg')
print('end')

怎麼樣,同級目錄下是不是多出來兩張圖片,顯示了分組的詳細情況?

優點:形象,直觀

缺點:並沒有真正將數據分組,計算量比較大,很耗時

k-均值聚類

k-均值聚類首先確認每一列元素的範圍(計算最大值最小值),然後隨機生成k個行,這些行裏的每一個列元素都在範圍之內

之後我們可以根據元素和這k個隨機行之間的距離把元素分爲k組,得到了k個組之後我們再把這k個組的所有數據取平均值,就得到了新的組,然後以新的組爲中心,不斷的重複運算直到組不再變化,就得到了k個組,過程如下:

k-均值聚類

代碼實現:

# k均值聚類
def kclust(data, rowNames, distance=pearson, k=5):
    # 存一下列數,經常要用
    col_num = len(data[0])
    # 隨機列的數據
    randomrows = []
    # 通過k-v的形式存儲每個隨機聚點下的子數據
    last_clusts = {}
    new_clusts = {}

    # 循環列,拿到每一列的最大值和最小值
    # for x in range(len(data[0])):
    #     # 最大值
    #     col_max = max([row[x] for row in data])
    #     # 最小值
    #     col_min = min([row[x] for row in data])
    #     # 每一列對應的正好是索引
    #     max_min.append((col_max, col_min))
    # 簡寫如下:
    # 存儲每一列的最大值和最小值
    max_min = [(max([row[x] for row in data]), min([row[x] for row in data])) for x in range(col_num)]

    # 隨機k個行數據
    for i in range(k):
        # max_min[j][0]-max_min[j][1]表示取最大值和最小值的差值,在用這個值X隨機數,在加上最小值
        # 得到了最大值和最小值之間的一個隨機值
        # 把上述過程進行列數個次數,就得到了一個隨機行
        random_row = [(random.randint(0, 1) * (max_min[j][0] - max_min[j][1]) + max_min[j][1]) for j in range(col_num)]
        randomrows.append(random_row)

    # 大循環進行到數據不再更改
    while True:
        for i in range(k):
            new_clusts[i] = []

        # 拿每一行去跟隨機行比,找到最近的,算進他的組裏
        for i in range(len(data)):
            # 默認最近的是第一個隨機行
            c_index = 0
            closest = distance(data[i], randomrows[c_index])
            for j in range(1, k):
                d = distance(randomrows[j], data[i])
                # 找到了更近的
                if d < closest:
                    c_index = j
                    closest = d
            # 把數據放入最近的聚點的名下
            new_clusts[c_index].append((rowNames[i], data[i]))
        # 如果重新排之後數據沒變化,說明已完成,退出循環
        if last_clusts == new_clusts: break
        # 數據複製,直接=的話會一直相同,用copy複製出來
        last_clusts = new_clusts.copy()

        # randomrows.clear()
        # for k in new_clusts:
        #
        #     # 如果組裏沒東西,過
        #     if new_clusts[k] is None or len(new_clusts[k]) == 0: continue
        #     # 對於組中的每一列,求平均值,形成一個結果組,放進原來的隨機組裏
        #     randomrows.append(
        #         [sum([row[x] for row in new_clusts[k]]) / len(new_clusts[k]) for x in range(col_num)]
        #     )
        # 簡寫如下:
        randomrows = [[sum([row[1][x] for row in new_clusts[k]]) / len(new_clusts[k]) for x in range(col_num)] for k in
                      new_clusts if new_clusts[k] is not None and len(new_clusts[k]) != 0]
    return new_clusts

由於函數使用了隨機的中心點作爲開始,所以每次聚類的結果都可能不同

對偏好的聚類

皮爾遜係數更適用於統計數據,如果我們採用的是0/1表示的有/無的數據,就需要其他的度量方法,Tanimoto係數可以滿足我們的需求

他是通過數據的交集除以並集得到元素的相關度的,結果越大說明元素越相關

def tanimoto(d1, d2):
    # r1/r2表示d1/d2中的非無數據個數,sr表示交集個數,此處我採用0表示沒有數據,1表示有
    r1, r2, sr = 0, 0, 0

    for i in range(len(d1)):
        if d1[i] == 1:
            r1 += 1
        if d2[i] == 1:
            r2 += 1
        if d1[i] == d2[i]:
            sr += 1
    # sr/(r1+r2-sr)得到的數據越大說明相似度越高,但是不利於我們看距離,
    # 所以用1-sr/(r1+r2-sr)來表示距離,值越小說明距離越近,相似度越高
    return 1.0-float(sr / (r1 + r2 - sr))

二維聚類

def scaledown(data, distance=pearson, rate=0.01):
    n = len(data)
    # 記錄上次的誤差值
    last_err = None
    # 記錄數據的真實距離,這是我們的目標結果
    realDis = [[distance(data[j], data[i]) for j in range(n)] for i in range(n)]

    # 每一列隨機生成一個座標點,代表這一列的位置
    rpoints = [[random.random(), random.random()] for i in range(n)]
    # 做一個雙層數組存儲數據信息
    fakeDis = [[[0.0] for j in range(n)] for i in range(n)]
    while True:
        # 求模擬點之間的距離,視爲當前距離
        for i in range(n):
            for j in range(n):
                fakeDis[i][j] = math.sqrt(sum([pow(rpoints[j][x] - rpoints[i][x], 2) for x in range(2)]))

        grad = [[0.0, 0.0] for i in range(n)]

        total_err = 0
        for i in range(n):
            for j in range(n):
                if i == j: continue
                # 記錄當前兩個點的誤差值
                err = (fakeDis[i][j] - realDis[i][j]) / realDis[i][j]
                # i來移動,移動的距離是i,j在x/y軸上的差值/當前距離X誤差
                grad[i][0] += ((rpoints[i][0] - rpoints[j][0]) / fakeDis[i][j]) * err
                grad[i][1] += ((rpoints[i][1] - rpoints[j][1]) / fakeDis[i][j]) * err

                total_err += abs(err)

        print(total_err)
        # 移動之後如果會更混亂,則停止
        if last_err is not None and total_err >= last_err: break
        last_err = total_err
        # 根據計算結果移動點的位置
        for i in range(n):
            rpoints[i][0] -= grad[i][0] * rate
            rpoints[i][1] -= grad[i][1] * rate

    return rpoints

繪製聚類結果

def drawPoints(points, labels, jpeg='sdc.jpeg'):
    # 白色背景圖
    image = Image.new('RGB', (2000, 2000), (255, 255, 255))
    draw = ImageDraw.Draw(image)
    # 取出移動完畢的點,拿到相應的名稱顯示出來
    for i in range(len(points)):
        x = points[i][0] * 1000
        y = points[i][1] * 1000
        draw.text((x, y), labels[i], fill=(0, 0, 0))
    image.save(jpeg)

調用

colNames, rowNames, data = readFile()
drawPoints(scaledown(data), rowNames)

思維導圖

聚類

由於代碼中都有很詳細的註解,所以沒有做過多的解釋,有問題請留言或私信解決

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