TowardsDataScience 2023 博客中文翻译(一百九十二)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

使用 JAX 和 Haiku 从头实现 Transformer 编码器 🤖

原文:towardsdatascience.com/implementing-a-transformer-encoder-from-scratch-with-jax-and-haiku-791d31b4f0dd?source=collection_archive---------7-----------------------#2023-11-07

理解 Transformers 的基本构建模块。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Ryan Pégoud

·

关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 11 月 7 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Transformers,以爱德华·霍珀(由 Dall.E 3 生成)的风格

在 2017 年的开创性论文“注意力机制就是你需要的[0]中介绍的 Transformer 架构可以说是近期深度学习历史上最具影响力的突破之一,它使大型语言模型的兴起成为可能,并且在计算机视觉等领域也找到了应用。

继承了依赖于递归的前沿架构,如长短期记忆(LSTM)网络或门控循环单元(GRU),Transformer引入了自注意力的概念,并结合了编码器/解码器架构。

在本文中,我们将从零开始一步一步实现 Transformer 的前半部分,即编码器。我们将使用JAX作为主要框架,并结合Haiku,这是 DeepMind 的深度学习库之一。

如果你对 JAX 不熟悉或需要对其惊人功能有一个新的提醒,我在我的上一篇文章中已经涵盖了这个话题:

使用 JAX 向量化和并行化强化学习环境:光速 Q-learning⚡

学习如何向量化一个 GridWorld 环境,并在 CPU 上并行训练 30 个 Q-learning 代理,步数达到 180 万……

towardsdatascience.com

我们将逐一讲解构成编码器的每个模块,并学习如何高效地实现它们。特别是,本文的纲要包括:

  • 嵌入层位置编码

  • 多头注意力

  • 残差连接层归一化

  • 位置-wise 前馈网络

免责声明:本文并非这些概念的完整介绍,我们将首先关注实现部分。如有需要,请参阅本文末尾的资源。

一如既往,本文的完整注释代码以及插图笔记本可在 GitHub上获得,如果你喜欢这篇文章,欢迎给仓库加星!

## GitHub — RPegoud/jab: 一系列在 JAX 中实现的基础深度学习模型

在 JAX 中实现的一系列基础深度学习模型 — GitHub — RPegoud/jab: 一系列…

github.com

主要参数

在我们开始之前,我们需要定义几个在编码器模块中发挥重要作用的参数:

  • 序列长度 (seq_len): 序列中的标记或词的数量。

  • 嵌入维度 (embed_dim): 嵌入的维度,换句话说,就是用来描述单个标记或词的数值数量。

  • 批量大小 (batch_size): 输入批量的大小,即同时处理的序列数量。

我们的编码器模型的输入序列通常是**(batch_size,** seq_len**)**的形状。在本文中,我们将使用batch_size=32seq_len=10,这意味着我们的编码器将同时处理 32 个 10 词的序列。

关注每一步处理中的数据形状将使我们更好地可视化和理解数据在编码器块中的流动。以下是我们编码器的高级概述,我们将从底部开始,介绍嵌入层位置编码

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Transformer 编码器块的表示(由作者制作)

嵌入层和位置编码

如前所述,我们的模型接受批处理的令牌序列作为输入。生成这些令牌可能像收集数据集中一组唯一词汇并为每个词汇分配一个索引一样简单。然后,我们将采样3210 词序列,并用词汇中的索引替换每个词。这一过程将为我们提供一个形状为**(batch_size,** seq_len**)**的数组,正如预期的那样。

我们现在准备开始使用编码器。第一步是为我们的序列创建“位置嵌入”。位置嵌入是词嵌入位置编码

词嵌入

词嵌入使我们能够编码词汇中意义语义关系。在本文中,嵌入维度固定为64。这意味着每个词由一个64 维向量表示,从而具有相似意义的词具有相似的坐标。此外,我们可以操作这些向量来提取词之间的关系,如下所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从词嵌入派生的类比示例(图片来自 developers.google.com)

使用 Haiku,生成可学习的嵌入就像调用一样简单:

hk.Embed(vocab_size, embed_dim)

这些嵌入将在模型训练期间与其他可学习的参数一起更新(稍后会详细介绍)。

位置编码

与递归神经网络不同,Transformers 无法根据共享的隐藏状态推断令牌的位置,因为它们缺乏递归卷积结构。因此,引入了位置编码,这些向量传达了令牌在输入序列中的位置

从本质上讲,每个令牌被分配一个由交替的正弦和余弦值组成的位置向量。这些向量的维度与词嵌入相匹配,以便两者可以相加。

特别是,原始的 Transformer 论文使用了以下函数:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

位置编码函数(转载自“Attention is all you need”,Vaswani 等,2017)

下面的图使我们能够进一步理解位置编码的功能。让我们来看一下最上面图的第一行,我们可以看到交替的零和一序列。实际上,行表示序列中一个 token 的位置(pos 变量),而列表示嵌入维度(i 变量)。

因此,当 pos=0 时,之前的方程对偶数嵌入维度返回 sin(0)=0,对奇数维度返回 cos(0)=1

此外,我们看到相邻的行具有相似的值,而第一行和最后一行则差异很大。这一特性有助于模型评估序列中单词之间的距离以及它们的顺序

最后,第三个图表示位置编码和嵌入的总和,这就是嵌入块的输出。

单词嵌入和位置编码的表示,seq_len=16 和 embed_dim=64(由作者制作)

使用 Haiku,我们将嵌入层定义如下。与其他深度学习框架类似,Haiku 允许我们定义自定义模块(此处为 hk.Module),以存储可学习的参数定义模型组件的行为

每个 Haiku 模块需要有一个 __init____call__ 函数。在这里,call 函数简单地使用 hk.Embed 函数和位置编码计算嵌入,然后对其进行求和。

位置编码函数使用 JAX 功能,如 vmaplax.cond 来提高性能。如果你对这些函数不熟悉,可以查看我的上一篇文章,那里对这些函数进行了更深入的介绍。

简而言之,vmap 允许我们为单个样本定义一个函数并将其向量化,以便它可以应用于数据批次in_axes 参数用于指定我们要遍历 dim 输入的第一个轴,即嵌入维度。另一方面,lax.cond 是 XLA 兼容版本的 Python if/else 语句。

自注意力和多头注意力

注意力旨在计算序列中每个单词的重要性相对于输入单词。例如,在句子中:

“那只黑猫跳上沙发,躺下并入睡,因为它累了。”

词语“”对于模型来说可能会相当模糊,因为从技术上讲,它可以指代“”或“沙发”。一个经过良好训练的注意力模型能够理解“”指的是“”,并因此为句子的其余部分分配相应的注意力值。

本质上,注意力值可以视为权重,描述了某个单词在给定输入上下文中的重要性。例如,“跳跃”一词的注意力向量会对“”(跳跃了什么?)、“”和“沙发”(跳跃到哪里?)等词具有较高的值,因为这些词与其上下文相关

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意力向量的可视化表示(由作者制作)

在 Transformer 论文中,注意力是使用缩放点积注意力计算的。其公式总结如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

缩放点积注意力(重现自“Attention is all you need”,Vaswani et al. 2017

在这里,Q、K 和 V 分别代表查询、键。这些矩阵是通过将学习到的权重向量 WQ、WK 和 WV 与位置嵌入相乘得到的。

这些名称主要是抽象概念,用于帮助理解信息在注意力块中的处理和加权方式。它们暗指检索系统的词汇[2](例如,在 YouTube 上搜索视频)。

这里是一个直观的解释:

  • 查询:它们可以被理解为关于序列中所有位置的“一组问题”。例如,询问一个单词的上下文并试图识别序列中最相关的部分。

  • :它们可以被视为包含查询交互的信息,查询与键之间的兼容性决定了查询应该给予对应值多少注意力。

  • :匹配的键和查询使我们能够决定哪些键是相关的,值是与键配对的实际内容。

在下图中,查询是 YouTube 搜索,键是视频描述和元数据,而值是相关联的视频。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查询、键、值概念的直观表示(由作者制作)

在我们的情况下,查询、键和值来自于同一来源(因为它们是从输入序列派生的),因此被称为自注意力

注意力分数的计算通常是多次并行执行的,每次使用部分嵌入。这一机制被称为“多头注意力”,使每个头可以并行地学习数据的几种不同表示,从而形成更强健的模型。

单个注意力头通常处理形状为 (batch_size, seq_len, d_k) 的数组,其中 d_k 可以设置为头数与嵌入维度的比率(d_k = n_heads/embed_dim)。这样,连接每个头的输出就能方便地得到形状为**(batch_size, seq_len, embed_dim**)的数组,作为输入。

注意力矩阵的计算可以分解为几个步骤:

  • 首先,我们定义可学习的权重向量 WQ、WK 和 WV。这些向量的形状为(n_heads, embed_dim, d_k)。

  • 同时,我们将位置嵌入权重向量相乘。我们得到形状为(batch_size, seq_len, d_k)的 Q、K 和 V 矩阵。

  • 然后,我们对 Q 和 K(转置)的点积进行缩放。这个缩放操作包括将点积的结果除以 d_k 的平方根,并在矩阵的行上应用 softmax 函数。因此,对于输入的令牌(即一行),注意力分数总和为一,这有助于防止值变得过大而减慢计算速度。输出的形状为 (batch_size, seq_len, seq_len)。

  • 最后,我们将上一操作的结果与 V 进行点乘,输出的形状为 (batch_size, seq_len, d_k)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意块内部的矩阵操作的可视化表示(作者制作)

  • 然后,每个注意力头的输出可以串联起来形成一个形状为(batch_size, seq_len, embed_dim)的矩阵。Transformer 论文还在多头注意力模块的最后添加了一个线性层,用于汇聚组合来自所有注意力头的学习表示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

多头注意力矩阵的串联和线性层(作者制作)

在 Haiku 中,多头注意力模块可以如下实现。__call__函数遵循与上述图表相同的逻辑,而类方法利用 JAX 实用程序,如vmap(在不同注意力头和矩阵上向量化我们的操作)和tree_map(在权重向量上映射矩阵点积)。

残差连接和层归一化

正如你在 Transformer 图中所注意到的,多头注意力块和前馈网络之后跟着残差连接层归一化

残差连接或跳跃连接

残差连接是解决梯度消失问题的标准解决方案,即当梯度变得太小以至于无法有效更新模型参数时。

由于这个问题在特别深的架构中自然而然地出现,所以残差连接被用在各种复杂模型中,比如在计算机视觉中的ResNetKaiming et al,2015 年),在强化学习中的AlphaZeroSilver et al,2017 年),当然还有Transformers

在实践中,残差连接简单地将特定层的输出直接转发到下一层,跳过一个或多个层。例如,围绕多头注意力的残差连接相当于将多头注意力的输出与位置嵌入求和。

这使得梯度在反向传播过程中更有效地流动,通常可以导致更快的收敛和更稳定的训练

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Transformer 中残差连接的表示(由作者制作)

层归一化

层归一化有助于确保通过模型传播的值不会“爆炸”(趋向无穷大),这种情况在注意力模块中很容易发生,因为在每次前向传递中会有多个矩阵相乘。

与在批次维度上进行归一化并假设均匀分布的批量归一化不同,层归一化在特征上进行归一化。此方法适用于句子批次,因为每个句子可能由于不同的意义词汇而具有独特的分布

通过在特征上进行归一化,例如嵌入注意力值,层归一化将数据标准化为一致的尺度,而不会混淆不同句子的特征,保持每个句子的独特分布。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Transformer 中层归一化的表示(由作者制作)

层归一化的实现非常简单,我们初始化可学习的参数 alpha 和 beta,并在所需的特征轴上进行归一化。

位置-wise 前馈网络

编码器中我们需要覆盖的最后一个组件是位置-wise 前馈网络。这个全连接网络以注意力块的归一化输出作为输入,用于引入非线性并增加模型的容量以学习复杂的函数。

它由两个密集层组成,中间隔着一个 gelu 激活函数

在这个模块之后,我们有另一个残差连接和层归一化,以完成编码器。

总结

就这样!到现在你应该对 Transformer 编码器的主要概念很熟悉了。这是完整的编码器类,请注意,在 Haiku 中,我们为每一层分配了一个名称,以便学习参数分开且易于访问。__call__函数很好地总结了我们编码器的不同步骤:

要在实际数据上使用此模块,我们必须将 hk.transform 应用于封装编码器类的函数。确实,你可能会记得 JAX 采用了函数式编程范式,因此,Haiku 遵循相同的原则。

我们定义了一个包含编码器类实例的函数,并返回前向传递的输出。应用 hk.transform 返回一个转换对象,具有两个函数:initapply

前者使我们能够用一个随机键和一些虚拟数据初始化模块(请注意这里我们传递的是一个形状为batch_size, seq_len的零数组),而后者则允许我们处理真实数据。

# Note: the two following syntaxes are equivalent
# 1: Using transform as a class decorator
@hk.transform
def encoder(x):
  ...
  return model(x) 

encoder.init(...)
encoder.apply(...)

# 2: Applying transfom separately
def encoder(x):
  ...
  return model(x)

encoder_fn = hk.transform(encoder)
encoder_fn.init(...)
encoder_fn.apply(...)

在下一篇文章中,我们将完成 Transformer 架构,通过添加一个 解码器,它重用了我们迄今为止介绍的大部分模块,并学习如何使用 Optax 训练模型 以完成特定任务!

结论

感谢你读到这里,如果你对代码感兴趣,可以在 GitHub 上找到完整的注释以及额外的细节和使用玩具数据集的示例。

