習(xí)文本分類實(shí)戰(zhàn):從數(shù)據(jù)預(yù)處理到模型部署全解析)
簡介面向希望入門自然語言處理的學(xué)生和開發(fā)者這份資源基于深度學(xué)習(xí)完成文本分類任務(wù)覆蓋從文本預(yù)處理、詞嵌入到模型構(gòu)建、訓(xùn)練與評估的完整項(xiàng)目流程。壓縮包共6個文件全部為Python腳本整體僅11KB分別承擔(dān)數(shù)據(jù)加載、TextCNN/TextRNN模型定義、訓(xùn)練與預(yù)測等功能代碼結(jié)構(gòu)緊湊適合配合入門教程邊讀邊練。資源已有224人學(xué)習(xí)借助訓(xùn)練與預(yù)測腳本可直接對比卷積網(wǎng)絡(luò)與循環(huán)網(wǎng)絡(luò)在文本分類中的表現(xiàn)并快速驗(yàn)證新樣本。項(xiàng)目實(shí)踐中的預(yù)處理環(huán)節(jié)涉及分詞、去停用詞等操作模型訓(xùn)練則采用交叉熵?fù)p失與準(zhǔn)確率、F1等評估指標(biāo)代碼可作為理解反向傳播、梯度下降與注意力機(jī)制的直觀示例。除基礎(chǔ)模型外資源還涵蓋預(yù)訓(xùn)練模型如BERT的微調(diào)思路有助于進(jìn)一步擴(kuò)展實(shí)驗(yàn)適合作為課程作業(yè)、入門實(shí)踐或論文復(fù)現(xiàn)的參考基礎(chǔ)。1. 這個 zip 里裝的不是代碼是一整套文本分類的落地思路搜索「基于深度學(xué)習(xí)的文本分類.zip」的人大多不是來找論文的而是想看看能不能直接解壓、跑通、改改參數(shù)用在自己的場景里。這份壓縮包如果整理得規(guī)范里面應(yīng)該是一個完整工程訓(xùn)練腳本、模型定義、數(shù)據(jù)集樣本、README 和依賴清單。深度學(xué)習(xí)文本分類的入門門檻不在模型本身而在數(shù)據(jù)預(yù)處理、訓(xùn)練流程和踩坑排查這三件事上恰好這些也是 zip 里最容易亂的部分。這篇筆記按「包有什么 → 數(shù)據(jù)怎么預(yù)處理 → 模型怎么選怎么練 → 參數(shù)怎么調(diào) → 坑在哪 → 怎么落地部署」的順序把整個鏈路拆開講適合準(zhǔn)備做輿情分類、工單自動打標(biāo)、評論情感判定的從業(yè)者照著復(fù)現(xiàn)。2. 從壓縮包到能跑的模型環(huán)境準(zhǔn)備與數(shù)據(jù)預(yù)處理2.1 解壓后的目錄結(jié)構(gòu)先確認(rèn)包里有什么再動手拿到 zip 后第一步不是急著跑訓(xùn)練腳本而是把目錄結(jié)構(gòu)摸清楚。常見的深度學(xué)習(xí)文本分類項(xiàng)目會分成這幾個部分data/放原始數(shù)據(jù)和預(yù)處理腳本models/放網(wǎng)絡(luò)結(jié)構(gòu)定義train.py是訓(xùn)練入口predict.py是推理入口requirements.txt列依賴庫。有的包還會帶config.py或config.yaml所有超參數(shù)集中在這里方便調(diào)參。先看依賴清單是不是完整。比較省事的做法是建一個干凈的虛擬環(huán)境再安裝依賴避免把系統(tǒng) Python 環(huán)境搞亂。在 Linux 或 macOS 下用python3 -m venvWindows 下同理只是路徑分隔符不同。裝依賴時注意 PyTorch 的版本要和本機(jī) CUDA 匹配如果requirements.txt里寫的是torch1.13.1但你的顯卡驅(qū)動只支持 CUDA 11.7那就要手動指定對應(yīng)版本否則后面訓(xùn)練時會報 CUDA 不可用的錯。# 創(chuàng)建虛擬環(huán)境并激活Windows 去掉 source 前綴 python3 -m venv text_cls_env source text_cls_env/bin/activate pip install -r requirements.txt提示如果requirements.txt不存在先裝上五個基礎(chǔ)庫再逐個補(bǔ)numpy、pandas、scikit-learn、torch、transformers。跑起來缺什么補(bǔ)什么比一次性裝全更省心。依賴裝完后先跑一句python -c import torch; print(torch.__version__)確認(rèn) PyTorch 能正常導(dǎo)入。很多所謂的「環(huán)境問題」其實(shí)只是 conda 和 venv 的 Python 路徑互相干擾進(jìn)入虛擬環(huán)境后用which python看一眼解釋器路徑是不是在虛擬環(huán)境里。2.2 數(shù)據(jù)預(yù)處理清洗、分詞、標(biāo)簽編碼與訓(xùn)練/驗(yàn)證集劃分文本分類的數(shù)據(jù)預(yù)處理決定了模型上限。即便是同一個模型預(yù)處理方式不同效果能差出三到五個百分點(diǎn)。核心步驟是四件事清洗、分詞、標(biāo)簽編碼、數(shù)據(jù)集劃分。清洗這步中英文場景差別很大。英文要處理大小寫和詞形還原中文則需要考慮繁簡轉(zhuǎn)換和全半角歸一。比較普適的是把 URL、郵箱、連續(xù)數(shù)字替換成特殊占位符因?yàn)檫@些 token 不會出現(xiàn)在預(yù)測數(shù)據(jù)里直接刪掉反而會切斷語義。常見做法是保留業(yè)務(wù)中出現(xiàn)頻率最高的那部分符號其余統(tǒng)一替換。分詞策略直接決定詞表大小和 OOVout-of-vocabulary未登錄詞比例。中文用jieba是默認(rèn)選擇但詞典需要按業(yè)務(wù)定制。做財經(jīng)輿情分類時「降準(zhǔn)」「北向資金」這種詞 jieba 默認(rèn)詞庫里沒有就會被拆成「降」和「準(zhǔn)」模型學(xué)到的是錯誤語義。建議在用 jieba 分詞前把業(yè)務(wù)詞表加載進(jìn)去并且開啟HMM參數(shù)來處理新詞發(fā)現(xiàn)。import jieba import pandas as pd from sklearn.model_selection import train_test_split jieba.set_dictionary(data/jieba_dict.txt) jieba.load_userdict(data/biz_words.txt) # 業(yè)務(wù)詞表 def clean_text(text): 清洗去HTML標(biāo)簽、統(tǒng)一空白、保留中文英文數(shù)字 import re text re.sub(r.*?, , text) text re.sub(r\s, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return text.strip() def tokenize(text): return [w for w in jieba.cut(clean_text(text), HMMTrue) if w.strip()] df pd.read_csv(data/raw.csv, header0) df[title] df[title].apply(tokenize) # 按標(biāo)簽分層劃分保證訓(xùn)練/驗(yàn)證集的類別分布一致 X_train, X_val, y_train, y_val train_test_split( df[title], df[label], test_size0.2, stratifydf[label], random_state42 )這段代碼里最容易忽略的是stratify參數(shù)。如果原始數(shù)據(jù)里正樣本只占 10%不做分層抽樣訓(xùn)練集可能只有 5% 的正樣本驗(yàn)證集卻有 20%模型訓(xùn)練時看到的正樣本分布波動很大直接表現(xiàn)為 F1 分?jǐn)?shù)忽高忽低。random_state42是固定隨機(jī)種子方便復(fù)現(xiàn)——同一份數(shù)據(jù)每次跑出來的數(shù)應(yīng)該完全一致不一致說明有隱含的隨機(jī)因素沒被固定。2.3 詞向量與序列填充模型輸入前最后一步分詞完成后下一步是構(gòu)建詞表并把文本轉(zhuǎn)成索引序列。這個環(huán)節(jié)有三個關(guān)鍵決策詞表大小上限、序列最大長度、OOV 詞的處理策略。詞表上限通常設(shè)在 5 萬到 10 萬之間。詞表設(shè)太大低頻詞太多模型會在訓(xùn)練時嚴(yán)重過擬合這些出現(xiàn)一兩次的詞設(shè)太小OOV 比例升高句子信息損失嚴(yán)重。比較穩(wěn)妥的做法是統(tǒng)計(jì)訓(xùn)練集詞頻保留出現(xiàn)次數(shù) top 5 萬的詞詞頻為 1 的詞直接標(biāo)為 OOV。序列長度選擇上既看業(yè)務(wù)也看模型。做短文本分類標(biāo)題、評論長度在 64 到 128 之間就覆蓋了絕大多數(shù)場景。做長文本分類工單描述、公告全文可以到 512。但序列變長TextCNN 和 Transformer 的顯存占用會呈線性到平方級增長不要一上來就設(shè) 512先用 128 跑通看長度分布再調(diào)。from collections import Counter import torch from torch.utils.data import Dataset, DataLoader MAX_VOCAB_SIZE 50000 MAX_SEQ_LEN 128 def build_vocab(tokenized_texts): freq Counter(w for tokens in tokenized_texts for w in tokens) vocab {w: i2 for i, (w, c) in enumerate(freq.most_common(MAX_VOCAB_SIZE))} vocab[PAD] 0 vocab[UNK] 1 return vocab def encode(tokens, vocab): return [vocab.get(w, vocab[UNK]) for w in tokens] def pad_sequence(ids, max_lenMAX_SEQ_LEN): if len(ids) max_len: return ids[:max_len] return ids [0] * (max_len - len(ids)) class TextDataset(Dataset): def __init__(self, tokenized_texts, labels, vocab): self.data [torch.tensor(pad_sequence(encode(t, vocab))) for t in tokenized_texts] self.labels torch.tensor(labels, dtypetorch.long) def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx], self.labels[idx] vocab build_vocab(X_train) train_ds TextDataset(X_train.tolist(), y_train.tolist(), vocab) val_ds TextDataset(X_val.tolist(), y_val.tolist(), vocab) train_loader DataLoader(train_ds, batch_size64, shuffleTrue)這里PAD和UNK分別占了索引 0 和 1從 2 開始才是真正的詞索引。有個別實(shí)現(xiàn)會從 1 開始編號把 0 留給 PAD但 UNK 就沒位置了會導(dǎo)致 OOV 詞直接被編碼成 PAD模型學(xué)不到「這個詞不在詞表里」這個信號。處理類別時用torch.long保證和 CrossEntropyLoss 的輸入類型匹配。注意build_vocab只應(yīng)該在訓(xùn)練集上執(zhí)行。如果先在整個數(shù)據(jù)集上建詞表再做劃分詞表里會混入驗(yàn)證集和測試集的詞頻信息這就是典型的數(shù)據(jù)泄露后面會專門展開講。3. 模型選型TextCNN、RNN 還是 BERT你的數(shù)據(jù)量說了算3.1 三種模型的原理差異和適用場景文本分類的模型選型本質(zhì)是「你有多少數(shù)據(jù)」和「你的推理延遲預(yù)算多少」之間的權(quán)衡。TextCNN 用多個尺寸的卷積核提取 n-gram 級別的局部特征計(jì)算量小、訓(xùn)練快適合數(shù)據(jù)量在幾萬到幾十萬條、延遲要求高的場景。RNN包括 LSTM、GRU按時間步處理序列能建模長距離依賴但訓(xùn)練速度慢且難以并行適合序列長度較長且數(shù)據(jù)量中等的場景。BERT 這類預(yù)訓(xùn)練模型通過大規(guī)模語料預(yù)訓(xùn)練獲得語義表示在小樣本幾千條條件下表現(xiàn)遠(yuǎn)好于前兩者但推理慢、顯存占用高。有個反直覺的結(jié)論數(shù)據(jù)量超過 50 萬條時TextCNN 微調(diào)后的效果不一定比 BERT 差多少。因?yàn)?CNN 的歸納偏置是局部窗口對詞序不敏感但從數(shù)據(jù)中學(xué)習(xí)的效率更高。數(shù)據(jù)量小的時候預(yù)訓(xùn)練模型大幅領(lǐng)先因?yàn)槟銢]有足夠的數(shù)據(jù)讓模型從零學(xué)會語義。3.2 用 PyTorch 實(shí)現(xiàn)一個 TextCNN 基線TextCNN 是最適合做基線的模型。結(jié)構(gòu)清晰、訓(xùn)練快、調(diào)參空間明確。核心思路是用多個不同寬度的卷積核并行提取 n-gram 特征然后做全局池化接全連接層分類。import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_filters256, filter_sizes(3, 4, 5), num_classes10, dropout0.5, pad_idx0): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idxpad_idx) self.convs nn.ModuleList() for size in filter_sizes: # 每個卷積核尺寸對應(yīng)一個 Conv2d 分支 self.convs.append(nn.Conv2d( in_channels1, out_channelsnum_filters, kernel_size(size, embed_dim) )) self.fc nn.Linear(num_filters * len(filter_sizes), num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): # x: (batch, seq_len) emb self.embedding(x) # (batch, seq_len, embed_dim) emb emb.unsqueeze(1) # 加通道維變成 (batch, 1, seq_len, embed_dim) conv_outputs [] for conv in self.convs: c conv(emb) # (batch, num_filters, conv_seq_len, 1) c F.relu(c).squeeze(3) # 去掉末尾的 1 c F.max_pool1d(c, c.size(2)).squeeze(2) # 全局最大池化 conv_outputs.append(c) out torch.cat(conv_outputs, dim1) out self.dropout(out) return self.fc(out)這段實(shí)現(xiàn)里有三處邊界細(xì)節(jié)值得說明。embedding指定了padding_idx0PAD 位置在反向傳播時梯度恒為 0不會參與詞向量更新如果不設(shè)置這個參數(shù)PAD 詞會被模型當(dāng)成真實(shí)的共同上下文學(xué)到離譜的噪聲。kernel_size(size, embed_dim)里第二個維度必須等于詞向量維度因?yàn)榫矸e核要在整個 embedding 寬度上滑動不能只覆蓋部分維度。max_pool1d對每個卷積輸出取最大值得到固定長度的向量這樣不管輸入序列多長全連接層的輸入維度都是確定的。3.3 模型訓(xùn)練循環(huán)損失函數(shù)、優(yōu)化器與學(xué)習(xí)率文本分類的損失函數(shù)用交叉熵即可但要注意三個細(xì)節(jié)。第一是類別權(quán)重如果數(shù)據(jù)集類別不平衡給少數(shù)類更高的權(quán)重能顯著提升 F1第二是標(biāo)簽平滑文本分類很容易過擬合標(biāo)簽平滑讓模型不那么自信訓(xùn)練更穩(wěn)第三是優(yōu)化器選擇AdamW 比 Adam 多了權(quán)重衰減的修正微調(diào) BERT 和從頭訓(xùn)練 CNN 都推薦用 AdamW。import torch.optim as optim from torch.nn import CrossEntropyLoss model TextCNN(len(vocab), num_classeslen(set(y_train))) optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) criterion CrossEntropyLoss() model.train() for epoch in range(10): total_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪 optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss: {total_loss/len(train_loader):.4f})clip_grad_norm_是訓(xùn)練穩(wěn)定性法寶。文本分類里最容易出現(xiàn) loss 突然變成 NaN 的情況大部分原因是某個 batch 里出現(xiàn)極端長的序列梯度范數(shù)爆掉。裁剪到 1.0 之后梯度方向不變但長度受限loss 曲線平滑很多。學(xué)習(xí)率的選擇上從頭訓(xùn)練的模型可以用 1e-3 起手BERT 微調(diào)則必須降到 2e-5 到 5e-5 之間——預(yù)訓(xùn)練模型的參數(shù)已經(jīng)很好了學(xué)習(xí)率太大會直接把語義表示沖壞。4. 訓(xùn)練參數(shù)怎么設(shè)從過擬合到欠擬合的排查路徑4.1 必調(diào)的五個參數(shù)及其合理范圍文本分類真正影響結(jié)果的參數(shù)其實(shí)只有五個embedding 維度、卷積核數(shù)量或隱層維度、dropout 率、學(xué)習(xí)率、batch size。embedding 維度一般取 128 或 256太小語義表示能力不足太大帶來過擬合和顯存開銷但收益遞減。conv 核數(shù)量取 256 是常見值和 filter_sizes 的三個尺寸配合后全連接層輸入是 768 維這個寬度足夠表達(dá)但不會過度參化。dropout 通常 0.3 到 0.6 之間。數(shù)據(jù)量大可以降到 0.3數(shù)據(jù)量小應(yīng)該升到 0.5 以上。一個比較實(shí)用的判斷方法看訓(xùn)練集和驗(yàn)證集的 loss 曲線。驗(yàn)證集 loss 開始回升而訓(xùn)練集還在下降說明過擬合加大 dropout 或減小模型復(fù)雜度兩邊都高說明欠擬合增大 embedding 維度或加深網(wǎng)絡(luò)。batch size 對最終效果影響不大但影響訓(xùn)練效率和顯存占用。batch size 翻倍學(xué)習(xí)率也應(yīng)翻倍linear scaling rule這是實(shí)踐中經(jīng)常忽略的細(xì)節(jié)。如果 batch size 從 32 調(diào)到 128 但學(xué)習(xí)率還是原來的值收斂會變慢且不穩(wěn)定。4.2 評估指標(biāo)準(zhǔn)確率不夠用F1 才是真尺子文本分類里只打印準(zhǔn)確率是自欺欺人。當(dāng)數(shù)據(jù)集中 90% 是負(fù)樣本時全部預(yù)測為負(fù)類準(zhǔn)確率就是 90%但這個模型毫無價值。真正的評估指標(biāo)要看 macro F1 和每個類別的 precision / recall。macro F1 對類別不平衡更敏感能反映模型在少數(shù)類上的表現(xiàn)。訓(xùn)練代碼里需要添加驗(yàn)證邏輯每輪訓(xùn)練結(jié)束后在驗(yàn)證集上計(jì)算 F1同時記錄最佳模型狀態(tài)。這里最容易踩的坑是「模型截止」的時機(jī)。很多實(shí)現(xiàn)會在驗(yàn)證 F1 連續(xù) N 輪不提升時保存模型但最佳驗(yàn)證 F1 可能出現(xiàn)在第 3 輪之后一直在過擬合。要在訓(xùn)練過程中保存 F1 最高的那一次 checkpoint而不是最后一步的模型。from sklearn.metrics import f1_score, classification_report best_f1 0.0 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch_x, batch_y in val_loader: logits model(batch_x) preds torch.argmax(logits, dim1) all_preds.extend(preds.tolist()) all_labels.extend(batch_y.tolist()) f1 f1_score(all_labels, all_preds, averagemacro) if f1 best_f1: best_f1 f1 torch.save(model.state_dict(), best_model.pt) print(fbest macro F1 updated: {f1:.4f})4.3 早停與模型保存讓訓(xùn)練跑得穩(wěn)早停的 patience 值取 3 到 5。文本分類的驗(yàn)證 F1 曲線波動比較大patience 太小容易誤停太大浪費(fèi)算力。配合 ReduceLROnPlateau 學(xué)習(xí)率調(diào)度效果更好——驗(yàn)證 F1 連續(xù) 2 輪不升就降學(xué)習(xí)率連續(xù) 4 輪不升就提前停。學(xué)習(xí)率調(diào)度器的參數(shù)設(shè)置要謹(jǐn)慎。modemax表示監(jiān)控驗(yàn)證 F1 這樣的指標(biāo)越大越好factor0.5表示學(xué)習(xí)率減半patience2表示容忍 2 輪不提升才降。這套組合在大多數(shù)文本分類工程里都能讓訓(xùn)練過程穩(wěn)定收斂。5. 避坑文本分類最常見的 5 個翻車現(xiàn)場5.1 中文編碼與分詞不一致現(xiàn)象訓(xùn)練時 loss 正常下降但驗(yàn)證集的 F1 驟降或者同一句話預(yù)測結(jié)果和訓(xùn)練時完全不一致還伴隨亂碼。原因訓(xùn)練數(shù)據(jù)讀進(jìn)來的時候是 GBK 編碼預(yù)處理腳本里用的是 UTF-8分詞結(jié)果全是錯亂的「錕斤拷」。另一個常見原因是訓(xùn)練時 jieba 用的是默認(rèn)詞典推理時加載了業(yè)務(wù)詞典兩次分出來的詞序列不一致模型看到的輸入完全不同。解決在數(shù)據(jù)處理入口統(tǒng)一聲明編碼pd.read_csv(..., encodingutf-8)并在讀入后做一次標(biāo)準(zhǔn)化把 jieba 的詞典和 tokenize 邏輯封裝成一個單獨(dú)的模塊訓(xùn)練和推理都調(diào)用同一份代碼永遠(yuǎn)不要在兩處各寫一份。5.2 數(shù)據(jù)泄露標(biāo)簽混進(jìn)了特征現(xiàn)象訓(xùn)練集 F1 高達(dá) 0.99驗(yàn)證集也不錯但上線后效果崩盤。原因預(yù)處理時在整份數(shù)據(jù)集上做了 fit比如 build_vocab驗(yàn)證集和測試集的詞頻信息通過詞表泄露給了模型。更隱蔽的情況是清洗過程中把標(biāo)簽字段當(dāng)作特征輸入了——有些文本數(shù)據(jù)本身包含「已投訴」「已解決」等業(yè)務(wù)狀態(tài)字段這些字段和標(biāo)簽高度相關(guān)但線上預(yù)測時這些字段根本不存在。解決先劃分?jǐn)?shù)據(jù)集再做任何統(tǒng)計(jì)類操作。清理字段時明確區(qū)分輸入特征和標(biāo)簽列建議寫死列名清單防止后續(xù)迭代時新增字段被誤當(dāng)成特征。驗(yàn)證方法是把訓(xùn)練好的模型拿來預(yù)測訓(xùn)練集樣本如果 F1 接近 1.0多半有特征泄露。5.3 類別極端不平衡模型全猜多數(shù)類現(xiàn)象訓(xùn)練 loss 還在下降但少數(shù)類的 recall 是 0精確率也沒意義。原因CrossEntropyLoss 默認(rèn)給每個類別相同的權(quán)重模型學(xué)到的最優(yōu)策略是全猜數(shù)量最多的那個類別。工單分類里「咨詢」類占 85%「投訴」類占 3%模型直接把所有樣本都預(yù)測為「咨詢」宏觀 F1 慘不忍睹。解決給 CrossEntropyLoss 傳入類別權(quán)重。權(quán)重比例按樣本數(shù)倒數(shù)算比如多數(shù)類權(quán)重為 1少數(shù)類權(quán)重為多數(shù)類樣本數(shù)除以少數(shù)類樣本數(shù)。另一個思路是用 Focal Loss讓模型把注意力集中在難分類的樣本上但要注意超參數(shù)調(diào)優(yōu)成本。5.4 解壓失敗或文件損壞現(xiàn)象解壓時提示 CRC 校驗(yàn)失敗、文件缺失或者 README 里提到的文件在目錄里找不到。原因zip 文件在傳輸過程中被截斷或者用的是非標(biāo)準(zhǔn)壓縮工具產(chǎn)生兼容問題。「zip 偽加密」也會導(dǎo)致解壓異?!募募用軜?biāo)志位被改動但實(shí)際內(nèi)容并未加密。解決先驗(yàn)證文件完整性。對比壓縮包附帶的 MD5 或 SHA256 校驗(yàn)值如果哈希對不上就重新下載用 7-Zip 打開看文件列表是否能正常預(yù)覽。偽加密的情況可以先查看壓縮包詳情確認(rèn)加密標(biāo)志位如果確實(shí)無法解壓直接放棄這個文件向作者索要重新打包的版本。5.5 顯存不足與 OOM現(xiàn)象訓(xùn)練跑到第 3 個 epoch 突然報CUDA out of memorybatch size 從 64 降到 32 還是爆。原因絕大多數(shù)情況是序列太長。TextCNN 輸入是三維張量batch size 乘以序列長度乘以詞向量維度再乘參數(shù)規(guī)模決定顯存占用。如果數(shù)據(jù)里存在幾千字的超長文本padding 到統(tǒng)一長度后整批樣本大多是無意義的 PAD 填充。解決先看訓(xùn)練數(shù)據(jù)的序列長度分布用pd.Series([len(t) for t in X_train]).describe()統(tǒng)計(jì) p95 和 p99 長度把 max_seq_len 設(shè)在 p95 附近即可沒必要覆蓋 p100。另一個辦法是梯度累積小 batch 多次前向傳播積累梯度后統(tǒng)一做一次反向更新效果等效于大 batch顯存卻小得多。6. 進(jìn)階從單模型到可部署的文本分類系統(tǒng)6.1 用 ONNX 導(dǎo)出模型做推理驗(yàn)證模型在你本地跑得好不代表能順利進(jìn)服務(wù)。PyTorch 的推理鏈路依賴 Python 運(yùn)行時部署環(huán)境未必裝得了整套依賴。通常做法是把模型導(dǎo)出為 ONNX 格式用 ONNX Runtime 做推理進(jìn)一步還可以轉(zhuǎn)成 TensorRT 在 GPU 上提速。import torch.onnx import onnxruntime as ort model.load_state_dict(torch.load(best_model.pt)) model.eval() dummy_input torch.zeros(1, 128, dtypetorch.long) torch.onnx.export( model, dummy_input, text_cnn.onnx, input_names[input_ids], output_names[logits], dynamic_axes{input_ids: {0: batch}, logits: {0: batch}} ) ort_session ort.InferenceSession(text_cnn.onnx) result ort_session.run( [logits], {input_ids: dummy_input.numpy()} ) print(result[0].shape)導(dǎo)出 ONNX 時最關(guān)鍵的參數(shù)是dynamic_axes。如果不設(shè)置這個參數(shù)導(dǎo)出的模型會固化為固定的 batch size線上接口一次只能預(yù)測一條樣本或固定條數(shù)很不靈活。設(shè)置 batch 為動態(tài)后任意批量都能跑。導(dǎo)出的模型數(shù)值和 PyTorch 原模型可能有一點(diǎn)點(diǎn)浮點(diǎn)誤差需要準(zhǔn)備幾個真實(shí)樣本對比兩者的 softmax 輸出差異偏差在 1e-4 以內(nèi)就能放心上線。6.2 增量訓(xùn)練與版本管理模型上線后最大的問題是數(shù)據(jù)漂移。線上真實(shí)文本的風(fēng)格和訓(xùn)練集存在分布差異剛開始效果還行跑一兩個月后準(zhǔn)確率逐周下降。應(yīng)對方法是建立回流機(jī)制把線上預(yù)測置信度低于閾值的樣本積累下來人工標(biāo)注后再做增量訓(xùn)練。增量訓(xùn)練不是簡單地在原模型上繼續(xù)跑幾個 epoch那樣很容易災(zāi)難性遺忘。常見做法是降低新數(shù)據(jù)的學(xué)習(xí)率——原模型所有參數(shù)用一個很小的學(xué)習(xí)率比如 5e-5新數(shù)據(jù)上只訓(xùn)練 3 到 5 個 epoch然后用驗(yàn)證集檢驗(yàn)原類別和新類別的 F1 變化。如果舊類別 F1 下降超過 2 個百分點(diǎn)說明學(xué)習(xí)率太大或新數(shù)據(jù)占比失衡。我一般會給舊樣本保留部分采樣新數(shù)據(jù)按召回策略重采樣后混合訓(xùn)練這樣兩個分布都能兼顧。模型版本管理上固定打包輸入預(yù)處理邏輯、詞表、模型權(quán)重三個文件為一個版本號文本分類系統(tǒng)的線上問題排查非常依賴版本可回滾。曾經(jīng)在生產(chǎn)環(huán)境踩過一次坑新版本改了 jieba 詞典導(dǎo)致線上分詞結(jié)果變了老版本的模型輸入分布錯亂準(zhǔn)確率掉到 30% 以下。這種問題根本查不出來只能整體回滾。從那以后我把分詞邏輯找到的每個版本都單獨(dú)歸檔絕不因?yàn)椤钢皇歉膫€詞典」就放任不管。希望這個習(xí)慣對你有幫助。本文還有配套的精品資源點(diǎn)擊獲取