游乐游手机版
首页/AI热点日报/热点详情

LLM+LoRA微调加速技术原理解析

类型:热点整理2026-07-11
如何以更低的算力成本高效完成大模型微调训练,已成为当前业界关注的核心议题。LLaMA、Alpaca、Instruct微调、LoRA微调……各类技术概念层出不穷。近期在持续学习过程中,我发现了几个值得深入探讨的关键话题。 Instruct微调与LoRA微调:核心差异与选择指南 Instruct微调与L

如何以更低的算力成本高效完成大模型微调训练,已成为当前业界关注的核心议题。LLaMA、Alpaca、Instruct微调、LoRA微调……各类技术概念层出不穷。近期在持续学习过程中,我发现了几个值得深入探讨的关键话题。

Instruct微调与LoRA微调:核心差异与选择指南

Instruct微调与LoRA微调是两种截然不同的技术路线。Instruct微调会对整个预训练模型的所有参数进行更新,使模型更好地适配新的下游任务。而LoRA微调则是在每个Transformer块中注入可训练的低秩矩阵,仅优化这些新增的少量参数,无需计算大部分模型权重的梯度,从而显著降低GPU内存需求并缩短训练时间。研究表明,LoRA的微调质量可与全参数微调相媲美,但速度更快、计算量更小。如果对低延迟和低内存有强烈需求,LoRA无疑是更优的选择。

LLaMA与LoRA:两种模型的关系与协同

基于LoRA的微调会产生新的权重,这些LoRA权重可视为原版LLaMA模型的一个“补丁”。而LLaMA权重本身,是由Meta公司开源的大模型预训练权重。两者搭配使用,才能获得完整的微调效果。

词表扩充:为什么不能直接在原版LLaMA上用中文预训练?

原版LLaMA对中文的支持并不理想。大多数中文相关的工作会直接在原版上进行预训练或微调,但更激进的做法是扩充中文词表——这可能会加剧中文训练不充分的问题,但从长远看是否有利于后续预训练,还需要时间检验。扩充词表带有一定破坏性:一是破坏原有分词体系,二是引入未训练的权重。如果不能进行充分训练,可能会带来较大问题。除非是特别专业的领域(如生物医学等涉及大量专业词汇),否则没必要扩充英文词表。

原版LLaMA的词表大小为32K,主要针对英语训练(详见LLaMA论文),对多语种支持不够好——对比一下多语言经典模型XLM-R的词表大小为250K就可以看出差距。初步统计显示,LLaMA词表中仅包含极少数中文字符,导致中文在分词时被切得更碎,需要多个byte token才能拼成一个完整的汉字,信息密度显著降低。

例如,在扩展词表后的模型中,单个汉字倾向于被切成1个token,而原版LLaMA可能需要2-3个token才能组合成一个汉字,编解码效率明显下降。

Chinese-LLaMA-Alpaca项目在原版LLaMA基础上扩充了中文词表:在通用中文语料上训练了基于sentencepiece的20K中文词表,与原版LLaMA的32K词表合并,排除重复token后,最终中文LLaMA词表大小为49953。需要注意的是,Alpaca在fine-tune阶段比LLaMA多一个pad token,所以中文Alpaca的词表大小为49954。

深入理解LoRA:基本原理与PEFT实现

目前,很多基于LoRA的适配器微调模型(如Alpaca LoRA、Chinese-LLaMA-Alpaca)都已开源,使用时需要注明:中文LLaMA/Alpaca LoRA模型无法单独使用,需要搭配原版LLaMA模型,发布的LoRA权重就是原模型上的一个“补丁”,两者合并可获得完整版权重。

LoRA的核心原理是:冻结预训练模型权重,将可训练的秩分解矩阵注入到Transformer层的每个权重中,大幅减少下游任务的可训练参数数量。通俗来说,就是在右侧增加一个“旁支”:先用Linear层A将数据从d维降到r,再用Linear层B将数据从r变回d维,最后将左右两部分的结果相加融合,得到输出的hidden_state。

如上图所示,左边是预训练模型的权重,输入输出维度都是d,在训练期间被冻结,不接受梯度更新。右边部分对A使用随机的高斯初始化,B在训练开始时为零,r是秩,会对ΔWx做缩放 α/r。

值得庆幸的是,HuggingFace的PEFT库(参数高效微调,地址:https://github.com/huggingface/peft)提供了微调加速的方法。PEFT方法只对少量额外参数进行微调,同时冻结预训练LLM的大部分参数,大幅降低计算和存储成本,还能避免全参数微调中常见的“灾难性遗忘”问题。在低数据场景下,PEFT甚至比全参数微调效果更好,泛化能力更强。

例如,使用PEFT-LoRA进行加速微调的效果如下,优势一目了然:

PEFT对LoRA做了良好的封装,几步即可使用:

from peft import get_peft_model, LoraConfig, TaskType

peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, 
    inference_mode=False, 
    r=8, 
    lora_alpha=32, 
    lora_dropout=0.1,
    target_modules=['query_key_value']
)
model = "加载的模型"
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()

论文中总结了LoRA的几大优势:

1)一个预训练模型可以共享,为不同任务构建多个小的LoRA模块。通过替换图中的矩阵A和B就能高效切换任务,大幅降低存储需求和切换成本。

2)使用自适应优化器时,LoRA训练更高效,硬件门槛降低约3倍——不需要计算梯度和维护大多数参数的优化器状态,只优化注入的小型低秩矩阵。

3)简单的线性设计允许在部署时将可训练矩阵与冻结权重合并,不会引入额外推理延迟。

4)LoRA与许多先前方法正交,可与前缀调整等方法结合使用(论文附录E提供了示例)。

1. 引入开源组件

“+”表示增加代码:

from transformers import AutoModelForSeq2SeqLM
+ from peft import get_peft_model, LoraConfig, TaskType
model_name_or_path = "bigscience/mt0-large"
tokenizer_name_or_path = "bigscience/mt0-large"

2. 引入LoRA配置信息

peft_config = LoraConfig(
    task_type=TaskType.SEQ_2_SEQ_LM, 
    inference_mode=False, 
    r=8, 
    lora_alpha=32, 
    lora_dropout=0.1
)

3. 进行推理

from transformers import AutoModelForSeq2SeqLM
+ from peft import PeftModel, PeftConfig
peft_model_id = "smangrul/twitter_complaints_bigscience_T0_3B_LORA_SEQ_2_SEQ_LM"
config = PeftConfig.from_pretrained(peft_model_id)
model = AutoModelForSeq2SeqLM.from_pretrained(config.base_model_name_or_path)
+ model = PeftModel.from_pretrained(model, peft_model_id)
tokenizer = AutoTokenizer.from_pretrained(config.base_model_name_or_path)
model = model.to(device)
model.eval()
inputs = tokenizer("Tweet text : @HondaCustSvc Your customer service has been horrible during the recall process. I will never purchase a Honda again. Label :", return_tensors="pt")
with torch.no_grad():
    outputs = model.generate(input_ids=inputs["input_ids"].to("cuda"), max_new_tokens=10)
    print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True)[0])
# 'complaint'

基于mt0-large + LoRA的完整实践

接下来,我们使用HuggingFace的PEFT库运行一个完整的LoRA实践案例。模型选用mt0-large(仅1.2b参数),模型地址:https://huggingface.co/bigscience/mt0-large。

模型权重地址:https://huggingface.co/bigscience/mt0-large/tree/main

先说说mt0-large是什么。多任务提示微调(MTF)已被证明能帮助大型语言模型在zero-shot环境下生成新任务,但以往主要集中在英语数据和模型上。将MTF应用于预训练的多语言BLOOM和mT5模型,就产生了BLOOMZ和mT0这些微调变体。

具体来说,共生产了三种不同尺寸的核心型号:

  • BLOOMZ-P3 / mT0-P3:在纯英语的P3上微调的模型。
  • BLOOMZ / mT0:在xP3上微调的模型,xP3包含带英语提示的多语言数据集。
  • BLOOMZ-MT / mT0-MT:在xP3mt上微调的模型,xP3mt包含多语言数据集和机器翻译的提示语。

任务方面,选用金融领域情感分析任务financial_sentiment_analysis。给定一个句子,判断它是negative、positive还是neutral。数据样式如下:

{'sentence': "The 10,000-odd square metre plot that Stockmann has bought for the Nevsky Center shopping center is located on Nevsky Prospect , St Petersburg 's high street , next to the Vosstaniya Square underground station , in the immediate vicinity of Moscow Station .", 'label': 1, 'text_label': 'neutral'}

可以通过datasets组件直接调用。

1. 引入组件并设置参数

from transformers import AutoModelForSeq2SeqLM
from peft import get_peft_config, get_peft_model, get_peft_model_state_dict, LoraConfig, TaskType
import torch
from datasets import load_dataset
import os
os.environ["TOKENIZERS_PARALLELISM"] = "false"
from transformers import AutoTokenizer
from torch.utils.data import DataLoader
from transformers import default_data_collator, get_linear_schedule_with_warmup
from tqdm import tqdm
from datasets import load_dataset

device = "cuda"
model_name_or_path = "bigscience/mt0-large"
tokenizer_name_or_path = "bigscience/mt0-large"
checkpoint_name = "financial_sentiment_analysis_lora_v1.pt"
text_column = "sentence"
label_column = "text_label"
max_length = 128
lr = 1e-3
num_epochs = 3
batch_size = 8

2. 搭建模型

peft_config = LoraConfig(task_type=TaskType.SEQ_2_SEQ_LM, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()

3. 加载数据

dataset = load_dataset("financial_phrasebank", "sentences_allagree")
dataset = dataset["train"].train_test_split(test_size=0.1)
dataset["validation"] = dataset["test"]
del dataset["test"]
classes = dataset["train"].features["label"].names
dataset = dataset.map(
    lambda x: {"text_label": [classes[label] for label in x["label"]]},
    batched=True,
    num_proc=1,
)

4. 训练数据预处理

tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)

