Lección 3 · 20 min · Gratis

Limpieza y preparación de datos con 🤗 Datasets

La mayoría de las veces, los datos con los que trabajas no estarán perfectamente preparados para entrenar modelos. En esta sección exploraremos las diversas características que 🤗 Datasets proporciona para limpiar tus conjuntos de datos.

Video: youtube.com/watch?v=tqfSFcPMgOI

Dividiendo y organizando nuestros datos[[slicing-and-dicing-our-data]]

De forma similar a Pandas, 🤗 Datasets proporciona varias funciones para manipular el contenido de los objetos Dataset y DatasetDict. Ya encontramos el método Dataset.map() en el Capítulo 3, y en esta sección exploraremos algunas de las otras funciones a nuestra disposición.

Para este ejemplo, usaremos el Conjunto de Datos de Reseñas de Medicamentos alojado en el Repositorio de Aprendizaje Automático de UC Irvine, que contiene reseñas de pacientes sobre varios medicamentos, junto con la condición tratada y una calificación de 10 estrellas de la satisfacción del paciente.

Primero necesitamos descargar y extraer los datos, lo que se puede hacer con los comandos wget y unzip:

!wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip"
!unzip drugsCom_raw.zip

Dado que TSV es solo una variante de CSV que usa tabulaciones en lugar de comas como separador, podemos cargar estos archivos usando el script de carga csv y especificando el argumento delimiter en la función load_dataset() de la siguiente manera:

from datasets import load_dataset

data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
# \t is the tab character in Python
drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")

Una buena práctica al realizar cualquier tipo de análisis de datos es tomar una pequeña muestra aleatoria para tener una idea rápida del tipo de datos con los que estás trabajando. En 🤗 Datasets, podemos crear una muestra aleatoria encadenando las funciones Dataset.shuffle() y Dataset.select():

drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
# Peek at the first few examples
drug_sample[:3]
{'Unnamed: 0': [87571, 178045, 80482],
 'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
 'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'],
 'review': ['"like the previous person mention, I'm a strong believer of aleve, it works faster for my gout than the prescription meds I take. No more going to the doctor for refills.....Aleve works!"',
  '"I have taken Cymbalta for about a year and a half for fibromyalgia pain. It is great\r\nas a pain reducer and an anti-depressant, however, the side effects outweighed \r\nany benefit I got from it. I had trouble with restlessness, being tired constantly,\r\ndizziness, dry mouth, numbness and tingling in my feet, and horrible sweating. I am\r\nbeing weaned off of it now. Went from 60 mg to 30mg and now to 15 mg. I will be\r\noff completely in about a week. The fibro pain is coming back, but I would rather deal with it than the side effects."',
  '"I have been taking Mobic for over a year with no side effects other than an elevated blood pressure.  I had severe knee and ankle pain which completely went away after taking Mobic.  I attempted to stop the medication however pain returned after a few days."'],
 'rating': [9.0, 3.0, 10.0],
 'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'],
 'usefulCount': [36, 13, 128]}

Ten en cuenta que hemos fijado la semilla en Dataset.shuffle() para fines de reproducibilidad. Dataset.select() espera un iterable de índices, por lo que hemos pasado range(1000) para tomar los primeros 1,000 ejemplos del conjunto de datos mezclado. De esta muestra ya podemos ver algunas peculiaridades en nuestro conjunto de datos:

  • La columna Unnamed: 0 parece sospechosamente un ID anonimizado para cada paciente.
  • La columna condition incluye una mezcla de etiquetas en mayúsculas y minúsculas.
  • Las reseñas tienen longitudes variables y contienen una mezcla de separadores de línea de Python (\r\n) así como códigos de caracteres HTML como &\#039;.

Veamos cómo podemos usar 🤗 Datasets para lidiar con cada uno de estos problemas. Para probar la hipótesis del ID de paciente para la columna Unnamed: 0, podemos usar la función Dataset.unique() para verificar que el número de IDs coincide con el número de filas en cada división:

for split in drug_dataset.keys():
    assert len(drug_dataset[split]) == len(drug_dataset[split].unique("Unnamed: 0"))

Esto parece confirmar nuestra hipótesis, así que limpiemos un poco el conjunto de datos renombrando la columna Unnamed: 0 a algo un poco más interpretable. Podemos usar la función DatasetDict.rename_column() para renombrar la columna en ambas divisiones de una sola vez:

drug_dataset = drug_dataset.rename_column(
    original_column_name="Unnamed: 0", new_column_name="patient_id"
)
drug_dataset
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
        num_rows: 161297
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
        num_rows: 53766
    })
})

[!TIP] ✏️ ¡Pruébalo! Usa la función Dataset.unique() para encontrar el número de medicamentos y condiciones únicos en los conjuntos de entrenamiento y prueba.

A continuación, normalicemos todas las etiquetas condition usando Dataset.map(). Como hicimos con la tokenización en el Capítulo 3, podemos definir una función simple que se puede aplicar a todas las filas de cada división en drug_dataset:

def lowercase_condition(example):
    return {"condition": example["condition"].lower()}


drug_dataset.map(lowercase_condition)
AttributeError: 'NoneType' object has no attribute 'lower'

¡Oh no, hemos encontrado un problema con nuestra función de mapeo! Del error podemos inferir que algunas de las entradas en la columna condition son None, que no se pueden convertir a minúsculas ya que no son cadenas. Eliminemos estas filas usando Dataset.filter(), que funciona de manera similar a Dataset.map() y espera una función que reciba un solo ejemplo del conjunto de datos. En lugar de escribir una función explícita como:

def filter_nones(x):
    return x["condition"] is not None

y luego ejecutar drug_dataset.filter(filter_nones), podemos hacer esto en una sola línea usando una función lambda. En Python, las funciones lambda son funciones pequeñas que puedes definir sin nombrarlas explícitamente. Tienen la forma general:

lambda <arguments> : <expression>

donde lambda es una de las palabras clave especiales de Python, <arguments> es una lista/conjunto de valores separados por comas que definen las entradas de la función, y <expression> representa las operaciones que deseas ejecutar. Por ejemplo, podemos definir una función lambda simple que eleva un número al cuadrado de la siguiente manera:

lambda x : x * x

Para aplicar esta función a una entrada, necesitamos envolverla a ella y a la entrada entre paréntesis:

(lambda x: x * x)(3)
9

De manera similar, podemos definir funciones lambda con múltiples argumentos separándolos con comas. Por ejemplo, podemos calcular el área de un triángulo de la siguiente manera:

(lambda base, height: 0.5 * base * height)(4, 8)
16.0

Las funciones lambda son útiles cuando quieres definir funciones pequeñas de un solo uso (para más información sobre ellas, te recomendamos leer el excelente tutorial de Real Python de Andre Burgaud). En el contexto de 🤗 Datasets, podemos usar funciones lambda para definir operaciones simples de mapeo y filtrado, así que usemos este truco para eliminar las entradas None en nuestro conjunto de datos:

drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None)

Con las entradas None eliminadas, podemos normalizar nuestra columna condition:

drug_dataset = drug_dataset.map(lowercase_condition)
# Check that lowercasing worked
drug_dataset["train"]["condition"][:3]
['left ventricular dysfunction', 'adhd', 'birth control']

¡Funciona! Ahora que hemos limpiado las etiquetas, echemos un vistazo a la limpieza de las reseñas.

Creando nuevas columnas[[creating-new-columns]]

Siempre que estés tratando con reseñas de clientes, una buena práctica es verificar el número de palabras en cada reseña. Una reseña podría ser solo una palabra como "¡Genial!" o un ensayo completo con miles de palabras, y dependiendo del caso de uso, deberás manejar estos extremos de manera diferente. Para calcular el número de palabras en cada reseña, usaremos una heurística aproximada basada en dividir cada texto por espacios en blanco.

Definamos una función simple que cuente el número de palabras en cada reseña:

def compute_review_length(example):
    return {"review_length": len(example["review"].split())}

A diferencia de nuestra función lowercase_condition(), compute_review_length() devuelve un diccionario cuya clave no corresponde a uno de los nombres de columna en el conjunto de datos. En este caso, cuando compute_review_length() se pasa a Dataset.map(), se aplicará a todas las filas del conjunto de datos para crear una nueva columna review_length:

drug_dataset = drug_dataset.map(compute_review_length)
# Inspect the first training example
drug_dataset["train"][0]
{'patient_id': 206461,
 'drugName': 'Valsartan',
 'condition': 'left ventricular dysfunction',
 'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"',
 'rating': 9.0,
 'date': 'May 20, 2012',
 'usefulCount': 27,
 'review_length': 17}

Como era de esperar, podemos ver que se ha añadido una columna review_length a nuestro conjunto de entrenamiento. Podemos ordenar esta nueva columna con Dataset.sort() para ver cómo son los valores extremos:

drug_dataset["train"].sort("review_length")[:3]
{'patient_id': [103488, 23627, 20558],
 'drugName': ['Loestrin 21 1 / 20', 'Chlorzoxazone', 'Nucynta'],
 'condition': ['birth control', 'muscle spasm', 'pain'],
 'review': ['"Excellent."', '"useless"', '"ok"'],
 'rating': [10.0, 1.0, 6.0],
 'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'],
 'usefulCount': [5, 2, 10],
 'review_length': [1, 1, 1]}

Como sospechábamos, algunas reseñas contienen solo una palabra, lo que, aunque puede estar bien para el análisis de sentimientos, no sería informativo si queremos predecir la condición.

[!TIP] 🙋 Una forma alternativa de añadir nuevas columnas a un conjunto de datos es con la función Dataset.add_column(). Esto te permite proporcionar la columna como una lista de Python o un array de NumPy y puede ser útil en situaciones en las que Dataset.map() no es adecuado para tu análisis.

Usemos la función Dataset.filter() para eliminar las reseñas que contengan menos de 30 palabras. De manera similar a lo que hicimos con la columna condition, podemos filtrar las reseñas muy cortas exigiendo que las reseñas tengan una longitud superior a este umbral:

drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
print(drug_dataset.num_rows)
{'train': 138514, 'test': 46108}

Como puedes ver, esto ha eliminado alrededor del 15% de las reseñas de nuestros conjuntos de entrenamiento y prueba originales.

[!TIP] ✏️ ¡Pruébalo! Usa la función Dataset.sort() para inspeccionar las reseñas con el mayor número de palabras. Consulta la documentación para ver qué argumento necesitas usar para ordenar las reseñas por longitud en orden descendente.

Lo último que debemos abordar es la presencia de códigos de caracteres HTML en nuestras reseñas. Podemos usar el módulo html de Python para decodificar estos caracteres, así:



text = "I&#039;m a transformer called BERT"
html.unescape(text)
"I'm a transformer called BERT"

Usaremos Dataset.map() para decodificar todos los caracteres HTML en nuestro corpus:

drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])})

Como puedes ver, el método Dataset.map() es bastante útil para procesar datos, ¡y ni siquiera hemos arañado la superficie de todo lo que puede hacer!

Los superpoderes del método map()[[the-map-methods-superpowers]]

El método Dataset.map() toma un argumento batched que, si se establece en True, hace que envíe un lote de ejemplos a la función de mapeo a la vez (el tamaño del lote es configurable pero por defecto es 1,000). Por ejemplo, la función de mapeo anterior que decodificaba todo el HTML tardó un poco en ejecutarse (puedes leer el tiempo transcurrido en las barras de progreso). Podemos acelerar esto procesando varios elementos al mismo tiempo usando una comprensión de lista.

Cuando especificas batched=True, la función recibe un diccionario con los campos del conjunto de datos, pero cada valor es ahora una lista de valores, y no solo un único valor. El valor de retorno de Dataset.map() debe ser el mismo: un diccionario con los campos que queremos actualizar o añadir a nuestro conjunto de datos, y una lista de valores. Por ejemplo, aquí hay otra forma de decodificar todos los caracteres HTML, pero usando batched=True:

new_drug_dataset = drug_dataset.map(
    lambda x: {"review": [html.unescape(o) for o in x["review"]]}, batched=True
)

Si estás ejecutando este código en un notebook, verás que este comando se ejecuta mucho más rápido que el anterior. Y no es porque nuestras reseñas ya hayan sido decodificadas de HTML; si vuelves a ejecutar la instrucción de la sección anterior (sin batched=True), tardará la misma cantidad de tiempo que antes. Esto se debe a que las comprensiones de lista suelen ser más rápidas que ejecutar el mismo código en un bucle for, y también obtenemos algo de rendimiento al acceder a muchos elementos al mismo tiempo en lugar de uno por uno.

Usar Dataset.map() con batched=True será esencial para desbloquear la velocidad de los tokenizadores "rápidos" que encontraremos en el Capítulo 6, que pueden tokenizar rápidamente grandes listas de textos. Por ejemplo, para tokenizar todas las reseñas de medicamentos con un tokenizador rápido, podríamos usar una función como esta:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")


def tokenize_function(examples):
    return tokenizer(examples["review"], truncation=True)

Como viste en el Capítulo 3, podemos pasar uno o varios ejemplos al tokenizador, por lo que podemos usar esta función con o sin batched=True. Aprovechemos esta oportunidad para comparar el rendimiento de las diferentes opciones. En un notebook, puedes cronometrar una instrucción de una sola línea añadiendo %time antes de la línea de código que deseas medir:

%time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True)

También puedes cronometrar una celda completa poniendo %%time al principio de la celda. En el hardware en el que ejecutamos esto, mostró 10.8s para esta instrucción (es el número escrito después de "Wall time").

[!TIP] ✏️ ¡Pruébalo! Ejecuta la misma instrucción con y sin batched=True, luego inténtalo con un tokenizador lento (añade use_fast=False en el método AutoTokenizer.from_pretrained()) para que puedas ver qué números obtienes en tu hardware.

Aquí están los resultados que obtuvimos con y sin procesamiento por lotes, con un tokenizador rápido y uno lento:

Opciones Tokenizador rápido Tokenizador lento
batched=True 10.8s 4min41s
batched=False 59.2s 5min3s

Esto significa que usar un tokenizador rápido con la opción batched=True es 30 veces más rápido que su contraparte lenta sin procesamiento por lotes, ¡esto es realmente asombroso! Esa es la razón principal por la que los tokenizadores rápidos son los predeterminados al usar AutoTokenizer (y por qué se les llama "rápidos"). Logran tal aceleración porque, detrás de escena, el código de tokenización se ejecuta en Rust, que es un lenguaje que facilita la paralelización de la ejecución del código.

La paralelización también es la razón del aumento de velocidad de casi 6x que logra el tokenizador rápido con el procesamiento por lotes: no puedes paralelizar una sola operación de tokenización, pero cuando quieres tokenizar muchos textos al mismo tiempo, puedes simplemente dividir la ejecución entre varios procesos, cada uno responsable de sus propios textos.

Dataset.map() también tiene algunas capacidades de paralelización propias. Dado que no están respaldadas por Rust, no permitirán que un tokenizador lento alcance a uno rápido, pero aún pueden ser útiles (especialmente si estás usando un tokenizador que no tiene una versión rápida). Para habilitar el multiprocesamiento, usa el argumento num_proc y especifica el número de procesos a usar en tu llamada a Dataset.map():

slow_tokenizer = AutoTokenizer.from_pretrained("bert-base-cased", use_fast=False)


def slow_tokenize_function(examples):
    return slow_tokenizer(examples["review"], truncation=True)


tokenized_dataset = drug_dataset.map(slow_tokenize_function, batched=True, num_proc=8)

Puedes experimentar un poco con el tiempo para determinar el número óptimo de procesos a usar; en nuestro caso, 8 pareció producir la mejor ganancia de velocidad. Aquí están los números que obtuvimos con y sin multiprocesamiento:

Opciones Tokenizador rápido Tokenizador lento
batched=True 10.8s 4min41s
batched=False 59.2s 5min3s
batched=True, num_proc=8 6.52s 41.3s
batched=False, num_proc=8 9.49s 45.2s

Esos son resultados mucho más razonables para el tokenizador lento, pero el rendimiento del tokenizador rápido también mejoró sustancialmente. Sin embargo, ten en cuenta que no siempre será así; para valores de num_proc distintos de 8, nuestras pruebas mostraron que era más rápido usar batched=True sin esa opción. En general, no recomendamos usar el multiprocesamiento de Python para tokenizadores rápidos con batched=True.

[!TIP] Usar num_proc para acelerar tu procesamiento suele ser una gran idea, siempre y cuando la función que estés usando no esté haciendo ya algún tipo de multiprocesamiento por sí misma.

Toda esta funcionalidad condensada en un solo método ya es bastante asombrosa, ¡pero hay más! Con Dataset.map() y batched=True puedes cambiar el número de elementos en tu conjunto de datos. Esto es súper útil en muchas situaciones en las que quieres crear varias características de entrenamiento a partir de un ejemplo, y necesitaremos hacer esto como parte del preprocesamiento para varias de las tareas de PNL que emprenderemos en el Capítulo 7.

[!TIP] 💡 En aprendizaje automático, un ejemplo se define generalmente como el conjunto de características que alimentamos al modelo. En algunos contextos, estas características serán el conjunto de columnas en un Dataset, pero en otros (como aquí y para la respuesta a preguntas), se pueden extraer múltiples características de un solo ejemplo y pertenecer a una sola columna.

¡Veamos cómo funciona! Aquí tokenizaremos nuestros ejemplos y los truncaremos a una longitud máxima de 128, pero le pediremos al tokenizador que devuelva todos los fragmentos de los textos en lugar de solo el primero. Esto se puede hacer con return_overflowing_tokens=True:

def tokenize_and_split(examples):
    return tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )

Probemos esto en un ejemplo antes de usar Dataset.map() en todo el conjunto de datos:

result = tokenize_and_split(drug_dataset["train"][0])
[len(inp) for inp in result["input_ids"]]
[128, 49]

Así, nuestro primer ejemplo en el conjunto de entrenamiento se convirtió en dos características porque se tokenizó a más del número máximo de tokens que especificamos: la primera de longitud 128 y la segunda de longitud 49. ¡Ahora hagamos esto para todos los elementos del conjunto de datos!

tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000

¡Oh no! ¡Eso no funcionó! ¿Por qué no? Mirar el mensaje de error nos dará una pista: hay una falta de coincidencia en las longitudes de una de las columnas, una de longitud 1,463 y la otra de longitud 1,000. Si has consultado la documentación de Dataset.map(), quizás recuerdes que es el número de muestras pasadas a la función que estamos mapeando; aquí esos 1,000 ejemplos dieron 1,463 nuevas características, lo que resultó en un error de forma.

El problema es que estamos intentando mezclar dos conjuntos de datos diferentes de distintos tamaños: las columnas drug_dataset tendrán un cierto número de ejemplos (los 1,000 de nuestro error), pero el tokenized_dataset que estamos construyendo tendrá más (los 1,463 del mensaje de error; es más de 1,000 porque estamos tokenizando reseñas largas en más de un ejemplo usando return_overflowing_tokens=True). Eso no funciona para un Dataset, así que necesitamos eliminar las columnas del conjunto de datos antiguo o hacer que tengan el mismo tamaño que en el nuevo conjunto de datos. Podemos hacer lo primero con el argumento remove_columns:

tokenized_dataset = drug_dataset.map(
    tokenize_and_split, batched=True, remove_columns=drug_dataset["train"].column_names
)

Ahora esto funciona sin error. Podemos verificar que nuestro nuevo conjunto de datos tiene muchos más elementos que el conjunto de datos original comparando las longitudes:

len(tokenized_dataset["train"]), len(drug_dataset["train"])
(206772, 138514)

Mencionamos que también podemos lidiar con el problema de la longitud no coincidente haciendo que las columnas antiguas tengan el mismo tamaño que las nuevas. Para hacer esto, necesitaremos el campo overflow_to_sample_mapping que el tokenizador devuelve cuando configuramos return_overflowing_tokens=True. Nos da un mapeo de un nuevo índice de característica al índice de la muestra de la que se originó. Usando esto, podemos asociar cada clave presente en nuestro conjunto de datos original con una lista de valores del tamaño correcto repitiendo los valores de cada ejemplo tantas veces como genere nuevas características:

def tokenize_and_split(examples):
    result = tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )
    # Extract mapping between new and old indices
    sample_map = result.pop("overflow_to_sample_mapping")
    for key, values in examples.items():
        result[key] = [values[i] for i in sample_map]
    return result

Podemos ver que funciona con Dataset.map() sin que tengamos que eliminar las columnas antiguas:

tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
tokenized_dataset
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
        num_rows: 206772
    })
    test: Dataset({
        features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
        num_rows: 68876
    })
})

Obtenemos el mismo número de características de entrenamiento que antes, pero aquí hemos conservado todos los campos antiguos. Si los necesitas para algún posprocesamiento después de aplicar tu modelo, quizás quieras usar este enfoque.

Ahora has visto cómo se puede usar 🤗 Datasets para preprocesar un conjunto de datos de varias maneras. Aunque las funciones de procesamiento de 🤗 Datasets cubrirán la mayoría de tus necesidades de entrenamiento de modelos, puede haber momentos en los que necesites cambiar a Pandas para acceder a características más potentes, como DataFrame.groupby() o APIs de alto nivel para visualización. Afortunadamente, 🤗 Datasets está diseñado para ser interoperable con bibliotecas como Pandas, NumPy, PyTorch, TensorFlow y JAX. Veamos cómo funciona esto.

De Dataset a DataFrame y viceversa[[from-datasets-to-dataframes-and-back]]

Video: youtube.com/watch?v=tfcY1067A5Q

Para permitir la conversión entre varias bibliotecas de terceros, 🤗 Datasets proporciona una función Dataset.set_format(). Esta función solo cambia el formato de salida del conjunto de datos, por lo que puedes cambiar fácilmente a otro formato sin afectar el formato de datos subyacente, que es Apache Arrow. El formato se realiza in situ. Para demostrarlo, convirtamos nuestro conjunto de datos a Pandas:

drug_dataset.set_format("pandas")

Ahora, cuando accedemos a los elementos del conjunto de datos, obtenemos un pandas.DataFrame en lugar de un diccionario:

drug_dataset["train"][:3]
patient_id drugName condition review rating date usefulCount review_length
0 95260 Guanfacine adhd "My son is halfway through his fourth week of Intuniv..." 8.0 April 27, 2010 192 141
1 92703 Lybrel birth control "I used to take another oral contraceptive, which had 21 pill cycle, and was very happy- very light periods, max 5 days, no other side effects..." 5.0 December 14, 2009 17 134
2 138000 Ortho Evra birth control "This is my first time using any form of birth control..." 8.0 November 3, 2015 10 89

