Шаг 225.
Глубокое обучение на Python. Продвинутые приемы глубокого обучения ... . Современные архитектурные шаблоны сверточных сетей. Остаточные связи

    На этом шаге мы рассмотрим использование этих связей.

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

    Как оказалось, обратное распространение в последовательной модели глубокого обучения очень похоже на игру в "испорченный телефон". У вас есть цепочка функций, например:

  y = f4(f3(f2(f1(x))))

    Цель игры - настроить параметры каждой функции в цепочке, основываясь на ошибке, полученной на выходе f4() (потеря модели). Чтобы настроить f1(), нужно передать информацию об ошибке через f2(), f3() и f4(). Однако каждая следующая функция в цепочке вносит свои искажения. Если цепочка функций слишком глубокая, искажения начинают подавлять информацию о градиенте, и обратное распространение перестает работать. Ваша модель вообще не будет обучаться. Это проблема затухания градиентов.

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


Рис.1. Остаточная связь в обход блока, выполняющего обработку

    Остаточные связи действуют как короткие пути для распространения информации в обход деструктивных блоков или блоков, вносящих существенные искажения (таких как блоки с нежелательными активациями или слоями прореживания), позволяя информации градиента ошибок проходить по глубокой сети без искажений. Этот метод был представлен в 2015 году в семействе моделей ResNet (разработанном Каймином Хе с коллегами в Microsoft).


He Kaiming et al. Deep Residual Learning for Image Recognition // Conference on Computer Vision and Pattern Recognition, 2015, https://arxiv.org/abs/1512.03385.

    На практике остаточная связь реализуется следующим образом.


Пример 9.1. Реализация остаточной связи в псевдокоде
# Некоторый входной тензор
x = ...
# Сохранить указатель на исходные данные
residual = x
# Это вычислительный блок, который может вносить искажения
x = block(x)
# Добавить исходные данные в выход слоя: получившиеся выходные
# данные будут содержать полную информацию о входе
x = add([x, residual])

    Обратите внимание: добавление входных данных блока в выходные подразумевает, что выход должен иметь ту же форму, что и вход. Однако этот прием не подходит для случаев, когда блок включает сверточные слои с увеличенным количеством фильтров или слой выбора максимального по соседям. В таких случаях можно использовать слой Conv2D 1 #× 1 без активации для линейного проецирования остатков в желаемую форму вывода (пример 9.2). Обычно сверточные слои в целевом блоке создаются с аргументом padding="same", чтобы избежать уменьшения пространственного разрешения из-за дополнения, и берутся увеличенные шаги свертки в остаточной проекции, чтобы обеспечить соответствие любому уменьшению пространственного разрешения, вызванному слоем выбора максимального по соседним значениям (пример 9.3) .


Пример 9.2. Остаточный блок, в котором изменяется число фильтров
from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation="relu")(inputs)
# Сохранить исходные данные для остаточной связи
residual = x
# Это слой, в обход которого создается остаточная связь: 
# он увеличивает количество фильтров на выходе с 32 до 64. Обратите внимание, 
# что аргумент padding="same" используется здесь для того, чтобы 
# избежать уменьшения разрешения из-за дополнения
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
# В residual имеется только 32 фильтра, поэтому мы используем слой 
# Conv2D 1 #× 1 для преобразования в требуемую форму
residual = layers.Conv2D(64, 1)(residual)
# Теперь выход блока и тензор residual имеют одинаковую форму, 
# и их можно сложить
x = layers.add([x, residual])

Пример 9.3. Случай, когда целевой блок включает слой выбора максимального по соседям
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation="relu")(inputs)
# Сохранить исходные данные для остаточной связи
residual = x
# Блок из двух слоев, вокруг которого создается остаточная связь: 
# он включает слой 2 #× 2 выбора максимального по соседям. Обратите внимание, 
# что аргумент padding="same" используется здесь в обоих слоях - Conv2D и 
# MaxPooling2D, - чтобы избежать уменьшения разрешения из-за дополнения
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.MaxPooling2D(2, padding="same")(x)
# В слое преобразования остатков используется аргумент strides=2, чтобы 
# обеспечить соответствие с уменьшенным разрешением, созданным слоем 
# MaxPooling2D
residual = layers.Conv2D(64, 1, strides=2)(residual)
# Теперь выход блока и тензор residual имеют одинаковую форму, 
# и их можно сложить
x = layers.add([x, residual])

    Для большей конкретности ниже приводится пример простой сверточной сети, организованной в серию блоков, каждый из которых состоит из двух сверточных слоев и одного необязательного слоя MaxPooling2D с остаточными связями в обход каждого блока:


inputs = keras.Input(shape=(32, 32, 3))
x = layers.Rescaling(1./255)(inputs)

# Вспомогательная функция для создания блока с двумя сверточными слоями и 
# одним необязательным слоем MaxPooling2D и остаточной связи вокруг него
def residual_block(x, filters, pooling=False): 
  residual = x 
  x = layers.Conv2D(filters, 3, activation="relu", padding="same")(x) 
  x = layers.Conv2D(filters, 3, activation="relu", padding="same")(x) 
  if pooling: 
    x = layers.MaxPooling2D(2, padding="same")(x) 
    # Если требуется добавить слой MaxPooling2D, нужно также добавить 
    # сверточный слой с увеличенным шагом свертки, чтобы преобразовать 
    # остатки в необходимую форму
    residual = layers.Conv2D(filters, 1, strides=2)(residual) 
  elif filters != residual.shape[-1]: 
    # Если слой MaxPooling2D добавлять не требуется, преобразовывать 
    # остатки нужно, только если количество каналов изменилось
    residual = layers.Conv2D(filters, 1)(residual) 
  x = layers.add([x, residual]) 
  return x
  
# Первый блок
x = residual_block(x, filters=32, pooling=True)
# Второй блок; обратите внимание, что число фильтров в каждом 
# блоке увеличивается
x = residual_block(x, filters=64, pooling=True)
# Последний блок создается без слоя MaxPooling2D, потому что далее 
# применяется слой глобального усреднения
x = residual_block(x, filters=128, pooling=False)

x = layers.GlobalAveragePooling2D()(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
model.summary()

Блокнот с этим примером можно взять здесь.

    Ниже приводится сводная информация о созданной модели:

Model: "functional"

 Layer (type)         Output Shape          Param #  Connected to      

 input_layer_2        (None, 32, 32, 3)           0  -                 
 (InputLayer)                                                          

 rescaling            (None, 32, 32, 3)           0  input_layer_2[0]: 
 (Rescaling)                                                           

 conv2d_6 (Conv2D)    (None, 32, 32,            896  rescaling[0][0]   
                      32)                                              

 conv2d_7 (Conv2D)    (None, 32, 32,          9,248  conv2d_6[0][0]    
                      32)                                              

 max_pooling2d_1      (None, 16, 16,              0  conv2d_7[0][0]    
 (MaxPooling2D)       32)                                              

 conv2d_8 (Conv2D)    (None, 16, 16,            128  rescaling[0][0]   
                      32)                                              

 add_2 (Add)          (None, 16, 16,              0  max_pooling2d_1[: 
                      32)                            conv2d_8[0][0]    

 conv2d_9 (Conv2D)    (None, 16, 16,         18,496  add_2[0][0]       
                      64)                                              

 conv2d_10 (Conv2D)   (None, 16, 16,         36,928  conv2d_9[0][0]    
                      64)                                              

 max_pooling2d_2      (None, 8, 8, 64)            0  conv2d_10[0][0]   
 (MaxPooling2D)                                                        

 conv2d_11 (Conv2D)   (None, 8, 8, 64)        2,112  add_2[0][0]       

 add_3 (Add)          (None, 8, 8, 64)            0  max_pooling2d_2[: 
                                                     conv2d_11[0][0]   

 conv2d_12 (Conv2D)   (None, 8, 8, 128)      73,856  add_3[0][0]       

 conv2d_13 (Conv2D)   (None, 8, 8, 128)     147,584  conv2d_12[0][0]   

 conv2d_14 (Conv2D)   (None, 8, 8, 128)       8,320  add_3[0][0]       

 add_4 (Add)          (None, 8, 8, 128)           0  conv2d_13[0][0],  
                                                     conv2d_14[0][0]   

 global_average_poo:  (None, 128)                 0  add_4[0][0]       
 (GlobalAveragePool:                                                   

 dense (Dense)        (None, 1)                 129  global_average_p: 


Total params: 297,697 (1.14 MB)
Trainable params: 297,697 (1.14 MB)
Non-trainable params: 0 (0.00 B)

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

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

    На следующем шаге мы рассмотрим пакетную нормализацию.




Предыдущий шаг Содержание