Cómo entrenar un nuevo modelo de lenguaje desde cero utilizando Transformers y Tokenizers

Entrenamiento de un nuevo modelo de lenguaje con Transformers y Tokenizers

En los últimos meses, hemos realizado varias mejoras en nuestras bibliotecas de transformers y tokenizers, con el objetivo de facilitar más que nunca el entrenamiento de un nuevo modelo de lenguaje desde cero.

En esta publicación, mostraremos cómo entrenar un modelo “pequeño” (84 M parámetros = 6 capas, tamaño oculto de 768, 12 cabezas de atención) – el mismo número de capas y cabezas que DistilBERT – en Esperanto. Luego, ajustaremos el modelo en una tarea secundaria de etiquetado de partes del discurso.

Esperanto es un idioma construido con el objetivo de ser fácil de aprender. Lo elegimos para esta demostración por varias razones:

  • es un idioma relativamente poco común (aunque lo hablan ~2 millones de personas), por lo que esta demostración es menos aburrida que entrenar otro modelo en inglés 😁
  • su gramática es altamente regular (por ejemplo, todos los sustantivos comunes terminan en -o, todos los adjetivos en -a), por lo que deberíamos obtener resultados lingüísticos interesantes incluso con un conjunto de datos pequeño.
  • finalmente, el objetivo principal en la base del idioma es acercar a las personas (fomentando la paz mundial y la comprensión internacional), lo que se podría argumentar que está alineado con el objetivo de la comunidad de procesamiento del lenguaje natural 💚

N.B. No necesitarás entender Esperanto para comprender esta publicación, pero si deseas aprenderlo, Duolingo tiene un buen curso con 280,000 estudiantes activos.

Nuestro modelo se llamará… espera un momento… EsperBERTo 😂

1. Encuentra un conjunto de datos

Primero, busquemos un corpus de texto en Esperanto. Aquí utilizaremos la porción en Esperanto del corpus OSCAR de INRIA. OSCAR es un corpus multilingüe enorme obtenido mediante clasificación de idioma y filtrado de volcados de Common Crawl de la web.

La porción en Esperanto del conjunto de datos tiene solo 299M, por lo que la concatenaremos con el subconjunto en Esperanto de la Colección de Corpus de Leipzig, que está compuesto por texto de diversas fuentes como noticias, literatura y Wikipedia.

El corpus de entrenamiento final tiene un tamaño de 3 GB, que sigue siendo pequeño: para obtener mejores resultados en tu modelo, cuanto más datos puedas obtener para el preentrenamiento, mejores resultados obtendrás.

2. Entrena un tokenizador

Elegimos entrenar un tokenizador de codificación de pares de bytes a nivel de bytes (al igual que GPT-2), con los mismos tokens especiales que RoBERTa. Vamos a elegir arbitrariamente un tamaño de 52,000.

Recomendamos entrenar un tokenizador de BPE a nivel de bytes (en lugar de, por ejemplo, un tokenizador de WordPiece como BERT) porque comenzará a construir su vocabulario a partir de un alfabeto de bytes individuales, por lo que todas las palabras se podrán descomponer en tokens (¡no más tokens <unk>!).

#! pip install tokenizers

from pathlib import Path

from tokenizers import ByteLevelBPETokenizer

paths = [str(x) for x in Path("./eo_data/").glob("**/*.txt")]

# Inicializar un tokenizador
tokenizer = ByteLevelBPETokenizer()

# Personalizar el entrenamiento
tokenizer.train(files=paths, vocab_size=52_000, min_frequency=2, special_tokens=[
    "<s>",
    "<pad>",
    "</s>",
    "<unk>",
    "<mask>",
])

# Guardar los archivos en disco
tokenizer.save_model(".", "esperberto")

Y aquí tienes una captura ligeramente acelerada de la salida:

En nuestro conjunto de datos, el entrenamiento duró aproximadamente ~5 minutos.

🔥🔥 ¡Wow, eso fue rápido! ⚡️🔥

Ahora tenemos tanto un vocab.json, que es una lista de los tokens más frecuentes clasificados por frecuencia, como un merges.txt con una lista de fusiones.

