[PT-BR] Classificando textos com Redes Neurais e TensorFlow

Todo mundo diz que para aprender Machine Learning você precisa saber bem como os algoritmos funcionam internamente. Eu concordo, mas também acho que começar a aprender como eles funcionam sem ter uma visão geral sobre a aplicação deles torna o aprendizado bem mais difícil.

Com uma visão geral da utilização dos algoritmos, o processo de aprender como eles funcionam se torna bem mais fácil.

Tá, e como a gente pode desenvolver essa intuição então? Um bom jeito é simplesmente utilizando os algoritmos. Ou seja, criando modelos de predição. Assumindo que a gente ainda não sabe como criar os algoritmos do zero, vamos precisar utilizar alguma biblioteca que possua as implementações. É aí onde chegamos no TensorFlow.

Nesse artigo você vai criar um modelo de aprendizagem de máquina capaz de classificar textos em categorias. Os tópicos que vamos abordar são esses:

  1. Como o TensorFlow funciona
  2. O que é um modelo de Aprendizagem de Máquina
  3. O que é uma Rede Neural
  4. Como uma Rede Neural aprende
  5. Como manipular os dados e passar para as entradas da Rede Neural
  6. Como rodar o modelo de predição e obter os resultados

Você provavelmente vai aprender várias coisas novas, então vamos começar logo! 😀

TensorFlow

O TensorFlow é uma biblioteca para tarefas de aprendizagem de máquina. Ela é open source e foi inicialmente desenvolvida pelo Google. O nome da biblioteca ajuda a entender a forma de se trabalhar com ela: tensores são arrays multidimensionais, que vão fluindo pelos nós de um grafo.

tf.Graph

Todas as computações no TensorFlow são representadas como um dataflow graph. Esse grafo é composto por dois elementos:

Por exemplo, vamos criar o seguinte dataflow graph:

Um dataflow graph, que realiza a operação x + y

Vamos definir que x = [1,3,6] e y = [1,1,1]. Como o dataflow graph trabalha com tf.Tensor, você precisa criá-los dessa forma:

  import tensorflow as tf
  x = tf.constant([1,3,6])
  y = tf.constant([1,1,1])

Agora você vai definir a adição, que e no nosso caso é a de adição:

  import tensorflow as tf

  x = tf.constant([1,3,6])
  y = tf.constant([1,1,1])

  op = tf.add(x,y)

Você tem todos os elementos do grafo, agora precisa montá-lo. Ao rodar esse código o TensorFlow já registraria um grafo default automaticamente, mas vamos criar um grafo na mão para entender como tudo isso funciona:

  import tensorflow as tf

  my_graph = tf.Graph()

  with my_graph.as_default():
      x = tf.constant([1,3,6])
      y = tf.constant([1,1,1])

      op = tf.add(x,y)

O ciclo de trabalho no TensowFlow é esse, primeiro definimos o grafo e só depois realizamos as computações (‘rodar’ a operação de cada nó do grafo). Para isso precisamos criar uma tf.Session.

tf.Session

Uma tf.Session encapsula o ambiente onde as operações do grafo são executadas e os tensors são avaliados. Para isso a gente precisa definir qual o grafo que vai ser usado na sessão.

  import tensorflow as tf

  my_graph = tf.Graph()

  with tf.Session(graph=my_graph) as sess: #aqui
      x = tf.constant([1,3,6])
      y = tf.constant([1,1,1])

      op = tf.add(x,y)

Para executar as operações você vai usar o método tf.Session.run(). Esse método executa um ‘passo’ da computação do grafo. Definimos o que queremos rodar através do argumento fetches. Ele pode ser um elemento do grafo, uma lista arbitrária, um dicionário, etc. No nosso caso vamos rodar um passo da nossa adição:

  import tensorflow as tf

  my_graph = tf.Graph()

  with tf.Session(graph=my_graph) as sess:
      x = tf.constant([1,3,6])
      y = tf.constant([1,1,1])

      op = tf.add(x,y)

      result = sess.run(fetches=op)
      print(result)

  >>> [2 4 7]

Modelo de Predição

