监视ANN训练的更好方法
有时让人工神经网络表现良好可能是一个挑战。你需要够在训练过程中可视化模型内部工作的、能够帮助你快速识别问题根源的工具。

有时让人工神经网络表现良好可能是一个挑战。当训练停滞或失控时,很难确定问题的原因。你需要好的工具。能够揭示潜在问题的工具。能够在训练过程中可视化模型内部工作的工具。能够帮助你快速识别问题根源的工具。
在我学习旅程中最重要的教训之一就是在开发早期阶段就暴露模型内部的工作指标。尽早暴露这些信息可以加快开发周期,通过更早发现问题来实现这一点。
然而,我觉得作为一个社区,我们已经满足于只使用最基础的工具来监控模型的训练进展。在许多情况下,人们使用的只是损失函数和一些领域相关的准确性版本。而仅凭这些,他们试图判断模型架构和训练方案的有效性。
像TensorBoard和Weights & Biases这样的工具有所帮助,但它们可以做得更好。这些工具旨在简化生成和交互标准损失和准确率图表的过程,同时还可以查看模型输出。它们还添加了权重直方图,这是一个进步的方向,但它们提供的分析超出了这一点并不多。它们的主要重点是易用性。这很重要,但不幸的是,它往往是以提供浅显信息为代价的。虽然它们提供了添加自定义插件或数据收集方式的方法,但大多数从业者可能只是在使用开箱即用的功能——因为,还有什么其他东西是你想绘制的呢?
这是系列文章的第一篇,我将在其中回答这个问题——你还想绘制什么?我还将向你介绍一系列有用的指标和值,以帮助你深入了解模型动态,并告诉你如何收集这些数据。我希望能激励你添加一些新的工具到你的工具箱中。这些工具可以帮助你更快地发现和解决问题,给你比你想象中更多的控制权。
例如这个仪表板,它在训练过程中提供了关于模型内部行为的多个方面的概览:

这只是个预告。还有很多背景需要覆盖。希望到这篇文章结束时,我能激励你去创建自己的版本。
1、问题
在深入探讨解决方案之前,让我先详细说明一下当前的问题。
许多问题会影响神经网络模型的训练。从简单的编码错误,到历史上常见的梯度消失和学习率过大,再到更微妙且常常看不见的问题,如神经元死亡。学术文献已经讨论这些问题很长时间了。它也充满了关于如何测量和可视化模型活动以及这些问题影响的想法。常用的技术包括计算和绘制权重或梯度的均值和标准差,计算这些量的欧几里得范数,使用时间上的相关性,使用PCA减少收集的数据以进行二维可视化,数据流聚类等。
这些是非常强大的技术,但并不总是容易使用。也不是每个人都意识到它们或意识到这些技术对于实际、现实世界中的神经网络设计和故障排除有多么有用。
如果你正在使用TensorFlow,你会发现的第一个问题是它没有提供任何开箱即用的方式来获取梯度。你甚至不能使用自定义回调来收集这些信息。相反,你必须创建一个自定义训练循环。如果你正在使用PyTorch,那么编写自定义训练循环更为常见,因此至少第一步更容易。
下一个问题是如何高效地收集你需要的数据,而不显著减慢训练时间,也不耗尽RAM。然后你需要找到一种方法来可视化所有内容。典型的权重数组是一个多维张量,包含成千上万个值。典型的网络有多个这样的权重。
2、解决方案
在这篇文章中,我将描述几种可视化技术,这些技术可以在训练过程中为你提供详细的模型内部工作视图。我还将概述如何收集所需的数据。
为了生成这里展示的可视化结果,我选择了使用matplotlib进行简单绘图。这样可以避免TensorBoard等工具施加的任何渲染限制,并使结果更具可重复性。你可能会想要自己进行实验,以找到在你最喜欢的平台上生成这些可视化的方法。
为了让任何人都能轻松重复这些步骤,我已经将模型内省和可视化打包为工具包,并将其作为git仓库发布。如果你只想快速上手,它可以很容易地插入你的模型训练中。它遵循Callback风格,这对于使用TensorFlow的人来说应该很熟悉。
或者,所有代码都以MIT许可证发布。所以欢迎并鼓励你取走你喜欢的部分,并以此为基础进行自己的实验。我唯一的要求是附上一个带有仓库链接或本文链接的致谢说明。
本文中使用的所有可视化都是使用该工具包创建的。然而,我即将分享的想法和技术独立于工具包,因此在这里不会让你感到厌烦。
让我们开始吧。
3、更好的损失和指标图
在开发神经网络模型时,我们通常会先进行短时间的训练运行,以了解模型是否基本有效。问题是,往往有一些细微的问题在训练时间较长后才会更加明显。当你只有损失曲线这一信息时,这些细微问题在最初可能不可见。但有更好的方法。
为了说明这一点,我针对一个简单的分类问题训练了两个相似的模型。以下是它们在最初几个epoch的损失曲线:

