1.BERT模型的初步认识

BERT(Pre-training of Deep Bidirectional Transformers,原文链接:BERT)是近年来NLP圈中最火爆的模型,让我们来看一些数据。

自从2018年BERT模型发布以来,BERT模型仅用 2019 年一年的时间,便以势如破竹的姿态成为了 NLP 领域首屈一指的红人,BERT 相关的论文也如涌潮般发表出来。2019 年,可谓是 NLP 发展历程中具有里程碑意义的一年,而其背后的最大功臣当属 BERT !在NLP领域,如果把2019年称为“BERT年”也不为过。
据统计,2019以BERT为主要内容的论文发表数量近200篇,具体数据可以看看下面图片的github链接呦。

图片出处:BERT_papers

2.BERT之前NLP的状态

在BERT模型发布之前,NLP任务的解决方案是基于word2vec这样的词特征+RNNs等网络结构的解决方案。由于RNNs模型的特征提取能力不足,为了满足业务指标,往往需要大量的标注数据才能满足上线需求。这样,小一些的NLP公司,由于相对数据的匮乏,难以推动NLP业务,致使NLP技术的发展并没有像计算机视觉领域那么顺利。不过,自然语言处理和计算机视觉的研究本身就是互相交叉互相影响的,所以就有学者基于图像领域的思想,将NLP问题的思路转化为预训练+微调的模式,取得了优异的成绩,在BERT模型发表之前,ELMo、GPT就是这一模式的典型开创者。

ELMo提出的预训练+微调的模式,当时只是为了解决word2vec词向量不能表达”一词多义“的问题提出来的。它是一种动态词向量的思想,不过这种预训练+微调的模式借助迁移学习的思想为后来BERT的出现打下了一定的基础,本章不会具体阐述ELMo的原理,如果你还不了解ELMo,我强烈建议你阅读下面的材料来补充相关的知识。