{
    "<s>": 0,
    "<pad>": 1,
    "</s>": 2,
    "<unk>": 3,
    "<mask>": 4,
    "!": 5,
    "\"": 6,
    "#": 7,
    "$": 8,
    "%": 9,
    "&": 10,
    "'": 11,
    "(": 12,
    ")": 13,
    # ...
}

# merges.txt
l a
Ġ k
o n
Ġ la
t a
Ġ e
Ġ d
Ġ p
# ...

Lo genial es que nuestro tokenizador está optimizado para Esperanto. En comparación con un tokenizador genérico entrenado para inglés, más palabras nativas están representadas por un solo token sin dividir. Los diacríticos, es decir, los caracteres acentuados utilizados en Esperanto – ĉ, ĝ, ĥ, ĵ, ŝ y ŭ – se codifican de forma nativa. También representamos secuencias de manera más eficiente. Aquí en este corpus, la longitud promedio de las secuencias codificadas es aproximadamente un 30% más pequeña que cuando se utiliza el tokenizador GPT-2 preentrenado.

Así es como puedes usarlo en tokenizers, incluido el manejo de los tokens especiales de RoBERTa; por supuesto, también podrás usarlo directamente desde transformers.

from tokenizers.implementations import ByteLevelBPETokenizer
from tokenizers.processors import BertProcessing


tokenizer = ByteLevelBPETokenizer(
    "./models/EsperBERTo-small/vocab.json",
    "./models/EsperBERTo-small/merges.txt",
)
tokenizer._tokenizer.post_processor = BertProcessing(
    ("</s>", tokenizer.token_to_id("</s>")),
    ("<s>", tokenizer.token_to_id("<s>")),
)
tokenizer.enable_truncation(max_length=512)

print(
    tokenizer.encode("Mi estas Julien.")
)
# Encoding(num_tokens=7, ...)
# tokens: ['<s>', 'Mi', 'Ġestas', 'ĠJuli', 'en', '.', '</s>']

3. Entrenar un modelo de lenguaje desde cero

Actualización: El cuaderno de Colab asociado utiliza nuestro nuevo Trainer directamente, en lugar de a través de un script. Siéntete libre de elegir el enfoque que más te guste.

Ahora entrenaremos nuestro modelo de lenguaje utilizando el script run_language_modeling.py de transformers (recientemente renombrado de run_lm_finetuning.py ya que ahora admite un entrenamiento desde cero de manera más fluida). Solo recuerda dejar --model_name_or_path en None para entrenar desde cero en lugar de a partir de un modelo o punto de control existente.

Entrenaremos un modelo similar a RoBERTa, que es similar a BERT con algunos cambios (consulta la documentación para obtener más detalles).

Como el modelo es similar a BERT, lo entrenaremos en una tarea de modelado de lenguaje enmascarado, es decir, predecir cómo completar tokens arbitrarios que se ocultan aleatoriamente en el conjunto de datos. Esto se encarga del script de ejemplo.

Solo necesitamos hacer dos cosas:

  • implementar una subclase simple de Dataset que cargue datos de nuestros archivos de texto
    • Dependiendo de tu caso de uso, es posible que ni siquiera necesites escribir tu propia subclase de Dataset, si alguno de los ejemplos proporcionados (TextDataset y LineByLineTextDataset) funciona, pero hay muchas personalizaciones que es posible que desees agregar según cómo sea tu corpus.
  • Elegir y experimentar con diferentes conjuntos de hiperparámetros.

Aquí hay una versión simple de nuestro EsperantoDataset.

from torch.utils.data import Dataset

class EsperantoDataset(Dataset):
    def __init__(self, evaluate: bool = False):
        tokenizer = ByteLevelBPETokenizer(
            "./models/EsperBERTo-small/vocab.json",
            "./models/EsperBERTo-small/merges.txt",
        )
        tokenizer._tokenizer.post_processor = BertProcessing(
            ("</s>", tokenizer.token_to_id("</s>")),
            ("<s>", tokenizer.token_to_id("<s>")),
        )
        tokenizer.enable_truncation(max_length=512)
        # or use the RobertaTokenizer from `transformers` directly.

        self.examples = []

        src_files = Path("./data/").glob("*-eval.txt") if evaluate else Path("./data/").glob("*-train.txt")
        for src_file in src_files:
            print("🔥", src_file)
            lines = src_file.read_text(encoding="utf-8").splitlines()
            self.examples += [x.ids for x in tokenizer.encode_batch(lines)]

    def __len__(self):
        return len(self.examples)

    def __getitem__(self, i):
        # We’ll pad at the batch level.
        return torch.tensor(self.examples[i])

