EM算法到底是什么东东

EM(Expectation-Maximization期望最大化)算法是机器学习中非常重要的一类算法,广泛应用于聚类、缺失数据建模、隐变量模型学习等场景,比如高斯混合模型(GMM)就是经典应用。

🐤 第一步:直观理解

EM算法的核心是:

我不知道这个数据是哪一类(隐变量),就先猜;然后根据可见的情况,慢慢猜的更准。

EM算法就是一个"猜→修正→再猜"的循环。

例子1:

  • 给你一篇文章让你读
  • 可观测数据:文档中的词语。
  • 隐变量:文档的主题分布。
  • 本质:主题是潜在的,决定了词语的出现概率。

例子2:

假设有两个数据分布(两类),然后随机从这两个分布里抽出一些样本交给你,你不知道给你的样本点属于哪一类(隐含的类别),以及这两个数据分布的统计特性(均值,方差)

EM算法的做法是:

  1. 随便猜一下每个点属于哪个类别(初始猜测)
  2. 计算:在当前参数下,每个点属于各个类别的"概率"(这是E步)
  3. 用这些概率来"反推"出最合理的类别参数(比如均值、方差)(这是M步)
  4. 重复步骤2-3,直到参数不怎么变为止。

✍️ 第二步:数学公式

你有一堆数据点 x 1 , ... , x n \mathbf{x}_1, \dots, \mathbf{x}_n x1,...,xn,你相信这些数据来自 K K K 个不同的高斯分布:

  • 每个分布 k k k 有自己的参数:均值 μ k \mu_k μk、方差 σ k 2 \sigma_k^2 σk2、权重 π k \pi_k πk(概率总和为1)
  • 但你不知道哪个点来自哪个分布(这是隐变量)

E步(Expectation),即"先猜"

初始化:随机初始化均值 μ k \mu_k μk、方差 σ k 2 \sigma_k^2 σk2 和权重 π k \pi_k πk

计算每个样本属于每个高斯分布的"后验概率":

γ i k = π k ⋅ N ( x i ∣ μ k , σ k 2 ) ∑ j = 1 K π j ⋅ N ( x i ∣ μ j , σ j 2 ) \gamma_{ik} = \frac{\pi_k \cdot \mathcal{N}(x_i | \mu_k, \sigma_k^2)}{\sum_{j=1}^K \pi_j \cdot \mathcal{N}(x_i | \mu_j, \sigma_j^2)} γik=∑j=1Kπj⋅N(xi∣μj,σj2)πk⋅N(xi∣μk,σk2)

这表示:样本 x i x_i xi 属于第 k k k 个高斯分布的概率。


M步(Maximization),即"反推参数"

根据这些概率 γ i k \gamma_{ik} γik 来重新估计参数:

μ k = ∑ i γ i k x i ∑ i γ i k , σ k 2 = ∑ i γ i k ( x i − μ k ) 2 ∑ i γ i k , π k = 1 n ∑ i γ i k \mu_k = \frac{\sum_i \gamma_{ik} x_i}{\sum_i \gamma_{ik}}, \quad \sigma_k^2 = \frac{\sum_i \gamma_{ik} (x_i - \mu_k)^2}{\sum_i \gamma_{ik}}, \quad \pi_k = \frac{1}{n} \sum_i \gamma_{ik} μk=∑iγik∑iγikxi,σk2=∑iγik∑iγik(xi−μk)2,πk=n1i∑γik


🧊 第三步 :一个具体的例子------高斯混合模型(GMM)

什么是GMM?

高斯混合模型(GMM)就是用多个"高斯分布"加权叠加来组合描述一个复杂的数据分布。GMM 的参数(每个高斯的均值、方差、权重)不能直接算出来,但可以用 EM算法 来一步步逼近!

  • GMM = 模型框架
  • EM = 参数求解方法

GMM分布可视化

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']  # 使用黑体显示中文
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

# 定义两个高斯分布的参数
# 每个分布由均值(mu)、标准差(sigma)和权重(weight)组成
mu1, sigma1, weight1 = 5, 1, 0.4  # 分布1: 均值5, 标准差1, 权重40%
mu2, sigma2, weight2 = 15, 2, 0.6  # 分布2: 均值15, 标准差2, 权重60%

# 生成X轴范围,覆盖两个分布的3σ范围
x_min = min(mu1 - 3*sigma1, mu2 - 3*sigma2)
x_max = max(mu1 + 3*sigma1, mu2 + 3*sigma2)
x = np.linspace(x_min, x_max, 1000)  # 在合理范围内生成1000个点

# 计算单个分布的概率密度函数(PDF)
pdf1 = weight1 * norm.pdf(x, mu1, sigma1)  # 第一个高斯分布的加权PDF
pdf2 = weight2 * norm.pdf(x, mu2, sigma2)  # 第二个高斯分布的加权PDF

# 计算混合后的整体分布(GMM的概率密度)
pdf_total = pdf1 + pdf2  # 高斯混合模型的PDF是两个加权高斯分布的和

# 创建图形并设置大小
plt.figure(figsize=(10, 6))

# 绘制各个分布
plt.plot(x, pdf1, label=f"高斯分布1 (μ={mu1}, σ={sigma1}, 权重={weight1})",
         linestyle='--', color='blue')
plt.plot(x, pdf2, label=f"高斯分布2 (μ={mu2}, σ={sigma2}, 权重={weight2})",
         linestyle='--', color='green')
plt.plot(x, pdf_total, label="混合分布 GMM", linestyle='--',color='red', linewidth=1.5)

# 添加图形标题和标签
plt.title("高斯混合模型(GMM)示意图", fontsize=14)
plt.xlabel("特征值 (示例:糖分含量)", fontsize=12)
plt.ylabel("概率密度", fontsize=12)

# 添加图例和网格
plt.legend(fontsize=10)
plt.grid(True, linestyle='--', alpha=0.6)

# 显示图形
plt.tight_layout()  # 自动调整子图参数,使之填充整个图像区域
plt.show()

问题背景

假设我们有一堆学生身高数据,比如160cm、155cm、175cm等等,但我们不知道每个学生是小学生还是中学生。我们猜测这些身高来自两个群体:

  • 小学生:身高服从一个正态分布(高斯分布),有自己的均值和标准差。
  • 中学生:身高服从另一个正态分布,也有自己的均值和标准差。

此外,每个群体在总数据中占一定比例。我们的目标是:

  1. 弄清楚每个学生属于小学生还是中学生的概率。
  2. 估计两个群体的参数:比例( π π π)、均值( μ μ μ)、标准差( σ σ σ)。

因为我们不知道真实的类别和参数,所以要用EM算法通过迭代来解决这个问题。


EM算法是什么?

EM算法(Expectation-Maximization)是一种用来处理"隐变量"问题的工具。这里,隐变量就是"每个学生属于哪个群体",我们看不到它,但可以通过数据推测。EM算法分为两步:

  • E步(期望):根据当前猜测的参数,算出每个学生属于小学生或中学生的概率。
  • M步(最大化):用这些概率更新参数,让模型更好地拟合数据。

这两步不断重复,直到参数稳定。


具体案例:一步步拆解

1. 初始化:随便猜参数

我们先随便猜一下两个群体的参数,作为起点:

  • 小学生
    • 比例( π 1 π_1 π1):50%(0.5)
    • 均值( μ 1 μ_1 μ1):150cm
    • 标准差( σ 1 σ_1 σ1):5cm
  • 中学生
    • 比例( π 2 π_2 π2):50%(0.5)
    • 均值( μ 2 μ_2 μ2):170cm
    • 标准差( σ 2 σ_2 σ2):6cm

这些是初始猜测,不一定准确,但EM算法会帮我们调整。


2. E步:算概率(责任值)

现在拿一个学生,身高是160cm。我们要算他属于小学生还是中学生的概率。

(1)用正态分布公式算"可能性"

每个群体都有一个正态分布曲线:

  • 小学生:均值150cm,标准差5cm。
  • 中学生:均值170cm,标准差6cm。

正态分布的公式是:
P ( X ) = 1 2 π ⋅ σ exp ⁡ ( − ( X − μ ) 2 2 σ 2 ) P(X)=\frac{1}{\sqrt{2\pi}\cdot\sigma}\exp\left(-\frac{(X-\mu)^2}{2\sigma^2}\right) P(X)=2π ⋅σ1exp(−2σ2(X−μ)2)

  • 小学生
    P ( 小学生 ∣ 160 ) = 1 2 π ⋅ 5 exp ⁡ ( − ( 160 − 150 ) 2 2 ⋅ 5 2 ) P(\text{小学生}|160)=\frac{1}{\sqrt{2\pi}\cdot5}\exp\left(-\frac{(160-150)^2}{2\cdot5^2}\right) P(小学生∣160)=2π ⋅51exp(−2⋅52(160−150)2)

    计算指数部分:(160 - 150)² = 100,2 × 5² = 50,-100 / 50 = -2,exp(-2) ≈ 0.135。所以结果是一个较小的数。

  • 中学生
    P ( 中学生 ∣ 160 ) = 1 2 π ⋅ 6 exp ⁡ ( − ( 160 − 170 ) 2 2 ⋅ 6 2 ) P(\text{中学生}|160)=\frac{1}{\sqrt{2\pi}\cdot6}\exp\left(-\frac{(160-170)^2}{2\cdot6^2}\right) P(中学生∣160)=2π ⋅61exp(−2⋅62(160−170)2)

    计算指数部分:(160 - 170)² = 100,2 × 6² = 72,-100 / 72 ≈ -1.39,exp(-1.39) ≈ 0.25。结果比小学生的稍大。

简单来说:

  • 160cm离150cm(小学生均值)较远,所以可能性较低。
  • 160cm离170cm(中学生均值)较近,所以可能性较高。
(2)结合比例算后验概率(责任值)

光看可能性还不够,还要考虑每个群体占的总比例( π 1 = 0.5 π_1=0.5 π1=0.5, π 2 = 0.5 π_2=0.5 π2=0.5)。用贝叶斯公式:
P ( 小学生 ∣ 160 ) = π 1 ⋅ P ( 160 ∣ 小学生 ) π 1 ⋅ P ( 160 ∣ 小学生 ) + π 2 ⋅ P ( 160 ∣ 中学生 ) P(\text{小学生}|160)=\frac{π_1\cdot P(160|\text{小学生})}{π_1\cdot P(160|\text{小学生})+π_2\cdot P(160|\text{中学生})} P(小学生∣160)=π1⋅P(160∣小学生)+π2⋅P(160∣中学生)π1⋅P(160∣小学生)

假设计算后:

  • P ( 小学生 ∣ 160 ) ≈ 0.3 P(\text{小学生}|160)≈0.3 P(小学生∣160)≈0.3(30%)
  • P ( 中学生 ∣ 160 ) ≈ 0.7 P(\text{中学生}|160)≈0.7 P(中学生∣160)≈0.7(70%)

意思是:这个160cm的学生有30%概率是小学生,70%概率是中学生。对所有学生都做类似计算。


3. M步:更新参数

现在我们用所有学生的概率来调整参数。假设有3个学生:160cm、155cm、175cm,E步算出的概率如下:

身高 P(小学生) P(中学生)
160cm 0.3 0.7
155cm 0.6 0.4
175cm 0.1 0.9
(1)更新比例( π π π)
  • 新 π 1 = π_1= π1=所有学生属于小学生的概率平均值:
    π 1 = 0.3 + 0.6 + 0.1 3 = 1.0 3 ≈ 0.33 π_1=\frac{0.3+0.6+0.1}{3}=\frac{1.0}{3}≈0.33 π1=30.3+0.6+0.1=31.0≈0.33
  • 新 π 2 = 1 − π 1 ≈ 0.67 π_2=1-π_1≈0.67 π2=1−π1≈0.67