[## GitHub - RPegoud/jab: 一组用 JAX 实现的基础深度学习模型

一组用 JAX 实现的基础深度学习模型 - GitHub - RPegoud/jab: 一组…

github.com](https://github.com/RPegoud/jab?source=post_page-----791d31b4f0dd--------------------------------)

如果你想更深入了解 Transformers,以下部分包含了一些帮助我撰写本文的文章。

下次见 👋

参考文献和资源:

[1] Attention is all you need (2017), Vaswani 等,谷歌

[2] 注意机制中的键、查询和值到底是什么?*(2019)*Stack Exchange

[3] 图解 Transformer(2018), Jay Alammar

[4] Transformer 模型中位置编码的温和介绍(2023), Mehreen Saeed,Machine Learning Mastery

图片来源

实施人工智能就像买车和开车(但有所不同)

原文:towardsdatascience.com/implementing-ai-is-like-buying-and-driving-a-car-but-different-1e85f6afcc92?source=collection_archive---------16-----------------------#2023-01-18

一个帮助解释常见误区和陷阱给管理层的类比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Koen Peters

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 1 月 18 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实施人工智能的常见误区和陷阱 — 作者:Babette Huisman

到现在,大多数公司已经开始涉足 AI。然而,只有少数的举措似乎最终成功实施。根据 Gartner 的最新 AI 调查,54%的 AI 项目进入生产阶段[1]。根据我作为数据科学顾问的个人经验,进入生产阶段的项目要少得多。鉴于 Gartner 报告还提到 40%的受调查公司部署了成千上万的模型,这似乎并没有很好地代表那些刚刚开始涉足 AI 的公司。这些公司在整个组织中对 AI 的知识不足。决策者是其中的一层。这种知识的缺乏导致了不良决策、解决方案未能达到预期以及对所需努力的低估。为了帮助决策者提出正确的问题,并最终做出良好的、知情的决策,我提出了一个类比,解释了实施 AI 的一些复杂性、误区和陷阱。通过在这里分享这个类比,我希望也能帮助其他人。让我们深入探讨一下吧!

实施 AI 就像买车和驾驶汽车(但有所不同)

陷阱:出色的销售推介

不论销售推介多么出色或汽车看起来多么华丽,你都应该事先检查几个方面:

  • 你是否检查了汽车是否适合你的车道或车库?类似地,你也应该检查 AI 是否适合你要解决的问题。尽管这看起来是显而易见的,但利益相关者往往会为了使用特定技术而推动解决方案。是的,AI 是过去十年的热门词汇,那些首先掌握其实施的人期望获得巨额收益。但如果你的解决方案不适合你的问题,这种期望将永远无法实现。

提示:在开始使用 AI 之前,你应该先尝试一些更简单的方法。比如增加更多的业务逻辑,更改或自动化部分工作流程等。

  • 汽车在不同地形上的表现如何?它能否在草地、土路或越野路上行驶?类似地,你要了解你的 AI 有多么健壮和公平。它在数据的不同分布中表现是否一样?例如,不同的年龄组、特定类别如性别等。尽管你可能期望 AI 在各方面表现良好,但最近的历史告诉我们,这不一定总是如此[2]。这可能对你的业务和受 AI 结果影响的人造成严重后果。确定你的模型将用于什么,因此了解其要求是非常重要的。

  • 汽车的马力能告诉你一些关于它行驶速度的信息。然而,没有足够的扭矩,你的车将无法加速并达到速度。对于 AI 来说,准确率能告诉你一些关于其性能的信息。然而,当 100 个案例中有 1 个代表欺诈或病人时,AI 可能“预测”没有人是欺诈/病人,准确率达到 99%。这样的模型实际上是无用的。因此,查看其他性能指标也是很重要的,以便对你的 AI 性能有一个坚实的理解。例如,“召回率”表示正确识别的实际欺诈/病人的百分比[3]。在这种情况下,召回率将为零,表明 AI 模型的表现不如预期。

提示:数据科学家受过训练,能够解释这些指标,并应能够为你提供有关 AI 性能的建议。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

100 个中需要被识别的一个 — 作者:巴贝特·赫伊斯曼

总结一下,如果你听说了一个很棒的解决方案,深入了解一下,看看它是否真的能解决你的问题。

陷阱:成本与收益

无论 AI 解决方案多么出色,如果没有对你尝试改进的过程的基线测量,你无法判断收益是否超过成本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

车速表遇见 KPI 仪表 — 作者:巴贝特·赫伊斯曼

  • 在你买车之前,你会想检查里程表(以公里/英里计算的行驶距离)和导航系统。你可能想跟踪你在特定时间段内行驶了多少公里,并且是否接近你的目标或目的地。如果你驾驶的次数非常少,那么拥有一辆车的成本可能不会超过其收益。项目失败往往是因为无法量化额外的收益与成本,使决策者只能猜测,有时甚至会放弃本来很好的项目。因此,在实施 AI 之前,你需要对过程有一个良好的理解,了解该过程当前的表现,并设定一个改进过程的目标。你将收集数据,定义、计算并跟踪过程 KPI,以便在实施 AI 解决方案后,你可以监控过程的改进程度或预计达到某个目标的时间。这些指标反过来可以让你计算投资是否值得,并让决策者对(继续或停止)项目做出明智的决策。

能够沟通结果、成本和效益对于做出项目决策至关重要。

神话:AI 是“设定即忘”

对于不熟悉 AI 的人来说,一个常见的误解是,一旦你实施了解决方案,你就完成了,它可以正常工作,不需要偶尔检查一下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

警报:当你的 AI 表现不如预期时 — 作者:巴贝特·赫伊斯曼

  • 就像汽车一样,AI 也需要维护。然而,与汽车不同的是,AI 通常没有预装的仪表盘来显示警告灯,也不会发出奇怪的噪音。如果你希望获得通知(而且你应该这样做),你的数据团队需要建立自己的仪表盘,并在 AI 显示性能下降的迹象时进行监控或发出警报。你甚至可以实施类似于“定期车辆检查”的措施,让你的 AI 通过检查(或需要进行某些调整以便通过检查),以确保它能够持续在生产中运行。此外,AI 是一款软件,就像其他任何软件产品一样,你可能需要在有新功能和安全措施时进行更新。

让 AI 在生产环境中成功运行需要你数据团队的持续努力。

神话:AI 是即插即用的。

不幸的是,你不能只是获取一个 AI,启动它并期望它能正常工作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

gaspump 与你自己的数据管道 — 作者:Babette Huisman

  • 使用汽车时,你可以开车去加油站,加注正确类型的汽油,然后就可以出发了。(或者对于电动车,你可以直接插上电源)。AI 解决方案也需要燃料,即数据。然而,与汽车不同的是,公开可用的“加油站”并不多,你不能从中获得正确处理的数据。你必须自己收集/挖掘数据,然后建立自己的数据精炼工厂(ETL 过程、数据管道等)。获得可用数据所需的努力往往被低估了。大型公司有专门的团队专注于获取数据、清理数据、提高数据质量(最好是在数据收集阶段)并确保数据代表现实。此外,更重要的是,数据随着时间的推移而变化,你需要不断监控你的数据管道,并调整数据处理或 AI 引擎,以确保它持续接收可以操作的“燃料”。

获取正确类型和质量的数据也需要你数据团队的持续努力。

陷阱和神话:AI 替代人或使人变得冗余。

另一个常见的误解,如果处理不当,会对公司产生负面长期影响。

  • 今天的汽车使驾驶变得更加轻松,但豪华车的车道辅助或自动驾驶功能并不意味着驾驶员不再需要掌握方向盘!(至少现在是这样)。同样,拥有 AI 解决方案并不意味着你不再需要员工。你应该在可能的情况下以不同的方式使用这些员工。他们的工作的一部分可以包括为公司增加更高价值的活动,例如制定未来目标、投资客户关系或解决 AI 无法处理的复杂问题。另一部分工作应包括检查 AI 完成的随机样本。这被称为“人类参与”(‘human in the loop’)[4],不仅为 AI 提供了宝贵的反馈,从而使其更准确,也为员工提供了额外的“安全”层,能够发现监控仪表板可能遗漏的异常行为。此外,如果员工能够监督他们的 AI 同事并通过纠正其行为来不断改进 AI,将增加信任和采用度。在这里,观察者间的可靠性是实现最佳结果的关键。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

人类参与 — 作者:Babette Huisman

  • 就像你仍然需要坐在方向盘后面一样,你还需要知道如何驾驶。如果因为某种原因你不能使用 AI,并且公司里没有人能手动完成工作,这对公司将是有害的。你希望这些业务流程的专业知识留在公司里,最好的办法是让员工“监督”AI 或解决上述更复杂的案例。此外,每当你的监控仪表板突出显示一个问题时,公司内拥有专家知识来确定和实施所需的修复是保持 AI 在生产中运行的关键。

一般来说,你不希望失去公司内的专家知识,因为这些知识可以帮助指导公司的活动。

考虑到这里描述的要点,你离成功实施 AI 并真正让其加速业务又近了一步。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

终于可以加速的汽车 — 作者:Babette Huisman

我相信还有更多的汽车与 AI 的类比,如果你想到一个好的,请在下面的评论中告诉我!此外,我想感谢 Babette Huisman 提供的精彩插图(这些插图在本文中经她许可使用)以及 Maikel Grobbe 的建设性批评。他们都帮助提升了这篇文章的水平。

参考资料:

  1. Gartner AI 调查 2022

  2. 有偏见或不稳定的 AI

  3. 精确度和召回率

  4. 环中人

在 PyTorch 中实现自定义损失函数

原文:towardsdatascience.com/implementing-custom-loss-functions-in-pytorch-50739f9e0ee1?source=collection_archive---------4-----------------------#2023-01-16

使用 MNIST 数据集理解 PyTorch 中自定义损失函数的理论和实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Marco Sanguineti

·

关注 发表在 Towards Data Science ·12 min 阅读·2023 年 1 月 16 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Markus WinklerUnsplash 提供

介绍

在机器学习中,损失函数是一个关键组成部分,用于衡量预测输出与实际输出之间的差异。它在模型训练中起着至关重要的作用,因为它通过指示模型应改进的方向来指导优化过程。损失函数的选择依赖于具体任务和数据类型。本文将深入探讨 PyTorch 中自定义损失函数的理论和实现,以 MNIST 数据集的数字分类为例。

MNIST 数据集是一个广泛使用的图像分类数据集,包含 70,000 张手写数字图像,每张图像的分辨率为 28x28 像素。任务是将这些图像分类为 10 个数字中的一个(0–9)。该任务旨在训练一个模型,使其能够准确地分类新的手写数字图像,基于 MNIST 数据集中提供的训练示例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Carlos Muza 拍摄,来源于 Unsplash

处理这个任务的一个典型方法是使用多类逻辑回归模型,即软最大分类器。软最大函数将模型的输出映射到 10 个类别的概率分布上。交叉熵损失通常作为这种模型的损失函数。交叉熵损失计算预测概率分布与实际概率分布之间的差异。

然而,在某些情况下,交叉熵损失可能不是特定任务的最佳选择。例如,考虑一个场景,其中误分类某些类别的代价远高于其他类别。在这种情况下,有必要使用自定义损失函数来考虑每个类别的相对重要性。

在这篇文章中,我将展示如何为 MNIST 数据集实现自定义损失函数,其中误分类数字 9 的代价远高于其他数字。我们将使用 Pytorch 作为框架,首先讨论自定义损失函数背后的理论,然后展示如何使用 Pytorch 实现自定义损失函数。最后,我们将使用自定义损失函数在 MNIST 数据集上训练一个线性模型,并评估模型的性能。

自定义损失函数:原因

实现自定义损失函数非常重要,原因有几个:

  1. 特定问题:损失函数的选择依赖于具体任务和数据类型。可以设计自定义损失函数,以更好地适应问题的特征,从而提高模型性能。

  2. 类别不平衡:在许多实际数据集中,每个类别的样本数量可能差异很大。可以设计自定义损失函数以考虑类别不平衡,并对不同类别分配不同的代价。

  3. 成本敏感:在一些任务中,错误分类某些类别的成本可能远高于其他类别。可以设计自定义损失函数以考虑每个类别的相对重要性,从而得到一个更鲁棒的模型。

  4. 多任务学习:自定义损失函数可以设计成同时处理多个任务。这在需要一个模型执行多个相关任务的情况下非常有用。

  5. 正则化:自定义损失函数也可以用于正则化,这有助于防止过拟合并提高模型的泛化能力。

  6. 对抗训练:自定义损失函数还可以用于训练模型,使其对对抗攻击具有鲁棒性。

总结来说,自定义损失函数可以提供一种更好地优化模型以适应特定问题的方法,并能提供更好的性能和泛化能力。

PyTorch 中的自定义损失函数

MNIST 数据集包含 70,000 张手写数字图像,每张图像的分辨率为 28x28 像素。任务是将这些图像分类为 10 个数字中的一个(0–9)。这种任务的典型方法是使用多类逻辑回归模型,即 softmax 分类器。softmax 函数将模型的输出映射到 10 个类别的概率分布上。交叉熵损失通常用作这种类型模型的损失函数。

交叉熵损失计算预测概率分布和实际概率分布之间的差异。预测概率分布是通过对模型的输出应用 softmax 函数获得的。实际概率分布是一个 one-hot 向量,其中对应正确类别的元素值为 1,其他元素的值为 0。交叉熵损失定义为:

L = -∑(y_i * log(p_i))

其中 y_i 是类别 i 的实际概率,p_i 是类别 i 的预测概率。

然而,在某些情况下,交叉熵损失可能不是特定任务的最佳选择。例如,考虑一种场景,其中错误分类某些类别的成本远高于其他类别。在这种情况下,有必要使用自定义损失函数来考虑每个类别的相对重要性。

在 PyTorch 中,自定义损失函数可以通过创建nn.Module类的子类并重写forward方法来实现。forward方法以预测输出和实际输出为输入,并返回损失的值。

这里是一个针对 MNIST 分类任务的自定义损失函数示例,其中错误分类数字 9 的成本远高于其他数字:

class CustomLoss(nn.Module):
    def __init__(self):
        super(CustomLoss, self).__init__()

    def forward(self, output, target):
        target = torch.LongTensor(target)
        criterion = nn.CrossEntropyLoss()
        loss = criterion(output, target)
        mask = target == 9
        high_cost = (loss * mask.float()).mean()
        return loss + high_cost

在这个示例中,我们首先使用 nn.CrossEntropyLoss() 函数计算交叉熵损失。接下来,我们创建一个掩码,对于属于 9 类的样本掩码值为 1,对于其他样本掩码值为 0。然后,我们计算属于 9 类的样本的平均损失。最后,我们将这个高成本的损失添加到原始损失中,以获得最终损失。

要使用自定义损失函数,我们需要实例化它并将其作为参数传递给训练循环中的优化器的 criterion 参数。以下是如何使用自定义损失函数来训练 MNIST 数据集模型的示例:

import torch.nn as nn
import torch
from torchvision import datasets, transforms
from torch import nn, optim
import torch.nn.functional as F
import torchvision
import os

class CustomLoss(nn.Module):
    def __init__(self):
        super(CustomLoss, self).__init__()

    def forward(self, output, target):
        target = torch.LongTensor(target)
        criterion = nn.CrossEntropyLoss()
        loss = criterion(output, target)
        mask = target == 9
        high_cost = (loss * mask.float()).mean()
        return loss + high_cost

# Load the MNIST dataset
train_loader = torch.utils.data.DataLoader(
  torchvision.datasets.MNIST('/files/', train=True, download=True,
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.1307,), (0.3081,))
                             ])),
  batch_size=32, shuffle=True)

test_loader = torch.utils.data.DataLoader(
  torchvision.datasets.MNIST('/files/', train=False, download=True,
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.1307,), (0.3081,))
                             ])),
  batch_size=32, shuffle=True)

# Define the model, loss function and optimizer
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x)

network = Net()
optimizer = optim.SGD(network.parameters(), lr=0.01,
                      momentum=0.5)
criterion = CustomLoss()

# Training loop
n_epochs = 10

train_losses = []
train_counter = []
test_losses = []
test_counter = [i*len(train_loader.dataset) for i in range(n_epochs + 1)]

if os.path.exists('results'):
  os.system('rm -r results')

os.mkdir('results')

def train(epoch):
  network.train()
  for batch_idx, (data, target) in enumerate(train_loader):
    optimizer.zero_grad()
    output = network(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()
    if batch_idx % 1000 == 0:
      print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
        epoch, batch_idx * len(data), len(train_loader.dataset),
        100\. * batch_idx / len(train_loader), loss.item()))
      train_losses.append(loss.item())
      train_counter.append(
        (batch_idx*64) + ((epoch-1)*len(train_loader.dataset)))
      torch.save(network.state_dict(), 'results/model.pth')
      torch.save(optimizer.state_dict(), 'results/optimizer.pth')

def test():
  network.eval()
  test_loss = 0
  correct = 0
  with torch.no_grad():
    for data, target in test_loader:
      output = network(data)
      test_loss += criterion(output, target).item()
      pred = output.data.max(1, keepdim=True)[1]
      correct += pred.eq(target.data.view_as(pred)).sum()
  test_loss /= len(test_loader.dataset)
  test_losses.append(test_loss)
  print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
    test_loss, correct, len(test_loader.dataset),
    100\. * correct / len(test_loader.dataset)))

test()
for epoch in range(1, n_epochs + 1):
  train(epoch)
  test()

这段代码实现了一个自定义损失函数,用于 PyTorch 中的 MNIST 数据集。MNIST 数据集包含 70,000 张手写数字图像,每张图像的分辨率为 28x28 像素。任务是将这些图像分类为 10 个数字(0–9)中的一个。

第一个代码块通过继承 PyTorch 的 nn.Module 创建了一个名为 “CustomLoss” 的自定义损失函数。它有一个 forward 方法,接受两个输入:模型的输出和目标标签。forward 方法首先将目标标签转换为长整型张量。然后,它创建一个内置的 PyTorch 交叉熵损失函数的实例,并使用它计算模型输出与目标标签之间的损失。接下来,它创建一个掩码,以识别目标标签是否等于 9,然后将损失乘以这个掩码,并计算结果张量的平均值。最后,它返回原始损失与高成本损失的平均值之和。

