16.4. Вывод в естественном языке и набор данных

В разделе Section 16.1 мы обсуждали задачу анализа тональности. Эта задача заключается в классификации одной текстовой последовательности на заранее определённые категории, например, на набор полярностей тональности. Однако, когда требуется определить, следует ли одно предложение из другого, или устранить избыточность, выявив семантически эквивалентные предложения, знания о классификации одной текстовой последовательности оказываются недостаточными. Вместо этого нам необходимо уметь рассуждать над парами текстовых последовательностей.

16.4.1. Вывод на естественном языке

Вывод на основе естественного языка изучает, можно ли сделать вывод (hypothesis) из посылки (premise), где обе они представляют собой текстовую последовательность. Иными словами, вывод на основе естественного языка определяет логическую связь между парой текстовых последовательностей. Такие связи обычно подразделяются на три типа:

  • Энтейлмент (логическое следствие): гипотеза может быть выведена из предпосылки.

  • Противоречие: из посылки можно вывести отрицание гипотезы.

  • Нейтральность: все остальные случаи.

Вывод на основе естественного языка также известен как задача распознавания текстового следствия. ⟦For example⟧ следующая пара будет помечена как следование, потому что «проявление привязанности» в гипотезе можно вывести из «объятий друг друга» в посылке.

Предпосылка: Две женщины обнимают друг друга.

Гипотеза: Две женщины проявляют нежность.

Ниже приведён пример противоречия, поскольку «выполнение примера кода» означает «не спящий», а не «спящий».

Предпосылка: Мужчина запускает пример кода из книги «Погружение в глубокое обучение».

Гипотеза: Мужчина спит.

Третий пример демонстрирует отношение нейтральности, поскольку из факта «выступают для нас» нельзя вывести ни «известны», ни «неизвестны».

Посылка: Музыканты выступают для нас.

Гипотеза: Музыканты знамениты.

Вывод на основе естественного языка (natural language inference) является центральной темой в области понимания естественного языка. Он находит широкое применение — от информационного поиска до ответа на вопросы в открытых доменах. Чтобы изучить эту задачу, мы начнем с исследования популярного бенчмарка для вывода на основе естественного языка.

16.4.2. Набор данных Stanford Natural Language Inference (SNLI)

Корпус Stanford Natural Language Inference (SNLI) представляет собой коллекцию из более чем 500 000 помеченных пар английских предложений (Bowman et al., 2015). Мы загружаем и сохраняем извлеченный набор данных SNLI в путь ../data/snli_1.0.

import os
import re
import torch
from torch import nn
from d2l import torch as d2l

#@save
d2l.DATA_HUB['SNLI'] = (
    'https://nlp.stanford.edu/projects/snli/snli_1.0.zip',
    '9fcde07509c7e87ec61c640c1b2753d9041758e4')

data_dir = d2l.download_extract('SNLI')

16.4.2.1. Чтение набора данных

Исходный набор данных SNLI содержит гораздо больше информации, чем нам действительно нужно в наших экспериментах. Поэтому мы определяем функцию read_snli, которая извлекает только часть набора данных, а затем возвращает списки предпосылок, гипотез и их меток.

#@save
def read_snli(data_dir, is_train):
    """Read the SNLI dataset into premises, hypotheses, and labels."""
    def extract_text(s):
        # Remove information that will not be used by us
        s = re.sub('\\(', '', s)
        s = re.sub('\\)', '', s)
        # Substitute two or more consecutive whitespace with space
        s = re.sub('\\s{2,}', ' ', s)
        return s.strip()
    label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2}
    file_name = os.path.join(data_dir, 'snli_1.0_train.txt'
                             if is_train else 'snli_1.0_test.txt')
    with open(file_name, 'r') as f:
        rows = [row.split('\t') for row in f.readlines()[1:]]
    premises = [extract_text(row[1]) for row in rows if row[0] in label_set]
    hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set]
    labels = [label_set[row[0]] for row in rows if row[0] in label_set]
    return premises, hypotheses, labels

Теперь выведем первые 3 пары предпосылок и гипотез, а также их метки («0», «1» и «2» соответствуют «entailment» (подтверждение), «contradiction» (противоречие) и «neutral» (нейтральность) соответственно).

train_data = read_snli(data_dir, is_train=True)
for x0, x1, y in zip(train_data[0][:3], train_data[1][:3], train_data[2][:3]):
    print('premise:', x0)
    print('hypothesis:', x1)
    print('label:', y)

Обучающая выборка содержит около 550 000 пар, а тестовая — около 10 000 пар. Ниже показано, что в обеих выборках, обучающей и тестовой, три ⟦метки «entailment», «contradiction» и «neutral» сбалансированы⟧.

test_data = read_snli(data_dir, is_train=False)
for data in [train_data, test_data]:
    print([[row for row in data[2]].count(i) for i in range(3)])

16.4.2.2. Определение класса для загрузки набора данных

Ниже мы определяем класс для загрузки набора данных SNLI, наследуясь от класса Dataset в Gluon. Аргумент num_steps в конструкторе класса указывает длину текстовой последовательности, чтобы каждый мини-батч последовательностей имел одинаковую форму. Иными словами, токены после первых num_steps в более длинных последовательностях отбрасываются, а специальные токены «<pad>» добавляются к более коротким последовательностям до тех пор, пока их длина не станет равной num_steps. Реализовав функцию __getitem__, мы можем произвольным образом обращаться к посылке, гипотезе и метке по индексу idx.

#@save
class SNLIDataset(torch.utils.data.Dataset):
    """A customized dataset to load the SNLI dataset."""
    def __init__(self, dataset, num_steps, vocab=None):
        self.num_steps = num_steps
        all_premise_tokens = d2l.tokenize(dataset[0])
        all_hypothesis_tokens = d2l.tokenize(dataset[1])
        if vocab is None:
            self.vocab = d2l.Vocab(all_premise_tokens + all_hypothesis_tokens,
                                   min_freq=5, reserved_tokens=['<pad>'])
        else:
            self.vocab = vocab
        self.premises = self._pad(all_premise_tokens)
        self.hypotheses = self._pad(all_hypothesis_tokens)
        self.labels = torch.tensor(dataset[2])
        print('read ' + str(len(self.premises)) + ' examples')

    def _pad(self, lines):
        return torch.tensor([d2l.truncate_pad(
            self.vocab[line], self.num_steps, self.vocab['<pad>'])
                         for line in lines])

    def __getitem__(self, idx):
        return (self.premises[idx], self.hypotheses[idx]), self.labels[idx]

    def __len__(self):
        return len(self.premises)

16.4.2.3. Собирая всё вместе

Теперь мы можем вызвать функцию read_snli и класс SNLIDataset, чтобы загрузить набор данных SNLI и вернуть экземпляры DataLoader как для обучающей, так и для тестовой выборок, вместе со словарём обучающей выборки. Примечательно, что мы должны использовать словарь, построенный на основе обучающей выборки, и для тестовой выборки. В результате любой новый токен из тестовой выборки будет неизвестен модели, обученной на обучающей выборке.

#@save
def load_data_snli(batch_size, num_steps=50):
    """Download the SNLI dataset and return data iterators and vocabulary."""
    num_workers = d2l.get_dataloader_workers()
    data_dir = d2l.download_extract('SNLI')
    train_data = read_snli(data_dir, True)
    test_data = read_snli(data_dir, False)
    train_set = SNLIDataset(train_data, num_steps)
    test_set = SNLIDataset(test_data, num_steps, train_set.vocab)
    train_iter = torch.utils.data.DataLoader(train_set, batch_size,
                                             shuffle=True,
                                             num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(test_set, batch_size,
                                            shuffle=False,
                                            num_workers=num_workers)
    return train_iter, test_iter, train_set.vocab

Здесь мы задаем размер батча равным 128, а длину последовательности — 50, и вызываем функцию load_data_snli для получения итераторов данных и словаря. Затем выводим размер словаря.

train_iter, test_iter, vocab = load_data_snli(128, 50)
len(vocab)

Теперь выведем форму первого мини-батча. В отличие от анализа тональности, у нас есть два входа X[0] и X[1], представляющие пары посылок и гипотез.

for X, Y in train_iter:
    print(X[0].shape)
    print(X[1].shape)
    print(Y.shape)
    break

16.4.3. Итоги

  • Задача естественного языкового вывода заключается в определении того, можно ли сделать вывод о гипотезе на основе предпосылки, где обе они являются текстовыми последовательностями.

  • В задаче естественного языкового вывода отношения между посылками и гипотезами включают логическое следование, противоречие и нейтральность.

  • Корпус Stanford Natural Language Inference (SNLI) — это популярный бенчмарк-набор данных для задач естественного языкового вывода.

16.4.4. Упражнения

  1. Машинный перевод долгое время оценивался на основе поверхностного \(n\)-граммного совпадения между полученным переводом и эталонным переводом. Можете ли вы разработать метрику для оценки результатов машинного перевода с использованием естественного языкового вывода?

  2. Как можно изменить гиперпараметры, чтобы уменьшить размер словаря?

Обсуждения