English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

访客
1 / ?
返回课程列表

欢迎

欢迎来到实践性NLP课程。

你将从零开始构建一个实际可用的英文词干提取器:一种将单词化简到其根形式的算法。

到课程结束,你将拥有一个真实、经过测试的算法,可以转换单词如 runningrunhappinesshappi、& organizationalorgan

你还将为你的词干提取器编写单元测试、集成测试和功能测试:因为未经测试的算法只是一个猜测。

什么是词干提取?

问题

搜索引擎面临一个根本问题:用户搜索"running",但文档包含"run""runs""runner"。这些都是同一个概念:但它们是不同的字符串。


词干提取将屈折词还原为通用基础形式(词干)。它不需要是真实单词:它只需要保持一致。


单词词干
runningrun
runsrun
runnerrunner
happinesshappi
happilyhappi
happyhappi

注意happi不是真实的英文单词。没关系。词干提取的目的是分组,而不是含义。只要happinesshappily&happy都化简为同一个词干,搜索和检索就会改进。

词干提取:多种形式化简为一个词干,改进搜索结果

用你自己的话解释为什么使用词干提取的搜索引擎相比只匹配精确字符串的引擎会返回更好的结果。给出一个具体例子。

Zellig Harris与分布分析

计算词干提取的起源

1955年,语言学家Zellig Harris发表了《From Phoneme to Morpheme》,描述了一种在单词中寻找有意义单位(形素)边界的方法。


他的洞察是分布性的:如果你查看大量英文单词的语料库,词干与后缀之间的边界会表现为统计信号。


后继多样性方法

对于任何单词前缀,计算在语料库中有多少不同的字符可以跟在其后。Harris称这为后继多样性


考虑前缀"work"在包含以下单词的语料库中:worked, worker, working, works, workshop


前缀后面跟着后继多样性
wo1
wor1
work1
worke, i, s, sh4
worked, r2

"work"之后,四个不同的字符可以跟随:这是多样性的激增。那个激增标记了一个形素边界。词干是work,其后的一切都是后缀。


这在1955年是革命性的。没有语言规则,没有字典:只是计数。Harris证明了语言结构通过分布显现自身。

Harris后继多样性:在'work'处的激增标记形素边界

理解后继多样性

Harris的方法适用于任何语言。你不需要了解语法:统计会揭示形素边界。


在实践中,纯后继多样性需要大型语料库和仔细的峰值检测。后来的研究者:Lovins(1968年)、Porter(1980年):将方法简化为基于规则的后缀去除:他们不是从语料库计算后继多样性,而是直接编码后缀规则。


今天你将构建一个受Harris洞察启发的基于规则的后缀去除工具。你将明确定义后缀,然后从单词中去除它们。这是大多数生产词干提取器的工作方式。

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
编写`stem`函数。它应该从单词中去除-ing、-ed、-ly或-s(第一个匹配,按顺序),但仅当剩余词干有3个或更多字符时。通过打印stem('running')、stem('walked')、stem('quickly')&stem('cats')来测试它。

处理边界情况

让词干提取器更聪明

你的基本去除器有一个问题:runningrunn&hopinghop。我们需要两个改进:


1. 双辅音清理:如果去除-ing或-ed后留下末尾的双辅音(如runn),去除最后一个字母→run

2. 无声e恢复:如果去除-ing后的词干以辅音结尾(不是元音),&原始可能有无声e(如hope来自hoping),把e加回→hope


对于无声e规则,保持简单:如果去除-ing后,词干是3+个字符,以辅音结尾,&倒数第二个字符是元音(如hopmaktak的模式),加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+字符时才去除。

用新后缀、双辅音清理&无声e恢复更新你的`stem`函数。打印结果:stem('running')、stem('hoping')、stem('happiness')、stem('organization')、stem('readable')。

-ies & -ier 规则

更多形态学

英语有另一个常见模式:以-y结尾的单词在屈折时变为-ies-ied-ier


单词应该提取到
babiesbabi
carriedcarri
earlierearli
fliesfli
studiedstudi

在-s&-ed检查之前添加这些规则:

- -ies → 去除&添加i (babies → babi)

- -ied → 去除&添加i (carried → carri)

- -ier → 去除&添加i (earlier → earli)


相同的最小词干长度规则:只有在结果是3+字符时才转换。

将-ies、-ied&-ier规则添加到你的stem函数。打印结果:stem('babies')、stem('carried')、stem('earlier')、stem('happiness')、stem('running')。

为什么测试?

测试不是可选的

你有一个有效的词干提取器。你怎么知道它真的有效?现在,你在用手运行几个例子。这是无法扩展的。


专业软件使用三个级别的测试:


单元测试:在隔离状态下用已知输入和预期输出测试一个函数。快速、数量众多、具体。


集成测试:测试多个组件是否一起工作。对于词干提取器,这意味着用一批单词进行测试并验证结果是一致的。


功能测试:从外部测试系统,就像用户一样。对于词干提取器,这意味着供给它真实文本并验证输出对真实用例(如搜索)有意义。


你将编写全部三种。

三个测试级别:单元、集成和功能测试金字塔

编写单元测试

单元测试

编写一个叫做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
包含你完整的`stem`函数AND编写`run_unit_tests`,至少有15个测试用例覆盖上面所有7个类别。在最后调用`run_unit_tests()`。

编写集成测试

集成测试

单元测试验证单个输入。集成测试验证组件是否一起正确工作。


对于词干提取器,一个关键的集成属性是一致性:如果你对同一单词进行两次词干提取,你得到相同的结果。应该分组在一起的单词应该产生相同的词干。


编写一个叫做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
    ...
包含你的`stem`函数&编写`run_integration_tests`,所有三个测试类别。在最后调用它。

编写功能测试

功能测试

功能测试验证系统对其预期用例的工作。你的词干提取器存在于改进搜索:所以测试那个。


编写一个叫做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
    ...
包含你的`stem`函数&编写`run_functional_tests`,所有三个测试类别。在最后调用它。

你构建的内容

你构建的内容

你实现了一个有效的英文词干提取器,包含:

- 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进行比较以测量精确性


你今天编写的代码是在地球上每个搜索引擎内运行的相同基本操作。对一天工作来说不错。

词干提取器血统:Harris 1955到Snowball 2001

反思你构建的内容。你学到的最令人惊讶的事情是什么?如果你要改进你的词干提取器,你会添加或改变什么?