YAN's BLOG

transformer

2019-05-26

1
2
3
4
5
6
7
8
9
10
11
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Transformer model for language understanding








View on TensorFlow.org




Run in Google Colab




View source on GitHub

This tutorial trains a Transformer model to translate Portuguese to English. This is an advanced example that assumes knowledge of text generation and attention.

The core idea behind the Transformer model is self-attention—the ability to attend to different positions of the input sequence to compute a representation of that sequence. Transformer creates stacks of self-attention layers and is explained below in the sections Scaled dot product attention and Multi-head attention.

A transformer model handles variable-sized input using stacks of self-attention layers instead of RNNs or CNNs. This general architecture has a number of advantages:

  • It make no assumptions about the temporal/spatial relationships across the data. This is ideal for processing a set of objects (for example, StarCraft units).
  • Layer outputs can be calculated in parallel, instead of a series like an RNN.
  • Distant items can affect each other’s output without passing through many RNN-steps, or convolution layers (see Scene Memory Transformer for example).
  • It can learn long-range dependencies. This is a challenge in many sequence tasks.

The downsides of this architecture are:

  • For a time-series, the output for a time-step is calculated from the entire history instead of only the inputs and current hidden-state. This may be less efficient.
  • If the input does have a temporal/spatial relationship, like text, some positional encoding must be added or the model will effectively see a bag of words.

After training the model in this notebook, you will be able to input a Portuguese sentence and return the English translation.

Attention heatmap

1
2
3
4
5
6
7
8
9
from __future__ import absolute_import, division, print_function, unicode_literals

# !pip install tf-nightly-gpu-2.0-preview
import tensorflow_datasets as tfds
import tensorflow as tf

import time
import numpy as np
import matplotlib.pyplot as plt
/home/dongnanzhy/miniconda3/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters

Setup input pipeline

Use TFDS to load the Portugese-English translation dataset from the TED Talks Open Translation Project.

This dataset contains approximately 50000 training examples, 1100 validation examples, and 2000 test examples.

1
2
3
examples, metadata = tfds.load('ted_hrlr_translate/pt_to_en', with_info=True,
as_supervised=True)
train_examples, val_examples = examples['train'], examples['validation']
Downloading and preparing dataset ted_hrlr_translate (124.94 MiB) to /home/dongnanzhy/tensorflow_datasets/ted_hrlr_translate/pt_to_en/0.0.1...

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

WARNING: Logging before flag parsing goes to stderr.
W0525 16:13:27.155797 140479363516160 deprecation.py:323] From /home/dongnanzhy/miniconda3/lib/python3.6/site-packages/tensorflow_datasets/core/file_format_adapter.py:247: tf_record_iterator (from tensorflow.python.lib.io.tf_record) is deprecated and will be removed in a future version.
Instructions for updating:
Use eager execution and: 
`tf.data.TFRecordDataset(path)`

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Failed to display Jupyter Widget of type HBox.



If you’re reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
that the widgets JavaScript is still loading. If this message persists, it
likely means that the widgets JavaScript library is either not installed or
not enabled. See the Jupyter
Widgets Documentation
for setup instructions.



If you’re reading this message in another frontend (for example, a static
rendering on GitHub or NBViewer),
it may mean that your frontend doesn’t currently support widgets.

Dataset ted_hrlr_translate downloaded and prepared to /home/dongnanzhy/tensorflow_datasets/ted_hrlr_translate/pt_to_en/0.0.1. Subsequent calls will reuse this data.

Create a custom subwords tokenizer from the training dataset.

1
2
3
4
5
for pt, en in train_examples:
print(pt)
print(en)
print(en.numpy())
break
tf.Tensor(b'agora aqui temos imagens sendo extra\xc3\xaddas em tempo real diretamente do feed ,', shape=(), dtype=string)
tf.Tensor(b'now here are live images being pulled straight from the feed .', shape=(), dtype=string)
b'now here are live images being pulled straight from the feed .'
1
2
3
4
5
tokenizer_en = tfds.features.text.SubwordTextEncoder.build_from_corpus(
(en.numpy() for pt, en in train_examples), target_vocab_size=2**13)

tokenizer_pt = tfds.features.text.SubwordTextEncoder.build_from_corpus(
(pt.numpy() for pt, en in train_examples), target_vocab_size=2**13)
1
2
3
4
5
6
7
8
9
sample_string = 'Transformer is awesome.'

tokenized_string = tokenizer_en.encode(sample_string)
print ('Tokenized string is {}'.format(tokenized_string))

original_string = tokenizer_en.decode(tokenized_string)
print ('The original string: {}'.format(original_string))

assert original_string == sample_string
Tokenized string is [7915, 1248, 7946, 7194, 13, 2799, 7877]
The original string: Transformer is awesome.

The tokenizer encodes the string by breaking it into subwords if the word is not in its dictionary.

Tensforflow Tokenizer自动把不在vocab里面的词按char level分开了,确保所有tokenize后的词都在vocab里

1
2
for ts in tokenized_string:
print ('{} ----> {}'.format(ts, tokenizer_en.decode([ts])))
7915 ----> T
1248 ----> ran
7946 ----> s
7194 ----> former 
13 ----> is 
2799 ----> awesome
7877 ----> .
1
2
BUFFER_SIZE = 20000
BATCH_SIZE = 64

Add a start and end token to the input and target.

1
2
3
4
5
6
7
8
9
10
def encode(lang1, lang2):
# vocab加入 'start of sentence' and 'end of sentence'
# lang1, lang2 is Tensor, use '.numpy()' to binary string
lang1 = [tokenizer_pt.vocab_size] + tokenizer_pt.encode(
lang1.numpy()) + [tokenizer_pt.vocab_size+1]

lang2 = [tokenizer_en.vocab_size] + tokenizer_en.encode(
lang2.numpy()) + [tokenizer_en.vocab_size+1]

return lang1, lang2

Note: To keep this example small and relatively fast, drop examples with a length of over 40 tokens.

1
2
3
4
MAX_LENGTH = 40
def filter_max_length(x, y, max_length=MAX_LENGTH):
return tf.logical_and(tf.size(x) <= max_length,
tf.size(y) <= max_length)

Operations inside .map() run in graph mode and receive a graph tensor that do not have a numpy attribute. The tokenizer expects a string or Unicode symbol to encode it into integers. Hence, you need to run the encoding inside a tf.py_function, which receives an eager tensor having a numpy attribute that contains the string value.

1
2
def tf_encode(pt, en):
return tf.py_function(encode, [pt, en], [tf.int64, tf.int64])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Train set
# string to integer(token id)
train_dataset = train_examples.map(tf_encode)
# fileter by max length(40)
train_dataset = train_dataset.filter(filter_max_length)
# cache the dataset to memory to get a speedup while reading from it.
train_dataset = train_dataset.cache()
train_dataset = train_dataset.shuffle(BUFFER_SIZE).padded_batch(
BATCH_SIZE, padded_shapes=([-1], [-1]))
train_dataset = train_dataset.prefetch(tf.data.experimental.AUTOTUNE)

# Validation set
val_dataset = val_examples.map(tf_encode)
val_dataset = val_dataset.filter(filter_max_length).padded_batch(
BATCH_SIZE, padded_shapes=([-1], [-1]))
1
2
de_batch, en_batch = next(iter(val_dataset))
de_batch, en_batch
(<tf.Tensor: id=311424, shape=(64, 40), dtype=int64, numpy=
 array([[8214, 1259,    5, ...,    0,    0,    0],
        [8214,  299,   13, ...,    0,    0,    0],
        [8214,   59,    8, ...,    0,    0,    0],
        ...,
        [8214,   95,    3, ...,    0,    0,    0],
        [8214, 5157,    1, ...,    0,    0,    0],
        [8214, 4479, 7990, ...,    0,    0,    0]])>,
 <tf.Tensor: id=311425, shape=(64, 40), dtype=int64, numpy=
 array([[8087,   18,   12, ...,    0,    0,    0],
        [8087,  634,   30, ...,    0,    0,    0],
        [8087,   16,   13, ...,    0,    0,    0],
        ...,
        [8087,   12,   20, ...,    0,    0,    0],
        [8087,   17, 4981, ...,    0,    0,    0],
        [8087,   12, 5453, ...,    0,    0,    0]])>)

Positional encoding

Since this model doesn’t contain any recurrence or convolution, positional encoding is added to give the model some information about the relative position of the words in the sentence.

The positional encoding vector is added to the embedding vector. Embeddings represent a token in a d-dimensional space where tokens with similar meaning will be closer to each other. But the embeddings do not encode the relative position of words in a sentence. So after adding the positional encoding, words will be closer to each other based on the similarity of their meaning and their position in the sentence, in the d-dimensional space.

See the notebook on positional encoding to learn more about it. The formula for calculating the positional encoding is as follows:

$$\Large{PE_{(pos, 2i)} = sin(pos / 10000^{2i / d_{model}})} $$
$$\Large{PE_{(pos, 2i+1)} = cos(pos / 10000^{2i / d_{model}})} $$

1
2
3
4
5
6
def get_angles(pos, i, d_model):
# pos: 句子中的位置
# i: embedding 第 i 位
# d_model: embedding dimension
angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
return pos * angle_rates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def positional_encoding(position, d_model):
angle_rads = get_angles(np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model)

# apply sin to even indices in the array; 2i
sines = np.sin(angle_rads[:, 0::2])

# apply cos to odd indices in the array; 2i+1
cosines = np.cos(angle_rads[:, 1::2])

# 注意:这里直接把sines和consines concat在一起,并没有按原始i奇偶穿插
pos_encoding = np.concatenate([sines, cosines], axis=-1)

pos_encoding = pos_encoding[np.newaxis, ...]

# 注意:这里返回tensor默认batch=1,
# 因为position encoding与输入无关,后续计算时利用broadcast相加,保证batch每个instance都有position encoding
return tf.cast(pos_encoding, dtype=tf.float32) # (1, seq_len, embedding_dim)
1
2
3
4
5
6
7
8
9
pos_encoding = positional_encoding(50, 512)
print (pos_encoding.shape)

plt.pcolormesh(pos_encoding[0], cmap='RdBu')
plt.xlabel('Depth')
plt.xlim((0, 512))
plt.ylabel('Position')
plt.colorbar()
plt.show()
(1, 50, 512)

png

Masking

Mask all the pad tokens in the batch of sequence. It ensures that the model does not treat padding as the input. The mask indicates where pad value 0 is present: it outputs a 1 at those locations, and a 0 otherwise. (1代表被mask掉,0代表通过)

1
2
3
4
5
6
def create_padding_mask(seq):
seq = tf.cast(tf.math.equal(seq, 0), tf.float32)

# add extra dimensions so that we can add the padding
# to the attention logits.
return seq[:, tf.newaxis, tf.newaxis, :] # (batch_size, 1, 1, seq_len)
1
2
x = tf.constant([[7, 6, 0, 0, 1], [1, 2, 3, 0, 0], [0, 0, 0, 4, 5]])
create_padding_mask(x)
<tf.Tensor: id=311442, shape=(3, 1, 1, 5), dtype=float32, numpy=
array([[[[0., 0., 1., 1., 0.]]],


       [[[0., 0., 0., 1., 1.]]],


       [[[1., 1., 1., 0., 0.]]]], dtype=float32)>

The look-ahead mask is used to mask the future tokens in a sequence. In other words, the mask indicates which entries should not be used.

This means that to predict the third word, only the first and second word will be used. Similarly to predict the fourth word, only the first, second and the third word will be used and so on.

look-ahead mask 确保sequence只能看到之前出现的word (1代表被mask掉,0代表通过)

