【阅读总结】A Recipe for Training Neural Networks

神经网络训练往往调用高度抽象的api,因而难以准确定位错误。cs231n课程的设计者之一Andrej Karpathy在博客中提出一套运用神经网络解决新问题的研究思路,从简单到复杂逐步建立模型,来避免一次性引入大量“未经验证”的复杂性。博客主要以作者擅长的cv+CNN领域为例,对其他深度学习方向也有很高的参考价值。

本文简要翻译这一思路,加*语句为笔者补充。

1.检查数据

不借用神经网络代码,手动检查数据,观察分布并寻找模式,如重复、数据损坏、类不平衡和偏见等等。

同时思考数据特点对应的架构:local features or global context,数据变动大吗/以什么形式变化,哪些变化是可以预处理掉的噪声,空间位置重要吗/可以使用平均化,可以下采样吗,标签噪声大吗。

可以在预处理中简单地搜索/过滤/排序来了解数据,尤其是数据中的异常值。

2.端到端的训练评估框架

建立一个完整的训练+评估框架,通过实验验证框架的正确性,最好使用简单模型来获取基线表现

此阶段的提示和技巧:

固定随机种子

*如torch,以下代码可做固定随机性的参考,以保证实验结果可复现。

def seed_everything(seed=3407):
    random.seed(seed)
    np.random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
    torch.backends.cudnn.deterministic = True
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed) 
    torch.cuda.manual_seed(seed)
    if args.n_gpu > 0:
        torch.cuda.manual_seed_all(args.seed)

简化

确保禁用任何不必要的技巧。举个例子,在这个阶段一定要关闭任何数据增强。数据增强是一种正则化策略,稍后可能会采用,但在这一阶段容易引入无用的错误。

在评估中添加有效数字

绘制测试损失时,对整个(大)测试集运行评估。不要只绘制批次的测试损失,然后依靠 Tensorboard 对其进行平滑。

验证损失

验证您的损失是否从正确的损失值开始。例如。如果正确初始化最后一层,则应该在初始化时在 softmax 上测量 -log(1/n_classes) 。对于 L2 回归、Huber 损失等,可以得出相同的默认值。

初始化

正确初始化最终层权重。例如。如果您要对平均值为 50 的某些值进行回归,则将最终偏差初始化为 50。如果您的正数:负数比率为 1:10 的不平衡数据集,请在 Logits 上设置偏差,以便您的网络预测初始化时为 0.1。如果在前几次迭代中网络基本上只是学习偏差,正确初始化将加速收敛并消除类似曲棍球棒(*下降后走平)的损失曲线。

人类基线

监控除loss之外的人类可解释和可检查的指标(例如准确性)。只要有可能,评估自己(人类)的准确性并与之进行比较。

与输入无关的基线

训练与输入无关的基线(例如,最简单的方法是将所有输入设置为零)。这应该使得表现更差。即,模型是否学会从输入中提取任何信息?

一批过拟合

仅对少数示例(例如,少至两个)的一个Batch进行过拟合。为此,我们增加模型的容量(例如添加层或卷积核)并验证我们是否可以达到可实现的最低损失(例如零)。我还喜欢在同一个图中可视化标签和预测,并确保一旦我们达到最小损失,它们最终会完美对齐。如果不能,则说明某处存在错误,我们无法继续下一阶段。

验证训练损失的下降

在这个阶段,您可能会在数据集上拟合不足,因为您正在使用最简单的玩具模型。尝试稍微增加其容量。您的训练损失是否按预期下降了?

可视化网络输入

y_hat = model(x) (或 tf 中的 sess.run)之前可视化数据。也就是说,您要准确地可视化进入网络的内容,将数据和标签的原始张量解码为可视化。

可视化预测动态

我喜欢在训练过程中可视化固定测试批次的模型预测。这些模型预测如何变化的“动态”将为您提供关于训练如何进展的直觉。很多时候,如果网络在某些方面波动太大,从而暴露出不稳定性,您可能会感到网络“努力”适应您的数据。非常低或非常高的学习率在抖动量中也很容易被注意到。

使用反向传播来绘制依赖关系图

您的深度学习代码通常包含复杂的、矢量化的和广播的操作。我遇到过几次的一个相对常见的错误是人们犯​​了这个错误(例如,他们使用view 而不是 transpose/permute)并错误地在batch维度上混合信息。令人沮丧的事实是,您的网络通常仍能正常训练,因为它会学会忽略其他示例中的数据。调试此问题(以及其他相关问题)的一种方法是将调整损失为,例如示例 **i 的所有输出的总和,然后反向传播到输入,确保仅在第 i **个输入上得到非零梯度。

相同的策略可用于例如确保您的自回归模型在时间 t 仅取决于 1…t-1。更一般地说,梯度为您提供有关网络中哪些内容依赖于哪些内容的信息,这对于调试非常有用。

