Изучите Word2Vec, реализовав его в тензорном потоке

Hi!

Я считаю, что лучший способ понять алгоритм - это реализовать его. Итак, в этой статье я научу вас встраиванию слов, реализуя его в Tensor Flow .

Идея, лежащая в основе этой статьи, состоит в том, чтобы избежать всех вступлений и обычной болтовни, связанной с встраиванием слов / word2vec, и сразу перейти к сути вещей. Таким образом, большая часть примеров король-мужчина-женщина-королева будет пропущена.

Как мы делаем эти вложения?

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

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

Как работает word2vec:

Идея word2vec заключается в следующем:

  1. Возьмем трехслойную нейронную сеть. (1 входной слой + 1 скрытый слой + 1 выходной слой)
  2. Накормите его словом и научите предсказывать соседнее слово.
  3. Удалите последний (выходной слой) и оставьте входной и скрытый слои.
  4. Теперь введите слово из словаря. Вывод на скрытом уровне - это «вложение слова» входного слова.

Вот и все! Простое выполнение этой простой задачи позволяет нашей сети изучать интересные представления слов.

Давайте начнем реализовывать это, чтобы конкретизировать это понимание.

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

Это необработанный текст, над которым мы будем работать:

(Я намеренно разделил точки и избегал их в конце для упрощения. Как только вы поймете, не стесняйтесь использовать токенизатор)

import numpy as np
import tensorflow as tf
corpus_raw = 'He is the king . The king is royal . She is the royal  queen '
# convert to lower case
corpus_raw = corpus_raw.lower()

Нам нужно преобразовать это в пару ввода-вывода, чтобы при вводе слова оно предсказывало, что соседние слова: n слов до и после него, где n - параметр window_size Здесь удобный пример из этого удивительного поста Криса МакКормика на word2vec.

Примечание. Если слово стоит в начале или в конце предложения, окно игнорирует внешние слова.

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

words = []
for word in corpus_raw.split():
    if word != '.': # because we don't want to treat . as a word
        words.append(word)
words = set(words) # so that all duplicate words are removed
word2int = {}
int2word = {}
vocab_size = len(words) # gives the total number of unique words
for i,word in enumerate(words):
    word2int[word] = i
    int2word[i] = word

Эти словари позволяют нам:

print(word2int['queen'])
-> 42 (say)
print(int2word[42])
-> 'queen'

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

# raw sentences is a list of sentences.
raw_sentences = corpus_raw.split('.')
sentences = []
for sentence in raw_sentences:
    sentences.append(sentence.split())

Это даст нам список предложений, где каждое предложение представляет собой список слов.

print(sentences)
-> [['he', 'is', 'the', 'king'], ['the', 'king', 'is', 'royal'], ['she', 'is', 'the', 'royal', 'queen']]

Теперь мы сгенерируем наши обучающие данные:

(Это может быть трудно читать на носителе. См. ссылку на код)

data = []
WINDOW_SIZE = 2
for sentence in sentences:
    for word_index, word in enumerate(sentence):
        for nb_word in sentence[max(word_index - WINDOW_SIZE, 0) : min(word_index + WINDOW_SIZE, len(sentence)) + 1] : 
            if nb_word != word:
                data.append([word, nb_word])

Это в основном дает список слов, пар слов. (мы рассматриваем размер окна 2)

print(data)
[['he', 'is'],
 ['he', 'the'],
 ['is', 'he'],
 ['is', 'the'],
 ['is', 'king'],
 ['the', 'he'],
 ['the', 'is'],
 ['the', 'king'],
.
.
.
]

У нас есть данные по обучению. Но он должен быть представлен таким образом, чтобы компьютер мог его понять, то есть с помощью чисел. Вот где наш word2int изречение пригодится.

Давайте сделаем еще один шаг и преобразуем эти числа в одни горячие векторы.

i.e., 
say we have a vocabulary of 3 words : pen, pineapple, apple
where 
word2int['pen'] -> 0 -> [1 0 0]
word2int['pineapple'] -> 1 -> [0 1 0]
word2int['apple'] -> 2 -> [0 0 1]

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

# function to convert numbers to one hot vectors
def to_one_hot(data_point_index, vocab_size):
    temp = np.zeros(vocab_size)
    temp[data_point_index] = 1
    return temp
x_train = [] # input word
y_train = [] # output word
for data_word in data:
    x_train.append(to_one_hot(word2int[ data_word[0] ], vocab_size))
    y_train.append(to_one_hot(word2int[ data_word[1] ], vocab_size))
# convert them to numpy arrays
x_train = np.asarray(x_train)
y_train = np.asarray(y_train)

Итак, теперь у нас есть x_train и y_train:

print(x_train)
->
[[ 0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.]]

Оба имеют форму:

print(x_train.shape, y_train.shape)
->
(34, 7) (34, 7)
# meaning 34 training points, where each point has 7 dimensions

Сделайте модель тензорного потока

# making placeholders for x_train and y_train
x = tf.placeholder(tf.float32, shape=(None, vocab_size))
y_label = tf.placeholder(tf.float32, shape=(None, vocab_size))

Как видно на приведенной выше диаграмме, мы берем наши обучающие данные и конвертируем их во встроенное представление.

EMBEDDING_DIM = 5 # you can choose your own number
W1 = tf.Variable(tf.random_normal([vocab_size, EMBEDDING_DIM]))
b1 = tf.Variable(tf.random_normal([EMBEDDING_DIM])) #bias
hidden_representation = tf.add(tf.matmul(x,W1), b1)

Затем мы берем то, что у нас есть во встроенном измерении, и делаем прогноз относительно соседа. Чтобы сделать прогноз, мы используем softmax.

W2 = tf.Variable(tf.random_normal([EMBEDDING_DIM, vocab_size]))
b2 = tf.Variable(tf.random_normal([vocab_size]))
prediction = tf.nn.softmax(tf.add( tf.matmul(hidden_representation, W2), b2))

Итак, подведем итог:

input_one_hot  --->  embedded repr. ---> predicted_neighbour_prob
predicted_prob will be compared against a one hot vector to correct it.

Теперь осталось его натренировать:

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init) #make sure you do this!
# define the loss function:
cross_entropy_loss = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(prediction), reduction_indices=[1]))
# define the training step:
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy_loss)
n_iters = 10000
# train for n_iter iterations
for _ in range(n_iters):
    sess.run(train_step, feed_dict={x: x_train, y_label: y_train})
    print('loss is : ', sess.run(cross_entropy_loss, feed_dict={x: x_train, y_label: y_train}))

На тренировке вы получите отображение проигрыша:

loss is :  2.73213
loss is :  2.30519
loss is :  2.11106
loss is :  1.9916
loss is :  1.90923
loss is :  1.84837
loss is :  1.80133
loss is :  1.76381
loss is :  1.73312
loss is :  1.70745
loss is :  1.68556
loss is :  1.66654
loss is :  1.64975
loss is :  1.63472
loss is :  1.62112
loss is :  1.6087
loss is :  1.59725
loss is :  1.58664
loss is :  1.57676
loss is :  1.56751
loss is :  1.55882
loss is :  1.55064
loss is :  1.54291
loss is :  1.53559
loss is :  1.52865
loss is :  1.52206
loss is :  1.51578
loss is :  1.50979
loss is :  1.50408
loss is :  1.49861
.
.
.

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

Давайте посмотрим на них:

print(sess.run(W1))
print('----------')
print(sess.run(b1))
print('----------')
->
[[-0.85421133  1.70487809  0.481848   -0.40843448 -0.02236851]
 [-0.47163373  0.34260952 -2.06743765 -1.43854153 -0.14699034]
 [-1.06858993 -1.10739779  0.52600187  0.24079895 -0.46390489]
 [ 0.84426647  0.16476244 -0.72731972 -0.31994426 -0.33553854]
 [ 0.21508843 -1.21030915 -0.13006891 -0.24056002 -0.30445012]
 [ 0.17842589  2.08979321 -0.34172744 -1.8842833  -1.14538431]
 [ 1.61166084 -1.17404735 -0.26805425  0.74437028 -0.81183684]]
----------
[ 0.57727528 -0.83760375  0.19156453 -0.42394346  1.45631313]
----------

Почему одни горячие векторы?

Когда мы умножаем один горячий вектор на W1, мы в основном получаем доступ к строке W1, которая фактически является встроенным представлением слова, представленного входным одним горячим вектором. Итак, W1, по сути, действует как справочная таблица.

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

vectors = sess.run(W1 + b1)

# if you work it out, you will see that it has the same effect as running the node hidden representation
print(vectors)
->
[[-0.74829113 -0.48964909  0.54267412  2.34831429 -2.03110814]
 [-0.92472583 -1.50792813 -1.61014366 -0.88273793 -2.12359881]
 [-0.69424796 -1.67628145  3.07313657 -1.14802659 -1.2207377 ]
 [-1.7077738  -0.60641652  2.25586247  1.34536338 -0.83848488]
 [-0.10080346 -0.90931684  2.8825531  -0.58769202 -1.19922316]
 [ 1.49428082 -2.55578995  2.01545811  0.31536022  1.52662396]
 [-1.02735448  0.72176981 -0.03772151 -0.60208392  1.53156447]]

Если мы хотим представить «королеву», все, что нам нужно сделать, это:

print(vectors[ word2int['queen'] ])
# say here word2int['queen'] is 2
-> 
[-0.69424796 -1.67628145  3.07313657 -1.14802659 -1.2207377 ]

Итак, что мы можем сделать с этими красивыми красивыми векторами?

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

def euclidean_dist(vec1, vec2):
    return np.sqrt(np.sum((vec1-vec2)**2))

def find_closest(word_index, vectors):
    min_dist = 10000 # to act like positive infinity
    min_index = -1
    query_vector = vectors[word_index]
    for index, vector in enumerate(vectors):
        if euclidean_dist(vector, query_vector) < min_dist and not np.array_equal(vector, query_vector):
            min_dist = euclidean_dist(vector, query_vector)
            min_index = index
    return min_index

Теперь мы запросим эти векторы с помощью «король», «королева» и «королевский».

print(int2word[find_closest(word2int['king'], vectors)])
print(int2word[find_closest(word2int['queen'], vectors)])
print(int2word[find_closest(word2int['royal'], vectors)])
->
queen
king
he

Интересно, что наше встраивание узнало, что

king is closest to queen
queen is closest to king
royal is closest to he

Третий - за счет нашего корпуса (все еще неплохой). Чем больше корпус, тем лучше результаты. (Примечание: из-за случайной инициализации весов вы можете получить разные результаты. При необходимости выполните несколько раз)

Построим им векторы!

Сначала давайте уменьшим размерность с 5 до 2 с помощью нашей любимой техники уменьшения размерности: tSNE (черти!)

from sklearn.manifold import TSNE
model = TSNE(n_components=2, random_state=0)
np.set_printoptions(suppress=True)
vectors = model.fit_transform(vectors)

Затем нам нужно нормализовать результаты, чтобы мы могли более комфортно их просматривать в matplotlib.

from sklearn import preprocessing
normalizer = preprocessing.Normalizer()
vectors =  normalizer.fit_transform(vectors, 'l2')

Наконец, мы построим 2D нормализованные векторы

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
for word in words:
    print(word, vectors[word2int[word]][1])
    ax.annotate(word, (vectors[word2int[word]][0],vectors[word2int[word]][1] ))
plt.show()

Ух ты! she близок к queen, а king одинаково удален от royal и queen. Нам нужен больший корпус, чтобы увидеть некоторые из более сложных отношений.

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

Почему это происходит?

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

Чтобы сформировать эти представления, сеть использует контексты / соседей. В нашем корпусе king и royal отображаются как соседи, а royal и queen - как соседи.

Почему прогнозирование соседей - это задача?

Ну и другие задачи тоже успевают составить хорошее представление. Предсказание того, является ли слово действительной n-граммой, как показано здесь, также может привести к хорошим векторам!

Мы пытались угадать соседнее слово по заданному слову. Это известно как модель пропуска грамма. Мы могли бы использовать в качестве входных данных соседние слова среднего слова и попросить сеть предсказать среднее слово. Это известно как модель непрерывного мешка слов.

Дальнейшее чтение:

Это отнюдь не полное понимание word2vec. Часть красоты w2v заключается в двух модификациях того, о чем я только что говорил. Эти:

  • Отрицательная выборка
  • Иерархический Softmax

Отрицательная выборка: он предполагает, что вместо обратного распространения всех нулей в правильном выходном векторе (для размера словаря 10mill есть 10mill минус 1 ноль), мы просто распространяем обратно несколько из них (скажем, 14).

Иерархический Softmax: Расчет softmax для словаря 10mill требует значительных затрат времени и вычислений. Иерархический Softmax предлагает более быстрый способ вычисления с использованием деревьев Хаффмана.

В интересах простоты поста я старался не вдаваться в подробности. Я определенно рекомендую прочитать об этом подробнее.

Заключительные замечания:

  • Векторы слов супер крутые
  • Не используйте этот код тензорного потока для реального использования. Просто чтобы понять. Используйте библиотеку типа gensim

Надеюсь, это помогло кому-то лучше понять этих красоток. Если да, дайте мне знать!

Если я допустил ошибку, пожалуйста дайте мне знать.

Я хотел бы связаться с вами через twitter, linkedin или / и email.

До свидания!