BERT(一) Transformer原理理解
一年前看了transformer,时间有点久了,现在也忘记很多,今天还是回顾一下,把知识点记下,方便日后回忆~ 😄,自己做算法刚开始也是做了一段NLP方向,后来完全转向了图像算法,随着这段时间transformer领域的大火,不仅仅在NLP领域实现了跨时代的意义,同时最近 发布文章
在图像领域也不断地被transformer刷屏,感觉会有替代CNN的趋势,如此重要的知识点肯定不能丢,这里想了解的可以看我这篇博客VIT(Transformers for Image Recognition at Scale)论文及代码解读。废话不多说先上论文和代码
论文地址:Attention is All You Need
github地址: transformers
在这里我也给出几个方便大家理解transformer的教学视频。
一、 Transfomer概念
Transformer这个模型主要有Google mind团队提出来的一种模型,可以说这边模型出来之后,NLP领域实现了大一统的趋势,这个模型是2017年发表的 attention all you need 中所提到的seq2seq模型。BERT(Bidirectional Encoder Representations from Transformers)就是transformer衍生出来的预训练模型。在过去我们使用word2vec,lstm等技术也逐渐被BERT模型所替代,其成为NLP主流框架,基本很多NLP很多任务都可以用BERT去解决。
我们知道word2vec需要训练得到词embedding,但是每一个词(或字)在不同的上下文是拥有不同的含义的,不同语境下的词含有不同的意思,因此如果基于固定的词的embedding进行语言模型构建是有很大缺陷的,而transformer可以利用上下文的关系来得到我们每一个词的向量。而说到LSTM等时序性的网络不能只能做到串行训练,即下一个词的训练需要等到上一个词训练完成才可以进行,同时也影响训练速度,相对而言BERT的预言模型则可以做到并行计算,这往往是因为其自注意力机制以及位置嵌入机制发挥的功能,在后面我们也会讲到。
Transformer一般分为上游任务和下游任务,上游任务一般训练一个预训练预言模型,然后利于我们训练的预训练模型在进行下游任务如分类,生成等。
二、 Transfomer模型结构讲解
一般理解Trasformer无外乎通过两个部分去理解分为Encoder以及Decoder即编码器以及解码器。这里我们可以这么理解Encoder可以当作我们的特征提取器,来提取语言序列的特征表达得到隐藏层,而解码器则是将这些隐藏层特征表达解决我们自然语言中实际需要的问题如情感分类,命名实体识别等。说到这里先上一下大家都非常熟悉的Transformer的结构图,图像左边是编码器,图像的右边是解码器。
![](https://img.haomeiwen.com/i5356150/46c19192fcd4a521.png)
刚看到这样的图片大家估计会比较懵,这边对于每一个部分我们都会细致讲解。那这种模型结构他是怎么怎么工作的呢?这边我给一张下面的图片方便大家理解transfomer中seq2sq是如何进行工作的。
![](https://img.haomeiwen.com/i5356150/a0c35531532a5091.png)
这张图是一个已经简单的翻译模型,将英文翻译成中文,比如我们输入英文 Why do we work ? 我们需要将其翻译成中文,我们根据序号来看
【1】将英文文本输入到输入到编码器的模型中去
【2】通过编码器得到我们的隐藏层特征
【3】我们输入<start>开始token
【4】解码器结合我们的隐藏层特征输出 为
【5】我们再将我们得到的 为 输出到解码器中
【6】解码器结合我们隐藏层特征会输出什
【7】依次按照这种方式不断输入到解码器中,我们将得出我们解码器翻译出来的中文语句。
当然很多自然语言的运用中很多也只用到编码器的部分,导致很多文章再说到解码器的部分让人觉得很模糊没有看懂,后面我也会详细说明,好了下面分别从encoder以及decoder来说下transformer模型吧。
1. Encoder
对于Transformer的Encoder部分讲解我也准备通过4个部分去给大家做讲解,当然再模型比较难懂的地方我也会结合代码给大家做说明,当然我讲解的代码框架还是基于 pytorch, 毕竟已经很久没有接触tensorflow了,主要是pytorch是动态图,二tensorflow是静态图,因此pytorch框架其代码书写方式以及理解程度往往会比tensorflow更容易理解。
这四个部分对应图上的结构分别是
a. input embedding以及positional encoding
![](https://img.haomeiwen.com/i5356150/775921434a92e8fa.png)
b. multi-head attention
![](https://img.haomeiwen.com/i5356150/f2b0a8116df367bc.png)
c. add & norm
![](https://img.haomeiwen.com/i5356150/67d3f1ae6d227b39.png)
d. feedforward
![](https://img.haomeiwen.com/i5356150/b70a166e8085e382.png)
1.1 Input Embedding and Positional encoding
- input embedding
首先我们定义inputs它的维度大小为[batch_size, sequence_length, embedding dimention]
, batch_size大家很容易理解,sequence_length
指的是一句话的长度如果是中文的话我们可以理解为一句话中所有字的数量,embedding dimention
指的是每个字向量的维度。
刚开始怎么得到我们的input embedding呢?对于英文来说我们可以通过结合切换符号以及结合wordpiece机制进行分词,关于wordpiece的理解可以认为就是将本来有的英文单词再进行再切分得到subword英文考虑到单词的时态,单复数等变化情况,这种方式可以使得模型训练效果更加好。如果对于中文语句的话,则可以通过分词或者按照每个字进行切分,这里建议推荐利用字进行切分,因为相对于字来说常用的汉字也就是4000多个,但是词会有很多组合,所以需要准备大型的词库,对于训练模型不够友好。初始化代码如下
class Dataset(object):
def __init__(self, word2idx):
self.word2idx = word2idx # word2idex={'积':20, '想':21, ...}
self.unk_index = 1 # 这里的unknow表示不在此字向量表中我们用1 表示
def tokenize_char(segments):
sen_embedding = [self.word2idx.get(char, self.unk_index) for char in segments]
我们知道我们模型的输入大小都要保持一致性,所以对于一句话的长度也就是 sequence_length
要保证每一句话都一样长度,对于不同长度大小的sequence我们使用如下的方法:
(1)对于长度短于sequence的语句我们一般通过加入padding项的方法,使其长度补齐到sequence_length。
(2)对于长于sequence的语句我们一般采用截断的方法,使其截取到sequence长度。
- positional encoding(位置编码)
这里我们也是通过代码来了解一下咱们位置是怎么编码的。
def get_positional_encoding(max_seq_len, embed_dim):
# 初始化一个positional encoding
# embed_dim: 字嵌入的维度
# max_seq_len: 最大的序列长度
positional_encoding = np.array([
[pos / np.power(10000, 2 * i / embed_dim) for i in range(embed_dim)]
if pos != 0 else np.zeros(embed_dim) for pos in range(max_seq_len)])
positional_encoding[1:, 0::2] = np.sin(positional_encoding[1:, 0::2]) # dim 2i 偶数
positional_encoding[1:, 1::2] = np.cos(positional_encoding[1:, 1::2]) # dim 2i+1 奇数
# 归一化, 用位置嵌入的每一行除以它的模长
# denominator = np.sqrt(np.sum(position_enc**2, axis=1, keepdims=True))
# position_enc = position_enc / (denominator + 1e-8)
return positional_encoding
上面的指的是句中字的位置,取值范围[0, sequence_length],
指的是词向量的维度,取值的范围是[0, embedding_dimension]。上面有
和
一组公式, 也就是对应着embedding dimension维度的一组奇数和偶数的序号的维度, 例如0,1一组, 2,3一组, 分别用上面的
和
函数做处理, 从而产生不同的周期性变化, 而位置嵌入在embedding dimension维度上随着维度序号增大, 周期变化会越来越慢, 而产生一种包含位置信息的纹理, 就像论文原文中第六页讲的, 位置嵌入函数的周期从
到
变化, 而每一个位置在embedding dimension维度上都会得到不同周期的
和
函数的取值组合, 从而产生独一的纹理位置信息, 模型从而学到位置之间的依赖关系和自然语言的时序特性.
下面这张图可以看出随着embeddding维度的增加,周期变化越慢。
![](https://img.haomeiwen.com/i5356150/f528697878a48d31.png)
通过上述我们可以得出如何得到input embeding 以及 positional encoding。
1.2 Multi-head Attention
关于attention机制算是整个transformer最有价值的部分,从论文的标题**Attention is All You Need。attention机制是让模型知道其关注点在哪里,那些信息是有价值值得关注的。比如下面这句话
![](https://img.haomeiwen.com/i5356150/6d468475626e2643.png)
不同的 it 是指代是不一样的,到底是指代 street 还是指代 animal呢?这里我们使用的attention机制称为 self-attention,顾名思义也就是自己和自己做attention。
![](https://img.haomeiwen.com/i5356150/423e6a8c44dc46be.png)
下面具体聊下self-attention,这里我会用到李宏毅老师的ppt作为辅助理解。
![](https://img.haomeiwen.com/i5356150/cf5ffbc72d4f6798.png)
从上面的seq2seq图中可以看出对于每个来说已经观察到了整个sequence。同时
同时被输出出来。
![](https://img.haomeiwen.com/i5356150/c6314e3886b113d1.png)
首先我们输入sequence
【1】query矩阵(查询其他词的矩阵)
【2】key矩阵(用来被其他词查询的矩阵)
【3】value矩阵(用来表示被提取信息值的矩阵)
然后我们会让我们emdding后的矩阵
得到此矩阵后,我们需要拿每一个query矩阵q对每一个key矩阵k通过点乘做attention(我们知道通过点乘也是求余弦相似度,也就是说两个token越相似其对应的值也就是越大)。得到如下的图
![](https://img.haomeiwen.com/i5356150/5074037e8f24d959.png)
这里可以理解就是第一个
![](https://img.haomeiwen.com/i5356150/19dfe1975f2c87fb.png)
之后我们会用softmax机制对于输出的
![](https://img.haomeiwen.com/i5356150/ceba6f00f680387e.png)
最后我们不要忘了还有v矩阵,将其与上述得到的
☆ 从这里可以看到这里我们产生的
![](https://img.haomeiwen.com/i5356150/b1c41c76cd1206bd.png)
同理如此我们也可以得到.
![](https://img.haomeiwen.com/i5356150/5382067e6d0e5d4f.png)
因此这里的
![](https://img.haomeiwen.com/i5356150/4e3d3904b9f428eb.png)
我们可以将
![](https://img.haomeiwen.com/i5356150/a1c5b8cb01ac6c57.png)
![](https://img.haomeiwen.com/i5356150/729130cefc1343b9.png)
上图表达如何用矩阵计算得到到
即得到输出矩阵
。
说到了这里我们已经知道了什么是 Self-Attention 机制,那什么是 Multi-head Self-attention 呢?下面我们用两个head做举例。
![](https://img.haomeiwen.com/i5356150/25989373bd352228.png)
其实很好理解就是将本来的一个
![](https://img.haomeiwen.com/i5356150/1f06ae3ae33f551d.png)
![](https://img.haomeiwen.com/i5356150/7a46776ba93f68ad.png)
根据上面的图以及前面所说的positional encoding我们也可以得知对于self-attention来说input sequence的顺序来说不重要的,因为对于他来说token和每一个token都做self-attentation。图片也可以方便我们理解为什么positional encoding是相加而不是concate。
OK,在我们已经说完transformer的作用后如何进行seq2seq呢?
![](https://img.haomeiwen.com/i5356150/ee14fdf49b5db4ed.png)
![](https://img.haomeiwen.com/i5356150/edab735bce81c05f.png)
encoding提供的是K, V矩阵,和前面的一样,Decoding的过程在得到Q矩阵后与对应的K, V矩阵计算的出结果。
这张图是一个已经简单的翻译模型,将英文翻译成中文,比如我们输入英文 Why do we work ? 我们需要将其翻译成中文,我们根据序号来看
【1】将英文文本输入到输入到编码器的模型中去
【2】通过编码器得到我们的隐藏层特征
【3】我们输入<start>开始token
【4】解码器结合我们的隐藏层特征输出 为
【5】我们再将我们得到的 为 输出到解码器中
【6】解码器结合我们隐藏层特征会输出什
【7】依次按照这种方式不断输入到解码器中,我们将得出我们解码器翻译出来的中文语句。
下面的一个动画可以很方便的让我们了解其中的过程。
![](https://img.haomeiwen.com/i5356150/2a7db61ecb10c31e.gif)
究竟过程是如何训练的?我们后面说到BERT再去详细介绍。
下面给出self-attention代码,里面也加了很多注释,很容易理解。
class BertSelfAttention(nn.Module):
"""自注意力机制层, 见Transformer(一), 讲编码器(encoder)的第2部分"""
def __init__(self, config):
super(BertSelfAttention, self).__init__()
# 判断embedding dimension是否可以被num_attention_heads整除
if config.hidden_size % config.num_attention_heads != 0:
raise ValueError(
"The hidden size (%d) is not a multiple of the number of attention "
"heads (%d)" % (config.hidden_size, config.num_attention_heads))
self.num_attention_heads = config.num_attention_heads
self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
self.all_head_size = self.num_attention_heads * self.attention_head_size
# Q, K, V线性映射
self.query = nn.Linear(config.hidden_size, self.all_head_size)
self.key = nn.Linear(config.hidden_size, self.all_head_size)
self.value = nn.Linear(config.hidden_size, self.all_head_size)
self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
def transpose_for_scores(self, x):
# 输入x为QKV中的一个, 维度: [batch_size, seq_length, embedding_dim]
# 输出的维度经过reshape和转置: [batch_size, num_heads, seq_length, embedding_dim / num_heads]
new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
x = x.view(*new_x_shape)
return x.permute(0, 2, 1, 3)
def forward(self, hidden_states, attention_mask, get_attention_matrices=False):
# Q, K, V线性映射
# Q, K, V的维度为[batch_size, seq_length, num_heads * embedding_dim]
mixed_query_layer = self.query(hidden_states)
mixed_key_layer = self.key(hidden_states)
mixed_value_layer = self.value(hidden_states)
# 把QKV分割成num_heads份
# 把维度转换为[batch_size, num_heads, seq_length, embedding_dim / num_heads]
query_layer = self.transpose_for_scores(mixed_query_layer)
key_layer = self.transpose_for_scores(mixed_key_layer)
value_layer = self.transpose_for_scores(mixed_value_layer)
# Take the dot product between "query" and "key" to get the raw attention scores.
# Q与K求点积
attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
# attention_scores: [batch_size, num_heads, seq_length, seq_length]
# 除以K的dimension, 开平方根以归一为标准正态分布
attention_scores = attention_scores / math.sqrt(self.attention_head_size)
# Apply the attention mask is (precomputed for all layers in BertModel forward() function)
attention_scores = attention_scores + attention_mask
# attention_mask 注意力矩阵mask: [batch_size, 1, 1, seq_length]
# 元素相加后, 会广播到维度: [batch_size, num_heads, seq_length, seq_length]
# softmax归一化, 得到注意力矩阵
# Normalize the attention scores to probabilities.
attention_probs_ = nn.Softmax(dim=-1)(attention_scores)
# This is actually dropping out entire tokens to attend to, which might
# seem a bit unusual, but is taken from the original Transformer paper.
attention_probs = self.dropout(attention_probs_)
# 用注意力矩阵加权V
context_layer = torch.matmul(attention_probs, value_layer)
# 把加权后的V reshape, 得到[batch_size, length, embedding_dimension]
context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
context_layer = context_layer.view(*new_context_layer_shape)
# 输出attention矩阵用来可视化
if get_attention_matrices:
return context_layer, attention_probs_
return context_layer, None
1.3 Add & Norm
![](https://img.haomeiwen.com/i5356150/fdcc60e9e98ed49c.png)
这里的Add其实内容和残差神经网络的原理一样,因为随着深度学习网络层数的加深,咱们需要是残差连接的方式即经过multi-head的输出与输入进行相加。
关于Norm,文章使用的是layer-normalization。layer-normalization的作用就是要把神经网络中隐藏层归一为标准正态分布,也就是独立同分布,一起到加快训练速度,加速收敛的作用。
矩阵的行(row)为单位求平均:, 矩阵的行为单位求方差
,然后用每一行的每一个元素减去这行的均值,再除以这行的标准差,从而得到归一化的数值,
是为了防止分母为0,我们得到下面的式子。
。
下面的图展示了batch norm与layer norm的区别。
![](https://img.haomeiwen.com/i5356150/d38c1067a8402e37.png)
1.4 feedforward
其实就是linear层就是简单的线性变化,, 两层线性映射并用激活函数激活,一般第一个线性变化都是升维度,第二次在降维加快残差训练速度代码如下:
class BertIntermediate(nn.Module):
# 封装的FeedForward层和激活层
def __init__(self, config):
super(BertIntermediate, self).__init__()
self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
self.intermediate_act_fn = ACT2FN[config.hidden_act]
def forward(self, hidden_states):
hidden_states = self.dense(hidden_states)
hidden_states = self.intermediate_act_fn(hidden_states)
return hidden_states
通过上面的讲解我们发现整个transfomer的运算没有通过矩阵的角度去理解,而是通过矩阵中的每个元素去理解,我们再通过矩阵的角度去理解transformer。
![](https://img.haomeiwen.com/i5356150/a7f977250682563b.png)
![](https://img.haomeiwen.com/i5356150/0aea46b28a5ca710.png)
上面的图通过上述的讲解应该很容易去理解了。
![](https://img.haomeiwen.com/i5356150/8c531c3107fbc6d0.png)
![](https://img.haomeiwen.com/i5356150/41af32f5bd9b8c0e.png)
![](https://img.haomeiwen.com/i5356150/662503e546c95d81.png)
![](https://img.haomeiwen.com/i5356150/a7615fc95e4936b9.png)
![](https://img.haomeiwen.com/i5356150/1e7666376448743e.png)
![](https://img.haomeiwen.com/i5356150/7fe4152b64a261bb.png)
这里还有三个小点需要解释
【1】上述的这个过程是一个block,但是我们可以看到上面有一个乘以N,如下图所示,也就是它是有很多个block组成。
![](https://img.haomeiwen.com/i5356150/09c7777deb15d49d.png)
【2】我们在做self-attention的过程中,我们通常使用mini-batch来进行计算,也就是说一次计算多几句话,这样的话我们的输入
[batch_size, sequence_length, embedding_dimension]
, ![](https://img.haomeiwen.com/i5356150/9753f798ee351e78.png)
经过上式的masking我们使无效区域经过softmax计算之后还几乎为0,这样就避免了无效区域参与计算
【3】不同的头与输入进行运算之后我们需要将我们的不同头得出的结构进行concate
总结
下面我们将我们对transformer的结构做一个总结
【1】子向量与位置编码
![](https://img.haomeiwen.com/i5356150/f924add5ed0fdb8d.png)
【2】自注意力机制
![](https://img.haomeiwen.com/i5356150/3f495755a7a5e4f6.png)
【3】 残差连接与LayerNormalization
![](https://img.haomeiwen.com/i5356150/3c3605c30bcf3b38.png)
【4】feedforward
![](https://img.haomeiwen.com/i5356150/a81a9954d2feafe1.png)
2. Decoder
说完了encoder的部分我们再来看咱们的decoder的部分。相对encoder的部分decoder主要的的区别在于它有一个 Masked Multi-Head Attention的部分![](https://img.haomeiwen.com/i5356150/8b4425dea95aae94.png)
关于这一点我讲通过下一篇有关Seq2Seq去讲解。先给出一篇博客他的说明
现在我们已经了解了编码器侧的大部分概念,也基本了解了解码器的工作方式,下面看下他们是如何共同工作的。
编码器从输入序列的处理开始,最后的编码器的输出被转换为K和V,它俩被每个解码器的"encoder-decoder atttention"层来使用,帮助解码器集中于输入序列的合适位置。
transformer_decoding_1.gif
下面的步骤一直重复直到一个特殊符号出现表示解码器完成了翻译输出。每一步的输出被喂到下一个解码器中。正如编码器的输入所做的处理,对解码器的输入增加位置向量。transformer_decoding_2.gif
在解码器中的self attention 层与编码器中的稍有不同,在解码器中,self-attention 层仅仅允许关注早于当前输出的位置。在softmax之前,通过遮挡未来位置(将它们设置为-inf)来实现。
"Encoder-Decoder Attention "层工作方式跟multi-headed self-attention是一样的,除了一点,它从前层获取输出转成query矩阵,接收最后层编码器的key和value矩阵做key和value矩阵。The Final Linear and Softmax Layer
解码器最后输出浮点向量,如何将它转成词?这是最后的线性层和softmax层的主要工作。
线性层是个简单的全连接层,将解码器的最后输出映射到一个非常大的logits向量上。假设模型已知有1万个单词(输出的词表)从训练集中学习得到。那么,logits向量就有1万维,每个值表示是某个词的可能倾向值。
softmax层将这些分数转换成概率值(都是正值,且加和为1),最高值对应的维上的词就是这一步的输出单词。
![]()
最后给一个csdn博客,也是非常大家去看的。
图解Transformer(完整版)
2. Transformer 优缺点
优点: 这里Path length指的是要计算一个序列长度为n的信息要经过的路径长度。cnn需要增加卷积层数来扩大视野,rnn需要从1到n逐个进行计算,而self-attention只需要一步矩阵计算就可以。所以也可以看出,self-attention可以比rnn更好地解决长时依赖问题。当然如果计算量太大,比如序列长度n>序列维度d这种情况,也可以用窗口限制self-attention的计算数量. Transformer是第一个用纯attention搭建的模型,不仅计算速度更快,在翻译任务上获得了更好的结果,也为后续的BERT模型做了铺垫。
缺点: 欢迎补充
学习完了Transformer, 那如何应用到我们的NLP(自然语言处理)的任务中去了,请看我下一篇的博客BERT(二) BERT解读及应用。