Creemos un pandas.DataFrame para todo el conjunto de entrenamiento seleccionando todos los elementos de drug_dataset["train"]:

train_df = drug_dataset["train"][:]

[!TIP] 🚨 Bajo el capó, Dataset.set_format() cambia el formato de retorno para el método dunder __getitem__() del conjunto de datos. Esto significa que cuando queremos crear un nuevo objeto como train_df a partir de un Dataset en el formato "pandas", necesitamos dividir todo el conjunto de datos para obtener un pandas.DataFrame. Puedes verificar por ti mismo que el tipo de drug_dataset["train"] es Dataset, independientemente del formato de salida.

Desde aquí podemos usar toda la funcionalidad de Pandas que queramos. Por ejemplo, podemos hacer encadenamientos sofisticados para calcular la distribución de clases entre las entradas condition:

frequencies = (
    train_df["condition"]
    .value_counts()
    .to_frame()
    .reset_index()
    .rename(columns={"index": "condition", "count": "frequency"})
)
frequencies.head()
condition frequency
0 birth control 27655
1 depression 8023
2 acne 5209
3 anxiety 4991
4 pain 4744

Y una vez que hayamos terminado con nuestro análisis de Pandas, siempre podemos crear un nuevo objeto Dataset usando la función Dataset.from_pandas() de la siguiente manera:

from datasets import Dataset

freq_dataset = Dataset.from_pandas(frequencies)
freq_dataset
Dataset({
    features: ['condition', 'frequency'],
    num_rows: 819
})

[!TIP] ✏️ ¡Pruébalo! Calcula la calificación promedio por medicamento y almacena el resultado en un nuevo Dataset.

Esto concluye nuestro recorrido por las diversas técnicas de preprocesamiento disponibles en 🤗 Datasets. Para finalizar la sección, creemos un conjunto de validación para preparar el conjunto de datos para entrenar un clasificador. Antes de hacerlo, restableceremos el formato de salida de drug_dataset de "pandas" a "arrow":

drug_dataset.reset_format()

Creando un conjunto de validación[[creating-a-validation-set]]

Aunque tenemos un conjunto de prueba que podríamos usar para la evaluación, es una buena práctica dejar el conjunto de prueba intacto y crear un conjunto de validación separado durante el desarrollo. Una vez que estés satisfecho con el rendimiento de tus modelos en el conjunto de validación, puedes realizar una verificación final en el conjunto de prueba. Este proceso ayuda a mitigar el riesgo de que te sobreajustes al conjunto de prueba y despliegues un modelo que falle con datos del mundo real.

🤗 Datasets proporciona una función Dataset.train_test_split() que se basa en la famosa funcionalidad de scikit-learn. Usémosla para dividir nuestro conjunto de entrenamiento en divisiones train y validation (establecemos el argumento seed para reproducibilidad):

drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
# Rename the default "test" split to "validation"
drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
# Add the "test" set to our `DatasetDict`
drug_dataset_clean["test"] = drug_dataset["test"]
drug_dataset_clean
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 110811
    })
    validation: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 27703
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 46108
    })
})

¡Genial, ahora hemos preparado un conjunto de datos listo para entrenar algunos modelos! En la sección 5 te mostraremos cómo subir conjuntos de datos al Hugging Face Hub, pero por ahora, terminemos nuestro análisis viendo algunas formas en que puedes guardar conjuntos de datos en tu máquina local.

Guardar un dataset[[saving-a-dataset]]

Video: youtube.com/watch?v=blF9uxYcKHo

Aunque 🤗 Datasets almacenará en caché cada dataset descargado y las operaciones realizadas en él, hay ocasiones en las que querrás guardar un dataset en el disco (por ejemplo, en caso de que la caché se elimine). Como se muestra en la siguiente tabla, 🤗 Datasets proporciona tres funciones principales para guardar tu dataset en diferentes formatos:

