Узнайте, как использовать CNN для идентификации собак (и человеческих лиц) на изображениях. Затем используйте предварительно обученный CNN ResNet50 для классификации собак по породам.

Введение

В рамках финального проекта Udacity Data Scietist Nanodegree я разработал алгоритм с использованием сверточных нейронных сетей (CNN) для классификации собак в соответствии с их породой. Алгоритм также работает с человеческими лицами — подсказывает, на какую породу собак больше всего похож человек.

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

В этой статье я покажу, как:

  • Используйте каскадные классификаторы на основе признаков Хаара для обнаружения человеческих лиц на изображениях;
  • Используйте ResNet50 для обнаружения собак на изображениях;
  • Создайте CNN с нуля;
  • Создайте CNN с трансферным обучением, используя ResNet50 для классификации пород собак;
  • Реализуйте алгоритм, который классифицирует породы собак по изображениям.

Пошаговая реализация доступна на https://github.com/fdemacedof/dog_breed_classifier.

Загрузка данных

Загрузка изображений морды собаки и человека:

from sklearn.datasets import load_files       
from keras.utils import np_utils
import numpy as np
from glob import glob

# define function to load train, test, and validation datasets
def load_dataset(path):
    data = load_files(path)
    dog_files = np.array(data['filenames'])
    dog_targets = np_utils.to_categorical(np.array(data['target']), 133)
    return dog_files, dog_targets

# load train, test, and validation datasets
train_files, train_targets = load_dataset('data/dog_images/train')
valid_files, valid_targets = load_dataset('data/dog_images/valid')
test_files, test_targets = load_dataset('data/dog_images/test')

# load list of dog names
dog_names = [item[20:-1] for item in sorted(glob("../../../data/dog_images/train/*/"))]

# print statistics about the dataset
print('There are %d total dog categories.' % len(dog_names))
print('There are %s total dog images.\n' % len(np.hstack([train_files, valid_files, test_files])))
print('There are %d training dog images.' % len(train_files))
print('There are %d validation dog images.' % len(valid_files))
print('There are %d test dog images.'% len(test_files))
There are 133 total dog categories.
There are 8351 total dog images.

There are 6680 training dog images.
There are 835 validation dog images.
There are 836 test dog images.

import random
random.seed(8675309)

# load filenames in shuffled human dataset
human_files = np.array(glob("data/lfw/*/*"))
random.shuffle(human_files)

# print statistics about the dataset
print('There are %d total human images.' % len(human_files))
There are 13233 total human images.

Обнаружение человеческих лиц

Чтобы определить, есть ли на предоставленном изображении человеческое лицо, я использовал предварительно обученную модель под названием Каскадный классификатор Хаара на основе признаков. Необходимо преобразовать цветное изображение в оттенки серого:

import cv2

img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

Затем мы можем использовать функцию detectMultiScale() для обнаружения человеческих лиц на сером изображении. Функция возвращает список координат, по одной для каждого обнаруженного человеческого лица, который можно использовать для поиска лица на изображении:

# loag model
face_cascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_alt.xml')

# find faces in image
faces = face_cascade.detectMultiScale(gray)

# print number of faces detected in the image
print('Number of faces detected:', len(faces))

# get bounding box for each detected face
for (x,y,w,h) in faces:
    # add bounding box to color image
    cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)
    
# convert BGR image to RGB for plotting
cv_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# display the image, along with bounding box
plt.imshow(cv_rgb)
plt.show()

Поскольку нас интересует только обнаружение человеческого лица, мы проверяем, возвращает лиDetectMultiScale() непустой список:

# extract pre-trained face detector
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

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

human_files_short = human_files[:100]
dog_files_short = train_files[:100]

detected_faces_human = 0
for human in human_files_short:
    if face_detector(human):
        detected_faces_human += 1
        
detected_faces_dog = 0
for dog in dog_files_short:
    if face_detector(dog):
        detected_faces_dog += 1
        
