sklearn中的EM算法

在 Python 中有第三方的 EM 算法工具包。由于 EM 算法是一个聚类框架,所以你需要明确你要用的具体算法,比如是采用 GMM 高斯混合模型,还是 HMM 隐马尔科夫模型。

  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。
你能看出来拟合训练和预测可以传入相同的特征矩阵,这是因为聚类是无监督学习,你不需要事先指定聚类的结果,也无法基于先验的结果经验来进行学习。只要在训练过程中传入特征值矩阵,机器就会按照特征值矩阵生成聚类器,然后就可以使用这个聚类器进行聚类了。

二维高斯分布

多维变量实战1 - 图1的联合概率密度函数为:
实战1 - 图2
其中:
- 实战1 - 图3:变量维度。对于二维高斯分布,有d=2;
- 实战1 - 图4:各位变量的均值;
- 实战1 - 图5:协方差矩阵,描述各维变量之间的相关度。对于二维高斯分布,有:
实战1 - 图6

数据解析

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from matplotlib.patches import Ellipse
  4. from scipy.stats import multivariate_normal
  5. plt.style.use('seaborn')
  1. def plot_cluster(X, Mu_true, Var_true, color, ang = 0):
  2. plt.figure(figsize=(10, 8))
  3. plt.axis([-10, 15, -5, 15])
  4. ax = plt.gca()
  5. plt.scatter(X[:, 0], X[:, 1], s=5, c=color)
  6. plot_args = {'fc': 'None', 'lw': 2, 'edgecolor': color, 'alpha': 0.5}
  7. ellipse = Ellipse(Mu_true, 3 * Var_true[0], 3 * Var_true[1], angle=ang, **plot_args)
  8. ax.add_patch(ellipse)
  9. plt.show()
  10. nums = [400, 600, 1000, 500]
  11. true_Mu = [[0.5, 0.5], [5.5, 2.5], [1, 7], [9, 4.5]]
  12. true_Var = [[1, 3], [2, 2], [6, 2], [1, 3]]
  13. var = [np.diag(true_Var[0]), np.diag(true_Var[1]),np.diag(true_Var[2]),np.array([[1,-1],[-1,3]])]

第一个例子
实战1 - 图7

  1. num1, mu1, var1 = nums[0], true_Mu[0], true_Var[0]
  2. X1 = np.random.multivariate_normal(mu1, var[0], num1)
  3. plot_cluster(X1, [0.5, 0.5], [1, 3], 'b')

image.png
第二个例子
实战1 - 图9

  1. num2, mu2, var2 = nums[1], true_Mu[1], true_Var[1]
  2. X2 = np.random.multivariate_normal(mu2, var[1], num2)
  3. plot_cluster(X2, [5.5, 2.5], [2,2], 'g')

image.png
第三个例子
实战1 - 图11

  1. num3, mu3, var3 = nums[2], true_Mu[2], true_Var[2]
  2. X3 = np.random.multivariate_normal(mu3, var[2], num3)
  3. plot_cluster(X3, [1, 7], [6, 2], 'r')

image.png
第四个例子
实战1 - 图13

  1. num4, mu4, var4 = nums[3], true_Mu[3], true_Var[3]
  2. X4 = np.random.multivariate_normal(mu4, var[3], num4)
  3. plot_cluster(X4, [9, 4.5], [1, 3], 'c', 25)

image.png
最后来看一下汇总图
image.png

变量初始化

首先要对GMM模型参数以及隐变量进行初始化。通常可以用一些固定的值或者随机值。

  • n_clusters是GMM模型中聚类的个数,和K-Means一样我们需要提前确定。这里我们除去倾斜的蓝色数据,所以聚类个数为3。
  • n_points是样本点的个数。
  • Mu是每个高斯分布的均值。
  • Var是每个高斯分布的方差,为了过程简便,我们这里假设协方差矩阵都是对角阵。
  • W是上面提到的隐变量,也就是每个样本属于每一簇的概率,在初始时,我们可以认为每个样本属于某一簇的概率都是1/3。
  • Pi是每一簇的比重,可以根据W求得,在初始时,Pi = [1/3, 1/3, 1/3]

    1. n_clusters = 3
    2. n_points = len(X)
    3. Mu = [[0, -1], [6, 0], [0, 9]]
    4. Var = [[1, 1], [1, 1], [1, 1]]
    5. Pi = [1 / n_clusters] * 3
    6. W = np.ones((n_points, n_clusters)) / n_clusters
    7. Pi = W.sum(axis=0) / W.sum()

    如何确定GMM中的聚类个数

  • 方法一:BIC

实战1 - 图16
L是likelihood,k是component的个数,n是样本的个数。

  • 方法二:cross validation

另一个方法是根据split test的结果(或者说cross validation的结果),先用训练集得到GMM的参数,然后再在测试集上计算log-likelihood。两者明显分叉的地方就是component个数的最佳候选。
image.png
参考:https://scikit-learn.org/stable/auto_examples/mixture/plot_gmm_selection.html#sphx-glr-auto-examples-mixture-plot-gmm-selection-py

E步骤

实战1 - 图18
E步骤中,我们的主要目的是更新W。第i个变量属于第m簇的概率为(这里的$\pi{m}$就是隐变量,我们最开始设置其为1/3,也就代表在当前的均值和方差的情况下,我们隐变量$\pi{m}的先验概率为1/3$):
实战1 - 图19
根据W,我们就可以更新每一簇的占比$\pi{m}$,
![](https://cdn.nlark.com/yuque/__latex/318e5761a8dc856c9a7cd468bc05c3da.svg#card=math&code=%5Cpi
%7Bm%7D%3D%5Cfrac%7B%5Csum%7Bi%3D1%7D%5E%7Bn%7D%20W%7Bi%2C%20m%7D%7D%7B%5Csum%7Bj%3D1%7D%5E%7Bk%7D%20%5Csum%7Bi%3D1%7D%5E%7Bn%7D%20W_%7Bi%2C%20j%7D%7D&height=54&width=164)

  1. def update_W(X, Mu, Var, Pi):
  2. n_points, n_clusters = len(X), len(Pi)
  3. pdfs = np.zeros(((n_points, n_clusters)))
  4. for i in range(n_clusters):
  5. # multivariate_normal.pdf:多元正态分布的概率密度函数
  6. pdfs[:, i] = Pi[i] * multivariate_normal.pdf(X, Mu[i], np.diag(Var[i]))
  7. W = pdfs / pdfs.sum(axis=1).reshape(-1, 1)
  8. return W
  9. def update_Pi(W):
  10. Pi = W.sum(axis=0) / W.sum()
  11. return Pi

以下是计算对数似然函数的logLH以及用来可视化数据的plotclusters。
![](https://cdn.nlark.com/yuque/__latex/fd065655c1dc29be1e025145f671fd96.svg#card=math&code=L%5Cleft%28%5Ctheta%2C%20%5Ctheta%5E%7B%5Cprime%7D%5Cright%29%3D%5Csum
%7Bi%3D1%7D%5E%7Bm%7D%20%5Csum%7Bz%5E%7B%28i%29%7D%7D%20Q%7Bi%7D%5Cleft%28z%5E%7B%28i%29%7D%5Cright%29%20%5Clog%20P%5Cleft%28x%5E%7B%28i%29%7D%2C%20z%5E%7B%28i%29%7D%20%3B%20%5Ctheta%5Cright%29&height=53&width=330)

  1. def logLH(X, Pi, Mu, Var):
  2. n_points, n_clusters = len(X), len(Pi)
  3. pdfs = np.zeros(((n_points, n_clusters)))
  4. for i in range(n_clusters):
  5. pdfs[:, i] = Pi[i] * multivariate_normal.pdf(X, Mu[i], np.diag(Var[i]))
  6. return np.mean(np.log(pdfs.sum(axis=1)))
  7. def plot_clusters(X, Mu, Var, Mu_true=None, Var_true=None):
  8. colors = ['b', 'g', 'r']
  9. n_clusters = len(Mu)
  10. plt.figure(figsize=(10, 8))
  11. plt.axis([-10, 15, -5, 15])
  12. plt.scatter(X[:, 0], X[:, 1], s=5)
  13. ax = plt.gca()
  14. for i in range(n_clusters):
  15. plot_args = {'fc': 'None', 'lw': 2, 'edgecolor': colors[i], 'ls': ':'}
  16. ellipse = Ellipse(Mu[i], 3 * Var[i][0], 3 * Var[i][1], **plot_args)
  17. ax.add_patch(ellipse)
  18. if (Mu_true is not None) & (Var_true is not None):
  19. for i in range(n_clusters):
  20. plot_args = {'fc': 'None', 'lw': 2, 'edgecolor': colors[i], 'alpha': 0.5}
  21. ellipse = Ellipse(Mu_true[i], 3 * Var_true[i][0], 3 * Var_true[i][1], **plot_args)
  22. ax.add_patch(ellipse)
  23. plt.show()

M步骤

M步骤中,我们需要根据上面一步得到的W来更新均值Mu和方差Var。 Mu和Var是以W的权重的样本X的均值和方差。
因为这里的数据是二维的,第m簇的第k个分量的均值,
实战1 - 图20
第m簇的第k个分量的方差,
实战1 - 图21
以上迭代公式写成如下函数update_Mu和update_Var。

  1. def update_Mu(X, W):
  2. n_clusters = W.shape[1]
  3. Mu = np.zeros((n_clusters, 2))
  4. for i in range(n_clusters):
  5. Mu[i] = np.average(X, axis=0, weights=W[:, i])
  6. return Mu
  7. def update_Var(X, Mu, W):
  8. n_clusters = W.shape[1]
  9. Var = np.zeros((n_clusters, 2))
  10. for i in range(n_clusters):
  11. Var[i] = np.average((X - Mu[i]) ** 2, axis=0, weights=W[:, i])
  12. return Var

迭代求解

下面我们进行迭代求解。
图中实线是真实的高斯分布,虚线是我们估计出的高斯分布。可以看出,经过5次迭代之后,两者几乎完全重合。

  1. loglh = []
  2. for i in range(5):
  3. plot_clusters(X, Mu, Var, [mu1, mu2, mu3], [var1, var2, var3])
  4. loglh.append(logLH(X, Pi, Mu, Var))
  5. W = update_W(X, Mu, Var, Pi)
  6. Pi = update_Pi(W)
  7. Mu = update_Mu(X, W)
  8. print('log-likehood:%.3f'%loglh[-1])
  9. Var = update_Var(X, Mu, W)

每次迭代的log-likelihood如下

  1. log-likelihood:-8.163
  2. log-likelihood:-4.701
  3. log-likelihood:-4.698
  4. log-likelihood:-4.697
  5. log-likelihood:-4.697

image.png
image.png
image.png
image.png
image.png

