PEFT 库中文本生成LoRA 教程

在本教程中介绍如何使用的 peft 库和 bitsandbytes 来以 8-bits 加载大语言模型,并对其进行高效微调。微调方法使用"低秩适配器"(LoRA)的方法

一、加载模型

Facebook opt-1.3b 模型,模型权重大约需要1.53GB显存。

python 复制代码
import os

import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import GPT2Tokenizer, AutoConfig, OPTForCausalLM

model_id = "facebook/opt-6.7b"

model = OPTForCausalLM.from_pretrained(model_id, load_in_8bit=True)

tokenizer = GPT2Tokenizer.from_pretrained(model_id)

二、模型处理

在使用 peft 训练 int8 模型之前,需要进行一些预处理:

  • 将所有非 int8 模块转换为全精度(fp32)以保证稳定性
  • 为输入嵌入层添加一个 forward_hook,以启用输入隐藏状态的梯度计算
  • 启用梯度检查点以实现更高效的内存训练

使用 peft 库预定义的工具函数 prepare_model_for_int8_training,便可自动完成以上模型处理工作。

python 复制代码
from peft import prepare_model_for_int8_training

model = prepare_model_for_int8_training(model)

获取当前模型占用的 GPU显存大小:

python 复制代码
memory_footprint_bytes = model.get_memory_footprint()
memory_footprint_mib = memory_footprint_bytes / (1024 ** 3)  # 转换为 GB

print(f"{memory_footprint_mib:.2f}GB")

输出:

复制代码
1.53GB

查看model参数:

python 复制代码
print(model)

输出:

python 复制代码
OPTForCausalLM(
  (model): OPTModel(
    (decoder): OPTDecoder(
      (embed_tokens): Embedding(50272, 2048, padding_idx=1)
      (embed_positions): OPTLearnedPositionalEmbedding(2050, 2048)
      (final_layer_norm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
      (layers): ModuleList(
        (0-23): 24 x OPTDecoderLayer(
          (self_attn): OPTAttention(
            (k_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
            (v_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
            (q_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
            (out_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
          )
          (activation_fn): ReLU()
          (self_attn_layer_norm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
          (fc1): Linear8bitLt(in_features=2048, out_features=8192, bias=True)
          (fc2): Linear8bitLt(in_features=8192, out_features=2048, bias=True)
          (final_layer_norm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
        )
      )
    )
  )
  (lm_head): Linear(in_features=2048, out_features=50272, bias=False)
)

三、LoRA Adapter 配置

peft 中使用LoRA非常简捷,借助 PeftModel抽象,可以快速使用低秩适配器(LoRA)到任意模型。

通过使用 peft 中的 get_peft_model 工具函数来实现。

  • 关于 LoRA 超参数的说明:

    MatMul(B,A) * Scaling
    Scaling = LoRA_Alpha / Rank

python 复制代码
# 从peft库导入LoraConfig和get_peft_model函数
from peft import LoraConfig, get_peft_model

# 创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
    r=8,  # LoRA的秩,影响LoRA矩阵的大小
    lora_alpha=32,  # LoRA适应的比例因子
    # 指定将LoRA应用到的模型模块,通常是attention和全连接层的投影
    target_modules = ["q_proj", "k_proj", "v_proj", "out_proj", "fc_in", "fc_out"],
    lora_dropout=0.05,  # 在LoRA模块中使用的dropout率
    bias="none",  # 设置bias的使用方式,这里没有使用bias
    task_type="CAUSAL_LM"  # 任务类型,这里设置为因果(自回归)语言模型
)

# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
model = get_peft_model(model, config)

# 打印出模型中可训练的参数
model.print_trainable_parameters()

输出:

复制代码
trainable params: 8,388,608 || all params: 6,666,862,592 || trainable%: 0.12582542214183376

参考:打印待训练模型参数的实现逻辑

python 复制代码
def print_trainable_parameters(self,):
    """
    Prints the number of trainable parameters in the model.
    """
    trainable_params = 0
    all_param = 0
    for _, param in model.named_parameters():
        all_param += param.numel()
        if param.requires_grad:
            trainable_params += param.numel()
    print(
        f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
    )

四、加载数据

加载数据集

python 复制代码
from datasets import load_dataset

dataset = load_dataset("Abirate/english_quotes")
print(dataset["train"])

输出:

python 复制代码
Dataset({
    features: ['quote', 'author', 'tags'],
    num_rows: 2508
})

查看数据集:

python 复制代码
from datasets import ClassLabel, Sequence
import random
import pandas as pd
from IPython.display import display, HTML

def show_random_elements(dataset, num_examples=10):
    assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
    picks = []
    for _ in range(num_examples):
        pick = random.randint(0, len(dataset)-1)
        while pick in picks:
            pick = random.randint(0, len(dataset)-1)
        picks.append(pick)
    
    df = pd.DataFrame(dataset[picks])
    for column, typ in dataset.features.items():
        if isinstance(typ, ClassLabel):
            df[column] = df[column].transform(lambda i: typ.names[i])
        elif isinstance(typ, Sequence) and isinstance(typ.feature, ClassLabel):
            df[column] = df[column].transform(lambda x: [typ.feature.names[i] for i in x])
    display(HTML(df.to_html()))
 
show_random_elements(dataset["train"], num_examples=1)

输出:

| | quote | author | tags |

0 "As usual, there is a great woman behind every idiot." John Lennon [beatles, men, women]

数据收集器,用于处理语言模型的数据,这里设置为不使用掩码语言模型(MLM)

python 复制代码
from transformers import DataCollatorForLanguageModeling

tokenized_dataset = dataset.map(lambda samples: tokenizer(samples["quote"]), batched=True)

data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

输出:

python 复制代码
Generating train split: 2508 examples [00:00, 112455.52 examples/s]
Map: 100%|██████████| 2508/2508 [00:00<00:00, 4468.83 examples/s]

五、微调模型

设置模型训练参数:

python 复制代码
from transformers import TrainingArguments, Trainer

model_dir = "models"

training_args = TrainingArguments(
        output_dir=f"{model_dir}/{model_id}-lora",  # 指定模型输出和保存的目录
        per_device_train_batch_size=4,  # 每个设备上的训练批量大小
        learning_rate=2e-4,  # 学习率
        fp16=True,  # 启用混合精度训练,可以提高训练速度,同时减少内存使用
        logging_steps=20,  # 指定日志记录的步长,用于跟踪训练进度
        # max_steps=100, # 最大训练步长
        num_train_epochs=1  # 训练的总轮数
    )

模型开始训练:

python 复制代码
trainer = Trainer(
    model=model,  # 指定训练时使用的模型
    train_dataset=tokenized_dataset["train"],  # 指定训练数据集
    args=training_args,
    data_collator=data_collator,
)

model.use_cache = False
trainer.train()

输出:

复制代码
{'train_runtime': 565.5103, 'train_samples_per_second': 4.435, 'train_steps_per_second': 1.109, 'train_loss': 2.33140508600019, 'epoch': 1.0}
  • 保存 LoRA 模型
python 复制代码
model_path = f"{model_dir}/{model_id}-lora-int8"

#trainer.save_model(model_path)
model.save_pretrained(model_path)

六、文本预测

通过在 english_quotes 数据集上的少量微调,LoRA 适配器恢复了阿尔伯特·爱因斯坦的名言警句。

加载模型:

python 复制代码
lora_model = trainer.model

进行文本生成测试:

python 复制代码
text = "Two things are infinite: "
inputs = tokenizer(text, return_tensors="pt").to(0)

out = lora_model.generate(**inputs, max_new_tokens=48)
print(tokenizer.decode(out[0], skip_special_tokens=True))

输出:

复制代码
Two things are infinite:  The universe and human stupidity; and I'm not sure about the universe.
I think the universe is infinite, but I'm not sure about the stupidity.
I think the universe is infinite, but I'm not sure about the stupidity

附件(完整代码)

python 复制代码
import torch
import torch.nn as nn
import bitsandbytes as bnb
from datasets import load_dataset
from peft import LoraConfig, get_peft_model
from peft import prepare_model_for_kbit_training
from transformers import TrainingArguments, Trainer
from transformers import DataCollatorForLanguageModeling
from transformers import GPT2Tokenizer, AutoConfig, OPTForCausalLM

def print_model_memory(model):
    """
    打印模型占用的显存(以 GB 为单位)、模型结构以及可训练参数数量。
    """
    # 获取模型在 GPU 上占用的内存(字节)
    memory_footprint_bytes = model.get_memory_footprint()
    # 转换为 GB(注意:1024^3 = 1 GiB,但此处注释写为 GB,实际是 GiB)
    memory_footprint_mib = memory_footprint_bytes / (1024 ** 3)
    print(f"模型显存占用: {memory_footprint_mib:.2f} GB")
    # 打印模型结构
    print(model)
    # 打印可训练参数的数量和占比(PEFT 特有方法)
    model.print_trainable_parameters()


# === 1. 加载预训练模型(使用 8-bit 量化以节省显存)===
model_id = "facebook/opt-1.3b" 
# 从预训练模型加载 OPT-1.3B,并启用 8-bit 量化(大幅降低显存需求)
model = OPTForCausalLM.from_pretrained(model_id, load_in_8bit=True)

# 对量化后的模型进行适配,使其支持后续的梯度训练(如 LoRA)
model = prepare_model_for_kbit_training(model)

# 加载与模型配套的 tokenizer(OPT 使用 GPT-2 的 tokenizer)
tokenizer = GPT2Tokenizer.from_pretrained(model_id)


# === 2. 配置并应用 LoRA(低秩自适应)===
# 创建 LoRA 配置对象,用于指定哪些层、如何插入低秩矩阵
config = LoraConfig(
    r=8,  # LoRA 矩阵的秩(rank),控制可训练参数量;值越小,参数越少
    lora_alpha=32,  # 缩放因子,控制 LoRA 更新的幅度(通常 alpha/r 决定实际学习率缩放)
    # 指定在哪些模块上插入 LoRA。OPT 模型中常见的注意力和 FFN 投影层:
    target_modules=["q_proj", "k_proj", "v_proj", "out_proj", "fc_in", "fc_out"],
    lora_dropout=0.05,  # LoRA 层中的 dropout 概率,用于防止过拟合
    bias="none",  # 不对 bias 参数进行 LoRA 微调(也可选 'all' 或 'lora_only')
    task_type="CAUSAL_LM"  # 任务类型为因果语言模型(即自回归生成)
)

# 将 LoRA 适配器注入到原始模型中,返回一个可训练的 PEFT 模型
model = get_peft_model(model, config)

# 打印模型显存占用、结构及可训练参数信息(用于调试和资源评估)
print_model_memory(model)


# === 3. 加载并预处理数据集 ===
# 从 Hugging Face Datasets 加载英文名言数据集
dataset = load_dataset("Abirate/english_quotes")

# 对数据集中的 "quote" 字段进行分词(批量处理以提高效率)
tokenized_dataset = dataset.map(
    lambda samples: tokenizer(samples["quote"]),  # 对每条 quote 进行 tokenize
    batched=True  # 启用批处理加速
)

# 创建用于语言建模的数据整理器(collator)
# mlm=False 表示不使用掩码语言建模(MLM),而是用于因果语言建模(CLM)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)


# === 4. 配置训练参数并初始化 Trainer ===
model_dir = "train-model"  # 本地模型保存根目录

# 定义训练超参数
training_args = TrainingArguments(
    output_dir=f"{model_dir}/{model_id}-lora",  # 模型和日志保存路径
    per_device_train_batch_size=4,  # 每个 GPU 的 batch size(根据显存调整)
    learning_rate=2e-4,  # 学习率(LoRA 常用范围:1e-4 ~ 3e-4)
    fp16=True,  # 启用混合精度训练(加速 + 节省内存)
    logging_steps=50,  # 每 50 步记录一次训练日志
    num_train_epochs=1  # 只训练 1 个 epoch(可根据需要调整)
    # max_steps=100,  # 可选:限制最大训练步数(与 epochs 二选一)
)

# 初始化 Hugging Face Trainer
trainer = Trainer(
    model=model,  # 使用已注入 LoRA 的模型
    train_dataset=tokenized_dataset["train"],  # 使用训练集
    args=training_args,  # 训练配置
    data_collator=data_collator,  # 数据整理器
)

# 关闭模型的缓存机制(在训练时通常设为 False,避免梯度计算错误)
model.use_cache = False

# 训练,取消下面两行注释
trainer.train()

# 保存微调后的 LoRA 权重(仅保存适配器,非全模型)
# model_path = f"{model_dir}/{model_id}-lora-int8"
model.save_pretrained(model_path)


# === 5. 使用微调后的模型进行文本生成(推理)===
# 获取训练器中的模型(即当前的 LoRA 模型)
lora_model = trainer.model

# 输入提示文本
text = "Two things are infinite: "

# 对输入文本进行分词,并将张量移动到 GPU(设备 0)
inputs = tokenizer(text, return_tensors="pt").to(0)

# 使用模型生成新文本(最多生成 48 个新 token)
out = lora_model.generate(**inputs, max_new_tokens=48)

# 解码生成的 token,跳过特殊符号(如 <s>, </s> 等)
print(tokenizer.decode(out[0], skip_special_tokens=True))
相关推荐
YongCheng_Liang2 小时前
零基础学 AI:AI 工程化部署与项目实战(从优化到落地全指南)
人工智能
励ℳ2 小时前
【CNN网络入门】基于PyTorch的MNIST手写数字识别:从数据准备到模型部署全流程详解
人工智能·pytorch·深度学习
香芋Yu2 小时前
【深度学习教程——05_生成模型(Generative)】25_扩散模型为什么能生成高质量图像?Diffusion数学推导
人工智能·深度学习
乐鑫科技 Espressif3 小时前
基于 ESP32-P4 的工业级智能机械臂设计与实现
人工智能·乐鑫科技
yubo05093 小时前
完整的 YOLO26 自定义模块注册 & 训练步骤
人工智能·深度学习
Sylvia33.3 小时前
火星数据:棒球数据API
java·前端·人工智能
nihao5613 小时前
OpenClaw 保姆级安装部署教程
人工智能
X54先生(人文科技)4 小时前
碳硅协同开发篇-ELR诞生记章
人工智能·ai编程·ai写作·程序员创富