Um guia para Deeplearning4j

Um guia para Deeplearning4j

*1. Introdução *

Neste artigo, criaremos uma rede neural simples com a biblioteca https://deeplearning4j.org [deeplearning4j] (dl4j) - uma ferramenta moderna e poderosa para aprendizado de máquina.

Antes de começarmos, não que este guia não exija um conhecimento profundo de álgebra linear, estatística, teoria de aprendizado de máquina e muitos outros tópicos necessários para um engenheiro de ML bem fundamentado.

===* 2. O que é aprendizado profundo? *

Redes neurais são modelos computacionais que consistem em camadas interconectadas de nós.

Os nós são processadores semelhantes a neurônios de dados numéricos. Eles pegam os dados de suas entradas, aplicam alguns pesos e funções a esses dados e enviam os resultados para as saídas. Essa rede pode ser treinada com alguns exemplos dos dados de origem.

O treinamento está essencialmente salvando algum estado numérico (pesos) nos nós, que mais tarde afeta o cálculo. Os exemplos de treinamento podem conter itens de dados com recursos e determinadas classes conhecidas desses itens (por exemplo, “este conjunto de 16 × 16 pixels contém uma letra manuscrita“ a ”).

Após o término do treinamento,* uma rede neural pode * obter informações de novos dados, mesmo que não tenha visto esses itens de dados antes . Uma rede bem modelada e bem treinada pode reconhecer imagens, cartas escritas à mão, fala, processar dados estatísticos para produzir resultados para business intelligence e muito mais.

Redes neurais profundas tornaram-se possíveis nos últimos anos, com o avanço da computação paralela e de alto desempenho. Tais redes diferem das redes neurais simples, pois consistem em várias camadas intermediárias (ou ocultas) . Essa estrutura permite que as redes processem dados de maneira muito mais complicada (de maneira recursiva, recorrente, convolucional etc.) e extraem muito mais informações.

*3. Configurando o projeto *

Para usar a biblioteca, precisamos pelo menos do Java 7. Além disso, devido a alguns componentes nativos, ele funciona apenas com a versão da JVM de 64 bits.

Antes de começar com o guia, vamos verificar se os requisitos foram atendidos:

$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

Primeiro, vamos adicionar as bibliotecas necessárias ao nosso arquivo Maven pom.xml. Extrairemos a versão da biblioteca para uma entrada de propriedade (para a versão mais recente das bibliotecas, consulte o https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org. deeplearning4j% 22 (repositório do Maven Central)):

<properties>
    <dl4j.version>0.9.1</dl4j.version>
</properties>

<dependencies>

    <dependency>
        <groupId>org.nd4j</groupId>
        <artifactId>nd4j-native-platform</artifactId>
        <version>${dl4j.version}</version>
    </dependency>

    <dependency>
        <groupId>org.deeplearning4j</groupId>
        <artifactId>deeplearning4j-core</artifactId>
        <version>${dl4j.version}</version>
    </dependency>
</dependencies>

Observe que a dependência nd4j-native-platform é uma das várias implementações disponíveis.

Ele conta com bibliotecas nativas disponíveis para muitas plataformas diferentes (macOS, Windows, Linux, Android, etc.). Também poderíamos mudar o backend para nd4j-cuda-8.0-platform, se quiséssemos executar cálculos em uma placa gráfica que suporta o modelo de programação CUDA.

===* 4. Preparando os dados *

====* 4.1 Preparando o arquivo DataSet *

Escreveremos o “Hello World” do aprendizado de máquina - classificação do conjunto de dados iris flower. Este é um conjunto de dados que foram coletados das flores de diferentes espécies (íris setosa, íris versicolor e íris virginica).

Essas espécies diferem em comprimentos e larguras de pétalas e sépalas. Seria difícil escrever um algoritmo preciso que classifique um item de dados de entrada (isto é, determina a que espécie pertence uma determinada flor). Mas uma rede neural bem treinada pode classificá-la rapidamente e com poucos erros.

Usaremos uma versão CSV desses dados, em que as colunas 0..3 contêm os diferentes recursos da espécie e a coluna 4 contém a classe do registro, ou a espécie, codificada com o valor 0, 1 ou 2:

5.1,3.5,1.4,0.2,0
4.9,3.0,1.4,0.2,0
4.7,3.2,1.3,0.2,0
…
7.0,3.2,4.7,1.4,1
6.4,3.2,4.5,1.5,1
6.9,3.1,4.9,1.5,1
…

====* 4.2 Vetorizando e lendo os dados *

Codificamos a classe com um número porque as redes neurais funcionam com números.* A transformação de itens de dados do mundo real em séries de números (vetores) é denominada vetorização * - deeplearning4j usa a datavec para fazer isso.

Primeiro, vamos usar esta biblioteca para inserir o arquivo com os dados vetorizados. Ao criar o CSVRecordReader, podemos especificar o número de linhas a serem puladas (por exemplo, se o arquivo tiver uma linha de cabeçalho) e o símbolo do separador (no nosso caso, uma vírgula):

try (RecordReader recordReader = new CSVRecordReader(0, ',')) {
    recordReader.initialize(new FileSplit(
      new ClassPathResource("iris.txt").getFile()));

   //…
}

Para iterar sobre os registros, podemos usar qualquer uma das múltiplas implementações da interface DataSetIterator. Os conjuntos de dados podem ser bastante grandes, e a capacidade de paginar ou armazenar em cache os valores pode ser útil.

Mas nosso pequeno conjunto de dados contém apenas 150 registros, então vamos ler todos os dados na memória de uma só vez com uma chamada de _iterator.next () _.

*Também especificamos o índice da coluna da classe* que, no nosso caso, é o mesmo que a contagem de recursos (4) *e o número total de classes* (3).

Além disso, observe que precisamos embaralhar o conjunto de dados para nos livrar da ordem das classes no arquivo original.

Especificamos uma semente aleatória constante (42) em vez da chamada padrão _System.currentTimeMillis () _ para que os resultados da reprodução aleatória sejam sempre os mesmos. Isso nos permite obter resultados estáveis ​​sempre que executarmos o programa:

DataSetIterator iterator = new RecordReaderDataSetIterator(
  recordReader, 150, FEATURES_COUNT, CLASSES_COUNT);
DataSet allData = iterator.next();
allData.shuffle(42);

4.3 Normalizando e dividindo

Outra coisa que devemos fazer com os dados antes do treinamento é normalizá-los. A normalização é um processo de duas fases:

  • coleta de algumas estatísticas sobre os dados (ajuste)

  • alterar (transformar) os dados de alguma forma para torná-los uniformes

    *A normalização pode ser diferente para diferentes tipos de dados.*

Por exemplo, se queremos processar imagens de vários tamanhos, devemos primeiro coletar as estatísticas de tamanho e depois escalar as imagens para um tamanho uniforme.

Mas para números, normalização geralmente significa transformá-los em uma distribuição normal. A classe NormalizerStandardize pode nos ajudar com isso:

DataNormalization normalizer = new NormalizerStandardize();
normalizer.fit(allData);
normalizer.transform(allData);

Agora que os dados estão preparados, precisamos dividir o conjunto em duas partes.

A primeira parte será usada em uma sessão de treinamento. Usaremos a segunda parte dos dados (que a rede não veria) para testar a rede treinada.

Isso nos permitiria verificar se a classificação funciona corretamente. Tomaremos 65% dos dados (0,65) para o treinamento e deixaremos o restante 35% para o teste:

SplitTestAndTrain testAndTrain = allData.splitTestAndTrain(0.65);
DataSet trainingData = testAndTrain.getTrain();
DataSet testData = testAndTrain.getTest();

*5. Preparando a configuração de rede *

====* 5.1. Construtor de configurações fluentes *

Agora podemos construir uma configuração de nossa rede com um construtor fluente sofisticado:

MultiLayerConfiguration configuration
  = new NeuralNetConfiguration.Builder()
    .iterations(1000)
    .activation(Activation.TANH)
    .weightInit(WeightInit.XAVIER)
    .learningRate(0.1)
    .regularization(true).l2(0.0001)
    .list()
    .layer(0, new DenseLayer.Builder().nIn(FEATURES_COUNT).nOut(3).build())
    .layer(1, new DenseLayer.Builder().nIn(3).nOut(3).build())
    .layer(2, new OutputLayer.Builder(
      LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .activation(Activation.SOFTMAX)
        .nIn(3).nOut(CLASSES_COUNT).build())
    .backprop(true).pretrain(false)
    .build();

Mesmo com essa maneira simplificada e fluente de criar um modelo de rede, há muito a digerir e muitos parâmetros a serem ajustados. Vamos quebrar esse modelo.

====* 5.2 Definindo parâmetros de rede *

  • O método do construtor _iterations () _ especifica o número de iterações de otimização.

A otimização iterativa significa executar várias passagens no conjunto de treinamento até a rede convergir para um bom resultado.

Normalmente, ao treinar conjuntos de dados reais e grandes, usamos várias épocas (passagens completas de dados pela rede) e uma iteração para cada época. Mas como nosso conjunto de dados inicial é mínimo, usaremos uma época e várias iterações.

  • A _activation () _ é uma função que é executada dentro de um nó para determinar sua saída.

A função de ativação mais simples seria linear f (x) = x. Mas acontece que apenas funções não lineares permitem que as redes resolvam tarefas complexas usando alguns nós.

Existem várias funções de ativação diferentes disponíveis, as quais podemos procurar no enum _org.nd4j.linalg.activations.Activation_en. Também poderíamos escrever nossa função de ativação, se necessário. Mas usaremos a função tangente hiperbólica fornecida (tanh).

*O método _weightInit () _ especifica uma das muitas maneiras de configurar os pesos iniciais para a rede.* Os pesos iniciais corretos podem afetar profundamente os resultados do treinamento. Sem entrar muito na matemática, vamos configurá-la para uma forma de distribuição gaussiana (_WeightInit.XAVIER_), pois essa geralmente é uma boa opção para começar.

Todos os outros métodos de inicialização de ponderação podem ser consultados no enum org.deeplearning4j.nn.weights.WeightInit.

*Taxa de aprendizado* é um parâmetro crucial que afeta profundamente a capacidade da rede de aprender.

Poderíamos gastar muito tempo ajustando esse parâmetro em um caso mais complexo. Mas, para nossa tarefa simples, usaremos um valor bastante significativo de 0,1 e o configuraremos com o método do construtor _learningRate () _.

*Um dos problemas com o treinamento de redes neurais é o caso de excesso de ajustes* quando uma rede “memoriza” os dados de treinamento.

Isso acontece quando a rede define pesos excessivamente altos para os dados de treinamento e produz resultados ruins em outros dados.

*Para resolver esse problema, vamos configurar a regularização l2* com a linha _.regularization (true) .l2 (0,0001) _. A regularização “penaliza” a rede por pesos muito grandes e evita o ajuste excessivo.

5.3. Criando camadas de rede

Em seguida, criamos uma rede de camadas densas (também conhecidas como totalmente conectadas).

A primeira camada deve conter a mesma quantidade de nós que as colunas nos dados de treinamento (4).

A segunda camada densa conterá três nós. Esse é o valor que podemos variar, mas o número de saídas na camada anterior deve ser o mesmo.

A camada de saída final deve conter o número de nós que correspondem ao número de classes (3). A estrutura da rede é mostrada na figura:

link:/wp-content/uploads/2017/10/Untitled-Diagram.png [imagem:/wp-content/uploads/2017/10/Untitled-Diagram-300x212.png [imagem, largura = 519, altura = 367] ]

Após um treinamento bem-sucedido, teremos uma rede que recebe quatro valores por meio de suas entradas e envia um sinal para uma de suas três saídas. Este é um classificador simples.

Por fim, para concluir a construção da rede, configuramos a propagação de retorno (um dos métodos de treinamento mais eficazes) e desativamos o pré-treinamento com a linha _.backprop (true) .pretrain (false) _.

*6. Criando e treinando uma rede *

Agora vamos criar uma rede neural a partir da configuração, inicialize e execute-a:

MultiLayerNetwork model = new MultiLayerNetwork(configuration);
model.init();
model.fit(trainingData);

Agora podemos testar o modelo treinado usando o restante do conjunto de dados e verificar os resultados com métricas de avaliação para três classes:

INDArray output = model.output(testData.getFeatureMatrix());
Evaluation eval = new Evaluation(3);
eval.eval(testData.getLabels(), output);

Se agora imprimirmos o _eval.stats () _, veremos que nossa rede é muito boa em classificar flores de íris, embora tenha confundido a classe 1 com a classe 2 três vezes.

Examples labeled as 0 classified by model as 0: 19 times
Examples labeled as 1 classified by model as 1: 16 times
Examples labeled as 1 classified by model as 2: 3 times
Examples labeled as 2 classified by model as 2: 15 times

==========================Scores========================================
# of classes: 3
Accuracy: 0.9434
Precision: 0.9444
Recall: 0.9474
F1 Score: 0.9411
Precision, recall & F1: macro-averaged (equally weighted avg. of 3 classes)
========================================================================

O construtor de configurações fluentes nos permite adicionar ou modificar camadas da rede rapidamente ou ajustar alguns outros parâmetros para ver se nosso modelo pode ser aprimorado.

===* 7. Conclusão*

Neste artigo, criamos uma rede neural simples, porém poderosa, usando a biblioteca deeplearning4j.

Como sempre, o código fonte do artigo está disponível over no GitHub.