No se han encontrado widgets en la barra lateral

Los Grandes Modelos de lenguaje están introduciendo cambios en muchos ámbitos. En el campo que nos toca: el de dar soluciones mediante la inteligencia artificial ha provocado una pequeña revolución.

La proliferación de diferentes aplicaciones empresariales que empiezan a basarse en Grandes Modelos de Lenguaje nos ha conducido a la necesidad de medir la calidad de las soluciones aportadas por estas aplicaciones. Nos encontramos que las métricas que hemos ido usando hasta ahora con modelos más tradicionales, como Accuracy, F1 Score o Recall no nos sirven para evaluar el resultado de modelos que no dejan de ser generativos.

Empezamos a usar métricas, como BLEU, ROUGE , o METEOR, métricas que difieren dependiendo de la utilidad que le demos al Modelo.

En este artículo veremos como usar ROUGE para medir la calidad de los resúmenes generados por un modelo de lenguaje.

¿Que es ROUGE?

ROUGE no es una sola métrica. Está compuesto por un conjunto de métricas que se usan para contrastar la calidad de un texto generado en comparación a un texto de referencia. Es por eso que se puede usar siempre que tengamos un texto que nos sirva como referencia. Algunos de los usos más comunes son en traducciones y en generación de resúmenes, o extracción de información de un texto más grande.

Hay que tener en cuenta que ROUGE es un indicativo tan bueno como lo sea el texto de referencia. Si partimos de unas referencias de mala calidad, los resultados que nos devuelva ROUGE no serán indicativos de la calidad del texto generado.

Otra utilidad es medir como de diferentes son las salidas producidas por dos modelos diferentes. Hay veces en las que lo que queremos ver es si existen diferencias significativas entre dos modelos. Ta sean modelos diferentes, o el mismo modelo, pero con algún tipo de entreno diferente. O Incluso medir si, por ejemplo, un proceso de pruning o de quantization (reducción de peso de modelos) ha alterado de forma significativa el resultado producido por nuestro modelo para la tarea especifica que queremos usarlo.

Aunque hay muchas librerías que implementan ROUGE en el notebook he usado la que se está convirtiendo en estándar. Las métricas que nos devuelve son:

  • ROUGE-1: Nos indica la coincidencia entre el texto generado y el texto de referencia usando 1 grama, es decir, una palabra.
  • ROUGE-2: Toma conjuntos de 2 gramas, y busca coincidencias.
  • ROUGE-L: Evalúa la coincidencia de la cadena de palabras más larga entre los dos textos. No hace falta que las palabras estén en el mismo orden estricto.
  • ROUGE-LSUM. Parecido a Rouge L. Pero tiene en consideración saltos de línea como límites entre las oraciones.

Más información sobre las métricas de ROUGE en: https://pypi.org/project/rouge-score/

¿Qué vamos a medir en nuestro Notebook?

Mediremos un modelo T5 base con un modelo T5 tuneado para generar resúmenes. Primero analizaremos las diferencias entre los dos modelos para ver si él fine tuneado ha producido un modelo con un comportamiento diferente. Después mediremos los dos modelos con unos textos de referencia que nos vienen en el Dataset CNN-DailyMail.

El código fuente de este artículo, y de todo el curso de Grandes Modelos de Lenguaje, puede encontrarse en mi repositorio de Github, junto con los artículos en Inglés. El notebook usado para este artículo es: https://github.com/peremartra/Large-Language-Model-Notebooks-Course/blob/main/rouge-evaluation-untrained-vs-trained-llm.ipynb

Los dos modelos usados se pueden encontrar en Hugging-Face:

Cargamos los datos.

Vamos a usar dos Datasets diferentes. Primero usaremos un Dataset disponible en Kaggle, de noticias del MIT, que no tienen resúmenes generados. Este Dataset lo usaremos para comprobar que los dos modelos generan resúmenes diferentes partiendo de un mismo texto con el que no han sido entrenados.

https://www.kaggle.com/datasets/deepanshudalal09/mit-ai-news-published-till-2023

Empezamos importando las típicas librerías de Python:

#Import generic libraries
import numpy as np 
import pandas as pd
import torch

