2机器学习

模型评估与选择

如何评价模型

经验误差过拟合模型选择交叉验证

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第2章:模型评估与选择(Model Evaluation and Selection)

一、导读

1.1 本章学习目标

第2章是机器学习方法论的核心章节。当我们训练了一个模型后,如何知道它好不好?如何比较两个模型的优劣?如何避免模型"记住"了训练数据而非"学到"了规律?这些问题构成了模型评估与选择的核心内容。

本章的核心学习目标包括:

  • 区分经验误差与泛化误差:训练集上的准确率和测试集上的准确率为什么不同?泛化误差的真正含义是什么?
  • 理解过拟合与欠拟合:模型复杂度与拟合程度之间的权衡关系是什么?如何诊断和解决?
  • 掌握数据集划分方法:留出法、交叉验证法、自助法各自的适用场景和优缺点是什么?
  • 掌握性能度量指标:准确率、精确率、召回率、F1、ROC/AUC 各自的含义和适用场景是什么?
  • 理解偏差-方差分解:泛化误差可以分解为哪几个部分?偏差-方差困境的本质是什么?
  • 掌握模型比较的方法:如何用统计检验判断两个模型的优劣?

1.2 为什么本章如此重要

模型评估与选择是机器学习实践中最重要的环节之一。在实际应用中,一个模型的好坏不仅取决于算法本身,更取决于我们如何评估和选择它。错误的评估方法可能导致我们选择了过拟合的模型,在实际部署中表现糟糕。

周志华教授在本章中系统地介绍了模型评估的理论和方法。其中,偏差-方差分解是理解泛化误差来源的理论框架,它告诉我们:泛化误差可以由偏差(bias)、方差(variance)和噪声(noise)三部分组成。这个分解揭示了学习算法的根本困境——偏差和方差往往不可兼得。

此外,性能度量的选择直接影响我们对模型优劣的判断。在不平衡数据集上,准确率可能具有欺骗性;在医疗诊断中,召回率可能比精确率更重要。理解这些度量的含义和适用场景,是做出正确决策的前提。

1.3 核心问题引导

在阅读本章时,请带着以下问题思考:

为什么一个在训练集上准确率达到 99% 的模型,在测试集上可能只有 60%?

如果你只有 100 个样本,如何可靠地评估模型性能?

在疾病筛查中,你更关注精确率还是召回率?为什么?

偏差高和方差高分别对应什么问题?如何判断当前模型是高偏差还是高方差?

为什么交叉验证比简单的训练-测试划分更可靠?

两个模型在测试集上的准确率分别是 90% 和 91%,能否断定第二个模型更好?


二、核心概念详解

2.1 经验误差与泛化误差

2.1.1 定义

  • 经验误差(训练误差):模型在训练集上的误差,即模型对训练数据的拟合程度
  • 泛化误差:模型在新样本上的误差,即模型对未见数据的预测能力
python
import numpy as np

def empirical_vs_generalization():
    """演示经验误差与泛化误差的差异"""
    np.random.seed(42)
    n_train = 100
    n_test = 1000
    
    # 真实函数: y = sin(2πx) + noise
    X_train = np.random.rand(n_train)
    y_train = np.sin(2 * np.pi * X_train) + np.random.randn(n_train) * 0.3
    X_test = np.random.rand(n_test)
    y_test = np.sin(2 * np.pi * X_test) + np.random.randn(n_test) * 0.3
    
    # 模型1:线性模型(欠拟合)
    w = np.polyfit(X_train, y_train, 1)
    y_pred_train_1 = np.polyval(w, X_train)
    y_pred_test_1 = np.polyval(w, X_test)
    
    # 模型2:20阶多项式(过拟合)
    w2 = np.polyfit(X_train, y_train, 20)
    y_pred_train_2 = np.polyval(w2, X_train)
    y_pred_test_2 = np.polyval(w2, X_test)
    
    # 模型3:3阶多项式(适中)
    w3 = np.polyfit(X_train, y_train, 3)
    y_pred_train_3 = np.polyval(w3, X_train)
    y_pred_test_3 = np.polyval(w3, X_test)
    
    models = [
        ("线性模型(欠拟合)", y_pred_train_1, y_pred_test_1),
        ("3阶多项式(适中)", y_pred_train_3, y_pred_test_3),
        ("20阶多项式(过拟合)", y_pred_train_2, y_pred_test_2),
    ]
    
    print(f"{'模型':<25} {'训练误差':<12} {'测试误差':<12} {'差距':<10}")
    print("-" * 60)
    for name, pred_train, pred_test in models:
        train_err = np.mean((pred_train - y_train) ** 2)
        test_err = np.mean((pred_test - y_test) ** 2)
        print(f"{name:<25} {train_err:<12.4f} {test_err:<12.4f} {test_err-train_err:<10.4f}")

