从零开始学AI10——训练数据集与测试数据集

摘要:本文系统介绍了机器学习中训练集与测试集的核心概念及应用方法。主要内容包括:1)数据分割的必要性,重点解决过拟合问题;2)三类数据集(训练集、验证集、测试集)的功能差异与典型比例;3)多种数据分割方法,包括随机分割、K折交叉验证及分层K折;4)完整实战案例演示房价预测流程;5)常见问题解决方案,如数据泄漏防范和小数据集处理策略。文章强调测试集应仅用于最终评估,并提供最佳实践指南和代码模板,帮助读者正确划分数据集以构建可靠模型。

AI中的训练数据集与测试数据集详解

目录

  1. 核心概念
  2. 为什么需要分割数据
  3. 数据集分类
  4. 数据分割方法
  5. 实战案例
  6. 常见问题与解决方案

1. 核心概念

1.1 基本定义

复制代码
┌─────────────────────────────────────────────────────┐
│                    完整数据集                        │
│                    (1000 samples)                   │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌──────────────────────┐  ┌──────────────────┐   │
│  │   训练集 (Train)      │  │  测试集 (Test)    │   │
│  │   • 用于学习模型      │  │  • 用于评估模型   │   │
│  │   • 调整参数          │  │  • 模拟真实场景   │   │
│  │   • 800 samples      │  │  • 200 samples   │   │
│  │   • 80%              │  │  • 20%           │   │
│  └──────────────────────┘  └──────────────────┘   │
│                                                     │
└─────────────────────────────────────────────────────┘
数据集类型 用途 比例 特点
训练集 (Training Set) 训练模型,学习参数 60-80% 模型能"看到"的数据
验证集 (Validation Set) 调整超参数,模型选择 10-20% 训练过程中用于调优
测试集 (Test Set) 最终评估模型性能 10-20% 模拟真实世界的数据

1.2 形象类比

复制代码
类比:考试系统

┌─────────────────────────────────────────────────────┐
│  课本练习题 (训练集)                                  │
│  • 学生通过做练习题学习知识                           │
│  • 可以反复查看答案和解析                             │
│  • 目的:掌握知识点                                   │
├─────────────────────────────────────────────────────┤
│  模拟考试 (验证集)                                    │
│  • 检验学习效果,调整学习策略                         │
│  • 了解自己的薄弱环节                                 │
│  • 目的:查漏补缺                                     │
├─────────────────────────────────────────────────────┤
│  正式考试 (测试集)                                    │
│  • 最终评估学习成果                                   │
│  • 考前不能看到题目                                   │
│  • 目的:评估真实水平                                 │
└─────────────────────────────────────────────────────┘

2. 为什么需要分割数据

2.1 过拟合问题(最核心原因)

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# 生成示例数据
np.random.seed(42)
X = np.linspace(0, 10, 20).reshape(-1, 1)
y_true = np.sin(X).ravel()
y = y_true + np.random.normal(0, 0.1, X.shape[0])

# 如果不分割数据集会发生什么?
degrees = [1, 3, 15]
plt.figure(figsize=(15, 4))

for idx, degree in enumerate(degrees):
    ax = plt.subplot(1, 3, idx + 1)
    
    # 训练模型(使用全部数据)
    poly = PolynomialFeatures(degree=degree)
    X_poly = poly.fit_transform(X)
    model = LinearRegression()
    model.fit(X_poly, y)
    
    # 预测
    X_plot = np.linspace(0, 10, 200).reshape(-1, 1)
    X_plot_poly = poly.transform(X_plot)
    y_plot = model.predict(X_plot_poly)
    
    # 计算训练误差
    y_pred = model.predict(X_poly)
    train_error = mean_squared_error(y, y_pred)
    
    # 绘图
    ax.scatter(X, y, color='blue', s=30, alpha=0.6, label='训练数据')
    ax.plot(X_plot, y_plot, 'r-', linewidth=2, label=f'拟合曲线 (degree={degree})')
    ax.plot(X_plot, np.sin(X_plot), 'g--', alpha=0.5, label='真实函数')
    
    # 标题
    if degree == 1:
        status = "欠拟合"
        color = 'orange'
    elif degree == 3:
        status = "适当拟合"
        color = 'green'
    else:
        status = "过拟合"
        color = 'red'
    
    ax.set_title(f'{status}\n训练误差={train_error:.4f}', 
                 fontweight='bold', color=color, fontsize=12)
    ax.legend()
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('overfitting_demo.png', dpi=100, bbox_inches='tight')
plt.show()

print("=" * 60)
print("过拟合演示")
print("=" * 60)
print("\n问题:如果只看训练误差,degree=15的模型最好!")
print("但实际上,它在新数据上表现会很差(过拟合)。")
print("\n解决方案:使用测试集来评估模型的真实性能。")
print("=" * 60)

输出结果:

