ОБЗОР
Этот проект направлен на разработку классификатора пород собак с использованием сверточной нейронной сети (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. Полный исходный код можно найти здесь
Возможные улучшения
Я считаю, что существуют возможные способы улучшения модели для достижения более высокой точности, но я не мог изучить все эти методы, так как работал в срок.
- Использование планировщика скорости обучения для изменения скорости обучения на разных этапах обучения.
- Другим вариантом является дальнейший анализ изображений, дающих неверные прогнозы во время проверки, чтобы выявить возможный шум.
- Внимательно изучите обучающий набор данных и удалите изображения низкого качества.
- Увеличьте набор данных и увеличьте количество обучающих данных