Рубрики
Без рубрики

Решение задач последовательности с помощью LSTM в Keras: Часть 2

Автор оригинала: Usman Malik.

Это вторая и заключительная часть серии статей из двух частей, посвященных решению задач последовательности с помощью LSTMs. В части 1 этой серии я объяснил , как решать задачи последовательности “один к одному” и “многие к одному” с помощью LSTM. В этой части вы увидите, как решать задачи последовательности “один ко многим” и “многие ко многим” с помощью LSTM в Keras.

Субтитры к изображениям-это классический пример проблем с последовательностью “один ко многим”, когда у вас есть одно изображение в качестве входных данных, и вы должны предсказать описание изображения в виде последовательности слов. Аналогичным образом, прогнозирование фондового рынка на следующие X дней, где входными данными являются цены акций за предыдущие дни года, является классическим примером проблем последовательности “многие ко многим”.

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

Проблемы последовательности “Один ко многим”

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

Проблемы последовательности “Один ко многим” с одной функцией

Давайте сначала создадим набор данных и поймем проблему, которую мы собираемся решить в этом разделе.

Создание набора данных

Следующий сценарий импортирует необходимые библиотеки:

from numpy import array
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers.core import Activation, Dropout, Dense
from keras.layers import Flatten, LSTM
from keras.layers import GlobalMaxPooling1D
from keras.models import Model
from keras.layers.embeddings import Embedding
from sklearn.model_selection import train_test_split
from keras.preprocessing.text import Tokenizer
from keras.layers import Input
from keras.layers.merge import Concatenate
from keras.layers import Bidirectional

import pandas as pd
import numpy as np
import re

import matplotlib.pyplot as plt

И следующий сценарий создает набор данных:

X = list()
Y = list()
X = [x+3 for x in range(-2, 43, 3)]

for i in X:
    output_vector = list()
    output_vector.append(i+1)
    output_vector.append(i+2)
    Y.append(output_vector)

print(X)
print(Y)

Вот результат:

[1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43]
[[2, 3], [5, 6], [8, 9], [11, 12], [14, 15], [17, 18], [20, 21], [23, 24], [26, 27], [29, 30], [32, 33], [35, 36], [38, 39], [41, 42], [44, 45]]

Наши входные данные содержат 15 выборок с одним временным шагом и одним значением функции. Для каждого значения во входном образце соответствующий выходной вектор содержит следующие два целых числа. Например, если входной сигнал равен 4, выходной вектор будет содержать значения 5 и 6. Следовательно, задача является простой задачей последовательности “один ко многим”.

Следующий сценарий изменяет наши данные в соответствии с требованиями LSTM:

X = np.array(X).reshape(15, 1, 1)
Y = np.array(Y)

Теперь мы можем обучать наших моделей. Мы будем тренировать простой и сложенный Lstm.

Решение с помощью простого LSTM
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(1, 1)))
model.add(Dense(2))
model.compile(optimizer='adam', loss='mse')
model.fit(X, Y, epochs=1000, validation_split=0.2, batch_size=3)

После того как модель обучена мы можем делать прогнозы на основе тестовых данных:

test_input = array([10])
test_input = test_input.reshape((1, 1, 1))
test_output = model.predict(test_input, verbose=0)
print(test_output)

Тестовые данные содержат значение 10. На выходе мы должны получить вектор, содержащий 11 и 12. Результат, который я получил, равен [10.982891 12.109697], что на самом деле очень близко к ожидаемому результату.

Решение через Stacked LSTM

Следующий сценарий тренирует сложенные LSTMS на наших данных и делает прогноз по тестовым точкам:

model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(1, 1)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(2))
model.compile(optimizer='adam', loss='mse')
history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

test_output = model.predict(test_input, verbose=0)
print(test_output)

Ответ – [11.00432 11.99205], что очень близко к фактическому выходу.

Решение с помощью двунаправленного LSTM

Следующий сценарий обучает двунаправленный LSTM на наших данных, а затем делает прогноз на тестовом наборе.

from keras.layers import Bidirectional

model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(1, 1)))
model.add(Dense(2))
model.compile(optimizer='adam', loss='mse')

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)
test_output = model.predict(test_input, verbose=0)
print(test_output)

Результат, который я получил, – это [11.035181 12.082813]

Проблемы последовательности “Один ко многим” с несколькими функциями

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

Создание набора данных

Как всегда, первым шагом является создание набора данных:

nums = 25

X1 = list()
X2 = list()
X = list()
Y = list()

X1 = [(x+1)*2 for x in range(25)]
X2 = [(x+1)*3 for x in range(25)]

for x1, x2 in zip(X1, X2):
    output_vector = list()
    output_vector.append(x1+1)
    output_vector.append(x2+1)
    Y.append(output_vector)

X = np.column_stack((X1, X2))
print(X)

Наш входной набор данных выглядит следующим образом:

[[ 2  3]
 [ 4  6]
 [ 6  9]
 [ 8 12]
 [10 15]
 [12 18]
 [14 21]
 [16 24]
 [18 27]
 [20 30]
 [22 33]
 [24 36]
 [26 39]
 [28 42]
 [30 45]
 [32 48]
 [34 51]
 [36 54]
 [38 57]
 [40 60]
 [42 63]
 [44 66]
 [46 69]
 [48 72]
 [50 75]]

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

Давайте изменим наши данные:

X = np.array(X).reshape(25, 1, 2)
Y = np.array(Y)
Решение с помощью простого LSTM
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(1, 2)))
model.add(Dense(2))
model.compile(optimizer='adam', loss='mse')
model.fit(X, Y, epochs=1000, validation_split=0.2, batch_size=3)

Давайте теперь создадим нашу тестовую точку и посмотрим, насколько хорошо работает наш алгоритм:

test_input = array([40, 60])
test_input = test_input.reshape((1, 1, 2))
test_output = model.predict(test_input, verbose=0)
print(test_output)

Вход – [40, 60], выход – [41, 61]. Выход, предсказанный нашим простым LSTM, равен [40.946873 60.941723], что очень близко к ожидаемому выходу.

Решение через Stacked LSTM
model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(1, 2)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(2))
model.compile(optimizer='adam', loss='mse')
history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

test_input = array([40, 60])
test_input = test_input.reshape((1, 1, 2))
test_output = model.predict(test_input, verbose=0)
print(test_output)

Выход в этом случае: [40.978477 60.994644]

Решение с помощью двунаправленного LSTM
from keras.layers import Bidirectional

model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(1, 2)))
model.add(Dense(2))
model.compile(optimizer='adam', loss='mse')

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)
test_output = model.predict(test_input, verbose=0)
print(test_output)

Полученный результат: [41.0975 61.159065]

Проблемы последовательности “Многие ко многим”

В задачах “один ко многим” и “многие к одному” мы видели, что выходной вектор может содержать несколько значений. В зависимости от задачи выходной вектор, содержащий несколько значений, можно рассматривать как имеющий один (так как выход содержит один временной шаг данных в строгих терминах) или несколько (так как один вектор содержит несколько значений) выходов.

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

Модель Кодер-декодер

Для решения таких задач последовательности была разработана модель кодер-декодер. Модель кодер-декодер – это в основном причудливое название архитектуры нейронной сети с двумя слоями LSTM.

Первый слой работает как кодирующий слой и кодирует входную последовательность. Декодер также является слоем LSTM, который принимает три входа: кодированную последовательность из кодера LSTM, предыдущее скрытое состояние и текущий вход. Во время обучения фактический выход на каждом временном шаге используется для обучения модели кодер-декодер. При выполнении предсказаний выход кодера, текущее скрытое состояние и предыдущий выход используются в качестве входных данных для выполнения предсказания на каждом временном шаге. Эти понятия станут более понятными, когда вы увидите их в действии в следующем разделе.

Проблемы последовательности “Многие ко многим” с одной функцией

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

Давайте сначала создадим наш набор данных.

Создание набора данных
X = list()
Y = list()
X = [x for x in range(5, 301, 5)]
Y = [y for y in range(20, 316, 5)]

X = np.array(X).reshape(20, 3, 1)
Y = np.array(Y).reshape(20, 3, 1)

Вход X содержит 20 выборок, где каждая выборка содержит 3 временных шага с одной функцией. Один входной образец выглядит следующим образом:

[[[  5]
  [ 10]
  [ 15]]

Вы можете видеть, что входной образец содержит 3 значения, которые в основном являются 3 последовательными кратными 5. Соответствующая выходная последовательность для приведенного выше входного образца выглядит следующим образом:

[[[ 20]
  [ 25]
  [ 30]]

Выходные данные содержат следующие три последовательных кратных 5. Вы можете видеть, что результат в этом случае отличается от того, что мы видели в предыдущих разделах. Для модели кодер-декодер выходные данные также должны быть преобразованы в 3D-формат, содержащий количество выборок, временных шагов и объектов. Это происходит потому, что декодер генерирует выходной сигнал на каждый временной шаг.

Мы создали наш набор данных; следующий шаг-обучение наших моделей. В следующих разделах мы будем обучать сложенные модели LSTM и двунаправленные модели LSTM.

Решение через Stacked LSTM

Следующий сценарий создает модель кодер-декодер с использованием stacked Lstm:

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()

# encoder layer
model.add(LSTM(100, activation='relu', input_shape=(3, 1)))

# repeat vector
model.add(RepeatVector(3))

# decoder layer
model.add(LSTM(100, activation='relu', return_sequences=True))

model.add(TimeDistributed(Dense(1)))
model.compile(optimizer='adam', loss='mse')

print(model.summary())

В приведенном выше сценарии первый слой LSTM-это слой кодера.

Затем мы добавили вектор повторения в нашу модель. Вектор повторения принимает выходной сигнал от кодера и повторно подает его в качестве входного сигнала на каждом временном шаге в декодер. Например, на выходе мы имеем три временных шага. Для предсказания каждого выходного временного шага декодер будет использовать значение из вектора повторения, скрытое состояние из предыдущего выхода и текущий вход.

Далее у нас есть слой декодера. Поскольку выходные данные представлены в виде временного шага, который является 3D-форматом, параметр return_sequences для модели декодера был установлен True . Слой Time Distributed используется для индивидуального прогнозирования выходных данных для каждого временного шага.

Краткое описание модели для модели кодер-декодер, созданной в приведенном выше скрипте, выглядит следующим образом:

Layer (type)                 Output Shape              Param #
=================================================================
lstm_40 (LSTM)               (None, 100)               40800
_________________________________________________________________
repeat_vector_7 (RepeatVecto (None, 3, 100)            0
_________________________________________________________________
lstm_41 (LSTM)               (None, 3, 100)            80400
_________________________________________________________________
time_distributed_7 (TimeDist (None, 3, 1)              101
=================================================================
Total params: 121,301
Trainable params: 121,301
Non-trainable params: 0

Вы можете видеть, что вектор повтора только повторяет выход кодера и не имеет параметров для обучения.

Следующий сценарий обучает приведенную выше модель кодера-декодера.

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

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

test_input = array([300, 305, 310])
test_input = test_input.reshape((1, 3, 1))
test_output = model.predict(test_input, verbose=0)
print(test_output)

Наша входная последовательность содержит три значения временного шага 300, 305 и 310. Выход должен быть следующими тремя кратными 5, то есть 315, 320 и 325. Я получил следующий вывод:

[[[316.02878]
  [322.27145]
  [328.5536 ]]]

Вы можете видеть, что выходные данные находятся в 3D-формате.

Решение с помощью двунаправленного LSTM

Давайте теперь создадим модель кодер-декодер с двунаправленным Lstm и посмотрим, сможем ли мы получить лучшие результаты:

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()
model.add(Bidirectional(LSTM(100, activation='relu', input_shape=(3, 1))))
model.add(RepeatVector(3))
model.add(Bidirectional(LSTM(100, activation='relu', return_sequences=True)))
model.add(TimeDistributed(Dense(1)))
model.compile(optimizer='adam', loss='mse')

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

Приведенный выше сценарий обучает модель кодер-декодер с помощью двунаправленного LSTM. Давайте теперь сделаем предсказания на тестовой точке, то есть [300, 305, 310].

test_output = model.predict(test_input, verbose=0)
print(test_output)

Вот результат:

[[[315.7526 ]
  [321.47153]
  [327.94025]]]

Выход, который я получил с помощью двунаправленных LSTMS, лучше, чем то, что я получил с помощью простой модели кодера-декодера на основе stacked LSTM.

Проблемы последовательности “Многие ко многим” с несколькими функциями

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

Создание набора данных

Давайте создадим простой набор данных для нашей задачи:

X = list()
Y = list()
X1 = [x1 for x1 in range(5, 301, 5)]
X2 = [x2 for x2 in range(20, 316, 5)]
Y = [y for y in range(35, 331, 5)]

X = np.column_stack((X1, X2))

В приведенном выше скрипте мы создаем два списка X1 и X2 . Список X1 содержит все кратные 5 от 5 до 300 (включительно), а список X2 содержит все кратные 5 от 20 до 315 (включительно). Наконец, список Y , который оказывается выходным, содержит все кратные 5 между 35 и 330 (включительно). Окончательный входной список X представляет собой столбцовое слияние X1 и X2 .

Как всегда, нам нужно изменить наши входные X и выходные Y , прежде чем их можно будет использовать для обучения LSTM.

X = np.array(X).reshape(20, 3, 2)
Y = np.array(Y).reshape(20, 3, 1)

Вы можете видеть, что вход X был преобразован в 20 выборок из трех временных шагов с 2 объектами, где выход был преобразован в аналогичные размеры, но с 1 объектом.

Первый образец из входных данных выглядит следующим образом:

[[ 5  20]
[ 10  25]
[ 15  30]]

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

[[ 35]
[ 40]
[ 45]]

Как вы можете видеть, выходные данные содержат следующие три последовательных кратных 5.

Давайте теперь обучим нашу модель кодер-декодер, чтобы выучить приведенную выше последовательность. Сначала мы обучим простой штабелированный кодер-декодер на основе LSTM.

Решение через Stacked LSTM

Следующий сценарий обучает сложенную модель LSTM. Вы можете видеть, что входная форма теперь (3, 2) соответствует трем временным шагам и двум объектам на входе.

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()
model.add(LSTM(100, activation='relu', input_shape=(3, 2)))
model.add(RepeatVector(3))
model.add(LSTM(100, activation='relu', return_sequences=True))
model.add(TimeDistributed(Dense(1)))
model.compile(optimizer='adam', loss='mse')

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

Теперь давайте создадим тестовую точку, которая будет использоваться для составления прогноза.

X1 = [300, 305, 310]
X2 = [315, 320, 325]

test_input = np.column_stack((X1, X2))

test_input = test_input.reshape((1, 3, 2))
print(test_input)

Тестовая точка выглядит следующим образом:

[[[300 315]
  [305 320]
  [310 325]]]

Фактический выход вышеуказанной контрольной точки равен [330, 335, 340]. Давайте посмотрим, что предсказывает модель:

test_output = model.predict(test_input, verbose=0)
print(test_output)

Прогнозируемый выход таков:

[[[324.5786 ]
  [328.89658]
  [335.67603]]]

Результат далек от того, чтобы быть правильным.

Решение с помощью двунаправленного LSTM

Давайте теперь обучим модель кодер-декодер на основе двунаправленных LSTMS и посмотрим, сможем ли мы получить улучшенные результаты. Следующий сценарий обучает модель.

from keras.layers import RepeatVector
from keras.layers import TimeDistributed

model = Sequential()
model.add(Bidirectional(LSTM(100, activation='relu', input_shape=(3, 2))))
model.add(RepeatVector(3))
model.add(Bidirectional(LSTM(100, activation='relu', return_sequences=True)))
model.add(TimeDistributed(Dense(1)))
model.compile(optimizer='adam', loss='mse')

history = model.fit(X, Y, epochs=1000, validation_split=0.2, verbose=1, batch_size=3)

Следующий сценарий делает прогнозы на тестовом наборе:

test_output = model.predict(test_input, verbose=0)
print(test_output)

Вот результат:

[[[330.49133]
  [335.35327]
  [339.64398]]]

Полученный результат довольно близок к фактическому, то есть [330, 335, 340]. Следовательно, наш двунаправленный LSTM превзошел простой LSTM.

Вывод

Это вторая часть моей статьи “Решение задач последовательности с LSTM в Keras” ( часть 1 здесь ). В этой статье вы видели, как решать задачи последовательности “один ко многим” и “многие ко многим” в LSTM. Вы также видели, как модель кодер-декодер может быть использована для прогнозирования многоступенчатых выходов. Модель кодер-декодер используется в различных приложениях обработки естественного языка, таких как нейронный машинный перевод и разработка чат-ботов.

В следующей статье мы рассмотрим применение модели кодер-декодер в НЛП.