这一小节在开始搞神经网络之前,我们先熟悉几个概念,主要还是把模型训练的流程打通。
过拟合和欠拟合
我们在日常的工作中,训练好的模型往往是要去评价它的准确率的,通过此来判断我们的模型是否符合我的要求。
几个可能的方案是,对我们训练使用的数据再输入到训练好的模型中,查看输出的结果是否跟预期的结果是一致的,当然这个在我们的线性模型上跟训练过程没有区别。另外一个比较靠谱的方案是把一部分在训练的时候没有用过的数据放进模型里,看预测结果是否和预期结果一致。
过拟合(overfitting):对于上述两个方案获得的结果,一种情况是在训练用的数据上表现良好,但是对于新数据预测的结果比较差,这时候就是过拟合了,模型学到了训练数据上太多的细节,导致模型的泛化能力变差。
欠拟合(underfitting):另外一个可能的情况是,不光在新数据上表现不好,就在训练数据上表现也不好,这种情况就是欠拟合,连训练数据的特点都没学好。
如下图中画的,左边的模型算是比较好的,中间的模型就是欠拟合,只学到了上半部分数据的特征,而右边那副图就是过拟合。对于处理过拟合和欠拟合问题,有很多解决方案,比如说增加数据,增加迭代轮次,调整参数,增加噪声,随机丢弃等等,这里我们先不纠缠这个问题。
image.png
训练集和验证集
关于上面提到的两份数据,我们就可以称为训练集和验证集,当然有些时候还有一个叫测试集,有时候认为测试集介于训练集和验证集之间,也就是拿训练集去训练模型,使用测试集测试并进行调整,最后用验证集确定最终的效果。在这本书上只写了训练集和验证集,所以我们这里也先按照这个思路来介绍。
image.png
正如上图绘制的那样,在原始数据到来的时候,把它分成两份,一份是训练集,一份是验证集。训练集用来训练模型,当模型迭代到一定程度的时候,我们使用验证集输入到训练好的模型里,评估模型的表现。
torch.randperm方法:将0~n-1(包括0和n-1)随机打乱后获得的数字序列,函数名是random permutation缩写
下面用代码来实现一下
n_samples = t_u.shape[0] #获取样本数量
n_val = int(0.2 * n_samples) #验证集的数量,取全集的20%,这里是2个
shuffled_indices = torch.randperm(n_samples) #打乱顺序
train_indices = shuffled_indices[:-n_val] #训练集位置信息
val_indices = shuffled_indices[-n_val:] #验证集位置信息
train_indices, val_indices
outs:(tensor([2, 5, 9, 8, 6, 1, 4, 3, 7]), tensor([10, 0]))
紧接着是获取训练数据和验证数据
train_t_u = t_u[train_indices]
train_t_c = t_c[train_indices]
val_t_u = t_u[val_indices]
val_t_c = t_c[val_indices]
train_t_un = 0.1 * train_t_u
val_t_un = 0.1 * val_t_u
定义训练方法,这些跟之前的都差不多
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
train_t_c, val_t_c):
for epoch in range(1, n_epochs + 1):
train_t_p = model(train_t_u, *params) # <1>
train_loss = loss_fn(train_t_p, train_t_c)
val_t_p = model(val_t_u, *params) # <1>
val_loss = loss_fn(val_t_p, val_t_c)
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
if epoch <= 3 or epoch % 500 == 0:
print(f"Epoch {epoch}, Training loss {train_loss.item():.4f},"
f" Validation loss {val_loss.item():.4f}")
return params
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)
training_loop(
n_epochs = 3000,
optimizer = optimizer,
params = params,
train_t_u = train_t_un, # <1>
val_t_u = val_t_un, # <1>
train_t_c = train_t_c,
val_t_c = val_t_c)
out:
Epoch 1, Training loss 91.7660, Validation loss 29.0568
Epoch 2, Training loss 43.7766, Validation loss 2.3025
Epoch 3, Training loss 36.0900, Validation loss 3.5195
Epoch 500, Training loss 7.0920, Validation loss 4.6118
Epoch 1000, Training loss 3.4116, Validation loss 4.0901
Epoch 1500, Training loss 2.9273, Validation loss 3.9970
Epoch 2000, Training loss 2.8636, Validation loss 3.9759
Epoch 2500, Training loss 2.8552, Validation loss 3.9700
Epoch 3000, Training loss 2.8541, Validation loss 3.9680
tensor([ 5.4240, -17.2490], requires_grad=True)
从上面的结果可以看到,训练集损失持续下降,验证集损失前期波动比较大,这可能是因为我们的验证集数量太少导致的,不过在500代以后训练损失和验证损失都趋于稳定。
这里作者给出了几个对比训练损失和验证损失的图片,很有意思。其中蓝色实线是训练损失,红色虚线是验证损失。对于图A,训练损失和验证损失随着训练轮次的增长都没啥变化,表明数据并没有提供什么有价值的信息;图B中,随着训练轮次增加,训练损失逐步下降,而验证损失逐步上升,这说明出现了过拟合现象;C图中验证损失和训练损失同步下降,是一种比较理想化的模型效果;D图中验证损失和训练损失也是同步下降,但是训练损失下降幅度更大一些,这种情况显示存在一定的过拟合,但是仍在可以接受的范围内。
image.png
关闭自动求导
在上面的过程中,我们涉及到一个问题,就是对于验证损失计算完以后,我们并没有调用backward(),那是因为我们只想用验证集数据来检查模型效果,而不希望验证集数据影响我们的模型训练,不然的话就相当于验证集数据也加入了训练,那就很难判断模型是否存在过拟合了。就像下图所写的,使用模型预测和计算损失的步骤是一样的,但是只对train_loss进行反向传播。
image.png
因此在验证过程中,我们实际不需要进行自动求导,但是如果我们前面都设置了自动求导怎么办呢,这会带来大量不必要的运算开销。于是PyTorch提供了关闭自动求导的方法,就是使用torch.no_grad()。
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
train_t_c, val_t_c):
for epoch in range(1, n_epochs + 1):
train_t_p = model(train_t_u, *params)
train_loss = loss_fn(train_t_p, train_t_c)
with torch.no_grad(): # 上下文管理器,关闭自动求导
val_t_p = model(val_t_u, *params)
val_loss = loss_fn(val_t_p, val_t_c)
assert val_loss.requires_grad == False
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
这里还有另外一个方式,就是使用set_grad_enabled(),这个方法接收一个bool类型的参数,来设置是否自动求导。
def calc_forward(t_u, t_c, is_train):
with torch.set_grad_enabled(is_train): #这里传入了是否是训练这样一个bool类型来显示当前的前向传播是训练还是验证
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
return loss
今天写的比较短,感觉轻松多了。