Tokenize粒度和TokenizeringFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【1】Tokenize有三种粒度:word/char/subword【1】Tokenize有三种粒度:word/char/subworda.Tokenize的目标:Tokenize的目标是把文本text切分成子串token,每个子串token相对有完整的语义,便于学习embedding表达和后续模型的使用。b.Tokenize的粒度:基于词[word]的切分,基于字符[char]的切分,基于子词级[subword]的切分。以英文文本 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的各粒度的优缺对比: a.基于word的分词方式优点是保留单词的边界和意义,相比于char语义表达更加充分,缺点是会导致词表变大,稀有词学不好,易出现OOV问题,无法处理单词词缀关系。b.基于char的分词方式优点是词表小,缺点是缺乏单词的语义信息,分词的结果较长,增加文本表征的成本。c.基于subword的分词方式平衡以上两种方法优缺点,可以较好的平衡词表大小和语义表达能力。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等。  HuggingFace-Tokenizer对输入文本的分词处理流程
抱抱脸文档: https://huggingface.co/docs/tokenizers/api/normalizers
https://huggingface.co/docs/tokenizers/api/pre-tokenizershttps://huggingface.co/docs/tokenizers/api/modelshttps://huggingface.co/docs/tokenizers/api/post-processors【★】BPE、WordPiece和SentencePiece#1. 背景与基础-简书@ Jarkata【202204】:https://www.jianshu.com/p/d4de091d1367【★】大模型基础组件- Tokenizer #2.切分流程-知乎@nghuyong【202308】:https://zhuanlan.zhihu.com/p/651430181【★】大模型词表构建#2.技术基础-博客园@努力生活的叶子吖【202312】:https://www.cnblogs.com/Leahy/p/17808159.html【★】优雅谈大模型:Token与分词方法-@庞德公【202406】:https://mp.weixin.qq.com/s/miiC6DEjroPLq33D6sC4BQingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【2】subword切分与word,char切分对比【2】subword切分与word,char切分对比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方法能够大大降低词典的大小,同时对相近词能更好地处理。
ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【3】Tokenizer在训练,微调模型中使用代码【3】Tokenizer在训练,微调模型中使用代码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
ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【4】模拟Tokenizer执行过程代码【4】模拟Tokenizer执行过程#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 你好,我是人工智能助手。很高兴为您服务!请问有什么问题我可以帮您解答?'] """ ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【5】训练Tokenizer代码【5】训练Tokenizer代码importtorch
fromdatasetsimportload_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【Byte Pair Encoding】ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【1】简要介绍BPEBPE是字符级别的无监督词汇编码算法。BPE构建词典是从字符级词表开始,不断在语料库中找词频最高且连续的token合并再加入词表,直到达到目标词数。
BPE的优点:1)能够解决OOV问题;2)减少词汇表大小;3)具有一定的泛化能力;4)可以很有效地平衡词典大小和编码步骤数[将语料编码所需要的token数量]。BPE缺点:1)是基于统计的分词算法,对语料依赖性很强,如果语料规模很小,则效果一般不佳; BPE是主流采用的subword分词器。经典模型(如GPT、GPT-2、RoBERTa、LLaMA、ChatGLM-6B, Baichuan等)使用BPE作为分词器。
ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【2】BPE如何构建词典【2】BPE如何构建词典BPE[字符级-字节对编码]构建词典是从字符级词表开始,不断在语料库中找词频最高且连续的token合并再加入词表,直到达到目标词数。1.初始化词典:将每个字符视为一个初始的词。 2.统计词频:对于每个词,统计其在文本中的频率。 3.合并频率最高的词对:在每次迭代中,选择频率最高的词对进行合并。合并的方式是将两个词连接起来。 4.更新词频:更新合并后的词频。对于合并的词,统计其在文本中的频率。 5.重复步骤3和4:重复步骤3和4,直到达到预设的词典大小或者满足其他停止条件。每次迭代都会合并频率最高的词对,并更新词频。 通过BPE算法,可以将文本分解为多个子词,其中一些子词可能是常见的词汇,而其他子词则是根据输入文本的特点生成的。这种方式可以更好地处理未登录词和稀有词,并提高模型对复杂词汇和短语的处理能力。【注:合并频率最高的词对是指所有相邻的tokens组成的pair里频次最高的!】 ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【2】BPE构建词典模拟代码【2】BPE构建词典模拟代码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 ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【3】BPE实现编码和解码编码:1).在BPE构建词典中得到subword的词表,对该subword词表按照字符个数由多到少排序。2).编码时,对于每个单词,遍历排好序的subword子词词表寻找是否有token是当前单词的子字符串,如果有,则该 token 是表示单词的 tokens 之一。从最长的token迭代到最短的token,尝试将每个单词中的子字符串替换为token。3).最终,在迭代所有的tokens后,将所有子字符串替换为tokens。如果仍然有子字符串没被替换但所有token都已迭代完毕,则将剩余的子词替换为特殊token,如<unk>。#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>"] 解码:如果相邻token间没有</w>中止符,则将两token直接拼接,否则两token之间添加分隔符。如果仍然有子字符串没被替换但所有 token 都已迭代完毕,则将剩余的子词替换为特殊 token,如<unk>。#编码序列 ["the</w>","high","est</w>","moun","tain</w>"] #解码序列 "the</w>highest</w>mountain</w>"
【★】BPE算法详解-简书@ Jarkata【202202】:https://www.jianshu.com/p/6415a2e9ec09 ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;letter-spacing: normal;text-align: left;background-color: rgb(255, 255, 255);border-bottom: 2px solid rgb(239, 112, 96);visibility: visible;margin-top: 8px;margin-bottom: 8px;">【4】BPE代码实现-创建BPE Tokenizer【4】BPE代码实现-创建BPE Tokenizerfromtokenizersimport(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))
【5】BPE代码实现-BPE 构建词典过程【5】BPE代码实现-BPE构建词典过程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构建词典【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>",二者意义截然不同。
【★】理解NLP最重要的编码方式(BPE)-知乎@Jinjie Ni【202304】:https://zhuanlan.zhihu.com/p/424631681 【★】BPE算法详解-简书@ Jarkata【202202】:https://www.jianshu.com/p/6415a2e9ec09【★】NLP三大Subword模型详解:BPE、WordPiece、ULM#BPE构建词典过程模拟-知乎@阿北【202008】:https://zhuanlan.zhihu.com/p/191648421
【6】BBPE和BPE的区别【6】BBPE和BPE的区别BPE的问题是,如果遇到了unicode,基本字符集可能会很大。一种处理方法BBPE是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256。例如,像GPT-2的词汇表大小为50257 = 256 +<EOS>+ 50000 mergers,<EOS>是句子结尾的特殊标记。 BBPE是BPE的扩展版本,BBPE核心思想将BPE的从字符级别扩展到字节级别。即字节级 BPE 将所有 Unicode 代码点转换为多个字节级字符)。LLaMA,ChatGLM...都基于BBPE实现。
使用BBPE有什么好处?使用Byte-level BPE主要优点是:较小的词汇表。没有未知token,不会出现OOV问题。
| 特点 | CharacterSet|字符集 | | 传统BPE-字符级别BPE。 | 传统的BPE基于char粒度去执行合并的过程生成词表。 | (ASCII编码--) 传统BPE使用1个字节对字符进行编码 以字符粒度的编码 | | BBPE-字节级别的BPE。 | BBPE是基于字节粒度 去执行合并过程生成词表。 BBPE能比较好支持语料是多种语言的分词。 | (Unicode编码-改进utf-8编码) BBPE使用1~4个字节对字符进行编码 以字节粒度的编码 | 对于英文、拉美体系的语言来说使用BPE分词足以在可接受的词表大小下解决OOV的问题。但面对中文、日文等语言时,其稀有的字符可能会不必要地占用词汇表,因此考虑使用字节级别byte-level解决不同语言进行分词时OOV的问题。【★】1.深入理解NLP Subword算法:BPE、WordPiece、ULM-知乎@Luke【201910】:https://zhuanlan.zhihu.com/p/86965595【★】2.BPE 算法原理及使用指南【深入浅出】-知乎@Suprit【202112】:https://zhuanlan.zhihu.com/p/448147465【★】3.BPE、WordPiece和SentencePiece-简书@ Jarkata【202204】:https://www.jianshu.com/p/d4de091d1367【★】3x【HugBert11】聚沙成塔:关于tokenization(词元化)的解疑释惑【202105】:https://zhuanlan.zhihu.com/p/371300063【★】4.大模型系列:BPE理论简述和实践【202405】:https://blog.csdn.net/2401_85325397/article/details/139472993【★】5.理解NLP最重要的编码方式(BPE)-知乎@Jinjie Ni【202304】:https://zhuanlan.zhihu.com/p/424631681【★】5xBPE算法详解-简书@ Jarkata【202202】:https://www.jianshu.com/p/6415a2e9ec09【★】6.NLP三大Subword模型详解:BPE、WordPiece、ULM-知乎@阿北【202008】:https://zhuanlan.zhihu.com/p/191648421分词算法:WordPiece
【1】简要介绍WordPiece【1】简要介绍WordPieceWordPiece算法可以看作是BPE的变种。每次从词表中选出两个子词合并成新的子词。不同点在于,WordPiece基于 下个最高互信息值的pair生成新的subword而不是下个最高频pair。v2:WordPiece分词与BPE非常类似,只是在训练阶段合并pair的策略不是pair的频率而是互信息。Score=log(P(AB))−[log(P(A))+log(P(B))]=log( P(AB)/P(A)*P(B) )。WordPiece每次合并的两个字符串A和B,应该具有最大的P(AB)/P(A)*P(B)值。合并AB之后,所有原来切成A+B两个tokens的就只保留AB一个token,整个训练集上最大似然变化量与P(AB)/P(A)*P(B)成正比。【2】WordPiece是如何选取子词的【2】WordPiece是如何选取子词的 WordPiece每次选择合并的两个子词具有最大的互信息值,即两个子词在语言模型上具有较强的关联性,它们经常在语料中以相邻方式同时出现。
【3】WordPiece 与 BPE 的异同点是什么【3】WordPiece 与 BPE 的异同点是什么
| BPE(Byte-PairEncoding) | WordPiece | 相似点: | 分词目标:WordPiece和BPE都旨在将文本分解为子词或字符级别的单位,以便更好地处理未登录词和稀有词,提高模型对复杂词汇和短语的处理能力。 无监督学习:WordPiece和BPE都是无监督学习方法,不需要依赖外部的标注数据,而是通过分析输入文本自动构建词典。 | 不同点: 拆分策略 | BPE则采用自底向上的拆分策略,通过合并频率最高的词对来构建词典。它使用词频来选择合并的词对,并通过更新词频来更新词典。 | WordPiece采用贪婪的自顶向下的拆分策略,将词汇表中的词分解为更小的子词。它使用最大似然估计来确定最佳的分割点,并通过词频来更新词典。 | 不同点: 分割粒度 | BPE则将词分解为更小的子词或字符级别的单位。它不使用特殊的前缀或后缀来表示子词。 | WordPiece通常将词分解为更小的子词,例如将"running"分解为"run"和"##ning"。这些子词通常以"##"前缀表示它们是一个词的一部分。 | 不同点: 处理未登录词 | BPE则将未登录词作为单独的词处理,不进行进一步的拆分。 | WordPiece通常将未登录词分解为更小的子词,以便模型可以更好地处理它们。 | 典型模型 | 典型使用BPE模型有:GPT-2 与RoBERTa | 典型使用WordPiece模型有:Bert/Electra/DistilBERT。 |
【4】WordPiece代码实现-CreatingWordPiece Tokenizer【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 分词算法:Unigram【Unigram Language Model】
【1】简要介绍ULM【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)。
当词汇表确定时,每个词tokenize的方法集合S(xi)就是确定的,而每种方法对应着一个概率p(x)。如果从词汇表中删除部分词,则某些词的tokenize的种类集合就会变少,log(*)中的求和项就会减少,从而增加整体loss【删除重要的subword对loss增长影响大,不删除】。Unigram算法每次会从词汇表中挑出使得loss增长最小的10%~20%的词汇来删除。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
【2】ULM如何来构造词表以及求解分词概率【2】ULM算法如何来构造词表以及求解分词概率输入:训练语料;词表大小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.Unigram-Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates:https://arxiv.org/abs/1804.10959【★】2.NLP三大Subword模型详解:BPE、WordPiece、ULM#Unigram Language Model (ULM)-知乎@阿北【202008】:https://zhuanlan.zhihu.com/p/191648421 【★】3.Subword Tokenization算法-@DonngZH【202303】:https://blog.csdn.net/weixin_44750512/article/details/129435981
【1】简要介绍SentencePiece【1】简要介绍SentencePiece SentencePiece是分词工具,内置BPE,Unigram等多种分词方法,基于Unicode编码并且将空格视为特殊的token。当前主流的大模型都是基于SentencePiece实现,例如ChatGLM的tokenizer。
SentencePiece 在大模型领域具有以下优势:分词效果好速度快,能够准确地识别词语和符号的边界。编码效率高,能够节省空间。能够将词语或符号之间的关系编码到编码中,有利于模型学习。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特性 1.唯一Token数量是预先确定的;与大多数假设无限词汇量的无监督分词算法不同,SentencePiece 在训练分词模型时,使最终的词汇表大小固定,例如:8k、16k 或 32k。 2.可以从原始句子进行训练;以前的子词(sub-word)实现假设输入句子是预标记(pre-tokenized)的。这种约束是有效训练所必需的,但由于我们必须提前运行依赖于语言的分词器,因此使预处理变得复杂。SentencePiece 的实现速度足够快,可以从原始句子训练模型。这对于训练中文和日文的tokenizer和detokenizer很有用,因为在这些词之间不存在明确的空格。3.空格被视为基本符号;SentencePiece 将输入文本视为一系列 Unicode 字符。空格也作为普通符号处理。为了明确地将空格作为基本标记处理,SentencePiece 首先使用元符号"▁"(U+2581)转义空格。
【1】SentencePiece技术优势【1】SentencePiece技术优势 1.纯数据驱动:SentencePiece 从句子中训练 tokenization 和 detokenization 模型。并不总是需要Pre-tokenization(Moses tokenizer/MeCab/KyTea)。2.独立于语言:SentencePiece 将句子视为 Unicode 字符序列。没有依赖于语言的逻辑。3.多子词算法:支持 BPE 和 unigram 语言模型。4.子词正则化:SentencePiece 实现子词正则化和 BPE-dropout 的子词采样,有助于提高 NMT 模型的鲁棒性和准确性。5.快速且轻量级:分割速度约为 50k 句子/秒,内存占用约为 6MB。6.Self-contained:只要使用相同的模型文件,就可以获得相同的tokenization/detokenization。7.直接从词汇 ID 生成:SentencePiece 管理词汇到 ID 的映射,可以直接从原始句子生成词汇 ID 序列。8.基于 NFKC 的 normalization:SentencePiece 执行基于 NFKC 的文本 normalization。【★】转载于:大模型词表扩充必备工具SentencePiece#训练模型-知乎@吃果冻不吐果冻皮【202305】:https://zhuanlan.zhihu.com/p/630696264
【2】使用SentencePiece训练分词器【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/620508648
【3】使用SentencePiece加载和使用词表模型文件tokenizer.model【3】使用SentencePiece加载和使用词表模型文件tokenizer.modelChatGLM3-6B中tokenizer.model模型是使用SentencePiece训练得到的词表模型文件。  tokenization_chatglm.py:分词器的 .py 文件,用于模型分词器,加载和使用模型的必要部分(只是处理文本,不涉及任何向量操作)。tokenizer.model:包含了训练好的分词模型,用于将输入文本转换为标记序列;二进制使用 pickle 或者其他序列化工具进行存储和读取。tokenizer_config.json:分词模型的配置信息,用于指定分词模型的超参和其他的相关信息,例如分词器的类型、词汇表大小、最大序列长度、特殊标记等。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转为句子结果:你好,你是谁?
【4】使用SentencePiece对LLM词表扩充【4】使用SentencePiece对LLM词表扩充使用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.vocab
importsentencepieceasspm
sp=spm.SentencePieceProcessor()
text="这贾雨村原系胡州人氏,也是诗书仕宦之族,因他生于末世,父母祖宗根基已尽,人口衰丧,只剩得他一身一口,在家乡无益,因进京求取功名,再整基业。"
sp.Load("hongluomeng-tokenizer.model")
print(sp.EncodeAsPieces(text)) ['▁','这','贾','雨','村','原','系','胡','州','人','氏',',','也','是','诗','书','仕','宦','之','族',',','因','他','生','于','末','世',',','父','母','祖','宗','根','基','已','尽',',','人','口','衰','丧',',','只','剩','得','他','一','身','一','口',',','在','家','乡','无','益',',','因','进','京','求','取','功','名',',','再','整','基','业','。'] 【★】转载于:大模型词表扩充必备工具SentencePiece#训练模型-掘金@吃果冻不吐果冻皮【202305】:https://zhuanlan.zhihu.com/p/630696264 【5】SentencePiece-byte回退【5】SentencePiece-byte回退当SentencePiece在训练BPE的时开启--byte_fallback, 在效果上类似BBPE,遇到UNK会继续按照byte进行进一步的切分。参见:https://github.com/google/sentencepiece/issues/621具体实现上是将<0x00> ... <0xFF>这256个token添加到词表中。分析ChatGLM的模型,可以发现ChatGLM就是开启了--byte_fallback。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
【6】和SentencePiece类似的工具【6】和SentencePiece类似的工具SentencePiece:英文分词工具,使用无监督学习的分词方法。基于BPE编码的编码方法。分词效果好,编码效率高,但需要训练模型。 Jieba:中文分词工具,使用最大似然法的方法进行分词。基于哈希编码的编码方法。Jieba的分词效果较好,并且速度较快,但需要人工制定分词规则。 Hmmseg:中文分词工具,使用隐马尔可夫模型的方法进行分词。基于哈希编码的编码方法。Hmmseg的分词效果较好,并且可以支持多种语言,但需要训练模型。 StanfordCoreNLP:自然语言处理工具包,包含分词、词性标注、句法分析等功能。使用基于规则的方法分词。基于哈希编码的编码方法。Stanford CoreNLP 的分词效果较好,并且可以支持多种语言。 Tiktoken:基于BPE算法的快速分词器,专门针对GPT-4和ChatGPT等大模型进行了优化。Tiktoken 的主要特点是分词速度比 SentencePiece 快很多,分词效果与SentencePiece相当,提供简单的 API接口,方便使用。 【7】分词器使用建议分词器使用建议: 1.如果需要对中文文本进行分词,并且对分词效果要求较高,可以选择 SentencePiece、Jieba 或 Hmmseg。如果需要对多种语言文本进行分词,可以选择 Stanford CoreNLP。2.如果需要对文本进行分词和编码,并且对速度要求较高,可以选择 Jieba。对分词效果要求较高,可以选择 SentencePiece 或 Hmmseg。【★】大模型分词:sentencepiece vs titoken-知乎@王几行【202404】:https://zhuanlan.zhihu.com/p/691609961推荐阅读:
【★】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 Toknenizer相关面试题总结 【X】简要介绍常用Tokenizer的原理和区别【X】简要介绍常用Tokenizer的原理和区别1.BPE构建词典从字符级词表开始,不断在语料库中找词频最高且连续的token合并加入词表,直到达到目标词数。生成大小可控的开放词汇表。 2.WordPiece分词与BPE非常类似,只是在训练阶段合并pair的策略不是pair的频率而是互信息。?????=???(?(??))−(???(?(?))+???(?(?)))=???(?(??)/?(?)?(?))。 3.与BPE和WordPiece不同,Unigram算法是从预分词器分的词+所有高频的子词构成的大词汇表出发,再逐步删除其中重要性较低的subword,直到满足预定义size。
【X】BBPE和BPE的区别BPE的问题是,如果遇到了unicode,基本字符集可能会很大。一种处理方法BBPE是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256。例如,像GPT-2的词汇表大小为50257 = 256 +<EOS>+ 50000 mergers,<EOS>是句子结尾的特殊标记。 BBPE是BPE的扩展版本,BBPE核心思想将BPE的从字符级别扩展到字节级别。即字节级 BPE 将所有 Unicode 代码点转换为多个字节级字符)。LLaMA,ChatGLM...都基于BBPE实现。 使用BBPE有什么好处?使用BBPE主要优点是:较小的词汇表。没有未知token,不会出现OOV问题。
|