習(xí)的文本分類實戰(zhàn):從解壓環(huán)境到BERT微調(diào)全流程)
簡介這是一份面向Python開發(fā)者與NLP初學(xué)者的深度學(xué)習(xí)文本分類實戰(zhàn)代碼包聚焦自然語言處理中的核心技術(shù)任務(wù)幫助讀者掌握從文本預(yù)處理、詞嵌入到模型訓(xùn)練與評估的完整流程。壓縮包內(nèi)共6個文件全部為.py腳本涵蓋數(shù)據(jù)加載、CNN與RNN模型構(gòu)建、預(yù)測調(diào)用及訓(xùn)練入口等模塊結(jié)構(gòu)緊湊適合快速上手。包體大小僅11KB輕量易用便于直接閱讀和修改。目前已有224人學(xué)習(xí)下載適合希望通過實際代碼理解TextCNN、TextRNN以及LSTM/GRU在中文分類任務(wù)中應(yīng)用的人群。資源中的腳本與NLP-beginner-Task2實踐項目對應(yīng)可支撐學(xué)習(xí)者完成基于深度學(xué)習(xí)的文本分類實驗并在此基礎(chǔ)上進(jìn)行調(diào)參和模型對比。}1. 基于深度學(xué)習(xí)的文本分類.zip從壓縮包到可復(fù)現(xiàn)的分類工程基于深度學(xué)習(xí)的文本分類.zip 這個壓縮包通常不是給你一個訓(xùn)練好的模型而是一整套訓(xùn)練鏈路數(shù)據(jù)、代碼、依賴清單、權(quán)重文件都壓在一個包里。它要解決的是真實業(yè)務(wù)里最常見的分類需求把評論按情感分正負(fù)、把工單自動派給對應(yīng)團(tuán)隊、把客服投訴自動打標(biāo)簽。適合誰用做畢設(shè)的學(xué)生、接二手項目的新人以及想在真實數(shù)據(jù)上驗證深度學(xué)習(xí)文本分類那套流程的工程師。一個常見誤判是拿到 zip 就直接解壓跑訓(xùn)練結(jié)果被編碼、依賴版本和樣本劃分問題卡住半天。這篇文章從解壓開始把“把壓縮包變成一套能復(fù)現(xiàn)、能調(diào)參、能交付的文本分類系統(tǒng)”的完整路徑講清楚。2. 解壓前的盤點和環(huán)境準(zhǔn)備先看清 zip 包里裝的什么提示zip 文件名不帶版本號時先翻 README再動代碼。2.1 壓縮包內(nèi)部該有什么從 README 找入口我拿到這種 zip 的第一件事不是解壓是先看壓縮包列表。常見做法是用unzip -l列目錄先知道里面有幾層文件夾、有沒有 README、數(shù)據(jù)文件用什么后綴。這類包的目錄結(jié)構(gòu)大概率長成下面這張表這樣路徑常見內(nèi)容需要確認(rèn)的點README.md運(yùn)行環(huán)境、入口命令描述和實際代碼是否一致requirements.txtPython 依賴版本是否鎖定、torch 是否在里面data/訓(xùn)練/驗證/測試數(shù)據(jù)分隔符、編碼、標(biāo)簽取值src/train.py、model.py、utils.py入口文件到底叫什么models/訓(xùn)練好的權(quán)重用什么框架保存的logs/訓(xùn)練日志有沒有跑通過的記錄先執(zhí)行下面命令把包里的內(nèi)容列出來cd 基于深度學(xué)習(xí)的文本分類 unzip -l 基于深度學(xué)習(xí)的文本分類.zip | head -30unzip -l不實際解壓只讀 zip 的中央目錄輸出每一行的文件名、壓縮前大小、壓縮方式。head -30是限制只打印前 30 行防止日志文件把列表刷屏。這一步能確認(rèn)兩件事頂層是不是套了一個多余的文件夾以及 requirements.txt 用的什么格式。頂部多套一層目錄在項目交付里很常見如果解壓后直接找路徑容易找不到入口。2.2 用 conda 建一個干凈的深度學(xué)習(xí)環(huán)境Python 版本與 PyTorch 匹配環(huán)境配置的坑通常不在 torch 本身在于你裝了一堆全局包后版本互相打架。我一般不會用系統(tǒng) Python 直接裝而是用 conda 開一個干凈環(huán)境把 Python 版本固定下來。conda create -n text-cls python3.9 -y conda activate text-cls # 先把 PyTorch 裝好再裝其他依賴 pip install torch --index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple為什么先把 torch 裝好再裝 requirements因為很多 zip 里的 requirements.txt 寫了torch1.10.0這種舊版本如果你按順序直接裝可能裝上老版本和后面要用的 transformers、torchtext 對不上。先不指定版本裝 torch保證拿到當(dāng)前 Python 3.9 下能用的穩(wěn)定版再讓其余依賴去適配它。python3.9是我建議的中間值3.8 太老新版本的一些庫已經(jīng)放棄3.10 雖然沒問題但個別老項目里 torch 的擴(kuò)展可能編譯不過。如果你機(jī)器的 GPU 驅(qū)動已經(jīng)裝好先跑nvidia-smi看一眼支持的 CUDA 版本再按 PyTorch 官網(wǎng)給出的對應(yīng)命令重新裝 GPU 版。CPU 版先把鏈路跑通后續(xù)換 GPU 版不用改業(yè)務(wù)代碼只重裝 torch 就行。2.3 解壓就翻車zip 偽加密、CRLF 換行、目錄名不一致交付 zip 最容易翻車的第一個點是“偽加密”。表現(xiàn)是解壓到一半彈窗要密碼但你根本沒有密碼。真加密的文件必須拿到密碼才能解偽加密只是壓縮工具把加密標(biāo)志位改了文件內(nèi)容本身沒加密。這時候去搜“zip 密碼移除”多半白費勁先確認(rèn)是不是偽加密再決定下一步。用一段 Python 就能識別import zipfile with zipfile.ZipFile(基于深度學(xué)習(xí)的文本分類.zip) as z: for info in z.infolist(): print(info.filename, info.file_size, info.compress_type, encrypted if info.flag_bits 0x1 else plain)flag_bits是 zip 文件條目的通用位標(biāo)志第 0 位是 1 表示加密。如果打印出來大部分條目都顯示 encrypted但交付方明確說沒設(shè)密碼那基本就是偽加密。用 7-Zip 這類工具解壓時點“確定”跳過密碼通常就能解開。當(dāng)然如果確實帶密碼直接找交付方要比暴力破解靠譜得多。第二個坑是換行符 CRLF。Windows 下編輯過的腳本文件行尾是\r\n放到 Linux 或 macOS 上跑會報出類似python3\r: No such file or directory的錯誤。先用file src/train.py看輸出里有沒有 CRLF 字樣再用下面命令批量轉(zhuǎn)掉sed -i s/\r$// src/train.pysed的替換規(guī)則把每行結(jié)尾的\r去掉-i原地修改跑完再用file確認(rèn)變成 LF。如果 zip 里文件多可以在解壓后對整個目錄跑find . -name *.py -exec sed -i s/\r$// {} \;。第三個坑是頂層目錄名不一致。zip 包叫文本分類解壓出來的文件夾可能叫TextCls_v3你按 zip 名去找路徑會撲空。解壓后先ls -la看一下頂層再用mv修正成自己習(xí)慣的名字避免后面所有絕對路徑都跟著錯。3. 文本數(shù)據(jù)長什么樣清洗、分詞與標(biāo)簽映射3.1 CSV / JSON / TXT三種常見存放格式與讀取代碼文本分類項目里數(shù)據(jù)格式是第一個分叉口。同一個 zip 包里常見三種情況CSV 按列存、JSONL 逐行存、TXT 用分隔符切。我習(xí)慣寫一個統(tǒng)一讀取函數(shù)避免每個腳本各讀一遍import pandas as pd import json def load_dataset(path): if path.endswith(.csv): df pd.read_csv(path, encodingutf-8-sig) return df[text].tolist(), df[label].tolist() if path.endswith(.jsonl): text, label [], [] with open(path, encodingutf-8) as f: for line in f: obj json.loads(line) text.append(obj[text]) label.append(obj[label]) return text, label if path.endswith(.txt): labels, texts [], [] for line in open(path, encodingutf-8): label, text line.strip().split(\t, 1) labels.append(label) texts.append(text) return texts, labelsCSV 用utf-8-sig而不是utf-8是為了把 BOM 頭去掉。Windows 下 Excel 另存的 CSV 常帶 BOM用utf-8讀會讓第一列列名變成\ufefftext后續(xù)按列名取數(shù)直接 KeyError。JSONL 適合大文件逐行解析不會一次性把全量數(shù)據(jù)壓進(jìn)內(nèi)存TXT 按“標(biāo)簽 \t 文本”存是最簡單的交換格式但注意split(\t, 1)里的第二個參數(shù) 1意思是只按第一個 tab 切一刀防止文本里還藏著 tab。3.2 中文分詞選 jieba 的精確模式還是全模式兩個關(guān)鍵參數(shù)中文文本和英文不一樣詞之間沒有空格所以要分詞。這個 zip 里如果處理的是商品評論、新聞稿、工單描述常見做法是直接上 jieba。我給的默認(rèn)配置是精確模式import jieba import re STOPWORDS set(的 了 是 在 我 你 他 它.split()) def tokenize(text, max_len128): text re.sub(r\s, , text) words jieba.lcut(text, cut_allFalse) words [w for w in words if w not in STOPWORDS] return words[:max_len]cut_all是 jieba 最關(guān)鍵的參數(shù)。cut_allFalse是精確模式切出來的詞最接近自然語義比如“研究生命科學(xué)”會切成“研究/生命科學(xué)”cut_allTrue是全模式會把所有可能的詞都切出來變成“研究/研究生/生命/生命科學(xué)/科學(xué)”召回高但噪聲大文本分類場景基本不用全模式。另一個參數(shù)HMM默認(rèn)是 True它負(fù)責(zé)識別詞典里沒有的新詞在醫(yī)療、法律這類專業(yè)文本里建議保持開啟。max_len128用來截斷長文本。這個值不是越大越好因為文本分類任務(wù)里決定性特征通常分布在開頭和結(jié)尾。BERT 類模型最長支持 512TextCNN 用 128 已經(jīng)覆蓋大部分場景。去停用詞這步看起來簡單但我建議第一版先別做太狠只去掉高頻無意義的虛詞因為有些情感詞恰好是短詞比如“服了”“絕了”去多了反而丟信息。3.3 類別不均衡過采樣、欠采樣與 class_weight文本分類里“投訴”可能只占 1%如果直接訓(xùn)練模型學(xué)到的就是把所有文本都預(yù)測成“正?!币驗檫@樣準(zhǔn)確率也有 99%。這個 zip 如果沒給你預(yù)處理好的均衡樣本你就要自己處理。常見做法三選一class_weight 加權(quán)、過采樣少數(shù)類、欠采樣多數(shù)類。我一般先用 class_weight因為它不動數(shù)據(jù)分布from sklearn.utils.class_weight import compute_class_weight import numpy as np classes np.array(sorted(set(labels))) weights compute_class_weight(class_weightbalanced, classesclasses, ynp.array(labels)) print(dict(zip(classes, weights)))class_weightbalanced會自動按類別頻率反比算權(quán)重少數(shù)類樣本少權(quán)重就大。后續(xù)把weights轉(zhuǎn)成 tensor 傳給CrossEntropyLoss(weight...)少數(shù)類的 loss 會被放大梯度更新的力度也更大。注意這個方案只在單標(biāo)簽多分類下直接用如果是多標(biāo)簽分類——一篇文章同時屬于“科技”和“政策”——不能用 softmax 加 CrossEntropyLoss要換成 sigmoid 加BCEWithLogitsLoss此時類別不均衡要按每個標(biāo)簽單獨算正負(fù)樣本比例。如果用采樣器做過采樣from torch.utils.data import WeightedRandomSampler label_ids [label2id[l] for l in labels] counts np.bincount(label_ids) sample_weights 1.0 / counts[label_ids] sampler WeightedRandomSampler(sample_weights, num_sampleslen(labels), replacementTrue)replacementTrue表示允許重復(fù)采樣同一個樣本少數(shù)類會在每個 epoch 被多次抽到。過采樣和 class_weight 建議二選一兩個疊加會讓少數(shù)類梯度更新過猛容易過擬合。4. 從 FastText 到 TextCNN 再到 BERT三個基線怎么選4.1 FastText先把數(shù)據(jù)鏈路驗證通再訓(xùn)練模型文本分類項目拿到手最快驗證數(shù)據(jù)鏈路的方式不是直接上 BERT而是 FastText。它訓(xùn)練快、占用內(nèi)存低還能在訓(xùn)練過程中順便學(xué)到子詞信息對中文這種詞形變化不明顯的語言來說是一個夠用的基線。我用 gensim 版本比較多因為和 sklearn、pandas 配合沒有編譯負(fù)擔(dān)from gensim.models import FastText sentences [jieba.lcut(t) for t in texts] model FastText(sentences, vector_size128, window5, min_count2, workers4, epochs20) def encode(text): words [w for w in jieba.lcut(text) if w in model.wv] return np.mean([model.wv[w] for w in words], axis0) if words else np.zeros(128)邏輯上FastText 的每一條詞向量由內(nèi)部的字符 n-gram 疊加而成詞典外的詞也能通過子詞組合得到近似向量這是它對中文未登錄詞兜底的原因。encode函數(shù)把一句話里每個詞的向量取平均得到句子向量然后喂給 sklearn 的邏輯回歸分類器。取平均會丟掉詞序信息所以它上限不高但作為第一個版本已經(jīng)足夠驗證整條數(shù)據(jù)鏈路是不是通的。參數(shù)上vector_size128在幾萬到幾十萬條文本規(guī)模下夠用沒必要一上來就 300min_count2表示出現(xiàn)次數(shù)低于 2 的詞直接丟棄太低會引入噪聲window5是上下文窗口短文本場景可以降到 3。如果你的目標(biāo)是先拿指標(biāo)可以把編碼后的向量交給邏輯回歸等這個分?jǐn)?shù)出來了再決定要不要上更復(fù)雜的深度模型。很多 case 里邏輯回歸的 F1 已經(jīng)很高說明特征線性可分后續(xù)深度模型的增量有限。4.2 TextCNNPyTorch 實現(xiàn)的核心代碼與調(diào)參要點TextCNN 是文本分類里最穩(wěn)的深度模型參數(shù)少、訓(xùn)練快在中小規(guī)模數(shù)據(jù)上不容易過擬合。它和 FastText 的本質(zhì)區(qū)別是會保留詞序用多組卷積核提取局部短語特征。下面是最小可跑的模型定義import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_filters100, filter_sizes(2, 3, 4), num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, size) for size in filter_sizes ]) self.dropout nn.Dropout(dropout) self.fc nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): x self.embedding(x) # (B, L, E) x x.transpose(1, 2) # (B, E, L) pooled [] for conv in self.convs: h torch.relu(conv(x)) # (B, F, L-k1) pooled.append(h.max(dim2).values) x torch.cat(pooled, dim1) x self.dropout(x) return self.fc(x)forward 里發(fā)生了什么embedding 把每個 token 映射成稠密向量得到(B, L, E)transpose(1, 2)把維度換成(B, E, L)因為 Conv1d 期望的輸入是“通道維在第二維”每個卷積核在序列方向滑動提取對應(yīng) n-gram 模式max(dim2)是全局最大池化取每個特征圖最大的激活值相當(dāng)于“這條文本里最強(qiáng)烈的特征信號”。最后把多個卷積核的輸出拼起來過一層全連接。參數(shù)上filter_sizes(2, 3, 4)對應(yīng) 2-gram、3-gram、4-gram讓模型同時看不同長度的短語num_filters100是每組卷積核的輸出通道數(shù)越大擬合能力越強(qiáng)但也更容易過擬合padding_idx0保證 padding 位置不參與 embedding 更新。訓(xùn)練循環(huán)里有兩個點值得單獨說明criterion nn.CrossEntropyLoss(weightclass_weight_tensor) optimizer torch.optim.AdamW(model.parameters(), lr1e-3) for epoch in range(15): model.train() for x, y in train_loader: logits model(x) loss criterion(logits, y) optimizer.zero_grad() loss.backward() optimizer.step()CrossEntropyLoss的weight參數(shù)直接接上一章算出的類別權(quán)重少數(shù)類每個 batch 的損失被放大。優(yōu)化器用AdamW而不是Adam因為 AdamW 把權(quán)重衰減從梯度更新中解耦在文本分類里普遍更穩(wěn)。lr1e-3是隨機(jī)初始化模型的標(biāo)準(zhǔn)起點如果你的 loss 曲線在前 5 個 epoch 里震蕩降到3e-4重跑不是一個玄學(xué)問題是學(xué)習(xí)率超過了這個數(shù)據(jù)集的合理范圍。4.3 BERT 微調(diào)什么時候才值得上預(yù)訓(xùn)練模型BERT 這類預(yù)訓(xùn)練模型在文本分類上的效果上限高但它有成本顯存占用大、推理慢、訓(xùn)練時間以小時計。用 transformers 庫微調(diào)的代碼很短from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 ) trainer Trainer( modelmodel, argsTrainingArguments( output_dir./bert-out, learning_rate2e-5, per_device_train_batch_size16, num_train_epochs3, warmup_ratio0.1, evaluation_strategyepoch, ), train_datasettrain_ds, eval_datasetval_ds, ) trainer.train()learning_rate2e-5是預(yù)訓(xùn)練權(quán)重微調(diào)的標(biāo)準(zhǔn)起點比隨機(jī)初始化模型的 1e-3 小兩個數(shù)量級。原因是 BERT 的權(quán)重已經(jīng)收斂過了學(xué)習(xí)率太大會把學(xué)到的基礎(chǔ)語言表示沖刷掉。warmup_ratio0.1的意思是前 10% 的訓(xùn)練步數(shù)把學(xué)習(xí)率從 0 線性升到設(shè)定值減少剛開始訓(xùn)練時的震蕩。如果下載bert-base-chinese太慢可以換成 ModelScope 上對應(yīng)的中文模型路徑模型結(jié)構(gòu)一樣代碼幾乎不用改。什么時候不值得上 BERT訓(xùn)練集不足 5000 條時BERT 很容易過擬合到訓(xùn)練集上驗證集指標(biāo)反而不如 TextCNN 加早停推理延遲要求控制在幾百毫秒以內(nèi)時BERT 的參數(shù)量也未必劃算。嚴(yán)格說LLM 也屬于深度學(xué)習(xí)但文本分類這種任務(wù)沒必要一上來就用最貴的模型。我的判斷順序是FastText 拿基線TextCNN 看有沒有提升空間BERT 只在數(shù)據(jù)量和預(yù)算都撐得住的時候才出手。5. 避坑記錄這個 zip 里最常見的 4 個翻車現(xiàn)場5.1 中文亂碼編碼探測與統(tǒng)一轉(zhuǎn) UTF-8現(xiàn)象pd.read_csv讀完中文全是亂碼或者直接報UnicodeDecodeError代碼文件在 Linux 下跑起來后中文注釋變成一片亂碼。原因Excel 另存的 CSV 默認(rèn)是 GBK而 pandas 在 Linux 下默認(rèn)按 UTF-8 解碼反過來如果文件真的是 UTF-8 但帶了 BOM有些解析器會把 BOM 當(dāng)成字符讀進(jìn)去。解決先探測編碼再決定用什么參數(shù)讀文件。import chardet with open(path, rb) as f: raw f.read(10000) enc chardet.detect(raw)[encoding] print(enc) df pd.read_csv(path, encodingenc if enc else utf-8)chardet.detect會掃描字節(jié)統(tǒng)計給出最可能的編碼但它在樣本太小時會誤判所以只取前 10000 字節(jié)就夠。拿到編碼后我一般會順手把文件重存成 UTF-8后續(xù)所有腳本統(tǒng)一用encodingutf-8避免每個文件都要探測一次。記住給 Python 源碼加# -*- coding: gbk -*-只代表解釋器按 GBK 讀源碼不代表運(yùn)行時文件讀寫會自動轉(zhuǎn)碼。5.2 訓(xùn)練集驗證集重疊虛假的高準(zhǔn)確率現(xiàn)象訓(xùn)練集 F1 接近 0.99驗證集一跑掉到 0.6線上效果更差整個過程表現(xiàn)出典型的“訓(xùn)練越好驗證越差”。原因數(shù)據(jù)劃分用了隨機(jī)切分但沒去重。同一個文本被復(fù)制多次或者同一用戶的文本同時分到訓(xùn)練集和驗證集。深度模型有很強(qiáng)的記憶能力同一句話在訓(xùn)練集見過了驗證集再遇到就直接輸出“正確”答案。解決先按文本內(nèi)容去重再去切分。如果樣本還帶用戶 ID考慮按用戶分組切分。df df.drop_duplicates(subset[text], keepfirst) df df.sample(frac1, random_state42) train df.iloc[:int(0.8 * len(df))] val df.iloc[int(0.8 * len(df)):]drop_duplicates(subset[text])只按 text 列去重keepfirst保留第一次出現(xiàn)的那條。random_state42固定隨機(jī)種子保證每次跑出來的劃分一致。如果數(shù)據(jù)有時間屬性比如工單按日期到達(dá)用時間切分比隨機(jī)切分更接近真實上線場景因為模型永遠(yuǎn)只能拿過去預(yù)測未來。這個坑在別人交付的 zip 里幾乎必然存在跑通第一版后第一件事就是檢查它。5.3 小樣本訓(xùn)飛了學(xué)習(xí)率、warmup 和模型復(fù)雜度現(xiàn)象loss 反復(fù)橫跳準(zhǔn)確率卡在多數(shù)類占比上驗證 F1 只有零點幾。原因數(shù)據(jù)量太小時模型直接“背題”學(xué)習(xí)率過大讓梯度更新不穩(wěn)定隨機(jī)初始化的 embedding 在前幾個 epoch 里梯度噪聲很大甚至把原有的語義信息沖掉。解決先把學(xué)習(xí)率降到1e-4或3e-5再凍結(jié) embedding 層讓模型先學(xué)卷積層。for param in model.embedding.parameters(): param.requires_grad False這段代碼把 embedding 參數(shù)設(shè)為不更新。前幾個 epoch 里模型只能調(diào)整卷積層和全連接層的參數(shù)embedding 保持初始狀態(tài)梯度噪聲大幅減少。等 loss 曲線開始平滑下降再把requires_grad改回 True對全模型微調(diào)。如果你嫌這個操作麻煩還有一個更樸素的選擇直接把詞表截到 top 20000去掉長尾詞模型要學(xué)的參數(shù)一下就少了很多。小樣本還有一個反直覺的坑模型復(fù)雜度越高驗證集指標(biāo)越差。如果 TextCNN 都過擬合換 BERT 只會更糟。先加早停、加 dropout再考慮換模型。5.4 GPU 顯存不足batch size 與梯度累積現(xiàn)象訓(xùn)練到第二個 batch 直接報CUDA out of memory調(diào)用torch.cuda.empty_cache()也沒用。原因顯存被 batch 里最長的那條序列占滿empty_cache()只能釋放緩存塊不能降低單次 forward 的峰值顯存。文本分類的數(shù)據(jù)長尾分布很明顯一條 5000 字的樣本會把整個 batch 的顯存頂爆。解決梯度累積用時間換顯存。accumulation_steps 4 optimizer.zero_grad() for i, (x, y) in enumerate(train_loader): loss criterion(model(x), y) loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()accumulation_steps4的意思是每 4 個 batch 才更新一次參數(shù)等效于把 batch size 放大 4 倍。注意loss要除以累積步數(shù)否則每個 batch 的梯度都被放大 4 倍學(xué)習(xí)率就得跟著降。如果梯度累積到 8 還爆那就直接做序列截斷把max_len壓到 64。文本分類的關(guān)鍵特征往往在開頭和結(jié)尾中間大段內(nèi)容對分類貢獻(xiàn)有限短文本截斷對指標(biāo)的傷害通常遠(yuǎn)小于顯存不足帶來的折騰。6. 驗證模型的通用做法混淆矩陣、宏平均 F1 與壞樣本復(fù)盤6.1 一次跑全混淆矩陣與分類報告文本分類的評估不能只看 accuracy。類別不平衡時accuracy 會被多數(shù)類帶著走模型全預(yù)測成“正?!币材苣?0.9 的準(zhǔn)確率。我每跑完一個模型都會固定輸出這兩行from sklearn.metrics import classification_report, confusion_matrix preds [] model.eval() with torch.no_grad(): for x in test_loader: preds.extend(model(x).argmax(dim1).tolist()) print(classification_report(test_y, preds, digits4)) print(confusion_matrix(test_y, preds))classification_report里最值得看的是 macro F1它把每個類別的 F1 單獨算出來再平均不受多數(shù)類影響能真實反映少數(shù)類到底被分對了多少。confusion_matrix能看出兩個類別總被混淆——比如“投訴”和“咨詢”邊界模糊它們互相錯分的數(shù)值會一眼暴露。6.2 壞樣本復(fù)盤比調(diào)參更重要指標(biāo)看完我會把錯分樣本導(dǎo)出來import pandas as pd bad pd.DataFrame({text: test_texts, true: test_y, pred: preds}) bad bad[bad[true] ! bad[pred]] bad.to_csv(bad_cases.csv, indexFalse, encodingutf-8-sig)encodingutf-8-sig保證 Excel 打開這個 CSV 時中文正常。逐條看壞樣本通常只有兩種結(jié)果標(biāo)簽本身標(biāo)錯了或者文本確實有歧義。前者說明是數(shù)據(jù)質(zhì)量問題修數(shù)據(jù)比重訓(xùn)模型更有用后者才是模型真正該優(yōu)化的方向。大部分團(tuán)隊在這個 zip 上花的時間不是訓(xùn)練而是被數(shù)據(jù)問題和評估方式誤導(dǎo)繞了一大圈。我現(xiàn)在拿到任何文本分類項目第一件事一定是先跑基線、看壞樣本確認(rèn)數(shù)據(jù)沒問題再談深度學(xué)習(xí)算法。真正把模型往前推的往往是那句“標(biāo)簽定義不對齊”而不是誰的神經(jīng)網(wǎng)絡(luò)更復(fù)雜。這個 zip 跑通了后面的路就順暢了。希望幫到你。本文還有配套的精品資源點擊獲取