14.3. Tập dữ liệu để Tiền Huấn luyện Embedding Từ

Trong phần này, chúng tôi sẽ giới thiệu cách tiền xử lý một tập dữ liệu với phương pháp lấy mẫu âm Section 14.2 và tạo các minibatch để huấn luyện word2vec. Tập dữ liệu mà ta sẽ sử dụng là Penn Tree Bank (PTB), một kho ngữ liệu nhỏ nhưng được sử dụng phổ biến. Tập dữ liệu này được thu thập từ các bài báo của Wall Street Journal và bao gồm các tập huấn luyện, tập kiểm định và tập kiểm tra.

Đầu tiên, ta nhập các gói và mô-đun cần thiết cho thí nghiệm.

from d2l import mxnet as d2l
import math
from mxnet import gluon, np
import os
import random

14.3.1. Đọc và Tiền xử lý Dữ liệu

Tập dữ liệu này đã được tiền xử lý trước. Mỗi dòng của tập dữ liệu được xem là một câu. Tất cả các từ trong một câu được phân cách bằng dấu cách. Trong bài toán embedding từ, mỗi từ là một token.

#@save
d2l.DATA_HUB['ptb'] = (d2l.DATA_URL + 'ptb.zip',
                       '319d85e578af0cdc590547f26231e4e31cdf1e42')

#@save
def read_ptb():
    data_dir = d2l.download_extract('ptb')
    with open(os.path.join(data_dir, 'ptb.train.txt')) as f:
        raw_text = f.read()
    return [line.split() for line in raw_text.split('\n')]

sentences = read_ptb()
f'# sentences: {len(sentences)}'
'# sentences: 42069'

Tiếp theo ta sẽ xây dựng bộ từ vựng, trong đó các từ xuất hiện dưới 10 lần sẽ được xem như token “<unk>”. Lưu ý rằng tập dữ liệu PTB đã được tiền xử lý cũng chứa các token “<unk>” đại diện cho các từ hiếm gặp.

vocab = d2l.Vocab(sentences, min_freq=10)
f'vocab size: {len(vocab)}'
'vocab size: 6719'

14.3.2. Lấy mẫu con

Trong dữ liệu văn bản, thường có một số từ xuất hiện với tần suất cao, chẳng hạn như các từ “the”, “a” và “in” trong tiếng Anh. Nói chung, trong cửa sổ ngữ cảnh, sẽ tốt hơn nếu ta huấn luyện mô hình embedding từ khi một từ bình thường (chẳng hạn như “chip”) và một từ có tần suất thấp hơn (chẳng hạn như “microprocessor”) xuất hiện cùng lúc, hơn là khi một từ bình thường xuất hiện với một từ có tần suất cao hơn (chẳng hạn như “the”). Do đó, khi huấn luyện mô hình embedding từ, ta có thể thực hiện lấy mẫu con [2] trên các từ. Cụ thể, mỗi từ \(w_i\) được gán chỉ số trong tập dữ liệu sẽ bị loại bỏ với một xác suất nhất định. Xác suất loại bỏ được tính như sau:

(14.3.1)\[P(w_i) = \max\left(1 - \sqrt{\frac{t}{f(w_i)}}, 0\right),\]

Ở đây, \(f(w_i)\) là tỷ lệ giữa số lần xuất hiện từ \(w_i\) với tổng số từ trong tập dữ liệu, và hằng số \(t\) là một siêu tham số (có giá trị bằng \(10^{-4}\) trong thí nghiệm này). Như ta có thể thấy, từ \(w_i\) chỉ có thể được loại bỏ trong lúc lấy mẫu con khi \(f(w_i) > t\). Tần suất của từ càng cao, xác suất loại bỏ càng lớn.

#@save
def subsampling(sentences, vocab):
    # Map low frequency words into <unk>
    sentences = [[vocab.idx_to_token[vocab[tk]] for tk in line]
                 for line in sentences]
    # Count the frequency for each word
    counter = d2l.count_corpus(sentences)
    num_tokens = sum(counter.values())

    # Return True if to keep this token during subsampling
    def keep(token):
        return(random.uniform(0, 1) <
               math.sqrt(1e-4 / counter[token] * num_tokens))

    # Now do the subsampling
    return [[tk for tk in line if keep(tk)] for line in sentences]

subsampled = subsampling(sentences, vocab)

So sánh độ dài chuỗi trước và sau khi lấy mẫu, ta có thể thấy việc lấy mẫu con làm giảm đáng kể độ dài chuỗi.

d2l.set_figsize()
d2l.plt.hist([[len(line) for line in sentences],
              [len(line) for line in subsampled]])
d2l.plt.xlabel('# tokens per sentence')
d2l.plt.ylabel('count')
d2l.plt.legend(['origin', 'subsampled']);
../_images/output_word-embedding-dataset_vn_cc63db_9_0.svg

Với các token riêng lẻ, tỉ lệ lấy mẫu của các từ có tần suất cao như từ “the” nhỏ hơn 1/20.

def compare_counts(token):
    return (f'# of "{token}": '
            f'before={sum([line.count(token) for line in sentences])}, '
            f'after={sum([line.count(token) for line in subsampled])}')

compare_counts('the')
'# of "the": before=50770, after=2094'

Nhưng các từ có tần số thấp như từ “join” hoàn toàn được giữ nguyên.

compare_counts('join')
'# of "join": before=45, after=45'

Cuối cùng, ta ánh xạ từng token tới một chỉ số tương ứng để xây dựng kho ngữ liệu.

corpus = [vocab[line] for line in subsampled]
corpus[0:3]
[[0], [392, 2132, 145, 275, 406], [12, 5464, 3080, 1595]]

14.3.3. Nạp Dữ liệu

Tiếp theo, ta đọc kho ngữ liệu với các chỉ số token thành các batch dữ liệu cho quá trình huấn luyện.

14.3.3.1. Trích xuất từ Đích Trung tâm và Từ Ngữ cảnh

Ta sử dụng các từ với khoảng cách tới từ đích trung tâm không quá độ dài cửa sổ ngữ cảnh để làm từ ngữ cảnh cho từ đích trung tâm đó. Hàm sau đây trích xuất tất cả từ đích trung tâm và các từ ngữ cảnh của chúng. Ta chọn kích thước cửa sổ ngữ cảnh là một số nguyên từ 1 tới max_window_size (kích thước cửa sổ tối đa), được lấy ngẫu nhiên theo phân phối đều.

#@save
def get_centers_and_contexts(corpus, max_window_size):
    centers, contexts = [], []
    for line in corpus:
        # Each sentence needs at least 2 words to form a "central target word
        # - context word" pair
        if len(line) < 2:
            continue
        centers += line
        for i in range(len(line)):  # Context window centered at i
            window_size = random.randint(1, max_window_size)
            indices = list(range(max(0, i - window_size),
                                 min(len(line), i + 1 + window_size)))
            # Exclude the central target word from the context words
            indices.remove(i)
            contexts.append([line[idx] for idx in indices])
    return centers, contexts

Kế tiếp, ta tạo một tập dữ liệu nhân tạo chứa hai câu có lần lượt 7 và 3 từ. Hãy giả sử cửa sổ ngữ cảnh cực đại là 2 và in tất cả các từ đích trung tâm và các từ ngữ cảnh của chúng.

tiny_dataset = [list(range(7)), list(range(7, 10))]
print('dataset', tiny_dataset)
for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
    print('center', center, 'has contexts', context)
dataset [[0, 1, 2, 3, 4, 5, 6], [7, 8, 9]]
center 0 has contexts [1, 2]
center 1 has contexts [0, 2, 3]
center 2 has contexts [1, 3]
center 3 has contexts [2, 4]
center 4 has contexts [2, 3, 5, 6]
center 5 has contexts [3, 4, 6]
center 6 has contexts [5]
center 7 has contexts [8]
center 8 has contexts [7, 9]
center 9 has contexts [8]

Ta thiết lập cửa sổ ngữ cảnh cực đại là 5. Đoạn mã sau trích xuất tất cả các từ đích trung tâm và các từ ngữ cảnh của chúng trong tập dữ liệu.

all_centers, all_contexts = get_centers_and_contexts(corpus, 5)
f'# center-context pairs: {len(all_centers)}'
'# center-context pairs: 353626'

