DenseNet (Densely Connected Convolutional Networks) - одна из новейших нейронных сетей для распознавания визуальных объектов. Он очень похож на ResNet, но имеет несколько принципиальных отличий.

Со всеми улучшениями DenseNets имеет один из самых низких показателей ошибок в наборах данных CIFAR / SVHN:

А для набора данных ImageNet DenseNets требует меньше параметров, чем ResNet, с той же точностью:

Этот пост предполагает предыдущие знания нейронных сетей (NN) и сверток (convs). Здесь я не буду объяснять, как работают NN или convs, а сосредоточусь на двух темах:

  • Чем плотная сеть отличается от других сверточных сетей.
  • С какими трудностями я столкнулся при реализации DenseNet в тензорном потоке.

Если вы знаете, как работает DenseNets, и вас интересует только реализация тензорного потока, смело переходите ко второй главе или проверяйте исходный код на GitHub. Если вы не знакомы с какими-либо темами, но хотите получить некоторые знания - очень советую вам CS231n Stanford classes.

Сравните DenseNet с другими сверточными сетями

Обычно ConvNets работают так:
У нас есть исходное изображение, скажем, имеющее форму (28, 28, 3). После того, как мы применили к нему набор фильтров свертки / объединения, сжимая размеры по ширине и высоте и увеличивая размер функций.
Таким образом, выходные данные слоя Lᵢ вводятся в слой Lᵢ₊₁. Вроде так:

В архитектуре ResNet предложено остаточное соединение, от предыдущих слоев к текущему. Грубо говоря, вход в слой Lᵢ был получен путем суммирования выходных данных с предыдущих слоев.

В отличие от этого, документ DenseNet предлагает объединение выходных данных с предыдущих слоев вместо использования суммирования.
Итак, давайте представим, что у нас есть изображение с формой (28, 28, 3). Сначала мы распространяем изображение на начальные 24 канала и получаем изображение (28, 28, 24). Каждый следующий слой свертки будет генерировать k = 12 элементов, а ширина и высота останутся неизменными.
Результатом слоя Lᵢ будет (28, 28, 12).
Но вход в Lᵢ₊₁ будет быть (28, 28, 24 + 12), для Lᵢ₊₂ (28, 28, 24 + 12 + 12) и так далее.

Через некоторое время мы получаем изображение той же ширины и высоты, но с множеством функций (28, 28, 48).
Все эти N слоев на бумаге называются Block. Также есть пакетная нормализация, нелинейность и выпадение внутри блока.
Чтобы уменьшить размер, DenseNet использует переходные слои. Эти слои содержат свертку с размером ядра = 1, за которой следует среднее объединение 2x2 с шагом = 2. Это уменьшает размеры по высоте и ширине, но оставляет размер признаков прежним. В результате мы получаем изображение с формами (14, 14, 48).

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

Примечания о реализации

В статье существует два класса сетей: для наборов данных ImageNet и CIFAR / SVHN. Подробнее об этом я расскажу позже.

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

Во-вторых, я попытался понять, сколько функций должна сгенерировать сеть на начальном уровне свертки (перед всеми блоками). Согласно исходному коду количество первых функций должно быть равно скорости роста (k) * 2.

Несмотря на то, что по умолчанию у нас три блока, мне было интересно построить сеть с другим параметром. Таким образом, каждый блок не был жестко закодирован вручную, а вызывался N раз как функция. Последняя итерация была выполнена без переходного слоя. Упрощенный пример:

for block in range(required_blocks):
    output = build_block(output)
    if block != (required_blocks — 1):
        output = transition_layer(output)

Для инициализации весов авторы предложили использовать инициализацию MRSA (согласно этой статье). В тензорном потоке эту инициализацию легко реализовать с помощью инициализатора масштабирования дисперсии.

В последней версии бумаги были введены плотные сети со слоями горлышка бутылки. Основное отличие этой сети в том, что каждый блок теперь содержит два сверточных фильтра. Первый - это конв. 1x1, а второй - как обычно - 3x3 конв. Итак, весь блок теперь будет:

batch norm -> relu -> conv 1x1 -> dropout -> batch norm -> relu -> conv 3x3 -> dropout -> output.

Несмотря на два фильтра conv, только последний вывод будет объединен с основным пулом функций.

Также на переходных слоях будут уменьшены не только ширина и высота, но и характеристики. Итак, если у нас есть форма изображения после одного блока (28, 28, 48) после переходного слоя, мы получим (14, 14, 24).

Где theta - некоторые значения приведения в диапазоне (0, 1).

В случае использования DenseNet со слоями узких мест общая глубина будет разделена на 2. Это означает, что если с глубиной 20 у вас ранее было 16 сверточных слоев 3x3 (некоторые слои являются переходными), то теперь у вас будет 8 сверточных слоев 1x1 и 8 3x3. извилины.

И последнее, но не менее важное, о предварительной обработке данных. В статье использовалась нормализация по каналам. При таком подходе каждый канал изображения следует уменьшить на среднее значение и разделить на стандартное отклонение. Во многих реализациях использовалась другая нормализация - просто разделите каждый пиксель изображения на 255, чтобы у нас были значения пикселей в диапазоне [0, 1].

Сначала я реализовал решение, которое делит изображение на 255. Все работает нормально, но немного хуже, чем результаты, представленные в статье. Ладно, дальше я реализовал поканальную нормализацию ... И сети стали работать еще хуже. Мне было непонятно почему. Поэтому я решил написать авторам. Спасибо Чжуан Лю, который ответил мне и указал на другой исходный код, который я почему-то пропустил. После точной отладки становится очевидным, что изображения должны быть нормализованы по среднему / стандартному значению всех изображений в наборе данных (обучающем или тестовом), а не только по отдельности.

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

# without this line next slice assignment will silently fail!
# at least in numpy 1.12.0
images = images.astype(‘float64’)
for i in range(channels):
    images[:, :, :, i] = (
        (images[:, :, :, i] — self.images_means[i]) /
         self.images_stds[i])

Заключение

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

На этом пока все! Надеюсь, этот пост был вам чем-то полезен или указал на некоторые интересные идеи. Исходный код можно найти в этом репозитории. Спасибо за прочтение!