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

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

Долгая краткосрочная память (англ. 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] передаются далее по цепочке.

Вариации[править]

Cмотровые глазки[править]

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

Lstm-peephole-connections.png

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

Объединенные фильтры[править]

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

Lstm-mod-1.png

Управляемые рекуррентные нейроны[править]

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

Lstm-gru.png

Глубокие управляемые рекуррентные нейроны[править]

Существует множество других модификаций, как, например, глубокие управляемые рекуррентные нейронные сети (англ. Depth Gated RNNs), представленные в работе Каишенга Яо (англ. Kaisheng Yao)[8]. Глубокие управляемые рекуррентные нейронные сети привносят фильтр глубины для подключения ячеек памяти соседних слоев. Это вводит линейную зависимость между нижними и верхними рекуррентными единицами. Важно отметить, что линейная зависимость проходит через функцию стробирования, которая называется фильтром забывания. Данная архитектура способна улучшить машинный перевод и языковое моделирование.

Механизм часов[править]

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

Примеры кода[править]

Keras[править]

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

 # Импорты
 import numpy as np
 import keras.backend as K
 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
 
 def f1(y_true, y_pred):
   def recall(y_true, y_pred):
       true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
       possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))
       recall = true_positives / (possible_positives + K.epsilon())
       return recall
 
   def precision(y_true, y_pred):
       true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
       predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))
       precision = true_positives / (predicted_positives + K.epsilon())
       return precision
 
   precision = precision(y_true, y_pred)
   recall = recall(y_true, y_pred)
   return 2*((precision*recall)/(precision+recall+K.epsilon()))
 
 # Устанавливаем 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', 'f1'])
 
 # Обучаем нейронную сеть (данные для обучения, ответы к данным для обучения, количество рецензий после анализа которого будут изменены веса, число эпох обучения, тестовые данные, показывать 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))
 print('F1 на тестовых данных: %.2f%%' % (scores[2] * 100))

Результат:

 Точность на тренировочных данных: 89.64%
 F1 на тренировочных данных: 89.55%
 
 Точность на тестовых данных: 83.01%
 F1 на тестовых данных: 82.48%

TensorFlow[править]

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

 # Импорты
 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}))

Результат:

 Точность на тренировочных данных: 91.40%
 F1 на тренировочных данных: 91.05%
 
 Точность на тестовых данных: 85.15%
 F1 на тестовых данных: 84.28%

Пример на языке Java[править]

Пример реализации рекуррентной нейронной сети, использующей механизм LSTM и натренированной на текстах Шекспира, с применением библиотеки deeplearning4j.

См. также[править]

Примечания[править]

Источники информации[править]