def preprocess_function(examples):
    inputs = examples[text_column]
    targets = examples[label_column]
    model_inputs = tokenizer(inputs, max_length=max_length, padding="max_length", truncation=True, return_tensors="pt")
    labels = tokenizer(targets, max_length=3, padding="max_length", truncation=True, return_tensors="pt")
    labels = labels["input_ids"]
    labels[labels == tokenizer.pad_token_id] = -100
    model_inputs["labels"] = labels
    return model_inputs

processed_datasets = dataset.map(
    preprocess_function,
    batched=True,
    num_proc=1,
    remove_columns=dataset["train"].column_names,
    load_from_cache_file=False,
    desc="Running tokenizer on dataset",
)
train_dataset = processed_datasets["train"]
eval_dataset = processed_datasets["validation"]

train_dataloader = DataLoader(
    train_dataset, shuffle=True, collate_fn=default_data_collator, batch_size=batch_size, pin_memory=True
)
eval_dataloader = DataLoader(eval_dataset, collate_fn=default_data_collator, batch_size=batch_size, pin_memory=True)

5. 设定优化器和正则项

optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
lr_scheduler = get_linear_schedule_with_warmup(
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=(len(train_dataloader) * num_epochs),
)

6. 训练与评估

model = model.to(device)

for epoch in range(num_epochs):
    model.train()
    total_loss = 0
    for step, batch in enumerate(tqdm(train_dataloader)):
        batch = {k: v.to(device) for k, v in batch.items()}
        outputs = model(**batch)
        loss = outputs.loss
        total_loss += loss.detach().float()
        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()

    model.eval()
    eval_loss = 0
    eval_preds = []
    for step, batch in enumerate(tqdm(eval_dataloader)):
        batch = {k: v.to(device) for k, v in batch.items()}
        with torch.no_grad():
            outputs = model(**batch)
        loss = outputs.loss
        eval_loss += loss.detach().float()
        eval_preds.extend(
            tokenizer.batch_decode(torch.argmax(outputs.logits, -1).detach().cpu().numpy(), skip_special_tokens=True)
        )

    eval_epoch_loss = eval_loss / len(eval_dataloader)
    eval_ppl = torch.exp(eval_epoch_loss)
    train_epoch_loss = total_loss / len(train_dataloader)
    train_ppl = torch.exp(train_epoch_loss)
    print(f"{epoch=}: {train_ppl=} {train_epoch_loss=} {eval_ppl=} {eval_epoch_loss=}")

训练日志输出如下:

100%|████████████████████████████████████████████████████████████████████████████████████████| 255/255 [02:21<00:00,  1.81it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████| 29/29 [00:07<00:00,  4.13it/s]
epoch=0: train_ppl=tensor(14.6341, device='cuda:0') train_epoch_loss=tensor(2.6834, device='cuda:0') eval_ppl=tensor(1.0057, device='cuda:0') eval_epoch_loss=tensor(0.0057, device='cuda:0')
100%|████████████████████████████████████████████████████████████████████████████████████████| 255/255 [02:00<00:00,  2.11it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████| 29/29 [00:05<00:00,  5.66it/s]
epoch=1: train_ppl=tensor(1.7576, device='cuda:0') train_epoch_loss=tensor(0.5640, device='cuda:0') eval_ppl=tensor(1.0052, device='cuda:0') eval_epoch_loss=tensor(0.0052, device='cuda:0')
100%|████████████████████████████████████████████████████████████████████████████████████████| 255/255 [01:33<00:00,  2.74it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████| 29/29 [00:04<00:00,  6.23it/s]
epoch=2: train_ppl=tensor(1.3830, device='cuda:0') train_epoch_loss=tensor(0.3243, device='cuda:0') eval_ppl=tensor(1.0035, device='cuda:0') eval_epoch_loss=tensor(0.0035, device='cuda:0')

7. 模型保存

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
model.sa ve_pretrained(peft_model_id)

8. 模型推理预测

from peft import PeftModel, PeftConfig

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
config = PeftConfig.from_pretrained(peft_model_id)
model = AutoModelForSeq2SeqLM.from_pretrained(config.base_model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
model.eval()

inputs = tokenizer(dataset["validation"][text_column][i], return_tensors="pt")
print(dataset["validation"][text_column][i])
print(inputs)
with torch.no_grad():
    outputs = model.generate(input_ids=inputs["input_ids"], max_new_tokens=10)
    print(outputs)
    print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True))

运行实例,例如输入:

Demand for fireplace products was lower than expected , especially in Germany .

输出:

{'input_ids': tensor([[  259,   264,   259, 82903,   332,  1090, 10040, 10371,   639,   259,
         19540,  2421,   259, 25505,   259,   261,   259, 21230,   281, 17052,
           259,   260,     1]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
tensor([[    0,   259, 32588,     1]])
['negative']

总结

本文从LoRA的基本原理和PEFT中的实现出发,结合mt0-large + LoRA的完整实践,给出了清晰的操作流程。关于更多细节,建议在理解原理后亲自动手实践,这是加深理解的最佳路径。

来源:https://m.elecfans.com/article/2066480.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。