Todas son librerías muy conocidas del universo Python. Es necesario importar torch, porque ROUGE la necesita.

Carguemos el Dataset.

news = pd.read_csv('/kaggle/input/mit-ai-news-published-till-2023/articles.csv')
DOCUMENT="Article Body"

#Because it is just a course we select a small portion of News.
MAX_NEWS = 3
subset_news = news.head(MAX_NEWS)

subset_news.head()

El cuerpo completo del artículo está contenido en el campo Article Body. Realmente va a ser el único campo que usemos del Dataset para nuestro fin.

Tan solo seleccionamos tres noticias para acelerar la ejecución del Notebook.

articles = subset_news[DOCUMENT].tolist()

Hemos guardado en la variable articles el texto completo de las tres noticias que hemos cargado. Si queremos trabajar con más o menos noticias tan solo hay que cambiar el valor de la variable MAX_NEWS.

Cargar los modelos y crear los resúmenes.

Los dos modelos usados son de la familia T5. Uno de ellos, modelo fundacional, T5-Base, y el otro uno creado a partir del T5-Base, pero fine tuneado con el Dataset CNN-DailyMail.

Como los dos modelos están disponibles en Hugging Face, trabajaremos con las librerías transformers.

import transformers
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

model_name_small = "t5-base"
model_name_reference = "flax-community/t5-base-cnn-dm"

Los nombres de los modelos se guardan en las variables model_name_small y model_name_reference. los usaremos después para cargar los tokenizadores y modelos correspondientes.

Para cargarlos usaremos una función:

#This function returns the tokenizer and the Model. 
def get_model(model_id):
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    model = AutoModelForSeq2SeqLM.from_pretrained(model_id)
    
    return tokenizer, model

La función recibe como entrada el model_id, que es el nombre del modelo, y nos devuelve el modelo cargado junto a su tokenizador. El tokenizador se utiliza para convertir el texto en secuencias de tokens que son procesables por el modelo.

Para cargar el modelo se usa AutoModelForSeq2SeqLM. Los modelos secuencia a secuencia se usan para generación de texto como la creación de resúmenes.

Con esta función obtenemos los dos tokenizadores y modelos:

tokenizer_small, model_small = get_model(model_name_small)
tokenizer_reference, model_reference = get_model(model_name_reference)

Con este código descargamos tanto los tokenizadores como los modelos indicados. Con lo que ya estaríamos listos para generar los resúmenes.

Para generar los resúmenes crearemos una función que recibirá cuatro parámetros:

  • Lista de los textos a resumir.
  • El tokenizador.
  • El modelo.
  • La longitud máxima para el resumén obtenido.

La función debe formatear un poco el texto de los artículos a resumir. Introduciendo un prefijo delante de cada uno de los artículos. Este prefijo serán las instrucciones para el modelo. En nuestro caso le indicamos que debe realizar un resumen con la noticia.

Cada noticia será transformada en encodings con el tokenizer y se la pasará al modelo, que le devolverá una respuesta. Esta respuesta será el resumen, que debemos decodificador, es decir, pasar de encodings a texto otra vez.

def create_summaries(texts_list, tokenizer, model, max_l=125):
    
    # We are going to add a prefix to each article to be summarized 
    # so that the model knows what it should do
    prefix = "Summarize this news: "  
    summaries_list = [] #Will contain all summaries

    texts_list = [prefix + text for text in texts_list]
    
    for text in texts_list:
        
        summary=""
        
        #calculate the encodings
        input_encodings = tokenizer(text, 
                                    max_length=1024, 
                                    return_tensors='pt', 
                                    padding=True, 
                                    truncation=True)

        # Generate summaries
        with torch.no_grad():
            output = model.generate(
                input_ids=input_encodings.input_ids,
                attention_mask=input_encodings.attention_mask,
                max_length=max_l,  # Set the maximum length of the generated summary
                num_beams=2,     # Set the number of beams for beam search
                early_stopping=True
            )
            
        #Decode to get the text
        summary = tokenizer.batch_decode(output, skip_special_tokens=True)
        
        #Add the summary to summaries list 
        summaries_list += summary
    return summaries_list 
    

