【5】过拟合处理的一些技巧

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1.过拟合与欠拟合

  • Underfitting欠拟合的特点:模型的复杂度 < 数据分布的复杂度

1)训练集的准确率不好,loss会比较高 2)测试集的准确率也不好,loss也会比较高

  • Overfitting过拟合的特点:模型的复杂度 > 数据分布的复杂度

1)训练集的准确度比较高,但是测试集的准确度比较低 2)过分的准确降低loss,导致效果不好

由于现在的网络层数都比较大,所以出现过拟合的情况比较多,欠拟合的情况相对会少一点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w9gVrLdg-1617122224856)(attachment:image.png)] 一般来说,如何防止过拟合的一些基本的操作:

1)提供更多的数据

2)降低模型的复杂度 降低神经网络结构的层数 或者 正则化方法

3)Dropout 去掉一部分的神经元

4)Data Enhancement 数据增强

5)Early Stopping对训练过程做一个提前的终结


2.交叉验证

  • train-val-test交叉验证结构的思路: 由于datasets.MNIST函数可以将数据样本划分成数据集与测试集两类,但是这是不够的,可以引入第三类,也就是train-val-test交叉验证结构。可以将样本数量较多的训练集再度的划分为train样本集与val样本集,这样可以使用train样本集来训练神经网络,然后使用val样本集来测试来选择一个较好的参数值,最后test数据集是作为最后的样本得出最终的结果

在这里插入图片描述

  • 参考代码
# 导入需要的模块
import  torch
import  torch.nn as nn
import  torch.nn.functional as F
import  torch.optim as optim
import  torchvision
from    torchvision import transforms,datasets

# 参数的初始化
batch_size=200
learning_rate=0.01
epochs=10

# 训练集下载
train_db = datasets.MNIST('datasets/mnist_data', 
                    train=True, 
                    download=True,
                    transform=transforms.Compose([
                    transforms.ToTensor(),
                    transforms.Normalize((0.1307,), (0.3081,))
]))

# 测试集下载
test_db = datasets.MNIST('datasets/mnist_data', 
                    train=False, 
                    download=True,
                    transform=transforms.Compose([
                    transforms.ToTensor(),
                    transforms.Normalize((0.1307,), (0.3081,))
]))

# 将训练集再度分成两类,train数据集样本数为50k,val数据集的样本数为10k
train_db, val_db = torch.utils.data.random_split(train_db, [50000, 10000])

# 接下来进行导入数据
train_loader = torch.utils.data.DataLoader( train_db, batch_size=batch_size, shuffle=True)
test_loader  = torch.utils.data.DataLoader( test_db,  batch_size=batch_size, shuffle=True)
val_loader   = torch.utils.data.DataLoader( val_db,   batch_size=batch_size, shuffle=True)

# 打印三种数据集的样本数量
print('train:', len(train_db),'val:', len(val_db), 'test:', len(test_db))
# 输出为:train: 50000 val: 10000 test: 10000

# 定义神经网络结构
class MLP(nn.Module):

    def __init__(self):
        super(MLP, self).__init__()

        self.model = nn.Sequential(
            nn.Linear(784, 200),
            nn.LeakyReLU(inplace=True),
            nn.Linear(200, 200),
            nn.LeakyReLU(inplace=True),
            nn.Linear(200, 10),
            nn.LeakyReLU(inplace=True),
        )

    def forward(self, x):
        x = self.model(x)

        return x
    
# 使用GPU来加速计算
device = torch.device('cuda:0')
net = MLP().to(device)

# 设置容器
optimizer = optim.SGD(net.parameters(), lr=learning_rate)
criteon = nn.CrossEntropyLoss().to(device)

# 一次epoch是直接训练全部的样本
for epoch in range(epochs):

    # 对于train数据集,主要的功能是不断的改进神经网络结构
    for batch_idx, (data, target) in enumerate(train_loader):
        data = data.view(-1, 28*28)
        data, target = data.to(device), target.cuda()

        logits = net(data)
        loss = criteon(logits, target)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch_idx % 100 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.item()))

    # 然后使用val数据集来挑选一个精度最高的参数值来进行下一次的epoch训练,主要主要是监视时候出现过拟合的情况
    # 根据val样本集输出的情况,进行人为的选择最低loss指的参数,来作为做好的参数,来给最后的test样本集来做验证
    test_loss = 0
    correct = 0
    for data, target in val_loader:
        data = data.view(-1, 28 * 28)
        data, target = data.to(device), target.cuda()
        logits = net(data)
        test_loss += criteon(logits, target).item()
		
		# data.shape,pred.shape输出为:(torch.Size([200, 784]), torch.Size([200]))
		# 也就是data为200张28*28的图片
        pred = logits.data.max(1)[1]
# 得出每一个数据的预测数字      
# 输出为:
# tensor([3, 2, 7, 0, 2, 9, 3, 5, 0, 1, 8, 8, 1, 8, 5, 0, 8, 0, 5, 9, 1, 9, 4, 8,
#        1, 0, 4, 3, 7, 7, 2, 2, 8, 7, 1, 2, 9, 9, 1, 0, 2, 3, 5, 4, 8, 8, 2, 1,
#        3, 8, 2, 6, 3, 2, 1, 1, 7, 9, 2, 1, 2, 9, 9, 3, 0, 0, 1, 9, 7, 5, 6, 4,
#        5, 3, 8, 3, 1, 6, 0, 1, 6, 1, 1, 2, 6, 6, 5, 3, 4, 9, 9, 5, 8, 3, 1, 5,
#        7, 9, 6, 1, 2, 5, 3, 4, 3, 1, 7, 2, 0, 3, 0, 3, 1, 4, 1, 6, 6, 5, 3, 1,
#        1, 6, 2, 7, 2, 8, 4, 4, 0, 8, 2, 9, 0, 1, 6, 7, 3, 1, 6, 1, 0, 5, 4, 9,
#        9, 3, 3, 0, 9, 8, 9, 7, 8, 1, 7, 6, 4, 0, 1, 8, 4, 2, 4, 2, 1, 1, 3, 4,
#        4, 8, 8, 7, 4, 8, 2, 1, 7, 6, 0, 9, 9, 6, 1, 7, 9, 4, 7, 5, 8, 7, 3, 4,
#        4, 0, 0, 5, 8, 3, 2, 1], device='cuda:0')
        correct += pred.eq(target.data).sum()

    test_loss /= len(val_loader.dataset)
    print('\nVAL set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(val_loader.dataset),
        100. * correct / len(val_loader.dataset)))

# 测试集得出最后的结果,根据上述的输出人为的选择一个最好的参数,去避免神经网络训练次数过多而导致过拟合的情况
# 这部分代码与val数据集的样本是完全一模一样的
test_loss = 0
correct = 0
for data, target in test_loader:
    data = data.view(-1, 28 * 28)
    data, target = data.to(device), target.cuda()
    logits = net(data)
    test_loss += criteon(logits, target).item()

    pred = logits.data.max(1)[1]
    correct += pred.eq(target.data).sum()

test_loss /= len(test_loader.dataset)

# 得出最后一个最终结果
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
    test_loss, correct, len(test_loader.dataset),
    100. * correct / len(test_loader.dataset)))
复制代码

结果输出:

Train Epoch: 0 [0/50000 (0%)]	Loss: 2.306638
Train Epoch: 0 [20000/50000 (40%)]	Loss: 2.004949
Train Epoch: 0 [40000/50000 (80%)]	Loss: 1.304654

VAL set: Average loss: 0.0048, Accuracy: 7999/10000 (80%)

Train Epoch: 1 [0/50000 (0%)]	Loss: 0.950421
Train Epoch: 1 [20000/50000 (40%)]	Loss: 0.750856
Train Epoch: 1 [40000/50000 (80%)]	Loss: 0.486089

VAL set: Average loss: 0.0024, Accuracy: 8725/10000 (87%)

Train Epoch: 2 [0/50000 (0%)]	Loss: 0.551328
Train Epoch: 2 [20000/50000 (40%)]	Loss: 0.393225
Train Epoch: 2 [40000/50000 (80%)]	Loss: 0.364113

VAL set: Average loss: 0.0019, Accuracy: 8911/10000 (89%)

Train Epoch: 3 [0/50000 (0%)]	Loss: 0.349589
Train Epoch: 3 [20000/50000 (40%)]	Loss: 0.392292
Train Epoch: 3 [40000/50000 (80%)]	Loss: 0.436958

VAL set: Average loss: 0.0017, Accuracy: 9017/10000 (90%)

Train Epoch: 4 [0/50000 (0%)]	Loss: 0.258544
Train Epoch: 4 [20000/50000 (40%)]	Loss: 0.338164
Train Epoch: 4 [40000/50000 (80%)]	Loss: 0.359373

VAL set: Average loss: 0.0016, Accuracy: 9062/10000 (91%)

Train Epoch: 5 [0/50000 (0%)]	Loss: 0.273114
Train Epoch: 5 [20000/50000 (40%)]	Loss: 0.253162
Train Epoch: 5 [40000/50000 (80%)]	Loss: 0.311780

VAL set: Average loss: 0.0015, Accuracy: 9127/10000 (91%)

Train Epoch: 6 [0/50000 (0%)]	Loss: 0.250342
Train Epoch: 6 [20000/50000 (40%)]	Loss: 0.315225
Train Epoch: 6 [40000/50000 (80%)]	Loss: 0.349633

VAL set: Average loss: 0.0014, Accuracy: 9168/10000 (92%)

Train Epoch: 7 [0/50000 (0%)]	Loss: 0.279099
Train Epoch: 7 [20000/50000 (40%)]	Loss: 0.223900
Train Epoch: 7 [40000/50000 (80%)]	Loss: 0.316459

VAL set: Average loss: 0.0014, Accuracy: 9201/10000 (92%)

Train Epoch: 8 [0/50000 (0%)]	Loss: 0.349246
Train Epoch: 8 [20000/50000 (40%)]	Loss: 0.241840
Train Epoch: 8 [40000/50000 (80%)]	Loss: 0.193532

VAL set: Average loss: 0.0013, Accuracy: 9231/10000 (92%)

Train Epoch: 9 [0/50000 (0%)]	Loss: 0.238849
Train Epoch: 9 [20000/50000 (40%)]	Loss: 0.193404
Train Epoch: 9 [40000/50000 (80%)]	Loss: 0.204639

VAL set: Average loss: 0.0013, Accuracy: 9251/10000 (93%)


Test set: Average loss: 0.0012, Accuracy: 9305/10000 (93%)
复制代码

由于设置的epoch过小,神经网络没有完全训练好,所以目前loss还是不断地在降低,还没有出现过拟合的情况,所以目前还不需要人为的挑选参数,来给最后的test样本集来进行训练。


3.regularization正则化

问题: 正则化的操作是为了使得参数分布减小,从而避免过拟合的情况,但是为什么参数范数接近于0的时候,其模型的复杂度会减少?

答案: 因为将一个高维函数的高维部分的参数下降为接近于0,所以正则化在另一方面也称为Weight Decay

正则化分为两类:划分的依据是范数的不同 1) L1-regularizationimage

在这里插入图片描述

2) L2-regularization(较常用)

在这里插入图片描述

对于L2-regularization来说,pytorch有直接API接口可以直接使用,比较的方便,而L1-regularization需要人为进行添加。

# L2-regularization的使用参考
device = torch.device('cuda:0')
net = MLP().to(device)
# 其实就是添加了一个参数weight_decay = 0.01
optimizer = optim.SGD(net.parameters(),lr = learning_rate,weight_decay = 0.01)
criteon = nn.CrossEntropyLoss().to(device)
# weight_decay = 0.01的含义:相当于设置了L2-regularization公式中的λ为0.01
# 由于net.parameters()已经得到了神经网络的全部参数,w’=w-▽w,然后使得参数的二范数|w|——>0
复制代码

需要注意的是,正则化的操作是为了避免网络结果过于的复杂所以需要约束其参数不能过大,也就是避免过拟合的情况,如果网络并没有出现过拟合的情况而使用正则化操作会使得训练的效果差很多

# L1-regularization的使用参考
regulatization_loss = 0          # 遍历参数计算出参数的1范数,根据公式也就是模的相加
for param in model.parameters():
    regulatization_loss += torch.sum(torch.abs(param))   # 对参数取绝对值求和
classify_loss = criteon(logits,target)    # 计算出结果的交叉熵
# 交叉熵 + 参数的1范数损失 = 总的loss, 现在降低这个loss就可以改善网络
# 根据上述的公式,0.01就是L1-regularization公式中的λ
# 以下这行代码就是模仿公式的结果
loss = classify_loss + 0.01 * regulatization_loss   

# 基本的三部曲,向后传播更新参数
optimizer.zero_grad()
loss.backward()
optimizer.step()
复制代码

4.动量与学习量的衰减

1)动量

在这里插入图片描述

其中的zk可以理解为上一次梯度更新的方向,增加的动量的机制相当于不仅仅考虑了当前梯度的方向,还考虑了之前的梯度的方向,也就是综合的结合了两次梯度的变化信息来权衡。

  • No momentum

在这里插入图片描述

  • With appr. momentum

在这里插入图片描述 参考代码:

# 方法1
# 同样的,pytorch中的SGD优化器也提供的一个接口,使用起来还是比较的方便
optimizer = torch.optim.SGD(model.parameters().args.lr,
                               momentum = args.momentum,   # 仅仅是添加了了着一个参数
                               weight_decay = args.weight_decay)

# 方法2
# 0.8表示新一轮产生的数据与前一轮产生的数据之间的使用比例,也就是动量操作
nn.BatchNorm2d(128, 0.8),  
# 所以在构造神经网络的时候可以使用BN层来间接使用动量操作
# 参考解释:https://blog.csdn.net/t20134297/article/details/104960101/
复制代码
2)Learing rate decay

