Ensinando português ao GPT-2

O GPT-2 leu a Wikipédia em português e aprendeu a inventar pessoas, cidades e eventos históricos

Erick Fonseca
Ensina.AI
13 min readAug 20, 2019

--

O GPT-2 é uma rede neural desenvolvida pela OpenAI que ganhou bastante atenção tanto nos meios técnicos quanto na mídia, por causa de sua incrível capacidade de gerar textos que à primeira vista parecem escritos por humanos. Tecnicamente, é um modelo de língua, e eu escrevi recentemente um post em inglês revisando estes tipos de modelos.

A versão maior e mais poderosa do GPT-2 não foi disponibilizada para o público, mas outras duas menores foram. É interessante que podemos fazer o fine-tuning destes modelos, isto é, treiná-los mais um pouco com dados específicos que queiramos que eles aprendam. Ou seja, em vez de começar o treinamento do zero, podemos já começar a partir de um modelo já pré-treinado, e alcançar resultados melhores e mais rápidos.

Já há alguns exemplos muito interessantes de versões do GPT-2 especializadas em diferentes estilos de texto em inglês: há um subreddit exclusivo para bots conversarem, gerador de poesia, simuladores de chat, entre outros.

Mas… e quanto a outras línguas? Especificamente, será que dá para fazer o GPT-2 aprender português? A resposta simples é que sim! Neste post, vou explicar como fiz o fine-tuning do GPT-2 na Wikipedia em português e mostrar os resultados.

GPT-2 original

Inglês vs.outras línguas

O GPT-2 original foi treinado com um corpus de cerca de 40 GB de textos coletados da Internet, a grande maioria em inglês. O GPT-2 é agnóstico quanto a línguas — ele não faz distinção se está tratando de inglês, português ou árabe, simplesmente aprende a predizer palavras nos textos que vê.

Na verdade, o GPT-2 nem lida bem com palavras, mas com Byte-Pair Encodings, ou BPE. Resumidamente, é como se fosse um vocabulário composto por caracteres e sequências de caracteres mais comuns vistas nos textos. Algumas palavras pequenas ou muito comuns fazem parte deste vocabulário, enquanto outras são decompostas em partes menores, e tratadas como mais de um token. Isso traz muita flexibilidade, pois não se fica amarrado ao vocabulário de uma língua específica.

A implementação de BPE usada no GPT-2 tem alguns truques para dar conta de caracteres Unicode, de modo que caracteres especiais e alfabetos diferentes podem ser representados sem problemas. A questão é apenas ter dados suficientes para aprender representações para todos eles.

Custo computacional

O custo computacional para se treinar um modelo de língua estado-da-arte é enorme, chegando a horas de uso de várias GPUs ou TPUs em paralelo. Isso implica em um gasto de dezenas de milhares de dólares com equipamento e eletricidade, além de toda a emissão de carbono gerada.

Por isso, é fundamental que possamos reaproveitar ao máximo os modelos pré-treinados. A boa notícia é que o transfer learning (aproveitar um modelo de machine learning para inicializar outro) tem dado ótimos resultados em NLP, e o fine-tuning gasta uma quantidade de processamento minúscula em comparação com o treinamento inicial. Mais uma motivação para este experimento!

WikiWriter

Usar a Wikipedia como um corpus é comum em NLP. Ela tem uma grande quantidade de textos facilmente acessíveis em várias línguas, e tem um vocabulário bastante amplo, ainda que com um estilo de escrita muito enciclopédico. Mas tudo bem, vai ser interessante ver o nosso modelo, que vou chamar de WikiWriter, aprender a escrever neste estilo.

Obtendo os dados

Primeiramente, precisamos baixar um dump da Wikipédia (com acento, a versão em português!) na página de dumps do MediaWiki. Acessando o primeiro link (Database backup dumps) chegamos a uma listagem de diversas línguas e bases. Para o nosso experimento, estamos interessados no ptwiki mais recente, que nos leva a uma página com vários links de dados e metadados da Wikipédia em português.

Desta lista, eu baixei o primeiro, que se chama ptwiki-[DATA]-pages-articles-multistream.xml.bz2, com a data do dump. Trata-se de um arquivo compactado com 1,6 GB, e que descompactado tem cerca de 7,2 GB — estamos falando da Wikipédia lusófona inteira, mais de um milhão de artigos!

Pré-processamento

O dump é um XML cheio de metadados, e nós queremos apenas o texto corrido. Para extraí-lo, usei o WikiExtractor, um script que trata diversas particularidades destes arquivos.

O texto da Wikipedia contém vários códigos de template, que são usados para incluir nos artigos coisas como imagens, listas, tabelas, ou mesmo trechos de texto, como links ou a escrita fonética de uma palavra. O WikiExtractor, a princípio, consegue processar os templates para gerar um resultado bem próximo do que se vê na Wikipedia, mas fica extremamente lento. Tive então de desabilitar essa funcionalidade (com a flag --no_templates), e em poucos minutos toda a Wikipedia estava convertida em 15 arquivos JSON.

