LoRA(低秩自适应)是最常用的参数高效微调技术之一。它可以应用于分类微调中,也可以应用于监督指令微调中。LoRA是一种通过仅调整模型权重参数的一小部分,使预训练模型更好地适应特定且通常较小的数据集的技术。

低秩指的是将模型调整限制在总权重参数空间的较小维度子空间,从而有效捕获训练过程中对权重参数变化影响最大的方向。LoRA之所以有用且受到广泛欢迎,是因为它能够高效地对大模型进行特定任务地微调,显著降低了通常所需地计算成本和资源。

在常规地训练和微调中。在更新权重时,会得到更新值\Delta W,权重更新后W_{updated} = W + \Delta W。在LoRA中,提出一种近似替代权重更新值\Delta W,即\Delta W \approx ABW_{updated} = W + AB。其中A,B是两个比W小得多的矩阵。

通过引入了两个低秩矩阵后,就可以把原始的预训练权重参数W冻结起来,不参与梯度更新,只训练低秩矩阵A和B。这样一来,仅需训练少量参数,避免了预训练知识被覆盖,提高了训练效率地同时,又降低了储存需求。

接下来,将LoRA应用于垃圾消息分类中。

1、准备数据集

可以直接使用第六讲地数据准备部分代码。

from pathlib import Path
import pandas as pd
import torch
from torch.utils.data import Dataset, DataLoader
import tiktoken
from classificationFine_tuning import download_and_unzip_spam_data, crate_balanced_dataset, \
    random_split, SpamDataset


if __name__ == "__main__":
    ###准备数据
    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 = crate_balanced_dataset(df)
    # 调整标签为0、1
    balanced_df["Label"] = balanced_df["Label"].map({"ham": 0, "spam": 1})
    # 划分数据集
    train_df, val_df, test_df = random_split(balanced_df, 0.7, 0.1)
    # 保存为csv文件
    train_df.to_csv("train.csv", index=None)
    val_df.to_csv("val.csv", index=None)
    test_df.to_csv("test.csv", index=None)

    ###实例化数据集
    tokenizer = tiktoken.get_encoding("gpt2")
    train_dataset = SpamDataset("train.csv", max_length=None, tokenizer=tokenizer)
    val_dataset = SpamDataset("val.csv", max_length=None, tokenizer=tokenizer)
    test_dataset = SpamDataset("test.csv", max_length=None, tokenizer=tokenizer)
    ###创建数据加载器
    num_workers = 0
    batch_size = 4
    
    torch.manual_seed(123)
    
    train_loader = DataLoader(dataset=train_dataset,
                              batch_size=batch_size,
                              shuffle=True,
                              drop_last=True,
                              num_workers=num_workers)
    val_loader = DataLoader(dataset=val_dataset,
                                  batch_size=batch_size,
                                  shuffle=False,
                                  drop_last=False,
                                  num_workers=num_workers)
    test_loader = DataLoader(dataset=test_dataset,
                                  batch_size=batch_size,
                                  shuffle=False,
                                  drop_last=False,
                                  num_workers=num_workers)

2、初始化模型

复用第六讲地代码来加载和准备预训练的GPT模型。下载权重后加载到模型中。

from gpt_download import download_and_load_gpt2
from gpt2 import GPTModel
from PreTrain import load_weight_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, "num_layers": 12, "num_heads": 12},
    "gpt2-medium (355M)": {"emb_dim": 1024, "num_layers": 24, "num_heads": 16},
    "gpt2-large (774M)": {"emb_dim": 1280, "num_layers": 36, "num_heads": 20},
    "gpt2-xl (1558M)": {"emb_dim": 1600, "num_layers": 48, "num_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_weight_into_gpt(model, params)

替换最后的输出层,为微调做准备

#替换输出层
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)

3、LoRA进行参数高效微调

使用LoRA来微调大模型时,首先初始化一个LoRA层。它创建了矩阵A和B,并设置了alpha缩放因子和内部维度rank(r)。该层可以接受输入并计算相应的输出。rank决定了LoRA引入的额外参数量,可以通过调整r的大小来平衡参数量和模型性能。而alpha作为低秩自适应输出的缩放因子,决定了适应层的输出对原始层输出的影响程度。

class LoRALayer(nn.Module):
    def __init__(self, in_dim, out_dim, rank, alpha):
        super().__init__()
        self.A = nn.Parameter(torch.empty(in_dim, rank))
        #对线性层基于 He/Kaiming 初始化
        nn.init.kaiming_uniform(self.A, a=math.sqrt(5))
        self.B = nn.Parameter(torch.zeros(rank, out_dim))
        self.alpha = alpha

    def forward(self, x):
        x = self.alpha * (x @ self.A @ self.B)
        return x

为了整合原始线性层的权重,创建一个LinearWithLoRA层,用于替换神经网络中的现有线性层。

class LinearWithLoRA(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)

#替换线性层函数
def replace_linear_with_lora(model, rank, alpha):
    for name, module in model.named_children():
        if isinstance(module, nn.Linear):
            setattr(model, name, LinearWithLoRA(module, rank, alpha))
        else: #递归 处理子模块
            replace_linear_with_lora(module, rank, alpha)

在前面给B矩阵初始化为0,这样AB矩阵乘积就是零矩阵,加上原始权重后,也不会改变原始权重。接下来就是应用replace_linear_with_lora方法将GPTModel模型中的多头注意力模块、前馈模块和输出层中所有的Linear层替换成带有LoRA层。

#Linear替换成LoRAwithLoRA层
#冻结原始模型参数
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable params before: {total_params}") #Total trainable params before: 124441346
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 params after: {total_params}") #Total trainable params after: 0

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 params before: {total_params}") #Total trainable LoRA params before: 2666528

可以看到模型参数得到大幅度降低。通常会将alpha设置为rank的一半、两倍或相等的值。最后就是微调修改后的LoRA层。这里继续调用第六讲的train_classifier_simple方法训练模型。

#开始微调LoRA层参数
import time
from classificationFine_tuning import train_classifier_simple

model.to(device)

start_time = time.time()
torch.manual_seed(123)
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.1)
epochs = 5

train_losses, val_losses, train_accs, val_accs, examples_seen = train_classifier_simple(
    model, train_loader, val_loader, optimizer, device, epochs,
    eval_freq=50, eval_iter=5
)
end_time = time.time()
print("Training completed in {(end_time - start_time) / 60:.2f} minutes.")

在这里,使用LoRA训练模型比不使用LoRA时,花费的时间可能要更长,因为LoRA层在前向传播过程中引入了额外的计算。但是,对于较大的模型,反向传播的成本更高,此时使用LoRA的模型通常比不使用LoRA的模型训练速度更快。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