9.5 Машинный перевод и набор данных
Языковые модели — ключ к обработке естественного языка, а машинный перевод (machine translation) — самый успешный эталон для их проверки. Машинный перевод — это центральная задача моделей преобразования последовательностей (sequence transduction), которые превращают входную последовательность в выходную. Такие модели играют решающую роль во многих современных приложениях искусственного интеллекта, поэтому им будут посвящены оставшаяся часть этой главы и глава 10. В этом разделе мы опишем задачу машинного перевода и набор данных, который понадобится дальше.
Машинный перевод (machine translation) — это автоматический перевод последовательности с одного языка на другой. Эта область восходит к 1940-м годам, сразу после появления цифровых компьютеров, когда их использовали для взлома языковых шифров во время Второй мировой войны. На протяжении десятилетий, до появления сквозного обучения с помощью нейронных сетей, доминировали статистические методы (Brown et al., 1988; Brown et al., 1990). Поскольку статистический машинный перевод (statistical machine translation) включает статистический анализ таких компонентов, как модель перевода и языковая модель, подходы на основе нейронных сетей часто называют нейронным машинным переводом (neural machine translation), чтобы отличать их от статистических.
В этой книге мы сосредоточимся на нейронных методах машинного перевода, подчёркивающих сквозное обучение. В отличие от задачи языкового моделирования, где корпус состоит из текста на одном языке (см. раздел 8.3), набор данных для машинного перевода состоит из пар текстовых последовательностей на исходном и целевом языках. Поэтому нам нужен совсем другой подход к предобработке набора данных для машинного перевода, а не повторное использование предобработки для языковых моделей. Далее посмотрим, как загрузить предобработанные данные в минибатчи для обучения.
MXNet:
from d2l import mxnet as d2l
from mxnet import np, npx
import os
npx.set_np()
PyTorch:
from d2l import torch as d2l
import torch
import os
TensorFlow:
from d2l import tensorflow as d2l
import tensorflow as tf
import os
PaddlePaddle:
from d2l import paddle as d2l
import warnings
warnings.filterwarnings("ignore")
import paddle
import os
9.5.1 Загрузка и предобработка набора данных
Сначала загрузим набор данных «английский — французский», состоящий из двуязычных пар предложений проекта Tatoeba. Каждая строка набора — пара текстовых последовательностей, разделённых табуляцией: английская последовательность и её французский перевод. Заметим, что каждая текстовая последовательность может быть как одним предложением, так и абзацем из нескольких предложений. В этой задаче перевода с английского на французский английский — исходный язык (source language), а французский — целевой язык (target language).
#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')
#@save
def read_data_nmt():
"""Загружает набор данных «английский — французский»"""
data_dir = d2l.download_extract('fra-eng')
with open(os.path.join(data_dir, 'fra.txt'), 'r',
encoding='utf-8') as f:
return f.read()
raw_text = read_data_nmt()
print(raw_text[:75])
После загрузки набора данных исходный текст проходит через несколько шагов предобработки. Например, мы заменяем неразрывные пробелы (non-breaking space) обычными пробелами, заменяем заглавные буквы строчными и вставляем пробелы между словами и знаками препинания.
#@save
def preprocess_nmt(text):
"""Предобрабатывает набор данных «английский — французский»"""
def no_space(char, prev_char):
return char in set(',.!?') and prev_char != ' '
# Заменяем неразрывные пробелы обычными пробелами
# Заменяем заглавные буквы строчными
text = text.replace(' ', ' ').replace('\xa0', ' ').lower()
# Вставляем пробелы между словами и знаками препинания
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])
9.5.2 Токенизация
В отличие от посимвольной токенизации из раздела 8.3, в машинном переводе мы предпочитаем токенизацию по словам (самые современные модели могут использовать более сложные методы токенизации). Функция tokenize_nmt ниже токенизирует первые num_examples пар текстовых последовательностей. Каждый токен — это слово или знак препинания. Функция возвращает два списка токенов: source и target. source[i] — это список токенов -й текстовой последовательности исходного языка (здесь английского), а target[i] — список токенов -й текстовой последовательности целевого языка (здесь французского).
#@save
def tokenize_nmt(text, num_examples=None):
"""Токенизирует набор данных «английский — французский»"""
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]
Построим гистограмму числа токенов в каждой текстовой последовательности. В этом простом наборе данных «английский — французский» у большинства текстовых последовательностей меньше токенов.
#@save
def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):
"""Строит гистограмму длин пар списков"""
d2l.set_figsize()
_, _, patches = d2l.plt.hist(
[[len(l) for l in xlist], [len(l) for l in ylist]])
d2l.plt.xlabel(xlabel)
d2l.plt.ylabel(ylabel)
for patch in patches[1].patches:
patch.set_hatch('/')
d2l.plt.legend(legend)
show_list_len_pair_hist(['source', 'target'], '# tokens per sequence',
'count', source, target);
9.5.3 Словарь
Поскольку набор данных для машинного перевода состоит из пар языков, мы можем построить два словаря: один для исходного языка и один для целевого. При пословной токенизации словарь получается заметно больше, чем при посимвольной. Чтобы смягчить эту проблему, здесь мы будем считать редкие токены, которые встречаются меньше 2 раз, одним и тем же неизвестным токеном <unk>. Кроме того, мы задаём дополнительные служебные токены: токен заполнения <pad>, который используется для выравнивания длины последовательностей в минибатче, а также токены начала последовательности <bos> и конца последовательности <eos>. Эти служебные токены часто используются в задачах обработки естественного языка.
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)
9.5.4 Загрузка набора данных
Напомним, что в языковой модели последовательности-примеры имеют фиксированную длину, независимо от того, являются ли они частью предложения или фрагментом, охватывающим несколько предложений. Эта длина задаётся параметром num_steps (число временных шагов, или число токенов) из раздела 8.3. В машинном переводе каждый пример — это пара текстовых последовательностей, исходной и целевой, и длина каждой из них может быть разной.
Чтобы повысить эффективность вычислений, мы по-прежнему можем обрабатывать минибатчи текстовых последовательностей с помощью усечения (truncation) и дополнения (padding). Пусть все последовательности одного минибатча имеют одну и ту же длину num_steps. Если в последовательности меньше num_steps токенов, мы дописываем в её конец служебный токен <pad>, пока её длина не станет равна num_steps. Если же последовательность длиннее, мы усекаем её, оставляя первые num_steps токенов, а остальные отбрасываем. Так каждая текстовая последовательность получает одинаковую длину и может загружаться минибатчами одной формы.
Как и раньше, функция truncate_pad ниже обрезает или дополняет текстовую последовательность.
#@save
def truncate_pad(line, num_steps, padding_token):
"""Обрезает или дополняет текстовую последовательность"""
if len(line) > num_steps:
return line[:num_steps] # Обрезка
return line + [padding_token] * (num_steps - len(line)) # Дополнение
truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])
Теперь определим функцию, которая преобразует текстовые последовательности в минибатч для обучения. Мы добавляем служебный токен <eos> в конец каждой последовательности, чтобы обозначить её окончание. Когда модель генерирует последовательность токен за токеном, появление <eos> означает, что вывод завершён. Кроме того, мы записываем длину каждой текстовой последовательности, не учитывая токены заполнения: некоторые модели, которые мы рассмотрим позже, используют эту информацию.
#@save
def build_array_nmt(lines, vocab, num_steps):
"""Преобразует текстовые последовательности машинного перевода в минибатч"""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['<eos>']] for l in lines]
array = d2l.tensor([truncate_pad(
l, num_steps, vocab['<pad>']) for l in lines])
valid_len = d2l.reduce_sum(
d2l.astype(array != vocab['<pad>'], d2l.int32), 1)
return array, valid_len
9.5.5 Обучение модели
Наконец, определим функцию load_data_nmt, которая возвращает итератор по данным, а также словари для исходного и целевого языков.
#@save
def load_data_nmt(batch_size, num_steps, num_examples=600):
"""Возвращает итератор по набору данных для перевода и словари"""
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
Теперь прочитаем первый минибатч из набора данных «английский — французский».
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:', d2l.astype(X, d2l.int32))
print('действительная длина X:', X_valid_len)
print('Y:', d2l.astype(Y, d2l.int32))
print('действительная длина Y:', Y_valid_len)
break
Резюме
- Машинный перевод — это автоматический перевод текстовой последовательности с одного языка на другой.
- При пословной токенизации словарь заметно больше, чем при посимвольной. Чтобы его уменьшить, можно считать редкие токены одним и тем же неизвестным токеном.
- Усечение и дополнение текстовых последовательностей позволяют сделать все последовательности одинаковой длины и загружать их минибатчами.
Упражнения
- Попробуйте разные значения параметра
num_examplesв функцииload_data_nmt. Как это влияет на размеры словарей исходного и целевого языков? - В некоторых языках (например, китайском и японском) в тексте нет границ слов, таких как пробелы. Хорошая ли идея использовать пословную токенизацию в таком случае? Почему?
MXNet: Обсуждение
PyTorch: Обсуждение
PaddlePaddle: Обсуждение
Литература
- Brown, P. F., Cocke, J., Della Pietra, S. A., Della Pietra, V. J., Jelinek, F., Mercer, R. L., & Roossin, P. (1988). A statistical approach to language translation. Coling Budapest 1988 Volume 1: International Conference on Computational Linguistics.
- Brown, P. F., Cocke, J., Della Pietra, S. A., Della Pietra, V. J., Jelinek, F., Lafferty, J., Mercer, R. L., & Roossin, P. S. (1990). A statistical approach to machine translation. Computational Linguistics, 16(2), 79–85.