深度学习—从入门到放弃(五)正则化

1.正则化引入

在说到正则化的概念之前,我们先来回想一下我们花费力气建立神经网络的目的是什么?我们之所以建立模型,那么肯定是希望它能处理真实世界里的数据,换言之,也就是网络的泛化。那么为了达到良好的泛化效果,我们究竟要让模型训练时拟合效果达到哪种程度呢?

1.1 过拟合

过拟合是一个普遍存在的问题,尤其是在神经网络领域,神经网络模型动辄都有上万个参数,现代的深度网络参数则更是上百万的参数,所以深度网络更容易出现过拟合现象。
在这里插入图片描述

  • 存在过拟合现象的网络通常无法良好的泛化,这是因为它可能把数据噪声也一并进行了拟合。

  • 过拟合现象主要体现在accuracy rate和cost两方面:

    1.模型在测试集上的准确率趋于饱和而训练集上的cost仍处于下降趋势

    2.训练集数据的cost趋于下降但测试集数据的cost却趋于饱和或上升

1.2 欠拟合

欠拟合则与过拟合正好相反,过于简单的模型可能不足以概括模型的所有特征。
在这里插入图片描述
因此我们需要在尽量减少过拟合,欠拟合的可能性下选择合适复杂度的模型。

  • 过拟合会导致在不同数据集上拟合出完全不同的模型
  • 欠拟合会导致在不同数据集上拟合出完全相同的模型
    在这里插入图片描述

2.案例分析

在固定训练集大小和固定模型复杂度的情况下解决过拟合问题就是今天正则化所要讨论的内容。

理解正则化的一种方法是根据模型的整体权重的大小来考虑。具有大权重的模型可以完美地拟合更多数据,但有可能出现过拟合;而具有较小权重的模型往往在训练集上表现不佳,但在测试集上却表现出色,可能出现欠拟合。

  • 在这里我们使用Frobenius 范数来进行模型整体权重的度量。(m×n 的矩阵 A 元素的绝对平方和的平方根)

2.1 可视化过拟合

2.1.1 数据准备

set_seed(seed=SEED)

# creating train data
# input
X = torch.rand((10, 1))
# output
Y = 2*X + 2*torch.empty((X.shape[0], 1)).normal_(mean=0, std=1)  # adding small error in the data

#visualizing trian data
plt.figure(figsize=(8, 6))
plt.scatter(X.numpy(),Y.numpy())
plt.xlabel('input (x)')
plt.ylabel('output(y)')
plt.title('toy dataset')
plt.show()

#creating test dataset
X_test = torch.linspace(0, 1, 40)
X_test = X_test.reshape((40, 1, 1))

2.1.2 构建网络

class Net(nn.Module):
  def __init__(self):
    super(Net, self).__init__()
    #构建了3个全连接层(下一讲CNN中会详细说到)
    self.fc1 = nn.Linear(1, 300)
    self.fc2 = nn.Linear(300, 500)
    self.fc3 = nn.Linear(500, 1)

  def forward(self, x):
    x = F.leaky_relu(self.fc1(x))#激活函数为leaky relu
    x = F.leaky_relu(self.fc2(x))
    output = self.fc3(x)
    return output

2.1.3 训练网络

set_seed(seed=SEED)

# train the network on toy dataset
model = Net()

criterion = nn.MSELoss()#这里使用均方误差作为目标函数
optimizer = optim.Adam(model.parameters(), lr=1e-4)#优化器选择Adam

iters = 0
# Calculates frobenius before training
normi, wsi, label = calculate_frobenius_norm(model)
set_seed(seed=SEED)
# initializing variables

# losses
train_loss = []
test_loss = []
# model norm
model_norm = []
# Initializing variables to store weights
norm_per_layer = []

max_epochs = 10000 #训练事件

running_predictions = np.empty((40, int(max_epochs / 500 + 1)))

for epoch in tqdm(range(max_epochs)):
  # frobenius norm per epoch
  norm, pl, layer_names = calculate_frobenius_norm(model)

  # 训练
  model_norm.append(norm)
  norm_per_layer.append(pl)
  model.train()
  # 针对目标函数的梯度下降
  optimizer.zero_grad()#初始化
  predictions = model(X)
  loss = criterion(predictions, Y)#应用目标函数求loss
  loss.backward()#反向传播
  optimizer.step()#梯度下降

  train_loss.append(loss.data)
  model.eval()
  Y_test = model(X_test)
  loss = criterion(Y_test, 2*X_test)
  test_loss.append(loss.data)

  if (epoch % 500 == 0 or epoch == max_epochs - 1):
    running_predictions[:, iters] = Y_test[:, 0, 0].detach().numpy()
    iters += 1

在这里插入图片描述
这里就可以看到我们的神经网络在训练过程中呈现了一个较为明显的过拟合趋势(训练集数据的loss趋于下降但测试集数据的loss却趋于上升)。
在这里插入图片描述
Frobenius 范数随着训练事件增加也趋于不断上升,说明模型整体权重过大,那么我们该如何在训练过程中得知目前的神经网络是处于哪种状态呢?