在这里插入图片描述

当梯度的长时间保持不变的时候,也有可以是learning rate的问题,有时我们调整了一下学习率就可能让loss跳转,所以可以动态的设置learning rate,训练过程一直保持同样的learning rate有时进行的会是非常的缓慢的

在这里插入图片描述

在这里插入图片描述 参考代码:

# 方法1
# 在SGD容器中设置了正则化操作与动量操作
optimizer = torch.optim.SGD(model.parameters().args.lr,
                               momentum = args.momentum,
                               weight_decay = args.weight_decay)
# ReduceLOonPlateau当loss已经平坦了很长时间之后,可以通过减小learning rate来进一步减小loss
# 接口函数为:class ReduceLROnPlateau(builtins.object)
#  |  ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, 
#                      threshold=0.0001, threshold_mode='rel', cooldown=0, 
#                      min_lr=0, eps=1e-08, verbose=False)
scheduler = ReduceLOonPlateau(optimizer,'min')
# mode='min':表示模式是减小lr
# patience=10:表示如果10个epoch都没有减小,则降低learing rate
# factor=0.1:表示降低learing rate的倍数,lr = factor*lr# 

# 训练过程
for epoch in xrange(args.start_epoch,args.epochs):
    train(train_loader,model,criterion,optimizer,epoch)
    result_avg,loss_val = validate(val_loader,model,criterion,epoch)
    # 每调用一次就监听一次loss,若平淡10次就会做出改变
    scheduler.step(loss_val)
复制代码
# 方法2:
# 此方法不需要监听,而是没经过一定数量的训练次数就直接降低learning rate
# Assuming optimizer uses lr = 0.05 for all groups
# lr = 0.05   if epoch < 30
# lr = 0.005  if 30 <= epoch < 60
# lr = 0.0005 if 60 <= epoch < 90
# ......
scheduler = StepLR(optimizer,step_size = 30,gamma = 0.1)
for epoch in range(100):
    scheduler.step()
    train(...)
    validate(...)
复制代码

5.Early Stopping

思想很简单,就是使用val数据要进行验证,如果其准确度已经开始下降时,可以提前的终止训练,而使用当前认为最后的一组参数,其实这个思想前面的推文也有涉及,其简单的步骤归结为:

1)Validation set to select parameters :val数据集验证来挑选参数

2)Monitor validation perforamce :人工监视来查看性能

3)Stop at the highest val perf:在最高的准确度时停止实验 在这里插入图片描述

6.Dropout

Dropout的思想是去除掉一部分是神经元,也就是通过简化神经网络结构来避免过拟合的情况

  • Learning less to learn better

  • Each connection has p = (0,1) to lose

在这里插入图片描述

其与正则化的思想是有部分类似的,但是正则化的思想是使用2范数迫使参数的复杂度降低|w|->0,既使得参数的总范数接近于0 而Dropout是想不需要使用全部的参数,可以简化一部分也就是断了部分参数之间的输入输出练习 ∑w->0,既使得有效的参数越小越好 Dropout可以是的loss函数变化的比较平和,因为其使得有效的参数量减少了,避免了网络结构学习了噪声对最后的结果造成了干扰

# Dropout的使用方法如下:
net_dropped = torch.nn.Sequential(
    torch.nn.Linear(784,200),
    # 层(784,200)与层(200,200)之间的连线有一定几率断掉
    torch.nn.Dropout(0.5),     # drop 50% of the neuron
    torch.nn.ReLU(),
    torch.nn.Linear(200,200),
    # 层(200,200)与层(200,10)之间的连线有一定几率断掉
    torch.nn.Dropout(0.5),     # drop 50% of the neuron
    torch.nn.ReLU(),
    torch.nnLinear(200,10),
)
# 注意,此代码是使用pytorch的接口,如果是tensorflow是参数意义是相反的
# tf.nn.dropout(keep_prob)
# 代码迁移可能会出现这种情况

# 在训练的时候会使用Dropout,但是在val样本测试的时候需要恢复
# 也就是test的时候全部的连接都会考虑,不会考虑Dropout的这个行为
for epoch in range(epochs):
    net_dropped.train()
    for batch_idx,(data,target) in enumerate(train_loader):
        ...
        # 关键代码,切换,连接全部用上
        net_dropped.eval()
        test_loss = 0
        correct = 0
        for data,target in test_loader:
            ...
# 一定要注意,在val来测试的时候需要加上net_dropped.eval()一句,否则效果会差一点
复制代码

猜你喜欢

转载自juejin.im/post/7096293191065796615