文本处理:分词、词干提取与词形还原


文档摘要

文本处理:分词、词干提取与词形还原 本节摘要:语言是连续的,模型是离散的,预处理是连接二者的桥。模型读不懂 "The cats were running.",它只读得懂整数。每一个 NLP 系统开篇都要回答三个问题:一个词从哪里开始;一个词的词根是什么;什么时候该把 "run"、"running"、"ran" 当成同一个东西,什么时候又该当成不同的东西。分词(Tokenization)错一步,模型就在垃圾上学习;词干提取(Stemming)收敛过度,主题模型就垮掉;词形还原(Lemmatization)缺了词性上下文,动词就被当成名词。本节将带你从零实现这三步预处理,再用 NLTK 与 spaCy 对比验证,看清各自的取舍。

文本处理:分词、词干提取与词形还原

本节摘要:语言是连续的,模型是离散的,预处理是连接二者的桥。模型读不懂 "The cats were running.",它只读得懂整数。每一个 NLP 系统开篇都要回答三个问题:一个词从哪里开始;一个词的词根是什么;什么时候该把 "run"、"running"、"ran" 当成同一个东西,什么时候又该当成不同的东西。分词(Tokenization)错一步,模型就在垃圾上学习;词干提取(Stemming)收敛过度,主题模型就垮掉;词形还原(Lemmatization)缺了词性上下文,动词就被当成名词。本节将带你从零实现这三步预处理,再用 NLTK 与 spaCy 对比验证,看清各自的取舍。

对应原课程:Phase 5 · Lesson 01 · text-processing(原英文 phases/05-nlp-foundations-to-advanced/01-text-processing/docs/en.md)。前置依赖:Phase 2 · 14(朴素贝叶斯)。

学习目标

阅读完本节,你应当能够:

  1. 说清分词、词干提取、词形还原各自的任务与典型失败模式,并据此为任务选型。
  2. 从零实现一个正则分词器、Porter 词干提取器的关键步骤,以及一个查表式词形还原器。
  3. 把三者串成预处理流水线,并理解为何词性标注是高质量还原的前提。
  4. NLTK 与 spaCy 复现同样的工作,识别版本漂移与「训练/推理不一致」这两大生产陷阱。

一、问题与直觉

一句话摆在你面前:The cats were running.。模型要消化它,第一步必须把它变成机器能处理的单元。可单元的边界在哪里,从来不是一个显而易见的问题。

每个 NLP 系统都要先回答三个问题:

  1. 一个词从哪里开始(分词)。
  2. 一个词的词根是什么(词干/词形)。
  3. 什么时候该把 "run"、"running"、"ran" 当成同一个东西(有助于泛化时),什么时候又该当成不同的东西(信息量有差别时)。

这三步任何一步走错,后面全是麻烦:

  • 分词器把 don't 当成一个 token、却把 do n't 当成两个,训练分布就此分裂
  • 词干提取器把 organizationorgan 收敛成同一个词干,主题模型就此报废
  • 词形还原器本该用词性上下文,你却没传给它,动词就被当成了名词。

三种操作,各有职责,各有失败模式:

  • 分词把字符串切成 token。「token」这个词故意定义得模糊,因为正确的粒度取决于任务:经典 NLP 用词级,Transformer 用子词级,无空格的语言(如中文)用字级。
  • 词干提取用规则砍后缀。快、狠、糙。running -> runorganization -> organ。第二个例子就是它的失败模式。
  • 词形还原借助语法知识把词还原成词典形式。慢、准、需要查表或形态分析器。ran -> run(得知道 "ran" 是 "run" 的过去式)、better -> good(得知道比较级形式)。

💡 经验法则:速度要紧、能容忍噪声时,用词干提取(搜索索引、粗分类);语义要紧时,用词形还原(问答、语义搜索、任何要给人看的输出)。

二、从零实现

第 1 步:正则词级分词器

最简单可用的分词器,按非字母数字字符切分,同时把标点保留为独立 token。不完美、不终极,但一行就能跑起来。

import re def tokenize(text): return re.findall(r"[A-Za-z]+(?:'[A-Za-z]+)?|[0-9]+|[^\sA-Za-z0-9]", text)

三条模式,按优先级从高到低匹配:带可选内部撇号的单词(don'tit's);纯数字;任何单个非空白、非字母数字字符作为独立 token(标点)。