Si tu conjunto de datos es muy grande, puedes optar por cargar y tokenizar ejemplos sobre la marcha, en lugar de hacerlo como paso de preprocesamiento.

Aquí tienes un conjunto específico de hiperparámetros y argumentos que pasamos al script:

    --output_dir ./models/EsperBERTo-small-v1
    --model_type roberta
    --mlm
    --config_name ./models/EsperBERTo-small
    --tokenizer_name ./models/EsperBERTo-small
    --do_train
    --do_eval
    --learning_rate 1e-4
    --num_train_epochs 5
    --save_total_limit 2
    --save_steps 2000
    --per_gpu_train_batch_size 16
    --evaluate_during_training
    --seed 42

Como siempre, elige el tamaño de lote más grande que quepa en tu(s) GPU(s).

🔥🔥🔥 ¡Comencemos el entrenamiento! 🔥🔥🔥

Aquí puedes consultar nuestro Tensorboard para un conjunto específico de hiperparámetros:

Nuestros scripts de ejemplo se registran en el formato de Tensorboard de forma predeterminada, en la carpeta runs/. Luego, para ver tu tablero, simplemente ejecuta tensorboard dev upload --logdir runs: esto configurará tensorboard.dev, una versión alojada y administrada por Google que te permite compartir tu experimento de ML con cualquier persona.

4. Verifica que el modelo de lenguaje haya sido entrenado

Además de observar cómo disminuyen las pérdidas durante el entrenamiento y la evaluación, la forma más sencilla de verificar si nuestro modelo de lenguaje está aprendiendo algo interesante es mediante el FillMaskPipeline.

Las canalizaciones son envoltorios simples alrededor de tokenizadores y modelos, y la canalización ‘fill-mask’ te permite ingresar una secuencia que contiene un token enmascarado (aquí, <mask>) y devuelve una lista de las secuencias rellenadas más probables, con sus probabilidades.

from transformers import pipeline

fill_mask = pipeline(
    "fill-mask",
    model="./models/EsperBERTo-small",
    tokenizer="./models/EsperBERTo-small"
)

# La suno <mask>.
# =>

resultado = fill_mask("La suno <mask>.")

# {'score': 0.2526160776615143, 'sequence': '<s> La suno brilis.</s>', 'token': 10820}
# {'score': 0.0999930202960968, 'sequence': '<s> La suno lumis.</s>', 'token': 23833}
# {'score': 0.04382849484682083, 'sequence': '<s> La suno brilas.</s>', 'token': 15006}
# {'score': 0.026011141017079353, 'sequence': '<s> La suno falas.</s>', 'token': 7392}
# {'score': 0.016859788447618484, 'sequence': '<s> La suno pasis.</s>', 'token': 4552}

Ok, la sintaxis/gramática simple funciona. Intentemos un ejemplo un poco más interesante:

fill_mask("Jen la komenco de bela <mask>.")

# Esta es la comienzo de una <mask> hermosa.
# =>

# {
#     'score':0.06502299010753632
#     'sequence':'<s> Jen la komenco de bela vivo.</s>'
#     'token':1099
# }
# {
#     'score':0.0421181358397007
#     'sequence':'<s> Jen la komenco de bela vespero.</s>'
#     'token':5100
# }
# {
#     'score':0.024884626269340515
#     'sequence':'<s> Jen la komenco de bela laboro.</s>'
#     'token':1570
# }
# {
#     'score':0.02324388362467289
#     'sequence':'<s> Jen la komenco de bela tago.</s>'
#     'token':1688
# }
# {
#     'score':0.020378097891807556
#     'sequence':'<s> Jen la komenco de bela festo.</s>'
#     'token':4580
# }

¡En el inicio de un hermoso día!“, ¡en efecto!

Con promt más complejos, puedes verificar si tu modelo de lenguaje capturó más conocimiento semántico o incluso algún tipo de razonamiento (estadístico) de sentido común.

5. Ajusta tu modelo de lenguaje en una tarea secundaria

Ahora podemos ajustar nuestro nuevo modelo de lenguaje en esperanto en una tarea secundaria de etiquetado de partes del discurso.

Como se mencionó antes, el esperanto es un lenguaje altamente regular donde las terminaciones de las palabras típicamente condicionan la parte gramatical del discurso. Utilizando un conjunto de datos de etiquetas POS anotadas en esperanto formateadas en el formato CoNLL-2003 (ver ejemplo abajo), podemos usar el script run_ner.py de transformers.

El etiquetado POS es una tarea de clasificación de tokens al igual que la NER, por lo que podemos utilizar el mismo script exacto.

Nuevamente, aquí está el Tensorboard hospedado para este ajuste fino. Entrenamos durante 3 épocas utilizando un tamaño de lote de 64 por GPU.

Las pérdidas de entrenamiento y evaluación convergen a valores residuales pequeños ya que la tarea es bastante fácil (el lenguaje es regular) – aún así es divertido poder entrenarlo de principio a fin 😃.

Esta vez, usemos un TokenClassificationPipeline:

from transformers import TokenClassificationPipeline, pipeline


MODEL_PATH = "./models/EsperBERTo-small-pos/"

nlp = pipeline(
    "ner",
    model=MODEL_PATH,
    tokenizer=MODEL_PATH,
)
# o instanciar un TokenClassificationPipeline directamente.

nlp("Mi estas viro kej estas tago varma.")

# {'entity': 'PRON', 'score': 0.9979867339134216, 'word': ' Mi'}
# {'entity': 'VERB', 'score': 0.9683094620704651, 'word': ' estas'}
# {'entity': 'VERB', 'score': 0.9797462821006775, 'word': ' estas'}
# {'entity': 'NOUN', 'score': 0.8509314060211182, 'word': ' tago'}
# {'entity': 'ADJ', 'score': 0.9996201395988464, 'word': ' varma'}

¡Parece que funcionó! 🔥

Para un conjunto de datos más desafiante para la NER, @stefan-it recomendó que podríamos entrenar en el conjunto de datos de estándar de plata de WikiANN

6. Comparte tu modelo 🎉

Finalmente, cuando tengas un buen modelo, por favor piensa en compartirlo con la comunidad:

  • sube tu modelo usando la CLI: transformers-cli upload
  • escribe una tarjeta README.md del modelo y añádela al repositorio en model_cards/. Tu tarjeta del modelo debe incluir idealmente:
    • una descripción del modelo,
    • parámetros de entrenamiento (conjunto de datos, preprocesamiento, hiperparámetros),
    • resultados de evaluación,
    • usos previstos y limitaciones,
    • ¡cualquier otra cosa que sea útil! 🤓

¡TADA!

➡️ Tu modelo tiene una página en https://huggingface.co/models y todos pueden cargarlo usando AutoModel.from_pretrained("nombre_de_usuario/nombre_del_modelo").

Si quieres ver modelos en diferentes idiomas, visita https://huggingface.co/models

¡Gracias!

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Superconductor LK-99 Tal vez un avance, tal vez solo una nueva esperanza

Expertos se oponen a afirmaciones extraordinarias sobre un superconductor a temperatura ambiente. Pero incluso un fra...

Inteligencia Artificial

Conoce a cinco innovadores en IA generativa en África y Oriente Medio

Los emprendedores están cultivando IA generativa desde la costa oeste de África hasta el borde oriental del desierto ...

Inteligencia Artificial

Investigadores de IA de Salesforce presentan la evolución de los agentes autónomos mejorados con LLM y la innovadora estrategia BOLAA

I had trouble accessing your link so I’m going to try to continue without it. Los recientes logros de los model...

Inteligencia Artificial

Principales bibliotecas de procesamiento de imágenes en Python

La visión por computadora es una rama de la inteligencia artificial (IA) que permite a las computadoras y sistemas ex...