假设你要判断一名学生能否通过考试。学习时间越长,通过的可能性通常越大,但现实不是“学 3.9 小时一定挂科、4 小时突然 100% 通过”。我们更希望模型给出“通过概率 82%”这样的平滑判断,再用一个阈值把概率变成类别。Logistic 回归正是为这种任务设计的。

读完你会做到

  • 理解 Logistic 回归为什么是分类算法
  • 看懂 Sigmoid、线性分数、概率和决策边界的关系
  • 从零实现批量与随机梯度训练
  • 正确解读准确率、阈值、损失函数和病马测试结果

一、Logistic 回归到底在做什么

Logistic 回归(Logistic Regression)是一种监督学习算法。它最常用于二分类,也就是答案只有两类的问题,例如:

它不直接从特征跳到类别,而是分成三步:

  1. 把每个特征乘上一个权重,再全部相加,得到线性分数 z
  2. 用 Sigmoid 函数把任意大小的 z 压缩到 0 和 1 之间。
  3. 把结果解释为类别 1 的估计概率,再用阈值完成分类。
Logistic 回归将输入特征加权求和,经过 Sigmoid 得到概率,再以0.5为阈值分类;二维空间中形成直线决策边界
图 1:模型输出的是概率;默认以 0.5 为界时,边界恰好位于线性分数 z = 0 的地方。

二、为什么名字叫“回归”却用来分类

“回归”来自它对特征做的线性组合,形式与线性回归很像:

z = w₀ + w₁x₁ + w₂x₂ + … + wₙxₙ

其中:

线性回归会直接把 z 当作预测值,因此可能输出 −3 或 7.5;概率却必须在 0 到 1 之间。Logistic 回归在后面接上 Sigmoid,把线性分数变成概率,所以最终解决的是分类问题。

三、Sigmoid 函数:概率转换器

Sigmoid 也叫 Logistic 函数,公式是:

σ(z) = 1 ÷ (1 + e⁻ᶻ)

看似复杂,实际只需记住三个性质:

Sigmoid S形曲线,将任意实数映射到0和1之间,0对应0.5
图 2:Sigmoid 是一条平滑的 S 形曲线。它不会真正等于 0 或 1,但在两端会无限接近。

用第五章代码实际观察

import numpy as np


def sigmoid(value):
    safe_value = np.clip(value, -500, 500)
    return 1.0 / (1.0 + np.exp(-safe_value))


test_values = np.array([-10.0, -2.0, 0.0, 2.0, 10.0])
print(sigmoid(test_values))

实际输出:

[0.0000454, 0.1192029, 0.5, 0.8807971, 0.9999546]

np.clip 把极端输入限制在 −500 到 500。数学上,极大的正负数本来就会让 Sigmoid 无限接近 1 或 0;限制范围只是避免计算 exp 时溢出,不会实质改变分类结果。

四、亲手完成一次概率分类

假设模型使用两个特征:

训练得到的权重假设为 w₀ = −6、w₁ = 0.8、w₂ = 0.5。某位学生每天学习 5 小时、出勤分数 8:

z = −6 + 0.8 × 5 + 0.5 × 8 = 2

再送入 Sigmoid:

p = σ(2) ≈ 0.881

模型估计他属于类别 1,也就是“通过”的概率约为 88.1%。若阈值为 0.5,因为 0.881 > 0.5,最终预测为通过。

特征 → 加权求和得到 z → Sigmoid 得到概率 p → 与阈值比较得到类别。

五、决策边界为什么是一条直线

默认阈值为 0.5,而 σ(0) = 0.5,因此分类边界满足:

w₀ + w₁x₁ + w₂x₂ = 0

解出 x₂

x₂ = (−w₀ − w₁x₁) ÷ w₂

这就是二维平面上的直线。线的一边 z > 0,概率大于 0.5;另一边 z < 0,概率小于 0.5。特征更多时,它会变成高维空间里的一个超平面

六、模型怎样学会权重

一开始,代码把权重全部设为 1。这个初始模型大概率预测不准,于是要反复执行:

  1. 01
    前向计算

    用当前权重计算每个样本的分数和预测概率。

  2. 02
    比较答案

    用真实标签减去预测概率,得到误差方向和大小。

  3. 03
    计算梯度

    判断每个权重应当向哪个方向调整,调整多少。

  4. 04
    更新权重

    按学习率迈出一小步,然后重新计算。

第五章使用梯度上升,目标是让训练数据的对数似然越来越大:

weights ← weights + 学习率 × Xᵀ × (真实标签 − 预测概率)

七、对数似然和交叉熵是什么

模型需要一个总分来衡量当前权重好不好。对单个样本而言:

把两种情况写在一起,单样本似然是:

P(y | x) = pʸ × (1 − p)¹⁻ʸ

训练时希望所有样本似然的乘积尽量大。为了避免小概率连乘造成数值问题,也为了把乘法变成加法,通常取对数:

对数似然 = Σ [y log(p) + (1 − y) log(1 − p)]

