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