Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Резюме

  • Машинный перевод — это автоматический перевод текстовой последовательности с одного языка на другой.
  • При пословной токенизации словарь заметно больше, чем при посимвольной. Чтобы его уменьшить, можно считать редкие токены одним и тем же неизвестным токеном.
  • Усечение и дополнение текстовых последовательностей позволяют сделать все последовательности одинаковой длины и загружать их минибатчами.

Упражнения

  1. Попробуйте разные значения параметра num_examples в функции load_data_nmt. Как это влияет на размеры словарей исходного и целевого языков?
  2. В некоторых языках (например, китайском и японском) в тексте нет границ слов, таких как пробелы. Хорошая ли идея использовать пословную токенизацию в таком случае? Почему?

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.