跳转至

语言学基础

语言学为NLP系统提供了它们隐式学习并利用的结构化词汇。本文涵盖形态学、句法学、语义学、语用学、音系学、成分句法和依存句法分析,以及分布假设——这些人类语言科学构成了AI中词元化、语法和意义的基础。

  • 在构建能够理解或生成语言的系统之前,我们需要理解语言本身是如何运作的。

  • 语言学是对语言的科学研究,它为NLP提供了不断借用的概念性词汇。

  • 即使是现代神经模型——它们从原始数据中学习语言——也会隐式地重新发现语言学家们几十年来已经编目的许多结构。

  • 语言在每一层都具有结构:组成单词的声音、组成单词的部件、将单词组合成句子的规则、这些句子所承载的意义,以及语境如何塑造解读。我们将自下而上地逐层探索。

  • 形态学是对单词内部结构的研究。单词并非不可分割的原子;它们由更小的有意义的单元构建而成,这些单元称为语素

  • 单词"unhappiness"包含三个语素:"un-"(前缀,意为"不")、"happy"(词根)和"-ness"(后缀,将形容词转化为名词)。每个语素都对意义有所贡献。

  • 词根(或称词干)是承载主要意义的核心语素。"Happy"、"run"、"compute"都是词根。

  • 词缀是附加到词根上以修饰其意义或语法功能的语素。

  • 英语中有前缀(位于词根之前:un-、re-、pre-)和后缀(位于词根之后:-ing、-ed、-tion)。一些语言还包含中缀(插入词根内部)和环缀(包裹在词根周围)。

语素树:"unhappiness"分解为前缀"un"、词根"happy"、后缀"ness"

  • 形态学对NLP很重要,因为它影响词元化。一个基于词级的词元化器会将"run"、"runs"、"running"和"ran"视为四个互不相关的符号。

  • 一个具有形态学意识的系统会识别出它们共享同一个词根。子词词元化(BPE、WordPiece)——我们将在文件02中讨论——是形态学分析的统计近似方法。

  • 句法学研究单词如何组合成短语和句子。每种语言都有控制词序和结构的规则;违反这些规则会产生无意义的输出。

  • "The cat sat on the mat"是合乎语法的英语;"Mat the on sat cat the"则不是。

  • 描述句法结构主要有两种框架。

  • 短语结构语法(也称为成分语法)认为句子是通过将一个短语嵌套在另一个短语内部构建而成的。一个句子(S)由一个名词短语(NP)和一个动词短语(VP)组成。

  • 一个名词短语可能由一个限定词(Det)后跟一个名词(N)组成。一个动词短语可能由一个动词(V)后跟一个名词短语组成。这些规则构建出一棵树:

成分树:"the cat sat on the mat":S分支为NP和VP,NP分支为Det"the"和N"cat",VP分支为V"sat"和PP,PP分支为P"on"和NP

  • 这棵树称为成分树(或分析树)。每个内部节点是一个短语类型,每个叶子节点是一个单词。这棵树捕捉了层次化分组:"on the mat"是一个单元(介词短语),"sat on the mat"是一个单元(动词短语),而整个结构是一个句子。

  • 上下文无关文法(CFG)将这些规则形式化。它由一组产生式规则组成,每条规则的形式为 \(A \to \alpha\),其中 \(A\) 是一个非终结符(如NP或VP这样的短语类型),\(\alpha\) 是一个由终结符(单词)和非终结符组成的序列。例如:

S  → NP VP
NP → Det N
NP → Det N PP
VP → V NP
VP → V PP
PP → P NP
Det → "the" | "a"
N  → "cat" | "mat" | "dog"
V  → "sat" | "chased"
P  → "on" | "under"
  • 从S开始,反复应用规则,你可以生成该文法允许的所有句子。分析则是相反的过程:给定一个句子,找出产生它的树(或所有可能的树)。一个有多个有效分析树的句子称为句法歧义。"I saw the man with the telescope"有两种分析:我使用望远镜看到了那个男人,或者我看到了一个拿着望远镜的男人。

  • 依存语法采取了一种不同的视角。它不依赖短语嵌套,而是描述单词之间的直接关系。句子中的每个单词都恰好依赖于另一个单词(它的核心词),除了句子的根节点。结果是一个依存树,其中边标有语法关系标签(主语、宾语、修饰语等)。

