ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

逻辑回归本质是概率建模:从最大似然到校准落地

逻辑回归本质是概率建模:从最大似然到校准落地

简介:本资源是一份面向人工智能与机器学习初学者及进阶学习者的理论结合实践型教学文档,聚焦逻辑回归这一核心分类算法,深入阐释其背后的概率论与数理统计原理。文档系统梳理了Sigmoid函数建模思想、条件概率与贝叶斯定理在模型推导中的作用,并通过电子邮件分类、疾病诊断、客户流失预测和情感分析四大典型场景,直观展现算法落地逻辑;附带完整Python实现代码(基于sklearn的乳腺癌数据集建模),涵盖数据加载、划分、训练、预测与评估全流程。资源为单文件Word文档(.docx),共1个文件,大小38KB,结构清晰、图文配合得当,适合作为课堂补充材料或自学笔记。目前已有111人学习下载,内容兼具数学严谨性与工程可操作性,帮助读者真正理解“为何用逻辑回归”与“如何解释预测结果”。

1. 逻辑回归不是“回归”:它用概率论把线性模型焊死在分类任务上,但很多人调参时连损失函数长什么样都没见过

你手头正跑着一个信用评分模型,训练集AUC 0.82,测试集掉到0.71;或者你在做医疗诊断二分类,模型输出“0.63”这个数字,却不知道它到底代表“63%得病概率”还是“模型瞎猜的置信度”;又或者你刚学完《概率论与数理统计》课本里那堆贝叶斯公式、最大似然估计、中心极限定理,结果写逻辑回归代码时——全忘了怎么跟sigmoid、对数损失、梯度下降挂钩。这不是你数学差,而是逻辑回归本身就是一个被严重低估的“概率接口层”:它不直接预测类别,而是强制建模 P(y=1|x),再用阈值切出分类结果。它的核心不是“拟合直线”,而是“用线性组合去逼近真实概率的对数几率(log-odds)”。这决定了它对异常值敏感、对特征尺度极度挑剔、对非线性边界束手无策——但反过来说,只要数据满足线性可分假设、特征工程到位、概率校准合理,它比很多黑盒模型更可解释、更稳定、更易部署。本文不讲推导,只讲你明天就能改的三件事:为什么sklearn.LogisticRegression默认C=1.0会害你线上翻车;怎么用predict_proba()输出的两个概率值反向验证模型是否真在学概率;以及——最关键的——当你的训练集正负样本比是1:100时,class_weight='balanced'背后到底调了哪几个参数、调了多少、为什么不能光靠它救命。适合正在赶机器学习大作业、准备西电/山大/湖南大学期末考、或刚接手信贷风控模型的工程师。

2. 从概率论出发:逻辑回归不是“套个sigmoid”,而是最大似然估计的必然结果

2.1 为什么逻辑回归的输出必须是概率?——从伯努利分布到似然函数

逻辑回归的起点不是“我要分类”,而是“我假设每个样本 y_i 是独立同分布的伯努利随机变量,其成功概率 p_i 由 x_i 决定”。这句话翻译成数学就是:
y_i ∼ Bernoulli(p_i),其中 p_i = P(y_i = 1 | x_i)

而伯努利分布的概率质量函数(PMF)是:
P(y_i | p_i) = p_i^{y_i} (1 - p_i)^{1 - y_i}

把所有样本联合起来,得到似然函数:
L(θ) = ∏_{i=1}^n p_i^{y_i} (1 - p_i)^{1 - y_i}

注意:这里 p_i 不是常数,它是 x_i 和参数 θ 的函数。逻辑回归强行规定:
p_i = σ(θ^T x_i) = 1 / (1 + exp(-θ^T x_i))

代入后,似然函数变成 θ 的函数。我们不直接最大化 L(θ),而是最大化它的对数(计算更稳、求导更简单):
ℓ(θ) = ∑_{i=1}^n [ y_i log(p_i) + (1 - y_i) log(1 - p_i) ]

这就是对数似然函数。你会发现,它和 sklearn 里LogisticRegression默认使用的loss='log_loss'完全一致——只是符号相反(因为优化器默认最小化损失,所以实际最小化的是 -ℓ(θ))。

提示:别再记“逻辑回归用交叉熵损失”这种模糊说法。准确说是“在伯努利分布假设下,最大似然估计导出的对数似然函数,取负号后即为二分类交叉熵损失”。这是它不可替代的理论根基,不是随便选的。

2.2 为什么用 sigmoid?——它本质是 log-odds 的反函数

“log-odds”(对数几率)定义为:
log(p / (1 - p))

如果 p 是真实概率,那么 log-odds 可以取任意实数(-∞ 到 +∞),而线性模型 θ^T x_i 的输出也正好是任意实数。所以最自然的建模方式就是:
θ^T x_i = log(p_i / (1 - p_i))

解这个方程,就得到:
p_i = exp(θ^T x_i) / (1 + exp(θ^T x_i)) = 1 / (1 + exp(-θ^T x_i)) = σ(θ^T x_i)

所以 sigmoid 不是“为了平滑”或“为了让输出在0-1之间”而硬加的激活函数,它是从线性可加性假设(log-odds 线性)严格推导出来的唯一解。这也是为什么逻辑回归要求特征与 log-odds 线性相关——如果真实关系是 log-odds = θ^T x_i + γ x_i^2,那逻辑回归就会系统性偏差。

2.3 正则项从哪来?——不是“防止过拟合”的经验补丁,而是先验分布的贝叶斯解释

标准逻辑回归损失函数是:
J(θ) = -ℓ(θ) + λ ||θ||_2^2

前半部分来自最大似然,后半部分呢?贝叶斯视角下,如果我们假设参数 θ 服从均值为0、方差为 σ^2 的高斯先验:
θ ∼ N(0, σ^2 I)

那么后验概率 P(θ | D) ∝ P(D | θ) P(θ),取对数后:
log P(θ | D) ∝ ℓ(θ) + log P(θ) ∝ ℓ(θ) - (1/(2σ^2)) ||θ||_2^2

最大化后验(MAP)等价于最小化:
-ℓ(θ) + (1/(2σ^2)) ||θ||_2^2

对比发现:λ = 1/(2σ^2)。也就是说,L2正则强度 λ 直接对应你对参数“应该多小”的信念强度。σ^2 越小(你越相信参数该接近0),λ 就越大。sklearn 中C = 1/λ,所以C越小,正则越强——这和直觉相反,但有坚实的概率论依据。

3. 动手复现:用 NumPy 从零实现逻辑回归,看清每一步的数学含义

3.1 构造数据:模拟真实场景中的不平衡与噪声

我们生成一个符合逻辑回归假设的数据集:真实 log-odds 是线性的,但加入少量高斯噪声,并人为制造 1:5 的正负样本比(模拟信贷场景中坏账率低的特点):

import numpy as np import matplotlib.pyplot as plt np.random.seed(42) n_samples = 1000 # 真实权重:x1 对风险影响大,x2 影响小,截距为 -2(使正样本少) true_theta = np.array([2.0, 0.5, -2.0]) # [w1, w2, b] # 生成特征:x1 服从 N(0,1),x2 服从 N(1,0.5),避免全零 X = np.random.randn(n_samples, 2) X[:, 1] = np.random.normal(1, 0.5, n_samples) # 添加偏置列 X_with_bias = np.column_stack([X, np.ones(n_samples)]) # 计算真实 log-odds,加噪声 log_odds_true = X_with_bias @ true_theta log_odds_noisy = log_odds_true + np.random.normal(0, 0.5, n_samples) # 转为概率,再采样二值标签 p_true = 1 / (1 + np.exp(-log_odds_true)) p_noisy = 1 / (1 + np.exp(-log_odds_noisy)) y = np.random.binomial(1, p_noisy) print(f"正样本比例: {y.mean():.3f} ({y.sum()}/{n_samples})") # 输出:正样本比例: 0.198 (198/1000)

这段代码的关键在于:p_noisy是带噪声的真实概率,y是按此概率采样的伯努利结果。这比直接用p_true生成y更贴近现实——模型永远看不到真实 p,只能看到采样后的 y。

3.2 损失函数与梯度:手写对数损失及其导数

