2024年大模型面试准备(二):LLM容易被忽略的Tokenizer与Embedding

分词和嵌入一直是LM被忽略的一部分。随着各大框架如HF的不断完善,大家对tokenization和embedding的重视程度越来越低,到现在初学者大概只能停留在调用tokenizer.encode这样的程度了。

知其然不知其所以然是很危险的。比如你要调用ChatGPT的接口,但是经常发现输出被截断了,原因可能就是你输入的句子太长了。你计算句子长度是直接用空格分词,而ChatGPT是用不同的分词法(BPE分词法)。又如你不清楚T5的tokenizer的运作机理,输入数据中存在一些特殊token如’{}',但不知道T5使用的SentencePiece tokenizer不原生支持这些token,因此发现自己生成的结果有问题,都是。

以上这些问题听上去比较低级,但是debug起来往往是最头秃的,如果只会调用接口但不明白其中的机理,相信如果面试时问起来也是答不上来的。为了解决这些疑惑,并助准备面试的人一臂之力,本文会提供一份详细的关于分词和嵌入的解答。

喜欢本文记得收藏、关注、点赞。更多技术、面试交流,加入文末我们的社群。

Tokenizer

如果大家用过HuggingFace,对tokenizer肯定不会陌生。在使用模型前,都需要将sequence过一遍tokenizer,进去的是word序列(句子),出来的是number序列。但是,HF的tokenizer到底在做什么?事实上,tokenizer总体上做三件事情:

  1. 分词
    tokenizer将字符串分为一些sub-word token string,再将token string映射到id,并保留来回映射的mapping。从string映射到id为tokenizer encode过程,从id映射回token为tokenizer decode过程。映射方法有多种,例如BERT用的是WordPiece,GPT-2和RoBERTa用的是BPE等等,后面会详细介绍。

  2. 扩展词汇表
    部分tokenizer会用一种统一的方法将训练语料出现的且词汇表中本来没有的token加入词汇表。对于不支持的tokenizer,用户也可以手动添加。

  3. 识别并处理特殊token
    特殊token包括[MASK], <|im_start|>, , 等等。tokenizer会将它们加入词汇表中,并且保证它们在模型中不被切成sub-word,而是完整保留。

分词粒度

我们首先来看一下几种不同的分词粒度。

最直观的分词是单词分词法(word base)。单词分词法将一个word作为最小元,也就是根据空格或者标点分词。举例来说,Today is Sunday用word-base来进行分词会变成[‘Today’, ‘is’, ‘Sunday’]。

最详尽的分词是单字分词法(character-base)。单字分词法会穷举所有出现的字符,所以是最完整的。在上面的例子中,单字分词法会生成[‘T’, ‘o’, ‘d’, …, ‘a’, ‘y’]。

另外还有一种最常用的、介于两种方法之间的分词法叫子词分词法,会把上面的句子分成最小可分的子词[‘To’, ‘day’, ‘is’, ‘S’, ‘un’, ‘day’]。子词分词法有很多不同取得最小可分子词的方法,例如BPE(Byte-Pair Encoding,字节对编码法),WordPiece,SentencePiece,Unigram等等。

接下来我们具体看看各大主流模型用的是什么分词法。

GPT族:Byte-Pair Encoding (BPE)

从GPT-2开始一直到GPT-4,OpenAI一直采用BPE分词法。这种方法的子词构造算法是这样的:

1. 统计输入中所有出现的单词并在每个单词后加一个单词结束符</w> -> ['hello</w>': 6, 'world</w>': 8, 'peace</w>': 2]
2. 将所有单词拆成单字 -> {'h': 6, 'e': 10, 'l': 20, 'o': 14, 'w': 8, 'r': 8, 'd': 8, 'p': 2, 'a': 2, 'c': 2, '</w>': 3}
3. 合并最频繁出现的单字(l, o) -> {'h': 6, 'e': 10, 'lo': 14, 'l': 6, 'w': 8, 'r': 8, 'd': 8, 'p': 2, 'a': 2, 'c': 2, '</w>': 3}
4. 合并最频繁出现的单字(lo, e) -> {'h': 6, 'lo': 4, 'loe': 10, 'l': 6, 'w': 8, 'r': 8, 'd': 8, 'p': 2, 'a': 2, 'c': 2, '</w>': 3}
5. 反复迭代直到满足停止条件