ELMo相关阅读资料:(ELMo原理解析及简单上手使用

这里留一个思考题,大家可以思考下:ELMo的缺点是什么呢?

为了解决上述问题,2017年Google发布了特征提取能力更为强大的Transformer网络,论文链接:Attention is all you need。Transformer中的具体结构以及代码都会在后面章节结合BERT详细剖析,在此不过多介绍。

有了Transformer网络结构后,改造ELMo解决更多更难的问题,提供了一个方向。

对于近年来NLP领域模型发展的历史可以观看下图,该图出自ACL2019大会报告(The Bright Future of ACL/NLP)。

不过话说回来,第一个使用Transformer的预训练模型不是bert,而是GPT。想要进一步了解GPT模型的同学,可以阅读补充资料(OpenAI GPT: Generative Pre-Training for Language Understanding),如果你不了解Transformer结构,我建议你先不要阅读,等阅读完系列文章后,再来品味一下GPT与BERT的不同。

提前透露一下GPT和BERT的最大的不同,GPT里的基本结构是由单向的Transformer-Decoder结构组成,而BERT是由双向的Transformer-Encoder结构组成。

不管是ELMo、GPT还是BERT,他们改变解决NLP的重要思路是预训练+微调的模式。如图所示,预训练+微调的模式是一种迁移学习的思想,预训练阶段可以使用大规模的数据(比如wiki),使得一个强大的模型学习到大量的知识,而且这些知识的学习方式是无监督的。通过预训练的学习之后,模型就已经具备大量的先验知识,在微调阶段继续使用预训练好的模型参数,采用业务自身标注数据在此基础上完成最后一步的监督学习。

了解到这,大家对于BERT应该有了一个初步的认识,那顺便思考一下。

BERT采用了迁移学习的思想,如果在相同的NLP任务上达到传统模型(如RNN等)一样的性能指标,比如准确度都是90%,在准备数据成本上有优势么,什么样的优势?

3.带你读BERT原论文

该部分会精读一下BERT的原文,我也已经将重要的信息做了标注和解释。

把标注的地方全部弄清楚,就可以进行下面的学习了,一个新的算法,理论啃完,在把源码吃透,才算真的掌握,希望大家多注意细节。

完整解读版,可以自行下载,链接: https://pan.baidu.com/s/1Q0DcoIR1boHd4qi7vkwoTg 密码: ee20

4.跑通BERT代码

BERT当年发表时就在SQuAD v1.1上,获得了93.2%的 F1 分数,超过了之前最高水准的分数91.6%,同时超过了人类的分数91.2%。

BERT 还在极具挑战性的 GLUE 基准测试中将准确性的标准提高了7.6%。这个基准测试包含 9 种不同的自然语言理解(NLU)任务。在这些任务中,具有人类标签的训练数据跨度从 2,500 个样本到 400,000 个样本不等。BERT 在所有任务中都大大提高了准确性。

上述的微调任务介绍如下表

  • MNLI:给定一个前提 (Premise) ,根据这个前提去推断假设 (Hypothesis) 与前提的关系。该任务的关系分为三种,蕴含关系 (Entailment)、矛盾关系 (Contradiction) 以及中立关系 (Neutral)。所以这个问题本质上是一个分类问题,我们需要做的是去发掘前提和假设这两个句子对之间的交互信息。
  • QQP:基于Quora,判断 Quora 上的两个问题句是否表示的是一样的意思。
  • QNLI:用于判断文本是否包含问题的答案,类似于我们做阅读理解定位问题所在的段落。
  • STS-B:预测两个句子的相似性,包括5个级别。
  • MRPC:也是判断两个句子是否是等价的。
  • RTE:类似于MNLI,但是只是对蕴含关系的二分类判断,而且数据集更小。
  • SWAG:从四个句子中选择为可能为前句下文的那个。
  • SST-2:电影评价的情感分析。
  • CoLA:句子语义判断,是否是可接受的(Acceptable)。

初步了解了BERT以后,我们就简单跑一个BERT的例子吧,让大家有一个整体的认识。

# 正式开始实验之前首先通过如下命令安装最新版本的 paddlenlp
!python -m pip install --upgrade paddlenlp -i https://pypi.org/simple
# 导入所需要的包
from functools import partial
import argparse
import os
import random
import time

import numpy as np
import paddle
import paddle.nn.functional as F

import paddlenlp as ppnlp
from paddlenlp.data import Stack, Tuple, Pad
from paddlenlp.datasets import load_dataset
from paddlenlp.transformers import LinearDecayWithWarmup
# 下载paddlenlp预置数据
train_ds, dev_ds, test_ds = load_dataset("chnsenticorp", splits=["train", "dev", "test"])
# 输出训练集的前 10 条样本
for idx, example in enumerate(train_ds):
    if idx <= 10:
        print(example)
# 超参数
EPOCHS = 10  # 训练的轮数
BATCH_SIZE = 8  # 批大小
MAX_LEN = 300  # 文本最大长度
LR = 1e-5  # 学习率
WARMUP_STEPS = 100  # 热身步骤
T_TOTAL = 1000  # 总步骤
# 调用bert模型用的tokenizer
tokenizer = ppnlp.transformers.BertTokenizer.from_pretrained('bert-base-chinese')
# 将文本内容转化成模型所需要的token id
def convert_example(example, tokenizer, max_seq_length=512, is_test=False):
    """
    Builds model inputs from a sequence or a pair of sequence for sequence classification tasks
    by concatenating and adding special tokens. And creates a mask from the two sequences passed 
    to be used in a sequence-pair classification task.
    """
    encoded_inputs = tokenizer(text=example["text"], max_seq_len=max_seq_length)
    input_ids = encoded_inputs["input_ids"]
    token_type_ids = encoded_inputs["token_type_ids"]

    if not is_test:
        label = np.array([example["label"]], dtype="int64")
        return input_ids, token_type_ids, label
    else:
        return input_ids, token_type_ids
# 创建dataloader
def create_dataloader(dataset,
                      mode='train',
                      batch_size=1,
                      batchify_fn=None,
                      trans_fn=None):
    if trans_fn:
        dataset = dataset.map(trans_fn)

    shuffle = True if mode == 'train' else False
    if mode == 'train':
        batch_sampler = paddle.io.DistributedBatchSampler(
            dataset, batch_size=batch_size, shuffle=shuffle)
    else:
        batch_sampler = paddle.io.BatchSampler(
            dataset, batch_size=batch_size, shuffle=shuffle)

    return paddle.io.DataLoader(
        dataset=dataset,
        batch_sampler=batch_sampler,
        collate_fn=batchify_fn,
        return_list=True)
# 样本转换函数
trans_func = partial(
    convert_example,
    tokenizer=tokenizer,
    max_seq_length=MAX_LEN)
batchify_fn = lambda samples, fn=Tuple(
    Pad(axis=0, pad_val=tokenizer.pad_token_id),  # input
    Pad(axis=0, pad_val=tokenizer.pad_token_type_id),  # segment
    Stack(dtype="int64")  # label
    ): [data for data in fn(samples)]
train_data_loader = create_dataloader(
    train_ds,
    mode='train',
    batch_size=BATCH_SIZE,
    batchify_fn=batchify_fn,
    trans_fn=trans_func)
dev_data_loader = create_dataloader(
    dev_ds,
    mode='dev',
    batch_size=BATCH_SIZE,
    batchify_fn=batchify_fn,
    trans_fn=trans_func)
test_data_loader = create_dataloader(
    test_ds,
    mode='test',
    batch_size=BATCH_SIZE,
    batchify_fn=batchify_fn,
    trans_fn=trans_func)
# 调用bert的预训练模型
model = ppnlp.transformers.BertForSequenceClassification.from_pretrained(
    'bert-base-chinese', num_classes=len(train_ds.label_list))
# 完成训练设置
num_training_steps = len(train_data_loader) * EPOCHS

lr_scheduler = LinearDecayWithWarmup(LR, num_training_steps, WARMUP_STEPS)

# Generate parameter names needed to perform weight decay.
# All bias and LayerNorm parameters are excluded.
decay_params = [
    p.name for n, p in model.named_parameters()
    if not any(nd in n for nd in ["bias", "norm"])
]
optimizer = paddle.optimizer.AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters(),
    weight_decay=0.0,
    apply_decay_param_fun=lambda x: x in decay_params)

criterion = paddle.nn.loss.CrossEntropyLoss()
metric = paddle.metric.Accuracy()
# 开始训练
global_step = 0
tic_train = time.time()
for epoch in range(1, EPOCHS + 1):
    for step, batch in enumerate(train_data_loader, start=1):
        input_ids, token_type_ids, labels = batch
        logits = model(input_ids, token_type_ids)
        loss = criterion(logits, labels)
        probs = F.softmax(logits, axis=1)
        correct = metric.compute(probs, labels)
        metric.update(correct)
        acc = metric.accumulate()

        global_step += 1
        if global_step % 10 == 0:
            print(
                "global step %d, epoch: %d, batch: %d, loss: %.5f, accu: %.5f, speed: %.2f step/s"
                % (global_step, epoch, step, loss, acc,
                    10 / (time.time() - tic_train)))
            tic_train = time.time()
        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.clear_grad()

        if global_step % 100 == 0:
            break
    break

print('bert训练Demo完成了...')

ok,终于可以完成一个BERT的训练demo了。

5.后续计划

本节课内容就这些,喜欢的同学可以关注后续内容。

计划:

《BERT模型的核心架构》

《BERT如何完成预训练》

《BERT微调细节详解》

《使用BERT完成任务》

《。。。》

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