Научитесь настраивать типичный сквозной конвейер для обучения CNN

В этой статье мы рассмотрим сквозной конвейер обучающих сверточных нейронных сетей, то есть организацию данных в каталогах, предварительную обработку, увеличение данных, построение модели и т. Д.

Мы потратим много времени на методы предварительной обработки данных, обычно используемые при обработке изображений. Это связано с тем, что предварительная обработка занимает около 50–80% вашего времени в большинстве проектов глубокого обучения, и знание некоторых полезных приемов очень поможет вам в ваших проектах. Мы будем использовать набор данных цветов от Kaggle, чтобы продемонстрировать ключевые концепции. Для непосредственного доступа к кодам на Kaggle опубликована соответствующая записная книжка (пожалуйста, используйте CPU для запуска начальных частей кода и GPU для обучения модели).



Импорт набора данных

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

# Importing necessary libraries
import keras
import tensorflow
from skimage import io
import os
import glob
import numpy as np
import random
import matplotlib.pyplot as plt
%matplotlib inline
# Importing and Loading the data into data frame
#class 1 - Rose, class 0- Daisy
DATASET_PATH = '../input/flowers-recognition/flowers/'
flowers_cls = ['daisy', 'rose']

# glob through the directory (returns a list of all file paths)
flower_path = os.path.join(DATASET_PATH, flowers_cls[1], '*')
flower_path = glob.glob(flower_path)
# access some element (a file) from the list
image = io.imread(flower_path[251])

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

Изображения - каналы и размеры

Изображения бывают разных форм и размеров . Они также поступают из разных источников. Например, некоторые изображения мы называем «естественными изображениями», что означает, что они сделаны в цвете в реальном мире. Например:

  • Изображение цветка - это естественный образ.
  • Рентгеновское изображение не является естественным.

Принимая во внимание все эти вариации, нам необходимо выполнить некоторую предварительную обработку любых данных изображения. RGB - самый популярный формат кодирования, и большинство «естественных изображений», с которыми мы сталкиваемся, находятся в RGB. Кроме того, одним из первых шагов предварительной обработки данных является создание изображений одинакового размера. Давайте перейдем к тому, как мы можем изменить форму и форму изображений.

# plotting the original image and the RGB channels
f, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, sharey=True)
f.set_figwidth(15)
ax1.imshow(image)

# RGB channels
# CHANNELID : 0 for Red, 1 for Green, 2 for Blue. 
ax2.imshow(image[:, : , 0]) #Red
ax3.imshow(image[:, : , 1]) #Green
ax4.imshow(image[:, : , 2]) #Blue
f.suptitle('Different Channels of Image')

Морфологические преобразования

Термин морфологическое преобразование относится к любому изменению, касающемуся формы и формы изображений. Они очень часто используются в задачах анализа изображений. Хотя они используются со всеми типами изображений, они особенно эффективны для изображений, которые не являются естественными (поступают из источника, отличного от изображения реального мира). Типичные преобразования - это эрозия, расширение, раскрытие и закрытие. Давайте теперь посмотрим на код для реализации этих морфологических преобразований.

1. пороговое значение

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

# bin_image will be a (240, 320) True/False array
#The range of pixel varies between 0 to 255
#The pixel having black is more close to 0 and pixel which is white is more close to 255
# 125 is Arbitrary heuristic measure halfway between 1 and 255 (the range of image pixel) 
bin_image = image[:, :, 0] > 125
plot_image([image, bin_image], cmap='gray')

2. Эрозия, расширение, открытие и закрытие

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

Открытие - это эрозия, за которой следует расширение. Открытие может удалить небольшие яркие пятна (например, «соль») и соединить небольшие темные трещинки. Это имеет тенденцию «открывать» (темные) промежутки между (яркими) элементами.

Закрытие - расширение, за которым следует эрозия. Закрытие позволяет удалить небольшие темные пятна (например, «перец») и соединить небольшие светлые трещинки. Это имеет тенденцию «закрывать» (темные) промежутки между (яркими) объектами.

Все это можно сделать с помощью модуля skimage.morphology. Основная идея состоит в том, чтобы перемещать круглый диск определенного размера (3 ниже) вокруг изображения и применять эти преобразования с его помощью.

from skimage.morphology import binary_closing, binary_dilation, binary_erosion, binary_opening
from skimage.morphology import selem

# use a disk of radius 3
selem = selem.disk(3)

# oprning and closing
open_img = binary_opening(bin_image, selem)
close_img = binary_closing(bin_image, selem)

# erosion and dilation
eroded_img = binary_erosion(bin_image, selem)
dilated_img = binary_dilation(bin_image, selem)

plot_image([bin_image, open_img, close_img, eroded_img, dilated_img], cmap='gray')

Нормализация

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

#way1-this is common technique followed in case of RGB images 
norm1_image = image/255
#way2-in case of medical Images/non natural images 
norm2_image = image - np.min(image)/np.max(image) - np.min(image)
#way3-in case of medical Images/non natural images 
norm3_image = image - np.percentile(image,5)/ np.percentile(image,95) - np.percentile(image,5)

plot_image([image, norm1_image, norm2_image, norm3_image], cmap='gray')

Дополнение

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

Как вы знаете, объединение увеличивает неизменность. Если изображение собаки находится в верхнем левом углу изображения, с объединением вы сможете определить, собака находится немного влево / вправо / вверх / вниз в верхнем левом углу. Но с обучающими данными, состоящими из увеличения данных, таких как переворот, поворот, обрезка, перевод, освещение, масштабирование, добавление шума и т. Д., Модель изучает все эти варианты. Это значительно повышает точность модели. Так что, даже если собака окажется в любом углу изображения, модель сможет распознать ее с высокой точностью.

Возможны несколько типов аугментаций. Базовые преобразуют исходное изображение с помощью одного из следующих типов преобразований:

  1. Линейные преобразования
  2. Аффинные преобразования
from skimage import transform as tf

# flip left-right, up-down
image_flipr = np.fliplr(image)
image_flipud = np.flipud(image)

plot_image([image, image_flipr, image_flipud])

# specify x and y coordinates to be used for shifting (mid points)
shift_x, shift_y = image.shape[0]/2, image.shape[1]/2

# translation by certain units
matrix_to_topleft = tf.SimilarityTransform(translation=[-shift_x, -shift_y])
matrix_to_center = tf.SimilarityTransform(translation=[shift_x, shift_y])

# rotation
rot_transforms =  tf.AffineTransform(rotation=np.deg2rad(45))
rot_matrix = matrix_to_topleft + rot_transforms + matrix_to_center
rot_image = tf.warp(image, rot_matrix)

# scaling 
scale_transforms = tf.AffineTransform(scale=(2, 2))
scale_matrix = matrix_to_topleft + scale_transforms + matrix_to_center
scale_image_zoom_out = tf.warp(image, scale_matrix)

scale_transforms = tf.AffineTransform(scale=(0.5, 0.5))
scale_matrix = matrix_to_topleft + scale_transforms + matrix_to_center
scale_image_zoom_in = tf.warp(image, scale_matrix)

# translation
transaltion_transforms = tf.AffineTransform(translation=(50, 50))
translated_image = tf.warp(image, transaltion_transforms)


plot_image([image, rot_image, scale_image_zoom_out, scale_image_zoom_in, translated_image])

# shear transforms
shear_transforms = tf.AffineTransform(shear=np.deg2rad(45))
shear_matrix = matrix_to_topleft + shear_transforms + matrix_to_center
shear_image = tf.warp(image, shear_matrix)

bright_jitter = image*0.999 + np.zeros_like(image)*0.001

plot_image([image, shear_image, bright_jitter])

Строительство сети

Теперь давайте построим и обучим модель.

Выбор архитектуры

В этом разделе мы будем использовать архитектуру ResNet. Поскольку ResNets стали довольно популярными в отрасли, стоит потратить некоторое время, чтобы понять важные элементы их архитектуры. Начнем с оригинальной архитектуры предложенной здесь. Также в 2016 году команда ResNet предложила некоторые улучшения в исходной архитектуре здесь. Используя эти модификации, они обучили сети из более 1000 слоев (например, ResNet-1001).

Используемый здесь модуль ResNet builder по сути представляет собой модуль Python, содержащий все строительные блоки ResNet. Мы будем использовать этот модуль для импорта вариантов ResNets (ResNet-18, ResNet-34 и т. Д.). Модуль resnet.py взят из здесь. Его самый большой плюс в том, что механизм пропуска соединений позволяет использовать очень глубокие сети.

Запустить генератор данных

Генератор данных поддерживает предварительную обработку - он нормализует изображения (делит на 255) и обрезает центральную часть изображения (100 x 100).