empirical_vs_generalization()

2.1.2 为什么泛化误差更重要

我们真正关心的是模型在新数据上的表现(泛化误差),而非在训练数据上的表现(经验误差)。原因很简单:部署后遇到的是新数据,不是训练数据。

但问题是:泛化误差无法直接计算,因为我们不知道新数据的分布。我们只能通过测试集上的误差来近似估计泛化误差。

2.2 过拟合与欠拟合

2.2.1 过拟合

过拟合(overfitting)是指模型过于复杂,不仅学到了数据的规律,还学到了训练数据中的噪声。表现为:训练误差很低,但测试误差很高。

python
# 过拟合的直观例子
# 用高阶多项式拟合少量数据点
X = np.array([0, 1, 2, 3, 4, 5])
y = np.array([0, 1, 0, 1, 0, 1])

# 5阶多项式完美拟合训练数据
w = np.polyfit(X, y, 5)
X_new = np.linspace(0, 5, 100)
y_pred = np.polyval(w, X_new)

print("训练误差:", np.mean((np.polyval(w, X) - y) ** 2))
# 但在训练数据之间,曲线剧烈震荡
print("中间点预测值:", y_pred[25], y_pred[50], y_pred[75])
# 这些预测值可能非常离谱

2.2.2 欠拟合

欠拟合(underfitting)是指模型过于简单,无法捕捉数据的规律。表现为:训练误差和测试误差都很高。

2.2.3 模型复杂度与误差的关系

python
def plot_complexity_curve():
    """展示模型复杂度与训练/测试误差的关系"""
    complexities = list(range(1, 21))
    train_errors = []
    test_errors = []
    
    np.random.seed(42)
    X_train = np.random.rand(50)
    y_train = np.sin(2 * np.pi * X_train) + np.random.randn(50) * 0.3
    X_test = np.random.rand(200)
    y_test = np.sin(2 * np.pi * X_test) + np.random.randn(200) * 0.3
    
    for degree in complexities:
        w = np.polyfit(X_train, y_train, degree)
        train_err = np.mean((np.polyval(w, X_train) - y_train) ** 2)
        test_err = np.mean((np.polyval(w, X_test) - y_test) ** 2)
        train_errors.append(train_err)
        test_errors.append(test_err)
    
    print(f"{'复杂度':<10} {'训练误差':<12} {'测试误差':<12}")
    print("-" * 35)
    for i, d in enumerate(complexities):
        if d in [1, 3, 5, 10, 15, 20]:
            print(f"{d:<10} {train_errors[i]:<12.4f} {test_errors[i]:<12.4f}")
    
    print("\n规律:")
    print("- 复杂度增加 → 训练误差单调递减")
    print("- 测试误差先降后升(U型曲线)")
    print("- 最优复杂度在测试误差最低点")

plot_complexity_curve()

2.3 数据集划分方法

2.3.1 留出法(Hold-out)

留出法直接将数据集划分为两个互斥的集合:训练集和测试集。