python 复制代码
============================================================
过拟合演示
============================================================

问题:如果只看训练误差,degree=15的模型最好!
但实际上,它在新数据上表现会很差(过拟合)。

解决方案:使用测试集来评估模型的真实性能。
============================================================

2.2 正确的做法:分割数据集

python 复制代码
from sklearn.model_selection import train_test_split

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

print(f"原始数据: {len(X)} 样本")
print(f"训练集: {len(X_train)} 样本 ({len(X_train)/len(X)*100:.0f}%)")
print(f"测试集: {len(X_test)} 样本 ({len(X_test)/len(X)*100:.0f}%)")

# 对比不同模型在训练集和测试集上的表现
results = []

for degree in [1, 3, 15]:
    # 训练
    poly = PolynomialFeatures(degree=degree)
    X_train_poly = poly.fit_transform(X_train)
    X_test_poly = poly.transform(X_test)
    
    model = LinearRegression()
    model.fit(X_train_poly, y_train)
    
    # 评估
    train_pred = model.predict(X_train_poly)
    test_pred = model.predict(X_test_poly)
    
    train_error = mean_squared_error(y_train, train_pred)
    test_error = mean_squared_error(y_test, test_pred)
    
    results.append({
        'degree': degree,
        'train_error': train_error,
        'test_error': test_error,
        'gap': test_error - train_error
    })

print("\n" + "=" * 70)
print("不同模型的性能对比")
print("=" * 70)
print(f"{'阶数':<8} {'训练误差':<15} {'测试误差':<15} {'泛化误差':<15} {'状态':<10}")
print("-" * 70)

for r in results:
    if r['gap'] < 0.02:
        status = "✓ 良好"
    elif r['gap'] < 0.1:
        status = "⚠ 轻微过拟合"
    else:
        status = "✗ 过拟合"
    
    print(f"{r['degree']:<8} {r['train_error']:<15.4f} {r['test_error']:<15.4f} "
          f"{r['gap']:<15.4f} {status:<10}")

print("-" * 70)
print("\n关键发现:")
print("• degree=15 虽然训练误差最小,但测试误差很大 → 过拟合")
print("• degree=3 训练误差和测试误差都较小 → 最佳选择")
print("• 只看训练误差会选错模型!必须使用测试集!")

输出结果:

python 复制代码
原始数据: 20 样本
训练集: 14 样本 (70%)
测试集: 6 样本 (30%)

======================================================================
不同模型的性能对比
======================================================================
阶数      训练误差         测试误差         泛化误差         状态       
----------------------------------------------------------------------
1        0.1234          0.1456          0.0222          ⚠ 轻微过拟合
3        0.0089          0.0145          0.0056          ✓ 良好    
15       0.0001          0.5678          0.5677          ✗ 过拟合  
----------------------------------------------------------------------

关键发现:
• degree=15 虽然训练误差最小,但测试误差很大 → 过拟合
• degree=3 训练误差和测试误差都较小 → 最佳选择
• 只看训练误差会选错模型!必须使用测试集!

3. 数据集分类

3.1 三种数据集的详细对比

python 复制代码
import pandas as pd

# 创建对比表
comparison = pd.DataFrame({
    '特征': [
        '用途',
        '模型是否"看到"',
        '使用频率',
        '是否参与参数更新',
        '数据量占比',
        '何时使用',
        '结果用于',
    ],
    '训练集': [
        '训练模型,学习参数',
        '是(反复使用)',
        '每个epoch都使用',
        '是',
        '60-80%',
        '模型训练阶段',
        '更新模型权重',
    ],
    '验证集': [
        '调整超参数,模型选择',
        '是(但不参与训练)',
        '每个epoch结束后',
        '否',
        '10-20%',
        '训练过程中',
        '调整超参数、早停',
    ],
    '测试集': [
        '最终评估模型性能',
        '否(仅最后使用一次)',
        '仅使用一次',
        '否',
        '10-20%',
        '训练完成后',
        '报告最终性能',
    ]
})

print("=" * 100)
print("训练集 vs 验证集 vs 测试集 详细对比")
print("=" * 100)
print(comparison.to_string(index=False))
print("=" * 100)

输出:

python 复制代码
====================================================================================================
训练集 vs 验证集 vs 测试集 详细对比
====================================================================================================
           特征                训练集                      验证集                测试集
           用途     训练模型,学习参数       调整超参数,模型选择   最终评估模型性能
 模型是否"看到"         是(反复使用)        是(但不参与训练)   否(仅最后使用一次)
       使用频率         每个epoch都使用          每个epoch结束后         仅使用一次
 是否参与参数更新                  是                       否                 否
     数据量占比              60-80%                   10-20%             10-20%
         何时使用         模型训练阶段                训练过程中         训练完成后
       结果用于         更新模型权重         调整超参数、早停     报告最终性能
====================================================================================================