显然,这是一种贪婪的算法。在上面的例子中,'loe’这样的子词貌似不会经常出现,但是当语料库很大的时候,诸如est,ist,sion,tion这样的特征会很清晰地显示出来。

在获得子词词表后,就可以将句子分割成子词了,算法见下面的例子:

# 给定单词序列
["the</w>", "highest</w>", "mountain</w>"]

# 从一个很大的corpus中排好序的subword表如下
# 长度 6         5           4        4         4       4          2
["errrr</w>", "tain</w>", "moun", "est</w>", "high", "the</w>", "a</w>"]

# 迭代结果
"the</w>" -> ["the</w>"]
"highest</w>" -> ["high", "est</w>"]
"mountain</w>" -> ["moun", "tain</w>"]

注意,在上述算法执行后,如果句子中仍然有子字符串没被替换, 但所有subword都已迭代完毕,则将剩余的子词替换为特殊token,如 。从这里大家也可以发现了,原则上这个token出现的越少越好,所以我们也往往用的数量来评价一个tokenizer的好坏程度,这个token出现的越少,tokenizer的效果往往越好。

管中窥豹,根据BPE算法,我们可以发现,tokenizer基本上是无法并行的,因为存在大量if-else的branch。学过GPU Programming的同学应该知道,conditional branch越多,GPU提供的加速越有限,有时候还会造成负加速,因为数据传输有很大开销。这就是为什么在tokenizing的时候,我们看到GPU util都是0。

BERT族:Word-Piece

Word-Piece和BPE非常相似,BPE使用出现最频繁的组合构造子词词表,而WordPiece使用出现概率最大的组合构造子词词表。换句话说,WordPiece每次选择合并的两个子词,通常在语料中以相邻方式同时出现。比如说 P(ed) 的概率比P(e) + P(d)单独出现的概率更大(可能比他们具有最大的互信息值),也就是两个子词在语言模型上具有较强的关联性。这个时候,Word-Piece会将它们组合成一个子词。

有关BERT的tokenizer还有一个重点:BERT在使用Word-Piece时加入了一些特殊的token,例如[CLS]和[SEP]。我们可以自己试一下:

inputs = 'Coolest pretrained Asmita!'
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
input_ids = tokenizer.encode(inputs,return_tensors='pt')
print(input_ids)
for input_id in input_ids [0]:
    print(tokenizer.decode(input_id))

# outputs
tensor([[101, 4658435536532365420982004229302050999102]])
[CLS] 
cool
##est
pre
##train
##ed 
as
##mit
##a
!
[SEP]

发现BERT在句首加上了[CLS],句尾加上了[SEP],而且对coolest做了子词分解,对词根est加上了##来表示这是一个后缀。对于没有出现在词汇表里的单词例如asmita(是个印度人名),BERT所用的Word-Piece tokenizer会将它分解为Word-Piece算法形成的子词词汇表中存在的as,mit和a,组成一个子词。

多语言支持:Sentence-Piece

大家在使用HF的时候有时候会提示安装Sentence-Piece,这个包其实是HF里面大量模型会调用的包,例如ALBERT,XLM-RoBERTa和T5:

图片

这个包主要是为了多语言模型设计的,它做了两个重要的转化:

  1. 以unicode方式编码字符,将所有的输入(英文、中文等不同语言)都转化为unicode字符,解决了多语言编码方式不同的问题。

  2. 将空格编码为‘_’, 如’New York’ 会转化为[‘_’, ‘New’, ‘_York’],这也是为了能够处理多语言问题,比如英文解码时有空格,而中文没有, 类似这种语言区别。

