二分类的基石
前言
逻辑回归(Logistic Regression)是最经典的分类算法之一。虽然名字中有”回归”,但它实际上是分类算法,输出的是样本属于某一类别的概率。
从线性回归到分类
问题引入
线性回归输出范围是 $(-\infty, +\infty)$,但分类概率需要在 $[0, 1]$。
解决方案:使用Sigmoid函数将线性输出映射到概率:
\[\sigma(z) = \frac{1}{1 + e^{-z}}\]Sigmoid函数性质
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
z = np.linspace(-10, 10, 100)
plt.figure(figsize=(10, 4))
# Sigmoid函数
plt.subplot(1, 2, 1)
plt.plot(z, sigmoid(z), 'b-', linewidth=2)
plt.axhline(0.5, color='r', linestyle='--', alpha=0.5)
plt.axvline(0, color='r', linestyle='--', alpha=0.5)
plt.xlabel('z')
plt.ylabel('σ(z)')
plt.title('Sigmoid函数')
plt.grid(True, alpha=0.3)
# 导数
plt.subplot(1, 2, 2)
sig = sigmoid(z)
derivative = sig * (1 - sig)
plt.plot(z, derivative, 'g-', linewidth=2)
plt.xlabel('z')
plt.ylabel("σ'(z)")
plt.title('Sigmoid导数: σ(1-σ)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
关键性质:
- 输出范围:$(0, 1)$
- $\sigma(0) = 0.5$
- 导数:$\sigma’(z) = \sigma(z)(1-\sigma(z))$
- 对称性:$\sigma(-z) = 1 - \sigma(z)$
逻辑回归模型
模型定义
给定输入 $\mathbf{x}$,预测属于正类的概率:
\[P(y=1|\mathbf{x}) = \sigma(\mathbf{w}^T\mathbf{x} + b) = \frac{1}{1 + e^{-(\mathbf{w}^T\mathbf{x} + b)}}\]属于负类的概率:
\[P(y=0|\mathbf{x}) = 1 - P(y=1|\mathbf{x})\]决策边界
通常以0.5为阈值:
\[\hat{y} = \begin{cases} 1 & \text{if } P(y=1|\mathbf{x}) \geq 0.5 \\ 0 & \text{otherwise} \end{cases}\]即决策边界为:$\mathbf{w}^T\mathbf{x} + b = 0$(一个超平面)
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
# 生成二分类数据
np.random.seed(42)
X, y = make_classification(n_samples=200, n_features=2, n_informative=2,
n_redundant=0, n_clusters_per_class=1, random_state=42)
# 训练模型
lr = LogisticRegression()
lr.fit(X, y)
# 绘制决策边界
xx, yy = np.meshgrid(np.linspace(X[:, 0].min()-1, X[:, 0].max()+1, 100),
np.linspace(X[:, 1].min()-1, X[:, 1].max()+1, 100))
Z = lr.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
Z = Z.reshape(xx.shape)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.contourf(xx, yy, Z, levels=np.linspace(0, 1, 11), cmap='RdYlBu_r', alpha=0.8)
plt.colorbar(label='P(y=1)')
plt.scatter(X[y==0, 0], X[y==0, 1], c='blue', edgecolors='k', label='Class 0')
plt.scatter(X[y==1, 0], X[y==1, 1], c='red', edgecolors='k', label='Class 1')
plt.contour(xx, yy, Z, levels=[0.5], colors='k', linewidths=2)
plt.title('决策边界与概率等高线')
plt.legend()
plt.subplot(1, 2, 2)
# 3D概率曲面
from mpl_toolkits.mplot3d import Axes3D
ax = plt.subplot(1, 2, 2, projection='3d')
ax.plot_surface(xx, yy, Z, cmap='RdYlBu_r', alpha=0.8)
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.set_zlabel('P(y=1)')
ax.set_title('概率曲面')
plt.tight_layout()
plt.show()
损失函数
为什么不用MSE?
如果使用MSE:$L = (y - \sigma(z))^2$,损失函数是非凸的,存在多个局部最小值。
交叉熵损失
对于单个样本:
\[L(y, \hat{p}) = -[y\log(\hat{p}) + (1-y)\log(1-\hat{p})]\]对于整个数据集:
\[J(\mathbf{w}) = -\frac{1}{N}\sum_{i=1}^{N}[y_i\log(\hat{p}_i) + (1-y_i)\log(1-\hat{p}_i)]\]直观理解:
- 当 $y=1$,$L = -\log(\hat{p})$,正确预测($\hat{p} \to 1$)时损失趋近0
- 当 $y=0$,$L = -\log(1-\hat{p})$,正确预测($\hat{p} \to 0$)时损失趋近0
# 交叉熵损失可视化
p = np.linspace(0.001, 0.999, 100)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(p, -np.log(p), 'b-', linewidth=2)
plt.xlabel('预测概率 p')
plt.ylabel('损失')
plt.title('y=1时的损失: -log(p)')
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.plot(p, -np.log(1-p), 'r-', linewidth=2)
plt.xlabel('预测概率 p')
plt.ylabel('损失')
plt.title('y=0时的损失: -log(1-p)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
梯度推导
损失函数对参数的梯度
\[\frac{\partial J}{\partial w_j} = \frac{1}{N}\sum_{i=1}^{N}(\hat{p}_i - y_i)x_{ij}\] \[\frac{\partial J}{\partial b} = \frac{1}{N}\sum_{i=1}^{N}(\hat{p}_i - y_i)\]向量形式:
\[\nabla_{\mathbf{w}}J = \frac{1}{N}\mathbf{X}^T(\hat{\mathbf{p}} - \mathbf{y})\]形式与线性回归完全一致!这不是巧合,而是广义线性模型的共同性质。
从零实现
class LogisticRegressionScratch:
def __init__(self, learning_rate=0.1, n_iterations=1000, regularization=None, alpha=0.01):
self.lr = learning_rate
self.n_iter = n_iterations
self.reg = regularization # 'l1', 'l2', or None
self.alpha = alpha
self.w = None
self.b = None
self.history = []
def _sigmoid(self, z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
def _loss(self, y, p):
"""计算交叉熵损失"""
eps = 1e-15
p = np.clip(p, eps, 1 - eps)
loss = -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p))
if self.reg == 'l2':
loss += 0.5 * self.alpha * np.sum(self.w ** 2)
elif self.reg == 'l1':
loss += self.alpha * np.sum(np.abs(self.w))
return loss
def fit(self, X, y):
n_samples, n_features = X.shape
# 初始化参数
self.w = np.zeros(n_features)
self.b = 0
self.history = []
for i in range(self.n_iter):
# 前向传播
z = X @ self.w + self.b
p = self._sigmoid(z)
# 记录损失
loss = self._loss(y, p)
self.history.append(loss)
# 计算梯度
error = p - y
dw = (X.T @ error) / n_samples
db = np.mean(error)
# 正则化梯度
if self.reg == 'l2':
dw += self.alpha * self.w
elif self.reg == 'l1':
dw += self.alpha * np.sign(self.w)
# 更新参数
self.w -= self.lr * dw
self.b -= self.lr * db
return self
def predict_proba(self, X):
z = X @ self.w + self.b
return self._sigmoid(z)
def predict(self, X, threshold=0.5):
return (self.predict_proba(X) >= threshold).astype(int)
def score(self, X, y):
return np.mean(self.predict(X) == y)
# 测试
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
lr_scratch = LogisticRegressionScratch(learning_rate=0.5, n_iterations=500)
lr_scratch.fit(X_train, y_train)
print(f"训练准确率: {lr_scratch.score(X_train, y_train):.4f}")
print(f"测试准确率: {lr_scratch.score(X_test, y_test):.4f}")
# 损失曲线
plt.figure(figsize=(8, 4))
plt.plot(lr_scratch.history)
plt.xlabel('迭代次数')
plt.ylabel('损失')
plt.title('训练损失曲线')
plt.grid(True, alpha=0.3)
plt.show()
多分类扩展
Softmax回归
对于 $K$ 类分类问题:
\[P(y=k|\mathbf{x}) = \frac{e^{\mathbf{w}_k^T\mathbf{x}}}{\sum_{j=1}^{K}e^{\mathbf{w}_j^T\mathbf{x}}}\]from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
# 生成三分类数据
X_multi, y_multi = make_classification(n_samples=300, n_features=2, n_informative=2,
n_redundant=0, n_classes=3, n_clusters_per_class=1,
random_state=42)
# 训练多分类模型
lr_multi = LogisticRegression(multi_class='multinomial', solver='lbfgs')
lr_multi.fit(X_multi, y_multi)
# 绘制决策区域
xx, yy = np.meshgrid(np.linspace(X_multi[:, 0].min()-1, X_multi[:, 0].max()+1, 100),
np.linspace(X_multi[:, 1].min()-1, X_multi[:, 1].max()+1, 100))
Z = lr_multi.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, Z, alpha=0.3, cmap='viridis')
for i, color in enumerate(['blue', 'orange', 'green']):
plt.scatter(X_multi[y_multi==i, 0], X_multi[y_multi==i, 1],
c=color, edgecolors='k', label=f'Class {i}')
plt.title('Softmax多分类决策区域')
plt.legend()
plt.show()
One-vs-Rest (OvR)
# OvR策略
lr_ovr = LogisticRegression(multi_class='ovr')
lr_ovr.fit(X_multi, y_multi)
print(f"OvR准确率: {lr_ovr.score(X_multi, y_multi):.4f}")
# 查看每个分类器的系数
print(f"系数形状: {lr_ovr.coef_.shape}") # (3, 2) - 3个分类器,每个2个特征
正则化
L2正则化(默认)
from sklearn.model_selection import cross_val_score
C_values = [0.001, 0.01, 0.1, 1, 10, 100]
for C in C_values:
lr = LogisticRegression(C=C, penalty='l2') # C = 1/lambda
scores = cross_val_score(lr, X, y, cv=5)
print(f"C={C:6.3f}: Accuracy = {scores.mean():.4f} ± {scores.std():.4f}")
L1正则化(特征选择)
# 高维数据
X_hd, y_hd = make_classification(n_samples=200, n_features=100, n_informative=5,
n_redundant=0, random_state=42)
lr_l1 = LogisticRegression(penalty='l1', C=0.1, solver='saga', max_iter=1000)
lr_l1.fit(X_hd, y_hd)
print(f"非零系数数量: {np.sum(lr_l1.coef_ != 0)}")
print(f"准确率: {lr_l1.score(X_hd, y_hd):.4f}")
评估指标
混淆矩阵与指标
from sklearn.metrics import (confusion_matrix, classification_report,
roc_curve, roc_auc_score, precision_recall_curve)
# 训练模型
lr = LogisticRegression()
lr.fit(X_train, y_train)
y_pred = lr.predict(X_test)
y_proba = lr.predict_proba(X_test)[:, 1]
# 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
print("混淆矩阵:")
print(cm)
# 详细报告
print("\n分类报告:")
print(classification_report(y_test, y_pred))
# ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_proba)
auc = roc_auc_score(y_test, y_proba)
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
import seaborn as sns
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('预测')
plt.ylabel('实际')
plt.title('混淆矩阵')
plt.subplot(1, 3, 2)
plt.plot(fpr, tpr, 'b-', linewidth=2, label=f'AUC = {auc:.3f}')
plt.plot([0, 1], [0, 1], 'k--', alpha=0.5)
plt.xlabel('假正例率 (FPR)')
plt.ylabel('真正例率 (TPR)')
plt.title('ROC曲线')
plt.legend()
plt.subplot(1, 3, 3)
precision, recall, _ = precision_recall_curve(y_test, y_proba)
plt.plot(recall, precision, 'g-', linewidth=2)
plt.xlabel('召回率')
plt.ylabel('精确率')
plt.title('PR曲线')
plt.tight_layout()
plt.show()
阈值调整
# 不同阈值的影响
thresholds = [0.3, 0.5, 0.7]
for thresh in thresholds:
y_pred_thresh = (y_proba >= thresh).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred_thresh).ravel()
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
print(f"阈值={thresh}: Precision={precision:.3f}, Recall={recall:.3f}")
实战示例
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
# 加载乳腺癌数据集
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
# 预处理
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 训练模型
lr = LogisticRegression(C=1.0, max_iter=1000)
lr.fit(X_train_scaled, y_train)
print(f"训练准确率: {lr.score(X_train_scaled, y_train):.4f}")
print(f"测试准确率: {lr.score(X_test_scaled, y_test):.4f}")
# 特征重要性
importance = np.abs(lr.coef_[0])
indices = np.argsort(importance)[-10:]
plt.figure(figsize=(10, 6))
plt.barh(range(10), importance[indices])
plt.yticks(range(10), [cancer.feature_names[i] for i in indices])
plt.xlabel('|系数|')
plt.title('Top 10 重要特征')
plt.tight_layout()
plt.show()
常见问题
Q1: 逻辑回归能处理非线性问题吗?
逻辑回归本身是线性分类器。处理非线性的方法:
- 添加多项式特征
- 使用核方法
- 改用非线性模型(如SVM、神经网络)
Q2: 如何处理类别不平衡?
- 设置
class_weight='balanced' - 调整决策阈值
- 使用过采样(SMOTE)或欠采样
lr_balanced = LogisticRegression(class_weight='balanced')
Q3: 为什么叫逻辑”回归”?
历史原因。逻辑回归实际上是回归模型的输出通过Sigmoid变换,可以理解为”对数几率的回归”。
Q4: 逻辑回归的优缺点?
| 优点 | 缺点 |
|---|---|
| 简单高效 | 线性决策边界 |
| 可解释性强 | 难以处理非线性 |
| 输出概率 | 对特征工程依赖大 |
| 不易过拟合 | 大规模数据训练慢 |
总结
| 概念 | 说明 |
|---|---|
| 模型 | $P(y=1|x) = \sigma(w^Tx + b)$ |
| 损失函数 | 交叉熵(对数损失) |
| 优化 | 梯度下降(凸优化问题) |
| 多分类 | Softmax / OvR |
| 正则化 | L1(稀疏)/ L2(收缩) |
参考资料
- 《统计学习方法》李航 第6章
- Andrew Ng Machine Learning Course
- scikit-learn 文档:Logistic Regression
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:《 机器学习基础系列——逻辑回归 》
本文链接:http://localhost:3015/ai/%E9%80%BB%E8%BE%91%E5%9B%9E%E5%BD%92.html
本文最后一次更新为 天前,文章中的某些内容可能已过时!