Agora que a gente já entendeu como o TensorFlow funciona, vamos ver como é criado um modelo de predição. Basicamente temos que:

Algoritmo de aprendizagem de máquina + dados = modelo de predição

O processo para a criação do modelo é mais ou menos assim:

O processo para a criação de um modelo de predição

Note que o modelo é composto do algoritmo de aprendizagem de máquina “treinado” com os dados que você forneceu. Quando tivermos o modelo criado, usamos os dados na entrada para obter os resultados.

Workflow do processo de predição

O objetivo do modelo que você irá construir é categorizar textos, então teremos que:

entrada: texto, saída: categoria do texto

Temos um conjunto de treinamento com todos os textos e as labels (cada texto tem uma label indicando a que categoria ele pertence). Em Aprendizagem de Máquina esse tipo de tarefa é chamada de Aprendizagem supervisionada.

Além de ser uma tarefa supervisionada (vamos ensinar o algoritmo, corrigindo quando ele cometer erros) essa também é uma tarefa de Classificação (vamos classificar os textos em classes).

Para criar o modelo vamos utilizar Redes Neurais.


A rede neural

Uma Rede Neural é um modelo computacional (ou seja, uma forma de descrever um sistema utilizando linguagem e conceitos matemáticos). Esse modelo é capaz de aprender e ser treinado sozinho.

As Redes Neurais são inspiradas no sistema nervoso central do nosso cérebro. Ela possui nós conectados que parecem muito com os neurônios.

Uma rede neural

O Perceptron foi o primeiro algoritmo de Rede Neural criado. Esse site aqui explica muito bem como o perceptron funciona internamente (a animação do “Inside an artificial neuron” é maravilhosa).

Para entender como uma rede neural funciona internamente vamos usar como exemplo de arquitetura a própria rede neural que você irá construir no TensorFlow. Essa arquitetura foi usada por Aymeric Damien nesse exemplo.

Arquitetura da rede neural

A Rede Neural que você irá criar possui 2 hidden layers (essa escolha de quantas hidden layers a Rede Neural terá faz parte do processo de criar a arquitetura da rede). O trabalho de cada hidden layer é transformar a entrada para algo que a próxima layer possa usá-la.

Hidden layer 1

Entrada e hidden layer 1

Você vai definir quantos nós a hidden layer 1 terá. Esses nós também são chamados de features ou neurônios, que na imagem acima são representados pelos círculos.

Na camada de entrada (input layer) cada nó corresponde a uma palavra do conjunto de dados (vamos ver como isso funciona daqui a pouco).

Como explicado aqui , cada neurônio é multiplicado por um peso (weight). Cada unidade possui um peso, e na etapa de treinamento a rede neural ajusta o valor de cada peso para que a saída seja correta (calma, vamos ver melhor essa parte depois).

Além de multiplicar cada nó da entrada por um peso, a rede também adiciona um bias (role of bias in neural networks).

Na sua arquitetura, depois de multiplicar as entradas pelos pesos e adicionar o bias, a primeira hidden layer também passa por uma função de ativação. Essa função de ativação é quem define a saída final de cada nó (analogia: imagine que cada nó é uma lâmpada, a função de ativação é quem diz se essa lâmpada irá acender ou não).

Existem várias funções de ativação, você irá usar a rectified linear unit (ReLu). Ela é definida por:

*f(x) = max(0,x) *[a saída é o valor de x ou 0 (zero), depende de quem for maior]

Exemplos: se x = -1, então f(x) = 0(zero); se *x = 0.7, então *f(x) = 0.7.

Hidden layer 2

A segunda hidden layer faz exatamente o que a primeira hidden layer fez, só que agora a entrada é a hidden layer 1.

Hidden layer 1 e hidden layer 2

Output layer

E finalmente chegamos na última camada, a output layer. Vamos usar a codificação one-hot, onde só um bit tem o valor de 1 e os outros tem valor zero. Por exemplo, se quisermos representar 3 categorias (sports, space e computer graphics):

    +-------------------+-----------+
    |    category       |   value   |
    +-------------------|-----------+
    |      sports       |    001    |
    |      space        |    010    |
    | computer graphics |    100    |
    |-------------------|-----------|

