Использование функционального API Keras для построения остаточной нейронной сети

Что такое остаточная нейронная сеть?

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

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

Остаточные сети пытаются решить эту проблему, добавляя так называемые пропускаемые соединения. Пропускное соединение показано на изображении выше. Как я сказал ранее, более глубокие сети должны иметь возможность изучать, по крайней мере, сопоставления идентичности; это то, что делают пропущенные соединения: они добавляют сопоставления идентичности из одной точки в сети в точку пересылки, а затем позволяют сети узнать только это дополнительное 𝐹 (𝑥). Если сети нечего больше узнать, то она просто узнает 𝐹 (𝑥) как равное 0. Оказывается, сети легче выучить отображение, близкое к 0, чем отображение идентичности.

Блок с пропуском соединения, как на изображении выше, называется остаточным блоком, а остаточная нейронная сеть (ResNet) - это просто конкатенация таких блоков.

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

Краткое введение в Keras Functional API

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

from keras.models import Sequential
from keras.layers import Dense, Activation

model = Sequential([
    Dense(32, input_shape=(784,)),
    Activation('relu'),
    Dense(10),
    Activation('softmax'),
])

Но этого способа построения нейронных сетей недостаточно для наших нужд. С классом Sequential мы не можем добавлять пропускаемые соединения. У Keras также есть класс Model, который можно использовать вместе с функциональным API для создания слоев для построения более сложных сетевых архитектур.
При построении класс keras.layers.Input возвращает тензорный объект. Объект слоя в Keras также можно использовать как функцию, вызывая ее с тензорным объектом в качестве параметра. Возвращаемый объект - это тензор, который затем может быть передан в качестве входных данных другому слою и т. Д.

В качестве примера:

from keras.layers import Input, Dense
from keras.models import Model

inputs = Input(shape=(784,))
output_1 = Dense(64, activation='relu')(inputs)
output_2 = Dense(64, activation='relu')(output_1)
predictions = Dense(10, activation='softmax')(output_2)

model = Model(inputs=inputs, outputs=predictions)
model.compile(optimizer='adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(data, labels)

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

Пример, использующий Add:

from keras.layers import Input, Dense, Add
from keras.models import Model

input1 = Input(shape=(16,))
x1 = Dense(8, activation='relu')(input1)
input2 = Input(shape=(32,))
x2 = Dense(8, activation='relu')(input2)

added = Add()([x1, x2])

out = Dense(4)(added)
model = Model(inputs=[input1, input2], outputs=out)

Это ни в коем случае не исчерпывающее руководство по функциональному API Keras. Если вы хотите узнать больше, обратитесь к документации.

Давайте реализуем ResNet

Затем мы реализуем ResNet вместе с его простым (без пропуска соединений) аналогом для сравнения.

ResNet, которую мы здесь построим, имеет следующую структуру:

  • Ввод с формой (32, 32, 3)
  • 1 слой Conv2D с 64 фильтрами
  • 2, 5, 5, 2 остаточных блока с 64, 128, 256 и 512 фильтрами
  • Слой AveragePooling2D с размером пула = 4
  • Сгладить слой
  • Плотный слой с 10 выходными узлами

Он имеет в общей сложности 30 конв + плотных слоев. Все размеры ядра 3x3. Мы используем активацию ReLU и BatchNormalization после слоев свертки.
Обычная версия такая же, за исключением пропуска соединений.

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

def relu_bn(inputs: Tensor) -> Tensor:
    relu = ReLU()(inputs)
    bn = BatchNormalization()(relu)
    return bn

Затем мы создаем функцию для построения остаточного блока. В качестве входных данных он принимает тензор x и передает его через 2 сверточных слоя; назовем вывод этих двух сверточных слоев как y. Затем добавляет вход x к y, добавляет relu и пакетную нормализацию, а затем возвращает результирующий тензор. Когда параметр downsample == True первый сверточный слой использует strides=2, чтобы вдвое уменьшить размер вывода, и мы используем сверточный слой с kernel_size=1 на входе x, чтобы придать ему ту же форму, что и y. Слой Add требует, чтобы входные тензоры имели одинаковую форму.

def residual_block(x: Tensor, downsample: bool, filters: int,                                        kernel_size: int = 3) -> Tensor:
    y = Conv2D(kernel_size=kernel_size,
               strides= (1 if not downsample else 2),
               filters=filters,
               padding="same")(x)
    y = relu_bn(y)
    y = Conv2D(kernel_size=kernel_size,
               strides=1,
               filters=filters,
               padding="same")(y)

    if downsample:
        x = Conv2D(kernel_size=1,
                   strides=2,
                   filters=filters,
                   padding="same")(x)
    out = Add()([x, y])
    out = relu_bn(out)
    return out

create_res_net() объединяет все вместе.
Вот полный код для этого:

Обычная сеть построена аналогичным образом, но в ней нет пропуска соединений, и мы не используем вспомогательную функцию residual_block(); все делается внутри create_plain_net().
Код для простой сети:

Обучение работе с CIFAR-10 и просмотр результатов

CIFAR-10 - это набор данных 32x32 rgb изображений по 10 категориям. Он содержит 50 тыс. Изображений поездов и 10 тыс. Тестовых изображений.
Ниже приведены 10 случайных изображений из каждого класса:

Мы обучим ResNet и PlainNet работе с этим набором данных за 20 эпох, а затем сравним результаты.

Обучение длилось около 55 минут для каждой ResNet и PlainNet на машине с 1 NVIDIA Tesla K80. Нет существенной разницы во времени обучения между ResNet и PlainNet.
Полученные нами результаты показаны ниже.

Таким образом, мы получили увеличение точности проверки на 1,59% за счет использования ResNet для этого набора данных. В более глубоких сетях разница должна быть больше. Не стесняйтесь экспериментировать и видеть результаты, которые вы получите.

использованная литература

  1. Глубокое остаточное обучение для распознавания изображений
  2. Остаточная нейронная сеть - Википедия
  3. Руководство по функциональному API - документация Keras
  4. Модель (функциональный API) - документация Keras
  5. Объединить слои - документация Keras
  6. Наборы данных CIFAR-10 и CIFAR-100

Надеюсь, эта информация была для вас полезной, и спасибо за внимание!

Эта статья также размещена на моем собственном сайте здесь. Не стесняйтесь смотреть!