草稿

Hugging Face Transformers + PEFT 的 LoRA 微调

让大模型微调变得轻量灵活,LoRA 让每个人都能拥有自己的 AI。

Hugging Face 的 Transformers 库结合 PEFT(Parameter-Efficient Fine-Tuning,参数高效微调)提供了便捷的接口来对大语言模型进行参数高效微调(PEFT)。其中,LoRA(Low-Rank Adaptation,低秩适配)是一种流行的 PEFT 方法,通过在预训练模型的部分权重上添加低秩矩阵实现微调,而非调整原模型的大量参数。这样可以大幅减少需要训练的参数量,从而降低显存和算力需求,使得在消费级硬件上微调大模型成为可能。

背景原理与核心概念

在特定业务场景下,企业往往希望微调(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 并运行:

📄 LoRA 微调示例代码:lora_finetune_opt125m.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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 技术的不断发展,更多企业和开发者将能以更低成本享受大模型带来的智能红利。

文章导航

章节内容

这是章节的内容页面。

章节概览