transformer_nmt_student.py 51.7 KB
Newer Older
20200318029 committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
#!/usr/bin/env python
# coding: utf-8

# # Transformer翻译项目

# <h1>Table of Contents<span class="tocSkip"></span></h1>
# <div class="toc"><ul class="toc-item"><li><span><a href="#一.-建立Transformer模型的直观认识" data-toc-modified-id="一.-建立Transformer模型的直观认识-1">一. 建立Transformer模型的直观认识</a></span></li><li><span><a href="#二.-编码器部分(Encoder)" data-toc-modified-id="二.-编码器部分(Encoder)-2">二. 编码器部分(Encoder)</a></span><ul class="toc-item"><li><span><a href="#0.-先准备好输入的数据" data-toc-modified-id="0.-先准备好输入的数据-2.1">0. 先准备好输入的数据</a></span></li><li><span><a href="#1.-positional-encoding(即位置嵌入或位置编码)" data-toc-modified-id="1.-positional-encoding(即位置嵌入或位置编码)-2.2">1. positional encoding(即位置嵌入或位置编码)</a></span></li><li><span><a href="#2.-self-attention(自注意力机制)" data-toc-modified-id="2.-self-attention(自注意力机制)-2.3">2. self attention(自注意力机制)</a></span></li><li><span><a href="#3.-Attention-Mask" data-toc-modified-id="3.-Attention-Mask-2.4">3. Attention Mask</a></span></li><li><span><a href="#4.-Layer-Normalization-和残差连接" data-toc-modified-id="4.-Layer-Normalization-和残差连接-2.5">4. Layer Normalization 和残差连接</a></span></li><li><span><a href="#5.-Transformer-Encoder-整体结构" data-toc-modified-id="5.-Transformer-Encoder-整体结构-2.6">5. Transformer Encoder 整体结构</a></span></li></ul></li><li><span><a href="#三.-解码器部分(Decoder)" data-toc-modified-id="三.-解码器部分(Decoder)-3">三. 解码器部分(Decoder)</a></span></li><li><span><a href="#四.-Transformer模型" data-toc-modified-id="四.-Transformer模型-4">四. Transformer模型</a></span></li><li><span><a href="#五.-模型训练" data-toc-modified-id="五.-模型训练-5">五. 模型训练</a></span></li><li><span><a href="#六.-模型预测" data-toc-modified-id="六.-模型预测-6">六. 模型预测</a></span></li></ul></div>

# 在这份notebook当中,我们会(尽可能)实现 $Transformer$ 模型来完成翻译任务。
# (参考论文:$Attention\; Is\; All\; You\; Need$   https://arxiv.org/pdf/1706.03762.pdf )
# > **其中的`TODO`内容即为待完成的部分**
#
# 我们的数据集非常小,只有一万多个句子的训练数据,从结果来看训练出来的模型在测试集上的表现其实已经算还可以了。
# 如果想得到更好的效果,则需要更大的数据量并进行更多的训练迭代次数,感兴趣(并且有硬件条件)的同学可以进行尝试。

# ## 一. 建立Transformer模型的直观认识
#
# 首先来说一下**Transformer**和**LSTM**的最大区别,就是LSTM的训练是迭代(自回归)的,是一个接一个字的来,当前这个字过完LSTM单元,才可以进下一个字,而 $Transformer$ 的训练是并行了,就是所有字是全部同时训练的,这样就大大加快了计算效率,$Transformer$ 使用了位置嵌入$(positional \ encoding)$来理解语言的顺序,使用自注意力机制和全连接层来进行计算,这些后面都会详细讲解。
#
# $Transformer$ 模型主要分为**两大部分**,分别是**编码器($Encoder$)**和**解码器($Decoder$)**:
# - **编码器($Encoder$)**负责把自然语言序列映射成为**隐藏层**(下图中**第2步**用九宫格比喻的部分),含有自然语言序列的数学表达
# - **解码器($Decoder$)**再把隐藏层映射为自然语言序列,从而使我们可以解决各种问题,如情感分类、命名实体识别、语义关系抽取、摘要生成、机器翻译等等。

# <img src="./imgs/intuition.jpg" width=650>

# ## 二. 编码器部分(Encoder)
#
#
# 我们会**重点介绍编码器的结构**,因为理解了编码器中的结构, 理解解码器就非常简单了。而且我们用编码器就能够完成一些自然语言处理中比较主流的任务, 如情感分类, 语义关系分析, 命名实体识别等。
#
# **编码器($Encoder$)**部分, 即把**自然语言序列映射为隐藏层的数学表达的过程**。

# ** 以下为一个Transformer Encoder Block结构示意图**
# > 注意: 为方便查看, 下面各部分的内容分别对应着图中第1, 2, 3, 4个方框的序号:

# <img src="./imgs/encoder.jpg" width=550>

# ### 0. 先准备好输入的数据

# In[2]:


import os
import math
import copy
import time
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

from nltk import word_tokenize
from collections import Counter
from torch.autograd import Variable

from langconv import Converter


import jieba


# 初始化参数设置
UNK = 1  # 未登录词的标识符对应的词典id
PAD = 0  # padding占位符对应的词典id
BATCH_SIZE = 64  # 每批次训练数据数量
EPOCHS = 20  # 训练轮数
LAYERS = 6  # transformer中堆叠的encoder和decoder block层数
H_NUM = 8  # multihead attention hidden个数
D_MODEL = 256  # embedding维数
D_FF = 1024  # feed forward第一个全连接层维数
DROPOUT = 0.1  # dropout比例
MAX_LENGTH = 60  # 最大句子长度

TRAIN_FILE = 'nmt/en-cn/train.txt'  # 训练集数据文件
DEV_FILE = "nmt/en-cn/dev.txt"  # 验证(开发)集数据文件
SAVE_FILE = 'save/model.pt'  # 模型保存路径(注意如当前目录无save文件夹需要自己创建)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")


# In[5]:


def seq_padding(X, padding=PAD):
    """
    对一个batch批次(以单词id表示)的数据进行padding填充对齐长度
    """
    # 计算该批次数据各条数据句子长度
    L = [len(x) for x in X]
    # 获取该批次数据最大句子长度
    ML = max(L)
    # 对X中各条数据x进行遍历,如果长度短于该批次数据最大长度ML,则以padding id填充缺失长度ML-len(x)
    return np.array([
        np.concatenate([x, [padding] * (ML - len(x))]) if len(x) < ML else x for x in X
    ])

def cht_to_chs(sent):
    sent = Converter('zh-hans').convert(sent)
    sent.encode('utf-8')
    return sent


# In[6]:


class PrepareData:
    def __init__(self, train_file, dev_file):
        # 读取数据并分词
        self.train_en, self.train_cn = self.load_data(train_file)
        self.dev_en, self.dev_cn = self.load_data(dev_file)

        # 构建单词表
        self.en_word_dict, self.en_total_words, self.en_index_dict =             self.build_dict(self.train_en)
        self.cn_word_dict, self.cn_total_words, self.cn_index_dict =             self.build_dict(self.train_cn)

        # id化
        self.train_en, self.train_cn = self.word2id(self.train_en, self.train_cn, self.en_word_dict, self.cn_word_dict)
        self.dev_en, self.dev_cn = self.word2id(self.dev_en, self.dev_cn, self.en_word_dict, self.cn_word_dict)

        # 划分batch + padding + mask
        self.train_data = self.split_batch(self.train_en, self.train_cn, BATCH_SIZE)
        self.dev_data = self.split_batch(self.dev_en, self.dev_cn, BATCH_SIZE)

    def load_data(self, path):
        """
        读取翻译前(英文)和翻译后(中文)的数据文件
        每条数据都进行分词,然后构建成包含起始符(BOS)和终止符(EOS)的单词(中文为字符)列表
        形式如:en = [['BOS', 'i', 'love', 'you', 'EOS'], ['BOS', 'me', 'too', 'EOS'], ...]
                cn = [['BOS', '我', '爱', '你', 'EOS'], ['BOS', '我', '也', '是', 'EOS'], ...]
        """
        en = []
        cn = []
        # TODO ...
        with open(path, mode="r", encoding="utf-8") as f:

            for line in f.readlines():
                sent_en, sent_cn = line.strip().split("\t")
                sent_en = sent_en.lower()
                sent_cn = cht_to_chs(sent_cn)
                sent_en = ["BOS"] + word_tokenize(sent_en) + ["EOS"]
                # sent_cn = " ".join([char for char in sent_cn])
                # sent_cn = ["BOS"] + word_tokenize(sent_cn) + ["EOS"]
                sent_cn = ["BOS"] + [word for word in jieba.cut(sent_cn)] + ["EOS"]
                en.append(sent_en)
                cn.append(sent_cn)

        return en, cn

    def build_dict(self, sentences, max_words = 50000):
        """
        传入load_data构造的分词后的列表数据
        构建词典(key为单词,value为id值)
        """
        # 对数据中所有单词进行计数
        word_count = Counter()

        for sentence in sentences:
            for s in sentence:
                word_count[s] += 1
        # 只保留最高频的前max_words数的单词构建词典
        # 并添加上UNK和PAD两个单词,对应id已经初始化设置过
        ls = word_count.most_common(max_words)
        # 统计词典的总词数
        total_words = len(ls) + 2

        word_dict = {w[0]: index + 2 for index, w in enumerate(ls)}
        word_dict['UNK'] = UNK
        word_dict['PAD'] = PAD
        # 再构建一个反向的词典,供id转单词使用
        index_dict = {v: k for k, v in word_dict.items()}

        return word_dict, total_words, index_dict

    def word2id(self, en, cn, en_dict, cn_dict, sort=True):
        """
        该方法可以将翻译前(英文)数据和翻译后(中文)数据的单词列表表示的数据
        均转为id列表表示的数据
        如果sort参数设置为True,则会以翻译前(英文)的句子(单词数)长度排序
        以便后续分batch做padding时,同批次各句子需要padding的长度相近减少padding量
        """
        # 计算英文数据条数
        length = len(en)

        # TODO: 将翻译前(英文)数据和翻译后(中文)数据都转换为id表示的形式
        out_en_ids = [[en_dict.get(word, UNK) for word in sent] for sent in en]
        out_cn_ids = [[cn_dict.get(word, UNK) for word in sent] for sent in cn]

        # 构建一个按照句子长度排序的函数
        def len_argsort(seq):
            """
            传入一系列句子数据(分好词的列表形式),
            按照句子长度排序后,返回排序后原来各句子在数据中的索引下标
            """
            return sorted(range(len(seq)), key=lambda x: len(seq[x]))

        # 把中文和英文按照同样的顺序排序
        if sort:
            # 以英文句子长度排序的(句子下标)顺序为基准
            sorted_index = len_argsort(out_en_ids)

            # TODO: 对翻译前(英文)数据和翻译后(中文)数据都按此基准进行排序
            out_en_ids = [out_en_ids[idx] for idx in sorted_index]
            out_cn_ids = [out_cn_ids[idx] for idx in sorted_index]

        return out_en_ids, out_cn_ids

    def split_batch(self, en, cn, batch_size, shuffle=True):
        """
        将以单词id列表表示的翻译前(英文)数据和翻译后(中文)数据
        按照指定的batch_size进行划分
        如果shuffle参数为True,则会对这些batch数据顺序进行随机打乱
        """
        # 在按数据长度生成的各条数据下标列表[0, 1, ..., len(en)-1]中
        # 每隔指定长度(batch_size)取一个下标作为后续生成batch的起始下标
        idx_list = np.arange(0, len(en), batch_size)
        # 如果shuffle参数为True,则将这些各batch起始下标打乱
        if shuffle:
            np.random.shuffle(idx_list)
        # 存放各个batch批次的句子数据索引下标
        batch_indexs = []
        for idx in idx_list:
            # 注意,起始下标最大的那个batch可能会超出数据大小
            # 因此要限定其终止下标不能超过数据大小
            """
            形如[array([4, 5, 6, 7]),
                 array([0, 1, 2, 3]),
                 array([8, 9, 10, 11]),
                 ...]
            """
            batch_indexs.append(np.arange(idx, min(idx + batch_size, len(en))))

        # 按各batch批次的句子数据索引下标,构建实际的单词id列表表示的各batch句子数据
        batches = []
        for batch_index in batch_indexs:
            # 按当前batch的各句子下标(数组批量索引)提取对应的单词id列表句子表示数据
            batch_en = [en[index] for index in batch_index]
            batch_cn = [cn[index] for index in batch_index]
            # 对当前batch的各个句子都进行padding对齐长度
            # 维度为:batch数量×batch_size×每个batch最大句子长度
            batch_cn = seq_padding(batch_cn)
            batch_en = seq_padding(batch_en)
            # 将当前batch的英文和中文数据添加到存放所有batch数据的列表中
            batches.append(Batch(batch_en, batch_cn))

        return batches