完整代码

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from matplotlib.patches import Ellipse
  4. from scipy.stats import multivariate_normal
  5. plt.style.use('seaborn')
  6. def generate_X(true_Mu, true_Var):
  7. '''
  8. 生成三个高斯分布数据
  9. :param true_Mu: 均值
  10. :param true_Var: 方差
  11. :return:
  12. '''
  13. # 第一簇的数据
  14. num1, mu1, var1 = 400, true_Mu[0], true_Var[0]
  15. X1 = np.random.multivariate_normal(mu1, np.diag(var1), num1)
  16. # 第二簇的数据
  17. num2, mu2, var2 = 600, true_Mu[1], true_Var[1]
  18. X2 = np.random.multivariate_normal(mu2, np.diag(var2), num2)
  19. # 第三簇的数据
  20. num3, mu3, var3 = 1000, true_Mu[2], true_Var[2]
  21. X3 = np.random.multivariate_normal(mu3, np.diag(var3), num3)
  22. # 合并在一起
  23. X = np.vstack((X1, X2, X3))
  24. # 显示数据
  25. plt.figure(figsize=(10, 8))
  26. plt.axis([-10, 15, -5, 15])
  27. plt.scatter(X1[:, 0], X1[:, 1], s=5)
  28. plt.scatter(X2[:, 0], X2[:, 1], s=5)
  29. plt.scatter(X3[:, 0], X3[:, 1], s=5)
  30. plt.show()
  31. return X
  32. # E步骤更新W,也就是第i个变量属于第m簇的概率
  33. # 更新W
  34. def update_W(X, Mu, Var, Pi):
  35. n_points, n_clusters = len(X), len(Pi)
  36. pdfs = np.zeros(((n_points, n_clusters)))
  37. for i in range(n_clusters):
  38. # multivariate_normal.pdf:多元正态分布的概率密度函数
  39. pdfs[:, i] = Pi[i] * multivariate_normal.pdf(X, Mu[i], np.diag(Var[i]))
  40. W = pdfs / pdfs.sum(axis=1).reshape(-1, 1)
  41. return W
  42. # 根据更新的W,更新每一簇的占比
  43. # 更新pi
  44. def update_Pi(W):
  45. Pi = W.sum(axis=0) / W.sum()
  46. return Pi
  47. # 计算log似然函数
  48. def logLH(X, Pi, Mu, Var):
  49. n_points, n_clusters = len(X), len(Pi)
  50. pdfs = np.zeros(((n_points, n_clusters)))
  51. for i in range(n_clusters):
  52. pdfs[:, i] = Pi[i] * multivariate_normal.pdf(X, Mu[i], np.diag(Var[i]))
  53. return np.mean(np.log(pdfs.sum(axis=1)))
  54. # 画出聚类图像
  55. def plot_clusters(X, Mu, Var, Mu_true=None, Var_true=None):
  56. colors = ['b', 'g', 'r']
  57. n_clusters = len(Mu)
  58. plt.figure(figsize=(10, 8))
  59. plt.axis([-10, 15, -5, 15])
  60. plt.scatter(X[:, 0], X[:, 1], s=5)
  61. ax = plt.gca()
  62. for i in range(n_clusters):
  63. plot_args = {'fc': 'None', 'lw': 2, 'edgecolor': colors[i], 'ls': ':'}
  64. ellipse = Ellipse(Mu[i], 3 * Var[i][0], 3 * Var[i][1], **plot_args)
  65. ax.add_patch(ellipse)
  66. if (Mu_true is not None) & (Var_true is not None):
  67. for i in range(n_clusters):
  68. plot_args = {'fc': 'None', 'lw': 2, 'edgecolor': colors[i], 'alpha': 0.5}
  69. ellipse = Ellipse(Mu_true[i], 3 * Var_true[i][0], 3 * Var_true[i][1], **plot_args)
  70. ax.add_patch(ellipse)
  71. plt.show()
  72. # M步根据更新的W和PI来跟新均值Mu与方差Var
  73. # 更新Mu
  74. def update_Mu(X, W):
  75. n_clusters = W.shape[1]
  76. Mu = np.zeros((n_clusters, 2))
  77. for i in range(n_clusters):
  78. Mu[i] = np.average(X, axis=0, weights=W[:, i])
  79. return Mu
  80. # 更新Var
  81. def update_Var(X, Mu, W):
  82. n_clusters = W.shape[1]
  83. Var = np.zeros((n_clusters, 2))
  84. for i in range(n_clusters):
  85. Var[i] = np.average((X - Mu[i]) ** 2, axis=0, weights=W[:, i])
  86. return Var
  87. if __name__ == '__main__':
  88. # 生成数据
  89. true_Mu = [[0.5, 0.5], [5.5, 2.5], [1, 7]]
  90. true_Var = [[1, 3], [2, 2], [6, 2]]
  91. X = generate_X(true_Mu, true_Var)
  92. # 初始化
  93. n_clusters = 3 #聚类的个数
  94. n_points = len(X)
  95. Mu = [[0, -1], [6, 0], [0, 9]]
  96. Var = [[1, 1], [1, 1], [1, 1]]
  97. Pi = [1 / n_clusters] * 3
  98. W = np.ones((n_points, n_clusters)) / n_clusters #隐变量
  99. Pi = W.sum(axis=0) / W.sum() #每一簇的比重,可以根据W求得
  100. # 迭代
  101. loglh = []
  102. for i in range(5):
  103. plot_clusters(X, Mu, Var, true_Mu, true_Var)
  104. loglh.append(logLH(X, Pi, Mu, Var))
  105. W = update_W(X, Mu, Var, Pi)
  106. Pi = update_Pi(W)
  107. Mu = update_Mu(X, W)
  108. print('log-likehood:%.3f'%loglh[-1])
  109. Var = update_Var(X, Mu, W)