>>> tokenize("The cats weren't running at 3pm.") ['The', 'cats', "weren't", 'running', 'at', '3', 'pm', '.']

留意失败模式:3pm 被切成 ['3', 'pm'],因为正则在字母段和数字段之间来回切换。对多数任务够用;但 URL、邮箱、话题标签(#tag)全都会坏。生产环境要在通用模式之前补上专用模式。

第 2 步:Porter 词干提取(仅第 1a 步)

完整的 Porter 算法有五个阶段的规则。仅第 1a 步就覆盖了最常见的英文后缀,也最能说明这套规则模式的精髓。

def stem_step_1a(word): if word.endswith("sses"): return word[:-2] if word.endswith("ies"): return word[:-2] if word.endswith("ss"): return word if word.endswith("s") and len(word) > 1: return word[:-1] return word
>>> [stem_step_1a(w) for w in ["caresses", "ponies", "caress", "cats"]] ['caress', 'poni', 'caress', 'cat']

规则从上往下读。ies -> i 这条规则就是 ponies -> poni(而非 pony)的原因;真正的 Porter 有第 1b 步会修正它。规则互相竞争,靠前的规则胜出——顺序比任何单条规则都重要。

第 3 步:查表式词形还原

真正的词形还原需要形态学。一个适合教学的版本,用一张小词形表加一个回退策略。

LEMMA_TABLE = { ("running", "VERB"): "run", ("ran", "VERB"): "run", ("runs", "VERB"): "run", ("better", "ADJ"): "good", ("best", "ADJ"): "good", ("cats", "NOUN"): "cat", ("cat", "NOUN"): "cat", ("were", "VERB"): "be", ("was", "VERB"): "be", ("is", "VERB"): "be", } def lemmatize(word, pos): key = (word.lower(), pos) if key in LEMMA_TABLE: return LEMMA_TABLE[key] if pos == "VERB" and word.endswith("ing"): return word[:-3] if pos == "NOUN" and word.endswith("s"): return word[:-1] return word.lower()
>>> lemmatize("running", "VERB") 'run' >>> lemmatize("cats", "NOUN") 'cat' >>> lemmatize("better", "ADJ") 'good' >>> lemmatize("watched", "VERB") 'watched'

最后一个例子是关键的教学时刻。watched 不在表里,而回退策略只处理 ing。真正的词形还原要覆盖 ed、不规则动词、比较级形容词、发音变化的复数(children -> child)。这正是生产系统要用 WordNet、spaCy 的形态分析器或完整形态分析器的原因。

第 4 步:串成流水线

def preprocess(text, pos_tagger=None): tokens = tokenize(text) stems = [stem_step_1a(t.lower()) for t in tokens] tags = pos_tagger(tokens) if pos_tagger else [(t, "NOUN") for t in tokens] lemmas = [lemmatize(word, pos) for word, pos in tags] return {"tokens": tokens, "stems": stems, "lemmas": lemmas}

缺的那块是词性标注器。本系列第 07 节(词性标注与句法分析)会从零搭一个。眼下,先把所有 token 默认成 NOUN,并承认这个局限。

三、框架对比

NLTK 与 spaCy 自带生产级实现,各自几行就能跑。

NLTK

import nltk nltk.download("punkt_tab") nltk.download("wordnet") nltk.download("averaged_perceptron_tagger_eng") from nltk.tokenize import word_tokenize from nltk.stem import PorterStemmer, WordNetLemmatizer from nltk import pos_tag text = "The cats were running." tokens = word_tokenize(text) stems = [PorterStemmer().stem(t) for t in tokens] lemmatizer = WordNetLemmatizer() tagged = pos_tag(tokens) def nltk_pos_to_wordnet(tag): if tag.startswith("V"): return "v" if tag.startswith("J"): return "a" if tag.startswith("R"): return "r" return "n" lemmas = [lemmatizer.lemmatize(t, nltk_pos_to_wordnet(tag)) for t, tag in tagged]

word_tokenize 能处理缩写、Unicode、各种正则漏掉的边界情况;PorterStemmer 跑满五个阶段;WordNetLemmatizer 需要把 NLTK 的 Penn Treebank 词性方案翻译成 WordNet 的缩写集。上面那段翻译代码,正是大多数教程略过不讲的「接线」。

spaCy

import spacy nlp = spacy.load("en_core_web_sm") doc = nlp("The cats were running.") for token in doc: print(token.text, token.lemma_, token.pos_)
The the DET cats cat NOUN were be AUX running run VERB . . PUNCT

spaCy 把整条流水线藏在 nlp(text) 背后:分词、词性标注、词形还原一次全跑完。大规模上比 NLTK 快,开箱即用也更准。代价是很难单独替换某个组件

如何取舍

场景 选什么
教学、研究、要换组件 NLTK
生产、多语言、速度要紧 spaCy
Transformer 流水线(反正用模型自带的分词器) 直接用 tokenizers / transformers,跳过经典预处理

两个没人提醒你的失败模式

大多数教程讲完算法就收尾。真实预处理流水线会被两件事咬一口,而它们几乎从不被提及。

可复现性漂移(Reproducibility drift)。 NLTK 和 spaCy 会在版本之间改变分词与还原行为。spaCy 2.x 产出 ['do', "n't"],3.x 可能产出 ["don't"]。你的模型是在一种分布上训练的,推理时跑在另一种上,准确率悄悄下滑,谁也不知道为什么。requirements.txt 里钉死版本,再写一个预处理回归测试,冻结 20 个样例句的期望分词结果,每次升级都跑一遍。

训练/推理不一致(Training/inference mismatch)。 训练时激进预处理(小写化、去停用词、词干提取),部署时直接喂原始用户输入,性能当场崩。这是生产 NLP 最常见的单点故障。训练时做了什么预处理,推理时就必须跑同一个函数。把预处理打包成模型包里的一个函数,而不是一个让上线团队重写一遍的 notebook 单元。

四、可复用产物

一个可复用提示,帮工程师在不啃三本教材的情况下选定预处理策略。保存为 outputs/prompt-preprocessing-advisor.md:

--- name: preprocessing-advisor description: Recommends a tokenization, stemming, and lemmatization setup for an NLP task. phase: 5 lesson: 01 --- You advise on classical NLP preprocessing. Given a task description, you output: 1. Tokenization choice (regex, NLTK word_tokenize, spaCy, or transformer tokenizer). Explain why. 2. Whether to stem, lemmatize, both, or neither. Explain why. 3. Specific library calls. Name the functions. Quote the POS-tag translation if NLTK is involved. 4. One failure mode the user should test for. Refuse to recommend stemming for user-visible text. Refuse to recommend lemmatization without POS tags. Flag non-English input as needing a different pipeline.

五、练习

  1. 基础:扩展 tokenize,让 URL 保持为单个 token。测试:tokenize("Visit https://example.com today.") 应产出一个 URL token。
  2. 进阶:实现 Porter 第 1b 步。若一个词含元音且以 eding 结尾,则去除;处理双辅音规则(hopping -> hop,而非 hopp)。
  3. 挑战:搭一个用 WordNet 做查表的词形还原器,当 WordNet 无条目时回退到你的 Porter 词干提取器。在标注语料上测准确率,与纯 WordNet、纯 Porter 对比。

本节要点回顾

  1. 语言连续、模型离散,预处理是桥——任何 NLP 系统开篇都要回答分词、词根、归并三个问题。
  2. 三种操作各有失败模式:分词错则训练分布分裂;词干收敛过度则主题模型报废;词形还原缺词性则动词当名词。
  3. 分词粒度看任务:经典 NLP 用词级,Transformer 用子词级,无空格语言用字级。
  4. 词干提取快狠糙(organization -> organ),词形还原慢准稳(ran -> run,需词性上下文)。
  5. 选型法则:速度优先、能忍噪声用词干;语义优先、要给人看用词形还原。
  6. 正则分词器一行可跑,但 URL/邮箱/标签会坏,生产要补专用模式。
  7. Porter 规则按顺序竞争,靠前者胜出,顺序比单条规则更重要。
  8. 词性标注是高质量还原的前提,本节先默认 NOUN,第 07 节补齐。
  9. NLTK 适合换组件,spaCy 适合生产,Transformer 流水线跳过经典预处理。
  10. 两大生产陷阱:可复现性漂移(钉版本+回归测试)、训练/推理不一致(预处理必须打包成同一函数)。

下一节,我们将把预处理后的 token 变成模型能算的数值——进入「词袋与 TF-IDF」,看文本如何第一次变成向量。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U