基礎(chǔ):從數(shù)據(jù)準(zhǔn)備到LoRA實戰(zhàn)與調(diào)優(yōu)全流程)
這幾年大模型微調(diào)基本成了算法工程師的一個常規(guī)動作。無論你是想做一個行業(yè)問答助手還是想讓模型按固定格式輸出結(jié)構(gòu)化信息最后大概率都會走到這一步拿一個開源底座模型用自己攢的數(shù)據(jù)重新訓(xùn)練一下。這篇文章把“大模型微調(diào)基礎(chǔ)”這條路完整走一遍從要不要調(diào)、調(diào)什么、怎么準(zhǔn)備數(shù)據(jù)到訓(xùn)練參數(shù)怎么設(shè)置、碰到loss不降怎么辦、調(diào)完怎么評估和部署全部過一遍。適合剛接觸微調(diào)的同學(xué)也適合已經(jīng)跑通過一次、但感覺還差點細(xì)節(jié)的開發(fā)者。全文按實際操作順序講不繞彎子。1. 先想清楚你真的需要微調(diào)嗎1.1 微調(diào)到底解決什么問題大模型預(yù)訓(xùn)練階段做的事情說白了就是在海量文本上“把下一個詞預(yù)測準(zhǔn)”。模型學(xué)到的是通用語言規(guī)律和大量世界知識像一個上了很多年學(xué)、知識面很廣的人。但這個人沒有專門練過你手頭那件事比如寫某種格式的巡檢報告、把客服對話歸類到固定標(biāo)簽、用特定口吻回答醫(yī)療問題。微調(diào)做的事情就是拿一批“輸入-輸出”的例子讓這個通用人才在你關(guān)心的任務(wù)上再多練一段時間練到一看到類似輸入就知道該按什么套路輸出。微調(diào)的全稱叫監(jiān)督微調(diào)本質(zhì)上還是用交叉熵?fù)p失繼續(xù)訓(xùn)練。只不過數(shù)據(jù)從“網(wǎng)頁文本”換成“你精心整理的任務(wù)數(shù)據(jù)”。你告訴模型當(dāng)用戶問“XXX”的時候正確答案不是模型隨口編的那種而是這種。模型會通過調(diào)整權(quán)重把這種映射刻進(jìn)去。這里有個很重要的區(qū)分微調(diào)不是“從零學(xué)知識到腦子里”。底座模型該懂的知識基本都懂微調(diào)更多是“調(diào)整輸出的方式和邊界”。所以不要指望用幾十條數(shù)據(jù)往模型里灌新知識有效果但很有限而且容易翻車。這也是很多人對微調(diào)的第一個誤解。1.2 先用提示詞和檢索別急著微調(diào)我見過太多人上來就問“這個任務(wù)是不是得微調(diào)一個模型”我一般先反問一句你試過把任務(wù)寫成一段清晰的系統(tǒng)提示詞嗎你試過把相關(guān)知識放到上下文里讓模型先讀一遍嗎這兩個方案的成本幾乎為零卻能在相當(dāng)多場景里替代微調(diào)。提示詞工程解決的問題是“把話說明白”。比如你要模型輸出JSON你就把JSON結(jié)構(gòu)、字段含義、枚舉值全部寫清楚再給兩個例子。現(xiàn)在底座模型的指令跟隨能力很強(qiáng)很多問題在提示詞層面就解決了。檢索增強(qiáng)生成RAG解決的問題是“讓模型看到最新或私有的資料”。你把文檔切成片段用戶提問時先用向量檢索找出最相關(guān)的幾段拼進(jìn)Prompt里讓模型基于這些內(nèi)容回答。這種方式特別適合處理知識更新快、來源雜、需要引用的場景。那什么時候才值得上微調(diào)我的判斷標(biāo)準(zhǔn)有三個第一提示詞和RAG都試過了效果仍然不達(dá)標(biāo)第二任務(wù)要求非常固定的輸出格式比如特定報表、特定語氣、特定結(jié)構(gòu)化字段模型在通用情況下穩(wěn)定還原不了第三你希望模型的行為邊界更可控比如某些問題必須拒絕回答某些問題必須用嚴(yán)謹(jǐn)措辭。滿足其中一兩條再考慮微調(diào)。微調(diào)的成本不只是訓(xùn)練那幾小時還包括數(shù)據(jù)清洗、標(biāo)注、評估、后續(xù)維護(hù)。模型調(diào)壞一次你得重新標(biāo)注數(shù)據(jù)、重新訓(xùn)練、重新驗證時間成本遠(yuǎn)超一次提示詞迭代。所以把微調(diào)放在“低成本方案已經(jīng)驗證過、確實不行”之后才是理性的技術(shù)路線。2. 微調(diào)技術(shù)路線全參數(shù)、LoRA、QLoRA 怎么選2.1 三條路線的原理差異圈內(nèi)聊微調(diào)默認(rèn)都在聊“參數(shù)高效微調(diào)”。原因是底座模型動輒幾十億參數(shù)全量更新一輪的成本太高。當(dāng)前主流有三條路線原理差異很大。全參數(shù)微調(diào)Full Fine-tuning指的是把底座模型所有參數(shù)全部解凍在任務(wù)數(shù)據(jù)上做完整更新。這種做法效果上限最高因為它能最大程度重塑模型行為但代價也最直觀需要很大的顯存。除了模型本身的參數(shù)還要存梯度、優(yōu)化器狀態(tài)一個幾十億參數(shù)的模型可能直接吃掉幾百GB顯存。在普通單卡上基本不用想。LoRALow-Rank Adaptation是目前最常用的一種參數(shù)高效微調(diào)。它的思路很簡單凍結(jié)底座模型的原始權(quán)重在每個需要調(diào)整的線性層旁邊額外加兩個低秩小矩陣訓(xùn)練時只更新這兩個小矩陣。推理時可以把這兩個小矩陣合并回原權(quán)重相當(dāng)于給原模型做了一個“小補(bǔ)丁”。這個補(bǔ)丁里的參數(shù)量通常只有原模型的百分之一甚至更少所以顯存占用和訓(xùn)練成本大幅下降效果在多數(shù)任務(wù)上和全參數(shù)微調(diào)差距不大。QLoRA是在LoRA的基礎(chǔ)上多做一個動作先把底座模型以4bit量化加載進(jìn)來再在量化后的模型上掛LoRA矩陣訓(xùn)練。4bit量化讓模型參數(shù)本身的顯存占用縮小到原來的四分之一左右比如一個7B模型用完整精度加載需要約14GB顯存4bit加載后大約4到5GB。這樣一張普通的24GB消費級顯卡就能訓(xùn)練十幾B甚至更大一點的模型對沒有多卡資源的開發(fā)者來說幾乎是當(dāng)前最佳方案。2.2 現(xiàn)實中的選擇策略和參數(shù)規(guī)模計算怎么選關(guān)鍵看硬件和任務(wù)復(fù)雜度我的建議如下場景推薦路線理由任務(wù)復(fù)雜、領(lǐng)域差異大、有專業(yè)級GPU資源全參數(shù)微調(diào)效果上限最高但需要大顯存和更長訓(xùn)練時間常規(guī)對話風(fēng)格調(diào)整、格式控制、單卡資源LoRA性價比最高絕大多數(shù)場景夠用單卡顯存小、想跑大尺寸底座、快速試錯QLoRA顯存占用最低但量化會帶來輕微精度損失我給一個直觀的LoRA參數(shù)計算示例。假設(shè)底座的隱藏維度是4096這個維度上的隱含層權(quán)重矩陣大小就是4096×4096。如果我們在這一層掛上秩為8的LoRA新增兩個矩陣分別是4096×8和8×4096參數(shù)量是4096×8×2也就是65536個參數(shù)。原本這一層的參數(shù)量是4096×4096約1678萬LoRA新增量只占大約0.4%。把全模型十幾個線性層都掛上一遍累計新增參數(shù)通常也只有幾百萬和原模型幾個B、幾十個B的體量比確實非常小。這也解釋了為什么LoRA能在單卡上訓(xùn)練訓(xùn)練時只需要更新這幾百萬個LoRA參數(shù)優(yōu)化器狀態(tài)和梯度也只跟這幾百萬參數(shù)相關(guān)顯存壓力主要來自“底座模型本身要占的空間”和“前向計算時臨時用到的中間激活值”這兩個可以用量化、梯度檢查點進(jìn)一步壓縮。我現(xiàn)在個人做項目默認(rèn)先跑一版QLoRA確認(rèn)數(shù)據(jù)本身沒有大問題后再決定要不要換LoRA或者全參數(shù)。這個小習(xí)慣能省掉大量無效訓(xùn)練時間。3. 數(shù)據(jù)準(zhǔn)備微調(diào)中最花時間的部分3.1 數(shù)據(jù)格式先對齊底座的“語言習(xí)慣”如果說微調(diào)是給模型上一個任務(wù)培訓(xùn)班那數(shù)據(jù)就是培訓(xùn)班里的教材。教材格式不對模型學(xué)得再多也是亂的。當(dāng)前絕大多數(shù)底座模型在預(yù)訓(xùn)練和指令微調(diào)階段都采用“對話式”訓(xùn)練模板所以你準(zhǔn)備的數(shù)據(jù)也最好跟它們保持一致。單輪指令數(shù)據(jù)最常見的長這樣{ instruction: 把下面這句話翻譯成英文, input: 今天天氣很好, output: The weather is nice today. }如果底座用的是Alpaca那類指令格式這種結(jié)構(gòu)直接用沒問題。更通用的是消息對話格式適合多輪場景{ messages: [ {role: user, content: 你是誰}, {role: assistant, content: 我是一個經(jīng)過微調(diào)的助手。}, {role: user, content: 你能做什么}, {role: assistant, content: 我可以幫你處理文檔、回答問題…} ] }訓(xùn)練時模型會把整段messages拼成一個序列但損失只會計算在assistant回復(fù)的位置這樣模型才知道“用戶說的話不用學(xué)助手該怎么說才是重點”。一個經(jīng)常被忽略的點是用什么模板并不取決于你自己喜歡什么格式而是取決于底座在預(yù)訓(xùn)練和指令對齊時見慣了什么格式。格式越接近底座原生格式模型越容易學(xué)進(jìn)去。你在跑微調(diào)之前最好先用該底座的對話模板手動拼一個樣本喂給底座模型生成一下確認(rèn)能正常對話再開始處理數(shù)據(jù)。這一步花十分鐘能避免后面一個小時的“格式對不上”排查。3.2 數(shù)據(jù)量與質(zhì)量控制我剛接觸微調(diào)時也有個錯覺數(shù)據(jù)越多越好。后來被現(xiàn)實教育了。微調(diào)和預(yù)訓(xùn)練不同預(yù)訓(xùn)練是海量無監(jiān)督文本幾百G上千G都不嫌多微調(diào)是高度有監(jiān)督的任務(wù)數(shù)據(jù)Python里跑幾百條、幾千條很常見效果未必比幾萬條差。更準(zhǔn)確的說法是可控的對話風(fēng)格微調(diào)只需要幾百到兩三千條高質(zhì)量樣本需要掌握復(fù)雜邏輯或?qū)I(yè)推理的任務(wù)可能需要上萬條甚至更多。這里的核心不是“條數(shù)”而是“覆蓋度”。你要讓模型見過的邊界情況夠多而不是讓同一個模式重復(fù)幾百遍。質(zhì)量控制比數(shù)量重要得多。我在處理完一批數(shù)據(jù)后一定會跑一遍檢查腳本把明顯有問題的樣本挑出來。常見檢查點包括有沒有空輸出字段答案是不是來自被截斷的文本同一問題有沒有互相矛盾的答案有沒有包含真實隱私信息有沒有語義重復(fù)到幾乎一模一樣的樣本。特別是重復(fù)樣本會讓模型把某個高頻答案當(dāng)成萬能輸出測試時不管用戶問什么都往那個方向帶。還有一個小技巧把數(shù)據(jù)按任務(wù)類型做個分布統(tǒng)計。比如你是做客服助手有“查訂單”“改地址”“退款”“投訴”四類意圖那就看每類樣本數(shù)量是否均衡。如果不均衡模型會被多數(shù)類帶跑偏少數(shù)類要么頻繁拒絕要么用錯誤話術(shù)。少的那一類哪怕只有幾十條也要優(yōu)先補(bǔ)齊至少讓每個關(guān)鍵分支都有基本量的代表。3.3 構(gòu)造指令數(shù)據(jù)的一些實用技巧數(shù)據(jù)到底從哪來幾種常見途徑從生產(chǎn)環(huán)境日志里清洗真實對話人工編寫一批符合業(yè)務(wù)規(guī)范的樣例讓更強(qiáng)的大模型幫你生成初稿再人工校對。其中“模型生成人工校對”效率最高但我強(qiáng)烈建議每條都要過人工否則錯誤答案會被模型當(dāng)成“正確答案”學(xué)進(jìn)去形成隱性錯誤放大。我常用的構(gòu)造策略是這樣先定義任務(wù)邊界列出這個模型必須能處理的所有輸入類型再為每個類型寫5到10個種子問題然后基于種子問題擴(kuò)展變體比如換說法、換專有名詞、換場景最后補(bǔ)一批“邊界問題”也就是模型應(yīng)該拒絕回答的問題。比如你做一個醫(yī)療問答邊界問題就是“請告訴我這個患者該用什么藥”這類需要醫(yī)生判斷的內(nèi)容模型應(yīng)該拒絕并引導(dǎo)用戶線下就醫(yī)。拒絕類樣本不要寫得太單調(diào)。一個容易犯的錯誤是所有拒絕樣本都長得一樣“對不起我無法回答”。模型學(xué)會的是“遇到不確定就念這句”用戶換個問法它還想不出來到底該不該拒絕。更好的做法是讓拒絕回復(fù)多樣化并且給出解釋“關(guān)于具體用藥建議咨詢執(zhí)業(yè)醫(yī)師我不能代替醫(yī)生做判斷?!边@樣模型學(xué)到的不是一句套話而是一個判斷邏輯。最后一定要從全量數(shù)據(jù)里留出一部分作為驗證集比例5%到10%都行不要讓它參與訓(xùn)練。驗證集的作用是判斷模型“學(xué)沒學(xué)過”過擬合也是你后期評估模型的底稿。數(shù)據(jù)泄漏的事兒我吃過虧有次偷懶直接用全部數(shù)據(jù)訓(xùn)練模型在測試集上分?jǐn)?shù)高得驚人一上真實場景立刻現(xiàn)原形就是因為驗證樣本跟訓(xùn)練樣本太相似。4. 實操跑通一次LoRA微調(diào)4.1 環(huán)境準(zhǔn)備和坑這套流程的基礎(chǔ)環(huán)境其實不復(fù)雜Python、PyTorch、transformers、peft、accelerate、datasets一個都不能少。如果要用QLoRA還需要bitsandbytes這個量化支持庫。我的安裝經(jīng)驗是不要用家里環(huán)境硬懟新建一個虛擬環(huán)境干凈很多。pip install torch transformers peft accelerate datasets bitsandbytes版本方面有個經(jīng)典坑bitsandbytes的版本跟CUDA版本、PyTorch版本必須匹配。很多人裝完庫之后訓(xùn)練一啟動就報“CUDA errormismatch”大概率就是這里不對。穩(wěn)妥的做法是先去查當(dāng)前PyTorch對應(yīng)什么CUDA版本再按這個來安裝適配的bitsandbytes。如果你的顯卡不支持4bit量化也可以先用8bit或者干脆退回純LoRA。多說一句我不建議一上來就追最新的transformers版本。大模型生態(tài)更新快一些老模型在新版本里接口會變出現(xiàn)莫名其妙的問題。鎖定一個自己驗證過的版本組合項目跑穩(wěn)定之后再決定要不要升級這是從業(yè)者的基本習(xí)慣。4.2 加載模型、數(shù)據(jù)與LoRA配置下面這段是QLoRA微調(diào)的核心代碼骨架我按實際項目的簡化版寫。先把底座和分詞器加載進(jìn)來from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_id ./base_model # 本地路徑也可以填社區(qū)模型路徑 tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForCausalLM.from_pretrained( model_id, load_in_4bitTrue, torch_dtypetorch.bfloat16, device_mapauto ) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token這里最關(guān)鍵的是load_in_4bitTrue它讓底座模型以4bit格式進(jìn)入顯存大幅降低空間占用。device_mapauto讓庫自動把各層分配到可用的設(shè)備上單卡時就是全部放同一張卡多卡時會自動切分。pad_token的處理也要注意。很多開源底座只有eos_token沒有pad_token訓(xùn)練時batch內(nèi)樣本長度不一樣需要padding到相等長度如果沒有pad_token程序會直接報錯。最省事的方法是讓pad_token等于eos_token代價是會讓一部分輸入序列在訓(xùn)練時提前遇到結(jié)束符但對大多數(shù)短文本任務(wù)影響不大。接著掛上LoRAfrom peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha16, lora_dropout0.05, biasnone, task_typeCAUSAL_LM, target_modules[q_proj, k_proj, v_proj, o_proj], ) model get_peft_model(model, lora_config)r是低秩矩陣的秩決定微調(diào)容量的“寬度”lora_alpha是縮放系數(shù)控制LoRA更新對原模型影響的程度。r8、alpha16是實踐里一個兼顧效果和穩(wěn)定性的常見起點。target_modules指定掛LoRA的層一般注意力模塊的四個投影都要掛上有些項目也會掛FFN里的線性層效果略好但訓(xùn)練量也略增。4.3 啟動訓(xùn)練、看日志、保存和合并訓(xùn)練配置我用TrainingArguments實現(xiàn)。一個我驗證過穩(wěn)定不爆顯存的起點配置如下from transformers import TrainingArguments, Trainer training_args TrainingArguments( output_dir./lora_out, per_device_train_batch_size2, gradient_accumulation_steps8, learning_rate2e-4, num_train_epochs2, logging_steps20, save_strategyepoch, bf16True, report_tonone, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, ) trainer.train()per_device_train_batch_size2看著很小但配合gradient_accumulation_steps8實際等效batch size是2×816這是為了讓小顯存也能獲得較大batch的穩(wěn)定性。bf16需要顯卡支持如果不支持就改成fp16True但要留意數(shù)值溢出問題。訓(xùn)練過程中把日志打開觀察loss曲線是否在合理區(qū)間下降。保存策略我習(xí)慣按epoch保存這樣哪怕最后一個epoch過擬合了還能回退到前面某個checkpoint。訓(xùn)練完有兩種使用方式。一是推理時直接加載LoRA權(quán)重適合調(diào)試期二是把LoRA矩陣合并回底座權(quán)重然后保存成一個完整的模型適合部署。合并和保存的代碼非常簡單model model.merge_and_unload() model.save_pretrained(./merged_model) tokenizer.save_pretrained(./merged_model)這里有個注意點合并之前先把模型切回原精度或者你打算部署用的精度。如果你是用4bit底座訓(xùn)練的合并后直接保存的是一個帶著量化痕跡的模型部署階段性能可能會有波動。我一般會先用同一底座重新加載一次LoRA權(quán)重再merge這樣可以手動控制加載精度避免一些潛在大坑。5. 訓(xùn)練過程中的關(guān)鍵監(jiān)控與參數(shù)調(diào)優(yōu)5.1 學(xué)會看loss曲線很多人第一次跑微調(diào)盯著loss窗口瘋狂刷新感覺loss不降到0.5就是沒學(xué)好。這其實是誤區(qū)。對話生成任務(wù)的loss是一個句子里所有token的平均交叉熵數(shù)值大小跟底座的初始化狀態(tài)、數(shù)據(jù)難度、序列長度都有關(guān)系一個7B模型在幾百條指令數(shù)據(jù)上跑到loss1.2已經(jīng)算不錯強(qiáng)行壓到很低大概率是過擬合了。我更關(guān)注的是兩條曲線訓(xùn)練loss和驗證loss。訓(xùn)練loss持續(xù)下降是正常的但如果你發(fā)現(xiàn)訓(xùn)練loss還在降驗證loss已經(jīng)開始回升那說明模型已經(jīng)開始“背誦”訓(xùn)練集而不是“學(xué)習(xí)”任務(wù)規(guī)律。這個轉(zhuǎn)折點通常在第二個epoch附近出現(xiàn)。遇到這種情況優(yōu)先考慮減少epoch數(shù)量、增加驗證集多樣性、或者降低學(xué)習(xí)率。還有一個小指標(biāo)值得看訓(xùn)練結(jié)束前最后一次eval的損失跟第一次eval的損失差距。如果差距很小可能說明模型根本沒怎么學(xué)進(jìn)去數(shù)據(jù)質(zhì)量或者學(xué)習(xí)率有問題如果差距很大甚至驗證loss比預(yù)訓(xùn)練底座還高那過擬合基本實錘。5.2 學(xué)習(xí)率、epoch 和災(zāi)難性遺忘LoRA微調(diào)的我常用學(xué)習(xí)率是1e-4到3e-4比全參數(shù)微調(diào)通常用的1e-5到2e-5高一截。原因也好理解LoRA只動幾個小矩陣更新量本來就不大學(xué)習(xí)率太低會讓“補(bǔ)丁”更新得過慢訓(xùn)練半天沒效果。但學(xué)習(xí)率也不是越大越好超過5e-4之后loss很容易發(fā)散變成NaN或者震蕩模型輸出也會開始胡言亂語。epoch數(shù)我建議從1到3起步。沒錯訓(xùn)練集只有幾百條時1個epoch有時候就夠了2個epoch能進(jìn)一步穩(wěn)定格式3個epoch以上基本是在賭它不過擬合。你可以觀察一個現(xiàn)象到某個epoch之后loss雖然還在降但生成的內(nèi)容開始變得“千篇一律”開頭結(jié)尾總是那幾句固定話這就是過度學(xué)習(xí)的信號。為什么微調(diào)特別怕過擬合因為底座模型的語言能力是千百億token堆出來的你微調(diào)的數(shù)據(jù)是幾百萬token都不到的小樣本。訓(xùn)練太久模型會把它原本學(xué)到的多樣表達(dá)方式覆蓋掉也就是所謂的“災(zāi)難性遺忘”。表現(xiàn)是模型回答你業(yè)務(wù)問題很標(biāo)準(zhǔn)但一旦問個常識問題它反而不如微調(diào)前聰明了。為了防止這一點除了控制epoch和學(xué)習(xí)率另一個常用技巧是在訓(xùn)練集里摻入一部分通用對話數(shù)據(jù)讓模型別把舊知識全忘光。5.3 顯存不夠時的三板斧單卡跑微調(diào)顯存是最容易卡脖子的地方。我的排查順序是固定的先看batch大小是不是太大再看序列長度是不是太長最后看有沒有開梯度檢查點。第一板斧是減小per_device_train_batch_size從8降到4再降到2。batch變小后梯度方差會變大所以同時調(diào)大gradient_accumulation_steps保證等效batch_size不變。等效batch_sizeper_device_train_batch_size×gradient_accumulation_steps×顯卡數(shù)這個公式建議隨手寫在筆記里。第二板斧是控制序列長度。有些指令數(shù)據(jù)特別長比如把整個網(wǎng)頁塞進(jìn)input字段序列長度直接飆到4096甚至8192。長序列意味著中間激活值占用巨大顯存。先看一下訓(xùn)練集里的長度分布如果大部分樣本都在512到1024之間那把max_length設(shè)成1024或者2048能省出一大塊顯存。少數(shù)超長樣本要么截斷要么拆分。第三板斧是梯度檢查點gradient checkpointing。它的原理是“用時間換空間”前向傳播時不保存所有中間激活值反向傳播時又重新計算一遍。開啟后顯存占用能明顯下降但訓(xùn)練速度會慢一些。DataCollator里設(shè)置paddingTrue時也要小心一邊倒的長padding浪費顯存必要時用動態(tài)padding讓模型只按當(dāng)前batch最長的樣本做padding。6. 常見問題排查一個人debug的經(jīng)驗清單6.1 loss不降、NaN訓(xùn)練到一半loss突然變成NaN是最嚇人的情況。我遇到過的原因主要有三類學(xué)習(xí)率太高梯度更新一步跨太大直接溢出數(shù)據(jù)里有異常值比如某個樣本的input全是重復(fù)字符或長度畸形到爆混合精度下bf16/fp16數(shù)值溢出特別是fp16在Loss值高的時候不穩(wěn)定。排查順序也別亂。先看一眼是第幾步開始NaN的如果剛開始幾步就NaN基本是學(xué)習(xí)率問題降到1e-5試一把如果跑了幾百步才NaN優(yōu)先懷疑數(shù)據(jù)里混進(jìn)了壞樣本抽出一條條過一遍如果是fp16訓(xùn)練換成bf16或者純fp32再試。還有一個隱蔽情況4bit量化層在某些顯卡驅(qū)動版本下有數(shù)值問題換新版本或關(guān)掉4bit能解決。loss完全不降是另一類頭疼問題。如果從頭到尾loss都紋絲不動先檢查數(shù)據(jù)有沒有真正加載進(jìn)模型。有次我因為tokenizer的配置寫錯所有的input都被截成了空序列模型每個token的預(yù)測都模棱兩可loss自然降不下去。把訓(xùn)練集里那條實際輸入打印出來看一眼成本最低。6.2 顯存爆掉顯存爆掉OOM的報錯信息基本是“CUDA out of memory”。如果你看到這個八成不是程序有bug而是配置超了物理限制。按我上面的三板斧先減batch、減長度、開梯度檢查點。還有一個容易被忽略的地方驗證階段同樣占顯存如果訓(xùn)練正常驗證OOM就把eval的batch size也調(diào)小或者減少驗證集樣本量。多卡訓(xùn)練時OOM經(jīng)常不是所有卡一起爆而是某一兩張卡先爆這是因為模型分布不均。device_mapauto會自動分配但有些層特別吃顯存比如embedding層。你也可以手動指定某些模塊放在CPU上把顯存讓給計算密集的Transformer層。不過CPU和GPU之間頻繁通信會拖慢速度只能在實在沒辦法的時候用。6.3 模型變笨、復(fù)讀、亂拒絕訓(xùn)練完了模型在你業(yè)務(wù)問題上表現(xiàn)可能很好但問它一個基礎(chǔ)常識它開始胡扯或者不管問什么都回“我不能回答”。這很大概率不是模型壞了而是數(shù)據(jù)配比出了問題?!皬?fù)讀機(jī)”現(xiàn)象通常是因為訓(xùn)練集里的回答太單一所有樣本的輸出結(jié)構(gòu)都一模一樣模型學(xué)會了那個高頻結(jié)構(gòu)不會變通。解決辦法是在數(shù)據(jù)里引入更多樣的表達(dá)方式同一語義寫在5到10種句式里?!皝y拒絕”的根源往往是拒絕類樣本太多或者太統(tǒng)一模型被訓(xùn)練成“遇到不認(rèn)識的輸入就觸發(fā)拒絕”。我之前做業(yè)務(wù)時加多了安全拒絕樣本結(jié)果正常問題也攔掉一部分后來把拒絕類樣本比例控制在10%以下并且讓拒絕回復(fù)多樣化情況才好轉(zhuǎn)。還有一種情況是模型“學(xué)到了格式但丟了邏輯”。比如要求輸出JSON模型確實輸出JSON了但JSON里的字段值全是胡編的。這說明數(shù)據(jù)里有不少邏輯錯誤的樣本模型把“答非所問”也當(dāng)成了一種格式學(xué)會?;仡^檢查數(shù)據(jù)質(zhì)量比繼續(xù)調(diào)參有用得多。6.4 多輪對話能力退化有些項目只準(zhǔn)備了單輪指令數(shù)據(jù)進(jìn)行微調(diào)訓(xùn)練完成后發(fā)現(xiàn)模型在連續(xù)對話里沒法正確引用上文或者壓根不記得用戶之前說過什么。原因很簡單你在訓(xùn)練數(shù)據(jù)里根本沒有讓模型見過“歷史消息”這種結(jié)構(gòu)它自然不擅長處理。對策是在數(shù)據(jù)里混入多輪對話樣本比例建議至少20%到30%。多輪樣本也不是越長越好4到6輪比較合適。如果業(yè)務(wù)場景確實不需要多輪那單輪無所謂但只要你預(yù)期用戶會在對話里繼續(xù)追問就必須在數(shù)據(jù)里體現(xiàn)多輪上下文。另一個細(xì)節(jié)多輪訓(xùn)練時前面輪次的assistant回復(fù)也會參與計算損失如果你希望模型優(yōu)先學(xué)習(xí)“最后一輪該怎么回”可以使用只對最后一輪計算損失的策略否則模型會把前期輪次的話術(shù)也一并學(xué)進(jìn)去整體風(fēng)格容易變得冗長。6.5 排查表現(xiàn)象優(yōu)先排查方向常用解法loss為NaN學(xué)習(xí)率、數(shù)據(jù)異常、fp16精度、量化層降學(xué)習(xí)率、清洗數(shù)據(jù)、開bf16、升級庫版本loss不降數(shù)據(jù)是否為空/截斷、學(xué)習(xí)率過低打印訓(xùn)練樣本、調(diào)大學(xué)習(xí)率、檢查tokenizer顯存OOMbatch過大、序列過長、激活值過大減batch、開梯度檢查點、限制max_length復(fù)讀機(jī)數(shù)據(jù)輸出單一擴(kuò)樣本句式多樣性亂拒絕負(fù)樣本過多且單一調(diào)低負(fù)樣本比例、豐富拒絕話術(shù)多輪能力退化數(shù)據(jù)全部是單輪混入20%以上多輪樣本訓(xùn)練后常識變差epoch過多、學(xué)習(xí)率過大、數(shù)據(jù)太偏早停、降低lr、混入通用對話數(shù)據(jù)7. 微調(diào)完的評估與部署7.1 怎么設(shè)計一套夠用的評估集微調(diào)的過程有意義但很多人訓(xùn)練完只會看兩眼loss然后憑感覺說“好像變聰明了”。這種做法風(fēng)險很大因為你看到的幾段示例可能恰好是訓(xùn)練里見過的模式真實效果好不好根本沒量化。正確做法是單獨留一份不用參與訓(xùn)練的評估集數(shù)量和訓(xùn)練集保持同分布但內(nèi)容完全不同。評估時不能只看模型能不能說對還要看它是不是“用符合業(yè)務(wù)要求的方式說對”。我給自己的項目打分會看五個維度內(nèi)容正確性是否出現(xiàn)事實性錯誤指令遵循度用戶要求JSON它給沒給JSON格式穩(wěn)定性字段名、枚舉值、換行符是不是都按約定來邊界處理不該回答的問題有沒有克制風(fēng)格一致性是否貼近業(yè)務(wù)想要的口吻。自動指標(biāo)可以用BLEU/Rouge這類文本相似度但對話生成任務(wù)里它們經(jīng)常失靈兩句話意思一樣但詞面不同分?jǐn)?shù)會很難看。我通常做的是“人工雙盲對比”把微調(diào)前后的模型各自生成20到50個回答去掉來源標(biāo)記混在一起讓人打分排序。別嫌土這種方法最接近真實使用感受而且能發(fā)現(xiàn)很多指標(biāo)看不出來的問題。7.2 合并權(quán)重、量化與部署評估通過之后進(jìn)入部署。如果你的推理服務(wù)不依賴開源生態(tài)合并權(quán)重是最省事的方案。訓(xùn)練端執(zhí)行model.merge_and_unload()再把完整模型存下來部署端直接加載這個合并后的模型不需要額外加載LoRA adapter。合并后的模型在參數(shù)量上等于一個完整底座推理速度比“底座adapter”的方式更快因為少了一層動態(tài)計算分支。部署時量化又是一個選擇。如果你對顯存依然敏感可以把合并后的模型用量化工具壓到4bit或8bit再部署犧牲少量精度換吞吐量。但注意一點訓(xùn)練階段用的是QLoRA的4bit底座的權(quán)重在訓(xùn)練中是被凍結(jié)的量化誤差是可控的推理階段的新量化是另一套量化參數(shù)需要做一輪校準(zhǔn)不能直接照搬訓(xùn)練時的量化配置。真正上線前還有兩個細(xì)節(jié)值得檢查tokenizer的chat template是否和微調(diào)數(shù)據(jù)一致否則部署后模型會以錯誤方式拼上下文效果直線下降生成參數(shù)比如溫度、top_p是不是設(shè)置合理我見過很多項目因為推理時temperature設(shè)太高把微調(diào)學(xué)到的嚴(yán)謹(jǐn)風(fēng)格全沖散了。這兩個細(xì)節(jié)排查一遍比繼續(xù)訓(xùn)練三個epoch更有效。我個人在實際操作中還有一個習(xí)慣跑微調(diào)之前先把訓(xùn)練數(shù)據(jù)里隨機(jī)抽20條讓原始底座直接生成一遍。這一步能提前暴露模板寫錯、問題太刁鉆、輸出格式提示詞不完整等一系列問題。有了這個baseline微調(diào)后的效果才有真正的對照而不是憑感覺覺得“模型變強(qiáng)了”?,F(xiàn)在每次交測試報告我都先貼出這20條的微調(diào)前回答和微調(diào)后回答否則一切指標(biāo)都像空中樓閣。這套流程跑順了以后再來新任務(wù)半天內(nèi)就能出一版可評估的微調(diào)模型。