Узнайте, как использовать 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
Вот и все! Вы можете использовать класс, который я реализовал, на своих собственных изображениях, получайте удовольствие!