[使用Excel通俗易懂理解Transformer!link]
[一步步解析Attention is All You Need!link]
[参考transformer论文中文翻译博客link]
[transformer参考中文博客link]
[参考博客link]
[参考reformer博客link]
主要参考17年的Transformer论文、20年的Reformer文章:
发表年月 | 论文链接 | 代码链接 |
---|---|---|
1706.03762 | Transformer_Self-Attention | http://nlp.seas.harvard.edu/2018/04/03/attention.html (非官方)、 https://github.com/Kyubyong/transformer/ (非官方) |
1803.02155 | Self-Attention_RPR | - |
2020.01 | Reformer: The Efficient Transformer | - |
-1- Transformer
-1.0- Transformer背景知识
场景 | 论文 | Q、K、V关系 | 注释 |
---|---|---|---|
问答QA | Key-Value Memory Network | 表示Question,是一句话; 表示一个关键词; 是与该关键词相对应的答案 |
|
机器翻译 | NMT_BahdanauAttention | 表示Output Sentence中的,是目标语言的一个词; 表示Input Sentence中的,是源句语言的一个词 |
|
Transformer_Self-Attention | 、()表示一个Sentence中的各个 |
-1.0.1- Question:
-1.0.2- question_embedding (query) :
-1.0.3- Key-Value pairs:
-1.0.4- key_embedding :
-1.0.5- value_embedding :
-1.0.6- 和 的匹配度:
-1.0.7- question与召回各key匹配度在其对应上的加权求和得output:
如果对以上概念不明白的的话,可以先看Key-Value Memory Network这篇论文。
-1.1- Transformer模型结构
Transformer整体架构上分为Encoder
和Decoder
结构,而Encoder和Decoder又分别由Encoder_cell和Decoder_cell堆叠而成
Encoder
:
Transformer的Encoder由6
个完全相同的Encoder_cell
堆叠而成。每个Encoder_cell有2
个子层:第一个子层是多头自注意力层,第二层是一个简单的全联接的前馈神经网络。
Encoder_cell中的两个子层之间通过残差网络结构进行连接。
Decoder
:
Transformer的Decoder是由6
各完全相同的Decoder_cell
堆叠而成。每个Decoder_cell有3
个子层:第一层是Masked多头自注意力层,第二层也是多头自注意力层(但是该层会对Encoder的输出实现Attention),第三层是简单的全联接的前馈神经网络。
Decoder_cell中的三个子层之间通过残差网络结构进行连接。
现在我们看一下encoder的计算过程:
*
首先,在将数据传输到encoder之前,模型需要对输入的数据进行一个embedding操作,(也可以理解为类似w2c的操作)
*
embedding结束之后,输入到第一个encoder层
,self-attention处理完数据后把数据送给前馈神经网络,前馈神经网络的计算可以并行,
*
当前encoder 得到的输出会输入到下一个encoder。
-1.2- Transformer模型中的Attention层
Transformer的Attention机制本身是一个函数,对于给定的和召回的一系列Key-Value pairs ,, ... , ,通过函数计算一个output向量。
输出结果其实就是对,, ... , 进行加权求和,其中每个的权重是和相关度计算得到的。
论文中给出了伸缩点乘Attention机制和多头Attention机制的layer细节组件,下图中左边是伸缩点乘Attention Layer细节,右边是多头Attention Layer细节:
注意⚠️不同于Key-Value Memory Network中的、、都是相同的维度:d*1维。Transformer中的、是相同的维度:维;是维。
注意⚠️不同于Key-Value Memory Network中计算时,是选取1
个Question的query向量
,并针对该Question从大小为M的Key-Value池中召回N个pair。Transformer中计算时,是将所有Question的query进行打包成一个大的矩阵
,也并不再对Key-Value池进行有选择性的召回,而是将池中所有的, 向量打包成两个大的矩阵、。
注意⚠️综上可得:
是维的矩阵
是维的矩阵
是维的矩阵
Scaled Dot-Product Attention - 伸缩点乘注意力机制
其中:
是维的矩阵,是Question_Embedding
是维的矩阵,是Key_Embedding
是维的矩阵,是Value_Embedding
是维的向量,记作,是与的各元素的相关程度or匹配程度
最终计算得到的:
是维的矩阵,是按、匹配度加权的
Scaled Dot-Product Attention - 多头注意力机制
其中:
是维的参数矩阵
其中:
是维的矩阵,是Question_Embedding
是维的矩阵,是Key_Embedding
是维的矩阵,是Value_Embedding
是维的参数矩阵
是维的参数矩阵
是维的参数矩阵
*
、、、在此处统称为映射器(projections)
,
*
功能类似于CNN中的卷积kernel,对feature做抽象压缩。(个人理解,存疑)
是的矩阵
是的矩阵
是的矩阵
*
计算得到的是维的矩阵,是按、匹配度加权的
*
是维的矩阵
*
最终计算得到的是维的矩阵
在论文中,使用8
个并行的Attention_Head(即),对于每个Attention_Head,我们设置:
正是由于在每一次函数运算都通过映射器(projections)
进行了维度上的压缩,所以整体计算成本和单一的注意力机制相差不大。
-1.3- 举例说明Transformer模型中的Attention层
现在我们来举例说明,Transformer是如何通过self-attention的方式,来理解句子中各个单词的相关性的:
比如说"The animal didn't cross the street because it was too tired"
这句话,这里的it到底代表的是animal
还是street
呢?这对于我们人类来说来说能很简单就可以判断出来,可对于机器而言却是很难的。但是self-attention机制就能够让机器把it和animal联系起来。
* step1
首先,self-attention会对Input Sentence中的每个word_x的计算出三个不同的向量:、、。在论文中,这三个向量的维度都设置成维。这三个向量是用embedding向量与三个不同的矩阵相乘得到的结果,这个矩阵是随机初始化的,比如说维度为(64,512),即、。这三个待训练的参数矩阵会在BP(反向传播)的过程中一直迭代更新。
得到的这三个向量的维度是64 ???低于embedding维度的。???
* step2
计算self-attention的attention权重,该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点乘,以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2
* step3
接下来,把点成的结果除以一个常数,这里我们除以,这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算。得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会会很大
-1.4- Transformer模型中的PE(Positional Encoding 位置编码)
[参考link]
[参考link]
transformer模型的attention机制并没有包含位置信息,即一句话中词语在不同的位置时在transformer中是没有区别的,这当然是不符合实际的。
因此,在transformer中引入位置信息,相比CNN, RNN等模型,有更加重要的作用。
论文中,作者添加位置编码的方法是:
*
构造一个跟输入embedding维度一样的矩阵
*
然后跟输入embedding相加得到multi-head attention 的输入
*
其中:
pos表示单词的位置
i表示embedding的维度
*
为什么使用三角函数呢?
是三角函数的两条性质可以既考虑到绝对位置又可以考虑到相对位置。通过上面的公式就可以用位置k的线性表达来表示位置k+x。
*
附上pytorch的代码来方便理解:
class PositionalEncoding(nn.Module):
"Implement the PE function."
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
return self.dropout(x)
-1.5- Transformer模型中待训练的参数矩阵
-1.6- Transformer模型的前向传播算法
-2- Transformer代码解读
[参考link]
*0
m = Transformer(hp)
--train.py第46行:
m = Transformer(hp)
loss, train_op, global_step, train_summaries = m.train(xs, ys)
y_hat, eval_summaries = m.eval(xs, ys)
这里初始化了一个Transformer对象,该Transformer Model对象对样本进行了train。
*1
train(xs, ys)
--model.py第137行:
def train(self, xs, ys):
"""
Returns
loss: scalar.
train_op: training operation
global_step: scalar.
summaries: training summary node
"""
# forward
memory, sents1, src_masks = self.encode(xs)
logits, preds, y, sents2 = self.decode(ys, memory, src_masks)
# train scheme
y_ = label_smoothing(tf.one_hot(y, depth=self.hp.vocab_size))
ce = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y_)
nonpadding = tf.to_float(tf.not_equal(y, self.token2idx[""])) # 0:
loss = tf.reduce_sum(ce * nonpadding) / (tf.reduce_sum(nonpadding) + 1e-7)
global_step = tf.train.get_or_create_global_step()
lr = noam_scheme(self.hp.lr, global_step, self.hp.warmup_steps)
optimizer = tf.train.AdamOptimizer(lr)
train_op = optimizer.minimize(loss, global_step=global_step)
tf.summary.scalar('lr', lr)
tf.summary.scalar("loss", loss)
tf.summary.scalar("global_step", global_step)
summaries = tf.summary.merge_all()
return loss, train_op, global_step, summaries
*2
encode(xs, training=True)
--model.py第43行
def encode(self, xs, training=True):
"""
Returns
memory: encoder outputs. (N, T1, d_model)
"""
with tf.variable_scope("encoder", reuse=tf.AUTO_REUSE):
x, seqlens, sents1 = xs
# src_masks
src_masks = tf.math.equal(x, 0) # (N, T1)
# embedding
enc = tf.nn.embedding_lookup(self.embeddings, x) # (N, T1, d_model)
enc *= self.hp.d_model**0.5 # scale
enc += positional_encoding(enc, self.hp.maxlen1)
enc = tf.layers.dropout(enc, self.hp.dropout_rate, training=training)
## Blocks
for i in range(self.hp.num_blocks):
with tf.variable_scope("num_blocks_{}".format(i), reuse=tf.AUTO_REUSE):
# self-attention
enc = multihead_attention(queries=enc,
keys=enc,
values=enc,
key_masks=src_masks,
num_heads=self.hp.num_heads,
dropout_rate=self.hp.dropout_rate,
training=training,
causality=False)
# feed forward
enc = ff(enc, num_units=[self.hp.d_ff, self.hp.d_model])
memory = enc
return memory, sents1, src_masks
*3
decode(ys, memory, src_masks, training=True)
--model.py第79行:
def decode(self, ys, memory, src_masks, training=True):
"""
memory: encoder outputs. (N, T1, d_model)
src_masks: (N, T1)
Returns
logits: (N, T2, V). float32.
y_hat: (N, T2). int32
y: (N, T2). int32
sents2: (N,). string.
"""
with tf.variable_scope("decoder", reuse=tf.AUTO_REUSE):
decoder_inputs, y, seqlens, sents2 = ys
# tgt_masks
tgt_masks = tf.math.equal(decoder_inputs, 0) # (N, T2)
# embedding
dec = tf.nn.embedding_lookup(self.embeddings, decoder_inputs) # (N, T2, d_model)
dec *= self.hp.d_model ** 0.5 # scale
dec += positional_encoding(dec, self.hp.maxlen2)
dec = tf.layers.dropout(dec, self.hp.dropout_rate, training=training)
# Blocks
for i in range(self.hp.num_blocks):
with tf.variable_scope("num_blocks_{}".format(i), reuse=tf.AUTO_REUSE):
# Masked self-attention (Note that causality is True at this time)
dec = multihead_attention(queries=dec,
keys=dec,
values=dec,
key_masks=tgt_masks,
num_heads=self.hp.num_heads,
dropout_rate=self.hp.dropout_rate,
training=training,
causality=True,
scope="self_attention")
# Vanilla attention
dec = multihead_attention(queries=dec,
keys=memory,
values=memory,
key_masks=src_masks,
num_heads=self.hp.num_heads,
dropout_rate=self.hp.dropout_rate,
training=training,
causality=False,
scope="vanilla_attention")
### Feed Forward
dec = ff(dec, num_units=[self.hp.d_ff, self.hp.d_model])
# Final linear projection (embedding weights are shared)
weights = tf.transpose(self.embeddings) # (d_model, vocab_size)
logits = tf.einsum('ntd,dk->ntk', dec, weights) # (N, T2, vocab_size)
y_hat = tf.to_int32(tf.argmax(logits, axis=-1))
return logits, y_hat, y, sents2
*4
positional_encoding()
--modules.py第360行:
def positional_encoding(inputs,
maxlen,
masking=True,
scope="positional_encoding"):
"""
Sinusoidal Positional_Encoding. See 3.5
inputs: 3d tensor. (N, T, E)
maxlen: scalar. Must be >= T
masking: Boolean. If True, padding positions are set to zeros.
scope: Optional scope for `variable_scope`.
returns
3d tensor that has the same shape as inputs.
"""
E = inputs.get_shape().as_list()[-1] # static
N, T = tf.shape(inputs)[0], tf.shape(inputs)[1] # dynamic
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
# position indices
position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1]) # (N, T)
# First part of the PE function: sin and cos argument
position_enc = np.array([
[pos / np.power(10000, (i-i%2)/E) for i in range(E)]
for pos in range(maxlen)])
# Second part, apply the cosine to even columns and sin to odds.
position_enc[:, 0::2] = np.sin(position_enc[:, 0::2]) # dim 2i
position_enc[:, 1::2] = np.cos(position_enc[:, 1::2]) # dim 2i+1
position_enc = tf.convert_to_tensor(position_enc, tf.float32) # (maxlen, E)
# lookup
outputs = tf.nn.embedding_lookup(position_enc, position_ind)
# masks
if masking:
outputs = tf.where(tf.equal(inputs, 0), inputs, outputs)
return tf.to_float(outputs)
*5
multihead_attention()
--modules.py第154行
def multihead_attention(queries, keys, values, key_masks,
num_heads=8,
dropout_rate=0,
training=True,
causality=False,
scope="multihead_attention"):
"""
Applies multihead attention. See 3.2.2
queries: A 3d tensor with shape of [N, T_q, d_model].
keys: A 3d tensor with shape of [N, T_k, d_model].
values: A 3d tensor with shape of [N, T_k, d_model].
key_masks: A 2d tensor with shape of [N, key_seqlen]
num_heads: An int. Number of heads.
dropout_rate: A floating point number.
training: Boolean. Controller of mechanism for dropout.
causality: Boolean. If true, units that reference the future are masked.
scope: Optional scope for `variable_scope`.
Returns
A 3d tensor with shape of (N, T_q, C)
"""
d_model = queries.get_shape().as_list()[-1]
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
# Linear projections
Q = tf.layers.dense(queries, d_model, use_bias=True) # (N, T_q, d_model)
K = tf.layers.dense(keys, d_model, use_bias=True) # (N, T_k, d_model)
V = tf.layers.dense(values, d_model, use_bias=True) # (N, T_k, d_model)
# Split and concat
Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # (h*N, T_q, d_model/h)
K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # (h*N, T_k, d_model/h)
V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # (h*N, T_k, d_model/h)
# Attention
outputs = scaled_dot_product_attention(Q_, K_, V_, key_masks, causality, dropout_rate, training)
# Restore shape
outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2 ) # (N, T_q, d_model)
# Residual connection
outputs += queries
# Normalize
outputs = ln(outputs)
return outputs
*6
ff()
--modules.py第202行
def ff(inputs, num_units, scope="positionwise_feedforward"):
"""
position-wise feed forward net. See 3.3
inputs: A 3d tensor with shape of [N, T, C].
num_units: A list of two integers.
scope: Optional scope for `variable_scope`.
Returns:
A 3d tensor with the same shape and dtype as inputs
"""
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
# Inner layer
outputs = tf.layers.dense(inputs, num_units[0], activation=tf.nn.relu)
# Outer layer
outputs = tf.layers.dense(outputs, num_units[1])
# Residual connection
outputs += inputs
# Normalize
outputs = ln(outputs)
return outputs
*7
eval(xs, ys)
-- model.py第168行
def eval(self, xs, ys):
"""
Predicts auto regressively
At inference, input ys is ignored.
Returns
y_hat: (N, T2)
"""
decoder_inputs, y, y_seqlen, sents2 = ys
decoder_inputs = tf.ones((tf.shape(xs[0])[0], 1), tf.int32) * self.token2idx[""]
ys = (decoder_inputs, y, y_seqlen, sents2)
memory, sents1, src_masks = self.encode(xs, False)
logging.info("Inference graph is being built. Please be patient.")
for _ in tqdm(range(self.hp.maxlen2)):
logits, y_hat, y, sents2 = self.decode(ys, memory, src_masks, False)
if tf.reduce_sum(y_hat, 1) == self.token2idx[""]: break
_decoder_inputs = tf.concat((decoder_inputs, y_hat), 1)
ys = (_decoder_inputs, y, y_seqlen, sents2)
# monitor a random sample
n = tf.random_uniform((), 0, tf.shape(y_hat)[0]-1, tf.int32)
sent1 = sents1[n]
pred = convert_idx_to_token_tensor(y_hat[n], self.idx2token)
sent2 = sents2[n]
tf.summary.text("sent1", sent1)
tf.summary.text("pred", pred)
tf.summary.text("sent2", sent2)
summaries = tf.summary.merge_all()
return y_hat, summaries
-3- Self-Attention_RPR
[参考link]
论文介绍了一种在一个Transformer内部编码输入序列的位置信息的方法。特别的是,论文改进了Tranformer的自注意力机制,让其能够更有效地将序列中的词之间的相对距离考虑进来。