构建大型语言模型(从零开始)--附录E 使用LoRA进行参数高效微调
低秩自适应 (LoRA) 是参数高效微调中使用最广泛的技术之一。以下讨论基于第六章中给出的垃圾邮件分类微调示例。但是, LoRA 微调也适用于第七章中讨论的监督指令微调。
E.1 LoRA简介
LoRA 是一种技术,它通过仅调整模型权重参数的一小部分来调整预训练模型以更好地适应特定的、通常较小的数据集。“低秩”方面是指将模型调整限制在总权重参数空间的较小维度子空间的数学概念。这有效地捕获了训练期间权重参数变化的最有影响力的方向。LoRA 方法非常有用且很受欢迎,因为它能够根据特定任务的数据对大型模型进行高效微调,从而显著降低微调通常需要的计算成本和资源。
假设一个较大的权重矩阵 W 与特定层相关联。LoRA 可应用于 LLM 中的所有线性层。但是,出于说明目的,我们专注于单个层。
在训练深度神经网络时,在反向传播期间,我们学习一个 ΔW 矩阵,其中包含有关我们希望在训练期间更新多少原始权重参数以最小化损失函数的信息。在下文中,我将使用术语“权重”作为模型权重参数的简写。
在常规训练和微调过程中,权重更新定义如下:

Hu等人(https://arxiv.org/abs/2106.09685)提出的LoRA方法提供了一种更有效的替代方法来计算权重更新ΔW,通过学习其近似值:

其中A和B是比W小得多的两个矩阵,AB表示A和B之间的矩阵乘积。
使用LoRA,我们可以重新定义前面定义的权重更新:

图E.1展示了完整的微调和LoRA的权重更新公式。

图E.1 比较权重更新方法:常规微调和LoRA。 常规微调涉及直接使用ΔW更新预训练的权重矩阵W(左)。 LoRA使用两个较小的矩阵A和B来近似ΔW,其中AB的乘积加到W上,r表示内维数,这是一个可调的超参数(右)。
如果您仔细观察,您可能已经注意到图 E.1 中完全微调和 LoRA 的视觉表示与前面介绍的公式略有不同。这种变化归因于矩阵乘法的分配定律,它允许我们将原始权重和更新的权重分开,而不是将它们合并。例如,在以 x 作为输入数据进行常规微调的情况下,我们可以将计算表示为

同样,我们可以为 LoRA 编写以下内容:

除了减少训练期间要更新的权重数量外,将 LoRA 权重矩阵与原始模型权重分开的能力使 LoRA 在实践中更加有用。实际上,这允许预训练模型权重保持不变,在使用模型时,LoRA 矩阵在训练后动态应用。
在实践中,将 LoRA 权重分开非常有用,因为它支持模型定制,而无需存储 LLM 的多个完整版本。这降低了存储要求并提高了可扩展性,因为当我们为每个特定客户或应用程序定制 LLM 时,只需要调整和保存较小的 LoRA 矩阵。
接下来,让我们看看如何使用LoRA来微调一个用于垃圾邮件分类的LLM,类似于第六章中的微调示例。
E.2 准备数据集
在将LoRA应用于垃圾邮件分类示例之前,我们必须加载我们将要使用的数据集和预训练模型。 这里的代码重复了第六章的数据准备过程。(我们也可以打开并运行第六章的笔记本,在那里插入E.4部分的LoRA代码。)
首先,我们下载数据集并将其保存为CSV文件。
清单E.1 下载和准备数据集
from pathlib import Path` `import pandas as pd` `from ch06 import (` `download_and_unzip_spam_data,` `create_balanced_dataset,` `random_split` `)` `url = \` `"https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip"` `zip_path = "sms_spam_collection.zip"` `extracted_path = "sms_spam_collection"` `data_file_path = Path(extracted_path) / "SMSSpamCollection.tsv"` `download_and_unzip_spam_data(url, zip_path, extracted_path, data_file_path)` `df = pd.read_csv(` `data_file_path, sep="\t", header=None, names=["Label", "Text"]` `)` `balanced_df = create_balanced_dataset(df)` `balanced_df["Label"] = balanced_df["Label"].map({"ham": 0, "spam": 1})` `train_df, validation_df, test_df = random_split(balanced_df, 0.7, 0.1)` `train_df.to_csv("train.csv", index=None)``validation_df.to_csv("validation.csv", index=None)` `test_df.to_csv("test.csv", index=None)
接下来,我们创建SpamDataset实例。
清单E.2 实现PyTorch数据集
import torch` `from torch.utils.data import Dataset` `import tiktoken` `from chapter06 import SpamDataset` `tokenizer = tiktoken.get_encoding("gpt2")` `train_dataset = SpamDataset("train.csv", max_length=None,` `tokenizer=tokenizer` `)` `val_dataset = SpamDataset("validation.csv",` `max_length=train_dataset.max_length, tokenizer=tokenizer` `)` `test_dataset = SpamDataset(` `"test.csv", max_length=train_dataset.max_length, tokenizer=tokenizer` `)
创建PyTorch数据集对象后,我们实例化数据加载器。
清单E.3 创建PyTorch数据加载器
from torch.utils.data import DataLoader` `num_workers = 0` `batch_size = 8` `torch.manual_seed(123)` `train_loader = DataLoader(` `dataset=train_dataset,` `batch_size=batch_size,` `shuffle=True,` `num_workers=num_workers,` `drop_last=True,` `)` `val_loader = DataLoader(` `dataset=val_dataset,` `batch_size=batch_size,` `num_workers=num_workers,` `drop_last=False,` `)` `test_loader = DataLoader(` `dataset=test_dataset,` `batch_size=batch_size,` `num_workers=num_workers,` `drop_last=False,` `)
作为验证步骤,我们遍历数据加载器并检查批次是否包含8个训练示例,其中每个训练示例由120个标记组成:
print("Train loader:")` `for input_batch, target_batch in train_loader:` `pass` `print("Input batch dimensions:", input_batch.shape)` `print("Label batch dimensions", target_batch.shape)
输出为
Train loader:
Input batch dimensions: torch.Size([8, 120])
Label batch dimensions torch.Size([8])
最后,我们打印每个数据集中的批次总数:
print(f"{len(train_loader)} training batches")` `print(f"{len(val_loader)} validation batches")` `print(f"{len(test_loader)} test batches")
在这种情况下,每个数据集的批次数量如下:
130 training batches
19 validation batches
38 test batches
E.3 初始化模型
我们重复第六章的代码来加载和准备预训练的GPT模型。我们首先下载模型权重并将其加载到GPTModel类中。
清单E.4 加载预训练的GPT模型
from gpt_download import download_and_load_gpt2` `from chapter04 import GPTModel` `from chapter05 import load_weights_into_gpt` `CHOOSE_MODEL = "gpt2-small (124M)"` `INPUT_PROMPT = "Every effort moves"` `BASE_CONFIG = {` `"vocab_size": 50257,` `"context_length": 1024,` `"drop_rate": 0.0,` `"qkv_bias": True` `}``model_configs = {` `"gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},` `"gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},` `"gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},` `"gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},` `}` `BASE_CONFIG.update(model_configs[CHOOSE_MODEL])` `model_size = CHOOSE_MODEL.split(" ")[-1].lstrip("(").rstrip(")")` `settings, params = download_and_load_gpt2(` `model_size=model_size, models_dir="gpt2"` `)` `model = GPTModel(BASE_CONFIG)` `load_weights_into_gpt(model, params)` `model.eval()
为了确保模型加载是正确的,让我们检查一下它是否生成了连贯的文本:
from chapter04 import generate_text_simple` `from chapter05 import text_to_token_ids, token_ids_to_text` `text_1 = "Every effort moves you"` `token_ids = generate_text_simple(` `model=model,` `idx=text_to_token_ids(text_1, tokenizer),` `max_new_tokens=15,` `context_size=BASE_CONFIG["context_length"]` `)` `print(token_ids_to_text(token_ids, tokenizer))
以下输出显示模型生成连贯的文本,这表明模型权重已正确加载:
Every effort moves you forward.
The first step is to understand the importance of your work
接下来,我们准备模型进行分类微调,类似于第六章,在那里我们替换输出层:
torch.manual_seed(123)` `num_classes = 2` `model.out_head = torch.nn.Linear(in_features=768, out_features=num_classes)` `device = torch.device("cuda" if torch.cuda.is_available() else "cpu")` `model.to(device)
最后,我们计算未微调模型的初始分类准确率(我们预计其准确率为50%左右,这意味着该模型还不能可靠地区分垃圾邮件和非垃圾邮件):
from chapter06 import calc_accuracy_loader` `torch.manual_seed(123)` `train_accuracy = calc_accuracy_loader(` `train_loader, model, device, num_batches=10` `)` `val_accuracy = calc_accuracy_loader(` `val_loader, model, device, num_batches=10` `)` `test_accuracy = calc_accuracy_loader(` `test_loader, model, device, num_batches=10` `)` `print(f"Training accuracy: {train_accuracy*100:.2f}%")` `print(f"Validation accuracy: {val_accuracy*100:.2f}%")` `print(f"Test accuracy: {test_accuracy*100:.2f}%")
初始预测准确度为
Training accuracy: 46.25%
Validation accuracy: 45.00%
Test accuracy: 48.75%
E.4 使用LoRA进行参数高效微调
接下来,我们使用LoRA修改并微调LLM。 我们首先初始化一个LoRA层,该层生成矩阵A和B,以及alpha缩放因子和秩®设置。 此层可以接受输入并计算相应的输出,如图E.2所示。

图E.2 LoRA矩阵A和B应用于层输入,并参与计算模型输出。 这些矩阵的内部维度r作为设置,通过改变A和B的大小来调整可训练参数的数量。
在代码中,LoRA层可以实现如下。
清单E.5 实现LoRA层
import math` `class LoRALayer(torch.nn.Module):` `def __init__(self, in_dim, out_dim, rank, alpha):` `super().__init__()` `self.A = torch.nn.Parameter(torch.empty(in_dim, rank))` `torch.nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))` `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
秩决定了矩阵A和B的内部维度。 本质上,这种设置决定了LoRA引入的额外参数的数量,从而在模型的适应性和通过使用的参数数量实现的效率之间建立了平衡。
另一个重要的设置 alpha 用作 low-rank adaptation 输出的比例因子。它主要决定适应图层的输出对原始图层输出的影响程度。这可以看作是调节低秩自适应对层输出的影响的一种方式。到目前为止,我们实现的 LoRALayer 类使我们能够转换层的输入。
在 LoRA 中,典型目标是替换现有的线性层,允许将权重更新直接应用于预先存在的预训练权重,如图 E.3 所示。

图E.3 LoRA集成到模型层中。 层的原始预训练权重(W)与LoRA矩阵(A和B)的输出结合,这些矩阵近似于权重更新矩阵(ΔW)。 最终输出通过将适应层的输出(使用LoRA权重)与原始输出相加计算得出。
为了集成原始 Linear 层权重,我们现在创建一个 LinearWithLoRA 层。该层利用了以前实现的 LoRALayer,旨在替换神经网络中现有的线性层,例如 GPTModel 中的自注意力模块或前馈模块。
清单E.6 用线性层替换Lora层
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)
此代码将标准线性层与LoRALayer结合在一起。 前向方法通过将原始线性层和LoRA层的结果相加来计算输出。
由于权重矩阵B(LoRALayer中的self.B)初始化为零值,因此A和B的乘积结果为零矩阵。 这确保了乘法不会改变原始权重,因为添加零不会改变它们。
为了将LoRA应用于之前定义的GPTModel,我们引入了一个replace_linear_with_lora函数。 此函数将用新创建的LinearWithLoRA层替换模型中所有现有的线性层:
def replace_linear_with_lora(model, rank, alpha):` `for name, module in model.named_children():` `if isinstance(module, torch.nn.Linear):` `setattr(model, name, LinearWithLoRA(module, rank, alpha))` `else:` `replace_linear_with_lora(module, rank, alpha)
我们现在已经实现了所有必要的代码,将 GPTModel 中的 Linear 层替换为新开发的 LinearWithLoRA 层,以实现参数高效的微调。接下来,我们将 LinearWithLoRA 升级应用于多头注意力、前馈模块和 GPTModel 输出层中的所有线性层,如图 E.4 所示。

