.. _sec_lenet: Redes Neurais Convolucionais (LeNet) ==================================== Agora temos todos os ingredientes necessários para montar uma CNN totalmente funcional. Em nosso encontro anterior com dados de imagem, nós aplicamos um modelo de regressão *softmax* (:numref: ``sec_softmax_scratch``) e um modelo MLP (:numref:`sec_mlp_scratch`) a fotos de roupas no conjunto de dados Fashion-MNIST. Para tornar esses dados passíveis de regressão *softmax* e MLPs, primeiro nivelamos cada imagem de uma matriz :math:`28\times28` em um vetor de comprimento fixo :math:`784`-dimensional, e depois os processamos com camadas totalmente conectadas. Agora que temos um controle sobre as camadas convolucionais, podemos reter a estrutura espacial em nossas imagens. Como um benefício adicional de substituir camadas totalmente conectadas por camadas convolucionais, desfrutaremos de modelos mais parcimoniosos que requerem muito menos parâmetros. Nesta seção, apresentaremos *LeNet*, entre as primeiras CNNs publicadas para chamar a atenção para seu desempenho em tarefas de visão computacional. O modelo foi apresentado por (e nomeado em homenagem a) Yann LeCun, em seguida, um pesquisador da AT&T Bell Labs, para fins de reconhecimento de dígitos manuscritos em imagens :cite:`LeCun.Bottou.Bengio.ea.1998`. Este trabalho representou o culminar de uma década de pesquisa desenvolvendo a tecnologia. Em 1989, LeCun publicou o primeiro estudo com sucesso treinar CNNs via retropropagação. Na época, LeNet alcançou resultados excelentes combinando o desempenho das máquinas de vetores de suporte, em seguida, uma abordagem dominante na aprendizagem supervisionada. LeNet foi eventualmente adaptado para reconhecer dígitos para processar depósitos em caixas eletrônicos. Até hoje, alguns caixas eletrônicos ainda executam o código que Yann e seu colega Leon Bottou escreveram na década de 1990! LeNet ----- Em um alto nível, LeNet (LeNet-5) consiste em duas partes: (i) um codificador convolucional que consiste em duas camadas convolucionais; e (ii) um bloco denso que consiste em três camadas totalmente conectadas; A arquitetura é resumida em :numref:`img_lenet`. .. _img_lenet: .. figure:: ../img/lenet.svg Fluxo de dados em LeNet. A entrada é um dígito escrito à mão, a saída uma probabilidade de mais de 10 resultados possíveis. As unidades básicas em cada bloco convolucional são uma camada convolucional, uma função de ativação *sigmoid* e uma subsequente operação média de *pooling*. Observe que, embora ReLUs e *max-pooling* funcionem melhor, essas descobertas ainda não haviam sido feitas na década de 1990. Cada camada convolucional usa um *kernel* :math:`5\times 5` e uma função de ativação *sigmoid*. Essas camadas mapeiam entradas organizadas espacialmente a uma série de mapas de recursos bidimensionais, normalmente aumentando o número de canais. A primeira camada convolucional tem 6 canais de saída, enquanto o segundo tem 16. Cada operação de *pooling* :math:`2\times2` (passo 2) reduz a dimensionalidade por um fator de :math:`4` por meio da redução da resolução espacial. O bloco convolucional emite uma saída com forma dada por (tamanho do lote, número de canal, altura, largura). Para passar a saída do bloco convolucional para o bloco denso, devemos nivelar cada exemplo no *minibatch*. Em outras palavras, pegamos essa entrada quadridimensional e a transformamos na entrada bidimensional esperada por camadas totalmente conectadas: como um lembrete, a representação bidimensional que desejamos usa a primeira dimensão para indexar exemplos no *minibatch* e o segundo para dar a representação vetorial plana de cada exemplo. O bloco denso do LeNet tem três camadas totalmente conectadas, com 120, 84 e 10 saídas, respectivamente. Porque ainda estamos realizando a classificação, a camada de saída de 10 dimensões corresponde ao número de classes de saída possíveis. Chegar ao ponto em que você realmente entende o que está acontecendo dentro do LeNet pode dar um pouco de trabalho, mas espero que o seguinte *snippet* de código o convença que a implementação de tais modelos com estruturas modernas de *deep learning* é extremamente simples. Precisamos apenas instanciar um bloco ``Sequential`` e encadear as camadas apropriadas. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python from mxnet import autograd, gluon, init, np, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np() net = nn.Sequential() net.add(nn.Conv2D(channels=6, kernel_size=5, padding=2, activation='sigmoid'), nn.AvgPool2D(pool_size=2, strides=2), nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'), nn.AvgPool2D(pool_size=2, strides=2), # `Dense` will transform an input of the shape (batch size, number of # channels, height, width) into an input of the shape (batch size, # number of channels * height * width) automatically by default nn.Dense(120, activation='sigmoid'), nn.Dense(84, activation='sigmoid'), nn.Dense(10)) .. raw:: html
.. raw:: html
.. code:: python import torch from torch import nn from d2l import torch as d2l class Reshape(torch.nn.Module): def forward(self, x): return x.view(-1, 1, 28, 28) net = torch.nn.Sequential( Reshape(), nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(), nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 10)) .. raw:: html
.. raw:: html
.. code:: python import tensorflow as tf from d2l import tensorflow as d2l def net(): return tf.keras.models.Sequential([ tf.keras.layers.Conv2D(filters=6, kernel_size=5, activation='sigmoid', padding='same'), tf.keras.layers.AvgPool2D(pool_size=2, strides=2), tf.keras.layers.Conv2D(filters=16, kernel_size=5, activation='sigmoid'), tf.keras.layers.AvgPool2D(pool_size=2, strides=2), tf.keras.layers.Flatten(), tf.keras.layers.Dense(120, activation='sigmoid'), tf.keras.layers.Dense(84, activation='sigmoid'), tf.keras.layers.Dense(10)]) .. raw:: html
.. raw:: html
Tomamos uma pequena liberdade com o modelo original, removendo a ativação gaussiana na camada final. Fora isso, esta rede corresponde a arquitetura LeNet-5 original. Ao passar por um canal único (preto e branco) :math:`28 \times 28` imagem através da rede e imprimir a forma de saída em cada camada, podemos inspecionar o modelo para ter certeza que suas operações se alinham com o que esperamos de :numref:`img_lenet_vert`. .. _img_lenet_vert: .. figure:: ../img/lenet-vert.svg Compressed notation for LeNet-5. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python X = np.random.uniform(size=(1, 1, 28, 28)) net.initialize() for layer in net: X = layer(X) print(layer.name, 'output shape:\t', X.shape) .. parsed-literal:: :class: output conv0 output shape: (1, 6, 28, 28) pool0 output shape: (1, 6, 14, 14) conv1 output shape: (1, 16, 10, 10) pool1 output shape: (1, 16, 5, 5) dense0 output shape: (1, 120) dense1 output shape: (1, 84) dense2 output shape: (1, 10) .. raw:: html
.. raw:: html
.. code:: python X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) for layer in net: X = layer(X) print(layer.__class__.__name__,'output shape: \t',X.shape) .. parsed-literal:: :class: output Reshape output shape: torch.Size([1, 1, 28, 28]) Conv2d output shape: torch.Size([1, 6, 28, 28]) Sigmoid output shape: torch.Size([1, 6, 28, 28]) AvgPool2d output shape: torch.Size([1, 6, 14, 14]) Conv2d output shape: torch.Size([1, 16, 10, 10]) Sigmoid output shape: torch.Size([1, 16, 10, 10]) AvgPool2d output shape: torch.Size([1, 16, 5, 5]) Flatten output shape: torch.Size([1, 400]) Linear output shape: torch.Size([1, 120]) Sigmoid output shape: torch.Size([1, 120]) Linear output shape: torch.Size([1, 84]) Sigmoid output shape: torch.Size([1, 84]) Linear output shape: torch.Size([1, 10]) .. raw:: html
.. raw:: html
.. code:: python X = tf.random.uniform((1, 28, 28, 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, 28, 28, 6) AveragePooling2D output shape: (1, 14, 14, 6) Conv2D output shape: (1, 10, 10, 16) AveragePooling2D output shape: (1, 5, 5, 16) Flatten output shape: (1, 400) Dense output shape: (1, 120) Dense output shape: (1, 84) Dense output shape: (1, 10) .. raw:: html
.. raw:: html
Observe que a altura e largura da representação em cada camada ao longo do bloco convolucional é reduzido (em comparação com a camada anterior). A primeira camada convolucional usa 2 pixels de preenchimento para compensar a redução de altura e largura que de outra forma resultaria do uso de um *kernel* :math:`5 \times 5`. Em contraste, a segunda camada convolucional dispensa o preenchimento, e, portanto, a altura e a largura são reduzidas em 4 pixels. Conforme subimos na pilha de camadas, o número de canais aumenta camada sobre camada de 1 na entrada a 6 após a primeira camada convolucional e 16 após a segunda camada convolucional. No entanto, cada camada de *pooling* divide a altura e a largura pela metade. Finalmente, cada camada totalmente conectada reduz a dimensionalidade, finalmente emitindo uma saída cuja dimensão corresponde ao número de classes. Trainamento ----------- Agora que implementamos o modelo, vamos fazer um experimento para ver como o LeNet se sai no Fashion-MNIST. .. code:: python batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size) Embora as CNNs tenham menos parâmetros, eles ainda podem ser mais caros para computar do que MLPs igualmente profundas porque cada parâmetro participa de muitas multiplicações a mais. Se você tiver acesso a uma GPU, este pode ser um bom momento para colocá-la em ação para acelerar o treinamento. Para avaliação, precisamos fazer uma pequena modificação para a função ``evaluate_accuracy`` que descrevemos em :numref:`sec_softmax_scratch`. Uma vez que o conjunto de dados completo está na memória principal, precisamos copiá-lo para a memória da GPU antes que o modelo use a GPU para calcular com o conjunto de dados. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def evaluate_accuracy_gpu(net, data_iter, device=None): #@save """Compute the accuracy for a model on a dataset using a GPU.""" if not device: # Query the first device where the first parameter is on device = list(net.collect_params().values())[0].list_ctx()[0] # No. of correct predictions, no. of predictions metric = d2l.Accumulator(2) for X, y in data_iter: X, y = X.as_in_ctx(device), y.as_in_ctx(device) metric.add(d2l.accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] .. raw:: html
.. raw:: html
.. code:: python def evaluate_accuracy_gpu(net, data_iter, device=None): #@save """Compute the accuracy for a model on a dataset using a GPU.""" if isinstance(net, torch.nn.Module): net.eval() # Set the model to evaluation mode if not device: device = next(iter(net.parameters())).device # No. of correct predictions, no. of predictions metric = d2l.Accumulator(2) for X, y in data_iter: if isinstance(X, list): # Required for BERT Fine-tuning (to be covered later) X = [x.to(device) for x in X] else: X = X.to(device) y = y.to(device) metric.add(d2l.accuracy(net(X), y), y.numel()) return metric[0] / metric[1] .. raw:: html
.. raw:: html
Também precisamos atualizar nossa função de treinamento para lidar com GPUs. Ao contrário do ``train_epoch_ch3`` definido em :numref:`sec_softmax_scratch`, agora precisamos mover cada *minibatch* de dados para o nosso dispositivo designado (esperançosamente, a GPU) antes de fazer as propagações para frente e para trás. A função de treinamento ``train_ch6`` também é semelhante para ``train_ch3`` definido em :numref:`sec_softmax_scratch`. Como iremos implementar redes com muitas camadas daqui para frente, contaremos principalmente com APIs de alto nível. A função de treinamento a seguir pressupõe um modelo criado a partir de APIs de alto nível como entrada e é otimizado em conformidade. Inicializamos os parâmetros do modelo no dispositivo indicado pelo argumento ``device``, usando a inicialização do Xavier conforme apresentado em :numref:`subsec_xavier`. Assim como com MLPs, nossa função de perda é entropia cruzada, e o minimizamos por meio da descida gradiente estocástica de *minibatch*. Como cada época leva dezenas de segundos para ser executada, visualizamos a perda de treinamento com mais frequência. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python #@save def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): """Train a model with a GPU (defined in Chapter 6).""" net.initialize(force_reinit=True, ctx=device, init=init.Xavier()) loss = gluon.loss.SoftmaxCrossEntropyLoss() trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=['train loss', 'train acc', 'test acc']) timer, num_batches = d2l.Timer(), len(train_iter) for epoch in range(num_epochs): # Sum of training loss, sum of training accuracy, no. of examples metric = d2l.Accumulator(3) for i, (X, y) in enumerate(train_iter): timer.start() # Here is the major difference from `d2l.train_epoch_ch3` X, y = X.as_in_ctx(device), y.as_in_ctx(device) with autograd.record(): y_hat = net(X) l = loss(y_hat, y) l.backward() trainer.step(X.shape[0]) metric.add(l.sum(), d2l.accuracy(y_hat, y), X.shape[0]) timer.stop() train_l = metric[0] / metric[2] train_acc = metric[1] / metric[2] if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (train_l, train_acc, None)) test_acc = evaluate_accuracy_gpu(net, test_iter) animator.add(epoch + 1, (None, None, test_acc)) print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, ' f'test acc {test_acc:.3f}') print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' f'on {str(device)}') .. raw:: html
.. raw:: html
.. code:: python #@save def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): """Train a model with a GPU (defined in Chapter 6).""" def init_weights(m): if type(m) == nn.Linear or type(m) == nn.Conv2d: nn.init.xavier_uniform_(m.weight) net.apply(init_weights) print('training on', device) net.to(device) optimizer = torch.optim.SGD(net.parameters(), lr=lr) loss = nn.CrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=['train loss', 'train acc', 'test acc']) timer, num_batches = d2l.Timer(), len(train_iter) for epoch in range(num_epochs): # Sum of training loss, sum of training accuracy, no. of examples metric = d2l.Accumulator(3) net.train() for i, (X, y) in enumerate(train_iter): timer.start() optimizer.zero_grad() X, y = X.to(device), y.to(device) y_hat = net(X) l = loss(y_hat, y) l.backward() optimizer.step() with torch.no_grad(): metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0]) timer.stop() train_l = metric[0] / metric[2] train_acc = metric[1] / metric[2] if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (train_l, train_acc, None)) test_acc = evaluate_accuracy_gpu(net, test_iter) animator.add(epoch + 1, (None, None, test_acc)) print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, ' f'test acc {test_acc:.3f}') print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' f'on {str(device)}') .. raw:: html
.. raw:: html
.. code:: python class TrainCallback(tf.keras.callbacks.Callback): #@save """A callback to visiualize the training progress.""" def __init__(self, net, train_iter, test_iter, num_epochs, device_name): self.timer = d2l.Timer() self.animator = d2l.Animator( xlabel='epoch', xlim=[1, num_epochs], legend=[ 'train loss', 'train acc', 'test acc']) self.net = net self.train_iter = train_iter self.test_iter = test_iter self.num_epochs = num_epochs self.device_name = device_name def on_epoch_begin(self, epoch, logs=None): self.timer.start() def on_epoch_end(self, epoch, logs): self.timer.stop() test_acc = self.net.evaluate( self.test_iter, verbose=0, return_dict=True)['accuracy'] metrics = (logs['loss'], logs['accuracy'], test_acc) self.animator.add(epoch + 1, metrics) if epoch == self.num_epochs - 1: batch_size = next(iter(self.train_iter))[0].shape[0] num_examples = batch_size * tf.data.experimental.cardinality( self.train_iter).numpy() print(f'loss {metrics[0]:.3f}, train acc {metrics[1]:.3f}, ' f'test acc {metrics[2]:.3f}') print(f'{num_examples / self.timer.avg():.1f} examples/sec on ' f'{str(self.device_name)}') #@save def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): """Train a model with a GPU (defined in Chapter 6).""" device_name = device._device_name strategy = tf.distribute.OneDeviceStrategy(device_name) with strategy.scope(): optimizer = tf.keras.optimizers.SGD(learning_rate=lr) loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) net = net_fn() net.compile(optimizer=optimizer, loss=loss, metrics=['accuracy']) callback = TrainCallback(net, train_iter, test_iter, num_epochs, device_name) net.fit(train_iter, epochs=num_epochs, verbose=0, callbacks=[callback]) return net .. raw:: html
.. raw:: html
Agora vamos treinar e avaliar o modelo LeNet-5. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python lr, num_epochs = 0.9, 10 train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) .. parsed-literal:: :class: output loss 0.471, train acc 0.822, test acc 0.827 38651.1 examples/sec on gpu(0) .. figure:: output_lenet_4a2e9e_50_1.svg .. raw:: html
.. raw:: html
.. code:: python lr, num_epochs = 0.9, 10 train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) .. parsed-literal:: :class: output loss 0.474, train acc 0.820, test acc 0.814 85084.5 examples/sec on cuda:0 .. figure:: output_lenet_4a2e9e_53_1.svg .. raw:: html
.. raw:: html
.. code:: python lr, num_epochs = 0.9, 10 train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) .. parsed-literal:: :class: output loss 0.468, train acc 0.823, test acc 0.827 60295.1 examples/sec on /GPU:0 .. parsed-literal:: :class: output .. figure:: output_lenet_4a2e9e_56_2.svg .. raw:: html
.. raw:: html
Resumo ------ - A CNN é uma rede que emprega camadas convolucionais. - Em uma CNN, intercalamos convoluções, não linearidades e (frequentemente) operações de agrupamento. - Em uma CNN, as camadas convolucionais são normalmente organizadas de forma que diminuam gradualmente a resolução espacial das representações, enquanto aumentam o número de canais. - Em CNNs tradicionais, as representações codificadas pelos blocos convolucionais são processadas por uma ou mais camadas totalmente conectadas antes de emitir a saída. - LeNet foi indiscutivelmente a primeira implantação bem-sucedida de tal rede. Exercícios ---------- 1. Substitua o *pooling* médio pelo *pooling* máximo. O que acontece? 2. Tente construir uma rede mais complexa baseada em LeNet para melhorar sua precisão. 1. Ajuste o tamanho da janela de convolução. 2. Ajuste o número de canais de saída. 3. Ajuste a função de ativação (por exemplo, ReLU). 4. Ajuste o número de camadas de convolução. 5. Ajuste o número de camadas totalmente conectadas. 6. Ajuste as taxas de aprendizagem e outros detalhes de treinamento (por exemplo, inicialização e número de épocas). 3. Experimente a rede aprimorada no conjunto de dados MNIST original. 4. Exibir as ativações da primeira e da segunda camada do LeNet para diferentes entradas (por exemplo, suéteres e casacos). .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
.. raw:: html