Transformer原理
1. motivation
为了解决seq2seq的问题,之前一般都是使用RNN模型进行求解。RNN的一大劣势就是无法进行并行化计算,比如要想输出 b 4 b^4 b4就必须要先获得 a 1 a^1 a1到 a 4 {a^4} a4才行。而接下来就有学者想采把CNN用来取代RNN,每个小三角形都是一个filter,但是问题是如下图所示每个小三角仅能考虑到很少的一部分输入,但是我们可以通过叠多层的CNN,则上层的filter就可以考虑到比较多的语句,如下所示蓝色的filter可以看到 b 1 b^1 b1到 b 3 b^3 b3,而 b 1 b^1 b1到 b 3 b^3 b3是由 a 1 a^1 a1到 a 4 {a^4} a4决定的,相当于蓝色的filter已经看到了整个句子中所有的内容。但是这里有个问题是你需要叠很多层CNN才能够看到整个句子,那如果你想第二层就看到整个句子就会比较困难,所以有一个新的想法就是self-attention。
self-attention就是做了这样一件事情,输入和输出和RNN一样都是seq,然后 b 1 b^1 b1到 b 4 b^4 b4可以看到整个输入seq,但是 b 1 b^1 b1到 b 4 {b^4} b4却又可以被并行计算。
2. self-attention原理
2.1 self-attention计算过程
如下所示,首先input是 x 1 x^1 x1到 x 4 x^4 x4(一个sequence),每个input先通过一个embedding(乘以一个W矩阵)变成 a 1 {a^1} a1到 a 4 {a^4} a4,然后把他们丢进self-attention layer。self-attention里面做的是,先把每个input都乘上三个不同的matrix产生三个不同的vector,这三个不同的vector我们分别命名为q、k、v。q代表query(to match others),把每一个input a都乘上某一个matrix W q {W^q} Wq,然后就得到了 q 1 {q^1} q1到 q 4 {q^4} q4,这些东西叫做query。用同样的方法把input乘上 W k {W^k} Wk就得到 k 1 {k^1} k1到 k 4 {k^4} k4,这个k叫做key,用来被querymatch的。v是指被抽取出来的信息,获取方式也是同理。
接下来要做的是拿每一个q对每一个k做attention,我们先把 q 1 {q^1} q1拿出来对 k 1 {k^1} k1做attention得到 a 1 , 1 a_{1,1} a1,1,那接下来拿 q 1 {q^1} q1对 k 2 {k^2} k2做attention得到 a 1 , 2 a_{1,2} a1,2,拿 q 1 {q^1} q1对 k 3 {k^3} k3做attention得到 a 1 , 3 a_{1,3} a1,3,拿 q 1 {q^1} q1对 k 4 {k^4} k4做attention得到 a 1 , 4 {a_{1,4}} a1,4。那这个a是如何计算的呢?其实就是如下所示的Scaled Dot-Product Attention,为什么要除以根号d呢?因为q和k的dot product数值会随着d(q和k的维度)的增大而增大,很直观就是q和k的维度越大,相乘之后再相加的数就越多。为什么是除以根号d而不是别的呢?原文中有个注脚有写,但是具体的目前还是不清楚,有人说根号d是q和k的标准差,但是无从考证。
接下来就是做一个softmax,得到 a 1 , 1 a_{1,1} a1,1到 a 1 , 4 {a_{1,4}} a1,4 head.
有了 a 1 , 1 ^ \widehat {a_{1,1}} a1,1 到 a 1 , 4 ^ \widehat {a_{1,4}} a1,4 之后就那每一个a去和v相乘再做weight sum得到 b 1 b^1 b1,你会发现产生 b 1 b^1 b1的时候用到了整个sequence,因为 b 1 b^1 b1用到了 v 1 {v^1} v1到 v 4 {v^4} v4,而 v 1 {v^1} v1到 v 4 {v^4} v4使用了全部的 a 1 {a^1} a1到 a 4 {a^4} a4。
b 2 b^2 b2的计算过程也和前面的类似,这里就不展开讲了。
可以看到, b 1 b^1 b1到 b 4 b^4 b4是可以被并行计算的。
2.2 self-attention矩阵计算过程
下面可以把前面介绍的那些过程用矩阵运算来进行计算。把前面的经过embedding转化的 a 1 a^1 a1到 a 4 a^4 a4乘以可学习矩阵 W q W^q Wq就得到了 q 1 q^1 q1到 q 4 q^4 q4,同理 k 1 k^1 k1到 k 4 k^4 k4以及 v 1 v^1 v1到 v 4 v^4 v4也是如此计算。
这里为了方便计算忽略分母的根号d, k 1 k^1 k1做一个转置然后乘以 q 1 q^1 q1就得到了 a 1 , 1 a_{1,1} a1,1,那我们把 k 1 k^1 k1到 k 4 k^4 k4叠在一起变成一个matrix,直接把这个matrix乘以 q 1 q^1 q1得到的结果是一个向量,这个向量就是 a 1 , 1 a_{1,1} a1,1到 a 1 , 4 a_{1,4} a1,4, a 1 , 1 a_{1,1} a1,1到 a 1 , 4 a_{1,4} a1,4的计算也是可以并行的。
同理 q 2 q^2 q2拿出来和 k 1 k^1 k1到 k 4 k^4 k4相乘就得到了 a 2 , 1 a_{2,1} a2,1到 a 2 , 4 a_{2,4} a2,4,剩下的 q 3 q^3 q3和 q 4 q^4 q4也是同理。这个整个计算attention的过程其实就相当于把K矩阵做一个转置,直接乘上Q,即得到了attention A,每一个input两两之间都有attention,如果这边有四个input那得到的attention就是4*4,如果输入sequence长度是n则得到的attention矩阵就是 n 2 {n^2} n2。接下来把A做一个softmax就可以。
接下来就是把 v 1 v_1 v1到 v 4 v_4 v4根据 a ^ \hat a a^做weight sum。我们就是把 v 1 v_1 v1和 a 1 , 1 ^ \widehat {a_{1,1}} a1,1 到 a 1 , 4 ^ \widehat {a_{1,4}} a1,4 对应相乘再相加得到 b 1 b_1 b1,同理得到 b 2 b_2 b2到 b 4 b_4 b4,最后把 b 1 b_1 b1到 b 4 b_4 b4串起来就得到了最终的结果O
最后让我们来看一下整体的矩阵运算,从输入到输出就是一堆矩阵乘法,而矩阵乘法是很轻易地可以用GPU加速的。
2.3 multi-head机制
这里就用2head的情况进行举例,每一个 a i a^i ai都会得到 q i {q^i} qi、 k i {k^i} ki和 v i {v^i} vi,在multi-head机制下,你会继续把 q i {q^i} qi乘上两个不同的matrix( W q , 1 {W^{q,1}} Wq,1和 W q , 2 {W^{q,2}} Wq,2)得到 q i , 1 {q^{i,1}} qi,1和 q i , 2 {q^{i,2}} qi,2,同理可以产生 k i , 1 {k^{i,1}} ki,1和 k i , 2 {k^{i,2}} ki,2,以及 v i , 1 {v^{i,1}} vi,1和 v i , 2 {v^{i,2}} vi,2。但是这里 q i , 1 {q^{i,1}} qi,1只会和 k i , 1 {k^{i,1}} ki,1和 k j , 1 {k^{j,1}} kj,1做attention最后计算出 b i , 1 {b^{i,1}} bi,1。
和上面原理类似,你也可以计算出 b i , 2 {b^{i,2}} bi,2,然后我们会把他们concat起来。
那如果concat之后的维度过大,你可以乘以一个矩阵 W o {W^o} Wo使其降维,最终就得到了 b i b^i bi。那采用multi-head有什么好处吗?其实不同的head他们关注的点不一样,举例来说有的head可能想要看的就是local的信息,有的head想要看的是比较长时间的信息,就是从不同的角度看问题。
2.4 Positional Encoding
前面self-attention过程中你会发现,input的顺序是不重要的。因为他做的事情就是对每一个input的内容都做self-attention,每一个时间点来说跟它是邻居还是在远处对它来说都是一样的,在self-attention心里并没有所谓位置的概念。a点乘b还是b点乘a对它来说好像是一样的,显然我们不希望这样,我们希望能够把input sequence的顺序考虑进来到self-attention里面去。
最初的transformer论文中的做法是人为设计的一个表示位置的矩阵,每一个位置i都有一个向量 e i {e^i} ei,这个向量就可以表示该词的位置信息。然后这个位置向量 e i {e^i} ei(维度和 a i {a^i} ai相同)直接加到原始的 a i {a^i} ai上就融合了位置信息了,那为什么是相加而不是concat呢?这里李宏毅老师给出了一种自己的解释,我们把 x i {x^i} xi再填上一个维度的one-hot向量叫 p i {p^i} pi,就是说第i维是1,其他都是0。看到这个one-hot向量你就知道这个词落在哪个位置,然后把 x i {x^i} xi和 p i {p^i} pi做concat,然后把它乘上一个W生成embedding然后做transformer,那我们可以把这个W拆成两个矩阵 W I {W^I} WI和 W P {W^P} WP,这样相当于于把 x i {x^i} xi和 p i {p^i} pi串起来乘以W转换为把 W I {W^I} WI乘以 x i {x^i} xi再加上 W P {W^P} WP乘以 p i {p^i} pi。而这两个部分恰好分别是 a i {a^i} ai和 e i {e^i} ei,因此直接把 a i {a^i} ai和 e i {e^i} ei相加并没有什么奇怪。
这里又有一个新的疑问就是 W P {W^P} WP如何确定,论文中给出了就是长成如下的样子(可用正弦和余弦表示),后面的bert直接就设为一个科学习的参数矩阵了。
3. Seq2seq with Attention
transformer整体结构如下:
具体的细节如下图所示,这里讲一下batch norm和layer norm的区别,假设我们有一个batch的数据,batchsize=4,BN是对同一个batch里面不同的data的同样的dimension做norm,我们希望同一个batch里面同一个dimension的均值等于0,方差等于1。LN不需要考虑batch的,LN就是给你data我们希望各个不同dimension的均值是0,方差是1。
4. 源码解读
github:https://github.com/Kyubyong/transformer
4.1 数据生成
数据生成的代码在根目录下的train.py中,使用的是get_batch这个函数,分别生成train_batches和eval_batches,后面生成一个迭代器iter,迭代的获取训练数据和标签。
train_batches, num_train_batches, num_train_samples = get_batch(hp.train1, hp.train2,
hp.maxlen1, hp.maxlen2,
hp.vocab, hp.batch_size,
shuffle=True)
eval_batches, num_eval_batches, num_eval_samples = get_batch(hp.eval1, hp.eval2,
100000, 100000,
hp.vocab, hp.batch_size,
shuffle=False)
# create a iterator of the correct shape and type
iter = tf.data.Iterator.from_structure(train_batches.output_types, train_batches.output_shapes)
xs, ys = iter.get_next()
train_init_op = iter.make_initializer(train_batches)
eval_init_op = iter.make_initializer(eval_batches)
logging.info("# Load model")
m = Transformer(hp) # 主模型
loss, train_op, global_step, train_summaries = m.train(xs, ys) # train这个模型
y_hat, eval_summaries = m.eval(xs, ys)
4.2 transformer类
transformer类是模型中最重要的类,下面将介绍encoder、decoder、train、eval部分的代码。
4.2.1 encoder
encoder的输入是训练数据xs,先经过查找表生成embedding,然后加上位置编码信息后经过dropout层。中间就是多个multi-head attention模块,最后再经过一个feed forward。
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
具体的positional_encoding代码如下,分奇偶使用sin和cos进行位置编码:
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)
multihead_attention的具体代码如下,先当于把Q、K、V先做线性投影,然后做split再concat,最后Q_ 、K_ 、V_的第一个维度(batchsize)增加了N倍,第三个维度(隐藏层维度)减小N倍。
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
4.2.2 decoder
下面是decoder的代码。
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.2.3 train
训练代码,包含encoder、decoder和参数优化更新的代码。
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["<pad>"])) # 0: <pad>
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
4.2.4 eval
评估代码。
def eval(self, xs, ys):
'''Predicts autoregressively
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["<s>"]
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["<pad>"]: 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