Узнайте, как в этой статье были решены две самые большие проблемы глубокого обучения, а также написан код, чтобы увидеть его в действии.

В декабре 2015 года была опубликована статья, которая потрясла мир глубокого обучения.

Широко известная как одна из самых влиятельных статей в области современного глубокого обучения, она была процитирована более 110 000 раз. Название этой статьи войдет в анналы истории глубокого обучения: Deep Residual Learning for Image Recognition (также известная как статья ResNet).

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

Когда AlexNet появился на сцене в 2012 году, преобладающее мнение предполагало, что добавление большего количества слоев в нейронные сети приведет к лучшим результатам. Об этом свидетельствуют прорывы, исходящие от VGGNet, GoogleNet и других.

Это заставило сообщество глубокого обучения стремиться к более глубокому изучению.

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

Ошибка обучения будет увеличиваться по мере добавления слоев в уже глубокую сеть.

В первую очередь это было связано с двумя проблемами

1) Исчезающие/взрывающиеся градиенты

2) Проблема деградации

Исчезающие/взрывающиеся градиенты

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

Это приводит к тому, что более ранние уровни обновляются все меньше и меньше (обучения происходит немного).

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

Причина этой проблемы?

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

Но есть еще одна любопытная проблема…

Проблема деградации

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

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

Давайте представим, что у вас есть неглубокая сеть, которая работает хорошо.

Если вы возьмете «мелкую» сеть и просто наложите больше слоев, чтобы создать более глубокую сеть, производительность более глубокой сети должна быть, по крайней мере, такой же хорошей, как у мелкой сети. Почему? Потому что теоретически более глубокая сеть может изучить мелкую сеть. Неглубокая сеть является подмножеством более глубокой сети. Но на практике так не бывает!

Вы даже можете установить новые сложенные слои как слои идентичности, и все равно обнаружите, что ваша ошибка обучения усугубляется, когда вы накладываете больше слоев поверх более мелкой модели. Более глубокие сети приводили к более высокой ошибке обучения!

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

В документе ResNet было представлено новое решение этих двух надоедливых проблем, которые преследовали архитекторов глубоких нейронных сетей.

Пропустить соединение

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

Изобретение скипового соединения дало нам возможность создавать все более и более глубокие сети, избегая при этом проблемы исчезновения/взрыва градиентов и деградации.

Вот как это работает…

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

Затем X и Z будут складываться вместе, элемент за элементом, чтобы получить результат для следующего слоя/блока.

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

Присоединяйтесь к 18 000 своих коллег на Еженедельном глубоком обучении, чтобы узнавать о последних продуктах, приобретениях, технологиях, подробностях и многом другом.

Реснет в действии!

Теперь пришло время увидеть ResNet в действии.

Можно попробовать внедрить ResNet с нуля, обучить его на ImageNet и самому попытаться найти оптимальные параметры обучения… но зачем это делать, если можно использовать что-то из коробки? Вот что дает вам SuperGradients: предварительно обученную модель ResNet с надежным набором параметров обучения, которая готова к использованию с минимальной конфигурацией!

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

Вы можете следовать здесь или открыть этот блокнот в Google Colab, чтобы получить доступ к:



Установить зависимости

Вам нужно будет установить следующие зависимости.

!pip install super_gradients==3.0.0 gwpy &> /dev/null
!pip install matplotlib==3.1.3 &> /dev/null
!pip install torchinfo &> /dev/null

Импорт пакетов

Теперь вы можете импортировать все пакеты, необходимые для этого урока.

import os
import requests
import zipfile
import random
import numpy as np
import torchvision
import pprint
import torch
import pathlib

from matplotlib import pyplot as plt
from torchinfo import summary
from pathlib import Path, PurePath
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
from PIL import Image
from typing import List, Tuple
import super_gradients
from super_gradients.training import models
from super_gradients.training import dataloaders
from super_gradients.training import Trainer
from super_gradients.training import training_hyperparams

Скачать данные

Вы будете использовать набор данных use miniplaces. Подробнее о наборе данных можно узнать здесь.

torchvision.datasets.utils.download_and_extract_archive('https://dissect.csail.mit.edu/datasets/miniplaces.zip','datasets')

Конфигурации

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