请注意,到目前为止,这两个模型似乎都在正常训练,而且它们之间没有太多差异。如果我要猜测的话,我会说模型B可能略优于模型A。现在,训练6个epoch有些人为设定,但在更现实的场景中这个教训依然成立。让我们看看训练更长时间会发生什么:

现在这两个模型看起来明显不同了。模型B显示出梯度振荡的迹象。这并不奇怪,因为秘密在于这两个模型是相同的,唯一的区别是模型A使用了合适的学习率,而模型B使用的学习率稍微高了一点——还不足以立即引起问题,但足够接近最优解时就会出现问题。
TensorFlow包括开箱即用的支持来收集训练历史记录。它在整个训练过程中记录损失和其他配置的指标,并且很容易用来生成上面的图表。这种日志记录的分辨率是每epoch一次。在每个epoch结束时取一个样本点,通常记录整个epoch期间损失或其他指标的平均值。这在尝试保持简单时是有意义的——构成每个epoch内更新步骤的小批量包含了不同的训练数据子集,因此如果你按每步记录历史记录,那么你会暴露在大量的随机噪声中。如果使用验证集,那么每次运行模型通过验证集也没有意义。但是通过限制我们自己只记录每epoch的数据,我们错过了大量关于模型在训练期间表现的有用信息。 以下图表来自与上述相同的训练运行,但使用了不同的方式来处理每步损失值。第一行显示的是基于每个周期的数据,但表示了百分位分布:中位数(实线)、±25百分位和最小/最大范围。第二行显示的是原始的每步数据。

现在不同学习率的结果变得非常明显。我们在模型A的每步图中看到的小刻度发生在周期边界之间——这是TensorFlow中模型拟合的典型现象,并且是由于它在整个周期内如何收集和平均指标的结果。这导致了一个非常薄的分布(低方差)的每周期图。相比之下,模型B的每步图波动剧烈,反映在每周期图中显示的宽广而嘈杂的分布上。
即使只有上面一行,我们也可以更早地识别潜在问题。为了比较,这里是本节开头的相同6个周期的图表,加上添加了百分位分布的相同图表:

该优势同样适用于你在训练过程中可能计算的其他模型输出指标,例如准确率。
在TensorFlow中,收集每步损失和指标只需要创建自定义版本的History回调即可。示例可以在工具包中找到。
4、权重可视化
根据使用的初始化方案,模型中的权重可能会被初始化为类似以下分布之一:

某些层在整个训练过程中会基本保持其基本分布,而另一些层则会显著偏离。在一个表现良好的模型中,权重将主要在零的两侧保持平衡。但有时情况并非如此,有时获取这些信息是有用的。此外,能够比较各层之间以及随时间变化的权重规模也是有用的。
一种流行的绘制权重和偏置的方法是将多个直方图堆叠在一起,形成一个三维分层表示。最旧的状态位于后面,最新的状态位于前面:

这些看起来确实很漂亮,而且是唯一可以表示动态直方图随时间变化的方式。但它们并不适合所有情况。
当你尝试对梯度使用基本直方图时,会出现一个问题。下图显示了来自不同模型的两个代表性层中权重和梯度的分布。最左边的图表显示了初始化后(使用He-normal初始化)的权重分布,中间的图表显示了训练后的分布,最右边的图表显示了训练结束时的梯度分布:

请注意,权重可以保留正态分布,甚至改善它。另一方面,梯度可能非常尖锐且广泛。它们不符合正态分布,并且它们的极端程度使得在这些类型的图表上看不到任何有意义的内容。简而言之,你需要在某个地方加入对数尺度。
下图展示了三种替代方法来表示权重、梯度或其他任何类型的张量的分布:

