Это вторая и последняя часть из двух частей серии статей о решении проблем последовательности с помощью LSTM. В первой части этой серии статей я объяснил, как решать задачи последовательности один-к-одному и многие-к-одному с помощью LSTM. В этой части вы увидите, как решать проблемы последовательности «один ко многим» и «многие ко многим» с помощью LSTM в Keras.
Подписи к изображениям - классический пример проблем с последовательностью «один ко многим», когда в качестве входных данных используется одно изображение, и вы должны предсказать описание изображения в виде последовательности слов. Точно так же прогноз фондового рынка на следующие X дней, где входными данными является цена акций за предыдущие Y дней, является классическим примером задач последовательности «многие ко многим».
В этой статье вы увидите очень простые примеры задач «один ко многим» и «многие ко многим». Однако концепции, изученные в этой статье, заложат основу для решения сложных задач последовательности, таких как прогнозирование цен на акции и автоматические подписи к изображениям, которые мы увидим в следующих статьях.
Проблемы последовательности "один ко многим"
Проблемы последовательности "один ко многим" - это тип проблем последовательности, в которых входные данные имеют один временной шаг, а выходные данные содержат вектор из нескольких значений или нескольких временных шагов. В этом разделе мы увидим, как решить задачи последовательности «один ко многим», когда входные данные имеют одну функцию. Затем мы перейдем к рассмотрению того, как работать с несколькими входными функциями для решения задач последовательности «один ко многим».
Задачи последовательности один-ко-многим с одним признаком
Давайте сначала создадим набор данных и поймем проблему, которую мы собираемся решить в этом разделе.
Создание набора данных
Следующий скрипт импортирует необходимые библиотеки:
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.
Решение через Simple 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
Следующий скрипт обучает сложенные LSTM на наших данных и делает прогнозы на тестовых точках:
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)
Решение через Simple 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
Следующий скрипт создает модель кодировщика-декодера с использованием составных 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
. TimeDistributed
используется
для индивидуального прогнозирования выходных данных для каждого
временного шага.
Краткое описание модели кодировщика-декодера, созданной в приведенном выше скрипте, выглядит следующим образом:
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]]]
Вывод, который я получил с помощью двунаправленных LSTM, лучше, чем то, что я получил с помощью простой модели кодировщика-декодера на основе 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
Давайте теперь обучим модель кодировщика-декодера на основе двунаправленных LSTM и посмотрим, сможем ли мы получить улучшенные результаты. Следующий сценарий обучает модель.
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. Вы также видели, как модель кодер-декодер может использоваться для прогнозирования многоступенчатых выходных данных. Модель кодировщика-декодера используется во множестве приложений обработки естественного языка, таких как нейронный машинный перевод и разработка чат-ботов.
В следующей статье мы увидим применение модели кодировщика-декодера в НЛП.