(2)更新均值( μ μ μ)
  • 新 μ 1 = μ_1= μ1=身高 × 属于小学生的概率的加权平均:
    μ 1 = ( 160 ⋅ 0.3 ) + ( 155 ⋅ 0.6 ) + ( 175 ⋅ 0.1 ) 0.3 + 0.6 + 0.1 = 48 + 93 + 17.5 1.0 = 158.5   cm μ_1=\frac{(160\cdot0.3)+(155\cdot0.6)+(175\cdot0.1)}{0.3+0.6+0.1}=\frac{48+93+17.5}{1.0}=158.5\,\text{cm} μ1=0.3+0.6+0.1(160⋅0.3)+(155⋅0.6)+(175⋅0.1)=1.048+93+17.5=158.5cm
  • 新 μ 2 = μ_2= μ2=类似计算:
    μ 2 = ( 160 ⋅ 0.7 ) + ( 155 ⋅ 0.4 ) + ( 175 ⋅ 0.9 ) 0.7 + 0.4 + 0.9 = 112 + 62 + 157.5 2.0 = 165.75   cm μ_2=\frac{(160\cdot0.7)+(155\cdot0.4)+(175\cdot0.9)}{0.7+0.4+0.9}=\frac{112+62+157.5}{2.0}=165.75\,\text{cm} μ2=0.7+0.4+0.9(160⋅0.7)+(155⋅0.4)+(175⋅0.9)=2.0112+62+157.5=165.75cm
(3)更新标准差( σ σ σ)
  • 新 σ 1 = σ_1= σ1= 身高偏离新均值 μ 1 μ_1 μ1的加权方差:
    σ 1 = 0.3 ⋅ ( 160 − 158.5 ) 2 + 0.6 ⋅ ( 155 − 158.5 ) 2 + 0.1 ⋅ ( 175 − 158.5 ) 2 1.0 σ_1=\sqrt{\frac{0.3\cdot(160-158.5)^2+0.6\cdot(155-158.5)^2+0.1\cdot(175-158.5)^2}{1.0}} σ1=1.00.3⋅(160−158.5)2+0.6⋅(155−158.5)2+0.1⋅(175−158.5)2
    计算后可能得到一个新值,比如4.8cm。
  • 新 σ 2 = σ_2= σ2=类似计算,得到新值,比如5.5cm。

4. 重复迭代

用新参数( π 1 = 0.33 , μ 1 = 158.5 , σ 1 = 4.8 , π 2 = 0.67 , μ 2 = 165.75 , σ 2 = 5.5 π_1=0.33,μ_1=158.5,σ_1=4.8,π_2=0.67,μ_2=165.75,σ_2=5.5 π1=0.33,μ1=158.5,σ1=4.8,π2=0.67,μ2=165.75,σ2=5.5)再跑一遍E步和M步。每轮迭代后,参数会更接近真实值。重复直到参数几乎不变,比如:

  • 小学生 : π 1 ≈ 0.4 , μ 1 ≈ 148 cm , σ 1 ≈ 4 cm π_1≈0.4,μ_1≈148\text{cm},σ_1≈4\text{cm} π1≈0.4,μ1≈148cm,σ1≈4cm
  • 中学生 : π 2 ≈ 0.6 , μ 2 ≈ 172 cm , σ 2 ≈ 5 cm π_2≈0.6,μ_2≈172\text{cm},σ_2≈5\text{cm} π2≈0.6,μ2≈172cm,σ2≈5cm

这意味着EM算法成功把混合的身高数据分成了两个群体,并估计了它们的特征。


📊 第四步:完整Python代码

python 复制代码
import numpy as np
from scipy.stats import norm
from sklearn.cluster import KMeans
import seaborn as sns
import matplotlib.pyplot as plt


class GMM_EM:
    """高斯混合模型(GMM)的EM算法核心实现 - 用于学生身高分布分析

    案例背景:
    假设数据包含两个学生群体的身高数据:
    1. 小学生:服从N(μ1, σ1²)
    2. 中学生:服从N(μ2, σ2²)
    每个群体在总样本中占有比例π
    目标:通过EM算法估计这两个群体的分布参数(π, μ, σ)
    """

    def __init__(self, n_components=2, max_iter=100, tol=1e-6, random_state=42):
        """模型初始化

        参数:
        n_components : int, default=2
            要区分的学生群体数量(默认2类:小学生/中学生)
        max_iter : int, default=100
            EM算法最大迭代次数
        tol : float, default=1e-6
            参数变化收敛阈值(当参数变化小于此值时停止迭代)
        random_state : int, default=42
            随机种子,保证结果可重复
        """
        self.n_components = n_components  # 学生群体数量
        self.max_iter = max_iter  # 最大迭代次数
        self.tol = tol  # 收敛判断阈值
        self.random_state = random_state  # 随机种子
        self.pi = None  # 各群体比例(小学生/中学生的样本占比)
        self.mu = None  # 各群体身高均值(单位:厘米)
        self.sigma = None  # 各群体身高标准差
        self.converged = False  # 是否收敛标志
        self.iterations = 0  # 实际迭代次数

    def _validate_input(self, data):
        """输入验证 - 确保数据适合学生身高分析

        验证条件:
        1. 输入必须是1维数组(每个元素代表一个学生的身高)
        2. 样本量必须大于群体数量(防止无法区分群体)
        """
        if not isinstance(data, np.ndarray) or data.ndim != 1:
            raise ValueError("输入应为1维数组,表示学生身高测量值")
        if len(data) < self.n_components:
            raise ValueError("样本量需大于群体数量才能进行有效分析")

    def _initialize_parameters(self, data):
        """参数初始化 - 使用K-means进行初步群体划分

        初始化策略:
        1. 通过K-means将学生按身高初步分为n_components个群体
        2. 按群体身高均值升序排列(确保小学生群体在前)
        3. 初始化参数:
           - π: 各群体样本占比
           - μ: 各群体身高均值
           - σ: 各群体身高标准差(至少1cm防止数值问题)
        """
        np.random.seed(self.random_state)
        kmeans = KMeans(n_clusters=self.n_components,
                        random_state=self.random_state)
        labels = kmeans.fit_predict(data.reshape(-1, 1))

        # 按身高均值升序排列群体(保证小学生群体在前)
        unique_labels = np.unique(labels)
        means = np.array([data[labels == lbl].mean() for lbl in unique_labels])
        order = np.argsort(means)

        # 初始化参数
        self.pi = np.array([np.mean(labels == lbl) for lbl in unique_labels[order]])
        self.mu = np.array([data[labels == lbl].mean() for lbl in unique_labels[order]])
        self.sigma = np.array([
            data[labels == lbl].std() if np.sum(labels == lbl) > 1 else 1.0
            for lbl in unique_labels[order]
        ])

    def _e_step(self, data):
        """期望步(E-step)- 计算学生归属各群体的后验概率

        计算公式:
        P(群体k|身高) = π_k * N(身高|μ_k, σ_k²) / Σ(π_j * N(身高|μ_j, σ_j²))

        返回:
        responsibilities : array, shape (n_samples, n_components)
            每个学生属于各群体的概率矩阵
        """
        # 计算各群体的概率密度
        pdf = norm.pdf(data[:, np.newaxis], self.mu, self.sigma)
        # 计算责任矩阵(未归一化的后验概率)
        responsibilities = self.pi * pdf
        # 归一化使各学生概率和为1
        responsibilities /= responsibilities.sum(axis=1, keepdims=True)
        return responsibilities

    def _m_step(self, data, responsibilities):
        """最大化步(M-step)- 更新群体参数

        更新公式:
        1. π_k = 群体k的责任值总和 / 总样本数
        2. μ_k = Σ(责任值_ki * 身高_i) / 群体k的责任值总和
        3. σ_k = sqrt(Σ(责任值_ki * (身高_i - μ_k)^2) / 群体k的责任值总和)
        """
        # 各群体有效样本数
        N_k = responsibilities.sum(axis=0)
        # 更新群体比例
        self.pi = N_k / len(data)
        # 更新群体均值
        self.mu = np.dot(responsibilities.T, data) / N_k
        # 更新群体标准差
        diff_sq = (data[:, np.newaxis] - self.mu) ** 2
        self.sigma = np.sqrt(np.sum(responsibilities * diff_sq, axis=0) / N_k)

    def _has_converged(self, prev_params):
        """收敛判断 - 检查参数是否稳定

        判断标准:
        新旧参数(π, μ, σ)的变化是否均小于tol阈值
        """
        return all(np.allclose(new, old, atol=self.tol) for new, old in
                   zip([self.pi, self.mu, self.sigma], prev_params))

    def fit(self, data):
        """训练模型 - EM算法主循环

        执行流程:
        1. 输入验证
        2. 参数初始化
        3. 迭代执行E步和M步
        4. 检查收敛或达到最大迭代次数
        5. 最终按身高均值排序群体
        """
        self._validate_input(data)
        self._initialize_parameters(data)
        self.converged = False

        for self.iterations in range(1, self.max_iter + 1):
            prev_params = [arr.copy() for arr in [self.pi, self.mu, self.sigma]]
            # E步:计算后验概率
            responsibilities = self._e_step(data)
            # M步:更新参数
            self._m_step(data, responsibilities)
            # 检查收敛
            if self._has_converged(prev_params):
                self.converged = True
                break

        # 最终按身高均值排序群体(保证小学生群体在前)
        order = np.argsort(self.mu)
        self.mu = self.mu[order]
        self.pi = self.pi[order]
        self.sigma = self.sigma[order]

        return self

    def predict_proba(self, data):
        """预测概率 - 返回每个学生属于各群体的概率

        返回:
        array, shape (n_samples, n_components)
            每个元素表示对应学生属于该群体的概率
        """
        return self._e_step(data)

    def predict(self, data):
        """预测类别 - 返回最可能的群体标签

        返回:
        array, shape (n_samples,)
            每个元素为0(小学生)或1(中学生)
        """
        return np.argmax(self.predict_proba(data), axis=1)

    def get_params(self):
        """获取训练后的模型参数

        返回:
        dict 包含:
            - pi : 各群体比例
            - mu : 各群体平均身高(cm)
            - sigma : 各群体身高标准差(cm)
            - converged : 是否收敛
            - iterations : 实际迭代次数
        """
        return {
            'pi': self.pi,
            'mu': self.mu,
            'sigma': self.sigma,
            'converged': self.converged,
            'iterations': self.iterations
        }


