Cats vs Dogs — это проект начального уровня для классификации изображений. Может ли предварительно обученная нейронная сеть Resnet V2 точно идентифицировать собак и кошек на изображении?

Примечание!

Эта статья является копией и вставкой моего блокнота Kaggle: Computer Vision: 🐱Cats vs Dogs🐶 с Resnet V2 101

Введение

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

В этой записной книжке мы будем использовать набор данных Microsoft’s Cats-vs-Dogs. Этот набор данных, состоящий из набора изображений кошек и собак, обычно используется для обучения нейронных сетей задачам бинарной классификации.

Для решения этой задачи мы будем использовать нейронную сеть pre-trained ResNet V2 101. Это очень мощная модель, известная своей точностью в задачах классификации изображений. Загружая модель из моделей Kaggle, мы используем ее предварительно обученные веса, полученные в результате обширного обучения на большом количестве изображений. Это позволяет нам иметь прочную основу для нашей конкретной задачи классификации.

В приведенных ниже функциях мы импортируем все необходимые библиотеки и определяем некоторые полезные функции.

# Importing Libraries 

# Numpy and Pandas
import numpy as np
import pandas as pd

# Plotly for Data-Viz
from plotly.subplots import make_subplots
import plotly.subplots as sp
import plotly.graph_objs as go
import plotly.express as px
from plotly.offline import init_notebook_mode
init_notebook_mode(connected=True)

# Library for OS interactivity
import os

# Image library
from PIL import Image

# Rnadom generations lib
import random

# TensorFlow for Deep Learning
import tensorflow as tf
import tensorflow_hub as hub
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.utils import plot_model

# Creating temporary files and directories
import tempfile

# Module for copying files
from shutil import copyfile

# Module for finding all the pathnames matching a specified pattern
import glob

# Ignore warnings.
import warnings
warnings.filterwarnings("ignore")
def plot_images_list(images, title):
    '''
    This functions helps to plot a matrix of images in a list
    '''
    fig = sp.make_subplots(rows=3, cols=3)
    
    for i in range(min(9, len(images))):
        img = go.Image(z=images[i])
        fig.add_trace(img, row=i//3+1, col=i%3+1)

    fig.update_layout(
        title={'text': f'<b>{title}<br> <sub>Image matrix</sub></b>'},
        height=950,
        width=950,
        margin=dict(t=100, l=80),
        template='simple_white'
    )
    fig.show()
def plot_images_from_generator(images, labels, title):
    '''
    This functions helps to plot a matrix of images and their labels
    '''
    subplot_titles = [label_map[int(labels[i])] for i in range(min(9, len(images)))]

    fig = sp.make_subplots(rows=3, cols=3, subplot_titles=subplot_titles)

    for i in range(min(9, len(images))):
        img_data = np.clip(images[i] * 255, 0, 255).astype(np.uint8)
        img = go.Image(z=img_data)
        fig.add_trace(img, row=i//3+1, col=i%3+1)

    fig.update_layout(
        title={'text': f'<b>{title}<br> <sub>Image matrix</sub></b>'},
        height=950,
        width=950,
        margin=dict(t=150, l=80),
        template='simple_white'
    )
    fig.show()

Исследовательский анализ

Мы начинаем записную книжку с определения каталогов для изображений, содержащих изображения кошек и собак, cat_dir и dog_dir.

Функция os.listdir( ) получает список всех имен файлов в каждом из каталогов для кошек и собак. Наконец, функция random.sample( ) выбирает 9 имен файлов для каждого списка файлов кошек и собак, чтобы позже отобразить случайные образцы изображений для каждой метки домашних животных.

cat_dir = '/kaggle/input/microsoft-catsvsdogs-dataset/PetImages/Cat'
dog_dir = '/kaggle/input/microsoft-catsvsdogs-dataset/PetImages/Dog'

cat_files = os.listdir(cat_dir)
dog_files = os.listdir(dog_dir)

cat_files = random.sample(cat_files, 9)
dog_files = random.sample(dog_files, 9)

В ячейке ниже мы создаем список из Image объектов. С помощью Image.open() мы можем открыть и визуализировать файл изображения.

cat_images = [Image.open(os.path.join(cat_dir, f)) for f in cat_files]
dog_images = [Image.open(os.path.join(dog_dir, f)) for f in dog_files]

Давайте теперь посмотрим на случайные образцы изображений в каталоге кошек и в каталоге собак.

Предварительная обработка

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

Во-первых, мы собираемся создать временный базовый каталог с именем cats-v-dogs. Впредь мы создаем подкаталоги, в которых будем сохранять файлы изображений как для собак, так и для кошек, разделенные на три отдельных набора: обучение, проверка и тест.

base_dir = '/tmp/cats-v-dogs' # Base directory
# Subdirectories
sub_dirs = ['training/cat', 
            'training/dog', 
            'validation/cat', 
            'validation/dog', 
            'test/cat', 
            'test/dog']

# Adding the sub_dirs into the base_dir
for sub_dir in sub_dirs:
    os.makedirs(os.path.join(base_dir, sub_dir), exist_ok=True)
# Creating a directory for each set for cats
training_cats_dir = os.path.join(base_dir, 'training/cat')
validation_cats_dir = os.path.join(base_dir, 'validation/cat')
test_cats_dir = os.path.join(base_dir, 'test/cat')

# Creating a directory for each set for dogs
training_dogs_dir = os.path.join(base_dir, 'training/dog')
validation_dogs_dir = os.path.join(base_dir, 'validation/dog')
test_dogs_dir = os.path.join(base_dir, 'test/dog')

Сначала наши только что созданные папки совершенно пусты. Вы можете увидеть это, распечатав длину списка имен файлов в '/training/cat'.

len(os.listdir('/tmp/cats-v-dogs/training/cat'))
0

Приведенная ниже функция split_data предназначена для разделения набора данных на наборы для обучения, проверки и тестирования.

Сначала он собирает все пути к файлам из базового каталога. После этого он случайным образом перемешивает эти пути к файлам.

Затем он определяет индексы, по которым нужно разделить данные, и определяет, что обучающий набор будет содержать первые 80% файлов, а проверочный и тестовый наборы будут содержать следующие 10% каждый.

Наконец, мы копируем пути к файлам в их каталоги.

def split_data(base_dir, training_dir, validation_dir, test_dir, split_size=0.8):
    files = glob.glob(os.path.join(base_dir, '*'))
    
    np.random.shuffle(files)

    train_idx = int(len(files) * split_size)
    val_idx = int(len(files) * (split_size + (1 - split_size) / 2))

    train_files = files[:train_idx]
    val_files = files[train_idx:val_idx]
    test_files = files[val_idx:]

    for file in train_files:
        copyfile(file, os.path.join(training_dir, os.path.basename(file)))
    for file in val_files:
        copyfile(file, os.path.join(validation_dir, os.path.basename(file)))
    for file in test_files:
            copyfile(file, os.path.join(test_dir, os.path.basename(file)))
# Applying fuction to the 'cats' directories
split_data(cat_dir,
          training_cats_dir,
          validation_cats_dir,
          test_cats_dir)
# Applying fuction to the 'dogs' directories
split_data(dog_dir,
          training_dogs_dir,
          validation_dogs_dir,
          test_dogs_dir)

Теперь мы можем вывести общую длину списков имен файлов в каждом каталоге.

print('Cat files by directories: \n')
print('\n')
print(f"\nTraining Directory: {format(len(os.listdir('/tmp/cats-v-dogs/training/cat')), ',')} files")
print(f"\nValidation Directory: {format(len(os.listdir('/tmp/cats-v-dogs/validation/cat')), ',')} files")
print(f"\nTest Directory: {format(len(os.listdir('/tmp/cats-v-dogs/test/cat')), ',')} files")
Cat files by directories: 




Training Directory: 10,000 files

Validation Directory: 1,250 files

Test Directory: 1,251 files
print('Dog files by directories: \n')
print('\n')
print(f"\nTraining Directory: {format(len(os.listdir('/tmp/cats-v-dogs/training/dog')), ',')} files")
print(f"\nValidation Directory: {format(len(os.listdir('/tmp/cats-v-dogs/validation/dog')), ',')} files")
print(f"\nTest Directory: {format(len(os.listdir('/tmp/cats-v-dogs/test/dog')), ',')} files")
Dog files by directories: 




Training Directory: 10,000 files

Validation Directory: 1,250 files

Test Directory: 1,251 files

Приведенная ниже функция remove_corrupted_images предназначена для поиска поврежденных файлов и их удаления из каталогов. Это сделано для того, чтобы избежать проблем во время обучения.

Функция os.walk() используется для прохода сверху вниз или снизу вверх для каждого каталога в цикле for, инициированном в начале.

При обнаружении поврежденного файла или файла, не являющегося изображением, функция возвращает распечатку с указанием имени этого файла. Наконец, используя os.remove(), мы удаляем эти плохие файлы из наших каталогов.

def remove_corrupted_images(dir_path):
    for subdir, dirs, files in os.walk(dir_path):
        for file in files:
            file_path = os.path.join(subdir, file)
            try:
                img = Image.open(file_path) # open the image file
                img.verify() # verify that it is, in fact an image
            except (IOError, SyntaxError) as e:
                print('Bad file:', file_path) # print out the names of corrupt files
                os.remove(file_path)

remove_corrupted_images('/tmp/cats-v-dogs')
Bad file: /tmp/cats-v-dogs/training/dog/11702.jpg
Bad file: /tmp/cats-v-dogs/training/dog/Thumbs.db
Bad file: /tmp/cats-v-dogs/training/cat/Thumbs.db
Bad file: /tmp/cats-v-dogs/training/cat/666.jpg

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

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

В нашем случае мы используем параметр rescale для изменения масштаба значений пикселей из диапазона 0–225 в диапазон 0–1. По сути, мы нормализуем входные данные, чтобы они правильно передавались в нейронную сеть.

train_datagen = ImageDataGenerator(rescale=1./255)
val_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

Теперь мы можем успешно использовать flow_from_directory для чтения изображений, их предварительной обработки, организации их в пакеты и подачи их в модель Keras.

Вот что делает каждый параметр:

.target_size: определяет размеры, до которых будут изменены все изображения (высота 227 x ширина 227).

. batch_size: размер пакетов данных

.class_mode: определяет тип возвращаемых массивов меток. В данном случае это одномерные бинарные метки.

.shuffle: Перетасовывать ли данные. Для обучения и проверки установлено значение True, чтобы модель получала разнообразные данные в разном порядке, и False для проверки, чтобы данные сохранялись в исходном порядке.

.classes : соответствует списку имен классов classes.

classes = ['cat', 'dog']
train_generator = train_datagen.flow_from_directory(
    os.path.join(base_dir, 'training'),
    target_size=(224, 224), 
    batch_size=32,
    class_mode='binary',
    shuffle=True,
    classes=classes 
)

validation_generator = val_datagen.flow_from_directory(
    os.path.join(base_dir, 'validation'),
    target_size=(224, 224),
    batch_size=32,
    class_mode='binary',
    shuffle=True,
    classes=classes 
)

test_generator = test_datagen.flow_from_directory(
    os.path.join(base_dir, 'test'),
    target_size=(224, 224),
    batch_size=32,
    class_mode='binary',
    shuffle=False,
    classes=classes 
)
Found 19996 images belonging to 2 classes.
Found 2500 images belonging to 2 classes.
Found 2502 images belonging to 2 classes.

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

train_images, train_labels = next(train_generator)
val_images, val_labels = next(validation_generator)
label_map = {v: k for k, v in train_generator.class_indices.items()}

Теперь мы можем построить набор изображений поезда и проверки.

Моделирование

Теперь мы готовы начать процесс обучения нашей модели для классификации изображений как кошек или собак.

Мы строим последовательную модель, используя класс Keras Sequential, укладывая слои линейно, один за другим.

Затем мы используем TensorFlow Hub для загрузки предварительно обученной модели ResNet V2. TensorFlow Hub служит хранилищем для многих предварительно обученных моделей.

Наконец, мы используем метод build, чтобы установить входную форму для слоев. Наша модель предназначена для приема четырехмерных тензоров в качестве входных данных, при этом второе и третье измерения представляют собой высоту и ширину изображения (оба 224), а четвертое измерение представляет количество цветовых каналов (3 для красного, зеленого и синего). Для первого измерения установлено значение «Нет», чтобы учесть переменный размер партии.

# Importing and building model
model = tf.keras.Sequential([
    hub.KerasLayer('https://www.kaggle.com/models/google/resnet-v2/frameworks/TensorFlow2/variations/101-classification/versions/2')
])
model.build([None, 224, 224, 3])

Модель ResNet 101 V2 — это вариант архитектуры ResNet (Residual Network), популярной модели глубокого обучения, предназначенной для многих задач распознавания изображений. 101 относится к глубине сети, т. е. состоит из 101 слоя. С другой стороны, V2 указывает, что это вторая версия модели ResNet с улучшенной производительностью.

Когда мы используем hub.KerasLayer('...'), мы загружаем предварительно обученную модель из TensorFlow Hub с предопределенными весами. Принимая во внимание, что когда мы просто используем from tensorflow.keras.applications import ResNet101V2, мы импортируем модель без предварительно определенных весов, что может потребовать дальнейшего обучения. Согласно описанию модели здесь на Kaggle, веса модели, которую мы здесь используем, 'были первоначально получены путем обучения набору данных ILSVRC-2012-CLS для классификации изображений ("Imagenet").

Прямо ниже я нарисовал архитектуру модели ResNet 101 V2, которая может помочь в дальнейшем понимании того, как работает эта модель.

Поскольку мы используем предварительно обученную модель, которая была обучена на большом наборе данных, мы установим для trainable значение False, чтобы существующие слои в модели не изменялись во время обучения. учитывая, что они уже изучили полезные функции.

Однако мы собираемся добавить два новых слоя:

Сначала мы добавляем в модель новый слой Dense. Этот слой имеет 512 нейронов и использует функцию активации ReLU.

Во-вторых, мы добавляем еще один слой Dense всего с одним нейроном. Это выходной слой нашей модели. Он использует сигмовидную функцию активации для вывода значения от 0 до 1, эффективно классифицируя входные изображения.

for layer in model.layers:
    layer.trainable = False

model.add(tf.keras.layers.Dense(512, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))  # 'sigmoid' function used for binary classification

Затем мы можем скомпилировать модель для настройки всего процесса обучения.

Сначала мы определяем функцию потерь, которую модель попытается минимизировать для повышения производительности. Функция binary_crossentropy — очень распространенная функция для задач бинарной классификации. Это просто логарифмическая потеря, и она определяется следующим уравнением:

Где:

. L(y,ŷ)– функция потерь, оцениваемая между истинными значениями y и прогнозируемые значения ŷ.

. N — это просто количество образцов.

. yᵢ — это истинная метка для образца iᵗʰ.

. ŷ – прогнозируемая вероятность того, что выборка iᵗʰ относится к классу = 1.

. ∑ представляет собой сумму по всем образцам в партии.

Мы также собираемся определить adam как оптимизатор. Это также широко используемый алгоритм оптимизации для минимизации функции потерь.

Наконец, в metrics мы определяем accuracy для оценки модели во время тренировки и отдыха. Точность просто дает нам процент правильно предсказанных меток.

model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

Мы также собираемся определить две функции обратного вызова. Во-первых, мы используем ModelCheckpoint для сохранения лучших весов после каждой эпохи в соответствии с точностью проверочного набора.

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

checkpoint_cb = tf.keras.callbacks.ModelCheckpoint("best_model.h5", 
                                                   save_best_only=True, 
                                                   monitor='val_accuracy',
                                                   verbose = 1)

early_stopping_cb = tf.keras.callbacks.EarlyStopping(monitor='val_loss', 
                                                     patience=5,
                                                     verbose = 1)

Наконец-то мы можем начать тренироваться, используя model.fit()!

history = model.fit(train_generator,
                    epochs=20,
                    validation_data=validation_generator,
                    callbacks=[checkpoint_cb, early_stopping_cb])
Epoch 1/20
625/625 [==============================] - ETA: 0s - loss: 0.0548 - accuracy: 0.9856
Epoch 1: val_accuracy improved from -inf to 0.99480, saving model to best_model.h5
625/625 [==============================] - 128s 172ms/step - loss: 0.0548 - accuracy: 0.9856 - val_loss: 0.0180 - val_accuracy: 0.9948
Epoch 2/20
625/625 [==============================] - ETA: 0s - loss: 0.0183 - accuracy: 0.9939
Epoch 2: val_accuracy did not improve from 0.99480
625/625 [==============================] - 106s 169ms/step - loss: 0.0183 - accuracy: 0.9939 - val_loss: 0.0317 - val_accuracy: 0.9880
Epoch 3/20
625/625 [==============================] - ETA: 0s - loss: 0.0112 - accuracy: 0.9958
Epoch 3: val_accuracy did not improve from 0.99480
625/625 [==============================] - 107s 170ms/step - loss: 0.0112 - accuracy: 0.9958 - val_loss: 0.0614 - val_accuracy: 0.9872
Epoch 4/20
625/625 [==============================] - ETA: 0s - loss: 0.0122 - accuracy: 0.9959
Epoch 4: val_accuracy did not improve from 0.99480
625/625 [==============================] - 107s 171ms/step - loss: 0.0122 - accuracy: 0.9959 - val_loss: 0.0383 - val_accuracy: 0.9908
Epoch 5/20
625/625 [==============================] - ETA: 0s - loss: 0.0087 - accuracy: 0.9971
Epoch 5: val_accuracy did not improve from 0.99480
625/625 [==============================] - 106s 170ms/step - loss: 0.0087 - accuracy: 0.9971 - val_loss: 0.0417 - val_accuracy: 0.9900
Epoch 6/20
625/625 [==============================] - ETA: 0s - loss: 0.0074 - accuracy: 0.9973
Epoch 6: val_accuracy did not improve from 0.99480
625/625 [==============================] - 106s 170ms/step - loss: 0.0074 - accuracy: 0.9973 - val_loss: 0.0347 - val_accuracy: 0.9904
Epoch 6: early stopping

Ниже мы можем видеть графики, отображающие как точность, так и потери по эпохам.

Важно отметить, что, глядя на accuracy over epochs, мы стремимся к максимально возможному значению. Глядя на loss over epochs, мы стремимся к наименьшему возможному значению.

💡 Наивысшая точность на проверочном наборе была получена в течение первой эпохи.

💡 Наименьшие потери на проверочном наборе также были получены в течение первой эпохи.

Ниже мы загружаем лучшие веса, полученные в процессе обучения и проверки. Затем мы можем предсказать метки на test_generator и измерить точность обучающих пакетов.

model.load_weights('best_model.h5')
predictions = model.predict(test_generator)
predicted_labels = (predictions >= 0.5).astype(int)
actual_labels = test_generator.classes

accuracy = np.mean(predicted_labels.flatten() == actual_labels)
print(f"Accuracy: {accuracy * 100:.2f}%")
79/79 [==============================] - 12s 149ms/step
Accuracy: 98.68%

💡 Мы правильно предсказали 98,68 % этикеток в тестовых партиях.

Наконец, ниже мы запускаем прогнозы для одной партии test_generator и строим изображения в этой партии с истинными и предсказанными метками.

test_images, test_labels = next(test_generator)
predicted_probs = model.predict(test_images)
predicted_labels = (predicted_probs >= 0.5).astype(int)
plot_images_from_generator(test_images, test_labels, predicted_labels, "Test Images, Labels & Predictions")
1/1 [==============================] - 0s 40ms/step

💡 Всем изображениям, кроме первого, были правильно присвоены ярлыки.

Развертывание

Для развертывания модели мы используем метод .save(). Модель .ℎ5 будет сохранена в папке Output здесь, на Kaggle, в /kaggle/working, которую вы можете легко загрузить.

model.save('cats_vs_dogs.h5') # Saving model for deployment

Я использовал модель, сохраненную выше, для создания приложения с Gradio.

Вы можете получить доступ и протестировать приложение, а также проверить файлы и то, как я их закодировал, в репозитории 🐱 x 🐶 Распознавание изображений — Кошки против собак с Resnet 101 V2 🐱 x 🐶, доступном на Hugging Face.

Заключение

В приведенном выше исследовании мы приступили к одной из самых известных задач в Computer Vision — задаче классификации изображений «Кошки против собак».

Мы изучили изображения в наших данных, а затем выполнили эффективную предварительную обработку данных для использования этих изображений в качестве входных данных для предварительно обученной модели ResNet 101 V2, доступной здесь, на Kaggle.

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

Я также предоставил ссылку на онлайн-приложение Hugging Face, где развернул эту модель. Вы можете использовать его для бесплатной загрузки изображений кошек и собак и получения прогноза от модели.

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

Спасибо за чтение.

Луис Фернандо Торрес

Подключаемся!🔗
LinkedInKaggleHuggingFace