transformer_nmt_student.ipynb 559 KB

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 Transformer 模型来完成翻译任务。
(参考论文:$Attention\; Is\; All\; You\; Need$ https://arxiv.org/pdf/1706.03762.pdf

其中的TODO内容即为待完成的部分

我们的数据集非常小,只有一万多个句子的训练数据,从结果来看训练出来的模型在测试集上的表现其实已经算还可以了。
如果想得到更好的效果,则需要更大的数据量并进行更多的训练迭代次数,感兴趣(并且有硬件条件)的同学可以进行尝试。

一. 建立Transformer模型的直观认识

首先来说一下TransformerLSTM的最大区别,就是LSTM的训练是迭代(自回归)的,是一个接一个字的来,当前这个字过完LSTM单元,才可以进下一个字,而 Transformer 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 [1]:
# import pdb
# from IPython.core.interactiveshell import InteractiveShell

# InteractiveShell.ast_node_interactivity = "all"
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
In [3]:
import jieba
In [4]:
# 初始化参数设置
UNK = 1  # 未登录词的标识符对应的词典id
PAD = 0  # padding占位符对应的词典id
BATCH_SIZE = 128  # 每批次训练数据数量
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 Batch 类在后面的 Encoder Encoder 内容的 Attention Mask Attention\ Mask 部分定义

Embeddings

与其他序列传导模型类似,我们使用learned embeddings将输入标记和输出标记转换为维度 dmodel d_{model} 的向量。我们还使用通常学习的线性变换和softmax函数将 Decoder Decoder(解码器)的输出转换为预测的下一个标签的概率。

在我们的模型中,我们在两个embedding层和pre-softmax线性变换层之间共享相同的权重矩阵。这么做可以节省参数,也是一种正则化方式。
在其中的embedding层,我们会将这些权重乘以 dmodel \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 Transformer 模型

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

In [8]:
# 导入依赖库
import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib inline
In [9]:
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">

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)

参数里面的hd_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函数中,是首先将querykeyvalue进行相应的变换,然后需要经过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 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}}}}$, e0 e^0是1, 是有值的, 这样的话$softmax$中被$padding$的部分就参与了运算, 就等于是让无效的部分参与了运算, 会产生很大隐患, 这时就需要做一个$mask$让这些无效区域不参与运算, 我们一般给无效区域加一个很大的负数的偏置, 也就是:

zillegal=zillegal+biasillegalz_{illegal} = z_{illegal} + bias_{illegal}

经过上式的$masking$我们使无效区域经过$softmax$计算之后几乎为$0$, 这样就避免了无效区域参与计算.

Transformer里面,EncoderDecoderAttention计算都需要相应的Mask处理,但功能却不同。

Encoder中,就如上述介绍的,Mask就是为了让那些在一个batch中长度较短的序列的padding部分不参与Attention的计算。因此我们定义一个Batch批处理对象,它包含用于训练的src(翻译前)和trg(翻译后)句子,以及构造其中的Mask掩码。

加了MaskAttention原理如图(另附Multi-Head Attention):

注意:这里的Attention Mask是加在ScaleSoftmax之间

<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 和残差连接

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 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 Encoder\ Block 都有两个子层组成。第一个子层实现了“多头”的 Self-attention Self\text{-}attention,第二个子层则是一个简单的 Position-wise 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 Decoder 部分(右半部分),它同样也是由 N N 层(在论文中,仍取 N=6 N=6 )堆叠起来。

对于其中的每一层,除了与 Encoder Encoder 中相同的 self-attention self\text{-}attentionFeedForward Feed Forward 两层之外,还在中间插入了一个传统的 Encoder-Decoder Encoder\text{-}Decoder 框架中的 context-attention context\text{-}attention 层(上图中的$sub\text{-}layer\ 2$),即将 Decoder Decoder 的输出作为 query query 去查询 Encoder Encoder 的输出,同样用的是 Multi-Head Attention Multi\text{-}Head\ Attention ,使得在 Decode Decode 的时候能看到 Encoder Encoder 的所有输出。

这里明确一下 $Decoder$ 的输入输出和解码过程:

  • 输入:$Encoder$ 的输出 & 对应 $i-1$ 位置 $Decoder$ 的输出。所以中间的 $Attention$ 不是 $Self\text{-}Attention$ ,它的 $K$,$V$ 来自 $Encoder$ ,Q来自上一位置 $Decoder$ 的输出
  • 输出:对应 $i$ 位置的输出词的概率分布
  • 解码:这里要特别注意一下,编码可以并行计算,一次性全部encoding出来,但解码不是一次把所有序列解出来的,而是像rnn一样一个一个解出来的,因为要用上一个位置的输入当作 $Attention$ 的 $query$

