Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
257 X_train_auto = (X_train - X_train.min()) / (X_train.max() - X_train.min()) * 2 - 1 X_test_auto = (X_test - X_train.min()) / (X_train.max() - X_train.min()) * 2 - 1 И в-третьих, можем обучить нашу модель, используя тренировочный код, который к настоящему времени должен выглядеть знакомым (в качестве размерности вывода кодирования наугад зададим 28): model = Autoencoder(hidden_dim=28) criterion = nn.MSELoss() optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) trainer = PyTorchTrainer(model, optimizer, criterion) trainer.fit(X_train_auto, X_train_auto, X_test_auto, X_test_auto, epochs=1, batch_size=60) Запустив этот код и обучив модель, мы можем посмотреть как на вос- становленные изображения, так и на представления изображений, про- сто пропустив X_test_auto через модель (поскольку прямой метод был определен для возврата двух величин): reconstructed_images, image_representations = model(X_test_auto) Каждый элемент reconstructed_images — это Tensor [1, 28, 28] , который представляет собой лучшую попытку нейронной сети восстановить соответствующее исходное изображение после прохождения его через архитектуру автокодировщика, пропустившего изображение через слой с более низкой размерностью. На рис. 7.3 показано случайно выбранное восстановленное изображение и исходное изображение. Восстановленное изображение Исходное изображение Рис. 7.3. Изображение из тестового набора MNIST вместе с восстановленным после его пропуска через автокодировщик 258 Глава 7. Библиотека PyTorch Визуально изображения выглядят одинаково, а это значит, что нейронная сеть действительно взяла исходные изображения размером 784 пикселя и сопоставила их с пространством более низкой размерности, а именно 28, и большая часть информации о 784-пиксельном изображении закодиро- вана в векторе длиной 28. Как нам исследовать весь набор данных, чтобы увидеть, действительно ли нейронная сеть изучила структуру данных изо- бражения без каких-либо меток? «Структура данных» здесь означает, что лежащие в основе данные на самом деле представляют собой изображения из 10 различных рукописных цифр. Таким образом, изображения, близ- кие к данному изображению в новом 28-мерном пространстве, в идеале должны иметь одну и ту же цифру или, по крайней мере, визуально быть очень похожими, поскольку мы, люди, различаем изображения именно по визуальному сходству. Можно проверить, так ли это, применив технику уменьшения размерности, изобретенную Лоренсом ван дер Маатеном в бытность его аспирантом у Джеффри Хинтона (одного из «отцов-осно- вателей» нейронных сетей): t-распределенное стохастическое вложение соседей, или t-SNE. Метод t-SNE выполняет уменьшение размерности способом, аналогичным обучению нейронных сетей: начинает с началь- ного представления нижнего измерения, а затем обновляет его, так что со временем оно приближается к решению со свойством, которое указывает, что «близко друг к другу» в многомерном пространстве означает «близко друг к другу» в низкоразмерном пространстве, и наоборот 1 Сделаем следующее: y Пропустим 10 000 изображений через t-SNE и уменьшим размерность до 2. y Визуализируем получающееся двумерное пространство, окрашивая его различные точки реальной меткой (которую не видел автокоди- ровщик). На рис. 7.4 показан результат. Получается, что изображения каждой цифры в основном сгруппированы в отдельном кластере; это показывает, что обучение нашей архитектуры автокодировщика позволило ей восстанавливать исходные изображения из низкоразмерного представления и в целом выявить общую структуру 1 Оригинальная статья 2008 года — Visualizing Data using t-SNE Лоренса ван дер Маатена и Джеффри Хинтона ( oreil.ly/2KIAaOt ). P. S. Обучение без учителя через автокодировщик 259 и закономерности у изображений без каких-либо меток 1 . Более того, по- мимо отдельных кластеров для всех 10 цифр мы видим, что визуально похожие цифры также расположены ближе друг к другу: сверху и чуть правее у нас есть группы цифр 3, 5 и 8, внизу рядом 4 и 9, и недалеко от 1 Кроме того, это произошло без особых усилий: архитектура здесь очень проста, и мы не используем никаких хитростей, например снижения скорости обучения, поскольку обучение выполняется всего за одну эпоху. Это показывает, что основ- ная идея использования подобной автокодировщику архитектуры для изучения структуры набора данных без меток в целом хороша и просто не сработала в данном конкретном случае. 3 2 1 7 9 5 8 4 0 6 Рис. 7.4. Результат запуска t-SNE на 28-мерном пространстве автокодировщика 260 Глава 7. Библиотека PyTorch них 7. Наконец, самые разные цифры — 0, 1 и 6 — образуют самые уда- ленные кластеры. Более сильный тест и решение для обучения без учителя Сейчас мы проверили, смогла ли модель выявить базовую структуру про- странства входных изображений. Уже не должен удивлять тот факт, что сверточная нейронная сеть может изучать представления изображений цифр, наделяя при этом визуально похожие изображения похожими представлениями. Возьмемся за задачу покруче: попробуем проверить, обнаружит ли нейронная сеть «гладкое» базовое пространство: простран- ство, в котором любой вектор длиной 28, а не только векторы, полученные в результате передачи реальных цифр через сеть кодировщика, может быть сопоставлен с реалистично выглядящей цифрой. Оказывается, наш автокодировщик не может этого сделать. На рис. 7.5 показан результат генерации пяти случайных векторов длиной 28 и пропуска их через сеть декодера с использованием того факта, что автокодировщик содержал декодер в качестве атрибута: test_encodings = np.random.uniform(low=-1.0, high=1.0, size=(5, 28)) test_imgs = model.decoder(Tensor(test_encodings)) Рис. 7.5. Результат пропуска пяти случайно сгенерированных векторов через декодер Видно, что получающиеся изображения не похожи на цифры. Получается, что хотя автокодировщик и способен разумно отобразить наши данные в пространстве меньшего размера, он, по-видимому, не в состоянии осво- ить «гладкое» пространство. Решение проблемы обучения нейронной сети представлению изобра- жений в обучающем наборе в «гладком» базовом пространстве является одним из главных достижений порождающих состязательных сетей (generative adversarial networks, GAN). Изобретенные в 2014 году, GAN Заключение 261 наиболее широко известны тем, что позволяют нейронным сетям гене- рировать реалистичные изображения с помощью процедуры обучения, в которой две нейронные сети обучаются одновременно. В 2015 году GAN серьезно продвинулись, когда исследователи использовали их с глубоко сверточной архитектурой в обеих сетях не только для создания реалистично выглядящих 64 × 64 цветных изображений спален, но и для генерации большой выборки указанных изображений из случайно сгене- рированных 100-размерных векторов 1 . Это признак того, что нейронные сети действительно изучили базовое представление «пространства» за- данных немаркированных изображений. GAN заслуживают отдельной книги, поэтому подробно на них останавливаться я не буду. Заключение Теперь у вас есть глубокое понимание механики самых популярных пере- довых архитектур глубокого обучения, а также того, как реализовать эти архитектуры в одной из самых популярных высокопроизводительных сред глубокого обучения. Чтобы использовать модели глубокого обучения для решения реальных проблем, нужно лишь одно — практика. Вам пред- стоит (без особых проблем) читать чужой код и быстро осваивать детали и приемы реализации, заставляющие определенные архитектуры моделей работать над конкретными проблемами. Список рекомендуемых шагов приведен в репозитории GitHub книги ( https://oreil.ly/2N4H8jz ). Вперед! 1 Ознакомьтесь с документацией DCGAN Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks Алека Рэдфорда и др. ( arxiv.org/ abs/1511.06434 ), а также с документацией по PyTorch ( oreil.ly/2TEspgG ). ПРИЛОЖЕНИЕ A Глубокое погружение Рассмотрим подробно некоторые технические области. Это важная для понимания информация, но необязательная. Цепное правило Сначала поясним, почему мы можем заменить в выражении по цепному правилу из главы 1. Известно, что L означает следующее: а это лишь сокращение: и так далее. Давайте распишем одно из этих выражений. Как бы это вы- глядело, если бы мы взяли частную производную, скажем, по каждому элементу X (что, в конечном счете, мы и хотим сделать со всеми шестью компонентами L)? Поскольку , нетрудно понять, что частная производная этого по x 1 благодаря очень простому применению цепного правила имеет вид: Цепное правило 263 Поскольку единственное, на что умножается x 11 в выражении XW 11 , это w 11 , частная производная по отношению ко всему остальному равна 0. Итак, вычисляя частную производную по всем элементам X, получим следующее общее выражение для : Аналогично мы можем, например, определить частную производную от по каждому элементу X: Теперь у нас есть все компоненты для вычисления . Мы можем просто вычислить шесть матриц той же формы, что и предыдущие ма- трицы, и сложить результаты вместе. Обратите внимание, что математика стала запутанной, но не сложной. Вы можете пропустить следующие вычисления и сразу перейти к заключе- нию — там одно простое выражение. Но работа с выражениями даст вам большее понимание того, насколько удивительно прост вывод. А что есть жизнь, как не радость от чего-то классного? Здесь всего два шага. Во-первых, напишем, что есть сумма шести только что описанных матриц: 264 Приложение A. Глубокое погружение Теперь объединим эту сумму в одну большую матрицу. Эта матрица по- началу не будет иметь какой-либо интуитивно понятной формы, но на самом деле она является результатом вычисления предыдущей суммы: Цепное правило 265 Теперь самое интересное. Напомним, что: При этом W спрятано в предыдущей матрице — оно просто транспони- ровано. Напомним, что: Получается, что предыдущая матрица эквивалентна следующему: Далее, помните, что мы искали что-то, чтобы заменить вопросительный знак в следующем уравнении: Что ж, выходит, что это W. Результат сам идет в руки. Также обратите внимание, что это тот же самый результат, который мы уже видели ранее. Опять же, это объясняет, почему глубокое обучение работает, и позволя- ет правильно его реализовать. Означает ли это, что мы можем заменить знак вопроса в предыдущем уравнении и сказать, что ? Нет, не совсем. Но если перемножить два входа (X и W), чтобы получить результат N, и пропустить эти входы через некоторую нелинейную функ- цию σ, чтобы в итоге получить выход S, можно сказать следующее: 266 Приложение A. Глубокое погружение Этот математический факт позволяет эффективно вычислять и выражать обновления градиента, используя обозначение умножения матриц. Кроме того, аналогичными рассуждениями можно понять, что: Градиент потерь с учетом смещения Далее мы подробно рассмотрим, почему при вычислении производной по- тери по смещению в полносвязной нейронной сети мы суммируем по axis=0 Добавление смещения в нейронную сеть происходит в следующем кон- тексте: у нас есть пакет данных, представленный матрицей измерения n строк (размер пакета) на f столбцов (количество объектов), и мы прибав- ляем число к каждой из функций. Например, в примере нейронной сети из главы 2 у нас есть 13 функций, а смещение B имеет 13 чисел; первое число будет добавлено к каждой строке в первом столбце M1 = np.dot (X, weights [W1]) , второе число будет добавлено к каждой строке во втором столбце и т. д. Далее, в сети B2 будет содержать одно число, которое будет просто добавлено к каждой строке в одном столбце M2 . Таким образом, по- скольку в каждую строку матрицы будут добавлены одинаковые числа, на обратном проходе нам нужно сложить градиенты вдоль измерения, пред- ставляющего строки, к которым был добавлен каждый элемент смещения. Вот почему мы суммируем выражения для dLdB1 и dLdB2 вдоль axis=0 , например dLdB1 = (dLdN1 × dN1dB1).sum (axis = 0) . На рис. A.1 приведено визуальное объяснение с некоторыми комментариями. Свертка с помощью умножения матриц Наконец, мы покажем, как выразить пакетную операцию многоканальной свертки в контексте пакетного умножения матриц, чтобы эффективно реализовать ее в NumPy. Чтобы понять, как работает свертка, рассмотрим, что происходит на прямом проходе полносвязной нейронной сети: y Получаем ввод размера [batch_size, in_features] y Умножаем его на параметр размера [in_features, out_features] y Получаем результирующий вывод размера [batch_size, out_features] Свертка с помощью умножения матриц 267 В сверточном слое наоборот: y Получаем ввод размера [batch_size, in_channels, img_height, img_width] y Сворачиваем его с параметром размера [in_channels, out_channels, param_height, param_width] y Получаем результирующий вывод размера [batch_size, in_channels, img_height, img_width] Ключ к тому, чтобы операция свертки выглядела более похожей на операцию передачи вперед, состоит в том, чтобы сначала извлечь «фрагменты изображе- ния» размером img_height × img_width из каждого канала входного изображе- ния. Как только эти фрагменты будут извлечены, входные данные могут быть изменены, так что операция свертки может быть выражена как умножение пакетной матрицы с использованием функции Num.Py np.matmul . Сначала: def _get_image_patches(imgs_batch: ndarray, fil_size: int): ''' imgs_batch: [batch_size, channels, img_width, img_height] fil_size: int ''' # подача изображений imgs_batch_pad = np.stack([_pad_2d_channel(obs, fil_size // 2) for obs in imgs_batch]) patches = [] img_height = imgs_batch_pad.shape[2] x 11 + b 11 x n1 + b 11 : = x 1f + b 1f x nf + b 1f O 11 O 21 O n1 O 12 O 22 O n2 O 1f O 2f O nf Включает b 11 Включает b 12 Включает b 1f Вывод операции BiasAdd b 11 влияет на производительность пропорционально: (0 11 grad + 0 21 grad + ... + 0 n1 grad ) b 1f влияет на производительность пропорционально: (0 1f grad + 0 2f grad + ... + 0 nf grad ) Общий градиент [b 11 b 12 ...b 1f ] представляет собой сумму выходного градиента по строкам (ось = 0) : = Рис. A.1. Краткое изложение того, почему вычисление производной выходного сигнала полносвязного слоя по смещению включает суммирование по axis = 0 268 Приложение A. Глубокое погружение # для каждого места на изображении... for h in range(img_height-fil_size+1): for w in range(img_height-fil_size+1): # ...берем фрагмент размером [fil_size, fil_size] patch = imgs_batch_pad[:, :, h:h+fil_size, w:w+fil_size] patches.append(patch) # Получение вывода размером # [img_height * img_width, batch_size, n_channels, fil_size, # fil_size] return np.stack(patches) Затем можно вычислить вывод операции свертки следующим образом: 1. Взять фрагменты изображения размером [batch_size, in_channels, img_height x img_width, filter_size, filter_size] 2. Заменить их на [batch_size, img_height × img_width, in_ channels × filter_size × filter_size] 3. Параметр формы должен быть [in_channels × filter_size × filter_ size, out_channels] 4. После умножения пакетной матрицы результатом будет [batch_size, img_height × img_width, out_channels] 5. Заменить на [batch_size, out_channels, img_height, img_width] def _output_matmul(input_: ndarray, param: ndarray) -> ndarray: ''' conv_in: [batch_size, in_channels, img_width, img_height] param: [in_channels, out_channels, fil_width, fil_height] ''' param_size = param.shape[2] batch_size = input_.shape[0] img_height = input_.shape[2] patch_size = param.shape[0] * param.shape[2] * param.shape[3] patches = _get_image_patches(input_, param_size) patches_reshaped = ( patches Свертка с помощью умножения матриц 269 .transpose(1, 0, 2, 3, 4) .reshape(batch_size, img_height * img_height, -1) ) param_reshaped = param.transpose(0, 2, 3, 1).reshape(patch_size, -1) output = np.matmul(patches_reshaped, param_reshaped) output_reshaped = ( output .reshape(batch_size, img_height, img_height, -1) .transpose(0, 3, 1, 2) ) return output_reshaped Это был прямой проход. Для обратного прохода нужно рассчитать как градиент параметра, так и входные градиенты. Опять же, в качестве основы можно использовать способ для полносвязной нейронной сети. Начиная с градиента параметра градиент полносвязной нейронной сети: np.matmul(self.inputs.transpose(1, 0), output_grad) Это позволяет понять, как реализуется обратный проход через опера- цию свертки: здесь форма ввода — [batch_size, in_channels, img_height, img_width] , а полученный градиент вывода — [batch_size, out_channels, img_height, img_width] . Учитывая, что форма параметра имеет вид [in_chan nels, out_channels, param_height, param_width] , мы можем выполнить это преобразование с помощью следующих шагов: 1. Извлечь фрагменты изображения из входного изображения, что приведет к тому же результату, что и в прошлый раз, в форме [batch_ size, in_channels, img_height x img_width, filter_size, filter_size] 2. Используя умножение из полносвязного случая в качестве моти- вации, изменить его, чтобы оно имело форму [in_channels × param_ height × param_width, batch_size × img_height × img_width] 3. Изменить форму выходных данных — из [batch_size, out_channels, img_height, img_width] на [batch_size × img_height × img_width, out_channels] 4. Перемножить их, чтобы получить результат в форме [ i n _ channels × param_height × param_width, out_channels] 270 Приложение A. Глубокое погружение 5. Изменить форму, чтобы получить окончательный градиент параме- тра формы [in_channels, out_channels, param_height, param_width] Этот процесс реализован следующим образом: def _param_grad_matmul(input_: ndarray, param: ndarray, output_grad: ndarray): ''' input_: [batch_size, in_channels, img_width, img_height] param: [in_channels, out_channels, fil_width, fil_height] output_grad: [batch_size, out_channels, img_width, img_height] ''' param_size = param.shape[2] batch_size = input_.shape[0] img_size = input_.shape[2] ** 2 in_channels = input_.shape[1] out_channels = output_grad.shape[1] patch_size = param.shape[0] * param.shape[2] * param.shape[3] patches = _get_image_patches(input_, param_sizes) patches_reshaped = ( patches .reshape(batch_size * img_size, -1) ) output_grad_reshaped = ( output_grad .transpose(0, 2, 3, 1) .reshape(batch_size * img_size, -1) ) param_reshaped = param.transpose(0, 2, 3, 1).reshape(patch_size, -1) param_grad = np.matmul(patches_reshaped.transpose(1, 0), output_grad_reshaped) param_grad_reshaped = ( param_grad .reshape(in_channels, param_size, param_size, out_channels) .transpose(0, 3, 1, 2) ) return param_grad_reshaped Свертка с помощью умножения матриц 271 Аналогично можно получить входной градиент, имитируя операции в полносвязном слое, а именно: np.matmul(output_grad, self.param.transpose(1, 0)) Код, приведенный ниже, вычисляет входной градиент: def _input_grad_matmul(input_: ndarray, param: ndarray, output_grad: ndarray): param_size = param.shape[2] batch_size = input_.shape[0] img_height = input_.shape[2] in_channels = input_.shape[1] output_grad_patches = _get_image_patches(output_grad, param_size) output_grad_patches_reshaped = ( output_grad_patches .transpose(1, 0, 2, 3, 4) .reshape(batch_size * img_height * img_height, -1) ) param_reshaped = ( param .reshape(in_channels, -1) ) input_grad = np.matmul(output_grad_patches_reshaped, param_ reshaped.transpose(1, 0)) input_grad_reshaped = ( input_grad .reshape(batch_size, img_height, img_height, 3) .transpose(0, 3, 1, 2) ) return input_grad_reshaped Эти три функции образуют ядро Conv2DOperation , в частности его методы _output, _param_grad и _input_grad , которые есть в библиотеке Lincoln ( https://oreil.ly/2KPdFay ) в репозитории GitHub книги. Об авторе Сет Вейдман — data scientist, несколько лет практиковал и преподавал основы машинного обучения. Начинал как первый специалист по данным в Trunk Club, где создавал модели оценки потенциальных клиентов и си- стемы рекомендаций. В настоящее время работает в Facebook, где создает модели машинного обучения в составе команды по инфраструктуре. В про- межутках преподавал Data Science и машинное обучение на буткемпах и в команде по корпоративному обучению в компании Metis. Обожает объяснять сложные концепции, стремясь найти простое в сложном. Об обложке Птица на обложке — берберийская каменная куропатка (Alectoris barbara). Берберийская куропатка родом из Северной Африки, Гибралтара и Ка- нарских островов, обитает в лесах, кустарниках и засушливых районах. Позже поселилась в Португалии, Италии и Испании. Берберийские куропатки предпочитают ходить, а не летать. Они весят до полукилограмма (300–400 граммов) и имеют размах крыльев 45 см. Птицы имеют светло-серый окрас, за исключением красновато-коричневой шеи с белыми пятнами, темно-желтого брюшка и бело-коричневых полос на боку. Ноги, клюв и кольца вокруг глаз красные. Берберийские куропатки питаются семенами, разнообразной раститель- ностью и насекомыми. Весной самки откладывают от 10 до 16 яиц. Этот вид известен как территориальный. Часто издают резкие крякающие звуки, заявляя о своем присутствии. Хотя берберийской куропатке ничто не угрожает в глобальном масштабе, многие животные на обложках книг O'Reilly находятся под угрозой ис- чезновения. Все они важны для мира. Иллюстрация на обложке выполнена Карен Монтгомери на основе черно- белой гравюры British Birds. |