第五章的“梯度上升”是在最大化对数似然。现代库常说“梯度下降最小化二元交叉熵”,其中:

二元交叉熵 = −对数似然

一个加负号后最大化,另一个最小化,本质是同一件事。不要被“上升”和“下降”两个名字误导。

八、从零实现批量梯度上升

批量方法每一轮都用全部训练样本计算一次更新。第五章已经为 NumPy 2 做了兼容:旧代码里的 np.mat 已移除,改用普通数组和 @ 做矩阵乘法。

import numpy as np


def gradient_ascent(features, labels, learning_rate=0.001, epochs=500):
    X = np.asarray(features, dtype=float)
    y = np.asarray(labels, dtype=float)
    weights = np.ones(X.shape[1])

    for _ in range(epochs):
        probabilities = sigmoid(X @ weights)
        errors = y - probabilities
        gradient = X.T @ errors
        weights = weights + learning_rate * gradient

    return weights

四句核心代码

  1. X @ weights:所有样本分别完成加权求和,得到一列线性分数。
  2. sigmoid(...):把所有分数转换成类别 1 的预测概率。
  3. y - probabilities:真实值减预测值。真实为 1 但概率太低时,误差为正;真实为 0 但概率太高时,误差为负。
  4. X.T @ errors:汇总全部样本对每个权重的修正意见。

九、截距项为什么要在每行前加 1

第五章读取二维数据时,每个样本保存成:

sample = [1.0, feature_1, feature_2]

前面的 1.0 与权重 w₀ 相乘后仍是 w₀,因此可以把截距放进同一次矩阵乘法:

w₀ × 1 + w₁x₁ + w₂x₂

如果没有截距,分类直线会被强制经过原点。加入它相当于给边界增加一个可以平移的旋钮。

十、二维数据实际训练结果

第五章的 testSet.txt 包含 100 个二维样本。使用学习率 0.001、训练 500 轮后,批量梯度上升实际得到:

训练得到的权重:[4.12414349, 0.48007329, -0.61684820]
训练样本数量:100
训练集分类准确率:0.96

对应的分类边界是:

x₂ = (−4.1241 − 0.4801x₁) ÷ (−0.6168)

96% 是训练集准确率,不是测试准确率。它说明模型能用一条直线较好地拟合这 100 个已见样本,但不能单独证明遇到新数据也有 96% 准确率。评价泛化能力必须保留独立测试集或使用交叉验证。

十一、批量与随机梯度有什么区别

批量梯度

每次更新前查看全部样本,方向较稳定,但数据很大时每一步计算较慢。适合小数据和理解矩阵公式。

随机梯度

每看一个样本就立即更新,单步快、能在线学习,但更新轨迹会抖动,结果受样本顺序影响。

基础随机梯度只按固定顺序扫一遍数据:

def stochastic_gradient_once(X, y, learning_rate=0.01):
    weights = np.ones(X.shape[1])

    for index in range(len(X)):
        probability = sigmoid(np.sum(X[index] * weights))
        error = y[index] - probability
        weights += learning_rate * error * X[index]

    return weights

实际运行时,基础方法训练集准确率只有 72%。第五章的改进版本做了三件事:

def improved_stochastic_gradient(X, y, epochs=150):
    weights = np.ones(X.shape[1])

    for epoch in range(epochs):
        remaining_indices = list(range(len(X)))

        for step in range(len(X)):
            learning_rate = 4 / (1.0 + epoch + step) + 0.0001
            random_position = int(np.random.uniform(0, len(remaining_indices)))
            sample_index = remaining_indices[random_position]

            probability = sigmoid(np.sum(X[sample_index] * weights))
            error = y[sample_index] - probability
            weights += learning_rate * error * X[sample_index]
            del remaining_indices[random_position]

    return weights

固定随机种子为 0 后,改进版本实际训练准确率为 95%。它略低于批量方法的 96%,并不表示算法一定更差;两种训练策略、轮数和学习率不同,而且这里只比较训练集。

十二、怎样把概率变成类别

def classify(sample, weights, threshold=0.5):
    probability = sigmoid(np.sum(sample * weights))
    return 1 if probability > threshold else 0

0.5 只是常用默认值,不是自然界规定的标准。阈值决定两类错误之间的权衡:

降低阈值

更多样本会被判为类别 1,通常提高召回率,但也会带来更多假阳性。例如疾病筛查时宁愿多做复查,也不想漏掉患者。

提高阈值

只有概率很高才判为类别 1,通常提高精确率,但可能漏掉更多真正的类别 1。例如昂贵干预前需要更高把握。

选择阈值时要看业务代价,并结合混淆矩阵、精确率、召回率、F1、ROC-AUC 或 PR-AUC,而不是永远固定 0.5。

十三、实战:预测病马生存结果

第五章使用两个配套文件:

每个样本读取前 21 列作为特征,第 22 列作为标签。模型使用改进随机梯度上升训练 1000 轮,再逐条预测测试集:

weights = improved_stochastic_gradient(
    np.array(training_features),
    training_labels,
    epochs=1000,
)

errors = 0
for features, true_label in zip(test_features, test_labels):
    prediction = classify(np.array(features), weights)
    if prediction != true_label:
        errors += 1

error_rate = errors / len(test_labels)

固定随机种子后,单次实际结果为:

the error rate of this test is: 0.447761

随机梯度的抽样顺序会影响最终权重,因此第五章又重复测试 10 次。实际错误率依次约为:

44.78%, 37.31%, 25.37%, 37.31%, 26.87%,
41.79%, 28.36%, 34.33%, 29.85%, 31.34%

10 次平均错误率:33.73%

这里最值得学的不是“33.73% 好不好”,而是:

十四、用 scikit-learn 完成正规流程

真实项目建议使用成熟实现,并把标准化与模型放进同一个 Pipeline。标准化能避免数值范围大的特征在优化过程中主导更新,也能让正则化更公平地作用于各个权重。

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler


X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

model = make_pipeline(
    StandardScaler(),
    LogisticRegression(
        C=1.0,
        max_iter=2000,
        random_state=42,
    ),
)

model.fit(X_train, y_train)
predictions = model.predict(X_test)
probabilities = model.predict_proba(X_test)[:, 1]

print(confusion_matrix(y_test, predictions))
print(classification_report(y_test, predictions))
print("ROC-AUC:", roc_auc_score(y_test, probabilities))

predict 给出最终类别,predict_proba 给出概率。评估排序能力时要使用概率,而不是已经被阈值切成 0/1 的结果。

十五、正则化:防止权重过度膨胀

特征很多、数据较少或特征高度相关时,模型可能学出绝对值很大的权重,对训练数据变化非常敏感。正则化会对大权重增加代价:

在 scikit-learn 中,C 是正则化强度的倒数:C 越小,约束越强;C 越大,约束越弱。应使用交叉验证选择,而不是凭感觉把它设得极大。

十六、系数能怎样解释

Logistic 回归的线性分数也叫对数几率(log-odds)

log[p ÷ (1 − p)] = w₀ + w₁x₁ + … + wₙxₙ

在其他特征不变时,xᵢ 增加 1,会让对数几率增加 wᵢ;对应的几率会乘以 eʷⁱ。不过要谨慎:

十七、Logistic 回归的优点与局限

它为什么常被当作基线

  • 训练和预测速度快,适合大规模稀疏数据
  • 直接输出概率,方便调整分类阈值
  • 加入正则化后通常比较稳定
  • 线性关系下系数相对容易解释

什么时候要谨慎

  • 默认只能学习线性决策边界
  • 对异常值和特征尺度较敏感
  • 严重类别不平衡时,准确率和0.5阈值可能误导
  • 强相关特征会让系数解释不稳定

十八、新手最常踩的 8 个坑

  1. 因为名字里有“回归”就把它当连续值预测。基础 Logistic 回归的主要用途是分类和概率估计。
  2. 忘记截距。分类边界被强制经过原点,表达能力无端受限。
  3. 直接计算极端指数。exp 可能溢出,应使用稳定实现或成熟库。
  4. 特征尺度相差巨大却不标准化。优化会变慢,正则化对各特征也不公平。
  5. 只看训练准确率。训练集表现不能代替独立测试或交叉验证。
  6. 永远使用 0.5 阈值。阈值应结合类别比例和错误代价选择。
  7. 把概率当作天然可靠的置信度。概率可能需要用独立数据检查校准情况。
  8. 把系数解释成因果效应。预测关联不等于“这个特征导致了结果”。

十九、完成自己的 Logistic 分类项目

  1. 01
    定义正类

    明确标签 1 代表什么,以及漏判、误判分别有什么代价。

  2. 02
    划分数据

    在任何调参和预处理拟合之前保留测试集,分类时尽量分层抽样。

  3. 03
    处理特征

    处理缺失值、类别编码和异常值,并只在训练数据上拟合标准化器。

  4. 04
    训练基线

    先使用带 L2 正则化的 Logistic 回归,检查是否收敛。

  5. 05
    多指标评价

    同时查看混淆矩阵、精确率、召回率、F1 和概率指标。

  6. 06
    选择阈值

    在验证数据上按实际代价调整阈值,不要用测试集反复试。

  7. 07
    解释与记录

    记录特征单位、系数、版本、随机种子和数据划分方式。

最后,把 Logistic 回归记成五个词

特征加权概率阈值更新

模型把输入特征乘以各自权重并相加,用 Sigmoid 把加权分数变成类别 1 的概率,再与阈值比较得到类别;训练则根据真实答案反复更新权重。

建议你接下来把二维数据的学习率从 0.001 改成 0.0001、0.01、0.1,同时记录对数损失和训练准确率,而不只是看最终权重。你会直观看到:步子太小收敛很慢,步子太大可能震荡;理解这个现象,就理解了梯度方法最核心的训练节奏。