答案就是在进行训练集和测试集划分时增添一个验证集!我们在每一epoch训练之后计算验证集的预测准确度,而验证集准确度会在模型过度拟合之前达到峰值,因为一旦出现过拟合,模型的泛化能力就会大打折扣,相对的在训练集外的数据集上模型的表现就会变差。

2.1 提前终止 early stopping

现在我们已经确定验证准确度在模型过度拟合之前就达到了峰值,我们希望以某种方式提前停止训练。换句话说当验证准确率停止增加时,我们的模型就在当前epoch上停止了训练。
以下是提前终止这一方法的代码实现:

def early_stopping_main(args, model, train_loader, val_loader):
  device = args['device']
  model = model.to(device)
  optimizer = optim.SGD(model.parameters(),
                        lr=args['lr'],
                        momentum=args['momentum'])#这里使用随机梯度下降里的momentum方法

  best_acc = 0.0#初始化测试集准确度峰值对应epoch
  best_epoch = 0#初始化停止epoch

  # 在提前终止后还需训练的epoch数量
  patience = 20

  # 等到val_acc < best_acc时的epoch
  wait = 0

  val_acc_list, train_acc_list = [], []
  for epoch in tqdm(range(args['epochs'])):

    # 训练模型
    trained_model = train(args, model, train_loader, optimizer)

    # 计算测试集准确度
    train_acc = test(trained_model, train_loader, device=device)

    # 计算验证集准确度
    val_acc = test(trained_model, val_loader, device=device)

    if (val_acc > best_acc):#验证集准确度持续增加时(模型还未过拟合)
      best_acc = val_acc
      best_epoch = epoch
      best_model = copy.deepcopy(trained_model)
      wait = 0
    else:#模型过拟合时
      wait += 1

    if (wait > patience):
      print(f'early stopped on epoch: {epoch}')
      break

    train_acc_list.append(train_acc)
    val_acc_list.append(val_acc)

  return val_acc_list, train_acc_list, best_model, best_epoch

# 设置参数
args = {
    'epochs': 200,
    'lr': 5e-4,
    'momentum': 0.99,
    'device': DEVICE
}

# 初始化模型
set_seed(seed=SEED)
model = AnimalNet()

val_acc_earlystop, train_acc_earlystop, best_model, best_epoch = early_stopping_main(args, model, train_loader, val_loader)
print(f'Maximum Validation Accuracy is reached at epoch: {best_epoch:2d}')
with plt.xkcd():
  early_stop_plot(train_acc_earlystop, val_acc_earlystop, best_epoch)

这里实现了在验证准确率停止增加后的20个epochs时模型停止训练的目标,这里设置patience = 20是因为我们无法确定验证集准确度的变化曲线是一个凸函数,它仍然存在有局部最大值的可能性,所以我们为了减小这种可能性的发生将提前终止延迟到了20个val_acc < best_acc的epoch之后。
在这里插入图片描述
从这里不难发现我们目前想到的解决办法还是有所欠缺,那么该如何从根本,也就是缩小权值这个角度入手解决过拟合的问题呢?答案就是正则化!

3.L1正则

正则化的一般方法为:在cost function里添加一个惩罚项(penalty)来进行更新,从而使模型整体权重更小,提供更简单的模型,不会过度拟合。

而我们这里提到的L1正则的惩罚项则是深度学习网络中所有权重的绝对值之和,形成以下损失函数:L通常指交叉熵损失, λ \lambda λ为惩罚项超参数
在这里插入图片描述
我们都知道在神经网络里我们需要对损失函数进行梯度下降从而找到最佳参数,所以为了更好的了解L1正则是如何缩小权重的,我们来看一下梯度下降后的权值更新规则:
对上面的等式取导数,我们得到:
在这里插入图片描述
在这里sgn就相当于求绝对值。从这个梯度下降后的权值更新规则中我们可以看出L1正则化却是使权值通过减去一个常数,因此权重是有可能被削减为0的,也就达到了降低模型复杂度的目的。

  • L1正则的惩罚项是深度学习网络中所有权重的绝对值之和
  • L1正则可能会让某些权重归零
def l1_reg(model):
  """
    Inputs: Pytorch model
    This function calculates the l1 norm of the all the tensors in the model
  """
  l1 = 0.0

  for param in model.parameters():
    l1 += torch.sum(torch.abs(param))

  return l1
# Set the arguments
args1 = {
    'test_batch_size': 1000,
    'epochs': 150,
    'lr': 5e-3,
    'momentum': 0.99,
    'device': DEVICE,
    'lambda1': 0.001  # penalty超参数lambda
}

# intialize the model
set_seed(seed=SEED)
model = AnimalNet()

# Train the model
val_acc_l1reg, train_acc_l1reg, param_norm_l1reg, _ = main(args1,
                                                           model,
                                                           reg_train_loader,
                                                           reg_val_loader,
                                                           img_test_dataset,
                                                           reg_function1=l1_reg)

4.L2正则

L1正则和L2正则的不同点就在于它们的惩罚项和压缩权重的方式不同。L2正则的惩罚项是深度学习网络中所有权重的平方和。 L2正则也称权值衰减。
在这里插入图片描述
同样我们来看一下梯度下降后的权值更新规则:
对上面的等式取导数,我们得到:
在这里插入图片描述
从这个梯度下降后的权重更新规则中我们可以看出L2正则化使权值乘上一个系数,换句话说也就是给权值打(1-2 η λ \eta\lambda ηλ)折,从而压缩权值,达到减小模型复杂度的目的。并且这种削减对于原本就比较大的权值作用会更加明显(权值越大打折后变越小)。

  • L2正则的惩罚项是深度学习网络中所有权重的平方和
  • L2正则可以较为明显的削减那些很大的权值
def l2_reg(model):

  """
    Inputs: Pytorch model
    This function calculates the l2 norm of the all the tensors in the model
  """

  l2 = 0.0
  for param in model.parameters():
    l2 += torch.sum(torch.abs(param)**2)

  return l2

# Set the arguments
args2 = {
    'test_batch_size': 1000,
    'epochs': 150,
    'lr': 5e-3,
    'momentum': 0.99,
    'device': DEVICE,
    'lambda2': 0.001  #penalty超参数lambda
}

# intialize the model
set_seed(seed=SEED)
model = AnimalNet()

# Train the model
val_acc_l2reg, train_acc_l2reg, param_norm_l2reg, model = main(args2,
                                                               model,
                                                               train_loader,
                                                               val_loader,
                                                               img_test_dataset,
                                                               reg_function2=l2_reg)

在这里插入图片描述
在这里我们可以发现在进行L1、L2、L1+L2正则化后模型在测试集上的准确度逐渐升高,说明正则化确实提高了模型的泛化水平!

5.Drop out

Dropout相对于L1,L2而言,则是一种更为暴力的正则化方法。L1,L2根本方式都是在损失函数上增加一个惩罚项,使得模型在训练的过程中对高模型复杂度也进行惩罚,但Dropout是不通过损失而直接对网络本身进行修改。
在这里插入图片描述
在 dropout 中,我们实际上在训练期间丢弃(归零)一些神经元。在整个训练过程中,在每次迭代中,标准 dropout 在计算后续层之前将每层中节点的一部分(通常为 p=1/2)归零。随机选择不同的子集进行 dropout 会在过程中引入噪声并减少过拟合。
以下是应用dropout时对模型进行的更改:

# Network Class - 2D
class NetDropout(nn.Module):
  def __init__(self):
    super(NetDropout, self).__init__()

    self.fc1 = nn.Linear(1, 300)
    self.fc2 = nn.Linear(300, 500)
    self.fc3 = nn.Linear(500, 1)
    # 增加了两个drop out层
    self.dropout1 = nn.Dropout(0.4)#drop out 概率p=0.4
    self.dropout2 = nn.Dropout(0.2)#p=0.2

  def forward(self, x):
    x = F.leaky_relu(self.dropout1(self.fc1(x)))
    x = F.leaky_relu(self.dropout2(self.fc2(x)))
    output = self.fc3(x)
    return output

在这里插入图片描述
从上图我们可以看出drop out确实在一定程度上压缩了权值,那么该如何理解drop out是怎样发挥这种作用的呢?

  • 在优化器这一章中有提到小批次梯度下降的概念,而当我们随机选择不同子集进行 dropout时,也就是在不同的小批次里以概率p随机丢弃(归零)一些神经元,这样做不仅会在训练过程中引入噪声,减小过拟合的可能性;同时我们可以把每一个小批次里的神经网络看成独立的新网络——因为不同网络可能会有不同的过拟合问题,但把它们综合到一起考虑,这种问题可能会被弱化甚至消除

有关正则化的其他讲解

6.数据增强

数据增强通常用于增加训练样本的数量。现在我们将探讨数据增强对正则化的影响。这里的正则化是通过在每个 epoch 之后向训练数据中添加噪声来实现的。

  • Pytorch 的 torchvision 模块提供了一些内置的数据增强技术,我们可以在图像数据集上使用这些技术。我们最常使用的一些技术是:随机裁剪、随机旋转、垂直翻转、水平翻转
    在这里插入图片描述
    在这里插入图片描述

从上图我们可以发现在进行数据增强后模型准确度也大幅上升,不仅如此,模型整体权值也得到缩减。

7.随机梯度下降SGD

说到梯度下降,那么最重要的一个超参数就是学习率。在本节中,我们将看到学习率如何在训练神经网络时充当正则化器。

  • 学习率越小,正则化越少。相应目标函数的收敛也会很慢,无法起到对权值的调整
  • 较大的学习率通过跳过局部最小值并收敛到更优的最小值(可能是全局最小值)来进行正则化,这通常可以更好地泛化。但是请注意参考之前所讲的过大的学习率带来的影响。

8.超参数微调

在这里插入图片描述
欢迎大家关注公众号奇趣多多一起交流!同时也希望大家能够多多点赞评论支持!
在这里插入图片描述
深度学习—从入门到放弃(一)pytorch基础
深度学习—从入门到放弃(二)简单线性神经网络
深度学习—从入门到放弃(三)多层感知器MLP
深度学习—从入门到放弃(四)优化器

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