Пошаговое руководство по обучению модели Word2vec

Введение

Важным компонентом обработки естественного языка (NLP) является способность переводить слова, фразы или большие объемы текста в непрерывные числовые векторы. Существует много методов для выполнения этой задачи, но в этом посте мы сосредоточимся на методе, опубликованном в 2013 году, под названием word2vec.

Word2vec — это алгоритм, опубликованный Mikolov et al. в статье под названием Эффективная оценка представлений слов в векторном пространстве. Эту статью стоит прочитать, хотя я предоставлю обзор, поскольку мы строим ее с нуля в PyTorch. Короче говоря, word2vec использует искусственную нейронную сеть с одним скрытым слоем для изучения плотных вложений слов. Эти вложения слов позволяют нам идентифицировать слова, которые имеют сходные семантические значения. Кроме того, вложения слов позволяют нам применять алгебраические операции. Например, вектор('Король') - вектор('Мужчина') + вектор('Женщина') приводит к вектору, наиболее близкому к векторному представлению слова Королева ("Эффективная оценка представлений слов в векторном пространстве" 2).

Рисунок 1 представляет собой пример встраивания слов в трех измерениях. Вложения слов могут изучать семантические отношения между словами. Пример «мужчина-женщина» показывает, насколько отношения между «мужчиной» и «женщиной» очень похожи на отношения между «королем» и «королевой». Синтаксические отношения могут быть закодированы вложениями, как показано в примере «Время глагола».

Обзор вложений Word

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

Зачем вообще нужны вложения слов?

Компьютеры — это просто абстрактные калькуляторы. Они действительно эффективны при выполнении математических вычислений. Каждый раз, когда мы хотим выразить свои мысли компьютеру, мы должны использовать числовой язык. Если мы хотим выяснить тональность отзыва или тему популярной книги, нам нужно сначала перевести текст в векторы. Только после этого мы можем использовать последующие процедуры для извлечения интересующей информации из текста.

Простейшие вложения слов

Встраивание слов — это именно то, как мы переводим наши мысли на язык, понятный компьютерам. Давайте рассмотрим пример, взятый из статьи Википедии о python, в которой говорится, что python неизменно считается одним из самых популярных языков программирования. Это предложение содержит 11 слов, так почему бы нам не создать вектор длиной 11, где каждый индекс принимает значение 1, если слово присутствует, и 0, если его нет? Это широко известно как горячее кодирование.

python       = [1,0,0,0,0,0,0,0,0,0,0]
consistently = [0,1,0,0,0,0,0,0,0,0,0]
ranks        = [0,0,1,0,0,0,0,0,0,0,0]
as           = [0,0,0,1,0,0,0,0,0,0,0]
one          = [0,0,0,0,1,0,0,0,0,0,0]
of           = [0,0,0,0,0,1,0,0,0,0,0]
the          = [0,0,0,0,0,0,1,0,0,0,0]
most         = [0,0,0,0,0,0,0,1,0,0,0]
popular      = [0,0,0,0,0,0,0,0,1,0,0]
programming  = [0,0,0,0,0,0,0,0,0,1,0]
languages    = [0,0,0,0,0,0,0,0,0,0,1]

Этот метод преобразования слов в векторы, пожалуй, самый простой. Тем не менее, есть несколько недостатков, которые послужат мотивацией для встраивания word2vec. Во-первых, длина векторов вложения увеличивается линейно с размером словаря. Как только нам нужно встроить миллионы слов, этот метод встраивания становится проблематичным с точки зрения пространственной сложности. Во-вторых, проблема в том, что эти векторы разрежены. Каждый вектор имеет только одну запись со значением 1, а все остальные записи имеют значение 0. Опять же, это значительная трата памяти. Наконец, каждый вектор слов ортогонален любому другому вектору слов. Поэтому нет способа определить, какие слова наиболее похожи. Я бы сказал, что слова python и программирование следует считать более похожими друг на друга, чем python и ранги. К сожалению, векторные представления для каждого из этих слов одинаково отличаются от любого другого вектора.

