大模型LLM(三)--大模型LoRA微調(diào)原理及實現(xiàn)(Qwen Peft)

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三個合并成新的權總矩陣,再進行計算。

重新訓練,只訓練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ù)學公式

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


實際訓練中數(shù)學公式

其中,r為LoRA秩,r越大信息越豐富,但計算量越大,α為超參

合并模型參數(shù)的數(shù)學公式表示如下:

合并模型參數(shù)的數(shù)學公式

加號前面的W0表示新知識,后面的△W表示舊知識

LoRA的訓練過程如下:


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。

LoRA和其余微調(diào)方法對比

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
LoRA 微調(diào)完成

3.3 啟動大模型并疊加lora微調(diào)的參數(shù)

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

3.4 通過python實現(xiàn)的客戶端訪問大模型

重新開一個終端,啟動客戶端

# 因為是新開的終端,記得切到虛擬環(huán)境
# 返回llm-server的目錄,并執(zhí)行
source activate qwen
python chatmachine.py

4、LoRA微調(diào)效果

訓練數(shù)據(jù)集的回答

實際大模型的回答

參考文章
[1] https://zhuanlan.zhihu.com/p/702629428

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容