Back to TILs

Feed Forward Neural Network no Debian com mlpack

Este artigo mostra como criar uma rede neural simples no Debian stable utilizando a mlpack. A mlpack é uma biblioteca popular e de alta performance para aprendizado de máquina em C++.

Versões do mlpack disponíveis do Debian

As versões disponíveis no momento da escrita deste artigo são:

Versão do Debian Versão do mlpack Arquiteturas
jessie (oldstable) 1.0.10-1 amd64 armel armhf i386
stretch (stable) 2.1.1-1 amd64 arm64 armel armhf i386 mips mips64el mipsel ppc64el s390x
buster (testing) 3.0.4-1 amd64 arm64 armel armhf i386 mips mips64el mipsel ppc64el s390x
sid (unstable) 3.0.4-1+b1 kfreebsd-amd64 kfreebsd-i386

As versões do libmlpack-dev disponíveis para o Debian podem ser consultadas neste link.

Neste artigo usaremos a versão 3.0.4 presente no testing.

Utilizando pacotes testing junto com stable

Eu prefiro utilizar somente os pacotes stable nos servidores por serem versões muito bem testadas. Normalmente as últimas versões não estão disponíveis no repositório stable, mas sim nos repositórios testing e unstable. O repositório testing contém próximo candidato a se tornar stable de cada pacote. O repositório unstable contém a última versão disponível de cada pacote.

Misturar repositórios diferente pode ser algo tranquilo de manter se feito com cuidado. É só indicar precisamente qual pacote deve ser pego de cada repositório. O Debian tem um mecanismo bem fácil de usar para realizar esta tarefa. No meu caso eu tenho uma regra geral: pegar sempre do stable e algumas poucas exceções para pacotes específicos.

Para mesclar diferentes repositórios basta pinar os pacotes para indicar quais versões serão usadas.

Para marcar o mlpack e o armadillo para serem instalados a partir do testing crie um arquivo chamado /etc/apt/preferences.d/mlpack com o seguinte conteúdo:

Package: *mlpack*
Pin: release a=testing
Pin-Priority: 1002

Package: *armadillo*
Pin: release a=testing
Pin-Priority: 1002

O arquivo /etc/apt/preferences.d/mlpack poderia ter qualquer nome, mas é conveniente usar algo descritivo.

E para desfazer a pinagem basta remover o arquivo.

Então é só proceder a instalação normal com o apt como de costume:

apt install libmlpack-dev

Dados de treinamento

Vamos utilizar dois grupos de dados, cada um com duas variáveis (x e y).

Olhando nosso conjunto de dados de cima podemos ver claramente duas curvas, uma para cada conjunto.

Grupo de dados de treinamento
Fig. 1 - Grupo de dados de treinamento

Para facilitar a visualização, o primeiro grupo está em z=0 e o segundo em z=1, conforme pode ser visto na animação abaixo. É mais ou menos isso que a rede deverá aprender.

Grupo de dados de treinamento
Fig. 2 - Grupo de dados de treinamento

O arquivo foo.csv contém as duas curvas com os pontos distribuídos do modo aleatório para não favorecer ou influenciar o treinamento. São ao todo 400 linhas e 3 colunas. As duas primeiras colunas são as entradas e a terceira é o grupo a qual pertencem.

-1.336471627143056118e+00,3.770125142464317847e+00,0.000000000000000000e+00
1.804415958408375431e+00,2.999013679368799146e+00,1.000000000000000000e+00
1.401588838630331679e+00,3.746404773569839364e+00,0.000000000000000000e+00
9.741670427761830453e-01,3.361695788254598583e+00,1.000000000000000000e+00
-2.602637515616321284e+00,2.340144859256901189e+00,1.000000000000000000e+00
...

Implementação

Carregando as dependências

Incluindo os cabeçalhos específicos para rede neural artificial (ANN).

#include <mlpack/core.hpp>
#include <mlpack/methods/ann/ffn.hpp>
#include <mlpack/methods/ann/layer/layer.hpp>
#include <mlpack/methods/ann/loss_functions/mean_squared_error.hpp>

Para deixar o código bem mais legível alguns namespaces serão usados por padrão.

using namespace mlpack;
using namespace mlpack::ann;
using namespace mlpack::optimization;
using namespace arma;
using namespace std;
using mlpack::data::Load;
using mlpack::data::Save;

Carregar e transpor os dados do arquivo CSV

As matrizes do Armadillo (pacote de álgebra linear do mlpack) são armazenada no formato column-major; isto significa que no disco cada coluna é localizada numa região contígua de memória. Veja detalhes na documentação do mlpack.

Esta é uma conveniência bem interessante, pois para escrever os arquivos os registros são entrados por linha e para processamento são tratados em colunas.

Então além de carregar é preciso transpor a matriz de dados.

  // lança uma exceção std::runtime_error se não conseguir carregar. default false
  const auto THROW_EXCEPTION = true;
  // transpôe a matriz depois de carregar. default true
  const auto TRANSPOSE_INPUT = true;
  Load( "foo.csv", data, THROW_EXCEPTION, TRANSPOSE_INPUT );
  cout << "Linhas:  " << data.n_rows << endl; // 3
  cout << "Colunas: " << data.n_cols << endl; // 400

A matriz de dados foi carregada e precisa ser dividida entre dados de treinamento e dados de teste.

Dados depois de carregados
Fig. 3 - Dados depois de carregados

É muito importante que o modelo nunca veja os dados de teste para que se tenha uma boa generalização.

Constantes para os índices