Не было особой причины для включения 100 в качестве размера, но он был выбран таким образом, чтобы мы могли обрабатывать все изображения, размер которых превышает 100 * 100. Если какой-либо размер (высота или ширина) изображения меньше 100 пикселей, то это изображение удаляется автоматически. Вы можете изменить его на 150 или 200 в соответствии с вашими потребностями.

Теперь настроим генератор данных. В приведенном ниже коде настраивается собственный генератор данных, который немного отличается от того, который поставляется с API keras. Причина использования настраиваемого генератора состоит в том, чтобы иметь возможность изменять его в соответствии с решаемой проблемой (возможность настройки).

import numpy as np
import keras

class DataGenerator(keras.utils.Sequence):
    'Generates data for Keras'
    
    def __init__(self, mode='train', ablation=None, flowers_cls=['daisy', 'rose'], 
                 batch_size=32, dim=(100, 100), n_channels=3, shuffle=True):
        """
        Initialise the data generator
        """
        self.dim = dim
        self.batch_size = batch_size
        self.labels = {}
        self.list_IDs = []
        
        # glob through directory of each class 
        for i, cls in enumerate(flowers_cls):
            paths = glob.glob(os.path.join(DATASET_PATH, cls, '*'))
            brk_point = int(len(paths)*0.8)
            if mode == 'train':
                paths = paths[:brk_point]
            else:
                paths = paths[brk_point:]
            if ablation is not None:
                paths = paths[:ablation]
            self.list_IDs += paths
            self.labels.update({p:i for p in paths})
            
        self.n_channels = n_channels
        self.n_classes = len(flowers_cls)
        self.shuffle = shuffle
        self.on_epoch_end()

    def __len__(self):
        'Denotes the number of batches per epoch'
        return int(np.floor(len(self.list_IDs) / self.batch_size))

    def __getitem__(self, index):
        'Generate one batch of data'
        # Generate indexes of the batch
        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]

        # Find list of IDs
        list_IDs_temp = [self.list_IDs[k] for k in indexes]

        # Generate data
        X, y = self.__data_generation(list_IDs_temp)

        return X, y

    def on_epoch_end(self):
        'Updates indexes after each epoch'
        self.indexes = np.arange(len(self.list_IDs))
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def __data_generation(self, list_IDs_temp):
        'Generates data containing batch_size samples' # X : (n_samples, *dim, n_channels)
        # Initialization
        X = np.empty((self.batch_size, *self.dim, self.n_channels))
        y = np.empty((self.batch_size), dtype=int)
        
        delete_rows = []

        # Generate data
        for i, ID in enumerate(list_IDs_temp):
            # Store sample
            img = io.imread(ID)
            img = img/255
            if img.shape[0] > 100 and img.shape[1] > 100:
                h, w, _ = img.shape
                img = img[int(h/2)-50:int(h/2)+50, int(w/2)-50:int(w/2)+50, : ]
            else:
                delete_rows.append(i)
                continue
            
            X[i,] = img
          
            # Store class
            y[i] = self.labels[ID]
        
        X = np.delete(X, delete_rows, axis=0)
        y = np.delete(y, delete_rows, axis=0)
        return X, keras.utils.to_categorical(y, num_classes=self.n_classes)

Для начала у нас есть обучающие данные, хранящиеся в nn каталогах (если есть nn классов). Для заданного размера пакета мы хотим создать пакеты точек данных и передать их модели.

Первый for цикл "просматривает" каждый из классов (каталогов). Для каждого класса он сохраняет путь к каждому изображению в списке paths. В режиме обучения это подмножество paths, содержащее первые 80% изображений; в режиме проверки он подбирает последние 20%. В частном случае эксперимента по абляции он просто подбирает первые ablation изображений каждого класса.

Мы храним пути всех изображений (всех классов) в объединенном списке self.list_IDs. Словарь self.labels содержит метки (как пары ключ: значение path: class_number (0/1)).

После цикла мы вызываем метод on_epoch_end(), который создает массив self.indexes длины self.list_IDs и перемешивает их (для перемешивания всех точек данных в конце каждой эпохи).

Метод _getitem_ использует (перемешанный) массив self.indexes для выбора batch_size количества записей (путей) из списка путей self.list_IDs.

Наконец, метод __data_generation возвращает пакет изображений в виде пары X, y, где X имеет форму (batch_size, height, width, channels), а y имеет форму (batch size, ). Обратите внимание, что __data_generation также выполняет некоторую предварительную обработку - он нормализует изображения (делится на 255) и обрезает центральную часть изображения размером 100 x 100. Таким образом, каждое изображение имеет форму (100, 100, num_channels). Если какой-либо размер (высота или ширина) изображения меньше 100 пикселей, это изображение удаляется.

Эксперименты по абляции

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

Первая часть построения сети - заставить ее работать с вашим набором данных. Давайте попробуем разместить в сети всего несколько изображений и одну эпоху. Обратите внимание, что, поскольку указано ablation=100, используются 100 изображений каждого класса, поэтому общее количество пакетов составляет np.floor(200/32) = 6.

Обратите внимание, что класс DataGenerator «наследует» от класса keras.utils.Sequence, поэтому он имеет все функции базового класса keras.utils.Sequence (например, метод model.fit_generator).

# using resnet 18
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy', optimizer='SGD',
              metrics=['accuracy'])

# create data generator objects in train and val mode
# specify ablation=number of data points to train on
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)

# fit: this will fit the net on 'ablation' samples, only 1 epoch
model.fit_generator(generator=training_generator,
                    validation_data=validation_generator,
                    epochs=1,)

Переоснащение обучающих данных

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

Мы будем использовать ablation = 100 (т.е. обучение на 100 изображениях каждого класса), так что это все еще очень маленький набор данных, и мы будем использовать 20 эпох. В каждую эпоху будет использовано 200/32 = 6 пакетов.

# resnet 18
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer='SGD',
              metrics=['accuracy'])

# generators
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)

# fit
model.fit_generator(generator=training_generator,
                    validation_data=validation_generator,
                    epochs=20)

Результаты показывают, что точность обучения постоянно увеличивается с каждой эпохой. Точность проверки также увеличивается, а затем выходит на плато - это признак «хорошего соответствия», т. Е. Мы знаем, что модель, по крайней мере, способна учиться на небольшом наборе данных, поэтому мы можем надеяться, что она сможет учиться на весь комплект тоже.

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

Настройка гиперпараметров

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

  1. Скорость обучения и варианты + оптимизаторы
  2. Техники аугментации

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

Обратные вызовы Keras

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

Формально обратный вызов - это просто функция (если вы хотите выполнить одно действие) или список функций (если вы хотите выполнить несколько действий), которые должны выполняться при определенных событиях (конец эпохи, начало каждую партию, когда точность выходит на плато и т. д.). Keras предоставляет некоторые очень полезные функции обратного вызова через класс keras.callbacks.Callback.

У Keras есть много встроенных обратных вызовов (перечисленных здесь). Общий способ создания настраиваемого обратного вызова в keras:

from keras import optimizers
from keras.callbacks import *

# range of learning rates to tune
hyper_parameters_for_lr = [0.1, 0.01, 0.001]

# callback to append loss
class LossHistory(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        self.losses = []

    def on_epoch_end(self, epoch, logs={}):
        self.losses.append(logs.get('loss'))

# instantiate a LossHistory() object to store histories
history = LossHistory()
plot_data = {}

# for each hyperparam: train the model and plot loss history
for lr in hyper_parameters_for_lr:
    print ('\n\n'+'=='*20 + '   Checking for LR={}  '.format(lr) + '=='*20 )
    sgd = optimizers.SGD(lr=lr, clipnorm=1.)
    
    # model and generators
    model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
    model.compile(loss='categorical_crossentropy',optimizer= sgd,
                  metrics=['accuracy'])
    training_generator = DataGenerator('train', ablation=100)
    validation_generator = DataGenerator('val', ablation=100)
    model.fit_generator(generator=training_generator,
                        validation_data=validation_generator,
                        epochs=3, callbacks=[history])
    
    # plot loss history
    plot_data[lr] = history.losses

В приведенном выше коде мы создали настраиваемый обратный вызов, чтобы добавить потерю в список в конце каждой эпохи. Обратите внимание, что logs является атрибутом (словарем) keras.callbacks.Callback, и мы используем его, чтобы получить значение ключа «потеря». Некоторые другие ключи этого слова: acc, val_loss и т. Д.

Чтобы сообщить модели, что мы хотим использовать обратный вызов, мы создаем объект LossHistory с именем history и передаем его model.fit_generator, используя callbacks=[history]. В этом случае у нас есть только один обратный вызов history, хотя вы можете передать несколько объектов обратного вызова через этот список (пример нескольких обратных вызовов находится в разделе ниже - см. Блок кода DecayLR()).

Здесь мы настроили гиперпараметр скорости обучения и обнаружили, что скорость 0,1 является оптимальной скоростью обучения по сравнению с 0,01 и 0,001. Однако использование такой высокой скорости обучения для всего процесса обучения - не лучшая идея, поскольку позже потери могут начать колебаться вокруг минимумов. Итак, в начале обучения мы используем высокую скорость обучения для быстрого обучения модели, но по мере того, как мы обучаемся дальше и приближаемся к минимумам, мы постепенно уменьшаем скорость обучения.

# plot loss history for each value of hyperparameter
f, axes = plt.subplots(1, 3, sharey=True)
f.set_figwidth(15)

plt.setp(axes, xticks=np.arange(0, len(plot_data[0.01]), 1)+1)

for i, lr in enumerate(plot_data.keys()):
    axes[i].plot(np.arange(len(plot_data[lr]))+1, plot_data[lr])

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

Мы используем еще один настраиваемый обратный вызов (DecayLR), чтобы снизить скорость обучения в конце каждой эпохи. Скорость затухания указана как 0,5 ^ эпох. Также обратите внимание, что на этот раз мы говорим модели, чтобы она использовала два обратных вызова (переданных в виде списка callbacks=[history, decay] в model.fit_generator).

Хотя здесь мы использовали нашу собственную реализацию затухания, вы можете использовать те, которые встроены в оптимизаторы keras (используя аргумент decay).

# learning rate decay
class DecayLR(keras.callbacks.Callback):
    def __init__(self, base_lr=0.001, decay_epoch=1):
        super(DecayLR, self).__init__()
        self.base_lr = base_lr
        self.decay_epoch = decay_epoch 
        self.lr_history = []
        
    # set lr on_train_begin
    def on_train_begin(self, logs={}):
        K.set_value(self.model.optimizer.lr, self.base_lr)

    # change learning rate at the end of epoch
    def on_epoch_end(self, epoch, logs={}):
        new_lr = self.base_lr * (0.5 ** (epoch // self.decay_epoch))
        self.lr_history.append(K.get_value(self.model.optimizer.lr))
        K.set_value(self.model.optimizer.lr, new_lr)

# to store loss history
history = LossHistory()
plot_data = {}

# start with lr=0.1
decay = DecayLR(base_lr=0.1)

# model
sgd = optimizers.SGD()
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer= sgd,
              metrics=['accuracy'])
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)

model.fit_generator(generator=training_generator,
                    validation_data=validation_generator,
                    epochs=3, callbacks=[history, decay])

plot_data[lr] = decay.lr_history

plt.plot(np.arange(len(decay.lr_history)), decay.lr_history)

Методы увеличения

Давайте теперь напишем код для реализации увеличения данных. Расширение обычно выполняется с помощью генераторов данных, то есть расширенные данные генерируются пакетно, на лету. Вы можете использовать встроенный keras ImageDataGenerator или написать свой собственный генератор данных (для некоторых пользовательских функций и т. Д., Если хотите). В приведенном ниже коде показано, как это реализовать.

import numpy as np
import keras

# data generator with augmentation
class AugmentedDataGenerator(keras.utils.Sequence):
    'Generates data for Keras'
    def __init__(self, mode='train', ablation=None, flowers_cls=['daisy', 'rose'], 
                 batch_size=32, dim=(100, 100), n_channels=3, shuffle=True):
        'Initialization'
        self.dim = dim
        self.batch_size = batch_size
        self.labels = {}
        self.list_IDs = []
        self.mode = mode
        
        for i, cls in enumerate(flowers_cls):
            paths = glob.glob(os.path.join(DATASET_PATH, cls, '*'))
            brk_point = int(len(paths)*0.8)
            if self.mode == 'train':
                paths = paths[:brk_point]
            else:
                paths = paths[brk_point:]
            if ablation is not None:
                paths = paths[:ablation]
            self.list_IDs += paths
            self.labels.update({p:i for p in paths})
        
            
        self.n_channels = n_channels
        self.n_classes = len(flowers_cls)
        self.shuffle = shuffle
        self.on_epoch_end()

    def __len__(self):
        'Denotes the number of batches per epoch'
        return int(np.floor(len(self.list_IDs) / self.batch_size))

    def __getitem__(self, index):
        'Generate one batch of data'
        # Generate indexes of the batch
        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]

        # Find list of IDs
        list_IDs_temp = [self.list_IDs[k] for k in indexes]

        # Generate data
        X, y = self.__data_generation(list_IDs_temp)

        return X, y

    def on_epoch_end(self):
        'Updates indexes after each epoch'
        self.indexes = np.arange(len(self.list_IDs))
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def __data_generation(self, list_IDs_temp):
        'Generates data containing batch_size samples' # X : (n_samples, *dim, n_channels)
        # Initialization
        X = np.empty((self.batch_size, *self.dim, self.n_channels))
        y = np.empty((self.batch_size), dtype=int)
        
        delete_rows = []

        # Generate data
        for i, ID in enumerate(list_IDs_temp):
            # Store sample
            img = io.imread(ID)
            img = img/255
            if img.shape[0] > 100 and img.shape[1] > 100:
                h, w, _ = img.shape
                img = img[int(h/2)-50:int(h/2)+50, int(w/2)-50:int(w/2)+50, : ]
            else:
                delete_rows.append(i)
                continue
            
            X[i,] = img
          
            # Store class
            y[i] = self.labels[ID]
        
        X = np.delete(X, delete_rows, axis=0)
        y = np.delete(y, delete_rows, axis=0)
        
        # data augmentation
        if self.mode == 'train':
            aug_x = np.stack([datagen.random_transform(img) for img in X])
            X = np.concatenate([X, aug_x])
            y = np.concatenate([y, y])
        return X, keras.utils.to_categorical(y, num_classes=self.n_classes)

