第七章-從文本中提取信息

對於任何給定的問題,都可能有人在某處寫下了答案。以電子形式提供的自然語言文本數量確實驚人,而且每天都在增加。然而,自然語言的複雜性使得獲取文本中的信息非常困難。NLP的技術水平離從無限制的文本構建通用意義表示還有很長的路要走。如果我們把精力集中在有限的一系列問題或「實體關係」上,比如「不同的設施在哪裏」或「哪家公司僱用了誰」,我們就能取得重大進展。本章的目標是回答以下問題:
1.如何構建一個從非結構化文本中提取結構化數據(如表)的系統?
2.有哪些健壯的方法可以識別文本中描述的實體和關係?
3.哪些語料庫適合這項工作,我們如何使用它們來訓練和評估我們的模型?
在此過程中,我們將應用前兩章中的技術來解決分塊和命名實體識別的問題。

7.1 信息提取

信息有多種形狀和大小。一種重要的形式是結構化數據,其中存在規則且可預測的實體和關係組織。
例如,我們可能對公司和地點之間的關係感興趣。 鑑於某家公司,我們希望能夠確定其開展業務的地點; 相反,在給定位置的情況下,我們希望瞭解哪些公司在該位置開展業務。 如果我們的數據是表格形式,例如1.1中的示例,那麼回答這些查詢很簡單。
Table1.1 locations data

如果這個位置數據作爲元組列表(實體,關係,實體)存儲在Python中,則問題是「哪些組織在亞特蘭大運行?」 可翻譯如下:

locs=[('Omnicom', 'IN', 'New York'),
      ('DDB Needham', 'IN', 'New York'),
      ('Kaplan Thaler Group', 'IN', 'New York'),
      ('BBDO South', 'IN', 'Atlanta'),
      ('Georgia-Pacific', 'IN', 'Atlanta')]
query = [e1 for (e1, rel, e2) in locs if e2=='Atlanta']

Table1.2 result

如果我們試圖從文本中獲取類似的信息,事情就會變得更加棘手。 例如,請考慮以下代碼段(corpus.ieer裏的某個文件)

text (1)
如果您通讀(1),您將收集回答示例問題所需的信息。但是我們如何讓機器充分了解(1)返回table1.2中的答案?這顯然是一項艱鉅的任務。與table1.1不同,(1)不包含將組織名稱與位置名稱鏈接的結構。
解決這個問題的一種方法是建立一個非常普遍的意義表示(第十章)
在本章中,我們採用不同的方法,事先決定我們只會在文本中查找非常具體的信息,例如組織和位置之間的關係。
我們首先將自然語言句子的非結構化數據轉換爲1.1的結構化數據,而不是嘗試使用像(1)這樣的文本來直接回答問題。
然後我們獲得了強大的查詢工具(如SQL)的好處。
這種從文本中獲取意義的方法稱爲信息提取。
信息提取有許多應用,包括商業智能,簡歷收集,媒體分析,情感檢測,專利檢索和電子郵件掃描。
當前研究中一個特別重要的領域涉及嘗試從電子可用的科學文獻中提取結構化數據,特別是在生物學和醫學領域。

7.1.1信息提取架構

圖1.1顯示了簡單信息提取系統的體系結構。
它首先使用第三章和第五章中討論的幾個過程處理文檔:首先,使用句子分割器將文檔的原始文本分成句子,並且使用分詞器將每個句子進一步細分爲單詞。
接下來,每個句子都標有詞性標籤,這將在下一步命名實體檢測中證明非常有用。
在這一步中,我們在每個句子中搜索可能有趣的實體。
最後,我們使用關係檢測來搜索文本中不同實體之間的可能關係。

Simple Pipeline Architecture for an Information Extraction System

要執行前三個任務,我們可以定義一個簡單的函數,簡單地將NLTK的默認句子分割器,單詞標記器和詞性標註器連接在一起.
接下來,在命名實體檢測中,我們對可能參與彼此有趣關係的實體進行細分和標記。
最後,在關係提取中,我們搜索文本中彼此接近的實體對之間的特定模式,並使用這些模式來構建記錄實體之間關係的元組。

