微信扫码
添加专属顾问
我要投稿
以英文文本 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方法能够大大降低词典的大小,同时对相近词能更好地处理。
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,AutoTokenizer,
BitsAndBytesConfig, HfArgumentParser,
TrainingArguments,pipeline,logging,
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer
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
from transformers import AutoTokenizer, 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 你好,我是人工智能助手。很高兴为您服务!请问有什么问题我可以帮您解答?']
"""
import torch
from datasets import load_dataset
from tokenizers import (decoders,models,normalizers,pre_tokenizers,processors,trainers,Tokenizer,)
from transformers import PreTrainedTokenizerFast
dataset_path = "TurboPascal/tokenizers_example_zh_en"
tokenize_path = "BPE_tokenizer.json"
# 加载训练数据集
dataset = load_dataset(data_files=dataset_path, cache_dir='./cache/')
def batch_iterator(batch_size=10000):
for i in range(0, len(dataset), batch_size):
yield dataset['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)
# 创建BPE tokenizer对象
tokenizer = Tokenizer(models.BPE())
tokenizer.train_from_iterator(batch_iterator(), trainer=trainer, length=len(dataset['train']))
# 保存trained tokenizer
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,直到达到预设的词典大小或者满足其他停止条件。每次迭代都会合并频率最高的词对,并更新词频。
import re, collections
def get_vocab(filename):
vocab = collections.defaultdict(int)
with open(filename, 'r', encoding='utf-8') as fhand:
for line in fhand:
words = line.strip().split()
for word in words:
vocab[' '.join(list(word)) + ' </w>'] += 1
return vocab
def get_stats(vocab):
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i],symbols[i+1]] += freq
return pairs
def merge_vocab(pair, v_in):
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out
def get_tokens(vocab):
tokens = collections.defaultdict(int)
for word, freq in vocab.items():
word_tokens = word.split()
for token in word_tokens:
tokens[token] += freq
return tokens
vocab = {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
# Get free book from Gutenberg
# wget http://www.gutenberg.org/cache/epub/16457/pg16457.txt
# vocab = get_vocab('pg16457.txt')
print('==========')
print('Tokens Before BPE')
tokens = get_tokens(vocab)
print('Tokens: {}'.format(tokens))
print('Number of tokens: {}'.format(len(tokens)))
print('==========')
num_merges = 5
for i in range(num_merges):
pairs = get_stats(vocab)
if not pairs:
break
best = max(pairs, key=pairs.get)
vocab = merge_vocab(best, vocab)
print('Iter: {}'.format(i))
print('Best pair: {}'.format(best))
tokens = get_tokens(vocab)
print('Tokens: {}'.format(tokens))
print('Number of tokens: {}'.format(len(tokens)))
print('==========')
#BPE算法详解:https://www.jianshu.com/p/6415a2e9ec09
# 0)给定单词序列
["the</w>", "highest</w>", "mountain</w>"]
# 1)在BPE构建词典中得到subword 的词表,对该subword词表按照字符个数由多到少排序。
# 长度 6 5 4 4 4 4 2
["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
from tokenizers import (decoders,models,normalizers,pre_tokenizers,processors,trainers,Tokenizer,)
from transformers import PreTrainedTokenizerFast
from datasets import Dataset
# Creating Byte-Pair Encoding tokenizer
raw_tokenizer = Tokenizer(models.BPE(unk_token="[UNK]")) #设置Tokenizer
raw_tokenizer.normalizer = normalizers.Sequence([normalizers.NFC()] + [normalizers.Lowercase()] if LOWERCASE else []) #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
def train_corp_iter():
for i in range(0, len(dataset), 25):
yield dataset[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 = []
for text in tqdm(test['text'].tolist()):
tokenized_texts_test.append(tokenizer.tokenize(text))
tokenized_texts_train = []
for text in tqdm(train['text'].tolist()):
tokenized_texts_train.append(tokenizer.tokenize(text))
import re, collections
def get_stats(vocab):
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i],symbols[i+1]] += freq
return pairs
def merge_vocab(pair, v_in):
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out
vocab = {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
num_merges = 1000
for i in range(num_merges):
pairs = get_stats(vocab)
if not pairs:
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为{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}
第1轮合并,best-pair值为('es', 't'),|||合并后vocab为{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}
第2轮合并,best-pair值为('est', '</w>'),|||合并后vocab为{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}
第3轮合并,best-pair值为('l', 'o'),|||合并后vocab为{'lo w </w>': 5, 'lo w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}
第4轮合并,best-pair值为('lo', 'w'),|||合并后vocab为{'low </w>': 5, 'low e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}
第5轮合并,best-pair值为('n', 'e'),|||合并后vocab为{'low </w>': 5, 'low e r </w>': 2, 'ne w est</w>': 6, 'w i d est</w>': 3}
第6轮合并,best-pair值为('ne', 'w'),|||合并后vocab为{'low </w>': 5, 'low e r </w>': 2, 'new est</w>': 6, 'w i d est</w>': 3}
第7轮合并,best-pair值为('new', 'est</w>'),|||合并后vocab为{'low </w>': 5, 'low e r </w>': 2, 'newest</w>': 6, 'w i d est</w>': 3}
第8轮合并,best-pair值为('low', '</w>'),|||合并后vocab为{'low</w>': 5, 'low e r </w>': 2, 'newest</w>': 6, 'w i d est</w>': 3}
第9轮合并,best-pair值为('w', 'i'),|||合并后vocab为{'low</w>': 5, 'low e r </w>': 2, 'newest</w>': 6, 'wi d est</w>': 3}
第10轮合并,best-pair值为('wi', 'd'),|||合并后vocab为{'low</w>': 5, 'low e r </w>': 2, 'newest</w>': 6, 'wid est</w>': 3}
第11轮合并,best-pair值为('wid', 'est</w>'),|||合并后vocab为{'low</w>': 5, 'low e r </w>': 2, 'newest</w>': 6, 'widest</w>': 3}
第12轮合并,best-pair值为('low', 'e'),|||合并后vocab为{'low</w>': 5, 'lowe r </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},其中数字代表的是对应单词在语料中的频数。
输入【语料】:{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}。此时词表:{'l' , 'o' ,'w' ,'e' ,'r' ,'n' ,'s' ,'t' ,'i' ,'d' ,'</w>' ,}
Iter 1, 最高频连续字节对"e"和"s"出现了6+3=9次,合并成"es"。输出【新语料】:{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}。【新词表】:{'l' , 'o' ,'w' ,'e' ,'r' ,'n' ,'s' ,'t' ,'i' ,'d' ,'</w>' ,'es' ,}。
Iter 2, 最高频连续字节对"es"和"t"出现了6+3=9次, 合并成"est"。输出【新语料】:{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}。【新词表】:{'l' , 'o' ,'w' ,'e' ,'r' ,'n' ,'t' ,'i' ,'d' ,'</w>' ,'es','est' ,}。
Iter 3, 以此类推,最高频连续字节对为"est"和"</w>" 。输出【新语料】:{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}。【新词表】:{'l' , 'o' ,'w' ,'e' ,'r' ,'n' ,'i' ,'d' ,'</w>' ,'est' ,'est</w>',}。
……
Iter n, 继续迭代直到达到预设的subword词表大小或下一个最高频的字节对出现频率为1。
注:停止符"</w>"的意义在于表示subword是词后缀。举例来说:"st"字词不加"</w>"可以出现在词首如"st ar",加了"</w>"表明该字词位于词尾,如"wide st</w>",二者意义截然不同。
BPE的问题是,如果遇到了unicode,基本字符集可能会很大。一种处理方法BBPE是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256。例如,像GPT-2的词汇表大小为50257 = 256 + <EOS> + 50000 mergers,<EOS>是句子结尾的特殊标记。
BBPE是BPE的扩展版本,BBPE核心思想将BPE的从字符级别扩展到字节级别。即字节级 BPE 将所有 Unicode 代码点转换为多个字节级字符)。LLaMA,ChatGLM...都基于BBPE实现。
特点 | Character Set| 字符集 | |
传统BPE-字符级别BPE。 | 传统的BPE基于char粒度去执行合并的过程生成词表。 | (ASCII编码--) 传统BPE使用1个字节对字符进行编码 以字符粒度的编码 |
BBPE-字节级别的BPE。 | BBPE是基于字节粒度 去执行合并过程生成词表。 BBPE能比较好支持语料是多种语言的分词。 | (Unicode编码-改进utf-8编码) BBPE使用1~4个字节对字符进行编码 以字节粒度的编码 |
【2】WordPiece是如何选取子词的
【3】WordPiece 与 BPE 的异同点是什么
【4】WordPiece代码实现-Creating WordPiece Tokenizer
from tokenizers import (decoders,models,normalizers,pre_tokenizers,processors,trainers,Tokenizer,)
from transformers import PreTrainedTokenizerFast
from datasets import Dataset
# Creating WordPiece tokenizer
raw_tokenizer = Tokenizer(models.WordPiece()) #models
raw_tokenizer.normalizer = normalizers.Sequence([normalizers.NFC()] + [normalizers.Lowercase()] if LOWERCASE else []) #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
...
class TextTokenizer:
def __init__(self, model_path):
self.sp = spm.SentencePieceProcessor()
self.sp.Load(model_path)
self.num_tokens = self.sp.vocab_size()
def encode(self, text):
return self.sp.EncodeAsIds(text)
def decode(self, ids: List[int]):
return self.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训练分词器
##v1 Train a BPE Model
import sentencepiece as spm
# train sentencepiece model from our blog corpus
spm.SentencePieceTrainer.train(input='blog_test.txt',model_prefix=bpe --vocab_size=500, user_defined_symbols=['foo', 'bar'])
# makes segmenter instance and loads the BPE model file (bpe.model)
sp_bpe = spm.SentencePieceProcessor()
sp_bpe.load('bpe.model')
####################################################################################
##v2 Train a Unigram Model
import sentencepiece as spm
# train sentencepiece model from our blog corpus
spm.SentencePieceTrainer.train(input='blog_test.txt',model_prefix=bpe --vocab_size=500, user_defined_symbols=['foo', 'bar'])
# makes segmenter instance and loads the BPE model file (bpe.model)
sp_uni = spm.SentencePieceProcessor()
sp_uni.load('uni.model')
####################################################################################
## 比较一下两个分词器结果
print("BPE: {}".format(sp_bpe.encode_as_pieces('This is a test')))
print("UNI: {}".format(sp_uni.encode_as_pieces('This is a test')))
BPE: ['▁This', '▁is', '▁a', '▁t', 'est']
UNI: ['▁Thi', 's', '▁is', '▁a', '▁t', 'est']
#https://zhuanlan.zhihu.com/p/620508648
ChatGLM3-6B中tokenizer.model模型是使用SentencePiece训练得到的词表模型文件。
from sentencepiece import SentencePieceProcessor
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 命令行工具
>sudo apt-get install cmake build-essential pkg-config libgoogle-perftools-dev
>git clone https://github.com/google/sentencepiece.git
>cd sentencepiece | mkdir build| cd build | cmake .. | make -j $(nproc) | make install | 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 #60 254 70 333 468 400 14 733 1476 317 603 510 15
#将句子片段(token) id 解码为原始文本。
> echo "60 254 70 333 468 400 14 733 1476 317 603 510 15" | 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
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
text="这贾雨村原系胡州人氏,也是诗书仕宦之族,因他生于末世,父母祖宗根基已尽,人口衰丧,只剩得他一身一口,在家乡无益,因进京求取功名,再整基业。"
sp.Load("hongluomeng-tokenizer.model")
print(sp.EncodeAsPieces(text))
['▁', '这', '贾', '雨', '村', '原', '系', '胡', '州', '人', '氏', ',', '也', '是', '诗', '书', '仕', '宦', '之', '族', ',', '因', '他', '生', '于', '末', '世', ',', '父', '母', '祖', '宗', '根', '基', '已', '尽', ',', '人', '口', '衰', '丧', ',', '只', '剩', '得', '他', '一', '身', '一', '口', ',', '在', '家', '乡', '无', '益', ',', '因', '进', '京', '求', '取', '功', '名', ',', '再', '整', '基', '业', '。']
from sentencepiece import sentencepiece_model_pb2
m = sentencepiece_model_pb2.ModelProto()
with open('chatglm-6b/ice_text.model', 'rb') as f:
m.ParseFromString(f.read())
print('ChatGLM tokenizer\n\n'+str(m.trainer_spec))
"""
ChatGLM tokenizer
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的分词效果较好,并且可以支持多种语言,但需要训练模型。
Stanford CoreNLP:自然语言处理工具包,包含分词、词性标注、句法分析等功能。使用基于规则的方法分词。基于哈希编码的编码方法。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。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-08-13
2024-06-13
2024-08-21
2024-09-23
2024-07-31
2024-05-28
2024-08-04
2024-04-26
2024-07-09
2024-09-17
2025-04-20
2025-04-18
2025-04-16
2025-04-13
2025-04-13
2025-04-13
2025-04-12
2025-04-12