实话说,我第一次学逻辑回归的时候,是很懵的。刚把线性回归的“用一条直线拟合数据”搞明白,紧接着就来了个逻辑回归,名字里带着“回归”,结果干的全是分类的活儿。查资料看到一堆 sigmoid、极大似然、梯度下降的术语,代码倒是几行能跑,但心里一直没底。
后来在项目里反复用、又把损失函数的推导自己推了两遍之后,我才算真正“看透”了逻辑回归。它之所以是入门机器学习绕不开的算法,就是因为它是从线性模型通向分类问题的桥:原理不复杂,可视化起来非常直观,代码写出来也短,但背后那些设计逻辑,足够让初学者理解很多机器学习的核心思想。
这篇文章我想用一组代码示例和可视化图,把逻辑回归拆开讲清楚。适合刚学完线性回归、准备接触分类问题的读者,也适合那些用 sklearn 调包调了很久、但始终没想明白模型背后在干什么的朋友。我会先讲它解决什么问题,再拆 sigmoid 和决策边界,然后带你手写一遍梯度下降,最后用完整代码画出决策边界和损失曲线。全程配代码,你可以直接复制到自己环境里跑。
1. 为什么叫“回归”却在做分类:先搞清楚它解决什么问题
1.1 线性回归在分类任务上的挫败
很多初学者对逻辑回归的第一个困惑是:既然是分类算法,为什么不叫“逻辑分类”?
要回答这个问题,得先看线性回归直接拿来分类会出什么洋相。
假设我们要根据肿瘤大小判断良性还是恶性,把“良性”编码成0、“恶性”编码成1,然后用线性回归去拟合。看起来好像可行:模型输出接近0就是良性,接近1就是恶性。
但问题来了。如果数据里出现一个特别大的肿瘤尺寸,线性回归为了拉低这个点的误差,会把整条直线往下压,导致原本该输出0.8、0.9的样本被压到0.4、0.5,分类就乱了。更麻烦的是,线性回归的输出范围是负无穷到正无穷,你怎么解释“输出 -0.3 是良性还是恶性”?没法解释。
我当初试着把线性回归硬用在二分类上,效果可以说是一塌糊涂。尤其是特征分布不太均匀的时候,那条回归直线为了照顾极端值,把决策边界带得完全偏离直觉。
1.2 “逻辑”到底加在了哪里
逻辑回归的核心改动只有一处:把线性回归的输出套进一个叫做 sigmoid 的函数里,把负无穷到正无穷的实数,压缩到 0 到 1 之间。
这样一来,模型输出的含义就变了——不再是“预测值”,而是“属于正类的概率”。一旦有了概率,我们就能设定阈值(通常是0.5),大于阈值判为正类,小于等于阈值判为负类。
所以“逻辑回归”这个名字里的“逻辑”,指的就是这个 sigmoid 函数。它本质上还是在做回归——先算一个线性得分,只是最后加了一个逻辑变换,把输出变成了概率。这就是为什么它叫“回归”,却常被用来分类。
理解了这一步,逻辑回归的大框架就清楚了:
code复制线性得分 z = w1*x1 + w2*x2 + ... + wn*xn + b
概率输出 p = sigmoid(z) = 1 / (1 + e^(-z))
分类结果 = (p > 0.5) ? 正类 : 负类
整条链路非常干净,没有任何黑魔法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. sigmoid 函数和决策边界:把数学公式变成看得懂的图
2.1 sigmoid 函数:把任意实数压到 0 到 1 之间
sigmoid 的数学形式是:
code复制sigmoid(z) = 1 / (1 + exp(-z))
它的图形长得像一个拉长的 S。z 趋近正无穷时,输出趋近 1;z 趋近负无穷时,输出趋近 0;z = 0 时,输出刚好是 0.5。
第一次看到这个函数时,我心里想的是:这不就是把一个数掰弯了吗?线性输出是一条直线,经过 sigmoid 之后就变成了一条平滑的 S 曲线。
但这条曲线妙就妙在两个地方:
- 处处可导,梯度下降可以顺利使用。
- 在 z 接近0的地方斜率最大,也就是对“模棱两可”的区域最敏感;离0越远,曲线越平缓,输出越接近0或1,确定性越高。
你可以把 sigmoid 想成一个“概率挤压器”:模型先自由地算出一个可能很大的得分,然后 sigmoid 负责把这个得分掰成一个符合概率意义的 0~1 数值。
2.2 决策边界是怎么画出来的
当逻辑回归的输入是两个特征时,我们可以在二维平面上画一条线,这条线就是决策边界。
怎么算?非常简单。决策边界就是模型输出的概率刚好等于 0.5 的地方,而概率等于 0.5 等价于 z = 0。所以决策边界的方程是:
code复制w1*x1 + w2*x2 + b = 0
这是一条直线。
这就是逻辑回归最可爱的地方:它的决策边界天生是线性的。二维是一条直线,三维是一个平面,更高维就是一个超平面。
初学者常常误以为逻辑回归只能做简单的分类,其实不是。它只是“决策边界是线性”的,但如果我们在特征工程里给模型喂入 x1^2、x1*x2 这样的组合特征,它也能画出圆形的决策边界。后面我会在代码示例里展示这个进阶玩法,先记住结论:逻辑回归对“特征组合”是开放的,它的线性限制不等于能力上限。
2.3 阈值为什么默认是 0.5,以及什么时候要改
很多教程会告诉你:p > 0.5 判为正类,否则判为负类。但你想过没有,为什么是 0.5?
因为 0.5 表示“两种类别概率相同”,是天然的平分点。当正负样本数量相当时,0.5 是合理的默认选择。
但实际项目很少这么理想。比如欺诈检测场景里,欺诈样本可能只占 1%,模型就算把所有样本都判为正常,准确率也有 99%。这时候如果还死死抱着 0.5 阈值,你会得到一个“看起来准确率很高、实际上完全没用”的模型。
正确的做法是把阈值当作一个可调的旋钮。你想更保守、宁可错杀也不放过欺诈,就把阈值调低到 0.3;你想减少误报,就调高到 0.7。这就是为什么逻辑回归输出的“概率”比“分类结果”更有价值——阈值操作空间全在那个概率值上。
我一般在项目里会画出不同阈值下的精确率和召回率曲线(PR 曲线),然后根据业务场景选阈值。逻辑回归好在它的概率输出比较“诚实”,不会像某些复杂模型那样给出虚高的概率。
3. 损失函数与梯度下降:模型是怎么一步步学出参数的
3.1 为什么不能照搬线性回归的均方误差
线性回归用的损失函数是均方误差(MSE):
code复制loss = (1/n) * sum((y_true - y_pred)^2)
这个函数在数学上叫凸函数,梯度下降能找到全局最优解,用起来非常舒服。
但如果你把同样的损失函数搬到逻辑回归上,麻烦就来了。因为逻辑回归的输出经过了 sigmoid,sigmoid 是一条 S 形曲线,它把均方误差函数变形成了一个波浪状的非凸函数。这意味着梯度下降很可能掉进局部最小值,模型怎么也训练不好。
更重要的是,均方误差对概率类的输出不友好。我们想要的是“模型对错误预测的惩罚要足够重”,而均方误差对所有误差的惩罚是均匀的,导致训练过程特别“钝”。
3.2 对数损失:惩罚错误预测的直觉理解
逻辑回归用的是对数损失,也叫交叉熵损失。二分类情况下,它的公式长这样:
code复制loss = -(1/n) * sum( y_true * log(p) + (1 - y_true) * log(1 - p) )
乍一看有点唬人,但拆开理解特别简单:
- 当真实标签 y = 1 时,损失函数只剩第一项 -log(p)。如果模型预测 p = 1,损失是 0;如果模型预测 p = 0.1,损失是 -log(0.1) ≈ 2.3,很大。
- 当真实标签 y = 0 时,损失函数只剩第二项 -log(1 - p)。模型预测 p = 0,损失是 0;预测 p = 0.9,损失同样巨大。
所以对数损失的核心逻辑就是一句话:模型越自信地给出错误答案,惩罚越重。这种惩罚是指数级的,不是线性的,它逼着模型在不确定的时候不要乱下结论。
我用一个生活化的例子给你讲。想象你在考驾照,科目二压线。教练不会只罚你一秒钟,而是压线越多扣分越狠,甚至直接不及格。对数损失就是这个教练,它让模型每次犯错都“痛感”更强。
3.3 梯度下降:沿着山坡往下走
有了损失函数,接下来的问题就是:怎么找到一组参数让损失最小?答案是梯度下降。
“梯度”在数学里表示函数上升最快的方向,那么梯度的反方向就是下降最快的方向。所以参数更新规则是:
code复制w = w - learning_rate * gradient
学习率(learning_rate)控制每一步走多大。走大了容易一步跨过山谷,在小坑里震荡;走小了训练速度慢得像蜗牛。我在调参时通常从 0.1 开始试,如果损失下降太慢就调大,如果损失在震荡就调小。
逻辑回归的参数更新其实没有想象中复杂。对对数损失求导之后,梯度会落成一个非常漂亮的表达式:
code复制gradient = (1/n) * sum((p - y) * x)
也就是说,每个参数的梯度等于“预测概率与真实标签的差”乘以对应特征值,再求平均。预测越离谱,梯度越大,参数更新幅度就越大。
这个形式真的是逻辑回归里最优美的部分。它把损失函数、sigmoid 和梯度下降串成了一条线,理解了这个,你基本上就掌握了逻辑回归的核心机制。
4. 带代码示例的实战:生成数据、训练模型、把结果画出来
4.1 构造一份二维演示数据,先画散点图
代码示例我打算用两个特征做一次完整的二分类实验,因为二维数据画出来最容易看明白。我用 make_classification 生成 300 个样本,两个特征、两个类别,然后直接可视化。
python复制import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix
# 生成二分类数据集
X, y = make_classification(
n_samples=300,
n_features=2,
n_informative=2,
n_redundant=0,
n_clusters_per_class=1,
random_state=42
)
# 训练集/测试集划分
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 画数据散点图
plt.figure(figsize=(6, 6))
plt.scatter(X_train[y_train == 0][:, 0], X_train[y_train == 0][:, 1],
c='skyblue', edgecolors='k', label='类别 0')
plt.scatter(X_train[y_train == 1][:, 0], X_train[y_train == 1][:, 1],
c='salmon', edgecolors='k', label='类别 1')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.title('训练数据分布')
plt.legend()
plt.show()
这张散点图是后续所有可视化的起点。你能直观地看到两个类别存在一定重叠,不存在一条直线能把两类完美切开——这是真实数据的常态。逻辑回归要做的就是找一条“尽可能好”的直线,而不是“完美”的直线。
4.2 手写逻辑回归参数更新,画出损失曲线
为了让你真正理解逻辑回归内部发生了什么,我决定先用 NumPy 手写一遍参数训练过程,不做任何封装。这样梯度下降的每一步我们都能看见,损失曲线也能画出来。
python复制def sigmoid(z):
return 1 / (1 + np.exp(-z))
def compute_loss(X, y, w):
n = len(y)
p = sigmoid(X @ w)
# 加一个极小值防止 log(0)
loss = -np.mean(y * np.log(p + 1e-15) + (1 - y) * np.log(1 - p + 1e-15))
return loss
def gradient_descent(X, y, learning_rate=0.1, epochs=500):
# 给 X 加一列 1,对应偏置项 b
X_b = np.c_[np.ones((X.shape[0], 1)), X]
n, m = X_b.shape
w = np.zeros(m)
loss_history = []
for epoch in range(epochs):
p = sigmoid(X_b @ w)
gradient = (1 / n) * (X_b.T @ (p - y))
w -= learning_rate * gradient
if epoch % 10 == 0:
loss_history.append(compute_loss(X_b, y, w))
return w, loss_history
w_hand, loss_hist = gradient_descent(X_train, y_train, learning_rate=0.1, epochs=500)
# 画损失下降曲线
plt.figure(figsize=(8, 5))
plt.plot(np.arange(len(loss_hist)) * 10, loss_hist, marker='o', color='teal')
plt.xlabel('迭代轮数')
plt.ylabel('损失')
plt.title('逻辑回归训练过程中的损失下降')
plt.grid(alpha=0.3)
plt.show()
print('手写训练得到的参数:', w_hand)
你在自己电脑上跑这段代码时,会看到损失曲线从高处一路滑下来,很快就进入平台期。这就是梯度下降最直观的视觉效果。
我特别提醒一句:这里我选了 learning_rate=0.1 和 500 次迭代,是我试出来的稳妥组合。如果你把学习率调到 1.0,可能会看到损失曲线不降反升;如果调到 0.001,500 次迭代可能还没收敛。这就是调参的乐趣所在。
4.3 用 sklearn 训练并绘制决策边界
手写版本理解了原理,接下来用 sklearn 的 LogisticRegression 走一遍标准流程。注意我特意加了 StandardScaler,这一步后面会细说为什么重要。
python复制scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = LogisticRegression()
model.fit(X_train_scaled, y_train)
# 在测试集上预测
y_pred = model.predict(X_test_scaled)
print('准确率:', accuracy_score(y_test, y_pred))
# 画出决策边界
def plot_decision_boundary(model, scaler, X, y):
# 生成网格
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
np.arange(y_min, y_max, 0.02))
grid_points = np.c_[xx.ravel(), yy.ravel()]
grid_points_scaled = scaler.transform(grid_points)
Z = model.predict(grid_points_scaled)
Z = Z.reshape(xx.shape)
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, Z, alpha=0.3, cmap='bwr')
plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1],
c='blue', edgecolors='k', label='类别 0')
plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1],
c='red', edgecolors='k', label='类别 1')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.title('逻辑回归决策边界可视化')
plt.legend()
plt.show()
plot_decision_boundary(model, scaler, X_train, y_train)
渲染出来的图里,背景被分成蓝红两个区域,中间那条分界线就是逻辑回归学出的决策边界。你会发现它是一条直线,但角度和位置并不是“肉眼看起来最优”的那条,而是“让对数损失最小”的那条。
这里有个容易忽略的点:LogisticRegression 默认带 L2 正则化,所以它的决策边界会稍微平滑一些,不会完全贴合训练噪声点。正则化强度由参数 C 控制,默认是 1.0。你把它调成 100,决策边界会更贴合训练数据;调成 0.01,边界会更简单。
4.4 从混淆矩阵看模型真正好不好
准确率一个指标不足以评判模型,尤其是类别不平衡时。我每次训练完分类模型,都会打印混淆矩阵看看模型具体错在哪里。
python复制cm = confusion_matrix(y_test, y_pred)
print('混淆矩阵:')
print(cm)
tn, fp, fn, tp = cm.ravel()
print(f'真正例 (TP): {tp}')
print(f'真负例 (TN): {tn}')
print(f'假正例 (FP): {fp}')
print(f'假负例 (FN): {fn}')
混淆矩阵四个格子包含了所有信息:
- 真正例:实际是正类,模型也预测为正类。
- 真负例:实际是负类,模型也预测为负类。
- 假正例:实际是负类,模型预测成了正类(误报)。
- 假负例:实际是正类,模型预测成了负类(漏报)。
不同业务对误报和漏报的容忍度完全不一样。垃圾邮件过滤更讨厌漏报(垃圾邮件混进收件箱),而医疗筛查更讨厌漏报(有病没查出来)。逻辑回归的阈值就是调节这两种错误比重的杠杆。
5. 初学者最容易踩的坑和我的实操建议
5.1 特征缩放真的很重要,重要到会改变结果
我刚学逻辑回归时,总觉得特征缩放是“锦上添花”,不做也能跑。直到一次在项目里用真实数据做预测,特征一个取值范围是 0~1,另一个是 10000~20000,结果模型训练出来的系数一个大到离谱、一个小到几乎为零,损失函数下降慢得像乌龟爬。
原因在于梯度更新的幅度跟特征尺度直接挂钩。特征数值大,对应梯度的绝对值就大,参数更新一步就可能飞出去;特征数值小,参数更新又慢吞吞。多个特征尺度差异巨大时,梯度下降会在某些方向上震荡、在另一些方向上停滞。
解决方案就是我前面代码里已经用到的 StandardScaler。它把每个特征变成均值 0、方差 1 的标准分布,让梯度下降的步伐在各个方向上大致均匀。
初学者最容易犯的错是:在划分训练集、测试集之后忘记让 scaler 只 fit 训练集。正确做法是:
python复制scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 只 fit 训练集
X_test_scaled = scaler.transform(X_test) # 测试集只用 transform
这样做的目的是防止测试集信息泄漏到训练过程里。如果你先对整个数据集做标准化再划分,模型在训练时已经“偷看”了测试集的均值方差,得出的评估结果会偏乐观。这个坑我在早期调参时踩过不止一次。
5.2 类别不平衡时,别用 0.5 当分类阈值
前面提到阈值默认 0.5,但类别不平衡时必须重新考虑。
举例来说,假设 95% 的样本是负类,5% 是正类。模型只要永远输出 0.5 以下,准确率就有 95%。但这不是我们要的模型。
逻辑回归模型本身不会自动处理类别不平衡,它只会努力让整体损失最小。正类样本太少时,模型很容易学成“全部预测为负类”。这是因为对数损失里负类样本贡献了大部分梯度。
我常用的几种处理方式:
- 调整类别权重:sklearn 的
LogisticRegression(class_weight='balanced')会自动按类别频率反比加权,给少数类样本更高的权重。 - 重采样:对少数类做上采样,或对多数类做下采样。
- 调阈值:训练完之后在验证集上画 PR 曲线或 ROC 曲线,根据业务需求选阈值。
这三种方法不是互斥的,我通常先设 class_weight='balanced' 训练,然后在验证集上选阈值,效果比较稳。但要注意,调整类别权重会改变模型输出的概率含义,这时的概率不能直接解释为“真实概率”,而是“经过先验平衡调整后的倾向分数”。
5.3 正则化参数 C 不是玄学,调它对结果影响很大
sklearn 的 LogisticRegression 默认带 L2 正则化,正则化强度通过 C 控制。C 越小,正则化惩罚越强,模型越简单;C 越大,模型越倾向拟合训练数据。
实际项目中数据往往有很多特征,特征之间还存在相关性。如果不加正则化,逻辑回归的系数会变得非常大,模型会对训练数据过度自信(输出概率接近 0 或 1),泛化能力随之下降。
我见过一个有趣的现象:用 L2 正则化后,模型预测的错误样本从“高达 0.99 的自信错误”变成“0.6~0.7 的犹豫错误”。后者至少能让你知道模型拿不准,可以通过阈值或人工介入处理。
初学阶段,建议把 C 当作超参数来调,不要直接默认 1.0 就结束。可以用网格搜索跑一遍:
python复制from sklearn.model_selection import GridSearchCV
param_grid = {'C': [0.01, 0.1, 1, 10, 100]}
grid = GridSearchCV(LogisticRegression(), param_grid, cv=5, scoring='f1')
grid.fit(X_train_scaled, y_train)
print('最佳参数:', grid.best_params_)
一个小提醒:当特征数量远大于样本数量时,L1 正则化往往比 L2 更合适,因为它会把不重要的特征系数压缩成 0,起到特征选择的作用。sklearn 里设 penalty='l1',但求解器要换成 liblinear 或 saga,默认的 lbfgs 不支持 L1。
5.4 多分类不是魔法,本质是一对多的组合
逻辑回归原生是二分类,但也能处理多分类。sklearn 里你只需要把 y 改成多个类别标签,模型会自动用“一对多”(OvR)或“多项式”(multinomial)两种策略之一去训练。
- OvR:为每个类别训练一个二分类器,区分“这个类别”和“其他所有类别”,预测时选概率最高的分类器。
- multinomial:也叫 softmax 回归,直接在所有类别上计算概率分布,所有类别共享一套参数更新。
初学者经常纠结选哪个。我的经验是:类别数少、数据量一般时,两种方法结果差别不大;类别数多且类别间有相似性时,multinomial 通常更稳。sklearn 的 LogisticRegression 默认 multi_class='auto',它会根据求解器和数据情况自动选择,所以大多数场景不需要手动指定。
需要留意的是多分类下的评估方式。二分类的准确率、精确率、召回率可以直接算,但多分类要考虑是“宏平均”还是“微平均”。宏平均是对每个类别分别计算指标再取均值,微平均是先把所有类别的混淆矩阵格子加起来再算。如果类别不平衡,宏平均更能反映少数类表现。
最后再分享一点个人体会
逻辑回归是一个特别适合“亲手拆开来看”的算法。它的数学门槛不高,但包含了损失函数设计、梯度下降、正则化、阈值选择这些贯穿机器学习的核心思路。你用 numpy 手写一遍训练循环,再用 sklearn 对照一遍,最后画几张可视化图,理解深度会远超直接调包。
我在实际项目中仍然经常用逻辑回归,不是因为它花哨,而是因为它可控、可解释、训练速度快,还容易排查问题。很多时候业务方问“为什么这个样本被判为正类”,逻辑回归能直接给出每个特征的贡献度,换成复杂树模型或深度学习模型,解释成本会高很多。
如果你刚接触逻辑回归,建议你照着这篇文章的代码自己跑一遍,改一改学习率、调一调 C 值、换一份数据,观察决策边界和损失曲线怎么变化。很多困惑会在动手之后自然消失。
