从回归到分类的优雅跳跃
线性回归输出连续值,而分类任务需要输出类别概率。逻辑回归通过Sigmoid函数将线性输出映射到[0,1]区间,实现概率预测与分类。
| 性质 | 公式 | 意义 |
|---|---|---|
| 输出范围 | (0, 1) | 天然概率 |
| 单调递增 | σ'(z) > 0 | 输入越大,P(y=1)越高 |
| 导数简洁 | σ'(z) = σ(z)(1-σ(z)) | 梯度计算高效 |
| 对称性 | σ(-z) = 1 - σ(z) | 正负对称 |
| z=0时 | σ(0) = 0.5 | 决策边界 |
import numpy as np
np.random.seed(42)
# 生成二分类数据
n = 200
X_class0 = np.random.multivariate_normal([2, 2], [[1, 0.3], [0.3, 1]], n//2)
X_class1 = np.random.multivariate_normal([5, 5], [[1, 0.3], [0.3, 1]], n//2)
X = np.vstack([X_class0, X_class1])
y = np.array([0]*(n//2) + [1]*(n//2))
# 添加偏置项
X_with_bias = np.c_[np.ones(n), X]
def sigmoid(z):
"""Sigmoid激活函数"""
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
# 梯度下降训练
weights = np.zeros(3)
lr = 0.1
for epoch in range(1000):
z = X_with_bias @ weights
predictions = sigmoid(z)
error = predictions - y
gradient = (1/n) * X_with_bias.T @ error
weights -= lr * gradient
if (epoch+1) % 200 == 0:
loss = -np.mean(y * np.log(predictions+1e-15) +
(1-y) * np.log(1-predictions+1e-15))
print(f"Epoch {epoch+1}: loss={loss:.4f}")
# 预测与评估
z_pred = X_with_bias @ weights
y_pred = (sigmoid(z_pred) >= 0.5).astype(int)
accuracy = np.mean(y_pred == y)
print(f"\n准确率: {accuracy:.4f}") # 0.965
# 混淆矩阵计算
tp = np.sum((y_pred == 1) & (y == 1)) # 真阳性
tn = np.sum((y_pred == 0) & (y == 0)) # 真阴性
fp = np.sum((y_pred == 1) & (y == 0)) # 假阳性
fn = np.sum((y_pred == 0) & (y == 1)) # 假阴性
precision = tp / (tp + fp) # 精确率
recall = tp / (tp + fn) # 召回率
f1 = 2*precision*recall / (precision+recall) # F1
print(f"TP={tp}, FP={fp}, FN={fn}, TN={tn}")
print(f"精确率: {precision:.4f}") # 0.9697
print(f"召回率: {recall:.4f}") # 0.9600
print(f"F1分数: {f1:.4f}") # 0.9648
| 指标 | 公式 | 侧重 | 适用场景 |
|---|---|---|---|
| 准确率(Accuracy) | (TP+TN)/N | 整体正确 | 类别均衡 |
| 精确率(Precision) | TP/(TP+FP) | 少误报 | 垃圾邮件、医疗诊断 |
| 召回率(Recall) | TP/(TP+FN) | 少漏报 | 癌症筛查、欺诈检测 |
| F1分数 | 2PR/(P+R) | 平衡P和R | 通用分类评估 |
| AUC-ROC | 曲线下面积 | 排序能力 | 不均衡数据 |
ROC曲线绘制不同阈值下的TPR(召回率) vs FPR,AUC衡量分类器的整体排序能力。
# 手动绘制ROC曲线数据
thresholds = np.linspace(0, 1, 100)
tpr_list, fpr_list = [], []
prob_pos = sigmoid(X_with_bias @ weights)
for thresh in thresholds:
y_t = (prob_pos >= thresh).astype(int)
tp_t = np.sum((y_t == 1) & (y == 1))
fp_t = np.sum((y_t == 1) & (y == 0))
fn_t = np.sum((y_t == 0) & (y == 1))
tn_t = np.sum((y_t == 0) & (y == 0))
tpr_list.append(tp_t / (tp_t + fn_t) if (tp_t + fn_t) > 0 else 0)
fpr_list.append(fp_t / (fp_t + tn_t) if (fp_t + tn_t) > 0 else 0)
# AUC计算(梯形法)
auc = np.trapz(tpr_list, fpr_list)
print(f"AUC: {auc:.4f}") # ≈0.99
# Softmax多分类(One-vs-Rest策略)
def softmax(z):
"""Softmax: 将多个输出转化为概率分布"""
e_z = np.exp(z - np.max(z, axis=1, keepdims=True)) # 数值稳定
return e_z / e_z.sum(axis=1, keepdims=True)
# 交叉熵损失
def cross_entropy_loss(probs, y_onehot):
return -np.mean(np.sum(y_onehot * np.log(probs + 1e-15), axis=1))
# 3类分类
np.random.seed(42)
n3 = 300
X3 = np.vstack([
np.random.normal([0, 0], 1, (100, 2)),
np.random.normal([3, 3], 1, (100, 2)),
np.random.normal([6, 0], 1, (100, 2))
])
y3 = np.array([0]*100 + [1]*100 + [2]*100)
# One-hot编码
n_classes = 3
y_onehot = np.eye(n_classes)[y3]
# 训练
X3_bias = np.c_[np.ones(n3), X3]
W = np.zeros((3, n3))
W = np.zeros((3, X3_bias.shape[1]))
for epoch in range(500):
logits = X3_bias @ W.T
probs = softmax(logits)
gradient = (probs - y_onehot).T @ X3_bias / n3
W -= 0.1 * gradient
y3_pred = softmax(X3_bias @ W.T).argmax(axis=1)
print(f"多分类准确率: {np.mean(y3_pred == y3):.4f}")
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 过采样(SMOTE) | 合成少数类样本 | ✅简单 ❌可能过拟合 |
| 欠采样 | 随机删除多数类 | ✅快速 ❌丢失信息 |
| 类别权重 | 损失函数加权 | ✅不改变数据 ❌权重难调 |
| Focal Loss | 降低易样本权重 | ✅自动调节 ❌超参敏感 |
| 阈值调整 | 不用0.5默认阈值 | ✅简单有效 ❌需业务配合 |
# 类别权重调整
def weighted_logistic_regression(X, y, class_weight={0: 1, 1: 5}, lr=0.1, epochs=1000):
n = len(y)
weights = np.zeros(X.shape[1])
sample_weights = np.array([class_weight[yi] for yi in y])
for epoch in range(epochs):
z = X @ weights
pred = sigmoid(z)
error = pred - y
gradient = (X.T @ (error * sample_weights)) / n
weights -= lr * gradient
return weights