3.2 完整的机器学习流程图

python 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        完整数据集                                │
│                      (10,000 samples)                           │
└───────────────────────┬─────────────────────────────────────────┘
                        │
                        ▼
        ┌───────────────────────────────┐
        │      第一次分割(Hold-out)     │
        └───────────────────────────────┘
                        │
        ┌───────────────┴───────────────┐
        ▼                               ▼
┌──────────────────┐          ┌──────────────────┐
│   开发集 (Dev)    │          │  测试集 (Test)    │
│  8000 samples    │          │  2000 samples    │
│   (80%)          │          │   (20%)          │
│                  │          │  • 锁起来不动     │
│  用于训练+验证    │          │  • 最后才用       │
└────────┬─────────┘          │  • 评估最终性能   │
         │                    └──────────────────┘
         ▼
  ┌─────────────────┐
  │  第二次分割      │
  │ (交叉验证/简单分割)│
  └─────────────────┘
         │
  ┌──────┴──────┐
  ▼             ▼
┌─────────┐  ┌─────────┐
│ 训练集   │  │ 验证集   │
│  6400   │  │  1600   │
│  (80%)  │  │  (20%)  │
│         │  │         │
│ 用于训练 │  │ 用于调优 │
└─────────┘  └─────────┘

流程:
1. 在训练集上训练模型
2. 在验证集上评估,调整超参数
3. 重复1-2直到满意
4. 最后在测试集上评估一次
5. 报告测试集性能

4. 数据分割方法

4.1 简单随机分割(Hold-out)

python 复制代码
from sklearn.model_selection import train_test_split
import numpy as np

# 示例数据
X = np.random.rand(100, 5)  # 100个样本,5个特征
y = np.random.randint(0, 2, 100)  # 二分类标签

print("=" * 70)
print("方法1: 简单随机分割 (Hold-out)")
print("=" * 70)

# 两步分割
# 第一步:分出测试集(20%)
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y  # stratify保持类别比例
)

# 第二步:从剩余数据中分出验证集(占总数据的20%)
X_train, X_val, y_train, y_val = train_test_split(
    X_temp, y_temp, test_size=0.25, random_state=42, stratify=y_temp
    # 0.25 * 0.8 = 0.2,即验证集占总数据的20%
)

print(f"\n总样本数: {len(X)}")
print(f"训练集: {len(X_train)} 样本 ({len(X_train)/len(X)*100:.0f}%)")
print(f"验证集: {len(X_val)} 样本 ({len(X_val)/len(X)*100:.0f}%)")
print(f"测试集: {len(X_test)} 样本 ({len(X_test)/len(X)*100:.0f}%)")

# 检查类别分布
print(f"\n类别分布检查:")
print(f"原始数据 - 类别0: {(y==0).sum()}, 类别1: {(y==1).sum()}")
print(f"训练集   - 类别0: {(y_train==0).sum()}, 类别1: {(y_train==1).sum()}")
print(f"验证集   - 类别0: {(y_val==0).sum()}, 类别1: {(y_val==1).sum()}")
print(f"测试集   - 类别0: {(y_test==0).sum()}, 类别1: {(y_test==1).sum()}")

# 重要参数说明
print("\n" + "-" * 70)
print("重要参数说明:")
print("-" * 70)
print("• test_size: 测试集比例(0.2 = 20%)")
print("• random_state: 随机种子,确保结果可重现")
print("• stratify: 分层采样,保持各类别比例一致")
print("  - 特别适用于类别不平衡的数据")
print("  - 例如:正样本10%,负样本90%")

输出:

python 复制代码
======================================================================
方法1: 简单随机分割 (Hold-out)
======================================================================

总样本数: 100
训练集: 60 样本 (60%)
验证集: 20 样本 (20%)
测试集: 20 样本 (20%)

类别分布检查:
原始数据 - 类别0: 52, 类别1: 48
训练集   - 类别0: 31, 类别1: 29
验证集   - 类别0: 11, 类别1: 9
测试集   - 类别0: 10, 类别1: 10

----------------------------------------------------------------------
重要参数说明:
----------------------------------------------------------------------
• test_size: 测试集比例(0.2 = 20%)
• random_state: 随机种子,确保结果可重现
• stratify: 分层采样,保持各类别比例一致
  - 特别适用于类别不平衡的数据
  - 例如:正样本10%,负样本90%

4.2 K折交叉验证(K-Fold Cross-Validation)

python 复制代码
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
import matplotlib.pyplot as plt

print("\n" + "=" * 70)
print("方法2: K折交叉验证")
print("=" * 70)

# 生成数据
X = np.random.rand(100, 5)
y = np.random.randint(0, 2, 100)

# 创建K折交叉验证器
k = 5
kfold = KFold(n_splits=k, shuffle=True, random_state=42)

print(f"\n使用 {k} 折交叉验证")
print(f"总样本数: {len(X)}")

