分类是机器学习中最常见的问题类型之一。不管是识别垃圾邮件、诊断疾病、检测欺诈交易还是人脸识别,背后都是分类算法在起作用。本文将深入介绍几种最核心的分类算法,从原理到实战带你全面掌握。

分类问题概述

分类问题的目标是根据输入特征预测一个离散的类别标签。根据类别数量分为:

  • 二分类:只有两个类别,如垃圾邮件/非垃圾邮件
  • 多分类:有三个或以上类别,如手写数字识别(0-9)
  • 多标签分类:每个样本可以属于多个类别,如图片标签

本文我们将重点介绍二分类和多分类场景。

逻辑回归

逻辑回归虽然名字里有"回归",但它实际上是一种分类算法。它通过 Sigmoid 函数将线性回归的输出映射到 0 到 1 之间的概率值。

Sigmoid 函数

Sigmoid 函数的数学形式:

$$\sigma(z) = \frac{1}{1 + e^{-z}}$$

它把任意实数压缩到 (0, 1) 区间,非常适合表示概率。

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

z = np.linspace(-10, 10, 100)
s = sigmoid(z)

plt.figure(figsize=(10, 6))
plt.plot(z, s, 'b-', linewidth=2)
plt.axhline(y=0.5, color='gray', linestyle='--', alpha=0.5)
plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
plt.xlabel('z')
plt.ylabel('σ(z)')
plt.title('Sigmoid 函数')
plt.grid(alpha=0.3)
plt.show()

当 $\sigma(z) \geq 0.5$ 时,预测为正类;否则预测为负类。$\sigma(z) = 0.5$ 对应的 z=0 就是决策边界

决策边界

逻辑回归的决策边界在原始特征空间中是一条直线(或超平面)。通过添加多项式特征,可以拟合非线性决策边界。

from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 生成二分类数据
X, y = make_classification(n_samples=500, n_features=2, n_informative=2,
                           n_redundant=0, n_clusters_per_class=1,
                           class_sep=1.5, random_state=42)

# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 训练逻辑回归
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)

# 预测
y_pred = log_reg.predict(X_test)
y_prob = log_reg.predict_proba(X_test)[:, 1]

print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")

赔率比(Odds Ratio)

逻辑回归的系数 $\beta_j$ 的解释非常有意思。$\exp(\beta_j)$ 表示在其他特征不变的情况下,该特征每增加一个单位,赔率(odds) 的变化倍数。

print("系数与赔率比:")
for i, (coef, name) in enumerate(zip(log_reg.coef_[0], ['特征1', '特征2'])):
    odds_ratio = np.exp(coef)
    print(f"  {name}: 系数={coef:.3f}, 赔率比={odds_ratio:.3f}")
    print(f"    该特征每增加1单位, 正类的赔率变为原来的{odds_ratio:.2f}倍")

决策树

决策树是一种直观且可解释性强的分类算法。它通过一系列 if-else 规则对数据进行划分。

信息增益与熵

决策树在选择划分特征时,会优先选择能最大程度降低不纯度的特征。常用的不纯度度量有:

熵(Entropy)

$$H(S) = -\sum_{i=1}^{k} p_i \log_2(p_i)$$

基尼不纯度(Gini Impurity)

$$G(S) = 1 - \sum_{i=1}^{k} p_i^2$$

信息增益等于父节点不纯度减去子节点加权平均不纯度:

$$IG(S, A) = H(S) - \sum_{v \in Values(A)} \frac{|S_v|}{|S|} H(S_v)$$
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.datasets import load_iris

# 使用经典鸢尾花数据集
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names
class_names = iris.target_names

# 训练决策树(限制深度防止过拟合)
tree_clf = DecisionTreeClassifier(max_depth=3, random_state=42)
tree_clf.fit(X, y)

# 可视化决策树
plt.figure(figsize=(20, 10))
plot_tree(tree_clf, feature_names=feature_names, 
          class_names=list(class_names), filled=True, rounded=True)
plt.title("决策树可视化 (max_depth=3)")
plt.show()

# 特征重要性
for name, importance in zip(feature_names, tree_clf.feature_importances_):
    print(f"  {name}: {importance:.4f}")

剪枝

决策树很容易过拟合。剪枝是防止过拟合的主要手段:

  • 预剪枝:在树生长过程中提前停止(设置 max_depthmin_samples_split 等参数)
  • 后剪枝:先让树完全生长,再从底部剪掉不重要的分支
# 不同深度的决策树对比
depths = [1, 2, 3, 5, 10, None]  # None = 不限制
train_scores = []
test_scores = []

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