class GMM_Visualizer:
    """GMM可视化工具类"""
    def __init__(self, model, data, true_labels=None):
        self.model = model
        self.data = data
        self.true_labels = true_labels
        self.colors = sns.color_palette("husl", model.n_components)
        self.group_labels = ['Primary school student', 'Middle school student'] \
            if model.n_components == 2 else [f'Group {i+1}' for i in range(model.n_components)]

    def plot_results(self, save_path=None):
        """可视化拟合结果"""
        plt.figure(figsize=(12, 7))
        x = np.linspace(self.data.min()-15, self.data.max()+15, 1000)

        # 绘制直方图
        sns.histplot(self.data, bins=30, kde=False, stat='density',
                     color='gray', alpha=0.3, label='Original Data')

        # 绘制各成分和混合分布
        mixture_pdf = np.zeros_like(x)
        for k in range(self.model.n_components):
            component_pdf = self.model.pi[k] * norm.pdf(x, self.model.mu[k], self.model.sigma[k])
            plt.plot(x, component_pdf, color=self.colors[k], lw=2,
                     label=f'{self.group_labels[k]} (π={self.model.pi[k]:.2f}, μ={self.model.mu[k]:.1f}, σ={self.model.sigma[k]:.1f})')
            mixture_pdf += component_pdf

        # 绘制混合分布
        plt.plot(x, mixture_pdf, 'k--', lw=2.5, label='Mixture Distribution')

        # 绘制真实分布(如果存在)
        if self.true_labels is not None:
            for k in range(self.model.n_components):
                sns.histplot(self.data[self.true_labels == k], bins=15, kde=False, stat='density',
                             color=self.colors[k], alpha=0.3, label=f'True {self.group_labels[k]}')

        plt.title('GMM Fitting Results', fontsize=14)
        plt.xlabel('Height (cm)')
        plt.ylabel('Probability Density')
        plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', frameon=True)
        plt.grid(alpha=0.2)

        if save_path:
            plt.savefig(save_path, dpi=300, bbox_inches='tight')
            print(f"图像已保存到: {save_path}")
        else:
            plt.show()
        plt.close()


def generate_data(n_primary=100, n_secondary=100,
                  primary_mean=160, primary_std=5,
                  secondary_mean=175, secondary_std=7,
                  random_state=42):
    """生成模拟学生身高数据

    案例背景:
    生成包含两个学生群体的身高数据集,模拟实际观测数据:
    - 小学生群体:身高服从正态分布N(μ1, σ1²)
    - 中学生群体:身高服从正态分布N(μ2, σ2²)

    参数:
    n_primary : int, default=100
        小学生样本数量(默认100人)
    n_secondary : int, default=100
        中学生样本数量(默认100人)
    primary_mean : float, default=160
        小学生群体平均身高(厘米)
    primary_std : float, default=5
        小学生群体身高标准差(厘米)
    secondary_mean : float, default=175
        中学生群体平均身高(厘米)
    secondary_std : float, default=7
        中学生群体身高标准差(厘米)
    random_state : int, default=42
        随机种子,保证数据生成可重复

    返回:
    data : ndarray, shape (n_samples,)
        打乱后的混合身高数据(单位:厘米)
    labels : ndarray, shape (n_samples,)
        对应的学生群体标签(0:小学生, 1:中学生)
    """

    # 设置随机种子保证结果可重复
    np.random.seed(random_state)

    # 生成小学生身高数据(正态分布)
    primary = np.random.normal(primary_mean, primary_std, n_primary)

    # 生成中学生身高数据(正态分布)
    secondary = np.random.normal(secondary_mean, secondary_std, n_secondary)

    # 合并数据并创建标签
    data = np.concatenate([primary, secondary])
    labels = np.concatenate([np.zeros(n_primary), np.ones(n_secondary)])

    # 打乱数据以模拟真实观测场景
    # 保持数据与标签的对应关系
    idx = np.random.permutation(len(data))

    return data[idx], labels[idx].astype(int)


def print_results(model, data, true_labels=None):
    """打印结果对比"""
    params = model.get_params()
    pi, mu, sigma = params['pi'], params['mu'], params['sigma']

    print("\n" + "=" * 60)
    print("GMM-EM 模型预测结果")
    print("=" * 60)
    for k in range(len(pi)):
        print(f"Group {k+1}: 比例={pi[k]:.4f}, 均值={mu[k]:.2f}cm, 方差={sigma[k]:.2f}cm")

    if true_labels is not None:
        print("\n真实参数:")
        for k in range(len(pi)):
            mask = true_labels == k
            print(f"Group {k+1}: 比例={np.mean(mask):.4f}, "
                  f"均值={data[mask].mean():.2f}cm, 方差={data[mask].std():.2f}cm")

        accuracy = np.mean(model.predict(data) == true_labels)
        print(f"\n分类准确率: {accuracy:.2%}")
    print(f"是否收敛: {params['converged']}, 迭代次数: {params['iterations']}")
    print("=" * 60)


if __name__ == "__main__":
    # 生成数据
    data, labels = generate_data()

    # 训练模型
    model = GMM_EM(n_components=2)
    model.fit(data)

    # 打印结果
    print_results(model, data, labels)

    # 可视化
    visualizer = GMM_Visualizer(model, data, labels)
    visualizer.plot_results(save_path="./fitted_distribution.png")

📌 第五步:总结重点

概念 含义
隐变量 不知道但是存在的变量(比如样本的真实类别)
E步 计算每个数据点属于哪个分布的"概率"
M步 根据这个概率重新计算每个分布的参数
收敛 参数变化很小,不再更新,算法停止
应用场景 高斯混合聚类、缺失数据估计、协同过滤、HMM 等等
相关推荐
文人sec3 小时前
从接口压测到全链路质量保障:AI智能客服系统的软件测试实践
人工智能
声讯电子3 小时前
实测A-59F音频模组:降噪稳、不啸叫、通话清晰
人工智能·语音识别·ai降噪·usb接口·回音消除
神奇霸王龙3 小时前
2026旗舰六阶段MCP流水线实战指南
人工智能·ai·ai作画·prompt·aigc·mcp
山河不见老3 小时前
【Cursor 、Qoder安装问题】Cursor 、Qoder安装卡在“正在准备安装”问题排查及解决
人工智能·windows·编辑器
新知图书3 小时前
2.3 开发工作流
人工智能·agent·ai agent·智能体
tuanxiang3 小时前
用免费 AI 改写工具处理批量文案的踩坑记录,附完整降噪脚本
人工智能
小邓的技术笔记3 小时前
DeepSeek 大模型落地应用与场景实战指南
大数据·人工智能
wuminyu3 小时前
JUC组件逐层剥离与深度剖析
java·linux·c语言·jvm·c++·算法
石榴4 小时前
从 Codex + Obsidian 到本地 RAG:我的私人知识库实践
人工智能
QZSJTR4 小时前
技术解析|从SEO到GEO:生成式搜索时代实体企业数字化适配方案
人工智能