TensorFlow實現kmeans算法(字節跳動面試題)

目錄

導言

關於Kmeans

kmeans本質是EM算法的特殊情況

Kmeans收斂性證明

爲什麼在計算k-means之前要將數據點在各維度上歸一化

k-means不適用哪些數據(異常值對聚類中心影響很大,需要離羣點檢測和剔除)

K值選擇

質心選擇

時間複雜度

僞代碼

具體實現


導言

這題是我今年秋招面試字節跳動遇到的題,當時比較緊張,磕磕絆絆寫了個大概,面試完後我又整理了一下代碼,同時參考了其他一些博客的代碼,寫了一個我的實現版本供大家參考。

 

關於Kmeans

kmeans其實是一個思想非常簡單,但是深究有很多可以提問的算法。

 

kmeans本質是EM算法的特殊情況

對比混合高斯模型和K-means可以發現,混合高斯使用了“軟”指定,爲每個樣例分配的類z(i)是有一定的概率的,同時計算量也變大了,每個樣例i都要計算屬於每一個類別j的概率。而kmeans使用的是“硬”指定,對於每個樣例,它屬於一個類的概率是1,而屬於其他類的概率是0。

 

Kmeans收斂性證明

這也是面試字節跳動一輪中的一道面試題,當時沒有回答出來,感覺字節跳動面試官的水平很高,我回去之後詳細的研究了一下kmeans,收穫了許多。

最小化E是一個NP難問題,kmeans採用了貪心策略,通過迭代來近似求解,關於收斂性證明我看到最好的解答是百面機器學習上的解答:

上面把kmeans與EM算法對應上了,然後我們只需證明EM算法是收斂的即可,這裏具體可見李航老師的統計學習方法。

 

爲什麼在計算k-means之前要將數據點在各維度上歸一化

因爲數據點各維度的量級不同。

舉例來說,基於RFM模型的會員分羣,每個會員分別有R(最近一次購買距今的時長)、F(來店消費的頻率)和M(購買金額)。如果這是一家奢侈品商店,你會發現M的量級(可能幾萬元)遠大於F(可能平均10次以下),如果不歸一化就算k-means,相當於F這個特徵完全無效。如果我希望能把常客與其他顧客區別開來,不歸一化就做不到。

 

k-means不適用哪些數據(異常值對聚類中心影響很大,需要離羣點檢測和剔除)

數據特徵極強相關`的數據集,因爲會很難收斂(損失函數是非凸函數),一般要用kernal k-means,將數據點`映射到更高維度再分羣。

數據集可分出來的簇密度不一,或有很多離羣值(outliers),這時候考慮使用密度聚類。

 

K值選擇

(1)經驗法

一般來說,我們會根據對數據的`先驗經驗`選擇一個合適的k值,如果沒有什麼先驗知識,則可以通過`交叉驗證`選擇一個合適的k值。 

(2)關於肘部法則

增加簇數有助於降低每個簇的簇內方差之和,給定k>0,計算簇內方差和var(k),繪製var關於k的曲線,曲線的第一個(或最顯著的)拐點暗示正確的簇數。

 

質心選擇

(1)KMeans++算法

 1.假設分爲K類;

 2.從輸入的數據點集合中隨機選擇一個點作爲`第一個聚類中心`;

 3.對於數據集中的每一個點x,計算其與最近的聚類中心(指已選擇的聚類中心)的距離D(x);

 4.選擇一個新的數據點作爲新的聚類中心,選擇的原則是:D(x)較大的點被選取爲聚類中心的概率較大;

 5.重複3和4兩個步驟直到`K個聚類中心被選出來`;

 6.利用這K個初始的聚類中心運行標準的K-Means算法;

 簡單來說就是選擇中心點時各中心點的距離要做到儘可能的遠

(2)二分k均值(bisecting k-means)算法

主要思想:

首先將所有點作爲一個簇,然後將該簇`一分爲二`,之後選擇能最大程度降低聚類代價函數(也就是誤差平方和)的簇劃分爲兩個簇,以此進行下去,直到簇的數目等於用戶給定的數目k爲止。

隱含着一個原則:

因爲聚類的誤差平方和能夠衡量聚類性能,該值越小表示數據點越接近於它們的質心,聚類效果就越好。所以我們就需要對誤差平方和最大的簇進行再一次的劃分,因爲誤差平方和越大,表示該簇聚類越不好,越有可能是多個簇被當成一個簇了,所以我們首先需要對這個簇進行劃分。

 

時間複雜度

O(K*N)

 

僞代碼

創建 k 個點作爲起始質心 (隨機選擇):
    當任意一個點的簇分配結果發生改變的時候:
        對數據集中的每個數據點:
            對每個質心:
                計算質心與數據點之間的距離
                將數據點分配到距其最近的簇
        對每一個簇:
            求出均值並將其更新爲質心

 

具體實現

# -*- coding: utf-8 -*-
import tensorflow as tf
from random import shuffle

def kmeans(data, k, max_iter):
    """

    :param data: batch_size*feature_len
    :param k: k clusters
    :param max_iter:
    :return:
    """
    n_data = len(data)
    dim = len(data[0])

    r_indexs = [i for i in range(len(data))]
    shuffle(r_indexs)

    graph = tf.Graph()
    with graph.as_default():
        sess = tf.Session()

        centers = [tf.Variable(data[i]) for i in r_indexs[:k]]

        # 計算兩個樣本歐式距離
        v1 = tf.placeholder(tf.float64, [dim])
        v2 = tf.placeholder(tf.float64, [dim])
        euclid_dist = tf.sqrt(tf.reduce_sum(tf.pow(tf.subtract(v1, v2), 2)))

        # 獲得樣本所在簇
        cluster_dists = tf.placeholder(tf.float64, [k])
        cluster_assignment = tf.argmin(cluster_dists, axis=0)

        # assignments保存每個樣本分簇結果
        assignments = [tf.Variable(0) for i in range(n_data)]
        # 更新每個樣本分簇結果
        assignment_value = tf.placeholder(tf.int32)
        cluster_assigns = []
        for assignment in assignments:
            cluster_assigns.append(tf.assign(assignment, assignment_value))

        # 計算新的簇中心
        cluster_data = tf.placeholder(tf.float64, [None, dim])
        mean_op = tf.reduce_mean(cluster_data, axis=0)

        # 簇中心
        new_center = tf.placeholder(tf.float64, [dim])
        # 更新簇中心
        center_assign = []
        for center in centers:
            center_assign.append(tf.assign(center, new_center))


        '''
        初始化所有的狀態值
        這會幫助初始化圖中定義的所有Variables。Variable-initializer應該
        定義在所有的Variables被構造之後,這樣所有的Variables纔會被納入初始化
        '''
        init = tf.global_variables_initializer()
        sess.run(init)

        num_iter = 0
        # 開始迭代
        while num_iter < max_iter:
            # E步
            for idx in range(len(data)):
                x = data[idx]
                # 爲每個實例計算距離
                distances = [sess.run(euclid_dist, feed_dict={v1: x, v2: sess.run(center)}) for center in centers]
                # 獲得簇分配
                assignment = sess.run(cluster_assignment, feed_dict={cluster_dists: distances})
                # 更新分配結果
                sess.run(cluster_assigns[idx], feed_dict={assignment_value: assignment})

            # M步
            for cluster_idx in range(k):
                # 收集所有分配給該集羣的向量
                cluster_contain = [data[i] for i in range(len(data)) if sess.run(assignments[i]) == cluster_idx]
                # 計算新的集羣中心點
                new_location = sess.run(mean_op, feed_dict={cluster_data: cluster_contain})
                # 更新中心點
                sess.run(center_assign[cluster_idx], feed_dict={new_center: new_location})

            num_iter += 1

        centers = sess.run(centers)
        assignments = sess.run(assignments)
        return centers, assignments

 

發佈了84 篇原創文章 · 獲贊 23 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章