☰
逻辑回归与分类模型评估:从Sigmoid到ROC曲线实战指南
2026/10/19 7:51:21 网站建设 项目流程

我最早做用户续费预测的时候,踩过一个很典型的坑:直接用线性回归去拟合二分类标签,结果模型输出一堆小数和负数。负数代表什么?没法解释。后来才反应过来,分类问题的输出应该是一个有边界的概率,而线性回归算出来的是一个无界的数值打分。逻辑回归就是在这个节点上进入我视野的:它的输出经过了压缩,永远落在0到1之间,可以直接当作概率来用。

这篇是机器学习入门系列的第十一篇,聚焦两件事:逻辑回归为什么能承担分类任务,以及分类模型到底应该如何评估。我会从函数原理、损失函数、sklearn实操,一直讲到准确率陷阱、混淆矩阵、ROC曲线和不平衡数据集的处理。内容偏实践,同时也把“为什么这么做”讲清楚,适合正在学经典监督学习、准备把模型从训练阶段推向真实业务判断的同学参考。

1. 逻辑回归与线性回归的分水岭:为什么分类要用Sigmoid

1.1 分类问题的输出到底是什么

分类和回归,本质区别在预测目标上。回归要预测的是一个连续数值,比如房价、温度、销量;分类要预测的是一个离散类别,比如“会不会流失”“是不是垃圾邮件”“肿瘤是良性还是恶性”。

很多人第一反应是:那我把类别转成0和1,再用线性回归去拟合,不就行了?我当年就是这么干的。问题在于,线性回归的输出范围是整个实数轴,它可以输出0.3,也可以输出-2.1甚至5.8。可对于二分类来说,任何小于0或大于1的输出都没有实际含义。你可以强行“截断”成0或1,但截断位置怎么定就变得很随意,模型本身的拟合目标也和“概率预测”南辕北辙。

更实际的问题是:线性回归对离群值极端敏感。分类标签只有0和1,如果你某个样本的拟合值是3.5,那这个样本单独贡献的误差平方就高达12.25,会把整条拟合曲线拉偏。分类问题需要的是一个天然带边界、输出可解释为概率的模型,而不是一个无界的数值回归器。

1.2 Sigmoid的来历:从“打分”到“概率”

逻辑回归解决这个问题的办法,是在线性组合的外面套一层Sigmoid函数。先让模型算出一个“原始分”z:

z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b

然后通过Sigmoid函数把z压缩到(0, 1)区间:

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

这个函数有三个直观特点。第一,它是单调递增的:z越大,p越大。第二,z = 0的时候p = 0.5,正好可以作为二分类的默认分界线。第三,它把“靠近0”和“靠近1”的区域压得很平,把中间区域拉得很陡,这样模型既不会轻易输出极端概率,又能对中等置信度的样本保持区分度。

Sigmoid不是拍脑袋选的。把p看成概率,那么1 - p就是反概率,两者之比p/(1-p)叫“几率”。对它取自然对数,会得到一个神奇的结果:

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

这个变换叫logit变换。也就是说,逻辑回归本质上是在对“对数几率”做线性回归,这也是“逻辑回归”这个名字的来源。很多初学者以为“逻辑回归”就是“有逻辑的回归”,其实是“logistic”的音译,更准确的叫法是“对数几率回归”。

1.3 决策边界与线性分界面

逻辑回归预测的是概率,但实际分类时,我们需要一个判断标准。最常见的规则是:p ≥ 0.5 判为正类,p < 0.5 判为负类。因为p = σ(z),p ≥ 0.5等价于z ≥ 0,所以决策边界就是:

wᵀx + b = 0

这是一个线性超平面。也就是说,逻辑回归本质上是一个线性分类器,它的决策边界永远是一条直线、一个平面,或者高维空间里的一个超平面。如果数据本身就是线性可分或者近似线性可分的,逻辑回归会表现得很稳健;如果数据分布是那种弯弯绕绕的形态,比如一个圆形区域内部是一类、外部是另一类,那逻辑回归就无能为力了,得靠核方法或者树模型。

这一点我建议入门阶段就记住:逻辑回归不是万能的,它的强项是“可解释性”和“训练效率”。在广告点击率预估、信用评分、风控建模这些场景里,业务方往往需要知道每个特征对预测结果的影响方向,逻辑回归的系数天然就能提供这种解释,所以即便有更强的模型,逻辑回归依然是大量业务建模的首选基线。

2. 损失函数与梯度下降:交叉熵是更合理的标尺

2.1 为什么最小化MSE在逻辑回归上行不通

模型要训练,就得有损失函数。很多人顺理成章想到均方误差(MSE):预测概率和真实标签差多少,平方一下加起来。逻辑回归如果用MSE,会遇到两个很麻烦的问题。

第一个问题是非凸。线性回归里,MSE对参数w是一个开口向上的二次函数,只有一个最低点,梯度下降怎么走都能走到全局最优。但逻辑回归把线性组合套了一层Sigmoid,MSE曲面会变得坑坑洼洼,存在多个局部极小值。梯度下降很可能困在某个局部低点出不来。

第二个问题是梯度消失。Sigmoid在两端是饱和的,当z很大或很小时,它的导数趋近于0。如果一个样本被严重错判,比如真实标签是1但预测概率接近0,按直觉这时候损失应该很大、梯度也应该很大,好让参数赶紧纠正。但MSE求导之后恰好会乘上一个接近0的导数项,导致梯度反而很小,模型“想改也改不动”。这个现象在深度学习里叫梯度饱和,放在逻辑回归里一样存在。

所以逻辑回归不用MSE,改用交叉熵(也叫对数损失)。

2.2 从极大似然到交叉熵

交叉熵的推导逻辑在统计里非常经典:极大似然估计。

对于单个样本,如果真实标签是1,模型预测概率是p,那么这个样本出现的概率就是p;如果真实标签是0,模型预测概率是1-p。把两种情况写成一个式子,就是:

L = p^y · (1-p)^(1-y)

把所有训练样本的概率乘在一起,就是整个训练集出现的概率。我们希望找到一组参数,让这个整体概率最大——这就是最大似然。

直接连乘有两个问题:样本多的时候数值会小到溢出;乘法也不方便求导。解决办法是取对数,把连乘变成连加:

ln L = Σ [ yᵢ ln(pᵢ) + (1-yᵢ) ln(1-pᵢ) ]

机器学习习惯把“最大似然”转成“最小损失”,所以前面加个负号,再除以样本数N做平均:

J(w) = -1/N · Σ [ yᵢ ln(pᵢ) + (1-yᵢ) ln(1-pᵢ) ]

这就是交叉熵损失。它的好处在于:当预测概率越接近真实标签,损失越小;当预测概率和真实标签完全相反,损失会趋向无穷大,惩罚非常重。这比MSE的“平方惩罚”更符合分类场景的直觉。

2.3 梯度更新为什么简单干净

对交叉熵损失求关于参数wⱼ的偏导,结果会非常简洁:

∂J/∂wⱼ = (1/N) · Σ (pᵢ - yᵢ) · xᵢⱼ

注意,pᵢ是模型的预测概率,yᵢ是真实标签,pᵢ - yᵢ正是“预测和真实的偏差”。这个形式说明了一个关键规律:误差越大,梯度越大,参数更新越猛;预测越准,更新越平缓。没有任何饱和项卡住梯度,训练过程自然也就更稳定。

这个更新公式和线性回归的梯度形式几乎一样,区别只在于“输出”从线性值变成了Sigmoid压缩后的概率。所以逻辑回归在工程上实现起来非常轻量,收敛速度也快,这也是它在工业界长期被使用的原因之一。

3. 用sklearn跑通第一个逻辑回归:完整步骤与三个高频坑

3.1 数据准备:加载、划分、标准化

先看一段完整可跑的代码。我用的是sklearn内置的乳腺癌数据集,二分类任务,特征维度适中,很适合演示逻辑回归的完整流程。

import numpy as np from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report data = load_breast_cancer() X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) model = LogisticRegression(max_iter=1000, random_state=42) model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) print(classification_report(y_test, y_pred, target_names=data.target_names))

这里有一个新手特别容易忽略的细节:标准化必须先在训练集上执行,拿到均值和标准差,然后再用同一套参数去转换测试集。代码里我用的是fit_transform处理训练集,transform处理测试集,两者不能颠倒。不少同学图省事,把整个数据集一起做标准化再划分,这属于数据泄漏,会让测试集的评估结果虚高。

逻辑回归的梯度下降对特征尺度很敏感,尤其在使用L2正则化时,尺度大的特征会被错误地惩罚得更重。不标准化的直接后果是:模型训练不稳定,特征系数的解释也会失真。所以拿到特征后的第一件事,永远先想“需不需要标准化”。

3.2 训练与评估:先看分类报告里发生了什么

跑完上面的代码,classification_report会输出准确率、每个类别的精确率、召回率和F1值。第一次看这个报告的同学可能只盯着最底下那个准确率,我建议反过来,先把每个类别的精确率和召回率看清楚。

为什么?因为准确率在类别不平衡时会骗人。乳腺癌数据集本身正负样本比例还算均衡,看不出大问题,但如果换到欺诈检测、罕见病筛查这类场景,“整体有多准”和“模型在关键类别上有不有用”完全是两码事。这个我后面专门用一章展开。

3.3 坑1:max_iter不够引发收敛警告

如果你把max_iter设成一个很小的值,比如50,sklearn可能会报ConvergenceWarning: Liblinear failed to converge。这个警告的意思是:迭代次数太少,模型还没收敛就停了。

解决办法很简单:调大max_iter,比如设成1000。但更优雅的做法是选对solver。sklearn的LogisticRegression提供了多个优化器:

  • liblinear:适合中小规模数据,二分类效果好,但不支持多分类的multinomial模式。
  • lbfgs:拟牛顿法,收敛快,是目前默认的通用选择,适合大多数场景。
  • sag和saga:适合大规模数据,尤其是样本量很大时,但需要做特征缩放。

我的习惯是:小数据集直接用默认的lbfgs,碰到收敛问题先加max_iter,还不行就换solver。不要盲目堆迭代次数,理解了优化器的区别才能对症下药。

3.4 坑2:C值不是越大越好,要调参

LogisticRegression里的C是正则化强度的倒数,C越小,正则化越强;C越大,模型越倾向于完全拟合训练集。默认C=1.0,但这不等于最适合你的数据。

实操中最好用GridSearchCV在一组候选值里搜索,比如从0.001到100之间按对数尺度取几个值:

from sklearn.model_selection import GridSearchCV param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100]} search = GridSearchCV(LogisticRegression(max_iter=1000), param_grid, cv=5) search.fit(X_train_scaled, y_train) print(search.best_params_) print(search.best_score_)

调C的意义在于平衡偏差和方差。C过大会让模型在训练集上抠得太细,把噪声也学进去;C过小则可能欠拟合,连训练集的主规律都没抓住。搜索完C之后,还要用验证集或交叉验证分数判断,而不是只看训练集分数。

3.5 坑3:多分类场景下OvR与multinomial的区别

逻辑回归天生是二分类模型,但sklearn把它扩展到了多分类。两种常见策略:

  • OvR(一对余):对每个类别都训练一个二分类器,判断“是不是这个类”,最后比较置信度选最高者。
  • multinomial(多项式):直接用多分类的softmax版本,所有类别共享一次优化,理论上更合理,但仅部分solver支持。

在sklearn中,multi_class参数默认是auto,它会根据solver自动选择:liblinear只能用OvR,其他solver用multinomial。实际使用中,如果你的solver选的是liblinear,注意它会把多分类退化成多个二分类器,速度和效果与multinomial会有差异。

我刚开始做多分类的时候,就在这个细节上吃过亏:换solver之后发现参数含义完全变了,查文档才反应过来是multi_class被自动切换了。建议多分类项目里显式写明multi_class='multinomial',不要依赖默认值。

4. 分类评估:不要迷信准确率

4.1 一个让“99%准确率”翻车的场景

假设一个欺诈检测系统,99.5%的交易都是正常的,只有0.5%是欺诈。你写一个极其愚蠢的模型:无论输入什么,都输出“正常”。它的准确率是多少?99.5%。如果只看准确率,这个模型简直完美。但它一个欺诈都没抓到,业务上线第一天就会被骂死。

准确率只有在正负样本比例接近时才有参考价值。样本一旦不平衡,准确率就会被多数类“绑架”。这也是我每篇评估相关文章里都要强调的一句话:分类评估,先看数据分布,再谈指标。

4.2 混淆矩阵:先把四个格子看清楚

混淆矩阵是分类评估的底层工具,精确率、召回率、F1都是从这四个格子里算出来的。

| 预测为正类 | 预测为负类 | | 真实为正类 | TP(真正例) | FN(假负例) | | 真实为负类 | FP(假正例) | TN(真负例) |

四个格子的含义用业务语言翻译一下:

  • TP:实际是正类,模型也预测成正类。比如“确实是欺诈交易,系统拦截了”。
  • FP:实际是负类,模型却预测成正类。比如“正常交易,系统误判为欺诈”,这就是误报。
  • FN:实际是正类,模型却预测成负类。比如“确实是欺诈交易,系统放行了”,这就是漏报。
  • TN:实际是负类,模型也预测成负类。比如“正常交易,放行通过”。

看混淆矩阵,不要只看对角线上的TP和TN有多大,更要看FP和FN这两个“错误”方向分别有多严重。不同业务对两个方向的容忍度完全不一样。

4.3 精确率、召回率与F1的取舍

精确率(Precision)和召回率(Recall)的公式是:

  • 精确率 = TP / (TP + FP):预测为正类的样本里,有多少是真的正类。它衡量的是“判得准不准”。
  • 召回率 = TP / (TP + FN):真实的正类样本里,有多少被模型找出来了。它衡量的是“找得全不全”。

用一个生活化的类比:精确率像是你从瓜棚里挑瓜,挑出来的10个瓜里有多少是熟的;召回率像是这片瓜地里所有熟瓜,你一共摘到了多少。挑得严,可能漏掉不少熟瓜,精确率高但召回率低;挑得松,熟瓜基本都摘了,但生瓜也跟着混进来,召回率高但精确率低。

这两个指标是一对天然矛盾体,没有一个模型能同时做到两项都拉满。业务场景决定了你要牺牲哪一边:

  • 癌症筛查:漏掉一个病人是致命的,所以宁可多复查、多误报,也要保证召回率接近100%。
  • 垃圾邮件过滤:误删一封重要邮件比多收几封垃圾邮件更糟糕,所以精确率更要紧。
  • 推荐系统:推荐列表里全是用户不喜欢的内容会导致流失,精确率优先;但如果追求用户想看的内容全覆盖,召回率优先。

F1分数是精确率和召回率的调和平均:

F1 = 2 · (Precision · Recall) / (Precision + Recall)

调和平均对低值特别敏感,如果精确率和召回率一个高一个特别低,F1也会被拉得很低。它适合在不知道业务偏好时,用一个单一数值快速衡量模型好坏。

4.4 用classification_report快速体检

sklearn的classification_report已经把上述指标都算好了。拿到报告后我建议养成一个习惯:先看少数类的召回率。多数类通常表现好,少数类的召回率才是模型真正的短板。

print(classification_report(y_test, y_pred, target_names=data.target_names))

输出里会对每一行显示precision、recall、f1-score。如果某个类别样本很少,它的指标波动会很大,原因在于分母太小,个别样本的错判就会导致指标剧烈变化。这时候不要急着下“模型不行”的结论,先看看测试集里这个类到底有多少样本。

5. ROC曲线与AUC:评估模型的排序能力

5.1 阈值变化下TPR与FPR的运作

前面说的精确率、召回率、F1,都是基于一个固定阈值(通常是0.5)得出的指标。但逻辑回归真正输出的是概率,阈值本身就是可以移动的。把阈值从0.5降到0.3,会有更多样本被判成正类,召回率上升、精确率下降;反过来,阈值升到0.7,正类判得更严格,精确率上升、召回率下降。

ROC曲线把这种阈值移动的全过程画出来。横轴是假正例率FPR = FP / (FP + TN),纵轴是真正例率TPR = TP / (TP + FN)。注意TPR其实就是召回率。每取一个阈值,都能算出一组(FPR, TPR),把从0到1之间所有阈值对应的点连起来,就是ROC曲线。

曲线越靠近左上角,说明模型在两个方向上表现都好。那条从左下到右上的对角线,对应的是“随机猜测”的效果。模型如果连对角线都不如,说明这个模型的实际预测能力还不如抛硬币。

5.2 AUC到底在说什么

AUC是ROC曲线下方的面积,它的数值有一个非常直觉化的概率解释:随机抽一个正类样本和一个负类样本,模型给正类样本打分高于负类样本的概率。换句话说,AUC衡量的是模型把正类和负类“排对顺序”的能力,与具体阈值无关。

AUC的取值区间是0.5到1之间。0.5就是随机水平;0.7到0.8属于“有参考价值”;0.8到0.9属于“区分度良好”;超过0.9要警惕是不是过拟合或者数据泄漏了。真实业务里,AUC能到0.85以上的二分类模型已经算很不错了。

但AUC有一个特点容易被误解:它只看排序,不在意概率的绝对大小。模型把所有预测概率统一除以2,AUC完全不变,因为大小顺序没变。如果你要的不只是排序,而是精准的概率估计,那还得看校准曲线,只看AUC是不够的。

5.3 绘制ROC曲线的代码与读图要点

用sklearn画ROC很简单,几行代码就能跑通:

from sklearn.metrics import roc_curve, roc_auc_score import matplotlib.pyplot as plt y_proba = model.predict_proba(X_test_scaled)[:, 1] fpr, tpr, thresholds = roc_curve(y_test, y_proba) auc_value = roc_auc_score(y_test, y_proba) plt.plot(fpr, tpr, label=f'AUC = {auc_value:.3f}') plt.plot([0, 1], [0, 1], linestyle='--', color='gray') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.legend() plt.show()

这里有个细节:model.predict_proba返回的是二维数组,第一列是负类概率,第二列是正类概率,画ROC要用正类那一列,也就是[:, 1]。我用model.decision_function也可以画,输出的是Sigmoid之前的原始分数z,ROC曲线形状其实一样,因为Sigmoid是单调的,单调变换不改变排序。初学时我在这里困惑过很久,搞清楚以后,再看任何“概率变分数AUC没变”的说法就都不疑惑了。

读图的时候,重点关注曲线开头那段是否快速上扬。如果曲线前期陡峭,说明模型在阈值较低时就能抓到大量正类样本,同时误报很少,这是理想形态;如果曲线前期平缓、后期才冲高,说明模型需要很高的阈值才能区分正负类,实际应用价值会打折扣。

5.4 什么时候别只看ROC:PR曲线的必要性

ROC曲线在正负样本极度不平衡时,会显得过于乐观。原因在于横轴FPR的分母是FP + TN,TN是负类样本里正确预测的那一部分。如果负类样本数量极大,即使FP被误报得很多,FPR的数值还是会被巨大的TN稀释掉,曲线看起来依然很漂亮。

在欺诈检测这类场景里,负类样本可能占到99%以上,这时我更推荐同时看一眼PR曲线(Precision-Recall曲线)。PR曲线的横轴是召回率,纵轴是精确率,两个指标都直接聚焦正类,不会被海量负类“稀释”。如果正类样本占比非常低,PR曲线能更真实地反映模型对正类的识别能力。

做一个简单判断:正负样本比例失衡超过10倍,或者你只关心少数类,那就别只看ROC,把PR曲线和AUC、PR-AUC(PR曲线下面积)一起列出来。

6. 不平衡数据的评估与调优

6.1 先做数据再调参?顺序错了就泄漏

处理不平衡数据,很多人第一反应是上过采样工具。但有一个顺序问题必须强调:必须先划分训练集和测试集,再在训练集上做重采样,绝不能先对整个数据集做SMOTE再划分。

原因很直接:SMOTE会根据近邻关系生成新样本。如果先对整个数据集合成新样本再划分,生成的“新样本”和信息原本就留在测试集里的样本存在重叠关系,模型相当于提前“见过”了测试集的分布,评估结果必然虚高。正确流程是:先train_test_split,然后只在X_train上做SMOTE,X_test保持原始分布不动。

代码上这也是一个容易疏忽的点,尤其是用pipeline的时候。我自己的习惯是:把重采样放进独立的预处理步骤,而不是和特征标准化混在一起,保证每一步的输入输出都很清晰。

6.2 class_weight与过采样:训练端的两种思路

第一种简单粗暴:给少数类更高的样本权重。sklearn里直接设class_weight='balanced',框架会根据类别频率自动反推权重。少数类的每个样本在损失函数里占据更大份额,模型就会更卖力地学少数类。

model_balanced = LogisticRegression( max_iter=1000, class_weight='balanced', random_state=42 ) model_balanced.fit(X_train_scaled, y_train)

第二种是过采样,最常用的是SMOTE。它不是简单复制少数类样本,而是在少数类样本之间根据近邻插值生成新样本,能一定程度上缓解过拟合。

from imblearn.over_sampling import SMOTE smote = SMOTE(random_state=42) X_train_res, y_train_res = smote.fit_resample(X_train_scaled, y_train)

这两种方式可以搭配,也可以单独用。我的经验是:class_weight='balanced'适合快速试验,效果稳定;SMOTE能增加少数类的多样性,但样本量很大时计算开销不小,而且如果少数类本身分布很散,合成的样本可能落在噪声区域,反而误导模型。

6.3 阈值移动:不改变模型,改变判断标准

训练时用0.5作为默认分界线,这个约定太常被当成理所当然。在不平衡数据里,0.5往往不是最优阈值。模型预测出的概率是0.2,听起来很低,但如果负类占99%,0.2已经意味着正类的可能性比先验概率高了几十倍,这时候判成正类可能才是正确的决策。

阈值移动的思路是:在验证集上扫描多个阈值,选出业务指标最优的那个,拿到预测端使用。举个例子:

from sklearn.metrics import f1_score best_threshold = 0.5 best_f1 = 0 for t in np.arange(0.3, 0.71, 0.05): pred_t = (model.predict_proba(X_test_scaled)[:, 1] >= t).astype(int) score = f1_score(y_test, pred_t) if score > best_f1: best_f1 = score best_threshold = t print(f'best threshold: {best_threshold:.2f}, best F1: {best_f1:.3f}')

还要提醒一句:调阈值是在独立的验证集上完成的,不能直接在测试集上扫。如果阈值是根据测试集的表现反推出来的,测试集就不再是“未知数据”,最终报告的数字会有水分。正确做法是:训练集→调模型参数,验证集→调阈值,测试集→只做最终一次评估。

这些操作本质都是在“数据分布不均衡”的前提下,把模型的预测能力尽可能转化到业务真正关心的方向上。等哪一天数据分布发生漂移,比如正类占比从1%变成10%,你之前调好的阈值、权重、重采样策略都得重新验证一遍。模型上线之后仍然要持续关注这些指标,这是我在实际项目中感触最深的一点。

最后再补充一个小建议:如果你刚开始学分类模型评估,不用急着把所有指标背下来。先拿一个数据集跑通逻辑回归,把confusion_matrix、classification_report、ROC曲线这三样东西打印出来,对着真实案例反复看几次,指标之间的关系自然就记住了。分类评估的底层逻辑并不复杂,复杂的是把“指标”和“业务损失”对应起来;而这一步,恰好是模型能不能在真实环境里站住脚的关键。

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

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

立即咨询