print(f"detected faces in {detected_faces_human}% of pictures in human dataset\n")
print(f"detected faces in {detected_faces_dog}% of pictures in dog dataset")
detected faces in 100% of pictures in human dataset

detected faces in 11% of pictures in dog dataset

Детектор собак

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

# import and load restnet50 pre-trained model with imagenet weights
from keras.applications.resnet50 import ResNet50
ResNet50_model = ResNet50(weights='imagenet')

Теперь keras использует TesndorFlow в качестве серверной части — и ему требуются 4D-тензоры (4D-массивы) в качестве входных данных для его CNN. Поэтому нам нужно предварительно обработать изображения, чтобы преобразовать их в 4D-тензор. Я использовал следующие функции:

from keras.preprocessing import image                  
from tqdm import tqdm

def path_to_tensor(img_path):
    # loads RGB image as PIL.Image.Image type
    img = image.load_img(img_path, target_size=(224, 224))
    # convert PIL.Image.Image type to 3D tensor with shape (224, 224, 3)
    x = image.img_to_array(img)
    # convert 3D tensor to 4D tensor with shape (1, 224, 224, 3) and return 4D tensor
    return np.expand_dims(x, axis=0)

# this second function is a handy way to convert a list of images, instead of
# converting one by one
def paths_to_tensor(img_paths):
    list_of_tensors = [path_to_tensor(img_path) for img_path in tqdm(img_paths)]
    return np.vstack(list_of_tensors)

Имея четырехмерные тензоры, мы можем использовать их, чтобы предсказать, какой объект присутствует на картинке.

ResNet50 предоставляет словарь с метками (цифрами) и объектами. Метод Predict() модели ResNet50 дает вероятность для каждой из этих меток в словаре, представляя вероятность изображения, содержащего помеченный объект. Мы используем функцию ниже, чтобы идентифицировать метку с максимальной вероятностью.

from keras.applications.resnet50 import preprocess_input, decode_predictions

def ResNet50_predict_labels(img_path):
    # returns prediction vector for image located at img_path
    img = preprocess_input(path_to_tensor(img_path))
    return np.argmax(ResNet50_model.predict(img))

Поскольку собаки помечены между 151 и 268, если метка максимальной вероятности попадает между этими числами, на картинке есть собака:

### returns "True" if a dog is detected in the image stored at img_path
def dog_detector(img_path):
    prediction = ResNet50_predict_labels(img_path)
    return ((prediction <= 268) & (prediction >= 151)) 

Протестируйте функцию, снова со 100 изображениями собак и еще 100 изображениями людей:

dogs_in_human_dataset = 0
for human in human_files_short:
    if dog_detector(human):
        dogs_in_human_dataset += 1

dogs_in_dog_dataset = 0
for dog in dog_files_short:
    if dog_detector(dog):
        dogs_in_dog_dataset += 1
        
print(f"dogs found in {dogs_in_human_dataset}% of pictures in human dataset")
print(f"dogs found in {dogs_in_dog_dataset}% of picttures in dog dataset")
dogs found in 0% of pictures in human dataset
dogs found in 100% of picttures in dog dataset

Классификация пород собак с CNN

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

Во-первых, мы конвертируем изображения test/valid/train в данные в тензоры:

from PIL import ImageFile                            
ImageFile.LOAD_TRUNCATED_IMAGES = True                 

# pre-process the data for Keras
train_tensors = paths_to_tensor(train_files).astype('float32')/255
valid_tensors = paths_to_tensor(valid_files).astype('float32')/255
test_tensors = paths_to_tensor(test_files).astype('float32')/255

Мы предварительно обработали изображения, чтобы они имели размер 224x224 пикселей. Поскольку это цветные изображения, у них также есть 3 разных канала для цвета (красный, зеленый, синий). Это даст нам формат изображения (224,224,3).

Keras использует 4D-тензоры в качестве входных данных, и, поскольку мы преобразовали изображения, 4D-тензоры будут иметь формат (1 224 224,3).

Это важно, потому что первый слой нашей сверточной сети должен получать входные данные этого формата для работы.

Я сделал следующий пользовательский CNN, используя:

  • Два нулевых заполнения, за которыми следуют 2D-слои свертки — таким образом, у нас есть два слоя свертки без потери размера ввода.
  • Еще одна свертка 2D, за которой следует слой maxpooling2D, который уменьшает размер ввода вдвое.
  • Сглаживание слоя для преобразования входных данных в одномерный массив.
  • Плотный слой с активацией ReLu, выпадение с вероятностью 0,2
  • Плотный слой активации softmax со 133 (количество пород в тренировочном наборе)

Что выглядит так:

rom keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D, ZeroPadding2D
from keras.layers import Dropout, Flatten, Dense
from keras.models import Sequential

model=Sequential()

model.add(ZeroPadding2D((1,1),input_shape=(224,224,3)))
model.add(Conv2D(32,kernel_size=(3,3),activation='relu'))
model.add(ZeroPadding2D(padding=(1,1)))
model.add(Conv2D(32,kernel_size=(3,3),activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))

model.add(Flatten())
model.add(Dense(64,activation='relu'))
model.add(Dropout(0.2))

model.add(Dense(133,activation='softmax'))

# model.compile(loss=categorical_crossentropy,optimizer='adam',metrics=['accuracy'])
model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
zero_padding2d_1 (ZeroPaddin (None, 226, 226, 3)       0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 224, 224, 32)      896       
_________________________________________________________________
zero_padding2d_2 (ZeroPaddin (None, 226, 226, 32)      0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 224, 224, 32)      9248      
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 112, 112, 32)      0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 401408)            0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                25690176  
_________________________________________________________________
dropout_1 (Dropout)          (None, 64)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 133)               8645      
=================================================================
Total params: 25,708,965
Trainable params: 25,708,965
Non-trainable params: 0
_________________________________________________________________

Скомпилируйте и обучите модель:

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

from keras.callbacks import ModelCheckpoint  

### TODO: specify the number of epochs that you would like to use to train the model.

epochs = 5

### Do NOT modify the code below this line.

checkpointer = ModelCheckpoint(filepath='saved_models/weights.best.from_scratch.hdf5', 
                               verbose=1, save_best_only=True)

model.fit(train_tensors, train_targets, 
          validation_data=(valid_tensors, valid_targets),
          epochs=epochs, batch_size=20, callbacks=[checkpointer], verbose=1)

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

Теперь проверяем точность модели:

# get index of predicted dog breed for each image in test set
dog_breed_predictions = [np.argmax(model.predict(np.expand_dims(tensor, axis=0))) for tensor in test_tensors]

# report test accuracy
test_accuracy = 100*np.sum(np.array(dog_breed_predictions)==np.argmax(test_targets, axis=1))/len(dog_breed_predictions)
print('Test accuracy: %.4f%%' % test_accuracy)
Test accuracy: 5.1435%

Что было не очень хорошо, набрав лишь немногим более 5%.

Использование предварительно обученных функций ResNet50 узких мест CNN

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

### TODO: Obtain bottleneck features from another pre-trained CNN.

bottleneck_features = np.load('bottleneck_features/DogResnet50Data.npz')
train_DogResnet50 = bottleneck_features['train']
valid_DogResnet50 = bottleneck_features['valid']
test_DogResnet50 = bottleneck_features['test']

Узким местом является формат (1,2048). На этот раз я использовал очень простой:

  • GlobalAveragePooling2D с входным размером (1,2048);
  • Dense с активацией softmax для получения вывода размером 133.
DogResnet50_model = Sequential()

DogResnet50_model.add(GlobalAveragePooling2D(input_shape=train_DogResnet50.shape[1:]))
DogResnet50_model.add(Dense(133,activation='softmax'))

DogResnet50_model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
global_average_pooling2d_13  (None, 2048)              0         
_________________________________________________________________
dense_14 (Dense)             (None, 133)               272517    
=================================================================
Total params: 272,517
Trainable params: 272,517
Non-trainable params: 0
_________________________________________________________________

Мы компилируем, обучаем и тестируем модель, как и раньше:

DogResnet50_model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

### TODO: Train the model.

checkpointer = ModelCheckpoint(filepath='saved_models/weights.best.DogResnet50.hdf5', 
                               verbose=1, save_best_only=True)

DogResnet50_model.fit(train_DogResnet50, train_targets, 
          validation_data=(valid_DogResnet50, valid_targets),
          epochs=20, batch_size=20, callbacks=[checkpointer], verbose=1)

DogResnet50_model.load_weights('saved_models/weights.best.DogResnet50.hdf5')

### TODO: Calculate classification accuracy on the test dataset.
#make predictions 
DogResnet50_predictions = [np.argmax(DogResnet50_model.predict(np.expand_dims(feature, axis=0))) for feature in test_DogResnet50]

# report test accuracy
test_accuracy = 100*np.sum(np.array(DogResnet50_predictions)==np.argmax(test_targets, axis=1))/len(DogResnet50_predictions)
print('Test accuracy: %.4f%%' % test_accuracy)
Test accuracy: 77.1531%

Точность может варьироваться от запуска к запуску, но она работает намного лучше, чем наша CNN с нуля!

Теперь давайте сохраним нашу окончательную модель и используем ее для реализации пригодной для использования программы:

DogResnet50_model.save('saved_models/DogResnet50_model')

Собираем все вместе

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

Пользователи могут запускать метод DogBreedClassifier.classify. Он получает путь к изображению и:

  • Определяет, есть ли на изображении лицо собаки или человека. Если нет, программа останавливается;
  • Если это собака, классифицируйте по породе собак;
  • Если это человек, скажите нам, на какую породу он больше всего похож.
### TODO: Write your algorithm.
### Feel free to use as many code cells as needed.

import keras
from keras.preprocessing import image                  
from keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions
from tqdm import tqdm
import cv2                
import matplotlib.pyplot as plt 
import numpy as np

