ОБЗОР

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

Рабочий процесс, принятый для этого проекта, выглядит следующим образом:

Шаг 0: Импорт модулей

Шаг 1: Обнаружение людей

Шаг 2: Обнаружение собак

Шаг 3: Создайте CNN для классификации пород собак (с использованием трансферного обучения)

Шаг 4: Напишите свой алгоритм

Шаг 5: Проверьте свой алгоритм

Шаг 6: разработайте API

Импорт модулей

Pytorch будет основной библиотекой машинного обучения (ML), используемой в этом проекте. Эталонной моделью для этого проекта будет VGG-16, модель сверточной нейронной сети, предложенная К. Симоняном и А. Зиссерманом из Оксфордского университета. Эта модель достигла точности 92,7% в топ-5 тестов. в ImageNet, который представляет собой набор данных из более чем 14 миллионов изображений, принадлежащих к 1000 классам. Достижение точности 92,7% для этого проекта с небольшим доступным набором данных (8351 изображение собак) по сравнению с набором данных сети изображений, который был обучен VGG-16, безусловно, далеко, но к этому нужно стремиться.

Блокнот проекта можно скачать с GitHub здесь

Ссылка для скачивания набора изображений человека: https://s3-us-west-1.amazonaws.com/udacity-aind/dog-project/lfw.zip

Ссылка для скачивания набора изображений собак: https://s3-us-west-1.amazonaws.com/udacity-aind/dog-project/dogImages.zip

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

import os
from glob import glob
import numpy as np
import cv2                
import matplotlib.pyplot as plt  
from tqdm import tqdm
from PIL import Image
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
import torch
import torchvision.models as models
import torchvision.transforms as transforms
from torchvision import datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

Обнаружение людей

Сначала мы загружаем изображения человека и собаки и конвертируем каждое изображение в массив numpy:

# load filenames for human and dog images
human_files = np.array(glob("/data/lfw/*/*"))
dog_files = np.array(glob("/data/dog_images/*/*/*"))
# print number of images in each dataset
print('There are %d total human images.' % len(human_files))
print('There are %d total dog images.' % len(dog_files))

Всего 13233 человеческих изображения.

Всего 8351 изображение собаки.

Мы будем использовать реализацию OpenCV каскадных классификаторов на основе признаков Хаара для обнаружения человеческих лиц на изображениях.

OpenCV предоставляет множество предварительно обученных детекторов лиц, хранящихся в виде XML-файлов на [inert link here]. Мы скачали один из этих детекторов и сохранили его в каталоге haarcascades. В следующей ячейке кода мы пишем функцию, которая принимает путь к изображению и возвращает «Истина», если обнаружено человеческое лицо, иначе она возвращает «Ложь».

# returns "True" if face is detected in image stored at img_path
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

Выполнение функции face_detector для первых 100 изображений человека и собаки приводит к следующему результату:

human_files_short = human_files[:100]
dog_files_short = dog_files[:100]
## TODO: Test the performance of the face_detector algorithm 
## on the images in human_files_short and dog_files_short.
humans_detected_human_file = 0
humans_detected_dog_file = 0
for img in human_files_short:
    if face_detector(img):
        humans_detected_human_file += 1
        
for img in dog_files_short:
    if face_detector(img):
        humans_detected_dog_file += 1
print(f'Number of humans detected in human file: {humans_detected_human_file}\nNumber of humans detected in dog file: {humans_detected_dog_file}')

Количество людей, обнаруженных в человеческом файле: 98

Количество людей, обнаруженных в файле с собакой: 17

Обнаружение собак

Чтобы определить, содержит ли изображение собаку, мы будем использовать предварительно обученную модель VGG-16 по умолчанию. Категории, соответствующие собакам в модели VGG-16, появляются в непрерывной последовательности и соответствуют индексам 151–268 включительно. Таким образом, чтобы проверить, предсказывает ли предварительно обученная модель VGG-16 присутствие собаки на изображении, нам нужно только проверить, предсказывает ли предварительно обученная модель индекс от 151 до 268 (включительно).

Изображения должны быть предварительно обработаны в тензоры перед передачей в нейронную сеть, функция process_image ниже обрабатывает эту задачу.

def process_image(image):
"""Processes image to be input into a model for prediction.
    
    Scales, crops, and normalizes a PIL image for a PyTorch model.
        :param image: file path to the image file to be processed.
        :return: a tensor of the processed image.
    """
    
    if not os.path.exists(image):
        raise Exception('Target image could not be found')
image = Image.open(image)
image_norm = transforms.Compose([transforms.Resize(255),
                                    transforms.CenterCrop(244),
                                    transforms.ToTensor(),
                                    transforms.Normalize([0.485, 0.456, 0.406],
                                                        [0.229, 0.224, 0.225])])
    
    return image_norm(image)
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
    '''
#process input image using the function defined above
image = process_image(img_path)  
    
    model = VGG16
    top_k = 1
    
    if torch.cuda.is_available():
            
        device = torch.device("cuda")
        model.to(device)
        image = image.to(device)
else:
        model.cpu()
    model.eval()
    image = image.unsqueeze(0)
with torch.no_grad():
        output = model(image)
        prediction = torch.argmax(output).item()
            
    return prediction
### returns "True" if a dog is detected in the image stored at img_path
def dog_detector(img_path):
    
    result = VGG16_predict(img_path)
    
    return result in range(151, 269)

Функция dog_detector вернет «Истина», если предсказание для изображения находится между 151 и 268 (включительно), в противном случае она вернет «Ложь».

Создайте CNN для классификации пород собак

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

Загрузите и предварительно обработайте набор данных изображения.VGG-16, который будет использоваться в этом проекте, был обучен на наборе данных ImageNet, где каждый цветовой канал был нормализован отдельно. Средние значения и стандартные отклонения изображений всех трех наборов должны быть нормализованы в соответствии с ожиданиями сети. Для средних это [0.485, 0.456, 0.406], а для стандартных отклонений [0.229, 0.224, 0.225], рассчитанных по изображениям ImageNet. Эти значения будут сдвигать каждый цветовой канал, чтобы центрировать его на 0 и варьироваться от -1 до 1. Изображения будут обрезаны в соответствии с требованиями библиотеки Pytorch 224x224 пикселей для входных изображений. Затем набор данных будет разделен на наборы для обучения, проверки и тестирования. Затем к случайным выборкам обучающего набора будет применена дальнейшая предварительная обработка в надежде повысить точность модели. Некоторые из этих предварительных обработок включают поворот изображения, переворот изображения и т. д.

## TODO: Specify data loaders
def data_loader(base_folder='/data/dog_images/'):
"""Load and transform train, validation and test data-sets.
This method will raise a Exception if the base_folder argument provided does not exist.
:param base_folder: folder containing train, validation and test data
:return: dataloader for train, validation and test as well as class to index mapper
"""
if not os.path.exists(base_folder):
            raise Exception('Target folder can not be found')  # raise exception is base_folder is not found
train_dir = base_folder + '/train'
        valid_dir = base_folder + '/valid'
        test_dir = base_folder + '/test'
train_transform = transforms.Compose([transforms.Resize(258),
                                              transforms.RandomRotation(30),
                                              transforms.RandomHorizontalFlip(),
                                              transforms.CenterCrop(244),
                                              transforms.ToTensor(),
                                              transforms.Normalize([0.485, 0.456, 0.406],
                                                                   [0.229, 0.224, 0.225])])
valid_tranform = transforms.Compose([transforms.Resize(255),
                                             transforms.CenterCrop(244),
                                             transforms.ToTensor(),
                                             transforms.Normalize([0.485, 0.456, 0.406],
                                                                  [0.229, 0.224, 0.225])])
test_transform = transforms.Compose([transforms.Resize(255),
                                             transforms.CenterCrop(244),
                                             transforms.ToTensor(),
                                             transforms.Normalize([0.485, 0.456, 0.406],
                                                                  [0.229, 0.224, 0.225])])
train_data = datasets.ImageFolder(train_dir, transform=train_transform)
valid_data = datasets.ImageFolder(valid_dir, transform=valid_tranform)
test_data = datasets.ImageFolder(test_dir, transform=test_transform)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
valid_loader = torch.utils.data.DataLoader(valid_data, batch_size=64)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=64)
return train_loader, valid_loader, test_loader, train_data.class_to_idx

Классификатор модели: поскольку у нас есть 133 класса собак, нам нужно будет создать собственный классификатор, который заменит классификатор предварительно обученной модели. После повторения различных конфигураций я остановился на классификаторе из 3 полносвязных слоев с отсевом 0,2 после каждого из первых 2 слоев, а последний слой выдал 133 класса.

def create_model():
"""Create model from input arguments.
Supports only three pre-trained models: 'vgg16','densenet121' and'alexnet'.
    :param cl_output: number of output predictions from the model. (type: int)
    :param model_name: name of pre-trained model.
    :param hidden_layer: number of hidden layer. (type: int)
    :param learning_rate: learning rate for model. (type: float)
    :return: model, optimizer, criterion
    """
model = models.vgg16(pretrained=True)
classifier_input = model.classifier[0].in_features
    print(classifier_input)
for param in model.parameters():
        param.requires_grad = False
model.classifier = nn.Sequential(nn.Linear(classifier_input, 1024),
                                    nn.ReLU(),
                                    nn.Dropout(0.2),
                                    nn.Linear(1024, 256),
                                    nn.ReLU(),
                                    nn.Dropout(0.2),
                                    
                                    nn.Linear(256, 133),
                                    nn.LogSoftmax(dim=1))
return model
model_transfer = create_model()
criterion_transfer = nn.NLLLoss()
optimizer_transfer = optim.Adam(model_transfer.classifier.parameters(), lr=0.001)

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

Наконец, мы обучаем, проверяем и тестируем нашу модель.

train_loader, valid_loader, test_loader, class_to_idx = data_loader(base_folder='/data/dog_images/')
def validation(model, criterion, test_loader):
"""Use for model validation.
This method can be used for both validation and testing. When used for validation, the test_loader argument
    should point to a validation data. For testing, it should point to a test data.
    :param model: trained model to be used for validation or testing
    :param criterion: criterion to be used
    :param test_loader: folder containing test or validation data
    :return: accuracy and loss.
        """
    
    valid_loss = 0
    accuracy = 0
with torch.no_grad():
for inputs, labels in test_loader:
            inputs, labels = inputs.cuda(), labels.cuda()
            logps = model.forward(inputs)
            batch_loss = criterion(logps, labels)
valid_loss += batch_loss.item()
# Calculate accuracy
            ps = torch.exp(logps)
            top_p, top_class = ps.topk(1, dim=1)
            equals = top_class == labels.view(*top_class.shape)
            accuracy += torch.mean(equals.type(torch.FloatTensor)).item()
return round(accuracy / len(test_loader), 3), round(valid_loss / len(test_loader), 3)
# train the model
def train(epochs, train_loader, model, optimizer, criterion, valid_loader, save_path):
    
    """Train model and performs validation.
This method trains a model and performs validation after every 20 iteration of the train data set. Validation is
    performed using the validation method of this class. train loss, test loss and validation accuracy are printed
    after every 20 iteration of the train data.
    validation is performed is eval mode of the model.
:param optimizer:
    :param model: model to be trained.
    :param criterion: criterion to be used.
    :param train_loader: folder containing training data set.
    :param valid_loader: folder containing validation data set
    :param epochs: number of training epochs.
    """
    
    valid_loss_min = np.Inf 
#     steps = 0
    running_loss = 0
#     print_every = 20
    for epoch in range(epochs):
        for inputs, labels in train_loader:
#                 steps += 1
                # Move input and label tensors to the default device
            inputs, labels = inputs.cuda(), labels.cuda()
optimizer.zero_grad()
logps = model.forward(inputs)
            loss = criterion(logps, labels)
            loss.backward()
            optimizer.step()
running_loss += loss.item()
#                 if steps % print_every == 0:
        model.eval()
accuracy, validation_loss = validation(model, criterion, valid_loader)  # performs validation
print(f"Epoch {epoch + 1}/{epochs}.. "
                f"Train loss: {running_loss:.3f}.. "
                f"Valid loss: {validation_loss}.. "
                f"Validation accuracy: {accuracy}")
        running_loss = 0
        model.train()
    
        if validation_loss < valid_loss_min:
            torch.save(model.state_dict(), save_path)
            print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(
            valid_loss_min,
            validation_loss))
            valid_loss_min = validation_loss
    
    print('Training Completed!!!')
return model
        
model_transfer = train(10, train_loader, model_transfer, optimizer_transfer, criterion_transfer, valid_loader, 'model_transfer.pt')

Тестируем нашу модель:

def test(loaders, model, criterion, use_cuda):
# monitor test loss and accuracy
    test_loss = 0.
    correct = 0.
    total = 0.
model.eval()
    for batch_idx, (data, target) in enumerate(test_loader):
        # move to GPU
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        # forward pass: compute predicted outputs by passing inputs to the model
        output = model(data)
        # calculate the loss
        loss = criterion(output, target)
        # update average test loss 
        test_loss = test_loss + ((1 / (batch_idx + 1)) * (loss.data - test_loss))
        # convert output probabilities to predicted class
        pred = output.data.max(1, keepdim=True)[1]
        # compare predictions to true label
        correct += np.sum(np.squeeze(pred.eq(target.data.view_as(pred))).cpu().numpy())
        total += data.size(0)
            
    print('Test Loss: {:.6f}\n'.format(test_loss))
print('\nTest Accuracy: %2d%% (%2d/%2d)' % (
        100. * correct / total, correct, total))
test(test_loader, model_transfer, criterion_transfer, use_cuda)

Потеря теста: 0,703256

Точность теста: 78% (656/836)

Напишите свой алгоритм

Результатом нашей модели является число, указывающее индекс класса (0–132), к которому принадлежит входное изображение. В идеале мы хотим сопоставить этот индекс с именем, чтобы вместо индекса модель выдавала имя, соответствующее индексу. К счастью, Pytorch предоставляет словарь class_to_idx, который является атрибутом метода DataLoader. Мы берем этот словарь и меняем местами пары ключ-значение, чтобы вместо этого индексы отображались на имена классов. Затем мы сохраняем это в файл JSON.

key_split = [key.split('.')[1] for key in class_to_idx.keys()]
name_to_idx_map = dict(zip(key_split,class_to_idx.values()))
name_to_idx_map_rev = {name_to_idx_map[k]: k for k in name_to_idx_map}
with open('idx_name_map.json', 'w') as file:
    file.write(json.dumps(name_to_idx_map_rev))

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

def predict_breed_transfer(img_path, model, idx_name_path):
    # load the image and return the predicted breed
    
    if not os.path.exists(img_path):
        raise Exception('Target image could not be found')
image = Image.open(img_path)
image_norm = transforms.Compose([transforms.Resize(255),
                                    transforms.CenterCrop(244),
                                    transforms.ToTensor(),
                                    transforms.Normalize([0.485, 0.456, 0.406],
                                                        [0.229, 0.224, 0.225])])
    
    image = image_norm(image)
if torch.cuda.is_available():
        device = torch.device("cuda")
        model.to(device)
        image = image.to(device)
else:
        model.cpu()
        
    model.eval()
image = image.unsqueeze(0)
    
    with torch.no_grad():
        output = model(image)
        prediction = torch.argmax(output).item()
    
    with open('idx_name_map.json', 'r') as file:
        cls_map = json.loads(file.read())
    
    return cls_map[str(prediction)]

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

  • если на изображении обнаружена собака, вернуть предсказанную породу.
  • если на изображении обнаружен человек, вернуть похожую породу собаки.
  • если ни один не обнаружен на изображении, предоставьте вывод, указывающий на ошибку.
def run_app(img_path):
    ## handle cases for a human face, dog, and neither
    if face_detector(img_path):
        print('Hello Human!')
        plt.imshow(Image.open(img_path))
        plt.show()
        result = predict_breed_transfer(img_path, model_transfer, 'idx_name_map.json')
        print(f"You look like a ... {result}")
        print('\n-----------------------------------\n')
    elif dog_detector(img_path):
        plt.imshow(Image.open(img_path))
        plt.show()
        result = predict_breed_transfer(img_path, model_transfer, 'idx_name_map.json')
        print(f'This is a picture of a ... {result}')
        print('\n-----------------------------------\n')
    else:
        plt.imshow(Image.open(img_path))
        plt.show()
        print('Sorry, I did not detect a human or a dog in this image.')
        print('\n-----------------------------------\n')

Проверьте свой алгоритм

Наконец, мы можем протестировать нашу программу на нескольких образцах изображений.

API

Я перешел к разработке программы в API с использованием flask. Полный исходный код можно найти здесь

Возможные улучшения

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

- Использование планировщика скорости обучения для изменения скорости обучения на разных этапах обучения.

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

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

- Увеличьте набор данных и увеличьте количество обучающих данных