对数几率回归(逻辑回归)原理与手写实现:从极大似然到梯度下降
2026/9/16 3:09:11 网站建设 项目流程

这篇是《机器学习》系列的第五篇,主线就一件事:对数几率回归,也就是大家常说的逻辑回归(Logistic Regression),以及如何用极大似然法把它的参数求出来。先别被名字骗了,虽然它挂着“回归”的招牌,干的却是分类的活儿,而且是所有分类模型里最基础、最值得手推一遍的那个。这篇文章适合三类人:正在入门机器学习、想搞懂分类器底层原理的同学;马上要考“西电机器学习期末”这类考试、正在复习对数几率回归的同学;以及那些觉得“sklearn调包我也会,但让我手写一个就懵了”的人。我尽量不堆公式吓人,但该有的推导一步都不会省——尤其梯度推导那一段,建议你拿纸跟着画一遍,很多东西一下子就通了。

1. 为什么线性模型解决不了分类问题

1.1 线性回归在分类任务上的三个尴尬之处

上一篇文章我们聊了线性回归,它的核心是用一条直线(或者一个超平面)去拟合连续数值,比如预测房价、预测气温。现在问题来了:如果我想判断一封邮件是不是垃圾邮件、一个病人有没有糖尿病、一个用户会不会流失,这些标签是离散的,通常是 0 和 1。能不能直接拿线性回归的预测值来做分类?

理论上你可以说:输出大于等于 0.5 判为正类,否则判为负类。但实际一跑就会发现问题。第一,线性回归的输出范围是整个实数域,预测房价 30 万、50 万能理解,但预测“垃圾邮件概率”输出 1.8 是什么意思?概率不可能大于 1。第二,线性回归的最小二乘目标函数,本质上是假设误差服从高斯分布,但分类标签显然不是高斯分布的——它不是连续变量。第三,也是最直观的问题:线性回归对离群点极其敏感。你在一堆点旁边多放几个异常点,整条拟合直线会被硬生生拉偏,决策边界跟着平移,原本能分干净的类别全乱了。这几个问题合在一起,说明线性回归的“输出”形式和“目标”之间是有本质错位的。

1.2 需要一个把实数映射到 0 到 1 的函数

既然直接输出实数不行,那很自然的想法就是:能不能找这样一个函数,把线性回归的输出 z = w^T x + b 压缩到 (0, 1) 区间,这样我就可以把它当成概率来解读。

你可能会先想到单位阶跃函数:z 大于 0 输出 1,小于 0 输出 0,等于 0 输出 0.5。但这个函数有两个致命伤:它不连续,更不可导。后面所有梯度下降的优化方法都需要求导,一个不可导的函数根本没法用梯度去更新参数。所以我们需要一个“平滑版的阶跃函数”,这就是 sigmoid 函数(也叫 Logistic 函数):

σ(z) = 1 / (1 + e^(-z))

这个函数的性质非常漂亮:当 z 趋近正无穷时,σ(z) 趋近 1;当 z 趋近负无穷时,σ(z) 趋近 0;z = 0 时恰好是 0.5。而且它处处可导,导数还有一个极其好用的形式:

σ'(z) = σ(z)(1 - σ(z))

这个性质在后面推导梯度时会让代码简化到令人发指的地步,你可以先记住这个结论,后面会反复用到。

现在我们把线性回归的输出塞进 sigmoid:p = σ(w^T x + b),p 就落在 0 到 1 之间,表示样本属于正类的概率。这也解释了为什么叫“对数几率回归”——你把式子倒过来算一下:

ln(p / (1 - p)) = w^T x + b

这个 p/(1-p) 叫“几率”(odds),再取对数就是“对数几率”(log odds)。也就是说,对数几率回归本质上还是在做线性回归,只是它回归的目标不是 y,而是 y 的对数几率。这么一看,“回归”两个字也就不那么违和了。

2. 极大似然法怎么搭起损失函数

2.1 极大似然思想:找到让“已经发生的事”出现概率最大的参数

