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:
Ở đâ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']);
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)