BERT技术详细介绍 |
文章目录
- 一. BERT整体模型架构
- 1.1. Attention机制
- 1.2. 基础架构-Transformer中的Encoder
- 1.3. BERT输入的三部分
- 二. 如何做BERT预训练(参数+MLM+NSP)
- 2.1. MLM(Mask Language Model)
- 2.2. NSP(Next Sentence Prediction)
- 三. 如何微调BERT,提升BERT在下游任务中的效果
- 3.1. 如何微调BERT
- 3.2. 提升BERT在下游任务中的效果
- 四. Transformer包中Bert
- 五. Bert生成句向量
- 5.1. 文本处理
- 5.1.1. 输入文本是两个sentence
- 5.1.2. 输入文本是一个sentence
- 5.2. 构建BertModel
- 5.2.1 完整代码
- 六. 参考
- 使用BERT模型解决NLP任务需要分为两个阶段:
- pre-train阶段:用大量的无监督文本通过自监督训练的方式进行训练,把文本中包含的 语言知识(包括:词法、语法、语义等特征) 以 参数的形式编码到 Transformer-encoder layer 中。预训练模型学习到的是文本的 通用知识,不依托于某一项NLP任务;
- fine-tune阶段:使用预训练的模型,在 特定的任务 中进行微调,得到用于解决该任务的定制模型;
一. BERT整体模型架构
- BERT全名Bidirection Encoder Representations from Transformers,是谷歌于2018年发布的 NLP领域的预训练模型,一经发布就霸屏了NLP领域的相关新闻,味道是真香。
- 从名字就可以看出,BERT模型是使用 双向Transformer模型的Encoder Layer 进行特征提取(BERT模型中没有 Decoder部分)。
- Transformer是组成BERT的核心模块,而Attention机制又是Transformer中最关键的部分,因此,下面我们从Attention机制开始,介绍如何利用Attention机制构建Transformer模块,在此基础上,用多层Transformer组装BERT模型。
1.1. Attention机制
- Attention机制的中文名叫“注意力机制”,顾名思义,它的主要作用是让神经网络把“注意力”放在一部分输入上,即:区分输入的不同部分对输出的影响。这里,我们从增强字/词的语义表示这一角度来理解一下Attention机制。
- 我们知道,一个字/词在一篇文本中表达的意思通常与它的上下文有关。比如:光看“鹄”字,我们可能会觉得很陌生(甚至连读音是什么都不记得吧),而看到它的上下文“鸿鹄之志”后,就对它立马熟悉了起来。因此,字/词的上下文信息有助于增强其语义表示。同时,上下文中的不同字/词对增强语义表示所起的作用往往不同。比如在上面这个例子中,“鸿”字对理解“鹄”字的作用最大,而“之”字的作用则相对较小。为了有区分地利用上下文字信息增强目标字的语义表示,就可以用到Attention机制。
- Attention机制主要涉及到三个概念:Query、Key和Value。在上面增强字的语义表示这个应用场景中,目标字及其上下文的字都有各自的原始Value,Attention机制将 目标字作为Query、其上下文的各个字作为Key,并将Query与各个Key的相似性作为权重,把上下文各个字的Value融入目标字的原始Value中。如下图所示,Attention机制将目标字和上下文各个字的语义向量表示作为输入,首先通过线性变换获得目标字的Query向量表示、上下文各个字的Key向量表示以及目标字与上下文各个字的原始Value表示,然后计算Query向量与各个Key向量的相似度作为权重,加权融合目标字的Value向量和各个上下文字的Value向量,作为Attention的输出,即:目标字的增强语义向量表示。
- Self-Attention: 对于输入文本,我们需要对其中的 每个字分别增强语义向量表示,因此,我们分别将每个字作为Query,加权融合文本中所有字的语义信息,得到各个字的增强语义向量,如下图所示。在这种情况下,Query、Key和Value的向量表示均来自于同一输入文本,因此,该Attention机制也叫Self-Attention。
- Multi-head Self-Attention: 为了增强Attention的多样性,文章作者进一步利用不同的Self-Attention模块获得文本中每个字在不同语义空间下的增强语义向量,并将每个字的多个增强语义向量进行线性组合,从而获得一个最终的与原始字向量长度相同的增强语义向量,如下图所示。
- 这里,我们再给出一个例子来帮助理解Multi-head Self-Attention(注:这个例子仅用于帮助理解,并非严格正确)。看下面这句话:“南京市长江大桥”,在不同语义场景下对这句话可以有不同的理解:“南京市/长江大桥”,或 “南京市长/江大桥”。对于这句话中的“长”字,在前一种语义场景下需要和“江”字组合才能形成一个正确的语义单元;而在后一种语义场景下,它则需要和“市”字组合才能形成一个正确的语义单元。我们前面提到,Self-Attention旨在用文本中的其它字来增强目标字的语义表示。在不同的语义场景下,Attention所重点关注的字应有所不同。因此,Multi-head Self-Attention可以理解为考虑多种语义场景下目标字与文本中其它字的语义向量的不同融合方式。可以看到,Multi-head Self-Attention的输入和输出在形式上完全相同,输入为文本中各个字的原始向量表示,输出为各个字融合了全文语义信息后的增强向量表示。因此,Multi-head Self-Attention可以看作是对文本中每个字分别增强其语义向量表示的黑盒。
1.2. 基础架构-Transformer中的Encoder
- 在Multi-head Self-Attention的基础上再添加一些“佐料”,就构成了大名鼎鼎的Transformer Encoder。实际上,Transformer模型还包含一个Decoder模块用于生成文本,但由于BERT模型中并未使用到Decoder模块,因此这里对其不作详述。下图展示了Transformer Encoder的内部结构,可以看到,Transformer Encoder在Multi-head Self-Attention之上又添加了三种关键操作:
- ①残差连接:将模块的输入与输出直接相加,作为最后的输出。这种操作背后的一个基本考虑是:修改输入比重构整个输出更容易(“锦上添花”比“雪中送炭”容易多了!)。这样一来,可以使网络更容易训练。
- ②Layer Normalization:对某一层神经网络节点作0均值1方差的标准化。
- ③线性转换:对每个字的增强语义向量再做两次线性变换,以增强整个模型的表达能力。这里,变换后的向量与原向量保持长度相同。
- 可以看到,Transformer Encoder的输入和输出在形式上还是完全相同,因此,Transformer Encoder同样可以表示为将输入文本中各个字的语义向量转换为相同长度的增强语义向量的一个黑盒。
- 组装好TransformerEncoder之后,再把多个Transformer Encoder一层一层地堆叠起来,BERT模型就大功告成了!
- 在论文中,作者分别用12层(BERT base)和24层(BERT large) Transformer Encoder组装了两套BERT模型,两套模型的参数总数分别为110M和340M。
- BERT的基础架构部分使用的是Tranformer的Encoder部分。如下图:
- 从上图可以看出,BERT由三个部分:输入部分、多头注意力部分以及前馈神经网络部分。
- 注意: 12个Encoder堆叠在一起,而不是12个Tranformer堆叠在一起。Transformer在原论文中应该是6个Encoder(编码端)和6个Decoder(解码端)堆叠在一起。Transformer原论文结构如下:
- 下面回到一个Encoder去讲解:
- 上图(左边)对于Tranformer的输入,第一部分是Input embedding(做词向量,比如使用随机初始化或者使用word2vec)、第二部分是Positional Encoding(使用的是三角函数,也就是正余弦函数)。
- 上图(右边),但是在BERT中,分为三部分:第一部分是token embedding;第二步是segment embedding;第三部分是position embedding(注意这里是position embedding,区别于Tranformer中的positional encoding)。
1.3. BERT输入的三部分
- BERT输入的具体三部分如下(左边输入的是中文的字,右边是英文):
- 首先说明一下:在基于深度神经网络的NLP方法中,文本中的 字/词 通常都用一维向量来表示(一般称之为 “词向量”)。在此基础上,神经网络会将文本中各个 字或词的一维词向量 作为输入,经过一系列复杂的转换后,输出一个一维词向量作为文本的语义表示。特别地,我们通常 希望语义相近的字/词在特征向量空间上的距离也比较接近,如此一来,由字/词向量转换而来的文本向量也能够包含更为准确的语义信息。因此,BERT模型的主要输入是文本中各个字/词的原始词向量,该向量既可以随机初始化,也可以利用Word2Vector等算法进行预训练以作为初始值;输出是文本中各个字/词融合了全文语义信息后的向量表示,如下图所示左边(统一以字向量作为输入):
- 可以看出,BERT模型通过查询字向量表将文本中的每个字转换为一维向量,作为模型输入;模型输出则是输入各字对应的融合全文语义信息后的向量表示。此外,模型输入除了字向量,还包含另外两个部分:
- 文本向量:该向量的取值在模型训练过程中自动学习,用于刻画文本的全局语义信息,并与单字/词的语义信息相融合。
- 位置向量:由于出现在文本不同位置的字/词所携带的语义信息存在差异(比如:“我爱你”和“你爱我”),因此,BERT模型对不同位置的字/词分别附加一个不同的向量以作区分。
- 最后,BERT模型 将字向量、文本向量和位置向量的加和作为模型输入。特别地,在目前的BERT模型中,文章作者还将英文词汇作进一步切割,划分为更细粒度的语义单位(WordPiece),例如:将playing分割为play和##ing;此外,对于中文,目前作者尚未对输入文本进行分词,而是直接将单字作为构成文本的基本单位。
- 对于Input这一行重点关注两部分:第一部分是正常词汇
(my dog is cute he likes play ##ing)
其中##ing
是Bert分词之后的东西,不用关注。第二步是特殊词汇(第一个[CLS],第二个[SEP],最后的[SEP]
),这3个是两种特殊符号,存在都是因为BERT的预训练中有一个是 NSP(Next Sentence Prediction,下句预测) 任务,是判断两个句子之间的关系(后面介绍,这里只需要知道NSP处理的是两个句子,所以需要一个符号告诉模型符号之前是一个句子,符号之后是另一个句子,这是SEP的作用。而且要做的NSP任务又是一个二分类任务,就是句子之间什么关系的二分类任务,怎么去做这个二分类任务,作者在句子前面加一个CLS的特殊符号,在训练的时候,将CLS的输出向量接一个二分类器,去做一个二分类任务,这个是CLS的作用)- 注意:CLS向量不能代表整个句子的语义信息。 说到这里,很多人对CLS有一个误解,很多人认为CLS这个输出向量代表了整2个句子的语义信息?简单说一下个人理解,并不能代表句子的语义信息,也没有在原论文看到过作者相关的说法,自己理解CLS这个向量用在了NSP二分类任务,它和编码整个句子的语义信息任务相差甚远,所以大家都会发现一个问题,用CLS这个向量去做无监督的文本相似度的时候,效果会非常差。看下面的经验:
- BERT pretrain模型直接拿来用作 sentence embedding效果甚至不如word embedding,
CLS
的embedding效果最差(也就是你说的pooled output)。把所有普通token embedding做pooling勉强能用(这个也是开源项目bert-as-service的默认做法),但也不会比word embedding更好。究竟能不以无监督的方式用CLS
处理文本相似度?大家可以看一下苏建林的文章,BERT的白话。- Token embeddings: 很简单就是input中的所有词汇(包括正常词汇和特殊词汇)都去做正常的embedding,比如是随机初始化。
- Segment embeddings: 这个由于处理的是2个句子,所以需要对2个句子进行区分,那么第1个句子全用0表示,后面这个句子全用1表示,上图中第1个句子都用 表示,第2个句子都用 。
- Position embeddings: 也就是BERT的输入部分和Tranformer输入部分很大的不同点(Transformer中使用的是正余弦函数,BERT中使用随机初始化,让模型自己学出来)。比如上图中第1个位置定位 ,第2个位置定位 ,第3个位置定位 ,…,以此类推。index最大是511(输入长度最大是512),让它自己学出来每个位置的embedding是什么样子的(为什么使用embedding?还没有一个好的解释)。
二. 如何做BERT预训练(参数+MLM+NSP)
2.1. MLM(Mask Language Model)
- 遮掩语言模型(Masked Language Modeling,MLM)可以描述为: 给定一句话,随机抹去这句话中的一个或几个词,要求根据剩余词汇预测被抹去的几个词分别是什么,如下图所示。
- 这不就是我们高中英语常做的完形填空么!所以说,BERT模型的预训练过程其实就是在模仿我们学语言的过程。具体来说,文章作者在一句话中随机选择15%的词汇用于预测。对于在原句中被抹去的词汇,80%情况下采用一个特殊符号
[MASK]
替换,10%情况下采用一个任意词替换,剩余10%情况下保持原词汇不变。这么做的主要原因是:在后续微调任务中语句中并不会出现[MASK]
标记,而且这么做的另一个好处是:预测一个词汇时,模型并不知道输入对应位置的词汇是否为正确的词汇(10%概率),这就迫使模型更多地依赖于上下文信息去预测词汇,并且赋予了模型一定的纠错能力。- 首先我们知道BERT中使用的是大量的无标注的语料,随手可见的一些文本,属于一个无监督任务。
- 对于无监督的目标函数来讲,有两种目标函数比较收到重视:第一种是 自回归模型(autoregressive,AR),只能考虑单侧的信息,典型的就是GPT。第二种是 自编码模型(autoencoding,AE),从损坏的输入数据中预测重建原始数据,可以 使用上下文的信息,BERT就是使用的AE。
- 举一个最简单的例子说明:现在原始的输入语料是
[我爱吃饭]
四个字,那么AR模型在做的时候不会对这个句子本身我爱吃饭去做操作,它的优化目标如下:这个句子出现的概率等于我出现的概率乘以在我条件下,爱出现的概率,再乘以再我爱的条件下吃的概率,最后乘以我爱吃的条件下饭的概率。
仔细看这个优化目标,它是有一个前后依赖关系的,从这里 可以看到AR模型知识用到了单侧的信息,也就是从左右顺序过来的。
- AE模型是对句子做一个mask(遮掩,面具的意思),简单理解就是用面具掩盖掉句子中某些或者某几个单词,这里假设mask之后是如下图
[我爱mask饭]
,那么它的优化目标如下:这个句子出现的概率等于我爱mask饭条件下出现我爱吃饭的概率,又等于我爱饭的条件下mask等于吃的概率。
仔细体会一下这个优化目标,它预测出来的是mask单词是什么的意思。它打破了文本原有的信息,原本是我爱吃饭,它把吃这个字掩盖掉了,让模型不知道,预训练的时候让它去做文本重建,在做文本重建的时候,模型绞尽脑汁从周围的文本中学习各种信息,让自己预测的mask词汇无限接近原本的信息(也就是让mask学习出来无限接近吃这个字)。- 比如 mask右边这个饭告诉模型,前面可能是一个动词,左边我爱这个词组告诉模型,在爱之后的单词很大概率是个词组。比如我爱放风筝,我爱旅游,我爱吃睡觉等等,爱之后都是动词词组,它就会告诉模型爱之后很大概率是一个动词词组,这就是模型绞尽脑汁从文本中学习到的某种规律,学习到规律之后,它就能把mask这个词汇重建出来变为吃。
- 深究一下mask模型有没有缺点呢? 肯定有,比如还是刚才这个例子,mask掉了两个单词,变成了如下
[我爱mask mask]
。优化目标变成了下面,发现一个问题这个优化目标认为吃和饭是相互独立的,也就是认为mask和mask之间是相互独立的,实时上吃和饭又关系的,很多情况下mask之间是有关系的。
- BERT做预训练的时候第一个任务就是MLM,用到了mask策略。需要注意的,mask的概率问题。随机mask百分之15的单词,而且这15%单词又不是全部mask,而是10%替换成其它单词,10%保持不变,80%替换为真正的mask。 关于这个概率,没有人解释为什么这样做。
- mask代码实践:
random.random()用于生成一个0到1的随机符点数: 0 <= n < 1.0;函数是在 [0, 1) 的均匀分布中产生随机数。
2.2. NSP(Next Sentence Prediction)
- 下句预测(Next Sentence Prediction,NSP) 任务描述为:给定一篇文章中的两句话,判断第二句话在文本中是否紧跟在第一句话之后,如下图所示。
- 大家应该都做过 段落重排序,即:将一篇文章的各段打乱,让我们通过重新排序把原文还原出来,这其实需要我们对全文大意有充分、准确的理解。NSP任务实际上就是段落重排序的简化版:只考虑两句话,判断是否是一篇文章中的前后句。在实际预训练过程中,文章作者从文本语料库中随机选择50%正确语句对和50%错误语句对进行训练,与Masked LM任务相结合,让模型能够更准确地刻画语句乃至篇章层面的语义信息。
- NSP任务最重要的一个点就是理解它 样本的构造模式,样本如下:
- ① 从训练语料库中取出两个连续的段落作为正样本
- ② 从不同的文档中随机创建一对段落作为负样本
- 从①中可以理解出两个意思:两个连续的文档说明来自同一个文档,一个文档是不是就是一个主题,也就是同一个主题下的两个连续的段落。
- 从②中可以理解出:不同的主题随便抽一个作为负样本。
- 缺点:把主题预测(两个样本是不是来自同一个文档)和连贯性预测(两个段落是不是连续关系)合并为单项任务。 由于主题预测是非常简单的,导致整个任务就变的简单起来了,相比连贯性预测主题预测非常容易学习,这也是后续好多实验验证NSP任务没有效果的一个原因。因为存在主题预测这个任务它变得简单了起来,二后续的一些改进,如LBERT直接就抛弃了主题预测,而是做类似于连贯性预测任务,预测句子的顺序。 LBERT中的样本都是来自于同一个文档,正样本就是同一个文档中两个顺着的句子,负样本就是这2个句子颠倒过来,都是来自同一文档。
三. 如何微调BERT,提升BERT在下游任务中的效果
3.1. 如何微调BERT
- 主要分类四类:
-
(a)语句对的分类任务;(b)单文本的分类任务;(c) 问答;(d)序列标注
。对于不同的NLP任务,模型输入会有微调,对模型输出的利用也有差异:- 序列标注任务:其实就是把所有的token输出做了一个softmax去看它属于实体中的哪一个。序列标注任务:该任务的实际应用场景包括:中文分词&新词发现(标注每个字是词的首字、中间字或末字)、答案抽取(答案的起止位置)等。对于该任务,BERT模型利用文本中每个字对应的输出向量对该字进行标注(分类),如下图左边所示(中文) (B、I、E分别表示一个词的第一个字、中间字和最后一个字)。
- 单个句子的文本分类:就是使用这个ClS(class label)的输出做一个微调,二分类或者多分类。对于文本分类任务,BERT模型在文本前插入一个
[CLS]
符号,并将该符号对应的输出向量作为整篇文本的语义表示,用于文本分类,如下图中间所示(中文)。可以理解为:与文本中已有的其它字/词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个字/词的语义信息。- 语句对的分类任务:这个本质就是文本匹配的任务,就是把两个句子拼接起来,判断它们是否相似,这里也是用
CLS
输出去判断0不相似,1相似。基本都是这样,其实在下游任务中它使用还是比较简单的。语句对分类任务:该任务的实际应用场景包括:问答(判断一个问题与一个答案是否匹配)、语句匹配(两句话是否表达同一个意思)等。对于该任务,BERT模型除了添加[CLS]
符号并将对应的输出作为文本的语义表示,还对输入的两句话用一个[SEP]
符号作分割,并分别对两句话附加两个不同的文本向量以作区分,如下图右边所示(中文)。
3.2. 提升BERT在下游任务中的效果
- 实际应用中,很少会重头训练一个BERT,一般都是用大公司训练好的模型,然后在自己的任务中微调。
- 一般的做法:先获取谷歌中文BERT或者其他大公司BERT,基于自己任务数据做微调。但是想要更好的性能,有许多技巧可以用,这里提的第一点POS traning,把下面这2个步骤,分为4个步骤做。
- 比如做微博文本情感分析: 先Domain transfer再Task transfer,最后微调效果是最好的。
- ①在大量通用语料上训练一个LM(Language Model )预训练模型(Pretrain);这一步一般不用做,直接用中文谷歌BERT就可以。
- ②在相同领域上继续训练这个LM(Domain transfer,领域自适应或者叫迁移);在大量微博文本上继续训练这个BERT
- ③在任务相关的小数据上继续训练LM(Task transfer);在微博情感文本上(第二步部分有的文本不属于情感分析的范畴)
- ④在任务相关数据上做具体任务(Fine-tune)。
- 按照上面这四个步骤来做,一般任务可以提升1-3个百分点。
- 在大量微博文本上继续训练这个BERT,其实也是训练一个BERT,在这个过程中有什么Trick可以使用呢?有2个经验是可以告诉大家的:
- ①动态mask:什么意思呢?BERT在训练过程中,它其实使用的是固定的mask,就是把文本mask之后存在本地,然后每次训练的过程中都是使用的同一个文件(同样的mask,比如我爱吃饭,每次训练都是mask掉吃)。动态mask就是在每个epoch训练之前,再去做mask,相当于每个epoch很大概率mask掉的词是不一样的。
- ②ngram-mask:其实比如ERNIE和SpanBERT都是类似做了实体词的mask。如果训练的时候没有特别准确的实体词,你可以不实体词的mask,做ngram-mask。
- ③参数: 微调之后一般epochs选择3到4个,不会选择太大。使用warmup搭配线性衰减是比较重要的。
- ④数据增强/自蒸馏/外部知识融入
四. Transformer包中Bert
-
预训练模型库 Transformers
是Hugging face公司开源 (一家总部位于纽约的聊天机器人初创服务商,开发的应用在青少年中颇受欢迎,相比于其他公司,Hugging Face更加注重产品带来的情感以及环境因素。官网链接在此 https://huggingface.co/)- Huggingface简介及BERT代码浅析:https://zhuanlan.zhihu.com/p/120315111
- 预训练模型官网:https://huggingface.co/transformers/pretrained_models.html
名称 | 模型的细节(前4个是英文模型,multilingual是多语言模型,最后一个是中文模型(只有字级别的),其中 Uncased 是字母全部转换成小写,而Cased是保留了大小写) |
bert-base-uncased(下载链接) | 12个层,768个隐藏节点,12个heads,110M参数量。在小写英语文本上训练。 |
bert-large-uncased(下载链接) | 24个层,1024个隐藏节点,16个heads,340M参数量。在小写英语文本上训练。 |
bert-base-cased(下载链接) | 12个层,768个隐藏节点,12个heads,110M参数量。在区分大小写的英语文本上训练。 |
bert-large-cased(下载链接) | 24个层,1024个隐藏节点,16个heads,340M参数量。在区分大小写的英语文本上训练。 |
bert-base-multilingual-uncased(下载链接) | [原始,不推荐] 12个层,768个隐藏节点,12个heads,110M参数量。用维基百科的前102种语言在小写文本上训练(见细节:https://github.com/google-research/bert/blob/master/multilingual.md) |
bert-base-multilingual-cased(下载链接) | [新的,推荐] 12个层,768个隐藏节点,12个heads,110M参数量。用维基百科的前104种语言在区分大小写的文本上训练(见细节:https://github.com/google-research/bert/blob/master/multilingual.md) |
bert-base-chinese(下载链接) | 12个层,768个隐藏节点,12个heads,110M参数量。在 中文简体和繁体中文上训练。 |
- bert-base-chinese 是最常见的中文bert语言模型,基于中文维基百科相关语料进行预训练。把它作为baseline,在领域内无监督数据进行语言模型预训练很简单。只需要使用官方给的例子就好。
- roberta-wwm-ext 是哈工大讯飞联合实验室发布的预训练语言模型。预训练的方式是采用roberta类似的方法,比如动态mask,更多的训练数据等等。在很多任务中,该模型效果要优于bert-base-chinese。对于中文roberta类的pytorch模型,使用方法如下
import torch
from transformers import BertTokenizer, BertModel
tokenizer = BertTokenizer.from_pretrained("hfl/chinese-roberta-wwm-ext")
roberta = BertModel.from_pretrained("hfl/chinese-roberta-wwm-ext")
- pytorch中文语言模型bert预训练代码:https://zhuanlan.zhihu.com/p/161301389
import torch
from transformers import BertModel, BertTokenizer
# 这里我们调用bert-base模型,同时模型的词典经过小写处理
model_name = 'bert-base-uncased'
# 读取模型对应的tokenizer
tokenizer = BertTokenizer.from_pretrained(model_name)
# 载入模型
model = BertModel.from_pretrained(model_name)
# 输入文本
input_text = "Here is some text to encode"
input_ids = tokenizer.encode(input_text, add_special_tokens=True)
print(input_ids)
# [101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102]
# input_ids = tokenizer.encode_plus(input_text, add_special_tokens=True)
# print(input_ids)
# {'input_ids': [101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
input_ids = torch.tensor([input_ids])
# tensor([[ 101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102]])
# 获得BERT模型最后一个隐层结果
with torch.no_grad():
last_hidden_states = model(input_ids)[0] # Models outputs are now tuples
last_hidden_states.shape
# torch.Size([1, 9, 768])
- 可以看到,包括import在内的不到十行代码,我们就实现了读取一个预训练过的BERT模型,来encode我们指定的一个文本,对文本的每一个token生成768维的向量。如果是二分类任务,我们接下来就可以把第一个token也就是[CLS]的768维向量,接一个linear层,预测出分类的logits,或者根据标签进行训练。
- tokenizer.encode 和 tokenizer.encode_plus 的区别(例子上面有)
-
encode
仅返回input_ids
-
encode_plus
返回所有的编码信息,具体如下:-
input_ids
:是单词在词典中的编码-
token_type_ids
:区分两个句子的编码(上句全为0,下句全为1)-
attention_mask
:指定对哪些词进行self-Attention操作- Huggingface简介及BERT代码浅析:https://zhuanlan.zhihu.com/p/120315111
五. Bert生成句向量
- 下面介绍如何调用transformers(链接)这个包来提取一个句子的特征。
- Transformers是TensorFlow 2.0和PyTorch的最新自然语言处理库
- Transformers(以前称为pytorch-transformers(链接)和pytorch-pretrained-bert)提供用于自然语言理解(NLU)和自然语言生成(NLG)的最先进的模型(BERT,GPT-2,RoBERTa,XLM,DistilBert,XLNet,CTRL …) ,拥有超过32种预训练模型,支持100多种语言,并且在TensorFlow 2.0和PyTorch之间具有深厚的互操作性。
- 对于每个模型,transformers库里都对应有三个类:
-
model classes
which are PyTorch models (torch.nn.Modules) of the 6 models architectures currently provided in the library, e.g. BertModel
-
configuration classes
which store all the parameters required to build a model, e.g. BertConfig
. You don’t always need to instantiate these your-self, in particular if you are using a pretrained model without any modification, creating the model will automatically take care of instantiating the configuration (which is part of the model)-
tokenizer classes
which store the vocabulary for each model and provide methods for encoding/decoding strings in list of token embeddings indices to be fed to a model, e.g. BertTokenizer
- 简单来说,model classes是模型的网络结构,configuration classes是模型的相关参数,tokenizer classes是分词工具,一般建议直接使用from_pretrained()方法加载已经预训练好的模型或者参数。
-
from_pretrained()
let you instantiate a model/configuration/tokenizer
from a pretrained version either provided by the library itself (currently 27 models are provided as listed 链接here) or stored locally (or on a server) by the user,
# 1、安装transformers库
pip install transformers
# 2、从transformers库中导入Bert的上面所说到的3个类
from transformers import BertModel, BertConfig,BertTokenizer
5.1. 文本处理
- 用
BertTokenizer
对输入文本进行处理,从预训练模型中加载tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
- 如果不想下载,可以先把
bert-base-chinese-vocab.txt
下载下来加载进去。
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese-vocab.txt')
5.1.1. 输入文本是两个sentence
- 需要在文本开头加上
’[CLS]‘
,在每个句子后面加上’[SEP]’
,这样输入到BertModel中才能被正确识别。
import torch
from transformers import BertModel, BertConfig, BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
text = "[CLS]今天天气不错,适合出行。[SEP]今天是晴天,可以出去玩。[SEP]"
# 1、用tokenizer对句子分词
tokenized_text = tokenizer.tokenize(text)
print(tokenized_text)
# ['[CLS]', '今', '天', '天', '气', '不', '错', ',', '适', '合', '出', '行', '。', '[SEP]', '今', '天', '是', '晴', '天', ',', '可', '以', '出', '去', '玩', '。', '[SEP]']
# 2、词在预训练词表中的索引列表
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
print(indexed_tokens)
# [101, 791, 1921, 1921, 3698, 679, 7231, 8024, 6844, 1394, 1139, 6121, 511, 102, 791, 1921, 3221, 3252, 1921, 8024, 1377, 809, 1139, 1343, 4381, 511, 102]
#3、用来指定哪个是第一个句子,哪个是第二个句子,0的部分代表句子一, 1的部分代表句子二
segments_ids = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
#4、转换成PyTorch tensors
tokens_tensor = torch.tensor([indexed_tokens])
segments_tensors = torch.tensor([segments_ids])
print(tokens_tensor)
print(segments_tensors)
-
tokens_tensor,segments_tensors
将作为BertModel的输入。
5.1.2. 输入文本是一个sentence
- 很多时候输入文本是只有一个句子的,上面两个句子的情况只是简单提一下,下面主要是以一个句子为主。同样,先在句子前面加上
’[CLS]’
,后面加上’[SEP]’
。一般神经网络提取文本特征是以batch为单位的,因此还需要用到一个输入input_masks,假设text是一个batch的数据。
import torch
from transformers import BertModel, BertConfig, BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
texts = ["[CLS] 今天天气不错,适合出行。 [SEP]",
"[CLS] 今天是晴天,我们几个人一起去杭州西湖玩吧。 [SEP]"]
tokens, segments, input_masks = [], [], []
for text in texts:
tokenized_text = tokenizer.tokenize(text) # 用tokenizer对句子分词
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text) # 索引列表
tokens.append(indexed_tokens)
segments.append([0] * len(indexed_tokens))
input_masks.append([1] * len(indexed_tokens))
max_len = max([len(single) for single in tokens]) # 最大的句子长度
print("token", tokens)
print("segments", segments)
print("input_masks", input_masks)
print("*" * 100, "max_len", max_len)
for j in range(len(tokens)):
padding = [0] * (max_len - len(tokens[j]))
tokens[j] += padding
segments[j] += padding
input_masks[j] += padding
print("token", tokens)
print("segments", segments)
print("input_masks", input_masks)
# segments列表全0,因为只有一个句子1,没有句子2
# input_masks列表1的部分代表句子单词,而后面0的部分代表paddig,只是用于保持输入整齐,没有实际意义。
# 相当于告诉BertModel不要利用后面0的部分
# 转换成PyTorch tensors
tokens_tensor = torch.tensor(tokens)
segments_tensors = torch.tensor(segments)
input_masks_tensors = torch.tensor(input_masks)
- 输出结果:
token [[101, 791, 1921, 1921, 3698, 679, 7231, 8024, 6844, 1394, 1139, 6121, 511, 102], [101, 791, 1921, 3221, 3252, 1921, 8024, 2769, 812, 1126, 702, 782, 671, 6629, 1343, 3343, 2336, 6205, 3959, 4381, 1416, 511, 102]]
segments [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
input_masks [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
**************************************************************************************************** max_len 23
token [[101, 791, 1921, 1921, 3698, 679, 7231, 8024, 6844, 1394, 1139, 6121, 511, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0], [101, 791, 1921, 3221, 3252, 1921, 8024, 2769, 812, 1126, 702, 782, 671, 6629, 1343, 3343, 2336, 6205, 3959, 4381, 1416, 511, 102]]
segments [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
input_masks [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
-
tokens_tensor,segments_tensors,input_masks_tensors
将作为BertModel的输入。
5.2. 构建BertModel
- 在
BertModel
后面加上一个全连接层,能够调整输出feature
的维度。
class BertTextNet(nn.Module):
def __init__(self, code_length): # code_length为fc映射到的维度大小
super(BertTextNet, self).__init__()
modelConfig = BertConfig.from_pretrained(config_path)
self.textExtractor = BertModel.from_pretrained(model_path, config=modelConfig)
embedding_dim = self.textExtractor.config.hidden_size
self.fc = nn.Linear(embedding_dim, code_length)
self.tanh = nn.Tanh()
def forward(self, tokens, segments, input_masks):
output = self.textExtractor(tokens, token_type_ids=segments, attention_mask=input_masks)
text_embeddings = output[0][:, 0, :]
# output[0](batch size, sequence length, model hidden dimension)
features = self.fc(text_embeddings)
features = self.tanh(features)
return features
- 使用
pytorch_transformers
本身提供的预训练BertConfig
,以及加载预训练模型。
config = BertConfig.from_pretrained('bert-base-chinese')
self.textExtractor = BertModel.from_pretrained('bert-base-chinese', config=modelConfig)
- 否则还是像上面模型那样加载本地下载的预训练模型。把输入到BertModel后得到的输出output,一般是使用它的第0维信息。
outputs[0] # The last hidden-state is the first element of the output tuple
- 其中
output[0][:,0,:]
代表下图中的C
的输出向量,参考论文:Bert,链接
5.2.1 完整代码
# !/usr/bin/env python
# -*- encoding: utf-8 -*-
"""=====================================
@author : kaifang zhang
@time : 2021/10/24 09:45 上午
@contact: [email protected]
====================================="""
import torch
from torch import nn
from transformers import BertModel, BertConfig, BertTokenizer
# 自己下载模型相关的文件,并指定路径
config_path = 'bert_base_chinese/config.json'
model_path = 'bert_base_chinese/pytorch_model.bin'
vocab_path = 'bert_base_chinese/vocab.txt'
class BertTextNet(nn.Module):
def __init__(self, code_length): # code_length为fc映射到的维度大小
super(BertTextNet, self).__init__()
modelConfig = BertConfig.from_pretrained(config_path)
self.textExtractor = BertModel.from_pretrained(model_path, config=modelConfig)
embedding_dim = self.textExtractor.config.hidden_size
self.fc = nn.Linear(embedding_dim, code_length)
self.tanh = nn.Tanh()
def forward(self, tokens, segments, input_masks):
output = self.textExtractor(tokens, token_type_ids=segments, attention_mask=input_masks)
text_embeddings = output[0][:, 0, :]
# output[0](batch size, sequence length, model hidden dimension)
features = self.fc(text_embeddings)
features = self.tanh(features)
return features
textNet = BertTextNet(code_length=32)
# --------------------------处理输入--------------------------
tokenizer = BertTokenizer.from_pretrained(vocab_path)
texts = ["[CLS] 今天天气不错,适合出行。 [SEP]",
"[CLS] 今天是晴天,我们几个人一起去杭州西湖玩吧。 [SEP]"]
tokens, segments, input_masks = [], [], []
for text in texts:
tokenized_text = tokenizer.tokenize(text) # 用tokenizer对句子分词
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text) # 索引列表
tokens.append(indexed_tokens)
segments.append([0] * len(indexed_tokens))
input_masks.append([1] * len(indexed_tokens))
max_len = max([len(single) for single in tokens]) # 最大的句子长度
print("token", tokens)
print("segments", segments)
print("input_masks", input_masks)
print("*" * 100, "max_len", max_len)
for j in range(len(tokens)):
padding = [0] * (max_len - len(tokens[j]))
tokens[j] += padding
segments[j] += padding
input_masks[j] += padding
print("token", tokens)
print("segments", segments)
print("input_masks", input_masks)
# segments列表全0,因为只有一个句子1,没有句子2
# input_masks列表1的部分代表句子单词,而后面0的部分代表paddig,只是用于保持输入整齐,没有实际意义。
# 相当于告诉BertModel不要利用后面0的部分
# 转换成PyTorch tensors
tokens_tensor = torch.tensor(tokens)
segments_tensors = torch.tensor(segments)
input_masks_tensors = torch.tensor(input_masks)
# --------------------------提取文本特征--------------------------
text_embedding = textNet(tokens_tensor, segments_tensors, input_masks_tensors)
print(text_embedding.shape)
- 输出结果:
token [[101, 791, 1921, 1921, 3698, 679, 7231, 8024, 6844, 1394, 1139, 6121, 511, 102], [101, 791, 1921, 3221, 3252, 1921, 8024, 2769, 812, 1126, 702, 782, 671, 6629, 1343, 3343, 2336, 6205, 3959, 4381, 1416, 511, 102]]
segments [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
input_masks [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
**************************************************************************************************** max_len 23
token [[101, 791, 1921, 1921, 3698, 679, 7231, 8024, 6844, 1394, 1139, 6121, 511, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0], [101, 791, 1921, 3221, 3252, 1921, 8024, 2769, 812, 1126, 702, 782, 671, 6629, 1343, 3343, 2336, 6205, 3959, 4381, 1416, 511, 102]]
segments [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
input_masks [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
torch.Size([2, 32])
六. 参考
标签:NLP,BERT,模型,笔记,tokens,input,文本,向量 From: https://blog.51cto.com/u_15866474/5956844
- 主要参考dasou博主的视频:https://www.bilibili.com/video/BV1Ey4y1874y?p=6&spm_id_from=pageDriver
- 腾讯Bugly的专栏:图解BERT模型:从零开始构建BERT
- Bert源代码解读-以BERT文本分类代码为例子:https://github.com/DA-southampton/Read_Bert_Code
- BERT大火却不懂Transformer?读这一篇就够了:https://zhuanl