# 注意,上述预处理中使用的 $Batch$ 类在后面的 $Encoder$ 内容的 $Attention\ Mask$ 部分定义

# **Embeddings**
#
# 与其他序列传导模型类似,我们使用learned embeddings将输入标记和输出标记转换为维度 $d_{model}$ 的向量。我们还使用通常学习的线性变换和softmax函数将 $Decoder$(解码器)的输出转换为预测的下一个标签的概率。
#
# 在我们的模型中,我们在两个embedding层和pre-softmax线性变换层之间共享相同的权重矩阵。这么做可以节省参数,也是一种正则化方式。
# 在其中的embedding层,我们会将这些权重乘以 $\sqrt{d_{model}}$ 。

# In[7]:


class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        super(Embeddings, self).__init__()
        # Embedding层
        self.lut = nn.Embedding(vocab, d_model)
        # Embedding维数
        self.d_model = d_model

    def forward(self, x):
        # 返回x对应的embedding矩阵(需要乘以math.sqrt(d_model))
        return self.lut(x) * math.sqrt(self.d_model)


# 数据全部处理完成,现在我们开始理解和构建 $Transformer$ 模型

# ### 1. positional encoding(即位置嵌入或位置编码)

# 由于 $Transformer$ 模型**没有**循环神经网络的迭代操作,所以我们必须提供每个字的位置信息给 $Transformer$,才能识别出语言中的顺序关系。
#
# 因此,我们定义一个位置嵌入的概念,也就是$positional \ encoding$,位置嵌入的维度为$[max \ sequence \ length,\ embedding \ dimension]$,嵌入的维度同词向量的维度,$max \ sequence \ length$属于超参数,指的是限定的最大单个句长。
#
# 注意,我们一般以字为单位训练transformer模型,也就是说我们不用分词了,首先我们要初始化字向量为$[vocab \ size,\ embedding \ dimension]$,$vocab \ size$为总共的字库数量,$embedding \ dimension$为字向量的维度,也是每个字的数学表达。
#
# 在论文 **attention is all you need**( https://arxiv.org/pdf/1706.03762.pdf )中使用了$sine$和$cosine$函数的线性变换来提供给模型位置信息:
#
# $$PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) \quad \quad PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})\tag{eq.1}$$
#
# 上式中$pos$指的是句中字的位置,取值范围是$[0, \ max \ sequence \ length)$,$i$指的是词向量的维度,取值范围是$[0, \ embedding \ dimension)$,上面有$sin$和$cos$一组公式,也就是对应着$embedding \ dimension$维度的一组奇数和偶数的序号的维度,例如$0, 1$一组,$2, 3$一组,分别用上面的$sin$和$cos$函数做处理,从而产生不同的周期性变化,而位置嵌入在$embedding \ dimension$维度上随着维度序号增大,周期变化会越来越慢,而产生一种包含位置信息的纹理,就像论文原文中第六页讲的,位置嵌入函数的波长从$2 \pi$到$10000 * 2 \pi$变化,而每一个位置在$embedding \ dimension$维度上都会得到不同周期的$sin$和$cos$函数的取值组合,从而产生独一的纹理位置信息,模型从而学到位置之间的依赖关系和自然语言的时序特性。

# In[8]:


# 导入依赖库
import matplotlib.pyplot as plt
import seaborn as sns


class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        # 初始化一个size为 max_len(设定的最大长度)×embedding维度 的全零矩阵
        # 来存放所有小于这个长度位置对应的porisional embedding
        pe = torch.zeros(max_len, d_model, device=DEVICE)
        # 生成一个位置下标的tensor矩阵(每一行都是一个位置下标)
        """
        形式如:
        tensor([[0.],
                [1.],
                [2.],
                [3.],
                [4.],
                ...])
        """
        position = torch.arange(0., max_len, device=DEVICE)
        position.unsqueeze_(1)
        # 这里幂运算太多,我们使用exp和log来转换实现公式中pos下面要除以的分母(由于是分母,要注意带负号)
        div_term = torch.exp(torch.arange(0., d_model, 2, device=DEVICE) * (- math.log(1e4) / d_model))
        div_term.unsqueeze_(0)

        # TODO: 根据公式,计算各个位置在各embedding维度上的位置纹理值,存放到pe矩阵中
        pe[:, 0 : : 2] = torch.sin(torch.mm(position, div_term))
        pe[:, 1 : : 2] = torch.cos(torch.mm(position, div_term))

        # 加1个维度,使得pe维度变为:1×max_len×embedding维度
        # (方便后续与一个batch的句子所有词的embedding批量相加)
        pe.unsqueeze_(0)
        # 将pe矩阵以持久的buffer状态存下(不会作为要训练的参数)
        self.register_buffer('pe', pe)

    def forward(self, x):
        # 将一个batch的句子所有词的embedding与已构建好的positional embeding相加
        # (这里按照该批次数据的最大句子长度来取对应需要的那些positional embedding值)
        x = x + Variable(self.pe[:, : x.size(1), :], requires_grad=False)
        return self.dropout(x)


# 可见,这里首先是按照最大长度max_len生成一个位置,而后根据公式计算出所有的向量,在forward函数中根据长度取用即可,非常方便。
#
# > 注意要设置requires_grad=False,因其不参与训练。

# 下面画一下位置嵌入,可见纵向观察,随着$embedding \ dimension$增大,位置嵌入函数呈现不同的周期变化。

# In[10]:


pe = PositionalEncoding(16, 0, 100)
positional_encoding = pe.forward(torch.zeros(1, 100, 16, device=DEVICE))
plt.figure(figsize=(10,10))
sns.heatmap(positional_encoding.squeeze().to("cpu"))
plt.title("Sinusoidal Function")
plt.xlabel("hidden dimension")
plt.ylabel("sequence length")
plt.show()


# In[11]:


plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)
y = pe.forward(torch.zeros(1, 100, 20, device=DEVICE))
y = y.to("cpu").numpy()
plt.plot(np.arange(100), y[0, :, 4:8])
plt.legend(["dim %d" % p for p in [4,5,6,7]])
plt.show()


# ### 2. self attention(自注意力机制)

# <img src="./imgs/attention_0.jpg">
#
# <img src="./imgs/attention_1.jpg">

