Прогнозирование породы собак с использованием CNN и трансферного обучения

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

Это проблема контролируемого обучения, в частности проблема мультиклассовой классификации, и поэтому к ее решению можно подойти, выполнив следующие шаги:

  1. Накапливайте помеченные данные. В данном случае это означает создание хранилища изображений с собаками известных пород.
  2. Постройте модель, способную извлекать данные из обучающих изображений, которые выводят данные, которые можно интерпретировать для определения породы собаки.
  3. Обучите модель на данных обучения, проверьте производительность во время обучения с данными проверки
  4. Оцените показатели производительности, возможно, вернитесь к шагу 2 с изменениями для повышения производительности
  5. Протестируйте модель на тестовых данных

Каждый шаг, конечно же, имеет ряд подэтапов, которые я буду подробно описывать по мере продвижения.

Прелюдия

Акт обучения нейронной сети, даже относительно простой, может быть чрезвычайно затратным в вычислительном отношении. Многие компании используют серверные стойки с графическими процессорами, предназначенные для такого рода задач; Я буду работать на своем локальном ПК, который оснащен видеокартой GTX 1070, которую я найду для этого упражнения. Чтобы выполнить эту задачу на вашем локальном компьютере, вы должны предпринять несколько шагов, чтобы определить подходящую среду программирования, и я подробно расскажу о них здесь. Если вас не интересует настройка серверной части, перейдите к следующему разделу.

Сначала я создал новую среду в Anaconda и установил следующие пакеты:

  • tenorflow-gpu
  • юпитер
  • glob2
  • scikit-learn
  • Керас
  • matplotlib
  • opencv (это для идентификации человеческих лиц в конвейере изображений - не обязательная возможность, но полезная в некоторых приложениях)
  • tqdm
  • подушка
  • морской

Затем я обновил драйверы видеокарты. Это важно, потому что обновления драйверов выпускаются довольно регулярно, даже для такой карты, как моя, которой 3 года, и если вы используете современную версию tensorflow, необходимо ли использовать последние версии драйверов для совместимости. В моем случае драйверы, которым было всего 5 месяцев, были несовместимы с новейшей версией tensorflow.

Наконец, откройте блокнот jupyter из приглашения anaconda в новой среде, чтобы выполнить свою работу, и убедитесь, что jupyter использует правильную среду для ядра. Если вы этого не сделаете, у Tensorflow могут возникнуть проблемы.

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

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

Успех! Вот и моя GTX 1070.

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

# tensorflow local GPU configuration
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
session = tf.Session(config=config)

Шаг 1. Компиляция данных

В моем случае это было тривиально, так как Udacity предоставил мне 1,08 ГБ изображений собак, охватывающих 133 породы, уже в надлежащей файловой структуре. Правильная файловая структура, в случае классификационной CNN, построенной с помощью keras, означает, что файлы разделяются путем обучения, проверки и тестирования, а затем разделяются в этих папках по породам собак. Имена каждой папки должны быть именами классов, которые вы планируете идентифицировать.

Очевидно, что в мире существует более 133 пород собак - американский авторитет AKC перечисляет 190 пород, а мировой авторитет FCI перечисляет 360 пород. Если бы я хотел увеличить размер своего набора данных для обучения, чтобы включить в него больше пород или просто больше изображений каждой породы, одним из способов, которым я мог бы заняться, было бы установить python Flickr API и запросить у него изображения, помеченные именами любой породы, которую я хотел . Однако для целей этого проекта я продолжаю использовать этот базовый набор данных.

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

