Lección 13 · 15 min · Gratis

Entendiendo los Tokenizers de Mistral

[!IMPORTANT] Este documento está obsoleto. Por favor, consulta la documentación completa y detallada aquí.

Desde el tokenizer original V1 hasta los más recientes tokenizers V3 y Tekken, los tokenizers de Mistral han experimentado cambios sutiles relacionados con la forma de tokenizar para los modelos de instrucción. Hemos avanzado mucho en optimización e investigación para encontrar el mejor enfoque. Hoy, profundizaremos en estos tokenizers, desmitificaremos cualquier fuente de debate y exploraremos cómo funcionan, las plantillas de chat adecuadas para usar con cada uno y su historia dentro de la comunidad.

En este artículo, nos centraremos principalmente en la tokenización de instrucciones y las plantillas de chat para el seguimiento de instrucciones simples. No profundizaremos en la llamada a funciones ni en el "fill in the middle" (FIM).

La información fundamental para todo lo que se presenta aquí se puede encontrar explorando los repositorios en Hugging Face, así como en GitHub, específicamente mistral_common.

TL;DR

Tokenizer V1:

"<s> [INST] user message [/INST] assistant message</s> [INST] new user message [/INST]"

Con mistral-common, el prompt del sistema se antepone al primer mensaje del usuario por defecto (puedes personalizarlo)

Tokenizer V2:

"<s>[INST] user message[/INST] assistant message</s>[INST] new user message[/INST]"

Con mistral-common, el prompt del sistema se antepone al último mensaje del usuario por defecto (puedes personalizarlo)

FIM

"<s>[SUFFIX]suffix[PREFIX] prefix"

Tokenizer V3:

"<s>[INST] user message[/INST] assistant message</s>[INST] new user message[/INST]"

V3 es muy similar a V2; la única diferencia concierne a la llamada a funciones.

FIM

"<s>[SUFFIX]suffix[PREFIX] prefix"

Tokenizer V3 - Tekken (Nemo):

"<s>[INST]user message[/INST]assistant message</s>[INST]new user message[/INST]"

Con mistral-common, el prompt del sistema se antepone al último mensaje del usuario por defecto (puedes personalizarlo)

EDITAR: Puedes encontrar un nuevo documento explicativo en nuestros cookbooks

Tokenizer V1

Las primeras versiones, Mistral 7B V1 y V2, así como Mixtral 8x7B V1, fueron recibidas con mucho aprecio y cariño por parte de la comunidad. Sin embargo, esto no impidió algunos desacuerdos y debates dentro de la comunidad con respecto a las plantillas de chat correctas para los tokenizers de instrucción. Hoy, puedes confiar de manera fiable en mistral_common como la verdad fundamental para el proceso de tokenización, pero en aquel entonces, la mayor parte de la comunidad se basaba en las plantillas de chat Jinja disponibles.

La comunidad fue muy rápida en notar inconsistencias en torno a los tokenizers y las plantillas de chat, logrando mejores resultados con versiones personalizadas que ajustaban los espacios en blanco alrededor de las cadenas especiales. ¡Es increíble lo rápida y útil que es la comunidad para detectar cualquier problema y error relacionado con los modelos!

El tokenizer utilizado para estas primeras versiones, basado en sentencepiece, utiliza una plantilla similar a la de los antiguos modelos Llama. Este tokenizer maneja los mensajes de instrucción de la siguiente manera:

Plantilla de Chat de Instrucción

La plantilla de chat era, como se mencionó anteriormente, muy similar a la primera plantilla de Llama, y resultaría en cadenas como la siguiente.

<s> [INST] user message [/INST] assistant message</s> [INST] new user message [/INST]

Aquí, las únicas cadenas especiales eran [INST] para iniciar el mensaje del usuario y [/INST] para finalizar el mensaje del usuario, dando paso a la respuesta del asistente. El BOS (inicio de cadena) estaba y sigue estando representado con <s>, y el EOS (fin de cadena) es </s>, utilizado al final de cada finalización, terminando cualquier mensaje del asistente.

Los espacios en blanco son de extrema importancia.

Ten en cuenta que <s> y </s> son más como cadenas que representan el BOS y el EOS que cadenas reales. Sus IDs correspondientes son 1 y 2.

⚠️ Esta plantilla siempre hará que el modelo genere un token que comienza con un espacio en blanco, siempre teniendo un espacio inicial. Dependiendo de tu motor de inferencia, puede que elimine o no ese espacio en blanco por defecto. Por lo tanto, es esencial asegurarse de que un espacio en blanco (y solo uno) esté presente después de cada respuesta del asistente al usar la plantilla.

La plantilla Jinja para el transformers de HuggingFace para este tipo de cadena podría verse así:

{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}

Puedes ver la que se está usando actualmente por defecto echando un vistazo al tokenizer_config.json.

Aquí está formateada para que sea más fácil de leer, lo que facilita la comprensión de cómo funcionan:

{{ bos_token }}
{% for message in messages %}
    {% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}
        {{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}
    {% endif %}
    {% if message['role'] == 'user' %}
        {{ ' [INST] ' + message['content'] + ' [/INST]' }}
    {% elif message['role'] == 'assistant' %}
        {{ ' ' + message['content'] + eos_token }}
    {% else %}
        {{ raise_exception('Only user and assistant roles are supported!') }}
    {% endif %}
{% endfor %}

⚠️ Como puedes ver, cada message['content'] siempre va precedido de un espacio en blanco. Esto asume que message['content'] tiene el primer espacio en blanco eliminado manualmente por ti o por tu biblioteca de inferencia.

Si quieres aplicar la plantilla manualmente, puedes hacerlo en Python:

from jinja2 import Template, Environment, exceptions

JINJA_TEMPLATE = "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ ' [INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}"

def raise_exception(message):
    raise ValueError(message)

def apply_jinja_template(messages, bos_token='<s>', eos_token='</s>'):
    template_str = JINJA_TEMPLATE

    env = Environment()
    env.globals['raise_exception'] = raise_exception

    template = env.from_string(template_str)

    try:
        result = template.render(messages=messages, bos_token=bos_token, eos_token=eos_token)
        return result
    except exceptions.TemplateError as e:
        raise ValueError(f"Template rendering error: {e}")

print(apply_jinja_template(messages))

Lógica de Tokenización de Instrucciones

Ahora profundicemos en la lógica original detrás del proceso de tokenización que puedes ver en mistral_common. A menudo no simplemente proporcionarías el texto al tokenizer y codificarías toda la cadena tal como está, mistral-common en realidad va directamente de request -> int, mientras que otros enfoques como el utilizado por transformers están más cerca de request -> str -> int. La forma en que se hace realmente para el proceso de tokenización es más cercana a la siguiente lógica:

  • Tokenizar cada mensaje:
    Tendrías mensajes de usuario y mensajes de asistente. Los tomarías por separado como cadenas individuales, con el mensaje del usuario encapsulado con las cadenas especiales, y los codificarías. En el ejemplo anterior, esto sería algo como:
    encode("[INST] user message [/INST]"), encode("assistant message"), y encode("[INST] new user message [/INST]"), y así sucesivamente...

  • Concatenar:
    Una vez que cada mensaje es tokenizado y codificado, los concatenarías todos, anteponiéndoles el ID de BOS y separándolos por pares con el ID de EOS:
    BOS_ID + encode("[INST] user message [/INST]") + encode("assistant message") + EOS_ID + encode("[INST] new user message [/INST]")

Podrías notar que si se sigue paso a paso, esto en realidad coincidiría con una cadena y espacios en blanco añadidos de la siguiente manera:

<s>[INST]_user message_[/INST]_assistant message</s>[INST]_new user message_[/INST]

"_" representa los espacios en blanco añadidos al encapsular con las cadenas especiales

Si se compara con la plantilla de chat mencionada anteriormente, esto omite algunos espacios en blanco después del BOS y el EOS. ¿De dónde vienen entonces? Vienen de sentencepiece al codificar. Por defecto, comenzará con un espacio en blanco inicial. Así que cada vez que encode, estamos añadiendo un espacio en blanco inicial. Por lo tanto:

  • encode("[INST] user message [/INST]") en realidad está codificando encode(" [INST] user message [/INST]")

Esto añade los espacios en blanco iniciales, haciendo que la cadena final se vea más como:

<s>_[INST] user message [/INST] assistant message</s>_[INST] new user message [/INST]

"_" representa los nuevos espacios en blanco añadidos por sentencepiece

Finalmente, token por token dará lo siguiente:

<s> _[ INST ] _user _message _[ / INST ] _assistant _message </s> _[ INST ] _new _user _message _[ / INST ]
1 733 16289 28793 2188 2928 733 28748 16289 28793 13892 2928 2 733 16289 28793 633 2188 2928 733 28748 16289 28793

"_" representa cualquier espacio en blanco

Prompt del Sistema

Las primeras versiones no tenían una metodología específica para los prompts del sistema. Por defecto, usualmente anteponemos el prompt del sistema al primer mensaje del usuario, seguido de dos nuevas líneas:

<s> [INST] system prompt

user message [/INST] assistant message</s> [INST] new user message [/INST]

Tokenizer V2

Después del tokenizer anterior, y utilizado principalmente por los modelos propietarios, la segunda versión del tokenizer impulsó modelos como los ahora antiguos Mistral Small 2402 y Mistral Large 2402. Este nuevo tokenizer introdujo tokens de control, específicamente para las cadenas especiales anteriores [INST] y [/INST], que se convirtieron en tokens de control con los IDs correspondientes 3 y 4, pero también nuevos tokens de control para la llamada a funciones.

Plantilla de Chat de Instrucción

Debido a la introducción de los tokens de control, la plantilla de chat cambió ligeramente debido a la ausencia de algunos espacios en blanco.

<s>[INST] user message[/INST] assistant message</s>[INST] new user message[/INST]

Para comparar, aquí tienes una cadena con la plantilla de chat anterior del tokenizer previo:

<s> [INST] user message [/INST] assistant message</s> [INST] new user message [/INST]

⚠️ Esta plantilla siempre hará que el modelo genere un token que comienza con un espacio en blanco, siempre teniendo un espacio inicial. Dependiendo de tu motor de inferencia, puede que elimine o no ese espacio en blanco por defecto. Por lo tanto, es esencial asegurarse de que un espacio en blanco (y solo uno) esté presente después de cada respuesta del asistente al usar la plantilla.

La plantilla Jinja para el transformers de HuggingFace para este tipo de cadena podría verse así:

{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}

Después de ser formateado para que sea más fácil de leer:

{{ bos_token }}
{% for message in messages %}
    {% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}
        {{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}
    {% endif %}
    {% if message['role'] == 'user' %}
        {{ '[INST] ' + message['content'] + '[/INST]' }}
    {% elif message['role'] == 'assistant' %}
        {{ ' ' + message['content'] + eos_token }}
    {% else %}
        {{ raise_exception('Only user and assistant roles are supported!') }}
    {% endif %}
{% endfor %}

⚠️ Como puedes ver, cada message['content'] siempre va precedido de un espacio en blanco. Esto asume que message['content'] tiene el primer espacio en blanco eliminado manualmente por ti o por tu biblioteca de inferencia.

Si quieres aplicar la plantilla manualmente, puedes hacerlo en Python:

from jinja2 import Template, Environment, exceptions

JINJA_TEMPLATE = "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}"

def raise_exception(message):
    raise ValueError(message)

def apply_jinja_template(messages, bos_token='<s>', eos_token='</s>'):
    template_str = JINJA_TEMPLATE

    env = Environment()
    env.globals['raise_exception'] = raise_exception

    template = env.from_string(template_str)

    try:
        result = template.render(messages=messages, bos_token=bos_token, eos_token=eos_token)
        return result
    except exceptions.TemplateError as e:
        raise ValueError(f"Template rendering error: {e}")

print(apply_jinja_template(messages))

FIM
Para FIM, la plantilla sería la siguiente:

<s>[SUFFIX]suffix[PREFIX] prefix

Lógica de Tokenización de Instrucciones

Ahora que tenemos tokens de control, el proceso de tokenización ha cambiado ligeramente. Aquí está la nueva lógica detrás de la plantilla de chat:

  • Tokenizar cada mensaje:
    Tienes mensajes de usuario y mensajes de asistente. Ahora los tomas por separado como cadenas individuales y los codificas:
    encode(user_message), encode(assistant_message), y encode(new_user_message), y así sucesivamente...

  • Concatenar:
    Una vez que cada mensaje es tokenizado y codificado, los concatenarías todos, anteponiéndoles el ID de BOS y separándolos por pares con el ID de EOS mientras encapsulas los mensajes de usuario con los tokens de control, específicamente el ID [INST] y el ID [/INST]:
    BOS_ID + INST_ID + encode("user message") + /INST_ID + encode("assistant message") + EOS_ID + INST_ID + encode("new user message") + /INST_ID

Siguiendo esto, la cadena sería algo así:

<s>[INST]user message[/INST]assistant message</s>[INST]new user message[/INST]

Sin embargo, una vez más, esto omite algunos espacios en blanco, esta vez después de los tokens de control. Vienen del mismo lugar que antes, de sentencepiece:

  • encode("user message") en realidad está codificando encode(" user message")
  • encode("assistant message") en realidad está codificando encode(" assistant message")
  • encode("new user message") en realidad está codificando encode(" new user message")

Esto añade los espacios en blanco que faltan, haciendo que la cadena final se vea más como:

<s>[INST]_user message[/INST]_assistant message</s>[INST]_new user message[/INST]

"_" representa los nuevos espacios en blanco añadidos por sentencepiece

Finalmente, token por token dará lo siguiente:

<s> [INST] _user _message [/INST] _assistant _message </s> [INST] _new _user _message [/INST]
1 3 2956 3696 4 14660 3696 2 3 1401 2956 3696 4
"_" representa cualquier espacio en blanco

Prompt del Sistema

La segunda versión del tokenizer implementa un enfoque ligeramente diferente para el prompt del sistema. Por defecto, lo antepone al último mensaje del usuario:

<s>[INST] user message[/INST] assistant message</s>[INST] system prompt

new user message[/INST]

⚠️ Así es como mistral_common y las plantillas implementan los prompts del sistema, pero esto se puede personalizar fácilmente. Siéntete libre de usar los prompts del sistema en diferentes lugares, como el penúltimo o simplemente como el primer mensaje del usuario, como antes.

Tokenizer V3

Este tokenizer impulsa modelos como Mixtral 8x22B, Codestral 22B, Mathstral 7B, Mamba Codestral 7B, Small 2409 y Large 2 (Large 2407). Es muy similar a la segunda versión, con solo el uso de herramientas siendo ligeramente diferente.

La plantilla de chat, la tokenización y el prompt del sistema para la instrucción básica son los mismos que los anteriores.

Tekken

Tekken es una versión diferente del tokenizer V3 y potencia Mistral Nemo 12B y Pixtral 12B. Mientras que el original y los tokenizers anteriores se basaban en sentencepiece, Tekken se basa en tiktoken. Con un tamaño de vocabulario considerablemente mayor, también maneja la codificación de manera diferente. La principal diferencia para la plantilla de chat es que no antepone un espacio en blanco como sentencepiece.

Esto resulta en una plantilla de chat más simple y una tokenización más intuitiva.

Plantilla de Chat de Instrucción de Tekken

Con aún menos espacios en blanco intrusivos, la nueva plantilla es tan simple como la siguiente:

<s>[INST]user message[/INST]assistant message</s>[INST]new user message[/INST]

Para comparar, aquí está la plantilla de chat del tokenizer anterior:

<s>[INST] user message[/INST] assistant message</s>[INST] new user message[/INST]

La plantilla Jinja para el transformers de HuggingFace para este tipo de cadena podría verse así:

{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}

Formateado para que sea más fácil de leer:

{{ bos_token }}
{% for message in messages %}
    {% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}
        {{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}
    {% endif %}
    {% if message['role'] == 'user' %}
        {{ '[INST]' + message['content'] + '[/INST]' }}
    {% elif message['role'] == 'assistant' %}
        {{ message['content'] + eos_token }}
    {% else %}
        {{ raise_exception('Only user and assistant roles are supported!') }}
    {% endif %}
{% endfor %}

Si quieres aplicar la plantilla manualmente, puedes hacerlo en Python:

from jinja2 import Template, Environment, exceptions

JINJA_TEMPLATE = "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST]' + message['content'] + '[/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}"

def raise_exception(message):
    raise ValueError(message)

def apply_jinja_template(messages, bos_token='<s>', eos_token='</s>'):
    template_str = JINJA_TEMPLATE

    env = Environment()
    env.globals['raise_exception'] = raise_exception

    template = env.from_string(template_str)

    try:
        result = template.render(messages=messages, bos_token=bos_token, eos_token=eos_token)
        return result
    except exceptions.TemplateError as e:
        raise ValueError(f"Template rendering error: {e}")

print(apply_jinja_template(messages))

Lógica de Tokenización de Instrucciones de Tekken

La lógica para la tokenización sigue siendo la misma que la anterior, que es:

Y eso es todo. La cadena resultante es más intuitiva, sin espacios en blanco añadidos ya que tiene un comportamiento diferente al de sentencepiece, haciendo que la cadena final sea la siguiente:

<s>[INST]user message[/INST]assistant message</s>[INST]new user message[/INST]

Token por token:

<s> [INST] user _message [/INST] ass istant _message </s> [INST] new _user _message [/INST]
1 3 3263 5117 4 1503 19464 5117 2 3 3080 3330 5117 4
"_" representa cualquier espacio en blanco

Prompt del Sistema

Por defecto, el prompt del sistema se maneja de manera similar a las versiones anteriores:

<s>[INST]user message[/INST]assistant message</s>[INST]system prompt

new user message[/INST]

Conclusión

Los tokenizers han evolucionado significativamente desde V1 hasta V3 y la variante Tekken, cada iteración trayendo mejoras y cambios que a veces han llevado a debates y problemas dentro de la comunidad. Comprender los matices de estos tokenizers es crucial para optimizar el rendimiento de los modelos Mistral en diversas aplicaciones y para un ajuste fino adecuado.

Puntos Clave:

  1. Tokenizer V1:

    • Utilizaba sentencepiece y una plantilla similar a los modelos Llama.
    • Cadenas especiales [INST] y [/INST] con manejo de espacios en blanco.
    • Prompt del sistema antepuesto al primer mensaje del usuario con dos nuevas líneas.
  2. Tokenizer V2:

    • Introdujo tokens de control para [INST] y [/INST], así como otros tokens de control.
    • Plantilla de chat ligeramente ajustada debido a la ausencia de algunos espacios en blanco.
    • Prompt del sistema antepuesto al último mensaje del usuario.
  3. Tokenizer V3:

    • Similar a V2, pero con uso de herramientas mejorado.
    • Misma plantilla de chat, tokenización y enfoque de prompt del sistema que V2.
  4. Tokenizer V3 - Tekken:

    • Basado en tiktoken con un vocabulario más grande.
    • Plantilla de chat más simple sin espacios en blanco iniciales.

Mejores Prácticas:

Comunidad y Recursos:

Lección del curso «Mistral Cookbook» de Mistral AI, publicado con licencia MIT. Traducción y adaptación al español de IA con Clase. IA con Clase no está afiliado a Mistral AI. Ver el original · Licencia
Esta lección es gratuita. El resto del curso se abre con la Membresía de IA con Clase, que incluye todos los cursos del catálogo. Ver precios
← AnteriorSiguiente: Tokens de control →