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

Технология переноса обучения (transfer learning) позволяет использовать готовые нейронные сети для решения задач нового типа, не тех, для которых сети предварительно обучались. В этой статье я расскажу как использовать перенос обучения для задач компьютерного зрения в Keras. Мы рассмотрим как с помощью сети VGG16 распознавать собак и кошек на фотографиях.

Предварительно обученные нейронные сети

Keras включает набор предварительно обученных нейронных сетей в модуле Keras Applications. Среди наиболее популярных сети VGG16 и VGG19, разработанные в Oxford Visual Geometry Group, InceptionV3 компании Google и ResNet50 компании Microsoft. В каждой новой версии Keras повяляется все больше предварительно обученных сетей.

Многие нейронные сети для задач классификации объектов на изображениях обучены на наборе данных ImageNet. Этот набор данных включает 14 миллионов изображений, относящихся к 21 тысяче классов. Однако нейронные сети обучают не на всем наборе ImageNet, а на его части из 1000 классов объектов. Ежегодно проводятся соревнования Large Scale Visual Recognition Challenge по распознаванию именно этих 1000 классов из набора данных ImageNet.

Распознавание собак и кошек

В качестве примера мы рассмотрим, как можно применить предварительно обученные нейронные сети не для распознавания объектов из набора данных ImageNet, а для более простой задачи распознавания собак и кошек. Данные мы возьмем из соревнования Kaggle “Cats vs Dogs”. Задача соревнования - определить, кто находится на фотографии: кот или собака. Классов всего два, поэтому задача бинарной классификации. Вот примеры фотографий:

Примеры фотографий кошек и собак с соревнования Kaggle

Следует отметить, что в наборе данных ImageNet также есть фотографии котов и собак. Но классы в ImageNet образуют иерархию. Нет классов “кот” и “собака”, есть много классов с разными породами котов и собак. Поэтому в чистом виде сеть, обученную на наборе данных ImageNet, применить для бинарной классификации котов и собак нельзя.

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

Перенос обучения

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

Нейронные сети, обученные для решения задач классификации изображений, состоят из двух частей:

  • Сверточная часть используется для выделения характерных признаков из изображения.
  • Полносвязная часть реализует классификацию - определяет, что за объект находится на изображении на основе признаков, которые извлекла сверточная часть.

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

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

Архитектура сети VGG16

Сверточная часть сети VGG16 состоит из пяти каскадов свертки и подвыборки. В первых двух каскадах используются по два слоя свертки и слой подвыборки с выбором максимального значения (max pooling). На трех следующих каскадах по три слоя свертки и один слой подвыборки. Размер ядер во всех слоях свертки 3х3.

Полносвязная часть сети VGG16 включает три уровня. На выходном уровне 1000 нейронов по количеству классов объектов. Используется формат one-hot encoding: значение только одного выходного нейрона должно быть близко к единице, остальные близки к нулю. Класс объекта на картинке соответствует нейрону, значение которого близко к единице. Перед выходным слоем в сети VGG16 еще два полносвязных слоя по 4096 нейронов.

На первом этапе необходимо убрать полносвязную часть из сети VGG16. Получится следующая сеть:

Архитектура сети VGG16 без полносвязных слоев

Второй этап: к сверточной части сети VGG16 добавляем новый классификатор для распознавания собак и кошек:

Архитектура сети VGG16 для распознавания кошек и собак

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

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

Реализация переноса обучения на Keras

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

Для начала подключаем необходимые модули Keras. Для TensorFlow версии 1.4 и выше, которая уже содержит Keras:

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Activation, Dropout, Flatten, Dense
from tensorflow.python.keras.applications import VGG16
from tensorflow.python.keras.optimizers import Adam

Если Keras установлен отдельно от TensorFlow, в том числе с другим бэкендом:

from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Activation, Dropout, Flatten, Dense
from keras.applications import VGG16
from keras.optimizers import Adam

Задаем константы:

# Каталог с данными для обучения
train_dir = 'train'
# Каталог с данными для проверки
val_dir = 'val'
# Каталог с данными для тестирования
test_dir = 'test'
# Размеры изображения
img_width, img_height = 150, 150
# Размерность тензора на основе изображения для входных данных в нейронную сеть
# backend Tensorflow, channels_last
input_shape = (img_width, img_height, 3)
# Размер мини-выборки
batch_size = 64
# Количество изображений для обучения
nb_train_samples = 17500
# Количество изображений для проверки
nb_validation_samples = 3750
# Количество изображений для тестирования
nb_test_samples = 3750

Фотографии собак и кошек должны быть разделены на три набора данных: для обучения, проверки и тестирования, которые находятся в трех каталогах. Как это сделать, подробно описано в статье.

Загружаем предварительно обученную нейронную сеть

Для загрузки сети VGG16 используем следующую команду:

vgg16_net = VGG16(weights='imagenet', 
                  include_top=False, 
                  input_shape=(150, 150, 3))

Обратите внимание, что мы используем параметр include_top=False, который говорит Keras, что не нужно загружать часть сети VGG16, отвечающую за классификацию. Будет загружена только сверточная часть сети.

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

Для запрещения обучения сети VGG16 устанавливаем значение поля trainable в False.

vgg16_net.trainable = False

vgg16_net.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 150, 150, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 150, 150, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 150, 150, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 75, 75, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 75, 75, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 75, 75, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 37, 37, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 37, 37, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 18, 18, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 18, 18, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 9, 9, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0         
=================================================================
Total params: 14,714,688
Trainable params: 0
Non-trainable params: 14,714,688

В выводе диагностической информации видно, что количество параметров (то есть весов входов в нейроны) в сети 14,714,688, но количество обучаемых параметров равно нулю (Trainable params: 0).

Создаем составную сеть

Наша новая сеть будет включать сверточную часть VGG16 и новый классификатор для распознавания собак и кошек. Для создания такой составной сети мы будем использовать последовательную (Sequential) модель Keras. Модель задается следующим образом:

model = Sequential()
model.add(vgg16_net)
model.add(Flatten())
model.add(Dense(256))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))

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

model.add(vgg16_net)

После этого мы добавляем в модель слой Flatten, который преобразует двумерные вектора признаков, полученные от сверточной части сети VGG16, в одномерный вектор. Полносвязные слои, используемые для классификации, не могут работать с двумерными данными на входе, им нужен одномерный вектор. Первый полносвязный (Dense) слой содержит 256 нейронов, функция активации полулинейная. Затем идет слой Dropout, который используется для снижения переобучения.

На выходном полносвязном слое один нейрон. У нас два возможных класса объектов: кот и собака. Им соответствуют значения выходного нейрона ноль и единица. Функция активации выходного нейрона сигмоидальная. Эта функция плавно меняет свое значение от нуля до единицы, поэтому она хорошо подходит для бинарной классификации.

Напечатаем информацию о составной сети:

model.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 8192)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 256)               2097408   
_________________________________________________________________
activation_1 (Activation)    (None, 256)               0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 256)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 257       
_________________________________________________________________
activation_2 (Activation)    (None, 1)                 0         
=================================================================
Total params: 16,812,353
Trainable params: 2,097,665
Non-trainable params: 14,714,688
_________________________________________________________________

Видно, что первый слой в сети - это отдельная модель. Всего в сети 16,812,353 парметров, из них обучаемых - 2,097,665. Все обучаемые параметры относятся к полносвязным слоям (2,097,408 параметров на слое dense_1 и 257 параметров на выходном слое dense_2).

Обучаем составную сеть на новом наборе данных

Перед обучением сеть необходимо скомпилировать:

model.compile(loss='binary_crossentropy',
              optimizer=Adam(lr=1e-5), 
              metrics=['accuracy'])

В качестве функции ошибки указываем binary_crossentropy, потому что у нас задача бинарной классификации. Если классов больше, чем два, то нужно использовать categorical_crossentropy.

Часть сети у нас уже предварительно обучена, поэтому для новой части нужно использовать небольшую скорость обучения, иначе алгоритм обучения может не сойтись. Параметр скорости обучения мы указываем при создании оптимизатора Adam в параметре lr (сокращение от learning rate, скорость обучения). Значение параметра lr в примере единица на десять в минус пятой степени (для оптимизатора Adam по умолчанию lr=0.001, на два порядка больше).

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

datagen = ImageDataGenerator(rescale=1. / 255)