Оптимизируемые показатели

В зависимости от ситуации подбираем подходящие метрики. Для задач двоичной классификации AUC обычно является лучшим показателем.

AUC часто является лучшим показателем, чем точность. Поэтому вместо оптимизации точности, давайте будем отслеживать AUC и выбирать лучшую модель на основе AUC для данных проверки. Мы будем использовать обратные вызовы on_train_begin и on_epoch_end для инициализации (в начале каждой эпохи) и сохранения AUC (в конце эпохи).

from sklearn.metrics import roc_auc_score

class roc_callback(Callback):
    
    def on_train_begin(self, logs={}):
        logs['val_auc'] = 0

    def on_epoch_end(self, epoch, logs={}):
        y_p = []
        y_v = []
        for i in range(len(validation_generator)):
            x_val, y_val = validation_generator[i]
            y_pred = self.model.predict(x_val)
            y_p.append(y_pred)
            y_v.append(y_val)
        y_p = np.concatenate(y_p)
        y_v = np.concatenate(y_v)
        roc_auc = roc_auc_score(y_v, y_p)
        print ('\nVal AUC for epoch{}: {}'.format(epoch, roc_auc))
        logs['val_auc'] = roc_auc

Финальный забег

Давайте теперь обучим окончательную модель. Обратите внимание, что мы будем сохранять веса лучшей модели в models/best_models.hdf5, поэтому вам нужно будет создать каталог models. Обратите внимание, что веса моделей обычно сохраняются в файлах hdf5.

Сохранение наилучшей модели выполняется с помощью функции обратного вызова, входящей в ModelCheckpoint. Мы в основном указываем filepath, где должны сохраняться веса модели, monitor='val_auc' указывает, что вы выбираете лучшую модель на основе точности проверки, save_best_only=True сохраняет только лучшие веса, а mode='max' указывает, что точность проверки должна быть максимальной.

# model
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer= sgd,
              metrics=['accuracy'])
training_generator = AugmentedDataGenerator('train', ablation=32)
validation_generator = AugmentedDataGenerator('val', ablation=32)

# checkpoint 
filepath = 'models/best_model.hdf5'
checkpoint = ModelCheckpoint(filepath, monitor='val_auc', verbose=1, save_best_only=True, mode='max')
auc_logger = roc_callback()

# fit 
model.fit_generator(generator=training_generator,
                    validation_data=validation_generator,
                    epochs=3, callbacks=[auc_logger, history, decay, checkpoint])

plt.imshow(image)

#standardizing image
#moved the origin to the centre of the image
h, w, _ = image.shape
img = image[int(h/2)-50:int(h/2)+50, int(w/2)-50:int(w/2)+50, : ]

model.predict(img[np.newaxis,: ])

Эй, у нас очень высокая вероятность для класса 1, т.е. выросла. Если вы помните, класс 0 был ромашковым, а класс 1 - розовым (в верхней части блога). Итак, модель усвоила на отлично. Мы построили модель с хорошим AUC в конце трех эпох. Если вы тренируете это с использованием большего количества эпох, вы сможете достичь лучшего значения AUC.

Если у вас есть какие-либо вопросы, рекомендации или критические замечания, со мной можно связаться через LinkedIn или в разделе комментариев.