.. _sec_resnet: Redes Residuais (ResNet) ======================== À medida que projetamos redes cada vez mais profundas, torna-se imperativo entender como a adição de camadas pode aumentar a complexidade e a expressividade da rede. Ainda mais importante é a capacidade de projetar redes onde adicionar camadas torna as redes estritamente mais expressivas, em vez de apenas diferentes. Para fazer algum progresso, precisamos de um pouco de matemática. Classes Função -------------- Considere :math:`\mathcal{F}`, a classe de funções que uma arquitetura de rede específica (junto com as taxas de aprendizado e outras configurações de hiperparâmetros) pode alcançar. Ou seja, para todos os :math:`f \in \mathcal{F}` existe algum conjunto de parâmetros (por exemplo, pesos e vieses) que podem ser obtidos através do treinamento em um conjunto de dados adequado. Vamos supor que :math:`f^*` seja a função “verdade” que realmente gostaríamos de encontrar. Se estiver em :math:`\mathcal{F}`, estamos em boa forma, mas normalmente não teremos tanta sorte. Em vez disso, tentaremos encontrar :math:`f^*_\mathcal{F}`, que é nossa melhor aposta em :math:`\mathcal{F}`. Por exemplo, dado um conjunto de dados com recursos :math:`\mathbf{X}` e rótulos :math:`\mathbf{y}`, podemos tentar encontrá-lo resolvendo o seguinte problema de otimização: .. math:: f^*_\mathcal{F} \stackrel{\mathrm{def}}{=} \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \text{ subject to } f \in \mathcal{F}. É razoável supor que, se projetarmos uma arquitetura diferente e mais poderosa :math:`\mathcal{F}'`, chegaremos a um resultado melhor. Em outras palavras, esperaríamos que :math:`f^*_{\mathcal{F}'}` seja “melhor” do que :math:`f^*_{\mathcal{F}}`. No entanto, se :math:`\mathcal{F} \not\subseteq \mathcal{F}'` não há garantia de que isso acontecerá. Na verdade, :math:`f^*_{\mathcal{F}'}` pode muito bem ser pior. Conforme ilustrado por :numref:`fig_functionclasses`, para classes de função não aninhadas, uma classe de função maior nem sempre se aproxima da função “verdade” :math:`f^*`. Por exemplo, à esquerda de: numref: ``fig_functionclasses``, embora\ :math:`\mathcal{F}_3` esteja mais perto de :math:`f^*` do que :math:`\mathcal{F}_1`, :math:`\mathcal{F}_6` se afasta e não há garantia de que aumentar ainda mais a complexidade pode reduzir o distância de :math:`f^*`. Com classes de função aninhadas onde :math:`\mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6` à direita de :numref:`fig_functionclasses`, podemos evitar o problema mencionado nas classes de função não aninhadas. .. _fig_functionclasses: .. figure:: ../img/functionclasses.svg Para classes de função não aninhadas, uma classe de função maior (indicada por área) não garante a aproximação da função “verdade” (:math:`f^*`). Isso não acontece em classes de funções aninhadas. Por isso, somente se as classes de função maiores contiverem as menores teremos a garantia de que aumentá-las aumenta estritamente o poder expressivo da rede. Para redes neurais profundas, se pudermos treinar a camada recém-adicionada em uma função de identidade :math:`f(\mathbf{x}) = \mathbf{x}`, o novo modelo será tão eficaz quanto o modelo original. Como o novo modelo pode obter uma solução melhor para se ajustar ao conjunto de dados de treinamento, a camada adicionada pode facilitar a redução de erros de treinamento. Essa é a pergunta que He et al. considerado quando se trabalha em modelos de visão computacional muito profundos :cite:`He.Zhang.Ren.ea.2016`. No cerne de sua proposta de *rede residual* (*ResNet*) está a ideia de que cada camada adicional deve mais facilmente conter a função de identidade como um de seus elementos. Essas considerações são bastante profundas, mas levaram a uma soluçao surpreendentemente simples, um *bloco residual*. Com ele, a ResNet venceu o Desafio de Reconhecimento Visual em Grande Escala da ImageNet em 2015. O design teve uma profunda influência em como construir redes neurais profundas. Blocos Residuais ---------------- Vamos nos concentrar em uma parte local de uma rede neural, conforme descrito em :numref:`fig_residual_block`. Denote a entrada por :math:`\mathbf{x}`. Assumimos que o mapeamento subjacente desejado que queremos obter aprendendo é :math:`f(\mathbf{x})`, a ser usado como entrada para a função de ativação no topo. À esquerda de :numref:`fig_residual_block`, a parte dentro da caixa de linha pontilhada deve aprender diretamente o mapeamento :math:`f(\mathbf{x})`. A direita, a parte de dentro da caixa de linha pontilhada precisa aprender o *mapeamento residual* :math:`f(\mathbf{x}) - \mathbf{x}`, que é como o bloco residual deriva seu nome. Se o mapeamento de identidade :math:`f(\mathbf{x}) = \mathbf{x}` for o mapeamento subjacente desejado, o mapeamento residual é mais fácil de aprender: nós só precisamos empurrar os pesos e preconceitos da camada de peso superior (por exemplo, camada totalmente conectada e camada convolucional) dentro da caixa de linha pontilhada a zero. A figura certa em :numref:`fig_residual_block` ilustra o *bloco residual* do ResNet, onde a linha sólida carregando a entrada da camada :math:`\mathbf{x}` para o operador de adição é chamada de *conexão residual* (ou *conexão de atalho*). Com blocos residuais, as entradas podem para a frente se propagam mais rápido através das conexões residuais entre as camadas. .. _fig_residual_block: .. figure:: ../img/residual-block.svg Um bloco regular (esquerda) e um bloco residual (direita). ResNet segue o design de camada convolucional :math:`3\times 3` completo do VGG. O bloco residual tem duas camadas convolucionais :math:`3\times 3` com o mesmo número de canais de saída. Cada camada convolucional é seguida por uma camada de normalização em lote e uma função de ativação ReLU. Em seguida, pulamos essas duas operações de convolução e adicionamos a entrada diretamente antes da função de ativação final do ReLU. Esse tipo de projeto requer que a saída das duas camadas convolucionais tenham o mesmo formato da entrada, para que possam ser somadas. Se quisermos mudar o número de canais, precisamos introduzir uma camada convolucional adicional :math:`1\times 1` para transformar a entrada na forma desejada para a operação de adição. Vamos dar uma olhada no código abaixo. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python from mxnet import np, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np() class Residual(nn.Block): #@save """The Residual block of ResNet.""" def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): super().__init__(**kwargs) self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, strides=strides) self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2D(num_channels, kernel_size=1, strides=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm() self.bn2 = nn.BatchNorm() def forward(self, X): Y = npx.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) return npx.relu(Y + X) .. raw:: html
.. raw:: html
.. code:: python import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l class Residual(nn.Module): #@save """The Residual block of ResNet.""" def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides) self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm2d(num_channels) self.bn2 = nn.BatchNorm2d(num_channels) def forward(self, X): Y = F.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) Y += X return F.relu(Y) .. raw:: html
.. raw:: html
.. code:: python import tensorflow as tf from d2l import tensorflow as d2l class Residual(tf.keras.Model): #@save """The Residual block of ResNet.""" def __init__(self, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = tf.keras.layers.Conv2D( num_channels, padding='same', kernel_size=3, strides=strides) self.conv2 = tf.keras.layers.Conv2D( num_channels, kernel_size=3, padding='same') self.conv3 = None if use_1x1conv: self.conv3 = tf.keras.layers.Conv2D( num_channels, kernel_size=1, strides=strides) self.bn1 = tf.keras.layers.BatchNormalization() self.bn2 = tf.keras.layers.BatchNormalization() def call(self, X): Y = tf.keras.activations.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3 is not None: X = self.conv3(X) Y += X return tf.keras.activations.relu(Y) .. raw:: html
.. raw:: html
Este código gera dois tipos de redes: uma onde adicionamos a entrada à saída antes de aplicar a não linearidade ReLU sempre que ``use_1x1conv = False``, e outra onde ajustamos os canais e a resolução por meio de uma convolução :math:`1 \times 1` antes de adicionar. :numref:`fig_resnet_block` ilustra isso: .. _fig_resnet_block: .. figure:: ../img/resnet-block.svg Bloco ResNet com e sem convolução :math:`1 \times 1`. Agora, vejamos uma situação em que a entrada e a saída têm a mesma forma. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python blk = Residual(3) blk.initialize() X = np.random.uniform(size=(4, 3, 6, 6)) blk(X).shape .. parsed-literal:: :class: output (4, 3, 6, 6) .. raw:: html
.. raw:: html
.. code:: python blk = Residual(3,3) X = torch.rand(4, 3, 6, 6) Y = blk(X) Y.shape .. parsed-literal:: :class: output torch.Size([4, 3, 6, 6]) .. raw:: html
.. raw:: html
.. code:: python blk = Residual(3) X = tf.random.uniform((4, 6, 6, 3)) Y = blk(X) Y.shape .. parsed-literal:: :class: output TensorShape([4, 6, 6, 3]) .. raw:: html
.. raw:: html
Também temos a opção de reduzir pela metade a altura e largura de saída, aumentando o número de canais de saída. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python blk = Residual(6, use_1x1conv=True, strides=2) blk.initialize() blk(X).shape .. parsed-literal:: :class: output (4, 6, 3, 3) .. raw:: html
.. raw:: html
.. code:: python blk = Residual(3,6, use_1x1conv=True, strides=2) blk(X).shape .. parsed-literal:: :class: output torch.Size([4, 6, 3, 3]) .. raw:: html
.. raw:: html
.. code:: python blk = Residual(6, use_1x1conv=True, strides=2) blk(X).shape .. parsed-literal:: :class: output TensorShape([4, 3, 3, 6]) .. raw:: html
.. raw:: html
Modelo ResNet ------------- As duas primeiras camadas do ResNet são iguais às do GoogLeNet que descrevemos antes: a camada convolucional :math:`7\times 7` com 64 canais de saída e uma passada de 2 é seguida pela camada de pooling máxima :math:`3\times 3` com uma passada de 2. A diferença é a camada de normalização de lote adicionada após cada camada convolucional no ResNet. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python net = nn.Sequential() net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3), nn.BatchNorm(), nn.Activation('relu'), nn.MaxPool2D(pool_size=3, strides=2, padding=1)) .. raw:: html
.. raw:: html
.. code:: python b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) .. raw:: html
.. raw:: html
.. code:: python b1 = tf.keras.models.Sequential([ tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same'), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation('relu'), tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')]) .. raw:: html
.. raw:: html
GoogLeNet usa quatro módulos compostos de blocos de iniciação. No entanto, o ResNet usa quatro módulos compostos de blocos residuais, cada um dos quais usa vários blocos residuais com o mesmo número de canais de saída. O número de canais no primeiro módulo é igual ao número de canais de entrada. Como uma camada de pooling máxima com uma passada de 2 já foi usada, não é necessário reduzir a altura e a largura. No primeiro bloco residual para cada um dos módulos subsequentes, o número de canais é duplicado em comparação com o do módulo anterior e a altura e a largura são reduzidas à metade. Agora, implementamos este módulo. Observe que o processamento especial foi executado no primeiro módulo. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python def resnet_block(num_channels, num_residuals, first_block=False): blk = nn.Sequential() for i in range(num_residuals): if i == 0 and not first_block: blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) else: blk.add(Residual(num_channels)) return blk .. raw:: html
.. raw:: html
.. code:: python def resnet_block(input_channels, num_channels, num_residuals, first_block=False): blk = [] for i in range(num_residuals): if i == 0 and not first_block: blk.append(Residual(input_channels, num_channels, use_1x1conv=True, strides=2)) else: blk.append(Residual(num_channels, num_channels)) return blk .. raw:: html
.. raw:: html
.. code:: python class ResnetBlock(tf.keras.layers.Layer): def __init__(self, num_channels, num_residuals, first_block=False, **kwargs): super(ResnetBlock, self).__init__(**kwargs) self.residual_layers = [] for i in range(num_residuals): if i == 0 and not first_block: self.residual_layers.append( Residual(num_channels, use_1x1conv=True, strides=2)) else: self.residual_layers.append(Residual(num_channels)) def call(self, X): for layer in self.residual_layers.layers: X = layer(X) return X .. raw:: html
.. raw:: html
Em seguida, adicionamos todos os módulos ao ResNet. Aqui, dois blocos residuais são usados para cada módulo. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python net.add(resnet_block(64, 2, first_block=True), resnet_block(128, 2), resnet_block(256, 2), resnet_block(512, 2)) .. raw:: html
.. raw:: html
.. code:: python b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True)) b3 = nn.Sequential(*resnet_block(64, 128, 2)) b4 = nn.Sequential(*resnet_block(128, 256, 2)) b5 = nn.Sequential(*resnet_block(256, 512, 2)) .. raw:: html
.. raw:: html
.. code:: python b2 = ResnetBlock(64, 2, first_block=True) b3 = ResnetBlock(128, 2) b4 = ResnetBlock(256, 2) b5 = ResnetBlock(512, 2) .. raw:: html
.. raw:: html
Finalmente, assim como GoogLeNet, adicionamos uma camada de pooling global média, seguida pela saída da camada totalmente conectada. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python net.add(nn.GlobalAvgPool2D(), nn.Dense(10)) .. raw:: html
.. raw:: html
.. code:: python net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(), nn.Linear(512, 10)) .. raw:: html
.. raw:: html
.. code:: python # Recall that we define this as a function so we can reuse later and run it # within `tf.distribute.MirroredStrategy`'s scope to utilize various # computational resources, e.g. GPUs. Also note that even though we have # created b1, b2, b3, b4, b5 but we will recreate them inside this function's # scope instead def net(): return tf.keras.Sequential([ # The following layers are the same as b1 that we created earlier tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same'), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation('relu'), tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same'), # The following layers are the same as b2, b3, b4, and b5 that we # created earlier ResnetBlock(64, 2, first_block=True), ResnetBlock(128, 2), ResnetBlock(256, 2), ResnetBlock(512, 2), tf.keras.layers.GlobalAvgPool2D(), tf.keras.layers.Dense(units=10)]) .. raw:: html
.. raw:: html
Existem 4 camadas convolucionais em cada módulo (excluindo a camada convolucional :math:`1\times 1`). Junto com a primeira camada convolucional :math:`7\times 7` e a camada final totalmente conectada, há 18 camadas no total. Portanto, esse modelo é comumente conhecido como ResNet-18. Configurando diferentes números de canais e blocos residuais no módulo, podemos criar diferentes modelos de ResNet, como o ResNet-152 de 152 camadas mais profundo. Embora a arquitetura principal do ResNet seja semelhante à do GoogLeNet, a estrutura do ResNet é mais simples e fácil de modificar. Todos esses fatores resultaram no uso rápido e generalizado da ResNet. :numref:`fig_resnet18` representa o ResNet-18 completo. .. _fig_resnet18: .. figure:: ../img/resnet18.svg A arquiteturaResNet-18. Antes de treinar o ResNet, vamos observar como a forma da entrada muda nos diferentes módulos do ResNet. Como em todas as arquiteturas anteriores, a resolução diminui enquanto o número de canais aumenta até o ponto em que uma camada de pooling média global agrega todos os recursos. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python X = np.random.uniform(size=(1, 1, 224, 224)) net.initialize() for layer in net: X = layer(X) print(layer.name, 'output shape:\t', X.shape) .. parsed-literal:: :class: output conv5 output shape: (1, 64, 112, 112) batchnorm4 output shape: (1, 64, 112, 112) relu0 output shape: (1, 64, 112, 112) pool0 output shape: (1, 64, 56, 56) sequential1 output shape: (1, 64, 56, 56) sequential2 output shape: (1, 128, 28, 28) sequential3 output shape: (1, 256, 14, 14) sequential4 output shape: (1, 512, 7, 7) pool1 output shape: (1, 512, 1, 1) dense0 output shape: (1, 10) .. raw:: html
.. raw:: html
.. code:: python X = torch.rand(size=(1, 1, 224, 224)) for layer in net: X = layer(X) print(layer.__class__.__name__,'output shape:\t', X.shape) .. parsed-literal:: :class: output Sequential output shape: torch.Size([1, 64, 56, 56]) Sequential output shape: torch.Size([1, 64, 56, 56]) Sequential output shape: torch.Size([1, 128, 28, 28]) Sequential output shape: torch.Size([1, 256, 14, 14]) Sequential output shape: torch.Size([1, 512, 7, 7]) AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1]) Flatten output shape: torch.Size([1, 512]) Linear output shape: torch.Size([1, 10]) .. raw:: html
.. raw:: html
.. code:: python X = tf.random.uniform(shape=(1, 224, 224, 1)) for layer in net().layers: X = layer(X) print(layer.__class__.__name__,'output shape:\t', X.shape) .. parsed-literal:: :class: output Conv2D output shape: (1, 112, 112, 64) BatchNormalization output shape: (1, 112, 112, 64) Activation output shape: (1, 112, 112, 64) MaxPooling2D output shape: (1, 56, 56, 64) ResnetBlock output shape: (1, 56, 56, 64) ResnetBlock output shape: (1, 28, 28, 128) ResnetBlock output shape: (1, 14, 14, 256) ResnetBlock output shape: (1, 7, 7, 512) GlobalAveragePooling2D output shape: (1, 512) Dense output shape: (1, 10) .. raw:: html
.. raw:: html
Treinamento ----------- Treinamos ResNet no conjunto de dados Fashion-MNIST, assim como antes. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python lr, num_epochs, batch_size = 0.05, 10, 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) .. parsed-literal:: :class: output loss 0.013, train acc 0.997, test acc 0.922 4725.7 examples/sec on gpu(0) .. figure:: output_resnet_46beba_99_1.svg .. raw:: html
.. raw:: html
.. code:: python lr, num_epochs, batch_size = 0.05, 10, 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) .. parsed-literal:: :class: output loss 0.014, train acc 0.997, test acc 0.861 4691.0 examples/sec on cuda:0 .. figure:: output_resnet_46beba_102_1.svg .. raw:: html
.. raw:: html
.. code:: python lr, num_epochs, batch_size = 0.05, 10, 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) .. parsed-literal:: :class: output loss 0.011, train acc 0.997, test acc 0.921 5295.9 examples/sec on /GPU:0 .. parsed-literal:: :class: output .. figure:: output_resnet_46beba_105_2.svg .. raw:: html
.. raw:: html
Sumário ------- - As classes de funções aninhadas são desejáveis. Aprender uma camada adicional em redes neurais profundas como uma função de identidade (embora este seja um caso extremo) deve ser facilitado. - O mapeamento residual pode aprender a função de identidade mais facilmente, como empurrar parâmetros na camada de peso para zero. - Podemos treinar uma rede neural profunda eficaz tendo blocos residuais. As entradas podem se propagar para frente mais rápido através das conexões residuais entre as camadas. - O ResNet teve uma grande influência no projeto de redes neurais profundas subsequentes, tanto de natureza convolucional quanto sequencial. Exercícios ---------- 1. Quais são as principais diferenças entre o bloco de iniciação em :numref:`fig_inception` e o bloco residual? Depois de remover alguns caminhos no bloco de *Inception*, como eles se relacionam? 2. Consulte a Tabela 1 no artigo ResNet :cite:`He.Zhang.Ren.ea.2016` para implementar variantes diferentes. 3. Para redes mais profundas, a ResNet apresenta uma arquitetura de “gargalo” para reduzir complexidade do modelo. Tente implementá-lo. 4. Nas versões subsequentes do ResNet, os autores alteraram a configuração “convolução, lote normalização e ativação”estrutura para a" normalização em lote, estrutura de ativação e convolução ". Faça esta melhoria você mesmo. Veja a Figura 1 em :cite:`He.Zhang.Ren.ea.2016 * 1` para detalhes. 5. Por que não podemos simplesmente aumentar a complexidade das funções sem limites, mesmo se as classes de função estiverem aninhadas? .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
.. raw:: html