#
Construir un tokenizador, bloque por bloque

Como hemos visto en las secciones previas, la tokenización está compuesta de varias etapas:

- Normalización (cualquier limpieza del texto que se considere necesaria, tales como remover espacios o acentos, normalización Unicode, etc.)
- Pre-tokenización (separar la entrada en palabras)
- Pasar las entradas (inputs) por el modelo (usar las palabras pre-tokenizadas para producir una secuencia de tokens)
- Post-procesamiento (agregar tokens especiales del tokenizador, generando la máscara de atención (attention mask) y los IDs de tipo de token)

Como recordatorio, acá hay otro vistazo al proceso en totalidad:

La librería 🤗 Tokenizers ha sido construida para proveer varias opciones para cada una de esas etapas, las cuales se pueden mezclar y combinar. En esta sección veremos cómo podemos construir un tokenizador desde cero, opuesto al entrenamiento de un nuevo tokenizador a partir de uno existente como hicimos en la Sección 2. Después de esto, serás capaz de construir cualquier tipo de tokenizador que puedas imaginar!

De manera más precisa, la librería está construida a partir de una clase central `Tokenizer` con las unidades más básica reagrupadas en susbmódulos:

- `normalizers` contiene todos los posibles tipos de `Normalizer` que puedes usar (la lista completa aquí).
- `pre_tokenizers` contiene todos los posibles tipos de `PreTokenizer` que puedes usar (la lista completa aquí).
- `models` contiene los distintos tipos de `Model` que puedes usar, como `BPE`, `WordPiece`, and `Unigram` (la lista completa aquí).
- `trainers` contiene todos los distintos tipos de `Trainer` que puedes usar para entrenar tu modelo en un corpus (uno por cada tipo de modelo; la lista completa aquí).
- `post_processors` contiene varios tipos de `PostProcessor` que puedes usar (la lista completa aquí).
- `decoders` contiene varios tipos de `Decoder` que puedes usar para decodificar las salidas de la tokenización (la lista completa aquí).

Puedes encontrar la lista completas de las unidades más básicas aquí.

##
Adquirir un corpus

Para entrenar nuestro nuevo tokenizador, usaremos un pequeño corpus de texto (para que los ejemplos se ejecuten rápido). Los pasos para adquirir el corpus son similares a los que tomamos al beginning of this chapter, pero esta vez usaremos el conjunto de datos WikiText-2:

    from datasets import load_dataset

    dataset = load_dataset("wikitext", name="wikitext-2-raw-v1", split="train")

    def get_training_corpus():
        for i in range(0, len(dataset), 1000):
            yield dataset[i : i + 1000]["text"]

La función `get_training_corpus()` es un generador que entregará lotes de 1.000 textos, los cuales usaremos para entrenar el tokenizador.

🤗 Tokenizers puedes también ser entrenada en archivos de textos directamente. Así es como podemos generar un archivo de texto conteniendo todos los textos/entradas de WikiText-2 que podemos usar localmente:

    with open("wikitext-2.txt", "w", encoding="utf-8") as f:
        for i in range(len(dataset)):
            f.write(dataset[i]["text"] + "\n")

A continuación mostraremos como construir tu propios propios tokenizadores BERT, GPT-2 y XLNet, bloque por bloque. Esto nos dará un ejemplo de cada una de los tres principales algoritmos de tokenización: WordPiece, BPE y Unigram. Empecemos con BERT!

##
Construyendo un tokenizador WordPiece desde cero

Para construir un tokenizador con la librería 🤗 Tokenizers, empezamos instanciando un objeto `Tokenizer` con un `model`, luego fijamos sus atributos `normalizer`, `pre_tokenizer`, `post_processor`, y `decoder` a los valores que queremos.

Para este ejemplo, crearemos un `Tokenizer` con modelo WordPiece:

    from tokenizers import (
        decoders,
        models,
        normalizers,
        pre_tokenizers,
        processors,
        trainers,
        Tokenizer,
    )

    tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))

Tenemos que especificar el `unk_token` para que el modelo sepa que retornar si encuentra caracteres que no ha visto antes. Otros argumentos que podemos fijar acá incluyen el `vocab` de nuestro modelo (vamos a entrenar el modelo, por lo que no necesitamos fijar esto) y `max_input_chars_per_word`, el cual especifica el largo máximo para cada palabra (palabras más largas que el valor pasado se serpararán).

El primer paso de la tokenización es la normalizacion, así que empecemos con eso. Dado que BERT es ampliamente usado, hay un `BertNormalizer` con opciones clásicas que podemos fijar para BERT: `lowercase` (transformar a minúsculas) y `strip_accents` (eliminar acentos); `clean_text` para remover todos los caracteres de control y reemplazar espacios repetidos en uno solo; y `handle_chinese_chars` el cual coloca espacios alrededor de los caracteres en Chino. Para replicar el tokenizador `bert-base-uncased`, basta con fijar este normalizador:

    tokenizer.normalizer = normalizers.BertNormalizer(lowercase=True)

Sin embargo, en términos generales, cuando se construye un nuevo tokenizador no tendrás acceso a tan útil normalizador ya implementado en la librería 🤗 Tokenizers — por lo que veamos como crear el normalizador BERT a mano. La librería provee un normalizador `Lowercase` y un normalizador `StripAccents`, y puedes componer varios normalizadores usando un `Sequence` (secuencia):

    tokenizer.normalizer = normalizers.Sequence(
        [normalizers.NFD(), normalizers.Lowercase(), normalizers.StripAccents()]
    )

También estamos usando un normalizador Unicode `NFD`, ya que de otra manera el normalizador `StripAccents` no reconocerá apropiadamente los caracteres acentuados y por lo tanto, no los eliminará.

Como hemos visto antes, podemos usar el método `normalize_str()` del `normalizer` para chequear los efectos que tiene en un texto dado:

    # print(tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))

    hello how are u?

> Para ir más allá Si pruebas las dos versiones de los normalizadores previos en un string conteniendo un caracter unicode `u"\u0085"` de seguro notarás que los dos normalizadores no son exactamente equivalentes. Para no sobre-complicar demasiado la version con `normalizers.Sequence`, no hemos incluido los reemplazos usando Expresiones Regulares (Regex) que el `BertNormalizer` requiere cuando el argumento `clean_text` se fija como `True` - lo cual es el comportamiento por defecto. Pero no te preocupes, es posible obtener la misma normalización sin usar el útil `BertNormalizer` agregando dos `normalizers.Replace` a la secuencia de normalizadores.

A continuación está la etapa de pre-tokenización. De nuevo, hay un `BertPreTokenizer` pre-hecho que podemos usar:

    tokenizer.pre_tokenizer = pre_tokenizers.BertPreTokenizer()

O podemos constuirlo desde cero:

    tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()