这里EncoderDecoderAttention的区别如下图所示

<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 subsequent\_mask ,因为训练时的output都是ground truth,这样可以确保预测第 i i 个位置时不会接触到未来的信息,具体解释如下。

对于 Encoder Encodersrc srcmask mask 方式就比较简单,直接把 pad pad 部分给 mask mask 掉即可。
但对于 Decoder Decodertrg trgmask mask 计算略微复杂一些,不仅需要把 pad pad 部分 mask mask 掉,还需要进行一个 subsequent_mask subsequent\_mask 的操作。
即作为 decoder decoder,在预测当前步的时候,是不能知道后面的内容的,即 attention attention 需要加上 mask mask,将当前步之后的分数全部置为$-\infty$,然后再计算 softmax softmax,以防止发生数据泄露。这种 Masked MaskedAttention Attention 是考虑到输出 Embedding Embedding 会偏移一个位置,确保了生成位置 i i 的预测时,仅依赖小于 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 subsequent\_mask 矩阵的形式,直观进行理解。
这里的 Attentionmask Attention mask 图显示了允许每个目标词(行)查看的位置(列)。在训练期间,当前解码位置的词不能 Attend Attend 到后续位置的词。

这里是给定一个序列长度size,生成一个下三角矩阵,在主对角线右上的都是 False,其示意图如下:
<img src="./imgs/subsequent_mask.png" width=450>
就是在decoder层的self-Attention中,由于生成 $si$ 时, $s{i+1}$ 并没有产生,所以不能有 $si$ 和 $s{i+1}$ 的关联系数,即只有下三角矩阵有系数,即下图中黄色部分

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

四. Transformer模型

最后,我们把 Encoder EncoderDecoder Decoder 组成 Transformer 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分数。

我们使用 KLdivloss KL\; div\; loss(KL散度损失)实现标签平滑。
对于输出的分布,从原始的 one-hot one\text{-}hot 分布转为在groundtruth上使用一个confidence值,而后其他的所有非groudtruth标签上采用 1confidenceodim1 \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)
Out [27]:
tensor([[0.0000, 0.1333, 0.6000, 0.1333, 0.1333],
        [0.0000, 0.6000, 0.1333, 0.1333, 0.1333],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000]])

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

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)])
Out [28]:
[<matplotlib.lines.Line2D at 0x1ae1ad08c18>]

计算损失

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优化器

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 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"])
Out [31]:
<matplotlib.legend.Legend at 0x1ae1ac797b8>

训练迭代

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

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")
Out [33]:
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\zhaoyin\AppData\Local\Temp\jieba.cache
Loading model cost 0.671 seconds.
Prefix dict has been built successfully.

六. 模型预测

In [34]:
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 [35]:
# 预测
# 加载模型
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")
Out [35]:
>>>>>>> start evaluate

BOS look around . EOS
BOS 四处 看看 。 EOS
translation: 转过 来 睡觉 。

BOS hurry up . EOS
BOS 赶快 ! EOS
translation: 赶快 !

BOS keep trying . EOS
BOS 继续 努力 。 EOS
translation: 继续 继续 继续 努力 的 。

BOS take it . EOS
BOS 拿走 吧 。 EOS
translation: 拿走 吧 。

BOS birds fly . EOS
BOS 鸟类 飞行 。 EOS
translation: 鸟类 飞行 。

BOS hurry up . EOS
BOS 快点 ! EOS
translation: 赶快 !

BOS look there . EOS
BOS 看 那里 。 EOS
translation: 看 那里 必须 在 那里 。

BOS how annoying ! EOS
BOS 真 UNK 。 EOS
translation: 真 好 !

BOS get serious . EOS
BOS 认真 点 。 EOS
translation: 认真 点 。

BOS once again . EOS
BOS 再 一次 。 EOS
translation: 再 一次 。

BOS stay sharp . EOS
BOS UNK 。 EOS
translation: UNK 被 吓 到 了 。

BOS i won ! EOS
BOS 我 赢 了 。 EOS
translation: 我 赢得 了 !

BOS get away ! EOS
BOS 滚 ! EOS
translation: 滚 !

BOS i resign . EOS
BOS 我 放弃 。 EOS
translation: 我 放弃 了 继续 尝试 。

BOS how strange ! EOS
BOS 真 奇怪 。 EOS
translation: 多美 啊 !

BOS tom UNK . EOS
BOS 汤姆 脸红 了 。 EOS
translation: 汤姆 在 撒谎 。

BOS who cares ? EOS
BOS 爱 谁 谁 。 EOS
translation: 谁 关心 谁 ?

BOS sweet dreams ! EOS
BOS 祝 你好 梦 。 EOS
translation: 祝 你好 梦 。

BOS step inside . EOS
BOS 进来 。 EOS
translation: 沿着 这条 街直 走 。

BOS go away ! EOS
BOS 滚 ! EOS
translation: 滚 !

BOS anything else ? EOS
BOS 还有 别的 吗 ? EOS
translation: 别人 有 什么 事 吗 ?

BOS i 'm sleepy . EOS
BOS UNK 了 。 EOS
translation: 我 在 这个 时候 一直 在 工作 。

BOS i ate UNK . EOS
BOS 我 吃 了 UNK 。 EOS
translation: 我 在 白天 的 工作 。

BOS i like sports . EOS
BOS 我 喜欢 运动 。 EOS
translation: 我 喜欢 运动 。

BOS she may come . EOS
BOS 她 可以 来 。 EOS
translation: 她 也许 来 吧 。

BOS everybody will die . EOS
BOS 人 UNK UNK 。 EOS
translation: 大家 都 会 死 。

BOS answer the question . EOS
BOS 回答 问题 。 EOS
translation: 回答 这个 问题 。

BOS is that better ? EOS
BOS 那 更好 吗 ? EOS
translation: 那 是 纯金 吗 ?

BOS i like you . EOS
BOS 我 喜欢 你 。 EOS
translation: 我 喜欢 你 。

BOS let him in . EOS
BOS 让 他 进来 。 EOS
translation: 让 他 进来 。

BOS tom is laughing . EOS
BOS 汤姆 在 笑 。 EOS
translation: 汤姆 在 笑 。

BOS tom began talking . EOS
BOS 汤姆 开始 说话 。 EOS
translation: 汤姆 开始 说话 了 。

BOS draw a circle . EOS
BOS 画 一个圈 。 EOS
translation: 画 一个圈 。

BOS this is mine . EOS
BOS 这 是 我 的 。 EOS
translation: 这 是 我 的 。

BOS she might come . EOS
BOS 她 也许 会 来 。 EOS
translation: 她 也许 会 来 。

BOS i hate you . EOS
BOS 我 恨 你 。 EOS
translation: 我 讨厌 你 。

BOS are you lost ? EOS
BOS 您 迷路 了 吗 ? EOS
translation: 你 迷路 了 吗 ?

BOS i miss you . EOS
BOS 我 想念 你 。 EOS
translation: 我 想念 你 。

BOS they 're children . EOS
BOS UNK 是 孩子 。 EOS
translation: 他们 是 孩子 的 孩子 。

BOS i played tennis . EOS
BOS 我 打网球 了 。 EOS
translation: 我 打 了 网球 。

BOS he gave in . EOS
BOS 他 UNK 了 . EOS
translation: 他 被 一颗 球 藏 了 。

BOS shame on you ! EOS
BOS 你 真 丢脸 ! EOS
translation: 蠢货 !

BOS read this book . EOS
BOS 看 这 本书 。 EOS
translation: 念 这个 。

BOS he is american . EOS
BOS 他 是 美国 人 。 EOS
translation: 他 是 美国 人 。

BOS is breakfast UNK ? EOS
BOS 包括 早饭 吗 ? EOS
translation: 包括 早饭 吗 ?

BOS my shoulder hurts . EOS
BOS 我 肩膀 痛 。 EOS
translation: 我 肩膀 痛 。

BOS everybody thinks so . EOS
BOS 大家 都 是 这样 想 的 。 EOS
translation: 大家 都 认为 那 是 这样 的 。

BOS the bell rang . EOS
BOS 铃响 了 。 EOS
translation: 铃响 了 电话 。

BOS it 's true . EOS
BOS 这是 真的 。 EOS
translation: 这是 真的 。

BOS she fooled him . EOS
BOS 她 愚弄 了 他 。 EOS
translation: 她 愚弄 了 他 。

