.. _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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html