正如我们在学习曲线中看到的那样,仅仅最小化训练数据上的损失并不能保证在未见过的数据上表现良好。我们需要可靠的途径来估算模型在部署前的泛化能力。这需要仔细划分数据,以模拟遇到新示例的情形。留出验证集深度学习中,尤其是在大型数据集上,最常见的策略是将可用数据分为三个不同的数据集:训练集: 这是数据中最大的一部分,专门用于通过反向传播训练模型参数(权重和偏置)。模型从这些数据中学习其潜在的规律。验证集(或开发集): 该数据集在训练和开发阶段使用,用于评估模型在未直接训练过的数据上的表现。其主要目的是指导关于超参数(如学习率、正则化强度、网络结构选择)和模型选择的决定。你可以训练几种模型变体或调整超参数,而验证集会帮助你选择表现最好的模型,而不触及最终的测试集。监测验证集与训练集上的表现(如学习曲线所示)对于诊断过拟合或欠拟合很重要。测试集: 该数据集被预留并仅使用一次,在所有训练和超参数调整完成后。它提供了模型在真正未见过数据上泛化表现的最终、无偏估计。在模型开发过程中避免将测试集用于任何决策很重要;否则,你可能会不经意地对测试集过拟合,导致对性能的估计过于乐观。典型的划分比例可能是训练集占70%,验证集占15%,测试集占15%,但这些百分比会根据数据集的总大小而显著不同。对于非常大型的数据集(数百万个示例),验证集和测试集所占的比例可能小得多(例如,各占1%),但其绝对数量仍足以提供可靠的评估。留出法的主要优点是其简单性和计算效率。然而,在验证集上获得的性能估计可能对特定的随机划分敏感,特别是当数据集不是很大时。“幸运”或“不幸运”的划分可能会对模型真实能力产生误导性印象。交叉验证(CV)当数据稀缺时,或者当你需要更可靠的泛化性能评估时,K折交叉验证是一种有用的方法。相较于单一验证划分,K折交叉验证使用多次划分来减少与留出法相关的方差。其工作原理如下:划分: 原始训练数据(不包括最终的测试集,测试集保持独立)被随机划分为 k 个大小相等的折(子集)。k 的常见取值是 5 或 10。迭代: 模型训练 k 次。在每次迭代 i 中(从 1 到 k):折 i 被作为该次迭代的验证集。其余的 k-1 个折被组合起来作为训练集。评估: 每次训练运行后,模型在该次迭代中使用的验证折上的性能(例如,准确率、损失)被计算。平均: 最终的性能评估是所有 k 次迭代中获得的性能指标的平均值。digraph KFold { rankdir=LR; node [shape=rect, style=filled, color="#ced4da", margin="0.1,0.05"]; edge [arrowhead=none]; subgraph cluster_data { label = "训练数据"; bgcolor="#e9ecef"; Data [label="折 1 | 折 2 | 折 3 | ... | 折 k"]; } subgraph cluster_iter1 { label = "迭代 1"; bgcolor="#f1f3f5"; node [color="#a5d8ff"]; Train1 [label="折 2 到 k"]; Val1 [label="折 1", color="#ffc9c9"]; Train1 -> Val1 [style=invis]; } subgraph cluster_iter2 { label = "迭代 2"; bgcolor="#f1f3f5"; node [color="#a5d8ff"]; Train2 [label="折 1, 3 到 k"]; Val2 [label="折 2", color="#ffc9c9"]; Train2 -> Val2 [style=invis]; } subgraph cluster_iterk { label = "迭代 k"; bgcolor="#f1f3f5"; node [color="#a5d8ff"]; Traink [label="折 1 到 k-1"]; Valk [label="折 k", color="#ffc9c9"]; Traink -> Valk [style=invis]; } Data -> Train1 [style=invis]; Data -> Train2 [style=invis]; Data -> Traink [style=invis]; Perf1 [shape=oval, label="指标 1", color="#ced4da"]; Perf2 [shape=oval, label="指标 2", color="#ced4da"]; Perfk [shape=oval, label="指标 k", color="#ced4da"]; AvgPerf [shape=oval, label="平均(指标)", color="#495057", fontcolor="white"]; Val1 -> Perf1; Val2 -> Perf2; Valk -> Perfk; {Perf1, Perf2, Perfk} -> AvgPerf; }K折交叉验证图示。训练数据被分成 k 个折。在每次迭代中,一个折作为验证集(红色),而其他折用于训练(蓝色)。每次迭代的性能指标被平均。K折交叉验证的优点:更可靠的评估: 通过对 k 次运行求平均,性能评估对单一随机划分的依赖性较小。数据使用效率高: 每个数据点都恰好被用于验证一次,并被用于训练 k-1 次。K折交叉验证的缺点:计算成本: 训练模型 k 次比训练一次要昂贵得多,对于需要数天或数周训练的非常大型的深度学习模型来说,这可能难以承受。分层K折: 在处理分类问题时,特别是当类别不平衡时,重要的是每个折都能保留与完整数据集近似的每个类别的样本百分比。分层K折交叉验证确保这种分层,从而得到更可靠的评估。尽管由于成本原因,K折交叉验证在深度学习中较少用于最终模型训练,但它是一种非常有用的方法,可用于:在较小数据集上评估不同的模型结构或特征集。在研究中比较不同方法时,获得可靠的性能评估。超参数调整,尽管在 k 个折上调整会进一步增加计算成本。实际数据划分无论是使用简单的留出划分还是准备交叉验证,都需要仔细实现。像 Scikit-learn 这样的库提供了方便的功能。import torch from sklearn.model_selection import train_test_split, StratifiedKFold import numpy as np # 假设 X_data 包含你的特征,y_data 包含你的标签(作为 NumPy 数组或张量) # 示例模拟数据 X_data = np.random.rand(1000, 20) # 1000 samples, 20 features y_data = np.random.randint(0, 2, 1000) # 1000 binary labels # --- 留出划分 --- # 首先,划分为训练+验证集和测试集 X_train_val, X_test, y_train_val, y_test = train_test_split( X_data, y_data, test_size=0.15, random_state=42, stratify=y_data # 分类问题使用 stratify 参数进行分层 ) # 然后,将训练+验证集划分为实际的训练集和验证集 X_train, X_val, y_train, y_val = train_test_split( X_train_val, y_train_val, test_size=0.1765, # 大约是原始数据的 15% (0.15 / (1-0.15)) random_state=42, stratify=y_train_val # 保持分层 ) print(f"留出划分大小:训练集={len(X_train)}, 验证集={len(X_val)}, 测试集={len(X_test)}") # 输出:留出划分大小:训练集=700, 验证集=150, 测试集=150 (近似比例) # --- K折交叉验证设置(使用分层K折)--- n_splits = 5 skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) print(f"\n{n_splits}折交叉验证索引(使用原始训练+验证数据):") # 注意:这里我们使用 X_train_val,因为测试集总是保持独立。 fold_num = 1 for train_index, val_index in skf.split(X_train_val, y_train_val): print(f"折 {fold_num}:") print(f" 训练样本: {len(train_index)}, 验证样本: {len(val_index)}") # 在真实的交叉验证循环中,你会使用这些索引来选择数据: # X_train_fold, X_val_fold = X_train_val[train_index], X_train_val[val_index] # y_train_fold, y_val_fold = y_train_val[train_index], y_train_val[val_index] # ... 然后在这些折特定的数据集上训练和评估模型 fold_num += 1 # 如果训练需要,转换为 PyTorch 张量 X_train_tensor = torch.tensor(X_train, dtype=torch.float32) y_train_tensor = torch.tensor(y_train, dtype=torch.long) # 假设是分类标签 X_val_tensor = torch.tensor(X_val, dtype=torch.float32) y_val_tensor = torch.tensor(y_val, dtype=torch.long) X_test_tensor = torch.tensor(X_test, dtype=torch.float32) y_test_tensor = torch.tensor(y_test, dtype=torch.long)使用 Scikit-learn 创建留出划分并生成 K折交叉验证的索引。random_state 确保可复现性,stratify 保持类别比例。请记住:在划分数据之前,务必打乱数据(除非是时间序列数据,其中时间顺序很重要)。大多数划分函数在 shuffle=True 时默认会这样做。对于分类任务使用分层,以确保类别分布在不同划分中得到保留。测试集在项目结束之前始终不被触及。所有模型开发、调整和选择都使用训练集和验证集(或在训练数据上进行交叉验证)进行。选择正确的验证策略很重要。它使我们能够可靠地评估泛化性能,诊断过拟合等问题,并在模型开发过程中做出明智的决策,最终产生在实际遇到的数据上表现良好的模型。