Advertisement

『NLP学习笔记』BERT技术详细介绍

阅读量:

要回答“如何用BERT提取文本向量”,我们可以按照以下步骤进行:
导入必要的库
使用transformers库加载预训练BERT模型,并进行前向传播以获取句子表示。
定义BertTextNet类
这个类继承自nn.Module类,并初始化了一个BertModel以及一个全连接层(fc)来映射高维向量到所需的维度。
前向传播过程
将输入文本通过分词器转换为 tokenids 和 tokentypeids 格式,并构建注意力掩码 inputmasks。然后将这些输入传递给BertModel进行前向传播。BertModel返回最后一个隐藏层的状态向量(lasthiddenstate),从中提取出[CLS]位置的表示作为句子表示。
应用激活函数
使用tanh函数对输出进行激活处理,得到最终的句子嵌入向量。
输出结果
该过程返回一个二维数组features,其中每一行对应一个样本的嵌入向量。
具体代码实现
`python
import torch
from transformers import BertModel, BertTokenizer
class BertTextNet(nn.Module):
def init(self, code_length):
super(BertTextNet, self).init()
modelConfig = BertConfig()
self.textExtractor = BertModel.from_pretrained('bert-base-chinese')
self.fc = nn.Linear(self.textExtractor.config.hiddensize, codelength)
self.tanh = nn.Tanh()
def forward(self, tokenstensor, segmentstensor, inputmaskstensor):
outputs = self.textExtractor(tokenstensor, tokentypeids=segmentstensor, attentionmask=inputmasks_tensor)
textembeddings = outputs[0][:, 0, :] # [batchsize, hidden_size]
features = self.fc(text_embeddings)
features = self.tanh(features)
return features
初始化BERT Text Net
textnet = BertTextNet(codelength=32)
加载 tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
示例输入
text1 = "今天天气不错"
text2 = "今天是晴天"
tokens_list = []
segments_list = []
inputmaskslist = []
for text in [text1, text2]:
tokens = tokenizer.tokenize(text) # 分词
tokenized_text = tokenizer(tokens) # 转换为id列表
indexedtokens = tokenizer.converttokenstoids(tokenized_text) # 转换为id列表

创建mask

inputmask = [1] * len(indexedtokens)
tokenslist.append(indexedtokens)
segmentslist.append([0] * len(indexedtokens))
inputmaskslist.append(input_mask)
maxlen = max(len(t) for t in tokenslist)
填充到最大长度
for i in range(len(tokens_list)):
while len(tokenslist[i]) < maxlen:
tokens_list[i].append(0)
tokenstensor = torch.LongTensor(tokenslist) # 转换为张量
segments_tensors = torch.LongTensor(segments

BERT技术详细介绍

文章目录

  • 一、BERT的整体架构设计

      • 1.1 注意力机制研究
      • 1.2 基础架构解析——基于Transformer机制的Encoder模块
      • 1.3 BERT输入结构解析——包含标题、正文及辅助信息三部分的内容
  • 二. 如何实现BERT的预训练(包含参数配置、Mask语言模型与Next句预测任务)

      • 2.1 MLM机制(Mask语言模型)
      • 2.2 NSP任务(Next句预测)
  • 二. 如何实现BERT的预训练(包含参数配置、Mask语言模型与Next句预测任务)

      • 2.1 MLM机制(Mask语言模型)
      • 2.2 NSP任务(Next句预测)
  • 三、探索BERT的迁移学习路径以优化其适用性

    • 3.1. 详细阐述具体的微调方法
      • 3.2. 深入分析如何优化BERT以适应不同应用场景
  • 四、Transformer框架中的预训练语言模型Bert

  • 五、利用Bert生成句向量

    • 5.1、文本的处理流程

        • 5.1.1、输入是一对句子
        • 5.1.2、输入是一段单独的句子
      • 5.2. 构建BertModel

        • 5.2.1 完整代码
    • 六. 参考

  • 基于BERT模型解决NLP任务需遵循两个关键步骤
  • 预训练阶段 :运用大量无监督文本采用自监督学习方法进行训练,在Transformer-encoder层中编码语言知识(包括词法、语法、语义等多个方面的特征)。该过程使预训练模型获取了广泛适用的语言通用知识;
  • 微调优化阶段 :以该预训练好的模型为基础,在具体的任务领域内进行进一步优化调整以实现精准建模;

一. BERT整体模型架构

  • BERT全名是Bidirectional Encoder Representations from Transformers(双层方向编码器代表自变压器),是由谷歌于2018年推出的自然语言处理领域的预训练模型。
    该模型一经发布便迅速占领了NLP领域相关话题的头条新闻版面,
    令人倍感振奋。
  • 从其命名即可看出,
    BERT模型实际上是基于双层Transformer架构中的Encoder Layer进行特征提取设计的,
    值得注意的是,
    该模型本身并未包含Decoder部分。
  • Transformer架构构成了BERT的核心组件,
    而其中最为关键的技术要点莫过于自注意力机制。
    基于此,
    我们接下来将从自注意力机制入手,
    深入探讨如何利用这一核心技术构建完整的Transformer架构,
    在此基础上逐步组合而成完整的BERT模型架构。

1.1. Attention机制

  • 注意力机制的中文名叫 “注意力机制” ” ,从其名称可知它的主要作用是让神经网络将注意力**集中在某些特定位置上的输入上 (即:使网络能够聚焦于输入中的某些部分) 。这种机制有助于提升目标字及其上下文信息的语义表示效果 (也就是通过上下文信息来增强对目标字的理解)
    我们都知道一个字/词在一篇文本中表达的意思一般而言与它的上下文有关 (即:同一个字在不同语境中的意义可能不同) 。例如,“鹄”这个汉字如果没有上下文可能会让人觉得陌生(甚至记不清读音),但当它出现在如“鸿鹄之志”的情境中时立刻就能让人联想到高远深远的意思 (这说明同一个字在不同语境中的意义确实存在显著差异) 。由此可见,在理解词语含义时上下文信息起到了至关重要的作用。
    为了实现对目标字及其上下文信息的语义增强 (即:通过引入其他相关的信息来提升对目标词的理解效果) ,我们需要引入三个关键概念:Query、Key和Value (简称Q-K-V) 。具体来说,在这一过程中:
  • 目标字会被定义为Query
  • 目标字的所有上下文字会被定义为Key
  • 目标字与所有上下文字原有的语义表示会被分别定义为对应的Value
    随后系统会计算Query与各个Key之间的相似性并将其作为权重值 (即:衡量各个Key对Query的相关程度)
    最终系统会将各Key对应的Value按照权重进行加权融合以生成最终的目标字增强后的语义向量表示。
  • 自注意力机制:对于输入文本中的每一个字符(character),我们需要单独提升其语义向量表示(semantic vector representation)。为此目的,在此过程中我们将每个字符单独视为Query(Q),并通过融合整个文本中所有字符的语义信息来生成各字符对应的增强语义向量(enhanced semantic vectors)。如图所示,在这种情形下来自同一输入序列的所有Q、K、V向量都具有相同的来源(source),因此这种机制被称为自注意力机制(Self-Attention).
  • Multiple-head self-attention mechanism: 以增加注意力机制的多样性为目标,
    文章研究者通过多套不同的自注意力模块来提取文本中每个字符在不同语义维度上的强化表示。
    随后,
    为每个字符生成了多维度的强化语义表示向量,
    并将其多个强化后的向量进行加权求和得到最终输出。
    如图所示。

以下是基于给定规则对原文的改写内容

改写说明

1.2. 基础架构-Transformer中的Encoder

在基于Multi-head Self-Attention进行适当拓展的基础上构建起完整的体系架构后就形成了大名鼎鼎的Transformer Encoder这一架构在自然语言处理领域具有里程碑式的意义然而由于BERT模型本身的设计特点并未采用Decoder模块这一设计选择在本论文中不做深入探讨如图所示在Transformer Encoder内部采用了以下关键组件:

①残差连接:将输入信号与输出信号直接叠加这种设计选择背后的理论基础是通过保持原有信息为主实现微调效率的最大化("锦上添花"比"雪中送炭"更为容易!)从而使得整个网络架构具备更强的学习能力。

②Layer Normalization:对各层神经网络节点进行均值为0、方差为1的标准归一化处理这一标准化操作有助于加速训练过程并改善模型收敛性能。

③特征增强:对每个位置上的增强语义向量进行两次线性变换以进一步提升模型的表现同时保证变换后的向量长度与原始向量保持一致。

观察到,在形式上与输入和输出完全一致的情况下

完成对一个TransformerEncoder的搭建后,随后将这些TransformerEncoder依次叠加,最终使得BERT模型实现大获全胜的目标.研究者在构建BERT架构时采用了两种方案:一种是基于12层的Transformer Encoder(Bert-base),另一种是基于24层的(Bert-large).他们通过这两套架构实现了大约110亿和340亿参数量的BERT模型.

  • BERT的基础架构部分使用的是Tranformer的Encoder部分。如下图:

通过观察图表可知,在BERT系统中主要包含三个关键组成部分:输入模块负责接收原始数据;多头注意力机制用于捕捉信息间的复杂关联;而前馈神经网络层则通过非线性变换提升模型的表达能力。

在本设计中采用了12层编码器模块而非传统的12层变换器模块。参考原论文结构,在编码解码器架构中包含6层编码器模块和6层解码器模块。

  • 下面回到一个Encoder去讲解:
  • 上图左侧展示了Transformer模型的输入结构。其中第一部分为Input embedding技术(如使用随机初始化或Word2vec模型),第二部分则采用了Positional Encoding机制(基于三角函数计算)。
  • 右侧展示的是BERT模型的设计架构。其具体分为三个关键模块:第一模块为Token Embedding技术;第二模块为Segment Embedding机制;第三模块则是Position Embedding机制(注意此处与Transformer中的位置编码存在差异)。

1.3. BERT输入的三部分

  • BERT输入的具体三部分如下(左边输入的是中文的字,右边是英文):

下面将详细阐述BERT模型的基本原理及其工作机制

  • 对于Input这一行重点关注两部分:第一部分是正常词汇 (my dog is cute he likes play ##ing) 其中##ing是Bert分词之后的东西 不用关注。第二步是特殊标记 (前面的第一个标记 后面的第二个标记 最后的第三个标记) 这三个标记的存在都是由于BERT预训练时设置了一个NSP任务 用于判断两个句子之间的关系 这一点会在后续内容中详细阐述 因此这两个特殊符号的存在是为了标识两个独立的句子 这种标识对二分类任务来说至关重要。

  • **注意:*CLS向量无法反映整个句子的意义信息 。 这一点值得注意的是 许多人对CLS的作用存在误解 认为它代表了整个句子的信息 但实际上这一观点并不正确 简单来说 个人理解上 CLS向量仅用于NSP二分类任务 而非编码整个句子的意义信息 因此在未经作者官方解释的情况下 我们可以从实验结果中发现一个问题 使用CLS向量进行无监督文本相似度计算往往效果不佳

  • BERT pretrain模型直接应用于sentence embedding的效果甚至不如word embedding单独使用的效果显著* 尽管将普通token进行聚合处理仍有一定的适用性(这也是开源工具bert-as-service默认的做法) 但在无监督场景下其表现远逊于专门设计的任务模型

看到这里大家可能会疑惑 那么究竟如何利用CLS处理文本相似度问题呢?对此感兴趣的读者可以参考苏建林的文章《BERT白话》以获得进一步的答案。

  • Token embeddings相对简单就是input中的所有单词(包括常规单词以及特殊标记词)都经过嵌入过程进行处理,例如通常会采用随机初始化的方式设置初始权重值。
  • Segment embeddings在处理包含两个句子的情况下,为了区分这两个不同的文本片段,我们采用如下方法:第一个片段的所有位置标记为0,第二个片段的所有位置标记为1。具体来说,在图中可以看到第一个片段中的每一个位置都被标记为\mathbf E_\text{A},而第二个片段则全部使用\mathbf E_\text{B}进行表示。
  • Position embeddings这一概念与BERT模型以及Transformer架构的设计有着显著的区别:BERT模型采用的是自监督学习机制来优化生成过程(为什么使用嵌入?目前还没有完善的理论解释),而Transformer架构则直接利用正弦余弦函数来进行位置编码。具体来说,在图中可以看到第i个位置被编码为\mathbf E_i的形式,其中i从0开始计数直到最大值511(输入序列的最大长度设定为512)。这种编码方式能够使模型通过自监督学习机制不断优化生成效果并最终收敛到理想的状态。

二. 如何做BERT预训练(参数+MLM+NSP)

2.1. MLM(Mask Language Model)

*遮掩语言模型(Masked Language Modeling, MLM)的本质:在给定的一句话中,随机选择一个或多个词进行遮蔽处理,并要求通过剩余的词语推测这些被遮蔽的具体内容。

这也是我们在高中英语学习中常遇到的一个练习项目。因此可以看出,在语言学习的过程中,“BERT模型的预训练过程实际上就是在模拟这种能力的发展”。具体而言,在一段话中随机选取15%左右的文字进行预测任务。对于这些被移除的部分,“[MASK]”符号将被用来代替其中80%的内容;有10%的情况会替换成其他任意词语;而剩下的10%,则会保留原本的文字内容。这种设计的主要原因在于:在后续的任务训练过程中,“[MASK]”标记不会出现在实际的数据集中;此外还有一个重要的好处是,在预测单词时无需知道输入位置是否为正确词语(约有10%的概率),这迫使模型必须更多地依赖于上下文信息来进行推测,并赋予其一定的纠错能力。

首先可知,在BERT体系中所使用的都是大量标注度较低的语料资源,在这些常见可用的文本资料中所涉及的任务均为无监督学习范畴。
对于无监督学习场景下的目标函数设计而言,则有两种较为受关注的方式:第一种是基于自回归模型(Autoregressive, AR)的方法,在这种模式下仅能单向捕捉信息;第二种则是基于自编码模型(Autoencoding, AE)的技术路径,在面对输入数据损坏的情况下能够通过上下文信息实现数据重建的任务;而BERT正是采用了后者方案。
举一个最简单的实例来说明问题:假设原始输入语料为"我爱吃饭"这四个汉字组成的短语序列,则AR模型在执行相关操作时并不会直接对这个完整的句子进行处理优化;其具体的优化目标可表述为:"该短语出现的概率等于我出现的概率乘以其条件概率下爱出现的概率再乘以再其条件概率下吃到的概率最后乘以包含吃在内的整体条件概率值"这一串连乘关系式揭示了AR模型仅关注单向的信息流动特性即仅考虑前后文之间的顺序关联性。

  • AE模型通过引入mask(遮蔽、蒙面的概念)技术对句子进行处理。具体而言,在实际应用中将某些或多个单词用遮蔽的概念覆盖起来(如图所示:[我爱mask饭])。该优化目标可表示为:在给定[我爱mask饭]的情况下, 该句出现的概率等于后续完整的句子'我爱吃饭'的概率, 同时也等于在条件'被遮蔽的部分为'的情况下, '饭'作为被遮蔽词项出现的概率。

  • 例如, 在实际应用中,右侧的'饭'这一词项会向模型传递动词词组的相关信息, 左侧的'我爱'这一短语则会向模型传递与之相关的情感色彩 。因此, 模型会从整体语境出发, 推测出被遮蔽的部分可能对应的具体词项。

  • 比如说, 在具体实例中,"我爱放风筝"、"我爱旅游"、"我爱吃睡觉"等短语都表明了**'爱+动词词组'这一固定搭配模式** 。基于这种语言学规律的学习与应用, 模型便能够通过这种规律的学习与应用, 最终恢复出被遮蔽的词语'吃', 这种机制正是语言建模过程中重要的能力体现。

  • 深入探讨一下:这个方法是否存在明显的缺点呢?比如,在刚才的例子中,

  • 该模型忽略了这两个词语的影响,

  • 经过优化后得到的目标是:

    ,我们发现了一个问题:这个优化目标将吃和饭的关系视为无关。
    即使如此,
    我们知道的是,
    实际上吃和饭之间存在密切的关系。

也就是说,
该方法在某些情况下会表现出局限性。

BERT模型在预训练过程中设置了三个主要任务: masked language modeling(MLM)、 sentence next sentence prediction 以及 sentence similarity learning. 其中最为关键的任务便是 MLM, 其中采用了 mask 策略进行操作. 需要特别注意的是 mask 的概率设置. 在具体实现时, 会随机标记 15% 的词, 其中 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)**旨在判断给定文章中的两句话是否连续出现。
    • 许多读者可能都曾尝试过**段落重组(Reordering)**练习,在此过程中需要仔细理解全文内容以便正确重组段落顺序。
    • NSP任务实际上是将复杂的语义理解任务简化为分析两句话之间的关系。
    • 在实际训练过程中,模型会从大量文本数据中随机选取大约一半的正确句子对和另一半错误的句子对进行学习。
    • 这种方法结合了半监督学习策略(Semi-Supervised Learning),通过与masked语言模型(Masked LM)结合提升了模型在理解上下文和生成连贯文本方面的能力。
  • NSP任务最重要的一个点就是理解它 样本的构造模式,样本如下:
  • ① 从训练语料库中取出两个连续的段落作为正样本
  • ② 从不同的文档中随机创建一对段落作为负样本
  • 从①中可以理解出两个意思:两个连续的文档说明来自同一个文档,一个文档是不是就是一个主题,也就是同一个主题下的两个连续的段落。
  • 从②中可以理解出:不同的主题随便抽一个作为负样本。
  • 缺点:把主题预测(两个样本是不是来自同一个文档)和连贯性预测(两个段落是不是连续关系)合并为单项任务。 由于主题预测是非常简单的,导致整个任务就变的简单起来了,相比连贯性预测主题预测非常容易学习,这也是后续好多实验验证NSP任务没有效果的一个原因。因为存在主题预测这个任务它变得简单了起来,二后续的一些改进,如LBERT直接就抛弃了主题预测,而是做类似于连贯性预测任务,预测句子的顺序。 LBERT中的样本都是来自于同一个文档,正样本就是同一个文档中两个顺着的句子,负样本就是这2个句子颠倒过来,都是来自同一文档。

三. 如何微调BERT,提升BERT在下游任务中的效果

3.1. 如何微调BERT

  • 主要分类四类:
  • (a)语句对的分类任务;(b)单文本的分类任务;(c) 问答;(d)序列标注。对于不同类型的自然语言处理(NLP)任务来说,在模型输入形式和输出应用上会有所差异:
  • 序列标注任务:本质上就是将所有token进行softmax处理后判断其属于实体中的哪一个类别。该任务的实际应用场景包括中文分词及新词发现等(如标注每个字是词的首字、中间字或末字)、答案抽取(确定答案的具体起止位置)等。具体而言,在序列标注任务中,BERT模型会利用文本中每个字对应的输出向量对其进行标注(即进行分类),如下图所示(中文):B表示一个词的第一个字、I表示中间字、E表示最后一个字。
  • 单个句子的文本分类:就是通过在文本前插入[CLS]符号来进行微调,并将其对应的输出向量作为整篇文本的意义表示用于后续处理。这可以理解为与文本中的其他字符相比,[CLS]符号由于没有明显语义信息而能够更加公平地融合整个句子的信息。
  • 语句对的分类任务:本质上是一个文本匹配的任务,在此过程中需要将两个句子拼接起来并判断其相似性或不相似性(通过查看对应的0/1标签来实现)。在下游应用中该方法的应用相对简单且普遍适用。例如,在问答系统中判断一个问题与答案是否匹配,在语义匹配中判断两句话是否表达相同的意思。

在该类型的任务中,默认的做法是除了在两句话之间插入[SEP]符号外,并分别为这两句话附加两个不同的向量以加以区分。

如下图所示(中文):

左边为仅包含[BERT编码器输入]的情况;

中间为加入[CLS]符号后的输入情况;

右边则是在两侧分别插入了[SEP]符号的情况。

3.2. 提升BERT在下游任务中的效果

在实际应用场景中,
通常不会从零开始进行BERT模型的重头训练,
而是会选择那些经过大规模预训练的开源模型,
并在特定任务领域进行微调优化。
主要流程通常是先获取预训练好的中文BERT或类似开源模型,
并根据自身任务数据进行微调优化。
其中第一个建议是关于词性(POS)预训练策略,
将这两个主要步骤进一步划分为四个具体实施步骤。

  • 微博文本情感分析案例:采用领域迁移后接着任务迁移的方法,并最终进行微调优化。
    • ①首先使用大规模通用语料库预先训练一个语言模型;这里推荐直接使用中文谷歌BERT模型即可。
    • ②接着在相同领域的数据上进行迁移学习(Domain adaptation);建议在此基础上继续利用微博文本数据集进行模型优化。
    • ③随后,在任务相关的微调数据集上进一步优化模型;需要注意的是,在此过程中约有部分微博文本可能不属于情感分析的数据类型。
    • ④最后,在具体的任务目标数据集上完成微调(Fine-tuning)。
    • 按照以上步骤操作通常能带来1到3个百分点的性能提升。

在大量微博文本上微调BERT其实也就是训练一个BERT,在这一过程中有哪些实用技巧可以分享呢?共有两个实用技巧需要介绍:第一点是动态mask机制其核心思想是在每个epoch开始前重新生成新的mask序列这样每次训练时被遮蔽的内容会有所不同;第二点则是关于ngram-mask的应用这种方法特别适用于当实体词信息不够明确的情况下它能够帮助模型更好地捕捉到词语之间的关联关系。此外模型微调时通常选择3-4个epoch进行训练建议采用warmup策略并配合线性衰减学习率优化以加快收敛速度提高模型性能。

  • 数据增强/自蒸馏/外部知识融入

四. Transformer包中Bert

名称 模型的细节(前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相似的技术手段,在动态mask和丰富多样的数据集上展开学习。在多个任务场景中表现优异,在中文BERT系列模型中具有更好的性能表现。具体实现步骤如下:

复制代码
    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")
复制代码
    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])

我们发现,在仅包含不到十行代码的情况下,
我们成功地完成了对预训练BERT模型的读取,
并将其用于编码指定的一个文本,
每个token都会被转换为768维向量。
假设这是一个二分类的任务,
接下来我们可以使用第一个token(即[CLS])的768维向量作为输入,
然后接一个linear层以预测logits,
或者根据标签进行进一步训练。

  • tokenizer与之相比, encode函数与encode_plus函数的主要区别在于其提供的功能组件。
  • 基本编码机制:通过encode方法仅能获得输入序列的数值表示。
  • 高级编码功能:相较于简单的Token IDs获取, encode_plus提供了更为全面的信息组合。
    • 其中, input_ids表示词汇表中的唯一标识符。
    • token_type_ids则用于区分不同句子的标记方式。
    • special_tokens_mask则用于标注特殊符号的位置信息。
    • attention_mask则是指导自注意力机制的作用范围设定。
复制代码
    from transformers import BertTokenizer
    
    tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
    sentences = ['选择珠江花园的原因就是方便。', '笔记不的键盘确实爽。']
    out = tokenizer.encode_plus(
    text=sentences[0],
    text_pair=sentences[1],
    
    truncation=True,  # 当句子长度大于maxlength时,截断
    
    max_length=30,
    padding='max_length',
    add_special_tokens=True,
    
    return_tensors=None,  # 可取值tf,pt,np,默认为返回list
    return_token_type_ids=True,
    return_special_tokens_mask=True,
    return_attention_mask=True,
    return_length=True
    )
    for k, v in out.items():
    print(k, ":", v)
    
    print(tokenizer.decode(out['input_ids']))
复制代码
    input_ids : [101, 6848, 2885, 4403, 3736, 5709, 1736, 4638, 1333, 1728, 2218, 3221, 3175, 912, 511, 102, 5011, 6381, 679, 4638, 7241, 4669, 4802, 2141, 4272, 511, 102, 0, 0, 0]
    token_type_ids : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]
    special_tokens_mask : [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]
    attention_mask : [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, 0, 0, 0]
    length : 30
    [CLS] 选 择 珠 江 花 园 的 原 因 就 是 方 便 。 [SEP] 笔 记 不 的 键 盘 确 实 爽 。 [SEP] [PAD] [PAD] [PAD]

视频标题

五. Bert生成句向量

  • 请了解如何使用transformers库调用链接提取句子特征。
  • Transformers(原名:pytorch-transformers链接 和 pytorch-pretrained-bert)是TensorFlow 2.0和PyTorch官方推荐的最新自然语言处理库。
  • 这个库专注于NLU(自然语言理解)和NLP(自然语言生成)领域的最先进模型(如BERT、GPT-2、RoBERTa等),支持超过32种预训练模型,并覆盖100多种语言,在TensorFlow 2.0与PyTorch之间实现良好的互操作性。
  • 对于每个模型,在transformers库里都对应有三个关键类:
  • model classes 是基于PyTorch的6种模型架构(torch.nn.Modules),例如BertModel。
  • configuration classes 持有构建模型所需的必要参数(例如BertConfig)。除非你修改了预训练模型,默认情况下从该模型中会自动初始化配置参数。
  • tokenizer classes 包含每个模型的词汇表,并提供编码/解码字符串到列表形式token索引的功能(例如BertTokenizer)。
  • 简而言之:
    • model classes模型架构
    • configuration classes模型参数配置
    • tokenizer classes分词工具
      一般建议直接使用from_pretrained()方法加载已预训练好的模型或参数。

from_pretrained() 方法允许从库中预训练版本(目前支持27种)或本地服务器上的版本加载预训练好的模型或参数(链接here)。

复制代码
    # 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]标记,并确保其正确识别

复制代码
    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]'这两个特殊标记。假设text变量表示一批待处理的数据样本,则后续特征提取过程将基于这些经过标记的输入数据进行操作。

复制代码
    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]]

这三个张量会被采用为该模型的输入参数 fed 到 BERT 模型中。

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

采用...内置的预训练配置参数...并用于加载预训练模型的实现。

复制代码
    config = BertConfig.from_pretrained('bert-base-chinese')
    self.textExtractor = BertModel.from_pretrained('bert-base-chinese', config=modelConfig)
  • 除此之外,则是按照上文所述的方式加载本地已下载好的预训练模型。
复制代码
将输入至BertModel后所生成的输出output,则通常采用其首维度信息进行后续处理。
复制代码
    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: kaifang.zkf@dtwave-inc.com
    ====================================="""
    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])

六. 参考

全部评论 (0)

还没有任何评论哟~