02 / 37 · Основы работы с текстом
Текст как данные
Что такое строка на самом деле (байты, кодовые точки, формы нормализации), как её режут на единицы, какие шаги очистки сжимают словарь и почему правильная предобработка зависит от задачи.
На этой странице 10
Каждая модель этого курса потребляет числа. Текст приходит строкой, и первая задача — решить, какие куски этой строки станут единицами, которые видит модель. Решение выглядит канцелярским, но оно фиксирует словарь, разреженность каждого следующего представления и некоторые ошибки, которые не устранит никакое обучение. Эта заметка проходит путь от сырых байтов до списка очищенных единиц и заканчивается вопросом, с которого начинается следующая заметка: что делать, когда единицы — целые слова, а словарь разрастается.
Обозначения. Документ — одна строка. Токен — одна вырезанная из неё единица. Словарь — множество различных токенов по всему корпусу, — его размер. Через обозначаем число вхождений токена в .
Вся заметка на одном фрагменте — от байтов файла до единиц, которые посчитала бы модель мешка слов (bag of words):
От байтов к очищенным единицам
Что такое строка#
Текстовый файл — последовательность байтов. Кодировка (encoding) отображает байты в кодовые точки (code points) — целые числа, которые Unicode присваивает символам. UTF-8 имеет переменную ширину: латинские буквы ASCII занимают один байт, большинство кириллических — два, большинство иероглифов CJK — три, эмодзи — четыре. Поэтому «длина в байтах», «длина в кодовых точках» и «длина в том, что читатель воспринимает как символы» — три разных числа.
Третье число отличается от второго, потому что Unicode допускает несколько способов записать один и тот же видимый знак. Буква é — это либо одна кодовая точка U+00E9, либо пара U+0065 (e) и U+0301 (комбинируемый знак акута, combining acute accent). Поэтому две строки, которые выглядят одинаково, могут оказаться неравными при сравнении, считаться разными словами и получить разные векторы. Формы нормализации Unicode (normalization forms) фиксируют канонический выбор:
| Форма | Что делает | Типичное применение |
|---|---|---|
| NFC | Собирает символ в готовую кодовую точку (precomposed), если она есть | Хранение, сравнение |
| NFD | Раскладывает на базовый символ и комбинируемые знаки | Удаление диакритики |
| NFKC | NFC плюс совместимое свёртывание (compatibility folding): fi → fi, полноширинное A → A, верхний индекс ² → 2 | Поиск, обучение токенизатора |
| NFKD | NFD плюс совместимое свёртывание | Поиск без учёта диакритики |
K-формы нормализации теряют информацию: ² и 2 становятся одним токеном, что правильно для поиска и неправильно для формулы. Это первый пример правила, которое проходит через всю заметку: шаг очистки — решение о модели, а не бесплатное упрощение. Современный токенизатор подслов (subword) нормализует текст прежде всего остального, и два семейства из следующей заметки делают это по-разному: SentencePiece по умолчанию применяет NFKC, а токенизатор WordPiece у BERT раскладывает текст по NFD, удаляет комбинируемые знаки (strip_accents) и приводит его к нижнему регистру, так что Café и cafe становятся одним токеном.
Сегментация: как разрезать строку на единицы#
Простейшие единицы — слова. В ходу два определения «слова», и они расходятся в обращении с пунктуацией:
import re
from nltk.tokenize import wordpunct_tokenize
text = "What TV shows or books help you read people's body language?"
re.findall(r"\w+", text)
# ['What', 'TV', 'shows', 'or', 'books', 'help', 'you', 'read',
# 'people', 's', 'body', 'language']
wordpunct_tokenize(text)
# ['What', 'TV', 'shows', 'or', 'books', 'help', 'you', 'read',
# 'people', "'", 's', 'body', 'language', '?']
Регулярное выражение \w+ оставляет непрерывные последовательности букв, цифр и подчёркиваний и отбрасывает всё остальное, поэтому вопросительный знак исчезает, а притяжательная форма распадается на people и s. Токенизатор, учитывающий пунктуацию, оставляет ' и ? отдельными токенами. Ни один вариант не ошибочен. Для классификатора тем вопросительный знак не несёт сигнала; для языковой модели, которая должна порождать вопросы, это самый важный токен предложения.
Кроме того, сегментация на слова предполагает, что слова разделены пробелами. Китайский, японский и тайский так не пишутся, немецкий и финский склеивают слова в длинные составные, а #deeplearning — один токен в твите и два слова для читателя. Желание хорошо обрабатывать такие случаи — один из мотивов методов подслов из следующей заметки.
Очистка: информация в обмен на меньший словарь#
После сегментации у нас есть список токенов для каждого документа. Несколько преобразований отображают много форм токена в одну. Каждое из них уменьшает , объединяет статистику форм, которые, вероятно, значат одно и то же, и выбрасывает то, что их различало.
Приведение к нижнему регистру. Cat, cat и CAT становятся одним токеном. Теряется сигнал, который отличает Apple от apple и US от us.
Удаление стоп-слов. Служебные слова (the, is, of, not, …) выбрасываются по фиксированному списку. В модели мешка слов они доминируют в счётчиках, не неся информации о теме. Стандартный английский список в NLTK содержит 198 слов, включая not, no и nor. Их удаление превращает «not good» в «good» — известный способ сломать классификатор тональности.
Стемминг. Процедура на правилах отрезает аффиксы: studies → studi, running → run, кошками → кошк. Результат не обязан быть словом. Стемминг быстр и сливает формы агрессивно, в том числе те, которые должны остаться разными (у universe и university общая основа Портера univers).
Лемматизация. Каждая форма отображается в свою словарную форму — лемму: studies → study, went → go, better → good, дети → ребёнок. Для этого нужны решение о части речи и морфологический словарь, поэтому лемматизация медленнее и зависит от языка, зато результат — настоящее слово, а неправильные формы обрабатываются верно.
Сильнее всего два подхода различаются на неправильной морфологии и по стоимости. Лемматизатору нужна часть речи: WordNet-лемматизатор из NLTK считает каждое слово существительным, если не указано иное, поэтому went и better возвращаются без изменений, пока его не вызовут с pos="v" и pos="a". В таблице дана лемма для правильной части речи:
| Форма | Основа Портера | Лемма WordNet |
|---|---|---|
studies | studi | study |
went | went | go |
better | better | good |
people | peopl | people |
Стемминг — несколько строковых операций на токен. Лемматизация — поиск по словарю, которому иногда предшествует разметка частей речи, а за ним следует морфологический анализ, поэтому она медленнее на постоянный множитель, зависящий от реализации. В конвейере, который один раз обрабатывает большой корпус, обе стоимости пренебрежимо малы по сравнению с обучением модели; в сервисе, который должен токенизировать каждый запрос, разница может оказаться существенной.
Разобранный пример#
Возьмём предложение выше и прогоним его через обычный конвейер: нижний регистр, разбиение по \w+, удаление стоп-слов, затем лемматизация или стемминг.
| Шаг | Результат |
|---|---|
| Нижний регистр + разбиение | what tv shows or books help you read people s body language |
| Удаление стоп-слов | tv shows books help read people body language |
| Лемматизация | tv show book help read people body language |
| Стемминг | tv show book help read peopl bodi languag |
Двенадцать токенов превратились в восемь; shows и books свелись к единственному числу, так что документ со словом «book» и документ со словом «books» теперь делят один признак.
На целом корпусе эти решения двигают размер словаря. 120 000 обучающих текстов AG News, разбитых правилом scikit-learn по умолчанию (последовательности из двух и более словесных символов), содержат 88 057 различных типов слов (word types); одно лишь приведение к нижнему регистру сокращает их число до 65 006. Если отбросить типы, которые встречаются меньше чем в четырёх документах (min_df = 4), остаётся 29 350: 55% словаря в нижнем регистре, 35 656 типов, встречаются не более чем в трёх документах — слишком редко, чтобы модель чему-то на них научилась. Ни в одном из трёх подсчётов стоп-слова не удаляются и лемматизация не применяется.
Почему предобработка зависит от задачи#
Конвейер выше хорош для одного семейства моделей — классификаторов на мешке слов, где каждый различный токен — отдельный признак, а редкие признаки — шум. В других задачах те же шаги уничтожают сигнал:
- Тональность. Слова отрицания — стоп-слова. «not done» и «done» схлопываются в один и тот же мешок. Заметка о TF-IDF показывает стандартное частичное решение — подсчёт биграмм.
- Распознавание именованных сущностей. Заглавная буква — самый сильный отдельный признак имени. Заметка о NER использует модель с учётом регистра (cased) именно поэтому.
- Языковое моделирование и генерация. Модель должна воспроизводить все различия, которые нужны её выходу, — пунктуацию, регистр, словоизменение, — поэтому удалять их нельзя. Байтовый токенизатор подслов сохраняет каждый символ и оставляет модели самой выучить, какие вариации важны. Не всякий токенизатор подслов обратим: uncased WordPiece у BERT приводит к нижнему регистру и удаляет диакритику, а токенизатор, в базовом словаре которого нет какого-то символа, отображает его в неизвестный токен.
- Поиск. Стемминг повышает полноту (
runнаходитrunning) ценой точности; выгоден ли такой обмен, зависит от того, что ищут пользователи.
Практическое правило: чистить то, что доказуемо вредит (HTML-теги, повторяющиеся пробелы, артефакты кодировки, точные дубликаты), и сохранять всё, что нужно читателю, чтобы понять текст, — если только эксперимент не показывает, что без этого модель работает лучше.
Ограничения#
- Нормализация и приведение к нижнему регистру необратимы. Если приложение должно показывать текст пользователю, оригинал нужно хранить рядом с очищенной версией.
- Списки стоп-слов, стеммеры и лемматизаторы свои для каждого языка, и их качество сильно различается между языками. Морфологически богатым языкам (русскому, турецкому, финскому) лемматизация нужна гораздо больше, чем английскому, а инструменты для них хуже.
- Единицы-слова оставляют открытыми две проблемы: любой токен, которого не было в обучающем словаре, при тестировании неизвестен, а большой словарь означает большую таблицу эмбеддингов и длинный хвост форм, встреченных по одному разу. Следующая заметка посвящена этим двум проблемам.
Что нужно запомнить#
- Текстовый файл хранит байты; декодирование по кодировке превращает их в строку кодовых точек (
strв Python); то, что читатель воспринимает как один символ, — графемный кластер — может состоять из нескольких кодовых точек. Форма нормализации выбирает одну каноническую запись для визуально одинаковых строк. Сравнивать и считать можно только нормализованный текст. - Сегментация определяет единицы. Регулярное выражение и токенизатор с учётом пунктуации расходятся в обращении с пунктуацией и клитиками; правильный выбор зависит от того, несёт ли пунктуация сигнал для задачи.
- Нижний регистр, удаление стоп-слов, стемминг и лемматизация уменьшают словарь, сливая формы, и каждое слияние выбрасывает различие, которое нужно какой-то задаче.
- Стемминг основан на правилах и быстр, лемматизация основана на словаре и верна на неправильных формах; для морфологически богатых языков лемматизация предпочтительнее, если её стоимость приемлема.
- Предобработка — часть модели. Её нужно выбирать под задачу и измерять её эффект.
Ноутбук#
Сопровождающий ноутбук — week01_word_embeddings.ipynb. Номера ячеек считают все ячейки, включая текстовые, начиная с 1. Приведённые числа взяты из прогона, сохранённого в курсе; у ячеек, изменённых с тех пор под текущие версии библиотек, сохранённого выхода нет.
- Ячейки 4–6 загружают файл вопросов Quora, по одному на строку, и печатают строку 50 — пример, использованный выше.
- Ячейки 9–13 определяют два токенизатора слов (
\w+иwordpunct_tokenize) и показывают, что они расходятся на апострофе и вопросительном знаке. - Ячейки 14–17 приводят каждую строку к нижнему регистру и токенизируют её; ячейки 18–22 удаляют английские стоп-слова NLTK и печатают строку 50 до и после этого.
- Ячейки 23–31 сравнивают WordNet-лемматизатор и стеммер Портера на одних и тех же токенах. Ячейка 26 сводит неправильные формы в таблицу; ячейка 31 печатает оба результата для строки 50 — две последние строки разобранного примера (ноутбук разбивает текст функцией
wordpunct_tokenize, поэтому'и?там сохраняются). Ячейка 28 вызывает лемматизатор без части речи. - Ячейки 33–38 переходят к набору для классификации AG News (120 000 обучающих и 7600 тестовых текстов, четыре класса) и строят распределение классов, которое оказывается сбалансированным; к нему возвращается заметка об оценке.
- Ячейки 44–46 обучают на текстах AG News векторизатор TF-IDF с
min_df=4, без удаления стоп-слов и лемматизации; ячейка 46 печатает размер словаря 29 350. Два других размера словаря из разобранного примера, 88 057 и 65 006, в ноутбуке не считаются: они получены тем же векторизатором без отсечения по частоте, первый — сlowercase=False.
Дальше#
Теперь у нас есть список единиц для каждого документа, и эти единицы — слова. Остаются две проблемы. У слова, которое не встречалось при обучении, нет записи в словаре, поэтому модель может отобразить его лишь в единственный символ «неизвестно». А словарь уровня слов для реального корпуса насчитывает сотни тысяч записей, большинство из которых встречались считанные разы. Следующая заметка заменяет слова выученными единицами-подсловами, которые ограничивают размер словаря и дают новому слову представление из известных частей; если в базовом словаре есть все 256 байтов, представление есть у любой строки.
Литература#
- The Unicode Consortium. Unicode Standard Annex #15: Unicode Normalization Forms. Определения NFC, NFD, NFKC и NFKD.
- M. F. Porter. An algorithm for suffix stripping. Program, 14(3), 1980. Стеммер Портера.
- S. Bird, E. Klein, E. Loper. Natural Language Processing with Python. O’Reilly, 2009. Глава 3 (обработка сырого текста): токенизаторы, стеммеры и WordNet-лемматизатор.
- D. Jurafsky, J. H. Martin. Speech and Language Processing, 3rd ed. draft. Глава 2 (регулярные выражения, токенизация, нормализация).