设计深度学习模型有时就像一门艺术。有很多决策点,而且很难说哪种是最好的。一种方法是通过反复试验,并在实际数据上评估结果。因此,拥有一个科学的方法来评估神经网络和深度学习模型的性能非常重要。事实上,这也是比较任何机器学习模型在特定应用上性能的相同方法。
在本帖中,您将发现稳健评估模型性能的常用工作流程。在示例中,我们将使用 PyTorch 来构建模型,但该方法也适用于其他模型。完成本帖后,您将了解:
- 如何使用验证数据集评估 PyTorch 模型
- 如何使用 k 折交叉验证评估 PyTorch 模型
通过我的《用PyTorch进行深度学习》一书来启动你的项目。它提供了包含可用代码的自学教程。
让我们开始吧。

如何评估 PyTorch 模型的性能
图片来自 Kin Shing Lai。部分权利保留。
概述
本章分为四个部分,它们是:
- 模型的经验评估
- 数据分割
- 使用验证训练 PyTorch 模型
- k 折交叉验证
模型的经验评估
从头开始设计和配置深度学习模型时,需要做出很多决定。这包括设计决策,例如深度学习模型中使用多少层、每层的大小以及使用何种层或激活函数。还可能包括损失函数、优化算法、训练的 epoch 数以及模型输出的解释。幸运的是,有时您可以复制其他人的网络结构。有时,您可以通过一些启发式方法自己做出选择。要判断您的选择是否好,最好的方法是通过实际数据对多个替代方案进行经验评估并进行比较。
深度学习通常用于具有非常大的数据集的问题。即成千上万甚至数十万的数据样本。这提供了充足的测试数据。但是,您需要有一个强大的测试策略来估计模型在未见过的数据上的性能。基于此,您可以有一个指标来比较不同的模型配置。
想开始使用PyTorch进行深度学习吗?
立即参加我的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
数据分割
如果您有成千上万甚至更多样本的数据集,您不必总是将所有数据都用于模型训练。这会不必要地增加复杂性并延长训练时间。越多不一定越好。您可能无法获得最佳结果。
当您拥有大量数据时,您应该从中抽取一部分作为 训练集,输入模型进行训练。另一部分保留为 测试集,从训练中排除,但使用训练或部分训练的模型进行验证评估。此步骤通常称为“训练-测试拆分”。
让我们考虑 Pima 印第安人糖尿病数据集。您可以使用 NumPy 加载数据。
1 2 |
import numpy as np data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",") |
有 768 个数据样本。数量不多,但足以演示拆分。让我们将前 66% 作为训练集,剩余的作为测试集。最简单的方法是切片数组。
1 2 3 4 5 6 |
# 查找总样本数的 66% 的边界 count = len(data) n_train = int(count * 0.66) # 在边界处拆分数据 train_data = data[:n_train] test_data = data[n_train:] |
选择 66% 是任意的,但您不希望训练集太小。有时您可能使用 70%-30% 的拆分。但如果数据集非常大,您甚至可以使用 30%-70% 的拆分,只要 30% 的训练数据足够大。
如果您以这种方式拆分数据,您就假设数据集已打乱,因此训练集和测试集同样多样化。如果您发现原始数据集已排序,并且只在最后取测试集,您可能会发现您的所有测试数据都属于同一类别,或者在一个输入特征中具有相同的值。这并不理想。
当然,您可以在拆分前调用 np.random.shuffle(data)
来避免这种情况。但许多机器学习工程师通常使用 scikit-learn 来完成此操作。请看这个示例:
1 2 3 4 5 |
import numpy as np from sklearn.model_selection import train_test_split data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",") train_data, test_data = train_test_split(data, test_size=0.33) |
但更常见的是,在您分离输入特征和输出标签之后进行。请注意,此 scikit-learn 函数不仅可以处理 NumPy 数组,还可以处理 PyTorch 张量。
1 2 3 4 5 6 7 8 9 10 |
import numpy as np import torch from sklearn.model_selection import train_test_split data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",") X = data[:, 0:8] y = data[:, 8] X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33) |
使用验证训练 PyTorch 模型
让我们回顾一下构建和训练该数据集上的深度学习模型的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import torch import torch.nn as nn import torch.optim as optim import tqdm ... model = nn.Sequential( nn.Linear(8, 12), nn.ReLU(), nn.Linear(12, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid() ) # 损失函数和优化器 loss_fn = nn.BCELoss() # 二元交叉熵 optimizer = optim.Adam(model.parameters(), lr=0.0001) n_epochs = 50 # 运行的 epoch 数 batch_size = 10 # 每个批次的大小 batches_per_epoch = len(Xtrain) // batch_size for epoch in range(n_epochs): with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar: bar.set_description(f"Epoch {epoch}") for i in bar: # 获取一个批次 start = i * batch_size X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] # 前向传播 y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) # 反向传播 optimizer.zero_grad() loss.backward() # 更新权重 optimizer.step() # 打印进度 bar.set_postfix( loss=float(loss) ) |
在此代码中,每个迭代都会从训练集中提取一个批次,并在前向传播中输入模型。然后,您在后向传播中计算梯度并更新权重。
虽然在本例中,您在训练循环中使用了二元交叉熵作为损失度量,但您可能更关心预测的准确性。计算准确性很容易。您将输出(在 0 到 1 之间)四舍五入到最近的整数,这样您就可以得到 0 或 1 的二元值。然后,您计算预测与标签匹配的百分比;这就是准确性。
但是您的预测是什么?它是当前模型对 X_batch
的预测 y_pred
。将准确性添加到训练循环中会变成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
for epoch in range(n_epochs): with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar: bar.set_description(f"Epoch {epoch}") for i in bar: # 获取一个批次 start = i * batch_size X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] # 前向传播 y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) # 反向传播 optimizer.zero_grad() loss.backward() # 更新权重 optimizer.step() # 打印进度,带有准确性 acc = (y_pred.round() == y_batch).float().mean() bar.set_postfix( loss=float(loss) acc=float(acc) ) |
但是,X_batch
和 y_batch
被优化器使用,优化器会微调模型,使其能够从 X_batch
预测 y_batch
。而您现在使用准确性来检查 y_pred
是否与 y_batch
匹配。这就像作弊,因为如果您的模型以某种方式记住了解决方案,它可以直接向您报告 y_pred
并获得完美准确性,而无需实际从 X_batch
推断 y_pred
。
确实,深度学习模型可能非常复杂,以至于您无法知道模型是简单地记住了答案还是正在推断答案。因此,最好的方法是 不要 从 X_batch
或 X_train
中的任何内容计算准确性,而是从其他内容:您的测试集计算。让我们在每个 epoch 之后 使用 X_test
添加一个准确性测量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
for epoch in range(n_epochs): with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar: bar.set_description(f"Epoch {epoch}") for i in bar: # 获取一个批次 start = i * batch_size X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] # 前向传播 y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) # 反向传播 optimizer.zero_grad() loss.backward() # 更新权重 optimizer.step() # 打印进度 acc = (y_pred.round() == y_batch).float().mean() bar.set_postfix( loss=float(loss), acc=float(acc) ) # 在 epoch 结束时评估模型 y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() acc = float(acc) print(f"End of {epoch}, accuracy {acc}") |
在这种情况下,内部 for 循环中的 acc
只是一个显示进度的指标。与显示损失度量相比,差异不大,只是它不参与梯度下降算法。您期望随着损失度量的提高,准确性也会提高。
在外部 for 循环中,每个 epoch 结束时,您会根据 X_test
计算准确性。工作流程类似:您将测试集输入模型并请求其预测,然后计算与测试集标签匹配的结果数量。但是,这个准确性是您应该关心的。它应该随着训练的进行而提高,但如果您没有看到它提高(即准确性增加)甚至下降,您就必须中断训练,因为它似乎开始过拟合。过拟合是指模型开始记住训练集而不是学习从中推断预测。其迹象是训练集的准确性不断提高,而测试集的准确性却在下降。
以下是实现上述所有内容的完整代码,从数据拆分到使用测试集进行验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import numpy as np import torch import torch.nn as nn import torch.optim as optim 最后,您可以使用 matplotlib 绘制每个 epoch 的损失和准确率,如下所示: from sklearn.model_selection import train_test_split data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",") X = data[:, 0:8] y = data[:, 8] X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33) model = nn.Sequential( nn.Linear(8, 12), nn.ReLU(), nn.Linear(12, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid() ) # 损失函数和优化器 loss_fn = nn.BCELoss() # 二元交叉熵 optimizer = optim.Adam(model.parameters(), lr=0.0001) n_epochs = 50 # 运行的 epoch 数 batch_size = 10 # 每个批次的大小 batches_per_epoch = len(X_train) // batch_size for epoch in range(n_epochs): with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, #, disable=True) as bar: bar.set_description(f"Epoch {epoch}") for i in bar: # 获取一个批次 start = i * batch_size X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] # 前向传播 y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) # 反向传播 optimizer.zero_grad() loss.backward() # 更新权重 optimizer.step() # 打印进度 acc = (y_pred.round() == y_batch).float().mean() bar.set_postfix( loss=float(loss), acc=float(acc) ) # 在 epoch 结束时评估模型 y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() acc = float(acc) print(f"End of {epoch}, accuracy {acc}") |
上面的代码将打印以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
End of 0, accuracy 0.5787401795387268 End of 1, accuracy 0.6102362275123596 End of 2, accuracy 0.6220472455024719 End of 3, accuracy 0.6220472455024719 End of 4, accuracy 0.6299212574958801 End of 5, accuracy 0.6377952694892883 End of 6, accuracy 0.6496062874794006 End of 7, accuracy 0.6535432934761047 End of 8, accuracy 0.665354311466217 End of 9, accuracy 0.6614173054695129 End of 10, accuracy 0.665354311466217 End of 11, accuracy 0.665354311466217 End of 12, accuracy 0.665354311466217 End of 13, accuracy 0.665354311466217 End of 14, accuracy 0.665354311466217 End of 15, accuracy 0.6732283234596252 End of 16, accuracy 0.6771653294563293 End of 17, accuracy 0.6811023354530334 End of 18, accuracy 0.6850393414497375 End of 19, accuracy 0.6889764070510864 End of 20, accuracy 0.6850393414497375 End of 21, accuracy 0.6889764070510864 End of 22, accuracy 0.6889764070510864 End of 23, accuracy 0.6889764070510864 End of 24, accuracy 0.6889764070510864 End of 25, accuracy 0.6850393414497375 End of 26, accuracy 0.6811023354530334 End of 27, accuracy 0.6771653294563293 End of 28, accuracy 0.6771653294563293 End of 29, accuracy 0.6692913174629211 End of 30, accuracy 0.6732283234596252 End of 31, accuracy 0.6692913174629211 End of 32, accuracy 0.6692913174629211 End of 33, accuracy 0.6732283234596252 End of 34, accuracy 0.6771653294563293 End of 35, accuracy 0.6811023354530334 End of 36, accuracy 0.6811023354530334 End of 37, accuracy 0.6811023354530334 End of 38, accuracy 0.6811023354530334 End of 39, accuracy 0.6811023354530334 End of 40, accuracy 0.6811023354530334 End of 41, accuracy 0.6771653294563293 End of 42, accuracy 0.6771653294563293 End of 43, accuracy 0.6771653294563293 End of 44, accuracy 0.6771653294563293 End of 45, accuracy 0.6771653294563293 End of 46, accuracy 0.6771653294563293 End of 47, accuracy 0.6732283234596252 End of 48, accuracy 0.6732283234596252 End of 49, accuracy 0.6732283234596252 |
k 折交叉验证
在上面的示例中,您从测试集中计算了准确性。随着训练的进行,它被用作模型的 分数。您希望在分数达到最大值时停止。事实上,仅通过比较此测试集的分数,您就可以知道您的模型在 epoch 21 之后效果最好,之后开始过拟合。对吗?
如果您构建了两个不同设计的模型,您是否应该仅仅在同一个测试集上比较这些模型的准确性,并声称一个比另一个更好?
实际上,您可以辩称,即使您在提取测试集之前对数据集进行了混洗,测试集也不够有代表性。您还可以争辩说,碰巧一个模型更适合这个特定的测试集,但不总是更好。为了更有力地证明哪个模型更好,并且独立于测试集的选择,您可以尝试 多个测试集 并计算平均准确性。
这就是 k 折交叉验证的作用。它是一个决定哪个 设计 效果更好的过程。它的工作方式是,从头开始重复训练过程 $k$ 次,每次使用不同的训练集和测试集组合。因此,您将有 $k$ 个模型和 $k$ 个各自测试集的准确性分数。您不仅对平均准确性感兴趣,还对标准差感兴趣。标准差说明准确性分数是否一致,或者某个测试集是否对某个模型特别好或不好。
由于 k 折交叉验证会从头开始多次训练模型,因此最好将训练循环包装在一个函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
def model_train(X_train, y_train, X_test, y_test): # 创建新模型 model = nn.Sequential( nn.Linear(8, 12), nn.ReLU(), nn.Linear(12, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid() ) # 损失函数和优化器 loss_fn = nn.BCELoss() # 二元交叉熵 optimizer = optim.Adam(model.parameters(), lr=0.0001) n_epochs = 25 # 运行的 epoch 数 batch_size = 10 # 每个批次的大小 batches_per_epoch = len(X_train) // batch_size for epoch in range(n_epochs): with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar: bar.set_description(f"Epoch {epoch}") for i in bar: # 获取一个批次 start = i * batch_size X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] # 前向传播 y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) # 反向传播 optimizer.zero_grad() loss.backward() # 更新权重 optimizer.step() # 打印进度 acc = (y_pred.round() == y_batch).float().mean() bar.set_postfix( loss=float(loss), acc=float(acc) ) # 在训练结束时评估准确率 y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() return float(acc) |
上面的代码故意不打印任何内容(在 tqdm
中设置了 disable=True
),以保持屏幕整洁。
同样,在 scikit-learn 中,有一个用于 k 折交叉验证的函数。您可以利用它来对模型的准确率进行稳健的估计。
1 2 3 4 5 6 7 8 9 10 11 12 |
from sklearn.model_selection import StratifiedKFold # 定义 5 折交叉验证测试框架 kfold = StratifiedKFold(n_splits=5, shuffle=True) cv_scores = [] for train, test in kfold.split(X, y): # 创建模型,训练,并获取准确率 acc = model_train(X[train], y[train], X[test], y[test]) print("Accuracy: %.2f" % acc) cv_scores.append(acc) # 评估模型 print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100)) |
运行此代码会打印出:
1 2 3 4 5 6 |
Accuracy: 0.64 Accuracy: 0.67 Accuracy: 0.68 Accuracy: 0.63 Accuracy: 0.59 64.05% (+/- 3.30%) |
在 scikit-learn 中,有多种 k 折交叉验证函数,这里使用的是分层 k 折。它假定 y
是类别标签,并考虑了其值,以便在划分中提供平衡的类别表示。
上面的代码使用了 $k=5$ 或 5 个划分。这意味着将数据集分成五个相等的部分,选择其中一个作为测试集,其余部分组合成训练集。有五种方法可以做到这一点,因此上面的 for 循环将有五次迭代。在每次迭代中,您调用 model_train()
函数并返回准确率得分。然后将其保存在一个列表中,该列表将在最后用于计算均值和标准差。
kfold
对象将返回 索引。因此,您无需提前运行训练-测试拆分,而是在调用 model_train()
函数时,使用提供的索引即时提取训练集和测试集。
上面的结果显示该模型表现中等,平均准确率为 64%。并且由于标准差为 3%,因此该分数是稳定的。这意味着在大多数情况下,您期望模型的准确率为 61% 到 67%。您可以尝试更改上述模型,例如添加或删除一个层,并观察均值和标准差的变化。您还可以尝试增加训练中使用的 epoch 数并观察结果。
k 折交叉验证得到的均值和标准差是您应该用来为模型设计设定基准的指标。
总而言之,以下是 k 折交叉验证的完整代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
import numpy as np import torch import torch.nn as nn import torch.optim as optim 最后,您可以使用 matplotlib 绘制每个 epoch 的损失和准确率,如下所示: from sklearn.model_selection import StratifiedKFold data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",") X = data[:, 0:8] y = data[:, 8] X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) def model_train(X_train, y_train, X_test, y_test): # 创建新模型 model = nn.Sequential( nn.Linear(8, 12), nn.ReLU(), nn.Linear(12, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid() ) # 损失函数和优化器 loss_fn = nn.BCELoss() # 二元交叉熵 optimizer = optim.Adam(model.parameters(), lr=0.0001) n_epochs = 25 # 运行的 epoch 数 batch_size = 10 # 每个批次的大小 batches_per_epoch = len(X_train) // batch_size for epoch in range(n_epochs): with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar: bar.set_description(f"Epoch {epoch}") for i in bar: # 获取一个批次 start = i * batch_size X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] # 前向传播 y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) # 反向传播 optimizer.zero_grad() loss.backward() # 更新权重 optimizer.step() # 打印进度 acc = (y_pred.round() == y_batch).float().mean() bar.set_postfix( loss=float(loss), acc=float(acc) ) # 在训练结束时评估准确率 y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() return float(acc) # 定义 5 折交叉验证测试框架 kfold = StratifiedKFold(n_splits=5, shuffle=True) cv_scores = [] for train, test in kfold.split(X, y): # 创建模型,训练,并获取准确率 acc = model_train(X[train], y[train], X[test], y[test]) print("Accuracy: %.2f" % acc) cv_scores.append(acc) # 评估模型 print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100)) |
总结
在这篇文章中,您了解了对深度学习模型在未见过的数据上进行性能估计的稳健方法的重要性,并学会了如何做到这一点。您看到了:
- 如何使用 scikit-learn 将数据拆分为训练集和测试集
- 如何借助 scikit-learn 进行 k 折交叉验证
- 如何修改 PyTorch 模型中的训练循环以包含测试集验证和交叉验证
非常感谢您的努力,非常赞赏!
Oladimeji,不客气!我们感谢您的支持和反馈!
做得好!谢谢 James!
Jahangir,不客气!我们非常感谢您的支持和反馈!
谢谢 James!
不客气!我们感谢您的支持!