python
def holdout_split(X, y, test_ratio=0.3, seed=42):
    """留出法划分数据集"""
    np.random.seed(seed)
    n = len(X)
    indices = np.random.permutation(n)
    test_size = int(n * test_ratio)
    test_idx = indices[:test_size]
    train_idx = indices[test_size:]
    
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    
    return X_train, X_test, y_train, y_test

# 分层采样:保持训练集和测试集中各类别比例一致
def stratified_holdout(X, y, test_ratio=0.3, seed=42):
    """分层采样的留出法"""
    np.random.seed(seed)
    classes = np.unique(y)
    train_idx, test_idx = [], []
    
    for c in classes:
        c_indices = np.where(y == c)[0]
        np.random.shuffle(c_indices)
        n_test = int(len(c_indices) * test_ratio)
        test_idx.extend(c_indices[:n_test])
        train_idx.extend(c_indices[n_test:])
    
    return X[train_idx], X[test_idx], y[train_idx], y[test_idx]

注意事项:

  • 训练集和测试集的比例通常为 7:3 或 8:2
  • 应使用分层采样,保持数据分布的一致性
  • 单次留出法的结果不够稳定,通常需要多次重复

2.3.2 交叉验证法(Cross-validation)

交叉验证将数据集划分为 k 个互斥子集,每次用 k-1 个子集训练,剩余 1 个子集测试,重复 k 次。

python
def k_fold_cross_validation(X, y, k=5, seed=42):
    """k折交叉验证"""
    np.random.seed(seed)
    n = len(X)
    indices = np.random.permutation(n)
    fold_sizes = np.full(k, n // k)
    fold_sizes[:n % k] += 1
    
    folds = []
    current = 0
    for fold_size in fold_sizes:
        folds.append(indices[current:current + fold_size])
        current += fold_size
    
    results = []
    for i in range(k):
        test_idx = folds[i]
        train_idx = np.concatenate([folds[j] for j in range(k) if j != i])
        
        X_train, X_test = X[train_idx], X[test_idx]
        y_train, y_test = y[train_idx], y[test_idx]
        
        # 这里用简单的KNN作为示例
        from collections import Counter
        preds = []
        for x in X_test:
            dists = np.sqrt(((X_train - x) ** 2).sum(axis=1))
            nearest = np.argsort(dists)[:3]
            pred = Counter(y_train[nearest]).most_common(1)[0][0]
            preds.append(pred)
        
        acc = np.mean(np.array(preds) == y_test)
        results.append(acc)
        print(f"Fold {i+1}: 准确率 = {acc:.4f}")
    
    print(f"\n平均准确率: {np.mean(results):.4f} ± {np.std(results):.4f}")
    return results

# 生成示例数据
np.random.seed(42)
X_demo = np.vstack([
    np.random.randn(50, 2) + [1, 1],
    np.random.randn(50, 2) + [-1, -1]
])
y_demo = np.array([0] * 50 + [1] * 50)

k_fold_cross_validation(X_demo, y_demo, k=5)

留一法(LOO):当 k = n(样本数)时,称为留一法。每次只留一个样本做测试。留一法不受随机划分的影响,但计算开销大,且在大规模数据集上不实用。

2.3.3 自助法(Bootstrapping)

自助法通过有放回采样来生成训练集和测试集。

python
def bootstrap_split(X, y, n_bootstrap=1000, seed=42):
    """自助法评估"""
    np.random.seed(seed)
    n = len(X)
    results = []
    
    for b in range(n_bootstrap):
        # 有放回采样
        train_idx = np.random.choice(n, n, replace=True)
        # 未被选中的样本作为测试集
        test_idx = np.setdiff1d(np.arange(n), train_idx)
        
        if len(test_idx) == 0:
            continue
        
        X_train, X_test = X[train_idx], X[test_idx]
        y_train, y_test = y[train_idx], y[test_idx]
        
        # 简单KNN
        preds = []
        for x in X_test:
            dists = np.sqrt(((X_train - x) ** 2).sum(axis=1))
            nearest = np.argsort(dists)[:3]
            from collections import Counter
            pred = Counter(y_train[nearest]).most_common(1)[0][0]
            preds.append(pred)
        
        acc = np.mean(np.array(preds) == y_test)
        results.append(acc)
    
    print(f"自助法评估 ({n_bootstrap} 次):")
    print(f"平均准确率: {np.mean(results):.4f} ± {np.std(results):.4f}")
    return results

bootstrap_split(X_demo, y_demo, n_bootstrap=100)

自助法的特点:

  • 训练集大小与原数据集相同,但约有 63.2% 的样本不重复
  • 约 36.8% 的样本不出现在训练集中,作为测试集
  • 适合小数据集,但会改变原始数据分布

2.4 性能度量

2.4.1 分类任务的性能度量

python
def performance_metrics(y_true, y_pred):
    """计算分类任务的各种性能指标"""
    tp = np.sum((y_true == 1) & (y_pred == 1))  # 真正例
    fp = np.sum((y_true == 0) & (y_pred == 1))  # 假正例
    tn = np.sum((y_true == 0) & (y_pred == 0))  # 真负例
    fn = np.sum((y_true == 1) & (y_pred == 0))  # 假负例
    
    accuracy = (tp + tn) / (tp + fp + tn + fn)
    precision = tp / (tp + fp) if (tp + fp) > 0 else 0
    recall = tp / (tp + fn) if (tp + fn) > 0 else 0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
    
    print(f"混淆矩阵:")
    print(f"              预测正例    预测负例")
    print(f"  真实正例    {tp:<10} {fn:<10}")
    print(f"  真实负例    {fp:<10} {tn:<10}")
    print(f"\n准确率 (Accuracy):  {accuracy:.4f}")
    print(f"精确率 (Precision): {precision:.4f}")
    print(f"召回率 (Recall):    {recall:.4f}")
    print(f"F1 分数:            {f1:.4f}")
    
    return {
        'accuracy': accuracy,
        'precision': precision,
        'recall': recall,
        'f1': f1,
        'tp': tp, 'fp': fp, 'tn': tn, 'fn': fn
    }

# 示例:疾病诊断
y_true = np.array([1, 1, 1, 0, 0, 0, 0, 0, 1, 0])
y_pred = np.array([1, 1, 0, 0, 0, 1, 0, 0, 1, 0])
performance_metrics(y_true, y_pred)

2.4.2 ROC 曲线与 AUC

ROC 曲线展示了不同阈值下真正例率(TPR)和假正例率(FPR)的权衡。

python
def roc_curve_manual(y_true, y_score):
    """手动计算ROC曲线"""
    thresholds = np.sort(np.unique(y_score))[::-1]
    tpr_list, fpr_list = [], []
    
    for thresh in thresholds:
        y_pred = (y_score >= thresh).astype(int)
        tp = np.sum((y_true == 1) & (y_pred == 1))
        fp = np.sum((y_true == 0) & (y_pred == 1))
        fn = np.sum((y_true == 1) & (y_pred == 0))
        tn = np.sum((y_true == 0) & (y_pred == 0))
        
        tpr = tp / (tp + fn) if (tp + fn) > 0 else 0
        fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
        tpr_list.append(tpr)
        fpr_list.append(fpr)
    
    # 计算AUC(梯形法则)
    auc = 0
    for i in range(1, len(fpr_list)):
        auc += (fpr_list[i] - fpr_list[i-1]) * (tpr_list[i] + tpr_list[i-1]) / 2
    
    return fpr_list, tpr_list, auc

# 示例
np.random.seed(42)
y_true = np.array([1, 1, 0, 1, 0, 0, 1, 0, 1, 0])
y_score = np.array([0.9, 0.8, 0.7, 0.6, 0.55, 0.4, 0.35, 0.2, 0.15, 0.1])

fpr, tpr, auc = roc_curve_manual(y_true, y_score)
print(f"AUC = {auc:.4f}")

2.4.3 回归任务的性能度量

python
def regression_metrics(y_true, y_pred):
    """回归任务的性能指标"""
    mae = np.mean(np.abs(y_true - y_pred))
    mse = np.mean((y_true - y_pred) ** 2)
    rmse = np.sqrt(mse)
    
    ss_res = np.sum((y_true - y_pred) ** 2)
    ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
    r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0
    
    print(f"MAE (平均绝对误差):   {mae:.4f}")
    print(f"MSE (均方误差):       {mse:.4f}")
    print(f"RMSE (均方根误差):    {rmse:.4f}")
    print(f"R² (决定系数):        {r2:.4f}")
    
    return {'mae': mae, 'mse': mse, 'rmse': rmse, 'r2': r2}

y_true = np.array([3.0, -0.5, 2.0, 7.0])
y_pred = np.array([2.5, 0.0, 2.0, 8.0])
regression_metrics(y_true, y_pred)

2.5 偏差-方差分解

2.5.1 理论框架

偏差-方差分解将泛化误差分解为三个部分:

E(f; D) = bias²(x) + var(x) + noise²
  • 偏差(bias):模型预测值与真实值之间的差距,反映模型的系统性误差
  • 方差(variance):模型在不同训练集上的预测波动程度,反映模型对数据变化的敏感性
  • 噪声(noise):数据本身的随机噪声,是任何模型都无法消除的
python
def bias_variance_demo():
    """
    偏差-方差分解的数值演示
    通过多次采样训练集,观察不同复杂度模型的偏差和方差
    """
    np.random.seed(42)
    n_samples = 50
    n_trials = 100
    X_test = np.linspace(0, 1, 200)
    
    # 真实函数
    f_true = np.sin(2 * np.pi * X_test)
    
    results = {}
    for degree in [1, 3, 10, 20]:
        predictions = np.zeros((n_trials, len(X_test)))
        
        for t in range(n_trials):
            X_train = np.random.rand(n_samples)
            y_train = np.sin(2 * np.pi * X_train) + np.random.randn(n_samples) * 0.3
            w = np.polyfit(X_train, y_train, degree)
            predictions[t] = np.polyval(w, X_test)
        
        # 平均预测
        mean_pred = predictions.mean(axis=0)
        
        # 偏差²: (E[f(x)] - f(x))²
        bias_sq = np.mean((mean_pred - f_true) ** 2)
        
        # 方差: E[(f(x) - E[f(x)])²]
        variance = np.mean(np.mean((predictions - mean_pred) ** 2, axis=0))
        
        # 总误差
        total_error = np.mean(np.mean((predictions - f_true) ** 2, axis=0))
        
        results[degree] = {
            'bias²': bias_sq,
            'variance': variance,
            'total_error': total_error
        }
    
    print(f"{'模型':<15} {'偏差²':<12} {'方差':<12} {'总误差':<12}")
    print("-" * 55)
    for degree, metrics in results.items():
        print(f"{'阶多项式':<15} {metrics['bias²']:<12.4f} "
              f"{metrics['variance']:<12.4f} {metrics['total_error']:<12.4f}")

bias_variance_demo()

2.5.2 偏差-方差困境

偏差和方差往往不可兼得:

  • 简单模型:高偏差,低方差(欠拟合)
  • 复杂模型:低偏差,高方差(过拟合)

选择合适的模型复杂度,就是在偏差和方差之间找到最佳平衡点。

2.6 模型比较与统计检验

2.6.1 假设检验的基本思想

当我们比较两个模型时,不能仅凭测试集上的准确率差异就断定优劣。需要借助统计检验来判断差异是否显著。

python
def mcnemar_test(y_true, pred_a, pred_b):
    """McNemar检验:比较两个分类器"""
    # 构建2x2列联表
    n00 = np.sum((pred_a == y_true) & (pred_b == y_true))
    n01 = np.sum((pred_a == y_true) & (pred_b != y_true))
    n10 = np.sum((pred_a != y_true) & (pred_b == y_true))
    n11 = np.sum((pred_a != y_true) & (pred_b != y_true))
    
    print("McNemar 列联表:")
    print(f"              B正确    B错误")
    print(f"  A正确       {n00:<8} {n01:<8}")
    print(f"  A错误       {n10:<8} {n11:<8}")
    
    # McNemar统计量
    if n01 + n10 > 0:
        chi2 = (n01 - n10) ** 2 / (n01 + n10)
        # 近似p值(卡方分布,自由度1)
        from math import exp, sqrt
        # 简化计算
        p_value = exp(-chi2 / 2)  # 近似
        print(f"\nMcNemar 统计量: {chi2:.4f}")
        print(f"近似 p 值: {p_value:.4f}")
        if p_value < 0.05:
            print("结论: 两个模型的差异显著 (p < 0.05)")
        else:
            print("结论: 两个模型的差异不显著 (p >= 0.05)")
    
    return n00, n01, n10, n11

# 示例
np.random.seed(42)
y_true = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0])
pred_a = np.array([1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0])
pred_b = np.array([1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0])

mcnemar_test(y_true, pred_a, pred_b)

三、实战案例

3.1 案例一:模型选择实战

python
import numpy as np

def model_selection_pipeline():
    """完整的模型选择流程"""
    np.random.seed(42)
    
    # 1. 生成数据
    n = 200
    X = np.random.rand(n, 3)
    y = (X[:, 0] + X[:, 1] > 1.0).astype(int)
    y = y.astype(float)
    # 加入一些噪声
    flip_idx = np.random.choice(n, 20, replace=False)
    y[flip_idx] = 1 - y[flip_idx]
    
    # 2. 使用交叉验证评估不同复杂度的模型
    degrees = [1, 2, 3, 5, 10]
    cv_scores = {}
    
    for degree in degrees:
        # 5折交叉验证
        fold_size = n // 5
        indices = np.random.permutation(n)
        scores = []
        
        for fold in range(5):
            test_idx = indices[fold * fold_size:(fold + 1) * fold_size]
            train_idx = np.setdiff1d(np.arange(n), test_idx)
            
            X_train, X_test = X[train_idx], X[test_idx]
            y_train, y_test = y[train_idx], y[test_idx]
            
            # 简单KNN
            preds = []
            for x in X_test:
                dists = np.sqrt(((X_train - x) ** 2).sum(axis=1))
                k = min(degree + 1, len(X_train))
                nearest = np.argsort(dists)[:k]
                pred = np.round(np.mean(y_train[nearest]))
                preds.append(pred)
            
            acc = np.mean(np.array(preds) == y_test)
            scores.append(acc)
        
        cv_scores[degree] = (np.mean(scores), np.std(scores))
    
    print(f"{'模型复杂度(K)':<15} {'平均准确率':<12} {'标准差':<10}")
    print("-" * 40)
    for degree, (mean_score, std_score) in sorted(cv_scores.items()):
        print(f"{degree:<15} {mean_score:<12.4f} {std_score:<10.4f}")
    
    best_degree = max(cv_scores, key=lambda k: cv_scores[k][0])
    print(f"\n推荐模型复杂度: K={best_degree}")

