Logistic Regression —— 逻辑回归与Softmax多分类
前言
在上一篇文章中,我们深入探讨了线性回归及其正则化变体。线性回归解决的是回归问题——预测一个连续数值。但现实世界中大量的机器学习问题其实是分类问题:这封邮件是垃圾邮件还是正常邮件?这个肿瘤是良性还是恶性?这张图片里是猫、狗还是鸟?
逻辑回归(Logistic Regression) 就是解决二分类问题最经典的算法。尽管名字里带着“回归”,它本质上是一个分类模型。而当我们需要处理三个或更多类别时,逻辑回归的自然推广就是 Softmax回归(也称为多项逻辑回归)。
这篇文章,我们会从 Sigmoid 函数出发,一步步推导逻辑回归的损失函数(最大似然估计),理解“对数几率”的含义和决策边界,然后扩展到 Softmax 多分类,最后讨论多分类模型的评价指标。
一、从线性回归到逻辑回归
1.1 为什么线性回归不能做分类?
线性回归的输出是一个连续值,取值范围是 (−∞,+∞)。而二分类问题的输出是离散的类别标签,比如 y∈{0,1}。
如果我们直接用线性回归 z=wTx 去拟合 0/1 标签,会遇到两个问题:
- 预测值可能远超出 [0, 1] 区间,无法解释为概率
- 模型对异常值极其敏感
因此,我们需要一个映射函数,将线性回归的输出 z∈(−∞,+∞) 压缩到 [0,1] 区间,使其可以解释为概率。
1.2 Sigmoid 函数
最常用的映射函数就是 Sigmoid 函数(也叫 Logistic 函数):
σ(z)=1+e−z1
它的图像是一个优美的 S 形曲线,具有以下重要性质:
- 值域为 (0, 1):可以将任意实数映射为概率
- 单调递增:z 越大,输出越接近 1
- 中心对称:σ(0)=0.5,关于点 (0, 0.5) 对称
- 处处可导、光滑连续
- 导数形式极其简洁:
σ′(z)=σ(z)⋅(1−σ(z))
这个导数特性非常关键——它意味着我们可以根据函数值直接计算导数值,而不需要额外的计算,在梯度下降中效率极高。
1import numpy as np2import matplotlib.pyplot as plt3
4def sigmoid(z):5 return 1 / (1 + np.exp(-z))6
7def sigmoid_derivative(z):8 s = sigmoid(z)9 return s * (1 - s)10
11z = np.linspace(-10, 10, 100)12plt.plot(z, sigmoid(z), label='σ(z)')13plt.plot(z, sigmoid_derivative(z), label="σ'(z)", linestyle='--')14plt.axhline(0.5, color='gray', linestyle=':')15plt.axvline(0, color='gray', linestyle=':')16plt.xlabel('z')17plt.ylabel('value')18plt.legend()19plt.title('Sigmoid 函数及其导数')20plt.show()1.3 逻辑回归的模型形式
逻辑回归的模型假设是:
hθ(x)=σ(θTx)=1+e−θTx1
其中 hθ(x) 表示在给定参数 θ 和输入 x 的条件下,样本属于正类(y=1) 的概率:
P(y=1∣x;θ)=hθ(x)
P(y=0∣x;θ)=1−hθ(x)
将两个式子统一写成:
P(y∣x;θ)=hθ(x)y⋅(1−hθ(x))1−y,y∈{0,1}
二、对数几率(Log Odds)与决策边界
2.1 什么是对数几率?
几率(Odds) 是指事件发生的概率与不发生的概率之比:
Odds=P(y=0∣x)P(y=1∣x)=1−hθ(x)hθ(x)
对几率取自然对数,得到对数几率(Log Odds) ,也叫 Logit:
Logit=log(1−hθ(x)hθ(x))
将 hθ(x)=σ(θTx) 代入:
log(1−hθ(x)hθ(x))=log(1+e−θTxe−θTx1+e−θTx1)=log(eθTx)=θTx
这就是逻辑回归被称为“对数几率回归”的原因:模型实际上是用线性回归 θTx 去逼近真实标签的对数几率。
2.2 决策边界
从上面的推导可以看出:
hθ(x)>0.5⟺θTx>0
hθ(x)<0.5⟺θTx<0
因此,决策边界就是 θTx=0 这条线(或超平面)。
关键洞察:逻辑回归的决策边界本质上是线性的。如果数据本身不是线性可分的,我们需要通过特征工程(如添加多项式特征、交互特征)来构造非线性决策边界。
1from sklearn.datasets import make_classification2from sklearn.linear_model import LogisticRegression3
4# 生成二分类数据5X, y = make_classification(n_samples=200, n_features=2, n_redundant=0,6 n_clusters_per_class=1, random_state=42)7
8# 训练逻辑回归9model = LogisticRegression()10model.fit(X, y)11
12# 决策边界: θ0 + θ1*x1 + θ2*x2 = 0 => x2 = -(θ0 + θ1*x1) / θ213coef = model.coef_[0]14intercept = model.intercept_[0]15# 绘制决策边界...三、最大似然估计:推导逻辑回归的损失函数
3.1 为什么要用最大似然估计?
在线性回归中,我们通过最小化均方误差(MSE) 来求解参数。但在逻辑回归中,MSE 不再是凸函数,用梯度下降容易陷入局部最优。
因此,逻辑回归采用最大似然估计(Maximum Likelihood Estimation, MLE) 来估计参数。
3.2 似然函数
给定训练集 {(x(i),y(i))}i=1n,其中 y(i)∈{0,1}。
根据模型假设,每个样本的概率为:
P(y(i)∣x(i);θ)=hθ(x(i))y(i)⋅(1−hθ(x(i)))1−y(i)
假设样本之间独立同分布,则似然函数为所有样本概率的乘积:
L(θ)=∏i=1nhθ(x(i))y(i)⋅(1−hθ(x(i)))1−y(i)
3.3 对数似然与损失函数
直接最大化乘积不方便(容易数值下溢),取对数将乘积变为求和:
ℓ(θ)=logL(θ)=∑i=1n[y(i)loghθ(x(i))+(1−y(i))log(1−hθ(x(i)))]
机器学习中习惯最小化损失函数,所以在对数似然前加负号,得到交叉熵损失(Cross-Entropy Loss) :
J(θ)=−n1i=1∑n[y(i)loghθ(x(i))+(1−y(i))log(1−hθ(x(i)))]
3.4 梯度下降求解
对 J(θ) 求偏导:
∂θj∂J(θ)=n1∑i=1n(hθ(x(i))−y(i))⋅xj(i)
这个形式与线性回归的梯度形式惊人地相似!区别仅在于线性回归的预测值是 θTx,而逻辑回归的预测值是 hθ(x)=σ(θTx)。
梯度下降更新公式:
θj:=θj−α⋅n1∑i=1n(hθ(x(i))−y(i))⋅xj(i)
1class LogisticRegressionGD:2 """使用梯度下降的逻辑回归"""3
4 def __init__(self, learning_rate=0.01, n_iterations=1000):5 self.lr = learning_rate6 self.n_iterations = n_iterations7 self.theta = None8
9 def sigmoid(self, z):10 return 1 / (1 + np.exp(-z))11
12 def fit(self, X, y):13 n_samples, n_features = X.shape14 X_b = np.c_[np.ones((n_samples, 1)), X] # 添加截距项15 self.theta = np.zeros(n_features + 1)16
17 for _ in range(self.n_iterations):18 z = X_b @ self.theta19 h = self.sigmoid(z)20 gradient = (1 / n_samples) * (X_b.T @ (h - y))21 self.theta -= self.lr * gradient22
23 self.intercept_ = self.theta[0]24 self.coef_ = self.theta[1:]25 return self26
27 def predict_proba(self, X):28 X_b = np.c_[np.ones((X.shape[0], 1)), X]29 return self.sigmoid(X_b @ self.theta)30
31 def predict(self, X, threshold=0.5):32 return (self.predict_proba(X) >= threshold).astype(int)四、Softmax 回归:从二分类到多分类
4.1 为什么要用 Softmax?
当类别数 K>2 时,二分类逻辑回归无法直接使用。有两种常见的处理策略:
- One-vs-Rest(OvR) :为每个类别训练一个二分类器,将该类 vs 其他所有类
- Softmax 回归(多项逻辑回归):直接建模多分类问题
Softmax 回归是逻辑回归在多分类问题上的自然推广。它假设类别之间互斥(每个样本只属于一个类别)。
4.2 Softmax 函数的定义
给定 K 个类别的得分(logits)z1,z2,...,zK,Softmax 函数将其转换为概率分布:
Softmax(z)k=∑j=1Kezjezk,k=1,2,...,K
Softmax 的输出满足概率分布的两个性质:
- 每个输出在 (0, 1) 之间
- 所有输出之和为 1
在 Softmax 回归中,每个类别 k 有自己的权重向量 θk,得分 zk=θkTx:
P(y=k∣x;Θ)=∑j=1KeθjTxeθkTx
4.3 最大似然推导
对于多分类问题,标签 y∈{1,2,...,K}。用独热编码(One-Hot Encoding) 表示:如果样本属于类别 k,则 yk=1,其余为 0。
似然函数为:
L(Θ)=∏i=1n∏k=1KP(y=k∣x(i);Θ)I(y(i)=k)
取负对数,得到多分类交叉熵损失:
J(Θ)=−n1i=1∑nk=1∑KI(y(i)=k)⋅log(∑j=1KeθjTx(i)eθkTx(i))
对 θk 求梯度:
∂θk∂J(Θ)=n1∑i=1n(P(y=k∣x(i))−I(y(i)=k))⋅x(i)
这个形式与二分类逻辑回归的梯度完全一致——只是从 2 类扩展到了 K 类。
1class SoftmaxRegression:2 """使用梯度下降的Softmax回归"""3
4 def __init__(self, learning_rate=0.01, n_iterations=1000):5 self.lr = learning_rate6 self.n_iterations = n_iterations7 self.theta = None8
9 def softmax(self, Z):10 # Z: (n_samples, K)11 exp_Z = np.exp(Z - np.max(Z, axis=1, keepdims=True)) # 数值稳定12 return exp_Z / np.sum(exp_Z, axis=1, keepdims=True)13
14 def fit(self, X, y):15 n_samples, n_features = X.shape16 self.classes = np.unique(y)17 self.K = len(self.classes)18
19 # 将标签转为独热编码20 y_onehot = np.eye(self.K)[y]21
22 X_b = np.c_[np.ones((n_samples, 1)), X]23 self.theta = np.zeros((n_features + 1, self.K))24
25 for _ in range(self.n_iterations):26 scores = X_b @ self.theta # (n_samples, K)27 probs = self.softmax(scores)28 gradient = (1 / n_samples) * (X_b.T @ (probs - y_onehot))29 self.theta -= self.lr * gradient30
31 self.intercept_ = self.theta[0]32 self.coef_ = self.theta[1:]33 return self34
35 def predict_proba(self, X):36 X_b = np.c_[np.ones((X.shape[0], 1)), X]37 return self.softmax(X_b @ self.theta)38
39 def predict(self, X):40 return self.classes[np.argmax(self.predict_proba(X), axis=1)]五、Softmax 的数值稳定性:Log-Sum-Exp Trick
5.1 数值不稳定的原因
直接计算 Softmax 时,需要计算 ezk。当 zk 很大时(比如 zk=1000),e1000 会溢出(overflow) ,Python 会返回 inf。当所有 zk 都是很大的负数时,所有 ezk 都趋近于 0,导致下溢(underflow) 。
1# 不安全的实现 —— 会溢出!2def bad_softmax(scores):3 exp_scores = np.exp(scores) # 如果 scores 包含 1000,这里就爆了4 return exp_scores / exp_scores.sum()5.2 Log-Sum-Exp Trick
解决方案是利用一个恒等变换:
log∑k=1Kezk=m+log∑k=1Kezk−m
其中 m=maxkzk。这个等式对任意 m 都成立。
取 m 为所有得分中的最大值后,zk−m≤0,所以 ezk−m∈(0,1],绝对不会溢出。
对于 Log-Softmax(即 log(Softmax),在计算交叉熵时常用):
log(Softmax(z)k)=zk−log∑j=1Kezj=zk−m−log∑j=1Kezj−m
1def stable_softmax(scores):2 """数值稳定的 Softmax 实现"""3 m = np.max(scores, axis=-1, keepdims=True)4 safe_scores = scores - m5 exp_scores = np.exp(safe_scores)6 return exp_scores / np.sum(exp_scores, axis=-1, keepdims=True)7
8def stable_log_softmax(scores):9 """数值稳定的 Log-Softmax 实现"""10 m = np.max(scores, axis=-1, keepdims=True)11 safe_scores = scores - m12 log_sum_exp = m + np.log(np.sum(np.exp(safe_scores), axis=-1, keepdims=True))13 return safe_scores - log_sum_exp14
15# 测试16scores = np.array([1000, 1001, 1002])17print("Stable softmax:", stable_softmax(scores))18print("Stable log_softmax:", stable_log_softmax(scores))19# 输出: 概率分布合理,不会溢出5.3 实际应用中的最佳实践
在实际的深度学习框架(PyTorch、TensorFlow)中,永远不要单独计算 Softmax 再取 Log,而是直接使用 log_softmax 或 CrossEntropyLoss。
CrossEntropyLoss内部已经集成了LogSoftmax+NLLLoss,数值稳定且高效- 自己实现时,务必使用 Log-Sum-Exp Trick
六、多分类评价指标:宏平均与微平均 F1-Score
6.1 从二分类到多分类的混淆矩阵
在二分类中,我们有 TP、FP、TN、FN。在多分类中,每个类别都有自己的 TP、FP、FN:
- TPk:真实类别为 k,预测也为 k 的样本数(混淆矩阵主对角线)
- FPk:真实类别不是 k,但预测为 k 的样本数
- FNk:真实类别为 k,但预测不是 k 的样本数
每个类别的 Precision 和 Recall 为:
Precisionk=TPk+FPkTPk,Recallk=TPk+FNkTPk
每个类别的 F1-Score 为:
F1k=2⋅Precisionk+RecallkPrecisionk⋅Recallk
6.2 宏平均(Macro-Average)
宏平均对每个类别平等对待,先计算每个类别的指标,再取算术平均:
F1macro=K1∑k=1KF1k
特点:
- 每个类别权重相同,不受类别不平衡影响
- 少数类的表现会被同等重视
- 适合每个类别都很重要的场景
6.3 微平均(Micro-Average)
微平均先汇总所有类别的 TP、FP、FN,再统一计算:
Precisionmicro=∑kTPk+∑kFPk∑kTPk
Recallmicro=∑kTPk+∑kFNk∑kTPk
F1micro=2⋅Precisionmicro+RecallmicroPrecisionmicro⋅Recallmicro
注意:在多分类单标签问题中,微平均 F1 等于准确率(Accuracy) 。
特点:
- 每个样本被平等对待,大类别主导指标
- 对类别不平衡不敏感(实际上是被大类主导了)
- 适合整体准确性比每个类别的公平性更重要的场景
6.4 代码实现
1from sklearn.metrics import confusion_matrix, f1_score2import numpy as np3
4def macro_f1(y_true, y_pred, num_classes):5 """手动计算宏平均 F1"""6 cm = confusion_matrix(y_true, y_pred, labels=range(num_classes))7 f1s = []8 for k in range(num_classes):9 TP = cm[k, k]10 FP = cm[:, k].sum() - TP11 FN = cm[k, :].sum() - TP12 precision = TP / (TP + FP) if (TP + FP) > 0 else 013 recall = TP / (TP + FN) if (TP + FN) > 0 else 014 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 015 f1s.append(f1)16 return np.mean(f1s)17
18def micro_f1(y_true, y_pred, num_classes):19 """手动计算微平均 F1"""20 cm = confusion_matrix(y_true, y_pred, labels=range(num_classes))21 total_TP = np.trace(cm)22 total_FP = cm.sum() - total_TP # 在多分类中,总FP = 总FN23 total_FN = cm.sum() - total_TP24 precision = total_TP / (total_TP + total_FP) if (total_TP + total_FP) > 0 else 025 recall = total_TP / (total_TP + total_FN) if (total_TP + total_FN) > 0 else 026 return 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 027
28# 示例29y_true = np.array([0, 0, 1, 1, 2, 2, 2])30y_pred = np.array([0, 1, 1, 2, 2, 0, 1])31
32print(f"Macro F1 (手动): {macro_f1(y_true, y_pred, 3):.4f}")33print(f"Micro F1 (手动): {micro_f1(y_true, y_pred, 3):.4f}")34print(f"Macro F1 (sklearn): {f1_score(y_true, y_pred, average='macro'):.4f}")35print(f"Micro F1 (sklearn): {f1_score(y_true, y_pred, average='micro'):.4f}")6.5 什么时候用哪个?
| 场景 | 推荐指标 | 原因 |
|---|---|---|
| 类别均衡 | Macro 或 Micro 差别不大 | 两者结果相近 |
| 类别严重不平衡,关心少数类 | Macro | 平等对待每个类别 |
| 类别严重不平衡,关心整体表现 | Micro | 受多数类主导,反映整体准确性 |
| 不知道选哪个 | 两个都报告 | 全面展示模型表现 |
七、总结
| 概念 | 二分类(逻辑回归) | 多分类(Softmax 回归) |
|---|---|---|
| 激活函数 | Sigmoid: σ(z)=1/(1+e−z) | Softmax: pk=ezk/∑ezj |
| 输出 | 1 个概率(正类) | K 个概率(和为 1) |
| 损失函数 | 二分类交叉熵 | 多分类交叉熵 |
| 参数数量 | p+1 | K×(p+1) |
| 梯度形式 | (h−y)x | (pk−Iy=k)x |
核心要点回顾
-
Sigmoid 函数将线性输出映射到 (0, 1) 区间,其导数 σ′(z)=σ(z)(1−σ(z)) 形式简洁,是梯度下降高效计算的关键。
-
对数几率(Log Odds) 揭示了逻辑回归的本质:用线性模型 θTx 去逼近真实标签的对数几率。决策边界 θTx=0 是线性的。
-
最大似然估计推导出交叉熵损失函数,梯度形式与线性回归惊人地相似,区别仅在于预测函数从 θTx 变成了 σ(θTx)。
-
Softmax 回归是逻辑回归在多分类(类别互斥)场景下的自然推广,使用 Log-Sum-Exp Trick 保证数值稳定性——永远不要单独计算 Softmax 再取 Log。
-
宏平均(Macro) 平等对待每个类别,适合不平衡数据;微平均(Micro) 平等对待每个样本,适合关注整体准确性的场景。
Some information may be outdated