图E.4 GPT模型的架构。 它突出了模型中线性层升级为线性带LoRA层的部分,用于参数高效的微调。
在应用LinearWithLoRA层升级之前,我们首先冻结原始模型参数:
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)` `print(f"Total trainable parameters before: {total_params:,}")` `for param in model.parameters():` `param.requires_grad = False``total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)` `print(f"Total trainable parameters after: {total_params:,}")
现在,我们可以看到,1.24亿个模型参数中没有一个是可训练的:
Total trainable parameters before: 124,441,346
Total trainable parameters after: 0
接下来,我们使用replace_linear_with_lora替换线性层:
replace_linear_with_lora(model, rank=16, alpha=16)` `total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)` `print(f"Total trainable LoRA parameters: {total_params:,}")
添加LoRA层后,可训练参数数量如下:
Total trainable LoRA parameters: 2,666,528
正如我们所见,使用LoRA时,可训练参数的数量减少了近50×。 秩和alpha值为16是很好的默认选择,但通常也会增加秩参数,这反过来又会增加可训练参数的数量。 alpha值通常被选为秩的一半、两倍或相等。
通过打印模型架构来验证各层是否按预期进行了修改:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")` `model.to(device)` `print(model)
输出为
GPTModel(
(tok_emb): Embedding(50257, 768)
(pos_emb): Embedding(1024, 768)
(drop_emb): Dropout(p=0.0, inplace=False)
(trf_blocks): Sequential(
…
(11): TransformerBlock(
(att): MultiHeadAttention(
(W_query): LinearWithLoRA(
(linear): Linear(in_features=768, out_features=768, bias=True)
(lora): LoRALayer()
)
(W_key): LinearWithLoRA(
(linear): Linear(in_features=768, out_features=768, bias=True)
(lora): LoRALayer()
)
(W_value): LinearWithLoRA(
(linear): Linear(in_features=768, out_features=768, bias=True)
(lora): LoRALayer()
)
(out_proj): LinearWithLoRA(
(linear): Linear(in_features=768, out_features=768, bias=True)
(lora): LoRALayer()
)
(dropout): Dropout(p=0.0, inplace=False)
)
(ff): FeedForward(
(layers): Sequential(
(0): LinearWithLoRA(
(linear): Linear(in_features=768, out_features=3072, bias=True)
(lora): LoRALayer()
)
(1): GELU()
(2): LinearWithLoRA(
(linear): Linear(in_features=3072, out_features=768, bias=True)
(lora): LoRALayer()
)
)
)
(norm1): LayerNorm()
(norm2): LayerNorm()
(drop_resid): Dropout(p=0.0, inplace=False)
)
)
(final_norm): LayerNorm()
(out_head): LinearWithLoRA(
(linear): Linear(in_features=768, out_features=2, bias=True)
(lora): LoRALayer()
)
)
该模型现在包括新的LinearWithLoRA层,它们本身由原始的线性层组成,设置为不可训练,以及我们将进行微调的新LoRA层。
在我们开始对模型进行微调之前,让我们计算初始分类准确率:
torch.manual_seed(123)` `train_accuracy = calc_accuracy_loader(` `train_loader, model, device, num_batches=10` `)` `val_accuracy = calc_accuracy_loader(` `val_loader, model, device, num_batches=10` `)` `test_accuracy = calc_accuracy_loader(` `test_loader, model, device, num_batches=10` `)` `print(f"Training accuracy: {train_accuracy*100:.2f}%")` `print(f"Validation accuracy: {val_accuracy*100:.2f}%")` `print(f"Test accuracy: {test_accuracy*100:.2f}%")
所得的准确度值为
Training accuracy: 46.25%
Validation accuracy: 45.00%
Test accuracy: 48.75%
这些精度值与第六章中的值相同。这一结果是因为我们将LoRA矩阵B初始化为零。 因此,矩阵AB的乘积是一个零矩阵。 这确保了乘法不会改变原始权重,因为加上零不会改变它们。
现在让我们进入激动人心的部分——使用第六章的训练函数对模型进行微调。在M3 MacBook Air笔记本电脑上,训练大约需要15分钟,在V100或A100 GPU上,训练不到半分钟。
清单E.7 使用LoRA层对模型进行微调
import time` `from chapter06 import train_classifier_simple` `start_time = time.time()` `torch.manual_seed(123)` `optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.1)` `num_epochs = 5` `train_losses, val_losses, train_accs, val_accs, examples_seen = \` `train_classifier_simple(` `model, train_loader, val_loader, optimizer, device,` `num_epochs=num_epochs, eval_freq=50, eval_iter=5,` `tokenizer=tokenizer` `)` `end_time = time.time()` `execution_time_minutes = (end_time - start_time) / 60` `print(f"Training completed in {execution_time_minutes:.2f} minutes.")
我们在训练过程中看到的输出是
Ep 1 (Step 000000): Train loss 3.820, Val loss 3.462
Ep 1 (Step 000050): Train loss 0.396, Val loss 0.364
Ep 1 (Step 000100): Train loss 0.111, Val loss 0.229
Training accuracy: 97.50% | Validation accuracy: 95.00%
Ep 2 (Step 000150): Train loss 0.135, Val loss 0.073
Ep 2 (Step 000200): Train loss 0.008, Val loss 0.052
Ep 2 (Step 000250): Train loss 0.021, Val loss 0.179
Training accuracy: 97.50% | Validation accuracy: 97.50%
Ep 3 (Step 000300): Train loss 0.096, Val loss 0.080
Ep 3 (Step 000350): Train loss 0.010, Val loss 0.116
Training accuracy: 97.50% | Validation accuracy: 95.00%
Ep 4 (Step 000400): Train loss 0.003, Val loss 0.151
Ep 4 (Step 000450): Train loss 0.008, Val loss 0.077
Ep 4 (Step 000500): Train loss 0.001, Val loss 0.147
Training accuracy: 100.00% | Validation accuracy: 97.50%
Ep 5 (Step 000550): Train loss 0.007, Val loss 0.094
Ep 5 (Step 000600): Train loss 0.000, Val loss 0.056
Training accuracy: 100.00% | Validation accuracy: 97.50%
Training completed in 12.10 minutes.
使用LoRA训练模型比不使用LoRA训练时间更长(见第六章),因为LoRA层在前向传播过程中引入了额外的计算。 然而,对于较大的模型,反向传播成本更高时,通常使用LoRA训练速度更快。
我们可以看到,模型接受了完美的训练和非常高的验证准确率。让我们也可视化损失曲线,以便更好地了解训练是否收敛:
from chapter06 import plot_values` `epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))` `examples_seen_tensor = torch.linspace(0, examples_seen, len(train_losses))` `plot_values(` `epochs_tensor, examples_seen_tensor,` `train_losses, val_losses, label="loss"` `)
图E.5绘制了结果。

