Классификация изображений является одним из наиболее известных вариантов использования сверточных нейронных сетей (CNN). CNN используются в промышленном, медицинском и развлекательном секторах.
Одним из самых больших препятствий при построении CNN является тот факт, что им требуется большое количество предварительно классифицированных обучающих данных и многочисленные обучающие циклы для получения хорошей точности. В основном это фактор стоимости, поскольку данные необходимо классифицировать вручную, а сети требуется, в зависимости от ее сложности, большая вычислительная мощность, много времени или и то, и другое.
В этой статье я покажу вам, как создать CNN для классификации пород собак с нуля и обучить VGG16, хорошо известную CNN, чтобы показать преимущества трансферного обучения для небольших наборов данных.
Постановка задачи
У меня есть довольно небольшой набор данных изображений собак и набор данных изображений людей, любезно предоставленный Udacity для этой цели. Я хочу создать небольшое приложение, которое принимает изображение в качестве входных данных и возвращает одно из следующих сообщений:
- если это изображение собаки, верните предсказанную породу
- если это изображение человека, верните его наиболее напоминающую породу собаки
- если это не собака и не человек, вернуть сообщение об ошибке
Обнаружение собак и людей
Моей отправной точкой для приложения является определение того, напоминает ли данное изображение человека, собаку или ни то, ни другое. Для этого я использую реализацию классификатора Хаара OpenCV для обнаружения изображений людей и предварительно обученную сеть VGG16 для обнаружения изображений собак.
Реализация детектора человеческого лица
Я использовал один из предварительно обученных детекторов лиц OpenCV, который можно легко извлечь из .xml, предоставленного одной командой:
import cv2 face_cascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_alt.xml')
Детектор лиц написан в несколько строк кода, так как данные требуют небольшой предварительной обработки.
def face_detector(img_path): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray) return len(faces) > 0
функция выше делает следующее:
- чтение изображения из его пути к файлу
- преобразование его в оттенки серого
- создание списка изображений, содержащих обнаруженные лица
- возвращает, содержит ли результирующий список какие-либо записи
затем я могу импортировать оба набора данных, чтобы опробовать мой удобный маленький детектор лиц:
from glob import glob import numpy as np # load filenames for human and dog images human_files = np.array(glob("/data/lfw/*/*")) dog_files = np.array(glob("/data/dog_images/*/*/*"))
И оцените его с помощью следующего кода:
from tqdm import tqdm human_files_short = human_files[:100] dog_files_short = dog_files[:100] detected_faces_list = [] for x in human_files_short: detected_faces_list.append(face_detector(x)) print("detected Faces in human files: " + str(sum(detected_faces_list))) detected_faces_list = [] for x in dog_files_short: detected_faces_list.append(face_detector(x)) print("detected Faces in dog files: "+str(sum(detected_faces_list))
В наборе данных изображений человека было обнаружено 98% человеческих лиц, что является высокой точностью. Количество ложных срабатываний составляет 17% человеческих лиц, обнаруженных в наборе данных изображений собак, что соответствует нашему варианту использования.
Реализация детектора собак
Для обнаружения собак мы будем использовать предварительно обученную сеть VGG16, предоставленную Torchvision.
Сеть предварительно обучена в базе данных ImageNet и может быть создана с помощью этого фрагмента кода:
import torch import torchvision.models as models # define VGG16 model VGG16 = models.vgg16(pretrained=True)
Чтобы сделать прогноз по изображению, нам нужно:
- преобразовать его в тензор с теми же размерностями, что и входная размерность нашей сети.
- передать его в предварительно обученную сеть
- преобразовать выходной тензор в классификатор с наивысшей достоверностью
from PIL import Image import torchvision.transforms as transforms import torchvision.datasets as datasets import os import numpy as np def VGG16_predict(img_path): ''' Use pre-trained VGG-16 model to obtain index corresponding to predicted ImageNet class for image at specified path Args: img_path: path to an image Returns: Index corresponding to VGG-16 model's prediction ''' ## Load and pre-process an image from the given img_path transform = transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), ]) data = Image.open(img_path) data = transform(data) data = np.expand_dims(data, 0) data = torch.from_numpy(data) ## Return the *index* of the predicted class for that image prediction = VGG16(data) return torch.max(prediction,1)[1].numpy()[0] # predicted class index
Глядя на классификационный словарь, мы видим, что изображения собак варьируются от 151 до 268, поэтому мы можем сделать детектор собак, используя наш предиктор VGG16 со следующей функцией:
### returns "True" if a dog is detected in the image stored at img_path def dog_detector(img_path): prediction = VGG16_predict(img_path) is_dog = prediction>=151 and prediction<=268 return is_dog
Когда мы оцениваем наш детектор собак, мы получаем следующий результат:
91% точность обнаружения собак и 0(!) ложных срабатываний, что удивительно.
Создание CNN для классификации изображений с нуля
Для обучения и тестирования нашей сети нам нужны загрузчики данных для обучения, проверки и тестирования. Эти загрузчики данных также автоматически создают наши метки, если мы отсортировали данные в соответствии с метками в нашей файловой системе. Они также сделают преобразования для нас.
Чтобы увеличить наше довольно небольшое количество изображений, я горизонтально переверну и поверну некоторые изображения.
transformations_training = transforms.Compose([transforms.Resize(256), transforms.RandomCrop(224),transforms.RandomRotation(20),transforms.RandomHorizontalFlip(p=0.5) , transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) transformations_validation_test = transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) training_set = datasets.ImageFolder(root='/data/dog_images/train' , transform = transformations_training) validation_set = datasets.ImageFolder(root='/data/dog_images/valid' , transform = transformations_validation_test) test_set = datasets.ImageFolder(root='/data/dog_images/test' , transform = transformations_validation_test) train = torch.utils.data.DataLoader(training_set, batch_size = 32, shuffle = True) test= torch.utils.data.DataLoader(test_set, batch_size = 32, shuffle = False) valid= torch.utils.data.DataLoader(validation_set, batch_size = 32, shuffle = True) loaders_scratch = {'train':train, 'test':test, 'valid':valid}
Далее нам нужно построить нашу CNN. Я ориентировался на архитектуру VGG16:
import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): ### TODO: choose an architecture, and complete the class def __init__(self): super(Net, self).__init__() ## Define layers of a CNN self.first_conv_layer_output =64 self.second_conv_layer_output = self.first_conv_layer_output*2 self.last_conv_layer_output = self.second_conv_layer_output*2 self.conv1 = nn.Conv2d(3, self.first_conv_layer_output, 3, padding = 1) self.conv2 = nn.Conv2d(self.first_conv_layer_output, self.second_conv_layer_output, 3, padding = 1) self.conv3 = nn.Conv2d(self.second_conv_layer_output, self.last_conv_layer_output, 3, padding = 1) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(((224/2/2/2)**2)*self.last_conv_layer_output, 1024) self.fc2 = nn.Linear(1024,512) self.fc3 = nn.Linear(512, 133) self.dropout = nn.Dropout(0.25) def forward(self, x): ## Define forward behavior x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, ((224/2/2/2)**2)*self.last_conv_layer_output) x = self.dropout(F.relu(self.fc1(x))) x = self.dropout(F.relu(self.fc2(x))) x = self.fc3(x) return x #-#-# You so NOT have to modify the code below this line. #-#-# # instantiate the CNN model_scratch = Net() print(model_scratch) use_cuda = torch.cuda.is_available() # move tensors to GPU if CUDA is available if use_cuda: model_scratch.cuda()
Для обучения CNN используется следующая функция:
from PIL import ImageFile import numpy as np ImageFile.LOAD_TRUNCATED_IMAGES = True # One image is truncated and better use it than writing "continue" exception handling def train(n_epochs, loaders, model, optimizer, criterion, use_cuda, save_path): """returns trained model""" # initialize tracker for minimum validation loss valid_loss_min = np.Inf scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=15, gamma=0.5) for epoch in range(1, n_epochs+1): # initialize variables to monitor training and validation loss train_loss = 0.0 valid_loss = 0.0 ################### # train the model # ################### model.train() for batch_idx, (data, target) in enumerate(loaders['train']): # move to GPU if use_cuda: data, target = data.cuda(), target.cuda() optimizer_scratch.zero_grad() estimation = model(data) loss = criterion(estimation, target) loss.backward() optimizer.step() train_loss = train_loss + ((1 / (batch_idx + 1)) * (loss.data - train_loss)) ###################### # validate the model # ###################### model.eval() with torch.no_grad(): for batch_idx, (data, target) in enumerate(loaders['valid']): # move to GPU if use_cuda: data, target = data.cuda(), target.cuda() ## update the average validation loss estimation = model(data) loss = criterion(estimation, target) valid_loss = valid_loss + ((1 / (batch_idx + 1)) * (loss.data - valid_loss)) # print training/validation statistics print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format( epoch, train_loss, valid_loss )) scheduler.step() ## TODO: save the model if validation loss has decreased if valid_loss < valid_loss_min: torch.save(model.state_dict(), save_path) #save full model to resume training #does not work due to lack of space #sadface # torch.save({ # 'epoch': epoch, # 'model_state_dict': model.state_dict(), # 'optimizer_state_dict': optimizer.state_dict(), # 'loss': criterion, # }, 'model_scratch_full.pth') valid_loss_min = valid_loss # return trained model return model # train the model model_scratch = train(30, loaders_scratch, model_scratch, optimizer_scratch, criterion_scratch, use_cuda, 'model_scratch.pt') # load the model that got the best validation accuracy model_scratch.load_state_dict(torch.load('model_scratch.pt'))
Я кратко расскажу о своей тренировочной функции. Его можно разделить на 3 части:
- Инициализация
- Обучение
- Проверка
Я инициализирую планировщик и параметры потерь, затем начинаю повторять свои эпохи.
valid_loss_min = np.Inf scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=15, gamma=0.5) for epoch in range(1, n_epochs+1): # initialize variables to monitor training and validation loss train_loss = 0.0 valid_loss = 0.0
Затем я устанавливаю свою модель в режим обучения и перебираю пакет данных:
- сбросить градиенты моего оптимизатора в начале новой партии
- сделать оценку с моей моделью
- вызовите функцию потерь, чтобы получить потерю этой партии
- рассчитать средний убыток
model.train() for batch_idx, (data, target) in enumerate(loaders['train']): # move to GPU if use_cuda: data, target = data.cuda(), target.cuda() optimizer_scratch.zero_grad() estimation = model(data) loss = criterion(estimation, target) loss.backward() optimizer.step() train_loss = train_loss + ((1 / (batch_idx + 1)) * (loss.data - train_loss))
После обучения со всеми партиями я проверяю свою сеть:
- установить сеть в режим проверки
- отключить корректировку градиентов оптимизатора
- делать пакетные оценки
- определить потери на партию
- рассчитать средний убыток
- если модель работает лучше, чем предыдущие итерации, я ее сохраню
model.eval() with torch.no_grad(): for batch_idx, (data, target) in enumerate(loaders['valid']): # move to GPU if use_cuda: data, target = data.cuda(), target.cuda() ## update the average validation loss estimation = model(data) loss = criterion(estimation, target) valid_loss = valid_loss + ((1 / (batch_idx + 1)) * (loss.data - valid_loss)) # print training/validation statistics print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format( epoch, train_loss, valid_loss )) scheduler.step() torch.save(model.state_dict(), save_path) valid_loss_min = valid_loss
Поскольку аппроксимация правильной скорости обучения довольно сложна, я решил использовать планировщик скорости обучения и начать с довольно высокой скорости обучения 0,06 и уменьшить ее вдвое через 15 эпох.
Закомментированная часть
# torch.save({ # 'epoch': epoch, # 'model_state_dict': model.state_dict(), # 'optimizer_state_dict': optimizer.state_dict(), # 'loss': criterion, # }, 'model_scratch_full.pth')
сохраняет состояние модели, состояние оптимизатора и функцию потерь таким образом, что позволяет нам возобновить обучение. К сожалению, у меня не хватило места на рабочем месте, чтобы попробовать
Я заканчиваю через 30 эпох и тестирую свою модель.
Точность 19% более чем в 25 раз лучше, чем случайный выбор, но этого недостаточно, чтобы сделать надежный прогноз, давайте попробуем перенести обучение.
Создание CNN с помощью трансферного обучения
Я загружаю тот же предварительно обученный VGG16, который я уже использовал для своего детектора собак, и изменяю выходные слои на 133 признака, что соответствует количеству различных пород собак, которые я хочу классифицировать.
Я использую те же загрузчики данных, функции обучения и проверки, что и в сети, которую я построил с нуля. Я только настроил планировщик скорости обучения, чтобы уменьшить скорость обучения на 90% после 8 эпох.
Точность теста 86% — это очень хорошо, учитывая, что некоторые породы настолько похожи друг на друга, что их очень трудно различить даже человеку.
Подобно детектору собак, я написал программу классификации собак, которая принимает изображение в качестве входных данных и возвращает соответствующую породу собаки в виде строки.
### TODO: Write a function that takes a path to an image as input ### and returns the dog breed that is predicted by the model. # list of class names by index, i.e. a name can be accessed like class_names[0] class_names = [item[4:].replace("_", " ") for item in loaders_transfer['train'].dataset.classes] def predict_breed_transfer(img_path): #if torch.cuda.is_available(): # model_transfer= model_transfer.cuda() # load the image and return the predicted breed #copy paste of vgg16 predict transform = transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(),transforms.Normalize([0.5, 0.5, 0.5],[0.5, 0.5, 0.5])]) data = Image.open(img_path) data = transform(data) data = np.expand_dims(data, 0) data = torch.from_numpy(data) if use_cuda: data = data.cuda() model_transfer.eval() ## Return the *index* of the predicted class for that image prediction = model_transfer(data) prediction = prediction.cpu() class_index = torch.max(prediction,1)[1].numpy()[0] # predicted class index return class_names[class_index]
Создайте приложение
Теперь пришло время объединить разные части, чтобы получить приложение для классификации пород собак, которое выводит разные сообщения для изображений, содержащих собаку, человека или ничего, и показывает (похожую) породу собак для первых двух.
def run_app(img_path): ## handle cases for a human face, dog, and neither print("==============================================") print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") if dog_detector(img_path): predicted_dog = predict_breed_transfer(img_path) print("you are a " +predicted_dog) elif face_detector(img_path): predicted_dog = predict_breed_transfer(img_path) print("The dog breed that resembles you most is " +predicted_dog) else: print("Error") plt.imshow(Image.open(img_path)) plt.show() print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("==============================================")
Следующим улучшением приложения может стать развертывание CNN в веб-службе и предоставление доступа к ней с веб-сайта, на который вы можете загружать свои собственные изображения и получать соответствующие выходные данные.
Точность классификации пород собак можно повысить за счет использование другой сети, обучение большего количества эпох и использование большего количества изображений в качестве обучающих данных, особенно для менее представленных пород собак.
Обучение сетей заняло в целом менее 2 часов с использованием предоставленного ноутбука с улучшенным графическим процессором. Скорее всего, это будет быстрее, если вы используете RTX 1080 или лучше.
19% Точность тестирования на нашей недавно построенной CNN превосходит угадывание в 25 раз, что, безусловно, является успехом, если принять во внимание наш небольшой набор данных и менее 1 часа обучения.
Менее 1 часа обучения для точности 86% с использованием трансферного обучения. поразительно, учитывая, что обучение VGG16 с нуля занимает больше недели с большей вычислительной мощностью, чем просто 1080 и больший набор данных.