Natural Language Processing(NLP), закон Ципфа, классификация документов по их содержимому методами машинного обучения.

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Ноутбук с кодом, который был написан сотрудниками Samsung для курса «Нейронные сети и обработка текста» можно найти тут.

Ну, начнём по порядку. Что такое NLP — можно почитать тут.

Касаемо задачи классификации документа по его содержимому(а в общем случае — для любой задачи) на первый взгляд всё может показаться очень простым — заменяем слова на числа:
Вместо «Мама мыла раму» делаем: «1 2 3», а дальше уже работаем с этими числовыми(а вернее — категориальными) величинами, применяем к ним методы машинного обучения.

Дела обстоят немного иначе :)
Естественные языки бывают флективными(например, как русский). Это означает, что одно слово я могу в русском языке просклонять:
кошка, кошке, кошку, у кошки и т.д. В таком случае я уже не могу вот так просто взять и заменить слова на какие-то величины как в примере «Мама мыла раму». Вернее, я могу так сделать, но толку не будет абсолютно никакого. Для решения этой проблемы используют стемминг и лемматизацию, но тут мы теряем часть информации(например, о части речи).

Сравните теперь с аналитическим(почти) языком — английским:
cat, cats. И все! Третьего не дано :)

Вторая проблема — омонимы, в том числе и частеречные.
Сравните: «Мама мыла раму» и «Петя пришел домой и обнаружил, что мыла нет!». Слово «мыла» пишется одинаково в двух этих предложениях(омоним), но является в первом предложении — глаголом(мыть), а во втором — существительным(мыло).

Третья проблема — большой размер данных. В корпусе(множестве документов, документ = какой-то отдельный текст) может находиться огромное кол-во документов, каждый документ состоит из предложений. Каждое предложения является набором слов. И вот каждое такое уникальное слово мы должны где-то хранить. Об этой проблеме поговорим чуть ниже :)

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

На первом этапе с помощью регулярных выражений разбиваем каждый документ в корпусе на токены. Например:
Документ 1:
«Петя сегодня пошел в школу, а Маша осталась дома в виду болезни.»
Мы хотим получить что-то в духе: ['петя', 'сегодня', 'пошел', 'в', 'школу', 'а', 'маша', 'осталась', 'дома', 'в' 'виду', 'болезни']
Токенизацию можно выполнить с помощью регулярных выражений. Пример токенизации можете найти в первой ячейке ноутбука в подзаголовке «Подготовка признаков».

На втором этапе нам нужно составить словарь(для тех кто не знает что это — краткое интро), ключами которого будут являться наши токены, а значениями — некие числа, которые будут характеризовать данные ключи(токены). Для этого мы считаем частоту вхождения каждого токена в корпус, сортируем эти значения по убыванию/возрастанию частоты — таким образом мы и получаем то самое «число, что описывает некое слово»(эту частоту запишем, например, в массив — читать дальше). Словарь готов! Теперь нам нужно для каждого ключа этого словаря(для каждого токена нашего корпуса документов) просчитать его частоту в каждом документе по незамысловатой формуле: (кол-во документов, в которые входит слово) / (кол-во всех документов)Например, у нас есть 4 документа:

1 = "Казнить нельзя, помиловать. Нельзя наказывать."

2 = "Казнить, нельзя помиловать. Нельзя освободить."

3 = "Нельзя не помиловать."

4 = "Обязательно освободить.»

Тогда на выходе мы должны получить вот такой вот словарь:

{'наказывать': 0, 'не': 1, 'обязательно': 2, 'казнить': 3, 'освободить': 4, 'помиловать': 5, 'нельзя': 6}
И вот такие вот частоты для каждого соответствующего токена:
[0.25 0.25 0.25 0.5 0.5 0.75 0.75]

Тут-то мы и встречаем ту самую нашу, третью, проблему! Корпус-то большой, слов много! Предлоги, союзы, опечатки — всё в словарь полезет. Нам нужно его отфильтровать, но как это сделать, потеряв как можно меньше информации?
Все очень просто — закон Ципфа. Очень грубо он звучит так: «Токенов, которые встречаются в каждом(почти) документе, очень мало. А токенов, которые встречаются редко(т.е. в отдельных документах — разные токены) — очень много.» Первый вариант токенов — это как раз союзы, предлоги и прочие слова, от которых мы можем избавиться, утратив наименьшее кол-во информации. Второй вариант — опечатки, очень редкие слова, которые мы тоже можем убрать :)Делается это очень просто(см. график для закона Ципфа) — берем ограничение по оси Х слева и справа. Например, будем брать в наш словарь только те токены, которые встречались более 5-ти раз и только те токены, чья частота ниже 80%. Остальные токены — мы просто выкидываем. На этом этапе словарь готов. В коде ноутбука использована библиотека Samsung'а. Метод для построения словаря — build_vocabulary(): https://github.com/Samsung-IT-Academy/stepik-dl-nlp/blob/master/dlnlputils/data/base.py

tokenized_texts — это список списков. Внешний список — как оболочка. Внутренние списки — список токенов для каждого документа.
max_doc_freq=0.8 — максимальная частота токена, которую мы будем включать в словарь.
min_count=5 — минимальное кол-во вхождений слова в документ(для того, чтобы оно попало в словарь).
Про padding поговорим в следующей статье.

Словарь построили, отфильтровали — здорово! Осталось векторизовать текст(представить его в виде, условно, матрицы). По одной оси у нас будут располагаться номера документов, а по другой оси — слова, которые встречались в документе. А что будет на пересечении строк и столбцов? Мера важности слова в документе — логично же :)
Воспользуемся TF-IDF. На вики все достаточно ясно и лаконично повествуется — так что рекомендую к прочтению

Например, у нас есть 4 документа:

line1 = "Казнить нельзя, помиловать. Нельзя наказывать."

line2 = "Казнить, нельзя помиловать. Нельзя освободить."

line3 = "Нельзя не помиловать."

line4 = "Обязательно освободить.»

Получаем матрицу вида: (x, y) value

(0, 0) 0.39999983
(0, 3) 0.19999991
(0, 5) 0.26666656
(0, 6) 0.13333328
(1, 3) 0.19999991
(1, 4) 0.19999991
(1, 5) 0.26666656
(1, 6) 0.13333328
(2, 1) 0.6666664
(2, 5) 0.22222213
(2, 6) 0.22222213
(3, 2) 0.9999995
(3, 4) 0.49999976

Где x — номер документа, а y — номер токена из нашего предыдущего словаря:

{'наказывать': 0, 'не': 1, 'обязательно': 2, 'казнить': 3, 'освободить': 4, 'помиловать': 5, 'нельзя': 6}

За это в коде отвечает метод vectorize_texts — https://github.com/Samsung-IT-Academy/stepik-dl-nlp/blob/master/dlnlputils/data/bag_of_words.py
tokenized_texts — список списков токеновword2id — наш словарь, который представлен выше
word2freq — частоты токенов из словаря, что указан выше
mode='tfidf' — возможность выбрать режим для расчета меры важности токена
scale=True — очень важный параметр. Это, по сути, флаг. Если он принимает значение True, то нужно стандартизировать значения меры важности. Вообще, стандартизация — это очень важно!!! Здесь использована MinMax-стандартизация. У нас есть матрица, мы вычитаем из каждого ее элемента минимальный элемент самой матрицы(теперь минимальное значение матрицы равно 0). Теперь каждый элемент такой матрицы делим на максимальный элемент самой матрицы(таким образом мы задаем верхнюю планку значения каждому элементу матрицы).

Стоит подметить, что метрика accuracy взята в ноутбуке в виду того, что данные распределены равномерно по всем классам. В любом другом случае распределения кол-во элементов по классам — использовать accuracy — не очень целесообразно. Если не понимаете — почему, то спросите в комментариях — отвечу.

Эти две строчки кода просто оборачивают наш датасет в оболочку, которую удобнее использовать в процессе обучения нашей модели:
train_dataset = SparseFeaturesDataset(train_vectors, train_source['target'])
test_dataset = SparseFeaturesDataset(test_vectors, test_source['target'])

Дальнейшие действия в коде мы уже разбирали на других примерах, но давайте еще раз пробежимся:

model = nn.Linear(UNIQUE_WORDS_N, UNIQUE_LABELS_N) — задаем модель из одного линейного слоя. Первый параметр — кол-во входных значений(слов/токенов). Второй параметр — кол-во классов, на которые мы хотим научиться делить наши тексты.
optimizer = torch.optim.Adam(model.parameters()) — создаем оптимизатор, который будет с помощью градиентного спуска и различных эвристик(см. документацию) минимизировать нашу функцию потерь.

Ну и всё, по сути-то. Под конец лишь обращу внимание на то, что здесь используется softmax(т.к. здесь многоклассовая классификация). Что это такое и с чем его едят — я покажу и расскажу в одной из следующих статей.

P.S. — такой метод называется bag of words(мешок слов) — подразумевается, что мы теряем какую-либо связь во взаимном расположении слов.


Источник: m.vk.com

Комментарии: