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当中,我们会(尽可能)实现 模型来完成翻译任务。
(参考论文:$Attention\; Is\; All\; You\; Need$ https://arxiv.org/pdf/1706.03762.pdf )
其中的
TODO
内容即为待完成的部分
我们的数据集非常小,只有一万多个句子的训练数据,从结果来看训练出来的模型在测试集上的表现其实已经算还可以了。
如果想得到更好的效果,则需要更大的数据量并进行更多的训练迭代次数,感兴趣(并且有硬件条件)的同学可以进行尝试。
一. 建立Transformer模型的直观认识
首先来说一下Transformer和LSTM的最大区别,就是LSTM的训练是迭代(自回归)的,是一个接一个字的来,当前这个字过完LSTM单元,才可以进下一个字,而 的训练是并行了,就是所有字是全部同时训练的,这样就大大加快了计算效率,$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. 先准备好输入的数据
# import pdb # from IPython.core.interactiveshell import InteractiveShell # InteractiveShell.ast_node_interactivity = "all"
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 = 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")
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
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
注意,上述预处理中使用的 类在后面的 内容的 部分定义
Embeddings
与其他序列传导模型类似,我们使用learned embeddings将输入标记和输出标记转换为维度 的向量。我们还使用通常学习的线性变换和softmax函数将 (解码器)的输出转换为预测的下一个标签的概率。
在我们的模型中,我们在两个embedding层和pre-softmax线性变换层之间共享相同的权重矩阵。这么做可以节省参数,也是一种正则化方式。
在其中的embedding层,我们会将这些权重乘以 。
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)
数据全部处理完成,现在我们开始理解和构建 模型
1. positional encoding(即位置嵌入或位置编码)
# 导入依赖库 import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline
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$增大,位置嵌入函数呈现不同的周期变化。
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()
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">
def clones(module, N): """ 克隆模型块,克隆的模型块参数不共享 """ return nn.ModuleList([ copy.deepcopy(module) for _ in range(N) ])
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]$, 是句长, 而一个$mini \ batch$是由多个不等长的句子组成的, 我们就需要按照这个$mini \ batch$中最大的句长对剩余的句子进行补齐长度, 我们一般用$0$来进行填充, 这个过程叫做$padding$.
但这时在进行$softmax$的时候就会产生问题, 回顾$softmax$函数$\sigma (\mathbf {z} ){i}={\frac {e^{z{i}}}{\sum {j=1}^{K}e^{z{j}}}}$, 是1, 是有值的, 这样的话$softmax$中被$padding$的部分就参与了运算, 就等于是让无效的部分参与了运算, 会产生很大隐患, 这时就需要做一个$mask$让这些无效区域不参与运算, 我们一般给无效区域加一个很大的负数的偏置, 也就是:
经过上式的$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">
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 和残差连接
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里面实现好的函数。
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$(前馈网络)层其实就是两层线性映射并用激活函数激活
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$ 由 个相同的层组成。
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)
每层 都有两个子层组成。第一个子层实现了“多头”的 ,第二个子层则是一个简单的 的全连接前馈网络。
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">
接着来看 部分(右半部分),它同样也是由 层(在论文中,仍取 )堆叠起来。
对于其中的每一层,除了与 中相同的 及 两层之外,还在中间插入了一个传统的 框架中的 层(上图中的$sub\text{-}layer\ 2$),即将 的输出作为 去查询 的输出,同样用的是 ,使得在 的时候能看到 的所有输出。
这里明确一下 $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">
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多加了一个 ,因为训练时的output都是ground truth,这样可以确保预测第 个位置时不会接触到未来的信息,具体解释如下。
对于 中 的 方式就比较简单,直接把 部分给 掉即可。
但对于 中 的 计算略微复杂一些,不仅需要把 部分 掉,还需要进行一个 的操作。
即作为 ,在预测当前步的时候,是不能知道后面的内容的,即 需要加上 ,将当前步之后的分数全部置为$-\infty$,然后再计算 ,以防止发生数据泄露。这种 的 是考虑到输出 会偏移一个位置,确保了生成位置 的预测时,仅依赖小于 的位置处的已知输出,相当于把后面不该看到的信息屏蔽掉。
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
我们可视化一下 矩阵的形式,直观进行理解。
这里的 图显示了允许每个目标词(行)查看的位置(列)。在训练期间,当前解码位置的词不能 到后续位置的词。
这里是给定一个序列长度size,生成一个下三角矩阵,在主对角线右上的都是 False,其示意图如下:
<img src="./imgs/subsequent_mask.png" width=450>
就是在decoder层的self-Attention中,由于生成 $si$ 时, $s{i+1}$ 并没有产生,所以不能有 $si$ 和 $s{i+1}$ 的关联系数,即只有下三角矩阵有系数,即下图中黄色部分
。
plt.figure(figsize=(5, 5)) plt.imshow(subsequent_mask(20)[0]) plt.show()
四. Transformer模型
最后,我们把 和 组成 模型
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)
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)
定义设置超参并连接完整模型的函数
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散度损失)实现标签平滑。
对于输出的分布,从原始的 分布转为在groundtruth上使用一个confidence值,而后其他的所有非groudtruth标签上采用 作为概率值进行平滑。
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上面的概率值。
在这里,我们可以看到标签平滑的示例。
# 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)
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]])
如果对给定的选择非常有信心,标签平滑实际上会开始惩罚模型。
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)])
[<matplotlib.lines.Line2D at 0x1ae1ad08c18>]
计算损失
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优化器
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))
主要调节是在 这个函数中,其中
- $model\size$ 即为 $d{model}$
- $warmup$ 即为 $warmup\_steps$
- $factor$ 可以理解为初始的学习率
以下对该优化器在不同模型大小($model\_size$)和不同超参数($marmup$)值的情况下的学习率($lrate$)曲线进行示例。
# 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"])
<matplotlib.legend.Legend at 0x1ae1ac797b8>
训练迭代
接下来,我们创建一个通用的训练和评分功能来跟踪损失。 我们传入一个上面定义的损失计算函数,它也处理参数更新。
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()
# 数据预处理 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")
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.
六. 模型预测
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))
# 预测 # 加载模型 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")
>>>>>>> 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