教你用MindSpore实现Bert

发布于:2022-12-28 ⋅ 阅读:(894) ⋅ 点赞:(0)

自然语言处理(Natural Language Understanding, NLP)是人工智能领域的一个重要分支,旨在让机器理解人类语言,以实现人机交流的目的。人类语言(文本)相较于图片、视频等信息,属于非结构数据,但是它包含的信息量巨大。使用NLP技术可以充分分析、利用这些文本信息,搭建起机器语言和人类语言之间沟通的桥梁。

由于文本信息有强烈的时序关系,在基于深度学习的NLP算法早期,基于LSTM结构的网络备受青睐,其中带有注意力机制的seq2seq模型曾经风靡一时。

2017年,由 ”Attention is all you need” 中提出的Transformer结构,抛弃了传统的CNN和RNN,整个网络完全由attention机制组成;2018年,基于Transformer结构的Bert(Bidirectional Encoder Representations from Transformers)网络横空出世,在NLP的11项任务中取得了效果的大幅提升,极大地推进了NLP技术的发展,自此之后,各类基于Transformer结构的NLP网络层出不穷,效果也逐步提升。

有人说现在的NLP就是拿Transformer中的Encoder或者Decoder不停的堆叠,构成一张巨大的网络,然后拿海量的数据过来训练就可以了。今天我们就来介绍一下Bert是如何“大力出奇迹”的。

Bert的训练分为pretrain和finetune两部分,pretrain是在大量无标注数据上的去噪自编码训练,finetune是在少量细分领域数据上的训练。

下图是论文中给出的示意图。

Bert网络的输入是两“句”话,一“句”话是由一个自然段中的连续若干词组成,在首位添加[CLS]用来表征整个输入的全局属性,两段话间插入[SEP]用以分隔。加入两“句”话是,“只是因为在人群中多看了你一眼 再也没能忘掉你容颜“,会被改造为:

[CLS] 只是因为在人群中多看了你一眼[SEP] 再也没能忘掉你容颜[SEP]

为了避免每个输入不定长的问题,会设置每一个输入的seq_length为一固定值,当输入过长,会从前后进行裁剪,输入过短会从后面开始padding。上面这个例子就变为:

[CLS] 只是因为在人群中多看了你一眼[SEP] 再也没能忘掉你容颜[SEP][PAD] [PAD]

Bert的预训练包含两个任务Masked LM和NSP。Masked LM任务中会随机遮挡某些词,然后在输出的相应位置预测这些被遮挡词的原词,NSP任务会随机从另一个段落中取出第二“句”话,即两“句”话不一定是相连的两个,然后从[CLS]位判断两句话是否是相连的,经过处理后,上面的输入可能就变成:

[CLS] 再也[MASK]能忘掉[MASK]容颜[SEP]只[MASK]因为在人群[MASK]多看了你一眼[SEP] [PAD] [PAD]

每一个词都会被映射为一个向量,这个向量包含三个部分:

词向量、A/B句子信息、词位置信息

这样我们的输入shape就是(batch_size, seq_length, hidden_size)。

Bert根据网络规模分为Bert-Base和Bert-Large两种,Bert-Base包含12层Encoder结构,词向量长度为768,Bert-Large包含24层Encoder结构,词向量长度为1024

Transformer结构详解可以参照Jay Alammar的博客:

http://jalammar.github.io/illustrated-transformer/

预训练的目的就是从无标注的文本当中学习到一定的自然语言特征,所以Bert的这种预训练也被称为了NLP领域的ImageNet,在得到一个预训练模型之后,根据细分领域的下游任务,在最后添加一层来进行finetune。如果是分类任务,则添加一层全连接层(hidden_size, cls_nums)将[CLS]映射到分类数,如果是序列标注任务,则将每一个对应输出都经过此全连接层映射到分类数。

接下来介绍一下MindSpore中Bert的相关代码,完整代码请参考:

https://gitee.com/mindspore/mindspore/tree/master/model_zoo/bert

Bert主体的网络结构定义在src/bert_model.py内,生成数据集的相关代码在src/dataset.py中,src/ finetune_eval_model.py内存放了finetune相关的网络改造代码,src/bert_for_pre_training.py和src/bert_for_finetune.py封装放了训练相关的类,src/config.py中存放了配置信息。

1. 配置信息

下图所示是优化器相关配置信息,每一种优化器的学习率、衰减指数、warmup数等等。

下面这一部分是模型配置信息,seq_length、hidden_size、层数、抽头数等。使用时通过运行脚本中的命令行入参来选中相应的优化器和模型尺寸,特定需求的优化器参数或模型尺寸可以自行修改此文件。

2. 网络结构

在src/bert_model.py中存放了定义网络结构相关的所有代码,其中BertModel这个类是最顶层的接口

在init中声明了这几个类,EmbeddingLookup是生成词向量表然后得到输入的对应词向量、EmbeddingPostporcessor是为输入加上对应的A/B句子信息和位置信息,即EmbeddingPostporcessor输出的就是完整的输入(batch_size, seq_length, hidden_size)、CreateAttentionMaskFromInputMask是为self-attention中计算value值时加上一些mask即控制某些位置不对value做贡献,可以通过这个mask来控制每个词能看到的周围的词,默认是每个词可以看到句子中全部的词、BertTransformer中定义的是Transformer的结构。

Construct函数结构及每句话的含义如下

3. 预训练Loss

预训练loss相关代码在src/bert_for_pre_training.py中,GetMaskedLMOutput对应的Masked LM任务,

GetNextSentenceOutput对应的是NSP任务

交叉熵的计算放在BertPretrainingLoss这个类中

Bert预训练总的对外接口为BertNetworkWithLoss

4. 下游任务

下游任务对应的网络结构代码存放在src/finetune_eval_model.py中,其中包括了三个类BertCLSModel,BertSquadModel和BertNERModel,分别对应分类任务、问答任务和命名实体识别任务。

分类任务只需拿到[CLS]位对应的向量,然后添加一层全连接映射到分类数的维度即可

BertSquadModel是专门为Squadv1.1问答任务写的类,需要拿到Encoder最后一层的输出,将每一个词对应的向量映射到2维,表示是否是起始/结束位置

BertNERModel是应对命名实体识别任务的类,拿到Encoder最后一层的输出,将每一个词对应的向量映射到命名实体数,

5. 数据集

构造数据pipeline的代码存放在src/dataset.py中,是事先将文本数据转换成需要的输入格式然后封装为tfrecord或者mindrecord格式进行读取。

进行预训练/finetune的代码为run_pretrain.py、run_ner.py等

我们在wikipedia_cn_news数据集上(约400G)用80卡进行数据并行,进行Bert-large预训练,得到的loss曲线如下

在各类下游任务中的结果如下表所示

下游任务

精度

COLA (base model)

53.8 MCC

MNLI

86.09%

SNLI

93.48%

QNLI

90.01%

MRPC (base model)

88.33  F1 score

RTE

71.48%

SST-2

92.59%

QQP (base model)

71.2  F1 score

STS-B (base model)

87.41 spearman-correlation

Squadv1.1 (base model)

80.6 EM  88.1 F1 score

Clue NER (base model)

78.94 F1 score

Clue NER CRF (base model)

78.95 F1 score

Bert引领了NLP领域发展的新潮,影响深远,MindSpore的Model_zoo中存放有Bert的预训练和finetune脚本,针对各类下游任务有其对应的代码,针对NER任务还配有条件随机场(CRF)的相关代码,均可达到论文中所述精度。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到