Обработка естественного языка (Natural Language Processing, NLP) — область ИИ, в которой исследуются подходы и инструменты интерпретации (понимания) и генерации естественных языков в виде текста.
Естественный язык — это человеческий язык, который сложился исторически для повседневного общения, а не был специально спроектирован (в отличие от формальных или искусственных языков).
Примеры задач NLP:
Решение любой задачи NLP (в большей или меньшей степени) требует подготовительного этапа — предварительной обработки текстовых данных
Мне нравится делить все задачи NLP на два типа: понимание языка (текст -> ...) и языковое моделирование (... -> текст)
Предварительная обработка — это совокупность методов и техник для подготовки сырых текстовых данных к анализу с применением языковых моделей.
Как и в любой задаче, связанной с ИИ: принцип "garbage in — garbage out"
Конвейер предварительной обработки текстовых данных выглядит следующим образом:
Основные цели предварительной обработки текстовых данных: уменьшение размера признакового пространства и устранение избыточности
Принципы предварительной обработки:
import re
from bs4 import BeautifulSoup
# Регулярные выражения
text = re.sub(r'<.*?>', '', text)
# BeautifulSoup
text = BeautifulSoup(text, "html.parser").get_text()
Регулярные выражения — это язык шаблонов для поиска, проверки и преобразования текста.
Конечно, вот таблица с основными элементами, используемыми в регулярных выражениях.
| Категория | Символ | Описание | Пример |
|---|---|---|---|
| Символьные литералы | a, 1, ! |
Любой обычный символ соответствует самому себе . | кот найдет "кот" |
| Метасимволы | . |
Соответствует любому одиночному символу, кроме символа новой строки. | к.т найдет "кот", "кит", "к9т" |
\ |
Экранирует следующий символ, превращая его в литерал. | \. найдет точку "." |
|
| Классы символов | [...] |
Соответствует любому символу из указанного набора. | [абв] найдет "а", "б" или "в" |
[^...] |
Соответствует любому символу, не входящему в указанный набор. | [^0-9] найдет любой символ, кроме цифры |
|
\d |
Соответствует любой цифре. Эквивалентно [0-9]. |
\d{3} найдет три цифры подряд |
|
\D |
Соответствует любому символу, кроме цифры. Эквивалентно [^0-9]. |
\D+ найдет одну или более не-цифр |
|
\w |
Соответствует любой букве, цифре или знаку подчеркивания. Эквивалентно [a-zA-Z0-9_]. |
\w+ найдет одно или более "словесных" символов |
|
\W |
Соответствует любому символу, кроме буквы, цифры или знака подчеркивания. | \W найдет пробел, !, @, # и т.д. |
|
\s |
Соответствует любому пробельному символу (пробел, табуляция, новая строка). | слово\sслово найдет два слова, разделенных пробелом |
|
\S |
Соответствует любому непробельному символу. | \S+ найдет последовательность символов без пробелов |
|
| Якоря (Указатели) | ^ |
Соответствует началу строки. | ^Старт найдет "Старт" только в начале строки |
$ |
Соответствует концу строки. | Конец$ найдет "Конец" только в конце строки |
|
| Квантификаторы | * |
Соответствует предыдущему элементу 0 или более раз. | a* найдет "", "a", "aa", "aaa" и т.д. |
+ |
Соответствует предыдущему элементу 1 или более раз. | a+ найдет "a", "aa", "aaa" и т.д. |
|
? |
Соответствует предыдущему элементу 0 или 1 раз. | цвет(а)? найдет "цвет" и "цвета" |
|
{n} |
Соответствует предыдущему элементу ровно n раз. |
\d{4} найдет ровно 4 цифры |
|
{n,} |
Соответствует предыдущему элементу не менее n раз. |
\d{2,} найдет 2 или более цифр |
|
{n,m} |
Соответствует предыдущему элементу от n до m раз. |
\w{3,5} найдет от 3 до 5 словесных символов |
|
| Группировка и чередование | (...) |
Группирует несколько элементов в единое целое. | (абв)+ найдет "абв", "абвабв" и т.д. |
| |
Работает как оператор "ИЛИ" (чередование). | кот|собака найдет "кот" или "собака" |
import re
# Удаление URL
text = re.sub(r'http\S+|www\.\S+', '', text)
# Удаление email
text = re.sub(r'\S+@\S+', '', text)
text = re.sub(r'[^\w\s]', '', text)
# Оставить только русские буквы и пробельные символы
text = re.sub(r'[^а-яё\s]', '', text, flags=re.IGNORECASE)
# Полное удаление числительных
text = re.sub(r'\d+', '', text)
# Замена числительных на специальный токен
text = re.sub(r'\d+', 'NUM', text)
# Удаление лишних пробелов
text = re.sub(r'\s+', ' ', text).strip()
# Удаление пробелов в начале и конце строк
text = '\n'.join([line.strip() for line in text.split('\n')])
Для очистки текстовых данных также рекомендую использовать библиотеки
clean-textиemoji
Токенизацией называется разделение текстового документа на неделимые единицы (токены). В качестве токенов в NLP могут выступать:
Самый простой пример токенизации:
text = "Токенизация - это просто!"
tokens = text.split()
print(tokens)
# Вывод: ['Токенизация', '-', 'это', 'просто!']
Токенизация на основе правил (с применением nltk):
import nltk
nltk.download('punkt')
from nltk.tokenize import word_tokenize, sent_tokenize
text = "Привет! Как у тебя дела?"
tokens = word_tokenize(text)
sentences = sent_tokenize(text)
print(tokens)
# Вывод: ['Привет', '!', 'Как', 'у', 'тебя', 'дела', '?']
Применение регулярных выражений (не рекомендуется при решении прикладных задач):
import re
text = "Я люблю AI, ML и NLP!"
tokens = re.findall(r'\b\w+\b', text)
print(tokens)
# Вывод: ['Я', 'люблю', 'AI', 'ML', 'и', 'NLP']
При токенизации текстов на русском языке на уровне слов рекомендуется применять библиотеку razdel, которая является частью проекта Natasha. Он запущен в 2020 году Лабораторией анализа данных Александра Кукушкина.
Также можно использовать мультиязычные фреймворки-конвейеры Spacy и Stanza.
Для определения языка текста можно использовать это решение.
Забегая немного вперёд, важно сказать, что токены впоследствие будут ассоциироваться с числами и векторами.
Токенизация на уровне слов подвержена проблеме Out of Vocabulary (OOV) и приводит к большим объёмам словарей. В то же время токенизация на уровне символов дает возможность работать с небольшими словарями, однако в задачах, связанных с языковым моделированием, при векторизации символа (даже с учетом информации о позиции), сохраняется меньше деталей, чем при векторизации на уровне слов.
Современным подходом к токенизации является токенизация на уровне частей слов. Актуальные подходы:
tiktokenПример обучения и работы BPE
Пусть есть небольшой обучающий корпус (так в NLP называют наборы текстовых данных), состоящий всего из одной фразы (одного текстового документа), где некоторые слова повторяются или имеют общие корни:
старый читатель перечитал старые книги
Считаем частоту каждого слова и разбиваем все слова на последовательности символов. Чтобы отличать конец слова добавляем специальный маркер </w>.
старый (1 раз) -> с т а р ы й </w>
читатель (1 раз) -> ч и т а т е л ь </w>
перечитал (1 раз) -> п е р е ч и т а л </w>
старые (1 раз) -> с т а р ы е </w>
книги (1 раз) -> к н и г и </w>
Начальный словарь токенов состоит из символов: {с, т, а, р, ы, й, ч, и, е, л, ь, п, к, н, г, </w>}.
Слияние 1:
Подсчитаем все пары токенов. Необходимо выбрать наиболее часто встречающуюся. Пара (с, т) встречается 2 раза (в старый и старые). Пара (т, а) — 2 раза (читатель, перечитал). Пара (ч, и) — 2 раза. Выбираем (с, т) (для примера). Объединяем с и т в новый токен ст. Результат:
ст а р ы й </w>
ч и т а т е л ь </w>
п е р е ч и т а л </w>
ст а р ы е </w>
к н и г и </w>
Процесс повторяется заданное количество раз (количество слияний — гиперпараметр). Получим словарь (с, т, ..., ст, ар, чи, стар, ...) и список правил слияния. Токенизация слова старость будет:
["стар", "о", "с", "т", "ь", "</w>"]
Важный вывод: мы наблюдаем процесс обучения токенизатора
Можно использовать библиотеку tokenizers (пример обучения токенизатора на своих данных):
from tokenizers import Tokenizer
from tokenizers.models import BPE
tokenizer = Tokenizer(BPE())
tokenizer.train(files=["corpus.txt"])
encoded = tokenizer.encode("машинное обучение")
WordPiece (также реализован в библиотеке tokenizers) отличается правилом выбора пары токенов для объединения. Для каждой пары вычисляется оценка выгоды от объединения по формуле:
оценка = частота(AB) / (частота(A) * частота(B))
Стоп-слова — часто встречающиеся слова, которые несут мало смысловой информации: "в", "и", "на", "с", "по" и т.д.
В общем случае стоп-слова определяются индивидуально для каждой задачи, однако следующие вещи принято всегда относить к стоп-словам:
Удаление стоп-слов с применением NLTK:
from nltk.corpus import stopwords
nltk.download('stopwords')
russian_stopwords = set(stopwords.words('russian'))
filtered_tokens = [word for word in tokens if word not in russian_stopwords]
При работе с мультиязычными текстами не забывайте удалять стоп-слова из других языков
Расширение списка стоп-слов:
custom_stopwords = russian_stopwords.union({
'это', 'который', 'может', 'один', 'два'
})
domain_stopwords = {'компания', 'организация', 'система'}
Слова в естественных языках часто употребляются в различных видах и формах, при этом семантика слова не изменяется (слова 'красивый', 'красивая', 'красивое' с точки зрения семантики одинаковы). Нормализация данных предполагает, что все семантически одинаковые слова мы заменяем одним словом — нормальной формой. Существует два подхода к нормализации:
Пример стемминга:
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer('russian')
stemmed_words = [stemmer.stem(word) for word in tokens]
# "программирование" - "программирован"
# "программист" - "программист"
# "программы" - "программ"
Пример лемматизации с использованием библиотеки pymorphy3:
import pymorphy3
morph = pymorphy3.MorphAnalyzer()
def lemmatize_word(word):
return morph.parse(word)[0].normal_form
lemmatized_words = [lemmatize_word(word) for word in tokens]
Библиотека использует данные из открытого корпуса для русского языка OpenCorpora:
morph.tag('красивая')
# [OpencorporaTag('ADJF,Qual femn,sing,nomn')]
Более подробно ознакомиться с информацией о тегах можно в документации.