BERT类预训练语言模型
文章目录
- BERT类预训练语言模型
- 1. BERT简介
- 1.1 BERT简介及特点
- 1.2 传统方法和预训练方法
- 1.3 BERT的性质
- 2. BERT结构
- 2.1 输入层以及位置编码
- 2.2 Transformer编码器层
- 2.3 前馈神经网络层
- 2.4 残差连接层
- 2.5 输出层
- 3. BERT类模型简要笔记
- 4. 代码工程实践
1. BERT简介
1.1 BERT简介及特点
BERT(Bidirectional Encoder Representations from Transformers)是一种预训练模型,它是自然语言处理(NLP)领域的重大里程碑,被认为是当前的State-of-the-Art模型之一。BERT的设计理念和结构基于Transformer模型,通过无监督学习方式进行训练,并且能够适配各种NLP任务。
预训练模型是指在大规模文本数据上进行大量无监督训练,学习得到丰富的语言表示。BERT的预训练任务是通过掩码语言模型(Masked Language Model, MLM)和下一句预测(Next Sentence Prediction, NSP)两个阶段进行的。在MLM中,输入文本的一部分被随机掩码,模型需要预测这些被掩码的词。在NSP中,模型需要判断两个句子是否是原文中的连续句子。
BERT的无监督训练使得它能够学习到丰富的句子级和词级表示,因为它需要理解上下文并进行语言推理。这种能力使得BERT成为适配各种NLP任务的理想选择。在使用BERT进行下游任务时,可以将其作为一个特征提取器或者通过在BERT之上添加一个任务特定的输出层进行微调。
BERT的结构基于Transformer模型,Transformer是一种基于自注意力机制的深度神经网络架构。它能够捕捉句子中的上下文信息,使得模型能够同时考虑句子中的前后词语。这种双向建模的能力是BERT的关键特点之一,也是相对于之前的模型(如基于循环神经网络的模型)的显著改进。
BERT的出现引起了NLP领域的重大变革。它在多个NLP任务上取得了突破性的结果,如问答、文本分类、命名实体识别等。BERT的成功也启发了许多后续模型的设计和改进,包括GPT、RoBERTa等。这些模型在NLP领域取得了巨大的进展,使得研究人员和从业者能够更好地处理自然语言数据,并且在多个任务上取得更高的性能。
1.2 传统方法和预训练方法
对于传统方法,主要集中于:
- 设计模型结构
- 收集/标准训练数据
- 使用标注数据进行模型训练
- 真实场景预测
以上流程也称为Fine-tune,也即微调的过程
预训练的方法相较上面的流程多了Pre-train的过程: - 收集海量无标注文本数据
- 进行模型预训练,并在任务模型中使用
此两步称为Pre-train的过程
- 设计模型结构
- 收集/标准训练数据
- 使用标注数据进行模型训练
- 真实场景预测
预训练的方法也即Pre-train+Fine-tune的过程
BERT的预训练方式包括两个任务:掩码语言模型(Masked Language Model, MLM)和下一句预测(Next Sentence Prediction, NSP)。
在掩码语言模型(MLM)任务中,BERT会对输入的文本进行随机的掩码处理。具体来说,对于输入文本中的一部分词,约15%的词会被掩码成特定的标记(通常是"[MASK]"),一小部分词会被替换成随机的词,而其余的词保持不变。模型的目标是通过上下文中的其他词来预测这些被掩码的词的原始内容。
可以通俗理解为完形填空的过程,与n-gram的区别,双向,考虑的是词前后两个方向的信息
下一句预测(NSP)任务旨在训练模型对句子级别的语义进行建模。模型会接收一对句子作为输入,并需要判断这两个句子是否在原文中是连续的。为了生成训练数据,BERT从大规模的文本数据中随机挑选一对句子,并在输入中加入特殊的标记来指示两个句子的边界。
可以浅显的视为句子级别的语言模型
通过这两个任务的预训练,BERT学习到了丰富的语言表示。MLM任务使得模型能够理解上下文并预测被掩码的词,促使模型学会对句子中的语义进行建模。NSP任务则让模型能够理解句子之间的关系和语义连贯性。
这种预训练方式使得BERT能够从大规模无标签数据中学习到通用的语言表示,这些表示可以应用于各种下游NLP任务,通过微调或作为特征提取器来实现任务特定的性能提升。
1.3 BERT的性质
首先,BERT的本质是一种文本表征(context representation),做的是一个文本->矩阵(max length × \times ×hidden size)或者文本->向量(1 × \times ×hidden size),word2vec也可以做到同样的事,但是word2vec是静态的,而BERT是动态的,因为BERT是将输入考虑语序后经过transformer输出的,一对一的
BERT的表现比RNN优秀,原因概括有两点:
- 采用了预训练的思想
- 采用了更有效的网络结构
2. BERT结构
Transformer中最核心的点是注意力机制,核心文章为《Attention is All you Need》,BERT的模型主体结构使用Google在17年提出的Transformer结构。
BERT的Encoder结构
BERT的Encoder部分是由多个相同结构的Transformer编码器组成,可以逐层分析其结构。假设BERT包含L个Transformer编码器层。
2.1 输入层以及位置编码
输入层:输入是一个经过词嵌入(Word Embedding)的序列,其中每个词被表示为一个向量。输入序列还包括特殊的标记,如句子的开始([CLS])和分隔符([SEP])。
Segment Embedding 每一段文本的此部分相同,此部分的参数量为段数
×
768
\times 768
×768,Token Embedding 将每个词映射为一个向量(包括[CLS]和[SEP]),参数量为词表大小
×
768
\times 768
×768
位置编码(Positional Encoding):在输入序列的词嵌入中,Transformer编码器需要一种方式来编码单词的位置信息。为此,BERT使用了位置编码,它将位置信息嵌入到词嵌入向量中,使得模型能够考虑单词在序列中的顺序。
Position Embedding 将词所在的位置映射成为一个向量,参数量为
512
×
768
512\times 768
512×768,每个向量均为768维,最大长度限制为512,论文规定,上面三个embedding加和之后会做Layer Normalization
2.2 Transformer编码器层
Transformer编码器层:BERT的Encoder部分由L个Transformer编码器层堆叠而成,堆叠了12次。
自注意力层(Self-Attention Layer):每个Transformer编码器层包含多个自注意力头(self-attention head),这些头可以并行计算。自注意力层的输入包括三个向量序列:查询(query)、键(key)和值(value)。自注意力机制通过计算注意力权重来将查询与键进行交互,然后将注意力权重应用于值向量,最后对加权后的值向量进行求和。这种交互能够捕捉到输入序列中每个位置与其他位置的关系,并为每个位置生成一个更新的表示。
查询(query)(Q):查询向量用于确定当前位置与其他位置的关联程度。在自注意力机制中,通过将查询向量与其他位置的键进行点积操作,得到了每个位置对其他位置的注意力权重。查询向量可以理解为当前位置所关注的信息。
键(key)(K):键向量用于提供其他位置的信息,以便查询向量可以计算与它们的关联程度。通过将查询向量与键向量进行点积操作,可以得到注意力权重。键向量可以理解为其他位置的表示。
值(value)(V):值向量包含了与每个位置相关的信息。注意力权重会被应用于值向量,以得到每个位置的加权和作为最终的输出。值向量可以理解为当前位置所携带的信息。
在Transformer算法中使用qkv参数的具体步骤:
-
线性变换:首先,输入序列经过三个独立的线性变换来计算查询(Q)、键(K)和值(V)。对于输入序列中的每个位置,使用不同的权重矩阵对输入进行线性变换,得到相应的查询向量Q、键向量K和值向量V。
-
计算注意力权重:使用查询向量Q与键向量K进行点积操作,然后通过缩放(一般是将结果除以一个缩放因子)来计算注意力权重。缩放可以使得注意力权重的分布更稳定。注意力权重衡量了每个位置与其他位置的关联程度,决定了每个位置在注意力机制中的重要性。
-
注意力权重和值的加权求和:将注意力权重与值向量V相乘,并对这些乘积进行加权求和,得到注意力机制的输出。注意力权重决定了每个位置对最终表示的贡献程度,而值向量提供了实际的特征信息。
-
多头注意力机制:在Transformer中通常采用多头注意力机制,通过并行计算多组qkv参数来提高模型的表示能力。多头注意力机制中,每组qkv参数都会进行独立的线性变换和注意力计算,得到多组注意力输出。最后,将多组注意力输出进行拼接,并通过线性变换得到最终的表示。
在方阵的运算中解决了远字的关系问题,由于是方阵,管你多远,都能计算到,每个字对自己相对应的字的关系,所以是自注意力。
自注意力多头机制:在传统的注意力机制中,通过计算注意力权重来给定一个位置与序列中其他位置的关联程度。而多头注意力机制则通过同时使用多个注意力头,每个头都可以学习到不同的注意力权重,从而提供了更丰富的表达能力。具体而言,多头注意力机制将输入序列进行多次线性变换,得到多组查询(query)、键(key)和值(value)向量。然后,在每个注意力头中,通过计算注意力权重并将权重应用于值向量,得到每个头的输出。最后,将多个头的输出进行拼接,并通过线性变换获得最终的注意力表示。
通过多头注意力机制,模型能够同时学习到不同位置之间的不同关系。不同的注意力头可以关注不同的语义信息,例如一个头可以关注局部的语法结构,另一个头可以关注全局的上下文语义。这样,模型能够从不同角度综合考虑输入序列的信息,提供更全面的表示能力。
在BERT中,多头注意力机制被广泛应用于每个Transformer编码器层中的自注意力层。通过使用多个注意力头,BERT能够同时捕捉到不同关系的语义信息,提供更丰富的上下文表示。每个头学习到的注意力权重可以被看作是一种关注不同方面的“专家”,它们共同工作来提供全局的上下文理解。
对于“头”的size计算如下
a t t e n t i o n _ h e a d _ s i z e = i n t ( h i d d e n _ s i z e / n u m _ a t t e n t i o n _ h e a d s ) attention\_head\_size = int(hidden\_size / num\_attention\_heads) attention_head_size=int(hidden_size/num_attention_heads)
attention_head_size是个固定值为64,来自谷歌实验,hidden_size为768,num_attention_heads为超参数固定为12,将768砍成12份(将矩阵拧了变形的操作),相当于12个模型并行运行,然后12个模型都去自注意力
除以根号64的原因是为了方便归一化(反映在算法的代码中),防止向量中的数值差距过大导致的非0即1的问题,最终的输出结果在多次变换后维度未发生改变
2.3 前馈神经网络层
前馈神经网络层(Feed-Forward Neural Network Layer):在自注意力层之后,每个Transformer编码器层都包含一个前馈神经网络层。前馈神经网络层由两个全连接层组成,通过一个非线性激活函数进行连接。这个前馈神经网络层能够对自注意力层输出的每个位置的表示进行非线性变换和映射。
2.4 残差连接层
残差连接(Residual Connection)和层归一化(Layer Normalization):在每个子层之后,BERT采用残差连接和层归一化来增强信息流动和减轻梯度消失问题。残差连接将子层的输入与子层的输出相加,从而保留了原始输入的信息。层归一化则对残差连接的结果进行归一化,使得每个子层的输入都有相似的均值和方差。
将输入前和输入后的向量加起来再给下一层输入
2.5 输出层
输出层:BERT的Encoder部分的最后一层输出被用作下游NLP任务的输入。对于分类任务,例如文本分类,可以在输出层之上添加一个全连接层和Softmax激活函数来进行预测。
sequence_output:整句话的每个字符对应的总体矩阵;pooler_output:代表整句话的一个向量
3. BERT类模型简要笔记
4. 代码工程实践
手动实现Bert结构
import torch
import math
import numpy as np
from transformers import BertModel
'''
通过手动矩阵运算实现Bert结构
模型文件下载 https://huggingface.co/models
'''
bert = BertModel.from_pretrained(r"D:\badou\pretrain_model\chinese_bert_likes\bert-base-chinese", return_dict=False)
state_dict = bert.state_dict()
bert.eval()
x = np.array([2450, 15486, 15167, 2110]) #通过vocab对应输入:深度学习
torch_x = torch.LongTensor([x]) #pytorch形式输入
# seqence_output, pooler_output = bert(torch_x)
# print(seqence_output.shape, pooler_output.shape)
# print(seqence_output, pooler_output)
# print(bert.state_dict().keys()) #查看所有的权值矩阵名称
#softmax归一化
def softmax(x):
return np.exp(x)/np.sum(np.exp(x), axis=-1, keepdims=True)
#gelu激活函数
def gelu(x):
return 0.5 * x * (1 + np.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * np.power(x, 3))))
class DiyBert:
#将预训练好的整个权重字典输入进来
def __init__(self, state_dict):
self.num_attention_heads = 12
self.hidden_size = 768
self.num_layers = 1
self.load_weights(state_dict)
def load_weights(self, state_dict):
#embedding部分
self.word_embeddings = state_dict["embeddings.word_embeddings.weight"].numpy()
self.position_embeddings = state_dict["embeddings.position_embeddings.weight"].numpy()
self.token_type_embeddings = state_dict["embeddings.token_type_embeddings.weight"].numpy()
self.embeddings_layer_norm_weight = state_dict["embeddings.LayerNorm.weight"].numpy()
self.embeddings_layer_norm_bias = state_dict["embeddings.LayerNorm.bias"].numpy()
self.transformer_weights = []
#transformer部分,有多层
for i in range(self.num_layers):
q_w = state_dict["encoder.layer.%d.attention.self.query.weight" % i].numpy()
q_b = state_dict["encoder.layer.%d.attention.self.query.bias" % i].numpy()
k_w = state_dict["encoder.layer.%d.attention.self.key.weight" % i].numpy()
k_b = state_dict["encoder.layer.%d.attention.self.key.bias" % i].numpy()
v_w = state_dict["encoder.layer.%d.attention.self.value.weight" % i].numpy()
v_b = state_dict["encoder.layer.%d.attention.self.value.bias" % i].numpy()
attention_output_weight = state_dict["encoder.layer.%d.attention.output.dense.weight" % i].numpy()
attention_output_bias = state_dict["encoder.layer.%d.attention.output.dense.bias" % i].numpy()
attention_layer_norm_w = state_dict["encoder.layer.%d.attention.output.LayerNorm.weight" % i].numpy()
attention_layer_norm_b = state_dict["encoder.layer.%d.attention.output.LayerNorm.bias" % i].numpy()
intermediate_weight = state_dict["encoder.layer.%d.intermediate.dense.weight" % i].numpy()
intermediate_bias = state_dict["encoder.layer.%d.intermediate.dense.bias" % i].numpy()
output_weight = state_dict["encoder.layer.%d.output.dense.weight" % i].numpy()
output_bias = state_dict["encoder.layer.%d.output.dense.bias" % i].numpy()
ff_layer_norm_w = state_dict["encoder.layer.%d.output.LayerNorm.weight" % i].numpy()
ff_layer_norm_b = state_dict["encoder.layer.%d.output.LayerNorm.bias" % i].numpy()
self.transformer_weights.append([q_w, q_b, k_w, k_b, v_w, v_b, attention_output_weight, attention_output_bias,
attention_layer_norm_w, attention_layer_norm_b, intermediate_weight, intermediate_bias,
output_weight, output_bias, ff_layer_norm_w, ff_layer_norm_b])
#pooler层
self.pooler_dense_weight = state_dict["pooler.dense.weight"].numpy()
self.pooler_dense_bias = state_dict["pooler.dense.bias"].numpy()
#bert embedding,使用3层叠加,在经过一个embedding层
def embedding_forward(self, x):
# x.shape = [max_len]
we = self.get_embedding(self.word_embeddings, x) # shpae: [max_len, hidden_size]
# position embeding的输入 [0, 1, 2, 3]
pe = self.get_embedding(self.position_embeddings, np.array(list(range(len(x))))) # shpae: [max_len, hidden_size]
# token type embedding,单输入的情况下为[0, 0, 0, 0]
te = self.get_embedding(self.token_type_embeddings, np.array([0] * len(x))) # shpae: [max_len, hidden_size]
embedding = we + pe + te
# 加和后有一个归一化层
embedding = self.layer_norm(embedding, self.embeddings_layer_norm_weight, self.embeddings_layer_norm_bias) # shpae: [max_len, hidden_size]
return embedding
#embedding层实际上相当于按index索引,或理解为onehot输入乘以embedding矩阵
def get_embedding(self, embedding_matrix, x):
return np.array([embedding_matrix[index] for index in x])
#执行全部的transformer层计算
def all_transformer_layer_forward(self, x):
for i in range(self.num_layers):
x = self.single_transformer_layer_forward(x, i)
return x
#执行单层transformer层计算
def single_transformer_layer_forward(self, x, layer_index):
weights = self.transformer_weights[layer_index]
#取出该层的参数,在实际中,这些参数都是随机初始化,之后进行预训练
q_w, q_b, \
k_w, k_b, \
v_w, v_b, \
attention_output_weight, attention_output_bias, \
attention_layer_norm_w, attention_layer_norm_b, \
intermediate_weight, intermediate_bias, \
output_weight, output_bias, \
ff_layer_norm_w, ff_layer_norm_b = weights
#self attention层
attention_output = self.self_attention(x,
q_w, q_b,
k_w, k_b,
v_w, v_b,
attention_output_weight, attention_output_bias,
self.num_attention_heads,
self.hidden_size)
#bn层,并使用了残差机制
x = self.layer_norm(x + attention_output, attention_layer_norm_w, attention_layer_norm_b)
#feed forward层
feed_forward_x = self.feed_forward(x,
intermediate_weight, intermediate_bias,
output_weight, output_bias)
#bn层,并使用了残差机制
x = self.layer_norm(x + feed_forward_x, ff_layer_norm_w, ff_layer_norm_b)
return x
# self attention的计算
def self_attention(self,
x,
q_w,
q_b,
k_w,
k_b,
v_w,
v_b,
attention_output_weight,
attention_output_bias,
num_attention_heads,
hidden_size):
# x.shape = max_len * hidden_size
# q_w, k_w, v_w shape = hidden_size * hidden_size
# q_b, k_b, v_b shape = hidden_size
q = np.dot(x, q_w.T) + q_b # shape: [max_len, hidden_size] W * X + B lINER
k = np.dot(x, k_w.T) + k_b # shpae: [max_len, hidden_size]
v = np.dot(x, v_w.T) + v_b # shpae: [max_len, hidden_size]
attention_head_size = int(hidden_size / num_attention_heads)
# q.shape = num_attention_heads, max_len, attention_head_size
q = self.transpose_for_scores(q, attention_head_size, num_attention_heads)
# k.shape = num_attention_heads, max_len, attention_head_size
k = self.transpose_for_scores(k, attention_head_size, num_attention_heads)
# v.shape = num_attention_heads, max_len, attention_head_size
v = self.transpose_for_scores(v, attention_head_size, num_attention_heads)
# qk.shape = num_attention_heads, max_len, max_len
qk = np.matmul(q, k.swapaxes(1, 2))
qk /= np.sqrt(attention_head_size)
qk = softmax(qk)
# qkv.shape = num_attention_heads, max_len, attention_head_size
qkv = np.matmul(qk, v)
# qkv.shape = max_len, hidden_size
qkv = qkv.swapaxes(0, 1).reshape(-1, hidden_size)
# attention.shape = max_len, hidden_size
attention = np.dot(qkv, attention_output_weight.T) + attention_output_bias
return attention
#多头机制
def transpose_for_scores(self, x, attention_head_size, num_attention_heads):
# hidden_size = 768 num_attent_heads = 12 attention_head_size = 64
max_len, hidden_size = x.shape
x = x.reshape(max_len, num_attention_heads, attention_head_size)
x = x.swapaxes(1, 0) # output shape = [num_attention_heads, max_len, attention_head_size]
return x
#前馈网络的计算
def feed_forward(self,
x,
intermediate_weight, # intermediate_size, hidden_size
intermediate_bias, # intermediate_size
output_weight, # hidden_size, intermediate_size
output_bias, # hidden_size
):
# output shpae: [max_len, intermediate_size]
x = np.dot(x, intermediate_weight.T) + intermediate_bias
x = gelu(x)
# output shpae: [max_len, hidden_size]
x = np.dot(x, output_weight.T) + output_bias
return x
#归一化层
def layer_norm(self, x, w, b):
x = (x - np.mean(x, axis=1, keepdims=True)) / np.std(x, axis=1, keepdims=True)
x = x * w + b
return x
#链接[cls] token的输出层
def pooler_output_layer(self, x):
x = np.dot(x, self.pooler_dense_weight.T) + self.pooler_dense_bias
x = np.tanh(x)
return x
#最终输出
def forward(self, x):
x = self.embedding_forward(x)
sequence_output = self.all_transformer_layer_forward(x)
pooler_output = self.pooler_output_layer(sequence_output[0])
return sequence_output, pooler_output
#自制
db = DiyBert(state_dict)
diy_sequence_output, diy_pooler_output = db.forward(x)
#torch
torch_sequence_output, torch_pooler_output = bert(torch_x)
print(diy_sequence_output)
print(torch_sequence_output)
# print(diy_pooler_output)
# print(torch_pooler_output)