原文:LoRA: Low-Rank Adaptation of Large Language Models

1 简介

冻结预训练模型的权重,在 Transformer 架构的每一层注入可训练的 秩分解矩阵,从而大大减少了下游任务的可训练参数数量。

术语:

  • the input and output dimension size of a Transformer layer: $d_{model}$
  • the query/key/value/output projection matrices in the self-attention module: $W_q,W_k, W_v, W_o$
  • the pretrained weight matrix: $W$ ($W_0$)
  • accumulated gradient update during adaption: $\Delta W$
  • the rank of a LoRA module: $r$
  • Transformer MLP feedforward dimension $d_{ffn} = 4\times d_{model}$

问题描述:

  • 给定一个由 $\Phi$ 参数化的预训练自回归语言模型 $P_{\Phi}(y|x)$ ,考虑将这个预训练模型调整到下游条件文本生成任务。
  • 在完全微调过程中,模型初始化为预训练权重 $\Phi_0$ ,并通过反复遵循梯度以最大化条件语言建模目标,从而更新为 $\Phi_0 + \Delta\Phi$ :

    $$ \max_{\Phi}\sum_{(x,y)\in \mathcal Z} \sum_{t=1}^{|y|}\log (P_{\Phi}(y_t|x,y_{<t})) $$

    对于完全微调,$|\Phi_0| = |\Delta\Phi|$ ,对于大语言模型会导致存储和部署面临挑战。

  • 本方法中,任务特定的参数增量 $\Delta\Phi = \Delta\Phi(\Theta)$ 被进一步编码为一组更小规模的参数,其维度满足 $|\Theta|\ll|\Phi_0|$,寻找 $\Delta\Phi$ 的任务变成了对 $\Theta$ 的优化:

    $$ \max_{\Theta}\sum_{(x,y)\in \mathcal Z} \sum_{t=1}^{|y|}\log (P_{\Phi + \Delta\Phi(\Theta)}(y_t|x,y_{<t})) $$

2 方法

  • 出发点:当适应特定任务时,预训练语言模型具有较低的“内在维度”
  • 假设在 适应过程 中 权重的更新具有较低的“内在秩”
  • 对于一个预训练的权重矩阵 $W_0\in \mathbb R^{d\times k}$ ,通过 低秩分解 $W_0 + \Delta W = W_0 + BA$ 来约束它的更新,其中 $B\in \mathbb R^{d\times r}, A\in \mathbb R^{r\times k}$ ,且秩 $r\ll \min(d, k)$ 。
  • 在训练过程中,$W_0$ 是冻结的,不接受梯度更新,而 A 和 B 包含可训练的参数。

    • 注意: $W_0$ 和 $\Delta W = BA$ 都与相同的输入 $x$ 相乘,它们各自输出向量在坐标上相加,对于 $h = W_0 x$ ,修改后的前向传播结果为:

      $$ h = W_0 x + \Delta Wx = W_0 x + BAx $$

      • 对 $A$ 使用随机高斯初始化
      • 对 $B$ 使用零初始化
    • 将 $\Delta W x$ 扩展为 $\frac{\alpha}{r}$ ,其中 $\alpha$ 是 $r$ 中的一个常数,调节 $\alpha$ 与 Adam 优化中调节学习率大致相同。
  • LoRA的秩 $r$ 设置为 预训练权重矩阵的秩,将等同于完全微调。

3 将 LoRA 应用于 Transformer

Transformer 架构中:

  • self-attention 模块中有 四个权重矩阵:$W_q$, $W_k$, $W_v$, $W_o$
  • MLP模块中有两个

将 $W_q$ ($W_k$ 或 $W_v$) 视为一个维度为 $d_{model}\times d_{model}$ 的单一矩阵,即使输出维度通常会被切分为 attention heads。论文中的研究限制在仅适配 attention weights 用于下游任务,并冻结 MLP 模块(因此它们在下游任务不参与训练),以简化和提高参数效率。

4 源代码

地址:https://github.com/microsoft/LoRA/blob/main/loralib

LoRALayer:LoRA层的基类

class LoRALayer():
    def __init__(
        self, 
        r: int,  # 秩
        lora_alpha: int,  # 缩放因子
        lora_dropout: float,
        merge_weights: bool,  # 决定在推理时将 LoRA 权重合并到原始权重中
    ):
        self.r = r
        self.lora_alpha = lora_alpha
        # Optional dropout
        if lora_dropout > 0.:
            self.lora_dropout = nn.Dropout(p=lora_dropout)
        else:
            self.lora_dropout = lambda x: x
        # Mark the weight as unmerged
        self.merged = False
        self.merge_weights = merge_weights

结合 LoRA 的 Embedding层:

class Embedding(nn.Embedding, LoRALayer):
    # LoRA implemented in a dense layer
    def __init__(
        self,
        num_embeddings: int,  # 词汇表大小
        embedding_dim: int,   # 嵌入向量的维度
        r: int = 0,
        lora_alpha: int = 1,
        merge_weights: bool = True,
        **kwargs  # 其他传递给 nn.Embedding 的参数
    ):
        nn.Embedding.__init__(self, num_embeddings, embedding_dim, **kwargs)
        LoRALayer.__init__(self, r=r, lora_alpha=lora_alpha, lora_dropout=0,
                           merge_weights=merge_weights)
        # Actual trainable parameters
        if r > 0:  # 具有 LoRA 层的 Embedding 层
            # A: r*num_embedding    B: embedding_dim*r
            self.lora_A = nn.Parameter(self.weight.new_zeros((r, num_embeddings)))  
            self.lora_B = nn.Parameter(self.weight.new_zeros((embedding_dim, r)))
            self.scaling = self.lora_alpha / self.r
            # 冻结 预训练权重矩阵
            self.weight.requires_grad = False
        self.reset_parameters()

    def reset_parameters(self):  # 重置嵌入层和LoRA层的参数
        nn.Embedding.reset_parameters(self)
        if hasattr(self, 'lora_A'):
            # initialize A the same way as the default for nn.Linear and B to zero
            nn.init.zeros_(self.lora_A)  # 零初始化
            nn.init.normal_(self.lora_B)  # 初始化为正态分布随机矩阵

    def train(self, mode: bool = True):
        nn.Embedding.train(self, mode)
        if mode:  # 训练模式
            if self.merge_weights and self.merged:  # 权重合并
                # Make sure that the weights are not merged
                if self.r > 0:
                    self.weight.data -= (self.lora_B @ self.lora_A).transpose(0, 1) * self.scaling
                self.merged = False
        else:  # 推理模式
            if self.merge_weights and not self.merged:  # 权重未合并
                # Merge the weights and mark it
                if self.r > 0:
                    self.weight.data += (self.lora_B @ self.lora_A).transpose(0, 1) * self.scaling
                self.merged = True
        
    def forward(self, x: torch.Tensor):
        if self.r > 0 and not self.merged:  # 权重未合并
            result = nn.Embedding.forward(self, x)
            after_A = F.embedding(
                x, self.lora_A.transpose(0, 1), self.padding_idx, self.max_norm,
                self.norm_type, self.scale_grad_by_freq, self.sparse
            )
            # x: 输入数据, (batch_size, sequence_length)
            # lora_A.transpose(0,1): (num_embeddings, r)
            # after_A: 输入x 通过 lora_A 映射后的结果, (batch_size, sequence_length, r)
            result += (after_A @ self.lora_B.transpose(0, 1)) * self.scaling
            # lora_B.transpose(0,1): (r, embedding_dim)
            # result: 矩阵乘法, (batch_size, sequence_length, embedding_dim)
            # scaling: 缩放因子, alpha/r
            return result
        else:
            return nn.Embedding.forward(self, x)
Theme Jasmine by Kent Liao
赣ICP备2024043307号 赣公网安备36060002000103号