图E.5 机器学习模型的训练和验证损失曲线在六个周期内。 最初,训练和验证损失急剧下降,然后趋于平稳,表明模型正在收敛,这意味着它不会随着进一步的训练而明显改善。
除了根据损失曲线评估模型外,我们还计算完整训练、验证和测试集的准确率(在训练期间,我们通过 eval_iter=5 设置近似了五个批次的训练和验证集准确率):
train_accuracy = calc_accuracy_loader(train_loader, model, device)` `val_accuracy = calc_accuracy_loader(val_loader, model, device)` `test_accuracy = calc_accuracy_loader(test_loader, model, device)` `print(f"Training accuracy: {train_accuracy*100:.2f}%")` `print(f"Validation accuracy: {val_accuracy*100:.2f}%")` `print(f"Test accuracy: {test_accuracy*100:.2f}%")
所得的准确度值为
Training accuracy: 100.00%
Validation accuracy: 96.64%
Test accuracy: 98.00%
这些结果表明,模型在训练集、验证集和测试集上的表现均良好。 训练准确率达到100%,模型完美地学习了训练数据。 然而,验证集和测试集的准确率略低(分别为96.64%和97.33%),这表明模型存在一定程度的过拟合,因为与训练集相比,模型在未见过的数据上的泛化能力稍差。 总体而言,考虑到我们仅微调了相对较少的模型权重(270万个LoRA权重,而不是原来的1.24亿个模型权重),这一结果非常令人印象深刻。
零基础入门AI大模型
今天贴心为大家准备好了一系列AI大模型资源,包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
有需要的小伙伴,可以点击下方链接免费领取【保证100%免费】
1.学习路线图

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;
第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;
第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;
第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;
第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;
第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;
第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。
2.视频教程
网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己整理的大模型视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。


(都打包成一块的了,不能一一展开,总共300多集)
3.技术文档和电子书
这里主要整理了大模型相关PDF书籍、行业报告、文档,有几百本,都是目前行业最新的。

4.LLM面试题和面经合集
这里主要整理了行业目前最新的大模型面试题和各种大厂offer面经合集。

👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;
• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;
• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;
• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。
1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集
5.免费获取
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码或者点击以下链接都可以免费领取【保证100%免费】

本文转自 https://mp.weixin.qq.com/s/p6bdNXaT87h2RVjcW7bfxQ,如有侵权,请联系删除。
更多推荐

所有评论(0)