从零开始实施LoRA

从零开始学习并应用LoRA技术

从零开始实现LoRA及一些实用技巧

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

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

LoRA是Low-Rank Adaptation(低秩适配)或者Low-Rank Adaptors(低秩适配器)的缩写,为微调预先存在的语言模型提供了一种高效且轻量级的方法。这包括像BERT和RoBERTa这样的掩码语言模型,以及GPT、Llama和Mistral等因果(或聊天机器人)模型。

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

此外,LoRA可以提高泛化性能。通过限制模型复杂性,它们有助于预防过拟合,尤其是在训练数据有限的情况下。这样可以得到更具韧性的模型,在处理新的、未见过的数据时表现出色,或者至少保留其初始训练任务的知识。

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

我们将首先深入研究LoRA的工作原理,然后我将演示您如何为RoBERTa模型从零开始开发它,随后使用GLUE和SQuAD基准对我们的实现进行评估,并讨论一些常规技巧和改进。

LoRA的工作原理

LoRA的基本思想是保持预训练矩阵(即原始模型的参数)固定状态,并仅向原始矩阵添加一个较小的增量,该增量的参数数量少于原始矩阵。

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

显然,如果W-orig的维度是n×m,并且我们只是初始化一个具有相同尺寸的新增量矩阵进行微调,那么我们没有获得任何好处;相反,我们会使参数数量翻倍。

关键在于通过来自较低维度矩阵B和A的矩阵乘法构造一个比原始矩阵更“低维度”的ΔW。

我们首先定义一个显著小于基础矩阵维度的秩r,即r≪n且r≪m。然后,矩阵B为n×r,矩阵A为r×m。将它们相乘得到一个具有与W相同尺寸的矩阵,但使用了更少的参数。

显然,我们希望在训练开始时,增量是零,以使微调的起始与原始模型相同。因此,B通常被初始化为全零,而A则被初始化为随机(通常是正态分布)的值。

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

一个关于LoRA可能在实际矩阵上如何看起来的示例图

想象一个我们基本维度为1024且我们选择了LoRA的秩r为4的情况:

  • W 具有 1024 * 1024 ≈ 100 万个参数
  • A & B 每个都具有 r * 1024 = 4 * 1024 ≈ 4k 个参数,总共 8k 个参数
  • 因此,我们只需要训练 0.8% 的参数来使用 LoRA 更新我们的矩阵

稍有离题,在 LoRA 论文中,他们使用 alpha 参数对 delta 矩阵进行加权:

如果你将 α 设置为你实验中的第一个 r,并微调学习率,你通常可以在不再微调学习率的情况下稍后更改 r 参数(至少近似如此)。虽然我们在实现中可以忽略此细节,但这是许多其他 LoRA 库中的常见功能,例如 Hugging Face 的 PEFT。

实现 LoRA

对于我们的实现,我们希望与原始的 LoRA 论文保持紧密联系。他们测试了在实际需要替换的 transformer 矩阵中。他们发现,在 GPT-3 微调任务中比较不同策略时,只有调整自注意机制的查询和值向量就足够了。

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

我们这里的实现将使用 PyTorch,但应该很容易适应不同的框架。

为了这篇博文,我简化了代码,使其更易于阅读,同时仍然显示了关键要素。完整的代码和一些已经训练好的 LoRA 权重可以在这里找到:https://github.com/Montinger/Transformer-Workbench

重新实现自注意模型

我们希望适应的模型是 Huggingface 的 RoBERTa 模型。最简单的方法是重新包装原始的自注意机制 RobertaSelfAttention。新的类 LoraRobertaSelfAttention 将初始化 LoRA 矩阵。所有 B 矩阵都将初始化为零,所有 A 矩阵将从正态分布中随机选取。

class LoraRobertaSelfAttention(RobertaSelfAttention):    """    使用 LoRA(低秩调整)矩阵扩展了 RobertaSelfAttention。    LoRA 通过仅更新查询和值矩阵来增强效率。    此类添加了 LoRA 矩阵,并在前向方法中应用 LoRA 逻辑。    参数:    - r(整数):LoRA 矩阵的秩。    - 配置:Roberta 模型的配置。    """    def __init__(self, r=8, *args, **kwargs):        super().__init__(*args, **kwargs)        d = self.all_head_size        # 为查询和值初始化 LoRA 矩阵        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):        """        将 LoRA 应用于查询组件。通过将 LoRA 适应添加到标准查询输出中计算出修改后的查询输出。在训练之前需要冻结常规线性层。        """        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):        """        将 LoRA 应用于值组件。通过将 LoRA 适应添加到标准值输出中计算出修改后的值输出。在训练之前需要冻结常规线性层。        """        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的原始前向函数。虽然这有点硬编码(参见后面的改进讨论),但非常简单。首先,我们从https://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):        """从https://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py        复制代码,但将query和value的调用替换为lora_query和lora_value函数的调用。        在这里,我们只是简要介绍了如何调整这些调用。         更改实际版本中对self.value和self.query的每个调用。        """        # 原始的query代码:        ## mixed_query_layer = self.query(hidden_states)        # 更新后的LoRA查询:        mixed_query_layer = self.lora_query(hidden_states)        # 键没有LoRA,因此将这些调用保持不变        key_layer = self.transpose_for_scores(self.key(hidden_states))        # 原始的value代码:        ## value_layer = self.transpose_for_scores(self.value(hidden_states))        # 更新后的LoRA值:        value_layer = self.transpose_for_scores(self.lora_value(hidden_states))                # ...(剩下的前向代码,不变)

塔达,这就是我们的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):        """        一个用于各种NLP任务的带有低秩适应(LoRA)的RoBERTa包装器。        - task_type:NLP任务的类型('glue'、'squad_v1'、'squad_v2')。        - num_classes:用于分类的类数(随任务而变)。        - dropout_rate:模型中的dropout率。        - model_id:预训练的RoBERTa模型ID。        - lora_rank:LoRA适应的秩。        - train_biases, train_embedding, train_layer_norms:是否在初始化LoRA后保持某些参数可训练的标志。                例子:            model = LoraWrapperRoberta(task_type='glue')        """        super().__init__()        # 1. 使用参数初始化基本模型        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. 为基准任务添加图层        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. 为训练设置LoRA模型        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):
        """        
        用 LoraRobertaSelfAttention 替换 model 中的 RobertaSelfAttention。
        该方法递归地应用替换到所有子组件。
        
        Parameters
        ----------
        model : nn.Module
            要修改的 PyTorch 模块或模型。
        """
        
        for name, module in model.named_children():
            if isinstance(module, RobertaSelfAttention):
                # 用 LoraRobertaSelfAttention 替换 RobertaSelfAttention
                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:
                # 子模块递归调用
                self.replace_multihead_attention_recursion(module)

我们需要递归循环遍历所有模型部分,因为在 PyTorch 中,网络的部分可以(实际上对于 RoBERTa 来说是)打包到一个单独的 PyTorch 模块中。

现在,我们需要冻结不再训练的所有参数:

class LoraWrapperRoberta(nn.Module):
    # ...
    def freeze_parameters_except_lora_and_bias(self):
        """        
        冻结所有模型参数,除了根据配置设置可训练的特定层和类型的参数。
        LoRA 层、finetune 头部、偏置参数、嵌入、以及层归一化参数可以根据类的设定设置为可训练。
        """
        
        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 的重点是评估问答模型。它涉及从 Wikipedia 段落中提取答案,其中模型识别相关的文本片段。SQuAD v2 是一个更高级的版本,引入了无法回答的问题,增加了复杂性,并模拟了模型必须在文本缺少答案时识别的真实情况。

请注意,在下面的基准测试中,我没有调整任何超参数,也没有进行多次运行(特别是较小的 GLUE 数据集容易受到随机噪声的影响),也没有进行任何早停、也没有从先前的 GLUE 任务的微调开始(这通常用于减少小数据集噪声的变异性和防止过拟合)。

所有运行:

  • 从新初始化的 rank 8 的 LoRA 插入 RoBERTa-base 模型开始。
  • 每个任务的训练持续 6 个 epoch,没有进行任何早停。
  • 在前两个 epoch 中,学习率线性缩放到最大值,然后在剩余的 4 个 epoch 中线性衰减为零。
  • 所有任务的最大学习率为 5e-4。
  • 所有任务的批量大小为 16。

