Предварительно обученные нейронные сети можно использовать для решения не только тех задач, на которых они обучались, но и других, достаточно похожих, с помощью технологии переноса обучения (transfer learning). Для этого от предварительно обученной сети “обрезается” классификатор и вместо него добавляется новый, приспособленный для нашей задачи. Например, вместо классификатора, обученного на наборе данных ImageNet с 1000 классов, мы добавляли в нейронную сеть классификатор для распознавания собак и кошек, в котором всего два класса. Затем этот классификатор обучается на новых данных.

Тонкая настройка сети (fine tuning) позволяет пойти дальше и еще больше увеличить качество работы предварительно обученной сети на новой задаче. Для этого обучается не только новый классификатор, который был добавлен в сеть, но и некоторые слои предварительно обученной нейронной сети. Это особенно эффективно, когда новый набор данных достаточно сильно отличается от исходного набора, на котором обучалась сеть.

Алгоритм тонкой настройки предварительно обученной нейронной сети

Для тонкой настройки сети необходимо выполнить следующие действия:

  1. Заменить классификатор предварительно обученной нейронной сети новым классификатором, подходящим под нашу задачу.
  2. “Заморозить” сверточные слои предварительно обученной нейронной сети. В результате эти слои не будут обучаться.
  3. Провести обучение составной сети с новым классификатором на новом наборе данных.
  4. “Разморозить” несколько слоев сверточной части предварительно обученной нейронной сети.
  5. Дообучить сеть с размороженными сверточными слоями на новом наборе данных. Именно этот этап и называется тонкой настройкой (fine tuning).

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

Тонкая настройка нейронной сети для распознавание собак и кошек

Мы будем продолжать рассматривать пример распознавания собак и кошек из соревнования Kaggle с помощью нейронной сети VGG16.

Обучаем сеть с новым классификатором

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

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

Полный текст программы есть в статье “Перенос обучения в Keras.”

Убедиться, что слои сети VGG16 не обучаются, можно в выводе vgg16_net.summary():

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

Количество параметров, которые обучаются (Trainable params), равно 0.

Затем создаем составную сеть с VGG16 и новым классификатором:

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.compile(loss='binary_crossentropy',
              optimizer=Adam(lr=1e-5), 
              metrics=['accuracy'])
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)

Теперь наша сеть готова к тонкой настройке. Но перед этим оценим качество работы сети на тестовых данных

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

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

Тонкая настройка сети

Первый этап тонкой настройки – разморозить несколько сверточных слоев в сети. VGG16 состоит из нескольких блоков, в каждом из которых несколько сверточных слоев и слой подвыборки. Мы разморозим последний блок с номером 5. В этом блоке четыре слоя, их имена (см. вывод vgg16_net.summary() выше):

  • block5_conv1 - первый сверточный слой;
  • block5_conv2 - второй сверточный слой;
  • block5_conv3 - третий сверточный слой;
  • block5_pool - слой подвыборки.

Код для “разморозки” сверточных слоев VGG16:

# Разрешаем обучение на уровне сети
vgg16_net.trainable = True
# Флаг, который говорит о том, обучается слой, или нет
# На первом этапе флаг равен False, слои не обучаются
trainable = False
# В цикле перебираем слои сети VGG16
for layer in vgg16_net.layers:
    # Как только дошли до слоя с именем block5_conv1
    # устанавливаем флаг trainable в True
    # Все слои после block5_conv1 (включая его), будут обучаться
    if layer.name == 'block5_conv1':
        trainable = True
    # Устанавливаем признак обучения отдельно для каждого слоя
    layer.trainable = trainable

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

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: 9,177,089
Non-trainable params: 7,635,264
_________________________________________________________________

Количество обучаемых параметров (Trainable params) примерно 9 млн. При этом в новом классификаторе (слой Flatten и последующие) примерно 2 млн. параметров. Остальные 7 млн. относятся к сверточной части сети VGG16.

После “разморозки” сверточных уровней сеть нужно скопмилировать заново:

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

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

После компиляции можно обучить сеть с “размороженными” сверточными слоями на новом наборе данных. При этом не обязательно использовать большое количество эпох:

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

Результаты оценки качества работы сети после тонкой настройки:

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

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

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

Итоги

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

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

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

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