體識(shí)別實(shí)戰(zhàn):BiLSTM+CRF、IDCNN+CRF與BERT模型對(duì)比)
簡(jiǎn)介面向中文命名實(shí)體識(shí)別NER研究與實(shí)踐的Python源碼資源整合了雙向長(zhǎng)短期記憶網(wǎng)絡(luò)與條件隨機(jī)場(chǎng)BILSTMCRF、膨脹卷積與條件隨機(jī)場(chǎng)IDCNNCRF、預(yù)訓(xùn)練模型BERT結(jié)合雙向長(zhǎng)短期記憶網(wǎng)絡(luò)與條件隨機(jī)場(chǎng)BERTBILSTMCRF三條主流模型路線并附帶人民日?qǐng)?bào)、MSRA等經(jīng)典中文語(yǔ)料的預(yù)處理腳本適合自然語(yǔ)言處理方向的學(xué)生、算法工程師以及以畢設(shè)、課設(shè)為目標(biāo)的初學(xué)者參考。壓縮包內(nèi)共有五十八個(gè)文件核心是十六個(gè)Python腳本涵蓋模型定義、數(shù)據(jù)清洗、訓(xùn)練與預(yù)測(cè)全流程另有十九個(gè)編譯后的緩存文件、九個(gè)文本說(shuō)明、五張示意圖、四個(gè)說(shuō)明文檔和三個(gè)XML工程配置文件整體大小約十三點(diǎn)七五MB目錄結(jié)構(gòu)清晰便于按模塊查閱。代碼均經(jīng)過(guò)運(yùn)行測(cè)試并配有說(shuō)明文檔和可視化結(jié)果圖學(xué)習(xí)與二次開(kāi)發(fā)門檻較低。目前已有五百二十五人學(xué)習(xí)多模型對(duì)比實(shí)現(xiàn)可幫助讀者直觀理解不同結(jié)構(gòu)在中文實(shí)體識(shí)別任務(wù)中的表現(xiàn)差異也能直接用于畢業(yè)設(shè)計(jì)或課程設(shè)計(jì)、項(xiàng)目初期立項(xiàng)演示。1. 中文命名實(shí)體識(shí)別三套主流模型的 Python 源碼怎么落地先回答最直接的問(wèn)題中文命名實(shí)體識(shí)別到底難在哪難在中文沒(méi)有天然空格分詞、實(shí)體的邊界和類別常常要依賴上下文而常見(jiàn)的規(guī)則匹配和 HMM 在真實(shí)語(yǔ)料上很快就到天花板。這份 NER_DEMO 源碼包把三條主流路線放在一起對(duì)比BILSTMCRF、IDCNNCRF以及熱度最高的 BERTBILSTMCRF并自帶人民日?qǐng)?bào)、MSRA 等數(shù)據(jù)集的預(yù)處理腳本和訓(xùn)練入口對(duì)正在做課程設(shè)計(jì)、畢設(shè)或者剛接觸序列標(biāo)注的開(kāi)發(fā)者來(lái)說(shuō)是少見(jiàn)的能直接跑起來(lái)的完整工程。它解決的是一類很具體的問(wèn)題從原始標(biāo)注文本到最終能識(shí)別出人名、地名、組織名的完整鏈路包含數(shù)據(jù)清洗、vocab 構(gòu)建、模型訓(xùn)練、日志輸出和預(yù)測(cè)。適合的人群是已經(jīng)會(huì)寫(xiě) Python、能看懂神經(jīng)網(wǎng)絡(luò)基本結(jié)構(gòu)但沒(méi)完整跑過(guò) NER 項(xiàng)目的同學(xué)或者想快速對(duì)比三種模型效果的候選人。2. 目錄結(jié)構(gòu)和數(shù)據(jù)準(zhǔn)備先看懂這份資源怎么組織再談?dòng)?xùn)練2.1 源碼包的文件清單和職責(zé)劃分拿到 NER_DEMO-master.zip 解壓后第一件事不是急著裝環(huán)境而是把目錄結(jié)構(gòu)摸清楚。整個(gè)工程可以分成四塊模型定義、數(shù)據(jù)預(yù)處理、公共工具、入口腳本。模型定義部分放在根目錄下五個(gè) Python 文件對(duì)應(yīng)五種網(wǎng)絡(luò)結(jié)構(gòu)BILSTM_CRF.py 是最經(jīng)典的 BiLSTM 加 CRF 序列標(biāo)注IDCNN_CRF.py 是膨脹卷積替代 LSTM 的變體IDCNN5_CRF.py 是 IDCNN 的加深版本BILSTM_Attetion_CRF.py 在 BiLSTM 基礎(chǔ)上加了 attention 層BERT_BILSTM_CRF.py 則是用預(yù)訓(xùn)練 BERT 接 BiLSTM 再接 CRF 的最強(qiáng)組合。預(yù)處理腳本在 DataProcess 目錄下msra_preprocessing.py、renminribao_preprocessing.py、data2_preprocessing.py 分別處理不同來(lái)源的數(shù)據(jù)集。process_data.py 負(fù)責(zé)統(tǒng)一流程。工具層是 utils.py 和 path.py前者存放標(biāo)簽映射、數(shù)據(jù)讀取、批次生成這一類公共函數(shù)后者統(tǒng)一管理數(shù)據(jù)、模型、日志的文件路徑。入口腳本是 train.py所有模型的訓(xùn)練都從這里啟動(dòng)日志輸出到 log 目錄下。提示先打開(kāi) README.md 再動(dòng)手它會(huì)說(shuō)明當(dāng)前代碼依賴的深度學(xué)習(xí)框架版本以及各模型對(duì)應(yīng)哪個(gè)數(shù)據(jù)集格式。這能幫你省掉最常見(jiàn)的框架不兼容問(wèn)題。2.2 數(shù)據(jù)集的格式標(biāo)準(zhǔn)和 vocab 構(gòu)建流程N(yùn)ER 訓(xùn)練數(shù)據(jù)的標(biāo)準(zhǔn)格式是每一行一個(gè)詞空行分隔句子標(biāo)簽序列和詞語(yǔ)序列按行對(duì)齊。以人民日?qǐng)?bào)語(yǔ)料為例預(yù)處理腳本會(huì)把原始文本解析成這種格式保存比如“中 國(guó) B-ORG”“人 民 E-ORG”“銀 行 B-ORG”這種 BIESO 標(biāo)注體系。vocab.py 和 process_data.py 負(fù)責(zé)把清洗后的文本轉(zhuǎn)成訓(xùn)練用的 id 序列。vocab 構(gòu)建的核心邏輯是把每個(gè)字符映射到一個(gè)唯一的索引再補(bǔ)充特殊標(biāo)記。常見(jiàn)做法是把 vocab 分成四個(gè)部分pad 用于批內(nèi)補(bǔ)齊、unk 用于未登錄詞、sos 和 eos 用于句子邊界。具體代碼邏輯一般是def build_vocab(files, min_count1): word_count {} for file in files: for line in open(file, encodingutf-8): for char in line.strip(): word_count[char] word_count.get(char, 0) 1 vocab {word: idx for word, idx in zip([pad, unk, sos, eos], range(4))} for word, count in word_count.items(): if count min_count: vocab[word] len(vocab) return vocab這段代碼先統(tǒng)計(jì)所有字符的出現(xiàn)次數(shù)再按最低頻次過(guò)濾低頻字符。min_count 取 1 時(shí)保留全部字符取 2 或 3 時(shí)能過(guò)濾一部分錯(cuò)別字和噪聲但也會(huì)讓模型對(duì)生僻字的泛化能力下降。我一般會(huì)先把 min_count 設(shè)為 1 跑通再看訓(xùn)練集大小決定是否過(guò)濾。2.3 預(yù)處理腳本的參數(shù)含義和實(shí)際用法msra_preprocessing.py 是處理 MSRA 數(shù)據(jù)集的典型例子。原始 MSRA 語(yǔ)料以換行符分隔句子每行是一句完整的中文句子附帶的標(biāo)注文件需要按字符位置對(duì)齊才能生成訓(xùn)練數(shù)據(jù)。python msra_preprocessing.py --input_dir ./data/msra_raw --output_dir ./data2這個(gè)命令把 MSRA 原始文本和標(biāo)注文件轉(zhuǎn)換成統(tǒng)一的 BIO 格式輸出到 data2 目錄。腳本內(nèi)部會(huì)檢查句子長(zhǎng)度、標(biāo)簽對(duì)齊關(guān)系如果發(fā)現(xiàn)字符數(shù)和標(biāo)簽數(shù)不匹配會(huì)自動(dòng)跳過(guò)該句。運(yùn)行完檢查 data2 下的文件行數(shù)是否和原始標(biāo)注數(shù)據(jù)大致匹配如果出現(xiàn)大量句子被跳過(guò)通常是原始標(biāo)注文件里有全角空格或空行需要先做清洗。參數(shù)說(shuō)明--input_dir 指向原始數(shù)據(jù)目錄--output_dir 指向處理后數(shù)據(jù)存放的位置。不同數(shù)據(jù)集腳本的輸入輸出參數(shù)基本一致這套工程的預(yù)處理腳本是按統(tǒng)一接口設(shè)計(jì)的所以換數(shù)據(jù)集時(shí)不需要改模型代碼。2.4 環(huán)境配置和快速驗(yàn)證數(shù)據(jù)完整性訓(xùn)練前先確認(rèn)三件事PyTorch 版本、GPU 是否可用、transformers 庫(kù)是否安裝。BERT 模型需要從 huggingface 加載預(yù)訓(xùn)練權(quán)重而國(guó)內(nèi)網(wǎng)絡(luò)環(huán)境下載中文預(yù)訓(xùn)練模型通常比較慢建議先手動(dòng)下載 chinese_L-12_H-768_A-12 到本地 data 目錄。pip install torch transformers tqdm numpy python train.py --model BERT_BILSTM_CRF --epochs 3 --batch_size 8 --gpu 0注意如果報(bào) transformers 加載權(quán)重超時(shí)直接把預(yù)訓(xùn)練模型下載到 data/chinese_L-12_H-768_A-12 目錄然后修改 path.py 里的預(yù)訓(xùn)練模型路徑指向本地目錄能大幅縮短啟動(dòng)時(shí)間。3. 三種模型的原理和訓(xùn)練配置為什么選 BiLSTMCRF、IDCNNCRF 和 BERTBiLSTMCRF3.1 BiLSTMCRF 的原理和代碼走讀BiLSTMCRF 是序列標(biāo)注領(lǐng)域的經(jīng)典結(jié)構(gòu)很多線上 NER 系統(tǒng)到目前仍是這套組合。BiLSTM 負(fù)責(zé)編碼上下文信息把字符序列轉(zhuǎn)換成每個(gè)位置對(duì)應(yīng)的隱狀態(tài)向量而 CRF 層負(fù)責(zé)學(xué)習(xí)標(biāo)簽之間的轉(zhuǎn)移約束比如“B-PER 后面不能直接跟 E-ORG”這樣的規(guī)則。class BiLSTM_CRF(nn.Module): def __init__(self, vocab_size, tag_size, embedding_dim128, hidden_dim256): super(BiLSTM_CRF, self).__init__() self.embedding nn.Embedding(vocab_size, embedding_dim) self.lstm nn.LSTM(embedding_dim, hidden_dim // 2, num_layers1, bidirectionalTrue, batch_firstTrue) self.hidden2tag nn.Linear(hidden_dim, tag_size) self.crf CRF(tag_size)這段代碼里 embedding_dim 是字符向量維度hidden_dim 是 LSTM 隱層大小因?yàn)槭请p向結(jié)構(gòu)所以 hidden_dim 會(huì)被平分到兩個(gè)方向。CRF 層接收 LSTM 輸出的發(fā)射分?jǐn)?shù)矩陣然后通過(guò)維特比解碼找到最優(yōu)標(biāo)簽序列。訓(xùn)練時(shí)使用負(fù)對(duì)數(shù)似然損失這個(gè)損失由真實(shí)路徑分?jǐn)?shù)減去所有可能路徑的 logsumexp 分?jǐn)?shù)得到。3.2 IDCNNCRF 的結(jié)構(gòu)差異和適用場(chǎng)景IDCNN 是膨脹卷積網(wǎng)絡(luò)的縮寫(xiě)它在普通 CNN 的基礎(chǔ)上引入了不同膨脹率的卷積核讓感受野在不增加參數(shù)量和層數(shù)的情況下指數(shù)級(jí)擴(kuò)大。和 BiLSTM 相比IDCNN 的訓(xùn)練速度更快推理時(shí)也不需要逐步計(jì)算適合對(duì)實(shí)時(shí)性有要求的場(chǎng)景。class IDCNN_CRF(nn.Module): def __init__(self, vocab_size, tag_size, filters128, dilations[1, 1, 2]): super(IDCNN_CRF, self).__init__() self.embedding nn.Embedding(vocab_size, embedding_dim) self.idcnn IDCNN(filtersfilters, dilationsdilations) self.linear nn.Linear(filters, tag_size) self.crf CRF(tag_size)dilations 參數(shù)是 IDCNN 的核心控制項(xiàng)[1, 1, 2] 表示三層卷積的膨脹率分別是 1、1、2。膨脹率為 1 時(shí)是普通卷積膨脹率為 2 時(shí)每個(gè)卷積核的采樣點(diǎn)間隔為 1 個(gè)字符。實(shí)際使用中 IDCNN5_CRF.py 會(huì)疊加更多層以擴(kuò)大感受野。這里的核心權(quán)衡是膨脹率越大感受野越大但過(guò)大會(huì)導(dǎo)致局部信息丟失所以工程上常采用多組小膨脹率的堆疊而不是一組大膨脹率。3.3 BERTBiLSTMCRF 的模型結(jié)構(gòu)和訓(xùn)練技巧BERTBILSTMCRF 是目前效果最好的組合之一原理是先用 BERT 預(yù)訓(xùn)練模型生成融合上下文語(yǔ)義的字符向量再把向量序列輸入 BiLSTM 進(jìn)一步編碼最后由 CRF 解碼。這份源碼里 BERT 部分用的是 chinese_L-12_H-768_A-12即 12 層 transformer、768 維隱層這是 BERT-base 的中文版本。class BERT_BILSTM_CRF(nn.Module): def __init__(self, bert_path, tag_size, hidden_dim256): super(BERT_BILSTM_CRF, self).__init__() self.bert BertModel.from_pretrained(bert_path) self.lstm nn.LSTM(768, hidden_dim // 2, bidirectionalTrue, batch_firstTrue) self.hidden2tag nn.Linear(hidden_dim, tag_size) self.crf CRF(tag_size) def forward(self, input_ids, attention_mask, labelsNone): outputs self.bert(input_ids, attention_maskattention_mask) sequence_output outputs[0] lstm_out, _ self.lstm(sequence_output) emissions self.hidden2tag(lstm_out) if labels is not None: loss -self.crf(emissions, labels, maskattention_mask) return loss return self.crf.decode(emissions, maskattention_mask)訓(xùn)練 BERT 系模型時(shí)有幾個(gè)關(guān)鍵點(diǎn)。一是學(xué)習(xí)率要比 BiLSTM 模型低一等數(shù)量級(jí)BERT 部分的微調(diào)學(xué)習(xí)率一般設(shè)置在 5e-5 到 2e-5而 BiLSTM 部分可以用 1e-3二是 batch_size 受顯存限制通常只能設(shè) 8 到 16所以訓(xùn)練輪數(shù)要相應(yīng)增加三是最耗時(shí)間的不是構(gòu)思模型而是 BERT 預(yù)訓(xùn)練權(quán)重的加載建議固定隨機(jī)種子保證可復(fù)現(xiàn)。3.4 三種模型的選型對(duì)比和場(chǎng)景建議三種模型的效果和代價(jià)差異非常明顯。在一個(gè) 2 萬(wàn)句規(guī)模的中文語(yǔ)料上BiLSTMCRF 訓(xùn)練大約需要 2 到 4 小時(shí)IDCNNCRF 能快 30% 以上而 BERTBiLSTMCRF 在同樣數(shù)據(jù)上可能要多花 4 到 8 小時(shí)但 F1 值通常能高 3 到 8 個(gè)百分點(diǎn)。模型組合訓(xùn)練速度顯存占用精度上限適用場(chǎng)景BiLSTMCRF快低中標(biāo)注數(shù)據(jù)少、硬件受限的場(chǎng)景IDCNNCRF更快低中上需要快速迭代或?qū)崟r(shí)推理的業(yè)務(wù)BERTBiLSTMCRF慢高高追求效果上限硬件資源充裕4. 訓(xùn)練與調(diào)參實(shí)戰(zhàn)從啟動(dòng)命令到日志分析全流程可復(fù)現(xiàn)4.1 訓(xùn)練入口的參數(shù)體系和啟動(dòng)方式train.py 是整個(gè)工程的統(tǒng)一入口通過(guò) --model 參數(shù)選擇不同的模型結(jié)構(gòu)。這個(gè)腳本定義了完整的訓(xùn)練循環(huán)包括批次生成、反向傳播、梯度裁剪、驗(yàn)證集評(píng)估和模型保存。# 訓(xùn)練 BiLSTMCRF 模型 python train.py --model BILSTM_CRF --epochs 10 --batch_size 32 --lr 0.001 # 訓(xùn)練 IDCNNCRF 模型 python train.py --model IDCNN_CRF --epochs 10 --batch_size 32 --lr 0.001 # 訓(xùn)練 BERTBiLSTMCRF 模型 python train.py --model BERT_BILSTM_CRF --epochs 3 --batch_size 8 --lr 5e-5第二個(gè)命令運(yùn)行時(shí)會(huì)在根目錄創(chuàng)建一個(gè) log 目錄每次訓(xùn)練會(huì)生成一個(gè)以時(shí)間戳命名的輸出文件。日志里記錄了 loss 和準(zhǔn)確率等指標(biāo)訓(xùn)練到最優(yōu) F1 時(shí)保存模型權(quán)重。這里 --lr 的選擇很講究對(duì)于 BiLSTM 和 IDCNN0.001 是 Adam 優(yōu)化器的默認(rèn)推薦值BERT 模型必須調(diào)低否則預(yù)訓(xùn)練權(quán)重很快會(huì)被沖掉。提示使用 BERT_BILSTM_CRF 模型前先檢查 python train.py --help 輸出的參數(shù)列表確認(rèn)當(dāng)前代碼里 BERT 相關(guān)參數(shù)名是否帶 bert_ 前綴不同版本的源碼差異很大。4.2 train.py 核心訓(xùn)練循環(huán)的代碼走讀for epoch in range(args.epochs): for batch in train_batches: optimizer.zero_grad() loss model(batch.ids, batch.labels) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() val_f1 evaluate(model, valid_batches) print(fepoch {epoch}, loss {loss.item():.4f}, F1 {val_f1:.4f})這里最關(guān)鍵的是 clip_grad_norm_ 這一行把梯度的 L2 范數(shù)裁剪到 5.0防止 LSTM 在長(zhǎng)序列上出現(xiàn)梯度爆炸。設(shè)置太小會(huì)導(dǎo)致模型收斂變慢設(shè)置太大等于沒(méi)裁剪。在 BiLSTMCRF 的序列標(biāo)注任務(wù)里我一般會(huì)先用 5.0 起步如果 loss 出現(xiàn) NaN 就降到 1.0。evaluate 函數(shù)在驗(yàn)證集上做維特比解碼統(tǒng)計(jì)實(shí)體的精確率、召回率和 F1 值。4.3 驗(yàn)證集和測(cè)試集的劃分邏輯數(shù)據(jù)切分時(shí)訓(xùn)練集、驗(yàn)證集、測(cè)試集的比例按照 8:1:1 劃分驗(yàn)證集用于每個(gè) epoch 結(jié)束后評(píng)估并選擇保存哪一版模型測(cè)試集只做最終評(píng)估不參與調(diào)參。在實(shí)際使用中驗(yàn)證集切分時(shí)需要按句子分而不是按字符分因?yàn)橥痪渥拥淖址瑫r(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集會(huì)污染數(shù)據(jù)。此外數(shù)據(jù)文件在預(yù)處理階段就做了隨機(jī)打亂所以訓(xùn)練時(shí) story 層的批次生成不再重復(fù) shuffle。4.4 從日志判斷模型是否正常收斂模型跑起來(lái)后的 30 分鐘內(nèi)基本就能看出問(wèn)題。正常收斂的 loss 曲線是平滑下降的偶爾有小幅波動(dòng)但在合理范圍內(nèi)如果 loss 在前 100 個(gè) batch 內(nèi)直接降到接近 0大概率是標(biāo)簽對(duì)齊錯(cuò)了或者 CRF 層的轉(zhuǎn)移矩陣初始化出了問(wèn)題。另一個(gè)常見(jiàn)現(xiàn)象是訓(xùn)練集 F1 很高但驗(yàn)證集 F1 很低這說(shuō)明過(guò)擬合已經(jīng)出現(xiàn)把 dropout 打開(kāi)或者提前停止訓(xùn)練即可。參數(shù)組合建議語(yǔ)料在 1 萬(wàn)句以下時(shí)用 BiLSTMCRFembedding_dim 設(shè) 128hidden_dim 設(shè) 256dropout 設(shè) 0.5訓(xùn)練 15 輪左右語(yǔ)料在 3 萬(wàn)句以上且 GPU 顯存大于 8G 時(shí)再上 BERTBiLSTMCRFbatch_size 從 4 起步顯存不夠就加梯度累積用 4 個(gè) batch 累積一次反向傳播等效于 batch_size 為 16。5. 避坑與常見(jiàn)問(wèn)題我在復(fù)現(xiàn)這套源碼時(shí)踩過(guò)的五個(gè)真實(shí)坑5.1 Python 版本和依賴沖突現(xiàn)象是運(yùn)行 train.py 時(shí) import 階段報(bào)錯(cuò)錯(cuò)誤信息顯示某個(gè)庫(kù)的模塊里沒(méi)有需要的函數(shù)比如 transformers 的 BertModel 加載失敗。原因是源碼是基于較老的 PyTorch 版本編寫(xiě)的現(xiàn)代版本的 transformers 接口不再兼容舊版寫(xiě)法。解決辦法是把 transformers 降級(jí)到 4.x 早期版本同時(shí) PyTorch 安裝在 1.8 到 2.0 之間。如果非要使用新版需要手動(dòng)修改 BERT_BILSTM_CRF.py 中的 from_pretrained 調(diào)用方式。5.2 預(yù)處理階段句子被大量跳過(guò)現(xiàn)象是 msra_preprocessing.py 運(yùn)行結(jié)束后輸出文件的行數(shù)比原始標(biāo)注文件少了很多。原因是原始語(yǔ)料里有很多全角空格、連續(xù)換行和編碼不一致的字符腳本在檢查標(biāo)簽對(duì)齊時(shí)會(huì)跳過(guò)這些異常句子。解決方法是先用文本編輯器把原始數(shù)據(jù)統(tǒng)一轉(zhuǎn)成 UTF-8 編碼刪除所有全角空格再重新運(yùn)行預(yù)處理。如果 skip 率還是超過(guò) 5%用 Python 腳本打印幾條被跳過(guò)的樣本看看具體是在哪個(gè)環(huán)節(jié)斷掉的。5.3 BERT 權(quán)重加載超時(shí)現(xiàn)象是第一次啟動(dòng) BERT_BILSTM_CRF 訓(xùn)練時(shí)程序長(zhǎng)時(shí)間卡在 from_pretrained 這一步最后報(bào)網(wǎng)絡(luò)超時(shí)錯(cuò)誤。原因是 huggingface 的預(yù)訓(xùn)練權(quán)重下載地址在國(guó)內(nèi)訪問(wèn)不穩(wěn)定尤其是 chinese_L-12_H-768_A-12 這個(gè)文件體積接近 400M。解決方法是手動(dòng)下載權(quán)重文件后放到 data/chinese_L-12_H-768_A-12 目錄然后在 path.py 或直接用 from_pretrained 指定本地路徑。5.4 Sigmoid 和 Softmax 混淆導(dǎo)致的實(shí)體邊界錯(cuò)誤現(xiàn)象是 BERT_BILSTM_CRF 的 loss 一直在下降但驗(yàn)證集上預(yù)測(cè)出來(lái)的實(shí)體永遠(yuǎn)只有單字不會(huì)有連續(xù)的多個(gè)字。原因是部分實(shí)現(xiàn)把輸出層誤寫(xiě)成 sigmoid 激活然后按閾值判定而不是用線性層輸出發(fā)射分?jǐn)?shù)交給 CRF 解碼。CRF 的維度數(shù)量必須等于標(biāo)簽數(shù)加一因?yàn)槠鹗紭?biāo)記要單獨(dú)占一個(gè)位置這個(gè)細(xì)節(jié)屬于最隱蔽的那類問(wèn)題排查時(shí)直接看標(biāo)簽類別數(shù)量和預(yù)測(cè)結(jié)果來(lái)定位。5.5 顯存不足和 OOM 崩潰現(xiàn)象是 BERT_BILSTM_CRF 模型訓(xùn)練到第二個(gè) epoch 時(shí)直接報(bào) CUDA out of memory 崩潰。原因是 BERT 本身占用顯存非常高batch_size 設(shè)成 32 對(duì) 12G 顯存來(lái)說(shuō)完全不現(xiàn)實(shí)。解決辦法是把 batch_size 降到 4同時(shí)在 data 加載時(shí)用 num_workers 提高數(shù)據(jù)吞吐。要是還撐不住就檢查訓(xùn)練代碼里是否每個(gè) epoch 結(jié)束后把梯度和中間變量都釋放了這通常能省出 30% 的顯存。6. 進(jìn)階驗(yàn)證與微調(diào)技巧讓三套模型在自定義數(shù)據(jù)集上真正可用6.1 用模型預(yù)測(cè)結(jié)果做人工校驗(yàn)訓(xùn)練好模型后不能只看 F1 值就認(rèn)為完事了。我習(xí)慣準(zhǔn)備一份不帶標(biāo)簽的測(cè)試文本直接跑預(yù)測(cè)來(lái)看實(shí)際輸出。抽取一段新聞文本模型輸出的實(shí)體列表如果能把“北京”“騰訊”“小明”等實(shí)體準(zhǔn)確切分出來(lái)說(shuō)明邊界沒(méi)有問(wèn)題如果連續(xù)出現(xiàn)實(shí)體被拆分或合并大概率是標(biāo)簽體系的 BIO 和 BIESO 不一致。python train.py --model BILSTM_CRF --predict --input_text 小明來(lái)自北京在騰訊擔(dān)任算法工程師。這段偽代碼展示的是預(yù)測(cè)入口的方式。實(shí)際源碼中會(huì)把預(yù)測(cè)邏輯封裝在 eval 分支或者提供單獨(dú)的小腳本讀取訓(xùn)練好的模型參數(shù)。輸出格式通常是一行文字加一行對(duì)應(yīng)標(biāo)簽人工檢查時(shí)重點(diǎn)看三類錯(cuò)誤實(shí)體漏召回、實(shí)體內(nèi)多字、實(shí)體間粘連。6.2 自定義數(shù)據(jù)集的標(biāo)簽體系對(duì)齊每個(gè)數(shù)據(jù)集都有自己的一套標(biāo)簽體系。人民日?qǐng)?bào)語(yǔ)料常用 PER、ORG、LOC 三類一些金融語(yǔ)料還會(huì)增加 DATE、MONEY 等類別。在換數(shù)據(jù)集前必須做兩件事把原始標(biāo)注統(tǒng)一成 BIO 格式標(biāo)簽類別映射表要和 utils.py 里的 category_to_index 對(duì)應(yīng)。常見(jiàn)做法是先寫(xiě)小腳本把數(shù)據(jù)轉(zhuǎn)成通用格式標(biāo)簽用 B-xxx、I-xxx、O 這三種基礎(chǔ)標(biāo)記然后在需要的時(shí)候轉(zhuǎn)換成 BIESO 的 B、I、E、S、O 五種標(biāo)記來(lái)切分單字實(shí)體。這里的核心準(zhǔn)則是標(biāo)簽不統(tǒng)一的問(wèn)題要在預(yù)處理階段解決不要指望模型自己適應(yīng)不同標(biāo)簽體系。def convert_bio_to_bieso(labels): # labels 是一串 Bio 標(biāo)簽的列表 converted [] for idx, label in enumerate(labels): if label.startswith(B-): # 如果下一個(gè)是 O 或者 B 開(kāi)頭說(shuō)明這是單字實(shí)體 if idx 1 len(labels) or labels[idx 1] O or labels[idx 1].startswith(B-): converted.append(S- label[2:]) else: converted.append(B- label[2:]) elif label.startswith(I-): if idx 1 len(labels) or labels[idx 1].startswith(B-) or labels[idx 1] O: converted.append(E- label[2:]) else: converted.append(I- label[2:]) else: converted.append(O) return converted這段轉(zhuǎn)換邏輯的處理順序是先判斷是否為單字實(shí)體再?zèng)Q定標(biāo)記是 B、I、E 還是 S。轉(zhuǎn)換完成后必須檢查一下代碼里 CRF 層的標(biāo)簽數(shù)量是否等于五類乘實(shí)體類別數(shù)加 O否則張量維度就會(huì)報(bào)錯(cuò)。6.3 學(xué)習(xí)率衰減和早停策略如果訓(xùn)練到后期 F1 值一直在 80 到 82 之間震蕩不上升問(wèn)題大概率是學(xué)習(xí)率沒(méi)做衰減。我的習(xí)慣是每?jī)蓚€(gè) epoch 把學(xué)習(xí)率乘 0.8或者在驗(yàn)證集 F1 連續(xù)三個(gè) epoch 不增長(zhǎng)時(shí)把學(xué)習(xí)率減半。早停設(shè)置在驗(yàn)證 F1 連續(xù)五個(gè) epoch 不提升時(shí)直接停止訓(xùn)練并加載歷史上最優(yōu)權(quán)重這個(gè)辦法能明顯減少 BERT 模型在過(guò)擬合邊緣的無(wú)效訓(xùn)練時(shí)間。6.4 多模型集成的一個(gè)輕量技巧資源包里五套模型經(jīng)常被當(dāng)成選擇題其實(shí)可以做成組合。時(shí)間允許的話用同一個(gè)驗(yàn)證集分別訓(xùn)練 BiLSTMCRF、IDCNNCRF 和 BERTBILSTMCRF在預(yù)測(cè)階段把三個(gè)模型的輸出做投票或者對(duì)每個(gè) token 計(jì)算不同模型的概率平均。實(shí)際體驗(yàn)中 BERT 模型的權(quán)重通常要比其他兩個(gè)略高0.5 比 0.3 比 0.2 的組合在多數(shù)數(shù)據(jù)集上效果不錯(cuò)。雖然比單一模型增加了一些推理耗時(shí)但性能不穩(wěn)定時(shí)用 10 分鐘換取幾個(gè)點(diǎn)的準(zhǔn)確率提升是劃算的。這類的坑我在每一次換數(shù)據(jù)集時(shí)都會(huì)再踩一遍尤其是標(biāo)簽體系對(duì)齊和預(yù)處理階段的數(shù)據(jù)清洗。后來(lái)養(yǎng)成的習(xí)慣是不管用什么數(shù)據(jù)集先跑完預(yù)處理腳本后馬上檢查行數(shù)和異常樣本再進(jìn)入訓(xùn)練環(huán)節(jié)。從拿到這份源碼到完全跑通我前前后后花了大半天時(shí)間但把里面的坑摸清后后續(xù)換任何數(shù)據(jù)都能在兩小時(shí)內(nèi)完成全流程。希望這份實(shí)戰(zhàn)筆記能幫到你沿著這個(gè)流程走三套模型都能順利跑起來(lái)。本文還有配套的精品資源點(diǎn)擊獲取