for depth in depths:
    clf = DecisionTreeClassifier(max_depth=depth, random_state=42)
    clf.fit(X_train, y_train)
    train_scores.append(clf.score(X_train, y_train))
    test_scores.append(clf.score(X_test, y_test))

for depth, train_acc, test_acc in zip(depths, train_scores, test_scores):
    depth_str = f"max_depth={depth}" if depth else "max_depth=None"
    print(f"{depth_str:20s} 训练集: {train_acc:.4f} 测试集: {test_acc:.4f}")
# 深度过大时,训练集准确率很高但测试集下降 = 过拟合

随机森林

随机森林通过集成多个决策树来提升预测性能和稳定性。它属于集成学习中的 Bagging 方法。

Bagging 原理

Bagging(Bootstrap Aggregating)的核心思想:

  1. 从原数据集中有放回地抽样,生成多个子数据集
  2. 在每个子数据集上训练一个决策树
  3. 取所有树的预测结果投票(分类)或平均(回归)

特征随机性

随机森林在 Bagging 的基础上增加了一层随机性:每次划分时只考虑随机选取的一部分特征。这降低了树之间的相关性,让集成效果更好。

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

# 训练随机森林
rf = RandomForestClassifier(
    n_estimators=100,       # 树的数量
    max_depth=10,           # 每棵树的最大深度
    min_samples_split=5,    # 内部节点最小样本数
    min_samples_leaf=2,     # 叶节点最小样本数
    random_state=42,
    n_jobs=-1               # 使用所有 CPU 核心
)
rf.fit(X_train, y_train)

# 预测
y_pred_rf = rf.predict(X_test)

# 评估
print("随机森林分类报告:")
print(classification_report(y_test, y_pred_rf, target_names=class_names))

# 特征重要性
for name, importance in zip(feature_names, rf.feature_importances_):
    print(f"  {name}: {importance:.4f}")

Out-of-Bag 误差

随机森林对每个样本大约有 1/3 的概率未被抽入某个子数据集。这些未被使用的样本称为 OOB(Out-of-Bag)样本,可以用来评估模型,无需额外的验证集。

rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X_train, y_train)
print(f"OOB 准确率: {rf_oob.oob_score_:.4f}")
print(f"测试集准确率: {rf_oob.score(X_test, y_test):.4f}")

支持向量机(SVM)

支持向量机寻找一个最大间隔超平面来分隔不同类别的数据。

核心思想

SVM 的核心是找到能够将不同类别分开且距离最近样本点(支持向量)最远的超平面。这个"最大间隔"原则让 SVM 具有很好的泛化能力。

from sklearn.svm import SVC

# 训练 SVM
svm_clf = SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42, probability=True)
svm_clf.fit(X_train, y_train)

y_pred_svm = svm_clf.predict(X_test)
print(f"SVM 准确率: {accuracy_score(y_test, y_pred_svm):.4f}")

核技巧(Kernel Trick)

当数据不是线性可分时,SVM 通过核函数将数据映射到高维空间,在高维空间中寻找线性决策边界。

常用核函数:

核函数 参数 适用场景
linear C 线性可分数据
rbf(高斯径向基) C, gamma 最常用,处理非线性关系
poly(多项式) C, degree, coef0 特定非线性模式
sigmoid C, coef0 类似神经网络
# 不同核函数对比
kernels = ['linear', 'rbf', 'poly']
for kernel in kernels:
    svm = SVC(kernel=kernel, C=1.0, random_state=42)
    svm.fit(X_train, y_train)
    acc = svm.score(X_test, y_test)
    print(f"kernel={kernel:8s} 准确率: {acc:.4f}")

评估指标详解

分类问题的评估比回归复杂得多,不能只看准确率。

混淆矩阵

混淆矩阵是分类评估的基础:

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

cm = confusion_matrix(y_test, y_pred_rf)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names)
disp.plot(cmap='Blues')
plt.title('随机森林混淆矩阵')
plt.show()

# 以二分类为例解释
TN, FP, FN, TP = cm.ravel() if len(cm) == 2 else (0, 0, 0, 0)

核心指标

指标 公式 含义
准确率 (Accuracy) $\frac{TP+TN}{TP+TN+FP+FN}$ 所有预测中正确的比例
精确率 (Precision) $\frac{TP}{TP+FP}$ 预测为正类中实际为正类的比例
召回率 (Recall) $\frac{TP}{TP+FN}$ 实际为正类中被正确找出的比例
F1 分数 $2 \times \frac{P \times R}{P + R}$ 精确率和召回率的调和平均
特异度 (Specificity) $\frac{TN}{TN+FP}$ 实际为负类中被正确识别出的比例
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

# 计算各种指标
print(f"准确率: {accuracy_score(y_test, y_pred_rf):.4f}")
print(f"精确率: {precision_score(y_test, y_pred_rf, average='weighted'):.4f}")
print(f"召回率: {recall_score(y_test, y_pred_rf, average='weighted'):.4f}")
print(f"F1 分数: {f1_score(y_test, y_pred_rf, average='weighted'):.4f}")

ROC 曲线与 AUC

ROC 曲线展示了不同阈值下真正率(TPR)和假正率(FPR)的关系。AUC 是曲线下面积,值越大表示模型区分正负类的能力越强。

from sklearn.metrics import roc_curve, auc, RocCurveDisplay
from sklearn.preprocessing import label_binarize

# 对于多分类,需要对标签进行二值化
y_test_bin = label_binarize(y_test, classes=[0, 1, 2])
y_score = rf.predict_proba(X_test)

# 绘制 ROC 曲线
plt.figure(figsize=(10, 8))
for i, class_name in enumerate(class_names):
    fpr, tpr, _ = roc_curve(y_test_bin[:, i], y_score[:, i])
    roc_auc = auc(fpr, tpr)
    plt.plot(fpr, tpr, label=f'{class_name} (AUC = {roc_auc:.3f})')

plt.plot([0, 1], [0, 1], 'k--', label='随机猜测')
plt.xlabel('假正率 (FPR)')
plt.ylabel('真正率 (TPR)')
plt.title('ROC 曲线 - 多分类 One-vs-Rest')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

处理不平衡数据

现实中的分类问题往往面临数据不平衡:正类样本远少于负类(如欺诈检测中正常交易占 99.9%)。

问题所在

在不平衡数据上,一个预测所有样本为负类的"傻子模型"也能达到 99.9% 的准确率,但这毫无意义。

解决方法

方法一:使用 class_weight

许多分类器支持 class_weight='balanced' 参数,自动根据类别频率调整权重。

# 不平衡数据处理
from sklearn.utils.class_weight import compute_class_weight

# 使用 balanced 权重
lr_balanced = LogisticRegression(class_weight='balanced', random_state=42)
lr_balanced.fit(X_train, y_train)

# 或手动计算权重
weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train)
print(f"自动计算的类别权重: {dict(zip(np.unique(y_train), weights))}")

方法二:SMOTE(合成少数类过采样)

SMOTE 通过在少数类样本之间插值生成新的合成样本,而不是简单复制。

# 需要安装 imbalanced-learn: pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline

smote = SMOTE(random_state=42)

# 生成平衡数据
X_balanced, y_balanced = smote.fit_resample(X_train, y_train)
print(f"SMOTE 前: {np.bincount(y_train)}")
print(f"SMOTE 后: {np.bincount(y_balanced)}")

方法三:调整评估指标

在不平衡场景下,改用精确率、召回率、F1 或 AUC 来评估模型,避免被准确率误导。

实战:完整分类流程

现在让我们把所有内容整合起来,构建一个完整的分类流水线:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

# 完整的分类流水线
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('classifier', RandomForestClassifier(
        n_estimators=100, max_depth=8, 
        class_weight='balanced',
        random_state=42, n_jobs=-1
    ))
])

pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)

print("分类报告:")
print(classification_report(y_test, y_pred, target_names=class_names))
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(f"F1 分数: {f1_score(y_test, y_pred, average='weighted'):.4f}")

算法对比总结

算法 优点 缺点 适用场景
逻辑回归 可解释性强、训练快、概率输出 决策边界线性、不能处理复杂关系 基线模型、需要解释的金融医疗场景
决策树 完全可解释、无需特征缩放 容易过拟合、不稳定(微小变化导致树结构大变) 需要规则提取的场景
随机森林 高准确率、抗过拟合、特征重要性 模型大、可解释性差、计算资源需求高 通用分类任务、特征选择
SVM 高维空间表现好、理论基础扎实 大数据集训练慢、参数敏感、不直接输出概率 文本分类、中小数据集

总结

本文介绍了四种最核心的分类算法:

  1. 逻辑回归:基于概率的分类,可解释性强
  2. 决策树:规则直观,容易理解
  3. 随机森林:集成多个决策树,提升稳定性和精度
  4. 支持向量机:通过最大间隔和核技巧处理复杂分类

同时我们学习了评估指标、混淆矩阵、ROC/AUC 以及不平衡数据的处理策略。

选择哪种算法没有固定答案,通常建议从逻辑回归或随机森林开始作为基线,再根据问题特点尝试其他方法。下一篇文章,我们将从监督学习切换到无监督学习,探索聚类分析的世界。