【大模型微调】4步上手 LoRA:从核心原理到部署,新手也能调大模型(超详细教程,手把手教你)

「鸿蒙心迹」“2025・领航者闯关记“主题征文活动 10w+人浏览 181人参与

作为大模型微调的 “性价比之王”,LoRA(Low-Rank Adaptation)凭啥成为开源模型微调的标配?不用海量算力,不用动大模型的 “核心代码”,只需训练几个 “小插件”,就能实现接近全量微调的效果 —— 这篇入门文,用通俗比喻讲清核心原理,再用 step-by-step 实践教你上手,新手也能快速落地。

-----------------------------------------点赞关注,后续干货持续更新~-------------------------------------

一、为什么LoRA行?

想象一下,那个巨大的预训练模型已经读过整个互联网的数据,它非常聪明,大脑里有数十亿个神经元连接。这些连接已经非常完美地处于一种平衡状态。

当我们为了一个特定的小任务(比如让它学会写某种特定格式的邮件,或者画一种特定风格的画)去微调它时,研究人员发现了一个惊人的现象:

虽然模型的参数矩阵非常巨大,但是为了适应新任务,这些参数矩阵需要发生的“变化量”,其实并不复杂。

用术语来说,就是权重更新矩阵的“内在维度”很低

二、LoRA 核心原理

基于这个洞察,LoRA 的发明者想:既然所需的变化量不复杂(低内在维度),那我们为什么要用一个和原模型一样巨大的矩阵来表示这个变化呢?这是巨大的浪费。

我们用一点点数学来表示。

原来的样子: 模型的某一层有一个巨大的权重矩阵 W(比如大小是 1000×1000)。微调时,我们要学一个同样巨大的变化矩阵 △W(也是 1000×1000),然后把它们加起来:

        W新 = W旧+△W。

LoRA 的做法: 既然△W的内在维度很低,我们就可以把它分解成两个非常小的“瘦子”矩阵相乘。

我们引入两个小矩阵:

  • 一个瘦高的矩阵B(比如1000×8)

  • 一个扁平的矩阵A(比如8×1000)

神奇的线性代数告诉我们,这两个小矩阵相乘 (B×A),结果竟然会得到一个1000×1000 的大矩阵!

LoRA 的核心公式就是:让 △W≈B×A。

这里的关键数字是中间那个8。这个数字叫做秩 (Rank),在 LoRA 中用r表示。这个r非常非常小(通常是 4, 8, 16, 32 等),而原模型的维度通常是几千。

为什么行?

因为我们相信,那个巨大的变化矩阵△W中真正包含的有效信息,其实只需要这r=8个维度的组合就能概括了。我们不需要训练1000×1000 = 1,000,000个参数,只需要训练(1000×8) + (8×1000) = 16,000个参数。

参数量瞬间减少了 98% 以上!这就是 LoRA 高效的根本原因。

三、代码入门实战

使用经典的 RoBERTa 模型 (比较小)进行一个 情感分析任务(判断评论是好评还是差评)。可以在 AutoDL https://www.autodl.com/的最基础显卡(如 RTX 3090 甚至 2080Ti)上轻松运行,全流程大约只需 5-10 分钟。

🛠️ 第一步:AutoDL 环境准备

  1. 租赁实例: 在 AutoDL 上租一个实例(推荐 PyTorch 2.0.0+, Python 3.8+, CUDA 11.8+ 的镜像)。

  2. 打开终端: 登录 JupyterLab 后,打开终端。

3. 开启学术加速,下载数据:

# 1. 开启学术加速
source /etc/network_turbo

# 2. 下载训练集 (保存为 train.parquet)
wget https://huggingface.co/datasets/imdb/resolve/main/plain_text/train-00000-of-00001.parquet -O train.parquet

# 3. 下载测试集 (保存为 test.parquet)
wget https://huggingface.co/datasets/imdb/resolve/main/plain_text/test-00000-of-00001.parquet -O test.parquet

4. 安装核心依赖:

pip install transformers peft datasets accelerate scikit-learn

💻 第二步:编写代码 (run_lora.py)

新建一个文件 run_lora.py,并将以下代码复制进去。

import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer, 
    AutoModelForSequenceClassification, 
    TrainingArguments, 
    Trainer,
    DataCollatorWithPadding
)
from peft import LoraConfig, get_peft_model, TaskType
import numpy as np
import os

# -------------------------------------------------------------------------
# 1. 准备配置与辅助函数
# -------------------------------------------------------------------------
MODEL_ID = "roberta-base" # 基础模型
LR = 1e-3                 # 学习率 (LoRA通常可以使用比全量微调更大的学习率)
BATCH_SIZE = 16

