В глубоком обучении рекуррентные нейронные сети (RNN) - это семейство нейронных сетей, которые превосходно обучаются на основе последовательных данных. Класс RNN, нашедший практическое применение, - это Long Short-Term Memory (LSTM), потому что он устойчив к проблемам долгосрочной зависимости. Нет недостатка в статьях и ссылках, объясняющих LSTM. Две рекомендуемые ссылки:

Глава 10 книги по глубокому обучению Гудфеллоу и др. др.

Понимание сетей LSTM, Крис Олах

Также нет недостатка в хороших библиотеках для создания приложений машинного обучения на основе LSTM. В GitHub Tensorflow от Google на момент написания этой статьи набрал более 50 000 звезд, что говорит о большой популярности среди практиков машинного обучения.

Чего, похоже, не хватает, так это хорошей документации и примера того, как создать простое для понимания приложение Tensorflow на основе LSTM. Это мотивация для этой статьи.

Предположим, мы хотим обучить LSTM предсказывать следующее слово, используя образец рассказа Басни Эзопа:

Давным-давно у мышей был общий совет, чтобы решить, какие меры они могут предпринять, чтобы перехитрить своего общего врага, кошку. некоторые говорили это, а некоторые говорили так, но в конце концов молодой мышонок встал и сказал, что у него есть предложение, которое, как он думал, будет подходящим. - Вы все согласитесь, - сказал он, - что наша главная опасность заключается в коварстве и коварстве врага, приближающегося к нам. теперь, если бы мы могли получить какой-нибудь сигнал о ее приближении, мы могли бы легко от нее сбежать. Я осмелюсь поэтому предложить раздобыть маленький колокольчик и привязать его ленточкой к шее кошки. таким образом, мы всегда должны знать, когда она была рядом, и могли легко уйти в отставку, пока она была по соседству. это предложение встретило всеобщие аплодисменты, пока старая мышка не встала и не сказала, что все в порядке, но кто будет звонить коту? мыши смотрели друг на друга, но никто не говорил. тогда старая мышь сказала, что легко предложить невозможные средства правовой защиты.

Листинг 1. Рассказ из басен Эзопа со 112 уникальными символами. И слова, и знаки препинания считаются символами.

Если мы введем в LSTM правильные последовательности из текста, состоящего из трех символов в качестве входных данных и одного помеченного символа, в конечном итоге нейронная сеть научится правильно предсказывать следующий символ (рисунок 1).

Рисунок 1. Ячейка LSTM с тремя входами и 1 выходом.

Технически входы LSTM могут понимать только действительные числа. Способ преобразования символа в число - присвоить каждому символу уникальное целое число в зависимости от частоты его появления. Например, в тексте выше 112 уникальных символов. Функция в листинге 2 создает словарь со следующими записями [«,»: 0] [«the»: 1],…, [«совет»: 37],…, [«говорил»: 111]. Также создается обратный словарь, поскольку он будет использоваться при декодировании вывода LSTM.

def build_dataset(words):
    count = collections.Counter(words).most_common()
    dictionary = dict()
    for word, _ in count:
        dictionary[word] = len(dictionary)
    reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
    return dictionary, reverse_dictionary

Листинг 2. Функция для построения словаря и обратного словаря.

Точно так же предсказание представляет собой уникальное целое число, идентифицирующее индекс в обратном словаре предсказанного символа. Например, если предсказание 37, предсказанный символ на самом деле является «советом».

Генерация вывода может показаться простой, но на самом деле LSTM создает 112-элементный вектор вероятностей предсказания для следующего символа, нормализованного функцией softmax (). Индекс элемента с наивысшей вероятностью - это предсказанный индекс символа в обратном словаре (т. Е. Один-горячий вектор). На рисунке 2 показан процесс.

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

В основе приложения лежит модель LSTM. Удивительно, но реализовать в Tensorflow очень просто:

def RNN(x, weights, biases):

    # reshape to [1, n_input]
    x = tf.reshape(x, [-1, n_input])

    # Generate a n_input-element sequence of inputs
    # (eg. [had] [a] [general] -> [20] [6] [33])
    x = tf.split(x,n_input,1)

    # 1-layer LSTM with n_hidden units.
    rnn_cell = rnn.BasicLSTMCell(n_hidden)

    # generate prediction
    outputs, states = rnn.static_rnn(rnn_cell, x, dtype=tf.float32)

    # there are n_input outputs but
    # we only want the last output
    return tf.matmul(outputs[-1], weights['out']) + biases['out']

Листинг 3. Модель с ячейкой LSTM размером 512 единиц.

Самая сложная часть - это подача входных данных в правильном формате и последовательности. В этом примере LSTM использует последовательность из 3 целых чисел (например, вектор 1x3 типа int).