依存树:"the cat sat on the mat":从"sat"到"cat"的箭头(nsubj)和到"on"的箭头(prep),从"on"到"mat"的箭头(pobj),从"cat"到"the"的箭头(det),从"mat"到"the"的箭头(det)

  • 在依存视角下,"sat"是根节点。"Cat"作为主语(nsubj)依赖于"sat"。"On"作为介词修饰语依赖于"sat"。"Mat"作为介词宾语依赖于"on"。每个单词都挂在恰好一个核心词上,形成一棵树。

  • 依存语法已成为现代NLP中的主导框架,因为依存树更容易用统计分析器生成,而且这些关系更直接地映射到语义角色(谁对谁做了什么)。

  • 配价描述一个动词需要多少个论元。"Sleep"是不及物动词(一个论元:睡觉者)。"Eat"是及物动词(两个:吃者和被吃之物)。"Give"是双及物动词(三个:给予者、给予之物和接受者)。了解动词的配价可以约束哪些分析树是有效的。

  • 语义学是对意义的研究。句法学告诉你句子是如何结构的;语义学告诉你句子意味着什么。

  • 词汇语义学关注单个单词的意义。单词之间以系统性的方式相互关联:

    • 同义关系:具有(几乎)相同意义的单词。"Big"和"large"是同义词。真正完美的同义词是罕见的;几乎总是存在含义或用法上的细微差别。
    • 反义关系:具有相反意义的单词。"Hot"和"cold","buy"和"sell"。
    • 上位关系/下位关系:"是一种"关系。"Dog"是"animal"的下位词(狗是一种动物)。"Animal"是"dog"的上位词。这些关系形成分类层次结构。
    • 部分整体关系:"组成部分"关系。"Wheel"是"car"的部分词。
    • 多义关系:一个单词具有多个相关意义。"Bank"可以指金融机构或河岸。语境可以消除歧义。
  • 词义消歧(WSD)是根据上下文确定多义词的哪个义项被使用的任务。在"I deposited money at the bank"中,金融义项是正确的。在"We sat by the river bank"中,地理义项是正确的。WSD是早期NLP中的一个核心问题;现代的上下文嵌入(ELMo、BERT)通过为同一个单词的不同用法生成不同的向量表示,在很大程度上解决了这个问题。

  • 组合语义学研究单个单词的意义如何组合以形成短语或句子的意义。组合性原则(归功于弗雷格)指出,一个复杂表达式的意义由其组成部分的意义以及组合这些部分的规则共同决定。"The cat chased the dog"与"the dog chased the cat"意义不同,因为句法结构(谁是主语、谁是宾语)与单词意义相互作用。

  • 并非所有意义都是组合性的。习语如"kick the bucket"(意为"去世")具有无法从其组成部分推导出的意义。这对任何组合性方法都是一个挑战。

  • 分布语义学是支撑现代NLP的计算性意义研究方法。分布假设(Firth, 1957)指出:"观其伴,知其意。"(You shall know a word by the company it keeps.)出现在相似语境中的单词往往具有相似的意义。这是词嵌入(Word2Vec、GloVe)的理论基础,我们将在文件03中深入探讨。

  • 语用学研究语境如何影响意义。同一个句子根据说话者、时间、地点和原因的不同,可能意味着不同的事情。

  • "Can you pass the salt?"在句法上是一个关于能力的疑问句。在语用上,它是一个请求。你不会回答"是的,我能"然后坐着不动。理解这一点需要超越字面意义的知识,具体来说,是关于言语行为的惯例知识。

  • 言语行为理论(Austin, Searle)区分了:

    • 言内行为:字面内容("Can you pass the salt?")
    • 言外行为:意图实现的功能(一个请求)
    • 言后行为:对听者产生的效果(他们递过盐)
  • 隐涵(Grice)是指被暗示但未明确陈述的意义。如果有人问"Is John a good cook?"而你回答"He's British",你并没有从字面上回答问题,但听者可以推断(通过文化刻板印象,无论公平与否)你的意思是"不好"。Grice的合作原则指出,说话者通常会努力做到信息充分、真实、相关和清晰,而听者假定这些准则成立来进行解读。

  • 共指是一种语用现象,其中不同的表达指向同一个实体。在"Alice went to the store. She bought milk"中,"she"指代Alice。解决共指问题对于理解多句文本至关重要,是NLP中的一个关键任务。

  • 篇章结构描述句子如何连接以形成连贯的文本。叙事有开头、中间和结尾。论证有主张和证据。修辞结构理论(RST)将文本分析为篇章关系(阐述、对比、因果等)的树状结构。

  • 语用学是NLP中最困难的领域。现代语言模型通过训练数据隐式地处理了大部分句法和语义,但语用推理——理解讽刺、隐涵和依赖语境的意义——仍然是一个前沿挑战。

  • 音系学研究语言的声音系统。虽然本章主要关注文本,但简要概述可以衔接音频和语音章节(第09章)。

  • 音位是区分意义的最小声音单位。英语约有44个音位。单词"bat"和"pat"相差一个音位(/b/ 与 /p/),而意义的改变是完全性的。这被称为最小对立体

  • 音位变体是同一个音位的不同物理实现,不改变意义。"pin"中的"p"(送气音,带一股气流)和"spin"中的"p"(不送气音)在英语中是音位/p/的音位变体;母语者将它们视为同一个声音。

  • 国际音标(IPA)为所有语言的音位提供了标准化的记法。单词"cat"转录为/kæt/。IPA是书面文本和语音系统之间的桥梁。

  • 韵律涵盖语音的节奏、重音和语调。"I didn't say he stole the money"根据重音落在哪个单词上,可以有七种不同的含义。韵律携带了纯文本所丢失的信息,这就是为什么文本转语音系统必须仔细建模韵律的原因。

  • 在NLP中,音系学知识出现在文本转语音(字形到音位的转换)、语音识别(将声学信号映射到音位),甚至拼写纠正和音译中。