RoBERTa-base 模型有 1.246 亿个参数。在 LoRA 参数、偏差和层归一化的作用下,我们只有 42 万个可训练的未冻结参数。这意味着我们实际上只对原始参数的 0.34% 进行训练。

对于这些特定任务,LoRA 引入的参数数量非常少,实际占用了仅 1.7 MB 的磁盘空间。你可以在 Git 分支的输出文件夹中找到训练好的 LoRA 模型。

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

使用 LoRA 在 GLUE 基准测试上的性能
使用 LoRA 在 SQuAD 数据集上的性能

通过对一些超参数进行微调,这些结果很可能会有很大的改进。然而,这清楚地证明了我们的 LoRA 实现是有效的,我们注入的低秩矩阵是在学习的。

可能的改进

回顾我们的实现,人们可能会想:“是否存在一种更高效、更具通用性(即适用于其他网络架构)的方法,而不是重新编写自注意力类并进行复杂的替换?”

实际上,我们可以简单地在 pytorch 的 nn.Linear 函数周围包装一个包装器,并更具体地指定我们想要使用它替换哪些层,通过检查它们的名称。同样,您可以在大多数基本的 pytorch 层周围编写包装器,并能够快速地将 LoRA 适应到新的网络架构中。以下是一个快速的示例:

class LoraLinear(nn.Linear):    """    使用低秩适应(LoRA)扩展了 PyTorch 线性层。    LoRA 添加了两个矩阵到该层,可以有效地训练大模型。    """    def __init__(self, in_features, out_features, r=8, *args, **kwargs):        super().__init__(in_features, out_features, *args, **kwargs)        # 初始化 LoRA 矩阵        self.lora_matrix_B = nn.Parameter(torch.zeros(out_features, r))        self.lora_matrix_A = nn.Parameter(torch.randn(r, in_features))                # 冻结原始权重矩阵        self.weight.requires_grad = False    def forward(self, x: Tensor) -> Tensor:        # 计算 LoRA 权重调整        lora_weights = torch.matmul(self.lora_matrix_B, self.lora_matrix_A)        # 应用原始和 LoRA 调整的线性变换        return super().forward(x) + F.linear(x, lora_weights)

这实际上(接近)是 huggingface PEFT(Parameter-Efficient Fine-Tuning)库实现 LoRA 的方式。对于任何实际应用,你不打算学习的情况下,我强烈建议使用它,而不是自己编写。

通常,将 LoRA 注入到所有线性层中,包括自注意力的所有矩阵和全连接前向网络的两个线性层,已经成为一种相当普遍的做法。通常,保持偏差和层归一化可训练是一个好主意,除了 LoRA 参数之外。由于它们已经很小,你不需要为它们进行低秩注入。

将原始矩阵权重量化以节省 GPU VRAM 也是可取的,在给定的 GPU 上便于训练更大的模型。使用与 Hugging Face 完全集成的 bits-and-bytes 库可以有效地实现这一点(参见参考资料)。

总结起来,在严肃的环境中,以下是低秩适应的五个戒律:

低秩适应的五个戒律

如果你发现刻在石碑上的文字很难阅读,这里再次以纯文本形式呈现出来:

初级适应的五个戒律

1. 使用LoRA进行高效模型微调,注重减少参数大小。

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

3. 将LoRA适用于所有线性层,增强整体模型能力。

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

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

请记住,使用QLoRA训练可能比LoRA稍慢一些,因为它涉及到在每次乘法运算中对矩阵进行反量化。例如,当对像Llama-7B这样庞大的模型进行微调时,QLoRA需要大约比标准的LoRA少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 bnbfrom transformers import AutoModel, AutoModelForSequenceClassification, BitsAndBytesConfig# 加载定量化模型的配置文件bnb_config = BitsAndBytesConfig(    load_in_4bit=True,  # 启用4位加载    bnb_4bit_quant_type="nf4",    bnb_4bit_compute_dtype=torch.bfloat16,    llm_int8_skip_modules=['classifier', 'qa_outputs'],  # 跳过这些模块用于定量化)# 使用定量化的Huggingface加载模型model = AutoModelForSequenceClassification.from_pretrained('roberta-base',          torch_dtype="auto", quantization_config=bnb_config)

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

# 验证4位元素加载print("在注意力层中验证4位元素(Linear4bit):")print(model.roberta.encoder.layer[4].attention)print("检查uint8数据类型:")print(model.roberta.encoder.layer[4].attention.self.query.weight.dtype)

现在就来介绍一下如何使用PEFT参数注入LoRA。请注意,PEFT库更加灵活,也可以处理自定义模型或其他复杂结构,只要您使用LoRA而不是QLoRA(通常定量化是比较棘手的部分)。

PEFT库通过模型的名称来定位要替换的模块,因此我们需要查看模型的model.named_parameters()。以下是未定量化的roberta-base模型的名称:

模块                                                              参数----------------------------------------------------------  ------------roberta.embeddings.word_embeddings.weight                     38_603_520roberta.embeddings.position_embeddings.weight                    394_752roberta.embeddings.token_type_embeddings.weight                      768roberta.embeddings.LayerNorm.weight                                  768roberta.embeddings.LayerNorm.bias                                    768roberta.encoder.layer.0.attention.self.query.weight              589_824roberta.encoder.layer.0.attention.self.query.bias                    768roberta.encoder.layer.0.attention.self.key.weight                589_824roberta.encoder.layer.0.attention.self.key.bias                      768roberta.encoder.layer.0.attention.self.value.weight              589_824roberta.encoder.layer.0.attention.self.value.bias                    768roberta.encoder.layer.0.attention.output.dense.weight            589_824roberta.encoder.layer.0.attention.output.dense.bias                  768roberta.encoder.layer.0.attention.output.LayerNorm.weight            768roberta.encoder.layer.0.attention.output.LayerNorm.bias              768roberta.encoder.layer.0.intermediate.dense.weight              2_359_296roberta.encoder.layer.0.intermediate.dense.bias                    3_072roberta.encoder.layer.0.output.dense.weight                    2_359_296roberta.encoder.layer.0.output.dense.bias                            768roberta.encoder.layer.0.output.LayerNorm.weight                      768roberta.encoder.layer.0.output.LayerNorm.bias                        768roberta.encoder.layer.1.attention.self.query.weight              589_824...roberta.encoder.layer.11.output.LayerNorm.bias                       768classifier.dense.weight                                          589_824classifier.dense.bias                                                768classifier.out_proj.weight                                         1_536classifier.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。我们将alpha参数设为8,因为这是我们首先尝试的秩,应该允许我们保持与我们从头开始的示例相同的学习率。

import peft#通过PEFTpeft_config导入LoRA注入配置= peft.LoraConfig(    r = 2,#注入矩阵的秩维度    lora_alpha = 8,#缩放参数,这里使用8使其与我们自己的实现可比较的    target_modules = ['query', 'key', 'value', 'intermediate.dense', 'output.dense'],#关于密集注意precise,因为分类器也是密集的    modules_to_save = [“LayerNorm”,“classifier”,“qa_outputs”], #重新训练LayerNorm;分类器是微调头;qa_outputs用于SQuAD    lora_dropout = 0.1,#层的丢失概率    bias =“all”,#none,all或lora_only)模型= peft.get_peft_model(model,peft_config)

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

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

import torchimport bitsandbytes as bnb#替换为optimizer = torch.optim.AdamW(此处的参数)#用下面的optimizer = bnb.optim.AdamW8bit(相同的参数)

然后,可以像以前一样训练此模型,而无需在训练期间明确担心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.ipynb和Load-LoRA-Weights-PEFT.ipynb的笔记本,提供了PEFT训练和加载模型的逐步示例。

结论

“我们不会停止探索,我们探索的终点将是我们最初所熟知的地方。”

— 出自T.S. Eliot的《小耶尔丁》

这次旅程带领我们从一个简单但硬编码的LoRA实现,深入了解了低秩适配器、它们的实际实现和基准测试。

我们探索了一种替代性、更高效的实现策略,并深入研究了现有库(如PEFT)在LoRA集成方面的优雅之处。

我们的冒险以在实际应用中高效、有效地使用LoRA所需的实用指南总结,其中包括“五大戒律”,和一步步实现它们的实践指南。

参考资料

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