参考:淺談神經機器翻譯 & 用 Transformer 與 TensorFlow 2 英翻中 运行环境:Google Colab (也可以选择本地或服务器的 jupyter notebook,只是在线的 Colab 免费提供GPU) TensorFlow API:Modelu: tf | TensorFlow Core v2.5.0

1、环境设置

  1. import os
  2. import time
  3. import numpy as np
  4. import matplotlib as mpl
  5. import matplotlib.pyplot as plt
  6. from pprint import pprint
  7. from IPython.display import clear_output
  1. import tensorflow as tf
  2. import tensorflow_datasets as tfds
  3. print(tf.__version__)

输出 tensorflow 版本号:2.5.0

由于使用的是 Colab,将数据存储在 Google Drive,因此需要先挂载 Google Drive:

参考:Colab:在本地装载 Google 云端硬盘

  1. from google.colab import drive
  2. drive.mount('./mount')

定义一些之后存储各种数据时会用到的路径:

  1. output_dir = "mount/My Drive/Colab Notebooks/DLHLP20"
  2. en_vocab_file = os.path.join(output_dir, "en_vocab")
  3. zh_vocab_file = os.path.join(output_dir, "zh_vocab")
  4. checkpoint_path = os.path.join(output_dir, "checkpoints")
  5. log_dir = os.path.join(output_dir, 'logs')
  6. download_dir = "mount/My Drive/Colab Notebooks/DLHLP20/datasets"
  7. if not os.path.exists(output_dir):
  8. os.makedirs(output_dir)
  9. if not os.path.exists(download_dir):
  10. os.makedirs(download_dir)

2、数据处理 & 建立输入管道

通过 tf.data API 以及前面导入的 TensorFlow Datasets (import tensorflow_datasets as tfds) 来建立高效的输入管道,以在当前训练步骤完成之前就准备好下一个时间点 GPU 需要使用的数据,使得 GPU 并行计算能发挥最佳性能

2.1 下载并准备数据集

本实验最终让 Transformer 实现英翻中,使用的数据集是 机器翻译竞赛 WMT 2019 的中英数据集

先查看 tfdstfds API)中 WMT 2019 的中英翻译有哪些数据来源:

  1. tmp_builder = tfds.builder("wmt19_translate/zh-en")
  2. pprint(tmp_builder.subsets)

输出如下,包含多个来源的数据集

  1. {Split('train'): ['newscommentary_v14', // 新闻评论数据集
  2. 'wikititles_v1', // 维基百科标题数据集
  3. 'uncorpus_v1', // 联合国数据集
  4. 'casia2015',
  5. 'casict2011',
  6. 'casict2015',
  7. 'datum2015',
  8. 'datum2017',
  9. 'neu2017'],
  10. Split('validation'): ['newstest2018']}

为了节省 Transformer 的训练时间,只选择其中的新闻评论数据集当作训练数据集,即 newscommentary_v14

  1. config = tfds.translate.wmt.WmtConfig(
  2. version="0.0.3", # 注意这一行与参考博文不一样,否则会报错
  3. language_pair=("zh", "en"),
  4. subsets={
  5. tfds.Split.TRAIN: ["newscommentary_v14"]
  6. }
  7. )
  8. builder = tfds.builder("wmt_translate", config=config)
  9. builder.download_and_prepare(download_dir=download_dir)
  10. clear_output()

2.2 切割数据集

然只下载了一个新闻评论的数据集,但里面还是有超过30万对中英语句,为了减少训练时间,将此数据集切割成多个部分的 split ,20% 当作训练集,1% 当作验证集,剩下的 79% 数据不使用

参考博文使用 tfds.Split.TRAIN.subsplit 切割,但这个函数已经被官方移除,是过时的写法,会报错 修改方法参考:How to split a tensorflow dataset into train, test and validation in a Python script?

  1. train_perc = 20 # 训练集
  2. val_prec = 1 # 验证集
  3. drop_prec = 100 - train_perc - val_prec # 不用的数据
  4. split = ["train[:20%]","train[20%:21%]","train[21%:]"]
  5. split
  1. examples = builder.as_dataset(split=split, as_supervised=True)
  2. train_examples, val_examples, _ = examples
  3. print(train_examples) # 训练集
  4. print(val_examples) # 验证集

上面的 train_examplesval_examples 都已經是 tf.data.Dataset
先拿几笔数据出来看看:

  1. for en, zh in train_examples.take(3):
  2. print(en)
  3. print(zh)
  4. print('-' * 10)

输出如下,每一对 example (即每一次 take)都包含相同语义的中、英文两个句子,并且是以 unicode 呈现的 tf.Tensor

  1. tf.Tensor(b'The fear is real and visceral, and politicians ignore it at their peril.', shape=(), dtype=string)
  2. tf.Tensor(b'\xe8\xbf\x99\xe7\xa7\x8d\xe6\x81\x90\xe6\x83\xa7\xe6\x98\xaf\xe7\x9c\x9f\xe5\xae\x9e\xe8\x80\x8c\xe5\x86\x85\xe5\x9c\xa8\xe7\x9a\x84\xe3\x80\x82 \xe5\xbf\xbd\xe8\xa7\x86\xe5\xae\x83\xe7\x9a\x84\xe6\x94\xbf\xe6\xb2\xbb\xe5\xae\xb6\xe4\xbb\xac\xe5\x89\x8d\xe9\x80\x94\xe5\xa0\xaa\xe5\xbf\xa7\xe3\x80\x82', shape=(), dtype=string)
  3. ----------
  4. tf.Tensor(b'In fact, the German political landscape needs nothing more than a truly liberal party, in the US sense of the word \xe2\x80\x9cliberal\xe2\x80\x9d \xe2\x80\x93 a champion of the cause of individual freedom.', shape=(), dtype=string)
  5. tf.Tensor(b'\xe4\xba\x8b\xe5\xae\x9e\xe4\xb8\x8a\xef\xbc\x8c\xe5\xbe\xb7\xe5\x9b\xbd\xe6\x94\xbf\xe6\xb2\xbb\xe5\xb1\x80\xe5\x8a\xbf\xe9\x9c\x80\xe8\xa6\x81\xe7\x9a\x84\xe4\xb8\x8d\xe8\xbf\x87\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe7\xac\xa6\xe5\x90\x88\xe7\xbe\x8e\xe5\x9b\xbd\xe6\x89\x80\xe8\xb0\x93\xe2\x80\x9c\xe8\x87\xaa\xe7\x94\xb1\xe2\x80\x9d\xe5\xae\x9a\xe4\xb9\x89\xe7\x9a\x84\xe7\x9c\x9f\xe6\xad\xa3\xe7\x9a\x84\xe8\x87\xaa\xe7\x94\xb1\xe5\x85\x9a\xe6\xb4\xbe\xef\xbc\x8c\xe4\xb9\x9f\xe5\xb0\xb1\xe6\x98\xaf\xe4\xb8\xaa\xe4\xba\xba\xe8\x87\xaa\xe7\x94\xb1\xe4\xba\x8b\xe4\xb8\x9a\xe7\x9a\x84\xe5\x80\xa1\xe5\xaf\xbc\xe8\x80\x85\xe3\x80\x82', shape=(), dtype=string)
  6. ----------
  7. tf.Tensor(b'Shifting to renewable-energy sources will require enormous effort and major infrastructure investment.', shape=(), dtype=string)
  8. tf.Tensor(b'\xe5\xbf\x85\xe9\xa1\xbb\xe4\xbb\x98\xe5\x87\xba\xe5\xb7\xa8\xe5\xa4\xa7\xe7\x9a\x84\xe5\x8a\xaa\xe5\x8a\x9b\xe5\x92\x8c\xe5\x9f\xba\xe7\xa1\x80\xe8\xae\xbe\xe6\x96\xbd\xe6\x8a\x95\xe8\xb5\x84\xe6\x89\x8d\xe8\x83\xbd\xe5\xae\x8c\xe6\x88\x90\xe5\x90\x91\xe5\x8f\xaf\xe5\x86\x8d\xe7\x94\x9f\xe8\x83\xbd\xe6\xba\x90\xe7\x9a\x84\xe8\xbf\x87\xe6\xb8\xa1\xe3\x80\x82', shape=(), dtype=string)
  9. ----------

取 10 笔数据,将这些 Tensors 实际存储的字符串利用 numpy() 取出并解码查看:

  1. sample_examples = []
  2. num_samples = 10
  3. for en_t, zh_t in train_examples.take(num_samples):
  4. en = en_t.numpy().decode("utf-8")
  5. zh = zh_t.numpy().decode("utf-8")
  6. print(en)
  7. print(zh)
  8. print('-' * 10)
  9. # 之後用來簡單評估模型的訓練情況
  10. sample_examples.append((en, zh))

输出如下:

  1. The fear is real and visceral, and politicians ignore it at their peril.
  2. 这种恐惧是真实而内在的。 忽视它的政治家们前途堪忧。
  3. ----------
  4. In fact, the German political landscape needs nothing more than a truly liberal party, in the US sense of the word liberal a champion of the cause of individual freedom.
  5. 事实上,德国政治局势需要的不过是一个符合美国所谓“自由”定义的真正的自由党派,也就是个人自由事业的倡导者。
  6. ----------
  7. Shifting to renewable-energy sources will require enormous effort and major infrastructure investment.
  8. 必须付出巨大的努力和基础设施投资才能完成向可再生能源的过渡。
  9. ----------
  10. In this sense, it is critical to recognize the fundamental difference between urban villages and their rural counterparts.
  11. 在这方面,关键在于认识到“城市村落”和农村村落之间的根本区别。
  12. ----------
  13. A strong European voice, such as Nicolas Sarkozys during the French presidency of the EU, may make a difference, but only for six months, and at the cost of reinforcing other European countries nationalist feelings in reaction to the expression of Gallic pride.”
  14. 法国担任轮值主席国期间尼古拉·萨科奇统一的欧洲声音可能让人耳目一新,但这种声音却只持续了短短六个月,而且付出了让其他欧洲国家在面对“高卢人的骄傲”时民族主义情感进一步被激发的代价。
  15. ----------
  16. Most of Japans bondholders are nationals (if not the central bank) and have an interest in political stability.
  17. 日本债券持有人大多为本国国民(甚至中央银行 政治稳定符合他们的利益。
  18. ----------
  19. Paul Romer, one of the originators of new growth theory, has accused some leading names, including the Nobel laureate Robert Lucas, of what he calls mathiness using math to obfuscate rather than clarify.
  20. 新增长理论创始人之一的保罗·罗默(Paul Romer)也批评一些著名经济学家,包括诺贝尔奖获得者罗伯特·卢卡斯(Robert Lucas)在内,说他们“数学性 罗默的用语)太重,结果是让问题变得更加模糊而不是更加清晰。
  21. ----------
  22. It is, in fact, a capsule depiction of the United States Federal Reserve and the European Central Bank.
  23. 事实上,这就是对美联储和欧洲央行的简略描述。
  24. ----------
  25. Given these variables, the degree to which migration is affected by asylum-seekers will not be easy to predict or control.
  26. 考虑到这些变量,移民受寻求庇护者的影响程度很难预测或控制。
  27. ----------
  28. WASHINGTON, DC In the 2016 American presidential election, Hillary Clinton and Donald Trump agreed that the US economy is suffering from dilapidated infrastructure, and both called for greater investment in renovating and upgrading the countrys public capital stock.
  29. 华盛顿—在2016年美国总统选举中,希拉里·克林顿和唐纳德·特朗普都认为美国经济饱受基础设施陈旧的拖累,两人都要求加大投资用于修缮和升级美国公共资本存量。
  30. ----------

2.3 建立中文 & 英文字典

和大多数 NLP 案例相同,有了原始的中英文语句后,分别为其建立字典,来将每个词会转换成索引(index)。
tfds.features.text 底下的 SubwordTextEncoder 提供了非常方便的 API 让我们扫过整个训练集并建立字典

2.3.1 建立英文字典

首先为英文语料建立字典(为节省时间,如果之前已经建好字典并保存,直接读取即可):

注意:参考博文使用的是 tfds.features.text 底下的 SubwordTextEncoder 提供的 API,来扫过整个训练集并建立字典,但 tfds.features.text 已经被移除了,是过时的写法,会报错 修改方法:通过 tfds.deprecated.text 强制使用 参考:module ‘tensorflow_datasets.core.features’ has no attribute ‘text’

  1. %%time
  2. try:
  3. subword_encoder_en = tfds.deprecated.text.SubwordTextEncoder.load_from_file(en_vocab_file)
  4. print(f"載入已建立的字典: {en_vocab_file}")
  5. except:
  6. print("沒有已建立的字典,從頭建立。")
  7. subword_encoder_en = tfds.deprecated.text.SubwordTextEncoder.build_from_corpus(
  8. (en.numpy() for en, _ in train_examples),
  9. target_vocab_size=2**13) # 有需要可以调整字典大小
  10. # 存储字典以方便下次 warmstart
  11. subword_encoder_en.save_to_file(en_vocab_file)
  12. print(f"字典大小:{subword_encoder_en.vocab_size}")
  13. print(f"前 10 個 subwords:{subword_encoder_en.subwords[:10]}")
  14. print()

输出:

  1. 載入已建立的字典: mount/My Drive/Colab Notebooks/DLHLP20/en_vocab
  2. 字典大小:8113
  3. 10 subwords:[', ', 'the_', 'of_', 'to_', 'and_', 's_', 'in_', 'a_', 'is_', 'that_']
  4. CPU times: user 40.7 ms, sys: 948 µs, total: 41.6 ms
  5. Wall time: 387 ms

可以用上面建立的字典将一个英文句子转换成对应的索引序列:

  1. sample_string = 'Taiwan is beautiful.'
  2. indices = subword_encoder_en.encode(sample_string)
  3. indices

输出句子 'Taiwan is beautiful.' 对应的索引序列如下:

  1. [3461, 7889, 9, 3502, 4379, 1134, 7903]

再将上面输出的索引分别还原成对应的 tokens:

  1. print("{0:10}{1:6}".format("Index", "Subword"))
  2. print("-" * 15)
  3. for idx in indices:
  4. subword = subword_encoder_en.decode([idx])
  5. print('{0:5}{1:6}'.format(idx, ' ' * 5 + subword))

输出如下,可以看到 beautiful 被拆分成了 bea uti ful,即,当 subword tokenizer 遇到字典里没有的词汇,就将该词拆成多个子词(subwords),因此这种断词方法(wordpieces)不用担心有字典里没出现过的词汇

  1. Index Subword
  2. ---------------
  3. 3461 Taiwan
  4. 7889
  5. 9 is
  6. 3502 bea
  7. 4379 uti
  8. 1134 ful
  9. 7903 .

编码(词汇→索引)、解码(索引→词汇)是可逆的:

  1. sample_string = 'Taiwan is beautiful.'
  2. indices = subword_encoder_en.encode(sample_string)
  3. decoded_string = subword_encoder_en.decode(indices)
  4. assert decoded_string == sample_string
  5. pprint((sample_string, decoded_string))

输出:

  1. ('Taiwan is beautiful.', 'Taiwan is beautiful.')

2.3.2 建立中文字典

然后为中文也建立一个字典,注意下面的代码中令 max_subword_length=1 ,这样每个汉字会被视为一个单位(对应一个索引)。BERT 等模型处理中文时实际上以字作为 token 更为合适。

  1. %%time
  2. try:
  3. subword_encoder_zh = tfds.deprecated.text.SubwordTextEncoder.load_from_file(zh_vocab_file)
  4. print(f"載入已建立的字典: {zh_vocab_file}")
  5. except:
  6. print("沒有已建立的字典,從頭建立。")
  7. subword_encoder_zh = tfds.deprecated.text.SubwordTextEncoder.build_from_corpus(
  8. (zh.numpy() for _, zh in train_examples),
  9. target_vocab_size=2**13, # 有需要可以調整字典大小
  10. max_subword_length=1) # 每一個中文字就是字典裡的一個單位
  11. # 將字典檔案存下以方便下次 warmstart
  12. subword_encoder_zh.save_to_file(zh_vocab_file)
  13. print(f"字典大小:{subword_encoder_zh.vocab_size}")
  14. print(f"前 10 個 subwords:{subword_encoder_zh.subwords[:10]}")
  15. print()

输出:

  1. 載入已建立的字典: mount/My Drive/Colab Notebooks/DLHLP20/zh_vocab
  2. 字典大小:4205
  3. 10 subwords:['的', ',', '。', '国', '在', '是', '一', '和', '不', '这']
  4. CPU times: user 30.4 ms, sys: 5.13 ms, total: 35.5 ms
  5. Wall time: 350 ms

取一个中文句子测试一下:

  1. sample_string = sample_examples[0][1]
  2. indices = subword_encoder_zh.encode(sample_string)
  3. print(sample_string)
  4. print(indices)

输出如下:

  1. 这种恐惧是真实而内在的。 忽视它的政治家们前途堪忧。
  2. [10, 151, 574, 1298, 6, 374, 55, 29, 193, 5, 1, 3, 3981, 931, 431, 125, 1, 17, 124, 33, 20, 97, 1089, 1247, 861, 3]

用一个例子(example,指一组同语义的中英文语句)测试一下,分别将其转换成对应的索引序列

  1. en = "The eurozone’s collapse forces a major realignment of European politics."
  2. zh = "欧元区的瓦解强迫欧洲政治进行一次重大改组。"
  3. # 將文字轉成為 subword indices
  4. en_indices = subword_encoder_en.encode(en)
  5. zh_indices = subword_encoder_zh.encode(zh)
  6. print("[英中原文](轉換前)")
  7. print(en)
  8. print(zh)
  9. print()
  10. print('-' * 20)
  11. print()
  12. print("[英中序列](轉換後)")
  13. print(en_indices)
  14. print(zh_indices)

输出如下:

  1. [英中原文](轉換前)
  2. The eurozones collapse forces a major realignment of European politics.
  3. 欧元区的瓦解强迫欧洲政治进行一次重大改组。
  4. --------------------
  5. [英中序列](轉換後)
  6. [16, 900, 11, 6, 1527, 874, 8, 230, 2259, 2728, 239, 3, 89, 1236, 7903]
  7. [44, 202, 168, 1, 852, 201, 231, 592, 44, 87, 17, 124, 106, 38, 7, 279, 86, 18, 212, 265, 3]

2.4 前处理数据

2.4.1 BOS & EOS

为序列前后分别加上特殊的 token:BOSEOS,分别代表序列的开始和结束。

定义一个 encode(en_t, zh_t) 函数,输入是一笔同语义的中英文语句,输出是加上了 BOSEOS 后的字典序列

  1. def encode(en_t, zh_t):
  2. """
  3. :param en_t: 英文语句
  4. :param zh_t: 同语义的中文语句
  5. :return en_indices: 英文语句加上 BOS、EOS 后转换成的字典序列
  6. :return zh_indices: 中文语句加上 BOS、EOS 后转换成的字典序列
  7. """
  8. # 因為字典的索引從 0 開始,
  9. # 我們可以使用 subword_encoder_en.vocab_size 這個值作為 BOS 的索引值
  10. # 用 subword_encoder_en.vocab_size + 1 作為 EOS 的索引值
  11. en_indices = [subword_encoder_en.vocab_size] + subword_encoder_en.encode(
  12. en_t.numpy()) + [subword_encoder_en.vocab_size + 1]
  13. # 同理,不過是使用中文字典的最後一個索引 + 1
  14. zh_indices = [subword_encoder_zh.vocab_size] + subword_encoder_zh.encode(
  15. zh_t.numpy()) + [subword_encoder_zh.vocab_size + 1]
  16. return en_indices, zh_indices

从训练集中取一笔中英文 Tensors 查看上面这个函数的实际输出

  1. en_t, zh_t = next(iter(train_examples))
  2. en_indices, zh_indices = encode(en_t, zh_t)
  3. print('英文 BOS 的 index:', subword_encoder_en.vocab_size)
  4. print('英文 EOS 的 index:', subword_encoder_en.vocab_size + 1)
  5. print('中文 BOS 的 index:', subword_encoder_zh.vocab_size)
  6. print('中文 EOS 的 index:', subword_encoder_zh.vocab_size + 1)
  7. print('\n輸入為 2 個 Tensors:')
  8. pprint((en_t, zh_t))
  9. print('-' * 15)
  10. print('輸出為 2 個索引序列:')
  11. pprint((en_indices, zh_indices))

输出如下:

  1. 英文 BOS index 8113
  2. 英文 EOS index 8114
  3. 中文 BOS index 4205
  4. 中文 EOS index 4206
  5. 輸入為 2 Tensors
  6. (<tf.Tensor: shape=(), dtype=string, numpy=b'The fear is real and visceral, and politicians ignore it at their peril.'>,
  7. <tf.Tensor: shape=(), dtype=string, numpy=b'\xe8\xbf\x99\xe7\xa7\x8d\xe6\x81\x90\xe6\x83\xa7\xe6\x98\xaf\xe7\x9c\x9f\xe5\xae\x9e\xe8\x80\x8c\xe5\x86\x85\xe5\x9c\xa8\xe7\x9a\x84\xe3\x80\x82 \xe5\xbf\xbd\xe8\xa7\x86\xe5\xae\x83\xe7\x9a\x84\xe6\x94\xbf\xe6\xb2\xbb\xe5\xae\xb6\xe4\xbb\xac\xe5\x89\x8d\xe9\x80\x94\xe5\xa0\xaa\xe5\xbf\xa7\xe3\x80\x82'>)
  8. ---------------
  9. 輸出為 2 個索引序列:
  10. ([8113, 16, 1284, 9, 243, 5, 1275, 1756, 156, 1, 5, 1016, 5566, 21, 38, 33, 2982, 7965, 7903, 8114],
  11. [4205, 10, 151, 574, 1298, 6, 374, 55, 29, 193, 5, 1, 3, 3981, 931, 431, 125, 1, 17, 124, 33, 20, 97, 1089, 1247, 861, 3, 4206])

但还不能直接使用 train_dataset = train_examples.map(encode)encode 函数直接套用到整个训练集,因为 tf.data.Dataset.map 里的计算是在计算图模式下执行的,因此里面的 Tensors 没有 Eager Execution 下才有的 numpy 属性,需要先用 tf.py_function 将上面定义的 encode 函数包成一个以 eager 模式执行的 TensorFlow 操作,然后再套用到整个训练集中

  1. def tf_encode(en_t, zh_t):
  2. # 在 `tf_encode` 函式裡頭的 `en_t` 與 `zh_t` 都不是 Eager Tensors
  3. # 要到 `tf.py_funtion` 裡頭才是
  4. # 另外因為索引都是整數,所以使用 `tf.int64`
  5. return tf.py_function(encode, [en_t, zh_t], [tf.int64, tf.int64])
  6. # `tmp_dataset` 為說明用資料集,說明完所有重要的 func,
  7. # 我們會從頭建立一個正式的 `train_dataset`
  8. tmp_dataset = train_examples.map(tf_encode)
  9. en_indices, zh_indices = next(iter(tmp_dataset))
  10. print(en_indices)
  11. print(zh_indices)

输出:

  1. tf.Tensor(
  2. [8113 16 1284 9 243 5 1275 1756 156 1 5 1016 5566 21
  3. 38 33 2982 7965 7903 8114], shape=(20,), dtype=int64)
  4. tf.Tensor(
  5. [4205 10 151 574 1298 6 374 55 29 193 5 1 3 3981
  6. 931 431 125 1 17 124 33 20 97 1089 1247 861 3 4206], shape=(28,), dtype=int64)

2.4.2 过滤长序列

为了加快 Transformer 的训练,在实验中将长度超过 40 个 tokens 的序列都去掉:

  1. MAX_LENGTH = 40
  2. def filter_max_length(en, zh, max_length=MAX_LENGTH):
  3. # en, zh 分別代表英文與中文的索引序列
  4. return tf.logical_and(tf.size(en) <= max_length,
  5. tf.size(zh) <= max_length)
  6. # tf.data.Dataset.filter(func) 只會回傳 func 為真的例子
  7. tmp_dataset = tmp_dataset.filter(filter_max_length)

检查是否有序列超过设定的长度(40),顺便计算过滤掉长序列后剩余的训练集笔数:

  1. # 因為我們數據量小可以這樣 count
  2. num_examples = 0
  3. for en_indices, zh_indices in tmp_dataset:
  4. cond1 = len(en_indices) <= MAX_LENGTH
  5. cond2 = len(zh_indices) <= MAX_LENGTH
  6. assert cond1 and cond2
  7. num_examples += 1
  8. print(f"所有英文與中文序列長度都不超過 {MAX_LENGTH} 個 tokens")
  9. print(f"訓練資料集裡總共有 {num_examples} 筆數據")

输出如下,过滤掉长句子后仍有近 3 万笔数据,数据量还是足够的

  1. 所有英文與中文序列長度都不超過 40 tokens
  2. 訓練資料集裡總共有 29784 筆數據

2.4.3 填充至等长

经过上述操作后,每个例子中的索引序列长度还是不同的,这在建立 batch 时可能会有问题,因此使用 padded_batch 函数将每个 batch 中所有序列填充至等长

  1. # 使用 padded_batch 函式將 batch 裡的所有序列都 pad 到同樣長度
  2. BATCH_SIZE = 64
  3. tmp_dataset = tmp_dataset.padded_batch(BATCH_SIZE, padded_shapes=([-1], [-1]))
  4. en_batch, zh_batch = next(iter(tmp_dataset))
  5. print("英文索引序列的 batch")
  6. print(en_batch)
  7. print('-' * 20)
  8. print("中文索引序列的 batch")
  9. print(zh_batch)

输出如下,英文 batch 中最长序列长度为 39,就都填充到 39(未达到 39 的就补 0 填充到 39);中文 batch 中最长序列长度为 40,就都填充到 40

  1. 英文索引序列的 batch
  2. tf.Tensor(
  3. [[8113 16 1284 ... 0 0 0]
  4. [8113 1894 1302 ... 0 0 0]
  5. [8113 44 40 ... 0 0 0]
  6. ...
  7. [8113 122 506 ... 0 0 0]
  8. [8113 16 215 ... 0 0 0]
  9. [8113 7443 7889 ... 0 0 0]], shape=(64, 39), dtype=int64)
  10. --------------------
  11. 中文索引序列的 batch
  12. tf.Tensor(
  13. [[4205 10 151 ... 0 0 0]
  14. [4205 206 275 ... 0 0 0]
  15. [4205 5 10 ... 0 0 0]
  16. ...
  17. [4205 34 6 ... 0 0 0]
  18. [4205 317 256 ... 0 0 0]
  19. [4205 167 326 ... 0 0 0]], shape=(64, 40), dtype=int64)

2.4.4 建立训练集 & 验证集

上面介绍了一些建立训练集 & 验证集时要用到的前处理,现在开始从头建立训练集和验证集

  1. MAX_LENGTH = 40
  2. BATCH_SIZE = 128
  3. BUFFER_SIZE = 15000
  4. # 訓練集
  5. train_dataset = (train_examples # 輸出:(英文句子, 中文句子)
  6. .map(tf_encode) # 輸出:(英文索引序列, 中文索引序列)
  7. .filter(filter_max_length) # 同上,且序列長度都不超過 40
  8. .cache() # 加快讀取數據
  9. .shuffle(BUFFER_SIZE) # 將例子洗牌確保隨機性
  10. .padded_batch(BATCH_SIZE, # 將 batch 裡的序列都 pad 到一樣長度
  11. padded_shapes=([-1], [-1]))
  12. .prefetch(tf.data.experimental.AUTOTUNE)) # 加速
  13. # 驗證集
  14. val_dataset = (val_examples
  15. .map(tf_encode)
  16. .filter(filter_max_length)
  17. .padded_batch(BATCH_SIZE,
  18. padded_shapes=([-1], [-1])))

取一笔数据看看最终建立的数据集的样子:

  1. en_batch, zh_batch = next(iter(train_dataset))
  2. print("英文索引序列的 batch")
  3. print(en_batch)
  4. print('-' * 20)
  5. print("中文索引序列的 batch")
  6. print(zh_batch)

输出如下:

  1. 英文索引序列的 batch
  2. tf.Tensor(
  3. [[8113 41 233 ... 0 0 0]
  4. [8113 16 190 ... 0 0 0]
  5. [8113 3872 42 ... 0 0 0]
  6. ...
  7. [8113 435 7341 ... 0 0 0]
  8. [8113 3413 2088 ... 0 0 0]
  9. [8113 1560 1 ... 0 0 0]], shape=(128, 36), dtype=int64)
  10. --------------------
  11. 中文索引序列的 batch
  12. tf.Tensor(
  13. [[4205 34 17 ... 0 0 0]
  14. [4205 16 4 ... 0 0 0]
  15. [4205 200 77 ... 0 0 0]
  16. ...
  17. [4205 10 66 ... 0 0 0]
  18. [4205 104 25 ... 0 0 0]
  19. [4205 9 803 ... 0 0 0]], shape=(128, 40), dtype=int64)

至此,我们已建立了一个可供训练的输入管道。
训练集/验证集中:

  • 一次回传大小为 128 的 2 个 batch,分别包含 128 个英文索引序列和 128 个中文索引序列
  • 序列开头皆为 BOS 对应的索引,英文为 8113,中文为 4205
  • 中英文 batch 里的序列都被 padding 到等长,且不超过前面定义的最长序列长度 40

因此,之后每步训练拿出的数据 Tensors 的 shape 应为 (batch_size, seq_len),且其中的每个索引数字都代表一个中/英文 token(包含 BOS/EOS)

3、Transformer 相关的处理

3.1 输入数据 & 词嵌入

为了直观理解 Transformer,我们建立两对同语义的中英文句子,用来在后面的步骤中将其丢入 Transformer,观察 Transformer 对它们做了些什么转换。

3.1.1 输入数据

建立两对同语义的中英文句子:

  1. demo_examples = [
  2. ("It is important.", "这很重要。"),
  3. ("The numbers speak for themselves.", "数字证明了一切。"),
  4. ]
  5. pprint(demo_examples)

然后,对这两对中英句子做前处理,并以 Tensor 的方式读出:

  1. batch_size = 2
  2. demo_examples = tf.data.Dataset.from_tensor_slices((
  3. [en for en, _ in demo_examples], [zh for _, zh in demo_examples]
  4. ))
  5. # 將兩個句子透過之前定義的字典轉換成子詞的序列(sequence of subwords)
  6. # 並添加 padding token: <pad> 來確保 batch 裡的句子有一樣長度
  7. demo_dataset = demo_examples.map(tf_encode)\
  8. .padded_batch(batch_size, padded_shapes=([-1], [-1]))
  9. # 取出這個 demo dataset 裡唯一一個 batch
  10. inp, tar = next(iter(demo_dataset))
  11. print('inp:', inp) # shape=(2,8),代表英文句子数目为2,句子长度为8
  12. print('' * 10)
  13. print('tar:', tar) # shape=(2,10),代表英文句子数目为2,句子长度为10

输出如下,inp 的 shape = (2, 8),代表英文 batch 有 2 个句子,句子长度(tokens 数)为 8;tar 的 shape = (2, 10),代表中文 batch 有 2 个句子,句子长度(tokens 数)为 10

  1. inp: tf.Tensor(
  2. [[8113 103 9 1066 7903 8114 0 0]
  3. [8113 16 4111 6735 12 2750 7903 8114]], shape=(2, 8), dtype=int64)
  4. tar: tf.Tensor(
  5. [[4205 10 241 86 27 3 4206 0 0 0]
  6. [4205 165 489 398 191 14 7 560 3 4206]], shape=(2, 10), dtype=int64)

3.1.2 词嵌入

上面得到的输入(句子的索引序列)是二维的,在将索引序列丢入神经网络之前,一般会先做词嵌入(word embedding),将词汇从维度为字典大小的高维离散空间嵌入到低维的连续空间中。

为英文与中文分别建立一个词嵌入层,并对 inptar 做转换,将输入从二维向量转换为三维向量

  1. # + 2 是因為我們額外加了 <start> 以及 <end> tokens
  2. vocab_size_en = subword_encoder_en.vocab_size + 2
  3. vocab_size_zh = subword_encoder_zh.vocab_size + 2
  4. # 為了方便 demo, 將詞彙轉換到一個 4 維的詞嵌入空間
  5. d_model = 4
  6. embedding_layer_en = tf.keras.layers.Embedding(vocab_size_en, d_model)
  7. embedding_layer_zh = tf.keras.layers.Embedding(vocab_size_zh, d_model)
  8. emb_inp = embedding_layer_en(inp) # shape=(2, 8, 4),代表英文句子数即batch_size=2,每个句子8个词,每个词向量维度为4
  9. emb_tar = embedding_layer_zh(tar) # shape=(2, 10, 4),代表中文句子数即batch_size=2,每个句子10个词,每个词向量维度为4
  10. emb_inp, emb_tar

输出如下,做词嵌入转换后,emb_inp 的 shape = (2, 8, 4),代表英文 batch 有 2 个句子,句子长度(tokens 数)为 8,每个 token (词嵌入向量)的维度为 4;emb_tar 的 shape = (2, 10, 4),代表中文 batch 有 2 个句子,句子长度(tokens 数)为 10,每个 token (词嵌入向量)的维度为 4

  1. (<tf.Tensor: shape=(2, 8, 4), dtype=float32, numpy=
  2. array([[[ 0.0290383 , -0.04547672, -0.02772095, -0.03357754],
  3. [-0.00695816, -0.04078375, 0.02525837, -0.02481749],
  4. [-0.04623505, 0.04233763, -0.01499236, 0.0204999 ],
  5. [ 0.01926272, -0.00047588, -0.04174998, -0.03272629],
  6. [-0.02661264, -0.01885304, -0.04105211, 0.04283339],
  7. [-0.03520732, -0.04360742, 0.02240748, 0.043366 ],
  8. [-0.00359789, 0.03168226, -0.04263718, 0.02017691],
  9. [-0.00359789, 0.03168226, -0.04263718, 0.02017691]],
  10. [[ 0.0290383 , -0.04547672, -0.02772095, -0.03357754],
  11. [ 0.0057646 , 0.01873441, 0.04519582, 0.01169586],
  12. [-0.04670626, -0.0461443 , -0.03423715, 0.04910291],
  13. [-0.0080081 , -0.0066364 , -0.01258793, -0.0427192 ],
  14. [ 0.03887614, -0.03308231, 0.00964315, 0.04348907],
  15. [-0.04985246, -0.04806296, 0.03991742, -0.00247025],
  16. [-0.02661264, -0.01885304, -0.04105211, 0.04283339],
  17. [-0.03520732, -0.04360742, 0.02240748, 0.043366 ]]],
  18. dtype=float32)>, <tf.Tensor: shape=(2, 10, 4), dtype=float32, numpy=
  19. array([[[-9.4237924e-03, -1.8982053e-02, -3.8755499e-02, 1.6131904e-02],
  20. [ 1.3820972e-02, -3.2755092e-02, 1.0215558e-02, 2.3236815e-02],
  21. [ 5.3795800e-03, 2.7922321e-02, 4.9203541e-02, 5.4208413e-03],
  22. [ 3.0345544e-03, -3.4656405e-02, -2.3234559e-02, 3.9151311e-03],
  23. [ 4.8759926e-02, 4.2193059e-02, -2.9141665e-02, 4.5896284e-03],
  24. [-4.5878422e-02, 4.3194380e-02, -4.8125375e-02, -2.7835155e-02],
  25. [-1.0285042e-02, 5.3374879e-03, 4.0048312e-02, 1.6815785e-02],
  26. [-5.7560802e-03, -4.3076027e-02, 3.9412268e-03, -3.4347549e-03],
  27. [-5.7560802e-03, -4.3076027e-02, 3.9412268e-03, -3.4347549e-03],
  28. [-5.7560802e-03, -4.3076027e-02, 3.9412268e-03, -3.4347549e-03]],
  29. [[-9.4237924e-03, -1.8982053e-02, -3.8755499e-02, 1.6131904e-02],
  30. [ 4.1677501e-02, 6.7135915e-03, 3.7391197e-02, -3.8386367e-02],
  31. [-2.9780090e-02, -3.5157301e-02, 2.0691562e-02, 3.0919526e-02],
  32. [ 2.7362112e-02, -1.5102543e-02, 1.0358501e-02, 4.9035549e-03],
  33. [ 3.9686177e-02, 4.7571074e-02, -4.3680418e-02, -9.9581480e-04],
  34. [ 7.1074963e-03, -1.7719496e-02, -7.9342239e-03, -3.0051971e-02],
  35. [-1.1939298e-02, -3.7533417e-03, 2.2292137e-05, 4.3857586e-02],
  36. [-2.3507465e-02, -3.2441415e-02, 1.8460218e-02, -4.7260523e-02],
  37. [-4.5878422e-02, 4.3194380e-02, -4.8125375e-02, -2.7835155e-02],
  38. [-1.0285042e-02, 5.3374879e-03, 4.0048312e-02, 1.6815785e-02]]],
  39. dtype=float32)>)

理解了上面的 3 维张量后,就能明白 emb_tar 的第一个中文句子的倒数 3 行为什么长得是一样的,因为它们对应的 token 都是填充的 0

  1. print("tar[0]:", tar[0][-3:])
  2. print("-" * 20)
  3. print("emb_tar[0]:", emb_tar[0][-3:])

输出如下:

  1. tar[0]: tf.Tensor([0 0 0], shape=(3,), dtype=int64)
  2. --------------------
  3. emb_tar[0]: tf.Tensor(
  4. [[-0.00575608 -0.04307603 0.00394123 -0.00343475]
  5. [-0.00575608 -0.04307603 0.00394123 -0.00343475]
  6. [-0.00575608 -0.04307603 0.00394123 -0.00343475]], shape=(3, 4), dtype=float32)

3.2 MASK 遮罩

Transformer 使用遮罩机制,使得在进行自注意力机制时不至于看到不该看的
Transformer 中有两种遮罩(mask)

  • padding mask:将序列中填充 0 (padding)的部分遮盖住,使得 Transformer 不会关注到这部分位置,而只关注实际的序列内容
  • look ahead mask:确保 Decoder 在进行自注意力机制时,只关注当前 token 之前就已经产生的 tokens,而避免关注到 Decoder 未来才会生成的 tokens

对于两种遮罩,mask 矩阵都是将需要遮蔽的位置的值设为 1。

3.2.1 padding mask

创建 padding mask 矩阵:

  1. # padding mask
  2. def create_padding_mask(seq):
  3. # padding mask 的工作就是把索引序列中為 0 的位置設為 1
  4. mask = tf.cast(tf.equal(seq, 0), tf.float32)
  5. return mask[:, tf.newaxis, tf.newaxis, :] # broadcasting
  6. inp_mask = create_padding_mask(inp) # 得到四维张量
  7. inp_mask

输出如下,得到的是四维张量,因为在中间加了两个维度以便之后做 broadcasting。

具体可以看后面多头注意力的部分,在 padding mask 加入两个新维度分别是:

  • 一个是用来遮住同个句子但不同头的注意力权重
  • 一个是用来 broadcast 到 2 维注意力权重的
  1. <tf.Tensor: shape=(2, 1, 1, 8), dtype=float32, numpy=
  2. array([[[[0., 0., 0., 0., 0., 0., 1., 1.]]],
  3. [[[0., 0., 0., 0., 0., 0., 0., 0.]]]], dtype=float32)>

先将 inp_mask 遮罩的额外维度去掉,以方便和 inp 比较:

  1. print("inp:", inp)
  2. print("-" * 20)
  3. print("tf.squeeze(inp_mask):", tf.squeeze(inp_mask))

输出如下,可以看到 inp_mask 就是将 inp 中为 0 的对应位置设为 1 凸显出来,这样后续的程序就知道应该把那些地方遮盖住

  1. inp: tf.Tensor(
  2. [[8113 103 9 1066 7903 8114 0 0]
  3. [8113 16 4111 6735 12 2750 7903 8114]], shape=(2, 8), dtype=int64)
  4. --------------------
  5. tf.squeeze(inp_mask): tf.Tensor(
  6. [[0. 0. 0. 0. 0. 0. 1. 1.]
  7. [0. 0. 0. 0. 0. 0. 0. 0.]], shape=(2, 8), dtype=float32)

3.2.2 look ahead mask

创建 look ahead mask 矩阵,以遮住 Decoder 未来生成的 tokens 不让之前的 token 关注:

  1. # 建立一個 2 維矩陣,維度為 (size, size),
  2. # 其遮罩為一個右上角的三角形
  3. def create_look_ahead_mask(size):
  4. mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
  5. return mask # (seq_len, seq_len)
  6. seq_len = emb_tar.shape[1] # 注意這次我們用中文的詞嵌入張量 `emb_tar`
  7. look_ahead_mask = create_look_ahead_mask(seq_len)
  8. print("emb_tar:", emb_tar)
  9. print("-" * 20)
  10. print("look_ahead_mask", look_ahead_mask)

