LSTM — нейронная сеть с долгой краткосрочной памятью
Люди не запускают мыслительный процесс с нуля в каждый момент времени. Читая статью, вы понимаете смысл каждого слова на основе значений предыдущих слов. Мысли имеют свойство накапливаться и влиять друг на друга. Этот принцип используется в сетях LSTM.
Простые нейронные сети не могут этого сделать, и это серьезный недостаток. Представьте, что вы хотите в реальном времени классифицировать события, происходящие в фильме. Неясно, как обычная нейронная сеть может использовать знания о предыдущих событиях, чтобы изучить последующие.
Рекуррентные нейронные сети
Рекуррентные нейронные сети (РНС) решают эту проблему. В них присутствуют циклы, сохраняющие информацию.
На приведенной выше диаграмме часть нейронной сети A принимает входной сигнал x и выводит значение h. Цикл позволяет передавать информацию с одного шага сети на другой.
Из-за наличия циклов РНС выглядят загадочно. Однако если подумать, становится понятно, что они не отличаются от простой нейронной сети. РНС можно представить как несколько копий одной и той же сети, каждый из которых передает сообщение преемнику. Рассмотрим, что произойдет, если мы выполним развертку цикла:
Развернутая структура РНС демонстрирует, что повторяющиеся нейронные сети тесно связаны с последовательностями и списками. Это естественная архитектура нейронной сети, используемая для данных такого типа.
И они, безусловно, используются! В последние годы достигнут невероятный успех в применении РНС к широкому кругу проблем: распознавание речи, лингвистическое моделирование, перевод, описание изображений… Этот список можно продолжить.
Существенным подспорьем в решении перечисленных задач стали LSTM — специфического типа рекуррентные нейронные сети, которые решают отдельные задачи гораздо эффективнее стандартных методов. С использованием сетей LSTM связаны все захватывающие результаты, основанные на РНС.
Проблема долгосрочных связей
Рекуррентная нейронная сеть используют полученную ранее информацию для решения последующих задач, например, уже полученные видеофрагменты для анализа последующих.
Иногда для решения задачи требуется просмотреть только последнюю информацию. Например, построить лингвистическую модель, которая пытается предсказать следующее слово, основываясь на предыдущих. Если мы пытаемся предсказать последнее слово в словосочетании «облака в небе», нам не нужен дополнительный контекст — очевидно, что следующее слово будет «небе». В тех случаях, когда разрыв между предыдущей информацией и местом, в котором она нужна, невелик, РНС справится с задачей.
Но иногда требуется больше контекста. Рассмотрим попытку предсказать последнее слово в тексте «Я вырос во Франции … Я свободно говорю на французском». Из предыдущих слов понятно, что следующим словом, вероятно, будет название языка, но если мы хотим назвать правильный язык, нужно учесть упоминание о Франции. Разрыв между необходимой для учета информацией и точкой, в которой она нужна, становится бОльшим.
К сожалению, по мере увеличения этого разрыва РНС теряют связь между информацией.
В теории РНС способны справиться с такими «долгосрочными зависимостями». Исследователь может тщательно подобрать параметры сети для устранения этой проблемы. К сожалению, на практике РНС не способны решить эту задачу. Проблема подробно исследована в работах [Hochreiter (1991)] и [Bengio, et al. (1994)], в которых выявлены фундаментальные ограничения РНС.
К счастью, у LSTM нет этой проблемы!
Сети LSTM
LSTM (long short-term memory, дословно (долгая краткосрочная память) — тип рекуррентной нейронной сети, способный обучаться долгосрочным зависимостям. LSTM были представлены в работе [Hochreiter & Schmidhuber (1997)], впоследствии усовершенствованы и популяризированы другими исследователями, хорошо справляются со многими задачами и до сих пор широко применяются.
LSTM специально разработаны для устранения проблемы долгосрочной зависимости. Их специализация — запоминание информации в течение длительных периодов времени, поэтому их практически не нужно обучать!
Все рекуррентные нейронные сети имеют форму цепочки повторяющихся модулей нейронной сети. В стандартных РНС этот повторяющийся модуль имеет простую структуру, например, один слой tanh.
Не думайте о деталях, давайте просто постараемся запомнить обозначения, которые мы будем использовать.
На приведенной выше диаграмме каждая линия является вектором. Розовый круг означает поточечные операции, например, суммирование векторов. Под желтыми ячейками понимаются слои нейронной сети. Совмещение линий есть объединение векторов, а знак разветвления — копирование вектора с последующим хранением в разных местах.
Принцип работы LSTM сети
Ключевым понятием LSTM является состояние ячейки: горизонтальная линия, проходящая через верхнюю часть диаграммы.
Состояние ячейки напоминает конвейерную ленту. Оно проходит через всю цепочку, подвергаясь незначительным линейным преобразованиям.
В LSTM уменьшает или увеличивает количество информации в состоянии ячейки, в зависимости от потребностей. Для этого используются тщательно настраиваемые структуры, называемые гейтами.
Гейт — это «ворота», пропускающие или не пропускающие информацию. Гейты состоят из сигмовидного слоя нейронной сети и операции поточечного умножения.
На выходе сигмовидного слоя выдаются числа от нуля до единицы, определяя, сколько процентов каждой единицы информации пропустить дальше. Значение «0» означает «не пропустить ничего», значение «1» — «пропустить все».
Пошаговая схема работы LSTM сети
LSTM имеет три таких гейта для контроля состояния ячейки.
Слой утраты
На первом этапе LSTM нужно решить, какую информацию мы собираемся выбросить из состояния ячейки. Это решение принимается сигмовидным слоем, называемым «слоем гейта утраты». Он получает на вход h и x и выдает число от 0 до 1 для каждого номера в состоянии ячейки C. 1 означает «полностью сохранить», а 0 — «полностью удалить».
Вернемся к нашему примеру лингвистической модел. Попытаемся предсказать следующее слово, основанное на всех предыдущих. В такой задаче состояние ячейки включает языковой род подлежащего, чтобы использовать правильные местоимения. Когда появляется новое подлежащее, уже требуется забыть род предыдущего подлежащего.
Слой сохранения
На следующем шаге нужно решить, какую новую информацию сохранить в состоянии ячейки. Разобьем процесс на две части. Сначала сигмоидный слой, называемый «слоем гейта входа», решает, какие значения требуется обновить. Затем слой tanh создает вектор новых значений-кандидатов C, которые добавляются в состояние. На следующем шаге мы объединим эти два значения для обновления состояния.
В примере нашей лингвистической модели мы хотели бы добавить род нового подлежащего в состояние ячейки, чтобы заменить им род старого.
Новое состояние
Теперь обновим предыдущее состояние ячейки для получения нового состояния C. Способ обновления выбран, теперь реализуем само обновление.
Умножим старое состояние на f, теряя информацию, которую решили забыть. Затем добавляем i*C. Это новые значения кандидатов, масштабируемые в зависимости от того, как мы решили обновить каждое значение состояния.
В случае с лингвистической моделью мы отбросим информацию о роде старого субъекта и добавим новую информацию.
Наконец, нужно решить, что хотим получить на выходе. Результат будет являться отфильтрованным состоянием ячейки. Сначала запускаем сигмоидный слой, который решает, какие части состояния ячейки выводить. Затем пропускаем состояние ячейки через tanh (чтобы разместить все значения в интервале [-1, 1]) и умножаем его на выходной сигнал сигмовидного гейта.
Для лингвистической модели, так как сеть работала лишь с подлежащим, она может вывести информацию, относящуюся к глаголу. Например, сеть выведет информацию о том, в каком числе представлено подлежащее (единственное или множественное) для правильного спряжения глагола.
Примеры LSTM
Описанная выше схема — традиционная для LSTM. Но не все LSTM идентичны. На самом деле почти в каждой статье используются отличающиеся версии. Различия незначительны, но стоит упомянуть о некоторых из них.
В популярном варианте LSTM, представленном в [Gers & Schmidhuber (2000)], мы позволяем слоям гейтов просматривать состояние ячейки.
На диаграмме вверху «глазок» есть у всех гейтов, но во многих статьях он есть лишь у некоторых гейтов.
Другой вариант — использование связанных гейтов утраты и входа. Вместо того, чтобы отдельно решать, что забыть, а к чему добавить новую информацию, мы принимаем эти решения одновременно. Мы забываем информацию только тогда, когда нужно поместить что-то новое на том же месте. Новые значения вносятся в состояние только тогда, когда мы забываем что-то более старое.
Несколько отличающейся пример LSTM — Gated Recurrent Unit или GRU, введенная в [Cho, et. al. (2014)]. Она объединяет гейты утраты и входа в единый «шлюз обновления». В нем также объединяются состояние ячейки и скрытое состояние и вносятся некоторые другие изменения. Получающаяся модель проще по сравнению со стандартными типами LSTM и становится все более популярной.
Это лишь некоторые из наиболее известных вариантов LSTM. Есть много других, таких как РНС с гейтом глубины (см. [Yao, et al. (2015)]). Известен также совершенно иной подход к решению долгосрочных зависимостей, приведенный в [Clockwork RNN by Koutnik, et al. (2014)].
Какой из перечисленных вариантов лучший? Имеют ли большое значение различия? В [Greff, et al. (2015)] приведено широкое сравнение популярных версий LSTM, а также продемонстрировано, что все они примерно одинаковы. В [Jozefowicz, et al. (2015)] опробованы более десяти тысяч архитектур РНС. Некоторые из них работают лучше LSTM при решении определенных задач.
Заключение
Представленные как набор уравнений, LSTM выглядят довольно устрашающе. Надеюсь, проход всей схемы шаг за шагом в этом посте сделало их немного более доступными.
LSTM были большим шагом в развитии РНС. Естественно задаться вопросом: а что, можно пойти дальше? Общее для исследователей мнение: «Да! Cледующий шаг заключается в использовании механизма внимания!» Идея состоит в том, чтобы РНС на каждом шаге выбирала информацию для просмотра из некого большего количества данных. Например, если вы используете РНС для создания описания изображения, сеть может выбрать часть изображения для просмотра каждого выводимого слова. Именно это и было проделано в [Xu, et al. (2015)] и станет отправной точкой для изучения механизма внимания.
Интересные статьи:
- Как работает сверточная нейронная сеть: архитектура, примеры, особенности
- Генеративно-состязательная нейросеть (GAN). Руководство для новичков
- Как создать собственную нейронную сеть с нуля на языке Python
Мама мыла LSTM: как устроены рекуррентные нейросети с долгой краткосрочной памятью
Крафтовый техно-лонгрид издания Системный Блокъ, в котором мы разбираем по винтикам одну из самых ходовых технологий в современной компьютерной лингвистике — рекуррентные нейросети с архитектурой LSTM
Этот текст — про то, что такое языковая модель и зачем она нужна. Еще расскажем, почему рекуррентная нейросеть (RNN), хорошо подходит под машинную обработку языка и как работает LSTM — усложненная модель RNN.
Зачем обрабатывать текст на компьютере
Чтобы научить его подражать человеку
Было бы круто научить компьютер генерировать связный текст, выделять логические конструкции, потом делать с ними что-нибудь интересное, как умеет человек. Может получиться чат-бот, поисковая машина, «умная» клавиатура на телефоне, онлайн-переводчик, генератор пересказов. Эти задачи решает обработка естественного языка. С ней есть сложности: в языке бывают омонимы, бывают многозначные слова. А что делать, если «Трофей не поместился в чемодан, потому что он был слишком большим»? Как тут программе сориентироваться, к чему относится слово «он»?
К счастью, речь людей статистически предсказуема. Есть популярные цепочки слов, которые повторяют почти все. Велика вероятность после слов «чайник уже» найти слово «вскипел». И напротив, есть последовательности, которые никогда не услышишь в речи. Например, «чайник уже… обиделся».
Как использовать неслучайность речи?
Можно предсказывать самые вероятные слова
В компьютерной лингвистике есть понятие «языковая модель». Она описывает вероятность встретить в речи человека данную последовательность слов или символов. Насколько вероятно, что произвольный набор слов может быть нормальным предложением?
Языковые модели полезны: они могут, например, генерировать текст. Допустим, известно, что первое слово в предложении — «кошка». В базе данных языковой модели указано, что если «кошка» — часть последовательности, то после нее будет стоять слово «села». Записываем в блокноте слова «кошка села» и снова подаем на вход языковой модели. Модель знает, что после «кошка села» вероятнее всего встретить слово «на», а после «кошка села на» — слово «такси». Кошка села на такси.
Идея в том, чтобы итеративно (шаг за шагом) давать языковой модели дополнять самым вероятным следующим словом то, что она написала на предыдущем шаге. Типичный пример — клавиатура GBoard. Автор этого текста ввел на клавиатуре слово «английский» — модель предложила продолжение «язык». Дальше автор слово за словом дополнял фразу подсказками от модели, пока не получилось «английский язык аж в общем я не могу найти в интернете». Все такое видели. На вашем телефоне результат может быть иным, потому что GBoard запоминает цепочки введенных слов и обучается на них (если стоит соответствующая галочка в настройках). Но почему получается такая бессмыслица?
Вкратце: чтобы сделать хорошо, нужно учитывать больше контекста, а не одно-два последних слова. Но это уже не реализуешь «в лоб».
Языковая модель без нейросетей
Языковая модель под капотом у экранной клавиатуры — несложная по сравнению с большими современными нейросетями, до которых мы еще доберемся. Как именно языковая модель решает, возможно ли после «манная» найти слово «каша»? Очень популярный вариант — использовать цепи Маркова (скорее всего, в вашем смартфоне они и используются — это гораздо легче нейросетей, а результат часто не сильно уступает).
Для этого разбиваем большой текст на биграммы (куски по два слова подряд) и смотрим, ага, биграмма «солнце ярко» встретилось 100 раз, а все остальные биграммы с «солнцем» — гораздо меньше. Может, в этом тексте часто повторяли «солнце ярко светило». А иногда было «взошло солнце. Запели петухи»: Тогда нужная нам биграмма получится «солнце запели». Но она встречалась реже, а после «солнца» чаще всего стоит «ярко». Так и запишем. В следующий раз после «солнца» подставим «ярко».
Если у разработчика мощные компьютеры, он может смотреть не на одно, а на три, на пять, на десять слов назад и все их учитывать. Но резко возрастает сложность вычислений: если в словаре сто слов, и для каждого из них мы записали самое вероятное следующее, это сто записей. Если же в словаре сто слов, и мы хотим смотреть на два слова назад, как на цельную последовательность, придется сохранять в словаре все комбинации из двух слов, то есть 100 в квадрате (10000), и писать самое вероятное следующее для каждой из них. Для трех слов назад — сто в кубе вариантов (1000000) комбинаций и так далее.
Дела еще усложняются, если учесть, что сохраняем не только самое вероятное следующее слово, а несколько самых вероятных, а то и распределение вероятностей для всего словаря, да и словарь содержит не сто слов. В общем, у такой языковой модели очень короткая память, иначе она сразу сжигает компьютер.
Поэтому важная задача разработчика языковой модели — обеспечить ей «долгосрочную память». Одно из решений — рекуррентные нейросети.
Что такое рекуррентность?
Рекуррентность нейросети означает, что она смотрит на свою работу в прошлом
Почему нейросеть — рекуррентная? Рекуррентный — значит, регулярно к чему-то возвращающийся. Рекуррентная нейросеть возвращается к своей работе с прошлого шага, потому и получила свое название.
Условимся, что работаем с текстом и хотим дописывать его по одному слову, опираясь на предыдущие слова. Как раньше делали с экранной клавиатурой, только теперь с нейросетью. Нейросеть не понимает человеческие слова, для работы с ней их нужно закодировать в виде векторов, то есть наборов чисел.
Вот простой способ: считаем слова в словаре, с которым мы работаем: допустим, их сто. Нарисуем сто нулей и посмотрим, какой номер по алфавиту у слова, которое мы кодируем. Оно 53-е. Тогда среди ста нулей заменим 53-й ноль на единицу, получится вектор вроде 000010000, только длиннее. Такой подход называется one-hot кодированием, и он не очень крутой, потому что вектор выходит слишком уж длинный.
Более продвинутый вариант — взять вектора, полученные через word2vec, FastText или другие алгоритмы кодирования, о них мы писали ранее. В них внутрь вектора «зашивается» информация о синонимах слова и его семантических отношениях с другими словами. Как именно эта информация туда попадает — читайте в наших статьях по ссылкам (1, 2).
Нейросеть — это несколько групп (слоев) нейронов. С отдельным нейроном никто не работает: в компьютере хранятся и обрабатываются вектора (одномерные строки из чисел), матрицы (двумерные таблицы) или тензоры (трехмерные «стопки» из матриц). Мы упростим и будем писать, что везде вектор или матрица. Для разработчика важно, что нейрон — это число, которое надо хранить, изменять и куда-то передавать.
Когда слово попадает на слой нейросети, некоторые нейроны меняют значение (активируются) и по картине активации предсказывается словарный номер следующего слова в тексте. Сначала это случайные активации и случайные слова, но после обучения нейросети мера ошибки уменьшится, и слова будут похожи на правду. Активироваться (можете представить себе, что они включаются или загораются как лампочки) будут уже не случайные нейроны, а вполне конкретные. Ага, получается, если тренированная нейросеть получает слово «мыла» после слова «мама», всегда зажигаются вон те 15 нейронов. Наверное, в этом есть какой-то смысл? Да, смысл есть. Если слово активирует одни и те же нейроны, активацию (состояние нейронов) можно рассматривать как «след» контекста.
RNN сохраняет свое состояние и передает его дальше
В RNN есть числовой вектор (т.е. список циферок), где хранится память о контексте: сохраним его и сделаем входными данными на следующем слое.
Вот важный момент: состояния нейронов в слое представим вектором из нулей и единиц (подряд выписав состояния). Перед началом работы все слои либо деактивированы (вектор 0000000000000, нулей в нем — по количеству нейронов в слое), либо заданы случайно.
На вход подается первое слово — мама. Некоторые нейроны активируются согласно правилу (функции активации), которое определил разработчик. Функция «перемалывает» только вектора, а от самих слов уже ничего не осталось, поэтому нельзя сказать, что один нейрон реагирует на букву «с», а другой — на третий символ с конца. Слой нейронов выдает вектор — это код следующего слова (мыла), предсказание нейросети. Поскольку некоторые нейроны слоя активировались, вектор «состояния» поменялся, там появились единицы: 01001110100. В этих единицах зашифрованы данные о том, что первым словом была «мама».
Теперь мы хотим угадать третье слово (мама мыла что? — раму!) с учетом догадки о втором слове и контекста до него. Для этого на следующий слой подаем старый вектор состояния (в нем зашифрована «мама») и вектор слова «мыла».
Так мы объяснили нейросети, что нужно посмотреть, какие слова чаще всего стоят после слова «мыла», если до этого еще была «мама».
Контекст не ограничивается одним словом: окно памяти RNN широко, но старые слова из него все равно «вымываются». Это проблема, и ниже мы рассказываем, как умеют ее решать.
На каждом шаге следующий слой делает то же, что прошлый, но с новыми векторами. Поэтому рекуррентные нейросети схематически рисуют так, как будто они снова и снова загружают разные данные в один и тот же «черный ящик».
Выше — иллюстрация рекуррентности, ее принято обозначать такой «петлей» вокруг слоя.
Первое слово (х0) попадает на первый слой, и он делает предсказание (h0). Первый слой сохраняет вектор активации нейронов и передает его «себе в будущем», то есть второму слою. Первое предсказание h0 становится вторым входным словом x1.
Так обеспечивается память нейросети о контексте. По еще не очень эффективно, но уже гораздо лучше, чем «в лоб» сохранять 10 000 комбинаций из двух слов и запоминать для них следующее вероятное.
Вектор контекста размывается, и это проблема
Переписывать вектор контекста — как делать ксерокопию ксерокопии: в конце-концов станет ничего не понятно. Надо переписывать отдельные части
Почему неэффективно передавать контекст со слоя на слой? Потому что в него все время что-то записывается прямо поверх старой информации. Вектор состояния, двигаясь по тексту, впитывает все больше данных о прежних словах, а его размер при этом не увеличивается — это все то же количество ноликов и единиц. А значит, информация о контексте при постоянной перезаписи вектора состояния спрессовывается все плотнее — и это ведет к потерям.
При этом система не различает важные данные о контексте и неважные — просто запоминает всё подряд. Поэтому важная языковая информация размывается, затирается новой, и, если предложение длинное, высоки шансы записать на месте важного какой-нибудь грамматический мусор. В очень длинном предложении мусор запишется наверняка. Если добавить на каждый слой нейронов и так увеличить длину вектора состояния, это не добавит памяти нейросети: поверх нужных битов все равно могут записаться ненужные, зато это сильно добавит вычислительной сложности. Снова нейросеть во мнемоническом тупике.
Но из этого тупика нашли выход! Даже несколько: первый, о нем ниже в тексте, называется LSTM (Long Short Term Memory Network), или сеть с долгой краткосрочной памятью. Эта архитектура сама решает, что «запомнить», а что «забыть» на каждом временном шаге.
В другой раз мы расскажем о более продвинутом способе улучшить контекстную память — о механизме внимания: самые современные нейросети не запоминают всё, а определяют, какие части предложения важны, и концентрируются только на них.
Что происходит внутри одного слоя нейронов
Умножение на матрицу, сдвиг и функция активации
Мы уже знаем, как читать эту иллюстрацию: вектор X входного слова попадает в «черный ящик» A, там как-то активируются некоторые нейроны, они предсказывают следующее слово h. А потом мы сообщаем на следующий слой о том, какие нейроны активировались.
Что в черном ящике? Как активируются нейроны? Довольно просто:
Слой нейронов (здесь) — это три операции:
- умножение входного вектора X на матрицу весов
- прибавление сдвига
- функция активации нейронов «гиперболический тангенс» tanh (чаще всего она, но там может быть и другая)
График функции tanh выглядит просто (как и графики других возможных функций активации):
Выбор функции активации зависит от целей и идеи разработчика. Кроме того, он может для удобства назвать слоем не умножение-активацию, а, например, умножение, сдвиг, активацию, потом опять умножение и сдвиг. Состав слоя обычно описан в документации к архитектуре.
В нашем примере вектор слова сперва умножается на матрицу весов — при первом запуске нейросети в ней случайные значения, но их нужно скорректировать, «выучить» во время тренировки. Чтобы выучить матрицу весов, нейросеть начинает со случайных предсказаний, сильно ошибается, и постепенно уменьшает меру ошибки на следующих предсказаниях, меняя матрицу так, чтобы предсказывать точнее. За обучение отвечают обратное распространение ошибки и градиентный спуск.
У каждого слоя в RNN своя отдельная матрица, и все надо выучить, поэтому эта архитектура тренируются долго.
Вектор слова умножается на матрицу весов — и получается новый вектор. Он складывается со «сдвигом» («bias»), и вектор-сумму «пропускают» через одну из функций активации (в RNN обычно — через tanh, гиперболический тангенс).
Через функцию активации каждый элемент вектора-суммы проходит отдельно. То, что записано в векторе — и есть состояния нейронов. Пока они могут быть любыми, но после функции tanh становятся в диапазон между −1 и 1. Таким образом нейроны в RNN могут активироваться сильнее или слабее, генерируя сигнал от −1 до 1 (или от 0 до 1, если позволяет функция активации).
Tanh расшифруем так: нейрон немного активируется, если элемент, с которым он работал — немного больше, чем −2. Если сильно больше — выдаст максимальный сигнал. На практике функцию могут сдвигать вдоль осей, прибавляя к ней аргументы, поэтому не обязательно началом активации будет именно эта точка.
LSTM — Long Short Term Memory
Нейросеть, которая сама решает, какой контекст нужно запомнить
LSTM — одна из самых крутых архитектур для обработки естественного языка вплоть до июня 2017. Ниже — наш пересказ фрагментов статьи из блога Кристофера Олаха, сотрудника Google Brain и OpenAI.
До этого мы рассматривали простую рекуррентную нейронную сеть, то есть RNN. Ниже — схема LSTM. Главное — без паники!
Во-первых, по «трубам» этой схемы текут вектора. Входное слово X(t) в синем кружочке — в виде вектора, в стрелочках — вектор, все операции — с векторами.
В желтых кирпичах — слой нейросети. Напомним: это значит, что там спрятаны три операции: сначала входной вектор умножается на матрицу весов слоя (которую нейросеть вырабатывает в ходе тренировки), к произведению прибавляется сдвиг (bias), наконец, вектор-сумма поэлементно проходит через функцию активации нейронов: сигмоиду или гиперболический тангенс. Их графики вы видели выше.
Посимвольная операция означает, что что каждый элемент вектора по отдельности терпит какие-то изменения (как с функцией tanh), а «склеивание» из векторов [1, 2] и [3, 4] дает один вектор [1, 2, 3, 4].
Вектор памяти LSTM
Передается со слоя на слой, из него составляется предсказание
Важнейшая часть LSTM — вот эта труба сверху. Она передает со слоя на слой вектор, кодирующий контекст (будем звать его «вектор памяти»).
К трубе подключены два розовых «вентиля»: слева направо — вентиль «забывания» и вентиль «запоминания». Они контролируют, что нужно забыть, а что — запомнить.
После забывания и запоминания часть вектора памяти становится вектором-предсказанием слоя или его «скрытым состоянием» — hidden state, сокращенно h(t). Как это делается — расскажем чуть позже.
Забывание контекста в LSTM
Первый шаг LSTM — оценить, какой контекст ей больше не нужен
Работа начинается так: с предыдущего слоя (или с начала работы нейросети) приехали два вектора: первый — h(t-1) предсказание прошлого слоя. Второй вектор — X(t), кодирует новое входное слово.
Вектора X(t) и h(t-1) сначала склеиваем, а затем умножаем на матрицу весов, которую выучила нейросеть в процессе тренировки (если вы забыли, как происходит эта тренировка, снова предложим нашу статью про градиентный спуск). На формуле сверху эта матрица обозначена как W(f), что значит forget weights, веса забывания. К произведению добавляем сдвиг b(f).
Получившийся вектор-сумму поэлементно пропускаем через сигмоидную функцию активации (буква в желтом квадрате — «сигма»). Она решает, какие значения старого контекста нужно забыть. Для каждого элемента вектора функция выдает значение от 0 до 1, где 1 значит «оставить элемент целиком», а 0 — «целиком избавиться от элемента». Выходит вектор, который оценивает, насколько сильно нужно забыть ту или иную часть прошлого контекста. Так и назовём — «оценочный» вектор f(t).
Забыть контекст бывает нужно, например, если появилось новое подлежащее и надо запомнить его род и число. Для этого род и число старого подлежащего придется стереть. Разумеется, нейросеть не оперирует категориями подлежащего — но архитектура LSTM скорее всего вычислит, что оно появилось, и сумеет предположить, что старые род и число стали нерелевантны. Читайте дальше, чтобы понять, как.
Итак, мы посмотрели на контекстный вектор, на вектор нового слова, решили, что хотим забыть части старого контекста, и выразили это желание вектором.
Запоминание контекста в LSTM
Второй шаг LSTM — оценить, какой новый контекст надо записать и насколько он важен
Что теперь? Сперва решили, что «забыть», теперь решаем, что «запомнить». На входе, опять старый вектор-предсказание h(t-1) и новое слово X(t). Эти вектора склеиваются и попадают на два независимых слоя.
На слое справа (где написано tanh) вектора X(t) и h(t-1) умножаются на матрицу весов W(c), она получается в процессе тренировки. К произведению прибавляют сдвиг, и вектор-сумма поэлементно проходит через функцию активации «гиперболический тангенс», tanh. Так составляется вектор из новых значений, которые хочется записать в «вектор памяти». Назовем его «вектор новых значений».
Нейронный слой слева умножает вход на выученную матрицу весов W(i) — input weights, веса входа. Добавляется сдвиг b(i), и вектор-сумма поэлементно проходит через сигмоидную функцию. Так формируется «оценочный» вектор i(t): на него умножим «вектор новых значений». Если в оценочном векторе есть нули, какие-то элементы новых значений на них умножатся и не дойдут то контекста.
LSTM выразило, что важного хочется добавить в контекст. Но пока ничего туда не записывается.
Запись новых значений в контекст
Забыть контекст — значит умножить его ненужные части на ноль из «оценочного вектора». Чтобы запомнить новый — прибавляем взвешенный «вектор новых значений»
Запись в вектор памяти происходит на этом шаге: мы умножаем старый вектор памяти на «оценочный» f(t), таким образом забывая (умножая на ноль) то, что решили забыть. А то, что решили не забывать, умножаем примерно на единицу (оставляем как есть).
К «забывшему» вектору памяти прибавляем взвешенный «вектор новых значений». Взвешенный — значит умноженный на «оценочный» i(t), то есть среди «сырых» новых значений некоторые тоже умножились на ноль и так и не попали в вектор контекста.
Если LSTM работает с языковой моделью, то это был как раз тот момент, когда мы стираем информацию о роде старого подлежащего и добавляем род нового.
Предсказание LSTM
Предсказание получается из «фильтрованного» вектора контекста
Наконец решаем, какую часть «вектора памяти» подать дальше как предсказание. Слой с сигмоидной активацией снова определяет, какие части вектора памяти важны, строит свой «оценочный» вектор o(t) — это первая строка формул с картинки.
Допустим, в векторе контекста записано, что где-то в прошлом встретились слова «пушистый» и «деревянный». На вход подается «гладкий», и нужно сгенерировать следующее слово. Мы бы хотели увидеть «стол». Хороший оценочный вектор умножит на ноль контекстные данные о «пушистом» и на единицу — данные о «деревянном». Такой вектор получится умножением входных данных на хорошую матрицу весов: ее элементы правильно подбираются при тренировке. Матрица изменяема, она подстраивается под входные примеры во время обучения. Поэтому в нейросетях так важно найти качественные тренировочные данные.
Вектор памяти C(t) поэлементно «пропускается» через функцию tanh, и все крупные элементы становятся либо 1, либо −1. «Пушистый» и «деревянный» в контексте как бы приводятся к общему знаменателю. Наконец вектор контекста перемножается с «оценочным» — на второй строке формул. Раз теперь на входе «гладкий», прошлую «пушистость» чего-то там удалось забыть и сконцентрироваться на «деревянности»: так больше вероятность предсказать «деревянный гладкий стол ». Получился вектор h(t).
Именно h(t) — предсказание, результат работы слоя LSTM.
Вот способ превратить h(t) в словарный номер следующего слова. h(t) умножим на тренируемую матрицу длиной в словарь (допустим, в словаре 100 тысяч слов), и «подсветим» самые большие значения результата функцией Softmax. Без подробностей — Softmax превращает элементы вектора в нули или в положительные числа, суммарно дающие единицу. Такой набор удобно толковать как вероятность встретить то или иное слово. Если третий элемент вектора — 0.8, значит, 0.8 — вероятность, что следующее слово — абажур (или что у нас третье в словаре по алфавиту).
Заключение
LSTM широко применяется там, где есть неслучайные последовательности. До изобретения трансформеров это вообще был стандартный ответ на вопрос, какую нейросеть брать для обработки языка — бери один из вариантов LSTM (а есть модификации) или GRU, похожую на LSTM архитектуру. Машинный перевод, распознавание речи, рукописного текста, анализ музыки, жестов, семантики, потоков машин в городе, белковых последовательностей — везде LSTM. Это мощная нейросетевая архитектура, и тем не менее, она всего лишь определяет, в каком порядке складываются и перемножаются вектора. Конкретный смысл в свой проект вкладывает сам разработчик.
Может быть, было сложновато, но если вы дочитали сюда, то спасибо за интерес к теме. «Системный Блокъ» постарался сохранить баланс между техническими деталями и упрощением, и надеется, что рекуррентные нейронные сети сегодня стали чуточку менее таинственными! В следующий раз расскажем, как улучшить модель LSTM при помощи «механизма внимания».
Источники
- Understanding LSTM Networks — послужило основой нашего текста, загляните сюда
- Complete Guide of Activation Functions
- AI Language Models & Transformers — Computerphile
- AI YouTube Comments — Computerphile
Долгая краткосрочная память
Долгая краткосрочная память (англ. Long short-term memory, LSTM) — особая разновидность архитектуры рекуррентных нейронных сетей, способная к обучению долговременным зависимостям, предложенная в 1997 году Сеппом Хохрайтером и Юргеном Шмидхубером [1] .
Описание
Рекуррентные нейронные сети добавляют память к искуственным нейронным сетям, но реализуемая память получается короткой — на каждом шаге обучения информация в памяти смешивается с новой и через несколько итераций полностью перезаписывается.
LSTM-модули разработаны специально, чтобы избежать проблемы долговременной зависимости, запоминая значения как на короткие, так и на длинные промежутки времени. Это объясняется тем, что LSTM-модуль не использует функцию активации внутри своих рекуррентных компонентов. Таким образом, хранимое значение не размывается во времени и градиент не исчезает при использовании метода обратного распространения ошибки во времени (англ. Backpropagation Through Time, BPTT) [2] [3] при тренировке сети.
Ключевые компоненты LSTM-модуля: состояние ячейки и различные фильтры. О состоянии ячейки можно говорить, как о памяти сети, которая передает соответствующую информацию по всей цепочке модулей. Таким образом, даже информация из ранних временных шагов может быть получена на более поздних, нивелируя эффект кратковременной памяти.
По мере того, как происходит обучение, состояние ячейки изменяется, информация добавляется или удаляется из состояния ячейки структурами, называемыми фильтрами. Фильтры контролируют поток информации на входах и на выходах модуля на основании некоторых условий. Они состоят из слоя сигмоидальной [4] нейронной сети и операции поточечного умножения.
Сигмоидальный слой возвращает числа в диапазоне [0; 1], которые обозначают, какую долю каждого блока информации следует пропустить дальше по сети. Умножение на это значение используется для пропуска или запрета потока информации внутрь и наружу памяти. Например, входной фильтр контролирует меру вхождения нового значения в память, а фильтр забывания контролирует меру сохранения значения в памяти. Выходной фильтр контролирует меру того, в какой степени значение, находящееся в памяти, используется при расчёте выходной функции активации.
Основные компоненты
- Состояние ячейки
- Фильтры, контролирующие состояние ячейки
- Забывания
- Входной
- Выходной
Принцип работы
Сперва “слой фильтра забывания” (англ. forget gate layer) определяет, какую информацию можно забыть или оставить. Значения предыдущего выхода [math]h_[/math] и текущего входа [math]x_t[/math] пропускаются через сигмоидальный слой. Полученные значения находятся в диапозоне [0; 1]. Значения, которые ближе к 0 будут забыты, а к 1 оставлены.
Далее решается, какая новая информация будет храниться в состоянии ячейки. Этот этап состоит из двух частей. Сначала сигмоидальный слой под названием “слой входного фильтра” (англ. input layer gate) определяет, какие значения следует обновить. Затем tanh-слой [5] строит вектор новых значений-кандидатов [math]\tilde_t[/math] , которые можно добавить в состояние ячейки.
Для замены старого состояния ячейки [math]C_[/math] на новое состояние [math]C_t[/math] . Необходимо умножить старое состояние на [math]f_t[/math] , забывая то, что решили забыть ранее. Затем прибавляем [math]i_t * \tilde_t[/math] . Это новые значения-кандидаты, умноженные на [math]t[/math] – на сколько обновить каждое из значений состояния.
На последнем этапе определяется то, какая информация будет получена на выходе. Выходные данные будут основаны на нашем состоянии ячейки, к ним будут применены некоторые фильтры. Сначала значения предыдущего выхода [math]h_[/math] и текущего входа [math]x_t[/math] пропускаются через сигмоидальный слой, который решает, какая информация из состояния ячейки будет выведена. Затем значения состояния ячейки проходят через tanh-слой, чтобы получить на выходе значения из диапазона от -1 до 1, и перемножаются с выходными значениями сигмоидального слоя, что позволяет выводить только требуемую информацию.
Полученные таким образом [math]h_t[/math] и [math]C_t[/math] передаются далее по цепочке.
Вариации
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=) if step % display_step == 0 or step == 1: loss, acc = sess.run([loss_op, accuracy], feed_dict=) print("Step " + str(step) + ", Minibatch Loss= " + \ "".format(loss) + ", Training Accuracy= " + \ "".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=))
Точность на тренировочных данных: 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
LSTM в машинном обучении
Модель сети LSTM расшифровывается как сети с долговременной краткосрочной памятью (Long Short Term Memory). Это особый вид нейронных сетей, которые обычно способны различать долгосрочные зависимости. Модель LSTM, была разработана для предотвращения проблем с долгосрочными зависимостями, и с этим они обычно справляются очень хорошо. В этой статье я расскажу, как мы можем использовать LSTM в прогнозировании временных рядов.
Модели сети LSTM обычно имеют потенциал для тщательного удаления или добавления данных, что регулируется специальной структурой, известной как ворота. Первый шаг в обработке LSTM – определить, какую информацию нам нужно выбросить из ячейки.
Следующий шаг – решить, какую новую информацию мы должны хранить в ячейке. И наконец мы решаем, что хотим получить на выходе. Вывод обычно основан на состоянии ячеек. Давайте разберемся в процессе LSTM на примере прогнозирования временных рядов для прогнозирования цен на акции.
Прогноз цен на акции с использованием LSTM
Давайте посмотрим, как мы можем использовать модель LSTM для прогнозирования цен на акции с помощью прогнозирования временных рядов. Для этой задачи я очищу данные из Yahoo Finance с помощью библиотеки pandas_datareader. Итак, перед этим давайте начнем с импорта всех пакетов, которые нам нужны для этой задачи:
import math import matplotlib.pyplot as plt import keras import pandas as pd import numpy as np from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.layers import Dropout from keras.layers import * from sklearn.preprocessing import MinMaxScaler from sklearn.metrics import mean_squared_error from sklearn.metrics import mean_absolute_error from sklearn.model_selection import train_test_split from keras.callbacks import EarlyStopping import pandas_datareader as web
Чтобы получить данные:
data = web.DataReader("", data_source="yahoo", start=None, end=None) data.reset_index(inplace=True) data.head()
Следующим шагом является разделение данных на наборы для обучения и тестирования, чтобы избежать переобучения и получить возможность изучить возможность обобщения нашей модели:
training_set = data.iloc[:800, 1:2].values test_set = data.iloc[800:, 1:2].values
Прогнозируемое целевое значение будет значением «закрытой» цены акции. Перед подгонкой модели рекомендуется нормализовать данные. Это повысит общую производительность. Давайте создадим входные объекты с задержкой в 1 день:
# Feature Scaling sc = MinMaxScaler(feature_range = (0, 1)) training_set_scaled = sc.fit_transform(training_set) # Creating a data structure with 60 time-steps and 1 output X_train = [] y_train = [] for i in range(60, 800): X_train.append(training_set_scaled[i-60:i, 0]) y_train.append(training_set_scaled[i, 0]) X_train, y_train = np.array(X_train), np.array(y_train) X_train = np.reshape(X_train, (X_train.shape[0], X_train.shape[1], 1))
Построение модели LSTM
Пришло время построить модель долгосрочной краткосрочной памяти. Итак, я построю нейронную сеть с 50 нейронами и четырьмя скрытыми слоями:
model = Sequential() #Adding the first LSTM layer and some Dropout regularisation model.add(LSTM(units = 50, return_sequences = True, input_shape = (X_train.shape[1], 1))) model.add(Dropout(0.2)) # Adding a second LSTM layer and some Dropout regularisation model.add(LSTM(units = 50, return_sequences = True)) model.add(Dropout(0.2)) # Adding a third LSTM layer and some Dropout regularisation model.add(LSTM(units = 50, return_sequences = True)) model.add(Dropout(0.2)) # Adding a fourth LSTM layer and some Dropout regularisation model.add(LSTM(units = 50)) model.add(Dropout(0.2)) # Adding the output layer model.add(Dense(units = 1)) # Compiling the RNN model.compile(optimizer = 'adam', loss = 'mean_squared_error') # Fitting the RNN to the Training set model.fit(X_train, y_train, epochs = 100, batch_size = 32)
Для запуска модели потребуется некоторое время, после выполнения вы увидите следующий результат:
Epoch 1/100 24/24 [==============================] — 2s 99ms/step — loss: 0.0496 Epoch 2/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0085 Epoch 3/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0057 Epoch 4/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0057 Epoch 5/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0054 Epoch 6/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0062 Epoch 7/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0055 Epoch 8/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0050 Epoch 9/100 24/24 [==============================] — 3s 113ms/step — loss: 0.0044 Epoch 10/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0053 Epoch 11/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0048 Epoch 12/100 24/24 [==============================] — 3s 118ms/step — loss: 0.0051 Epoch 13/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0054 Epoch 14/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0046 Epoch 15/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0046 Epoch 16/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0045 Epoch 17/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0044 Epoch 18/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0042 Epoch 19/100 24/24 [==============================] — 3s 112ms/step — loss: 0.0039 Epoch 20/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0042 Epoch 21/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0043 Epoch 22/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0038 Epoch 23/100 24/24 [==============================] — 3s 113ms/step — loss: 0.0037 Epoch 24/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0039 Epoch 25/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0038 Epoch 26/100 24/24 [==============================] — 3s 114ms/step — loss: 0.0040 Epoch 27/100 24/24 [==============================] — 3s 117ms/step — loss: 0.0034 Epoch 28/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0039 Epoch 29/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0035 Epoch 30/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0055 Epoch 31/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0038 Epoch 32/100 24/24 [==============================] — 3s 113ms/step — loss: 0.0033 Epoch 33/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0035 Epoch 34/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0033 Epoch 35/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0035 Epoch 36/100 24/24 [==============================] — 3s 114ms/step — loss: 0.0036 Epoch 37/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0033 Epoch 38/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0039 Epoch 39/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0033 Epoch 40/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0032 Epoch 41/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0031 Epoch 42/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0028 Epoch 43/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0038 Epoch 44/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0034 Epoch 45/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0034 Epoch 46/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0032 Epoch 47/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0033 Epoch 48/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0046 Epoch 49/100 24/24 [==============================] — 3s 115ms/step — loss: 0.0032 Epoch 50/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0028 Epoch 51/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0028 Epoch 52/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0025 Epoch 53/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0025 Epoch 54/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0027 Epoch 55/100 24/24 [==============================] — 3s 116ms/step — loss: 0.0033 Epoch 56/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0029 Epoch 57/100 24/24 [==============================] — 3s 112ms/step — loss: 0.0026 Epoch 58/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0026 Epoch 59/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0029 Epoch 60/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0028 Epoch 61/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0026 Epoch 62/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0030 Epoch 63/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0027 Epoch 64/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0030 Epoch 65/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0024 Epoch 66/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0028 Epoch 67/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0024 Epoch 68/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0022 Epoch 69/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0023 Epoch 70/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0024 Epoch 71/100 24/24 [==============================] — 3s 115ms/step — loss: 0.0022 Epoch 72/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0022 Epoch 73/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0025 Epoch 74/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0020 Epoch 75/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0021 Epoch 76/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0022 Epoch 77/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0021 Epoch 78/100 24/24 [==============================] — 3s 112ms/step — loss: 0.0022 Epoch 79/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0020 Epoch 80/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0020 Epoch 81/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0021 Epoch 82/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0021 Epoch 83/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0019 Epoch 84/100 24/24 [==============================] — 3s 110ms/step — loss: 0.0020 Epoch 85/100 24/24 [==============================] — 3s 105ms/step — loss: 0.0019 Epoch 86/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0018 Epoch 87/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0018 Epoch 88/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0020 Epoch 89/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0027 Epoch 90/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0024 Epoch 91/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0021 Epoch 92/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0016 Epoch 93/100 24/24 [==============================] — 3s 106ms/step — loss: 0.0017 Epoch 94/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0018 Epoch 95/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0019 Epoch 96/100 24/24 [==============================] — 3s 109ms/step — loss: 0.0018 Epoch 97/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0020 Epoch 98/100 24/24 [==============================] — 3s 107ms/step — loss: 0.0017 Epoch 99/100 24/24 [==============================] — 3s 108ms/step — loss: 0.0019 Epoch 100/100 24/24 [==============================] — 3s 111ms/step — loss: 0.0018
Теперь давайте изменим тестовые данные:
dataset_train = data.iloc[:800, 1:2] dataset_test = data.iloc[800:, 1:2] dataset_total = pd.concat((dataset_train, dataset_test), axis = 0) inputs = dataset_total[len(dataset_total) - len(dataset_test) - 60:].values inputs = inputs.reshape(-1,1) inputs = sc.transform(inputs) X_test = [] for i in range(60, 519): X_test.append(inputs[i-60:i, 0]) X_test = np.array(X_test) X_test = np.reshape(X_test, (X_test.shape[0], X_test.shape[1], 1))
А сейчас давайте сделаем прогнозы, используя модель LSTM на тестовом наборе:
predicted_stock_price = model.predict(X_test) predicted_stock_price = sc.inverse_transform(predicted_stock_price)
Теперь давайте посмотрим на наши прогнозы:
plt.plot(df.loc[800:, 'Date'],dataset_test.values, color = 'red', label = 'Real TESLA Stock Price') plt.plot(df.loc[800:, 'Date'],predicted_stock_price, color = 'blue', label = 'Predicted TESLA Stock Price') plt.xticks(np.arange(0,459,50)) plt.title('TESLA Stock Price Prediction') plt.xlabel('Time') plt.ylabel('TESLA Stock Price') plt.legend() plt.show()
Мы видим, что наша модель работает очень хорошо. Она может точно отслеживать самые неожиданные скачки и падения; однако для самых последних отметок времени мы можем видеть, что модель ожидает более низкие (прогнозируемые) значения по сравнению с фактическими ценовыми значениями действия.
Надеюсь, вам понравилась эта статья о LSTM в машинном обучении.