word2vec和GloVe之类的词嵌入模型是与上下文无关。他们将相同的预训练向量分配给相同的单词,而不管该单词的上下文(如果有)。他们很难用自然语言处理好多义性或复杂的语义。这种上下文无关的很有局限性,因此出现了很多上下文敏感的模型,ELMo为其中之一。
ELMo将来自预训练的双向LSTM的所有中间层表示形式组合为输出表示形式,然后ELMo表示将作为附加功能添加到下游任务的现有模型中。虽然这解决了上下文无感的问题,但是解决的方案取决与特定的任务体系。但是,为每个自然语言任务设计特定的体系比价困难。为此GPT模型设计为用于上下文相关且非任务确定的通用模型。GPT建立在Transformer解码器上,可以预训练将用于表示文本序列的语言模型。当将GPT应用于下游任务时,语言模型的输出将被馈送到添加的线性输出层中,以预测任务的标签。但是由于模型具有自回归特征,只能向前看(从左到右),在“我去银行存钱”和“我去银行坐下”的情况下,由于“银行”对左侧的上下文很敏感,因此GPT将为“银行”返回相同的表示形式有不同的含义。
ELMo双向编码上下文,GPT与任务无关。BERT吸取了二者的有点,可以双向编码上下文,使用预训练的Transformer编码器,BERT可以基于其双向上下文表示任何token。下图展示了三者的差异:
用于下游任务的监督学习时:
- BERT表示将被输入到一个附加的输出层中,根据任务的性质对模型体系结构的更改应尽可能少。
- 对预训练的Transformer编码其的所有参数进行微调,而额外的输出层将从头开始进行训练。
BERT在进一步改进了以下自然语言处理任务的技术水平:
- 单一文本分类(情感分析)
- 文本对分类(自然语言推断)
- 问答
- 文本标记(命名实体标记)
from d2l import mxnet as d2l
from mxnet import gluon, np, npx
from mxnet.gluon import nn
npx.set_np()
1. 输入表示
在nlp处理中,有些任务是单文本输入(情感分析),有些是一对文本(自然语言推论)。BERT输入序列需要明确单个文本和文本对。对于单个输入,使用<cls>tokens_a<sep>
, 输入一对文本使用<cls>tokens_a<sep>tokens_b<sep>
。编写一个函数用于获取BERT输入。
def get_tokens_and_segments(tokens_a, tokens_b=None):
tokens = ['<cls>'] + tokens_a + ['<sep>']
# 通过segments的0和1区分文本对
segments = [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编码器中很常见,在BERT输入序列的每个位置都添加了位置嵌入。但是,与原始的Transformer编码器不同,BERT使用可学习的位置嵌入。BERT输入序列的嵌入为token嵌入以及段嵌入和位置嵌入的总和。
编写一个BERTEncoder类用于Transformer。
class BERTEncoder(nn.Block):
def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_layers, dropout, max_len=1000, **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 _ in range(num_layers):
self.blks.add(d2l.EncoderBlock(num_hiddens, ffn_num_hiddens, num_heads, dropout, True))
# 在BERT中,位置嵌入是可以学习的,这里创建位置嵌入的参数
self.pos_embedding = self.params.get('pos_embedding', shape=(1, max_len, num_hiddens))
def forward(self, tokens, segments, valid_lens):
# X的形状保持不变: (批量大小, 最大序列长度, 隐藏层)
X = self.token_embedding(tokens) + self.segment_embedding(segments)
X = X + self.pos_embedding.data(ctx=X.ctx)[:, :X.shape[1], :]
for blk in self.blks:
X = blk(X, valid_lens)
return X
使用1w词的示例,测试一下BERTEncoder。
vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4
num_layers, dropout = 2, 0.2
encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens, num_heads,num_layers, dropout)
encoder.initialize()
定义tokens为长度为8的2个BERT输入序列,其中每个标记都是词汇表的索引。BERTEncoder输入的正向推理将tokens返回编码结果,其中每个令牌都由一个向量表示,向量的长度由隐藏层的隐藏单位大小决定。
tokens = np.random.randint(0, vocab_size, (2, 8))
segments = np.array([[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
# (2, 8, 768)
2. 预训练
预训练由两部分组成:
- 掩盖语言建模
- 下句预测
2.1 Masked Language Model(MLM)
之前的语言模型使用左侧的上下文来预测token,为了使用双向上下文来表示每个token,BERT会随机屏蔽一些token,并使用双向上下文中的令牌来预测掩码的token。这个过程称为Masked Language Model(MLM)。
在此预训练任务中,将随机选择15%的token作为用于预测的屏蔽token。通过使用<mask>
标签替换屏蔽的token,如果全部都换成标签的话,会导致在fine-tuning的时候屏蔽的token不在出现,因此通过设置80%的时候会直接替换为<mask>
,10%的时候将其替换为其它任意单词,10%的时候会保留原始Token的方法解决:
- 80%:my dog is hairy -> my dog is
<mask>
- 10%:my dog is hairy -> my dog is apple
- 10%:my dog is hairy -> my dog is hairy
加入随机Token的原因是因为Transformer要保持对每个输入token的分布式表征,但是只有有 15 15%*10% =1.5% 15的概率会插入随机的token,因此影响比较小。
下面实现MaskLM类,用于预训练的MLM处理。
class MaskLM(nn.Block):
def __init__(self, vocab_size, num_hiddens, **kwargs):
super(MaskLM, self).__init__(**kwargs)
self.mlp = nn.Sequential()
self.mlp.add(
nn.Dense(num_hiddens, flatten=False, activation='relu'),
nn.LayerNorm(),
nn.Dense(vocab_size, flatten=False)
)
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 = np.arange(0, batch_size)
# 假设 `batch_size` = 2, `num_pred_positions` = 3, 那么`batch_idx` 为 `np.array([0, 0, 0, 1, 1, 1])`
batch_idx = np.repeat(batch_idx, num_pred_positions)
masked_X = X[batch_idx, pred_positions]
masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))
mlm_Y_hat = self.mlp(masked_X)
return mlm_Y_hat
演示MaskLM,创建实例mlm并初始化,我们定义mlm_positions为3个索引,以在的任意BERT输入序列中进行预测encoded_X。对于每个预测,结果的大小等于词汇量。
mlm = MaskLM(vocab_size, num_hiddens)
mlm.initialize()
mlm_positions = np.array([[1, 5, 2], [6, 1, 5]])
mlm_Y_hat = mlm(encoded_X, mlm_positions)
mlm_Y_hat.shape
# (2, 3, 10000)
使用预测标签mlm_Y_hat可以计算BERT训练中任务的交叉熵损失。
mlm_Y = np.array([[7, 8, 9], [10, 20, 30]])
loss = gluon.loss.SoftmaxCrossEntropyLoss()
mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape(-1))
mlm_l.shape
# (6,)
2.2 预测下一句
尽管屏蔽语言建模能够对表示单词的双向上下文进行编码,但它并未明确建模文本对之间的逻辑关系。为了帮助理解两个文本序列之间的关系,BERT在其预训练中考虑了二进制分类任务,即 下一句预测。在生成句子对进行预训练时,有一半的时间确实是带有标签“ True”的连续句子。在另一半时间中,第二句话是从语料库中随机抽取的,标签为“ False”。
class NextSentencePred(nn.Block):
def __init__(self, **kwargs):
super(NextSentencePred, self).__init__(**kwargs)
self.output = nn.Dense(2)
def forward(self, X):
# `X` 形状: (batch size, `num_hiddens`)
return self.output(X)
我们可以看到,NextSentencePred 实例的前向推断会为每个BERT输入序列返回二进制预测。
nsp = NextSentencePred()
nsp.initialize()
nsp_Y_hat = nsp(encoded_X)
nsp_Y_hat.shape
# (2, 2)
2.3 模型整合
在对BERT进行预训练时,最终损失函数是用于蒙版语言建模和下一句预测的损失函数的线性组合。现在,我们可以定义BERTModel通过实例化三个班班BERTEncoder,MaskLM和 NextSentencePred。前向推断返回编码的BERT表示encoded_X,掩盖语言建模的 mlm_Y_hat预测以及下一句的预测nsp_Y_hat。
class BERTModel(nn.Block):
def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads,
num_layers, dropout, max_len=1000):
super(BERTModel, self).__init__()
self.encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens,num_heads, num_layers, dropout, max_len)
self.hidden = nn.Dense(num_hiddens, activation='tanh')
self.mlm = MaskLM(vocab_size, num_hiddens)
self.nsp = NextSentencePred()
def forward(self, tokens, segments, valid_lens=None, pred_positions=None):
encoded_X = self.encoder(tokens, segments, valid_lens)
if pred_positions is not None:
mlm_Y_hat = self.mlm(encoded_X, pred_positions)
else:
mlm_Y_hat = None
# MLP 分类器的隐藏层用于预测下一句 0 是 '<cls>' token的index
nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
return encoded_X, mlm_Y_hat, nsp_Y_hat
3. 用于BERT数据集
为了促进BERT预训练的演示,我们使用了较小的WikiText-2语料库:在WikiText-2数据集中,每行表示一个段落,在该段落中,任何标点符号及其前一个标记之间都应插入空格。保留至少包含两个句子的段落。为简单起见,我们仅使用句点作为定界符。
d2l.DATA_HUB['wikitext-2'] = (
'https://s3.amazonaws.com/research.metamind.io/wikitext/'
'wikitext-2-v1.zip', '3c914d17d80b1459be871a5039ac23e752a53cbe')
def _read_wiki(data_dir):
file_name = os.path.join(data_dir, 'wiki.train.tokens')
with open(file_name, 'r') as f:
lines = f.readlines()
# 将大写字母转为小写
paragraphs = [line.strip().lower().split(' . ') for line in lines if len(line.split(' . ')) >= 2]
random.shuffle(paragraphs)
return paragraphs
3.1 下一句预测生成
编写函数用于生成训练示例。
def _get_next_sentence(sentence, next_sentence, paragraphs):
if random.random() < 0.5:
is_next = True
else:
# paragraphs是[[]]类型,因此使用两次随机
next_sentence = random.choice(random.choice(paragraphs))
is_next = False
return sentence, next_sentence, is_next
编写函数用于从输入中生成用于下一句预测的训练示例。这paragraph是句子列表,其中每个句子都是标记列表。该参数 max_len指定在预训练期间BERT输入序列的最大长度。
def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len):
nsp_data_from_paragraph = []
for i in range(len(paragraph) - 1):
tokens_a, tokens_b, is_next = _get_next_sentence(
paragraph[i], paragraph[i + 1], paragraphs)
# 1 为'<cls>' token , 2 为 '<sep>' tokens
if len(tokens_a) + len(tokens_b) + 3 > max_len:
continue
tokens, segments = get_tokens_and_segments(tokens_a, tokens_b)
nsp_data_from_paragraph.append((tokens, segments, is_next))
return nsp_data_from_paragraph
3.2 MLM功能实现
编写一个函数实现屏蔽部分语言功能,在其输入中,tokens是代表BERT输入序列candidate_pred_positions 的token列表,是BERT输入序列的token索引列表,不包括特殊令牌的token(在屏蔽语言建模任务中未预测特殊token),并num_mlm_preds指示预测(调用15%的随机token进行预测)
def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds,
vocab):
# 深度复制tokens
mlm_input_tokens = [token for token in tokens]
pred_positions_and_labels = []
# 打乱用于随机选取15%
random.shuffle(candidate_pred_positions)
for mlm_pred_position in candidate_pred_positions:
if len(pred_positions_and_labels) >= num_mlm_preds:
break
masked_token = None
# 80%概率用于替换为`<mask>`标签
if random.random() < 0.8:
masked_token = '<mask>'
else:
# 10%概率不变
if random.random() < 0.5:
masked_token = tokens[mlm_pred_position]
# 10% 概率使用随机token
else:
masked_token = random.randint(0, len(vocab) - 1)
mlm_input_tokens[mlm_pred_position] = masked_token
pred_positions_and_labels.append(
(mlm_pred_position, tokens[mlm_pred_position]))
return mlm_input_tokens, pred_positions_and_labels
通过下面函数将将BERT输入序列(tokens)作为输入,并返回输入令牌的索引。
def _get_mlm_data_from_tokens(tokens, vocab):
candidate_pred_positions = []
for i, token in enumerate(tokens):
# 跳过特殊token
if token in ['<cls>', '<sep>']:
continue
candidate_pred_positions.append(i)
# 获取总数的15%用于屏蔽处理
num_mlm_preds = max(1, round(len(tokens) * 0.15))
mlm_input_tokens, pred_positions_and_labels = _replace_mlm_tokens(
tokens, candidate_pred_positions, num_mlm_preds, vocab)
pred_positions_and_labels = sorted(pred_positions_and_labels, key=lambda x: x[0])
pred_positions = [v[0] for v in pred_positions_and_labels]
mlm_pred_labels = [v[1] for v in pred_positions_and_labels]
return vocab[mlm_input_tokens], pred_positions, vocab[mlm_pred_labels]
3.3 pad数据
我们仍然需要定义一个辅助函数, _pad_bert_inputs以将特殊的“<pad>
”标记附加到输入中。
def _pad_bert_inputs(examples, max_len, vocab):
max_num_mlm_preds = round(max_len * 0.15)
all_token_ids, all_segments, valid_lens, = [], [], []
all_pred_positions, all_mlm_weights, all_mlm_labels = [], [], []
nsp_labels = []
for (token_ids, pred_positions, mlm_pred_label_ids, segments,
is_next) in examples:
all_token_ids.append(np.array(token_ids + [vocab['<pad>']] * (
max_len - len(token_ids)), dtype='int32'))
all_segments.append(np.array(segments + [0] * (
max_len - len(segments)), dtype='int32'))
# 不及max_len部分使用pad填充
valid_lens.append(np.array(len(token_ids), dtype='float32'))
all_pred_positions.append(np.array(pred_positions + [0] * (
max_num_mlm_preds - len(pred_positions)), dtype='int32'))
# 将填充token设置0权重以计算损失的时候不计入
all_mlm_weights.append(
np.array([1.0] * len(mlm_pred_label_ids) + [0.0] * (
max_num_mlm_preds - len(pred_positions)), dtype='float32'))
all_mlm_labels.append(np.array(mlm_pred_label_ids + [0] * (
max_num_mlm_preds - len(mlm_pred_label_ids)), dtype='int32'))
nsp_labels.append(np.array(is_next))
return (all_token_ids, all_segments, valid_lens, all_pred_positions,
all_mlm_weights, all_mlm_labels, nsp_labels)
3.4 文本转换为数据集
为简单起见,我们使用该d2l.tokenize函数进行标记化。出现少于五次的不频繁标记将被滤除。
class _WikiTextDataset(gluon.data.Dataset):
def __init__(self, paragraphs, max_len):
paragraphs = [d2l.tokenize( paragraph, token='word') for paragraph in paragraphs]
sentences = [sentence for paragraph in paragraphs
for sentence in paragraph]
self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ '<pad>', '<mask>', '<cls>', '<sep>'])
examples = []
for paragraph in paragraphs:
examples.extend(_get_nsp_data_from_paragraph(
paragraph, paragraphs, self.vocab, max_len))
# 获取下一个句子预测任务的数据
examples = [(_get_mlm_data_from_tokens(tokens, self.vocab)
+ (segments, is_next))
for tokens, segments, is_next in examples]
# 获得pad之后数据
(self.all_token_ids, self.all_segments, self.valid_lens,
self.all_pred_positions, self.all_mlm_weights,
self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs(
examples, max_len, self.vocab)
def __getitem__(self, idx):
return (self.all_token_ids[idx], self.all_segments[idx],
self.valid_lens[idx], self.all_pred_positions[idx],
self.all_mlm_weights[idx], self.all_mlm_labels[idx],
self.nsp_labels[idx])
def __len__(self):
return len(self.all_token_ids)
通过上面的函数定义load_data_wiki函数用于将数据下载并生成数据迭代器。
def load_data_wiki(batch_size, max_len):
num_workers = d2l.get_dataloader_workers()
data_dir = d2l.download_extract('wikitext-2', 'wikitext-2')
paragraphs = _read_wiki(data_dir)
train_set = _WikiTextDataset(paragraphs, max_len)
train_iter = gluon.data.DataLoader(train_set, batch_size, shuffle=True,num_workers=num_workers)
return train_iter, train_set.vocab
将批次大小设置为512,将BERT输入序列的最大长度设置为64,我们将打印出BERT预训练示例的小批量的形状。请注意,在每个BERT输入序列中, 10 ( 64×0.15 )的位置是针对掩盖语言建模任务的预测。
batch_size, max_len = 512, 64
train_iter, vocab = load_data_wiki(batch_size, max_len)
for (tokens_X, segments_X, valid_lens_x, pred_positions_X, mlm_weights_X, mlm_Y, nsp_y) in train_iter:
print(tokens_X.shape, segments_X.shape, valid_lens_x.shape,
pred_positions_X.shape, mlm_weights_X.shape, mlm_Y.shape,
nsp_y.shape)
break
# (512, 64) (512, 64) (512,) (512, 10) (512, 10) (512, 10) (512,)
4. 预训练BERT
基本模型( B E R T B A S E BERT_{BASE} BERTBASE )使用12层(变压器编码器块)和768个隐藏单元(隐藏大小)和12个自我关注头。大型模型 ( B E R T L A R G E BERT_{LARGE} BERTLARGE )使用24个图层,其中包含1024个隐藏单元和16个自我关注头。值得注意的是,前者有1.1亿个参数,而后者有3.4亿个参数。为了便于演示,我们定义了一个小BERT,它使用2层,128个隐藏单元和2个自我关注头。
net = d2l.BERTModel(len(vocab), num_hiddens=128, ffn_num_hiddens=256, num_heads=2, num_layers=2, dropout=0.2)
devices = d2l.try_all_gpus()
net.initialize(init.Xavier(), ctx=devices)
loss = gluon.loss.SoftmaxCELoss()
定义一个辅助函数用于计算MLM和下一句预测的损失。BERT预训练的最终损失是MLM和下一句预测损失之和。
def _get_batch_loss_bert(net, loss, vocab_size, tokens_X_shards,
segments_X_shards, valid_lens_x_shards,
pred_positions_X_shards, mlm_weights_X_shards,
mlm_Y_shards, nsp_y_shards):
mlm_ls, nsp_ls, ls = [], [], []
for (tokens_X_shard, segments_X_shard, valid_lens_x_shard,
pred_positions_X_shard, mlm_weights_X_shard, mlm_Y_shard,
nsp_y_shard) in zip(
tokens_X_shards, segments_X_shards, valid_lens_x_shards,
pred_positions_X_shards, mlm_weights_X_shards, mlm_Y_shards,
nsp_y_shards):
_, mlm_Y_hat, nsp_Y_hat = net( tokens_X_shard, segments_X_shard, valid_lens_x_shard.reshape(-1), pred_positions_X_shard)
# MLM损失
mlm_l = loss(
mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y_shard.reshape(-1),
mlm_weights_X_shard.reshape((-1, 1)))
mlm_l = mlm_l.sum() / (mlm_weights_X_shard.sum() + 1e-8)
# 下一句预测损失
nsp_l = loss(nsp_Y_hat, nsp_y_shard)
nsp_l = nsp_l.mean()
mlm_ls.append(mlm_l)
nsp_ls.append(nsp_l)
ls.append(mlm_l + nsp_l)
npx.waitall()
return mlm_ls, nsp_ls, ls
编写训练函数定义net对WikiText-2(train_iter)数据集上的BERT进行预训练的过程。
import plotly.express as px
import pandas as pd
def train_bert(train_iter, net, loss, vocab_size, devices, log_interval,
num_steps):
trainer = gluon.Trainer(net.collect_params(), 'adam', {
'learning_rate': 1e-3})
step, timer = 0, d2l.Timer()
data_lst = []
# 记录为mlm的损失,nsp损失,句子数,token数
metric = d2l.Accumulator(4)
num_steps_reached = False
while step < num_steps and not num_steps_reached:
for batch in train_iter:
(tokens_X_shards, segments_X_shards, valid_lens_x_shards,
pred_positions_X_shards, mlm_weights_X_shards,
mlm_Y_shards, nsp_y_shards) = [gluon.utils.split_and_load(
elem, devices, even_split=False) for elem in batch]
timer.start()
with autograd.record():
mlm_ls, nsp_ls, ls = _get_batch_loss_bert(
net, loss, vocab_size, tokens_X_shards, segments_X_shards,
valid_lens_x_shards, pred_positions_X_shards,
mlm_weights_X_shards, mlm_Y_shards, nsp_y_shards)
for l in ls:
l.backward()
trainer.step(1)
mlm_l_mean = sum([float(l) for l in mlm_ls]) / len(mlm_ls)
nsp_l_mean = sum([float(l) for l in nsp_ls]) / len(nsp_ls)
metric.add(mlm_l_mean, nsp_l_mean, batch[0].shape[0], 1)
timer.stop()
if (step + 1) % log_interval == 0:
data_lst.append((step + 1, metric[0] / metric[3], metric[1] / metric[3]))
step += 1
if step == num_steps:
num_steps_reached = True
break
print(f'MLM loss {metric[0] / metric[3]:.3f}, NSP loss {metric[1] / metric[3]:.3f}')
print(f'{metric[2] / timer.sum():.1f} sentence pairs/sec on {str(devices)}')
fig = px.line(pd.DataFrame(data_lst, columns=['step', 'mlm', 'nsp']), x='step', y=['mlm', 'nsp'], width=500, height=360, labels={
'value':'loss'})
fig.show()
train_bert(train_iter, net, loss, len(vocab), devices, 1, 50)
5. 使用BERT表是文本
在对BERT进行预训练之后,我们可以使用它来表示单个文本,文本对或其中的任何标记。以下函数返回和中 net所有toekn
def get_bert_encoding(net, tokens_a, tokens_b=None):
tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b)
token_ids = np.expand_dims(np.array(vocab[tokens], ctx=devices[0]), axis=0)
segments = np.expand_dims(np.array(segments, ctx=devices[0]), axis=0)
valid_len = np.expand_dims(np.array(len(tokens), ctx=devices[0]), axis=0)
encoded_X, _, _ = net(token_ids, segments, valid_len)
return encoded_X
“a crane is flying”这句话输入模型,获取crane的向量。
tokens_a = ['a', 'crane', 'is', 'flying']
encoded_text = get_bert_encoding(net, tokens_a)
encoded_text_crane = encoded_text[:, 2, :]
encoded_text_crane[0][:5]
# array([-1.3856754 , 0.36426234, -0.81602454, 0.61464363, 0.55311435], ctx=gpu(0))
在将一对句子“a crane driver came” 和 “he just left”输入。获取crane向量,与上面的对比发现同一个词其上下文不同特征不同。
tokens_a, tokens_b = ['a', 'crane', 'driver', 'came'], ['he', 'just', 'left']
encoded_pair = get_bert_encoding(net, tokens_a, tokens_b)
encoded_pair_crane = encoded_pair[:, 2, :]
encoded_pair_crane[0][:5]
# array([-1.4317628 , 0.23290965, -0.81553113, 0.7340084 , 0.45793456], ctx=gpu(0))
6. 参考
https://d2l.ai/chapter_natural-language-processing-pretraining/bert-pretraining.html
https://zhuanlan.zhihu.com/p/48612853
7.代码
来源:oschina
链接:https://my.oschina.net/u/4400642/blog/4667794