Introdução ao Pytorch - Parte 2

Transfer Learning - usando modelos pré-treinados

Vamos usar redes neurais para resolver nosso problema de classificação de flores, mas você tem idéia de qual arquitetura usar? Ou quanto tempo vamos levar para treinar uma rede neural do zero?

Para facilitar nossa vida, vamos usar redes neurais pré-treinadas, disponíveis no Pytorch, e modificar apenas sua última camada, conforme descrito neste tutorial. Existem diversos modelos pra escolher, e eles estão em ordem crescente de complexidade e decrescente de taxas de error, ou seja, quanto mais complexo o modelo, geralmente o erro é menor às custas de tempo de processamento.

Ao realizar este exercício, a rede Resnet50 ofereceu uma taxa satisfatória para mim (algo em torno de 95% de acerto) e um tempo de treinamento da última camada melhor que as de resultados semelhantes, mas nada impede que você teste modelos mais complexos, para aprender. Vamos usar o VGG19 no exercício apenas para fins de demonstração.

model = models.vgg19(pretrained=True)
model_path = 'vgg_model.pth'

Essa atribuição vai fazer o download da rede VGG19 no momento de execução. O próximo passo é fazer alguns ajustes na rede para o nosso problema: vamos adicionar a última camada na rede que será uma rede linear (ou seja, fully connected) e também vamos fazer com que somente essa última camada seja treinada removendo toda a rede pré-treinada da atualização no back-propagation. Aqui você já pode encontrar seu primeiro problema: como a arquitetura de redes neurais são diferentes, o jeito como é feita essa adaptação de uma nova última camada pode mudar. Preste atenção nisso!

# Freeze training for all "features" layers
for param in model.features.parameters():
    param.requires_grad = False
    
n_inputs = model.classifier[-1].in_features

# add last linear layer (n_inputs -> cat_to_name)
# new layers automatically have requires_grad = True
last_layer = nn.Linear(n_inputs, len(cat_to_name))
model.classifier[-1] = last_layer


# if GPU is available, move the model to GPU
if train_on_gpu:
    model.cuda()

# check to see that your last layer produces the expected number of outputs
#print(model.classifier[-1].out_features)

criterion = nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

loss_hist = []


# move tensors to GPU if CUDA is available
if train_on_gpu:
    model.cuda()
Note que também já definimos como será calculado nossa perda (Cross Entropy), e qual será nosso algoritmo de cálculo do gradiente no back propagation (no caso, SGD). Também instanciamos a lista que contém a sequência dos valores de erros (loss_hist) para plotarmos num gráfico e acompanhar como está sendo sua evolução ao longo das epochs.

É importante notar que existem vários métodos para calcular a perda. Utilizamos o Cross Entropy para casos em que o resultado final de nossa rede neural é discreto, como por exemplo no caso de definir a qual espécie de flor nossa imagem pertence. No final de nossa rede temos um vetor com tamanho igual ao número de espécies de flor, e cada valor é a probabilidade de nosso input pertencer a cada uma. Para saber mais sobre os tipos de perda vale a pena consultar as definições do PyTorch.

Outro ponto crucial da aplicação da rede ao nosso problema foi a definição da otimização, que é como a rede vai usar o erro calculado para decidir como adequar seus parâmetros para sua minimização. No momento de realização deste exercício os dois principais otimizadores eram o SGD e o ADAM, mas como este campo da ciência tem sido um com maiores descobertas e melhorias nos últimos anos, creio que logo existirão outros específicos para cada tipo de problema e arquitetura. O otimizador possui diversos parâmetros de ajuste, e para obter os melhores resultados é interessante que você saiba do que se trata. Mas não se desespere pela complexidade que cada um deles possui: esta é uma das partes mais difíceis das redes neurais, todos sofrem para absorver essa quantidade enorme e complexa de conceitos.

Agora, resta o laço de repetição para treinar a rede.

# number of epochs to train the model
n_epochs = 80

for epoch in range(1, n_epochs+1):

    # keep track of training and validation loss
    train_loss = 0.0
    
    ###################
    # train the model #
    ###################
    # model by default is set to train
    for batch_i, (data, target) in enumerate(train_loader):
        # move tensors to GPU if CUDA is available
        if train_on_gpu:
            data, target = data.cuda(), target.cuda()
        # clear the gradients of all optimized variables
        optimizer.zero_grad()
        # forward pass: compute predicted outputs by passing inputs to the model
        output = model(data)
        # calculate the batch loss
        loss = criterion(output, target)
        # backward pass: compute gradient of the loss with respect to model parameters
        loss.backward()
        # perform a single optimization step (parameter update)
        optimizer.step()
        # update training loss 
        train_loss += loss.item()
        
        if batch_i % batch_size == (batch_size-1):    # print training loss every specified number of mini-batches
            print('Epoch %d, Batch %d loss: %.16f' %
                  (epoch, batch_i + 1, train_loss / batch_size))
            loss_hist.append(train_loss/batch_size)
            train_loss = 0.0
E finalmente, vamos acompanhar como o erro está se comportando a cada batch:

plt.plot(loss_hist)
selected_model_name = ''
if model_used==1:
    selected_model_name = 'VGG 19'
elif model_used==2:
    selected_model_name = 'Resnet50'
elif model_used == 3:
    selected_model_name = "AlexNet"
plt.ylabel(selected_model_name)
plt.show()

O que nos imprime o seguinte gráfico:

Do qual podemos tirar algumas conclusões. Primeiramente, no início do treinamento o erro cai de maneira muito rápida, mas nos estágios mais avançados temos dificuldade em saber se o erro está efetivamente diminuindo ou se está se mantendo o mesmo. Por outro lado, quanto mais deixamos a rede treinando, mesmo que o erro caia, podemos estar cometendo overfitting, o que significa que erros menores podem resultar em desempenho pior para um conjunto de dados diferente do usado para treino. Porém essa discussão fica para um próximo post.

Salvando e carregando nosso progresso

Como foi dito na parte 1, redes neurais profundas são caras computacionalmente pela quantidade de cálculos que executam e uma das únicas razões que conseguiram obter tempos de treinamento satisfatórios é pelo uso de placas de vídeo para fazer processamento paralelo. Não faria muito sentido termos que comprar um PC com placa de vídeo de última geração apenas para treinar redes neurais (embora seria uma desculpa perfeita para justificar o gasto para minha esposa), por causa disso é comum utilizarmos computadores dedicados somente para rodar o treinamento. É por isso que estamos no Colab do Google: é grátis e disponibiliza GPUs. Mas por ser grátis, o tempo que podemos utilizar continuamente uma máquina é limitado, e temos que ajustar nosso treinamento para salvar o estado quando o tempo está próximo de terminar para podermos mandar rodar novamente depois. Assim, é interessante colocar no seu modelo uma função de carregar modelo e outra de salvar, conforme os exemplos abaixo.

def load_model(model_path):
    model = torch.load(model_path)
    train_dataset.class_to_idx = model.class_to_idx
    loss_hist = model.loss_hist
    model.eval()
    if train_on_gpu:
        model.cuda()
    return model
#comment line below not to load the previously saved model
model = load_model('model.pth')

# TODO: Save the checkpoint 
model.class_to_idx = train_dataset.class_to_idx
model.loss_hist = loss_hist
torch.save(model,model_path)

Finalmente

Agradeço a paciência em ler até se você conseguiu, e se tiver sugestão ou dúvida use a parte de comentários! Lembrando que o código todo usado neste tutorial pode ser encontrado aqui

Comentários

Top 3 em 1 ano: