Explorando o mundo do aprendizado profundo com PyTorch — Parte 1

Uma nova abordagem para prever preços de imóveis

Felipe Sassi
Datarisk.io
9 min readFeb 22, 2021

--

Este artigo será o primeiro de uma série que irá abordar diferentes tipos e aplicações do framework PyTorch na indústria.

Aqui, apresentarei conceitos introdutórios e extremamente importantes sobre o framework e seu ecossistema. A ideia é desenvolver uma trilha de aprendizado, indo do básico (abordado nesta parte 1) até os conceitos mais avançados, como métodos para lidar com big data, treinamento em GPU etc.

Por ora, falaremos de conceitos como tensores, camadas e funções custo, aplicando-os no desenvolvimento de um modelo muito difundido em aprendizado de máquina: regressão linear.

Todos os códigos desenvolvidos durante nossa jornada ficarão no seguinte repositório: https://github.com/felipesassi/pytorch-course.

Por que usar o PyTorch?

O PyTorch foi disponibilizado para o público geral em determinado período de janeiro de 2017. Seu desenvolvimento foi realizado nos laboratórios de IA do Facebook.

Primeiramente, é importante deixar claro que o PyTorch é um framework para deep learning (ou aprendizado profundo). Um framework nada mais é do que um conjunto de ferramentas que auxiliam no desenvolvimento de projetos.

A preferência por um determinado framework ou outro é pessoal, mas vou elencar alguns pontos que me levaram a optar pelo PyTorch:

  1. Facilidade de uso;
  2. Ótima documentação;
  3. Aumento de uso crescente ao longo do tempo.

No início, o PyTorch era muito utilizado dentro das academias (principalmente devido à facilidade de desenvolver arquiteturas mais complexas nele), mas com o passar dos anos, diferentes empresas (como a Tesla) o adotaram para desenvolver seus produtos.

Regressão linear — Fundamentos

Antes de começar a trabalhar, mais alguns conceitos devem ser vistos. A ideia aqui não é rever tudo o que existe sobre regressões lineares, mas sim apresentar alguns recursos que serão úteis para seu desenvolvimento com PyTorch.

Uma regressão linear é usada para prever dados contínuos (ou seja: uma variável que queremos prever pode assumir infinitos valores). Vamos supor que precisamos prever o preço de venda de uma casa em função de diferentes características do imóvel, como área total, quantidade de quartos, presença de piscina etc.

Suponha que temos um conjunto de dados x, que pode ser uma matriz 1000x5 (onde 1000 se refere ao número de dados e 5 aos atributos medidos em cada observação), e um conjunto de dados observados y que, para fazer sentido com o exemplo, deve ser um vetor de 1000 observações.

Com isso, podemos treinar uma regressão linear para obter um modelo que represente a relação entre os conjuntos de dados x e y, ou seja: conseguimos obter uma função aproximada que modele o nosso problema. Nosso intuito é obter o modelo que gere a menor diferença possível entre nossas observações reais (y) e nossas predições (ŷ).

Utilizando um pouco de álgebra linear, podemos ver uma regressão linear da seguinte forma:

Equação 1 — Definindo uma regressão linear com álgebra linear.

No nosso caso, y é a variável a se prever e w são os pesos que devem ser informados na entrada x.

Para se obter a matriz w, duas técnicas podem ser utilizadas:

  • A partir de álgebra linear. Nesse Caso, nossa matriz w fica da seguinte forma:
Equação 2 — Solução exata para a equação 1.
  • Numericamente, utilizando a minimização de uma função custo com o método do gradiente descendente.

A utilização do gradiente descendente é a mais comum, visto que as operações apresentadas na equação 1 (principalmente a inversão matricial) são extremamente custosas.

Para avaliar a qualidade da nossa predição, podemos usar uma métrica conhecida como erro quadrático médio, apresentada na equação 3:

Equação 3 — Erro quadrático médio.

Quanto mais próximo de zero esse erro estiver, melhor.

Conceitos básicos sobre PyTorch

Grande parte do que será apresentado a seguir foi publicado na documentação oficial do PyTorch, que pode ser uma excelente fonte de pesquisa e esclarecimento de dúvidas para esse tipo de implementação.

Tensores:

Tensores são uma estrutura de dados usada pelo PyTorch, semelhante a uma estrutura de arrays do numpy (valendo mencionar que uma conversão entre tensores e arrays do numpy é muito simples).

