目錄
k-means不適用哪些數據(異常值對聚類中心影響很大,需要離羣點檢測和剔除)
導言
這題是我今年秋招面試字節跳動遇到的題,當時比較緊張,磕磕絆絆寫了個大概,面試完後我又整理了一下代碼,同時參考了其他一些博客的代碼,寫了一個我的實現版本供大家參考。
關於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