Mesmo assim, os textos ficam bastante bons. Os templates mais comuns servem apenas para gerar links — algo do tipo [[Astronomia]] escreve a palavra Astronomia no texto com um link para o artigo de mesmo nome — , e são tratado pelo WikiExtractor mesmo no modo no_templates. Um dos poucos problemas que notei foi a ocorrência de vários parênteses vazios, onde havia algum tipo de informação gerada por templates mais complexos, mas dá para corrigir manualmente.

Escrevi um script simples em Python para ler o conteúdo dos arquivos JSON, extrair o atributo text, remover pares de parênteses vazios e agregar tudo em um único arquivo de texto. O resultado foi um arquivo de "apenas" 1,5 GB.

Treino

O repositório oficial da OpenAI no GitHub contém apenas o código para rodar um modelo treinado, mas não para treinar um novo ou fazer fine-tuning de um existente. Não que seja tão difícil assim escrever um script para treino, especialmente para o segundo caso; tanto é que logo surgiu este fork com um código fácil de usar, e depois o gpt-2-simple, que simplifica ainda mais o processo.

A primeira coisa que o GPT-2 faz com o texto é tokenizá-lo (usando o BPE) e converter cada token em um código numérico. Esse processo é demorado para textos muito longos, e como podemos querer experimentar diferentes configurações com os mesmos dados, é bom tokenizar apenas uma vez e salvar o resultado intermediário. Para isso, usei o script encode.py do fork do nshepperd. No caso do meu texto da Wikipedia, o resultado foi um arquivo numpy com cerca de 780 MB.

Fluxograma da coleta de dados ao treino do WikiWriter.

Para usar o codificar os tokens, é necessário já ter o modelo pré-treinado do GPT-2! Ele pode ser baixado usando o gpt-2-simple ou um script da OpenAI. O nome do modelo que usei é 345M.

Com isso, temos tudo pronto para começar o treino de fato! Fiz a seguinte chamada ao gpt_2_simple:

Treinar um modelo neural deste tamanho leva muito tempo em CPU, e por isso é extremamente recomendado usar uma GPU. Em uma GeForce Titan Xp, o treino levou pouco mais de uma hora.

E assim surgiu o nosso WikiWriter, fine-tunado por 10 mil passos, com uma amostra gerada a cada 500. O --model_name especifica que queremos treinar a partir do modelo 345M, a maior versão do GPT-2 atualmente disponível. A cada 10 passos, o gpt_2_simple mostra a média móvel da função de perda (ou loss) do modelo, uma indicação do quão bem ele consegue gerar textos. Não vou entrar em detalhes, mas a loss do GPT-2 é calculada como a entropia cruzada de todo o vocabulário a cada token gerado. Eis o gráfico da evolução da loss:

Média móvel da loss do WikiWriter ao longo do treino

O pico no começo do treino é normal, já que pode haver uma certa variação no cálculo em pequenos batches (como é o caso aqui), mas os valores ficam mais consistentes ao longo de várias iterações. Podemos ver que a loss cai bastante até 2,5, a partir de onde fica mais difícil melhorar o WikiWriter.

Resultado

Enfim, vamos ver o que o WikiWriter aprendeu! Aqui estão alguns textos gerados:

Temos aí uma mistura curiosa. Segundo a Wikipedia em inglês, Armored Warfare é um jogo de videogame e PC, mas este artigo nem existe na Wikipedia em português — aliás, estas duas palavras não aparecem em lugar nenhum lá! Já a The Band existe mesmo, mas só surgiu em 1965. Uma pena que o WikiWriter não tenha sido muito criativo com nomes dos outros álbuns. Fora isso, há alguns erros que tornam o texto estranho, mas à primeira vista é impressionante como o WikiWriter aprendeu a estrutura geral das frases em português.

Existem algumas empresas chamadas Columbia-alguma-coisa (como Columbia Pictures), e existe apenas um pequeno povoado chamado Columbia em Maryland. Novamente, o WikiWriter não foi super criativo com os nomes, mas é interessante notar como ele alterna perfeitamente entre inglês e português para falar dos nomes das instituições e o texto em si. Temos uma inconsistência semântica (uma empresa foi fundada após já ter feito outras coisas!), mas o único deslize gramatical foi usar o verbo no presente no final do segundo parágrafo. Impressionante!

A História numa realidade paralela? Dessa vez, nenhum erro gramatical, apesar dos absurdos semânticos — a suposta confederação foi encerrada antes de sua fundação — , da sigla SBN e de um pouco de repetições. Dá pra imaginar que alguém cansado numa leitura rápida sem muita atenção acredite que foi escrito por um humano.

