:從數(shù)據(jù)清洗到TextCNN與BERT模型落地)
簡介這份資源面向希望入門或實踐文本分類的開發(fā)者與安全方向學習者提供了一套基于深度學習的網絡欺凌網絡暴力檢測完整項目可用于識別帶有攻擊性、侮辱性的言論適合作為課程設計、畢業(yè)設計或NLP實戰(zhàn)練手素材。壓縮包共8個文件約2.66MB包含2個ipynb筆記本、2個json數(shù)據(jù)與詞表文件、1個py腳本、1個h5模型、1個md說明及1個license覆蓋從數(shù)據(jù)、訓練到推理的完整鏈路。其中訓練數(shù)據(jù)集以逐行json對象組織訓練筆記本展示模型構建與訓練流程純Python腳本與筆記本兩種形式給出實際運行示例并附帶已訓練好的模型文件與構建好的詞表開箱即可對指定文本進行預測。目前已有200人學習下載。讀者可借此理解文本預處理、詞表構建、模型訓練與推理的完整流程并在此基礎上替換數(shù)據(jù)、調整網絡結構或遷移到其他有害內容識別任務具備較強的可擴展性與參考價值。1. 網絡欺凌檢測項目落地從標題拆解到能跑起來的第一條基線社交平臺每天產生海量文本人工審核根本扛不住網絡欺凌檢測就成了一個典型的 NLP 落地場景。這個標題里其實藏了四件事Python 工程、深度學習模型、源代碼、數(shù)據(jù)集缺一個都跑不通。我見過太多人卡在第一步——數(shù)據(jù)拿到手是 CSV標簽列叫l(wèi)abel還是toxic都不確定更別說后面訓練了。這篇筆記按真實落地順序走先把數(shù)據(jù)格式和標簽體系定死再搭一個能跑通的基線模型然后講清楚參數(shù)怎么調、坑在哪最后給一套驗證和進階的路子。適合兩類人剛入門想拿一個完整項目練手的以及已經做過文本分類但沒碰過欺凌檢測這個細分場景的。全程不依賴任何不存在的官方文檔所有代碼和參數(shù)都是我實際跑過或驗證過的常見做法。2. 數(shù)據(jù)準備與標簽體系欺凌檢測和普通情感分類差在哪2.1 為什么不能直接拿情感分析數(shù)據(jù)集湊合網絡欺凌檢測和普通情感二分類最大的區(qū)別在于負面情感不等于欺凌。一句“我討厭這個電影”是負面但不是欺凌而“你這種人就該消失”才是。欺凌的核心特征是針對性、重復性、傷害意圖這三個維度在普通情感數(shù)據(jù)集里根本沒有標注。所以如果你拿 IMDB 影評或電商評論去訓一個欺凌檢測模型準確率可能虛高到 90% 以上但一上真實社交文本就翻車——這就是典型的“數(shù)據(jù)集選錯后面全白做”。常見做法是找公開的欺凌標注數(shù)據(jù)集比如 Formspring、Twitter 欺凌語料、Jigsaw 有毒評論挑戰(zhàn)賽數(shù)據(jù)。這些數(shù)據(jù)集通常包含text和label兩列l(wèi)abel 可能是二分類欺凌/非欺凌也可能是多標簽種族、性別、宗教等。我一般會先做一件事統(tǒng)計標簽分布。如果正負樣本比例超過 1:10后面訓練必須處理類別不平衡否則模型會學會“全預測為負”來刷準確率。2.2 數(shù)據(jù)清洗與標簽對齊的實操步驟拿到原始數(shù)據(jù)后別急著喂模型。先跑一遍清洗腳本把 URL、提及、重復字符、HTML 標簽處理掉。下面這段代碼是我常用的清洗模板import re import pandas as pd def clean_text(text): # 轉小寫 text text.lower() # 去掉 URL text re.sub(rhttp\S|www\.\S, , text) # 去掉 提及但保留被提及者信息可另存 text re.sub(r\w, , text) # 壓縮重復字符比如 sooooo - soo text re.sub(r(.)\1{2,}, r\1\1, text) # 去掉 HTML 標簽 text re.sub(r.*?, , text) # 去掉多余空格 text re.sub(r\s, , text).strip() return text df pd.read_csv(bullying_data.csv) df[clean_text] df[text].apply(clean_text) # 標簽對齊確保 label 是 0/1 或統(tǒng)一的多標簽格式 df[label] df[label].astype(int) print(df[label].value_counts())邏輯說明re.sub(r(.)\1{2,}, r\1\1, text)這行是把三個以上連續(xù)相同字符壓成兩個既保留了強調語氣又避免模型把“sooooo”和“sooo”當成兩個詞。參數(shù)上\1{2,}里的 2 表示至少重復兩次才觸發(fā)壓縮你可以根據(jù)數(shù)據(jù)噪聲程度改成 3。標簽對齊那步必須做我見過 label 列里混著 “yes/no” 和 “1/0” 的情況不統(tǒng)一后面全亂。清洗完還要做一步去重。社交文本里轉發(fā)和復制粘貼極多重復樣本會讓模型過擬合。用df.drop_duplicates(subset[clean_text])去重后再統(tǒng)計一次標簽分布。如果正樣本太少可以用sklearn的class_weightbalanced或簡單過采樣但別用 SMOTE 生成文本——文本不像數(shù)值特征插值出來的句子根本不通順。2.3 數(shù)據(jù)集劃分的坑時間泄漏和用戶泄漏普通做法是隨機劃分 train/test但欺凌檢測有個隱蔽問題同一個用戶的多條文本可能同時出現(xiàn)在訓練集和測試集里。模型會記住這個用戶的說話風格測試準確率虛高。正確做法是按用戶 ID 劃分或者至少按時間劃分——用早期數(shù)據(jù)訓練后期數(shù)據(jù)測試。如果你拿到的數(shù)據(jù)集沒有用戶 ID那就退而求其次按文本哈希去重后再隨機劃分并在論文或報告里注明這個局限。劃分比例我一般用 70/15/15訓練/驗證/測試。驗證集用來調參和早停測試集只在最后跑一次。別用測試集調參這是血淚經驗——調著調著模型就“記住”測試集了上線必翻車。3. 模型選型與基線搭建從 TextCNN 到 BERT 的取舍3.1 為什么先跑 TextCNN 而不是直接上 BERT很多人一上來就from transformers import BertModel結果顯存不夠、訓練慢、調參復雜三天后放棄。我的建議是先用 TextCNN 跑一個基線確認數(shù)據(jù)管道和標簽沒問題再換 BERT 做提升。TextCNN 結構簡單訓練快在幾千到幾萬條數(shù)據(jù)上就能看出數(shù)據(jù)質量好壞。如果 TextCNN 的 F1 連 0.6 都不到換 BERT 也救不了——問題在數(shù)據(jù)不在模型。TextCNN 的核心就三層嵌入層、多尺寸卷積核、最大池化。卷積核尺寸一般取 2、3、4分別捕捉二元、三元、四元詞組特征。下面是一個可復現(xiàn)的 PyTorch 實現(xiàn)import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_classes2, kernel_sizes[2,3,4], num_filters100): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (k, embed_dim)) for k in kernel_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): # x: [batch, seq_len] x self.embedding(x) # [batch, seq_len, embed_dim] x x.unsqueeze(1) # [batch, 1, seq_len, embed_dim] x [F.relu(conv(x)).squeeze(3) for conv in self.convs] x [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] x torch.cat(x, 1) x self.dropout(x) return self.fc(x)邏輯說明unsqueeze(1)是把文本序列當成單通道圖像卷積核寬度等于嵌入維度所以每個卷積核實際上是在滑動窗口上做 n-gram 特征提取。max_pool1d對每個特征圖取最大值保留最強信號。參數(shù)上embed_dim128適合小數(shù)據(jù)集數(shù)據(jù)量上萬后可以加到 256num_filters100是每個卷積核的輸出通道數(shù)太小欠擬合太大過擬合100 到 200 之間比較穩(wěn)dropout0.5是防過擬合的常規(guī)操作如果訓練損失下降但驗證損失上升可以加到 0.6 或 0.7。3.2 訓練循環(huán)里的三個必調參數(shù)訓練代碼網上一搜一大把但真正影響結果的就三個學習率、批次大小、早停耐心值。學習率我一般從 1e-3 開始用 Adam 優(yōu)化器如果損失震蕩就降到 1e-4。批次大小 32 或 64顯存夠就 64不夠就 16別用 1——梯度太吵。早停耐心值設 3 到 5意思是驗證集 F1 連續(xù) 3 個 epoch 不提升就停防止過擬合。還有一個容易被忽略的最大序列長度。社交文本大多很短統(tǒng)計一下 95% 分位數(shù)如果 95% 的文本在 50 個詞以內就設max_len64別設 512。設大了不僅浪費計算還會讓 padding 淹沒真實信號。我見過有人把 max_len 設成 256 跑 TextCNN結果 F1 比設 64 還低——就是因為 padding 太多卷積核滑到 padding 區(qū)域提取的都是噪聲。3.3 從 TextCNN 換到 BERT 的時機和注意事項當 TextCNN 的驗證集 F1 穩(wěn)定在 0.75 以上且你還有提升需求時再換 BERT。換的時候注意三點第一BERT 的 tokenizer 和 TextCNN 的詞表不通用必須重新處理數(shù)據(jù)第二BERT 學習率要小得多一般 2e-5 到 5e-5用 AdamW 優(yōu)化器第三BERT 訓練慢建議先凍結底層只訓分類頭再解凍全部微調。如果數(shù)據(jù)量少于 5000 條BERT 很容易過擬合這時候 TextCNN 加預訓練詞向量反而是更穩(wěn)的選擇。4. 避坑與排查欺凌檢測項目里最容易翻車的五件事4.1 準確率 95% 但上線就廢類別不平衡的鍋現(xiàn)象訓練完一看準確率 95%興沖沖上線結果用戶反饋“什么都判成正?!?。原因正樣本只占 5%模型學會全預測為負就能拿 95% 準確率。解決別只看準確率看 F1 和召回率。訓練時加class_weightbalanced或者用 focal loss 讓模型關注難樣本。評估時用混淆矩陣確認正樣本召回率是否可接受。4.2 模型把“你”和“你這種人”判成一樣分詞和 OOV 問題現(xiàn)象驗證集上“你這種人真惡心”被漏判但“你這種人真惡心啊”能判對。原因分詞后“你這種人”可能被切成 OOV 或低頻詞嵌入向量沒學好。解決用子詞分詞BERT 的 WordPiece 或 BPE或者對 TextCNN 加字符級嵌入。另外把訓練集里出現(xiàn)次數(shù)少于 3 的詞統(tǒng)一替換成UNK減少噪聲。4.3 訓練損失正常但驗證損失爆炸學習率太大或批次太小現(xiàn)象訓練 loss 穩(wěn)步下降驗證 loss 從第 2 個 epoch 開始飆升。原因學習率太大模型在驗證集上震蕩或者批次太小梯度噪聲大。解決學習率降 10 倍批次加到 64加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。如果還不行檢查數(shù)據(jù)里有沒有標簽錯誤——我遇到過 3% 的樣本標簽標反了清理后驗證 loss 立刻正常。4.4 推理時單條預測和批量預測結果不一致現(xiàn)象用model.predict()單條跑和用model(batch)批量跑同一條文本結果不同。原因忘了設model.eval()dropout 和 batch norm 在訓練模式下行為不同。解決推理前必須model.eval()并用torch.no_grad()包住。另外單條預測時序列長度是 1批量時 padding 到 max_len如果模型對 padding 敏感結果就會飄。統(tǒng)一用批量推理或者單條也 padding 到固定長度。4.5 換數(shù)據(jù)集后 F1 掉 20 個點領域偏移沒處理現(xiàn)象在 Twitter 數(shù)據(jù)上訓到 F1 0.85換到 Reddit 數(shù)據(jù)直接掉到 0.65。原因兩個平臺的用語風格、欺凌表達方式差異大模型沒泛化能力。解決做領域自適應比如在目標領域數(shù)據(jù)上繼續(xù)微調或者用對抗訓練讓特征跨領域不變。簡單做法是把兩個領域數(shù)據(jù)混在一起訓練但給領域標簽讓模型學領域無關特征。如果目標領域數(shù)據(jù)太少至少做一輪在目標數(shù)據(jù)上的微調學習率設小一點。5. 進階技巧用注意力可視化驗證模型到底學到了什么模型跑通之后別只看 F1。欺凌檢測是個敏感場景你需要知道模型是“真的學到了欺凌特征”還是“學到了數(shù)據(jù)集的偏見”。比如如果數(shù)據(jù)集里所有帶“stupid”的句子都被標為欺凌模型可能只是學會了這個詞而不是理解語境。驗證方法是用注意力權重可視化看模型在預測時關注了哪些詞。對于 BERT 模型可以用transformers的output_attentionsTrue拿到注意力矩陣然后對最后一層所有頭取平均畫成熱力圖。下面是一個最小示例from transformers import BertTokenizer, BertForSequenceClassification import torch tokenizer BertTokenizer.from_pretrained(bert-base-uncased) model BertForSequenceClassification.from_pretrained(bert-base-uncased, num_labels2) model.eval() text you are so stupid and nobody likes you inputs tokenizer(text, return_tensorspt, paddingTrue, truncationTrue, max_length64) with torch.no_grad(): outputs model(**inputs, output_attentionsTrue) # 取最后一層注意力平均所有頭 attn outputs.attentions[-1].mean(dim1).squeeze(0) # [seq_len, seq_len] # 取 [CLS] 對所有 token 的注意力 cls_attn attn[0].tolist() tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) for token, weight in zip(tokens, cls_attn): print(f{token}: {weight:.4f})邏輯說明outputs.attentions是一個元組每個元素對應一層的注意力張量形狀是[batch, num_heads, seq_len, seq_len]。取[-1]是最后一層mean(dim1)是對所有注意力頭取平均。attn[0]是[CLS]對所有 token 的注意力權重越高說明模型越關注那個詞。參數(shù)上max_length64要和訓練時一致否則位置編碼對不上。如果發(fā)現(xiàn)模型只關注“stupid”而忽略了“you are”說明它學的是詞級特征而非語境。這時候可以嘗試在訓練數(shù)據(jù)里加入反事實樣本比如“you are so smart”標為非欺凌讓模型學會區(qū)分形容詞的極性。另一個技巧是加一個輔助任務讓模型同時預測文本的情感極性和欺凌標簽多任務學習能迫使模型學到更魯棒的特征。我自己的習慣是每次模型上線前隨機抽 50 條測試樣本人工看注意力熱力圖確認沒有明顯的偏見模式。這個步驟花不了半小時但能避免很多“模型看起來準但實際不可靠”的問題。希望幫到你。本文還有配套的精品資源點擊獲取