7.2分塊

我們將用於實體檢測的基本技術是分塊,分段和標記多標記序列,如2.1所示。
較小的框顯示字級標記化和詞性標記,而大框顯示更高級別的分塊。
這些較大的盒子中的每一個都稱爲塊。 與省略空格的標記化一樣,分塊通常選擇標記的子集。與標記化一樣,由chunker生成的片段在源文本中不重疊。

Figure 2.1: Segmentation and Labeling at both the Token and Chunk Levels

7.2.1 名詞短語分塊

我們將首先考慮名詞短語分塊或NP分塊的任務,其中我們搜索對應於單個名詞短語的塊。例如,這裏有一些華爾街日報文本,NP塊使用括號標記:

text (2)
NP分塊最有用的信息來源之一是詞性標籤。這是在我們的信息提取系統中執行詞性標註的動機之一。

sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"),
            ("dog", "NN"), ("barked", "VBD"), ("at", "IN"),  ("the", "DT"), ("cat", "NN")]
grammar = "NP: {<DT>?<JJ>*<NN>}"
cp = nltk.RegexpParser(grammar)
result = cp.parse(sentence)
result.draw()

result_draw

7.2.2 標籤模式

構成塊語法的規則使用標記模式來描述帶標記的單詞序列。標記模式是使用尖括號分隔的詞性標記序列,例如

?*。標記模式類似於正則表達式模式(3.4)。現在,考慮一下《華爾街日報》上的下列名詞短語:
WallStreetJournal

我們可以使用上面第一個標記圖案的略微細化來匹配這些名詞短語,即

?<JJ. ><NN.*>+。
JJ.*可以匹配形容詞,以及其比較級最高級,NN.*可以匹配常用名詞單複數,專有名詞單複數
但是,很容易找到許多更復雜的例子,這條匹配規則就不適用了,如下:
complicated_text

7.2.3 用正則表達式分塊

2.3展示了一個由兩條規則組成的簡單塊語法。第一條規則匹配一個可選的限定詞或所有格代詞,零或多個形容詞,然後是名詞。第二條規則匹配一個或多個專有名詞。我們還定義了一個被分塊[1]的示例語句,並在這個輸入[2]上運行分塊程序。

grammar = r"""
  NP: {<DT|PP\$>?<JJ>*<NN>}  
      {<NNP>+}                 
"""
cp = nltk.RegexpParser(grammar)
sentence = [("Rapunzel", "NNP"), ("let", "VBD"), ("down", "RP"),
                 ("her", "PP$"), ("long", "JJ"), ("golden", "JJ"), ("hair", "NN")]
result=cp.parse(sentence)
print(result)
result.draw()

如果標記模式在重疊的位置匹配,則最左邊的匹配優先。例如,如果我們將匹配兩個連續名詞的規則應用到包含三個連續名詞的文本中,那麼只有前兩個名詞將被分塊:

nouns = [("money", "NN"), ("market", "NN"), ("fund", "NN")]
grammar = "NP: {<NN>+}  # Chunk two consecutive nouns"
cp = nltk.RegexpParser(grammar)
result=cp.parse(nouns)
print(result)
result.draw()

7.2.4 探索文本語料庫

在2中,我們看到了如何查詢標記語料庫以提取與特定詞性標籤序列匹配的短語。我們可以使用chunker更輕鬆地完成相同的工作,如下所示

cp = nltk.RegexpParser('CHUNK: {<V.*> <TO> <V.*>}')
for sent in brown.tagged_sents():
  tree = cp.parse(sent)
  for subtree in tree.subtrees():
    if subtree.label() == 'CHUNK':
      print(subtree)

7.2.5 分塊

Chinking是從塊中刪除一個令牌序列的過程。如果匹配的令牌序列跨越整個塊,則刪除整個塊;如果令牌序列出現在塊的中間,這些令牌將被刪除,留下兩個之前只有一個令牌的塊。如果序列位於塊的外圍,則刪除這些標記,並保留較小的塊。這三種可能性在2.1中進行了說明。

chinking rules applied to the same chunk

grammar = r"""
  NP:
    {<.*>+}          # Chunk everything
    }<VBD|IN>+{      # Chink sequences of VBD and IN
  """
sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"),
       ("dog", "NN"), ("barked", "VBD"), ("at", "IN"),  ("the", "DT"), ("cat", "NN")]
cp = nltk.RegexpParser(grammar)
result=cp.parse(sentence) 
result.draw()

7.2.6 表示塊:標籤與樹

I (inside), O (outside), or B (begin).
標記表示塊結構

Tag Representation of Chunk Structures

IOB標記已經成爲在文件中表示塊結構的標準方法,我們也將使用這種格式。以下是2.5中的信息如何顯示在文件中:

IOB in files

在這種表示中,每行有一個標記,每個標記都帶有詞性標記和塊標記。這種格式允許我們表示多個塊類型,只要這些塊沒有重疊。正如我們前面看到的,塊結構也可以使用樹來表示。這樣做的好處是,每個塊都是可以直接操作的組成部分。如2.6所示:
樹表示塊結構

Tree Representation of Chunk Structures

注意:NLTK使用樹來表示塊的內部表示,但提供了將這些樹讀取和寫入IOB格式的方法。
開發和評價分塊器

7.3 開發和評價分塊器

現在您已經瞭解了分塊的功能,但我們還沒有解釋如何評估分塊器。像往常一樣,這需要適當註釋的語料庫。我們首先看一下將IOB格式轉換爲NLTK樹的機制,然後討論如何使用分塊語料庫在更大規模上完成此操作。我們將看到如何評估chunker相對於語料庫的準確性,然後查看一些更多數據驅動的方法來搜索NP塊。我們始終關注的重點是擴大分組的覆蓋範圍。

7.3.1閱讀IOB格式和CoNLL 2000語料庫