# **除以$\sqrt{d_k}$的解释**
#
# 假设 $q$ 和 $k$ 是独立的随机变量,平均值为 0,方差 1,这样他们的点积后形成的注意力矩阵为 $q⋅k=\sum_{i=1}^{d_k}{q_i k_i}$,均值为 0 但方差放大为 $d_k$ 。为了抵消这种影响,我们用$\sqrt{d_k}$来缩放点积,可以使得Softmax归一化时结果更稳定(不至于点积后得到注意力矩阵的值差别太大),以便反向传播时获取平衡的梯度

# In[12]:


def clones(module, N):
    """
    克隆模型块,克隆的模型块参数不共享
    """
    return nn.ModuleList([
        copy.deepcopy(module) for _ in range(N)
    ])


# In[13]:


def attention(query, key, value, mask=None, dropout=None):
    # 将query矩阵的最后一个维度值作为d_k
    d_k = query.size(-1)

    # TODO: 将key的最后两个维度互换(转置),才能与query矩阵相乘,乘完了还要除以d_k开根号
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

    # 如果存在要进行mask的内容,则将那些为0的部分替换成一个很大的负数
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)

    # TODO: 将mask后的attention矩阵按照最后一个维度进行softmax
    p_attn = F.softmax(scores, dim=-1)

    # 如果dropout参数设置为非空,则进行dropout操作
    if dropout is not None:
        p_attn = dropout(p_attn)
    # 最后返回注意力矩阵跟value的乘积,以及注意力矩阵
    return torch.matmul(p_attn, value), p_attn


class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        # 保证可以整除
        assert d_model % h == 0
        # 得到一个head的attention表示维度
        self.d_k = d_model // h
        # head数量
        self.h = h
        # 定义4个全连接函数,供后续作为WQ,WK,WV矩阵和最后h个多头注意力矩阵concat之后进行变换的矩阵
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        if mask is not None:
            mask = mask.unsqueeze(1)
        # query的第一个维度值为batch size
        nbatches = query.size(0)
        # 将embedding层乘以WQ,WK,WV矩阵(均为全连接)
        # 并将结果拆成h块,然后将第二个和第三个维度值互换(具体过程见上述解析)
        query, key, value = [
            l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
            for l, x in zip(self.linears, (query, key, value))
        ]
        # 调用上述定义的attention函数计算得到h个注意力矩阵跟value的乘积,以及注意力矩阵
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        # 将h个多头注意力矩阵concat起来(注意要先把h变回到第三维的位置)
        x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
        # 使用self.linears中构造的最后一个全连接函数来存放变换后的矩阵进行返回
        return self.linears[-1](x)


# 参数里面的`h`和`d_model`分别表示注意力头的个数,以及模型的隐层单元数。
#
# 另外在`__init__`函数中,我们定义了`self.linears = clones(nn.Linear(d_model, d_model), 4), clone(x, N)`即为深拷贝N份,这里定义了4个全连接函数,实际上是3+1,其中的3个分别是Q、K和V的变换矩阵,最后一个是用于最后将`h`个多头注意力矩阵concat之后进行变换的矩阵。
#
# 在`forward`函数中,是首先将`query`、`key`和`value`进行相应的变换,然后需要经过`attention`这个函数的计算,这个函数实际上就是论文中“Scaled Dot-Product Attention”这个模块的计算

# ### 3. Attention Mask

# <img src="./imgs/attention_mask.jpg">

# 注意, 在上面$self \ attention$的计算过程中, 我们通常使用$mini \ batch$来计算, 也就是一次计算多句话, 也就是$X$的维度是$[batch \ size, \ sequence \ length]$, $sequence \ length$是句长, 而一个$mini \ batch$是由多个不等长的句子组成的, 我们就需要按照这个$mini \ batch$中最大的句长对剩余的句子进行补齐长度, 我们一般用$0$来进行填充, 这个过程叫做$padding$.
#
# 但这时在进行$softmax$的时候就会产生问题, 回顾$softmax$函数$\sigma (\mathbf {z} )_{i}={\frac {e^{z_{i}}}{\sum _{j=1}^{K}e^{z_{j}}}}$, $e^0$是1, 是有值的, 这样的话$softmax$中被$padding$的部分就参与了运算, 就等于是让无效的部分参与了运算, 会产生很大隐患, 这时就需要做一个$mask$让这些无效区域不参与运算, 我们一般给无效区域加一个很大的负数的偏置, 也就是:
#
# $$z_{illegal} = z_{illegal} + bias_{illegal}$$
# $$bias_{illegal} \to -\infty$$
# $$e^{z_{illegal}} \to 0 $$
#
# 经过上式的$masking$我们使无效区域经过$softmax$计算之后几乎为$0$, 这样就避免了无效区域参与计算.

# 在`Transformer`里面,`Encoder`和`Decoder`的`Attention`计算都需要相应的`Mask`处理,但功能却不同。
#
# 在`Encoder`中,就如上述介绍的,`Mask`就是为了让那些在一个`batch`中长度较短的序列的`padding`部分不参与`Attention`的计算。因此我们定义一个`Batch`批处理对象,它包含用于训练的`src`(翻译前)和`trg`(翻译后)句子,以及构造其中的`Mask`掩码。
#
# **加了`Mask`的`Attention`原理如图(另附`Multi-Head Attention`):**
# > 注意:这里的`Attention Mask`是加在`Scale`和`Softmax`之间
#
# <img src="./imgs/attention_mask2.jpg">

# In[14]:


class Batch:
    "Object for holding a batch of data with mask during training."
    def __init__(self, src, trg=None, pad=PAD):
        # 将输入与输出的单词id表示的数据规范成整数类型
        src = torch.from_numpy(src).to(DEVICE).long()
        trg = torch.from_numpy(trg).to(DEVICE).long()
        self.src = src
        # 对于当前输入的句子非空部分进行判断成bool序列
        # 并在seq length前面增加一维,形成维度为 1×seq length 的矩阵
        self.src_mask = (src != pad).unsqueeze(-2)
        # 如果输出目标不为空,则需要对decoder要使用到的target句子进行mask
        if trg is not None:
            # decoder要用到的target输入部分
            self.trg = trg[:, : -1]
            # decoder训练时应预测输出的target结果
            self.trg_y = trg[:, 1 :]
            # 将target输入部分进行attention mask
            self.trg_mask = self.make_std_mask(self.trg, pad)
            # 将应输出的target结果中实际的词数进行统计
            self.ntokens = (self.trg_y != pad).data.sum()

    # Mask掩码操作
    @staticmethod
    def make_std_mask(tgt, pad):
        "Create a mask to hide padding and future words."
        tgt_mask = (tgt != pad).unsqueeze(-2)
        tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))
        return tgt_mask