BOS count me in . EOS
BOS UNK 我 一个 . EOS
translation: 把 我 锁 在 一个 小偷 前面 。

BOS she kept working . EOS
BOS 她 UNK 地 工作 。 EOS
translation: 她 继续 工作 。

BOS please forgive me . EOS
BOS 请原谅 我 。 EOS
translation: 请原谅 我 。

BOS tom likes swimming . EOS
BOS 汤姆 喜欢 游泳 。 EOS
translation: 汤姆 喜欢 游泳 。

BOS i am sick . EOS
BOS 我 生病 了 。 EOS
translation: 我 被 我病 得 很 英俊 。

BOS here goes nothing . EOS
BOS UNK , 白费 UNK 。 EOS
translation: 这 就要 看 了 。

BOS hello , tom . EOS
BOS 你好 , 汤姆 。 EOS
translation: 嗨 , 汤姆 欠 汤姆 。

BOS who drew it ? EOS
BOS 谁 画 的 ? EOS
translation: 谁 画 的 画 是 谁 ?

BOS i like running . EOS
BOS 我 喜欢 跑步 。 EOS
translation: 我 喜欢 跑步 。

BOS i 'm married . EOS
BOS 我 结婚 了 。 EOS
translation: 我 已婚 。

BOS someone is watching . EOS
BOS 有人 在 UNK 。 EOS
translation: 有人 在 看 那个 人 旁边 。

BOS he looks young . EOS
BOS 他 看起来 很 年轻 。 EOS
translation: 他 看起来 像是 个 年轻 。

BOS it 's night . EOS
BOS 是 晚上 了 。 EOS
translation: 这是 一个 星期 。

BOS we are boys . EOS
BOS 我们 是 男孩 。 EOS
translation: 我们 是 男孩 。

BOS life is beautiful . EOS
BOS 生活 是 美丽 的 。 EOS
translation: 人生 是 美丽 的 。

BOS they trust tom . EOS
BOS 他们 信任 汤姆 。 EOS
translation: 他们 信任 汤姆 。

BOS who is that ? EOS
BOS 那 是 谁 ? EOS
translation: 谁 负责 那个 ?

BOS i almost drowned . EOS
BOS 我 差点 被 淹死 。 EOS
translation: 我 差点 被 淹死 了 。

BOS i was wrong . EOS
BOS 我 搞错 了 。 EOS
translation: 我 错 了 。

BOS i love you . EOS
BOS 我 爱 您 。 EOS
translation: 我 爱 你 。

BOS please sit down . EOS
BOS UNK 。 EOS
translation: 请 小心 小心 小心 。

BOS these are pens . EOS
BOS 这些 是 笔 。 EOS
translation: 这些 照片 是 蓝色 的 。

BOS are you mad ? EOS
BOS 您 生气 了 吗 ? EOS
translation: 你 生气 了 吗 ?

BOS she UNK loudly . EOS
BOS 她 大声 UNK 。 EOS
translation: 她 大声 笑 。

BOS are you ready ? EOS
BOS 你 准备 好了吗 ? EOS
translation: 你 准备 好了吗 ?

BOS he won everything . EOS
BOS 他 赢得 一切 了 。 EOS
translation: 他 赢得 一切 了 。

BOS you look UNK . EOS
BOS 你 看起来 很 紧张 。 EOS
translation: 你 看上去 紧张 。

BOS why blame tom ? EOS
BOS 为什么 责备 汤姆 ? EOS
translation: 为什么 责备 汤姆 ?

BOS please speak slowly . EOS
BOS 请 说 慢 一点 。 EOS
translation: 请 永远 航运 。

BOS they love that . EOS
BOS 他们 喜欢 那个 EOS
translation: 他们 爱 那个 事 。

BOS it 's snowing . EOS
BOS 正在 下雪 。 EOS
translation: 正在 下雪 下雪 。

BOS i borrow money . EOS
BOS 我 借钱 。 EOS
translation: 我 钱 痛 。

BOS watch your step . EOS
BOS 小心 UNK 。 EOS
translation: 小心 让 你 的 手 驾驶 。

BOS please speak slowly . EOS
BOS 请 说 慢 一些 。 EOS
translation: 请 永远 航运 。

BOS what a pity ! EOS
BOS 太 可惜 了 ! EOS
translation: 太 可惜 了 !

BOS tom sat down . EOS
BOS 汤姆 坐下 了 。 EOS
translation: 汤姆 坐在 自己 旁边 的 声音 。

