9.5. Tradução Automática e o Conjunto de Dados¶ Open the notebook in SageMaker Studio Lab
Usamos RNNs para projetar modelos de linguagem, que são essenciais para o processamento de linguagem natural. Outro benchmark emblemático é a tradução automática, um domínio de problema central para modelos de transdução de sequência que transformam sequências de entrada em sequências de saída. Desempenhando um papel crucial em várias aplicações modernas de IA, modelos de transdução de sequência formarão o foco do restante deste capítulo e Section 10. Para este fim, esta seção apresenta o problema da tradução automática e seu conjunto de dados que será usado posteriormente.
Tradução automática refere-se ao tradução automática de uma sequência de um idioma para outro. Na verdade, este campo pode remontar a 1940 logo depois que os computadores digitais foram inventados, especialmente considerando o uso de computadores para decifrar códigos de linguagem na Segunda Guerra Mundial. Por décadas, abordagens estatísticas tinha sido dominante neste campo [Brown et al., 1988][ Brown.Cocke.Della-Pietra.ea.1990] antes da ascensão de aprendizagem ponta a ponta usando redes neurais. O último é frequentemente chamado tradução automática neural para se distinguir de tradução automática de estatística que envolve análise estatística em componentes como o modelo de tradução e o modelo de linguagem.
Enfatizando o aprendizado de ponta a ponta, este livro se concentrará em métodos de tradução automática neural. Diferente do nosso problema de modelo de linguagem in Section 8.3 cujo corpus está em um único idioma, conjuntos de dados de tradução automática são compostos por pares de sequências de texto que estão em o idioma de origem e o idioma de destino, respectivamente. Desse modo, em vez de reutilizar a rotina de pré-processamento para modelagem de linguagem, precisamos de uma maneira diferente de pré-processar conjuntos de dados de tradução automática. Na sequência, nós mostramos como carregar os dados pré-processados em minibatches para treinamento.
import os
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
import os
import torch
from d2l import torch as d2l
import os
import tensorflow as tf
from d2l import tensorflow as d2l
9.5.1. Download e Pré-processamento do Conjunto de Dados¶
Começar com, baixamos um conjunto de dados inglês-francês que consiste em pares de frases bilíngues do Projeto Tatoeba. Cada linha no conjunto de dados é um par delimitado por tabulação de uma sequência de texto em inglês e a sequência de texto traduzida em francês. Observe que cada sequência de texto pode ser apenas uma frase ou um parágrafo de várias frases. Neste problema de tradução automática onde o inglês é traduzido para o francês, Inglês é o idioma de origem e o francês é o idioma de destino.
#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')
#@save
def read_data_nmt():
"""Load the English-French dataset."""
data_dir = d2l.download_extract('fra-eng')
with open(os.path.join(data_dir, 'fra.txt'), 'r') as f:
return f.read()
raw_text = read_data_nmt()
print(raw_text[:75])
Downloading ../data/fra-eng.zip from http://d2l-data.s3-accelerate.amazonaws.com/fra-eng.zip...
Go. Va !
Hi. Salut !
Run! Cours !
Run! Courez !
Who? Qui ?
Wow! Ça alors !
#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')
#@save
def read_data_nmt():
"""Load the English-French dataset."""
data_dir = d2l.download_extract('fra-eng')
with open(os.path.join(data_dir, 'fra.txt'), 'r') as f:
return f.read()
raw_text = read_data_nmt()
print(raw_text[:75])
Downloading ../data/fra-eng.zip from http://d2l-data.s3-accelerate.amazonaws.com/fra-eng.zip...
Go. Va !
Hi. Salut !
Run! Cours !
Run! Courez !
Who? Qui ?
Wow! Ça alors !
#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')
#@save
def read_data_nmt():
"""Load the English-French dataset."""
data_dir = d2l.download_extract('fra-eng')
with open(os.path.join(data_dir, 'fra.txt'), 'r') as f:
return f.read()
raw_text = read_data_nmt()
print(raw_text[:75])
Downloading ../data/fra-eng.zip from http://d2l-data.s3-accelerate.amazonaws.com/fra-eng.zip...
Go. Va !
Hi. Salut !
Run! Cours !
Run! Courez !
Who? Qui ?
Wow! Ça alors !
Depois de baixar o conjunto de dados, continuamos com várias etapas de pré-processamento para os dados de texto brutos. Por exemplo, substituímos o espaço ininterrupto por espaço, converter letras maiúsculas em minúsculas, e insira espaço entre palavras e sinais de pontuação.
#@save
def preprocess_nmt(text):
"""Preprocess the English-French dataset."""
def no_space(char, prev_char):
return char in set(',.!?') and prev_char != ' '
# Replace non-breaking space with space, and convert uppercase letters to
# lowercase ones
text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
# Insert space between words and punctuation marks
out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
for i, char in enumerate(text)]
return ''.join(out)
text = preprocess_nmt(raw_text)
print(text[:80])
go . va !
hi . salut !
run ! cours !
run ! courez !
who ? qui ?
wow ! ça alors !
#@save
def preprocess_nmt(text):
"""Preprocess the English-French dataset."""
def no_space(char, prev_char):
return char in set(',.!?') and prev_char != ' '
# Replace non-breaking space with space, and convert uppercase letters to
# lowercase ones
text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
# Insert space between words and punctuation marks
out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
for i, char in enumerate(text)]
return ''.join(out)
text = preprocess_nmt(raw_text)
print(text[:80])
go . va !
hi . salut !
run ! cours !
run ! courez !
who ? qui ?
wow ! ça alors !
#@save
def preprocess_nmt(text):
"""Preprocess the English-French dataset."""
def no_space(char, prev_char):
return char in set(',.!?') and prev_char != ' '
# Replace non-breaking space with space, and convert uppercase letters to
# lowercase ones
text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
# Insert space between words and punctuation marks
out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
for i, char in enumerate(text)]
return ''.join(out)
text = preprocess_nmt(raw_text)
print(text[:80])
go . va !
hi . salut !
run ! cours !
run ! courez !
who ? qui ?
wow ! ça alors !
9.5.2. Tokenização¶
Diferente da tokenização em nível de personagem
in:numref:sec_language_model
, para tradução automática preferimos
tokenização em nível de palavra aqui (modelos de última geração podem
usar técnicas de tokenização mais avançadas). A seguinte função
tokenize_nmt
tokeniza os primeiros pares de sequência de texto
num_examples
, Onde cada token é uma palavra ou um sinal de
pontuação. Esta função retorna duas listas de listas de tokens:
source
etarget
. Especificamente, source [i]
é uma lista de
tokens do \(i^\mathrm{th}\) sequência de texto no idioma de origem
(inglês aqui) e target [i]
é a do idioma de destino (francês aqui).
#@save
def tokenize_nmt(text, num_examples=None):
"""Tokenize the English-French dataset."""
source, target = [], []
for i, line in enumerate(text.split('\n')):
if num_examples and i > num_examples:
break
parts = line.split('\t')
if len(parts) == 2:
source.append(parts[0].split(' '))
target.append(parts[1].split(' '))
return source, target
source, target = tokenize_nmt(text)
source[:6], target[:6]
([['go', '.'],
['hi', '.'],
['run', '!'],
['run', '!'],
['who', '?'],
['wow', '!']],
[['va', '!'],
['salut', '!'],
['cours', '!'],
['courez', '!'],
['qui', '?'],
['ça', 'alors', '!']])
#@save
def tokenize_nmt(text, num_examples=None):
"""Tokenize the English-French dataset."""
source, target = [], []
for i, line in enumerate(text.split('\n')):
if num_examples and i > num_examples:
break
parts = line.split('\t')
if len(parts) == 2:
source.append(parts[0].split(' '))
target.append(parts[1].split(' '))
return source, target
source, target = tokenize_nmt(text)
source[:6], target[:6]
([['go', '.'],
['hi', '.'],
['run', '!'],
['run', '!'],
['who', '?'],
['wow', '!']],
[['va', '!'],
['salut', '!'],
['cours', '!'],
['courez', '!'],
['qui', '?'],
['ça', 'alors', '!']])
#@save
def tokenize_nmt(text, num_examples=None):
"""Tokenize the English-French dataset."""
source, target = [], []
for i, line in enumerate(text.split('\n')):
if num_examples and i > num_examples:
break
parts = line.split('\t')
if len(parts) == 2:
source.append(parts[0].split(' '))
target.append(parts[1].split(' '))
return source, target
source, target = tokenize_nmt(text)
source[:6], target[:6]
([['go', '.'],
['hi', '.'],
['run', '!'],
['run', '!'],
['who', '?'],
['wow', '!']],
[['va', '!'],
['salut', '!'],
['cours', '!'],
['courez', '!'],
['qui', '?'],
['ça', 'alors', '!']])
Deixe-nos representar graficamente o histograma do número de tokens por sequência de texto. Neste conjunto de dados inglês-francês simples, a maioria das sequências de texto tem menos de 20 tokens.
d2l.set_figsize()
_, _, patches = d2l.plt.hist(
[[len(l) for l in source], [len(l) for l in target]],
label=['source', 'target'])
for patch in patches[1].patches:
patch.set_hatch('/')
d2l.plt.legend(loc='upper right');
d2l.set_figsize()
_, _, patches = d2l.plt.hist(
[[len(l) for l in source], [len(l) for l in target]],
label=['source', 'target'])
for patch in patches[1].patches:
patch.set_hatch('/')
d2l.plt.legend(loc='upper right');
d2l.set_figsize()
_, _, patches = d2l.plt.hist(
[[len(l) for l in source], [len(l) for l in target]],
label=['source', 'target'])
for patch in patches[1].patches:
patch.set_hatch('/')
d2l.plt.legend(loc='upper right');
9.5.3. Vocabulário¶
Uma vez que o conjunto de dados da tradução automática consiste em pares de línguas, podemos construir dois vocabulários para tanto o idioma de origem quanto o idioma de destino separadamente. Com tokenização em nível de palavra, o tamanho do vocabulário será significativamente maior do que usando tokenização em nível de caractere. Para aliviar isso, aqui tratamos tokens infrequentes que aparecem menos de 2 vezes como o mesmo token desconhecido (“<bos>”). Além disso, especificamos tokens especiais adicionais como para sequências de preenchimento (“<bos>”) com o mesmo comprimento em minibatches, e para marcar o início (“<bos>”) ou o fim (“<bos>”) das sequências. Esses tokens especiais são comumente usados em tarefas de processamento de linguagem natural.
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)
10012
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)
10012
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)
10012
9.5.4. Carregando o Conjunto de Dados¶
Lembre-se de que na modelagem de linguagem cada exemplo de sequência, ou
um segmento de uma frase ou uma extensão de várias frases, tem um
comprimento fixo. Isso foi especificado pelo num_steps
(número de
etapas de tempo ou tokens) argumento em Section 8.3.
Na tradução automática, cada exemplo é um par de sequências de texto de
origem e destino, onde cada sequência de texto pode ter comprimentos
diferentes.
Para eficiência computacional, ainda podemos processar um minibatch de
sequências de texto ao mesmo tempo por truncamento e preenchimento.
Suponha que cada sequência no mesmo minibatch deve ter o mesmo
comprimento num_steps
. Se uma sequência de texto tiver menos de
tokens num_steps
, continuaremos acrescentando a seção especial
“<pad>” símbolo ao final até que seu comprimento alcance num_steps
.
Por outro lado, vamos truncar a sequência de texto pegando apenas seus
primeiros tokens num_steps
e descartando o restante. Desta maneira,
cada sequência de texto terá o mesmo comprimento para ser carregado em
minibatches do mesmo formato.
A seguinte função truncate_pad
trunca ou preenche sequências de
texto conforme descrito anteriormente.
#@save
def truncate_pad(line, num_steps, padding_token):
"""Truncate or pad sequences."""
if len(line) > num_steps:
return line[:num_steps] # Truncate
return line + [padding_token] * (num_steps - len(line)) # Pad
truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])
[47, 4, 1, 1, 1, 1, 1, 1, 1, 1]
#@save
def truncate_pad(line, num_steps, padding_token):
"""Truncate or pad sequences."""
if len(line) > num_steps:
return line[:num_steps] # Truncate
return line + [padding_token] * (num_steps - len(line)) # Pad
truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])
[47, 4, 1, 1, 1, 1, 1, 1, 1, 1]
#@save
def truncate_pad(line, num_steps, padding_token):
"""Truncate or pad sequences."""
if len(line) > num_steps:
return line[:num_steps] # Truncate
return line + [padding_token] * (num_steps - len(line)) # Pad
truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])
[47, 4, 1, 1, 1, 1, 1, 1, 1, 1]
Agora definimos uma função para transformar sequências de texto em minibatches para treinamento. Anexamos o especial “<eos>” símbolo ao final de cada sequência para indicar o fim da sequência. Quando um modelo está prevendo de gerar um token de sequência após o token, a geração do “<eos>” símbolo pode sugerir que a sequência de saída está completa. Além do mais, nós também gravamos o comprimento de cada sequência de texto, excluindo os tokens de preenchimento. Esta informação será necessária por alguns modelos que nós cobriremos mais tarde.
#@save
def build_array_nmt(lines, vocab, num_steps):
"""Transform text sequences of machine translation into minibatches."""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['<eos>']] for l in lines]
array = np.array([truncate_pad(
l, num_steps, vocab['<pad>']) for l in lines])
valid_len = (array != vocab['<pad>']).astype(np.int32).sum(1)
return array, valid_len
#@save
def build_array_nmt(lines, vocab, num_steps):
"""Transform text sequences of machine translation into minibatches."""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['<eos>']] for l in lines]
array = torch.tensor([truncate_pad(
l, num_steps, vocab['<pad>']) for l in lines])
valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
return array, valid_len
#@save
def build_array_nmt(lines, vocab, num_steps):
"""Transform text sequences of machine translation into minibatches."""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['<eos>']] for l in lines]
array = tf.constant([truncate_pad(
l, num_steps, vocab['<pad>']) for l in lines])
valid_len = tf.reduce_sum(
tf.cast(array != vocab['<pad>'], tf.int32), 1)
return array, valid_len
9.5.5. Juntando todas as coisas¶
Finalmente, definimos a função load_data_nmt
para retornar o
iterador de dados, junto com os vocabulários do idioma de origem e do
idioma de destino.
#@save
def load_data_nmt(batch_size, num_steps, num_examples=600):
"""Return the iterator and the vocabularies of the translation dataset."""
text = preprocess_nmt(read_data_nmt())
source, target = tokenize_nmt(text, num_examples)
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
tgt_vocab = d2l.Vocab(target, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
data_iter = d2l.load_array(data_arrays, batch_size)
return data_iter, src_vocab, tgt_vocab
Vamos ler o primeiro minibatch do conjunto de dados inglês-francês.
train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
print('X:', X.astype(np.int32))
print('valid lengths for X:', X_valid_len)
print('Y:', Y.astype(np.int32))
print('valid lengths for Y:', Y_valid_len)
break
X: [[67 8 4 3 1 1 1 1]
[ 7 84 4 3 1 1 1 1]]
valid lengths for X: [4 4]
Y: [[120 28 5 3 1 1 1 1]
[ 12 16 4 3 1 1 1 1]]
valid lengths for Y: [4 4]
train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
print('X:', X.type(torch.int32))
print('valid lengths for X:', X_valid_len)
print('Y:', Y.type(torch.int32))
print('valid lengths for Y:', Y_valid_len)
break
X: tensor([[ 6, 18, 153, 4, 3, 1, 1, 1],
[166, 15, 5, 3, 1, 1, 1, 1]], dtype=torch.int32)
valid lengths for X: tensor([5, 4])
Y: tensor([[ 6, 7, 60, 4, 3, 1, 1, 1],
[ 0, 50, 174, 5, 3, 1, 1, 1]], dtype=torch.int32)
valid lengths for Y: tensor([5, 5])
train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
print('X:', tf.cast(X, tf.int32))
print('valid lengths for X:', X_valid_len)
print('Y:', tf.cast(Y, tf.int32))
print('valid lengths for Y:', Y_valid_len)
break
X: tf.Tensor(
[[ 9 28 4 3 1 1 1 1]
[69 17 9 11 3 1 1 1]], shape=(2, 8), dtype=int32)
valid lengths for X: tf.Tensor([4 5], shape=(2,), dtype=int32)
Y: tf.Tensor(
[[ 73 5 3 1 1 1 1 1]
[ 90 172 9 3 1 1 1 1]], shape=(2, 8), dtype=int32)
valid lengths for Y: tf.Tensor([3 4], shape=(2,), dtype=int32)
9.5.6. Sumário¶
Tradução automática refere-se à tradução automática de uma sequência de um idioma para outro.
Usando tokenização em nível de palavra, o tamanho do vocabulário será significativamente maior do que usando tokenização em nível de caractere. Para aliviar isso, podemos tratar tokens raros como o mesmo token desconhecido.
Podemos truncar e preencher sequências de texto para que todas tenham o mesmo comprimento para serem carregadas em minibatches.
9.5.7. Exercícios¶
Tente valores diferentes do argumento
num_examples
na funçãoload_data_nmt
. Como isso afeta os tamanhos do vocabulário do idioma de origem e do idioma de destino?O texto em alguns idiomas, como chinês e japonês, não tem indicadores de limite de palavras (por exemplo, espaço). A tokenização em nível de palavra ainda é uma boa ideia para esses casos? Por que ou por que não?