下一个代码块使用 PyTorch 的内置数据加载工具加载 MNIST 数据集。train_loader 加载训练数据集并对图像应用指定的变换,例如将图像转换为张量和规范化像素值。test_loader 加载测试数据集,并应用相同的变换。

以下代码块通过继承 PyTorch 的 nn.Module 定义了一个卷积神经网络(CNN),称为 “Net”。该 CNN 包含 2 个卷积层、2 个线性层,以及一些用于正则化的 dropout 层。Net 类的 forward 方法按顺序应用卷积层和线性层,通过 ReLU 激活函数和最大池化层传递输出。它还对输出应用 dropout 层,并返回最终输出的 log-softmax。

下一个代码块创建了一个 Net 类的实例,一个优化器(随机梯度下降),以及一个自定义损失函数的实例。

最后一块代码是训练循环,其中模型训练了 10 个周期。在每个周期中,模型遍历训练数据集,将图像通过网络,使用自定义损失函数计算损失并反向传播梯度。然后,它使用优化器更新模型的参数。同时,它跟踪训练损失和测试损失,并定期将当前损失打印到控制台。此外,它还创建了一个名为“results”的新目录来存储训练过程的结果和输出。

import matplotlib.pyplot as plt

fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

自定义损失趋势 — 图片由作者提供

这段代码正在为 MNIST 数据集创建自定义损失函数的图形。该图形将显示训练集和测试集的自定义损失。

它首先导入了 Matplotlib 库,这是一个用于 Python 的绘图库。然后,使用plt.figure()函数创建了一个指定大小的图形对象。

下一行代码使用plt.plot()函数绘制训练集的自定义损失。它使用train_countertrain_losses变量分别作为 x 轴和 y 轴的值。图的颜色通过color参数设置为蓝色。

然后,它使用plt.scatter()函数绘制测试集的自定义损失。它使用test_countertest_losses变量分别作为 x 轴和 y 轴的值。图的颜色通过color参数设置为红色。

plt.legend()函数为图形添加图例,指明哪个图代表训练损失,哪个图代表测试损失。loc参数设置为’upper right’,这意味着图例将位于图形的右上角。

plt.xlabel()plt.ylabel()函数分别为图形的 x 轴和 y 轴添加标签。x 轴标签设置为’number of training examples seen’,y 轴标签设置为’Custom loss’。

最后,使用plt.show()函数显示图形。

这段代码将显示一个图形,展示自定义损失函数在训练样本中的变化情况。蓝色线条代表训练集的自定义损失,红色点代表测试集的自定义损失。这个图形将帮助你观察自定义损失函数在训练过程中的表现,并评估模型的性能。

examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
with torch.no_grad():
  output = network(example_data)
fig = plt.figure()
for i in range(6):
  plt.subplot(2,3,i+1)
  plt.tight_layout()
  plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
  plt.title("Prediction: {}".format(
    output.data.max(1, keepdim=True)[1][i].item()))
  plt.xticks([])
  plt.yticks([])

plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

测试集样本和预测 — 图片由作者提供

这段代码显示了来自测试集的 6 张图像及其对应的训练网络的预测结果。

它开始使用enumerate()函数循环遍历 test_loader,这是一个按批次加载测试数据集的迭代器。使用next()函数获取测试集的第一个批次样本。

example_data 变量包含图像,example_targets 变量包含相应的标签。

然后使用 PyTorch 的 torch.no_grad() 函数,该函数用于暂时将 requires_grad 标志设置为 False。它将减少内存使用并加速计算,但也不会跟踪操作。

接下来的代码块使用 plt.figure() 函数创建一个新的图形对象。然后,使用 for 循环遍历测试集中的前 6 个示例。对于每个示例,它使用 plt.subplot() 函数在当前图形中创建一个子图。plt.tight_layout() 函数用于调整子图之间的间距。

然后它使用 plt.imshow() 函数在当前子图中显示图像。cmap 参数设置为 ‘gray’ 以灰度显示图像,interpolation 参数设置为 ‘none’ 以无插值显示图像。

plt.title() 函数用于为当前子图添加标题。标题显示了网络对当前示例的预测结果。网络的输出通过 output.data.max(1, keepdim=True)[1] 得到预测类别的索引。[i].item() 提取了预测类别的整数值。

plt.xticks()plt.yticks() 函数分别用于去除当前子图的 x 和 y 轴刻度。

最后,使用 plt.show() 函数来显示图像。此代码将显示一个包含 6 张来自测试集的图像及其由训练网络生成的预测结果的图形。图像以灰度显示且没有任何插值,预测的类别以标题形式显示在每张图像上方。这是一个有用的工具,可以用来可视化模型在测试集上的表现,识别潜在问题或误分类。

问候

在本文中,我们讨论了 PyTorch 中自定义损失函数的理论和实现,使用 MNIST 数据集进行数字分类作为例子。我们展示了如何通过子类化 nn.Module 类并重写 forward 方法来创建自定义损失函数。我们还提供了一个如何使用自定义损失函数在 MNIST 数据集上训练模型的示例。在某些类别误分类的成本远高于其他类别的场景中,自定义损失函数可能非常有用。需要注意的是,实施自定义损失函数时应谨慎,因为它们可能对模型性能产生重大影响。

加入 Medium 会员

如果你喜欢这篇文章并希望继续了解更多相关内容,我邀请你通过此链接加入 Medium 会员。

[## 通过我的推荐链接加入 Medium — Marco Sanguineti

阅读 Marco Sanguineti 的每一篇故事(以及 Medium 上成千上万其他作家的故事)。对文化的投资是最好的……

marcosanguineti.medium.com

成为会员后,你将能够访问更多种类的高质量内容,并获得专属会员故事的访问权限,同时你还将支持像我这样独立的作家和创作者。此外,作为会员,你可以高亮你喜欢的段落,保存故事以便稍后阅读,并获得个性化的阅读推荐。今天就注册,和我们一起继续探索这个话题和其他话题吧。

感谢你的支持!下次见,

马尔科

使用 fastai 实现深度学习——图像分类

原文:towardsdatascience.com/implementing-deep-learning-using-fastai-eff2fa05449e

快速而轻松地入门深度学习,无需涉猎所有细节

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Wei-Meng Lee

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 4 月 19 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源:NASA 通过 Unsplash

近年来,人工智能 (AI) 受到了广泛关注,尤其是最近几个月 ChatGPT 的发布。人工智能中的基础技术之一是深度学习。深度学习是一种机器学习技术,使用神经网络来学习数据集中特征和标签之间的关系。一个神经网络通常如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所有图片由作者提供

上述各个圆圈称为神经元(或节点)。每个神经元有一个值叫做偏置,每个神经元彼此连接。每个连接都有一个值叫做权重。最左侧的神经元层是输入层(你将数据发送到此处进行预测),而最右侧的层称为输出层(预测结果在此处显示)。神经网络可以有任意多(或少)的隐藏层。

学习深度学习需要了解一些内容:

  • 什么是层、权重和偏置

  • 激活函数

  • 损失函数

  • 优化器

  • 反向传播

此外,还有几种类型的神经网络,例如:

  • 人工神经网络 (ANN)

  • 卷积神经网络 (CNN)

  • 循环神经网络 (RNN)

深度学习初学者通常需要掌握这些概念,然后才能开始构建有效的模型。

这就是 fastai 的作用所在。fastai 是一个深度学习库,允许初学者和从业者快速入门标准的深度学习模型,同时提供定制所构建模型的能力。

在这篇文章中,我将帮助你开始使用 fastai 构建你的第一个深度学习模型。

fastai 由 Jeremy Howard 和 Rachel Thomas 于 2016 年创立,旨在普及深度学习。他们通过提供一个名为“编程人员实用深度学习”(Practical Deep Learning for Coders)的开放在线课程(MOOC)来实现这一目标,该课程唯一的先决条件是掌握 Python 编程语言。来源:en.wikipedia.org/wiki/Fast.ai

fastai 到底是什么?

对于那些熟悉 TensorFlow 的人来说,fastaiPyTorch 的作用就像 KerasTensorFlow 的作用一样——简而言之,它是 PyTorch 的一个封装:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

那么 PyTorch 是什么呢?PyTorch 是一个基于 Torch 库的机器学习框架,用于计算机视觉和自然语言处理等应用,最初由 Meta AI 开发,现在是 Linux 基金会的一部分。它是一个自由开源的软件,发布在修改版 BSD 许可证下(来源:en.wikipedia.org/wiki/PyTorch)。

TensorFlow 另一方面是另一个开源机器学习和人工智能库(由 Google 开发)。它的主要优势在于深度神经网络的训练和推断。由于 TensorFlow 的使用较为复杂,Keras 提供了一个高层次的库,能够在 TensorFlow 之上工作。Keras 使得深度学习对开发者来说更加容易上手。

这意味着,你现在可以构建深度学习神经网络,而不必真正理解神经网络是如何构建的细节。

fastai 的主要目标之一是使深度学习变得 易于接近迅速高效fastai 提供了四个主要应用领域的 API:

  • 视觉

  • 文本

  • 表格和时间序列分析

  • 协同过滤

在本文中,作为对 fastai 的快速入门,我将通过使用 fastai 构建一个视觉模型来识别图像。

安装 fastai

在本文中,我将使用 Jupyter Notebook。要安装 fastai,请在新单元格中输入以下命令:

!pip install fastai

训练图像识别的视觉模型

使用 fastai,你可以非常快速地训练深度学习模型,而无需过多接触底层实现。

虽然我说你可以在不直接接触 fastai 的情况下构建深度学习模型,但如果你对深度学习有一些背景知识——什么是神经网络、权重和偏差是什么、不同类型的神经网络(特别是本文中使用的卷积神经网络),以及如何训练和测试神经网络——那会非常有用。如果你感兴趣,请参考我在 Code Magazine 上关于深度学习工作原理的文章。

## Introduction to Deep Learning

作者:Wei-Meng Lee 发表在:CODE Magazine:2020 年 3 月/4 月 最后更新:2022 年 8 月 31 日 人工智能…

www.codemag.com

使用示例图片

fastai附带了一组你可以下载并用于训练的示例图片。首先,从fastai中导入vision库:

from fastai.vision.all import *

然后,打印出URLs.PETS的值:

URLs.PETS

它将返回以下 URL:

'https://s3.amazonaws.com/fast-ai-imageclas/oxford-iiit-pet.tgz'

这个 URL 指向一个包含一系列猫和狗图片的 tar 文件。

除了PETS项目,URLs对象还包含其他示例图片,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要下载示例图片并解压它,请使用untar_data()函数,如下所示:

path = untar_data(URLs.PETS)/'images'    # path to images
path

图片现在将被扩展到/Users/weimenglee/.fastai/data/oxford-iiit-pet/images目录。

images文件夹中,你可以看到包含各种猫和狗品种的图片列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如你所见,文件名以动物名称开头,后跟一个索引。

使用 ImageDataLoaders 加载图片

images文件夹包含 37 种猫和狗品种的图片,因此你需要一种方法为每张图片分配标签,并将它们放入适当的类别中。对于标签提取,我们将提取文件名中最后一个下划线字符(“_”)之前的部分,并将其用作动物的标签:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们如何对这些图片进行分类?幸运的是,你可以使用ImageDataLoaders类。ImageDataLoaders类可以自动从图片列表中构建验证和训练数据。使用from_name_func()函数,你可以从存储在文件夹中的图片系列中加载训练和验证数据。

首先,你需要定义一个函数来提取图片的标签:

def animal_labels(filename):    
    return filename[:filename.rfind('_')]  # extract the filename until the last underscore

然后,使用ImageDataLoaders类中的from_name_func()函数,并传递以下参数:

 dls = ImageDataLoaders.from_name_func(
    path,                                # path to images 
    get_image_files(path),               # get the list of path for all images
    valid_pct = 0.25,                    # percentages to use for validation
    seed = 42,                           # random seed
    label_func = animal_labels,          # labelling for the images
    item_tfms = Resize(224))             # transform the images to 224x224

dls变量的类型是fastai.data.core.DataLoaders

结果现在可以用于训练。

打印标签

如果你对训练数据集中各种猫狗品种感到好奇,可以使用vocab属性在ImageDataLoaders对象中将它们打印出来:

for vocab in dls.vocab:
    print(vocab)

这里列出了 37 种猫狗品种:

Abyssinian
Bengal
Birman
Bombay
British_Shorthair
Egyptian_Mau
Maine_Coon
Persian
Ragdoll
Russian_Blue
Siamese
Sphynx
american_bulldog
american_pit_bull_terrier
basset_hound
beagle
boxer
chihuahua
english_cocker_spaniel
english_setter
german_shorthaired
great_pyrenees
havanese
japanese_chin
keeshond
leonberger
miniature_pinscher
newfoundland
pomeranian
pug
saint_bernard
samoyed
scottish_terrier
shiba_inu
staffordshire_bull_terrier
wheaten_terrier
yorkshire_terrier

模型训练

由于我们正在构建一个视觉模型,我们将使用vision_learner()函数:

learn = vision_learner(dls, 
                       resnet34, 
                       metrics = error_rate)
learn.fine_tune(1)

vision_learner()函数接受三个参数:

  • 数据加载器

  • 使用的预训练模型

  • 用于评估模型的度量标准

Resnet34是什么?Resnet34 是一个图像分类模型。它是一个结构为 34 层的卷积神经网络(CNN)(因此得名)。Resnet34 在 ImageNet 数据集上进行了预训练,该数据集包含超过 100,000 张图像,涵盖 200 个类别。CNN 通常用于图像分类目的。

在这里,我们使用一个预训练模型——resnet34。预训练模型是已经用其自身数据集训练过的模型。还记得在上一节我们需要将图像调整为 224x224 吗?这是因为resnet34是在这种尺寸的图像上进行训练的。你可能会问为什么不使用更大的图像?其实,虽然更大的图像确实会更好,但这会带来速度和内存需求的增加。

在这个示例中,我们使用迁移学习。迁移学习是一种机器学习方法,其中一个为某个任务开发的模型被用作第二个任务模型的起点。迁移学习减少了你在训练上需要花费的时间。下图展示了迁移学习的工作原理。上部分展示了预训练模型的网络结构。在卷积神经网络(CNN)中,早期的层(从左开始)已经训练来识别基本形状、边缘、颜色等。而最右侧的几层则训练来识别模型已被训练识别的特定对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在迁移学习中,你不需要重新训练整个模型——你可以保留那些能够识别基本形状的早期层,同时保留最右侧的几层以识别你想训练的特定对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要开始迁移学习过程,你可以使用fine_tune()函数:

learn.fine_tune(1)

你指定的1epoch,即模型对你的图像进行训练的次数。

你应该使用的 epoch 数量取决于你有多少时间用于训练。较高的 epoch 需要较多的时间,但结果通常比低 epoch 更准确。另一方面,指定较高的 epoch 意味着模型会重复查看你的图像,这可能导致过拟合。过拟合意味着你的模型现在记住了你的图像,当给它一个之前从未见过的新图像时,它的表现会很差。

训练之后,你应该得到一些关于训练的统计数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

进行预测

我们的模型现在已经训练完成,是时候进行测试了!为此,我下载了一张美国斗牛犬的图像(1024px-Bulldog_inglese.jpg):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来源:en.wikipedia.org/wiki/Bulldog#/media/File:Bulldog_inglese.jpg

将图像保存到与 Jupyter Notebook 相同的目录中。现在,你可以加载该图像并在 Jupyter Notebook 中显示它:

img = PILImage.create('1024px-Bulldog_inglese.jpg')
img.to_thumb(192)

然后,你可以使用 predict() 函数发送进行预测:

animal_type, index, probs = learn.predict(img)
print(f"Predicted animal: {animal_type}.") 
print(f"Predicted animal probability: {probs[index]}")
print(f"Probabilities: {probs}")

predict() 函数返回三个值:

  • 预测标签

  • 一个 torch.Tensor 对象,表示预测标签的索引

  • 每个标签的概率

以下是预测结果:

Predicted animal: american_bulldog.
Predicted animal probability: 0.6161370277404785
Probabilities: tensor([7.9024e-05, 4.4181e-04, 6.9500e-05, 8.0544e-05, 1.8523e-04, 7.2056e-05,
        2.3234e-05, 7.4391e-04, 3.1683e-05, 7.3335e-05, 1.9286e-04, 2.3213e-04,
        6.1614e-01, 4.6759e-03, 2.0838e-02, 4.2569e-04, 7.0168e-02, 4.3025e-05,
        2.1922e-05, 4.6172e-05, 1.0497e-04, 2.4546e-04, 2.3985e-04, 6.9188e-04,
        1.0650e-04, 2.1837e-04, 1.5202e-04, 1.7642e-04, 8.6259e-05, 1.4461e-02,
        2.5644e-01, 2.9418e-04, 4.5159e-04, 1.2193e-04, 1.0669e-02, 4.7959e-04,
        4.7566e-04])

从结果中可以看出,模型正确预测了图像是美国斗牛犬。

如果你喜欢阅读我的文章,并且它对你的职业/学习有帮助,请考虑注册成为 Medium 会员。每月 $5,你可以无限制访问 Medium 上所有文章(包括我的)。如果你通过以下链接注册,我将获得少量佣金(对你没有额外费用)。你的支持意味着我将能花更多时间写像这样的文章。

## 使用我的推荐链接加入 Medium - Wei-Meng Lee

阅读 Wei-Meng Lee 的每一篇故事(以及 Medium 上成千上万其他作家的故事)。你的会员费直接支持…

weimenglee.medium.com

总结

我希望这个快速入门能对如何使用 fastai 提供一些帮助。本文中讨论了一些深度学习的术语,但一开始你不必详细了解它们。你应该专注于让模型训练好,然后使用它进行预测。未来的文章中,我会给你更多如何使用 fastai 构建一些非常有趣的模型的例子。祝你好运!

从零实现 LoRA

原文:towardsdatascience.com/implementing-lora-from-scratch-20f838b046f1?source=collection_archive---------0-----------------------#2023-12-12

如何从零实现 LoRA 以及一些实用技巧

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 马丁·迪特根

·

关注 发表于 Towards Data Science ·17 分钟阅读·2023 年 12 月 12 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由 DALLE 创建的 LoRA 抽象艺术表现

在这篇博客文章中,我将向你展示如何从零开始实现 LoRA。

LoRA,即低秩适配Low-Rank Adaptation)或低秩适配器Low-Rank Adaptors),提供了一种高效且轻量级的方法来微调现有的语言模型。这包括像BERTRoBERTa这样的掩码语言模型,以及像GPTLlamaMistral这样的因果(或聊天机器人)模型。