# ### 4. Layer Normalization 和残差连接

# 1). **LayerNorm**:
#
# $Layer Normalization$的作用是把神经网络中隐藏层归一为标准正态分布, 也就是 $i.i.d$(独立同分布), 以起到加快训练速度, 加速收敛的作用:
# $$\mu_{i}=\frac{1}{m} \sum^{m}_{i=1}x_{ij}$$
#
# 上式中以矩阵的行$(row)$为单位求均值;
#
# $$\sigma^{2}_{j}=\frac{1}{m} \sum^{m}_{i=1}
# (x_{ij}-\mu_{j})^{2}$$
#
# 上式中以矩阵的行$(row)$为单位求方差;
#
# $$LayerNorm(x)=\alpha \odot \frac{x_{ij}-\mu_{i}}
# {\sqrt{\sigma^{2}_{i}+\epsilon}} + \beta \tag{eq.5}$$
#
# 然后用**每一行**的**每一个元素**减去**这行的均值**, 再除以**这行的标准差**, 从而得到归一化后的数值, $\epsilon$是为了防止除$0$;
# 之后引入两个**可训练参数**$\alpha, \ \beta$来弥补归一化的过程中损失掉的信息, 注意$\odot$表示元素相乘而不是点积, 我们一般初始化$\alpha$为全$1$, 而$\beta$为全$0$.
#
# > 注:有关Batch Normalization和Layer Normalization的区别可参考如下文章——https://zhuanlan.zhihu.com/p/33173246

# 2). **残差连接**:
#
# 我们在上一步得到了经过注意力矩阵加权之后的`V`, 也就是`Attention(Q, K, V)`, 我们对它进行一下转置, 使其和`X_embedding`的维度一致, 也就是`[batch size, sequence length, embedding dimension]`, 然后把他们加起来做残差连接, 直接进行元素相加, 因为他们的维度一致:
#
# `X_embedding + Attention(Q, K, V)`
#
# 在之后的运算里, 每经过一个模块的运算, 都要把运算之前的值和运算之后的值相加, 从而得到残差连接, 训练的时候可以使梯度直接走捷径反传到最初始层:
#
# $$X + SubLayer(X) \tag{eq. 6}$$
#
# > **注意:这里我们对`SubLayer(X)`一般会进行dropout后再与X连接,即`X + Dropout(SubLayer(X))`**

# In[15]:


class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        # 初始化α为全1, 而β为全0
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        # 平滑项
        self.eps = eps

    def forward(self, x):
        # TODO: 请利用init中的成员变量实现LayerNorm层的功能
        # 按最后一个维度计算均值和方差
        mean = x.mean(dim=-1, keepdim=True)
        std = x.std(dim=-1, keepdim=True)
        # mean = x.mean(dim=[-2, -1], keepdim=True)
        # std = x.std(dim=[-2, -1], keepdim=True)

        # TODO: 返回Layer Norm的结果
        x = (x - mean) / torch.sqrt(std ** 2 + self.eps)
        return self.a_2 * x + self.b_2


# 以上是`LayerNormalization`的实现,其实PyTorch里面已经集成好了`nn.LayerNorm`,这里实现出来是为了学习其中的原理。而实际中,为了代码简洁,可以直接使用PyTorch里面实现好的函数。

# In[16]:


class SublayerConnection(nn.Module):
    """
    SublayerConnection的作用就是把Multi-Head Attention和Feed Forward层连在一起
    只不过每一层输出之后都要先做Layer Norm再残差连接
    """
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        # TODO: 请利用init中的成员变量实现LayerNorm和残差连接的功能
        # 返回Layer Norm和残差连接后结果
        x_ = self.norm(x)
        x_ = sublayer(x_)
        x_ = self.dropout(x_)
        return x + x_


# ### 5. Transformer Encoder 整体结构

# 经过上面4个步骤, 我们已经基本了解到来$transformer$编码器的主要构成部分, 我们下面用公式把一个$transformer \ block$的计算过程整理一下:
#
# 1). **字向量与位置编码:**
# $$X = EmbeddingLookup(X) + PositionalEncoding \tag{eq.2}$$
# $$X \in \mathbb{R}^{batch \ size  \ * \  seq. \ len. \  * \  embed. \ dim.} $$
#
# 2). **自注意力机制:**
# $$Q = Linear(X) = XW_{Q}$$
# $$K = Linear(X) = XW_{K} \tag{eq.3}$$
# $$V = Linear(X) = XW_{V}$$
# $$X_{attention} = SelfAttention(Q, \ K, \ V) \tag{eq.4}$$
#
# 3). **残差连接与$Layer \ Normalization$**
# $$X_{attention} = LayerNorm(X_{attention}) \tag{eq. 5}$$
# $$X_{attention} = X + X_{attention} \tag{eq. 6}$$
#
# 4). **$FeedForward$,其实就是两层线性映射并用激活函数(比如说$ReLU$)激活:**
# $$X_{hidden} = Linear(Activate(Linear(X_{attention}))) \tag{eq. 7}$$
#
# 5). **重复3).:**
# $$X_{hidden} = LayerNorm(X_{hidden})$$
# $$X_{hidden} = X_{attention} + X_{hidden}$$
# $$X_{hidden} \in \mathbb{R}^{batch \ size  \ * \  seq. \ len. \  * \  embed. \ dim.} $$

# $Feed Forward$(前馈网络)层其实就是两层线性映射并用激活函数激活

# In[17]:


class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        self.w_2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # TODO: 请利用init中的成员变量实现Feed Forward层的功能
        x = self.w_1(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.w_2(x)
        return x


# $Encoder$ 由 $N=6$ 个相同的层组成。

# In[18]:


class Encoder(nn.Module):
    # layer = EncoderLayer
    # N = 6
    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        # 复制N个encoder layer
        self.layers = clones(layer, N)
        # Layer Norm
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        """
        使用循环连续encoder N次(这里为6次)
        这里的Eecoderlayer会接收一个对于输入的attention mask处理
        """
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)


# 每层 $Encoder\ Block$ 都有两个子层组成。第一个子层实现了“多头”的 $Self\text{-}attention$,第二个子层则是一个简单的 $Position\text{-}wise$ 的全连接前馈网络。

# In[19]:


class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        # SublayerConnection的作用就是把multi和ffn连在一起
        # 只不过每一层输出之后都要先做Layer Norm再残差连接
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
        # d_model
        self.size = size

    def forward(self, x, mask):
        # 将embedding层进行Multi head Attention
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        # 注意到attn得到的结果x直接作为了下一层的输入
        return self.sublayer[1](x, self.feed_forward)


