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

un

guest
1 / ?
back to lessons

歡迎

歡迎來到一堂實踐的自然語言處理課程。

你將從頭開始構建一個有效的英文詞幹提取器:一種算法,將單詞剝離到它們的根形式。

到最後,你將擁有一個真實的、經過測試的算法,可以轉換單詞,如 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(如來自 hopinghop),加回 ehope


對於無聲 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+ 個字符時才剝離。

更新你的 `stem` 函數,包括新的後綴、雙輔音清理 & 無聲 e 還原。打印結果: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` 函數 & 編寫 `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' 可能共享詞幹:檢查你的詞幹提取器是否處理這個(如果它分組它們是可以的;記錄行為)。

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 還原

- 單元測試、集成測試 & 功能測試


血統

你的詞幹提取器來自於從 1955 年 Zellig Harris 開始的工作線:


- Harris (1955):發現詞素邊界顯示為統計信號(後繼種類)

- Lovins (1968):首個發布的詞幹提取算法,294 個後綴規則

- Porter (1980):簡化為 ~60 個規則分為 5 個步驟,成為幾十年的標準

- Snowball (2001):Porter 的框架推廣到多種語言

- 你的詞幹提取器(今天):12 個規則,相同的核心原理


接下來你可以做什麼

- 實現完整的 Porter 算法(它已被很好地記錄 & 是一個很好的練習)

- 將你的詞幹提取器移植到 C 以獲得 100 倍的速度改進

- 構建一個簡單的搜索引擎,使用你的詞幹提取器來索引 & 查詢文本文件

- 將你的詞幹提取器的輸出與 NLTK 的 PorterStemmer 比較以測量準確性


你今天編寫的代碼是運行在地球上每個搜索引擎內部的相同基本操作。對於一天的工作來說不錯。

詞幹提取器血統:Harris 1955 到 Snowball 2001

反思你構建了什麼。你學到的最令人驚訝的事是什麼?如果你要改進你的詞幹提取器,你會添加或改變什麼?