def print_trainable_parameters(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"\n=======================================================")
    print(f"总参数量: {all_param:,}")
    print(f"可训练参数 (Trainable): {trainable_params:,}")
    print(f"参数占比: {100 * trainable_params / all_param:.2f}%")
    print(f"=======================================================\n")

# -------------------------------------------------------------------------
# 2. 加载数据与预处理 (读取本地 Parquet 文件)
# -------------------------------------------------------------------------
print(">>> 正在加载本地数据文件...")

# 确保文件存在,否则报错提示
if not os.path.exists("train.parquet") or not os.path.exists("test.parquet"):
    raise FileNotFoundError("请确保 train.parquet 和 test.parquet 都在当前目录下!")

# 使用 parquet 引擎加载本地文件
dataset = load_dataset(
    "parquet", 
    data_files={
        "train": "train.parquet", 
        "test": "test.parquet"
    }
)

print(">>> 数据加载成功!正在预处理...")

# 为了演示速度,我们依然只取前 1000 条训练,前 500 条验证
# 注意:本地加载后,key 是我们在 data_files 里定义的 'train' 和 'test'
small_train_dataset = dataset["train"].shuffle(seed=42).select(range(1000)) 
small_eval_dataset = dataset["test"].shuffle(seed=42).select(range(500))

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

def preprocess_function(examples):
    # IMDB 数据集通常包含 'text' 和 'label' 两列
    return tokenizer(examples["text"], truncation=True, padding=True, max_length=256)

tokenized_train = small_train_dataset.map(preprocess_function, batched=True)
tokenized_eval = small_eval_dataset.map(preprocess_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

# -------------------------------------------------------------------------
# 3. 加载原始模型 (冻结的大机器)
# -------------------------------------------------------------------------
print(f">>> 正在加载基础模型: {MODEL_ID}...")
# 这里会从 Hugging Face 下载模型权重,通常学术加速下没问题
model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_ID, num_labels=2, id2label={0: "NEGATIVE", 1: "POSITIVE"}, label2id={"NEGATIVE": 0, "POSITIVE": 1}
)

print("\n>>> [状态 1] 原始模型加载完毕。如果现在训练,是全量微调:")
print_trainable_parameters(model) # 此时应该是 100% 参数可训练

# -------------------------------------------------------------------------
# 4. 注入 LoRA (插入小旋钮) -- 核心步骤
# -------------------------------------------------------------------------
peft_config = LoraConfig(
    task_type=TaskType.SEQ_CLS, # 任务类型:序列分类
    inference_mode=False, 
    r=8,                        # 【重点】秩 Rank = 8
    lora_alpha=32,              # LoRA 的缩放系数
    lora_dropout=0.1,
    target_modules=["query", "value"] # RoBERTa 的注意力层
)

model = get_peft_model(model, peft_config)

print("\n>>> [状态 2] LoRA 注入完毕。现在看看还需要训练多少参数:")
print_trainable_parameters(model) # 期待看到 < 1% 的参数量

# -------------------------------------------------------------------------
# 5. 开始训练 (拧小旋钮)
# -------------------------------------------------------------------------
training_args = TrainingArguments(
    output_dir="./lora_roberta_output",
    learning_rate=LR,
    per_device_train_batch_size=BATCH_SIZE,
    per_device_eval_batch_size=BATCH_SIZE,
    num_train_epochs=3,           
    weight_decay=0.01,
    evaluation_strategy="epoch",  
    save_strategy="epoch",
    load_best_model_at_end=True,
    logging_steps=10,
    report_to="none"             
)

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    predictions = np.argmax(predictions, axis=1)
    return {"accuracy": (predictions == labels).mean()}

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_train,
    eval_dataset=tokenized_eval,
    tokenizer=tokenizer,
    data_collator=data_collator,
    compute_metrics=compute_metrics,
)

print("\n>>> 开始训练...")
trainer.train()

# -------------------------------------------------------------------------
# 6. 保存与测试
# -------------------------------------------------------------------------
print("\n>>> 训练完成,正在保存 LoRA 适配器...")
# 这里只会保存很小的 adapter 文件,而不是整个大模型
model.save_pretrained("./lora_roberta_final")

print(">>> 推理测试:")
text = "This movie is absolutely fantastic! The acting was superb."
inputs = tokenizer(text, return_tensors="pt").to(model.device)

# 确保模型在评估模式
model.eval()
with torch.no_grad():
    logits = model(**inputs).logits
predicted_class_id = logits.argmax().item()