Veamos con detalle una parte del código de la función: la llamada al modelo, para generar los resúmenes.

   # Generate summaries
    with torch.no_grad():
        output = model.generate(
            input_ids=input_encodings.input_ids,
            attention_mask=input_encodings.attention_mask,
            max_length=max_l,  # Set the maximum length of the generated summary
            num_beams=2,     # Set the number of beams for beam search
            early_stopping=True
        )

La línea with torch.no_grad() está indicando que queremos que el código que contiene se ejecute en un contexto donde no se calculen los gradientes. Sirve para acelerar la ejecución de el código, y se usa solo en inferencia.

Dentro del contexto sin cálculo de gradientes llamamos a model.generate, con los siguientes parámetros:

  • input_ids. Los encodings recién creados.
  • attention_mask: El tokenizer también nos devuelve la máscara de atención. En un modelo que usa Attention, lo que le indica es a que tokens debe prestar más atención.
  • max_length: La longitud máxima de la respuesta del modelo.
  • num_beams: Como más beams indiquemos, mayor será la diversidad de la respuesta generada por el modelo. Lo mantenemos en 2 que nos dará un poco de variedad.
  • early_stoping. Le damos al modelo la posibilidad de para antes de la longitud máxima en caso de que lo considere necesario. Se recomienda informarla a True a no ser que por alguna razón queramos que el modelo nos devuelva el texto de la longitud indicada.

Veamos como llamar a la función create_summaries para obtener los resúmenes:

# Creating the summaries for both models. 
summaries_small = create_summaries(articles, 
                                  tokenizer_small, 
                                  model_small)

summaries_reference = create_summaries(articles, 
                                      tokenizer_reference, 
                                      model_reference)

Como se puede ver es muy sencillo, tan solo pasamos los textos a resumir y el tokenizer y el modelo que hemos recuperado previamente con la función get_model.

Veamos como son los resumenes generados por los dos modelos:

summaries_small

['MIT and MIT-Watson AI Lab have developed a unified framework. the system can simultaneously predict molecular properties and generate new molecules. it uses this grammar to construct viable molecules and predict their properties.', '\'BioAutoMATED\' is an automated machine-learning system that can select and build an appropriate model for a given dataset. it can even take care of the laborious task of data preprocessing, whittling down a months-long process to just a few hours. \'"We want to lower these barriers for a lot of folks that want to use machine learning or biology," says first co-author Jacqueline Valeri.', "MIT and IBM research scientists have made a computer vision model more robust by training it to work like a part of the brain that humans and other primates rely on for object recognition. 'we asked the artificial neural network to make the function of one of your inside simulated “neural” layers as similar as possible to the corresponding biological neural layer,' says MIT professor."]

summaries_reference

['Researchers created a machine-learning system that automatically learns the "language" of molecules using only a small, domain-specific dataset. The system learns to construct viable molecules and predict their properties. Computational design and Fabrication Group will be presented at the International Conference for Machine Learning.', "Automated machine-learning system can select and build an appropriate model for a given dataset. 'BioAutoMATED' is an automated machine-learning system. The tool includes binary classification models, multi-class classification models, and more complex neural networks.", "MIT and IBM researchers have found that artificial neural networks resemble the multilayered brain circuits that process visual information in humans and other primates. 'We asked it to do both of those things as well as the standard, computer vision approach,' said one expert. The network found to be more robust by training it to work like a part of the brain that humans rely on for object recognition."]

A primera vista ya se puede observar que los resúmenes generados son diferentes. Pero lo que no podemos determinar es cuál de ellos es mejor, o se adapta mejor a nuestras necesidades.

Yo diría que incluso es complicado discernir si hay muchas similitudes entre ellos, o si las diferencias en la generación de texto son significativas entre ambos modelos.

Para averiguar si los resúmenes son muy diferentes usaremos ROUGE. Al comparar dos resúmenes generados, donde ninguno de ellos se usa como modelo de referencia, no estamos obteniendo una idea de la calidad de los resúmenes generados. Obtenemos una indicación de lo diferentes entre sí que son los resultados. Con lo que podemos identificar si el proceso de fine tuneado, como mínimo, ha tenido algún tipo de repercusión en el resultado de los resúmenes.

ROUGE

Vamos a instalar y cargar las librerías necesarias para que podamos correr la métrica ROUGE.

Aunque hay varias librerías que implementan el cálculo de Rouge, yo he decidido usar la librería evaluate. A veces no hay otra razón que la costumbre, y es la librería que suelo usar.

!pip install evaluate 

import evaluate
from nltk.tokenize import sent_tokenize

#With the function load of the library evaluate 
#we create a rouge_score object
rouge_score = evaluate.load("rouge")

Una vez tenemos las librerías importadas, realizar el cálculo de la métrica ROUGE es tan sencillo como una sola llamada a la función compute del objeto rouge_score que acabamos de crear.

A la función le pasaremos los textos a comparar y un tercer parámetro que indica si queremos usar las raíces de las palabras o las palabras enteras para ejecutar las comparaciones. Lo más normal es comparar tan solo las raíces para que palabras como Saltar o Salto sean consideradas iguales.

Algunos ejemplos podrían ser:

  • Saltando -> Saltar.
  • Corriendo -> Correr.
  • Gatos -> Gato.

En el caso de que usemos True en Steamer estas palabras se consideran la misma, y en el caso de usar False se consideran palabras diferentes.

Eso sí, el texto hay que prepararlo un poco insertando saltos de línea al inicio de cada línea, y eliminando los caracteres vacíos. Para ello creamos la función compute_rouge_scrore.

def compute_rouge_score(generated, reference):
    
    #We need to add '\n' to each line before send it to ROUGE
    generated_with_newlines = ["\n".join(sent_tokenize(s.strip())) for s in generated]
    reference_with_newlines = ["\n".join(sent_tokenize(s.strip())) for s in reference]
    
    return rouge_score.compute(
        predictions=generated_with_newlines,
        references=reference_with_newlines,
        use_stemmer=True,
        
    )

Como se puede ver, esta función, recibe el texto generado, el texto de referencia y les añade el salto de línea a cada sentencia. Finalmente realiza una sola llamada a compute.

 compute_rouge_score(summaries_small, summaries_reference)

En esta llamada le estamos pasando el texto generado con el modelo T5 Base y el modelo T5 fine tuneado. El resultado que nos devuelve es:

{'rouge1': 0.47018752391886715, 
'rouge2': 0.3209013209013209, 
'rougeL': 0.34330271718331423, 
'rougeLsum': 0.44692881745120555}

Estos son los resultados de comparar los resúmenes producidos por los dos modelos.

Se puede ver que el grado de similitud en ROUGE-1 es del 47 %, mientras que en ROUGE-2 es del 32 %. Lo que nos indica que los resultados son diferentes, con algunos parecidos, pero unas diferencias que parecen suficientes como para considerar que el proceso de fine tunning ha afectado a como el modelo genera los resúmenes.

Pero todavía no podemos decir que modelo es mejor, ya que no hemos comparado con ningún texto de referencia.

Comparando con un texto de referencia.

Cargaremos el Dataset CNN_Dailymail. Un Dataset muy conocido que forma parte de la librería Datasets. Que nos va como anillo al dedo para nuestro ejemplo.

El Dataset aparte de las noticias, contiene un resumen de estas, generados por humanos, que podemos tomar como texto de referencia.

Compararemos los resúmenes generados por los dos modelos con los que vienen en el Dataset y así determinar cuál de ellos produce unos resúmenes más parecidos a los que hemos tomado como texto de referencia.

from datasets import load_dataset

cnn_dataset = load_dataset(
    "cnn_dailymail", version="3.0.0"
)

#Get just a few news to test
sample_cnn = cnn_dataset["test"].select(range(MAX_NEWS))

sample_cnn

Cargamos tan solo un número limitado de noticias, ya que se trata de un ejemplo y así contendremos el tiempo de ejecución necesario.

Como tenemos que indicarle a la función que genera los resúmenes, que hemos creado anteriormente, la longitud máxima vamos a recuperar está del resumen más largo disponible de los que hemos cargado del Dataset. Así les damos a los modelos la posibilidad de generar resúmenes de la misma longitud.

