Создаем ResNet подобную архитектуру для классификации изображений CIFAR-10

Мы с вами видели, что относительно простые архитектуры нейронных сетей показывают низкую эффективность классификации БД изображений CIFAR-10 на уровне 70-73 %. Давайте попробуем увеличить долю правильного распознавания за счет использования более глубокой модели.

Как мы уже с вами знаем, простое увеличение числа слоев не приводит к желаемым результатам. Нужно создавать «обходные пути», которые впервые были применены в архитектуре сети ResNet. На этом занятии я лишь приведу пример нейросети, состоящей из 12 слоев, содержащей обходные пути. Этот пример целиком взят из документации по Tensorflow по адресу:

https://www.tensorflow.org/guide/keras/functional

Структура модели, следующая:

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

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

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
 
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.datasets import cifar10, mnist
 
tf.random.set_seed(1)

Затем, сформируем обучающую и тестовую выборки БД изображений CIFAR-10:

(x_train, y_train), (x_test, y_test) = cifar10.load_data()
 
x_train = x_train / 255
x_test = x_test / 255
 
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)

А, далее, будем описывать архитектуру сети. Первый блок из последовательных слоев можно представить, следующим образом:

inputs = keras.Input(shape=(32, 32, 3), name="img")
x = layers.Conv2D(32, 3, activation="relu")(inputs)
x = layers.Conv2D(64, 3, activation="relu")(x)
block_1_output = layers.MaxPooling2D(3)(x)

Его выход связываем со следующим блоком:

x = layers.Conv2D(64, 3, activation="relu", padding="same")(block_1_output)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)

И формируем суммарный сигнал с выходов x и block_1_output:

block_2_output = layers.add([x, block_1_output])

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

tf.keras.layers.Layer

Далее, суммарный выход block_2_output подаем на вход следующего блока и по аналогии формируем суммарный слой для выходов x и block_2_output:

x = layers.Conv2D(64, 3, activation="relu", padding="same")(block_2_output)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
block_3_output = layers.add([x, block_2_output])

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

x = layers.Conv2D(64, 3, activation="relu")(block_3_output)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dense(256, activation="relu")(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(10, activation='softmax')(x)

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

[batch_size, channels]

А перед выходным слоем используется Dropout() для увеличения обобщающей способности сети. Параметр 0,5 означает, что в среднем, при обучении будет использоваться половина нейронов из 256 предыдущего полносвязного слоя. Этим самым, мы пытаемся снизить (а в идеале исключить) эффект переобучения.

Все слои нейронной сети описаны с использованием функционального подхода. Теперь создадим ее модель с помощью классаModel, указывая входные и выходные слои:

model = keras.Model(inputs, outputs, name="toy_resnet")

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

model.compile(optimizer='adam',
             loss='categorical_crossentropy',
             metrics=['accuracy'])
 
model.fit(x_train, y_train, batch_size=64, epochs=15, validation_split=0.2)
 
print( model.evaluate(x_test, y_test) )

Я здесь указал 15 эпох, чтобы не возникало переобучение сети. Также используется выборка валидации для контроля за процессом переобучения. В глубоких нейронных сетях это необходимо делать, так как они «склонны» к этому недостатку.

После 15 эпох обучения, были получены следующие результаты:

Epoch 15/15
625/625 [==============================] - 75s 121ms/step - loss: 0.3810 - accuracy: 0.8675 - val_loss: 0.7246 - val_accuracy: 0.7738
313/313 [==============================] - 3s 8ms/step - loss: 0.7722 - accuracy: 0.7649
[0.7721941471099854, 0.7649000287055969]

Как видите, на тестовой выборке результаты повысились с 72% до 76% благодаря использованию более глубокой сети. Причем, сходимость в процессе обучения оказалась медленнее, чем для менее глубоких моделей. Но конечный результат стал лучше.

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

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

Видео по теме