Улучшенные вложения слов

Теперь наша цель более конкретизирована: можем ли мы создать вложения фиксированной длины, которые позволят нам определить, какие слова наиболее похожи друг на друга? Примером может быть:

python       = [0.5,0.8,-0.1]
ranks        = [-0.5,0.1,0.8]
programming  = [0.9,0.4,0.1]

Если мы возьмем точечный продукт питона и рангов, мы получим:

И если мы возьмем скалярное произведение «python» и «программирования», мы получим:

Поскольку оценка между «python» и «рангами» ниже, чем у «python» и «программирования», мы бы сказали, что «python» и «программирование» более похожи. Как правило, скалярное произведение двух вложений не используется для вычисления показателя сходства. Вместо этого мы будем использовать косинусное сходство, поскольку оно устраняет влияние векторных норм и возвращает более стандартизированную оценку. Несмотря на это, обе проблемы, с которыми мы столкнулись при использовании метода горячего кодирования, решены — наши векторы встраивания имеют фиксированную длину, и они позволяют нам вычислять сходство между словами.

Пропустить архитектуру Word2Vec

Теперь, когда мы разобрались с вложениями слов, возникает вопрос, как изучать эти вложения. Здесь в игру вступает модель Миколова word2vec. Если вы не знакомы с искусственными нейронными сетями, следующие разделы будут непонятны, так как word2vec принципиально основан на этом типе модели. Я настоятельно рекомендую ознакомиться с бесплатным онлайн-курсом Майкла Нильсена Глубокое обучение и нейронные сети и серией статей 3Blue1Brown YouTube по нейронным сетям, если этот материал для вас новый.

Скипграммы

Вспомните предложение «python неизменно считается одним из самых популярных языков программирования». Представьте, что кто-то не знает слова «программирование» и хочет понять его значение. Разумный подход состоит в том, чтобы проверить соседние слова на наличие подсказки о значении этого неизвестного слова. Они бы заметили, что он был окружен «популярностью» и «языком». Эти слова могут дать им намек на возможное значение слова «программирование». Именно так работает модель скипграмм. В конечном счете, мы обучим нейронную сеть предсказывать соседние контекстные слова по входному слову. На рисунке 2 зеленое слово — это неизвестное целевое слово, а синие слова, окружающие его, — контекстные слова, которые наша нейронная сеть будет обучена предсказывать.

В этом примере размер окна равен 2. Это означает, что каждое целевое слово будет окружено 2 контекстными словами, которые модель должна будет предсказать. Поскольку слово «ранг» имеет 2 слова слева и 2 слова справа, результирующие обучающие данные представляют собой 4 примера для этого целевого слова.

Архитектура модели

Нейронная сеть, используемая для изучения этих вложений слов, представляет собой сеть прямой связи с одним скрытым слоем. Входными данными для сети являются целевые слова. Ярлыки — это контекстные слова. Единственный скрытый слой будет измерением, в которое мы решим встроить наши слова. В этом примере мы будем использовать размер встраивания 300.

Давайте рассмотрим пример того, как работает эта модель. Если мы хотим встроить слово, первый шаг — найти его индекс в словаре. Затем этот индекс передается в сети как индекс строки в матрице внедрения. На рисунке 3 входное слово является второй записью в нашем словарном векторе. Это означает, что теперь мы введем зеленую матрицу встраивания во вторую строку. Эта строка имеет длину 300 — размерность внедрения, N. Затем мы умножаем этот вектор, который является скрытым слоем, на вторую матрицу вложения формы N x V, чтобы получить вектор длины V.

Обратите внимание, что во второй матрице встраивания (фиолетовая матрица) Vcolumns. Каждый из этих столбцов представляет слово в словаре. Другой способ концептуализировать это матричное умножение — признать, что оно приводит к скалярному произведению между вектором для целевого слова (скрытый слой) и каждым словом в словаре (столбцы фиолетовой матрицы). Результатом является вектор длины V, представляющий предсказания контекстных слов. Поскольку размер нашего контекстного окна равен 2, у нас будет 4 вектора предсказания длины V. Затем мы сравниваем эти векторы предсказания с соответствующими наземными векторами истинности, чтобы вычислить потери, которые мы распространяем обратно по сети для обновления параметров модели. В этом случае параметры модели являются элементами матриц вложения. Обсуждение механики этой обучающей процедуры будет конкретизировано в коде PyTorch позже.

Отрицательная выборка

В статье под названием Распределенные представления слов и фраз и их композиционность Миколова и др. Авторы предлагают два улучшения исходной модели word2vec — отрицательную выборку и подвыборку.

На рисунке 3 обратите внимание на то, что каждый вектор предсказания имеет длину V. Наземные векторы истинности, которые будут сравниваться с каждым вектором прогнозирования, также будут иметь длину V, но наземные векторы истинности будут чрезвычайно разреженными, поскольку только один элемент вектора будет помечен 1 — истинное контекстное слово, которое обучает модель. предсказывать. Это истинное контекстное слово будет упоминаться как «позитивное контекстное слово». Каждое второе слово в словаре, состоящее из V — 1 слов, будет помечено 0, поскольку оно не является контекстным словом в обучающем примере. Все эти слова будут называться «словами отрицательного контекста».

Миколов и др. предложил методологию под названием отрицательная выборка, которая уменьшает размер вектора истинности земли и, следовательно, вектора предсказания. Это снижает вычислительные требования сети и ускоряет обучение. Вместо использования всех слов отрицательного контекста Миколов предложил метод выборки небольшого количества слов отрицательного контекста из существующихV — 1 слов отрицательного контекста с использованием условного распределения вероятностей.

В представленном коде я реализую процедуру отрицательной выборки, отличную от метода, предложенного Миколовым. Его было проще построить, и он по-прежнему обеспечивает высокое качество встраивания. В статье Миколова вероятность того, что будет выбрана отрицательная выборка, основана на условной вероятности увидеть слово-кандидат в контексте целевого слова. Таким образом, для каждого слова в словаре мы создадим распределение вероятности для каждого другого слова в словаре. Эти распределения представляют собой условную вероятность увидеть другое слово в контексте целевого слова. Затем слова отрицательного контекста будут отбираться с вероятностью, обратно пропорциональной условной вероятности слова контекста.

Я реализовал отрицательную выборку немного по-другому, избегая условных распределений. Во-первых, я нашел частотность каждого слова в словаре. Я проигнорировал условную вероятность, найдя общую частоту. Затем произвольно большой отрицательный вектор выборки заполняется словарными индексами, пропорциональными частоте слова. Например, если слово «есть» составляет 0,01% корпуса, и мы решили, что вектор отрицательной выборки должен иметь размер 1 000 000, то 100 элементов (0,01% x 1 000 000) вектора отрицательной выборки будут заполнены словарным индексом. от слова «есть». Затем для каждого обучающего примера мы случайным образом выбираем небольшое количество элементов из вектора отрицательной выборки. Если это небольшое число равно 20, а словарный запас равен 10 001 слову, мы просто уменьшили длину векторов предсказания и наземной истины на 9 980 элементов. Это сокращение существенно ускоряет время обучения модели.

Подвыборка

Подвыборка — еще один метод, предложенный Mikolov et al. для сокращения времени обучения и повышения производительности модели. Фундаментальное наблюдение, из которого возникает подвыборка, заключается в том, что слова с высокой частотой «обеспечивают меньшую информационную ценность, чем редкие слова» («Распределенные представления слов и фраз и их композиционность» 4). Например, такие слова, как «есть», «тот» или «в» встречаются довольно часто. Эти слова, скорее всего, будут сочетаться со многими другими словами. Это означает, что контекстные слова вокруг этих высокочастотных слов передают мало контекстуальной информации о самом высокочастотном слове. Таким образом, вместо того, чтобы использовать каждую пару слов в корпусе, мы выбираем слова с вероятностью, обратно пропорциональной частоте слов в паре. Точные детали реализации будут объяснены в следующем разделе.

Реализация PyTorch

После обзора вложений слов, архитектуры word2vec, отрицательной выборки и подвыборки, давайте углубимся в код. Обратите внимание, что существуют фреймворки, которые абстрагируются от деталей реализации word2vec. Эти параметры чрезвычайно эффективны и обеспечивают расширяемость для пользователя. Например, gensim предоставляет word2vec API, который включает в себя дополнительные функции, такие как использование предварительно обученных моделей и многословных n-грамм. Однако в этом уроке мы создадим модель word2vec без использования какой-либо из этих платформ.

Весь код, который мы рассмотрим в этом руководстве, можно найти на моем GitHub. Обратите внимание, что код в репозитории может быть изменен, пока я работаю над ним. Для целей этого руководства здесь будет представлена ​​упрощенная версия этого кода в блокноте Google Colab.

Получение данных

Мы будем использовать набор данных википедии под названием WikiText103, предоставленный PyTorch, для обучения нашей модели word2vec. В приведенном ниже коде вы увидите, как я импортирую и печатаю первые несколько строк набора данных. Первый текст взят из статьи в Википедии Хроники Валькирии III.

Установка параметров и конфигурация

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

Здесь мы создаем класс данных, содержащий параметры, которые определяют нашу модель word2vec. Первый раздел управляет предварительной обработкой текста и построением скипграммы. Мы будем рассматривать только слова, которые встречаются не менее 50 раз. Это контролируется параметром MIN_FREQ. SKIPGRAM_N_WORDS — это размер окна, который мы будем учитывать при построении скипграмм. Это означает, что мы рассмотрим 8 слов до и после целевого слова. T управляет тем, как мы вычисляем вероятность подвыборки. Это означает, что слова с частотой в 85-м процентиле будут иметь небольшую вероятность того, что они будут подвергнуты подвыборке, как мы описали в разделе о подвыборке выше. NEG_SAMPLES — это количество отрицательных выборок для использования в каждом обучающем примере, как описано выше в разделе отрицательной выборки. NS_ARRAY_LEN — это длина отрицательного вектора выборки, из которого мы будем выбирать отрицательные наблюдения. SPECIALS — это строка-заполнитель для слов, которые исключаются из словаря, если они не соответствуют минимальному требованию частоты. TOKENIZER относится к тому, как мы хотим преобразовать корпус текста в токены. Токенизатор «basic_english» разбивает весь текст пробелами.

Второй раздел определяет конфигурацию модели и гиперпараметры. BATCH_SIZE — это количество документов, которые будут в каждом мини-пакете, используемом для обучения сети. EMBED_DIM — это размерность вложений, которые мы будем использовать для каждого слова в словаре. EMBED_MAX_NORM — это максимальная норма, которой может быть каждый вектор встраивания. N_EPOCHS — это количество эпох, для которых мы будем обучать модель. DEVICE сообщает PyTorch, использовать ли ЦП или ГП для обучения модели. CRITERION — используемая функция потерь. Обсуждение выбора функции потерь будет продолжено при обсуждении процедуры обучения модели.

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

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

Не просматривая каждую строку в этом коде, обратите внимание, что класс Vocab имеет stoi, itos, и total_tokens атрибуты, а также методы get_index(), get_freq(), и lookup_token(). Нижеследующая суть покажет, что делают эти атрибуты и методы.

