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

Одним из замечательных результатов в области глубокого обучения стал метод, известный как трансферное обучение. Это ситуация, в которой использование сети, обученной для конкретной задачи, используется в качестве инициализации для другой (закрытой) проблемы. Это возможно, поскольку первые несколько уровней сети изучают общие функции, которые могут быть полезны в нескольких доменах, а прогнозирующая часть сети настроена для выполнения поставленной задачи. Одним из таких примеров может быть использование сети, способной распознавать виды деревьев (задача A) для задачи дифференциации другой растительности (задача B).

В общем, этот процесс требует 2 этапа:

0. Обучите / получите модель для задания A.

1. Измените структуру сети, чтобы она соответствовала задаче B.

2. Выполните точную настройку сети для выполнения задачи B.

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

Загрузить данные

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

from torchvision import transforms as T
from PIL import Image
import requests
import efemarai as ef
url= "https://data.efemarai.com/samples/dino.jpg"
img = Image.open(requests.get(url, stream=True).raw).convert('RGB')
ef.inspect(T.ToTensor()(img))

Шаг 0 - поиск предварительно обученной модели

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

import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)
densenet = models.densenet161(pretrained=True)

Давайте посмотрим на несколько моделей. Ниже мы можем увидеть структуру модели ResNet.

with ef.scan():
    output = resnet18(T.ToTensor()(img).unsqueeze())

Если мы посмотрим на карты функций, то увидим, что некоторые ядра нацелены на высокую частоту (или высокую точность / детализацию), а другие смотрят на общую структуру (низкую частоту).

Шаг 1

Давайте воспользуемся предварительно обученной моделью ImageNet и настроим ее для задачи CIFAR10.

На предыдущем изображении следует отметить выходной тензор. Он имеет размеры 1x1000. Это связано с тем, что задача ImageNet включала классификацию в пределах 1000 классов.

Если мы начнем обучение на настраиваемом наборе данных с другим количеством классов. Мы быстро обнаружим ошибку несоответствия между данными и структурой модели.

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

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

Щелкнув по блоку, мы увидим, что resnet18.fc на самом деле является слоем torch.nn.functional.linear. При дальнейшем расширении мы можем увидеть большие веса, связанные с линейным слоем (512 x 1000 float).

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

import torch
num_ftrs = resnet18.fc.in_features
resnet18.fc = torch.nn.Linear(num_ftrs, 10)

Ура! Теперь осталось обучить модель. Это происходит на шаге 2.

Шаг 2 - обучение модели

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

import efemarai as ef

import torch
import torchvision
import torchvision.transforms as transforms
from torch import nn
import torch.optim as optim

#
# Get data
#
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
    download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
    shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
    download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
    shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

#
# Learning process
#
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(resnet18.parameters(),lr=0.0001,momentum=0.9)

# Let’s freeze the training in all of the layers, but the FC one we initialized
for name, param in resnet18.named_parameters():
  if 'fc' not in name:
      param.requires_grad = False

for epoch in range(10):  # loop over the dataset 
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data
        # zero the parameter gradients
        optimizer.zero_grad()
        # Let's scan the computation
        with ef.scan(wait=False):
            # forward + backward + optimize
            outputs = resnet18(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

print('Finished Training')

Если мы переключимся на представление сети с градиентами, мы увидим, что мы генерируем значения градиента только для последнего слоя, так как все остальное имеет «requres_grad = False». Обратите внимание, что изменение параметров оптимизатора не означает, что другие градиенты не вычисляются, поэтому нам нужно явно отметить их.

Когда мы удовлетворены величиной градиентов (например, через 10–20 эпох), мы можем установить флаг для расчета градиентов по всем параметрам. Таким образом, уже настроенный последний слой будет в лучшем состоянии, чем при случайной инициализации. Если мы просканируем график еще раз, мы увидим, что есть градиенты, текущие к входу сети. Успех!

Распространенные ошибки

Распространенная ошибка - предоставление одного изображения для вывода модели. Сеть ожидает увидеть пакет, поэтому требуется дополнительное измерение. Посмотрите на разницу ниже.

from torchvision import transforms as T
from PIL import Image
import efemarai as ef

img = Image.open("dino.jpg")
ef.inspect(T.ToTensor()(img), name='Normal image')
ef.inspect(T.ToTensor()(img).unsqueeze(dim=0), name="Unsqueezed")

Заключение

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