Содержание страницы
Обновляемый РНС-нейрон
Здравствуйте и вновь добро пожаловать на занятия по теме «Рекуррентные нейронные сети на языке Python: глубокое обучение, часть 5».
В этой статье я продемонстрирую вам очень простую модификацию простого рекуррентного нейрона.
Суть в том, что нам нужно узнать удельный вес двух вещей. Во-первых, это f(x(t), h(t-1)) – выход, который бы мы хотели получить из простого рекуррентного нейрона, а во-вторых, h(t-1) – предыдущее значение скрытого состояния. Для этого используется матрица z, называемая матрицей обновления, того же размера, что и скрытый слой, с проведением поэлементного умножения по каждой размерности:
z(t) или просто z называют обновлением. Обратите внимание, что это похоже на фильтр нижних частот, созданный нами в последнем введении по Theano scan – это должно дать вам некоторое интуитивное представление об эффекте рассматриваемого нейрона.
Если вы следили за кодом, то, вероятно, понимаете, что это очень простое изменение, состоящее лишь в добавлении нового параметра размерности M и включении приведенного выше уравнения в функцию recurrence.
Есть ряд вариантов использования матрицы обновления. К примеру, мы можем сделать её зависимой от входных данных и предыдущего скрытого состояния с помощью сигмоиды и других матриц весовых коэффициентов:
Мы также можем сделать так, чтобы она умножалась на h с помощью матричного умножения вместо поэлементного. Получившееся можно изобразить как ситуацию, в которой всё присоединено ко всему в скрытых слоях.
Мы не будем реализовывать этих ситуаций, но после написания в коде управляемого рекуррентного нейрона и сетей долгой краткосрочной памяти проблем тут возникнуть не должно, поскольку они включают в себя в точности такие же типы соединений. Если вам интересно, рекомендую попробовать самим в качестве упражнения, чтобы оценить эффект.
В следующей лекции мы переделаем пример с генератором стихов с обновляемым рекуррентным нейроном и посмотрим, что получилось улучшить. Вы увидите, что это лишь небольшая модификация простого рекуррентного нейрона, так что код останется почти прежним.
Обратите внимание, что наш новый параметр обучается в точности так же, как и другие, то есть с помощью градиентного спуска. Фактически при рассмотрении более сложных моделей обучение всё равно остаётся прежним.
Чтобы сделать задачу чуть сложнее, мы также попробуем решить проблему чересчур быстрого окончания строк. Напомним, это происходит потому, что у нас много обучающих образцов, каждый из которых заканчивается токеном END в конце строки. Поскольку токен END встречается в каждой строке, он также присутствует в обучающем наборе данных. Бороться с этим мы будем следующим образом. Будем обучать модель до конца последовательности лишь в 10% случаев, а во всех остальных – останавливаться в промежутке от второго до предпоследнего слова (токена).
Ещё одним изменением будет то, что мы больше не будем моделировать начальное распределение. Напомню, что использовался он для того, чтобы строки не начинались с одного и того же слова, иначе нейронная сеть будет каждый раз выдавать один и тот же результат. Вместо этого мы используем токен START в качестве входа, а выход будет представлять распределение вероятностей. Это корректно, поскольку мы уже знаем, что софтмакс даёт корректное распределение вероятностей. Затем мы можем делать выборку из этого распределения для генерации следующего слова. В этом случае генерируемые нами последовательности будут более стохастическими, а исходящая вероятность будет рассматриваться как фактическая вероятность, а не предопределённый прогноз.
Обновляемая рекуррентная нейронная сеть в коде – возвращение к генерации стихов
Мы напишем обновляемую рекуррентную нейронную сеть и посмотрим, получится ли улучшить генерацию стихов.
Для начала я просто скопировал весь код из предыдущего файла по генерации стихов, так что у нас уже есть рабочий шаблон, а всё, что нам нужно, – это добавить обновление. Поэтому весь код вам уже должен быть знаком, если вы смотрели соответствующую лекцию. Всё то же самое, но теперь мы должны создать переменную обновления z, а также кое-что удалить, поскольку у нас есть функция set.
def fit(self, X, learning_rate=10e-1, mu=0.9, reg=1.0, activation=T.tanh, epochs=500, show_fig=False):
N = len(X)
D = self.D
M = self.M
V = self.V
# initial weights
We = init_weight(V, D)
Wx = init_weight(D, M)
Wh = init_weight(M, M)
bh = np.zeros(M)
h0 = np.zeros(M)
# z = np.ones(M)
Wxz = init_weight(D, M)
Whz = init_weight(M, M)
bz = np.zeros(M)
Wo = init_weight(M, V)
bo = np.zeros(V)
thX, thY, py_x, prediction = self.set(We, Wx, Wh, bh, h0, Wxz, Whz, bz, Wo, bo, activation)
Обучающий цикл почти тот же, но кое-что нужно изменить. Мы теперь будем обучаться до конца последовательности только в 10% случаев.
costs = []
for i in xrange(epochs):
X = shuffle(X)
n_correct = 0
n_total = 0
cost = 0
for j in xrange(N):
if np.random.random() < 0.1:
input_sequence = [0] + X[j]
output_sequence = X[j] + [1]
else:
input_sequence = [0] + X[j][:-1]
output_sequence = X[j]
n_total += len(output_sequence)
Всё остальное остаётся прежним.
Далее функция load – тут тоже надо включить z.
@staticmethod
def load(filename, activation):
# TODO: would prefer to save activation to file too
npz = np.load(filename)
We = npz[‘arr_0’]
Wx = npz[‘arr_1’]
Wh = npz[‘arr_2’]
bh = npz[‘arr_3’]
h0 = npz[‘arr_4’]
Wxz = npz[‘arr_5’]
Whz = npz[‘arr_6’]
bz = npz[‘arr_7’]
Wo = npz[‘arr_8’]
bo = npz[‘arr_9’]
V, D = We.shape
_, M = Wx.shape
rnn = SimpleRNN(D, M, V)
rnn.set(We, Wx, Wh, bh, h0, Wxz, Whz, bz, Wo, bo, activation)
return rnn
def set(self, We, Wx, Wh, bh, h0, Wxz, Whz, bz, Wo, bo, activation):
self.f = activation
# redundant – see how you can improve it
self.We = theano.shared(We)
self.Wx = theano.shared(Wx)
self.Wh = theano.shared(Wh)
self.bh = theano.shared(bh)
self.h0 = theano.shared(h0)
self.Wxz = theano.shared(Wxz)
self.Whz = theano.shared(Whz)
self.bz = theano.shared(bz)
self.Wo = theano.shared(Wo)
self.bo = theano.shared(bo)
self.params = [self.We, self.Wx, self.Wh, self.bh, self.h0, self.Wxz, self.Whz, self.bz, self.Wo, self.bo]
thX = T.ivector(‘X’)
Ei = self.We[thX] # will be a TxD matrix
thY = T.ivector(‘Y’)
Меняется также функция recurrence.
def recurrence(x_t, h_t1):
# returns h(t), y(t)
hhat_t = self.f(x_t.dot(self.Wx) + h_t1.dot(self.Wh) + self.bh)
z_t = T.nnet.sigmoid(x_t.dot(self.Wxz) + h_t1.dot(self.Whz) + self.bz)
h_t = (1 – z_t) * h_t1 + z_t * hhat_t
y_t = T.nnet.softmax(h_t.dot(self.Wo) + self.bo)
return h_t, y_t
[h, y], _ = theano.scan(
fn=recurrence,
outputs_info=[self.h0, None],
sequences=Ei,
n_steps=Ei.shape[0],
)
py_x = y[:, 0, :]
prediction = T.argmax(py_x, axis=1)
self.predict_op = theano.function(
inputs=[thX],
outputs=[py_x, prediction],
allow_input_downcast=True,
)
return thX, thY, py_x, prediction
В функции generate мы больше не используем π, делаем выборку с помощью софтмакс в качестве вероятности.
def generate(self, word2idx):
# convert word2idx -> idx2word
idx2word = {v:k for k,v in iteritems(word2idx)}
V = len(word2idx)
# generate 4 lines at a time
n_lines = 0
# why? because using the START symbol will always yield the same first word!
X = [ 0 ]
while n_lines < 4:
# print “X:”, X
PY_X, _ = self.predict_op(X)
PY_X = PY_X[-1].flatten()
P = [ np.random.choice(V, p=PY_X)]
X = np.concatenate([X, P]) # append to the sequence
# print “P.shape:”, P.shape, “P:”, P
P = P[-1] # just grab the most recent prediction
if P > 1:
# it’s a real word, not start/end token
word = idx2word[P]
print(word, end=” “)
elif P == 1:
# end token
n_lines += 1
X = [0]
print(”)
Функция train_poetry остаётся прежней, а функция generate_poetry становится намного проще, поскольку нам больше не требуется π. Разве что размер матрицы векторного представления слов и размер скрытого слоя увеличим до 50, а также изменим название файла, чтобы знать, что это обновляемая рекуррентная сеть.
def train_poetry():
# students: tanh didn’t work but you should try it
sentences, word2idx = get_robert_frost()
rnn = SimpleRNN(50, 50, len(word2idx))
rnn.fit(sentences, learning_rate=10e-5, show_fig=True, activation=T.nnet.relu, epochs=2000)
rnn.save(‘RRNN_D50_M50_epochs2000_relu.npz’)
def generate_poetry():
sentences, word2idx = get_robert_frost()
rnn = SimpleRNN.load(‘RRNN_D50_M50_epochs2000_relu.npz’, T.nnet.relu)
rnn.generate(word2idx)
Запустим программу и посмотрим, что это нам даст…
Попробуем теперь сгенерировать стихи с помощью обновляемой рекуррентной нейронной сети.
Как видите, когда мы не доводим обучение до конца предложения, то сами предложения становится длиннее. Кроме того, получается куда более разнообразный результат при выборке из исходящего распределения, нежели при простом использовании прогноза.