BERT学习与实践:为紧追潮流ChatGPT做好技术准备

★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>


BERT学习与实践:为紧追潮流ChatGPT做好技术准备!

本项目的主角是:ChatGPT的技术底座,NLP自然语言机器学习的开山之作,ERNIE相爱相杀的好基友–BERT模型!当然ChatGPT更直接的技术底座是GPT3.0。

如果有人问NLP自然语言机器学习有啥用?恐怕半年前还需要用800字才能讲明白,但现在有了ChatGPT,事实胜于雄辩,千千万万的公司和个人不再迷茫于NLP有啥用,而是发现了商机并投入到ChatGPT研发和应用的热潮中。ChatGPT的表现让人震惊,它可能正在捅破强人工智能和弱人工智能之间的那层窗户纸!

文心大模型,它是基于ERNIE模型的。而ERNIE模型跟BERT模型,就像它们俩在《芝麻街》中的故事一样,是亦敌亦友的关系。

总而言之,BERT开启了NLP新时代,ChatGPT引爆了NLP应用的市场!三种核心技术BERT、GPT和ERNIE互相借鉴,共同提高。现在的问题是:ERNIE大模型没有开源,GPT3.0没有开源,ChatGPT没有开源,而且这三个大模型即使开源了普通人也没有那么多算力去训练,甚至连放模型和数据集的存储空间都不够。聪明的你,肯定能够明白:如果想紧跟时代潮流,对于大多数人,扎扎实实把BERT弄熟弄透才是正解!

2018年的BERT横扫多个评测,开启NLP新时代!

本项目参考自李沐老师《动手学深度学习》中文飞桨版,https://zh.d2l.ai 。 可以观看李沐老师B站讲解视频

也可以在AIStudio学习课程:https://aistudio.baidu.com/aistudio/education/group/info/25851

一、BERT原理:来自Transformers的双向编码器表示

什么是BERT?我们首先来思考下面的问题

什么是NLP里的迁移学习

在BERT之前

  • 是Word2Vec或者语言模型
  • 一般不更新预训练好的模型
  • 换新任务需要构建新的网络来抓取新任务需要的信息

BERT的动机

  • 基于微调的NLP模型,像CV一样用通用大数据进行预训练,然后针对特定任务微调
  • 这样预训练模型抽取了足够多的信息,权重可以重复使用,
  • 新任务只需要增加一个简单的输出层

BERT的架构

BERT是只有编码器的Transformer模型。也就是把解码器砍了的Transformer模型。

也就是只有下图的编码部分。

但是如果只是砍了解码器,并不会使BERT成为划时代的模型,因为BERT还有一样大创新,那就是:

对输入的修改

  • 每个样本是一个句子对
  • 加入额外的片段嵌入,给前一个句子Ea向量,后一个句子Eb向量
  • 位置编码可学习

单单这三个技术修改,每个都平平无奇,但是它们结合起来,使BERT获得了自监督学习的能力,再配合后面的预训练任务,使BERT的语言处理发挥出惊人的能力,引领了其后的整个NLP发展的方向!

BERT代码实践

实践是最好的学习方法!让我们开始从头到尾用飞桨来实现BERT模型吧!

import warnings
from d2l import paddle as d2lwarnings.filterwarnings("ignore")
import paddle
from paddle import nn

输入表示

在自然语言处理中,有些任务(如情感分析)以单个文本作为输入,而有些任务(如自然语言推断)以一对文本序列作为输入。BERT输入序列明确地表示单个文本和文本对。当输入为单个文本时,BERT输入序列是特殊类别词元“<cls>”、文本序列的标记、以及特殊分隔词元“<sep>”的连结。当输入为文本对时,BERT输入序列是“<cls>”、第一个文本序列的标记、“<sep>”、第二个文本序列标记、以及“<sep>”的连结。我们将始终如一地将术语“BERT输入序列”与其他类型的“序列”区分开来。例如,一个BERT输入序列可以包括一个文本序列或两个文本序列

为了区分文本对,根据输入序列学到的片段嵌入 e A \mathbf{e}_A eA e B \mathbf{e}_B eB分别被添加到第一序列和第二序列的词元嵌入中。对于单文本输入,仅使用 e A \mathbf{e}_A eA

下面的get_tokens_and_segments将一个句子或两个句子作为输入,然后返回BERT输入序列的标记及其相应的片段索引。

def get_tokens_and_segments(tokens_a, tokens_b=None):"""获取输入序列的词元及其片段索引"""tokens = ['<cls>'] + tokens_a + ['<sep>']# 0和1分别标记片段A和Bsegments = [0] * (len(tokens_a) + 2)if tokens_b is not None:tokens += tokens_b + ['<sep>']segments += [1] * (len(tokens_b) + 1)return tokens, segments

