Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
Рис. 6.3. Обычная нейронная сеть, в которой наблюдение передается вперед и преобразуется в разные представления после каждого слоя Однако когда через сеть пройдет следующий набор наблюдений, эти представ- ления удалятся. Ключевым нововведением рекуррентных нейронных сетей и всех их вариантов является передача этих представлений обратно в сеть вместе со следующим набором наблюдений. Это будет выглядеть вот так: 1. На первом временном шаге t = 1 мы пропускаем через сеть наблю- дение с первого временного шага (возможно, с некоторыми случайно инициализированными представлениями). Получаем прогноз для t = 1 , а также представления на каждом слое. 202 Глава 6. Рекуррентные нейронные сети 2. На следующем временном шаге мы пропускаем наблюдение со вто- рого временного шага, t = 2 вместе с представлениями, вычисленны- ми на первом временном шаге (которые, опять же, являются просто выходами слоев нейронной сети), и каким-то образом объединяем их (именно по способам объединения и различаются варианты RNN, о которых чуть позже). И та и другая информация используется для вычисления прогноза для t = 2 , а также обновленных представле- ний на каждом слое, которые теперь являются функцией входных данных, передаваемых при t = 1 и при t = 2 3. На третьем временном шаге мы передаем наблюдение от t = 3 , а так- же представления, которые теперь включают информацию от t = 1 и t = 2 , используем эту информацию, чтобы составить прогнозы для t = 3 , а также обновляем представления каждого слоя, которые теперь содержат информацию из временных шагов 1–3. Этот процесс изображен на рис. 6.4. Вход (t = 1) Временной шаг 1: (t =1) Скрытый вектор (t = 1) Скрытый вектор n (t = 1) W 1 W 2 W r1 W n W n+1 Вход (t = 2) Временной шаг 2: (t = 2) W 1 W 2 W n W n+1 Вектор прогноза (t = 2) Вектор прогноза (t = 2) W rN W r1 W rN Рис. 6.4. В рекуррентных сетях представления каждого слоя используются на следующих временных шагах У каждого слоя есть «постоянное» представление, постепенно обновляю- щееся по мере прохождения новых наблюдений. Именно поэтому RNN не Введение в рекуррентные нейронные сети 203 позволяют использовать придуманную нами структуру Operation , которую мы сделали в предыдущих главах: объект ndarray , содержащий состояние каждого слоя, постоянно обновляется и многократно используется для составления прогнозов для последовательности данных в RNN. Поскольку мы не можем использовать структуру из предыдущей главы, начать при- дется с понимания того, какие классы потребуются для работы с RNN. Первый класс для RNN: RNNLayer Исходя из описания того, чего мы ожидаем от RNN, становится ясно, что нам понадобится класс RNNLayer , который будет передавать последователь- ность данных по одному элементу последовательности за раз. Давайте теперь подробно рассмотрим, как такой класс должен работать. Как мы говорили в этой главе, RNN работает с данными, в которых каждое на- блюдение является двумерным и имеет размерность (sequence_length, num_features) ; и поскольку с вычислительной точки зрения всегда более эффективно передавать данные в пакетном режиме, класс RNNLayer должен принимать трехмерные объекты ndarray размера (batch_size, sequence_ length, num_features) . Однако в предыдущем разделе я объяснил, что мы хотим передавать наши данные через RNNLayer по одному элементу последовательности за раз. Как это сделать, имея формат данных, данные (batch_size, sequence_length, num_features) ? А вот как: 1. Выберем двумерный массив по второй размерности начиная с data [:, 0,:] . Этот ndarray будет иметь форму ( batch_size , num_features ). 2. Инициализируем «скрытое состояние» для объекта RNNLayer , кото- рое будет постоянно обновляться по мере передачи элементов по- следовательности, оно будет иметь форму ( batch_size , hidden_size ). Этот ndarray хранит «накопленную информацию» слоя о данных, которые были переданы на предыдущих временных шагах. 3. Пропустим эти два ndarray вперед через первый временной шаг в этом слое. В конечном итоге наш RNNLayer будет выводить ndarrays не той же размерности, что были на входе, в отличие от плотных сло- ев, поэтому выходные данные будут иметь форму (batch_size , num_ outputs ). Кроме того, нужно обновить представление нейронной сети для каждого наблюдения: на каждом временном шаге наш RNNLayer должен также выводить ndarray формы ( batch_size , hidden_size ). 4. Выбираем следующий двумерный массив из данных: data [:, 1,:] 204 Глава 6. Рекуррентные нейронные сети 5. Передаем эти данные, а также значения представлений RNN, выве- денных на первом временном шаге, на второй временной шаг на этом слое, чтобы получить еще один вывод формы ( batch_size , num_outputs ), а также обновленные представления формы ( batch_size , hidden_size ). 6. Продолжаем эти действия, пока через слой не пройдут все вре- менные шаги в количестве sequence_length . Затем объединяем все результаты, чтобы получить выходные данные этого слоя формы ( batch_size , sequence_length , num_outputs ). Этот алгоритм задает представление о том, как должен работать класс RNNLayer , и мы разберемся еще лучше, когда будем писать код. Но это еще не все — нам понадобится еще один класс для получения данных и обновления скрытого состояния слоя на каждом шаге. Для этого мы будем использовать RNNNode Второй класс для RNN: RNNNode Исходя из описания, которое мы привели в предыдущем разделе, класс RNNNode должен иметь метод forward со следующими входами и выходами: y Два ndarray в качестве входных данных: x один для ввода данных в сеть, с формой [batch_size, num_fea tures] ; x один для представлений наблюдений на этом временном шаге, с формой [batch_size, hidden_size] y Два ndarray в качестве выходных данных: x один для выходных сети на этом временном шаге, с формой [batch_ size, num_outputs] ; x один для обновленных представлений наблюдений на этом времен- ном шаге, с формой [batch_size, hidden_size] Далее мы покажем, как классы RNNNode и RNNLayer будут работать вместе. Объединение двух классов Класс RNNLayer оборачивается вокруг списка объектов RNNNode и (по край- ней мере) будет содержать метод forward , у которого будут следующие входные и выходные данные: Введение в рекуррентные нейронные сети 205 y входные данные: пакет последовательностей наблюдений формы [batch_size, sequence_length, num_features] ; y выходные данные: выход нейронной сети для этих последовательностей формы [batch_size, sequence_length, num_outputs] На рис. 6.5 показан порядок передачи данных через RNN с двумя слоями RNNL по пять узлов RNN в каждом. На каждом временном шаге входные данные, изначально имеющие размерность feature_size , последователь- но передаются через первый RNNNode в каждом RNNLayer , при этом сеть в конечном итоге выводит на этом временном шаге прогноз размерности output_size . Кроме того, каждый RNNNode передает «скрытое состояние» следующему RNNNode в каждом слое. Как только данные каждого из пяти временных шагов пройдут через все слои, мы получим окончательный набор прогнозов формы (5, output_size) , где output_size должен быть того же размера, что и цель. Затем эти прогнозы сравниваются с целевы- ми значениями, и рассчитывается градиент потерь на обратном проходе. На рис. 6.5 все это показано вместе на примере того, как данные формата 5 × 2 RNNNode проходят от первого до последнего (10) слоя, hidden_size hidden_size [batch_size, feature_size] RNNLayer RNNLayer RNNNode Выходной массив: [batch_size, sequence_length, output_size] Входной массив: [batch_size, sequence_length, feature_size] 2 4 6 8 10 1 3 5 7 9 Рис. 6.5. Порядок передачи данных через RNN с двумя слоями, разработанную для обработки последовательностей длиной 5 В качестве альтернативы данные могут проходить через RNN в порядке, показанном на рис. 6.6. Каким бы порядок ни был, в целом, должно про- изойти следующее: 206 Глава 6. Рекуррентные нейронные сети y Каждый слой должен обрабатывать данные до следующего слоя, на- пример на рис. 6.5 пункт 2 не может произойти раньше 1, а 4 не может произойти раньше 3. y Аналогично каждый слой должен обрабатывать все свои временные шаги по порядку — например, на рис. 6.5 пункт 4 не может происходить раньше 2, а 3 не может происходить раньше 1. y Последний слой должен иметь размерность выходных данных feature_ size для каждого наблюдения. hidden_size hidden_size [batch_size, feature_size] RNNLayer RNNLayer RNNNode Выходной массив: [batch_size, sequence_length, output_size] Входной массив: [batch_size, sequence_length, feature_size] 6 7 8 9 10 1 2 3 4 5 Рис. 6.6. Другой вариант порядка прохождения данных через ту же RNN на прямом проходе Все это касается прямого прохода RNN. А что насчет обратного прохода? Обратный проход Обратное распространение через рекуррентные нейронные сети часто описывается как отдельный алгоритм, называемый «обратное распростра- нение по времени». Название соответствует тому, как это происходит, но звучит намного сложнее, чем на самом деле. Помня наше рассуждение о том, как данные передаются через RNN, мы можем описать, что про- исходит на обратном проходе, следующим образом: обратный проход по RNN есть передача градиентов в обратном направлении через сеть в по- рядке, обратном тому, в каком мы передавали входные данные на прямом проходе. То есть мы делаем то же самое, что и в обычных сетях. Введение в рекуррентные нейронные сети 207 Исходя из рис. 6.5 и 6.6, на прямом проходе происходит следующее: 1. Изначально есть серия наблюдений, каждое из которых имеет форму (feature_size , sequence_length) 2. Эти входные данные разбиваются на отдельные элементы sequence_ length и передаются в сеть по одному. 3. Каждый элемент проходит через все слои, и в конечном итоге полу- чается выход размера output_size 4. Одновременно с этим слой передает скрытое состояние для расчетов в этом слое на следующем временном шаге. 5. Это проделывается для всех временных шагов sequence_length , в результате чего получается выход размера ( output_size , sequence_ length ). Обратное распространение работает так же, но наоборот: 1. Изначально у нас есть градиент формы [output_size, sequence_ length] , который говорит о том, как сильно каждый элемент вы- ходных данных (тоже размера [output_size, sequence_length] ) в конечном итоге влияет на потери для данной серии наблюдений. 2. Эти градиенты разбиваются на отдельные элементы sequence_length и пропускаются через слои в обратном порядке. 3. Градиент каждого элемента передается через все слои. 4. Одновременно с этим слои передают градиент потерь по отношению к скрытому состоянию для данного временного шага назад в вычис- ления слоев на предыдущие шаги. 5. Это продолжается для всех временных шагов sequence_length , пока градиенты не будут переданы назад каждому слою в сети, что позво- лит вычислить градиент потерь по каждому весу, как мы это делаем в обычных нейросетях. Соотношение между обратным и прямым проходами показано на рис. 6.7, из которого видно, как данные проходят через RNN во время обратного прохода. Вы, конечно, заметите, что это то же самое, что и на рис. 6.5, но с перевернутыми стрелками и измененными числами. На высоком уровне прямой и обратный проходы для слоя RNNLay очень похожи на проходы слоя в обычной нейронной сети: на вход приходит 208 Глава 6. Рекуррентные нейронные сети ndarray определенной формы, на выходе получается ndarray другой фор- мы, а на обратном проходе приходит выходной градиент той же формы, что и их выходные данные, и создается входной градиент той же формы, что и входные данные. Но есть важное отличие в том, как в RNNLayer об- рабатываются градиенты веса по сравнению с другими слоями, поэтому кратко рассмотрим этот вопрос, прежде чем перейти к написанию кода. hidden_size (опционально) hidden_size [batch_size, feature_size] Выходной градиент: [batch_size, sequence_length, output_size] [batch_size, output_size] Входной массив: [batch_size, sequence_length, feature_size] 9 7 5 3 1 10 8 6 4 2 Рис. 6.7. На обратном проходе RNN передают данные в направлении, противоположном тому, как данные передаются во время прямого прохода Накопление градиентов для весов в РНН В рекуррентных нейронных сетях, как и в обычных нейросетях, у каждого слоя свой набор весов. Это означает, что один и тот же набор весов будет влиять на вывод слоя на всех временных шагах sequence_length . Следо- вательно, во время обратного распространения один и тот же набор весов будет получать разные градиенты sequence_length . Например, в кружке под номером «1» в схеме обратного распространения, показанной на рис. 6.7, второй слой получит градиент для последнего временного шага, а в кружке под номером «3» слой получит градиент предпоследнего шага. В обоих случаях будет использоваться один и тот же набор весов. Таким образом, во время обратного распространения нужно будет накапливать градиенты для весов по последовательности временных шагов. Это оз- начает, что независимо от выбранного способа хранения весов придется обновлять градиенты следующим образом: weight_grad += grad_from_time_step RNN: код 209 Этот подход отличается от слоев Dense и Conv2D , в которых мы просто сохраняли параметры в аргументе param_grad Мы раскрыли, как работают RNN и какие классы потребуются для их реализации; теперь поговорим подробнее. RNN: код Давайте начнем с тех вещей, в которых реализация RNN будет аналогична другим реализациям нейронных сетей, которые мы рассмотрели в этой книге: 1. RNN по-прежнему передает данные по слоям, при этом на прямом про- ходе передаются данные, а на обратном проходе — градиенты. Таким образом, независимо от того, каким окажется эквивалент нашего клас- са NeuralNetwork, у него все равно будет список RNNLayer в качестве атрибута слоев и прямой проход будет реализован примерно так: def forward(self, x_batch: ndarray) -> ndarray: assert_dim(ndarray, 3) x_out = x_batch for layer in self.layers: x_out = layer.forward(x_out) return x_out 2. Реализация Loss у RNN будут такой же, как и раньше: выходной ndarray производится последним Layer и сравнивается с вектором y_batch , вычисляется одно значение и градиент этого значения от- носительно входа в Loss с той же формой возвращается в качестве вывода. Нужно изменить функцию softmax , чтобы она работала соответствующим образом с ndarrays формы [batch_size, sequence_ length, feature_size] , но это не проблема. 3. Класс Trainer в основном не меняется: мы циклически перебираем наши обучающие данные, выбираем пакеты входных данных и паке- ты выходных данных и непрерывно передаем их через нашу модель, получая значения потерь, которые говорят нам, обучается ли наша модель, и обновляем весовые коэффициенты после каждой партии. Кстати, о весах… 210 Глава 6. Рекуррентные нейронные сети 4. Класс Optimizer остается прежним. Как мы увидим, придется на каж- дом временном шаге обновлять способ извлечения params и param_ grads , но «правила обновления» (которые мы записали в функции _update_rule в нашем классе) остаются прежними. Интересные вещи появляются именно в классе Layer Класс RNNLayer Ранее мы давали классу Layer набор Operation , который передавал данные вперед, а градиенты — назад. Слои RNNLayer будут совершенно другими. В них должно поддерживаться «скрытое состояние», которое постоянно обновляется по мере поступления новых данных и каким-то образом «объединяется» с данными на каждом временном шаге. Как именно это должно работать? За основу можно взять рис. 6.5 и 6.6. В них предпо- лагается, что у каждого RNNLayer должен быть список RNNNode в качестве атрибута, после чего каждый элемент последовательности из входных данных слоя должен проходить через каждый RNNNode , по одному за раз. Каждый RNNNode будет принимать этот элемент последовательности, а так- же «скрытое состояние» для этого слоя и создавать выходные данные для слоя на этом временном шаге, а также обновлять скрытое состояние слоя. Чтобы прояснить все это, давайте углубимся в код: рассмотрим по порядку, как должен инициализироваться класс RNNLayer , как он должен переда- вать данные вперед во время прямого прохода и как должен отправлять данные назад во время обратного хода. Инициализация У экземпляра RNNLayer должны быть: y целое значение hidden_size ; y целое значение output_size ; y ndarray start_H формы (1, hidden_size) , представляющей собой скры- тое состояние слоя. Кроме того, как и в обычных нейронных сетях, мы установим флаг self. first = True при инициализации слоя. При первой передаче данных в метод forward мы передадим полученный массив ndarray в метод _init_params , инициализируем параметры и установим self.first = False |