В этом руководстве показано, как самостоятельно создать ResNet.

Увеличение глубины сети не достигается простым сложением слоев вместе. Глубокие сети трудно обучать из-за пресловутой «проблемы исчезающего градиента» — поскольку градиент распространяется обратно на более ранние слои, повторное умножение может сделать градиент окончательно малым.

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

Прежде чем вы прочитаете эту статью, я предполагаю, что вы уже знаете, что такое сверточная полносвязная сеть. Кроме того, вы должны быть знакомы с python и tensorflow2.

Conv2D в Tensorflow

Давайте посмотрим, как использовать Conv2D в Tensorflow Keras.

import tensorflow.keras as keras
from keras import layers
layers.Conv2D(filters, kernel_size, strides, padding)
  1. фильтры: Integer, размерность выходного пространства (он же выходные каналы). например, если вы вводите (h, w, c) и устанавливаете фильтры = 64, вы получите вывод (h’, w’, 64). В TensorFlow нам не нужно учитывать размер входных каналов, однако в Pytorch нам нужно учитывать размер входных каналов
  2. kernel_size: целое число или кортеж/список из двух целых чисел, указывающих высоту и ширину окна свертки 2D. Мы будем использовать только 1, 3, 7 в следующем уроке.
  3. шаги: целое число или кортеж/список из 2 целых чисел, указывающий шаги свертки, а также высоту и ширину. Может быть одним целым числом, чтобы указать одно и то же значение для всех пространственных измерений.
  4. заполнение: одно из valid или same (без учета регистра). valid означает отсутствие заполнения. same приводит к равномерному заполнению нулями слева/справа или вверх/вниз от ввода. Когда padding="same" и strides=1, выход имеет тот же размер, что и вход.

Рассчитать выходной размер

В TensorFlow мы часто используем формат channel_last. Размер тензора равен (b, h, w, c), где

  • b – размер пакета.
  • h — высота (или строки) изображения (карты объектов).
  • w — ширина (или столбцов) изображения (карты объектов).
  • c — это каналы (или характеристики, глубина) изображения (карты характеристик).

Когда мы используем padding = same , мы можем просто рассчитать высоту вывода и ширину вывода, как показано ниже:

вывод = потолок (ввод / шаги)

Если мы используем padding = valid , мы также можем рассчитать выходной размер, как показано ниже:

вывод = ceil((ввод - фильтры + 1) /шаги)

Реснет18, 34

Существует много видов ResNet, поэтому сначала мы видим самый простой, ResNet18. Предположим, что наш ввод — это изображение RGB 224*224, а вывод — 1000 классов.

Есть 3 основных компонента, которые составляют сеть.

  1. входной уровень (conv1 + max pooling) (обычно называется уровнем 0)
  2. ResBlocks (conv2 без максимального pooing ~ conv5) (обычно обозначается как layer1 ~ layer4)
  3. последний слой

ШАГ 0: ResBlocks (уровень 1 ~ слой 4)

Самый важный компонент — ResBlocks. Давайте посмотрим, как его собрать!

В приведенной выше реализации есть 3 проблемы.

  1. Нам нужно понизить выборку (т. е. уменьшить размер карты объектов) на conv3_1, conv4_1 и conv5_1
  2. Мы можем использовать переменную для управления количеством выходных фильтров.
  3. Мы должны применить пакетную нормализацию и функцию ReLu посреди слоев.

ШАГ 1: Входной слой (layer0)

Layer0 состоит из свертки 7*7 и максимального пула 3*3.

self.layer0 = keras.Sequential([
    layers.Conv2D(64, 7, 2, padding='same'),
    layers.MaxPool2D(pool_size=3, strides=2, padding='same'),
    layers.BatchNormalization(),
    layers.ReLU()
], name='layer0')

ШАГ 2: Заключительный слой

Последний слой состоит из глобального среднего пула (gap) и полносвязного слоя (fc). GAP вычисляет среднее значение каждой карты объектов (h, w, 1), а затем объединяет все значения в список.

self.gap = layers.GlobalAveragePooling2D()
self.fc = laers.Dense(1000, activation='softmax')

ШАГ 3: Готово!

class ResNet18(keras.Model):
    def __init__(self, outputs=1000):
        super().__init__()
        self.layer0 = keras.Sequential([
            layers.Conv2D(64, 7, 2, padding='same'),
            layers.MaxPool2D(pool_size=3, strides=2, padding='same'),
            layers.BatchNormalization(),
            layers.ReLU()
        ], name='layer0')

        self.layer1 = keras.Sequential([
            ResBlock(64, downsample=False),
            ResBlock(64, downsample=False)
        ], name='layer1')

        self.layer2 = keras.Sequential([
            ResBlock(128, downsample=True),
            ResBlock(128, downsample=False)
        ], name='layer2')

        self.layer3 = keras.Sequential([
            ResBlock(256, downsample=True),
            ResBlock(256, downsample=False)
        ], name='layer3')

        self.layer4 = keras.Sequential([
            ResBlock(512, downsample=True),
            ResBlock(512, downsample=False)
        ], name='layer4')

        self.gap = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(outputs, activation='softmax')
    def call(self, input):
        input = self.layer0(input)
        input = self.layer1(input)
        input = self.layer2(input)
        input = self.layer3(input)
        input = self.layer4(input)
        input = self.gap(input)
        input = self.fc(input)

        return input

Чтобы построить ResNet34, нам нужно только изменить количество ResBlocks в ResNet18.

class ResNet34(keras.Model):
    def __init__(self, outputs=1000):
        super().__init__()
        self.layer0 = keras.Sequential([
            layers.Conv2D(64, 7, 2, padding='same'),
            layers.MaxPool2D(pool_size=3, strides=2, padding='same'),
            layers.BatchNormalization(),
            layers.ReLU()
        ], name='layer0')

        self.layer1 = keras.Sequential([
            ResBlock(64, downsample=False),
            ResBlock(64, downsample=False),
            ResBlock(64, downsample=False)
        ], name='layer1')

        self.layer2 = keras.Sequential([
            ResBlock(128, downsample=True),
            ResBlock(128, downsample=False),
            ResBlock(128, downsample=False),
            ResBlock(128, downsample=False)
        ], name='layer2')

        self.layer3 = keras.Sequential([
            ResBlock(256, downsample=True),
            ResBlock(256, downsample=False),
            ResBlock(256, downsample=False),
            ResBlock(256, downsample=False),
            ResBlock(256, downsample=False),
            ResBlock(256, downsample=False)
        ], name='layer3')

        self.layer4 = keras.Sequential([
            ResBlock(512, downsample=True),
            ResBlock(512, downsample=False),
            ResBlock(512, downsample=False)
        ], name='layer4')

        self.gap = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(outputs, activation='softmax')
    def call(self, input):
        input = self.layer0(input)
        input = self.layer1(input)
        input = self.layer2(input)
        input = self.layer3(input)
        input = self.layer4(input)
        input = self.gap(input)
        input = self.fc(input)

        return input

Реснет50, 101, 152

ШАГ0: ResBottleneckBlock

Самая большая разница между ResNet34 и ResNet50 — это ResBlocks. нам нужно переписать другую версию, и мы называем новую версию «ResBottleneckBlock».

Существует 3 типа ResBottleneckBlock.

Мы можем думать об этих 3 типах как о 2 состояниях. Один — это ярлык, который ничего не делает, а другой — ярлык, который должен что-то делать. Когда функции не нуждаются в субдискретизации, а входной канал равен выходному каналу, ярлык ничего не делает. Когда функциям требуется либо субдискретизация, либо изменение каналов, ярлык что-то делает.

def __init__(self, filters, downsample):
    super().__init__()
    self.filters = filters
    self.downsample = downsample
    self.conv1 = layers.Conv2D(filters, 1, 1, padding='same')
    
    if downsample:
        self.conv2 = layers.Conv2D(filters, 3, 2, padding='same')
    else:
        self.conv2 = layers.Conv2D(filters, 3, 1, padding='same')
    
    self.conv3 = layers.Conv2D(filters*4, 1, 1, padding='same')

Легко реализовать conv1, conv2 и conv3. Давайте посмотрим, как реализовать ярлык!

Когда мы реализуем ярлык, мы должны знать, как получить входные каналы. На самом деле это не сложно! Нам просто нужно расширить метод build() из layer.Layer или keras.Model, чтобы получить input_shape. input_shape — это список с измерениями. Как упоминалось ранее, в TensorFlow мы обычно используем формат channel_last, поэтому мы можем получить количество входных каналов, получив последнее значение входного канала.

def build(self, input_shape):
    if self.downsample or self.filters * 4 != input_shape[-1]:
        self.shortcut = layers.Conv2D(self.filters*4, 1,
            2 if self.downsample else 1, padding='same')
    else:
        self.shortcut = keras.Sequential()
class ResBottleneckBlock(keras.Model):
    def __init__(self, filters, downsample):
        super().__init__()
        self.downsample = downsample
        self.filters = filters
        self.conv1 = layers.Conv2D(filters, 1, 1)
        if downsample:
            self.conv2 = layers.Conv2D(filters, 3, 2, padding='same')
        else:
            self.conv2 = layers.Conv2D(filters, 3, 1, padding='same')
        self.conv3 = layers.Conv2D(filters*4, 1, 1)

    def build(self, input_shape):
        if self.downsample or self.filters * 4 != input_shape[-1]:
            self.shortcut = keras.Sequential([
                layers.Conv2D(
                    self.filters*4, 1, 2 if self.downsample else 1, padding='same'),
                layers.BatchNormalization()
            ])
        else:
            self.shortcut = keras.Sequential()

    def call(self, input):
        shortcut = self.shortcut(input)

        input = self.conv1(input)
        input = layers.BatchNormalization()(input)
        input = layers.ReLU()(input)

        input = self.conv2(input)
        input = layers.BatchNormalization()(input)
        input = layers.ReLU()(input)

        input = self.conv3(input)
        input = layers.BatchNormalization()(input)
        input = layers.ReLU()(input)

        input = input + shortcut
        return layers.ReLU()(input)

Мы можем создать 3 типа ResBottleneckBlock отдельно, как показано на рисунке 2.

  • Слева: ResBottleneckBlock(64, downsample=False)
  • Середина: ResBottleneckBlock(64, downsample=False)
  • Справа: ResBottleneckBlock(128, downsample=True)

Разница между левым и средним в том, что у них разные входные каналы.

ШАГ 1: Готово!

class ResNet(keras.Model):
    def __init__(self, repeat, outputs=1000):
        super().__init__()
        self.layer0 = keras.Sequential([
            layers.Conv2D(64, 7, 2, padding='same'),
            layers.MaxPool2D(pool_size=3, strides=2, padding='same'),
            layers.BatchNormalization(),
            layers.ReLU()
        ], name='layer0')

        self.layer1 = keras.Sequential([
            ResBottleneckBlock(64, downsample=False) for _ in range(repeat[0])
        ], name='layer1')

        self.layer2 = keras.Sequential([
            ResBottleneckBlock(128, downsample=True)
        ] + [
            ResBottleneckBlock(128, downsample=False) for _ in range(1, repeat[1])
        ], name='layer2')

        self.layer3 = keras.Sequential([
            ResBottleneckBlock(256, downsample=True)
        ] + [
            ResBottleneckBlock(256, downsample=False) for _ in range(1, repeat[2])
        ], name='layer3')

        self.layer4 = keras.Sequential([
            ResBottleneckBlock(512, downsample=True)
        ] + [
            ResBottleneckBlock(512, downsample=False) for _ in range(1, repeat[3])
        ], name='layer4')

        self.gap = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(outputs, activation='softmax')

    def call(self, input):
        input = self.layer0(input)
        input = self.layer1(input)
        input = self.layer2(input)
        input = self.layer3(input)
        input = self.layer4(input)
        input = self.gap(input)
        input = self.fc(input)

        return input
class ResNet50(ResNet):
    def __init__(self):
        super().__init__([3, 4, 6, 3])
    def call(self, input):
        return super().call(input)

class ResNet101(ResNet):
    def __init__(self):
        super().__init__([3, 4, 23, 3])
    def call(self, input):
        return super().call(input)

class ResNet152(ResNet):
    def __init__(self):
        super().__init__([3, 8, 36, 3])
    def call(self, input):
        return super().call(input)

Полный код смотрите здесь: https://github.com/ksw2000/ML-Notebook/blob/main/ResNet/ResNet_TensorFlow.ipynb

Ссылка

  1. https://arxiv.org/pdf/1512.03385.pdf
  2. https://zhuanlan.zhihu.com/p/141998754
  3. https://blog.csdn.net/abc13526222160/article/details/90057121