技术详解:BERT的分词预处理、输入Embedding、中间编码与输出向量解析

BERT是一个充分利用上下文的编码器,能够以文本中各个字/词(token)的原始词向量为输入,输出文本中各个字/词(token)融合了全文语义信息后的向量表示,并且在每一层都可以单独输出一个向量进行下游任务使用。

图片

从构成上来说,一个基本BERT模型包括BertEmbeddings、BertEncoder、BertPooler三个基本组成部分。

对应的,本文分别从BERT的输入输出逻辑、BERT输入前的分词器、BERT输入中的input Embeddings、BERT处理中的Transformer建模、BERT输出后的效果评估等几个方面进行分析。

一、BERT的输入输出逻辑

BERT模型的主要输入是文本中各个字/词(token)的原始词向量,该向量既可以随机初始化,也可以利用 Word2Vector等算法进行预训练以作为初始值,输出是文本中各个字/词(token)融合了全文语义信息后的向量表示。

在这里插入图片描述

1、BERT的输入

使用BERT预训练模型最多只能输入512个字符,最多只能两个句子合成一句,在处理逻辑上,针对输入的文本,先进行进行分词,然后利用bert固定的词库将切分好的token映射为对应的ID,并逐步获取其对应的向量表示。

而最基本的处理,就是对输入进行切分,具体地: 将句子切分为tokens->在句子的开头添加[CLS] token->在句子的末尾添加[SEP] token->用[PAD] token填充句子,以使总长度等于最大长度->将每个token转换为模型中相应的ID->创建注意掩码,以明确区分真实token和[PAD]token BertTokenizer是其中输入处理的重要模块,如下图所示。图片

例如,对于句子“here is some text to encode”: 1)from_pretrained: 从包含词表文件(vocab.txt)的目录中初始化一个分词器;

2)tokenize: 将文本(词或者句子)分解为子词列表:
[‘[CLS]’, ‘here’, ‘is’, ‘some’, ‘text’, ‘to’, ‘en’, ‘##code’, ‘[SEP]’];
3)convert_tokens_to_ids: 将子词列表转化为子词对应下标的列表;例如,vocab.txt中第0个token是[pad],第101个token是[CLS],第102个token是[SEP],得到的 [101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102];
4)encode: 对于单个句子输入,分解词并加入特殊词形成“[CLS], x, [SEP]”的结构并转换为词表对应下标的列表;对于两个句子输入(多个句子只取前两个),分解词并加入特殊词形成“[CLS], x1, [SEP], x2, [SEP]”的结构并转换为下标列表;

2、BERT的输出

在输出上个,bert有model.get_pooled_out()、model.get_sequence_out()两种方式。

第一种输出[CLS]的表示,输出shape是[batch size,hidden size]。

第二种获取的是整个句子每一个token的向量表示,输出shape是[batch_size, seq_length, hidden_size],model.get_sequence_output()结果中头尾是[CLS]和[SEP]的向量。

二、BERT输入前的字符资源

当使用预训练bert时,词库大小已经固定,不同的模型会有不同的词表大小,例如bert-base-uncased中,包括30522个字符,中文bert(chinese_L-12_H-768_A-12/vocab.txt)一般为21168个字符。

1、词表中的特殊字符

而在这个vacab词表中,存在许多特殊字符,

[unused1]:BERT-base预训练参数中给出的vocab中预留了很多个[unused]标记,例如bert-base-uncased中,包括[unused0]至[unused993]共994个占位符。这部分unused的token是从未训练过的但是他们在bert模型的token embedding中都有一个随机初始化,从来没训练过的token embedding向量对应。

当下游finetune任务时,预留[unused]的情况下,想要少量扩充词表,直接覆盖需要的[unused]即可,类比于[sep]、[cls]等标签,可以减少引入更多维度的embedding向量,如果没有预留[unused],当我们pretrain时需要将词表中单词个数从N扩充M,直接使用unused的token对应的embedding即可

[UNK]: 为了input的句子里有embedding矩阵没有的token、分token的时候,这类token都会变成UNK,同样bert的embedding矩阵里也有一行embedding向量专门用来表示UNK。
[CLS]: 默认是给句子配一个[cls]和一个[seq],分别在句首和句尾,[cls]表示一个句子的句首。在第一句前会加一个[CLS]标志,最后一层该位对应向量可以作为整句话的语义表示,从而用于下游的分类任务等。[CLS]位本身没有语义,经过12层,得到的是attention后所有词的加权平均,相比其他正常词,可以更好的表征句子语义。
[SEP]: SEP用来区分两个句子。
[PAD]: 预训练模型只能接受长度相同的输入,所以用[pad]让所有短句都能够对齐,长句就直接做截断。
[MASK]: [mask]主要是为了MLM任务,Bert的embedding向量中有专门存放[mask]token的embedding向量,设计的目的也是为了保证程序正常运行,如果embedding layer里没有[mask]则程序报错

