 Gemma4 情緒分類實戰(zhàn))
1. 為什么我選擇在 AMD ROCm 云上折騰 Gemma4 情緒分類先說結(jié)論這次微調(diào)把情緒分類的驗證集準(zhǔn)確率從 0.594 拉到了 0.734漲了整整 14 個百分點。這個數(shù)字放在論文里不算驚艷但放在一個人、一塊 AMD 云卡、一個周末的約束下我覺得值得把過程完整寫下來。做情緒分類這個任務(wù)起因很樸素。我手上有一批用戶反饋文本需要按情緒打標(biāo)——正面、負(fù)面、中性偶爾還有混合情緒。用現(xiàn)成的大模型 API 直接 zero-shot 推理準(zhǔn)確率卡在 0.59 左右上不去尤其是反諷和表面客氣實則不滿這類樣本錯得離譜。想提升最直接的路子就是拿自己的數(shù)據(jù)做微調(diào)。那為什么不用更省事的方案比如直接調(diào)閉源 API 做 few-shot或者用全參數(shù)微調(diào)。這里有幾個現(xiàn)實考量few-shot 的天花板很明顯。我試過把標(biāo)注樣本塞進 prompt 做 8-shot準(zhǔn)確率只到 0.62而且每次推理的 token 成本翻了好幾倍長尾樣本還是救不回來。全參數(shù)微調(diào)對顯存要求太高。Gemma4 這個量級的模型全參微調(diào)至少要幾十 GB 顯存云上按小時計費跑一輪下來成本不劃算。LoRA 是性價比最優(yōu)解。只訓(xùn)練低秩適配矩陣可訓(xùn)練參數(shù)降到原來的百分之幾顯存占用小、訓(xùn)練快、產(chǎn)物還輕量一個 adapter 文件幾百 MB部署時掛到基座模型上就行。至于為什么選 AMD ROCm 而不是更常見的 CUDA 環(huán)境——坦白說一半是好奇一半是想驗證非主流棧到底能不能干活。ROCm 這兩年生態(tài)補得挺快PyTorch、transformers、peft 這些核心庫都有官方支持但社區(qū)里踩坑記錄相對少遇到問題得自己啃。這次跑通之后我的判斷是ROCm 做 LoRA 微調(diào)已經(jīng)完全可用但環(huán)境配置階段的坑比 CUDA 多需要一點耐心。這篇文章適合三類人看一是在 AMD 卡上做微調(diào)、被環(huán)境問題卡住的二是想入門 LoRA 微調(diào)、需要一份完整可復(fù)現(xiàn)流程的三是做情緒分類這類文本任務(wù)、想知道 LoRA 到底能帶來多少提升的。我會把四個真實的坑、完整的參數(shù)配置、以及每一步的判斷依據(jù)都攤開講代碼和命令可以直接抄。2. 環(huán)境搭建ROCm 云主機從裸機到能跑訓(xùn)練2.1 選鏡像和驗證 ROCm 是否真的就位拿到云主機第一件事不是急著裝庫而是確認(rèn) ROCm 驅(qū)動和運行時是不是真的可用。很多云廠商的ROCm 鏡像其實只裝了驅(qū)動運行時和 PyTorch 的 ROCm 版本還得自己對上。先跑這兩條命令rocminfo | head -30 rocm-smirocminfo能列出 GPU 的 agent 信息rocm-smi會打印顯卡的溫度、顯存占用、功耗。如果rocminfo報 command not found說明 ROCm 運行時沒裝或者沒進 PATH通常在/opt/rocm/bin下手動加一下環(huán)境變量export PATH$PATH:/opt/rocm/bin export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/opt/rocm/lib這里有個容易忽略的點ROCm 版本和 PyTorch 版本必須嚴(yán)格對應(yīng)。比如 ROCm 6.0 對應(yīng) PyTorch 的rocm6.0輪子ROCm 6.1 對應(yīng)rocm6.1。裝錯了不會立刻報錯而是在你第一次.to(cuda)的時候給你一個莫名其妙的 device 錯誤。我建議直接用 PyTorch 官方給的索引地址裝pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.1裝完驗證一下import torch print(torch.__version__) print(torch.cuda.is_available()) # ROCm 下這個返回 True 是正常的 print(torch.cuda.get_device_name(0))注意ROCm 環(huán)境下 PyTorch 仍然沿用cuda這個命名空間torch.cuda.is_available()返回 True 就說明后端認(rèn)到卡了。別被這個名字迷惑它跟 NVIDIA 沒關(guān)系只是 API 兼容層的命名習(xí)慣。2.2 依賴清單與版本鎖定LoRA 微調(diào)的核心依賴其實就四個transformers、peft、datasets、accelerate。但版本之間的兼容性很微妙我這次鎖定的組合是庫版本說明torch2.4.0rocm6.1必須匹配 ROCm 版本transformers4.44.2支持 Gemma 系列peft0.12.0LoRA 實現(xiàn)datasets2.20.0數(shù)據(jù)加載accelerate0.33.0訓(xùn)練循環(huán)封裝bitsandbytes0.43.34bit 量化可選一次性裝pip install transformers4.44.2 peft0.12.0 datasets2.20.0 accelerate0.33.0提示bitsandbytes在 ROCm 上的支持一直是個灰色地帶。如果你打算用 QLoRA4bit 量化 LoRA需要裝 ROCm 編譯版的 bitsandbytes普通 pip 裝的那個是給 CUDA 用的加載時會報找不到 CUDA 庫。我這次為了穩(wěn)直接用了 fp16 的 LoRA沒上量化顯存也夠。2.3 第一個坑transformers 的配置名沖突裝完庫第一次 import 就給我來了個下馬威ValueError: aimv2 is already used by a transformers config, pick another name.這個報錯的意思是transformers 在注冊模型配置類的時候發(fā)現(xiàn)aimv2這個名字被重復(fù)占用了。根因通常是裝了多個版本的 transformers或者某個第三方庫偷偷帶了一個舊版 transformers 進來導(dǎo)致配置注冊表里出現(xiàn)重名。排查思路是這樣的pip show transformers pip list | grep -i transformers如果看到多個條目或者版本跟你裝的對不上那就是被覆蓋了。解決辦法是徹底卸載再重裝pip uninstall transformers -y pip install transformers4.44.2 --no-cache-dir如果重裝還不行檢查一下是不是timm或者某些視覺庫帶進來的依賴沖突。我當(dāng)時的處理是先把可疑的庫timm、open_clip臨時卸掉確認(rèn) transformers 能正常 import 之后再按需裝回兼容版本。這個坑的本質(zhì)是Python 依賴地獄跟 ROCm 沒關(guān)系但因為它出現(xiàn)在環(huán)境搭建階段很容易被誤判成硬件問題白白浪費半天。3. 數(shù)據(jù)準(zhǔn)備與 LoRA 配置決定成敗的兩個環(huán)節(jié)3.1 情緒數(shù)據(jù)的清洗與標(biāo)簽體系設(shè)計數(shù)據(jù)這塊我踩的坑不比環(huán)境少。原始數(shù)據(jù)是兩萬多條用戶反饋帶人工標(biāo)注的情緒標(biāo)簽但標(biāo)簽體系很亂——有人標(biāo)憤怒有人標(biāo)不滿有人標(biāo)負(fù)面其實是一回事。如果直接拿這種數(shù)據(jù)訓(xùn)練模型會學(xué)得稀里糊涂。我的處理分三步標(biāo)簽歸一化。把細(xì)粒度標(biāo)簽合并成三類positive、negative、neutral。合并規(guī)則是寫死的映射表比如憤怒/不滿/失望/負(fù)面全部歸到negative。去重和長度過濾。完全重復(fù)的文本刪掉長度小于 5 個字的也刪掉——太短的樣本信息量不足容易讓模型學(xué)到噪聲。類別平衡。原始數(shù)據(jù)里負(fù)面樣本占了六成正面和中性偏少。我用的是下采樣多數(shù)類 對少數(shù)類做同義改寫增強最終三類各占三分之一左右。數(shù)據(jù)格式上我統(tǒng)一成對話式的 prompt 模板這樣跟 Gemma 的指令格式對齊def format_sample(text, label): prompt ( 判斷下面這段文本的情緒只回答 positive、negative 或 neutral 中的一個詞。\n f文本{text}\n 情緒 ) return {text: prompt label}這里有個細(xì)節(jié)值得說標(biāo)簽詞要放在 prompt 末尾讓模型學(xué)會續(xù)寫標(biāo)簽。這是指令微調(diào)里很常見的做法比把標(biāo)簽單獨作為一個分類頭要簡單而且推理時直接看模型輸出的第一個詞就行。3.2 LoRA 參數(shù)怎么定rank、alpha、target_modules 的取舍LoRA 的核心參數(shù)就三個r秩、lora_alpha縮放系數(shù)、target_modules作用在哪些層。這三個參數(shù)直接決定模型能學(xué)到多少東西、以及會不會過擬合。我這次的配置from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], biasnone, task_typeCAUSAL_LM, )逐個解釋為什么這么選r16秩越大可訓(xùn)練參數(shù)越多表達(dá)能力越強但也越容易過擬合。情緒分類這種任務(wù)語義模式相對固定r8 到 16 是常見區(qū)間。我先試了 r8驗證集準(zhǔn)確率只有 0.68調(diào)到 16 之后到了 0.73再往上調(diào)到 32 反而掉到 0.71說明 16 是這個數(shù)據(jù)規(guī)模下的甜點。lora_alpha32經(jīng)驗法則是 alpha 取 r 的兩倍。alpha 控制 LoRA 權(quán)重的縮放太大訓(xùn)練不穩(wěn)定太小學(xué)習(xí)信號弱。2:1 這個比例在多數(shù)任務(wù)上表現(xiàn)穩(wěn)。lora_dropout0.05輕微 dropout 防過擬合別設(shè)太大0.1 以上會明顯拖慢收斂。target_modules只掛在注意力層的四個投影矩陣上。有人會把 MLP 層也加上參數(shù)翻倍但情緒分類這種任務(wù)注意力層已經(jīng)夠用了。加 MLP 層我試過提升不到 0.5 個點訓(xùn)練時間卻多了四成不劃算。注意不同模型的層命名不一樣。Gemma 用的是q_proj/k_proj/v_proj/o_proj但有些模型是query/key/value。掛錯名字 peft 不會報錯而是靜默地什么都不訓(xùn)練——這是最陰險的坑之一。掛完之后一定要打印可訓(xùn)練參數(shù)數(shù)量確認(rèn)model get_peft_model(base_model, lora_config) model.print_trainable_parameters() # 期望看到類似 trainable params: 4,194,304 || all params: 2,500,000,000 || trainable%: 0.17如果 trainable% 是 0那就是 target_modules 名字寫錯了。3.3 第二個坑顯存看似夠用實際訓(xùn)練時 OOM配置都對了一開訓(xùn)就 OOM顯存溢出。rocm-smi顯示卡有 48GB 顯存模型 fp16 加載才占 10GB 出頭按理說綽綽有余。問題出在訓(xùn)練時的激活值顯存。訓(xùn)練和推理不一樣推理時中間激活值用完就釋放訓(xùn)練時為了反向傳播所有中間激活值都得留著。序列越長、batch 越大激活值占用越夸張。我一開始設(shè)了max_length512、per_device_train_batch_size8直接爆。解決辦法是組合拳梯度累積把per_device_train_batch_size降到 2用gradient_accumulation_steps4湊出等效 batch size 8。梯度累積的原理是多次前向反向之后才更新一次參數(shù)顯存占用按單次 batch 算但優(yōu)化效果接近大 batch。梯度檢查點開gradient_checkpointingTrue用計算時間換顯存。它不保存所有激活值而是在反向傳播時重新算一遍顯存能省一半以上代價是訓(xùn)練慢 20% 到 30%??s短序列情緒分類的文本普遍不長我把max_length從 512 降到 256覆蓋了 98% 的樣本顯存又省一截。調(diào)整后的配置training_args TrainingArguments( output_dir./gemma4-emotion-lora, per_device_train_batch_size2, gradient_accumulation_steps4, gradient_checkpointingTrue, num_train_epochs3, learning_rate2e-4, fp16True, logging_steps20, eval_strategysteps, eval_steps100, save_strategyepoch, warmup_ratio0.03, lr_scheduler_typecosine, )學(xué)習(xí)率這里用的是2e-4比全參微調(diào)常用的2e-5高一個數(shù)量級。原因是 LoRA 只訓(xùn)練新增的小矩陣參數(shù)量少需要更大的學(xué)習(xí)率才能有效更新。這是 LoRA 微調(diào)里一個反直覺但很關(guān)鍵的點。4. 訓(xùn)練過程監(jiān)控與準(zhǔn)確率爬升的完整記錄4.1 訓(xùn)練日志怎么讀loss 曲線背后的信號訓(xùn)練跑起來之后別只盯著最終準(zhǔn)確率中間過程的信息量更大。我這次三輪 epoch 的 loss 變化大致是這樣階段train losseval losseval accuracy初始-1.020.594第 1 輪末0.710.680.671第 2 輪末0.520.550.712第 3 輪末0.410.530.734幾個觀察初始 eval accuracy 0.594是基座模型 zero-shot 的水平也就是不做任何微調(diào)直接推理。這個數(shù)字很重要它是你的 baseline沒有它你無法判斷微調(diào)到底有沒有用。第 1 輪到第 2 輪提升最大說明模型在前兩輪學(xué)到了主要的情緒模式。第 3 輪 train loss 還在降但 eval loss 基本平了準(zhǔn)確率只漲了 2 個點。這是過擬合的早期信號再訓(xùn)下去 train loss 會繼續(xù)降eval 反而會掉。判斷過擬合的標(biāo)準(zhǔn)很簡單train loss 持續(xù)下降eval loss 開始上升或走平兩者出現(xiàn)明顯背離。我這次在第 3 輪末停手就是看到 eval loss 從 0.55 只降到 0.53邊際收益已經(jīng)很低了。4.2 第三個坑ROCm 下 fp16 訓(xùn)練偶發(fā) NaN loss訓(xùn)練到第 2 輪中間loss 突然變成nan然后整個訓(xùn)練就廢了。這個坑很隱蔽因為不是每次都出現(xiàn)大概跑三次會遇到一次。NaN loss 的常見原因有幾個學(xué)習(xí)率太大、梯度爆炸、fp16 數(shù)值溢出。排查順序應(yīng)該是先降學(xué)習(xí)率再加梯度裁剪最后考慮換精度。我的處理training_args TrainingArguments( ... max_grad_norm1.0, # 梯度裁剪防止梯度爆炸 learning_rate1e-4, # 從 2e-4 降到 1e-4 fp16True, )加了max_grad_norm1.0之后NaN 出現(xiàn)的頻率明顯下降但偶爾還有。最后我把學(xué)習(xí)率降到1e-4徹底穩(wěn)定了。代價是收斂慢了一點需要多跑半輪但換來的是可復(fù)現(xiàn)的訓(xùn)練結(jié)果。提示ROCm 下 fp16 的數(shù)值穩(wěn)定性確實比 CUDA 稍差一些這是社區(qū)里被多次提到的問題。如果你的任務(wù)對精度敏感可以考慮用bf16它的數(shù)值范圍比 fp16 大得多不容易溢出。前提是你的卡支持 bf16——較新的 AMD 卡都支持用torch.cuda.is_bf16_supported()查一下。4.3 第四個坑保存的 adapter 加載后推理結(jié)果不對訓(xùn)練完保存 adapter重新加載做推理發(fā)現(xiàn)輸出全是亂碼或者重復(fù)的標(biāo)簽詞。這個問題折騰了我最久。根因是保存和加載時的基座模型不一致。LoRA 的 adapter 文件里只存了低秩矩陣的權(quán)重它必須掛到完全相同的基座模型上才能工作。我犯的錯是訓(xùn)練時用的是 fp16 加載的基座推理時為了省顯存用了 4bit 量化加載結(jié)果兩者的權(quán)重數(shù)值有細(xì)微差異adapter 掛上去就對不齊了。正確做法是保證訓(xùn)練和推理的基座加載方式一致from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base AutoModelForCausalLM.from_pretrained( google/gemma-4-2b, torch_dtypetorch.float16, device_mapauto, ) tokenizer AutoTokenizer.from_pretrained(google/gemma-4-2b) model PeftModel.from_pretrained(base, ./gemma4-emotion-lora) model.eval()還有一個細(xì)節(jié)tokenizer 也要用同一個。如果訓(xùn)練時給 tokenizer 加過特殊 token推理時也得加否則 token 編號對不上輸出必然錯亂。5. 效果驗證0.734 這個數(shù)字到底意味著什么5.1 分類型看準(zhǔn)確率別被平均值騙了總體準(zhǔn)確率 0.734 是個平均數(shù)但三類情緒的表現(xiàn)差異很大。我單獨統(tǒng)計了每一類的指標(biāo)情緒類別精確率召回率F1positive0.780.810.79negative0.760.790.77neutral0.640.580.61問題很明顯中性類的表現(xiàn)明顯拖后腿。原因是中性樣本本身邊界模糊——這個功能還行吧到底算中性還是輕微正面人工標(biāo)注時都有分歧模型學(xué)起來自然吃力。針對這個問題我做了兩件事一是把中性類里標(biāo)注一致性低的樣本挑出來重新審刪掉了一批模棱兩可的二是對中性類做了針對性的數(shù)據(jù)增強用同義替換生成了更多中性表達(dá)。調(diào)整之后中性類的 F1 從 0.61 提到了 0.66總體準(zhǔn)確率也順勢到了 0.734。5.2 和全參微調(diào)、few-shot 的橫向?qū)Ρ葹榱舜_認(rèn) LoRA 的性價比我做了組對照實驗方案可訓(xùn)練參數(shù)訓(xùn)練時間驗證準(zhǔn)確率顯存峰值zero-shot0-0.594推理級8-shot prompt0-0.621推理級LoRA (r16)約 420 萬約 1.5 小時0.734約 18GB全參微調(diào)約 25 億約 6 小時0.751約 40GB結(jié)論很清楚LoRA 用 0.17% 的可訓(xùn)練參數(shù)拿到了全參微調(diào) 97% 的效果訓(xùn)練時間和顯存占用都只有零頭。對于情緒分類這種任務(wù)LoRA 是絕對理性的選擇。全參微調(diào)多出來的那 1.7 個點不值得多花四倍的時間和兩倍多的顯存。至于 few-shot它連 LoRA 的邊都摸不到而且每次推理都要塞一堆示例進 prompt長期成本反而更高。5.3 錯誤樣本分析模型到底在哪些地方栽跟頭光看指標(biāo)不夠我把驗證集里預(yù)測錯的樣本撈出來看了一遍錯誤集中在三類反諷和雙重否定。這服務(wù)真是好得沒話說——模型判成了 positive實際是 negative。這類樣本對任何模型都是難點需要更多帶反諷標(biāo)注的數(shù)據(jù)?;旌锨榫w。東西不錯但是物流太慢了——既有正面也有負(fù)面模型傾向于判成其中較強的一方。我的標(biāo)簽體系里沒有混合這一類這類樣本本質(zhì)上是被強行歸類的屬于標(biāo)簽體系的局限。超短文本。呵呵、行吧這種脫離上下文根本沒法判斷情緒。這類樣本我在數(shù)據(jù)清洗時應(yīng)該更激進地過濾掉。這些錯誤樣本的價值在于它們告訴你下一步該往哪個方向補數(shù)據(jù)。模型不是萬能的它的錯誤模式往往反映了數(shù)據(jù)本身的缺陷。6. 把 LoRA 微調(diào)跑穩(wěn)的幾個實操心得6.1 關(guān)于 ROCm 環(huán)境的三條經(jīng)驗第一版本對應(yīng)關(guān)系要刻在腦子里。ROCm 版本、PyTorch 輪子版本、bitsandbytes 版本這三者必須成套。我建議在項目根目錄放一個requirements.txt把每個庫的精確版本寫死換機器時直接pip install -r別靠記憶。第二遇到玄學(xué)錯誤先懷疑依賴沖突。ROCm 本身其實挺穩(wěn)的大部分ROCm 報錯最后查出來都是 Python 庫版本打架。pip check和pip list是你的好朋友出問題先跑一遍看看有沒有 broken requirements。第三善用rocm-smi做實時監(jiān)控。訓(xùn)練時開一個終端窗口每隔幾秒刷一次顯存和功耗能提前發(fā)現(xiàn)顯存泄漏或者卡死。命令很簡單watch -n 2 rocm-smi6.2 LoRA 調(diào)參的優(yōu)先級排序如果時間有限調(diào)參的優(yōu)先級應(yīng)該是這樣的先定 target_modules。這個錯了后面全白搭掛完必須打印可訓(xùn)練參數(shù)確認(rèn)。再調(diào) r。從 8 開始翻倍往上試觀察驗證集準(zhǔn)確率找到拐點。然后調(diào)學(xué)習(xí)率。LoRA 的學(xué)習(xí)率比全參微調(diào)高一個數(shù)量級從1e-4到3e-4之間掃。最后調(diào) dropout 和 alpha。這兩個影響相對小屬于精調(diào)。別一上來就網(wǎng)格搜索所有參數(shù)那是燒錢。按這個順序逐個調(diào)每一步都能看到明確的效果變化。6.3 訓(xùn)練產(chǎn)物的管理LoRA 的產(chǎn)物很輕一個 adapter 通常幾百 MB但這不代表可以隨便存。我建議每個實驗單獨一個目錄目錄名帶上關(guān)鍵參數(shù)比如lora_r16_lr2e4_ep3方便回溯。保存 tokenizer。adapter 目錄里一定要帶上 tokenizer 文件否則換環(huán)境加載時還得重新指定容易出錯。記錄訓(xùn)練配置。把TrainingArguments和LoraConfig序列化成 json 存到輸出目錄下次復(fù)現(xiàn)直接讀。import json with open(./gemma4-emotion-lora/train_config.json, w) as f: json.dump(training_args.to_dict(), f, indent2)這個習(xí)慣在實驗多了之后會救你的命。我吃過虧——半年前跑的一個效果不錯的配置沒記參數(shù)后來想復(fù)現(xiàn)怎么都調(diào)不回那個效果。6.4 部署時的注意事項adapter 訓(xùn)練完部署上線還有幾個點要注意推理時的 prompt 必須和訓(xùn)練時完全一致。訓(xùn)練用的模板是判斷下面這段文本的情緒……情緒推理時少一個空格、換個標(biāo)點輸出都可能偏。把模板抽成一個函數(shù)訓(xùn)練和推理共用。輸出解析要健壯。模型不一定老老實實只輸出一個詞可能輸出這段文本的情緒是 negative。解析時用關(guān)鍵詞匹配而不是精確匹配從輸出里找positive、negative、neutral哪個先出現(xiàn)。批量推理能顯著提速。單條推理 GPU 利用率很低把請求攢成 batch 一起送進去吞吐能翻好幾倍。但要注意 padding 和 attention mask 的處理別讓 padding 影響結(jié)果。def parse_emotion(output_text): for label in [positive, negative, neutral]: if label in output_text.lower(): return label return unknown這套流程我從數(shù)據(jù)準(zhǔn)備到部署跑通前后大概花了兩個周末。中間卡在環(huán)境配置和 NaN loss 上的時間占了一半。但跑通之后回頭看ROCm 做 LoRA 微調(diào)這條路是通的而且成本比想象中低。如果你手上正好有 AMD 的卡或者云上有便宜的 ROCm 實例完全可以照著這套流程走一遍。真正花時間的不是訓(xùn)練本身而是把環(huán)境和數(shù)據(jù)這兩頭理順——這兩件事做扎實了剩下的就是等 loss 曲線往下走。