当前位置:   article > 正文

基于Tensorflow实现一个Transformer翻译器_tensorflow transformer

tensorflow transformer

Transformer是谷歌在2017年的一篇论文"Attention is all you need"提出的一个seq2seq的模型架构,其创造性的提出了自注意力的思想,可以很好的表达序列中各个单词之间的相互注意力关系。这个模型在NLP领域取得了巨大的成功。此外这个模型架构在最近几年也在CV领域取得了令人瞩目的进展,在图像识别,目标检测等方面都达到或超过CNN模型的性能。因此Transformer可以说是人工智能领域最近最值得关注和学习的一个架构。目前有网上已经有很多文章详细解读了Transformer的架构和其细节,这里我将不再重复这方面的内容,而是关注在实战方面,基于Tensorflow来搭建一个Transformer模型,实现法语和英语的翻译。

在Tensorflow的官网上有一个详细的教程,介绍了如何搭建Tranformer来实现葡萄牙语翻译为英语。我也是学习了这个教程之后,进行一些改造,以实现对法语-英语的翻译。

以下是本代码需要导入的库

  1. import re
  2. import tensorflow as tf
  3. from tensorflow_text.tools.wordpiece_vocab import bert_vocab_from_dataset as bert_vocab
  4. import tensorflow_text as text
  5. import pandas as pd
  6. import random
  7. import numpy as np
  8. import matplotlib.pyplot as plt
  9. import time

数据集的准备

在这个网站Tab-delimited Bilingual Sentence Pairs from the Tatoeba Project (Good for Anki and Similar Flashcard Applications)可以找到很多不同的语言与英语的翻译。这里我们下载法语-英语的数据作为训练集和验证集。下载http://www.manythings.org/anki/fra-eng.zip这个文件并解压之后,我们可以看到里面每一行对应一个英语句子和一个法语句子,以及句子的贡献者,中间以TAB分隔。

以下代码是读取文件的数据并查看法语和英语的句子:

  1. fra = []
  2. eng = []
  3. with open('fra.txt', 'r') as f:
  4. content = f.readlines()
  5. for line in content:
  6. temp = line.split(sep='\t')
  7. eng.append(temp[0])
  8. fra.append(temp[1])

查看这些句子,可以看到有些句子包含特殊字符,例如'Cours\u202f!' 我们需要把这些特殊的不可见字符(\u202f, \xa0 ...)去除掉

  1. new_fra = []
  2. new_eng = []
  3. for item in fra:
  4. new_fra.append(re.sub('\s', ' ', item).strip().lower())
  5. for item in eng:
  6. new_eng.append(re.sub('\s', ' ', item).strip().lower())

单词处理为token

因为模型只能处理数字,需要把这些法语和英语的单词转为token。这里采用BERT tokenizer的方式来处理,具体可以参见tensorflow的教程Subword tokenizers  |  Text  |  TensorFlow

首先创建两个dataset,分别包含了法语和英语的句子。

  1. ds_fra = tf.data.Dataset.from_tensor_slices(new_fra)
  2. ds_eng = tf.data.Dataset.from_tensor_slices(new_eng)

调用tensorflow的bert_vocab库来创建词汇表,这里定义了一些保留token用于特殊目的,例如[START]标识句子的开始,[UNK]标识一个不在词汇表出现的新单词。

  1. bert_tokenizer_params=dict(lower_case=True)
  2. reserved_tokens=["[PAD]", "[UNK]", "[START]", "[END]"]
  3. bert_vocab_args = dict(
  4. # The target vocabulary size
  5. vocab_size = 8000,
  6. # Reserved tokens that must be included in the vocabulary
  7. reserved_tokens=reserved_tokens,
  8. # Arguments for `text.BertTokenizer`
  9. bert_tokenizer_params=bert_tokenizer_params,
  10. # Arguments for `wordpiece_vocab.wordpiece_tokenizer_learner_lib.learn`
  11. learn_params={},
  12. )
  13. fr_vocab = bert_vocab.bert_vocab_from_dataset(
  14. ds_fra.batch(1000).prefetch(2),
  15. **bert_vocab_args
  16. )
  17. en_vocab = bert_vocab.bert_vocab_from_dataset(
  18. ds_eng.batch(1000).prefetch(2),
  19. **bert_vocab_args
  20. )

词汇表处理完成之后,我们可以看看里面包含哪些内容:

  1. print(en_vocab[:10])
  2. print(en_vocab[100:110])
  3. print(en_vocab[1000:1010])
  4. print(en_vocab[-10:])

输出如下,可以看到词汇表不是严格按照每个英语单词来划分的,例如'##ers'表示某个单词如果以ers结尾,则会划分出一个'##ers'的token

  1. ['[PAD]', '[UNK]', '[START]', '[END]', '!', '"', '$', '%', '&', "'"]
  2. ['ll', 'there', 've', 'and', 'him', 'time', 'here', 'about', 'get', 'didn']
  3. ['##ers', 'chair', 'earth', 'honest', 'succeed', '##ted', 'animals', 'bill', 'drank', 'lend']
  4. ['##?', '##j', '##q', '##z', '##°', '##–', '##—', '##‘', '##’', '##€']

把词汇表保存为文件,然后我们就可以实例化两个tokenizer,以实现对法语和英语句子的token化处理。

  1. def write_vocab_file(filepath, vocab):
  2. with open(filepath, 'w') as f:
  3. for token in vocab:
  4. print(token, file=f)
  5. write_vocab_file('fr_vocab.txt', fr_vocab)
  6. write_vocab_file('en_vocab.txt', en_vocab)
  7. fr_tokenizer = text.BertTokenizer('fr_vocab.txt', **bert_tokenizer_params)
  8. en_tokenizer = text.BertTokenizer('en_vocab.txt', **bert_tokenizer_params)

下面我们可以测试一下对一些英语句子进行token处理后的结果,这里我们需要给每个句子的开头和结尾分别加上[START]和[END]这两个特殊的token,这样可以方便以后模型的训练。

  1. START = tf.argmax(tf.constant(reserved_tokens) == "[START]")
  2. END = tf.argmax(tf.constant(reserved_tokens) == "[END]")
  3. def add_start_end(ragged):
  4. count = ragged.bounding_shape()[0]
  5. starts = tf.fill([count,1], START)
  6. ends = tf.fill([count,1], END)
  7. return tf.concat([starts, ragged, ends], axis=1)
  8. sentences = ["Hello Roy!", "The sky is blue.", "Nice to meet you!"]
  9. add_start_end(en_tokenizer.tokenize(sentences).merge_dims(1,2)).to_tensor()

输出结果如下:

  1. <tf.Tensor: shape=(3, 7), dtype=int64, numpy=
  2. array([[ 2, 1830, 45, 3450, 4, 3, 0],
  3. [ 2, 62, 1132, 64, 996, 13, 3],
  4. [ 2, 353, 61, 416, 60, 4, 3]])>

构建数据集

现在我们可以构建训练集和验证集了。这里需要把法语和英语的句子都包括在数据集中,其中法语句子作为Transformer编码器的输入,英语句子作为解码器的输入以及模型输出的Target。这里我们用Pandas构造一个Dataframe,随机划分其中80%的数据为训练集,其余为验证集。然后转换为Tensorflow的dataset

  1. df = pd.DataFrame(data={'fra':new_fra, 'eng':new_eng})
  2. # Shuffle the Dataframe
  3. recordnum = df.count()['fra']
  4. indexlist = list(range(recordnum-1))
  5. random.shuffle(indexlist)
  6. df_train = df.loc[indexlist[:int(recordnum*0.8)]]
  7. df_val = df.loc[indexlist[int(recordnum*0.8):]]
  8. ds_train = tf.data.Dataset.from_tensor_slices((df_train.fra.values, df_train.eng.values))
  9. ds_val = tf.data.Dataset.from_tensor_slices((df_val.fra.values, df_val.eng.values))

查看训练集的句子最多包含多少个token

  1. lengths = []
  2. for fr_examples, en_examples in ds_train.batch(1024):
  3. fr_tokens = fr_tokenizer.tokenize(fr_examples)
  4. lengths.append(fr_tokens.row_lengths())
  5. en_tokens = en_tokenizer.tokenize(en_examples)
  6. lengths.append(en_tokens.row_lengths())
  7. print('.', end='', flush=True)
  8. all_lengths = np.concatenate(lengths)
  9. plt.hist(all_lengths, np.linspace(0, 100, 11))
  10. plt.ylim(plt.ylim())
  11. max_length = max(all_lengths)
  12. plt.plot([max_length, max_length], plt.ylim())
  13. plt.title(f'Max tokens per example: {max_length}');

从结果中可以看到训练集的句子转换为token后最多包含67个token:

之后就可以为数据集生成batch,如以下代码:

  1. BUFFER_SIZE = 20000
  2. BATCH_SIZE = 64
  3. MAX_TOKENS = 67
  4. def filter_max_tokens(fr, en):
  5. num_tokens = tf.maximum(tf.shape(fr)[1],tf.shape(en)[1])
  6. return num_tokens < MAX_TOKENS
  7. def tokenize_pairs(fr, en):
  8. fr = add_start_end(fr_tokenizer.tokenize(fr).merge_dims(1,2))
  9. # Convert from ragged to dense, padding with zeros.
  10. fr = fr.to_tensor()
  11. en = add_start_end(en_tokenizer.tokenize(en).merge_dims(1,2))
  12. # Convert from ragged to dense, padding with zeros.
  13. en = en.to_tensor()
  14. return fr, en
  15. def make_batches(ds):
  16. return (
  17. ds
  18. .cache()
  19. .shuffle(BUFFER_SIZE)
  20. .batch(BATCH_SIZE)
  21. .map(tokenize_pairs, num_parallel_calls=tf.data.AUTOTUNE)
  22. .filter(filter_max_tokens)
  23. .prefetch(tf.data.AUTOTUNE))
  24. train_batches = make_batches(ds_train)
  25. val_batches = make_batches(ds_val)

可以生成一个batch来查看一下:

  1. for a in train_batches.take(1):
  2. print(a)

结果如下,可见每个batch包含两个tensor,分别对应法语和英语句子转化为token之后的向量,每个句子以token 2开头,以token 3结尾:

  1. (<tf.Tensor: shape=(64, 24), dtype=int64, numpy=
  2. array([[ 2, 39, 9, ..., 0, 0, 0],
  3. [ 2, 62, 43, ..., 0, 0, 0],
  4. [ 2, 147, 70, ..., 0, 0, 0],
  5. ...,
  6. [ 2, 4310, 14, ..., 0, 0, 0],
  7. [ 2, 39, 9, ..., 0, 0, 0],
  8. [ 2, 68, 64, ..., 0, 0, 0]])>, <tf.Tensor: shape=(64, 20), dtype=int64, numpy=
  9. array([[ 2, 36, 76, ..., 0, 0, 0],
  10. [ 2, 36, 75, ..., 0, 0, 0],
  11. [ 2, 92, 80, ..., 0, 0, 0],
  12. ...,
  13. [ 2, 68, 60, ..., 0, 0, 0],
  14. [ 2, 36, 75, ..., 0, 0, 0],
  15. [ 2, 67, 9, ..., 0, 0, 0]])>)

给输入数据添加位置信息

把上面得到的batch数据输入到embedding层,就可以把每个token转化为一个高位向量,例如转换为一个128维的向量。之后我们需要给这个向量增加一个位置信息以表示这个token在句子中的位置。论文给出了一种对位置信息进行编码的方法,如以下的公式:

PE_{(pos,2i)}=sin(pos/10000^{2i/ d_{model}})

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

公式中pos表示词语的位置,例如一个句子有50个单词,pos取值范围为0-49. d_model表示embedding的维度,例如把每个单词映射为一个128维的向量,d_model=128. i表示这128维里面的维度,取值范围为0-127
因此公式的含义为,对第N个单词,在其128维的嵌入向量中,每个维度都加上对应的位置信息.
以第3个单词为例,pos=2, 在其对应的128维向量,其偶数维(0,2,4...)需要加上sin(2/10000^(2i/128)),2i的对应取值是(0,2,4...). 第2i+1维(1,3,5...)需要加上cos(2/10000^(2i/128)),2i的对应取值是(0,2,4...)

以下代码将生成位置编码向量,这个向量可以加入到token的嵌入向量中。

  1. def get_angles(pos, i, d_model):
  2. angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
  3. return pos * angle_rates
  4. def positional_encoding(position, d_model):
  5. angle_rads = get_angles(np.arange(position)[:, np.newaxis],
  6. np.arange(d_model)[np.newaxis, :],
  7. d_model)
  8. # apply sin to even indices in the array; 2i
  9. angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
  10. # apply cos to odd indices in the array; 2i+1
  11. angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
  12. pos_encoding = angle_rads[np.newaxis, ...]
  13. return tf.cast(pos_encoding, dtype=tf.float32)

创建Padding掩码和look ahead掩码

Mask用于标识输入序列中为0的位置,如果为0,则Mask为1. 这样可以使得padding的字符不会参与到模型的训练中
Look ahead mask是用于在预测是掩盖未来的字符,例如翻译一句法语,对应的英语是目标数据,在训练时,当预测第一个英语单词时,需要把整句英语都掩盖,当预测第二个英语单词时,需要把整句英语的第一个单词之后的都掩盖。这个目的是避免让模型看到之后要预测的单词,影响模型的训练。

  1. def create_padding_mask(seq):
  2. seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
  3. # add extra dimensions to add the padding
  4. # to the attention logits.
  5. return seq[:, tf.newaxis, tf.newaxis, :] # (batch_size, 1, 1, seq_len)
  6. def create_look_ahead_mask(size):
  7. mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
  8. return mask # (seq_len, seq_len)

自注意力计算

现在来到了Transformer的核心概念了,我们需要把输入的向量,通过三个线性转换的矩阵,把它变为Q,K,V三个向量。
通过计算Q和K的相似性来得到注意力系数,再和V相乘,得到对应的数值,如以下的图片:

注意力权重的计算公式如下:

Attention(Q,K,V)=Softmax_{k}\left ( \frac{QK^T}{\sqrt{d_{k}}} \right )V

解释一下这个公式,这里的K和V代表了Key和Value,Q是查询的内容。假设有一句话"Tom is a boy",这句话有4个单词,也就是4个token。通过线性变换之后,每个token都有对应的Q,K, V。当用Tom这个token的Q来做查询时,将比较这个token的Q值与所有4个token的K值,看哪个最相似,然后计算出一个注意力权重,例如我们假定Tom除了和Tom最相似外,和boy是第二相似的,那么通过softmax之后得到的注意力权重是[0.9, 0.005, 0.005, 0.09], 然后再和每个Token的V值相乘,得到最后的注意力值,这个值里面就是每个token的V值根据注意力权重分配后累加之后的数值,包含了token之间的关系。

另外也可以用电商网站的例子来做类比,每个产品都有一个Key来描述,例如PS3游戏机,Value表示这个产品的价格。那么我们输入一个Query词语"PS游戏"时,网站就会进行比对,找到最相似的产品并展示。

具体到上面的计算公式,例如每个token都编码为一个128维的向量。通过三个Q,K,V线性变换矩阵来做变换,其中Q,K矩阵的输出维度为64,V矩阵的输出维度为100。以输入一个批量32个句子为例,这些句子最长的一个有20个token,那么输入的维度是32×20×128。变换之后,Q是32×20×64,K是32×20×64,V是32×20×100。对Q和K的转置矩阵K'进行矩阵乘法,即matmul(Q, K'),得到的结果的维度是32×20×20,表示每个句子中的每个token的Q都和这个句子中的所有token的K做了点乘,计算相似度。在公式中对这个计算结果还要进行缩放,除以维度的开方,即64的开方8,这样做可以使得无论Q,K的维度多大,最后得到的结果的方差保持不变。对这个结果进行Softmax归一处理,得到每个token和其他token的注意力权重。再把这个值与V相乘,得到的结果的维度为32×20×100,即每个句子中的每个token都获得了一个100维的向量表达,这里面编码了token和其他token之间的一些关系。

在代码实现的时候,还要给句子的padding_mask乘以一个很大的负数,加到注意力权重的结果中,再进行softmax计算。这个目的是,对于padding_mask为1的位置,表示这个token是一个padding,没有实际的含义。因此这个位置的注意力权重加上一个很大的负数之后,softmax的结果就是接近于0,这样就可以排除掉padding token的影响。

以下是代码实现:

  1. def scaled_dot_product_attention(q, k, v, mask):
  2. """Calculate the attention weights.
  3. q, k, v must have matching leading dimensions.
  4. k, v must have matching penultimate dimension, i.e.: seq_len_k = seq_len_v.
  5. The mask has different shapes depending on its type(padding or look ahead)
  6. but it must be broadcastable for addition.
  7. Args:
  8. q: query shape == (..., seq_len_q, depth)
  9. k: key shape == (..., seq_len_k, depth)
  10. v: value shape == (..., seq_len_v, depth_v)
  11. mask: Float tensor with shape broadcastable
  12. to (..., seq_len_q, seq_len_k). Defaults to None.
  13. Returns:
  14. output, attention_weights
  15. """
  16. matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)
  17. # scale matmul_qk
  18. dk = tf.cast(tf.shape(k)[-1], tf.float32)
  19. scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
  20. # add the mask to the scaled tensor.
  21. if mask is not None:
  22. scaled_attention_logits += (mask * -1e9)
  23. # softmax is normalized on the last axis (seq_len_k) so that the scores
  24. # add up to 1.
  25. attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k)
  26. output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v)
  27. return output, attention_weights

多头注意力

了解了注意力机制的原理后,可以构造一个多头注意力。这里多头的意思是使得模型可以从不同的层面来关注token之间的关系。例如可以想象其中一头是关注token之间的表达含义的关系,另一头是关注token之间的语法关系。

Multi-head的结构如下图:

 这个Multi-head的结构包括了3部分:

  • 线性变换层
  • Scaled dot product attention
  • 最后的线性变换层

在具体编码实现的时候,我们可以把以上的层按照heads数量进行合并,最后计算完之后再拆分。
例如有8个head, 每个head的线性变换层是转换为一个32维的输出,那么我们可以用一个大的线性变换层来统一处理,输出为32*8维,再把结果的维度修改为[..., 8, 32],把结果统一用一个scaled dot product attention处理,处理之后把结果再按照head数整合,然后经过最后的线性变换层输出。以下是代码实现,封装为一个keras的层:

  1. class MultiHeadAttention(tf.keras.layers.Layer):
  2. def __init__(self,*, d_model, num_heads):
  3. super(MultiHeadAttention, self).__init__()
  4. self.num_heads = num_heads
  5. self.d_model = d_model
  6. assert d_model % self.num_heads == 0
  7. self.depth = d_model // self.num_heads
  8. self.wq = tf.keras.layers.Dense(d_model)
  9. self.wk = tf.keras.layers.Dense(d_model)
  10. self.wv = tf.keras.layers.Dense(d_model)
  11. self.dense = tf.keras.layers.Dense(d_model)
  12. def split_heads(self, x, batch_size):
  13. """Split the last dimension into (num_heads, depth).
  14. Transpose the result such that the shape is (batch_size, num_heads, seq_len, depth)
  15. """
  16. x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
  17. return tf.transpose(x, perm=[0, 2, 1, 3])
  18. def call(self, v, k, q, mask):
  19. batch_size = tf.shape(q)[0]
  20. q = self.wq(q) # (batch_size, seq_len, d_model)
  21. k = self.wk(k) # (batch_size, seq_len, d_model)
  22. v = self.wv(v) # (batch_size, seq_len, d_model)
  23. q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
  24. k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
  25. v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)
  26. # scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth)
  27. # attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
  28. scaled_attention, attention_weights = scaled_dot_product_attention(
  29. q, k, v, mask)
  30. scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_q, num_heads, depth)
  31. concat_attention = tf.reshape(scaled_attention,
  32. (batch_size, -1, self.d_model)) # (batch_size, seq_len_q, d_model)
  33. output = self.dense(concat_attention) # (batch_size, seq_len_q, d_model)
  34. return output, attention_weights

Point wise feed forward network

Multi-head attention输出的结果再通过一个point wise feed forward network进行转换,这个网络由两个全连接层组成,连接层之间采用ReLU进行激活,代码如下:

  1. def point_wise_feed_forward_network(d_model, dff):
  2. return tf.keras.Sequential([
  3. tf.keras.layers.Dense(dff, activation='relu'), # (batch_size, seq_len, dff)
  4. tf.keras.layers.Dense(d_model) # (batch_size, seq_len, d_model)
  5. ])

Transformer模型

有了以上的基础模块之后,我们就可以搭建整个transformer模型了。模型由编码器和解码器两大部分组成,如下图:

我们首先看左边的编码器部分,这个编码器由N个编码层顺序连接组成。第一个编码层接收最下方的输入,对于我们的例子来说,输入就是法语的句子,经过编码之后的向量。例如是一个[64, 32, 128]的向量,表示每个批次有64个句子,这个批次里面最长的句子包括了32个token,每个token被编码为128维的向量表达。这个输入向量加入位置编码信息之后,就是编码器的第一个编码层的输入了。

除了第一个编码层之外,其他编码层以上一个编码层的输出为输入。最后一个编码层的输出V,K作为解码器的输入。

再看一下右边的解码器部分,同样解码器也是由N个解码层顺序连接组成。每个解码层包括了两个multi-head attention(MHA)模块。第一个解码层接收最下方的输入,对于我们的例子来说,就是法语的句子对应的英语句子翻译,经过编码之后的向量。例如是一个[64, 48, 128]的向量,表示每个批次有64个句子,这个批次里面最长的句子包括了48个token,每个token被编码为128维的向量表达。这个输入向量加入位置编码信息之后,就是解码器的第一个编码层的输入了。这个输入经过第一个编码层的MHA处理之后,输出的值作为第二个MHA的Q值输入,第二个MHA的V,K输入是编码器的输出。最终这个解码层的输出结果作为第二个解码层的第一个MHA的输入,MHA的输出作为第二个MHA的Q值,V,K是编码器的输出,从而得到第二个解码层的输出。如此类推,直到第N个解码层处理完毕,把结果通过一个线性变化之后,通过Softmax计算预测的概率。

这里解码器的输入需要把对应的look head mask传入,以使得模型不会看到实际预测的单词。
例如我们输入一个法语句子,最终翻译的英语句子是"Tom is a boy",这个句子编码后是6个token,包含了[start]和[end]两个token. 对应的look ahead mask是一个6*6的矩阵。

编码器

编码器可以包括多个编码层,首先定义一个编码层,如以下代码

  1. class EncoderLayer(tf.keras.layers.Layer):
  2. def __init__(self,*, d_model, num_heads, dff, rate=0.1):
  3. super(EncoderLayer, self).__init__()
  4. self.mha = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
  5. self.ffn = point_wise_feed_forward_network(d_model, dff)
  6. self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  7. self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  8. self.dropout1 = tf.keras.layers.Dropout(rate)
  9. self.dropout2 = tf.keras.layers.Dropout(rate)
  10. def call(self, x, training, mask):
  11. attn_output, _ = self.mha(x, x, x, mask) # (batch_size, input_seq_len, d_model)
  12. attn_output = self.dropout1(attn_output, training=training)
  13. out1 = self.layernorm1(x + attn_output) # (batch_size, input_seq_len, d_model)
  14. ffn_output = self.ffn(out1) # (batch_size, input_seq_len, d_model)
  15. ffn_output = self.dropout2(ffn_output, training=training)
  16. out2 = self.layernorm2(out1 + ffn_output) # (batch_size, input_seq_len, d_model)
  17. return out2

定义编码器,这个编码器包括了以下3部分:

  • 输入的编码
  • 位置编码
  • 多个编码层

输入的句子的每个单词token化之后,根据token id查找对应的嵌入向量,然后根据token的位置添加位置编码信息,然后作为编码器的输入。编码器最后的输出,将作为解码器的输入。

  1. class Encoder(tf.keras.layers.Layer):
  2. def __init__(self,*, num_layers, d_model, num_heads, dff, input_vocab_size, rate=0.1):
  3. super(Encoder, self).__init__()
  4. self.d_model = d_model
  5. self.num_layers = num_layers
  6. self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
  7. self.pos_encoding = positional_encoding(MAX_TOKENS, self.d_model)
  8. self.enc_layers = [
  9. EncoderLayer(d_model=d_model, num_heads=num_heads, dff=dff, rate=rate)
  10. for _ in range(num_layers)]
  11. self.dropout = tf.keras.layers.Dropout(rate)
  12. def call(self, x, training, mask):
  13. seq_len = tf.shape(x)[1]
  14. # adding embedding and position encoding.
  15. x = self.embedding(x) # (batch_size, input_seq_len, d_model)
  16. x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
  17. x += self.pos_encoding[:, :seq_len, :]
  18. x = self.dropout(x, training=training)
  19. for i in range(self.num_layers):
  20. x = self.enc_layers[i](x, training, mask)
  21. return x # (batch_size, input_seq_len, d_model)

解码器

以下是解码层的代码

  1. class DecoderLayer(tf.keras.layers.Layer):
  2. def __init__(self,*, d_model, num_heads, dff, rate=0.1):
  3. super(DecoderLayer, self).__init__()
  4. self.mha1 = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
  5. self.mha2 = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
  6. self.ffn = point_wise_feed_forward_network(d_model, dff)
  7. self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  8. self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  9. self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  10. self.dropout1 = tf.keras.layers.Dropout(rate)
  11. self.dropout2 = tf.keras.layers.Dropout(rate)
  12. self.dropout3 = tf.keras.layers.Dropout(rate)
  13. def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
  14. # enc_output.shape == (batch_size, input_seq_len, d_model)
  15. attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask) # (batch_size, target_seq_len, d_model)
  16. attn1 = self.dropout1(attn1, training=training)
  17. out1 = self.layernorm1(attn1 + x)
  18. attn2, attn_weights_block2 = self.mha2(
  19. enc_output, enc_output, out1, padding_mask) # (batch_size, target_seq_len, d_model)
  20. attn2 = self.dropout2(attn2, training=training)
  21. out2 = self.layernorm2(attn2 + out1) # (batch_size, target_seq_len, d_model)
  22. ffn_output = self.ffn(out2) # (batch_size, target_seq_len, d_model)
  23. ffn_output = self.dropout3(ffn_output, training=training)
  24. out3 = self.layernorm3(ffn_output + out2) # (batch_size, target_seq_len, d_model)
  25. return out3, attn_weights_block1, attn_weights_block2

定义解码器

  1. class Decoder(tf.keras.layers.Layer):
  2. def __init__(self,*, num_layers, d_model, num_heads, dff, target_vocab_size,
  3. rate=0.1):
  4. super(Decoder, self).__init__()
  5. self.d_model = d_model
  6. self.num_layers = num_layers
  7. self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
  8. self.pos_encoding = positional_encoding(MAX_TOKENS, d_model)
  9. self.dec_layers = [
  10. DecoderLayer(d_model=d_model, num_heads=num_heads, dff=dff, rate=rate)
  11. for _ in range(num_layers)]
  12. self.dropout = tf.keras.layers.Dropout(rate)
  13. def call(self, x, enc_output, training,
  14. look_ahead_mask, padding_mask):
  15. seq_len = tf.shape(x)[1]
  16. attention_weights = {}
  17. x = self.embedding(x) # (batch_size, target_seq_len, d_model)
  18. x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
  19. x += self.pos_encoding[:, :seq_len, :]
  20. x = self.dropout(x, training=training)
  21. for i in range(self.num_layers):
  22. x, block1, block2 = self.dec_layers[i](x, enc_output, training, look_ahead_mask, padding_mask)
  23. attention_weights[f'decoder_layer{i+1}_block1'] = block1
  24. attention_weights[f'decoder_layer{i+1}_block2'] = block2
  25. # x.shape == (batch_size, target_seq_len, d_model)
  26. return x, attention_weights

组装模型

定义好了编码器和解码器之后,就可以组装整个模型了。

  1. class Transformer(tf.keras.Model):
  2. def __init__(self,*, num_layers, d_model, num_heads, dff, input_vocab_size,
  3. target_vocab_size, rate=0.1):
  4. super().__init__()
  5. self.encoder = Encoder(num_layers=num_layers, d_model=d_model,
  6. num_heads=num_heads, dff=dff,
  7. input_vocab_size=input_vocab_size, rate=rate)
  8. self.decoder = Decoder(num_layers=num_layers, d_model=d_model,
  9. num_heads=num_heads, dff=dff,
  10. target_vocab_size=target_vocab_size, rate=rate)
  11. self.final_layer = tf.keras.layers.Dense(target_vocab_size)
  12. def call(self, inputs, training):
  13. # Keras models prefer if you pass all your inputs in the first argument
  14. inp, tar = inputs
  15. padding_mask, look_ahead_mask = self.create_masks(inp, tar)
  16. enc_output = self.encoder(inp, training, padding_mask) # (batch_size, inp_seq_len, d_model)
  17. # dec_output.shape == (batch_size, tar_seq_len, d_model)
  18. dec_output, attention_weights = self.decoder(
  19. tar, enc_output, training, look_ahead_mask, padding_mask)
  20. final_output = self.final_layer(dec_output) # (batch_size, tar_seq_len, target_vocab_size)
  21. return final_output, attention_weights
  22. def create_masks(self, inp, tar):
  23. # Encoder padding mask (Used in the 2nd attention block in the decoder too.)
  24. padding_mask = create_padding_mask(inp)
  25. # Used in the 1st attention block in the decoder.
  26. # It is used to pad and mask future tokens in the input received by
  27. # the decoder.
  28. look_ahead_mask = create_look_ahead_mask(tf.shape(tar)[1])
  29. dec_target_padding_mask = create_padding_mask(tar)
  30. look_ahead_mask = tf.maximum(dec_target_padding_mask, look_ahead_mask)
  31. return padding_mask, look_ahead_mask

优化器定义

按照论文,Optimizer采用Adam算法,学习率按照以下公式来计算:

lrate=d_{model}^{-0.5}*min(step\_num^{-0.5}, step\_num*warmup\_steps^{-1.5})

  1. class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
  2. def __init__(self, d_model, warmup_steps=4000):
  3. super(CustomSchedule, self).__init__()
  4. self.d_model = d_model
  5. self.d_model = tf.cast(self.d_model, tf.float32)
  6. self.warmup_steps = warmup_steps
  7. def __call__(self, step):
  8. step = tf.cast(step, tf.float32)
  9. arg1 = tf.math.rsqrt(step)
  10. arg2 = step * (self.warmup_steps ** -1.5)
  11. return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
  12. learning_rate = CustomSchedule(d_model)
  13. optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98, epsilon=1e-9)

损失函数

模型的预测值是token的序号,可以理解为类别。因此采用类别的交叉熵来计算Loss值。以下代码定义了一个损失函数,以及一个计算模型准确率指标的函数。

  1. loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
  2. from_logits=True, reduction='none')
  3. def loss_function(real, pred):
  4. mask = tf.math.logical_not(tf.math.equal(real, 0))
  5. loss_ = loss_object(real, pred)
  6. mask = tf.cast(mask, dtype=loss_.dtype)
  7. loss_ *= mask
  8. return tf.reduce_sum(loss_)/tf.reduce_sum(mask)
  9. def accuracy_function(real, pred):
  10. accuracies = tf.equal(real, tf.argmax(pred, axis=2))
  11. mask = tf.math.logical_not(tf.math.equal(real, 0))
  12. accuracies = tf.math.logical_and(mask, accuracies)
  13. accuracies = tf.cast(accuracies, dtype=tf.float32)
  14. mask = tf.cast(mask, dtype=tf.float32)
  15. return tf.reduce_sum(accuracies)/tf.reduce_sum(mask)
  16. train_loss = tf.keras.metrics.Mean(name='train_loss')
  17. train_accuracy = tf.keras.metrics.Mean(name='train_accuracy')

模型训练

现在我们可以对模型进行训练了。我们的输入是法语和英语的句子对,经过token处理和向量化表达的数据。其中法语的数据作为编码器的输入,英语的数据分为tar_inp和tar_real两部分。tar_inp作为解码器的输入。tar_real作为模型训练的目标值,和模型输出的预测值作loss的计算。

例如英语的句子为'SOS A lion in the jungle is sleeping EOS',SOS和EOS分别表示开头和结束的特殊Token。那么tar_inp为'SOS A lion in the jungle is sleeping',tar_real为'A lion in the jungle is sleeping EOS'。可以理解为首先输入这个英语句子对应的法语句子到编码器,并且输入tar_inp的第一个token 'SOS'到解码器,我们预期模型应该能够翻译出第一个英语单词,把这个英语单词和tar_real的目标'A'相比较,计算loss。然后我们再输入tar_inp的头两个token'SOS A'到解码器,预期模型能翻译出第二个英语单词,计算这第二个英语单词和tar_real的目标'lion'的loss。如此类推直到tar_inp的最后一个token。在实际训练中,tar_inp和tar_real是一次全部传给模型的,结合look_ahead_mask就可以完成以上的训练过程。

首先我们实例化一个Transformer,如以下代码:

  1. input_vocab_size = 0
  2. target_vocab_size = 0
  3. with open('fr_vocab.txt', 'r') as f:
  4. input_vocab_size = len(f.readlines())
  5. with open('en_vocab.txt', 'r') as f:
  6. target_vocab_size = len(f.readlines())
  7. transformer = Transformer(
  8. num_layers=num_layers,
  9. d_model=d_model,
  10. num_heads=num_heads,
  11. dff=dff,
  12. input_vocab_size=input_vocab_size,
  13. target_vocab_size=target_vocab_size,
  14. rate=dropout_rate)

定义checkpoint在训练过程中保存模型:

  1. checkpoint_path = './checkpoints/train'
  2. #定义两个trackable object需要保存
  3. ckpt = tf.train.Checkpoint(transformer=transformer, optimizer=optimizer)
  4. ckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_path, max_to_keep=5)
  5. # if a checkpoint exists, restore the latest checkpoint.
  6. if ckpt_manager.latest_checkpoint:
  7. ckpt.restore(ckpt_manager.latest_checkpoint)
  8. print('Latest checkpoint restored!!')

定义一个训练函数:

  1. EPOCHS = 20
  2. # The @tf.function trace-compiles train_step into a TF graph for faster
  3. # execution. The function specializes to the precise shape of the argument
  4. # tensors. To avoid re-tracing due to the variable sequence lengths or variable
  5. # batch sizes (the last batch is smaller), use input_signature to specify
  6. # more generic shapes.
  7. train_step_signature = [
  8. tf.TensorSpec(shape=(None, None), dtype=tf.int64),
  9. tf.TensorSpec(shape=(None, None), dtype=tf.int64),
  10. ]
  11. @tf.function(input_signature=train_step_signature)
  12. def train_step(inp, tar):
  13. tar_inp = tar[:, :-1]
  14. tar_real = tar[:, 1:]
  15. print(tar_real)
  16. with tf.GradientTape() as tape:
  17. predictions, _ = transformer([inp, tar_inp], training = True)
  18. loss = loss_function(tar_real, predictions)
  19. gradients = tape.gradient(loss, transformer.trainable_variables)
  20. optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
  21. train_loss(loss)
  22. train_accuracy(accuracy_function(tar_real, predictions))

然后就可以开始训练了,在训练了20个回合后,准确率去到86.3%:

  1. for epoch in range(EPOCHS):
  2. start = time.time()
  3. train_loss.reset_states()
  4. train_accuracy.reset_states()
  5. # inp -> portuguese, tar -> english
  6. for (batch, (inp, tar)) in enumerate(train_batches):
  7. try:
  8. train_step(inp, tar)
  9. except ValueError:
  10. print(inp)
  11. print('-------')
  12. print(tar)
  13. if batch % 50 == 0:
  14. print(f'Epoch {epoch + 1} Batch {batch} Loss {train_loss.result():.4f} Accuracy {train_accuracy.result():.4f}')
  15. if (epoch + 1) % 5 == 0:
  16. ckpt_save_path = ckpt_manager.save()
  17. print(f'Saving checkpoint for epoch {epoch+1} at {ckpt_save_path}')
  18. print(f'Epoch {epoch + 1} Loss {train_loss.result():.4f} Accuracy {train_accuracy.result():.4f}')
  19. print(f'Time taken for 1 epoch: {time.time() - start:.2f} secs\n')

模型预测

模型训练完成后,就到了激动人心的时刻了。我们可以检验一下这个法语英语翻译器到底能否完成翻译任务呢。为此我们构建一个Translator的类,这个类在翻译的时候接收一个法语句子,在翻译前需要先添加上START, END这两个token,然后模型就会给出预测的英语Token,直到预测的TOKEN为END

  1. class Translator(tf.Module):
  2. START = tf.argmax(tf.constant(reserved_tokens) == "[START]")
  3. END = tf.argmax(tf.constant(reserved_tokens) == "[END]")
  4. def __init__(self, fr_tokenizer, en_tokenizer, transformer):
  5. self.fr_tokenizer = fr_tokenizer
  6. self.en_tokenizer = en_tokenizer
  7. self.transformer = transformer
  8. def _add_start_end(self, ragged):
  9. count = ragged.bounding_shape()[0]
  10. starts = tf.fill([count,1], START)
  11. ends = tf.fill([count,1], END)
  12. return tf.concat([starts, ragged, ends], axis=1)
  13. def __call__(self, sentence, max_length=MAX_TOKENS):
  14. # input sentence is french, hence adding the start and end token
  15. assert isinstance(sentence, tf.Tensor)
  16. if len(sentence.shape) == 0:
  17. sentence = sentence[tf.newaxis]
  18. #print(sentence)
  19. #print(self.fr_tokenizer.tokenize(sentence))
  20. #print(self.fr_tokenizer.tokenize(sentence).merge_dims(1,2))
  21. sentence = self._add_start_end(self.fr_tokenizer.tokenize(sentence).merge_dims(1,2)).to_tensor()
  22. encoder_input = sentence
  23. # As the output language is english, initialize the output with the
  24. # english start token.
  25. #start_end = self.en_tokenizer.tokenize([''])[0]
  26. start_end = self._add_start_end(en_tokenizer.tokenize(['']).merge_dims(1,2))[0]
  27. start = start_end[0][tf.newaxis]
  28. end = start_end[1][tf.newaxis]
  29. # `tf.TensorArray` is required here (instead of a python list) so that the
  30. # dynamic-loop can be traced by `tf.function`.
  31. output_array = tf.TensorArray(dtype=tf.int64, size=0, dynamic_size=True)
  32. output_array = output_array.write(0, start)
  33. for i in tf.range(max_length):
  34. output = tf.transpose(output_array.stack())
  35. predictions, _ = self.transformer([encoder_input, output], training=False)
  36. # select the last token from the seq_len dimension
  37. predictions = predictions[:, -1:, :] # (batch_size, 1, vocab_size)
  38. predicted_id = tf.argmax(predictions, axis=-1)
  39. # concatentate the predicted_id to the output which is given to the decoder
  40. # as its input.
  41. output_array = output_array.write(i+1, predicted_id[0])
  42. if predicted_id == end:
  43. break
  44. output = tf.transpose(output_array.stack())
  45. # output.shape (1, tokens)
  46. text = en_tokenizer.detokenize(output)[0] # shape: ()
  47. #tokens = en_tokenizer.lookup(output)[0]
  48. # `tf.function` prevents us from using the attention_weights that were
  49. # calculated on the last iteration of the loop. So recalculate them outside
  50. # the loop.
  51. _, attention_weights = self.transformer([encoder_input, output[:,:-1]], training=False)
  52. #return text, tokens, attention_weights
  53. return text, attention_weights
  54. translator = Translator(fr_tokenizer, en_tokenizer, transformer)

定义一个辅助函数,打印模型输入的法语句子,对应的英语句子和模型预测的英语句子:

  1. def print_translation(sentence, tokens, ground_truth):
  2. prediction_text = []
  3. tokens_numpy = tokens.numpy()
  4. for i in range(1, tokens_numpy.shape[0]-1):
  5. prediction_text.append(tokens_numpy[i].decode("utf-8"))
  6. prediction_text = ' '.join(prediction_text)
  7. print(f'{"Input:":15s}: {sentence}')
  8. print(f'{"Prediction":15s}: {prediction_text}')
  9. print(f'{"Ground truth":15s}: {ground_truth}')

 下面我们可以从验证集中选取几个法语句子来测试一下:

  1. sentence = "c’est une histoire tellement triste."
  2. ground_truth = "this is such a sad story."
  3. translated_text, attention_weights = translator(
  4. tf.constant(sentence))
  5. print_translation(sentence, translated_text, ground_truth)

输出如下:

  1. Input: : c’est une histoire tellement triste.
  2. Prediction : that ' s such a sad story .
  3. Ground truth : this is such a sad story.

然后我试一下随便输入一个法语句子,因为我不懂法语,只能先造一个英语句子,然后在谷歌翻译里面翻译为法语句子。

  1. sentence = "Ces pratiques sont essentiellement inefficaces et peuvent entraîner des risques pour la santé et la pollution de l'environnement."
  2. ground_truth = "These practices are essentially ineffective, and can cause health hazards and environmental pollution."
  3. translated_text, attention_weights = translator(
  4. tf.constant(sentence))
  5. print_translation(sentence, translated_text, ground_truth)

结果如下,可见翻译的不太准确,但是大概意思还是接近的,可见目前的训练集还不够大,如果有更多的数据,应该能提升模型的性能。

  1. Input: : Ces pratiques sont essentiellement inefficaces et peuvent entraîner des risques pour la santé et la pollution de l'environnement.
  2. Prediction : these practices are essentially invinivities and practicing health and pollution .
  3. Ground truth : These practices are essentially ineffective, and can cause health hazards and environmental pollution.

结论

通过对TensorFlow官网的transformer教程的学习,实现了一个法语翻译为英语的模型,下一步可以尝试一下中文翻译为英语,按照官网的介绍,中文,日语等语言的Token化的过程和英语法语不同,需要尝试另外一种token的方法,这个留待以后进一步研究。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/654787
推荐阅读
相关标签
  

闽ICP备14008679号