资讯动态

别再死磕公式了,图解原理助你避开极大似然三大坑

发布时间:2026/9/23 3:22:14 来源:尧图企业网站定制
别再死磕公式了,图解原理助你避开极大似然三大坑 官方文档翻了三遍还是云里雾里?别急,这真不是你的问题。统计学习里的极大似然估计(MLE),公式推导看着简单,代码一跑就崩,或者结果完全不对劲。很多开发者卡在“为什么我的参数估计和预期差这么多”上,其实都是掉进了几个经典的坑。 今天不堆砌数学符号,我们用图解原理的思路,拆解 Python 实战中关于极大似然最常见的三个“暗坑”。从报错现象到源码级修复,带你一次性理清逻辑。 坑一:梯度爆炸与数值不稳定 现象:Loss 变成 NaN 或 Inf 很多同学在用自定义损失函数实现 MLE 时,第一步就栽了跟头。运行几轮迭代,控制台直接抛出 RuntimeWarning: overflow encountered in exp,或者 Loss 直接变成了 nan。 你去看代码,发现似然函数里有个 np.exp(),数据稍微大一点,指数部分就溢出浮点数范围。这就是典型的数值不稳定。 根本原因:直接计算概率 vs 对数概率 极大似然的数学定义是最大化概率连乘积 \(L(\theta) = \prod P(x_i|\theta)\)。但在计算机里,概率值通常很小(比如 \(1e-10\)),几个数一乘,直接下溢变成 0。 为了计算方便,我们通常取对数,变成求和 \(\log L(\theta) = \sum \log P(x_i|\theta)\)。这就是对数似然。 但问题在于,很多分布的概率密度函数(PDF)本身包含指数项,比如高斯分布。如果你先算出概率值,再取对数,中间过程极易溢出。正确的做法是直接计算对数概率密度函数(Log-PDF),跳过“先算概率再取对”这一步。 正确写法对比 ❌ 错误写法:先算概率,再取对数 import numpy as np# 假设 x 是数据, mu 是均值参数 # 高斯分布概率密度公式 def wrong_log_likelihood(x, mu, sigma):# 先计算 exp 部分,极易溢出exp_term = np.exp(-0.5 * ((x - mu) / sigma) ** 2)prob = (1 / (sigma * np.sqrt(2 * np.pi))) * exp_term# 再取对数,如果 prob 下溢为0,log(0) = -infreturn np.sum(np.log(prob))✅ 正确写法:直接推导对数概率密度 import numpy as np# 正确的高斯对数似然函数 def correct_log_likelihood(x, mu, sigma):# 直接展开 log(N(x|mu, sigma))# log(1/(sigma*sqrt(2pi))) - 0.5 * ((x-mu)/sigma)^2term1 = -np.log(sigma) - 0.5 * np.log(2 * np.pi)term2 = -0.5 * ((x - mu) / sigma) ** 2return np.sum(term1 + term2)复现与修复代码 让我们用一段代码验证一下这个坑。假设我们有一组均值较大、方差较小的数据。 import numpy as np# 模拟数据:均值 1000,标准差 1 data = np.random.normal(loc=1000, scale=1, size=100)# 测试错误写法 try:val_wrong = wrong_log_likelihood(data, mu=1000, sigma=1)print(f错误写法结果: {val_wrong}) except Exception as e:print(f错误写法报错: {e})# 测试正确写法 val_correct = correct_log_likelihood(data, mu=1000, sigma=1) print(f正确写法结果: {val_correct})运行结果你会发现,错误写法可能返回 -inf 或者触发警告,而正确写法能稳定输出一个负数(对数似然通常为负,因为概率小于1)。 避坑建议:永远优先寻找 logpdf 而不是 pdf。 如果你必须自己推导公式,把 log(exp(a)) 直接写成 a,把 log(a * b) 写成 log(a) + log(b)。 在 PyTorch 或 TensorFlow 中,尽量使用内置的 torch.distributions 或 tfp.distributions,它们内部已经处理了数值稳定性。坑二:梯度方向搞反,优化器不收敛 现象:Loss 不降反升,参数乱跑 假设你已经解决了数值溢出问题,开始用 scipy.optimize.minimize 或 PyTorch 的 optimizer 进行参数估计。结果发现,Loss 不降反升,或者震荡剧烈,永远找不到最优解。 这时候你可能怀疑自己数据有问题,或者学习率没调好。但大概率,是你搞反了极大似然和最小化损失的关系。 根本原因:最大化 vs 最小化 极大似然估计的目标是最大化似然函数 \(L(\theta)\)。 而绝大多数优化算法(如 SGD, Adam, LBFGS)的设计初衷是最小化目标函数。 所以,我们在代码里实际最小化的,应该是负对数似然(Negative Log-Likelihood, NLL)。 很多初学者在定义 loss 函数时,忘记加负号,直接返回了 log_likelihood。优化器以为你在让它最小化一个本来应该最大化的量,于是它拼命把参数推向让似然变小的方向——也就是离真实分布越来越远的地方。 正确写法对比 ❌ 错误写法:忘记取负号 import torch import torch.nn as nn# 假设 we 定义了一个简单的模型 class SimpleModel(nn.Module):def __init__(self):super(SimpleModel, self).__init__()self.mu = nn.Parameter(torch.tensor(0.0))self.sigma = nn.Parameter(torch.tensor(1.0))def forward(self, x):# 返回对数似然 (注意:这里是正值越大越好)return -0.5 * torch.log(2 * torch.pi * self.sigma ** 2) - 0.5 * ((x - self.mu) / self.sigma) ** 2model = SimpleModel() optimizer = torch.optim.Adam(model.parameters(), lr=0.01)def train_step_wrong(x):optimizer.zero_grad()# 错误:直接最大化对数似然,但 optimizer 是求最小化# 相当于最小化 log_likelihood,这会导致参数发散loss = -model(x).sum() # 等等,这里我故意写错逻辑来演示:# 如果 model 返回的是 log_likelihood,我们要最小化 -log_likelihood# 但如果新手误以为 model 返回的是 loss,直接 loss = model(x)# 那么他就在最小化 log_likelihood,这是错误的# 模拟新手的错误认知:认为返回的越大越好,但 optimizer 默认是 minimize# 所以如果直接传 log_likelihood 给 optimizer.step,就是错的loss = model(x).sum() # 错误:最小化对数似然loss.backward()optimizer.step()return loss✅ 正确写法:最小化负对数似然 import torchdef train_step_correct(x):optimizer.zero_grad()# 正确:计算负对数似然 (NLL)# 我们要最小化 NLL,等价于最大化 LLnll = -model(x).sum() nll.backward()optimizer.step()return nll复现与修复代码 我们用 PyTorch 快速复现这个问题。 import torch import torch.nn as nn import numpy as np# 1. 准备数据 np_data = np.random.normal(5.0, 2.0, 1000) data = torch.tensor(np_data, dtype=torch.float32)# 2. 定义模型 class GaussianModel(nn.Module):def __init__(self):super(GaussianModel, self).__init__()self.mu = nn.Parameter(torch.tensor(0.0))self.log_sigma = nn.Parameter(torch.tensor(0.0)) # 用 log_sigma 保证 sigma 0def forward(self, x):sigma = torch.exp(self.log_sigma)# 返回对数似然ll = -0.5 * (2 * torch.log(sigma) + torch.log(2 * torch.pi) + ((x - self.mu) / sigma) ** 2)return ll.sum()model = GaussianModel() optimizer = torch.optim.Adam(model.parameters(), lr=0.05)# 3. 错误训练:最小化 LL for i in range(100):optimizer.zero_grad()loss = model(data) # 错误:这是在最大化 LL 的反面loss.backward()optimizer.step()print(f错误训练后的 mu: {model.mu.item():.4f}) # 预计偏离 5.0 很远# 4. 重置模型 model = GaussianModel() optimizer = torch.optim.Adam(model.parameters(), lr=0.05)# 5. 正确训练:最小化 NLL for i in range(100):optimizer.zero_grad()loss = -model(data) # 正确:最小化负对数似然loss.backward()optimizer.step()print(f正确训练后的 mu: {model.mu.item():.4f}) # 预计接近 5.0 print(f正确训练后的 sigma: {torch.exp(model.log_sigma).item():.4f}) # 预计接近 2.0避坑建议:记住口诀:“似然取负,交给优化器”。 如果你使用 scipy.optimize.minimize,确保传入的目标函数是 negative_log_likelihood。 在使用 PyTorch 时,torch.distributions.Normal 提供了 log_prob 方法,你可以直接 loss = -dist.log_prob(x).sum(),这样最安全。坑三:过拟合与正则化缺失 现象:训练集拟合完美,验证集惨不忍睹 当你处理小样本数据(比如只有几百条记录)时,极大似然估计可能会“太认真”了。它会把每一个数据点的噪声都当作真实信号去拟合,导致模型在训练集上 Loss 极低,但在新的数据上表现一塌糊涂。 这在金融风控、医疗诊断等小样本场景下非常致命。 根本原因:MLE 的无偏性与方差 极大似然估计在大样本下是一致的、渐近无偏的。但在小样本下,它的方差可能很大。也就是说,不同的样本批次,估计出的参数可能差异巨大。 为了抑制这种方差,我们需要引入正则化(Regularization)。在统计学里,这对应着贝叶斯估计中的先验分布。 如果你给参数加一个高斯先验(即 L2 正则化),或者拉普拉斯先验(即 L1 正则化),你就从纯 MLE 转向了最大后验估计(MAP)。 正确写法对比 ❌ 错误写法:纯 MLE,无正则化 import numpy as npdef pure_mle_loss(x, y, weights, bias):# 简单的线性回归 MLE (假设高斯噪声)predictions = x @ weights + biasresidual = y - predictions# 最小化平方误差等价于高斯 MLEreturn np.sum(residual ** 2)✅ 正确写法:加入 L2 正则化 (MAP with Gaussian Prior) import numpy as npdef regularized_mle_loss(x, y, weights, bias, lambda_l2=0.1):predictions = x @ weights + biasresidual = y - predictionsdata_likelihood = np.sum(residual ** 2)# L2 正则化项:相当于假设 weights 服从 N(0, sigma_prior^2)# 这一项越大,惩罚越重,参数越趋向于 0regularization = lambda_l2 * np.sum(weights ** 2)return data_likelihood + regularization复现与修复代码 我们用 sklearn 快速对比一下纯 MLE(普通最小二乘)和带正则化的结果(Ridge Regression)。 import numpy as np from sklearn.linear_model import LinearRegression, Ridge from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error# 生成有噪声的小样本数据 np.random.seed(42) n_samples = 100 X = np.random.randn(n_samples, 5) y = X @ np.array([1.0, 2.0, 0.5, -1.0, 3.0]) + np.random.randn(n_samples) * 0.5# 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)# 1. 纯 MLE (Linear Regression) lr_model = LinearRegression() lr_model.fit(X_train, y_train) lr_pred = lr_model.predict(X_test) lr_mse = mean_squared_error(y_test, lr_pred)# 2. 正则化 MLE (Ridge Regression) ridge_model = Ridge(alpha=1.0) # alpha 对应 lambda_l2 ridge_model.fit(X_train, y_train) ridge_pred = ridge_model.predict(X_test) ridge_mse = mean_squared_error(y_test, ridge_pred)print(f纯 MLE (OLS) Test MSE: {lr_mse:.4f}) print(f正则化 MLE (Ridge) Test MSE: {ridge_mse:.4f}) print(fOLS Coefficients: {lr_model.coef_}) print(fRidge Coefficients: {ridge_model.coef_})通常你会发现,在小样本高噪声场景下,Ridge 的测试集 MSE 比 OLS 更低,且系数更稳定(更接近真实值,而不是被噪声拉偏)。 避坑建议:数据量少?一定要上正则化。 正则化强度 \(\lambda\) 不要拍脑袋,用交叉验证(Cross-Validation)来选最优值。 在 PyTorch 中,nn.L1Loss 或手动添加 L2 = torch.sum(param ** 2) 都是标准做法。总结与进阶:如何构建稳健的 MLE 流程 避开这三个坑,你的极大似然估计代码就能稳定 90% 的情况。但想要做到生产级可靠,还有几点进阶建议:检查边界条件:确保参数在合法范围内。例如,概率必须大于 0,方差必须大于 0。在代码中,可以使用 log 变换来强制正数,或者在优化后检查参数合法性。 多起点初始化:极大似然的目标函数往往是非凸的(尤其是混合模型、HMM 等)。局部最优解很多。建议运行多次优化,每次随机初始化参数,保留 Loss 最低的那次结果。 使用成熟库:除非你需要定制非常特殊的分布,否则优先使用 statsmodels、scipy.stats、PyTorch Distributions 或 TensorFlow Probability。这些库的底层实现经过了无数次的数值稳定性优化,自己造轮子容易漏掉细节。 监控收敛性:不要只看最终 Loss。记录每一步的 Loss 变化曲线,观察是否震荡、是否过早停滞。如果 Loss 下降极其缓慢,可能是学习率太大或太小,或者是陷入了平坦区域。极大似然估计是统计学习的基石,也是连接数据与模型的桥梁。理解它的数值陷阱,能让你在调试模型时少掉几个坑,多睡几小时觉。 代码只是工具,理解背后的数学直觉才是核心。希望这篇避坑指南能帮你理清思路。 你在实际项目中还遇到过哪些关于极大似然或参数估计的奇葩 Bug?或者你对某个分布的 MLE 推导有疑问?评论区留言,我挨个回!

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价