高分攻略:中文文本分類項(xiàng)目全流程實(shí)戰(zhàn))
簡介自然語言處理NLP中文本分類是最基礎(chǔ)且應(yīng)用廣泛的任務(wù)之一其核心在于通過算法自動(dòng)將文本劃分到預(yù)定義類別。從傳統(tǒng)機(jī)器學(xué)習(xí)到深度學(xué)習(xí)模型通過提取詞序與語義特征實(shí)現(xiàn)分類。在工程實(shí)踐中數(shù)據(jù)預(yù)處理的質(zhì)量直接影響模型上限而消融實(shí)驗(yàn)與對(duì)比分析則是驗(yàn)證技術(shù)價(jià)值的關(guān)鍵。這種能力廣泛應(yīng)用于輿情監(jiān)測、新聞分類、情感分析等真實(shí)場景。對(duì)于NLP期末大作業(yè)而言構(gòu)建一個(gè)完整的中文文本分類項(xiàng)目覆蓋數(shù)據(jù)清洗、詞表構(gòu)建、TextCNN與BiLSTM模型搭建、實(shí)驗(yàn)對(duì)比及報(bào)告撰寫能有效提升項(xiàng)目綜合評(píng)分。 NLP期末大作業(yè)這東西說實(shí)話每屆都有大批人栽在同一個(gè)坑里模型跑通了代碼也能出結(jié)果但最后分?jǐn)?shù)就是上不去。我見過太多人拿TextCNN跑個(gè)二分類就交差實(shí)驗(yàn)報(bào)告寫得像README數(shù)據(jù)預(yù)處理部分一筆帶過結(jié)果被老師批得體無完膚。反觀那些拿高分的項(xiàng)目往往不是在模型上多花哨而是整個(gè)項(xiàng)目完整度、實(shí)驗(yàn)設(shè)計(jì)的嚴(yán)謹(jǐn)度、報(bào)告的邏輯性都做到了位。這篇就圍繞一個(gè)能拿高分的NLP期末大作業(yè)項(xiàng)目從選題設(shè)計(jì)、數(shù)據(jù)預(yù)處理、模型搭建、實(shí)驗(yàn)對(duì)比到報(bào)告撰寫把整個(gè)項(xiàng)目的核心環(huán)節(jié)和踩坑經(jīng)驗(yàn)一次性講透。項(xiàng)目定位是用深度學(xué)習(xí)做中文文本分類任務(wù)覆蓋從原始語料到最終實(shí)驗(yàn)報(bào)告的全流程。適合正在做NLP課程大作業(yè)、準(zhǔn)備畢業(yè)設(shè)計(jì)相關(guān)課題、或者想系統(tǒng)入門NLP實(shí)踐的同學(xué)不管你是第一次接觸PyTorch還是已經(jīng)有基礎(chǔ)想查漏補(bǔ)缺這篇文章都能讓你少走很多彎路。1. 項(xiàng)目選題與整體設(shè)計(jì)思路1.1 題目定調(diào)為什么選文本分類而不是其他任務(wù)NLP課程大作業(yè)可選的方向很多文本分類、序列標(biāo)注、文本生成、機(jī)器翻譯、問答系統(tǒng)都有。但我的經(jīng)驗(yàn)是期末大作業(yè)首選文本分類原因很直接數(shù)據(jù)獲取容易。文本分類的公開數(shù)據(jù)集非常多中文的有THUCNews、今日頭條新聞分類、情感分析數(shù)據(jù)集英文的有IMDB、AG News不需要自己吭哧吭哧標(biāo)注數(shù)據(jù)。反觀序列標(biāo)注或者問答系統(tǒng)數(shù)據(jù)集格式復(fù)雜預(yù)處理環(huán)節(jié)就能勸退一批人。模型難度梯度合理。文本分類可以從最簡單的詞袋模型做起一路做到TextCNN、RNN/LSTM、BERT能清晰展示不同模型的性能變化這種“由簡到繁”的對(duì)比正是老師最想看到的實(shí)驗(yàn)內(nèi)容。評(píng)價(jià)指標(biāo)清晰。準(zhǔn)確率、精確率、召回率、F1值一套標(biāo)準(zhǔn)評(píng)估體系能直接量化模型好壞寫報(bào)告時(shí)不用費(fèi)勁解釋評(píng)估標(biāo)準(zhǔn)。我當(dāng)年選的是中文新聞文本分類THUCNews子集10個(gè)類別每個(gè)類別6500條訓(xùn)練數(shù)據(jù)。這個(gè)選擇既保證了數(shù)據(jù)量足夠訓(xùn)練深度學(xué)習(xí)模型又不會(huì)因?yàn)閿?shù)據(jù)太大導(dǎo)致訓(xùn)練時(shí)間過長剛好適配課程作業(yè)的時(shí)間預(yù)算。1.2 技術(shù)方案選型傳統(tǒng)模型到深度模型的遞進(jìn)路線整個(gè)項(xiàng)目的技術(shù)路線我設(shè)計(jì)成三層遞進(jìn)TF-IDF 機(jī)器學(xué)習(xí)分類器作為baselineTextCNN和RNN/LSTM作為深度學(xué)習(xí)主模型BERT作為可選的高配模型。這么設(shè)計(jì)不是為了讓工作量看起來大而是有實(shí)際考量的。Baseline的作用是提供一個(gè)性能參照系。很多同學(xué)一上來直接跑深度學(xué)習(xí)模型卻不知道深度學(xué)習(xí)相比傳統(tǒng)方法到底強(qiáng)在哪里。有了TF-IDFSVM的baseline你就能明確說出“深度學(xué)習(xí)在測試集上比傳統(tǒng)方法高了多少個(gè)點(diǎn)”這種量化對(duì)比在實(shí)驗(yàn)報(bào)告里是非常重要的論據(jù)。深度學(xué)習(xí)主模型選TextCNN和LSTM也是一對(duì)經(jīng)典組合TextCNN利用卷積核捕捉局部n-gram特征LSTM能建模長距離依賴。這兩類模型代表了深度學(xué)習(xí)處理文本的兩種基本范式對(duì)比它們?cè)诓煌悇e上的表現(xiàn)差異能引申出很多有價(jià)值的分析討論。BERT我建議作為擴(kuò)展實(shí)驗(yàn)放進(jìn)去哪怕只是用現(xiàn)成的transformers庫加載預(yù)訓(xùn)練模型做微調(diào)也能提升項(xiàng)目的下限。BERT在文本分類任務(wù)上通常能比LSTM高出3到5個(gè)百分點(diǎn)這個(gè)結(jié)果放在實(shí)驗(yàn)報(bào)告里就是亮點(diǎn)。1.3 開發(fā)環(huán)境與工具鏈環(huán)境這塊直接說結(jié)論避免大家糾結(jié)Python 3.8PyTorch 1.10別用太新的版本有些老代碼庫兼容性容易出問題2.x也可以但沒必要追求最新transformers 4.x做BERT微調(diào)用scikit-learnTF-IDF、SVM、評(píng)估指標(biāo)jieba中文分詞pandas numpy數(shù)據(jù)處理離不開GPU方面文本分類這種任務(wù)其實(shí)對(duì)算力要求不算高GTX 1660級(jí)別就能很輕松跑LSTM做BERT微調(diào)建議至少6GB顯存。如果只有CPU也別慌把數(shù)據(jù)量降一些、embedding維度設(shè)小一點(diǎn)LSTM還是能跑的無非多等一會(huì)兒。代碼結(jié)構(gòu)我建議按模塊拆分別把所有邏輯都堆在一個(gè)文件里。我的項(xiàng)目結(jié)構(gòu)大致是project/ ├── data/ # 原始數(shù)據(jù)與預(yù)處理后的緩存 ├── src/ │ ├── preprocess.py # 數(shù)據(jù)清洗、分詞、詞表構(gòu)建 │ ├── dataset.py # Dataset與DataLoader封裝 │ ├── models/ # 模型定義 │ │ ├── textcnn.py │ │ ├── lstm.py │ │ └── bert_finetune.py │ ├── train.py # 訓(xùn)練與驗(yàn)證主腳本 │ └── evaluate.py # 測試集評(píng)估與指標(biāo)輸出 ├── report/ # 實(shí)驗(yàn)報(bào)告相關(guān)圖表 └── requirements.txt這樣寫的好處是后期跑實(shí)驗(yàn)對(duì)比時(shí)只需改動(dòng)配置文件或者調(diào)參不用在幾千行代碼里翻來找去。老師檢查代碼時(shí)看到這種結(jié)構(gòu)第一印象就會(huì)好很多。2. 數(shù)據(jù)準(zhǔn)備與預(yù)處理決定上限的隱藏環(huán)節(jié)2.1 數(shù)據(jù)集獲取與基礎(chǔ)清洗很多同學(xué)覺得數(shù)據(jù)處理無聊隨便下載個(gè)數(shù)據(jù)集就開始訓(xùn)練這是大忌。文本分類任務(wù)中數(shù)據(jù)質(zhì)量直接影響最終模型性能和報(bào)告質(zhì)量。我用的THUCNews子集雖然號(hào)稱是清洗過的數(shù)據(jù)實(shí)際檢查還是發(fā)現(xiàn)問題重復(fù)樣本不少部分樣本含有HTML轉(zhuǎn)義字符還有一些類別標(biāo)簽錯(cuò)亂。所以第一步做數(shù)據(jù)審計(jì)很重要統(tǒng)計(jì)每個(gè)類別的樣本量檢查類別分布是否均衡。類別不平衡會(huì)嚴(yán)重影響模型訓(xùn)練如果發(fā)現(xiàn)某個(gè)類別樣本特別少要在報(bào)告里說明應(yīng)對(duì)策略。去除完全重復(fù)的樣本。用pandas的drop_duplicates就能搞定但要注意按“文本內(nèi)容”去重而不是按整行去重因?yàn)榇嬖谙嗤谋静煌瑯?biāo)簽的臟數(shù)據(jù)這種建議直接刪除。清洗無效字符串。新聞數(shù)據(jù)里常見的\u3000全角空格、\xa0不間斷空格、HTML標(biāo)簽等統(tǒng)一用正則表達(dá)式或者BeautifulSoup去掉。寫入詞表。2.2 中文分詞與詞表構(gòu)建細(xì)節(jié)決定體驗(yàn)中文文本分類繞不開分詞。雖然現(xiàn)在很多預(yù)訓(xùn)練模型用字級(jí)別輸入但對(duì)于TextCNN和LSTM這類從零訓(xùn)練的模型詞級(jí)別的效果通常會(huì)更好因?yàn)樵~包含了更豐富的語義信息。分詞工具我用的jieba主要原因不是它效果最好而是它穩(wěn)定、文檔全、遇到問題一搜就有解決方案。使用時(shí)有幾個(gè)細(xì)節(jié)加載自定義詞典。新聞文本里經(jīng)常出現(xiàn)人名、機(jī)構(gòu)名、專業(yè)術(shù)語比如“機(jī)器學(xué)習(xí)”、“人工智能”、“區(qū)塊鏈”這類詞jieba默認(rèn)詞典可能會(huì)切錯(cuò)。做法是維護(hù)一個(gè)自定義詞典文件把這些詞加進(jìn)去并指定詞頻權(quán)重。關(guān)閉HMM開關(guān)有時(shí)會(huì)有奇效。jieba默認(rèn)開啟HMM新詞發(fā)現(xiàn)但會(huì)誤把一些人名拆開或者把某些詞錯(cuò)誤合并。對(duì)新聞數(shù)據(jù)關(guān)掉HMM反而分詞更穩(wěn)定這個(gè)可以通過調(diào)參對(duì)比測試。詞表構(gòu)建我單獨(dú)說一點(diǎn)心得min_count最低詞頻的設(shè)置非常關(guān)鍵。我做了兩組對(duì)比實(shí)驗(yàn)min_count1時(shí)詞表大小超過20萬模型參數(shù)量暴漲訓(xùn)練速度慢且容易過擬合min_count5時(shí)詞表壓縮到8萬左右效果反而更好。原因是低頻詞大多是噪聲保留它們只會(huì)讓模型去記憶那些只出現(xiàn)過一兩次的詞不利于泛化。最終我選定的詞表大小是50000這個(gè)數(shù)字兼顧了覆蓋率測試集OOV率不到2%和模型規(guī)模embedding層參數(shù)量適中。詞表構(gòu)建代碼如下from collections import Counter def build_vocab(texts, min_count3, max_size50000): counter Counter() for tokens in texts: counter.update(tokens) # 按詞頻降序排列 vocab_tuples sorted(counter.items(), keylambda x: -x[1]) # 過濾低頻詞并按上限截?cái)?vocab_tuples [(w, c) for w, c in vocab_tuples if c min_count][:max_size] # 預(yù)留特殊token vocab {pad: 0, unk: 1} for w, _ in vocab_tuples: vocab[w] len(vocab) return vocab2.3 序列長度設(shè)計(jì)與Padding策略文本長度處理是新手最容易忽視但影響很大的環(huán)節(jié)。THUCNews的數(shù)據(jù)平均長度為幾百字而LSTM和TextCNN對(duì)輸入長度都有上限要求不可能把整篇文本全塞進(jìn)去。我的做法是統(tǒng)計(jì)訓(xùn)練集文本長度的分布然后決定序列最大長度。畫一下長度分布的箱線圖就能發(fā)現(xiàn)絕大多數(shù)樣本長度集中在50到300之間超過500的很少。我最終把max_len定為200原因有三200個(gè)字能覆蓋訓(xùn)練集中85%以上的樣本信息損失可以接受。對(duì)LSTM來說序列越長梯度消失問題和訓(xùn)練時(shí)間越長200是個(gè)不錯(cuò)的平衡點(diǎn)。對(duì)TextCNN來說卷積核覆蓋的是局部窗口過長的序列尾部信息對(duì)分類貢獻(xiàn)不大反而增加計(jì)算量。Padding位置的選擇也值得說明一下。RNN系列模型在處理長文本時(shí)前向傳播從序列開頭開始如果開頭全是pad會(huì)影響信息傳遞。所以我統(tǒng)一用后向padding即paddingpost這樣實(shí)際文本內(nèi)容盡量靠前保證模型優(yōu)先處理真實(shí)信息。BERT場景則用paddingmax_length配合attention_mask來處理。3. 核心模型搭建與實(shí)現(xiàn)細(xì)節(jié)3.1 TextCNN用卷積捕捉文本局部特征TextCNN的原理一句話就能講清楚通過多個(gè)不同尺寸的卷積核在詞嵌入序列上滑動(dòng)提取文本的n-gram特征再通過最大池化得到定長向量最后接全連接層分類。核心參數(shù)配置如下import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, num_filters256, filter_sizes[2, 3, 4]): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_sizefs, paddingfs // 2) for fs in filter_sizes ]) self.fc nn.Linear(num_filters * len(filter_sizes), num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): # x: (batch, seq_len) emb self.embedding(x) # (batch, seq_len, embed_dim) emb emb.transpose(1, 2) # (batch, embed_dim, seq_len) conv_outputs [] for conv in self.convs: c torch.relu(conv(emb)) # (batch, num_filters, seq_len) c c.max(dim2).values # 全局最大池化 → (batch, num_filters) conv_outputs.append(c) out torch.cat(conv_outputs, dim1) # (batch, num_filters * len(filter_sizes)) out self.dropout(out) return self.fc(out)這里的細(xì)節(jié)值得展開講。filter_sizes[2, 3, 4]分別對(duì)應(yīng)二元、三元、四元n-gram特征。為什么選這三個(gè)尺寸因?yàn)橹形膯卧~組合的語義單位通常集中在2到4個(gè)詞之間。比如“人工智能”是2個(gè)詞構(gòu)成的復(fù)合詞“深度學(xué)習(xí)”是3個(gè)詞有些固定搭配如“自然語言處理”是4個(gè)詞。如果filter_size設(shè)為1基本等價(jià)于只看詞本身不看上下文效果會(huì)差不少。每個(gè)filter_size用256個(gè)卷積核也就是每個(gè)尺寸學(xué)習(xí)256種不同的局部特征模式。三個(gè)尺寸總共768個(gè)特征最后拼接到一起輸入全連接層。這個(gè)參數(shù)組合是學(xué)術(shù)界和工業(yè)界驗(yàn)證過多次的經(jīng)典配置新手直接抄作業(yè)就行不用自己瞎調(diào)。TextCNN有一個(gè)明顯的優(yōu)勢(shì)是訓(xùn)練速度極快因?yàn)榫矸e操作可以高度并行化在GPU上跑起來比LSTM快好幾倍。這在實(shí)際項(xiàng)目里非常重要因?yàn)樗馕吨隳茉谕瑯拥臅r(shí)間內(nèi)做更多的調(diào)參和對(duì)比實(shí)驗(yàn)。3.2 BiLSTM建模長距離依賴的利器LSTM通過門控機(jī)制解決了RNN的梯度消失問題能在一定程度上建模長距離依賴。但單向LSTM有個(gè)缺點(diǎn)當(dāng)前時(shí)刻的隱藏狀態(tài)只包含過去的信息不包含未來的信息。對(duì)文本分類來說某個(gè)詞的含義往往由它前后文共同決定所以雙向LSTMBiLSTM是文本分類的主流選擇。我的BiLSTM實(shí)現(xiàn)核心部分class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers, num_classes): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM( embed_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue ) self.fc nn.Linear(hidden_dim * 2, num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): emb self.embedding(x) # (batch, seq_len, embed_dim) output, (h_n, c_n) self.lstm(emb) # output: (batch, seq_len, hidden*2) # 取最后一層的兩個(gè)方向最后一個(gè)時(shí)刻的隱藏狀態(tài)拼接 last_hidden torch.cat((h_n[-2], h_n[-1]), dim1) # (batch, hidden*2) return self.fc(self.dropout(last_hidden))這里有幾個(gè)實(shí)現(xiàn)細(xì)節(jié)容易踩坑。第一個(gè)是h_n的形狀。當(dāng)bidirectionalTrue且num_layers2時(shí)h_n的shape是(num_layers * 2, batch, hidden_dim)。前向和后向的隱藏狀態(tài)交替排列即索引0是第1層前向索引1是第1層后向索引2是第2層前向以此類推。所以要取最后一層的兩個(gè)方向需要h_n[-2]和h_n[-1]而不是h_n[0]和h_n[1]。第二個(gè)是分類特征的提取方式。我用的是最后一層雙向LSTM的末時(shí)刻隱藏狀態(tài)拼接但還有一種常用做法是取所有時(shí)刻隱藏狀態(tài)的均值或最大池化。實(shí)測下來在文本分類任務(wù)中均值池化通常比單純?nèi)∧r(shí)刻更穩(wěn)定因?yàn)樗C合了序列所有位置的信息。我在報(bào)告中針對(duì)池化方式做了小規(guī)模對(duì)比實(shí)驗(yàn)均值池化比取末時(shí)刻高約0.8個(gè)百分點(diǎn)。代碼可以改為output, _ self.lstm(emb) output output.mean(dim1) # 對(duì)所有時(shí)刻取平均 out self.fc(self.dropout(output))取末時(shí)刻 vs 均值池化的選擇本質(zhì)上是“最終狀態(tài)包含什么信息”的問題。末時(shí)刻狀態(tài)理論上攜帶了整句的壓縮信息但受限于LSTM的記性上限序列過長時(shí)早期信息會(huì)丟失。均值池化相當(dāng)于做了一個(gè)簡單的注意力平均把各時(shí)刻信息均勻融合不容易丟信息但也不突出關(guān)鍵部分。所以如果你的序列長度普遍較長超過100均值池化通常更好如果句子很短兩種方法差異不大。第三個(gè)是Dropout的位置。我把Dropout放在LSTM輸出和全連接層之間這是最常見的做法。要注意LSTM內(nèi)部還有一個(gè)dropout參數(shù)指多層LSTM各層之間的dropout它與最后特征上的dropout是兩回事不要搞混。3.3 Embedding層初始化方式對(duì)效果的影響Embedding層的初始化往往被忽視但它對(duì)訓(xùn)練效果有直接影響。PyTorch的nn.Embedding默認(rèn)使用標(biāo)準(zhǔn)正態(tài)分布初始化這本身沒問題但有幾個(gè)注意點(diǎn)padding_idx0要設(shè)對(duì)。如果指定了padding_idx該位置的詞向量在訓(xùn)練中不會(huì)更新始終為初始值。但要注意即便設(shè)為0如果不小心用了非零值填充padding位置對(duì)應(yīng)的詞向量仍會(huì)被隨機(jī)初始化并參與訓(xùn)練導(dǎo)致padding位置學(xué)到奇怪的表示。Embedding向量的范數(shù)會(huì)導(dǎo)致訓(xùn)練不穩(wěn)定。我曾用加載預(yù)訓(xùn)練Word2Vec詞向量來初始化Embedding效果比隨機(jī)初始化提升約2個(gè)百分點(diǎn)但代價(jià)是詞表外的詞OOV只能隨機(jī)初始化這會(huì)造成OOV詞向量與整體分布不一致的域偏移問題。解決辦法是設(shè)置一個(gè)較小的初始化方差如0.1來縮小OOV詞向量與預(yù)訓(xùn)練向量的分布差距。凍結(jié)還是微調(diào)加載預(yù)訓(xùn)練詞向量后可以選擇凍結(jié)Embedding層或者讓它繼續(xù)參與訓(xùn)練。我的實(shí)驗(yàn)結(jié)果微調(diào)Embedding層比凍結(jié)效果好但要配合較小的學(xué)習(xí)率Embedding層專用的學(xué)習(xí)率可以設(shè)為主模型學(xué)習(xí)率的一半否則容易破壞預(yù)訓(xùn)練詞向量的語義結(jié)構(gòu)。這一點(diǎn)用同各層不同學(xué)習(xí)率實(shí)現(xiàn)optimizer torch.optim.Adam([ {params: model.embedding.parameters(), lr: 1e-4}, {params: model.lstm.parameters(), lr: 2e-4}, {params: model.fc.parameters(), lr: 2e-4}, ])4. 訓(xùn)練過程、實(shí)驗(yàn)對(duì)比與結(jié)果分析4.1 超參數(shù)配置與調(diào)整策略先說一組我實(shí)驗(yàn)下來比較穩(wěn)定、適合THUCNews這種規(guī)模的超參數(shù)配置超參數(shù)TextCNNBiLSTMembedding_dim128128hidden_dim—256num_layers—2num_filters256—filter_sizes[2, 3, 4]—dropout0.50.5batch_size12864learning_rate1e-35e-4epochs1015optimizerAdamAdam有幾個(gè)經(jīng)驗(yàn)可以分享。Batch size的選擇直接影響模型收斂速度和效果。TextCNN用128沒問題因?yàn)榫矸e參數(shù)共享且結(jié)構(gòu)簡單大batch能提升訓(xùn)練吞吐。BiLSTM我用64主要是受限于序列長度和顯存占用。序列長度為200的樣本batch為64時(shí)每個(gè)batch就是64*200*128的嵌入矩陣反向傳播還要存梯度顯存壓力比TextCNN大不少。學(xué)習(xí)率方面Adam的默認(rèn)學(xué)習(xí)率是1e-3但BiLSTM這種深度循環(huán)網(wǎng)絡(luò)對(duì)學(xué)習(xí)率更敏感我用5e-4更穩(wěn)。如果你發(fā)現(xiàn)訓(xùn)練loss在震蕩不下降優(yōu)先降低學(xué)習(xí)率而不是增大。這是新手最容易犯的錯(cuò)誤看到loss不降就盲目加大學(xué)習(xí)率結(jié)果模型在局部最優(yōu)附近反復(fù)橫跳永遠(yuǎn)不收斂。Epoch的選擇不要拍腦袋一定要結(jié)合驗(yàn)證集曲線。我的做法是每訓(xùn)練一個(gè)epoch就在驗(yàn)證集上評(píng)估一次如果驗(yàn)證集準(zhǔn)確率連續(xù)3個(gè)epoch沒有提升就觸發(fā)早停并恢復(fù)最好的模型參數(shù)。早停的Patience容忍度不要設(shè)成1因?yàn)轵?yàn)證集準(zhǔn)確率會(huì)有小波動(dòng)很容易誤殺還在上升期的模型建議3到5比較合理。4.2 消融實(shí)驗(yàn)讓自己的結(jié)論站得住腳一份能拿高分的實(shí)驗(yàn)報(bào)告核心在于消融實(shí)驗(yàn)做得到不到位。所謂消融實(shí)驗(yàn)就是控制變量地去掉某個(gè)模塊或者改變某個(gè)設(shè)置觀察模型性能變化從而證明你設(shè)計(jì)的每個(gè)選擇都是有用的。我做了一組消融實(shí)驗(yàn)觀察BiLSTM在去掉某個(gè)組件后的性能變化實(shí)驗(yàn)設(shè)置準(zhǔn)確率相對(duì)完整模型的變化完整BiLSTM雙向均值池化Dropout91.2%—去除雙向改為單向LSTM88.6%-2.6%去除均值池化改為取末時(shí)刻狀態(tài)90.4%-0.8%去除Dropout90.1%-1.1%隨機(jī)Embedding替換預(yù)訓(xùn)練詞向量89.2%-2.0%這組數(shù)據(jù)放在報(bào)告里非常有說服力它證明了“雙向”是貢獻(xiàn)最大的設(shè)計(jì)貢獻(xiàn)2.6個(gè)點(diǎn)預(yù)訓(xùn)練詞向量次之2個(gè)點(diǎn)Dropout和池化策略也有可量化的貢獻(xiàn)。做完消融實(shí)驗(yàn)后我還建議做錯(cuò)誤分析。具體操作是用測試集跑一次預(yù)測把預(yù)測錯(cuò)誤的樣本拿出來看統(tǒng)計(jì)錯(cuò)誤類型。我統(tǒng)計(jì)后發(fā)現(xiàn)幾個(gè)規(guī)律容易混淆的類別都是語義相近的類別。比如“體育”和“娛樂”經(jīng)常混——因?yàn)閵蕵啡π侣劺锎罅砍霈F(xiàn)“比賽”“冠軍”等體育詞體育新聞里也有“明星”“演出”等娛樂詞。短文本更容易出錯(cuò)。少于50個(gè)字的樣本錯(cuò)誤率是長文本的2倍。原因是信息量不足CNN的n-gram特征和LSTM的上下文建模都難以捕捉充分的語義。數(shù)字和特殊符號(hào)密集的文本如“世界杯2022年卡塔爾”這類表述分詞效果差直接拖累模型性能。這些錯(cuò)誤分析寫在報(bào)告里能體現(xiàn)出你不僅會(huì)跑代碼還懂分析問題根因這是拉開分?jǐn)?shù)差距的關(guān)鍵。4.3 可視化用圖表講好你的實(shí)驗(yàn)結(jié)果實(shí)驗(yàn)報(bào)告里可視化圖表不是點(diǎn)綴而是支撐結(jié)論的證據(jù)。我習(xí)慣用三張圖訓(xùn)練曲線圖、混淆矩陣圖、文本長度與準(zhǔn)確率關(guān)系圖。訓(xùn)練曲線圖包含loss和accuracy隨epoch變化的曲線訓(xùn)練集和驗(yàn)證集各兩條線。這張圖最重要的作用是展示過擬合趨勢(shì)如果訓(xùn)練集loss持續(xù)下降但驗(yàn)證集loss在第5個(gè)epoch后開始上升這就是過擬合的直觀證據(jù)配合早停機(jī)制說明就很完整。混淆矩陣圖能直觀展示模型在哪些類別上犯錯(cuò)。用matplotlib的imshow畫10x10的矩陣行索引和列索引都標(biāo)上類別名對(duì)角線越深紅越好對(duì)角線外的亮點(diǎn)就是錯(cuò)誤集中區(qū)。這張圖放在實(shí)驗(yàn)分析章節(jié)配合錯(cuò)誤類別分析一起看邏輯很完整。我用的畫圖代碼大致這樣import matplotlib.pyplot as plt import itertools from sklearn.metrics import confusion_matrix def plot_confusion_matrix(y_true, y_pred, classes, save_path): cm confusion_matrix(y_true, y_pred) plt.figure(figsize(10, 8)) plt.imshow(cm, interpolationnearest, cmapBlues) plt.colorbar() tick_marks np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation45) plt.yticks(tick_marks, classes) thresh cm.max() / 2 for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, format(cm[i, j], d), horizontalalignmentcenter, colorwhite if cm[i, j] thresh else black) plt.ylabel(True label) plt.xlabel(Predicted label) plt.tight_layout() plt.savefig(save_path, dpi150, bbox_inchestight)5. 常見問題與排查技巧實(shí)錄5.1 訓(xùn)練不收斂或損失值異常這個(gè)是最常遇到的問題基本每個(gè)跑深度學(xué)習(xí)項(xiàng)目的人都會(huì)碰到。先說現(xiàn)象loss呈NaN或者loss不下降或者準(zhǔn)確率一直徘徊在隨機(jī)水平附近。loss為NaN的排查順序先看學(xué)習(xí)率是否過大尤其是Adam調(diào)小學(xué)習(xí)率能解決大部分NaN問題再看數(shù)據(jù)是否有問題包括輸入是否包含NaN值、標(biāo)簽是否越界最后看模型結(jié)構(gòu)比如softmax輸出維度是不是改過但忘了同步。loss不下降的排查順序數(shù)據(jù)預(yù)處理檢查優(yōu)先這是最容易被忽視的環(huán)節(jié)。我用過一個(gè)常見的數(shù)據(jù)集按行讀入后沒有去除換行符結(jié)果整個(gè)模型學(xué)到的都是換行相關(guān)的特征準(zhǔn)確率始終在60%左右對(duì)10分類來說比隨機(jī)好一點(diǎn)但遠(yuǎn)低于正常水平。其次是詞表是否構(gòu)建正確如果unk比例太高模型幾乎看不到有效信息。最后才是模型結(jié)構(gòu)問題但一般預(yù)定義模型結(jié)構(gòu)出問題的概率很小。5.2 GPU顯存溢出CUDA Out of Memory顯存溢出在訓(xùn)練BiLSTM和BERT時(shí)特別常見。我的經(jīng)驗(yàn)是優(yōu)先減小batch size然后觀察GPU顯存峰值逐步加大。但要注意batch size過小會(huì)影響B(tài)NBatch Normalization的效果不過在NLP模型里大多數(shù)情況下不用BatchNorm所以影響不大。另一個(gè)容易踩的坑是在訓(xùn)練循環(huán)里每次迭代累加梯度時(shí)忘記調(diào)用optimizer.zero_grad()。這樣會(huì)導(dǎo)致梯度不斷累加顯存爆炸式增長。如果發(fā)現(xiàn)顯存占用隨迭代次數(shù)線性上升大概率就是這個(gè)原因。還一個(gè)實(shí)用技巧用torch.cuda.empty_cache()可以釋放緩存顯存但不要指望它能解決所有問題它只是把不用的顯存塊釋放回緩存池真正的內(nèi)存占用還是取決于模型和數(shù)據(jù)大小。5.3 實(shí)驗(yàn)結(jié)果復(fù)現(xiàn)不穩(wěn)定同一份代碼每次跑的結(jié)果不一樣這個(gè)現(xiàn)象很正常因?yàn)镻yTorch的隨機(jī)初始化、GPU計(jì)算的不確定性以及DataLoader的隨機(jī)打亂都會(huì)引入隨機(jī)性。但如果浮動(dòng)太大比如超過2個(gè)百分點(diǎn)就要注意排查了。復(fù)現(xiàn)實(shí)驗(yàn)時(shí)我會(huì)固定三處隨機(jī)種子Python內(nèi)置random、numpy.random、torch.manual_seed。如果用了CUDA還要設(shè)置torch.cuda.manual_seed_all。但要注意固定種子只能減少隨機(jī)性不能完全消除CPU與GPU上常微分計(jì)算帶來的微小差異這是硬件層面的原因不用過分糾結(jié)。在實(shí)驗(yàn)報(bào)告中我建議每個(gè)實(shí)驗(yàn)至少跑3次報(bào)告平均值和標(biāo)準(zhǔn)差。比如“BiLSTM在測試集上的準(zhǔn)確率91.2% ± 0.3%”這種寫法遠(yuǎn)比只報(bào)一次結(jié)果更能說明模型的穩(wěn)定性也會(huì)被老師認(rèn)為你的實(shí)驗(yàn)做得很嚴(yán)謹(jǐn)。5.4 中文分詞后數(shù)據(jù)量驟增導(dǎo)致OOM有同學(xué)在分詞后把所有文本一次性to_tensor全部載入內(nèi)存序列長度為200的8萬條樣本每條約占200 * 8 1600字節(jié)int64型總內(nèi)存占用大約128MB看起來不大但如果做了詞向量映射后再全量往GPU上搬顯存很容易爆。正確做法是使用DataLoader按batch分批加載train_loader DataLoader( train_dataset, batch_sizebatch_size, shuffleTrue, num_workers4, pin_memoryTrue, collate_fncollate_fn )collate_fn的作用是把一個(gè)batch的樣本不定長文本統(tǒng)一padding到當(dāng)前batch的最大長度這種方式比全局padding到固定長度更省內(nèi)存。如果要做性能對(duì)照實(shí)驗(yàn)建議兩個(gè)模型統(tǒng)一使用相同的長度策略否則對(duì)照不齊。6. 實(shí)驗(yàn)報(bào)告撰寫拿高分的關(guān)鍵一步6.1 報(bào)告結(jié)構(gòu)按評(píng)審邏輯倒推章節(jié)安排寫實(shí)驗(yàn)報(bào)告不是一個(gè)“做完實(shí)驗(yàn)再寫”的流程而是應(yīng)該在實(shí)驗(yàn)設(shè)計(jì)階段就規(guī)劃好報(bào)告結(jié)構(gòu)。我通常按這樣的框架來組織一份NLP實(shí)驗(yàn)報(bào)告章節(jié)核心內(nèi)容篇幅建議1. 引言任務(wù)定義、研究背景與問題意義0.5頁2. 相關(guān)工作文本分類的經(jīng)典方法和深度模型簡述0.5-1頁3. 數(shù)據(jù)集與預(yù)處理數(shù)據(jù)來源、規(guī)模、預(yù)處理流程與統(tǒng)計(jì)1-2頁4. 模型設(shè)計(jì)模型結(jié)構(gòu)解釋、參數(shù)設(shè)置、設(shè)計(jì)理由2-3頁5. 實(shí)驗(yàn)與結(jié)果訓(xùn)練細(xì)節(jié)、對(duì)比實(shí)驗(yàn)、消融實(shí)驗(yàn)、可視化2-3頁6. 分析與討論錯(cuò)誤分析、不足之處、改進(jìn)方向1頁報(bào)告的重點(diǎn)要放在“你做了什么”“為什么這樣做”“結(jié)果說明了什么”這條邏輯線上。很多同學(xué)把報(bào)告寫成代碼注釋搬運(yùn)工整篇都是“我用了一個(gè)LSTM模型輸入是詞向量輸出是分類結(jié)果”一點(diǎn)分析都沒有這種報(bào)告注定分?jǐn)?shù)不高。6.2 數(shù)據(jù)統(tǒng)計(jì)與可視化在報(bào)告中的呈現(xiàn)報(bào)告中應(yīng)該有數(shù)據(jù)統(tǒng)計(jì)的部分這既是學(xué)術(shù)規(guī)范也是基本素養(yǎng)。最少需要包含三張表和兩張圖表1數(shù)據(jù)集劃分統(tǒng)計(jì)表包含各類別的原始樣本數(shù)、訓(xùn)練集/驗(yàn)證集/測試集數(shù)量。表2預(yù)處理超參數(shù)表包括max_len、min_count、詞表大小、OOV率等。表3模型參數(shù)量和訓(xùn)練時(shí)間對(duì)比表。圖1序列長度分布圖直方圖或箱線圖支撐你設(shè)置max_len的依據(jù)。圖2模型訓(xùn)練曲線圖展示收斂過程。表2的OOV率計(jì)算值得單獨(dú)說。計(jì)算公式是測試集中詞表外詞的數(shù)量/測試集中總詞數(shù)。如果OOV率過高超過5%說明詞表構(gòu)建有缺陷要么min_count設(shè)太高導(dǎo)致好詞被過濾掉了要么數(shù)據(jù)預(yù)處理不統(tǒng)一導(dǎo)致訓(xùn)練和測試分詞結(jié)果不一致。這個(gè)指標(biāo)能幫助老師快速判斷你的數(shù)據(jù)處理是否規(guī)范。6.3 分析與討論這部分最能拉分實(shí)驗(yàn)報(bào)告里純實(shí)驗(yàn)結(jié)果只能拿基礎(chǔ)分真正拉分的部分在“分析”。我的經(jīng)驗(yàn)是要圍繞實(shí)驗(yàn)結(jié)果做出至少三個(gè)層次的深度分析第一層結(jié)果現(xiàn)象描述。解釋為什么BiLSTM比TextCNN效果好的原因——BiLSTM能建模長距離依賴而TextCNN的卷積核尺寸限制了它只能看到局部n-gram。這里就可以引出卷積核大小、感受野等概念。第二層錯(cuò)誤模式分析。結(jié)合錯(cuò)誤分析中的案例說明模型在語義相近的類別上容易混淆并討論這背后的語言學(xué)原因——中文新聞?lì)悇e之間的邊界本身就有模糊性。第三層局限性討論。模型的訓(xùn)練樣本量只有6.5萬條相比大規(guī)模公開基準(zhǔn)還差一個(gè)量級(jí)使用的預(yù)訓(xùn)練詞向量是靜態(tài)的無法解決一詞多義的問題未做類別加權(quán)處理類別不平衡會(huì)影響少數(shù)類效果。這些局限性恰恰引出BERT等預(yù)訓(xùn)練語言模型能解決的問題——?jiǎng)討B(tài)上下文表示。這三個(gè)層次的分析寫完報(bào)告的說服力和深度就上來了遠(yuǎn)遠(yuǎn)超過那種“實(shí)驗(yàn)結(jié)果如表所示”的空泛寫法。7. 項(xiàng)目經(jīng)驗(yàn)總結(jié)與擴(kuò)展方向做完這個(gè)NLP期末大作業(yè)我最大的感受是深度學(xué)習(xí)項(xiàng)目最核心的競爭力不在于用了多高級(jí)的模型而在于把每個(gè)環(huán)節(jié)做扎實(shí)。從數(shù)據(jù)清洗到詞表構(gòu)建從模型設(shè)計(jì)到消融實(shí)驗(yàn)每一步都做規(guī)范了項(xiàng)目質(zhì)量自然就上去了。很多同學(xué)覺得作業(yè)嘛能跑通就行但事實(shí)上期末大作業(yè)往往是課程中唯一能讓你完整走一遍項(xiàng)目流程的機(jī)會(huì)敷衍對(duì)待損失的其實(shí)是一次寶貴的實(shí)戰(zhàn)鍛煉。最后分享一個(gè)拿高分的小技巧做完主實(shí)驗(yàn)后選一個(gè)方向做延伸實(shí)驗(yàn)?zāi)呐轮蛔鲆粋€(gè)也能讓報(bào)告明顯超出預(yù)期。我當(dāng)時(shí)選的方向是類別的復(fù)雜數(shù)據(jù)處理——把新聞標(biāo)題和正文拼接在一起訓(xùn)練對(duì)比“僅用標(biāo)題”和“標(biāo)題正文”兩種輸入的效果。結(jié)果顯示“標(biāo)題正文”特征更豐富準(zhǔn)確率提升了近1個(gè)百分點(diǎn)。這種簡單的擴(kuò)展實(shí)驗(yàn)既不會(huì)占用太多時(shí)間又能展示你的思考深度比堆砌十個(gè)模型效果好得多。如果學(xué)有余力也可以考慮用BERT替代Word2Vec做特征提取或者嘗試用注意力機(jī)制替換LSTM的均值池化這些都是很好的探索方向。本文還有配套的精品資源點(diǎn)擊獲取