以英文文本 Today is sunday为例,切分结果如下:
word:按照词进行分词,可根据空格或标点进行切分。如: Today is sunday. 切分成[today, is, sunday, .]
char:按照单字符进行分词,就是以char为最小粒度。如:Today is sunday. 切分成[t,o,d,a,y, ... ,s,u,n,d,a,y, .]
subword:按照词的subword进行分词,将word拆分为子串。如:Today is sunday. 切分成[to, day,is , s,un,day, .]3.Tokenize的各粒度的优缺对比:
4.基于subword的切分是目前主流切分方式。subword的切分包括: BPE(/BBPE), WordPiece 和 ULM三种分词模型。WordPiece是种特殊的BPE。
a.使用 BPE模型【主流】:GPT, GPT-2, RoBERTa, BART, DeBERTa, LLaMA, ChatGLM, Baichuan等。
b.使用WordPiece模型:BERT, DistilBERT, MobileBERT, Funnel Transformers, MPNET等。
c.使用ULM模型:AlBERT,T5,mBART,Big Bird,XLNet等。
Tonkenizer各种分词方法对比
5. Tokenizer对输入文本的分词处理流程包括:Normalization文本归一化,Pre-tokenization预分词,Model基于分词模型的切分,Postprocessor后处理。
a.Normalization:文本归一化阶段,进行常规清理例如删除多余换行、空格、转小写、以及删除重音符号等。
b.Pre-tokenization:预分词阶段,会把句子切分成更小的“词”单元。可以基于空格或者标点进行切分。
c.Model:基于分词模型的切分阶段,使用如BPE分词模型执行分词从而生成token序列。
d.Postprocessor:后处理阶段,针对具体的任务插入special token,以及生成attention mask等。
抱抱脸文档:
https://huggingface.co/docs/tokenizers/api/normalizers
a.基于词word的切分会造成:1.无法很好的处理未知或罕见的词汇[OOV问题]。2.一定会存在UNK,造成信息丢失。3. 不利于模型学习词缀之间的关系,例如:dog与dogs,happy与unhappy。4.词表中的低频词/稀疏词在模型训练过程中无法得到充分训练,进而模型不能充分理解这些词的语义。
b.基于字符char的切分会造成:1.每个token的粒度太细,丢失了词的语义信息。2.导致模型输入序列过长,解码效率很低,使得模型的训练更加复杂难以收敛。
c.基于subword的切分可以实现:1.词表规模适中,解码效率较高。不存在UNK,信息不丢失。2.能学习到词缀之间的关系。能够较好的平衡OOV问题。
subword的基本切分原则是:1.高频词依旧切分成完整的整词。2.低频词被切分成有意义的子词,例如 dogs =>[dog, ##s]。因而subword方法能够大大降低词典的大小,同时对相近词能更好地处理。
importtorch
fromdatasetsimportload_dataset
fromtransformersimport(
AutoModelForCausalLM,AutoTokenizer,
BitsAndBytesConfig,HfArgumentParser,
TrainingArguments,pipeline,logging,
)
frompeftimportLoraConfig,PeftModel
fromtrlimportSFTTrainer
base_model="Llama2-7b-chat-hf"#基础模型路径
guanaco_dataset="guanaco-llama2-1k"#数据集路径
new_model="llama-2-7b-chat-guanaco"#微调模型名称
dataset=load_dataset(guanaco_dataset,split="train")#加载微调数据集
tokenizer=AutoTokenizer.from_pretrained(base_model,trust_remote_code=True)#加载tokenizer
tokenizer.pad_token=tokenizer.eos_token#序列结束的标记eos_token默认是[SEP]
tokenizer.padding_side="right"#padding_side设置为right以修复fp16的问题
...
#完整微调代码:https://github.com/zzzichen277/LLM_SFT
#kaggle link:https://www.kaggle.com/code/zichen998/chatglm3-6b-tokenizer
fromtransformersimportAutoTokenizer,AutoModel
model_path="/kaggle/input/chatglm3-6b/pytorch/6b/6"
tokenizer=AutoTokenizer.from_pretrained(model_path,trust_remote_code=True)
model=AutoModel.from_pretrained(model_path,trust_remote_code=True,device='cuda')
model=model.eval()
text="你好,我是人工智能助手。"
print(f"1.用户的提问:{text}")
seg_words=tokenizer.tokenize(text)
print(f"2.将用户的提问分词成token结果:{seg_words}")
seg_word_ids=tokenizer.convert_tokens_to_ids(seg_words)
print(f"3.将用户的提问分词成token编码ids结果:{seg_word_ids}")
model_inputs=tokenizer([text],return_tensors="pt").to("cuda")
print(f"4.将用户的提问tokenizer后结果:{model_inputs}")
print("###############################################################")
generated_ids=model.generate(model_inputs.input_ids,max_new_tokens=512)
print(f"5.模型返回提问结果回答token编码ids结果:{generated_ids}")
generated_seg_word=tokenizer.convert_ids_to_tokens(generated_ids[0])
print(f"6.将模型回复编码ids 反编码为token结果:{generated_seg_word}")
response=tokenizer.batch_decode(generated_ids,skip_special_tokens=True)
print(f"7.将模型回复反编码ids 反编码为token并合并结果:{response}")
"""
1.用户的提问:你好,我是人工智能助手。
2.将用户的提问分词成token结果:['▁你', '好', ',', '我是', '人工智能', '助手', '。']
3.将用户的提问分词成token编码ids结果:[36474, 54591, 31123, 33030, 34797, 42481, 31155]
4.将用户的提问tokenizer后结果:{'input_ids': tensor([[64790, 64792, 36474, 54591, 31123, 33030, 34797, 42481, 31155]],
device='cuda:0'),'attention_mask':tensor([[1,1,1,1,1,1,1,1,1]],device='cuda:0'),'position_ids':tensor([[0,1,2,3,4,5,6,7,8]],device='cuda:0')}
###############################################################
5.模型返回提问结果回答token编码ids结果:tensor([[64790, 64792, 36474, 54591, 31123, 33030, 34797, 42481, 31155, 48895,
38549,31645,31404,42693,33277,31639,40648,55268,55353,36295,
31514,2]],device='cuda:0')
6.将模型回复编码ids 反编码为token结果:['[gMASK]', 'sop', '▁你', '好', ',', '我是', '人工智能', '助手', '。', '很高兴', '为您', '服务', '!', '请问', '有什么', '问题', '我可以', '帮', '您', '解答', '?', '']
7.将模型回复反编码ids 反编码为token并合并结果:['[gMASK] sop 你好,我是人工智能助手。很高兴为您服务!请问有什么问题我可以帮您解答?']
"""
importtorchfromdatasetsimportload_dataset
fromtokenizersimport(decoders,models,normalizers,pre_tokenizers,processors,trainers,Tokenizer,)
fromtransformersimportPreTrainedTokenizerFast
dataset_path="TurboPascal/tokenizers_example_zh_en"
tokenize_path="BPE_tokenizer.json"
#加载训练数据集
dataset=load_dataset(data_files=dataset_path,cache_dir='./cache/')
defbatch_iterator(batch_size=10000):
foriinrange(0,len(dataset),batch_size):
yielddataset['train'][i:i+batch_size]["text"]
special_tokens=["[CLS]","[SEP]","[PAD]","[MASK]","<s>","</s>","<t>","</t>"]
trainer=trainers.BpeTrainer(special_tokens=special_tokens,vocab_size=54000)
#创建BPEtokenizer对象
tokenizer=Tokenizer(models.BPE())
tokenizer.train_from_iterator(batch_iterator(),trainer=trainer,length=len(dataset['train']))
#保存trainedtokenizer
tokenizer.save(tokenize_path)
#加载trained tokenizer
tokenizer=Tokenizer.from_file(tokenize_path)
output=tokenizer.encode(samplexxx)
print(output.tokens)
BPE的优点:1)能够解决OOV问题;2)减少词汇表大小;3)具有一定的泛化能力;4)可以很有效地平衡词典大小和编码步骤数[将语料编码所需要的token数量]。BPE缺点:1)是基于统计的分词算法,对语料依赖性很强,如果语料规模很小,则效果一般不佳;
BPE是主流采用的subword分词器。经典模型(如GPT、GPT-2、RoBERTa、LLaMA、ChatGLM-6B, Baichuan等)使用BPE作为分词器。
1.初始化词典:将每个字符视为一个初始的词。
2.统计词频:对于每个词,统计其在文本中的频率。
3.合并频率最高的词对:在每次迭代中,选择频率最高的词对进行合并。合并的方式是将两个词连接起来。
4.更新词频:更新合并后的词频。对于合并的词,统计其在文本中的频率。
5.重复步骤3和4:重复步骤3和4,直到达到预设的词典大小或者满足其他停止条件。每次迭代都会合并频率最高的词对,并更新词频。importre,collections
defget_vocab(filename):
vocab=collections.defaultdict(int)
withopen(filename,'r',encoding='utf-8')asfhand:
forlineinfhand:
words=line.strip().split()
forwordinwords:
vocab[''.join(list(word))+'</w>']+=1
returnvocab
defget_stats(vocab):
pairs=collections.defaultdict(int)
forword,freqinvocab.items():
symbols=word.split()
foriinrange(len(symbols)-1):
pairs[symbols[i],symbols[i+1]]+=freq
returnpairs
defmerge_vocab(pair,v_in):
v_out={}
bigram=re.escape(''.join(pair))
p=re.compile(r'(?<!\S)'+bigram+r'(?!\S)')
forwordinv_in:
w_out=p.sub(''.join(pair),word)
v_out[w_out]=v_in[word]
returnv_out
defget_tokens(vocab):
tokens=collections.defaultdict(int)
forword,freqinvocab.items():
word_tokens=word.split()
fortokeninword_tokens:
tokens[token]+=freq
returntokens
vocab={'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
#GetfreebookfromGutenberg
#wgethttp://www.gutenberg.org/cache/epub/16457/pg16457.txt
#vocab=get_vocab('pg16457.txt')
print('==========')
print('TokensBeforeBPE')
tokens=get_tokens(vocab)
print('Tokens:{}'.format(tokens))
print('Numberoftokens:{}'.format(len(tokens)))
print('==========')
num_merges=5
foriinrange(num_merges):
pairs=get_stats(vocab)
ifnotpairs:
break
best=max(pairs,key=pairs.get)
vocab=merge_vocab(best,vocab)
print('Iter:{}'.format(i))
print('Bestpair:{}'.format(best))
tokens=get_tokens(vocab)
print('Tokens:{}'.format(tokens))
print('Numberoftokens:{}'.format(len(tokens)))
print('==========')
#BPE算法详解:https://www.jianshu.com/p/6415a2e9ec09 #0)给定单词序列
["the</w>","highest</w>","mountain</w>"]
# 1)在BPE构建词典中得到subword 的词表,对该subword词表按照字符个数由多到少排序。
#长度6544442
["errrr</w>","tain</w>","moun","est</w>","high","the</w>","a</w>"]
# 2)编码时,尝试将每个单词中的子字符串替换为token。
# 3)在迭代所有的tokens后,将所有子字符串替换为tokens。返回迭代结果
"the</w>"->["the</w>"]
"highest</w>"->["high","est</w>"]
"mountain</w>"->["moun","tain</w>"]#编码序列
["the</w>","high","est</w>","moun","tain</w>"]
#解码序列
"the</w>highest</w>mountain</w>"【★】BPE算法详解-简书@ Jarkata【202202】:https://www.jianshu.com/p/6415a2e9ec09
fromtokenizersimport(decoders,models,normalizers,pre_tokenizers,processors,trainers,Tokenizer,)
fromtransformersimportPreTrainedTokenizerFast
fromdatasetsimportDataset
#CreatingByte-PairEncodingtokenizer
raw_tokenizer=Tokenizer(models.BPE(unk_token="[UNK]"))#设置Tokenizer
raw_tokenizer.normalizer=normalizers.Sequence([normalizers.NFC()]+[normalizers.Lowercase()]ifLOWERCASEelse[])#normalizers
raw_tokenizer.pre_tokenizer=pre_tokenizers.ByteLevel()#pre_tokenizers
special_tokens=["[UNK]","[PAD]","[CLS]","[SEP]","[MASK]"]
trainer=trainers.BpeTrainer(vocab_size=VOCAB_SIZE,special_tokens=special_tokens)#trainers
dataset=Dataset.from_pandas(test[['text']])#Dataset
deftrain_corp_iter():
foriinrange(0,len(dataset),25):
yielddataset[i:i+25]["text"]
raw_tokenizer.train_from_iterator(train_corp_iter(),trainer=trainer)
tokenizer=PreTrainedTokenizerFast(#PreTrainedTokenizerFast
tokenizer_object=raw_tokenizer,
unk_token="[UNK]",pad_token="[PAD]",cls_token="[CLS]",sep_token="[SEP]",mask_token="[MASK]",
)
tokenized_texts_test=[]
fortextintqdm(test['text'].tolist()):
tokenized_texts_test.append(tokenizer.tokenize(text))
tokenized_texts_train=[]
fortextintqdm(train['text'].tolist()):
tokenized_texts_train.append(tokenizer.tokenize(text))
importre,collections
defget_stats(vocab):
pairs=collections.defaultdict(int)
forword,freqinvocab.items():
symbols=word.split()
foriinrange(len(symbols)-1):
pairs[symbols[i],symbols[i+1]]+=freq
returnpairs
defmerge_vocab(pair,v_in):
v_out={}
bigram=re.escape(''.join(pair))
p=re.compile(r'(?<!\S)'+bigram+r'(?!\S)')
forwordinv_in:
w_out=p.sub(''.join(pair),word)
v_out[w_out]=v_in[word]
returnv_out
vocab={'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
num_merges=1000
foriinrange(num_merges):
pairs=get_stats(vocab)
ifnotpairs:
print(f"第{i}轮合并结束")
break
best=max(pairs,key=pairs.get)
vocab=merge_vocab(best,vocab)
print(f"第{i}轮合并,best-pair值为{best},|||合并后vocab为{vocab}")
"""
第0轮合并,best-pair值为('e','s'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第1轮合并,best-pair值为('es','t'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第2轮合并,best-pair值为('est','</w>'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第3轮合并,best-pair值为('l','o'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第4轮合并,best-pair值为('lo','w'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第5轮合并,best-pair值为('n','e'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第6轮合并,best-pair值为('ne','w'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第7轮合并,best-pair值为('new','est</w>'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第8轮合并,best-pair值为('low','</w>'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第9轮合并,best-pair值为('w','i'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第10轮合并,best-pair值为('wi','d'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第11轮合并,best-pair值为('wid','est</w>'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第12轮合并,best-pair值为('low','e'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第13轮合并,best-pair值为('lowe','r'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第14轮合并,best-pair值为('lower','</w>'),|||合并后vocab为{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>':3}
第15轮合并结束
"""
【5】通过例子说明BPE构建词典
假设有语料集经过统计后表示为{'low':5,'lower':2,'newest':6,'widest':3},其中数字代表的是对应单词在语料中的频数。
输入【语料】:{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>': 3}。此时词表:{'l','o','w','e','r','n','s','t','i','d','</w>',}
Iter1,最高频连续字节对"e"和"s"出现了6+3=9次,合并成"es"。输出【新语料】:{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>': 3}。【新词表】:{'l','o','w','e','r','n','s','t','i','d','</w>','es',}。
Iter2,最高频连续字节对"es"和"t"出现了6+3=9次,合并成"est"。输出【新语料】:{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>': 3}。【新词表】:{'l','o','w','e','r','n','t','i','d','</w>','es','est',}。
Iter3,以此类推,最高频连续字节对为"est"和"</w>"。输出【新语料】:{'low</w>':5,'lower</w>':2,'newest</w>':6,'widest</w>': 3}。【新词表】:{'l','o','w','e','r','n','i','d','</w>','est','est</w>',}。
……
Iter n, 继续迭代直到达到预设的subword词表大小或下一个最高频的字节对出现频率为1。
注:停止符"</w>"的意义在于表示subword是词后缀。举例来说:"st"字词不加"</w>"可以出现在词首如"star",加了"</w>"表明该字词位于词尾,如"widest</w>",二者意义截然不同。
BPE的问题是,如果遇到了unicode,基本字符集可能会很大。一种处理方法BBPE是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256。例如,像GPT-2的词汇表大小为50257 = 256 +<EOS>+ 50000 mergers,<EOS>是句子结尾的特殊标记。
BBPE是BPE的扩展版本,BBPE核心思想将BPE的从字符级别扩展到字节级别。即字节级 BPE 将所有 Unicode 代码点转换为多个字节级字符)。LLaMA,ChatGLM...都基于BBPE实现。
| 特点 | CharacterSet|字符集 | |
| 传统BPE-字符级别BPE。 | 传统的BPE基于char粒度去执行合并的过程生成词表。 | (ASCII编码--) 传统BPE使用1个字节对字符进行编码 以字符粒度的编码 |
| BBPE-字节级别的BPE。 | BBPE是基于字节粒度 去执行合并过程生成词表。 BBPE能比较好支持语料是多种语言的分词。 | (Unicode编码-改进utf-8编码) BBPE使用1~4个字节对字符进行编码 以字节粒度的编码 |
【2】WordPiece是如何选取子词的
【3】WordPiece 与 BPE 的异同点是什么
【4】WordPiece代码实现-CreatingWordPiece Tokenizer
fromtokenizersimport(decoders,models,normalizers,pre_tokenizers,processors,trainers,Tokenizer,)
fromtransformersimportPreTrainedTokenizerFast
fromdatasetsimportDataset
#CreatingWordPiecetokenizer
raw_tokenizer=Tokenizer(models.WordPiece())#models
raw_tokenizer.normalizer=normalizers.Sequence([normalizers.NFC()]+[normalizers.Lowercase()]ifLOWERCASEelse[])#normalizers
raw_tokenizer.pre_tokenizer=pre_tokenizers.ByteLevel()#pre_tokenizers
special_tokens=["[UNK]","[PAD]","[CLS]","[SEP]","[MASK]"]
trainer=trainers.BpeTrainer(vocab_size=VOCAB_SIZE,special_tokens=special_tokens)#trainers
...推荐阅读:
【★】1.LLM 分词算法(BPE, WordPiece, Unigram)简介【202311】:https://zhuanlan.zhihu.com/p/664717335
【★】2.BPE、WordPiece和SentencePiece-简书@ Jarkata【202204】:https://www.jianshu.com/p/d4de091d1367 -
【★】3.NLP三大Subword模型详解:BPE、WordPiece、ULM【202008】:https://zhuanlan.zhihu.com/p/191648421
【1】简要介绍ULM
与BPE和WordPiece不同,Unigram算法是从预分词器分的词+所有高频的子词构成的大词汇表出发,再逐步删除其中重要性较低的subword,直到满足预定义size。
Unigram语言模型通过计算删除不同subword造成的损失loss 来衡量subword的重要性,删除loss较小或者说重要性较低的subword。每次从词汇表中删除词汇的原则是使预定义的损失loss最小。
训练时,计算loss的公式为:
。
Unigram算法每次会从词汇表中挑出使得loss增长最小的10%~20%的词汇来删除。
理论基础:假设训练文档中的所有词分别为(x1,x2,x3...,xn),由n个子词组成,而每个词tokenize的方法是对应集合S(xi)。
ULM使用大量符号初始化基础词汇,并逐渐修剪每个符号以获得较小的词汇。在每个训练步骤中,计算当前词汇和unigram语言模型给定训练数据的损失。移除影响整体损失最小的符号,重复此过程直到词汇达到所需大小。Unigram保留基本字符,以便能够对任何单词进行分词。
ULM用EM算法求解每个子词subword在语料上的概率?(??)。
假设当前词表V, 语料库中语料数量是|D|,则M步最大化的对象是如下似然函数:
ULM会保留那些以较高频率出现在很多句子的分词结果中的子词,因为这些子词如果被丢弃,其损失会很大。
【★】Subword Tokenization算法-@DonngZH【202303】:https://blog.csdn.net/weixin_44750512/article/details/129435981
【★】NLP三大Subword模型详解:BPE、WordPiece、ULM-知乎@阿北【202008】:https://zhuanlan.zhihu.com/p/191648421
输入:训练语料;词表大小V;保留阈值X;输出:ULM算法得到的subword词表。
ULM算法采用不断迭代的方法来构造词表以及求解分词概率:
1.准备基础词表:初始时建立一个足够大的词表。可用语料中的所有字符+常见的子字符串初始化词表,也可以通过BPE算法初始化。
2.针对当前词表,用EM算法求解每个子词subword在语料上的概率。
3.对于每个子词,计算当该子词被从词表中移除时,总的loss降低了多少,记为该子词的loss。
4.将子词按照loss大小进行排序,丢弃一定比例loss最小的子词(比如20%),保留前X%的子词生成新的词表。注意单字符不能被丢弃,为避免OOV情况。
5.重复步骤2到4,直到词表大小减少到设定范围V或第4步的结果不再变化。
可以看出,ULM会保留那些以较高频率出现在很多句子的分词结果中的子词,因为这些子词如果被丢弃,其损失会很大。
【★】NLP三大Subword模型详解:BPE、WordPiece、ULM#Unigram Language Model (ULM)-知乎@阿北【202008】:https://zhuanlan.zhihu.com/p/191648421
【1】简要介绍SentencePiece
...
classTextTokenizer:
def__init__(self,model_path):
self.sp=spm.SentencePieceProcessor()
self.sp.Load(model_path)
self.num_tokens=self.sp.vocab_size()
defencode(self,text):
returnself.sp.EncodeAsIds(text)
defdecode(self,ids
ist[int]):
returnself.sp.DecodeIds(ids)
...
#https://huggingface.co/THUDM/chatglm-6b/blob/main/tokenization_chatglm.py#L21【1】SentencePiece特性
【1】SentencePiece技术优势
【★】转载于:大模型词表扩充必备工具SentencePiece#训练模型-知乎@吃果冻不吐果冻皮【202305】:https://zhuanlan.zhihu.com/p/630696264
【2】使用Sentencepiece训练分词器
##v1TrainaBPEModel
importsentencepieceasspm
#trainsentencepiecemodelfromourblogcorpus
spm.SentencePieceTrainer.train(input='blog_test.txt',model_prefix=bpe--vocab_size=500,user_defined_symbols=['foo','bar'])
#makessegmenterinstanceandloadstheBPEmodelfile(bpe.model)
sp_bpe=spm.SentencePieceProcessor()
sp_bpe.load('bpe.model')
####################################################################################
##v2TrainaUnigramModel
importsentencepieceasspm
#trainsentencepiecemodelfromourblogcorpus
spm.SentencePieceTrainer.train(input='blog_test.txt',model_prefix=bpe--vocab_size=500,user_defined_symbols=['foo','bar'])
#makessegmenterinstanceandloadstheBPEmodelfile(bpe.model)
sp_uni=spm.SentencePieceProcessor()
sp_uni.load('uni.model')
####################################################################################
##比较一下两个分词器结果
print("BPE:{}".format(sp_bpe.encode_as_pieces('Thisisatest')))
print("UNI:{}".format(sp_uni.encode_as_pieces('Thisisatest')))
BPE:['▁This','▁is','▁a','▁t','est']
UNI:['▁Thi','s','▁is','▁a','▁t','est']
#https://zhuanlan.zhihu.com/p/620508648ChatGLM3-6B中tokenizer.model模型是使用SentencePiece训练得到的词表模型文件。
fromsentencepieceimportSentencePieceProcessor
model_path="/kaggle/input/chatglm3-6b/pytorch/6b/6/tokenizer.model"
sp_model=SentencePieceProcessor(model_file=model_path)
text="你好,你是谁?"
tokens=sp_model.EncodeAsPieces(text)
print(f"1.句子转为tokens结果:{tokens}")
ids=sp_model.EncodeAsIds(text)
print(f"2.句子转为ids结果:{ids}")
decode_text=sp_model.Decode(ids)
print(f"3.ids转为句子结果:{decode_text}")
#1.句子转为tokens结果:['▁你', '好', ',', '你是', '谁', '?']
#2.句子转为ids结果:[36474, 54591, 31123, 34607, 55622, 31514]
#3.ids转为句子结果:你好,你是谁?使用sentencepiece训练模型:
#0.从C++源构建和安装SentencePiece命令行工具
>sudoapt-getinstallcmakebuild-essentialpkg-configlibgoogle-perftools-dev
>gitclonehttps://github.com/google/sentencepiece.git
>cdsentencepiece|mkdirbuild|cdbuild|cmake..|make-j$(nproc)|makeinstall|ldconfig-v
#1.spm_train进行模型训练spm_train--input=训练语料文件--model_prefix=输出模型名称前缀--vocab_size=训练后的词表大小--character_coverage=模型覆盖的字符数量--model_type=模型类型如bpe
>spm_train--input=/workspace/data/book/hongluomeng_clean.txt--model_prefix=/workspace/model/book/hongluomeng-tokenizer--vocab_size=4000--character_coverage=0.9995--model_type=bpe
#2.模型输出文件(词表及模型权重)
>ls-al/workspace/model/book
#3.查看词表:
>head-n20/workspace/model/book/hongluomeng-tokenizer.vocab
#4.基于命令行使用模型,将原始文本编码成句子片段(token)。
>echo"白日依山尽,黄河入海流。"|spm_encode--model=/workspace/model/book/hongluomeng-tokenizer.model#▁白日依山尽 , 黄河入海流。
>echo"白日依山尽,黄河入海流。"|spm_encode--model=/workspace/model/book/hongluomeng-tokenizer.model--output_format=id#602547033346840014733147631760351015
#将句子片段(token) id 解码为原始文本。
>echo"602547033346840014733147631760351015"|spm_decode--model=/workspace/model/book/hongluomeng-tokenizer.model--input_format=id#白日依山尽,黄河入海流。
#5.spm_export_vocab基于模型文件导出词汇表。# spm_export_vocab --model=<模型文件>--output=<输出文件>
>spm_export_vocab--model=/workspace/model/book/hongluomeng-tokenizer.model--output=/workspace/output/hongluomeng.vocabimportsentencepieceasspm
sp=spm.SentencePieceProcessor()
text="这贾雨村原系胡州人氏,也是诗书仕宦之族,因他生于末世,父母祖宗根基已尽,人口衰丧,只剩得他一身一口,在家乡无益,因进京求取功名,再整基业。"
sp.Load("hongluomeng-tokenizer.model")
print(sp.EncodeAsPieces(text))
['▁','这','贾','雨','村','原','系','胡','州','人','氏',',','也','是','诗','书','仕','宦','之','族',',','因','他','生','于','末','世',',','父','母','祖','宗','根','基','已','尽',',','人','口','衰','丧',',','只','剩','得','他','一','身','一','口',',','在','家','乡','无','益',',','因','进','京','求','取','功','名',',','再','整','基','业','。']
fromsentencepieceimportsentencepiece_model_pb2
m=sentencepiece_model_pb2.ModelProto()
withopen('chatglm-6b/ice_text.model','rb')asf:
m.ParseFromString(f.read())
print('ChatGLMtokenizer\n\n'+str(m.trainer_spec))
"""
ChatGLMtokenizer
input:"/root/train_cn_en.json"
model_prefix:"new_ice_unigram"
vocab_size:130000
character_coverage:0.9998999834060669
split_digits:true
user_defined_symbols:"<n>"
byte_fallback:true
pad_id:3
train_extremely_large_corpus:true
"""可以看到 byte_fallback: true。同样的方法,可以验证LLaMA, ChatGLM-6B, Baichuan这些大模型都是基于sentencepiece实现的BPE的分词算法,并且采用byte回退。
【★】大模型基础组件Tokenizer-SentencePiece @nghuyong【202308】:https://zhuanlan.zhihu.com/p/651430181
SentencePiece:英文分词工具,使用无监督学习的分词方法。基于BPE编码的编码方法。分词效果好,编码效率高,但需要训练模型。
Jieba:中文分词工具,使用最大似然法的方法进行分词。基于哈希编码的编码方法。Jieba的分词效果较好,并且速度较快,但需要人工制定分词规则。
Hmmseg:中文分词工具,使用隐马尔可夫模型的方法进行分词。基于哈希编码的编码方法。Hmmseg的分词效果较好,并且可以支持多种语言,但需要训练模型。
StanfordCoreNLP:自然语言处理工具包,包含分词、词性标注、句法分析等功能。使用基于规则的方法分词。基于哈希编码的编码方法。Stanford CoreNLP 的分词效果较好,并且可以支持多种语言。
Tiktoken:基于BPE算法的快速分词器,专门针对GPT-4和ChatGPT等大模型进行了优化。Tiktoken 的主要特点是分词速度比 SentencePiece 快很多,分词效果与SentencePiece相当,提供简单的 API接口,方便使用。
分词器使用建议:
推荐阅读:
【★】1.SentencePiece:https://arxiv.org/pdf/1808.06226.pdf
【★】2.Github:https://github.com/google/sentencepiece
【★】3.大模型分词:sentencepiece vs titoken-知乎@王几行【202404】:https://zhuanlan.zhihu.com/p/691609961
【★】4.大模型词表扩充必备工具SentencePiece-掘金@吃果冻【202305】:https://zhuanlan.zhihu.com/p/630696264
1.BPE构建词典从字符级词表开始,不断在语料库中找词频最高且连续的token合并加入词表,直到达到目标词数。生成大小可控的开放词汇表。
2.WordPiece分词与BPE非常类似,只是在训练阶段合并pair的策略不是pair的频率而是互信息。?????=???(?(??))−(???(?(?))+???(?(?)))=???(?(??)/?(?)?(?))。
3.与BPE和WordPiece不同,Unigram算法是从预分词器分的词+所有高频的子词构成的大词汇表出发,再逐步删除其中重要性较低的subword,直到满足预定义size。
| 欢迎光临 链载Ai (https://www.lianzai.com/) | Powered by Discuz! X3.5 |