背景与基础
目前的机器学习模型都是数学模型,其对应的输入要求必须是数字形式(number)的,而我们处理的真实场景往往会包含许多非数字形式的输入(有时候即使原始输入是数字形式,我们也需要转换),最典型的就是NLP 中的文字(string),为了让文字能够作为输入参与到模型的计算中去,我们就需要构建一个映射关系(mapping):将对应的文字映射到一个数字形式上去,而其对应的数字就是token。而对应的这个映射关系,就是我们的tokenizer:他可以将文字映射到其对应的数字上去(encode),也可以将数字映射回对应的文字上(decode)。
通常情况下,Tokenizer有三种粒度:word/char/subword
- word: 按照词进行分词,如:Today is sunday. 则根据空格或标点进行分割[today, is, sunday, .]
- character:按照单字符进行分词,就是以char为最小粒度。 如:Today is sunday.则会分割成[t, o, d,a,y, …. ,s,u,n,d,a,y, .]
- subword:按照词的subword进行分词。如:Today is sunday.则会分割成[to, day,is , s,un,day, .]
Word Level Tokenizer
word/词,是最自然的语言单元。对于英文等自然语言来说,存在着天然的分隔符,如空格或一些标点符号等,对词的切分相对容易。但是对于一些东亚文字包括中文来说,就需要某种分词算法才行。对于中文,词到底分成什么粒度,“苹果手机” 到底是一个词还是二个词呢?“武汉市/长江/大桥/欢迎/你” 与“武汉/市长/江大桥/欢迎/你” 应该选择哪个方案呢?此外,对于“因吹斯汀”这些新词怎么识别呢?
除了如何切分成词外,还有一个最大的问题是结果集太大。比如TransformerXL 中,使用标点和空格,切分后的词表大小有267K,如此大的词表,不管是对存储还是计算都有压力。
基于词的切分,会造成:
- 词表规模过大
- 一定会存在UNK(在自然语言处理中,UNK通常表示“未知”或“未登录”等意思。当在训练数据中遇到一个未知的词时,就会用UNK来代替这个未知的词。在基于词的切分中,由于词表规模过大或一些生僻的词没有出现在词表中,或者一些单词形式变化(如单数变成复数)没有在词表中,因此会出现UNK,造成信息丢失。),造成信息丢失
- 不能学习到词缀之间的关系,例如:dog与dogs,happy与unhappy
Char Level Tokenizer
既然分词这么麻烦,我不分不好了,我就按“字”(char) 来做最小单元做映射。这样词表就小多了:英文只需要26个字母即可,中文根据2013年中华人民共和国教育部《通用规范汉字表》定义“规范汉字”,国家规定的通用规范汉字一共为8105个,相比之下也不算大。
然而char level 的主要问题是切分的太细:pneumonoultramicroscopicsilicovolcanoconiosis这个单词是我找到的最长的英语单词,他有45个字母组成,而中文中也存在大量的成语/歇后语/专用名词等,如:只许州官放火不许百姓点灯。
目前我们NLP 的主要思路是对句子进行,即:
$$P(S)=P(w_1,w_2,..w_n)=P(w_1)∗P(w_2|w_1)∗P(w_3|w_1,w_2)∗…∗P(w_n|w_1,w_2,..w_{n-1})$$
切分太细,则对应S的长度会变长,无疑大大增加建模难度(通过字来学习词的语义),也常常导致模型效果不理想。
对于character粒度分词:
- 优点:词表极小,比如:26个英文字母几乎可以组合出所有词,5000多个中文常用字基本也能组合出足够的词汇
- 缺点:
- 每个token的信息密度低,无法承载丰富的语义,英文中尤为明显,但中文却是较为合理,中文中用此种方式较多。
- 序列长度大幅增长,解码效率很低
Subword Level Tokenizer
所以基于词和基于字的切分方式是两个极端,其优缺点也是互补的。而折中的subword就是一种相对平衡的方案。
subword/子词级,它介于字符和单词之间。比如说’Transformers’可能会被分成’Transform’和’ers’两个部分。这个方案平衡了词汇量和语义独立性,是相对较优的方案。
subword的基本切分原则是:
- 高频词依旧切分成完整的整词
- 低频词被切分成有意义的子词,例如 dogs => [dog, ##s]
它的处理原则是,常用词应该保持原状,生僻词应该拆分成子词以共享token压缩空间。
基于subword的切分可以实现:
- 词表规模适中,解码效率较高
- 不存在UNK,信息不丢失
- 能学习到词缀之间的关系
常见的子词算法有Byte-Pair Encoding (BPE) / Byte-level BPE(BBPE)、Unigram LM、WordPiece、SentencePiece等。
- BPE:即字节对编码。其核心思想是从字母开始,不断找词频最高、且连续的两个token合并,直到达到目标词数。
- BBPE:BBPE核心思想将BPE的从字符级别扩展到子节(Byte)级别。BPE的一个问题是如果遇到了unicode编码,基本字符集可能会很大。BBPE就是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256(2^8)。采用BBPE的好处是可以跨语言共用词表,显著压缩词表的大小。而坏处就是,对于类似中文这样的语言,一段文字的序列长度会显著增长。因此,BBPE based模型可能比BPE based模型表现的更好。然而,BBPE sequence比起BPE来说略长,这也导致了更长的训练/推理时间。BBPE其实与BPE在实现上并无大的不同,只不过基础词表使用256的字节集。
- WordPiece:WordPiece算法可以看作是BPE的变种。不同的是,WordPiece基于概率生成新的subword而不是下一最高频字节对。WordPiece算法也是每次从词表中选出两个子词合并成新的子词。BPE选择频数最高的相邻子词合并,而WordPiece选择使得语言模型概率最大的相邻子词加入词表。
- Unigram:它和 BPE 以及 WordPiece 从表面上看一个大的不同是,前两者都是初始化一个小词表,然后一个个增加到限定的词汇量,而 Unigram Language Model 却是先初始一个大词表,接着通过语言模型评估不断减少词表,直到限定词汇量。
- SentencePiece:SentencePiece它是谷歌推出的子词开源工具包,它是把一个句子看作一个整体,再拆成片段,而没有保留天然的词语的概念。一般地,它把空格也当作一种特殊字符来处理,再用BPE或者Unigram算法来构造词汇表。SentencePiece除了集成了BPE、ULM子词算法之外,SentencePiece还能支持字符和词级别的分词。
下图是一些主流模型使用的分词算法,比如:GPT-1 使用的BPE实现分词,LLaMA/BLOOM/GPT2/ChatGLM使用BBPE实现分词。BERT/DistilBERT/Electra使用WordPiece进行分词,XLNet则采用了SentencePiece进行分词。
常见Subword子词算法
Byte-Pair Encoding (BPE)
Byte-Pair Encoding (BPE) 是一种数据压缩技术,后来被引入到自然语言处理中作为一种子词级别的切分方法。它的核心思想是将常见的字节对(或字符对)替换为单个、未使用的字节(或字符),从而减少数据中的总字节数。在自然语言处理中,BPE 通过合并频繁出现的字符对来创建新的词汇,逐步建立起一个子词的词表。
BPE算法的步骤通常如下:
- 准备一份足够大的文本数据集,并确定词表的大小。
- 将文本数据集中的词汇切分为基础字符(如字母、标点符号等),并为每个基础字符后加上终止符,以表示词的边界。
- 统计所有相邻字符对的频率,并找出出现次数最多的字符对。
- 将最频繁的字符对合并,创建一个新的符号,并且将文本中的这些字符对替换为这个新符号。
- 重复步骤3和步骤4,直到达到预定的词表大小或者没有可以合并的字符对为止。
使用BPE有助于缓解词表大小膨胀的问题,并且能够更好地处理罕见词和外来词。通过分解为子词,BPE 可以将词汇的形态变化(如复数、时态变化等)和词缀(如前缀、后缀)进行有效的编码,从而使模型能够利用这些信息来更好地理解和生成文本。
在现代自然语言处理模型中,比如GPT、BERT等,BPE及其变体(如SentencePiece)被广泛使用作为词表构建和文本预处理的重要方法。
如aaabdaaabac这个序列,首先a频率是最高的,其次是aa,这是用Z替换aa,然后两个字符连在一起频率最高的是ab,因而用Y替换ab,得到ZYdZYac,可以依次类推,这样将第一行的原始序列压缩为了最后一样的序列。在信号压缩领域中BPE过程可视化如下:
要使用Byte-Pair Encoding (BPE)算法,你可以使用 Hugging Face 的 tokenizers 库。以下是如何训练BPE tokenizer的示例:
你可以使用以下代码训练一个BPE tokenizer:
from tokenizers import Tokenizer from tokenizers.models import BPE from tokenizers.trainers import BpeTrainer from tokenizers.pre_tokenizers import Whitespace # 创建一个基本的BPE模型 tokenizer = Tokenizer(BPE(unk_token="[UNK]")) # 使用 Whitespace作为预处理器 tokenizer.pre_tokenizer = Whitespace() # 创建一个 BPE trainer,设置vocab_size为5000 trainer = BpeTrainer(vocab_size=5000, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]) # 指定文件路径列表来训练tokenizer files = ["your_text_file_1.txt", "your_text_file_2.txt"] tokenizer.train(files, trainer) # 保存tokenizer到文件 tokenizer.save("bpe.tokenizer.json")
Byte-level BPE (BBPE)
Byte-level BPE (BBPE),即字节级别的Byte-Pair Encoding,是一种特殊的词切分方法,主要用于自然语言处理任务中。它的出现主要是为了解决传统BPE或者WordPiece等方法中的一些问题,如未登录词和词表规模过大等。
BBPE最初由OpenAI在GPT-2模型中提出并使用。不同于常规的基于字符的BPE,BBPE是直接在字节层面进行操作。它首先将文本转换为UTF-8编码,然后在这些编码上应用BPE算法。由于所有的字符都可以转换成字节序列,所以BBPE的词表中可以包含所有可能的单字节符号,这样就确保了模型不会出现任何未登录词,同时也可以有效地控制词表的规模。
BBPE的主要优点在于,它可以实现真正的无UNK处理,同时也能较好地处理多语言文本。因为它基于字节,所以可以很好地处理任何UTF-8编码的文本,无论这些文本是哪种语言,都不需要做任何特殊处理。
BBPE的核心思想是用byte来构建最基础的词表而不是字符。首先将文本按照UTF-8进行编码,每个字符在UTF-8的表示中占据1-4个byte。在byte序列上再使用BPE算法,进行byte level的相邻合并。编码形式如下图所示:
通过这种方式可以更好的处理跨语言和不常见字符的特殊问题(例如,颜文字),相比传统的BPE更节省词表空间(同等词表大小效果更好),每个token也能获得更充分的训练。
但是在解码阶段,一个byte序列可能解码后不是一个合法的字符序列,这里需要采用动态规划的算法进行解码,使其能解码出尽可能多的合法字符。具体算法如下:
假定$f(k)$表示字符序列$ B_{1,k} $最大能解码的合法字符数量,$f(k)$有最优的子结构:
$$f(k) = max_{t=1,2,3,4}{f(k-t) + g(k-t+1, k)}$$
这里如果$B_{i,j}$为一个合法字符$ g(i,j) = 1 $,否则$g(i,j) = 0$。
要在Python中使用BBPE,你可以使用 Hugging Face 的 tokenizers 库。以下是一个例子,展示如何使用这个库训练一个BBPE tokenizer:
你可以使用以下代码训练一个BBPE tokenizer:
from tokenizers import Tokenizer from tokenizers.models import BPE from tokenizers.trainers import BpeTrainer from tokenizers.pre_tokenizers import ByteLevel # 创建一个 Byte-level BPE 模型 tokenizer = Tokenizer(BPE()) # 使用 Byte-level pre-tokenizer tokenizer.pre_tokenizer = ByteLevel() # 创建一个 BPE trainer trainer = BpeTrainer(vocab_size=30000, min_frequency=2) # 训练 tokenizer files = ["your_text_file_1.txt", "your_text_file_2.txt"] tokenizer.train(files, trainer) # 现在你可以使用这个 tokenizer 来做词条化和逆词条化 output = tokenizer.encode("Hello, world!") print(output.tokens) # 你也可以将这个 tokenizer 保存为一个文件,以便以后使用 tokenizer.save("byte-level-bpe.tokenizer.json")
请注意,你需要在上述代码中替换 files 变量的值,使其包含你的训练数据文件路径列表。同时,你也可以根据需要调整 BpeTrainer 中的 vocab_size 和 min_frequency 参数。
WordPiece
WordPiece 是一种子词切分的方法,它和 BPE (Byte-Pair Encoding) 很相似,都是一种处理未登录词和减小词表规模的方法。它被 Google 在他们的一些重要模型中使用,比如 Transformer 和 BERT。
WordPiece 的操作步骤通常是这样的:
- 初始化词表,一般包含所有的基础字符。
- 逐步添加新的词块到词表中。每次选择可以最大化数据集似然度的词块添加到词表。
- 重复第二步,直到词表达到设定的大小。
WordPiece 在对新词块进行选择的时候,不仅考虑了词块在数据集中出现的频次,还会考虑词块的长度以及它构成的词的频次。这样可以确保词表中既有高频的短词块,也有低频的长词块。
在实际应用中,为了解决某些语言(如德语和土耳其语)中的子词连接问题,WordPiece 还引入了一个特殊的前缀“##”来标记一个词块是否出现在单词的最开始。例如,单词 “unhappiness” 会被切分为 [“un”, “##happy”, “##ness”]。
总的来说,WordPiece 切分方法有效地解决了未登录词问题,减小了词表规模,且能够灵活地处理不同语言中的词形变化和复合词。
WordPiece选取子词的方法如下,假设句子$S=(t_1,t_2,\cdots, t_n)$由n个子词组成,$t_i$表示子词,且假设各个子词之间是独立存在的,则句子S的语言模型似然值等价于所有子词概率的乘积:
$$ \log P(s) = \sum_{i=1}^N \log P(t_i)$$
设把相邻位置的x和y两个子词进行合并,合并后产生的子词记为z,此时句子S似然值的变化可表示为:
$$\log P(t_z) -(log P(t_x)+log P(t_y))= \log(\frac{ P(t_z) }{P(t_x)P(t_y)})$$
似然值的变化就是两个子词之间的互信息。简而言之,WordPiece每次选择合并的两个子词,他们具有最大的互信息值,也就是两子词在语言模型上具有较强的关联性,它们经常在语料中以相邻方式同时出现。
使用以下代码来训练一个WordPiece tokenizer:
from tokenizers import BertWordPieceTokenizer # 初始化tokenizer tokenizer = BertWordPieceTokenizer( clean_text=True, # 清理文本,去除一些特殊字符 handle_chinese_chars=True, # 处理中文字符 strip_accents=True, # 去除重音符号(只对拉丁字母有效) lowercase=True, # 文本小写化 ) # 指定数据集路径,训练tokenizer files = ["your_text_file_1.txt", "your_text_file_2.txt"] tokenizer.train( files, vocab_size=5000, min_frequency=2, show_progress=True, special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"], limit_alphabet=1000, wordpieces_prefix="##", ) # 现在你可以使用这个tokenizer来做词条化和逆词条化 output = tokenizer.encode("Hello, world!") print(output.tokens) # 你也可以将这个tokenizer保存到文件中,以便以后使用 tokenizer.save_model("wordpiece")
Unigram Language Model (ULM)
与WordPiece一样,Unigram Language Model(ULM)同样使用语言模型来挑选子词。不同之处在于,BPE和WordPiece算法的词表大小都是从小到大变化,属于增量法。而Unigram Language Model则是减量法,即先初始化一个大词表,根据评估准则不断丢弃词表,直到满足限定条件。ULM算法考虑了句子的不同分词可能,因而能够输出带概率的多个子词分段。
对于句子S,$X=(x_1,x_2,\cdots, x_m)$为句子的一个分词结果,由m个子词组成。所以,当前分词下句子S的似然值可以表示为:
$$P(X)=\prod \limits_{i=1}^mP(x_i)$$
对于句子S,挑选似然值最大的作为分词结果,则可以表示为:
$$x^*=\arg \max_{x \in U(x)}P(X)$$
这里$U(x)$包含了句子的所有分词结果。在实际应用中,词表大小有上万个,直接罗列所有可能的分词组合不具有操作性。针对这个问题,可通过维特比算法得到$x^*$来解决。
每个字词的概率$P(x_i)$用最大期望的方法计算,假设当前词表V,则M步最大化对象是如下似然函数:
$$L=\sum_{s=1}^{|D|}\log (P(X^{(s)}))=\sum_{s=1}^{|D|}\log(\sum_{x \in U(X^{(s)})}P(x))$$
其中,|D|是语料库中语料数量。上述公式的一个直观理解是,将语料库中所有句子的所有分词组合形成的概率相加。
初始时,词表V并不存在,因而,ULM算法采用不断迭代的方法来构造词表以及求解分词概率:
- 初始时,建立一个足够大的词表,一般,可用语料中的所有字符加上常见的字符串初始化词表,也可以通过BPE初始化;
- 针对当前词表,用EM算法求解买个subword在语料上的概率;
- 对于每个字词,计算当该字词从词表中移除时,总的loss降低了多少,记为该字词的loss。
- 将字词按照loss大小进行排序,丢弃一定比例loss最小的字词(比如20%),保留下来的字词生成新的词表。这里需要注意的是,单字符不能被丢弃,这是为了避免OOV情况,
- 重复步骤2到4,直到词表大小减少到设定范围。
SentencePiece
SentencePiece 是一种开源的无监督文本词条化和分词工具,由 Google 开发。SentencePiece 是一种无监督的文本 tokenizer 和 detokenizer,主要用于基于神经网络的文本生成系统,其中,词汇量在神经网络模型训练之前就已经预先确定了。 SentencePiece 实现了subword单元(例如,字节对编码 (BPE))和 unigram 语言模型),并可以直接从原始句子训练字词模型(subword model)。 这使得我们可以制作一个不依赖于特定语言的预处理和后处理的纯粹的端到端系统。
SentencePiece包括下图蓝色的Normaliser,Encoder,Decoder以及Trainer四个部分。
- Normaliser并不是指对tokenizer之后的数字进行均值和方差归一化,在NLP中,归一化是指标准化文本中的单词,使它们遵循合适的格式。Sentence Piece采用的归一化方法是将单词/字母更改为等效的NFKC Unicode(例如以U+0026开头)。当然也可以用不同unicode方法。对于那些感兴趣的人,可以在这里找到规范化器的C++[https://github.com/google/sentencepiece/blob/master/src/normalizer.cc]实现。
- trainer使用算法根据subword建立词汇表。SentencePiece支持BPE和unigram两种语言模型。
- Encoder和Decoder分别是前处理和后处理。
图中的过程可以用SentencePiece 论文如下公式:
Decode(Encode(Normalized(text))) = Normalized(text)
这被称为无损tokenization.
SentencePiece 的特性
- 唯一Token数量是预先确定的。神经网络机器翻译模型通常使用固定的词汇表进行操作。 与大多数假设无限词汇量的无监督分词算法不同,SentencePiece 在训练分词模型时,使最终的词汇表大小固定,例如:8k、16k 或 32k。
- 从原始句子进行训练。以前的子词(sub-word)实现假设输入句子是预标记(pre-tokenized)的。 这种约束是有效训练所必需的,但由于我们必须提前运行依赖于语言的分词器,因此使预处理变得复杂。 SentencePiece 的实现速度足够快,可以从原始句子训练模型。 这对于训练中文和日文的tokenizer和detokenizer很有用,因为在这些词之间不存在明确的空格。
- 空格被视为基本符号。自然语言处理的第一步是文本 tokenization。例如,标准的英语分词器(tokenizer)将对文本Hello world进行分段。 分为[Hello] [World] [.]这三个token。 这种情况将导致原始输入和标记化(tokenized)序列不可逆转换。 例如,“World”和“.”之间没有空格的信息。空格将从标记化序列中删除,例如:Tokenize(“”) == Tokenize(“World .”)但是,SentencePiece 将输入文本视为一系列 Unicode 字符。 空格也作为普通符号处理。 为了明确地将空格作为基本标记处理,SentencePiece 首先使用元符号 “▁” (U+2581) 转义空格。由于空格保留在分段文本中,我们可以毫无歧义地对文本进行detokenize。此特性可以在不依赖特定于语言的资源的情况下执行detokenization。
- 子词正则化和 BPE-dropout。子词正则化和 BPE-dropout 是简单的正则化方法,它们实际上通过实时子词采样来增强训练数据,这有助于提高神经网络机器翻译(NMT)模型的准确性和鲁棒性。
SentencePiece 的技术优势
- 纯数据驱动:SentencePiece 从句子中训练 tokenization 和 detokenization 模型。 并不总是需要Pre-tokenization(Moses tokenizer/MeCab/KyTea) 。
- 独立于语言:SentencePiece 将句子视为 Unicode 字符序列。 没有依赖于语言的逻辑。
- 多子词算法:支持 BPE 和 unigram 语言模型。
- 子词正则化:SentencePiece 实现子词正则化和 BPE-dropout 的子词采样,有助于提高 NMT 模型的鲁棒性和准确性。
- 快速且轻量级:分割速度约为 50k 句子/秒,内存占用约为 6MB。
- Self-contained:只要使用相同的模型文件,就可以获得相同的tokenization/detokenization。
- 直接词汇 ID 生成:SentencePiece 管理词汇到 ID 的映射,可以直接从原始句子生成词汇 ID 序列。
- 基于 NFKC 的 normalization:SentencePiece 执行基于 NFKC 的文本 normalization。
SentencePiece的应用场景
随着ChatGPT迅速出圈,最近几个月开源的大模型也是遍地开花。目前,开源的大语言模型主要有三大类:
- ChatGLM衍生的大模型(wenda、ChatSQL等)
- LLaMA衍生的大模型(Alpaca、Vicuna、BELLE、Phoenix、Chimera等)
- Bloom衍生的大模型(Bloomz、BELLE、Phoenix等)
其中,ChatGLM-6B主要以中英双语进行训练,LLaMA主要以英语为主要语言的拉丁语系进行训练,而Bloom使用了46种自然语言、13种编程语言进行训练。
模型 | 训练数据量 | 模型参数 | 训练数据范围 | 词表大小 | 分词算法 | 分词器(Tokenizer)后端 |
LLaMA | 1T~1.4T tokens(其中,7B/13B使用1T,33B/65B使用1.4T) | 7B~65B | 以英语为主要语言的拉丁语系 | 32000 | BBPE | 基于SentencePiece工具实现 |
ChatGLM-6B | 约 1T tokens | 6B | 中英双语 | 130528 | BBPE | 基于SentencePiece工具实现 |
Bloom | 1.6TB预处理文本,转换为 350B 唯一 tokens | 300M~176B | 46种自然语言,13种编程语言 | 250680 | BBPE | HuggingFace 的 tokenizers (类SentencePiece) |
目前来看,在开源大模型中,LLaMA无疑是其中最闪亮的星。但是,与ChatGLM-6B和Bloom原生支持中文不同。LLaMA 原生仅支持 Latin 或 Cyrillic 语系,对于中文支持不是特别理想。原版LLaMA模型的词表大小是32K,而多语言模型(如:XLM-R、Bloom)的词表大小约为250K。以中文为例,LLaMA词表中的中文token比较少(只有几百个)。这将导致了两个问题:
- LLaMA 原生tokenizer词表中仅包含少量中文字符,在对中文字进行tokenzation时,一个中文汉字往往被切分成多个token(2-3个Token才能组合成一个汉字),显著降低编解码的效率。
- 预训练中没有出现过或者出现得很少的语言学习得不充分。
为了解决这些问题,我们可能就需要进行中文词表扩展。比如:在中文语料库上使用SentencePiec训练一个中文tokenizer模型,然后将中文 tokenizer 与 LLaMA 原生的 tokenizer 进行合并,通过组合它们的词汇表,最终获得一个合并后的 tokenizer 模型。
首先,让我们安装必要的库,包括 sentencepiece 和 transformers(Hugging Face的库,用于加载LLaMA的tokenizer)。
pip install sentencepiece transformers
接下来,我们在中文语料库上训练一个SentencePiece tokenizer模型:
import sentencepiece as spm spm.SentencePieceTrainer.train('--input=chinese_corpus.txt --model_prefix=m --vocab_size=32000')
这将生成两个文件:m.model(模型文件)和m.vocab(词汇表文件)。
然后我们需要加载LLaMA tokenizer和你刚刚创建的SentencePiece tokenizer。加载LLaMA模型的tokenizer可以使用Hugging Face的Transformers库:
from transformers import AutoTokenizer llama_tokenizer = AutoTokenizer.from_pretrained("meta-llama")
加载我们的SentencePiece tokenizer:
sp = spm.SentencePieceProcessor() sp.load('m.model')
然后,你需要将这两个tokenizer的词汇表进行合并。然而,这部分有些棘手,因为不同的tokenizer可能使用不同的标记(token)策略和词汇表大小,可能需要自定义函数进行合并。以下提供一个简单的示例,主要是将新词汇添加到LLaMA tokenizer的词汇表中,并重新分配词条ID:
# 获取LLaMA tokenizer的词汇表 llama_vocab = llama_tokenizer.get_vocab() # 获取SentencePiece tokenizer的词汇表 sp_vocab = {sp.id_to_piece(id): id for id in range(sp.get_piece_size())} # 将SentencePiece的词汇表添加到LLaMA的词汇表中 merged_vocab = {**llama_vocab, **sp_vocab} # 将LLaMA tokenizer的词汇表更新为新词汇表 llama_tokenizer.vocab = merged_vocab llama_tokenizer.ids_to_tokens = {id: token for token, id in merged_vocab.items()}
这里需要注意的是,这个简单的示例假设两个词汇表没有重叠。然而实际情况中,你可能需要处理词汇表重叠和词条ID冲突的问题。具体的处理方式可能依赖于你的应用需求和具体的合并策略。
此外,你需要知道,合并后的tokenizer在使用LLaMA模型进行预测时可能会出现问题,因为模型是在原始的词汇表上进行训练的。为了解决这个问题,你可能需要在合并的词汇表上对模型进行进一步的微调或预训练。