Определение пород собак с помощью конволюционных нейронных сетей (CNN)

RedDeveloper
07.03.2023 14:10
Определение пород собак с помощью конволюционных нейронных сетей (CNN)

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

Введение

В рамках финального проекта 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()
Face Recognition With  Haar Feature-Based Cascade Classifier .
Face Recognition With Haar Feature-Based Cascade Classifier .

Поскольку нас интересует только обнаружение наличия человеческого лица, мы проверяем, возвращает ли функция 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

Прежде чем использовать трансферное обучение, я покажу, как реализовать простую 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, используя:

  • Два слоя с нулевым заполнением, за которыми следуют слои свертки 2D - таким образом, мы имеем два слоя свертки без потери размера входных данных.
  • Еще одна свертка 2D с последующим слоем maxpooling2D, который уменьшает размер входных данных в два раза.
  • Слой flatten для преобразования входных данных в одномерный массив.
  • Плотный слой с активацией 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
Наконец вот порода на которую по мнению алгоритма я больше всего похожа

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

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

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