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

Альтернативный вариант, который мы рассмотрим в этой статье – извлечение признаков из изображений с помощью предварительно обученной глубокой нейронной сети (по-английски такие признаки называются deep features) и последующий анализ уже этих признаков, а не исходных изображений. Как правило, объем признаков значительно меньше, чем изображений, из которых они извлечены, поэтому сеть обучается гораздо быстрее. Часто при таком подходе удается обучить нейронную сеть за разумное время даже без GPU.

Как и зачем анализировать признаки, извлеченные глубокой нейронной сетью

В качестве примера мы по-прежнему будем рассматривать задачу классификации изображений котов и собак на картинках из соревнования Kagge с использованием глубокой нейронной сети VGG16. Эта сеть имеет следующую архитектуру:

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

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

Сохраняем векторы, которые выдает предварительно обученная сеть.

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

Анализ векторов, извлеченных из изображений.

Анализ признаков из изображений в Keras

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

Подготовка набора данных

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

cats_vs_dogs/
|----train/
|    |----cats/
|    |----dogs/
|
|----validation/
|    |----cats/
|    |----dogs/
|
|----test/
     |----cats/
     |----dogs/

Набор данных разделен на три части: данные для обучения (каталог train), данные для проверки (каталог validation) и данные для тестирования (каталог test). В каждом каталоге два подкаталога: cats и dogs с фотографиями котов и собак соответственно.

Генераторы для чтения изображений

В Keras для загрузки изображений с диска используются генераторы. Если мы собираемся не обучать сеть на изображениях, а извлекать признаки из изображений, то генераторы нужно настраивать специальным образом. Сначала зададим константы для удобства:

# Каталог с данными для обучения
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 = 10
# Количество изображений для обучения
nb_train_samples = 17500
# Количество изображений для проверки
nb_validation_samples = 3750
# Количество изображений для тестирования
nb_test_samples = 3750

Мы используем 70% набора изображений котов и собак для обучения (17,5 тыс. из 25 тыс.) и по 15% для проверки и тестирования (3750 изображений).

Задаем ImageDataGenerator, в котором каждый пиксель изображения будет делиться на 255, чтобы данные на входе в сеть были в диапазоне от 0 до 1:

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=None,
    shuffle=False)

Основное отличие от того, что мы делали раньше, в двух последних параметрах генератора - class_mode и shuffle. Параметр class_mode=None говорит о том, что генератор не возвращает правильные ответы, а только исходные изображения. Правильные ответы нам на этом этапе не нужны, т.к. мы не планируем дообучать предварительно обученную сеть. С помощью shuffle=False мы говорим генератору, что не нужно перемешивать изображения. Тогда генератор будет выдавать изображения в том порядке, в котором они записаны на диск: сначала все изображения котов, а потом собак. В этом случае мы легко сможем сгенирировать метки для векторов, извлеченных из изображений.

Похожим образом задаем генераторы для проверки и тестирования. Они отличатся только каталогом, из которого берут данные:

val_generator = datagen.flow_from_directory(
    val_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode=None,
    shuffle=False)

test_generator = datagen.flow_from_directory(
    test_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode=None,
    shuffle=False)

Извлекаем признаки из изображений

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

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

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

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

features_train = vgg16_net.predict_generator(
        train_generator, nb_train_samples // batch_size)

Переменная features_train – это numpy массив размерности (17500, 4, 4, 512). Первая размерность 17500 соответствует количеству изображений в наборе данных для обучения. (4, 4, 512) - это размерность вектора признаков, который выдает сверточная часть сети VGG16: 512 карт признаков размером 4х4. Можно посмотреть фрагмент вектора признаков:

print(features_train[0])
array([[[0.48350716, 0.        , 0.        , ..., 0.        ,
         0.8291736 , 0.        ],
        [0.0348561 , 0.        , 0.22486642, ..., 0.        ,
         1.038972  , 0.        ],
        [0.31540298, 0.        , 0.        , ..., 0.        ,
         0.49017912, 0.        ],
        [0.23459446, 0.        , 0.        , ..., 0.        ,
         0.28118512, 0.        ]],

...

       [[0.44808802, 0.        , 0.        , ..., 0.        ,
         0.3077254 , 0.        ],
        [0.43148196, 0.        , 1.0594087 , ..., 0.        ,
         0.5165488 , 0.        ],
        [0.2360073 , 0.        , 1.3714772 , ..., 0.        ,
         0.8434944 , 0.        ],
        [0.        , 0.        , 1.3223135 , ..., 0.30869496,
         0.5962191 , 0.        ]]], dtype=float32)

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

Сгенерированные векторы признаков записываем в массив с помощью средств numpy:

np.save(open('features_train.npy', 'wb'), features_train)

Аналогичным образом генерируем и сохраняем векторы признаков для наборов данных проверки и тестирования:

# Генерируем и сохраняем векторы изображений проверочного набора данных
features_val  = vgg16_net.predict_generator(
        val_generator, nb_validation_samples // batch_size)
np.save(openopen('features_val.npy', 'wb'), features_val)

# Генерируем и сохраняем векторы изображений набора данных для тестирования
features_test = vgg16_net.predict_generator(
        test_generator, nb_test_samples // batch_size)
np.save(open('features_test.npy', 'wb'), features_test)

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

Нейронная сеть для классификации векторов признаков

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

features_train = np.load(open('features_train.npy', 'rb'))
features_val = np.load(open('features_val.npy', 'rb'))
features_test = np.load(open('features_test.npy', 'rb'))

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

labels_trainlabels_t  =  np.array(
        [0] * (nb_train_samples // 2) + [1] * (nb_train_samples // 2))
labels_vallabels_v  =  np.array(
        [0] * (nb_validation_samples // 2) + [1] * (nb_validation_samples // 2))
labels_test =  np.array(
        [0] * (nb_test_samples // 2) + [1] * (nb_test_samples // 2))

Для первой половины данных записывается метка 0, что соответствует коту, а для второй половины – метка 1 (собака).

Классифицировать векторы признаков будем с помощью простой полносвязной нейронной сети:

model = Sequential()
model.add(Flatten(input_shape=features_train.shape[1:]))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))

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

Компилируем модель и обучаем на векторах признаков, которые мы загрузили из файлов, и метках классов для них:

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

model.fit(features_train, labels_train,
              epochs=15,
              batch_size=64,
              validation_data=(features_val, labels_val), verbose=2)

Фрагмент диагностического вывода при обучения нейронной сети в течение 15 эпох:

Train on 17500 samples, validate on 3750 samples
Epoch 1/15
 - 5s - loss: 0.3228 - acc: 0.8636 - val_loss: 0.2185 - val_acc: 0.9109
Epoch 2/15
 - 4s - loss: 0.2234 - acc: 0.9053 - val_loss: 0.2346 - val_acc: 0.9019
Epoch 3/15
 - 4s - loss: 0.1967 - acc: 0.9171 - val_loss: 0.2090 - val_acc: 0.9157
...
Epoch 13/15
 - 4s - loss: 0.0943 - acc: 0.9618 - val_loss: 0.2332 - val_acc: 0.9197
Epoch 14/15
 - 4s - loss: 0.0774 - acc: 0.9676 - val_loss: 0.2623 - val_acc: 0.9173
Epoch 15/15
 - 4s - loss: 0.0823 - acc: 0.9669 - val_loss: 0.2431 - val_acc: 0.9205

Аккуратность на проверочной выборке получилась 92%. Это меньше, чем при дообучении сети VGG16 на новом наборе данных. Однако время обучения, 4-5 секунд на одну эпоху на GPU, также значительно меньше. Такую сеть действительно можно обучить на CPU за разумное время.

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

scoresscores  ==  modelmodel..evaluateevaluate(features_test, labels_test, verbose=1)
print("Аккуратность на тестовых данных: %.2f%%" % (scores[1]*100))
3750/3750 [==============================] - 0s 78us/step
Аккуратность на тестовых данных: 91.25%

Ожидаемо, точность работы на тестовых данных меньше, чем на обучающем и проверочном наборах.

Не забудьте сохранить нейронную сеть после окончания обучения!

Итоги

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

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

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

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