自然語言處理中,詞彙分析和 N-gram 模型僅能處理語言表層結構,無法完整解析句子結構複雜性。為深入理解語法分析,本文將探討如何運用形式語法描述無限句子結構、如何使用語法樹表示句子結構,以及解析器如何分析句子並自動構建語法樹。過程中,我們將涵蓋英語語法基礎,並闡述如何透過語法結構擷取語義特徵,並使用 Python 和 NLTK 函式庫進行實作和範例演示。

分析句子結構:語法分析的基礎與實踐

在前面的章節中,我們探討了詞彙的識別、結構分析、詞類別判定以及語義擷取等議題。同時,我們也研究了詞序列中的模式識別,也就是所謂的n-gram分析。然而,這些方法僅僅觸及了語言結構的表面,無法全面處理句子結構的複雜性和多樣性。為瞭解決這些挑戰,本章節將探討以下關鍵問題:

  1. 如何使用形式語法來描述無限多句子的結構?
  2. 如何利用語法樹來表示句子的結構?
  3. 解析器如何分析句子並自動構建語法樹?

在這個過程中,我們將涵蓋英語語法的基礎知識,並展示一旦識別出句子的結構,就可以更容易地擷取其語義中的系統性特徵。

8.1 語法分析的基本概念

8.1.1 語法規則與句法結構

語法分析是指根據一組語法規則來分析句子的結構。這些規則定義瞭如何將詞彙組合成短語,再進一步組成句子。在自然語言處理(NLP)中,這些規則通常以形式語法(Formal Grammar)的形式呈現。

語法規則通常由以下幾個部分組成:

  • 終結符號(Terminal Symbols):代表句子中的詞彙。
  • 非終結符號(Non-terminal Symbols):代表語法範疇,如名詞短語(NP)、動詞短語(VP)等。
  • 產生式規則(Production Rules):定義如何將非終結符號展開為其他符號的組合。

例如,以下是一個簡單的語法規則:

S -> NP VP
NP -> Det N
VP -> V NP
Det -> 'the'
N -> 'cat'
V -> 'sat'

這個語法規則定義了一個簡單的句子結構,其中 S 代表句子,NP 代表名詞短語,VP 代表動詞短語。

8.1.2 語法樹的表示

語法樹(Syntax Tree)是一種樹狀結構,用於表示句子的語法結構。每個節點代表一個語法範疇或詞彙,樹的結構反映了句子的層次結構。

例如,對於句子 “The cat sat on the mat.",其語法樹可能如下所示:

     S
   /   \
  NP   VP
 / \   / \
Det  N  V   PP
|   |  |  /   \
'the' 'cat' 'sat' P   NP
                 |   / \
                 'on' Det  N
                           |   |
                          'the' 'mat'

8.1.3 解析器的運作原理

解析器(Parser)是一種程式,用於分析句子並根據語法規則構建語法樹。解析器的目標是確定句子的語法結構,並將其表示為語法樹。

常見的解析方法包括:

  • 遞迴下降解析(Recursive Descent Parsing):一種自頂向下的解析方法,根據語法規則遞迴地分析句子。
  • 移進-歸約解析(Shift-Reduce Parsing):一種自底向上的解析方法,透過移進詞彙和歸約操作來構建語法樹。

8.2 語法分析的實踐

8.2.1 使用NLTK進行語法分析

NLTK(Natural Language Toolkit)是一個流行的Python函式庫,用於自然語言處理。NLTK提供了豐富的工具和資源,用於語法分析。

以下是一個使用NLTK進行語法分析的示例:

import nltk
from nltk import CFG

# 定義語法規則
grammar = CFG.fromstring("""
S -> NP VP
NP -> Det N | NP PP
VP -> V NP | VP PP
PP -> P NP
Det -> 'the'
N -> 'cat' | 'mat'
V -> 'sat'
P -> 'on'
""")

# 建立解析器
parser = nltk.ChartParser(grammar)

# 分析句子
sentence = "the cat sat on the mat".split()
trees = list(parser.parse(sentence))

# 輸出語法樹
for tree in trees:
    print(tree)

8.2.2 語法分析的挑戰

語法分析面臨著多個挑戰,包括:

  • 歧義性(Ambiguity):自然語言中存在大量的歧義性,例如詞彙的多義性、句法結構的歧義性等。
  • 複雜性(Complexity):句子的結構可能非常複雜,包含多層巢狀和遞迴結構。

為瞭解決這些挑戰,研究人員開發了各種技術,例如:

  • 機率語法(Probabilistic Grammar):透過引入機率模型來處理歧義性和不確定性。
  • 深度學習方法(Deep Learning Methods):利用神經網路來學習句子的語法結構和語義表示。

