Skip to content

语言学基础

语言学提供了 NLP 系统隐式学习并利用的结构性词汇。本文件涵盖形态学、句法、语义学、语用学、音系学、成分句法分析、依存句法分析以及分布假说——这些是支撑 AI 中分词、语法和意义的人类语言科学。

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

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

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

  • 语言的每一个层面都有结构:构成单词的声音、构成单词的部件、将单词组合成句子的规则、这些句子携带的意义,以及上下文塑造解读的方式。我们将自下而上地逐一考察每个层面。

  • 形态学研究单词的内部结构。单词并非不可再分;它们由更小的、有意义的单位构成,这些单位称为语素

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

  • 词根是承载核心意义的语素。“happy”(快乐)、“run”(跑)、“compute”(计算)等都是词根。

  • 词缀是附着在词根上以修饰其意义的语素。

  • 英语中有前缀(位于词根前:un-, re-, pre-)和后缀(位于词根后:-ing, -ed, -tion)。有些语言还有中缀(插入词根内部)和环缀(环绕词根)。

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

  • 形态过程分为两类。屈折变化改变一个词的语法属性,而不改变其核心意义或词性:“run” 变成 “runs”(第三人称)、“running”(进行体)、“ran”(过去时)。这个词仍然是动词,意义相同。

  • 派生创造一个新词,通常会改变词性:“happy”(形容词)变成 “happiness”(名词),“compute”(动词)变成 “computation”(名词)再变成 “computational”(形容词)。每次派生都会改变意义和语法范畴。

  • 不同语言的形态复杂程度差异巨大。英语相对分析型(每个单词的语素较少,依赖词序)。

  • 土耳其语和芬兰语是黏着型(单词可以串联多个语素)。阿拉伯语和希伯来语使用词模形态(词根是辅音骨架,如 k-t-b 表示“写”,通过插入不同的元音模式形成不同的词:kitab “书”,kataba “他写了”,maktub “被写的/书信”)。

  • 形态学对 NLP 很重要,因为它影响分词。一个词级的分词器将 “run”,“runs”,“running” 和 “ran” 视为四个不相关的符号。

  • 而具有形态感知的系统会识别出它们共享一个词根。我们将在文件02中介绍的子词分词(BPE,WordPiece)是形态分析的一种统计近似。

  • 句法研究单词如何组合成短语和句子。每种语言都有关于词序和结构的规则;违反规则会产生无意义的乱码。

  • “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” 是一个单位(动词短语),整个句子是一个句子。

  • 上下文无关文法形式化了这些规则。它由一组产生式规则组成,每条规则形式为 \(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”。
    • 上下位关系:“是一种”(is-a)关系。“Dog” 是 “animal” 的下位词(狗是一种动物)。“Animal” 是 “dog” 的上位词。这些形成了分类层次。
    • 部分-整体关系:“部分-整体” 关系。“Wheel” 是 “car” 的部分词。
    • 多义词:一个单词有多个相关的意义。“Bank” 可以指金融机构,也可以指河岸。上下文可以消除歧义。
  • 词义消歧是确定多义词在给定上下文中是哪个意义的任务。在 “I deposited money at the bank” 中,金融意义是正确的。在 “We sat by the river bank” 中,地理意义是正确的。词义消歧是早期 NLP 的核心问题;现代上下文嵌入在很大程度上通过为同一个词的不同用法生成不同的向量表示来解决它。

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

  • 并非所有意义都是组合的。像 “kick the bucket”(意为“死亡”)这样的习语,其意义无法从其组成部分推导出来。这对任何组合方法都是挑战。

  • 分布语义学是支撑现代 NLP 的计算意义方法。分布假说指出:“观其伴,知其意。”出现在相似上下文中的单词往往具有相似的意义。这是词嵌入的理论基础,我们将在文件03中探讨。

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

  • “Can you pass the salt?” 从句法上是一个关于能力的 yes/no 问句。从语用上,它是一个请求。你不会回答“是的,我可以”然后坐着不动。理解这一点需要超越字面单词的知识,特别是关于言语行为的惯例。

  • 言语行为理论区分了:

    • 言内行为:字面内容(“Can you pass the salt?”)
    • 言外行为:预期的功能(一个请求)
    • 言后行为:对听者的效果(他们递过盐)
  • 含义是隐含但未明确陈述的意义。如果有人问“John 是个好厨师吗?”,你回答“他是英国人。”你没有从字面上回答问题,但听者可以推断(通过文化刻板印象,无论公平与否)你的意思是“不是”。格赖斯的合作原则认为,说话者通常试图提供信息、真实、相关和清晰,而听者假设这些准则成立来理解话语。

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

  • 语篇结构描述了句子如何连接成连贯的文本。叙述有开头、中间和结尾。论证有主张和证据。修辞结构理论将文本分析为语段之间语篇关系的树(详述、对比、原因等)。

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

  • 音系学研究语言的语音系统。虽然这一章侧重于文本,但简要概述可以架起通往音频和语音章节(第09章)的桥梁。

  • 音位是区分意义的最小语音单位。英语大约有44个音位。“Bat” 和 “pat” 这两个词相差一个音位(/b/ vs /p/),意义完全不同。这被称为最小对立体

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

  • 国际音标为所有语言的音位提供了标准化的记音符号。单词 “cat” 记作 /kæt/。国际音标是书面文本和语音系统之间的桥梁。

  • 韵律涵盖语音的节奏、重音和语调。“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"[前缀: {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}]")
                parts.append(f"[后缀: {s}]")
                remaining = None
                break
    
        if remaining is not None:
            parts.append(f"[词根: {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("  (未找到解析)")
    

  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):
        """通过关系图广度优先搜索两个词之间的路径。"""
        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}: 未找到路径")