Введение
Эта статья продолжает цикл работ по созданию наиболее популярных конволюционных нейронных сетей с нуля в PyTorch. Предыдущая статья здесь, в которой мы построили LeNet5. В этой статье мы создадим AlexNet, одну из наиболее важных инновационных алгоритмов в компьютерном зрении.
Мы начнем с исследования и понимания архитектуры AlexNet. Затем мы сразу же войдем в код, загрузим нашу базу данных, CIFAR-10, и проведем некоторую предварительную обработку данных. Затем мы создадим нашу AlexNet с нуля с использованием PyTorch
и обучим ее на наших предварительно обработанных данных. Наконец, обученная модель будет тестироваться на неизвестных (тестовых) данных для оценки ее эффективности.
Предварительные знания
Знания о нейронных сетях помогут вам better understand this article. Это включает знание различных слоев нейронных сетей (входного слоя, скрытых слоев, выходного слоя), активационных функций, оптимизационных алгоритмов (varieties of gradient descent), функций потерь и т. д. Кроме того, знание синтаксиса Python и библиотеки PyTorch является необходимым для понимания приведенных в этой статье кусочек кода.
Сущностное понимание CNN является необходимым. Это включает в себя знание о слоях свертки, слоях池инга (pooling) и их роли в извлечении признаков из входных данных. Понимание таких концепций, как шаг (stride), дополнение (padding) и влияние размера ядра/фильтра, также будет полезно.
AlexNet
AlexNet представляет собой глубокую сверточную нейронную сеть, которая была впервые разработана Алексом Кріжевским и его коллегами в 2012 году. Она была создана для классификации изображений для соревнования ImageNet LSVRC-2010, где достигла результата на уровне лучших. Вы можете подробно прочитать о модели в оригинальной исследовательской работе здесь.
Пройдемся по ключевым выводам из работы с AlexNet. Во-первых, AlexNet работала с изображениями с тремя каналами размером (224x224x3). Она использовала максимальное пулинг (max pooling) вместе с активациями ReLU при подвыборке. Ядра для свертки были либо 11×11, 5×5, либо 3×3, в то время как ядра для максимального пулинга имели размер 3×3. Она классифицировала изображения в 1000 классов. Также она использовала несколько GPU.
Набор данных
Давайте начнем с загрузки и предварительной обработки данных. Для наших целей мы будем использовать набор данных CIFAR-10. Набор данных состоит из 60000 изображений 32×32 цветных картинок в 10 классах, по 6000 картинок в каждом классе. Есть 50000 картинок для обучения и 10000 картинок для теста.
Вот классы в наборе данных, а также 10 случайных образцов каждого класса:
Источник: source
Классы полностью взаимно исключают друг друга. нет пересечения между легкими и грузовиками. “Автомобиль” включает легковые автомобили, внедорожники и такие вещи. “Грузовик” включает только большие грузовики. Ни один из них не включает пикапы.
Импорт библиотек
Начнем с импорта необходимых библиотек и определения переменной device
, чтобы ноутбук знал, что нужно использовать GPU для обучения модели, если он доступен.
import numpy as np
import torch
import torch.nn as nn
from torchvision import datasets
from torchvision import transforms
from torch.utils.data.sampler import SubsetRandomSampler
# Конфигурация устройства
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
Загрузка набора данных
Используя torchvision
(поддержка библиотека для задач компьютерного зрения), мы загрузим нашу выборку данных. Этот метод содержит несколько помощничких функций, делающих предварительную обработку простой и последовательной. Подdef по functions get_train_valid_loader
и get_test_loader
, и потом их вызвать, чтобы загрузить и обработать нашу данных CIFAR-10:
def get_train_valid_loader(data_dir,
batch_size,
augment,
random_seed,
valid_size=0.1,
shuffle=True):
normalize = transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2023, 0.1994, 0.2010],
)
# define transforms
valid_transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
if augment:
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize,
])
else:
train_transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
# load the dataset
train_dataset = datasets.CIFAR10(
root=data_dir, train=True,
download=True, transform=train_transform,
)
valid_dataset = datasets.CIFAR10(
root=data_dir, train=True,
download=True, transform=valid_transform,
)
num_train = len(train_dataset)
indices = list(range(num_train))
split = int(np.floor(valid_size * num_train))
if shuffle:
np.random.seed(random_seed)
np.random.shuffle(indices)
train_idx, valid_idx = indices[split:], indices[:split]
train_sampler = SubsetRandomSampler(train_idx)
valid_sampler = SubsetRandomSampler(valid_idx)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=batch_size, sampler=train_sampler)
valid_loader = torch.utils.data.DataLoader(
valid_dataset, batch_size=batch_size, sampler=valid_sampler)
return (train_loader, valid_loader)
def get_test_loader(data_dir,
batch_size,
shuffle=True):
normalize = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
)
# define transform
transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
dataset = datasets.CIFAR10(
root=data_dir, train=False,
download=True, transform=transform,
)
data_loader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=shuffle
)
return data_loader
# CIFAR10 dataset
train_loader, valid_loader = get_train_valid_loader(data_dir = './data', batch_size = 64,
augment = False, random_seed = 1)
test_loader = get_test_loader(data_dir = './data',
batch_size = 64)
Позвольте нам разбить код:
- Мы def two functions
get_train_valid_loader
иget_test_loader
, чтобы загрузить train/validation и test sets respectively - Мы начинаем def переменной
normalize
с mean и стандартных отклонений каждого из каналов (красного, зеленого и синего) в выборке. Эти могут быть рассчитаны вручную, но также доступны в Интернете, так как CIFAR-10 достаточно популярна - Для нашего обучающего датасета, мы добавляем опцию для увеличения dataset, делая train также для more robust training and increasing the number of images as well. Примечание: увеличение применяется только для train subset и не для validation и test subsets, поскольку они используются только для оценки
- Мы разделяем обучающую выборку на train и validation sets (90:10 отношение), и subset ее случайно из всего train set
- Мы указали batch size и shuffle dataset при загрузке, так что каждый batch будет have some variance в типах label, которые он содержит. Это увеличит эффективность нашего пришедшего model.
- В конечном итоге мы используем данные загрузчики. Это, возможно, не скажется на производительности для небольших наборов данных, таких как CIFAR-10, но это может сильно помешать производительности для больших наборов данных и, в целом, считается хорошей практикой. Данные загрузчики позволяют нам итерироваться по данным в batch’ах, и данные загружаются во время итерации, а не все сразу при запуске в RAM.
AlexNet с начала
Начнем с кода:
class AlexNet(nn.Module):
def __init__(self, num_classes=10):
super(AlexNet, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=0),
nn.BatchNorm2d(96),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2))
self.layer2 = nn.Sequential(
nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2))
self.layer3 = nn.Sequential(
nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(384),
nn.ReLU())
self.layer4 = nn.Sequential(
nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(384),
nn.ReLU())
self.layer5 = nn.Sequential(
nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2))
self.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(9216, 4096),
nn.ReLU())
self.fc1 = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU())
self.fc2= nn.Sequential(
nn.Linear(4096, num_classes))
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.layer5(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
out = self.fc1(out)
out = self.fc2(out)
return out
Определение модели AlexNet
Посмотрим, как работает вышеуказанный код:
- Первым шагом при определении любой нейронной сети (как это CNN или нет) в PyTorch является определение класса, наследующего от
nn.Module
, так как он содержит много методов, которые мы будем использовать. - После этого идет два основных шага. Первым является инициализация слоев, которые мы будем использовать в нашем CNN, внутри метода
__init__
, а вторым является определение последовательности, в которой эти слои будут обрабатывать изображение. Это определяется внутри методаforward
. - В самой архитектуре, сначала мы определяем конволюционные слои использованием функции
nn.Conv2D
с соответствующим размером кERNELа и каналами ввода/вывода. Мы также применяем максимальное о́тсечение с использованием функцииnn.MaxPool2D
. Nice thing about PyTorch is that we can combine the convolutional layer, activation function, and max pooling into one single layer (they will be separately applied, but it helps with organization) using thenn.Sequential
function - Then we define the fully connected layers using linear (
nn.Linear
) and dropout (nn.Dropout
) along with ReLu activation function (nn.ReLU
) and combining these with thenn.Sequential
function - Finally, our last layer outputs 10 neurons which are our final predictions for the 10 classes of objects
Setting Hyperparameters
Before training, we need to set some hyperparameters, such as the loss function and the optimizer to be used along with batch size, learning rate, and number of epochs.
num_classes = 10
num_epochs = 20
batch_size = 64
learning_rate = 0.005
model = AlexNet(num_classes).to(device)
# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)
# Train the model
total_step = len(train_loader)
Мы начинаем с определения простых гиперпараметров (epochs, размер batch и скорость обучения) и инициализации нашей модели с использованием количества классов в качестве аргумента, который в данном случае равен 10, а также переноса модели на соответствующее устройство (CPU или GPU). Затем мы определяем нашу функцию потерь как функцию交叉熵 и оптимизатор как Adam. Есть много вариантов для них, но эти, как правило, дают хорошие результаты с моделью и данными. Наконец, мы определяем total_step
, чтобы лучше следить за шагами во время обучения
Тренировка
На данный момент мы готовы к тренировке нашей модели:
total_step = len(train_loader)
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
#Перемещаем тензоры на настроенное устройство
images = images.to(device)
labels = labels.to(device)
#Проход вперед
outputs = model(images)
loss = criterion(outputs, labels)
#Обратный проход и оптимизация
optimizer.zero_grad()
loss.backward()
optimizer.step()
print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
.format(epoch+1, num_epochs, i+1, total_step, loss.item()))
# Validation
with torch.no_grad():
correct = 0
total = 0
for images, labels in valid_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
del images, labels, outputs
print('Accuracy of the network on the {} validation images: {} %'.format(5000, 100 * correct / total))
Посмотрим, что делает наш код:
- Мы начинаем с итерации по числу эпох, а затем по batch’ам в нашей тренировочной выборке
- Мы конвертируем изображения и метки согласно устройству, которое мы используем, то есть GPU или CPU
- В проходе вперед мы делаем предсказания с помощью нашей модели и вычисляем потерю на основе этих предсказаний и наших реальных метк
- Далее мы выполняем обратный проход, где мы на самом деле обновляем наши веса для улучшения модели
- Потом мы устанавливаем градиенты в нуль перед каждым обновлением с использованием функции
optimizer.zero_grad()
- Потом мы вычисляем новые градиенты с использованием функции
loss.backward()
- И наконец, мы обновляем веса с помощью функции
optimizer.step()
- Также наконец, в конце каждого эпоха мы используем нашу выборку валидации, чтобы вычислить точность модели. В данном случае мы не нужны градиенты, поэтому мы используем
with torch.no_grad()
для более быстрой оценки
Мы можем видеть выходной результат следующим образом:
Обучение потерь и точность валидации
Как мы можем видеть, потери уменьшаются с каждой эпохой, что показывает, что наша модель действительно учится. Заметим, что эта потеря на тренировочной выборке, и если потеря слишком мала, это может указывать на переобучение. Поэтому мы используем и валидационную выборку. Точность, кажется, увеличивается на валидационной выборке, что указывает на то, что риск переобучения крайне мал. Теперь попробуем проверить, как наша модель выполняет свои обязанности.
Проверка
Теперь мы посмотрим, как наша модель выполняет свои обязанности на невиданных данных:
with torch.no_grad():
correct = 0
total = 0
for images, labels in test_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
del images, labels, outputs
print('Accuracy of the network on the {} test images: {} %'.format(10000, 100 * correct / total))
Обратите внимание, что код идентичен тому, который использовался для нашей валидации.
Используя модель и обучаясь только 6 эпохам, мы кажется, получаем около 78,8% точности на валидационной выборке.
Точность теста
Заключение
Теперь посмотрим, что мы сделали в этой статье:
- Мы начали с понимания архитектуры и различных слоев в модели AlexNet
- Далее мы загрузили и провели предварительную обработку датасета CIFAR-10 с использованием
torchvision
- Потом мы использовали
PyTorch
для создания модели AlexNet с нуля - В конце концов, мы обучили и тестировали нашу модель на датасете CIFAR-10, и, кажется, модель хорошо работает на тестовом датасете после минимальной тренировки (6 эпох)
Будущая работа
Эта статья предоставляет твердую основу и практический опыт, но вы получите еще больше знаний, исследуя далее и испытывая, что еще можно достичь.
- Вы можете попробовать использовать другие датасеты. Один из таких датасетов – CIFAR-100, который является расширением датасета CIFAR-10 с 100 классами
- Вы можете экспериментировать с различными гиперпараметрами и найти лучшую комбинацию их для модели
- В конце концов, вы можете попробовать добавлять или убирать слои из датасета, чтобы проверить их влияние на возможности модели.
Source:
https://www.digitalocean.com/community/tutorials/alexnet-pytorch