词汇表不全问题

但是,也是因为这两个转化,SentencePiece的tokenizer往往会出现词汇表不全的问题。下面是部分SentencePiece中可能出现的问题:

图片

如果某个token被识别成,那它就无法与其他也被识别成的token区分开来。例如在训练的时候有大量{hello world}的样本,在输出的时候就会变成 hello world 的样本。

这些问题不存在于WordPiece中。这是因为SentencePiece需要对多语言情况进行优化,有些token迫不得已要被删掉。想要加上某些本来tokenizer中不存在的token,可以使用add_tokens()方法。

inputs = '{ }'
# tokenizer = BartTokenizer.from_pretrained('facebook/bart-base')
tokenizer=T5Tokenizer.from pretrained('t5-small')
tokenizer.add_tokens(['{''}'])
input_ids = tokenizer.encode(inputs,return_tensors='pt') 
print(input_ids) 
for input_id in input_ids[0]:
    print(tokenizer.decode(input_id)) 

# save the tokenizer 
tokenizer.save_pretrained('tokenizer')

# outputs
tensor([[32100321011]]) 
{
}
</s>

使用后,保存的文件夹里面会出现一个added_tokens.json文件,里面就包含这两个新的token, 如下所示。这个时候再load这个tokenizer,这两个token就可以被模型识别了。

{
  "{": 32100,
  "}": 32101
}

这之后,还需要告诉模型我已经更新了词汇表,使用model.resize_token_embeddings(len(tokenizer))。完整的添加例子如下:

from transformers import AutoTokenizer, AutoModel

# pick the model type
model_type = "roberta-base"
tokenizer = AutoTokenizer.from_pretrained(model_type)
model = AutoModel.from_pretrained(model_type)

# new tokens
new_tokens = ["new_token"]

# check if the tokens are already in the vocabulary
new_tokens = set(new_tokens) - set(tokenizer.vocab.keys())

# add the tokens to the tokenizer vocabulary
tokenizer.add_tokens(list(new_tokens))

# add new, random embeddings for the new tokens
model.resize_token_embeddings(len(tokenizer))

词汇表不全的问题在Word-Piece和BPE中也存在,在使用tokenizer的时候要格外小心。

各路语言模型中的tokenizer

各大LM用的tokenizer和对应的词汇表大小如下:

图片

另外,有大佬做了各大LLM的词汇表大小和性能:

图片

更多关于tokenizer的知识可以参见huggingface的官方博客:https://huggingface.co/docs/transformers/tokenizer_summary

Embedding

tokenize完的下一步就是将token的one-hot编码转换成更dense的embedding编码。在ELMo之前的模型中,embedding模型很多是单独训练的,而ELMo之后则爆发了直接将embedding层和上面的语言模型层共同训练的浪潮(ELMo的全名就是Embeddings from Language Model)。不管是哪种方法,Embedding层的形状都是一样的。我们举个例子来看看embedding层是怎么工作的。在HuggingFace中,seq2seq模型往往是这样调用的:

input_ids = tokenizer.encode('Hello World!', return_tensors='pt')
output = model.generate(input_ids, max_length=50)
tokenizer.decode(output[0])

上面的代码主要涉及三个操作:tokenizer将输入encode成数字输入给模型,模型generate出输出数字输入给tokenizer,tokenizer将输出数字decode成token并返回。

例如,如果我们使用T5TokenizerFast来tokenize ‘Hello World!’,则:

  1. tokenizer会将token序列 [‘Hello’, ‘World’, ‘!’] 编码成数字序列[8774, 1150, 55, 1],也就是[‘Hello’, ‘World’, ‘!’, ‘’],在句尾加一个表示句子结束。

  2. 这四个数字会变成四个one-hot向量,例如8774会变成[0, 0, …, 1, 0, 0…, 0, 0],其中向量的index为8774的位置为1,其他位置全部为0。假设词表里面一共有30k个可能出现的token,则向量长度也是30k,这样才能保证出现的每个单词都能被one-hot向量表示。

  3. 也就是说,一个形状为 (4) 的输入序列向量,会变成形状为 (4, 30k) 的输入one-hot向量。为了将每个单词转换为一个word embedding,每个向量都需要被被送到embedding层进行dense降维。

  4. 现在思考一下,多大的矩阵可以满足这个要求?没错,假设embedding size为768,则矩阵的形状应该为(30k, 768) ,与BERT的实现一致:

BertForSequenceClassification(
  (bert):BertModel(
    (embeddings):BertEmbeddings(
      (word_embeddings):Embedding(30522768,padding_idx=0)
      (position_embeddings):Embedding(512768) 
      (token_type_embeddings):Embedding(2768)
      (LayerNorm): LayerNorm((768,),eps=1e-12,elementwise_affine=True)
      (dropout): Dropout(p=0.1, inplace=False) 
    )
  )
)

理解Embedding矩阵

Embedding矩阵的本质就是一个查找表。由于**输入向量是one-hot的,embedding矩阵中有且仅有一行被激活。**行间互不干扰。这是什么意思呢?如下图所示,假设词汇表一共有6个词,则one-hot表示的长度为6。现在我们有三个单词组成一个句子,则输入矩阵的形状为 (3, 6)。然后我们学出来一个embedding矩阵,根据上面的推导,如果我们的embedding size为4,则embedding矩阵的形状应该为(6, 4)。这样乘出来的输出矩阵的形状应为(3, 4)。

在这里插入图片描述

图中用不同颜色标明了三个subword embedding分别的计算过程。对于第一个单词’I’,假设其one-hot编码为 [0, 0, 1, 0, 0, 0],将其与embedding矩阵相乘,相当于取出embedding矩阵的第3行(index为2)。同理,对于单词’love’,相当于取出embedding矩阵的第二行(index为1)。这样一来大家就理解了,embedding矩阵的本质是一个查找表,每个单词会定位这个表中的某一行,而这一行就是这个单词学习到的在嵌入空间的语义。

技术交流群

前沿技术资讯、算法交流、求职内推、算法竞赛、面试交流(校招、社招、实习)等、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企开发者互动交流~

我们建了大模型算法岗技术与面试交流群, 想要进交流群、需要源码&资料、提升技术的同学,可以直接加微信号:mlc2040。加的时候备注一下:研究方向 +学校/公司+CSDN,即可。然后就可以拉你进群了。

方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2040,备注:技术交流

用通俗易懂方式讲解系列

  • 大模型面试宝典》(2024版) 正式发布!

  • 大模型实战宝典》(2024版)正式发布!

  • 用通俗易懂的方式讲解:自然语言处理初学者指南(附1000页的PPT讲解)

  • 用通俗易懂的方式讲解:1.6万字全面掌握 BERT

  • 用通俗易懂的方式讲解:NLP 这样学习才是正确路线

  • 用通俗易懂的方式讲解:28张图全解深度学习知识!

  • 用通俗易懂的方式讲解:不用再找了,这就是 NLP 方向最全面试题

  • 用通俗易懂的方式讲解:实体关系抽取入门教程

  • 用通俗易懂的方式讲解:灵魂 20 问帮你彻底搞定Transformer

  • 用通俗易懂的方式讲解:图解 Transformer 架构

  • 用通俗易懂的方式讲解:大模型算法面经指南(附答案)

  • 用通俗易懂的方式讲解:十分钟部署清华 ChatGLM-6B,实测效果超预期

  • 用通俗易懂的方式讲解:内容讲解+代码案例,轻松掌握大模型应用框架 LangChain

  • 用通俗易懂的方式讲解:如何用大语言模型构建一个知识问答系统

  • 用通俗易懂的方式讲解:最全的大模型 RAG 技术概览

  • 用通俗易懂的方式讲解:利用 LangChain 和 Neo4j 向量索引,构建一个RAG应用程序

  • 用通俗易懂的方式讲解:使用 Neo4j 和 LangChain 集成非结构化知识图增强 QA

  • 用通俗易懂的方式讲解:面了 5 家知名企业的NLP算法岗(大模型方向),被考倒了。。。。。

  • 用通俗易懂的方式讲解:NLP 算法实习岗,对我后续找工作太重要了!。

  • 用通俗易懂的方式讲解:理想汽车大模型算法工程师面试,被问的瑟瑟发抖。。。。

  • 用通俗易懂的方式讲解:基于 Langchain-Chatchat,我搭建了一个本地知识库问答系统

  • 用通俗易懂的方式讲解:面试字节大模型算法岗(实习)

  • 用通俗易懂的方式讲解:大模型算法岗(含实习)最走心的总结

  • 用通俗易懂的方式讲解:大模型微调方法汇总