编程练习(使用CoLab或notebook)

  1. 构建一个简单的形态分析器,使用常见前缀和后缀列表将英语单词分解为可能的语素。

    prefixes = ['un', 're', 'pre', 'dis', 'mis', 'over', 'under', 'out', 'non']
    suffixes = ['ing', 'ed', 'ly', 'ness', 'ment', 'tion', 'able', 'ible', 'er', 'est', 'ful', 'less', 'ous']
    
    def analyse_morphemes(word):
        """使用已知词缀进行简单的语素分析。"""
        parts = []
        remaining = word.lower()
    
        # 检查前缀
        for p in sorted(prefixes, key=len, reverse=True):
            if remaining.startswith(p) and len(remaining) > len(p) + 2:
                parts.append(f"[prefix: {p}]")
                remaining = remaining[len(p):]
                break
    
        # 检查后缀
        for s in sorted(suffixes, key=len, reverse=True):
            if remaining.endswith(s) and len(remaining) > len(s) + 2:
                root = remaining[:-len(s)]
                parts.append(f"[root: {root}]")
                parts.append(f"[suffix: {s}]")
                remaining = None
                break
    
        if remaining is not None:
            parts.append(f"[root: {remaining}]")
    
        return parts
    
    for word in ['unhappiness', 'reusable', 'disconnected', 'overreacting', 'kindness']:
        print(f"{word:20s}{' + '.join(analyse_morphemes(word))}")
    

  2. 实现一个使用递归下降法的简单上下文无关文法分析器。定义一个小型文法,并将句子分析为成分树。

    class CFGParser:
        """用于小型英语文法的递归下降分析器。"""
        def __init__(self, tokens):
            self.tokens = tokens
            self.pos = 0
    
        def peek(self):
            return self.tokens[self.pos] if self.pos < len(self.tokens) else None
    
        def consume(self, expected=None):
            tok = self.peek()
            if expected and tok != expected:
                return None
            self.pos += 1
            return tok
    
        def parse_det(self):
            if self.peek() in ('the', 'a'):
                return ('Det', self.consume())
            return None
    
        def parse_noun(self):
            if self.peek() in ('cat', 'dog', 'mat', 'man'):
                return ('N', self.consume())
            return None
    
        def parse_verb(self):
            if self.peek() in ('sat', 'chased', 'saw'):
                return ('V', self.consume())
            return None
    
        def parse_prep(self):
            if self.peek() in ('on', 'under', 'with'):
                return ('P', self.consume())
            return None
    
        def parse_np(self):
            save = self.pos
            det = self.parse_det()
            noun = self.parse_noun()
            if det and noun:
                # 检查可选的PP
                pp = self.parse_pp()
                if pp:
                    return ('NP', det, noun, pp)
                return ('NP', det, noun)
            self.pos = save
            return None
    
        def parse_pp(self):
            save = self.pos
            prep = self.parse_prep()
            np = self.parse_np()
            if prep and np:
                return ('PP', prep, np)
            self.pos = save
            return None
    
        def parse_vp(self):
            save = self.pos
            verb = self.parse_verb()
            if verb:
                np = self.parse_np()
                if np:
                    return ('VP', verb, np)
                pp = self.parse_pp()
                if pp:
                    return ('VP', verb, pp)
            self.pos = save
            return None
    
        def parse_sentence(self):
            np = self.parse_np()
            vp = self.parse_vp()
            if np and vp and self.pos == len(self.tokens):
                return ('S', np, vp)
            return None
    
    def print_tree(tree, indent=0):
        if isinstance(tree, str):
            print(' ' * indent + tree)
        elif isinstance(tree, tuple):
            print(' ' * indent + tree[0])
            for child in tree[1:]:
                print_tree(child, indent + 2)
    
    sentences = [
        "the cat sat on the mat",
        "a dog chased the cat",
    ]
    
    for sent in sentences:
        tokens = sent.split()
        parser = CFGParser(tokens)
        tree = parser.parse_sentence()
        print(f"\n'{sent}':")
        if tree:
            print_tree(tree)
        else:
            print("  (no parse found)")
    

  3. 通过构建一个简单的词图来探索词汇关系。给定一个包含同义、反义和上位关系的小型词汇表,查找单词之间的路径。

    relations = {
        ('big', 'large'): 'synonym',
        ('big', 'small'): 'antonym',
        ('small', 'tiny'): 'synonym',
        ('dog', 'animal'): 'hypernym',
        ('cat', 'animal'): 'hypernym',
        ('puppy', 'dog'): 'hypernym',
        ('happy', 'glad'): 'synonym',
        ('happy', 'sad'): 'antonym',
        ('hot', 'cold'): 'antonym',
        ('hot', 'warm'): 'synonym',
    }
    
    # 构建邻接列表
    from collections import defaultdict, deque
    
    graph = defaultdict(list)
    for (w1, w2), rel in relations.items():
        graph[w1].append((w2, rel))
        graph[w2].append((w1, rel))
    
    def find_path(start, end):
        """使用BFS在关系图中查找两个单词之间的路径。"""
        queue = deque([(start, [(start, None)])])
        visited = {start}
        while queue:
            node, path = queue.popleft()
            if node == end:
                return path
            for neighbor, rel in graph[node]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append((neighbor, path + [(neighbor, rel)]))
        return None
    
    pairs = [('big', 'tiny'), ('puppy', 'cat'), ('happy', 'sad')]
    for w1, w2 in pairs:
        path = find_path(w1, w2)
        if path:
            steps = " → ".join(f"{w}({r})" if r else w for w, r in path)
            print(f"{w1}{w2}: {steps}")
        else:
            print(f"{w1}{w2}: no path found")