轉換函數chunk.conllstr2tree()從這些多行字符串之一構建樹表示。
此外,它允許我們選擇要使用的三種塊類型的任何子集,這裏僅用於NP塊:

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
of IN B-PP
Carlyle NNP B-NP
Group NNP I-NP
, , O
a DT B-NP
merchant NN I-NP
banking NN I-NP
concern NN I-NP
. . O
'''
nltk.chunk.conllstr2tree(text, chunk_types=['NP']).draw()
nltk.chunk.conllstr2tree(text).draw()

String to Tree Representation

String to Tree Representation

我們可以使用NLTK語料庫模塊訪問更多的分塊文本。CoNLL 2000語料庫包含270k字的華爾街日報文本,分爲「訓練」和「測試」部分,用IOB格式的詞性標籤和塊標籤註釋。我們可以使用nltk.corpus.conll2000訪問數據。下面是讀取語料庫的「訓練」部分的第100個句子的例子:

print(conll2000.chunked_sents('train.txt')[99])

String to Tag Representation

正如您所看到的,CoNLL 2000語料庫包含三種塊類型:名詞語塊,我們已經看過了; 動詞語塊,如「has already delivered」; 介詞語塊,例如「because of」。
由於我們現在只對NP塊感興趣,我們可以使用chunk_types參數來選擇它們:

String to Tag Representation

7.3.2簡單的評價和基準

現在,我們可以訪問分塊語料庫,我們可以評估chunkers。我們首先爲簡單的塊解析器cp(chunkparser)建立一個不創建分塊的基線:
FirstResult_no training no grammar
IOB標籤準確度表示超過三分之一的單詞被標記爲O,即不在NP塊中。但是,由於我們的標記器沒有找到任何塊,因此其精度,召回率和f度量均爲零。現在讓我們嘗試一個正則表達式chunker,它尋找以名詞短語標籤(例如CD,DT和JJ)爲特徵的字母開頭的標籤。(初級的正則表達式分塊器)
SecondResult_no training with grammar
在3.1中,我們定義了UnigramChunker類,它使用unigram標記符來標記帶有塊標記的句子。此類中的大多數代碼僅用於在NLTK的ChunkParserI接口使用的塊樹表示和嵌入式標記器使用的IOB表示之間來回轉換。該類定義了兩個方法:在構建新的UnigramChunker時調用的構造函數; 以及用於分塊新句子的解析方法

# 使用訓練語料找到對每個詞性標記最有可能的塊標記(I、O或B)
# 可以用unigram標註器建立一個分塊器,但不是要確定每個詞的正確詞性標記,
# 而是給定每個詞的詞性標記,嘗試確定正確的塊標記
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]
    # 爲詞性標註IOB塊標記
    conlltags = [(word, pos, chunktag) for ((word, pos), chunktag)
                 in zip(sentence, chunktags)]
    # 轉換成分塊樹狀圖
    return nltk.chunk.conlltags2tree(conlltags)

構造函數需要一個訓練句列表,這些句子將以塊樹的形式出現。它首先將訓練數據轉換爲適合訓練標記器的形式,使用tree2conlltags將每個塊樹映射到word,tag,chunk三元組列表。然後,它使用轉換後的訓練數據來訓練unigram標記器,並將其存儲在self.tagger中供以後使用。

解析方法將標記的句子作爲其輸入,並從該句子中提取詞性標籤開始。然後,它使用在構造函數中訓練的tagger self.tagger,使用IOB塊標記標記詞性標記。接下來,它提取塊標籤,並將它們與原始句子組合,以產生conlltags。最後,它使用conlltags2tree將結果轉換回塊樹。

現在我們有了UnigramChunker,我們可以使用CoNLL 2000語料庫對其進行訓練,並測試其最終性能:
ThirdResult_with training with grammar
這個組合相當不錯,整體f-measure得分爲83%。
讓我們看一下它的學習內容,使用它的unigram標記器爲語料庫中出現的每個詞性標籤分配一個標籤:
postag
它發現大多數標點符號都出現在NP塊之外,除了#和 ( D T ) ( P R P ,它們都被用作貨幣標記。 研究還發現,限定詞(DT)和所有詞(PRP 和WP$)出現在NP塊的開頭,而名詞類型(NN、NNP、NNPS、NNS)大多出現在NP塊內部
構建了一個unigram chunker後,很容易構建一個bigram chunker:我們只需將類名更改爲BigramChunker,並修改3.1中的行來構造一個BigramTagger而不是一個UnigramTagger。
由此產生的chunker性能略高於unigram chunker:
class BigramChunker
基於正則表達式的chunkers和n-gram chunkers都決定了基於詞性標籤完全創建的塊。
但是,有時詞性標籤不足以確定句子應該如何分塊。 例如,請考慮以下兩個陳述

class statements
基於分類器的NP chunker的基本代碼如3.2所示。它由兩個類組成:
第一個類幾乎與1.5中的ConsecutivePosTagger類相同。唯一的兩個區別是它調用了一個不同的特徵提取器,它使用的是MaxentClassifier而不是NaiveBayesClassifier。
第二個類基本上是一個圍繞tagger類的包裝器,它將它變成一個chunker。
在訓練期間,該第二類將訓練語料庫中的塊樹映射到標籤序列中;在parse()方法中,它將標記器提供的標記序列轉換回塊樹
Figure 3.2: Noun Phrase Chunking with a Consecutive Classifier

ConsecutivePosTagger 剩下要填寫的唯一部分是特徵提取器。我們首先定義一個簡單的特徵提取器,它只提供當前令牌的詞性標記。使用這個特徵提取器,我們的基於分類器的chunker與unigram chunker非常相似,正如其性能所反映的那樣。