.. raw:: html .. _sec_word2vec_data: Tập dữ liệu để Tiền Huấn luyện Embedding Từ =========================================== .. raw:: html 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 :numref:`sec_approx_train` 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. .. raw:: html Đầu tiên, ta nhập các gói và mô-đun cần thiết cho thí nghiệm. .. code:: python from d2l import mxnet as d2l import math from mxnet import gluon, np import os import random .. raw:: html Đọc và Tiền xử lý Dữ liệu ------------------------- .. raw:: html 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. .. code:: python #@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)}' .. parsed-literal:: :class: output '# sentences: 42069' .. raw:: html 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 “”. Lưu ý rằng tập dữ liệu PTB đã được tiền xử lý cũng chứa các token “” đại diện cho các từ hiếm gặp. .. code:: python vocab = d2l.Vocab(sentences, min_freq=10) f'vocab size: {len(vocab)}' .. parsed-literal:: :class: output 'vocab size: 6719' .. raw:: html Lấy mẫu con ----------- .. raw:: html 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ừ :math:`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: .. math:: P(w_i) = \max\left(1 - \sqrt{\frac{t}{f(w_i)}}, 0\right), .. raw:: html Ở đây, :math:`f(w_i)` là tỷ lệ giữa số lần xuất hiện từ :math:`w_i` với tổng số từ trong tập dữ liệu, và hằng số :math:`t` là một siêu tham số (có giá trị bằng :math:`10^{-4}` trong thí nghiệm này). Như ta có thể thấy, từ :math:`w_i` chỉ có thể được loại bỏ trong lúc lấy mẫu con khi :math:`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. .. code:: python #@save def subsampling(sentences, vocab): # Map low frequency words into 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) .. raw:: html 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. .. code:: python 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']); .. figure:: output_word-embedding-dataset_vn_cc63db_9_0.svg .. raw:: html 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. .. code:: python 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') .. parsed-literal:: :class: output '# of "the": before=50770, after=2094' .. raw:: html Nhưng các từ có tần số thấp như từ “join” hoàn toàn được giữ nguyên. .. code:: python compare_counts('join') .. parsed-literal:: :class: output '# of "join": before=45, after=45' .. raw:: html 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. .. code:: python corpus = [vocab[line] for line in subsampled] corpus[0:3] .. parsed-literal:: :class: output [[0], [392, 2132, 145, 275, 406], [12, 5464, 3080, 1595]] .. raw:: html Nạp Dữ liệu ----------- .. raw:: html 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. .. raw:: html Trích xuất từ Đích Trung tâm và Từ Ngữ cảnh ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. raw:: html 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. .. code:: python #@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 .. raw:: html 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. .. code:: python 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) .. parsed-literal:: :class: output 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] .. raw:: html 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. .. code:: python all_centers, all_contexts = get_centers_and_contexts(corpus, 5) f'# center-context pairs: {len(all_centers)}' .. parsed-literal:: :class: output '# center-context pairs: 353626' .. raw:: html Lấy mẫu Âm ~~~~~~~~~~ .. raw:: html 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 :math:`K` từ nhiễu (:math:`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 :math:`P(w)` là tỷ lệ giữa tần suất xuất hiện của từ :math:`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]. .. raw:: html 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. .. code:: python #@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)] .. parsed-literal:: :class: output [1, 2, 2, 0, 2, 1, 0, 0, 2, 2] .. code:: python #@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) .. raw:: html Đọc Dữ liệu thành Batch ~~~~~~~~~~~~~~~~~~~~~~~ .. raw:: html 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. .. raw:: html Trong một minibatch dữ liệu, mẫu thứ :math:`i` bao gồm một từ đích trung tâm cùng :math:`n_i` từ ngữ cảnh và :math:`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, :math:`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 :math:`\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. .. raw:: html 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ạ. .. code:: python #@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)) .. raw:: html Tạo hai ví dụ mẫu đơn giản: .. code:: python 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) .. parsed-literal:: :class: output 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.]] .. raw:: html 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``. .. raw:: html Kết hợp mọi thứ cùng nhau ------------------------- .. raw:: html 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. .. code:: python #@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 .. raw:: html Ta hãy cùng in ra minibatch đầu tiên trong iterator dữ liệu. .. code:: python 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 .. parsed-literal:: :class: output centers shape: (512, 1) contexts_negatives shape: (512, 60) masks shape: (512, 60) labels shape: (512, 60) Tóm tắt ------- .. raw:: html - 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. Bài tập ------- .. raw:: html 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? Thảo luận --------- - `Tiếng Anh - MXNet `__ - `Tiếng Việt `__ 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)*