逻辑回归:从直线到概率的分类算法
逻辑回归(Logistic Regression)名字里有“回归”,但它最常用于分类任务,尤其是二分类问题(逻辑回归说白了就是在线性回归的基础上使用Sigmoid 函数将原先结果转为概率的形式,然后根据设定的阈值来判断是否是正类)。
它要回答的问题不是“这个样本的数值是多少”,而是“这个样本属于某一类的概率有多大”。例如:
- 一封邮件是垃圾邮件的概率是多少?
- 一个用户会不会点击广告?
- 一个肿瘤样本是恶性的概率是多少?
- 一个学生是否能通过考试?
逻辑回归的核心思想可以概括为一句话:先用一条直线算出一个分数,再用 Sigmoid 函数把这个分数压缩成 $0$ 到 $1$ 之间的概率。
1. 从线性回归说起
在线性回归中,我们用一个线性函数预测连续值:
$$
z = wx + b
$$
如果是多个特征(w可以看作权重),可以写成:
$$
z = \mathbf{w}^{T}\mathbf{x} + b
$$
这里的 $z$ 可以理解为模型给样本打出的“原始分数”。
但是分类问题不能直接使用这个分数。比如判断一封邮件是不是垃圾邮件时,我们希望模型输出的是:
$$
P(y=1|\mathbf{x})
$$
也就是“在输入特征为 $\mathbf{x}$ 的条件下,样本属于正类的概率”。
概率必须满足两个条件:
- 不能小于 $0$
- 不能大于 $1$
而线性函数 $z=\mathbf{w}^{T}\mathbf{x}+b$ 的取值范围是 $(-\infty,+\infty)$。所以逻辑回归需要一个函数,把任意实数压缩到 $0$ 和 $1$ 之间。
这个函数就是 Sigmoid 函数。
2. Sigmoid 函数:把分数变成概率
Sigmoid 函数定义为(这里的z是线性回归算出的结果):
$$
\sigma(z)=\frac{1}{1+e^{-z}}
$$
它有几个重要特点:
- 当 $z$ 很大时,$\sigma(z)$ 接近 $1$
- 当 $z$ 很小时,$\sigma(z)$ 接近 $0$
- 当 $z=0$ 时,$\sigma(z)=0.5$
所以逻辑回归的完整模型可以写成:
$$
\hat{y}=P(y=1|\mathbf{x})=\sigma(\mathbf{w}^{T}\mathbf{x}+b)
$$
其中 $\hat{y}$ 表示模型预测为正类的概率。
直观地看:
- $z$ 越大,模型越相信样本属于正类。
- $z$ 越小,模型越相信样本属于负类。
- $z=0$ 时,模型刚好一半一半,不偏向任何一类。
3. 分类规则:概率超过阈值就判为正类
逻辑回归输出的是概率,不是直接输出类别。因此还需要设定一个阈值。
最常见的阈值是 $0.5$:
$$
\hat{y}_{class} =
\begin{cases}
1, & \hat{y} \ge 0.5 \
0, & \hat{y} < 0.5
\end{cases}
$$
由于 $\sigma(0)=0.5$,所以:
$$
\hat{y} \ge 0.5 \Longleftrightarrow \mathbf{w}^{T}\mathbf{x}+b \ge 0
$$
这说明逻辑回归的决策边界其实是一条线,或者在高维空间里是一个超平面:
$$
\mathbf{w}^{T}\mathbf{x}+b=0
$$
图中蓝色点和红色点代表两类样本,中间的黑色直线就是决策边界。边界的一侧被预测为正类,另一侧被预测为负类。
4. 一个具体例子:学习时长与是否通过考试
假设我们只用“学习时长”这一个特征,预测学生是否通过考试。
| 学习时长/小时 | 是否通过 |
|---|---|
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
| 4 | 1 |
| 5 | 1 |
| 6 | 1 |
我们希望模型学到一个函数:
$$
P(\text{通过}|x)=\sigma(wx+b)
$$
假设模型学到:
$$
w=1.4,\quad b=-5.6
$$
那么当 $x=4$ 时:
$$
z=1.4 \times 4 - 5.6 = 0
$$
于是:
$$
P(\text{通过}|x=4)=\sigma(0)=0.5
$$
这意味着学习 $4$ 小时是模型眼中的临界点。
当 $x=5$ 时:
$$
z=1.4 \times 5 - 5.6 = 1.4
$$
$$
P(\text{通过}|x=5)=\sigma(1.4)\approx0.80
$$
模型会认为通过概率较高。
当 $x=2$ 时:
$$
z=1.4 \times 2 - 5.6 = -2.8
$$
$$
P(\text{通过}|x=2)=\sigma(-2.8)\approx0.06
$$
模型会认为通过概率很低。
这就是逻辑回归最直观的含义:输入特征先转成分数,再转成概率,最后根据阈值做分类。
5. 为什么不用均方误差
在线性回归中,我们常用均方误差:
$$
J=\frac{1}{m}\sum_{i=1}^{m}(\hat{y}^{(i)}-y^{(i)})^2
$$
但逻辑回归通常不用它,而是使用交叉熵损失(Cross Entropy Loss)。
原因是:逻辑回归输出的是概率。对于概率预测,我们更关心“模型对真实类别有多自信”。
如果真实标签 $y=1$,模型预测 $\hat{y}=0.99$,说明模型非常正确;如果预测 $\hat{y}=0.01$,说明模型非常自信地错了,应该受到很重的惩罚。
交叉熵正好有这个特点。
6. 交叉熵损失
对单个样本,逻辑回归的损失函数为:
$$
L(\hat{y},y)=-\left[y\log(\hat{y})+(1-y)\log(1-\hat{y})\right]
$$
分两种情况看就很清楚。
当 $y=1$ 时:
$$
L=-\log(\hat{y})
$$
如果模型把正类预测成 $0.99$,损失很小;如果预测成 $0.01$,损失会非常大。
当 $y=0$ 时:
$$
L=-\log(1-\hat{y})
$$
如果模型把负类预测成 $0.01$,损失很小;如果预测成 $0.99$,损失会非常大。
对全部训练样本,代价函数为:
$$
J(\mathbf{w},b)=
-\frac{1}{m}\sum_{i=1}^{m}
\left[
y^{(i)}\log(\hat{y}^{(i)})
+(1-y^{(i)})\log(1-\hat{y}^{(i)})
\right]
$$
训练逻辑回归,就是找到合适的 $\mathbf{w}$ 和 $b$,让这个代价函数尽可能小。
7. 梯度下降如何学习参数
逻辑回归也可以用梯度下降来训练。
每轮迭代大致做三件事:
- 用当前的 $\mathbf{w}$ 和 $b$ 计算每个样本的预测概率。
- 根据交叉熵损失计算模型错得有多严重。
- 沿着损失下降最快的方向更新参数。
参数更新公式可以写成:
$$
w_j := w_j - \alpha \frac{\partial J}{\partial w_j}
$$
$$
b := b - \alpha \frac{\partial J}{\partial b}
$$
其中 $\alpha$ 是学习率,控制每次更新走多远。
逻辑回归的直觉就是:如果正类样本被预测得太低,就把决策边界往能提高它概率的方向推;如果负类样本被预测得太高,就把边界往能降低它概率的方向推。
8. Python 实战
下面先根据数学公式手写一个逻辑回归,再用 scikit-learn 训练一个二维逻辑回归模型,并画出决策边界。
安装依赖:
pip install scikit-learn numpy matplotlib
8.1 从公式手写逻辑回归
逻辑回归的核心计算链路是:
$$
z = \mathbf{w}^{T}\mathbf{x}+b
$$
$$
\hat{y}=\sigma(z)=\frac{1}{1+e^{-z}}
$$
$$
J(\mathbf{w},b)=-\frac{1}{m}\sum_{i=1}^{m}\left[y^{(i)}\log(\hat{y}^{(i)})+(1-y^{(i)})\log(1-\hat{y}^{(i)})\right]
$$
对应代码如下:
import numpy as np
class SimpleLogisticRegression:
def __init__(self, learning_rate=0.1, epochs=2000):
# 对应模块:学习率,控制每次参数更新走多大一步
self.learning_rate = learning_rate
self.epochs = epochs
def _sigmoid(self, z):
# 对应模块:Sigmoid 函数,把任意分数压缩成 0 到 1 之间的概率
return 1 / (1 + np.exp(-z))
def fit(self, X, y):
X = np.asarray(X, dtype=float)
y = np.asarray(y, dtype=float)
m, n = X.shape
# 对应模块:初始化权重 w 和偏置 b
self.w = np.zeros(n)
self.b = 0.0
for epoch in range(self.epochs):
# 对应模块:线性打分,分数 = 特征和权重的加权和 + 偏置
z = np.dot(X, self.w) + self.b
# 对应模块:把线性分数转成正类概率
y_pred = self._sigmoid(z)
# 防止 log(0),避免数值溢出
eps = 1e-12
y_pred_safe = np.clip(y_pred, eps, 1 - eps)
# 对应模块:交叉熵损失,用来衡量概率预测和真实标签的差距
loss = -np.mean(
y * np.log(y_pred_safe)
+ (1 - y) * np.log(1 - y_pred_safe)
)
# 对应模块:权重 w 的梯度,表示每个特征权重应该如何调整
dw = (1 / m) * np.dot(X.T, (y_pred - y))
# 对应模块:偏置 b 的梯度,表示整体分类边界应该如何平移
db = (1 / m) * np.sum(y_pred - y)
# 对应模块:梯度下降更新权重,新 w = 旧 w - 学习率 * w 的梯度
self.w = self.w - self.learning_rate * dw
# 对应模块:梯度下降更新偏置,新 b = 旧 b - 学习率 * b 的梯度
self.b = self.b - self.learning_rate * db
if epoch % 400 == 0:
print(f"epoch={epoch}, loss={loss:.4f}")
return self
def predict_proba(self, X):
X = np.asarray(X, dtype=float)
# 对应模块:输出属于正类的概率
return self._sigmoid(np.dot(X, self.w) + self.b)
def predict(self, X, threshold=0.5):
# 对应模块:阈值分类,概率大于等于 0.5 判为正类,否则判为负类
return (self.predict_proba(X) >= threshold).astype(int)
# 简单二维数据:每行有两个特征,标签 0/1 表示两个类别
X_train = np.array([
[0.2, 1.3],
[1.0, 1.8],
[1.3, 0.6],
[2.8, 3.0],
[3.2, 2.2],
[3.8, 3.5],
])
y_train = np.array([0, 0, 0, 1, 1, 1])
model = SimpleLogisticRegression(learning_rate=0.5, epochs=2000)
model.fit(X_train, y_train)
test = np.array([[2.0, 2.1]])
print("属于正类的概率 =", model.predict_proba(test)[0])
print("预测类别 =", model.predict(test)[0])
这段手写实现对应了逻辑回归的完整数学链路:先用线性函数得到 $z$,再用 Sigmoid 得到概率,然后用交叉熵衡量错误,最后通过梯度下降更新参数。
8.2 使用 sklearn 训练并绘制决策边界
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# 构造二维二分类数据,方便画出决策边界
X, y = make_classification(
n_samples=120,
n_features=2,
n_redundant=0,
n_informative=2,
n_clusters_per_class=1,
class_sep=1.4,
random_state=42,
)
# 对应模块:特征标准化,让不同特征处在相近尺度上
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
model = LogisticRegression()
# 对应模块:训练逻辑回归模型,内部会学习权重 w 和偏置 b
model.fit(X_scaled, y)
# 生成二维网格,用来观察每个位置的预测概率
x_min, x_max = X_scaled[:, 0].min() - 0.5, X_scaled[:, 0].max() + 0.5
y_min, y_max = X_scaled[:, 1].min() - 0.5, X_scaled[:, 1].max() + 0.5
xx, yy = np.meshgrid(
np.linspace(x_min, x_max, 200),
np.linspace(y_min, y_max, 200),
)
grid = np.c_[xx.ravel(), yy.ravel()]
# 对应模块:计算网格中每个位置属于正类的概率
proba = model.predict_proba(grid)[:, 1].reshape(xx.shape)
plt.figure(figsize=(7, 5))
# 背景颜色表示不同区域的正类概率
plt.contourf(xx, yy, proba, levels=20, cmap="RdBu", alpha=0.35)
# 对应模块:分类边界,也就是正类概率等于 0.5 的位置
plt.contour(xx, yy, proba, levels=[0.5], colors="black", linewidths=2)
# 绘制训练样本点
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap="RdBu", edgecolors="k")
plt.title("Logistic Regression Decision Boundary")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()
这段代码中的黑色线就是 $P(y=1|\mathbf{x})=0.5$ 的位置,也就是逻辑回归的分类边界。
9. 逻辑回归的优缺点
优点
- 简单、稳定、可解释性强。
- 输出的是概率,而不仅仅是类别。
- 训练速度快,适合做二分类基线模型。
- 对线性可分或近似线性可分的数据效果很好。
缺点
- 决策边界是线性的,难以直接处理复杂非线性数据。
- 对特征缩放比较敏感,实践中通常需要标准化。
- 如果特征之间关系很复杂,单纯逻辑回归可能欠拟合。
- 对异常值和强相关特征也需要适当处理。
10. 实践建议
使用逻辑回归时,可以优先注意下面几点:
- 对连续特征做标准化。
- 用验证集或交叉验证选择正则化强度。
- 如果类别不平衡,不要只看准确率,可以关注精确率、召回率、F1、AUC。
- 如果数据明显非线性,可以加入多项式特征,或者换用树模型、SVM、神经网络等方法。
- 业务上需要概率解释时,逻辑回归往往比只输出类别的模型更方便。
11. 总结
逻辑回归虽然名字叫“回归”,但它解决的是分类问题。
它的核心链路是:
$$
\mathbf{x}
\rightarrow
z=\mathbf{w}^{T}\mathbf{x}+b
\rightarrow
\hat{y}=\sigma(z)
\rightarrow
\text{类别}
$$
从图像上看,逻辑回归用一条直线或一个超平面切分空间;从概率上看,它用 Sigmoid 函数把线性分数转成属于正类的概率;从优化上看,它用交叉熵损失惩罚错误预测,并通过梯度下降学习参数。
因此,逻辑回归非常适合作为理解分类算法的第一站:它足够简单,但又包含了概率、损失函数、决策边界、梯度下降这些机器学习中非常核心的概念。