BOS words express thoughts . EOS
BOS UNK UNK 。 EOS
translation: UNK UNK 。

BOS mail this letter . EOS
BOS 把 这 封信 寄 了 。 EOS
translation: 把 这 封信 寄 了 。

BOS you will fail . EOS
BOS 你 会 失败 。 EOS
translation: 你 会 失败 。

BOS please contact us . EOS
BOS 请 联系 我们 。 EOS
translation: 请 联系 我们 。

BOS UNK are sour . EOS
BOS UNK 是 酸 的 。 EOS
translation: UNK 是 酸 的 。

BOS please keep this secret . EOS
BOS 请 保守 这个 秘密 。 EOS
translation: 请 保守 这个 秘密 。

BOS do you like music ? EOS
BOS 你 爱 音乐 吗 ? EOS
translation: 你 喜欢 音乐 吗 ?

BOS are you all ready ? EOS
BOS 你们 都 准备 好了吗 ? EOS
translation: 你 全都 准备 好了吗 ?

BOS you 're really beautiful . EOS
BOS 你 真的 很漂亮 。 EOS
translation: 你 真的 很漂亮 。

BOS i have a computer . EOS
BOS 我 有 一台 电脑 。 EOS
translation: 我 有 一台 电脑 。

BOS that is your book . EOS
BOS 那 是 你 的 书 。 EOS
translation: 那 是 你 的 书 。

BOS any UNK are welcome . EOS
BOS 欢迎 作 任何 评论 。 EOS
translation: 欢迎 作 任何 评论 。

BOS we 're very different . EOS
BOS 我们 很 不 一样 。 EOS
translation: 我们 很 感激 任何 可 一样 。

BOS i 'm so excited . EOS
BOS 我 很 UNK 。 EOS
translation: 我 很 兴奋 。

BOS you have nice skin . EOS
BOS 你 的 皮肤 真 好 。 EOS
translation: 你 的 皮肤 真 好 。

BOS you 're UNK correct . EOS
BOS 你 UNK 正确 。 EOS
translation: 你 是 正确 的 。

BOS everyone admired his courage . EOS
BOS 每个 人 都 佩服 他 的 勇气 。 EOS
translation: 每个 人 都 佩服 他 的 勇气 。

BOS what time is it ? EOS
BOS 几点 了 ? EOS
translation: 它 发生 了 什么 事 ?

BOS i 'm free tonight . EOS
BOS 我 今晚 有空 。 EOS
translation: 我 今晚 有空 。

BOS here is your book . EOS
BOS 这 是 你 的 书 。 EOS
translation: 这里 的 书 是 你 的 。

BOS they are at lunch . EOS
BOS 他们 在 吃 午饭 。 EOS
translation: 他们 在 吃 午饭 。

BOS this chair is UNK . EOS
BOS 这 把 椅子 UNK 。 EOS
translation: 这 把 椅子 UNK 。

BOS it 's pretty heavy . EOS
BOS 它 UNK 。 EOS
translation: 它 被 吓 到 了 。

BOS many attended his funeral . EOS
BOS 很多 人 都 参加 了 他 的 葬礼 。 EOS
translation: 许多 人 的 葬礼 上 。

BOS training will be provided . EOS
BOS 会 有 训练 。 EOS
translation: 会 有 训练 。

BOS someone is watching you . EOS
BOS 有人 在 看着 你 。 EOS
translation: 有人 在 看着 你 。

BOS i slapped his face . EOS
BOS 我 掴 了 他 的 脸 。 EOS
translation: 我 掴 了 他 的 脸 。

BOS i like UNK music . EOS
BOS 我 喜欢 UNK 。 EOS
translation: 我 喜欢 那个 音乐 。

BOS tom had no children . EOS
BOS Tom 没有 孩子 。 EOS
translation: 汤姆 没有 孩子 。

BOS please lock the door . EOS
BOS 请 把 门锁 上 。 EOS
translation: 请 锁门 。

BOS tom has calmed down . EOS
BOS 汤姆 冷静下来 了 。 EOS
translation: 汤姆 冷静下来 了 !

BOS please speak more loudly . EOS
BOS 请 说 大声 一点儿 。 EOS
translation: 请 大声 说话 大声 一点 。

BOS keep next sunday free . EOS
BOS 把 下 周日 空 出来 。 EOS
translation: 继续 看 天气 。

BOS i made a mistake . EOS
BOS UNK 了 一个 错 。 EOS