把 prompt 藏进 tokenizer
拥抱的脸tokenizer.jsonspec 允许你在后期处理器中隐藏提示。在这篇文章中,我将向你展示它是如何运作的,以及它如何提升代币化器的可移植性。
前缀提示
提示,或者更技术上说,设置系统提示,自从OpenAI允许用户通过API调用向模型发送系统提示后,已经变得无处不在。也许不太为人所知的是,许多普通变换器是在文档前缀前缀简单提示词来训练的,而不是作为单独的消息。例如,intfloat/multilingual-e5-base请你先做前言query :查询或一般文档,以及passage :要取回文件。这些提示词本身并非模型工件的一部分;你需要知道如何使用它们,忘记使用它们会降低性能。 我觉得最后一点特别重要:忘记添加提示会降低性能,但这种下降在不做评估的情况下很难察觉。你的模型能正常工作,只是不会达到最佳状态。
sentence-transformers它很好地支持提示及其模型管理:提示词以字符串形式存储在sentence-transformers-config.json然后在文档前加上,之后整个字符串被标记化。字符串连接的一个潜在问题是分词器可以跨提示边界分词,这可能会改变结果。
不过,我认为训练模型是很常见的sentence-transformers然后通过专门的服务框架来服务它们,例如:fastembed.在这样做时,你必须记得以同样的方式在同样的提示前加上。
我展示了隐藏一个单曲在分词器中提示,使其能够存活到任何正确吸收tokenizer.json被拥抱脸代币管理器使用。这使得你可以直接在 vLLM、句子变换器、fastembed 或你选择的框架中重用你的模型,而无需考虑提示支持。我成功地用这个方法,将模型从训练转移到评估再到推理,而无需考虑提示词。
它是如何运作的?
在 Hugging Face 框架中,分词器可能包含后处理模块。该模块负责添加特殊标记,例如[CLS]以及[SEP]到令牌序列常规分词化后.虽然有几种不同的后处理器,但大多数都可以表示为TemplatePostProcessor. 顾名思义,一TemplatePostProcessor定义了一个模板,通常看起来像SPECIAL_START $A SPECIAL_END.在此格式下,$A代表已经被分牌化的序列,模板被应用到该序列上。SPECIAL_START以及SPECIAL_END是占位符,代表定义在TemplatePostProcessor(见下文)注意,因为后处理是应用的分词化后,任何特殊标记都必须入在提示。
这里有一个特殊标记数组的具体示例:
{
"SPECIAL_START": {
"id": "SPECIAL_START",
"ids": [
101
],
"tokens": [
"[CLS]"
]
},
"SPECIAL_END": {
"id": "SPECIAL_END",
"ids": [
102
],
"tokens": [
"[SEP]"
]
}
}
如你所见,特殊令牌实际上可以包含多个ID和多个令牌,这正是我们将用来插入提示词的机制。具体来说,要在分词器中插入提示词,我们将这样做:
- 向特殊令牌数组添加一个新的特殊令牌,并使用分词提示令牌.假设我们用密钥加它
PROMPT - 添加
PROMPT模板关键:SPECIAL_START $A SPECIAL_END->SPECIAL_START PROMPT $A SPECIAL_END.
就这样。现在你的分词器就有一个总是嵌入的提示词。用户完全不会在没有提示的情况下误入文档,或者输入错误提示。
有几点需要注意:
- 提示词是预先标记好的。这意味着你会永远无论你的文档是什么,都要为你的提示获得相同的ID。代币不可能“渗透”到提示词中。这可能也帮你节省了一些计算,但这可能无关紧要。
- 没有办法禁用提示以及插入特殊代币。你可以设置
add_special_tokens到False但这也会禁用普通的特殊标记。 - 你只能在一个分词器中加载一个提示词。每个不同的提示词都需要内存中的额外分词器。
骷髅代币
skeletoken新增了对提示加法的支持v0.6.0.其工作原理如下:
from skeletoken import TokenizerModel
t = TokenizerModel.from_pretrained("bert-base-uncased")
t.prompt = "query: "
encoded = t.tokenizer.encode("hello")
print(encoded.tokens)
# ['[CLS]', 'query', ':', 'hello', '[SEP]']
该提示现存储在tokenizer.json.使用它完全不需要skeletoken:
from tokenizers import Tokenizer
t.tokenizer.save("query.json")
tokenizer = Tokenizer.from_file("query.json")
print(tokenizer.encode("hello").tokens)
# ['[CLS]', 'query', ':', 'hello', '[SEP]']
注意,与普通特殊标记不同,提示词在解码后依然存在。这是因为特殊标记在解码时会被移除。
from skeletoken import TokenizerModel
t = TokenizerModel.from_pretrained("bert-base-uncased")
t.prompt = "query: "
decoded = t.tokenizer.decode(t.tokenizer.encode("hello").ids)
print(decoded)
# 'query : hello'
设置新提示词会移除旧提示,并设置为None删除提示。
from skeletoken import TokenizerModel
t = TokenizerModel.from_pretrained("bert-base-uncased")
t.prompt = "query: "
print(t.tokenizer.encode("hello").tokens)
# ['[CLS]', 'query', ':', 'hello', '[SEP]']
t.prompt = "passage: "
print(t.tokenizer.encode("hello").tokens)
# ['[CLS]', 'passage', ':', 'hello', '[SEP]']
t.prompt = None
print(t.tokenizer.encode("hello").tokens)
# ['[CLS]', 'hello', '[SEP]']
切换提示词会让分词器重新加载,因此这是否适合临时执行取决于分词器的大小。对于小型分词器(例如,bert-base-uncased),在我的机器上加载大约只需30毫秒,但对于大型多语言代币器(例如,intfloat/multilingual-e5-base),这大约需要500毫秒。
更好的策略是单独保存多个分词器。
from skeletoken import TokenizerModel
t = TokenizerModel.from_pretrained("bert-base-uncased")
t.prompt = "query: "
t_query = t.to_tokenizer()
t.prompt = "passage: "
t_passage = t.to_tokenizer()
不过我完全承认这正是我们开篇时遇到的问题。这同样要求用户和框架跟踪多个代币化器和提示。不过,这可能比把字符串存储在用户可能忘记使用的地方要好得多。
结论
那么,这样做值得吗?如果你的模型被多个下游框架消耗,是的:在推理时将提示符添加到分词器时不会产生任何成本,并且能保护你免受棘手的错误。它还能防止提示剪辑你的文档。不过,如果你需要多个提示词,或者只用一个框架,可能就不够那对你有用。对我来说,这个方法真的很有效:我可以随便把一个模型丢到某个地方,每次都能重现提示。
注释
- 就这个特定型号而言,更大的问题是这些提示实际上除了在 README 里外没有任何文档。
- 例如,如果你的提示词是“passage: ”,但分词器中包含“: the”但没有“: these”,那么字符串“passage: the”可能被分词为
["passage", ": the"],而字符串“passage: these”则被分牌为["passage", ":", " these"].这同样难以被发现。 - Hugging Face有一些专门用于BERT和RoBERTa的后期处理器,但据我所知,使用这些后处理器并没有比
TemplatePostProcessor. - 相关的是,如果在提示词中添加了特殊标记,解码时也会被移除。