train_generator = datagen.flow_from_directory(
    train_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

val_generator = datagen.flow_from_directory(
    val_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

test_generator = datagen.flow_from_directory(
    test_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

Обучаем сеть с использованием генераторов:

model.fit_generator(
    train_generator,
    steps_per_epoch=nb_train_samples // batch_size,
    epochs=10,
    validation_data=val_generator,
    validation_steps=nb_validation_samples // batch_size)

Диагностический вывод обучения сети:

Epoch 1/10
273/273 [==============================] - 462s - loss: 0.2524 - acc: 0.8857 - val_loss: 0.1329 - val_acc: 0.9472
Epoch 2/10
273/273 [==============================] - 458s - loss: 0.0975 - acc: 0.9630 - val_loss: 0.1237 - val_acc: 0.9512
Epoch 3/10
273/273 [==============================] - 454s - loss: 0.0606 - acc: 0.9762 - val_loss: 0.0804 - val_acc: 0.9710
Epoch 4/10
273/273 [==============================] - 454s - loss: 0.0446 - acc: 0.9840 - val_loss: 0.0791 - val_acc: 0.9696
Epoch 5/10
273/273 [==============================] - 454s - loss: 0.0254 - acc: 0.9915 - val_loss: 0.0976 - val_acc: 0.9699
Epoch 6/10
273/273 [==============================] - 453s - loss: 0.0169 - acc: 0.9942 - val_loss: 0.0883 - val_acc: 0.9693
Epoch 7/10
273/273 [==============================] - 451s - loss: 0.0125 - acc: 0.9960 - val_loss: 0.0702 - val_acc: 0.9740
Epoch 8/10
273/273 [==============================] - 452s - loss: 0.0146 - acc: 0.9951 - val_loss: 0.1078 - val_acc: 0.9704
Epoch 9/10
273/273 [==============================] - 450s - loss: 0.0049 - acc: 0.9986 - val_loss: 0.1971 - val_acc: 0.9563
Epoch 10/10
273/273 [==============================] - 451s - loss: 0.0054 - acc: 0.9983 - val_loss: 0.1101 - val_acc: 0.9756

Сеть обучается достаточно долго. У меня на GPU NVIDIA GTX 1050Ti одна эпоха обучения занимает примерно 450 секунд, общее время обучения примерно 75 минут. На центральном процессоре время обучения будет значительно больше. Поэтому не рекомендуется обучать такую сеть без GPU.

После 10 эпох обучения аккуратность на обучающем наборе данных составляет 99,83%, на проверочном наборе - 97,56%.

Оцениваем качество обучения сети

После завершения обучения сети необходимо обязательно проверить качество ее работы на данных, которые сеть не видела в процессе обучения. Для проверки мы будем использовать подготовленный ранее генератор тестовых данных test_generator:

scores = model.evaluate_generator(test_generator, nb_test_samples // batch_size)
print("Аккуратность на тестовых данных: %.2f%%" % (scores[1]*100))

Аккуратность на тестовых данных: 97.31%

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

С другой стороны, при обучении сверточной нейронной сети распознавать кошек и собак с нуля, аккуратность на тестовом наборе данных была 84.30%. Использование предварительно обученной сверточной нейронной сети VGG16 позволило нам поднять точность работы более чем на 10%.

Итоги

Мы научились использовать технологию переноса обучения (transfer learning) для применения предварительно обученных сетей к новым типам задач. Краткая схема переноса обучения для задач компьютерного зрения:

  1. Выбрать предварительно обученную нейронной сети и взять от нее только сверточную часть, удалив полносвязную. В Keras нужно указать параметр include_top=False при создании сети.
  2. Создать составную сеть на основе предварительно обученной сети и нового классификатора, который создается для нашей задачи. Предварительно обученную сеть можно добавить в составную сеть таким же образом, как добавляем отдельные слои.
  3. Обучить составную сеть на новом наборе данных. Обучать нужно только веса добавленного в сеть классификатора, предварительно обученная сверточная часть должна быть “заморожена”.

В качестве примера мы рассмотрели применение сети VGG16 для распознавания кошек и собак на фотографиях. Keras включает другие предварительно обученные сети, вы можете попробовать использовать их для этой же задачи. Особенно рекомендую протестировать сети InceptionV3 и ResNet50, они были разработаны позже сети VGG16, имеют более сложную структуру и обеспечивают более высокую точность.

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

P.S. Как всегда, не забудьте сохранить обученную сеть для последующего использования!

Полезные ссылки

  1. Нейронная сеть VGG16.
  2. Соревнования Kaggle “Cats vs Dogs”.
  3. Предварительно обученные сети в модуле Keras Applications.
  4. Как подготовить набор изображений для обучения нейронной сети в Keras.
  5. Jupyter ноутбук с полной версией программы программы распознавания собак и кошек с помощью сети VGG16
  6. Учебный курс “Программирование глубоких нейронных сетей на Python”.