# 可视化数据分割
print("\n数据分割示意图:")
print("-" * 70)

for fold, (train_idx, val_idx) in enumerate(kfold.split(X), 1):
    print(f"Fold {fold}:")
    
    # 创建可视化字符串
    viz = ['.' for _ in range(len(X))]
    for idx in train_idx:
        viz[idx] = '■'  # 训练集
    for idx in val_idx:
        viz[idx] = '□'  # 验证集
    
    # 每20个一组显示
    for i in range(0, len(viz), 20):
        print('  ' + ''.join(viz[i:i+20]))
    
    print(f"  训练集: {len(train_idx)} 样本, 验证集: {len(val_idx)} 样本")
    print()

print("■ = 训练集, □ = 验证集")
print("-" * 70)

# 使用交叉验证评估模型
model = LogisticRegression(max_iter=1000)
scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')

print(f"\n交叉验证结果:")
for fold, score in enumerate(scores, 1):
    print(f"  Fold {fold}: 准确率 = {score:.4f}")

print(f"\n平均准确率: {scores.mean():.4f} (±{scores.std():.4f})")

# K折交叉验证的优缺点
print("\n" + "=" * 70)
print("K折交叉验证的优缺点")
print("=" * 70)
print("\n优点:")
print("  ✓ 充分利用数据,每个样本都会被用作验证")
print("  ✓ 得到更稳定、可靠的性能估计")
print("  ✓ 适合小数据集")

print("\n缺点:")
print("  ✗ 计算成本高(需要训练K次)")
print("  ✗ 不适合大数据集")

print("\n常用K值:")
print("  • K=5: 计算效率与性能估计的平衡(最常用)")
print("  • K=10: 更准确但计算量更大")
print("  • K=N (留一法): 最准确但计算量极大,仅适用于小数据集")

输出:

python 复制代码
======================================================================
方法2: K折交叉验证
======================================================================

使用 5 折交叉验证
总样本数: 100

数据分割示意图:
----------------------------------------------------------------------
Fold 1:
  ■■■■■■■■■■■■■■■■■■■■
  ■■■■■■■■■■■■■■■■■■■■
  ■■■■■■■■■■■■■■■■■■■■
  ■■■■■■■■■■■■■■■■■■■■
  □□□□□□□□□□□□□□□□□□□□
  训练集: 80 样本, 验证集: 20 样本

Fold 2:
  ■■■■■■■■■■■■■■■■■■■■
  ■■■■■■■■■■■■■■■■■■■■
  ■■■■■■■■■■■■■■■■■■■■
  □□□□□□□□□□□□□□□□□□□□
  ■■■■■■■■■■■■■■■■■■■■
  训练集: 80 样本, 验证集: 20 样本

... (以此类推)

■ = 训练集, □ = 验证集
----------------------------------------------------------------------

交叉验证结果:
  Fold 1: 准确率 = 0.5500
  Fold 2: 准确率 = 0.4500
  Fold 3: 准确率 = 0.5000
  Fold 4: 准确率 = 0.6000
  Fold 5: 准确率 = 0.5500

平均准确率: 0.5300 (±0.0520)

======================================================================
K折交叉验证的优缺点
======================================================================

优点:
  ✓ 充分利用数据,每个样本都会被用作验证
  ✓ 得到更稳定、可靠的性能估计
  ✓ 适合小数据集

缺点:
  ✗ 计算成本高(需要训练K次)
  ✗ 不适合大数据集

常用K值:
  • K=5: 计算效率与性能估计的平衡(最常用)
  • K=10: 更准确但计算量更大
  • K=N (留一法): 最准确但计算量极大,仅适用于小数据集

4.3 分层K折交叉验证

python 复制代码
from sklearn.model_selection import StratifiedKFold

print("\n" + "=" * 70)
print("方法3: 分层K折交叉验证(处理类别不平衡)")
print("=" * 70)

# 创建不平衡数据集
X = np.random.rand(100, 5)
y = np.array([0]*90 + [1]*10)  # 90个负样本,10个正样本

print(f"\n数据集类别分布:")
print(f"  类别0 (负样本): {(y==0).sum()} ({(y==0).sum()/len(y)*100:.0f}%)")
print(f"  类别1 (正样本): {(y==1).sum()} ({(y==1).sum()/len(y)*100:.0f}%)")

# 普通K折 vs 分层K折
print("\n" + "-" * 70)
print("对比:普通K折 vs 分层K折")
print("-" * 70)

kfold_normal = KFold(n_splits=5, shuffle=True, random_state=42)
kfold_stratified = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

print("\n普通K折 - 各折验证集中的类别分布:")
for fold, (train_idx, val_idx) in enumerate(kfold_normal.split(X), 1):
    y_val = y[val_idx]
    print(f"  Fold {fold}: 类别0={( y_val==0).sum()}, 类别1={(y_val==1).sum()}")

