在学习分词器相关技术的时候偶然看到一个讲解视频,觉得很惊艳,于是写篇博客记录一下
视频地址:LLM分词器相关技术介绍
文本分词
这个阶段的目标是对文本进行分词和嵌入,以便用于LLM
text = "Hello, world. This, is a test."
preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text) #对文本根据各种标点符号进行切割
preprocessed = [item.strip() for item in result if item.strip()] #去掉空白字符并做成词汇表
print(len(result))
将词元转换为词元IDs
从这些词元中,我们可以构建一个词汇表,它包含了所有独特的词元
all_words = sorted(list(set(preprocessed)))
vocab_size = len(all_words)
print(vocab_size)
将所有这些整合到一个分词器类中(tokenizer class)中
class SimpleTokenizerV1:
def __init__(self, vocab):
self.str_to_int = vocab
self.int_to_str = {i:s for s,i in vocab.items()}
def encode(self, text):
preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
ids = [self.str_to_int[s] for s in preprocessed]
return ids
def decode(self, ids):
text = " ".join([self.int_to_str[i] for i in ids])
# Replace spaces before the specified punctuations
text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
return text
添加特殊上下文词元
一些新文本中的词可能不在词汇表中,这种情况被称为OOV。
一些分词器使用特殊词元来帮助LLM获取额外上下文信息
- [BOS]标志文本的开始
- [EOS]标记文本结束的位置(通常用于连接多个不相关的文本,如两篇不同的维基百科文章或者两本不同的书等)
- [PAD]如果我们训练LLM的批量大于1(我们可能会包含多个长度不同的文本;使用填充标记,我们会将较短的文本填充为最长的文本,这样所有文本的长度就相等了)
- [UNK]代表未列入词汇表的单词
BPE分词器
实现原理
- 反复合并最常见的字符对,逐渐生成常见字词单元,将所有词汇表示为这些字词单元的组合
- 中文的最小单位为汉字,初始词汇表为汉字
- 英文的最小单位为字母,初始词汇表为字母
- 中文以句子为单位统计相邻的词汇频率,英文以单词为单位统计相邻的词汇频率
- 当两个字符合并为一个新的字词单元后,原来的两个单个字符通常会保留在词汇表
- 统计所有字符对的出现频率,采用贪心算法将频率最高的字符进行合并
- 例如,合并"wo"后,词汇表将增加"wo",而"word"将被表示为['wo','r','d']
- 不断重复统计频率和合并,直到达到预定的词汇表大小或达到指定的合并次数为止
WordPiece分词器
- 可以有效处理OOV的词汇
- 使用了基于联合概率的策略,特别适合上下文相关的任务,如语言建模和预训练模型
- 当出现OOV的情况,WordPiece会将OOV词分解为更小的字词(subword)或者字符(character);如果OOV词无法分解为已知的字词或者字符,WordPiece会将其替换为[UNK] BPE和WordPiece都是字词分词方法,BPE是根据字符对频率进行合并,而WordPiece是根据语言模型概率合并,生成的字词可能更细粒度(例如,"Hello"经过BPE可能被保留为一个完整的字词,而经过WP可能会被拆分为["hel", "##lo"])