BERT
BERT的目的是构建一个具有良好推广性的语言模型。它的主要目的并不是进行预测,而是理解句子的意思从而推断上下文关系。
在大量未进行标注的文本数据进行学习后获得一个强理解力的通用模型,这个过程称为预训练。在应用于下游任务时再次在具体任务的数据集上对预训练的参数进行更新,这个过程叫做微调,以期获得更加符合任务的模型。
1. BERT 之前的相关工作
ELMo双向RNN架构,使用双向特征提取,对每个任务构建神经网络。将训练好的特征和输入一起输入模型进行训练。
GPT已经在未标记的文本进行了预训练,又在训练下游任务时进行参数微调,但只使用从左到右的单向顺序。
2. BERT 的模型架构
2.1 BERT 的预训练策略
BERT预训练的使用是长篇的文本,预训练会对句子进行切分。切分分为两种级别,一种是单词级的,一种是句子级的。
单词级切分使用的是WordPiece方法,有点像根据词根分割,如果有较长词汇出现,可能会被分成两个常见词或者是一个常见词和一个后缀,最后能够获得一个较小词典。
切词:flightless → flight ##less
playing → play ##ing ##表示该词语是前面词语的后缀
句子切分会在关键位置添加标记。句首 [CLS] ,句子分隔 [SEP],掩模 [MASK]。
BERT的输入由三个部分组成:单词编码,位置编码,句子编码。
其中位置编码和句子编码都是整型标记
简易模拟编码过程:
# 正则表达
import re
# 数学函数
import math
import torch
# 数组处理
import numpy as np
from random import *
import torch.nn as nn
# 优化算法
import torch.optim as optim
import torch.utils.data as Data
text = (
'Hello, how are you? I am Romeo.\n' # R
'Hello, Romeo My name is Juliet. Nice to meet you.\n' # J
'Nice meet you too. How are you today?\n' # R
'Great. My baseball team won the competition.\n' # J
'Oh Congratulations, Juliet\n' # R
'Thank you Romeo\n' # J
'Where are you going today?\n' # R
'I am going shopping. What about you?\n' # J
'I am going to visit my grandmother. she is not very well' # R
)
# 将标点代替为空格 转小写 分割句子
sentences = re.sub("[.,!?\\-]", ' ', text.lower()).split('\n')
# 使用空格连接元素(每个句子) 分割(空格)为单词列表 列表转集合(去除重复元素,做字典词表) 集合转回列表
word_list = list(set(" ".join(sentences).split()))
# 创建标志字典(将添加单词映射)
word2idx = {'[PAD]':0, '[CLS]':1, '[SEP]':2, '[MASK]':3} # 填充 开始 分隔 掩蔽
# 创建映射字典(从4开始)
for i, w in enumerate(word_list):
word2idx[w] = i + 4
# 映射回单词的字典
idx2word = {i:w for i, w in enumerate(word2idx)}
# 字典长度
vocab_size = len(word2idx)
# 创建句子的单词编号|列表
token_list = list()
# 存储单词标号
for sentence in sentences:
arr = [word2idx[s] for s in sentence.split()]
token_list.append(arr)
'''输出演示'''
print(token_list)
print(sentence)
2.1.1 Masked LM (MLM) 掩蔽语言模型
给定一句话,随机抹去这句话中的一个或几个词,要求根据剩余词汇预测被抹去的几个词分别是什么。
选取句子中15%的词语进行随机选取掩模。其中,选中的80%使用 [MASK] 标记替代;10%使用随机词汇替代;10%保持原状。
由于在后续微调任务中语句中并不会出现 [MASK] 标记,这种类似于“完形填空”的方法如果都使用 [MASK] 标记替代会对下游任务造成影响。另一个好处是:预测一个词汇时,模型并不知道输入对应位置的词汇是否为正确的词汇( 10% 概率),这就迫使模型更多地依赖于上下文信息去预测词汇,并且赋予了模型一定的纠错能力。
2.1.2 Next Sentence Prediction (NSP) 下一句推断
给定一篇文章中的两句话,判断第二句话在文本中是否紧跟在第一句话之后。
随机提取句子对,判断后一个是否是前一个的下一句(概率为50%)。
预训练代码
# 参数设置
# 设置句子长度,不够的PAD补全
maxlen = 30
# 批次大小
batch_size = 6
# 最大掩蔽单词数量
max_pred = 5
# encoder层数
n_layers = 6
# 注意力头数
n_heads = 12
# 特征数量
d_model = 768
# 全连接层隐藏层参数数量
d_ff = d_model * 4
# KQV大小(多头映射,KQ一样大)
d_k = d_v = 64
# 解码器输入有多少句话
n_segments = 2
# 数据预处理
# 随机掩模替换
def make_data():
batch = []
# 上下文是否相连数量
positive = negetive = 0
# 选取正负例至某一方达到批次量一半
while positive != batch_size/2 or negetive != batch_size/2:
# 随机选取句子
token_a_index, token_b_index = randrange(len(sentences)), randrange(len(sentences))
# 获取单词序列
token_a, token_b = token_list[token_a_index], token_list[token_b_index]
# 添加开始和分隔标志,将两个单词序列连起来
input_ids = [word2idx['[CLS]']] + token_a + [word2idx['[SEP]']] + token_b + [word2idx['[SEP]']]
# 创建句子序号序列,开头和中间的分割符号算进第一个句子
segment_ids = [0] * (1 + len(token_a) + 1) + [1] * (len(token_b) + 1)
# MASK LM任务
# 要求既不超过15%也不超过最大值5
n_pred = min(max_pred, int(len(input_ids) * 0.15))
# 获取可掩蔽在输入序列的编号,标志符号不算
cand_mask_pos = [i for i, token in enumerate(input_ids) if token != word2idx['[CLS]'] and token != word2idx['[SEP]']]
# 打乱编号顺序
shuffle(cand_mask_pos)
# 掩蔽词序列 掩蔽编码序列
masked_tokens, masked_pos = [], []
# 取已打乱编号进行掩蔽
for pos in cand_mask_pos[: n_pred]:
masked_pos.append(pos)
masked_tokens.append(input_ids[pos])
# 取随机数 80%,10%,10%进行划分
if random() < 0.8:
input_ids[pos] = word2idx['[MASK]']
elif random() < 0.9:
index = randint(0, vocab_size - 1)
while index < 4:
index = randint(0, vocab_size - 1)
input_ids[pos] = index
# 计算填充长度
n_pad = maxlen - len(input_ids)
input_ids.extend([0] * n_pad)
segment_ids.extend([0] * n_pad)
# 将掩蔽序列都填充到最大长度
if n_pred < max_pred:
n_pad = max_pred - n_pred
masked_pos.extend([0] * n_pad)
masked_tokens.extend([0] * n_pad)
# 句子是否邻接
if token_a_index + 1 == token_b_index and positive < batch_size/2:
# 加入批次
batch.append([input_ids, segment_ids, masked_pos, masked_tokens, True])
elif token_a_index + 1 != token_b_index and negetive < batch_size/2:
batch.append([input_ids, segment_ids, masked_pos, masked_tokens, False])
return batch
'''输出演示'''
tokens_a_index, tokens_b_index = randrange(len(sentences)), randrange(len(sentences)) # sample random index in sentences
tokens_a, tokens_b = token_list[tokens_a_index], token_list[tokens_b_index]
input_ids = [word2idx['[CLS]']] + tokens_a + [word2idx['[SEP]']] + tokens_b + [word2idx['[SEP]']]
segment_ids = [0] * (1 + len(tokens_a) + 1) + [1] * (len(tokens_b) + 1)
print(tokens_a, tokens_b)
print(tokens_a_index)
2.2 BERT 的主体模型
BERT只使用了Transformer的Encoder部分,作者分别用 12 层和 24 层 Transformer Encoder 组装了两套 BERT 模型
- B E R T B A S E : L = 12 , H = 768 , A = 12 , T o t a l P a r a m e t e r s = 110 M BERT_{BASE} : L = 12, H = 768, A = 12, TotalParameters = 110M BERTBASE:L=12,H=768,A=12,TotalParameters=110M
- B E R T L A R G E : L = 24 , H = 1024 , A = 16 , T o t a l P a r a m e t e r s = 340 M BERT_{LARGE} : L = 24, H = 1024, A = 16, TotalParameters = 340M BERTLARGE:L=24,H=1024,A=16,TotalParameters=340M
其中 L 表示 Transformer Encoder层的层数;H 表示隐藏层数量(即每个词元的特征数);A 表示注意力的头数。
参数总数的计算:
结论:字典长度× H H H+ H 2 H^2 H2×4+ H 2 H^2 H2×8
下面是一个潦草的过程: