Рекуррентный блок GRU

Смотреть материал на YouTube | RuTube

На предыдущих занятиях мы с вами познакомились с рекуррентной архитектурой LSTM-блока. Она эффективна при анализе долгосрочного контента, но имеет и существенный недостаток – большое число настраиваемых параметров (весовых коэффициентов). Это приводит к большим затратам памяти и длительному процессу обучения таких сетей. Поэтому в 2014 году было предложено упрощение LSTM, которое стало известно под названием управляемые рекуррентные блоки:

Gated Recurrent Units (GRU)

По эффективности блоки GRU сравнимы с LSTM во многих практических задачах: моделирования музыкальных и речевых сигналов, обработка текста и так далее. Существует несколько вариантов этого блока и, как всегда, мы рассмотрим их классическую, базовую архитектуру:

Работа этого блока (ячейки) похожа на блок LSTM, только здесь долгосрочный элемент памяти объединен с вектором скрытого состояния . Наверху мы также видим две поэлементные операции умножения и сложения для забывания ненужной информации и запоминания новой:

Для определения что забыть, а что оставить, используется вектор

который, затем, поэлементно вычитается из 1 и умножается на вектор предыдущего состояния :

Почему мы здесь делаем это вычитание? Чтобы противоположную информацию использовать как маркер для добавления нового в соседней ветке. Если ее полностью расписать, то получится следующая операция:

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

Эта формула и определяет принцип работы ячейки GRU. Если кто из вас знаком с калмановской фильтрацией случайных сигналов, то полученная формула очень похожа на реализацию фильтра Калмана, только в нелинейном исполнении. В некотором смысле она работает по этому же принципу: отбрасывает случайные (незначительные) детали и сохраняет главное (важное).

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

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

Одним из общих недостатков блоков LSTM и GRU является их большая склонность к переобучению. Так как каждая ячейка содержит большое число нейронов, то сеть, на их основе также получается большой. А это, как мы знаем, прямой путь к переобучению. Чтобы этого избежать, во-первых, нужно контролировать эффект переобучения по выборке валидации. И, во-вторых, применять инструменты:

Dropout и Batch Normalization

Причем, в PyTorch Dropout можно использовать и для внутренних слоев в блоке, используя параметр:

  • dropout – для всех внутренних слоев, кроме выходного.

По умолчанию этот параметр равен нулю, то есть, dropout отключен. Инструмент Batch Normalization используется только между слоями (внутри ячеек он не применяется).

Использование блока GRU в PyTorch

Давайте посмотрим, как можно создавать и использовать такие блок GRU в PyTorch. Чтобы создать GRU-слой следует воспользоваться классом:

torch.nn.GRU

который принимает те же параметры, что и ранее рассмотренный класс LSTM:

  • input_size – размер входного тензора;
  • hidden_size – размер вектора скрытого состояния;
  • num_layers=1 – число рекуррентных слоев;
  • bias=True – использование bias;
  • batch_first=False – формат входного тензора с первой осью batch_size;
  • dropout=0.0 – Dropout для всех внутренних слоев, кроме выходного;
  • bidirectional=False – однонаправленный или двунаправленный слой.

Для примера давайте создадим такой слой и пропустим через него тензор x размерностью:

(batch_size, sq_length, d_size)

import torch
import torch.nn as nn
 
rnn = nn.GRU(10, 20, batch_first=True)
 
x = torch.randn(7, 3, 10)    # (batch_size, sq_length, d_size)
y, h = rnn(x)
 
print('y:', y.size())
print('h:', h.size())

В консоли увидим строчки:

y: torch.Size([7, 3, 20])
h: torch.Size([1, 7, 20])

Если же слой будет двунаправленным:

rnn = nn.GRU(10, 20, batch_first=True, bidirectional=True)

то тензоры y, h будут иметь размеры:

y: torch.Size([7, 3, 40])
h: torch.Size([2, 7, 20])

Как видите, здесь все работает по аналогии с RNN-слоем.

В качестве рабочего примера давайте в программе прогноза слов заменим RNN-слой на GRU и посмотрим, что получится. Класс модели в этом случае будет иметь вид:

class WordsRNN(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.hidden_size = 64
        self.in_features = in_features
        self.out_features = out_features
 
        self.rnn = nn.GRU(in_features, self.hidden_size, batch_first=True)
        self.out = nn.Linear(self.hidden_size, out_features)
 
    def forward(self, x):
        x, h = self.rnn(x)
        y = self.out(h)
        return y

После 20 эпох обучения, получим:

подумал встал и снова лег тем другим шутя и думать о форме плана и как

Вот принцип работы GRU-блока и пример его использования во фреймворке PyTorch.

Видео по теме