低秩适配器的主要优点之一是其高效性。通过使用更少的参数,LoRA 显著降低了计算复杂性和内存使用。这使我们能够在消费级 GPU 上训练大型模型,并轻松将我们紧凑(以兆字节为单位)的 LoRA 分发给他人。

此外,LoRA 可以提高泛化性能。通过限制模型复杂性,它们有助于防止过拟合,特别是在训练数据有限的情况下。这导致模型更能适应新的、未见过的数据,或者至少保留其初始训练任务的知识。

此外,低秩适配器可以无缝集成到现有的神经网络架构中。这种集成允许在最小的额外训练成本下进行预训练模型的微调和适应,使其非常适用于迁移学习应用。

我们将首先深入探讨 LoRA 的功能,然后我将演示如何为 RoBERTa 模型从头开始开发它,并使用 GLUESQuAD 基准测试我们的实现,并讨论一般的技巧和改进。

LoRA 的工作原理

LoRA 的基本理念是保持预训练的矩阵(即原始模型的参数)冻结(即保持固定状态),只向原始矩阵添加一个小的 delta,其参数比原始矩阵少。

例如考虑矩阵 W,它可以是完全连接层的参数,或者是 transformer 的自注意机制中的一个矩阵之一:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

显然,如果W*-orig* 的尺寸为 n×m,我们只需初始化一个具有相同尺寸的新的 delta 矩阵进行微调,我们将毫无收获;相反,我们将会增加参数的数量。

这个技巧在于通过从较低维度矩阵 BA 的矩阵乘法构建 ΔW 比原始矩阵更少*“维度化”*。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们首先定义一个秩 r,要显著小于基本矩阵的维度 r≪nr≪m。然后矩阵 Bn×r,矩阵 Ar×m。将它们相乘得到一个具有相同尺寸的 W 矩阵,但是是由较低参数数量构建而成的。

显然,我们希望在训练开始时我们的 delta 是零,这样微调才能像原始模型一样开始。因此,B 通常初始化为全零,A 初始化为随机(通常是正态分布)值。

例如,这可能看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个 LoRA 可能在实际矩阵中如何看的示例

想象一种情况,我们的基础维度是 1024,并且我们选择了 LoRA 的秩 r 为 4,则:

  • W 具有 1024 * 1024 ≈ 100 万个参数

  • AB 每个都有 r * 1024 = 4 * 1024 ≈ 4k 个参数,总共是 8k

  • 因此,我们只需要训练 0.8% 的参数来使用 LoRA 更新我们的矩阵

顺便说一句,在 LoRA 论文中,他们使用 alpha 参数对 delta 矩阵进行加权:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你只是将α设置为你实验的第一个r并微调学习率,通常可以在以后改变r参数,而无需再次微调学习率(至少大致如此)。虽然在我们的实现中可以忽略这一细节,但这是许多其他 LoRA 库(如 Hugging Face 的 PEFT)的常见特性。

实现 LoRA

对于我们的实现,我们希望紧密跟随原始的 LoRA 论文。在那里,他们测试了变换器中实际需要替换哪些矩阵。他们发现,在对 GPT-3 微调任务进行不同策略比较时,仅适配自注意力机制的查询和数值向量就足够了。

注意到现在很多人忽视了这种评估,并允许每个矩阵进行微调,无论任务或模型如何(参见 QLoRA 论文)。

我们的实现将在 PyTorch 中完成,但应该很容易适配到不同的框架中。

对于这篇博客文章,我简化了一些代码,以便更容易阅读,同时仍展示了核心要素。完整代码和一些训练好的 LoRA 权重可以在这里找到:github.com/Montinger/Transformer-Workbench

重新实现自注意力模型

我们希望适配的模型是来自 Huggingface 的 RoBERTa 模型。最直接的方法是重新包装原始的自注意力机制RobertaSelfAttention。新类LoraRobertaSelfAttention将初始化 LoRA 矩阵。所有的 B 矩阵将初始化为零,所有的 A 矩阵将用正态分布的随机数初始化。

class LoraRobertaSelfAttention(RobertaSelfAttention):
    """
    Extends RobertaSelfAttention with LoRA (Low-Rank Adaptation) matrices.
    LoRA enhances efficiency by only updating the query and value matrices.
    This class adds LoRA matrices and applies LoRA logic in the forward method.

    Parameters:
    - r (int): Rank for LoRA matrices.
    - config: Configuration of the Roberta Model.
    """
    def __init__(self, r=8, *args, **kwargs):
        super().__init__(*args, **kwargs)
        d = self.all_head_size

        # Initialize LoRA matrices for query and value
        self.lora_query_matrix_B = nn.Parameter(torch.zeros(d, r))
        self.lora_query_matrix_A = nn.Parameter(torch.randn(r, d))
        self.lora_value_matrix_B = nn.Parameter(torch.zeros(d, r))
        self.lora_value_matrix_A = nn.Parameter(torch.randn(r, d))

给定这些矩阵,我们现在定义新的类方法lora_querylora_value。这些方法计算ΔW矩阵,即BA,并将其添加到原始矩阵中,这些原始矩阵由原始方法queryvalue调用。

class LoraRobertaSelfAttention(RobertaSelfAttention):
    # ...

    def lora_query(self, x):
        """
        Applies LoRA to the query component. Computes a modified query output by adding 
        the LoRA adaptation to the standard query output. Requires the regular linear layer 
        to be frozen before training.
        """
        lora_query_weights = torch.matmul(self.lora_query_matrix_B, self.lora_query_matrix_A)
        return self.query(x) + F.linear(x, lora_query_weights)

    def lora_value(self, x):
        """
        Applies LoRA to the value component. Computes a modified value output by adding 
        the LoRA adaptation to the standard value output. Requires the regular linear layer 
        to be frozen before training.
        """
        lora_value_weights = torch.matmul(self.lora_value_matrix_B, self.lora_value_matrix_A)
        return self.value(x) + F.linear(x, lora_value_weights)

现在是难看的部分:为了使用这些方法,我们必须重写RobertaSelfAttention的原始前向函数。虽然这有点硬编码(参见后续改进讨论),但其实很简单。首先,我们从github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py复制原始前向代码。然后我们将每个query调用替换为lora_query,每个value调用替换为lora_value。函数看起来如下:

class LoraRobertaSelfAttention(RobertaSelfAttention):
    # ...
    def forward(self, hidden_states, *args, **kwargs):
        """Copied from
https://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py
        but replaced the query and value calls with calls to the
        lora_query and lora_value functions.
        We will just sketch of how to adjust this here. 
        Change every call to self.value and self.query in the actual version.
        """
        # original code for query:
        ## mixed_query_layer = self.query(hidden_states)
        # updated query for LoRA:
        mixed_query_layer = self.lora_query(hidden_states)

        # The key has no LoRA, thus leave these calls unchanged
        key_layer = self.transpose_for_scores(self.key(hidden_states))

        # original code for value:
        ## value_layer = self.transpose_for_scores(self.value(hidden_states))
        # updated value for LoRA:
        value_layer = self.transpose_for_scores(self.lora_value(hidden_states))

        # ... (rest of the forward code, unchanged)

轰隆隆,我们完成了:我们的 LoRA 自注意力实现。现在唯一剩下的任务是将原始 RoBERTa 模型中的注意力模块替换出来。

替换模块

很好,我们已经用我们自己的实现替换了自注意力;但是我们如何将这个新类加入旧的 RoBERTa 模型中呢?实质上,我们必须遍历 RoBERTa 模型的每个命名组件,检查它是否是RobertaSelfAttention类,如果是,则替换为LoraRobertaSelfAttention,同时确保保留原始的权重矩阵。

为了实现这一点,我们将编写一个新的包装函数来进行此替换。此外,我们还希望稍后在一些实际任务上对 RoBERTa 模型进行微调。

class LoraWrapperRoberta(nn.Module):
    def __init__(self, task_type, num_classes=None, dropout_rate=0.1, model_id="roberta-large",
                 lora_rank=8, train_biases=True, train_embedding=False, train_layer_norms=True):
        """
        A wrapper for RoBERTa with Low-Rank Adaptation (LoRA) for various NLP tasks.
        - task_type: Type of NLP task ('glue', 'squad_v1', 'squad_v2').
        - num_classes: Number of classes for classification (varies with task).
        - dropout_rate: Dropout rate in the model.
        - model_id: Pre-trained RoBERTa model ID.
        - lora_rank: Rank for LoRA adaptation.
        - train_biases, train_embedding, train_layer_norms: 
            Flags whether to keep certain parameters trainable 
            after initializing LoRA.

        Example:
            model = LoraWrapperRoberta(task_type='glue')
        """
        super().__init__()
        # 1\. Initialize the base model with parameters
        self.model_id = model_id
        self.tokenizer = RobertaTokenizer.from_pretrained(model_id)
        self.model = RobertaModel.from_pretrained(model_id)
        self.model_config = self.model.config

        # 2\. Add the layer for the benchmark tasks
        d_model = self.model_config.hidden_size
        self.finetune_head_norm = nn.LayerNorm(d_model)
        self.finetune_head_dropout = nn.Dropout(dropout_rate)
        self.finetune_head_classifier = nn.Linear(d_model, num_classes)

        # 3\. Set up the LoRA model for training
        self.replace_multihead_attention()
        self.freeze_parameters_except_lora_and_bias()

正如您所见,我们在初始化中调用了两个辅助方法:

  1. self.replace_multihead_attention:这将使用我们之前编写的LoraRobertaSelfAttention替换所有神经网络部分的注意力。

  2. self.freeze_parameters_except_lora_and_bias:这将冻结所有主要参数,以便在训练中仅应用于 LoRA 参数以及我们希望保持可训练的其他偏置和层归一化参数。

class LoraWrapperRoberta(nn.Module):
    # ...

    def replace_multihead_attention_recursion(self, model):
        """
        Replaces RobertaSelfAttention with LoraRobertaSelfAttention in the model.
        This method applies the replacement recursively to all sub-components.

        Parameters
        ----------
        model : nn.Module
            The PyTorch module or model to be modified.
        """
        for name, module in model.named_children():
            if isinstance(module, RobertaSelfAttention):
                # Replace RobertaSelfAttention with LoraRobertaSelfAttention
                new_layer = LoraRobertaSelfAttention(r=self.lora_rank, config=self.model_config)
                new_layer.load_state_dict(module.state_dict(), strict=False)
                setattr(model, name, new_layer)
            else:
                # Recursive call for child modules
                self.replace_multihead_attention_recursion(module)

我们必须递归循环遍历所有模型部分,在 PyTorch 中,这些部分(实际上是 RoBERTa 的一部分)可以打包到一个单独的 PyTorch 模块中。

现在我们必须冻结所有不想再训练的参数:

class LoraWrapperRoberta(nn.Module):
    # ...

    def freeze_parameters_except_lora_and_bias(self):
        """
        Freezes all model parameters except for specific layers and types based on the configuration.
        Parameters in LoRA layers, the finetune head, bias parameters, embeddings, and layer norms 
        can be set as trainable based on class settings.
        """
        for name, param in self.model.named_parameters():
            is_trainable = (
                "lora_" in name or
                "finetune_head_" in name or
                (self.train_biases and "bias" in name) or
                (self.train_embeddings and "embeddings" in name) or
                (self.train_layer_norms and "LayerNorm" in name)
            )
            param.requires_grad = is_trainable

此外,我们还必须实现前向方法,以考虑我们将在其上进行微调的任务,以及两种保存和加载 LoRA 权重的方法,以便我们可以加载先前训练模型的适配器。

悬念:有一种方法,可以让代码变得更加简洁,并且更容易推广到其他网络架构(因为我们的代码相对于 RoBERTa 模型而言相当硬编码)。你能想到这可能是什么吗?在下面的可能的改进部分讨论之前,你有时间思考这个问题。但在此之前:让我们测试一些基准,看看我们的实现是否真的有效。

使用 GLUE 和 SQuAD 进行基准测试结果

我们的实现现在已准备好使用 GLUE(通用语言理解评估)和 SQuAD(斯坦福问答数据集)基准进行评估。

GLUE 基准测试是一套八项多样化的 NLP 任务,评估语言模型的全面理解能力。它包括情感分析、文本蕴涵和句子相似性等挑战,提供了模型语言适应能力和熟练度的强有力衡量。

另一方面,SQuAD 侧重于评估问答模型。它涉及从维基百科段落中提取答案,模型识别相关的文本片段。更高级的版本 SQuAD v2 引入了无法回答的问题,增加了复杂性,模拟了现实中模型必须识别文本缺失答案的情况。

请注意,对于以下基准测试,我没有调整任何超参数,没有进行多次运行(特别是较小的 GLUE 数据集容易受到随机噪声的影响),没有进行任何早停策略,并且没有从前一个 GLUE 任务的精细调整开始(通常用于减少小数据集噪声的可变性和防止过拟合)。