練習題

  1. 設計一個簡單的語法規則,用於描述名詞短語的結構,並使用NLTK實作一個解析器來分析名詞短語。

    import nltk
    from nltk import CFG
    
    # 定義名詞短語的語法規則
    np_grammar = CFG.fromstring("""
    NP -> Det N | Det N PP
    Det -> 'the'
    N -> 'cat' | 'dog'
    PP -> P NP
    P -> 'with'
    """)
    
    # 建立解析器
    np_parser = nltk.ChartParser(np_grammar)
    
    # 分析名詞短語
    np_sentence = "the cat with the dog".split()
    np_trees = list(np_parser.parse(np_sentence))
    
    # 輸出語法樹
    for np_tree in np_trees:
        print(np_tree)
    
  2. 實作一個遞迴下降解析器,用於分析簡單的算術表示式。

    class RecursiveDescentParser:
        def __init__(self, tokens):
            self.tokens = tokens
            self.index = 0
    
        def parse(self):
            return self.expression()
    
        def expression(self):
            value = self.term()
            while self.index < len(self.tokens) and self.tokens[self.index] in ['+', '-']:
                op = self.tokens[self.index]
                self.index += 1
                term_value = self.term()
                if op == '+':
                    value += term_value
                else:
                    value -= term_value
            return value
    
        def term(self):
            value = self.factor()
            while self.index < len(self.tokens) and self.tokens[self.index] in ['*', '/']:
                op = self.tokens[self.index]
                self.index += 1
                factor_value = self.factor()
                if op == '*':
                    value *= factor_value
                else:
                    value /= factor_value
            return value
    
        def factor(self):
            if self.index < len(self.tokens) and self.tokens[self.index].isdigit():
                value = int(self.tokens[self.index])
                self.index += 1
                return value
            else:
                raise SyntaxError("Invalid syntax")
    
    # 示例用法
    tokens = ['2', '+', '3', '*', '4']
    parser = RecursiveDescentParser(tokens)
    result = parser.parse()
    print("Result:", result)
    
  3. 比較不同解析方法的優缺點,並討論在不同應用場景下的選擇。

    • 遞迴下降解析:優點是實作簡單、易於除錯;缺點是可能導致堆疊溢位,處理左遞迴語法規則較為困難。
    • 移進-歸約解析:優點是能夠處理更廣泛的語法規則,效能較好;缺點是實作較為複雜,除錯困難。

在選擇解析方法時,需要根據具體的應用場景和需求進行權衡。例如,在處理簡單的語法規則時,遞迴下降解析可能是一個不錯的選擇;而在處理複雜的語法規則或需要高效能的場景下,移進-歸約解析可能更為合適。

進一步閱讀

  • 《自然語言處理綜論》:詳細介紹了自然語言處理的基礎知識和技術,包括語法分析。
  • 《計算語言學》:探討了計算語言學的理論和實踐,涵蓋了語法分析的各種方法。
  • NLTK官方檔案:提供了NLTK函式庫的詳細檔案和示例程式碼,是學習和使用NLTK的重要資源。

內容解密:

本章節介紹了語法分析的基本概念和實踐方法,包括語法規則、語法樹和解析器等。透過NLTK函式庫的示例,展示瞭如何實作語法分析。練習題提供了進一步的實踐機會,幫助讀者深入理解語法分析的原理和技術。進一步閱讀的資源提供了更深入的學習材料,幫助讀者拓展知識面。

8.1 文法困境:語言資料與無限可能性