Mais uma sigla que não corresponde às iniciais, uma área muito pequena para uma cidade, alguns erros de concordância, mas um texto bastante fluido com absurdos que me fizeram rir.

E tem mais São Luís Freitas!

Na minha opinião, este foi o melhor gerado até agora, ao menos do ponto de vista cômico! Alguns erros ortográficos (police, vehículo e até run) parecem uma influência do pré-treino em inglês que não foi totalmente esquecido, mas que raramente vem à tona. Mas os absurdos lógicos praticamente não comprometem a gramaticalidade do texto, e é justamente essa fluência do WikiWriter junto com seu desconhecimento do mundo que tem a capacidade de tornar a biografia de um político imaginário inesperadamente hilária. A propósito: não existe nenhum São Luís Freitas nem Pedro Díaz de Oliveira na Wikipédia. Mais um ponto para a criatividade de nomes.

Uma página fake da Wikipedia escrita pelo WikiWriter

Conclusões

Eu criei o WikiWriter como uma forma de aprender na prática a usar o GPT-2 para transfer learning. E também, claro, para me divertir com o resultado. Estes objetivos foram alcançados sem muita dificuldade, mas vamos discutir um pouco mais.

Aplicações

Mas qual a utilidade de uma IA como o WikiWriter, além de escrever artigos surreais? Novamente, transfer learning. Redes neurais para diversas tarefas de NLP já se beneficiaram de aproveitar o GPT-2, BERT, ou outros modelos de língua pré-treinados. No entanto, estes modelos são normalmente treinados com textos majoritariamente em inglês, o que não é muito interessante quando trabalhamos com outras línguas.

Uma estratégia interessante é primeiro fazer o fine-tuning de um modelo de língua para um idioma novo (como o que fiz aqui), para depois aplicá-lo a outra tarefa específica neste idioma — como classificação de textos, análise de sentimentos ou detecção de entidades. Para ser sincero, ainda não conheço nenhum relato deste transfer learning em dois estágios, mas como esta é uma técnica um pouco recente em NLP e as aplicações para línguas sem ser o inglês costumam demorar para aparecer, me parece bastante razoável.

Qualidade

Como avaliar a qualidade dos textos gerados? Isso é bem subjetivo. Temos a função de loss, que mede basicamente o quão distante o modelo estava de predizer as palavras corretas durante seu treino. Mas ao gerar textos, nem sempre existe a palavra correta. Há muitas possibilidades que um gerador pode explorar, e avaliar isso automaticamente é um problema em aberto em NLP.

Como deu pra ver nos exemplos, o WikiWriter comete alguns erros de gramática e ortografia, além de produzir absurdos lógicos. Mas ao mesmo tempo, o estilo de escrita é bastante coerente com a Wikipédia. Eu diria que os textos acima são muito bons.

Mas as amostras que eu mostrei aqui foram escolhidas manualmente por serem as melhores que encontrei. Na verdade, o WikiWriter também produziu artigos bastante sem graça, cheios de repetição, às vezes com a mesma frase repetida mais de dez vezes. Então, na média, já não é nenhuma maravilha, e dificilmente se passaria por humano.

Em comparação com textos gerados em inglês após o fine-tuning do GPT-2 345M (como por exemplo o subreddit SubSimulatorGPT2), descontado o problema da repetição, me parece que a diferença de qualidade é bem pequena. Considerando que o WikiWriter deve ter visto muito pouco texto em português em seu pré-treino, isso parece excelente. Aliás, no subreddit, os posts dos bots costumam ser bem curtos, o que diminui bastante o problema de repetição. Se olharmos só para a primeira ou segunda frase gerada pelo WikiWriter, temos uma comparação mais justa.

Naturalmente, se tivéssemos acesso a um modelo de língua pré-treinado em textos em português, ou mesmo uma versão maior do GPT-2, já teríamos melhoras no texto gerado. Mas uma busca mais cuidadosa por hiperparâmetros na fase de fine-tuning, especialmente a taxa de aprendizado, também poderia dar melhores resultados. Eu não quis dedicar tempo demais a este experimento, mas seria uma coisa natural a se fazer se quiséssemos o melhor gerador possível.

Amostras

Aqui está uma amostra com 30 rodadas do modelo para gerar artigos. Cada rodada é separada por uma sequência de =====, e o <|endoftext|> indica o fim de um artigo. Veja e tire suas próprias conclusões!

Modelo Treinado

(atualizado 22/01/2020)

Agora o modelo treinado está disponível aqui! Para usá-lo, siga as instruções do gpt_2_simple.

--

--

Erick Fonseca
Ensina.AI

Data Scientist at Kaufland, Germany. Doing Natural Language Processing stuff.