所有运行:

  • 从 RoBERTa-base 模型中刚初始化的 LoRA 注入开始,其秩为 8

  • 每个任务确切地进行了 6 个 epoch 的训练,没有任何早停策略。

  • 在前 2 个 epoch 期间,学习率线性增加到最大值,然后在剩余的 4 个 epoch 期间线性衰减至零。

  • 所有任务的最大学习率为 5e-4。

  • 所有任务的批处理大小为 16

RoBERTa-base 模型有 1.246 亿个参数。包括 LoRA 参数、偏差和层规范化,我们只有 42 万个未冻结参数需要训练。这意味着我们实际上只对原始参数的 0.34%进行了训练。

LoRA 为这些特定任务引入的参数数量非常少,实际磁盘大小仅为 1.7 MB。您可以在 Git 仓库的Output文件夹中找到训练过的 LoRA。

训练后,我们重新加载了 LoRA 参数,重新应用它们,并在每个任务的验证集上测试性能。以下是结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用 LoRA 在 GLUE 基准测试中的性能

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用 LoRA 在 SQuAD 数据集上的性能

很可能这些结果可以通过一些超参数的微调大大改善。尽管如此,这清楚地证明了我们的 LoRA 实现是有效的,我们注入的低秩矩阵正在学习中。

可能的改进

回顾我们的实现,人们可能会想:“是否存在比重新编码自注意力类和执行复杂替换更有效、更可推广(即适用于其他网络架构)的方法?”

实际上,我们可以简单地在 pytorch 的nn.Linear函数周围实现一个包装器,并具体说明我们想要替换的层的名称。同样地,您可以编写包装器来适应大多数基本的 pytorch 层,并能够快速调整 LoRA 以适应新的网络架构。以下是如何快速实现这一点的简要草图:

class LoraLinear(nn.Linear):
    """
    Extends a PyTorch linear layer with Low-Rank Adaptation (LoRA).
    LoRA adds two matrices to the layer, allowing for efficient training of large models.
    """
    def __init__(self, in_features, out_features, r=8, *args, **kwargs):
        super().__init__(in_features, out_features, *args, **kwargs)

        # Initialize LoRA matrices
        self.lora_matrix_B = nn.Parameter(torch.zeros(out_features, r))
        self.lora_matrix_A = nn.Parameter(torch.randn(r, in_features))

        # Freeze the original weight matrix
        self.weight.requires_grad = False

    def forward(self, x: Tensor) -> Tensor:
        # Compute LoRA weight adjustment
        lora_weights = torch.matmul(self.lora_matrix_B, self.lora_matrix_A)
        # Apply the original and LoRA-adjusted linear transformations
        return super().forward(x) + F.linear(x, lora_weights)

实际上,这接近了 huggingface PEFT(参数高效微调)库实现 LoRA 的方式。对于任何实际应用场景,如果您不打算学习,我强烈建议使用它,而不是编写自己的代码。

同样,将 LoRA 注入所有线性层(即自注意力的所有矩阵以及全连接前向网络的两个线性层)也已成为一种相当常见的做法。通常,除了 LoRA 参数外,保持偏置和层归一化可训练也是个好主意。由于它们已经很小,你不需要对它们进行低秩注入。

量化原始矩阵权重以节省 GPU VRAM 也是明智的,这样可以在给定 GPU 上训练更大的模型。这可以通过使用 bits-and-bytes 库有效完成,该库现在已完全与 Hugging Face 集成(见参考文献)。

总结一下,这里是在严肃环境中低秩适配的五大法则:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

低秩适配的五大法则

如果你觉得刻有法则的石碑难以阅读,这里以纯文本重新呈现:

低秩适配的五大法则

1. 利用 LoRA 高效地对模型进行微调,重点保持参数规模最小。

2. 使用 PEFT 库进行 LoRA 实现,避免复杂的编码工作。

3. 将 LoRA 适配扩展到所有线性层,增强整体模型能力。

4. 保持偏置和层归一化可训练,因为它们对模型适应性至关重要,不需要低秩适配。

5. 应用量化 LoRA — QLoRA — 以保护 GPU VRAM 并训练模型,使训练更大模型成为可能。

请记住,使用 QLoRA 训练可能比 LoRA 慢一些,因为它涉及在每次乘法期间对矩阵进行反量化。例如,在微调像 Llama-7B 这样的大型模型时,QLoRA 需要约 75% 更少的 VRAM,但比标准 LoRA 慢大约 40%。更多见解,请查看我在参考文献中链接的博客文章。

PEFT 实现的逐步指南

让我们看看如何真正遵守我们的法则,并通过 PEFT 实现更好的版本。

首先,让我们以量化的方式加载模型。得益于 bitsandbytes 与 Huggingface transformers 库的集成(于 2023 年 5 月推出),这变得非常简单。

我们必须指定一个配置文件,然后直接从 huggingface 加载模型以进行量化。一般来说,最好使用 transformers 中的AutoModel对象。将量化模型作为较大、新定义的nn.module对象的子模块加载是困难的。你通常应该使用 huggingface 的原始模型,因此直接导入 GLUE 任务的AutoModelForSequenceClassification和 SQuAD 基准的AutoModelForQuestionAnswering。在配置中,我们还可以指定不进行量化的参数:在这里,我们必须注册分类或 qa 输出头,因为我们希望对这些头进行完整的训练,即不使用 LoRA,因为这些头是为微调而新初始化的,且从未成为预训练基础模型的一部分。

import bitsandbytes as bnb
from transformers import AutoModel, AutoModelForSequenceClassification, BitsAndBytesConfig

# Configuration to load a quantized model
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,  # Enable 4-bit loading
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    llm_int8_skip_modules=['classifier', 'qa_outputs'],  # Skip these for quantization
)

# Load the model from Huggingface with quantization
model = AutoModelForSequenceClassification.from_pretrained('roberta-base',
          torch_dtype="auto", quantization_config=bnb_config)

你可以通过检查模型的模块和参数数据类型来验证 4 位加载:

# Verify 4-bit loading
print("Verifying 4-bit elements (Linear4bit) in the attention layer:")
print(model.roberta.encoder.layer[4].attention)

print("Checking for uint8 data type:")
print(model.roberta.encoder.layer[4].attention.self.query.weight.dtype)

现在开始使用 PEFT 注入 LoRA 参数。请注意,PEFT 库在处理自定义模型或其他复杂结构时更加灵活,因此只要您只进行 LoRA 而不是 QLoRA(量化通常是棘手的部分)。

PEFT 库通过它们的名称来定位要替换的模块;因此,我们必须查看模型的model.named_parameters()。这是在非量化 roberta-base 模型中的样子。

Module                                                        Parameters
----------------------------------------------------------  ------------
roberta.embeddings.word_embeddings.weight                     38_603_520
roberta.embeddings.position_embeddings.weight                    394_752
roberta.embeddings.token_type_embeddings.weight                      768
roberta.embeddings.LayerNorm.weight                                  768
roberta.embeddings.LayerNorm.bias                                    768
roberta.encoder.layer.0.attention.self.query.weight              589_824
roberta.encoder.layer.0.attention.self.query.bias                    768
roberta.encoder.layer.0.attention.self.key.weight                589_824
roberta.encoder.layer.0.attention.self.key.bias                      768
roberta.encoder.layer.0.attention.self.value.weight              589_824
roberta.encoder.layer.0.attention.self.value.bias                    768
roberta.encoder.layer.0.attention.output.dense.weight            589_824
roberta.encoder.layer.0.attention.output.dense.bias                  768
roberta.encoder.layer.0.attention.output.LayerNorm.weight            768
roberta.encoder.layer.0.attention.output.LayerNorm.bias              768
roberta.encoder.layer.0.intermediate.dense.weight              2_359_296
roberta.encoder.layer.0.intermediate.dense.bias                    3_072
roberta.encoder.layer.0.output.dense.weight                    2_359_296
roberta.encoder.layer.0.output.dense.bias                            768
roberta.encoder.layer.0.output.LayerNorm.weight                      768
roberta.encoder.layer.0.output.LayerNorm.bias                        768
roberta.encoder.layer.1.attention.self.query.weight              589_824
...
roberta.encoder.layer.11.output.LayerNorm.bias                       768
classifier.dense.weight                                          589_824
classifier.dense.bias                                                768
classifier.out_proj.weight                                         1_536
classifier.out_proj.bias                                               2
----------------------------------------------------------  ------------
TOTAL                                                        124_647_170

然后,我们可以指定 LoRA 目标以选择这些字符串。检查的方法是,如果其完整名称中包含指定的子字符串,则为真。因此,写queryvalue等效于我们的从头开始实现上述内容。对于密集层,我们必须更加小心,因为分类器还具有密集输出。如果我们希望微调其他密集层,我们必须通过intermediate.denseoutput.dense更为具体。

所有未注入 LoRA 参数的参数都会自动冻结,即不会接收任何梯度更新。如果有任何我们希望以其原始形式训练的层,我们可以通过将列表传递给 Lora-Config 的modules_to_save参数来指定它们。在我们的情况下,我们想在这里添加LayerNorm和 GLUE 以及 SQuAD 的微调头。请注意,列表的每个元素不必匹配某个内容。我们可以简单地将classifierqa_outputs添加到此列表中,然后拥有一个可以正确工作于两个任务的单个配置文件。

对于偏置参数,你可以使用方便的配置参数bias。你可以指定all以重新训练所有模块的所有偏置,lora_only以仅训练注入的偏置,或者none在训练期间保持所有偏置不变。

以下示例注入了一个秩为 2 的 LoRA。我们用上面的 8 指定 alpha 参数,因为这是我们首先尝试的秩,并且应该允许我们保持从头开始示例的原始学习率。

import peft

# Config for the LoRA Injection via PEFT
peft_config = peft.LoraConfig(
    r=2, # rank dimension of the LoRA injected matrices
    lora_alpha=8, # parameter for scaling, use 8 here to make it comparable with our own implementation
    target_modules=['query', 'key', 'value', 'intermediate.dense', 'output.dense'], # be precise about dense because classifier has dense too
    modules_to_save=["LayerNorm", "classifier", "qa_outputs"], # Retrain the layer norm; classifier is the fine-tune head; qa_outputs is for SQuAD
    lora_dropout=0.1, # dropout probability for layers
    bias="all", # none, all, or lora_only
)

model = peft.get_peft_model(model, peft_config)

请记住,为 LoRA 注入指定更多模块可能会增加 VRAM 要求。如果遇到 VRAM 限制,请考虑减少目标模块的数量或 LoRA 秩。

对于训练,特别是使用 QLoRA 时,选择与量化矩阵兼容的优化器。用 bitsandbytes 变体替换你的标准 torch 优化器,如下所示:

import torch
import bitsandbytes as bnb

# replace this
optimizer = torch.optim.AdamW(args here)
# with this
optimizer = bnb.optim.AdamW8bit(same args here)

然后,您可以像以前一样训练此模型,而无需在训练过程中明确担心 QLoRA。

训练完成后,保存和重新加载模型的过程非常简单。使用model.save_pretrained保存您的模型,并指定所需的文件名。PEFT 库将在此位置自动创建一个目录,其中存储模型权重和配置文件。此文件包括基础模型和 LoRA 配置参数等重要细节。

要重新加载模型,请使用 peft.AutoPeftModel.from_pretrained,并将目录路径作为参数传递。一个关键点是,LoRA 配置当前不保留 AutoModelForSequenceClassification 初始化时的类别数量。在使用 from_pretrained 时,你需要手动输入这个类别数量作为附加参数。如果不这样做,将会导致错误。

重新加载的模型将包括应用了 LoRA 适配器的原始基础模型。如果你决定将 LoRA 适配器永久集成到基础模型矩阵中,只需执行 model.merge_and_unload()

要获得更为实操的理解和详细的说明,请查看 GitHub 仓库。在那里,你会找到两个名为Train-QLoRA-with-PEFT.ipynbLoad-LoRA-Weights-PEFT.ipynb的笔记本,提供了使用 PEFT 训练和加载模型的逐步示例。

结论

“我们不会停止探索,我们所有的探索最终将是到达我们开始的地方,并第一次了解这个地方。”

—— 摘自 T.S. 艾略特的《小吉丁》

这段旅程带领我们从简单的、尽管是硬编码的 LoRA 实现,深入了解了低秩适配器、它们的实际应用以及基准测试。

我们探讨了一种更高效的实现策略,并深入了解了像 PEFT 这样的现有库在 LoRA 集成中的优雅。

我们的冒险以实际的 LoRA 使用指南结束,这些指南被概括为“五项戒律”,确保在实际应用中有效且高效地使用这一技术,并提供了逐步实施的指南。

参考资料

所有图片,除非另有说明,均由作者提供。

将深度学习论文中的数学公式转化为高效的 PyTorch 代码:SimCLR 对比损失

原文:towardsdatascience.com/implementing-math-in-deep-learning-papers-into-efficient-pytorch-code-simclr-contrastive-loss-be94e1f63473?source=collection_archive---------5-----------------------#2023-07-05

学习如何将深度学习论文中的高级数学公式转化为高效的 PyTorch 代码,共分为三步。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Moein Shariatnia

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 7 月 5 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

摄影:Jeswin ThomasUnsplash 提供

介绍

加深对深度学习模型和损失函数背后数学理解的最佳方法之一,也是提高 PyTorch 技能的好方法,是自己动手实现深度学习论文。

书籍和博客帖子可以帮助你入门编程和学习机器学习/深度学习的基础知识,但在学习了几个相关资源并掌握了领域中的常规任务后,你会很快意识到在学习的过程中你只能依靠自己,并且你会发现大多数在线资源都很枯燥且过于浅薄。然而,我相信,如果你能在新深度学习论文发表时学习,并理解其中所需的数学部分(不一定要理解作者理论背后的所有数学证明),同时,你又是一个能够将其实现为高效代码的能干程序员,那么没有什么能阻止你在该领域保持最新并学习新思想。

对比损失的实现

我将介绍我的常规方法和实现数学在深度学习论文中的步骤,使用一个不简单的例子:在SimCLR 论文中的对比损失

这是损失的数学公式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SimCLR 论文中的对比(NT-Xent)损失 | 来源于 arxiv.org/pdf/2002.05709.pdf

我同意公式的外观可能会让人感到畏惧!你可能会想,GitHub 上一定有很多现成的 PyTorch 实现,所以我们就使用它们吧 😃 是的,你说得对。网上确实有很多实现。然而,我认为这是一个练习这种技能的好例子,也可以作为一个很好的起点。

将数学实现到代码中的步骤

我将数学实现到 PyTorch 高效代码中的常规方法如下:

  1. 理解数学,将其用简单的术语解释

  2. 使用简单的 Python “for” 循环 实现一个初始版本,现在不进行复杂的矩阵乘法

  3. 将你的代码转换成高效 矩阵友好的 PyTorch 代码

好的,让我们直接进入第一步。

步骤 1:理解数学并用简单的术语解释

我假设你具备基本的线性代数知识并熟悉数学符号。如果没有,你可以使用这个工具来了解这些符号的含义和功能,只需绘制符号即可。你还可以查看这个很棒的维基百科页面,其中描述了大多数符号。这些都是你在需要时学习新知识的机会。我认为这是一种更高效的学习方式,而不是从头开始阅读数学教科书,几天后就放在一边 😃

回到我们的主题。正如公式上方的段落增加了更多的背景,在 SimCLR 学习策略中,你从 N 张图像开始,将每张图像转换 2 次以获得这些图像的增强视图(现在有 2*N 张图像)。然后,你将这些 2 * N 张图像通过一个模型,得到每张图像的嵌入向量。现在,你希望使同一图像的 2 个增强视图(一个正样本对)的嵌入向量在嵌入空间中更接近(对所有其他正样本对也做同样的处理)。一种测量两个向量相似度(接近,相同方向)的方法是使用余弦相似度,它被定义为 sim(u, v)(请参见上图的定义)。

简而言之,公式描述的是,对于我们批次中的每个项目,即图像的一个增强视图的嵌入,(记住:批次包含不同图像的所有增强视图的嵌入→如果从 N 张图像开始,批次大小为 2N),我们首先找到该图像的另一个增强视图的嵌入以形成一个正样本对。然后,我们计算这两个嵌入的余弦相似度并对其进行指数运算(公式的分子)。接着,我们计算与我们开始时的第一个嵌入向量构建的所有其他对的余弦相似度的指数运算(除了与自身的对,这就是公式中的 1[k!=i]的含义),并将它们相加以构建分母。现在,我们可以将分子除以分母,取自然对数并翻转符号!现在,我们得到了批次中第一个项目的损失。我们只需对批次中的所有其他项目重复相同的过程,然后取平均值,以便调用 PyTorch 的.backward()*方法来计算梯度。

第 2 步:使用简单的 Python 代码实现,采用幼稚的“for”循环!

使用慢速“for”循环的简单 Python 实现

让我们看一下代码。假设我们有两张图像:A 和 B。变量 aug_views_1 保存了这两张图像的一个增强视图(A1 和 B1)的嵌入(每个大小为 3),与 aug_views_2(A2 和 B2)相同;因此,两个矩阵中的第一个项目与图像 A 相关,第二个项目与图像 B 相关。我们将这两个矩阵拼接成projections矩阵(其中包含 4 个向量:A1,B1,A2,B2)。

为了保持投影矩阵中向量的关系,我们定义了pos_pairs字典来存储在拼接矩阵中哪些两个项目是相关的。(稍后我会解释*F.normalize()*的事!)

正如你在接下来的代码行中看到的,我在一个for 循环中遍历投影矩阵中的项,使用我们的字典找到相关向量,然后计算余弦相似度。你可能会想为什么不按照余弦相似度公式除以向量的大小。关键是,在开始循环之前,使用 F.normalize 函数,我将投影矩阵中的所有向量标准化为大小为 1。因此,在计算余弦相似度的那一行不需要除以大小。

在构建好分子后,我会找到批次中所有其他向量的索引(除了相同的索引 i),以计算包含分母的余弦相似度。最后,我通过将分子除以分母,并应用对数函数和翻转符号来计算损失。确保玩转代码以理解每一行的作用。

第 3 步:将其转换为高效的矩阵友好的 PyTorch 代码

之前的 Python 实现的问题是太慢,无法用于我们的训练流程;我们需要摆脱缓慢的“for”循环,并将其转换为矩阵乘法和数组操作,以利用并行化的优势。

PyTorch 实现

让我们看看这个代码片段发生了什么。这一次,我引入了 labels_1labels_2 张量来编码这些图像所属的任意类别,因为我们需要一种方法来编码 A1、A2 和 B1、B2 图像之间的关系。你选择标签 0 和 1(就像我做的)还是 5 和 8 都无所谓。

在连接了所有的嵌入和标签后,我们首先创建一个包含所有可能配对的余弦相似度的 sim_matrix

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

sim_matrix 的样子:绿色单元格包含我们的正样本对,橙色单元格是需要在分母中忽略的配对 | 作者提供的可视化

上面的可视化图是你理解代码如何工作的全部所需 😃 以及为什么我们要进行其中的步骤。考虑到 sim_matrix 的第一行,我们可以按如下方式计算批次中第一个项目 (A1) 的损失:我们需要将 A1A2(指数化)除以 A1B1、A1A2 和 A1B2(每个都先指数化)的总和,并将结果保存在存储所有损失的张量的第一个项目中。因此,我们需要首先制作一个掩码,以找到上面可视化图中的绿色单元格。代码中定义变量mask的两行正是做这件事。分子是通过将我们的 sim_matrix 乘以刚创建的掩码来计算的,然后对每行的项目求和(掩码后,每行将只有一个非零项目;即绿色单元格)。为了计算分母,我们需要在每行上求和,忽略对角线上的橙色单元格。为此,我们将使用 PyTorch 张量的*.diag()*方法。其余部分不言自明!

额外内容:使用 AI 助手(ChatGPT、Copilot 等)来实现公式

我们有很棒的工具可以帮助我们理解和实现深度学习论文中的数学。例如,你可以在给出论文中的公式后,要求 ChatGPT(或其他类似工具)用 PyTorch 实现代码。根据我的经验,如果你能在python-for-loop实现步骤中找到自己,ChatGPT 最能提供最好的最终答案,并减少试错次数。把那个初步实现交给 ChatGPT,要求它将其转换为仅使用矩阵乘法和张量操作的高效 PyTorch 代码;你会对答案感到惊讶 😃

进一步阅读

我鼓励你查看以下两个相同理念的优秀实现,以了解如何将这一实现扩展到更微妙的情况中,比如在监督对比学习设置中。

  1. 监督对比损失,由 Guillaume Erhard 编写

  2. SupContrast,由 Yonglong Tian 编写

关于我

我是 Moein Shariatnia,一名机器学习开发者和医学学生,专注于使用深度学习解决方案进行医学影像应用。我的研究主要集中在研究深度模型在各种情况下的泛化能力。欢迎通过电子邮件、Twitter 或 LinkedIn 与我联系。

在 PyTorch 中实现软最近邻损失

原文:towardsdatascience.com/implementing-soft-nearest-neighbor-loss-in-pytorch-b9ed2a371760?source=collection_archive---------4-----------------------#2023-11-27

数据集的类邻域可以通过软最近邻损失来学习

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Abien Fred Agarap

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 11 月 27 日

在本文中,我们讨论了如何实现软最近邻损失,我们也在这里谈到过这个话题。

表示学习是通过深度神经网络学习给定数据集中最显著特征的任务。通常这是在监督学习范式中隐式完成的任务,并且是深度学习成功的关键因素 (Krizhevsky et al., 2012He et al., 2016Simonyan et al., 2014)。换句话说,表示学习自动化了特征提取过程。通过这个过程,我们可以将学到的表示用于下游任务,如分类、回归和合成。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1. 来源于 SNNL (Frosst et al., 2019). 通过最小化软最近邻损失,类相似数据点(如其颜色所示)之间的距离被最小化,而类不同数据点之间的距离被最大化。

我们也可以影响学习到的表示的形成,以适应特定的应用场景。在分类的情况下,表示会被调整使得同一类的数据点聚集在一起,而在生成(例如在 GAN 中)中,表示会被调整使得真实数据点与合成数据点聚集在一起。

同样,我们也享受了使用主成分分析(PCA)来编码特征以用于下游任务。然而,在 PCA 编码的表示中没有任何类或标签信息,因此在下游任务上的表现可能会进一步提升。我们可以通过学习数据集的邻域结构来改进编码的表示,即哪些特征被聚集在一起,这样的聚集会暗示这些特征属于同一类,如半监督学习文献中的聚类假设所示 (Chapelle et al., 2009)。

为了将邻域结构整合进表示中,已经引入了流形学习技术,如局部线性嵌入(LLE)(Roweis & Saul, 2000)、邻域组件分析(NCA)(Hinton et al., 2004)和 t-随机邻域嵌入(t-SNE)(Maaten & Hinton, 2008)。

然而,上述流形学习技术各有其缺点。例如,LLE 和 NCA 编码的是线性嵌入,而非非线性嵌入。同时,t-SNE 嵌入的结构依赖于所使用的超参数。

为了避免这种缺陷,我们可以使用改进的 NCA 算法,即软最近邻损失或 SNNL(Salakhutdinov & Hinton, 2007; Frosst et al., 2019)。SNNL 通过引入非线性改进了 NCA 算法,它是在神经网络的每一隐藏层上计算的,而不仅仅是最后的编码层。此损失函数用于优化数据集中点的纠缠

在这种情况下,纠缠定义为类相似的数据点彼此之间的接近程度,相较于类不同的数据点。低纠缠意味着类相似的数据点彼此之间要比类不同的数据点更接近(见图 1)。拥有这样的数据点集合将使下游任务更容易完成,且性能更佳。Frosst 等人(2019)通过引入温度因子T扩展了 SNNL 目标。因此,我们得到以下最终损失函数,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2. 软最近邻损失函数。图由作者提供。

其中d是原始输入特征或神经网络隐藏层表示上的距离度量,T是与隐藏层中数据点之间的距离直接成正比的温度因子。对于此实现,我们使用余弦距离作为我们的距离度量,以获得更稳定的计算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3. 余弦距离公式。图由作者提供。

本文的目的是帮助读者理解和实现软最近邻损失,因此我们将详细分析损失函数以更好地理解它。

距离度量

我们应该首先计算的是数据点之间的距离,这些距离可以是原始输入特征或网络的隐藏层表示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4. 计算 SNNL 的第一步是计算输入数据点的距离度量。图由作者提供。

对于我们的实现,我们使用余弦距离度量(图 3)以获得更稳定的计算。暂时,我们忽略上图中标记的子集ijik,只专注于计算输入数据点之间的余弦距离。我们通过以下 PyTorch 代码实现:

normalized_a = torch.nn.functional.normalize(features, dim=1, p=2)
normalized_b = torch.nn.functional.normalize(features, dim=1, p=2)
normalized_b = torch.conj(normalized_b).T
product = torch.matmul(normalized_a, normalized_b)
distance_matrix = torch.sub(torch.tensor(1.0), product)

在上面的代码片段中,我们首先在第 1 和第 2 行使用欧几里得范数对输入特征进行归一化。然后在第 3 行,我们获取归一化输入特征第二组的共轭转置。我们计算共轭转置以考虑复数向量。在第 4 和第 5 行,我们计算输入特征的余弦相似度和距离。

具体来说,考虑以下特征集,

tensor([[ 1.0999, -0.9438,  0.7996, -0.4247],
        [ 1.2150, -0.2953,  0.0417, -1.2913],
        [ 1.3218,  0.4214, -0.1541,  0.0961],
        [-0.7253,  1.1685, -0.1070,  1.3683]])

使用我们上面定义的距离度量,我们得到以下距离矩阵,

tensor([[ 0.0000e+00,  2.8502e-01,  6.2687e-01,  1.7732e+00],
        [ 2.8502e-01,  0.0000e+00,  4.6293e-01,  1.8581e+00],
        [ 6.2687e-01,  4.6293e-01, -1.1921e-07,  1.1171e+00],
        [ 1.7732e+00,  1.8581e+00,  1.1171e+00, -1.1921e-07]])

采样概率

现在我们可以计算表示选择每个特征的概率的矩阵,给定其与所有其他特征的成对距离。这仅仅是选择i点的概率,基于ijk点之间的距离。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5。第二步是计算基于距离的采样概率。图由作者提供。

我们可以通过以下代码计算这一点:

pairwise_distance_matrix = torch.exp(
    -(distance_matrix / temperature)
) - torch.eye(features.shape[0]).to(model.device)

代码首先计算距离矩阵的负指数除以温度因子,将值缩放到正值。温度因子决定如何控制给定点对之间距离的重要性,例如,在低温下,损失由小距离主导,而广泛分隔表示之间的实际距离变得不那么重要。

在减去torch.eye(features.shape[0])(即对角矩阵)之前,张量如下,

tensor([[1.0000, 0.7520, 0.5343, 0.1698],
        [0.7520, 1.0000, 0.6294, 0.1560],
        [0.5343, 0.6294, 1.0000, 0.3272],
        [0.1698, 0.1560, 0.3272, 1.0000]])

我们从距离矩阵中减去一个对角矩阵,以去除所有自相似项(即每个点到自身的距离或相似度)。

接下来,我们可以通过以下代码计算每对数据点的采样概率:

pick_probability = pairwise_distance_matrix / (
    torch.sum(pairwise_distance_matrix, 1).view(-1, 1)
    + stability_epsilon
)

掩码采样概率

到目前为止,我们计算的采样概率不包含任何标签信息。我们通过用数据集标签掩盖采样概率来整合标签信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6。我们利用标签信息来隔离属于同一类别的点的概率。图由作者提供。

首先,我们必须从标签向量中推导出一个成对矩阵:

masking_matrix = torch.squeeze(
    torch.eq(labels, labels.unsqueeze(1)).float()
)

我们应用掩码矩阵,利用标签信息来隔离属于同一类别的点的概率:

masked_pick_probability = pick_probability * masking_matrix

接下来,我们通过计算每行的掩码采样概率的总和来计算特定特征的总采样概率,

summed_masked_pick_probability = torch.sum(masked_pick_probability, dim=1)

最后,我们可以计算采样概率总和的对数,为计算便利性添加额外的计算稳定变量,并求平均作为网络的最近邻损失,

snnl = torch.mean(
    -torch.log(summed_masked_pick_probability + stability_epsilon
)

我们现在可以将这些组件组合在一起,在前向传递函数中计算整个深度神经网络的软最近邻损失,

def forward(
    self,
    model: torch.nn.Module,
    features: torch.Tensor,
    labels: torch.Tensor,
    outputs: torch.Tensor,
    epoch: int,
) -> Tuple:
    if self.use_annealing:
        self.temperature = 1.0 / ((1.0 + epoch) ** 0.55)

    primary_loss = self.primary_criterion(
        outputs, features if self.unsupervised else labels
    )

    activations = self.compute_activations(model=model, features=features)

    layers_snnl = []
    for key, value in activations.items():
        value = value[:, : self.code_units]
        distance_matrix = self.pairwise_cosine_distance(features=value)
        pairwise_distance_matrix = self.normalize_distance_matrix(
            features=value, distance_matrix=distance_matrix
        )
        pick_probability = self.compute_sampling_probability(
            pairwise_distance_matrix
        )
        summed_masked_pick_probability = self.mask_sampling_probability(
            labels, pick_probability
        )
        snnl = torch.mean(
            -torch.log(self.stability_epsilon + summed_masked_pick_probability)
        )
        layers_snnl.append(snnl)

    snn_loss = torch.stack(layers_snnl).sum()

    train_loss = torch.add(primary_loss, torch.mul(self.factor, snn_loss))

    return train_loss, primary_loss, snn_loss

可视化解耦表示

我们使用软最近邻损失训练了一个自编码器,并可视化了其学习到的解缠表示。该自编码器包含 (x-500–500–2000-d-2000–500–500-x) 单元,并在 MNIST、Fashion-MNIST 和 EMNIST-Balanced 数据集的小型标注子集上进行训练。这是为了模拟标注样本的稀缺性,因为自编码器通常是无监督模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7. 3D 可视化比较了三个数据集的原始表示和解缠潜在表示。为了实现这种可视化,表示使用 t-SNE 编码,困惑度 = 50,学习率 = 10,优化 5000 次迭代。图由作者提供。

我们仅可视化了任意选择的 10 个簇,以便更简单、更清晰地展示 EMNIST-Balanced 数据集。从上图可以看出,潜在代码表示变得更适合聚类,通过集群分散度和正确的集群分配(由集群颜色指示)体现了良好的定义簇。

结束语

在本文中,我们详细分析了软最近邻损失函数,并讨论了如何在 PyTorch 中实现它。

软最近邻损失首次由 Salakhutdinov & Hinton (2007) 引入,用于计算自编码器潜在代码(瓶颈)表示上的损失,然后将该表示用于下游的 kNN 分类任务。

Frosst, Papernot, & Hinton (2019) 通过引入温度因子并计算神经网络所有层的损失,扩展了软最近邻损失。

最后,我们采用了退火温度因子来进一步改善网络的学习解缠表示,并加速解缠过程 (Agarap & Azcarraga, 2020)。

完整的代码实现可在 GitLab 上获得。

参考文献

  • Agarap, Abien Fred, 和 Arnulfo P. Azcarraga. “通过解缠内部表示来改善 k-means 聚类性能。” 2020 国际神经网络联合会议 (IJCNN). IEEE, 2020.

  • Chapelle, Olivier, Bernhard Scholkopf 和 Alexander Zien. “半监督学习 (chapelle, o. 等, 编;2006)[书评]。” IEEE 神经网络交易 20.3 (2009): 542–542.

  • Frosst, Nicholas, Nicolas Papernot 和 Geoffrey Hinton. “分析和改进软最近邻损失的表示。” 国际机器学习会议. PMLR, 2019.

  • Goldberger, Jacob 等. “邻域组件分析。” 神经信息处理系统进展. 2005.

  • He, Kaiming, 等. “用于图像识别的深度残差学习。” IEEE 计算机视觉与模式识别会议论文集。2016 年。

  • Hinton, G., 等. “邻域组件分析。” NIPS 会议论文集。2004 年。

  • Krizhevsky, Alex, Ilya Sutskever, 和 Geoffrey E. Hinton. “使用深度卷积神经网络进行 ImageNet 分类。” 神经信息处理系统进展 25 (2012)。

  • Roweis, Sam T., 和 Lawrence K. Saul. “通过局部线性嵌入进行非线性维度约简。” 科学 290.5500 (2000): 2323–2326。

  • Salakhutdinov, Ruslan, 和 Geoff Hinton. “通过保留类别邻域结构来学习非线性嵌入。” 人工智能与统计学。2007 年。

  • Simonyan, Karen, 和 Andrew Zisserman. “用于大规模图像识别的非常深的卷积网络。” arXiv 预印本 arXiv:1409.1556 (2014)。

  • Van der Maaten, Laurens, 和 Geoffrey Hinton. “使用 t-SNE 进行数据可视化。” 机器学习研究杂志 9.11 (2008)。

从头实现最速下降算法

原文:towardsdatascience.com/implementing-the-steepest-descent-algorithm-in-python-from-scratch-d32da2906fe2

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Nicolo Cosimo Albanese

·发表于 Towards Data Science ·11 分钟阅读·2023 年 2 月 20 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

目录

  1. 介绍

  2. 最速下降算法

    2.1 搜索方向

    2.2 步长

    2.3 算法

  3. 实现

    3.1 常数步长

    3.2 带有 Armijo 条件的线搜索

  4. 结论

1. 介绍

优化是寻找一组变量x,使得目标函数f(x)最小化或最大化的过程。由于最大化一个函数等同于最小化它的负数,我们可以专注于最小化问题:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于我们的例子,我们将定义一个二次的多变量目标函数f(x)如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它的梯度 ∇f(x)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

import numpy as np

def f(x):
    '''Objective function'''
    return 0.5*(x[0] - 4.5)**2 + 2.5*(x[1] - 2.3)**2

def df(x):
    '''Gradient of the objective function'''
    return np.array([x[0] - 4.5, 5*(x[1] - 2.3)])

可以利用流行的[scipy.optimize.minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)函数来快速找到最优值,该函数来自于流行的[SciPy](https://scipy.org/)库:

from scipy.optimize import minimize

result = minimize(
    f, np.zeros(2), method='trust-constr', jac=df)

result.x
array([4.5, 2.3])

我们可以绘制目标函数及其最小值:

import matplotlib.pyplot as plt

# Prepare the objective function between -10 and 10
X, Y = np.meshgrid(np.linspace(-10, 10, 20), np.linspace(-10, 10, 20))
Z = f(np.array([X, Y]))

# Minimizer
min_x0, min_x1 = np.meshgrid(result.x[0], result.x[1])   
min_z = f(np.stack([min_x0, min_x1]))

# Plot
fig = plt.figure(figsize=(15, 20))

# First subplot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.view_init(40, 20)

# Second subplot
ax = fig.add_subplot(1, 2, 2, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(90, -90);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

现在我们介绍最速下降算法并从头实现它。我们的目标是解决优化问题并找到最小值[4.5, 2.3]

2. 最速下降算法

要解决优化问题minₓ f(x),我们首先在坐标空间中的某一点开始。然后,我们通过搜索方向p迭代地移动,朝着f(x)的最小值更好的近似值前进:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个表达式中:

  • x 是输入变量;

  • p搜索方向

  • α > 0步长步幅;它描述了我们在每次迭代 k 中应该沿着方向 p 移动多少。

这种方法需要适当选择步长 α 和搜索方向 p

2.1. 搜索方向

作为搜索方向,最陡下降算法使用在当前迭代 xₖ 中评估的负梯度 -∇f(xₖ)。这是一个合理的选择,因为函数的负梯度总是指向函数下降最快的方向。

因此,我们可以将表达式重写为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于最小值是一个驻点,当梯度的范数小于给定的容忍度时,停止算法是合理的:如果梯度达到零,我们可能找到了最小值。

2.2. 步长

由于我们试图最小化 f(x),理想的步长 α 是以下目标函数 φ(α) 的最小值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不幸的是,这需要在当前优化任务中解决额外的优化任务:minₓ f(x)。此外,虽然 φ(α) 是一元的,但找到它的最小值可能需要对 f(x) 及其梯度进行过多的评估。

简而言之,我们在寻找α的选择与做出选择所需时间之间的权衡。为此,我们可以简单地选择一个步长值,确保目标函数至少减少一定量。实际上,一个流行的不精确线搜索条件指出,α应该通过以下不等式导致f(x)充分减少

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在文献中,这被称为充分减少Armijo 条件,并且它属于 Wolfe 条件 的集合。

常数 c 被选择为较小的值;一个常见的值是 10^-4。

由于最陡下降法使用负梯度 -∇f(xₖ) 作为搜索方向 pₖ,表达式 + ∇f(xₖ)^T * pₖ 等于梯度的负平方范数。在 Python 中:-np.linalg.norm(∇f(xₖ))**2。因此,我们的充分减少条件变为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.3. 算法

我们现在可以写出所需的最陡下降法步骤:

  1. 选择一个起始点 x = x₀

  2. 选择一个最大迭代次数 M

  3. 选择一个接近零的容忍度 tol 来评估梯度

  4. 设置步数计数器 n

  5. 在循环中重复:

    5.1 通过 Armijo 条件(线搜索)更新 α

    5.2 构造下一个点 x = x - α ⋅ ∇f(x)

    5.3 评估新的梯度 ∇f(x) 5.4 更新步数计数器 n = n + 1

    5.5 如果当前梯度的范数足够小 ||∇f(x)|| < tol 或达到最大迭代次数 n = M,则退出循环

  6. 返回 x

让我们用 Python 来实现它。

3. 实现

在这一部分,我们分享了最陡下降算法的实现。特别是,我们按步骤进行:

  1. 我们从常数步长开始,然后

  2. 我们添加了带有 Armijo 条件的线搜索。

3.1 常数步长

让我们从实现我们迭代方法的简化版本开始

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在所有迭代中应用 常数 步长值 α。我们的目的是实际验证对于任何常数 α 收敛性并不保证,因此需要实现线搜索:

def steepest_descent(gradient, x0 = np.zeros(2), alpha = 0.01, max_iter = 10000, tolerance = 1e-10): 
    '''
    Steepest descent with constant step size alpha.

    Args:
      - gradient: gradient of the objective function
      - alpha: line search parameter (default: 0.01)
      - x0: initial guess for x_0 and x_1 (default values: zero) <numpy.ndarray>
      - max_iter: maximum number of iterations (default: 10000)
      - tolerance: minimum gradient magnitude at which the algorithm stops (default: 1e-10)

    Out:
      - results: <numpy.ndarray> of size (n_iter, 2) with x_0 and x_1 values at each iteration
      - number of steps: <int>
    '''

    # Prepare list to store results at each iteration 
    results = np.array([])

    # Evaluate the gradient at the starting point 
    gradient_x = gradient(x0)

    # Initialize the steps counter 
    steps_count = 0

    # Set the initial point 
    x = x0 
    results = np.append(results, x, axis=0)

    # Iterate until the gradient is below the tolerance or maximum number of iterations is reached
    # Stopping criterion: inf norm of the gradient (max abs)
    while any(abs(gradient_x) > tolerance) and steps_count < max_iter:

        # Update the step size through the Armijo condition
        # Note: the first value of alpha is commonly set to 1
        #alpha = line_search(1, x, gradient_x)

        # Update the current point by moving in the direction of the negative gradient 
        x = x - alpha * gradient_x

        # Store the result
        results = np.append(results, x, axis=0)

        # Evaluate the gradient at the new point 
        gradient_x = gradient(x) 

        # Increment the iteration counter 
        steps_count += 1 

    # Return the steps taken and the number of steps
    return results.reshape(-1, 2), steps_count

让我们使用这个函数来解决我们的优化任务:

# Steepest descent
points, iters = steepest_descent(
  df, x0 = np.array([-9, -9]), alpha=0.30)

# Found minimizer
minimizer = points[-1].round(1)

# Print results
print('- Final results: {}'.format(minimizer))
print('- N° steps: {}'.format(iters))
Final results: [4.5 2.3]
N° steps: 72

使用 α = 0.3 时,最小值在 72 步中达到了。我们可以绘制每次迭代中的点 x

# Steepest descent steps
X_estimate, Y_estimate = points[:, 0], points[:, 1] 
Z_estimate = f(np.array([X_estimate, Y_estimate]))

# Plot
fig = plt.figure(figsize=(20, 20))

# First subplot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.view_init(20, 20)

# Second subplot
ax = fig.add_subplot(1, 2, 2, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(90, -90);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于作者。

我们观察到最陡下降法的特征是通过一个显著的“之”字形路径向最小值前进。这是由于选择了负梯度 -∇f(x) 作为搜索方向 p

正如我们讨论的,对于任何步长值收敛性并不保证。在之前的示例中,我们使用了常数步长 α = 0.3,但如果选择不同的步长会发生什么呢?

# Step sizes to be tested
alphas = [0.01, 0.25, 0.3, 0.35, 0.4]

# Store the iterations for each step size
X_estimates, Y_estimates, Z_estimates = [], [], []

# Plot f(x) at each iteration for different step sizes
fig, ax = plt.subplots(len(alphas), figsize=(8, 9))
fig.suptitle('$f(x)$ at each iteration for different $α$')

# For each step size
for i, alpha in enumerate(alphas):

    # Steepest descent
    estimate, iters = steepest_descent(
      df, x0 = np.array([-5, -5]), alpha=alpha, max_iter=3000)

    # Print results
    print('Input alpha: {}'.format(alpha))
    print('\t- Final results: {}'.format(estimate[-1].round(1)))
    print('\t- N° steps: {}'.format(iters))

    # Store for 3D plots
    X_estimates.append(estimate[:, 0])
    Y_estimates.append(estimate[:, 1])  
    Z_estimates.append(f(np.array([estimate[:, 0], estimate[:, 1]])))

    # Subplot of f(x) at each iteration for current alpha
    ax[i].plot([f(var) for var in estimate], label='alpha: '+str(alpha))
    ax[i].axhline(y=0, color='r', alpha=0.7, linestyle='dashed')
    ax[i].set_xlabel('Number of iterations')
    ax[i].set_ylabel('$f(x)$')
    ax[i].set_ylim([-10, 200])
    ax[i].legend(loc='upper right')
Input alpha: 0.01
 - Final results: [4.5 2.3]
 - N° steps: 2516
Input alpha: 0.25
 - Final results: [4.5 2.3]
 - N° steps: 88
Input alpha: 0.3
 - Final results: [4.5 2.3]
 - N° steps: 71
Input alpha: 0.35
 - Final results: [4.5 2.3]
 - N° steps: 93
Input alpha: 0.4
 - Final results: [ 4.5 -5\. ]
 - N° steps: 3000

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于作者。

当步长过大(α = 0.4)时,算法不收敛:xₖ 一直振荡而没有达到最小值,直到达到最大迭代次数。

我们可以通过观察每次迭代中的点 x 来更好地理解这种行为:

fig = plt.figure(figsize=(25, 60))

# For each step size
for i in range(0, len(alphas)):

    # First subplot
    ax = fig.add_subplot(5, 2, (i*2)+1, projection='3d')
    ax.contour3D(X, Y, Z, 60, cmap='viridis')
    ax.plot(X_estimates[i], Y_estimates[i], Z_estimates[i], color='red', label='alpha: '+str(alphas[i]) , linewidth=3)
    ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
    ax.set_xlabel('$x_{0}$')
    ax.set_ylabel('$x_{1}$')
    ax.set_zlabel('$f(x)$')
    ax.view_init(20, 20)
    plt.legend(prop={'size': 15})

    # Second third
    ax = fig.add_subplot(5, 2, (i*2)+2, projection='3d')
    ax.contour3D(X, Y, Z, 60, cmap='viridis')
    ax.plot(X_estimates[i], Y_estimates[i], Z_estimates[i], color='red', label='alpha: '+str(alphas[i]) , linewidth=3)
    ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
    ax.set_xlabel('$x_{0}$')
    ax.set_ylabel('$x_{1}$')
    ax.set_zlabel('$f(x)$')
    ax.axes.zaxis.set_ticklabels([])
    ax.view_init(90, -90)
    plt.legend(prop={'size': 15})

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于作者。

为了保证最陡下降法的收敛性,我们需要通过线搜索迭代更新 α

3.3. 使用 Armijo 条件的线搜索

让我们通过添加 Armijo 规则来修改我们之前的方法。在最陡下降循环中,计算下一个点 x - α ⋅ ∇f(x) 之前,我们需要选择一个合适的步长:我们从初始猜测 α = 1 开始,逐步将其值减半,直到满足 Armijo 条件。这个过程称为 回溯线搜索

def line_search(step, x, gradient_x, c = 1e-4, tol = 1e-8):
    '''
    Inexact line search where the step length is updated through the Armijo condition:
    $ f (x_k + α * p_k ) ≤ f ( x_k ) + c * α * ∇ f_k^T * p_k $

    Args:
      - step: starting alpha value
      - x: current point
      - gradient_x: gradient of the current point
      - c: constant value (default: 1e-4)
      - tol: tolerance value (default: 1e-6)
    Out:
      - New value of step: the first value found respecting the Armijo condition
    '''
    f_x = f(x)
    gradient_square_norm = np.linalg.norm(gradient_x)**2

    # Until the sufficient decrease condition is met 
    while f(x - step * gradient_x) >= (f_x - c * step * gradient_square_norm):

        # Update the stepsize (backtracking)
        step /= 2

        # If the step size falls below a certain tolerance, exit the loop
        if step < tol:
            break

    return step

def steepest_descent(gradient, x0 = np.zeros(2), max_iter = 10000, tolerance = 1e-10): 
    '''
    Steepest descent with alpha updated through line search (Armijo condition).

    Args:
      - gradient: gradient of the objective function
      - x0: initial guess for x_0 and x_1 (default values: zero) <numpy.ndarray>
      - max_iter: maximum number of iterations (default: 10000)
      - tolerance: minimum gradient magnitude at which the algorithm stops (default: 1e-10)

    Out:
      - results: <numpy.ndarray> with x_0 and x_1 values at each iteration
      - number of steps: <int>
    '''

    # Prepare list to store results at each iteration 
    results = np.array([])

    # Evaluate the gradient at the starting point 
    gradient_x = gradient(x0)

    # Initialize the steps counter 
    steps_count = 0

    # Set the initial point 
    x = x0 
    results = np.append(results, x, axis=0)

    # Iterate until the gradient is below the tolerance or maximum number of iterations is reached
    # Stopping criterion: inf norm of the gradient (max abs)
    while any(abs(gradient_x) > tolerance) and steps_count < max_iter:

        # Update the step size through the Armijo condition
        # Note: the first value of alpha is commonly set to 1
        alpha = line_search(1, x, gradient_x)

        # Update the current point by moving in the direction of the negative gradient 
        x = x - alpha * gradient_x

        # Store the result
        results = np.append(results, x, axis=0)

        # Evaluate the gradient at the new point 
        gradient_x = gradient(x) 

        # Increment the iteration counter 
        steps_count += 1 

    # Return the steps taken and the number of steps
    return results.reshape(-1, 2), steps_count

现在让我们使用线搜索优化目标函数:

# Steepest descent
points, iters = steepest_descent(
  df, x0 = np.array([-9, -9]))

# Found minimizer
minimizer = points[-1].round(1)

# Print results
print('- Final results: {}'.format(minimizer))
print('- N° steps: {}'.format(iters))

# Steepest descent steps
X_estimate, Y_estimate = points[:, 0], points[:, 1] 
Z_estimate = f(np.array([X_estimate, Y_estimate]))

# Plot
fig = plt.figure(figsize=(20, 20))

# First subplot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.view_init(20, 20)

# Second subplot
ax = fig.add_subplot(1, 2, 2, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(90, -90);
- Final results: [4.5 2.3]
- N° steps: 55

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于作者。

我们注意到在 55 步中达到了最小值,而使用常数步长 α 则导致了更多的迭代,或者没有收敛。

4. 结论

在这篇文章中,我们介绍并实现了最陡下降法,并在两个变量的二次函数上进行了测试。特别是,我们展示了如何使用足够减少条件(Armijo)迭代更新步长。

带有 Armijo 线搜索的最陡下降法保证了收敛,但通常较慢,因为它需要大量的迭代。

在未来的帖子中,我们将探讨通过改变线性搜索策略并提供不同的步长初始化方法来修改这个基本算法。

从零实现 Vision Transformer (ViT)

原文:towardsdatascience.com/implementing-vision-transformer-vit-from-scratch-3e192c6155f0?source=collection_archive---------9-----------------------#2023-03-07

通过从零实现 Vision Transformer (ViT) 了解其工作原理

Tin Nguyen 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Towards Data Science

·

关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 3 月 7 日

Vision Transformer (ViT) 是将 Transformer 模型适配于计算机视觉任务的一种方法。它由 Google 研究人员在 2020 年提出,并因其在各种图像分类基准测试中的卓越表现而获得广泛关注。ViT 已显示出在多个计算机视觉任务中实现了最先进的性能,并引起了计算机视觉社区的极大兴趣。

在这篇文章中,我们将从头开始实现 ViT 用于图像分类,使用 PyTorch。我们还将用 CIFAR-10 数据集训练我们的模型,这是一个流行的图像分类基准。通过这篇文章,你应该能很好地理解 ViT 的工作原理以及如何将其应用于自己的计算机视觉项目。

实现的代码可以在这个仓库中找到。

ViT 架构概述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

改编自 arxiv.org/abs/2010.11929

ViT 的架构灵感来源于 BERT,一个仅包含编码器的 transformer 模型,通常用于 NLP 监督学习任务,如文本分类或命名实体识别。ViT 的主要思想是图像可以被视为一系列小块,这些小块可以在 NLP 任务中作为 token 处理。

输入图像被分割成小块,然后将这些小块展平为向量序列。这些向量随后由一个 transformer 编码器处理,使得模型能够通过自注意力机制学习小块之间的交互。transformer 编码器的输出被送入分类层,输出输入图像的预测类别。

在接下来的部分,我们将逐一实现模型的每个组件,并使用 PyTorch 进行实现。这将帮助我们理解 ViT 模型的工作原理及其在计算机视觉任务中的应用。

将图像转换为嵌入

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了将输入图像馈送到 Transformer 模型中,我们需要将图像转换为向量序列。这通过将图像拆分成不重叠的小块,然后将这些小块线性投影以获得每个小块的固定大小嵌入向量来完成。我们可以使用 PyTorch 的nn.Conv2d层来实现这一点:

class PatchEmbeddings(nn.Module):
    """
    Convert the image into patches and then project them into a vector space.
    """

    def __init__(self, config):
        super().__init__()
        self.image_size = config["image_size"]
        self.patch_size = config["patch_size"]
        self.num_channels = config["num_channels"]
        self.hidden_size = config["hidden_size"]
        # Calculate the number of patches from the image size and patch size
        self.num_patches = (self.image_size // self.patch_size) ** 2
        # Create a projection layer to convert the image into patches
        # The layer projects each patch into a vector of size hidden_size
        self.projection = nn.Conv2d(self.num_channels, self.hidden_size, kernel_size=self.patch_size, stride=self.patch_size)

    def forward(self, x):
        # (batch_size, num_channels, image_size, image_size) -> (batch_size, num_patches, hidden_size)
        x = self.projection(x)
        x = x.flatten(2).transpose(1, 2)
        return x

kernel_size=self.patch_sizestride=self.patch_size 是为了确保层的过滤器应用于不重叠的小块。

在小块被转换为嵌入序列后,[CLS] token 被添加到序列的开头,它将在分类层中用于对图像进行分类。[CLS] token 的嵌入在训练过程中学习。

由于来自不同位置的小块可能对最终预测的贡献不同,我们还需要一种方法将小块的位置编码到序列中。我们将使用可学习的位置嵌入将位置信息添加到嵌入中。这类似于在 NLP 任务的 Transformer 模型中使用位置嵌入的方式。

class Embeddings(nn.Module):
    """
    Combine the patch embeddings with the class token and position embeddings.
    """

    def __init__(self, config):
        super().__init__()
        self.config = config
        self.patch_embeddings = PatchEmbeddings(config)
        # Create a learnable [CLS] token
        # Similar to BERT, the [CLS] token is added to the beginning of the input sequence
        # and is used to classify the entire sequence
        self.cls_token = nn.Parameter(torch.randn(1, 1, config["hidden_size"]))
        # Create position embeddings for the [CLS] token and the patch embeddings
        # Add 1 to the sequence length for the [CLS] token
        self.position_embeddings = \
            nn.Parameter(torch.randn(1, self.patch_embeddings.num_patches + 1, config["hidden_size"]))
        self.dropout = nn.Dropout(config["hidden_dropout_prob"])

    def forward(self, x):
        x = self.patch_embeddings(x)
        batch_size, _, _ = x.size()
        # Expand the [CLS] token to the batch size
        # (1, 1, hidden_size) -> (batch_size, 1, hidden_size)
        cls_tokens = self.cls_token.expand(batch_size, -1, -1)
        # Concatenate the [CLS] token to the beginning of the input sequence
        # This results in a sequence length of (num_patches + 1)
        x = torch.cat((cls_tokens, x), dim=1)
        x = x + self.position_embeddings
        x = self.dropout(x)
        return x

在这一步,输入图像被转换为带有位置信息的嵌入序列,并准备好输入 transformer 层。

多头注意力

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在深入 transformer 编码器之前,我们首先探索多头注意力模块,这是其核心组件。多头注意力用于计算输入图像中不同块之间的交互。多头注意力由多个注意力头组成,每个头部是一个单一的注意力层。

让我们实现多头注意力模块的一个头部。该模块接受一个嵌入序列作为输入,并为每个嵌入计算查询、键和值向量。查询和键向量随后用于计算每个令牌的注意力权重。注意力权重被用于使用值向量的加权和计算新的嵌入。我们可以将这种机制视为数据库查询的软版本,其中查询向量在数据库中找到最相关的键向量,并检索值向量以计算查询输出。

class AttentionHead(nn.Module):
    """
    A single attention head.
    This module is used in the MultiHeadAttention module.
    """
    def __init__(self, hidden_size, attention_head_size, dropout, bias=True):
        super().__init__()
        self.hidden_size = hidden_size
        self.attention_head_size = attention_head_size
        # Create the query, key, and value projection layers
        self.query = nn.Linear(hidden_size, attention_head_size, bias=bias)
        self.key = nn.Linear(hidden_size, attention_head_size, bias=bias)
        self.value = nn.Linear(hidden_size, attention_head_size, bias=bias)

        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # Project the input into query, key, and value
        # The same input is used to generate the query, key, and value,
        # so it's usually called self-attention.
        # (batch_size, sequence_length, hidden_size) -> (batch_size, sequence_length, attention_head_size)
        query = self.query(x)
        key = self.key(x)
        value = self.value(x)
        # Calculate the attention scores
        # softmax(Q*K.T/sqrt(head_size))*V
        attention_scores = torch.matmul(query, key.transpose(-1, -2))
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)
        attention_probs = nn.functional.softmax(attention_scores, dim=-1)
        attention_probs = self.dropout(attention_probs)
        # Calculate the attention output
        attention_output = torch.matmul(attention_probs, value)
        return (attention_output, attention_probs)

所有注意力头的输出随后被拼接并线性映射,以获得多头注意力模块的最终输出。

class MultiHeadAttention(nn.Module):
    """
    Multi-head attention module.
    This module is used in the TransformerEncoder module.
    """

    def __init__(self, config):
        super().__init__()
        self.hidden_size = config["hidden_size"]
        self.num_attention_heads = config["num_attention_heads"]
        # The attention head size is the hidden size divided by the number of attention heads
        self.attention_head_size = self.hidden_size // self.num_attention_heads
        self.all_head_size = self.num_attention_heads * self.attention_head_size
        # Whether or not to use bias in the query, key, and value projection layers
        self.qkv_bias = config["qkv_bias"]
        # Create a list of attention heads
        self.heads = nn.ModuleList([])
        for _ in range(self.num_attention_heads):
            head = AttentionHead(
                self.hidden_size,
                self.attention_head_size,
                config["attention_probs_dropout_prob"],
                self.qkv_bias
            )
            self.heads.append(head)
        # Create a linear layer to project the attention output back to the hidden size
        # In most cases, all_head_size and hidden_size are the same
        self.output_projection = nn.Linear(self.all_head_size, self.hidden_size)
        self.output_dropout = nn.Dropout(config["hidden_dropout_prob"])

    def forward(self, x, output_attentions=False):
        # Calculate the attention output for each attention head
        attention_outputs = [head(x) for head in self.heads]
        # Concatenate the attention outputs from each attention head
        attention_output = torch.cat([attention_output for attention_output, _ in attention_outputs], dim=-1)
        # Project the concatenated attention output back to the hidden size
        attention_output = self.output_projection(attention_output)
        attention_output = self.output_dropout(attention_output)
        # Return the attention output and the attention probabilities (optional)
        if not output_attentions:
            return (attention_output, None)
        else:
            attention_probs = torch.stack([attention_probs for _, attention_probs in attention_outputs], dim=1)
            return (attention_output, attention_probs)

Transformer 编码器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

transformer 编码器由一系列 transformer 层组成。每个 transformer 层主要由我们刚刚实现的多头注意力模块和一个前馈网络组成。为了更好地扩展模型和稳定训练,transformer 层中添加了两个层归一化层和跳过连接。

让我们实现一个 transformer 层(在代码中称为Block,因为它是 transformer 编码器的构建块)。我们将从前馈网络开始,它是一个简单的两层 MLP,中间有 GELU 激活函数。

class MLP(nn.Module):
    """
    A multi-layer perceptron module.
    """

    def __init__(self, config):
        super().__init__()
        self.dense_1 = nn.Linear(config["hidden_size"], config["intermediate_size"])
        self.activation = NewGELUActivation()
        self.dense_2 = nn.Linear(config["intermediate_size"], config["hidden_size"])
        self.dropout = nn.Dropout(config["hidden_dropout_prob"])

    def forward(self, x):
        x = self.dense_1(x)
        x = self.activation(x)
        x = self.dense_2(x)
        x = self.dropout(x)
        return x

我们已经实现了多头注意力和 MLP,可以将它们结合起来创建 transformer 层。跳过连接和层归一化被应用于每一层的输入。

class Block(nn.Module):
    """
    A single transformer block.
    """

    def __init__(self, config):
        super().__init__()
        self.attention = MultiHeadAttention(config)
        self.layernorm_1 = nn.LayerNorm(config["hidden_size"])
        self.mlp = MLP(config)
        self.layernorm_2 = nn.LayerNorm(config["hidden_size"])

    def forward(self, x, output_attentions=False):
        # Self-attention
        attention_output, attention_probs = \
            self.attention(self.layernorm_1(x), output_attentions=output_attentions)
        # Skip connection
        x = x + attention_output
        # Feed-forward network
        mlp_output = self.mlp(self.layernorm_2(x))
        # Skip connection
        x = x + mlp_output
        # Return the transformer block's output and the attention probabilities (optional)
        if not output_attentions:
            return (x, None)
        else:
      return (x, attention_probs)

transformer 编码器将多个 transformer 层按顺序堆叠在一起:

class Encoder(nn.Module):
    """
    The transformer encoder module.
    """

    def __init__(self, config):
        super().__init__()
        # Create a list of transformer blocks
        self.blocks = nn.ModuleList([])
        for _ in range(config["num_hidden_layers"]):
            block = Block(config)
            self.blocks.append(block)

    def forward(self, x, output_attentions=False):
        # Calculate the transformer block's output for each block
        all_attentions = []
        for block in self.blocks:
            x, attention_probs = block(x, output_attentions=output_attentions)
            if output_attentions:
                all_attentions.append(attention_probs)
        # Return the encoder's output and the attention probabilities (optional)
        if not output_attentions:
            return (x, None)
        else:
            return (x, all_attentions)

用于图像分类的 ViT

将图像输入到嵌入层和 transformer 编码器后,我们获得了图像块和[CLS]标记的新嵌入。此时,嵌入经过 transformer 编码器处理后应该具有一些用于分类的有用信号。类似于 BERT,我们将仅使用[CLS]标记的嵌入传递给分类层。

分类层是一个全连接层,它接受[CLS]嵌入作为输入,并输出每张图像的 logits。以下代码实现了用于图像分类的 ViT 模型:

class ViTForClassfication(nn.Module):
    """
    The ViT model for classification.
    """

    def __init__(self, config):
        super().__init__()
        self.config = config
        self.image_size = config["image_size"]
        self.hidden_size = config["hidden_size"]
        self.num_classes = config["num_classes"]
        # Create the embedding module
        self.embedding = Embeddings(config)
        # Create the transformer encoder module
        self.encoder = Encoder(config)
        # Create a linear layer to project the encoder's output to the number of classes
        self.classifier = nn.Linear(self.hidden_size, self.num_classes)
        # Initialize the weights
        self.apply(self._init_weights)

    def forward(self, x, output_attentions=False):
        # Calculate the embedding output
        embedding_output = self.embedding(x)
        # Calculate the encoder's output
        encoder_output, all_attentions = self.encoder(embedding_output, output_attentions=output_attentions)
        # Calculate the logits, take the [CLS] token's output as features for classification
        logits = self.classifier(encoder_output[:, 0])
        # Return the logits and the attention probabilities (optional)
        if not output_attentions:
            return (logits, None)
        else:
            return (logits, all_attentions)

要训练模型,可以遵循训练分类模型的标准步骤。你可以在这里找到训练脚本。

结果

由于目标不是实现最先进的性能,而是展示模型的工作原理,我训练的模型远小于论文中描述的原始 ViT 模型,这些模型至少有 12 层,隐藏层大小为 768。我用于训练的模型配置是:

{
    "patch_size": 4,
    "hidden_size": 48,
    "num_hidden_layers": 4,
    "num_attention_heads": 4,
    "intermediate_size": 4 * 48,
    "hidden_dropout_prob": 0.0,
    "attention_probs_dropout_prob": 0.0,
    "initializer_range": 0.02,
    "image_size": 32,
    "num_classes": 10,
    "num_channels": 3,
    "qkv_bias": True,
}

该模型在 CIFAR-10 数据集上训练了 100 轮,批量大小为 256。学习率设置为 0.01,并且没有使用学习率调整。经过 100 轮训练后,模型达到了 75.5% 的准确率。下图展示了训练期间的训练损失、测试损失和测试集上的准确率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下图展示了模型对一些测试图像的注意力图。你可以看到模型能够识别不同类别的对象。它学会了关注对象并忽略背景。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论

在这篇文章中,我们学习了 Vision Transformer 的工作原理,从嵌入层到变换器编码器,最后到分类层。我们还学习了如何使用 PyTorch 实现模型的每个组件。

由于该实现不用于生产环境,如果你打算训练全尺寸模型或在大型数据集上训练,建议使用更成熟的变换器库,如 HuggingFace

最初发布于 https://tintn.github.io 2023 年 3 月 7 日。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值