词性标注与句法分析
语法曾一度不入时。后来每条 LLM 流水线都得校验结构化抽取,它就回来了。
类型: Build 语言: Python 前置要求: Phase 5 · 01(文本处理)、Phase 2 · 14(朴素贝叶斯) 预计时间: ~45 分钟
问题背景
第 01 课许诺过:词形还原需要词性标签。不知道 running 是动词,词形还原器就没法把它还原成 run。不知道 better 是形容词,就没法还原成 good。
那个许诺背后藏着一整个子领域。词性标注给词分配语法类别。句法分析恢复句子的树状结构:哪个词修饰哪个,哪个动词支配哪些论元。经典 NLP 花了二十年打磨这两件事。然后深度学习把它们塌缩成预训练 transformer 之上的一个 token 分类任务,研究界就往前走了。
但应用界没走。每条结构化抽取流水线底下仍然用着词性和依存树。LLM 生成的 JSON 要拿语法约束去校验。问答系统用依存分析拆解查询。机器翻译质量评估器检查分析树之间的对齐。
值得懂。这节课介绍标签集、各种基线,以及你该在哪个点停止从零实现、转而调用 spaCy。
核心概念
词性标注 给每个 token 标一个语法类别。Penn Treebank(PTB) 标签集是英语的默认选择。36 个标签,有些区分在随意的读者看来很挑剔:NN 单数名词、NNS 复数名词、NNP 单数专有名词、VBD 动词过去式、VBZ 动词第三人称单数现在时,等等。Universal Dependencies(UD) 标签集更粗(17 个标签)、与语言无关;它成了跨语言工作的默认选择。
The/DET cats/NOUN were/AUX running/VERB at/ADP 3pm/NOUN ./PUNCT句法分析 产出一棵树。两大流派:
- 成分句法分析(Constituency)。 名词短语、动词短语、介词短语相互嵌套。输出是一棵非终结符类别(NP、VP、PP)的树,词作为叶子。
- 依存句法分析(Dependency)。 每个词有唯一一个它所依赖的中心词(head),并标上语法关系。输出是一棵树,每条边都是一个 (head, dependent, relation) 三元组。
依存分析在 2010 年代胜出,因为它能干净地跨语言泛化,尤其是自由语序的语言。
running is ROOT
cats is nsubj of running
were is aux of running
at is prep of running
3pm is pobj of atpos-taggerdependency-arcs动手构建
第 1 步:最频繁标签基线
最笨却管用的词性标注器。对每个词,预测它在训练里出现最频繁的那个标签。
from collections import Counter, defaultdict
def train_mft(train_examples):
word_tag_counts = defaultdict(Counter)
all_tags = Counter()
for tokens, tags in train_examples:
for token, tag in zip(tokens, tags):
word_tag_counts[token.lower()][tag] += 1
all_tags[tag] += 1
word_best = {w: c.most_common(1)[0][0] for w, c in word_tag_counts.items()}
default_tag = all_tags.most_common(1)[0][0]
return word_best, default_tag
def predict_mft(tokens, word_best, default_tag):
return [word_best.get(t.lower(), default_tag) for t in tokens]在 Brown 语料上,这个基线命中约 85% 准确率。不算好,但任何严肃模型都不该跌破这条地板线。
第 2 步:二元 HMM 标注器
建模序列的联合概率:
P(tags, words) = prod P(tag_i | tag_{i-1}) * P(word_i | tag_i)两张表:转移概率(给定前一个标签时的标签),发射概率(给定标签时的词)。两者都用拉普拉斯平滑从计数里估计。用 Viterbi 解码(在标签网格上做动态规划)。
import math
def train_hmm(train_examples, alpha=0.01):
transitions = defaultdict(Counter)
emissions = defaultdict(Counter)
tags = set()
vocab = set()
for tokens, ts in train_examples:
prev = "<BOS>"
for token, tag in zip(tokens, ts):
transitions[prev][tag] += 1
emissions[tag][token.lower()] += 1
tags.add(tag)
vocab.add(token.lower())
prev = tag
transitions[prev]["<EOS>"] += 1
return transitions, emissions, tags, vocab
def log_prob(table, given, key, smooth_denom, alpha):
return math.log((table[given].get(key, 0) + alpha) / smooth_denom)
def viterbi(tokens, transitions, emissions, tags, vocab, alpha=0.01):
tags_list = list(tags)
n = len(tokens)
V = [[0.0] * len(tags_list) for _ in range(n)]
back = [[0] * len(tags_list) for _ in range(n)]
for j, tag in enumerate(tags_list):
em_denom = sum(emissions[tag].values()) + alpha * (len(vocab) + 1)
tr_denom = sum(transitions["<BOS>"].values()) + alpha * (len(tags_list) + 1)
tr = log_prob(transitions, "<BOS>", tag, tr_denom, alpha)
em = log_prob(emissions, tag, tokens[0].lower(), em_denom, alpha)
V[0][j] = tr + em
back[0][j] = 0
for i in range(1, n):
for j, tag in enumerate(tags_list):
em_denom = sum(emissions[tag].values()) + alpha * (len(vocab) + 1)
em = log_prob(emissions, tag, tokens[i].lower(), em_denom, alpha)
best_prev = 0
best_score = -1e30
for k, prev_tag in enumerate(tags_list):
tr_denom = sum(transitions[prev_tag].values()) + alpha * (len(tags_list) + 1)
tr = log_prob(transitions, prev_tag, tag, tr_denom, alpha)
score = V[i - 1][k] + tr + em
if score > best_score:
best_score = score
best_prev = k
V[i][j] = best_score
back[i][j] = best_prev
last_best = max(range(len(tags_list)), key=lambda j: V[n - 1][j])
path = [last_best]
for i in range(n - 1, 0, -1):
path.append(back[i][path[-1]])
return [tags_list[j] for j in reversed(path)]Brown 上的二元 HMM 命中约 93% 准确率。从 85% 到 93% 的跃升大部分来自转移概率——模型学到 DET NOUN 很常见,NOUN DET 很罕见。
第 3 步:现代标注器为什么打得过它
转移 + 发射概率是局部的。它们捕捉不到 saw 在 "I bought a saw" 里是名词、在 "I saw the movie" 里是动词。一个带任意特征(后缀、词形、前后词、词本身)的 CRF 命中约 97%。BiLSTM-CRF 或 transformer 命中约 98%+。
这个任务的上限由标注者分歧决定。人类标注者在 Penn Treebank 上约有 97% 的一致率。超过 98% 的模型多半是在过拟合测试集。
第 4 步:依存句法分析的勾勒
从零做完整的依存分析超出范围;经典教科书的处理见 Jurafsky 和 Martin。两个要知道的经典流派:
- 基于转移的(transition-based) 分析器(arc-eager、arc-standard)像移进-归约分析器:读 token,把它们移进一个栈,再施加创建弧的归约动作。贪心解码很快。经典实现是 MaltParser。现代神经版:Chen 和 Manning 的基于转移的分析器。
- 基于图的(graph-based) 分析器(Eisner 算法、Dozat-Manning biaffine)给每一条可能的 head-dependent 边打分,挑出最大生成树。更慢但更准。
大多数应用工作里,调 spaCy 就行:
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("The cats were running at 3pm.")
for token in doc:
print(f"{token.text:10s} tag={token.tag_:5s} pos={token.pos_:6s} dep={token.dep_:10s} head={token.head.text}")The tag=DT pos=DET dep=det head=cats
cats tag=NNS pos=NOUN dep=nsubj head=running
were tag=VBD pos=AUX dep=aux head=running
running tag=VBG pos=VERB dep=ROOT head=running
at tag=IN pos=ADP dep=prep head=running
3pm tag=NN pos=NOUN dep=pobj head=at
. tag=. pos=PUNCT dep=punct head=running把 dep 列从下往上读,句子的语法结构就浮出来了。
实际使用
每个生产级 NLP 库都把词性和依存分析器作为标准流水线的一部分提供。
- spaCy(
en_core_web_sm/md/lg/trf)。快、准,和分词 + NER + 词形还原集成在一起。token.tag_(Penn)、token.pos_(UD)、token.dep_(依存关系)。 - Stanford NLP(stanza)。斯坦福对 CoreNLP 的继任者。在 60+ 种语言上处于最前沿。
- trankit。基于 transformer,UD 准确率不错。
- NLTK。
pos_tag。能用,慢,较旧。教学够用。
它在 2026 年仍然重要的地方
- 词形还原。 第 01 课需要词性才能正确还原。永远如此。
- 从 LLM 输出做结构化抽取。 校验生成的句子是否遵守语法约束(比如主谓一致、必需的修饰语)。
- 基于方面的情感。 依存分析告诉你哪个形容词修饰哪个名词。
- 查询理解。 "movies directed by Wes Anderson starring Bill Murray" 通过分析拆解成结构化约束。
- 跨语言迁移。 UD 标签和依存关系与语言无关,使得对新语言做 zero-shot 结构分析成为可能。
- 低算力流水线。 如果你没法上 transformer,词性 + 依存分析 + 词典能带你走得出奇地远。
拿去用
存为 outputs/skill-grammar-pipeline.md:
---
name: grammar-pipeline
description: Design a classical POS + dependency pipeline for a downstream NLP task.
version: 1.0.0
phase: 5
lesson: 07
tags: [nlp, pos, parsing]
---
Given a downstream task (information extraction, rewrite validation, query decomposition, lemmatization), you output:
1. Tagset to use. Penn Treebank for English-only legacy pipelines, Universal Dependencies for multilingual or cross-lingual.
2. Library. spaCy for most production, stanza for academic-grade multilingual, trankit for highest UD accuracy. Name the specific model ID.
3. Integration pattern. Show the 3-5 lines that call the library and consume the needed attributes (`.pos_`, `.dep_`, `.head`).
4. Failure mode to test. Noun-verb ambiguity (`saw`, `book`, `can`) and PP-attachment ambiguity are the classical traps. Sample 20 outputs and eyeball.
Refuse to recommend rolling your own parser. Building parsers from scratch is a research project, not an application task. Flag any pipeline that consumes POS tags without handling lowercase/uppercase variants as fragile.练习
- 简单。 在一个小型带标注语料(比如 NLTK 的 Brown 子集)上用最频繁标签基线,在留出句子上测准确率。验证那个约 85% 的结果。
- 中等。 训练上面的二元 HMM,报告各标签的精确率/召回率。HMM 把哪些标签搞混得最多?
- 困难。 用 spaCy 的依存分析从一个 1000 句的样本里抽取主-谓-宾三元组。在 50 个手工标注的三元组上评估。记录抽取在哪里失败(往往是被动语态、并列结构和省略主语)。
关键术语
| 术语 | 人们怎么说 | 它实际是什么 |
|---|---|---|
| 词性标签(POS tag) | 词的类型 | 语法类别。PTB 有 36 个;UD 有 17 个。 |
| Penn Treebank | 标准标签集 | 英语专用。动词时态和名词数都很细。 |
| Universal Dependencies | 多语言标签集 | 比 PTB 粗;与语言无关;跨语言工作的默认选择。 |
| 依存分析 | 句子树 | 每个词有一个 head,每条边有一个语法关系。 |
| Viterbi | 动态规划 | 给定发射和转移,找出概率最高的标签序列。 |
延伸阅读
- Jurafsky and Martin — Speech and Language Processing, chapters 8 and 18 —— 词性与句法分析的经典教科书处理。
- Universal Dependencies project —— 每个多语言分析器都在用的跨语言标签集与树库合集。
- spaCy linguistic features guide ——
Token上每个属性的实用参考。 - Chen and Manning (2014). A Fast and Accurate Dependency Parser using Neural Networks —— 把神经分析器带入主流的那篇论文。