# 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('dogImages/train')
valid_files, valid_targets = load_dataset('dogImages/valid')
test_files, test_targets = load_dataset('dogImages/test')
# load list of dog names
# the [20:-1] portion simply removes the filepath and folder number
dog_names = [item[20:-1] for item in sorted(glob("dogImages/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.

Затем я выполняю шаг по нормализации данных, разделяя каждый пиксель изображения на 255 и форматирую вывод как тензор - вектор, который может использоваться keras. Примечание: следующий код загружает в память тысячи файлов в виде тензоров. Хотя это возможно с относительно небольшим набором данных, лучше использовать систему пакетной загрузки, которая загружает только небольшое количество тензоров за раз. Я сделаю это позже, для последней модели, которую я проектирую.

# define functions for reading in image files as tensors
def path_to_tensor(img_path, target_size=(224, 224)):
    # loads RGB image as PIL.Image.Image type
    # 299 is for xception, 224 for the other models
    img = image.load_img(img_path, target_size=target_size)
    # 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, (target_size,) 3) and return 4D tensor
    return np.expand_dims(x, axis=0)
def paths_to_tensor(img_paths, target_size = (224, 224)):
    list_of_tensors = [path_to_tensor(img_path, target_size) for img_path in tqdm(img_paths)]
    return np.vstack(list_of_tensors)
# run above functions
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

Конструировать, обучать, тестировать, оценивать

Есть бесконечное количество способов сделать это, некоторые из них будут работать намного лучше, чем другие. Я собираюсь изучить 3 уникальных подхода и проследить их от создания до тестирования и оценки. Я использую следующие подходы:

  1. Тривиальное решение. Я построю и обучу очень простую CNN на наборе данных и оценю ее производительность.
  2. Перенос обучения с использованием узких мест. Я воспользуюсь существующей CNN, обученной на огромной библиотеке изображений, и адаптирую ее к своему приложению, используя ее для преобразования моих входных изображений в «узкие места» : абстрактные функции представления изображений.
  3. Передача обучения с увеличением изображения. Подобно подходу с использованием узких мест, но я попытаюсь получить лучшее обобщение модели, создав модель, которая представляет собой набор предварительно обученных узких мест CNN с настраиваемым выходным слоем для моей приложение, и я буду кормить его входными изображениями, которые случайным образом дополняются пиктографическими преобразованиями.

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

Шаг 2а: построение тривиальной модели

Я создаю простую CNN со следующим кодом, используя keras с бэкэндом tenorflow.

from keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D
from keras.layers import Dropout, Flatten, Dense
from keras.models import Sequential
model = Sequential()
# Define model architecture.
model.add(Conv2D(16, kernel_size=2, activation='relu', input_shape=(224,224,3))) # activation nonlinearity typically performed before pooling
model.add(MaxPooling2D()) # defaults to pool_size = (2,2), stride = None = pool_size
model.add(Conv2D(32, kernel_size=2, activation='relu'))
model.add(MaxPooling2D())
model.add(Conv2D(64, kernel_size=2, activation='relu'))
model.add(MaxPooling2D())
model.add(GlobalAveragePooling2D())
model.add(Dense(133, activation='softmax'))
model.summary()

Метод model.summary () выводит следующую структуру модели:

Здесь я создал 8-ми слойную последовательную нейронную сеть, использующую 3 сверточных слоя в паре с максимальными уровнями объединения и заканчивающуюся полностью связанным слоем со 133 узлами - по одному для каждого класса, который я пытаюсь предсказать. Обратите внимание, что в плотном слое я использую функцию активации softmax; причина этого в том, что он имеет диапазон от 0 до 1 и заставляет сумму всех узлов в выходном слое равняться 1. Это позволяет нам интерпретировать выходные данные одного узла как прогнозируемую модель вероятность того, что вход имел класс, соответствующий этому узлу. Другими словами, если второй узел в слое имеет значение активации 0,8 для конкретного изображения, мы можем сказать, что модель предсказала, что входные данные с вероятностью 80% принадлежат второму классу. Обратите внимание на 19 000 параметров модели - это веса, смещения и ядра (фильтры свертки), которые моя сеть будет пытаться оптимизировать. Теперь должно быть очевидно, почему этот процесс требует больших вычислительных ресурсов.

Наконец, я компилирую эту модель, чтобы ее можно было обучить. Обратите внимание, что есть много функций потерь и оптимизаторов, которые я мог бы использовать здесь, но текущее общее соглашение для прогнозирования меток многоклассовых изображений использует Адама в качестве оптимизатора и категориальную кроссэнтропию в качестве функции потерь. Я тестировал Адама против SGD и RMSProp и обнаружил, что Адам тренировался намного быстрее.

model.compile(optimizer=’adam’, loss=’categorical_crossentropy’, metrics=[‘accuracy’])

Шаг 3а: Обучите тривиальную модель

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

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=3, batch_size=20, callbacks=[checkpointer], verbose=2)

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

Модель завершила обучение с точностью обучения 1,77% и точностью проверки 1,68%. Хотя это лучше, чем случайное предположение, в этом нет ничего особенного.

В качестве примечания: при обучении этой модели мы сразу видим скачок в использовании моего графического процессора! Это здорово - это означает, что серверная часть tenorflow действительно использует мою видеокарту.

Шаг 4а: оценка тривиальной модели

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

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

Шаг 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)

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

Шаг 2b: построение модели узких мест

Один из способов, которым я могу значительно повысить производительность, - это использовать трансферное обучение, то есть я могу использовать существующую CNN, которая была предварительно обучена распознавать особенности общих данных изображения и адаптировать ее для своих собственных целей. У Кераса есть ряд таких предварительно обученных моделей, доступных для загрузки и использования. Каждая из них представляет собой модель, обученную на репозитории изображений, известном как imagenet, который содержит миллионы изображений, распределенных по 1000 категориям. Модели, обученные в Imagenet, обычно представляют собой глубокие CNN с несколькими полностью подключенными выходными слоями, которые обучены классифицировать скрытые функции, отображаемые слоями свертки, в 1 из этих 1000 категорий. Я могу взять одну из этих предварительно обученных моделей и просто заменить выходные слои моими собственными полностью связанными слоями, которые затем я могу обучить классифицировать каждое входное изображение как одну из моих 133 пород собак. Здесь важно отметить, что я больше не обучаю CNN - я собираюсь заморозить веса и ядра сверточных слоев, которые уже обучены распознавать абстрактные особенности изображения, и обучу только свой собственный вывод сеть. Это экономит огромное количество времени.

Есть по крайней мере два способа сделать это. Один из способов - сшить предварительно обученную сеть и мою настраиваемую сеть, как описано выше. Другой, еще более простой способ - передать каждое изображение в моем наборе данных через предварительно обученную сеть и сохранить выходные данные в виде массивов для последующей передачи через мою сеть. Преимущество последнего метода состоит в том, что он экономит время вычислений, потому что в каждую тренировочную эпоху я выполняю только прямой проход и обратное распространение через мою собственную модель, а не на модель imagenet и мою модель вместе. Удобно, что Udacity уже пропустил все предоставленные ими обучающие образы через несколько встроенных CNN и предоставил необработанный вывод или функции узких мест, чтобы я мог просто прочитать.

Здесь я определяю свою собственную полностью подключенную сеть, чтобы принимать узкие места и выводить 133 узла, по одному для каждой породы. Это для сети VGG16, используемой в качестве примера. Я использую разные сети для своего фактического обучения, что будет видно в следующем разделе.

VGG16_model = Sequential()
VGG16_model.add(GlobalAveragePooling2D(input_shape=train_VGG16.shape[1:]))
VGG16_model.add(Dense(133, activation='softmax'))

Здесь следует отметить несколько моментов:

  • Я начинаю со слоя GlobalAveragePooling - это потому, что последний уровень VGG16 и фактически все модели изображений, которые я тестирую, представляют собой последовательность свертки / объединения. Слои Global Pooling уменьшают размерность этого вывода и значительно сокращают время обучения при подаче в плотный слой.
  • Форма ввода первого слоя в моей сети должна быть адаптирована к модели, для которой он разработан. Я могу сделать это, просто получив форму данных об узких местах. Первое измерение отсекается от формы узкого места, чтобы позволить keras добавить измерение для пакетной обработки.
  • Я снова использую функцию активации softmax по тем же причинам, что и для тривиальной модели.

Шаг 3b: модель узких мест в обучении

Udacity предоставил узкие места для 4 сетей: VGG19, ResNet50, InceptionV3 и Xception. Следующий блок кода считывает узкие места для каждой модели, создает полностью подключенную выходную сеть и обучает эту сеть в течение 20 эпох. Наконец, выводится точность каждой модели.

Как видно из последней строки, все 4 модели работали намного лучше, чем моя собственная тривиальная CNN, при этом модель Xception достигла точности проверки 85%!

Шаг 4b: оценка модели узких мест

Лучшие исполнители - Xception и ResNet50 - оба достигли замечательной точности проверки, но, покопавшись в журналах, мы видим, что их точность в отношении данных обучения составила почти 100%. Это торговая марка переобучения. Это неудивительно, Xception имеет 22 миллиона параметров, а ResNet50 - 23 миллиона, что означает, что обе модели обладают огромной энтропийной способностью и способны просто запоминать изображения обучающих данных. Чтобы бороться с этим, я внесу некоторые изменения в свою полностью подключенную модель и переучусь.

Я добавил второй плотный слой в надежде, что модель сможет немного меньше полагаться на предварительно обученные параметры, а также дополнил оба полностью плотных слоя регуляризацией L2 и выпадением. Регуляризация L2 наказывает сеть за высокие веса отдельных параметров, а выпадение из сети случайным образом отбрасывает сетевые узлы во время обучения. Оба борются с переобучением, требуя от сети большего обобщения во время обучения. Также обратите внимание, что я изменил стратегии оптимизации; в реальной исследовательской среде это можно было бы сделать с помощью GridSearch, который принимает списки гиперпараметров (например, оптимизаторы с диапазонами гиперпараметров), но в интересах экономии времени я поэкспериментировал с некоторыми из них самостоятельно. Обратите внимание, что я вернулся к использованию SGD - экспериментируя, я обнаружил, что, хотя Адам тренируется очень быстро, учитывая достаточное количество тренировочных эпох, SGD неизменно превосходит Адама (открытие, на которое намекает эта статья).

После тренировки в течение 100 эпох (5 минут):

Модель достигла сравнимой точности проверки с предыдущей, но точность обучения намного ниже. Низкая точность обучения связана с отсевом, поскольку он никогда не использует полную модель для оценки входных данных обучения. Я удовлетворен тем, что эта модель больше не переоснащается в той же степени, что и раньше. Похоже, что и точность проверки, и потери примерно выровнялись - возможно, еще через 100 эпох я смогу выжать еще 1–2% точности, но есть другие методы обучения, которые я могу использовать в первую очередь.

Шаг 5b: Тестирование модели функции "узкое место"

Точность почти 83% на тестовом наборе данных. Очень похоже на набор проверки, как и ожидалось. Глядя на матрицу путаницы:

Намного лучше, чем предыдущий. Здесь мы видим, что есть несколько пород собак, с которыми модель работает достаточно хорошо, и несколько, с которыми она действительно не справляется. Глядя на пример этого, становится совершенно ясно, почему.

Давайте увеличим выброс на полпути вверх по оси y и примерно на 1/4 оси x.

Модель последовательно считает, что 66-й класс - это фактически 35-й класс. То есть думает, что полевой спаниель на самом деле бойкин-спаниель. Вот эти две породы бок о бок.

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

Шаг 2c: Компиляция дополненной модели ввода

В моделях, обученных на данных изображения, есть форма начальной загрузки обучающих данных, называемая увеличением изображения, где во время обучения я могу применять случайные повороты, увеличения и преобразования к обучающим изображениям. Это дает эффект искусственного увеличения размера обучающих данных путем изменения пикселей обучающих изображений при сохранении целостности контента; например если я поверну изображение корги на 15 градусов и переверну его по горизонтали, изображение все равно будет распознаваться как корги, но модель никогда не увидит это точное изображение во время обучения. Есть надежда, что этот метод улучшит точность модели в течение большого количества эпох, а также поможет бороться с переобучением. Для этого я больше не могу использовать узкие места, которые использовал раньше, и должен скомпилировать всю модель вместе для прямого и обратного распространения. Также обратите внимание, поскольку я все еще не хочу редактировать параметры предварительно обученной модели imagenet, я заморозю эти слои перед обучением.

Сначала я определяю свой конвейер загрузки данных:

Затем я загружаю модель imagenet, определяю настраиваемую полностью подключенную модель вывода и объединяю их в одну последовательную модель. Я вернулся к использованию Адама здесь из-за времени, затраченного на тренировки. Если бы у меня было больше вычислительных ресурсов, наверное, стоило бы использовать SGD, как раньше.

Шаг 3c: обучение расширенной модели ввода

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

Модель очень быстро достигла относительно высокой точности на наборах данных для обучения и проверки - это благодаря моему переходу на оптимизатор Adam. Обратите внимание, что точность обучения все еще ниже точности проверки - это потому, что я все еще использую отсев. Еще одна вещь, на которую следует обратить внимание, - это большой разброс в точности проверки. Это может быть признаком высокой скорости обучения (глядя на вас, Адам) или высокой энтропийной способности (слишком много параметров). Кажется, что со временем он выровняется, поэтому меня это не волнует.

Шаг 4c: оценка модели расширенных входных данных

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

Шаг 5c: протестируйте расширенную модель ввода

Я могу передать данные тестирования расширенной модели так же, как я подавал ему данные обучения и проверки, с помощью keras ImageDataGenerator:

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

Чтобы углубиться в точность и вспомнить, я провожу тот же анализ, что и раньше, с матрицей неточностей:

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

Иди разберись.

Конечный продукт

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

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

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

Репозиторий github: https://github.com/jfreds91/DSND_t2_capstone