在动手求参数之前,得先解决一个问题:什么才算“好”的参数?在线性回归里,我们追求预测值和真实值的平方误差最小;但在分类任务里,我们希望模型给出的概率尽量贴近真实标签。这个目标可以用一个经典的思想来落地——极大似然估计。

我用自己的话解释一下极大似然:你现在手里有一枚不知道是否均匀的硬币,扔了 10 次,结果是 9 次正面、1 次反面。请问你认为这枚硬币正面朝上的概率 p 更接近 0.5 还是 0.9?正常人都会觉得是 0.9,因为如果 p = 0.5,扔出“9 正 1 反”这种结果的概率很小;而如果 p = 0.9,这个结果就合理多了。极大似然法就是把这种直觉严格数学化:先写出在当前参数下,观测到这一组数据的概率 L(θ),然后找一组让 L(θ) 最大的参数 θ。这个 L(θ) 就叫“似然函数”。

一个特别容易踩的坑是:似然和概率在概念上容易混淆。概率是在参数已知的情况下,预测某个结果出现的可能性;似然则是在结果已经出现的情况下,反过来评估不同参数“解释”这个结果的合理程度。逻辑回归用的正是后者:我们手上的训练集已经是事实了,要找一组参数,让这组事实出现的可能性最大化。

2.2 从似然函数到我们熟悉的交叉熵损失

现在把极大似然用到逻辑回归上。设训练集有 N 个样本,第 i 个样本的特征是 x_i,标签 y_i 取 0 或 1。模型给出的概率是:

P(y_i = 1 | x_i) = σ(w^T x_i + b)

为了让式子能同时处理 y_i = 0 和 y_i = 1 两种情况,有一个很经典的写法:

P(y_i | x_i) = p_i^{y_i} (1 - p_i)^(1 - y_i)

你可以验证一下:当 y_i = 1 时,这个式子只剩下 p_i;当 y_i = 0 时,只剩下 1 - p_i。这个写法在推导中会反复出现,一定要看习惯。

假设各样本独立,那么整个训练集的似然函数就是所有样本概率的连乘:

L(w, b) = ∏_{i=1}^N p_i^{y_i} (1 - p_i)^(1 - y_i)

连乘有个问题:乘的数一多,结果会变得特别小,计算机算着算着就下溢(变成 0)了。所以常规操作是取对数,把连乘变成连加,而且对数函数是单调递增的,不会改变最大值对应的参数位置:

ln L(w, b) = Σ_{i=1}^N [y_i ln p_i + (1 - y_i) ln(1 - p_i)]

这个叫“对数似然”。机器学习里我们习惯最小化损失函数,于是加个负号,再除以 N 求平均,就得到:

J(w, b) = -1/N Σ_{i=1}^N [y_i ln p_i + (1 - y_i) ln(1 - p_i)]

眼熟吗?这就是交叉熵损失。信息论里交叉熵衡量的是两个概率分布的差异,这里衡量的是模型预测分布和真实标签分布之间的差距。所以逻辑回归用极大似然推出交叉熵,不是偶然的,它本质上是在最小化预测分布和真实分布之间的“信息距离”。

2.3 一个关键问题:为什么不能用最小二乘

看到这里可能有同学会问:上一篇线性回归用的最小二乘不是挺好的吗?逻辑回归能不能接着用平方误差?

我建议你不要这么做,原因有两层。第一层从概率视角看:分类标签是伯努利分布,不是高斯分布,最小二乘对应的概率假设从一开始就不成立。第二层从优化视角看:把 sigmoid 的输出套进平方误差,得到的损失函数是非凸的,里面会有很多局部极小值,梯度下降很容易陷进去;而交叉熵损失是凸函数,理论上能够收敛到全局最优。当年我为了验证这一点,专门画过不同参数的损失曲线——平方误差的线是波浪形的,像一个一个小坑;交叉熵的线是平滑的碗状,一眼就能看出来哪个好优化。

所以结论很明确:极大似然法给逻辑回归“规定”了正确的损失函数,那就是交叉熵,不是拍脑袋定的,是推出来的。

3. 梯度下降求解参数:手推一遍你就全明白了

