自然語言處理中,區塊化是將句子切分為語法單位的關鍵步驟。理解區塊化原理及其實作方法,有助於提升自然語言理解的準確性。本文將介紹區塊化的常見表示方法,包含 IOB 標籤和樹狀結構,並使用 Python 的 NLTK 函式庫示範如何操作與轉換。同時,我們會使用 CoNLL-2000 語料函式庫進行區塊分析器的效能評估,比較正規表示式、Unigram 和 Bigram 標記器等方法的優劣。透過實際程式碼範例,讀者可以更深入理解區塊化技術的應用,並學習如何建構和評估自己的區塊分析器。
7.2 區塊化(Chunking)與區塊擷取(Chinking)技術詳解
區塊化是自然語言處理中的重要步驟,主要功能是將句子中的詞語根據語法規則分組,形成有意義的區塊(Chunk)。本章節將探討區塊化的技術細節,包括其表示方法、評估方法以及相關工具的使用。
7.2.1 區塊表示法:標籤(Tags)與樹狀結構(Trees)
區塊結構可以用兩種主要方式表示:標籤和樹狀結構。這兩種表示方法各有其優缺點和適用場景。
IOB 標籤格式
IOB 標籤是一種廣泛使用的區塊表示方法。在這種格式中,每個詞語都被標記為以下三種類別之一:
- B-X:表示某個區塊(型別 X)的開始
- I-X:表示某個區塊(型別 X)的內部
- O:表示該詞語不在任何區塊內
例如,名詞短語(NP)的 IOB 標籤表示如下:
We PRP B-NP
saw VBD O
the DT B-NP
little JJ I-NP
yellow JJ I-NP
dog NN I-NP
這種表示方法的優點是可以方便地表示多種型別的區塊,並且可以輕鬆地儲存在檔案中。以下是使用 Python 程式碼讀取 IOB 格式的範例:
import nltk
from nltk.corpus import conll2000
# 載入 CoNLL-2000 區塊化語料函式庫
text = '''
he PRP B-NP
accepted VBD B-VP
the DT B-NP
position NN I-NP
of IN B-PP
vice NN B-NP
chairman NN I-NP
'''
# 將 IOB 文字轉換為 NLTK 樹狀結構
tree = nltk.chunk.conllstr2tree(text, chunk_types=['NP'])
tree.draw() # 視覺化樹狀結構
樹狀結構表示法
NLTK 內部使用樹狀結構來表示區塊,這種表示方法允許直接操作區塊結構。例如:
(S
(NP the/DT little/JJ yellow/JJ dog/NN)
barked/VBD
at/IN
(NP the/DT cat/NN)
)
樹狀結構的優點是可以方便地進行結構化的操作和分析。以下是將 IOB 資料轉換為樹狀結構的範例程式碼:
# 從 CoNLL-2000 語料函式庫讀取已區塊化的句子
sentence = conll2000.chunked_sents('train.txt')[99]
print(sentence)
# 輸出:
# (S
# (PP Over/IN)
# (NP a/DT cup/NN)
# (PP of/IN)
# (NP coffee/NN)
# ,/,
# (NP Mr./NNP Stone/NNP)
# (VP told/VBD)
# (NP his/PRP$ story/NN)
# ./.)
內容解密:
nltk.chunk.conllstr2tree()函式用於將 IOB 格式的字串轉換為 NLTK 樹狀結構。chunk_types=['NP']引數指定只保留名詞短語(NP)區塊。- 樹狀結構可以清晰地表示句子的層次結構,便於進一步的語法分析。
7.3 區塊分析器的開發與評估
評估區塊分析器的效能需要使用已標註的語料函式庫。本文將介紹如何使用 CoNLL-2000 區塊化語料函式庫來評估區塊分析器。
7.3.1 使用 CoNLL-2000 語料函式庫
CoNLL-2000 語料函式庫包含27萬字的華爾街日報文字,並已標註了詞性標籤和區塊標籤。以下是存取該語料函式庫的範例:
from nltk.corpus import conll2000
# 取得測試資料集中的第100句
sentence = conll2000.chunked_sents('test.txt')[99]
print(sentence)
簡單評估與基準測試
首先,我們建立一個不進行任何區塊化的基準測試:
cp = nltk.RegexpParser("")
test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP'])
print(cp.evaluate(test_sents))
輸出結果顯示:
ChunkParse score:
IOB Accuracy: 43.4%
Precision: 0.0%
Recall: 0.0%
F-Measure: 0.0%
使用正規表示式進行區塊化
接下來,我們嘗試使用一個簡單的正規表示式區塊分析器:
grammar = r"NP: {<[CDJNP].*>+}"
cp = nltk.RegexpParser(grammar)
print(cp.evaluate(test_sents))
輸出結果:
ChunkParse score:
IOB Accuracy: 87.7%
Precision: 70.6%
Recall: 67.8%
F-Measure: 69.2%
內容解密:
- 評估指標包括 IOB 準確率、精確率、召回率和 F1 分數。
- 簡單的正規表示式區塊分析器可以達到不錯的效果,但仍有改進空間。
- 可以進一步使用機器學習方法(如 Unigram 標記器)來改進區塊分析器的效能。
進一步最佳化區塊分析器
我們可以使用根據 Unigram 標記器的方法來進一步提升區塊分析器的效能。這種方法根據詞性標籤預測最可能的區塊標籤。
使用 Unigram 標記器進行區塊化
class UnigramChunker(nltk.ChunkParserI):
def __init__(self, train_sents):
train_data = [(sent, get_chunk_tags(sent)) for sent in train_sents]
self.tagger = nltk.UnigramTagger(train_data)
def parse(self, sentence):
pos_tags = [pos for (word, pos) in sentence]
tagged_pos_tags = self.tagger.tag(pos_tags)
chunk_tags = [chunk for (pos, chunk) in tagged_pos_tags]
wtc = [(w, t, c) for ((w, t), c) in zip(sentence, chunk_tags)]
return nltk.chunk.conlltags2tree(wtc)
# 準備訓練資料
train_sents = conll2000.chunked_sents('train.txt', chunk_types=['NP'])
# 建立並評估 UnigramChunker
unigram_chunker = UnigramChunker(train_sents)
print(unigram_chunker.evaluate(test_sents))
內容解密:
UnigramChunker類別繼承自nltk.ChunkParserI,實作了根據 Unigram 標記器的區塊分析器。- 訓練過程中,將詞性標籤序列與對應的區塊標籤序列作為訓練資料。
- 在解析時,先取得句子的詞性標籤序列,然後使用訓練好的標記器預測區塊標籤。
- 最後將詞語、詞性標籤和預測的區塊標籤轉換為樹狀結構。
這種根據機器學習的方法通常比簡單的正規表示式方法具有更好的效能和泛化能力。
- 探索使用更先進的機器學習模型(如 CRF、BiLSTM 等)進行區塊化。
- 結合詞嵌入技術提升區塊分析器的效能。
- 將區塊化技術應用於更廣泛的 NLP 任務中。
透過持續最佳化和創新,區塊化技術將在自然語言處理領域發揮更大的作用。
利用單字標記器進行名詞短語分塊
在範例7-4中,我們定義了UnigramChunker類別,這個類別使用單字標記器為句子加上分塊標籤。這個類別中的大多數程式碼是用來轉換NLTK的ChunkParserI介面所使用的分塊樹表示法,以及嵌入式標記器所使用的IOB表示法之間的轉換。該類別定義了兩個方法:建構函式__init__,在建立新的UnigramChunker時呼叫;以及parse方法,用於對新句子進行分塊。
範例7-4:使用單字標記器進行名詞短語分塊
class UnigramChunker(nltk.ChunkParserI):
def __init__(self, train_sents):
train_data = [[(t,c) for w,t,c in nltk.chunk.tree2conlltags(sent)]
for sent in train_sents]
self.tagger = nltk.UnigramTagger(train_data)
def parse(self, sentence):
pos_tags = [pos for (word,pos) in sentence]
tagged_pos_tags = self.tagger.tag(pos_tags)
chunktags = [chunktag for (pos, chunktag) in tagged_pos_tags]
conlltags = [(word, pos, chunktag) for ((word,pos),chunktag)
in zip(sentence, chunktags)]
return nltk.chunk.conlltags2tree(conlltags)
內容解密:
__init__方法預期一個訓練句子列表,這些句子將以分塊樹的形式呈現。- 它首先使用
tree2conlltags將訓練資料轉換為適合訓練標記器的形式,將每個分塊樹對映到一個單詞、標籤、分塊三元組的列表。 - 然後,它使用轉換後的訓練資料來訓練單字標記器,並將其儲存在
self.tagger中,以供稍後使用。 parse方法接受一個已標記的句子作為輸入,並首先從該句子中提取詞性標籤。- 然後,它使用在建構函式中訓練的標記器
self.tagger為詞性標籤加上IOB分塊標籤。 - 接下來,它提取分塊標籤,並將它們與原始句子結合,生成
conlltags。 - 最後,它使用
conlltags2tree將結果轉換回分塊樹。
現在我們有了UnigramChunker,我們可以使用CoNLL-2000分塊語料函式庫來訓練它,並測試其效能:
>>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP'])
>>> train_sents = conll2000.chunked_sents('train.txt', chunk_types=['NP'])
>>> unigram_chunker = UnigramChunker(train_sents)
>>> print(unigram_chunker.evaluate(test_sents))
ChunkParse score:
IOB Accuracy: 92.9%
Precision: 79.9%
Recall: 86.8%
F-Measure: 83.2%
這個分塊器的效能相當不錯,達到了整體F-measure得分83%。讓我們來看看它學到了什麼,透過使用其單字標記器為語料函式庫中出現的每個詞性標籤分配標籤:
>>> postags = sorted(set(pos for sent in train_sents
... for (word,pos) in sent.leaves()))
>>> print(unigram_chunker.tagger.tag(postags))
[('#', 'B-NP'), ('$', 'B-NP'), ("''", 'O'), ('(', 'O'), (')', 'O'),
(',', 'O'), ('.', 'O'), (':', 'O'), ('CC', 'O'), ('CD', 'I-NP'),
('DT', 'B-NP'), ('EX', 'B-NP'), ('FW', 'I-NP'), ('IN', 'O'),
('JJ', 'I-NP'), ('JJR', 'B-NP'), ('JJS', 'I-NP'), ('MD', 'O'),
('NN', 'I-NP'), ('NNP', 'I-NP'), ('NNPS', 'I-NP'), ('NNS', 'I-NP'),
('PDT', 'B-NP'), ('POS', 'B-NP'), ('PRP', 'B-NP'), ('PRP$', 'B-NP'),
('RB', 'O'), ('RBR', 'O'), ('RBS', 'B-NP'), ('RP', 'O'), ('SYM', 'O'),
('TO', 'O'), ('UH', 'O'), ('VB', 'O'), ('VBD', 'O'), ('VBG', 'O'),
('VBN', 'O'), ('VBP', 'O'), ('VBZ', 'O'), ('WDT', 'B-NP'),
('WP', 'B-NP'), ('WP$', 'B-NP'), ('WRB', 'O'), ('``', 'O')]
它發現大多數標點符號出現在NP分塊之外,除了#和$,它們都被用作貨幣標記。它還發現限定詞(DT)和所有格(PRP$和WP$)出現在NP分塊的開頭,而名詞型別(NN、NNP、NNPS、NNS)大多出現在NP分塊內。
訓練根據分類別器的分塊器
無論是根據正規表示式的分塊器還是n-gram分塊器,它們決定要建立哪些分塊完全根據詞性標籤。然而,有時詞性標籤不足以確定句子應該如何分塊。例如,請考慮以下兩個陳述式:
(3) a. Joey/NN sold/VBD the/DT farmer/NN rice/NN ./. b. Nick/NN broke/VBD my/DT computer/NN monitor/NN ./.
這兩個句子具有相同的詞性標籤,但它們的分塊方式不同。在第一個句子中,農夫和米飯是分開的分塊,而第二個句子中的相應材料,即電腦監視器,則是一個單一的分塊。顯然,如果我們希望最大限度地提高分塊效能,我們需要利用有關單詞內容的資訊,而不僅僅是它們的詞性標籤。
內容解密:
- 為了提高分塊效能,我們需要使用單詞的內容資訊,而不僅僅是詞性標籤。
- 一種方法是使用根據分類別器的標記器來對句子進行分塊,就像前一節中討論的n-gram分塊器一樣。
- 這個根據分類別器的分塊器將透過為句子中的每個詞分配IOB標籤來工作。
進一步改進:使用BigramChunker
要建立一個BigramChunker,我們只需要將UnigramChunker類別的名稱更改為BigramChunker,並修改範例7-4中的一行程式碼,以建構一個BigramTagger而不是UnigramTagger。結果表明,這個分塊器的效能略高於單字分塊器:
>>> bigram_chunker = BigramChunker(train_sents)
>>> print(bigram_chunker.evaluate(test_sents))
ChunkParse score:
IOB Accuracy: 93.3%
Precision: 82.3%
Recall: 86.8%
F-Measure: 84.5%
圖表翻譯:
此圖表顯示了使用單字標記器和雙字標記器進行名詞短語分塊的結果比較。結果表明,雙字標記器的效能略高於單字標記器。
graph LR;
A[訓練資料] -->|轉換成IOB格式|> B[訓練單字標記器];
B --> C[訓練雙字標記器];
C --> D[評估單字分塊器];
D --> E[評估雙字分塊器];
E --> F[比較結果];
圖表翻譯: 此圖表呈現了訓練資料轉換成IOB格式後,分別訓練單字標記器和雙字標記器的流程,並對兩種分塊器進行評估,最後比較結果。