def sigmoid(z): # 防止溢出:z > 0 时用 1 - 1/(1+exp(z)), z < 0 时用 1/(1+exp(-z)) return np.where(z >= 0, 1 / (1 + np.exp(-z)), np.exp(z) / (1 + np.exp(z))) def logistic_loss(theta, X, y, C=1.0): """ 逻辑回归损失函数:-log-likelihood + L2 penalty theta: (n_features + 1,) 参数向量 X: (n_samples, n_features + 1) 设计矩阵(含偏置) y: (n_samples,) 标签向量 C: sklearn 风格的正则强度(C越大,正则越弱) """ n_samples = len(y) z = X @ theta # 线性组合 p = sigmoid(z) # 对数似然部分:sum [ y_i * log(p_i) + (1-y_i) * log(1-p_i) ] # 注意:log(0) 会报错,用 clip 防止 p 过于接近 0 或 1 p_clipped = np.clip(p, 1e-15, 1 - 1e-15) log_likelihood = np.sum(y * np.log(p_clipped) + (1 - y) * np.log(1 - p_clipped)) # L2 正则项:lambda * ||theta||^2,其中 lambda = 1/(2*C) # 注意:通常不正则化偏置项(theta[-1]),但 sklearn 默认正则全部参数 # 这里按 sklearn 行为,正则全部 l2_penalty = (1.0 / (2 * C)) * np.sum(theta ** 2) return -log_likelihood + l2_penalty def logistic_gradient(theta, X, y, C=1.0): """ 损失函数对 theta 的梯度 """ n_samples = len(y) z = X @ theta p = sigmoid(z) # 对数似然梯度:X.T @ (p - y) # 推导:d/dθ [y*log(p) + (1-y)*log(1-p)] = (y/p - (1-y)/(1-p)) * dp/dθ # 而 dp/dθ = p*(1-p)*X,最终简化为 X.T @ (p - y) grad_ll = X.T @ (p - y) # L2 正则梯度:(1/C) * theta grad_l2 = (1.0 / C) * theta return grad_ll + grad_l2

关键点说明:

  • sigmoid函数做了数值稳定处理,避免exp(100)溢出;
  • logistic_loss中p_clipped防止log(0);
  • 梯度grad_ll = X.T @ (p - y)是核心——它表明:当预测概率p_i大于真实标签y_i(即模型过度自信预测为1,但实际是0),梯度为正,推动参数减小;反之亦然。这是“误差驱动更新”的直观体现;
  • 正则梯度(1/C) * theta直接拉参数向0,C越小,拉力越强。

3.3 梯度下降训练:监控损失与梯度范数,判断收敛

def gradient_descent(X, y, theta_init, C=1.0, lr=0.01, max_iter=1000, tol=1e-5): theta = theta_init.copy() losses = [] grad_norms = [] for i in range(max_iter): loss = logistic_loss(theta, X, y, C) grad = logistic_gradient(theta, X, y, C) # 记录历史 losses.append(loss) grad_norms.append(np.linalg.norm(grad)) # 更新参数 theta_new = theta - lr * grad # 检查收敛:梯度足够小 if np.linalg.norm(grad) < tol: print(f"GD converged at iteration {i}") break theta = theta_new return theta, losses, grad_norms # 初始化参数(全零) theta_init = np.zeros(X_with_bias.shape[1]) theta_opt, losses, grad_norms = gradient_descent( X_with_bias, y, theta_init, C=1.0, lr=0.01, max_iter=2000 ) plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(losses) plt.title("Loss vs Iteration") plt.xlabel("Iteration") plt.ylabel("Loss") plt.subplot(1, 2, 2) plt.plot(grad_norms) plt.title("Gradient Norm vs Iteration") plt.xlabel("Iteration") plt.ylabel("||∇J||") plt.yscale('log') plt.show() print(f"Optimized theta: {theta_opt}") print(f"True theta: {true_theta}")

运行后你会看到:损失单调下降,梯度范数指数衰减。最终theta_opt应该接近true_theta(允许一定误差,因有噪声)。这个过程让你亲眼看到:损失函数不是抽象概念,它是可计算、可监控、可调试的实体。如果grad_norms不下降,说明学习率lr太大(震荡)或太小(爬行);如果losses后期波动,说明lr没衰减。

4. 避坑:逻辑回归落地中最常踩的5个坑,每个都让模型在生产环境静默失效

4.1 坑1:C=1.0是万能默认值?——它在不平衡数据上会让模型彻底放弃少数类

现象:训练集 AUC 0.95,测试集 AUC 0.65;混淆矩阵显示模型几乎全预测为负类(如“不违约”),召回率(Recall)接近 0。
原因:C=1.0对应lambda=1.0,正则强度固定。当正负样本比悬殊(如 1:100),模型发现:把所有样本预测为负类,损失函数中-log-likelihood部分 ≈ 0(因 y=0 占绝大多数),而正则项很小,总损失极低。模型“学会”了偷懒。
解决:

  • 方法一(推荐):用class_weight='balanced',它内部计算weight_for_class_k = n_samples / (n_classes * n_samples_in_class_k),然后在损失函数中加权:J(θ) = -∑ w_i [y_i log(p_i) + (1-y_i) log(1-p_i)] + ...
  • 方法二:手动设置class_weight={0:1, 1:100}(若正样本是 class 1);
  • 方法三:用sample_weight给每个正样本赋更高权重。

注意:class_weight='balanced'不改变正则强度,只改变损失函数中每个样本的贡献权重。它不能替代特征工程或阈值调整。

4.2 坑2:用predict()而不用predict_proba()—— 丢失所有概率信息,无法做阈值优化

现象:模型上线后,业务方说“坏账率太高”,你调低阈值,结果通过率暴跌;或者“审批太严”,你调高阈值,坏账率又飙升。反复试错,没有量化依据。
原因:predict()只返回 0/1,硬切在阈值 0.5。但最优阈值 rarely 是 0.5,尤其在不平衡场景。predict_proba()返回[p0, p1],让你能画 ROC 曲线、选 Youden 指数最大点、或按业务成本设定阈值。
解决:

from sklearn.metrics import roc_curve, auc, precision_recall_curve proba = model.predict_proba(X_test)[:, 1] # 取正类概率 fpr, tpr, _ = roc_curve(y_test, proba) roc_auc = auc(fpr, tpr) # 找 Youden 指数最大点(灵敏度+特异度-1 最大) youden = tpr - fpr optimal_idx = np.argmax(youden) optimal_threshold = _[optimal_idx] print(f"Optimal threshold: {optimal_threshold:.3f}, Youden: {youden[optimal_idx]:.3f}")

4.3 坑3:特征没标准化,C参数失去物理意义,网格搜索失效

现象:C在[0.01, 1, 100]上调优,发现C=0.01最好;但换一组量纲不同的特征(如收入单位从“元”变“万元”),同样的C=0.01却导致欠拟合。
原因:L2 正则项||θ||^2对特征尺度极度敏感。若x1范围是 [0,1000],x2是 [0,0.001],那么θ1会被强烈压缩,θ2几乎不受约束。C的“强度”完全取决于特征单位。
解决:

  • 必须在LogisticRegression前加StandardScaler(或MinMaxScaler);
  • StandardScaler后,所有特征均值为0、标准差为1,此时C才有跨特征可比性;
  • 网格搜索C时,范围设为[0.001, 0.01, 0.1, 1, 10, 100]足够覆盖。

4.4 坑4:忽略多重共线性,coef_解释失效,C微调引发系数剧烈震荡

现象:model.coef_显示x1系数是 5.2,x2是 -3.1;但当你移除x2,x1系数变成 8.7;或者C从 1.0 改为 0.9,x1系数从 5.2 跳到 12.4。
原因:当x1和x2高度相关(如x2 = 0.9*x1 + noise),损失函数的等高线呈狭长椭圆,最小值点不唯一。正则项||θ||^2会偏好“能量分散”的解(即多个小系数),而非“能量集中”的解(一个大系数)。C微小变化,解就在椭圆谷底来回跳。
解决:

  • 用from statsmodels.stats.outliers_influence import variance_inflation_factor计算 VIF,VIF > 5 视为共线性严重;
  • 移除高 VIF 特征,或用 PCA 降维;
  • 改用ElasticNet(L1+L2混合),L1 有特征选择作用,能缓解共线性。

4.5 坑5:用decision_function()当概率——它输出的是 raw log-odds,不是概率

现象:你用model.decision_function(X)得到一堆负数,直接当“置信度”排序,结果 AUC 低于predict_proba()。
原因:decision_function()输出θ^T x_i,即 log-odds。它和概率p_i = σ(θ^T x_i)是非线性关系。例如,log-odds 从 -1 到 0(p 从 0.27→0.5),和从 0 到 1(p 从 0.5→0.73)变化幅度不同,但decision_function值变化相同。用它排序,等价于用 log-odds 排序,而 AUC 本质是概率排序的指标。
解决:

  • 一律用predict_proba()[:, 1]做排序和阈值选择;
  • 如果只想快速计算,sigmoid(decision_function(X))等价于predict_proba(X)[:, 1],但没必要绕路。

5. 进阶验证:用校准曲线(Calibration Curve)检验模型是否真在学概率

5.1 为什么 AUC 高不等于概率准?——区分能力 ≠ 概率准确性

AUC 衡量的是模型区分正负样本的能力(ranking quality),但它对概率值本身的准确性(calibration)不做要求。一个模型可以完美排序(AUC=1.0),但所有p_i都被系统性高估(如真实 p=0.1 的样本,模型输出 p=0.3)。这在需要概率做决策的场景(如风险定价、动态阈值)中是灾难性的。校准曲线(Reliability Diagram)就是专门检验这个的。

5.2 绘制校准曲线:分箱统计,看“预测概率” vs “实际频率”

from sklearn.calibration import CalibratedClassifierCV from sklearn.isotonic import IsotonicRegression from sklearn.calibration import calibration_curve import numpy as np # 原始模型(未校准) proba_raw = model.predict_proba(X_test)[:, 1] # 校准模型(Platt scaling,即用sigmoid拟合log-odds) calibrated_model = CalibratedClassifierCV(model, method='sigmoid', cv=3) calibrated_model.fit(X_train, y_train) proba_calibrated = calibrated_model.predict_proba(X_test)[:, 1] # 绘制校准曲线 plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) fraction_of_positives, mean_predicted_value = calibration_curve( y_test, proba_raw, n_bins=10, strategy='uniform' ) plt.plot(mean_predicted_value, fraction_of_positives, marker='o', label='Uncalibrated') plt.plot([0, 1], [0, 1], 'k:', label='Perfectly calibrated') plt.xlabel('Mean Predicted Probability') plt.ylabel('Fraction of Positives') plt.title('Calibration Curve (Raw)') plt.legend() plt.subplot(1, 2, 2) fraction_of_positives_cal, mean_predicted_value_cal = calibration_curve( y_test, proba_calibrated, n_bins=10, strategy='uniform' ) plt.plot(mean_predicted_value_cal, fraction_of_positives_cal, marker='o', label='Calibrated') plt.plot([0, 1], [0, 1], 'k:', label='Perfectly calibrated') plt.xlabel('Mean Predicted Probability') plt.ylabel('Fraction of Positives') plt.title('Calibration Curve (Sigmoid)') plt.legend() plt.tight_layout() plt.show()

解读图表:

  • 左图(Raw):若点都在对角线上,说明“预测概率为0.3的样本中,实际有30%是正类”,即完美校准;若整体高于对角线(如预测0.5,实际0.7),说明模型悲观(under-confident);低于则乐观(over-confident)。
  • 右图(Calibrated):Platt scaling 用一个 sigmoid 函数拟合log-odds和y的关系,强制让输出概率更接近真实频率。你会发现右图点更靠近对角线。

5.3 校准不是万能药:何时校准有效,何时无效?

场景校准效果原因应对
模型本身线性假设成立,但正则过强(C太小)显著改善过正则压制了 log-odds 的尺度,sigmoid 可拉伸恢复调大 C,再校准
特征存在强非线性(如真实 log-odds = x₁²)改善有限逻辑回归结构错误,校准只能修“表”,不能改“里”加入多项式特征或换树模型
训练集和测试集分布偏移(如时间漂移)可能恶化校准在训练集上拟合,对新分布泛化差用最新数据重训+校准,或用保序回归(Isotonic)

我的习惯是:每次上线逻辑回归,必跑校准曲线。如果原始模型的 Brier Score(概率版 MSE)> 0.1,或校准曲线上最大偏离 > 0.15,我就不会直接用predict_proba()做定价,而是先校准。这不是玄学,是把概率论从课本搬到生产的最后一道保险。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进