Содержание страницы
Код для GloVe. Градиентный спуск в Numpy
Здравствуйте и вновь добро пожаловать на занятия по теме «Обработка естественного языка: глубокое обучение на языке Python, часть 6».
В этой статье мы напишем код для алгоритма метода глобальных векторов, используя Numpy, Theano и для градиентного спуска, а так же сделаем визуализацию аналогий с помощью t-SNE.
Начинаемс импорта библиотек, включая и Theano, хотя пока чтоона нам не нужна. Импортируем также функции get_wikipedia_dataи find_analogies из нашей файла word2vec.
import numpy as np
import json
import os
import theano
import theano.tensor as T
import matplotlib.pyplot as plt
from datetime import datetime
from sklearn.utils import shuffle
from word2vec import get_wikipedia_data, find_analogies
Итак,определим нашу модель глобальных векторов. Вход у нас тот же, что и в word2vec – размерностьвекторных представлений, размер словаря и размур контекста.
class Glove:
def __init__(self, D, V, context_sz):
self.D =D
self.V =V
self.context_sz = context_sz
Пишем функцию fit.
def fit(self, sentences, cc_matrix=None, learning_rate=10e-5, reg=0.1,xmax=100, alpha=0.75, epochs=10, gd=False, use_theano=True):
t0 =datetime.now()
V =self.V
D =self.D
if notos.path.exists(cc_matrix):
X =np.zeros((V, V))
N =len(sentences)
print “number of sentences to process:”, N
it =0
forsentence in sentences:
it += 1
if it % 10000 == 0:
print “processed”, it, “/”, N
n= len(sentence)
Пишемцикл по каждому токену в предложении. Обратите внимание – i в данном случае не индекс слова! i – индекс индекса слова, то есть позиция слова впредложении. Вот когда делается предложение из i– тогда это индекс слова. Убедитесь, что вы понимаете разницу.
fori inxrange(n):
# i is not the word index!!!
# j is not the word index!!!
wi = sentence[i]
start = max(0, i – self.context_sz)
end = min(n, i + self.context_sz)
# start and end tokens
if i – self.context_sz < 0:
points = 1.0 / (i + 1)
X[wi,0] += points
X[0,wi] += points
if i + self.context_sz > n:
points = 1.0 / (n – i)
X[wi,1] += points
X[1,wi] += points
Переходимк левой части контекста. При этом важно не забывать, что контекст имеетсвойство симметрии: если слово i находится в контексте слова j, то и слово j находится вконтексте слова i.
# left side
for j in xrange(start, i):
wj = sentence[j]
points = 1.0 / (i – j)
X[wi,wj] += points
X[wj,wi] += points
Ито же самое с правой частью контекста.
# right side
for j in xrange(i + 1, end):
wj = sentence[j]
points = 1.0 / (j – i)
X[wi,wj] += points
X[wj,wi] += points
Сохраняемнаше X. Название файла – cc_matrix.
np.save(cc_matrix, X)
else:
X =np.load(cc_matrix)
ВычисляемXmax и взвешиваем значения по ужерассматривавшейся формуле.
print “max value in X:”, X.max()
fX =np.zeros((V, V))
fX[X< xmax] = (X[X < xmax] / float(xmax))** alpha
fX[X>= xmax] = 1
logX =np.log(X + 1)
print“time to build co-occurrence matrix:”, (datetime.now() – t0)
Следующее– инициируем весовые коэффициенты.
# initialize weights
W = np.random.randn(V, D) / np.sqrt(V + D)
b =np.zeros(V)
U =np.random.randn(V, D) / np.sqrt(V + D)
c =np.zeros(V)
mu =logX.mean()
Далеемы оставим условный оператор для версии с Theano.Сейчас мы этим заниматься не будем.
if gd and use_theano:
pass
Теперьмы можем переходить к основному циклу.
costs = []
sentence_indexes = range(len(sentences))
forepoch in xrange(epochs):
delta = W.dot(U.T) + b.reshape(V, 1) + c.reshape(1, V) + mu – logX
cost= ( fX * delta * delta ).sum()
costs.append(cost)
print “epoch:”, epoch, “cost:”, cost
Оставимместо для кода в Theano.
if gd:
if use_theano:
pass
else:
oldW = W.copy()
for i in xrange(V):
W[i] -= learning_rate*(fX[i,:]*delta[i,:]).dot(U)
W-= learning_rate*reg*W
Обновляем b. Возможно, сейчас вам будет не совсем ясно, как мывекторизовали уравнения, но мы делали это ранее множество раз – в «Глубокомобучении, часть 1», логистической регрессии… Так что вы уже должны с этимосвоиться. Но если нет – вернитесь назад и попробуйте сделать всёсамостоятельно.
for i in xrange(V):
b[i] -=learning_rate*fX[i,:].dot(delta[i,:])
b -= learning_rate*reg*b
Обновляем U.
for j in xrange(V):
U[j] -= learning_rate*(fX[:,j]*delta[:,j]).dot(oldW)
U -= learning_rate*reg*U
Теперь обновляем c.
for j in xrange(V):
c[j] -= learning_rate*fX[:,j].dot(delta[:,j])
c-= learning_rate*reg*c
Иоставим место для чередующихся наименьших квадратов.
else:
# ALS method
pass
self.W =W
self.U =U
plt.plot(costs)
plt.show()
Пришёл черёд функции save.
def save(self, fn):
arrays = [self.W, self.U.T]
np.savez(fn, *arrays)
Следующийэтап – написание функции main.
def main(we_file,w2i_file, n_files=50):
cc_matrix = “cc_matrix_%s.npy” % n_files
ifos.path.exists(cc_matrix):
with open(w2i_file) as f:
word2idx = json.load(f)
sentences = []
else:
sentences, word2idx = get_wikipedia_data(n_files=n_files, n_vocab=2000)
with open(w2i_file, ‘w’) as f:
json.dump(word2idx, f)
V = len(word2idx)
model =Glove(80, V, 10)
model.fit(
sentences=sentences,
cc_matrix=cc_matrix,
learning_rate=3*10e-5,
reg=0.01,
epochs=2000,
gd=True,
use_theano=False,
)
model.save(we_file)
if __name__ == ‘__main__’:
we =‘glove_model_50.npz’
w2i =‘glove_word2idx_50.json’
main(we,w2i, use_brown=True)
for concatin (True, False):
print “** concat:”, concat
Проверимуже использовавшиеся словесные аналогии.
find_analogies(‘king’, ‘man’, ‘woman’, concat, we, w2i)
find_analogies(‘france’, ‘paris’, ‘london’, concat, we, w2i)
find_analogies(‘france’, ‘paris’, ‘rome’, concat, we, w2i)
find_analogies(‘paris’, ‘france’, ‘italy’, concat, we, w2i)
find_analogies(‘france’, ‘french’, ‘english’, concat, we, w2i)
find_analogies(‘japan’, ‘japanese’, ‘chinese’, concat, we, w2i)
find_analogies(‘japan’, ‘japanese’, ‘italian’, concat, we, w2i)
find_analogies(‘japan’, ‘japanese’, ‘australian’, concat, we, w2i)
find_analogies(‘december’, ‘november’, ‘june’, concat, we, w2i)
Код для GloVe. Градиентный спуск в Theano
Мы сделаем версию градиентного спуска в Theano. В use_theano ставим флаг True и переходим к месту, где должен быть код Theano. Это место, где идёт if gd and use_theano.
ifgd and use_theano:
thW= theano.shared(W)
thb= theano.shared(b)
thU= theano.shared(U)
thc= theano.shared(c)
thLogX = T.matrix(‘logX’)
thfX= T.matrix(‘fX’)
params = [thW, thb, thU, thc]
Обратитевнимание, что мы должны b и c преобразовать в правильную форму. b должно бытьвектором-столбцом, c – вектором-строкой. Иначе ничего работать не будет.
thDelta= thW.dot(thU.T) + T.reshape(thb, (V, 1)) + T.reshape(thc, (1, V)) + mu – thLogX
thCost = ( thfX * thDelta * thDelta ).sum()
grads = T.grad(thCost, params)
updates = [(p, p – learning_rate*g) for p, g in zip(params, grads)]
train_op = theano.function(
inputs=[thfX, thLogX],
updates=updates,
)
Ипереходим к месту выполнения градиентного спуска. Тут всё просто, ведь train_op мы ужеопределили. Мы должны её только вызвать.
ifgd:
ifuse_theano:
train_op(fX, logX)
W = thW.get_value()
b = thb.get_value()
U = thU.get_value()
c = thc.get_value()
Всё сделано. Запустим наш файл.
Код для GloVe. Чередующиеся наименьшие квадраты
Наэтой лекции мы продолжим наш пример с написанием кода для GloVe и реализуем метод чередующихся наименьшихквадратов.
Итак,переходим к месту, оставленному для чередующихся наименьших квадратов.
Вначале обновляем w.
# ALS method
for i in xrange(V):
matrix = reg*np.eye(D) + (fX[i,:]*U.T).dot(U)
vector = (fX[i,:]*(logX[i,:] – b[i] – c – mu)).dot(U)
W[i] = np.linalg.solve(matrix, vector)
Незабывайте, что векторные и матричные операции в Numpyнамного более эффективны, поэтому не стоит писать прямое решение. Вы можетесделать это вначале, чтобы убедиться в правильности своего решения, но вообщеговоря нужно переводить уравнения в векторную форму, поскольку так намногобыстрее. Если вы хотите посмотреть первоначальное решение, без перевода ввекторную форму, то зайдите в репозитарий Github.
for i in xrange(V):
denominator = fX[i,:].sum()
numerator = fX[i,:].dot(logX[i,:] – W[i].dot(U.T) – c – mu)
b[i] = numerator / denominator / (1 + reg)
Итак,мы обновили wи b.Перейдём теперь к обновлению u и c.
for j in xrange(V):
matrix = reg*np.eye(D) + (fX[:,j]*W.T).dot(W)
vector = (fX[:,j]*(logX[:,j] – b – c[j] – mu)).dot(W)
U[j] = np.linalg.solve(matrix, vector)
Ипоследнее обновление – для c.
for j in xrange(V):
denominator = fX[:,j].sum()
numerator = fX[:,j].dot(logX[:,j] – W.dot(U[j]) – b – mu)
c[j] = numerator / denominator / (1 + reg)
Ивнизу в функции main.
model.fit(sentences,cc_matrix=cc_matrix, epochs=20) # ALS
Запустим наш код.
Визуализация аналогий по странам с помощью t-SNE
Сейчас мы напишем код для наглядной демонстрации векторных представлений слов, полученных с помощью метода глобальных векторов, в частности, посмотрим на страны и национальности, чтобы узнать, получилось ли найти какие-то интересные закономерности.
Итак,начинаем с импорта обычных библиотек – Json,Numpy, Matplotlib и SkiKit Learn.
import json
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
Определяемнашу функцию main и указываем конкретные слова, которые хотим увидетьна диаграмме: «Япония», «японский», «Англия», «английский», «Австралия»,«австралийский» и другие. В качестве упражнения можете попробовать что-то своё,чтобы найти совсем другие аналогии.
def main(we_file=’glove_model_50.npz’,w2i_file=’glove_word2idx_50.json’):
words =[‘japan’, ‘japanese’, ‘england’, ‘english’, ‘australia’, ‘australian’, ‘china’,‘chinese’, ‘italy’, ‘italian’, ‘french’, ‘france’, ‘spain’, ‘spanish’]
with open(w2i_file) as f:
word2idx= json.load(f)
npz =np.load(we_file)
W =npz[‘arr_0’]
V =npz[‘arr_1’]
We = (W +V.T) / 2
idx = [word2idx[w] for w in words]
tsne =TSNE()
Z =tsne.fit_transform(We)
Z = Z[idx]
plt.scatter(Z[:,0], Z[:,1])
for i in xrange(len(words)):
plt.annotate(s=words[i], xy=(Z[i,0], Z[i,1]))
plt.show()
if __name__ == ‘__main__’:
main()
Запустимпрограмму и посмотрим, что получится.
Итак, любопытно, что все национальности расположены в одной области, а названия стран – в другой. В предыдущем случае использования t-SNE мы видели, что некоторые слова располагались неуместно. В данном же примере мы видим, что похожие слова находятся в относительной близости друг от друга, даже в случае рассмотрения похожих, но всё же различных понятий или, другими словами, аналогий.
Проблема гиперпараметров
В завершении нашей темы, я хочу прямо предложить вам выполнить ещё одно упражнение. Я часто косвенно предлагаю упражнения, но в данном случае очень важно, чтобы вы сделали его самостоятельно.
Каквы могли заметить, наши словесные аналогии были отнюдь не идеальными. Вчастности, при проведении аналогий со странами и городами мы получили Берлин,когда ожидался Рим. Я предлагаю провести больше словесных аналогий для проверкиэффективности модели. Мы уже перепробовали страны и города, страны инациональности, месяцы года. Но есть и другие варианты. Можно попробоватьпрофессии. К примеру, программисты создают программы, а художники – картины.Это тоже словесные аналогии.
Естьряд гиперпараметров, с которыми можно поиграть для получения лучшегорезультата. Есть привычные нам коэффициент обучения, импульс и регуляризация.От импульса вряд ли будет польза, поскольку в моделях вроде word2vec на каждом этапеобновляются различные векторы, так что смысла в его изменении не будет. Чтокасается регуляризации, что иногда длину всех слов-векторов устанавливаютравной 1.
Крометого, мы можем захотеть, чтобы наша модель узнала как можно больше о том, какиспользуются слова. В этом случае нам нужно использовать как можно большеучебных файлов. Но не забывайте: чем больше имеющихся данных, тем дольше будетидти обучение. Исключением тут является метод глобальных векторов, при которомобучение всегда происходит на матрице размерностью VxV. Много времени тут занимает толькопредварительная работа по созданию целевой матрицы.
Регулируютсятакже размерность векторных представлений слов и размер словаря. Можнопоэкспериментировать с ними. Но если размерность представлений чересчур велика,то пространство поиска будет слишком большим, если же чересчур низка, то модельне сможет правильно разместить все векторы. Если чересчур большим будет размерсловаря, то может не хватить данных, но при чересчур малом словаре вряд линайдутся интересные аналогии. Попробуйте самостоятельно найти компромиссныезначения величин и поделитесь своими результатами с коллегами-слушателями.