BERT选择Transformer编码器作为其双向架构。在Transformer编码器中常见是,位置嵌入被加入到输入序列的每个位置。然而,与原始的Transformer编码器不同,BERT使用可学习的位置嵌入。总之,
BERT输入序列的嵌入是词元嵌入、片段嵌入和位置嵌入的和。

BERT输入序列的嵌入是词元嵌入、片段嵌入和位置嵌入的和

编码器

下面的BERTEncoder类类似于 TransformerEncoder类,但是使用了片段嵌入和可学习的位置嵌入。

class BERTEncoder(nn.Layer):"""BERT编码器"""def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,ffn_num_hiddens, num_heads, num_layers, dropout,max_len=1000, key_size=768, query_size=768, value_size=768,**kwargs):super(BERTEncoder, self).__init__(**kwargs)self.token_embedding = nn.Embedding(vocab_size, num_hiddens)self.segment_embedding = nn.Embedding(2, num_hiddens)self.blks = nn.Sequential()for i in range(num_layers):self.blks.add_sublayer(f"{i}", d2l.EncoderBlock(key_size, query_size, value_size, num_hiddens, norm_shape,ffn_num_input, ffn_num_hiddens, num_heads, dropout, True))# 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数
#         self.pos_embedding = nn.Parameters(paddle.randn(1, max_len,
#                                                       num_hiddens))x = paddle.randn([1, max_len, num_hiddens])    self.pos_embedding = paddle.create_parameter(shape=x.shape, dtype=str(x.numpy().dtype),default_initializer=paddle.nn.initializer.Assign(x))print(self.pos_embedding, self.pos_embedding.shape, type(self.pos_embedding))
#     param = paddle.create_parameter(shape=x.shape,
#                         dtype=str(x.numpy().dtype),
#                         default_initializer=paddle.nn.initializer.Assign(x))def forward(self, tokens, segments, valid_lens):# 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens)X = self.token_embedding(tokens) + self.segment_embedding(segments)
#         X = X + self.pos_embedding.data[:, :X.shape[1], :] # nn.ParameterX = X + self.pos_embedding[:, :X.shape[1], :]for blk in self.blks:X = blk(X, valid_lens)return X

假设词表大小为10000,为了演示BERTEncoder的前向推断,让我们创建一个实例并初始化它的参数。

vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4
norm_shape, ffn_num_input, num_layers, dropout = [768], 768, 2, 0.2
encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, ffn_num_input,ffn_num_hiddens, num_heads, num_layers, dropout)

我们将tokens定义为长度为8的2个输入序列,其中每个词元是词表的索引。使用输入tokensBERTEncoder的前向推断返回编码结果,其中每个词元由向量表示,其长度由超参数num_hiddens定义。此超参数通常称为Transformer编码器的隐藏大小(隐藏单元数)。

tokens = paddle.randint(0, vocab_size, (2, 8))
segments = paddle.to_tensor([[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 1, 1, 1, 1, 1]])
encoded_X = encoder(tokens, segments, None)
encoded_X.shape

二、BERT预训练

预训练任务1:带掩码的语言模型

Transformer编码是双向的,而标准语言模型要求是单向的,BERT为了解决这个问题,提出了带掩码的语言模型这个概念,训练的时候,随机(15%概率)将一些词元换成看不见的<mask>,具体来说,在这15%的词元里:
80%几率将选中的词元变成<mask>
10%几率换成一个随机词元
10%几率保持原有的词元

下面的MaskLM类来预测BERT预训练的掩蔽语言模型任务中的掩蔽标记。预测使用单隐藏层的多层感知机(self.mlp)。在前向推断中,它需要两个输入:BERTEncoder的编码结果和用于预测的词元位置。输出是这些位置的预测结果。

import warnings
from d2l import paddle as d2lwarnings.filterwarnings("ignore")
import paddle
from paddle import nn
class MaskLM(nn.Layer):"""BERT的掩蔽语言模型任务"""def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs):super(MaskLM, self).__init__(**kwargs)self.mlp = nn.Sequential(nn.Linear(num_inputs, num_hiddens),nn.ReLU(),nn.LayerNorm(num_hiddens),nn.Linear(num_hiddens, vocab_size))def forward(self, X, pred_positions):num_pred_positions = pred_positions.shape[1]pred_positions = pred_positions.reshape([-1])batch_size = X.shape[0]batch_idx = paddle.arange(0, batch_size) # torch.arange()# 假设batch_size=2,num_pred_positions=3# 那么batch_idx是np.array([0,0,0,1,1])
#         print(f'batch_idx:{batch_idx} num_pred_positions:{num_pred_positions}')batch_idx = paddletile(batch_idx, [num_pred_positions])
#         print(f"'batch_idx:{batch_idx}")masked_X = X[batch_idx, pred_positions]
#         print("masked_X{masked_X} pred_positions{pred_positions} masked_X{masked_X}")masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))mlm_Y_hat = self.mlp(masked_X)return mlm_Y_hatdef paddletile(x, n) : # 写一个飞桨的代码,满足torch.repeat_interleave命令。飞桨自带的tile不满足要求。out = paddle.to_tensor([])for i in x:tmp = paddle.tile(i, n)out = paddle.concat([out, tmp])return out

