機器學習經典演算法之EM

程式設計師姜小白發表於2019-07-06

一、簡介

EM 的英文是 Expectation Maximization,所以 EM 演算法也叫最大期望演算法。

我們先看一個簡單的場景:假設你炒了一份菜,想要把它平均分到兩個碟子裡,該怎麼分?

很少有人用稱對菜進行稱重,再計算一半的分量進行平分。大部分人的方法是先分一部分到碟子 A 中,然後再把剩餘的分到碟子 B 中,再來觀察碟子 A 和 B 裡的菜是否一樣多,哪個多就勻一些到少的那個碟子裡,然後再觀察碟子 A 和 B 裡的是否一樣多……整個過程一直重複下去,直到份量不發生變化為止。

你能從這個例子中看到三個主要的步驟:初始化引數、觀察預期、重新估計。首先是先給每個碟子初始化一些菜量,然後再觀察預期,這兩個步驟實際上就是期望步驟(Expectation)。如果結果存在偏差就需要重新估計引數,這個就是最大化步驟(Maximization)。這兩個步驟加起來也就是 EM 演算法的過程。

/*請尊重作者勞動成果,轉載請標明原文連結:*/

/* https://www.cnblogs.com/jpcflyer/p/11143638.html * /

二、EM 演算法的工作原理

說到 EM 演算法,我們先來看一個概念“最大似然”,英文是 Maximum Likelihood,Likelihood 代表可能性,所以最大似然也就是最大可能性的意思。

什麼是最大似然呢?舉個例子,有一男一女兩個同學,現在要對他倆進行身高的比較,誰會更高呢?根據我們的經驗,相同年齡下男性的平均身高比女性的高一些,所以男同學高的可能性會很大。這裡運用的就是最大似然的概念。

最大似然估計是什麼呢?它指的就是一件事情已經發生了,然後反推更有可能是什麼因素造成的。還是用一男一女比較身高為例,假設有一個人比另一個人高,反推他可能是男性。最大似然估計是一種通過已知結果,估計引數的方法。

那麼 EM 演算法是什麼?它和最大似然估計又有什麼關係呢?EM 演算法是一種求解最大似然估計的方法,通過觀測樣本,來找出樣本的模型引數。

再回過來看下開頭我給你舉的分菜的這個例子,實際上最終我們想要的是碟子 A 和碟子 B 中菜的份量,你可以把它們理解為想要求得的 模型引數 。然後我們通過 EM 演算法中的 E 步來進行觀察,然後通過 M 步來進行調整 A 和 B 的引數,最後讓碟子 A 和碟子 B 的引數不再發生變化為止。

實際我們遇到的問題,比分菜複雜。我再給你舉個一個投擲硬幣的例子,假設我們有 A 和 B 兩枚硬幣,我們做了 5 組實驗,每組實驗投擲 10 次,然後統計出現正面的次數,實驗結果如下:

投擲硬幣這個過程中存在隱含的資料,即我們事先並不知道每次投擲的硬幣是 A 還是 B。假設我們知道這個隱含的資料,並將它完善,可以得到下面的結果:

我們現在想要求得硬幣 A 和 B 出現正面次數的概率,可以直接求得:

而實際情況是我不知道每次投擲的硬幣是 A 還是 B,那麼如何求得硬幣 A 和硬幣 B 出現正面的概率呢?

這裡就需要採用 EM 演算法的思想。

1. 初始化引數。我們假設硬幣 A 和 B 的正面概率(隨機指定)是θA=0.5 和θB=0.9。

2. 計算期望值。假設實驗 1 投擲的是硬幣 A,那麼正面次數為 5 的概率為:

公式中的 C(10,5) 代表的是 10 個裡面取 5 個的組合方式,也就是排列組合公式,0.5 的 5 次方乘以 0.5 的 5 次方代表的是其中一次為 5 次為正面,5 次為反面的概率,然後再乘以 C(10,5) 等於正面次數為 5 的概率。

假設實驗 1 是投擲的硬幣 B ,那麼正面次數為 5 的概率為:

所以實驗 1 更有可能投擲的是硬幣 A。

然後我們對實驗 2~5 重複上面的計算過程,可以推理出來硬幣順序應該是{A,A,B,B,A}。

這個過程實際上是通過假設的引數來估計未知引數,即“每次投擲是哪枚硬幣”。

3. 通過猜測的結果{A, A, B, B, A}來完善初始化的引數θA 和θB。

然後一直重複第二步和第三步,直到引數不再發生變化。

簡單總結下上面的步驟,你能看出 EM 演算法中的 E 步驟就是通過舊的引數來計算隱藏變數。然後在 M 步驟中,通過得到的隱藏變數的結果來重新估計引數。直到引數不再發生變化,得到我們想要的結果。

 

EM 聚類的工作原理

上面你能看到 EM 演算法最直接的應用就是求引數估計。如果我們把潛在類別當做隱藏變數,樣本看做觀察值,就可以把聚類問題轉化為引數估計問題。這也就是 EM 聚類的原理。

相比於 K-Means 演算法,EM 聚類更加靈活,比如下面這兩種情況,K-Means 會得到下面的聚類結果。

因為 K-Means 是通過距離來區分樣本之間的差別的,且每個樣本在計算的時候只能屬於一個分類,稱之為是硬聚類演算法。而 EM 聚類在求解的過程中,實際上每個樣本都有一定的概率和每個聚類相關,叫做軟聚類演算法。

你可以把 EM 演算法理解成為是一個框架,在這個框架中可以採用不同的模型來用 EM 進行求解。常用的 EM 聚類有 GMM 高斯混合模型和 HMM 隱馬爾科夫模型。GMM(高斯混合模型)聚類就是 EM 聚類的一種。比如上面這兩個圖,可以採用 GMM 來進行聚類。

和 K-Means 一樣,我們事先知道聚類的個數,但是不知道每個樣本分別屬於哪一類。通常,我們可以假設樣本是符合高斯分佈的(也就是正態分佈)。每個高斯分佈都屬於這個模型的組成部分(component),要分成 K 類就相當於是 K 個組成部分。這樣我們可以先初始化每個組成部分的高斯分佈的引數,然後再看來每個樣本是屬於哪個組成部分。這也就是 E 步驟。

再通過得到的這些隱含變數結果,反過來求每個組成部分高斯分佈的引數,即 M 步驟。反覆 EM 步驟,直到每個組成部分的高斯分佈引數不變為止。

這樣也就相當於將樣本按照 GMM 模型進行了 EM 聚類。

 

三、 如何使用 EM 工具包

在 Python 中有第三方的 EM 演算法工具包。由於 EM 演算法是一個聚類框架,所以你需要明確你要用的具體演算法,比如是採用 GMM 高斯混合模型,還是 HMM 隱馬爾科夫模型。

我們主要講解 GMM 的使用,在使用前你需要引入工具包:

1 from sklearn.mixture import GaussianMixture

我們看下如何在 sklearn 中建立 GMM 聚類。

首先我們使用 gmm = GaussianMixture(n_components=1, covariance_type=‘full’, max_iter=100) 來建立 GMM 聚類,其中有幾個比較主要的引數(GMM 類的構造引數比較多,我篩選了一些主要的進行講解),我分別來講解下:

1.n_components:即高斯混合模型的個數,也就是我們要聚類的個數,預設值為 1。如果你不指定 n_components,最終的聚類結果都會為同一個值。

2.covariance_type:代表協方差型別。一個高斯混合模型的分佈是由均值向量和協方差矩陣決定的,所以協方差的型別也代表了不同的高斯混合模型的特徵。協方差型別有 4 種取值:

covariance_type=full,代表完全協方差,也就是元素都不為 0;

covariance_type=tied,代表相同的完全協方差;

covariance_type=diag,代表對角協方差,也就是對角不為 0,其餘為 0;

covariance_type=spherical,代表球面協方差,非對角為 0,對角完全相同,呈現球面的特性。

3.max_iter:代表最大迭代次數,EM 演算法是由 E 步和 M 步迭代求得最終的模型引數,這裡可以指定最大迭代次數,預設值為 100。

建立完 GMM 聚類器之後,我們就可以傳入資料讓它進行迭代擬合。

我們使用 fit 函式,傳入樣本特徵矩陣,模型會自動生成聚類器,然後使用 prediction=gmm.predict(data) 來對資料進行聚類,傳入你想進行聚類的資料,可以得到聚類結果 prediction。

你能看出來擬合訓練和預測可以傳入相同的特徵矩陣,這是因為聚類是無監督學習,你不需要事先指定聚類的結果,也無法基於先驗的結果經驗來進行學習。只要在訓練過程中傳入特徵值矩陣,機器就會按照特徵值矩陣生成聚類器,然後就可以使用這個聚類器進行聚類了。

 

四、 如何用 EM 演算法對王者榮耀資料進行聚類

瞭解了 GMM 聚類工具之後,我們看下如何對王者榮耀的英雄資料進行聚類。

首先我們知道聚類的原理是“人以群分,物以類聚”。通過聚類演算法把特徵值相近的資料歸為一類,不同類之間的差異較大,這樣就可以對原始資料進行降維。通過分成幾個組(簇),來研究每個組之間的特性。或者我們也可以把組(簇)的數量適當提升,這樣就可以找到可以互相替換的英雄,比如你的對手選擇了你擅長的英雄之後,你可以選擇另一個英雄作為備選。

我們先看下資料長什麼樣子:

這裡我們收集了 69 名英雄的 20 個特徵屬性,這些屬性分別是最大生命、生命成長、初始生命、最大法力、法力成長、初始法力、最高物攻、物攻成長、初始物攻、最大物防、物防成長、初始物防、最大每 5 秒回血、每 5 秒回血成長、初始每 5 秒回血、最大每 5 秒回藍、每 5 秒回藍成長、初始每 5 秒回藍、最大攻速和攻擊範圍等。

具體的資料集你可以在 GitHub 上下載: https://github.com/cystanford/EM_data

現在我們需要對王者榮耀的英雄資料進行聚類,我們先設定專案的執行流程:

首先我們需要載入資料來源;

在準備階段,我們需要對資料進行探索,包括採用資料視覺化技術,讓我們對英雄屬性以及這些屬性之間的關係理解更加深刻,然後對資料質量進行評估,是否進行資料清洗,最後進行特徵選擇方便後續的聚類演算法;

聚類階段:選擇適合的聚類模型,這裡我們採用 GMM 高斯混合模型進行聚類,並輸出聚類結果,對結果進行分析。

按照上面的步驟,我們來編寫下程式碼。完整的程式碼如下:

 1 # -*- coding: utf-8 -*-
 2 
 3 import pandas as pd
 4 
 5 import csv
 6 
 7 import matplotlib.pyplot as plt
 8 
 9 import seaborn as sns
10 
11 from sklearn.mixture import GaussianMixture
12 
13 from sklearn.preprocessing import StandardScaler
14 
15 # 資料載入,避免中文亂碼問題
16 
17 data_ori = pd.read_csv('./heros7.csv', encoding = 'gb18030')
18 
19 features = [u'最大生命',u'生命成長',u'初始生命',u'最大法力', u'法力成長',u'初始法力',u'最高物攻',u'物攻成長',u'初始物攻',u'最大物防',u'物防成長',u'初始物防', u'最大每 5 秒回血', u'每 5 秒回血成長', u'初始每 5 秒回血', u'最大每 5 秒回藍', u'每 5 秒回藍成長', u'初始每 5 秒回藍', u'最大攻速', u'攻擊範圍']
20 
21 data = data_ori[features]
22 
23 # 對英雄屬性之間的關係進行視覺化分析
24 
25 # 設定 plt 正確顯示中文
26 
27 plt.rcParams['font.sans-serif']=['SimHei'] # 用來正常顯示中文標籤
28 
29 plt.rcParams['axes.unicode_minus']=False # 用來正常顯示負號
30 
31 # 用熱力圖呈現 features_mean 欄位之間的相關性
32 
33 corr = data[features].corr()
34 
35 plt.figure(figsize=(14,14))
36 
37 # annot=True 顯示每個方格的資料
38 
39 sns.heatmap(corr, annot=True)
40 
41 plt.show()
42 
43 # 相關性大的屬性保留一個,因此可以對屬性進行降維
44 
45 features_remain = [u'最大生命', u'初始生命', u'最大法力', u'最高物攻', u'初始物攻', u'最大物防', u'初始物防', u'最大每 5 秒回血', u'最大每 5 秒回藍', u'初始每 5 秒回藍', u'最大攻速', u'攻擊範圍']
46 
47 data = data_ori[features_remain]
48 
49 data[u'最大攻速'] = data[u'最大攻速'].apply(lambda x: float(x.strip('%'))/100)
50 
51 data[u'攻擊範圍']=data[u'攻擊範圍'].map({'遠端':1,'近戰':0})
52 
53 # 採用 Z-Score 規範化資料,保證每個特徵維度的資料均值為 0,方差為 1
54 
55 ss = StandardScaler()
56 
57 data = ss.fit_transform(data)
58 
59 # 構造 GMM 聚類
60 
61 gmm = GaussianMixture(n_components=30, covariance_type='full')
62 
63 gmm.fit(data)
64 
65 # 訓練資料
66 
67 prediction = gmm.predict(data)
68 
69 print(prediction)
70 
71 # 將分組結果輸出到 CSV 檔案中
72 
73 data_ori.insert(0, '分組', prediction)
74 
75 data_ori.to_csv('./hero_out.csv', index=False, sep=',')

執行結果如下:

1 [28 14  8  9  5  5 15  8  3 14 18 14  9  7 16 18 13  3  5  4 19 12  4 12
2 
3 12 12  4 17 24  2  7  2  2 24  2  2 24  6 20 22 22 24 24  2  2 22 14 20
4 
5 14 24 26 29 27 25 25 28 11  1 23  5 11  0 10 28 21 29 29 29 17]

同時你也能看到輸出的聚類結果檔案 hero_out.csv(它儲存在你本地執行的資料夾裡,程式會自動輸出這個檔案,你可以自己看下)。

我來簡單講解下程式的幾個模組。

 

關於引用包

首先我們會用 DataFrame 資料結構來儲存讀取的資料,最後的聚類結果會寫入到 CSV 檔案中,因此會用到 pandas 和 CSV 工具包。另外我們需要對資料進行視覺化,採用熱力圖展現屬性之間的相關性,這裡會用到 matplotlib.pyplot 和 seaborn 工具包。在資料規範化中我們使用到了 Z-Score 規範化,用到了 StandardScaler 類,最後我們還會用到 sklearn 中的 GaussianMixture 類進行聚類。

 

資料視覺化的探索

你能看到我們將 20 個英雄屬性之間的關係用熱力圖呈現了出來,中間的數字代表兩個屬性之間的關係係數,最大值為 1,代表完全正相關,關係係數越大代表相關性越大。從圖中你能看出來“最大生命”“生命成長”和“初始生命”這三個屬性的相關性大,我們只需要保留一個屬性即可。同理我們也可以對其他相關性大的屬性進行篩選,保留一個。你在程式碼中可以看到,我用 features_remain 陣列保留了特徵選擇的屬性,這樣就將原本的 20 個屬性降維到了 13 個屬性。

 

關於資料規範化

我們能看到“最大攻速”這個屬性值是百分數,不適合做矩陣運算,因此我們需要將百分數轉化為小數。我們也看到“攻擊範圍”這個欄位的取值為遠端或者近戰,也不適合矩陣運算,我們將取值做個對映,用 1 代表遠端,0 代表近戰。然後採用 Z-Score 規範化,對特徵矩陣進行規範化。

 

在聚類階段

我們採用了 GMM 高斯混合模型,並將結果輸出到 CSV 檔案中。

這裡我將輸出的結果擷取了一段(設定聚類個數為 30):

第一列代表的是分組(簇),我們能看到張飛、程咬金分到了一組,牛魔、白起是一組,老夫子自己是一組,達摩、典韋是一組。聚類的特點是相同類別之間的屬性值相近,不同類別的屬性值差異大。因此如果你擅長用典韋這個英雄,不防試試達摩這個英雄。同樣你也可以在張飛和程咬金中進行切換。這樣就算你的英雄被別人選中了,你依然可以有備選的英雄可以使用。

 

相關文章