**##字符:**这是经过wordpiecetokenize后形成的子词。例如tokenizer这个词就可以拆解为“token”和“##izer”两部分,注意后面一个词的“##”表示接在前一个词后面。

2、OOV问题下Vocab词表的扩充

由于模型是在特定语料库上进行预训练的,因此词汇表也已固定。换句话说,当我们将预训练模型应用于其他一些数据时,新数据中的某些标记可能不会出现在预训练模型的固定词汇表中,这通常称为词汇不足(OOV)问题。

针对这类问题,有三种解决方法:

第一种是使用WordPiece算法,该算法将一个单词分解为几个子单词,如果还找不到,则使用[UNK]符号进行代替,Wordpiece算法能够把词的本⾝的意思和前缀、后缀分开,使最终的词表变得精简。

第一种是直接替换vocab.txt中的[unused],不过,其中预留的符号并不多,中文只有100个unused的位置,想知道新增的词超过100或者unused*位置都用光了,则必须要用到第二种方式。

第三种是加入新的token,使用tokenizer.add_special_tokens([str(unk_token)])函数加入字符,然后resize_token_embeddings(len(tokenizer))。

3、对于长文本BERT的处理

BERT最多只能处理512个字符,对于这类问题,需要专门进行处理。

1)head-only: 保存前 510 个 token (留两个位置给 [CLS] 和 [SEP] )

2)tail-only: 保存最后 510 个token

3)head + tail : 选择前128个 token 和最后382个 token(文本在800以内)或者前256个token+后254个token(文本大于800tokens)

4)slide_window: 滑动窗口。即把文档分成有重叠的若干段,然后每一段都当作独立的文档送入BERT进行处理

三、BERT输入前的分词器

BERT中的tokenization.py是预处理进行分词的程序,主要有BasicTokenizer、WordpieceTokenizer、FullTokenizer三个分词器。

>>> from transformers import BertTokenizer
>>> tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
>>> tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
>>> tokens = tokenizer.tokenize("今天是2021年11月28日")
>>> tokens
['[UNK]', '天', '[UNK]', '202', '##1', '年', '11', '月', '28', '日']>>> ids = tokenizer.convert_tokens_to_ids(tokens)
>>> ids
>[100, 1010, 100, 17881, 1475, 1026, 1429, 1037, 1743, 1033]
>>> decoded_string = tokenizer.decode([100, 1010, 100, 17881, 1475, 1026, 1429, 1037, 1743, 1033])
>>> decoded_string
>'[UNK] 天 [UNK] 2021 年 11 月 28 日'

1、BasicTokenizer

BasicTokenizer对于一个待分词字符串,做一些基础的大小写、unicode转换、标点符号分割、小写转换、中文字符分割、去除重音符号等操作,最后返回的是关于词的数组(中文是字的数组)。
在这里插入图片描述

1)转unicode: convert_to_unicode(text)该方法将输入转化为unicode字符串输出;
2)去除奇怪字符: self._clean_text(text)去除一些无法表示的字符(码位为0或0xfffd,或一些非\t \r \n的控制字符),同时将空白符(\r \t \n)转化为可见的空格字符输出;
3)在中文字符前后加上空格: self._tokenize_chinese_chars(text)通过self._is_chinese_char(cp)判断字符是否是中文字符,若是,则在前后加上可见空格符 ;
4)通过空格分词通过空格字符进行分词: 本质上就是text.strip().split(),去除多余accents字符,以及根据标点再次进行分词;

5)去除多余accents字符: self._run_strip_accents(text),即将á变为a;
6)根据标点再次进行分词: self._run_split_on_punc(text),再次进行空格分词,标点符号包括

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~。  

2、WordpieceTokenizer

WordpieceTokenizer基于词表vocabulary进行再一次切分,得到子词subword。subword介于char和word之间,既在一定程度保留了词的含义,又能够照顾到英文中单复数、时态导致的词表爆炸和未登录词的OOV问题,将词根与时态词缀等分割出来,从而减小词表,也降低了训练难度。

