单元测试是您的朋友
作家之间有一种说法,就是“所有写作都是重写”,也就是说,写作的大部分都在进行修改。对于程序员(或至少是数据科学家)而言,该表达可以改写为“所有编码都在调试”。
每次编写代码时,都需要验证其是否按预期工作。我找到的验证正确性的最佳方法是将您的代码分成小段,并验证每个段是否有效。这可以通过将段输出与您知道的正确答案进行比较来完成。这称为单元测试。编写好的单元测试是成为一名好的统计学家/数据科学家/机器学习专家/神经网络从业人员的关键。根本没有替代品。
在调整网络性能之前,您必须检查代码是否没有错误!否则,您不妨重新布置RMS Titanic上的躺椅。
神经网络的两个功能使验证比其他类型的机器学习或统计模型更为重要。
神经网络不是随机森林或逻辑回归那样的“现成”算法。即使对于简单的前馈网络,用户也有很大的责任来决定如何配置,连接,初始化和优化网络。这意味着编写代码,而编写代码意味着调试。
即使在执行神经网络代码而没有引发异常的情况下,网络仍然可能存在错误!这些错误甚至可能是网络将对其进行训练的隐患,但陷入了次优的解决方案,或者最终的网络没有所需的体系结构。(这是语法错误和语义错误之间区别的示例。)
Chase Roberts的这篇中级文章“ 如何对机器学习代码进行单元测试 ”详细讨论了机器学习模型的单元测试。我从文章中借用了这个错误代码示例:
def make_convnet(input_image):
net = slim.conv2d(input_image, 32, [11, 11], scope="conv1_11x11")
net = slim.conv2d(input_image, 64, [5, 5], scope="conv2_5x5")
net = slim.max_pool2d(net, [4, 4], stride=4, scope='pool1')
net = slim.conv2d(input_image, 64, [5, 5], scope="conv3_5x5")
net = slim.conv2d(input_image, 128, [3, 3], scope="conv4_3x3")
net = slim.max_pool2d(net, [2, 2], scope='pool2')
net = slim.conv2d(input_image, 128, [3, 3], scope="conv5_3x3")
net = slim.max_pool2d(net, [2, 2], scope='pool3')
net = slim.conv2d(input_image, 32, [1, 1], scope="conv6_1x1")
return net
看到错误了吗?实际上没有使用许多不同的操作,因为以前的结果被新变量覆盖。在网络中使用此代码块仍会进行训练,权重也会更新,并且损失甚至可能会减少-但该代码肯定没有达到预期的效果。(作者在使用单引号或双引号时也不一致,但这纯粹是风格。)
与神经网络有关的最常见的编程错误是
- 变量被创建但从未使用(通常是由于复制粘贴错误);
- 渐变更新的表达式不正确;
- 不应用体重更新;
- 损失函数的度量标准不正确(例如,交叉熵损失可以用概率或对数表示)
- 该损失不适用于该任务(例如,对回归任务使用分类交叉熵损失)。
走路前爬行;跑步前先走
机器学习中的热点之一是宽而深的神经网络,以及带有奇特布线的神经网络。但是这些网络并没有完全形成。他们的设计师从较小的单位开始为他们服务。首先,建立一个具有单个隐藏层的小型网络,并验证其是否正常运行。然后逐步增加其他模型的复杂性,并验证每个模型是否同样有效。
太少量 的神经元中的层可以限制网络学习,下配合使表示。太多的神经元会导致过度拟合,因为网络会“记忆”训练数据。
即使可以从数学上证明只需要少量的神经元即可对问题进行建模,通常情况下,拥有“更多”神经元会使优化器更容易找到“良好”配置。(但是我认为没有人完全理解为什么会这样。)我在这里的XOR问题的背景下提供了一个示例:我的迭代是否不需要为MSE <0.001太高的XOR训练NN?。
选择隐藏层的数量可使网络从原始数据中学习抽象。如今,深度学习风靡一时,具有大量层次的网络已显示出令人印象深刻的结果。但是添加过多的隐藏层可能会导致过度拟合的风险或使网络优化变得非常困难。
选择一个聪明的网络布线可以为您完成很多工作。您的数据源是否适合专用网络体系结构?卷积神经网络可以在“结构化”数据源,图像或音频数据上取得令人印象深刻的结果。递归神经网络可以很好地处理顺序数据类型,例如自然语言或时间序列数据。残留连接可以改善深度前馈网络。
神经网络训练就像撬锁一样
为了获得最先进的结果,甚至仅仅获得良好的结果,您必须设置所有配置为可以很好地协同工作的部件。设置一个实际学习的神经网络配置就像选择一把锁:所有部分都必须正确排列。仅仅在正确的位置放置一个翻转开关是不够的,仅正确地设置架构或优化器也是不够的。
调整配置选择并不是真正简单地说一种配置选择(例如学习率)比另一种(例如学习单元数)重要,因为所有这些选择都与所有其他选择相互作用,因此选择可以与其他地方的选择结合起来很好。
这是配置选项的非穷举列表,它们也不是正则化选项或数值优化选项。
所有这些主题都是活跃的研究领域。
非凸优化很难
神经网络的目标函数仅在没有隐藏单元,所有激活都是线性的且设计矩阵为全秩时才凸出-因为这种配置同样是一个普通的回归问题。
在所有其他情况下,优化问题是非凸的,非凸优化是困难的。训练神经网络的挑战是众所周知的(请参阅:为什么很难训练深度神经网络?)。此外,神经网络具有大量参数,这将我们限制为仅一阶方法(请参阅:为什么牛顿法没有广泛用于机器学习中?)。这是一个非常活跃的研究领域。
将学习速率设置得太大会导致优化方案发散,因为您将从“峡谷”的一侧跳到另一侧。将此值设置得太小将阻止您取得任何实际进展,并可能使SGD中固有的噪声使您的梯度估计值不堪重负。
如果渐变阈值高于某个阈值,则渐变裁剪会重新缩放该范数。我曾经以为这是一个“设置后遗忘”参数,通常为1.0,但是我发现通过将其设置为0.25,可以使LSTM语言模型大大改善。我不知道为什么。
学习率安排可能会降低训练过程中的学习率。以我的经验,尝试使用调度与regex很像:它用两个问题(“如何在某个时期后学会继续学习”代替一个问题(“如何在某个时期后学会继续学习”?)。吗?”和“如何选择合适的时间表?”)。其他人则坚持计划是必不可少的。我让你决定。
选择一个合适的小批量可以间接影响学习过程,因为较大的小批量将比较小的小批量具有较小的方差(大数定律)。您希望小批量生产足够大以提供有关梯度方向的信息,但又要足够小以使SGD可以规范您的网络。
随机梯度下降有许多变种,它们使用动量,自适应学习率,Nesterov更新等来改善原始SGD。设计更好的优化器是非常活跃的研究领域。一些例子:
刚推出时,Adam优化器引起了很多兴趣。但是最近的一些研究发现,具有动量的SGD可以胜过神经网络的自适应梯度法。“ 机器学习中自适应梯度方法的边际价值 ”,作者:Ashia C. Wilson,Rebecca Roelofs,Mitchell Stern,Nathan Srebro,Benjamin Recht
但是,另一方面,这篇最近的论文提出了一种新的自适应学习速率优化器,它可以缩小自适应速率方法和SGD之间的差距。“ 关闭自适应梯度法的训练深层神经网络的泛化峡 ”,由孟京辉陈,顾拳拳
在训练深度神经网络中,采用历史梯度信息来自动调整学习速率的自适应梯度方法普遍被认为比具有动量的随机梯度下降法(SGD)更差。这使得如何弥合自适应梯度法的泛化差距成为一个悬而未决的问题。在这项工作中,我们表明自适应梯度方法(例如Adam,Amsgrad)有时会“过度自适应”。我们设计了一种新算法,称为部分自适应动量估计方法(Padam),该算法将Adam / Amsgrad与SGD结合在一起,从而实现了两全其美。在标准基准测试上的实验表明,Padam可以像Adam / Amsgrad一样保持快速收敛速度,同时可以在训练深度神经网络时推广SGD和SGD。
正常化
数据的规模可以在训练上产生很大的不同。
正则化
选择和调整网络正则化是构建泛化效果好的模型(即,不会过度拟合训练数据的模型)的关键部分。但是,当您的网络正在努力减少训练数据的损失时(当网络不学习时),正则化可以掩盖问题所在。
当我的网络无法学习时,我会关闭所有正则化功能,并验证非正则化网络是否正常工作。然后,我将每个正则化片重新添加回去,并验证它们中的每一个都能正常工作。
此策略可以指出某些正则化设置不正确的地方。一些例子是
保留实验记录
设置神经网络时,我不会对任何参数设置进行硬编码。相反,我在一个配置文件(例如JSON)中执行此操作,该文件已读取并用于在运行时填充网络配置详细信息。我保留所有这些配置文件。如果我进行任何参数修改,我将创建一个新的配置文件。最后,我将所有因训练和验证而损失的每个周期附加为注释。
k
例如,我想学习LSTM语言模型,因此我决定制作一个Twitter机器人,该机器人可以编写新的推文来响应其他Twitter用户。我在业余时间(从研究生到工作之间)从事这项工作。用了大约一年的时间,我遍历了150多个不同的模型,然后找到了一个符合我想要的模型:生成有意义的新英语文本。(一个关键的症结,也是进行大量尝试的部分原因是,仅仅获得低的样本外损失是不够的,因为早期的低损失模型已经成功地记住了训练数据,因此,它只是逐字地复制德语文本块以响应提示-进行了一些调整,以使模型更自发且仍然具有较低的损失。)