stoi — это словарь, в котором ключами являются слова, а значениями являются кортежи индекса и частоты ключевых слов. Например, слово «питон» занимает 13 898 место среди самых распространенных слов, встречающихся 403 раза. Запись в словаре stoi для этого слова будет {"python": (13898, 403)}. itos похож на stoi,, но его ключ — это значение индекса, так что запись для «python» будет {13898: ("python", 403)}.Атрибут total_tokens — это общее количество токенов во всем корпусе. В нашем примере 77 514 579 слов.

Метод get_index() принимает на вход слово или список слов и возвращает индекс или список индексов этих слов. Если бы мы вызвали Vocab.get_index("python"), возвращаемое значение было бы 13898. Метод get_frequency() принимает слово или список слов в качестве входных данных и возвращает частоту встречаемости слов в виде целого числа или списка целых чисел. Если бы мы вызвали Vocab.get_freq("python"), возвращаемое значение было бы 403. Наконец, метод lookup_token() принимает целое число и возвращает слово, занимающее этот индекс. Например, если бы мы вызвали Vocab.lookup_token(13898), метод вернул бы "python".

Последние функции в приведенном выше списке — это yield_tokens() и build_vocab()functions. Функция yield_tokens() выполняет предварительную обработку и токенизацию текста. Предварительная обработка просто удаляет все символы, которые не являются буквами или цифрами. Функция build_vocab() берет необработанный текст из Википедии, размечает его, а затем создает объект Vocab. Еще раз, я не буду проходиться по каждой строчке в этой функции. Ключевым выводом является то, что объект Vocab создается этой функцией.

Создание наших загрузчиков данных PyTorch

Следующим шагом в этом процессе является создание скипграмм с подвыборкой, а затем создание загрузчиков данных для нашей модели PyTorch. Чтобы узнать, почему загрузчики данных так важны для моделей PyTorch, которые обучаются на большом количестве данных, ознакомьтесь с документацией.

Этот класс, вероятно, самый сложный из всех, над которыми мы когда-либо работали, поэтому давайте подробно рассмотрим каждый метод, начиная с последнего метода, collate_skipgram(). Начнем с инициализации двух списков, batch_input и batch_output. Каждый из этих списков будет заполнен словарными индексами. В конечном счете, список batch_input будет содержать индексы для каждого целевого слова, а список batch_output будет содержать положительные индексы слов контекста для каждого целевого слова. Первый шаг — перебрать каждый текст в пакете и преобразовать все токены в соответствующий индекс словаря:

for text in batch:
    text_tokens = self.vocab.get_index(self.tokenizer(text))

Следующий шаг проверяет, чтобы текст был достаточно длинным для создания обучающих примеров. Вспомните предложение «python неизменно считается одним из самых популярных языков программирования». 11 слов. Если мы установим SKIPGRAM_N_WORDS равным 8, то документа длиной 11 слов будет недостаточно, поскольку мы не сможем найти слово в документе, перед которым 8 слов контекста, а также 8 слов контекста после него.

if len(text_tokens) < self.params.SKIPGRAM_N_WORDS * 2 + 1:
    continue

Затем мы создаем список целевого слова и список всех контекстных слов, окружающих целевое слово, гарантируя, что у нас всегда будет полный набор контекстных слов:

for idx in range(len(text_tokens) - self.params.SKIPGRAM_N_WORDS*2):
    token_id_sequence = text_tokens[
        idx : (idx + self.params.SKIPGRAM_N_WORDS * 2 + 1)
        ]
    input_ = token_id_sequence.pop(self.params.SKIPGRAM_N_WORDS)
    outputs = token_id_sequence

Теперь реализуем подвыборку. Мы ищем вероятность того, что целевое слово будет отброшено, учитывая его частоту, а затем удаляем его с этой вероятностью. Вскоре мы увидим, как мы вычисляем эти вероятности отбрасывания.

prb = random.random() 
del_pair = self.discard_probs.get(input_) 
if input_==0 or del_pair >= prb: 
    continue

Затем мы выполняем ту же процедуру подвыборки для контекстных слов, окружающих целевое слово, если на предыдущем шаге само целевое слово не было удалено. Наконец, мы добавляем полученные данные в списки batch_input и batch_output соответственно.

else:
    for output in outputs:
        prb = random.random()
        del_pair = self.discard_probs.get(output)
        if output==0 or del_pair >= prb:
            continue
        else:
            batch_input.append(input_)
            batch_output.append(output)

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

Это немного отличается от формулы, предложенной Миколовым и др., но достигает той же цели. Небольшая разница заключается в компоненте +t в знаменателе. Если +t исключить из знаменателя, слова с частотой, превышающей t, будут эффективно удалены из данных, поскольку значение квадратного корня будет больше 1. Это формула, реализованная в методе _create_discard_dict(), который создает словарь Python, где ключ — это индекс слова, а значение — вероятность его отбрасывания. Следующий вопрос: откуда берется t? Напомним, наш Word2VecParams имеет параметр T. В нашем коде этот параметр установлен на 85. Это означает, что мы находим частоту слов 85-го процентиля, а затем устанавливаем t на это значение. Это фактически делает вероятность случайной выборки слова с частотой в 85-м процентиле или выше близкой, но немного превышающей 0%. Это вычисление достигается методом _t() в классе SkipGram.

Создание нашего отрицательного массива выборки

Последний шаг перед определением модели PyTorch — создание массива отрицательных выборок. Цель высокого уровня — создать массив длиной 5 000 000 и заполнить его словарными индексами, пропорциональными частоте слов в словаре.

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

Метод sample() возвращает список списков, где количество списков, содержащихся во внешнем списке, равно количеству примеров в пакете, а количество отсчетов во внутренних списках — это количество отрицательных отсчетов для каждого примера, которое мы установили значение 50 в классе данных Word2VecParams.

Определение модели PyTorch

Наконец, мы можем построить модель word2vec в PyTorch. Идиоматический подход к построению нейронной сети PyTorch заключается в определении различных слоев сетевой архитектуры в конструкторе и прямой передаче данных через сеть в методе под названием forward(). К счастью, PyTorch абстрагируется от обратного прохода, который обновляет параметры модели, поэтому нам не нужно вычислять градиенты вручную.

Модели PyTorch всегда наследуются от torch.nn.Module class. Мы будем использовать слой встраивания PyTorch, который создает справочную таблицу векторов слов. Два слоя, которые мы определяем в нашей модели word2vec, — это self.t_embeddings,, представляющий собой целевые вложения, которые нам интересны для изучения, и self.c_embeddings, представляющий собой вторичную (фиолетовую) матрицу вложений на рисунке 2. Обе эти матрицы вложений инициализируются случайным образом.

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

Давайте представим, что у нас есть один обучающий пример, который мы проходим как пакет. В этом примере inputs содержит только слово, связанное с индексом 1. Первым шагом прямого прохода является поиск вложений этого слова в таблице self.t_embeddings. Затем мы используем метод .view(), чтобы изменить его форму, чтобы у нас был отдельный вектор для входных данных, которые мы передаем через сеть. В реальной реализации размер пакета равен 100. Метод .view() создает матрицу (1 x N) для каждого слова в каждом из 100 обучающих примеров в пакете. Рисунок 4 поможет читателю представить себе, что делают эти первые четыре строки метода forward().

Затем для каждого из входных данных нам нужно получить вложения контекстных слов. Для этого примера предположим, что фактическое контекстное слово связано с 8-м индексом таблицы self.c_embeddings, а отрицательное контекстное слово связано с 6-м индексом таблицы self.c_embeddings. В этом игрушечном примере мы используем только 1 отрицательный образец. На рис. 5 показано, что делают следующие две строки PyTorch.

Целевой вектор встраивания имеет размерность (1 x N), а наша матрица встраивания контекста имеет размерность (N x 2). Итак, наше матричное умножение дает матрицу размерности (1 x 2). На рис. 6 показано, что делают последние две строки метода forward().

Альтернативный способ осмыслить этот прямой проход с отрицательной выборкой — представить его как скалярное произведение между целевым словом и каждым словом в контексте — словом положительного контекста и всеми словами отрицательного контекста. В этом примере наше слово положительного контекста было 8-м словом в словаре, а слово отрицательного контекста было 6-м словом в словаре. Результирующий вектор (1 x 2) содержит логиты для двух контекстных слов. Поскольку мы знаем, что первое слово контекста является словом положительного контекста, а второе слово контекста — словом отрицательного контекста, значение должно быть большим для первого элемента и маленьким для второго элемента. Для этого мы будем использовать torch.nn.BCEWithLogitsLoss в качестве функции потерь. Мы вернемся к выбору функции потерь в следующем разделе.

Последние 3 метода в классе Model — это normalize_embeddings(), get_similar_words() и get_similarity(). Не вдаваясь в подробности каждого метода, метод normalize_embeddings() масштабирует каждое вложение слова таким образом, чтобы оно представляло собой единичный вектор (т. е. имело норму 1). Метод get_similar_words() возьмет слово и вернет список первых n наиболее похожих слов. В качестве метрики подобия используется косинусное сходство. Другими словами, этот метод будет возвращать слова, векторные представления которых наиболее близки к интересующему слову, измеряемому углом между двумя векторами. Наконец, get_similarity() возьмет два слова и вернет косинусное сходство между двумя векторами слов.

Создание трейнера

Последним шагом процесса является создание класса, который я назвал Trainer. Код класса Trainer выглядит следующим образом:

Этот класс организует весь ранее разработанный код для обучения модели. В этом классе есть методы train(), _train_epoch(), _validate_epoch() и test_testwords(). Метод train() — это метод, который мы вызовем, чтобы начать обучение модели. Он перебирает все эпохи и вызывает методы _train_epoch() и validate_epoch(). После того, как эпоха будет обучена и проверена, она распечатает тестовые слова, вызвав метод test_testwords(), чтобы мы могли визуально проверить, улучшаются ли вложения. Наиболее важными методами в этом классе являются методы _train_epoch() и _validate_epoch(). Эти методы очень похожи в том, что они делают, но имеют одно небольшое отличие. Давайте углубимся в метод _train_epoch().

Сначала мы сообщаем модели, что она находится в режиме обучения, используяself.model.train(). Это позволяет PyTorch заставить определенные типы сетевых слоев вести себя ожидаемым образом во время обучения. Эти типы слоев не реализованы в этой модели, но рекомендуется сообщать PyTorch, что модель обучается. Следующий шаг — перебрать каждый пакет, получить слова положительного и отрицательного контекста и отправить их на соответствующее устройство (ЦП или ГП). Другими словами, мы создаем тензор context, который получает доступ к пакетным данным из загрузчиков данных, которые мы создали с помощью класса SkipGrams, и объединяет их с отрицательными выборками, которые мы создали с помощью нашего класса NegativeSampler. Затем мы строим основной тензор истинности, y. Поскольку мы знаем, что первый элемент тензора context — это слово положительного контекста, а все последующие элементы — слова отрицательного контекста, мы создаем тензор y, где первый элемент тензора равен 1, а все последующие элементы — 0 .

Теперь, когда у нас есть входные данные, контекстные данные и метки истинности, мы можем выполнить прямой проход. Первый шаг — указать PyTorch установить для всех градиентов значение 0. В противном случае каждый раз, когда мы пропускаем пакет данных через модель, будет добавляться градиент, что нежелательно. Затем мы выполняем прямой проход со следующей строкой:

outputs = self.model(inputs, context)

Далее подсчитываются потери. Мы используем целевую функцию torch.nn.BCEWithLogitsLoss, так как у нас есть проблема бинарной классификации, где первый элемент тензора, y, равен 1, а следующие элементы равны 0. Для получения дополнительной информации об этой функции потерь обратитесь к документации. В блоге Себастьяна Рашки есть очень хороший обзор потери бинарной перекрестной энтропии в PyTorch, который также может дать дополнительную информацию.

loss = self.params.CRITERION(outputs, y)

PyTorch автоматически вычислит градиент во время этого вычисления потерь. Градиент содержит всю информацию, необходимую для внесения небольших корректировок в параметры модели и уменьшения потерь. Это автоматическое вычисление выполняется в строке:

loss.backward()

Небольшие обновления параметров модели выполняются в следующей строке. Обратите внимание, мы используем оптимизатор torch.optim.Adam. Адам — один из самых передовых алгоритмов выпуклой оптимизации, являющийся потомком стохастического градиентного спуска. Я не буду вдаваться в подробности об Адаме в этом посте, но учту, что это, как правило, один из самых быстрых алгоритмов оптимизации, поскольку он использует адаптивное обучение и градиентный спуск.

self.optimizer.step()

Метод _validate_epoch() идентичен методу _train_epoch(), за исключением того, что он не отслеживает градиенты и не обновляет параметры модели с помощью шага оптимизатора. Все это достигается с помощью строки with torch.no_grad(). Кроме того, метод _validate_epoch() использует только данные проверки, а не данные обучения.

Собираем все вместе

Ниже приведен блокнот word2vec целиком:

Я запустил этот ноутбук в экземпляре Google Colab с графическим процессором. Как видите, я тренировал модель в течение 5 эпох, каждая из которых занимала от 42 до 43 минут. Итак, весь ноутбук пробежал менее чем за 4 часа. Пожалуйста, не стесняйтесь поиграть с ним и оставлять любые отзывы или вопросы!

Полученные результаты

После тренировки чуть менее 4 часов наблюдайте за результатами во фрагменте выше. В дополнение к уменьшению потерь обратите внимание на то, как наиболее похожие слова улучшались по мере обучения эпох. После первой эпохи обучения в топ-5 слов, наиболее похожих на военный, вошли: by, for, хотя , было и любое. После 5 периодов обучения в топ-5 слов, наиболее похожих на военные, вошли: армия, силы, офицеры, руководство и солдаты. Это, наряду с уменьшением потерь, показывает, что вложения, которые изучает модель, точно представляют семантическое значение слов в словаре.

Спасибо за чтение! Пожалуйста, оставьте комментарий, если вы нашли это полезным или у вас есть какие-либо вопросы или проблемы.

Заключение

В заключение мы рассмотрели реализацию word2vec в PyTorch с отрицательной выборкой и подвыборкой. Эта модель позволяет преобразовывать слова в непрерывные векторы в n-мерном векторном пространстве. Эти векторы встраивания изучаются таким образом, что слова с похожим семантическим значением группируются близко друг к другу. При наличии достаточного количества обучающих данных и достаточного времени для обучения модель word2vec также может изучать синтаксические шаблоны в текстовых данных. Встраивание слов является основополагающим компонентом NLP и имеет решающее значение в более продвинутых моделях, таких как модели больших языков на основе Transformer. Полное понимание word2vec является чрезвычайно полезной основой для дальнейшего изучения НЛП!

Все изображения, если не указано иное, принадлежат автору.

Рекомендации

[1] Т. Миколов, К. Чен, Г. Коррадо и Дж. Дин, Эффективная оценка представлений слов в векторном пространстве (2013), Google Inc.

[2] Т. Миколов, И. Суцкевер,К. Чен, Г. Коррадо и Дж. Дин, Распределенные представления слов и фраз и их композиционность (2013), Google Inc.

[3] С. Рашка, Извлеченные потери (2022), https://sebastianraschka.com