Hugging Face LoRA 微调

背景原理与核心概念

在特定业务场景下,企业往往希望微调(Fine-tuning)大模型以注入自有数据或偏好。LoRA(Low-Rank Adaptation)是一种流行的参数高效微调方法,它通过为预训练模型的权重添加低秩矩阵实现微调,而非调整原模型的大量参数。

具体来说,LoRA 会在模型的某些权重矩阵上引入两个小型可训练矩阵(通常秩 r 很小,如 4 或 8),只训练这部分新增参数,而冻结原模型的权重不变。经过训练后,在推理阶段这些低秩“适配器”的权重将与原权重相加,产生一个定制化的模型输出。

LoRA 的核心优势是大幅减少需要训练的参数量。例如,对一个 12 亿参数的模型应用 LoRA 微调,实际训练的参数可能不到原来的 0.2%。Hugging Face 的 PEFT 库提供的统计显示:在一个 12.3 亿参数的 mt0 模型上,LoRA(r=8)仅引入大约 236 万可训练参数,占比 0.19%。

如此小的训练开销意味着:

  • 资源需求低:微调时显著降低了显存和算力占用,使得在消费级硬件乃至 CPU 上微调小模型成为可能。
  • 训练速度快:需要更新的参数少,单步更新计算开销小,一定数据量下收敛更快。
  • 多任务适配:可以为同一个基础模型训练多个不同任务的 LoRA 适配器,在推理时按需加载对应适配器实现定制化。这样一来,一个基座模型 + 多套 LoRA 权重即可服务于多种场景,内存开销远低于为每个任务保存一个完整模型。

需要强调,LoRA 在推理时对速度几乎无影响——因为只是增加了对少量权重的线性组合计算。同时它不改变模型结构,所以不会影响上下文长度等模型原有特性。


安装与环境准备

在本节中,我们将介绍如何准备 Hugging Face Transformers 与 PEFT 库的环境,并说明硬件要求。

请确保已安装以下包:

pip install transformers peft datasets

提示

如需加速训练过程,建议在有 GPU 的环境执行,或安装 accelerate 配合多线程加速 CPU 运算。

本示例选择 Meta 的 OPT-125M 作为基底模型(约 1.2 亿参数),体量小便于 CPU 微调。此外还需要准备一小份示例数据(例如对话或问答数据)。这里为了演示,我们构造一个极简的合成数据集。


微调与运行步骤

下面详细介绍 LoRA 微调的完整流程,包括模型加载、配置、数据准备、训练与推理。

首先,加载预训练模型和分词器:

from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "facebook/opt-125m"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)

由于 OPT 是 Decoder 模型,我们使用 AutoModelForCausalLM。加载后 model 默认在 CPU。

接下来,准备 LoRA 配置并应用 PEFT:

from peft import LoraConfig, get_peft_model, TaskType
peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,   # 因为是自回归语言模型
    inference_mode=False,           # 训练模式
    r=8, lora_alpha=16, lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"]  # 指定只在注意力层的投影矩阵应用 LoRA
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()

以上配置创建了一个 LoRA 设置:秩 r=8,LoRA 内部尺度 alpha=16,dropout 5%。我们限定了在 OPT 结构中相应的 Q,K 投影层添加 LoRA 参数。get_peft_model 会返回一个 PEFT 封装的模型,其中原始模型参数被冻结,新加的 LoRA 参数将参与训练。调用 print_trainable_parameters() 可以看到当前可训练参数占比,例如输出类似:“trainable params: 104,000 || all params: 125,M || trainable%: 0.08%”。

然后,准备训练数据。这里我们构造一个简单的训练样本列表:

train_data = [
    {"prompt": "Q: 你好,你是谁?nA:", "response": " 我是一个 AI 语言模型。"},
    {"prompt": "Q: 今天天气怎么样?nA:", "response": " 很抱歉,我无法获取实时天气信息。"}
]
# 转换为 Dataset
from datasets import Dataset
dataset = Dataset.from_list(train_data)

每条数据包含一个 prompt 和期望的 response。实际应用中应替换为真实任务数据。

接下来,定义训练流程,使用 Transformers 自带的 Trainer 进行微调:

from transformers import Trainer, TrainingArguments

# 定义生成训练样本的函数(将 prompt 和 response 拼接)
def preprocess(example):
    text = example["prompt"] + example["response"]
    tokens = tokenizer(text, truncation=True, padding="max_length", max_length=128)
    # 构造 labels,只计算答案部分的 loss
    user_len = len(tokenizer(example["prompt"])["input_ids"])
    tokens["labels"] = [-100]*user_len + tokens["input_ids"][user_len:]
    return tokens

train_dataset = dataset.map(preprocess, remove_columns=["prompt", "response"])
training_args = TrainingArguments(
    output_dir="lora-opt125m",
    per_device_train_batch_size=1,
    num_train_epochs=5,
    learning_rate=1e-4,
    logging_steps=1,
    save_steps=50,
    save_total_limit=1
)
trainer = Trainer(model=model, args=training_args, train_dataset=train_dataset)
trainer.train()
model.save_pretrained("lora-opt125m")  # 保存 LoRA 微调后的模型适配器

上述步骤中,我们对每条输入做了预处理,将用户问和回答拼接,并设置了 labels 使得模型仅在回答部分计算 loss(防止模型改变提示部分)。我们使用小批量和较低学习率训练若干轮。由于示例数据极简且训练轮次少,几秒即可完成(在 CPU 上可能需要几分钟)。保存后的模型权重文件夹 lora-opt125m/ 中主要包含 LoRA 的适配器权重,而非整个模型。

最后,加载和推理。微调完成后,我们可以在 Mac 上加载基础模型+LoRA 适配器进行推理:

from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(model_name)
model = PeftModel.from_pretrained(base_model, "lora-opt125m")
model.eval()  # 切换到推理模式

prompt = "Q: 你好,你是谁?nA:"
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
output_ids = model.generate(input_ids, max_new_tokens=50, do_sample=False)
answer = tokenizer.decode(output_ids[0][input_ids.size(1):], skip_special_tokens=True)
print(answer)

这里,我们用基础模型加载 LoRA 权重(PeftModel.from_pretrained 会自动将 LoRA 权重合并到模型中仅在推理时生效)。然后对示例问题生成回答。理想情况下,应输出类似训练中给定的回答:“我是一个 AI 语言模型。”。您也可以尝试其他提示,模型会在微调知识的影响下产出新的结果。


推理方式与参数量对比

通过上述流程,我们完成了 LoRA 微调示例。在这个例子里,LoRA 的参数量和推理方式有如下特点。

  • 参数量对比:OPT-125M 模型有约 1.25 亿参数,而我们 LoRA 引入的参数大约只有十万级别,占比不到 0.1%。实际打印结果也验证了这一点。在更大型的模型上这种差异更为明显:LoRA 秩固定时,模型越大,训练参数占比越小。例如对 13B 模型用 LoRA,训练参数可能仅为原模型的万分之几。这体现了 LoRA 作为参数高效微调(PEFT)方法的强大之处。
  • 推理方式:LoRA 微调后的模型推理时,需要将 LoRA 适配器与基础模型合并。上面的 PeftModel.from_pretrained 实际上在内部将 LoRA 权重加到了 base 模型对应层上(仅在 forward 时暂时叠加,并不改动原权重)。也可以选择直接将 LoRA 权重合并到模型得到一个新的模型(这在 PEFT 库中也有支持),但通常保留分离可以灵活启停不同任务的 LoRA 模块。推理调用与原模型相同,例如使用 model.generate()pipeline 等皆可。由于 LoRA 不改变模型结构和大部分权重,推理速度与原模型几乎无差别。

完整示例

为了方便复现,以下是完整的 LoRA 微调示例代码,您可以保存为 lora_finetune_opt125m.py 并运行:

from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType, PeftModel
from datasets import Dataset
import torch

# LoRA 微调 Hugging Face Transformers 示例
# 适用于 OPT-125M 小模型,CPU/GPU 均可运行


def main():
    # 1. 加载预训练模型和分词器
    model_name = "facebook/opt-125m"
    model = AutoModelForCausalLM.from_pretrained(model_name)
    tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)

    # 添加 pad_token
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # 2. 配置 LoRA 并应用 PEFT
    peft_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        inference_mode=False,
        r=16,  # 增加 rank
        lora_alpha=32,  # 增加 alpha
        lora_dropout=0.05,
        target_modules=["q_proj", "v_proj", "k_proj", "out_proj"]  # 增加更多目标模块
    )
    model = get_peft_model(model, peft_config)
    model.print_trainable_parameters()

    # 3. 构造训练数据(增加更多相似样本)
    train_data = [
        {"prompt": "Q: Who are you?nA:", "response": " I am an AI language model."},
        {"prompt": "Q: Hello, who are you?nA:", "response": " I am an AI language model."},
        {"prompt": "Q: What are you?nA:", "response": " I am an AI language model."},
        {"prompt": "Q: Can you introduce yourself?nA:", "response": " I am an AI language model."},
        {"prompt": "Q: What's the weather today?nA:", "response": " Sorry, I can't access real-time weather information."},
        {"prompt": "Q: How is the weather?nA:", "response": " Sorry, I can't access real-time weather information."}
    ]
    dataset = Dataset.from_list(train_data)

    # 4. 数据预处理函数
    def preprocess(example):
        text = example["prompt"] + example["response"]
        tokens = tokenizer(text, truncation=True, padding="max_length", max_length=128)
        
        # 修正标签处理
        prompt_tokens = tokenizer(example["prompt"], add_special_tokens=False)
        prompt_len = len(prompt_tokens["input_ids"])
        
        labels = tokens["input_ids"].copy()
        # 将 prompt 部分的标签设为 -100,只计算 response 部分的损失
        labels[:prompt_len] = [-100] * prompt_len
        tokens["labels"] = labels
        return tokens

    train_dataset = dataset.map(preprocess, remove_columns=["prompt", "response"])

    # 5. 训练参数设置
    training_args = TrainingArguments(
        output_dir="lora-opt125m",
        per_device_train_batch_size=1,
        num_train_epochs=10,  # 增加训练轮次
        learning_rate=2e-4,   # 增加学习率
        logging_steps=1,
        save_steps=50,
        save_total_limit=1,
        warmup_steps=2,       # 添加预热
        gradient_accumulation_steps=2,  # 增加梯度累积
        dataloader_pin_memory=False,    # 禁用 pin_memory 避免 MPS 警告
        remove_unused_columns=False     # 避免列移除警告
    )

    # 6. 微调训练
    trainer = Trainer(model=model, args=training_args, train_dataset=train_dataset)
    trainer.train()
    model.save_pretrained("lora-opt125m")

    print("训练完成,LoRA 适配器已保存至 lora-opt125m/")

    # 7. 推理示例:加载 LoRA 权重并生成回答
    base_model = AutoModelForCausalLM.from_pretrained(model_name)
    lora_model = PeftModel.from_pretrained(base_model, "lora-opt125m")
    lora_model.eval()

    # 使用与训练数据完全一致的 prompt 格式
    prompt = "Q: Hello, who are you?nA:"
    input_ids = tokenizer(prompt, return_tensors="pt").input_ids
    
    # 测试不同的生成设置
    print("n=== 不同生成设置的对比 ===")
    
    # 1. 贪婪搜索(确定性输出)
    print("n1. 贪婪搜索 (do_sample=False):")
    with torch.no_grad():
        attention_mask = (input_ids != tokenizer.pad_token_id).long()
        output_ids = lora_model.generate(
            input_ids, 
            attention_mask=attention_mask,
            max_new_tokens=15,
            do_sample=False,
            pad_token_id=tokenizer.eos_token_id,
            eos_token_id=tokenizer.eos_token_id
        )
        answer = tokenizer.decode(output_ids[0][input_ids.size(1):], skip_special_tokens=True).strip()
        if 'n' in answer:
            answer = answer.split('n')[0]
        print(f"输出:{answer}")
    
    # 2. 低温度采样(偏向确定性)
    print("n2. 低温度采样 (temperature=0.1):")
    with torch.no_grad():
        output_ids = lora_model.generate(
            input_ids, 
            attention_mask=attention_mask,
            max_new_tokens=15,
            do_sample=True,
            temperature=0.1,
            pad_token_id=tokenizer.eos_token_id,
            eos_token_id=tokenizer.eos_token_id
        )
        answer = tokenizer.decode(output_ids[0][input_ids.size(1):], skip_special_tokens=True).strip()
        if 'n' in answer:
            answer = answer.split('n')[0]
        print(f"输出:{answer}")
    
    # 3. 中等温度采样(平衡创造性和一致性)
    print("n3. 中等温度采样 (temperature=0.7):")
    with torch.no_grad():
        output_ids = lora_model.generate(
            input_ids, 
            attention_mask=attention_mask,
            max_new_tokens=15,
            do_sample=True,
            temperature=0.7,
            top_p=0.9,
            pad_token_id=tokenizer.eos_token_id,
            eos_token_id=tokenizer.eos_token_id
        )
        answer = tokenizer.decode(output_ids[0][input_ids.size(1):], skip_special_tokens=True).strip()
        if 'n' in answer:
            answer = answer.split('n')[0]
        print(f"输出:{answer}")
    
    # 4. 高温度采样(更有创造性)
    print("n4. 高温度采样 (temperature=1.0):")
    with torch.no_grad():
        output_ids = lora_model.generate(
            input_ids, 
            attention_mask=attention_mask,
            max_new_tokens=15,
            do_sample=True,
            temperature=1.0,
            top_p=0.9,
            pad_token_id=tokenizer.eos_token_id,
            eos_token_id=tokenizer.eos_token_id
        )
        answer = tokenizer.decode(output_ids[0][input_ids.size(1):], skip_special_tokens=True).strip()
        if 'n' in answer:
            answer = answer.split('n')[0]
        print(f"输出:{answer}")
    
    print("n=== Temperature 参数说明 ===")
    print("• temperature=0.1: 输出更确定、保守")
    print("• temperature=0.7: 平衡确定性和创造性")
    print("• temperature=1.0: 输出更多样化、创造性")
    print("• do_sample=False: 贪婪搜索,完全确定性输出")

if __name__ == "__main__":
    main()

下面对 lora_finetune_opt125m.py 程序的主要步骤进行简要说明:

  • 导入依赖库:加载 Hugging Face Transformers、PEFT 和 Datasets 库,为模型微调和数据处理做准备。
  • 加载预训练模型和分词器:使用 AutoModelForCausalLMAutoTokenizer 加载 OPT-125M 基础模型及其分词器。
  • 配置 LoRA 并应用 PEFT:通过 LoraConfig 设置 LoRA 参数(如秩 r、alpha、dropout),指定目标模块,并用 get_peft_model 封装模型,使 LoRA 参数可训练,原模型参数冻结。
  • 构造训练数据集:创建包含 prompt 和 response 的训练样本,并用 Datasets 库转换为标准数据集格式。
  • 预处理数据:定义 preprocess 函数,将 prompt 和 response 拼接,设置 labels 只在回答部分计算 loss,保证模型学习目标明确。
  • 设置训练参数并微调:通过 TrainingArguments 配置训练参数(如 batch size、epoch、学习率等),使用 Trainer 进行模型微调,并保存 LoRA 适配器权重。
  • 加载微调后的模型进行推理:用 PeftModel.from_pretrained 加载基础模型和 LoRA 适配器,输入 prompt 进行文本生成,输出微调后的模型回答。

通过上述流程,你可以在 Hugging Face 生态下高效完成小模型的 LoRA 微调和推理,适用于个性化定制和低资源场景。


要点总结

  • LoRA 是什么:一种参数高效微调技术,通过增加小规模的低秩矩阵来调整模型,对原模型权重几乎零改动。直观理解:只学习“权重的差分”,并用极低的秩近似,达到几乎同等效果的同时,大幅减少训练开销。
  • 参数占比与资源优势:LoRA 通常只需训练不到 1% 的参数甚至更少。这意味着微调大型模型不再需要数亿的可训练参数和对应显存。较小的显存/内存即可运行微调,例如 7B 模型用 LoRA 可在单张消费级 GPU 甚至 CPU 上以较短时间收敛。这降低了定制大模型的门槛。
  • 效果与全微调对比:研究和实践表明,只要 LoRA 超参数选择得当(如秩、Alpha 等),LoRA 微调的效果能够接近全参数微调。同时,它保存下来的仅是适配器权重,小巧易部署;一个基础模型可以对应多个 LoRA 插件,根据不同任务动态加载,一模多用。
  • 推理整合:LoRA 微调后的模型在推理时需要与原模型配合使用——这可以通过 PEFT 库的 PeftModel 方便地实现。推理开销几乎不变。对于生产环境,这种方法允许我们部署一个基底模型,再根据用户需求选择加载特定 LoRA 权重,大大节省内存。很多开源大模型社区实践(如对 LLaMA 的各种微调版)都是用 LoRA 发布的,这种方式也使得分享和复现他人微调成果更加轻量(只需几个 MB 的适配器文件)。
  • 应用场景:当客户有自己的数据(FAQ 语料、行业文档等)希望在大模型基础上微调,LoRA 是首选方案之一。它能以最小成本将预训练模型个性化,例如定制客服机器人语气、特定领域问答等。而 Hugging Face 的 Transformers 和 PEFT 库提供了完整的工具链支持,从训练到部署非常便利。这也体现出在阿里云上,借助 PAI 平台或 Notebook,我们可以很快地 fine-tune 一个开源大模型并应用于业务。

总结

本文详细介绍了如何利用 Hugging Face Transformers 和 PEFT 库,通过 LoRA 技术实现大语言模型的高效微调。我们从原理、环境准备、完整代码流程到推理与参数量对比,系统梳理了 LoRA 的优势和应用场景。LoRA 让大模型微调变得轻量、灵活且低门槛,为个性化 AI 应用和行业定制提供了强大工具。未来,随着 PEFT 技术的不断发展,更多企业和开发者将能以更低成本享受大模型带来的智能红利。

技术落地

AI 原生应用架构简介

2026-1-11 14:44:58

技术落地

平台化

2026-1-11 14:45:01

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索