这些图表是从同一层中提取的,并显示了训练过程中分布的变化。第一行显示权重,第二行显示梯度。
这些图表的产生方式如下:
- 值分布(即原始分布):这些显示原始值的百分位数——中间的实线表示中位数,灰色表示最小/最大范围,中间的阴影表示其他百分位数。这些对于指示广泛的特征很有用。不幸的是,它们在识别分布中是否存在多个峰值方面并不那么好——你仍然需要详细的直方图来做到这一点。此外,虽然梯度的值图显示了最小/最大范围,但它揭示不了太多其他内容。
- 对数幅度分布:中间的图表显示了按值幅度的百分位数,采用对数尺度。这对于梯度来说更加合适。有时权重和其他东西也需要这样做。因为幅度忽略了符号,并且你有时关心值是否在正负之间均衡或偏向一侧,橙色线条表示“质量平衡”的位置。0%表示完美平衡,+100%表示所有值都是正的,-100%表示所有值都是负的。
- 范数:右侧的图表使用了一种在学术文献中很受欢迎的非常不同的度量标准——L2范数,也称为欧几里得范数。这是向量范数的多维扩展——计算为所有维度上个体元素平方和的平方根。可以将其视为将整个权重或梯度张量视为一个多维向量,然后测量其长度。上面使用的范数经过调整以独立于张量大小。这给出了张量中值的整体规模的概念。在上面的图表中,权重的规模略有增加,而梯度的规模减少到原来的40%。
每个图表中心的文字覆盖只是显示了计算度量标准的张量的形状。我发现这对解释结果很有帮助——你会从较小的张量(如大多数网络的偏差向量和最终密集层)中得到更混乱的分布。
在这三个选项中,我发现两种分布图最有用。具体使用哪种取决于数据,有时需要实验。例如,如上所示,原始值分布对模型参数效果很好。
对于梯度,对数幅度图是必须的。它更能应对梯度固有的宽广尺度范围。例如,它可以同时显示梯度的完整范围,同时也表明梯度何时变得太小而无用。在上面的梯度图中,有9个百分位数显示——从0%到100%均匀分布,每12.5%之间有一个。大约在18个周期时,深蓝色阴影下降到图表底部,表示37.5%的梯度已达到零。然后在47个周期时,50%的梯度已达到零。这可能是存在问题的迹象。
当您只想获得一个单一标量值来代表权重、梯度或其他任何内容时,范数是有用的。例如,它对于比较多个层非常有用。然而,有一个问题,因为L2范数受到张量大小的强烈影响。例如,由所有1组成的张量的平方和简单地等于张量的大小(张量中元素的总数)。我更喜欢稍微修改一下,通过将其除以其大小的平方根来大小归一化L2范数。这在数学上等同于张量元素的均方根(RMS)。通过大小归一化的范数,您可以比较网络模型中梯度流,即使各层具有不同的大小。
5、所有层的分布图
无论您更喜欢哪种特定方法,接下来为模型中的每个可训练参数张量绘制这些图表都是有用的。例如,下面两幅图像显示了整个训练过程中模型中所有层的模型参数(使用值分布)和梯度(使用对数幅度分布)的分布:


对于这两张图表,顶部的两个大图提供了所有层的高层次总结,重点是它们的规模。左上角的图通过显示log-magnitude中位数的百分位分布来表示跨层的规模范围(无论其他图表显示的是值还是幅度,这都更容易了解规模)。右上角的图通过将它们堆叠在一起比较各层的规模。稍后我会解释这个目的。
我喜欢这样绘制所有层的单个图表,因为它让你在一个紧凑的视图中获得一切。没有“搜索和发现”过程,不需要展开和折叠小的切换框,也许还错过了打开那个包含你不知道自己需要的重要信息的切换框。
通过这两组图表(每层的参数和梯度),您获得了关于训练期间模型动态的大量信息。这些信息是通过查看损失曲线无法获得的。
参数分布特别有助于检查是否需要权重正则化。许多网络似乎在没有任何显式权重正则化的情况下表现良好。这些图表立即显示任何层的权重是否变得过大。
在权重和梯度之间,你会发现可视化梯度是最重要的一部分。尽管机器学习入门课程中提到权重变得过大或过小的情况很少发生,即使没有正则化。在我的经验中,梯度的问题出现得更为频繁。“消失和爆炸梯度”——那就是梯度。梯度”——又是梯度。“神经元死亡”——这也最好通过梯度来识别。
6、层输出的分布图
有时查看单个层的输出是有用的。
历史上,计算机视觉研究人员在不同的隐藏层上观察特征嵌入,并用它们来理解模型的工作原理。在具有注意力层的模型中,你可以使用注意力矩阵来了解模型关注的地方。
如果你没有更好的领域特定方法来表示隐藏层输出的意义,该怎么办?一个选择是将上面讨论过的相同分布可视化应用于层输出。
例如,这里是来自与前一节使用的相同模型和训练运行的层输出的分布图:

注意ReLU激活函数的效果很明显,大多数层只产生正值。值的尺度从一层到另一层各不相同。如果它们变得太大,这可能是一个问题。还要注意批归一化层的影响。虽然其输入都是正值,但其输出大约均匀分布在正负之间。通过比较有无批归一化层的结果,你可能会看到它如何影响后续的密集层。Dropout层不出所料地产生了与其输入层相同的分布——有一种不同类型的图表可以帮助我们可视化Dropout的效果。
我第一次开始反思和可视化层输出是在调查神经元死亡时。在训练期间收集层输出数据需要一些额外的努力,所以这不是一件显而易见的事情。但我很快发现,层输出对于理解训练期间的模型动态几乎和梯度一样重要。原因之一是我们直观设计层输出时,当我们选择激活函数并插入归一化时,层输出是我们直观设计的内容。因此,能够看到我们构建的内容是有意义的。例如,如果你只是因为看到别人这样做过而在模型中添加了一个批归一化层,而没有直接检查它对模型的影响,那么你就处于盲目的状态。
如果我必须从权重、梯度和层输出中选择两个可视化内容,我会选择梯度和层输出。
那么层输出如此有用,关于它们的梯度呢?
“什么?但我们已经讨论过梯度了”,我听到你说。
慢点。反向传播算法为每一层计算梯度分为两步。我们通常关心的是第二步,它计算权重、偏置和其他参数的梯度。但在那之前还有一步,其结果会反馈到权重、偏置等的梯度中。第一步计算相对于层输出的梯度。
正如你可能猜到的那样,我们可以对层输出梯度应用相同的绘图技术:

层输出及其梯度与参数及其梯度的趋势相似。输出通常在原始值分布图上表现良好,尽管你可能需要尝试对数幅度图。输出梯度尖锐且宽广——它们需要对数幅度图。为了比较,以下是三种绘图类型针对单一代表性层:

那么输出梯度有何用处?在这方面,我没有像对其他方面那样清楚的答案。一般来说,我认为输出梯度在某些情况下会对故障排除有用。但我不知道哪些情况会这样。这是一个我希望你拥有的工具。你发现它在何处有用取决于你自己。
在输出梯度上进行反思和计算统计数据可能更有用的地方是测量神经元死亡的影响。
7、可视化神经元激活和死亡率
ReLU层有一个问题:它们的单元可能会“死亡”。这种情况发生在某些单元开始产生恒定输出(通常是零)或其梯度始终为零时。这是ReLU激活函数的一个已知问题,因此提出了各种替代的泄漏和光滑版本的ReLU。但关于神经元死亡的检测和监控却鲜有讨论。
我将在以后的文章中详细解释神经元死亡、其原因、解决方案以及如何对其进行测量。目前,我想向你介绍两个受此问题启发的指标,并展示给我最喜欢的图表类型。
我发现有用的有两个指标:
- 死亡率
- 激活率
两者都通过考虑层中的各个单元来测量。对于ReLU单元,当它产生非零值时可以说它是“活跃”的,当它产生零时则“不活跃”。一个死亡的ReLU单元对于每个数据样本都是“不活跃”(零)。死亡率衡量层中死亡单元的比例。
现在,一个非死亡的ReLU单元可能只为某些数据样本是活跃的,而ReLU层可能只为任何给定的数据样本有部分单元是活跃的。激活率衡量所有样本中平均活跃单元的比例。这是一个非常有用的补充,用于测量神经元死亡率,因为低激活率可以提示那些尚未完全死亡但接近死亡的单元。
通过将这一概念推广到任何张量中零率的计算,不仅可以针对层输出,还可以针对原始模型参数、其梯度甚至层输出梯度来测量激活率和死亡率。通过对卷积、循环和Transformer等架构应用相同的概念,这些架构对不同输入重复应用相同的单元,就像在一批样本中一样,因此我们有以下通用定义:
- 死亡率——所有样本、空间位置、时间位置和/或输入通道中具有零值的所有(输出-)通道的比例。
- 激活率——所有维度上的非零比例
通过稍微多做一些工作,也可以将这些概念扩展到其他激活函数。
下面是我称之为“活动图”的图表。它显示了每层的激活率和死亡率,加上顶部的整体模型摘要。填充蓝色代表激活率,填充红色代表单元/通道死亡率。它还包括“空间死亡率”——一种稍后文章中会解释的死亡率变体。每个小图内部的标签给出了被测量张量的形状(本例中为BHWC顺序)以及最终轮次结束时的死亡率。顶部的模型摘要包括所有层的平均激活率和死亡率,以及它们的最小/最大范围。

这张图是在训练一个卷积分类模型时生成的,针对的是流行的CIFAR-10图像数据集。
注意许多层的激活率低于50%。这对于ReLU层的输出来说是非常典型的。每个单元对于任何给定输入都有50:50的机会产生活跃输出,因此平均值往往会在这个区域附近。它们倾向于低于50%的事实可能表明它们正在学习区分不同类型样本的特征。
大多数层的死亡率接近于零,但在某些层中较高。两个层在早期达到了50%的死亡率,然后在其余训练过程中有所改善。这可能意味着很多事情。在极端情况下,这可能是模型架构的根本性问题。在另一个极端,这可能意味着某些层只是比实际需要的更大——多余的容量可以通过修剪使模型更高效。
上述内容是对层输出的活动图。当你刚开始了解神经元死亡时,这是非常有帮助的。神经元死亡在神经元的输出方面最有意义。你还可以使用这些图表直接观察Dropout的效果。Dropout层的激活率应该略低于之前的层,而Dropout后的层的死亡率应小于没有Dropout的情况下应有的死亡率。
然而,在训练期间获得原始层输出可能比获得梯度更为棘手,事实证明,针对梯度的活动图对于理解神经元死亡对训练性能的影响更为有用。以下是同一模型和相同训练运行的活动图,但这次是针对梯度测量的:

我发现这个版本的图表作为潜在模型问题的指示器更有意义。这是因为在一个行为良好的模型中,梯度激活率往往更接近100%。如果在这里显著下降,或者许多层的激活率较低,这是一个信号,表明梯度受到瓶颈限制,模型的学习速度可能比原本的要慢。
正如我们在考虑分布图时发现的那样,我们也可以将活动图的想法应用于模型参数和层输出的梯度。对于模型参数,这证明是没有帮助的,因为模型参数很少达到精确的零。对于输出梯度,它提供了对神经元死亡的不同视角,着眼于反向传播的过程。梯度直接对被测量的层产生影响。
上述示例中使用的模型尽管显然有很高的神经元死亡率,但对任务完成得相当好。不过,将其称为“典型”并不合适。在不同的模型架构、不同超参数和不同问题领域中,你会发现非常广泛的行为差异。
最大的好处在于能够在这些比率上获得可见性的同时比较模型——你可以尝试不同的模型大小、不同的正则化方法、不同的激活函数,并直接观察它们如何影响模型的内部运作。这些图表甚至可以在你没有意识到模型表现不佳时就提示你。并且它们还可以建议你的模型是否过大,可以进行优化。
事实是,神经元死亡影响了所有但最简单的神经网络模型。无论是ReLU还是其他类型的激活函数。只是你不知道,因为你没有这种可见性。
8、比较尺度
我在这里介绍的数据收集和可视化技术都是为了更容易识别常见问题。有一组与模型参数及其梯度的“尺度”相关的常见问题。其中最著名的分别被称为“梯度消失”和“梯度爆炸”。在梯度消失的情况下,早期层的梯度比后期层小。而在梯度爆炸的情况下,所有层的梯度都非常高,比1.0大几个数量级。但还有其他尺度问题,例如权重变得异常小或大。
前面讨论过的分布图在突出显示所有层的整体尺度以及各层范围方面表现良好。这些对于识别所有层朝错误方向移动的情况很有用,比如在梯度爆炸的情况下。
可以创建另一种类型的图表,专注于层之间的“相对尺度”。这些图表更擅长识别梯度消失之类的问题,其中层之间的差异是你想要强调的关键指标。
以下是一种常见的用于检测梯度消失的图表样式:

该图表沿底部列出了各层,并绘制了每层梯度尺度的一些度量值。这里我使用了大小归一化的范数。事实证明,为了避免梯度消失问题,最重要的是第一层的梯度与最后一层的梯度具有相似或更大的规模。这正是我们在上面图表中看到的情况,所以我们可以放心。
这个图表的一个问题是,当你添加更多训练周期时,它会迅速变得混乱且难以阅读。上面显示的只是整个训练过程中采样的5个周期,它很好地概述了梯度消失的检测情况。
然而,我倾向于使用那些不仅适用于单一问题的可视化技术。能够为每个周期表示相同信息可能是有用的。
这是我一直在使用的替代方案:

乍一看可能有点奇怪。这个想法是为了迎合直觉而不是数学准确性。每个交替的颜色带代表一层。带宽较大的层具有较大的梯度。带的高度是按对数比例缩放的,因此最小的带比最大的带小几个数量级。我们可以很容易地看到第一层(底部带)的梯度比最后一层(顶部带)大,所以我们没有梯度消失的问题。我们也可以很容易地看到在整个训练过程中这种模式保持相对稳定。
生成此图表的步骤如下:
- 计算每层的“尺度”指标(例如:大小归一化的范数)
- 对这些指标取对数
- 将所有对数减去min(log(metrics))-1,使所有值变为正值,最小值为1.0。
- 重新缩放,使其总和为1.0
- 对每个周期单独进行上述操作。
- 使用matplotlib的stackplot()绘图
这将最小值转换为单位参考,每个倍数代表一个数量级的增加。示例代码可在此处获取 链接。
我发现当梯度出现振荡时,这个图表非常有用。而不是相对平滑的带状结果,你会得到更明显的锯齿状结果,振荡越严重,结果越明显:

当事情变得非常糟糕时,它也可能很有用。下面是一个例子,由于高神经元死亡率导致梯度崩溃:

在选择用于“层梯度尺度”的指标时,有几个选项可以提供类似的结果:大小归一化的L2范数(即RMS)、平均幅值和原始值的标准差都表现良好并产生相似的度量值。这些提供了相对干净和平滑的结果,并能很好地处理边缘情况。
相比之下,原始值的平均值是偏离零的度量值,不是衡量尺度的好指标。如果你已经有百分位数据,那么25%到75%百分位之间的距离的一半给出了与平均幅值类似的度量值。然而,在存在边缘情况(如高神经元死亡率)时,它更容易崩溃。如果你不明确测量神经元死亡率,这可能是个好事。
9、模型分析
我已经介绍了几种不同类型的可视化,并建议了它们如何用于解决各种常见问题。但这仅仅是个开始。我在这里提出的更广泛的潜力是——通过修改模型训练以收集更多信息,你可以根据自己的具体问题或特定领域定制自己的可视化。要做到这一点,你需要一个高效且通用的数据收集策略,并且需要在不同的模型之间可重用。
除了基本的损失、准确率等之外,本文还讨论了关于模型的四组值。我认为这些可以看作是“视图”,因为它们从不同的角度告诉你关于模型的信息:
- 可训练的模型参数(即变量)——权重、偏置等。
- 梯度(相对于可训练模型参数的通常梯度)
- 层输出
- 关于层输出的梯度
不幸的是,这些并不总是容易访问。如果你遵循通常的教学教程来学习如何训练神经网络模型,你会发现很少有示例在训练后收集这些值进行统计分析。特别是获取层输出有些麻烦,因为层通常被包裹在一个更大的模型中,你必须“解包”模型以记录层输出,或者在层代码中添加记录输出的功能。
以下是TensorFlow中自定义训练循环的简化版本。它可以接受任何模型并收集损失、指标、模型参数、梯度、层输出和层输出梯度的原始值:
import tensorflow as tf
import tqdm
def custom_fit(model, train_dataset, epochs, optimizer):
# 在训练期间解包模型以便访问各个层的输出
monitoring_model = tf.keras.Model(
inputs=model.inputs,
outputs=[model.outputs] + [layer.output for layer in model.layers])
loss_history = []
metrics_history = []
variables_history = []
gradients_history = []
layer_outputs_history = []
layer_gradients_history = []
for epoch in tqdm.tqdm(range(epochs)):
for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
with tf.GradientTape() as tape:
monitoring_outputs = monitoring_model(x_batch_train, training=True)
y_pred = monitoring_outputs[0][0]
layer_outputs = monitoring_outputs[1:]
loss = model.compute_loss(x_batch_train, y_batch_train, y_pred, training=True)
loss = optimizer.scale_loss(loss)
all_grads = tape.gradient(loss, model.trainable_weights + layer_outputs)
trainable_grads = all_grads[:len(model.trainable_variables)]
output_grads = all_grads[len(model.trainable_variables):]
optimizer.apply_gradients(zip(trainable_grads, model.trainable_weights))
metrics = model.compute_metrics(x_batch_train, y_batch_train, y_pred)
loss_history.append(loss)
metrics_history.append(metrics)
variables_history.append([tf.identity(v) for v in model.trainable_variables])
gradients_history.append(trainable_grads)
layer_outputs_history.append(layer_outputs)
layer_gradients_history.append(output_grads)
return loss_history, metrics_history, variables_history, gradients_history, layer_outputs_history, layer_gradients_history
上述代码确实有效,但它效率低下,缺乏对多种不同数据集指定方式和其他训练超参数的支持,并且不包含任何在周期内聚合值所需的逻辑。
另一个问题是它太不灵活——总是记录所有数据。你希望有一种方式可以根据需求“插入”不同的数据收集器。TensorFlow采用回调方法,你可以在训练过程中定义自己的规则来记录或收集数据。因此,如果你使用TensorFlow,你可能会想这样做:
def MyDataCollectionCallback():
def on_epoch_end(variables, gradients, outputs, output_gradients):
...
custom_fit(model, dataset, epochs, callbacks=[MyDataCollectionCallback()])
这是完全可以实现的。然而,代码稍微复杂了一些。如需完整、高效且灵活的适用于 TensorFlow 的版本,请参阅工具包中的代码 。
如果您使用其他平台,我相信这个思路可以轻松适应。
在任何实际的神经网络中,模型的四种“视角”都是一个包含大量张量的列表,存储它们会占用大量RAM。有几种技术可以帮助你减少总内存使用。
首先,你需要实验一下是每步(per-step)存储这些值还是仅在每个epoch结束时存储一次(per-epoch)。
当存储为每epoch时,你需要考虑是在epoch结束时存储最新值还是在整个epoch期间进行聚合。这取决于你的目标,但这里有一些提示:
- 默认情况下,TensorFlow在整个epoch中计算平均损失,并且可能对其他指标也这样做。这对于统计信息和其他针对整个模型的度量非常有用,例如其输出。
- 可训练参数在epoch结束时反映了它们在完成一次完整训练数据集遍历后的最终状态,因此通常直接取它们的最终状态即可。
- 梯度会在每次更新步骤之间自然波动很大,这是随机小批量学习的一般模式。其中很多波动会相互抵消,所以一个好的策略是在整个epoch中将梯度求和。我认为这类似于近似模拟SGD如果在每个epoch中只采取一个大步而不是许多小步的情况。
- 层输出是为每个小批量生成的。它们从一步到另一步以及样本到样本之间都会变化。统计数据应该一般累积每个小批量中的每个样本,然后在epoch结束时计算。例如,Welford在线算法提供了一种高效且数值稳定的累积标准差计算方法。
另一种减少RAM使用的方法是只存储统计数据。你存储哪些统计数据取决于你想从数据中获取什么。你可能会先收集完整的张量,决定从数据中生成哪些可视化,然后通过仅计算和存储所需的统计数据来优化。
注意,有些统计数据比其他统计数据更昂贵。越昂贵,它对总训练时间的影响就越大。在这篇文章中我们已经使用了几种统计数据和其他指标。以下是一些已使用的指标,加上一些其他有用的指标,同时考虑了它们的计算成本:
- L2范数,即欧几里得范数。这是首次查看权重分布时引入的。它在学术文献中常用,因为它产生了一个代表张量大小的单个标量。基本的L2范数依赖于张量元素平方和的总和,因此对张量的大小敏感,容易产生数千的值(完整公式列在下面)。它对于观察随时间变化的单一张量的变化很有用,但对于检查张量中值的整体规模或跨层比较不同模型中的张量效果不佳。L2范数相对容易计算。
- 归一化范数,即RMS。 我更喜欢L2范数的一个归一化变体:基本的L2范数除以张量大小的平方根。结果等同于计算张量元素的均方根(RMS)。从数学上讲,L2范数和RMS的效率应该相同。实际上,在具有数万个元素的张量上,TensorFlow实现的RMS(
tf.sqrt(tf.reduce_mean(tf.square(tensor)))
)比L2范数(tf.norm(tensor)
)快约1.5倍到2倍(在TensorFlow 2.18和T4 GPU上测试过)。 - 均值和标准差。这些通常是任何数据集的首选。它们的优点是计算效率高,即使在大型张量上每步计算也不会显著增加训练时间。均值和标准差在某些机器学习文献中也有使用。然而,必须意识到一些显著的问题。首先,权重几乎总是正负值完美平衡,均值非常接近零。因此,均值是衡量权重与完美正负平衡之间的差异。这不是通常想要的。其次,梯度倾向于分布在非常窄、非常高的峰顶和非常长的尾部。最后,值通常不在均值两侧均匀分布。因此,不仅均值几乎无用,标准差的意义也值得怀疑。
- 对数均值和标准差。如果你要使用均值/标准差,我建议你改用张量元素幅度的对数的均值和标准差,并在对数尺度上绘制。使用对数尺度更好地反映了梯度的自然分布(非常窄的高尖峰和长尾)。测量幅度而不是原始(带符号的)值是使对数图工作所必需的,这也使均值有意义。现在它成为值的平均“尺度”的度量——事实上,它近似于原始梯度的标准差。
- 百分位分布。为了获得最准确的信息,你可以计算感兴趣的张量中所有元素的完整直方图。TensorBoard中的权重直方图就是这样做的。在我的实验中,我发现只需计算从第0百分位到第100百分位的几个百分位就足够了。在列出的所有指标中,这是迄今为止计算成本最高的。当用于模型参数、层输出及其梯度时,尤其是在每步计算时,它会明显减慢训练时间。在TensorFlow中,你可以使用TensorFlow概率库
tfp.stats.percentile(tensor, quantiles=[...])
来计算这个。
以下是上述描述的一些指标的方程:

注意,对于梯度,其中均值几乎总是非常接近零,E[X]项近似为零,标准差方程简化为均方根。
其他任务需要你定义自己的度量标准。活动图中使用的激活率和死亡率指标就是例子。这就是回调式方法的强大之处——能够实验自己的度量计算,并将其插入到更通用的训练循环中,而无需为每个任务创建自定义训练循环。
10、单一视图
如果你搜索有关模型调试的教程,你最常见的会找到关于模型权重和梯度的参考。我希望我已经向你展示了不仅仅是两种,而是四种模型“视角”的价值:
- 模型参数/变量
- 参数梯度
- 层输出
- 对层输出的梯度
我还展示了如何以不同的方式绘制这些视角的信息,每种绘图方式都以不同的方式有助于发现和调试模型。
我想留给你的最后一个想法是将所有这些信息整合到一个仪表板中,有时称为“单一玻璃窗”视图,覆盖整个模型训练过程。
模型存在许多不同的方式无法学习、学习太慢或其他问题。损失和准确率曲线不会给你任何关于问题根本原因的信息。它们只能表明有问题,而且往往甚至这一点也不清楚。因此,结合我所描述的所有技术以及其他技术,是一种强大的方式,可以更快地获取关于问题的信息。具体怎么做将很大程度上取决于你对你特定需求的关注点。
为了启发灵感,让我分享一下我一直在使用的:

在这个图表中,每个单独的图表显示了整个模型的某个方面。
左上角的图表显示了每个epoch中针对模型输出测量的常规损失和其他指标。
右上角的图表提取了关于神经元死亡的最糟糕信号。想法是扩展此功能以显示其他“警告”信号。
较小的行图表显示了针对四种模型视角(参数、梯度、输出、输出梯度)的各个指标。
左侧列通过显示各层的(归一化)范数分布来总结整个模型的层值。这有助于识别值的“规模”是否存在明显问题,特别是在随着时间推移时。例如,如果所有层的规模变得比开始时大幅缩小或增大。
中间列显示了层尺度的对数相对图。这对于识别消失和爆炸梯度很有用。它始终显示层之间的相对规模,忽略它们的整体趋势,这使得它比左手边的图表更适合识别不同的问题。
右侧列总结了整个模型的激活率和死亡率。它显示了激活率的均值、最小值和最大值分布,以及最坏的死亡率(最大值)。这种方法认为模型的好坏取决于其最薄弱环节(即最差的层),因此最好突出显示这一点。
11、结束语
在阅读本文之前,你可能一直根据基本的损失和指标曲线来判断模型的进展,比如这样:

与单个全景视图进行比较。
与权重、梯度和层输出的分层详细图进行比较。
与神经元激活率和死亡率的图表进行比较。
与通过在训练期间轻松访问梯度、层输出和其他属性可以获得的所有其他信息进行比较。
损失和准确率衡量了你的模型的输出。这些是外部行为。但在模型内部组件的动态中,有一个丰富的信息宝藏。它只是在等待被挖掘并加以利用。
在这篇文章中,我向你展示了我认为有用的方法,可以直接查看这些内部动态。这些只是几个选项,还有许多其他选项可供探索。只需要你去尝试自己的想法。
我通过解释一系列你可以使用的技巧,提供了一些实际工作的示例,以及提供一个工具包作为起点,试图让这个探索过程更容易。
所以走出去,收获成果,并享受乐趣。
但不要急着离开……
在本系列的其余部分,我将向你展示如何使用这些可视化技术来解决一些常见的困扰模型训练的问题(页面顶部有链接)。
本文中的所有结果和图表都可以从 Jupyter 笔记本中重现。
数据收集和图表是使用我创建的一个开源工具包生成的,以减少使用这些技术的惯性。你可以直接使用该工具包,或者将其代码和想法作为自己解决方案的基础。
原文链接:Better ways to monitor NNs while training
汇智网翻译整理,转载请标明出处