为了演示MaskLM的前向推断,我们创建了其实例mlm并对其进行了初始化。回想一下,来自BERTEncoder的正向推断encoded_X表示2个BERT输入序列。我们将mlm_positions定义为在encoded_X的任一输入序列中预测的3个指示。mlm的前向推断返回encoded_X的所有掩蔽位置mlm_positions处的预测结果mlm_Y_hat。对于每个预测,结果的大小等于词表的大小 ( x − 1 ) ( x + 3 ) a 2 + b 2 x = a 0 + 1 a 1 + 1 a 2 + 1 a 3 + a 4 \left(x-1\right)\left(x+3\right)\sqrt{a^2+b^2}x = a_0 + \frac{1}{a_1 + \frac{1}{a_2 + \frac{1}{a_3 + a_4}}} (x1)(x+3)a2+b2 x=a0+a1+a2+a3+a4111

mlm = MaskLM(vocab_size, num_hiddens)
mlm_positions = paddle.to_tensor([[1, 5, 2], [6, 1, 5]])
mlm_Y_hat = mlm(encoded_X, mlm_positions)
mlm_Y_hat.shape
# 通过掩码下的预测词元`mlm_Y`的真实标签`mlm_Y_hat`,我们可以计算在BERT预训练中的遮蔽语言模型任务的交叉熵损失。mlm_Y = paddle.to_tensor([[7, 8, 9], [10, 20, 30]])
# loss = nn.CrossEntropyLoss(reduction='none')
loss = nn.CrossEntropyLoss()
mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape([-1]))
mlm_l.shape

预训练任务2:下一句子预测

预测句子对中两句是否相邻

训练样本中,50%选相邻句子对,50%选随机句子对(也就是不相邻句子对)

<cls>对应的输出放到一个全连接层来预测

下面的NextSentencePred类使用单隐藏层的多层感知机来预测第二个句子是否是BERT输入序列中第一个句子的下一个句子。由于Transformer编码器中的自注意力,特殊词元“<cls>”的BERT表示已经对输入的两个句子进行了编码。因此,多层感知机分类器的输出层(self.output)以X作为输入,其中X是多层感知机隐藏层的输出,而MLP隐藏层的输入是编码后的“<cls>”词元。

class NextSentencePred(nn.Layer):"""BERT的下一句预测任务"""def __init__(self, num_inputs, **kwargs):super(NextSentencePred, self).__init__(**kwargs)self.output = nn.Linear(num_inputs, 2)def forward(self, X):# X的形状:(batchsize,num_hiddens)return self.output(X)

开始预训练BERT

在WikiText-2数据集上对BERT进行预训练。

加载数据集

加载WikiText-2数据集作为小批量的预训练样本,用于遮蔽语言模型和下一句预测。批量大小是512,BERT输入序列的最大长度是64。注意,在原始BERT模型中,最大长度是512。

数据集下载可能较慢,这里放在work目录,并在使用前拷贝到data目录,以提高运行速度。

!cp ~/work/wikitext-2-v1.zip ~/data
def load_data_wiki(batch_size, max_len):"""加载WikiText-2数据集Defined in :numref:`subsec_prepare_mlm_data`"""data_dir = d2l.download_extract('wikitext-2', 'wikitext-2')paragraphs = d2l._read_wiki(data_dir)train_set = d2l._WikiTextDataset(paragraphs, max_len)train_iter = paddle.io.DataLoader(dataset=train_set, batch_size=batch_size, return_list=True,shuffle=True, num_workers=0)return train_iter, train_set.vocabbatch_size, max_len = 512, 64
train_iter, vocab = load_data_wiki(batch_size, max_len)

预训练BERT

原始BERT有两个不同模型尺寸的版本。基本模型( BERT BASE \text{BERT}_{\text{BASE}} BERTBASE)使用12层(Transformer编码器块),768个隐藏单元(隐藏大小)和12个自注意头。大模型( BERT LARGE \text{BERT}_{\text{LARGE}} BERTLARGE)使用24层,1024个隐藏单元和16个自注意头。值得注意的是,前者有1.1亿个参数,后者有3.4亿个参数。为了便于演示,我们定义了一个小的BERT,使用了2层、128个隐藏单元和2个自注意头。

