欢迎
欢迎来到实践性NLP课程。
你将从零开始构建一个实际可用的英文词干提取器:一种将单词化简到其根形式的算法。
到课程结束,你将拥有一个真实、经过测试的算法,可以转换单词如 running → run、happiness → happi、& organizational → organ。
你还将为你的词干提取器编写单元测试、集成测试和功能测试:因为未经测试的算法只是一个猜测。
什么是词干提取?
问题
搜索引擎面临一个根本问题:用户搜索"running",但文档包含"run"或"runs"或"runner"。这些都是同一个概念:但它们是不同的字符串。
词干提取将屈折词还原为通用基础形式(词干)。它不需要是真实单词:它只需要保持一致。
| 单词 | 词干 |
|---|---|
| running | run |
| runs | run |
| runner | runner |
| happiness | happi |
| happily | happi |
| happy | happi |
注意happi不是真实的英文单词。没关系。词干提取的目的是分组,而不是含义。只要happiness、happily&happy都化简为同一个词干,搜索和检索就会改进。
Zellig Harris与分布分析
计算词干提取的起源
1955年,语言学家Zellig Harris发表了《From Phoneme to Morpheme》,描述了一种在单词中寻找有意义单位(形素)边界的方法。
他的洞察是分布性的:如果你查看大量英文单词的语料库,词干与后缀之间的边界会表现为统计信号。
后继多样性方法
对于任何单词前缀,计算在语料库中有多少不同的字符可以跟在其后。Harris称这为后继多样性。
考虑前缀"work"在包含以下单词的语料库中:worked, worker, working, works, workshop。
| 前缀 | 后面跟着 | 后继多样性 |
|---|---|---|
| w | o | 1 |
| wo | r | 1 |
| wor | k | 1 |
| work | e, i, s, sh | 4 |
| worke | d, r | 2 |
在"work"之后,四个不同的字符可以跟随:这是多样性的激增。那个激增标记了一个形素边界。词干是work,其后的一切都是后缀。
这在1955年是革命性的。没有语言规则,没有字典:只是计数。Harris证明了语言结构通过分布显现自身。
理解后继多样性
Harris的方法适用于任何语言。你不需要了解语法:统计会揭示形素边界。
在实践中,纯后继多样性需要大型语料库和仔细的峰值检测。后来的研究者:Lovins(1968年)、Porter(1980年):将方法简化为基于规则的后缀去除:他们不是从语料库计算后继多样性,而是直接编码后缀规则。
今天你将构建一个受Harris洞察启发的基于规则的后缀去除工具。你将明确定义后缀,然后从单词中去除它们。这是大多数生产词干提取器的工作方式。
你的第一个后缀去除器
让我们编码
从简单开始。编写一个叫做stem的函数,它将单词去除这些后缀(按顺序):
1. -ing (running → runn)
2. -ed (walked → walk)
3. -ly (quickly → quick)
4. -s (cats → cat)
规则:
- 首先将单词转换为小写
- 只去除一个后缀(顺序中的第一个匹配)
- 只有在剩余词干至少3个字符时才去除
- 返回词干
示例:
def stem(word):
word = word.lower()
# your suffix stripping logic here
return word
处理边界情况
让词干提取器更聪明
你的基本去除器有一个问题:running → runn&hoping → hop。我们需要两个改进:
1. 双辅音清理:如果去除-ing或-ed后留下末尾的双辅音(如runn),去除最后一个字母→run
2. 无声e恢复:如果去除-ing后的词干以辅音结尾(不是元音),&原始可能有无声e(如hope来自hoping),把e加回→hope
对于无声e规则,保持简单:如果去除-ing后,词干是3+个字符,以辅音结尾,&倒数第二个字符是元音(如hop、mak、tak的模式),加e回。
同样添加这些新后缀(在-ing、-ed、-ly、-s之前检查它们):
5. -tion (organization → organiza)
6. -ness (happiness → happi)
7. -ment (movement → move)
8. -able (readable → read)
9. -ible (sensible → sens)
更新的后缀优先级:-tion、-ness、-ment、-able、-ible、-ing、-ed、-ly、-s
保持最小词干长度规则:只有在剩余词干是3+字符时才去除。
-ies & -ier 规则
更多形态学
英语有另一个常见模式:以-y结尾的单词在屈折时变为-ies、-ied或-ier。
| 单词 | 应该提取到 |
|---|---|
| babies | babi |
| carried | carri |
| earlier | earli |
| flies | fli |
| studied | studi |
在-s&-ed检查之前添加这些规则:
- -ies → 去除&添加i (babies → babi)
- -ied → 去除&添加i (carried → carri)
- -ier → 去除&添加i (earlier → earli)
相同的最小词干长度规则:只有在结果是3+字符时才转换。
为什么测试?
测试不是可选的
你有一个有效的词干提取器。你怎么知道它真的有效?现在,你在用手运行几个例子。这是无法扩展的。
专业软件使用三个级别的测试:
单元测试:在隔离状态下用已知输入和预期输出测试一个函数。快速、数量众多、具体。
集成测试:测试多个组件是否一起工作。对于词干提取器,这意味着用一批单词进行测试并验证结果是一致的。
功能测试:从外部测试系统,就像用户一样。对于词干提取器,这意味着供给它真实文本并验证输出对真实用例(如搜索)有意义。
你将编写全部三种。
编写单元测试
单元测试
编写一个叫做run_unit_tests的函数,用至少15个测试用例测试你的stem函数,覆盖:
1. 基本后缀去除:以-ing、-ed、-ly、-s结尾的单词
2. 复杂后缀:-tion、-ness、-ment、-able、-ible
3. Y屈折:-ies、-ied、-ier
4. 边界情况:不应去除的短单词、没有后缀的单词、已经提取的单词
5. 双辅音清理:running → run、sitting → sit
6. 无声e恢复:hoping → hope
7. 大小写不敏感性:大写输入应被小写化
按这样的方式构建你的测试:
def run_unit_tests():
tests = [
('running', 'run'),
('cats', 'cat'),
# ... at least 15 test cases
]
passed = 0
failed = 0
for word, expected in tests:
result = stem(word)
if result == expected:
passed += 1
else:
failed += 1
print(f'FAIL: stem({word}) = {result}, expected {expected}')
print(f'{passed}/{passed + failed} unit tests passed')
return failed == 0
编写集成测试
集成测试
单元测试验证单个输入。集成测试验证组件是否一起正确工作。
对于词干提取器,一个关键的集成属性是一致性:如果你对同一单词进行两次词干提取,你得到相同的结果。应该分组在一起的单词应该产生相同的词干。
编写一个叫做run_integration_tests的函数,测试:
1. 幂等性:对已经提取的单词进行词干提取应返回相同词干。对所有单词,stem(stem(word)) == stem(word)。
2. 分组:应共享词干的单词实际上这样做。测试至少3个单词族(例如,run/runs/running/runner应该都共享一个词干)。
3. 批处理:处理20+个单词的列表并验证无崩溃、无空字符串、无None值。
def run_integration_tests():
# Test 1: idempotency
# Test 2: word family grouping
# Test 3: batch stability
...
编写功能测试
功能测试
功能测试验证系统对其预期用例的工作。你的词干提取器存在于改进搜索:所以测试那个。
编写一个叫做run_functional_tests的函数,:
1. 搜索模拟:给定文档字符串列表和查询单词,对文档和查询进行词干提取,然后检查词干化查询术语是否出现在词干化文档中。测试搜索'running'是否找到包含'run'和'runner'的文档。
2. 精确性检查:验证词干提取不会错误地分组无关单词。'university'和'universe'可能共享词干:检查你的词干提取器是否处理这个(如果分组它们是OK的;记录行为)。
3. 真实文本处理:对真实英文段落中的每个单词进行词干提取。验证输出是合理的:没有空字符串、无崩溃、输出与输入的单词数相同。
def run_functional_tests():
# Test 1: search finds related documents
# Test 2: precision: check over-stemming
# Test 3: real paragraph processing
...
你构建的内容
你构建的内容
你实现了一个有效的英文词干提取器,包含:
- 12个后缀规则(-tion、-ness、-ment、-able、-ible、-ies、-ied、-ier、-ing、-ed、-ly、-s)
- 双辅音清理
- 无声e恢复
- 单元测试、集成测试&功能测试
血统
你的词干提取器继承自始于Zellig Harris在1955年的一系列工作:
- Harris (1955):发现形素边界表现为统计信号(后继多样性)
- Lovins (1968):首个发布词干提取算法,294个后缀规则
- Porter (1980):简化为~60个规则分5个步骤,成为几十年的标准
- Snowball (2001):Porter的框架推广到多种语言
- 你的词干提取器(今天):12个规则,相同的核心原则
你接下来可以做什么
- 实现完整Porter算法(文档齐全&一个很好的练习)
- 将你的词干提取器移植到C获得100倍速度改进
- 构建一个简单搜索引擎,使用你的词干提取器索引&查询文本文件
- 将你的词干提取器输出与NLTK的PorterStemmer进行比较以测量精确性
你今天编写的代码是在地球上每个搜索引擎内运行的相同基本操作。对一天工作来说不错。