Tokenizadores
Los tokenizadores son uno de los componentes fundamentales del pipeline en NLP. Sirven para traducir texto en datos que los modelos puedan procesar; es decir, de texto a valores numéricos. En esta sección veremos en qué se fundamenta todo el proceso de tokenizado.
En las tareas de NLP, los datos generalmente ingresan como texto crudo. Por ejemplo:
Jim Henson era un titiritero
Sin embargo, necesitamos una forma de convertir el texto crudo a valores numéricos para los modelos. Eso es precisamente lo que hacen los tokenizadores, y existe una variedad de formas en que puede hacerse. El objetivo final es obtener valores que sean cortos pero muy significativos para el modelo.
Veamos algunos algoritmos de tokenización, e intentemos atacar algunas preguntas que puedas tener.
Tokenización Word-based
El primer tokenizador que nos ocurre es el word-based (basado-en-palabras). Es generalmente sencillo, con pocas normas, y generalmente da buenos resultados. Por ejemplo, en la imagen a continuación separamos el texto en palabras y buscamos una representación numérica.
Existen varias formas de separar el texto. Por ejemplo, podríamos usar los espacios para tokenizar usando Python y la función split()
.
tokenized_text = "Jim Henson era un titiritero".split()
print(tokenized_text)
['Jim', 'Henson', 'era', 'un', 'titiritero']
También hay variaciones de tokenizadores de palabras que tienen reglas adicionales para la puntuación. Con este tipo de tokenizador, podemos acabar con unos “vocabularios” bastante grandes, donde un vocabulario se define por el número total de tokens independientes que tenemos en nuestro corpus.
A cada palabra se le asigna un ID, empezando por 0 y subiendo hasta el tamaño del vocabulario. El modelo utiliza estos ID para identificar cada palabra.
Si queremos cubrir completamente un idioma con un tokenizador basado en palabras, necesitaremos tener un identificador para cada palabra del idioma, lo que generará una enorme cantidad de tokens. Por ejemplo, hay más de 500.000 palabras en el idioma inglés, por lo que para construir un mapa de cada palabra a un identificador de entrada necesitaríamos hacer un seguimiento de esa cantidad de identificadores. Además, palabras como “perro” se representan de forma diferente a palabras como “perros”, y el modelo no tendrá forma de saber que “perro” y “perros” son similares: identificará las dos palabras como no relacionadas. Lo mismo ocurre con otras palabras similares, como “correr” y “corriendo”, que el modelo no verá inicialmente como similares.
Por último, necesitamos un token personalizado para representar palabras que no están en nuestro vocabulario. Esto se conoce como el token “desconocido”, a menudo representado como ”[UNK]” o ”<unk>”. Generalmente, si el tokenizador está produciendo muchos de estos tokens es una mala señal, ya que no fue capaz de recuperar una representación de alguna palabra y está perdiendo información en el proceso. El objetivo al elaborar el vocabulario es hacerlo de tal manera que el tokenizador tokenice el menor número de palabras posibles en tokens desconocidos.
Una forma de reducir la cantidad de tokens desconocidos es ir un poco más allá, utilizando un tokenizador word-based.
Tokenización Character-based
Un tokenizador character-based separa el texto en caracteres, y no en palabras. Esto conlleva dos beneficios principales:
- Obtenemos un vocabulario mucho más corto.
- Habrá muchos menos tokens por fuera del vocabulario conocido.
No obstante, pueden surgir inconvenientes por los espacios en blanco y signos de puntuación.
Así, este método tampoco es perfecto. Dada que la representación se construyó con caracteres, uno podría pensar intuitivamente que resulta menos significativo: Cada una de las palabras no significa mucho por separado, mientras que las palabras sí. Sin embargo, eso es dependiente del idioma. Por ejemplo en Chino, cada uno de los caracteres conlleva más información que en un idioma latino.
Otro aspecto a considerar es que terminamos con una gran cantidad de tokens que el modelo debe procesar, mientras que en el caso del tokenizador word-based, un token representa una palabra, en la representación de caracteres fácilmente puede necesitar más de 10 tokens.
Para obtener lo mejor de ambos mundos, podemos usar una combinación de las técnicas: la tokenización por subword tokenization.
Tokenización por Subword
Los algoritmos de tokenización de subpalabras se basan en el principio de que las palabras de uso frecuente no deben dividirse, mientras que las palabras raras deben descomponerse en subpalabras significativas.
Por ejemplo, “extrañamente” podría considerarse una palabra rara y podría descomponerse en “extraña” y “mente”. Es probable que ambas aparezcan con más frecuencia como subpalabras independientes, mientras que al mismo tiempo el significado de “extrañamente” se mantiene por el significado compuesto de “extraña” y “mente”.
Este es un ejemplo que muestra cómo un algoritmo de tokenización de subpalabras tokenizaría la secuencia “Let’s do tokenization!“:
Estas subpalabras terminan aportando mucho significado semántico: por ejemplo, en el ejemplo anterior, “tokenización” se dividió en “token” y “ización”, dos tokens que tienen un significado semántico y a la vez son eficientes en cuanto al espacio (sólo se necesitan dos tokens para representar una palabra larga). Esto nos permite tener una cobertura relativamente buena con vocabularios pequeños y casi sin tokens desconocidos.
Este enfoque es especialmente útil en algunos idiomas como el turco, donde se pueden formar palabras complejas (casi) arbitrariamente largas encadenando subpalabras.
Y más!
Como es lógico, existen muchas más técnicas. Por nombrar algunas:
- Byte-level BPE (a nivel de bytes), como usa GPT-2
- WordPiece, usado por BERT
- SentencePiece or Unigram (pedazo de sentencia o unigrama), como se usa en los modelos multilingües
A este punto, deberías tener conocimientos suficientes sobre el funcionamiento de los tokenizadores para empezar a utilizar la API.
Cargando y guardando
Cargar y guardar tokenizadores es tan sencillo como lo es con los modelos. En realidad, se basa en los mismos dos métodos: from_pretrained()
y save_pretrained()
. Estos métodos cargarán o guardarán el algoritmo utilizado por el tokenizador (un poco como la arquitectura del modelo) así como su vocabulario (un poco como los pesos del modelo).
La carga del tokenizador BERT entrenado con el mismo punto de control que BERT se realiza de la misma manera que la carga del modelo, excepto que utilizamos la clase BertTokenizer
:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
Al igual que AutoModel
, la clase AutoTokenizer
tomará la clase de tokenizador adecuada en la librería basada en el nombre del punto de control, y se puede utilizar directamente con cualquier punto de control:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
Ahora podemos utilizar el tokenizador como se muestra en la sección anterior:
tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
Guardar un tokenizador es idéntico a guardar un modelo:
tokenizer.save_pretrained("directorio_en_mi_computador")
Hablaremos más sobre token_type_ids
en el Capítulo 3, y explicaremos la clave attention_mask
un poco más tarde. Primero, veamos cómo se generan los input_ids
. Para ello, tendremos que ver los métodos intermedios del tokenizador.
Encoding
La traducción de texto a números se conoce como codificación. La codificación se realiza en un proceso de dos pasos: la tokenización, seguida de la conversión a IDs de entrada.
Como hemos visto, el primer paso es dividir el texto en palabras (o partes de palabras, símbolos de puntuación, etc.), normalmente llamadas tokens. Hay múltiples reglas que pueden gobernar ese proceso, por lo que necesitamos instanciar el tokenizador usando el nombre del modelo, para asegurarnos de que usamos las mismas reglas que se usaron cuando se preentrenó el modelo.
El segundo paso es convertir esos tokens en números, para poder construir un tensor con ellos y alimentar el modelo. Para ello, el tokenizador tiene un vocabulario, que es la parte que descargamos cuando lo instanciamos con el método from_pretrained()
. De nuevo, necesitamos usar el mismo vocabulario que se usó cuando el modelo fue preentrenado.
Para entender mejor los dos pasos, los exploraremos por separado. Ten en cuenta que utilizaremos algunos métodos que realizan partes del proceso de tokenización por separado para mostrarte los resultados intermedios de esos pasos, pero en la práctica, deberías llamar al tokenizador directamente en tus inputs (como se muestra en la sección 2).
Tokenization
El proceso de tokenización se realiza mediante el método tokenize()
del tokenizador:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
La salida de este método es una lista de cadenas, o tokens:
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
Este tokenizador es un tokenizador de subpalabras: divide las palabras hasta obtener tokens que puedan ser representados por su vocabulario. Este es el caso de transformer
, que se divide en dos tokens: transform
y ##er
.
De tokens a IDs de entrada
La conversión a IDs de entrada se hace con el método del tokenizador convert_tokens_to_ids()
:
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
Estos resultados, una vez convertidos en el tensor del marco apropiado, pueden utilizarse como entradas de un modelo, como se ha visto anteriormente en este capítulo.
✏️ Try it out! Replica los dos últimos pasos (tokenización y conversión a IDs de entrada) en las frases de entrada que utilizamos en la sección 2 (“Llevo toda la vida esperando un curso de HuggingFace” y “¡Odio tanto esto!”). Comprueba que obtienes los mismos ID de entrada que obtuvimos antes!
Decodificación
La decodificación va al revés: a partir de los índices del vocabulario, queremos obtener una cadena. Esto se puede hacer con el método decode()
de la siguiente manera:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'
Notemos que el método decode
no sólo convierte los índices de nuevo en tokens, sino que también agrupa los tokens que formaban parte de las mismas palabras para producir una frase legible. Este comportamiento será extremadamente útil cuando utilicemos modelos que predigan texto nuevo (ya sea texto generado a partir de una indicación, o para problemas de secuencia a secuencia como la traducción o el resumen).
A estas alturas deberías entender las operaciones atómicas que un tokenizador puede manejar: tokenización, conversión a IDs, y conversión de IDs de vuelta a una cadena. Sin embargo, sólo hemos rozado la punta del iceberg. En la siguiente sección, llevaremos nuestro enfoque a sus límites y echaremos un vistazo a cómo superarlos.