Para facilitar a leitura do fonte e evitar números mágicos no código defini algumas constantes convenientes:

  const auto VAR1_ROW  = 0;
  const auto VAR2_ROW  = 1;
  const auto LABEL_ROW = 2;
  const auto FIRST_COL = 0;
  const auto LAST_COL  = data.n_cols - 1;
  const auto TEST_SIZE = 10;

Note que neste caso serão usadas 10 amostras para teste.

Dados de entrada para treinamento

Usando a função submat vamos recortar uma porção dos dados para formar os dados de entrada para treinamento.

  auto firtsRow  = VAR1_ROW;
  auto firtsCol  = FIRST_COL;
  auto lastRow   = VAR2_ROW;
  auto lastCol   = LAST_COL - TEST_SIZE;
  mat  traindata = data.submat( firtsRow, firtsCol, lastRow, lastCol );

Dados de saída para treinamento

Usando a função submat vamos recortar uma porção dos dados para formar os dados de saída para treinamento.

  firtsRow        = LABEL_ROW;
  lastRow         = LABEL_ROW;
  mat trainlabels = data.submat( firtsRow, firtsCol, lastRow, lastCol );

Dados de entrada para teste

Usando a função submat vamos recortar uma porção dos dados para formar os dados de entrada para teste.

  firtsRow     = VAR1_ROW;
  lastRow      = VAR2_ROW;
  firtsCol     = LAST_COL - TEST_SIZE + 1;
  lastCol      = LAST_COL;
  mat testdata = data.submat( firtsRow, firtsCol, lastRow, lastCol );

Dados de saída para teste

Usando a função submat vamos recortar uma porção dos dados para formar os dados de saída para teste.

  firtsRow       = LABEL_ROW;
  lastRow        = LABEL_ROW;
  mat testlabels = data.submat( firtsRow, firtsCol, lastRow, lastCol );

Visualizando os dados de teste

  cout << "Dados de entrada para teste: \n" << testdata << endl;
  cout << "Dados de saída para teste: \n" << testlabels << endl;

É importante notar que os dados de teste contém amostras de ambos os grupos e estão distribuídos em todo o espaço de amostra.

Dados de entrada para teste:
   0.7482   2.5516   2.1925  -1.3355   0.0608   0.6829  -2.6796  -0.6668  -2.9646  -2.8399
   3.9294   2.3957   2.7282   3.7705   3.9995   3.4327   2.2516   3.9440   1.8603   2.0458

Dados de saída para teste:
   0        1.0000   1.0000   0        0        1.0000   1.0000   0        1.0000   1.0000

Construindo a rede neural

img
Fig. 4 - img
  // Feed Forward Network
  // FFN<tipo-de-saída-das-camadas, regra-de-inicialização> model;
  FFN<MeanSquaredError<>, RandomInitialization> model;
  // Adiciona camada com 2 entradas e 8 saídas
  model.Add<Linear<>>( traindata.n_rows, 8 );
  model.Add<SigmoidLayer<>>();
  // Adiciona camada com 8 entradas e 8 saídas
  model.Add<Linear<>>( 8, 8 );
  model.Add<SigmoidLayer<>>();
  // Adiciona camada com 8 entradas e 1 saída
  model.Add<Linear<>>( 8, 1 );
  model.Add<SigmoidLayer<>>();

Treinamento

A partir da inicialização aleatória dos pesos da conexões inicia-se o treinamento.

  for( int i = 0; i < 4; ++i ) {
    model.Train( traindata, trainlabels );
    // Acompanha o erro do modelo
    mat assignments;
    model.Predict( testdata, assignments );
    // Diferença entre o obtido e o esperado
    mat diff = assignments - testlabels;
    cout << "Erro: " << diff * diff.t();
  }

Note o erro sendo reduzido a cada treinamento.

Erro:    0.0292
Erro:    0.0058
Erro:    0.0038
Erro:    0.0024

Testando o modelo ajustado

  mat assignments;
  model.Predict( testdata, assignments );
  cout << "Previsões:\n" << assignments << endl;
  cout << "Classificação correta:\n" << testlabels << endl;

Saída:

Previsões:
   0.0074   0.9985   0.9918   0.0097   0.0057   0.9853   0.9998   0.0107   1.0000   1.0000

Classificação correta:
   0        1.0000   1.0000   0        0        1.0000   1.0000   0        1.0000   1.0000

Salvando modelo para continuar depois

O modelo pode ser salvo a qualquer momento e recarregado para continuar o treinamento. Um arquivo XML serializado pelo boost é salvo.

  Save( "model.xml", "model", model, false );

Carregando o modelo salvo na sessão anterior

  Load( "model.xml", "model", model );

Nova sessão de treinamento para refinar

  for( int i = 0; i < 4; ++i ) {
    model.Train( traindata, trainlabels );
  }

Novos testes

  model.Predict( testdata, assignments );
  cout << "Previsões:\n" << assignments << endl;
  cout << "Classificação correta:\n" << testlabels << endl;

Saída:

Previsões:
   0.0045   0.9996   0.9893   0.0010   0.0049   0.9942   1.0000   0.0076   1.0000   1.0000

Classificação correta:
   0        1.0000   1.0000   0        0        1.0000   1.0000   0        1.0000   1.0000

Salvando o modelo atualizado

  Save( "model2.xml", "model", model, false );

Código fonte completo

O código fonte completo pode ser encontrado no github.

Para baixar e compilar o fonte deste artigo utilize:

git clone https://github.com/geraldolsribeiro/mlpack-tutorials/
cd mlpack-tutorials/ffn
make

Referências