Долгая краткосрочная память

Материал из Викиконспекты
Перейти к: навигация, поиск

Долгая краткосрочная память (англ. Long short-term memory, LSTM) — особая разновидность архитектуры рекуррентных нейронных сетей, способная к обучению долговременным зависимостям, предложенная в 1997 году Сеппом Хохрайтером и Юргеном Шмидхубером[1].

Описание

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

LSTM-модули разработаны специально, чтобы избежать проблемы долговременной зависимости, запоминая значения как на короткие, так и на длинные промежутки времени. Это объясняется тем, что LSTM-модуль не использует функцию активации внутри своих рекуррентных компонентов. Таким образом, хранимое значение не размывается во времени и градиент не исчезает при использовании метода обратного распространения ошибки во времени (англ. Backpropagation Through Time, BPTT)[2][3] при тренировке сети.

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

Состояние ячейки

По мере того, как происходит обучение, состояние ячейки изменяется, информация добавляется или удаляется из состояния ячейки структурами, называемыми фильтрами. Фильтры контролируют поток информации на входах и на выходах модуля на основании некоторых условий. Они состоят из слоя сигмоидальной[4] нейронной сети и операции поточечного умножения.

Фильтры

Сигмоидальный слой возвращает числа в диапазоне [0; 1], которые обозначают, какую долю каждого блока информации следует пропустить дальше по сети. Умножение на это значение используется для пропуска или запрета потока информации внутрь и наружу памяти. Например, входной фильтр контролирует меру вхождения нового значения в память, а фильтр забывания контролирует меру сохранения значения в памяти. Выходной фильтр контролирует меру того, в какой степени значение, находящееся в памяти, используется при расчёте выходной функции активации.

Основные компоненты

  • Состояние ячейки
  • Фильтры, контролирующие состояние ячейки
    • Забывания
    • Входной
    • Выходной

Принцип работы

Сперва “слой фильтра забывания” (англ. forget gate layer) определяет, какую информацию можно забыть или оставить. Значения предыдущего выхода [math]h_{t-1}[/math] и текущего входа [math]x_t[/math] пропускаются через сигмоидальный слой. Полученные значения находятся в диапозоне [0; 1]. Значения, которые ближе к 0 будут забыты, а к 1 оставлены.

Lstm-1.png

Далее решается, какая новая информация будет храниться в состоянии ячейки. Этот этап состоит из двух частей. Сначала сигмоидальный слой под названием “слой входного фильтра” (англ. input layer gate) определяет, какие значения следует обновить. Затем tanh-слой[5] строит вектор новых значений-кандидатов [math]\tilde{C}_t[/math], которые можно добавить в состояние ячейки.

Lstm-2.png

Для замены старого состояния ячейки [math]C_{t-1}[/math] на новое состояние [math]C_t[/math]. Необходимо умножить старое состояние на [math]f_t[/math], забывая то, что решили забыть ранее. Затем прибавляем [math]i_t * \tilde{C}_t[/math]. Это новые значения-кандидаты, умноженные на [math]t[/math] – на сколько обновить каждое из значений состояния.

Lstm-3.png

На последнем этапе определяется то, какая информация будет получена на выходе. Выходные данные будут основаны на нашем состоянии ячейки, к ним будут применены некоторые фильтры. Сначала значения предыдущего выхода [math]h_{t-1}[/math] и текущего входа [math]x_t[/math] пропускаются через сигмоидальный слой, который решает, какая информация из состояния ячейки будет выведена. Затем значения состояния ячейки проходят через tanh-слой, чтобы получить на выходе значения из диапазона от -1 до 1, и перемножаются с выходными значениями сигмоидального слоя, что позволяет выводить только требуемую информацию.

Lstm-4.png

Полученные таким образом [math]h_t[/math] и [math]C_t[/math] передаются далее по цепочке.

Вариации

Одна из популярных вариаций LSTM, предложенная Герсом и Шмидхубером[6], характеризуется добавлением так называемых “смотровых глазков” (“peephole connections”). С их помощью слои фильтров могут видеть состояние ячейки.

Lstm-peephole-connections.png

На схеме выше “глазки” есть у каждого слоя, но во многих работах они добавляются лишь к некоторым слоям.

Другие модификации включают объединенные фильтры “забывания” и входные фильтры. В этом случае решения, какую информацию следует забыть, а какую запомнить, принимаются не отдельно, а совместно. Информация забывается только тогда, когда необходимо записать что-то на ее место. Добавление новой информации в состояние ячейки выполняется только тогда, когда забываем старую.

Lstm-mod-1.png

Немного больше отличаются от стандартных LSTM управляемые рекуррентные нейроны (англ. Gated recurrent units, GRU), впервые описанные в работе Чо Кунгхун (англ. Cho Kyunghyun)[7]. В ней фильтры «забывания» и входа объединяют в один фильтр «обновления» (update gate). Кроме того, состояние ячейки объединяется со скрытым состоянием, есть и другие небольшие изменения. Построенная в результате модель проще, чем стандартная LSTM, и популярность ее неуклонно возрастает.

Lstm-gru.png

Существует множество других модификаций, как, например, глубокие управляемые рекуррентные нейронные сети (англ. Depth Gated RNNs), представленные в работе Yao[8]. Есть и другие способы решения проблемы долговременных зависимостей, например, Clockwork RNN Яна Кутника[9].

Примеры кода

TensorFlow

