В этой части блога мы сосредоточимся на обучении и оптимизации нашей нейронной сети. Первая часть блога посвящена основам реснетов, дополнению данных, нормализации данных и пакетной нормализации. В этом блоге мы рассмотрим следующие темы.

5. Планировщик скорости обучения

6. Распад веса

7. Оптимизатор Адама

8. Обучение модели

Мы рассмотрим каждый из них по мере их появления в нашем коде.

def accuracy(outputs, labels):
    _, preds = torch.max(outputs, dim=1)
    return torch.tensor(torch.sum(preds == labels).item() / len(preds))

class ImageClassificationBase(nn.Module):
    def training_step(self, batch):
        images, labels = batch 
        out = self(images)                  # Generate predictions
        loss = F.cross_entropy(out, labels) # Calculate loss
        return loss
    
    def validation_step(self, batch):
        images, labels = batch 
        out = self(images)                    # Generate predictions
        loss = F.cross_entropy(out, labels)   # Calculate loss
        acc = accuracy(out, labels)           # Calculate accuracy
        return {'val_loss': loss.detach(), 'val_acc': acc}
        
    def validation_epoch_end(self, outputs):
        batch_losses = [x['val_loss'] for x in outputs]
        epoch_loss = torch.stack(batch_losses).mean() # Combine losses
        batch_accs = [x['val_acc'] for x in outputs]
        epoch_acc = torch.stack(batch_accs).mean()  # Combine accuracies
        return {'val_loss': epoch_loss.item(), 'val_acc':   epoch_acc.item()}
    
    def epoch_end(self, epoch, result):
        print("Epoch [{}], last_lr: {:.5f}, train_loss: {:.4f},         val_loss: {:.4f}, val_acc: {:.4f}".format(
            epoch, result['lrs'][-1], result['train_loss'],   result['val_loss'], result['val_acc']))

Здесь мы определили точность функции. Каждое изображение проходит через модель, и вероятность для каждого класса является выходом. Функция возвращает метку с максимальной вероятностью. В последней строке функции мы проверяем, совпадают ли исходная метка и метка, предсказанная моделью. Если они равны, мы берем сумму таких экземпляров и делим ее на общее количество входных изображений, что дает точность модели.

Затем мы определили класс как классификацию изображений. Внутри этого класса у нас есть четыре функции. Первый этап – тренировочный. Эта функция пропускает пакет изображений через модель и вычисляет потери. Мы использовали перекрестную энтропию в качестве функции потерь для расчета потерь.

Вторая функция, которую мы определили, — это шаг проверки. В этой функции сгенерированная модель используется для прогнозирования класса изображений в наборе данных проверки. Эта функция возвращает точность набора проверки.

Функция окончания эпохи проверки просматривает пакеты данных и находит среднее значение для каждой эпохи. Это делается как для точности проверки, так и для потери проверки.

Конец эпохи конечной функции печатает потери и точность обучающего набора и проверочного набора данных.

def conv_block(in_channels, out_channels, pool=False):
 layers = [nn.Conv2d(in_channels, out_channels, kernel_size=3,           padding=1),nn.BatchNorm2d(out_channels),nn.ReLU(inplace=True)]
 if pool: layers.append(nn.MaxPool2d(2))
 return nn.Sequential(*layers)
class ResNet9(ImageClassificationBase):
 def __init__(self, in_channels, num_classes):
   super().__init__()
  self.conv1 = conv_block(in_channels, 64)
  self.conv2 = conv_block(64, 128, pool=True)
  self.res1 = nn.Sequential(conv_block(128, 128), conv_block(128,   128))
  self.conv3 = conv_block(128, 256, pool=True)
  self.conv4 = conv_block(256, 512, pool=True)
  self.res2 = nn.Sequential(conv_block(512, 512), conv_block(512, 512))
  self.classifier = nn.Sequential(nn.MaxPool2d(4),nn.Flatten(), nn.Dropout(0.2),nn.Linear(512, num_classes))
 def forward(self, xb):
  out = self.conv1(xb)
  out = self.conv2(out)
  out = self.res1(out) + out
  out = self.conv3(out)
  out = self.conv4(out)
  out = self.res2(out) + out
  out = self.classifier(out)
  return out

Следующим шагом для нас является определение блока свертки и формирование архитектуры Resnet 9. Прежде всего, мы определили здесь сверточный блок. Таким образом, входными данными для функции являются количество входных каналов и количество выходных каналов, а также требуется ли объединение. Слои были определены как последовательность операций, выполняемых в сверточном блоке. пп. последовательность используется здесь, чтобы получить последовательность операций, которые будут применяться к пакетам входных изображений. Таким образом, в nn.squential мы получаем набор модулей внутри конструктора слоев в определенной последовательности. Если требуется объединение, оно добавляется к последовательности операций слоев.

В архитектуре Resnet9 есть четыре блока сверточного слоя. Выход из первых двух блоков свертки добавляется к первому остаточному блоку. Аналогично для второго остаточного блока добавляются выходные данные третьего и четвертого сверточных блоков. Наконец, этот вывод подается в качестве входных данных для классификатора. Классификатор состоит из набора операций над этими входными данными и дает результат в виде количества классов в наших данных. Шаги, которые включены в этот классификатор, включают максимальное объединение, выравнивание данных (преобразование их в одномерный тензор), затем удаление и, наконец, линейное преобразование данных. Dropout используется для случайного обнуления некоторых элементов ввода, а значение 0,2 — это вероятность того, что элемент будет обнулен. Это помогает предотвратить переоснащение нашей модели, следовательно, помогает в регуляризации.

model = to_device(ResNet9(3, 10), device)
model

To device используется для передачи модели на GPU. Так что обучение модели происходит на графических процессорах.

@torch.no_grad()
def evaluate(model, val_loader):
  model.eval()
  outputs = [model.validation_step(batch) for batch in val_loader]
  return model.validation_epoch_end(outputs)
def get_lr(optimizer):
  for param_group in optimizer.param_groups:
  return param_group['lr']

def fit_one_cycle(epochs, max_lr, model, train_loader, val_loader,
weight_decay=0, grad_clip=None, opt_func=torch.optim.SGD):
  torch.cuda.empty_cache()
  history = []
  optimizer=opt_func(model.parameters(),max_lr,weight_decay = weight_decay)
  sched = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr, epochs=epochs,steps_per_epoch=len(train_loader))
for epoch in range(epochs):
   
  model.train()
  train_losses = []
  total_loss=[]
  lrs = []
  
  for batch in train_loader:
    
     total_loss = 0
     total_correct = 0
     loss = model.training_step(batch)
     train_losses.append(loss)
     loss.backward()
   
     if grad_clip:
           nn.utils.clip_grad_value_(model.parameters(), grad_clip)
      optimizer.step()
      optimizer.zero_grad()
  
      lrs.append(get_lr(optimizer))
   
       sched.step()
  for name, param in model.named_parameters():
          tb.add_histogram(name, param, epoch)
          tb.add_histogram(f'{name}.grad', param.grad, epoch)
         tb.add_scalar('Loss', total_loss, epoch)
         tb.add_scalar('Number Correct', total_correct, epoch)
         tb.add_scalar('Accuracy', total_correct / len(train_set), epoch)
# Validation phase
         result = evaluate(model, val_loader)
         result['train_loss'] = torch.stack (train_losses).mean()       .item()
         result['lrs'] = lrs
         model.epoch_end(epoch, result)
         history.append(result)
return history

Итак, у нас довольно много кода. Мы будем говорить об этом построчно. Итак, первая строка @torch.no_grad. Эта строка активирует расчеты автограда. это уменьшило использование памяти и увеличило скорость вычислений. Autograd — это движок дифференциации pytorch. Это имеет огромное значение в нейронных сетях, подобных нашей. Обратитесь к этой документации для получения более подробной информации: https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html#gradients. У нас есть функция, которая затем называется оценкой, которая принимает модель и набор данных проверки в качестве входных данных. На этапе проверки нам нужно отключить определенные функции, такие как нормализация пакетов, отсев и т. д. Таким образом, об этом позаботятся во время model.eval() . Обычно это работает в паре с torch.no_grad(), поэтому вычисления градиента временно приостанавливаются. Эта функция возвращает точность набора проверки и потери для всех пакетов в наборе проверки, вызывая функцию окончания эпохи проверки. Функция get_lr просто возвращает скорость обучения в функции оптимизатора.

Давайте посмотрим на функцию fit_one_cycle. Мы внесли много вкладов в эту функцию. Мы рассмотрим их один за другим. Во-первых, это количество эпох. Таким образом, эпоха определяется как количество раз, когда набор данных прошел через алгоритм машинного обучения. Модель уже указана. max_lr — это максимальная скорость обучения, которую может достичь оптимизатор. Здесь мы используем подход к тренировочному циклу. Скорость обучения проходит серию приращений, а затем снова уменьшается. Все это делается для того, чтобы найти оптимальную скорость обучения нейронных сетей. Если скорость обучения слишком низкая, нейронной сети потребуется много времени для достижения оптимальной скорости обучения. Если скорость обучения слишком высока, градиентный спуск может выйти за пределы минимума и не сойтись, а может расходиться. Итак, метод предложен Лесли Смитом в его статье Циклические скорости обучения для обучения нейронных сетей. В этой статье предлагается попробовать тест для нескольких эпох, в которых мы начинаем с некоторого низкого обучения и продолжаем увеличивать скорость обучения, пока потери не начнут резко расти. Изучается график зависимости скорости обучения от потерь, и строится сглаженный график зависимости потерь от скорости обучения. Скорость обучения на порядок меньше скорости обучения, при которой потери минимальны, и принимается за оптимальную скорость обучения. При этом значении потери все еще уменьшаются. В Дисциплинированном подходе к гиперпараметрам нейронной сети: Часть 1 — скорость обучения, размер партии, импульс и снижение веса в этой статье Лесли Смит предложила подход политики одного цикла для обучения нейронной сети. Максимальная скорость обучения выбирается на основе теста диапазона скорости обучения, выполненного ранее. Минимальная скорость обучения берется порядка 1/5 или 1/10 от максимальной скорости обучения. Здесь в одном циклическом тренировочном подходе делается два шага одинаковой длины. В самых последних циклах обучения, называемых фазой уничтожения, скорость обучения берется даже ниже самой низкой скорости обучения. Затем мы определили значение снижения веса. В функции потерь добавляются весовые термины, обеспечивающие некоторый вес к сумме квадратов весовых терминов. Это один из способов регуляризации, который, таким образом, помогает снизить сложность модели за счет штрафа за весовые коэффициенты. Параметр затухания веса (wd) управляет относительной важностью этого члена штрафа. Обычно значение затухания веса находится в диапазоне от 0 до 1.

Loss = MSE(y_hat, y) + wd * sum(w^2)

Отсечение градиента используется для решения проблемы взрывающихся градиентов. Взрывающиеся градиенты накапливаются во время обратного распространения и останавливают обучение нейронной сети. Это делает модель нестабильной и неспособной к обучению. Таким образом, принцип отсечения градиента очень прост. Значения за пределами определенного числа масштабируются, чтобы сделать его маленьким. Иногда возникает проблема исчезающего градиента, когда градиент принимает очень малые значения. Таким образом, в некоторых случаях с его помощью также можно указать диапазон значений градиента.

Также была определена функция оптимизации. Оптимизатор Адама — это используемый здесь алгоритм оптимизации. Алгоритмы оптимизации обновляют модель с учетом выходных данных функции потерь. Эти алгоритмы помогают минимизировать функцию потерь. Этот алгоритм представляет собой комбинацию градиентного спуска с импульсом и опорой RMS (среднеквадратичное распространение). В градиентном спуске с импульсом работает быстрее, чем в градиентном спуске, и раньше достигает сходимости. Градиентный спуск с импульсом мы берем скользящее среднее наших градиентов. Затем это используется для обновления весов нейронной сети. Поддержка RMS аналогична градиентному спуску с импульсом. В RMS prop квадрат градиента используется для обновления второго члена вектора моментов. RMS prop ограничивает движение алгоритма оптимизации в вертикальном направлении. Способ обновления градиентов — это разница между градиентным спуском с импульсом и опорой RMS. Оба эти подхода объединены в Adam Optimizer.

Мы объяснили каждую технику, используемую здесь. Давайте теперь сосредоточимся на оставшемся коде. Здесь была определена функция, а именно def_fit_one_cycle. Давайте разберемся, как работает эта функция. Мы хорошо знакомы со всеми входными данными, которые принимает эта функция. Итак, со следующей строки начинается наш цикл. model.train запускает обучение нашей модели. Затем мы определили все потери, точность, lrs (график скорости обучения) как пустые, так как мы будем записывать значения дальше. Затем потери в этом пакете данных рассчитываются с использованием функции training_step, определенной в классе ImageClassification. Эти значения потерь записываются путем добавления их к потерям. loss.backwards запускает расчет градиентов. Отсечение градиента выполняется на следующем шаге после проверки условий отсечения градиента. nn.utilis имеет функцию clip_grad_value, которая принимает параметры модели и условия отсечения градиента и выдает обрезанные градиенты в качестве вывода. Теперь рассчитанные веса обновляются в команде optimizer.step(). В следующей же строке мы очищаем все эти значения с помощью optimizer.zero_grad(). Для нашего исследования нам нужно знать скорость обучения на каждом этапе, поэтому они записываются lrs.append. Используя get_lr, мы получаем последнюю скорость обучения от оптимизатора и сохраняем ее. Следующая скорость обучения обновляется в соответствии с политикой одного цикла.

Следующие несколько строк до этапа проверки предназначены для любознательных умов. Мы использовали TensorBoard для визуализации потерь, точности и их гистограмм.

На этапе проверки снова все потери при проверке и соответствующая скорость обучения записываются в результаты, а затем распечатываются с использованием функции epoch_ends, определенной в классе ImageClassification. На последнем шаге все значения в результате добавляются в историю.

history = [evaluate(model, valid_dl)]
history

Теперь, прежде чем запускать предыдущую часть, мы должны оценить необработанную модель со случайными параметрами модели и проверить, насколько она верна изначально.

Теперь давайте обозначим все наши параметры.

epochs = 8
max_lr = 0.01
grad_clip = 0.1
weight_decay = 1e-4
opt_func = torch.optim.Adam

Теперь мы можем приступить к обучению нашей нейронной сети.

%%time
history += fit_one_cycle(epochs, max_lr, model, train_dl, valid_dl,
grad_clip=grad_clip,
weight_decay=weight_decay,
opt_func=opt_func)

В течение 7-8 эпох наша точность достигает порядка 90%. Нужно попробовать все возможные комбинации перестановок, варьируя максимальную скорость обучения, снижение веса, функции оптимизации и проверить точность и количество эпох для достижения желаемой точности.

Вы можете найти полный код на https://github.com/ketank26/Part-2-Cifar-10-classification-using-Convolutional-neural-network-using-Resnet9/blob/main/final_cnn_cifar10_resnet_reg_lec5_1.py

Особая благодарность за курс от Jovian.ml по лагерю свободного кода от Zero до GAN.

Использованная литература:

  1. https://medium.com/@nishantnikhil/adam-optimizer-notes-ddac4fd7218
  2. https://ruder.io/optimizing-gradient-descent/
  3. https://medium.com/analytics-vidhya/a-complete-guide-to-adam-and-rmsprop-optimizer-75f4502d83be
  4. https://towardsdatascience.com/finding-good-learning-rate-and-the-one-cycle-policy-7159fe1db5d6
  5. https://www.jeremyjordan.me/nn-learning-rate/
  6. https://towardsdatascience.com/what-is-gradient-clipping-b8e815cdfb48
  7. https://medium.com/optimization-algorithms-for-deep-neural-networks/gradient-descent-with-momentum-dce805cd8de8