Вы, наверное, слышали о встраивании word2vec. Но вы действительно понимаете, как это работает? Я думал, что знаю. Но я этого не сделал, пока не реализовал это.

Вот почему я создаю это руководство.

Обновление 2021 года: более подробную статью можно найти на странице https://neptune.ai/blog/word-embeddings-guide.

Предпосылки

Я полагаю, вы более-менее знаете, что такое word2vec.

Корпус

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

Создание словарного запаса

Самый первый шаг - это word2vec для создания словаря. Он должен быть построен в самом начале, так как его расширение не поддерживается.

Словарь - это в основном список уникальных слов с присвоенными индексами.

Корпус очень простой и короткий. В реальной реализации нам пришлось бы выполнить нормализацию регистра, удалить некоторые знаки препинания и т. Д., Но для простоты давайте использовать эти красивые и чистые данные. В любом случае, мы должны его токенизировать:

Это даст нам список токенов:

[['he', 'is', 'a', 'king'],
 ['she', 'is', 'a', 'queen'],
 ['he', 'is', 'a', 'man'],
 ['she', 'is', 'a', 'woman'],
 ['warsaw', 'is', 'poland', 'capital'],
 ['berlin', 'is', 'germany', 'capital'],
 ['paris', 'is', 'france', 'capital']]

Мы перебираем токены в корпусе и генерируем список уникальных слов (токенов). Затем мы создаем два словаря для сопоставления слова и индекса.

Что дает нам:

 0: 'he',
 1: 'is',
 2: 'a',
 3: 'king',
 4: 'she',
 5: 'queen',
 6: 'man',
 7: 'woman',
 8: 'warsaw',
 9: 'poland',
 10: 'capital',
 11: 'berlin',
 12: 'germany',
 13: 'paris',
 14: 'france'

Теперь мы можем генерировать пары center word, context word. Предположим, контекстное окно симметрично и равно 2.

Он дает нам список пар center, context индексов:

array([[ 0,  1],
       [ 0,  2],
       ...

Что можно легко перевести словами:

he is
he a
is he
is a
is king
a he
a is
a king

В этом есть смысл.

Определение цели

Теперь мы рассмотрим детали от самого первого уравнения до рабочей реализации.

Для скип-граммы нас интересует предсказание контекста, заданное центральное слово и некоторая параметризация. Это наше распределение вероятностей для одной пары.

Теперь мы хотим максимизировать его по всем парам слово / контекст.

Подождите, а почему?

Поскольку мы заинтересованы в предсказании контекста по центральному слову, мы хотим максимизировать P (context | center) для каждой пары context, center. Поскольку вероятность суммируется до 1 - мы неявно делаем P (context | center) близким к 0 для всех несуществующих пар context, center. Умножая эти вероятности, мы приближаем эту функцию к 1, если наша модель хороша, и к 0, если она плохая. Конечно, мы ищем хороший вариант - так что в начале есть оператор max.

Это выражение не очень подходит для вычислений. Вот почему мы собираемся выполнить несколько очень распространенных преобразований.

Шаг 1. Замените вероятность на отрицательную логарифмическую вероятность.

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

Шаг 2. Замените товары суммами

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

Шаг 3. Преобразование в правильную функцию потерь

И после деления на количество pars (T) мы получаем окончательный термин потерь:

Определить P

Отлично, но как мы определяем P (context | center)? А пока предположим, что у слова охвата на самом деле два вектора. Один, если присутствует как центральное слово (v), а второй, если контекст (u). Учитывая, что определение P выглядит следующим образом:

Это страшно!

Позвольте мне разбить его на более мелкие части. См. Следующую структуру:

Это просто функция softmax. Теперь посмотрим на номинатора поближе

И u, и v являются векторами. Это выражение является просто скалярным произведением данной пары center, context. Чем больше, тем больше они похожи друг на друга.

Теперь знаменатель:

Мы перебираем все слова в словаре.

И вычисление «сходства» для данного центрального слова и каждого слова в словаре, рассматриваемого как контекстное слово.

Подводя итог:

Для каждого существующего центра, контекстной пары в корпусе мы вычисляем их «показатель сходства». И разделите его на сумму каждого теоретически возможного контекста - чтобы узнать, высокий или низкий балл. Поскольку softmax гарантированно принимает значение от 0 до 1, он определяет допустимое распределение вероятностей.

Отлично, теперь давайте запрограммируем!

Нейронная сеть, реализующая эту концепцию, состоит из трех слоев: входного, скрытого и выходного.

Входной слой

Входной слой - это просто центральное слово, закодированное по очереди. Размеры [1, vocabulary_size]

Скрытый слой

Скрытый слой делает наши v векторами. Следовательно, в нем должно быть embedding_dims нейронов. Чтобы вычислить это значение, мы должны определить W1 матрицу весов. Конечно, это должно быть [embedding_dims, vocabulary_size].. Здесь нет функции активации - просто умножение матриц.

Что важно - в каждом столбце W1 хранится вектор v для одного слова. Почему? Поскольку x - это одноразовый вектор, и если вы умножите один горячий вектор на матрицу, результат будет таким же, как при выборе из него одного столбца. Попробуйте самостоятельно, используя лист бумаги;)

Выходной слой

Последний слой должен иметь vocabulary_size нейронов - потому что он генерирует вероятности для каждого слова. Следовательно, W2 [vocabulary_size, embedding_dims] по форме.

Вдобавок к этому мы должны использовать слой softmax. PyTorch предоставляет оптимизированную версию этого в сочетании с log - потому что обычный softmax на самом деле не является численно стабильным:

log_softmax = F.log_softmax(a2, dim=0)

Это эквивалентно вычислению softmax и последующему применению log.

Теперь мы можем подсчитать потери. Как обычно, PyTorch предоставляет все необходимое:

loss = F.nll_loss(log_softmax.view(1,-1), y_true)

nll_loss вычисляет отрицательную логарифмическую вероятность для logsoftmax. y_true - это контекстное слово - мы хотим сделать его как можно более высоким, потому что пара x, y_true взята из обучающих данных - так что это действительно центральный контекст.

Backprop

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

loss.backward()

Для оптимизации используется SDG. Это настолько просто, что быстрее было написать его вручную, а не создавать объект оптимизатора:

W1.data -= 0.01 * W1.grad.data
W2.data -= 0.01 * W2.grad.data

Последний шаг - обнулить градиенты, чтобы прояснить следующий проход:

W1.grad.data.zero_()
W2.grad.data.zero_()

Цикл обучения

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

Одна потенциально сложная вещь - это y_true определение. Мы не создаем one-hot явно - nll_loss делает это само.

Loss at epo 0: 4.241989389487675
Loss at epo 10: 3.8398486052240646
Loss at epo 20: 3.5548086541039603
Loss at epo 30: 3.343840673991612
Loss at epo 40: 3.183084646293095
Loss at epo 50: 3.05673006943294
Loss at epo 60: 2.953996729850769
Loss at epo 70: 2.867735825266157
Loss at epo 80: 2.79331214427948
Loss at epo 90: 2.727727291413716
Loss at epo 100: 2.6690095041479385

Извлечь векторы

Хорошо, мы обучили сеть. И последнее - извлечение векторов для слов. Это возможно тремя способами:

  • Используйте вектор v из W1
  • Используйте вектор u из W2
  • Используйте средние v и u

Постарайтесь самостоятельно подумать, когда использовать какой;)

Продолжение следует

Я работаю над интерактивной онлайн-демонстрацией этого. Скоро он будет доступен. Будьте на связи ;)

Вы можете скачать код здесь.