Обработка естественного языка
Регулярные выражения и текст
Цели урока
- Строить regex для извлечения email, телефонов и URL из неструктурированного текста
- Понимать разницу NFC/NFD нормализации и когда она ломает производственные системы
- Различать overstemming и understemming, выбирать между stemmer-ами
- Применять lemmatization с правильным POS-тегом для получения словарной формы
Предварительные знания
Alan Turing и машина, которая понимает язык
1950 год. Alan Turing публикует "Computing Machinery and Intelligence". Центральный вопрос: "Can machines think?" - Тьюринг считает его бессмысленным и предлагает другой: может ли машина вести диалог так, что человек не отличит её от человека? Тест Тьюринга. Этот вопрос запустил всё: от regex-паттернов 1950-х до BERT и GPT-4. Preprocessing текста - regex, нормализация, stemming - это нижний уровень той же пирамиды, которую Тьюринг обозначил 70 лет назад.
2003 год. Спам составлял 45% всей email-трафика. Первые фильтры - чистый regex: `V[1!i]AGRA`, `fr33 m0n3y`. Спамеры обходили через замену букв цифрами. Фильтры адаптировались. Это гонка вооружений продолжается. В 2024 году Gmail блокирует 99.9% спама - первый рубеж обороны всё ещё regex, но за ним стоят BERT и собственные LLM Google.
- **Elasticsearch** использует Snowball stemming для 23 языков: запрос "running" находит "runs", "runner", "ran" - без дополнительных настроек
- **Gmail spam filter** - regex паттерны как первый быстрый уровень фильтрации перед ML-моделями
- **spaCy production pipeline** - Unicode NFKC нормализация + lemmatization для 60+ языков, основа NER в медицинских и юридических системах
- **Hugging Face tokenizers** - Unicode normalization перед BPE: без NFKC один и тот же символ может кодироваться по-разному
Регулярные выражения
2003 год. Gmail запускает спам-фильтр. Первый уровень защиты - не нейросеть. **Regex**: паттерны `V[1!i]AGRA`, `fr33 m0n3y`, `click h3re` блокируют миллионы писем до любого ML. Сегодня в spaCy pipeline, Hugging Face tokenizers, OpenAI text prep - везде regex на первом шаге. Одна строка паттерна заменяет десятки строк кода.
| Символ | Значение | Пример | Совпадения |
|---|---|---|---|
| . | Любой символ | c.t | cat, cot, c9t |
| * | 0 или более | ab*c | ac, abc, abbc |
| + | 1 или более | ab+c | abc, abbc (не ac) |
| ? | 0 или 1 | colou?r | color, colour |
| [] | Класс символов | [aeiou] | a, e, i, o, u |
| \d | Цифра | \d{3} | 123, 999, 007 |
| \w | Буква/цифра/_ | \w+ | hello, var_1 |
| | | ИЛИ | cat|dog | cat, dog |
| () | Группа | (ab)+ | ab, abab, ababab |
**Regex - выразительный, но хрупкий инструмент.** Regex для валидации email по RFC 5322 занимает ~6000 символов. Для NLP-задач regex отлично подходит для preprocessing (удаление URL, hashtag-ов, HTML-тегов), но не для понимания смысла. Знак `$` в regex-паттернах - анкор конца строки, не доллар. Для денежных сумм в тексте писать словами.
**regex101.com** - тестирование паттернов в реальном времени с подсветкой совпадений и объяснением каждого символа. Незаменим при отладке сложных выражений.
Что найдёт выражение `re.findall(r'\b\w{3}\b', 'The cat sat on a mat')`?
Unicode-нормализация
2010 год. Поисковик авиакомпании не находит рейсы в "Sao Paulo" - в базе город записан как "São Paulo". Один диакритический знак, тысячи несостоявшихся бронирований. Корень проблемы: Unicode позволяет записать один символ разными способами. `"café" == "cafe\u0301"` возвращает `False`, хотя строки выглядят одинаково. **Нормализация** - приведение к каноническому виду, чтобы одинаковые по смыслу строки стали одинаковыми по байтам.
| Форма | Название | Что делает | Когда использовать |
|---|---|---|---|
| NFC | Canonical Composition | Складывает символы (e + combining -> é) | По умолчанию для NLP |
| NFD | Canonical Decomposition | Раскладывает символы (é -> e + combining) | Для удаления акцентов |
| NFKC | Compatibility Composition | NFC + совместимость (fi -> fi) | Поиск, индексация |
| NFKD | Compatibility Decomposition | NFD + совместимость | Максимальная нормализация |
**Case folding** - это расширенный lowercasing. В немецком `"ss".lower()` = `"ss"`, но `"ss".casefold()` = `"ss"`. В турецком `"I".lower()` = `"i"` (без точки!), а не `"i"`. Метод `str.casefold()` в Python учитывает языковые особенности - критично для multilingual NLP.
**Не удалять акценты для всех языков!** В испанском "ano" (год) и "ano" (анатомия) - разные слова. В чешском "s" и "s" - разные буквы. Удаление акцентов безопасно только для задач, где recall важнее precision.
Почему `"cafe\u0301" == "café"` возвращает False, хотя строки выглядят одинаково?
Stemming
Elasticsearch индексирует миллиарды документов. Запрос "running shoes" должен находить и "runs shoes" и "runner gear". **Stemming** - алгоритмическое отрезание суффиксов для приведения слова к основе (стему). Быстро, интерпретируемо, не требует GPU. Martin Porter опубликовал первый алгоритм в 1980 году - он до сих пор в ядре Elasticsearch и Lucene.
**Stemming - это грубый алгоритм.** Результат - не обязательно реальное слово. Porter превращает "university" и "universe" в одинаковый стем "univers" (overstemming), а "organization" -> "organ" (тоже не слово). Для задач, где важна точность смысла, stemming не подходит.
| Stemmer | Языки | Скорость | Качество |
|---|---|---|---|
| Porter | Только английский | Очень быстрый | Много ошибок |
| Snowball | 23 языка (вкл. русский) | Быстрый | Лучше Porter |
| Lancaster | Только английский | Быстрый | Самый агрессивный |
**Когда stemming оправдан:** поисковые системы, information retrieval, где важен recall (найти ВСЁ релевантное), а не precision. Elasticsearch использует Snowball stemming как анализатор для расширения поисковых запросов.
Porter Stemmer превращает "university" и "universe" в одинаковый стем "univers". Как называется эта ошибка?
Lemmatization
Stemming отрезает суффиксы механически. **Lemmatization** идёт другим путём - словарь и морфологический анализ, чтобы найти **лемму** (словарную форму). "better" -> "good", "mice" -> "mouse", "went" -> "go". Результат всегда реальное слово. spaCy, Stanza, NLTK WordNet - production инструменты, которые это делают. Google Assistant и Siri используют lemmatization на этапе intent parsing.
**Лемма** - каноническая (словарная) форма слова. Для существительных - именительный падеж единственного числа ("mice" -> "mouse"). Для глаголов - инфинитив ("went" -> "go"). Для прилагательных - положительная степень ("better" -> "good").
| Критерий | Stemming | Lemmatization |
|---|---|---|
| Метод | Алгоритм отрезания суффиксов | Словарь + морфологический анализ |
| Скорость | Очень быстрый (~1M слов/сек) | Медленнее (нужен словарь/модель) |
| Результат | Не обязательно слово ("univers") | Всегда реальное слово ("university") |
| POS нужен? | Нет | Да ("running" -> "run" только если глагол) |
| Неправильные формы | Не справляется ("went" -> "went") | Справляется ("went" -> "go") |
| Применение | Поиск, IR | Chatbots, QA, генерация |
**Правило большого пальца:** для поиска и индексации - stemming (скорость важнее точности). Для понимания смысла (intent detection, QA, sentiment analysis) - lemmatization. ChatGPT не использует stemming - его токенизатор BPE работает принципиально иначе, без словаря.
Stemming достаточно для любой NLP-задачи - lemmatization только медленнее, а результат тот же
Stemming и lemmatization дают РАЗНЫЕ результаты. Stemming ломает слова ('university' -> 'univers'), путает несвязанные слова (overstemming) и не справляется с неправильными формами ('went' не станет 'go'). Lemmatization сохраняет смысл, но требует знания части речи.
Stemming - это эвристика на основе правил отрезания суффиксов. Lemmatization - это поиск в лингвистическом словаре. Для задач, где важен смысл (sentiment analysis, intent detection, QA), stemming генерирует слишком много ошибок.
Почему `WordNetLemmatizer().lemmatize('running')` возвращает 'running', а не 'run'?
Ключевые идеи
- **Regex** - первый рубеж NLP pipeline: очистка HTML, URL, mentions. Одна строка паттерна = десятки строк кода. Gmail, Elasticsearch, spaCy начинают с этого
- **Unicode-нормализация** (NFC/NFD) - «Sao Paulo» vs «São Paulo» - это реальный баг реальной авиакомпании. `str.casefold()` вместо `str.lower()` для multilingual
- **Stemming** - быстрый и грубый: отрезает суффиксы, не гарантирует реальное слово. Overstemming ("university" = "universe") - цена скорости
- **Lemmatization** - точный, но медленный: словарь + POS = "went" -> "go". Google Assistant, Siri используют этот подход для intent parsing
- **Выбор инструмента**: поиск и recall -> stemming; понимание смысла и precision -> lemmatization. Нейросетевые токенизаторы (BPE) обходят оба подхода для LLM
Связанные темы
Текстовый preprocessing - фундамент для следующих шагов NLP:
- Введение в NLP — Preprocessing - часть NLP pipeline из предыдущего урока
- Bag of Words и TF-IDF — Нормализованные токены - вход для векторных представлений
Вопросы для размышления
- В каких случаях stemming может навредить результату поиска (hint: overstemming)?
- Почему spaCy-лемматизация работает лучше, чем WordNet без указания POS?
- Если строится поисковая система для турецкого языка - какие особенности Unicode/case folding нужно учесть?
Связанные уроки
- nlp-01 — NLP pipeline и понятие токенизации из вводного урока
- nlp-03 — Нормализованные токены - вход для TF-IDF и Bag of Words
- ir-01 — Stemming и regex используются в поисковых индексах Elasticsearch
- aie-03-llm-fundamentals — LLM токенизация строится поверх тех же принципов нормализации
- dl-01 — Препроцессинг текста предшествует эмбеддингам и нейросетевым моделям
- fl-05-regex