.. _sec_gru: Блоки с управляемыми переходами (GRU) ===================================== По мере того как рекуррентные нейронные сети и, в частности, архитектура LSTM (:numref:`sec_lstm`) быстро набирали популярность в 2010-х годах, ряд исследователей начал экспериментировать с упрощёнными архитектурами в надежде сохранить ключевую идею использования внутреннего состояния и механизмов мультипликативного управления, но с целью ускорения вычислений. Управляемый рекуррентный блок (GRU) :cite:`Cho.Van-Merrienboer.Bahdanau.ea.2014` предложил упрощённую версию ячейки памяти LSTM, которая часто достигает сопоставимой производительности, но имеет преимущество в виде более быстрой вычислительной скорости :cite:`Chung.Gulcehre.Cho.ea.2014`. .. raw:: latex \diilbookstyleinputcell .. code:: python import torch from torch import nn from d2l import torch as d2l Вентиль сброса и вентиль обновления ----------------------------------- В данном случае три вентиля LSTM заменяются двумя: *вентилем сброса* и *вентилем обновления*. Как и в LSTM, эти вентили используют сигмоидную функцию активации, что приводит к тому, что их значения лежат в интервале :math:`(0, 1)`. Интуитивно вентиль сброса контролирует, какая часть предыдущего состояния всё ещё может быть полезна для запоминания. Аналогично, вентиль обновления позволяет управлять тем, насколько новое состояние является копией старого. :numref:`fig_gru_1` иллюстрирует входы для вентилей сброса и обновления в GRU, учитывая вход текущего временного шага и скрытое состояние предыдущего временного шага. Выходы вентилей формируются двумя полносвязными слоями с сигмоидной функцией активации. .. _fig_gru_1: .. figure:: ../img/gru-1.svg Вычисление вентиля сброса и вентиля обновления в модели GRU. Математически, для заданного шага времени :math:`t`, предположим, что входом является мини-батч :math:`\mathbf{X}_t \in \mathbb{R}^{n \times d}` (количество примеров :math:`=n`; количество входов :math:`=d`), а скрытое состояние предыдущего шага времени равно :math:`\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}` (количество скрытых единиц :math:`=h`). Тогда вентиль сброса :math:`\mathbf{R}_t \in \mathbb{R}^{n \times h}` и вентиль обновления :math:`\mathbf{Z}_t \in \mathbb{R}^{n \times h}` вычисляются следующим образом: .. math:: \begin{aligned} \mathbf{R}_t = \sigma(\mathbf{X}_t \mathbf{W}_{\textrm{xr}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{hr}} + \mathbf{b}_\textrm{r}),\\ \mathbf{Z}_t = \sigma(\mathbf{X}_t \mathbf{W}_{\textrm{xz}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{hz}} + \mathbf{b}_\textrm{z}), \end{aligned} где :math:`\mathbf{W}_{\textrm{xr}}, \mathbf{W}_{\textrm{xz}} \in \mathbb{R}^{d \times h}` и :math:`\mathbf{W}_{\textrm{hr}}, \mathbf{W}_{\textrm{hz}} \in \mathbb{R}^{h \times h}` — параметры весов, а :math:`\mathbf{b}_\textrm{r}, \mathbf{b}_\textrm{z} \in \mathbb{R}^{1 \times h}` — параметры смещения. Кандидатное скрытое состояние ----------------------------- Далее мы объединяем вентиль сброса :math:`\mathbf{R}_t` с обычным механизмом обновления из :eq:`rnn_h_with_state`, что приводит к следующему *кандидату скрытого состояния* :math:`\tilde{\mathbf{H}}_t \in \mathbb{R}^{n \times h}` на шаге времени :math:`t`: .. math:: \tilde{\mathbf{H}}_t = \tanh(\mathbf{X}_t \mathbf{W}_{\textrm{xh}} + \left(\mathbf{R}_t \odot \mathbf{H}_{t-1}\right) \mathbf{W}_{\textrm{hh}} + \mathbf{b}_\textrm{h}), :label: gru_tilde_H где :math:`\mathbf{W}_{\textrm{xh}} \in \mathbb{R}^{d \times h}` и :math:`\mathbf{W}_{\textrm{hh}} \in \mathbb{R}^{h \times h}` — параметры весов, :math:`\mathbf{b}_\textrm{h} \in \mathbb{R}^{1 \times h}` — смещение, а символ :math:`\odot` обозначает поэлементное произведение Адамара. Здесь используется функция активации tanh. Полученное значение является *кандидатом*, поскольку нам всё ещё необходимо учесть действие вентиля обновления. Сравнивая с :eq:`rnn_h_with_state`, можно заметить, что влияние предыдущих состояний теперь может быть уменьшено за счёт поэлементного умножения :math:`\mathbf{R}_t` и :math:`\mathbf{H}_{t-1}` в :eq:`gru_tilde_H`. Если элементы вентиля сброса :math:`\mathbf{R}_t` близки к 1, мы получаем обычную рекуррентную нейронную сеть (vanilla RNN), такую как в :eq:`rnn_h_with_state`. Для всех элементов вентиля сброса :math:`\mathbf{R}_t`, близких к 0, кандидат скрытого состояния представляет собой результат многослойного перцептрона (MLP) с входом :math:`\mathbf{X}_t`. Таким образом, любое ранее существовавшее скрытое состояние *сбрасывается* до значений по умолчанию. :numref:`fig_gru_2` иллюстрирует вычислительный поток после применения вентиля сброса. .. _fig_gru_2: .. figure:: ../img/gru-2.svg Вычисление кандидата скрытого состояния в модели GRU. Скрытое состояние ----------------- Наконец, нам необходимо учесть влияние вентиля обновления :math:`\mathbf{Z}_t`. Он определяет, в какой степени новое скрытое состояние :math:`\mathbf{H}_t \in \mathbb{R}^{n \times h}` совпадает со старым состоянием :math:`\mathbf{H}_{t-1}` по сравнению с тем, насколько оно похоже на новый кандидат состояния :math:`\tilde{\mathbf{H}}_t`. Для этой цели можно использовать вентиль обновления :math:`\mathbf{Z}_t`, просто вычисляя поэлементные выпуклые комбинации :math:`\mathbf{H}_{t-1}` и :math:`\tilde{\mathbf{H}}_t`. Это приводит к окончательному уравнению обновления для GRU: .. math:: \mathbf{H}_t = \mathbf{Z}_t \odot \mathbf{H}_{t-1} + (1 - \mathbf{Z}_t) \odot \tilde{\mathbf{H}}_t. Когда вентиль обновления :math:`\mathbf{Z}_t` близок к 1, мы просто сохраняем предыдущее состояние. В этом случае информация из :math:`\mathbf{X}_t` игнорируется, что фактически пропускает временной шаг :math:`t` в цепи зависимостей. Напротив, когда вентиль обновления :math:`\mathbf{Z}_t` близок к 0, новое латентное состояние :math:`\mathbf{H}_t` приближается к кандидату латентного состояния :math:`\tilde{\mathbf{H}}_t`. :numref:`fig_gru_3` показывает вычислительный поток после того, как начинает действовать вентиль обновления. .. _fig_gru_3: .. figure:: ../img/gru-3.svg Вычисление скрытого состояния в модели GRU. Итак, GRU обладают следующими двумя отличительными особенностями: - Врата сброса помогают улавливать краткосрочные зависимости в последовательностях. - Врата обновления помогают захватывать долгосрочные зависимости в последовательностях. Реализация с нуля ----------------- Чтобы лучше понять модель GRU, давайте реализуем её с нуля. Инициализация параметров модели ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Первый шаг — инициализация параметров модели. Веса выбираются из гауссовского распределения со стандартным отклонением ``sigma``, а смещение устанавливается равным 0. Гиперпараметр ``num_hiddens`` определяет количество скрытых единиц. Инициализируются все веса и смещения, связанные с вентилем обновления, вентилем сброса и кандидатом скрытого состояния. .. raw:: latex \diilbookstyleinputcell .. code:: python class GRUScratch(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_xz, self.W_hz, self.b_z = triple() # Update gate self.W_xr, self.W_hr, self.b_r = triple() # Reset gate self.W_xh, self.W_hh, self.b_h = triple() # Candidate hidden state Определение модели ~~~~~~~~~~~~~~~~~~ Теперь мы готовы определить прямое распространение в GRU. Его структура аналогична структуре базовой ячейки RNN, за исключением того, что уравнения обновления более сложны. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(GRUScratch) def forward(self, inputs, H=None): if H is None: # Initial state with shape: (batch_size, num_hiddens) H = torch.zeros((inputs.shape[1], self.num_hiddens), device=inputs.device) outputs = [] for X in inputs: Z = torch.sigmoid(torch.matmul(X, self.W_xz) + torch.matmul(H, self.W_hz) + self.b_z) R = torch.sigmoid(torch.matmul(X, self.W_xr) + torch.matmul(H, self.W_hr) + self.b_r) H_tilde = torch.tanh(torch.matmul(X, self.W_xh) + torch.matmul(R * H, self.W_hh) + self.b_h) H = Z * H + (1 - Z) * H_tilde outputs.append(H) return outputs, H Обучение ~~~~~~~~ **Обучение** языковой модели на наборе данных *The Time Machine* происходит точно так же, как в :numref:`sec_rnn-scratch`. .. raw:: latex \diilbookstyleinputcell .. code:: python data = d2l.TimeMachine(batch_size=1024, num_steps=32) gru = GRUScratch(num_inputs=len(data.vocab), num_hiddens=32) model = d2l.RNNLMScratch(gru, 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 мы можем напрямую создать модель GRU. Это инкапсулирует все детали конфигурации, которые мы выше указывали явно. .. raw:: latex \diilbookstyleinputcell .. code:: python class GRU(d2l.RNN): def __init__(self, num_inputs, num_hiddens): d2l.Module.__init__(self) self.save_hyperparameters() self.rnn = nn.GRU(num_inputs, num_hiddens) Код значительно быстрее работает при **обучении**, поскольку использует скомпилированные операторы, а не Python. .. raw:: latex \diilbookstyleinputcell .. code:: python gru = GRU(num_inputs=len(data.vocab), num_hiddens=32) model = d2l.RNNLM(gru, 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, GRU обеспечивают сопоставимое качество, но требуют меньших вычислительных затрат. Как правило, по сравнению с простыми RNN, сети с вентилями, такие как LSTM и GRU, лучше улавливают зависимости в последовательностях с большими временными интервалами. GRU содержат простые RNN в качестве предельного случая, когда вентиль сброса включен. Они также могут пропускать подпоследовательности, включая вентиль обновления. Упражнения ---------- 1. Предположим, что мы хотим использовать только входные данные на временном шаге :math:`t'` для предсказания выхода на временном шаге :math:`t > t'`. Каковы наилучшие значения для врат сброса и врат обновления на каждом временном шаге? 2. Настройте гиперпараметры и проанализируйте их влияние на время выполнения, перплексивность и выходную последовательность. 3. Сравните время выполнения, перплексивность и выходные строки для реализаций ``rnn.RNN`` и ``rnn.GRU`` между собой. 4. Что произойдет, если реализовать только часть GRU, например, только вентиль сброса или только вентиль обновления? `Обсуждения `__