語言資料與無限延伸的挑戰 前面的章節已經展示瞭如何處理和分析文字語料函式庫,並且強調了自然語言處理(NLP)在處理日益增長的海量電子語言資料時的挑戰。現在讓我們更深入地探討這些資料,並進行一個思想實驗:假設我們擁有一個包含過去50年來所有英語口語和書面語的龐大語料函式庫。我們是否有理由將這個語料函式庫稱為「現代英語」?答案可能是否定的。回想第三章,我們曾要求你在網路上搜尋「the of」這個模式的例項。雖然在網路上很容易找到包含這個詞序的例子,例如「New man at the of IMG」(參見http://www.telegraph.co.uk/sport/2387900/New-man-at-the-of-IMG.html),但英語使用者會認為大多數這樣的例子是錯誤的,因此並不屬於英語的一部分。因此,我們可以認為「現代英語」並不等同於我們這個虛擬語料函式庫中的龐大詞序列。英語使用者能夠對這些序列做出判斷,並且會拒絕其中一些,認為它們不符合語法。

同樣,我們可以輕易地組成一個新的句子,並且讓其他英語使用者同意這個句子是完全正確的英語。例如,句子具有一個有趣的特性,即它們可以嵌入到更大的句子中。考慮以下句子:

(1) a. Usain Bolt 打破了100米記錄。 b. The Jamaica Observer 報導說Usain Bolt 打破了100米記錄。 c. Andre 說The Jamaica Observer 報導說Usain Bolt 打破了100米記錄。 d. 我認為Andre 說The Jamaica Observer 報導說Usain Bolt 打破了100米記錄。

如果我們用符號S來替換整個句子,我們會看到像「Andre 說S」和「我認為S」這樣的模式。這些是將一個句子嵌入到更大句子中的範本。我們還可以使用其他範本,如「S 但是S」和「S 當S」。只要有一點創意,我們就可以使用這些範本建構出一些非常長的句子。以下是一個來自A.A. Milne的《小熊維尼》故事中的令人印象深刻的例子,標題是《在水中的小豬》:

你可以想像,當船隻出現在視野中時,小豬的喜悅。在後來的歲月裡,他喜歡回想起自己在可怕的洪水中經歷了非常大的危險,但實際上他真正面臨的危險只是被囚禁的最後半小時,當時剛飛來的貓頭鷹坐在他樹枝上安慰他,並告訴他一個很長的故事,關於他的姑姑曾經錯誤地生下了一個海鷗蛋。這個故事像這樣的句子一樣,繼續不斷地延伸,直到小豬聽著窗外的聲音,毫無希望,靜靜地睡著了,慢慢地從窗戶滑向水裡,直到他只剩下腳趾掛著。幸運的是,貓頭鷹突然發出的響亮叫聲,其實是故事的一部分,是他姑姑說的話,喚醒了小豬,讓他及時將自己拉回安全的地方,並且說:「多有趣,她真的這樣做了嗎?」就在這時——你可以想像,當他最後看到那艘好船「小熊的腦袋」(船長C. Robin;大副P. Bear)駛過大海來救援他時,他的喜悅。

內容解密:

這個長句實際上具有一個簡單的結構,開始於「S 但是S 當S」。從這個例子中,我們可以看到語言為我們提供了可以無限擴充套件句子的結構。同樣值得注意的是,我們可以理解任意長度的句子,即使是我們以前從未聽過的句子:我們可以輕易地創造出一個全新的句子,一個在語言歷史上可能從未被使用過的句子,但所有使用該語言的人都能理解它。

文法的目的是對語言進行明確的描述。但是,我們對文法的思考方式與我們認為的語言是什麼密切相關。它是一個由觀察到的話語和書面文字組成的龐大但有限的集合嗎?它是一種更抽象的概念,例如具有關於合乎語法的句子的隱含知識的合格語言使用者所具備的知識?還是兩者的結合?我們不會在這個問題上表態,而是會介紹主要的方法。

在本章中,我們將採用「生成文法」的正式框架,其中「語言」被視為不過是所有合乎語法的句子的龐大集合,而文法是一種可以用來「生成」該集合成員的正式符號。文法使用遞迴生成式,如「S → S 和S」,我們將在8.3節中探討。在第10章中,我們將擴充套件這個框架,以自動構建句子的意義,使其由其各個部分的意義組成。

無處不在的歧義

一個著名的歧義例子如(2)所示,來自格魯喬·馬克斯的電影《動物餅乾》(1930年):

(2) 在非洲狩獵時,我穿著睡衣射殺了一頭大象。我永遠也不知道大象是如何穿上我的睡衣的。

讓我們仔細看看短語「I shot an elephant in my pajamas」的歧義。首先,我們需要定義一個簡單的文法:

>>> groucho_grammar = nltk.parse_cfg("""
... S -> NP VP
... PP -> P NP
... NP -> Det N | Det N PP | 'I'
... VP -> V NP | VP PP
... Det -> 'an' | 'my'
... N -> 'elephant' | 'pajamas'
... V -> 'shot'
... P -> 'in'
... """)

內容解密:

這個文法允許句子根據「in my pajamas」這個介詞短語是描述大象還是射擊事件而有兩種不同的分析方法。

>>> sent = ['I', 'shot', 'an', 'elephant', 'in', 'my', 'pajamas']
>>> parser = nltk.ChartParser(groucho_grammar)
>>> trees = parser.nbest_parse(sent)
>>> for tree in trees:
... print(tree)
(S
  (NP I)
  (VP
    (V shot)
    (NP (Det an) (N elephant) (PP (P in) (NP (Det my) (N pajamas))))))
(S
  (NP I)
  (VP
    (VP (V shot) (NP (Det an) (N elephant)))
    (PP (P in) (NP (Det my) (N pajamas)))))

內容解密:

這個程式生成了兩個括號結構,我們可以將其表示為樹,如(3)所示:

(3) a.

b.

內容解密:

這兩個樹結構代表了句子的兩種不同解析方式,分別對應於「我穿著睡衣射殺了一頭大象」和「我射殺了一頭穿著我睡衣的大象」這兩種不同的語義解釋。這個例子展示了語言中的歧義現象,即一個句子可以有多種不同的語法分析。