從LLaMA-Factory項目認識微調

概述

什麼是LLaMA-Factory?

LLaMA-Factory是一個在github上開源的,專爲大模型訓練設計的平臺。項目提供中文說明,可以參考官方文檔:https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md

爲什麼要學習LLaMA-Factory?

大模型技術發展到現在,企業想要真正利用大模型做些事情,一定需要懂得大模型微調的過程。注意,這裏說的是過程,而不是原理,專業技術人員才需要懂原理,普通用戶只要懂過程就可以完成對大模型的微調。
對於有微調大模型需求,卻對大模型微調完全是一個門外漢的用戶來說,通過學習LLaMA-Factory後,可以快速的訓練出自己需要的模型。
對於想要了解微調大模型技術的技術人員,通過學習LLaMA-Factory後也能快速理解模型微調的相關概念。
所以,我認爲LLaMA-Factory是走向大模型微調的一條捷徑。

如何學習?

如果你只想瞭解如何利用LLaMA-Factory進行模型的微調,直接通過官方文檔即可完成。無需閱讀本文。
如果你想對大模型微調技術本身感興趣,想要深入瞭解,可以繼續閱讀本專欄,筆者將通過閱讀源碼的方式,對大模型微調技術進行深入剖析,看到哪裏,遇到不懂的概念再去理解,最終展現出大模型訓練的全貌。
理解了微調技術後,再通過使用LLaMA-Factory進行模型的微調實踐,即可掌握大模型微調技術。

基礎知識

閱讀源碼之前,我們需要對模型微調相關概念有一定的認識,來協助我們理解源碼。

模型訓練階段

在理解模型微調概念之前,我們先來理解大模型訓練階段有哪些。

Pre-Training

Pre-Training:預訓練階段。
這個階段是用來訓練基礎模型的,是最消耗算力的階段,也是大模型誕生的起始階段。

Supervised Finetuning(SFT)

sft:指令微調/監督微調階段
和預訓練階段相比,這個階段最大的變化就是訓練數據由"量多質低"變爲"量少質高",訓練數據主要由人工進行篩選或生成。這個階段完成後其實已經能獲得一個可以上線的大模型了

RLHF

RLHF:基於人類反饋的強化學習(Rainforcement Learning from Human Feedback,RLHF)
可以分成兩個環節

獎勵建模階段(Reward Modeling)

在這一階段,模型學習和輸出的內容發生了根本性的改變。前面的兩個階段,預訓練和微調,模型的輸出是符合預期的文本內容;獎勵建模階段的輸出不僅包含預測內容,還包含獎勵值或者說評分值,數值越高,意味着模型的預測結果越好。這個階段輸出的評分,並不是給最終的用戶,而是在強化學習階段發揮重大作用。

強化學習階段(Reinforcement Learning)

這個階段非常“聰明”的整合了前面的成果:

  • 針對特定的輸入文本,通過 SFT 模型獲得多個輸出文本。
  • 基於 RM 模型對多個輸出文本的質量進行打分,這個打分實際上已經符合人類的期望了。
  • 基於這個打分,爲多個輸出文本結果加入權重。這個權重其實會體現在每個輸出 Token 中。
  • 將加權結果反向傳播,對 SFT 模型參數進行調整,就是所謂的強化學習。

常見的強化學習策略包括PPODPO,它們的細節我們不去研究,只要知道DPO主要用於分佈式訓練,適合大規模並行處理的場景,PPO通常指的是單機上的算法就可以了。

模型訓練模式

瞭解了模型訓練階段後,現在有個問題,我們應該在哪個階段進行微調訓練呢?
通常會有以下訓練模式進行選擇,根據領域任務、領域樣本情況、業務的需求我們可以選擇合適的訓練模式。
模式一:基於base模型+領域任務的SFT;
模式二:基於base模型+領域數據 continue pre-train +領域任務SFT;
模式三:基於base模型+領域數據 continue pre-train +通用任務SFT+領域任務SFT;
模式四:基於base模型+領域數據 continue pre-train +通用任務與領域任務混合SFT;
模式五:基於base模型+領域數據 continue pre-train(混入SFT數據+通用任務與領域任務混合SFT;
模式六:基於chat模型+領域任務SFT;
模式七:基於chat模型+領域數據 continue pre-train +領域任務SFT

是否需要continue pre-train

大模型的知識來自於pre-train階段,如果你的領域任務數據集與pre-train的數據集差異較大,比如你的領域任務數據來自公司內部,pre-train訓練樣本基本不可能覆蓋到,那一定要進行continue pre-train。
如果你的領域任務數據量較大(token在1B以上),並只追求領域任務的效果,不考慮通用能力,建議進行continue pre-train。

是選擇chat模型 還是base模型

如果你有一個好的base模型,在base模型基礎進行領域數據的SFT與在chat模型上進行SFT,效果上差異不大。
基於chat模型進行領域SFT,會很容導致災難性遺忘,在進行領域任務SFT之後,模型通用能力會降低,如只追求領域任務的效果,則不用考慮。
如果你的領域任務與通用任務有很大的相關性,那這種二階段SFT會提升你的領域任務的效果。
如果你既追求領域任務的效果,並且希望通用能力不下降,建議選擇base模型作爲基座模型。在base模型上進行多任務混合訓練,混合訓練的時候需要關注各任務間的數據配比。

其他經驗

  • 在資源允許的情況下,如只考慮領域任務效果,我會選擇模式二;
  • 在資源允許的情況下,如考慮模型綜合能力,我會選擇模式五;
  • 在資源不允許的情況下,我會考慮模式六;
  • 一般情況下,我們不用進行RLHF微調;

開發工具庫Transformers

Transformers是Hugging Face提供的Python庫,Hugging Face是什麼這裏就不介紹了。國內可以通過鏡像站訪問:https://hf-mirror.com/
Transformers庫的文檔地址:https://hf-mirror.com/docs/transformers/index
我們要關注那些內容呢?下邊將會列舉一些關鍵內容,詳細內容請查閱官方文檔。

Pipeline

Pipeline是一個用於模型推理的工具,它與模型訓練關係不大,它主要是將預訓練好的模型加載,推理預測使用的,我們瞭解它是什麼即可。

AutoClass

AutoClass是一個比較重要的角色,主要是用來加載預訓練模型的,通過from_pretrained()方法可以加載任意Hugging Face中的預訓練模型和本地模型。

AutoTokenizer

幾乎所有的NLP任務都以tokenizer開始,用它來加載模型對應的分詞器。

AutoModel

真正來加載模型實例的是AutoModel,不同任務使用的AutoModel也不同,針對大語言模型一般使用AutoModelForCausalLM。

模型量化

量化技術專注於用較少的信息表示數據,同時儘量不損失太多準確性。
Transformers支持三種量化方法:AWQ、GPTQ、 BNB。底層細節我們不必研究
GPTQ是專爲GPT模型設計的,AWQ適用於多種模型和任務,包括多模態語言模型。
BNB是將模型量化爲8位和4位的最簡單選擇,4位量化可以與QLoRA一起用於微調量化LLM。

PEFT庫

PEFT是Hugging Face提供的庫,是一個爲大型預訓練模型提供多種高效微調方法的python庫。
PEFT文檔地址:https://hf-mirror.com/docs/peft/index
PEFT可以輕鬆與Transformers庫集成,一起完成模型微調的工作。
微調方式包括LoRA、AdaLoRA、P-tuning等。
補充說明:QLoRA是量化LoRA的縮寫,需要把模型量化再進行訓練,細節暫不研究。

LLaMA-Factory源碼分析

從pt預訓練開始

首先從分析pt預訓練過程開始研究。
根據官方文檔可知,預訓練執行命令如下:

CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \
    --stage pt \
    --do_train \
    --model_name_or_path path_to_llama_model \
    --dataset wiki_demo \
    --finetuning_type lora \
    --lora_target q_proj,v_proj \
    --output_dir path_to_pt_checkpoint \
    --overwrite_cache \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --lr_scheduler_type cosine \
    --logging_steps 10 \
    --save_steps 1000 \
    --learning_rate 5e-5 \
    --num_train_epochs 3.0 \
    --plot_loss \
    --fp16
參數說明
其中很多訓練參數可能會看不懂,但沒關係,先整體有個印象就行。

--stage pt:指定訓練階段爲預訓練
--do_train:指定是訓練任務
--model_name_or_path:本地模型的文件路徑或 Hugging Face 的模型標識符
--dataset:指定數據集
--finetuning_type lora:指定微調方法爲lora
--lora_target q_proj,v_proj:Lora作用模塊爲q_proj,v_proj 此參數後續詳解
--output_dir: 保存訓練結果的路徑
--overwrite_cache: 覆蓋緩存的訓練集和評估集
--per_device_train_batch_size 4: 每個gpu的批處理大小,訓練參數
--gradient_accumulation_steps 4:梯度累計的步數,訓練參數
--lr_scheduler_type cosine:學習率調度器,訓練參數
--logging_steps 10:每兩次日誌輸出間的更新步數,訓練參數
--save_steps 1000:每兩次斷點保存間的更新步數,訓練參數
--learning_rate 5e-5:學習率,adamW優化器的默認值爲5e-5,訓練參數
--num_train_epochs 3.0:需要執行的訓練輪數,訓練參數
--plot_loss:是否保存訓練損失曲線
--fp16:使用fp16混合精度訓練,此參數後續詳解

lora_target

lora_target被設置到LoraConfig中的target_modules參數中,LoraConfig是PEFT庫中提供的。
文檔地址:https://hf-mirror.com/docs/peft/v0.9.0/en/package_reference/lora#peft.LoraConfig
LLaMA-Factory框架中通過lora_target進行了封裝,說明如下:

    lora_target: str = field(
        default="all",
        metadata={
            "help": """Name(s) of target modules to apply LoRA. \
                    Use commas to separate multiple modules. \
                    Use "all" to specify all the linear modules. \
                    LLaMA choices: ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], \
                    BLOOM & Falcon & ChatGLM choices: ["query_key_value", "dense", "dense_h_to_4h", "dense_4h_to_h"], \
                    Baichuan choices: ["W_pack", "o_proj", "gate_proj", "up_proj", "down_proj"], \
                    Qwen choices: ["c_attn", "attn.c_proj", "w1", "w2", "mlp.c_proj"], \
                    InternLM2 choices: ["wqkv", "wo", "w1", "w2", "w3"], \
                    Others choices: the same as LLaMA."""
        },

目前細節我們還無法理解,但可以通過以上說明進行對應的設置。
注意:經調試結果觀察,Qwen1.5的lora_target與LLaMA choices相同。

混合精度訓練

在深度學習中,混合精度訓練是一種利用半精度浮點數(16位)和單精度浮點數(32位)混合計算的訓練技術。傳統上,神經網絡訓練過程中使用的是單精度浮點數,這需要更多的內存和計算資源。而混合精度訓練通過將一部分計算過程轉換爲半精度浮點數來減少內存佔用和加快計算速度。
FP16(半精度浮點數):FP16通常用於混合精度訓練,其中大多數計算操作使用FP16來減少內存佔用和加快計算速度。
BF16(bfloat16):BF16提供了更大的動態範圍和更好的數值精度,相比於FP16更適合於保持梯度更新的穩定性。
FP32(單精度浮點數):傳統神經網絡訓練中使用FP32來表示參數、梯度等數值,但相對於FP16和BF16,它需要更多的內存和計算資源。
Pure BF16(純 bfloat16):這個術語通常用於強調使用純粹的BF16格式,而不是在混合精度環境中與其他精度混合使用。使用純BF16意味着所有的計算和存儲都使用BF16格式。

理解源碼

請自行配合源碼閱讀以下內容,文中不會粘貼完整源碼。

源碼入口

根據運行命令,我們可以從src/train_bash.py部分看起。
會進入src/llmtuner/train/pt/workflow.py中的run_pt方法中,

run_pt函數主要實現了以下功能:
加載tokenizer和dataset:根據傳入的參數,使用load_tokenizer函數加載tokenizer,然後使用get_dataset函數根據tokenizer、model_args、data_args和training_args獲取dataset。
加載模型:使用load_model函數根據tokenizer、model_args、finetuning_args和training_args的do_train參數加載模型。
初始化Trainer:使用CustomTrainer初始化一個Trainer實例,傳入模型、參數、tokenizer、data_collator、callbacks和其他額外的參數。data_collator是通過調用DataCollatorForLanguageModeling類的實例化來創建一個數據整理器,主要用於自然語言處理任務中,將原始文本數據轉換爲模型可以輸入的格式。
訓練模型:如果training_args.do_train爲True,則調用trainer的train方法進行訓練,根據需要恢復checkpoint。訓練完成後,保存模型、日誌和狀態。
評估模型:如果training_args.do_eval爲True,則調用trainer的evaluate方法進行評估,並計算perplexity。然後記錄和保存評估指標。
創建模型卡片:使用create_modelcard_and_push函數創建並推送模型卡片,其中包含了模型的相關信息和訓練、評估結果。

開頭的代碼如下:

    # 獲取分詞器
    tokenizer = load_tokenizer(model_args)
    # 獲取數據集
    dataset = get_dataset(tokenizer, model_args, data_args, training_args, stage="pt")
    # 獲取模型實例
    model = load_model(tokenizer, model_args, finetuning_args, training_args.do_train)

接下來我們將分析一下以上三部分的代碼實現。

load_tokenizer

先來分析load_tokenizer方法:

def load_tokenizer(model_args: "ModelArguments") -> "PreTrainedTokenizer":
    r"""
    Loads pretrained tokenizer. Must before load_model.

    Note: including inplace operation of model_args.
    """
    try_download_model_from_ms(model_args)
    init_kwargs = _get_init_kwargs(model_args)
    # 核心方法在這,加載分詞器內容,具體參數含義先忽略
    tokenizer = AutoTokenizer.from_pretrained(
        model_args.model_name_or_path,
        use_fast=model_args.use_fast_tokenizer,
        split_special_tokens=model_args.split_special_tokens,
        padding_side="right",
        **init_kwargs,
    )
    patch_tokenizer(tokenizer)
    return tokenizer

get_dataset

比較核心的方法其實是get_dataset,因爲要訓練模型,最重要的部分是組織訓練數據。
函數主要執行以下操作:

  1. 根據提供的tokenizer、model_args、data_args和training_args參數,獲取數據集的模板並進行一些預處理。
  2. 檢查是否需要從緩存路徑加載數據集,如果是,則加載並返回數據集。
  3. 如果緩存路徑不存在,則根據data_args參數獲取數據集列表,並加載每個數據集。
  4. 將所有加載的數據集合併爲一個數據集。
  5. 對數據集進行預處理,包括使用tokenizer對數據進行編碼、根據指定的stage進行額外的預處理。
  6. 如果指定的cache_path不爲空,則將預處理後的數據集保存到cache_path路徑。
  7. 如果指定should_log參數,則打印數據集的一個樣本

這裏內容比較多,我們一步一步來分析。

獲取數據集模板

首先來分析一下獲取數據集模板在做什麼。可以進入以下路徑查看代碼。
src/llmtuner/data/template.py的get_template_and_fix_tokenizer方法。

函數主要做了以下幾件事情:

  1. 根據輸入的 name,獲取相應的模板,如果沒有提供 name 或提供的 name 不存在,則使用默認模板 "vanilla"。
  2. 如果模板指示需要替換 EOS(End of String)標記,且模板還指定了停用詞,則取出第一個停用詞在分詞器中替換 EOS 標記。,並將剩餘的停用詞保存起來。
  3. 如果分詞器中沒有定義 EOS 標記,則在分詞器中添加一個空EOS 標記"<|endoftext|>"。
  4. 如果分詞器中沒有定義 PAD 標記,則將 PAD 標記設置爲與 EOS 標記相同的值,並在日誌中記錄。
  5. 如果模板指定了停用詞,並且有剩餘的停用詞,則將這些停用詞添加到分詞器的特殊標記中,並在日誌中記錄。如果成功添加了新的特殊標記,則會發出警告提醒用戶確認是否需要調整詞彙表大小。
  6. 嘗試將模板轉換爲 Jinja 模板,並將其與分詞器相關聯。

最後返回選定的模板對象。

這段代碼內容有點多,我們先不考慮模板的事,先來理解一下分詞器中對應的概念。

概念理解

首先我們理解一下什麼是分詞器。

在自然語言處理(NLP)中,分詞器(tokenizer)是一個將文本輸入分割成單詞、子詞或符號序列的工具。這個過程稱爲分詞或者標記化。在 NLP 中,文本通常以字符串的形式存在,而計算機需要將其轉換成可以處理的結構化數據形式,例如單詞序列或標記序列,以便進行後續的語言處理任務,如詞嵌入、語言模型訓練、序列標註等。

那分詞器的EOS 標記, PAD 標記,停用詞分別代表什麼呢?

  1. EOS 標記(End of String):
    • EOS 標記通常用於表示序列的結束。在自然語言處理任務中,特別是序列到序列的任務(如機器翻譯、文本生成等),需要在序列的末尾添加 EOS 標記以指示句子的結束。這有助於模型正確處理不同長度的輸入序列。
    • 在分詞器中,EOS 標記用於標記化後的文本中表示句子結束的位置。
  2. PAD 標記(Padding):
    • PAD 標記通常用於對不同長度的序列進行填充,使它們具有相同的長度。這在很多深度學習模型中是必要的,因爲它們需要輸入具有固定長度的序列。通過將序列填充到相同的長度,可以方便地將它們組成一個批次進行並行處理,提高訓練效率。
    • 在分詞器中,PAD 標記用於填充序列,使其達到指定的最大長度。
  3. 停用詞:
    • 停用詞是在自然語言處理任務中通常會被忽略的常見詞語,因爲它們通常不攜帶太多的信息。在一些任務中,特別是文本生成任務,停用詞可能會影響模型的生成結果,因此需要在預處理階段將其去除。
    • 在分詞器中,停用詞通常被用作特殊的標記,例如 EOS 或 PAD 標記的替代。在一些情況下,停用詞可能還會被添加到詞彙表中作爲特殊標記,以便模型學習到如何處理它們。

至於Jinja模板,這裏就不過多介紹了,後邊我們會去查看源碼,看看在做什麼的。

獲取模板

理解了以上內容,我們回過頭來分析一下最開始的根據name獲取相應模板是怎麼做到的,它獲取到的模板到底是什麼。
通過閱讀源碼,我們可以看到,模板就是一個字典:

templates: Dict[str, Template] = {}

字典中的內容是通過_register_template方法註冊進去的。我們來分析一下_register_template方法.
方法的入參比較多,我們可以分析各個參數的作用如下:

  • name: 模板的名稱。
  • format_user: 用戶對話部分的格式化器。
  • format_assistant: AI 助手對話部分的格式化器。
  • format_system: 系統對話部分的格式化器。
  • format_function: 函數對話部分的格式化器。
  • format_observation: 觀察部分的格式化器。
  • format_tools: 工具部分的格式化器。
  • format_separator: 分隔符部分的格式化器。
  • default_system: 默認系統消息。
  • stop_words: 停用詞列表。
  • efficient_eos: 是否使用高效 EOS 標記。
  • replace_eos: 是否替換 EOS 標記。
  • force_system: 是否強制使用系統。

函數的主要工作是根據提供的參數創建對應的格式化器,並使用這些格式化器創建一個新的對話模板(Template 對象或 Llama2Template 對象),然後將該模板註冊到全局變量 templates 中。
注意,這裏的Template 對象或 Llama2Template 對象都是項目自定義的類。類中的內容我們先不看。
只要知道在初始化時,會調用此方法註冊模板即可。
以qwen模板爲例,在代碼中我們可以看到如下注冊模板的內容:

_register_template(
    name="qwen",
    format_user=StringFormatter(slots=["<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),
    format_system=StringFormatter(slots=["<|im_start|>system\n{{content}}<|im_end|>\n"]),
    format_separator=EmptyFormatter(slots=["\n"]),
    default_system="You are a helpful assistant.",
    stop_words=["<|im_end|>"],
    replace_eos=True,
)

根據入參,我們再重新查看_register_template方法的源碼(請自行查看源碼往下看):
首先,efficient_eos沒有傳參,默認值爲False,以至於eos_slots爲[],由此可以理解所謂的高效EOS標記,就是可能不需要額外的 EOS 標記,從而節省了內存和計算資源。
後續就是在實例化Template 對象返回。

轉換爲 Jinja 模板

接下來我們看一下轉換Jinja模板做了什麼。

該函數 _get_jinja_template 的作用是根據輸入的模板和分詞器生成一個 Jinja2 模板字符串。
首先,函數會判斷模板中是否設置了默認系統消息,如果有,則將該消息添加到 Jinja2 模板中。
接着,函數會檢查模板消息列表中是否存在系統消息,並將其內容賦值給變量 system_message。
然後,根據模板類型和是否強制顯示系統消息,將 system_message 變量添加到 Jinja2 模板中。
接下來,函數會遍歷模板消息列表,並根據消息的角色(用戶或助手)將相應的內容添加到 Jinja2 模板中。
最後,函數返回生成的 Jinja2 模板字符串。
在處理過程中,函數會使用 _convert_slots_to_jinja 函數將模板中的佔位符轉換爲對應的 Jinja2 表達式,並使用 PreTrainedTokenizer 對模板內容進行分詞處理。

可以看到,Jinja2模板中支持if else 和 for,不過這些都不重要,我們只要知道組織好模板後將模板賦值給了tokenizer的chat_template屬性即可。

獲取數據集列表

接下來就是獲取數據集列表的實現了。主要代碼如下:

    with training_args.main_process_first(desc="load dataset"):
        all_datasets = []
        for dataset_attr in get_dataset_list(data_args):
            all_datasets.append(load_single_dataset(dataset_attr, model_args, data_args))
        dataset = merge_dataset(all_datasets, data_args, training_args)

這裏先來關注get_dataset_list方法。

該函數用於獲取數據集列表。根據輸入的data_args參數中的dataset字段,將數據集名稱列表進行處理並保存。然後從data_args參數中的dataset_dir目錄下讀取數據集配置文件DATA_CONFIG,並解析其中的內容。實際文件路徑爲data/dataset_info.json。
接下來,根據配置文件中定義的數據集信息,創建並填充DatasetAttr對象,並將其添加到dataset_list列表中。最後,返回dataset_list列表。
在創建DatasetAttr對象時,根據配置文件中的不同字段,選擇不同的數據集類型和屬性,並設置相應的屬性值。
如果配置文件中定義了列名,則將其添加到DatasetAttr對象的屬性中。
如果數據集格式爲sharegpt,並且配置文件中定義了標籤信息,則將其添加到DatasetAttr對象的屬性中。
如果在讀取配置文件時發生異常,將拋出相應的異常。

現在我們知道get_dataset_list方法會返回數據集的一些元數據,load_single_dataset方法就會根據元數據來加載真正的數據了。

get_dataset_list根據給定的dataset_attr、model_args和data_args參數加載單個數據集。根據dataset_attr.load_from的值,函數從不同的來源加載數據集。支持的來源包括"Hugging Face Hub"、"ModelScope Hub"、腳本或文件。
當從"Hugging Face Hub"或"ModelScope Hub"加載數據集時,函數會使用相應的庫加載數據集。
當從腳本或文件加載數據集時,函數會根據文件類型選擇合適的方式加載數據。
函數還支持數據集的截斷和對齊操作。

其中加載數據到內容的代碼如下:

        dataset = load_dataset(
            path=data_path,
            name=data_name,
            data_dir=data_dir,
            data_files=data_files,
            split=data_args.split,
            cache_dir=model_args.cache_dir,
            token=model_args.hf_hub_token,
            streaming=(data_args.streaming and (dataset_attr.load_from != "file")),
            **kwargs,
        )

這部分使用的是Hugging Face的datasets庫來進行加載的。具體使用方法可以參考官網。
https://hf-mirror.com/docs/datasets/index
這裏就不介紹了。
後續使用align_dataset對數據集進行轉換後返回單個數據集的結果

align_dataset函數用於對給定的dataset進行格式轉換,使其符合指定的dataset_attr屬性要求。
該函數根據dataset_attr的格式要求選擇不同的轉換函數(convert_alpaca或convert_sharegpt),併爲轉換後的數據集定義了特定的特徵字典(features)。
轉換函數將對數據集中的每個樣本進行處理,重新組織其字段,並添加額外的"prompt"、"response"、"system"和"tools"字段。
處理過程中,可以選擇是否使用批處理,並可以指定並行處理的工作線程數、是否從緩存文件加載以及是否覆蓋緩存文件等參數。最終返回轉換後的數據集。

具體的轉換邏輯我們先不用看了,知道是轉換成一種方便訓練的格式就可以了。
後續預處理的邏輯我們也先不用看,大體瞭解會使用tokenizer對數據進行處理就可以了。

load_model

數據準備就緒,接下來就是加載模型了。

函數首先通過model_args.model_name_or_path使用AutoConfig獲取模型的配置和初始化參數,然後根據是否可訓練和是否使用unsloth選擇不同的模型加載方式。
如果可訓練且使用unsloth,則使用FastLanguageModel.from_pretrained加載模型;
否則,使用AutoModelForCausalLM.from_pretrained加載模型。
接着,函數會對模型進行一些修改和註冊,然後根據是否添加值頭(value head)來初始化或修改模型。
最後,函數將模型設置爲相應的模式(可訓練或不可訓練),並返回模型。
參數說明:
tokenizer: 預訓練的分詞器。
model_args: 模型參數,包括模型名稱、最大序列長度、計算數據類型等。
finetuning_args: 微調參數。
is_trainable: 模型是否可訓練,默認爲False。
add_valuehead: 是否添加值頭,默認爲False。
返回值:
model: 加載的預訓練模型。

其中比較核心的就是對模型進行的一些修改和註冊了。這部分代碼如下:

    patch_model(model, tokenizer, model_args, is_trainable)
    register_autoclass(config, model, tokenizer)
    model = init_adapter(model, model_args, finetuning_args, is_trainable)

接下來會進入內部瞭解一下具體對模型做了哪些修改。

概念理解

這裏我們看到了新的概念,unsloth。所以我們先來理解一下新概念。
通過觀察LLaMA-Factory的可視化頁面中高級設置,可以看到加速方式。加速方式包括flashattn和unsloth,那它們代表什麼呢?

"unsloth" 和 "flashattn" 是兩種不同的加速技術,通常用於優化神經網絡模型的推理速度。

  1. Unsloth: Unsloth 是一種基於量化的加速技術,它的主要思想是通過減少模型參數的精度來降低計算的複雜度,從而提高推理速度。在 Unsloth 中,通常會將模型參數從浮點數轉換爲低精度的整數或定點數。這樣可以降低模型的存儲需求,並且在推理時減少了浮點運算的開銷,從而加快了模型的推理速度。不過,由於參數精度的降低可能會帶來一定的精度損失,因此在選擇使用 Unsloth 技術時需要權衡推理速度和模型精度之間的關係。
  2. FlashAttn: FlashAttn 是一種用於加速注意力機制(attention mechanism)的技術。注意力機制在深度學習模型中廣泛應用於處理序列數據,例如機器翻譯、文本生成等任務。然而,由於注意力機制的計算量較大,它可能成爲模型推理速度的瓶頸。FlashAttn 通過優化注意力計算的方式來加速模型推理過程。具體來說,FlashAttn 可能採用一些技巧,例如降低注意力矩陣的計算複雜度、減少注意力頭的數量、或者採用特定的注意力結構等。這些技巧可以有效地減少模型推理時的計算量,從而加速推理速度。

簡單來說,Unsloth 和 FlashAttn 都是用於加速神經網絡模型推理過程的技術,但它們的具體實現和優化方式有所不同,適用於不同類型的模型和應用場景。

patch_model

patch_model函數用於根據不同的模型類型和參數,對模型和分詞器進行一系列的修改和配置。
具體包括以下幾個方面:

如果模型的generate方法不是GenerationMixin的子類,則將其替換爲PreTrainedModel.generate方法。
如果模型配置中的model_type爲"chatglm",則設置模型的lm_head爲transformer.output_layer,並設置保存模型時忽略lm_head.weight。(chatglm需要一些特殊處理,我們暫不關心)
如果model_args.resize_vocab爲True,則調用_resize_embedding_layer函數來調整嵌入層的大小。
如果模型是可訓練的,則調用_prepare_model_for_training函數對模型進行訓練前的準備。
如果模型配置中的model_type爲"mixtral"且啓用了DeepSpeed的Zero3優化器,則導入set_z3_leaf_modules和MixtralSparseMoeBlock,並調用set_z3_leaf_modules函數將model中的葉子模塊設置爲MixtralSparseMoeBlock。如果模型是可訓練的,則調用patch_mixtral_replace_moe_impl函數。
嘗試向模型添加標籤"llama-factory",如果添加失敗則打印警告信息。
這些修改和配置的目的是爲了適應不同模型的需求,提高模型的性能和效率。

我們如果使用qwen模型,主要需要觀察_prepare_model_for_training函數對模型做了哪些準備。

該函數主要爲模型訓練做準備,具體包括以下操作:
如果model_args.upcast_layernorm爲True,則將模型中的層歸一化(layernorm)權重轉換爲float32類型。
如果model_args.disable_gradient_checkpointing爲False且模型支持梯度檢查點(gradient checkpointing),則啓用梯度檢查點,並設置相關屬性。
如果模型具有output_layer_name屬性且model_args.upcast_lmhead_output爲True,則將語言模型頭(lm_head)的輸出轉換爲float32類型

這裏的概念可能不是太懂,可以先了解個大概即可。

register_autoclass

這個方法的代碼如下:

def register_autoclass(config: "PretrainedConfig", model: "PreTrainedModel", tokenizer: "PreTrainedTokenizer"):
    if "AutoConfig" in getattr(config, "auto_map", {}):
        config.__class__.register_for_auto_class()
    if "AutoModelForCausalLM" in getattr(config, "auto_map", {}):
        model.__class__.register_for_auto_class()
    if "AutoTokenizer" in tokenizer.init_kwargs.get("auto_map", {}):
        tokenizer.__class__.register_for_auto_class()

就是字面意思,註冊Transformers框架中的自動類,具體用處目前還不明確。

init_adapter

init_adapter函數用於初始化適配器,並支持全參數、凍結和LoRA訓練。根據傳入的模型、模型參數、微調參數和是否可訓練,該函數將根據微調類型對模型進行相應的處理。此方法屬於比較核心的方法

如果模型不可訓練且沒有指定適配器名稱路徑,則加載基本模型。
如果微調類型爲"full"且模型可訓練,則將模型參數轉換爲float32類型。
如果微調類型爲"freeze"且模型可訓練,則根據num_layer_trainable和其他參數來確定可訓練的層,並將其他層的參數設置爲不可訓練。可訓練的層可以是最後n層、前面n層或指定的層。
如果微調類型爲"lora",則根據是否指定適配器名稱路徑和其他參數來加載、合併和恢復LoRA模型,並創建新的LoRA權重。
最終,該函數返回處理後的模型

這部分代碼內容還是比較多的,full和freeze我們不用關注,重點關注lora部分。由於這部分比較重要,我把lora部分代碼放到下邊,並用註釋解釋一下:

    if finetuning_args.finetuning_type == "lora":
        logger.info("Fine-tuning method: {}".format("DoRA" if finetuning_args.use_dora else "LoRA"))
        adapter_to_resume = None
        # 這部分是可以通過adapter_name_or_path路徑,來進行進行增量的訓練,增量邏輯我們可以先不看,代碼沒有放到這裏
        if model_args.adapter_name_or_path is not None:...
        # 重點內容在這裏
        if is_trainable and adapter_to_resume is None:  # create new lora weights while training
            if len(finetuning_args.lora_target) == 1 and finetuning_args.lora_target[0] == "all":
                # 通過調試,可以在這裏看到模型所有的lora_target
                target_modules = find_all_linear_modules(model)
            else:
                target_modules = finetuning_args.lora_target
            # 這裏通過可視化頁面,可以看到解釋:僅訓練塊擴展後的參數。細節我們先不看
            if finetuning_args.use_llama_pro:
                target_modules = find_expanded_modules(model, target_modules, finetuning_args.num_layer_trainable)
            # 這裏驗證了使用dora的時候,如果使用了量化,必須是使用BNB方式,否則不支持
            if finetuning_args.use_dora and getattr(model, "quantization_method", None) is not None:
                if getattr(model, "quantization_method", None) != QuantizationMethod.BITS_AND_BYTES:
                    raise ValueError("DoRA is not compatible with PTQ-quantized models.")

            peft_kwargs = {
                "r": finetuning_args.lora_rank,
                "target_modules": target_modules,
                "lora_alpha": finetuning_args.lora_alpha,
                "lora_dropout": finetuning_args.lora_dropout,
                "use_rslora": finetuning_args.use_rslora,
            }
            # 這裏使用了unsloth加速,在之前的章節中有講到
            if model_args.use_unsloth:
                from unsloth import FastLanguageModel  # type: ignore

                unsloth_peft_kwargs = {"model": model, "max_seq_length": model_args.model_max_length}
                model = FastLanguageModel.get_peft_model(**peft_kwargs, **unsloth_peft_kwargs)
            else:
                # 組織LoraConfig
                lora_config = LoraConfig(
                    task_type=TaskType.CAUSAL_LM,
                    inference_mode=False,
                    modules_to_save=finetuning_args.additional_target,
                    use_dora=finetuning_args.use_dora,
                    **peft_kwargs,
                )
                # 加載模型
                model = get_peft_model(model, lora_config)
        # 這裏的pure_bf16在前邊章節頁講過,混合精度訓練的一種模式
        if not finetuning_args.pure_bf16:
            for param in filter(lambda p: p.requires_grad, model.parameters()):
                param.data = param.data.to(torch.float32)

通過閱讀以上源碼和註釋,想要更好的理解,需要解決下邊的問題。

  1. lora_target應該怎麼設置比較合適?
  2. dora是什麼?
  3. LoraConfig應該怎麼配置更合適?

想要解決這些問題,我們應該去了解一下LoraConfig。

解讀LoraConfig

首先,LoraConfig屬於PEFT庫。所以可以閱讀一下官方文檔,來理解一下Lora,地址如下:
https://hf-mirror.com/docs/peft/developer_guides/lora
通過閱讀這部分文檔,我們可以對Lora整體有了一個認識。
之後我們可以閱讀這部分內容,來理解一下LoraConfig中每個參數的作用。
https://hf-mirror.com/docs/peft/package_reference/lora
回到我們自己的代碼中,現在可以解釋一下這部分代碼具體的含義了:

            peft_kwargs = {
                "r": finetuning_args.lora_rank,
                "target_modules": target_modules,
                "lora_alpha": finetuning_args.lora_alpha,
                "lora_dropout": finetuning_args.lora_dropout,
                "use_rslora": finetuning_args.use_rslora,
            }
            lora_config = LoraConfig(
                task_type=TaskType.CAUSAL_LM,
                inference_mode=False,
                modules_to_save=finetuning_args.additional_target,
                use_dora=finetuning_args.use_dora,
                **peft_kwargs,
            )
task_type:此參數不是LoraConfig的參數,而是它的父類PeftConfig的參數,可選值爲TaskType中的值,具體的含義是什麼呢?我們可以直接看源碼:
class TaskType(str, enum.Enum):
    """
    Enum class for the different types of tasks supported by PEFT.

    Overview of the supported task types:
    - SEQ_CLS: Text classification.
    - SEQ_2_SEQ_LM: Sequence-to-sequence language modeling.
    - CAUSAL_LM: Causal language modeling.
    - TOKEN_CLS: Token classification.
    - QUESTION_ANS: Question answering.
    - FEATURE_EXTRACTION: Feature extraction. Provides the hidden states which can be used as embeddings or features
      for downstream tasks.
    """

    SEQ_CLS = "SEQ_CLS"
    SEQ_2_SEQ_LM = "SEQ_2_SEQ_LM"
    CAUSAL_LM = "CAUSAL_LM"
    TOKEN_CLS = "TOKEN_CLS"
    QUESTION_ANS = "QUESTION_ANS"
    FEATURE_EXTRACTION = "FEATURE_EXTRACTION"

可以看到,其實就是在指定任務類型是大語言模型。
inference_mode:此參數也是父類PeftConfig的參數,表示模型是否是推理模型,由於我們要進行訓練,所以設置爲False
modules_to_save:除 LoRA 層以外的可訓練模塊名稱,我們先不用管這裏。
use_dora:使用權重分解的 LoRA。
r:lora微調的維度,我們默認設置的是8。
target_modules:就是要微調的模塊,前邊已經有介紹。
lora_alpha:LoRA微調的縮放因子,默認爲r * 2。
lora_dropout: LoRA微調的隨機丟棄率。瞭解過深度學習的一定可以理解這個指標。
use_rslora:是否使用LoRA層的秩穩定縮放因子,閱讀官網可以理解爲:將適配器的縮放因子設置爲 lora_alpha/math.sqrt(r) ,因爲它被證明工作得更好。 否則,它將使用原始的默認值 lora_alpha/r 。
至此,目前我們已經理解了項目中使用到的參數。
其他內容可根據官方文檔理解。

模型訓練部分

前邊的內容只是訓練的前提,接下來我們就來看一下訓練部分的實現。
我們回到src/llmtuner/train/pt/workflow.py中的run_pt方法中繼續往下看。這裏我把核心源碼直接放到下邊。

    # 部分主要是對數據轉換,轉換成模型可以輸入的格式。
    data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

    # Initialize our Trainer
    trainer = CustomTrainer(
        model=model,
        args=training_args,
        finetuning_args=finetuning_args,
        tokenizer=tokenizer,
        data_collator=data_collator,
        callbacks=callbacks,
        #就是數據集的拆分(訓練集/測試集)
        **split_dataset(dataset, data_args, training_args), 
    )

    # Training
    if training_args.do_train:
        # 開始訓練,resume_from_checkpoint可以是字符串或布爾值,如果爲字符串,則是本地保存的檢查點的路徑,如果爲布爾值且爲True,則加載args.output_dir中由之前的[Trainer]實例保存的最後一個檢查點。如果存在,則從加載的模型/優化器/調度器狀態繼續訓練。
        train_result = trainer.train(resume_from_checkpoint=training_args.resume_from_checkpoint)
        # 保存模型
        trainer.save_model()
        # 記錄指標日誌
        trainer.log_metrics("train", train_result.metrics)
        # 保存指標日誌
        trainer.save_metrics("train", train_result.metrics)
        # 保存訓練狀態
        trainer.save_state()
        # 如果是主進程,且plot_loss參數爲True,則保存損失曲線圖
        if trainer.is_world_process_zero() and finetuning_args.plot_loss:
            plot_loss(training_args.output_dir, keys=["loss", "eval_loss"])

首先我們先來理解一下DataCollatorForLanguageModeling,可以閱讀官網文檔進行理解,地址如下:
https://hf-mirror.com/docs/transformers/v4.39.1/zh/main_classes/data_collator
簡單來說就是轉換成模型可以輸入的格式。
CustomTrainer是一個比較重要的內容,接下來我們將對它進行解讀。

解讀CustomTrainer

CustomTrainer繼承自Trainer類,並且重寫了create_optimizer_and_scheduler方法。

create_optimizer_and_scheduler方法用於創建自定義的優化器和學習率調度器。它首先調用create_custom_optimzer函數來創建優化器,該函數根據模型、參數和finetuning_args來決定如何創建優化器。如果create_custom_optimzer返回None,則調用Trainer類的create_optimizer方法來創建優化器。

我們可以看一下create_custom_optimzer的底層實現,代碼內容不多,直接放到下邊:

def create_custom_optimzer(
    model: "PreTrainedModel",
    training_args: "Seq2SeqTrainingArguments",
    finetuning_args: "FinetuningArguments",
    max_steps: int,
) -> Optional["torch.optim.Optimizer"]:
    if finetuning_args.use_galore:
        return _create_galore_optimizer(model, training_args, finetuning_args, max_steps)

    if finetuning_args.loraplus_lr_ratio is not None:
        return _create_loraplus_optimizer(model, training_args, finetuning_args)

如何創建自定義優化器我們先不研究,目前我們還用不到。
要研究的重點其實是Trainer類,Trainer是Transformers庫中比較重要的一部分。可以閱讀官方文檔進行了解:https://hf-mirror.com/docs/transformers/v4.39.1/zh/main_classes/trainer
接下來我們分析一下代碼中傳入的參數的含義:

model:這個不用太解釋,就是傳入之前加載的模型
args=training_args:這個training_args實際上是Transformers庫中的Seq2SeqTrainingArguments,這其中包含的參數就比較多了。
finetuning_args:這個參數是自定義的參數,我們不關注。
tokenizer、data_collator:這兩個參數不再解釋,你應該也能看懂了。
callbacks:指定回調函數,LLaMA-Factory指定了一個打印日誌的回調函數。
split_dataset:這是自定義的函數,用來做數據拆分,實現細節我們先不看,主要就是返回split_dataset參數和eval_dataset參數,分別用來表示訓練的數據集和評估的數據集,是Trainer類自帶的參數。

至此,對CustomTrainer我們已經有了整體的認識。

解讀Seq2SeqTrainingArguments

爲了更進一步的瞭解訓練參數,我們可以看一下Seq2SeqTrainingArguments中都有哪些參數。官方文檔地址如下:
https://hf-mirror.com/docs/transformers/v4.39.1/en/main_classes/trainer#transformers.Seq2SeqTrainingArguments
由於參數太多了,就不挨個參數解讀了,只解釋一些我們常用到的參數,其他參數詳見官方文檔:

output_dir :輸出目錄,將寫入模型預測和檢查點。
overwrite_output_dir:如果設置爲 True,將覆蓋輸出目錄中的內容。可以在 output_dir 指向檢查點目錄時使用此選項繼續訓練。
do_train :是否進行訓練。這個參數不是直接由 [Trainer] 使用的
do_eval:是否在驗證集上運行評估。如果 evaluation_strategy 不是 "no" ,將被設置爲 True。這個參數不是直接由 [Trainer] 使用的
do_predict:是否在測試集上進行預測。這個參數不是直接由 [Trainer] 使用的
evaluation_strategy :訓練過程中採用的評估策略。可能的取值有:
"no": 不進行評估。
"steps": 每隔 eval_steps 進行評估。
"epoch": 每個 epoch 結束時進行評估
per_device_train_batch_size (int,可選,默認爲 8):每個 GPU/TPU 核心/CPU 的訓練批次大小。
per_device_eval_batch_size (int,可選,默認爲 8):每個 GPU/TPU 核心/CPU 的評估批次大小。
gradient_accumulation_steps (int,可選,默認爲 1):在執行反向傳播/更新步驟之前,累積梯度的更新步驟數。

模型評估部分

直接看剩餘部分的代碼,理解了之前訓練部分的代碼,評估部分代碼很容易就能看懂。

    # Evaluation
    if training_args.do_eval:
        # 評估
        metrics = trainer.evaluate(metric_key_prefix="eval")
        try:
            # 計算困惑度,困惑度是自然語言處理領域常用的評價模型生成或預測文本的能力的指標,它是損失函數指數運算的結果。越低代表模型越好。
            perplexity = math.exp(metrics["eval_loss"])
        except OverflowError:
            perplexity = float("inf")
        
        metrics["perplexity"] = perplexity
        trainer.log_metrics("eval", metrics)
        trainer.save_metrics("eval", metrics)

總結

至此,我們已經打通了pt預訓練這條通道,接下來我們就要開始查看sft指令微調部分的實現了。

sft指令微調

首先我們還是查看官方文檔提供的sft腳本,內容如下:

CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \
    --stage sft \
    --do_train \
    --model_name_or_path path_to_llama_model \
    --dataset alpaca_gpt4_zh \
    --template default \
    --finetuning_type lora \
    --lora_target q_proj,v_proj \
    --output_dir path_to_sft_checkpoint \
    --overwrite_cache \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --lr_scheduler_type cosine \
    --logging_steps 10 \
    --save_steps 1000 \
    --learning_rate 5e-5 \
    --num_train_epochs 3.0 \
    --plot_loss \
    --fp16

可以發現,只有--stage被設置成了sft,其他參數之前已經介紹過了。
所以我們直接開始查看源碼。

理解源碼

有了之前的經驗,源碼入口可以很快找到。
即src/llmtuner/train/sft/workflow.py中的run_sft方法中。這裏我直接把完整代碼放到下邊:

def run_sft(
    model_args: "ModelArguments",
    data_args: "DataArguments",
    training_args: "Seq2SeqTrainingArguments",
    finetuning_args: "FinetuningArguments",
    generating_args: "GeneratingArguments",
    callbacks: Optional[List["TrainerCallback"]] = None,
):
    tokenizer = load_tokenizer(model_args)
    # 數據預處理部分有變化,後期可以進入查看一下
    dataset = get_dataset(tokenizer, model_args, data_args, training_args, stage="sft")
    model = load_model(tokenizer, model_args, finetuning_args, training_args.do_train)

    if training_args.predict_with_generate:
        tokenizer.padding_side = "left"  # use left-padding in generation

    if getattr(model, "is_quantized", False) and not training_args.do_train:
        setattr(model, "_hf_peft_config_loaded", True)  # hack here: make model compatible with prediction
  
    data_collator = DataCollatorForSeq2Seq(
        tokenizer=tokenizer,
        pad_to_multiple_of=8 if tokenizer.padding_side == "right" else None,  # for shift short attention
        label_pad_token_id=IGNORE_INDEX if data_args.ignore_pad_token_for_loss else tokenizer.pad_token_id,
    )

    # Override the decoding parameters of Seq2SeqTrainer
    training_args.generation_max_length = training_args.generation_max_length or data_args.cutoff_len
    training_args.generation_num_beams = data_args.eval_num_beams or training_args.generation_num_beams

    # Initialize our Trainer
    # trainer使用了CustomSeq2SeqTrainer,這是一個比較大的變化
    trainer = CustomSeq2SeqTrainer(
        model=model,
        args=training_args,
        finetuning_args=finetuning_args,
        tokenizer=tokenizer,
        data_collator=data_collator,
        callbacks=callbacks,
        compute_metrics=ComputeMetrics(tokenizer) if training_args.predict_with_generate else None,
        **split_dataset(dataset, data_args, training_args),
    )

    # Keyword arguments for `model.generate`
    gen_kwargs = generating_args.to_dict()
    gen_kwargs["eos_token_id"] = [tokenizer.eos_token_id] + tokenizer.additional_special_tokens_ids
    gen_kwargs["pad_token_id"] = tokenizer.pad_token_id
    gen_kwargs["logits_processor"] = get_logits_processor()

    # Training
    if training_args.do_train:
        train_result = trainer.train(resume_from_checkpoint=training_args.resume_from_checkpoint)
        trainer.save_model()
        trainer.log_metrics("train", train_result.metrics)
        trainer.save_metrics("train", train_result.metrics)
        trainer.save_state()
        if trainer.is_world_process_zero() and finetuning_args.plot_loss:
            plot_loss(training_args.output_dir, keys=["loss", "eval_loss"])

    # Evaluation
    if training_args.do_eval:
        metrics = trainer.evaluate(metric_key_prefix="eval", **gen_kwargs)
        if training_args.predict_with_generate:  # eval_loss will be wrong if predict_with_generate is enabled
            metrics.pop("eval_loss", None)
        trainer.log_metrics("eval", metrics)
        trainer.save_metrics("eval", metrics)

    # Predict
    # 多了一個預測推理階段,基本過程都是一樣的,只不過調用了trainer.predict方法
    if training_args.do_predict:
        predict_results = trainer.predict(dataset, metric_key_prefix="predict", **gen_kwargs)
        if training_args.predict_with_generate:  # predict_loss will be wrong if predict_with_generate is enabled
            predict_results.metrics.pop("predict_loss", None)
        trainer.log_metrics("predict", predict_results.metrics)
        trainer.save_metrics("predict", predict_results.metrics)
        trainer.save_predictions(predict_results)

    # Create model card
    create_modelcard_and_push(trainer, model_args, data_args, training_args, finetuning_args)

發現了什麼?
沒錯,代碼結構基本與之前的預訓練部分差不多。
只有在get_dataset處理數據部分,會有所差異,具體差異我們暫時不看,只要知道sft階段數據預處理時,是需要增加指令、角色信息的就可以了。
實際上,如果選擇使用LLaMA-Factory進行微調,我們按照LLaMA-Factory的數據集格式要求準備數據就可以了。

解讀CustomSeq2SeqTrainer

這階段除了預處理數據部分有差別,最大的差別就是訓練器與之前不同,使用的是CustomSeq2SeqTrainer,
CustomSeq2SeqTrainer是Seq2SeqTrainer的子類,而Seq2SeqTrainer是Trainer的子類。
Seq2SeqTrainer的源碼我們就不去仔細閱讀了,簡單閱讀後發現,Seq2SeqTrainer主要是重寫了Trainer的評估和推理的方法,沒有重寫訓練方法,所以與使用Trainer進行訓練應該差別不大。
這裏爲什麼使用Seq2SeqTrainer,我們不必糾結。
閱讀qwen1.5官方提供的sft示例,可以看到示例中使用的也是Trainer而不是Seq2SeqTrainer。
示例地址:https://github.com/QwenLM/Qwen1.5/blob/main/examples/sft/finetune.py

總結

可以發現,理解了pt階段後,再來理解sft階段其實是很容易的,一通百通,sft階段我們就介紹到這裏,如果你對哪部分感興趣,可以自己去閱讀源碼,相信有了pt階段的知識儲備後,你可以很容易的閱讀源碼了。
另外,我們只要懂得pt與sft微調,就能實際上手微調了。所以後續的RLHF階段就先不去查看了。

微調實踐

我們已經理解了大模型微調的基本過程,但實踐是檢驗真理的唯一標準,所以接下來將與大家一起對大模型微調進行實踐,觀察一下微調效果。
注意:UI界面的使用請閱讀官方文檔,這裏不會介紹UI如何使用。
https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md

數據集準備

根據LLaMA-Factory官方提供的數據準備文檔,可以看到訓練數據的格式。地址如下:
https://github.com/hiyouga/LLaMA-Factory/blob/main/data/README_zh.md
文檔中比較重要的說明部分:

對於預訓練數據集,僅 prompt 列中的內容會用於模型訓練。
對於偏好數據集,response 列應當是一個長度爲 2 的字符串列表,排在前面的代表更優的回答

偏好數據集是用在獎勵建模階段的。

本次微調選擇了開源項目數據集,地址如下:
https://github.com/KMnO4-zx/huanhuan-chat/blob/master/dataset/train/lora/huanhuan.json
下載後,將json文件存放到LLaMA-Factory的data目錄下。
修改dataset_info.json 文件。
直接增加以下內容即可:

  "huanhuan": {
    "file_name": "huanhuan.json"
  }

添加後,在UI頁面中直接可以看到新增加的數據集:
image.png

開始微調

爲了減少資源消耗,本次選擇測試的模型是Qwen1.5-0.5B-Chat。
通過可視化頁面配置後,可以得到微調命令如下:

CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \
    --stage sft \
    --do_train True \
    --model_name_or_path /home/jqxxuser/model/Qwen1.5-0.5B-Chat \
    --finetuning_type lora \
    --template qwen \
    --dataset_dir data \
    --dataset huanhuan \
    --cutoff_len 1024 \
    --learning_rate 5e-05 \
    --num_train_epochs 2.0 \
    --max_samples 100000 \
    --per_device_train_batch_size 2 \
    --gradient_accumulation_steps 8 \
    --lr_scheduler_type cosine \
    --max_grad_norm 1.0 \
    --logging_steps 5 \
    --save_steps 100 \
    --warmup_steps 0 \
    --optim adamw_torch \
    --output_dir saves/Qwen1.5-0.5B-Chat/lora/train_2024-03-28-10-54-09 \
    --fp16 True \
    --lora_rank 8 \
    --lora_alpha 16 \
    --lora_dropout 0.1 \
    --lora_target q_proj,v_proj \
    --plot_loss True

說明:SFT階段是最常用訓練階段,所以我們在SFT階段進行微調,測試效果。
我們直接在UI中點擊開始即可進行微調,可以觀察到整個的過程,發現損失值在逐漸降低。
image.png
等待訓練完畢即可。

測試聊天效果

刷新適配器,可以看到我們剛剛訓練完的模型,選擇即可
image.png
選擇Chat功能,加載模型後即可開始聊天。
image.png
看看效果吧:
image.png
目測效果還可以,至少我們看到模型確實發生了改變。

評估模型

通過聊天觀察效果是一種直觀的感覺,我們可以在通過項目自帶的評估功能做一下評估。
注意,如果報錯,需要確保安裝了以下庫。
jieba
rouge-chinese
nltk

image.png

運行後可以在目錄中看到評估指標:

{
    "predict_bleu-4": 2.487403191204076,
    "predict_rouge-1": 16.790678761061947,
    "predict_rouge-2": 1.1607781979082865,
    "predict_rouge-l": 14.878193322606597,
    "predict_runtime": 900.9563,
    "predict_samples_per_second": 4.139,
    "predict_steps_per_second": 1.38
}

這些指標應該怎麼看呢?首先我們來了解一下這些指標的概念。

  1. predict_bleu-4:
    • BLEU(Bilingual Evaluation Understudy)是一種常用的用於評估機器翻譯質量的指標。
    • BLEU-4 表示四元語法 BLEU 分數,它衡量模型生成文本與參考文本之間的 n-gram 匹配程度,其中 n=4。
    • 值越高表示生成的文本與參考文本越相似,最大值爲 100。
  2. predict_rouge-1 和 predict_rouge-2:
    • ROUGE(Recall-Oriented Understudy for Gisting Evaluation)是一種用於評估自動摘要和文本生成模型性能的指標。
    • ROUGE-1 表示一元 ROUGE 分數,ROUGE-2 表示二元 ROUGE 分數,分別衡量模型生成文本與參考文本之間的單個詞和雙詞序列的匹配程度。
    • 值越高表示生成的文本與參考文本越相似,最大值爲 100。
  3. predict_rouge-l:
    • ROUGE-L 衡量模型生成文本與參考文本之間最長公共子序列(Longest Common Subsequence)的匹配程度。
    • 值越高表示生成的文本與參考文本越相似,最大值爲 100。
  4. predict_runtime:
    • 預測運行時間,表示模型生成一批樣本所花費的總時間。
    • 單位通常爲秒。
  5. predict_samples_per_second:
    • 每秒生成的樣本數量,表示模型每秒鐘能夠生成的樣本數量。
    • 通常用於評估模型的推理速度。
  6. predict_steps_per_second:
    • 每秒執行的步驟數量,表示模型每秒鐘能夠執行的步驟數量。
    • 對於生成模型,一般指的是每秒鐘執行生成操作的次數。

所以,單獨看指標數據的話,模型的效果並不是太好,這就需要大家自行摸索如何讓模型能力更好了。

導出模型

切換到Export選項卡,指定導出目錄,點擊開始導出,即可導出模型。
image.png
導出後的模型與其他大模型的使用方法一致。

總結

至此,我們的一次模型微調的嘗試就完成了。
可以發現,使用LLaMA-Factory進行微調基本上可以做到傻瓜式操作了。最大的工作量還是在組織訓練數據這個階段。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章