在切分上,采用最大正向匹配匹配方法,从左到右的顺序,将一个词拆分成多个子词,每个子词尽可能长,例如tokenizer这个词就可以拆解为“token”和“##izer”两部分,注意后面一个词的“##”表示接在前一个词后面。

例如,下图展示了切词的一个过程:

在这里插入图片描述

1)从第一个位置开始,由于是最长匹配,结束位置需要从最右端依次递减,所以遍历的第一个子词是其本身unaffable,该子词不在词汇表中;
2)结束位置左移一位得到子词unaffabl,同样不在词汇表中;
3)重复这个操作,直到un,该子词在词汇表中,将其加入output_tokens,以第一个位置开始的遍历结束;
4)跳过un,从其后的a开始新一轮遍历,结束位置依然是从最右端依次递减,但此时需要在前面加上##标记,得到##affable不在词汇表中;
5)结束位置左移一位得到子词##affabl,同样不在词汇表中;
6)重复这个操作,直到##aff,该字词在词汇表中,将其加入output_tokens,此轮遍历结束;
7)跳过aff,从其后的a开始新一轮遍历,结束位置依然是从最右端依次递减。##able在词汇表中,将其加入output_tokensable后没有字符了,整个遍历结束。

3、FullTokenizer

FullTokenizer是BasicTokenizer和WordpieceTokenizer的结合,先进行 BasicTokenizer得到一个分得比较粗的token列表,然后再对每个token进行一次WordpieceTokenizer,得到最终的分词结果。

在这里插入图片描述

四、BERT输入中的input Embeddings

BertEmbeddings类中BERT中的输入表示包括token embedding、segment embedding、position embedding三种,输入embedding由三种embedding相加得到,经过layernorm 和dropout后输出。

在这里插入图片描述

如上述的代码可以看到:input_ids、token_type_ids、position_ids是三分重要的参数,其中:

1)input_ids标记编码:[101, 146, 112, 182, 6949, 1158, 15642, 1116, 119, 102, 0, 0],标记编码就是上面的序列中每个标记转成编码后得到的向量;
2)position_id位置编码:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],位置编码记录每个标记的位置;;
3)token_type_ids句子位置编码:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],句子位置编码记录每个标记属于哪句话,0是第一句话,1是第二句话(注意:[CLS]标记对应的是0);
4)input_mask注意力掩码:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],注意力掩码记录某个标记是否是填充的,1表示非填充,0表示填充。这个用于attention_mask,其需要通过mask矩阵得到哪些位置有词。

下面对具体的embedding进行介绍:

1、token embedding

token embedding,BERT 模型通过查询字向量表将文本中的每个字转换为为词表大小x768维,使用embedding_lookup函数,将input_ids进行转换。

1)初始化词嵌入矩阵矩阵大小及 [vocab_size, embedding_size] 词表大小*词向量维度。
2)输入input_ids和嵌入矩阵相乘得到输入的嵌入矩阵,再reshape成[batch_size, seq_length, embedding_size]输出。

2、segment embedding

BERT的输入包括两种情况,单个句子或者一个句子对,segment embedding是为了区分第一个句子和第二个句子,segment embedding大小,也叫token_type_embeddings,为句子个数(type_vocab_size)x768维,BERT中的type_vocab_size设置为2,使用token_type_ids作为输入。

以“[CLS]今天心情好去哪玩[SEP]回江西吧[SEP]”为例:
“[CLS]今天心情好去哪玩[SEP]”中每一个字的segment embedding是一个1x768的向量,其中每一个向量元素全为0;
“回江西吧[SEP]”中每一个字的segment embedding是一个1x768的向量,其中每一个向量元素全为1。

在计算上,token_type_ids 输入大小是 [batch_size, seq_length],对应的嵌入矩阵是[token_type_vocab_size, embedding_size], 输入之后和嵌入矩阵相乘在reshape得到[batch_size, seq_length, embedding_size]

3、position embedding

因为Transformer 在结构上不能识别来自不同位置的 token,position embedding的作用是为了记录句子中字词的顺序信息。

在实现上, position embedding的shape为[max_positition_embeddings, embedding],max_positition_embeddings最长为512,在处理过程中根据seq_length直接截取前seq_length,此时矩阵大小是[seq_length,embedding]。

五、BERT处理中的Transformer建模

BERT中,使用了经典的Transformer-encoder结构进行建模,对于预先训练好的模型,通常会基于Config的模型参数加载,然后

1、基于Config的模型参数加载