3.1 关键推导:损失函数对参数的梯度长什么样

损失函数搭好了,接下来就用梯度下降去迭代求解参数。这里我强烈建议你跟着手推一遍,因为这一套链式法则在神经网络里还会反复出现,推一次保底用三年。

为方便起见,我们令 z_i = w^T x_i + b,p_i = σ(z_i)。损失函数对 w 求梯度,用链式法则拆成三步:

∂J/∂w = ∂J/∂p_i · ∂p_i/∂z_i · ∂z_i/∂w

逐项看。第一项:

∂J/∂p_i = -(y_i / p_i) + (1 - y_i)/(1 - p_i) = (p_i - y_i) / (p_i(1 - p_i))

第二项,用 sigmoid 导数的性质:

∂p_i/∂z_i = p_i(1 - p_i)

第三项:

∂z_i/∂w = x_i

三项一乘,后面两个分母约掉了,只剩:

∂J/∂w = (1/N) Σ_{i=1}^N (p_i - y_i) x_i

就这么干净。偏置 b 的梯度也一样,差别只在最后一项 ∂z_i/∂b = 1:

∂J/∂b = (1/N) Σ_{i=1}^N (p_i - y_i)

我想特别强调一下这个结果的直观含义:梯度里最关键的量是 (p_i - y_i),也就是“预测概率减真实标签”。预测对了一个样本(p_i 接近 y_i),这一项就接近 0,它对参数的贡献就小;预测错了,这一项就大,参数就往正确的方向修正一下。这跟线性回归的梯度形式有异曲同工之妙——线性回归的梯度是 (预测值 - 真实值) × 特征,逻辑回归的梯度是 (预测概率 - 真实标签) × 特征。唯一区别就是一个用的是连续预测值,一个用的是 sigmoid 压缩后的概率。这也是为什么很多框架底层实现里,逻辑回归和线性回归的代码可以共用同一套梯度框架,只是前面的“模型函数”不同。

3.2 参数更新公式与实现视角的简化

有了梯度,参数更新就顺理成章了:

w ← w - η · (1/N) Σ_{i=1}^N (p_i - y_i) x_i

b ← b - η · (1/N) Σ_{i=1}^N (p_i - y_i)

其中 η 是学习率,控制每次迈的步子大小。注意我们这里用的是批量梯度下降,也就是每轮迭代把全部 N 个样本都算一遍再更新一次。实际工程里数据量很大的时候,一般会用小批量梯度下降(mini-batch),每次随机抽一小批样本来算梯度,原理完全一样,只是迭代的节奏更快。

为了效率,实现的时候通常把整个训练集向量化计算。也就是把所有样本的特征矩阵 X 一次性传进去,算出所有 p_i,再统一更新参数。这个操作比 for 循环逐样本算快非常多,尤其在你的特征维度和样本量稍微上来一点之后,差距是两个数量级的。不要觉得“反正代码能跑就行”,手写模型是一次非常好的思维训练,但写出来的东西也得有工程项目的基本素养。

4. 实战:只用 NumPy 手写一个对数几率回归

4.1 准备数据与预处理:标准化为什么不能省

理论讲完,动手写代码。这里我用鸢尾花数据集做二分类:取前两个类别(setosa 和 versicolor),只保留两个特征(花萼长度和花瓣长度),方便可视化。整个流程就四步:加载数据、标准化特征、训练模型、画决策边界。

第一步必须先做特征标准化。很多初学者不理解:sklearn 里跑逻辑回归不标准化也经常能出结果啊?那是因为 sklearn 内部帮你做了很多数值上的兜底,而且样例数据本身量级还不算太离谱。但如果你自己手写梯度下降,不标准化就会踩大坑:两个特征一个量级是 1,另一个量级是 100,损失函数在参数空间里会变成一个很扁的椭圆,梯度下降走起来像在滑冰,一会儿往东跑一会儿往西跑,半天到不了最低点。标准化之后,特征均值变成 0、方差变成 1,损失函数的“碗”就接近圆形了,梯度下降的路径会直很多。这一步在所有基于梯度的模型里都是标配,不是可选项。

4.2 核心代码:训练、预测与损失监控

下面这段代码就是完整的核心逻辑,我建议你复制到 Jupyter 里跑一遍:

import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split def sigmoid(z): # 防止 exp 溢出,做了一个小保护 # 更严谨可以分段处理,但这样写对大多数场景够用 return 1 / (1 + np.exp(-np.clip(z, -500, 500))) def compute_loss(X, y, w, b): p = sigmoid(X @ w + b) # 防止 log(0) 导致 nan,加一个极小值 eps = 1e-12 loss = -np.mean(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps)) return loss def train_logistic_regression(X, y, lr=0.1, epochs=1000): n_samples, n_features = X.shape w = np.zeros(n_features) b = 0.0 loss_history = [] for epoch in range(epochs): p = sigmoid(X @ w + b) # 梯度:向量化的写法,一次算完 dw = (1 / n_samples) * (X.T @ (p - y)) db = (1 / n_samples) * np.sum(p - y) w -= lr * dw b -= lr * db if epoch % 100 == 0: loss = compute_loss(X, y, w, b) loss_history.append(loss) print(f"epoch {epoch}, loss: {loss:.6f}") return w, b, loss_history # 加载数据:只取前两类和两个特征 iris = load_iris() X = iris.data[:100, :2] y = iris.target[:100] # 0 和 1 # 标准化 mean = X.mean(axis=0) std = X.std(axis=0) X_std = (X - mean) / std w, b, loss_history = train_logistic_regression(X_std, y, lr=0.1, epochs=1000)

跑完之后你会看到 loss 在逐步下降,前几百轮降得很快,后面慢慢趋于平缓。这就是梯度下降的正常节奏——最开始离最佳点远,梯度大,步子也大;越靠近底部,梯度越小,参数更新越细腻。如果你看到 loss 一路飙到 nan,别慌,八成是学习率太大或者特征没标准化,去查一遍这两项基本能解决。

4.3 决策边界与模型评估

训练完之后,我习惯把决策边界画出来看一眼。因为 sigmoid 是单调函数,决策边界就是 z = 0 那条线,也就是 w1·x1 + w2·x2 + b = 0。这是一条直线,所以逻辑回归本质上是线性分类器。

你在二维平面上把散点图画出来,再画这条直线,会看到它正好把两类点分在两侧。如果数据本身线性不可分(比如一个圆形的类别包围另一个类别),逻辑回归的直线边界就不够用了。这时候要么做特征工程(比如加入 x1^2、x1·x2 这样的组合特征),要么换非线性模型。记住一句话:逻辑回归的“线性”是特征空间里的线性,不是数据分布的线性。

评估分类模型当然要看准确率,但对类别不平衡的数据,准确率会骗人。更合理的评估指标是精确率、召回率、F1,以及 ROC-AUC。在鸢尾花这个例子上这两类完全可分,准确率会接近 100%,看不出来什么门道;但你心里要清楚,真正工业级项目里极少有这么干净的数据。

5. 常见问题与排查技巧实录

5.1 损失震荡、发散或不下降怎么办

这是手写逻辑回归时遇到最多的问题。我的排查顺序通常是:先打印每个 epoch 的 loss,看曲线形态。如果 loss 忽高忽低像过山车,第一件事是把学习率调小 10 倍再试。学习率本质上是控制“步子大小”,步子太大就会一步迈过头,从一个坑沿跳到另一个坑沿,看起来就是在震荡。如果 loss 前几轮就变成 nan,大概率是梯度数值爆炸了,这时候除了调小学习率,还要检查特征是否做了标准化。很多人在 i 的特征上忘了标准化,然后大数相乘直接把梯度推到无穷大——这是新手最容易踩的坑,没有之一。

另外有个小技巧:如果损失曲线下降得很慢,不一定是坏了,可能是学习率太小。我见过有人 lr 设成 1e-6,训练了 5000 轮 loss 还在原地缓慢爬行。默认值建议从 0.1 开始,观察曲线再逐步调整,比凭空猜一个数靠谱得多。

5.2 参数初始化:为什么全 0 也能训

用神经网络的同学可能会疑惑:神经网络里参数不能全 0 初始化,因为有对称性问题。但逻辑回归没有隐藏层,模型就是一个直观的线性映射压缩到 sigmoid,它不存在对称性问题,所以全 0 初始化完全没毛病。我上面的代码就是这么干的。当然你随机初始化也行,得到的最终结果基本一样,因为交叉熵损失是凸函数,不管你从哪儿出发,理论上都会收敛到同一个全局最优解。这点跟后面学深度学习是完全不同的心态,学的时候感受一下这个对比也挺有意思。

5.3 类别不平衡:换阈值比换模型更快

真实业务里,正负样本比例经常是很离谱的,比如 1000 个用户只有 3 个会付费。这时候直接拿 0.5 当分类阈值会让你预测出一堆“负类”,看起来准确率很高,但正类几乎全漏。一个比换模型更快的补救办法是调整分类阈值:预测概率超过 0.3 甚至 0.2 就判为正类,然后去看精确率和召回率如何权衡。还可以给少数类样本加大权重,sklearn 的 LogisticRegression 里有个 class_weight='balanced' 参数,做的事情就是按类别频率自动放大少数类的损失贡献。手写实现的时候,你只要在损失函数和梯度里给正类样本的损失乘一个系数就行,改动不超过三行。

5.4 过拟合与正则化:加一个惩罚项就完事

逻辑回归虽然是线性模型,但当特征维度特别高、或者样本量很少的时候,照样会过拟合。最直接的信号就是训练准确率接近 100%,测试准确率却掉得厉害。常规解法是在损失函数后面加 L2 正则项:J_reg = J + (λ / 2N) ||w||^2,梯度里多一项 (λ/N) w。λ 越大,对“大权重”的惩罚越狠,模型就越“保守”。这个改动在手写代码里几乎不费劲,效果却很显著。我自己的经验是,在特征上百个、样本只有几百个的任务里,加不加正则的差距肉眼可见。

5.5 多分类:从二分类推广到 Softmax

这篇文章讲的是二分类逻辑回归,但你迟早会遇到三分类甚至更多的情况。两种主流做法:一是 One-vs-Rest(一对多),也就是训练 K 个二分类器,每个分类器负责判断“是不是第 k 类”,最后取得分最高的类;二是直接用 Softmax 回归,把 sigmoid 推广到多类,输出一个概率分布。Softmax 这名字你在深度学习里肯定见过——神经网络最后一层接的就是它。所以你会发现,搞懂二分类逻辑回归其实是理解深度学习的半条腿,另一半是反向传播和表示学习。

5.6 概率校准:别把你的 0.9 太当回事

最后聊一个进阶但很重要的话题:逻辑回归输出的概率,虽然比一般模型准,但严格来说它未必是“真实的概率”。在某些领域(比如风控、医疗),你需要的是校准良好的概率,也就是“预测 0.8 的样本实际也真的有大约 80% 是正类”。这时候可以额外做一步概率校准,常用的两个方案是 Platt Scaling(在模型的 logit 输出上再拟合一个逻辑回归)和 Isotonic Regression(保序回归,不假设单调形式)。判断概率是否校准的常用工具是校准曲线(calibration curve)。我的建议是:如果你的应用场景只是一个排序问题(比如给用户推荐 ABCD 排个序),那校准不校准无所谓;如果要做风险决策(比如判断要不要给这笔贷款放款),那就别省这一步。

写到最后,还是想分享一个我在实操里的小习惯:每学一个新模型,我都逼自己关掉 sklearn,先用 NumPy 从零写一遍训练流程,再打开框架源码做对照。做逻辑回归这一步时,你会第一次体验到“原来一个分类器本质上就是一条线加一个压缩函数加一个交叉熵”这种极简美感。这个底子打牢了,后面学 Softmax、学 MLP、哪怕是学 Transformer 的分类头,你都会觉得异常亲切——因为它们求梯度的核心思路,和这篇文章里推的东西是同一条脉络。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询