在本文中,我们将了解是什么导致神经网络表现不佳,以及我们可以通过可视化梯度和与模型训练相关的其他参数来调试此问题的方法。我们还将讨论梯度消失和梯度爆炸的问题以及克服这些问题的方法。
最后,我们将了解适当的权重初始化为何有用,如何正确进行初始化,并深入研究 dropout 和批量归一化等正则化方法如何影响模型性能。
NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割
1、神经网络的Bug从何而来?
神经网络的错误很难发现,因为:
- 代码永远不会崩溃、引发异常,甚至不会变慢。
- 网络仍在训练,损失仍会下降。
- 值在几个小时后收敛,但结果非常糟糕
如果想更深入地研究这个主题,那么我强烈建议你阅读 Andrej Karparthy 的神经网络训练秘诀。
那么我们如何才能更好地调试我们的神经网络?
在调试神经网络时,没有要遵循的决定性步骤。但这里有一个概念列表,如果正确实施,可以帮助调试你的神经网络。
2、调试模型输入
我们必须了解数据的细微差别 - 数据的类型、存储方式、目标和特征的类别平衡、数据的值尺度一致性等。
数据预处理
我们必须考虑数据预处理并尝试将领域知识融入其中。通常有两种情况会使用数据预处理:
- 数据清理:如果删除数据的某些部分(称为伪影),则可以轻松实现目标任务。
- 数据增强:当我们的训练数据有限时,我们会以多种方式转换每个数据样本以用于训练模型(例如,缩放、移动和旋转图像)。
这篇文章不关注由不良数据预处理引起的问题。
小数据集过拟合问题。
如果我们有一个包含 50-60 个数据样本的小型数据集,模型将很快过度拟合,即损失将在 2-5 个时期内为零。为了克服这个问题,一定要从模型中删除任何正则化。
如果你的模型没有过拟合,可能是因为你的模型架构不正确,或者你的损失选择不正确。也许当你试图进行多类分类时,你的输出层被激活为 sigmoid。这些错误很容易被忽略,从而导致错误。查看我的笔记本,其中演示了这一点。
那么如何避免这样的错误呢?继续阅读。
3、调试模型架构
从小型架构开始
使用花哨的正则化器和调度器可能有点过头了。如果出现错误,调试小型网络会更容易。常见错误包括忘记将张量从一层传递到另一层、输入与输出神经元比率过高等。
尽可能使用预训练模型(权重)
如果你的模型架构建立在标准主干(如 VGG、ResNet、Inception 等)之上,则可以在标准数据集上使用预训练权重 - 如果可以,请在你正在使用的数据集上找到一个。一篇有趣的近期论文 理解医学成像的迁移学习表明,即使使用预训练 ImageNet 模型中的几个早期层也可以提高医学成像模型的训练速度和最终准确性。
因此,你应该使用通用的预训练模型,即使它不属于你正在解决的问题的领域。并不是说本文确实指出,当应用于医学成像时,ImageNet 预训练模型的改进量并不是很大。因此,也没有太多保证可以领先。如需了解更多信息,我建议阅读 Jeremy Howard 的这篇精彩博客文章。
4、调试损失
选择正确的损失函数
首先,确保你针对给定任务使用了正确的损失函数。对于多类分类器,二元损失函数不会帮助提高准确性,因此分类交叉熵是正确的选择。
确定理论损失值
如果你的模型是从随机猜测开始的(即没有预先训练的模型),请检查初始损失是否接近你的预期损失。如果你使用的是交叉熵损失,请检查你的初始损失是否接近真实的概率分布。
你可以在此处获得关于理论损失的值更多建议。
学习率
学习率参数确定每次迭代时向损失函数最小值移动的步长。你可以根据损失函数的陡峭程度或平滑程度调整学习率。但这可能是一个耗时且耗资源的步骤。你能自动找到最佳学习率吗?
Leslie N. Smith 提出了一种非常聪明且简单的方法,可以在短时间内以最少的资源系统地找到学习率。你所需要的只是一个模型和一个训练集。该模型以较小的学习率初始化,并在一批数据上进行训练。保存相关的损失和学习率。然后以线性或指数方式增加学习率,并以此学习率更新模型。重复此过程,直到达到非常高的(最大)学习率。
在此笔记本中,你将在 PyTorch 中找到此方法的实现。我实现了一个 LRfinder 类。方法 range_test
包含上述逻辑。使用 wandb.log()
,我能够记录学习率和相应的损失:
if logwandb:
wandb.log({'lr': lr_schedule.get_lr()[0], 'loss': loss})
使用这个 LRFinder 自动为你的模型找到最佳学习率:
lr_finder = LRFinder(net, optimizer, device)
lr_finder.range_test(trainloader, end_lr=10, num_iter=100, logwandb=True)
现在,你可以转到 W&B 运行页面并找到 LR 曲线的最小值。将其用作学习率并在整个训练集上进行训练:
当学习率太低时,模型无法学习任何东西,并且会保持稳定。当学习率足够大时,它开始学习,你会发现图中突然下降。曲线的最小值就是你正在寻找的最佳学习率。当学习率很高时,损失会爆发,即损失突然增加。
如果你使用 Keras 来构建模型,你可以使用学习率查找器,如 PyImageSearch 在这篇博客中所示。你也可以参考这篇博文来了解 TensorFlow 2.0 中的实现。
5、调试激活函数
消失的梯度
10 年前,由于使用 sigmoid/tanh 激活函数,在训练深度神经网络时出现了一个重大问题:梯度消失。要理解这个问题,读者需要了解前馈和反向传播算法以及基于梯度的优化。我建议你观看这个视频或阅读这个博客,以更好地理解这个问题。
简而言之,在执行反向传播时,会计算相对于每层权重的损失梯度,并且随着我们在网络中不断向后移动,该梯度趋于变小。可以使用微分链式法则计算每层的梯度。由于 sigmoid 的导数在数值上仅在 0-0.25 之间,因此计算出的梯度非常小,因此发生的权重更新可以忽略不计。由于这个问题,模型无法收敛或需要很长时间才能收敛。
假设你正在构建一个不那么传统的神经网络架构。调试这种网络的最简单方法是可视化梯度。如果你使用 Pytorch 构建网络,W&B 会自动绘制每层的梯度。关于这一点可以查看我的笔记本。
你可以在笔记本中找到两个模型,NetwithIssue 和 Net。第一个模型使用 sigmoid 作为每层的激活函数。后者使用 Relu。两个模型中的最后一层都使用 softmax 激活函数:
net = Net().to(device)
optimizer = optim.Adam(net.parameters())
wandb.init(project='pytorchw_b')
wandb.watch(net, log='all')
for epoch in range(10):
train(net, device, trainloader, optimizer, epoch)
test(net, device, testloader, classes)
print('Finished Training')
W&B 为 PyTorch 提供一流的支持。要自动记录梯度并存储网络拓扑,你可以调用 watch 并传入你的 PyTorch 模型。如果你还想记录参数值的直方图,可以将 log='all'
参数传递给 watch 方法。
在此运行中,该模型在 MNIST 手写数据集上训练了 40 个时期。它最终以超过 80% 的训练测试准确率收敛。你可以注意到大多数时期的梯度为零。要查看下面的梯度图,请单击项目中的运行,然后单击“梯度”部分:
- 死ReLU
整流线性单元 (ReLU) 并不是灵丹妙药,因为它们在输入小于零的值时会“死亡”。如果大多数神经元在短时间的训练中死亡,网络的很大一部分可能会停止学习。在这种情况下,请仔细查看你的初始权重,或在权重中添加一个小的初始偏差。如果这不起作用,你可以尝试使用 Maxout、Leaky ReLU 和 ReLU6,如 MobileNetV2 论文中所示。
- 梯度爆炸问题
当后面的层比初始层学习得慢时,就会出现此问题,这与消失梯度问题不同,后者的学习速度比后面的层慢。当我们在层中向后移动时,梯度呈指数增长时,就会出现此问题。实际上,当梯度爆炸时,梯度可能会因为数值溢出而变为 NaN,或者我们可能会在训练损失曲线中看到不规则的振荡。在梯度消失的情况下,权重更新非常小,而在梯度爆炸的情况下,这些更新非常大,因此会错过局部最小值,模型不会收敛。你可以观看此视频以更好地理解此问题,也可以阅读此博客。
让我们尝试在梯度爆炸的情况下可视化梯度。查看此处的笔记本,我故意将权重初始化为 100 的大值,这样它们就会爆炸:
class NetforExplode(nn.Module):
def __init__(self):
super(NetforExplode, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv1.weight.data.fill_(100)
self.conv1.bias.data.fill_(-100)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.conv2.weight.data.fill_(100)
self.conv2.bias.data.fill_(-100)
self.fc1 = nn.Linear(9216, 128)
self.fc1.weight.data.fill_(100)
self.fc1.bias.data.fill_(-100)
self.fc2 = nn.Linear(128, 10)
self.fc2.weight.data.fill_(100)
self.fc2.bias.data.fill_(-100)
请注意,下图中的梯度如何呈指数增加。conv1 的梯度值约为 10^7,而 conv2 的梯度值约为 10^5。权重初始化不当可能是导致此问题的原因之一。
在基于 CNN 的架构中,通常不会遇到梯度爆炸。对于RNN 来说,梯度爆炸更是一个问题。查看此线程以了解更多见解。由于梯度爆炸导致的数值不稳定性,你可能会得到 NaN 作为损失。此笔记本演示了此问题。
有两种简单的方法可以解决这个问题。它们是:梯度缩放、梯度剪裁。
我在链接的笔记本中使用梯度剪裁来解决这个问题。梯度剪裁将“剪裁”梯度或将它们限制为阈值,以防止梯度变得太大。在 PyTorch 中,你可以用一行代码完成此操作:
torch.nn.utils.clip_grad_norm_(model.parameters(), 4.0)
此处 4.0 为阈值。此值适用于我的演示用例。查看笔记本中的 trainModified
函数以查看实现。
6、调试权重初始化
这是训练神经网络最重要的方面之一。图像分类、情绪分析或下围棋等问题无法使用确定性算法解决。你需要非确定性(non-deterministic)算法来解决此类问题。这些算法在执行算法期间做出决策时使用随机性元素。这些算法谨慎使用随机性。人工神经网络使用称为随机梯度下降的随机优化算法进行训练。训练神经网络只是对“好”解决方案的非确定性搜索。
随着搜索过程(训练)的展开,我们有可能陷入搜索空间的不利区域。陷入困境并返回“不太好”的解决方案的想法称为陷入局部最优。有时消失/爆炸梯度会阻止网络学习。
为了抵消这种作用,初始化是将谨慎的随机性引入搜索问题的一种方法。这种随机性是在开始时引入的。使用 shuffle=True
进行训练的小批量是另一种在搜索过程中引入随机性的方法。要更清楚地了解底层概念,请查看此博客。
良好的初始化有很多好处。它有助于网络实现基于梯度的优化算法的全局最小值(只是拼图的一小部分)。它可以防止消失/爆炸梯度问题。良好的初始化也可以加快训练时间。这篇博客很好地解释了权重初始化背后的基本思想。
初始化方法的选择取决于你的激活函数。要了解有关初始化的更多信息,请查看这篇文章:
- 使用 ReLU 或 leaky RELU 时,使用 He 初始化,也称为 Kaiming 初始化。
- 使用 SELU 或 ELU 时,使用 LeCun 初始化。
- 使用 Softmax 或 Tanh 时,使用 Glorot 初始化,也称为 Xavier 初始化。
大多数初始化方法都具有均匀和正态分布风格。查看此 PyTorch 文档了解更多信息。
查看我的笔记本,了解如何在 Pytorch 中初始化权重:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 32, 3, 1)
torch.nn.init.kaiming_uniform_(self.conv1.weight, mode='fan_in', nonlinearity='relu')
self.conv2 = nn.Conv2d(32, 32, 3, 1)
torch.nn.init.kaiming_uniform_(self.conv2.weight, mode='fan_in', nonlinearity='relu')
self.conv3 = nn.Conv2d(32, 64, 3, 1)
torch.nn.init.kaiming_uniform_(self.conv3.weight, mode='fan_in', nonlinearity='relu')
self.conv4 = nn.Conv2d(64, 64, 3, 1)
torch.nn.init.kaiming_uniform_(self.conv4.weight, mode='fan_in', nonlinearity='relu')
self.pool1 = torch.nn.MaxPool2d(2)
self.pool2 = torch.nn.MaxPool2d(2)
self.fc1 = nn.Linear(1600, 512)
self.fc2 = nn.Linear(512, 128)
self.fc3 = nn.Linear(128, 10)
7、调试正则化
Dropout
Dropout 是一种正则化技术,它随机“丢弃”或“停用”神经网络中的一些神经元,以避免过度拟合的问题。在训练期间,应用 dropout 的层中的一些神经元被“关闭”。
具有较少参数(更简单的模型)的神经网络集合可以减少过度拟合。与网络快照集合相反,Dropout 模拟了这种现象,而无需额外的计算成本来训练和维护多个模型。它将噪声引入神经网络,迫使它学会很好地泛化以处理噪声。
让我们实现 dropout 并看看它如何影响模型性能。查看我的笔记本,了解如何在 Pytorch 中使用批量标准化和 Dropout。我从一个基础模型开始为这项研究设定基准。实现的架构很简单,导致过度拟合。
请注意,在下面的图中,运行 base_model
测试损失最终如何增加。然后,我在 Conv 块之后应用了 Dropout 层,丢弃率为 0.5。要在 PyTorch 中初始化此层,只需调用 torch.nn 的 Dropout 方法即可:
self.drop = torch.nn.Dropout()
Dropout 可防止过度拟合(在下图中查找 dropout_model
运行),但模型没有像预期的那样快速收敛。这意味着集成网络需要更长的时间来学习。在 dropout 的背景下,并非每个神经元在学习时都可用:
- 批标准化
批标准化(Batch Normalization)是一种改进优化的技术。在训练输入数据之前对其进行标准化是一种很好的做法,这可以防止学习算法震荡。我们可以说一层的输出是下一层的输入。如果可以在将此输出用作输入之前对其进行标准化,则可以稳定学习过程。这大大减少了训练深度网络所需的训练次数。批标准化使标准化成为模型架构的一部分,并在训练时对小批量执行。批量标准化还允许使用更高的学习率,让我们对初始化不那么谨慎。
要在 PyTorch 中初始化此层,只需调用 torch.nn 的 BatchNorm2d 方法:
self.bn = torch.nn.BatchNorm2d(32)
批标准化花费较少的步骤来收敛模型(查看下图中的运行 batch_norm)。由于模型很简单,无法避免过度拟合。
现在让我们将这两个层一起使用。如果你同时使用 BN 和 Dropout,请遵循此顺序。有关更多见解,请查看这篇论文。
CONV/FC -> BatchNorm -> ReLu(or other activation) -> Dropout -> CONV/FC
请注意,在下面的运行 bn_drop 中,通过同时使用 Dropout 和批量标准化,可以消除过度拟合,同时模型收敛得更快。
当你拥有大型数据集时,优化很重要,而正则化则不那么重要,因此批标准化对于大型数据集更为重要。当然,你可以同时使用批量标准化和 dropout,尽管批量标准化也可以充当正则化器,在某些情况下可以消除对 Dropout 的需求:
原文链接:神经网络梯度可视化 - BimAnt