LLMs Tokenizer 常见面试篇
来源:AiGC面试宝典 更新时间:2024年6月16日
目录
- Byte-Pair Encoding (BPE) 篇
- Byte-level BPE 篇
- WordPiece 篇
- SentencePiece 篇
- Transformer Tokenizer 篇
- Unigram LM 篇
- 对比篇
Byte-Pair Encoding (BPE) 篇
1 介绍一下 Byte-Pair Encoding (BPE)?
BPE(Byte Pair Encoding) 是一种基于字符级别的分词算法,常用于处理未分词的文本。它从字符级别开始,逐步合并出现频率最高的字符或字符组合,形成更长的子词(subword)。
📝通俗解释:BPE就像教小朋友认字一样,先从单个笔画(字符)开始,然后慢慢把常见笔画组合合并成完整的字。比如"木"+"木"="林","林"+"木"="森"。这样模型就能认识更多词汇了。
2 Byte-Pair Encoding (BPE) 如何构建词典?
- 准备训练语料:准备足够的训练语料以及期望的词表大小
- 初始化拆分:将单词拆分为字符粒度,并在末尾添加特殊标记
</w>,统计单词频率 - 合并操作:统计每一个连续/相邻字节对的出现频率,将最高频的连续字节对合并为新的子词
- 迭代终止:重复第3步,直到词表达到设定的词表大小,或下一个最高频字节对出现频率为1
📝通俗解释:想象你有一堆积木(字符),你不断找出最常在一起的两块积木,把它们粘在一起变成新的大块,直到积木数量达到你想要的数目。
注:GPT2、BART 和 LLaMA 采用的就是 BPE 分词方式。
3 Byte-Pair Encoding (BPE) 具有什么优点?
- 无监督学习:BPE 是一种无监督学习算法,不需要人工标注的分词数据即可进行词汇划分
- 适应性强:BPE 可以根据语料库进行自适应,能够学习到不同语种、领域的词汇特点,适用范围广
- 有效处理未登录词:BPE 可以将未登录词(OOV,Out-of-Vocabulary)分割成较小的子词,从而提高模型对未登录词的处理能力
📝通俗解释:就像一个会自动学习新单词的词典,遇到不认识的词可以把它拆成认识的部分来理解,比如遇到"人工智能"不认识,但认识"人工"和"智能",就能猜出大概意思。
4 Byte-Pair Encoding (BPE) 具有什么缺点?
- OOV问题:由于 BPE 算法基于统计,其词汇表是固定大小的,如果遇到未在词汇表中出现的单元,将其分割为子词可能会增加语义上的困惑
- 过度分割问题:由于 BPE 算法在合并时选择频次最高的字符或字符组合,可能会导致某些词被过于粗糙地分割,产生一些无意义的子词片段
- 分词效率较低:BPE 算法是一个迭代的过程,可能需要大量的计算资源和时间来处理大规模的文本数据
📝通俗解释:有时候BPE会"聪明反被聪明误",比如把"deep learning"拆成"deep"+" le"+"ar"+"ning"这种奇怪的组合,反而让机器更难理解。
5 手撕 Byte-Pair Encoding (BPE)?
from collections import defaultdict
def get_stats(vocab):
"""统计所有相邻字符对的频率"""
pairs = 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 = ' '.join(pair)
for word in v_in:
v_out[word] = v_in[word]
new_word = word.replace(' '.join(pair), bigram)
if new_word != word:
v_out[new_word] = v_in[word]
return v_out
def bpe(text, num_iters):
"""BPE分词算法主函数"""
# 初始化:将每个单词拆分为字符
vocab = defaultdict(int)
for word in text:
vocab[' '.join(word)] += 1
# 迭代合并
for i in range(num_iters):
pairs = get_stats(vocab)
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
vocab = merge_vocab(best_pair, vocab)
return vocab
# 示例用法
text = ['low', 'lower', 'newest', 'widest', 'room', 'rooms']
num_iters = 10
vocab = bpe(text, num_iters)
print(vocab)输出示例:
{'l o w': 1, 'l o w e r': 1, 'n e w e s t': 1, 'w i d e s t': 1, 'r o o m': 1, 'r o o m s': 1}📝通俗解释:这段代码就像一个"词汇合并机器",它会不断找出最常一起出现的字符对,然后把它们合并成一个新的"积木块"。
Byte-level BPE 篇
1 介绍一下 Byte-level BPE?
Byte-level BPE(BBPE) 是将 BPE 的思想从字符级别扩展到字节级别。它使用 UTF-8 编码,将每个 Unicode 字符编码成 1 到 4 个字节,从而将句子建模为字节序列。
📝通俗解释:普通的BPE是在"字符"层面操作,而BBPE更底层,直接在"字节"层面操作。这就像不只是把汉字拆成偏旁部首,而是直接拆成笔画(字节)。
2 Byte-level BPE 如何构建词典?
- 将文本转换为 UTF-8 字节序列(只需要 256 个可能的字节中的 248 个)
- 学习关于字节级表示的 BPE 词汇,用字节 n-gram(字节级"子词")扩展 UTF-8 字节集
- 将字节序列分割成可变长度的 n-gram
📝通俗解释:想象所有语言的文章都是由256种不同颜色的小点(字节)组成的,BBPE就是学习哪些颜色组合最常出现,然后把常见组合变成新的"大色块"。
3 Byte-level BPE 具有什么优点?
- 词表大幅减小:效果与 BPE 相当,但词表大为减小
- 跨语言共享好:可以在多语言之间通过字节级别的子词实现更好的共享
- 迁移学习好:即使字符集不重叠,也可以通过字节层面的共享来实现良好的迁移
📝通俗解释:就像全世界的人都可以用同样的乐高积木(字节)来搭建不同的建筑(语言),这样不同语言之间可以更好地"借用"词汇。
4 Byte-level BPE 具有什么缺点?
- 编码长度增加:编码序列时,长度可能会略长于 BPE,计算成本更高
- 解码歧义:由 byte 解码时可能会遇到歧义,需要通过上下文信息和动态规划来进行解码
📝通俗解释:虽然BBPE很强大,但有时候会把简单的事情变复杂——比如用一个更长的序列来表示原本简单的词,而且还原回去的时候可能产生歧义。
WordPiece 篇
1 介绍一下 WordPiece?
WordPiece 是一种基于子词级别的分词算法,常用于将文本划分为更小的单元。它通过合并出现频率最高的子词或子词组合,构建词汇表,并将文本划分为这些子词。
📝通俗解释:WordPiece就像把单词"庖丁解牛"——把单词拆成有意义的小块,比如"庖"+"丁"+"解"+"牛",每个小块都有意义。
注:BERT 采用了 WordPiece。
2 WordPiece 如何构建词典?
- 初始化:将每个字符作为初始词汇表中的单元
- 统计频次:对语料进行统计,记录每个单词及子词的频次
- 合并:在每次迭代中,选择频次最高的相邻两个单元(字符或子词),将其合并为一个新的单元
- 更新词汇表:更新词汇表,将新合并的单元添加到词汇表中
- 迭代终止:重复步骤2和3,直到达到预设的词汇表大小或满足停止条件
📝通俗解释:这个过程就像拼图游戏,不断找出最常一起出现的两块小图,把它们拼成一个大块,直到拼图数量合适为止。
3 WordPiece 具有什么优点?
- 语言无关性:WordPiece 算法是一种通用的分词算法,不依赖于特定的语言或语料库,能够适应不同的语种和领域
- 词汇控制:通过合并相邻的单元,WordPiece 算法可以根据词汇表的需求,动态控制词汇大小
- 有效处理未登录词:WordPiece 算法将文本划分为子词,可以有效处理未登录词
📝通俗解释:不管你让它学中文、英文还是日文,它都能自动找到合适的"小积木"来搭建词汇,而且遇到不会的词也能拆成熟悉的小块来理解。
4 WordPiece 具有什么缺点?
- OOV问题:由于词汇表是固定大小的,WordPiece 算法在遇到未在词汇表中出现的单元时,将其分割为子词可能会导致一些语义上的困惑
- 词的不连续性:WordPiece 算法将单词分割为子词,可能导致词的不连续性,使得模型需要更长的上下文来理解词的语义
- 分割歧义:由于 WordPiece 算法仅根据频次合并单元,并不能解决所有的分割歧义问题
📝通俗解释:有时候拆分会让人误解,比如"篮球"拆成"篮"+"球"和"篮球"是完全不同的意思,WordPiece可能会选错。
5 手撕 WordPiece?
from collections import defaultdict
def get_stats(vocab):
"""统计所有相邻字符对的频率"""
pairs = 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 = ''.join(pair) # 注意:这里用空字符串拼接,与BPE不同
for word in v_in:
v_out[word] = v_in[word]
new_word = word.replace(' '.join(pair), bigram)
if new_word != word:
v_out[new_word] = v_in[word]
return v_out
def wordpiece(text, num_iters):
"""WordPiece分词算法主函数"""
# 初始化:每个字符为一个单元,并在末尾添加</w>标记
vocab = defaultdict(int)
for word in text:
vocab[' '.join(list(word)) + ' </w>'] += 1
# 迭代合并
for i in range(num_iters):
pairs = get_stats(vocab)
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
vocab = merge_vocab(best_pair, vocab)
return vocab
# 示例用法
text = ['low', 'lower', 'newest', 'widest', 'room', 'rooms']
num_iters = 10
vocab = wordpiece(text, num_iters)
print(vocab)输出示例:
{'l o w </w>': 1, 'lo w </w>': 1, 'l o w e r </w>': 1,
'lo w e r </w>': 1, 'l o w e r</w>': 1, 'lo w e r</w>': 1}📝通俗解释:WordPiece和BPE代码很像,核心区别是WordPiece用空字符串拼接(变成连续的词),而BPE用空格拼接(保留分隔)。
6 WordPiece 与 BPE 异同点是什么?
相同点:本质上都是 BPE 的思想
最大区别:如何选择两个子词进行合并
| 算法 | 合并策略 |
|---|---|
| BPE | 选择频次最大的相邻子词合并 |
| WordPiece | 选择能够提升语言模型概率最大的相邻子词进行合并 |
📝通俗解释:BPE就像选最常见的邻居合并,WordPiece则像选合并后能让句子最通顺的邻居合并。WordPiece更"聪明"一些,但计算也更复杂。
SentencePiece 篇
1 介绍一下 SentencePiece?
SentencePiece 是一种基于未分词语料的无监督分词算法,可以用于构建多语言的分词器。它可以通过训练,自动识别出多种语言的词汇,并对文本进行分词。
📝通俗解释:SentencePiece就像一个"语言自学机",你给它一堆没有标注过的文本,它能自动学会怎么切分这些文字,而且支持很多种语言。
注:ChatGLM、BLOOM、PaLM 采用了 SentencePiece。
2 SentencePiece 如何构建词典?
- 初始化:将每个字符作为初始词汇表中的单元
- 统计频次:对语料进行统计,记录每个单词及子词的频次
- 合并:在每次迭代中,选择频次最高的相邻两个单元(字符或子词),将其合并为一个新的单元
- 更新词汇表:更新词汇表,将新合并的单元添加到词汇表中
- 迭代终止:重复步骤2和3,直到达到预设的词汇表大小或满足停止条件
📝通俗解释:SentencePiece的建词典过程和其他BPE变体类似,但它更注重端到端的训练,直接从原始文本学习,不需要预先分词。
3 SentencePiece 具有什么优点?
- 语言无关性:SentencePiece 算法不依赖于特定的语言或语料库,可以适应不同的语种和领域
- 动态词汇表:可以动态控制词汇大小,使得词汇表能够适应不同的任务和数据规模
- 分词效果好:精细地将单词划分为子词,提供更好的语义表示能力
- 显著降低OOV问题:将未登录词分割成子词,显著降低了OOV问题的出现
📝通俗解释:SentencePiece就像一个"全能翻译",不管给它什么语言的文本,它都能自动学会怎么切分,而且切分效果通常很好。
4 SentencePiece 具有什么缺点?
- 分词复杂性:SentencePiece 算法在处理大规模的文本数据时可能需要较长的时间
- 模型大小:通过生成一个大词汇表来表示子词,可能会导致模型的大小增加
- 分词结果不唯一:由于合并过程是基于统计的,可能产生多个合理的分词结果
📝通俗解释:虽然SentencePiece很强,但有时候它可能会"选择困难"——对于同一个词可能有多种合理的切法,而且词表太大也会占用更多内存。
5 手撕 SentencePiece?
简化版实现
from collections import defaultdict
import os
def get_vocabulary(corpus):
"""从语料库中获取初始词汇表"""
vocab = defaultdict(int)
with open(corpus, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
tokens = line.split()
for token in tokens:
vocab[token] += 1
return vocab
def get_stats(vocab):
"""获取每个字符或字符组合的频次"""
pairs = 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, vocab):
"""根据频次最高的字符组合进行合并"""
vocab_out = {}
bigram = ''.join(pair)
for word in vocab:
vocab_out[word] = vocab[word]
new_word = word.replace(' '.join(pair), bigram)
if new_word != word:
vocab_out[new_word] = vocab[word]
return vocab_out
def train_sentencepiece(corpus, vocab_size, max_iters):
"""基于词频统计来迭代地进行符号合并"""
vocab = get_vocabulary(corpus)
for _ in range(max_iters):
pairs = get_stats(vocab)
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
vocab = merge_vocab(best_pair, vocab)
if len(vocab) >= vocab_size:
break
return vocab
def encode_sentencepiece(text, vocab):
"""将文本编码为SentencePiece标记序列"""
encoded_text = []
for token in text.split():
if token in vocab:
encoded_text.extend(vocab[token].split())
else:
encoded_text.append(token)
return encoded_text
def decode_sentencepiece(encoded_text, reverse_vocab):
"""将标记序列解码为原始文本"""
decoded_text = ' '.join(encoded_text)
for token, replacement in reverse_vocab.items():
decoded_text = decoded_text.replace(token, replacement)
return decoded_text
# 示例用法
# corpus = "data.txt" # 输入语料文件路径
# vocab_size = 1000 # 词汇表大小
# max_iters = 10 # 最大迭代次数
# vocab = train_sentencepiece(corpus, vocab_size, max_iters)
# text = "This is a sample sentence to encode with SentencePiece."
# encoded_text = encode_sentencepiece(text, vocab)
# print(f"Encoded text: {encoded_text}")📝通俗解释:这是一个简化版的SentencePiece实现,展示了核心思想。实际生产中建议使用Google官方的sentencepiece库。
官方库使用示例
import sentencepiece as spm
def train_sentencepiece(corpus, model_prefix, vocab_size):
"""使用官方库训练SentencePiece模型"""
spm.SentencePieceTrainer.train(
input=corpus,
model_prefix=model_prefix,
vocab_size=vocab_size
)
def encode_sentencepiece(model_prefix, text):
"""编码文本"""
sp = spm.SentencePieceProcessor(model_file=f"{model_prefix}.model")
encoded_text = sp.encode_as_pieces(text)
return encoded_text
def decode_sentencepiece(model_prefix, encoded_text):
"""解码文本"""
sp = spm.SentencePieceProcessor(model_file=f"{model_prefix}.model")
decoded_text = sp.decode_pieces(encoded_text)
return decoded_text
# 示例用法
# corpus = "data.txt" # 输入语料文件路径
# model_prefix = "spm_model" # SentencePiece模型前缀
# vocab_size = 1000 # 词汇表大小
# train_sentencepiece(corpus, model_prefix, vocab_size)
# text = "This is a sample sentence to encode with SentencePiece."
# encoded_text = encode_sentencepiece(model_prefix, text)
# print(f"Encoded text: {encoded_text}")
# decoded_text = decode_sentencepiece(model_prefix, encoded_text)
# print(f"Decoded text: {decoded_text}")📝通俗解释:Google的sentencepiece库是工业级实现,性能更好、更稳定,实际项目中建议直接使用这个库。
Transformer Tokenizer 篇
1 介绍一下 Transformer Tokenizer?
Transformer Tokenizer(如 BERT Tokenizer)是基于 Transformer 架构的分词器。它可以将文本划分为基于词、子词或字符级别的标记,并生成对应的词嵌入。
📝通俗解释:Transformer Tokenizer不是简单的切分文字,它是结合了深度学习理解能力的"智能分词器",能根据上下文决定怎么切分最合适。
2 Transformer Tokenizer 如何构建词典?
Transformer Tokenizer 的核心原理:
- 自注意力机制(Self-Attention):通过计算每个词与其他词之间的相似度得到注意力权重,将注意力权重作用于词向量上
- 编码器-解码器结构(Encoder-Decoder):编码器将输入序列转换为中间表示,解码器生成目标序列
- 多头注意力机制(Multi-Head Attention):将注意力机制应用到多个线性映射的投影中,捕捉不同类型的依赖关系
- 位置编码(Positional Encoding):对词的位置进行编码,与词向量相加获得每个词的综合表示
📝通俗解释:Transformer Tokenizer就像一个"理解上下文的高手",它不仅看词本身,还看这个词周围的其他词,根据整体语境来决定最好的表示方式。
3 Transformer Tokenizer 具有什么优点?
- 并行计算:每个词都可以并行计算其与其他词之间的关系,加快计算速度
- 长距离依赖性:能够捕捉到更远距离的上下文信息
- 上下文感知:可以对每个词进行不同程度的关注,更好地理解上下文语义
- 可解释性:可以直观地观察到哪些词对于当前词最重要
📝通俗解释:Transformer Tokenizer就像一个"全局观察者",它能一眼看到句子中所有词的关系,而不是像以前的方法只能看附近的词。
4 Transformer Tokenizer 具有什么缺点?
- 训练复杂性:包含大量参数和复杂计算,训练过程相对较慢
- 长距离依赖挑战:在某些复杂任务中,仍可能存在对长距离依赖的挑战
📝通俗解释:虽然Transformer很强大,但它就像一个"重型机器",训练和使用都需要更多的计算资源。
5 手撕 Transformer Tokenizer?
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import copy
class PositionalEncoding(nn.Module):
"""位置编码:为了让Transformer感知词的位置信息"""
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=0.1)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + self.pe[:x.size(0), :]
return self.dropout(x)
class TransformerEncoderLayer(nn.Module):
"""Transformer编码器层"""
def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1):
super(TransformerEncoderLayer, self).__init__()
self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, src, src_mask=None, src_key_padding_mask=None):
src2 = self.self_attn(src, src, src, attn_mask=src_mask,
key_padding_mask=src_key_padding_mask)[0]
src = src + self.dropout1(src2)
src = self.norm1(src)
src2 = self.linear2(self.dropout(F.relu(self.linear1(src))))
src = src + self.dropout2(src2)
src = self.norm2(src)
return src
class TransformerEncoder(nn.Module):
"""Transformer编码器:多个编码器层堆叠"""
def __init__(self, encoder_layer, num_layers, norm=None):
super(TransformerEncoder, self).__init__()
self.layers = nn.ModuleList([copy.deepcopy(encoder_layer) for _ in range(num_layers)])
self.num_layers = num_layers
self.norm = norm
def forward(self, src, mask=None, src_key_padding_mask=None):
output = src
for layer in self.layers:
output = layer(output, src_mask=mask, src_key_padding_mask=src_key_padding_mask)
if self.norm is not None:
output = self.norm(output)
return output
# 示例用法
d_model = 512 # 词嵌入维度
nhead = 8 # 多头自注意力头数
dim_feedforward = 2048 # 前馈网络隐藏层维度
dropout = 0.1 # 丢弃率
num_layers = 6 # 编码器层数
src = torch.randn(10, 32, d_model) # 输入张量 (seq_len, batch_size, d_model)
pos_encoder = PositionalEncoding(d_model)
encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout)
transformer_encoder = TransformerEncoder(encoder_layer, num_layers)
src = pos_encoder(src)
output = transformer_encoder(src)
print(f"输出形状: {output.shape}") # torch.Size([10, 32, 512])📝通俗解释:这段代码展示了Transformer的核心结构——位置编码让模型知道词的位置,多头注意力让模型关注不同方面的关系,层层堆叠形成强大的表示能力。
Unigram LM 篇
1 介绍一下 Unigram LM?
Unigram LM(Unigram Language Model) 是大模型中常用的分词算法之一。它是一种基于统计的单元划分方法,基于语言模型构建分词规则。
📝通俗解释:Unigram LM就像一个"概率统计员",它统计每个词在语料中出现的概率,然后根据概率来决定怎么切分最合理。
2 Unigram LM 如何构建词典?
- 数据预处理:将训练语料库进行预处理,包括分割句子、标记词性等
- 统计词频:统计训练样本中的每个词,得到每个词的出现频次
- 构建词表:根据词频,将训练样本的词按照频次从大到小进行排序,构建一个词表
- 构建分词模型:根据词表中每个词的出现频次,计算每个词在整个语料中出现的概率
- 分词:对于新的文本输入,利用 unigram LM 模型根据词的概率选择最可能的划分方式
📝通俗解释:想象你有一本小说,Unigram LM会统计里面每个词出现的次数,然后根据这个概率来决定如何切分新句子——哪个切法概率高就用哪个。
3 Unigram LM 具有什么优点?
- 简单高效:实现相对简单,计算效率较高,适合在大规模数据上使用
- 数据驱动:依靠训练语料库中的统计信息进行分词,具备较好的语言模型学习能力
- 高度可定制化:可以根据需要自定义不同规则,满足特定领域和任务的分词需求
📝通俗解释:Unigram LM就像一个"简单粗暴但有效"的方法——不需要复杂的模型,只要统计好词频,就能很好地完成分词任务。
4 Unigram LM 具有什么缺点?
- 上下文信息缺失:只考虑了每个词自身的出现概率,缺乏上下文信息
- 未登录词问题:对未登录词(未在训练集中出现的词)处理能力较差
- 歧义问题:某些词在不同的上下文中可能具有不同的含义,可能无法准确划分
📝通俗解释:Unigram LM的弱点是"目光短浅"——它只看到单个词的概率,看不到词与词之间的关系,所以有时候会做出错误的选择。
5 手撕 Unigram LM?
import random
from collections import defaultdict
class UnigramLM:
"""Unigram语言模型分词器"""
def __init__(self):
self.word_counts = defaultdict(int)
self.total_words = 0
def train(self, corpus):
"""训练模型:统计词频"""
with open(corpus, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
tokens = line.split()
for token in tokens:
self.word_counts[token] += 1
self.total_words += 1
def generate_sentence(self, length):
"""根据词频概率生成句子"""
sentence = []
for _ in range(length):
rand_idx = random.randint(0, self.total_words - 1)
for word, count in self.word_counts.items():
rand_idx -= count
if rand_idx < 0:
sentence.append(word)
break
return ' '.join(sentence)
# 示例用法
# corpus = "data.txt" # 训练语料文件路径
# length = 10 # 生成句子的长度
# lm = UnigramLM()
# lm.train(corpus)
# generated_sentence = lm.generate_sentence(length)
# print(generated_sentence)📝通俗解释:这个代码展示了Unigram LM的核心思想——根据词频来"随机"生成句子,出现概率高的词更容易被选中。
对比篇
1 对比一下不同分词器优缺点?
| 分词器名称 | 优点 | 缺点 |
|---|---|---|
| BPE | 能够处理未分词的文本,且容易实现和使用。根据语料频率合并字符或字符组合,生成对应的词汇表。对于语言中的常见词汇有较好的表示能力。 | 没有考虑语义信息,可能造成词汇划分的歧义。生成的词汇表可能较大,增加了存储和计算资源的需求。 |
| WordPiece | 可以处理未分词的文本,并根据语料频率合并子词或子词组合。考虑了语义信息,提供更好的词汇表示能力。适用于多语言场景。 | 生成的词汇表可能较大,增加了存储和计算资源的需求。 |
| SentencePiece | 可以通过无监督学习方式构建多语言分词器。能够根据语料自动学习语言的词汇,并生成对应的分词模型。 | 对于特定语料和应用场景,学习过程可能需要较长的时间和计算资源。生成的词汇表可能较大。 |
| Transformer Tokenizer | 基于Transformer模型,能够处理复杂的语言结构和上下文信息。生成的词嵌入能同时表示词汇和上下文,提高模型对语义和上下文关系的理解能力。 | 比较复杂,处理速度可能较慢。 |
| Unigram LM | 根据语料中词的频率构建词汇表,能够有效捕捉常见词汇。计算简单,速度较快。 | 没有考虑语义信息,可能造成词汇划分的歧义。对于非常见词汇的处理较为困难。 |
📝通俗解释:理想的分词器应该:能处理未分词的文本;考虑语义信息;词汇表大小适中;处理速度快;支持跨语言;可定制;稳定可靠。
2 举例介绍一下不同大模型LLMs的分词效果?
测试文本:男儿何不带吴钩,收取关山五十州。
| 模型 | 词表大小 | 分词结果 | Token数量 |
|---|---|---|---|
| LLaMA | 32000 | ['男', '<0xE5>', '<0x84>', '<0xBF>', '何', '不', '<0xE5>', '<0xB8>', '<0xA6>', '<0xE5>', '<0x90>', '<0xB4>', '<0xE9>', '<0x92>', '<0xA9>', ',', '收', '取', '关', '山', '五', '十', '州', '。'] | 24 |
| Chinese LLaMA | 49953 | ['男', '儿', '何', '不', '带', '吴', '钩', ',', '收取', '关', '山', '五十', '州', '。'] | 14 |
| ChatGLM-6B | 130528 | ['男儿', '何不', '带', '吴', '钩', ',', '收取', '关山', '五十', '州', '。'] | 11 |
| ChatGLM2-6B | 65024 | ['男', '儿', '何', '不', '带', '吴', '钩', ',', '收取', '关', '山', '五十', '州', '。'] | 14 |
| Bloom | 250880 | ['男', '儿', '何不', '带', '吴', '钩', ',', '收取', '关', '山', '五十', '州', '。'] | 13 |
| Falcon | 65024 | ['男', '儿', '', '', '', '不', '带', '吴', '', '', '', '', '', '收', '取', '', '', '山', '五', '十', '州', '。'] | 22 |
📝通俗解释:从结果可以看出,词表越大的模型(如ChatGLM-6B),对中文的处理越高效,token数量越少。LLaMA对中文支持较差,Falcon甚至出现了乱码。
3 介绍一下不同大模型LLMs的分词方式的区别?
| 模型 | 词表大小 | 中文平均token数 | 英文平均token数 | 中文处理时间(s) | 英文处理时间(s) |
|---|---|---|---|---|---|
| LLaMA | 32000 | 1.45 | 0.25 | 12.6 | 19.4 |
| Falcon | 65024 | 1.18 | 0.235 | 21.395 | 24.73 |
| Chinese LLaMA | 49953 | 0.62 | 0.249 | 8.65 | 19.12 |
| ChatGLM-6B | 130528 | 0.55 | 0.19 | 15.91 | 20.84 |
| ChatGLM2-6B | 65024 | 0.58 | 0.23 | 8.899 | 18.63 |
| Bloom | 250880 | 0.53 | 0.22 | 9.87 | 15.6 |
📝通俗解释:
- 词表越大,中文token数越少(效率越高),但处理时间可能增加
- ChatGLM-6B和Bloom的中文处理效率最高
- 英文处理效率各模型差异不大
- Chinese LLaMA专门针对中文优化,效果显著
总结
| 分词器 | 典型应用模型 | 核心特点 |
|---|---|---|
| BPE | GPT2, BART, LLaMA | 基于频次合并,简单高效 |
| WordPiece | BERT | 基于语言模型概率合并 |
| SentencePiece | ChatGLM, BLOOM, PaLM | 端到端无监督,支持多语言 |
| Transformer Tokenizer | BERT系列 | 结合注意力机制,上下文感知 |
| Unigram LM | ALBERT | 基于概率统计,简单快速 |
📝通俗解释:选择分词器就像选择工具——没有最好的,只有最适合的。要根据具体任务、语言特性、计算资源等因素来选择。