Fotos de Gabriel Santos and Lisa Fotios

Como criar um detector de sotaques de português com palestras TED

Raspei 5000 palestras TED e criei um classificador de sotaques portugueses em scikit-learn. Depois mexi com Flask e Vue.js para compartilhá-lo com vocês

Marek K. Cichy
Published in
9 min readNov 2, 2019

--

Click here for the English version.

Sim, sou um membro deste rebanho cada vez maior que recentemente decidiu surfar a onda da Data Science. Aprenda fazendo, disse a comundade. Crie um problema e busque a solução, disseram eles. Não tenha medo de quebrar as coisas no caminho. Estou tentando seguir esta filosofia. Como tradutor de polonês<->português na vida passada, imediatamente me ocorreu um problema do mundo real:

“Pode um modelo mirim (mas bacana) de aprendizagem automática reconhecer os sotaques de português?” 98.81% de confiança

Como podem imaginar, o mercado freela para os tradutores de português na Polônia é bastante restrito. Não tive o luxo de me especializar e me ater a apenas um dos sotaques. No papel duplo de tradutor de PT-PT e PT-BR, às vezes na mesma semana de trabalho, tive que ter o dobro de cuidado em manter a consistência numa determinada tradução. Por isso, uma das minhas primeiras ideias para um projeto de NLP foi um classificador de sotaques portugueses.

Não se trata de uma pesquisa inovadora que abre novos caminhos na PNL nem na IA. Minha motivação aqui é semelhante à do artigo anterior do Medium sobre meu Tweetbot (inglês) — mostrar como é simples dar os primeiros passos no processamento de linguagem natural em Python. Como tenho pouca formação técnica, tento apresentá-lo da maneira mais clara possível.

O artigo se divide em três partes principais:

  • Preparação de dados de origem (Scrapy);
  • Treinamento do modelo (scikit-learn);
  • Implementação duma API com o modelo (Flask + Vue.js).

Preparando dados de origem

Uma palestra. Possivelmente, uma de TED. Foto de Samuel Pereira em Unsplash

Que dados necessitava, de onde colhé-los e como? Era preciso buscar textos já rotulados como português europeu ou brasileiro. No caso, escolhi a TED.com como minha fonte. É um repositório razoavelmente grande de palestras transcritas. O que é particularmente importante nesse caso, a equipe de tradução da TED mantém uma divisão estrita entre os dois variantes de português (o que não é o caso da Wikipédia, por exemplo).

A TED costumava manter uma API oficial, mas deixou de disponibilizar a mesma em 2016. Por isso, aproveitei da biblioteca Scrapy e montei uma crawler para raspar os dados. A maior parte do código é baseada no tutorial da Scrapy. O crawler percorre o catálogo de palestras do TED na língua especificada e segue links para palestras individuais:

def parse_front(self, response):

talk_links = response.css(“a.ga-link::attr(href)”)
links_to_follow = talk_links.extract() for url in links_to_follow:
url = url.replace(‘?language’, ‘/transcript?language’)
yield response.follow(url=url, callback=self.parse_pages)

Em seguida, o segundo método de análise raspa os títulos e textos de palestras individuais e os recolhe num dicionário:

def parse_pages(self, response): 
title = response.xpath(‘//head/title/text()’).extract_first()
title = title.strip()
title = title.split(‘:’)[0]
talk = response.xpath( ‘//div[@class=”Grid__cell flx-s:1 p-r:4"]/p/text()’).extract()
for i in range(len(talk)):
line = talk[i].strip()
line = line.replace(‘\n’,’ ‘)
talk[i] = line.replace(‘\t’,’ ‘)
talk = ‘ ‘.join(talk)
talk = talk.replace(‘\n’,’ ‘)
ted_dict[title] = talk

No final, o dicionário é despejado em um arquivo CSV. Tudo o que eu precisava fazer da minha parte era determinar as expressões xpath apropriadas. Defini também um atraso de meio minuto entre cada baixa (sem ele, estava atacando o servidor com demasiada frequência e este bloqueava minhas consultas):

class TedSpiderPt (scrapy.Spider):
name = “ted_spider_pt”
download_delay = 0.5

O restante da preparação de dados e o processo de treinamento podem ser acompanhados no Jupyter Notebook aqui. Abaixo, passeio por ele e destaco os momentos cruciais.

Raspei cerca de 2500 palestras da TED para cada versão do português, cerca de 12 a 18 mil caracteres por palestra. Depois de limpar a formatação, rotulo as transcrições de “0” para PT-PT (português europeu) e “1” para PT-BR e, em seguida, aplico o método fit_transform() da classe CountVectorizer (que faz parte da biblioteca scikit-learn).

Um exemplo de dados preparados para o CountVectorizer.

Se este artigo é para você uma introdução ao processamento de linguagem natural: o CountVectorizer transforma um conjunto de documentos de texto em contagens de palavras. A forma numérica dos dados, em vez da textal, melhora substancialmente a capacidade de processá-los.

Primeiro, divido as palestras em conjuntos de treinamento e teste, na proporção de 2:1. O modelo aprenderá apenas nos dados de treinamento. A parte de teste é usada para verificar seu desempenho antes de liberá-lo no mundo.

Depois, aplico o fit() do Vectorizer aos dados de treinamento — em português simples, o método atribui um número único para cada palavra encontrada lá, criando um vocabulário. Para eliminar os valores extremos, defino o parâmetro min_df como 2 — as palavras que ocorrem apenas uma vez não são levadas em consideração. Depois, transformo o conjunto de treinamento — ou seja, conto as ocorrências de cada palavra do vocabulário. Depois da aplicação do CountVectorizer, cada uma das 59061 palavras é contada em cada uma das 3555 palestras que compõem o conjunto de treinamento.

Confira um exemplo abaixo:

Rastreando uma palavra específica.

A palavra “eu” foi indexada como “23892” no vocabulário. Nas contagens de palavras para este índice, vemos uma palestra com um número significativamente maior de “eu”. Podemos voltar atrás até a transcrição da palestra e … de fato, as experiências pessoais desempenham um papel importante na palestra da TED de Chiki Sarkar — daí a maior incidência do “eu” na fala dela.

Além de vetorizar e contar, evito qualquer outro pré-processamento. Não realizo a stemização, ou seja não reduzo as palavras aos seus troncos, pois quero manter as diferenças gramaticais (por exemplo, diferenças na conjugação de verbos) entre as duas versões do português. Ou seja, na frase “O João está fazendo” quero manter o verbo “fazer” no gerúndio, porque assim posso supor que se trata de um texto brasileiro.

Treinando o modelo

Foto de Jelmer Assink em Unsplash

Treino então um classificador Multinomial Naive Bayes (também da scikit-learn) utilizando o conjunto de dados de treinamento. Embora esta abordagem seja bastante básica, ela funciona surpreendentemente bem em aplicativos de PNL como este.

De novo, uma explicação para os iniciantes: em princípio, o classificador NaiveBayes compara as frequências de uma dada palavra nos conjuntos BR e PT e assim determina se a palavra sugere um texto mais “brasileiro” ou “português”. Durante a predição, todas as palavras no texto são consideradas e obtemos a probabilidade final.

Depois de treinar o classificador, transformo o conjunto de teste usando o mesmo vetorizador que antes. Ele já está preenchido com o vocabulário do conjunto de treinamento e contará apenas as palavras que aparecem nele.

Em seguida, o conjunto de teste vetorizado é classificado usando MultinomialNaiveBayes. Nesse caso em particular, fiz um pequeno ajuste no conjunto de testes — queria testar a capacidade do modelo de classificar textos curtos. Assim, dividi as palestras do conjunto de testes em fragmentos menores, de 200 a 760 caracteres. Para comparação, o parágrafo que você está lendo agora contém 400 caracteres.

Os resultados?

A captura de tela do Jupyter Notebook mostrando a pontuação e a matriz de confusão do modelo.
Baita pontuação!

Consegui 86,55% de exatidão (accuracy) em textos curtos, com uma proporção quase idêntica de exemplos PT-BR e PT-PT falsamente classificados. Será um resultado suficientemente bom? Não sou o juiz mais objetivo aqui, mas diria que é não é mau. Vamos ver alguns exemplos de frases classificadas incorretamente para nos ajudar a avaliar o modelo.

Três exemplos de textos PT-BR classificados como PT-BR

Olhando para as frases acima, é fácil imaginar outras tantas escritas duma maneira que não as faça soar tipicamente luso ou brasileiro. Esse é o caso de muitas das frases classificadas incorretamente pelo modelo. De qualquer forma, vou me aprofundar nas maneiras de melhorar o modelo numa futura parte dois deste artigo. Por enquanto, vamos compartilhá-lo com o mundo!

Implementando uma API com o modelo

Fonte da imagem e de outras riquezas de Flask: https://palletsprojects.com/p/flask/

Se quero usar meu modelo fora do caderno da Jupyter, preciso duma maneira de exportá-lo. A fim de fazé-lo, usei a biblioteca de serialização joblib que fornece uma solução leve para o problema.

Qualquer texto novo eviado para o modelo precisa ser transformado pelo vetorizador e depois classificado. Para isso, junto o vetorizador e o classificador numa pipeline (palavra que, se não me engano, não costuma ser traduzida) também da scikit-learn. Depois, basta despejá-lo em um arquivo joblib:

pipeline = Pipeline([('vectorizer',vectorizer), 'classifier',nb_classifier)])
dump(pipeline, 'ptclassifier.joblib', protocol = 2)

“Leve” é como podemos também descrever o Flask, um framework de aplicativos Web enxuto que usei para implementar a API. O código completo do meu aplicativo Flask está neste repositório na GitHub. Basicamente, ele carrega o modelo de joblib e escuta solicitações GET com um argumento só: uma cadeia de caracteres (string). Uma vez recebido, ele passa o texto pela pipeline e retorna o rótulo previsto junto com o valor de confiança da previsão.

Apliquei também o pacote flask-cors para permitir solicitações duma origem diferente. No nosso caso, o aplicativo frontend que vou apresentar abaixo, hospedado em outro lugar, precisará acessar esta API do Flask. Você pode ler mais sobre o CORS aqui.

Hospedei meu aplicativo Flask na PythonAnywhere, um serviço que descrevi com mais detalhes no artigo sobre o tweetbot. Se você está começando com scripts de Python hospedados on-line, recomendo o PyA de todo o coração — a equipe SAC deles sempre foi muito prestativo com minhas perguntas bem ingénuas.

Meu caminho tortuoso para um Front-end de AA

Cada serviço precisa de uma vitrine. Uma vitrine qualquer. Foto de Eric Muhr da Unsplash

Ah, o Front-end. A raiz e a causa da minha enorme frustração. Estava convencido de que a última milha, configurar a página mais simples com uma caixa de texto e um botão “ENVIAR” seria bastante fácil. Tão convencido estava que acabei num círculo vicioso.

Primeiro, alguém me sugeriu utilizar o Vue.js, mas começar com um livro sobre o próprio JavaScript, como o EloquentJS. Ahhhh. Não me entenda mal, gostei do EJS; sua versão online, com uma caixa de areia para os exercícios, é muito acessível. Mas depois de uma semana com o curso, percebi que meu objetivo naquele momento era costurar uma interface básica e voltar a construir modelos. Ainda não me sinto fluente o suficiente em Python para iniciar um novo idioma.

Pesquisei no Google por “modelo de interface de aprendizagem automática simples” e palavras-chave semelhantes, com resultados mistos. A armadilha em que caí? Minha paciência com uma solução mínima viável durou menos que o tempo necessário para entender e implantar o modelo.

Finalmente, encontrei um que funcionou rápido o suficiente para meu pavio curto — apresentado por James Salvatore no Uptake Tech Blog. Peguei o modelo dele e o ajustei um pouco para meus propósitos. Para simplificar, eliminei a barra de ferramentas, adicionei uma imagem que servia de cabeçalho e mantive apenas uma caixa de texto. Hospedei a versão de produção do aplicativo na AlwaysData, outro serviço que posso recomendar.

Veja o resultado final aqui, se você ainda não o fez.

Classificado como 97,5% brasileiro. Qual será o resultado para “A Portuguesa”? Confira aqui.

Brinque com ele, tente “enganar” o classificador. Ficarei muito feliz se você quebrar algo e me escrever o que aconteceu! Além disso, qualquer crítica construtiva do código no repositório vai ser alvo de minha eterna gratidão.

Próximos passos?

Vai ter uma parte 2 deste artigo. Juro. Num futuro próximo, quero:

  • Melhorar o aplicativo front-end para que mostre ao usuário como as palavras individuais influenciam a previsão;
  • Raspar algumas outras fontes de texto (notícias, blogs, legendas), verificar o desempenho do modelo;
  • Explorar os erros do classificador e tentar descobrir padrões;
  • Tentar uma abordagem LSTM para o problema e comparar os resultados;
  • E por último, mas não menos importante, espero ter alguns comentários dos meus leitores que valham a pena compartilhar!

Bônus!

Encontrei dois exemplos de pessoas lidando com problemas semelhantes com os dialetos em português:

Agradeço a Piotr Migdał pela orientação neste e noutros projetos, e a Kamil Herba e Piotr Kaznowski por comentários perspicazes, tanto sobre o artigo como o aplicativo web.

--

--

Marek K. Cichy

PL PT ES Linguist turned NLP/ML rookie. Striving to bridge various worlds.