泛化一个特例

这是一个更为普遍的编码技巧,但我经常看到有人因为贪多嚼不烂而犯错,他们一开始就试图从头编写一个相对通用的功能。我更喜欢先写一个非常具体的函数来解决当前的问题,使其能够正常工作,然后再进行泛化,确保得到相同的结果。这种方法通常适用于矢量化代码的场景,我几乎总是先写出完整的循环版本,然后再逐个循环地将其转化为矢量化代码。

*矢量化代码(Vectorized Code)是指利用向量操作代替显式循环的一种编程方法,通常用于处理数组或矩阵等数据结构。

3.过拟合

在这个阶段,我们应该对数据集有很好的理解,并且我们已经有了完整的训练+评估框架。对于任何给定的模型,我们可以(可重复地)计算我们信任的指标。我们还配备了与输入无关的基线的性能,一些简单模型基线的性能(我们最好击败这些),并且我们对人类手工识别的性能有一个粗略的认识(我们希望达到这一点)。现在已经准备好迭代一个好的模型了。

我喜欢采取的寻找优秀模型的方法分为两个阶段:首先,选择一个足够大的模型,使其可以过拟合(即,专注于训练损失);然后,适当地进行正则化(牺牲一些训练损失以提高验证损失)。我喜欢这两个阶段的原因是,如果我们无法用任何模型达到较低的错误率,这可能再次表明存在一些问题、错误或配置不当的情况。

此阶段的提示和技巧:

选择模型

为了获得良好的训练损失,您需要为数据选择合适的架构。我的第一建议是:不要做英雄。我见过很多人急于在神经网络工具箱的各种模块上叠加出疯狂而又创意十足的奇异架构。强烈建议在项目初期抵制这种诱惑。我总是建议人们简单地找到最相关的论文,并复制粘贴他们的最简单且表现良好的架构。例如,如果您正在对图像进行分类,不要做英雄,直接复制粘贴一个ResNet-50进行第一次尝试。之后您可以允许自己做一些自定义的调整并超越它。

Adam优化器是安全的

在设定基线的早期阶段,我喜欢使用Adam优化器,学习率为3e-4。根据我的经验,Adam对超参数(包括糟糕的学习率)更宽容。对于卷积神经网络(ConvNets),调整得当的SGD几乎总是略微优于Adam,但最优学习率范围更狭窄且依赖于具体问题。(注意:如果您使用的是RNN和相关的序列模型,使用Adam更加常见。在项目初期,同样不要做英雄,遵循最相关的论文中的方法。)

一次只增加一个复杂性

如果有多个信号要输入到分类器中,一次只插入一个,并且每次确保获得预期的性能提升。不要在一开始就把所有东西都丢进模型中。还有其他增加复杂性的方法——例如,您可以先尝试较小的图像,然后再逐渐增大它们的尺寸等。

不要相信默认的学习率衰减

如果您正在复用来自其他领域的代码,请始终小心学习率衰减。不同的问题需要使用不同的衰减计划(schedules)——更糟糕的是,在典型实现中,计划通常基于当前的epoch数,而这会因数据集的大小而有很大差异。例如,ImageNet在第30个epoch衰减10倍。如果您不是在训练ImageNet,那么几乎肯定不希望这样做。如果不小心,您的代码可能会在未被察觉的情况下过早地将学习率降至零,从而不允许模型收敛。

在我的工作中,我总是完全禁用学习率衰减(使用常数学习率),并在最后阶段进行调整。

4.正则化

理想情况下,我们现在有一个至少能拟合训练集的大模型。现在是时候对它进行正则化,并通过放弃一些训练精度来提高验证精度。

此阶段的提示和技巧:

获取更多数据

首先,在任何实际环境中,正则化模型的最佳和首选方法是增加更多的真实训练数据。花费大量工程时间试图从小数据集中榨取更多的价值,是一个非常常见的错误。实际上你可以收集更多的数据。据我所知,增加数据几乎是唯一一种可以保证单调提高配置良好的神经网络性能的方法,几乎没有止境。另一个方法是集成(如果你能负担得起),但这在大约5个模型后会达到顶峰。

数据增强

仅次于真实数据的是半假的数据——尝试更激进的数据增强。

创造性的数据增强

如果半假的数据不行,假的数据也可能有用。人们正在找到创造性的方法来扩展数据集;例如,domain随机化、使用模拟、插入(可能是模拟的)数据到场景中的巧妙混合,甚至是生成对抗网络(GANs)。

预训练

如果可以使用预训练网络,几乎从来没有坏处,即使你有足够的数据。

坚持监督学习

不要对无监督预训练过于兴奋。与2008年的那篇博客文章所说的不同,据我所知,在现代计算机视觉中,没有任何版本的无监督预训练报告过强大的结果(虽然NLP似乎在使用BERT和类似模型时表现相当不错,这很可能是由于文本的更为明确的性质和较高的信噪比)。

*结合多模态的预训练当下非常火热,但是在具体问题的实践上,大模型+少量有标注数据微调依然是较为通用的思路。

减小输入维度

移除可能包含虚假信号的特征。如果你的数据集很小,任何额外的虚假输入都可能导向过拟合。同样,如果低层次的细节不太重要,尝试输入较小的图像。

减小模型尺寸

在许多情况下,你可以使用领域知识对网络进行约束以减小其尺寸。举例来说,过去在ImageNet的骨干网络顶层使用全连接层曾经很流行,但这些已经被简单的平均池化所取代,从而消除了大量参数。

减小Batch

较小的batch大小在某种程度上对应于更强的正则化。这是因为batch的经验均值/标准差是全体均值/标准差的更近似版本,因此缩放和偏移会使你的批量波动更大。

添加Dropout

为卷积神经网络(ConvNets)使用dropout2d(空间dropout)。谨慎使用,因为dropout似乎与batch normalization不太兼容。

增加Weight decay

增加权重衰减惩罚。

Early stopping

尝试更大的模型

我最后提到这一点,而且只有在early stopping之后。我过去发现,尽管更大的模型最终会更多地过拟合,但它们的“提前停止”性能往往比较小的模型更好。

最后,为了增加对网络作为合理分类器的信心,我喜欢可视化网络的第一层权重,并确保得到的边缘是合理的。如果第一层卷积看起来像噪声,那么可能有问题。同样,网络内部的激活有时会显示出奇怪的伪影,暗示存在问题。

5.调优

现在您应该已经“进入循环”,开始探索广泛的模型空间,以找到实现低验证损失的架构。

此阶段的提示和技巧:

随机搜索优于网格搜索

当同时调整多个超参数时,使用网格搜索确保覆盖所有设置可能听起来很有诱惑力,但请记住,最好使用随机搜索。直观上,这是因为神经网络通常对某些参数比对其他参数更敏感。极限情况下,如果参数a很重要,但改变b没有效果,那么您宁愿更彻底地采样a,而不是在几个固定点上多次采样。

超参数优化

市面上有大量花哨的贝叶斯超参数优化工具箱,我的一些朋友也报告说使用这些工具取得了成功,但根据我的个人经验,探索广泛的模型和超参数空间的最先进方法是使用一个实习生 😃 只是开个玩笑。

6.其他

一旦找到最佳的架构类型和超参数,您仍然可以使用一些技巧从系统中榨取最后一点性能:

模型集成

模型集成几乎可以保证在任何任务上增加2%的准确率。如果在测试时无法负担计算成本,可以考虑使用暗知识将集成模型蒸馏到一个网络中。

继续训练

我经常看到人们在验证损失似乎趋于平稳时停止模型训练。根据我的经验,网络训练时间往往比预期的要长。有一次我在寒假期间不小心让一个模型继续训练,等到一月份回来时,它已经达到了“当前最优”(SOTA)的水平。

思路总结

### 扩散模型中的水印技术实现 扩散模型是一种基于概率分布建模的技术,在图像生成领域取得了显著成果。然而,随着这些模型的应用日益广泛,保护知识产权的需求也变得尤为重要。为此,研究者提出了多种针对扩散模型的水印嵌入方案。 一种常见的方法是在训练阶段通过修改损失函数来嵌入水印[^1]。具体而言,可以在目标函数中加入额外项以优化特定模式或特征向量作为隐秘标记。这种方法的优点在于不会明显影响模型性能的同时实现了版权标识的功能。 另一种方式则是在推理过程中动态添加个性化标签[^2]。例如当利用预训练好的扩散网络生成新图片时,可以调整某些超参数或者输入条件从而使得输出结果携带预定信息而不破坏视觉质量。 此外还有直接操作权重矩阵本身来进行永久性标注的做法[^3]。此策略涉及对神经元连接强度做细微改动以便于后期验证所有权归属情况而无需改变原有架构设计太多细节部分即可完成整个流程设置工作。 下面给出一段简单的伪代码用于演示如何在Python环境下构建基本框架: ```python import torch from diffusers import StableDiffusionPipeline def apply_watermark(model, watermark_data): """Apply a watermark by modifying the model's parameters.""" with torch.no_grad(): for param in model.parameters(): param.add_(watermark_data * 0.01) pipeline = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") # Example of embedding some random noise as watermark data. torch.manual_seed(42) watermark_tensor = torch.randn_like(next(pipeline.text_encoder.parameters())) apply_watermark(pipeline.unet, watermark_tensor) image = pipeline(prompt="A digital art piece").images[0] image.save("./output_with_watermark.png") ``` 上述脚本展示了怎样加载稳定扩散管道并对其内部组件施加自定义扰动达到隐藏签名效果的目的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值