)
簡介本資源為基于Python實現(xiàn)雙向LSTM條件隨機場CRF的命名實體識別模型課程作業(yè)完整包面向計算機、人工智能、自動化等專業(yè)的學生與教師適用于期末課程設計、課程大作業(yè)或畢業(yè)設計場景。項目聚焦NLP四大基礎任務之一的序列標注通過循環(huán)神經(jīng)網(wǎng)絡對句子逐字打標簽并在LSTM層后引入CRF模型利用梯度下降自動學習轉移參數(shù)有效避免連續(xù)B-LOC等非法標簽序列從而提升標注準確率。壓縮包共3個文件包含1個py源碼、1個pdf作業(yè)報告和1個md說明文檔整體約201KB源碼經(jīng)本地調試可直接運行報告則完整記錄了實驗思路與結果分析。目前已有138人學習下載得分96分讀者可借此掌握BiLSTM與CRF結合的建模流程、損失函數(shù)設計及序列標注評估方法也可在此基礎上修改調整以適配不同實體類型或數(shù)據(jù)集具備較高的學習借鑒價值。1. 從一份 96 分課設說起B(yǎng)iLSTMCRF 到底解決了什么如果你正在搜「雙向LSTM 命名實體識別 源碼」大概率是兩種情況要么課程大作業(yè)卡在序列標注這一步要么想找一個能跑通、有報告、結構清晰的參考實現(xiàn)。這份資源就是沖著這個場景來的——一個基于 Python 的 BiLSTMCRF 命名實體識別項目附帶作業(yè)報告和說明文檔得分 96。它要解決的核心問題很具體給定一句「我 愛 北 京」模型要輸出「O O B-LOC I-LOC」這樣的標簽序列把地名、人名、機構名從文本里摳出來。命名實體識別是 NLP 四大基礎任務里序列標注的典型代表也是很多人第一次接觸「輸入輸出等長」建模的入口。純 LSTM 能跑但會冒出「B-LOC 后面跟 B-LOC」這種現(xiàn)實中不存在的標簽組合CRF 層就是來兜這個底的。這份源碼把 LSTM 的上下文建模能力和 CRF 的標簽轉移約束拼在一起適合想搞懂序列標注完整鏈路、又不想從零搭框架的人。2. 拆開 sequence_tagging.py數(shù)據(jù)流、模型結構與訓練循環(huán)2.1 從原始句子到嵌入向量輸入管線的三個關鍵決策拿到一份序列標注源碼第一件事不是看模型多大而是看數(shù)據(jù)怎么進、標簽怎么對齊。這份項目的輸入是一條句子輸出是等長的標簽序列中間要經(jīng)過詞表映射、序列填充、批次對齊三步。常見做法是先把語料按字切分中文場景下字級比詞級更穩(wěn)避免分詞誤差傳導統(tǒng)計頻次建詞表低頻字統(tǒng)一映射到UNK然后每條樣本轉成索引序列。真正容易翻車的是填充和 mask。一個 batch 里句子長度不一短句要補PAD到統(tǒng)一長度但PAD位置的損失不能算進總損失否則模型會學著去預測填充位。我一般會在數(shù)據(jù)預處理階段就生成一個 mask 向量訓練時用它乘掉無效位置的 loss。下面這段是典型的批處理邏輯你可以對照自己的實現(xiàn)看有沒有漏掉 maskdef batchify(data, word2id, tag2id, batch_size, pad_tokenPAD): # data: [(words, tags), ...] batches [] for i in range(0, len(data), batch_size): chunk data[i:i batch_size] max_len max(len(w) for w, _ in chunk) word_ids, tag_ids, masks [], [], [] for words, tags in chunk: w_id [word2id.get(w, word2id[UNK]) for w in words] t_id [tag2id[t] for t in tags] # 補齊到 batch 內(nèi)最大長度 pad_len max_len - len(words) w_id [word2id[pad_token]] * pad_len t_id [tag2id[PAD]] * pad_len mask [1] * len(words) [0] * pad_len word_ids.append(w_id) tag_ids.append(t_id) masks.append(mask) batches.append((word_ids, tag_ids, masks)) return batches這段代碼里mask是后續(xù)損失計算的關鍵1表示真實 token0表示填充位。參數(shù)上batch_size一般設 16 到 64太小梯度震蕩太大顯存吃緊pad_token和UNK要分開別混用。如果你發(fā)現(xiàn)訓練 loss 降得很快但驗證集 F1 不動先查 mask 有沒有正確傳到 loss 函數(shù)里。2.2 BiLSTM 編碼層為什么雙向比單向更適合中文 NER單向 LSTM 只能看到當前字左邊的上下文但「北京」這個詞光看「北」不知道后面跟的是不是「京」得雙向才能同時利用前后信息。BiLSTM 就是把一個正向 LSTM 和一個反向 LSTM 的輸出拼起來每個時間步得到2 * hidden_dim維的表示。這份源碼里隱藏層維度通常設 128 或 256層數(shù) 1 到 2 層足夠再深對小數(shù)據(jù)集反而容易過擬合。實現(xiàn)上要注意兩點一是 pack_padded_sequence把填充位壓掉再送進 LSTM否則反向 LSTM 會從PAD開始讀污染上下文表示二是 dropout 的位置一般加在嵌入層之后和 LSTM 輸出之后概率 0.3 到 0.5。下面是一個簡化的編碼層寫法import torch import torch.nn as nn from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence class BiLSTMEncoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers1, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, bidirectionalTrue, batch_firstTrue) self.dropout nn.Dropout(dropout) def forward(self, word_ids, lengths): # word_ids: (B, L), lengths: 每條真實長度 emb self.dropout(self.embedding(word_ids)) packed pack_padded_sequence(emb, lengths, batch_firstTrue, enforce_sortedFalse) out, _ self.lstm(packed) out, _ pad_packed_sequence(out, batch_firstTrue) return self.dropout(out) # (B, L, 2*hidden_dim)padding_idx0讓嵌入層不更新填充位的向量enforce_sortedFalse省去手動排序的麻煩。hidden_dim設 128 時輸出維度是 256后面接 CRF 之前通常再加一個線性層把維度映射到標簽數(shù)。如果你用的是中文數(shù)據(jù)集嵌入維度 128 到 300 都常見預訓練詞向量能加就加對 F1 提升明顯。2.3 CRF 層標簽轉移矩陣是怎么學出來的LSTM 輸出的是每個位置對每個標簽的「發(fā)射分數(shù)」但標簽之間不是獨立的——「B-LOC」后面接「I-LOC」合理接「B-PER」就不合理。CRF 層引入一個轉移矩陣transitions[i][j]表示從標簽 i 轉到標簽 j 的分數(shù)訓練時這個矩陣和 LSTM 參數(shù)一起用梯度下降學。解碼時用 Viterbi 算法找全局最優(yōu)路徑而不是每個位置貪心取最大。這份源碼里 CRF 的實現(xiàn)有兩種常見寫法一種是直接調torchcrf這類庫一種是手寫前向算法和 Viterbi。手寫的好處是能看清 loss 怎么算的。核心是forward算所有路徑的分數(shù)和配分函數(shù)viterbi找最優(yōu)路徑。下面這段是手寫 CRF 的關鍵部分class CRF(nn.Module): def __init__(self, num_tags): super().__init__() self.num_tags num_tags # 轉移矩陣transitions[i][j] 表示 i - j 的分數(shù) self.transitions nn.Parameter(torch.randn(num_tags, num_tags)) # 非法轉移如 START - 非 B在訓練中會被壓低 self.start_trans nn.Parameter(torch.randn(num_tags)) self.end_trans nn.Parameter(torch.randn(num_tags)) def forward(self, emissions, tags, mask): # emissions: (B, L, num_tags), tags: (B, L), mask: (B, L) # 返回負對數(shù)似然訓練時最小化 ...transitions是隨機初始化的訓練幾輪后你會看到「B-LOC - I-LOC」的分數(shù)明顯高于「B-LOC - B-LOC」這就是 CRF 在自動學約束。參數(shù)上num_tags等于標簽集大小比如 BIO 標注下 LOC/PER/ORG 各兩個標簽加 O就是 7 個。注意PAD標簽不要參與轉移計算mask 要傳進 CRF 的 loss 里。2.4 訓練循環(huán)與評估loss 怎么算、F1 怎么看訓練循環(huán)本身不復雜但序列標注的評估不能只看準確率。因為「O」標簽通常占大多數(shù)全預測 O 也能有很高的 accuracy但 F1 會慘不忍睹。這份報告里用的應該是實體級別的 precision/recall/F1也就是把預測的標簽序列還原成實體區(qū)間再和真實區(qū)間比對。def train_step(model, crf, batch, optimizer): word_ids, tag_ids, masks batch lengths [sum(m) for m in masks] emissions model(word_ids, lengths) # CRF 的負對數(shù)似然 loss crf.neg_log_likelihood(emissions, tag_ids, masks) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() return loss.item()梯度裁剪max_norm5.0是防梯度爆炸的常規(guī)操作LSTM 加 CRF 的結構對學習率比較敏感一般用 Adam學習率 1e-3 起步訓練 20 到 50 輪看驗證集 F1 什么時候不再漲。如果你發(fā)現(xiàn) loss 變成 nan先查學習率是不是太大再查 CRF 的轉移矩陣有沒有出現(xiàn)極端值。3. 跑通這份源碼環(huán)境、參數(shù)與復現(xiàn)步驟3.1 環(huán)境配置Python 版本、依賴與常見安裝坑這份項目是純 Python 實現(xiàn)核心依賴是 PyTorch。環(huán)境上建議 Python 3.8 到 3.10太新的版本有些舊版 torch 裝不上。依賴清單一般包括torch、numpy、tqdm如果報告里用了預訓練詞向量可能還有gensim。安裝時最容易卡在 torch 的版本和 CUDA 匹配上——如果你沒有 GPU直接裝 CPU 版就行命令里帶cpu后綴。# 創(chuàng)建虛擬環(huán)境避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安裝 CPU 版 torch版本按你 Python 版本選 pip install torch1.13.1 --index-url https://download.pytorch.org/whl/cpu pip install numpy tqdm如果你用 VSCode 或 PyCharm記得把解釋器切到剛建的 venv 里否則跑起來會報「No module named torch」。Windows 下路徑分隔符和編碼問題也常見讀語料時統(tǒng)一用encodingutf-8別依賴系統(tǒng)默認編碼。3.2 數(shù)據(jù)格式與標簽體系BIO 標注怎么對齊序列標注的數(shù)據(jù)格式通常是每行「字 標簽」句子之間空行分隔。這份項目用的應該是 BIO 標注B- 表示實體開頭I- 表示實體內(nèi)部O 表示非實體。比如「我 愛 北 京」對應「O O B-LOC I-LOC」。你要做的是確認自己的數(shù)據(jù)標簽集和代碼里的tag2id一致不一致就改配置別硬跑。標簽含義示例O非實體我、愛B-LOC地點實體開頭北I-LOC地點實體內(nèi)部京B-PER人名實體開頭張I-PER人名實體內(nèi)部三標簽體系一旦定了訓練集、驗證集、測試集必須用同一套。常見錯誤是訓練集有 B-ORG 但驗證集沒有導致評估時除零。我一般會在預處理階段先掃一遍所有標簽打印標簽集和頻次確認沒有遺漏。3.3 參數(shù)怎么調嵌入維度、隱藏層、學習率的實操建議這份源碼的默認參數(shù)能跑但想拿到更好的 F1 得調幾個關鍵項。嵌入維度 128 起步有預訓練詞向量就設 300隱藏層維度 128 到 256層數(shù) 1 層夠用學習率 1e-3配合 Adam 和梯度裁剪dropout 0.3 到 0.5數(shù)據(jù)量小就調大。batch_size 看顯存CPU 跑就設 16GPU 可以 64。config { embed_dim: 128, hidden_dim: 256, num_layers: 1, dropout: 0.4, lr: 1e-3, batch_size: 32, epochs: 30, max_norm: 5.0 }調參順序建議先固定其他項單獨調學習率和 dropout。如果訓練 loss 震蕩大降學習率如果訓練 loss 降但驗證 F1 不漲加 dropout 或減層數(shù)。別一上來就網(wǎng)格搜索小數(shù)據(jù)集上隨機性很大跑三組對比就夠了。4. 避坑與排查跑這份源碼時最容易翻車的五個地方4.1 現(xiàn)象loss 一直是 nan原因學習率過大或 CRF 轉移矩陣溢出解決降學習率并加梯度裁剪這是最典型的翻車現(xiàn)場。BiLSTM 加 CRF 的結構里CRF 的前向算法涉及指數(shù)和對數(shù)運算如果發(fā)射分數(shù)或轉移分數(shù)數(shù)值過大logsumexp會溢出。先把學習率從 1e-3 降到 5e-4 或 1e-4再確認clip_grad_norm_有沒有加。如果還不行檢查 CRF 的transitions初始化是不是用了太大的方差改成torch.randn(num_tags, num_tags) * 0.1試試。4.2 現(xiàn)象驗證集 F1 始終為 0原因標簽映射錯位或 mask 沒傳進評估解決打印第一條樣本的預測和真實標簽逐位比對F1 為 0 通常不是模型沒學到而是評估代碼把標簽對錯了。先拿一條訓練集樣本過一遍模型把預測標簽序列和真實標簽序列都打印出來看是不是整體偏移了一位或者PAD標簽混進了實體區(qū)間。另一個常見原因是評估時沒排除填充位導致實體邊界算錯。我一般會在評估函數(shù)里加一句斷言確認預測長度和真實長度一致。4.3 現(xiàn)象訓練集 loss 降但驗證集 loss 漲原因過擬合解決加 dropout、減層數(shù)、早停小數(shù)據(jù)集上 BiLSTM 很容易過擬合訓練 loss 一路降驗證 loss 從第幾輪開始反彈。這時候別硬訓加 dropout 到 0.5隱藏層降到 128或者直接早?!炞C F1 連續(xù) 5 輪不漲就停。如果數(shù)據(jù)量實在小可以考慮凍結嵌入層只訓 LSTM 和 CRF 部分。4.4 現(xiàn)象CPU 跑一輪要幾小時原因batch_size 太小或沒壓填充解決用 pack_padded_sequence 并適當加大 batchCPU 上跑 LSTM 本來就慢如果還沒用pack_padded_sequence填充位也參與計算時間翻倍。確認編碼層有沒有壓填充batch_size 從 16 加到 32 或 64能明顯提速。如果還是慢考慮把隱藏層維度降到 128或者先用小規(guī)模數(shù)據(jù)調試通再上全量。4.5 現(xiàn)象換自己的數(shù)據(jù)后報 KeyError原因標簽集或詞表不匹配解決統(tǒng)一用配置文件管理標簽映射這份源碼的tag2id和word2id大概率是寫死或從訓練集建的。換數(shù)據(jù)后如果出現(xiàn)訓練集沒有的標簽就會 KeyError。正確做法是把標簽集和詞表都存成 json訓練和評估都從同一份配置讀。別在代碼里硬編碼標簽列表改起來容易漏。5. 從能跑到好用CRF 解碼驗證與一個提分技巧跑通之后怎么確認 CRF 真的在起作用最直接的辦法是對比實驗把 CRF 層去掉只留 BiLSTM 加 softmax看 F1 掉多少。如果掉得明顯說明轉移約束確實在壓非法序列。另一個驗證方法是拿一條測試樣本打印 Viterbi 解碼的最優(yōu)路徑分數(shù)再和貪心解碼的結果比看 CRF 有沒有糾正貪心解出來的非法標簽組合。# 對比 CRF 解碼和貪心解碼 with torch.no_grad(): emissions model(word_ids, lengths) # CRF Viterbi 解碼 best_path crf.viterbi_decode(emissions, mask)[0] # 貪心解碼每個位置取最大發(fā)射分數(shù) greedy_path emissions.argmax(dim-1)[0].tolist() print(Viterbi:, best_path) print(Greedy :, greedy_path)如果 Viterbi 路徑里沒有出現(xiàn)「B-LOC 接 B-LOC」這種組合而貪心路徑出現(xiàn)了就說明 CRF 的轉移矩陣學到了合理約束。這個對比我每次調完 CRF 參數(shù)都會跑一遍比只看 F1 更直觀。提分技巧上最劃算的是加預訓練詞向量。中文可以用騰訊詞向量或中文維基訓練的詞向量加載后凍結前幾輪再解凍微調。另一個技巧是調整 CRF 的初始轉移矩陣把明顯非法的轉移比如 O 到 I-XXX初始化成很小的值能加快收斂。我一般會在訓練前手動把transitions里非法組合設成 -1e4讓模型少走彎路。從那以后我每次拿到序列標注項目都強制先跑一遍「去 CRF 對比」和「Viterbi vs 貪心」這兩個驗證確認 CRF 不是擺設再繼續(xù)調參。希望幫到你。本文還有配套的精品資源點擊獲取