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 quemessage['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"), yencode("[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á codificandoencode(" [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 quemessage['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), yencode(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á codificandoencode(" user message")encode("assistant message")en realidad está codificandoencode(" assistant message")encode("new user message")en realidad está codificandoencode(" 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 |