Atualizado em Pytorch 1.5
Você pode encontrar o código aqui
Pytorch é uma estrutura de aprendizado profundo de código aberto que fornece uma maneira inteligente de criar modelos de ML. Mesmo que a documentação seja bem feita, ainda vejo que a maioria das pessoas não escreve código bem e organizado no PyTorch.
Hoje veremos como usar os três blocos de construção principais do PyTorch: Module, Sequential and ModuleList
. Começaremos com um exemplo e, iterativamente, iremos melhorá-lo.
Todas essas quatro classes estão contidas em torch.nn
import torch . nn as nn
# nn.Module
# nn.Sequential
# nn.Module
O Módulo é o bloco de construção principal, ele define a classe base para todas as redes neurais e você DEVE subclassificá-la.
Vamos criar um classificador CNN clássico como exemplo:
import torch . nn . functional as F
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , n_classes ):
super (). __init__ ()
self . conv1 = nn . Conv2d ( in_c , 32 , kernel_size = 3 , stride = 1 , padding = 1 )
self . bn1 = nn . BatchNorm2d ( 32 )
self . conv2 = nn . Conv2d ( 32 , 64 , kernel_size = 3 , stride = 1 , padding = 1 )
self . bn2 = nn . BatchNorm2d ( 64 )
self . fc1 = nn . Linear ( 64 * 28 * 28 , 1024 )
self . fc2 = nn . Linear ( 1024 , n_classes )
def forward ( self , x ):
x = self . conv1 ( x )
x = self . bn1 ( x )
x = F . relu ( x )
x = self . conv2 ( x )
x = self . bn2 ( x )
x = F . relu ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . fc1 ( x )
x = F . sigmoid ( x )
x = self . fc2 ( x )
return x
model = MyCNNClassifier ( 1 , 10 )
print ( model )
MyCNNClassifier(
(conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(fc1): Linear(in_features=50176, out_features=1024, bias=True)
(fc2): Linear(in_features=1024, out_features=10, bias=True)
)
Este é um classificador muito simples com uma parte de codificação que usa duas camadas com 3x3 convs + batchnorm + relu e uma parte de decodificação com duas camadas lineares. Se você não é novo no PyTorch, talvez já tenha visto esse tipo de codificação antes, mas há dois problemas.
Se quisermos adicionar uma camada, teremos que escrever novamente muito código na função __init__
e na função forward
. Além disso, se tivermos algum bloco comum que queremos usar em outro modelo, por exemplo, o 3x3 conv + batchnorm + relu, teremos que escrevê-lo novamente.
Sequencial é um contêiner de módulos que podem ser empilhados e executados ao mesmo tempo.
Você pode notar que temos que armazenar tudo em self
. Podemos usar Sequential
para melhorar nosso código.
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , n_classes ):
super (). __init__ ()
self . conv_block1 = nn . Sequential (
nn . Conv2d ( in_c , 32 , kernel_size = 3 , stride = 1 , padding = 1 ),
nn . BatchNorm2d ( 32 ),
nn . ReLU ()
)
self . conv_block2 = nn . Sequential (
nn . Conv2d ( 32 , 64 , kernel_size = 3 , stride = 1 , padding = 1 ),
nn . BatchNorm2d ( 64 ),
nn . ReLU ()
)
self . decoder = nn . Sequential (
nn . Linear ( 64 * 28 * 28 , 1024 ),
nn . Sigmoid (),
nn . Linear ( 1024 , n_classes )
)
def forward ( self , x ):
x = self . conv_block1 ( x )
x = self . conv_block2 ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , 10 )
print ( model )
MyCNNClassifier(
(conv_block1): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(conv_block2): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(decoder): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
(2): Linear(in_features=1024, out_features=10, bias=True)
)
)
Muito melhor, uhu?
Você notou que conv_block1
e conv_block2
parecem quase iguais? Poderíamos criar uma função que retorna um nn.Sequential
para simplificar ainda mais o código!
def conv_block ( in_f , out_f , * args , ** kwargs ):
return nn . Sequential (
nn . Conv2d ( in_f , out_f , * args , ** kwargs ),
nn . BatchNorm2d ( out_f ),
nn . ReLU ()
)
Então podemos simplesmente chamar esta função em nosso Módulo
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , n_classes ):
super (). __init__ ()
self . conv_block1 = conv_block ( in_c , 32 , kernel_size = 3 , padding = 1 )
self . conv_block2 = conv_block ( 32 , 64 , kernel_size = 3 , padding = 1 )
self . decoder = nn . Sequential (
nn . Linear ( 64 * 28 * 28 , 1024 ),
nn . Sigmoid (),
nn . Linear ( 1024 , n_classes )
)
def forward ( self , x ):
x = self . conv_block1 ( x )
x = self . conv_block2 ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , 10 )
print ( model )
MyCNNClassifier(
(conv_block1): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(conv_block2): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(decoder): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
(2): Linear(in_features=1024, out_features=10, bias=True)
)
)
Ainda mais limpo! Ainda conv_block1
e conv_block2
são quase iguais! Podemos mesclá-los usando nn.Sequential
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , n_classes ):
super (). __init__ ()
self . encoder = nn . Sequential (
conv_block ( in_c , 32 , kernel_size = 3 , padding = 1 ),
conv_block ( 32 , 64 , kernel_size = 3 , padding = 1 )
)
self . decoder = nn . Sequential (
nn . Linear ( 64 * 28 * 28 , 1024 ),
nn . Sigmoid (),
nn . Linear ( 1024 , n_classes )
)
def forward ( self , x ):
x = self . encoder ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , 10 )
print ( model )
MyCNNClassifier(
(encoder): Sequential(
(0): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(1): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
)
(decoder): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
(2): Linear(in_features=1024, out_features=10, bias=True)
)
)
self.encoder
agora contém booth conv_block
. Desacoplamos a lógica do nosso modelo e facilitamos a leitura e a reutilização. Nossa função conv_block
pode ser importada e usada em outro modelo.
E se pudermos adicionar novas camadas em self.encoder
, codificá-las não é conveniente:
self . encoder = nn . Sequential (
conv_block ( in_c , 32 , kernel_size = 3 , padding = 1 ),
conv_block ( 32 , 64 , kernel_size = 3 , padding = 1 ),
conv_block ( 64 , 128 , kernel_size = 3 , padding = 1 ),
conv_block ( 128 , 256 , kernel_size = 3 , padding = 1 ),
)
Seria bom se pudéssemos definir os tamanhos como um array e criar automaticamente todas as camadas sem escrever cada uma delas? Felizmente podemos criar um array e passá-lo para Sequential
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , n_classes ):
super (). __init__ ()
self . enc_sizes = [ in_c , 32 , 64 ]
conv_blocks = [ conv_block ( in_f , out_f , kernel_size = 3 , padding = 1 )
for in_f , out_f in zip ( self . enc_sizes , self . enc_sizes [ 1 :])]
self . encoder = nn . Sequential ( * conv_blocks )
self . decoder = nn . Sequential (
nn . Linear ( 64 * 28 * 28 , 1024 ),
nn . Sigmoid (),
nn . Linear ( 1024 , n_classes )
)
def forward ( self , x ):
x = self . encoder ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , 10 )
print ( model )
MyCNNClassifier(
(encoder): Sequential(
(0): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(1): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
)
(decoder): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
(2): Linear(in_features=1024, out_features=10, bias=True)
)
)
Vamos decompô-lo. Criamos um array self.enc_sizes
que contém os tamanhos do nosso codificador. Em seguida, criamos um array conv_blocks
iterando os tamanhos. Como temos que dar ao booth um tamanho interno e um tamanho maior para cada camada, zip
o size'array consigo mesmo, deslocando-o em um.
Só para ficar claro, dê uma olhada no seguinte exemplo:
sizes = [ 1 , 32 , 64 ]
for in_f , out_f in zip ( sizes , sizes [ 1 :]):
print ( in_f , out_f )
1 32
32 64
Então, como Sequential
não aceita lista, nós a decompomos usando o operador *
.
Tada! Agora, se quisermos apenas adicionar um tamanho, podemos facilmente adicionar um novo número à lista. É uma prática comum tornar o tamanho um parâmetro.
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , enc_sizes , n_classes ):
super (). __init__ ()
self . enc_sizes = [ in_c , * enc_sizes ]
conv_blocks = [ conv_block ( in_f , out_f , kernel_size = 3 , padding = 1 )
for in_f , out_f in zip ( self . enc_sizes , self . enc_sizes [ 1 :])]
self . encoder = nn . Sequential ( * conv_blocks )
self . decoder = nn . Sequential (
nn . Linear ( 64 * 28 * 28 , 1024 ),
nn . Sigmoid (),
nn . Linear ( 1024 , n_classes )
)
def forward ( self , x ):
x = self . encoder ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , [ 32 , 64 , 128 ], 10 )
print ( model )
MyCNNClassifier(
(encoder): Sequential(
(0): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(1): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(2): Sequential(
(0): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
)
(decoder): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
(2): Linear(in_features=1024, out_features=10, bias=True)
)
)
Podemos fazer o mesmo para a parte do decodificador
def dec_block ( in_f , out_f ):
return nn . Sequential (
nn . Linear ( in_f , out_f ),
nn . Sigmoid ()
)
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , enc_sizes , dec_sizes , n_classes ):
super (). __init__ ()
self . enc_sizes = [ in_c , * enc_sizes ]
self . dec_sizes = [ 64 * 28 * 28 , * dec_sizes ]
conv_blocks = [ conv_block ( in_f , out_f , kernel_size = 3 , padding = 1 )
for in_f , out_f in zip ( self . enc_sizes , self . enc_sizes [ 1 :])]
self . encoder = nn . Sequential ( * conv_blocks )
dec_blocks = [ dec_block ( in_f , out_f )
for in_f , out_f in zip ( self . dec_sizes , self . dec_sizes [ 1 :])]
self . decoder = nn . Sequential ( * dec_blocks )
self . last = nn . Linear ( self . dec_sizes [ - 1 ], n_classes )
def forward ( self , x ):
x = self . encoder ( x )
x = x . view ( x . size ( 0 ), - 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , [ 32 , 64 ], [ 1024 , 512 ], 10 )
print ( model )
MyCNNClassifier(
(encoder): Sequential(
(0): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(1): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
)
(decoder): Sequential(
(0): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
)
(1): Sequential(
(0): Linear(in_features=1024, out_features=512, bias=True)
(1): Sigmoid()
)
)
(last): Linear(in_features=512, out_features=10, bias=True)
)
Seguimos o mesmo padrão, criamos um novo bloco para a parte de decodificação, linear + sigmóide, e passamos um array com os tamanhos. Tivemos que adicionar um self.last
pois não queremos ativar a saída
Agora podemos até dividir nosso modelo em dois! Codificador + Decodificador
class MyEncoder ( nn . Module ):
def __init__ ( self , enc_sizes ):
super (). __init__ ()
self . conv_blocks = nn . Sequential ( * [ conv_block ( in_f , out_f , kernel_size = 3 , padding = 1 )
for in_f , out_f in zip ( enc_sizes , enc_sizes [ 1 :])])
def forward ( self , x ):
return self . conv_blocks ( x )
class MyDecoder ( nn . Module ):
def __init__ ( self , dec_sizes , n_classes ):
super (). __init__ ()
self . dec_blocks = nn . Sequential ( * [ dec_block ( in_f , out_f )
for in_f , out_f in zip ( dec_sizes , dec_sizes [ 1 :])])
self . last = nn . Linear ( dec_sizes [ - 1 ], n_classes )
def forward ( self , x ):
return self . dec_blocks ()
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , enc_sizes , dec_sizes , n_classes ):
super (). __init__ ()
self . enc_sizes = [ in_c , * enc_sizes ]
self . dec_sizes = [ self . enc_sizes [ - 1 ] * 28 * 28 , * dec_sizes ]
self . encoder = MyEncoder ( self . enc_sizes )
self . decoder = MyDecoder ( self . dec_sizes , n_classes )
def forward ( self , x ):
x = self . encoder ( x )
x = x . flatten ( 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , [ 32 , 64 ], [ 1024 , 512 ], 10 )
print ( model )
MyCNNClassifier(
(encoder): MyEncoder(
(conv_blocks): Sequential(
(0): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(1): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
)
)
(decoder): MyDecoder(
(dec_blocks): Sequential(
(0): Sequential(
(0): Linear(in_features=50176, out_features=1024, bias=True)
(1): Sigmoid()
)
(1): Sequential(
(0): Linear(in_features=1024, out_features=512, bias=True)
(1): Sigmoid()
)
)
(last): Linear(in_features=512, out_features=10, bias=True)
)
)
Esteja ciente de que MyEncoder
e MyDecoder
também podem ser funções que retornam um nn.Sequential
. Prefiro usar o primeiro padrão para modelos e o segundo para blocos de construção.
Ao mergulhar nosso módulo em submódulos é mais fácil compartilhar o código, depurá -lo e testá -lo.
ModuleList
permite armazenar Module
como uma lista. Pode ser útil quando você precisa iterar pela camada e armazenar/usar algumas informações, como no U-net.
A principal diferença entre Sequential
é que ModuleList
não possui um método forward
, portanto as camadas internas não estão conectadas. Supondo que precisamos de cada saída de cada camada no decodificador, podemos armazená-la por:
class MyModule ( nn . Module ):
def __init__ ( self , sizes ):
super (). __init__ ()
self . layers = nn . ModuleList ([ nn . Linear ( in_f , out_f ) for in_f , out_f in zip ( sizes , sizes [ 1 :])])
self . trace = []
def forward ( self , x ):
for layer in self . layers :
x = layer ( x )
self . trace . append ( x )
return x
model = MyModule ([ 1 , 16 , 32 ])
import torch
model ( torch . rand (( 4 , 1 )))
[ print ( trace . shape ) for trace in model . trace ]
torch.Size([4, 16])
torch.Size([4, 32])
[None, None]
E se quisermos mudar para LearkyRelu
em nosso conv_block
? Podemos usar ModuleDict
para criar um dicionário de Module
e alternar Module
dinamicamente quando quisermos
def conv_block ( in_f , out_f , activation = 'relu' , * args , ** kwargs ):
activations = nn . ModuleDict ([
[ 'lrelu' , nn . LeakyReLU ()],
[ 'relu' , nn . ReLU ()]
])
return nn . Sequential (
nn . Conv2d ( in_f , out_f , * args , ** kwargs ),
nn . BatchNorm2d ( out_f ),
activations [ activation ]
)
print ( conv_block ( 1 , 32 , 'lrelu' , kernel_size = 3 , padding = 1 ))
print ( conv_block ( 1 , 32 , 'relu' , kernel_size = 3 , padding = 1 ))
Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): LeakyReLU(negative_slope=0.01)
)
Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
Vamos encerrar tudo!
def conv_block ( in_f , out_f , activation = 'relu' , * args , ** kwargs ):
activations = nn . ModuleDict ([
[ 'lrelu' , nn . LeakyReLU ()],
[ 'relu' , nn . ReLU ()]
])
return nn . Sequential (
nn . Conv2d ( in_f , out_f , * args , ** kwargs ),
nn . BatchNorm2d ( out_f ),
activations [ activation ]
)
def dec_block ( in_f , out_f ):
return nn . Sequential (
nn . Linear ( in_f , out_f ),
nn . Sigmoid ()
)
class MyEncoder ( nn . Module ):
def __init__ ( self , enc_sizes , * args , ** kwargs ):
super (). __init__ ()
self . conv_blocks = nn . Sequential ( * [ conv_block ( in_f , out_f , kernel_size = 3 , padding = 1 , * args , ** kwargs )
for in_f , out_f in zip ( enc_sizes , enc_sizes [ 1 :])])
def forward ( self , x ):
return self . conv_blocks ( x )
class MyDecoder ( nn . Module ):
def __init__ ( self , dec_sizes , n_classes ):
super (). __init__ ()
self . dec_blocks = nn . Sequential ( * [ dec_block ( in_f , out_f )
for in_f , out_f in zip ( dec_sizes , dec_sizes [ 1 :])])
self . last = nn . Linear ( dec_sizes [ - 1 ], n_classes )
def forward ( self , x ):
return self . dec_blocks ()
class MyCNNClassifier ( nn . Module ):
def __init__ ( self , in_c , enc_sizes , dec_sizes , n_classes , activation = 'relu' ):
super (). __init__ ()
self . enc_sizes = [ in_c , * enc_sizes ]
self . dec_sizes = [ 32 * 28 * 28 , * dec_sizes ]
self . encoder = MyEncoder ( self . enc_sizes , activation = activation )
self . decoder = MyDecoder ( dec_sizes , n_classes )
def forward ( self , x ):
x = self . encoder ( x )
x = x . flatten ( 1 ) # flat
x = self . decoder ( x )
return x
model = MyCNNClassifier ( 1 , [ 32 , 64 ], [ 1024 , 512 ], 10 , activation = 'lrelu' )
print ( model )
MyCNNClassifier(
(encoder): MyEncoder(
(conv_blocks): Sequential(
(0): Sequential(
(0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): LeakyReLU(negative_slope=0.01)
)
(1): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): LeakyReLU(negative_slope=0.01)
)
)
)
(decoder): MyDecoder(
(dec_blocks): Sequential(
(0): Sequential(
(0): Linear(in_features=1024, out_features=512, bias=True)
(1): Sigmoid()
)
)
(last): Linear(in_features=512, out_features=10, bias=True)
)
)
Então, em resumo.
Module
quando você tiver um bloco grande composto por vários blocos menoresSequential
quando quiser criar um pequeno bloco a partir de camadasModuleList
quando precisar iterar por algumas camadas ou blocos de construção e fazer algoModuleDict
quando precisar parametrizar alguns blocos do seu modelo, por exemplo, uma função de ativaçãoIsso é tudo pessoal!
Obrigado por ler