max_length = max(len(item['highlights']) for item in sample_cnn)
max_length = max_length + 10

Ya tenemos todos los datos necesarios para generar los resúmenes.

summaries_t5_base = create_summaries(sample_cnn["article"], 
                                      tokenizer_small, 
                                      model_small, 
                                      max_l=max_length)

summaries_t5_finetuned = create_summaries(sample_cnn["article"], 
                                      tokenizer_reference, 
                                      model_reference, 
                                      max_l=max_length)

#Get the real summaries from the cnn_dataset
real_summaries = sample_cnn['highlights']

Ya tenemos los tres resúmenes en sus correspondientes variables, vamos a darle un vistazo al contenido.

summaries = pd.DataFrame.from_dict(
        {
            "base": summaries_t5_base, 
            "finetuned": summaries_t5_finetuned,
            "reference": real_summaries,
        }
    )
summaries.head()

La verdad es que a simple vista es imposible saber cuál de los dos modelos crea resúmenes más parecidos a los que contienen el Dataset y que hemos tomado como resúmenes de referencia.

compute_rouge_score(summaries_t5_base, real_summaries)
{'rouge1': 0.3050834824090638, 
'rouge2': 0.07211128178870115,
'rougeL': 0.2095520274299344, 
'rougeLsum': 0.2662418008348241}
compute_rouge_score(summaries_t5_finetuned, real_summaries)
{'rouge1': 0.31659149328289443, 
'rouge2': 0.11065084340946411, 
'rougeL': 0.22002036956205442, 
'rougeLsum': 0.24877540132887144}

Con estos resultados me atrevería a indicar que el modelo fine tuneado realiza unos resúmenes ligeramente mejores que el T5-Base. Ya que obtiene mejores puntuaciones en todas las métricas, excepto en LSUM, donde la diferencia es mínima.

También hay que indicar que las métricas ROUGE son altamente interpretables, y que no nos cuentan una realidad absoluta. Es decir, un modelo no tiene por qué ser mejor que otro porque su puntuación en las métricas de ROUGE sea mejor. Esto tan solo nos está indicando que los textos que produce tiene más parecido con los de referencia que los de otro modelo.

Los dos modelos tienen unos resultados muy parecidos, pero el del modelo fine tuneado saca mejores notas en todas las métricas, sobretodo en Rouge-2 i rougeL, a excepción de Lsum. Esto viene a significar que el modelo Base podría producir textos más similares, pero que el modelo fine tuneado utiliza un diccionario más parecido al de los textos de referencia.

En todo caso, no podemos sacar conclusiones claras, ya que tan solo hemos analizado res resúmenes. Tendríamos que definir una estrategia más amplia para decidir que modelo es mejor. Por ejemplo, agrupar las noticias por temática y ver si hay diferencias importantes entre los resultados dependiendo de la temática o estilo de la noticia.

Continuar aprendiendo.

Este artículo es la versión en castellano de los artículos que acompañan el curso de Grandes Modelos de Lenguaje. Donde podéis aprender más de evaluación de Modelos de Lenguaje y muchos otros temas.

Un resumen muy bueno con ejemplos lo podemos encontrar en Hugging Face: https://huggingface.co/spaces/evaluate-metric/rouge

Cómo crear una Siamese Network para comparar imágenes, con TensorFlow.

Tal como indica el título, vamos a construir con TensorFlow una red siamesa que aceptará dos entradas y nos dirá Read more

Kurond. Una criatura ineficiente.

Kurond ha sido mi primera criatura creada para moverse con ML Agents. Es ineficiente por que su cuerpo esta MAL Read more

Fine-Tuning eficiente con LoRA. Entrena de forma óptima los Grandes Modelos de Lenguaje.

LoRA es una de las técnicas más eficientes y efectivas de Fine-Tuning aplicable a Grandes Modelos de Lenguaje que existe Read more

Como crear sentencias SQL usando lenguaje natural con la API de OpenAI.

En mi trabajo tengo relación con varios equipos de desarrollo, uno de ellos dedica una buena parte de su tiempo Read more

Por Martra

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *