Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
246 Глава 7. Библиотека PyTorch Теперь, когда мы работаем с большими наборами данных, рассмотрим еще одну хорошую практику. Очевидно, что загружать все обучающие и тестовые наборы в память для обучения модели, как мы делаем с X_train , y_train , X_test и y_test , нерационально. У PyTorch есть способ обойти это: класс DataLoader Классы DataLoader и Transform Напомним, что в модели MNIST из главы 2 мы слегка обрабатывали данные MNIST, вычитая из них глобальное среднее и деля на глобальное стандартное отклонение, чтобы «нормализовать» данные: X_train, X_test = X_train - X_train.mean(), X_test - X_train.mean() X_train, X_test = X_train / X_train.std(), X_test / X_train.std() Для этого пришлось бы сначала полностью считать эти два массива в память. Но было бы намного эффективнее выполнить эту предвари- тельную обработку «на лету», по мере поступления данных в нейронную сеть. В PyTorch есть встроенные функции, которые реализуют это и чаще всего используются с изображениями. Это преобразования через модуль transforms и DataLoader из torch utils data : from torchvision.datasets import MNIST import torchvision.transforms as transforms from torch.utils.data import DataLoader Ранее мы считывали весь обучающий набор в X_train следующим образом: mnist_trainset = MNIST(root="../data/", train=True) X_train = mnist_trainset.train_data Затем преобразовывали X_train , чтобы привести его к форме, подходящей для моделирования. В PyTorch есть несколько удобных функций, которые позволяют нам выполнять множество преобразований для каждого пакета данных во время считывания. Это позволяет нам избежать считывания всего набора данных в память и использования преобразований PyTorch. Сначала мы определяем список преобразований, которые необходимо вы- полнять для каждой партии считываемых данных. Например, следующие действия преобразуют каждое изображение MNIST в Tensor (большинство Сверточные нейронные сети в PyTorch 247 наборов данных PyTorch по умолчанию являются «изображениями PIL», поэтому функция transforms.ToTensor() используется в первую очередь), а затем «нормализуют» набор данных — вычитая среднее значение, а за- тем деля на стандартное отклонение, используя общее среднее значение MNIST и стандартное отклонение 0.1305 и 0.3081 соответственно: img_transforms = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1305,), (0.3081,)) ]) Нормализация фактически вычитает среднее значение и стандарт- ное отклонение из каждого канала входного изображения. Таким образом, при работе с цветными изображениями с тремя входными каналами используется преобразование Normalize , у которого два кортежа по три числа в каждом, например transforms.Normalize ((0.1, 0.3, 0.6), (0.4, 0.2, 0.5)) DataLoader из этого полу- чает следующие задачи: y Нормализовать первый канал, используя среднее значение 0.1 и стандартное отклонение 0.4. y Нормализовать второй канал, используя среднее значение 0.3 и стандартное отклонение 0.2. y Нормализовать третий канал, используя среднее значение 0.6 и стандартное отклонение 0.5. После применения этих преобразований мы применяем их к набору данных, считывая их в пакетном режиме: dataset = MNIST("../mnist_data/", transform=img_transforms) Наконец, мы можем определить DataLoader , который принимает этот набор данных и определяет правила для последовательной генерации пакетов данных: dataloader = DataLoader(dataset, batch_size=60, shuffle=True) Затем мы можем модифицировать класс Trainer , чтобы использовать загрузчик данных для генерации пакетов, используемых для обучения 248 Глава 7. Библиотека PyTorch сети, вместо загрузки всего набора данных в память, а затем генерировать их вручную, используя функцию batch_generator , как мы делали раньше. На сайте книги ( https://oreil.ly/2N4H8jz ) 1 приведен пример обучения свер- точной нейронной сети с использованием таких DataLoaders . В классе Trainer достаточно лишь заменить строку: for X_batch, y_batch in enumerate(batch_generator): на: for X_batch, y_batch in enumerate(train_dataloader): Кроме того, вместо того чтобы вводить весь обучающий набор в функцию подгонки, мы передаем DataLoaders : trainer.fit(train_dataloader = train_loader, test_dataloader = test_loader, epochs=1, eval_every=1) Используя эту архитектуру и вызывая метод fit , как мы только что сде- лали, получаем около 97% точности в MNIST после одной эпохи. Однако более важным, чем точность, является то, что вы видели, как реализовать концепции, рассмотренные нами из первых принципов, в высокопроизво- дительной среде. Теперь, когда вы понимаете как базовые концепции, так и структуру, я призываю вас изменить код в репозитории GitHub книги ( https://oreil.ly/2N4H8jz ) и попробовать другие сверточные архитектуры, другие наборы данных и т. д. CNN были одной из двух продвинутых архитектур, которые мы рассмо- трели ранее в книге; давайте теперь посмотрим, как реализовать самый продвинутый вариант RNN, который мы рассмотрели, LSTM, в PyTorch. LSTM в PyTorch В предыдущей главе мы рассмотрели, как создать LSTM с нуля. Наш слой LSTMLayer получал входной массив размера [batch_size, sequence_ length, feature_size] и выводил ndarray размера [ batch_size , sequence_ length , feature_size ]. Кроме того, каждый слой получал скрытое состо- яние и состояние ячейки, каждое из которых изначально имело форму 1 См. раздел «Создание CNN с помощью PyTorch». Сверточные нейронные сети в PyTorch 249 [1, hidden_size] , затем расширялось до [batch_size, hidden_size] при передаче пакета, а затем сворачивалось обратно до [1, hidden_size ] по- сле завершения итерации. Исходя из этого, метод __init__ для нашего LSTMLayer будет выглядеть следующим образом: class LSTMLayer(PyTorchLayer): def __init__(self, sequence_length: int, input_size: int, hidden_size: int, output_size: int) -> None: super().__init__() self.hidden_size = hidden_size self.h_init = torch.zeros((1, hidden_size)) self.c_init = torch.zeros((1, hidden_size)) self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True) self.fc = DenseLayer(hidden_size, output_size) Как и в случае со сверточными слоями, в PyTorch есть операция nn.lstm для реализации LSTM . Обратите внимание, что в нашем LSTMLayer мы со- храняли DenseLayer в атрибуте self.fc . Вы можете вспомнить из предыду- щей главы, что последний шаг ячейки LSTM — это установка последнего скрытого состояния с помощью операций слоя Dense (умножение веса и добавление смещения), чтобы преобразовать скрытое состояние в раз- мерность out put_size для каждой операции. PyTorch работает немного иначе: операция nn.lstm просто выводит скрытые состояния для каждого временного шага. Таким образом, чтобы позволить нашему LSTMLayer вы- водить измерение, отличное от его входных данных (а нам так и надо), мы добавляем DenseLayer в конце, чтобы преобразовать скрытое состояние в измерение output_size В этой модификации функция forward становится похожа на функцию LSTMLayer из главы 6: def forward(self, x: Tensor) -> Tensor: batch_size = x.shape[0] h_layer = self._transform_hidden_batch(self.h_init, batch_size, 250 Глава 7. Библиотека PyTorch before_layer=True) c_layer = self._transform_hidden_batch(self.c_init, batch_size, before_layer=True) x, (h_out, c_out) = self.lstm(x, (h_layer, c_layer)) self.h_init, self.c_init = ( self._transform_hidden_batch(h_out, batch_size, before_layer=False).detach(), self._transform_hidden_batch(c_out, batch_size, before_layer=False).detach() ) x = self.fc(x) return x Ключевая строка здесь, которая должна выглядеть знакомо с учетом реализации LSTM в главе 6, выглядит так: x, (h_out, c_out) = self.lstm(x, (h_layer, c_layer)) Помимо этого, мы изменяем форму скрытого состояния и состояния ячей- ки до и после функции self.lstm с помощью вспомогательной функции self._transform_hidden_batch . Полная функция приведена в репозитории GitHub книги ( https://oreil.ly/2N4H8jz ). Наконец, обернем модель: class NextCharacterModel(PyTorchModel): def __init__(self, vocab_size: int, hidden_size: int = 256, sequence_length: int = 25): super().__init__() self.vocab_size = vocab_size self.sequence_length = sequence_length # В этой модели всего один слой, # с одинаковыми размерностями входа и выхода self.lstm = LSTMLayer(self.sequence_length, P. S. Обучение без учителя через автокодировщик 251 self.vocab_size, hidden_size, self.vocab_size) def forward(self, inputs: Tensor): assert_dim(inputs, 3) # batch_size, sequence_length, vocab_size out = self.lstm(inputs) return out.permute(0, 2, 1) Функция nn.CrossEntropyLoss ожидает, что первые две размерности будут равны batch_size и распределению по классам. Однако в на- шей реализации LSTM есть распределение по классам, когда послед- нее измерение ( vocab_size ) выходит из LSTMLayer . Поэтому, чтобы подготовить окончательный вывод модели для передачи в потерю, мы перемещаем измерение, содержащее распределение по буквам, во второе измерение, используя функцию out.permute (0, 2, 1) В репозитории GitHub книги ( https://oreil.ly/2N4H8jz ) я покажу, как написать класс LSTMTrainer для наследования от PyTorchTrainer и использования его для обучения NextCharacterModel для генерации текста. Мы исполь- зуем ту же предварительную обработку текста, что и в главе 6: выбор последовательностей текста, горячее кодирование букв и группирование последовательностей горячих кодированных букв в пакеты. Теперь вы знаете, как реализовать три рассмотренные нами архитектуры нейронных сетей (полносвязные, сверточные и рекуррентные) в PyTorch. В заключение мы кратко рассмотрим, как можно использовать нейронные сети для обучения без учителя. P. S. Обучение без учителя через автокодировщик В этой книге мы говорили о том, как используются модели глубокого об- у чения для решения задач обучения с учителем. Но есть и другая сторона машинного обучения: обучение без учителя. Она подразумевает так назы- ваемое «нахождение структуры в неразмеченных данных». Предпочитаю 252 Глава 7. Библиотека PyTorch считать это поиском взаимосвязей между признаками данных, которые еще не были измерены. В противовес этому, обучение с учителем — это поиск взаимосвязей между ранее измеренными признаками в данных. Предположим, у вас был набор неразмеченных данных изображений. Вы ничего не знаете об этих изображениях — например, вы не уверены, существует ли 10 различных цифр, или 5, или 20 (эти изображения могут быть из какого-то древнего алфавита), — и вы хотите знать ответы на такие вопросы, как: y Сколько есть разных цифр? y Какие цифры внешне похожи друг на друга? y Существуют ли «лишние» изображения, которые явно отличаются от других изображений? Чтобы понять, как глубокое обучение может помочь в этом, остановимся и подумаем, а что же пытаются сделать модели глубокого обучения. Обучение представлениям Мы уже видели, что модели глубокого обучения могут научиться делать точные прогнозы. Они делают это путем преобразования входных дан- ных, которые получают, в представления, которые постепенно становятся более абстрактными и более настроенными на решение любой задачи. В частности, последний слой сети, непосредственно перед слоем с самими предсказаниями (который будет иметь только один нейрон для задачи регрессии и num_classes нейронов для задачи классификации), является попыткой сети создать свое представление входных данных. Это макси- мально полезно для задачи прогнозирования (рис. 7.1). X Слой Слой Представление ввода сетью Предсказание Рис. 7.1. Последний слой нейронной сети, непосредственно перед предсказаниями, содержит представление входных данных с точки зрения сети, которое кажется ей наиболее подходящим P. S. Обучение без учителя через автокодировщик 253 После обучения модель может не только делать прогнозы для новых точек данных, но и генерировать представления этих точек данных. За- тем их можно будет использовать для кластеризации, анализа сходства или обнаружения лишних данных (в дополнение к прогнозированию). Не спешим вешать ярлыки Ограничение этого подхода состоит в том, что для обучения модели нуж- на какая-то классификация, метки. А как научить модель генерировать «полезные» представления без каких-либо меток? Если у нас нет меток, придется сгенерировать представления наших данных с помощью того, что у нас есть: самих данных. Эта идея лежит в основе класса нейросе- тевых архитектур, известных как автокодировщики, которые включают обучение нейронных сетей для восстановления обучающих данных, заставляя сеть изучать представление каждой точки данных, наиболее полезной для этой реконструкции. Визуализация На рис. 7.2 показан общий принцип работы автокодировщика: 1. Один набор слоев преобразует данные в сжатое представление данных. 2. Другой набор слоев преобразует это представление в выходные данные того же размера и формы, что и исходные данные. X X Слой Кодировщик Слой Скрытое представление Декодер Рис. 7.2. У автокодировщика есть один набор слоев (который можно рассматривать как сеть «кодировщика»), который отображает входные данные в более низкоразмерное представление, и другой набор слоев (который можно рассматривать как сеть «декодера»), который отображает низкоразмерное представление обратно на вход; эта структура заставляет сеть изучать низкоразмерное представление, которое наиболее полезно для восстановления входных данных 254 Глава 7. Библиотека PyTorch Реализация такой архитектуры иллюстрирует некоторые особенности PyTorch, которые мы еще не имели возможности представить. Реализация автокодировщика в PyTorch Теперь мы покажем простой автокодировщик, который принимает вход- ное изображение, пропускает его через два сверточных слоя, затем через плотный слой для генерации представления, а затем передает это пред- ставление обратно через плотный слой и два сверточных слоя для гене- рации выхода того же размера, что и вход. На этом примере я покажу две распространенные практики при реализации более сложных архитектур в PyTorch. Мы можем включить PyTorchModels в качестве атрибутов другой PyTorchModel , так же как ранее определили слои PyTorchLayer в качестве атрибутов таких моделей. В следующем примере мы реализуем наш ав- токодировщик как две PyTorchModel в качестве атрибутов: кодировщик и декодер. Как только мы обучим модель, то сможем использовать об- ученный кодировщик в качестве своей собственной модели для генерации представлений. Определение кодировщика: class Encoder(PyTorchModel): def __init__(self, hidden_dim: int = 28): super(Encoder, self).__init__() self.conv1 = ConvLayer(1, 14, activation=nn.Tanh()) self.conv2 = ConvLayer(14, 7, activation=nn.Tanh(), flatten=True) self.dense1 = DenseLayer(7 * 28 * 28, hidden_dim, activation=nn.Tanh()) def forward(self, x: Tensor) -> Tensor: assert_dim(x, 4) x = self.conv1(x) x = self.conv2(x) x = self.dense1(x) return x Определение декодера: P. S. Обучение без учителя через автокодировщик 255 class Decoder(PyTorchModel): def __init__(self, hidden_dim: int = 28): super(Decoder, self).__init__() self.dense1 = DenseLayer(hidden_dim, 7 * 28 * 28, activation=nn.Tanh()) self.conv1 = ConvLayer(7, 14, activation=nn.Tanh()) self.conv2 = ConvLayer(14, 1, activation=nn.Tanh()) def forward(self, x: Tensor) -> Tensor: assert_dim(x, 2) x = self.dense1(x) x = x.view(-1, 7, 28, 28) x = self.conv1(x) x = self.conv2(x) return x Если бы мы использовали шаг больше единицы, то просто не смогли бы использовать обычную свертку для преобразования кодирования в вывод. Вместо этого пришлось бы использовать транспонирован- ную свертку, где размер изображения окажется больше, чем размер изображения на входе. Смотрите операцию nn.ConvTrans pose2d в документации PyTorch для получения дополнительной информации ( https://oreil.ly/306qiV7 ). Тогда сам автокодировщик может выглядеть так: class Autoencoder(PyTorchModel): def __init__(self, hidden_dim: int = 28): super(Autoencoder, self).__init__() self.encoder = Encoder(hidden_dim) self.decoder = Decoder(hidden_dim) 256 Глава 7. Библиотека PyTorch def forward(self, x: Tensor) -> Tensor: assert_dim(x, 4) encoding = self.encoder(x) x = self.decoder(encoding) return x, encoding Метод forward в классе Autoencoder иллюстрирует вторую распространен- ную практику в PyTorch: поскольку в конечном итоге мы захотим увидеть скрытое представление, создаваемое моделью, метод forward возвращает два элемента: кодирование, а также вывод, который будет использоваться для обучения сети. Разумеется, придется видоизменить класс Trainer , чтобы приспособиться к этому методу. В частности, PyTorchModel в своем текущем виде выводит только один Tensor из своего метода forward . Как выясняется, несложно и полезно научить его выводить Tuple of Tensors по умолчанию, даже если этот Tuple имеет размер 1. Это позволит легко писать модели вроде Autoencoder . Нужно сделать всего три маленькие вещи. Во-первых, сделать сигнатуру функции метода forward нашего базового класса PyTorchModel : def forward(self, x: Tensor) -> Tuple[Tensor]: Затем в конце метода forward любой модели, которая наследуется от базового класса PyTorch Model , мы напишем return x вместо return x , как мы делали раньше. Во-вторых, изменим класс Trainer , чтобы он всегда брал в качестве ре- зультата первый элемент того, что возвращает модель: output = self.model(X_batch)[0] output = self.model(X_test)[0] Есть еще одна примечательная особенность модели Autoencoder : мы при- меняем функцию активации Tanh к последнему слою, что означает, что выходные данные модели будут лежать в диапазоне от –1 до 1. В любой модели выходные данные модели должны иметь тот же масштаб, что и целевые данные, а тут наши данные и являются целью. Таким образом, нужно масштабировать ввод в диапазоне от –1 до 1, как в следующем коде: |