第7章 逻辑回归:二分类的基础

"线性回归预测房价明白了,但如果是分类问题怎么办?比如判断邮件是不是垃圾邮件?"

这是个好问题。线性回归只能预测连续值,但实际中很多问题是分类问题。

分类问题分为二分类(两个类别)和多分类(多个类别)。

逻辑回归就是最基础的二分类算法。虽然名字里有"回归",但它实际上是个分类算法。


逻辑回归的基本思想

先看个例子。

假设你想判断一封邮件是否是垃圾邮件。

你可以看邮件的一些特征:

  • 是否包含"中奖"这个词
  • 是否包含大量大写字母
  • 是否包含很多链接

这些特征组合起来,决定邮件是垃圾邮件还是正常邮件。

逻辑回归的核心思想是:把特征线性组合后,通过一个激活函数映射到[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"对于复杂的数据分布,可能需要非线性模型(如决策树、神经网络)")

对于多分类问题,逻辑回归有两种方法:

  1. OvR(One-vs-Rest):训练多个二分类器,每个分类器区分"这一类"和"其他类"
  2. 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. 使正则化更有效")

逻辑回归的优势与局限

优势

  1. 简单高效:计算速度快,易于实现
  2. 可解释性强:权重系数可以解释特征的重要性
  3. 输出概率:可以直接得到类别概率
  4. 适合高维数据:可以处理大量特征

局限

  1. 线性决策边界:只能解决线性可分问题
  2. 对异常值敏感:需要数据预处理
  3. 特征工程依赖性强:效果很大程度上取决于特征质量

本章小结

  1. 基本原理:线性组合 + Sigmoid激活函数
  2. 损失函数:交叉熵损失,比MSE更适合分类
  3. Scikit-learn实现:简单易用,一行代码训练
  4. 决策边界:线性边界,适合简单问题
  5. 正则化:L1和L2防止过拟合
  6. 实战技巧:处理类别不平衡、特征缩放

逻辑回归虽然简单,但在实际项目中经常作为基线模型。如果它效果不好,再尝试更复杂的模型。

下一章,我们学习决策树,一个完全不同的分类算法。

相关推荐
DFT计算杂谈1 小时前
VASP+Wannier90 计算位移电流和二次谐波SHG
java·服务器·前端·python·算法
执着2592 小时前
力扣102、二叉树的层序遍历
数据结构·算法·leetcode
Tisfy2 小时前
LeetCode 2976.转换字符串的最小成本 I:floyd算法(全源最短路)
算法·leetcode··floyd·题解
v_for_van2 小时前
力扣刷题记录4(无算法背景,纯C语言)
c语言·算法·leetcode
dazzle2 小时前
Python数据结构(十五):归并排序详解
数据结构·python·算法
2301_764441332 小时前
基于paCy模型与jsoncrack进行依存句法分析
python·算法·自然语言处理
咩咩不吃草2 小时前
【逻辑回归】:从模型训练到评价
算法·机器学习·逻辑回归
ersaijun2 小时前
机器人运动控制关键算法体系:从理论框架到前沿实践
算法·机器人