diff --git a/.github/workflows/build_pr_documentation.yml b/.github/workflows/build_pr_documentation.yml index 45e3b3e09..5ae7db12d 100644 --- a/.github/workflows/build_pr_documentation.yml +++ b/.github/workflows/build_pr_documentation.yml @@ -9,7 +9,7 @@ concurrency: jobs: build: - uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main + uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@use_hf_hub with: commit_sha: ${{ github.event.pull_request.head.sha }} pr_number: ${{ github.event.number }} @@ -18,3 +18,6 @@ jobs: additional_args: --not_python_module languages: ar bn de en es fa fr gj he hi id it ja ko pt ru th tr vi zh-CN zh-TW hub_base_path: https://moon-ci-docs.huggingface.co + secrets: + token: ${{ secrets.HF_DOC_PUSH }} + comment_bot_token: ${{ secrets.HUGGINGFACE_PUSH }} diff --git a/.github/workflows/delete_doc_comment.yml b/.github/workflows/delete_doc_comment.yml index 9ec2aaf44..0ec59d485 100644 --- a/.github/workflows/delete_doc_comment.yml +++ b/.github/workflows/delete_doc_comment.yml @@ -7,7 +7,10 @@ on: jobs: delete: - uses: huggingface/doc-builder/.github/workflows/delete_doc_comment.yml@main + uses: huggingface/doc-builder/.github/workflows/delete_doc_comment.yml@use_hf_hub with: pr_number: ${{ github.event.number }} - package: course \ No newline at end of file + package: course + secrets: + token: ${{ secrets.HF_DOC_PUSH }} + comment_bot_token: ${{ secrets.HUGGINGFACE_PUSH }} \ No newline at end of file diff --git a/chapters/en/chapter2/2.mdx b/chapters/en/chapter2/2.mdx index e0be8610e..f7cd4d4e8 100644 --- a/chapters/en/chapter2/2.mdx +++ b/chapters/en/chapter2/2.mdx @@ -257,7 +257,7 @@ outputs = model(inputs) ``` {/if} -Now if we look at the shape of our inputs, the dimensionality will be much lower: the model head takes as input the high-dimensional vectors we saw before, and outputs vectors containing two values (one per label): +Now if we look at the shape of our outputs, the dimensionality will be much lower: the model head takes as input the high-dimensional vectors we saw before, and outputs vectors containing two values (one per label): ```python print(outputs.logits.shape) diff --git a/chapters/en/chapter3/3.mdx b/chapters/en/chapter3/3.mdx index 85ec90cf4..b10c92965 100644 --- a/chapters/en/chapter3/3.mdx +++ b/chapters/en/chapter3/3.mdx @@ -154,7 +154,7 @@ trainer = Trainer( Note that we create a new `TrainingArguments` with its `evaluation_strategy` set to `"epoch"` and a new model — otherwise, we would just be continuing the training of the model we have already trained. To launch a new training run, we execute: -``` +```py trainer.train() ``` diff --git a/chapters/es/_toctree.yml b/chapters/es/_toctree.yml index 6289440b6..80ab8accd 100644 --- a/chapters/es/_toctree.yml +++ b/chapters/es/_toctree.yml @@ -49,6 +49,26 @@ - local: chapter3/4 title: Entrenamiento completo +- title: 5. La librería 🤗 Datasets + sections: + - local: chapter5/1 + title: Introducción + - local: chapter5/2 + title: ¿Y si mi dataset no está en el Hub? + - local: chapter5/3 + title: Es momento de subdividir + - local: chapter5/4 + title: ¿Big data? 🤗 ¡Datasets al rescate! + - local: chapter5/5 + title: Crea tu propio dataset + - local: chapter5/6 + title: Búsqueda semántica con FAISS + - local: chapter5/7 + title: 🤗 Datasets, ¡listo! + - local: chapter5/8 + title: Quiz + quiz: 5 + - title: 8. ¿Cómo solicitar ayuda? sections: - local: chapter8/1 diff --git a/chapters/es/chapter5/1.mdx b/chapters/es/chapter5/1.mdx new file mode 100644 index 000000000..31a9a4c07 --- /dev/null +++ b/chapters/es/chapter5/1.mdx @@ -0,0 +1,22 @@ +# Introducción + + + +En el [Capítulo 3](/course/chapter3) tuviste tu primer acercamento a la librería 🤗 Datasets y viste que existían 3 pasos principales para ajustar un modelo: + +1. Cargar un conjunto de datos del Hub de Hugging Face. +2. Preprocesar los datos con `Dataset.map()`. +3. Cargar y calcular métricas. + +¡Esto es apenas el principio de lo que 🤗 Datasets puede hacer! En este capítulo vamos a estudiar a profundidad esta librería y responderemos las siguientes preguntas: + +* ¿Qué hacer cuando tu dataset no está en el Hub? +* ¿Cómo puedes subdividir tu dataset? (¿Y qué hacer si _realmente_ necesitas usar Pandas?) +* ¿Qué hacer cuando tu dataset es enorme y consume toda la RAM de tu computador? +* ¿Qué es la proyección en memoria (_memory mapping_) y Apache Arrow? +* ¿Cómo puedes crear tu propio dataset y subirlo al Hub? + +Las técnicas que aprenderás aquí te van a preparar para las tareas de _tokenización_ avanzada y ajuste que verás en el [Capítulo 6](/course/chapter6) y el [Capítulo 7](/course/chapter7). ¡Así que ve por un café y arranquemos! \ No newline at end of file diff --git a/chapters/es/chapter5/2.mdx b/chapters/es/chapter5/2.mdx new file mode 100644 index 000000000..a239ddf53 --- /dev/null +++ b/chapters/es/chapter5/2.mdx @@ -0,0 +1,166 @@ +# ¿Y si mi dataset no está en el Hub? + + + +Ya sabes cómo usar el [Hub de Hugging Face](https://huggingface.co/datasets) para descargar datasets, pero usualmente vas a tener que trabajar con datos que están guardados en tu computador o en un servidor remoto. En esta sección te mostraremos cómo usar 🤗 Datasets para cargar conjuntos de datos que no están disponibles en el Hub de Hugging Face. + + + +## Trabajando con datos locales y remotos + +🤗 Datasets contiene scripts para cargar datasets locales y remotos que soportan formatos comunes de datos como: + +| Formato de datos | Script de carga | Ejemplo | +| :----------------: | :------------: | :-----------------------------------------------------: | +| CSV y TSV | `csv` | `load_dataset("csv", data_files="my_file.csv")` | +| Archivos de texto | `text` | `load_dataset("text", data_files="my_file.txt")` | +| JSON y JSON Lines | `json` | `load_dataset("json", data_files="my_file.jsonl")` | +| Pickled DataFrames | `pandas` | `load_dataset("pandas", data_files="my_dataframe.pkl")` | + +Como ves en la tabla, para cada formato de datos solo tenemos que especificar el tipo de script de carga en la función `load_dataset()`, así como el argumento `data_files` que contiene la ruta a uno o más archivos. Comencemos por cargar un dataset desde archivos locales y luego veremos cómo hacer lo propio para archivos remotos. + +## Cargando un dataset local + +Para este ejemplo, vamos a usar el [dataset SQuAD-it], que es un dataset de gran escala para responder preguntas en italiano. + +Los conjuntos de entrenamiento y de prueba están alojados en GitHub, así que podemos descargarlos fácilmente con el comando `wget`: + +```python +!wget https://github.com/crux82/squad-it/raw/master/SQuAD_it-train.json.gz +!wget https://github.com/crux82/squad-it/raw/master/SQuAD_it-test.json.gz +``` + +Esto va a descargar dos archivos comprimidos llamados *SQuAD_it-train.json.gz* y *SQuAD_it-test.json.gz*, que podemos descomprimir con el comando `gzip` de Linux: + +```python +!gzip -dkv SQuAD_it-*.json.gz +``` + +```bash +SQuAD_it-test.json.gz: 87.4% -- replaced with SQuAD_it-test.json +SQuAD_it-train.json.gz: 82.2% -- replaced with SQuAD_it-train.json +``` + +De este modo, podemos ver que los archivos comprimidos son reemplazados por los archuvos en formato JSON _SQuAD_it-train.json_ y _SQuAD_it-test.json_. + + + +✎ Si te preguntas por qué hay un caracter de signo de admiración (`!`) en los comandos de shell, esto es porque los estamos ejecutando desde un cuaderno de Jupyter. Si quieres descargar y descomprimir el archivo directamente desde la terminal, elimina el signo de admiración. + + + +Para cargar un archivo JSON con la función `load_dataset()`, necesitamos saber si estamos trabajando con un archivo JSON ordinario (parecido a un diccionario anidado) o con JSON Lines (JSON separado por líneas). Como muchos de los datasets de respuesta a preguntas que te vas a encontrar, SQuAD-it usa el formato anidado, en el que el texto está almacenado en un campo `data`. Esto significa que podemos cargar el dataset especificando el argumento `field` de la siguiente manera: + +```py +from datasets import load_dataset + +squad_it_dataset = load_dataset("json", data_files="SQuAD_it-train.json", field="data") +``` + +Por defecto, cuando cargas archivos locales se crea un objeto `DatasetDict` con un conjunto de entrenamiento –`train`–. Podemos verlo al inspeccionar el objeto `squad_it_dataset`: + +```py +squad_it_dataset +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['title', 'paragraphs'], + num_rows: 442 + }) +}) +``` + +Esto nos muestra el número de filas y los nombres de las columnas asociadas al conjunto de entrenamiento. Podemos ver uno de los ejemplos al poner un índice en el conjunto de entrenamiento así: + +```py +squad_it_dataset["train"][0] +``` + +```python out +{ + "title": "Terremoto del Sichuan del 2008", + "paragraphs": [ + { + "context": "Il terremoto del Sichuan del 2008 o il terremoto...", + "qas": [ + { + "answers": [{"answer_start": 29, "text": "2008"}], + "id": "56cdca7862d2951400fa6826", + "question": "In quale anno si è verificato il terremoto nel Sichuan?", + }, + ... + ], + }, + ... + ], +} +``` + +¡Genial, ya cargamos nuestro primer dataset local! Sin embargo, esto funcionó únicamente para el conjunto de entrenamiento. Realmente, queremos incluir tanto el conjunto `train` como el conjunto `test` en un único objeto `DatasetDict` para poder aplicar las funciones `Dataset.map()` en ambos conjuntos al mismo tiempo. Para hacerlo, podemos incluir un diccionario en el argumento `datafiles` que mapea cada nombre de conjunto a su archivo asociado: + + +```py +data_files = {"train": "SQuAD_it-train.json", "test": "SQuAD_it-test.json"} +squad_it_dataset = load_dataset("json", data_files=data_files, field="data") +squad_it_dataset +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['title', 'paragraphs'], + num_rows: 442 + }) + test: Dataset({ + features: ['title', 'paragraphs'], + num_rows: 48 + }) +}) +``` + +Esto es exactamente lo que queríamos. Ahora podemos aplicar varias técnicas de preprocesamiento para limpiar los datos, _tokenizar_ las reseñas, entre otras tareas. + + + +El argumento `data_files` de la función `load_dataset()` es muy flexible. Puede ser una única ruta de archivo, una lista de rutas o un diccionario que mapee los nombres de los conjuntos a las rutas de archivo. También puedes buscar archivos que cumplan con cierto patrón específico de acuerdo con las reglas usadas por el shell de Unix (e.g., puedes buscar todos los archivos JSON en una carpeta al definir `datafiles="*.json"`). Revisa la [documentación](https://huggingface.co/docs/datasets/loading.html#local-and-remote-files) para más detalles. + + + +Los scripts de carga en 🤗 Datasets también pueden descomprimir los archivos de entrada automáticamente, así que podemos saltarnos el uso de `gzip` especificando el argumento `data_files` directamente a la ruta de los archivos comprimidos. + +```py +data_files = {"train": "SQuAD_it-train.json.gz", "test": "SQuAD_it-test.json.gz"} +squad_it_dataset = load_dataset("json", data_files=data_files, field="data") +``` + +Esto puede ser útil si no quieres descomprimir manualmente muchos archivos GZIP. La descompresión automática también aplica para otros formatos de archivo comunes como TAR y ZIP, así que solo necesitas dirigir el argumento `data_files` a los archivos comprimidos y ¡listo!. + +Ahora que sabes cómo cargar archivos locales en tu computador portátil o de escritorio, veamos cómo cargar archivos remotos. + +## Cargando un dataset remoto + +Si estás trabajando como científico de datos o desarrollador en una compañía, hay una alta probabilidad de que los datasets que quieres analizar estén almacenados en un servidor remoto. Afortunadamente, ¡la carga de archivos remotos es tan fácil como cargar archivos locales! En vez de incluir una ruta de archivo, dirigimos el argumento `data_files` de la función `load_datasets()` a una o más URL en las que estén almacenados los archivos. Por ejemplo, para el dataset SQuAD-it alojado en GitHub, podemos apuntar `data_files` a las URL de _SQuAD_it-*.json.gz_ así: + +```py +url = "https://github.com/crux82/squad-it/raw/master/" +data_files = { + "train": url + "SQuAD_it-train.json.gz", + "test": url + "SQuAD_it-test.json.gz", +} +squad_it_dataset = load_dataset("json", data_files=data_files, field="data") +``` + +Esto devuelve el mismo objeto `DatasetDict` que obtuvimos antes, pero nos ahorra el paso de descargar y descomprimir manualmente los archivos _SQuAD_it-*.json.gz_. Con esto concluimos nuestra exploración de las diferentes maneras de cargar datasets que no están alojados en el Hub de Hugging Face. Ahora que tenemos un dataset para experimentar, ¡pongámonos manos a la obra con diferentes técnicas de procesamiento de datos! + + + +✏️ **¡Inténtalo!** Escoge otro dataset alojado en GitHub o en el [Repositorio de Machine Learning de UCI](https://archive.ics.uci.edu/ml/index.php) e intenta cargarlo local y remotamente usando las técnicas descritas con anterioridad. Para puntos extra, intenta cargar un dataset que esté guardado en un formato CSV o de texto (revisa la [documentación](https://huggingface.co/docs/datasets/loading.html#local-and-remote-files) pata tener más información sobre estos formatos). + + diff --git a/chapters/es/chapter5/3.mdx b/chapters/es/chapter5/3.mdx new file mode 100644 index 000000000..241916c9d --- /dev/null +++ b/chapters/es/chapter5/3.mdx @@ -0,0 +1,741 @@ +# Es momento de subdividir + + + +La mayor parte del tiempo tus datos no estarán perfectamente listos para entrenar modelos. En esta sección vamos a explorar distintas funciones que tiene 🤗 Datasets para limpiar tus conjuntos de datos. + + + +## Subdivdiendo nuestros datos + +De manera similar a Pandas, 🤗 Datasets incluye varias funciones para manipular el contenido de los objetos `Dataset` y `DatasetDict`. Ya vimos el método `Dataset.map()` en el [Capítulo 3](/course/chapter3) y en esta sección vamos a explorar otras funciones que tenemos a nuestra disposición. + +Para este ejemplo, vamos a usar el [Dataset de reseñas de medicamentos](https://archive.ics.uci.edu/ml/datasets/Drug+Review+Dataset+%28Drugs.com%29) alojado en el [Repositorio de Machine Learning de UC Irvine](https://archive.ics.uci.edu/ml/index.php), que contiene la evaluación de varios medicamentos por parte de pacientes, junto con la condición por la que los estaban tratando y una calificación en una escala de 10 estrellas sobre su satisfacción. + +Primero, tenemos que descargar y extraer los datos, que se puede hacer con los comandos `wget` y `unzip`: + +```py +!wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip" +!unzip drugsCom_raw.zip +``` + +Dado que TSV es una variación de CSV en la que se usan tabulaciones en vez de comas como separadores, 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: + +```py +from datasets import load_dataset + +data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"} +# \t es el caracter para tabulaciones en Python +drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t") +``` + +Una buena práctica al hacer cualquier tipo de análisis de datos es tomar una muestra aleatoria del dataset para tener una vista rápida del tipo de datos con los que estás trabajando. En 🤗 Datasets, podemos crear una muestra aleatoria al encadenar las funciones `Dataset.shuffle()` y `Dataset.select()`: + +```py +drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000)) +# Mirar los primeros ejemplos +drug_sample[:3] +``` + +```python out +{'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]} +``` + +Puedes ver que hemos fijado la semilla en `Dataset.shuffle()` por motivos de reproducibilidad. `Dataset.select()` espera un interable de índices, así que incluimos `range(1000)` para tomar los primeros 1.000 ejemplos del conjunto de datos aleatorizado. Ya podemos ver algunos detalles para esta muestra: + +* La columna `Unnamed: 0` se ve sospechosamente como un ID anonimizado para cada paciente. +* La columna `condition` incluye una mezcla de niveles en mayúscula y minúscula. +* Las reseñas tienen longitud variable y contienen una mezcla de separadores de línea de Python (`\r\n`), así como caracteres de HTML como `&\#039;`. + +Veamos cómo podemos usar 🤗 Datasets para lidiar con cada uno de estos asuntos. Para probar la hipótesis de que la columna `Unnamed: 0` es un ID de los pacientes, podemos usar la función `Dataset.unique()` para verificar que el número de los ID corresponda con el número de filas de cada conjunto: + +```py +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 el dataset un poco al cambiar el nombre de la columna `Unnamed: 0` a algo más legible. Podemos usar la función `DatasetDict.rename_column()` para renombrar la columna en ambos conjuntos en una sola operación: + +```py +drug_dataset = drug_dataset.rename_column( + original_column_name="Unnamed: 0", new_column_name="patient_id" +) +drug_dataset +``` + +```python out +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 + }) +}) +``` + + + +✏️ **¡Inténtalo!** Usa la función `Dataset.unique()` para encontrar el número de medicamentos y condiciones únicas en los conjuntos de entrenamiento y de prueba. + + + +Ahora normalicemos todas las etiquetas de `condition` usando `Dataset.map()`. Tal como lo hicimos con la tokenización en el [Capítulo 3](/course/chapter3), podemos definir una función simple que pueda ser aplicada en todas las filas de cada conjunto en el `drug_dataset`: + +```py +def lowercase_condition(example): + return {"condition": example["condition"].lower()} + + +drug_dataset.map(lowercase_condition) +``` + +```python out +AttributeError: 'NoneType' object has no attribute 'lower' +``` + +¡Tenemos un problema en nuestra función de mapeo! Del error podemos inferir que algunas de las entradas de la columna `condición` son `None`, que no puede transformarse en minúscula al no ser un string. Filtremos estas filas usando `Dataset.filter()`, que funciona de una forma similar `Dataset.map()` y recibe como argumento una función que toma un ejemplo particular del dataset. En vez de escribir una función explícita como: + +```py +def filter_nones(x): + return x["condition"] is not None +``` + +y luego ejecutar `drug_dataset.filter(filter_nones)`, podemos hacerlo en una línea usando una _función lambda_. En Python, las funciones lambda son funciones pequeñas que puedes definir sin nombrarlas explícitamente. Estas toman la forma general: + +``` +lambda : +``` + +en la que `lambda` es una de las [palabras especiales](https://docs.python.org/3/reference/lexical_analysis.html#keywords) de Python, `` es una lista o conjunto de valores separados con coma que definen los argumentos de la función y `` representa las operaciones que quieres ejecutar. Por ejemplo, podemos definir una función lambda simple que eleve un número al cuadrado de la siguiente manera: + +``` +lambda x : x * x +``` + +Para aplicar esta función a un _input_, tenemos que envolverla a ella y al _input_ en paréntesis: + +```py +(lambda x: x * x)(3) +``` + +```python out +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 así: + +```py +(lambda base, height: 0.5 * base * height)(4, 8) +``` + +```python out +16.0 +``` + +Las funciones lambda son útiles cuando quieres definir funciones pequeñas de un único uso (para más información sobre ellas, te recomendamos leer este excelente [tutorial de Real Python](https://realpython.com/python-lambda/) escrito por Andre Burgaud). En el contexto de 🤗 Datasets, podemos usar las funciones lambda para definir operaciones simples de mapeo y filtrado, así que usemos este truco para eliminar las entradas `None` de nuestro dataset: + +```py +drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None) +``` + +Ahora que eliminamos los `None`, podemos normalizar nuestra columna `condition`: + +```py +drug_dataset = drug_dataset.map(lowercase_condition) +# Revisar que se pasaron a minúscula +drug_dataset["train"]["condition"][:3] +``` + +```python out +['left ventricular dysfunction', 'adhd', 'birth control'] +``` + +¡Funcionó! Como ya limpiamos las etiquetas, veamos cómo podemos limpiar las reseñas. + +## Creando nuevas columnas + +Cuando estás lidiando con reseñas de clientes, es una buena práctica revisar el número de palabras de cada reseña. Una reseña puede ser una única palabra como "¡Genial!" o un ensayo completo con miles de palabras y, según el caso de uso, tendrás que abordar estos extremos de forma diferente. Para calcular el número de palabras en cada reseña, usaremos una heurística aproximada basada en dividir cada texto por los espacios en blanco. + +Definamos una función simple que cuente el número de palabras en cada reseña: + +```py +def compute_review_length(example): + return {"review_length": len(example["review"].split())} +``` + +Contrario a la función `lowercase_condition()`, `compute_review_length()` devuelve un diccionario cuya llave no corresponde a uno de los nombres de las columnas en el conjunto de datos. En este caso, cuando se pasa `compute_review_length()` a `Dataset.map()`, la función se aplicará a todas las filas en el dataset para crear una nueva columna `review_length()`: + +```py +drug_dataset = drug_dataset.map(compute_review_length) +# Inspeccionar el primer ejemplo de entrenamiento +drug_dataset["train"][0] +``` + +```python out +{'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} +``` + +Tal como lo esperábamos, podemos ver que se añadió la columna `review_length` al conjunto de entrenamiento. Podemos ordenar esta columna nueva con `Dataset.sort()` para ver cómo son los valores extremos: + +```py +drug_dataset["train"].sort("review_length")[:3] +``` + +```python out +{'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 lo discutimos anteriormente, algunas reseñas incluyen una sola palabra, que si bien puede ser útil para el análisis de sentimientos, no sería tan informativa si quisieramos predecir la condición. + + + +🙋 Una forma alternativa de añadir nuevas columnas al dataset es a través de la función `Dataset.add_column()`. Esta te permite incluir la columna como una lista de Python o un array de NumPy y puede ser útil en situaciones en las que `Dataset.map()` no se ajusta a tu caso de uso. + + + +Usemos la función `Dataset.filter()` para quitar las reseñas que contienen menos de 30 palabras. Similar a lo que hicimos con la columna `condition`, podemos filtrar las reseñas cortas al incluir una condición de que su longitud esté por encima de este umbral: + +```py +drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30) +print(drug_dataset.num_rows) +``` + +```python out +{'train': 138514, 'test': 46108} +``` + +Como puedes ver, esto ha eliminado alrededor del 15% de las reseñas de nuestros conjuntos originales de entrenamiento y prueba. + + + +✏️ **¡Inténtalo!** Usa la función `Dataset.sort()` para inspeccionar las reseñas con el mayor número de palabras. Revisa la [documentación](https://huggingface.co/docs/datasets/package_reference/main_classes.html#datasets.Dataset.sort) para ver cuál argumento necesitas para ordenar las reseñas de mayor a menor. + + + +Por último, tenemos que lidiar con la presencia de códigos de caracteres HTML en las reseñas. Podemos usar el módulo `html` de Python para transformar estos códigos así: + +```py +import html + +text = "I'm a transformer called BERT" +html.unescape(text) +``` + +```python out +"I'm a transformer called BERT" +``` + +Usaremos `Dataset.map()` para transformar todos los caracteres HTML en el corpus: + +```python +drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])}) +``` + +Como puedes ver, el método `Dataset.map()` es muy útil para procesar datos y esta es apenas la punta del iceberg de lo que puede hacer. + +## Los superpoderes del método `map()` + +El método `Dataset.map()` recibe un argumento `matched` que, al definirse como `True`, envía un lote de ejemplos a la función de mapeo a la vez (el tamaño del lote se puede configurar, pero tiene un valor por defecto de 1.000). Por ejemplo, la función anterior de mapeo que transformó todos los HTML se demoró un poco en su ejecución (puedes leer el tiempo en las barras de progreso). Podemos reducir el tiempo al procesar varios elementos a la vez usando un _list comprehension_. + +Cuando especificas `batched=True`, la función recibe un diccionario con los campos del dataset, pero cada valor es ahora una _lista de valores_ y no un valor individual. La salida de `Dataset.map()` debería ser igual: un diccionario con los campos que queremos actualizar o añadir a nuestro dataset y una lista de valores. Por ejemplo, aquí puedes ver otra forma de transformar todos los caracteres HTML usando `batched=True`: + +```python +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 cuaderno, verás que este comando se ejecuta mucho más rápido que el anterior. Y no es porque los caracteres HTML de las reseñas ya se hubieran procesado; si vuelves a ejecutar la instrucción de la sección anterior (sin `batched=True`), se tomará el mismo tiempo de ejecución que antes. Esto es porque las _list comprehensions_ suelen ser más rápidas que ejecutar el mismo código en un ciclo `for` y porque también ganamos rendimiento al acceder a muchos elementos a la vez en vez de uno por uno. + +Usar `Dataset.map()` con `batched=True` será fundamental para desbloquear la velocidad de los tokenizadores "rápidos" que nos vamos a encontrar en el [Capítulo 6](/course/chapter6), que pueden tokenizar velozmente 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 la siguiente: + +```python +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](/course/chapter3), podemos pasar uno o varios ejemplos al tokenizador, así que podemos usar esta función con o sin `batched=True`. Aprovechemos esta oportunidad para comparar el desempeño de las distintas opciones. En un cuaderno, puedes medir el tiempo de ejecución de una instrucción de una línea añadiendo `%time` antes de la línea de código de tu interés: + +```python no-format +%time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True) +``` + +También puedes medir el tiempo de una celda completa añadiendo `%%time` al inicio de la celda. En el hardware en el que lo ejecutamos, nos arrojó 10.8s para esta instrucción (es el número que aparece después de "Wall time"). + + + +✏️ **¡Inténtalo!** Ejecuta la misma instrucción con y sin `batched=True` y luego usa un tokenizador "lento" (añade `use_fast=False` en el método `AutoTokenizer.from_pretrained()`) para ver cuánto tiempo se toman en tu computador. + + + +Estos son los resultados que obtuvimos con y sin la ejecución por lotes, con un tokenizador rápido y 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 usar lotes. ¡Realmente impresionante! Esta es la razón principal por la que los tokenizadores rápidos son la opción por defecto al usar `AutoTokenizer` (y por qué se denominan "rápidos"). Estos logran tal rapidez gracias a que el código de los tokenizadores corre en Rust, que es un lenguaje que facilita la ejecución del código en paralelo. + +La paralelización también es la razón para el incremento de 6x en la velocidad del tokenizador al ejecutarse por lotes: No puedes ejecutar una única operacón de tokenización en paralelo, pero cuando quieres tokenizar muchos textos al mismo tiempo puedes dividir la ejecución en diferentes procesos, cada uno responsable de sus propios textos. + +`Dataset.map()` también tiene algunas capacidades de paralelización. Dado que no funcionan con Rust, no van a hacer que un tokenizador lento alcance el rendimiento de uno rápido, pero aún así 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 para usar en `Dataset.map()`: + +```py +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) +``` + +También puedes medir el tiempo para determinar el número de procesos que vas a usar. En nuestro caso, usar 8 procesos produjo la mayor ganancia de velocidad. Aquí están algunos de los números que obtuvimos con y sin multiprocesamiento: + +Opciones | Tokenizador rápido | Rokenizador 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 + +Estos son resultados mucho más razonables para el tokenizador lento, aunque el desempeño del rápido también mejoró sustancialmente. Sin embargo, este no siempre será el caso: para valores de `num_proc` diferentes a 8, nuestras pruebas mostraron que era más rápido usar `batched=true` sin esta opción. En general, no recomendamos usar el multiprocesamiento de Python para tokenizadores rápidos con `batched=True`. + + + +Usar `num_proc` para acelerar tu procesamiento suele ser una buena idea, siempre y cuando la función que uses no esté usando multiples procesos por si misma. + + + +Que toda esta funcionalidad está incluida en un método es algo impresionante en si mismo, ¡pero hay más!. Con `Dataset.map()` y `batched=True` puedes cambiar el número de elementos en tu dataset. Esto es súper útil en situaciones en las que quieres crear varias características de entrenamiento de un ejemplo, algo que haremos en el preprocesamiento para varias de las tareas de PLN que abordaremos en el [Capítulo 7](/course/chapter7). + + + +💡 Un _ejemplo_ en Machine Learning se suele definir como el conjunto de _features_ que le damos al modelo. En algunos contextos estos features serán el conjuto de columnas en un `Dataset`, mientras que en otros se pueden extraer mútiples features de un solo ejemplo que pertenecen a una columna –como aquí y en tareas de responder preguntas-. + + + +¡Veamos cómo funciona! En este ejemplo vamos a tokenizar nuestros ejemplos y limitarlos a una longitud máxima de 128, pero le pediremos al tokenizador que devuelva *todos* los fragmentos de texto en vez de unicamente el primero. Esto se puede lograr con el argumento `return_overflowing_tokens=True`: + +```py +def tokenize_and_split(examples): + return tokenizer( + examples["review"], + truncation=True, + max_length=128, + return_overflowing_tokens=True, + ) +``` + +Probémoslo en un ejemplo puntual antes de usar `Dataset.map()` en todo el dataset: + +```py +result = tokenize_and_split(drug_dataset["train"][0]) +[len(inp) for inp in result["input_ids"]] +``` + +```python out +[128, 49] +``` + +El primer ejemplo en el conjunto de entrenamiento se convirtió en dos features porque fue tokenizado en un número superior de tokens al que especificamos: el primero de longitud 128 y el segundo de longitud 49. ¡Vamos a aplicarlo a todo el dataset! + +```py +tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True) +``` + +```python out +ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000 +``` + +¿Por qué no funcionó? El mensaje de error nos da una pista: hay un desajuste en las longitudes de una de las columnas, siendo una de logitud 1.463 y otra de longitud 1.000. Si has revisado la [documentación de `Dataset.map()`](https://huggingface.co/docs/datasets/package_reference/main_classes.html#datasets.Dataset.map), te habrás dado cuenta que estamos mapeando el número de muestras que le pasamos a la función: en este caso los 1.000 ejemplos nos devuelven 1.463 features, arrojando un error. + +El problema es que estamos tratando de mezclar dos datasets de tamaños diferentes: las columnas de `drug_dataset` tendrán un cierto número de ejemplos (los 1.000 en el error), pero el `tokenized_dataset` que estamos construyendo tendrá más (los 1.463 en el mensaje de error). Esto no funciona para un `Dataset`, así que tenemos que eliminar las columnas del anterior dataset o volverlas del mismo tamaño del nuevo. Podemos hacer la primera operación con el argumento `remove_columns`: + +```py +tokenized_dataset = drug_dataset.map( + tokenize_and_split, batched=True, remove_columns=drug_dataset["train"].column_names +) +``` + +Ahora funciona sin errores. Podemos revisar que nuestro dataset nuevo tiene más elementos que el original al comparar sus longitudes: + +```py +len(tokenized_dataset["train"]), len(drug_dataset["train"]) +``` + +```python out +(206772, 138514) +``` + +También mencionamos que podemos trabajar con el problema de longitudes que no coinciden al convertir las columnas viejas en el mismo tamaño de las nuevas. Para eso, vamos a necesitar el campo `overflow_to_sample_mapping` que devuelve el tokenizer cuando definimos `return_overflowing_tokens=True`. Esto devuelve un mapeo del índice de un nuevo feature al índice de la muestra de la que se originó. Usando lo anterior, podemos asociar cada llave presente en el dataset original con una lista de valores del tamaño correcto al repetir los valores de cada ejemplo tantas veces como genere nuevos features: + +```py +def tokenize_and_split(examples): + result = tokenizer( + examples["review"], + truncation=True, + max_length=128, + return_overflowing_tokens=True, + ) + # Extraer el mapeo entre los índices nuevos y viejos + 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 +``` + +De esta forma, podemos ver que funciona con `Dataset.map()` sin necesidad de eliminar las columnas viejas. + +```py +tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True) +tokenized_dataset +``` + +```python out +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 + }) +}) +``` + +Como resultado, tenemos el mismo número de features de entrenamiento que antes, pero conservando todos los campos anteriores. Quizás prefieras usar esta opción si necesitas conservarlos para algunas tareas de post-procesamiento después de aplicar tu modelo. + +Ya has visto como usar 🤗 Datasets para preprocesar un dataset de varias formas. Si bien las funciones de procesamiento de 🤗 Datasets van a suplir la mayor parte de tus necesidades de entrenamiento de modelos, hay ocasiones en las que puedes necesitar Pandas para tener acceso a herramientas más poderosas, como `DataFrame.groupby()` o algún API de alto nivel para visualización. Afortunadamente, 🤗 Datasets está diseñado para ser interoperable con librerías como Pandas, NumPy, PyTorch, TensoFlow y JAX. Veamos cómo funciona. + +## De `Dataset`s a `DataFrame`s y viceversa + + + +Para habilitar la conversión entre varias librerías de terceros, 🤗 Datasets provee la función `Dataset.set_format()`. Esta función sólo cambia el _formato de salida_ del dataset, de tal manera que puedas cambiar a otro formato sin cambiar el _formato de datos subyacente_, que es Apache Arrow. Este cambio de formato se hace _in place_. Para verlo en acción, convirtamos el dataset a Pandas: + +```py +drug_dataset.set_format("pandas") +``` + +Ahora, cuando accedemos a los elementos del dataset obtenemos un `pandas.DataFrame` en vez de un diccionario: + +```py +drug_dataset["train"][:3] +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
patient_iddrugNameconditionreviewratingdateusefulCountreview_length
095260Guanfacineadhd"My son is halfway through his fourth week of Intuniv..."8.0April 27, 2010192141
192703Lybrelbirth 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.0December 14, 200917134
2138000Ortho Evrabirth control"This is my first time using any form of birth control..."8.0November 3, 20151089
+ +Creemos un `pandas.DataFrame` para el conjunto de entrenamiento entero al seleccionar los elementos de `drug_dataset["train"]`: + +```py +train_df = drug_dataset["train"][:] +``` + + + +🚨 Internamente, `Dataset.set_format()` cambia el formato de devolución del método _dunder_ `__getitem()__`. Esto significa que cuando queremos crear un objeto nuevo como `train_df` de un `Dataset` en formato `"pandas"`, tenemos que seleccionar el dataset completo para obtener un `pandas.DataFrame`. Puedes verificar por ti mismo que el tipo de `drug_dataset["train"]` es `Dataset` sin importar el formato de salida. + + + +De aquí en adelante podemos usar toda la funcionalidad de pandas cuando queramos. Por ejemplo, podemos hacer un encadenamiento sofisticado para calcular la distribución de clase entre las entradas de `condition`: + +```py +frequencies = ( + train_df["condition"] + .value_counts() + .to_frame() + .reset_index() + .rename(columns={"index": "condition", "condition": "frequency"}) +) +frequencies.head() +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
conditionfrequency
0birth control27655
1depression8023
2acne5209
3anxiety4991
4pain4744
+ +Y una vez hemos concluido el análisis con Pandas, tenemos la posibilidad de crear un nuevo objeto `Dataset` usando la función `Dataset.from_pandas()` de la siguiente manera: + +```py +from datasets import Dataset + +freq_dataset = Dataset.from_pandas(frequencies) +freq_dataset +``` + +```python out +Dataset({ + features: ['condition', 'frequency'], + num_rows: 819 +}) +``` + + + +✏️ **¡Inténtalo!** Calcula la calificación promedio por medicamento y guarda el resultado en un nuevo `Dataset`. + + + +Con esto terminamos nuestro tour de las múltiples técnicas de preprocesamiento disponibles en 🤗 Datasets. Para concluir, creemos un set de validación para preparar el conjunto de datos y entrenar el clasificador. Antes de hacerlo, vamos a reiniciar el formato de salida de `drug_dataset` de `"pandas"` a `"arrow"`: + +```python +drug_dataset.reset_format() +``` + +## Creando un conjunto de validación + +Si bien 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 aparte durante el desarrollo. Una vez estés satisfecho con el desempeño de tus modelos en el conjunto de validación, puedes hacer un último chequeo con el conjunto de prueba. Este proceso ayuda a reducir el riesgo de sobreajustar al conjunto de prueba y desplegar un modelo que falle en datos reales. + +🤗 Datasets provee la función `Dataset.train_test_split()` que está basada en la famosa funcionalidad de `scikit-learn`. Usémosla para separar nuestro conjunto de entrenamiento en dos partes `train` y `validation` (definiendo el argumento `seed` por motivos de reproducibilidad): + +```py +drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42) +# Renombrar el conjunto "test" a "validation" +drug_dataset_clean["validation"] = drug_dataset_clean.pop("test") +# Añadir el conjunto "test" al `DatasetDict` +drug_dataset_clean["test"] = drug_dataset["test"] +drug_dataset_clean +``` + +```python out +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 + }) +}) +``` + +Súper, ya preparamos un dataset que está listo para entrenar modelos. En la [sección 5](/course/chapter5/5) veremos cómo subir datasets al Hub de Hugging Face, pero por ahora terminemos el análisis estudiando algunas formas de guardarlos en tu máquina local. + +## Saving a dataset + + + +A pesar de que 🤗 Datasets va a guardar en caché todo dataset que descargues, así como las operaciones que se ejecutan en él, hay ocasiones en las que querrás guardar un dataset en memoria (e.g., en caso que el caché se elimine). Como se muestra en la siguiente tabla, 🤗 Datasets tiene 3 funciones para guardar tu dataset en distintos formatos: + + +| Formato | Función | +| :---------: | :--------------------: | +| Arrow | `Dataset.save_to_disk()` | +| CSV | `Dataset.to_csv()` | +| JSON | `Dataset.to_json()` | + +Por ejemplo, guardemos el dataset limpio en formato Arrow: + +```py +drug_dataset_clean.save_to_disk("drug-reviews") +``` + +Esto creará una carpeta 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 +``` + +en las que podemos ver que cada parte del dataset está asociada con una 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 que está optimizada para construir aplicaciones de alto rendimiento que procesan y transportan datasets grandes. + +Una vez el dataset está guardado, podemos cargarlo usando la función `load_from_disk()` así: + +```py +from datasets import load_from_disk + +drug_dataset_reloaded = load_from_disk("drug-reviews") +drug_dataset_reloaded +``` + +```python out +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 guardar cada parte en un archivo separado. Una forma de hacerlo es iterando sobre las llaves y valores del objeto `DatasetDict`: + +```py +for split, dataset in drug_dataset_clean.items(): + dataset.to_json(f"drug-reviews-{split}.jsonl") +``` + +Esto guarda cada parte en formato [JSON Lines](https://jsonlines.org), donde cada fila del dataset está almacenada como una única línea de JSON. Así se ve el primer ejemplo: + +```py +!head -n 1 drug-reviews-train.jsonl +``` + +```python out +{"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} +``` + +Podemos usar las técnicas de la [sección 2](/course/chapter5/2) para cargar los archivos JSON de la siguiente manera: + +```py +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) +``` + +Esto es todo lo que vamos a ver en nuestra revisión del manejo de datos con 🤗 Datasets. Ahora que tenemos un dataset limpio para entrenar un modelo, aquí van algunas ideas que podrías intentar: + +1. Usa las técnicas del [Capítulo 3](/course/chapter3) para entrenar un clasificador que pueda predecir la condición del paciente con base en las reseñas de los medicamentos. +2. Usa el pipeline de `summarization` del [Capítulo 1](/course/chapter1) para generar resúmenes de las reseñas. + +En la siguiente sección veremos cómo 🤗 Datasets te puede ayudar a trabajar con datasets enormes ¡sin explotar tu computador! diff --git a/chapters/es/chapter5/4.mdx b/chapters/es/chapter5/4.mdx new file mode 100644 index 000000000..344fb0545 --- /dev/null +++ b/chapters/es/chapter5/4.mdx @@ -0,0 +1,286 @@ +# ¿Big data? 🤗 ¡Datasets al rescate! + + + +Hoy en día es común que tengas que trabajar con dataset de varios GB, especialmente si planeas pre-entrenar un transformador como BERT o GPT-2 desde ceros. En estos casos, _solamente cargar_ los datos puede ser un desafío. Por ejemplo, el corpus de WebText utilizado para preentrenar GPT-2 consiste de más de 8 millones de documentos y 40 GB de texto. ¡Cargarlo en la RAM de tu computador portatil le va a causar un paro cardiaco! + +Afortunadamente, 🤗 Datasets está diseñado para superar estas limitaciones: te libera de problemas de manejo de memoria al tratar los datasets como archivos _proyectados en memoria_ (_memory-mapped_) y de límites de almacenamiento al hacer _streaming_ de las entradas en un corpus. + + + +En esta sección vamos a explorar estas funcionalidades de 🤗 Datasets con un corpus enorme de 825 GB conocido como el [Pile](https://pile.eleuther.ai). ¡Comencemos! + +## ¿Qué es el Pile? + +El _Pile_ es un corpus de textos en inglés creado por [EleutherAI](https://www.eleuther.ai) para entrenar modelos de lenguaje de gran escala. Incluye una selección diversa de datasets que abarca artículos científicos, repositorios de código de Github y texto filtrado de la web. El corpus de entrenamiento está disponible en [partes de 14 GB](https://mystic.the-eye.eu/public/AI/pile/) y también puedes descargar varios de los [componentes individuales](https://mystic.the-eye.eu/public/AI/pile_preliminary_components/). Arranquemos viendo el dataset de los abstracts de PubMed, un corpus de abstracts de 15 millones de publicaciones biomédicas en [PubMed](https://pubmed.ncbi.nlm.nih.gov/). Este dataset está en formato [JSON Lines](https://jsonlines.org) y está comprimido con la librería `zstandard`, así que primero tenemos que instalarla: + +```py +!pip install zstandard +``` + +A continuación, podemos cargar el dataset usando el método para archivos remotos que aprendimos en la [sección 2](/course/chapter5/2): + +```py +from datasets import load_dataset + +# Esto toma algunos minutos para ejecutarse, así que ve por un te o un café mientras esperas :) +data_files = "https://mystic.the-eye.eu/public/AI/pile_preliminary_components/PUBMED_title_abstracts_2019_baseline.jsonl.zst" +pubmed_dataset = load_dataset("json", data_files=data_files, split="train") +pubmed_dataset +``` + +```python out +Dataset({ + features: ['meta', 'text'], + num_rows: 15518009 +}) +``` + +Como podemos ver, hay 15.518.009 filas y dos columnas en el dataset, ¡un montón! + + + +✎ Por defecto, 🤗 Datasets va a descomprimir los archivos necesarios para cargar un dataset. Si quieres ahorrar espacio de almacenamiento, puedes usar `DownloadConfig(delete_extracted=True)` al argumento `download_config` de `load_dataset()`. Revisa la [documentación](https://huggingface.co/docs/datasets/package_reference/builder_classes.html?#datasets.utils.DownloadConfig) para más detalles. + + + +Veamos el contenido del primer ejemplo: + +```py +pubmed_dataset[0] +``` + +```python out +{'meta': {'pmid': 11409574, 'language': 'eng'}, + 'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection.\nTo determine the prevalence of hypoxaemia in children aged under 5 years suffering acute lower respiratory infections (ALRI), the risk factors for hypoxaemia in children under 5 years of age with ALRI, and the association of hypoxaemia with an increased risk of dying in children of the same age ...'} +``` + +Ok, esto parece el abstract de un artículo médico. Ahora miremos cuánta RAM hemos usado para cargar el dataset. + +## La magia de la proyección en memoria + +Una forma simple de medir el uso de memoria en Python es con la librería [`psutil`](https://psutil.readthedocs.io/en/latest/), que se puede instalar con `pip` así: + +```python +!pip install psutil +``` + +Esta librería contiene una clase `Process` que nos permite revisar el uso de memoria del proceso actual: + +```py +import psutil + +# Process.memory_info está expresado en bytes, así que lo convertimos en megabytes +print(f"RAM used: {psutil.Process().memory_info().rss / (1024 * 1024):.2f} MB") +``` + +```python out +RAM used: 5678.33 MB +``` + +El atributo `rss` se refiere al _resident set size_, que es la fracción de memoria que un proceso ocupa en RAM. Esta medición también incluye la memoria usada por el intérprete de Python y las librerías que hemos cargado, así que la cantidad real de memoria usada para cargar el dataset es un poco más pequeña. A modo de comparación, veamos qué tan grande es el dataset en disco, usando el atributo `dataset_size`. Dado que el resultado está expresado en bytes, tenemos que convertirlo manualmente en gigabytes: + +```py +print(f"Number of files in dataset : {pubmed_dataset.dataset_size}") +size_gb = pubmed_dataset.dataset_size / (1024**3) +print(f"Dataset size (cache file) : {size_gb:.2f} GB") +``` + +```python out +Number of files in dataset : 20979437051 +Dataset size (cache file) : 19.54 GB +``` + +Bien, a pesar de que el archivo es de casi 20 GB, ¡podemos cargarlo y acceder a su contenido con mucha menos RAM! + + + +✏️ **¡Inténtalo!** Escoge alguno de los [subconjuntos](https://mystic.the-eye.eu/public/AI/pile_preliminary_components/) del _Pile_ que sea más grande que la RAM de tu computador portátil o de escritorio, cárgalo con 🤗 Datasets y mide la cantidad de RAM utilizada. Recuerda que para tener una medición precisa, tienes que hacerlo en un nuevo proceso. Puedes encontrar los tamaños de cada uno de los subconjuntos sin comprimir en la Tabla 1 del [paper de _Pile_](https://arxiv.org/abs/2101.00027). + + + +Si estás familiarizado con Pandas, este resultado puede ser sorprendente por la famosa [regla de Wes Kinney](https://wesmckinney.com/blog/apache-arrow-pandas-internals/) que indica que típicamente necesitas de 5 a 10 veces la RAM que el tamaño del archivo de tu dataset. ¿Cómo resuelve entonces 🤗 Datasets este problema de manejo de memoria? 🤗 Datasets trata cada dataset como un [archivo proyectado en memoria](https://en.wikipedia.org/wiki/Memory-mapped_file), lo que permite un mapeo entre la RAM y el sistema de almacenamiento de archivos, que le permite a la librería acceder y operar los elementos del dataset sin necesidad de tenerlos cargados completamente en memoria. + +Los archivos proyectados en memoria también pueden ser compartidos por múltiples procesos, lo que habilita la paralelización de métodos como `Dataset.map()` sin que sea obligatorio mover o copiar el dataset. Internamente, estas capacidades se logran gracias al formato de memoria [Apache Arrow](https://arrow.apache.org) y la librería [`pyarrow`](https://arrow.apache.org/docs/python/index.html), que permiten la carga y procesamiento de datos a gran velocidad. (Para ahondar más en Apache Arrow y algunas comparaciones con Pandas, revisa el [blog de Dejan Simic](https://towardsdatascience.com/apache-arrow-read-dataframe-with-zero-memory-69634092b1a)). Para verlo en acción, ejecutemos un test de velocidad iterando sobre todos los elementos del dataset de abstracts de PubMed: + +```py +import timeit + +code_snippet = """batch_size = 1000 + +for idx in range(0, len(pubmed_dataset), batch_size): + _ = pubmed_dataset[idx:idx + batch_size] +""" + +time = timeit.timeit(stmt=code_snippet, number=1, globals=globals()) +print( + f"Iterated over {len(pubmed_dataset)} examples (about {size_gb:.1f} GB) in " + f"{time:.1f}s, i.e. {size_gb/time:.3f} GB/s" +) +``` + +```python out +'Iterated over 15518009 examples (about 19.5 GB) in 64.2s, i.e. 0.304 GB/s' +``` + +Aquí usamos el módulo `timeit` de Python para medir el tiempo de ejecución que se toma `code_snippet`. Tipicamemente, puedes iterar a lo largo de un dataset a una velocidad de unas cuantas décimas de un GB por segundo. Esto funciona muy bien para la gran mayoría de aplicaciones, pero algunas veces tendrás que trabajar con un dataset que es tan grande para incluso almacenarse en el disco de tu computador. Por ejemplo, si quisieramos descargar el _Pile_ completo ¡necesitaríamos 825 GB de almacenamiento libre! Para trabajar con esos casos, 🤗 Datasets puede trabajar haciendo _streaming_, lo que permite la descarga y acceso a los elementos sobre la marcha, sin necesidad de descargar todo el dataset. Veamos cómo funciona: + + + +💡 En los cuadernos de Jupyter también puedes medir el tiempo de ejecución de las celdas usando [`%%timeit`](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-timeit). + + + +## Haciendo _streaming_ de datasets + +Para habilitar el _streaming_ basta con pasar el argumento `streaming=True` a la función `load_dataset()`. Por ejemplo, carguemos el dataset de abstracts de PubMed de nuevo, pero en modo _streaming_. + +```py +pubmed_dataset_streamed = load_dataset( + "json", data_files=data_files, split="train", streaming=True +) +``` + +En vez del `Dataset` común y corriente que nos hemos encontrado en el resto del capítulo, el objeto devuelto con `streaming=True` es un `IterableDataset`. Como su nombre lo indica, para acceder a los elementos de un `IterableDataset` tenemos que iterar sobre él. Podemos acceder al primer elemento de nuestro dataset de la siguiente manera: + +```py +next(iter(pubmed_dataset_streamed)) +``` + +```python out +{'meta': {'pmid': 11409574, 'language': 'eng'}, + 'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection.\nTo determine the prevalence of hypoxaemia in children aged under 5 years suffering acute lower respiratory infections (ALRI), the risk factors for hypoxaemia in children under 5 years of age with ALRI, and the association of hypoxaemia with an increased risk of dying in children of the same age ...'} +``` + +Los elementos de un dataset _streamed_ pueden ser procesados sobre la marcha usando `IterableDataset.map()`, lo que puede servirte si tienes que tokenizar los inputs. El proceso es exactamente el mismo que el que usamos para tokenizar nuestro dataset en el [Capítulo 3](/course/chapter3), con la única diferencia de que los outputs se devuelven uno por uno. + +```py +from transformers import AutoTokenizer + +tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") +tokenized_dataset = pubmed_dataset_streamed.map(lambda x: tokenizer(x["text"])) +next(iter(tokenized_dataset)) +``` + +```python out +{'input_ids': [101, 4958, 5178, 4328, 6779, ...], 'attention_mask': [1, 1, 1, 1, 1, ...]} +``` + + + +💡 Para acelerar la tokenización con _streaming_ puedes definir `batched=True`, como lo vimos en la sección anterior. Esto va a procesar los ejemplos lote por lote. Recuerda que el tamaño por defecto de los lotes es 1.000 y puede ser expecificado con el argumento `batch_size`. + + + +También puedes aleatorizar el orden de un dataset _streamed_ usando `IterableDataset.shuffle()`, pero a diferencia de `Dataset.shuffle()` esto sólo afecta a los elementos en un `buffer_size` determinado: + +```py +shuffled_dataset = pubmed_dataset_streamed.shuffle(buffer_size=10_000, seed=42) +next(iter(shuffled_dataset)) +``` + +```python out +{'meta': {'pmid': 11410799, 'language': 'eng'}, + 'text': 'Randomized study of dose or schedule modification of granulocyte colony-stimulating factor in platinum-based chemotherapy for elderly patients with lung cancer ...'} +``` + +En este ejemplo, seleccionamos un ejemplo aleatório de los primeros 10.000 ejemplos en el buffer. Apenas se accede a un ejemplo, su lugar en el buffer se llena con el siguiente ejemplo en el corpus (i.e., el ejemplo número 10.001). También peudes seleccionar elementos de un dataset _streamed_ usando las funciones `IterableDataset.take()` y `IterableDataset.skip()`, que funcionan de manera similar a `Dataset.select()`. Por ejemplo, para seleccionar los 5 primeros ejemplos en el dataset de abstracts de PubMed podemos hacer lo siguiente: + +```py +dataset_head = pubmed_dataset_streamed.take(5) +list(dataset_head) +``` + +```python out +[{'meta': {'pmid': 11409574, 'language': 'eng'}, + 'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection ...'}, + {'meta': {'pmid': 11409575, 'language': 'eng'}, + 'text': 'Clinical signs of hypoxaemia in children with acute lower respiratory infection: indicators of oxygen therapy ...'}, + {'meta': {'pmid': 11409576, 'language': 'eng'}, + 'text': "Hypoxaemia in children with severe pneumonia in Papua New Guinea ..."}, + {'meta': {'pmid': 11409577, 'language': 'eng'}, + 'text': 'Oxygen concentrators and cylinders ...'}, + {'meta': {'pmid': 11409578, 'language': 'eng'}, + 'text': 'Oxygen supply in rural africa: a personal experience ...'}] +``` + +También podemos usar la función `IterableDataset.skip()` para crear conjuntos de entrenamiento y validación de un dataset ordenado aleatóriamente así: + +```py +# Skip the first 1,000 examples and include the rest in the training set +train_dataset = shuffled_dataset.skip(1000) +# Take the first 1,000 examples for the validation set +validation_dataset = shuffled_dataset.take(1000) +``` + +Vamos a repasar la exploración del _streaming_ de datasets con una aplicación común: combinar múltiples datasets para crear un solo corpus. 🤗 Datasets provee una función `interleave_datasets()` que convierte una lista de objetos `IterableDataset` en un solo `IterableDataset`, donde la lista de elementos del nuevo dataset se obtiene al alternar entre los ejemplos originales. Esta función es particularmente útil cuando quieres combinar datasets grandes, así que como ejemplo hagamos _streaming_ del conjunto FreeLaw del _Pile_, que es un dataset de 51 GB con opiniones legales de las cortes en Estados Unidos. + +```py +law_dataset_streamed = load_dataset( + "json", + data_files="https://mystic.the-eye.eu/public/AI/pile_preliminary_components/FreeLaw_Opinions.jsonl.zst", + split="train", + streaming=True, +) +next(iter(law_dataset_streamed)) +``` + +```python out +{'meta': {'case_ID': '110921.json', + 'case_jurisdiction': 'scotus.tar.gz', + 'date_created': '2010-04-28T17:12:49Z'}, + 'text': '\n461 U.S. 238 (1983)\nOLIM ET AL.\nv.\nWAKINEKONA\nNo. 81-1581.\nSupreme Court of United States.\nArgued January 19, 1983.\nDecided April 26, 1983.\nCERTIORARI TO THE UNITED STATES COURT OF APPEALS FOR THE NINTH CIRCUIT\n*239 Michael A. Lilly, First Deputy Attorney General of Hawaii, argued the cause for petitioners. With him on the brief was James H. Dannenberg, Deputy Attorney General...'} +``` + +Este dataset es lo suficientemente grande como para llevar al límite la RAM de la mayoría de computadores portátiles. Sin embargo, ¡podemos cargarla y acceder a el sin esfuerzo! Ahora combinemos los ejemplos de FreeLaw y PubMed usando la función `interleave_datasets()`: + +```py +from itertools import islice +from datasets import interleave_datasets + +combined_dataset = interleave_datasets([pubmed_dataset_streamed, law_dataset_streamed]) +list(islice(combined_dataset, 2)) +``` + +```python out +[{'meta': {'pmid': 11409574, 'language': 'eng'}, + 'text': 'Epidemiology of hypoxaemia in children with acute lower respiratory infection ...'}, + {'meta': {'case_ID': '110921.json', + 'case_jurisdiction': 'scotus.tar.gz', + 'date_created': '2010-04-28T17:12:49Z'}, + 'text': '\n461 U.S. 238 (1983)\nOLIM ET AL.\nv.\nWAKINEKONA\nNo. 81-1581.\nSupreme Court of United States.\nArgued January 19, 1983.\nDecided April 26, 1983.\nCERTIORARI TO THE UNITED STATES COURT OF APPEALS FOR THE NINTH CIRCUIT\n*239 Michael A. Lilly, First Deputy Attorney General of Hawaii, argued the cause for petitioners. With him on the brief was James H. Dannenberg, Deputy Attorney General...'}] +``` + +Usamos la función `islice()` del módulo `itertools` de Python para seleccionar los primeros dos ejemplos del dataset combinado y podemos ver que corresponden con los primeros dos ejemplos de cada uno de los dos datasets de origen. + +Finalmente, si quieres hacer _streaming_ del _Pile_ de 825 GB en su totalidad, puedes usar todos los archivos preparados de la siguiente manera: + +```py +base_url = "https://mystic.the-eye.eu/public/AI/pile/" +data_files = { + "train": [base_url + "train/" + f"{idx:02d}.jsonl.zst" for idx in range(30)], + "validation": base_url + "val.jsonl.zst", + "test": base_url + "test.jsonl.zst", +} +pile_dataset = load_dataset("json", data_files=data_files, streaming=True) +next(iter(pile_dataset["train"])) +``` + +```python out +{'meta': {'pile_set_name': 'Pile-CC'}, + 'text': 'It is done, and submitted. You can play “Survival of the Tastiest” on Android, and on the web...'} +``` + + + +✏️ **¡Inténtalo!** Usa alguno de los corpus grandes de Common Crawl como [`mc4`](https://huggingface.co/datasets/mc4) u [`oscar`](https://huggingface.co/datasets/oscar) para crear un dataset _streaming_ multilenguaje que represente las proporciones de lenguajes hablados en un país de tu elección. Por ejemplo, los 4 lenguajes nacionales en Suiza son alemán, francés, italiano y romanche, así que podrías crear un corpus suizo al hacer un muestreo de Oscar de acuerdo con su proporción de lenguaje. + + + +Ya tienes todas las herramientas para cargar y procesar datasets de todas las formas y tamaños, pero a menos que seas muy afortunado, llegará un punto en tu camino de PLN en el que tendrás que crear el dataset tu mismo para resolver tu problema particular. De esto hablaremos en la siguiente sección. + diff --git a/chapters/es/chapter5/5.mdx b/chapters/es/chapter5/5.mdx new file mode 100644 index 000000000..3d3cd21c2 --- /dev/null +++ b/chapters/es/chapter5/5.mdx @@ -0,0 +1,466 @@ +# Crea tu propio dataset + + + +Algunas veces el dataset que necesitas para crear una aplicación de procesamiento de lenguaje natural no existe, así que necesitas crearla. En esta sección vamos a mostrarte cómo crear un corpus de [issues de GitHub](https://github.com/features/issues/), que se usan comúnmente para rastrear bugs o features en repositorios de GitHub. Este corpus podría ser usado para varios propósitos como: + +* Explorar qué tanto se demora el cierre un issue abierto o un pull request +* Entrenar un _clasificador de etiquetas múltiples_ que pueda etiquetar issues con metadados basado en la descripción del issue (e.g., "bug", "mejora" o "pregunta") +* Crear un motor de búsqueda semántica para encontrar qué issues coinciden con la pregunta del usuario + +En esta sección nos vamos a enfocar en la creación del corpus y en la siguiente vamos a abordar la aplicación de búsqueda semántica. Para que esto sea un meta-proyecto, vamos a usar los issues de Github asociados con un proyecto popular de código abierto: 🤗 Datasets! Veamos cómo obtener los datos y explorar la información contenida en estos issues. + +## Obteniendo los datos + +Puedes encontrar todos los issues de 🤗 Datasets yendo a la [pestaña de issues](https://github.com/huggingface/datasets/issues) del repositorio. Como se puede ver en la siguiente captura de pantalla, al momento de escribir esta sección habían 331 issues abiertos y 668 cerrados. + +
+The GitHub issues associated with 🤗 Datasets. +
+ +Si haces clic en alguno de estos issues te encontrarás con que incluyen un título, una descripción y un conjunto de etiquetas que lo caracterizan. Un ejemplo de esto se muestra en la siguiente captura de pantalla. + +
+A typical GitHub issue in the 🤗 Datasets repository. +
+ +Para descargar todos los issues del repositorio, usaremos el [API REST de GitHub](https://docs.github.com/en/rest) para obtener el [endpoint `Issues`](https://docs.github.com/en/rest/reference/issues#list-repository-issues). Este endpoint devuelve una lista de objetos JSON, en la que cada objeto contiene un gran número de campos que incluyen el título y la descripción, así como metadatos sobre el estado del issue, entre otros. + +Una forma conveniente de descargar los issues es a través de la librería `requests`, que es la manera estándar para hacer pedidos HTTP en Python. Puedes instalar esta librería instalando: + +```python +!pip install requests +``` + +Una vez la librería está instalada, puedes hacer pedidos GET al endpoint `Issues` ejecutando la función `requests.get()`. Por ejemplo, puedes correr el siguiente comando para obtener el primer issue de la primera página: + +```py +import requests + +url = "https://api.github.com/repos/huggingface/datasets/issues?page=1&per_page=1" +response = requests.get(url) +``` + +El objeto `response` contiene una gran cantidad de información útil sobre el pedido, incluyendo el código de status de HTTP: + +```py +response.status_code +``` + +```python out +200 +``` + +en el que un código de `200` significa que el pedido fue exitoso (puedes ver una lista de posibles códigos de status de HTTP [aquí](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)). No obstante, en lo que estamos interesados realmente es el _payload_, que se puede acceder en varios formatos como bytes, strings o JSON. Como ya sabemos que los issues están en formato JSON, inspeccionemos el _payload_ de la siguiente manera: + +```py +response.json() +``` + +```python out +[{'url': 'https://api.github.com/repos/huggingface/datasets/issues/2792', + 'repository_url': 'https://api.github.com/repos/huggingface/datasets', + 'labels_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792/labels{/name}', + 'comments_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792/comments', + 'events_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792/events', + 'html_url': 'https://github.com/huggingface/datasets/pull/2792', + 'id': 968650274, + 'node_id': 'MDExOlB1bGxSZXF1ZXN0NzEwNzUyMjc0', + 'number': 2792, + 'title': 'Update GooAQ', + 'user': {'login': 'bhavitvyamalik', + 'id': 19718818, + 'node_id': 'MDQ6VXNlcjE5NzE4ODE4', + 'avatar_url': 'https://avatars.githubusercontent.com/u/19718818?v=4', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/bhavitvyamalik', + 'html_url': 'https://github.com/bhavitvyamalik', + 'followers_url': 'https://api.github.com/users/bhavitvyamalik/followers', + 'following_url': 'https://api.github.com/users/bhavitvyamalik/following{/other_user}', + 'gists_url': 'https://api.github.com/users/bhavitvyamalik/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/bhavitvyamalik/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/bhavitvyamalik/subscriptions', + 'organizations_url': 'https://api.github.com/users/bhavitvyamalik/orgs', + 'repos_url': 'https://api.github.com/users/bhavitvyamalik/repos', + 'events_url': 'https://api.github.com/users/bhavitvyamalik/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/bhavitvyamalik/received_events', + 'type': 'User', + 'site_admin': False}, + 'labels': [], + 'state': 'open', + 'locked': False, + 'assignee': None, + 'assignees': [], + 'milestone': None, + 'comments': 1, + 'created_at': '2021-08-12T11:40:18Z', + 'updated_at': '2021-08-12T12:31:17Z', + 'closed_at': None, + 'author_association': 'CONTRIBUTOR', + 'active_lock_reason': None, + 'pull_request': {'url': 'https://api.github.com/repos/huggingface/datasets/pulls/2792', + 'html_url': 'https://github.com/huggingface/datasets/pull/2792', + 'diff_url': 'https://github.com/huggingface/datasets/pull/2792.diff', + 'patch_url': 'https://github.com/huggingface/datasets/pull/2792.patch'}, + 'body': '[GooAQ](https://github.com/allenai/gooaq) dataset was recently updated after splits were added for the same. This PR contains new updated GooAQ with train/val/test splits and updated README as well.', + 'performed_via_github_app': None}] +``` + +Wow, ¡es mucha información! Podemos ver campos útiles como `title`, `body` y `number`, que describen el issue, así como información del usuario de GitHub que lo abrió. + + + +✏️ **¡Inténtalo!** Haz clic en algunas de las URL en el _payload_ JSON de arriba para explorar la información que está enlazada al issue de GitHub. + + + +Tal como se describe en la [documentación](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) de GitHub, los pedidos sin autenticación están limitados a 60 por hora. Si bien puedes incrementar el parámetro de búsqueda `per_page` para reducir el número de pedidos que haces, igual puedes alcanzar el límite de pedidos en cualquier repositorio que tenga más que un par de miles de issues. En vez de hacer eso, puedes seguir las [instrucciones](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) de GitHub para crear un _token de acceso personal_ y que puedas incrementar el límite de pedidos a 5.000 por hora. Una vez tengas tu token, puedes incluirlo como parte del encabezado del pedido: + +```py +GITHUB_TOKEN = xxx # Copy your GitHub token here +headers = {"Authorization": f"token {GITHUB_TOKEN}"} +``` + + + +⚠️ No compartas un cuaderno que contenga tu `GITHUB_TOKEN`. Te recomendamos eliminar la última celda una vez la has ejecutado para evitar filtrar accidentalmente esta información. Aún mejor, guarda el token en un archivo *.env* y usa la librería [`python-dotenv`](https://github.com/theskumar/python-dotenv) para cargarla automáticamente como una variable de ambiente. + + + +Ahora que tenemos nuestro token de acceso, creemos una función que descargue todos los issues de un repositorio de GitHub: + +```py +import time +import math +from pathlib import Path +import pandas as pd +from tqdm.notebook import tqdm + + +def fetch_issues( + owner="huggingface", + repo="datasets", + num_issues=10_000, + rate_limit=5_000, + issues_path=Path("."), +): + if not issues_path.is_dir(): + issues_path.mkdir(exist_ok=True) + + batch = [] + all_issues = [] + per_page = 100 # Número de issues por página + num_pages = math.ceil(num_issues / per_page) + base_url = "https://api.github.com/repos" + + for page in tqdm(range(num_pages)): + # Query con state=all para obtener tanto issues abiertos como cerrados + query = f"issues?page={page}&per_page={per_page}&state=all" + issues = requests.get(f"{base_url}/{owner}/{repo}/{query}", headers=headers) + batch.extend(issues.json()) + + if len(batch) > rate_limit and len(all_issues) < num_issues: + all_issues.extend(batch) + batch = [] # Vacía el batch para el siguiente periodo de tiempo + print(f"Reached GitHub rate limit. Sleeping for one hour ...") + time.sleep(60 * 60 + 1) + + all_issues.extend(batch) + df = pd.DataFrame.from_records(all_issues) + df.to_json(f"{issues_path}/{repo}-issues.jsonl", orient="records", lines=True) + print( + f"Downloaded all the issues for {repo}! Dataset stored at {issues_path}/{repo}-issues.jsonl" + ) +``` + +Cuando ejecutemos `fetch_issues()`, se descargarán todos los issues en lotes para evitar exceder el límite de GitHub sobre el número de pedidos por hora. El resultado se guardará en un archivo _repository_name-issues.jsonl_, donde cada línea es un objeto JSON que representa un issue. Usemos esta función para cargar todos los issues de 🤗 Datasets: + +```py +# Dependiendo de tu conexión a internet, esto puede tomar varios minutos para ejecutarse... +fetch_issues() +``` + +Una vez los issues estén descargados, los podemos cargar localmente usando las habilidades aprendidas en la [sección 2](/course/chaper5/2): + +```py +issues_dataset = load_dataset("json", data_files="datasets-issues.jsonl", split="train") +issues_dataset +``` + +```python out +Dataset({ + features: ['url', 'repository_url', 'labels_url', 'comments_url', 'events_url', 'html_url', 'id', 'node_id', 'number', 'title', 'user', 'labels', 'state', 'locked', 'assignee', 'assignees', 'milestone', 'comments', 'created_at', 'updated_at', 'closed_at', 'author_association', 'active_lock_reason', 'pull_request', 'body', 'timeline_url', 'performed_via_github_app'], + num_rows: 3019 +}) +``` + +¡Genial! Hemos creado nuestro primer dataset desde cero. Pero, ¿por qué hay varios miles de issues cuando la [pestaña de Issues](https://github.com/huggingface/datasets/issues) del repositorio de 🤗 Datasets sólo muestra alrededor de 1.000 en total? Como se describe en la [documentación](https://docs.github.com/en/rest/reference/issues#list-issues-assigned-to-the-authenticated-user), esto sucede porque también descargamos todos los pull requests: + +> GitHub's REST API v3 considers every pull request an issue, but not every issue is a pull request. For this reason, "Issues" endpoints may return both issues and pull requests in the response. You can identify pull requests by the `pull_request` key. Be aware that the `id` of a pull request returned from "Issues" endpoints will be an issue id. + +Como el contenido de los issues y pull requests son diferentes, hagamos un preprocesamiento simple para distinguirlos entre sí. + +## Limpiando los datos + +El fragmento anterior de la documentación de GitHub nos dice que la columna `pull_request` puede usarse para diferenciar los issues de los pull requests. Veamos una muestra aleatoria para ver la diferencia. Como hicimos en la [sección 3](/course/chapter5/3), vamos a encadenar `Dataset.shuffle()` y `Dataset.select()` para crear una muestra aleatoria y luego unir las columnas de `html_url` y `pull_request` para comparar las distintas URL: + +```py +sample = issues_dataset.shuffle(seed=666).select(range(3)) + +# Imprime la URL y las entradas de pull_request +for url, pr in zip(sample["html_url"], sample["pull_request"]): + print(f">> URL: {url}") + print(f">> Pull request: {pr}\n") +``` + +```python out +>> URL: https://github.com/huggingface/datasets/pull/850 +>> Pull request: {'url': 'https://api.github.com/repos/huggingface/datasets/pulls/850', 'html_url': 'https://github.com/huggingface/datasets/pull/850', 'diff_url': 'https://github.com/huggingface/datasets/pull/850.diff', 'patch_url': 'https://github.com/huggingface/datasets/pull/850.patch'} + +>> URL: https://github.com/huggingface/datasets/issues/2773 +>> Pull request: None + +>> URL: https://github.com/huggingface/datasets/pull/783 +>> Pull request: {'url': 'https://api.github.com/repos/huggingface/datasets/pulls/783', 'html_url': 'https://github.com/huggingface/datasets/pull/783', 'diff_url': 'https://github.com/huggingface/datasets/pull/783.diff', 'patch_url': 'https://github.com/huggingface/datasets/pull/783.patch'} +``` + +Podemos ver que cada pull request está asociado con varias URL, mientras que los issues ordinarios tienen una entrada `None`. Podemos usar esta distinción para crear una nueva columna `is_pull_request` que revisa si el campo `pull_request` es `None` o no: + +```py +issues_dataset = issues_dataset.map( + lambda x: {"is_pull_request": False if x["pull_request"] is None else True} +) +``` + + + +✏️ **¡Inténtalo!** Calcula el tiempo promedio que toma cerrar issues en 🤗 Datasets. La función `Dataset.filter()` te será útil para filtrar los pull requests y los issues abiertos, y puedes usar la función `Dataset.set_format()` para convertir el dataset a un `DataFrame` para poder manipular fácilmente los timestamps de `created_at` y `closed_at`. Para puntos extra, calcula el tiempo promedio que toma cerrar pull requests. + + + +Si bien podemos limpiar aún más el dataset eliminando o renombrando algunas columnas, es una buena práctica mantener un dataset lo más parecido al original en esta etapa, para que se pueda usar fácilmente en varias aplicaciones. + +Antes de subir el dataset el Hub de Hugging Face, nos hace falta añadirle algo más: los comentarios asociados con cada issue y pull request. Los vamos a añadir con el API REST de GitHub. + +## Ampliando el dataset + +Como se muestra en la siguiente captura de pantalla, los comentarios asociados con un issue o un pull request son una fuente rica de información, especialmente si estamos interesados en construir un motor de búsqueda para responder preguntas de usuarios sobre la librería. + +
+Comments associated with an issue about 🤗 Datasets. +
+ +El API REST de GitHub tiene un [endpoint `Comments`](https://docs.github.com/en/rest/reference/issues#list-issue-comments) que devuelve todos los comentarios asociados con un número de issue. Probémos este endpoint para ver qué devuelve: + +```py +issue_number = 2792 +url = f"https://api.github.com/repos/huggingface/datasets/issues/{issue_number}/comments" +response = requests.get(url, headers=headers) +response.json() +``` + +```python out +[{'url': 'https://api.github.com/repos/huggingface/datasets/issues/comments/897594128', + 'html_url': 'https://github.com/huggingface/datasets/pull/2792#issuecomment-897594128', + 'issue_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792', + 'id': 897594128, + 'node_id': 'IC_kwDODunzps41gDMQ', + 'user': {'login': 'bhavitvyamalik', + 'id': 19718818, + 'node_id': 'MDQ6VXNlcjE5NzE4ODE4', + 'avatar_url': 'https://avatars.githubusercontent.com/u/19718818?v=4', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/bhavitvyamalik', + 'html_url': 'https://github.com/bhavitvyamalik', + 'followers_url': 'https://api.github.com/users/bhavitvyamalik/followers', + 'following_url': 'https://api.github.com/users/bhavitvyamalik/following{/other_user}', + 'gists_url': 'https://api.github.com/users/bhavitvyamalik/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/bhavitvyamalik/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/bhavitvyamalik/subscriptions', + 'organizations_url': 'https://api.github.com/users/bhavitvyamalik/orgs', + 'repos_url': 'https://api.github.com/users/bhavitvyamalik/repos', + 'events_url': 'https://api.github.com/users/bhavitvyamalik/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/bhavitvyamalik/received_events', + 'type': 'User', + 'site_admin': False}, + 'created_at': '2021-08-12T12:21:52Z', + 'updated_at': '2021-08-12T12:31:17Z', + 'author_association': 'CONTRIBUTOR', + 'body': "@albertvillanova my tests are failing here:\r\n```\r\ndataset_name = 'gooaq'\r\n\r\n def test_load_dataset(self, dataset_name):\r\n configs = self.dataset_tester.load_all_configs(dataset_name, is_local=True)[:1]\r\n> self.dataset_tester.check_load_dataset(dataset_name, configs, is_local=True, use_local_dummy_data=True)\r\n\r\ntests/test_dataset_common.py:234: \r\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \r\ntests/test_dataset_common.py:187: in check_load_dataset\r\n self.parent.assertTrue(len(dataset[split]) > 0)\r\nE AssertionError: False is not true\r\n```\r\nWhen I try loading dataset on local machine it works fine. Any suggestions on how can I avoid this error?", + 'performed_via_github_app': None}] +``` + +Podemos ver que el comentario está almacenado en el campo `body`, así que escribamos una función simple que devuelva todos los comentarios asociados con un issue al extraer el contenido de `body` para cada elemento en el `response.json()`: + +```py +def get_comments(issue_number): + url = f"https://api.github.com/repos/huggingface/datasets/issues/{issue_number}/comments" + response = requests.get(url, headers=headers) + return [r["body"] for r in response.json()] + + +# Revisar que el comportamiento de nuestra función es el esperado +get_comments(2792) +``` + +```python out +["@albertvillanova my tests are failing here:\r\n```\r\ndataset_name = 'gooaq'\r\n\r\n def test_load_dataset(self, dataset_name):\r\n configs = self.dataset_tester.load_all_configs(dataset_name, is_local=True)[:1]\r\n> self.dataset_tester.check_load_dataset(dataset_name, configs, is_local=True, use_local_dummy_data=True)\r\n\r\ntests/test_dataset_common.py:234: \r\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \r\ntests/test_dataset_common.py:187: in check_load_dataset\r\n self.parent.assertTrue(len(dataset[split]) > 0)\r\nE AssertionError: False is not true\r\n```\r\nWhen I try loading dataset on local machine it works fine. Any suggestions on how can I avoid this error?"] +``` + +Esto luce bien, así que usemos `Dataset.map()` para añadir una nueva columna `comments` a cada issue en el dataset: + +```py +# Dependiendo de tu conexión a internet, esto puede tomar varios minutos... +issues_with_comments_dataset = issues_dataset.map( + lambda x: {"comments": get_comments(x["number"])} +) +``` + +El último paso es guardar el dataset ampliado en el mismo lugar que los datos originales para poderlos subir al Hub: + +```py +issues_with_comments_dataset.to_json("issues-datasets-with-comments.jsonl") +``` + +## Subiendo un dataset al Hub de Hugging Face + + + +Ahora que tenemos nuestro dataset ampliado, es momento de subirlo al Hub para poder compartirlo con la comunidad. Para subir el dataset tenemos que usar la [librería 🤗 Hub](https://github.com/huggingface/huggingface_hub), que nos permite interactuar con el Hub de Hugging Face usando una API de Python. 🤗 Hub viene instalada con 🤗 Transformers, así que podemos usarla directamente. Por ejemplo, podemos usar la función `list_datasets()` para obtener información sobre todos los datasets públicos que están almacenados en el Hub: + +```py +from huggingface_hub import list_datasets + +all_datasets = list_datasets() +print(f"Number of datasets on Hub: {len(all_datasets)}") +print(all_datasets[0]) +``` + +```python out +Number of datasets on Hub: 1487 +Dataset Name: acronym_identification, Tags: ['annotations_creators:expert-generated', 'language_creators:found', 'languages:en', 'licenses:mit', 'multilinguality:monolingual', 'size_categories:10K + +✏️ **¡Inténtalo!** Usa tu nombre de usuario de Hugging Face Hub para obtener un token y crear un repositorio vacío llamado `girhub-issues`. Recuerda **nunca guardar tus credenciales** en Colab o cualquier otro repositorio, ya que esta información puede ser aprovechada por terceros. + + + +Ahora clonemos el repositorio del Hub a nuestra máquina local y copiemos nuestro dataset ahí. 🤗 Hub incluye una clase `Repositorio` que envuelve muchos de los comandos comunes de Git, así que para clonar el repositorio remoto solamente necesitamos dar la URL y la ruta local en la que lo queremos clonar: + +```py +from huggingface_hub import Repository + +repo = Repository(local_dir="github-issues", clone_from=repo_url) +!cp issues-datasets-with-comments.jsonl github-issues/ +``` + +Por defecto, varias extensiones de archivo (como *.bin*, *.gz*, and *.zip*) se siguen con Git LFS de tal manera que los archivos grandes se pueden versionar dentro del mismo flujo de trabajo de Git. Puedes encontrar una lista de extensiones que se van a seguir en el archivo *.gitattributes*. Para incluir el formato JSON Lines en la lista, puedes ejecutar el siguiente comando: + +```py +repo.lfs_track("*.jsonl") +``` + +Luego, podemos usar `$$Repository.push_to_hub()` para subir el dataset al Hub: + +```py +repo.push_to_hub() +``` + +Si navegamos a la URL que aparece en `repo_url`, deberíamos ver que el archivo del dataset se ha subido. + +
+Our dataset repository on the Hugging Face Hub. +
+ +Desde aqui, cualquier persona podrá descargar el dataset incluyendo el ID del repositorio en el argumento `path` de la función `load_dataset()`: + +```py +remote_dataset = load_dataset("lewtun/github-issues", split="train") +remote_dataset +``` + +```python out +Dataset({ + features: ['url', 'repository_url', 'labels_url', 'comments_url', 'events_url', 'html_url', 'id', 'node_id', 'number', 'title', 'user', 'labels', 'state', 'locked', 'assignee', 'assignees', 'milestone', 'comments', 'created_at', 'updated_at', 'closed_at', 'author_association', 'active_lock_reason', 'pull_request', 'body', 'performed_via_github_app', 'is_pull_request'], + num_rows: 2855 +}) +``` + +¡Genial, hemos subido el dataset al Hub y ya está disponible para que otras personas lo usen! Sólo hay una cosa restante por hacer: añadir una _tarjeta del dataset_ (_dataset card_) que explique cómo se creó el corpus y provea información útil para la comunidad. + + + +💡 También puedes subir un dataset al Hub de Hugging Face directamente desde la terminal usando `huggingface-cli` y un poco de Git. Revisa la [guía de 🤗 Datasets](https://huggingface.co/docs/datasets/share.html#add-a-community-dataset) para más detalles sobre cómo hacerlo. + + + +## Creando una tarjeta del dataset + +Los datasets bien documentados tienen más probabilidades de ser útiles para otros (incluyéndote a ti en el futuro), dado que brindan la información necesaria para que los usuarios decidan si el dataset es útil para su tarea, así como para evaluar cualquier sesgo o riesgo potencial asociado a su uso. + +En el Hub de Hugging Face, esta información se almacena en el archivo *README.md* del repositorio del dataset. Hay dos pasos que deberías hacer antes de crear este archivo: + +1. Usa la [aplicación `datasets-tagging`](https://huggingface.co/datasets/tagging/) para crear etiquetas de metadatos en el formato YAML. Estas etiquetas se usan para una variedad de funciones de búsqueda en el Hub de Hugging Face y aseguran que otros miembros de la comunidad puedan encontrar tu dataset. Dado que creamos un dataset personalizado en esta sección, tendremos que clonar el repositorio `datasets-tagging` y correr la aplicación localmente. Así se ve la interfaz de la aplicación: + +
+The `datasets-tagging` interface. +
+ +2. Lee la [guía de 🤗 Datasets](https://github.com/huggingface/datasets/blob/master/templates/README_guide.md) sobre cómo crear tarjetas informativas y usarlas como plantilla. + +Puedes crear el archivo *README.md* drectamente desde el Hub y puedes encontrar una plantilla de tarjeta en el repositorio `lewtun/github-issues`. Así se ve una tarjeta de dataset diligenciada: + +
+A dataset card. +
+ + + +✏️ **¡Inténtalo!** Usa la aplicación `dataset-tagging` y la [guía de 🤗 Datasets](https://github.com/huggingface/datasets/blob/master/templates/README_guide.md) para completar el archivo *README.md* para tu dataset de issues de GitHub. + + + +¡Eso es todo! Hemos visto que crear un buen dataset requiere de mucho esfuerzo de tu parte, pero afortunadamente subirlo y compartirlo con la comunidad no. En la siguiente sección usaremos nuestro nuevo dataset para crear un motor de búsqueda semántica con 🤗 Datasets que pueda emparejar pregunras con los issues y comentarios más relevantes. + + + +✏️ **¡Inténtalo!** Sigue los pasos descritos en esta sección para crear un dataset de issues de GitHub de tu librería de código abierto favorita (¡por supuesto, escoge algo distinto a 🤗 Datasets!). Para puntos extra, ajusta un clasificador de etiquetas múltiples para predecir las etiquetas presentes en el campo `labels`. + + diff --git a/chapters/es/chapter5/6.mdx b/chapters/es/chapter5/6.mdx new file mode 100644 index 000000000..2c4cbd13f --- /dev/null +++ b/chapters/es/chapter5/6.mdx @@ -0,0 +1,528 @@ + + +# Búsqueda semántica con FAISS + +{#if fw === 'pt'} + + + +{:else} + + + +{/if} + +En la [sección 5](/course/chapter5/5) creamos un dataset de issues y comentarios del repositorio de Github de 🤗 Datasets. En esta sección usaremos esta información para construir un motor de búsqueda que nos ayude a responder nuestras preguntas más apremiantes sobre la librería. + + + +## Usando _embeddings_ para la búsqueda semántica + +Como vimos en el [Capítulo 1](/course/chapter1), los modelos de lenguaje basados en Transformers representan cada token en un texto como un _vector de embeddings_. Resulta que podemos agrupar los _embeddings_ individuales en representaciones vectoriales para oraciones, párrafos o (en algunos casos) documentos completos. Estos _embeddings_ pueden ser usados para encontrar documentos similares en el corpus al calcular la similaridad del producto punto (o alguna otra métrica de similaridad) entre cada _embedding_ y devolver los documentos con la mayor coincidencia. + +En esta sección vamos a usar _embeddings_ para desarrollar un motor de búsqueda semántica. Estos motores de búsqueda tienen varias ventajas sobre abordajes convencionales basados en la coincidencia de palabras clave en una búsqueda con los documentos. + +
+Semantic search. + +
+ +## Cargando y preparando el dataset + +Lo primero que tenemos que hacer es descargar el dataset de issues de GitHub, así que usaremos la librería 🤗 Hub para resolver la URL en la que está almacenado nuestro archivo en el Hub de Hugging Face: + +```py +from huggingface_hub import hf_hub_url + +data_files = hf_hub_url( + repo_id="lewtun/github-issues", + filename="datasets-issues-with-comments.jsonl", + repo_type="dataset", +) +``` + +Con la URL almacenada en `data_files`, podemos cargar el dataset remoto usando el método introducido en la [sección 2](/course/chapter5/2): + +```py +from datasets import load_dataset + +issues_dataset = load_dataset("json", data_files=data_files, split="train") +issues_dataset +``` + +```python out +Dataset({ + features: ['url', 'repository_url', 'labels_url', 'comments_url', 'events_url', 'html_url', 'id', 'node_id', 'number', 'title', 'user', 'labels', 'state', 'locked', 'assignee', 'assignees', 'milestone', 'comments', 'created_at', 'updated_at', 'closed_at', 'author_association', 'active_lock_reason', 'pull_request', 'body', 'performed_via_github_app', 'is_pull_request'], + num_rows: 2855 +}) +``` + +Hemos especificado el conjunto `train` por defecto en `load_dataset()`, de tal manera que devuelva un objeto `Dataset` en vez de un `DatasetDict`. Lo primero que debemos hacer es filtrar los pull requests, dado que estos no se suelen usar para resolver preguntas de usuarios e introducirán ruido en nuestro motor de búsqueda. Como ya debe ser familiar para ti, podemos usar la función `Dataset.filter()` para excluir estas filas en nuestro dataset. A su vez, filtremos las filas que no tienen comentarios, dado que no van a darnos respuestas para las preguntas de los usuarios. + +```py +issues_dataset = issues_dataset.filter( + lambda x: (x["is_pull_request"] == False and len(x["comments"]) > 0) +) +issues_dataset +``` + +```python out +Dataset({ + features: ['url', 'repository_url', 'labels_url', 'comments_url', 'events_url', 'html_url', 'id', 'node_id', 'number', 'title', 'user', 'labels', 'state', 'locked', 'assignee', 'assignees', 'milestone', 'comments', 'created_at', 'updated_at', 'closed_at', 'author_association', 'active_lock_reason', 'pull_request', 'body', 'performed_via_github_app', 'is_pull_request'], + num_rows: 771 +}) +``` + +Podemos ver que hay un gran número de columnas en nuestro dataset, muchas de las cuales no necesitamos para construir nuestro motor de búsqueda. Desde la perspectiva de la búsqueda, las columnas más informativas son `title`, `body` y `comments`, mientras que `html_url` nos indica un link al issue correspondiente. Usemos la función `Dataset.remove_columns()` para eliminar el resto: + +```py +columns = issues_dataset.column_names +columns_to_keep = ["title", "body", "html_url", "comments"] +columns_to_remove = set(columns_to_keep).symmetric_difference(columns) +issues_dataset = issues_dataset.remove_columns(columns_to_remove) +issues_dataset +``` + +```python out +Dataset({ + features: ['html_url', 'title', 'comments', 'body'], + num_rows: 771 +}) +``` + +Para crear nuestros _embeddings_, vamos a ampliar cada comentario añadiéndole el título y el cuerpo del issue, dado que estos campos suelen incluir información de contexto útil. Dado que nuestra función `comments` es una lista de comentarios para cada issue, necesitamos "explotar" la columna para que cada fila sea una tupla `(html_url, title, body, comment)`. Podemos hacer esto en Pandas con la [función `DataFrame.explode()`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.explode.html), que crea una nueva fila para cada elemento en una columna que está en forma de lista, al tiempo que replica el resto de los valores de las otras columnas. Para verlo en acción, primero debemos cambiar al formato `DataFrame` de Pandas: + +```py +issues_dataset.set_format("pandas") +df = issues_dataset[:] +``` + +Si inspeccionamos la primera fila en este `DataFrame` podemos ver que hay 4 comentarios asociados con este issue: + +```py +df["comments"][0].tolist() +``` + +```python out +['the bug code locate in :\r\n if data_args.task_name is not None:\r\n # Downloading and loading a dataset from the hub.\r\n datasets = load_dataset("glue", data_args.task_name, cache_dir=model_args.cache_dir)', + 'Hi @jinec,\r\n\r\nFrom time to time we get this kind of `ConnectionError` coming from the github.com website: https://raw.githubusercontent.com\r\n\r\nNormally, it should work if you wait a little and then retry.\r\n\r\nCould you please confirm if the problem persists?', + 'cannot connect,even by Web browser,please check that there is some problems。', + 'I can access https://raw.githubusercontent.com/huggingface/datasets/1.7.0/datasets/glue/glue.py without problem...'] +``` + +Cuando "explotamos" `df`, queremos obtener una fila para cada uno de estos comentarios. Veamos si este es el caso: + +```py +comments_df = df.explode("comments", ignore_index=True) +comments_df.head(4) +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
html_urltitlecommentsbody
0https://github.com/huggingface/datasets/issues/2787ConnectionError: Couldn't reach https://raw.githubusercontent.comthe bug code locate in :\r\n if data_args.task_name is not None...Hello,\r\nI am trying to run run_glue.py and it gives me this error...
1https://github.com/huggingface/datasets/issues/2787ConnectionError: Couldn't reach https://raw.githubusercontent.comHi @jinec,\r\n\r\nFrom time to time we get this kind of `ConnectionError` coming from the github.com website: https://raw.githubusercontent.com...Hello,\r\nI am trying to run run_glue.py and it gives me this error...
2https://github.com/huggingface/datasets/issues/2787ConnectionError: Couldn't reach https://raw.githubusercontent.comcannot connect,even by Web browser,please check that there is some problems。Hello,\r\nI am trying to run run_glue.py and it gives me this error...
3https://github.com/huggingface/datasets/issues/2787ConnectionError: Couldn't reach https://raw.githubusercontent.comI can access https://raw.githubusercontent.com/huggingface/datasets/1.7.0/datasets/glue/glue.py without problem...Hello,\r\nI am trying to run run_glue.py and it gives me this error...
+ +Genial, podemos ver que las filas se han replicado y que la columna `comments` incluye los comentarios individuales. Ahora que hemos terminado con Pandas, podemos volver a cambiar el formato a `Dataset` cargando el `DataFrame` en memoria: + +```py +from datasets import Dataset + +comments_dataset = Dataset.from_pandas(comments_df) +comments_dataset +``` + +```python out +Dataset({ + features: ['html_url', 'title', 'comments', 'body'], + num_rows: 2842 +}) +``` + +¡Esto nos ha dado varios miles de comentarios con los que trabajar! + + + +✏️ **¡Inténtalo!** Prueba si puedes usar la función `Dataset.map()` para "explotar" la columna `comments` en `issues_dataset` _sin_ necesidad de usar Pandas. Esto es un poco complejo; te recomendamos revisar la sección de ["Batch mapping"](https://huggingface.co/docs/datasets/v1.12.1/about_map_batch.html?batch-mapping#batch-mapping) de la documentación de 🤗 Datasets para completar esta tarea. + + + +Ahora que tenemos un comentario para cada fila, creemos una columna `comments_length` que contenga el número de palabras por comentario: + +```py +comments_dataset = comments_dataset.map( + lambda x: {"comment_length": len(x["comments"].split())} +) +``` + +Podemos usar esta nueva columna para filtrar los comentarios cortos, que típicamente incluyen cosas como "cc @letwun" o "¡Gracias!", que no son relevantes para nuestro motor de búsqueda. No hay un número preciso que debamos filtrar, pero alrededor de 15 palabras es un buen comienzo: + +```py +comments_dataset = comments_dataset.filter(lambda x: x["comment_length"] > 15) +comments_dataset +``` + +```python out +Dataset({ + features: ['html_url', 'title', 'comments', 'body', 'comment_length'], + num_rows: 2098 +}) +``` + +Ahora que hemos limpiado un poco el dataset, vamos a concatenar el título, la descripción y los comentarios del issue en una nueva columna `text`. Como lo hemos venido haciendo, escribiremos una función para pasarla a `Dataset.map()`: + +```py +def concatenate_text(examples): + return { + "text": examples["title"] + + " \n " + + examples["body"] + + " \n " + + examples["comments"] + } + + +comments_dataset = comments_dataset.map(concatenate_text) +``` + +¡Por fin estamos listos para crear _embeddings_! + +## Creando _embeddings_ de texto + +En el [Capítulo 2](/course/chapter2) vimos que podemos obtener _embeddings_ usando la clase `AutoModel`. Todo lo que tenemos que hacer es escoger un punto de control adecuado para cargar el modelo. Afortunadamente, existe una librería llamada `sentence-transformers` que se especializa en crear _embeddings_. Como se describe en la [documentación](https://www.sbert.net/examples/applications/semantic-search/README.html#symmetric-vs-asymmetric-semantic-search) de esta librería, nuestro caso de uso es un ejemplo de _búsqueda semántica asimétrica_ porque tenemos una pregunta corta cuya respuesta queremos encontrar en un documento más grande, como un comentario de un issue. La tabla de [resumen de modelos](https://www.sbert.net/docs/pretrained_models.html#model-overview) en la documentación nos indica que el punto de control `multi-qa-mpnet-base-dot-v1` tiene el mejor desempeño para la búsqueda semántica, así que lo usaremos para nuestra aplicación. También cargaremos el tokenizador usando el mismo punto de control: + +{#if fw === 'pt'} + +```py +from transformers import AutoTokenizer, AutoModel + +model_ckpt = "sentence-transformers/multi-qa-mpnet-base-dot-v1" +tokenizer = AutoTokenizer.from_pretrained(model_ckpt) +model = AutoModel.from_pretrained(model_ckpt) +``` + +Para acelerar el proceso de _embedding_, es útil ubicar el modelo y los inputs en un dispositivo GPU, así que hagámoslo: + +```py +import torch + +device = torch.device("cuda") +model.to(device) +``` + +{:else} + +```py +from transformers import AutoTokenizer, TFAutoModel + +model_ckpt = "sentence-transformers/multi-qa-mpnet-base-dot-v1" +tokenizer = AutoTokenizer.from_pretrained(model_ckpt) +model = TFAutoModel.from_pretrained(model_ckpt, from_pt=True) +``` + +Ten en cuenta que hemos definido `from_pt=True` como un argumento del método `from_pretrained()`. Esto es porque el punto de control `multi-qa-mpnet-base-dot-v1` sólo tiene pesos de PyTorch, asi que usar `from_pt=True` los va a covertir automáticamente al formato TensorFlow. Como puedes ver, ¡es múy fácil cambiar entre frameworks usando 🤗 Transformers! + +{/if} + +Como mencionamos con anterioridad, queremos representar cada entrada en el corpus de issues de GitHub como un vector individual, así que necesitamos agrupar o promediar nuestros _embeddings_ de tokes de alguna manera. Un abordaje popular es ejecutar *CLS pooling* en los outputs de nuestro modelo, donde simplemente vamos a recolectar el último estado oculto para el token especial `[CLS]`. La siguiente función nos ayudará con esto: + +```py +def cls_pooling(model_output): + return model_output.last_hidden_state[:, 0] +``` + +Ahora crearemos una función que va a tokenizar una lista de documentos, ubicar los tensores en la GPU, alimentarlos al modelo y aplicar CLS pooling a los outputs: + +{#if fw === 'pt'} + +```py +def get_embeddings(text_list): + encoded_input = tokenizer( + text_list, padding=True, truncation=True, return_tensors="pt" + ) + encoded_input = {k: v.to(device) for k, v in encoded_input.items()} + model_output = model(**encoded_input) + return cls_pooling(model_output) +``` + +Podemos probar que la función sirve al pasarle la primera entrada de texto en el corpus e inspeccionando la forma de la salida: + +```py +embedding = get_embeddings(comments_dataset["text"][0]) +embedding.shape +``` + +```python out +torch.Size([1, 768]) +``` + +¡Hemos convertido la primera entrada del corpus en un vector de 768 dimensiones! Ahora podemos usar `Dataset.map()` para aplicar nuestra función `get_embeddings()` a cada fila del corpus, así que creemos una columna `embeddings` así: + +```py +embeddings_dataset = comments_dataset.map( + lambda x: {"embeddings": get_embeddings(x["text"]).detach().cpu().numpy()[0]} +) +``` + +{:else} + +```py +def get_embeddings(text_list): + encoded_input = tokenizer( + text_list, padding=True, truncation=True, return_tensors="tf" + ) + encoded_input = {k: v for k, v in encoded_input.items()} + model_output = model(**encoded_input) + return cls_pooling(model_output) +``` + +Podemos probar que la función sirve al pasarle la primera entrada de texto en el corpus e inspeccionando la forma de la salida: + +```py +embedding = get_embeddings(comments_dataset["text"][0]) +embedding.shape +``` + +```python out +TensorShape([1, 768]) +``` + +¡Hemos convertido la primera entrada del corpus en un vector de 768 dimensiones! Ahora podemos usar `Dataset.map()` para aplicar nuestra función `get_embeddings()` a cada fila del corpus, así que creemos una columna `embeddings` así: + +```py +embeddings_dataset = comments_dataset.map( + lambda x: {"embeddings": get_embeddings(x["text"]).numpy()[0]} +) +``` + +{/if} + +Los _embeddings_ se han convertido en arrays de NumPy, esto es porque 🤗 Datasets los necesita en este formato cuando queremos indexarlos con FAISS, que es lo que haremos a continuación. + +## Usando FAISS para una búsqueda eficiente por similaridad + +Ahora que tenemos un dataset de embeddings, necesitamos una manera de buscar sobre ellos. Para hacerlo, usaremos una estructura especial de datos en 🤗 Datasets llamada _índice FAISS_. [FAISS] (https://faiss.ai/) (siglas para _Facebook AI Similarity Search_) es una librería que contiene algoritmos eficientes para buscar y agrupar rápidamente vectores de _embeddings_. + +La idea básica detrás de FAISS es que crea una estructura especial de datos, llamada _índice_, que te permite encontrar cuáles embeddings son parecidos a un _embedding_ de entrada. La creación de un índice FAISS en 🤗 Datasets es muy simple: usamos la función `Dataset.add_faiss_index()` y especificamos cuál columna del dataset queremos indexar: + +```py +embeddings_dataset.add_faiss_index(column="embeddings") +``` + +Ahora podemos hacer búsquedas sobre este índice al hacer una búsqueda del vecino más cercano con la función `Dataset.get_nearest_examples()`. Probémoslo al hacer el _embedding_ de una pregunta de la siguiente manera: + +{#if fw === 'pt'} + +```py +question = "How can I load a dataset offline?" +question_embedding = get_embeddings([question]).cpu().detach().numpy() +question_embedding.shape +``` + +```python out +torch.Size([1, 768]) +``` + +{:else} + +```py +question = "How can I load a dataset offline?" +question_embedding = get_embeddings([question]).numpy() +question_embedding.shape +``` + +```python out +(1, 768) +``` + +{/if} + +Tal como en los documentos, ahora tenemos un vector de 768 dimensiones que representa la pregunta, que podemos comparar con el corpus entero para encontrar los _embeddings_ más parecidos: + +```py +scores, samples = embeddings_dataset.get_nearest_examples( + "embeddings", question_embedding, k=5 +) +``` + +La función `Dataset.get_nearest_examples()` devuelve una tupla de puntajes que calcula un ranking de la coincidencia entre la pregunta y el documento, así como un conjunto correspondiente de muestras (en este caso, los 5 mejores resultados). Recojámoslos en un `pandas.DataFrame` para ordenarlos fácilmente: + +```py +import pandas as pd + +samples_df = pd.DataFrame.from_dict(samples) +samples_df["scores"] = scores +samples_df.sort_values("scores", ascending=False, inplace=True) +``` + +Podemos iterar sobre las primeras filas para ver qué tanto coincide la pregunta con los comentarios disponibles: + +```py +for _, row in samples_df.iterrows(): + print(f"COMMENT: {row.comments}") + print(f"SCORE: {row.scores}") + print(f"TITLE: {row.title}") + print(f"URL: {row.html_url}") + print("=" * 50) + print() +``` + +```python out +""" +COMMENT: Requiring online connection is a deal breaker in some cases unfortunately so it'd be great if offline mode is added similar to how `transformers` loads models offline fine. + +@mandubian's second bullet point suggests that there's a workaround allowing you to use your offline (custom?) dataset with `datasets`. Could you please elaborate on how that should look like? +SCORE: 25.505046844482422 +TITLE: Discussion using datasets in offline mode +URL: https://github.com/huggingface/datasets/issues/824 +================================================== + +COMMENT: The local dataset builders (csv, text , json and pandas) are now part of the `datasets` package since #1726 :) +You can now use them offline +\`\`\`python +datasets = load_dataset("text", data_files=data_files) +\`\`\` + +We'll do a new release soon +SCORE: 24.555509567260742 +TITLE: Discussion using datasets in offline mode +URL: https://github.com/huggingface/datasets/issues/824 +================================================== + +COMMENT: I opened a PR that allows to reload modules that have already been loaded once even if there's no internet. + +Let me know if you know other ways that can make the offline mode experience better. I'd be happy to add them :) + +I already note the "freeze" modules option, to prevent local modules updates. It would be a cool feature. + +---------- + +> @mandubian's second bullet point suggests that there's a workaround allowing you to use your offline (custom?) dataset with `datasets`. Could you please elaborate on how that should look like? + +Indeed `load_dataset` allows to load remote dataset script (squad, glue, etc.) but also you own local ones. +For example if you have a dataset script at `./my_dataset/my_dataset.py` then you can do +\`\`\`python +load_dataset("./my_dataset") +\`\`\` +and the dataset script will generate your dataset once and for all. + +---------- + +About I'm looking into having `csv`, `json`, `text`, `pandas` dataset builders already included in the `datasets` package, so that they are available offline by default, as opposed to the other datasets that require the script to be downloaded. +cf #1724 +SCORE: 24.14896583557129 +TITLE: Discussion using datasets in offline mode +URL: https://github.com/huggingface/datasets/issues/824 +================================================== + +COMMENT: > here is my way to load a dataset offline, but it **requires** an online machine +> +> 1. (online machine) +> +> ``` +> +> import datasets +> +> data = datasets.load_dataset(...) +> +> data.save_to_disk(/YOUR/DATASET/DIR) +> +> ``` +> +> 2. copy the dir from online to the offline machine +> +> 3. (offline machine) +> +> ``` +> +> import datasets +> +> data = datasets.load_from_disk(/SAVED/DATA/DIR) +> +> ``` +> +> +> +> HTH. + + +SCORE: 22.893993377685547 +TITLE: Discussion using datasets in offline mode +URL: https://github.com/huggingface/datasets/issues/824 +================================================== + +COMMENT: here is my way to load a dataset offline, but it **requires** an online machine +1. (online machine) +\`\`\` +import datasets +data = datasets.load_dataset(...) +data.save_to_disk(/YOUR/DATASET/DIR) +\`\`\` +2. copy the dir from online to the offline machine +3. (offline machine) +\`\`\` +import datasets +data = datasets.load_from_disk(/SAVED/DATA/DIR) +\`\`\` + +HTH. +SCORE: 22.406635284423828 +TITLE: Discussion using datasets in offline mode +URL: https://github.com/huggingface/datasets/issues/824 +================================================== +""" +``` + +¡No está mal! El segundo comentario parece responder la pregunta. + + + +✏️ **¡Inténtalo!** Crea tu propia pregunta y prueba si puedes encontrar una respuesta en los documentos devueltos. Puede que tengas que incrementar el parámetro `k` en `Dataset.get_nearest_examples()` para aumentar la búsqueda. + + \ No newline at end of file diff --git a/chapters/es/chapter5/7.mdx b/chapters/es/chapter5/7.mdx new file mode 100644 index 000000000..1b13a989b --- /dev/null +++ b/chapters/es/chapter5/7.mdx @@ -0,0 +1,16 @@ +# 🤗 Datasets, ¡listo! + + + +Bueno, ese fue un gran tour de la librería 🤗 Datasets. ¡Felicitaciones por llegar hasta aquí! Con el conocimiento que adquiriste en este capítulo, deberías ser capaz de: + +- Cargar datasets de cualquier parte, sea del Hub de Hugging Face, tu computador o un servidor remoto en tu compañía. +- Preparar tus datos usando una combinación de las funciones `Dataset.map()` y `Dataset.filter()`. +- Cambiar rápidamente entre formatos de datos como Pandas y NumPy usando `Dataset.set_format()`. +- Crear tu propio dataset y subirlo al Hub de Hugging Face. +- Procesar tus documentos usando un modelo de Transformer y construir un motor de búsqueda semántica usando FAISS. + +En el [Capítulo 7](/course/chapter7) pondremos todo esto en práctica cuando veamos a profundidad las tareas de PLN en las que son buenos los modelos de Transformers. Antes de seguir, ¡es hora de poner a prueba tu conocimiento de 🤗 Datasets con un quiz! diff --git a/chapters/es/chapter5/8.mdx b/chapters/es/chapter5/8.mdx new file mode 100644 index 000000000..4718d8faf --- /dev/null +++ b/chapters/es/chapter5/8.mdx @@ -0,0 +1,231 @@ + + +# Quiz + + + +¡Vimos muchas cosas en este capítulo! No te preocupes si no te quedaron claros todos los detalles; los siguientes capítulos te ayudarán a entender cómo funcionan las cosas internamente. + +Antes de seguir, probemos lo que aprendiste en este capítulo: + +### 1. ¿Desde qué ubicaciones te permite cargar datasets la función `load_dataset()` en 🤗 Datasets? + +data_files de load_dataset() para cargar datasets locales.", + correct: true + }, + { + text: "El Hub de Hugging Face", + explain: "¡Correcto! Puedes cargar datasets del Hub pasando el ID del dataset, e.g. load_dataset('emotion').", + correct: true + }, + { + text: "Un servidor remoto", + explain: "¡Correcto! Puedes pasar URL al argumento data_files de la función load_dataset() psara cargar archivos remotos.", + correct: true + }, + ]} +/> + +### 2. Supón que cargas una de las tareas de GLUE así: + +```py +from datasets import load_dataset + +dataset = load_dataset("glue", "mrpc", split="train") +``` + +¿Cuál de los sigientes comandos a a producir una muestra aleatoria de 50 elementos de `dataset`? + +dataset.sample(50)", + explain: "Esto es incorrecto. No hay un método Dataset.sample()." + }, + { + text: "dataset.shuffle().select(range(50))", + explain: "¡Correcto! Como viste en el capítulo, primero tienes que ordenar aleatoriamente el dataset y luego seleccionar las muestras.", + correct: true + }, + { + text: "dataset.select(range(50)).shuffle()", + explain: "Esto es incorrecto. Si bien el código se va a ejecutar, sólo va a ordenar aleatoriamente los primeros 50 elementos del dataset." + } + ]} +/> + +### 3. Supón que tienes un dataset sobre mascotas llamado `pets_dataset`, que tiene una columna `name` que contiene el nombre de cada mascota. ¿Cuál de los siguientes acercamientos te permitiría filtrar el dataset para todas las mascotas cuyos nombres comienzan con la letra "L"? + +pets_dataset.filter(lambda x : x['name'].startswith('L'))", + explain: "¡Correcto! Usar una función lambda de Python para este tipo de filtros es una gran idea. ¿Se te ocurre otra solución?", + correct: true + }, + { + text: "pets_dataset.filter(lambda x['name'].startswith('L'))", + explain: "Esto es incorrecrto. Una función lambda toma la forma general lambda *arguments* : *expression*, así que tienes que definir los argumentos en este caso." + }, + { + text: "Crear una funcióin como def filter_names(x): return x['name'].startswith('L') y ejecutar pets_dataset.filter(filter_names).", + explain: "¡Correcto! Justo como con Dataset.map(), puedes pasar funciones explícitas a Dataset.filter(). Esto es útil cuando tienes una lógica compleja que no es adecuada para una función lambda. ¿Cuál de las otras soluciones podría funcionar?", + correct: true + } + ]} +/> + +### 4. ¿Qué es la proyección en memoria (_memory mapping_)? + + + +### 5. ¿Cuáles son los principales beneficios de la proyección en memoria? + + + +### 6. ¿Por qué no funciona el siguiente código? + +```py +from datasets import load_dataset + +dataset = load_dataset("allocine", streaming=True, split="train") +dataset[0] +``` + +IterableDataset.", + explain: "¡Correcto! Un IterableDataset es un generador, no un contenedor, así que deberías acceder a sus elementos usando next(iter(dataset)).", + correct: true + }, + { + text: "El dataset allocine no tiene un conjunto train.", + explain: "Incorrecto. Revisa la [tarjeta del dataset allocine](https://huggingface.co/datasets/allocine) en el Hub para ver qué conjuntos contiene." + } + ]} +/> + +### 7. ¿Cuáles son los principales beneficiones de crear una tarjeta para un dataset? + + + + +### 8. ¿Qué es la búsqueda semántica? + + + +### 9. Para la búsqueda semántica asimétrica, usualmente tienes: + + + +### 10. ¿Puedo usar 🤗 Datasets para cargar datos y usarlos en otras áreas, como procesamiento de habla? + +dataset MNIST en el Hub para un ejemplo de visión artificial." + }, + { + text: "Yes", + explain: "¡Correcto! Revisa los desarrollos con habla y visión en la librería 🤗 Transformers para ver cómo se puede usar 🤗 Datasets en estas áreas.", + correct : true + }, + ]} +/> diff --git a/chapters/fr/_toctree.yml b/chapters/fr/_toctree.yml index 2fd1d5873..58c4e9544 100644 --- a/chapters/fr/_toctree.yml +++ b/chapters/fr/_toctree.yml @@ -199,3 +199,8 @@ title: Événement de lancement de la partie 2 - local: events/3 title: Fête des blocs Gradio + +- title: Glossaire + sections: + - local: glossary/1 + title: Glossaire diff --git a/chapters/fr/chapter1/1.mdx b/chapters/fr/chapter1/1.mdx index 146e246ad..6ee2949cd 100644 --- a/chapters/fr/chapter1/1.mdx +++ b/chapters/fr/chapter1/1.mdx @@ -54,6 +54,55 @@ Après avoir terminé ce cours, nous vous recommandons de suivre la [Spécialisa **Leandro von Werra** est ingénieur en apprentissage machine dans l'équipe *open source* d'Hugging Face et également co-auteur du livre [*Natural Language Processing with Transformers*](https://www.oreilly.com/library/view/natural-language-processing/9781098136789/). Il a plusieurs années d'expérience dans l'industrie où il a pu déployer des projets de NLP en production et travailler sur toutes les étapes clefs du déploiement. + +## FAQ + +Voici quelques réponses aux questions fréquemment posées : + +- **Suivre ce cours mène-t-il à une certification ?** +Actuellement, nous n'avons pas de certification pour ce cours. Cependant, nous travaillons sur un programme de certification pour l'écosystème *Hugging Face*. Restez à l'écoute ! + +- **Combien de temps dois-je consacrer à ce cours ?** +Chaque chapitre de ce cours est conçu pour être complété en une semaine, avec environ 6 à 8 heures de travail par semaine. Cependant, vous pouvez prendre tout le temps nécessaire pour le suivre. + +- **Où puis-je poser une question si j'en ai une ?** +Si vous avez une question sur l'une des sections du cours, il vous suffit de cliquer sur la bannière « *Ask a question* » en haut de la page pour être automatiquement redirigé vers la bonne section du [forum d’*Hugging Face*] (https://discuss.huggingface.co/) : + +Link to the Hugging Face forums + +Notez qu'une liste d'[idées de projets](https://discuss.huggingface.co/c/course/course-event/25) est également disponible sur le forum si vous souhaitez pratiquer davantage une fois le cours terminé. + +- **Où puis-je obtenir le code du cours ?** +Pour chaque section, vous pouvez cliquer sur la bannière en haut de la page pour exécuter son code dans *Google Colab* ou *Amazon SageMaker Studio Lab* : + +Link to the Hugging Face course notebooks + +A noter que pour la version en français du cours, deux choix s’offrent à vous lorsque vous cliquez sur la bannière. Le premier est de sélectionner le *notebook* utilisant des modèles en anglais. L’intérêt est qu’il s’agit de celui sur lequel sont basées les explications du cours (interprétation des résultats, etc.). Le second est de sélectionner le *notebook* utilisant des modèles en français. Il s’agit alors d’une proposition d’adaptation (un modèle parmi tous ceux existant en français est utilisé). + +Si vous souhaitez accéder à l’ensemble des *notebooks* Jupyter du cours, il existe deux possibilités. La première est de cloner le dépôt [`huggingface/notebooks`](https://github.com/huggingface/notebooks) et de consulter les *notebooks* contenus dans le dossier *course*. La seconde est de générer les *notebooks* localement en suivant les instructions dans le *README* du dépôt [`course`](https://github.com/huggingface/course#-jupyter-notebooks) sur GitHub. + +- **Comment puis-je contribuer au cours ?** +Il existe de nombreuses façons de contribuer au cours ! Si vous trouvez une coquille ou un bug, veuillez ouvrir une « *Issue* » sur le dépôt [`course`](https://github.com/huggingface/course). Si vous souhaitez aider à traduire le cours dans votre langue maternelle, consultez les instructions [ici](https://github.com/huggingface/course#translating-the-course-into-your-language). + +- **Quels ont été les choix retenus pour la traduction ?** +Vous pouvez consulter le [glossaire](https://huggingface.co/course/fr/glossary/1) détaillant les choix retenus pour la traduction vers le français. + +- **Peut-on réutiliser ce cours?** +Bien sûr ! Le cours est publié sous la licence [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0.html). Cela signifie que vous devez créditer de manière appropriée, fournir un lien vers la licence et indiquer si des modifications ont été apportées. Vous pouvez le faire de toute manière raisonnable, mais pas d'une façon qui suggère que le distributeur de la licence vous approuve ou approuve votre utilisation. Si vous souhaitez citer le cours, veuillez utiliser le BibTeX suivant : + +``` +@misc{huggingfacecourse, + author = {Hugging Face}, + title = {The Hugging Face Course, 2022}, + howpublished = "\url{https://huggingface.co/course}", + year = {2022}, + note = "[Online; accessed ]" +} +``` + + +## C'est parti ! + Êtes-vous prêt à commencer ? Dans ce chapitre, vous apprendrez : * à utiliser la fonction `pipeline()` pour résoudre des problèmes de NLP comme la génération de texte et la classification, * l'architecture d'un *transformer*, diff --git a/chapters/fr/chapter1/3.mdx b/chapters/fr/chapter1/3.mdx index c61350d6c..5ba235a27 100644 --- a/chapters/fr/chapter1/3.mdx +++ b/chapters/fr/chapter1/3.mdx @@ -133,14 +133,15 @@ generator( ```python out [{'generated_text': 'In this course, we will teach you how to understand and use ' - # Dans ce cours, nous vous enseignerons comment comprendre et utiliser + # Dans ce cours, nous vous enseignerons comment comprendre et utiliser 'data flow and data interchange when handling user data. We ' - # flux de données et l'échange de données lors de la manipulation des données utilisateur. Nous + # flux de données et l'échange de données lors de la manipulation des données utilisateur. Nous 'will be working with one or more of the most commonly used ' - # travailleront avec un ou plusieurs des plus couramment utilisés + # travaillerons avec un ou plusieurs des plus couramment utilisés 'data flows — data flows of various types, as seen by the ' - # flux de données - flux de données de différents types, tels qu'ils sont vus par - 'HTTP'}] # HTTP + # flux de données - flux de données de différents types, tels qu'ils sont vus par + 'HTTP'}] + # HTTP ``` Il est possible de contrôler le nombre de séquences générées avec l'argument `num_return_sequences` et la longueur totale du texte généré avec l'argument `max_length`. @@ -172,15 +173,15 @@ generator( ```python out [{'generated_text': 'In this course, we will teach you how to manipulate the world and ' - # Dans ce cours, nous vous enseignerons comment manipuler le monde et + # Dans ce cours, nous vous enseignerons comment manipuler le monde et 'move your mental and physical capabilities to your advantage.'}, - # utiliser vos capacités mentales et physiques à votre avantage. + # utiliser vos capacités mentales et physiques à votre avantage. {'generated_text': 'In this course, we will teach you how to become an expert and ' - # Dans ce cours, nous vous apprendrons comment devenir un expert et + # Dans ce cours, nous vous apprendrons comment devenir un expert et 'practice realtime, and with a hands on experience on both real ' - # pratique en temps réel, et avec une expérience pratique à la fois sur de vrais + # pratique en temps réel, et avec une expérience pratique à la fois sur de vrais 'time and real'}] - # temps et réel + # temps et réel ``` Vous pouvez améliorer votre recherche de modèle en cliquant sur les *filtres* de langue et choisir un modèle qui génère du texte dans une autre langue. Le *Hub* contient également des *checkpoints* pour des modèles multilingues qui supportent plusieurs langues. @@ -337,19 +338,19 @@ summarizer( ```python out [{'summary_text': ' America has changed dramatically during recent years . The ' - # L'Amérique a changé de façon spectaculaire au cours des dernières années. Le + # L'Amérique a changé de façon spectaculaire au cours des dernières années. Le 'number of engineering graduates in the U.S. has declined in ' - # nombre de diplômés en ingénierie aux États-Unis a diminué dans + # nombre de diplômés en ingénierie aux États-Unis a diminué dans 'traditional engineering disciplines such as mechanical, civil ' - # dans les disciplines traditionnelles de l'ingénierie, telles que le génie mécanique, civil + # les disciplines traditionnelles de l'ingénierie, comme le génie mécanique, civil ', electrical, chemical, and aeronautical engineering . Rapidly ' - # l'électricité, la chimie et l'aéronautique. Les économies + # l'électricité, la chimie et l'aéronautique. Les économies 'developing economies such as China and India, as well as other ' - # en développement rapide comme la Chine et l'Inde, ainsi que d'autres + # en développement rapide comme la Chine et l'Inde, ainsi que d'autres 'industrial countries in Europe and Asia, continue to encourage ' - # pays industriels d'Europe et d'Asie, continuent d'encourager + # pays industriels d'Europe et d'Asie, continuent d'encourager 'and advance engineering.'}] - # et à faire progresser l'ingénierie. + # et à faire progresser l'ingénierie. ``` Comme pour la génération de texte, vous pouvez spécifier une `max_length` (longueur maximale) ou une `min_length` (longueur minimale) pour le résultat. diff --git a/chapters/fr/chapter7/2.mdx b/chapters/fr/chapter7/2.mdx index 8084e0df9..804e7fb82 100644 --- a/chapters/fr/chapter7/2.mdx +++ b/chapters/fr/chapter7/2.mdx @@ -371,7 +371,7 @@ Comme nous pouvons le voir, le deuxième jeu d'étiquettes a été complété à {:else} -Notre assembleur de données est prêt à fonctionner ! Maintenant, utilisons-le pour créer un `tf.data.Dataset` avec la méthode `to_tf_dataset()`. +Notre assembleur de données est prêt à fonctionner ! Maintenant, utilisons-le pour créer un `tf.data.Dataset` avec la méthode `to_tf_dataset()`. Une alternative est d'utiliser `model.prepare_tf_dataset()` pour faire cela qui prend un peu moins de code passe-partout. Vous verrez cela dans certaines des autres sections de ce chapitre. ```py tf_train_dataset = tokenized_datasets["train"].to_tf_dataset( @@ -616,7 +616,7 @@ import numpy as np all_predictions = [] all_labels = [] for batch in tf_eval_dataset: - logits = model.predict(batch)["logits"] + logits = model.predict_on_batch(batch)["logits"] labels = batch["labels"] predictions = np.argmax(logits, axis=-1) for prediction, label in zip(predictions, labels): diff --git a/chapters/fr/chapter7/3.mdx b/chapters/fr/chapter7/3.mdx index 4ca41a4b5..0579e6af9 100644 --- a/chapters/fr/chapter7/3.mdx +++ b/chapters/fr/chapter7/3.mdx @@ -637,18 +637,18 @@ dans votre terminal préféré et connectez-vous là. {#if fw === 'tf'} -Une fois que nous sommes connectés, nous pouvons créer nos jeux de données `tf.data`. Nous n'utiliserons ici que le collecteur de données standard, mais vous pouvez également essayer le collecteur de masquage de mots entiers et comparer les résultats à titre d'exercice : +Une fois que nous sommes connectés, nous pouvons créer nos jeux de données `tf.data`. Pour ce faire, nous utiliserons la méthode `prepare_tf_dataset()`, qui utilise notre modèle pour déduire automatiquement quelles colonnes doivent aller dans le jeu de données. Si vous voulez contrôler exactement les colonnes à utiliser, vous pouvez utiliser la méthode `Dataset.to_tf_dataset()` à la place. Pour garder les choses simples, nous n'utiliserons ici que le l’assembleur de données standard, mais vous pouvez aussi essayer l’assembleur masquant des mots entiers et comparer les résultats à titre d'exercice : ```python -tf_train_dataset = downsampled_dataset["train"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_train_dataset = model.prepare_tf_dataset( + downsampled_dataset["train"], collate_fn=data_collator, shuffle=True, batch_size=32, ) -tf_eval_dataset = downsampled_dataset["test"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_eval_dataset = model.prepare_tf_dataset( + downsampled_dataset["test"], collate_fn=data_collator, shuffle=False, batch_size=32, @@ -676,6 +676,7 @@ model.compile(optimizer=optimizer) # Entraîner en mixed-precision float16 tf.keras.mixed_precision.set_global_policy("mixed_float16") +model_name = model_checkpoint.split("/")[-1] callback = PushToHubCallback( output_dir=f"{model_name}-finetuned-imdb", tokenizer=tokenizer ) diff --git a/chapters/fr/chapter7/4.mdx b/chapters/fr/chapter7/4.mdx index 67a1f954b..db3d8640b 100644 --- a/chapters/fr/chapter7/4.mdx +++ b/chapters/fr/chapter7/4.mdx @@ -388,14 +388,14 @@ Nous allons transmettre ce `data_collator` au `Seq2SeqTrainer`. Ensuite, jetons Nous pouvons maintenant utiliser ce `data_collator` pour convertir chacun de nos jeux de données en un `tf.data.Dataset`, prêt pour l'entraînement : ```python -tf_train_dataset = tokenized_datasets["train"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +model.prepare_tf_dataset( + tokenized_datasets["train"], collate_fn=data_collator, shuffle=True, batch_size=32, ) -tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_eval_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], collate_fn=data_collator, shuffle=False, batch_size=16, @@ -505,28 +505,41 @@ Le score peut aller de 0 à 100. Plus il est élevé, mieux c'est. {#if fw === 'tf'} -Pour passer des sorties du modèle aux textes que la métrique peut utiliser, nous allons utiliser la méthode `tokenizer.batch_decode()`. Nous devons juste nettoyer tous les `-100` dans les étiquettes. Le *tokenizer* fera automatiquement la même chose pour le *token* de *padding*. Définissons une fonction qui prend notre modèle et un jeu de données et calcule des métriques sur ceux-ci. Comme la génération de longues séquences peut être lente, nous sous-échantillonnons l'ensemble de validation pour nous assurer que cela ne prend pas une éternité : +Pour passer des sorties du modèle aux textes utilisables par la métrique, nous allons utiliser la méthode `tokenizer.batch_decode()`. Nous devons juste nettoyer tous les `-100` dans les étiquettes. Le *tokenizer* fera automatiquement la même chose pour le *token* de remplissage. Définissons une fonction qui prend notre modèle et un jeu de données et calcule des métriques sur ceux-ci. Nous allons également utiliser une astuce qui augmente considérablement les performances : compiler notre code de génération avec [XLA](https://www.tensorflow.org/xla), le compilateur d'algèbre linéaire accéléré de TensorFlow. XLA applique diverses optimisations au graphe de calcul du modèle, ce qui permet d'améliorer considérablement la vitesse et l'utilisation de la mémoire. Comme décrit dans un article du [blog d’Hugging Face](https://huggingface.co/blog/tf-xla-generate), XLA fonctionne mieux lorsque nos formes d'entrée ne varient pas trop. Pour gérer cela, nous allons rembourrer nos entrées à des multiples de 128, et créer un nouveau jeu de données avec l’assembleur de rembourrage. Puis nous appliquerons le décorateur `@tf.function(jit_compile=True)` à notre fonction de génération, qui marque la fonction entière pour la compilation avec XLA. ```py import numpy as np +import tensorflow as tf +from tqdm import tqdm + +generation_data_collator = DataCollatorForSeq2Seq( + tokenizer, model=model, return_tensors="tf", pad_to_multiple_of=128 +) +tf_generate_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], + collate_fn=generation_data_collator, + shuffle=False, + batch_size=8, +) + + +@tf.function(jit_compile=True) +def generate_with_xla(batch): + return model.generate( + input_ids=batch["input_ids"], + attention_mask=batch["attention_mask"], + max_new_tokens=128, + ) def compute_metrics(): all_preds = [] all_labels = [] - sampled_dataset = tokenized_datasets["validation"].shuffle().select(range(200)) - tf_generate_dataset = sampled_dataset.to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], - collate_fn=data_collator, - shuffle=False, - batch_size=4, - ) - for batch in tf_generate_dataset: - predictions = model.generate( - input_ids=batch["input_ids"], attention_mask=batch["attention_mask"] - ) + + for batch, labels in tqdm(tf_generate_dataset): + predictions = generate_with_xla(batch) decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) - labels = batch["labels"].numpy() + labels = labels.numpy() labels = np.where(labels != -100, labels, tokenizer.pad_token_id) decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) decoded_preds = [pred.strip() for pred in decoded_preds] diff --git a/chapters/fr/chapter7/5.mdx b/chapters/fr/chapter7/5.mdx index 3f317d0e9..148bc1af5 100644 --- a/chapters/fr/chapter7/5.mdx +++ b/chapters/fr/chapter7/5.mdx @@ -305,14 +305,13 @@ max_target_length = 30 def preprocess_function(examples): model_inputs = tokenizer( - examples["review_body"], max_length=max_input_length, truncation=True + examples["review_body"], + max_length=max_input_length, + truncation=True, + ) + labels = tokenizer( + examples["review_title"], max_length=max_target_length, truncation=True ) - # Configurer le tokenizer pour les cibles - with tokenizer.as_target_tokenizer(): - labels = tokenizer( - examples["review_title"], max_length=max_target_length, truncation=True - ) - model_inputs["labels"] = labels["input_ids"] return model_inputs ``` @@ -701,14 +700,14 @@ Pour conclure cette section, voyons comment nous pouvons également *finetuner* Nous sommes presque prêts à nous entraîner ! Nous devons juste convertir nos jeux de données en `tf.data.Dataset` en utilisant le assembleur de données que nous avons défini ci-dessus, puis utiliser `compile()` et `fit()`. D'abord, les jeux de données : ```python -tf_train_dataset = tokenized_datasets["train"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_train_dataset = model.prepare_tf_dataset( + tokenized_datasets["train"], collate_fn=data_collator, shuffle=True, batch_size=8, ) -tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_eval_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], collate_fn=data_collator, shuffle=False, batch_size=8, @@ -755,18 +754,39 @@ model.fit( ) ``` -Nous avons obtenu quelques valeurs de perte pendant l'entraînement, mais nous aimerions vraiment voir les métriques ROUGE que nous avons calculées plus tôt. Pour obtenir ces métriques, nous devons générer les sorties du modèle et les convertir en chaînes de caractères. Construisons quelques listes d'étiquettes et de prédictions pour comparer la métrique ROUGE (notez que si vous obtenez des erreurs d'importation pour cette section, vous pouvez avoir besoin de "pip install tqdm") : +Nous avons obtenu quelques valeurs de perte pendant l'entraînement mais nous aimerions voir les métriques ROUGE que nous avons calculées plus tôt. Pour obtenir ces métriques, nous devons générer les sorties du modèle et les convertir en chaînes de caractères. Construisons une liste d'étiquettes et une liste de prédictions pour la métrique ROUGE pour comparer (notez que si vous obtenez des erreurs d'importation pour cette section, vous pouvez avoir besoin de faire `pip install tqdm`). Nous allons également utiliser une astuce qui augmente considérablement les performances : compiler notre code de génération avec [XLA](https://www.tensorflow.org/xla), le compilateur d'algèbre linéaire accéléré de TensorFlow. XLA applique diverses optimisations au graphe de calcul du modèle, ce qui permet d'améliorer considérablement la vitesse et l'utilisation de la mémoire. Comme décrit dans un article du [blog d’Hugging Face](https://huggingface.co/blog/tf-xla-generate), XLA fonctionne mieux lorsque nos formes d'entrée ne varient pas trop. Pour gérer cela, nous allons rembourrer nos entrées à des multiples de 128, et créer un nouveau jeu de données avec l’assembleur de rembourrage. Puis nous appliquerons le décorateur `@tf.function(jit_compile=True)` à notre fonction de génération, qui marque la fonction entière pour la compilation avec XLA. ```python from tqdm import tqdm import numpy as np +generation_data_collator = DataCollatorForSeq2Seq( + tokenizer, model=model, return_tensors="tf", pad_to_multiple_of=320 +) +tf_generate_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], + collate_fn=generation_data_collator, + shuffle=False, + batch_size=8, + drop_remainder=True, +) + + +@tf.function(jit_compile=True) +def generate_with_xla(batch): + return model.generate( + input_ids=batch["input_ids"], + attention_mask=batch["attention_mask"], + max_new_tokens=32, + ) + + all_preds = [] all_labels = [] -for batch in tqdm(tf_eval_dataset): - predictions = model.generate(**batch) +for batch, labels in tqdm(tf_generate_dataset): + predictions = generate_with_xla(batch) decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) - labels = batch["labels"].numpy() + labels = labels.numpy() labels = np.where(labels != -100, labels, tokenizer.pad_token_id) decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) decoded_preds = ["\n".join(sent_tokenize(pred.strip())) for pred in decoded_preds] diff --git a/chapters/fr/chapter7/6.mdx b/chapters/fr/chapter7/6.mdx index 4d1d27e59..0ff1ce37b 100644 --- a/chapters/fr/chapter7/6.mdx +++ b/chapters/fr/chapter7/6.mdx @@ -375,17 +375,17 @@ Nous pouvons voir que les exemples ont été empilés et que tous les tenseurs o {#if fw === 'tf'} -Maintenant nous pouvons utiliser la méthode `to_tf_dataset()` pour convertir nos jeux de données en jeux de données TensorFlow avec l'assembleur de données que nous avons créé ci-dessus : +Maintenant nous pouvons utiliser la méthode `prepare_tf_dataset()` pour convertir nos jeux de données en jeux de données TensorFlow avec l'assembleur de données que nous avons créé ci-dessus : ```python -tf_train_dataset = tokenized_dataset["train"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_train_dataset = model.prepare_tf_dataset( + tokenized_dataset["train"], collate_fn=data_collator, shuffle=True, batch_size=32, ) -tf_eval_dataset = tokenized_dataset["valid"].to_tf_dataset( - columns=["input_ids", "attention_mask", "labels"], +tf_eval_dataset = model.prepare_tf_dataset( + tokenized_dataset["valid"], collate_fn=data_collator, shuffle=False, batch_size=32, @@ -511,7 +511,7 @@ model.fit(tf_train_dataset, validation_data=tf_eval_dataset, callbacks=[callback {:else} -💡 Si vous avez accès à une machine avec plusieurs GPUs, vous pouvez essayer d'utiliser `MirroredStrategy` pour accélérer considérablement l'entraînement. Vous devrez créer un objet `tf.distribute.MirroredStrategy` et vous assurer que les commandes `to_tf_dataset` ainsi que la création du modèle et l'appel à `fit()` sont tous exécutés dans `scope()`. Vous pouvez consulter la documentation à ce sujet [ici](https://www.tensorflow.org/guide/distributed_training#use_tfdistributestrategy_with_keras_modelfit). +💡 Si vous avez accès à une machine avec plusieurs GPUs, vous pouvez essayer d'utiliser `MirroredStrategy` pour accélérer considérablement l'entraînement. Vous devrez créer un objet `tf.distribute.MirroredStrategy` et vous assurer que toutes les méthodes `to_tf_dataset()` ou `prepare_tf_dataset()` ainsi que la création du modèle et l'appel à `fit()` sont tous exécutés dans `scope()`. Vous pouvez consulter la documentation à ce sujet [ici](https://www.tensorflow.org/guide/distributed_training#use_tfdistributestrategy_with_keras_modelfit). {/if} diff --git a/chapters/fr/chapter7/7.mdx b/chapters/fr/chapter7/7.mdx index 9bb9b046b..5ba430b64 100644 --- a/chapters/fr/chapter7/7.mdx +++ b/chapters/fr/chapter7/7.mdx @@ -883,20 +883,14 @@ data_collator = DefaultDataCollator(return_tensors="tf") Et maintenant nous créons les jeux de données comme d'habitude. ```python -tf_train_dataset = train_dataset.to_tf_dataset( - columns=[ - "input_ids", - "start_positions", - "end_positions", - "attention_mask", - "token_type_ids", - ], +tf_train_dataset = model.prepare_tf_dataset( + train_dataset, collate_fn=data_collator, shuffle=True, batch_size=16, ) -tf_eval_dataset = validation_dataset.to_tf_dataset( - columns=["input_ids", "attention_mask", "token_type_ids"], +tf_eval_dataset = model.prepare_tf_dataset( + validation_dataset, collate_fn=data_collator, shuffle=False, batch_size=16, diff --git a/chapters/fr/glossary/1.mdx b/chapters/fr/glossary/1.mdx new file mode 100644 index 000000000..0c15c86b0 --- /dev/null +++ b/chapters/fr/glossary/1.mdx @@ -0,0 +1,63 @@ +# Glossaire + +| Original | Français | +|-----------------------------|--------------------------------- | +| Accuracy | Précision | +| Backward Pass | Passe arrière | +| Batch | *Batch* | +| Benchmark | *Benchmark* | +| Cache | Cache | +| Chapter | Chapitre | +| Checkpoint | *Checkpoint* (plus rarement « point de sauvegarde ») | +| Colab Notebook | *Notebook* Google Colab | +| Colator function | Fonction d'assemblement | +| Command | Commande | +| Configuration | Configuration | +| Course | Cours | +| Dataloader | Chargeur de données | +| Dependency | Dépendances | +| Deployment | Déploiement | +| Development | Développement | +| Dictionary | Dictionnaire | +| Download | Télécharger | +| Feature | Variable | +| Field | Champ | +| Fine-tuning | Finetuning | +| Folder | Dossier | +| Forward Pass | Passe avant | +| Google | *Google* | +| Hugging Face | *Hugging Face* | +| Inference | Inférence | +| Learning rate | Taux d’apprentissage | +| Library | Bibliothèque | +| Linux | Linux | +| Loss function | Fonction de perte/coût | +| Loop | Boucle | +| macOS | macOS | +| Model | Modèle | +| Hugging Face Hub | *Hub* d’*Hugging Face* | +| Module | Module | +| Natural Language Processing | Traitement du langage naturel | +| Package | Paquet | +| Padding | Rembourrage | +| Parameter | Paramètre | +| Python | Python | +| PyTorch | PyTorch | +| Samples | Echantillons | +| Scheduler | Planificateur | +| Script | Script | +| Setup | Installation | +| TensorFlow | TensorFlow | +| Terminal | Terminal | +| Tokenizer | Tokeniseur | +| Train | Entraîner | +| Transformer | *Transformer* | +| Virtual Environment | Environnement virtuel | +| Weight decay | Taux de décroissance des poids | +| Weights | Poids | +| Windows | *Windows* | +| Working Environment | Environnement de travail | + + +A noter que les mots anglais non traduits sont indiqués en italique dans le cours. +De plus, les abréviations techniques comme API, GPU, TPU, etc. ne sont pas traduites. diff --git a/chapters/ja/_toctree.yml b/chapters/ja/_toctree.yml index c1d0fcd7f..fb3be77b8 100644 --- a/chapters/ja/_toctree.yml +++ b/chapters/ja/_toctree.yml @@ -7,6 +7,24 @@ sections: - local: chapter1/1 title: イントロダクション + - local: chapter1/2 + title: 自然言語処理 / NLP(Natural Language Processing) + - local: chapter1/3 + title: Transformersで何ができる? + - local: chapter1/4 + title: Transformersの仕組みについて + - local: chapter1/5 + title: エンコーダーモデル + - local: chapter1/6 + title: デコーダーモデル + - local: chapter1/7 + title: Sequence-to-sequence モデル + - local: chapter1/8 + title: バイアスと限界 + - local: chapter1/9 + title: まとめ + - local: chapter1/10 + title: 章末クイズ - title: 4. モデルとトークナイザーの共有 sections: diff --git a/chapters/ja/chapter1/10.mdx b/chapters/ja/chapter1/10.mdx new file mode 100644 index 000000000..11b70518a --- /dev/null +++ b/chapters/ja/chapter1/10.mdx @@ -0,0 +1,262 @@ + + +# 章末クイズ + + + +この章では多くの物事を学びました!詳細を把握できなくても安心してください。次の章はどのようにこれらのツールが動いているか理解する上で役に立ちます。 + +まずは、この章で学んだことを確かめましょう! + + +### 1.Hubを探索して`roberta-large-mnli`チェックポイントを見つけましょう。 このモデルはどのタスクに適していますか? + +ページを見てみましょう。" + }, + { + text: "文章分類", + explain: "より正確には2つの文が論理的にどのような関係を持つか、3つのラベル(矛盾、中立、含意)について分類します。このタスクは自然言語推論とも呼ばれます。", + correct: true + }, + { + text: "文章生成", + explain: "もう一度roberta-large-mnliのページを見てみましょう。" + } + ]} +/> + +### 2.次のコードは何を返しますか? + +```py +from transformers import pipeline + +ner = pipeline("ner", grouped_entities=True) +ner("My name is Sylvain and I work at Hugging Face in Brooklyn.") +``` + +sentiment-analysis パイプラインを用いたときの動作です。" + }, + { + text: "この文章を完結させるための生成された文を返します。", + explain: "間違いです。それは text-generation パイプラインを用いたときの動作です。" + }, + { + text: "この文中の人物、団体、場所を表す単語を返します。", + explain: "さらに、grouped_entities=Trueを用いると、同じエンティティに属する単語をグループ化します。", + correct: true + } + ]} +/> + +### 3. このサンプルコードでは...をどのように置き換えればよいでしょうか? + +```py +from transformers import pipeline + +filler = pipeline("fill-mask", model="bert-base-cased") +result = filler("...") +``` + + has been waiting for you.", + explain: "間違いです。bert-base-casedのモデルカードをチェックして、あなたの間違いを見つけましょう。" + }, + { + text: "This [MASK] has been waiting for you.", + explain: "正解!このモデルのマスクトークンは[MASK]です。", + correct: true + }, + { + text: "This man has been waiting for you.", + explain: "間違いです。このパイプラインはマスクされた単語を埋めるので、どこかにマスクトークンが必要です。" + } + ]} +/> + +### 4. なぜこのコードは動かないのでしょうか? + +```py +from transformers import pipeline + +classifier = pipeline("zero-shot-classification") +result = classifier("This is a course about the Transformers library") +``` + +candidate_labels=[...]を含める必要があります。", + correct: true + }, + { + text: "このパイプラインには、一つの文だけでなく複数の文が必要です。", + explain: "これは間違いです。しかし、適切に使用すれば、このパイプラインは処理する文のリストを受け取ることができます(他のパイプラインも同様です)。" + }, + { + text: "いつもどおり、この🤗Transformersライブラリは壊れています。", + explain: "ノーコメント!" + }, + { + text: "このパイプラインはもっと長い入力が必要です。この入力は短すぎます。", + explain: "これは間違いです。ただし、とても長い文をこのパイプラインで処理すると、切り捨てられることに注意してください。" + } + ]} +/> + +### 5. 転移学習とはどのような意味ですか? + + + + +### 6. マルバツクイズ、言語モデルの事前学習にラベルは通常は必要ない? + + +自己教師あり学習で行われます。つまり、ラベルは入力から自動的に作成されます(例えば、次の単語を予測したり、マスクされた単語を埋めたりといったように)。", + correct: true + }, + { + text: "バツ", + explain: "これは正しい回答ではありません。" + } + ]} +/> + +### 7.「モデル」、「アーキテクチャ」、「重み」という用語を最も適切に説明している文を選んでください。 + + + + +### 8. 生成された文でプロンプトを完成させるために使うモデルはどれでしょうか? + + + + +### 9. 文章要約タスクに使うモデルはどれでしょうか? + + + +### 10. 入力された文を特定のラベルに分類したいときに使うモデルはどれでしょうか? + + + + +### 11. モデルが持つバイアスはどのような要因で生じますか? + + + diff --git a/chapters/ja/chapter1/2.mdx b/chapters/ja/chapter1/2.mdx new file mode 100644 index 000000000..11fb226da --- /dev/null +++ b/chapters/ja/chapter1/2.mdx @@ -0,0 +1,26 @@ +# 自然言語処理 / NLP(Natural Language Processing) + + + +Transformerモデルの詳細に飛び込んでいく前に、自然言語処理とはどんなもので、かつ、なぜ我々が注目する必要があるのかの大まかな概要を知っていきましょう。 + +## 自然言語処理とはどんなもの? + +自然言語処理とは、人の言語に関連した全てのことへの理解に焦点を当てた、言語学と機械学習の分野です。自然言語処理タスクの目標は、文章を個別に一単語ずつ理解するだけでなく、それらの単語で構成された文章の文脈を理解することです。 + +以下のリストで、具体例付きで一般的な自然言語処理タスクを紹介します。 + +- **文章の分類**:レビューの評価、スパムメールの検出、文法的に正しいかどうかの判断、2つの文が論理的に関連しているかどうかの判断 +- **文の中の単語分類**:品詞(名詞、動詞、形容詞)や、固有表現(人、場所、組織)の識別 +- **文章内容の生成**:自動生成されたテキストによる入力テキストの補完、文章の穴埋め +- **文章からの情報抽出**:質問と文脈が与えられたときの、文脈からの情報に基づいた質問に対する答えの抽出 +- **文章の変換**:ある文章の他の言語への翻訳、文章の要約 + +さらに、自然言語処理は文章に限ったものではありません。音声認識やコンピュータビジョンの分野でも、音声サンプルの書き起こしや画像の説明文の生成など、複雑な課題に取り組んでいます。 + +## なぜ自然言語処理は困難なのか? + +コンピュータは人間と同じように情報を処理するわけではありません。例えば、「私はお腹が空いています。」という文章を読むと、人間はその意味を簡単に理解することができます。同様に、「私はお腹が空いています。」と「私は悲しいです。」という2つの文章があれば、その類似性を人間は簡単に判断することができます。しかし、機械学習(ML)モデルにおいては、このようなタスクはより困難です。機械学習モデルが学習できるように、テキストを処理する必要があります。また、言語は複雑なため、どのように処理すべきかを慎重に考える必要があります。テキストをどのように表現するかについては多くの研究がなされており、次の章ではいくつかの方法について見ていきます。 diff --git a/chapters/ja/chapter1/3.mdx b/chapters/ja/chapter1/3.mdx new file mode 100644 index 000000000..93803b215 --- /dev/null +++ b/chapters/ja/chapter1/3.mdx @@ -0,0 +1,332 @@ +# Transformersで何ができる? + + + + +このセクションでは、Transformerモデルができることを見ていき、🤗 Transformersライブラリの最初のツールとして `pipeline()` 関数を使ってみましょう。 + + +👀 右上にOpen in Colabというボタンがありますよね?それをクリックすると、このセクションのすべてのコードサンプルを含むGoogle Colabノートブックが開きます。このボタンは、コードサンプルを含むどのセクションにも存在します。 + +ローカルでサンプルを実行したい場合は、セットアップを参照することをお勧めします。 + + +## Transformersは至るところに! + +Transformerモデルは前節で述べたようなあらゆる種類のNLPタスクを解決するために使用されています。ここでは、Hugging FaceとTransformerモデルを使用している企業や組織を紹介します。それらの組織はまた、モデルを共有することでコミュニティに還元しています。 + +Companies using Hugging Face + +[🤗 Transformers library](https://github.com/huggingface/transformers)は、それらの共有モデルを作成し、使用するための機能を提供します。[Model Hub](https://huggingface.co/models)には、誰でもダウンロードして使用できる何千もの事前学習済みモデルが含まれています。また、あなた自身のモデルをModel Hubにアップロードすることも可能です。 + + +⚠️Hugging Face Hubはトランスフォーマーモデルに限定されるものではありません。誰でも好きな種類のモデルやデータセットを共有することができます!すべての利用可能な機能の恩恵を受けるためにhuggingface.coのアカウントを作成しましょう! + + +
+ +Transformerのモデルがどのように機能するのかを知る前に、いくつかの興味深いNLPの問題を解決するため、Transformerがどのように使われるのか、いくつかの例で見ていきましょう。 + +## pipelineを使ってタスクを実行する + + + +🤗 Transformers ライブラリの中で最も基本的なオブジェクトは `pipeline()` 関数です。これはモデルを必要な前処理と後処理のステップに接続し、任意のテキストを直接入力して理解しやすい答えを得ることを可能にします。 + +```python +from transformers import pipeline + +classifier = pipeline("sentiment-analysis") +classifier("I've been waiting for a HuggingFace course my whole life.") +``` + +```python out +[{'label': 'POSITIVE', 'score': 0.9598047137260437}] +``` + +複数の文章を入力することも可能です。 + +```python +classifier( + ["I've been waiting for a HuggingFace course my whole life.", "I hate this so much!"] +) +``` + +```python out +[{'label': 'POSITIVE', 'score': 0.9598047137260437}, + {'label': 'NEGATIVE', 'score': 0.9994558095932007}] +``` + +デフォルトでは、このpipelineは英語の感情分析用にファインチューニングされた特定の事前学習モデルを使用します。このモデルは `classifier` オブジェクトを作成する際にダウンロードされ、キャッシュされます。コマンドを再実行すると、キャッシュされたモデルが代わりに使用され、モデルを再度ダウンロードする必要はありません。 + +pipelineにテキストを渡す場合、主に3つのステップがあります。 + +1. テキストはモデルが理解できる形式に前処理される。 +2. 前処理された入力がモデルに渡される。 +3. 予測結果を理解できるように、モデルの後処理が行われる。 + +現在[利用可能なpipeline](https://huggingface.co/transformers/main_classes/pipelines.html)の一部を紹介します。 + +- `feature-extraction` (テキストのベクトル表現を取得) +- `fill-mask` +- `ner` (固有表現認識) +- `question-answering` +- `sentiment-analysis` +- `summarization` +- `text-generation` +- `translation` +- `zero-shot-classification` + +では、いくつか見ていきましょう! + +## ゼロショット分類 + +まず、ラベル付けされていないテキストを分類する必要があるような、より困難なタスクに取り組むことから始めます。これは実際のプロジェクトでよくあるシナリオです。なぜなら、テキストにアノテーションをつけるのは通常時間がかかり、専門知識が必要だからです。このような場合、`zero-shot-classification` pipelineは非常に強力です。分類に使用するラベルを指定できるので、事前に学習したモデルのラベルに依存する必要がありません。肯定的か否定的かの2つのラベルを使って、モデルがどのようにテキストを分類するかは既に見たとおりです。しかし、他の任意のラベルセットを使ってテキストを分類することもできます。 + + +```python +from transformers import pipeline + +classifier = pipeline("zero-shot-classification") +classifier( + "This is a course about the Transformers library", + candidate_labels=["education", "politics", "business"], +) +``` + +```python out +{'sequence': 'This is a course about the Transformers library', + 'labels': ['education', 'business', 'politics'], + 'scores': [0.8445963859558105, 0.111976258456707, 0.043427448719739914]} +``` + +このpipelineは _zero-shot_ と呼ばれます。なぜなら、これを使うために自前のデータセットでモデルのファインチューニングをする必要がないからです。任意のラベルのリストに対して直接確率スコアを返すことができます。 + + + +✏️ **試してみよう!** 独自の入力とラベルで遊んでみて、モデルがどのように振る舞うか見てみましょう。 + + + +## 文章生成 + +では、pipelineを使ってテキストを生成する方法を見てみましょう。主なアイデアは、プロンプトを与えると、モデルが残りのテキストを生成してそれを補完することです。これは、多くの携帯電話に搭載されている予測入力機能に類似しています。テキスト生成にはランダム性が含まれるため、以下と同様な結果が得られないのが通常です。 + +```python +from transformers import pipeline + +generator = pipeline("text-generation") +generator("In this course, we will teach you how to") +``` + +```python out +[{'generated_text': 'In this course, we will teach you how to understand and use ' + 'data flow and data interchange when handling user data. We ' + 'will be working with one or more of the most commonly used ' + 'data flows — data flows of various types, as seen by the ' + 'HTTP'}] +``` + +引数`num_return_sequences`で異なるシーケンスの生成数を、引数`max_length`で出力テキストの合計の長さを制御することができます。 + + + + +✏️ **試してみよう!** `num_return_sequences` と `max_length` 引数を用いて、15語ずつの2つの文を生成してみましょう! + + + + +## pipelineでHubから任意のモデルを使用する + +これまでの例では、タスクに応じたデフォルトのモデルを使用しましたが、特定のタスク(例えばテキスト生成)のpipelineで使用するモデルをHubから選択することも可能です。[Model Hub](https://huggingface.co/models)にアクセスし、左側の対応するタグをクリックすると、そのタスクでサポートされているモデルのみが表示されます。[このようなページ](https://huggingface.co/models?pipeline_tag=text-generation)が表示されるはずです。 + +それでは、[`distilgpt2`](https://huggingface.co/distilgpt2)モデルを試してみましょう! 先ほどと同じpipelineでロードする方法を説明します。 + +```python +from transformers import pipeline + +generator = pipeline("text-generation", model="distilgpt2") +generator( + "In this course, we will teach you how to", + max_length=30, + num_return_sequences=2, +) +``` + +```python out +[{'generated_text': 'In this course, we will teach you how to manipulate the world and ' + 'move your mental and physical capabilities to your advantage.'}, + {'generated_text': 'In this course, we will teach you how to become an expert and ' + 'practice realtime, and with a hands on experience on both real ' + 'time and real'}] +``` + +言語タグをクリックして検索するモデルを絞り込み、他の言語でテキストを生成するモデルを選ぶことができます。Model Hubには、複数の言語をサポートする多言語モデルのチェックポイントもあります。 + +モデルをクリックで選択すると、オンラインで直接試用できるウィジェットが表示されます。このようにして、ダウンロードする前にモデルの機能をすばやくテストすることができます。 + + + +✏️ **試してみよう!** フィルターを使って、他の言語のテキスト生成モデルを探してみましょう。ウィジェットで自由に遊んだり、pipelineで使ってみてください! + + + + +### 推論API + +すべてのモデルは、Hugging Face [ウェブサイト](https://huggingface.co/)で公開されているInference APIを使って、ブラウザから直接テストすることが可能です。このページでは、任意の文字列を入力し、モデルが入力データを処理する様子を見ることで、直接モデルで遊ぶことができます。 + +このウィジェットを動かすInference APIは、有料製品としても提供されており、ワークフローに必要な場合は便利です。詳しくは[価格ページ](https://huggingface.co/pricing)をご覧ください。 + +## 空所穴埋め + +次に試すpipelineは`fill-mask`です。このタスクのアイデアは、与えられたテキストの空白を埋めることです。 + +```python +from transformers import pipeline + +unmasker = pipeline("fill-mask") +unmasker("This course will teach you all about models.", top_k=2) +``` + +```python out +[{'sequence': 'This course will teach you all about mathematical models.', + 'score': 0.19619831442832947, + 'token': 30412, + 'token_str': ' mathematical'}, + {'sequence': 'This course will teach you all about computational models.', + 'score': 0.04052725434303284, + 'token': 38163, + 'token_str': ' computational'}] +``` + +`top_k` 引数は、いくつの可能性を表示させたいかをコントロールします。ここでは、モデルが特別な `` という単語を埋めていることに注意してください。これはしばしば *mask token* と呼ばれます。他の空所穴埋めモデルは異なるマスクトークンを持つかもしれないので、他のモデルを探索するときには常に適切なマスクワードを確認するのが良いでしょう。それを確認する1つの方法は、ウィジェットで使用されているマスクワードを見ることです。 + + + +✏️ **試してみよう!** Hub で `bert-base-cased` モデルを検索し、推論API ウィジェットでそのマスクワードを特定します。このモデルは上記の `pipeline` の例文に対して何を予測するでしょうか? + + + +## 固有表現認識 + +固有表現認識(NER)は、入力されたテキストのどの部分が人物、場所、組織などの固有表現に対応するかをモデルが見つけ出すタスクです。例を見てみましょう。 + +```python +from transformers import pipeline + +ner = pipeline("ner", grouped_entities=True) +ner("My name is Sylvain and I work at Hugging Face in Brooklyn.") +``` + +```python out +[{'entity_group': 'PER', 'score': 0.99816, 'word': 'Sylvain', 'start': 11, 'end': 18}, + {'entity_group': 'ORG', 'score': 0.97960, 'word': 'Hugging Face', 'start': 33, 'end': 45}, + {'entity_group': 'LOC', 'score': 0.99321, 'word': 'Brooklyn', 'start': 49, 'end': 57} +] +``` + +ここでは、モデルはSylvainが人(PER)、Hugging Faceが組織(ORG)、Brooklynが場所(LOC)であることを正しく識別しています。 + +pipelineの作成機能でオプション `grouped_entities=True` を渡すと、同じエンティティに対応する文の部分を再グループ化するようpipelineに指示します。ここでは、名前が複数の単語で構成されていても、モデルは "Hugging" と "Face" を一つの組織として正しくグループ化しています。実際、次の章で説明するように、前処理ではいくつかの単語をより小さなパーツに分割することさえあります。例えば、`Sylvain`は4つの部分に分割されます。`S`, `##yl`, `##va`, and `##in`.です。後処理の段階で、pipelineはこれらの断片をうまく再グループ化しました。 + + + +✏️ **試してみよう!** Model Hubで英語の品詞タグ付け(通常POSと略される)を行えるモデルを検索してください。このモデルは、上の例の文に対して何を予測するでしょうか? + + + +## 質問応答 + +質問応答pipelineは、与えられた文脈から得た情報を使って質問に答えます。 + +```python +from transformers import pipeline + +question_answerer = pipeline("question-answering") +question_answerer( + question="Where do I work?", + context="My name is Sylvain and I work at Hugging Face in Brooklyn", +) +``` + +```python out +{'score': 0.6385916471481323, 'start': 33, 'end': 45, 'answer': 'Hugging Face'} +``` + +このpipelineは、提供されたコンテキストから情報を抽出することで動作し、答えを生成するわけではないことに注意してください。 + +## 要約 + +要約とは、文章中の重要な部分をすべて(あるいはほとんど)維持したまま、より短い文章にするタスクです。以下はその例です。 + +```python +from transformers import pipeline + +summarizer = pipeline("summarization") +summarizer( + """ + America has changed dramatically during recent years. Not only has the number of + graduates in traditional engineering disciplines such as mechanical, civil, + electrical, chemical, and aeronautical engineering declined, but in most of + the premier American universities engineering curricula now concentrate on + and encourage largely the study of engineering science. As a result, there + are declining offerings in engineering subjects dealing with infrastructure, + the environment, and related issues, and greater concentration on high + technology subjects, largely supporting increasingly complex scientific + developments. While the latter is important, it should not be at the expense + of more traditional engineering. + Rapidly developing economies such as China and India, as well as other + industrial countries in Europe and Asia, continue to encourage and advance + the teaching of engineering. Both China and India, respectively, graduate + six and eight times as many traditional engineers as does the United States. + Other industrial countries at minimum maintain their output, while America + suffers an increasingly serious decline in the number of engineering graduates + and a lack of well-educated engineers. +""" +) +``` + +```python out +[{'summary_text': ' America has changed dramatically during recent years . The ' + 'number of engineering graduates in the U.S. has declined in ' + 'traditional engineering disciplines such as mechanical, civil ' + ', electrical, chemical, and aeronautical engineering . Rapidly ' + 'developing economies such as China and India, as well as other ' + 'industrial countries in Europe and Asia, continue to encourage ' + 'and advance engineering .'}] +``` + +テキスト生成と同様に、結果に対して `max_length` や `min_length` を指定することができます。 + + +## 翻訳 + +翻訳の場合、タスク名に言語ペアを指定すれば(`"translation_en_to_fr"`など)デフォルトのモデルを使うこともできますが、一番簡単なのは [Model Hub](https://huggingface.co/models) で使いたいモデルを選ぶことです。ここでは、フランス語から英語への翻訳を試してみます。 + +```python +from transformers import pipeline + +translator = pipeline("translation", model="Helsinki-NLP/opus-mt-fr-en") +translator("Ce cours est produit par Hugging Face.") +``` + +```python out +[{'translation_text': 'This course is produced by Hugging Face.'}] +``` + +テキスト生成や要約と同様に、結果に対して `max_length` や `min_length` を指定することができます。 + + + +✏️ **試してみよう!** 他言語の翻訳モデルを検索して、前の文章をいくつかの異なる言語に翻訳してみましょう。 + + + +これまで紹介したpipelineは、ほとんどがデモンストレーションのためのものです。これらは特定のタスクのためにプログラムされたものであり、それらのバリエーションを実行することはできません。次の章では、`pipeline()`関数の中身と、その動作をカスタマイズする方法を学びます。 diff --git a/chapters/ja/chapter1/4.mdx b/chapters/ja/chapter1/4.mdx new file mode 100644 index 000000000..4c9f6e585 --- /dev/null +++ b/chapters/ja/chapter1/4.mdx @@ -0,0 +1,177 @@ +# Transformersの仕組みについて + + + +このセクションでは、Transformerモデルのアーキテクチャをざっくりと見ていきます。 + +## Transformerの歴史を簡単に + +Transformerモデルの(短い)歴史の中で、参考となるポイントをいくつか紹介します。 + +
+A brief chronology of Transformers models. + +
+ +[Transformerのアーキテクチャ](https://arxiv.org/abs/1706.03762)は2017年6月に登場しました。 当初の研究は翻訳タスクに焦点を置いていましたが、これに続くようにして以下のような影響力のあるモデルがいくつか登場します。 + +- **2018/6** [GPT](https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf): 様々な自然言語処理タスクに対してfine-tuningすることでSoTAを達成した、史上初の事前学習済みモデルです。 + +- **2018/10** [BERT](https://arxiv.org/abs/1810.04805): これも大規模な事前学習済みモデルで、文についてのより良い要約を生成するように設計されています。(こちらについては次の章で詳しく説明します!) + +- **2019/2** [GPT-2](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf): これはGPTを改良 & 大規模化したものですが、倫理的な問題から一般公開までには時間がかかったモデルです。 + +- **2019/10** [DistilBERT](https://arxiv.org/abs/1910.01108): これはBERTを60%高速化し40%のメモリ軽量化をしながら、97%の性能を維持した蒸留モデルです。 + +- **2019/10** [BART](https://arxiv.org/abs/1910.13461), [T5](https://arxiv.org/abs/1910.10683): オリジナルのTransformerモデルと同じアーキテクチャを採用した大規模な事前学習済みモデルです。 + +- **2020/5** [GPT-3](https://arxiv.org/abs/2005.14165): GPT-2をさらに大規模化したもので、fine-tuningなし(_zero-shot学習_)で様々なタスクを解くことができるようにしたモデルです。 + +このリストは決して包括的なものではなく、Transformerのモデルの種類をざっくり分けることを意図しています。種類については大きく以下の3つのカテゴリーに分類することができます。 + +- GPT型 (_auto-regressive_ Transformerモデルとも呼ばれます) +- BERT型 (_auto-encoding_ Transformerモデルとも呼ばれます) +- BART/T5型 (_sequence-to-sequence_ Transformerモデルとも呼ばれます) + +これらの種類についてこれから深掘りしていきます。 + +## Transformers = 言語モデル + +GPT、BERT、T5などの上記の全てのモデルは*言語モデル*として学習されています。これは大量の生文に対して自己教師あり学習を行ったことを意味しています。自己教師あり学習は、学習の目的となるものを、モデルに入力するデータから自動で算出する学習方法です。つまりデータに対する人手のラベル付が必要ないことを意味します。 + +このタイプのモデルは、学習させた言語に対する統計的な理解を深めることができますが、特定のタスクにはあまり役に立ちません。従って、一般的な事前学習済みモデルは、この後に*転移学習*と呼ばれるプロセスを経ます。このプロセスでは人手でラベル付されたデータを用いた教師あり学習を行なって、特定のタスクに対してfine-tuningされます。 + +タスク例の1つに、前のいくつかの単語を読んで、それに続く次の単語を予測するものがあります。これは出力が過去と現在の入力にのみ依存し、将来の入力には依存しないため、 *Causal Language Modeling (CLM)* と呼ばれます。 + +
+Example of causal language modeling in which the next word from a sentence is predicted. + +
+ +他の例としては *Masked Language Modeling (MLM)* があり、これは文中のマスクされた(隠された)単語が何かを予測するタスクになっています。 + +
+Example of masked language modeling in which a masked word from a sentence is predicted. + +
+ +## Transformers = 大規模モデル + +前述のDistilBERTなどの例外を除けば、より良いパフォーマンスを達成するための一般的な戦略として、モデルサイズと学習データ量を大きくするというものがあります。 + +
+Number of parameters of recent Transformers models +
+ +残念ながらモデルの学習(特に大規模なモデルの学習)には大量のデータが必要になります。これは時間と計算資源の面で非常にコストがかかります。また、以下のグラフから分かるように、環境にも影響を及ぼすものになります。 + +
+The carbon footprint of a large language model. + +
+ + + +そしてこの図は、事前学習の環境負荷を意識的に減らすことを目的とするチームが率いる、(超大規模)モデルのプロジェクトを示しています。最適なハイパーパラメータを得るための多くの試行による環境負荷は、より大きなものになると考えられます。 + +もし研究チームや学生団体、企業がその度にモデルを一から学習していたらどうでしょうか。これでは膨大で不必要なコストがかかってしまいます。 + +従って、学習済み言語モデルの重みを共有しそれを利用することで、コミュニティ全体の計算コストや環境負荷を削減することができるのです。 + +## 転移学習 + + + +*事前学習*とはモデルを一から学習することです。重みはランダムに初期化され、事前知識なしに学習が開始されます。 + +
+The pretraining of a language model is costly in both time and money. + +
+ +事前学習は大量のデータを使って行われます。よって、非常に大きなデータコーパスを必要とし、学習には数週間かかることがあります。 + +一方で*ファインチューニング*は事前学習の**後に**行われるものです。ファインチューニングを行うには、まず最初に事前学習済みモデルを取得し、次にタスクに応じたデータセットを用いて追加の学習を行います。ここで、「(事前学習を行わずに)初めからこのタスクに対して学習を行えば良いのでは?」と思った方がいるかもしれませんが、これにはいくつかの理由があります。 + +* 事前学習済みモデルは、ファインチューニング用のデータセットと何らかの類似性を持ったデータで既に学習が行われています。このため、ファインチューニングの過程において、事前学習済みモデルが既に獲得した知識を利用することができます。(例えば自然言語処理の問題では、事前学習済みのモデルは言語に対する何らかの統計的な理解をしているはずです。) +* また事前学習済みモデルは大量のデータを使って学習されているので、ファインチューニングでははるかに少ないデータで適切な結果を得ることが可能になります。 +* これと同じ理由で、良い結果を得るために必要な時間や資源を大きく削減することができます。 + +例えば、英語で訓練された事前学習済みモデルをarXivコーパスでファインチューニングすることで、科学/研究ベースのモデルを作ることができます。ファインチューニングは少ないデータで実施できます。これは事前学習済みモデルが獲得していた知識が「転移」しているためで、この特徴から「*転移学習*」と呼ばれているという訳です。 + +
+The fine-tuning of a language model is cheaper than pretraining in both time and money. + +
+ +従って、モデルのファインチューニングに必要な時間、データ、経済的/環境的コストは少なく済みます。また事前学習よりも制約が少ないため、様々なファインチューニングのスキームを素早く簡単に試すことができます。 + +このプロセスは(大量のデータがある場合を除いて)ゼロから学習するよりも良い結果をもたらします。だからこそ(目的のタスクにできるだけ近い)事前学習済みモデルを活用し、それをファインチューニングするべきだと言えます。 + +## 一般的なアーキテクチャ + +このセクションでは、Transformerモデルの一般的なアーキテクチャについて見ていきます。各構成要素については後ほど詳しく説明するので、理解できない部分があっても心配ありません! + + + +## イントロダクション + +モデルは主に2つの要素で構成されます。 + +* **エンコーダー (左)**: エンコーダーは入力を受け取り、その特徴量を生成します。これは入力から理解を得るためにモデルが最適化されることを意味します。 +* **デコーダー (右)**: デコーダーではエンコーダーが生成した特徴量とその他の入力を受け取って、目的の系列を生成します。これは出力を生成するためにモデルが最適化されることを意味します。 + +
+Architecture of a Transformers models + +
+ +これらの構成要素はタスクに応じてそれぞれ別々に使用することができます。 + +* **Encoder-only モデル**: 文章分類や固有表現抽出など入力に対する理解が必要となるタスクに適しています。 +* **Decoder-only モデル**: 文生成などの生成タスクに適しています。 +* **Encoder-Decoder(sequence-to-sequence) モデル**: 翻訳や要約など、入力を要する生成タスクに適しています。 + +これらのアーキテクチャについてはのちのセクションで個別に紹介します。 + + +## アテンション層 + +Transformerモデルは*アテンション層*と呼ばれる特殊な層で構築されていることが大きな特徴となっています。実際にTransformerが登場した論文のタイトルも["Attention Is All You Need"](https://arxiv.org/abs/1706.03762)というものでした! アテンション層については後ほど詳しく説明します。現段階においては、モデルが各単語の特徴量を扱う際に、入力されたテキストのどの単語に注目すべきかをアテンション層が指示してくれる(多かれ少なかれその他の単語は無視される)ということだけ知っておいてもらえれば十分です。 + +このことを理解するために、英語からフランス語への翻訳タスクを考えてみます。"You like this course" という入力があるとき、翻訳モデルは "like" という単語を適切に翻訳するために "You" という隣接する単語に注目する必要があります。これはフランス語の動詞 "like" は主語によって異なる活用がされるためです。ただこのとき、"like" の翻訳に他の単語の情報は役に立ちません。同じように、モデルは "this" という単語を翻訳する際に "course" という単語に注意を払う必要があり、これは "this" という単語の翻訳が関連する名詞が男性か女性かによって変化するためです。この場合においてもその他の単語は "this" の翻訳には関係ありません。より複雑な文(および文法規則)では、モデルは各単語を適切に翻訳するために、文中のより離れた位置に出現する可能性のある単語に対して特別な注意を払う必要があります。 + +単語はそれ自体で意味を持ちますが、その意味は文脈(前後に現れるその他の単語)に大きな影響を受けます。このため、翻訳タスクと同じ考え方が自然言語に関する色々なタスクに対して当てはまります。 + +さて、アテンション層がどのようなものかを理解頂いた上で、Transformerのアーキテクチャをより詳しく見ていきましょう! + +## オリジナルのアーキテクチャ + +Transformerのアーキテクチャは翻訳用に設計されました。学習過程において、エンコーダーはある言語の入力(文章)を受け取り、デコーダーは別言語で書かれた同じ文章を受け取ります。エンコーダーのアテンション層は文中の全ての単語を使うことができます。(先ほど見たように、、ある単語を翻訳するためにはその前後の単語に注意を払う必要があるためです。)一方でデコーダーは逐次的に動作します。このため既に翻訳して生成した単語にしか注意を向けることができません。(言い換えればこれから翻訳して生成される単語に対しては注意が張られないということです。)例えば、翻訳対象の最初の3単語を予測したらそれをデコーダーに渡すことで、デコーダーはエンコーダーに入力された情報を全て使いながら4単語目を予測します。 + +モデルの学習中、学習速度を上げるためにデコーダーには答えとなる翻訳文(ターゲット文)全体が与えられていますが、処理対象となる単語の後に続く単語を使うことは許されていません。例えば、4番目の単語を予測する際、アテンション層はターゲット文の1〜3番目の位置にある単語にしかアクセスすることができません。 + +Transformerのオリジナルのアーキテクチャの概観は、このように左側のエンコーダーと右側のデコーダーからなります。 + +
+Architecture of a Transformers models + +
+ +デコーダーブロックの最初のアテンション層は、デコーダーに対する全ての入力を使うことができますが、2番目のアテンション層はエンコーダーの出力を利用します。従って、入力文全体にアクセスすることで現在の単語の最適な予測が可能になるという訳です。これは言語によって単語の登場順が異なるような文法規則があったり、文の後半で提供される文脈情報が、現在の単語の翻訳に役立つ場合があるので、非常に便利なものとなっています。 + +*attentionマスク*はエンコーダー・デコーダーで、ある特別な単語に注目しないようにするために使用されます。(例えば、文をまとめて入力するときに、全ての文を同じ長さに揃えるために使われるpadding tokenなどです。) + +## アーキテクチャ vs. チェックポイント + +このコースでTransformerモデルについて掘り下げていくと、*モデル*と同様に*アーキテクチャ*や*チェックポイント*という単語についても言及されていることがわかります。これらの用語はそれぞれ少しずつ異なる意味を持っています。 + + +* **アーキテクチャ**: これはモデルの骨格を意味し、モデル内の各層と内部で起こる操作を定義したものになります。 +* **チェックポイント**: これは与えられたアーキテクチャに対して読み込まれる重みを意味します。 +* **モデル**: これは「アーキテクチャ」や「チェックポイント」ほど正確ではない、より包括的な用語で両方を意味することがあります。このコースでは曖昧さを回避するために、重要な場合は*アーキテクチャ*や*チェックポイント*を使うことにします。 + +例えばBERTはアーキテクチャを指し、`bert-base-cased`はGoogleの開発チームがBERTの最初のリリースのために用意した重みを指したチェックポイントとなります。しかしながら"BERTモデル"や"`bert-base-cased`モデル"と呼ぶこともできます。 diff --git a/chapters/ja/chapter1/5.mdx b/chapters/ja/chapter1/5.mdx new file mode 100644 index 000000000..894cc748c --- /dev/null +++ b/chapters/ja/chapter1/5.mdx @@ -0,0 +1,22 @@ +# エンコーダーモデル + + + + + +エンコーダーモデルとは、Transformerモデルのエンコーダーのみを使用したモデルを指します。 処理の各段階で、attention層は最初の文の全ての単語にアクセスすることができます。 これらのモデルは "bi-directional"(双方向)のattentionを持つものとして特徴付けられ、*オートエンコーダーモデル*と呼ばれます。 + +これらのモデルの事前学習は、何らかの方法で(例えば文中の単語をランダムにマスクするなどで)文を壊し、この文の再構築をタスクとして解くことを中心に展開されます。 + +エンコーダーモデルは、文の分類 ・ 固有表現認識(より一般的には単語の分類) ・ 抽出的質問応答など、文全体の理解を必要とするタスクに最も適しています。 + +エンコーダーモデルでは以下のものが代表的です: + +- [ALBERT](https://huggingface.co/transformers/model_doc/albert.html) +- [BERT](https://huggingface.co/transformers/model_doc/bert.html) +- [DistilBERT](https://huggingface.co/transformers/model_doc/distilbert.html) +- [ELECTRA](https://huggingface.co/transformers/model_doc/electra.html) +- [RoBERTa](https://huggingface.co/transformers/model_doc/roberta.html) diff --git a/chapters/ja/chapter1/6.mdx b/chapters/ja/chapter1/6.mdx new file mode 100644 index 000000000..6044ae35e --- /dev/null +++ b/chapters/ja/chapter1/6.mdx @@ -0,0 +1,22 @@ +# デコーダーモデル + + + + + +デコーダーモデルとは、Transformerモデルのデコーダーのみを使用したモデルを指します。 処理の各段階で、処理対象の単語について、attention層はその単語より前に出現した単語にのみアクセスすることができます。 このようなモデルは*自己回帰モデル*と呼ばれます。 + + デコーダーモデルの事前学習は、次に続く単語を予測するタスクを解くことを中心に展開されます。 + +これらのモデルは、文を生成するタスクに最も適しています。 + + +デコーダーモデルでは以下のものが代表的です: + +- [CTRL](https://huggingface.co/transformers/model_doc/ctrl.html) +- [GPT](https://huggingface.co/transformers/model_doc/gpt.html) +- [GPT-2](https://huggingface.co/transformers/model_doc/gpt2.html) +- [Transformer XL](https://huggingface.co/transformers/model_doc/transfo-xl.html) diff --git a/chapters/ja/chapter1/7.mdx b/chapters/ja/chapter1/7.mdx new file mode 100644 index 000000000..e4bf8ea27 --- /dev/null +++ b/chapters/ja/chapter1/7.mdx @@ -0,0 +1,23 @@ +# Sequence-to-sequence モデル + + + + + +Encoder-decoderモデル(*sequence-to-sequence models*とも呼ばれる)はTransformerアーキテクチャのエンコーダーとデコーダーの両方を使用します。 +それぞれのステージにおいて、エンコーダーのアテンション層は入力文のすべての単語にアクセスできるのに対して、デコーダーのアテンション層は入力中のある単語の前に位置する単語にのみアクセスできます。 + +これらのモデルの事前学習は、エンコーダー、またはデコーダーの学習と同じように行われますが、通常はより複雑な方法を含みます。 +例えば、[T5](https://huggingface.co/t5-base) は、特殊な単語で文中のスパン(複数の単語を含むことができる)をランダムにマスクしたときに、そのマスクされた文を予測する事を目的として事前学習されています。 + +Sequence-to-sequenceモデルは、要約、翻訳、質問応答生成などのように、与えられた入力文に対して新しい文を生成するタスクにとても適しています。 + +これらの系統のモデルの代表は次のとおりです: + +- [BART](https://huggingface.co/transformers/model_doc/bart.html) +- [mBART](https://huggingface.co/transformers/model_doc/mbart.html) +- [Marian](https://huggingface.co/transformers/model_doc/marian.html) +- [T5](https://huggingface.co/transformers/model_doc/t5.html) diff --git a/chapters/ja/chapter1/8.mdx b/chapters/ja/chapter1/8.mdx new file mode 100644 index 000000000..e81b75dc4 --- /dev/null +++ b/chapters/ja/chapter1/8.mdx @@ -0,0 +1,35 @@ +# バイアスと限界 + + + +事前学習済みモデルやファインチューニング済みのモデルを使う場合、これらのモデルは強力なツールですが、一方で限界もあることに注意しなければなりません。 +その代表例は、大量のデータによる事前学習を行うために、研究者はインターネット上にある利用可能なデータを良いものから悪いものまで手当たりしだいに集めてしまうことです。 + +簡単に説明するために、BERTによる`fill-mask`パイプラインの例に戻りましょう: + + +```python +from transformers import pipeline + +unmasker = pipeline("fill-mask", model="bert-base-uncased") +result = unmasker("This man works as a [MASK].") +print([r["token_str"] for r in result]) + +result = unmasker("This woman works as a [MASK].") +print([r["token_str"] for r in result]) +``` + +```python out +['lawyer', 'carpenter', 'doctor', 'waiter', 'mechanic'] +['nurse', 'waitress', 'teacher', 'maid', 'prostitute'] +``` + +これらの2つの文の欠落した単語を埋めさせたときに、モデルはジェンダーフリーの回答を一つだけしか与えません(waiter/waitress)。他はたいていの場合、特定の性別と関連付けられる職業です。そして、モデルは「女性」と「仕事」から連想される可能性のある職業としてトップ5に「売春婦(prostitute)」を上げています。 +BERTはインターネット上のあらゆるところからデータをかき集めて構築されたのではなく、中立的なデータ([English Wikipedia](https://huggingface.co/datasets/wikipedia)と[BookCorpus](https://huggingface.co/datasets/bookcorpus)を用いて学習されています) を用いて構築されためずらしいTransformerモデルであるにも関わらず、このような現象が発生してしまいます。 + +したがって、これらのツールを使用する際は、オリジナルのモデルがとても簡単に性的、差別的、あるいは同性愛嫌悪のコンテンツを生成してしまうことを念頭に置く必要があります。この本質的なバイアスは、あるデータでファインチューニングしても消えることはありません。 \ No newline at end of file diff --git a/chapters/ja/chapter1/9.mdx b/chapters/ja/chapter1/9.mdx new file mode 100644 index 000000000..4299784d1 --- /dev/null +++ b/chapters/ja/chapter1/9.mdx @@ -0,0 +1,16 @@ +# まとめ + + + +この章では、🤗 Transformersが提供する高レベルな`pipeline()` 関数を用いて、異なるNLPタスクにアプローチする方法を学びました。また、同様にHubを用いてモデルを探す方法や、推論APIを使ってブラウザ上でモデルを直接テストする方法も学びました。 + +さらに、Transformerモデルがどのように動作するかを高いレベルで議論し、さらに転移学習やファインチューニングの重要性について話しました。一つの重要な観点は、解きたいタスクに応じてアーキテクチャ全体を用いることや、エンコーダーやデコーダーの一方だけを用いることができるという点です。以下の表はそのまとめです。 + +| モデル | 例 | タスク | +|-----------------|--------------------------------------------|----------------------------------------------------------------------------------| +| Encoder | ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa |文章分類, 固有表現抽出, 抽出型質問応答 | +| Decoder | CTRL, GPT, GPT-2, Transformer XL | 文章生成 | +| Encoder-decoder | BART, T5, Marian, mBART | 文章要約, 翻訳, 生成型質問応答  | diff --git a/chapters/pt/_toctree.yml b/chapters/pt/_toctree.yml index 662e840c5..2078cf8df 100644 --- a/chapters/pt/_toctree.yml +++ b/chapters/pt/_toctree.yml @@ -47,6 +47,11 @@ - local: chapter2/8 title: Questionário de fim de capítulo quiz: 2 + +- title: 3. Ajustando um modelo pré treinado + sections: + - local: chapter3/1 + title: Introdução - title: 4. Compartilhamento de modelos e tokenizer sections: diff --git a/chapters/pt/chapter3/1.mdx b/chapters/pt/chapter3/1.mdx new file mode 100644 index 000000000..0166506f0 --- /dev/null +++ b/chapters/pt/chapter3/1.mdx @@ -0,0 +1,27 @@ + + +# Introdução + + + +No [Capítulo 2](/course/chapter2) exploramos como utilizar tokenizadores e modelos pré treinados para fazer previsões. Mas e se você quiser ajustar um modelo pré-treinado para seu próprio dataset? Esse é o tema deste capítulo! Você vai aprender: + +{#if fw === 'pt'} +* Como preparar um datset grande do Hub +* Como usar a API de alto nível `Trainer` para ajustar um modelo +* Como usar um loop de treinamento personalizado +* Como usar a biblioteca 🤗 Accelerate para executar facilmente esse loop de treinamento personalizado em qualquer configuração distribuída +{#else} + +{:else} +* Como preparar um dataset grande do Hub +* Como usar Keras para ajustar um modelo +* Como usar Keras para fazer previsões +* Como usar uma métrica personalizada + +{/if} + +Para fazer upload de seus checkpoints treinados para o Hugging Face Hub, você precisará de uma conta huggingface.co: [crie uma conta](https://huggingface.co/join) \ No newline at end of file diff --git a/chapters/ru/_toctree.yml b/chapters/ru/_toctree.yml index 12777ffce..643aef481 100644 --- a/chapters/ru/_toctree.yml +++ b/chapters/ru/_toctree.yml @@ -81,9 +81,9 @@ - local: chapter5/7 title: 🤗 Datasets, итоги! - local: chapter5/8 - title: Тест по главе 5 + title: Тест по главе 5 - title: 6. Бибилиотека 🤗 Tokenizers - sections: + sections: - local: chapter6/1 title: Введение - local: chapter6/2 diff --git a/chapters/ru/chapter3/2.mdx b/chapters/ru/chapter3/2.mdx index 231c33e65..250e553b1 100644 --- a/chapters/ru/chapter3/2.mdx +++ b/chapters/ru/chapter3/2.mdx @@ -23,8 +23,7 @@ {/if} {#if fw === 'pt'} -Продолжим с примером из [предыдущей главы](/course/ru/chapter2) -Continuing with the example from the [previous chapter](/course/ru/chapter2), вот как мы будем обучать классификатор последовательности на одном батче с помощью PyTorch: +Продолжим с примером из [предыдущей главы](/course/ru/chapter2), вот как мы будем обучать классификатор последовательности на одном батче с помощью PyTorch: ```python import torch