# ## 三. 解码器部分(Decoder)

# <img src="./imgs/decoder.jpg">

# 接着来看 $Decoder$ 部分(右半部分),它同样也是由 $N$ 层(在论文中,仍取 $N=6$ )堆叠起来。
# > 对于其中的每一层,除了与 $Encoder$ 中相同的 $self\text{-}attention$ 及 $Feed Forward$ 两层之外,还在中间插入了一个传统的 $Encoder\text{-}Decoder$ 框架中的 $context\text{-}attention$ 层(上图中的$sub\text{-}layer\ 2$),即将 $Decoder$ 的输出作为 $query$ 去查询 $Encoder$ 的输出,同样用的是 $Multi\text{-}Head\ Attention$ ,使得在 $Decode$ 的时候能看到 $Encoder$ 的所有输出。
#
# 这里明确一下 **$Decoder$ 的输入输出和解码过程:**
#
# - 输入:$Encoder$ 的输出 & 对应 $i-1$ 位置 $Decoder$ 的输出。所以中间的 $Attention$ 不是 $Self\text{-}Attention$ ,它的 $K$,$V$ 来自 $Encoder$ ,Q来自上一位置 $Decoder$ 的输出
# - 输出:对应 $i$ 位置的输出词的概率分布
# - 解码:这里要特别注意一下,编码可以并行计算,一次性全部encoding出来,但解码不是一次把所有序列解出来的,而是像rnn一样一个一个解出来的,因为要用上一个位置的输入当作 $Attention$ 的 $query$

# 这里**`Encoder`和`Decoder`的`Attention`的区别如下图所示**
#
# <img src="./imgs/attention.png">

# In[20]:


class Decoder(nn.Module):
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        # TODO: 参照EncoderLayer完成成员变量定义
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)


    def forward(self, x, memory, src_mask, tgt_mask):
        """
        使用循环连续decode N次(这里为6次)
        这里的Decoderlayer会接收一个对于输入的attention mask处理
        和一个对输出的attention mask + subsequent mask处理
        """
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)


class DecoderLayer(nn.Module):
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()
        self.size = size
        # Self-Attention
        self.self_attn = self_attn
        # 与Encoder传入的Context进行Attention
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        # 用m来存放encoder的最终hidden表示结果
        m = memory

        # TODO: 参照EncoderLayer完成DecoderLayer的forwark函数
        # Self-Attention:注意self-attention的q,k和v均为decoder hidden
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
        # Context-Attention:注意context-attention的q为decoder hidden,而k和v为encoder hidden
        x = self.sublayer[1](x, lambda x: self.self_attn(x, m, m, src_mask))
        return self.sublayer[2](x, self.feed_forward)


# 明确了解码过程之后最上面的图就很好懂了,这里主要的不同就是新加的另外要说一下新加的attention多加了一个 $subsequent\_mask$ ,因为训练时的output都是ground truth,这样可以确保预测第 $i$ 个位置时不会接触到未来的信息,具体解释如下。
#
# 对于 $Encoder$ 中 $src$ 的 $mask$ 方式就比较简单,直接把 $pad$ 部分给 $mask$ 掉即可。
# 但对于 $Decoder$ 中 $trg$ 的 $mask$ 计算略微复杂一些,不仅需要把 $pad$ 部分 $mask$ 掉,还需要进行一个 $subsequent\_mask$ 的操作。
# 即作为 $decoder$,在预测当前步的时候,是不能知道后面的内容的,即 $attention$ 需要加上 $mask$,将当前步之后的分数全部置为$-\infty$,然后再计算 $softmax$,以防止发生数据泄露。这种 $Masked$ 的 $Attention$ 是考虑到输出 $Embedding$ 会偏移一个位置,确保了生成位置 $i$ 的预测时,仅依赖小于 $i$ 的位置处的已知输出,相当于把后面不该看到的信息屏蔽掉。

# In[21]:


def subsequent_mask(size):
    "Mask out subsequent positions."
    # 设定subsequent_mask矩阵的shape
    attn_shape = (1, size, size)

    # TODO: 生成一个右上角(不含主对角线)为全1,左下角(含主对角线)为全0的subsequent_mask矩阵
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')

    # TODO: 返回一个右上角(不含主对角线)为全False,左下角(含主对角线)为全True的subsequent_mask矩阵
    return torch.from_numpy(subsequent_mask) == 0


# 我们可视化一下 $subsequent\_mask$ 矩阵的形式,直观进行理解。
# 这里的 $Attention mask$ 图显示了允许每个目标词(行)查看的位置(列)。在训练期间,当前解码位置的词不能 $Attend$ 到后续位置的词。
#
# > 这里是给定一个序列长度size,生成一个下三角矩阵,在主对角线右上的都是 False,其示意图如下:
# <img src="./imgs/subsequent_mask.png" width=450>
# 就是在decoder层的self-Attention中,由于生成 $s_i$ 时, $s_{i+1}$ 并没有产生,所以不能有 $s_i$ 和 $s_{i+1}$ 的关联系数,即只有下三角矩阵有系数,即下图中**`黄色部分`**。

# In[22]:


plt.figure(figsize=(5, 5))
plt.imshow(subsequent_mask(20)[0])
plt.show()


# ## 四. Transformer模型
#
# 最后,我们把 $Encoder$ 和 $Decoder$ 组成 $Transformer$ 模型

# In[23]:


class Transformer(nn.Module):
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(Transformer, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator

    def encode(self, src, src_mask):
        return self.encoder(self.src_embed(src), src_mask)

    def decode(self, memory, src_mask, tgt, tgt_mask):
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

    def forward(self, src, tgt, src_mask, tgt_mask):
        # encoder的结果作为decoder的memory参数传入,进行decode
        return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)


# In[24]:


class Generator(nn.Module):
    # vocab: tgt_vocab
    def __init__(self, d_model, vocab):
        super(Generator, self).__init__()
        # decode后的结果,先进入一个全连接层变为词典大小的向量
        self.proj = nn.Linear(d_model, vocab)

    def forward(self, x):
        # 然后再进行log_softmax操作(在softmax结果上再做多一次log运算)
        return F.log_softmax(self.proj(x), dim=-1)


# **定义设置超参并连接完整模型的函数**

# In[25]:


def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h = 8, dropout=0.1):
    c = copy.deepcopy
    # 实例化Attention对象
    attn = MultiHeadedAttention(h, d_model).to(DEVICE)
    # 实例化FeedForward对象
    ff = PositionwiseFeedForward(d_model, d_ff, dropout).to(DEVICE)
    # 实例化PositionalEncoding对象
    position = PositionalEncoding(d_model, dropout).to(DEVICE)
    # 实例化Transformer模型对象
    model = Transformer(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout).to(DEVICE), N).to(DEVICE),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout).to(DEVICE), N).to(DEVICE),
        nn.Sequential(Embeddings(d_model, src_vocab).to(DEVICE), c(position)),
        nn.Sequential(Embeddings(d_model, tgt_vocab).to(DEVICE), c(position)),
        Generator(d_model, tgt_vocab)).to(DEVICE)

    # This was important from their code.
    # Initialize parameters with Glorot / fan_avg.
    for p in model.parameters():
        if p.dim() > 1:
            # 这里初始化采用的是nn.init.xavier_uniform
            nn.init.xavier_uniform_(p)
    return model.to(DEVICE)


# ## 五. 模型训练

# **标签平滑**

# 在训练期间,我们采用了值$\epsilon_{ls}=0.1$的标签平滑(参见: https://arxiv.org/pdf/1512.00567.pdf ),其实还是从$Computer\; Vision$上搬过来的,具体操作可以看下面的代码实现,**在这里不作为重点**。
#
# 这种做法提高了困惑度,因为模型变得更加不确定,但提高了准确性和BLEU分数。
# >我们使用 $KL\; div\; loss$(KL散度损失)实现标签平滑。
# 对于输出的分布,从原始的 $one\text{-}hot$ 分布转为在groundtruth上使用一个confidence值,而后其他的所有非groudtruth标签上采用 $\frac{1 - confidence}{odim - 1}$ 作为概率值进行平滑。

# In[26]:


class LabelSmoothing(nn.Module):
    """标签平滑处理"""
    def __init__(self, size, padding_idx, smoothing=0.0):
        super(LabelSmoothing, self).__init__()
        self.criterion = nn.KLDivLoss(reduction='sum')
        self.padding_idx = padding_idx
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.size = size
        self.true_dist = None

    def forward(self, x, target):
        assert x.size(1) == self.size
        true_dist = x.data.clone()
        true_dist.fill_(self.smoothing / (self.size - 2))
        true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
        true_dist[:, self.padding_idx] = 0
        mask = torch.nonzero(target.data == self.padding_idx)
        if mask.dim() > 0:
            true_dist.index_fill_(0, mask.squeeze(), 0.0)
        self.true_dist = true_dist
        return self.criterion(x, Variable(true_dist, requires_grad=False))


# 这里的size是输出词表的大小,smoothing是用于分摊在非groundtruth上面的概率值。

# 在这里,我们可以看到标签平滑的示例。

# In[27]:


# Label smoothing的例子
crit = LabelSmoothing(5, 0, 0.4)  # 设定一个ϵ=0.4
predict = torch.FloatTensor([[0, 0.2, 0.7, 0.1, 0],
                             [0, 0.2, 0.7, 0.1, 0],
                             [0, 0.2, 0.7, 0.1, 0]])
v = crit(Variable(predict.log()),
         Variable(torch.LongTensor([2, 1, 0])))

# Show the target distributions expected by the system.
print(crit.true_dist)
plt.imshow(crit.true_dist)


# 如果对给定的选择非常有信心,标签平滑实际上会开始惩罚模型。

# In[28]:


crit = LabelSmoothing(5, 0, 0.1)
def loss(x):
    d = x + 3 * 1
    predict = torch.FloatTensor([[0, x / d, 1 / d, 1 / d, 1 / d]])
    #print(predict)
    return crit(Variable(predict.log()), Variable(torch.LongTensor([1]))).item()

plt.plot(np.arange(1, 100), [loss(x) for x in range(1, 100)])


# **计算损失**

# In[29]:


class SimpleLossCompute:
    """
    简单的计算损失和进行参数反向传播更新训练的函数
    """
    def __init__(self, generator, criterion, opt=None):
        self.generator = generator
        self.criterion = criterion
        self.opt = opt

    def __call__(self, x, y, norm):
        x = self.generator(x)
        loss = self.criterion(x.contiguous().view(-1, x.size(-1)),
                              y.contiguous().view(-1)) / norm
        loss.backward()
        if self.opt is not None:
            self.opt.step()
            self.opt.optimizer.zero_grad()
        return loss.data.item() * norm.float()


# **optimizer优化器**

# 论文里面提到了他们用的优化器,是以$\beta_1=0.9、\beta_2=0.98$ 和 $\epsilon = 10^{−9}$ 的 $Adam$ 为基础,而后使用一种warmup的学习率调整方式来进行调节。
# 具体公式如下:
#
# $$ lrate = d^{−0.5}_{model}⋅min(step\_num^{−0.5},\; step\_num⋅warmup\_steps^{−1.5})$$
#
# 基本上就是用一个固定的 $warmup\_steps$ **先进行学习率的线性增长(热身)**,而后到达 $warmup\_steps$ 之后会随着 $step\_num$ 的增长,以 $step\_num$(步数)的反平方根成比例地**逐渐减小它**,他们用的 $warmup\_steps = 4000$ ,这个可以针对不同的问题自己尝试。

# In[30]:


class NoamOpt:
    "Optim wrapper that implements rate."
    def __init__(self, model_size, factor, warmup, optimizer):
        self.optimizer = optimizer
        self._step = 0
        self.warmup = warmup
        self.factor = factor
        self.model_size = model_size
        self._rate = 0

    def step(self):
        "Update parameters and rate"
        self._step += 1
        rate = self.rate()
        for p in self.optimizer.param_groups:
            p['lr'] = rate
        self._rate = rate
        self.optimizer.step()

    def rate(self, step = None):
        "Implement `lrate` above"
        if step is None:
            step = self._step
        return self.factor * (self.model_size ** (-0.5) * min(step ** (-0.5), step * self.warmup ** (-1.5)))

def get_std_opt(model):
    return NoamOpt(model.src_embed[0].d_model, 2, 4000,
            torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))


# 主要调节是在 $rate$ 这个函数中,其中
# - $model\_size$ 即为 $d_{model}$
# - $warmup$ 即为 $warmup\_steps$
# - $factor$ 可以理解为初始的学习率

# 以下对该优化器在**不同模型大小($model\_size$)**和**不同超参数($marmup$)值**的情况下的学习率($lrate$)曲线进行示例。