net = d2l.BERTModel(len(vocab), num_hiddens=128, norm_shape=[128],ffn_num_input=128, ffn_num_hiddens=256, num_heads=2,num_layers=2, dropout=0.2, key_size=128, query_size=128,value_size=128, hid_in_features=128, mlm_in_features=128,nsp_in_features=128)
devices = d2l.try_all_gpus()
loss = nn.CrossEntropyLoss()

在定义训练代码实现之前,我们定义了一个辅助函数_get_batch_loss_bert。给定训练样本,该函数计算遮蔽语言模型和下一句子预测任务的损失。请注意,BERT预训练的最终损失是遮蔽语言模型损失和下一句预测损失的和。

def _get_batch_loss_bert(net, loss, vocab_size, tokens_X,segments_X, valid_lens_x,pred_positions_X, mlm_weights_X,mlm_Y, nsp_y):# 前向传播_, mlm_Y_hat, nsp_Y_hat = net(tokens_X, segments_X,valid_lens_x.reshape([-1]), pred_positions_X)# 计算遮蔽语言模型损失mlm_l = loss(mlm_Y_hat.reshape([-1, vocab_size]), mlm_Y.reshape([-1])) *\mlm_weights_X.reshape([-1, 1])mlm_l = mlm_l.sum() / (mlm_weights_X.sum() + 1e-8)# 计算下一句子预测任务的损失nsp_l = loss(nsp_Y_hat, nsp_y)l = mlm_l + nsp_lreturn mlm_l, nsp_l, l

通过调用上述两个辅助函数,下面的train_bert函数定义了在WikiText-2(train_iter)数据集上预训练BERT(net)的过程。训练BERT可能需要很长时间。以下函数的输入num_steps指定了训练的迭代步数,而不是像CV训练里那样指定训练的轮数。

def train_bert(train_iter, net, loss, vocab_size, devices, num_steps):trainer = paddle.optimizer.Adam(parameters=net.parameters(), learning_rate=0.01)step, timer = 0, d2l.Timer()animator = d2l.Animator(xlabel='step', ylabel='loss',xlim=[1, num_steps], legend=['mlm', 'nsp'])# 遮蔽语言模型损失的和,下一句预测任务损失的和,句子对的数量,计数metric = d2l.Accumulator(4)num_steps_reached = Falsewhile step < num_steps and not num_steps_reached:for tokens_X, segments_X, valid_lens_x, pred_positions_X,\mlm_weights_X, mlm_Y, nsp_y in train_iter:trainer.clear_grad()timer.start()mlm_l, nsp_l, l = _get_batch_loss_bert(net, loss, vocab_size, tokens_X, segments_X, valid_lens_x,pred_positions_X, mlm_weights_X, mlm_Y, nsp_y)l.backward()trainer.step()metric.add(mlm_l, nsp_l, tokens_X.shape[0], 1)timer.stop()animator.add(step + 1,(metric[0] / metric[3], metric[1] / metric[3]))step += 1if step == num_steps:num_steps_reached = Truebreakprint(f'MLM loss {metric[0] / metric[3]:.3f}, 'f'NSP loss {metric[1] / metric[3]:.3f}')print(f'{metric[2] / timer.sum():.1f} sentence pairs/sec on 'f'{str(devices)}')
# 测试50step 14秒。预计50000步需要3.8小时。
train_bert(train_iter, net, loss, len(vocab), devices[:1], 50)

用BERT表示文本

在预训练BERT之后,我们可以用它来表示单个文本、文本对或其中的任何词元。下面的函数返回tokens_a和tokens_b中所有词元的BERT(net)表示。

def get_bert_encoding(net, tokens_a, tokens_b=None):tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b)token_ids = paddle.to_tensor(vocab[tokens]).unsqueeze(0)segments = paddle.to_tensor(segments).unsqueeze(0)valid_len = paddle.to_tensor(len(tokens))encoded_X, _, _ = net(token_ids, segments, valid_len)return encoded_X

插入特殊标记“”(用于分类)和“”(用于分隔)后,BERT输入序列的长度为6。因为零是“”词元,encoded_text[:, 0, :]是整个输入语句的BERT表示。为了评估一词多义词元“crane”,我们还打印出了该词元的BERT表示的前三个元素。

tokens_a = ['a', 'crane', 'is', 'flying']
encoded_text = get_bert_encoding(net, tokens_a)
# 词元:'<cls>','a','crane','is','flying','<sep>'
encoded_text_cls = encoded_text[:, 0, :]
encoded_text_crane = encoded_text[:, 2, :]
encoded_text.shape, encoded_text_cls.shape, encoded_text_crane[0][:3]

