Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
233 print(a.grad) tensor([[35., 35.], [35., 35.]], dtype=torch.float64) Эта особенность PyTorch позволяет нам определять модели, задавая прямой проход, вычисляя потерю и вызывая функцию .backward , чтобы автоматически вычислить производную каждого из параметров относи- тельно этой потери. В частности, не нужно думать о повторном использо- вании одного и того же количества в прямом проходе (что ранее не давала делать структура класса Operation , которую мы использовали в первых нескольких главах). Как показывает этот простой пример, градиенты сами начнут вычисляться правильно, как только мы вызовем результаты наших вычислений в обратном направлении. В следующих нескольких разделах мы покажем, как фреймворк обучения, который мы рассмотрели ранее в книге, реализуется с помощью типов данных PyTorch. Глубокое обучение с PyTorch Как мы уже видели, у моделей глубокого обучения есть несколько эле- ментов, которые совокупно создают обученную модель: y Model (модель), которая содержит Layers (слои). y Optimizer (оптимизатор). y Loss (потери). y Trainer (учитель). Оказывается, что Optimizer и Loss реализуются в PyTorch одной строкой кода, а с Model и Layer все чуть сложнее. Давайте рассмотрим каждый из этих элементов по очереди. Элементы PyTorch: Model, Layer, Optimizer и Loss Ключевой особенностью PyTorch является возможность определять моде- ли и слои как простые в использовании объекты, которые автоматически отправляют градиенты назад и сохраняют параметры, просто наследуя 234 Глава 7. Библиотека PyTorch их от класса torch.nn.Module . Позже в этой главе вы увидите, как эти части собираются вместе. Сейчас просто знайте, что слой PyTorchLayer записывается так: from torch import nn, Tensor class PyTorchLayer(nn.Module): def __init__(self) -> None: super().__init__() def forward(self, x: Tensor, inference: bool = False) -> Tensor: raise NotImplementedError() а PyTorchModel можно записать так: class PyTorchModel(nn.Module): def __init__(self) -> None: super().__init__() def forward(self, x: Tensor, inference: bool = False) -> Tensor: raise NotImplementedError() Иными словами, каждый подкласс PyTorchLayer или PyTorchModel просто должен реализовать методы __init__ и forward , что позволит нам исполь- зовать их интуитивно понятными способами 1 Флаг вывода Как мы видели в главе 4, из-за отсева данных нужна возможность под- страивать поведение нашей модели в зависимости от того, работает ли она в режиме обучения или в режиме вывода. В PyTorch мы можем пере- ключать модель или слой из режима обучения (поведение по умолчанию) в режим вывода, запустив функцию m.eval на модели или слое (любой 1 Написание слоев и моделей таким способом с использованием PyTorch не реко- мендуется и не применяется. Здесь мы пишем так только для целей иллюстрации понятий, которые рассмотрели до сих пор. Более распространенный способ постро- ения блоков нейронной сети с помощью PyTorch приведен во вводном руководстве из официальной документации. Глубокое обучение с PyTorch 235 объект, который наследуется от nn.Module ). Кроме того, в PyTorch есть элегантный способ быстро изменять поведение всех подклассов слоя с помощью функции apply . Если мы определим: def inference_mode(m: nn.Module): m.eval() тогда мы можем включить следующее: if inference: self.apply(inference_mode) в метод forward каждого подкласса PyTorchModel или PyTorchLayer , который мы определяем, получая желаемый флаг. Давайте посмотрим, как все это соединить. Реализация строительных блоков нейронной сети с использованием PyTorch: DenseLayer Теперь у нас есть все предпосылки для того, чтобы начать реализовывать слои, которые мы видели ранее, но с применением операций PyTorch. Слой DenseLayer описывается следующим образом: class DenseLayer(PyTorchLayer): def __init__(self, input_size: int, neurons: int, dropout: float = 1.0, activation: nn.Module = None) -> None: super().__init__() self.linear = nn.Linear(input_size, neurons) self.activation = activation if dropout < 1.0: self.dropout = nn.Dropout(1 — dropout) def forward(self, x: Tensor, inference: bool = False) -> Tensor: if inference: self.apply(inference_mode) x = self.linear(x) # does weight multiplication + bias 236 Глава 7. Библиотека PyTorch if self.activation: x = self.activation(x) if hasattr(self, "dropout"): x = self.dropout(x) return x С помощью функции nn Linear мы увидели наш первый пример операции PyTorch, которая автоматически выполняет обратное распространение. Этот объект не только реализует умножение веса и добавление члена смещения в прямом проходе, но также вызывает накопление градиен- тов x , поэтому производные потери по параметрам в обратном проходе вычисляются правильно. Также обратите внимание, что поскольку все операции PyTorch наследуются от nn.Module , мы можем вызывать их как математические функции: например, в предыдущем случае мы пи- шем self.linear (x) , а не self.lin ear.forward (x) . Это также относится и к самому DenseLayer , как мы увидим, когда будем использовать его в будущей модели. Пример: моделирование цен на жилье в Бостоне в PyTorch Используя этот слой в качестве строительного блока, мы можем реали- зовать уже знакомую модель цен на жилье, которую упоминали в главах 2 и 3. Напомним, что в этой модели был один скрытый слой с сигмовид- ной функцией активации. В главе 3 мы реализовали это в нашей объек- тно-ориентированной среде, в которой были класс для слоев и модель, а в качестве атрибута слоев был список длины 2. Точно так же мы можем определить класс HousePricesModel , который наследуется от PyTorchModel : class HousePricesModel(PyTorchModel): def __init__(self, hidden_size: int = 13, hidden_dropout: float = 1.0): super().__init__() self.dense1 = DenseLayer(13, hidden_size, activation=nn.Sigmoid(), dropout = hidden_dropout) self.dense2 = DenseLayer(hidden_size, 1) Глубокое обучение с PyTorch 237 def forward(self, x: Tensor) -> Tensor: assert_dim(x, 2) assert x.shape[1] == 13 x = self.dense1(x) return self.dense2(x) Теперь создаем экземпляр: pytorch_boston_model = HousePricesModel (hidden_size = 13) Обратите внимание, что в моделях PyTorch писать отдельный класс Layer не принято. Чаще всего просто определяют модели с точки зрения от- дельных выполняемых операций, используя что-то вроде такого: class HousePricesModel(PyTorchModel): def __init__(self, hidden_size: int = 13): super().__init__() self.fc1 = nn.Linear(13, hidden_size) self.fc2 = nn.Linear(hidden_size, 1) def forward(self, x: Tensor) -> Tensor: assert_dim(x, 2) assert x.shape[1] == 13 x = self.fc1(x) x = torch.sigmoid(x) return self.fc2(x) При создании своих моделей PyTorch вы, возможно, будете писать свой код именно так, а не создавать отдельный класс Layer . При чтении кода других пользователей вы почти всегда увидите что-то похожее на по- казанный выше код. Слои и модели реализуются сложнее, чем оптимизаторы и потери, о ко- торых мы расскажем далее. 238 Глава 7. Библиотека PyTorch Элементы PyTorch: Optimizer и Loss Оптимизаторы и потери реализованы в PyTorch одной строкой кода. На- пример, потеря SGDMomentum , которую мы рассмотрели в главе 4, пишется так: import torch.optim as optim optimizer = optim.SGD(pytorch_boston_model.parameters(), lr=0.001) В PyTorch модели передаются в оптимизатор в качестве аргумента. Та- кой способ гарантирует, что оптимизатор «указывает» на правильные параметры модели, поэтому знает, что обновлять на каждой итерации (ранее мы делали это с помощью класса Trainer ). Кроме того, среднеквадратическую потерю, которую мы видели в главе 2, и SoftmaxCrossEntropyLoss , которая обсуждалась в главе 4, тоже можно записать просто: mean_squared_error_loss = nn.MSELoss () softmax_cross_entropy_loss = nn.CrossEntropyLoss () Как и слои, они наследуются от nn.Module , поэтому их можно вызывать так же, как слои. Обратите внимание: в названии класса nn.CrossEntropyLoss отсут- ствует слово softmax , но сама операция softmax там выполняется, так что мы можем передавать «сырой» вывод нейронной сети, не реализуя softmax вручную. Этот вариант Loss наследуется от nn.Module , как и Layer из примера выше, поэтому их можно вызывать одинаково, например с помощью loss(x) вместо loss.forward (x) Элементы PyTorch: Trainer Trainer (учитель) объединяет все эти элементы. Какие к нему предъявляются требования? Мы знаем, что он должен реализовать общую модель обучения нейронных сетей, которая уже неоднократно встречалась в этой книге: Глубокое обучение с PyTorch 239 1) пакет данных передается через модель; 2) результаты и целевые значения передаются в функцию потерь, чтобы вычислить значение потерь; 3) вычисляется градиент потерь по всем параметрам; 4) оптимизатор обновляет параметры согласно некоторому правилу. В PyTorch все это работает точно так же, за исключением двух небольших моментов: y по умолчанию оптимизаторы сохраняют градиенты параметров ( param_ grads ) после каждой итерации обновления параметров. Чтобы очистить эти градиенты перед следующим обновлением параметра, нужно вы- зывать функцию self.optim.zero_grad ; y как было показано ранее в простом примере с автоматическим диф- ференцированием, чтобы начать обратное распространение, нужно вызвать функцию loss.backward после вычисления значения потерь. Таким образом мы получаем следующий код, который приводится в курсах по PyTorch и фактически будет использоваться в классе PyTorchTrainer Как и класс Trainer из предыдущих глав, PyTorchTrainer принимает на вход оптимизатор, PyTorchModel и потери (либо nn.MSELoss , либо nn.CrossEntropyLoss) для пакета данных (X_batch, y_batch) . Имея объ- екты как self.optim , self.model и self.loss , запускаем обучение модели следующим кодом: # Сначала обнуляем градиенты self.optim.zero_grad() # пропускаем X_batch через модель output = self.model(X_batch) # вычисляем потери loss = self.loss(output, y_batch) # выполняем обратное распространение на потерях loss.backward() # вызываем функцию self.optim.step() (как и раньше), чтобы обновить # параметры self.optim.step() 240 Глава 7. Библиотека PyTorch Это самые важные строки. Остальной код для PyTorch Trainer в основ- ном похож на код для Trainer , который мы видели в предыдущих главах: class PyTorchTrainer(object): def __init__(self, model: PyTorchModel, optim: Optimizer, criterion: _Loss): self.model = model self.optim = optim self.loss = criterion self._check_optim_net_aligned() def _check_optim_net_aligned(self): assert self.optim.param_groups[0]['params']\ == list(self.model.parameters()) def _generate_batches(self, X: Tensor, y: Tensor, size: int = 32) -> Tuple[Tensor]: N = X.shape[0] for ii in range(0, N, size): X_batch, y_batch = X[ii:ii+size], y[ii:ii+size] yield X_batch, y_batch def fit(self, X_train: Tensor, y_train: Tensor, X_test: Tensor, y_test: Tensor, epochs: int=100, eval_every: int=10, batch_size: int=32): for e in range(epochs): X_train, y_train = permute_data(X_train, y_train) batch_generator = self._generate_batches(X_train, y_train, batch_size) for ii, (X_batch, y_batch) in enumerate(batch_generator): Глубокое обучение с PyTorch 241 self.optim.zero_grad() output = self.model(X_batch) loss = self.loss(output, y_batch) loss.backward() self.optim.step() output = self.model(X_test) loss = self.loss(output, y_test) print(e, loss) Поскольку мы передаем Model , Optimizer и Loss в Trainer , то долж- ны проверить, что параметры, на которые ссылается Optimizer , фактически совпадают с параметрами модели. Это делает функция _check_optim_net_aligned Теперь обучить модель проще простого: net = HousePricesModel() optimizer = optim.SGD(net.parameters(), lr=0.001) criterion = nn.MSELoss() trainer = PyTorchTrainer(net, optimizer, criterion) trainer.fit(X_train, y_train, X_test, y_test, epochs=10, eval_every=1) Этот код практически идентичен коду, который мы использовали для обу чения моделей в структуре, созданной в первых трех главах. Неважно, что мы используем — PyTorch, TensorFlow или Theano, — основные шаги обучения модели глубокого обучения остаются неизменными! Далее мы рассмотрим более продвинутые возможности PyTorch и пока- жем пару трюков для улучшения обучения, которое мы видели в главе 4. Хитрости для оптимизации обучения в PyTorch Из главы 4 мы знаем четыре таких хитрости: y импульс; y дропаут; 242 Глава 7. Библиотека PyTorch y инициализация веса; y снижение скорости обучения. В PyTorch это все легко реализовать. Например, чтобы включить в оп- тимизатор импульс, достаточно лишь добавить соответствующее слово в SGD , получая: optim.SGD(model.parameters(), lr=0.01, momentum=0.9) Отсев данных тоже реализуется легко. В PyTorch есть встроенный мо- дуль nn.Linear (n_in, n_out) , который вычисляет операции плотного слоя. Аналогично модуль nn.Dropout (dropout_prob) реализует операцию Dropout , с той лишь разницей, что в аргумент передается вероятность ис- ключения (дропаута) нейрона, а не его сохранения, как это было в нашей реализации ранее. Об инициализации весов думать вообще не надо: в большинстве операций PyTorch с параметрами, включая nn.Linear , веса автоматически масшта- бируются в зависимости от размера слоя. Наконец, в PyTorch есть класс lr_scheduler , который можно использо- вать для снижения скорости обучения. Нужно выполнить импорт from torch.optim import lr_scheduler 1 . Теперь вы можете легко использовать эти методы в любом будущем проекте глубокого обучения, над которым вы работаете! Сверточные нейронные сети в PyTorch В главе 5 мы говорили о том, как работают сверточные нейронные сети, уделяя особое внимание операции многоканальной свертки. Мы видели, что операция преобразует пиксели входных изображений в слои нейронов, организованных в карты признаков, где каждый нейрон говорит, при- сутствует ли данный визуальный элемент (определенный в сверточном фильтре) в данном месте на изображении. Операция многоканальной свертки для двух входов и выходов имеет следующие формы: 1 В репозитории GitHub книги, oreil.ly/301qxRk , вы можете найти пример кода, кото- рый реализует экспоненциальное снижение скорости обучения как часть PyTorch Trainer. Документацию по используемому там классу ExponentialLR можно найти на веб-сайте PyTorch ( oreil.ly/2Mj9IhH ). Сверточные нейронные сети в PyTorch 243 y форма ввода данных [batch_size, in_channels, image_height, image_ width] ; y форма ввода параметров [in_channels, out_channels, filter_size, filter_size] ; y форма вывода [batch_size, out_channels, image_height, image_width] Тогда операция многоканальной свертки в PyTorch: nn.Conv2d (in_channels, out_channels, filter_size) С этим определением можно легко обернуть ConvLayer вокруг этой опе- рации: class ConvLayer(PyTorchLayer): def __init__(self, in_channels: int, out_channels: int, filter_size: int, activation: nn.Module = None, flatten: bool = False, dropout: float = 1.0) -> None: super().__init__() # основная операция слоя self.conv = nn.Conv2d(in_channels, out_channels, filter_size, padding=filter_size // 2) # те же операции "activation" и "flatten", что и ранее self.activation = activation self.flatten = flatten if dropout < 1.0: self.dropout = nn.Dropout(1 — dropout) def forward(self, x: Tensor) -> Tensor: # всегда выполняется операция свертки x = self.conv(x) # свертка выполняется опционально if self.activation: x = self.activation(x) 244 Глава 7. Библиотека PyTorch if self.flatten: x = x.view(x.shape[0], x.shape[1] * x.shape[2] * x.shape[3]) if hasattr(self, "dropout"): x = self.dropout(x) return x В главе 5 мы автоматически добавляли выходные данные в зависи- мости от размера фильтра, чтобы размер выходного изображения соответствовал размеру входного изображения. PyTorch этого не де- лает. Чтобы добиться того же поведения, что и раньше, мы добавляем аргумент в параметр операции nn.Conv2d padding = filter_size // 2 Теперь осталось лишь определить PyTorchModel с его операциями в функ- ции __init__ и последовательность операций в функции forward , после чего можно начать обучение. Далее следует простая архитектура, которую можно использовать в наборе данных MNIST , использующемся в главах 4 и 5. В ней есть: y сверточный слой, который преобразует входной сигнал из 1 «канала» в 16 каналов; y другой слой, который преобразует эти 16 каналов в 8 (каждый канал по-прежнему содержит 28 × 28 нейронов); y два полносвязных слоя. Схема нескольких сверточных слоев, за которыми следует меньшее коли- чество полносвязных слоев, довольно обычна для сверточных архитектур. Здесь используется каждый по две штуки: class MNIST_ConvNet(PyTorchModel): def __init__(self): super().__init__() self.conv1 = ConvLayer(1, 16, 5, activation=nn.Tanh(), dropout=0.8) self.conv2 = ConvLayer(16, 8, 5, activation=nn.Tanh(), flatten=True, dropout=0.8) self.dense1 = DenseLayer(28 * 28 * 8, 32, activation=nn.Tanh(), dropout=0.8) self.dense2 = DenseLayer(32, 10) Сверточные нейронные сети в PyTorch 245 def forward(self, x: Tensor) -> Tensor: assert_dim(x, 4) x = self.conv1(x) x = self.conv2(x) x = self.dense1(x) x = self.dense2(x) return x Далее обучим эту модель так же, как обучали HousePricesModel : model = MNIST_ConvNet () criterion = nn.CrossEntropyLoss () optimizer = optim.SGD (model.parameters (), lr = 0,01, momentum = 0,9) trainer = PyTorchTrainer (model, optimizer, criterion) trainer.fit (X_train, y_train, X_test, y_test, epochs = 5, eval_every = 1) В отношении класса nn.CrossEntropyLoss есть важный момент. Напомним, что в нашей структуре из предыдущих глав класс Loss ожидал ввод той же формы, что и целевые данные. Для этого мы закодировали 10 различных целевых значений в данных MNIST , чтобы у каждой партии данных цель имела форму [batch_size, 10] В классе PyTorch nn.CrossEntropyLoss , который работает точно так же, как и предыдущий SoftmaxCrossEntropyLoss , этого делать не нужно. Эта функция потерь ожидает два объекта Tensor : y Tensor предсказания размера [batch_size, num_classes] , похожий на наш класс Softmax CrossEntropyLoss y Целевой Tensor размера [batch_size] с различными значениями num_classes Таким образом, в предыдущем примере y_train — это массив размера [60000] (количество наблюдений в обучающем наборе MNIST) , а y_test имеет размер [10000] (количество наблюдений в тестовом наборе). |