Изучите внутреннюю работу Word2Vec

Word2Vec рекламируется как один из самых больших и последних достижений в области обработки естественного языка (NLP). Концепция проста, элегантна и (относительно) легка для понимания. Быстрый поиск в Google возвращает несколько результатов о том, как их использовать со стандартными библиотеками, такими как Gensim и TensorFlow. Также, для любопытных, ознакомьтесь с оригинальной реализацией на языке C от Tomas Mikolov. Оригинал статьи тоже можно найти здесь.

Основное внимание в этой статье уделяется подробному представлению Word2Vec. Для этого я реализовал Word2Vec на Python с помощью NumPy (с большой помощью других руководств), а также подготовил Google Sheet для демонстрации вычислений. Вот ссылки на код и Google Sheet.

Интуиция

Целью Word2Vec является создание векторных представлений слов, которые несут семантические значения для дальнейших задач НЛП. Каждый вектор слова обычно имеет несколько сотен измерений, и каждому уникальному слову в корпусе назначается вектор в пространстве. Например, слово «счастливый» может быть представлено как вектор четырех измерений [0,24, 0,45, 0,11, 0,49], а слово «грустный» имеет вектор [0,88, 0,78, 0,45, 0,91].

Преобразование слов в векторы также известно как вложение слов. Причина этого преобразования заключается в том, что алгоритм машинного обучения может выполнять операции линейной алгебры с числами (в векторах) вместо слов.

Для реализации Word2Vec можно выбрать один из двух вариантов: Непрерывный пакет слов (CBOW) или непрерывный скип-грамм (SG). Короче говоря, CBOW пытается угадать результат (целевое слово) из своих соседних слов (контекстных слов), тогда как непрерывный Skip-Gram угадывает контекстные слова из целевого слова. Фактически Word2Vec основан на гипотезе распределения, в которой контекст каждого слова находится в соседних словах. Следовательно, глядя на соседние слова, мы можем попытаться предсказать целевое слово.

По словам Миколова (цитируется в этой статье), вот разница между Skip-gram и CBOW:

Пропуск грамма: хорошо работает с небольшим объемом обучающих данных, хорошо отображает даже редкие слова. или фразы

CBOW: в несколько раз быстрее обучается, чем скип-грамм, немного лучше для частые слова

Чтобы уточнить детали, поскольку Skip-gram учится предсказывать контекстные слова из заданного слова, в случае, когда два слова (одно встречается нечасто, а другое чаще) помещаются рядом, оба будут обрабатываться одинаково, когда дело доходит до минимизации потерь, поскольку каждое слово будет рассматриваться и как целевое, и как контекстное слово. Сравнивая это с CBOW, редкое слово будет только частью набора контекстных слов, используемых для предсказания целевого слова. Таким образом, модель присвоит редкому слову низкую вероятность.

Процесс реализации

В этой статье мы реализуем архитектуру Skip-gram. Для удобства чтения содержание разбито на следующие части:

  1. Подготовка данных - определение корпуса, очистка, нормализация и токенизация слов.
  2. Гиперпараметры - скорость обучения, эпохи, размер окна, размер встраивания.
  3. Создание обучающих данных - создание словаря, быстрое кодирование слов, создание словарей, которые сопоставляют идентификатор со словом и наоборот.
  4. Обучение модели. Передача закодированных слов через прямой проход, вычисление частоты ошибок, корректировка весов с помощью обратного распространения ошибки и вычисление потерь.
  5. Вывод - получите вектор слов и найдите похожие слова
  6. Дальнейшие улучшения - сокращение времени обучения с помощью функции Skip-gram Negative Sampling (SGNS) и Hierarchical Softmax.

1. Подготовка данных

Для начала мы начнем со следующего корпуса:

обработка естественного языка и машинное обучение - это весело и увлекательно

Для простоты мы выбрали предложение без знаков препинания и заглавных букв. Также мы не удалили стоп-слова «и» и «есть».

На самом деле текстовые данные неструктурированы и могут быть грязными. Их очистка включает в себя такие шаги, как удаление стоп-слов, знаков препинания, преобразование текста в нижний регистр (на самом деле зависит от вашего варианта использования), замена цифр и т. Д. У KDnuggets есть отличная статья об этом процессе. В качестве альтернативы Gensim также предоставляет функцию для выполнения простой предварительной обработки текста с использованием gensim.utils.simple_preprocess, где он преобразует документ в список токенов нижнего регистра, игнорируя слишком короткие или слишком длинные токены.

После предварительной обработки мы переходим к токенизации корпуса. Здесь мы разбиваем наш корпус на пробелы, и в результате получаем список слов:

[«Естественный», «язык», «обработка», «и», «машина», «обучение», «это», «развлечение», «и», «захватывающий»]

2. Гиперпараметры

Прежде чем мы перейдем к реальной реализации, давайте определим некоторые гиперпараметры, которые нам понадобятся позже.

[window_size]: Как упоминалось выше, контекстные слова - это слова, которые соседствуют с целевым словом. Но как далеко или близко должны быть эти слова, чтобы считаться соседом? Здесь мы определяем window_size равным 2, что означает, что слова, расположенные на 2 слева и справа от целевых слов, считаются контекстными словами. Ссылаясь на рисунок 3 ниже, обратите внимание, что каждое слово в корпусе будет целевым словом при перемещении окна.

[n]: Это размер встраивания слова, который обычно колеблется от 100 до 300 в зависимости от размера вашего словарного запаса. Размер, превышающий 300, имеет тенденцию иметь убывающую выгоду (см. Стр. 1538 Рисунок 2 (a)). Обратите внимание, что размер также является размером скрытого слоя.

[epochs]: Это количество эпох обучения. В каждую эпоху мы циклически перебираем все обучающие выборки.

[learning_rate]: Скорость обучения контролирует величину корректировки весов по отношению к градиенту потерь.

3. Создание обучающих данных

В этом разделе наша основная цель - превратить наш корпус в быстро закодированное представление для модели Word2Vec для обучения. В нашем корпусе на рис. 4 увеличено изображение каждого из 10 окон (от №1 до №10), как показано ниже. Каждое окно состоит как из целевого слова, так и из его контекстных слов, выделенных оранжевым и зеленым цветом соответственно.

Пример первого и последнего элементов в первом и последнем тренировочном окне показан ниже:

# 1 [Цель (естественный)], [Контекст ( язык , обработка )]
[список ([1, 0, 0, 0, 0, 0, 0, 0, 0])
список ([
[0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0] ])]

***** № 2–9 удалены ****

№10 [Цель (захватывающе)], [Контекст ( веселье , и )]
[список ([0, 0, 0, 0, 0, 0, 0, 0, 1])
список ([
[0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0] ])]

Чтобы сгенерировать горячие обучающие данные, мы сначала инициализируем объект word2vec(), а затем используем объект w2v для вызова функции generate_training_data, передавая settings и corpus в качестве аргументов.

Внутри функции generate_training_data мы выполнили следующие операции:

  1. self.v_count - Длина словарного запаса (обратите внимание, что словарный запас относится к количеству уникальных слов в корпусе)
  2. self.words_list - Список слов в словаре
  3. self.word_index - Словарь с каждым ключом как словом в словаре и значением как индексом
  4. self.index_word - Словарь с каждым ключом как индексом и значением как словом в словаре
  5. for цикл для добавления быстрого представления для каждой цели и ее контекстных слов в training_data с помощью функции word2onehot.

4. Модельное обучение

С нашим training_data, теперь мы готовы обучать нашу модель. Обучение начинается с w2v.train(training_data), где мы передаем данные обучения и вызываем функцию train.

Модель Word2Vec состоит из двух весовых матриц (w1 и w2), и для демонстрационных целей мы инициализировали значения в форме (9x10) и (10x9) соответственно. Это облегчает вычисление ошибки обратного распространения ошибки, о которой будет рассказано далее в статье. В реальном обучении вы должны случайным образом инициализировать веса (например, используя np.random.uniform()). Для этого закомментируйте строки 9 и 10 и раскомментируйте строки 11 и 12.

Тренировка - пас вперед

Затем мы начинаем обучение нашей первой эпохе, используя первый обучающий пример, передавая w_t, который представляет собой горячий вектор для целевого слова, функции theforward_pass. В функции forward_pass мы выполняем скалярное произведение между w1 и w_t, чтобы получить h (строка 24). Затем мы выполняем другое скалярное произведение, используя w2 и h, чтобы создать выходной слой u (строка 26). Наконец, мы запускаем u через softmax, чтобы заставить каждый элемент находиться в диапазоне от 0 до 1, чтобы дать нам вероятности для предсказания (строка 28) перед возвратом вектора для predictiony_pred, скрытого слоя h и выходного слоя u.