Formato de datos Función
Arrow Dataset.save_to_disk()
CSV Dataset.to_csv()
JSON Dataset.to_json()

Por ejemplo, guardemos nuestro dataset limpio en formato Arrow:

drug_dataset_clean.save_to_disk("drug-reviews")

Esto creará un directorio con la siguiente estructura:

drug-reviews/
├── dataset_dict.json
├── test
│   ├── dataset.arrow
│   ├── dataset_info.json
│   └── state.json
├── train
│   ├── dataset.arrow
│   ├── dataset_info.json
│   ├── indices.arrow
│   └── state.json
└── validation
    ├── dataset.arrow
    ├── dataset_info.json
    ├── indices.arrow
    └── state.json

donde podemos ver que cada división está asociada con su propia tabla dataset.arrow, y algunos metadatos en dataset_info.json y state.json. Puedes pensar en el formato Arrow como una tabla sofisticada de columnas y filas optimizada para construir aplicaciones de alto rendimiento que procesan y transportan grandes datasets.

Una vez que el dataset está guardado, podemos cargarlo usando la función load_from_disk() de la siguiente manera:

from datasets import load_from_disk

drug_dataset_reloaded = load_from_disk("drug-reviews")
drug_dataset_reloaded
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 110811
    })
    validation: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 27703
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 46108
    })
})

Para los formatos CSV y JSON, tenemos que almacenar cada división como un archivo separado. Una forma de hacerlo es iterando sobre las claves y valores en el objeto DatasetDict:

for split, dataset in drug_dataset_clean.items():
    dataset.to_json(f"drug-reviews-{split}.jsonl")

Esto guarda cada división en formato JSON Lines, donde cada fila del dataset se almacena como una sola línea de JSON. Así es como se ve el primer ejemplo:

!head -n 1 drug-reviews-train.jsonl
{"patient_id":141780,"drugName":"Escitalopram","condition":"depression","review":"\"I seemed to experience the regular side effects of LEXAPRO, insomnia, low sex drive, sleepiness during the day. I am taking it at night because my doctor said if it made me tired to take it at night. I assumed it would and started out taking it at night. Strange dreams, some pleasant. I was diagnosed with fibromyalgia. Seems to be helping with the pain. Have had anxiety and depression in my family, and have tried quite a few other medications that haven't worked. Only have been on it for two weeks but feel more positive in my mind, want to accomplish more in my life. Hopefully the side effects will dwindle away, worth it to stick with it from hearing others responses. Great medication.\"","rating":9.0,"date":"May 29, 2011","usefulCount":10,"review_length":125}

Luego podemos usar las técnicas de la sección 2 para cargar los archivos JSON de la siguiente manera:

data_files = {
    "train": "drug-reviews-train.jsonl",
    "validation": "drug-reviews-validation.jsonl",
    "test": "drug-reviews-test.jsonl",
}
drug_dataset_reloaded = load_dataset("json", data_files=data_files)

¡Y eso es todo por nuestra excursión al procesamiento de datos con 🤗 Datasets! Ahora que tenemos un dataset limpio para entrenar un modelo, aquí tienes algunas ideas que podrías probar:

  1. Usa las técnicas del Capítulo 3 para entrenar un clasificador que pueda predecir la condición del paciente basándose en la reseña del medicamento.
  2. Usa el pipeline summarization del Capítulo 1 para generar resúmenes de las reseñas.

A continuación, veremos cómo 🤗 Datasets puede permitirte trabajar con datasets enormes sin sobrecargar tu laptop.

Lección del curso «Hugging Face LLM Course» de Hugging Face, publicado con licencia Apache 2.0. Traducción y adaptación al español de IA con Clase. IA con Clase no está afiliado a Hugging Face. Ver el original · Licencia
Esta lección es gratuita. El resto del curso se abre con la Membresía de IA con Clase, que incluye todos los cursos del catálogo. Ver precios
← AnteriorSiguiente: Carga y procesamiento de datasets grandes →