[Autho] 余胜辉
1. Bert 模型简介
BERT是谷歌于2018年提出的预训练语言模型,它使用了Transformer编码器部分。
2. Bert 模型输入处理
以bert-base为例,模型包含12个层,12个注意力头,隐藏层尺寸为768,模型大小约为110MB,输入长度为256。这使得BERT模型在标准Transformer的深度上更深。BERT的输入输出是256个768维的向量,每个向量对应一个输入的token。区别在于,输入BERT之前的每个token转化为768维的向量时,token之间没有相互影响。而从BERT输出之后,token之间会根据上下文语义环境产生相互影响,这是通过多头注意力机制实现的。
from transformers import BertTokenizer, BertModel
import torch
# 加载预训练的BERT分词器和模型
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
# 输入文本
text1 = "Hello, how are you?"
text2 = "I love using BERT!"
# 对输入文本进行编码
inputs = tokenizer(text1, text2, padding=True, truncation=True, return_tensors='pt')
# 打印分词后的结果
print("Input tokens:", tokenizer.convert_ids_to_tokens(inputs['input_ids'][0]))
# 进行前向传播获取输出
with torch.no_grad():
outputs = model(**inputs)
# 获取最后一层的隐藏状态
last_hidden_states = outputs.last_hidden_state
# 打印结果
print("Input text 1:", text1)
print("Input text 2:", text2)
print("Tokenized input IDs:", inputs['input_ids'])
print("Last hidden states shape:", last_hidden_states.shape)
print("First token's vector (CLS):", last_hidden_states[:, 0, :].numpy())
3. 多头注意力机制
BERT模型的输入可以是两句话。BERT首先会在句子前面加上cls token,在两句话中间加入sep token,两句话的末尾也加入sep token。然后,将每个字转化为向量。具体做法是,将字符、句子、位置信息分别转化为随机生成的不可变的768维向量,并将三个向量相加,得到这句话里面每个字的向量。之后就是多头注意力的计算了。
为了讲清楚多头注意力的计算过程,我们先做一些简单的假设:
-
假设输入的句子只有“你好”这两个字,也不加cls token,因为加入cls token和不加cls token,在进行多头计算的时候,是一样的。
-
假设,我们只进行一个注意力头的计算,因为一个头的计算,和多个头的计算也是类似的。
-
假设,我们输入的句子是:“你好”。假设转成向量后,每个字为768维,那么这句话,就变成了一个2行768列的矩阵。
在进入多头注意力机制之前,先对这个矩阵乘以WQ,WK,WV三个参数矩阵,这三个参数矩阵将来都是要参与训练的。乘以这三个矩阵有两个作用:
-
第一个作用是,对原来的每个字的768维的向量,进行降维;
-
第二个作用是,通过乘以WQ,WK,WV这三个矩阵,构建出K、Q、V三个矩阵。
我们真正的目的,是实现上下文语义,按照重要性,对当前这个字,产生不同程度的影响。而kqv计算,就是实现这个目的的具体手段。
Kqv具体的计算过程是,将Q矩阵乘以K矩阵的转置,然后除以根号dk,得到的结果送入softmax函数,最后乘以V矩阵。这个计算过程的具体含义是这样的:
-
Q矩阵乘以K矩阵的转置,会得到一个2行2列的矩阵,代表的是一句话里面,字与字之间两两相关性。例如,如果我们输入的这句话是:“你好”,矩阵里面的元素,就代表的是:“你”和“你”的相关性,“好”和“好”的相关性,以及“你”和“好”的相关性。
-
但是此时的矩阵里的数值,有可能是大于1的,因此要把这个矩阵中所有的值,都压缩到0到1之间,才符合相关性矩阵的概念。
-
为了达到这个目的,最好的办法,就是将矩阵,送入softmax函数。但是为了避免数值过大,进入softmax的饱和区,在送入softmax函数之前,要先对矩阵进行缩小,来避免进入饱和区。具体的缩小办法是,将矩阵除以根号下dk,dk是每个字转向量之后的维度。
-
缩小之后的结果,再送入softmax函数,将所有数据压缩到0到1之间。此时矩阵就变成了一个真正的相关性矩阵了。矩阵也叫做注意力权重矩阵。
-
接下来,按照公式,应该是做矩阵乘以V矩阵的运算了。V矩阵是原来句子降维后的结果。第一行代表的是,句子中的第一个字的向量,也就是“你”,这个字的向量;第二行代表的是,句子中的第二个字的向量,也就是“好”,这个字的向量。矩阵代表的是这句话中,字与字的相关性。则矩阵乘以V矩阵,具体含义就是,其它字的对应维度的信息,按照矩阵中的相关性,来影响当前这个字的对应维度的数值。并将影响的具体数值,按照矩阵里面的权重,加到当前这个字对应的维度上。到这里就实现了,通过上下文语义,对当前这个字影响。也就是实现了,同一个字,在不同的上下文语义中,具有不同的向量表示。再通俗点说就是,同一个字,在不同的上下文语义中,具有不同的含义。
# 解释多头注意力机制
def explain_attention_mechanism(model, inputs):
# 获取模型的第一层
layer = model.encoder.layer[0]
# 获取Q, K, V矩阵
qkv = layer.attention.self.query_key_value(inputs['input_ids'])
q, k, v = qkv.chunk(3, dim=-1)
# 分离成多个头
num_heads = layer.attention.self.num_attention_heads
head_dim = layer.attention.self.attention_head_size
q = q.reshape(q.size(0), q.size(1), num_heads, head_dim).transpose(1, 2)
k = k.reshape(k.size(0), k.size(1), num_heads, head_dim).transpose(1, 2)
v = v.reshape(v.size(0), v.size(1), num_heads, head_dim).transpose(1, 2)
# 计算注意力分数
attention_scores = torch.matmul(q, k.transpose(-1, -2)) / torch.sqrt(torch.tensor(head_dim))
attention_probs = torch.nn.functional.softmax(attention_scores, dim=-1)
# 计算加权值
context_layer = torch.matmul(attention_probs, v)
context_layer = context_layer.transpose(1, 2).reshape(context_layer.size(0), context_layer.size(1), num_heads * head_dim)
return attention_scores, attention_probs, context_layer
# 获取注意力机制的结果
attention_scores, attention_probs, context_layer = explain_attention_mechanism(model, inputs)
# 打印注意力分数和概率
print("\nAttention Scores for the first head:\n", attention_scores[0][0].detach().numpy())
print("\nAttention Probabilities for the first head:\n", attention_probs[0][0].detach().numpy())
# 打印上下文层
print("\nContext Layer for the first head:\n", context_layer.detach().numpy())
4. 残差网络和归一化
上述KQA的计算流程,执行一次,就是一个注意力的“头”;并行执行多次,就实现了“多头注意力”,bert-base版本中,头数是12;多头注意力之后面,再加一个前馈神经网络,将多头注意力的输出结果,再调整成每个字是768为的向量。这样,就构成了一个完整的多图注意力层。多个包含多头注意力的层,串起来堆叠多次,就是多层的,多头注意力模型。也就构成了BERT的主体框架了。BERT-base版本有12层。 每个多头注意层之间在加入残差链接和归一化,构成错层堆叠的残差网络。残差网络,是将每层的输入层通过旁路,绕过当前层,直接接到当前层的输出上,与当前层的输出进行求和与归一化。加入残差网络的目的是避免当前层输出,不靠谱,用残差链接,可以将原始输入,直接链接到,该层的输出上,进行纠偏。同时也能防止梯度消失。
通过上述描述可知,BERT是一个12层,每层包含12个多头注意力,的编码器。里面包含很多参数。这些参数需要进行合理的训练,才能让BERT具备语义理解能力。我们将文本中的,85%不变,15%进行mask。再将这15%的mask掉的token,分成三种情况:其中10%不变,10%随机替换,80%被遮掩。最后 ,让BERT来预测,mask掉的字是什么。从而提高模型学习能力和上下文语义理解能力。
5. Bert 与 Word2Vec
最终训练好的BERT,实际上是一个编码器模型,它类似于一个高级的word2vector。BERT的输入是文字转成的向量,但是此时每个字的向量,是不包含上下文语义的。BERT接收到这些向量后,借助多头注意力机制进行编码,输出的还是每个自对应的向量,输入的矩阵形状和输出的矩阵形状是一样的。区别在于,输出的每个字的向量,已经包含上下文语义了。这就是BERT的作用。而这种效果,word2vector是做不到的。
另外,BERT在每个输入前面都加了cls token,所以cls token是一个特殊的token。在BERT与训练过程中,cls token后面可以接任何文本语料,并且cls token总是在输入的第一个位置。位置是固定的,这就给我们借助cls token做下游任务带来了方便。例如:可以给cls token后面接入一些隐藏层来做:文本分类,情感分析任务。同时,BERT还可以起到词语转向量的作用,将输入的中文文字,借助BERT转成对应向量。
bert论文领读 https://www.youtube.com/watch?v=ULD3uIb2MHQ
标签:BERT,attention,矩阵,token,详解,口语化,注意力,向量 From: https://blog.csdn.net/TrueYSH/article/details/144101521