Код для word2vec в Numpy и Theano

Код для word2vec в Numpy. Часть 1

Здравствуйте и вновь добро пожаловать на занятия по теме «Обработка естественного языка: глубокое обучение на языке Python, часть 6».

В этой статье мы напишем код для word2vec в Numpy. Если вы не хотите писать код сами или хотите свериться с моим, перейдите по адресу https://github.com/lazyprogrammer/machine_learning_examples и зайдите в папку nlp_class2. Соответствующий файл называется word2vec.py.

Итак,начинаем с импорта библиотек. При этом мы сразу импортируем и Theano, поскольку следующей будет именно версия в Theano, хотя сейчас она не нужна. Кроме того, мывоспользуемся функцией получения данных Википедии из курса по рекуррентнымнейронным сетям («Глубокое обучение, часть 5»).

import json

import re

import numpy as np

import theano

import theano.tensor as T

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from datetime import datetime

from util import find_analogies as _find_analogies

import os

import sys

sys.path.append(os.path.abspath(‘..’))

from rnn_class.util import get_wikipedia_data

ВNumpy нет функции сигмоиды, так что нам придётся создатьсобственную.

def sigmoid(x):

    return 1 /(1 + np.exp(-x))

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

def init_weights(shape):

    returnnp.random.randn(*shape).astype(np.float32) / np.sqrt(sum(shape))

Создадим класс Model.

class Model:

    def __init__(self, D, V, context_sz):

        self.D =D

        self.V =V

        self.context_sz= context_sz

Создадимтакже вспомогательную функцию _get_pnw для вычисления Pn(w) – распределения вероятностей для отрицательногосэмплирования. Не забывайте про возведение в степень 3/4.

    def _get_pnw(self, X):

        word_freq = {}

       word_count = sum(len(x) for x in X)

        for x inX:

            forxj in x:

               if xj not in word_freq:

                   word_freq[xj] = 0

               word_freq[xj] += 1

        self.Pnw= np.zeros(self.V)

        for j inxrange(2, self.V):

           self.Pnw[j] = (word_freq[j] / float(word_count))**0.75

       assert(np.all(self.Pnw[2:] > 0))

        return self.Pnw

Следующаяфункция, которую мы определим – _get_negative_samplesдля получения отрицательных примеров. Это несколько сложный процесс, поэтому онтребует своей функции.

    def _get_negative_samples(self, context, num_neg_samples):

        saved = {}

        forcontext_idx in context:

           saved[context_idx] = self.Pnw[context_idx]

           self.Pnw[context_idx] = 0

       neg_samples = np.random.choice(

            xrange(self.V),

            size=num_neg_samples

            replace=False,

            p=self.Pnw / np.sum(self.Pnw),

        )

        for j,pnwj in saved.iteritems():

           self.Pnw[j] = pnwj

       assert(np.all(self.Pnw[2:] > 0))

        returnneg_samples

Далее идёт функция fit.

    def fit(self, X, num_neg_samples=10, learning_rate=10e-5,mu=0.99, reg=0.1, epochs=10):

        N =len(X)

        V =self.V

        D =self.D

       self._get_pnw(X)

Инициируемвесовые коэффициенты и изменения импульса.

        self.W1 = init_weights((V, D))

        self.W2= init_weights((D, V))

        dW1 =np.zeros(self.W1.shape)

        dW2 =np.zeros(self.W2.shape)