初始化一个BERT模型,如configuration_bert.py,需要将bert的模型参数进行加载,参数信息包括dropout, hidden_size, num_hidden_layers, vocab_size等。以bert-base-uncased为例,配置信息bert_config.json中包括:

在这里插入图片描述

2、使用transformer进行建模

使用transformer-encoder中,创建attention_model,增加一个全连接层,构建残差网络将输入加到attention输出,再进行layer_norm,最终输出。有多少层,循环多少次。

具体的,通过定义 BertEncoder结构,然后将这个encoder层复制了num_hidden_layers 层,最后将这N层网络堆叠在一起组成 BertEncoder模块的网络结构。

在这里插入图片描述

3、使用BertPooler进行全连接建模

BertPooler模块本质上就是一个全连接层网络,接在bert的堆叠encoder层后,输出相应的编码结果。
在这里插入图片描述

六、BERT输出后的效果评估

通过加载一个预训练过的BERT模型,对一个文本进行编码,可以为文本每一个token生成768维的向量。利用这个向量,可以[CLS]token的768维向量作为整个句子向量的表示,也可以将所有token的向量进行求平均(类似于word2vec的做法),适用于文本聚类或者相似度计算等任务;又如,可以把第一个[CLS]token的768维向量,接一个线性层,然后进行分类。

因此,下游任务可以通过精调(改变预训练模型参数)或者特征抽取(不改变预训练模型参数,只是把预训练模型的输出作为特征输入到下游任务)两种方式进行使用。

下面的简短代码展示了一个使用transformers进行文本编码的过程:

在这里插入图片描述

实际上,当前对于bert输出的向量,哪一层是有效的,是个有趣的课题,例如,论文里验证了6种选择的方式,并在在命名实体识别任务中的效果,接近于微调 BERT模型的效果。图片

此外,对一bert每一层都学到了什么语义信息,《What does BERT learn about the structure of language?》一文通过bert各个层得到representation后利用t-sne进行可视化,可以发现浅层的representation会更加有利于短语级别的表征,也倾向于把属于同一个类别的chunk划分到同一个接近的语义空间,而高层layer的表征已经不再具有区分度。

图片

bert从浅层到高层可以分别学习到surface,短语级别的,句法级别的,和语义级别的信息,例如通过self-attention中的一些head学习到的权重,重建出来的dependency tree。

七、总结

本文分别从BERT的输入输出逻辑、BERT输入前的分词器、BERT输入中的input Embeddings、BERT处理中的Transformer建模、BERT输出后的效果评估等几个方面进行解读。

本文从参考文献中借鉴了优秀的内容,感谢前人整理的工作。

如何学习大模型 AI ?

由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

但是具体到个人,只能说是:

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段(10天):初阶应用

该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

  • 大模型 AI 能干什么?
  • 大模型是怎样获得「智能」的?
  • 用好 AI 的核心心法
  • 大模型应用业务架构
  • 大模型应用技术架构
  • 代码示例:向 GPT-3.5 灌入新知识
  • 提示工程的意义和核心思想
  • Prompt 典型构成
  • 指令调优方法论
  • 思维链和思维树
  • Prompt 攻击和防范

第二阶段(30天):高阶应用

该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

  • 为什么要做 RAG
  • 搭建一个简单的 ChatPDF
  • 检索的基础概念
  • 什么是向量表示(Embeddings)
  • 向量数据库与向量检索
  • 基于向量检索的 RAG
  • 搭建 RAG 系统的扩展知识
  • 混合检索与 RAG-Fusion 简介
  • 向量模型本地部署

第三阶段(30天):模型训练

恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

  • 为什么要做 RAG
  • 什么是模型
  • 什么是模型训练
  • 求解器 & 损失函数简介
  • 小实验2:手写一个简单的神经网络并训练它
  • 什么是训练/预训练/微调/轻量化微调
  • Transformer结构简介
  • 轻量化微调
  • 实验数据集的构建

第四阶段(20天):商业闭环

对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

  • 硬件选型
  • 带你了解全球大模型
  • 使用国产大模型服务
  • 搭建 OpenAI 代理
  • 热身:基于阿里云 PAI 部署 Stable Diffusion
  • 在本地计算机运行大模型
  • 大模型的私有化部署
  • 基于 vLLM 部署大模型
  • 案例:如何优雅地在阿里云私有部署开源大模型
  • 部署一套开源 LLM 项目
  • 内容安全
  • 互联网信息服务算法备案

学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值