现在考虑一个句子“a crane driver came”和“he just left”。类似地,encoded_pair[:, 0, :]是来自预训练BERT的整个句子对的编码结果。注意,多义词元“crane”的前三个元素与上下文不同时的元素不同。证明BERT表示是上下文敏感的。

tokens_a, tokens_b = ['a', 'crane', 'driver', 'came'], ['he', 'just', 'left']
encoded_pair = get_bert_encoding(net, tokens_a, tokens_b)
# 词元:'<cls>','a','crane','driver','came','<sep>','he','just',
# 'left','<sep>'
encoded_pair_cls = encoded_pair[:, 0, :]
encoded_pair_crane = encoded_pair[:, 2, :]
encoded_pair.shape, encoded_pair_cls.shape, encoded_pair_crane[0][:3]

预训练总结

BERT针对预训练和微调设计,基于Transformer的编码器做了如下修改:

  • 模型更大,训练数据更多
  • 输入句子对,片段嵌入
  • 可学习位置编码
  • 训练时使用两个任务:
  • 带掩码的语言模型
  • 下一个句子预测

一句话就是:BERT使用了上面的技术,实现了自监督训练(预训练),并为后面的微调应用打好了基础。

三、BERT微调应用

有了BERT预训练模型之后,我们只需要再一个额外的多层感知机(一层或几层线性层),即可实现NLP自然语言的很多下游任务,比如情感分析,文本分类、问答系统等。

将预训练BERT提供给基于多层感知机的自然语言推断架构

本节将下载一个预训练好的小版本的BERT,然后对其进行微调,以便在SNLI数据集上进行自然语言推断。

import warnings
from d2l import paddle as d2lwarnings.filterwarnings("ignore")
import json
import multiprocessing
import os
import paddle
from paddle import nn

加载预训练的BERT

前面已经在WikiText-2数据集上预训练BERT(请注意,原始的BERT模型是在更大的语料库上预训练的)。原始的BERT模型有数以亿计的参数。在下面,提供了两个版本的预训练的BERT:“bert.base”与原始的BERT基础模型一样大,需要大量的计算资源才能进行微调,而“bert.small”是一个小版本,以便于演示。

d2l.DATA_HUB['bert_small'] = ('https://paddlenlp.bj.bcebos.com/models/bert.small.paddle.zip', '9fcde07509c7e87ec61c640c1b277509c7e87ec6153d9041758e4')d2l.DATA_HUB['bert_base'] = ('https://paddlenlp.bj.bcebos.com/models/bert.base.paddle.zip', '9fcde07509c7e87ec61c640c1b27509c7e87ec61753d9041758e4')

两个预训练好的BERT模型都包含一个定义词表的“vocab.json”文件和一个预训练参数的“pretrained.params”文件。我们实现了以下load_pretrained_model函数来加载预先训练好的BERT参数。

def load_pretrained_model(pretrained_model, num_hiddens, ffn_num_hiddens,num_heads, num_layers, dropout, max_len, devices):data_dir = d2l.download_extract(pretrained_model)# 定义空词表以加载预定义词表vocab = d2l.Vocab()vocab.idx_to_token = json.load(open(os.path.join(data_dir,'vocab.json')))vocab.token_to_idx = {token: idx for idx, token in enumerate(vocab.idx_to_token)}bert = d2l.BERTModel(len(vocab), num_hiddens, norm_shape=[256],ffn_num_input=256, ffn_num_hiddens=ffn_num_hiddens,num_heads=4, num_layers=2, dropout=0.2,max_len=max_len, key_size=256, query_size=256,value_size=256, hid_in_features=256,mlm_in_features=256, nsp_in_features=256)# 加载预训练BERT参数bert.set_state_dict(paddle.load(os.path.join(data_dir,'pretrained.pdparams')))return bert, vocab
devices = d2l.try_all_gpus() # 这句devices赋值并没有使用,属于历史遗留问题
# 下载时间约14秒
bert, vocab = load_pretrained_model('bert_small', num_hiddens=256, ffn_num_hiddens=512, num_heads=4,num_layers=2, dropout=0.1, max_len=512, devices=devices)

微调BERT的数据集:SNLI数据集

对于SNLI数据集的下游任务自然语言推断,我们定义了一个定制的数据集类SNLIBERTDataset。在每个样本中,前提和假设形成一对文本序列,并被打包成一个BERT输入序列,如下图:

(其中的稠密层,就是我们常说的线性层)