# In[31]:


# Three settings of the lrate hyperparameters.
opts = [NoamOpt(512, 1, 4000, None),
        NoamOpt(512, 1, 8000, None),
        NoamOpt(256, 1, 4000, None)]
plt.plot(np.arange(1, 20000), [[opt.rate(i) for opt in opts] for i in range(1, 20000)])
plt.legend(["512:4000", "512:8000", "256:4000"])


# **训练迭代**

# 接下来,我们创建一个通用的训练和评分功能来跟踪损失。 我们传入一个上面定义的损失计算函数,它也处理参数更新。

# In[32]:


def run_epoch(data, model, loss_compute, epoch):
    start = time.time()
    total_tokens = 0.
    total_loss = 0.
    tokens = 0.

    for i , batch in enumerate(data):
        out = model(batch.src, batch.trg, batch.src_mask, batch.trg_mask)
        loss = loss_compute(out, batch.trg_y, batch.ntokens)

        total_loss += loss
        total_tokens += batch.ntokens
        tokens += batch.ntokens

        if i % 50 == 1:
            elapsed = time.time() - start
            print("Epoch %d Batch: %d Loss: %f Tokens per Sec: %fs" % (epoch, i - 1, loss / batch.ntokens, (tokens.float() / elapsed / 1000.)))
            start = time.time()
            tokens = 0

    return total_loss / total_tokens


def train(data, model, criterion, optimizer):
    """
    训练并保存模型
    """
    # 初始化模型在dev集上的最优Loss为一个较大值
    best_dev_loss = 1e5

    for epoch in range(EPOCHS):
        # 模型训练
        model.train()
        run_epoch(data.train_data, model, SimpleLossCompute(model.generator, criterion, optimizer), epoch)
        model.eval()

        # 在dev集上进行loss评估
        print('>>>>> Evaluate')
        dev_loss = run_epoch(data.dev_data, model, SimpleLossCompute(model.generator, criterion, None), epoch)
        print('<<<<< Evaluate loss: %f' % dev_loss)

        # TODO: 如果当前epoch的模型在dev集上的loss优于之前记录的最优loss则保存当前模型,并更新最优loss值
        if dev_loss < best_dev_loss:
            torch.save(model.state_dict(), SAVE_FILE)
            best_dev_loss = dev_loss
            print('****** Save model done... ******')

        print()


# In[33]:


# 数据预处理
data = PrepareData(TRAIN_FILE, DEV_FILE)
src_vocab = len(data.en_word_dict)
tgt_vocab = len(data.cn_word_dict)
print("src_vocab %d" % src_vocab)
print("tgt_vocab %d" % tgt_vocab)

# 初始化模型
model = make_model(
                    src_vocab,
                    tgt_vocab,
                    LAYERS,
                    D_MODEL,
                    D_FF,
                    H_NUM,
                    DROPOUT
                )

# 训练
print(">>>>>>> start train")
train_start = time.time()
criterion = LabelSmoothing(tgt_vocab, padding_idx = 0, smoothing= 0.0)
optimizer = NoamOpt(D_MODEL, 1, 2000, torch.optim.Adam(model.parameters(), lr=0, betas=(0.9,0.98), eps=1e-9))

train(data, model, criterion, optimizer)
print(f"<<<<<<< finished train, cost {time.time()-train_start:.4f} seconds")


# ## 六. 模型预测

# In[ ]:


def greedy_decode(model, src, src_mask, max_len, start_symbol):
    """
    传入一个训练好的模型,对指定数据进行预测
    """
    # 先用encoder进行encode
    memory = model.encode(src, src_mask)
    # 初始化预测内容为1×1的tensor,填入开始符('BOS')的id,并将type设置为输入数据类型(LongTensor)
    ys = torch.ones(1, 1).fill_(start_symbol).type_as(src.data)
    # 遍历输出的长度下标
    for i in range(max_len-1):
        # decode得到隐层表示
        out = model.decode(memory,
                           src_mask,
                           Variable(ys),
                           Variable(subsequent_mask(ys.size(1)).type_as(src.data)))
        # 将隐藏表示转为对词典各词的log_softmax概率分布表示
        prob = model.generator(out[:, -1])
        # 获取当前位置最大概率的预测词id
        _, next_word = torch.max(prob, dim = 1)
        next_word = next_word.data[0]
        # 将当前位置预测的字符id与之前的预测内容拼接起来
        ys = torch.cat([ys,
                        torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=1)
    return ys


def evaluate(data, model):
    """
    在data上用训练好的模型进行预测,打印模型翻译结果
    """
    # 梯度清零
    with torch.no_grad():
        # 在data的英文数据长度上遍历下标
        for i in range(len(data.dev_en)):
            # TODO: 打印待翻译的英文句子
            en_sent = " ".join([data.en_index_dict[w] for w in data.dev_en[i]])
            print("\n" + en_sent)

            # TODO: 打印对应的中文句子答案
            cn_sent = " ".join([data.cn_index_dict[w] for w in data.dev_cn[i]])
            print("".join(cn_sent))

            # 将当前以单词id表示的英文句子数据转为tensor,并放如DEVICE中
            src = torch.from_numpy(np.array(data.dev_en[i])).long().to(DEVICE)
            # 增加一维
            src = src.unsqueeze(0)
            # 设置attention mask
            src_mask = (src != 0).unsqueeze(-2)
            # 用训练好的模型进行decode预测
            out = greedy_decode(model, src, src_mask, max_len=MAX_LENGTH, start_symbol=data.cn_word_dict["BOS"])
            # 初始化一个用于存放模型翻译结果句子单词的列表
            translation = []
            # 遍历翻译输出字符的下标(注意:开始符"BOS"的索引0不遍历)
            for j in range(1, out.size(1)):
                # 获取当前下标的输出字符
                sym = data.cn_index_dict[out[0, j].item()]
                # 如果输出字符不为'EOS'终止符,则添加到当前句子的翻译结果列表
                if sym != 'EOS':
                    translation.append(sym)
                # 否则终止遍历
                else:
                    break
            # 打印模型翻译输出的中文句子结果
            print("translation: %s" % " ".join(translation))


# In[ ]:


# 预测
# 加载模型
model.load_state_dict(torch.load(SAVE_FILE))
# 开始预测
print(">>>>>>> start evaluate")
evaluate_start  = time.time()
evaluate(data, model)
print(f"<<<<<<< finished evaluate, cost {time.time()-evaluate_start:.4f} seconds")


# In[ ]: