☰
logistic回归原理与实操:从sigmoid到交叉熵的二分类深度学习入门
2026/10/16 12:37:27 网站建设 项目流程

说起深度学习课程里的“实验三”,十有八九会撞上这样一句话:用 logistic 回归预测二分类。我见过不少同学看到题目后的第一反应是“这算哪门子深度学习,不就是个线性模型套了个激活函数吗”,真等自己上手跑完,才发现这个实验从头到尾踩的坑,比后面好多大型网络实验加起来都要多。

其实 logistic 回归是理解整个深度学习最理想的一颗钉子。从网络结构上看,它就是一个没有隐藏层的单神经元模型:输入特征做加权求和,过一个 sigmoid 激活,输出一个 0 到 1 之间的概率。这套流程你吃透了,后续学多层感知机、CNN、RNN 时就会发现,大家都是在同一个骨架上做文章,只是把单神经元变成了大网络,把线性加权换成了卷积或循环结构。当年我带一位零基础的 A 同学做这个实验,他花了一个下午把前向传播、损失函数、梯度更新通了,隔周再去看多层网络的反向传播,明显比别人轻松一大截。

这篇文章我会把这个实验从原理到实操完整拆开,把 sigmoid 为什么长这样、交叉熵为什么比均方误差好用、数据预处理怎么做不泄露、训练曲线怎么看这些“老师不细讲但考试和工作都要考”的东西,一次性说清楚。适合正在做课程实验的同学,也适合想回头把这颗地基补扎实的自学者。

1. 实验整体认知与设计思路

1.1 实验三为什么偏偏选 logistic 回归

很多课程的实验顺序都是精心排过的,把 logistic 回归放在第三个实验,刚好卡在“你已经会线性回归了,但还没接触神经网络”的中间地带。它有两个身份:对经典机器学习来说,它是一个线性分类器;对深度学习来说,它是神经网络最朴素的雏形。

从结构上看,logistic 回归做的事情极其简单:把每个特征乘上一个权重,累加,再加一个偏置,然后过一个 sigmoid 函数把结果压到 0 到 1 之间。这个结构像什么?就是一个最精简的神经元。学完这个单神经元,下一步加一层隐藏层,就是两层神经网络;再加几层,就变成了一个标准的多层感知机。所以实验安排这门课的人,真实意图是让你先用最少的代码,把前向传播、损失计算、梯度反传、参数更新这条完整流水线走一遍。

这个实验的定位决定了它的学习重点不在模型本身的精度,而在于理解训练过程。说实话,二分类任务用 logistic 回归去跑,很多时候精度也不会特别高,尤其是数据不是线性可分的时候,它的表现可能就是“够用”。但正因为模型简单,所有中间过程都可以被清清楚楚地打印出来、画出来、算出来——权重怎么变、损失怎么降、决策边界怎么移动,全部都直观可见。这是深网络给不了你的学习体验。

1.2 二分类预测的建模思路

二分类在现实中随处可见:一封邮件是不是垃圾邮件,一个用户会不会下单,一个病人是不是得了某种疾病,本质上都是“二选一”的判断题。这个实验要做的事情,就是给出一组样本和它们的标签,训练一个模型,让模型在面对没见过的样本时,能给出一个正确的二分类判断。

这里有一个初学者很容易误解的地方:模型输出并不是直接的 0 或 1,而是一个概率值。比如模型对某条样本输出 0.87,意思就是“我觉得它有 87% 的概率属于类别 1”。然后我们再人为约定一个阈值——通常是 0.5——概率大于 0.5 判定为类别 1,否则判定为类别 0。后面我还会讲到,这个阈值不是固定死的,不同业务场景下调阈值可能比调模型参数还管用。

整个实验的建模流程可以归纳成一条清晰的流水线:

  1. 数据准备:拿到特征矩阵 X(形状是 样本数×特征数)和标签向量 y(形状是 样本数×1)。
  2. 数据预处理:标准化、划分训练集和测试集。
  3. 模型定义:初始化权重 W 和偏置 b,写出前向传播函数。
  4. 损失计算:用交叉熵衡量预测概率与真实标签的差距。
  5. 梯度计算与参数更新:让模型在训练数据上一点点往正确的方向调整。
  6. 评估:在测试集上验证泛化效果。

建议你在动手写代码之前,先在纸上把这个流程走一遍,把每一步的输入输出形状写清楚。很多人写着写着就乱了,回头查 bug 半天,最后发现是某个张量的维度没对上。

2. 核心原理拆解:sigmoid 与交叉熵的搭档逻辑

2.1 从线性回归到概率输出:sigmoid 到底在做什么

先回忆一下线性回归:给定特征 x1、x2…… 模型算出一个分数 z = w1x1 + w2x2 + ... + b。在回归任务里,这个 z 可以直接作为预测值。但二分类任务不行——分类要输出的是概率,概率有天然的取值范围,必须在 0 到 1 之间,而线性回归的 z 范围是从负无穷到正无穷,直接拿来当概率是不合理的。

sigmoid 函数就是来解决这个问题的,公式是:

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

它的作用是:把任意实数 z 压缩到 (0, 1) 区间。z 越大,输出越接近 1;z 越小,输出越接近 0;z 恰好为 0 时,输出为 0.5。这个函数图像是一条平滑的 S 型曲线,所以它本质上做的是一件非常优雅的事情:把“证据的强度”转化成“某种结果发生的概率”。

举个例子感受一下:假设某个特征的权重是 0.8,x 每增加一个单位,z 就增加 0.8,sigmoid 输出就会明显地向 1 靠拢。相当于模型每看到一点新的证据,就它对类别 1 的信心就更足一点,而且这种信心的增长不是线性的——靠近中间区域时变化快,到了极值附近就饱和了,这和人类的直觉也蛮契合的。

在代码里实现它也非常简单:

def sigmoid(z): return 1 / (1 + np.exp(-z))

不过这里有个小细节:如果你在损失函数里面用了 log(p),p 可能被算成 0,log(0) 会直接报警。后面我会讲怎么用数值技巧规避。

2.2 决策边界为什么是线性的

把前向传播的公式展开来看:p = σ(w1x1 + w2x2 + ... + b),当我们把判定阈值设为 0.5,也就是 p > 0.5 时预测为类别 1,那实际上等价于看 z = w1x1 + w2x2 + ... + b 是否大于 0。z = 0 这条线,就是模型的决策边界。

因为 z 是特征的线性组合,所以 z = 0 在二维空间里是一条直线,在三维空间里是一个平面,在高维空间里是一个超平面。这给 logistic 回归带来了两个鲜明的特点:

一是可解释性极强。每个特征的权重 W 的正负方向直接告诉我们:这个特征对预测结果是正向推动还是反向推动,绝对值大小代表影响强度。这是很多树模型和深度网络做不到的。

二是模型容量有限。当数据本身线性不可分时(比如两类样本像阴阳鱼一样纠缠在一起),单纯一个线性决策边界是搞不定的。这时你会看到训练精度卡在一个不上不下的位置,怎么调学习率都没用。这不是 bug,而是模型表达能力的限制,后续课程通常会通过引入隐藏层或者核技巧来解决。

2.3 交叉熵为什么完胜均方误差

训练模型需要定义一个损失函数来指导“往哪个方向调整参数”。二分类最常用的损失是交叉熵:

L = -[y·log(p) + (1-y)·log(1-p)]

拆开看:当真实标签 y = 1 时,损失变为 -log(p),p 越接近 1,损失越小;当 y = 0 时,损失变为 -log(1-p),p 越接近 0,损失越小。换句话说,模型越自信地猜错,惩罚越大;越接近正确答案,损失越趋近于 0。这种“对数惩罚”的特性,让模型不敢在错误的方向上过度自信。

为什么不直接用均方误差 MSE = (y - p)² 呢?这是实验报告里特别爱问的一道题。关键在于梯度。把 sigmoid 代入 MSE 之后,梯度里会冒出一个 σ'(z) = σ(z)·(1-σ(z)) 项。这个项有个致命弱点:当 z 很大的时候,σ 接近 1,σ(1-σ) 接近 0;当 z 很小的时候,σ 接近 0,σ(1-σ) 也接近 0。也就是说,模型在预测得越极端、越自信的时候,梯度反而越小,训练变得极其缓慢,这在深度学习中叫“梯度饱和”。

而交叉熵配合 sigmoid 之后,求导结果很漂亮:梯度变成了 (p - y)·x。这个式子直观又合理——预测和真实标签差距越大,梯度的幅度越大,模型学得越快;预测接近正确时,梯度自然变小。这也是为什么深度学习里几乎不会用 MSE 来做分类任务的损失函数。所以你在实验里如果不小心把损失换成了 MSE,会观察到训练半天损失都降不下去,不是代码 bug,而是数学选型出了问题。

建议你在实验中分别用交叉熵和 MSE 各跑一次,把损失下降曲线对比一下。这个对比实验做出来,你对这个知识点的理解会比只看书深刻得多。

3. 实操全流程:从数据准备到训练闭环

3.1 数据准备与预处理的几个细节

做这个实验我建议你先不要直接上复杂数据集,而是自己生成一批可分的二维模拟数据。好处是直观,后面画决策边界时一眼就能看出效果。比如生成两堆数据,类别 0 以某个点为中心呈高斯分布,类别 1 在另一个点附近分布,让它们之间有一条清晰的分界线。

拿到原始数据后,有几步预处理是整个实验成败的关键,很多人第一步就跑偏了。

特征标准化大概是这个实验里第二重要的预处理操作。如果 x1 的取值范围是 0 到 1,而 x2 的范围是 0 到 10000,那权重更新时会被大尺度的特征主导,梯度下降会在某些方向上震荡,收敛速度极慢。标准做法是把每个特征减去均值、除以标准差,让数据分布在一个相近的尺度上。

至于先划分数据集还是先标准化,这里有一个很容易被忽略的坑。正确顺序是:先把数据拆成训练集和测试集,然后用训练集的均值和标准差去标准化训练集和测试集。绝对不要在完整数据集上先算均值方差再做标准化,因为这样测试集的信息会偷偷混进训练阶段,评估结果会虚高,这种现象叫数据泄露。实验数据的随机性本就是这个实验要模拟的真实情况,一旦泄露,你后面调参得到的所有结论都不可信。

我见过不止一个同学在报告里写“我的模型在测试集上准确率 98%”,结果代码被细看后发现,他是把数据整体标准化之后才划分的测试集。做实验不是只看数字,而是要确保数字背后的逻辑站得住脚。

划分数据时还有一个小建议:二分类数据最好用分层采样,保证训练集和测试集里的正负样本比例大致一致,否则随机切分完全可能把某个类别的样本都切到测试集里去。

3.2 模型定义与前向传播

logistic 回归的前向传播,说白了就是三行核心公式:

# 输入 X: (n_samples, n_features) # 权重 W: (n_features, 1) # 偏置 b: 标量 # 1. 线性加权 z = X @ W + b # 2. 通过 sigmoid 得到概率 p = sigmoid(z)

这里X @ W是矩阵乘法的标准写法,如果你用的是带自动求导的深度学习框架,这行代码基本不用改,只是封装成网络层的写法会更简洁。但我始终建议至少手写一遍不借助自动求导的实现,因为你只有亲手写出梯度,才能真正理解反向传播的细节。

参数初始化方面,logistic 回归里把 W 设成全 0 理论上是可行的,因为模型只有一个神经元,不像多层网络那样存在对称性问题。但为了和后续深度学习实验保持习惯一致,我还是建议用小的随机数初始化,幅度控制在 0.01 左右。这样写的好处是,你以后改造成多层网络时不需要重新适应初始化规则。

构建完模型后,先用一个样本做前向传播,手动算一遍概率和损失,确认维度都正确之后再开始训练。这个“先手工算一遍”的习惯能帮你省下大量调试时间。

3.3 训练循环的搭建与监控

训练过程是一个不断重复的循环,核心动作可以归纳为三件事:做一次前向传播、算一次损失、更新一次参数。用伪代码写出来非常清晰:

epochs = 500 learning_rate = 0.1 for epoch in range(epochs): # 前向 p = sigmoid(X_train @ W + b) # 损失,加一个小 epsilon 防止 log(0) loss = -np.mean(y * np.log(p + 1e-8) + (1 - y) * np.log(1 - p + 1e-8)) # 梯度(推导结果就是 X^T @ (p-y) 除以样本数) grad_w = X_train.T @ (p - y) / n_samples grad_b = np.mean(p - y) # 参数更新 W -= learning_rate * grad_w b -= learning_rate * grad_b # 每 20 轮打印一次状态 if epoch % 20 == 0: acc = ((p > 0.5).reshape(-1) == y).mean() print(f"epoch {epoch}, loss {loss:.4f}, acc {acc:.4f}")

关于这个梯度公式 grad_w = X_train.T @ (p - y) / n_samples,我第一次推出来的时候觉得挺惊喜的:logistic 回归的梯度形式竟然和线性回归的梯度形式长得很像,都是“(预测值 - 真实值) 再乘上特征”。这背后的逻辑是,交叉熵和 sigmoid 这一对组合,恰好让非线性激活函数带来的影响在梯度里被抵消掉了,只剩下一项线性的误差信号。

训练循环里有一个地方值得单独强调:你应该同时记录训练集和验证集的损失与准确率。很多人只盯着训练集看,看到一个漂亮的收敛曲线就以为大功告成,其实训练集曲线再漂亮也不能说明泛化能力,只有验证集曲线才能看出模型是否过拟合。

还有 batch size 的概念。上面这个循环是每次把所有数据都拿进来更新一次梯度,这叫全量梯度下降,数据量小的时候没问题。如果数据量有几万条,每次全量计算会很慢,可以每次随机抽一小批数据(比如 32 或 64 条)来更新参数,这就是 mini-batch 梯度下降。实验阶段用全量就够了,但如果你将来做大模型训练,会发现 mini-batch 几乎是标配。

4. 训练调参与效果分析

4.1 学习率怎么调:别在玄学里浪费半天

学习率大概是初学者在实验中花费时间最多、也最容易让人抓狂的一个超参数。我自己常用的策略是先把学习率固定在一个基准值上跑一遍,观察损失曲线的形态,再决定调大还是调小。

学习率典型现象处理建议
1.0 及以上损失剧烈震荡,甚至出现 NaN立即调小 10 倍以上
0.1正常收敛,曲线顺滑可以作为默认起点
0.01稳定下降,但速度偏慢增加迭代轮数即可
0.001下降非常缓慢,效率低可能需要上万轮,不建议

看曲线时抓住一个核心信号:损失曲线如果一路顺滑下降且最终平稳,说明学习率基本合理;如果先快速下降然后开始锯齿状抖动,往往是学习率偏大;如果从头到尾都平坦得几乎不动,可能就是学习率太小,或者特征没做标准化导致梯度太弱。

迭代轮数也不是越大越好。模型在训练集上损失会一直下降,但验证集的损失通常会先降后升。所以更严谨的做法是:训练过程中每轮都记录验证集损失,当验证集损失开始上升时,说明继续训练已经没有意义了,完全可以早停。这个技巧叫 early stopping,在后面的深度学习课程中同样非常实用。

4.2 过拟合与欠拟合在曲线上的表现

很多人觉得 logistic 回归是线性模型,应该不会过拟合,这个想法不完全对。当特征数量远多于样本数量时,logistic 回归照样会过拟合。

欠拟合的特征很典型:训练集损失很高,训练集准确率也很低,而且无论如何延长训练时间都上不去。这种情况多半是数据线性不可分,或者特征确实和标签关系不大。解决方案要么是换更复杂的模型,要么是构造更有效的特征,单纯调参数是救不回来的。

过拟合的特征正好相反:训练集损失一路下降,训练准确率高达 95% 以上,但验证集准确率停在 75% 左右上不去,验证集损失不降反升。这时可以在损失函数中加入正则项,比如在交叉熵基础上加 λ/2·||W||²,对应的梯度更新代码只需要多减一项 λ·W。这一步加进去,你会观察到验证集表现有明显改善。这也是后续深度学习实验里 weight decay 概念的起源。

4.3 评估指标:别让准确率骗了你

实验报告里如果只写一个准确率,那你大概率会被追问一个问题:如果测试集里 90% 是负样本,10% 是正样本,一个无脑把所有样本都预测为负样本的模型,准确率是多少?答案是 90%。所以准确率在类别不平衡的数据集上是非常具有欺骗性的。

更完整的评估体系是看混淆矩阵,以及由它引出的几个指标:

  • 精确率 Precision = TP / (TP + FP),就是预测为正样本的那些里有多少是真的正样本。垃圾邮件过滤场景更看重这个,误杀一封正常邮件比漏掉一封垃圾邮件严重得多。
  • 召回率 Recall = TP / (TP + FN),就是真实正样本里有多少被找出来了。疾病筛查场景更看重这个,宁可多查几次也不能漏诊。
  • F1 分数是两者的调和平均,适合需要同时兼顾精确率和召回率的场景。

还有一个在实验报告中很加分的指标是 AUC,也就是 ROC 曲线下的面积。它衡量的是模型“随机抽一对正负样本,把正样本排在前面的概率”,所以它不受分类阈值的影响,能更全面地反映模型的排序能力。AUC 在 0.5 附近说明模型跟瞎猜差不多,越接近 1 说明区分能力越强。

我建议你在实验里把准确率、精确率、召回率、F1、AUC 都算一遍,然后写进报告,顺便对比一下它们在当前数据集下的表现。这样一份实验报告交上去,老师一眼就能看出你是真把二分类模型玩明白了,而不是只会调一个包然后打印 accuracy。

5. 常见问题与避坑实录

5.1 损失不下降、甚至变成 NaN

这是每次实验都有一半人会碰到的问题,排查顺序很重要,不要病急乱投医。

第一件事看学习率。如果 loss 从第一轮开始就是 NaN,那大概率是学习率设太大,参数更新一步就飞出去了。先把学习率降到 0.001 试试,如果恢复正常,再逐步往上加。

第二件事看特征尺度。没有标准化的数据,特征范围如果达到几千,z 的值也会变得很大,sigmoid 直接饱和,梯度小到几乎为 0,损失变化慢得像蜗牛。这时你看到的就是 loss 在下降趋势上“一动不动”,而不是上下震荡。

第三件事检查数据本身。用代码检查一下 X 和 y 里有没有 NaN,这个操作很多人会跳过,但真实数据里确实容易出现空值。

第四件事确认形状。如果 p 和 y 的 shape 不对,loss 和梯度都可能算出一个奇怪的值。打印 p.shape 和 y.shape,一眼就能发现。

这里也顺便给自己留个好习惯:在计算 log(p) 和 log(1-p) 时加一个极小值兜底,比如 log(p + 1e-8),防止 p 被算出恰好为 0 导致的无穷大问题。

5.2 训练集损失在降,验证集准确率却纹丝不动

这个现象背后的原因通常有三个。

  1. 数据划分有问题:没有分层抽样,导致验证集里某一类样本特别多,模型学到的模式在验证集上根本不适用。
  2. 标准化信息泄露:直接在全体数据上做了标准化再划分数据集,这种操作会让模型在验证集上的表现虚高,然后在换一份真实数据测试时瞬间打回原形。
  3. 样本量太少:验证集只有几十条样本时,评估结果方差会很大,今天 80%,明天 60%,根本没有参考价值。

最稳妥的方案是先划分训练集和测试集,再只对训练集做标准化。划分时使用分层采样,保证类别比例一致。如果数据量太少,可以考虑用交叉验证来替代单次划分。

5.3 准确率很高,但 F1 很低怎么办

准确率 85%、F1 却只有 30%?这种情况在类别不平衡的数据集里非常典型。你可以想象一下一个数据集有 900 个负样本和 100 个正样本,模型把所有样本都预测为负类,准确率是 90%,但召回率是 0%,F1 自然低得可怜。

想让模型真正学会抓少数类,有两条路:

  1. 调整分类阈值。不要把阈值死定为 0.5,可以把阈值调到 0.3 或更低,让模型更容易判定为正类。你可以在验证集上画一条 precision-recall 随阈值变化的曲线,看阈值放在哪个位置能让 F1 最高。
  2. 给少数类更大的权重。在损失函数中给正样本乘以一个大于 1 的权重,相当于人为放大正样本的犯错成本,让模型更愿意去拟合少数类。

这两条路在 logistic 回归里都可以轻松实现,建议你都试一遍,然后在报告里写清楚你对阈值的理解,这会比单纯说“我调了学习率”更有说服力。

5.4 一个救命调试技巧:先过拟合一个小批量

这是我自己用得非常多的调试套路。在训练之前,先只取 8 个样本作为训练集,跑一个小规模训练。如果代码逻辑没问题,模型的损失应该会在几十个 epoch 内快速降到接近于 0,训练准确率能到 100%。如果小批量数据都无法过拟合,那说明你的前向传播、损失函数或者梯度更新里面一定有 bug,先把这个 bug 找出来再说。

这么做最大的意义在于,它把“模型学习的逻辑”和“数据规模带来的问题”分开排查了。等小批量能过拟合,再切回全量数据,这时你面对的就只剩下学习率、正则化这些正经调参问题,而不是跟一个写错符号的梯度公式搏斗两个小时。

写到最后的一些实在话

这个实验我前前后后重写过很多遍,每写一遍都有新的理解。第一遍用深度学习框架跑通的时候,我只知道调包,很多细节是懵的;第二遍强迫自己手写前向和梯度更新,才算真正搞懂 sigmoid 和交叉熵为什么是一对好搭档;第三遍加上正则化和评估指标,才发现自己以前对实验报告的理解太浅了。我建议你也找时间做一次“手写版”的 logistic 回归,哪怕只是抽出半天时间,它的价值远比你跑来跑去调学习率大得多。

最后再分享一个实战小技巧:很多同学在这个实验里会把大量时间耗在调学习率上,但实际操作中最有效的路径是先固定一个学习率、把整个流程跑通,确保代码无误,再回头调参。先保证流程完整,再追求性能优化——这个顺序在后面的所有深度学习项目里都不会变。

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

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

立即咨询