PyTorch 101: A Deep Dive with PyTorch

Introdução

Olá leitores, isto é outro post na série que estamos fazendo sobre PyTorch. Este post está direcionado a usuários de PyTorch que estão familiarizados com os fundamentos de PyTorch e gostariam de avançar para um nível intermediário. Embora tenhamos abordado como implementar um classificador básico em um post anterior, neste post, vamos discutir como implementar funcionalidades de aprendizagem profunda mais complexas usando PyTorch. Algumas das objetivas deste post são tornar você entender.

  1. O que é a diferença entre as classes de PyTorch como nn.Module, nn.Functional, nn.Parameter e quando usar cada uma
  2. Como personalizar suas opções de treinamento, como diferentes taxas de aprendizagem para diferentes camadas, diferentes agendas de taxa de aprendizagem
  3. Inicialização de Pesos Personalizada

Então, vamos começar.

nn.Module vs nn.Functional

Isso é algo que aparece muito especialmente quando você está lendo código aberto. Em PyTorch, as camadas são frequentemente implementadas como objetos de um de torch.nn.Module ou funções de torch.nn.Functional. Qual usar? Qual é melhor?

Como já foi abordado na Parte 2, torch.nn.Module é basicamente a pedra angular de PyTorch. A maneira como funciona é que primeiro você define um objeto nn.Module, e depois chama seu método forward para executá-lo. Esta é uma forma Orientada a Objetos de fazer as coisas.

Por outro lado, nn.functional fornece algumas camadas/ativações sob a forma de funções que podem ser chamadas diretamente no input em vez de definir um objeto. Por exemplo, para reescalar um tensor de imagem, você chama torch.nn.functional.interpolate em um tensor de imagem.

Então, como escolher o que usar quando? Quando a camada/ativação/perda que estamos implementando tem uma perda.

Entendendo a Estado de Vida

Normalmente, qualquer camada pode ser vista como uma função. Por exemplo, uma operação de convolução é apenas um conjunto de operações de multiplicação e adição. Portanto, faz sentido implementá-la como uma função certo? Mas espere, a camada mantém pesos que precisam ser armazenados e atualizados enquanto estamos treinando. Portanto, de uma perspectiva programática, uma camada é mais do que uma função. Ela também precisa manter dados, que mudam conforme treinamos a nossa rede.

Eu agora quero que você enfatize o fato de que os dados mantidos pela camada convolucional mudam. Isso significa que a camada tem um estado que muda conforme treinamos. Para implementarmos uma função que realiza a operação de convolução, também precisaríamos definir uma estrutura de dados para manter os pesos da camada separadamente da própria função. E então, tornar essa estrutura de dados externa um input para nossa função.

Ou apenas para evitar a confusão, poderíamos simplesmente definir uma classe para manter a estrutura de dados e fazer a operação de convolução como uma função membro. Isso realmente simplificaria nossa tarefa, já que não precisaríamos se preocupar com variáveis estado fora da função. Nesses casos, preferiríamos usar objetos nn.Module onde temos pesos ou outros estados que poderiam definir o comportamento da camada. Por exemplo, uma camada dropout / Batch Norm se comporta de forma diferente durante o treinamento e na inferência.

Por outro lado, onde nenhum estado ou pesos são necessários, poderia-se usar o nn.functional. Exemplos sendo, redimensionar (nn.functional.interpolate), pooling médio (nn.functional.AvgPool2d).

Apesar dos argumentos acima, a maioria das classes nn.Module tem suas contrapartes no nn.functional. No entanto, a linha de raciocínio acima deve ser respeitada durante o trabalho prático.

nn.Parameter

Uma classe importante em PyTorch é a classe nn.Parameter, que surpreendeu-me, pois obteve pouco coverage em textos de Introdução a PyTorch. Considere o seguinte caso.

class net(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)

  def forward(self, x):
    return self.linear(x)

myNet = net()

#imprime os pesos e bias da camada linear
print(list(myNet.parameters()))