Пример кода с библиотекой TensorFlow[10]

 # Импорты
 from __future__ import print_function
 import tensorflow as tf
 from tensorflow.contrib import rnn
 
 # Импорт MNIST датасета
 from tensorflow.examples.tutorials.mnist import input_data
 mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
 
 # Определение параметров обучения
 learning_rate = 0.001
 training_steps = 10000
 batch_size = 128
 display_step = 200
 
 # Определение параметров сети
 num_input = 28
 timesteps = 28
 num_hidden = 128
 num_classes = 10
 
 # Входные данные для графа
 X = tf.placeholder("float", [None, timesteps, num_input])
 Y = tf.placeholder("float", [None, num_classes])
 
 # Определение весов
 weights = {
   'out': tf.Variable(tf.random_normal([num_hidden, num_classes]))
 }
 biases = {
   'out': tf.Variable(tf.random_normal([num_classes]))
 }
 
 def RNN(x, weights, biases):
     x = tf.unstack(x, timesteps, 1)
     # Определение LSTM ячейки
     lstm_cell = rnn.BasicLSTMCell(num_hidden, forget_bias=1.0)
     # Получение выхода LSTM ячейки
     outputs, states = rnn.static_rnn(lstm_cell, x, dtype=tf.float32)
     return tf.matmul(outputs[-1], weights['out']) + biases['out']
 
 logits = RNN(X, weights, biases)
 prediction = tf.nn.softmax(logits)
 
 # Определение функции потерь и оптимизатора
 loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
     logits=logits, labels=Y))
 optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
 train_op = optimizer.minimize(loss_op)
 
 # Оценка модели
 correct_pred = tf.equal(tf.argmax(prediction, 1), tf.argmax(Y, 1))
 accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
 
 # Инициализация
 init = tf.global_variables_initializer()
 
 with tf.Session() as sess:
     sess.run(init)
 
     for step in range(1, training_steps+1):
         batch_x, batch_y = mnist.train.next_batch(batch_size)
         batch_x = batch_x.reshape((batch_size, timesteps, num_input))
         # Запуск оптимизатора (обратное распространение ошибки)
         sess.run(train_op, feed_dict={X: batch_x, Y: batch_y})
         if step % display_step == 0 or step == 1:
             loss, acc = sess.run([loss_op, accuracy], feed_dict={X: batch_x, Y: batch_y})
             print("Step " + str(step) + ", Minibatch Loss= " + \
                 "{:.4f}".format(loss) + ", Training Accuracy= " + \
                 "{:.3f}".format(acc))
     print("Optimization Finished!")
 
     test_len = 128
     test_data = mnist.test.images[:test_len].reshape((-1, timesteps, num_input))
     test_label = mnist.test.labels[:test_len]
     print("Testing Accuracy:", \
         sess.run(accuracy, feed_dict={X: test_data, Y: test_label}))

Keras

Пример кода с использованием библиотеки Keras.[11]

 # Импорты
 import numpy as np
 from keras.preprocessing import sequence
 from keras.models import Sequential
 from keras.layers import Dense, Activation, Embedding
 from keras.layers import LSTM
 from keras.datasets import imdb
 
 # Устанавливаем seed для обеспечения повторяемости результатов
 np.random.seed(42)
 
 # Указываем количество слов из частотного словаря, которое будет использоваться (отсортированы по частоте использования)
 max_features = 5000
 
 # Загружаем данные (датасет IMDB содержит 25000 рецензий на фильмы с правильным ответом для обучения и 25000 рецензий на фильмы с правильным ответом для тестирования)
 (X_train, y_train), (X_test, y_test) = imdb.load_data(nb_words = max_features)
 
 # Устанавливаем максимальную длину рецензий в словах, чтобы они все были одной длины
 maxlen = 80
 
 # Заполняем короткие рецензии пробелами, а длинные обрезаем
 X_train = sequence.pad_sequences(X_train, maxlen = maxlen)
 X_test = sequence.pad_sequences(X_test, maxlen = maxlen)
 
 # Создаем модель последовательной сети
 model = Sequential()
 # Добавляем слой для векторного представления слов (5000 слов, каждое представлено вектором из 32 чисел, отключаем входной сигнал с вероятностью 20% для предотвращения переобучения)
 model.add(Embedding(max_features, 32, dropout = 0.2))
 # Добавляем слой долго-краткосрочной памяти (100 элементов для долговременного хранения информации, отключаем входной сигнал с вероятностью 20%, отключаем рекуррентный сигнал с вероятностью 20%)
 model.add(LSTM(100, dropout_W = 0.2, dropout_U = 0.2))
 # Добавляем полносвязный слой из 1 элемента для классификации, в качестве функции активации будем использовать сигмоидальную функцию
 model.add(Dense(1, activation = 'sigmoid'))
 
 # Компилируем модель нейронной сети
 model.compile(loss = 'binary_crossentropy',
               optimizer = 'adam',
               metrics = ['accuracy'])
 
 # Обучаем нейронную сеть (данные для обучения, ответы к данным для обучения, количество рецензий после анализа которого будут изменены веса, число эпох обучения, тестовые данные, показывать progress bar или нет)
 model.fit(X_train, y_train, 
           batch_size = 64,
           nb_epoch = 7,
           validation_data = (X_test, y_test),
           verbose = 1)
 
 # Проверяем качество обучения на тестовых данных (если есть данные, которые не участвовали в обучении, лучше использовать их, но в нашем случае таковых нет)
 scores = model.evaluate(X_test, y_test, batch_size = 64)
 print('Точность на тестовых данных: %.2f%%' % (scores[1] * 100))

См. также

Примечания