Nota que el pre-tokenizador `Whitespace` separa en espacios en blando y todos los caracteres que no son letras, dígitos o el guión bajo/guión al piso (_), por lo que técnicamente separa en espacios en blanco y puntuación:

    tokenizer.pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")

    [('Let', (0, 3)), ("'", (3, 4)), ('s', (4, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre', (14, 17)),
     ('-', (17, 18)), ('tokenizer', (18, 27)), ('.', (27, 28))]

Si sólo quieres separar en espacios en blanco, deberías usar el pre-tokenizador `WhitespaceSplit`:

    pre_tokenizer = pre_tokenizers.WhitespaceSplit()
    pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")

    [("Let's", (0, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre-tokenizer.', (14, 28))]

Al igual que con los normalizadores, puedes un `Sequence` para componer varios pre-tokenizadores:

    pre_tokenizer = pre_tokenizers.Sequence(
        [pre_tokenizers.WhitespaceSplit(), pre_tokenizers.Punctuation()]
    )
    pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")

    [('Let', (0, 3)), ("'", (3, 4)), ('s', (4, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre', (14, 17)),
     ('-', (17, 18)), ('tokenizer', (18, 27)), ('.', (27, 28))]

El siguiente paso en el pipeline de tokenización es pasar las entradas a través del modelo. Ya especificamos nuestro modelo en la inicialización, pero todavía necesitamos entrenarlo, lo cual requerirá un `WordPieceTrainer`. El aspecto principal a recordar cuando se instancia un entrenador (trainer) en 🤗 Tokenizers es que necesitas pasarle todos los tokens especiales que tiene la intención de usar — de otra manera no los agregará al vocabulario, dado que que no están en el corpus de entrenamiento:

    special_tokens = ["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"]
    trainer = trainers.WordPieceTrainer(vocab_size=25000, special_tokens=special_tokens)

Al igual que especificar `vocab_size` y `special_tokens`, podemos fijar `min_frequency` (el número de veces que un token debe aparecer para ser incluido en el vocabulario) o cambiar `continuing_subword_prefix` (si queremos usar algo diferente a `##`).

Para entrenar nuestro modelo usando el iterador que definimos antes, tenemos que ejecutar el siguiente comando:

    tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

También podemos usar archivos de texto para entrenar nuestro tokenizador, lo cual se vería así (reinicializamos el modelo con un `WordPiece` vacío de antemano):

    tokenizer.model = models.WordPiece(unk_token="[UNK]")
    tokenizer.train(["wikitext-2.txt"], trainer=trainer)

En ambos casos, podemos probar el tokenizador en un texto llamando al método `encode:

    encoding = tokenizer.encode("Let's test this tokenizer.")
    # print(encoding.tokens)

    ['let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '.']

El `encoding` (codificación) obtenido es un objeto `Encoding`, el cual contiene todas las salidas necesarias del tokenizador y sus distintos atributos: `ids`, `type_ids`, `tokens`, `offsets`, `attention_mask`, `special_tokens_mask`, y `overflowing`.

El último paso en el pipeline de tokenización es el post-procesamiento. Necesitamos agregar el token `[CLS]` al inicio y el token `[SEP]` al final (o después de cada oración, si tenemos un par de oraciones). Usaremos un `TemplateProcessor` para esto, pero primero necesitamos conocer los IDs de los tokens `[CLS]` y `[SEP]` en el vocabulario:

    cls_token_id = tokenizer.token_to_id("[CLS]")
    sep_token_id = tokenizer.token_to_id("[SEP]")
    # print(cls_token_id, sep_token_id)

    (2, 3)

Para escribir la plantilla (template) para un `TemplateProcessor`, tenemos que especificar como tratar una sóla oración y un par de oraciones. Para ambos, escribimos los tokens especiales que queremos usar; la primera oración se representa por `$A`, mientras que la segunda oración (si se está codificando un par) se representa por `$B`. Para cada uno de estos (tokens especiales y oraciones), también especificamos el ID del tipo de token correspondiente después de un dos puntos (:).

La clásica plantilla para BERT se define como sigue:

    tokenizer.post_processor = processors.TemplateProcessing(
        single=f"[CLS]:0 $A:0 [SEP]:0",
        pair=f"[CLS]:0 $A:0 [SEP]:0 $B:1 [SEP]:1",
        special_tokens=[("[CLS]", cls_token_id), ("[SEP]", sep_token_id)],
    )

Nota que necesitamos pasar los IDs de los tokens especiales, para que el tokenizador pueda convertirlos apropiadamente a sus IDs.

Una vez que se agrega esto, volviendo a nuestro ejemplo anterior nos dará:

    encoding = tokenizer.encode("Let's test this tokenizer.")
    # print(encoding.tokens)

    ['[CLS]', 'let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '.', '[SEP]']

Y en un par de oraciones, obtenemos el resultado apropiado:

    encoding = tokenizer.encode("Let's test this tokenizer...", "on a pair of sentences.")
    # print(encoding.tokens)
    # print(encoding.type_ids)

    ['[CLS]', 'let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '...', '[SEP]', 'on', 'a', 'pair', 'of', 'sentences', '.', '[SEP]']
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]

Ya casi finalizamos de construir este tokenizador desde cero — el último paso es incluir un decodificador:

    tokenizer.decoder = decoders.WordPiece(prefix="##")

Probemoslo en nuestro `encoding` previo:

    tokenizer.decode(encoding.ids)

    "let's test this tokenizer... on a pair of sentences."

Genial! Ahora podemos guardar nuestro tokenizador en un archivo JSON así:

    tokenizer.save("tokenizer.json")

Podemos cargar ese archivo en un objeto `Tokenizer` con el método `from_file()`:

    new_tokenizer = Tokenizer.from_file("tokenizer.json")

Para usar este tokenizador en 🤗 Transformers, tenemos que envolverlo en un `PreTrainedTokenizerFast`. Podemos usar una clase generica o, si nuestro tokenizador corresponde un modelo existente, usar esa clase (en este caso, `BertTokenizerFast`). Si aplicas esta lección para construir un tokenizador nuevo de paquete, tendrás que usar la primera opción.

Para envolver el tokenizador en un `PreTrainedTokenizerFast`, podemos pasar el tokenizador que construimos como un `tokenizer_object` o pasar el archivo del tokenizador que guardarmos como `tokenizer_file`. El aspecto clave a recordar es que tenemos que manualmente fijar los tokens especiales, dado que la clase no puede inferir del objeto `tokenizer` qué token es el el token de enmascaramiento (mask token), el token `[CLS]`, etc.:

    from transformers import PreTrainedTokenizerFast

    wrapped_tokenizer = PreTrainedTokenizerFast(
        tokenizer_object=tokenizer,
        # tokenizer_file="tokenizer.json", # You can load from the tokenizer file, alternatively
        unk_token="[UNK]",
        pad_token="[PAD]",
        cls_token="[CLS]",
        sep_token="[SEP]",
        mask_token="[MASK]",
    )

Si estás usando una clase de tokenizador específico (como `BertTokenizerFast`), sólo necesitarás especificar los tokens especiales diferentes a los que están por defecto (en este caso, ninguno):

    from transformers import BertTokenizerFast

    wrapped_tokenizer = BertTokenizerFast(tokenizer_object=tokenizer)

Luego puedes usar este tokenizador como cualquier otro tokenizador de 🤗 Transformers. Puedes guardarlo con el método `save_pretrained()`, o subirlo al Hub con el método `push_to_hub()`.

Ahora que hemos visto como construir el tokenizador WordPiece, hagamos lo mismo para un tokenizador BPE. Iremos un poco más rápido dato que conoces todos los pasos, y sólo destacaremos las diferencias.

##
Construyendo un tokenizador BPE desde cero

Ahora construyamos un tokenizador GPT-2. Al igual que el tokenizador BERT, empezamos inicializando un `Tokenizer` con un modelo BPE:

    tokenizer = Tokenizer(models.BPE())

También al igual que BERT, podríamos inicializar este modelo con un vocabulario si tuviéramos uno (necesitaríamos pasar el `vocab` y `merges`, en este caso), pero dado que entrenaremos desde cero, no necesitaremos hacer eso. Tampoco necesitamos especificar `unk_token` porque GPT-2 utiliza un byte-level BPE, que no lo requiere.

GPT-2 no usa un normalizador, por lo que nos saltamos este paso y vamos directo a la pre-tokenización:

    tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)

La opción que agregada acá `ByteLevel` es para no agregar un espacio al inicio de una oración (el cuál es el valor por defecto). Podemos echar un vistazo a la pre-tokenización de un texto de ejemplo como antes:

    tokenizer.pre_tokenizer.pre_tokenize_str("Let's test pre-tokenization!")

    [('Let', (0, 3)), ("'s", (3, 5)), ('Ġtest', (5, 10)), ('Ġpre', (10, 14)), ('-', (14, 15)),
     ('tokenization', (15, 27)), ('!', (27, 28))]

A continuación está el modelo, el cual necesita entrenamiento. Para GPT-2, el único token especial es el token de final de texto (end-of-text):

    trainer = trainers.BpeTrainer(vocab_size=25000, special_tokens=["<|endoftext|>"])
    tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

Al igual que con el `WordPieceTrainer`, junto con `vocab_size` y `special_tokens`, podemos especificar el `min_frequency` si queremos, o si tenemos un sufijo de fín de palabra (end-of-word suffix) (como `</w>`), podemos fijarlo con `end_of_word_suffix`.

Este tokenizador también se puede entrenar en archivos de textos:

    tokenizer.model = models.BPE()
    tokenizer.train(["wikitext-2.txt"], trainer=trainer)

Echemos un vistazo a la tokenización de un texto de muestra:

    encoding = tokenizer.encode("Let's test this tokenizer.")
    # print(encoding.tokens)

    ['L', 'et', "'", 's', 'Ġtest', 'Ġthis', 'Ġto', 'ken', 'izer', '.']

Aplicaremos el post-procesamiento byte-level para el tokenizador GPT-2 como sigue:

    tokenizer.post_processor = processors.ByteLevel(trim_offsets=False)

La opción `trim_offsets = False` indica al post-procesador que deberíamos dejar los offsets de los tokens que comiencen con ‘Ġ’ sin modificar: De esta manera el inicio de los offsets apuntarán al espacio antes de la palabra, no el primer caracter de la palabra (dado que el espacio es técnicamente parte del token). Miremos el resultado con el texto que acabamos de codificar, donde `'Ġtest'` el token en el índice 4:

    sentence = "Let's test this tokenizer."
    encoding = tokenizer.encode(sentence)
    start, end = encoding.offsets[4]
    sentence[start:end]

    ' test'

Finalmente, agregamos un decodificador byte-level:

    tokenizer.decoder = decoders.ByteLevel()

y podemos chequear si funciona de manera apropiada:

    tokenizer.decode(encoding.ids)

    "Let's test this tokenizer."

Genial! Ahora que estamos listos, podemos guardar el tokenizador como antes, y envolverlo en un `PreTrainedTokenizerFast` o `GPT2TokenizerFast` si queremos usarlo en 🤗 Transformers:

    from transformers import PreTrainedTokenizerFast

    wrapped_tokenizer = PreTrainedTokenizerFast(
        tokenizer_object=tokenizer,
        bos_token="<|endoftext|>",
        eos_token="<|endoftext|>",
    )

o:

    from transformers import GPT2TokenizerFast

    wrapped_tokenizer = GPT2TokenizerFast(tokenizer_object=tokenizer)

Como en el último ejemplo, mostraremos cómo construir un tokenizador Unigram desde cero.

##
Construyendo un tokenizador Unigran desde cero

Construyamos un tokenizador XLNet. Al igual que los tokenizadores previos, empezamos inicializando un `Tokenizer` con un modelo Unigram:

    tokenizer = Tokenizer(models.Unigram())

De nuevo, podríamos inicializar este modelo con un vocabulario si tuvieramos uno.

Para la normalización, XLNet utiliza unos pocos reemplazos (los cuales vienen de SentencePiece):

    from tokenizers import Regex

    tokenizer.normalizer = normalizers.Sequence(
        [
            normalizers.Replace("``", '"'),
            normalizers.Replace("''", '"'),
            normalizers.NFKD(),
            normalizers.StripAccents(),
            normalizers.Replace(Regex(" {2,}"), " "),
        ]
    )

Esto reemplaza `“` y `”` con `”` y cualquier secuencia de dos o más espacios con un espacio simple, además remueve los acentos en el texto a tokenizar.

El pre-tokenizador a usar para cualquier tokenizador SentencePiece es `Metaspace`:

    tokenizer.pre_tokenizer = pre_tokenizers.Metaspace()

Podemos echar un vistazo a la pre-tokenización de un texto de ejemplo como antes:

    tokenizer.pre_tokenizer.pre_tokenize_str("Let's test the pre-tokenizer!")

    [("▁Let's", (0, 5)), ('▁test', (5, 10)), ('▁the', (10, 14)), ('▁pre-tokenizer!', (14, 29))]

A continuación está el modelo, el cuál necesita entrenamiento. XLNet tiene varios tokens especiales:

    special_tokens = ["<cls>", "<sep>", "<unk>", "<pad>", "<mask>", "<s>", "</s>"]
    trainer = trainers.UnigramTrainer(
        vocab_size=25000, special_tokens=special_tokens, unk_token="<unk>"
    )
    tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

Un argumento muy importante a no olvidar para el `UnigramTrainer` es el `unk_token`. También podemos pasarle otros argumentos específicos al algoritmo Unigram, tales como el `shrinking_factor` para cada paso donde removemos tokens (su valor por defecto es 0.75) o el `max_piece_length` para especificar el largo máximo de un token dado (su valor por defecto es 16).

Este tokenizador también se puede entrenar en archivos de texto:

    tokenizer.model = models.Unigram()
    tokenizer.train(["wikitext-2.txt"], trainer=trainer)

Ahora miremos la tokenización de un texto de muestra:

    encoding = tokenizer.encode("Let's test this tokenizer.")
    # print(encoding.tokens)

    ['▁Let', "'", 's', '▁test', '▁this', '▁to', 'ken', 'izer', '.']

Una peculiariodad de XLNet es que coloca el token `<cls>` al final de la oración, con un ID de tipo de 2 (para distinguirlo de los otros tokens). Como el resultado el resultado se rellena a la izquierda (left padding). Podemos lidiar con todos los tokens especiales y el token de ID de tipo con una plantilla, al igual que BERT, pero primero tenemos que obtener los IDs de los tokens `<cls>` y `<sep>`:

    cls_token_id = tokenizer.token_to_id("<cls>")
    sep_token_id = tokenizer.token_to_id("<sep>")
    # print(cls_token_id, sep_token_id)

    0 1

La plantilla se ve así:

    tokenizer.post_processor = processors.TemplateProcessing(
        single="$A:0 <sep>:0 <cls>:2",
        pair="$A:0 <sep>:0 $B:1 <sep>:1 <cls>:2",
        special_tokens=[("<sep>", sep_token_id), ("<cls>", cls_token_id)],
    )

Y podemos probar si funciona codificando un par de oraciones:

    encoding = tokenizer.encode("Let's test this tokenizer...", "on a pair of sentences!")
    # print(encoding.tokens)
    # print(encoding.type_ids)

    ['▁Let', "'", 's', '▁test', '▁this', '▁to', 'ken', 'izer', '.', '.', '.', '<sep>', '▁', 'on', '▁', 'a', '▁pair',
      '▁of', '▁sentence', 's', '!', '<sep>', '<cls>']
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]

Finalmente, agregamos el decodificador `Metaspace`:

    tokenizer.decoder = decoders.Metaspace()

y estamos listos con este tokenizador! Podemos guardar el tokenizador como antes, y envolverlo en un `PreTrainedTokenizerFast` o `XLNetTokenizerFast` si queremos usarlo en 🤗 Transformers. Una cosa a notar al usar `PreTrainedTokenizerFast` es que además de los tokens especiales, necesitamos decirle a la librería 🤗 Transformers que rellene a la izquierda (agregar left padding):

    from transformers import PreTrainedTokenizerFast

    wrapped_tokenizer = PreTrainedTokenizerFast(
        tokenizer_object=tokenizer,
        bos_token="<s>",
        eos_token="</s>",
        unk_token="<unk>",
        pad_token="<pad>",
        cls_token="<cls>",
        sep_token="<sep>",
        mask_token="<mask>",
        padding_side="left",
    )

O de manera alternativa:

    from transformers import XLNetTokenizerFast

    wrapped_tokenizer = XLNetTokenizerFast(tokenizer_object=tokenizer)

Ahora que has visto como varias de nuestras unidades más básicas se usan para construir tokenizadores existentes, deberías ser capaz de escribir cualquier tokenizador que quieras con la librería 🤗 Tokenizers y ser capaz de usarlo en la librería 🤗 Transformers.

Update on GitHub