"线性回归预测房价明白了,但如果是分类问题怎么办?比如判断邮件是不是垃圾邮件?"
这是个好问题。线性回归只能预测连续值,但实际中很多问题是分类问题。
分类问题分为二分类(两个类别)和多分类(多个类别)。
逻辑回归就是最基础的二分类算法。虽然名字里有"回归",但它实际上是个分类算法。
逻辑回归的基本思想
先看个例子。
假设你想判断一封邮件是否是垃圾邮件。
你可以看邮件的一些特征:
- 是否包含"中奖"这个词
- 是否包含大量大写字母
- 是否包含很多链接
这些特征组合起来,决定邮件是垃圾邮件还是正常邮件。
逻辑回归的核心思想是:把特征线性组合后,通过一个激活函数映射到[0,1]区间,表示属于某个类别的概率。
数学表达:
- 线性部分:z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b
- 激活函数:p = σ(z) = 1 / (1 + e⁻ˣ)
其中σ(z)就是Sigmoid函数。
Sigmoid函数:从线性到概率
Sigmoid函数是逻辑回归的核心。
python
import numpy as np
import matplotlib.pyplot as plt
# Sigmoid函数
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 生成数据
z = np.linspace(-10, 10, 100)
y = sigmoid(z)
# 可视化
plt.figure(figsize=(10, 5))
plt.plot(z, y, linewidth=2, label='Sigmoid')
plt.axhline(y=0.5, color='r', linestyle='--', alpha=0.5)
plt.axvline(x=0, color='r', linestyle='--', alpha=0.5)
plt.xlabel('z', fontsize=12)
plt.ylabel('σ(z)', fontsize=12)
plt.title('Sigmoid函数', fontsize=14)
plt.grid(True, alpha=0.3)
plt.legend(fontsize=12)
plt.show()
print("=== Sigmoid函数特性 ===")
print(f"σ(0) = {sigmoid(0):.3f}")
print(f"σ(1) = {sigmoid(1):.3f}")
print(f"σ(-1) = {sigmoid(-1):.3f}")
print(f"σ(∞) ≈ 1")
print(f"σ(-∞) ≈ 0")
print(f"\nSigmoid函数的作用:")
print(f" 1. 将任意实数映射到[0,1]区间")
print(f" 2. 可以解释为概率")
print(f" 3. z=0时,σ(z)=0.5(分界点)")
print(f" 4. z>0时,σ(z)>0.5(正类)")
print(f" 5. z<0时,σ(z)<0.5(负类)")
Sigmoid函数就像一个"开关":
- 输入很大时,输出接近1
- 输入很小时,输出接近0
- 输入为0时,输出为0.5
逻辑回归 vs 线性回归
虽然逻辑回归和线性回归都叫"回归",但它们解决的问题不同:
python
print("=== 逻辑回归 vs 线性回归 ===")
print(f"\n线性回归:")
print(f" - 目标: 预测连续值")
print(f" - 输出范围: (-∞, +∞)")
print(f" - 例子: 房价预测、销量预测")
print(f"\n逻辑回归:")
print(f" - 目标: 预测类别概率")
print(f" - 输出范围: [0, 1]")
print(f" - 例子: 垃圾邮件检测、疾病诊断")
print(f"\n关键区别:")
print(f" - 线性回归: y = wX + b")
print(f" - 逻辑回归: p = σ(wX + b)")
print(f" - 逻辑回归多了Sigmoid激活函数")
逻辑回归本质上是在线性回归基础上,加了一个Sigmoid函数。
损失函数:交叉熵
逻辑回归的损失函数不是MSE,而是交叉熵损失(Cross-Entropy Loss)。
为什么不用MSE?
因为MSE是凸函数,但加了Sigmoid后,损失函数就变成非凸函数了,可能有多个局部最优解。
交叉熵损失函数:
Loss = -[y×log§ + (1-y)×log(1-p)]
其中:
- y:真实标签(0或1)
- p:预测概率
python
def cross_entropy_loss(y_true, y_pred):
"""交叉熵损失"""
# 避免log(0)
y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
# 测试
y_true = np.array([0, 1, 1, 0])
y_pred_good = np.array([0.1, 0.9, 0.85, 0.05]) # 预测准确
y_pred_bad = np.array([0.7, 0.3, 0.4, 0.8]) # 预测错误
loss_good = cross_entropy_loss(y_true, y_pred_good)
loss_bad = cross_entropy_loss(y_true, y_pred_bad)
print("=== 交叉熵损失 ===")
print(f"真实标签: {y_true}")
print(f"预测(准确): {y_pred_good}")
print(f"预测(错误): {y_pred_bad}")
print(f"\n交叉熵损失:")
print(f" 预测准确: {loss_good:.4f}")
print(f" 预测错误: {loss_bad:.4f}")
print(f"\n解读:")
print(f" - 预测越准确,损失越小")
print(f" - 预测越错误,损失越大")
print(f" - 完美预测时,损失为0")
Scikit-learn实现逻辑回归
用Scikit-learn实现逻辑回归非常简单。
1. 垃圾邮件检测
python
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report
from sklearn.preprocessing import StandardScaler
import numpy as np
import pandas as pd
# 生成模拟垃圾邮件数据
np.random.seed(42)
n_samples = 1000
# 特征1: 是否包含"中奖"关键词
contains_keyword = np.random.binomial(1, 0.3, n_samples)
# 特征2: 大写字母比例
uppercase_ratio = np.random.beta(2, 5, n_samples)
# 特征3: 链接数量
link_count = np.random.poisson(2, n_samples)
# 生成标签(垃圾邮件=1,正常邮件=0)
# 垃圾邮件更可能包含关键词、大写字母多、链接多
y = (contains_keyword * 3 +
uppercase_ratio * 2 +
link_count * 1 +
np.random.randn(n_samples) * 0.5) > 1.5
y = y.astype(int)
# 创建DataFrame
df = pd.DataFrame({
'contains_keyword': contains_keyword,
'uppercase_ratio': uppercase_ratio,
'link_count': link_count,
'is_spam': y
})
print("=== 垃圾邮件检测数据 ===")
print(f"数据集形状: {df.shape}")
print(f"\n数据前5行:")
print(df.head())
print(f"\n标签分布:")
print(f"正常邮件: {np.sum(y == 0)} ({np.mean(y == 0)*100:.1f}%)")
print(f"垃圾邮件: {np.sum(y == 1)} ({np.mean(y == 1)*100:.1f}%)")
# 准备数据
X = df.drop('is_spam', axis=1)
y = df['is_spam']
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"\n数据集划分:")
print(f"训练集: {X_train.shape[0]} 样本")
print(f"测试集: {X_test.shape[0]} 样本")
# 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 训练逻辑回归模型
model = LogisticRegression(random_state=42)
model.fit(X_train_scaled, y_train)
# 预测
y_pred = model.predict(X_test_scaled)
y_pred_proba = model.predict_proba(X_test_scaled)[:, 1]
# 评估
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
print(f"\n=== 模型评估 ===")
print(f"准确率: {accuracy:.4f}")
print(f"精确率: {precision:.4f}")
print(f"召回率: {recall:.4f}")
print(f"F1分数: {f1:.4f}")
print(f"\n=== 混淆矩阵 ===")
cm = confusion_matrix(y_test, y_pred)
print(f" 预测")
print(f" 正常 垃圾")
print(f"实际 正常 {cm[0, 0]} {cm[0, 1]}")
print(f" 垃圾 {cm[1, 0]} {cm[1, 1]}")
print(f"\n=== 分类报告 ===")
print(classification_report(y_test, y_pred, target_names=['正常邮件', '垃圾邮件']))
print(f"\n=== 模型参数 ===")
print(f"截距(偏置): {model.intercept_[0]:.4f}")
print(f"系数(权重):")
for feature, coef in zip(X.columns, model.coef_[0]):
print(f" {feature}: {coef:.4f}")
print(f"\n参数解读:")
print(f" - 系数为正: 该特征增加,垃圾邮件概率增加")
print(f" - 系数为负: 该特征增加,垃圾邮件概率减少")
2. 查看预测概率
python
# 查看预测概率
print("=== 预测概率示例 ===")
print("前10个测试样本的预测:")
results_df = pd.DataFrame({
'真实标签': y_test.values,
'预测标签': y_pred,
'预测概率': y_pred_proba,
'预测置信度': np.where(y_pred_proba > 0.5, y_pred_proba, 1 - y_pred_proba)
})
print(results_df.head(10))
print(f"\n概率分布:")
print(f"预测为正常邮件的概率范围: [{y_pred_proba[y_pred == 0].min():.3f}, {y_pred_proba[y_pred == 0].max():.3f}]")
print(f"预测为垃圾邮件的概率范围: [{y_pred_proba[y_pred == 1].min():.3f}, {y_pred_proba[y_pred == 1].max():.3f}]")
# 可视化预测概率
plt.figure(figsize=(10, 5))
plt.hist(y_pred_proba[y_test == 0], bins=20, alpha=0.7, label='正常邮件', edgecolor='black')
plt.hist(y_pred_proba[y_test == 1], bins=20, alpha=0.7, label='垃圾邮件', edgecolor='black')
plt.axvline(x=0.5, color='r', linestyle='--', linewidth=2, label='决策边界 (0.5)')
plt.xlabel('预测概率(垃圾邮件)', fontsize=12)
plt.ylabel('频数', fontsize=12)
plt.title('预测概率分布', fontsize=14)
plt.legend(fontsize=12)
plt.grid(True, alpha=0.3)
plt.show()
决策边界与多分类
逻辑回归的决策边界是线性的。意思是说,它用一条直线(或超平面)来划分两类。
python
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
import matplotlib.pyplot as plt
import numpy as np
# 生成二维数据
X, y = make_classification(
n_samples=200, n_features=2, n_redundant=0,
n_informative=2, random_state=42, n_clusters_per_class=1
)
# 训练逻辑回归
model = LogisticRegression(random_state=42)
model.fit(X, y)
# 绘制决策边界
def plot_decision_boundary(model, X, y):
# 设置网格
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))
# 预测
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制
plt.figure(figsize=(10, 8))
plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolor='black', s=50)
plt.xlabel('特征1', fontsize=12)
plt.ylabel('特征2', fontsize=12)
plt.title('逻辑回归决策边界', fontsize=14)
plt.grid(True, alpha=0.3)
plt.show()
plot_decision_boundary(model, X, y)
print("=== 决策边界分析 ===")
print(f"决策边界: {model.coef_[0][0]}*x1 + {model.coef_[0][1]}*x2 + {model.intercept_[0]} = 0")
print(f"\n逻辑回归的决策边界是线性的")
print(f"对于复杂的数据分布,可能需要非线性模型(如决策树、神经网络)")
对于多分类问题,逻辑回归有两种方法:
- OvR(One-vs-Rest):训练多个二分类器,每个分类器区分"这一类"和"其他类"
- OvO(One-vs-One):训练多个二分类器,每对类别之间训练一个分类器
Scikit-learn默认使用OvR方法:
python
# 多分类示例
X_multi, y_multi = make_classification(
n_samples=300, n_features=2, n_redundant=0,
n_informative=2, n_classes=3, random_state=42
)
# 训练多分类逻辑回归
model_multi = LogisticRegression(multi_class='ovr', random_state=42)
model_multi.fit(X_multi, y_multi)
# 预测
y_pred_multi = model_multi.predict(X_multi)
accuracy_multi = accuracy_score(y_multi, y_pred_multi)
print("=== 多分类示例 ===")
print(f"类别数量: {len(np.unique(y_multi))}")
print(f"准确率: {accuracy_multi:.4f}")
print(f"分类器数量: {model_multi.coef_.shape[0]}")
# 可视化
plot_decision_boundary(model_multi, X_multi, y_multi)
正则化
和线性回归一样,逻辑回归也可以使用正则化防止过拟合。
Scikit-learn的逻辑回归默认使用L2正则化,可以通过参数调节:
C:正则化强度的倒数(C越小,正则化越强)
python
# 比较不同正则化强度的效果
C_values = [0.01, 0.1, 1, 10, 100]
print("=== 正则化强度对比 ===")
plt.figure(figsize=(15, 3))
for i, C in enumerate(C_values):
model = LogisticRegression(C=C, random_state=42)
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
accuracy = accuracy_score(y_test, y_pred)
# 查看系数大小
coef_norm = np.linalg.norm(model.coef_)
print(f"C = {C:5.2f}: 准确率 = {accuracy:.4f}, 系数范数 = {coef_norm:.4f}")
# 可视化决策边界(使用原始数据)
plt.subplot(1, 5, i+1)
plot_decision_boundary(model, X_train_scaled, y_train)
plt.title(f'C = {C}')
plt.tight_layout()
plt.show()
print(f"\n正则化总结:")
print(f" - C越小,正则化越强,系数越小,可能欠拟合")
print(f" - C越大,正则化越弱,系数越大,可能过拟合")
print(f" - 需要通过交叉验证选择最优的C值")
实战技巧
1. 处理类别不平衡
如果数据集类别不平衡(比如正常邮件900封,垃圾邮件100封),模型可能倾向于预测多数类。
解决方案:
- 使用
class_weight='balanced'参数 - 使用SMOTE等过采样技术
- 使用不同的评估指标(F1、AUC)
python
# 类别不平衡示例
print("=== 处理类别不平衡 ===")
# 创建不平衡数据
y_imbalanced = np.zeros(len(y))
y_imbalanced[:100] = 1 # 只有10%是正类
X_train_imb, X_test_imb, y_train_imb, y_test_imb = train_test_split(
X, y_imbalanced, test_size=0.2, random_state=42, stratify=y_imbalanced
)
# 不处理不平衡
model_imb = LogisticRegression(random_state=42)
model_imb.fit(X_train_imb, y_train_imb)
y_pred_imb = model_imb.predict(X_test_imb)
accuracy_imb = accuracy_score(y_test_imb, y_pred_imb)
precision_imb = precision_score(y_test_imb, y_pred_imb, zero_division=0)
recall_imb = recall_score(y_test_imb, y_pred_imb, zero_division=0)
print(f"\n不处理不平衡:")
print(f" 准确率: {accuracy_imb:.4f}")
print(f" 精确率: {precision_imb:.4f}")
print(f" 召回率: {recall_imb:.4f}")
# 处理不平衡(使用class_weight)
model_balanced = LogisticRegression(class_weight='balanced', random_state=42)
model_balanced.fit(X_train_imb, y_train_imb)
y_pred_balanced = model_balanced.predict(X_test_imb)
accuracy_balanced = accuracy_score(y_test_imb, y_pred_balanced)
precision_balanced = precision_score(y_test_imb, y_pred_balanced, zero_division=0)
recall_balanced = recall_score(y_test_imb, y_pred_balanced, zero_division=0)
print(f"\n使用class_weight='balanced':")
print(f" 准确率: {accuracy_balanced:.4f}")
print(f" 精确率: {precision_balanced:.4f}")
print(f" 召回率: {recall_balanced:.4f}")
print(f"\n结论:")
print(f" - 类别不平衡时,准确率可能很高但实际效果差")
print(f" - class_weight='balanced'可以提高召回率")
print(f" - 需要根据业务场景权衡精确率和召回率")
2. 特征缩放
逻辑回归对特征尺度敏感,需要标准化:
python
print("=== 特征缩放的影响 ===")
# 不缩放
model_no_scale = LogisticRegression(random_state=42)
model_no_scale.fit(X_train, y_train)
y_pred_no_scale = model_no_scale.predict(X_test)
accuracy_no_scale = accuracy_score(y_test, y_pred_no_scale)
# 缩放
model_scaled = LogisticRegression(random_state=42)
model_scaled.fit(X_train_scaled, y_train)
y_pred_scaled = model_scaled.predict(X_test_scaled)
accuracy_scaled = accuracy_score(y_test, y_pred_scaled)
print(f"不缩放准确率: {accuracy_no_scale:.4f}")
print(f"缩放后准确率: {accuracy_scaled:.4f}")
print(f"\n特征缩放可以:")
print(f" 1. 加速梯度下降收敛")
print(f" 2. 提高模型性能")
print(f" 3. 使正则化更有效")
逻辑回归的优势与局限
优势
- 简单高效:计算速度快,易于实现
- 可解释性强:权重系数可以解释特征的重要性
- 输出概率:可以直接得到类别概率
- 适合高维数据:可以处理大量特征
局限
- 线性决策边界:只能解决线性可分问题
- 对异常值敏感:需要数据预处理
- 特征工程依赖性强:效果很大程度上取决于特征质量
本章小结
- 基本原理:线性组合 + Sigmoid激活函数
- 损失函数:交叉熵损失,比MSE更适合分类
- Scikit-learn实现:简单易用,一行代码训练
- 决策边界:线性边界,适合简单问题
- 正则化:L1和L2防止过拟合
- 实战技巧:处理类别不平衡、特征缩放
逻辑回归虽然简单,但在实际项目中经常作为基线模型。如果它效果不好,再尝试更复杂的模型。
下一章,我们学习决策树,一个完全不同的分类算法。