输出如下,look ahead 遮罩就是产生一个 2 维矩阵,其两个维度都和中文的词嵌入张量 emb_tar 的倒数第 2 个维度(序列长度)一样,且矩阵中元素 1 呈右上三角

  1. emb_tar: tf.Tensor(
  2. [[[-9.4237924e-03 -1.8982053e-02 -3.8755499e-02 1.6131904e-02]
  3. [ 1.3820972e-02 -3.2755092e-02 1.0215558e-02 2.3236815e-02]
  4. [ 5.3795800e-03 2.7922321e-02 4.9203541e-02 5.4208413e-03]
  5. [ 3.0345544e-03 -3.4656405e-02 -2.3234559e-02 3.9151311e-03]
  6. [ 4.8759926e-02 4.2193059e-02 -2.9141665e-02 4.5896284e-03]
  7. [-4.5878422e-02 4.3194380e-02 -4.8125375e-02 -2.7835155e-02]
  8. [-1.0285042e-02 5.3374879e-03 4.0048312e-02 1.6815785e-02]
  9. [-5.7560802e-03 -4.3076027e-02 3.9412268e-03 -3.4347549e-03]
  10. [-5.7560802e-03 -4.3076027e-02 3.9412268e-03 -3.4347549e-03]
  11. [-5.7560802e-03 -4.3076027e-02 3.9412268e-03 -3.4347549e-03]]
  12. [[-9.4237924e-03 -1.8982053e-02 -3.8755499e-02 1.6131904e-02]
  13. [ 4.1677501e-02 6.7135915e-03 3.7391197e-02 -3.8386367e-02]
  14. [-2.9780090e-02 -3.5157301e-02 2.0691562e-02 3.0919526e-02]
  15. [ 2.7362112e-02 -1.5102543e-02 1.0358501e-02 4.9035549e-03]
  16. [ 3.9686177e-02 4.7571074e-02 -4.3680418e-02 -9.9581480e-04]
  17. [ 7.1074963e-03 -1.7719496e-02 -7.9342239e-03 -3.0051971e-02]
  18. [-1.1939298e-02 -3.7533417e-03 2.2292137e-05 4.3857586e-02]
  19. [-2.3507465e-02 -3.2441415e-02 1.8460218e-02 -4.7260523e-02]
  20. [-4.5878422e-02 4.3194380e-02 -4.8125375e-02 -2.7835155e-02]
  21. [-1.0285042e-02 5.3374879e-03 4.0048312e-02 1.6815785e-02]]], shape=(2, 10, 4), dtype=float32)
  22. --------------------
  23. look_ahead_mask tf.Tensor(
  24. [[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  25. [0. 0. 1. 1. 1. 1. 1. 1. 1. 1.]
  26. [0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
  27. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  28. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  29. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  30. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  31. [0. 0. 0. 0. 0. 0. 0. 0. 1. 1.]
  32. [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
  33. [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]], shape=(10, 10), dtype=float32)

3.3 Scaled dot product attention

之前的文章中讲过,Transformer 中使用的自注意力机制和注意力机制实际上是一回事,因此在编码中也是用同一个注意力函数实现。
此外,Transformer 中,注意力机制计算注意力打分函数时,用的是缩放点积模型(Scaled dot product)
image.png
上图是缩放点积注意力机制的计算图和示意图,公式为 Transformer 的 TensorFlow2 实现:英翻中 - 图2.

  • MatMul:先将维度相同的 Q 和 K 做点积
  • Scale:然后除以一个缩放因子 Transformer 的 TensorFlow2 实现:英翻中 - 图3。这是为了避免 Q、K 点积的值由于 Q、K 的维度 Transformer 的 TensorFlow2 实现:英翻中 - 图4 太大而跟着太大,而太大的点积值丢入 softmax 后可能是其梯度变得很小,导致训练结果不理想
    • 生成的 scaled_attention_logitsshape=(batch_size, seq_len_q, seq_len_k) ,每一行代表序列 q 的某个 token 对序列 k 中每个 token 的注意力权重
  • Mask 遮罩(可选):避免将注意力放在没有实际语义(填充 0)的地方
  • Softmax:再丢入 softmax 函数中得到相加和为 1 的注意力权重
  • MatMul:最后将注意力权重对 V 做加权平均

缩放点积注意力的实现:

  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: 注意力机制的结果,是每个 token 的新的 representation
  15. attention_weights: 注意力权重矩阵
  16. """
  17. # 將 `q`、 `k` 做點積再 scale
  18. matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)
  19. dk = tf.cast(tf.shape(k)[-1], tf.float32) # 取得 seq_k 的序列長度
  20. scaled_attention_logits = matmul_qk / tf.math.sqrt(dk) # scale by sqrt(dk)
  21. # scaled_attention_logits 的 shape=(batch_size, seq_len_q, seq_len_k)
  22. # 最后一个维度代表序列 q 里的某个 token 与序列 k 中每个 token 的匹配程度,但加和还不为1(后面做 softmax 后和为 1)
  23. # 將遮罩「加」到被丟入 softmax 前的 logits
  24. # 將遮罩乘上一個接近負無窮大的 -1e9, 這樣可以讓這些被加上極大負值的位置變得無關緊要,在經過 softmax 以後的值趨近於 0
  25. if mask is not None:
  26. scaled_attention_logits += (mask * -1e9)
  27. # 对最后一个维度做 softmax 是為了得到總和為 1 的比例之後對 `v` 做加權平均
  28. attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k)
  29. # 以注意權重對 v 做加權平均(weighted average)
  30. output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v)
  31. return output, attention_weights

示例 1:这里拿已经被转换成词嵌入空间得英文张量 emb_inp 同时充当 Q 和 K,自己与自己做匹配(self-attention);随机生成一个二元张量作为 V

  1. # 設定一個 seed 確保我們每次都拿到一樣的隨機結果
  2. tf.random.set_seed(9527)
  3. # 自注意力機制:查詢 `q` 跟鍵值 `k` 都是 `emb_inp`
  4. q = emb_inp
  5. k = emb_inp
  6. # 随机產生一個跟 `emb_inp` 同樣 shape 的 binary vector
  7. v = tf.cast(tf.math.greater(tf.random.uniform(shape=emb_inp.shape), 0.5), tf.float32)
  8. v

输出如下:

  1. <tf.Tensor: shape=(2, 8, 4), dtype=float32, numpy=
  2. array([[[1., 0., 0., 0.],
  3. [0., 1., 0., 1.],
  4. [0., 0., 0., 1.],
  5. [1., 0., 1., 0.],
  6. [1., 0., 1., 0.],
  7. [0., 1., 0., 1.],
  8. [0., 0., 1., 0.],
  9. [0., 1., 0., 1.]],
  10. [[1., 0., 1., 1.],
  11. [1., 0., 1., 0.],
  12. [1., 0., 0., 0.],
  13. [1., 0., 1., 0.],
  14. [0., 1., 0., 1.],
  15. [1., 1., 1., 1.],
  16. [0., 0., 0., 0.],
  17. [0., 0., 1., 0.]]], dtype=float32)>

假设没有遮罩,将上面的 Q,K,V 输入到注意力函数,查看输出结果

  1. mask = None
  2. output, attention_weights = scaled_dot_product_attention(q, k, v, mask)
  3. print("output:", output) # 注意力机制的结果
  4. print("-" * 20)
  5. print("attention_weights:", attention_weights) # 句子 q 里每个 token 对句子 k 里每个 token 的注意权重

输出如下

  1. output: tf.Tensor(
  2. [[[0.3754064 0.37491858 0.37504062 0.49967808]
  3. [0.375093 0.3751777 0.37480363 0.5000596 ]
  4. [0.37471822 0.37491643 0.37509933 0.5001621 ]
  5. [0.37528062 0.37477034 0.37516797 0.49968264]
  6. [0.3749827 0.3749506 0.3751282 0.4999781 ]
  7. [0.37483385 0.3752799 0.3748202 0.5002752 ]
  8. [0.3749682 0.37477815 0.37523592 0.4998837 ]
  9. [0.3749682 0.37477815 0.37523592 0.4998837 ]]
  10. [[0.62511265 0.24998146 0.62504023 0.37525356]
  11. [0.62497264 0.2501231 0.6251355 0.37500358]
  12. [0.62471056 0.24997216 0.6245305 0.37481806]
  13. [0.6252279 0.24993078 0.6251918 0.3750667 ]
  14. [0.6247138 0.2501474 0.6247551 0.37513638]
  15. [0.62503874 0.25015905 0.62513757 0.37502298]
  16. [0.62474614 0.24991831 0.6245631 0.37481573]
  17. [0.6247693 0.2501465 0.6248009 0.37493715]]], shape=(2, 8, 4), dtype=float32)
  18. --------------------
  19. attention_weights: tf.Tensor(
  20. [[[0.12528133 0.12509221 0.1247595 0.12515798 0.12496709 0.12491082
  21. 0.12491555 0.12491555]
  22. [0.12513678 0.12521061 0.12488186 0.12500277 0.12495347 0.12511972
  23. 0.12484738 0.12484738]
  24. [0.12473849 0.12481638 0.1252457 0.12489983 0.1250799 0.12498043
  25. 0.12511961 0.12511961]
  26. [0.12514941 0.12494969 0.12491229 0.1251712 0.12496004 0.12478393
  27. 0.12503672 0.12503672]
  28. [0.12489372 0.12483564 0.12502751 0.12489522 0.12519377 0.12507571
  29. 0.12503922 0.12503922]
  30. [0.1249046 0.12506895 0.12499527 0.12478628 0.12514298 0.12532
  31. 0.12489095 0.12489095]
  32. [0.12488046 0.12476789 0.12510554 0.12501018 0.12507756 0.12486209
  33. 0.12514816 0.12514816]
  34. [0.12488046 0.12476789 0.12510554 0.12501018 0.12507756 0.12486209
  35. 0.12514816 0.12514816]]
  36. [[0.1252721 0.12482584 0.12497401 0.12508717 0.12502795 0.12495351
  37. 0.12495787 0.12490161]
  38. [0.12488046 0.1251864 0.1248944 0.1249486 0.12506035 0.12506276
  39. 0.12490975 0.1250573 ]
  40. [0.12484588 0.12471178 0.12533696 0.12478167 0.1249379 0.12503427
  41. 0.12519464 0.12515691]
  42. [0.12513591 0.12494262 0.1249584 0.12515086 0.12489066 0.12504013
  43. 0.12495913 0.12492236]
  44. [0.12498897 0.12496667 0.12502712 0.12480308 0.12521943 0.12492798
  45. 0.12499836 0.12506838]
  46. [0.12486393 0.12491844 0.12507285 0.12490181 0.12487736 0.12528169
  47. 0.12491225 0.1251717 ]
  48. [0.12489744 0.12479473 0.12526251 0.12485005 0.12497689 0.12494142
  49. 0.12519751 0.12507945]
  50. [0.12479065 0.12489156 0.12517406 0.12476277 0.12499625 0.12515025
  51. 0.1250288 0.12520567]]], shape=(2, 8, 8), dtype=float32)

示例 2:再为英文语句产生对应的 padding mask:

  1. def create_padding_mask(seq):
  2. # padding mask 的工作就是把索引序列中為 0 的位置設為 1
  3. mask = tf.cast(tf.equal(seq, 0), tf.float32)
  4. return mask[:, tf.newaxis, tf.newaxis, :] # broadcasting
  5. print("inp:", inp)
  6. inp_mask = create_padding_mask(inp)
  7. print("-" * 20)
  8. print("inp_mask:", inp_mask)

输出如下:

  1. inp: tf.Tensor(
  2. [[8113 103 9 1066 7903 8114 0 0]
  3. [8113 16 4111 6735 12 2750 7903 8114]], shape=(2, 8), dtype=int64)
  4. --------------------
  5. inp_mask: tf.Tensor(
  6. [[[[0. 0. 0. 0. 0. 0. 1. 1.]]]
  7. [[[0. 0. 0. 0. 0. 0. 0. 0.]]]], shape=(2, 1, 1, 8), dtype=float32)

inp_mask 降到 3 维,并和示例 1 的 q、k、v 一起丢入注意力函数中,查看注意力权重变化:

  1. mask = tf.squeeze(inp_mask, axis=1) # (batch_size, 1, seq_len_q), 這次把 inp_mask 降到 3 維
  2. _, attention_weights = scaled_dot_product_attention(q, k, v, mask)
  3. print("attention_weights:", attention_weights)

输出如下,可以看到由于第一个英文句子的倒数两个位置为 0(填充),因此每个 token 针对倒数两个 token 的注意力权重的值都为 0

  1. attention_weights: tf.Tensor(
  2. [[[0.16700415 0.16675204 0.16630854 0.16683973 0.16658527 0.16651024
  3. 0. 0. ]
  4. [0.16678117 0.16687958 0.16644143 0.16660257 0.16653687 0.16675843
  5. 0. 0. ]
  6. [0.16637106 0.16647494 0.16704756 0.16658624 0.16682641 0.16669376
  7. 0. 0. ]
  8. [0.16688222 0.1666159 0.16656603 0.16691127 0.1666297 0.16639486
  9. 0. 0. ]
  10. [0.16654237 0.16646492 0.16672078 0.16654438 0.16694249 0.16678506
  11. 0. 0. ]
  12. [0.16649106 0.16671014 0.16661192 0.16633335 0.16680881 0.16704477
  13. 0. 0. ]
  14. [0.16657309 0.16642293 0.16687332 0.16674611 0.16683598 0.16654858
  15. 0. 0. ]
  16. [0.16657309 0.16642293 0.16687332 0.16674611 0.16683598 0.16654858
  17. 0. 0. ]]
  18. [[0.1252721 0.12482584 0.12497401 0.12508717 0.12502795 0.12495351
  19. 0.12495787 0.12490161]
  20. [0.12488046 0.1251864 0.1248944 0.1249486 0.12506035 0.12506276
  21. 0.12490975 0.1250573 ]
  22. [0.12484588 0.12471178 0.12533696 0.12478167 0.1249379 0.12503427
  23. 0.12519464 0.12515691]
  24. [0.12513591 0.12494262 0.1249584 0.12515086 0.12489066 0.12504013
  25. 0.12495913 0.12492236]
  26. [0.12498897 0.12496667 0.12502712 0.12480308 0.12521943 0.12492798
  27. 0.12499836 0.12506838]
  28. [0.12486393 0.12491844 0.12507285 0.12490181 0.12487736 0.12528169
  29. 0.12491225 0.1251717 ]
  30. [0.12489744 0.12479473 0.12526251 0.12485005 0.12497689 0.12494142
  31. 0.12519751 0.12507945]
  32. [0.12479065 0.12489156 0.12517406 0.12476277 0.12499625 0.12515025
  33. 0.1250288 0.12520567]]], shape=(2, 8, 8), dtype=float32)

可以把针对最后两个位置的注意力权重拿出来,方便比较:

  1. attention_weights[:, :, -2:]

输出如下:

  1. <tf.Tensor: shape=(2, 8, 2), dtype=float32, numpy=
  2. array([[[0. , 0. ],
  3. [0. , 0. ],
  4. [0. , 0. ],
  5. [0. , 0. ],
  6. [0. , 0. ],
  7. [0. , 0. ],
  8. [0. , 0. ],
  9. [0. , 0. ]],
  10. [[0.12495787, 0.12490161],
  11. [0.12490975, 0.1250573 ],
  12. [0.12519464, 0.12515691],
  13. [0.12495913, 0.12492236],
  14. [0.12499836, 0.12506838],
  15. [0.12491225, 0.1251717 ],
  16. [0.12519751, 0.12507945],
  17. [0.1250288 , 0.12520567]]], dtype=float32)>

示例 3:用中文张量 emb_tar 同时充当 Q 和 K,来模拟 Decoder 自注意力机制的的处理情况,再随机生成 V,和 look ahead mask 一起输入注意力函数:

  • 建立 look ahead mask: ```python

    建立一個 2 維矩陣,維度為 (size, size),

    其遮罩為一個右上角的三角形

    def create_look_ahead_mask(size): mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0) return mask # (seq_len, seq_len)

seq_len = emb_tar.shape[1] # 注意這次我們用中文的詞嵌入張量 emb_tar look_ahead_mask = create_look_ahead_mask(seq_len) print(“emb_tar:”, emb_tar) print(“-“ * 20) print(“look_ahead_mask”, look_ahead_mask)

  1. - QKV mask 一起输入到注意力函数:
  2. ```python
  3. # 讓我們用目標語言(中文)的 batch
  4. # 來模擬 Decoder 處理的情況
  5. temp_q = temp_k = emb_tar
  6. temp_v = tf.cast(tf.math.greater(
  7. tf.random.uniform(shape=emb_tar.shape), 0.5), tf.float32)
  8. # 將 look_ahead_mask 放入注意函式
  9. _, attention_weights = scaled_dot_product_attention(
  10. temp_q, temp_k, temp_v, look_ahead_mask)
  11. print("attention_weights:", attention_weights)

输出如下,可以看到 look ahead mask 就是让每个 token 只关注序列中包含在自己左侧的 tokens,而不去看自己右侧(后面位置)的tokens

  1. attention_weights: tf.Tensor(
  2. [[[1. 0. 0. 0. 0. 0.
  3. 0. 0. 0. 0. ]
  4. [0.49982026 0.5001797 0. 0. 0. 0.
  5. 0. 0. 0. 0. ]
  6. [0.33289742 0.33326188 0.3338407 0. 0. 0.
  7. 0. 0. 0. 0. ]
  8. [0.25012672 0.25005642 0.24966863 0.2501483 0. 0.
  9. 0. 0. 0. 0. ]
  10. [0.19992453 0.19984037 0.19993336 0.19986834 0.2004335 0.
  11. 0. 0. 0. 0. ]
  12. [0.16670248 0.16635145 0.1664869 0.16656455 0.16668846 0.1672061
  13. 0. 0. 0. 0. ]
  14. [0.14277314 0.14289941 0.14301896 0.14278772 0.14276735 0.14274402
  15. 0.14300935 0. 0. 0. ]
  16. [0.12503995 0.12507923 0.12493233 0.12508415 0.12485924 0.12489284
  17. 0.12499405 0.12511827 0. 0. ]
  18. [0.11113493 0.11116984 0.11103928 0.11117421 0.11097432 0.11100418
  19. 0.11109413 0.11120454 0.11120454 0. ]
  20. [0.10001303 0.10004445 0.09992696 0.10004838 0.0998685 0.09989537
  21. 0.09997632 0.10007568 0.10007568 0.10007568]]
  22. [[1. 0. 0. 0. 0. 0.
  23. 0. 0. 0. 0. ]
  24. [0.49909472 0.5009053 0. 0. 0. 0.
  25. 0. 0. 0. 0. ]
  26. [0.33331496 0.3328927 0.3337923 0. 0. 0.
  27. 0. 0. 0. 0. ]
  28. [0.24989659 0.25008804 0.24994354 0.2500718 0. 0.
  29. 0. 0. 0. 0. ]
  30. [0.19998682 0.19998467 0.1995684 0.19993785 0.20052221 0.
  31. 0. 0. 0. 0. ]
  32. [0.16664901 0.16672754 0.16658452 0.16666073 0.16662599 0.16675225
  33. 0. 0. 0. 0. ]
  34. [0.1429154 0.14269434 0.14298356 0.14284788 0.14280201 0.14275636
  35. 0.14300044 0. 0. 0. ]
  36. [0.12494281 0.12506452 0.12503041 0.12497071 0.1247808 0.12508796
  37. 0.12487852 0.12524427 0. 0. ]
  38. [0.11114997 0.1109622 0.11098131 0.11095168 0.11122421 0.11109988
  39. 0.1110464 0.11109862 0.11148576 0. ]
  40. [0.09994049 0.1000277 0.10007813 0.1000115 0.09990876 0.09995521
  41. 0.10004681 0.1000054 0.09992012 0.10010584]]], shape=(2, 10, 10), dtype=float32)

可以看到两个中文句子的第一个 token 都只关注自己:

  1. attention_weights[:, 0, :]

输出:

  1. <tf.Tensor: shape=(2, 10), dtype=float32, numpy=
  2. array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
  3. [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=float32)>

而两个句子的第二个 token 都只关注第一个 token 及其自己(第二个 token),因此前两个位置的注意力权重和为 1,而后面的权重皆为 0:

  1. attention_weights[:, 1, :]

输出:

  1. <tf.Tensor: shape=(2, 10), dtype=float32, numpy=
  2. array([[0.49982026, 0.5001797 , 0. , 0. , 0. ,
  3. 0. , 0. , 0. , 0. , 0. ],
  4. [0.49909472, 0.5009053 , 0. , 0. , 0. ,
  5. 0. , 0. , 0. , 0. , 0. ]],
  6. dtype=float32)>

3.4 多头注意力

多头注意力就是将 Q、K、V 这三个张量先分别转换到 d_model 维空间,再分别将其拆成 N 个较低维度的 depth 维的 q、k、v,再将 q、k、v 分别丢入注意力函数得到 N 个头对应的结果,再将这 N 个头的结果穿起来,通过一次线性转换,就能得到多头注意力的输出(不同的头关注各 tokens 在不同子空间下的 representation)。

3.4.1 拆分多头

为了实现多头注意力,将一个头变成 **num_heads** 个头,实际上就是将 **d_model** 维度的向量拆成 **num_heads****depth** 维向量,使得 **num_heads * depth = d_model**

  1. def split_heads(x, d_model, num_heads):
  2. # x.shape: (batch_size, seq_len, d_model)
  3. batch_size = tf.shape(x)[0]
  4. # 我們要確保維度 `d_model` 可以被平分成 `num_heads` 個 `depth` 維度
  5. assert d_model % num_heads == 0
  6. depth = d_model // num_heads # 這是分成多頭以後每個向量的維度
  7. # 將最後一個 d_model 維度分成 num_heads 個 depth 維度。
  8. # 最後一個維度變成兩個維度,張量 x 從 3 維到 4 維
  9. # (batch_size, seq_len, num_heads, depth)
  10. reshaped_x = tf.reshape(x, shape=(batch_size, -1, num_heads, depth))
  11. # 將 head 的維度拉前使得最後兩個維度為子詞以及其對應的 depth 向量
  12. # (batch_size, num_heads, seq_len, depth)
  13. output = tf.transpose(reshaped_x, perm=[0, 2, 1, 3])
  14. return output
  15. # 我們的 `emb_inp` 裡頭的子詞本來就是 4 維的詞嵌入向量
  16. d_model = 4
  17. # 將 4 維詞嵌入向量分為 2 個 head 的 2 維矩陣
  18. num_heads = 2
  19. x = emb_inp
  20. output = split_heads(x, d_model, num_heads)
  21. print("x:", x)
  22. print("output:", output)

输出如下。观察 outputemb_inp(即 x)之间的关系,可以看到 3 维的 emb_inp 的最后一个维度 shape[-1] = 4 被拆成了两半,从而被转换成一个四维张量了。也就是序列中每个 token 原本为 **d_model** 维的 representation 被平均拆成 num_heads**depth** 维度的 representation。而每个 head 的二维矩阵仍代表原来的序列,只是序列中 token 的 representation 维度降低了。

  1. x: tf.Tensor(
  2. [[[ 0.0290383 -0.04547672 -0.02772095 -0.03357754]
  3. [-0.00695816 -0.04078375 0.02525837 -0.02481749]
  4. [-0.04623505 0.04233763 -0.01499236 0.0204999 ]
  5. [ 0.01926272 -0.00047588 -0.04174998 -0.03272629]
  6. [-0.02661264 -0.01885304 -0.04105211 0.04283339]
  7. [-0.03520732 -0.04360742 0.02240748 0.043366 ]
  8. [-0.00359789 0.03168226 -0.04263718 0.02017691]
  9. [-0.00359789 0.03168226 -0.04263718 0.02017691]]
  10. [[ 0.0290383 -0.04547672 -0.02772095 -0.03357754]
  11. [ 0.0057646 0.01873441 0.04519582 0.01169586]
  12. [-0.04670626 -0.0461443 -0.03423715 0.04910291]
  13. [-0.0080081 -0.0066364 -0.01258793 -0.0427192 ]
  14. [ 0.03887614 -0.03308231 0.00964315 0.04348907]
  15. [-0.04985246 -0.04806296 0.03991742 -0.00247025]
  16. [-0.02661264 -0.01885304 -0.04105211 0.04283339]
  17. [-0.03520732 -0.04360742 0.02240748 0.043366 ]]], shape=(2, 8, 4), dtype=float32)
  18. output: tf.Tensor(
  19. [[[[ 0.0290383 -0.04547672]
  20. [-0.00695816 -0.04078375]
  21. [-0.04623505 0.04233763]
  22. [ 0.01926272 -0.00047588]
  23. [-0.02661264 -0.01885304]
  24. [-0.03520732 -0.04360742]
  25. [-0.00359789 0.03168226]
  26. [-0.00359789 0.03168226]]
  27. [[-0.02772095 -0.03357754]
  28. [ 0.02525837 -0.02481749]
  29. [-0.01499236 0.0204999 ]
  30. [-0.04174998 -0.03272629]
  31. [-0.04105211 0.04283339]
  32. [ 0.02240748 0.043366 ]
  33. [-0.04263718 0.02017691]
  34. [-0.04263718 0.02017691]]]
  35. [[[ 0.0290383 -0.04547672]
  36. [ 0.0057646 0.01873441]
  37. [-0.04670626 -0.0461443 ]
  38. [-0.0080081 -0.0066364 ]
  39. [ 0.03887614 -0.03308231]
  40. [-0.04985246 -0.04806296]
  41. [-0.02661264 -0.01885304]
  42. [-0.03520732 -0.04360742]]
  43. [[-0.02772095 -0.03357754]
  44. [ 0.04519582 0.01169586]
  45. [-0.03423715 0.04910291]
  46. [-0.01258793 -0.0427192 ]
  47. [ 0.00964315 0.04348907]
  48. [ 0.03991742 -0.00247025]
  49. [-0.04105211 0.04283339]
  50. [ 0.02240748 0.043366 ]]]], shape=(2, 2, 8, 2), dtype=float32)

3.4.2 多头注意力层的实现

  • 总之,在 q、k、v 的最后一位已经是 d_model 的情况下,多头注意力和缩放点积一样,就是输出一个完全一样维度的输出张量
  • 多头注意力的输出张量 output 中每个句子的每个 token 的 representation 的维度虽然和输入张量相同,都是 d_model,但实际上已经变得包含了整个序列的语义资讯(如果是自注意力,那么就是从同个序列中不同位置且不同空间中的 representation 获得语义资讯的结果)
  1. # 實作一個執行多頭注意力機制的 keras layer
  2. # 在初始的時候指定輸出維度 `d_model` & `num_heads,
  3. # 在呼叫的時候輸入 `v`, `k`, `q` 以及 `mask`
  4. # 輸出跟 scaled_dot_product_attention 函式一樣有兩個:
  5. # output.shape == (batch_size, seq_len_q, d_model)
  6. # attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
  7. class MultiHeadAttention(tf.keras.layers.Layer):
  8. # 在初始的時候建立一些必要參數
  9. def __init__(self, d_model, num_heads):
  10. super(MultiHeadAttention, self).__init__()
  11. self.num_heads = num_heads # 指定要將 `d_model` 拆成幾個 heads
  12. self.d_model = d_model # 在 split_heads 之前的基底維度
  13. assert d_model % self.num_heads == 0 # 前面看過,要確保可以平分
  14. self.depth = d_model // self.num_heads # 每個 head 裡子詞的新的 repr. 維度
  15. self.wq = tf.keras.layers.Dense(d_model) # 分別給 q, k, v 的 3 個線性轉換
  16. self.wk = tf.keras.layers.Dense(d_model) # 注意我們並沒有指定 activation func
  17. self.wv = tf.keras.layers.Dense(d_model)
  18. self.dense = tf.keras.layers.Dense(d_model) # 多 heads 串接後通過的線性轉換
  19. # 這跟我們前面看過的函式有 87% 相似
  20. def split_heads(self, x, batch_size):
  21. """Split the last dimension into (num_heads, depth).
  22. Transpose the result such that the shape is (batch_size, num_heads, seq_len, depth)
  23. """
  24. x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
  25. return tf.transpose(x, perm=[0, 2, 1, 3])
  26. # multi-head attention 的實際執行流程,注意參數順序(這邊跟論文以及 TensorFlow 官方教學一致)
  27. def call(self, v, k, q, mask):
  28. """
  29. return:
  30. output: 序列中每个 token 的新的 representation,都包含序列其他位置的资讯
  31. attention_weights: 包含每个头的每个序列 q 中的 token 对 序列 k 各个 tokens 的注意力权重
  32. """
  33. batch_size = tf.shape(q)[0]
  34. # 將輸入的 q, k, v 都各自做一次線性轉換到 `d_model` 維空間
  35. q = self.wq(q) # (batch_size, seq_len, d_model)
  36. k = self.wk(k) # (batch_size, seq_len, d_model)
  37. v = self.wv(v) # (batch_size, seq_len, d_model)
  38. # 前面看過的,將最後一個 `d_model` 維度分成 `num_heads` 個 `depth` 維度
  39. q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
  40. k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
  41. v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)
  42. # 利用 broadcasting 讓每個句子的每個 head 的 qi, ki, vi 都各自進行注意力機制
  43. # 輸出會多一個 head 維度
  44. scaled_attention, attention_weights = scaled_dot_product_attention(
  45. q, k, v, mask)
  46. # scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth)
  47. # attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
  48. # 跟我們在 `split_heads` 函式做的事情剛好相反,先做 transpose 再做 reshape
  49. # 將 `num_heads` 個 `depth` 維度串接回原來的 `d_model` 維度
  50. scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])
  51. # (batch_size, seq_len_q, num_heads, depth)
  52. concat_attention = tf.reshape(scaled_attention,
  53. (batch_size, -1, self.d_model))
  54. # (batch_size, seq_len_q, d_model)
  55. # 最后通過一個線性轉換得到多头注意力的输出
  56. output = self.dense(concat_attention) # (batch_size, seq_len_q, d_model)
  57. return output, attention_weights

接下来初始化一个多头注意力层,并将英文词嵌入向量 emb_inp 输入进去看看:

  1. # emb_inp.shape == (batch_size, seq_len, d_model)
  2. # == (2, 8, 4)
  3. assert d_model == emb_inp.shape[-1] == 4
  4. num_heads = 2
  5. print(f"d_model: {d_model}")
  6. print(f"num_heads: {num_heads}\n")
  7. # 初始化一個 multi-head attention layer
  8. mha = MultiHeadAttention(d_model, num_heads)
  9. # 簡單將 v, k, q 都設置為 `emb_inp`
  10. # 順便看看 padding mask 的作用。
  11. # 別忘記,第一個英文序列的最後兩個 tokens 是 <pad>
  12. v = k = q = emb_inp
  13. padding_mask = create_padding_mask(inp)
  14. print("q.shape:", q.shape)
  15. print("k.shape:", k.shape)
  16. print("v.shape:", v.shape)
  17. print("padding_mask.shape:", padding_mask.shape)
  18. output, attention_weights = mha(v, k, q, mask)
  19. print("output.shape:", output.shape)
  20. print("attention_weights.shape:", attention_weights.shape)
  21. print("\noutput:", output)

输出如下

  1. d_model: 4
  2. num_heads: 2
  3. q.shape: (2, 8, 4)
  4. k.shape: (2, 8, 4)
  5. v.shape: (2, 8, 4)
  6. padding_mask.shape: (2, 1, 1, 8)
  7. output.shape: (2, 8, 4)
  8. attention_weights.shape: (2, 2, 8, 8)
  9. output: tf.Tensor(
  10. [[[ 0.00729086 -0.01088331 -0.02139376 -0.01110373]
  11. [ 0.00729388 -0.01088877 -0.02138113 -0.01107032]
  12. [ 0.00728141 -0.01087296 -0.02137667 -0.01108264]
  13. [ 0.00727768 -0.01086754 -0.02138589 -0.01110905]
  14. [ 0.00730402 -0.01089834 -0.02140144 -0.01109898]
  15. [ 0.00731609 -0.01091389 -0.02139529 -0.01106422]
  16. [ 0.00728705 -0.01087741 -0.02138997 -0.01110259]
  17. [ 0.00728705 -0.01087741 -0.02138997 -0.01110259]]
  18. [[-0.01179662 0.01222272 -0.00268441 -0.01616674]
  19. [-0.01177946 0.01221635 -0.00266444 -0.01613695]
  20. [-0.01177551 0.01221563 -0.00265255 -0.01613718]
  21. [-0.01181528 0.01224418 -0.00265591 -0.01615041]
  22. [-0.01175137 0.01218743 -0.00269742 -0.01615706]
  23. [-0.01179021 0.01222605 -0.00263447 -0.01611209]
  24. [-0.01177933 0.01221838 -0.00266229 -0.01614963]
  25. [-0.01176683 0.01220703 -0.00265213 -0.01612293]]], shape=(2, 8, 4), dtype=float32)

4、Transformer 的结构

Transformer 架构

Transformer 的 TensorFlow2 实现:英翻中 - 图5

4.1 Position-wise Feed-Forward Networks(FFN)

Encoder layer 和 Decoder layer 里都有 Feed Forward 子层,其中包含一组全连接层:

  • 输入张量最后一个维度为 d_model
  • 中间层维度为 dff,论文中为 2048
  • 输出张量的最后一个维度为 d_model ,论文中为 512
    1. # 建立 Transformer 裡 Encoder / Decoder layer 都有使用到的 Feed Forward 元件
    2. def point_wise_feed_forward_network(d_model, dff):
    3. # 此 FFN 對輸入做兩個線性轉換,中間加了一個 ReLU activation func
    4. return tf.keras.Sequential([
    5. tf.keras.layers.Dense(dff, activation='relu'), # (batch_size, seq_len, dff)
    6. tf.keras.layers.Dense(d_model) # (batch_size, seq_len, d_model)
    7. ])

尝试建立一个 FNN:

  1. batch_size = 64
  2. seq_len = 10
  3. d_model = 512
  4. dff = 2048
  5. x = tf.random.uniform((batch_size, seq_len, d_model))
  6. ffn = point_wise_feed_forward_network(d_model, dff)
  7. out = ffn(x)
  8. print("x.shape:", x.shape)
  9. print("out.shape:", out.shape)

输出如下,可以看到 FNN 的输出张量的维度和输入一模一样

  1. x.shape: (64, 10, 512) // 输入:(batch_size, seq_len, d_model)
  2. out.shape: (64, 10, 512) // 输出:(batch_size, seq_len, d_model)

这个 FNN 实际上对序列中所有位置做的线性转换都是一样的

  1. d_model = 4 # FFN 的輸入輸出張量的最後一維皆為 `d_model`
  2. dff = 6
  3. # 建立一個小 FFN
  4. small_ffn = point_wise_feed_forward_network(d_model, dff)
  5. # 懂子詞梗的站出來
  6. # 假想一个 2 维的 dummy_sentence,里面有 5 个以 4 维向量表示的 tokens
  7. dummy_sentence = tf.constant([[5, 5, 6, 6],
  8. [5, 5, 6, 6],
  9. [9, 5, 2, 7],
  10. [9, 5, 2, 7],
  11. [9, 5, 2, 7]], dtype=tf.float32)
  12. small_ffn(dummy_sentence)

输出如下,可以看到同一个 token 不会因为位置的改变而造成 FNN 的输出结构产生差异。

  1. <tf.Tensor: shape=(5, 4), dtype=float32, numpy=
  2. array([[ 2.8674245 , -2.174698 , -1.3073453 , -6.4233937 ],
  3. [ 2.8674245 , -2.174698 , -1.3073453 , -6.4233937 ],
  4. [ 3.6502066 , -0.97325826, -2.4126563 , -6.509499 ],
  5. [ 3.6502066 , -0.97325826, -2.4126563 , -6.509499 ],
  6. [ 3.6502066 , -0.97325826, -2.4126563 , -6.509499 ]],
  7. dtype=float32)>

但尽管 FNN 对所有位置的 tokens 都做一样的转换,但这个转换是独立进行的,因此称为 Position-wise Feed-Forward Networks

4.2 Multi-Head Attention(MHA)

3.4.2 多头注意力层的实现

4.3 Encoder Layer

每个 Encoder Layer 包含两个 sub layers:

每个 sub layer 之后做 dropout
每个 sub layer 还包括 Add & Norm:

  • Add 残差连接:帮助减缓梯度消失问题
  • Norm:层归一化

因此,Encoder Layer 内部的每个 sub layer 的处理逻辑如下:

  1. sub_layer_out = Sublayer(x) # Sublayer 可以是 MHA or FFN
  2. sub_layer_out = Dropout(sub_layer_out)
  3. out = LayerNorm(x + sub_layer_out)

Encoder Layer 的具体实现如下:

  1. # Encoder 裡頭會有 N 個 EncoderLayers,而每個 EncoderLayer 裡又有兩個 sub-layers: MHA & FFN
  2. class EncoderLayer(tf.keras.layers.Layer):
  3. # Transformer 論文內預設 dropout rate 為 0.1
  4. def __init__(self, d_model, num_heads, dff, rate=0.1):
  5. super(EncoderLayer, self).__init__()
  6. self.mha = MultiHeadAttention(d_model, num_heads)
  7. self.ffn = point_wise_feed_forward_network(d_model, dff)
  8. # layer norm 很常在 RNN-based 的模型被使用。一個 sub-layer 一個 layer norm
  9. self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  10. self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  11. # 一樣,一個 sub-layer 一個 dropout layer
  12. self.dropout1 = tf.keras.layers.Dropout(rate)
  13. self.dropout2 = tf.keras.layers.Dropout(rate)
  14. # 需要丟入 `training` 參數是因為 dropout 在訓練以及測試的行為有所不同
  15. def call(self, x, training, mask):
  16. # 除了 `attn`,其他張量的 shape 皆為 (batch_size, input_seq_len, d_model)
  17. # attn.shape == (batch_size, num_heads, input_seq_len, input_seq_len)
  18. # sub-layer 1: MHA
  19. # Encoder 利用注意機制關注自己當前的序列,因此 v, k, q 全部都是自己
  20. # 另外別忘了我們還需要 padding mask 來遮住輸入序列中的 <pad> token
  21. attn_output, attn = self.mha(x, x, x, mask)
  22. attn_output = self.dropout1(attn_output, training=training)
  23. out1 = self.layernorm1(x + attn_output)
  24. # sub-layer 2: FFN
  25. ffn_output = self.ffn(out1)
  26. ffn_output = self.dropout2(ffn_output, training=training) # 記得 training
  27. out2 = self.layernorm2(out1 + ffn_output)
  28. return out2

这里实验示例,为了减少计算量,将 d_model 设为 4,num_heads 设为 2,FNN 的中间层神经元数 dff 设为 8

  1. # 之後可以調的超參數。這邊為了 demo 設小一點
  2. d_model = 4
  3. num_heads = 2
  4. dff = 8
  5. # 新建一個使用上述參數的 Encoder Layer
  6. enc_layer = EncoderLayer(d_model, num_heads, dff)
  7. padding_mask = create_padding_mask(inp) # 建立一個當前輸入 batch 使用的 padding mask
  8. enc_out = enc_layer(emb_inp, training=False, mask=padding_mask) # (batch_size, seq_len, d_model)
  9. print("inp:", inp)
  10. print("-" * 20)
  11. print("padding_mask:", padding_mask)
  12. print("-" * 20)
  13. print("emb_inp:", emb_inp)
  14. print("-" * 20)
  15. print("enc_out:", enc_out)
  16. assert emb_inp.shape == enc_out.shape

输出如下,可以看到 Encoder Layer 的输出张量的维度和输入张量的维度相同: (batch_size, seq_len, d_model)。当然,通过内部的 MHA 和 FNN sub layer 的转换,每个 token 的 representation 肯定都大幅改变了

  1. inp: tf.Tensor(
  2. [[8113 103 9 1066 7903 8114 0 0]
  3. [8113 16 4111 6735 12 2750 7903 8114]], shape=(2, 8), dtype=int64)
  4. --------------------
  5. padding_mask: tf.Tensor(
  6. [[[[0. 0. 0. 0. 0. 0. 1. 1.]]]
  7. [[[0. 0. 0. 0. 0. 0. 0. 0.]]]], shape=(2, 1, 1, 8), dtype=float32)
  8. --------------------
  9. emb_inp: tf.Tensor(
  10. [[[ 0.0290383 -0.04547672 -0.02772095 -0.03357754]
  11. [-0.00695816 -0.04078375 0.02525837 -0.02481749]
  12. [-0.04623505 0.04233763 -0.01499236 0.0204999 ]
  13. [ 0.01926272 -0.00047588 -0.04174998 -0.03272629]
  14. [-0.02661264 -0.01885304 -0.04105211 0.04283339]
  15. [-0.03520732 -0.04360742 0.02240748 0.043366 ]
  16. [-0.00359789 0.03168226 -0.04263718 0.02017691]
  17. [-0.00359789 0.03168226 -0.04263718 0.02017691]]
  18. [[ 0.0290383 -0.04547672 -0.02772095 -0.03357754]
  19. [ 0.0057646 0.01873441 0.04519582 0.01169586]
  20. [-0.04670626 -0.0461443 -0.03423715 0.04910291]
  21. [-0.0080081 -0.0066364 -0.01258793 -0.0427192 ]
  22. [ 0.03887614 -0.03308231 0.00964315 0.04348907]
  23. [-0.04985246 -0.04806296 0.03991742 -0.00247025]
  24. [-0.02661264 -0.01885304 -0.04105211 0.04283339]
  25. [-0.03520732 -0.04360742 0.02240748 0.043366 ]]], shape=(2, 8, 4), dtype=float32)
  26. --------------------
  27. enc_out: tf.Tensor(
  28. [[[ 1.623388 -0.39707196 -1.090135 -0.13618113]
  29. [-0.32270592 -0.59458977 1.7079673 -0.7906716 ]
  30. [-1.3702308 1.3467281 -0.38100302 0.40450567]
  31. [ 0.2299977 1.241566 -1.5490661 0.0775026 ]
  32. [-1.0866984 0.88808054 -0.9033548 1.1019727 ]
  33. [-1.1917038 -0.5005804 0.17631385 1.5159702 ]
  34. [-0.90351737 1.4291942 -0.97096586 0.44528902]
  35. [-0.90351737 1.4291942 -0.97096586 0.44528902]]
  36. [[ 1.458591 -0.4763808 -1.2537323 0.27152228]
  37. [-1.6820657 0.9371168 0.45904127 0.28590757]
  38. [-1.1009151 0.28546828 -0.68177414 1.497221 ]
  39. [-0.3456568 1.5769095 -1.1794896 -0.05176307]
  40. [ 0.6062109 -1.1472129 -0.7711475 1.3121496 ]
  41. [-1.5314417 -0.24402368 0.9851706 0.7902947 ]
  42. [-0.99772143 0.8376673 -0.9900316 1.1500859 ]
  43. [-1.133343 -0.44576913 -0.00731844 1.5864305 ]]], shape=(2, 8, 4), dtype=float32)

4.4 Decoder Layer

每个 Decoder Layer 包含三个 sub layers:

因此,Decoder Layer 内部的每个 sub layer 的处理逻辑如下:

  1. sub_layer_out = Sublayer(x) # Sublayer 可以是 MHA1、MHA1 or FNN
  2. sub_layer_out = Dropout(sub_layer_out)
  3. out = LayerNorm(x + sub_layer_out)

Decoder Layer 的具体实现如下:

  1. # Decoder 裡頭會有 N 個 DecoderLayer,
  2. # 而 DecoderLayer 又有三個 sub-layers: 自注意的 MHA, 關注 Encoder 輸出的 MHA & FFN
  3. class DecoderLayer(tf.keras.layers.Layer):
  4. def __init__(self, d_model, num_heads, dff, rate=0.1):
  5. super(DecoderLayer, self).__init__()
  6. # 3 個 sub-layers 的主角們
  7. self.mha1 = MultiHeadAttention(d_model, num_heads)
  8. self.mha2 = MultiHeadAttention(d_model, num_heads)
  9. self.ffn = point_wise_feed_forward_network(d_model, dff)
  10. # 定義每個 sub-layer 用的 LayerNorm
  11. self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  12. self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  13. self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
  14. # 定義每個 sub-layer 用的 Dropout
  15. self.dropout1 = tf.keras.layers.Dropout(rate)
  16. self.dropout2 = tf.keras.layers.Dropout(rate)
  17. self.dropout3 = tf.keras.layers.Dropout(rate)
  18. def call(self, x, enc_output, training,
  19. combined_mask, inp_padding_mask):
  20. # 所有 sub-layers 的主要輸出皆為 (batch_size, target_seq_len, d_model)
  21. # enc_output 為 Encoder 輸出序列,shape 為 (batch_size, input_seq_len, d_model)
  22. # attn_weights_block_1 則為 (batch_size, num_heads, target_seq_len, target_seq_len)
  23. # attn_weights_block_2 則為 (batch_size, num_heads, target_seq_len, input_seq_len)
  24. # sub-layer 1: Decoder layer 自己對輸出序列做注意力。
  25. # 我們同時需要 look ahead mask 以及輸出序列的 padding mask
  26. # 來避免前面已生成的子詞關注到未來的子詞以及 <pad>
  27. attn1, attn_weights_block1 = self.mha1(x, x, x, combined_mask)
  28. attn1 = self.dropout1(attn1, training=training)
  29. out1 = self.layernorm1(attn1 + x)
  30. # sub-layer 2: Decoder layer 關注 Encoder 的最後輸出
  31. # 記得我們一樣需要對 Encoder 的輸出套用 padding mask 避免關注到 <pad>
  32. attn2, attn_weights_block2 = self.mha2(
  33. enc_output, enc_output, out1, inp_padding_mask) # (batch_size, target_seq_len, d_model)
  34. attn2 = self.dropout2(attn2, training=training)
  35. out2 = self.layernorm2(attn2 + out1) # (batch_size, target_seq_len, d_model)
  36. # sub-layer 3: FFN 部分跟 Encoder layer 完全一樣
  37. ffn_output = self.ffn(out2) # (batch_size, target_seq_len, d_model)
  38. ffn_output = self.dropout3(ffn_output, training=training)
  39. out3 = self.layernorm3(ffn_output + out2) # (batch_size, target_seq_len, d_model)
  40. # 除了主要輸出 `out3` 以外,輸出 multi-head 注意權重方便之後理解模型內部狀況
  41. return out3, attn_weights_block1, attn_weights_block2

MHA1 产生 combined_mask ,只要将 padding mask 和 look ahead mask两个遮罩取大的即可:

  1. tar_padding_mask = create_padding_mask(tar)
  2. look_ahead_mask = create_look_ahead_mask(tar.shape[-1])
  3. combined_mask = tf.maximum(tar_padding_mask, look_ahead_mask)
  4. print("tar:", tar)
  5. print("-" * 20)
  6. print("tar_padding_mask:", tar_padding_mask)
  7. print("-" * 20)
  8. print("look_ahead_mask:", look_ahead_mask)
  9. print("-" * 20)
  10. print("combined_mask:", combined_mask)

输出如下,利用 broadcasting 将 combined_mask 的 shape 也扩充为 4 维:(batch_size, num_heads, seq_len_tar, seq_len_tar)= (2, 1, 10, 10) ,以方便之后多头注意力的计算

  1. tar: tf.Tensor(
  2. [[4205 10 241 86 27 3 4206 0 0 0]
  3. [4205 165 489 398 191 14 7 560 3 4206]], shape=(2, 10), dtype=int64)
  4. --------------------
  5. tar_padding_mask: tf.Tensor(
  6. [[[[0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]]]
  7. [[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]]], shape=(2, 1, 1, 10), dtype=float32)
  8. --------------------
  9. look_ahead_mask: tf.Tensor(
  10. [[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  11. [0. 0. 1. 1. 1. 1. 1. 1. 1. 1.]
  12. [0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
  13. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  14. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  15. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  16. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  17. [0. 0. 0. 0. 0. 0. 0. 0. 1. 1.]
  18. [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
  19. [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]], shape=(10, 10), dtype=float32)
  20. --------------------
  21. combined_mask: tf.Tensor(
  22. [[[[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  23. [0. 0. 1. 1. 1. 1. 1. 1. 1. 1.]
  24. [0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
  25. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  26. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  27. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  28. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  29. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  30. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  31. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]]]
  32. [[[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  33. [0. 0. 1. 1. 1. 1. 1. 1. 1. 1.]
  34. [0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
  35. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  36. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  37. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  38. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  39. [0. 0. 0. 0. 0. 0. 0. 0. 1. 1.]
  40. [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
  41. [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]]], shape=(2, 1, 10, 10), dtype=float32)

示例:将目标语言(中文)的词嵌入张量以及相关的这招丢进 Decoder Layer 看看

  1. # 超參數
  2. d_model = 4
  3. num_heads = 2
  4. dff = 8
  5. dec_layer = DecoderLayer(d_model, num_heads, dff)
  6. # 來源、目標語言的序列都需要 padding mask
  7. inp_padding_mask = create_padding_mask(inp)
  8. tar_padding_mask = create_padding_mask(tar)
  9. # masked MHA 用的遮罩,把 padding 跟未來子詞都蓋住
  10. look_ahead_mask = create_look_ahead_mask(tar.shape[-1])
  11. combined_mask = tf.maximum(tar_padding_mask, look_ahead_mask)
  12. # 實際初始一個 decoder layer 並做 3 個 sub-layers 的計算
  13. dec_out, dec_self_attn_weights, dec_enc_attn_weights = dec_layer(
  14. emb_tar, enc_out, False, combined_mask, inp_padding_mask)
  15. print("emb_tar:", emb_tar)
  16. print("-" * 20)
  17. print("enc_out:", enc_out)
  18. print("-" * 20)
  19. print("dec_out:", dec_out)
  20. assert emb_tar.shape == dec_out.shape
  21. print("-" * 20)
  22. print("dec_self_attn_weights.shape:", dec_self_attn_weights.shape)
  23. print("dec_enc_attn_weights:", dec_enc_attn_weights.shape)

输出如下,可以看到 Decoder Layer 的输出张量的维度也和输入张量的维度相同: (batch_size, seq_len, d_model)dec_self_attn_weights 代表 Decoder Layer 的自注意力权重,因此最后两个维度都为中文序列的长度 10;dec_enc_attn_weights 是 Encoder-Decoder 自注意力权重,因此最后一维是 Encoder 输出序列的长度 8。

  1. emb_tar: tf.Tensor(
  2. [[[-9.4237924e-03 -1.8982053e-02 -3.8755499e-02 1.6131904e-02]
  3. [ 1.3820972e-02 -3.2755092e-02 1.0215558e-02 2.3236815e-02]
  4. [ 5.3795800e-03 2.7922321e-02 4.9203541e-02 5.4208413e-03]
  5. [ 3.0345544e-03 -3.4656405e-02 -2.3234559e-02 3.9151311e-03]
  6. [ 4.8759926e-02 4.2193059e-02 -2.9141665e-02 4.5896284e-03]
  7. [-4.5878422e-02 4.3194380e-02 -4.8125375e-02 -2.7835155e-02]
  8. [-1.0285042e-02 5.3374879e-03 4.0048312e-02 1.6815785e-02]
  9. [-5.7560802e-03 -4.3076027e-02 3.9412268e-03 -3.4347549e-03]
  10. [-5.7560802e-03 -4.3076027e-02 3.9412268e-03 -3.4347549e-03]
  11. [-5.7560802e-03 -4.3076027e-02 3.9412268e-03 -3.4347549e-03]]
  12. [[-9.4237924e-03 -1.8982053e-02 -3.8755499e-02 1.6131904e-02]
  13. [ 4.1677501e-02 6.7135915e-03 3.7391197e-02 -3.8386367e-02]
  14. [-2.9780090e-02 -3.5157301e-02 2.0691562e-02 3.0919526e-02]
  15. [ 2.7362112e-02 -1.5102543e-02 1.0358501e-02 4.9035549e-03]
  16. [ 3.9686177e-02 4.7571074e-02 -4.3680418e-02 -9.9581480e-04]
  17. [ 7.1074963e-03 -1.7719496e-02 -7.9342239e-03 -3.0051971e-02]
  18. [-1.1939298e-02 -3.7533417e-03 2.2292137e-05 4.3857586e-02]
  19. [-2.3507465e-02 -3.2441415e-02 1.8460218e-02 -4.7260523e-02]
  20. [-4.5878422e-02 4.3194380e-02 -4.8125375e-02 -2.7835155e-02]
  21. [-1.0285042e-02 5.3374879e-03 4.0048312e-02 1.6815785e-02]]], shape=(2, 10, 4), dtype=float32)
  22. --------------------
  23. enc_out: tf.Tensor(
  24. [[[ 1.623388 -0.39707196 -1.090135 -0.13618113]
  25. [-0.32270592 -0.59458977 1.7079673 -0.7906716 ]
  26. [-1.3702308 1.3467281 -0.38100302 0.40450567]
  27. [ 0.2299977 1.241566 -1.5490661 0.0775026 ]
  28. [-1.0866984 0.88808054 -0.9033548 1.1019727 ]
  29. [-1.1917038 -0.5005804 0.17631385 1.5159702 ]
  30. [-0.90351737 1.4291942 -0.97096586 0.44528902]
  31. [-0.90351737 1.4291942 -0.97096586 0.44528902]]
  32. [[ 1.458591 -0.4763808 -1.2537323 0.27152228]
  33. [-1.6820657 0.9371168 0.45904127 0.28590757]
  34. [-1.1009151 0.28546828 -0.68177414 1.497221 ]
  35. [-0.3456568 1.5769095 -1.1794896 -0.05176307]
  36. [ 0.6062109 -1.1472129 -0.7711475 1.3121496 ]
  37. [-1.5314417 -0.24402368 0.9851706 0.7902947 ]
  38. [-0.99772143 0.8376673 -0.9900316 1.1500859 ]
  39. [-1.133343 -0.44576913 -0.00731844 1.5864305 ]]], shape=(2, 8, 4), dtype=float32)
  40. --------------------
  41. dec_out: tf.Tensor(
  42. [[[ 0.36737707 0.37314212 -1.683959 0.9434399 ]
  43. [ 0.6378808 -1.5644754 -0.135131 1.0617255 ]
  44. [-0.50577945 0.10977709 1.5496157 -1.1536133 ]
  45. [ 0.76230735 -1.1451159 -0.81714 1.1999484 ]
  46. [ 0.61280537 0.9096174 -1.6662468 0.14382386]
  47. [-0.5249774 1.649297 -0.13078591 -0.9935337 ]
  48. [-0.9026026 -0.74446946 1.6218615 0.02521054]
  49. [ 0.57874995 -1.6322535 0.0545973 0.9989062 ]
  50. [ 0.57874995 -1.6322535 0.05459725 0.9989063 ]
  51. [ 0.57874995 -1.6322535 0.05459725 0.9989063 ]]
  52. [[ 0.61770844 0.29575855 -1.7044817 0.7910147 ]
  53. [ 1.5072398 -1.0740138 0.27373248 -0.7069585 ]
  54. [-0.04998673 -1.5770535 0.5079297 1.1191106 ]
  55. [ 1.0227284 -1.5438646 -0.205648 0.7267842 ]
  56. [ 0.8735957 0.69074917 -1.658559 0.09421426]
  57. [ 1.6364292 -1.0398554 -0.1210186 -0.47555515]
  58. [-0.3596239 -1.2438971 0.08143152 1.5220895 ]
  59. [ 0.49965852 -1.5166018 1.191542 -0.17459887]
  60. [-0.13645804 1.671995 -0.70915186 -0.826385 ]
  61. [-0.45344985 -1.3591743 1.2963259 0.51629823]]], shape=(2, 10, 4), dtype=float32)
  62. --------------------
  63. dec_self_attn_weights.shape: (2, 2, 10, 10)
  64. dec_enc_attn_weights: (2, 2, 10, 8)

4.5 Position Encoding

位置编码:想办法让被加入位置编码的 word embedding 在 d_model 维度的空间里不只会因为语义相近而靠近,也会因为在序列中的位置靠近而在该空间里靠近

位置编码的公式:

  • Transformer 的 TensorFlow2 实现:英翻中 - 图6
  • Transformer 的 TensorFlow2 实现:英翻中 - 图7

这样设计的好处:给定任一位置 pos 的位置编码 PE(pos),跟它距离 k 个单位的位置编码 PE(pos+k) 可以表示为 PE(pos) 的一个线性函数
因此,通过在 word embedding 中加入位置编码的资讯,可以帮助 Transformer 学会序列中 tokens 的相对位置关系

位置编码的实现:

  1. # 以下直接參考 TensorFlow 官方 tutorial
  2. def get_angles(pos, i, d_model):
  3. angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
  4. return pos * angle_rates
  5. def positional_encoding(position, d_model):
  6. angle_rads = get_angles(np.arange(position)[:, np.newaxis],
  7. np.arange(d_model)[np.newaxis, :],
  8. d_model)
  9. # apply sin to even indices in the array; 2i
  10. sines = np.sin(angle_rads[:, 0::2])
  11. # apply cos to odd indices in the array; 2i+1
  12. cosines = np.cos(angle_rads[:, 1::2])
  13. pos_encoding = np.concatenate([sines, cosines], axis=-1)
  14. pos_encoding = pos_encoding[np.newaxis, ...]
  15. return tf.cast(pos_encoding, dtype=tf.float32)
  16. seq_len = 50
  17. d_model = 512
  18. pos_encoding = positional_encoding(seq_len, d_model)
  19. pos_encoding

输出如下,位置编码的shape = (batch_size, seq_len, d_model)d_model 也是词嵌入向量的维度,seq_len 代表为序列中每个 token 都加入位置编码

  1. <tf.Tensor: shape=(1, 50, 512), dtype=float32, numpy=
  2. array([[[ 0. , 0. , 0. , ..., 1. ,
  3. 1. , 1. ],
  4. [ 0.84147096, 0.8218562 , 0.8019618 , ..., 1. ,
  5. 1. , 1. ],
  6. [ 0.9092974 , 0.9364147 , 0.95814437, ..., 1. ,
  7. 1. , 1. ],
  8. ...,
  9. [ 0.12357312, 0.97718984, -0.24295525, ..., 0.9999863 ,
  10. 0.99998724, 0.99998814],
  11. [-0.76825464, 0.7312359 , 0.63279754, ..., 0.9999857 ,
  12. 0.9999867 , 0.9999876 ],
  13. [-0.95375264, -0.14402692, 0.99899054, ..., 0.9999851 ,
  14. 0.9999861 , 0.9999871 ]]], dtype=float32)>

将位置编码绘图:

  1. plt.pcolormesh(pos_encoding[0], cmap='RdBu')
  2. plt.xlabel('d_model')
  3. plt.xlim((0, 512))
  4. plt.ylabel('Position')
  5. plt.colorbar()
  6. plt.show()

输出图像如下,x 轴代表跟词嵌入向量相同的维度 d_model,y 轴则代表序列中的每个位置
image.png

4.6 Encoder

Encoder 包含 3个元件:

  • 输入的词嵌入层
  • 位置编码
  • N 个 Encoder Layers

Encoder 的实现:

  1. class Encoder(tf.keras.layers.Layer):
  2. # Encoder 的初始參數除了本來就要給 EncoderLayer 的參數還多了:
  3. # - num_layers: 決定要有幾個 EncoderLayers, 前面影片中的 `N`
  4. # - input_vocab_size: 用來把索引轉成詞嵌入向量
  5. def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
  6. rate=0.1):
  7. super(Encoder, self).__init__()
  8. self.d_model = d_model
  9. self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
  10. self.pos_encoding = positional_encoding(input_vocab_size, self.d_model)
  11. # 建立 `num_layers` 個 EncoderLayers
  12. self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate)
  13. for _ in range(num_layers)]
  14. self.dropout = tf.keras.layers.Dropout(rate)
  15. def call(self, x, training, mask):
  16. # 輸入的 x.shape == (batch_size, input_seq_len)
  17. # 以下各 layer 的輸出皆為 (batch_size, input_seq_len, d_model)
  18. input_seq_len = tf.shape(x)[1]
  19. # 將 2 維的索引序列轉成 3 維的詞嵌入張量,並依照論文乘上 sqrt(d_model)
  20. # 再加上對應長度的位置編碼
  21. x = self.embedding(x)
  22. x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
  23. x += self.pos_encoding[:, :input_seq_len, :]
  24. # 對 embedding 跟位置編碼的總合做 regularization
  25. # 這在 Decoder 也會做
  26. x = self.dropout(x, training=training)
  27. # 通過 N 個 EncoderLayer 做編碼
  28. for i, enc_layer in enumerate(self.enc_layers):
  29. x = enc_layer(x, training, mask)
  30. # 以下只是用來 demo EncoderLayer outputs
  31. #print('-' * 20)
  32. #print(f"EncoderLayer {i + 1}'s output:", x)
  33. return x

示例:直接将索引序列 inp 丢入 Encoder:

  1. # 超參數
  2. num_layers = 2 # 2 層的 Encoder
  3. d_model = 4
  4. num_heads = 2
  5. dff = 8
  6. input_vocab_size = subword_encoder_en.vocab_size + 2 # 記得加上 <start>, <end>
  7. # 初始化一個 Encoder
  8. encoder = Encoder(num_layers, d_model, num_heads, dff, input_vocab_size)
  9. # 將 2 維的索引序列丟入 Encoder 做編碼
  10. enc_out = encoder(inp, training=False, mask=None)
  11. print("inp:", inp)
  12. print("-" * 20)
  13. print("enc_out:", enc_out)

输出如下,可以看到:

  • 输入:(batch_size, seq_len),直接将 2 维的索引序列 inp 作为输入
  • 输出:(batch_size, seq_len, d_model) ``` inp: tf.Tensor( [[8113 103 9 1066 7903 8114 0 0] [8113 16 4111 6735 12 2750 7903 8114]], shape=(2, 8), dtype=int64)

enc_out: tf.Tensor( [[[-0.7849331 -0.5919682 -0.33270508 1.7096064 ] [-0.5070654 -0.5110137 -0.7082318 1.726311 ] [-0.39270175 -0.03102623 -1.1583622 1.58209 ] [-0.5561628 0.38050288 -1.2407898 1.4164498 ] [-0.90432 0.19381054 -0.84728897 1.5577984 ] [-0.9732155 -0.22992782 -0.46524602 1.6683893 ] [-0.84681976 -0.54344714 -0.31013623 1.7004032 ] [-0.62432766 -0.56790507 -0.5390008 1.7312336 ]]

[[-0.77423745 -0.6076474 -0.32800597 1.709891 ] [-0.47978234 -0.5615608 -0.68602896 1.727372 ] [-0.3006829 -0.07366985 -1.197396 1.5717487 ] [-0.5147843 0.27872464 -1.229085 1.4651445 ] [-0.8963447 0.26754597 -0.8954111 1.5242099 ] [-0.9755361 -0.22618699 -0.46569642 1.6674196 ] [-0.87600434 -0.54483986 -0.27099535 1.6918396 ] [-0.60130465 -0.5993665 -0.53067714 1.7313485 ]]], shape=(2, 8, 4), dtype=float32)

  1. <a name="g54sz"></a>
  2. ## 4.7 Decoder
  3. Decoder 也包含 3 个元件:
  4. - 输入的词嵌入层
  5. - 位置编码
  6. - N 个 Decoder Layers
  7. Decoder 的实现:
  8. ```python
  9. class Decoder(tf.keras.layers.Layer):
  10. # 初始參數跟 Encoder 只差在用 `target_vocab_size` 而非 `inp_vocab_size`
  11. def __init__(self, num_layers, d_model, num_heads, dff, target_vocab_size,
  12. rate=0.1):
  13. super(Decoder, self).__init__()
  14. self.d_model = d_model
  15. # 為中文(目標語言)建立詞嵌入層
  16. self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
  17. self.pos_encoding = positional_encoding(target_vocab_size, self.d_model)
  18. self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate)
  19. for _ in range(num_layers)]
  20. self.dropout = tf.keras.layers.Dropout(rate)
  21. # 呼叫時的參數跟 DecoderLayer 一模一樣
  22. def call(self, x, enc_output, training,
  23. combined_mask, inp_padding_mask):
  24. tar_seq_len = tf.shape(x)[1]
  25. attention_weights = {} # 用來存放每個 Decoder layer 的注意權重
  26. # 這邊跟 Encoder 做的事情完全一樣
  27. x = self.embedding(x) # (batch_size, tar_seq_len, d_model)
  28. x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
  29. x += self.pos_encoding[:, :tar_seq_len, :]
  30. x = self.dropout(x, training=training)
  31. for i, dec_layer in enumerate(self.dec_layers):
  32. x, block1, block2 = dec_layer(x, enc_output, training,
  33. combined_mask, inp_padding_mask)
  34. # 將從每個 Decoder layer 取得的注意權重全部存下來回傳,方便我們觀察
  35. attention_weights['decoder_layer{}_block1'.format(i + 1)] = block1
  36. attention_weights['decoder_layer{}_block2'.format(i + 1)] = block2
  37. # x.shape == (batch_size, tar_seq_len, d_model)
  38. return x, attention_weights

示例:

  1. # 超參數
  2. num_layers = 2 # 2 層的 Decoder
  3. d_model = 4
  4. num_heads = 2
  5. dff = 8
  6. target_vocab_size = subword_encoder_zh.vocab_size + 2 # 記得加上 <start>, <end>
  7. # 遮罩
  8. inp_padding_mask = create_padding_mask(inp)
  9. tar_padding_mask = create_padding_mask(tar)
  10. look_ahead_mask = create_look_ahead_mask(tar.shape[1])
  11. combined_mask = tf.math.maximum(tar_padding_mask, look_ahead_mask)
  12. # 初始化一個 Decoder
  13. decoder = Decoder(num_layers, d_model, num_heads, dff, target_vocab_size)
  14. # 將 2 維的索引序列以及遮罩丟入 Decoder
  15. print("tar:", tar)
  16. print("-" * 20)
  17. print("combined_mask:", combined_mask)
  18. print("-" * 20)
  19. print("enc_out:", enc_out)
  20. print("-" * 20)
  21. print("inp_padding_mask:", inp_padding_mask)
  22. print("-" * 20)
  23. dec_out, attn = decoder(tar, enc_out, training=False,
  24. combined_mask=combined_mask,
  25. inp_padding_mask=inp_padding_mask)
  26. print("dec_out:", dec_out)
  27. print("-" * 20)
  28. for block_name, attn_weights in attn.items():
  29. print(f"{block_name}.shape: {attn_weights.shape}")

输出如下:

  1. tar: tf.Tensor(
  2. [[4205 10 241 86 27 3 4206 0 0 0]
  3. [4205 165 489 398 191 14 7 560 3 4206]], shape=(2, 10), dtype=int64)
  4. --------------------
  5. combined_mask: tf.Tensor(
  6. [[[[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  7. [0. 0. 1. 1. 1. 1. 1. 1. 1. 1.]
  8. [0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
  9. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  10. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  11. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  12. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  13. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  14. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  15. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]]]
  16. [[[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  17. [0. 0. 1. 1. 1. 1. 1. 1. 1. 1.]
  18. [0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
  19. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  20. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  21. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  22. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  23. [0. 0. 0. 0. 0. 0. 0. 0. 1. 1.]
  24. [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
  25. [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]]], shape=(2, 1, 10, 10), dtype=float32)
  26. --------------------
  27. enc_out: tf.Tensor(
  28. [[[-0.7849331 -0.5919682 -0.33270508 1.7096064 ]
  29. [-0.5070654 -0.5110137 -0.7082318 1.726311 ]
  30. [-0.39270175 -0.03102623 -1.1583622 1.58209 ]
  31. [-0.5561628 0.38050288 -1.2407898 1.4164498 ]
  32. [-0.90432 0.19381054 -0.84728897 1.5577984 ]
  33. [-0.9732155 -0.22992782 -0.46524602 1.6683893 ]
  34. [-0.84681976 -0.54344714 -0.31013623 1.7004032 ]
  35. [-0.62432766 -0.56790507 -0.5390008 1.7312336 ]]
  36. [[-0.77423745 -0.6076474 -0.32800597 1.709891 ]
  37. [-0.47978234 -0.5615608 -0.68602896 1.727372 ]
  38. [-0.3006829 -0.07366985 -1.197396 1.5717487 ]
  39. [-0.5147843 0.27872464 -1.229085 1.4651445 ]
  40. [-0.8963447 0.26754597 -0.8954111 1.5242099 ]
  41. [-0.9755361 -0.22618699 -0.46569642 1.6674196 ]
  42. [-0.87600434 -0.54483986 -0.27099535 1.6918396 ]
  43. [-0.60130465 -0.5993665 -0.53067714 1.7313485 ]]], shape=(2, 8, 4), dtype=float32)
  44. --------------------
  45. inp_padding_mask: tf.Tensor(
  46. [[[[0. 0. 0. 0. 0. 0. 1. 1.]]]
  47. [[[0. 0. 0. 0. 0. 0. 0. 0.]]]], shape=(2, 1, 1, 8), dtype=float32)
  48. --------------------
  49. dec_out: tf.Tensor(
  50. [[[-0.5652141 -1.0581812 1.6000751 0.02332012]
  51. [-0.34019786 -1.2377603 1.5330344 0.04492375]
  52. [ 0.36752528 -1.4228352 1.3287866 -0.2734766 ]
  53. [ 0.09472056 -1.353683 1.4559422 -0.19697976]
  54. [-0.38392052 -1.094072 1.6231282 -0.1451356 ]
  55. [-0.41729778 -1.0276326 1.6514215 -0.20649128]
  56. [-0.33023426 -1.045482 1.6500467 -0.27433017]
  57. [-0.19232102 -1.1254803 1.6149355 -0.29713416]
  58. [ 0.4082284 -1.3586452 1.3515029 -0.4010862 ]
  59. [ 0.19979596 -1.4183375 1.3857942 -0.16725269]]
  60. [[-0.56504554 -1.0544491 1.6026781 0.01681653]
  61. [-0.36043388 -1.2348609 1.5300142 0.06528072]
  62. [ 0.24521813 -1.4295446 1.3651296 -0.18080314]
  63. [-0.06483467 -1.3449187 1.4773033 -0.06755002]
  64. [-0.41885298 -1.0775514 1.6267893 -0.13038498]
  65. [-0.40018192 -1.0338532 1.650498 -0.21646306]
  66. [-0.3531929 -1.0375834 1.6523482 -0.26157215]
  67. [-0.24463183 -1.1371143 1.6107953 -0.22904909]
  68. [ 0.19615412 -1.362728 1.4271017 -0.2605277 ]
  69. [ 0.08419968 -1.3687491 1.4467623 -0.16221273]]], shape=(2, 10, 4), dtype=float32)
  70. --------------------
  71. decoder_layer1_block1.shape: (2, 2, 10, 10)
  72. decoder_layer1_block2.shape: (2, 2, 10, 8)
  73. decoder_layer2_block1.shape: (2, 2, 10, 10)
  74. decoder_layer2_block2.shape: (2, 2, 10, 8)

5、Transformer 的搭建 & 训练

5.1 Transformer 的搭建

Transformer 的实现:

  1. # Transformer 之上已經沒有其他 layers 了,我們使用 tf.keras.Model 建立一個模型
  2. class Transformer(tf.keras.Model):
  3. # 初始參數包含 Encoder & Decoder 都需要超參數以及中英字典數目
  4. def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
  5. target_vocab_size, rate=0.1):
  6. super(Transformer, self).__init__()
  7. self.encoder = Encoder(num_layers, d_model, num_heads, dff,
  8. input_vocab_size, rate)
  9. self.decoder = Decoder(num_layers, d_model, num_heads, dff,
  10. target_vocab_size, rate)
  11. # 這個 FFN 輸出跟中文字典一樣大的 logits 數,等通過 softmax 就代表每個中文字的出現機率
  12. self.final_layer = tf.keras.layers.Dense(target_vocab_size)
  13. # enc_padding_mask 跟 dec_padding_mask 都是英文序列的 padding mask,
  14. # 只是一個給 Encoder layer 的 MHA 用,一個是給 Decoder layer 的 MHA 2 使用
  15. def call(self, inp, tar, training, enc_padding_mask,
  16. combined_mask, dec_padding_mask):
  17. enc_output = self.encoder(inp, training, enc_padding_mask) # (batch_size, inp_seq_len, d_model)
  18. # dec_output.shape == (batch_size, tar_seq_len, d_model)
  19. dec_output, attention_weights = self.decoder(
  20. tar, enc_output, training, combined_mask, dec_padding_mask)
  21. # 將 Decoder 輸出通過最後一個 linear layer
  22. final_output = self.final_layer(dec_output) # (batch_size, tar_seq_len, target_vocab_size)
  23. return final_output, attention_weights

输入:

  • 英文序列:(batch_size, inp_seq_len)
  • 中文序列:(batch_size, tar_seq_len)

输出:

  • 生成序列:(batch_size, tar_seq_len, target_vocab_size)
  • 注意力权重的 dict

示例:搭建一个 Transformer,并用已经准备好的 demo 数据来训练它做英翻中:

注意下面的第 12-13 行的处理:

  • tar_inp 是将中文序列去掉最末尾一个 token 后的序列,是训练时 Decoder 的输入
  • tar_real 是将中文序列去掉最开头一个 token 后的序列,是训练时的 ground truth

再看第 26 行 Transformer 的输入:

  • inp:待翻译的英文序列
  • tar_inp:对应的中文序列去掉最末尾一个 token 后的序列

相当于在每一个 time step,是给定一个 token(tar_inp中的一个 token)去预测下一个 token(tar_real中对应位置的 token) 并且在训练时,不是将 Transfomer 的输出丢回给 Decoder 当作输入,而是直接拿 ground truth 即 tar_real 当作的输入,即 teacher forcing

  1. # 超參數
  2. num_layers = 1
  3. d_model = 4
  4. num_heads = 2
  5. dff = 8
  6. # + 2 是為了 <start> & <end> token
  7. input_vocab_size = subword_encoder_en.vocab_size + 2
  8. output_vocab_size = subword_encoder_zh.vocab_size + 2
  9. # 重點中的重點。訓練時用前一個字來預測下一個中文字
  10. tar_inp = tar[:, :-1]
  11. tar_real = tar[:, 1:]
  12. # 來源 / 目標語言用的遮罩。注意 `comined_mask` 已經將目標語言的兩種遮罩合而為一
  13. inp_padding_mask = create_padding_mask(inp)
  14. tar_padding_mask = create_padding_mask(tar_inp)
  15. look_ahead_mask = create_look_ahead_mask(tar_inp.shape[1])
  16. combined_mask = tf.math.maximum(tar_padding_mask, look_ahead_mask)
  17. # 初始化我們的第一個 transformer
  18. transformer = Transformer(num_layers, d_model, num_heads, dff,
  19. input_vocab_size, output_vocab_size)
  20. # 將英文、中文序列丟入取得 Transformer 預測下個中文字的結果
  21. predictions, attn_weights = transformer(inp, tar_inp, False, inp_padding_mask,
  22. combined_mask, inp_padding_mask)
  23. print("tar:", tar)
  24. print("-" * 20)
  25. print("tar_inp:", tar_inp)
  26. print("-" * 20)
  27. print("tar_real:", tar_real)
  28. print("-" * 20)
  29. print("predictions:", predictions)

输出如下:

  1. tar: tf.Tensor(
  2. [[4205 10 241 86 27 3 4206 0 0 0]
  3. [4205 165 489 398 191 14 7 560 3 4206]], shape=(2, 10), dtype=int64)
  4. --------------------
  5. tar_inp: tf.Tensor(
  6. [[4205 10 241 86 27 3 4206 0 0]
  7. [4205 165 489 398 191 14 7 560 3]], shape=(2, 9), dtype=int64)
  8. --------------------
  9. tar_real: tf.Tensor(
  10. [[ 10 241 86 27 3 4206 0 0 0]
  11. [ 165 489 398 191 14 7 560 3 4206]], shape=(2, 9), dtype=int64)
  12. --------------------
  13. predictions: tf.Tensor(
  14. [[[ 0.01349578 -0.00199539 -0.00217387 ... -0.03862738 -0.03212879
  15. -0.07692745]
  16. [ 0.03748299 0.01585471 -0.02548707 ... -0.04276202 -0.02495992
  17. -0.05491883]
  18. [ 0.05718527 0.0288353 -0.04577482 ... -0.0450176 -0.01315334
  19. -0.03639907]
  20. ...
  21. [ 0.01202047 -0.00400385 -0.00099438 ... -0.03859971 -0.03085512
  22. -0.0797975 ]
  23. [ 0.0235797 0.00501019 -0.0119309 ... -0.04091505 -0.02892826
  24. -0.06939012]
  25. [ 0.04867783 0.02382021 -0.03683802 ... -0.04392422 -0.01941059
  26. -0.04347047]]
  27. [[ 0.01676657 -0.00080313 -0.00556348 ... -0.03981712 -0.02937311
  28. -0.07665333]
  29. [ 0.03873826 0.01607162 -0.02685272 ... -0.04328423 -0.0234593
  30. -0.0552263 ]
  31. [ 0.0564083 0.02865588 -0.04492006 ... -0.04475704 -0.014088
  32. -0.03639094]
  33. ...
  34. [ 0.01514174 -0.00298803 -0.00426159 ... -0.0397689 -0.02800198
  35. -0.07974622]
  36. [ 0.02867933 0.00800282 -0.01704068 ... -0.04215823 -0.02618419
  37. -0.06638922]
  38. [ 0.05056309 0.02489874 -0.03880978 ... -0.04421616 -0.01803543
  39. -0.04204437]]], shape=(2, 9, 4207), dtype=float32)

5.2 定义损失函数与指标

序列生成任务实际上可以被视为分类任务,每次输出的都是中文字典中每个 token 的概率分布。因此可以使用交叉熵损失来计算生成任务中模型预测的分布和 ground truth 的差距

定义原始的损失函数:

  1. loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
  2. from_logits=True, reduction='none')
  3. # from_logits=True,是因为从 Transformer 得到的预测结果还没有经过 softmax,因此和不为 1
  4. # 可以通过 print(tf.reduce_sum(predictions, axis=-1)) 查看预测结果的和不为 1
  5. # reduction='none',让损失函数 loss_object 不要把每个位置的 error 加总,因为之后我们要自己把 <pad> token 出现的位置的损失舍弃不计
  6. # 假設我們要解的是一個 binary classifcation, 0 跟 1 個代表一個 label
  7. real = tf.constant([1, 1, 0], shape=(1, 3), dtype=tf.float32)
  8. pred = tf.constant([[0, 1], [0, 1], [0, 1]], dtype=tf.float32)
  9. loss_object(real, pred)

输出如下,由于 pred 中第三个预测结果错误,使得交叉熵损失之上升

  1. <tf.Tensor: shape=(3,), dtype=float32, numpy=array([0.31326166, 0.31326166, 1.3132616 ], dtype=float32)>

有了上面的 loss_object 计算交叉熵损失后,还需要封装一个损失函数来建立遮罩并加总序列中不包含 token 位置的损失

  1. def loss_function(real, pred):
  2. # 這次的 mask 將序列中不等於 0 的位置視為 1,其餘為 0
  3. mask = tf.math.logical_not(tf.math.equal(real, 0))
  4. # 照樣計算所有位置的 cross entropy 但不加總
  5. loss_ = loss_object(real, pred)
  6. mask = tf.cast(mask, dtype=loss_.dtype)
  7. loss_ *= mask # 只計算非 <pad> 位置的損失
  8. return tf.reduce_mean(loss_)

另外,再使用 tf.keras.metrics 定义两个指标,方便之后用 TensorBoard 来追踪模型的 performance

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

5.3 设置超参数

  1. num_layers = 4 # Transformer 中 Encoder/Decoder Layers 重复层数,论文中设为 6
  2. d_model = 128 # token 的 representation 维度,论文中为 512
  3. dff = 512 # FFN 中间层维度,论文中为 2048
  4. num_heads = 8 # 多头注意力的头数,论文中为 8
  5. input_vocab_size = subword_encoder_en.vocab_size + 2 # 输入语言(英文)的字典大小
  6. target_vocab_size = subword_encoder_zh.vocab_size + 2 # 输出语言(中文)的字典大小
  7. dropout_rate = 0.1 # 預設值
  8. print("input_vocab_size:", input_vocab_size)
  9. print("target_vocab_size:", target_vocab_size)

输出如下:

  1. input_vocab_size: 8115
  2. target_vocab_size: 4207

5.4 设置优化器 Optimizer

使用 Adam optimizer 以及自定义的 learning rate scheduler:Transformer 的 TensorFlow2 实现:英翻中 - 图9
让训练过程的前 warmup_steps 的学习率线性增加,之后则随步骤数的反平方根下降

  1. class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
  2. # 論文預設 `warmup_steps` = 4000
  3. def __init__(self, d_model, warmup_steps=4000):
  4. super(CustomSchedule, self).__init__()
  5. self.d_model = d_model
  6. self.d_model = tf.cast(self.d_model, tf.float32)
  7. self.warmup_steps = warmup_steps
  8. def __call__(self, step):
  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 schdeule 丟入 Adam opt.
  13. # Adam opt. 的參數都跟論文相同
  14. learning_rate = CustomSchedule(d_model)
  15. optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98,
  16. epsilon=1e-9)

观察这个 shedule 是怎么随训练步骤而改变学习率的:

  1. d_models = [128, 256, 512]
  2. warmup_steps = [1000 * i for i in range(1, 4)]
  3. schedules = []
  4. labels = []
  5. colors = ["blue", "red", "black"]
  6. for d in d_models:
  7. schedules += [CustomSchedule(d, s) for s in warmup_steps]
  8. labels += [f"d_model: {d}, warm: {s}" for s in warmup_steps]
  9. for i, (schedule, label) in enumerate(zip(schedules, labels)):
  10. plt.plot(schedule(tf.range(10000, dtype=tf.float32)),
  11. label=label, color=colors[i // 3])
  12. plt.legend()
  13. plt.ylabel("Learning Rate")
  14. plt.xlabel("Train Step")

输出如下图所示
image.png

5.5 实际训练 & 定时存档

用前面已经定义好的超参数来初始化一个全新的 Transformer

  1. transformer = Transformer(num_layers, d_model, num_heads, dff,
  2. input_vocab_size, target_vocab_size, dropout_rate)
  3. print(f"""這個 Transformer 有 {num_layers} 層 Encoder / Decoder layers
  4. d_model: {d_model}
  5. num_heads: {num_heads}
  6. dff: {dff}
  7. input_vocab_size: {input_vocab_size}
  8. target_vocab_size: {target_vocab_size}
  9. dropout_rate: {dropout_rate}
  10. """)

输出如下:

  1. 這個 Transformer 4 Encoder / Decoder layers
  2. d_model: 128
  3. num_heads: 8
  4. dff: 512
  5. input_vocab_size: 8115
  6. target_vocab_size: 4207
  7. dropout_rate: 0.1

设置 checkpoint 来定期存储/读取模型及 optimizer

  1. train_perc = 20
  2. val_prec = 1
  3. drop_prec = 100 - train_perc - val_prec
  4. # 方便比較不同實驗/ 不同超參數設定的結果
  5. run_id = f"{num_layers}layers_{d_model}d_{num_heads}heads_{dff}dff_{train_perc}train_perc"
  6. checkpoint_path = os.path.join(checkpoint_path, run_id)
  7. log_dir = os.path.join(log_dir, run_id)
  8. # tf.train.Checkpoint 可以幫我們把想要存下來的東西整合起來,方便儲存與讀取
  9. # 一般來說你會想存下模型以及 optimizer 的狀態
  10. ckpt = tf.train.Checkpoint(transformer=transformer,
  11. optimizer=optimizer)
  12. # ckpt_manager 會去 checkpoint_path 看有沒有符合 ckpt 裡頭定義的東西
  13. # 存檔的時候只保留最近 5 次 checkpoints,其他自動刪除
  14. ckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_path, max_to_keep=5)
  15. # 如果在 checkpoint 路徑上有發現檔案就讀進來
  16. if ckpt_manager.latest_checkpoint:
  17. ckpt.restore(ckpt_manager.latest_checkpoint)
  18. # 用來確認之前訓練多少 epochs 了
  19. last_epoch = int(ckpt_manager.latest_checkpoint.split("-")[-1])
  20. print(f'已讀取最新的 checkpoint,模型已訓練 {last_epoch} epochs。')
  21. else:
  22. last_epoch = 0
  23. print("沒找到 checkpoint,從頭訓練。")

输出如下:

  1. 已讀取最新的 checkpoint,模型已訓練 30 epochs

定义一个函数来产生所有的遮罩 mask

  1. # 為 Transformer 的 Encoder / Decoder 準備遮罩
  2. def create_masks(inp, tar):
  3. # 英文句子的 padding mask,要交給 Encoder layer 自注意力機制用的
  4. enc_padding_mask = create_padding_mask(inp)
  5. # 同樣也是英文句子的 padding mask,但是是要交給 Decoder layer 的 MHA 2
  6. # 關注 Encoder 輸出序列用的
  7. dec_padding_mask = create_padding_mask(inp)
  8. # Decoder layer 的 MHA1 在做自注意力機制用的
  9. # `combined_mask` 是中文句子的 padding mask 跟 look ahead mask 的疊加
  10. look_ahead_mask = create_look_ahead_mask(tf.shape(tar)[1])
  11. dec_target_padding_mask = create_padding_mask(tar)
  12. combined_mask = tf.maximum(dec_target_padding_mask, look_ahead_mask)
  13. return enc_padding_mask, combined_mask, dec_padding_mask

train_step:一个数据集包含多个 batch,而每次拿一个 batch 来训练的步骤就称为 train_step。一个训练步骤包含如下过程:

  • 对训练数据做必要的前处理
  • 将数据丢入模型,取得预测结果
  • 用预测结果跟 ground truth 计算 loss
  • 取出梯度并利用 optimizer 做梯度下降

    1. @tf.function # 讓 TensorFlow 幫我們將 eager code 優化並加快運算
    2. def train_step(inp, tar):
    3. # 前面說過的,用去尾的原始序列去預測下一個字的序列
    4. tar_inp = tar[:, :-1]
    5. tar_real = tar[:, 1:]
    6. # 建立 3 個遮罩
    7. enc_padding_mask, combined_mask, dec_padding_mask = create_masks(inp, tar_inp)
    8. # 紀錄 Transformer 的所有運算過程以方便之後做梯度下降
    9. with tf.GradientTape() as tape:
    10. # 注意是丟入 `tar_inp` 而非 `tar`。記得將 `training` 參數設定為 True
    11. predictions, _ = transformer(inp, tar_inp,
    12. True,
    13. enc_padding_mask,
    14. combined_mask,
    15. dec_padding_mask)
    16. # 跟影片中顯示的相同,計算左移一個字的序列跟模型預測分佈之間的差異,當作 loss
    17. loss = loss_function(tar_real, predictions)
    18. # 取出梯度並呼叫前面定義的 Adam optimizer 幫我們更新 Transformer 裡頭可訓練的參數
    19. gradients = tape.gradient(loss, transformer.trainable_variables)
    20. optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
    21. # 將 loss 以及訓練 acc 記錄到 TensorBoard 上,非必要
    22. train_loss(loss)
    23. train_accuracy(tar_real, predictions)

让 Transformer 训练 30 个 Epochs,每个 Epoch 包含如下步骤:

  • (非必要)重置写到 TensorBoard 的 metrics 的值
  • 将整个数据集的 batch 取出,交给 train_step 函数处理
  • (非必要)存 checkpoints
  • (非必要)将当前 epoch 结果写道 TensorBoard
  • (非必要)在标准输出显示当前 epoch 的结果 ```python

    定義我們要看幾遍數據集

    EPOCHS = 30 print(f”此超參數組合的 Transformer 已經訓練 {last_epoch} epochs。”) print(f”剩餘 epochs:{min(0, last_epoch - EPOCHS)}”)

用來寫資訊到 TensorBoard,非必要但十分推薦

summary_writer = tf.summary.create_file_writer(log_dir)

比對設定的 EPOCHS 以及已訓練的 last_epoch 來決定還要訓練多少 epochs

for epoch in range(last_epoch, EPOCHS): start = time.time()

  1. # 重置紀錄 TensorBoard 的 metrics
  2. train_loss.reset_states()
  3. train_accuracy.reset_states()
  4. # 一個 epoch 就是把我們定義的訓練資料集一個一個 batch 拿出來處理,直到看完整個數據集
  5. for (step_idx, (inp, tar)) in enumerate(train_dataset):
  6. # 每次 step 就是將數據丟入 Transformer,讓它生預測結果並計算梯度最小化 loss
  7. train_step(inp, tar)
  8. # 每個 epoch 完成就存一次檔
  9. if (epoch + 1) % 1 == 0:
  10. ckpt_save_path = ckpt_manager.save()
  11. print ('Saving checkpoint for epoch {} at {}'.format(epoch+1,
  12. ckpt_save_path))
  13. # 將 loss 以及 accuracy 寫到 TensorBoard 上
  14. with summary_writer.as_default():
  15. tf.summary.scalar("train_loss", train_loss.result(), step=epoch + 1)
  16. tf.summary.scalar("train_acc", train_accuracy.result(), step=epoch + 1)
  17. print('Epoch {} Loss {:.4f} Accuracy {:.4f}'.format(epoch + 1,
  18. train_loss.result(),
  19. train_accuracy.result()))
  20. print('Time taken for 1 epoch: {} secs\n'.format(time.time() - start))
  1. 输出如下,可以看到会从 checkpoint 加载进度,而不是从头开始训练。

此超參數組合的 Transformer 已經訓練 30 epochs。 剩餘 epochs:0

  1. <a name="Lr1u7"></a>
  2. ## 5.6 使用 TensorBoard
  3. 在 Colab 中开启 TensorBoard
  4. ```python
  5. try:
  6. # %tensorflow_version only exists in Colab.
  7. %tensorflow_version 2.x
  8. except Exception:
  9. pass
  10. # Load the TensorBoard notebook extension
  11. %load_ext tensorboard
  1. %tensorboard --logdir "{log_dir}" # 记得加上"",因为路径中有空格,不加""会报错
  2. # 参考:https://stackoverflow.com/questions/63364452/tensorboard-error-invalid-choice-code-choose-from-serve-dev-while

image.png

5.7 预测:实际进行英翻中

预测过程:

  • 将输入的英文句子利用 Subword Tokenizer 转换成 tokens 索引(inp)
  • 在该英文索引序列前后加上代表 BOS/EOS 的 tokens
  • 在 Transformer 输出序列长度达到 MAX_LENGTH 前重复以下步骤:
    • 为目前已经生成的中文索引序列产生新的遮罩
    • 将刚刚的英文序列、当前的中文序列以及各种遮罩放入 Transformer
    • 将 Transformer 输出序列的最后一个位置的向量取出,并取 argmax 取得新的预测中文索引
    • 将此索引加到目前的中文索引序列中作为 Transformer 到此为止的输出结果
    • 如果新产生的中文索引为 则代表中文翻译已经全部完毕,直接回传
  • 将最后得到的中文索引序列回传作为翻译结果 ```python

    給定一個英文句子,輸出預測的中文索引數字序列以及注意權重 dict

    def evaluate(inp_sentence):

    準備英文句子前後會加上的 ,

    start_token = [subword_encoder_en.vocab_size] end_token = [subword_encoder_en.vocab_size + 1]

    inp_sentence 是字串,我們用 Subword Tokenizer 將其變成子詞的索引序列

    並在前後加上 BOS / EOS

    inp_sentence = start_token + subword_encoder_en.encode(inp_sentence) + end_token encoder_input = tf.expand_dims(inp_sentence, 0)

    跟我們在影片裡看到的一樣,Decoder 在第一個時間點吃進去的輸入

    是一個只包含一個中文 token 的序列

    decoder_input = [subword_encoder_zh.vocab_size] output = tf.expand_dims(decoder_input, 0) # 增加 batch 維度

    auto-regressive,一次生成一個中文字並將預測加到輸入再度餵進 Transformer

    for i in range(MAX_LENGTH):

    1. # 每多一個生成的字就得產生新的遮罩
    2. enc_padding_mask, combined_mask, dec_padding_mask = create_masks(
    3. encoder_input, output)
    4. # predictions.shape == (batch_size, seq_len, vocab_size)
    5. predictions, attention_weights = transformer(encoder_input,
    6. output,
    7. False,
    8. enc_padding_mask,
    9. combined_mask,
    10. dec_padding_mask)
  1. # 將序列中最後一個 distribution 取出,並將裡頭值最大的當作模型最新的預測字
  2. predictions = predictions[: , -1:, :] # (batch_size, 1, vocab_size)
  3. predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)
  4. # 遇到 <end> token 就停止回傳,代表模型已經產生完結果
  5. if tf.equal(predicted_id, subword_encoder_zh.vocab_size + 1):
  6. return tf.squeeze(output, axis=0), attention_weights
  7. #將 Transformer 新預測的中文索引加到輸出序列中,讓 Decoder 可以在產生
  8. # 下個中文字的時候關注到最新的 `predicted_id`
  9. output = tf.concat([output, predicted_id], axis=-1)
  10. # 將 batch 的維度去掉後回傳預測的中文索引序列
  11. return tf.squeeze(output, axis=0), attention_weights
  1. 示例:通过 Transformer 做翻译
  2. ```python
  3. # 要被翻譯的英文句子
  4. sentence = "China, India, and others have enjoyed continuing economic growth."
  5. # 取得預測的中文索引序列
  6. predicted_seq, _ = evaluate(sentence)
  7. # 過濾掉 <start> & <end> tokens 並用中文的 subword tokenizer 幫我們將索引序列還原回中文句子
  8. target_vocab_size = subword_encoder_zh.vocab_size
  9. predicted_seq_without_bos_eos = [idx for idx in predicted_seq if idx < target_vocab_size]
  10. predicted_sentence = subword_encoder_zh.decode(predicted_seq_without_bos_eos)
  11. print("sentence:", sentence)
  12. print("-" * 20)
  13. print("predicted_seq:", predicted_seq)
  14. print("-" * 20)
  15. print("predicted_sentence:", predicted_sentence)

输出如下(enjoy 一词翻译错了):

  1. sentence: China, India, and others have enjoyed continuing economic growth.
  2. --------------------
  3. predicted_seq: tf.Tensor(
  4. [4205 16 4 36 378 100 8 35 32 4 33 111 11 52
  5. 405 238 103 294 22 49 105 83 3], shape=(23,), dtype=int32)
  6. --------------------
  7. predicted_sentence: 中国、印度和其他国家都有所担心持续经济增长。

输出这个 transformer 的参数量

  1. transformer.summary()

输出如下,约 400 万个参数,这个 Transformer 不算巨大

  1. Model: "transformer_1"
  2. _________________________________________________________________
  3. Layer (type) Output Shape Param #
  4. =================================================================
  5. encoder_2 (Encoder) multiple 1831808
  6. _________________________________________________________________
  7. decoder_2 (Decoder) multiple 1596800
  8. _________________________________________________________________
  9. dense_137 (Dense) multiple 542703
  10. =================================================================
  11. Total params: 3,971,311
  12. Trainable params: 3,971,311
  13. Non-trainable params: 0
  14. _________________________________________________________________

5.8 可视化注意力权重

Transformer 可以通过可视化注意力权重,来了解模型实际在生成序列的时候将注意力放在哪些地方。

先查看各个 Decoder Layer 的 MHA1、MHA2 的注意力权重的维度
之后将选择最后一个 Decoder layer 用来关注 Encoder 输出的的 MHA2(即 block2)来看一下 Transformer 在生成中文序列的各个 token 时分别关注在对应英文句子的哪些位置,该注意力的权重维度为 (batch_size, num_heads, zh_seq_len, en_seq_len) = (1, 8, 23, 15)

  1. predicted_seq, attention_weights = evaluate(sentence)
  2. # 在這邊我們自動選擇最後一個 Decoder layer 的 MHA 2,也就是 Decoder 關注 Encoder 的 MHA
  3. layer_name = f"decoder_layer{num_layers}_block2"
  4. print("sentence:", sentence)
  5. print("-" * 20)
  6. print("predicted_seq:", predicted_seq)
  7. print("-" * 20)
  8. print("attention_weights.keys():")
  9. for layer_name, attn in attention_weights.items():
  10. print(f"{layer_name}.shape: {attn.shape}")
  11. print("-" * 20)
  12. print("layer_name:", layer_name)

输出如下:

  1. sentence: China, India, and others have enjoyed continuing economic growth.
  2. --------------------
  3. predicted_seq: tf.Tensor(
  4. [4205 16 4 36 378 100 8 35 32 4 33 111 11 52
  5. 405 238 103 294 22 49 105 83 3], shape=(23,), dtype=int32)
  6. --------------------
  7. attention_weights.keys():
  8. decoder_layer1_block1.shape: (1, 8, 23, 23)
  9. decoder_layer1_block2.shape: (1, 8, 23, 15)
  10. decoder_layer2_block1.shape: (1, 8, 23, 23)
  11. decoder_layer2_block2.shape: (1, 8, 23, 15)
  12. decoder_layer3_block1.shape: (1, 8, 23, 23)
  13. decoder_layer3_block2.shape: (1, 8, 23, 15)
  14. decoder_layer4_block1.shape: (1, 8, 23, 23)
  15. decoder_layer4_block2.shape: (1, 8, 23, 15)
  16. --------------------
  17. layer_name: decoder_layer4_block2

还要先实现绘图函数。
为了输出中文,还要先从网上下载一个中文字体到系统字体目录。

参考:matplotlib 中文_Colab使用matplotlib和seaborn绘图中文乱码问题解决

  1. # 从网上下载一个支持中文的字体到系统字体目录
  2. !wget -O /usr/share/fonts/truetype/liberation/simhei.ttf "https://www.wfonts.com/download/data/2014/06/01/simhei/chinese.simhei.ttf"

输出如下,下载完成

  1. --2021-05-28 08:30:40-- https://www.wfonts.com/download/data/2014/06/01/simhei/chinese.simhei.ttf
  2. Resolving www.wfonts.com (www.wfonts.com)... 104.225.219.210
  3. Connecting to www.wfonts.com (www.wfonts.com)|104.225.219.210|:443... connected.
  4. HTTP request sent, awaiting response... 200 OK
  5. Length: 10050870 (9.6M) [application/octetstream]
  6. Saving to: ‘/usr/share/fonts/truetype/liberation/simhei.ttf
  7. /usr/share/fonts/tr 100%[===================>] 9.58M 5.79MB/s in 1.7s
  8. 2021-05-28 08:30:42 (5.79 MB/s) - ‘/usr/share/fonts/truetype/liberation/simhei.ttf saved [10050870/10050870]

实现绘图函数:

  1. import matplotlib as mpl
  2. # 你可能會需要自行下載一個中文字體檔案以讓 matplotlib 正確顯示中文
  3. zhfont = mpl.font_manager.FontProperties(fname="/usr/share/fonts/truetype/liberation/simhei.ttf")
  4. plt.style.use("seaborn-whitegrid")
  5. # 這個函式將英 -> 中翻譯的注意權重視覺化(注意:我們將注意權重 transpose 以最佳化渲染結果
  6. def plot_attention_weights(attention_weights, sentence, predicted_seq, layer_name, max_len_tar=None):
  7. fig = plt.figure(figsize=(17, 7))
  8. sentence = subword_encoder_en.encode(sentence)
  9. # 只顯示中文序列前 `max_len_tar` 個字以避免畫面太過壅擠
  10. if max_len_tar:
  11. predicted_seq = predicted_seq[:max_len_tar]
  12. else:
  13. max_len_tar = len(predicted_seq)
  14. # 將某一個特定 Decoder layer 裡頭的 MHA 1 或 MHA2 的注意權重拿出來並去掉 batch 維度
  15. attention_weights = tf.squeeze(attention_weights[layer_name], axis=0)
  16. # (num_heads, tar_seq_len, inp_seq_len)
  17. # 將每個 head 的注意權重畫出
  18. for head in range(attention_weights.shape[0]):
  19. ax = fig.add_subplot(2, 4, head + 1)
  20. # [注意]我為了將長度不短的英文子詞顯示在 y 軸,將注意權重做了 transpose
  21. attn_map = np.transpose(attention_weights[head][:max_len_tar, :])
  22. ax.matshow(attn_map, cmap='viridis') # (inp_seq_len, tar_seq_len)
  23. fontdict = {"fontproperties": zhfont}
  24. ax.set_xticks(range(max(max_len_tar, len(predicted_seq))))
  25. ax.set_xlim(-0.5, max_len_tar -1.5)
  26. ax.set_yticks(range(len(sentence) + 2))
  27. ax.set_xticklabels([subword_encoder_zh.decode([i]) for i in predicted_seq
  28. if i < subword_encoder_zh.vocab_size],
  29. fontdict=fontdict, fontsize=18)
  30. ax.set_yticklabels(
  31. ['<start>'] + [subword_encoder_en.decode([i]) for i in sentence] + ['<end>'],
  32. fontdict=fontdict)
  33. ax.set_xlabel('Head {}'.format(head + 1))
  34. ax.tick_params(axis="x", labelsize=12)
  35. ax.tick_params(axis="y", labelsize=12)
  36. plt.tight_layout()
  37. plt.show()
  38. plt.close(fig)

绘制注意力权重:

  1. plot_attention_weights(attention_weights, sentence,
  2. predicted_seq, layer_name, max_len_tar=18)

image.png