Главная страница
Навигация по странице:

  • Метод backward

  • Основные элементы узлов RNNNode

  • «Классические» узлы RNN

  • Пишем код для RNNNode

  • RNNNode: обратный проход

  • Ограничения классических узлов RNN

  • Решение: узлы GRUNode

  • GRUNode: схема

  • Курсовая работа. Глубокое обучение


    Скачать 4.97 Mb.
    НазваниеГлубокое обучение
    АнкорКурсовая работа
    Дата26.06.2022
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаVeydman_S_Glubokoe_obuchenie_Legkaya_razrabotka_proektov_na_Pyth.pdf
    ТипДокументы
    #615357
    страница18 из 22
    1   ...   14   15   16   17   18   19   20   21   22
    211
    После инициализации нашего слоя нужно описать передачу данных вперед.
    Метод forward
    В целом, метод forward состоит из приема на вход ndarray x_seq_in фор- мы
    (batch_size,
    sequence_length,
    feature_size)
    и ее последовательной передачи через все
    RNNNode данного слоя. В приведенном ниже коде self.
    nodes
    — это
    RNNNode для слоя, а
    H_in
    — скрытое состояние слоя:
    sequence_length = x_seq_in.shape[1]
    x_seq_out = np.zeros((batch_size, sequence_length, self.output_size))
    for t in range(sequence_length):
    x_in = x_seq_in[:, t, :]
    y_out, H_in = self.nodes[t].forward(x_in, H_in, self.params)
    x_seq_out[:, t, :] = y_out
    Небольшое замечание о скрытом состоянии
    H_in
    : скрытое состояние
    RNNLayer обычно представлено в виде вектора, но операции в каждом
    RNNNode требуют, чтобы скрытое состояние имело размерность ndarray
    (batch_size,
    hidden_size)
    . Поэтому в начале каждого прямого прохода мы просто «повторяем» скрытое состояние:
    batch_size = x_seq_in.shape[0]
    H_in = np.copy(self.start_H)
    H_in = np.repeat(H_in, batch_size, axis=0)
    После прямого прохода мы берем среднее значение по наблюдениям, составляющим пакет, чтобы получить обновленное скрытое состояние для этого слоя:
    self.start_H = H_in.mean(axis=0, keepdims=True)
    Кроме того, из этого кода видно, что у
    RNNNode должен быть метод forward
    , который принимает два массива со следующими размерностями:

    212
    Глава 6. Рекуррентные нейронные сети y
    (batch_size,
    feature_size)
    ;
    y
    (batch_size,
    hidden_size)
    и возвращает два массива размерностей:
    y
    (batch_size,
    output_size)
    ;
    y
    (batch_size,
    hidden_size)
    Мы рассмотрим реализацию класса
    RNNNode
    (и ее варианты) в следую- щем разделе. Но сначала давайте рассмотрим метод backward для класса
    RNNLayer
    Метод backward
    Так как выходом метода forward является x_seq_out
    , у метода backward на входе должен быть градиент той же формы, что и x_seq_out
    , под названи- ем x_seq_out_grad
    . Двигаясь в направлении, противоположном прямому методу, мы передаем этот градиент в обратном направлении через узлы
    RNN, в конечном итоге возвращая x_seq_in_grad формы
    (batch_size
    , sequence_length
    , self feature_size)
    в качестве градиента для всего слоя:
    h_in_grad = np.zeros((batch_size, self.hidden_size))
    sequence_length = x_seq_out_grad.shape[1]
    x_seq_in_grad = np.zeros((batch_size, sequence_length, self.feature_size))
    for t in reversed(range(sequence_length)):
    x_out_grad = x_seq_out_grad[:, t, :]
    grad_out, h_in_grad = \
    self.nodes[t].backward(x_out_grad, h_in_grad, self.params)
    x_seq_in_grad[:, t, :] = grad_out
    То есть у
    RNNNode должен быть метод backward
    , который, следуя шаблону, является противоположностью метода forward
    , принимая два массива фигур:

    RNN: код
    213
    y
    (batch_size,
    output_size)
    ;
    y
    (batch_size,
    hidden_size)
    и возвращая два массива фигур:
    y
    (batch_size,
    feature_size)
    ;
    y
    (batch_size,
    hidden_size)
    И это работа
    RNNLayer
    . Теперь кажется, что осталось только описать ядро рекуррентной нейронной сети: узлы
    RNNNode
    , где происходят реальные вычисления. Прежде чем мы это сделаем, давайте проясним роль
    RNNNode и их вариантов в общей работе
    RNN
    Основные элементы узлов RNNNode
    При работе с RNN разговор обычно начинается именно с узлов
    RNNNode
    Но мы рассмотрим узлы последними, так как до этого момента посредством схем и описаний пытались понять самую суть RNN: как структурируются данные и как данные и скрытые состояния передаются между слоями.
    Оказывается, существует несколько способов реализации самих
    RNNNode
    , фактической обработки данных с заданным временным шагом и обнов- ления скрытого состояния слоя. Первый способ — это так называемые
    «обычные» рекуррентные нейронные сети, которые мы будем также назы- вать «классическими RNN». Однако существуют и другие, более сложные способы реализации RNN. Пример такого способа — это вариант с узлами
    RNN под названием GRU, что означает «Gated Recurrent Units» (управ- ляемые рекуррентные блоки). Часто говорят, что GRU и другие варианты
    RNN значительно отличаются от классических RNN, но важно помнить, что в них все равно используется одна и та же структура слоев, которую мы уже рассмотрели. Например, во всех этих сетях одинаково передаются данные во времени и обновляются скрытые состояния на каждом шаге.
    Единственное отличие между ними — это внутренняя работа этих «узлов».
    Еще раз подчеркнем: если бы мы реализовали
    GRULayer вместо
    RNNLayer
    , код был бы точно таким же! Ядро прямого прохода выглядит следующим образом:
    sequence_length = x_seq_in.shape[1]
    x_seq_out = np.zeros((batch_size, sequence_length, self.output_size))

    214
    Глава 6. Рекуррентные нейронные сети for t in range(sequence_length):
    x_in = x_seq_in[:, t, :]
    y_out, H_in = self.nodes[t].forward(x_in, H_in, self.params)
    x_seq_out[:, t, :] = y_out
    Единственное отличие состоит в том, что каждый «узел» в self nodes будет реализован как
    GRUNode вместо
    RNNNode
    . Метод backward тоже не изменится.
    Это также почти справедливо для самого известного варианта классиче- ских RNN: LSTM, или ячеек «долгая краткосрочная память» (англ. Long
    Short Term Memory). Единственная разница состоит в том, что в слоях
    LSTMLayer нужно, чтобы слой запоминал две величины и обновлял их, когда элементы последовательности передаются во времени: в дополне- ние к «скрытому состоянию» в слое хранится «состояние ячейки», что позволяет лучше моделировать долгосрочные зависимости. В результате возникают небольшие различия между реализациями
    LSTMLayer и
    RNNLayer
    Так, у слоев
    LSTMLayer будет два массива ndarray для хранения состояния слоя с течением времени:
    y
    Ndarray start_H
    формы
    (1,
    hidden_size)
    , где хранится скрытое состо- яние слоя;
    y
    Ndarray start_C
    формы
    (1,
    cell_size)
    , где хранится состояние ячейки слоя.
    Каждый узел
    LSTMNode должен принимать входные данные, а также скрытое состояние и состояние ячейки. На прямом проходе это будет выглядеть так:
    y_out, H_in, C_in = self.nodes[t].forward(x_in, H_in, C_in self.params)
    а в методе backward
    :
    grad_out, h_in_grad, c_in_grad = \
    self.nodes[t].backward(x_out_grad, h_in_grad, c_in_grad, self.params)
    Мы упомянули всего три варианта, но их существует намного больше, например LSTM со «смотровыми глазкˆами» хранят состояние ячейки в дополнение к скрытому состоянию, а некоторые из них поддержива-

    RNN: код
    215
    ют только скрытое состояние
    1
    . Но в целом, слой, состоящий из узлов
    LSTMPeepholeConnectionNode
    , будет вписываться в
    RNNLayer так же, как и все другие реализации, и методы forward и backward будут такими же.
    Рассмотренная базовая структура RNN — способ, которым данные на- правляются вперед через слои, а также вперед с течением времени и затем назад во время обратного прохода, — уникальная черта рекуррентных сетей. Реальные структурные различия между классическими RNN и RNN на основе LSTM относительно невелики, а вот их характеристики могут существенно отличаться.
    Теперь давайте посмотрим на реализацию
    RNNNode
    «Классические» узлы RNN
    RNN принимают данные по одному элементу последовательности за раз; например, если мы хотим спрогнозировать цену на нефть, на каждом временном шаге RNN будет получать информацию о функциях, которые мы используем для прогнозирования цены на данном временном шаге.
    Кроме того, RNN хранит в своем «скрытом состоянии» кодировку, пред- ставляющую совокупную информацию о том, что произошло на преды- дущих временных шагах. Нам нужно превратить эти данные (а именно признаки текущей точки данных и накопленную информацию от всех предыдущих шагов) в прогноз для этого шага, а также в обновленное скрытое состояние.
    Чтобы понять, как RNN делает это, вспомним, что происходит в обычной нейронной сети. В прямой нейронной сети каждый слой получает набор
    «изученных признаков» от предыдущего слоя. Каждый такой набор пред- ставляет собой комбинацию исходных признаков, которую сеть «счита- ет» полезными. Затем слой умножает эти признаки на матрицу весов, что позволяет слою изучать объекты, которые являются комбинациями признаков, полученными слоем в качестве входных данных. Чтобы нор- мализовать выход, мы добавляем к этим новым признакам «смещение» и пропускаем их через функцию активации.
    В рекуррентных нейронных сетях нужно сделать так, чтобы наше обнов- ленное скрытое состояние включало в себя и входные данные, и старое
    1
    Загляните в «Википедию», чтобы почитать о разных вариантах LSTM, по адресу oreil.ly/2TysrXj

    216
    Глава 6. Рекуррентные нейронные сети скрытое состояние. Это похоже на то, что происходит в обычных ней- ронных сетях:
    1. Сначала мы объединяем ввод и скрытое состояние. Затем мы умно- жаем это значение на матрицу весов, добавляем смещение и передаем результат через тангенциальную функцию активации. Это и будет наше обновленное скрытое состояние.
    2. Затем мы умножаем это новое скрытое состояние на весовую ма- трицу, которая преобразует скрытое состояние в выход с требуемой размерностью. Например, если мы используем этот RNN для про- гнозирования одного непрерывного значения на каждом временном шаге, нам нужно умножить скрытое состояние на весовую матрицу размера
    (hidden_size, 1)
    Таким образом, наше обновленное скрытое состояние будет зависеть и от входных данных, полученных на текущем временном шаге, и от предыдущего скрытого состояния, а выходные данные будут являться результатом передачи этого обновленного скрытого состояния через операции полносвязанного слоя.
    Пора написать код.
    Пишем код для RNNNode
    В приведенном ниже коде мы реализуем шаги, описанные абзацем выше.
    Позже мы сделаем то же самое с GRU и LSTM (как уже поступали с про- стыми математическими функциями, которые обсуждали в главе 1), и идея будет общая: мы сохраняем все значения, вычисленные на прямом проходе, как атрибуты, хранящиеся в классе
    Node
    , чтобы затем использо- вать их для вычисления обратного прохода:
    def forward(self, x_in: ndarray,
    H_in: ndarray, params_dict: Dict[str, Dict[str, ndarray]]
    ) -> Tuple[ndarray]:
    '''
    param x: массив numpy формы (batch_size, vocab_size)
    param H_prev: массив numpy формы (batch_size, hidden_size)
    return self.x_out: массив numpy формы (batch_size, vocab_size)
    return self.H: массив numpy формы (batch_size, hidden_size)
    '''

    RNN: код
    217
    self.X_in = x_in self.H_in = H_in self.Z = np.column_stack((x_in, H_in))
    self.H_int = np.dot(self.Z, params_dict['W_f']['value']) \
    + params_dict['B_f']['value']
    self.H_out = tanh(self.H_int)
    self.X_out = np.dot(self.H_out, params_dict['W_v']['value']) \
    + params_dict['B_v']['value']
    return self.X_out, self.H_out
    Еще одно замечание: поскольку здесь мы не используем класс
    Param-
    Operations
    , хранить параметры придется по-другому. Мы будем хранить их в словаре params_dict
    , который ссылается на параметры по имени.
    Кроме того, у каждого параметра будет два ключа: значение и производная
    (то есть градиент). В прямом проходе нам потребуется только значение.
    RNNNode: обратный проход
    На обратном проходе
    RNNNode просто вычисляет значения градиентов потерь по отношению к входам в
    RNNNode
    , учитывая градиенты потерь по отношению к выходам
    RNNNode
    . Здесь работает такая же логика, которую мы обсуждали в главах 1 и 2. Поскольку
    RNNNode предоставляется в виде последовательности операций, то можно просто вычислить производную каждой операции, рассчитанной на ее входе, и последовательно умножить эти производные на те, что были раньше (по правилам умножения матриц), чтобы получить массивы ndarray
    , где будут храниться градиенты потерь по отношению к каждому из входов. Напишем код:
    def forward(self, x_in: ndarray,
    H_in: ndarray, params_dict: Dict[str, Dict[str, ndarray]]
    ) -> Tuple[ndarray]:
    '''
    param x: массив numpy формы (batch_size, vocab_size)
    param H_prev: массив numpy формы (batch_size, hidden_size)
    return self.x_out: массив numpy формы (batch_size, vocab_size)

    218
    Глава 6. Рекуррентные нейронные сети return self.H: массив numpy формы (batch_size, hidden_size)
    '''
    self.X_in = x_in self.H_in = H_in self.Z = np.column_stack((x_in, H_in))
    self.H_int = np.dot(self.Z, params_dict['W_f']['value']) \
    + params_dict['B_f']['value']
    self.H_out = tanh(self.H_int)
    self.X_out = np.dot(self.H_out, params_dict['W_v']['value']) \
    + params_dict['B_v']['value']
    return self.X_out, self.H_out
    Обратите внимание, что, как и в классе
    Operaton до этого, формы входов в функцию backward должны соответствовать формам выходов функции forward
    , а формы выходов backward
    — формам входов forward
    Ограничения классических узлов RNN
    Вспомним, что цель
    RNN
    — выявлять зависимости в последовательностях данных. В терминах нашего примера о прогнозировании цены на нефть это означает, что мы должны иметь возможность выявить связь между последовательностью признаков, которые мы определили ранее, и тем, что произойдет с ценой на нефть на следующем шаге. Но сколько по- следних шагов нужно взять? В отношении цен на нефть можно предпо- ложить, что наиболее важной будет «вчерашняя» информация, то есть на один временной шаг назад, «позавчерашняя» будет уже чуть менее важной, и т. д — ценность информации, как правило, снижается по мере продвижения назад во времени.
    Для многих задач это верно, но есть и такие области применения RNN, где требуется изучать много данных и длительные зависимости. Классиче- ский пример — моделирование языка, то есть построение модели, которая сможет предсказать следующий символ, слово или часть слова, учитывая весьма длинный ряд предыдущих слов или символов (это распространен- ная задача, и мы обсудим некоторые ее особенности позже в этой главе).
    Для этого классических RNN обычно недостаточно. Теперь, когда мы

    RNN: код
    219
    знаем чуть больше, становится ясно, почему: на каждом временном шаге скрытое состояние умножается на одну и ту же весовую матрицу. Рас- смотрим, что происходит, когда мы умножаем число на значение x
    снова и снова: если x
    <
    1
    , число уменьшается в геометрической прогрессии до
    0, и если x
    >
    1
    , число увеличивается экспоненциально до бесконечности.
    У рекуррентных нейронных сетей та же проблема: поскольку на каж- дом временном шаге один и тот же набор весов умножается на скрытое состояние, градиент весов со временем становится либо чрезвычайно маленьким, либо чрезвычайно большим. Первая проблема известна как
    «затухание градиента», а вторая — «взрыв градиента». Оба эти явления затрудняют обучение RNN моделированию долгосрочных зависимостей
    (50–100 временных шагов), необходимых для высококачественного моделирования языка. Есть две известные модификации классических архитектур RNN, которые мы рассмотрим далее и у которых эта проблема в значительной степени решена.
    Решение: узлы GRUNode
    Классические RNN берут входные значения и скрытое состояние, объ- единяют их, а затем с помощью умножения матриц определяют то, на- сколько важна информация в скрытом состоянии по сравнению с входной информацией для прогнозирования выходных данных. Более продвину- тые варианты RNN строятся вокруг идеи о том, что для моделирования долгосрочных зависимостей, таких как языковые, мы иногда получаем
    информацию, которая говорит, что нужно «забывать» или «сбрасывать»
    наше скрытое состояние. Простой пример — символы точки «.» или двоеточия «:». Если модель встречает один из этих символов, то знает, что нужно забыть все, что было ранее, и начинать моделировать новую последовательность с нуля.
    Первый вариант RNN с этой идеей — это GRU, или Gated Recurrent Units
    (управляемые рекуррентные нейроны), названные так потому, что вход и предыдущее скрытое состояние проходят через серию «шлюзов».
    1. Первый шлюз аналогичен операциям, которые выполняются в клас- сических RNN: входное и скрытое состояния объединяются, умно- жаются на матрицу весов, а затем проходят через сигмоиду. Это шлюз «обновления».

    220
    Глава 6. Рекуррентные нейронные сети
    2. Второй шлюз — это шлюз «сброса»: входное и скрытое состояния объединяются, умножаются на весовую матрицу, проходят через сигмоиду, а затем умножаются на предшествующее скрытое состо- яние. Это позволяет сети «учиться забывать», что было в скрытом состоянии, учитывая текущий вход.
    3. Затем выход второго шлюза умножается на другую матрицу и пере- дается через функцию
    Tanh
    , причем выходные данные получаются новым «потенциальным» скрытым состоянием.
    4. Наконец, в скрытое состояние попадает шлюз обновления, умно- женный на «потенциальное» новое скрытое состояние плюс старое скрытое состояние, умноженное на 1, минус шлюз обновления.
    В этой главе мы рассмотрим два усовершенствованных варианта классических RNN: GRU и LSTM. LSTM более популярны и были изобретены задолго до GRU. Однако GRU — это более простая версия
    LSTM, в которой яснее видно, как идея шлюзов может позволить RNN
    «научиться сбрасывать» свое скрытое состояние с учетом входных данных, поэтому начнем именно с GRU.
    GRUNode: схема
    На рис. 6.8 узел
    GRUNode изображен как серия шлюзов. Каждый шлюз со- держит операции плотного слоя: умножение на матрицу весов, добавление смещения и пропуск результата через функцию активации. Функции активации — сигмоида, и тогда результат оказывается в диапазоне от
    0 до 1, либо
    Tanh
    , в этом случае получается диапазон от –1 до 1. Диапазон каждого промежуточного ndarray
    , созданного следующим, показан под именем массива.
    На рис. 6.8, как и на рис. 6.9 и 6.10, входные данные узла окрашены в зе- леный цвет, вычисленные промежуточные величины окрашены в синий цвет, а выходные данные — в красный. Все веса (на рисунке не показаны) содержатся в шлюзах.
    Обратите внимание, что для обратного распространения через такую сеть нам нужно будет использовать последовательность экземпляров
    Operation
    , вычислить производную каждой операции по отношению к ее входу и перемножить результаты. Здесь это явно не показано, так как

    RNN: код
    1   ...   14   15   16   17   18   19   20   21   22


    написать администратору сайта