15.4. Suy luận ngôn ngữ tự nhiên và Tập dữ liệu

Trong Section 15.1, chúng ta đã thảo luận về bài toán phân tích sắc thái cảm xúc (sentiment analysis). Mục đích của bài toán là phân loại một chuỗi văn bản vào các hạng mục đã định trước, chẳng hạn như các sắc thái đối lập. Tuy nhiên, trong trường hợp cần xác định liệu một câu có thể suy ra được từ một câu khác không, hoặc khi cần loại bỏ sự dư thừa bằng việc xác định các câu tương đương về ngữ nghĩa thì việc phân lớp một chuỗi văn bản là không đủ. Thay vào đó ta cần khả năng suy luận trên các cặp chuỗi văn bản.

15.4.1. Suy luận Ngôn ngữ Tự nhiên

Suy luận ngôn ngữ tự nhiên nghiên cứu liệu một giả thuyết (hypothesis) có thể được suy ra được từ một tiền đề (premise) không, cả hai đều là chuỗi văn bản. Nói cách khác, suy luận ngôn ngữ tự nhiên quyết định mối quan hệ logic giữa một cặp chuỗi văn bản. Các mối quan hệ đó thường rơi vào một trong ba loại sau đây:

  • Kéo theo: giả thuyết có thể suy ra được từ tiền đề.
  • Đối lập: phủ định của giả thuyết có thể suy ra được từ tiền đề.
  • Trung tính: tất cả các trường hợp khác.

Suy luận ngôn ngữ tự nhiên còn được gọi là bài toán nhận dạng quan hệ kéo theo trong văn bản. Ví dụ, cặp sau được gán nhãn là kéo theo bởi vì “thể hiện tình cảm” trong giả thuyết có thể được suy ra từ “ôm nhau” trong tiền đề.

Tiền đề: Hai người đang ôm nhau.
Giả thuyết: Hai người đang thể hiện tình cảm.

Sau đây là một ví dụ về đối lập, vì “chạy đoạn mã ví dụ” cho biết “không ngủ” chứ không phải “ngủ”.

Tiền đề: Một bạn đang chạy đoạn mã ví dụ trong Đắm mình vào học sâu.
Giả thuyết: Bạn đó đang ngủ.

Ví dụ thứ ba cho thấy mối quan hệ trung tính vì cả “nổi tiếng” và “không nổi tiếng” đều không thể được suy ra từ thực tế là “đang biểu diễn cho chúng tôi”.

Tiền đề: Các nhạc công đang biểu diễn cho chúng tôi.
Giả thuyết: Các nhạc công rất nổi tiếng.

Suy luận ngôn ngữ tự nhiên là một chủ đề trung tâm trong việc hiểu ngôn ngữ tự nhiên. Nó có nhiều ứng dụng khác nhau, từ truy xuất thông tin đến hỏi đáp trong miền mở. Để nghiên cứu bài toán này, chúng ta sẽ bắt đầu bằng việc tìm hiểu một tập dữ liệu đánh giá xếp hạng phổ biến trong suy luận ngôn ngữ tự nhiên.

15.4.2. Tập dữ liệu Suy luận ngôn ngữ tự nhiên của Stanford (SNLI)

Tập ngữ liệu ngôn ngữ tự nhiên của Stanford (SNLI) là một tập hợp gồm hơn \(500,000\) cặp câu Tiếng Anh được gán nhãn [Bowman et al., 2015]. Ta tải xuống và giải nén tập dữ liệu SNLI trong đường dẫn ../data/snli_1.0.

import collections
from d2l import mxnet as d2l
from mxnet import gluon, np, npx
import os
import re
import zipfile

npx.set_np()

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

data_dir = d2l.download_extract('SNLI')
Downloading ../data/snli_1.0.zip from https://nlp.stanford.edu/projects/snli/snli_1.0.zip...

15.4.2.1. Đọc tập Dữ liệu

Tập dữ liệu SNLI gốc chứa thông tin phong phú hơn những gì thực sự cần cho thí nghiệm của chúng ta. Vì thế, ta định nghĩa một hàm read_snli để trích xuất một phần của tập dữ liệu, rồi trả về các danh sách tiền đề, giả thuyết và nhãn của chúng.

#@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

Bây giờ ta in \(3\) cặp tiền đề và giả thuyết đầu tiên cũng như nhãn của chúng (“0”, “1”, và “2” tương ứng với “kéo theo”, “đối lập”, và “trung tính”).

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)
premise: A person on a horse jumps over a broken down airplane .
hypothesis: A person is training his horse for a competition .
label: 2
premise: A person on a horse jumps over a broken down airplane .
hypothesis: A person is at a diner , ordering an omelette .
label: 1
premise: A person on a horse jumps over a broken down airplane .
hypothesis: A person is outdoors , on a horse .
label: 0

Tập huấn luyện có khoảng \(550,000\) cặp, và tập kiểm tra có khoảng \(10,000\) cặp. Đoạn mã dưới đây cho thấy rằng ba nhãn “kéo theo”, “đối lập”, và “trung tính” cân bằng trong cả hai tập huấn luyện và tập kiểm tra.

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)])
[183416, 183187, 182764]
[3368, 3237, 3219]

15.4.2.2. Định nghĩa Lớp để nạp Tập dữ liệu

Dưới đây ta định nghĩa một lớp để nạp tập dữ liệu SNLI bằng cách kế thừa lớp Dataset trong Gluon. Đối số num_steps trong phương thức khởi tạo chỉ định độ dài chuỗi văn bản, do đó mỗi minibatch sẽ có cùng kích thước. Nói cách khác, các token phía sau num_steps token đầu tiên ở trong chuỗi dài hơn sẽ được loại bỏ, trong khi token đặc biệt “<pad>” sẽ được nối thêm vào các chuỗi ngắn hơn đến khi độ dài của chúng bằng num_steps. Bằng cách lập trình hàm __getitem__, ta có thể truy cập vào các tiền đề, giả thuyết và nhãn bất kỳ với chỉ số idx.

#@save
class SNLIDataset(gluon.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 = np.array(dataset[2])
        print('read ' + str(len(self.premises)) + ' examples')

    def _pad(self, lines):
        return np.array([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)

15.4.2.3. Kết hợp tất cả lại

Bây giờ ta có thể gọi hàm read_snli và lớp SNLIDataset để tải xuống tập dữ liệu SNLI và trả về thực thể DataLoader cho cả hai tập huấn luyện và tập kiểm tra, cùng với bộ từ vựng của tập huấn luyện. Lưu ý rằng ta phải sử dụng bộ từ vựng được xây dựng từ tập huấn luyện cho tập kiểm tra. Như vậy, mô hình được huấn luyện trên tập huấn luyện sẽ không biết bất kỳ token mới nào từ tập kiểm tra nếu có.

#@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 = gluon.data.DataLoader(train_set, batch_size, shuffle=True,
                                       num_workers=num_workers)
    test_iter = gluon.data.DataLoader(test_set, batch_size, shuffle=False,
                                      num_workers=num_workers)
    return train_iter, test_iter, train_set.vocab

Ở đây ta đặt kích thước batch là \(128\) và độ dài chuỗi là \(50\), và gọi hàm load_data_snli để lấy iterator dữ liệu và bộ từ vựng. Sau đó ta in kích thước của bộ từ vựng.

train_iter, test_iter, vocab = load_data_snli(128, 50)
len(vocab)
read 549367 examples
read 9824 examples
18678

Bây giờ ta in kích thước của minibatch đầu tiên. Trái với phân tích sắc thái cảm xúc, ta có \(2\) đầu vào X[0]X[1] biểu diễn cặp tiền đề và giả thuyết.

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

15.4.3. Tóm tắt

  • Suy luận ngôn ngữ tự nhiên nghiên cứu liệu một giả thuyết có thể được suy ra từ một tiền đề hay không, khi cả hai đều là chuỗi văn bản.
  • Trong suy luận ngôn ngữ tự nhiên, mối quan hệ giữa tiền đề và giả thuyết bao gồm kéo theo, đối lập và trung tính.
  • Bộ dữ liệu suy luận ngôn ngữ tự nhiên Stanford (SNLI) là một tập dữ liệu đánh giá xếp hạng phổ biến cho suy luận ngôn ngữ tự nhiên.

15.4.4. Bài tập

  1. Dịch máy từ lâu nay vẫn được đánh giá bằng sự trùng lặp bề ngoài giữa các \(n\)-gram của bản dịch đầu ra và bản dịch nhãn gốc. Bạn có thể thiết kế một phép đo để đánh giá kết quả dịch máy bằng cách sử dụng suy luận ngôn ngữ tự nhiên không?
  2. Thay đổi siêu tham số như thế nào để giảm kích thước bộ từ vựng?

15.4.5. Thảo luận

15.4.6. Những người thực hiện

Bản dịch trong trang này được thực hiện bởi:

  • Đoàn Võ Duy Thanh
  • Nguyễn Thái Bình
  • Lê Khắc Hồng Phúc
  • Trần Yến Thy
  • Phạm Minh Đức
  • Nguyễn Văn Cường

Lần cập nhật gần nhất: 26/09/2020. (Cập nhật lần cuối từ nội dung gốc: 30/06/2020)