Константы, веса и смещения:

vocab_size = len(dictionary)
n_input = 3
# number of units in RNN cell
n_hidden = 512
# RNN output node weights and biases
weights = {
    'out': tf.Variable(tf.random_normal([n_hidden, vocab_size]))
}
biases = {
    'out': tf.Variable(tf.random_normal([vocab_size]))
}

Листинг 4. Константы и параметры обучения

В процессе обучения на каждом шаге из обучающих данных извлекаются 3 символа. Эти 3 символа преобразуются в целые числа для формирования входного вектора.

symbols_in_keys = [ [dictionary[ str(training_data[i])]] for i in range(offset, offset+n_input) ]

Листинг 5. Символы вектора для int в качестве входных данных

Обучающая метка - это горячий вектор, идущий от символа после трех входных символов.

symbols_out_onehot = np.zeros([vocab_size], dtype=float)
symbols_out_onehot[dictionary[str(training_data[offset+n_input])]] = 1.0

Листинг 6. Один горячий вектор как метка

После изменения формы для соответствия словарю каналов оптимизация запускается:

_, acc, loss, onehot_pred = session.run([optimizer, accuracy, cost, pred], feed_dict={x: symbols_in_keys, y: symbols_out_onehot})

Листинг 7. Оптимизация шага обучения

Точность и потери накапливаются для отслеживания прогресса тренировки. 50 000 итераций обычно достаточно для достижения приемлемой точности.

...
Iter= 49000, Average Loss= 0.528684, Average Accuracy= 88.50%
['could', 'easily', 'retire'] - [while] vs [while]
Iter= 50000, Average Loss= 0.415811, Average Accuracy= 91.20%
['this', 'means', 'we'] - [should] vs [should]

Листинг 8. Пример данных прогнозирования и точности для каждой обучающей части (1000 шагов).

Стоимость представляет собой перекрестную энтропию между прогнозированием метки и softmax (), оптимизированным с помощью RMSProp со скоростью обучения 0,001. В этом случае RMSProp в целом работает лучше, чем Adam и SGD.

pred = RNN(x, weights, biases)

# Loss and optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
optimizer = tf.train.RMSPropOptimizer(learning_rate=learning_rate).minimize(cost)

Листинг 9. Потери и оптимизатор.

Точность LSTM можно повысить за счет дополнительных уровней.

rnn_cell = rnn.MultiRNNCell([rnn.BasicLSTMCell(n_hidden),rnn.BasicLSTMCell(n_hidden)])

Листинг 10. Улучшенный LSTM.

А теперь самое интересное. Давайте создадим историю, вернув предсказанный результат в качестве следующего символа во входных данных. Вход для этого примера выходных данных - «имел общий», и он предсказал правильный выходной «совет». «Совет» возвращается как часть новых входных данных «общий совет» для прогнозирования нового выхода «для» и так далее. Удивительно, но LSTM создает историю, которая почему-то имеет смысл.

had a general council to consider what measures they could take to outwit their common enemy , the cat . some said this , and some said that but at last a young mouse got

Листинг 11. Пример созданной истории. Усечено до 32 прогнозов.

Если мы скармливаем другую последовательность (например, «мышь», «мышь», «мышь»), но не обязательно последовательность, найденную в рассказе, автоматически создается другое повествование.

mouse mouse mouse , neighbourhood and could receive a outwit always the neck of the cat . some said this , and some said that but at last a young mouse got up and said

Листинг 12. Входные данные с последовательностью, не найденной в истории.

Актуальный пример кода можно найти здесь. Образец текстового файла находится здесь.

Заключительные примечания:

  1. Использовать int для кодирования символов легко, но «смысл» слова теряется. Символ для int используется для упрощения обсуждения создания приложения LSTM с использованием Tensorflow. Word2Vec - более оптимальный способ кодирования символов в вектор.
  2. Одноразовое векторное представление вывода неэффективно, особенно если у нас есть реалистичный размер словарного запаса. Оксфордский словарь насчитывает более 170 000 слов. В приведенном выше примере 112. Опять же, это только для упрощения обсуждения.
  3. Используемый здесь код вдохновлен примерами Tensorflow.
  4. Количество входов в этом примере - 3, посмотрите, что произойдет, если вы используете другие числа (например, 4, 5 или более).
  5. Выполнение кода каждый раз может генерировать разные результаты и возможности прогнозирования, поскольку точность зависит от начальных случайных значений параметров. Лучшая точность может быть достигнута на более высоких ступенях обучения (более 150 000). Ожидайте, что при каждом запуске будет другой словарь.
  6. Tensorboard полезен при отладке, особенно при определении правильности построения графа кодом.
  7. Попробуйте использовать другую историю, особенно на другом языке.