在维基百科的词条中,正则化就是让答案变得更简单的,目的是防止过拟合。在分类上可以分为显式正则化(对目标函数添加先验、惩罚、约束、成本)和隐式正则化(所有其他形式的正则化如提前停止、集成学习、Dropout 等)。
在深度学习中最为常用的是 Dropout 方法。
提高模型泛化性能的各种方法,这里我们主要介绍数据增强和正则化的方法。
首先是数据增强,使用 imgaug 库对图片进行数据增强的方法可以看我的这篇博客,更为简便的,是直接在 PyTorch 使用 torchvision 中的 transforms 模块,详细用法可以看说明文档和各种变换的效果展示。下面的代码展示了如何用 transforms 的 Compose 组合需要进行的各种变换,然后输入到 datasets 最后再送入 DataLoader 中。
training_transforms = torchvision.transforms.Compose([torchvision.transforms.Resize(size=(32, 32)),torchvision.transforms.RandomCrop(size=(28, 28)),torchvision.transforms.RandomRotation(degrees=30, interpolation=PIL.Image.BILINEAR),torchvision.transforms.ToTensor(),torchvision.transforms.Normalize(mean=(0.5,), std=(0.5,)),# normalize does (x_i - mean) / std# if images are [0, 1], they will be [-1, 1] afterwards
])test_transforms = torchvision.transforms.Compose([torchvision.transforms.ToTensor(),torchvision.transforms.Resize(size=(32, 32)),torchvision.transforms.CenterCrop(size=(28, 28)),torchvision.transforms.Normalize(mean=(0.5,), std=(0.5,)),
])train_dataset = datasets.MNIST(root='data', train=True, transform=training_transforms,download=True)test_dataset = datasets.MNIST(root='data', train=False, transform=test_transforms)train_loader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)test_loader = DataLoader(dataset=test_dataset, batch_size=BATCH_SIZE, shuffle=False)
然后是提前停止,思路很简单,将数据集划分为训练集、验证集和测试集,使用验证集来进行模型选择,在这里就相当于是训练轮数 Epochs 的超参数调整,选择模型在验证集上的性能顶点作为最优的训练轮数。但是这个方法不常用,因为有论文指出模型的损失存在双重下降的现象,即先下降后上升然后又一次下降,对于大模型而言这种现象更为明显,如下面的两张图所示:
使用 L1 / L2 范数的正则化,常用于线性回归,L1 正则化对应的是 LASSO 回归,L2 正则化对应的是岭回归。
Logistic Regression 中的 L2 正则化,损失函数 L 就是二元交叉熵,则目标函数就是损失函数 + L2 正则化项,L2 正则化项就是对参数 w 求平方和,前面再乘以一个超参数 λ(越大正则化程度越高)。
在几何上理解 L2 正则化,如图所示,两条轴是两个参数(特征)的值,其中椭圆等高线(红色)代表线性回归的损失函数(MSE),它的优化目标就是下降到红色点;圆形(紫色)代表正则化(惩罚)项,它的优化目标就是下降到紫色点。这两者加起来就是带 L2 正则化的线性回归,两者折衷的目标就是下降到绿色点。
L2 正则化应用到神经网络中,就是给损失函数加上对于每层参数求取的平方和。
在梯度下降中也就只是加上正则化项的偏导数,利用 PyTorch 自己实现 L2 正则化的方法就是写两行公式,如果用 SGD 直接提供超参数 λ 即可。
最后是 Dropout 方法,实际上就是随机地丢弃隐藏层中的神经元,默认概率为 0.5
丢弃的实现方法,就是对于每一层中的各个神经元,假设有 5 个神经元,则进行 5 次的伯努利抽样,得到的值都是 0 到 1,然后抽样值小于 p 的就变为 0,大于 p 的就变为 1,再用它乘上神经元对应的输出(激活值)即可。
Dropout 只在训练时使用,在测试(推理)时不使用,因为不想得到随机的测试结果。还有就是 Dropout 随机丢弃某一层的神经元,则后面的神经元的输入会偏小,例如 2;而在测试时是没有 Dropout 的,此时后面神经元的输入会变大,例如 4,所以应该在测试时给神经元的输出(激活值)乘上一个系数 1 - p,让后面神经元的输入范围相同。
对 Dropout 的一种解释,就是它可以让模型不依赖于某些特定的神经元,使得更多的神经元可以被利用到。Dropout 的超参数只有一个,就是概率,在不同层进行 Dropout 可以取不同的概率。
另一种解释,Dropout 相当于一种集成学习的方法,假设有 h 个隐藏层的节点,则每个节点是否丢弃就有 2 种可能,即共有 2h{2^h}2h 个模型,但这些模型是受到约束的(前一个模型更新后的权重会传给下一个模型)。在测试(推理)时如果使用所有这些模型的预测平均值,就可以得到很好的结果,但是代价很大。
因为预测输出大多为概率,所以取的是几何平均数,即相乘再开方。而 Dropout 中使用最后一个模型并且缩放 1 - p 的做法,就是对这个几何平均数的一种近似。
目前在大多数深度学习框架中,实现的是 Inverted Dropout,实际上就只是把在推理阶段乘以系数 1 - p 的过程,变为在训练阶段乘以系数 11−p{1 \over {1 - p}}1−p1,这样做是因为推理的次数远多于训练,要减少推理过程的计算消耗。
PyTorch 实现 Dropout 的代码,实际上就是在激活函数后加上 torch.nn.Dropout
,对于 ReLU 这个激活函数来说,在它之前或者之后用 Dropout 都是一样的,因为它输入 0 时输出也为 0,但是对于 Sigmoid 这样的激活函数,如果在它之前用 Dropout,即使输入变为 0,其输出也是 0.5 不等于 0
必须养成习惯,明确模型是处于 train 模式还是 eval 模式,Dropout 只在训练模式下生效。
如果模型没有过拟合,就不需要使用 Dropout,反而应该提高模型的复杂度,使其过拟合之后,再使用 Dropout,这样的效果会优于直接使用一个没有过拟合的简单模型。
DropConnect 是 Dropout 的泛化方法,也就是随机丢弃权重,但是实际效果不如 Dropout