Cada nn.Module tem uma função parameters() que retorna, bem, seus parâmetros treináveis. Temos que definir implícitamente o que esses parâmetros são. Na definição de nn.Conv2d, os autores de PyTorch definiram os pesos e bias como parâmetros de uma camada. No entanto, note uma coisa, quando definimos net, não precisamos adicionar os parameters de nn.Conv2d aos parameters de net. Isso aconteceu implicitamente ao colocar o objeto nn.Conv2d como um membro do objeto net.

Isso é facilitado internamente pela classe nn.Parameter, que herda da classe Tensor. Quando chamamos a função parameters() the um objeto nn.Module, ela retorna todos os seus membros que são objetos nn.Parameter.

Na verdade, todos os pesos de treinamento das classes nn.Module são implementados como objetos nn.Parameter. Sempre que um nn.Module (nn.Conv2d no nosso caso) é atribuído como membro de outro nn.Module, os “parâmetros” do objeto atribuído (isto é, os pesos de nn.Conv2d) são também adicionados aos “parâmetros” do objeto ao qual está sendo atribuído (parâmetros do objeto net). Isso chama-se registrar os “parâmetros” de um nn.Module.

Se você tentar atribuir um tensor a um objeto nn.Module, ele não aparecerá na lista de parâmetros do parameters() a menos que você o defina como um objeto nn.Parameter. Isso foi feito para facilitar situações onde você pode precisar cachear um tensor não diferenciável, por exemplo, cachear o output anterior em casos de RNNs.

class net1(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)
    self.tens = torch.ones(3,4)                       # Isto não aparecerá na lista de parâmetros

  def forward(self, x):
    return self.linear(x)

myNet = net1()
print(list(myNet.parameters()))

##########################################################

class net2(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)
    self.tens = nn.Parameter(torch.ones(3,4))                       # Isto aparecerá na lista de parâmetros

  def forward(self, x):
    return self.linear(x)

myNet = net2()
print(list(myNet.parameters()))

##########################################################

class net3(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)
    self.net  = net2()                      # Parâmetros da rede2 aparecerão na lista de parâmetros da rede3

  def forward(self, x):
    return self.linear(x)

myNet = net3()
print(list(myNet.parameters()))

nn.ModuleList e nn.ParameterList()

Lembro que eu tive que usar um nn.ModuleList quando eu estava implementando o YOLO v3 em PyTorch. Eu tive que criar a rede parsing um arquivo de texto que continha a arquitetura. Eu armazenou todos os objetos nn.Module correspondentes em uma lista Python e depois fez a lista um membro do meu objeto nn.Module representando a rede.

Para simplificá-lo, algo como isto.

layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.layers = layer_list

  def forward(x):
    for layer in self.layers:
      x = layer(x)

net = myNet()

print(list(net.parameters()))  # Parâmetros dos módulos na layer_list não aparecem.

Como você pode ver, ao contrário de quando nós registrariamos módulos individuais, atribuir uma Lista Python não registra os parâmetros dos Módulos dentro da lista. Para corrigir isso, nós envolvemos nossa lista com a classe nn.ModuleList e, em seguida, atribuímo-la como um membro da classe de rede.

layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.layers = nn.ModuleList(layer_list)

  def forward(x):
    for layer in self.layers:
      x = layer(x)

net = myNet()

print(list(net.parameters()))  # Parâmetros de módulos na layer_list aparecem.

Similarmente, uma lista de tensores pode ser registrada envolvendo a lista com uma classe nn.ParameterList.

Inicialização de Pesos

A inicialização de pesos pode influenciar os resultados do seu treinamento. Mais ainda, você pode necessitar de diferentes esquemas de inicialização de pesos para diferentes tipos de camadas. Isso pode ser feito pelas funções modules e apply. modules é uma função membro da classe nn.Module que retorna um iterador contendo todos os objetos membros nn.Module de uma função nn.Module. Em seguida, a função apply pode ser chamada em cada nn.Module para definir sua inicialização.

import matplotlib.pyplot as plt
%matplotlib inline

class myNet(nn.Module):

  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(10,10,3)
    self.bn = nn.BatchNorm2d(10)

  def weights_init(self):
    for module in self.modules():
      if isinstance(module, nn.Conv2d):
        nn.init.normal_(module.weight, mean = 0, std = 1)
        nn.init.constant_(module.bias, 0)

