В этом руководстве показано, как самостоятельно создать 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)
- фильтры: Integer, размерность выходного пространства (он же выходные каналы). например, если вы вводите (h, w, c) и устанавливаете фильтры = 64, вы получите вывод (h’, w’, 64). В TensorFlow нам не нужно учитывать размер входных каналов, однако в Pytorch нам нужно учитывать размер входных каналов
- kernel_size: целое число или кортеж/список из двух целых чисел, указывающих высоту и ширину окна свертки 2D. Мы будем использовать только 1, 3, 7 в следующем уроке.
- шаги: целое число или кортеж/список из 2 целых чисел, указывающий шаги свертки, а также высоту и ширину. Может быть одним целым числом, чтобы указать одно и то же значение для всех пространственных измерений.
- заполнение: одно из
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 основных компонента, которые составляют сеть.
- входной уровень (conv1 + max pooling) (обычно называется уровнем 0)
- ResBlocks (conv2 без максимального pooing ~ conv5) (обычно обозначается как layer1 ~ layer4)
- последний слой
ШАГ 0: ResBlocks (уровень 1 ~ слой 4)
Самый важный компонент — ResBlocks. Давайте посмотрим, как его собрать!
В приведенной выше реализации есть 3 проблемы.
- Нам нужно понизить выборку (т. е. уменьшить размер карты объектов) на
conv3_1
,conv4_1
иconv5_1
- Мы можем использовать переменную для управления количеством выходных фильтров.
- Мы должны применить пакетную нормализацию и функцию 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