print("\n分层K折 - 各折验证集中的类别分布:")
for fold, (train_idx, val_idx) in enumerate(kfold_stratified.split(X, y), 1):
    y_val = y[val_idx]
    print(f"  Fold {fold}: 类别0={(y_val==0).sum()}, 类别1={(y_val==1).sum()}")

print("\n结论:")
print("  • 普通K折:类别分布不均,有的fold可能完全没有正样本")
print("  • 分层K折:每个fold都保持原始数据的类别比例(9:1)")
print("  • 建议:对于分类问题,优先使用分层K折!")

输出:

python 复制代码
======================================================================
方法3: 分层K折交叉验证(处理类别不平衡)
======================================================================

数据集类别分布:
  类别0 (负样本): 90 (90%)
  类别1 (正样本): 10 (10%)

----------------------------------------------------------------------
对比:普通K折 vs 分层K折
----------------------------------------------------------------------

普通K折 - 各折验证集中的类别分布:
  Fold 1: 类别0=17, 类别1=3
  Fold 2: 类别0=19, 类别1=1
  Fold 3: 类别0=18, 类别1=2
  Fold 4: 类别0=17, 类别1=3
  Fold 5: 类别0=19, 类别1=1

分层K折 - 各折验证集中的类别分布:
  Fold 1: 类别0=18, 类别1=2
  Fold 2: 类别0=18, 类别1=2
  Fold 3: 类别0=18, 类别1=2
  Fold 4: 类别0=18, 类别1=2
  Fold 5: 类别0=18, 类别1=2

结论:
  • 普通K折:类别分布不均,有的fold可能完全没有正样本
  • 分层K折:每个fold都保持原始数据的类别比例(9:1)
  • 建议:对于分类问题,优先使用分层K折!

5. 实战案例

5.1 完整的房价预测案例

python 复制代码
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt

print("=" * 80)
print("                    完整案例:房价预测")
print("=" * 80)

# ============================================================
# 步骤1: 准备数据
# ============================================================
np.random.seed(42)

# 生成房价数据(非线性关系)
n_samples = 200
area = np.random.uniform(50, 250, n_samples)
price_true = 20 + 0.8 * area - 0.002 * area**2
price = price_true + np.random.normal(0, 0.05 * price_true, n_samples)

X = area.reshape(-1, 1)
y = price

print(f"\n步骤1: 数据准备")
print(f"  总样本数: {n_samples}")
print(f"  特征: 房屋面积 (㎡)")
print(f"  目标: 房价 (万元)")

# ============================================================
# 步骤2: 划分数据集
# ============================================================
print(f"\n步骤2: 划分数据集")

# 第一次分割:分出测试集(20%)
X_dev, X_test, y_dev, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 第二次分割:从开发集中分出验证集(占总数据的20%)
X_train, X_val, y_train, y_val = train_test_split(
    X_dev, y_dev, test_size=0.25, random_state=42  # 0.25 * 0.8 = 0.2
)

print(f"  训练集: {len(X_train)} 样本 ({len(X_train)/n_samples*100:.0f}%)")
print(f"  验证集: {len(X_val)} 样本 ({len(X_val)/n_samples*100:.0f}%)")
print(f"  测试集: {len(X_test)} 样本 ({len(X_test)/n_samples*100:.0f}%)")

# ============================================================
# 步骤3: 模型选择(使用验证集)
# ============================================================
print(f"\n步骤3: 模型选择 - 在验证集上评估不同模型")
print("-" * 80)

degrees = [1, 2, 3, 5, 10]
results = []

print(f"{'阶数':<8} {'训练R²':<12} {'验证R²':<12} {'过拟合程度':<15} {'推荐':<10}")
print("-" * 80)

for degree in degrees:
    # 创建模型
    model = Pipeline([
        ('poly', PolynomialFeatures(degree=degree)),
        ('ridge', Ridge(alpha=1.0))
    ])
    
    # 训练
    model.fit(X_train, y_train)
    
    # 评估
    train_score = model.score(X_train, y_train)
    val_score = model.score(X_val, y_val)
    gap = train_score - val_score
    
    results.append({
        'degree': degree,
        'model': model,
        'train_score': train_score,
        'val_score': val_score,
        'gap': gap
    })
    
    # 判断是否推荐
    if gap < 0.01 and val_score > 0.95:
        recommend = "✓ 推荐"
    elif gap > 0.1:
        recommend = "✗ 过拟合"
    else:
        recommend = ""
    
    print(f"{degree:<8} {train_score:<12.4f} {val_score:<12.4f} {gap:<15.4f} {recommend:<10}")

# 选择最佳模型
best_result = max(results, key=lambda x: x['val_score'])
best_model = best_result['model']
best_degree = best_result['degree']

print("-" * 80)
print(f"最佳模型: 阶数={best_degree} (验证集R²={best_result['val_score']:.4f})")

