03-0005 Kmeans算法(Python)

1.介紹

k-平均算法(英文:k-means clustering):
源於信號處理中的一種向量量化方法,現在則更多地作爲一種聚類分析方法流行於數據挖掘領域。k-平均聚類的目的是:把 個點(可以是樣本的一次觀察或一個實例)劃分到k個聚類中,使得每個點都屬於離他最近的均值(此即聚類中心)對應的聚類,以之作爲聚類的標準。
來自於維基百科[校園網進不去了]

K均值聚類算法:
是先隨機選取K個對象作爲初始的聚類中心。然後計算每個對象與各個種子聚類中心之間的距離,把每個對象分配給距離它最近的聚類中心。聚類中心以及分配給它們的對象就代表一個聚類。每分配一個樣本,聚類的聚類中心會根據聚類中現有的對象被重新計算。這個過程將不斷重複直到滿足某個終止條件。終止條件可以是沒有(或最小數目)對象被重新分配給不同的聚類,沒有(或最小數目)聚類中心再發生變化,誤差平方和局部最小。
來自於百度百科

Kmeans是比較簡單的無監督聚類算法,思想正如以上所說,沒有任何疑義。就單一的Kmeans算法而言,終止條件會是一個難點,其他都好說

2.流程

沒有繪製流程圖,在計算過程中,思想不變的前提之下,可以作調整的點有兩個,一個是終止條件,一個是劃分條件。
在此處,劃分條件是距離最小,終止條件是聚類中心不變。
也是因此實驗被簡化很多,由於Python下float若經過運算可能會出現邏輯上相等但實際上不等的情況,故這裏的聚類中心計算出來的都是整形的值。

  1. 樣本初始化,聚類中心初始化。[這裏初始化一切自己需要的值]
  2. 求出每一個樣本對每一個聚類中心的距離,取最小的作爲該樣本的分類。
  3. 對於每一類[簇]求一箇中心點,作爲新的聚類中心。
  4. 判斷聚類中心是否改變,若改變回到步驟2,否則結束。

3.優缺點

3.1優點

  1. 原理簡單,收斂較快,容易實現,代碼量小
  2. 需要調整的參數少
  3. 聚類效果比較好

3.2缺點

  1. 需要分成幾類?k未知。
  2. 對於不是凸的數據集比較難收斂。[這點我還不太懂]
  3. 對於數據本身的要求比較高。[相性不合的不好分 ]
  4. 初始聚類中心的選擇上有一定難度。[選不好會出現簇元素爲0的情況]

4.源碼

我有儘量的減少圈複雜度以及代碼行數,但顯然,這不是一個好的辦法,因爲代碼變得很難懂,而且還不想寫註釋,我覺得身爲一個程序員既要有理解任何晦澀難懂代碼的能力又要有寫出簡潔明瞭代碼的能力,但是我寫完代碼之後,就有點o((⊙﹏⊙))o···,好慘淡的人生~
能用一行解決的問題,絕對不用兩行解決,爲了以後能更好額讀懂自己寫的代碼,還是堅持寫完了註釋。

import sys
from numpy import *
import numpy as np

# 對all_input進行處理,獲取所需聚成k類、k個初始聚類中心center、需聚類的點集數據dataMat
def dataGet(all_input):
	# 數據初始化,python對應賦值的操作,習慣就好
	# l:		輸入數據的長度
	# k:		聚類中心的個數
	# s:		樣本點的個數
	# center:	用於存儲聚類中心的列表
	# dataMat:	用於存儲樣本點的列表
	[l,k,s,center,dataMat]=[len(all_input),int(all_input[0]),int(all_input[0])*2+1,[],[]]
	[center,dataMat]=[[all_input[i+1:i+3] for i in range(0,int(all_input[0])*2,2)],[all_input[i+1:i+3] for i in range(s,l-1,2)]]
	return k,center,dataMat

# 計算距離,可選擇歐幾里得距離,或自行設計距離公式,輸入爲兩個點
def distEclud(vecA, vecB):
	# 利用公式返回範數,這裏也就是兩點之間的距離
	# 參考:https://zhuanlan.zhihu.com/p/33217726
	# 參考:https://blog.csdn.net/weixin_41043240/article/details/79540034
	return np.linalg.norm(vecA-vecB)

# k-means 聚類算法,輸入爲需要聚類的點集、聚集成幾類、距離公式,需同學補充完成,返回最終中心點集centroids、點集所屬標誌矩陣clusterAssment
def kMeans(dataSet,k,center, distMeans =distEclud):
	# 分別對於數據進行初始化
	# m: 					dataSet的元素個數,有幾行
	# centroidslist: 		用於保存上次的聚類中心,以便判斷[[x,y]]
	# centroids: 			存儲聚類中心,當前循環的聚類中心[[x,y]]
	# clusterAssment: 		存儲每一個點所屬的類別,以及到該類別的距離[[label,dis]]
	# mat(zeros((m,2)): 	生成一個有m行兩列的全零矩陣
	# mat(center):			將列表轉化爲矩陣
	[m,centroidslist]=[len(dataSet),[]]
	[centroids,clusterAssment]=[mat(center),mat(zeros((m,2)))]

	# 當聚類中心不發生改變時,終止循環。列表可以直接進行比較。免去了寫循環的操作。後者轉化爲了列表
	while centroidslist!=centroids.tolist():
		# 賦值操作
		# centroidslist: 	保存當前的循環的聚類中心,一邊與下輪循環比較
		# count: 			用於存儲每個聚類中心的橫、縱座標的累加和及該簇元素個數。[sum_x,sum_y,count]
		[centroidslist,count]=[centroids.tolist(),mat(zeros((k,3)))]
		for i in range(m):
			# 初始化該點到聚類中心的距離爲無窮大,此處爲999999.0
			clusterAssment[i,1]=999999.0
			for j in range(k):
				# 獲取距離
				dis=distMeans(centroids[j,:],dataSet[i,:])
				# 只要有更小的,就進行替換
				if(dis<clusterAssment[i,1]):
					[clusterAssment[i,1],clusterAssment[i,0]]=[dis,j]

			# 爲了不讓系統多次進行類型轉化,此處將語句提取出來
			# 對於count中的數值,進行更新
			clu=int(clusterAssment[i,0])
			[count[clu,0],count[clu,1],count[clu,2]]=[count[clu,0]+dataSet[i,0],count[clu,1]+dataSet[i,1],count[clu,2]+1]

		# 更近聚類中心,如果有出現分母爲零的情況,說明該聚類中心沒有聚到任何點,[簇內無元素]
		# 這是初始聚類中心選取的不合理,是數據不好,也是Kmeans算法的缺點
		# 若有分母爲零,則將其設爲無窮大,999999.0
		# Error:can not convert Nan to integer
		for i in range(k):
			[centroids[i,0],centroids[i,1]]=[count[i,0]/count[i,2],count[i,1]/count[i,2]] if count[i,2]!=0 else [999999,999999]
		pass

	return centroids, clusterAssment

def main(all_input):
	print("input:",all_input)
	k,center,dataMat=dataGet(all_input) # 外部可以直接調用
	myCentroids,clustAssing = kMeans(mat(dataMat),k,center)#得到聚類中心,以及數據分別屬於哪一類的標誌矩
	print("ouput:",clustAssing[:,0].flatten()[0])#輸出每個樣本點屬於哪類
	print()

# 輸入
# 第一個元素表示有幾個聚類中心,向後查幾對
# 緊接着的一個元素是有幾個樣本,向後查幾對,剛好到末尾
all_input1=[2,5,7,1,5,4,1,2,3,4,5,6,7,8]
all_input2=[3,0,0,5,7,1,5,6,1,2,3,4,5,6,7,8,12,23,11,45]
all_input3=[4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4]
all_input4=[2,5,8,9,6,3,2,1,4,5,6,9]
main(all_input1)
main(all_input2)
main(all_input3)
main(all_input4)

# 輸出
# input: [2, 5, 7, 1, 5, 4, 1, 2, 3, 4, 5, 6, 7, 8]
# ouput: [[1. 1. 0. 0.]]

# input: [3, 0, 0, 5, 7, 1, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8, 12, 23, 11, 45]
# ouput: [[0. 0. 2. 2. 1. 1.]]

# input: [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
# ouput: [[0. 0. 0. 0.]]

# input: [2, 5, 8, 9, 6, 3, 2, 1, 4, 5, 6, 9]
# ouput: [[1. 0. 0.]]

# [Finished in 0.4s]

5.結果

input: [2, 5, 7, 1, 5, 4, 1, 2, 3, 4, 5, 6, 7, 8]
ouput: [[1. 1. 0. 0.]]

input: [3, 0, 0, 5, 7, 1, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8, 12, 23, 11, 45]
ouput: [[0. 0. 2. 2. 1. 1.]]

input: [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
ouput: [[0. 0. 0. 0.]]

input: [2, 5, 8, 9, 6, 3, 2, 1, 4, 5, 6, 9]
ouput: [[0. 0. 0.]]

[Finished in 0.4s]

6.總結

在該程序運行過程中本來是不應該出現某簇中無元素的現象的,而且代碼中直接將其設置爲無窮大的方法並不可取,只是課程需要,應付一下,應該找到某種策略比如取中心點或者向着某一個簇心移動。
但是如果按照自己的策略進行運行,雖然有結果,但是所分類別的編號會有所改變,在某雲上跑的時候就會失敗,故而採取等於無窮大的方法,使其結果唯一。

在寫代碼的過程中很多地方是簡化的寫法,也因此來了興致,去看了看其他博文,推薦一下:
一行 Python 代碼
Python的22個編程技巧
python的30個編程技巧
Python if 和 for 的多種寫法

python if else for 寫在一行的形式:

a=[i if i%2==0 else 2 for i in range(10)]
# [0, 2, 2, 2, 4, 2, 6, 2, 8, 2]

但是一直都只能返回一個值,不能有進行批量返回。
話說昨天看了《復聯4》,笑罷哭罷。[2019年4月25日 9:30~1:30]

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