1、論文地址和代碼倉
《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》
《大語言模型的低秩適應》
https://arxiv.org/pdf/2106.09685
https://github.com/microsoft/LoRA
2、核心原理
凍結(jié)左邊的預訓練權重矩陣W,只訓練A和B兩個低秩矩陣,然后,通過W、A和B兩邊的計算結(jié)果疊加得到最后的計算結(jié)果。不過,工程實踐中是將W、A和B三個合并成新的權總矩陣,再進行計算。

圖中,x為輸入,h為輸出結(jié)果,W為預訓練的權重矩陣,A和B為重新訓練的低秩權重矩陣,dxd表示預訓練模型的維度,d x r表示低秩舉證的維度,其中r<<d,A=N(0,σ^2) 表示A的訓練初始值是均值為0,方差為σ^2的正態(tài)分布舉證,學術名稱高斯初始化;B=0 值全為0的矩陣,學術名稱零初始化。
LoRA數(shù)學公式表示如下:

實際訓練中數(shù)學公式表示如下:

其中,r為LoRA秩,r越大信息越豐富,但計算量越大,α為超參
合并模型參數(shù)的數(shù)學公式表示如下:

加號前面的W0表示新知識,后面的△W表示舊知識
LoRA的訓練過程如下:

左邊是Transform架構(gòu)圖,圖引用出處見參考文章[1]
有幾個關鍵點
1)為什么LoRA使重新訓練效率更高
因為全量微調(diào)重新訓練需要計算得到的參數(shù)個數(shù)為d x d,而LoRA計算得到的參數(shù)個數(shù)為2 x r x d,舉個例子,r=10,d=1000,則全量微調(diào)矩陣為1000 x 1000,參數(shù)為1000000個,LoRA矩陣為20000,則LoRA需要訓練地參數(shù)個數(shù)遠小于全量訓練舉證,所以效率更高。
2)為什么LoRA可以達到微調(diào)的目的(數(shù)學依據(jù))
因為全量微調(diào)中的矩陣d x d存在冗余的信息,可以通過低階的矩陣來表示
舉個例子
A = [[1, 2, 3],
[2, 4, 6],
[3, 6, 9]]
A矩陣為3 x 3的矩陣,實際上, [2, 4, 6](第二行) = 2 x [1, 2, 3] (第一行), [3, 6, 9] = 3 x [1, 2, 3](第一行) ,也就是說只需要有第一行的信息,就可以表示矩陣A。數(shù)學上給了個定義就叫做秩,矩陣的秩定義是非零子式的線性無關行(列)向量的最大個數(shù),秩表示的是矩陣的信息量,A 矩陣的最大線性無關的行行數(shù)為1。
簡而言之,就是3 x 3的矩陣,可以使用1 x 3 的矩陣來表示。這就是LoRA微調(diào)的數(shù)學依據(jù)。
3)實驗數(shù)據(jù)證明
根據(jù)LoRA論文實驗的結(jié)果,LoRA微調(diào)使得GPT3 175B的訓練,顯存消耗從1.2TB降至350GB。

