Usando Machine Learning para criar textos com TensorFlow

Dê uma olhada na letra dessa música funk abaixo:

rebola
até o assunto é amor
capricha
chega perto de mansin
carinho meu bem
sorte ver você
lo lo lo
lo lo lo
beijo e poder
lo lo lo
lo lo lo
vontade
lo lo lo
lo lo lo
parei
você do valor
essa música certo
por que sim

Isso foi gerado por um computador depois de ter aprendido com 1973 palavras de 64 músicas da Anitta (ok, eu poderia ter usado Capital Inicial, Chico Buarque ou qualquer outra banda/compositor/estilo musical, mas no meu julgamento, as letras de funk tem uma simplicidade extra =P).

No artigo anterior tem descrito de maneira simples como funciona Machine Learning. Criamos uma rede neural que aprendeu a converter graus Celsius em Fahrenheit usando somente dados. Fizemos isso passando para a rede neural alguns valores e falando "-10 graus Celsius = 14 graus Fahrenheit", ....

celsius = [-10.0, -9.0, -8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
fahrenheit = [14.0, 15.8, 17.6, 19.4, 21.2, 23.0, 24.8, 26.6, 28.4, 30.2, 32.0, 33.8, 35.6, 37.4, 39.2, 41.0, 42.8, 44.6, 46.4, 48.2, 50.0]

Para criar o gerador de texto que fez a música acima, a teoria é exatamente a mesma: passamos para a rede neural qual é a relação de uma palavra com a outra e dessa forma ela aprende quais palavras pode-se utilizar após uma outra palavra.

Vamos exemplificar usando a frase abaixo: O céu é azul

Vamos ensinar a rede neural que após a palavra "O", ela pode usar a palavra "céu"; que depois das palavras "O céu", pode-se usar a palavra "é"; depois das palavras "O céu é", pode-se usar a palavra "azul":

  • O -> céu
  • O céu -> é
  • O céu é -> azul

Uma segunda frase para exemplificar: O azul do oceano é lindo.

Vamos ensinar a rede neural que após a palavra "O", pode-se usar a palavra "azul"; que depois de "O azul", pode-se usar "do", e assim por diante:

  • O -> azul
  • O azul -> do
  • O azul do -> oceano
  • O azul do oceano -> é
  • O azul do oceano é -> lindo

Imagine fazer isso com dez mil frases com centenas de palavras diferentes. Conseguimos ensinar a rede neural quais são as possíveis palavras a serem utilizadas após uma sequência de palavras! E dessa forma ela é capaz de produzir um texto (de certa forma) coerente.

Só que é muito mais fácil para a rede neural processar números do que palavras, então precisamos criar um dicionário em que cada palavra é um número único, depois converter cada frase em sequências de números correspondentes ao dicionário e criar sequências com a "resposta" baseado no próximo número da sequência. O passo a passo é:

  1. Converter cada palavra em um número
  2. Converter cada frase em uma sequência de números
  3. Fazer com que, a partir da 2a palavra da frase, a próxima palavra seja a resposta do início da frase
  4. Garantir que cada nova frase tenha o mesmo número de caracteres
  5. Treinar nosso modelo
  6. Pedir para a rede neural nos dar as próximas X palavras após uma frase

Implementação

O código abaixo faz todos os passos! É um código bem maior do que o do primeiro artigo, mas não se assuste! A maior parte do código é trabalhando com as palavras! Após o código, vamos pedaço por pedaço explicar o que acontece:

from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout, Bidirectional
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
from tensorflow.keras import regularizers
import tensorflow.keras.utils as ku 
import tensorflow as tf
import numpy as np 

tokenizer = Tokenizer()
data = open('disc-songs-pt/all-formatted.txt').read()

### PASSO 1

corpus = data.lower().split("\n")
tokenizer.fit_on_texts(corpus)
total_words = len(tokenizer.word_index) + 1

### PASSO 2 e PASSO 3

input_sequences = []
for line in corpus:
    # convert each line into a sequence of tokens
    token_list = tokenizer.texts_to_sequences([line])[0]
    # convert each sequence of tokens into chunks of sequences ([1,2,3,4] -> [1,2] [1,2,3] [1,2,3,4])
    for i in range(1, len(token_list)):
        n_gram_sequence = token_list[:i+1]
        input_sequences.append(n_gram_sequence)

### PASSO 4

max_sequence_len = max([len(x) for x in input_sequences])
input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))

# create predictors and label (last token from each sequence)
predictors, label = input_sequences[:,:-1],input_sequences[:,-1]

# convert each label to a category
label = ku.to_categorical(label, num_classes=total_words)


### PASSO 5

model = Sequential()
model.add(Embedding(total_words, 100, input_length=max_sequence_len-1))
model.add(LSTM(100))
model.add(Dense(total_words, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(predictors, label, epochs=100, verbose=1)


### PASSO 6

seed_text = "rebola"
next_words = 100

for _ in range(next_words):
    token_list = tokenizer.texts_to_sequences([seed_text])[0]
    token_list = pad_sequences([token_list], maxlen=max_sequence_len-1, padding='pre')
    predicted = model.predict_classes(token_list, verbose=0)
    output_word = ""
    for word, index in tokenizer.word_index.items():
        if index == predicted:
            output_word = word
            break
    seed_text += " " + output_word
print(seed_text)

Passo 1

tokenizer = Tokenizer()
data = open('disc-songs-pt/all-formatted.txt').read()
corpus = data.lower().split("\n")
tokenizer.fit_on_texts(corpus)
  • 'disc-songs-pt/all-formatted.txt' é um arquivo de texto que contém todas a músicas da Anitta que foram utilizadas. Após cada frase tem uma nova linha \n. O split("\n") lê esse caracter e converte cada frase em um item de uma lista (array)
  • tokenizer.fit_on_texts(corpus) faz a mágica de converter cada palavra em um número (token)

Passos 2 e 3

input_sequences = []
for line in corpus:
    # convert each line into a sequence of tokens
    token_list = tokenizer.texts_to_sequences([line])[0]
    # convert each sequence of tokens into chunks of sequences ([1,2,3,4] -> [1,2] [1,2,3] [1,2,3,4])
    for i in range(1, len(token_list)):
        n_gram_sequence = token_list[:i+1]
        input_sequences.append(n_gram_sequence)

Não tem muito segredo nessa parte do código. É um loop em cada item da lista. A cada loop:

  • token_list = tokenizer.texts_to_sequences([line])[0] converterá a frase em uma sequência de dígitos, utilizando o dicionário criado no passo 1 (através do método fit_on_texts)
  • no segundo loop for i in range(1, len(token_list)): pegará o próximo número da sequência (que começa no 2o item) e adicionará na lista input_sequences (o input_sequences é a "resposta", o "Y" da nossa função)

Passo 4

max_sequence_len = max([len(x) for x in input_sequences])
input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))

# create predictors and label (last token from each sequence)
predictors, label = input_sequences[:,:-1], input_sequences[:,-1]

# convert each label to a category
label = ku.to_categorical(label, num_classes=total_words)
  • input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre')) adicionará 0 em cada sequência, para que todas as sequências tenham o mesmo tamanho. O parâmetro padding='pre' indica que é para adicionar no início (pre) da sequência
  • predictors = input_sequences[:,:-1] -> cria uma lista usando todos menos o último elemento de cada item (ex: [1, 2, 3] -> [1, 2])
  • label = input_sequences[:,-1] -> cria uma lista usando o último item de cada item da lista (ex: [1,2,3] -> [3])
  • ku.to_categorical(label, num_classes=total_words) -> converte cada item em uma categoria (sim! usaremos um classificador para gerar texto)

Passo 5

model = Sequential()
model.add(Embedding(total_words, 100, input_length=max_sequence_len-1))
model.add(LSTM(100))
model.add(Dense(total_words, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(predictors, label, epochs=100, verbose=1)
  • Sequential() -> igual ao primeiro artigo: agrupa as camadas de neurônios de forma sequencial (porém no exemplo, só temos um)
  • Embedding(...) -> cria um vetor em que cada palavra terá diferentes pesos que serão ajustados em cada treino e isso faz com que palavras semelhantes fiquem agrupadas.
  • LSTM() -> Long-short-term-memory. Faz com que o que foi aprendido em neurônios anterios seja carregado nos próximos para que a informação continue relevante.
  • Dense() -> neurônio final que terá todas as categorias criadas (palavras) e que será usado para prever qual é, estatisticamente, a melhor palavra a ser utilizada

Passo 6

seed_text = "rebola"
next_words = 100

for _ in range(next_words):
    token_list = tokenizer.texts_to_sequences([seed_text])[0]
    token_list = pad_sequences([token_list], maxlen=max_sequence_len-1, padding='pre')
    predicted = model.predict_classes(token_list, verbose=0)
    output_word = ""
    for word, index in tokenizer.word_index.items():
        if index == predicted:
            output_word = word
            break
    seed_text += " " + output_word
print(seed_text)

Aqui é aonde pedimos para a nossa rede neural criar o texto. O que acontece na verdade é: usando uma palavra (ou frase) inicial, pedimos para a rede neural identificar qual é a próxima palavra a ser usada. Então, usando um loop, vamos adicionando a palavra sugerida pela rede neural a nossa frase e então pedindo para sugerir uma outra palavra até termos completado a quantidade de palavras que pedimos.

Links

Ufa... esse foi um artigo mais intenso, certo? Tem muita informação aqui que podemos trabalhar e detalhar em artigos futuros! Espero que tenha ficado claro, ou pelo menos ter passado uma leve ideia de, como treinar redes neurais para produzir textos.