A seguir, mostrarei como os tensores funcionam com alguns exemplos bem simples.

Primeiro, apresento como converter um numpy array para um tensor e vice-versa.

Figura 1 — Pode-se notar a praticidade na conversão.

A conversão é realizada por meio do método .numpy(). Um jeito simples de iniciar determinado projeto é realizar todas as operações com o numpy e convertê-las para tensor apenas no momento necessário. Um ponto interessante é que essa compatibilidade faz com que todo o ecossistema de aprendizado de máquina que utiliza o numpy (como pandas, matplotlib, scikit-learn) possa ser usado com facilidade nos projetos envolvendo o PyTorch.

Os tensores também podem ser criados a partir de listas. Uma sintaxe para isso fica assim:

Figura 2 — Tensores podem ser criados a partir de listas também.

O próximo passo é apresentar alguns atributos interessantes dos tensores, como seu shape e seu tipo. Para isso, devemos fazer o seguinte:

Figura 3 — Saber o shape e o tipo do tensor muitas vezes é necessário.

Para conversão de tipos (o que muitas vezes é necessário), o PyTorch traz alguns métodos nativos:

Figura 4 — Geralmente você precisará aplicar o método .float() em seus dados.

Com esses conhecimentos em mente, podemos prosseguir para uma série de operações envolvendo tensores. O código abaixo mostra algumas das possibilidades.

Figura 5 — Diferentes operações matemáticas podem ser realizadas com tensores.

A última operação que devemos ver é a diferenciação. O PyTorch traz um sistema de diferenciação automática chamado autograd. Com ele, podemos computar facilmente derivadas (taxas de variação) de uma função em relação às suas variáveis. Vamos supor que precisamos calcular as derivadas parciais da função apresentada na figura 6.

Figura 6 — Função do nosso interesse.

O sistema autograd lida com isso diretamente. No exemplo abaixo, podemos ver que uma simples chamada ao método .backward() resolve o nosso problema. Note que passamos o argumento requires_grad=True pela função torch.tensor(). Esse argumento avisa ao PyTorch que queremos computar a derivada em relação a essa variável.

Figura 7 — Computando as derivadas.

Agora, vamos abordar alguns conceitos mais aprofundados do PyTorch e do mundo de deep learning. Não se preocupe se algumas passagens ficarem obscuras: à medida que avançarmos, o entendimento será mais natural.

Layers

Layers, ou camadas, são operações que podem ser aplicadas a tensores. Algumas dessas operações serão abordadas no futuro, mas no momento podemos citar:

  • nn.Linear: essa operação aplica uma transformação linear no tensor de entrada. Basicamente, o tensor de entrada é multiplicado (multiplicação matricial) por um tensor de pesos e o resultado final é somado com outro tensor de pesos (lembra-se da equação 1?);
  • nn.Conv2d: essa camada aplica uma convolução ao tensor de entrada. Essa operação é muito utilizada quando se deseja trabalhar com classificação de imagens.

Loss functions

Loss functions são funções que nos ajudam a medir como está o desempenho do nosso modelo. Além disso, elas servem como objetivo de minimização durante o treinamento, ou seja: durante o treino, os pesos do nosso modelo vão sendo atualizados para fazer com que nossa perda seja reduzida.

Um exemplo de loss function é a equação 3, muito utilizada em problemas de regressão.

Optimizers

Quem tem algum tipo de familiaridade com redes neurais sabe que seu treinamento é realizado por meio de uma técnica conhecida como gradiente descendente. Essa técnica é o modo mais simples de atualização de pesos em problemas de otimização numérica. O PyTorch implementa diferentes refinamentos desse método, de forma a auxiliar no treinamento das redes neurais. O otimizador mais utilizado é o Adam (artigo sobre ele e outros otimizadores nas referências).

Com esses conhecimentos, já é possível ter uma ideia das principais etapas necessárias para desenvolver um projeto com o PyTorch. Para não me alongar, deixarei alguns artigos nas referências sobre alguns temas interessantes (e mais complexos também), como automatic diferentiation.

Para aplicarmos nosso modelo, vamos utilizar um conjunto de dados bem conhecido em problemas de regressão, o boston housing dataset (que inclusive está disponível para uso dentro do módulo sklearn.datasets).

A ideia desse conjunto de dados é prever o preço de um imóvel com base em suas características, como número de quartos, acessibilidade do imóvel etc.

Para prosseguirmos, você deve assumir algumas coisas:

  1. Os arrays x_train, y_train, x_val, y_val (que serão utilizados) foram obtidos do conjunto de dados original (80% dos dados estão no conjunto de treino);
  2. Os arrays x_train e x_val estão normalizados.

Dito isso, podemos desenvolver nossa regressão linear. Uma das principais formas para desenvolver modelos no PyTorch é utilizando classes. O trecho de código abaixo apresenta a estrutura básica que seu modelo deve ter:

Figura 9 — O esqueleto de um modelo.

O método forward() é efetivamente chamado para realizar a predição. Utilizando o esqueleto apresentado, nossa regressão linear fica da seguinte forma:

Figura 10 — Regressão linear desenvolvida.

A classe nn.LinearModel() implementa a equação 1. O usuário só precisa informar a quantidade de features de entrada (que no nosso caso é 13, o número de características do imóvel) e a quantidade de features de saída (que no nosso caso é o preço do imóvel, ou seja: 1). No método forward(), a equação 1 é devidamente aplicada aos dados de entrada.

E agora? Como treinamos nosso modelo?

Abaixo, apresento uma função bem simples para treino:

Figura 11 — OK, talvez essa função não seja tão simples assim.

Essa função treina e valida o modelo por determinado número de épocas (variável epochs), sendo um esqueleto útil para os seus projetos futuros.

  1. Na linha 31, nosso otimizador (Adam) recebe dois parâmetros, os parâmetros do modelo (acessados por meio do método .parameters()) e a taxa de aprendizado lr, que é passada como argumento da função;
  2. Dentro do loop for, a primeira coisa a se fazer é chamar o método .zero_grad() (na linha 34) do nosso otimizador. Ele zera os gradientes que se acumulam durante o treino (não se preocupe se não entendeu 100% nesse momento, assuma que esta etapa é necessária);
  3. Nas linhas 35 e 36, os dados são passados ao modelo e a predição é salva na variável y_predicted. O custo é calculado;
  4. Na linha 37, é realizada a backpropagation aplicando o método .backward() na nossa loss function;
  5. Por fim, atualizamos os pesos com o método .step() (presente na linha 38);
  6. O restante serve para salvar as informações obtidas durante o treino. Note que eu utilizo o método .detach() antes da conversão para array do numpy. Esse método desacopla determinada variável do grafo computacional (colocarei uma referência sobre isso);
  7. Note o with torch.no_grad(): (na linha 41). Ele serve para avisar ao PyTorch que não é preciso salvar os gradientes das variáveis dentro desse escopo (salvar os gradientes só é útil durante o treinamento).
  8. Todo esse processo se repete por um determinado número de épocas (parâmetro epochs da nossa função). Uma época é completada quando “passamos” todos os nossos dados pelo modelo (vamos ver mais sobre isso durante nossa série).

E como ficaram os resultados?

A figura 12 mostra o trecho de código responsável por instanciar nossa classe LinearModel e a chamada da função train_validate_model (não se esqueça de definir a métrica mean_squared_error).

Figura 12 — Treinando e validando o modelo.

Os resultados foram os seguintes:

Figura 13 — Comportamento do custo / métrica a cada época. Pode-se notar a diferença nos valores finais. Você sabe explicar o motivo?

Podemos ver que a cada época, nosso custo decai até atingir um valor estável de 19,34 para o treinamento e 33,28 para a validação.

Conclusão

Tratamos de vários assuntos nesse primeiro momento, muitos deles para criar uma ponte entre conceitos que você já deve conhecer, como erro quadrático médio, e como isso se encaixa no framework PyTorch.

Nos próximos artigos, daremos continuidade aos nossos estudos, desenvolvendo redes neurais para os mais diversos fins.

Espero que tenham gostado!

Qualquer dúvida, é só me chamar no linkedin/e-mail.

Referências

(Não necessariamente na ordem de ocorrência)

https://pytorch.org/tutorials/beginner/blitz/tensor_tutorial.html#sphx-glr-beginner-blitz-tensor-tutorial-py

  1. https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html#sphx-glr-beginner-blitz-autograd-tutorial-py
  2. https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#sphx-glr-beginner-blitz-neural-networks-tutorial-py
  3. https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py
  4. https://www.youtube.com/watch?v=oBklltKXtDE&ab_channel=PyTorch
  5. https://arxiv.org/pdf/1412.6980.pdf

--

--

Felipe Sassi
Datarisk.io

Senior data scientist passionate about building things