- 🍨 本文为🔗365天深度学习训练营中的学习记录博客
- 🍖 原作者:K同学啊|接辅导、项目定制
目录
- 一、课题背景和开发环境
- 二、seq2seq是什么
- 三、seq2seq原理
- 四、Attention的引入
- 参考文献
- 五、代码
一、课题背景和开发环境
📌第N5周:seq2seq详解📌
- Python 3.8.12
- numpy==1.21.5 -> 1.24.3
- pytorch==1.8.1+cu111
📌本周任务:📌
-
- 了解seq2seq是什么?
-
- 基于RNN的seq2seq模型如何处理文本/长文本序列?
-
- seq2seq模型处理长文本序列有哪些难点?
-
- 基于RNN的seq2seq模型如何结合attention来改善模型效果?
-
- 可以先尝试着自己编写代码
二、seq2seq是什么
seq2seq(sequence to sequence)是一种常见的NLP模型结构,翻译为“序列到序列”,即:从一个文本序列得到一个新的文本序列。典型的任务有:机器翻译任务,文本摘要任务。谷歌翻译在2016年末开始使用seq2seq模型,并发表了2篇开创性的论文,感兴趣的同学可以阅读原文进行学习。
- Sequence to Sequence Learning with Neural Networks
- Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation
无论是否读过上述两篇谷歌的文章,NLP初学者想要充分理解并实现seq2seq模型很不容易。因为,我们需要拆解一系列相关的NLP概念,而这些NLP概念又是是层层递进的,所以想要清晰的对seq2seq模型有一个清晰的认识并不容易。但是,如果能够把这些复杂生涩的NLP概念可视化,理解起来其实就更简单了。因此,本文希望通过一系列图片、动态图帮助NLP初学者学习seq2seq以及attention相关的概念和知识。
首先看seq2seq干了什么事情?seq2seq模型的输入可以是一个(单词、字母或者图像特征)序列,输出是另外一个(单词、字母或者图像特征)序列。一个训练好的seq2seq模型如下图所示:
如下图所示,以NLP中的机器翻译任务为例,序列指的是一连串的单词,输出也是一连串单词。
三、seq2seq原理
将上图中蓝色的seq2seq模型进行拆解,如下图所示:seq2seq模型由编码器(Encoder)和解码器(Decoder)组成。绿色的编码器会处理输入序列中的每个元素并获得输入信息,这些信息会被转换成为一个黄色的向量(称为context向量)。当我们处理完整个输入序列后,编码器把 context向量 发送给紫色的解码器,解码器通过context向量中的信息,逐个元素输出新的序列。
在机器翻译任务中,seq2seq模型实现翻译的过程如下图所示。seq2seq模型中的编码器和解码器一般采用的是循环神经网络RNN,编码器将输入的法语单词序列编码成context向量
(在绿色encoder和紫色decoder中间出现),然后解码器根据context向量
解码出英语单词序列。关于循环神经网络,建议阅读 Luis Serrano写的循环神经网络精彩介绍.(youtube网址)
黄色的context向量
本质上是一组浮点数。而这个context的数组长度是基于编码器RNN的隐藏层神经元数量的。下图展示了长度为4的context向量
,但在实际应用中,context向量
的长度是自定义的,比如可能是256,512或者1024。在下文中,我们会可视化这些数字向量,使用更明亮的色彩来表示更高的值,如下图右边所示
那么RNN是如何具体地处理输入序列的呢?
- 假设序列输入是一个句子,这个句子可以由 n n n 个词表示: s e n t e n c e = { w 1 , w 2 , . . . , w n } sentence = \{w_1,w_2,...,w_n\} sentence={w1,w2,...,wn} 。
- RNN首先将句子中的每一个词映射成为一个向量得到一个向量序列: X = { x 1 , x 2 , . . . , x n } X = \{x_1,x_2,...,x_n\} X={x1,x2,...,xn} ,每个单词映射得到的向量通常又叫做:word embedding。
- 然后在处理第 t ∈ [ 1 , n ] t\in[1,n] t∈[1,n] 个时间步的序列输入 x t x_t xt 时,RNN网络的输入和输出可以表示为: h t = R N N ( x t , h t − 1 ) h_t = RNN(x_t, h_{t-1}) ht=RNN(xt,ht−1)
- 输入:RNN在时间步 t t t 的输入之一为单词 w t w_t wt 经过映射得到的向量 x t x_t xt 。
- 输入:RNN另一个输入为上一个时间步 t − 1 t-1 t−1 得到的hidden state向量 h t − 1 h_{t-1} ht−1 ,同样是一个向量。
- 输出:RNN在时间步 t t t 的输出为 h t h_t ht hidden state向量。
我们在处理单词之前,需要将单词映射成为向量,通常使用 word embedding 算法来完成。一般来说,我们可以使用提前训练好的 word embeddings,或者在自有的数据集上训练word embedding。为了简单起见,上图展示的word embedding维度是4。上图左边每个单词经过word embedding算法之后得到中间一个对应的4维的向量。
进一步可视化一下基于RNN的seq2seq模型中的编码器在第1个时间步是如何工作:
RNN在第2个时间步,采用第1个时间步得到hidden state#10(隐藏层状态)和第2个时间步的输入向量input#1,来得到新的输出hidden state#1。
看下面的动态图,详细观察一下编码器如何在每个时间步得到hidden sate,并将最终的hidden state传输给解码器,解码器根据编码器所给予的最后一个hidden state信息解码处输出序列。注意,最后一个 hidden state实际上是我们上文提到的context向量
。编码器逐步得到hidden state并传输最后一个hidden state给解码器。
接着,结合编码器处理输入序列,一起来看下解码器如何一步步得到输出序列。与编码器类似,解码器在每个时间步也会得到 hidden state(隐藏层状态),而且也需要把 hidden state(隐藏层状态)从一个时间步传递到下一个时间步。编码器首先按照时间步依次编码每个法语单词,最终将最后一个hidden state也就是context向量
传递给解码器,解码器根据context向量
逐步解码得到英文输出。
四、Attention的引入
基于RNN的seq2seq模型编码器所有信息都编码到了一个context向量
中,便是这类模型的瓶颈。一方面单个向量很难包含所有文本序列的信息,另一方面RNN递归地编码文本序列使得模型在处理长文本时面临非常大的挑战(比如RNN处理到第500个单词的时候,很难再包含1-499个单词中的所有信息了)。
面对以上问题,Bahdanau等2014发布的 Neural Machine Translation by Jointly Learning to Align and Translate 和 Luong等2015年发布的 Effective Approaches to Attention-based Neural Machine Translation 两篇论文中,提出了一种叫做注意力attetion的技术。通过attention技术,seq2seq模型极大地提高了机器翻译的质量。归其原因是:attention注意力机制,使得seq2seq模型可以有区分度、有重点地关注输入序列。
下图依旧是机器翻译的例子:
在第 7 个时间步,注意力机制使得解码器在产生英语翻译student英文翻译之前,可以将注意力集中在法语输入序列的:étudiant。
继续来理解带有注意力的seq2seq模型:一个注意力模型与经典的seq2seq模型主要有2点不同:
- 首先,编码器会把更多的数据传递给解码器。编码器把所有时间步的 hidden state(隐藏层状态)传递给解码器,而不是只传递最后一个 hidden state(隐藏层状态),如下面的动态图所示:
- 注意力模型的解码器在产生输出之前,做了一个额外的attention处理。如下图所示,具体为:
a. 由于编码器中每个 hidden state(隐藏层状态)都对应到输入句子中一个单词,那么解码器要查看所有接收到的编码器的 hidden state(隐藏层状态)。
b. 给每个 hidden state(隐藏层状态)计算出一个分数(我们先忽略这个分数的计算过程)。
c. 所有hidden state(隐藏层状态)的分数经过softmax进行归一化。
d. 将每个 hidden state(隐藏层状态)乘以所对应的分数,从而能够让高分对应的 hidden state(隐藏层状态)会被放大,而低分对应的 hidden state(隐藏层状态)会被缩小。
e. 将所有hidden state根据对应分数进行加权求和,得到对应时间步的context向量。
所以,attention可以简单理解为:一种有效的加权求和技术,其艺术在于如何获得权重。
现在,把所有内容都融合到下面的图中,来看看结合注意力的seq2seq模型解码器全流程,动态图展示的是第4个时间步:
1. 注意力模型的解码器 RNN 的输入包括:一个word embedding 向量,和一个初始化好的解码器 hidden state,图中是 h i n i t h_{init} hinit 。
2. RNN 处理上述的 2 个输入,产生一个输出和一个新的 hidden state,图中为h4。
3. 注意力的步骤:我们使用编码器的所有 hidden state向量和 h4 向量来计算这个时间步的context向量(C4)。
4. 再把 h4 和 C4 拼接起来,得到一个橙色向量。
5. 我们把这个橙色向量输入一个前馈神经网络(这个网络是和整个模型一起训练的)。
6. 根据前馈神经网络的输出向量得到输出单词:假设输出序列可能的单词有N个,那么这个前馈神经网络的输出向量通常是N维的,每个维度的下标对应一个输出单词,每个维度的数值对应的是该单词的输出概率。
7. 在下一个时间步重复1-6步骤。
最后,可视化一下注意力机制,看看在解码器在每个时间步关注了输入序列的哪些部分:
需要注意的是:注意力模型不是无意识地把输出的第一个单词对应到输入的第一个单词,它是在训练阶段学习到如何对两种语言的单词进行对应(在本文的例子中,是法语和英语)。
下图还展示了注意力机制的准确程度(图片来自于上面提到的论文):
可以看到模型在输出 “European Economic Area” 时,注意力分布情况。在法语中,这些单词的顺序,相对于英语,是颠倒的(“européenne économique zone”)。而其他词的顺序是类似的。
参考文献
- https://jalammar.github.io/visualizing-neural-machine-translation-mechanics-of-seq2seq-models-with-attention/
- Sequence to Sequence Learning with Neural Networks
- Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation
- Neural Machine Translation by Jointly Learning to Align and Translate
- Effective Approaches to Attention-based Neural Machine Translation
五、代码
数据集(data/eng-fra.txt)
参考代码
import re
import time
import math
import torch
import string
import random
import warnings
import numpy as np
import unicodedata
from io import open
from torch import nn
from torch import optim
import torch.nn.functional as F
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from torch.autograd import VariableSOS_token = 0
EOS_token = 1
MAX_LENGTH = 20
eng_prefixes = ("i am ", "i m ","he is", "he s ","she is", "she s ","you are", "you re ","we are", "we re ","they are", "they re "
)
teacher_forcing_ratio = 0.5
plt.switch_backend('agg')warnings.filterwarnings("ignore") #忽略警告信息
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print('device =', device)class Lang:def __init__(self, name):self.name = nameself.word2index = {}self.word2count = {}self.index2word = {0: "SOS", 1: "EOS"}self.n_words = 2 # Count SOS and EOSdef addSentence(self, sentence):for word in sentence.split(' '):self.addWord(word)def addWord(self, word):if word not in self.word2index:self.word2index[word] = self.n_wordsself.word2count[word] = 1self.index2word[self.n_words] = wordself.n_words += 1else:self.word2count[word] += 1# Turn a Unicode string to plain ASCII
def unicodeToAscii(s):return ''.join(c for c in unicodedata.normalize('NFD', s)if unicodedata.category(c) != 'Mn')# Lowercase, trim, and remove non-letter characters
def normalizeString(s):s = unicodeToAscii(s.lower().strip())s = re.sub(r"([.!?])", r" \1", s)s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)return sdef readLangs(lang1, lang2, reverse=False):print("Reading lines...")# Read the file and split into lineslines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8').read().strip().split('\n')# Split every line into pairs and normalizepairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]# Reverse pairs, make Lang instancesif reverse:pairs = [list(reversed(p)) for p in pairs]input_lang = Lang(lang2)output_lang = Lang(lang1)else:input_lang = Lang(lang1)output_lang = Lang(lang2)return input_lang, output_lang, pairsdef filterPair(p):return len(p[0].split(' ')) < MAX_LENGTH and \len(p[1].split(' ')) < MAX_LENGTH and \p[1].startswith(eng_prefixes)def filterPairs(pairs):return [pair for pair in pairs if filterPair(pair)]def prepareData(lang1, lang2, reverse=False):input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)print("Read %s sentence pairs" % len(pairs))pairs = filterPairs(pairs)print("Trimmed to %s sentence pairs" % len(pairs))print("Counting words...")for pair in pairs:input_lang.addSentence(pair[0])output_lang.addSentence(pair[1])print("Counted words:")print(input_lang.name, input_lang.n_words)print(output_lang.name, output_lang.n_words)return input_lang, output_lang, pairsdef indexesFromSentence(lang, sentence):return [lang.word2index[word] for word in sentence.split(' ')]def tensorFromSentence(lang, sentence):indexes = indexesFromSentence(lang, sentence)indexes.append(EOS_token)return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)def tensorsFromPair(pair):input_tensor = tensorFromSentence(input_lang, pair[0])target_tensor = tensorFromSentence(output_lang, pair[1])return (input_tensor, target_tensor)def showPlot(points):plt.figure()fig, ax = plt.subplots()# this locator puts ticks at regular intervalsloc = ticker.MultipleLocator(base=0.2)ax.yaxis.set_major_locator(loc)plt.plot(points)class EncoderRNN(nn.Module):# input_size为输入语言包含的词个数def __init__(self, input_size, hidden_size):super(EncoderRNN, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.embedding = nn.Embedding(self.input_size, self.hidden_size) # 每词hidden_size个属性self.gru = nn.GRU(self.hidden_size, self.hidden_size)def forward(self, x, hidden):embedded = self.embedding(x).view(1, 1, -1)output = embeddedoutput, hidden = self.gru(output, hidden)return output, hiddendef initHidden(self):result = Variable(torch.zeros(1, 1, self.hidden_size, device=device))return resultclass AttnDecoderRNN(nn.Module):# output_size为输出语言包含的所有单词数def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):super(AttnDecoderRNN, self).__init__()self.hidden_size = hidden_sizeself.output_size = output_sizeself.dropout_p = dropout_pself.max_length = max_lengthself.embedding = nn.Embedding(self.output_size, self.hidden_size)self.attn = nn.Linear(self.hidden_size*2, self.max_length)self.attn_combine = nn.Linear(self.hidden_size*2, self.hidden_size)self.dropout = nn.Dropout(self.dropout_p)self.gru = nn.GRU(self.hidden_size, self.hidden_size)self.out = nn.Linear(self.hidden_size, self.output_size) # 把hidden_size个特征转换成输出语言的词汇个数# x为每步输入,hidden为上一步结果,encoder_outputs编码的状态矩阵计算的值是各词出现的概率def forward(self, x, hidden, encoder_outputs):embedded = self.embedding(x).view(1, 1, -1)embedded = self.dropout(embedded)attn_weights = F.softmax(self.attn(torch.cat([embedded[0], hidden[0]], 1)), dim=1)attn_applied = torch.bmm(attn_weights.unsqueeze(0), # unsqueeze维度增加encoder_outputs.unsqueeze(0))output = torch.cat([embedded[0], attn_applied[0]], 1) # 注意力与当前输入拼接output = self.attn_combine(output).unsqueeze(0)output = F.relu(output) # 激活函数output, hidden = self.gru(output, hidden)output = F.log_softmax(self.out(output[0]), dim=1)return output, hidden, attn_weightsdef initHidden(self):result = Variable(torch.zeros(1, 1, self.hidden_size))return resultdef train(input_tensor,target_tensor,encoder,decoder,encoder_optimizer,decoder_optimizer,criterion,max_length = MAX_LENGTH):encoder_hidden = encoder.initHidden()# 分别优化encoder和decoderencoder_optimizer.zero_grad()decoder_optimizer.zero_grad()input_length = input_tensor.size(0)target_length = target_tensor.size(0)encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)loss = 0for ei in range(input_length): # 每次传入序列中一个元素encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)encoder_outputs[ei]=encoder_output[0, 0] # seq_len为1,batch_size为1,大小为 hidden_sizedecoder_input = torch.tensor([[SOS_token]], device=device) # SOS为标记句首decoder_hidden = encoder_hidden # 把编码的最终状态作为解码的初始状态use_teacher_forcing = True if random.random() < teacher_forcing_ratio else Falseif use_teacher_forcing:# Teacher forcing: Feed the target as the next inputfor di in range(target_length):decoder_output, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_hidden, encoder_outputs)loss += criterion(decoder_output, target_tensor[di])decoder_input = target_tensor[di] # Teacher forcingelse:# Without teacher forcing: use its own predictions as the next inputfor di in range(target_length): # 每次预测一个元素decoder_output, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_hidden, encoder_outputs)topv, topi = decoder_output.topk(1) # 将可能性最大的预测值加入译文序列decoder_input = topi.squeeze().detach() # detach from history as inputloss += criterion(decoder_output, target_tensor[di])if decoder_input.item()==EOS_token:breakloss.backward()encoder_optimizer.step()decoder_optimizer.step()return loss.item() / target_lengthdef evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):with torch.no_grad():input_tensor = tensorFromSentence(input_lang, sentence)input_length = input_tensor.size()[0]encoder_hidden = encoder.initHidden()encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)for ei in range(input_length):encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)encoder_outputs[ei] += encoder_output[0, 0]decoder_input = torch.tensor([[SOS_token]], device=device) # SOSdecoder_hidden = encoder_hiddendecoded_words = []decoder_attentions = torch.zeros(max_length, max_length)for di in range(max_length):decoder_output, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_hidden, encoder_outputs)decoder_attentions[di] = decoder_attention.datatopv, topi = decoder_output.data.topk(1)if topi.item() == EOS_token:decoded_words.append('<EOS>')breakelse:decoded_words.append(output_lang.index2word[topi.item()])decoder_input = topi.squeeze().detach()return decoded_words, decoder_attentions[:di+1]def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):start = time.time()plot_losses = []print_loss_total = 0 # Reset every print_everyplot_loss_total = 0 # Reset every plot_everyencoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)training_pairs = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]criterion = nn.NLLLoss()for iter in range(1, n_iters+1):training_pair = training_pairs[iter - 1]input_tensor = training_pair[0]target_tensor = training_pair[1]loss = train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion)print_loss_total += lossplot_loss_total += lossif iter % print_every == 0:print_loss_avg = print_loss_total / print_everyprint_loss_total = 0print('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters), iter, iter / n_iters * 100, print_loss_avg))if iter % plot_every == 0:plot_loss_avg = plot_loss_total / plot_everyplot_losses.append(plot_loss_avg)plot_loss_total = 0showPlot(plot_losses)def evaluateRandomly(encoder, decoder, n=10):for i in range(n):pair = random.choice(pairs)print('>', pair[0])print('=', pair[1])output_words, attentions = evaluate(encoder, decoder, pair[0])output_sentence = ' '.join(output_words)print('<', output_sentence)print('')def asMinutes(s):m = math.floor(s / 60)s -= m * 60return '%dm %ds' % (m, s)def timeSince(since, percent):now = time.time()s = now - sincees = s / (percent)rs = es - sreturn '%s (- %s)' % (asMinutes(s), asMinutes(rs))if __name__=='__main__':input_lang, output_lang, pairs = prepareData('eng', 'fra', True)print(random.choice(pairs))hidden_size = 256encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)# TraintrainIters(encoder1, attn_decoder1, 500, print_every=10, plot_every=10)# EvaluateevaluateRandomly(encoder1, attn_decoder1)# Predictoutput_words, attentions = evaluate(encoder1, attn_decoder1, "je suis trop froid .")plt.matshow(attentions.numpy())
device = cuda
Reading lines...
Read 135842 sentence pairs
Trimmed to 13033 sentence pairs
Counting words...
Counted words:
fra 5143
eng 3371
['vous devrez etre confrontes a ca .', 'you re going to have to deal with that .']
0m 3s (- 2m 39s) (10 2%) 7.3598
0m 3s (- 1m 30s) (20 4%) 5.4972
0m 4s (- 1m 4s) (30 6%) 5.2532
0m 4s (- 0m 49s) (40 8%) 3.9771
0m 4s (- 0m 40s) (50 10%) 5.2067
0m 4s (- 0m 34s) (60 12%) 3.5993
0m 4s (- 0m 29s) (70 14%) 4.0403
0m 5s (- 0m 26s) (80 16%) 4.4003
0m 5s (- 0m 23s) (90 18%) 3.8084
0m 5s (- 0m 21s) (100 20%) 3.5550
0m 5s (- 0m 19s) (110 22%) 3.9245
0m 5s (- 0m 18s) (120 24%) 3.7573
0m 6s (- 0m 17s) (130 26%) 3.8793
0m 6s (- 0m 15s) (140 28%) 3.8074
0m 6s (- 0m 14s) (150 30%) 2.9121
0m 6s (- 0m 13s) (160 32%) 3.5292
0m 6s (- 0m 13s) (170 34%) 4.1619
0m 7s (- 0m 12s) (180 36%) 4.2166
0m 7s (- 0m 11s) (190 38%) 3.4996
0m 7s (- 0m 11s) (200 40%) 3.2826
0m 7s (- 0m 10s) (210 42%) 3.6809
0m 7s (- 0m 9s) (220 44%) 3.3791
0m 8s (- 0m 9s) (230 46%) 3.5767
0m 8s (- 0m 8s) (240 48%) 3.8963
0m 8s (- 0m 8s) (250 50%) 3.1784
0m 8s (- 0m 7s) (260 52%) 2.8582
0m 8s (- 0m 7s) (270 54%) 3.4515
0m 9s (- 0m 7s) (280 56%) 3.7099
0m 9s (- 0m 6s) (290 57%) 3.6674
0m 9s (- 0m 6s) (300 60%) 3.4770
0m 9s (- 0m 5s) (310 62%) 3.3138
0m 9s (- 0m 5s) (320 64%) 3.6317
0m 10s (- 0m 5s) (330 66%) 3.3422
0m 10s (- 0m 4s) (340 68%) 3.7604
0m 10s (- 0m 4s) (350 70%) 4.2272
0m 10s (- 0m 4s) (360 72%) 2.9845
0m 10s (- 0m 3s) (370 74%) 3.7715
0m 11s (- 0m 3s) (380 76%) 3.2414
0m 11s (- 0m 3s) (390 78%) 3.3848
0m 11s (- 0m 2s) (400 80%) 3.0122
0m 11s (- 0m 2s) (410 82%) 3.5536
0m 11s (- 0m 2s) (420 84%) 3.5114
0m 12s (- 0m 1s) (430 86%) 3.2570
0m 12s (- 0m 1s) (440 88%) 2.8570
0m 12s (- 0m 1s) (450 90%) 3.4149
0m 12s (- 0m 1s) (460 92%) 3.0995
0m 12s (- 0m 0s) (470 94%) 3.6943
0m 13s (- 0m 0s) (480 96%) 3.7161
0m 13s (- 0m 0s) (490 98%) 3.3055
0m 13s (- 0m 0s) (500 100%) 3.6191
> tu es tres serviable .
= you re very helpful .
< i m not to to . <EOS>> vous etes tous prets .
= you re all set .
< i m not to . <EOS>> je ne suis pas occupe pour le moment .
= i m not busy right now .
< i m not to to . <EOS>> nous sommes remues .
= we re shaken .
< i m not to . <EOS>> on le dit decede .
= he is said to have died .
< i m not to to . <EOS>> je suis familier de sa musique .
= i am familiar with his music .
< i m not to to . <EOS>> nous sommes tristes .
= we re sad .
< i m not to . <EOS>> nous n en avons pas encore termine .
= we re not done yet .
< i m not to to . <EOS>> tu es le plus bel homme qu il m ait ete donne de voir .
= you re the most handsome man i ve ever seen .
< i m not to to . <EOS>> tu as toujours quelque chose a me reprocher .
= you are always finding fault with me .
< i m not to to . <EOS>