В рамках финального проекта Udacity Data Scietist Nanodegree я разработал алгоритм с использованием конволюционных нейронных сетей (CNN) для классификации собак в зависимости от их породы. Алгоритм также работает с человеческими лицами, определяя, на какую породу собак больше всего похож человек.
Написание программы для определения пород собак по изображениям может оказаться непростой задачей - даже человеку трудно определить породу собаки! Для этого нам нужно найти способы извлечения релевантных характеристик из изображения - и использовать эти характеристики для предсказания наиболее вероятной породы собаки.
В этой статье я покажу, как это сделать:
Пошаговая реализация доступна на сайте 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.
Для того чтобы определить, есть ли на предоставленном изображении человеческое лицо, я использовал предварительно обученную модель под названием Haar feature-based cascade classifier . Необходимо преобразовать цветное изображение в серую шкалу:
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)
Получив 4D-тензоры, мы можем использовать их для предсказания того, какой объект присутствует на картинке.
ResNet50 предоставляет словарь с метками (числами) и объектами. Метод predict() модели ResNet50 выдает вероятность для каждой из меток в словаре, представляющую вероятность того, что картинка содержит помеченный объект. Мы используем функцию bellow для определения метки с максимальной вероятностью.
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. Мы увидим, что она не очень эффективна.
Сначала мы преобразуем тестовые/валидные/тренировочные изображения в тензоры:
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, используя:
Что выглядит следующим образом:
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%.
Функции узкого места - это последние карты активации перед полностью подключенными слоями. То есть они являются выходом другой 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). В этот раз я использовал очень простой вариант:
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
Вот и все! Вы можете использовать реализованный мной класс на своих собственных изображениях, получайте удовольствие!
20.08.2023 18:21
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".
20.08.2023 17:46
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
19.08.2023 18:39
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.
19.08.2023 17:22
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!
18.08.2023 20:33
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.
14.08.2023 14:49
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.