http://www.niftyadmin.cn/n/5448155.html

相关文章

1.3.2 你好,矩形

元素缓冲对象&#xff08;Element Buffer Object&#xff0c;EBO&#xff09; 元素缓冲对象也叫索引缓冲对象&#xff08;Index Buffer Object&#xff0c; IBO&#xff09;&#xff0c;EBO是一个缓冲区&#xff0c;它存储OpenGL用来决定要绘制哪些顶点的索引。 假设要绘制一…

用眼过度?职场人必看的护眼攻略

在职场中&#xff0c;无论是面对电脑、手机还是各种文档&#xff0c;用眼过度已经成为了一个普遍现象。长时间用眼不仅会导致眼睛疲劳、干涩、视力下降&#xff0c;还可能引发其他眼部疾病。因此&#xff0c;掌握正确的护眼方法对于职场人来说至关重要。本文将为大家介绍一些实…

Java二阶知识点总结(七)SVN和Git

SVN 1、SVN和Git的区别 SVN是集中式的&#xff0c;也就是会有一个服务器保存所有代码&#xff0c;拉取代码的时候只能从这个服务器上拉取&#xff1b;Git是分布式的&#xff0c;也就是说每个人都保存有所有代码&#xff0c;如果要获取代码&#xff0c;可以从其他人手上获取SV…

@Transactional注解失效场景以及解决方法

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:说一说@Transactional注解失效的场景以及解决方法 @Transactional 是 Spring 框架提供的一个注解,用于声明事务的边界。它可以应用于类、方法或接…

老司机都懂的!【打赏】完美运营的最新视频打赏系统

完美运营的最新视频打赏系统优于市面上95%的打赏系统&#xff0c;与其他打赏系统相比&#xff0c;功能更加强大&#xff0c;完美运营且无bug。支付会调、短链接生成、代理后台、价格设置和试看功能等均没有问题。 以上为原简介&#xff0c;经测试验证。成功搭建并可以正常进入…

牛客网python练习题库记录

python格式化输出 python 读入整数数字并且换行输出 python规范输出小数点后几位 afloat(input()) format_a{.2f}.format(a) print(format_a) 小数化整数 afloat(input()) bint(a) print(b) 为整数增加小数点 input_integer int(input()) float_number float(input…

MySQL数据库的下载和安装以及命令行语法学习

MySQL数据库的下载和安装以及命令行语法学习 学习MYSQL&#xff0c;掌握住基础的SQL句型&#xff08;创建数据库、查看数据库列表、数据增、删、改、查等操作类型&#xff09; 首先要知道MySQL下载和安装方法&#xff1a; 提示&#xff1a;别嫌啰嗦&#xff0c;对于一个初识MY…

innodb 的 buffer pool 管理 page

page 页结构 page是整个InnoDB存储的最基本构件&#xff0c;也是InnoDB磁盘管理的最小单位&#xff0c;与数据库相关的所有内容都存储在这种Page结构里。 Page分为几种类型&#xff0c;常见的页类型有 数据页(Btree Node)Undo页(Undo Log Page)系统页(System Page)事务数据页 …