Sumarizando READMEs de projetos de software livre
Me siga no X | Me siga no LinkedIn | Apoie a Newsletter | Solicite uma consultoria
Sumarização é o processo de condensar um texto ou documento para uma forma mais concisa, mantendo as informações essenciais e principais ideias.
A tarefa de sumarização envolve a geração de um resumo que captura o significado e o contexto do texto original, permitindo aos leitores obter uma visão geral do conteúdo sem a necessidade de ler todo o documento.
A sumarização pode ser realizada de duas formas principais: sumarização extrativa (produz um resumo extraindo frases que representam em conjunto as informações mais relevantes do conteúdo original) e abstrativa (produz um resumo gerando novas frases do documento que resumem a ideia principal).
A sumarização é uma técnica que pode ser aplicada em diversos contextos, como resumos de notícias, resumos de artigos científicos ou livros. No processo de desenvolvimento de software, podemos também utilizar técnicas de sumarização em áreas como:
Extração de requisitos: A sumarização pode ser usada para extrair as informações relevantes dos documentos de requisitos. Isso pode ajudar os times de desenvolvimento a identificar rapidamente os principais objetivos ou restrições do sistema.
Análise de logs: Em sistemas de registro de eventos (logs), A sumarização pode ser aplicada para extrair as informações de registros de eventos (logs) relevantes, facilitando a compreensão de padrões de uso da aplicação ou até detectando eventuais eventos críticos no software.
Documentação de software: A sumarização pode ser usada para gerar resumos de componentes de software, APIs, bibliotecas ou documentos técnicos, auxiliando na criação de uma mais documentação concisa.
Nesse texto, vamos sumarizar a porta de entrada de projetos de software livre — o arquivo README. Com essa sumarização, poderíamos saber com mais facilidade se poderíamos utilizar um dado projeto de software livre, sem precisar ler todo o seu README.
Huggingface
O Hugging Face é uma empresa de tecnologia conhecida por suas contribuições na área do Processamento de Linguagem Natural (NLP) e aprendizado de máquina.
Eles são famosos por desenvolver e manter a biblioteca de código aberto chamada “transformers
”, que oferece diversos modelos pré-treinados para várias tarefas de NLP, incluindo tradução de idiomas, geração de texto, e sumarização de textos.
Iremos utilizar esta biblioteca transformers
para nos auxiliar nesse texto. Podemos começar a ligar os motores instalando a biblioteca localmente:
pip install transformers
Com a biblioteca instalada, vamos precisar seguir quatros passos para construir nosso modelo de sumarização de textos.
Escolher e carregar um modelo pre-treinado
Carregar um tokenizador
Pre-processar os dados de entrada
Gerar a sumarização
1. Escolher e carregar um modelo pre-treinado
A escolha do modelo apropriado para a sumarização depende do seu requisito específico, considerando fatores como o tamanho do texto, a qualidade da sumarização desejada e os recursos disponíveis.
Existem modelos específicos para sumarização, como o BART, o Pegasus ou o T5, que, de acordo com o site do Huggingface, podem ser utilizados para tarefas de sumarização.
Para nosso primeiro exemplo, utilizaremos o T5. Para conhecer um pouco mais sobre o T5, recomendo a leitura deste texto de blog oficial do Google, ou do artigo original.
De acordo com o site do T5 no Huggingface, o modelo é disponibilizado em cinco tamanhos:
t5-small (pre-treinado com 60 milhões de parâmetros);
t5-base (com 220 milhões de parâmetros);
t5-large (com 770 milhões de parâmetros);
t5-3b (com 3 bilhões de parâmetros);
t5-11b (com 11 bilhões de parâmetros).
Podemos começar com o t5-large. Para usar o T5 pela biblioteca transformers do Huggingface, basta importa-lo:
from transformers import T5ForConditionalGeneration
model_name = "t5-large"
model = T5ForConditionalGeneration.from_pretrained(model_name)
Na primeira vez que executarmos esse código, a classe T5ForConditionalGeneration
irá baixar os 2.9GB do modelo. Mas não se preocupe; o modelo é cacheado e execuções subsequentes não necessitam de download.
A classe T5ForConditionalGeneration
representa a arquitetura do modelo T5 pré-treinado para tarefas de geração condicional. Mas o que seria geração condiconal?
Tarefas de geração condicional são tarefas em que um modelo de linguagem gera texto condicionalmente, com base em uma entrada fornecida. Em vez de simplesmente prever a próxima palavra em uma sequência, o modelo leva em consideração um contexto específico e produz uma saída que está condicionada a essa entrada.
Alguns exemplos de tarefas de geração condicional incluem:
Tradução de texto: Dada uma frase em um idioma, o modelo gera a tradução correspondente em outro idioma.
Resposta a perguntas: Dada uma pergunta, o modelo gera uma resposta adequada com base em um contexto fornecido.
Sumarização de texto: Dado um texto longo, o modelo gera um resumo condensado e informativo desse texto.
O último é justamente o tipo de tarefa que estamos interessados.
2. Carregar um tokenizador
Tokenizar é o processo de dividir um texto em unidades menores chamadas tokens. Um token pode ser uma palavra, uma parte de uma palavra (subpalavra) ou até mesmo um caractere, dependendo do nível de granularidade definido pelo tokenizador.
Já abordamos o processo de tokenização em outros textos da newsletter. Por hora, vamos abstrair esse processo todo, utilizando a classe chamada T5Tokenizer
, disponível também na biblioteca transformers
.
from transformers import T5Tokenizer
tokenizer = T5Tokenizer.from_pretrained(model_name)
Repare que exemplo de código acima, usamos o mesmo modelo para o tokenizador e para instanciação do modelo (o t5-large
, que está armazenado na variável model_name)
. Isso acontece pois, para muitos modelos, o tokenizador e o modelo compartilham a mesma arquitetura e pesos pré-treinados.
No entanto, alguns modelos podem ter arquiteturas separadas para essas duas partes. Nesses casos, seria necessário usar tokenizadores e modelos diferentes.
3. Pre-processar os dados de entrada
Nosso objetivo atual é sumarizar READMEs de projetos de software livre, facilitando a vida de potenciais usuários desses projetos.
Para isso, nossa fonte de dados de entrada são arquivos READMEs. Vamos começar baixando o README do projeto gson.
import requests
url = "https://raw.githubusercontent.com/google/gson/main/README.md" README = requests.get(url).text
Como o README está em formato de arquivo Markdown, não é necessário realizar limpezas de conteúdo HTML. Ademais, o T5Tokenizer
já foi desenhado para remover diversos tipos de ruído nos dados. Podemos partir para o processo de pre-processamento:
inputs = tokenizer.encode("summarize: " + README, return_tensors="pt", max_length=512, truncation=True)
Basicamente, a mágica acontece no método encode, que recebe como parâmetro os dados, além de algumas flags (como return_tensors
, max_length
, e truncation
). Embora simples, o trecho de código acima merece alguns destaques.
Precisamos adicionar o prefixo “summarize: ” na entrada do prompt para que o T5 saiba que essa é uma tarefa de sumarização. O T5, assim como outros modelos de linguagem, foram treinados para diversas tarefas de geração de texto; logo é necessário avisar quando queremos uma tarefa específica.
O parâmetro
return_tensors
é opcional e controla o tipo de retorno da operação de tokenização. Quando o parâmetroreturn_tensors
é definido como"pt"
, o objeto retornado será um tensor PyTorch. Caso não tivéssemos fornecido nenhum parâmetro, o objeto de retorno seria uma lista.O parâmetro
max_length
limita o tamanho de tokens da sequência de saída.Por fim, quando parâmetro
truncation
é definido comoTrue
, o texto é truncado para se ajustar aomax_length
. Isso significa que, se o texto exceder o comprimento máximo, ele será cortado para se adequar ao limite especificado.
A variável inputs
é o resultado do processo de pre-processamento, representada como um vetor de tensors. A variável inputs
tem um formato parecido com:
tensor([[21603, 10, 1713, 350, 739, 350, 739, 19,
10318, 3595, 24, 54, 36, 261, 12, 5755,
17057, 7, 139, 70, 446, 10466, 6497, 5,
92, 36, 261, 12, 5755, 3, 9, 446,
...]]
4. Gerar a sumarização
Por fim, basta pedir para que o modelo gere a sumarização, através do método generate
:
outputs = model.generate(inputs, max_length=150, num_beams=3, early_stopping=True)
Novamente, embora simples, essa chamada de método merece alguns destaques:
Há um novo
max_length
no métodogenerate
. No métodoencode
, o parâmetromax_length
controla o comprimento máximo para o número de tokens permitidos na entrada que será codificada. Já no métodogenerate
, omax_length
controla o comprimento máximo de tokens permitidos no texto de saída.O parâmetro
num_beams
controla o número de hipóteses alternativas que o modelo mantém em consideração durante a geração de sequências. Uma hipótese alternativa pode ser entendida como diferentes caminhos para construção de textos. Por exemplo, se você define onum_beams
como 3, significa que podemos ter três possíveis formas de terminar o texto. Cada opção pode ser um pouco diferente das outras em palavras ou ideias, mas todas tendem a fazer sentido.Por fim, o
early_stopping
é um critério de parada que interrompe o modelo se a quantidade denum_beams
forem satisfeitas.
Similar ao método encode, o método generate também devolve um vetor de tensors:
tensor([[ 0, 350, 739, 19, 3, 9, 10318, 3595,
36, 261, 12, 5755, 10318, 3, 17057, 7,
446, 10466, 6497, 3, 5, 34, 54, 92,
12, 5755, 3, 9, 446, 10466, 6108, 12,
10318, 3735, 3, 5, 1]])
Como não conseguimos interpretar o conteúdo desse vetor, precisamos decodifica-lo. Podemos fazer isso através do decode, também disponível na classe T5Tokenizer
.
summary = tokenizer.decode(outputs[0], skip_special_tokens=True)
Se imprimirmos o valor da variável summary
, teríamos um resultado parecido com: “Gson is a Java library that can be used to convert Java Objects into their JSON representation. it can also be used to convert a JSON string to an equivalent Java object. it is currently in maintenance mode; existing bugs will be fixed, but large new features will likely not be added.”
Perceba que não somente o modelo foi capaz de fornecer uma descrição acurada do propósito da biblioteca Gson (“a Java library that can be used to convert Java Objects into their JSON representation”), mas também identificou que a biblioteca está em modo de manutenção e possivelmente não adicionará novas funcionalidades.
Essa informação está presente no README (“ℹ️ Gson is currently in maintenance mode; existing bugs will be fixed, but large new features will likely not be added. If you want to add a new feature, please first search for existing GitHub issues, or create a new one to discuss the feature and get feedback.”), embora não seja exatamente a mesma extraída pelo modelo. A capacidade de criar uma nova versão resumida baseada no seu entendimento do conteúdo é característica de uma estratégia abstrativa.
Será que o sumário do T5 é realmente bom?
Para avaliar a qualidade de uma sumarização, não basta comparar visualmente o texto original com o sumarizado. É necessário utilizar abordagens mais rigorsas.
Uma métrica comumente empregada para essa tarefa é a ROUGE (Recall-Oriented Understudy for Gisting Evaluation). Em resumo, ROUGE é um conjunto de métricas (como ROUGE-N, ROUGEL, etc) que calculam a sobreposição de n-gramas (sequências contíguas de n palavras) entre a sumarização gerada e o resumo de referência. Já abordamos um pouco sobre n-gramas nessa newsletter.
Para utilizar o ROUGE, podemos usar a bibliotecas o rouge_score
, que calculam o ROUGE automaticamente. Por exemplo:
pip install rouge-score
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'])
scores = scorer.score(README, summary)
rouge1_precision = scores['rouge1'].precision
rouge1_recall = scores['rouge1'].recall
rouge1_f1 = scores['rouge1'].fmeasure
Se imprimimos o valor das variáveis rouge1_precision
, rouge1_recall
e rouge1_f1
, obteríamos seguintes resultados, respectivamente: 1.0
, 0.0621
, 0.1169
.
Estes valores significam que nossa abordagem de sumarização obteve uma alta precisão, indicando que todas as informações relevantes estão presentes). Por outro lado, o modelo obteve uma baixa revocação, indicando que apenas uma pequena porcentagem das informações relevantes do sumário de referência foi incluída no sumário gerado. Por fim, uma vez que a métrica F1 é uma medida combinada que leva em consideração tanto a precisão quanto a revocação, obtivemos também um baixo valor de F1. Também escrevemos sobre essas métricas em outro texto da newsletter.
Conclusão
A tarefa de sumarização embora complexa tornou-se simplificada através das abstrações fornecidas pela biblioteca transformers
da Huggingface. Com as poucas linhas de código que escrevemos nesse texto, podemos agora ampliar o uso de sumarização para outras tarefas. Depois escreve nos comentários quais outras tarefas você gostaria de sumarizar?
Como de costume, o código completo do exemplo acima está disponível neste link, licenciado como MIT.