.. _sec_machine_translation: Машинный перевод и набор данных =============================== Среди ключевых прорывов, вызвавших широкий интерес к современным рекуррентным нейронным сетям (RNN), было значительное достижение в прикладной области статистического машинного перевода. В этой задаче модель получает на вход предложение на одном языке и должна предсказать соответствующее предложение на другом. Обратите внимание, что предложения могут иметь разную длину, а соответствующие слова в двух предложениях могут располагаться в разном порядке из-за различий в грамматической структуре языков. Многие задачи имеют схожую природу: они сводятся к отображению между двумя такими «невыровненными» последовательностями. Примерами служат преобразование диалоговых реплик в ответы или вопросов в ответы. В целом такие задачи называются задачами *последовательность-в-последовательность* (seq2seq), и они являются основным объектом нашего внимания как на протяжении оставшейся части этой главы, так и в значительной части :numref:`chap_attention-and-transformers`. В этом разделе мы рассмотрим задачу машинного перевода и пример набора данных, который будем использовать в последующих примерах. На протяжении десятилетий популярными оставались статистические формулировки перевода между языками :cite:`Brown.Cocke.Della-Pietra.ea.1988,Brown.Cocke.Della-Pietra.ea.1990`, даже до того, как исследователи реализовали подходы на основе нейронных сетей (методы часто объединяли под термином *нейронный машинный перевод*). Сначала нам потребуется новый код для обработки данных. В отличие от языкового моделирования, которое мы рассматривали в :numref:`sec_language-model`, здесь каждый пример состоит из двух отдельных текстовых последовательностей: одна на исходном языке, а другая (перевод) — на целевом языке. Приведённые ниже фрагменты кода показывают, как загрузить предварительно обработанные данные в мини-пакеты для обучения. .. raw:: latex \diilbookstyleinputcell .. code:: python import os import torch from d2l import torch as d2l Загрузка и предобработка набора данных -------------------------------------- Для начала скачаем англо-французский набор данных, состоящий из `двуязычных пар предложений из проекта Tatoeba `__. Каждая строка набора данных представляет собой пару, разделённую табуляцией, содержащую английскую текстовую последовательность (источник) и переведённую на французский язык текстовую последовательность (целевая переменная). Обратите внимание, что каждая текстовая последовательность может состоять как из одного предложения, так и из абзаца, содержащего несколько предложений. .. raw:: latex \diilbookstyleinputcell .. code:: python class MTFraEng(d2l.DataModule): #@save """The English-French dataset.""" def _download(self): d2l.extract(d2l.download( d2l.DATA_URL+'fra-eng.zip', self.root, '94646ad1522d915e7b0f9296181140edcf86a4f5')) with open(self.root + '/fra-eng/fra.txt', encoding='utf-8') as f: return f.read() .. raw:: latex \diilbookstyleinputcell .. code:: python data = MTFraEng() raw_text = data._download() print(raw_text[:75]) После загрузки набора данных мы переходим к нескольким шагам предварительной обработки исходных текстовых данных. Например, мы заменяем неразрывные пробелы на обычные, преобразуем заглавные буквы в строчные и добавляем пробелы между словами и знаками препинания. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(MTFraEng) #@save def _preprocess(self, text): # Replace non-breaking space with space text = text.replace('\u202f', ' ').replace('\xa0', ' ') # Insert space between words and punctuation marks no_space = lambda char, prev_char: char in ',.!?' and prev_char != ' ' out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char for i, char in enumerate(text.lower())] return ''.join(out) .. raw:: latex \diilbookstyleinputcell .. code:: python text = data._preprocess(raw_text) print(text[:80]) Токенизация ----------- В отличие от посимвольной токенизации в :numref:`sec_language-model`, для машинного перевода здесь мы предпочитаем пословную токенизацию (современные модели передового уровня используют более сложные методы токенизации). Приведённый ниже метод ``_tokenize`` выполняет токенизацию первых ``max_examples`` пар текстовых последовательностей, где каждый токен представляет собой либо слово, либо знак препинания. Мы добавляем специальный токен «» в конец каждой последовательности, чтобы обозначить её конец. Когда модель генерирует предсказание, создавая последовательность токен за токеном, появление токена «» может сигнализировать о том, что выходная последовательность завершена. В результате описанный ниже метод возвращает два списка списков токенов: ``src`` и ``tgt``. Конкретно, ``src[i]`` — это список токенов из исходной текстовой последовательности :math:`i^\textrm{th}` на языке источника (в данном случае английском), а ``tgt[i]`` — список токенов на целевом языке (в данном случае французском). .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(MTFraEng) #@save def _tokenize(self, text, max_examples=None): src, tgt = [], [] for i, line in enumerate(text.split('\n')): if max_examples and i > max_examples: break parts = line.split('\t') if len(parts) == 2: # Skip empty tokens src.append([t for t in f'{parts[0]} '.split(' ') if t]) tgt.append([t for t in f'{parts[1]} '.split(' ') if t]) return src, tgt .. raw:: latex \diilbookstyleinputcell .. code:: python src, tgt = data._tokenize(text) src[:6], tgt[:6] Давайте построим гистограмму количества токенов в каждой текстовой последовательности. В этом простом наборе данных для английского и французского языков большинство текстовых последовательностей содержат менее 20 токенов. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist): """Plot the histogram for list length pairs.""" 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) .. raw:: latex \diilbookstyleinputcell .. code:: python show_list_len_pair_hist(['source', 'target'], '# tokens per sequence', 'count', src, tgt); .. _subsec_loading-seq-fixed-len: Загрузка последовательностей фиксированной длины ------------------------------------------------ Вспомним, что в языковом моделировании **каждая последовательность примеров**, будь то фрагмент одного предложения или отрезок, охватывающий несколько предложений, имела фиксированную длину. Это задавалось аргументом ``num_steps`` (количество временных шагов или токенов) из :numref:`sec_language-model`. В машинном переводе каждый пример представляет собой пару исходной и целевой текстовых последовательностей, при этом две текстовые последовательности могут иметь различную длину. Для повышения вычислительной эффективности мы по-прежнему можем обрабатывать мини-батч текстовых последовательностей за один раз с помощью *обрезки* и *дополнения*. Предположим, что все последовательности в одном мини-батче должны иметь одинаковую длину ``num_steps``. Если текстовая последовательность содержит менее ``num_steps`` токенов, мы будем добавлять специальный токен “” в конец до тех пор, пока её длина не достигнет ``num_steps``. В противном случае мы обрежем текстовую последовательность, оставив только первые ``num_steps`` токенов и отбросив остальные. Таким образом, каждая текстовая последовательность будет иметь одинаковую длину, что позволит загружать их в мини-батчи одинаковой формы. Кроме того, мы также будем фиксировать длину исходной последовательности без учёта токенов дополнения. Эта информация понадобится некоторым моделям, которые мы рассмотрим позже. Поскольку набор данных для машинного перевода состоит из пар языков, мы можем построить два словаря отдельно для исходного и целевого языков. При токенизации на уровне слов размер словаря будет значительно больше, чем при токенизации на уровне символов. Чтобы уменьшить это влияние, здесь мы будем рассматривать редкие токены, встречающиеся менее двух раз, как один неизвестный токен («»). Как мы объясним позже (:numref:`fig_seq2seq`), при обучении с использованием целевых последовательностей выходные данные декодировщика (токены меток) могут совпадать с входными данными декодировщика (целевыми токенами), со сдвигом на один токен; а специальный токен начала последовательности «» будет использоваться в качестве первого входного токена для предсказания целевой последовательности (:numref:`fig_seq2seq_predict`). .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(MTFraEng) #@save def __init__(self, batch_size, num_steps=9, num_train=512, num_val=128): super(MTFraEng, self).__init__() self.save_hyperparameters() self.arrays, self.src_vocab, self.tgt_vocab = self._build_arrays( self._download()) .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(MTFraEng) #@save def _build_arrays(self, raw_text, src_vocab=None, tgt_vocab=None): def _build_array(sentences, vocab, is_tgt=False): pad_or_trim = lambda seq, t: ( seq[:t] if len(seq) > t else seq + [''] * (t - len(seq))) sentences = [pad_or_trim(s, self.num_steps) for s in sentences] if is_tgt: sentences = [[''] + s for s in sentences] if vocab is None: vocab = d2l.Vocab(sentences, min_freq=2) array = torch.tensor([vocab[s] for s in sentences]) valid_len = (array != vocab['']).type(torch.int32).sum(1) return array, vocab, valid_len src, tgt = self._tokenize(self._preprocess(raw_text), self.num_train + self.num_val) src_array, src_vocab, src_valid_len = _build_array(src, src_vocab) tgt_array, tgt_vocab, _ = _build_array(tgt, tgt_vocab, True) return ((src_array, tgt_array[:,:-1], src_valid_len, tgt_array[:,1:]), src_vocab, tgt_vocab) Чтение набора данных -------------------- Наконец, мы определяем метод ``get_dataloader``, возвращающий итератор данных. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(MTFraEng) #@save def get_dataloader(self, train): idx = slice(0, self.num_train) if train else slice(self.num_train, None) return self.get_tensorloader(self.arrays, train, idx) Давайте прочитаем первый мини-батч из набора данных English–French. .. raw:: latex \diilbookstyleinputcell .. code:: python data = MTFraEng(batch_size=3) src, tgt, src_valid_len, label = next(iter(data.train_dataloader())) print('source:', src.type(torch.int32)) print('decoder input:', tgt.type(torch.int32)) print('source len excluding pad:', src_valid_len.type(torch.int32)) print('label:', label.type(torch.int32)) Мы показываем пару исходной и целевой последовательностей, обработанных вышеописанным ``_build_arrays`` методом (в строковом формате). .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(MTFraEng) #@save def build(self, src_sentences, tgt_sentences): raw_text = '\n'.join([src + '\t' + tgt for src, tgt in zip( src_sentences, tgt_sentences)]) arrays, _, _ = self._build_arrays( raw_text, self.src_vocab, self.tgt_vocab) return arrays .. raw:: latex \diilbookstyleinputcell .. code:: python src, tgt, _, _ = data.build(['hi .'], ['salut .']) print('source:', data.src_vocab.to_tokens(src[0].type(torch.int32))) print('target:', data.tgt_vocab.to_tokens(tgt[0].type(torch.int32))) Итоги ----- В обработке естественного языка *машинный перевод* — это задача автоматического преобразования последовательности, представляющей строку текста на *исходном* языке, в строку, представляющую правдоподобный перевод на *целевом* языке. При использовании пословной токенизации размер словаря будет значительно больше, чем при посимвольной, но длины последовательностей будут гораздо короче. Чтобы уменьшить большой размер словаря, мы можем рассматривать редко встречающиеся токены как некий токен «неизвестно». Мы можем обрезать и дополнять текстовые последовательности так, чтобы все они имели одинаковую длину и могли загружаться мини-пакетами. Современные реализации часто группируют последовательности с близкими длинами, чтобы избежать избыточных вычислений при дополнении. Упражнения ---------- 1. Попробуйте различные значения аргумента ``max_examples`` в методе ``_tokenize``. Как это влияет на размеры словарей исходного языка и целевого языка? 2. Текст на некоторых языках, таких как китайский и японский, не содержит маркеров границ слов (например, пробелов). Является ли токенизация на уровне слов хорошим решением для таких случаев? Почему да или почему нет? `Обсуждения `__