model_selection_pipeline()

3.2 案例二:不平衡数据集的性能评估

python
def imbalanced_evaluation():
    """不平衡数据集上的性能度量"""
    # 模拟疾病筛查:99%健康,1%患病
    np.random.seed(42)
    n = 1000
    y_true = np.zeros(n)
    y_true[:10] = 1  # 10个患者
    
    # 模型A:总是预测"健康"
    pred_a = np.zeros(n)
    
    # 模型B:检测出8个患者,但有5个误报
    pred_b = np.zeros(n)
    pred_b[:8] = 1     # 正确检测8个患者
    pred_b[10:15] = 1  # 5个误报
    
    print("=== 模型A:总是预测'健康' ===")
    performance_metrics(y_true, pred_a)
    
    print("\n=== 模型B:检测部分患者 ===")
    performance_metrics(y_true, pred_b)
    
    print("\n启示:")
    print("- 在不平衡数据集上,准确率具有欺骗性")
    print("- 模型A的准确率为99%,但完全没用")
    print("- 应关注精确率、召回率、F1等指标")

imbalanced_evaluation()

3.3 案例三:偏差-方差分析

python
def bias_variance_analysis():
    """偏差-方差分析的实战"""
    np.random.seed(42)
    
    # 生成数据
    n_train = 30
    n_trials = 50
    X_test = np.linspace(0, 1, 100)
    f_true = X_test ** 2  # 真实函数
    
    models = {
        '线性': 1,
        '二次': 2,
        '五次': 5,
        '十次': 10,
    }
    
    print(f"{'模型':<10} {'偏差²':<10} {'方差':<10} {'噪声':<10} {'总误差':<10}")
    print("-" * 55)
    
    for name, degree in models.items():
        predictions = np.zeros((n_trials, len(X_test)))
        
        for t in range(n_trials):
            X_train = np.random.rand(n_train)
            noise = np.random.randn(n_train) * 0.1
            y_train = X_train ** 2 + noise
            w = np.polyfit(X_train, y_train, degree)
            predictions[t] = np.polyval(w, X_test)
        
        mean_pred = predictions.mean(axis=0)
        bias_sq = np.mean((mean_pred - f_true) ** 2)
        variance = np.mean(np.mean((predictions - mean_pred) ** 2, axis=0))
        total = np.mean(np.mean((predictions - f_true) ** 2, axis=0))
        noise = total - bias_sq - variance
        
        print(f"{name:<10} {bias_sq:<10.6f} {variance:<10.6f} "
              f"{max(0, noise):<10.6f} {total:<10.6f}")

bias_variance_analysis()

四、本章小结

第2章系统地介绍了模型评估与选择的理论和方法:

经验误差与泛化误差:训练误差和测试误差的区别。我们真正关心的是泛化误差,但它无法直接计算。

过拟合与欠拟合:模型复杂度与拟合程度的权衡。过拟合是训练误差低但测试误差高,欠拟合是两者都高。

数据集划分方法:留出法(简单但不够稳定)、交叉验证法(更可靠但计算开销大)、自助法(适合小数据集)。

性能度量:准确率、精确率、召回率、F1、ROC/AUC 各有适用场景。在不平衡数据集上,准确率可能具有欺骗性。

偏差-方差分解:泛化误差 = 偏差² + 方差 + 噪声。偏差-方差困境是学习算法的根本挑战。

模型比较:通过统计检验(如 McNemar 检验)判断模型差异是否显著。

核心启示:模型评估不是事后验证,而是贯穿整个机器学习流程的核心环节。正确的评估方法和指标选择,直接决定了我们能否选出真正优秀的模型。


五、思考与练习

思考题

为什么训练误差随着模型复杂度增加而单调递减,但测试误差却呈 U 型曲线?

在以下场景中,你更关注精确率还是召回率?为什么?

- (a) 垃圾邮件过滤

- (b) 疾病筛查

- (c) 推荐系统

如果两个模型在 5 折交叉验证中的平均准确率分别是 85% 和 86%,能否断定第二个模型更好?需要什么额外信息?

偏差-方差分解中,噪声项能否通过改进算法来降低?为什么?

自助法中,为什么约有 63.2% 的样本出现在训练集中?请给出数学推导。

编程练习

实现 10 折交叉验证,在鸢尾花数据集上比较 KNN(K=1,3,5,7)的性能。

绘制 ROC 曲线,比较至少两种分类器在同一数据集上的表现,并计算 AUC。

设计一个实验,验证偏差-方差分解定理。使用不同复杂度的模型,计算偏差²、方差和总误差,观察它们之间的关系。

2/16模型评估
0%