Предыдущий ‹‹ Введение в компьютерное зрение с помощью PyTorch (4/6)
Предварительно обученные модели и трансферное обучение
Обучение CNN может занять много времени, и для этой задачи требуется много данных. Однако большая часть времени тратится на изучение лучших низкоуровневых фильтров, которые сеть использует для извлечения шаблонов из изображений. Возникает закономерный вопрос — можем ли мы использовать нейронную сеть, обученную на одном наборе данных, и адаптировать ее для классификации разных изображений без полноценного процесса обучения?
Этот подход называется переносом обучения, поскольку мы переносим некоторые знания из одной модели нейронной сети в другую. При трансферном обучении мы обычно начинаем с предварительно обученной модели, которая была обучена на каком-то большом наборе данных изображений, например ImageNet. Эти модели уже могут хорошо справляться с извлечением различных признаков из общих изображений, и во многих случаях простое построение классификатора поверх этих извлеченных признаков может дать хороший результат.
!wget https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py !pip install -r https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/requirements.txt import torch import torch.nn as nn import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt from torchinfo import summary import numpy as np import os from pytorchcv import train, plot_results, display_dataset, train_long, check_image_dir
Изучите набор данных «Кошки против собак»
В этом разделе мы решим реальную задачу классификации изображений кошек и собак. По этой причине мы будем использовать Набор данных Kaggle Cats vs. Dogs. Давайте загрузим этот набор данных и распакуем его в каталог данных:
if not os.path.exists('data/kagglecatsanddogs_5340.zip'): !wget -P data -q https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip import zipfile if not os.path.exists('data/PetImages'): with zipfile.ZipFile('data/kagglecatsanddogs_5340.zip', 'r') as zip_ref: zip_ref.extractall('data')
К сожалению, в наборе данных есть несколько поврежденных файлов изображений. Нам нужно выполнить быструю очистку, чтобы проверить наличие поврежденных файлов. Чтобы не засорять этот блокнот, мы переместили код проверки набора данных в модуль и будем просто вызывать его здесь. check_image_dir просматривает весь набор данных изображение за изображением, пытается загрузить изображение и проверить, можно ли его загрузить правильно. Все поврежденные изображения будут удалены.
check_image_dir('data/PetImages/Cat/*.jpg') check_image_dir('data/PetImages/Dog/*.jpg') Corrupt image: data/PetImages/Cat/666.jpg Corrupt image: data/PetImages/Dog/11702.jpg /anaconda/envs/py38_default/lib/python3.8/site-packages/PIL/TiffImagePlugin.py:845: UserWarning: Truncated File Read warnings.warn(str(msg))
Далее давайте загрузим изображения в набор данных PyTorch, преобразовав их в тензоры и выполнив некоторую нормализацию. Мы определяем конвейер преобразования изображений, составив несколько примитивных преобразований с помощью Compose:
- Изменить размер изменяет размер нашего изображения до 256 × 256.
- CenterCrop получает центральную часть изображения размером 224 × 224. Предварительно обученная сеть VGG была обучена на изображениях 224 × 224, поэтому нам необходимо довести наш набор данных до этого размера.
- ToTensor нормализует интенсивность пикселей в диапазоне 0–1 и преобразует изображения в тензоры PyTorch.
- Преобразование std_normalize — это дополнительный шаг нормализации, специфичный для сети VGG. При обучении сети VGG исходные изображения из ImageNet были преобразованы путем вычитания средней интенсивности набора данных по цвету и деления на стандартное отклонение (также по цвету). Таким образом, нам нужно применить одно и то же преобразование к нашему набору данных, чтобы все изображения обрабатывались правильно.
Есть несколько причин, по которым мы изменили размер изображений до размера 256, а затем обрезали их до 224 пикселей:
- Мы хотели продемонстрировать больше возможных трансформаций.
- Домашние животные обычно находятся где-то в центральной части изображения, поэтому мы можем улучшить классификацию, уделив больше внимания центральной части.
- Поскольку некоторые изображения не являются квадратными, в конечном итоге мы получаем дополненные части изображения, которые не содержат никаких полезных данных изображения, а при небольшой обрезке изображения заполняемая часть уменьшается.
std_normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) trans = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), std_normalize]) dataset = torchvision.datasets.ImageFolder('data/PetImages',transform=trans) trainset, testset = torch.utils.data.random_split(dataset,[20000,len(dataset)-20000]) display_dataset(dataset)
Предварительно обученные модели
В модуле torchvision доступно множество различных предварительно обученных моделей, и еще больше моделей можно найти в Интернете. Давайте посмотрим, как можно загрузить и использовать простейшую модель VGG-16. Сначала мы загрузим веса для модели VGG-16, которые мы сохранили в локальном репозитории.
# Download model weights in the sandbox environment !mkdir -p models !wget -P models https://github.com/MicrosoftDocs/pytorchfundamentals/raw/main/computer-vision-pytorch/vgg16-397923af.pth
Далее мы загрузим веса в предварительно обученную модель VGG-16 с помощью метода load_state_dict. Затем используйте метод eval, чтобы перевести модель в режим вывода.
file_path = 'models/vgg16-397923af.pth' vgg = torchvision.models.vgg16() vgg.load_state_dict(torch.load(file_path)) vgg.eval() sample_image = dataset[0][0].unsqueeze(0) res = vgg(sample_image) print(res[0].argmax()) tensor(282)
Результат, который мы получили — это номер класса ImageNet, который можно посмотреть здесь. Мы можем использовать следующий код для автоматической загрузки этой таблицы классов и возврата результата:
import json, requests class_map = json.loads(requests.get("https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/imagenet_class_index.json").text) class_map = { int(k) : v for k,v in class_map.items() } class_map[res[0].argmax().item()] ['n02123159', 'tiger_cat']
Давайте также посмотрим архитектуру сети VGG-16:
========================================================================================== Layer (type:depth-idx) Output Shape Param # ========================================================================================== VGG [1, 1000] -- ├─Sequential: 1-1 [1, 512, 7, 7] -- │ └─Conv2d: 2-1 [1, 64, 224, 224] 1,792 │ └─ReLU: 2-2 [1, 64, 224, 224] -- │ └─Conv2d: 2-3 [1, 64, 224, 224] 36,928 │ └─ReLU: 2-4 [1, 64, 224, 224] -- │ └─MaxPool2d: 2-5 [1, 64, 112, 112] -- │ └─Conv2d: 2-6 [1, 128, 112, 112] 73,856 │ └─ReLU: 2-7 [1, 128, 112, 112] -- │ └─Conv2d: 2-8 [1, 128, 112, 112] 147,584 │ └─ReLU: 2-9 [1, 128, 112, 112] -- │ └─MaxPool2d: 2-10 [1, 128, 56, 56] -- │ └─Conv2d: 2-11 [1, 256, 56, 56] 295,168 │ └─ReLU: 2-12 [1, 256, 56, 56] -- │ └─Conv2d: 2-13 [1, 256, 56, 56] 590,080 │ └─ReLU: 2-14 [1, 256, 56, 56] -- │ └─Conv2d: 2-15 [1, 256, 56, 56] 590,080 │ └─ReLU: 2-16 [1, 256, 56, 56] -- │ └─MaxPool2d: 2-17 [1, 256, 28, 28] -- │ └─Conv2d: 2-18 [1, 512, 28, 28] 1,180,160 │ └─ReLU: 2-19 [1, 512, 28, 28] -- │ └─Conv2d: 2-20 [1, 512, 28, 28] 2,359,808 │ └─ReLU: 2-21 [1, 512, 28, 28] -- │ └─Conv2d: 2-22 [1, 512, 28, 28] 2,359,808 │ └─ReLU: 2-23 [1, 512, 28, 28] -- │ └─MaxPool2d: 2-24 [1, 512, 14, 14] -- │ └─Conv2d: 2-25 [1, 512, 14, 14] 2,359,808 │ └─ReLU: 2-26 [1, 512, 14, 14] -- │ └─Conv2d: 2-27 [1, 512, 14, 14] 2,359,808 │ └─ReLU: 2-28 [1, 512, 14, 14] -- │ └─Conv2d: 2-29 [1, 512, 14, 14] 2,359,808 │ └─ReLU: 2-30 [1, 512, 14, 14] -- │ └─MaxPool2d: 2-31 [1, 512, 7, 7] -- ├─AdaptiveAvgPool2d: 1-2 [1, 512, 7, 7] -- ├─Sequential: 1-3 [1, 1000] -- │ └─Linear: 2-32 [1, 4096] 102,764,544 │ └─ReLU: 2-33 [1, 4096] -- │ └─Dropout: 2-34 [1, 4096] -- │ └─Linear: 2-35 [1, 4096] 16,781,312 │ └─ReLU: 2-36 [1, 4096] -- │ └─Dropout: 2-37 [1, 4096] -- │ └─Linear: 2-38 [1, 1000] 4,097,000 ========================================================================================== Total params: 138,357,544 Trainable params: 138,357,544 Non-trainable params: 0 Total mult-adds (G): 15.48 ========================================================================================== Input size (MB): 0.60 Forward/backward pass size (MB): 108.45 Params size (MB): 553.43 Estimated Total Size (MB): 662.49 ==========================================================================================
Помимо уже известного нам слоя, существует еще один тип слоя, который называется Dropout. Эти слои действуют как метод регуляризации. Регуляризация вносит небольшие изменения в алгоритм обучения, поэтому модель лучше обобщается. Во время обучения выпадающие слои отбрасывают некоторую часть (около 30%) нейронов предыдущего слоя, и обучение происходит без них. Это помогает вывести процесс оптимизации из локальных минимумов и распределить решающую власть между различными нейронными путями, что повышает общую стабильность сети.
Вычисления на графическом процессоре
Глубокие нейронные сети, такие как VGG-16 и другие более современные архитектуры, требуют для работы довольно большой вычислительной мощности. Имеет смысл использовать ускорение GPU, если оно доступно. Для этого нам нужно явно переместить все тензоры, участвующие в вычислениях, на графический процессор.
Обычно это делается так: проверяется наличие графического процессора в коде и определяется переменная устройства, указывающая на вычислительное устройство — графический процессор или процессор.
device = 'cuda' if torch.cuda.is_available() else 'cpu' print('Doing computations on device = {}'.format(device)) vgg.to(device) sample_image = sample_image.to(device) vgg(sample_image).argmax()
Извлечение функций VGG
Если мы хотим использовать VGG-16 для извлечения признаков из наших изображений, нам нужна модель без окончательных слоев классификации. Фактически, этот «экстрактор функций» можно получить с помощью метода vgg.features:
res = vgg.features(sample_image).cpu() plt.figure(figsize=(15,3)) plt.imshow(res.detach().view(-1,512)) print(res.size()) torch.Size([1, 512, 7, 7])
Размер тензора признаков составляет 512 × 7 × 7, но для его визуализации нам пришлось преобразовать его в двумерную форму.
Теперь давайте попробуем посмотреть, можно ли использовать эти функции для классификации изображений.
Давайте вручную возьмем некоторую часть изображений (в нашем случае 800) и предварительно рассчитаем их векторы признаков. Мы сохраним результат в одном большом тензоре под названием feature_tensor, а также метки в label_tensor:
bs = 8 dl = torch.utils.data.DataLoader(dataset,batch_size=bs,shuffle=True) num = bs*100 feature_tensor = torch.zeros(num,512*7*7).to(device) label_tensor = torch.zeros(num).to(device) i = 0 for x,l in dl: with torch.no_grad(): f = vgg.features(x.to(device)) feature_tensor[i:i+bs] = f.view(bs,-1) label_tensor[i:i+bs] = l i+=bs print('.',end='') if i>=num: break
Теперь мы можем определить vgg_dataset, который берет данные из этого тензора, разделяет их на обучающий и тестовый наборы с помощью функции random_split и обучает небольшую однослойную плотную сеть классификаторов поверх извлеченные функции:
vgg_dataset = torch.utils.data.TensorDataset(feature_tensor,label_tensor.to(torch.long)) train_ds, test_ds = torch.utils.data.random_split(vgg_dataset,[700,100]) train_loader = torch.utils.data.DataLoader(train_ds,batch_size=32) test_loader = torch.utils.data.DataLoader(test_ds,batch_size=32) net = torch.nn.Sequential(torch.nn.Linear(512*7*7,2),torch.nn.LogSoftmax()).to(device) history = train(net,train_loader,test_loader) Epoch 0, Train acc=0.899, Val acc=0.960, Train loss=0.099, Val loss=0.050 Epoch 1, Train acc=0.989, Val acc=0.970, Train loss=0.016, Val loss=0.037 Epoch 2, Train acc=0.996, Val acc=0.970, Train loss=0.003, Val loss=0.155 Epoch 3, Train acc=0.999, Val acc=0.960, Train loss=0.001, Val loss=0.024 Epoch 4, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026 Epoch 5, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026 Epoch 6, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026 Epoch 7, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026 Epoch 8, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026 Epoch 9, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
Результат отличный, кошку от собаки различим почти с 98% вероятностью! Однако мы протестировали этот подход только на небольшом подмножестве всех изображений, поскольку ручное извлечение признаков занимает много времени.
Передача обучения с использованием сети VGG
Мы также можем избежать предварительного вычисления функций вручную, используя исходную сеть VGG-16 целиком во время обучения. Давайте посмотрим на структуру объекта VGG-16:
print(vgg) 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) ) )
Вы можете видеть, что сеть содержит:
- экстрактор функций (функции), состоящий из нескольких сверточных слоев и слоев объединения.
- средний уровень пула (avgpool)
- финальный классификатор (fc), состоящий из нескольких плотных слоев, который превращает 25088 входных объектов в 1000 классов (что соответствует числу классов в ImageNet)
Чтобы обучить сквозную модель, которая будет классифицировать наш набор данных, нам необходимо:
- замените окончательный классификатор на тот, который создаст необходимое количество классов. В нашем случае мы можем использовать один Линейный слой с 25088 входными и 2 выходными нейронами.
- заморозить веса экстрактора сверточных признаков, чтобы они не обучались. Рекомендуется изначально выполнить это замораживание, поскольку в противном случае необученный слой классификатора может разрушить исходные предварительно обученные веса сверточного экстрактора. Заморозить веса можно, установив для свойства requires_grad всех параметров значение False.
vgg.classifier = torch.nn.Linear(25088,2).to(device) for x in vgg.features.parameters(): x.requires_grad = False summary(vgg,(1, 3,244,244)) ========================================================================================== Layer (type:depth-idx) Output Shape Param # ========================================================================================== VGG [1, 2] -- ├─Sequential: 1-1 [1, 512, 7, 7] -- │ └─Conv2d: 2-1 [1, 64, 244, 244] (1,792) │ └─ReLU: 2-2 [1, 64, 244, 244] -- │ └─Conv2d: 2-3 [1, 64, 244, 244] (36,928) │ └─ReLU: 2-4 [1, 64, 244, 244] -- │ └─MaxPool2d: 2-5 [1, 64, 122, 122] -- │ └─Conv2d: 2-6 [1, 128, 122, 122] (73,856) │ └─ReLU: 2-7 [1, 128, 122, 122] -- │ └─Conv2d: 2-8 [1, 128, 122, 122] (147,584) │ └─ReLU: 2-9 [1, 128, 122, 122] -- │ └─MaxPool2d: 2-10 [1, 128, 61, 61] -- │ └─Conv2d: 2-11 [1, 256, 61, 61] (295,168) │ └─ReLU: 2-12 [1, 256, 61, 61] -- │ └─Conv2d: 2-13 [1, 256, 61, 61] (590,080) │ └─ReLU: 2-14 [1, 256, 61, 61] -- │ └─Conv2d: 2-15 [1, 256, 61, 61] (590,080) │ └─ReLU: 2-16 [1, 256, 61, 61] -- │ └─MaxPool2d: 2-17 [1, 256, 30, 30] -- │ └─Conv2d: 2-18 [1, 512, 30, 30] (1,180,160) │ └─ReLU: 2-19 [1, 512, 30, 30] -- │ └─Conv2d: 2-20 [1, 512, 30, 30] (2,359,808) │ └─ReLU: 2-21 [1, 512, 30, 30] -- │ └─Conv2d: 2-22 [1, 512, 30, 30] (2,359,808) │ └─ReLU: 2-23 [1, 512, 30, 30] -- │ └─MaxPool2d: 2-24 [1, 512, 15, 15] -- │ └─Conv2d: 2-25 [1, 512, 15, 15] (2,359,808) │ └─ReLU: 2-26 [1, 512, 15, 15] -- │ └─Conv2d: 2-27 [1, 512, 15, 15] (2,359,808) │ └─ReLU: 2-28 [1, 512, 15, 15] -- │ └─Conv2d: 2-29 [1, 512, 15, 15] (2,359,808) │ └─ReLU: 2-30 [1, 512, 15, 15] -- │ └─MaxPool2d: 2-31 [1, 512, 7, 7] -- ├─AdaptiveAvgPool2d: 1-2 [1, 512, 7, 7] -- ├─Linear: 1-3 [1, 2] 50,178 ========================================================================================== Total params: 14,764,866 Trainable params: 50,178 Non-trainable params: 14,714,688 Total mult-adds (G): 17.99 ========================================================================================== Input size (MB): 0.71 Forward/backward pass size (MB): 128.13 Params size (MB): 59.06 Estimated Total Size (MB): 187.91 ==========================================================================================
Как видно из сводки, эта модель содержит около 15 миллионов параметров, но только 50 тысяч из них обучаемы — это веса классификационного слоя. Это хорошо, потому что мы можем настроить меньшее количество параметров с меньшим количеством примеров.
Теперь давайте обучим модель, используя наш исходный набор данных. Этот процесс займет много времени, поэтому мы воспользуемся функцией train_long, которая будет печатать некоторые промежуточные результаты, не дожидаясь конца эпохи. Настоятельно рекомендуется проводить это обучение на компьютерах с поддержкой графического процессора!
trainset, testset = torch.utils.data.random_split(dataset,[20000,len(dataset)-20000]) train_loader = torch.utils.data.DataLoader(trainset,batch_size=16) test_loader = torch.utils.data.DataLoader(testset,batch_size=16) train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1,print_freq=90) Epoch 0, minibatch 0: train acc = 0.375, train loss = 0.049225371330976486 Epoch 0, minibatch 90: train acc = 0.945054945054945, train loss = 0.13971448206639553 Epoch 0, minibatch 180: train acc = 0.9537292817679558, train loss = 0.13658707971730943 Epoch 0, minibatch 270: train acc = 0.959409594095941, train loss = 0.12419342378848593 Epoch 0, minibatch 360: train acc = 0.9643351800554016, train loss = 0.1132235328906791 Epoch 0, minibatch 450: train acc = 0.9657705099778271, train loss = 0.1185816914966524 Epoch 0, minibatch 540: train acc = 0.9660351201478743, train loss = 0.1288116652511625 Epoch 0, minibatch 630: train acc = 0.9667194928684627, train loss = 0.12467173048886936 Epoch 0, minibatch 720: train acc = 0.9690533980582524, train loss = 0.11865157128703081 Epoch 0, minibatch 810: train acc = 0.9700215782983971, train loss = 0.12006495766222257 Epoch 0, minibatch 900: train acc = 0.9701720310765816, train loss = 0.13083069491201182 Epoch 0, minibatch 990: train acc = 0.9709258324924319, train loss = 0.13124172237638748 Epoch 0, minibatch 1080: train acc = 0.9714962997224792, train loss = 0.13153546215978407 Epoch 0, minibatch 1170: train acc = 0.9718723313407344, train loss = 0.13723152870810204 Epoch 0 done, validation acc = 0.9795918367346939, validation loss = 0.12322273880255227
Похоже, мы получили достаточно точный классификатор кошек и собак! Давайте сохраним его для дальнейшего использования!
torch.save(vgg,'data/cats_dogs.pth')
Затем мы можем загрузить модель из файла в любое время. Это может оказаться полезным на случай, если следующий эксперимент разрушит модель.
vgg = torch.load('data/cats_dogs.pth')
Точная настройка трансферного обучения
В предыдущем разделе мы обучили последний слой классификатора классификации изображений в нашем собственном наборе данных. Однако мы не переобучали экстрактор функций, и наша модель полагалась на функции, которые модель извлекла из данных ImageNet. Если ваши объекты визуально отличаются от обычных изображений ImageNet, такое сочетание функций может работать не лучшим образом. Таким образом, имеет смысл начать обучение и сверточных слоев.
Для этого мы можем разморозить параметры сверточного фильтра, которые мы ранее заморозили.
Примечание. Важно сначала заморозить параметры и выполнить несколько эпох обучения, чтобы стабилизировать веса на уровне классификации. Если вы сразу начнете обучение сквозной сети с незамороженными параметрами, большие ошибки, скорее всего, уничтожат предварительно обученные веса в сверточных слоях.
for x in vgg.features.parameters(): x.requires_grad = True
После разморозки мы можем провести еще несколько эпох обучения. Вы также можете выбрать более низкую скорость обучения, чтобы минимизировать влияние на предварительно обученные веса. Однако даже при низкой скорости обучения можно ожидать, что точность упадет в начале обучения, пока, наконец, не достигнет несколько более высокого уровня, чем в случае с фиксированными весами.
Примечание. Это обучение происходит намного медленнее, поскольку нам нужно распространять градиенты обратно по многим уровням сети! Возможно, вы захотите просмотреть первые несколько мини-пакетов, чтобы увидеть тенденцию, а затем остановить вычисления.
train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1,print_freq=90,lr=0.0001)
Другие модели резюме
VGG-16 — одна из простейших архитектур компьютерного зрения.
Пакет torchvision предоставляет гораздо больше предварительно обученных сетей. Среди них наиболее часто используются архитектуры ResNet, разработанные Microsoft, и Inception от Google. Например, давайте изучим архитектуру простейшей модели ResNet-18 (ResNet — это семейство моделей разной глубины, вы можете попробовать поэкспериментировать с ResNet-151, если хотите увидеть, как выглядит действительно глубокая модель):
resnet = torchvision.models.resnet18() print(resnet) ResNet( (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False) (layer1): Sequential( (0): BasicBlock( (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) (1): BasicBlock( (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (layer2): Sequential( (0): BasicBlock( (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (downsample): Sequential( (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (1): BasicBlock( (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (layer3): Sequential( (0): BasicBlock( (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (downsample): Sequential( (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (1): BasicBlock( (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (layer4): Sequential( (0): BasicBlock( (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (downsample): Sequential( (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (1): BasicBlock( (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (avgpool): AdaptiveAvgPool2d(output_size=(1, 1)) (fc): Linear(in_features=512, out_features=1000, bias=True) )
Как видите, модель содержит одни и те же строительные блоки: экстрактор признаков и окончательный классификатор (fc). Это позволяет нам использовать эту модель точно так же, как мы использовали VGG-16 для трансферного обучения. Вы можете попробовать поэкспериментировать с приведенным выше кодом, используя в качестве базовой модели разные модели ResNet, и посмотреть, как изменится точность.
Пакетная нормализация
Эта сеть содержит еще один тип слоя: Пакетная нормализация. Идея пакетной нормализации состоит в том, чтобы привести значения, проходящие через нейронную сеть, к правильному интервалу. Обычно нейронные сети работают лучше всего, когда все значения находятся в диапазоне [-1, 1] или [0, 1], и именно по этой причине мы соответствующим образом масштабируем/нормализуем наши входные данные.
Однако при обучении глубокой сети может случиться так, что значения существенно выходят за пределы этого диапазона, что делает обучение проблематичным. Уровень пакетной нормализации вычисляет среднее и стандартное отклонение для всех значений текущего мини-пакета и использует их для нормализации сигнала перед передачей его через уровень нейронной сети. Это значительно повышает стабильность глубоких сетей.
Еда на вынос
Используя трансферное обучение, мы смогли быстро собрать классификатор для нашей задачи классификации объектов и добиться высокой точности. Однако этот пример был не совсем справедливым, поскольку исходная сеть VGG-16 была предварительно обучена распознавать кошек и собак, и поэтому мы просто повторно использовали большинство шаблонов, которые уже присутствовали в сети. Вы можете ожидать более низкой точности для более экзотических объектов, специфичных для предметной области, таких как детали производственной линии на заводе или различные листья деревьев.
Вы можете видеть, что более сложные задачи, которые мы сейчас решаем, требуют более высокой вычислительной мощности и не могут быть легко решены на процессоре. В следующем разделе мы попробуем использовать более упрощенную реализацию для обучения той же модели с использованием меньших вычислительных ресурсов, что приведет лишь к немного меньшей точности.
Приятного обучения!
Далее ›› Введение в компьютерное зрение с помощью PyTorch (6/6)