Фаза 05 · урок 01
Обработка текста — токенизация, стемминг, лемматизация
Цель урока: Каждая NLP-система начинается с одних и тех же трёх вопросов. Где начинается слово? Каков корень слова? Как рассматривать run , running , ran как одно и то же, когда это полезно, и как разные вещи, когда это не так?
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Проблема
- Концепция
- Соберите это
- Шаг 1: regex-токенизатор слов
- Шаг 2: стеммер Портера (только шаг 1a)
- Шаг 3: лемматизатор на основе таблицы поиска
- Шаг 4: соедините их в конвейер
- Используйте это
- NLTK
- spaCy
- Когда выбирать что
- Два режима отказа, о которых никто не предупреждает
- Внедрите это
- Упражнения
- Ключевые термины
- Дополнительные материалы
Язык непрерывен. Модели дискретны. Предобработка — мост между ними.
Тип: Сборка Языки: 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.
Упражнения
- Легко. Расширьте
tokenize, чтобы URL оставались единичными токенами. Тест:tokenize("Visit https://example.com today.")должен вернуть один токен URL. - Средне. Реализуйте шаг 1b алгоритма Портера. Если слово содержит гласную и оканчивается на
edилиing, удалите окончание. Обработайте правило двойной согласной (hopping -> hop, а неhopp). - Сложно. Постройте лемматизатор, который использует WordNet как таблицу поиска, но возвращается к вашему стеммеру Портера, когда в WordNet нет записи. Измерьте точность на размеченном корпусе по сравнению с чистым WordNet и чистым Porter.
Ключевые термины
| Термин | Что обычно говорят | Что это на самом деле означает |
|---|---|---|
| Токен | Слово | Любая единица, которую потребляет модель. Это может быть слово, субслово, символ или байт. |
| Стем | Корень слова | Результат удаления суффиксов по правилам. Не всегда настоящее слово. |
| Лемма | Словарная форма | Форма, которую вы бы искали в словаре. Для правильного вычисления нужен грамматический контекст. |
| POS-тег | Часть речи | Категория вроде NOUN, VERB, ADJ. Нужна для точной лемматизации. |
| Морфология | Правила формы слова | Как слово меняет форму в зависимости от времени, числа, падежа. От неё зависит лемматизация. |
Дополнительные материалы
- Porter, M. F. (1980). An algorithm for suffix stripping — оригинальная статья: пять страниц и всё ещё самое ясное объяснение.
- spaCy 101 — linguistic features — как устроен настоящий конвейер.
- NLTK book, chapter 3 — крайние случаи токенизации, о которых вы ещё не думали.
Источник: Text Processing — Tokenization, Stemming, Lemmatization Фаза 05 — NLP: от основ к продвинутым темам · 05.02 — Мешок слов, TF-IDF и представление текста · Полный каталог