14.3.3.2. Lấy mẫu Âm

Ta thực hiện lấy mẫu âm để huấn luyện gần đúng. Với mỗi cặp từ đích trung tâm và ngữ cảnh, ta lẫy mẫu ngẫu nhiên \(K\) từ nhiễu (\(K=5\) trong thử nghiệm này). Theo đề xuất trong bài báo Word2vec, xác suất lấy mẫu từ nhiễu \(P(w)\) là tỷ lệ giữa tần suất xuất hiện của từ \(w\) và tổng tần suất xuất hiện của tất cả các từ, lấy mũ 0.75 [2].

Trước hết ta sẽ định nghĩa một lớp để lấy ra một ứng cử viên dựa theo các trọng số lấy mẫu. Lớp này sẽ lưu lại 10000 số ngẫu nhiên một lần thay vì gọi random.choices liên tục.

#@save
class RandomGenerator:
    """Draw a random int in [0, n] according to n sampling weights."""
    def __init__(self, sampling_weights):
        self.population = list(range(len(sampling_weights)))
        self.sampling_weights = sampling_weights
        self.candidates = []
        self.i = 0

    def draw(self):
        if self.i == len(self.candidates):
            self.candidates = random.choices(
                self.population, self.sampling_weights, k=10000)
            self.i = 0
        self.i += 1
        return self.candidates[self.i-1]

generator = RandomGenerator([2, 3, 4])
[generator.draw() for _ in range(10)]
[1, 2, 2, 0, 2, 1, 0, 0, 2, 2]
#@save
def get_negatives(all_contexts, corpus, K):
    counter = d2l.count_corpus(corpus)
    sampling_weights = [counter[i]**0.75 for i in range(len(counter))]
    all_negatives, generator = [], RandomGenerator(sampling_weights)
    for contexts in all_contexts:
        negatives = []
        while len(negatives) < len(contexts) * K:
            neg = generator.draw()
            # Noise words cannot be context words
            if neg not in contexts:
                negatives.append(neg)
        all_negatives.append(negatives)
    return all_negatives

all_negatives = get_negatives(all_contexts, corpus, 5)

14.3.3.3. Đọc Dữ liệu thành Batch

Chúng ta trích xuất tất cả các từ đích trung tâm all_centers, cũng như các từ ngữ cảnh all_contexts và những từ nhiễu của mỗi từ đích trung tâm trong tập dữ liệu, rồi đọc chúng thành các minibatch ngẫu nhiên.

Trong một minibatch dữ liệu, mẫu thứ \(i\) bao gồm một từ đích trung tâm cùng \(n_i\) từ ngữ cảnh và \(m_i\) từ nhiễu tương ứng với từ đích trung tâm đó. Do kích thước cửa sổ ngữ cảnh của mỗi mẫu có thể khác nhau, nên tổng số từ ngữ cảnh và từ nhiễu, \(n_i+m_i\), cũng sẽ khác nhau. Khi tạo một minibatch, chúng ta nối (concatenate) các từ ngữ cảnh và các từ nhiễu của mỗi mẫu, và đệm thêm các giá trị 0 để độ dài của các đoạn nối bằng nhau, tức bằng \(\max_i n_i+m_i\) (max_len). Nhằm tránh ảnh hưởng của phần đệm lên việc tính toán hàm mất mát, chúng ta tạo một biến mặt nạ masks, mỗi phần tử trong đó tương ứng với một phần tử trong phần nối giữa từ ngữ cảnh và từ nhiễu, contexts_negatives. Khi một phần tử trong biến contexts_negatives là đệm, thì phần tử trong biến mặt nạ masks ở vị trí đó sẽ là 0, còn lại là bằng 1. Để phân biệt giữa các mẫu dương và âm, chúng ta cũng cần phân biệt các từ ngữ cảnh với các từ nhiễu trong biến contexts_negatives. Dựa trên cấu tạo của biến mặt nạ, chúng ta chỉ cần tạo một biến nhãn labels có cùng kích thước với biến contexts_negatives và đặt giá trị các phần tử tương ứng với các từ ngữ cảnh (mẫu dương) bằng 1 và phần còn lại bằng 0.

Tiếp đó, chúng ta lập trình chức năng đọc minibatch batchify, với đầu vào minibatch data là một danh sách có độ dài là kích thước batch, mỗi phần tử trong đó chứa các từ đích trung tâm center, các từ ngữ cảnh context và các từ nhiễu negative. Dữ liệu trong minibatch được trả về bởi hàm này đều tuân theo định dạng chúng ta cần, bao gồm biến mặt nạ.

#@save
def batchify(data):
    max_len = max(len(c) + len(n) for _, c, n in data)
    centers, contexts_negatives, masks, labels = [], [], [], []
    for center, context, negative in data:
        cur_len = len(context) + len(negative)
        centers += [center]
        contexts_negatives += [context + negative + [0] * (max_len - cur_len)]
        masks += [[1] * cur_len + [0] * (max_len - cur_len)]
        labels += [[1] * len(context) + [0] * (max_len - len(context))]
    return (np.array(centers).reshape(-1, 1), np.array(contexts_negatives),
            np.array(masks), np.array(labels))

Tạo hai ví dụ mẫu đơn giản:

x_1 = (1, [2, 2], [3, 3, 3, 3])
x_2 = (1, [2, 2, 2], [3, 3])
batch = batchify((x_1, x_2))

names = ['centers', 'contexts_negatives', 'masks', 'labels']
for name, data in zip(names, batch):
    print(name, '=', data)
centers = [[1.]
 [1.]]
contexts_negatives = [[2. 2. 3. 3. 3. 3.]
 [2. 2. 2. 3. 3. 0.]]
masks = [[1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 0.]]
labels = [[1. 1. 0. 0. 0. 0.]
 [1. 1. 1. 0. 0. 0.]]

Chúng ta dùng hàm batchify vừa được định nghĩa để chỉ định phương thức đọc minibatch trong thực thể DataLoader.

14.3.4. Kết hợp mọi thứ cùng nhau

Cuối cùng, chúng ta định nghĩa hàm load_data_ptb để đọc tập dữ liệu PTB và trả về iterator dữ liệu.

#@save
def load_data_ptb(batch_size, max_window_size, num_noise_words):
    num_workers = d2l.get_dataloader_workers()
    sentences = read_ptb()
    vocab = d2l.Vocab(sentences, min_freq=10)
    subsampled = subsampling(sentences, vocab)
    corpus = [vocab[line] for line in subsampled]
    all_centers, all_contexts = get_centers_and_contexts(
        corpus, max_window_size)
    all_negatives = get_negatives(all_contexts, corpus, num_noise_words)
    dataset = gluon.data.ArrayDataset(
        all_centers, all_contexts, all_negatives)
    data_iter = gluon.data.DataLoader(dataset, batch_size, shuffle=True,
                                      batchify_fn=batchify,
                                      num_workers=num_workers)
    return data_iter, vocab

Ta hãy cùng in ra minibatch đầu tiên trong iterator dữ liệu.

data_iter, vocab = load_data_ptb(512, 5, 5)
for batch in data_iter:
    for name, data in zip(names, batch):
        print(name, 'shape:', data.shape)
    break
centers shape: (512, 1)
contexts_negatives shape: (512, 60)
masks shape: (512, 60)
labels shape: (512, 60)

14.3.5. Tóm tắt

  • Việc lấy mẫu con cố gắng giảm thiểu tác động của các từ có tần suất cao đến việc huấn luyện mô hình embedding từ.
  • Ta có thể đệm để tạo ra các minibatch với các mẫu có cùng độ dài và sử dụng các biến mặt nạ để phân biệt phần tử đệm, vì thế chỉ có những phần tử không phải đệm mới được dùng để tính toán hàm mất mát.

14.3.6. Bài tập

Chúng ta sử dụng hàm batchify để chỉ định phương thức đọc minibatch trong thực thể DataLoader và in ra kích thước của từng biến trong lần đọc batch đầu tiên. Những kích thước này được tính toán như thế nào?

14.3.7. Thảo luận

14.3.8. 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 Văn Quang
  • Nguyễn Mai Hoàng Long
  • Phạm Đăng Khoa
  • Phạm Minh Đức
  • Lê Khắc Hồng Phúc
  • Nguyễn Văn Cường
  • Phạm Hồng Vinh

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