1
2
3
def create_look_ahead_mask(size):
mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
return mask # (seq_len, seq_len)
1
2
3
x = tf.random.uniform((1, 3))
temp = create_look_ahead_mask(x.shape[1])
temp
<tf.Tensor: id=311458, shape=(3, 3), dtype=float32, numpy=
array([[0., 1., 1.],
       [0., 0., 1.],
       [0., 0., 0.]], dtype=float32)>

Preprocessing finished


Scaled dot product attention

scaled_dot_product_attention

The attention function used by the transformer takes three inputs: Q (query), K (key), V (value). The equation used to calculate the attention weights is:

$$\Large{Attention(Q, K, V) = softmax_k(\frac{QK^T}{\sqrt{d_k}}) V} $$

d_k 代表K的dimension,这么做可以保证Q和K相乘后依然保留var=1. The dot-product attention is scaled by a factor of square root of the depth. This is done because for large values of depth, the dot product grows large in magnitude pushing the softmax function where it has small gradients resulting in a very hard softmax.

For example, consider that Q and K have a mean of 0 and variance of 1. Their matrix multiplication will have a mean of 0 and variance of dk. Hence, square root of dk is used for scaling (and not any other number) because the matmul of Q and K should have a mean of 0 and variance of 1, so that we get a gentler softmax.

mask乘以无穷小,0没有影响,表示通过。1变成无穷小,计算softmax后变成0,表示不通过. The mask is multiplied with -1e9 (close to negative infinity). This is done because the mask is summed with the scaled matrix multiplication of Q and K and is applied immediately before a softmax. The goal is to zero out these cells, and large negative inputs to softmax are near zero in the output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def scaled_dot_product_attention(q, k, v, mask):
"""Calculate the attention weights.
q, k, v must have matching leading dimensions.
The mask has different shapes depending on its type(padding or look ahead)
but it must be broadcastable for addition.

Args:
q: query shape == (..., seq_len_q, depth)
k: key shape == (..., seq_len_k, depth)
v: value shape == (..., seq_len_v, depth)
mask: Float tensor with shape broadcastable
to (..., seq_len_q, seq_len_k). Defaults to None.

Returns:
output, attention_weights
"""

matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)

# scale matmul_qk
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)

# add the mask to the scaled tensor.
# (mask=1 means block, mask=0 means pass)
if mask is not None:
scaled_attention_logits += (mask * -1e9)

# 注意:softmax is normalized on the last axis (seq_len_k) so that the scores
# add up to 1.
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k)

output = tf.matmul(attention_weights, v) # (..., seq_len_v, depth)

return output, attention_weights

As the softmax normalization is done on K, its values decide the amount of importance given to Q.

The output represents the multiplication of the attention weights and the V (value) vector. This ensures that the words we want to focus on are kept as is and the irrelevant words are flushed out.

1
2
3
4
5
6
7
def print_out(q, k, v):
temp_out, temp_attn = scaled_dot_product_attention(
q, k, v, None)
print ('Attention weights are:')
print (temp_attn)
print ('Output is:')
print (temp_out)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
np.set_printoptions(suppress=True)

temp_k = tf.constant([[10,0,0],
[0,10,0],
[0,0,10],
[0,0,10]], dtype=tf.float32) # (4, 3)

temp_v = tf.constant([[ 1,0],
[ 10,0],
[ 100,5],
[1000,6]], dtype=tf.float32) # (4, 2)

# This `query` aligns with the second `key`, query=Key[1:]
# so the second `value` is returned.
temp_q = tf.constant([[0, 10, 0]], dtype=tf.float32) # (1, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor([[0. 1. 0. 0.]], shape=(1, 4), dtype=float32)
Output is:
tf.Tensor([[10.  0.]], shape=(1, 2), dtype=float32)
1
2
3
4
# This query aligns with a repeated key (third and fourth), 
# so all associated values get averaged.
temp_q = tf.constant([[0, 0, 10]], dtype=tf.float32) # (1, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor([[0.  0.  0.5 0.5]], shape=(1, 4), dtype=float32)
Output is:
tf.Tensor([[550.    5.5]], shape=(1, 2), dtype=float32)
1
2
3
4
# This query aligns equally with the first and second key, 
# so their values get averaged.
temp_q = tf.constant([[10, 10, 0]], dtype=tf.float32) # (1, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor([[0.5 0.5 0.  0. ]], shape=(1, 4), dtype=float32)
Output is:
tf.Tensor([[5.5 0. ]], shape=(1, 2), dtype=float32)

Pass all the queries together.

1
2
temp_q = tf.constant([[0, 10, 0], [0, 0, 10], [10, 10, 0]], dtype=tf.float32)  # (3, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor(
[[0.  1.  0.  0. ]
 [0.  0.  0.5 0.5]
 [0.5 0.5 0.  0. ]], shape=(3, 4), dtype=float32)
Output is:
tf.Tensor(
[[ 10.    0. ]
 [550.    5.5]
 [  5.5   0. ]], shape=(3, 2), dtype=float32)

Multi-head attention

multi-head attention

Multi-head attention consists of four parts:

  • Linear layers and split into heads. (先把Q,K,V分成几个Heads)
  • Scaled dot-product attention. (每个Head计算scaled dot-product attention)
  • Concatenation of heads. (concat每个Head attn结果)
  • Final linear layer. (再过一层FC[dimension 不变])

Each multi-head attention block gets three inputs; Q (query), K (key), V (value). These are put through linear (Dense) layers and split up into multiple heads.

The scaled_dot_product_attention defined above is applied to each head (broadcasted for efficiency). An appropriate mask must be used in the attention step. The attention output for each head is then concatenated (using tf.transpose, and tf.reshape) and put through a final Dense layer.

Instead of one single attention head, Q, K, and V are split into multiple heads because it allows the model to jointly attend to information at different positions from different representational spaces. After the split each head has a reduced dimensionality, so the total computation cost is the same as a single head attention with full dimensionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model

assert d_model % self.num_heads == 0

self.depth = d_model // self.num_heads

# 定义Q,K,V的FC
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)

# 定义最后输出的FC
self.dense = tf.keras.layers.Dense(d_model)

def split_heads(self, x, batch_size):
"""Split the last dimension into (num_heads, depth).
Transpose the result such that the shape is (batch_size, num_heads, seq_len, depth)
"""
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])

def call(self, v, k, q, mask):
batch_size = tf.shape(q)[0]

# step1: Q,K,V 先过FC layer, 做linear transform
q = self.wq(q) # (batch_size, seq_len, d_model)
k = self.wk(k) # (batch_size, seq_len, d_model)
v = self.wv(v) # (batch_size, seq_len, d_model)

# step2: 根据定义的num_heads来split Q,K,V
q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)

# scaled_attention.shape == (batch_size, num_heads, seq_len_v, depth) [attention_output]
# attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k) [attention_weight]
scaled_attention, attention_weights = scaled_dot_product_attention(
q, k, v, mask)

scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_v, num_heads, depth)

# step 3: concat Heads
concat_attention = tf.reshape(scaled_attention,
(batch_size, -1, self.d_model)) # (batch_size, seq_len_v, d_model)

# step 4: Final FC
output = self.dense(concat_attention) # (batch_size, seq_len_v, d_model)

return output, attention_weights

Create a MultiHeadAttention layer to try out. At each location in the sequence, y, the MultiHeadAttention runs all 8 attention heads across all other locations in the sequence, returning a new vector of the same length at each location.

1
2
3
4
temp_mha = MultiHeadAttention(d_model=512, num_heads=8)
y = tf.random.uniform((1, 60, 512)) # (batch_size, encoder_sequence, d_model)
out, attn = temp_mha(y, k=y, q=y, mask=None)
out.shape, attn.shape
(TensorShape([1, 60, 512]), TensorShape([1, 8, 60, 60]))

Point wise feed forward network

Point wise feed forward network consists of two fully-connected layers with a ReLU activation in between.

1
2
3
4
5
def point_wise_feed_forward_network(d_model, dff):
return tf.keras.Sequential([
tf.keras.layers.Dense(dff, activation='relu'), # (batch_size, seq_len, dff)
tf.keras.layers.Dense(d_model) # (batch_size, seq_len, d_model)
])
1
2
sample_ffn = point_wise_feed_forward_network(512, 2048)
sample_ffn(tf.random.uniform((64, 50, 512))).shape
TensorShape([64, 50, 512])

Attention and Multi-Head module finished


Encoder and decoder

transformer

The transformer model follows the same general pattern as a standard sequence to sequence with attention model.

  • The input sentence is passed through N encoder layers that generates an output for each word/token in the sequence.
  • The decoder attends on the encoder’s output and its own input (self-attention) to predict the next word.

Encoder layer

Each encoder layer consists of sublayers:

  1. Multi-head attention (with padding mask)
  2. Point wise feed forward networks.

Each of these sublayers has a residual connection around it followed by a layer normalization. Residual connections help in avoiding the vanishing gradient problem in deep networks.

The output of each sublayer is LayerNorm(x + Sublayer(x)). The normalization is done on the d_model (last) axis. There are N encoder layers in the transformer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class EncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(EncoderLayer, self).__init__()

self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, dff)

# TODO: 原文件定义为LayerNormalization,没有这个API
self.layernorm1 = tf.keras.layers.BatchNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.BatchNormalization(epsilon=1e-6)

self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)

def call(self, x, training, mask):

# step 1: Multi-head attention (encoder也做了self-attention,只不过Q, V, K 都是 x 自己)
attn_output, _ = self.mha(x, x, x, mask) # (batch_size, input_seq_len, d_model)
## add dropout
attn_output = self.dropout1(attn_output, training=training)
## add residual before norm
out1 = self.layernorm1(x + attn_output) # (batch_size, input_seq_len, d_model)

# step 2: Feed forward networks
ffn_output = self.ffn(out1) # (batch_size, input_seq_len, d_model)
## add dropout
ffn_output = self.dropout2(ffn_output, training=training)
## add residual before norm
out2 = self.layernorm2(out1 + ffn_output) # (batch_size, input_seq_len, d_model)

return out2
1
2
3
4
5
6
sample_encoder_layer = EncoderLayer(512, 8, 2048)

sample_encoder_layer_output = sample_encoder_layer(
tf.random.uniform((64, 43, 512)), False, None)

sample_encoder_layer_output.shape # (batch_size, input_seq_len, d_model)
TensorShape([64, 43, 512])

Decoder layer

Each decoder layer consists of sublayers:

  1. Masked multi-head attention (with look ahead mask and padding mask)
  2. Multi-head attention (with padding mask). V (value) and K (key) receive the encoder output as inputs. Q (query) receives the output from the masked multi-head attention sublayer.
  3. Point wise feed forward networks

Each of these sublayers has a residual connection around it followed by a layer normalization. The output of each sublayer is LayerNorm(x + Sublayer(x)). The normalization is done on the d_model (last) axis.

There are N decoder layers in the transformer.

As Q receives the output from decoder’s first attention block, and K receives the encoder output, the attention weights represent the importance given to the decoder’s input based on the encoder’s output. In other words, the decoder predicts the next word by looking at the encoder output and self-attending to its own output. See the demonstration above in the scaled dot product attention section.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DecoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(DecoderLayer, self).__init__()

self.mha1 = MultiHeadAttention(d_model, num_heads)
self.mha2 = MultiHeadAttention(d_model, num_heads)

self.ffn = point_wise_feed_forward_network(d_model, dff)

self.layernorm1 = tf.keras.layers.BatchNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.BatchNormalization(epsilon=1e-6)
self.layernorm3 = tf.keras.layers.BatchNormalization(epsilon=1e-6)

self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
self.dropout3 = tf.keras.layers.Dropout(rate)


def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
# enc_output: shape ==> (batch_size, input_seq_len, d_model)

# step 1: decoder self-attention, use look-ahead-mask
attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask) # (batch_size, target_seq_len, d_model)
attn1 = self.dropout1(attn1, training=training)
out1 = self.layernorm1(attn1 + x)

# step 2: decoder attention on encoder. V,K from encoder, Q from decoder
attn2, attn_weights_block2 = self.mha2(
enc_output, enc_output, out1, padding_mask) # (batch_size, target_seq_len, d_model)
attn2 = self.dropout2(attn2, training=training)
out2 = self.layernorm2(attn2 + out1) # (batch_size, target_seq_len, d_model)

# step 3: feed forward networks
ffn_output = self.ffn(out2) # (batch_size, target_seq_len, d_model)
ffn_output = self.dropout3(ffn_output, training=training)
out3 = self.layernorm3(ffn_output + out2) # (batch_size, target_seq_len, d_model)

return out3, attn_weights_block1, attn_weights_block2
1
2
3
4
5
6
7
sample_decoder_layer = DecoderLayer(512, 8, 2048)

sample_decoder_layer_output, _, _ = sample_decoder_layer(
tf.random.uniform((64, 50, 512)), sample_encoder_layer_output,
False, None, None)

sample_decoder_layer_output.shape # (batch_size, target_seq_len, d_model)
TensorShape([64, 50, 512])

Encoder

The Encoder consists of:

  1. Input Embedding
  2. Positional Encoding
  3. N encoder layers

The input is put through an embedding which is summed with the positional encoding. The output of this summation is the input to the encoder layers. The output of the encoder is the input to the decoder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Encoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
rate=0.1):
super(Encoder, self).__init__()

self.d_model = d_model
self.num_layers = num_layers

self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
self.pos_encoding = positional_encoding(MAX_LENGTH, self.d_model) # (1, max_seq_len, embedding_dim)


self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]

self.dropout = tf.keras.layers.Dropout(rate)

def call(self, x, training, mask):
# x: shape ==> (batch, seq_len)
seq_len = tf.shape(x)[1]

# step 1: adding embedding
x = self.embedding(x) # (batch_size, input_seq_len, d_model)
## 注意这里乘了一个scale
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))

# step 2: adding position encoding.
x += self.pos_encoding[:, :seq_len, :]

x = self.dropout(x, training=training)

# step 3: stack encoding layers
for i in range(self.num_layers):
x = self.enc_layers[i](x, training, mask)

return x # (batch_size, input_seq_len, d_model)
1
2
3
4
5
6
7
sample_encoder = Encoder(num_layers=2, d_model=512, num_heads=8, 
dff=2048, input_vocab_size=8500)

sample_encoder_output = sample_encoder(tf.random.uniform((64, 40)),
training=False, mask=None)

print (sample_encoder_output.shape) # (batch_size, input_seq_len, d_model)
(64, 40, 512)

Decoder

The Decoder consists of:

  1. Output Embedding
  2. Positional Encoding
  3. N decoder layers

The target is put through an embedding which is summed with the positional encoding. The output of this summation is the input to the decoder layers. The output of the decoder is the input to the final linear layer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Decoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, target_vocab_size,
rate=0.1):
super(Decoder, self).__init__()

self.d_model = d_model
self.num_layers = num_layers

self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
self.pos_encoding = positional_encoding(MAX_LENGTH, self.d_model)

self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]
self.dropout = tf.keras.layers.Dropout(rate)

def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
# x: shape ==> (batch, seq_len)
seq_len = tf.shape(x)[1]
attention_weights = {}

# step 1: adding embedding
x = self.embedding(x) # (batch_size, target_seq_len, d_model)
## 注意这里乘了一个scale
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))

# step 2: adding position encoding.
x += self.pos_encoding[:, :seq_len, :]

x = self.dropout(x, training=training)

for i in range(self.num_layers):
# 注意:这里decoder每一层用的encoder output是一样的
x, block1, block2 = self.dec_layers[i](x, enc_output, training,
look_ahead_mask, padding_mask)

attention_weights['decoder_layer{}_block1'.format(i+1)] = block1
attention_weights['decoder_layer{}_block2'.format(i+1)] = block2

# x.shape == (batch_size, target_seq_len, d_model)
return x, attention_weights
1
2
3
4
5
6
7
8
9
sample_decoder = Decoder(num_layers=2, d_model=512, num_heads=8, 
dff=2048, target_vocab_size=8000)

output, attn = sample_decoder(tf.random.uniform((64, 26)),
enc_output=sample_encoder_output,
training=False, look_ahead_mask=None,
padding_mask=None)

output.shape, attn['decoder_layer2_block2'].shape
(TensorShape([64, 26, 512]), TensorShape([64, 8, 26, 40]))

Each module built finished!


Create the Transformer

Transformer consists of the encoder, decoder and a final linear layer. The output of the decoder is the input to the linear layer and its output is returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Transformer(tf.keras.Model):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
target_vocab_size, rate=0.1):
super(Transformer, self).__init__()

self.encoder = Encoder(num_layers, d_model, num_heads, dff,
input_vocab_size, rate)

self.decoder = Decoder(num_layers, d_model, num_heads, dff,
target_vocab_size, rate)

self.final_layer = tf.keras.layers.Dense(target_vocab_size)

def call(self, inp, tar, training, enc_padding_mask,
look_ahead_mask, dec_padding_mask):

enc_output = self.encoder(inp, training, enc_padding_mask) # (batch_size, inp_seq_len, d_model)

# dec_output.shape == (batch_size, tar_seq_len, d_model)
dec_output, attention_weights = self.decoder(
tar, enc_output, training, look_ahead_mask, dec_padding_mask)

final_output = self.final_layer(dec_output) # (batch_size, tar_seq_len, target_vocab_size)

return final_output, attention_weights
1
2
3
4
5
6
7
8
9
10
11
12
13
sample_transformer = Transformer(
num_layers=2, d_model=512, num_heads=8, dff=2048,
input_vocab_size=8500, target_vocab_size=8000)

temp_input = tf.random.uniform((64, 40))
temp_target = tf.random.uniform((64, 26))

fn_out, _ = sample_transformer(temp_input, temp_target, training=False,
enc_padding_mask=None,
look_ahead_mask=None,
dec_padding_mask=None)

fn_out.shape # (batch_size, tar_seq_len, target_vocab_size)
TensorShape([64, 26, 8000])

Set hyperparameters

To keep this example small and relatively fast, the values for num_layers, d_model, and dff have been reduced.

The values used in the base model of transformer were; num_layers=6, d_model = 512, dff = 2048. See the paper for all the other versions of the transformer.

Note: By changing the values below, you can get the model that achieved state of the art on many tasks.

1
2
3
4
5
6
7
8
num_layers = 4
d_model = 128
dff = 512
num_heads = 8

input_vocab_size = tokenizer_pt.vocab_size + 2
target_vocab_size = tokenizer_en.vocab_size + 2
dropout_rate = 0.1

Optimizer

Use the Adam optimizer with a custom learning rate scheduler according to the formula in the paper.

$$\Large{lrate = d_{model}^{-0.5} min(step{_}num^{-0.5}, step{_}num warmup{_}steps^{-1.5})}$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
def __init__(self, d_model, warmup_steps=4000):
super(CustomSchedule, self).__init__()

self.d_model = d_model
self.d_model = tf.cast(self.d_model, tf.float32)

self.warmup_steps = warmup_steps

def __call__(self, step):
arg1 = tf.math.rsqrt(step)
arg2 = step * (self.warmup_steps ** -1.5)

return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
1
2
3
4
learning_rate = CustomSchedule(d_model)

optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98,
epsilon=1e-9)
1
2
3
4
5
temp_learning_rate_schedule = CustomSchedule(d_model)

plt.plot(temp_learning_rate_schedule(tf.range(40000, dtype=tf.float32)))
plt.ylabel("Learning Rate")
plt.xlabel("Train Step")
Text(0.5,0,'Train Step')

png

Loss and metrics

Since the target sequences are padded, it is important to apply a padding mask when calculating the loss.

1
2
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True, reduction='none')
1
2
3
4
5
6
7
8
def loss_function(real, pred):
mask = tf.math.logical_not(tf.math.equal(real, 0))
loss_ = loss_object(real, pred)

mask = tf.cast(mask, dtype=loss_.dtype)
loss_ *= mask

return tf.reduce_mean(loss_)
1
2
3
train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(
name='train_accuracy')

Training and checkpointing

1
2
transformer = Transformer(num_layers, d_model, num_heads, dff,
input_vocab_size, target_vocab_size, dropout_rate)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def create_masks(inp, tar):
# Encoder padding mask
enc_padding_mask = create_padding_mask(inp)

# Used in the 2nd attention block in the decoder.
# This padding mask is used to mask the encoder outputs.
dec_padding_mask = create_padding_mask(inp)

# Used in the 1st attention block in the decoder.
# It is used to pad and mask future tokens in the input received by
# the decoder.
look_ahead_mask = create_look_ahead_mask(tf.shape(tar)[1])
dec_target_padding_mask = create_padding_mask(tar)
combined_mask = tf.maximum(dec_target_padding_mask, look_ahead_mask)

return enc_padding_mask, combined_mask, dec_padding_mask

Create the checkpoint path and the checkpoint manager. This will be used to save checkpoints every n epochs.

1
2
3
4
5
6
7
8
9
10
11
checkpoint_path = "./model/transformer/checkpoints/train"

ckpt = tf.train.Checkpoint(transformer=transformer,
optimizer=optimizer)

ckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_path, max_to_keep=5)

# if a checkpoint exists, restore the latest checkpoint.
if ckpt_manager.latest_checkpoint:
ckpt.restore(ckpt_manager.latest_checkpoint)
print ('Latest checkpoint restored!!')

The target is divided into tar_inp and tar_real. tar_inp is passed as an input to the decoder. tar_real is that same input shifted by 1: At each location in tar_input, tar_real contains the next token that should be predicted.

SOS=start of sentence, EOS=end of sentence

For example, sentence = “SOS A lion in the jungle is sleeping EOS”

tar_inp = “SOS A lion in the jungle is sleeping”

tar_real = “A lion in the jungle is sleeping EOS”

The transformer is an auto-regressive model: it makes predictions one part at a time, and uses its output so far to decide what to do next.

注意teacher-forcing的概念,在train的时候用real label传给下一个step。

During training this example uses teacher-forcing (like in the text generation tutorial). Teacher forcing is passing the true output to the next time step regardless of what the model predicts at the current time step.

As the transformer predicts each word, self-attention allows it to look at the previous words in the input sequence to better predict the next word.

To prevent the model from peaking at the expected output the model uses a look-ahead mask.

1
EPOCHS = 20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@tf.function
def train_step(inp, tar):
tar_inp = tar[:, :-1]
tar_real = tar[:, 1:]

enc_padding_mask, combined_mask, dec_padding_mask = create_masks(inp, tar_inp)

with tf.GradientTape() as tape:
predictions, _ = transformer(inp, tar_inp,
True,
enc_padding_mask,
combined_mask,
dec_padding_mask)
loss = loss_function(tar_real, predictions)

gradients = tape.gradient(loss, transformer.trainable_variables)
optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))

train_loss(loss)
train_accuracy(tar_real, predictions)

Portuguese is used as the input language and English is the target language.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for epoch in range(EPOCHS):
start = time.time()

train_loss.reset_states()
train_accuracy.reset_states()

# inp -> portuguese, tar -> english
# 注意:dataset是通过自定义encode()和filter() function后生成的
for (batch, (inp, tar)) in enumerate(train_dataset):
train_step(inp, tar)

if batch % 500 == 0:
print ('Epoch {} Batch {} Loss {:.4f} Accuracy {:.4f}'.format(
epoch + 1, batch, train_loss.result(), train_accuracy.result()))

if (epoch + 1) % 5 == 0:
ckpt_save_path = ckpt_manager.save()
print ('Saving checkpoint for epoch {} at {}'.format(epoch+1,
ckpt_save_path))

print ('Epoch {} Loss {:.4f} Accuracy {:.4f}'.format(epoch + 1,
train_loss.result(),
train_accuracy.result()))

print ('Time taken for 1 epoch: {} secs\n'.format(time.time() - start))
Epoch 1 Batch 0 Loss 10.9017 Accuracy 0.0000
Epoch 1 Batch 500 Loss 4.0922 Accuracy 0.0294
Epoch 1 Loss 3.7079 Accuracy 0.0443
Time taken for 1 epoch: 110.26966953277588 secs

Epoch 2 Batch 0 Loss 2.5675 Accuracy 0.0855
Epoch 2 Batch 500 Loss 2.3795 Accuracy 0.1250
Epoch 2 Loss 2.3093 Accuracy 0.1324
Time taken for 1 epoch: 82.4690248966217 secs

Epoch 3 Batch 0 Loss 2.0194 Accuracy 0.1517
Epoch 3 Batch 500 Loss 1.9885 Accuracy 0.1704
Epoch 3 Loss 1.9433 Accuracy 0.1747
Time taken for 1 epoch: 84.78626227378845 secs

Epoch 4 Batch 0 Loss 1.7177 Accuracy 0.1863
Epoch 4 Batch 500 Loss 1.7245 Accuracy 0.1992
Epoch 4 Loss 1.6884 Accuracy 0.2023
Time taken for 1 epoch: 83.60331654548645 secs

Epoch 5 Batch 0 Loss 1.5059 Accuracy 0.2113
Epoch 5 Batch 500 Loss 1.5126 Accuracy 0.2209
Saving checkpoint for epoch 5 at ./model/transformer/checkpoints/train/ckpt-1
Epoch 5 Loss 1.4843 Accuracy 0.2230
Time taken for 1 epoch: 83.2209415435791 secs

Epoch 6 Batch 0 Loss 1.3257 Accuracy 0.2278
Epoch 6 Batch 500 Loss 1.3507 Accuracy 0.2370
Epoch 6 Loss 1.3275 Accuracy 0.2390
Time taken for 1 epoch: 79.70339369773865 secs

Epoch 7 Batch 0 Loss 1.2014 Accuracy 0.2451
Epoch 7 Batch 500 Loss 1.1911 Accuracy 0.2556
Epoch 7 Loss 1.1679 Accuracy 0.2577
Time taken for 1 epoch: 80.37797617912292 secs

Epoch 8 Batch 0 Loss 1.0367 Accuracy 0.2751
Epoch 8 Batch 500 Loss 1.0560 Accuracy 0.2726
Epoch 8 Loss 1.0396 Accuracy 0.2739
Time taken for 1 epoch: 84.6077356338501 secs

Epoch 9 Batch 0 Loss 0.9316 Accuracy 0.2829
Epoch 9 Batch 500 Loss 0.9606 Accuracy 0.2844
Epoch 9 Loss 0.9490 Accuracy 0.2852
Time taken for 1 epoch: 84.28984260559082 secs

Epoch 10 Batch 0 Loss 0.8716 Accuracy 0.2862
Epoch 10 Batch 500 Loss 0.8863 Accuracy 0.2937
Saving checkpoint for epoch 10 at ./model/transformer/checkpoints/train/ckpt-2
Epoch 10 Loss 0.8778 Accuracy 0.2942
Time taken for 1 epoch: 85.24914264678955 secs

Epoch 11 Batch 0 Loss 0.8273 Accuracy 0.2915
Epoch 11 Batch 500 Loss 0.8279 Accuracy 0.3018
Epoch 11 Loss 0.8208 Accuracy 0.3021
Time taken for 1 epoch: 84.971688747406 secs

Epoch 12 Batch 0 Loss 0.7629 Accuracy 0.3059
Epoch 12 Batch 500 Loss 0.7773 Accuracy 0.3086
Epoch 12 Loss 0.7714 Accuracy 0.3089
Time taken for 1 epoch: 84.43891286849976 secs

Epoch 13 Batch 0 Loss 0.7163 Accuracy 0.3063
Epoch 13 Batch 500 Loss 0.7394 Accuracy 0.3136
Epoch 13 Loss 0.7331 Accuracy 0.3139
Time taken for 1 epoch: 84.75990414619446 secs

Epoch 14 Batch 0 Loss 0.6871 Accuracy 0.3133
Epoch 14 Batch 500 Loss 0.7013 Accuracy 0.3190
Epoch 14 Loss 0.6963 Accuracy 0.3191
Time taken for 1 epoch: 85.32253313064575 secs

Epoch 15 Batch 0 Loss 0.6414 Accuracy 0.3162
Epoch 15 Batch 500 Loss 0.6717 Accuracy 0.3235
Saving checkpoint for epoch 15 at ./model/transformer/checkpoints/train/ckpt-3
Epoch 15 Loss 0.6675 Accuracy 0.3235
Time taken for 1 epoch: 84.33578681945801 secs

Epoch 16 Batch 0 Loss 0.6201 Accuracy 0.3232
Epoch 16 Batch 500 Loss 0.6434 Accuracy 0.3278
Epoch 16 Loss 0.6397 Accuracy 0.3277
Time taken for 1 epoch: 82.0535318851471 secs

Epoch 17 Batch 0 Loss 0.5928 Accuracy 0.3298
Epoch 17 Batch 500 Loss 0.6186 Accuracy 0.3315
Epoch 17 Loss 0.6156 Accuracy 0.3312
Time taken for 1 epoch: 86.0015115737915 secs

Epoch 18 Batch 0 Loss 0.5472 Accuracy 0.3343
Epoch 18 Batch 500 Loss 0.5961 Accuracy 0.3348
Epoch 18 Loss 0.5929 Accuracy 0.3346
Time taken for 1 epoch: 85.77375030517578 secs

Epoch 19 Batch 0 Loss 0.5477 Accuracy 0.3343
Epoch 19 Batch 500 Loss 0.5746 Accuracy 0.3385
Epoch 19 Loss 0.5726 Accuracy 0.3379
Time taken for 1 epoch: 86.64751982688904 secs

Epoch 20 Batch 0 Loss 0.5593 Accuracy 0.3331
Epoch 20 Batch 500 Loss 0.5569 Accuracy 0.3414
Saving checkpoint for epoch 20 at ./model/transformer/checkpoints/train/ckpt-4
Epoch 20 Loss 0.5543 Accuracy 0.3410
Time taken for 1 epoch: 82.36839008331299 secs

Training finished


Evaluate

The following steps are used for evaluation:

  • Encode the input sentence using the Portuguese tokenizer (tokenizer_pt). Moreover, add the start and end token so the input is equivalent to what the model is trained with. This is the encoder input.
  • The decoder input is the start token == tokenizer_en.vocab_size.
  • Calculate the padding masks and the look ahead masks.
  • The decoder then outputs the predictions by looking at the encoder output and its own output (self-attention).
  • Select the last word and calculate the argmax of that.
  • Concatentate the predicted word to the decoder input as pass it to the decoder.
  • In this approach, the decoder predicts the next word based on the previous words it predicted.

Note: The model used here has less capacity to keep the example relatively faster so the predictions maybe less right. To reproduce the results in the paper, use the entire dataset and base transformer model or transformer XL, by changing the hyperparameters above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def evaluate(inp_sentence):
start_token = [tokenizer_pt.vocab_size]
end_token = [tokenizer_pt.vocab_size + 1]

# step 1: inp sentence is portuguese, hence adding the start and end token
inp_sentence = start_token + tokenizer_pt.encode(inp_sentence) + end_token
encoder_input = tf.expand_dims(inp_sentence, 0)

# step 2: decoder
# as the target is english, the first word to the transformer should be the english start token.
# 注意:decoder这里只append了“Start of sentence”
decoder_input = [tokenizer_en.vocab_size]
output = tf.expand_dims(decoder_input, 0)

for i in range(MAX_LENGTH):
# step 3: create mask
enc_padding_mask, combined_mask, dec_padding_mask = create_masks(
encoder_input, output)

# step 4: make predicitons
# predictions.shape == (batch_size, seq_len, vocab_size)
predictions, attention_weights = transformer(encoder_input,
output,
False,
enc_padding_mask,
combined_mask,
dec_padding_mask)

# step 5: select the last word from the seq_len dimension
predictions = predictions[: ,-1:, :] # (batch_size, 1, vocab_size)

predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)

# step 6: 如果不是EOS,结果append到output里供下一次预测
# return the result if the predicted_id is equal to the end token
if tf.equal(predicted_id, tokenizer_en.vocab_size+1):
return tf.squeeze(output, axis=0), attention_weights

# concatentate the predicted_id to the output which is given to the decoder
# as its input.
output = tf.concat([output, predicted_id], axis=-1)

return tf.squeeze(output, axis=0), attention_weights
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def plot_attention_weights(attention, sentence, result, layer):
fig = plt.figure(figsize=(16, 8))

sentence = tokenizer_pt.encode(sentence)

attention = tf.squeeze(attention[layer], axis=0)

for head in range(attention.shape[0]):
ax = fig.add_subplot(2, 4, head+1)

# plot the attention weights
ax.matshow(attention[head][:-1, :], cmap='viridis')

fontdict = {'fontsize': 10}

ax.set_xticks(range(len(sentence)+2))
ax.set_yticks(range(len(result)))

ax.set_ylim(len(result)-1.5, -0.5)

ax.set_xticklabels(
['<start>']+[tokenizer_pt.decode([i]) for i in sentence]+['<end>'],
fontdict=fontdict, rotation=90)

ax.set_yticklabels([tokenizer_en.decode([i]) for i in result
if i < tokenizer_en.vocab_size],
fontdict=fontdict)

ax.set_xlabel('Head {}'.format(head+1))

plt.tight_layout()
plt.show()
1
2
3
4
5
6
7
8
9
10
11
def translate(sentence, plot=''):
result, attention_weights = evaluate(sentence)

predicted_sentence = tokenizer_en.decode([i for i in result
if i < tokenizer_en.vocab_size])

print('Input: {}'.format(sentence))
print('Predicted translation: {}'.format(predicted_sentence))

if plot:
plot_attention_weights(attention_weights, sentence, result, plot)
1
2
translate("este é um problema que temos que resolver.")
print ("Real translation: this is a problem we have to solve .")
Input: este é um problema que temos que resolver.
Predicted translation: this is a problem that we have to solve for.com .
Real translation: this is a problem we have to solve .
1
2
translate("os meus vizinhos ouviram sobre esta ideia.")
print ("Real translation: and my neighboring homes heard about this idea .")
Input: os meus vizinhos ouviram sobre esta ideia.
Predicted translation: my neighbors heard about this idea .
Real translation: and my neighboring homes heard about this idea .
1
2
translate("vou então muito rapidamente partilhar convosco algumas histórias de algumas coisas mágicas que aconteceram.")
print ("Real translation: so i 'll just share with you some stories very quickly of some magical things that have happened .")
Input: vou então muito rapidamente partilhar convosco algumas histórias de algumas coisas mágicas que aconteceram.
Predicted translation: so i 'm going to share a lot of little bit with you a few things that happened .
Real translation: so i 'll just share with you some stories very quickly of some magical things that have happened .

You can pass different layers and attention blocks of the decoder to the plot parameter.

1
2
translate("este é o primeiro livro que eu fiz.", plot='decoder_layer4_block2')
print ("Real translation: this is the first book i've ever done.")
Input: este é o primeiro livro que eu fiz.
Predicted translation: this is the first book i made .

png

Real translation: this is the first book i've ever done.

Summary

In this tutorial, you learned about positional encoding, multi-head attention, the importance of masking and how to create a transformer.

Try using a different dataset to train the transformer. You can also create the base transformer or transformer XL by changing the hyperparameters above. You can also use the layers defined here to create BERT and train state of the art models. Futhermore, you can implement beam search to get better predictions.