片段索引用于区分BERT输入序列中的前提和假设。利用预定义的BERT输入序列的最大长度(max_len),持续移除输入文本对中较长文本的最后一个标记,直到满足max_len。原作中为了加速生成用于微调BERT的SNLI数据集,使用4个工作进程并行生成训练或测试样本,但在AIStudio中,我们还是用单线程。

class SNLIBERTDataset(paddle.io.Dataset):def __init__(self, dataset, max_len, vocab=None):all_premise_hypothesis_tokens = [[p_tokens, h_tokens] for p_tokens, h_tokens in zip(*[d2l.tokenize([s.lower() for s in sentences])for sentences in dataset[:2]])]self.labels = paddle.to_tensor(dataset[2])self.vocab = vocabself.max_len = max_len(self.all_token_ids, self.all_segments,self.valid_lens) = self._preprocess(all_premise_hypothesis_tokens)print('read ' + str(len(self.all_token_ids)) + ' examples')def _preprocess(self, all_premise_hypothesis_tokens):# pool = multiprocessing.Pool(1)  # 使用4个进程# out = pool.map(self._mp_worker, all_premise_hypothesis_tokens)out = []for i in all_premise_hypothesis_tokens:tempOut = self._mp_worker(i)out.append(tempOut)all_token_ids = [token_ids for token_ids, segments, valid_len in out]all_segments = [segments for token_ids, segments, valid_len in out]valid_lens = [valid_len for token_ids, segments, valid_len in out]return (paddle.to_tensor(all_token_ids, dtype='int64'),paddle.to_tensor(all_segments, dtype='int64'),paddle.to_tensor(valid_lens))def _mp_worker(self, premise_hypothesis_tokens):p_tokens, h_tokens = premise_hypothesis_tokensself._truncate_pair_of_tokens(p_tokens, h_tokens)tokens, segments = d2l.get_tokens_and_segments(p_tokens, h_tokens)token_ids = self.vocab[tokens] + [self.vocab['<pad>']] \* (self.max_len - len(tokens))segments = segments + [0] * (self.max_len - len(segments))valid_len = len(tokens)return token_ids, segments, valid_lendef _truncate_pair_of_tokens(self, p_tokens, h_tokens):# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置while len(p_tokens) + len(h_tokens) > self.max_len - 3:if len(p_tokens) > len(h_tokens):p_tokens.pop()else:h_tokens.pop()def __getitem__(self, idx):return (self.all_token_ids[idx], self.all_segments[idx],self.valid_lens[idx]), self.labels[idx]def __len__(self):return len(self.all_token_ids)

下载完SNLI数据集后,我们通过实例化SNLIBERTDataset类来生成训练和测试样本。这些样本将在自然语言推断的训练和测试期间进行小批量读取。

同样为了提高运行速度,我们将数据集放在work目录,并在使用前拷贝到data目录

# 如果出现显存不足错误,请减少“batch_size”。在原始的BERT模型中,max_len=512
# 运行耗时84秒
!cp ~/work/snli_1.0.zip ~/data
batch_size, max_len, num_workers = 512, 128, d2l.get_dataloader_workers()
data_dir = d2l.download_extract('SNLI')
train_set = SNLIBERTDataset(d2l.read_snli(data_dir, True), max_len, vocab)
test_set = SNLIBERTDataset(d2l.read_snli(data_dir, False), max_len, vocab)
train_iter = paddle.io.DataLoader(train_set, batch_size=batch_size, shuffle=True, return_list=True)
test_iter = paddle.io.DataLoader(test_set, batch_size=batch_size, return_list=True)

微调BERT

如前面所述,用于自然语言推断的微调BERT只需要一个额外的多层感知机,该多层感知机由两个全连接层组成(请参见下面BERTClassifier类中的self.hidden和self.output)。这个多层感知机将特殊的“<cls>”词元的BERT表示进行了转换,该词元同时编码前提和假设的信息为自然语言推断的三个输出:蕴涵、矛盾和中性。

class BERTClassifier(nn.Layer):def __init__(self, bert):super(BERTClassifier, self).__init__()self.encoder = bert.encoderself.hidden = bert.hiddenself.output = nn.Linear(256, 3)def forward(self, inputs):tokens_X, segments_X, valid_lens_x = inputsencoded_X = self.encoder(tokens_X, segments_X, valid_lens_x.squeeze(1))return self.output(self.hidden(encoded_X[:, 0, :]))
net = BERTClassifier(bert)

预训练的BERT模型bert被送到用于下游应用的BERTClassifier实例net中。在BERT微调的常见实现中,只有额外的多层感知机(net.output)的输出层的参数将从零开始学习。预训练BERT编码器(net.encoder)和额外的多层感知机的隐藏层(net.hidden)的所有参数都将进行微调。

