总览
为了让语言变为模型能够理解的形式(tokens),每个字词必须映射为独一无二的序号,这时需要使用分词器 tokenizer 对语言进行转换。例如对于 “are you ok”,gemma 模型的 tokenizer 会将之转换为一个 List:[2, 895, 692, 4634]
。
顺便一提,第一个序号 2 是开始标记
<bos>
。
本文是学习 chat 模型微调留下的学习笔记,只记录了自认为必要的知识点。写得很乱,可能以后会整理一下。
使用 transformers 库的 Tokenizers
HuggingFace 家的 Tokenizers 提供了当下最常用的 tokenizer 实现。
为了方便学习,先实例化一个 tokenizers。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("google/gemma-2b-it")
gemma 的使用需要 token,为了方便可以换为
facebook/blenderbot-400M-distill
之类的模型。本文是以 gemma 为例。
现在,就可以通过 tokenizer.encode("are you ok")
看转换效果了。
Tokenizer 与 PreTrainedTokenizer
上面使用 AutoTokenizer.from_pretrained("google/gemma-2b-it")
获得的是 GemmaTokenizerFast
类变量,这个类继承于 transformers.PreTrainedTokenizerFast
。
可以通过 Tokenizer
对象实例化一个 PreTrainedTokenizerFast
对象。
from transformers import PreTrainedTokenizerFast
wrapped_tokenizer = PreTrainedTokenizerFast(
tokenizer_object=tokenizer,
bos_token="<|endoftext|>",
eos_token="<|endoftext|>",
)
通过 wrapped_tokenizer.save_pretrained("path")
可以将 tokenizer 的整体状态保存为三个文件:tokenizer_config.json、special_tokens_map.json 和 tokenizer.json。若要从文件加载,就使用 PreTrainedTokenizerFast.from_pretrained("path")
实例化。
tokenizers.Tokenizer 类
tokenizers 库中 Tokenizer
类能够涵盖转换 tokens 的四步骤。
要将字符串转换为 tokens,需要经过以下四步骤:
Normalization,让字符串更 “干净”,会进行像是删空格和统一大小写这样的操作。
若词表大小足够大,这一步就不需要太多处理了。对于 gemma,这一步仅将空格变为下划线。
Pre-Tokenization,将字符串分割为单词。对于英语,就是根据空格进行分词。
若 Model 步骤不使用 WordLevel 方法(现在一般都不用 WordLevel 了),则 Pre-Tokenization 步骤不是必须的。
Model,通过字典将字符映射为序号。是 Tokenizer 的核心。
Post-Processing,添加 CLS
SEP
等标记,便于让模型知晓何字符串时开始何时结束。
Tokenizer
对象是 GemmaTokenizerFast
的一部分。对于本文一开始生成的 tokenizer
,可以通过 tokenizer._tokenizer
获得其包含的 Tokenizer
对象。
Model 步骤
Model 步骤是 Tokenizer 的核心,是 Tokenizer 的唯一必需过程。
Model 通过字典将字符映射为序号。至于如何配合字典将字符串分段,有着不同的算法:
- WordLevel,最为经典的分段方法,直接将单词进行字典映射。这会导致像是
happy
happier
happiest
一类词被映射到完全不同的 ID,显著增加字典所需大小 - BPE(Byte Pair Encoding),整个单词被拆分为字节,并根据实际将常见的字节组合构成词典条目。如此,便可像搭积木一般构造单词,所需字典词汇表更小
- WordPiece,与 BPE 算法很像,目标都是希望像搭积木一般构造单词。但 WordPiece 不是先拆分为字节,而是先分为长句,若字典中不含该条目再进一步拆分
- Unigram,其输出的子词分段能够带概率。比起 BPE,Unigram 将概率纳入考虑,选择可能性最大的那一个分词方法
WordLevel 现在一般不会用了。gemma 使用的是 BPE。
本节参考:
- https://huggingface.co/docs/tokenizers/v0.13.4.rc2/en/components#models
- Luke,“深入理解NLP Subword算法:BPE、WordPiece、ULM”,https://zhuanlan.zhihu.com/p/86965595
对话模板
chat 模型本质上是下一词预测(next-token prediction)模型,但它遵循了一个对话模板。具体来说,差不多就是输入字符串 问: 1+1等于几?答:
然后让模型补全剩下的字符。
写成 json,这段对话应当长成这样:
[
{
"role": "user",
"content": "1+1等于几?",
},
{
"role": "model",
"content": "2",
},
]
能够通过 tokenizer 的对话模板将 json 转换为模型输入。
对话模板本质是一段以字符串形式存储的脚本,可以通过 tokenizer.chat_template
得到。gemma 的对话模板的脚本框架是这样:
{{ 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'] == 'assistant') %}
{% set role = 'model' %}{% else %}{% set role = message['role'] %}
{% endif %}
{{ '<start_of_turn>' + role + '\n' + message['content'] | trim + '<end_of_turn>\n' }}
{% endfor %}
{% if add_generation_prompt %}
{{'<start_of_turn>model\n'}}
{% endif %}
tokenizer.chat_template
返回的脚本被压成了一行,需要手动分段才能得到上面这样有较好可读性的形式。
脚本很易读。大致是先放一个 bos_token
,然后遍历每段对话进行处理。最后选择是否添加 <start_of_turn>model\n
来引导模型生成回答(而不是续写问题)。
调用
tokenizer.apply_chat_template()
时,可以传入add_generation_prompt=True
使得该脚本最后的判断为真。
准本好 json 对话 chat
,就可以使用 tokenizer.apply_chat_template(chat, tokenize=False)
看一下模板生成效果。
tokenizer.apply_chat_template(chat, tokenize=False)
# <bos><start_of_turn>user\n1+1等于几?<end_of_turn>\n<start_of_turn>model\n2<end_of_turn>\n
参考来源
- https://huggingface.co/docs/tokenizers/quicktour
- https://huggingface.co/docs/transformers/main/chat_templating#advanced-template-writing-tips
- https://www.huaxiaozhuan.com/工具/huggingface_transformer/chapters/1_tokenizer.html