5.1. Camadas e Blocos¶ Open the notebook in SageMaker Studio Lab
Quando introduzimos as redes neurais pela primeira vez, focamos em modelos lineares com uma única saída. Aqui, todo o modelo consiste em apenas um único neurônio. Observe que um único neurônio (i) leva algum conjunto de entradas; (ii) gera uma saída escalar correspondente; e (iii) tem um conjunto de parâmetros associados que podem ser atualizados para otimizar alguma função objetivo de interesse. Então, quando começamos a pensar em redes com múltiplas saídas, nós alavancamos a aritmética vetorizada para caracterizar uma camada inteira de neurônios. Assim como os neurônios individuais, camadas (i) recebem um conjunto de entradas, (ii) gerar resultados correspondentes, e (iii) são descritos por um conjunto de parâmetros ajustáveis. Quando trabalhamos com a regressão softmax, uma única camada era ela própria o modelo. No entanto, mesmo quando subsequentemente introduziu MLPs, ainda podemos pensar no modelo como mantendo esta mesma estrutura básica.
Curiosamente, para MLPs, todo o modelo e suas camadas constituintes compartilham essa estrutura. Todo o modelo recebe entradas brutas (os recursos), gera resultados (as previsões), e possui parâmetros (os parâmetros combinados de todas as camadas constituintes). Da mesma forma, cada camada individual ingere entradas (fornecido pela camada anterior) gera saídas (as entradas para a camada subsequente), e possui um conjunto de parâmetros ajustáveis que são atualizados de acordo com o sinal que flui para trás da camada subsequente.
Embora você possa pensar que neurônios, camadas e modelos dê-nos abstrações suficientes para cuidar de nossos negócios, Acontece que muitas vezes achamos conveniente para falar sobre componentes que são maior do que uma camada individual mas menor do que o modelo inteiro. Por exemplo, a arquitetura ResNet-152, que é muito popular na visão computacional, possui centenas de camadas. Essas camadas consistem em padrões repetidos de grupos de camadas. Implementar uma camada de rede por vez pode se tornar tedioso. Essa preocupação não é apenas hipotética — tal padrões de projeto são comuns na prática. A arquitetura ResNet mencionada acima venceu as competições de visão computacional ImageNet e COCO 2015 para reconhecimento e detecção [He et al., 2016] e continua sendo uma arquitetura indispensável para muitas tarefas de visão. Arquiteturas semelhantes nas quais as camadas são organizadas em vários padrões repetidos agora são onipresentes em outros domínios, incluindo processamento de linguagem natural e fala.
Para implementar essas redes complexas, introduzimos o conceito de uma rede neural block. Um bloco pode descrever uma única camada, um componente que consiste em várias camadas, ou o próprio modelo inteiro! Uma vantagem de trabalhar com a abstração de bloco é que eles podem ser combinados em artefatos maiores, frequentemente recursivamente. Isso é ilustrado em Fig. 5.1.1. Definindo o código para gerar blocos de complexidade arbitrária sob demanda, podemos escrever código surpreendentemente compacto e ainda implementar redes neurais complexas.
Fig. 5.1.1 Múltiplas camadas são combinadas em blocos, formando padrões repetitivos de um modelo maior.¶
Do ponto de vista da programação, um bloco é representado por uma classe. Qualquer subclasse dele deve definir uma função de propagação direta que transforma sua entrada em saída e deve armazenar todos os parâmetros necessários. Observe que alguns blocos não requerem nenhum parâmetro. Finalmente, um bloco deve possuir uma função de retropropagação, para fins de cálculo de gradientes. Felizmente, devido a alguma magia dos bastidores fornecido pela diferenciação automática (introduzido em Section 2.5) ao definir nosso próprio bloco, só precisamos nos preocupar com os parâmetros e a função de propagação direta.
Para começar, revisitamos o código que usamos para implementar MLPs (Section 4.3). O código a seguir gera uma rede com uma camada oculta totalmente conectada com 256 unidades e ativação ReLU, seguido por uma camada de saída totalmente conectada com 10 unidades (sem função de ativação).
from mxnet import np, npx
from mxnet.gluon import nn
npx.set_np()
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
X = np.random.uniform(size=(2, 20))
net(X)
array([[ 0.06240274, -0.03268593, 0.02582653, 0.02254181, -0.03728798,
-0.04253785, 0.00540612, -0.01364185, -0.09915454, -0.02272737],
[ 0.02816679, -0.03341204, 0.03565665, 0.02506384, -0.04136416,
-0.04941844, 0.01738529, 0.01081963, -0.09932579, -0.01176296]])
Neste exemplo, nós construímos nosso modelo instanciando um
nn.Sequential
, atribuindo o objeto retornado à variável net
. Em
seguida, chamamos repetidamente sua função add
, anexando camadas no
pedido que eles devem ser executados. Em suma, nn.Sequential
define
um tipo especial deBlock
, a classe que apresenta um bloco em
Gluon. Ele mantém uma lista ordenada de Block
constituintes. A
função add
simplesmente facilita a adição de cada Bloco
sucessivo à lista. Observe que cada camada é uma instância da classe
Dense
que é uma subclasse de Block
. A função de propagação
direta (forward
) também é notavelmente simples: ele encadeia cada
Block
na lista, passando a saída de cada um como entrada para o
próximo. Observe que, até agora, temos invocado nossos modelos através
da construção net (X)
para obter seus resultados. Na verdade, isso é
apenas um atalho para net.forward (X)
, um truque Python habilidoso
alcançado via a função __call__
da classeBlock
.
import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
X = torch.rand(2, 20)
net(X)
tensor([[-0.0256, 0.1279, 0.0679, 0.0072, -0.2088, 0.0412, -0.2575, -0.0133,
0.0521, 0.0697],
[ 0.0747, 0.1671, 0.1247, -0.1827, -0.1590, 0.0773, -0.2884, -0.1681,
0.0335, 0.2242]], grad_fn=<AddmmBackward>)
Neste exemplo, nós construímos nosso modelo instanciando um
nn.Sequential
, com camadas na ordem que eles devem ser executados
passados como argumentos. Em suma, nn.Sequential
define um tipo
especial de Module
, a classe que apresenta um bloco em PyTorch. Ele
mantém uma lista ordenada de Module
constituintes. Observe que cada
uma das duas camadas totalmente conectadas é uma instância da classe
Linear
que é uma subclasse de Module
. A função de propagação
direta (forward
) também é notavelmente simples: ele encadeia cada
bloco da lista, passando a saída de cada um como entrada para o próximo.
Observe que, até agora, temos invocado nossos modelos através da
construção net (X)
para obter seus resultados. Na verdade, isso é
apenas um atalho para net.__call__(X)
.
import tensorflow as tf
net = tf.keras.models.Sequential([
tf.keras.layers.Dense(256, activation=tf.nn.relu),
tf.keras.layers.Dense(10),
])
X = tf.random.uniform((2, 20))
net(X)
<tf.Tensor: shape=(2, 10), dtype=float32, numpy=
array([[ 0.02005391, 0.01748483, 0.25770283, 0.04745208, 0.01557797,
0.11936638, 0.0410642 , 0.05792776, -0.17585787, 0.01903499],
[-0.19403772, -0.0393811 , 0.30145183, 0.13547418, 0.07529071,
0.08008011, 0.12677108, 0.11462483, -0.15713724, -0.15506239]],
dtype=float32)>
Neste exemplo, nós construímos nosso modelo instanciando um
keras.models.Sequential
, com camadas na ordem que eles devem ser
executados passados como argumentos. Em suma, Sequential
define um
tipo especial dekeras.Model
, a classe que apresenta um bloco em
Keras. Ele mantém uma lista ordenada de Model
constituintes. Observe
que cada uma das duas camadas totalmente conectadas é uma instância da
classe Dense
que é uma subclasse de Model
. A função de
propagação direta (call
) também é extremamente simples: ele encadeia
cada bloco da lista, passando a saída de cada um como entrada para o
próximo. Observe que, até agora, temos invocado nossos modelos através
da construção net (X)
para obter seus resultados. Na verdade, isso é
apenas um atalho para net.call(X)
, um truque Python habilidoso
alcançado via a função __call__
da classe Block.
5.1.1. Um Bloco Personalizado¶
Talvez a maneira mais fácil de desenvolver intuição sobre como funciona um bloco é implementar um nós mesmos. Antes de implementar nosso próprio bloco personalizado, resumimos brevemente a funcionalidade básica que cada bloco deve fornecer:
Ingerir dados de entrada como argumentos para sua função de propagação direta.
Gere uma saída fazendo com que a função de propagação direta retorne um valor. Observe que a saída pode ter uma forma diferente da entrada. Por exemplo, a primeira camada totalmente conectada em nosso modelo acima ingere uma entrada de dimensão arbitrária, mas retorna uma saída de dimensão 256.
Calcule o gradiente de sua saída em relação à sua entrada, que pode ser acessado por meio de sua função de retropropagação. Normalmente, isso acontece automaticamente.
Armazene e forneça acesso aos parâmetros necessários para executar o cálculo de propagação direta.
Inicialize os parâmetros do modelo conforme necessário.
No seguinte trecho de código, nós codificamos um bloco do zero
correspondendo a um MLP com uma camada oculta com 256 unidades ocultas,
e uma camada de saída de 10 dimensões. Observe que a classe MLP
abaixo herda a classe que representa um bloco. Vamos contar muito com as
funções da classe pai, fornecendo apenas nosso próprio construtor (a
função __init__
em Python) e a função de propagação direta.
class MLP(nn.Block):
# Declare uma camada com parâmetros de modelo.
# Aqui, nós declaramos duas camadas completamente conectadas
def __init__(self, **kwargs):
# Chame o construtor da classe pai `MLP` `Block` para realizar
# as inicializações necessárias. Desta forma, outros argumentos das funções
# também podem ser especificados durante a instalação da classe,
# da mesma forma que os parâmetros do modelo, 'params' (a ser descrito posteriormente)
super().__init__(**kwargs)
self.hidden = nn.Dense(256, activation='relu') # Hidden layer
self.out = nn.Dense(10) # Output layer
# Defina a propagação direta do modelo, ou seja, como retornar
# a saída do modelo requirido baseado na entrada 'X'
def forward(self, X):
return self.out(self.hidden(X))
class MLP(nn.Module):
# Declare uma camada com parâmetros de modelo. Aqui, declaramos duas
# camadas totalmente conectadas
def __init__(self):
# Chame o construtor da classe pai `MLP` `Block` para realizar
# a inicialização necessária. Desta forma, outros argumentos de função
# também podem ser especificado durante a instanciação da classe, como
# os parametros do modelo, `params` (a ser descritos posteriormente)
super().__init__()
self.hidden = nn.Linear(20, 256) # Hidden layer
self.out = nn.Linear(256, 10) # Output layer
# Defina a propagação direta do modelo, ou seja, como retornar a
# saída do modelo necessária com base na entrada `X`
def forward(self, X):
# Observe aqui que usamos a versão funcional do ReLU definida no
# módulo nn.functional.
return self.out(F.relu(self.hidden(X)))
class MLP(tf.keras.Model):
# Declare uma camada com parâmetros de modelo. Aqui, declaramos duas
# camadas totalmente conectadas
def __init__(self):
# Chame o construtor da classe pai `MLP` `Block` para realizar
# a inicialização necessária. Desta forma, outros argumentos de função
# também podem ser especificados durante a instanciação da classe, como os
# parâmetros do modelo, `params` (a serem descritos mais tarde)
super().__init__()
# Camadas escondidas
self.hidden = tf.keras.layers.Dense(units=256, activation=tf.nn.relu)
self.out = tf.keras.layers.Dense(units=10) # Output layer
# Defina a propagação direta do modelo, ou seja, como retornar a
# saída do modelo necessária com base na entrada `X`
def call(self, X):
return self.out(self.hidden((X)))
Vamos primeiro nos concentrar na função de propagação direta. Observe
que leva X
como entrada, calcula a representação oculta com a função
de ativação aplicada, e produz seus logits. Nesta implementação
MLP
, ambas as camadas são variáveis de instância. Para ver por que
isso é razoável, imagine instanciando dois MLPs, net1
enet2
, e
treiná-los em dados diferentes. Naturalmente, esperaríamos que eles para
representar dois modelos aprendidos diferentes.
Nós instanciamos as camadas do MLP no construtor e posteriormente
invocar essas camadas em cada chamada para a função de propagação
direta. Observe alguns detalhes importantes: Primeiro, nossa função
__init__
personalizada invoca a função __init__
da classe pai
via super().__ init __()
poupando-nos da dor de reafirmar o código
padrão aplicável à maioria dos blocos. Em seguida, instanciamos nossas
duas camadas totalmente conectadas, atribuindo-os a self.hidden
eself.out
. Observe que, a menos que implementemos um novo
operador, não precisamos nos preocupar com a função de backpropagation
ou inicialização de parâmetro. O sistema irá gerar essas funções
automaticamente. Vamos tentar fazer isso.
net = MLP()
net.initialize()
net(X)
array([[-0.03989595, -0.10414709, 0.06799038, 0.05245074, 0.0252606 ,
-0.00640342, 0.04182098, -0.01665318, -0.02067345, -0.07863816],
[-0.03612847, -0.07210435, 0.09159479, 0.07890773, 0.02494171,
-0.01028665, 0.01732427, -0.02843244, 0.03772651, -0.06671703]])
net = MLP()
net(X)
tensor([[-0.0627, -0.0958, -0.0792, 0.2375, 0.3284, 0.1038, -0.0707, -0.0726,
-0.0738, 0.0958],
[ 0.0125, -0.1072, 0.0780, 0.1053, 0.2692, 0.1731, -0.0750, -0.1358,
-0.0111, -0.0523]], grad_fn=<AddmmBackward>)
net = MLP()
net(X)
<tf.Tensor: shape=(2, 10), dtype=float32, numpy=
array([[-0.26673675, 0.03081866, -0.02572505, 0.14906818, -0.34175998,
-0.13008447, -0.07260932, -0.09456596, 0.21496284, 0.04010665],
[-0.3382145 , -0.21301885, 0.13231796, 0.27938277, -0.55216247,
-0.35227174, 0.15120609, -0.05135735, 0.2197276 , -0.14093223]],
dtype=float32)>
Uma virtude fundamental da abstração em bloco é sua versatilidade.
Podemos criar uma subclasse de um bloco para criar camadas (como a
classe de camada totalmente conectada), modelos inteiros (como a classe
MLP
acima), ou vários componentes de complexidade intermediária. Nós
exploramos essa versatilidade ao longo dos capítulos seguintes, como ao
abordar redes neurais convolucionais.
5.1.2. O Bloco Sequencial¶
Agora podemos dar uma olhada mais de perto em como a classe
Sequential
funciona. Lembre-se de que Sequential
foi projetado
para conectar outros blocos em série. Para construir nosso próprio
MySequential
simplificado, só precisamos definir duas funções
principais: 1. Uma função para anexar um blocos a uma lista. 2. Uma
função de propagação direta para passar uma entrada através da cadeia de
blocos, na mesma ordem em que foram acrescentados.
A seguinte classe MySequential
oferece o mesmo funcionalidade da
classe Sequential
padrão.
class MySequential(nn.Block):
def add(self, block):
# Here, `block` is an instance of a `Block` subclass, and we assume
#
# that it has a unique name. We save it in the member variable
#
# `_children` of the `Block` class, and its type is OrderedDict. When
#
# the `MySequential` instance calls the `initialize` function, the
#
# system automatically initializes all members of `_children`
#
self._children[block.name] = block
def forward(self, X):
# OrderedDict guarantees that members will be traversed in the order
#
# they were added
#
for block in self._children.values():
X = block(X)
return X
A função add
adiciona um único bloco para o dicionário ordenado
_children
. Você deve estar se perguntando por que todo bloco de
Gluon possui um atributo _children
e por que o usamos em vez de
apenas definir uma lista Python nós mesmos. Resumindo, a principal
vantagem das _children
é que durante a inicialização do parâmetro do
nosso bloco, Gluon sabe olhar dentro do dicionário _children
para
encontrar sub-blocos cujo os parâmetros também precisam ser
inicializados.
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# Here, `module` is an instance of a `Module` subclass. We save it
#
# in the member variable `_modules` of the `Module` class, and its
#
# type is OrderedDict
#
self._modules[str(idx)] = module
def forward(self, X):
# OrderedDict guarantees that members will be traversed in the order
#
# they were added
#
for block in self._modules.values():
X = block(X)
return X
No método __init__
, adicionamos todos os módulos para o dicionário
ordenado _modules
um por um. Você pode se perguntar por que todo
Module
possui um atributo _modules
e por que o usamos em vez de
apenas definir uma lista Python nós mesmos. Em suma, a principal
vantagem de _modules
é que durante a inicialização do parâmetro do
nosso módulo, o sistema sabe olhar dentro do _modules
dicionário
para encontrar submódulos cujo os parâmetros também precisam ser
inicializados.
class MySequential(tf.keras.Model):
def __init__(self, *args):
super().__init__()
self.modules = []
for block in args:
# Here, `block` is an instance of a `tf.keras.layers.Layer`
#
# subclass
#
self.modules.append(block)
def call(self, X):
for module in self.modules:
X = module(X)
return X
Quando a função de propagação direta de nosso MySequential
é
invocada, cada bloco adicionado é executado na ordem em que foram
adicionados. Agora podemos reimplementar um MLP usando nossa classe
MySequential
.
net = MySequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
net(X)
array([[-0.0764568 , -0.01130233, 0.04952145, -0.04651389, -0.04131571,
-0.05884131, -0.06213811, 0.01311471, -0.01379425, -0.02514282],
[-0.05124623, 0.00711232, -0.00155933, -0.07555379, -0.06675334,
-0.01762914, 0.00589085, 0.0144719 , -0.04330775, 0.03317727]])
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
tensor([[-0.1604, 0.0811, -0.1572, 0.0730, 0.1101, -0.1630, 0.0652, 0.0542,
0.3904, 0.3679],
[-0.2220, -0.0184, -0.0323, 0.1314, 0.2649, -0.0249, -0.1741, -0.0846,
0.2679, 0.3209]], grad_fn=<AddmmBackward>)
net = MySequential(
tf.keras.layers.Dense(units=256, activation=tf.nn.relu),
tf.keras.layers.Dense(10))
net(X)
<tf.Tensor: shape=(2, 10), dtype=float32, numpy=
array([[-0.00352468, 0.26430547, 0.02623755, -0.06980857, -0.10228556,
0.06134659, -0.34809282, 0.02128534, 0.25998774, -0.09471862],
[ 0.12531577, 0.04913612, 0.07804356, -0.32592505, -0.20706768,
0.11600506, -0.40183872, 0.08425899, -0.01482138, -0.37459594]],
dtype=float32)>
Observe que este uso de MySequential
é idêntico ao código que
escrevemos anteriormente para a classe Sequential
(conforme descrito
em Section 4.3).
5.1.3. Execução de Código na Função de Propagação Direta¶
A classe Sequential
facilita a construção do modelo, nos permitindo
montar novas arquiteturas sem ter que definir nossa própria classe. No
entanto, nem todas as arquiteturas são cadeias simples. Quando uma maior
flexibilidade é necessária, vamos querer definir nossos próprios blocos.
Por exemplo, podemos querer executar o controle de fluxo do Python
dentro da função de propagação direta. Além disso, podemos querer
realizar operações matemáticas arbitrárias, não simplesmente depender de
camadas de rede neural predefinidas.
Você deve ter notado que até agora, todas as operações em nossas redes
agiram de acordo com as ativações de nossa rede e seus parâmetros. Às
vezes, no entanto, podemos querer incorporar termos que não são
resultado de camadas anteriores nem parâmetros atualizáveis. Chamamos
isso de parâmetros constantes. Digamos, por exemplo, que queremos uma
camada que calcula a função
\(f(\mathbf{x},\mathbf{w}) = c \cdot \mathbf{w}^\top \mathbf{x}\),
onde \(\mathbf{x}\) é a entrada, \(\mathbf{w}\) é nosso
parâmetro, e \(c\) é alguma constante especificada que não é
atualizado durante a otimização. Portanto, implementamos uma classe
FixedHiddenMLP
como a seguir.
class FixedHiddenMLP(nn.Block):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Parâmetros de peso aleatórios criados com a função `get_constant`
# não são atualizados durante o treinamento (ou seja, parâmetros constantes)
self.rand_weight = self.params.get_constant(
'rand_weight', np.random.uniform(size=(20, 20)))
self.dense = nn.Dense(20, activation='relu')
def forward(self, X):
X = self.dense(X)
# Use os parâmetros constantes criados, bem como as funções `relu` e` dot`
X = npx.relu(np.dot(X, self.rand_weight.data()) + 1)
# Reutilize a camada totalmente conectada. Isso é equivalente a compartilhar
# parâmetros com duas camadas totalmente conectadas
X = self.dense(X)
# Control flow
while np.abs(X).sum() > 1:
X /= 2
return X.sum()
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# Parâmetros de peso aleatórios que não computarão gradientes e
# portanto, mantem-se constante durante o treinamento
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)
def forward(self, X):
X = self.linear(X)
# Use os parâmetros constantes criados, bem como as funções `relu` e` mm`
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# Reutilize a camada totalmente conectada. Isso é equivalente a compartilhar
# parâmetros com duas camadas totalmente conectadas
X = self.linear(X)
# Controle de fluxo
while X.abs().sum() > 1:
X /= 2
return X.sum()
class FixedHiddenMLP(tf.keras.Model):
def __init__(self):
super().__init__()
self.flatten = tf.keras.layers.Flatten()
# Parâmetros de peso aleatório criados com `tf.constant` não são atualizados
# durante o treinamento (ou seja, parâmetros constantes)
self.rand_weight = tf.constant(tf.random.uniform((20, 20)))
self.dense = tf.keras.layers.Dense(20, activation=tf.nn.relu)
def call(self, inputs):
X = self.flatten(inputs)
# Use os parâmetros constantes criados, bem como as funções `relu` e `matmul`
X = tf.nn.relu(tf.matmul(X, self.rand_weight) + 1)
# Reutilize a camada totalmente conectada. Isso é equivalente a compartilhar
# parâmetros com duas camadas totalmente conectadas
X = self.dense(X)
# Control flow
#
while tf.reduce_sum(tf.math.abs(X)) > 1:
X /= 2
return tf.reduce_sum(X)
Neste modelo FixedHiddenMLP
, implementamos uma camada oculta cujos
pesos (self.rand_weight
) são inicializados aleatoriamente na
instanciação e daí em diante constantes. Este peso não é um parâmetro do
modelo e, portanto, nunca é atualizado por backpropagation. A rede
então passa a saída desta camada “fixa” através de uma camada totalmente
conectada.
Observe que antes de retornar a saída, nosso modelo fez algo incomum.
Executamos um loop while, testando na condição de que sua norma
\(L_1\) seja maior que \(1\), e dividindo nosso vetor de
produção por \(2\) até que satisfizesse a condição. Finalmente,
retornamos a soma das entradas em X
. Até onde sabemos, nenhuma rede
neural padrão executa esta operação. Observe que esta operação em
particular pode não ser útil em qualquer tarefa do mundo real. Nosso
objetivo é apenas mostrar como integrar código arbitrário no fluxo de
seu cálculos de rede neural.
net = FixedHiddenMLP()
net.initialize()
net(X)
array(0.52637565)
net = FixedHiddenMLP()
net(X)
tensor(0.1221, grad_fn=<SumBackward0>)
net = FixedHiddenMLP()
net(X)
<tf.Tensor: shape=(), dtype=float32, numpy=0.7815337>
Podemos misturar e combinar vários maneiras de montar blocos juntos. No exemplo a seguir, aninhamos blocos de algumas maneiras criativas.
class NestMLP(nn.Block):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.net = nn.Sequential()
self.net.add(nn.Dense(64, activation='relu'),
nn.Dense(32, activation='relu'))
self.dense = nn.Dense(16, activation='relu')
def forward(self, X):
return self.dense(self.net(X))
chimera = nn.Sequential()
chimera.add(NestMLP(), nn.Dense(20), FixedHiddenMLP())
chimera.initialize()
chimera(X)
array(0.9772054)
class NestMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU())
self.linear = nn.Linear(32, 16)
def forward(self, X):
return self.linear(self.net(X))
chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)
tensor(-0.0955, grad_fn=<SumBackward0>)
class NestMLP(tf.keras.Model):
def __init__(self):
super().__init__()
self.net = tf.keras.Sequential()
self.net.add(tf.keras.layers.Dense(64, activation=tf.nn.relu))
self.net.add(tf.keras.layers.Dense(32, activation=tf.nn.relu))
self.dense = tf.keras.layers.Dense(16, activation=tf.nn.relu)
def call(self, inputs):
return self.dense(self.net(inputs))
chimera = tf.keras.Sequential()
chimera.add(NestMLP())
chimera.add(tf.keras.layers.Dense(20))
chimera.add(FixedHiddenMLP())
chimera(X)
<tf.Tensor: shape=(), dtype=float32, numpy=0.621958>
5.1.4. Eficiência¶
O leitor ávido pode começar a se preocupar sobre a eficiência de algumas dessas operações. Afinal, temos muitas pesquisas de dicionário, execução de código e muitas outras coisas Pythônicas ocorrendo no que deveria ser uma biblioteca de Deep Learning de alto desempenho. Os problemas do Bloqueio do Interprete Global do Python são bem conhecidos. No contexto de Deep Learning, podemos nos preocupar que nossas GPU(s) extremamente rápidas pode ter que esperar até uma CPU insignificante executa o código Python antes de obter outro trabalho para ser executado. A melhor maneira de acelerar o Python é evitá-lo completamente.
Uma maneira de o Gluon fazer isso é permitindo hibridização, que será descrita mais tarde. Aqui, o interpretador Python executa um bloco na primeira vez que é invocado. O tempo de execução do Gluon registra o que está acontecendo e, da próxima vez, provoca um curto-circuito nas chamadas para Python. Isso pode acelerar as coisas consideravelmente em alguns casos mas é preciso ter cuidado ao controlar o fluxo (como acima) pois conduz a diferentes ramos em diferentes passagens através da rede. Recomendamos que o leitor interessado verifique a seção de hibridização (Section 12.1) para aprender sobre a compilação depois de terminar o capítulo atual.
O leitor ávido pode começar a se preocupar sobre a eficiência de algumas dessas operações. Afinal, temos muitas pesquisas de dicionário, execução de código e muitas outras coisas Pythônicas ocorrendo no que deveria ser uma biblioteca de Deep Learning de alto desempenho. Os problemas do bloqueio do interpretador global do Python são bem conhecidos. No contexto de Deep Learning, podemos nos preocupar que nossas GPU(s) extremamente rápidas pode ter que esperar até uma CPU insignificante executa o código Python antes de obter outro trabalho para ser executado.
O leitor ávido pode começar a se preocupar sobre a eficiência de algumas dessas operações. Afinal, temos muitas pesquisas de dicionário, execução de código e muitas outras coisas Pythônicas ocorrendo no que deveria ser uma biblioteca de aprendizado profundo de alto desempenho. Os problemas do bloqueio do interpretador global do Python são bem conhecidos. No contexto de Deep Learning, podemos nos preocupar que nossas GPU(s) extremamente rápidas pode ter que esperar até uma CPU insignificante executa o código Python antes de obter outro trabalho para ser executado. A melhor maneira de acelerar o Python é evitá-lo completamente.
5.1.5. Sumário¶
Camadas são blocos.
Muitas camadas podem incluir um bloco.
Muitos blocos podem incluir um bloco.
Um bloco pode conter código.
Os blocos cuidam de muitas tarefas domésticas, incluindo inicialização de parâmetros e backpropagation.
As concatenações sequenciais de camadas e blocos são tratadas pelo bloco
Sequencial
.
5.1.6. Exercícios¶
Que tipos de problemas ocorrerão se você alterar
MySequential
para armazenar blocos em uma lista Python?Implemente um bloco que tenha dois blocos como argumento, digamos
net1
enet2
e retorne a saída concatenada de ambas as redes na propagação direta. Isso também é chamado de bloco paralelo.Suponha que você deseja concatenar várias instâncias da mesma rede. Implemente uma função de fábrica que gere várias instâncias do mesmo bloco e construa uma rede maior a partir dele.