Долгая краткосрочная память
Долгая краткосрочная память (англ. Long short-term memory, LSTM) — особая разновидность архитектуры рекуррентных нейронных сетей, способная к обучению долговременным зависимостям, предложенная в 1997 году Сеппом Хохрайтером и Юргеном Шмидхубером[1].
Содержание
Описание
Рекуррентные нейронные сети добавляют память к искуственным нейронным сетям, но реализуемая память получается короткой — на каждом шаге обучения информация в памяти смешивается с новой и через несколько итераций полностью перезаписывается.
LSTM-модули разработаны специально, чтобы избежать проблемы долговременной зависимости, запоминая значения как на короткие, так и на длинные промежутки времени. Это объясняется тем, что LSTM-модуль не использует функцию активации внутри своих рекуррентных компонентов. Таким образом, хранимое значение не размывается во времени и градиент не исчезает при использовании метода обратного распространения ошибки во времени (англ. Backpropagation Through Time, BPTT)[2][3] при тренировке сети.
Ключевые компоненты LSTM-модуля: состояние ячейки и различные фильтры. О состоянии ячейки можно говорить, как о памяти сети, которая передает соответствующую информацию по всей цепочке модулей. Таким образом, даже информация из ранних временных шагов может быть получена на более поздних, нивелируя эффект кратковременной памяти.
По мере того, как происходит обучение, состояние ячейки изменяется, информация добавляется или удаляется из состояния ячейки структурами, называемыми фильтрами. Фильтры контролируют поток информации на входах и на выходах модуля на основании некоторых условий. Они состоят из слоя сигмоидальной[4] нейронной сети и операции поточечного умножения.
Сигмоидальный слой возвращает числа в диапазоне [0; 1], которые обозначают, какую долю каждого блока информации следует пропустить дальше по сети. Умножение на это значение используется для пропуска или запрета потока информации внутрь и наружу памяти. Например, входной фильтр контролирует меру вхождения нового значения в память, а фильтр забывания контролирует меру сохранения значения в памяти. Выходной фильтр контролирует меру того, в какой степени значение, находящееся в памяти, используется при расчёте выходной функции активации.
Основные компоненты
- Состояние ячейки
- Фильтры, контролирующие состояние ячейки
- Забывания
- Входной
- Выходной
Принцип работы
Сперва “слой фильтра забывания” (англ. forget gate layer) определяет, какую информацию можно забыть или оставить. Значения предыдущего выхода
и текущего входа пропускаются через сигмоидальный слой. Полученные значения находятся в диапозоне [0; 1]. Значения, которые ближе к 0 будут забыты, а к 1 оставлены.Далее решается, какая новая информация будет храниться в состоянии ячейки. Этот этап состоит из двух частей. Сначала сигмоидальный слой под названием “слой входного фильтра” (англ. input layer gate) определяет, какие значения следует обновить. Затем tanh-слой[5] строит вектор новых значений-кандидатов , которые можно добавить в состояние ячейки.
Для замены старого состояния ячейки
на новое состояние . Необходимо умножить старое состояние на , забывая то, что решили забыть ранее. Затем прибавляем . Это новые значения-кандидаты, умноженные на – на сколько обновить каждое из значений состояния.На последнем этапе определяется то, какая информация будет получена на выходе. Выходные данные будут основаны на нашем состоянии ячейки, к ним будут применены некоторые фильтры. Сначала значения предыдущего выхода
и текущего входа пропускаются через сигмоидальный слой, который решает, какая информация из состояния ячейки будет выведена. Затем значения состояния ячейки проходят через tanh-слой, чтобы получить на выходе значения из диапазона от -1 до 1, и перемножаются с выходными значениями сигмоидального слоя, что позволяет выводить только требуемую информацию.Полученные таким образом
и передаются далее по цепочке.Вариации
Cмотровые глазки
Одна из популярных вариаций LSTM, предложенная Герсом и Шмидхубером[6], характеризуется добавлением так называемых “смотровых глазков” (англ. peephole connections). С их помощью слои фильтров могут видеть состояние ячейки.
На схеме выше “глазки” есть у каждого слоя, но во многих работах они добавляются лишь к некоторым слоям.
Объединенные фильтры
Другие модификации включают объединенные фильтры “забывания” и входные фильтры. В этом случае решения, какую информацию следует забыть, а какую запомнить, принимаются не отдельно, а совместно. Информация забывается только тогда, когда необходимо записать что-то на её место. Добавление новой информации в состояние ячейки выполняется только тогда, когда забываем старую.
Управляемые рекуррентные нейроны
Немного больше отличаются от стандартных LSTM управляемые рекуррентные нейроны (англ. Gated recurrent units, GRU), впервые описанные в работе Кюнгхюна Чо (англ. Kyunghyun Cho)[7]. У них на один фильтр меньше, и они немного иначе соединены. Фильтры «забывания» и входа объединяют в один фильтр «обновления» (англ. update gate). Этот фильтр определяет сколько информации сохранить от последнего состояния, и сколько информации получить от предыдущего слоя. Кроме того, состояние ячейки объединяется со скрытым состоянием, есть и другие небольшие изменения. Фильтр сброса состояния (англ. reset gate) работает почти так же, как фильтр забывания, но расположен немного иначе. На следующие слои отправляется полная информация о состоянии, выходного фильтра нет. В большинстве случаем GRU работают так же, как LSTM, самое значимое отличие в том, что GRU немного быстрее и проще в эксплуатации, однако обладает немного меньшими выразительными возможностями. В результате модели проще, чем LSTM и их популярность неуклонно возрастает. Эффективность при решении задач моделирования музыкальных и речевых сигналов сопоставима с использованием долгой краткосрочной памяти.
Глубокие управляемые рекуррентные нейроны
Существует множество других модификаций, как, например, глубокие управляемые рекуррентные нейронные сети (англ. 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
.
См. также
- Рекуррентные нейронные сети
- Нейронные сети, перцептрон
- Сверточные нейронные сети
- Рекурсивные нейронные сети
Примечания
- ↑ Sepp Hochreiter, Jurgen Schmidhuber. Long short-term memory (1997). Neural Computation.
- ↑ Backpropagation Through Time
- ↑ Backpropagation Through Time
- ↑ Сигмоида.
- ↑ Гиперболические функции.
- ↑ Gers, Schmidhuber. Recurrent Nets that Time and Count (2000).
- ↑ Cho. Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation (2014).
- ↑ SeppKaisheng Yao. Depth-Gated Recurrent Neural Networks (2015).
- ↑ Jan Koutnik. A Clockwork RNN (2014).
- ↑ Keras RNN with LSTM layer
- ↑ TensorFlow
Источники информации
- Understanding LSTM Networks
- Illustrated Guide to LSTM’s and GRU’s: A step by step explanation
- The fall of RNN / LSTM
- Long short-term memory - статья на Википедии
- Long Short Term Memory (LSTM) - курс Andrew Ng