Loading... <div class="tip inlineBlock share"> 译文链接: https://lightning.ai/lightning-ai/studios/code-lora-from-scratch 译文代码: https://lightning.ai/lightning-ai/studios/code-lora-from-scratch?tab=files&layout=column&path=cloudspaces%2F01hm9hypqc6y1hrapb5prmtz0h 论文链接: https://arxiv.org/abs/2106.09685 论文代码: https://github.com/microsoft/LoRA </div> ## 前言 LoRA,即[低秩适应]([Low-Rank Adaptation](https://arxiv.org/abs/2106.09685)),是一种更高效地微调LLMs的流行技术。与调整深度神经网络的所有参数不同,LoRA 仅关注更新一小部分低秩矩阵。 ![Refer to caption](https://zoe.red/usr/uploads/2024/04/1734859757.png) 本文通过从头开始编码来解释 LoRA 的工作原理,这是一个了解算法内部机制的绝佳实践。 --- ## Following the Code Examples 您可以在附带的工作室中找到重现文章中结果的所有代码(文章开头代码库链接)。只需点击本页面顶部的“运行”按钮即可访问。在复制了工作室之后,您将获得一个完整的代码环境副本,并准备好运行代码文件。无需额外的安装、下载或其他设置步骤。 --- ## Understanding LoRA 预训练的LLMs通常被称为基础模型,因为它们在各种任务中具有多样性。然而,将预训练的LLM适应特定的数据集或任务通常是有用的,我们可以通过微调来实现。 微调允许模型适应特定领域,而无需昂贵的预训练,但更新所有层仍然可能计算成本高昂,特别是对于较大的模型。 LoRA 为常规微调提供了更高效的替代方案。正如在论文[LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685)中更详细地讨论的那样,LoRA 以低秩格式近似了训练过程中的一层权重变化$ΔW$。 例如,在常规微调中,我们将权重矩阵$ W$ 的权重更新计算为$ΔW$,而在 LoRA 中,我们通过两个较小矩阵$ AB$ 的矩阵乘法来近似ΔW(2层全连接变换),如下图所示。(如果您熟悉 PCA 或 SVD,请将其视为将$ΔW$ 分解为 $A$ 和$ B$。) ![](https://zoe.red/usr/uploads/2024/04/2880261298.png)常规微调(左)和 LoRA(右)中前向传递期间的权重更新的比较 请注意,上图中的 $r$ 是一个超参数(rank, 对应2层全连接矩阵中间的维度大小),我们可以用它来指定用于适应的低秩矩阵的秩。较小的 $r$ 会导致更简单的低秩矩阵,从而在适应期间减少要学习的参数。这可能导致更快的训练和潜在的降低计算要求。然而,较小的$ r$ 会降低低秩矩阵捕捉任务特定信息的能力。 举个例子,假设给定层的权重矩阵 $W$ 的大小为 5,000x10,000(总共有 50M 个参数)。如果我们选择一个秩$ r=8$,我们初始化两个矩阵:一个大小为 5,000x8 的矩阵 B 和一个大小为 8x10,000 的矩阵 A。将 A 和 B 相加,总共只有 80,000 + 40,000 = 120,000 个参数,这比通过$ΔW$ 进行常规微调的 50M 个参数小 400 倍。 在实践中,重要的是尝试不同的 $r $值,以找到在新任务上实现所需性能的正确平衡。 --- ## Coding LoRA from Scratch 由于概念解释有时可能比较抽象,让我们现在自己实现 LoRA 以更好地了解它的工作原理。在代码中,我们可以按照以下方式实现一个 LoRA 层: ```python class LoRALayer(torch.nn.Module): def __init__(self, in_dim, out_dim, rank, alpha): super().__init__() std_dev = 1 / torch.sqrt(torch.tensor(rank).float()) self.A = torch.nn.Parameter(torch.randn(in_dim, rank) * std_dev) self.B = torch.nn.Parameter(torch.zeros(rank, out_dim)) self.alpha = alpha def forward(self, x): x = self.alpha * (x @ self.A @ self.B) return x ``` (示例代码在`00_lora-layer.ipynb`) 在上面的代码中, `in_dim` 是我们想要使用 LoRA 修改的层的输入维度, `out_dim` 是该层的相应输出维度。 我们之前讨论过矩阵A和B的`rank`是一个超参数,它控制了LoRA引入的复杂性和额外参数的数量。 然而,从上面的代码可以看出,我们添加了另一个超参数,即缩放因子 `alpha` 。这个因子决定了 LoRA 层对模型现有权重的改变的幅度: `alpha * (x @ A @ B)` 。较大的 `alpha` 值意味着对模型行为的更大调整,而较小的值则导致更细微的变化。 另外需要注意的是,我们使用随机分布来初始化参数矩阵`A` 。在这里,该分布的标准差由`rank`的平方根确定(这个选择确保 `A`中的初始值不会太大)。然后我们使用零来初始化参数矩阵`B`。这样做的理由是,在训练开始之初,在通过反向传播更新``和`B`之前,`LoRALayer`不会对原始权重产生影响,因为`AB=0` if `B=0`. 。 请注意,LoRA 通常应用于神经网络的*线性(前馈)层*。例如,假设我们有一个简单的 PyTorch 模型或模块,其中包含两个线性层(例如,这可以是一个 transformer 块的前馈模块)。假设该模块的 forward 方法如下所示: ```python def forward(self, x): x = self.linear_1(x) x = F.relu(x) x = self.linear_2(x) return x ``` 如果我们使用LoRA,我们将将LoRA的更新添加到这些线性层的输出中,代码如下: ```python def forward(self, x): # 对与FFN中的2个线性层分别处理 x = self.linear_1(x) + self.lora_1(x) x = F.relu(x) x = self.linear_2(x) + self.lora_2(x) return logits ``` *在代码中,当通过修改现有的PyTorch模型来实现LoRA时,一种简单的方法是将每个线性层替换为一个 `LinearWithLoRA` 层,该层将 `Linear` 层与我们之前的 `LoRALayer` 实现结合起来。* ```python class LinearWithLoRA(torch.nn.Module): def __init__(self, linear, rank, alpha): super().__init__() self.linear = linear self.lora = LoRALayer( linear.in_features, linear.out_features, rank, alpha ) def forward(self, x): return self.linear(x) + self.lora(x) ``` 这些概念如下图所示总结。 ![](https://zoe.red/usr/uploads/2024/04/1986509019.jpg)为了应用LoRA,我们将神经网络中的现有`Linear`层替换为`LinearWithLoRA`层,该层结合了原始的`Linear`层和`LoRALayer`。 在实践中,为了使用 LoRA 来微调模型,我们只需要用我们的新的 `LinearWithLoRA` 层替换其预训练的 `Linear` 层。在下面的实践部分,我们将看到如何将这个 `LinearWithLoRA` 层应用于预训练的语言模型。 --- ## Finetuning with LoRA -- A Hands-On Example LoRA是一种可以应用于各种类型的神经网络的方法,不仅限于生成模型如GPT或图像生成模型。对于这个实际示例,我们将训练一个用于文本分类的小型BERT模型,因为分类准确性比生成文本更容易评估。(示例代码位置 `02_finetune-with-lora.ipynb` ) 特别是,我们将使用[transformers]([transformers](https://github.com/huggingface/transformers)) 库中的预训练[ DistilBERT]([DistilBERT](https://arxiv.org/abs/1910.01108)) 模型(BERT 的较小版本): ```python from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained( "distilbert-base-uncased", num_labels=2) ``` 由于我们只想训练新的 LoRA 权重,我们通过将所有可训练参数的属性 `requires_grad=False` 来冻结所有模型参数: ```python for param in model.parameters(): param.requires_grad = False ``` 接下来,让我们使用 `print(model)` 简要检查模型的结构: ```python DistilBertForSequenceClassification( (distilbert): DistilBertModel( (embeddings): Embeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) (transformer): Transformer( (layer): ModuleList( (0-5): 6 x TransformerBlock( (attention): MultiHeadSelfAttention( (dropout): Dropout(p=0.1, inplace=False) (q_lin): Linear(in_features=768, out_features=768, bias=True) (k_lin): Linear(in_features=768, out_features=768, bias=True) (v_lin): Linear(in_features=768, out_features=768, bias=True) (out_lin): Linear(in_features=768, out_features=768, bias=True) ) (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (ffn): FFN( (dropout): Dropout(p=0.1, inplace=False) (lin1): Linear(in_features=768, out_features=3072, bias=True) (lin2): Linear(in_features=3072, out_features=768, bias=True) (activation): GELUActivation() ) ... ) (pre_classifier): Linear(in_features=768, out_features=768, bias=True) (classifier): Linear(in_features=768, out_features=2, bias=True) (dropout): Dropout(p=0.2, inplace=False) ) ``` 根据输出,我们可以看到模型由包含线性层的 6 个 Transformer 层组成(参考Transformer编码器N=6): ```python (0-5): 6 x TransformerBlock( (attention): MultiHeadSelfAttention( ... (q_lin): Linear(in_features=768, out_features=768, bias=True) ... ) ... ``` 此外,该模型有两个 `Linear` 输出层(模型的输出模块): ```python ... (pre_classifier): Linear(in_features=768, out_features=768, bias=True) (classifier): Linear(in_features=768, out_features=2, bias=True) ``` 我们可以通过定义以下赋值函数和循环来选择性地为这些 `Linear` 层启用 LoRA ```python from functools import partial # default hyperparameter choices lora_r = 8 lora_alpha = 16 lora_dropout = 0.05 lora_query = True lora_key = False lora_value = True lora_projection = False lora_mlp = False lora_head = False layers = [] assign_lora = partial(LinearWithLoRA, rank=lora_r, alpha=lora_alpha) # 编码器部分 N=6 for layer in model.distilbert.transformer.layer: if lora_query: layer.attention.q_lin = assign_lora(layer.attention.q_lin) if lora_key: layer.attention.k_lin = assign_lora(layer.attention.k_lin) if lora_value: layer.attention.v_lin = assign_lora(layer.attention.v_lin) if lora_projection: layer.attention.out_lin = assign_lora(layer.attention.out_lin) if lora_mlp: layer.ffn.lin1 = assign_lora(layer.ffn.lin1) layer.ffn.lin2 = assign_lora(layer.ffn.lin2) # 输出部分 if lora_head: model.pre_classifier = assign_lora(model.pre_classifier) model.classifier = assign_lora(model.classifier) ``` 现在我们可以再次检查模型,使用 `print(model)` 来检查其更新后的结构: ```python ... (0-5): 6 x TransformerBlock( (attention): MultiHeadSelfAttention( (dropout): Dropout(p=0.1, inplace=False) (q_lin): LinearWithLoRA( (linear): Linear(in_features=768, out_features=768, bias=True) (lora): LoRALayer() ) (k_lin): Linear(in_features=768, out_features=768, bias=True) (v_lin): LinearWithLoRA( (linear): Linear(in_features=768, out_features=768, bias=True) (lora): LoRALayer() ... ... ``` 正如我们在上面所看到的, `Linear` 层已成功被 `LinearWithLoRA` 层替代。 如果我们使用上述默认的超参数选择来训练模型,在 IMDb 电影评论分类数据集上的表现如下: * Train acc: 92.15% * Val acc: 89.98% * Test acc: 89.44% (实验的完整代码在`02_finetune-with-lora.ipynb` ) --- ## Comparison to Traditional Finetuning 在上一节中,我们使用 LoRA 的默认设置获得了 89.44% 的测试准确率。这与传统微调相比如何? 让我们从训练 DistilBERT 模型开始,但在训练过程中只更新最后两层。我们可以通过首先冻结所有模型权重,然后解冻两个线性输出层来实现这一目标。 ```python # freeze all for param in model.parameters(): param.requires_grad = False # unfreeze output layers for param in model.pre_classifier.parameters(): param.requires_grad = True for param in model.classifier.parameters(): param.requires_grad = True ``` 在仅训练最后两层之后,得到的分类性能如下: * Train acc: 86.68% * Val acc: 87.26% * Test acc: 86.22% (完整代码在 `01_finetune-last-layers.ipynb` ) 正如我们所看到的,LoRA 在测试准确率达到 89.44%时,与仅训练上面的最后两层相比表现出色。此外,上述 LoRA 配置也更轻量,只需要训练 147,456 个参数。相比之下,对于较大的最后两层进行微调需要更新 592,130 个参数。 现在,对于所有层进行微调呢? 如果我们以传统方式微调 DistilBERT 模型的所有层,我们将获得以下结果: * Train acc: 96.41% * Val acc: 92.80% * Test acc: 92.31% (完整代码在`04_finetune-all-layers.ipynb`) 正如我们所看到的,微调所有层,涉及训练 66,955,010 个参数,比仅微调最后两层(592,130 个参数)以及 LoRA 默认设置(147,456 个参数)表现更好。 作为一个要点,到目前为止,LoRA 比传统的仅微调最后两层的方法表现更好,尽管使用的参数数量少了 4 倍。微调所有层需要更新 450 倍的参数,但测试准确率也提高了 2%。 然而,需要考虑的一个方面是,到目前为止我们只使用了 LoRA 的默认设置。也许我们可以通过不同的 LoRA 超参数配置来弥合全面微调和 LoRA 微调之间的差距?我们将在接下来的部分回答这个问题。 --- ## Optimizing the LoRA Configuration 前面的 LoRA 微调结果是基于以下超参数选择: ```python lora_r = 8 lora_alpha = 16 lora_dropout = 0.05 lora_query = True lora_key = False lora_value = True lora_projection = False lora_mlp = False lora_head = False ``` 注意,这仅涉及将 LoRA 应用于注意力层的查询和值权重矩阵。我们还可以选择启用其他层的 LoRA。此外,我们可以通过修改 rank( `lora_r` )来控制每个 LoRA 层中的可训练参数的数量。 要尝试不同的超参数配置,您可以使用我们的紧凑型 `03_finetune-lora.py`脚本 ,它接受超参数选择作为命令行参数: ```python python 03_finetune-lora.py --lora_alpha 32 --lora_r 16 ``` 此外,您还可以切换其他超参数设置,例如: ```python python 03_finetune-lora.py \ --lora_alpha 32 \ --lora_r 16 \ --lora_query True \ --lora_key True \ --lora_value True \ --lora_projection True \ --lora_mlp True \ --lora_head True ``` 提高LoRA性能的一种方法是手动调整这些超参数选择。然而,为了使超参数调整更加方便,您还可以使用 `03_gridsearch.py`脚本 ,它在所有可用的GPU上运行以下超参数网格: ```python alpha_values = [1, 4, 8, 16, 32, 64] rank_values = [1, 2, 4, 8, 16, 32] devices = range(torch.cuda.device_count()) lora_query = ["True"] lora_key = ["False", "True"] lora_value = ["True"] lora_projection = ["False", "True"] lora_mlp = ["False", "True"] lora_head = ["False", "True"] ``` 您可以从Visual Studio代码或命令行终端运行此脚本,或者更好的选择是将其提交为作业。作业在完成后会自动关闭。 ![](https://zoe.red/usr/uploads/2024/04/3292958132.png)提交脚本 根据 `03_finetune-lora-script.py` 脚本的设置,结果将保存到 `results.txt` 文件中。在运行完成后检查 `results.txt` 文件,最佳的超参数配置似乎是以下内容: ```python lora_r: 8 lora_alpha: 1 lora_query: True lora_key: False lora_value: True lora_projection: False lora_mlp: True lora_head: False ``` 结果为: * Val acc: 92.96% * Test acc: 92.39% 请注意,尽管 LoRA 设置只有可训练参数的一小部分(500k 对比 66M),但这些准确率甚至略高于通过完全微调获得的准确率。 ![](https://zoe.red/usr/uploads/2024/04/2899539480.jpg)为了应用LoRA,我们将神经网络中的现有Linear层替换为LinearWithLoRA层,该层结合了原始的Linear层和LoRALayer。 --- ## Conclusion 在这个工作室中,您通过从头开始编码来学习了LoRA。通过对 DistilBERT 模型进行微调进行分类,我们发现 LoRA 相比仅对模型的最后几层进行微调(92.39%的测试准确率对比 86.22%的测试准确率)。 要查看和运行代码,您可以通过点击页面顶部的“运行”按钮来复制此工作室。这将复制所有代码,包括环境,以便您可以开始而无需任何额外的设置步骤。 --- ## Additional Resources * [Parameter-Efficient LLM Finetuning With Low-Rank Adaptation (LoRA)](https://lightning.ai/pages/community/tutorial/lora-llm/) 提供了对 LoRA 的更长概念介绍 * [Finetuning LLMs with LoRA and QLoRA: Insights from Hundreds of Experiments](https://lightning.ai/pages/community/lora-insights/) 总结了数百次 LoRA 实验的经验教训 * [Practical Tips for Finetuning LLMs Using LoRA (Low-Rank Adaptation)](https://magazine.sebastianraschka.com/p/practical-tips-for-finetuning-llms) 回答了一些常见问题,并提供了些实用见解 THE END 本文作者:将夜 本文链接:https://zoe.red/2024/470.html 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。 最后修改:2024 年 04 月 09 日 © 转载自他站 赞 1 如果觉得我的文章对你有用,请随意赞赏