重要提示

您正在查看 NeMo 2.0 文档。此版本对 API 和一个新库 NeMo Run 进行了重大更改。我们目前正在将 NeMo 1.0 的所有功能移植到 2.0。有关先前版本或 2.0 中尚不可用的功能的文档,请参阅 NeMo 24.07 文档

适配器组件#

适配器可以被视为添加到预先存在的模块/模型的任何参数集。在我们的例子中,我们目前支持文献中的标准适配器,更高级的适配器模块正在研究中,并有可能得到 NeMo 的支持。

适配器模块可以是任何 pytorch 模块,但它必须遵循某些简单的要求 -

  1. 模型接受某种输入维度的输入,并且其输出必须与此维度匹配。

  2. 理想情况下,模块的初始化应使适配器初始化时的输出不会修改原始输入。这允许模型即使在添加了其他参数的情况下也能产生相同的输出结果。

根据 Junxian 等人 [1] 的研究,我们可以认为适配器由三个组件组成 -

  1. 功能形式 - 将修改输入的可训练参数

  2. 插入形式 - 适配器输出与原始输入集成的位置。适配器的输入可以是层的最后一个输出、某些注意力层的输入,甚至可以是模块本身的原始输入(甚至在模块前向传递之前)。

  3. 组合函数 - 适配器输出如何与输入集成。它可以像残差加法连接、级联或逐点乘法等一样简单。

功能形式 - 适配器网络#

适配器模块代表适配器的功能形式。我们讨论文献中最常用的适配器模块的示例,名为 LinearAdapter(或 Houlsby 适配器) [2]

注意

所有适配器模块都必须扩展 AdapterModuleUtil,并且理想情况下应该具有等效的 DataClass 配置以便于实例化!

class nemo.collections.common.parts.adapter_modules.AdapterModuleUtil

基类: AccessMixin

适配器模块的基类,为所有适配器模块提供通用功能。

setup_adapter_strategy(
adapter_strategy: AbstractAdapterStrategy | None,
)

设置此类的适配器策略,从而能够动态更改适配器输出与输入合并的方式。

成功调用后,会将变量 adapter_strategy 分配给模块。

参数:

adapter_strategy – 可以是 None 或 AbstractAdapterStrategy 的实现。

get_default_strategy_config() dataclass

返回默认适配器模块策略。

adapter_unfreeze()

将适配器中所有参数的 requires_grad 设置为 True。对于所需的任何自定义解冻行为,应覆盖此方法。例如,如果并非适配器的所有参数都应解冻。


class nemo.collections.common.parts.adapter_modules.LinearAdapter(*args: Any, **kwargs: Any)

基类: Module, AdapterModuleUtil

简单的线性前馈适配器模块,具有 LayerNorm 和带激活函数的单个隐藏层。注意:适配器显式地将其最后一层初始化为全零,以避免在禁用所有适配器时影响原始模型。

参数:
  • in_features – 模块的输入维度。请注意,对于适配器,input_dim == output_dim。

  • dim – 前馈网络的隐藏维度。

  • activation – 激活函数的 Str 名称。

  • norm_position – Str,可以是 prepost。默认为 pre。确定归一化将发生在第一层还是最后一层。某些架构可能更喜欢其中一种。

  • dropout – float 值,是否对适配器最后一层的输出执行 dropout。

  • adapter_strategy – 默认情况下为 ResidualAddAdapterStrategyConfig。适配器组合函数对象。

插入形式 - 模块适配器#

适配器模块可以集成到给定模块的许多不同位置。例如,可以有一个适配器仅影响每个模块中最后一层的输出。我们还可以有一个 Parallel Adapter [1],它在模块本身的输入处运行,与模块的前向传递并行。另一个插入位置是在多头注意力层内部。

除此之外,虽然适配器通常仅在包含最多参数的层(例如网络的编码器)中使用,但某些模型可以在多个位置支持适配器(用于语言模型、机器翻译的编码器-解码器架构,甚至用于具有 Transducer 损失的 ASR 的编码器-解码器-联合架构)。因此,NeMo 利用了 Module Adapters 的概念。

在添加适配器时,Module Adapters 的定义非常简单 - 通过指定应将适配器插入到的模块。

# Get the list of supported modules / locations in a adapter compatible Model
print(model.adapter_module_names)  # assume ['', 'encoder', 'decoder']

# When calling add_adapter, specify the module name in the left of the colon symbol, and the adapter name afterwords.
# The adapter is then directed to the decoder module instead of the default / encoder module.
model.add_adapter("decoder:first_adapter", cfg=...)

您可能会注意到 model.adapter_module_names 有时会返回 '' 作为支持的模块名称之一 - 这指的是“默认模块”。通常,我们尝试将默认值提供为文献中最常用的适配器 - 例如,NLP/NMT/ASR 中的编码器适配器。

组合函数 - 适配器策略#

最后,我们讨论如何组合适配器模块的输入和输出。为了概括此步骤,我们构建了 Adapter Strategies。策略是任何扩展 AbstractAdapterStrategy 的类(不是 torch.nn.Module!),并提供一个 forward() 方法,该方法接受输入的特定签名并生成一个输出张量,该张量使用某种特定方法组合输入和输出。

我们在下面讨论一个简单的残差加法连接策略 - 它接受适配器的输入和适配器的输出,并将它们简单地加在一起。它还支持 stochastic_depth,这使得适配器可以在训练期间动态关闭,从而使训练更加稳健。

class nemo.core.classes.mixins.adapter_mixin_strategies.AbstractAdapterStrategy

基类: ABC

forward(
input: torch.Tensor,
adapter: torch.nn.Module,
*,
module: AdapterModuleMixin,
)

前向方法,定义适配器的输出应如何与输入合并,或者是否应合并。

还提供了调用此策略的模块 - 从而允许访问调用模块中的所有其他适配器。如果一个适配器是元适配器,它组合了各种适配器的输出,这将非常有用。在这种情况下,输入可以跨所有其他适配器转发,收集它们的输出,然后可以通过某种策略合并这些输出。例如,请参阅

参数:
  • input – 模块的原始输出张量,或上一个适配器的输出(如果启用了多个适配器)。

  • adapter – 当前需要执行前向传递的适配器模块。

  • module – 调用模块的整体。它是一个实现 AdapterModuleMixin 的模块,因此策略可以通过 module.adapter_layer 访问此模块中的所有其他适配器。

返回:

结果张量,在其中一个活动适配器完成其前向传递之后。


class nemo.core.classes.mixins.adapter_mixin_strategies.ResidualAddAdapterStrategy(
stochastic_depth: float = 0.0,
l2_lambda: float = 0.0,
)

基类: AbstractAdapterStrategy

适配器模块与其输入的残差加法实现。支持随机深度正则化。

forward(
input: torch.Tensor,
adapter: torch.nn.Module,
*,
module: AdapterModuleMixin,
)

一种基本策略,包括在底层适配器前向传递后,输入上的残差连接。

参数:
  • input – 模块的原始输出张量,或上一个适配器的输出(如果启用了多个适配器)。

  • adapter – 当前需要执行前向传递的适配器模块。

  • module – 调用模块的整体。它是一个实现 AdapterModuleMixin 的模块,因此策略可以通过 module.adapter_layer 访问此模块中的所有其他适配器。

返回:

结果张量,在其中一个活动适配器完成其前向传递之后。

compute_output(
input: torch.Tensor,
adapter: torch.nn.Module,
*,
module: AdapterModuleMixin,
) torch.Tensor

计算单个适配器对某些输入的输出。

参数:
  • input – 模块的原始输出张量,或上一个适配器的输出(如果启用了多个适配器)。

  • adapter – 当前需要执行前向传递的适配器模块。

  • module – 调用模块的整体。它是一个实现 AdapterModuleMixin 的模块,因此策略可以通过 module.adapter_layer 访问此模块中的所有其他适配器。

返回:

结果张量,在其中一个活动适配器完成其前向传递之后。

apply_stochastic_depth(
output: torch.Tensor,
input: torch.Tensor,
adapter: torch.nn.Module,
*,
module: AdapterModuleMixin,
)

如果概率大于 0,则计算并应用随机深度。

参数:
  • output – 结果张量,在其中一个活动适配器完成其前向传递之后。

  • input – 模块的原始输出张量,或上一个适配器的输出(如果启用了多个适配器)。

  • adapter – 当前需要执行前向传递的适配器模块。

  • module – 调用模块的整体。它是一个实现 AdapterModuleMixin 的模块,因此策略可以通过 module.adapter_layer 访问此模块中的所有其他适配器。

返回:

结果张量,在可能对其应用随机深度之后。

compute_auxiliary_losses(
output: torch.Tensor,
input: torch.Tensor,
adapter: torch.nn.Module,
*,
module: AdapterModuleMixin,
)

计算任何辅助损失并将其保存在张量注册表中。

参数:
  • output – 结果张量,在其中一个活动适配器完成其前向传递之后。

  • input – 模块的原始输出张量,或上一个适配器的输出(如果启用了多个适配器)。

  • adapter – 当前需要执行前向传递的适配器模块。

  • module – 调用模块的整体。它是一个实现 AdapterModuleMixin 的模块,因此策略可以通过 module.adapter_layer 访问此模块中的所有其他适配器。


参考#

[1] (1,2)

Junxian He, Chunting Zhou, Xuezhe Ma, Taylor Berg-Kirkpatrick 和 Graham Neubig. Towards a unified view of parameter-efficient transfer learning. 2021. URL: https://arxiv.org/abs/2110.04366, doi:10.48550/ARXIV.2110.04366.

[2]

Neil Houlsby, Andrei Giurgiu, Stanislaw Jastrzebski, Bruna Morrone, Quentin De Laroussilhe, Andrea Gesmundo, Mona Attariyan 和 Sylvain Gelly. Parameter-efficient transfer learning for nlp. In International Conference on Machine Learning, 2790–2799. PMLR, 2019.