print(f"评论内容: {text}")
print(f"预测情感: {model.config.id2label[predicted_class_id]}")

🚀 第三步:运行与观察

在终端中运行:

python run_lora.py

输出:

这就是 LoRA 的核心魅力:我们仅仅训练了原本 0.2% 的参数量,就让模型学会了分辨电影评论的情感。

📊 第四步:应用

训练完了模型,怎么把它拿出来用呢?

在 LoRA 的世界里,加载模型有一个独特的合体过程: 我们需要先加载那个原本的笨重的大模型(Base Model),然后再把我们训练好的轻量级 LoRA 适配器(Adapter)“挂载”上去。

新建一个文件 inference.py,然后粘贴以下代码:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel, PeftConfig

# -------------------------------------------------------------------------
# 1. 路径设置
# -------------------------------------------------------------------------
# 这是我们之前训练代码中保存的文件夹路径
peft_model_id = "./lora_roberta_final" 
# 基础模型名称(必须与训练时一致)
base_model_id = "roberta-base"

print(f">>> 正在准备环境...")
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {device}")

# -------------------------------------------------------------------------
# 2. 加载原始大模型
# -------------------------------------------------------------------------
print(f">>> [步骤 1/3] 正在加载原始底座模型 ({base_model_id})...")
# 注意:这里我们重新定义了 label 映射,确保输出不仅仅是数字
base_model = AutoModelForSequenceClassification.from_pretrained(
    base_model_id, 
    num_labels=2, 
    id2label={0: "差评 (Negative)", 1: "好评 (Positive)"},
    label2id={"差评 (Negative)": 0, "好评 (Positive)": 1}
)
tokenizer = AutoTokenizer.from_pretrained(base_model_id)

# -------------------------------------------------------------------------
# 3. 加载LoRA 适配器 
# -------------------------------------------------------------------------
print(f">>> [步骤 2/3] 正在加载 LoRA 适配器并与底座合并...")
# PeftModel.from_pretrained 会自动把 save 好的 adapter 权重加到 base_model 上
model = PeftModel.from_pretrained(base_model, peft_model_id)
model.to(device)
model.eval() # 切换到评估模式

print(">>> 模型加载完成!准备就绪。")

# -------------------------------------------------------------------------
# 4. 定义推理函数
# -------------------------------------------------------------------------
def predict_sentiment(text):
    # 1. 预处理文本
    inputs = tokenizer(
        text, 
        return_tensors="pt", 
        truncation=True, 
        padding=True, 
        max_length=256
    ).to(device)

    # 2. 模型推理 (不计算梯度,节省显存)
    with torch.no_grad():
        outputs = model(**inputs)
    
    # 3. 获取概率最大的类别
    logits = outputs.logits
    probabilities = torch.nn.functional.softmax(logits, dim=-1)
    prediction_id = torch.argmax(logits, dim=-1).item()
    prediction_label = model.config.id2label[prediction_id]
    confidence = probabilities[0][prediction_id].item()

    return prediction_label, confidence

# -------------------------------------------------------------------------
# 5. 测试一下
# -------------------------------------------------------------------------
print("\n" + "="*50)
print("开始测试 (你可以修改 list 中的句子)")
print("="*50)

test_sentences = [
    "I absolutely loved this movie! The acting was incredible.",     # 明显的褒义
    "This was a complete waste of time. The plot was boring.",       # 明显的贬义
    "It was okay, but I would not watch it again.",                  # 中性偏贬义
    "The cinematography was great, but the story was weak.",         # 混合评价
    "AutoDL is a fantastic platform for learning AI."                # 域外测试
]

for text in test_sentences:
    label, score = predict_sentiment(text)
    print(f"\n📝 评论: {text}")
    print(f"🤖 预测: {label}")
    print(f"📊 置信度: {score:.4f}")
  1. 保存上述代码为 inference.py

  2. 确保 lora_roberta_final 文件夹在同一级目录下(这就是刚才 run_lora.py 生成的)。

  3. 在终端运行:

    python inference.py
    

代码原理解析

这段代码最关键的一行是:

model = PeftModel.from_pretrained(base_model, peft_model_id)

这就是 LoRA 的精髓:

  • base_model 是几百兆甚至几 G 的大文件。

  • peft_model_id 指向的文件夹里,其实只有一个非常小的 adapter_model.bin(可能只有几 MB)。

  • 这一行代码在内存中动态地把那“几 MB 的小旋钮”装到了“几百 MB 的大机器”上,让它瞬间拥有了你训练出的能力。

------------------------------------------点赞关注,后续干货持续更新~---------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值