用Python实战复现机器学习三大核心算法:线性回归、KL散度与GMM的EM实现

当理论公式遇上代码实践,才是真正掌握机器学习的开始。本文面向具备Python基础的数据科学学习者和工程实践者,我们将跳过教科书式的概念复述,直接通过NumPy和Scikit-learn实现机器学习期末考题中的三个经典算法:线性回归的参数更新、概率分布间的KL散度计算,以及高斯混合模型的EM算法迭代过程。不同于简单的API调用,我们将从数学原理出发,逐步构建可验证的代码实现,让你在调试代码的过程中真正理解梯度下降如何工作、信息熵如何量化、以及EM算法如何通过"猜谜游戏"优化参数。

1. 从数学推导到代码实现:线性回归的梯度下降

线性回归的梯度下降算法是理解参数优化的最佳起点。考试中要求推导的误差平方和公式,在实际编码时会遇到哪些意料之外的问题?让我们先明确数学表达:

损失函数 $J(\theta)$ 可表示为: $$ J(\theta) = \frac{1}{2m}\sum_{i=1}^m (h_\theta(x^{(i)}) - y^{(i)})^2 $$ 其中 $h_\theta(x) = \theta^T x$ 是我们的假设函数。

对应的梯度更新公式为: $$ \theta_j := \theta_j - \alpha \frac{\partial}{\partial \theta_j} J(\theta) $$ $$ \frac{\partial}{\partial \theta_j} J(\theta) = \frac{1}{m}\sum_{i=1}^m (h_\theta(x^{(i)}) - y^{(i)})x_j^{(i)} $$

现在用NumPy实现这个看似简单的过程:

import numpy as np

def linear_regression_gd(X, y, alpha=0.01, iterations=1000):
    m, n = X.shape
    theta = np.zeros(n)  # 初始化参数
    cost_history = []
    
    for _ in range(iterations):
        h = X.dot(theta)  # 假设函数计算
        error = h - y
        gradient = X.T.dot(error) / m  # 关键梯度计算
        theta -= alpha * gradient  # 参数更新
        
        # 记录每次迭代的损失值(非必需但有助于调试)
        cost = (error ** 2).sum() / (2 * m)
        cost_history.append(cost)
    
    return theta, cost_history

实际应用中容易忽略的几个关键点:

  • 特征缩放:当特征量纲差异大时,梯度下降会收敛缓慢。添加标准化处理:

    X = (X - np.mean(X, axis=0)) / np.std(X, axis=0)
    
  • 学习率选择:太大导致震荡,太小收敛慢。可通过损失曲线判断:

    import matplotlib.pyplot as plt
    plt.plot(cost_history)
    plt.xlabel('Iterations')
    plt.ylabel('Cost')
    

提示:在真实数据集上测试时,记得添加偏置项 X = np.c_[np.ones(m), X],否则模型将强制通过原点。

2. 信息论实践:KL散度的计算与陷阱

KL散度(Kullback-Leibler Divergence)衡量两个概率分布的差异,在模型评估和生成模型中广泛应用。给定离散分布P和Q,其定义为: $$ D_{KL}(P||Q) = \sum_i P(i) \log \frac{P(i)}{Q(i)} $$

考试题目中的具体计算:

  • P = [0.1, 0.3, 0.6]
  • Q = [0.1, 0.2, 0.7]

先计算信息熵 $H(P)$: $$ H(P) = -\sum P(x) \log_2 P(x) $$

Python实现揭示了一些易错细节:

def entropy(p):
    p = np.array(p)
    return -np.sum(p * np.log2(p + 1e-10))  # 加小量避免log(0)

def kl_divergence(p, q):
    p, q = np.array(p), np.array(q)
    assert np.all(p >= 0) and np.all(q >= 0), "概率需非负"
    assert np.isclose(np.sum(p), 1) and np.isclose(np.sum(q), 1), "概率和应为1"
    return np.sum(p * np.log2((p + 1e-10) / (q + 1e-10)))  # 防止除以0

# 考题计算
P = [0.1, 0.3, 0.6]
Q = [0.1, 0.2, 0.7]
print(f"H(P): {entropy(P):.2f} bits")  # 输出: H(P): 1.30 bits
print(f"D_KL(P||Q): {kl_divergence(P, Q):.2f}")  # 输出: D_KL(P||Q): 0.15

实际应用中需要注意:

  • 非对称性kl_divergence(P,Q) != kl_divergence(Q,P)
  • 零概率处理:真实代码必须包含平滑项(如1e-10
  • 数值稳定性:概率乘积可能导致下溢,可改用对数空间计算

3. EM算法解构:高斯混合模型的参数估计

EM(Expectation-Maximization)算法是处理隐变量估计的强大工具,尤其适合高斯混合模型(GMM)。考试要求解释的E步和M步,在代码中如何体现?

GMM的概率密度函数: $$ p(x) = \sum_{k=1}^K \pi_k \mathcal{N}(x|\mu_k, \Sigma_k) $$

EM算法的核心迭代过程:

  1. E步:计算后验概率 $\gamma(z_{nk})$ $$ \gamma(z_{nk}) = \frac{\pi_k \mathcal{N}(x_n|\mu_k, \Sigma_k)}{\sum_{j=1}^K \pi_j \mathcal{N}(x_n|\mu_j, \Sigma_j)} $$

  2. M步:更新参数 $\mu_k$, $\Sigma_k$, $\pi_k$ $$ \mu_k^{new} = \frac{1}{N_k}\sum_{n=1}^N \gamma(z_{nk})x_n $$ $$ \Sigma_k^{new} = \frac{1}{N_k}\sum_{n=1}^N \gamma(z_{nk})(x_n - \mu_k^{new})(x_n - \mu_k^{new})^T $$ $$ \pi_k^{new} = \frac{N_k}{N} \quad \text{其中} \quad N_k = \sum_{n=1}^N \gamma(z_{nk}) $$

Python实现展示了数学公式到代码的转换:

def gmm_em(X, K, max_iter=100, tol=1e-6):
    n, d = X.shape
    # 初始化参数
    mu = X[np.random.choice(n, K, replace=False)]
    sigma = [np.eye(d)] * K
    pi = np.ones(K) / K
    log_likelihood = -np.inf
    
    for _ in range(max_iter):
        # E步:计算后验概率
        gamma = np.zeros((n, K))
        for k in range(K):
            gamma[:, k] = pi[k] * multivariate_normal(mu[k], sigma[k]).pdf(X)
        gamma /= gamma.sum(axis=1, keepdims=True)
        
        # M步:更新参数
        N_k = gamma.sum(axis=0)
        for k in range(K):
            mu[k] = np.sum(gamma[:, k][:, None] * X, axis=0) / N_k[k]
            diff = X - mu[k]
            sigma[k] = (gamma[:, k][:, None] * diff).T @ diff / N_k[k]
        pi = N_k / n
        
        # 检查收敛
        new_log_likelihood = np.sum(np.log(np.sum([pi[k] * multivariate_normal(mu[k], sigma[k]).pdf(X) 
                                                  for k in range(K)], axis=0)))
        if np.abs(new_log_likelihood - log_likelihood) < tol:
            break
        log_likelihood = new_log_likelihood
    
    return mu, sigma, pi

实现中的关键考量:

  • 初始化策略:K-means++通常比随机选择更稳定
  • 协方差正则化:添加 sigma[k] += 1e-6 * np.eye(d) 防止奇异矩阵
  • 对数似然:改用对数计算避免数值下溢

4. 结果验证与工程实践建议

完成算法实现后,如何验证其正确性?以下是针对三个算法的验证策略:

线性回归验证矩阵

验证方法实现代码预期结果
解析解对比theta_normal = np.linalg.inv(X.T@X)@X.T@y应与梯度下降结果相近
Scikit-learn基准from sklearn.linear_model import LinearRegression参数误差应小于1e-3

KL散度验证案例

# 测试均匀分布与确定性分布的KL散度
P = [0.5, 0.5]
Q = [1.0, 0.0]
print(kl_divergence(P, Q))  # 应为1.0
print(kl_divergence(Q, P))  # 应为inf

GMM可视化验证

def plot_gmm(X, mu, sigma):
    plt.scatter(X[:, 0], X[:, 1], alpha=0.2)
    for k in range(len(mu)):
        draw_ellipse(mu[k], sigma[k])
    plt.show()

工程实践中的经验建议:

  1. 线性回归的扩展考量

    • 添加L2正则化防止过拟合(岭回归)
    • 使用随机梯度下降处理大规模数据
  2. 信息论应用的边界情况

    # 处理零概率的稳健方案
    def safe_kl(p, q):
        mask = (p > 0) & (q > 0)
        return np.sum(p[mask] * np.log(p[mask]/q[mask]))
    
  3. GMM的实战技巧

    • 采用贝叶斯GMM自动确定聚类数量
    • 使用对角协方差矩阵处理高维数据
    • 通过BIC准则选择最佳K值

在真实项目中使用这些算法时,推荐优先使用成熟的库实现(如scikit-learn的GaussianMixture),但理解底层实现原理能帮助你在出现异常结果时快速定位问题。例如,当EM算法不收敛时,可能是由于初始化不当或存在退化协方差矩阵。

更多推荐