Фаза 05 · урок 01

Обработка текста — токенизация, стемминг, лемматизация

Цель урока: Каждая NLP-система начинается с одних и тех же трёх вопросов. Где начинается слово? Каков корень слова? Как рассматривать run , running , ran как одно и то же, когда это полезно, и как разные вещи, когда это не так?

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
NLP: от основ к продвинутым темам
Чтение
10 мин.
Проверено
Содержание урока
  1. Проблема
  2. Концепция
  3. Соберите это
  4. Шаг 1: regex-токенизатор слов
  5. Шаг 2: стеммер Портера (только шаг 1a)
  6. Шаг 3: лемматизатор на основе таблицы поиска
  7. Шаг 4: соедините их в конвейер
  8. Используйте это
  9. NLTK
  10. spaCy
  11. Когда выбирать что
  12. Два режима отказа, о которых никто не предупреждает
  13. Внедрите это
  14. Упражнения
  15. Ключевые термины
  16. Дополнительные материалы

Язык непрерывен. Модели дискретны. Предобработка — мост между ними.

Тип: Сборка Языки: Python Предварительные требования: Фаза 2 · 14 (Наивный Байес) Время: ~45 минут

Схема к уроку «Обработка текста — токенизация, стемминг, лемматизация»

Проблема

Модель не может прочитать «The cats were running.» Она читает целые числа.

Каждая NLP-система начинается с одних и тех же трёх вопросов. Где начинается слово? Каков корень слова? Как рассматривать run, running, ran как одно и то же, когда это полезно, и как разные вещи, когда это не так?

Ошибитесь в токенизации — и модель будет учиться на мусоре. Если ваш токенизатор считает don't одним токеном, а do n't — двумя, обучающее распределение расщепляется. Если стеммер сводит organization и organ к одному стему, тематическое моделирование погибает. Если лемматизатору нужен контекст части речи, а вы его не передаёте, глаголы будут обрабатываться как существительные.

В этом уроке мы с нуля построим три шага предобработки, а затем покажем, как ту же работу выполняют NLTK и spaCy, чтобы вы увидели компромиссы.

Концепция

Три операции. У каждой есть назначение и режим отказа.

Токенизация (tokenization) разбивает строку на токены. «Токен» намеренно расплывчатое слово: правильная гранулярность зависит от задачи. Уровень слов — для классического NLP. Субслова — для трансформеров. Символы — для языков без пробелов.

Стемминг (stemming) отсекает суффиксы по правилам. Быстро, агрессивно, глупо. running -> run. organization -> organ. Второй пример и есть режим отказа.

Лемматизация (lemmatization) приводит слово к его словарной форме с помощью знаний о грамматике. Медленнее, точнее, требует таблицы соответствий или морфологического анализатора. ran -> run (нужно знать, что ran — прошедшая форма run). better -> good (нужно знать сравнительные формы).

Практическое правило. Используйте стемминг, когда важна скорость и вы можете допустить шум (индексация поиска, грубая классификация). Используйте лемматизацию, когда важен смысл (вопросно-ответные системы, семантический поиск, всё, что увидит пользователь).

edit-distance

Соберите это

Шаг 1: regex-токенизатор слов

Самый простой полезный токенизатор разбивает текст по неалфавитно-цифровым символам, сохраняя пунктуацию как отдельные токены. Не идеально и не окончательно, но работает в одну строку.

import re

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

Три шаблона в порядке приоритета. Слова с необязательным внутренним апострофом (don't, it's). Чистые числа. Любой одиночный непробельный неалфавитно-цифровой символ как самостоятельный токен (пунктуация).

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

Обратите внимание на режимы отказа. 3pm разбивается на ['3', 'pm'], потому что мы чередуем последовательности букв и последовательности цифр. Для большинства задач достаточно. URL, email и хештеги все сломаются. Для production добавляйте шаблоны перед общими.

Шаг 2: стеммер Портера (только шаг 1a)

Полный алгоритм Портера содержит пять фаз правил. Один только шаг 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). Вот почему production-системы используют 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}

Недостающая часть — теггер частей речи (POS tagger). В фазе 5 · 07 (POS Tagging) мы построим такой теггер. Пока по умолчанию считайте всё NOUN и признавайте ограничение.

Используйте это

NLTK и spaCy поставляются с production-версиями. По нескольку строк в каждой.

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 требует, чтобы тег POS был переведён из схемы Penn Treebank NLTK в набор сокращений 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). Выполняются токенизация, POS-тегирование и лемматизация. На масштабе он быстрее NLTK. Точнее «из коробки». Компромисс в том, что нельзя легко заменять отдельные компоненты.

Когда выбирать что

Ситуация Выбор
Обучение, исследования, замена компонентов NLTK
Production, многоязычность, важна скорость spaCy
Конвейер трансформера (вы всё равно будете токенизировать токенизатором модели) Используйте tokenizers / transformers и пропустите классическую предобработку

Два режима отказа, о которых никто не предупреждает

Большинство учебников объясняет алгоритмы и на этом останавливается. Две вещи сломают настоящий конвейер предобработки, и о них почти никогда не говорят.

Дрейф воспроизводимости. NLTK и spaCy меняют поведение токенизации и лемматизатора между версиями. То, что в spaCy 2.x давало ['do', "n't"], в 3.x может дать ["don't"]. Ваша модель обучалась на одном распределении. Теперь инференс работает на другом. Точность тихо ухудшается, и никто не знает почему. Закрепляйте версии библиотек в requirements.txt. Напишите регрессионный тест предобработки, фиксирующий ожидаемую токенизацию 20 предложений-примеров. Запускайте его при каждом обновлении.

Несоответствие обучения и инференса. Обучите модель с агрессивной предобработкой (нижний регистр, удаление стоп-слов, стемминг), разверните её на необработанном пользовательском вводе — и наблюдайте обвал качества. Это самый частый сбой production NLP. Если вы выполняете предобработку при обучении, при инференсе должна выполняться идентичная функция. Поставляйте предобработку как функцию внутри пакета модели, а не как ячейку ноутбука, которую команда обслуживания переписывает.

Внедрите это

Многоразовый промпт, который помогает инженерам выбрать стратегию предобработки, не читая три учебника.

Сохраните как 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 оставались единичными токенами. Тест: tokenize("Visit https://example.com today.") должен вернуть один токен URL.
  2. Средне. Реализуйте шаг 1b алгоритма Портера. Если слово содержит гласную и оканчивается на ed или ing, удалите окончание. Обработайте правило двойной согласной (hopping -> hop, а не hopp).
  3. Сложно. Постройте лемматизатор, который использует WordNet как таблицу поиска, но возвращается к вашему стеммеру Портера, когда в WordNet нет записи. Измерьте точность на размеченном корпусе по сравнению с чистым WordNet и чистым Porter.

Ключевые термины

Термин Что обычно говорят Что это на самом деле означает
Токен Слово Любая единица, которую потребляет модель. Это может быть слово, субслово, символ или байт.
Стем Корень слова Результат удаления суффиксов по правилам. Не всегда настоящее слово.
Лемма Словарная форма Форма, которую вы бы искали в словаре. Для правильного вычисления нужен грамматический контекст.
POS-тег Часть речи Категория вроде NOUN, VERB, ADJ. Нужна для точной лемматизации.
Морфология Правила формы слова Как слово меняет форму в зависимости от времени, числа, падежа. От неё зависит лемматизация.

Дополнительные материалы


Источник: Text Processing — Tokenization, Stemming, Lemmatization Фаза 05 — NLP: от основ к продвинутым темам · 05.02 — Мешок слов, TF-IDF и представление текста · Полный каталог