我们通过该函数使用SNLI的训练集(train_iter)和测试集(test_iter)对net模型进行训练和评估。若计算资源比较充裕,训练和测试精度可以进一步提高:比如换用更大的BERT模型、使用更大的数据集以及训练更长的步数!

# 总共耗时852秒
lr, num_epochs = 1e-4, 5
trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters())
loss = nn.CrossEntropyLoss(reduction='none')
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,devices)

四、思考题:用中文数据集进行BERT预训练和微调

数据集可以参考用这个【NLP】对联数据集:https://aistudio.baidu.com/aistudio/datasetdetail/110057
也可以自行寻找其它中文语料。

基本思路:

对中文语料数据集进行处理,写成句子对的形式。相较英文语料,需要增加一个中文分词的步骤

预训练和微调跟英文语料基本一致

另外需注意对数据集进行清洗,去掉含有错别字和丢字的部分。

如果能从头开始预训练中文语料,并微调解决自己的NLP实际问题,那么大家可以自豪的说:没有人比我更懂BERT! --特朗普名言 与诸君共勉

结束语

让我们荡起双桨,在AI的海洋乘风破浪!

飞桨官网:https://www.paddlepaddle.org.cn

因为水平有限,难免有不足之处,还请大家多多帮助。

作者:段春华, 网名skywalk 或 天马行空,济宁市极快软件科技有限公司的AI架构师,百度飞桨PPDE。

我在AI Studio上获得至尊等级,点亮10个徽章,来关注我呀~ https://aistudio.baidu.com/aistudio/personalcenter/thirdview/141218

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/3115.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学术ChatGPT——利用ChatGPT助力日常工作与学习

ChatGPT的出现给各个行业带来了前所未有的冲击&#xff0c;其中也包括学术研究领域。在学术研究领域&#xff0c;有着「数学天才」之称陶哲轩之前就曾表示&#xff0c;他已经将ChatGPT纳入了自己的工作流程。 那么做为一名普通的工作或者学习的人员&#xff0c;如何使用ChatGPT…

使用ChatGPT辅助学习——让你的学生主动找到学习的方法!

ChatGPT就像一座巨大的金矿&#xff0c;能挖到多少金子&#xff0c;完全取决于你的思维、认知和行动力。 当大部分人还在观望&#xff0c;或者拿着ChatGPT随便玩一玩的时候。 有的人&#xff0c;已经快速把它切入垂直领域&#xff0c;开始深耕。 如果你的孩子或者学生正在上初…

ChatGPT提示词学习手册

前言 欢迎来到《ChatGPT提示的艺术&#xff1a;制作清晰和有效提示的指南》&#xff01;在这本全面的指南中&#xff0c;您将学习到关于制作清晰和有效的ChatGPT提示的一切知识&#xff0c;以推动引人入胜和信息丰富的对话。 无论您是初学者还是有经验的ChatGPT用户&#xf…

ChatGPT神奇应用:制定学习计划

ChatGPT云炬学长 公众号&#xff1a;云炬网络 正文共 391 字&#xff0c;阅读大约需要 2 分钟 为了更好地完成学习任务&#xff0c;制定一个详细完善的学习计划显得尤为重要。今天就教大家如何通过使用ChatGPT&#xff0c;轻松地制定出一份高效率、具有可操作性、可量化的学习…

ChatGPT的初步学习和认识

文章目录 (一) 使用ChatGPT的体验(二) ChatGPT的优缺点ChatGPT的优势包括&#xff1a;ChatGPT的缺点&#xff1a; (三) ChatGPT的功能 (一) 使用ChatGPT的体验 1&#xff09;使用chatGPT进行学习&#xff0c;知识点整理 2&#xff09; 使用chatgpt进行编程 3&#xff09;请教…

正确使用chatgpt学习英语

以下照片均为截图搬运。

如何运用ChatGPT来学习英文?

利用ChatGPT来学习英文是一种有趣且有效的方式。以下是几种方法&#xff0c;可以帮助您利用ChatGPT来提升英语学习的效果&#xff1a; 对话交流&#xff1a;与ChatGPT进行对话是锻炼口语和写作能力的好方法。您可以模拟真实对话&#xff0c;提出问题并回答ChatGPT的回复。这样…

小i机器人悄无声息美股IPO:募资3876万美元 蹭ChatGPT热度

雷递网 雷建平 3月10日 小i机器人&#xff08;股票代码为&#xff1a;“AIXI”&#xff09;昨日在美国纳斯达克上市&#xff0c;发行价为6.8美元&#xff0c;位于发行区间6.8到8.8美元的最低端位置。 小i机器人此次发行570万股&#xff0c;募资总额为3876万美元&#xff0c;较最…

ChatGPT潮落 资金逃离AI概念股