Net = myNet()
Net.weights_init()

for module in Net.modules():
  if isinstance(module, nn.Conv2d):
    weights = module.weight
    weights = weights.reshape(-1).detach().cpu().numpy()
    print(module.bias)                                       # Bias para zero
    plt.hist(weights)
    plt.show()

Histograma de pesos inicializados com Média = 1 e Desvio Padrão = 1

Existem uma mirada de funções de inicialização inplace encontradas no módulo torch..nn.init.

módulos() vs filhos()

Uma função muito semelhante a modules é children. A diferença é pequena, mas importante. Como sabemos, um objeto nn.Module pode conter outros objetos nn.Module como membros de dados.

children() retornará apenas uma lista de objetos nn.Module que são membros de dados do objeto em que children está sendo chamado.

Por outro lado, nn.Modules vai recursivamente dentro de cada objeto nn.Module, criando uma lista de cada objeto nn.Module que aparece no caminho até que não houver mais objetos nn.module. Note, modules() também retorna o nn.Module em que foi chamado como parte da lista.

Observe que o comentário acima é verdadeiro para todos os objetos/classes que herdam da classe nn.Module.

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.convBN =  nn.Sequential(nn.Conv2d(10,10,3), nn.BatchNorm2d(10))
    self.linear =  nn.Linear(10,2)

  def forward(self, x):
    pass

Net = myNet()

print("Printing children\n------------------------------")
print(list(Net.children()))
print("\n\nPrinting Modules\n------------------------------")
print(list(Net.modules()))

Então, quando inicializamos os pesos, talvez queira usar a função modules() porque não podemos entrar no objeto nn.Sequential e inicializar o peso para seus membros.

Imprimindo Informações Sobre a Rede

Poderemos precisar de imprimir informações sobre a rede, quer seja para o usuário ou para fins de depuração. O PyTorch fornece uma maneira muito simples de imprimir muitas informações sobre sua rede usando suas funções named_*. Existem 4 funções desse tipo.

  1. named_parameters. Retorna um iterador que dá um tuplo contendo nome dos parâmetros (se uma camada convolucional for atribuída como self.conv1, então seus parâmetros seriam conv1.weight e conv1.bias) e o valor retornado pela função __repr__ do nn.Parameter

2. named_modules. O mesmo que acima, mas o iterador retorna módulos como a função modules() faz.

3. named_children O mesmo que acima, mas o iterador retorna módulos como a função children() retorna

4. named_buffers Retorna tensores de buffer como média móvel da camada de normalização de lote.

for x in Net.named_modules():
  print(x[0], x[1], "\n-------------------------------")

Diferentes Taxas de Aprendizagem para Diferentes Camadas

Nesta seção, nós aprendermos a usar diferentes taxas de aprendizagem para nossas diferentes camadas. Geralmente, abordaremos como ter diferentes hiperparâmetros para diferentes grupos de parâmetros, quer seja diferente taxa de aprendizagem para diferentes camadas ou diferente taxa de aprendizagem para bias e pesos.

A ideia de implementar algo assim é relativamente simples. Em noss post anterior, onde implementamos um classificador CIFAR, passamos todos os parâmetros da rede como um todo para o objeto optimizador.

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.fc1 = nn.Linear(10,5)
    self.fc2 = nn.Linear(5,2)

  def forward(self, x):
    return self.fc2(self.fc1(x))

Net = myNet()
optimiser = torch.optim.SGD(Net.parameters(), lr = 0.5)

No entanto, a classe torch.optim permite que nós proporcionemos diferentes conjuntos de parâmetros com diferentes taxas de aprendizagem em formato de dicionário.

optimiser = torch.optim.SGD([{"params": Net.fc1.parameters(), 'lr' : 0.001, "momentum" : 0.99},
                             {"params": Net.fc2.parameters()}], lr = 0.01, momentum = 0.9)

No cenário acima, os parâmetros de `fc1` usam uma taxa de aprendizagem de 0.01 e momento de 0.99. Se um hiperparâmetro não é especificado para um grupo de parâmetros (como `fc2`), eles usam o valor padrão desse hiperparâmetro, fornecido como argumento de entrada para a função optimizadora. Você pode criar listas de parâmetros com base em camadas diferentes, ou seja, se o parâmetro é um peso ou um bias, usando a função named_parameters() que abordamos acima.

Agendamento da Taxa de Aprendizagem

Agendar sua taxa de aprendizagem é um hiperparâmetro principal que você deseja ajustar. O PyTorch fornece suporte para agendamento de taxas de aprendizagem com seu módulo torch.optim.lr_scheduler que tem uma variedade de agendamentos de taxa de aprendizagem. O exemplo seguinte demonstra um exemplo tão simples.

scheduler = torch.optim.lr_scheduler.MultiStepLR(optimiser, milestones = [10,20], gamma = 0.1)

O agendador acima, multiplica a taxa de aprendizagem por gamma a cada vez que alcançamos epochs contidos na lista milestones. Em nosso caso, a taxa de aprendizagem é multiplicada por 0.1 no 10nº e no 20nº epoch. Você também terá que escrever a linha scheduler.step no loop no seu código que percorre os epochs para que a taxa de aprendizagem seja atualizada.

Geralmente, o loop de treinamento é composto por dois laços aninhados, onde um loop percorre os epochs e o outro aninhado percorre as batches nesses epochs. Certifique-se de chamar scheduler.step no início do loop de epoch para que sua taxa de aprendizagem seja atualizada. Tome cuidado para não escrever em loop de batch, caso contrário, sua taxa de aprendizagem pode ser atualizada no 10º batch, ao invés do 10nº epoch.

Também lembre-se que scheduler.step não é um substituto para optim.step e você terá que chamar optim.step sempre que você fizer backpropagation para trás. (Isto aconteceria no loop de “batch”).

Salvando seu Modelo

Você pode querer salvar seu modelo para uso futuro em inferencia ou talvez apenas queira criar pontos de verificação de treinamento. Quando se trata de salvar modelos no PyTorch, tem-se duas opções.

A primeira é usar torch.save. Isto é equivalente a serializar todo o objeto nn.Module usando Pickle. Este método salva todo o modelo no disco. Você pode carregar este modelo posteriormente na memória com torch.load.

torch.save(Net, "net.pth")

Net = torch.load("net.pth")

print(Net)

O texto acima irá salvar todo o modelo com pesos e arquitetura. Se você precisar apenas salvar os pesos, em vez de salvar todo o modelo, você pode salvar apenas o state_dict do modelo. O state_dict é basicamente um dicionário que mapeia os objetos nn.Parameter de uma rede para seus valores.

Como mostrado acima, é possível carregar um state_dict existente the um objeto nn.Module. Observe que isso não envolve salvar todo o modelo, mas apenas os parâmetros. Você precisará criar a rede com camadas antes de carregar o state dict. Se a arquitetura da rede não for exatamente a mesma que a do state_dict que salvamos, o PyTorch lançará um erro.

for key in Net.state_dict():
  print(key, Net.state_dict()[key])

torch.save(Net.state_dict(), "net_state_dict.pth")

Net.load_state_dict(torch.load("net_state_dict.pth"))

Um objeto de otimizador do torch.optim também tem um objeto state_dict que é usado para armazenar os hiperparâmetros dos algoritmos de otimização. Ele pode ser salvo e carregado de forma semelhante à que fizemos acima, chamando load_state_dict the um objeto de otimizador.

Conclusão

Isso conclui nossa discussão sobre algumas das funcionalidades avançadas do PyTorch. Espero que as coisas que você leu nestes posts o ajude a implementar ideias complexas de aprendizado profundo que você pode ter concebido. Aqui estão links para estudo adicional caso você se interesse.

  1. Uma lista de opções de programação de taxa de aprendizagem em PyTorch
  2. Salvando e Carregando Modelos – Tutoriais Oficiais do PyTorch
  3. O que é realmente torch.nn?

Source:
https://www.digitalocean.com/community/tutorials/pytorch-101-advanced