Я приложил несколько снимков экрана, чтобы показать расчет для первой обучающей выборки в первом окне (№1), где целевым словом является естественный, а контекстными словами - язык и обработка. Не стесняйтесь заглянуть в формулу в Google Sheet здесь.

Обучение - ошибка, обратное распространение и потеря

Ошибка - с y_pred, h и u мы переходим к вычислению ошибки для этого конкретного набора целевых и контекстных слов. Это делается путем суммирования разницы между y_pred и каждым из контекстных слов вw_c.

Обратное распространение. Затем мы используем функцию обратного распространения, backprop, чтобы вычислить величину корректировки, которая нам необходима для изменения весов с помощью функции backprop путем передачи ошибки EI, скрытого слоя h и вектора для целевого слова w_t .

Чтобы обновить веса, мы умножаем веса, которые нужно скорректировать (dl_dw1 и dl_dw2), на скорость обучения, а затем вычитаем их из текущих весов (w1 и w2).

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

5. Вывод

Теперь, когда мы завершили обучение для 50 эпох, оба веса (w1 и w2) готовы к выполнению логического вывода.

Получение вектора для слова

Имея обученный набор весов, первое, что мы можем сделать, - это посмотреть на вектор слова для слова в словаре. Мы можем просто сделать это, посмотрев индекс слова относительно натренированного веса (w1). В следующем примере мы ищем в векторе слово «машина».

> print(w2v.word_vec("machine"))
[ 0.76702922 -0.95673743  0.49207258  0.16240808 -0.4538815  -0.74678226  0.42072706 -0.04147312  0.08947326 -0.24245257]

Найдите похожие слова

Еще мы можем найти похожие слова. Несмотря на то, что наш словарный запас невелик, мы все же можем реализовать функцию vec_sim, вычислив косинусное сходство между словами.

> w2v.vec_sim("machine", 3)
machine 1.0
fun 0.6223490454018772
and 0.5190154215400249

6. Дальнейшие улучшения

Если вы все еще читаете статью, молодец и спасибо! Но это еще не конец. Как вы могли заметить на шаге выше, мы должны скорректировать веса для всех других слов, которые не были задействованы в обучающей выборке. Этот процесс может занять много времени, если размер вашего словарного запаса велик (например, десятки тысяч).

Чтобы решить эту проблему, ниже приведены две функции Word2Vec, которые вы можете реализовать для ускорения работы:

  • Skip-gram Negative Sampling (SGNS) помогает сократить время обучения и улучшить качество результирующих векторов слов. Это достигается путем обучения сети изменению лишь небольшого процента весов, а не всех их. Напомним, что в нашем примере выше мы обновляем веса для каждого второго слова, и это может занять очень много времени, если размер словаря большой. С SGNS нам нужно только обновить веса для целевого слова и небольшого количества (например, от 5 до 20) случайных отрицательных слов.
  • Иерархический Softmax - это еще одна уловка для сокращения времени обучения, заменяющая исходный softmax. Основная идея заключается в том, что вместо оценки всех выходных узлов для получения распределения вероятностей нам нужно только оценить его логарифм (на основе 2). Он использует представление двоичного дерева (дерево кодирования Хаффмана), где узлы в выходном слое представлены как листья, а его узлы представлены в виде относительных вероятностей его дочерних узлов.

Помимо этого, почему бы не попробовать настроить код для реализации архитектуры непрерывного мешка слов (CBOW)? 😃

Заключение

Эта статья представляет собой введение в Word2Vec и в мир встраивания слов. Также стоит отметить, что доступны предварительно обученные вложения, такие как GloVe, fastText и ELMo, которые вы можете скачать и использовать напрямую. Существуют также расширения Word2Vec, такие как Doc2Vec и самый последний Code2Vec, где документы и коды преобразуются в векторы. 😉

Наконец, я хочу поблагодарить Ren Jie Tan, Raimi и Yuxin за то, что они нашли время, чтобы прокомментировать и прочитать черновики этого документа. 💪

Примечание. Эта статья впервые появилась в моем блоге https://derekchia.com/an-implementation-guide-to-word2vec-using-numpy-and-google-sheets/

использованная литература