.. _sec_lstm: Долгая краткосрочная память (LSTM) ================================== Вскоре после того, как рекуррентные нейронные сети (RNN) стиля Элмана стали обучаться с помощью обратного распространения ошибки :cite:`elman1990finding`, стали очевидны проблемы обучения долгосрочным зависимостям (из-за затухания и взрыва градиентов), о которых писали Бенджио и Хохрайтер :cite:`bengio1994learning,Hochreiter.Bengio.Frasconi.ea.2001`. Хохрайтер сформулировал эту проблему ещё в 1991 году в своей магистерской диссертации, хотя результаты не получили широкой известности, поскольку диссертация была написана на немецком языке. Хотя обрезка градиентов помогает справиться с взрывом градиентов, обработка затухания градиентов, по-видимому, требует более сложного решения. Одним из первых и наиболее успешных методов решения проблемы затухания градиентов стала модель долгой краткосрочной памяти (LSTM) :cite:t:`Hochreiter.Schmidhuber.1997`. LSTMs напоминают стандартные рекуррентные нейронные сети, но в них каждый обычный рекуррентный узел заменен на *ячейку памяти*. Каждая ячейка памяти содержит *внутреннее состояние*, то есть узел с самопетлевой рекуррентной связью фиксированного веса 1, что обеспечивает прохождение градиента через множество временных шагов без его затухания или взрыва. Термин «долгая краткосрочная память» основан на следующем соображении. Простые рекуррентные нейронные сети обладают *долгосрочной памятью* в виде весов. Во время обучения веса изменяются медленно, кодируя общие знания о данных. Они также обладают *краткосрочной памятью* в виде временных активаций, которые передаются от каждого узла к последующим. Модель долгой краткосрочной памяти (LSTM) вводит промежуточный тип хранения информации с помощью ячейки памяти. Ячейка памяти — это составной элемент, построенный из более простых узлов по определённой схеме связей, с новым включением мультипликативных узлов. .. raw:: latex \diilbookstyleinputcell .. code:: python import torch from torch import nn from d2l import torch as d2l Ячейка памяти с вентилем ------------------------ Каждая ячейка памяти имеет *внутреннее состояние* и набор мультипликативных вентилей, которые определяют, (i) должно ли заданное входное воздействие влиять на внутреннее состояние (*вентиль входа*), (ii) следует ли обнулять внутреннее состояние до :math:`0` (*вентиль забывания*), и (iii) должно ли внутреннее состояние данного нейрона влиять на выход ячейки (*вентиль выхода*). Скрытое состояние с вентилем ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ключевое отличие обычных рекуррентных нейронных сетей (RNN) от LSTM заключается в том, что последние поддерживают вентилирование скрытого состояния. Это означает, что существуют выделенные механизмы, определяющие, когда скрытое состояние должно *обновляться*, а когда — *сбрасываться*. Эти механизмы обучаются и решают проблемы, перечисленные выше. Например, если первый токен имеет большое значение, мы научимся не обновлять скрытое состояние после первого наблюдения. Аналогично мы научимся игнорировать нерелевантные временные наблюдения. Наконец, мы научимся сбрасывать скрытое состояние всякий раз, когда это необходимо. Мы обсудим это подробно ниже. Вентиль входа, вентиль забывания и вентиль выхода ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Данные, поступающие на вентили LSTM, — это вход текущего шага времени и скрытое состояние предыдущего шага времени, как показано на рисунке :numref:`fig_lstm_0`. Три полносвязных слоя с функциями активации сигмоиды вычисляют значения входного, вентиля забывания и выходного вентилей. В результате применения сигмоидальной функции активации все значения этих трёх вентилей находятся в диапазоне :math:`(0, 1)`. Кроме того, нам требуется *входной узел*, который обычно вычисляется с использованием функции активации *tanh*. Интуитивно понятно, что *входной вентиль* определяет, какая часть значения входного узла должна быть добавлена к внутреннему состоянию текущей ячейки памяти. *Вентиль забывания* определяет, следует ли сохранить текущее значение памяти или сбросить его. А *выходной вентиль* определяет, должна ли ячейка памяти влиять на выходной сигнал на текущем шаге времени. .. _fig_lstm_0: .. figure:: ../img/lstm-0.svg Вычисление входного вентиля, вентиля забывания и выходного вентиля в модели LSTM. Математически предположим, что имеется :math:`h` скрытых единиц, размер батча равен :math:`n`, а количество входов — :math:`d`. Таким образом, вход — это :math:`\mathbf{X}_t \in \mathbb{R}^{n \times d}`, а скрытое состояние предыдущего временного шага — :math:`\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}`. Соответственно, вентили на временном шаге :math:`t` определяются следующим образом: вентиль ввода — :math:`\mathbf{I}_t \in \mathbb{R}^{n \times h}`, вентиль забывания — :math:`\mathbf{F}_t \in \mathbb{R}^{n \times h}`, а вентиль вывода — :math:`\mathbf{O}_t \in \mathbb{R}^{n \times h}`. Они вычисляются следующим образом: .. math:: \begin{aligned} \mathbf{I}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{\textrm{xi}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{hi}} + \mathbf{b}_\textrm{i}),\\ \mathbf{F}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{\textrm{xf}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{hf}} + \mathbf{b}_\textrm{f}),\\ \mathbf{O}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{\textrm{xo}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{ho}} + \mathbf{b}_\textrm{o}), \end{aligned} где :math:`\mathbf{W}_{\textrm{xi}}, \mathbf{W}_{\textrm{xf}}, \mathbf{W}_{\textrm{xo}} \in \mathbb{R}^{d \times h}` и :math:`\mathbf{W}_{\textrm{hi}}, \mathbf{W}_{\textrm{hf}}, \mathbf{W}_{\textrm{ho}} \in \mathbb{R}^{h \times h}` — параметры весов, а :math:`\mathbf{b}_\textrm{i}, \mathbf{b}_\textrm{f}, \mathbf{b}_\textrm{o} \in \mathbb{R}^{1 \times h}` — параметры смещения. Обратите внимание, что при суммировании происходит транслирование (см. :numref:`subsec_broadcasting`). Мы используем сигмоидные функции (как описано в :numref:`sec_mlp`), чтобы отобразить входные значения в интервал :math:`(0, 1)`. Входной узел ~~~~~~~~~~~~ Далее мы проектируем ячейку памяти. Поскольку действие различных вентилей ещё не определено, сначала введём *входной узел* :math:`\tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h}`. Его вычисления аналогичны вычислениям трёх описанных выше вентилей, но в качестве функции активации используется функция :math:`\tanh` со значениями в диапазоне :math:`(-1, 1)`. Это приводит к следующему уравнению на шаге времени :math:`t`: .. math:: \tilde{\mathbf{C}}_t = \textrm{tanh}(\mathbf{X}_t \mathbf{W}_{\textrm{xc}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{hc}} + \mathbf{b}_\textrm{c}), где :math:`\mathbf{W}_{\textrm{xc}} \in \mathbb{R}^{d \times h}` и :math:`\mathbf{W}_{\textrm{hc}} \in \mathbb{R}^{h \times h}` — параметры весов, а :math:`\mathbf{b}_\textrm{c} \in \mathbb{R}^{1 \times h}` — параметр смещения. На рисунке :numref:`fig_lstm_1` приведена краткая иллюстрация входного узла. .. _fig_lstm_1: .. figure:: ../img/lstm-1.svg Вычисление входного узла в модели LSTM. Внутреннее состояние ячейки памяти ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ В LSTM входной вентиль :math:`\mathbf{I}_t` управляет тем, насколько сильно мы учитываем новые данные через :math:`\tilde{\mathbf{C}}_t`, а вентиль забывания :math:`\mathbf{F}_t` определяет, какую часть старого внутреннего состояния ячейки :math:`\mathbf{C}_{t-1} \in \mathbb{R}^{n \times h}` мы сохраняем. Используя оператор поэлементного произведения (Хаддамара) :math:`\odot`, получаем следующее уравнение обновления: .. math:: \mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t. Если вентиль забывания всегда равен 1, а вентиль входа всегда равен 0, внутреннее состояние ячейки памяти :math:`\mathbf{C}_{t-1}` будет оставаться неизменным, передаваясь без изменений на каждый последующий временной шаг. Однако вентили входа и забывания обеспечивают модели гибкость, позволяя ей обучаться тому, когда следует сохранять это значение неизменным, а когда его следует изменять в ответ на последующие входные данные. На практике такая конструкция смягчает проблему затухающего градиента, что приводит к созданию моделей, которые значительно легче обучать, особенно при работе с наборами данных, содержащими длинные последовательности. Таким образом, мы приходим к диаграмме потока в :numref:`fig_lstm_2`. .. _fig_lstm_2: .. figure:: ../img/lstm-2.svg Вычисление внутреннего состояния ячейки памяти в модели LSTM. Скрытое состояние ~~~~~~~~~~~~~~~~~ Наконец, нам нужно определить, как вычислять выход ячейки памяти, то есть скрытое состояние :math:`\mathbf{H}_t \in \mathbb{R}^{n \times h}`, видимое другими слоями. Здесь вступает в игру вентиль выхода. В LSTMs мы сначала применяем :math:`\tanh` к внутреннему состоянию ячейки памяти, а затем выполняем ещё одну поэлементную операцию умножения, на этот раз с вентилем выхода. Это гарантирует, что значения :math:`\mathbf{H}_t` всегда находятся в интервале :math:`(-1, 1)`: .. math:: \mathbf{H}_t = \mathbf{O}_t \odot \tanh(\mathbf{C}_t). Когда значение вентиля выхода близко к 1, мы позволяем внутреннему состоянию ячейки памяти беспрепятственно влиять на последующие слои, тогда как при значениях вентиля выхода, близких к 0, мы предотвращаем влияние текущей ячейки памяти на другие слои сети на данном временном шаге. Обратите внимание, что ячейка памяти может накапливать информацию на протяжении многих временных шагов, не влияя на остальную часть сети (до тех пор, пока значения вентиля выхода близки к 0), и затем внезапно начать влиять на сеть на последующем временном шаге, как только значения вентиля выхода изменятся с близких к 0 на близкие к 1. :numref:`fig_lstm_3` содержит графическую иллюстрацию потока данных. .. _fig_lstm_3: .. figure:: ../img/lstm-3.svg Вычисление скрытого состояния в модели LSTM. Реализация с нуля ----------------- Теперь реализуем LSTM с нуля. Как и в экспериментах из :numref:`sec_rnn-scratch`, сначала загрузим набор данных *The Time Machine*. Инициализация параметров модели ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Далее необходимо определить и инициализировать параметры модели. Как и ранее, гиперпараметр ``num_hiddens`` определяет количество скрытых единиц. Веса инициализируются в соответствии с нормальным распределением со стандартным отклонением 0,01, а смещения принимаются равными нулю. .. raw:: latex \diilbookstyleinputcell .. code:: python class LSTMScratch(d2l.Module): def __init__(self, num_inputs, num_hiddens, sigma=0.01): super().__init__() self.save_hyperparameters() init_weight = lambda *shape: nn.Parameter(torch.randn(*shape) * sigma) triple = lambda: (init_weight(num_inputs, num_hiddens), init_weight(num_hiddens, num_hiddens), nn.Parameter(torch.zeros(num_hiddens))) self.W_xi, self.W_hi, self.b_i = triple() # Input gate self.W_xf, self.W_hf, self.b_f = triple() # Forget gate self.W_xo, self.W_ho, self.b_o = triple() # Output gate self.W_xc, self.W_hc, self.b_c = triple() # Input node Фактическая модель определяется описанным выше образом и состоит из трёх вентилей и входного узла. Обратите внимание, что в выходной слой передаётся только скрытое состояние. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(LSTMScratch) def forward(self, inputs, H_C=None): if H_C is None: # Initial state with shape: (batch_size, num_hiddens) H = torch.zeros((inputs.shape[1], self.num_hiddens), device=inputs.device) C = torch.zeros((inputs.shape[1], self.num_hiddens), device=inputs.device) else: H, C = H_C outputs = [] for X in inputs: I = torch.sigmoid(torch.matmul(X, self.W_xi) + torch.matmul(H, self.W_hi) + self.b_i) F = torch.sigmoid(torch.matmul(X, self.W_xf) + torch.matmul(H, self.W_hf) + self.b_f) O = torch.sigmoid(torch.matmul(X, self.W_xo) + torch.matmul(H, self.W_ho) + self.b_o) C_tilde = torch.tanh(torch.matmul(X, self.W_xc) + torch.matmul(H, self.W_hc) + self.b_c) C = F * C + I * C_tilde H = O * torch.tanh(C) outputs.append(H) return outputs, (H, C) Обучение и предсказание ~~~~~~~~~~~~~~~~~~~~~~~ Давайте обучим модель LSTM, создав экземпляр класса ``RNNLMScratch`` из :numref:`sec_rnn-scratch`. .. raw:: latex \diilbookstyleinputcell .. code:: python data = d2l.TimeMachine(batch_size=1024, num_steps=32) lstm = LSTMScratch(num_inputs=len(data.vocab), num_hiddens=32) model = d2l.RNNLMScratch(lstm, vocab_size=len(data.vocab), lr=4) trainer = d2l.Trainer(max_epochs=50, gradient_clip_val=1, num_gpus=1) trainer.fit(model, data) Краткая реализация ------------------ Используя высокоуровневые API, мы можем напрямую создать модель LSTM. Это инкапсулирует все параметры конфигурации, которые мы ранее задавали явно. Код выполняется значительно быстрее, так как использует компилируемые операторы вместо интерпретируемого Python для многих операций, которые мы ранее реализовывали вручную. .. raw:: latex \diilbookstyleinputcell .. code:: python class LSTM(d2l.RNN): def __init__(self, num_inputs, num_hiddens): d2l.Module.__init__(self) self.save_hyperparameters() self.rnn = nn.LSTM(num_inputs, num_hiddens) def forward(self, inputs, H_C=None): return self.rnn(inputs, H_C) .. raw:: latex \diilbookstyleinputcell .. code:: python lstm = LSTM(num_inputs=len(data.vocab), num_hiddens=32) model = d2l.RNNLM(lstm, vocab_size=len(data.vocab), lr=4) trainer.fit(model, data) .. raw:: latex \diilbookstyleinputcell .. code:: python model.predict('it has', 20, data.vocab, d2l.try_gpu()) LSTM-сети являются типичным примером авторегрессионной модели со скрытыми переменными, обладающей нетривиальным контролем над состоянием. За прошедшие годы было предложено множество её вариантов, например, использование нескольких слоёв, остаточных связей и различных видов регуляризации. Однако обучение LSTM и других моделей последовательностей (таких как GRU) обходится довольно дорого из-за зависимости последовательности на больших расстояниях. Позже мы встретим альтернативные модели, такие как трансформеры, которые можно использовать в некоторых случаях. Итоги ----- Хотя LSTM были опубликованы в 1997 году, они получили широкое распространение благодаря победам в соревнованиях по прогнозированию в середине 2000-х годов и стали доминирующими моделями для работы с последовательностями с 2011 года до появления трансформеров, начавшегося в 2017 году. Даже трансформеры обязаны некоторыми из своих ключевых идей архитектурным инновациям, предложенным LSTM. У LSTM есть три типа врат: врата входа, врата забывания и врата выхода, которые управляют потоком информации. Выход скрытого слоя LSTM включает скрытое состояние и внутреннее состояние ячейки памяти. Только скрытое состояние передается во выходной слой, тогда как внутреннее состояние ячейки памяти остается полностью внутренним. LSTM могут смягчать проблемы затухания и взрыва градиентов. Упражнения ---------- 1. Настройте гиперпараметры и проанализируйте их влияние на время выполнения, перплексивность и выходную последовательность. 2. Как нужно изменить модель, чтобы она генерировала корректные слова, а не просто последовательности символов? 3. Сравните вычислительную сложность GRU, LSTM и обычных RNN при заданной размерности скрытого состояния. Особое внимание уделите затратам на обучение и вывод. 4. Поскольку кандидат в ячейку памяти гарантирует, что значения находятся в диапазоне от :math:`-1` до :math:`1` благодаря использованию функции :math:`\tanh`, зачем скрытому состоянию снова применять функцию :math:`\tanh`, чтобы обеспечить, что выходные значения находятся в диапазоне от :math:`-1` до :math:`1`? 5. Реализуйте модель LSTM для прогнозирования временных рядов, а не для прогнозирования последовательностей символов. `Обсуждения `__