逻辑回归(Logistic Regression)名字里有“回归”,但它最常用于分类任务,尤其是二分类问题(逻辑回归说白了就是在线性回归的基础上使用Sigmoid 函数将原先结果转为概率的形式,然后根据设定的阈值来判断是否是正类)。

它要回答的问题不是“这个样本的数值是多少”,而是“这个样本属于某一类的概率有多大”。例如:

  • 一封邮件是垃圾邮件的概率是多少?
  • 一个用户会不会点击广告?
  • 一个肿瘤样本是恶性的概率是多少?
  • 一个学生是否能通过考试?

逻辑回归的核心思想可以概括为一句话:先用一条直线算出一个分数,再用 Sigmoid 函数把这个分数压缩成 $0$ 到 $1$ 之间的概率。

线性分数经过 Sigmoid 函数变成概率

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. 梯度下降如何学习参数

逻辑回归也可以用梯度下降来训练。

每轮迭代大致做三件事:

  1. 用当前的 $\mathbf{w}$ 和 $b$ 计算每个样本的预测概率。
  2. 根据交叉熵损失计算模型错得有多严重。
  3. 沿着损失下降最快的方向更新参数。

参数更新公式可以写成:

$$
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. 实践建议

使用逻辑回归时,可以优先注意下面几点:

  1. 对连续特征做标准化。
  2. 用验证集或交叉验证选择正则化强度。
  3. 如果类别不平衡,不要只看准确率,可以关注精确率、召回率、F1、AUC。
  4. 如果数据明显非线性,可以加入多项式特征,或者换用树模型、SVM、神经网络等方法。
  5. 业务上需要概率解释时,逻辑回归往往比只输出类别的模型更方便。

11. 总结

逻辑回归虽然名字叫“回归”,但它解决的是分类问题。

它的核心链路是:

$$
\mathbf{x}
\rightarrow
z=\mathbf{w}^{T}\mathbf{x}+b
\rightarrow
\hat{y}=\sigma(z)
\rightarrow
\text{类别}
$$

从图像上看,逻辑回归用一条直线或一个超平面切分空间;从概率上看,它用 Sigmoid 函数把线性分数转成属于正类的概率;从优化上看,它用交叉熵损失惩罚错误预测,并通过梯度下降学习参数。

因此,逻辑回归非常适合作为理解分类算法的第一站:它足够简单,但又包含了概率、损失函数、决策边界、梯度下降这些机器学习中非常核心的概念。