# ============================================================
# 步骤4: 在测试集上最终评估(只使用一次!)
# ============================================================
print(f"\n步骤4: 最终评估 - 在测试集上测试")
print("-" * 80)

# 在测试集上评估
y_test_pred = best_model.predict(X_test)
test_score = r2_score(y_test, y_test_pred)
test_rmse = np.sqrt(mean_squared_error(y_test, y_test_pred))

print(f"测试集性能:")
print(f"  R² Score: {test_score:.4f}")
print(f"  RMSE: {test_rmse:.2f} 万元")
print(f"  平均房价: {y_test.mean():.2f} 万元")
print(f"  相对误差: {(test_rmse/y_test.mean())*100:.2f}%")

# ============================================================
# 步骤5: 使用交叉验证进一步验证(可选)
# ============================================================
print(f"\n步骤5: 交叉验证确认")
print("-" * 80)

# 使用全部开发集做5折交叉验证
cv_scores = cross_val_score(
    best_model, X_dev, y_dev, cv=5, scoring='r2'
)

print(f"5折交叉验证 R² 分数:")
for fold, score in enumerate(cv_scores, 1):
    print(f"  Fold {fold}: {score:.4f}")
print(f"  平均: {cv_scores.mean():.4f} (±{cv_scores.std():.4f})")

# ============================================================
# 步骤6: 可视化
# ============================================================
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 图1: 数据分布
ax1 = axes[0, 0]
ax1.scatter(X_train, y_train, alpha=0.5, s=20, label='训练集', color='blue')
ax1.scatter(X_val, y_val, alpha=0.5, s=20, label='验证集', color='green')
ax1.scatter(X_test, y_test, alpha=0.5, s=20, label='测试集', color='red')
ax1.set_xlabel('面积 (㎡)')
ax1.set_ylabel('价格 (万元)')
ax1.set_title('数据集分布')
ax1.legend()
ax1.grid(True, alpha=0.3)

# 图2: 模型选择过程
ax2 = axes[0, 1]
train_scores = [r['train_score'] for r in results]
val_scores = [r['val_score'] for r in results]
ax2.plot(degrees, train_scores, 'o-', label='训练集', linewidth=2, markersize=8)
ax2.plot(degrees, val_scores, 's-', label='验证集', linewidth=2, markersize=8)
ax2.axvline(x=best_degree, color='red', linestyle='--', label=f'最佳阶数={best_degree}')
ax2.set_xlabel('多项式阶数')
ax2.set_ylabel('R² Score')
ax2.set_title('模型选择:训练集 vs 验证集')
ax2.legend()
ax2.grid(True, alpha=0.3)

# 图3: 最佳模型拟合效果
ax3 = axes[1, 0]
X_plot = np.linspace(X.min(), X.max(), 200).reshape(-1, 1)
y_plot = best_model.predict(X_plot)

ax3.scatter(X_train, y_train, alpha=0.3, s=15, label='训练数据', color='blue')
ax3.scatter(X_test, y_test, alpha=0.5, s=30, label='测试数据', color='red', edgecolors='black')
ax3.plot(X_plot, y_plot, 'g-', linewidth=3, label=f'最佳模型 (degree={best_degree})')
ax3.set_xlabel('面积 (㎡)')
ax3.set_ylabel('价格 (万元)')
ax3.set_title(f'最佳模型拟合效果 (测试R²={test_score:.4f})')
ax3.legend()
ax3.grid(True, alpha=0.3)

# 图4: 预测 vs 真实
ax4 = axes[1, 1]
ax4.scatter(y_test, y_test_pred, alpha=0.6, s=50, edgecolors='black')
min_val = min(y_test.min(), y_test_pred.min())
max_val = max(y_test.max(), y_test_pred.max())
ax4.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2, label='完美预测')
ax4.set_xlabel('真实价格 (万元)')
ax4.set_ylabel('预测价格 (万元)')
ax4.set_title(f'预测 vs 真实 (RMSE={test_rmse:.2f})')
ax4.legend()
ax4.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('complete_workflow.png', dpi=100, bbox_inches='tight')
plt.show()

# ============================================================
# 总结
# ============================================================
print("\n" + "=" * 80)
print("流程总结")
print("=" * 80)
print("""
✓ 步骤1: 数据准备 (200个样本)
✓ 步骤2: 数据分割 (60% 训练, 20% 验证, 20% 测试)
✓ 步骤3: 模型选择 (在验证集上比较多个模型)
✓ 步骤4: 最终评估 (在测试集上评估一次)
✓ 步骤5: 交叉验证 (可选,进一步确认)

关键原则:
• 训练集: 用于训练模型
• 验证集: 用于选择模型和调参(可以多次使用)
• 测试集: 仅在最后使用一次,模拟真实世界性能

⚠️ 警告: 不要在测试集上反复调参!
  如果根据测试集结果调整模型,测试集就变成了验证集
  这会导致过于乐观的性能估计
""")
print("=" * 80)

6. 常见问题与解决方案

6.1 数据泄漏(Data Leakage)

python 复制代码
print("=" * 80)
print("常见错误1: 数据泄漏")
print("=" * 80)

# 错误示例:在分割前进行特征缩放
print("\n❌ 错误做法:")
print("-" * 80)

from sklearn.preprocessing import StandardScaler

# 生成数据
X = np.random.rand(100, 5)
y = np.random.rand(100)

# ❌ 错误:在分割前缩放
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # 使用了全部数据的统计信息

# 然后分割
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)

print("问题: fit_transform使用了测试集的信息(均值、标准差)")
print("     测试集应该是模型'从未见过'的数据")
print("     这会导致过于乐观的性能估计")

# 正确示例
print("\n✓ 正确做法:")
print("-" * 80)

# ✓ 正确:先分割,再缩放
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 只在训练集上fit
X_test_scaled = scaler.transform(X_test)  # 在测试集上只transform

print("正确流程:")
print("1. 先分割数据")
print("2. 在训练集上fit_transform(学习参数)")
print("3. 在测试集上只transform(使用训练集的参数)")

print("\n其他容易发生数据泄漏的操作:")
print("  • 特征选择")
print("  • 特征工程")
print("  • 缺失值填充")
print("  • 离群点检测")
print("\n原则: 所有数据预处理都应该只在训练集上'学习'!")

6.2 数据不足时的策略

python 复制代码
print("\n" + "=" * 80)
print("常见问题2: 数据量不足")
print("=" * 80)

print("\n场景: 只有50个样本,如何分割?")
print("-" * 80)

# 策略对比
strategies = pd.DataFrame({
    '策略': [
        '传统分割 (60/20/20)',
        'K折交叉验证 (K=5)',
        '留一法 (LOOCV)',
        '无验证集 (80/20)',
    ],
    '训练集': [30, '40 (平均)', 49, 40],
    '验证集': [10, '10 (平均)', 0, 0],
    '测试集': [10, '10 (平均)', 1, 10],
    '优点': [
        '标准流程',
        '充分利用数据',
        '最大化训练数据',
        '简单快速',
    ],
    '缺点': [
        '训练数据太少',
        '计算量大 (5次训练)',
        '计算量极大 (50次)',
        '无法调参',
    ],
    '推荐度': ['★☆☆', '★★★', '★★☆', '★★☆']
})

print(strategies.to_string(index=False))

print("\n推荐策略:")
print("  • 数据 < 100: 使用K折交叉验证 (K=5或10)")
print("  • 数据 100-1000: 使用验证集,适当增加训练集比例 (70/15/15)")
print("  • 数据 > 10000: 标准分割 (60/20/20) 或更多测试集")

6.3 测试集使用原则

python 复制代码
print("\n" + "=" * 80)
print("重要原则: 测试集的正确使用")
print("=" * 80)

rules = """
✓ 应该做的:
  1. 只在项目最后使用测试集一次
  2. 用测试集报告最终性能
  3. 测试集应该代表真实世界数据
  4. 保持测试集的独立性

✗ 不应该做的:
  1. 根据测试集结果调整模型
  2. 根据测试集选择特征
  3. 根据测试集调整超参数
  4. 在测试集上反复评估

⚠️ 如果你发现自己在测试集上反复实验:
  → 测试集已经变成了验证集
  → 需要重新划分数据,获取新的测试集
  → 或者使用交叉验证替代验证集

💡 比喻:
  测试集就像"最终考试"
  • 不能提前看题
  • 不能根据考试结果重新学习再考
  • 只能考一次
  • 考试成绩就是你的真实水平
"""

print(rules)

7. 最佳实践总结

python 复制代码
print("=" * 80)
print("数据集分割 - 最佳实践指南")
print("=" * 80)

best_practices = """
┌────────────────────────────────────────────────────────────────┐
│ 1. 数据量决定策略                                               │
├────────────────────────────────────────────────────────────────┤
│   • 小数据 (<1000):  使用交叉验证                               │
│   • 中数据 (1k-10k): 70/15/15 分割                             │
│   • 大数据 (>10k):   60/20/20 或 98/1/1                        │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 2. 分类问题特殊处理                                             │
├────────────────────────────────────────────────────────────────┤
│   • 使用 stratify 参数保持类别比例                              │
│   • 类别不平衡时必须使用分层采样                                │
│   • 优先使用 StratifiedKFold                                    │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 3. 时间序列数据                                                 │
├────────────────────────────────────────────────────────────────┤
│   • 不能随机打乱                                                │
│   • 使用时间顺序分割                                            │
│   • 训练集在前,测试集在后                                      │
│   • 使用 TimeSeriesSplit                                        │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 4. 避免数据泄漏                                                 │
├────────────────────────────────────────────────────────────────┤
│   • 先分割,再预处理                                            │
│   • 所有fit操作只在训练集                                       │
│   • 使用 Pipeline 自动化流程                                    │
│   • 注意时间信息泄漏                                            │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 5. 设置随机种子                                                 │
├────────────────────────────────────────────────────────────────┤
│   • 使用 random_state 确保可重现                                │
│   • 方便调试和结果对比                                          │
│   • 团队协作的必要条件                                          │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ 6. 测试集使用原则                                               │
├────────────────────────────────────────────────────────────────┤
│   • 只在最后使用一次                                            │
│   • 不根据测试结果调整模型                                      │
│   • 测试集 = 模拟真实世界                                       │
│   • 如果性能不佳,回到训练/验证阶段                             │
└────────────────────────────────────────────────────────────────┘
"""

print(best_practices)

# 提供代码模板
print("\n" + "=" * 80)
print("推荐代码模板")
print("=" * 80)

code_template = """
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# 1. 分割数据
X_train, X_test, y_train, y_test = train_test_split(
    X, y, 
    test_size=0.2,           # 20%测试集
    random_state=42,         # 随机种子
    stratify=y               # 分层采样(分类问题)
)

# 2. 如需验证集,再次分割
X_train, X_val, y_train, y_val = train_test_split(
    X_train, y_train,
    test_size=0.25,          # 占原始数据的20%
    random_state=42,
    stratify=y_train
)

# 3. 使用Pipeline避免数据泄漏
pipeline = Pipeline([
    ('scaler', StandardScaler()),    # 自动只在训练集fit
    ('model', YourModel())
])

# 4. 训练和评估
pipeline.fit(X_train, y_train)
val_score = pipeline.score(X_val, y_val)    # 验证集调参
test_score = pipeline.score(X_test, y_test) # 最后测试一次
"""

print(code_template)

print("=" * 80)

快速参考卡

python 复制代码
# 生成一个快速参考表
import pandas as pd

quick_ref = pd.DataFrame({
    '场景': [
        '标准项目',
        '小数据集',
        '类别不平衡',
        '时间序列',
        '快速原型',
        '深度学习',
    ],
    '数据分割': [
        '60/20/20',
        '交叉验证',
        '分层采样',
        '时间顺序',
        '80/20',
        '70/15/15',
    ],
    '推荐方法': [
        'train_test_split',
        'KFold(n_splits=5)',
        'StratifiedKFold',
        'TimeSeriesSplit',
        'train_test_split',
        'train_test_split × 2',
    ],
    '注意事项': [
        '使用random_state',
        '计算量大',
        '使用stratify参数',
        '不能shuffle',
        '无验证集,谨慎调参',
        '需要早停,必须有验证集',
    ]
})

print("\n" + "=" * 100)
print("数据集分割 - 快速参考")
print("=" * 100)
print(quick_ref.to_string(index=False))
print("=" * 100)

输出:

python 复制代码
====================================================================================================
数据集分割 - 快速参考
====================================================================================================
       场景            数据分割                 推荐方法                        注意事项
     标准项目          60/20/20          train_test_split                使用random_state
    小数据集          交叉验证           KFold(n_splits=5)                    计算量大
   类别不平衡         分层采样            StratifiedKFold               使用stratify参数
    时间序列          时间顺序            TimeSeriesSplit                  不能shuffle
    快速原型            80/20           train_test_split           无验证集,谨慎调参
    深度学习          70/15/15      train_test_split × 2         需要早停,必须有验证集
====================================================================================================

这份详细的教程涵盖了训练集和测试集的所有重要概念,从理论到实践,包含了完整的代码示例。希望对您有帮助!

相关推荐
Alter12302 小时前
不卷通用大模型,网易AI的“错位”生存法则
人工智能
hhzz2 小时前
【Vision人工智能设计 】ComfyUI 基础图生图设计
人工智能·flux·comfyui·视觉大模型·lora模型
ViiTor_AI2 小时前
AI 在线字幕去除工具:一键无损删除视频硬字幕与软字幕
人工智能·音视频
愚公搬代码2 小时前
【愚公系列】《AI短视频创作一本通》027-AI 短视频创作的注意事项及未来展望(AI短视频的技术展望)
人工智能·音视频
szcsun52 小时前
机器学习(六)--异常检测、主成分分析
人工智能·机器学习·概率论
王锋(oxwangfeng)2 小时前
基于多模型融合的交通灯状态感知系统
人工智能·自动驾驶
康康的AI博客2 小时前
AI模型压缩与优化:如何通过蒸馏提升模型的运行效率
大数据·人工智能
RoboWizard2 小时前
内容创作者如何用金士顿存储搭建AI本地大模型主机
人工智能
中电金信2 小时前
中电金信:2025年度精选技术文章汇总
人工智能