class DogBreedClassifier():
    '''
    Detect and classify dog breeds from images.
    
    Attributes
    ----------
    model: keras.models.Sequential
        CNN model for classifying dog breeds - uses ResNet50 bottleneck features.
    
    Methods
    -------
    classify(image_path)
        if a dog is detected in the image, return the predicted breed;
        if a human is detected in the image, return the resembling dog breed;
        if neither is detected in the image, provide output that indicates an error.
    
    '''    
    
    def __init__(self):
        self.model = keras.models.load_model('saved_models/DogResnet50_model')
        self.face_cascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_alt.xml')
        self.ResNet50_model = ResNet50(weights='imagenet')
        self.dog_names = dog_names = [item[20:-1] for item in sorted(glob("../../../data/dog_images/train/*/"))]
        
    def __str__(self):
        return f'{self.model}'

    def __path_to_tensor(self, img_path):
        '''
        Takes a string-valued file path to a color image as input and returns a 4D tensor.
        
        INPUT:
        img_path (str) path to image
        
        RETURNS:
        tensor (numpy.ndarray) 4D image tensor
        '''
        # loads RGB image as PIL.Image.Image type
        img = image.load_img(img_path, target_size=(224, 224))
        # convert PIL.Image.Image type to 3D tensor with shape (224, 224, 3)
        x = image.img_to_array(img)
        # convert 3D tensor to 4D tensor with shape (1, 224, 224, 3) and return 4D tensor
        tensor = np.expand_dims(x, axis=0) 
        return tensor

    def __extract_Resnet50(self, tensor):
        '''
        Takes a tensor and extract Resnet50 bottleneck features.
        
        INPUT:
        tensor (numpy.ndarray) 4D image tensor.
        
        RETURNS:
        (numpy.ndarray) array of extracted bottleneck features for Resnet50        
        '''
        return ResNet50(weights='imagenet', include_top=False).predict(preprocess_input(tensor))
        
    def __face_detector(self, img_path):
        '''
        Takes a path to image file, loads the image and returns True if any human face is detected.
        
        INPUT:
        img_path (str) path to image file.
        
        RETURNS:
        (bool) True if any face is detected, False otherwise.        
        '''
        img = cv2.imread(img_path)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(gray)        
        return len(faces) > 0
    
    def __ResNet50_predict_labels(self, img_path):
        '''
        Takes a path to image file, preprocesses input, uses Resnet50 to classify the image and returns Resnet50 label with maximum probability.
        
        INPUT:
        img_path (str) path to image file.
        
        RETURNS:
        (int) label with maximum probability as classified by Resnet50_model.predict().        
        '''
        # returns prediction vector for image located at img_path
        img = preprocess_input(self.__path_to_tensor(img_path))
        return np.argmax(self.ResNet50_model.predict(img))

    def __dog_detector(self, img_path):
        '''
        Takes path to image file and returns True if a dog is detected.
        
        INPUT:
        img_path (str) path to image file.
        
        RETURNS:
        (bool) True if any dog is detected and False otherwise.
        '''
        prediction = self.__ResNet50_predict_labels(img_path)
        return ((prediction <= 268) & (prediction >= 151))
    
    
    def __DogResNet50_predict_breed(self, img_path):
        '''
        Takes path to image with a dog and human face and determines dog breed.
        
        INPUT:
        img_path (str) path to image file.
        
        RETURNS:
        dod_breed (str) name of a dog breed.
        '''
        # extract bottleneck features
        bottleneck_feature = self.__extract_Resnet50(self.__path_to_tensor(img_path))
        # obtain predicted vector
        predicted_vector = self.model.predict(bottleneck_feature)
        # return dog breed that is predicted by the model
        dog_breed = self.dog_names[np.argmax(predicted_vector)][15:].lower().replace("_"," ")
        return dog_breed

    def classify(self, img_path):
        '''
        Takes path to image file, determines if there is a dog or human face, prints the image and the dog breed (or dog breed thar most resembles human face).
        
        INPUT:
        img_path (str) path to image file.
        '''
        # print picture
        # print image
        
        img = cv2.imread(img_path)
        # convert BGR image to RGB for plotting
        cv_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

        # display the image
        plt.imshow(cv_rgb)
        plt.show()
        
        # checks if there is a dog or human face in the image
        dog_detected = self.__dog_detector(img_path)
        face_detected = self.__face_detector(img_path)
        if (dog_detected or face_detected) == 0:
            return "ERROR: no dog nor human face found - aborting..." 
        if dog_detected:
            print("found dog in picture!")        
        if face_detected:
            print("found human face in picture!")       
        
        breed = self.__DogResNet50_predict_breed(img_path)
        
        if dog_detected:
            print(f"dog in image is a {breed}")
        if face_detected:
            print(f"human in picture resembles a {breed}")

Загрузите пакет:

# load DogBreedClassifier

breed_classifier = DogBreedClassifier()

Я проверил это на некоторых изображениях:

breed_classifier.classify("images/Labrador_retriever_06449.jpg")

found dog in picture!

dog in image is a flat-coated retriever

breed_classifier.classify("images/Welsh_springer_spaniel_08203.jpg")
found dog in picture!
dog in image is a welsh springer spaniel

Однако на собаку моей семьи это не подействовало.

breed_classifier.classify("images/baby_totoro.jpg")
found dog in picture!
dog in image is a english cocker spaniel

Наконец, это та порода, на которую, по мнению алгоритма, я больше всего похожа…

found human face in picture!
human in picture resembles a bullmastiff

Вот и все! Вы можете использовать класс, который я реализовал, на своих собственных изображениях, получайте удовольствие!