Унас будет две функции затрат – одна для каждого примера и одна для цикла вцелом.

        costs = []

       cost_per_epoch = []

       sample_indices = range(N)

        for i inxrange(epochs):

            t0 =datetime.now()

           sample_indices = shuffle(sample_indices)

           cost_per_epoch_i = []

            forit in xrange(N):

               j = sample_indices[it]

               x = X[j]

               # too short to do 1 iteration, skip

               if len(x) < 2 * self.context_sz + 1:

                   continue

               cj = []

               n = len(x)

               for jj in xrange(n):

                    Z = self.W1[x[jj],:]

                    start = max(0, jj – self.context_sz)

                    end = min(n, jj + 1 + self.context_sz)

                    context =np.concatenate([x[start:jj], x[(jj+1):end]])

                    context = np.array(list(set(context)),dtype=np.int32)

                   posA = Z.dot(self.W2[:,context])

                    pos_pY = sigmoid(posA)

                    neg_samples =self._get_negative_samples(context, num_neg_samples)

                    negA =Z.dot(self.W2[:,neg_samples])

                    neg_pY = sigmoid(-negA)

                    c = -np.log(pos_pY).sum() –np.log(neg_pY).sum()

                    cj.append(c / (num_neg_samples +len(context)))

                    pos_err = pos_pY – 1

                    dW2[:, context] = mu*dW2[:,context] – learning_rate*(np.outer(Z, pos_err) + reg*self.W2[:, context])

                    neg_err = 1 – neg_pY

                    dW2[:, neg_samples] = mu*dW2[:,neg_samples] – learning_rate*(np.outer(Z, neg_err) + reg*self.W2[:,neg_samples])

                   self.W2[:, context] += dW2[:, context]

                   self.W2[:, neg_samples] += dW2[:, neg_samples]

                    gradW1 = pos_err.dot(self.W2[:,context].T) + neg_err.dot(self.W2[:, neg_samples].T)

                    dW1[x[jj], :] = mu*dW1[x[jj], :] –learning_rate*(gradW1 + reg*self.W1[x[jj], :])

                    self.W1[x[jj], :] += dW1[x[jj],:]

               cj = np.mean(cj)

               cost_per_epoch_i.append(cj)

               costs.append(cj)

               if it % 500 == 0:

                   sys.stdout.write(“epoch: %d j: %d/ %d cost: %f\r” % (i, it, N,cj))

                   sys.stdout.flush()

           epoch_cost = np.mean(cost_per_epoch_i)

            cost_per_epoch.append(epoch_cost)

           print “time to complete epoch %d:” % i, (datetime.now() – t0),“cost:”, epoch_cost

       plt.plot(costs)

       plt.title(“Numpy costs”)

       plt.show()

       plt.plot(cost_per_epoch)

        plt.title(“Numpycost at each epoch”)

        plt.show()

Далеенапишем функцию для сохранения наших векторных представлений.

    def save(self, fn):

        arrays =

[self.W1, self.W2]

       np.savez(fn, *arrays)

def main():

       sentences, word2idx = get_wikipedia_data(n_files=50, n_vocab=2000)

    with open(‘w2v_word2idx.json’, ‘w’) as f:

       json.dump(word2idx, f)

    V =len(word2idx)

    model =Model(50, V, 5)

   model.fit(sentences, learning_rate=10e-4,mu=0, epochs=0)

   model.save(‘w2v_model.npz’)

Следующийэтап – функция для нахождения аналогий.

def find_analogies(w1,w2, w3, concat=True, we_file=’w2v_model.npz’,w2i_file=’w2v_word2idx.json’):

    npz =np.load(we_file)

    W1 =npz[‘arr_0’]

    W2 = npz[‘arr_1’]

    with open(w2i_file) as f:

        word2idx= json.load(f)

    V =len(word2idx)

    if concat:

        We =np.hstack([W1, W2.T])

        print“We.shape:”, We.shape

        assert(V== We.shape[0])

    else:

        We = (W1+ W2.T) / 2

    _find_analogies(w1, w2, w3, We, word2idx)

Код для word2vec в Numpy. Часть 2

Функцияmain. Попробуем словесные аналогии для ряда слов.

if __name__ == ‘__main__’:

    main()

    for concatin (True, False):

        print“*** concat:”, concat

       find_analogies(‘king’, ‘man’, ‘woman’, concat=concat)

       find_analogies(‘france’, ‘paris’, ‘london’, concat=concat)

       find_analogies(‘france’, ‘paris’, ‘rome’, concat=concat)

        find_analogies(‘paris’, ‘france’, ‘italy’, concat=concat)

Преобразование последовательности индексов слов в последовательность слов-векторов

В этом подразделе я объясню одну маленькую деталь кода, которая может вызвать недоумение.

Унас есть матрица векторных представлений слов We с размерностью VxD, и нам нужнополучить последовательность слов-векторов, представляющих предложение, – а этовектор TxD. Но нам нужнообновлять векторные вложения слов с помощью алгоритма обратногораспространения. Следовательно, матрица TxD, получаемая после присваиваниявекторов-слов, не может служить входными данными для нейронной сети. Векторныепредставления слов должны быть частью нейронной сети, чтобы их можно былообновлять с помощью градиентного спуска вместе со всеми остальными весовымикоэффициентами. Это означает, что входными данными для нейронной сети будеттолько последовательность индексов слов, причём индексы будут соответствовать тому,как мы построили наш словарь индексов слов. Это экономит много места, посколькутеперь мы можем представить каждый вход в виде вектора целых чисел размерности Tx1, а не матрицы вещественных чисел размерности TxD.

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

word_vectors = []

for index in input_sequence:

    word_vector= We[index, :]

   word_vectors.append(word_vector)

return word_vectors

Сточки зрения математики, мы получаем слово-вектор путём умножения вектораиндекса слова, полученного методом прямого кодирования, на матрицу векторныхпредставлений слов. Мы умножаем вектор размерности 1xV на матрицуразмерности VxD и в результатеполучаем ожидаемый вектор размерности 1xD. Но посколькувектор, полученный прямым кодированием, у себя может иметь только одну единицу,а остальные числа должны быть нулями, то мы можем упростить задачу.

Ключ– в понимании того, что умножение векторных представлений на вектор горячегокодирования даёт строку, где и размещается этот вектор векторного представленияслова. Следовательно, можно сказать, что каждая строка матрицы векторныхпредставлений слов представляет соответствующее слово. Это должно бытьдостаточно очевидно, учитывая, что матрица векторных представлений слов имеет V строк. К томуже Theano и Numpy позволяют оченьлегко получить строку матрицы, надо просто проиндексировать её как массив.Более того, они позволяют нам индексировать массивы с помощью массивов, как вы,вероятно, уже заметили из многих моих примеров кода.

Пусть у нас есть матрица векторных представлений слов, в которой представляются слова «я», «люблю», «мороженное», «и», «пирожное», и мы хотим представить предложение «Я люблю мороженное». Тогда входом в нейронную сеть будет X = [0, 1, 2]. Тогда We для получения слов-векторов для предложения «Я люблю мороженное» будет матрицей размерности 3xD.

Еслимы захотим представить предложение «Я люблю пирожное», то входом в нейроннуюсеть будет X = [0, 1, 4], а в результате получим также матрицуразмерности 3xD. Если же мыпредставляем предложение «Пирожное я люблю», то входом в нейронную сеть будет X = [4, 0, 1] и так далее.

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


В английском языке это будет четыре слова, поэтому входом в нём будет [0, 1, 2, 3], как на слайде.

Как обновить только часть общих переменных Theano

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

Что будет, если мы попытаемся использовать существующие функции обновления Theano? У нас есть два весовых коэффициента – от входного слоя к скрытому и от скрытого слоя к исходящему. Обозначим их через W1 и W2. Пусть мы обновляем их обоих обычным для Theano способом, получаем градиент относительно функции затрат и подставляем выражение для градиентного спуска в обучающую функцию Theano. И тут мы сталкиваемся с проблемой, поскольку если мы рассмотрим W1, то увидим, что обновляться должно только слово-вектор для входного слова. Но при использовании градиентного спуска в Theano обновляться будут и все остальные слова-векторы, поскольку они являются частью W1, – и это несмотря на то, что функция затрат от них не зависит, а значит, градиент будет равен нулю.