3、代碼實現(xiàn)
3.1 通過peft實現(xiàn)LoRA微調(diào)
源碼地址:
https://github.com/xujinhelaw/chat-bot-ananas/tree/master/llm-server/llm-finetune
項目結(jié)構(gòu)如下 :
chat-bot-ananas/ (根項目)
└── llm-server/ (大模型服務端模塊)
│ └── llm-server/ (大模型服務端模塊)
│ ├── alpaca_data.json(大模型微調(diào)訓練的數(shù)據(jù)集)
│ ├── environment.yml(大模型微調(diào)需要的依賴包)
│ ├── load_lora_model.py (啟動大模型并疊加微調(diào)參數(shù)的代碼邏輯)
│ ├── lora_finetune.py (大模型微調(diào)的代碼邏輯)
│ └── README.md (大模型微調(diào)模塊的README)
│ ├── api.py(大模型啟動和開發(fā)接口代碼)
│ ├── chatmachine.py(大模型訪問客戶端代碼)
│ ├── download.py(大模型下載代碼)
│ ├── environment.yml(大模型部署和訪問客戶端需要的依賴包)
└── pom.xml(后端依賴管理pom文件)
└── pom.xml (根 POM,管理子模塊)
└──settings.xml(maven倉配置文件)
通過peft實現(xiàn)LoRA的微調(diào),代碼如下所示:
# lora_finetune.py
import os
#os.environ["WANDB_PROJECT"] = "lora-finetune-demo" # Optional: 使用 wandb 記錄訓練
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import load_dataset, Dataset
import json
import torch
import warnings
import datetime
#transformers >= 4.37.0 廢棄了舊的梯度檢查點設置方式_set_gradient_checkpointing() 方法(Qwen 就是這么做的)
warnings.filterwarnings("ignore", message="You are using an old version of the checkpointing format")
# -------------------------------
# 1. 模型與 tokenizer 加載
# -------------------------------
model_path = "../qwen/Qwen-7B-Chat" # 可替換為你想微調(diào)的模型
#從 Hugging Face 的模型倉庫中加載與指定預訓練模型(model_path)對應的分詞器(Tokenizer)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
#將分詞器(tokenizer)的填充標記(pad token) 設置為與結(jié)束標記(eos token) 相同
# ?? 關鍵:打印原始狀態(tài)
print(f"Original eos_token: {tokenizer.eos_token}, eos_token_id: {tokenizer.eos_token_id}")
print(f"Original pad_token: {tokenizer.pad_token}, pad_token_id: {tokenizer.pad_token_id}")
# ? 使用 add_special_tokens 真正設置 pad_token
tokenizer.pad_token = '<|endoftext|>'
tokenizer.pad_token_id = 151643
# ? 再次驗證
print(f"? Final pad_token: {tokenizer.pad_token}")
print(f"? Final pad_token_id: {tokenizer.pad_token_id}")
print(f"? Final vocab size: {len(tokenizer)}")
tokenizer.padding_side = "right"
# 是否使用 4-bit 量化 (QLoRA)
use_4bit = True
if use_4bit:
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_path,
quantization_config=bnb_config,
device_map="auto", # 自動分配到 GPU
trust_remote_code=True
)
# 為量化模型準備:添加梯度檢查點和激活檢查
model = prepare_model_for_kbit_training(model)
else:
model = AutoModelForCausalLM.from_pretrained(
model_path,
device_map="auto",
torch_dtype=torch.bfloat16,
trust_remote_code=True
)
# ?? 打印所有包含 'proj' 的 nn.Linear 層名稱
print("?? Finding projection layers in Qwen2-7B:")
target_candidates = []
for name, module in model.named_modules():
if 'proj' in name and isinstance(module, torch.nn.Linear):
print(f" {name}")
target_candidates.append(name)
# 可選:提取最后一級名稱(如 q_proj, v_proj 等)
# 例如:從 'model.layers.0.self_attn.q_proj' 提取 'q_proj'
base_names = list(set([name.split('.')[-1] for name in target_candidates]))
print(f"\n?? Candidate target_modules: {base_names}")
# -------------------------------
# 2. 加載與預處理數(shù)據(jù)集
# -------------------------------
# 使用 Alpaca 風格的指令數(shù)據(jù)集(示例用 'tatsu-lab/alpaca'),格式如下
#{
# "instruction": "解釋為什么天空是藍色的",
# "input": "", # 無額外輸入時為空
# "output": "天空呈現(xiàn)藍色是因為瑞利散射現(xiàn)象..."
#}
#
# 讀取本地的 JSON 數(shù)據(jù)
data_path = "alpaca_data.json" # 替換為你自己的數(shù)據(jù)路徑
with open(data_path, "r", encoding="utf-8") as f:
train_datas = json.load(f)
# 將alpaca格式的數(shù)據(jù)轉(zhuǎn)為qwen的chattemplate格式
def convert_format(data_list):
converted_datas = []
for item in data_list:
# 構(gòu)建 user 的 content
user_content = item["instruction"]
if item["input"].strip(): # 檢查 input 是否非空(去除空格后)
user_content = f"{item['instruction']}\n\n{item['input']}"
# 或者根據(jù)語義調(diào)整順序,比如 input 是主要文本時:f"{item['input']}\n\n{item['instruction']}"
messages = [
{"role": "system", "content": "你是一個智能助手"},
{"role": "user", "content": user_content},
{"role": "assistant", "content": item["output"]}
]
converted_datas.append({"messages": messages})
return converted_datas
# 調(diào)用轉(zhuǎn)換函數(shù)
converted_datas = convert_format(train_datas)
def create_and_prepare_dataset(data_list):
"""
將原始數(shù)據(jù)列表轉(zhuǎn)換為 Hugging Face Dataset 格式,并應用聊天模板。
"""
def apply_chat_template(example):
messages = example["messages"]
# 使用分詞器的 apply_chat_template 方法將消息列表轉(zhuǎn)換為模型輸入格式
try:
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
except Exception as e:
print(f"Error applying chat template: {e}")
prompt = "" # 或者可以跳過這個樣本
print(f"打印 LoRA 訓練數(shù)據(jù)。 text: {prompt}")
return { "text": prompt }
# 創(chuàng)建 Dataset 對象
raw_dataset = Dataset.from_list(data_list)
# 應用模板函數(shù)到整個數(shù)據(jù)集
processed_dataset = raw_dataset.map(apply_chat_template)
return processed_dataset
# 應用聊天模板
dataset = create_and_prepare_dataset(converted_datas)
print(f"?? 打印 LoRA 訓練數(shù)據(jù)。dataset:{dataset}")
# Tokenize 函數(shù)
def tokenize_function(examples):
return tokenizer(
examples["text"],
padding=False,
max_length=512,
truncation=True,
return_tensors=None, # 返回 Python list,由 Trainer 處理
)
# 3. 處理數(shù)據(jù)集
tokenized_dataset = dataset.map(
tokenize_function,
batched=True,
remove_columns=[col for col in ["messages","text"] if col in dataset.column_names],
num_proc=4
)
print(f"?? 打印 處理后的數(shù)據(jù)集 tokenized_dataset :{tokenized_dataset}")
# 數(shù)據(jù)整理器(自動處理 padding)
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
# -------------------------------
# 3. 配置 LoRA
# -------------------------------
lora_config = LoraConfig(
r=8, # LoRA 秩
lora_alpha=16, # 超參
# c_attn 是 Qwen 中 QKV 投影的統(tǒng)一層,還有["c_attn", "c_proj", "w1", "w2"]
target_modules=["c_attn", "c_proj", "w1", "w2"],
#在低秩更新模塊中引入 10% 的隨機丟棄概率,
#避免模型過度依賴 LoRA 新增參數(shù)擬合訓練數(shù)據(jù)中的噪聲,提高對未見過數(shù)據(jù)的適配能力
lora_dropout=0.1,
#指定不對模型的偏置參數(shù)(bias)進行微調(diào)或修改
bias="none",
task_type="CAUSAL_LM" # 因果語言建模
)
#將原始預訓練模型與 LoRA(或 QLoRA)配置結(jié)合,生成一個支持參數(shù)高效微調(diào)的 PEFT 模型
print(f"\n?? 將原始預訓練模型與 LoRA(或 QLoRA)配置結(jié)合!這個過程比較耗時,請耐心等待!")
start_time = datetime.datetime.now()
model = get_peft_model(model, lora_config)
end_time = datetime.datetime.now()
cos_time = (end_time - start_time).seconds
print(f"原始預訓練模型與 LoRA(或 QLoRA)配置結(jié)合完成。耗時:{cos_time} 秒。")
model.print_trainable_parameters() # 查看可訓練參數(shù)量(通常 <1%)
# -------------------------------
# 4. 配置訓練參數(shù)
# -------------------------------
training_args = TrainingArguments(
output_dir="./lora-alpaca-qwen2", # 模型訓練結(jié)果( checkpoint、日志等 )的保存路徑
num_train_epochs=200, # 訓練的總輪數(shù),即完整遍歷訓練集的次數(shù)
per_device_train_batch_size=4, # 每個設備(如單張GPU)上的訓練批次大小
gradient_accumulation_steps=4, # 梯度累積步數(shù),每累積4個批次后再更新一次參數(shù)(變相增大總batch size)
learning_rate=2e-4, # 學習率,LoRA微調(diào)常用2e-4 ~ 5e-4
logging_steps=10, # 每訓練10步記錄一次日志(如損失值)
save_steps=100, # 每訓練500步保存一次模型 checkpoint
save_total_limit=2, # 最多保留2個最新的模型 checkpoint,避免占用過多存儲空間
fp16=False, # 不使用FP16混合精度訓練
bf16=torch.cuda.is_bf16_supported(), # 若GPU支持BF16精度則啟用(比FP16更穩(wěn)定,顯存占用相似)
optim="paged_adamw_8bit", # 使用8位量化的PagedAdamW優(yōu)化器(配合bitsandbytes庫,減少顯存占用)
lr_scheduler_type="cosine", # 學習率調(diào)度器類型,采用余弦退火策略(訓練后期自動降低學習率)
warmup_ratio=0.03, # 學習率預熱比例,前3%的訓練步數(shù)逐漸將學習率從0提升到設定值(穩(wěn)定訓練初期)
#report_to="wandb", # 訓練日志報告到Weights & Biases平臺(需提前安裝wandb并登錄)
disable_tqdm=False, # 不禁用tqdm進度條(顯示訓練進度)
gradient_checkpointing=True, # 啟用梯度檢查點(犧牲少量計算速度,大幅減少顯存占用)
)
# -------------------------------
# 5. 創(chuàng)建 Trainer 并開始訓練
# -------------------------------
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,# <<< 這里傳入了數(shù)據(jù)集!
data_collator=data_collator,
tokenizer=tokenizer,
)
print("?? 開始 LoRA 微調(diào)...")
trainer.train()
# -------------------------------
# 6. 保存 LoRA 適配器
# -------------------------------
model.save_pretrained("lora-alpaca-qwen2-finetuned")
tokenizer.save_pretrained("lora-alpaca-qwen2-finetuned")
print("? LoRA 微調(diào)完成,適配器已保存到 'lora-alpaca-qwen2-finetuned'")
3.2 執(zhí)行l(wèi)ora微調(diào)的腳本
python lora_finetune.py

3.3 啟動大模型并疊加lora微調(diào)的參數(shù)
# 返回llm-server的目錄,并執(zhí)行,這里的api.py做了判斷處理,如果有微調(diào)參數(shù),則直接疊加
python api.py

3.4 通過python實現(xiàn)的客戶端訪問大模型
重新開一個終端,啟動客戶端
# 因為是新開的終端,記得切到虛擬環(huán)境
# 返回llm-server的目錄,并執(zhí)行
source activate qwen
python chatmachine.py
4、LoRA微調(diào)效果

