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

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

В этом блоге мы попробуем взглянуть на реализацию классификатора изображений с нуля в Pytorch.

Что такое Питорч?

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

Что такое набор данных CIFAR10?

Это установленный набор данных компьютерного зрения, используемый для распознавания объектов. Это подмножество набора данных 80 миллионов крошечных изображений и состоит из 60 000 цветных изображений 32x32, содержащих один из 10 классов объектов, по 6000 изображений на класс. Его собрали Алекс Крижевский, Винод Наир и Джеффри Хинтон.

Структура Кодекса:

  • Загрузка набора данных
  • Предварительная обработка изображения
  • Создайте модель Pytorch
  • Обучите модель
  • Прогнозы и результаты

1. Загрузите набор данных

Набор данных CIFAR-10 уже доступен с наборами данных, уже доступными в Pytorch. Чтобы использовать набор данных, нам нужно импортировать и загрузить набор данных с помощью Pytorch следующим образом:

#Get the CIFAR10 Dataset
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’)

2. Предварительная обработка изображения

import matplotlib.pyplot as plt
import numpy as np
# functions to show an image
def imshow(img):
    img = img / 2 + 0.5 # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()
# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()
# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(‘ ‘.join(‘%5s’ % classes[labels[j]] for j in range(4)))

3. Создание модели Pytorch

Используя Pytorch, мы попытаемся построить модель, которая может классифицировать изображения этих 10 классов (а именно: «самолет», «автомобиль», «птица», «кошка», «олень», «собака», «лягушка», «лошадь», «корабль», «грузовик»). Модель классификации изображений делится на части: изучение признаков и классификатор.

Изучение функций:

Эта часть модели связана с изучением различных особенностей и характеристик изображения, которые отличают его от остальных. Для изучения функций мы будем использовать CNN (Сверточные нейронные сети). Pytorch поставляется со сверточными 2D-слоями, которые можно использовать с помощью «torch.nn.conv2d».

Feature Learning осуществляется с помощью комбинации слоев свертки и объединения. Изображение можно рассматривать как матрицу пикселей, каждый пиксель имеет числовое значение, в котором хранится информация о цвете внутри этого пикселя. Сверточный слой использует ядро, которое выделяет характерные особенности/пиксели в конкретном изображении. Объединение используется для понижения разрешения этого изображения (т. е. уменьшения размеров), сохраняя только те пиксели, которые содержат важную информацию, и отбрасывая остальные. Объединение осуществляется двумя способами: глобальное среднее объединение и максимальное объединение. В этом случае мы будем использовать Max Pooling.

self.conv1 = nn.Conv2d(3, 6, 5)

Двумерный сверточный слой может быть объявлен следующим образом. Первый аргумент обозначает количество входных каналов, в данном случае это 3 (R, G и B). Второй аргумент обозначает количество выходных каналов, третий аргумент обозначает размер ядра, который в данном случае равен 5x5.

self.pool = nn.MaxPool2d(2, 2)

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

Начало классификации:

Изученные функции или выходные данные сверточных слоев передаются в слой Flatten, чтобы сделать его 1D. Затем это подается в линейные слои, активированные функцией активации ReLU (Rectified Linear Unit). Последний слой содержит 10 узлов, так как в этом примере количество классов равно 10.

self.fc1 = nn.Linear(16 * 5 * 5, 120)

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

Это важные слои, которые используются при построении модели. Давайте посмотрим на полный код для построения модели:

import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
    super(Net, self).__init__()
    self.conv1 = nn.Conv2d(3, 6, 5)
    self.pool = nn.MaxPool2d(2, 2)
    self.conv2 = nn.Conv2d(6, 16, 5)
    self.fc1 = nn.Linear(16 * 5 * 5, 120)
    self.fc2 = nn.Linear(120, 84)
    self.fc3 = nn.Linear(84, 10)

Класс Net используется для построения модели. Метод __init__ используется для определения слоев. После создания определений слоев следующим шагом является определение того, как данные проходят через эти слои при выполнении прямого прохода по сети:

def forward(self, x):
    x = self.pool(F.relu(self.conv1(x)))
    x = self.pool(F.relu(self.conv2(x)))
    x = x.view(-1, 16 * 5 * 5)
    x = F.relu(self.fc1(x))
    x = F.relu(self.fc2(x))
    x = self.fc3(x)
    return x

Первый слой или входной слой модели — conv1, а выходной слой — fc3. Эта функция определяет, как данные проходят через сеть — данные из входного слоя conv1 активируются функцией активации ReLU (F.relu()), затем они передаются на уровень пула, определенный как пул. Вывод первого уровня сохраняется в переменной x, которая затем отправляется на следующий уровень. Перед отправкой в ​​классификатор выходные данные последнего свернутого слоя выравниваются для линейных слоев. Первые два линейных слоя (fc1 и fc2) активируются функцией активации ReLU, а затем отправляют вывод на последний и последний слой fc3.

4. Обучите модель

Теперь архитектура модели готова к использованию, перед началом процесса обучения мы создадим экземпляр класса Net() и настроим оптимизатор и функцию потерь. В этой конкретной задаче мы будем использовать оптимизатор SGD со скоростью обучения 0,001. Скорость обучения определяет, насколько хорошо модель или как быстро модель должна соответствовать. Следовательно, чтобы предотвратить переоснащение модели, мы постараемся сохранить значение в нижней части. Используемая функция потерь — кросс-энтропия, поскольку это проблема классификации с несколькими метками.

#Define Loss Function and optimizer
import torch.optim as optim
net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

Вот код для компиляции модели перед обучением.

Затем с помощью этой функции модель обучается до желаемого количества эпох.

def train_the_classifier(net,trainloader,optimizer, criterion, epochs):
     #Training the Classifier
     for epoch in range(epochs): # loop over the dataset multiple times
          running_loss = 0.0
          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()
              # forward + backward + optimize
              outputs = net(inputs)
              loss = criterion(outputs, labels)
              loss.backward()
              optimizer.step()
              # print statistics
              running_loss += loss.item() 
              if i % 2000 == 1999: # print every 2000 mini-batches
                       print(‘[%d, %5d] loss: %.3f’ %
                        (epoch + 1, i + 1, running_loss / 2000))
                       running_loss = 0.0
    print(‘Finished Training’)

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

#train the classifier
train_the_classifier(net,trainloader,optimizer, criterion, 5)

Журнал тренировок выглядит следующим образом:

[1, 2000] loss: 2.217 
[1, 4000] loss: 1.835 
[1, 6000] loss: 1.697 
[1, 8000] loss: 1.638 
[1, 10000] loss: 1.565 
[1, 12000] loss: 1.545 
[2, 2000] loss: 1.467 
[2, 4000] loss: 1.389 
[2, 6000] loss: 1.395 
[2, 8000] loss: 1.346 
[2, 10000] loss: 1.325 
[2, 12000] loss: 1.297 
[3, 2000] loss: 1.239 
[3, 4000] loss: 1.224 
[3, 6000] loss: 1.238 
[3, 8000] loss: 1.227 
[3, 10000] loss: 1.201 
[3, 12000] loss: 1.193 
[4, 4000] loss: 1.133 
[4, 6000] loss: 1.125 
[4, 8000] loss: 1.135 
[4, 10000] loss: 1.112 
[4, 12000] loss: 1.123 
[5, 2000] loss: 1.025 
[5, 4000] loss: 1.063 
[5, 6000] loss: 1.058 
[5, 8000] loss: 1.049 
[5, 10000] loss: 1.052 
[5, 12000] loss: 1.066 
Finished Training

Полученный окончательный убыток равен 1,066. Давайте посмотрим, как модель работает на тестовом наборе данных. Теперь модель сохранена для тестирования.

#Save the Model
PATH = ‘./cifar_net.pth’
torch.save(net.state_dict(), PATH)

5. Тестирование и результаты

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

#load the model
net = Net()
net.load_state_dict(torch.load(PATH))

Результатом этого шага будет:

‹Все ключи успешно подобраны›

Сначала давайте протестируем изображение на первой партии изображений. Мы начинаем с печати первых 4 изображений вместе с этикеткой.

#Test the Network
dataiter = iter(testloader)
images, labels = dataiter.next()
# print images
imshow(torchvision.utils.make_grid(images))
print(‘GroundTruth: ‘, ‘ ‘.join(‘%5s’ % classes[labels[j]] for j in range(BATCH_SIZE)))

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

outputs = net(images)
_, predicted = torch.max(outputs, 1)
print(‘Predicted: ‘, ‘ ‘.join(‘%5s’ % classes[predicted[j]]
         for j in range(BATCH_SIZE)))

Прогнозируемые метки:

Прогноз: автомобиль, корабль, кошка, самолет

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

correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
            images, labels = data
            outputs = net(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            print(‘Accuracy of the network on the 10000 test images: %d %%’ % (100 * correct / total))

Общая точность составляет:

Точность сети на 10000 тестовых изображений: 60 %

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

class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
     for data in testloader:
         images, labels = data
         outputs = net(images)
         _, predicted = torch.max(outputs, 1)
         c = (predicted == labels).squeeze()
         for i in range(4):
              label = labels[i]
              class_correct[label] += c[i].item()
              class_total[label] += 1
for i in range(10):
     print(‘Accuracy of %5s : %2d %%’ % (
            classes[i], 100 * class_correct[i] / class_total[i]))

Точность по классам:

Точность самолета : 67 %
Точность автомобиля : 71 %
Точность птицы : 55 %
Точность кошки : 48 %
Точность оленя : 51 %
Точность собаки : 47 %
Точность лягушки : 57 %
Точность лошади : 60 %
Точность корабля : 74 %
Точность грузовика : 69 %

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

Повышение точности

Базовая модель имела точность 60%, мы постараемся улучшить общую точность, а также точность в каждом классе. Для этого делаем три изменения:

  • Добавьте слои пакетной нормализации
  • Сменить оптимизатор на Адама
  • Тренируйте его для большего количества эпох

Модель:

import torch.nn as nn
import torch.nn.functional as F
class ConvNet(nn.Module):
       def __init__(self):
            super(ConvNet, self).__init__()
            self.conv1 = nn.Conv2d(3, 16, 5)
            self.pool = nn.MaxPool2d(2, 2)
            self.bn1 = nn.BatchNorm2d(16)
            self.conv2 = nn.Conv2d(16, 32, 5)
            self.bn2 = nn.BatchNorm2d(32)
            
            self.fc1 = nn.Linear(32 * 5 * 5, 120)
            self.fc2 = nn.Linear(120, 84)
            self.fc3 = nn.Linear(84, 10)
       def forward(self, x):
            x = self.pool(F.relu(self.conv1(x)))
            x = self.bn1(x)
            x = self.pool(F.relu(self.conv2(x)))
            x = self.bn2(x)
            x = torch.flatten(x, 1)
            x = F.relu(self.fc1(x))
            x = F.relu(self.fc2(x))
            x = self.fc3(x)
       return x
#Define Loss Function and optimizer
import torch.optim as optim
net = ConvNet()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

Модель обучается, как и раньше, но на этот раз до 10 эпох, журнал обучения последних пяти эпох показан ниже.

Журнал тренировок:

[1, 2000] loss: 1.860 
[1, 4000] loss: 1.537 
[1, 6000] loss: 1.407 
....
[5, 2000] loss: 0.753 
[5, 4000] loss: 0.771 
[5, 6000] loss: 0.792 
[5, 8000] loss: 0.773 
[5, 10000] loss: 0.795 
[5, 12000] loss: 0.801 
[6, 2000] loss: 0.706 
[6, 4000] loss: 0.693 
[6, 6000] loss: 0.715 
[6, 8000] loss: 0.730 
[6, 10000] loss: 0.720 
[6, 12000] loss: 0.756 
[7, 2000] loss: 0.626 
[7, 4000] loss: 0.651 
[7, 6000] loss: 0.682 
[7, 8000] loss: 0.680 
[7, 10000] loss: 0.687 
[7, 12000] loss: 0.678 
[8, 2000] loss: 0.573 
[8, 4000] loss: 0.612 
[8, 6000] loss: 0.626 
[8, 8000] loss: 0.637 
[8, 10000] loss: 0.636 
[8, 12000] loss: 0.634 
[9, 2000] loss: 0.551 
[9, 4000] loss: 0.542 
[9, 6000] loss: 0.575 
[9, 8000] loss: 0.572 
[9, 10000] loss: 0.593 
[9, 12000] loss: 0.623 
[10, 2000] loss: 0.516 
[10, 4000] loss: 0.521 
[10, 6000] loss: 0.555 
[10, 8000] loss: 0.537 
[10, 10000] loss: 0.592 
[10, 12000] loss: 0.586 
Finished Training

Потери при обучении в конце 10 эпох резко сократились до 0,586.

Общая точность:

Точность сети на 10000 тестовых изображений: 67 %

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

Точность по классам:

Точность самолета : 77 %
Точность автомобиля : 79 %
Точность птицы : 52 %
Точность кошки : 50 %
Точность оленя : 64 %
Точность собаки : 54 %
Точность лягушки : 71 %
Точность лошади : 74 %
Точность корабля : 73 %
Точность грузовика : 76 %

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

Полный код и набор данных можно найти в этом Colab Notebook. Не стесняйтесь настраивать параметры и пытаться повысить точность.

Спасибо!

Использованная литература:

[1] https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html

[2] https://analyticsindiamag.com/how-to-implement-cnn-model-using-pytorch-with-tpu/