为什么要将数据集划分为三个部分?三个部分的作用?三个部分数据集的比例应如何设定?
另外一种常见的数据集划分方法是将数据集划分为两个部分(训练集和测试集),这种划分方法存在的问题在于,模型利用训练集数据进行训练,测试集数据进行模型泛化性测试。但我们不能利用测试集测试的bad case或者根据测试集的测试精度调整模型的参数。这是因为对于模型来说,数据集应该是只有训练集可见的,其他数据均不可见,如果利用测试集的结果调整模型相对于模型也”看到了“测试集的数据。将数据集划分为是独立同分布的三个部分就可以解决这个问题,将训练集数据用于模型训练,验证集(开发集)数据用于模型调参,测试集数据用于验证模型泛化性。
介绍
训练集
训练集(Training Dataset)是用来训练模型使用的。
验证集
当我们的模型训练好之后,我们并不知道他的表现如何。这个时候就可以使用验证集(Validation Dataset)来看看模型在新数据(验证集和测试集是不同的数据)上的表现如何。同时通过调整超参数,让模型处于最好的状态。
验证集有2个主要的作用:
1、评估模型效果,为了调整超参数而服务
2、调整超参数,使得模型在验证集上的效果最好
说明:
1、验证集不像训练集和测试集,它是非必需的。如果不需要调整超参数,就可以不使用验证集,直接用测试集来评估效果。
2、验证集评估出来的效果并非模型的最终效果,主要是用来调整超参数的,模型最终效果以测试集的评估结果为准。
在机器学习中,训练集是用来训练模型的数据,而验证集通常是从原始数据集中划分出来的一个子集,用于在训练过程中检查模型的性能,是在过拟合或欠拟合的情况下对模型进行评估和调整的数据。
验证集的主要目的是为了找到一个最佳的模型及参数,使得模型在未知数据上的表现最好。
之前提到,训练集一般会占用60%或80%的比例,对应的验证集则一般会占用20%或10%的比例。划分比例的依据可以根据实际需求和数据集的大小来确定。
通常情况下,我们可以使用随机抽样的方法从原始数据集中划分验证集
验证集在整个模型训练的过程中起着关键的作用,我们从几个方面出发,聊聊其重要性。
1.调整模型的超参数
在机器学习模型中,有许多超参数需要我们参与设置,例如学习率、隐藏层神经元数量等。这些超参数的选择对模型的性能有很大影响。
为了找到最优的超参数组合,我们可以将训练过程分为多个阶段,每个阶段使用不同的超参数组合进行训练。然后,我们可以使用验证集来评估每个阶段模型的性能,从而选择出最优的超参数组合。
2.早停策略
在训练过程中,如果我们发现模型在验证集上的性能不再提高时,可以提前停止训练。
具体来说,我们可以设置一个小的阈值,当模型在连续多个迭代周期内,验证集上的误差没有降低到这个阈值以下时,我们就认为模型已经收敛,可以停止训练。
这样既可以节省训练时间,也可以降低不必要的成本。
3. 防止过拟合
过拟合是机器学习中的一个常见问题,指的是模型在训练集上表现很好,但在测试集上表现较差的现象。这是因为模型过于复杂,学习到了训练集中的一些噪声和异常数据。
为了解决这个问题,我们可以使用验证集来监控模型在训练过程中的性能。
如果发现模型在训练集上的表现越来越好,但在验证集上的表现越来越差,那么我们可以考虑减少模型的复杂度或者增加正则化项,以防止过拟合的发生。
在训练过程中可以得到验证集的Loss或者acc.的曲线,在曲线上就能大致判断发生over-fitting的点,选取在这个点之前的模型的参数作为学习到的参数,能让模型有较好的泛化能力。
可以记录下在每个时间戳(在验证集上测试时)验证集上的performance和模型参数,然后最后再去选取认为最好的模型。这个可以用checkpoint来做。
过拟合:随着模型复杂度和训练Epoch的增加,CNN模型在训练集上的误差降低,但是在测试集上的误差会降低后升高,如下图所示:
这是由于训练过程过于细致,以至于把样本的特有特征也当做识别特征一起训练了,所以在验证集上会表现得较差。
针对过拟合的情况,可以从两个方面来解决(1)模型训练方式,(2)数据集。
1、模型训练方式
- 正则化(Regulation)
- Dropout:随机失活
- 提前停止训练
2、数据集
合理的从给定的数据集中拆分出训练集和验证集,将大大减低模型过拟合的可能,常用的验证集划分的方法有:
- 留出法(Hold-out):按一定比例直接将训练集划分为两部分。
- K折交叉验证(K-flod Cross Validation):将训练集划分为K份,将其中K-1份作为训练集,剩下一份作为验证集,循环K次训练。
- 自助采样(BootStrap):通过有放回的采样方式得到新的训练集和验证集,这样每次的训练集和验证集都是有区别的。这对小数据量的训练较为适用。
4. 对比不同模型结构
我们可以通过对比不同模型结构在验证集上的性能,选择最适合任务的模型结构。
这有助于避免选择过于简单或过于复杂的模型,从而提高模型的实际效果。
比如,比较卷积神经网络(CNN) 和循环神经网络(RNN) 在情感分析任务上的性能。通过观察它们的表现,选择更适合处理文本数据的模型结构。
测试集
当我们调好超参数后,就要开始「最终考试」了。我们通过测试集(Test Dataset)来做最终的评估。
通过测试集的评估,我们会得到一些最终的评估指标,例如:准确率、精确率、召回率、F1等。
扩展阅读:分类模型评估指标:Accuracy、Precision、Recall、F1、ROC曲线、AUC、PR曲线
图解
训练集&测试集 模式
需要注意的是这里C点所在的测试集一定只能用来测试,不能用来训练
训练集&验证集&测试集 模式
Training set & Validation set & Test set
既然有多种模型可选,且每种模型的参数可调。那么我们如何选择最好的模型呢?
——上一节我们提到测试集永远只能用来测试,因此我们将训练集拆成两个部分——训练集 -> 训练集+验证集
在训练集训练模型得到模型参数后,验证集就要对这个在各种超参数下得到的模型进行评估,找到一组最优的超参数。然后将超参数固定,再拿到整个训练集上重新训练模型,反复迭代比较,获取到基本的模型。最后,由测试集评估最终模型的性能
如何合理的划分数据集?
下面的数据集划分方式主要针对「留出法」的验证方式,除此之外还有其他的交叉验证法,详情见下文 — — 交叉验证法。
数据划分的方法并没有明确的规定,不过可以参考3个原则:
1、对于小规模样本集(几万量级),常用的分配比例是 60% 训练集、20% 验证集、20% 测试集。
2、对于大规模样本集(百万级以上),只要验证集和测试集的数量足够即可,例如有 100w 条数据,那么留 1w 验证集,1w 测试集即可。1000w 的数据,同样留 1w 验证集和 1w 测试集。
3、超参数越少,或者超参数很容易调整,那么可以减少验证集的比例,更多的分配给训练集。
交叉验证法
为什么要用交叉验证法?
假如我们教小朋友学加法:1个苹果+1个苹果=2个苹果
当我们再测试的时候,会问:1个香蕉+1个香蕉=几个香蕉?
如果小朋友知道「2个香蕉」,并且换成其他东西也没有问题,那么我们认为小朋友学习会了「1+1=2」这个知识点。
如果小朋友只知道「1个苹果+1个苹果=2个苹果」,但是换成其他东西就不会了,那么我们就不能说小朋友学会了「1+1=2」这个知识点。
评估模型是否学会了「某项技能」时,也需要用新的数据来评估,而不是用训练集里的数据来评估。这种「训练集」和「测试集」完全不同的验证方法就是交叉验证法。
3 种主流的交叉验证法
留出法(Holdout cross validation)
上文提到的,按照固定比例将数据集静态的划分为训练集、验证集、测试集的方式就是留出法。
留一法(Leave one out cross validation)
每次的测试集都只有一个样本,要进行 m 次训练和预测。 这个方法用于训练的数据只比整体数据集少了一个样本,因此最接近原始样本的分布。但是训练复杂度增加了,因为模型的数量与原始数据样本数量相同。 一般在数据缺乏时使用。
k 折交叉验证(k-fold cross validation)
静态的「留出法」对数据的划分方式比较敏感,有可能不同的划分方式得到了不同的模型。「k 折交叉验证」是一种动态验证的方式,这种方式可以降低数据划分带来的影响。具体步骤如下:
1、将数据集分为训练集和测试集,将测试集放在一边
2、将训练集分为 k 份
3、每次使用 k 份中的 1 份作为验证集,其他全部作为训练集。
4、通过 k 次训练后,我们得到了 k 个不同的模型。
5、评估 k 个模型的效果,从中挑选效果最好的超参数
6、使用最优的超参数,然后将 k 份数据全部作为训练集重新训练模型,得到最终模型。
k 一般取 10。数据量小的时候,k 可以设大一点,这样训练集占整体比例就比较大,不过同时训练的模型个数也增多。 数据量大的时候,k 可以设小一点。
PyTorch中的 训练和验证
首先应该设置模型的状态:如果是训练状态,那么模型的参数应该支持反向传播的修改;如果是验证/测试状态,则不应该修改模型参数。在PyTorch中,模型的状态设置非常简便,如下的两个操作二选一即可:
model.train() # 训练状态
model.eval() # 验证/测试状态
# 构造训练集
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=10,
shuffle=True,
num_workers=10, )
# 构造验证集
val_loader = torch.utils.data.DataLoader(
val_dataset,
batch_size=10,
shuffle=False,
num_workers=10, )
model = SVHN_Model1() # 加载模型
criterion = nn.CrossEntropyLoss (size_average=False) # 损失函数
optimizer = torch.optim.Adam(model.parameters(), 0.001) # 优化器
best_loss = 1000.0 # 最优损失
for epoch in range(20):
print('Epoch: ', epoch)
train(train_loader, model, criterion, optimizer, epoch)
val_loss = validate(val_loader, model, criterion)
# 记录下验证集精度
if val_loss < best_loss:
best_loss = val_loss
torch.save(model.state_dict(), './model.pt')
def train(train_loader, model, criterion, optimizer, epoch):
# 切换模型为训练模式
model.train()
for i, (input, target) in enumerate(train_loader):
c0, c1, c2, c3, c4, c5 = model(data[0])
loss = criterion(c0, data[1][:, 0]) + \
criterion(c1, data[1][:, 1]) + \
criterion(c2, data[1][:, 2]) + \
criterion(c3, data[1][:, 3]) + \
criterion(c4, data[1][:, 4]) + \
criterion(c5, data[1][:, 5])
loss /= 6
optimizer.zero_grad()
loss.backward()
optimizer.step()
验证/测试的流程基本与训练过程一致,不同点在于:
- 需要预先设置torch.no_grad,以及将model调至eval模式
- 不需要将优化器的梯度置零
- 不需要将loss反向回传到网络
- 不需要更新optimizer
def validate(val_loader, model, criterion):
# 切换模型为预测模型
model.eval()
val_loss = []
# 不记录模型梯度信息
with torch.no_grad():
for i, (input, target) in enumerate(val_loader):
c0, c1, c2, c3, c4, c5 = model(data[0])
loss = criterion(c0, data[1][:, 0]) + \
criterion(c1, data[1][:, 1]) + \
criterion(c2, data[1][:, 2]) + \
criterion(c3, data[1][:, 3]) + \
criterion(c4, data[1][:, 4]) + \
criterion(c5, data[1][:, 5])
loss /= 6
val_loss.append(loss.item())
return np.mean(val_loss)
保存模型
# 模型保存到 ./model.pt文件下
torch.save(model_object.state_dict(), 'model.pt')
# 加载模型 ./model.pt
model.load_state_dict(torch.load(' model.pt'))
# 或
checkpoint = torch.load( 'best_model.pt' )
model.load_state_dict(checkpoint)
模型训练通常需要花很久的时间,有时候会遇到被断电或各种中止程式的状况,所以随时存档是件重要的事,这样就能接续训练。但如果要接续训练,存档就不能只存模型,要把优化器一起储存,因为它会记录一些训练资讯(像是动量之类的值)。要同时存模型和优化器,可以用「字典」的方式存,简单来讲就是用大括号:{ }。范例如下:
torch.save({ 'model_state_dict' : model.state_dict(),
'optimizer_state_dict' : optimizer.state_dict()
}, 'best_model.pt' )
可以看到模型参数model.state_dict()存在’model_state_dict’标签下;优化器参数optimizer.state_dict()存在’optimizer_state_dict’标签下,标签名称可以随便设定没关系,要存多一点资讯进去也行,像是epoch、训练误差…等。
要读取模型和优化器,可以执行下面的程式:
checkpoint = torch.load( 'best_model.pt' )
model.load_state_dict(checkpoint[ 'model_state_dict' ])
optimizer.load_state_dict(checkpoint[ 'optimizer_state_dict' ])
回到刚刚的问题,储存最佳模型。我会先设定一个变数best_loss,用来记录最低的验证误差,我会把值设定的很大(如:999999),如果计算出来的误差比这个值更低,去把误差值取代,并存下模型,然后一直重复。这样存下来的模型就是验证误差最小的模型。程式码如下:
if val_loss < best_loss:
best_loss = val_loss
torch.save({ 'epoch' : epoch,
'model_state_dict' : model.state_dict(),
'optimizer_state_dict' : optimizer.state_dict()
}, 'best_model.pt' )
PyTorch中使用正则化项
L2 Regularization
若使用L2正则化项:
只要直接在训练前为optimizer设置正则化项的λ \lambdaλ参数(这里不叫Regularization而是用了Weight Decay这个叫法):
optimizer = optim.SGD(net.parameters(), lr=learning_rate, weight_decay=0.01)
正则化项目是用来克服over-fitting的,如果网络本身就没有发生over-fitting,那么设置了正则化项势必会导致网络的表达能力不足,引起网络的performance变差。
L1 Regularization
若使用L1正则化项,即对所有参数绝对值求和再乘以一个系数:
在PyTorch中还没有直接设置L1范数的方法,可以在训练时Loss做BP之前(也就是.backward()之前)手动为Loss加上L1范数:
# 为Loss添加L1正则化项
L1_reg = 0
for param in net.parameters():
L1_reg += torch.sum(torch.abs(param))
loss += 0.001 * L1_reg # lambda=0.001
Reference
【小萌五分钟】机器学习 | 数据集的划分(一): 训练集及测试集
一文看懂 AI 数据集:训练集、验证集、测试集(附:分割方法+交叉验证)
Pytorch实现模型训练与验证
深入浅出PyTorch
训练、验证、存档