Assim, a quantidade de nós da saída da rede neural vai ser igual a quantidade de classes do conjunto de entrada.

A saída também multiplica os valores da entrada (no caso a entrada agora é a hidden layer 2) pelos pesos e somados ao bias. Mas a função de ativação agora é diferente.

Você quer identificar a que categoria pertence cada texto, e essas categorias são mutuamente exclusivas (um texto não pertence à categoria esportes e à categoria economia, por exemplo). Para isso em vez da ReLu você irá utilizar a função softmax. Essa função transforma a saída de cada unidade para um valor entre 0 e 1 e também divide o valor de cada entrada de forma que a soma total das entradas seja igual a 1. Assim o valor da saída nos diz a probabilidade do texto pertencer a cada uma das classes.

    | 1.2                    0.46|
    | 0.9   -> [softmax] ->  0.34|
    | 0.4                    0.20|

Depois de todas essas etapas você finalmente montou o dataflow graph da rede neural. Transcrevendo tudo isso que nós vimos para código o resultado é esse:

  # Network Parameters
  n_hidden_1 = 10        # 1st layer number of features
  n_hidden_2 = 5         # 2nd layer number of features
  n_input = total_words  # Words in vocab
  n_classes = 3          # Categories: graphics, space and baseball

  def multilayer_perceptron(input_tensor, weights, biases):
    layer_1_multiplication = tf.matmul(input_tensor, weights['h1'])
    layer_1_addition = tf.add(layer_1_multiplication, biases['b1'])
    layer_1_activation = tf.nn.relu(layer_1_addition)

  # Hidden layer with RELU activation
    layer_2_multiplication = tf.matmul(layer_1_activation, weights['h2'])
    layer_2_addition = tf.add(layer_2_multiplication, biases['b2'])
    layer_2_activation = tf.nn.relu(layer_2_addition)

  # Output layer with linear activation
    out_layer_multiplication = tf.matmul(layer_2_activation, weights['out'])
    out_layer_addition = out_layer_multiplication + biases['out']

  return out_layer_addition

(vamos falar sobre o código para a função de ativação da output layer depois)

Como a rede neural aprende

A gente comentou que os pesos (weights) vão sendo atualizados enquanto a rede vai sendo treinada. Vamos ver como essa atualização acontece no TensorFlow.

tf.Variable

Os pesos e os bias são armazenados em variáveis (tf.Variable). Essas variáveis mantém o estado a cada chamada do método run(). Normalmente o que se faz é iniciar os valores dos weights e do bias através de uma distribuição normal.

    weights = {
        'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),
        'h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),
        'out': tf.Variable(tf.random_normal([n_hidden_2, n_classes]))
    }
    biases = {
        'b1': tf.Variable(tf.random_normal([n_hidden_1])),
        'b2': tf.Variable(tf.random_normal([n_hidden_2])),
        'out': tf.Variable(tf.random_normal([n_classes]))
    }

Ao rodar o algoritmo pela primeira vez (com os pesos definidos pela distribuição normal):

    valores de entrada: x
    weights: w
    bias: b
    saida da rede: z

    saida esperada: expected

Para saber se a rede neural está aprendendo ou não, você precisa comparar o valor da saída (z) com o valor esperado (expected). E como a gente computa essa diferença (loss)? Existem vários métodos para isso. Como estamos trabalhando com um problema de classificação e as classes são mutuamente exclusivas, a melhor métrica é medir o cross-entropy error.

James D. McCaffrey escreveu uma explicação brilhante sobre porque esse é o melhor método para esse tipo de problema.

No TensorFlow vamos computar o cross-entropy error com o método tf.nn.softmax_cross_entroy_with_logits(), e usar como loss a média dos erros de cada unidade (tf.reduced_mean()):

  # Construct model
  prediction = multilayer_perceptron(input_tensor, weights, biases)

  # Define loss
  entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=output_tensor)
  loss = tf.reduce_mean(entropy_loss)

O você quer é encontrar o melhor valor para os weights e o bias, de forma que eles minimizem o erro da saída (a diferença entre o valor que a gente obteve (z) e o valor correto (expected). Para isso se utiliza o método do gradiente, mais especificamente o método do gradiente descendente estocástico.

Gradiente descendente. Fonte: https://sebastianraschka.com/faq/docs/closed-form-vs-gd.html

Também existem vários algoritmos para computar o gradiente descendente, vamos utilizar o Adaptive Moment Estimation (Adam). Você precisa passar um valor para a learning rate, que determina quão rápido ou devagar vão ser os saltos para encontrar o valor ótimo dos weights.

O método tf.train.AdamOptimizer(learning_rate).minimize(loss)** **é um açúcar sintático que realiza duas coisas:

  1. compute_gradients(loss, )
  2. apply_gradients()

Dessa forma ele já atualiza todas as tf.Variables com os novos valores para os weights, então você não precisa passar a lista de variáveis. E finalmente você terminou a fase de treinamento da rede neural:

  learning_rate = 0.001

  # Construct model
  prediction = multilayer_perceptron(input_tensor, weights, biases)

  # Define loss
  entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=output_tensor)
  loss = tf.reduce_mean(entropy_loss)

  optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)

Data manipulation

O dataset que você irá utilizar é composto de vários textos em Inglês, e a gente precisa tratar esses dados para passar pra a rede neural. Você vai fazer o seguinte:

  1. Converter cada palavra dos textos para um número (index)
  2. Criar uma matriz para cada texto, onde os valores são 1 caso a palavra esteja presente no texto e 0 caso ela não esteja.
    import numpy as np    #numpy is a package for scientific computing
    from collections import Counter

    vocab = Counter()

    text = "Hi from Brazil"

    #Get all words
    for word in text.split(' '):
        word_lowercase = word.lower()
        vocab[word_lowercase]+=1

    #Convert words to indexes
    def get_word_2_index(vocab):
        word2index = {}
        for i,word in enumerate(vocab):
            word2index[word] = i

        return word2index

    #Now we have an index
    word2index = get_word_2_index(vocab)

    total_words = len(vocab)

    #This is how we create a numpy array (our matrix)
    matrix = np.zeros((total_words),dtype=float)

    #Now we fill the values
    for word in text.split():
        matrix[word2index[word]] += 1

    print(matrix)

    >>> [ 1.  1.  1.]

Por exemplo, se nosso texto fosse apenas “Hi”:

    matrix = np.zeros((total_words),dtype=float)

    text = "Hi"

    for word in text.split():
        matrix[word2index[word.lower()]] += 1

    print(matrix)

    >>> [ 1.  0.  0.]

Fazemos algo parecido para converter os valores das categorias, só que convertemos para o padrão one-hot:

  y = np.zeros((3),dtype=float)

  if category == 0:
    y[0] = 1.        # [ 1.  0.  0.]
  elif category == 1:
    y[1] = 1.        # [ 0.  1.  0.]
  else:
    y[2] = 1.       # [ 0.  0.  1.]

Rodando o Graph e vendo o resultado da rede

E agora chegou a melhor parte: rodar a rede com todo o conjunto de dados e testar o modelo final com os dados de teste.

O dataset de entrada

Você vai utilizar o 20 newsgroups, um conjunto de dados com cerca de 18.000 posts sobre 20 tópicos. Vamos carregar os dados através do scikit-learn. Só vamos utilizar 3 categorias: comp.graphics, sci.space, e rec.sport.baseball. O scikit-learn tem os dados em dois subconjuntos: um para treinamento e um para testes. A recomendação é que você nunca deve olhar os dados de testes, porque isso pode influenciar quando você for fazer escolhas durante a construção do modelo. Você quer criar um modelo com uma boa generalização, não um modelo que faça predições corretas especificamente para esse conjunto de testes.

Você irá carregar os dados assim:

  categories = ["comp.graphics","sci.space","rec.sport.baseball"]

  newsgroups_train = fetch_20newsgroups(subset='train', categories=categories)
  newsgroups_test = fetch_20newsgroups(subset='test', categories=categories)

Treinando o modelo

Na terminologia das redes neurais a gente chama de época (epoch) um forward pass (computar os valores das saídas) e um backward pass (atualizar os pesos) por todo o conjunto de treinamento.

Lembra do tf.Session.run()? Vamos ver melhor como ele funciona:

tf.Session.run(fetches, feed_dict=None, options=None, run_metadata=None)

No exemplo de somar as matrizes você passou só a operação de soma, mas esse parâmetro também pode ser um lista, uma tupla ou um dicionário com elementos de um graph. No seu caso você vai precisar executar duas coisas: calcular o valor da loss e chamar o optimizer para atualizar os pesos.

O parâmetro feed_dict é onde você passa os dados para cada step do método run. Para poder passar esses dados você precisa definir tf.placeholders (para alimentar ofeed_dict).

A placeholder exists solely to serve as the target of feeds. It is not initialized and contains no data. — Source

Você vai definir os placeholders assim:

  n_input = total_words # Words in vocab
  n_classes = 3         # Categories: graphics, sci.space and baseball

  input_tensor = tf.placeholder(tf.float32,[None, n_input],name="input")
  output_tensor = tf.placeholder(tf.float32,[None, n_classes],name="output")

Você vai precisar separar o conjunto de treinamento, em batches:

If you use placeholders for feeding input, you can specify a variable batch dimension by creating the placeholder with tf.placeholder(…, shape=[None, …]). The None element of the shape corresponds to a variable-sized dimension. — Source

Na fase de teste você vai usar um batch maior do que o usado na fase de treinamento, então vamos definir o batch do placeholder com um tamanho variável ([None, …]).

A função get_batches() retorna os textos separados pelo tamanho de cada batch.

  training_epochs = 10

  # Launch the graph
  with tf.Session() as sess:
      sess.run(init) #inits the variables (normal distribution, remember?)

  # Training cycle
      for epoch in range(training_epochs):
          avg_cost = 0.
          total_batch = int(len(newsgroups_train.data)/batch_size)
          # Loop over all batches
          for i in range(total_batch):
              batch_x,batch_y = get_batch(newsgroups_train,i,batch_size)
              # Run optimization op (backprop) and cost op (to get loss value)
              c,_ = sess.run([loss,optimizer], feed_dict={input_tensor: batch_x, output_tensor:batch_y})

E finalmente, para testar o se modelo você também vai criar um graph. Vamos medir a precisão, para isso você vai pegar o índice do valor da predição e o índice do valor correto (one-hot encoding), ver se eles são iguais e calcular a média de acertos para todo o conjunto de teste.

    # Test model
    index_prediction = tf.argmax(prediction, 1)
    index_correct = tf.argmax(output_tensor, 1)
    correct_prediction = tf.equal(index_prediction, index_correct)

    # Calculate accuracy
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
    total_test_data = len(newsgroups_test.target)
    batch_x_test,batch_y_test = get_batch(newsgroups_test,0,total_test_data)
    print("Accuracy:", accuracy.eval({input_tensor: batch_x_test, output_tensor: batch_y_test}))

>>> Epoch: 0001 loss= 1133.908114347
    Epoch: 0002 loss= 329.093700409
    Epoch: 0003 loss= 111.876660109
    Epoch: 0004 loss= 72.552971845
    Epoch: 0005 loss= 16.673050320
    Epoch: 0006 loss= 16.481995190
    Epoch: 0007 loss= 4.848220565
    Epoch: 0008 loss= 0.759822878
    Epoch: 0009 loss= 0.000000000
    Epoch: 0010 loss= 0.079848485
    Optimization Finished

    Accuracy: 0.75

E pronto, você acabou de criar um modelo de predição utilizando redes neurais para classificar textos em categorias. Parabéns! 😄

Você pode ver o notebook com todo o código aqui.

Uma dica é ir mudando todos os valores definidos e ver como eles afetam o tempo de treinamento e a precisão do modelo.

Dúvidas, erros ou sugestões? Pode escrever nos comentários. Ah, e obrigada pela leitura! 😄 ✌🏽