class config:
    EXPERIMENT_NAME = 'resnet_in_action'
    MODEL_NAME = 'resnet50'
    CHECKPOINT_DIR = 'checkpoints'

    # specify the paths to training and validation set 
    TRAIN_DIR = 'datasets/miniplaces/train'
    VAL_DIR = 'datasets/miniplaces/val'

    # set the input height and width
    INPUT_HEIGHT = 224
    INPUT_WIDTH = 224

    # set the input height and width
    IMAGENET_MEAN = [0.485, 0.456, 0.406]
    IMAGENET_STD = [0.229, 0.224, 0.225]

    NUM_WORKERS = os.cpu_count()

    DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'

    FLIP_PROB = 0.25
    ROTATION_DEG = 15
    JITTER_PARAM = 0.25
    BATCH_SIZE = 64

Настройка эксперимента

При работе с SuperGradients первое, что вам нужно сделать, это инициализировать ваш тренажер.

тренер отвечает практически за все: от обучения, оценки, сохранения контрольных точек, построения графиков и т. д. Аргумент название эксперимента важен, так как все контрольные точки, журналы и тензорные доски будут сохранены. в каталоге с тем же именем.

Этот каталог будет создан как подкаталог ckpt_root_dir следующим образом:

ckpt_root_dir
|─── experiment_name_1
│       ckpt_best.pth                     # Model checkpoint on best epoch
│       ckpt_latest.pth                   # Model checkpoint on last epoch
│       average_model.pth                 # Model checkpoint averaged over epochs
│       events.out.tfevents.1659878383... # Tensorflow artifacts of a specific run
│       log_Aug07_11_52_48.txt            # Trainer logs of a specific run
└─── experiment_name_2
        ...
trainer = Trainer(experiment_name=config.EXPERIMENT_NAME, ckpt_root_dir=config.CHECKPOINT_DIR, device=config.DEVICE)

Создание загрузчиков данных

Тренажер SG совместим с загрузчиками данных PyTorch, поэтому вы можете использовать свои собственные загрузчики данных.

def create_dataloaders(
    train_dir: str, 
    val_dir: str, 
    train_transform: transforms.Compose,
    val_transform:  transforms.Compose,
    batch_size: int, 
    num_workers: int=config.NUM_WORKERS
):
  """Creates training and validation DataLoaders.
  Args:
    train_dir: Path to training data.
    val_dir: Path to validation data.
    transform: Transformation pipeline.
    batch_size: Number of samples per batch in each of the DataLoaders.
    num_workers: An integer for number of workers per DataLoader.
  Returns:
    A tuple of (train_dataloader, val_dataloader, class_names).
  """
  # Use ImageFolder to create dataset
  train_data = datasets.ImageFolder(train_dir, transform=train_transform)
  val_data = datasets.ImageFolder(val_dir, transform=val_transform)

  print(f"[INFO] training dataset contains {len(train_data)} samples...")
  print(f"[INFO] validation dataset contains {len(val_data)} samples...")

  # Get class names
  class_names = train_data.classes
  print(f"[INFO] dataset contains {len(class_names)} labels...")

  # Turn images into data loaders
  print("[INFO] creating training and validation set dataloaders...")
  train_dataloader = DataLoader(
      train_data,
      batch_size=batch_size,
      shuffle=True,
      num_workers=num_workers,
      pin_memory=True,
  )
  val_dataloader = DataLoader(
      val_data,
      batch_size=batch_size,
      shuffle=False,
      num_workers=num_workers,
      pin_memory=True,
  )

  return train_dataloader, val_dataloader, class_names

Превращает

Этот следующий блок кода создаст конвейер преобразования как для обучения, так и для проверки.

# initialize our data augmentation functions
resize = transforms.Resize(size=(config.INPUT_HEIGHT,config.INPUT_WIDTH))

horizontal_flip = transforms.RandomHorizontalFlip(p=config.FLIP_PROB)

vertical_flip = transforms.RandomVerticalFlip(p=config.FLIP_PROB)

rotate = transforms.RandomRotation(degrees=config.ROTATION_DEG)

norm = transforms.Normalize(mean=config.IMAGENET_MEAN, std=config.IMAGENET_STD)

make_tensor = transforms.ToTensor()

# initialize our training and validation set data augmentation pipeline
train_transforms = transforms.Compose([resize, horizontal_flip, vertical_flip, rotate, make_tensor, norm])
val_transforms = transforms.Compose([resize, make_tensor, norm])

Создание экземпляров загрузчиков данных

Теперь вы можете создавать экземпляры ваших загрузчиков данных.

train_dataloader, valid_dataloader, class_names = create_dataloaders(train_dir=config.TRAIN_DIR,
                                                                     val_dir=config.VAL_DIR,
                                                                     train_transform=train_transforms,
                                                                     val_transform=val_transforms,
                                                                     batch_size=config.BATCH_SIZE)

NUM_CLASSES = len(class_names)

Некоторые эвристики для трансферного обучения

1) Ваш набор данных небольшой и похож на набор данных, на котором была предварительно обучена модель.

Когда ваши изображения похожи, вероятно, функции низкого уровня (например, края) и функции высокого уровня (например, формы) будут похожими.

Что делать: заморозить веса до последнего слоя, заменить полносвязный слой и переобучить. Почему? Меньше данных означает, что вы можете переобучиться, если будете обучать всю сеть.

Comet Artifacts позволяет отслеживать и воспроизводить сложные сценарии с несколькими экспериментами, повторно использовать точки данных и легко повторять наборы данных. Прочитайте этот краткий обзор Артефактов, чтобы изучить все, на что они способны.

2) Ваш набор данных большой и похож на набор данных, на котором была предварительно обучена модель.

Что делать: заморозить вес предыдущего слоя. Затем сохраните более поздние веса с новым полносвязным слоем.

3) Ваш набор данных мал и отличается от набора данных, на котором была предварительно обучена модель.

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

Что делать: удалить полностью связанные слои и сверточные слои ближе к выходным данным. Переобучите сверточные слои ближе к входным данным.

4) Ваш набор данных большой и отличается от набора данных, на котором была предварительно обучена модель.

Вы должны создать экземпляр предварительно обученных весов модели, чтобы ускорить обучение (много низкоуровневых сверток будут иметь одинаковые веса).

Что делать: переобучить всю сеть с нуля, обязательно заменив полностью подключенный выходной слой.

Чтобы обучить ResNet с нуля в SG, все, что вам нужно сделать, это опустить аргумент pretrained_weights="imagenet" из метода models.get.

Для этого примера мы выберем вариант 2.

Создайте свою модель ResNet

Использование SuperGradients упрощает изменение заголовка классификации вашей модели. Все, что вам нужно сделать, это передать количество классов для вашего варианта использования в аргумент num_classes, и заголовок классификации будет автоматически изменен для вас.

resnet50_imagenet_model = models.get(model_name=config.MODEL_NAME, num_classes=NUM_CLASSES, pretrained_weights="imagenet")

Этот следующий блок кода заморозит ранние слои и слои пакетной нормы созданной модели.

for param in resnet50_imagenet_model.conv1.parameters():
    param.requires_grad = False

for param in resnet50_imagenet_model.bn1.parameters():
    param.requires_grad = False

for param in resnet50_imagenet_model.layer1.parameters():
    param.requires_grad = False

for param in resnet50_imagenet_model.layer2.parameters():
    param.requires_grad = False

Настройка обучения

Параметры обучения в SuperGradients были оптимизированы для каждого набора данных и архитектуры с учетом используемого типа обучения (с нуля\обучение с переносом).

Дополнительные рекомендуемые параметры тренировки вы можете посмотреть в наших рецептах здесь.

В этом примере вам не нужно настраивать гиперпараметры, но вы можете поиграть с некоторыми из них!

training_params =  training_hyperparams.get("training_hyperparams/imagenet_resnet50_train_params")

#overriding the number of epochs to train for
training_params["max_epochs"] = 3

Обучение и оценка

Результаты эпох обучения сохраняются в вашем заданном пути CKPT. Давайте продолжим и обучим модель.

Вы заметите несколько показателей, отображаемых для каждой эпохи:

  1. Accuracy
  2. LabelSmoothingCrossEntropyLoss
  3. Top5

Давайте определим два, с которыми вы, возможно, не знакомы.

LabelSmoothingCrossEntropyLoss.

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

Топ-5

Точность Top-5 означает, что один из пяти прогнозов модели с самой высокой вероятностью соответствует действительности. Если это так, вы считаете это правильным прогнозом.

Чтобы обучить вашу модель, все, что вам нужно сделать, это запустить следующий код:

trainer.train(model=resnet50_imagenet_model, 
              training_params=training_params, 
              train_loader=train_dataloader,
              valid_loader=valid_dataloader)

# Load the best model that we trained
best_model = models.get(config.MODEL_NAME,
                        num_classes=NUM_CLASSES,
                        checkpoint_path=os.path.join(trainer.checkpoints_dir_path,"ckpt_best.pth"))

Давайте посмотрим, насколько хорошо наша модель предсказывает проверочные данные.

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

# 1. Take in a trained model, class names, image path, image size, a transform and target device
def pred_and_plot_image(model: torch.nn.Module,
                        image_path: str, 
                        class_names: List[str],
                        image_size: Tuple[int, int] = (config.INPUT_HEIGHT, config.INPUT_WIDTH),
                        transform: torchvision.transforms = None,
                        device: torch.device=config.DEVICE):
    
    
    # 2. Open image
    if isinstance(image_path, pathlib.PosixPath):
      img = Image.open(image_path)
    else: 
      img = Image.open(requests.get(image_path, stream=True).raw)

    # 3. Create transformation for image (if one doesn't exist)
    if transform is not None:
        image_transform = transform
    else:
        image_transform = transforms.Compose([
            transforms.Resize(image_size),
            transforms.ToTensor(),
            transforms.Normalize(mean=config.IMAGENET_MEAN,
                                 std=config.IMAGENET_STD),
        ])

    ### Predict on image ### 

    # 4. Make sure the model is on the target device
    model.to(device)

    # 5. Turn on model evaluation mode and inference mode
    model.eval()
    with torch.inference_mode():
      # 6. Transform and add an extra dimension to image (model requires samples in [batch_size, color_channels, height, width])
      transformed_image = image_transform(img).unsqueeze(dim=0)

      # 7. Make a prediction on image with an extra dimension and send it to the target device
      target_image_pred = model(transformed_image.to(device))

    # 8. Convert logits -> prediction probabilities (using torch.softmax() for multi-class classification)
    target_image_pred_probs = torch.softmax(target_image_pred, dim=1)

    # 9. Convert prediction probabilities -> prediction labels
    target_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)

    #actual label
    ground_truth = PurePath(image_path).parent.name

    # 10. Plot image with predicted label and probability 
    plt.figure()
    plt.imshow(img)
    if isinstance(image_path, pathlib.PosixPath):
      plt.title(f"Ground Truth: {ground_truth} | Pred: {class_names[target_image_pred_label]} | Prob: {target_image_pred_probs.max():.3f}")
    else:
      plt.title(f"Pred: {class_names[target_image_pred_label]} | Prob: {target_image_pred_probs.max():.3f}")
    plt.axis(False);

# Get a random list of image paths from test set
num_images_to_plot = 30
test_image_path_list = list(Path(config.VAL_DIR).glob("*/*.jpg")) # get list all image paths from test data 
test_image_path_sample = random.sample(population=test_image_path_list, # go through all of the test image paths
                                       k=num_images_to_plot) # randomly select 'k' image paths to pred and plot

# Make predictions on and plot the images
for image_path in test_image_path_sample:
    pred_and_plot_image(model=best_model, 
                        image_path=image_path,
                        class_names=class_names,
                        image_size=(config.INPUT_HEIGHT, config.INPUT_WIDTH))

Теперь, когда обучение завершено, вы можете использовать обученную модель для предсказания невидимого изображения!

Давайте посмотрим, какое место, по мнению модели, это изображение:

pred_and_plot_image(model=best_model, 
                    image_path="https://cdn.pastemagazine.com/www/articles/2021/05/18/the-office-NEW.jpg",
                    class_names=train_dataloader.dataset.classes,
                    image_size=(config.INPUT_HEIGHT, config.INPUT_WIDTH))

Если вам понравился этот урок и вы считаете, что SuperGradients — это классный инструмент для использования, подумайте о том, чтобы поставить ему звезду на GitHub. Ищете сообщество практиков глубокого обучения? Тогда общайтесь в сообществе Deep Learning Daily, куда приходят практики глубокого обучения, чтобы освоить новые навыки и решить свои самые сложные проблемы.

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

Независимая от редакции, Heartbeat спонсируется и публикуется Comet, платформой MLOps, которая позволяет специалистам по данным и командам машинного обучения отслеживать, сравнивать, объяснять и оптимизировать свои эксперименты. Мы платим нашим авторам и не продаем рекламу.

Если вы хотите внести свой вклад, перейдите к нашему призыву к участию. Вы также можете подписаться на получение нашего еженедельного информационного бюллетеня (Еженедельник глубокого обучения), заглянуть в блог Comet, присоединиться к нам в Slack и подписаться на Comet в Twitter и LinkedIn для получения ресурсов и событий. и многое другое, что поможет вам быстрее создавать более качественные модели машинного обучения.