Аналогичнаяситуация с W2. Обновляться должны только слова контекста иотрицательного сэмплирования. Предположим, размер контекста равен 10 и у насесть 10 отрицательных примеров, а размер словаря равен 2000. Тогда обновлятьсябудут 2000 векторов, хотя нужно только 20.

Ксчастью, Theano предоставляет возможность обновлять лишь частьобщих переменных. Вы увидите, как это делается, в коде, но сперва полезноразобрать несколько простых примеров. Делается это с помощью функций inc_subtensor и set_subtensor. Как можнопонять уже из названий, inc_subtensor служит для увеличения субтензора иэквивалентно оператору +=, а set_subtensor – для присваивания значения, чтоэквивалентно оператору присваивания. В частности, предположим, что мы хотимзаписать выражение

w[i] = w[i] – lr*T.grad(cost, w[i])

Дляэтого можно использовать функцию inc_subtensor, поскольку это одна из форм +=:

w = T.inc_subtensor(w[i], -lr*T.grad(cost, w[i]))

Важныйнюанс, который надо запомнить, – полная общая переменная находится слева.

Теперьпример с set_subtensor. Предположим,по какой-то причине нам нужна полная форма функции затрат, в которой логарифмвероятности p(y|x) умножается наматрицу целевых индикаторов. Пусть у нас есть всего один пример, так что имеемвектор размерности 1xD.В этом случае мы вначале создаём вектор размерности K, состоящий из нулей. Затем устанавливаем элемент,соответствующий метке, равным единице, используя функцию set_subtensor. И наконецумножаем целевые переменные на логарифм p(y|x) для полученияфункции затрат:

targets = T.zeros(K)

targets = T.set_subtensor(targets[label], 1)

cost = -targets * T.log(py_x)

Рассмотримчуть более сложный пример. У нас всё ещё нет всей необходимой информации, чтобыобновлять весовые коэффициенты в модели word2vec, в частности, это касается весовых коэффициентовмежду скрытым и исходящим слоями. Почему? Ранее я показал вам, как однократнопроиндексировать общую переменную для обновления. Theanoможет принимать скаляры или векторы в качестве индексов тензоров. Но проблемавсё равно остаётся. Состоит она в том, что у нас нет лишь одного индекса дляисходящих весовых коэффициентов, у нас их два. Один вектор целых чиселпредставляет контекст, а второй – отрицательные примеры. Есть два очевидных, нонеправильных решения этой проблемы, которые могут прийти вам в голову. Вначалевы можете подумать о простом объединении контекста и отрицательных примеров ииспользовать это для индексации исходящих весовых коэффициентов:

w2idx = T.concatenate([context, neg_samples])

Ноэто не сработает, поскольку Theano «пожалуется»,что ваши входные данные не являются частью графа Theano.Лично я полагаю, что это просто ошибка, поскольку формально они являются частьюокончательного графа и лишь объединяются для создания новой переменной. Тем неменее, это не работает.

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

gc = T.grad(cost, W2[:, context])

gn = T.grad(cost, W2[:, neg_samples])

updates = [

    …

    (W2,T.inc_subtensor(W2[:, context], -lr*gc)),

    (W2,T.inc_subtensor(W2[:, neg_samples], -lr*gn)),

]

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

Посколькуэти два очевидных решения в действительности не работают, возникает вопрос: ачто работает?

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

W2_update = T.inc_subtensor(T.inc_subtensor(W2[:,context], -lr*gc) [:, neg_samples], -lr*gn)

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

Теперь у вас есть весь инструментарий, чтобы обновить W1 и W2, с использованием Theano вместо Numpy. Попробуйте сделать всё сами, и если не получится – сверьте свой код с моим, чтобы понять, где вы ошиблись.

Код для word2vec в Theano

В завершении данной темы, мы продолжим написание кода для word2vec, в частности, напишем функцию fit, используя Theano вместо Numpy.

Назовём новую функцию fit, чтобы знать, что это Theano.

    def fitt(self, X, num_neg_samples=10, learning_rate=10e-5, mu=0.99, reg=0.1, epochs=10):

        N =len(X)

        V =self.V

        D =self.D

       self._get_pnw(X)

Инициируемвесовые коэффициенты и изменения импульса.

        # initialize weights and momentum changes

        W1 =init_weights((V, D))

        W2 =init_weights((D, V))

        W1 =theano.shared(W1)

        W2 =theano.shared(W2)

Следующее– определение входов и выходов.

        thInput = T.iscalar(‘input_word’)

       thContext = T.ivector(‘context’)

       thNegSamples = T.ivector(‘negative_samples’)

       W1_subset = W1[thInput]

       W2_psubset = W2[:, thContext]

       W2_nsubset = W2[:, thNegSamples]

       p_activation = W1_subset.dot(W2_psubset)

        pos_pY =T.nnet.sigmoid(p_activation)

       n_activation = W1_subset.dot(W2_nsubset)

        neg_pY =T.nnet.sigmoid(-n_activation)

        cost =-T.log(pos_pY).sum() – T.log(neg_pY).sum()

Далее– определение градиентов. С W1 всё просто, авот с W2 несколько сложнее.

        W1_grad = T.grad(cost, W1_subset)

        W2_pgrad= T.grad(cost, W2_psubset)

        W2_ngrad= T.grad(cost, W2_nsubset)

       W1_update = T.inc_subtensor(W1_subset, -learning_rate*W1_grad)

       W2_update = T.inc_subtensor(

           T.inc_subtensor(W2_psubset, -learning_rate*W2_pgrad)[:,thNegSamples],-learning_rate*W2_ngrad)

        updates= [(W1, W1_update), (W2, W2_update)]

        train_op= theano.function(

            inputs=[thInput, thContext,thNegSamples],

            outputs=cost,

            updates=updates,

            allow_input_downcast=True,

        )

        costs =[]

       cost_per_epoch = []

       sample_indices = range(N)

        for i inxrange(epochs):

            t0 =datetime.now()

           sample_indices = shuffle(sample_indices)

           cost_per_epoch_i = []

            forit in xrange(N):

               j = sample_indices[it]

               x = X[j]

               if len(x) < 2 * self.context_sz + 1:

                   continue

               cj = []

               n = len(x)

               for jj in xrange(n):

                   start = max(0, jj –self.context_sz)

                   end = min(n, jj + 1 + self.context_sz)

                   context = np.concatenate([x[start:jj], x[(jj+1):end]])

                   context = np.array(list(set(context)), dtype=np.int32)

                    neg_samples =self._get_negative_samples(context, num_neg_samples)

                   c = train_op(x[jj], context, neg_samples)

                   cj.append(c / (num_neg_samples + len(context)))

               cj = np.mean(cj)

                cost_per_epoch_i.append(cj)

               costs.append(cj)

               if it % 100 == 0:

                   sys.stdout.write(“epoch: %d j: %d/ %d cost: %f\r” % (i, it, N,cj))

                   sys.stdout.flush()

           epoch_cost = np.mean(cost_per_epoch_i)

           cost_per_epoch.append(epoch_cost)

           print “time to complete epoch %d:” % i, (datetime.now() – t0),“cost:”, epoch_cost

        self.W1= W1.get_value()

        self.W2= W2.get_value()

       plt.plot(costs)

       plt.title(“Theano costs”)

       plt.show()

       plt.plot(cost_per_epoch)

       plt.title(“Theano cost at each epoch”)

        plt.show()

Андрей Никитенко
Андрей Никитенко
Задать вопрос эксперту
Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Share via
Copy link