ChatGPT访问量首次出现负增长&#xff0c;寒气传导到证券市场。 上半年经历暴涨的一众AI概念股偃旗息鼓&#xff0c;蓝色光标、三六零、昆仑万维等知名个股均较高点跌超30%。微软、英伟达也未能幸免&#xff0c;同样有不同程度回调。 回顾这波过山车般的市场表现&#xff0c;…

chatgpt赋能python:Python如何获取股票数据——详细介绍

Python如何获取股票数据——详细介绍 Python作为一款专业的编程语言&#xff0c;其应用领域十分广泛&#xff0c;其中之一就是股票数据的获取。本文将详细介绍Python如何获取股票数据的方法&#xff0c;帮助大家快速获取所需的股票信息。 一、使用pandas-datareader获取股票数…

如何上传数据让chatGPT帮你做商业分析

如何上传数据让chatGPT帮你做商业分析 其实&#xff0c;如果能让ChatGPT分析外部数据&#xff0c;能做的事情真的很多。 一、验证思路&#xff0c;先做人简单的分析。 二、如何让ChatGPT识别外部数据。 三、选代反馈。 四、千万小心!ChatGPT城府很深! 最终方案&#xff1a…

火爆全球的ChatGPT到底能够帮助大学生什么??

近期&#xff0c;一款人工智能不断活跃在大众视野,它可以根据用户的问题进行实时分析答疑&#xff0c;而且质量不比该方面的专家差。上线2个月&#xff0c;月活突破1个亿&#xff0c;那ChatGPT到底是什么&#xff1f;那国内有没有类似的程序可以参考借鉴。朋友给我推荐了款类似…

我让 ChatGPT 做了个自我介绍

最近 ChatGPT 真的火得一塌糊涂&#xff0c;不仅仅是在 AI 圈、科技圈引起震动&#xff0c;还让我们这些圈外人士也对 ChatGPT 兴致勃勃。之前我试着自己写文章介绍过 ChatGPT&#xff0c;感觉并不够专业&#xff0c;于是我便萌生了一个想法&#xff1a;不如让 ChatGPT 来个自我…

ChatGPT能做的49件事情

把OpenAI官方的49个Examples整理成了“ChatGPT 能做的49件事”&#xff0c;看着还不错https://platform.openai.com/examples

ChatGPT能帮设计师做什么?

ChatGPT可不只是个“几句话生成器”哦&#xff01; 作为一个AI模型&#xff0c;它可以帮助设计师提供多方面的支持。 比如&#xff0c;它可以给你提供创意灵感、颜色搭配建议、字体选择以及布局排版等指导&#xff0c;甚至还可以自动化生成网站、文章和图像等... 接下来&…

ChatGPT是什么?能用它来干什么

ChatGPT 是一个原型人工智能聊天机器人&#xff0c;专注于可用性和对话。对着ChatGPT提问&#xff0c;有问必答&#xff0c;甚至可以创作&#xff0c;给的上下文越充分&#xff0c;生成的内容越符合要求。 ChatGPT能够理解和生成自然语言&#xff0c;不仅可以回答基础问题&…

ChatGpt能够用来做什么

作为计算机从业人员&#xff0c;chatgpt能够从多方面提高大家的工作效率&#xff0c;主要包括以下几点&#xff1a; 技术问题解答&#xff1a;当遇到技术问题时&#xff0c;可以向ChatGPT提问并获取解答。ChatGPT可以提供相关的知识、文档和示例代码&#xff0c;帮助程序员快速…

chatGPT做算法题

今天突发奇想&#xff0c;自己编了一道算法题让newbing和chatgpt做。问newbing后&#xff0c;没搜索到相关内容&#xff0c;回复无法理解题目&#xff1b;问chatGPT后给出了回答&#xff0c;见下图&#xff1a; 一开始我很震惊&#xff0c;算法居然如此简洁&#xff0c;我知道这…

ChatGPT 最热开源应用汇总

这会是一个关于 ChatGPT 的系列文章&#xff0c;主要记录日常使用 ChatGPT 的感想和相关信息以及有趣的开源项目&#xff0c;然后这些信息我都会汇总到一个 ChatGPT 信息群&#xff0c;有兴趣的朋友可以文末加入 &#x1f973;。最火热的 ChatGPT APPS 都在这了&#xff0c;信息…

chatgpt赋能python:在Mac上如何运行Python

在Mac上如何运行Python 作为一种易学易用的编程语言&#xff0c;Python在Mac系统上得到了广泛应用&#xff0c;尤其是在Web开发、数据科学和机器学习等领域。本文将介绍如何在Mac系统上运行Python&#xff0c;并提供一些有用的技巧和工具&#xff0c;帮助用户更加高效地利用这…