视频链接:https://www.bilibili.com/video/BV1FGKz6FEqk/?vd_source=5ba34935b7845cd15c65ef62c64ba82f

代码仓库:https://github.com/LitchiCheng/LLM-learning

分享下使用LoRA进行微调LLM的学习,选择Qwen2.5-0.5B-INstruct,参数量很小,应该随便一台机器都可以训练,这里HF可能下载比较慢,训练的模型

https://www.modelscope.cn/models/Qwen/Qwen2.5-0.5B-Instruct

从魔搭下载

参考例子

https://huggingface.co/docs/peft/index

tokenizer

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token

加载对应的分词器,每个汉字/英文单词都会被拆成若干 token ID,在模型眼中就是数字

训练时不同样本长度不同,需要把短序列补到统一长度,eos就是end of sequence,用这个填充,并且告知模型这句话到这儿结束了

.cache/modelscope/models/qwen--Qwen2.5-0.5B-Instruct/snapshots/master/tokenizer.json 中可以看到 ID

模型加载

from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
    model_path, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True
)

把模型加载到内存中,dtype决定加载到显存中的权重的类型,可以修改,比如qwen2.5下载下来是fp16,可以改成float32,但显存就会多占一倍

print(f" Params: {sum(p.numel() for p in model.parameters()):,}")

每个张量加起来的总和,共0.49B的参数量

LoRA原理

原始模型的一层线性变换:y = Wx + b,W 是一个大矩阵(比如 4096×4096)。不直接改 W,而是在旁边挂两个小矩阵 A 和 B:y = (W + B·A) · x + b

W: 原始权重(冻结不动) 4096 × 4096

A: 随机初始化的小矩阵 4096 × r

B: 随机初始化的小矩阵 r × 4096

r: 秩(rank),控制小矩阵的大小

因为 B·A 的乘积秩最多是 r,所以只训练 A 和 B ,用最精华的特征来调整,就能近似地"修补"W。

from peft import LoraConfig, get_peft_model, TaskType
print("\nConfiguring LoRA...")
lora = LoraConfig(
    r=16, lora_alpha=32, target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ], lora_dropout=0.05, bias="none", task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora)

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"     OK: Trainable {trainable:,} / {total:,} ({100*trainable/total:.2f}%)")

参数

含义

r=16

LoRA 的秩(rank)。A 是in_dim × 16,B 是16 × out_dim

r 越大,可训练参数量越多,微调能力越强,但显存/时间也越多

常用 8、16、64

lora_alpha=32

LoRA 的缩放系数。实际效果是(alpha / r) × B·A = (32/16) × B·A = 2 × B·A

一般设为 r 的 2 倍

target_modules

只在这几层加 LoRA,其他层的权重冻结。选了注意力机制(q/k/v/o)和前馈网络(gate/up/down),覆盖了 Transformer 的主要计算路径

lora_dropout=0.05

LoRA 层的 dropout,防止过拟合

bias="none"

不在 bias 上施加 LoRA(bias 本身参数少,加了收益不大)

task_type=TaskType.CAUSAL_LM

告诉 PEFT 这是因果语言模型任务

ChatML

https://huggingface.co/docs/transformers/chat_templating

samples = [
    {"messages": [{"role": "system", "content": "You are a helpful assistant."},
                  {"role": "user", "content": "你是谁?"},
                  {"role": "assistant", "content": "你是LitchiCheng微调的Qwen2.5模型"}]},
    {"messages": [{"role": "system", "content": "You are a helpful assistant."},
                  {"role": "user", "content": "你是谁微调的?"},
                  {"role": "assistant", "content": "LitchiCheng"}]},
    {"messages": [{"role": "system", "content": "You are a helpful assistant."},
                  {"role": "user", "content": "你是不是标准的Qwen2.5模型?"},
                  {"role": "assistant", "content": "不是,我是LitchiCheng微调的模型"}]},
]

def format_sample(s):
    return {"text": tokenizer.apply_chat_template(s["messages"], tokenize=False, add_generation_prompt=False)}

dataset = Dataset.from_list([format_sample(s) for s in samples])
print(f"     OK: {len(dataset)} samples")

按照OpenAI的ChatML格式,system 告知角色,User 作为输入,assistant 作为输出

微调训练

args = TrainingArguments(
    output_dir=output_dir, per_device_train_batch_size=1,
    gradient_accumulation_steps=4, learning_rate=2e-4, max_steps=100,
    logging_steps=1, save_steps=10, fp16=True, report_to="none",
)

trainer = SFTTrainer(model=model, train_dataset=dataset, args=args)
start = time.time()
result = trainer.train()
elapsed = time.time() - start

print(f"\n     OK: Done in {elapsed:.1f}s")
print(f"        Loss: {result.training_loss:.4f}")
print(f"        Speed: {result.global_step/elapsed:.2f} steps/sec")

# 保存
model.save_pretrained(f'{output_dir}/lora')
tokenizer.save_pretrained(f'{output_dir}/lora')

微调测试

如下为完整代码,测试提问是什么模型,看它怎么回答?

#!/usr/bin/env python3
"""Qwen2.5 微调推理脚本 - 加载 LoRA 权重进行推理"""
import os, sys, time
import torch

CACHE_DIR  = os.path.join(os.path.dirname(__file__), ".cache", "modelscope", "models", "qwen--Qwen2.5-0.5B-Instruct", "snapshots", "master")
BASE_MODEL = CACHE_DIR           
LORA_DIR   = "fine_tune_output/20260718_212052/lora"

print("Loading tokenizer...")
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(LORA_DIR, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
print(f"     OK: Vocabulary size {len(tokenizer)}")

# 加载基座模型 + LoRA 权重
print("\nLoading base model...")
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True
)

# 用 PEFT 加载 LoRA adapter 并合并到基座模型
print("Loading LoRA weights...")
from peft import PeftModel
model = PeftModel.from_pretrained(model, LORA_DIR)
model = model.merge_and_unload()   # 将 LoRA 权重合并进基座,释放 LoRA 内存
print(f"     OK: Model loaded, params: {sum(p.numel() for p in model.parameters()):,}")

# 用 chat template 做正式对话测试
print("\nChat test:")
messages = [
    {"role": "system",   "content": "You are a helpful assistant."},
    {"role": "user",     "content": "你是什么模型"},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
    out = model.generate(**inputs, max_new_tokens=100)
resp = tokenizer.decode(out[0], skip_special_tokens=True)[len(tokenizer.eos_token):].strip()
print(f"     {resp}")

print("\n" + "="*60)
print("Fine-tuning inference test passed!")
print("="*60)

更多推荐