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

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

Извлечение признаков

В случае изображений ядро ​​весов применяется к изображению с помощью процесса, известного как свертка, а результат свертки известен как активация или карта признаков. Сеть, в которой используются слои свертки, называется сверточной нейронной сетью (CNN).

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

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

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

Трансферное обучение

Современные нейронные сети, такие как VGG, предварительно обучаются исследователями на больших общих наборах данных, таких как ImageNet, содержащих более миллиона изображений из 1000 классов. Поскольку наборы данных, на которых они обучаются, очень большие и общие, их можно использовать для извлечения признаков из большинства изображений, даже из тех, которых нет в наборе данных. Следовательно, эти сети можно использовать для разных задач на совершенно разных наборах данных.

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

Для этой реализации мы будем использовать набор данных CIFAR-10. Он имеет 10 классов изображений и 60 000 изображений. Код Python для этого можно увидеть ниже.

import torch
from torch import nn, optim 
from torch.utils.data import DataLoader 
from torchvision.models import vgg16
from torchvision import datasets, transforms 
from tqdm import tqdm

BATCH_SIZE = 32
IMG_SIZE = 224 # input image size for VGG-16

transform = transforms.Compose([
    transforms.Resize(IMG_SIZE),
    transforms.ToTensor(),
])
data = datasets.CIFAR10(root = '~/Desktop/data', train=True, download=True, transform=transform)
loader = DataLoader(dataset=data, batch_size=BATCH_SIZE, shuffle=True) # train loader

test = datasets.CIFAR10(root = '~/Desktop/data', train=False, download=True, transform=transform)
test_loader = DataLoader(dataset=test, batch_size=BATCH_SIZE, shuffle=True)

dev = torch.device('mps') # "cpu" can also be used

Теперь загрузим саму предварительно обученную модель.

model = vgg16(pretrained=True).to(dev) # loading pre-trained model
print(model)

Оператор печати печатает слои модели, включая такие детали, как размеры ядра, каналы и шаг. Это позволяет нам получить представление о структуре модели.

VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

Во-первых, нам нужно заморозить параметры в этой модели, чтобы предотвратить их обновление во время обучения.

for parameter in model.parameters():
    parameter.requires_grad = False # Freezes the layer

Теперь нам нужно заменить последний слой. Как видно из вывода структуры модели, последним слоем является classifier[6], который мы должны заменить нашим собственным слоем. Наш слой будет иметь то же количество входных признаков, так как входные данные получены от классификатора [5], который остается неизменным. Нашими выходными функциями будет количество классов в нашем наборе данных, равное 10.

model.classifier[6] = nn.Linear(in_features=4096, out_features=10).to(dev) # replacing output layer

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

LR = 0.001
EPOCHS = 5

crit = nn.CrossEntropyLoss()
opt = optim.Adam(params=parameters_to_update, lr=LR)
# the params passed to the optimizer are only the ones that need to be updated


ALL_LOSS = []

for e in range(EPOCHS):

    loop = tqdm(enumerate(loader), total=len(loader), leave=False, position=0)

    # training
    model.train()
    for batch_id, (x, y) in loop : 

        x, y = x.to(dev), y.to(dev)
        opt.zero_grad() 
        yhat = model(x)
        loss = crit(yhat, y)
        ALL_LOSS.append(loss.item())
        loss.backward()
        opt.step()

        loop.set_description(f'Epoch : [{e}/{EPOCHS}]')
        loop.set_postfix(loss=loss.item())


    loop = tqdm(enumerate(test_loader), total=len(test_loader), leave=False, position=0)

    # testing
    model.eval()
    correct = 0

    for batch_id, (x, y) in loop :    
        x, y = x.to(dev), y.to(dev) 
        yhat = model(x).argmax(dim=1)
        correct += torch.sum(yhat == y).item()
    acc = correct / len(test)
    print(f"Accuracy : {acc}")

За 5 эпох обучения мы получаем следующие результаты.

Accuracy : 0.6932                                                                  
Accuracy : 0.7086                                                                    
Accuracy : 0.7096                                                                    
Accuracy : 0.7089
Accuracy : 0.7224

Всего за 5 эпох обучения мы смогли получить 72% точности! Это ни в коем случае не впечатляет по сравнению с производительностью современных моделей, но это невероятно хорошо, учитывая, как мало времени мы потратили на обучение и настройку гиперпараметров!