transformer代码学习


前言

一、 LayerNorm

在实现层归一化(Layer Normalization)时,self.a_2self.b_2 是用于缩放和平移归一化输出的可学习参数。具体来说:

  • self.a_2 = nn.Parameter(torch.ones(features)) 定义了一个与特征维度相同大小的向量,初始化为1。这个参数用于对归一化后的每个特征进行缩放(乘法操作)。通过学习这个参数,模型可以决定是否以及如何调整每个特征的重要性。

  • self.b_2 = nn.Parameter(torch.zeros(features)) 同样定义了一个与特征维度相同大小的向量,但初始化为0。这个参数用于对归一化后的每个特征进行平移(加法操作)。通过学习这个参数,模型可以在必要时为每个特征添加一个偏置。

这两个参数允许层归一化不仅仅是一个简单的标准化过程,而是可以通过学习来调整每个特征的分布,从而更好地适应特定任务的需求。这种机制类似于批量归一化(Batch Normalization)中的gammabeta参数。

层归一化的公式

在这里插入图片描述

PyTorch 中的 LayerNorm 实现

在PyTorch中,nn.LayerNorm 类已经内置了这些参数,并且默认情况下会自动创建并初始化它们。如果你查看PyTorch源代码,你会发现类似以下的构造函数定义:

class LayerNorm(nn.Module):
    def __init__(self, normalized_shape, eps=1e-05, elementwise_affine=True):
        super(LayerNorm, self).__init__()
        if elementwise_affine:
            self.weight = nn.Parameter(torch.ones(normalized_shape))
            self.bias = nn.Parameter(torch.zeros(normalized_shape))
        else:
            self.register_parameter('weight', None)
            self.register_parameter('bias', None)
        self.normalized_shape = tuple(normalized_shape)
        self.eps = eps
        self.elementwise_affine = elementwise_affine

这里,weight 对应于 self.a_2,而 bias 对应于 self.b_2。如果 elementwise_affine 设置为 True(默认值),则会创建并初始化这两个参数;否则,它们将不会被创建,这意味着层归一化将仅执行标准化而不进行任何额外的缩放或平移操作。

为什么需要这些参数?

引入 ab 参数的原因在于,尽管归一化有助于稳定训练过程,但它也可能导致信息丢失。例如,当所有的特征都被标准化到相同的均值和方差后,某些原始数据中的细微差异可能会被抹去。通过让模型学习适当的缩放和平移参数,我们可以部分地恢复这些信息,使得归一化后的表示更加丰富和有表现力。

总之,self.a_2self.b_2 是为了使层归一化更加灵活和强大,能够在保持其优势的同时,尽量减少潜在的信息损失。

二、残差及归一化顺序问题

确实,在实际应用中,关于层归一化(Layer Normalization)和残差连接(Residual Connection)的顺序存在两种主要的实现方式。这两种不同的顺序选择可以在不同的论文和代码实现中找到,并且它们各有其理论依据和支持者。以下是这两种常见的顺序:

归一化在前(Pre-Normalization)

在这种设计中,输入首先通过层归一化,然后传递给子层(如自注意力机制或前馈网络),最后将子层输出与原始输入相加形成残差连接。这是许多现代Transformer变体(如GPT-2, GPT-3, T5等)所采用的设计。

优点:

  • 更稳定的梯度流:由于归一化是在激活函数之前进行的,它有助于稳定梯度流动,从而促进更深层次模型的训练。
  • 加速收敛:实验证明,这种架构可以加快训练速度,并且通常能够达到更好的最终性能。

代码示例:

class PreNormSublayerConnection(nn.Module):
    def __init__(self, size, dropout):
        super(PreNormSublayerConnection, self).__init__()
        self.norm = nn.LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        # 先归一化再通过子层,最后加上残差
        return x + self.dropout(sublayer(self.norm(x)))

归一化在后(Post-Normalization)

另一种方法是先执行子层操作,然后添加残差连接,最后对结果进行层归一化。这是最初的Transformer论文(Vaswani et al., 2017)中提出的方案。

优点:

  • 符合直觉:对于一些开发者来说,这种顺序可能更容易理解,因为它遵循了“先变换再标准化”的逻辑流程。
  • 历史原因:作为原始Transformer架构的一部分,它被广泛接受并应用于许多早期实现中。

代码示例:

class PostNormSublayerConnection(nn.Module):
    def __init__(self, size, dropout):
        super(PostNormSublayerConnection, self).__init__()
        self.norm = nn.LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        # 先通过子层,再加上残差,最后归一化
        return self.norm(x + self.dropout(sublayer(x)))

实践中的选择

在实践中,选择哪种顺序取决于具体的任务需求、模型架构以及个人偏好。尽管有研究表明预归一化(Pre-Norm)版本在某些情况下表现更好,但并不是所有情况下都是如此。例如,对于特定的任务或数据集,后归一化(Post-Norm)可能会给出更好的结果。因此,最好的做法是尝试不同的配置,并根据实验结果做出决定。

此外,值得注意的是,随着研究的发展,新的规范化技术不断涌现,比如RMSNorm、GroupNorm等,它们也可能影响到归一化和残差连接的最佳实践。因此,保持对最新文献和技术趋势的关注是非常重要的。

总之,无论是采用归一化在前还是归一化在后的策略,关键是确保整个系统的设计能够有效地支持模型的学习过程,并为特定的应用场景提供最佳的性能。

三、lambda匿名函数使用

lambda x: self.self_attn(x, x, x, mask) 的使用是为了创建一个匿名函数(即 lambda 函数),它接受单个参数 x 并将其传递给 self.self_attn 方法。这种做法在某些情况下是有其特定目的和优势的,尤其是在与高阶函数或需要传递部分参数的情况下。

使用 Lambda 函数的原因

  1. 封装逻辑:通过使用 lambda,可以将对 self.self_attn 的调用封装在一个函数对象中,这使得代码更加模块化和可读。例如,在 SublayerConnection 类中,我们希望传递的是一个子层操作,而不仅仅是直接调用该操作的结果。这样做可以让 SublayerConnection 更加通用,因为它只需要知道如何处理一个函数对象,而不必关心这个函数的具体实现细节。

  2. 部分应用(Partial Application):有时候你可能只想固定某些参数,而让其他参数保持灵活。在这个例子中,mask 参数已经被固定了,但 x 仍然是变化的。通过 lambda 函数,我们可以轻松地创建一个新的函数,它只接受 x 作为参数,并且每次调用时都使用相同的 mask。这相当于实现了函数的部分应用。

  3. 延迟执行:使用 lambda 可以推迟实际的计算直到真正需要结果的时候。这对于优化性能或者确保在正确的上下文中执行某些操作非常有用。比如,在某些框架中,图构建阶段和执行阶段是分开的,这时你可能需要定义一个操作,但不立即执行它。

直接调用 vs Lambda 调用

理论上,如果你确定 self.self_attn(x, x, x, mask) 是你想要执行的确切操作,并且没有其他额外的需求(如上述提到的封装、部分应用或延迟执行),那么你可以直接调用 self.self_attn(x, x, x, mask)。然而,这样做会失去灵活性,因为:

  • 如果将来你需要改变传递给 self_attn 的参数顺序或添加新的参数,你必须修改所有直接调用的地方。
  • 它限制了 SublayerConnection 的复用性,因为你已经硬编码了具体的子层行为。

示例对比

使用 Lambda 函数
class SublayerConnection(nn.Module):
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = nn.LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))

# 在 EncoderLayer 中使用
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
直接调用
class SublayerConnection(nn.Module):
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = nn.LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        # 假设 sublayer 是一个方法而不是函数对象
        return x + self.dropout(sublayer(x))

# 在 EncoderLayer 中使用
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))  # 这里仍然需要使用 lambda 或者调整为:
x = self.sublayer[0](x, functools.partial(self.self_attn, mask=mask))

总结

虽然直接调用 self.self_attn(x, x, x, mask) 看起来更简单直接,但在实际开发中,使用 lambda 函数提供了一种更为灵活和强大的方式来处理复杂的逻辑。它不仅提高了代码的可读性和可维护性,还为未来的扩展和修改留下了空间。因此,在许多情况下,特别是当你不确定未来的需求变化或者希望编写更具通用性的代码时,选择使用 lambda 或类似的函数式编程技巧可能是更好的实践。

此外,值得注意的是,Python 提供了 functools.partial 来实现类似的功能,它可以用来固定一些参数并返回一个新的函数对象。对于更复杂的情况,考虑使用 partial 可能是一个更好的选择,因为它比 lambda 更清晰,并且支持多行表达式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值