14.4. Tiền huấn luyện word2vec

Trong phần này, ta sẽ huấn luyện một mô hình skip-gram đã được định nghĩa ở Section 14.1.

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

from d2l import mxnet as d2l
from mxnet import autograd, gluon, np, npx
from mxnet.gluon import nn
npx.set_np()

batch_size, max_window_size, num_noise_words = 512, 5, 5
data_iter, vocab = d2l.load_data_ptb(batch_size, max_window_size,
                                     num_noise_words)

14.4.1. Mô hình Skip-Gram

Ta sẽ lập trình mô hình skip-gram bằng cách sử dụng các tầng embedding và phép nhân minibatch. Các phương pháp này cũng thường được sử dụng để lập trình các ứng dụng xử lý ngôn ngữ tự nhiên khác.

14.4.1.1. Tầng Embedding

Để thu được các embedding từ, ta sử dụng tầng embedding, có thể được tạo bằng một thực thể nn.Embedding trong Gluon. Trọng số của tầng embedding là một ma trận có số hàng là kích thước từ điển (input_dim) và số cột là chiều của mỗi vector từ (output_dim). Ta đặt kích thước từ điển bằng \(20\) và chiều vector từ là \(4\).

embed = nn.Embedding(input_dim=20, output_dim=4)
embed.initialize()
embed.weight
Parameter embedding0_weight (shape=(20, 4), dtype=float32)

Đầu vào của tầng embedding là chỉ số của từ. Khi ta nhập vào chỉ số \(i\) của một từ, tầng embedding sẽ trả về vector từ tương ứng là hàng thứ \(i\) của ma trận trọng số. Dưới đây ta nhập vào tầng embedding một chỉ số có kích thước (\(2\), \(3\)). Vì số chiều vector từ là 4, ta thu được vector từ kích thước (\(2\), \(3\), \(4\)).

x = np.array([[1, 2, 3], [4, 5, 6]])
embed(x)
array([[[ 0.01438687,  0.05011239,  0.00628365,  0.04861524],
        [-0.01068833,  0.01729892,  0.02042518, -0.01618656],
        [-0.00873779, -0.02834515,  0.05484822, -0.06206018]],

       [[ 0.06491279, -0.03182812, -0.01631819, -0.00312688],
        [ 0.0408415 ,  0.04370362,  0.00404529, -0.0028032 ],
        [ 0.00952624, -0.01501013,  0.05958354,  0.04705103]]])

14.4.1.2. Phép nhân Minibatch

Ta có thể nhân các ma trận trong hai minibatch bằng toán tử nhân minibatch batch_dot. Giả sử batch đầu tiên chứa \(n\) ma trận \(\mathbf{X}_1, \ldots, \mathbf{X}_n\) có kích thước là \(a\times b\), và batch thứ hai chứa \(n\) ma trận \(\mathbf{Y}_1, \ldots, \mathbf{Y}_n\) có kích thước là \(b\times c\). Đầu ra của toán tử nhân ma trận trên hai batch đầu vào là \(n\) ma trận \(\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n\) có kích thước là \(a\times c\).
Do đó, với hai tensor có kích thước là (\(n\), \(a\), \(b\)) và (\(n\), \(b\), \(c\)), kích thước đầu ra của toán tử nhân minibatch là (\(n\), \(a\), \(c\)).
X = np.ones((2, 1, 4))
Y = np.ones((2, 4, 6))
npx.batch_dot(X, Y).shape
(2, 1, 6)

14.4.1.3. Tính toán Truyền xuôi của Mô hình Skip-Gram

Ở lượt truyền xuôi, đầu vào của mô hình skip-gram chứa chỉ số center của từ đích trung tâm và chỉ số contexts_and_negatives được nối lại từ chỉ số của từ ngữ cảnh và từ nhiễu. Trong đó, biến center có kích thước là (kích thước batch, 1), và biến contexts_and_negatives có kích thước là (kích thước batch, max_len). Đầu tiên hai biến này được biến đổi từ chỉ số từ thành vector từ bởi tầng embedding từ, sau đó đầu ra có kích thước là (kích thước batch, 1, max_len) thu được bằng phép nhân minibatch. Mỗi phần tử của đầu ra là tích vô hướng của vector từ đích trung tâm và vector từ ngữ cảnh hoặc vector từ nhiễu.

def skip_gram(center, contexts_and_negatives, embed_v, embed_u):
    v = embed_v(center)
    u = embed_u(contexts_and_negatives)
    pred = npx.batch_dot(v, u.swapaxes(1, 2))
    return pred

Hãy xác nhận kích thước đầu ra là (kích thước batch, 1, max_len).

skip_gram(np.ones((2, 1)), np.ones((2, 4)), embed, embed).shape
(2, 1, 4)

14.4.2. Huấn luyện

Trước khi huấn luyện mô hình embedding từ, ta cần định nghĩa hàm mất mát của mô hình.

14.4.2.1. Hàm Mất mát Entropy chéo Nhị phân

Theo định nghĩa hàm mất mát trong phương pháp lấy mẫu âm, ta có thể sử dụng trực tiếp hàm mất mát entropy chéo nhị phân của Gluon SigmoidBinaryCrossEntropyLoss.

loss = gluon.loss.SigmoidBinaryCrossEntropyLoss()

Lưu ý là ta có thể sử dụng biến mặt nạ để chỉ định một phần giá trị dự đoán và nhãn được dùng khi tính hàm mất mát trong minibatch: khi mặt nạ bằng 1, giá trị dự đoán và nhãn của vị trí tương ứng sẽ được dùng trong phép tính hàm mất mát; khi mặt nạ bằng 0, giá trị dự đoán và nhãn của vị trí tương ứng sẽ không được dùng trong phép tính hàm mất mát. Như đã đề cập, các biến mặt nạ có thể được sử dụng nhằm tránh ảnh hưởng của vùng đệm lên phép tính hàm mất mát.

Với hai mẫu giống nhau, mặt nạ khác nhau sẽ dẫn đến giá trị mất mát cũng khác nhau.

pred = np.array([[.5]*4]*2)
label = np.array([[1, 0, 1, 0]]*2)
mask = np.array([[1, 1, 1, 1], [1, 1, 0, 0]])
loss(pred, label, mask)
array([0.724077 , 0.3620385])

Ta có thể chuẩn hóa mất mát trong từng mẫu do các mẫu có độ dài khác nhau.

loss(pred, label, mask) / mask.sum(axis=1) * mask.shape[1]
array([0.724077, 0.724077])

14.4.2.2. Khởi tạo Tham số Mô hình

Ta khai báo tầng embedding lần lượt của từ trung tâm và từ ngữ cảnh, và đặt siêu tham số số chiều của vector từ embed_size bằng 100.

embed_size = 100
net = nn.Sequential()
net.add(nn.Embedding(input_dim=len(vocab), output_dim=embed_size),
        nn.Embedding(input_dim=len(vocab), output_dim=embed_size))

14.4.2.3. Huấn luyện

Hàm huấn luyện được định nghĩa như dưới đây. Do có phần đệm nên phép tính mất mát có một chút khác biệt so với các hàm huấn luyện trước.

def train(net, data_iter, lr, num_epochs, device=d2l.try_gpu()):
    net.initialize(ctx=device, force_reinit=True)
    trainer = gluon.Trainer(net.collect_params(), 'adam',
                            {'learning_rate': lr})
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[0, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # Sum of losses, no. of tokens
        for i, batch in enumerate(data_iter):
            center, context_negative, mask, label = [
                data.as_in_ctx(device) for data in batch]
            with autograd.record():
                pred = skip_gram(center, context_negative, net[0], net[1])
                l = (loss(pred.reshape(label.shape), label, mask)
                     / mask.sum(axis=1) * mask.shape[1])
            l.backward()
            trainer.step(batch_size)
            metric.add(l.sum(), l.size)
            if (i+1) % 50 == 0:
                animator.add(epoch+(i+1)/len(data_iter),
                             (metric[0]/metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, '
          f'{metric[1] / timer.stop():.1f} tokens/sec on {str(device)}')

Giờ ta có thể huấn luyện một mô hình skip-gram sử dụng phương pháp lấy mẫu âm.

lr, num_epochs = 0.01, 5
train(net, data_iter, lr, num_epochs)
loss 0.331, 24813.8 tokens/sec on gpu(0)
../_images/output_word2vec-pretraining_vn_8c89dc_23_1.svg

14.4.3. Áp dụng Mô hình Embedding Từ

Sau khi huấn luyện mô hình embedding từ, ta có thể biểu diễn sự tương tự về nghĩa giữa các từ dựa trên độ tương tự cô-sin giữa hai vector từ. Có thể thấy, khi sử dụng mô hình embedding từ đã được huấn luyện, các từ có nghĩa gần nhất với từ “chip” hầu hết là những từ có liên quan đến chip xử lý.

def get_similar_tokens(query_token, k, embed):
    W = embed.weight.data()
    x = W[vocab[query_token]]
    # Compute the cosine similarity. Add 1e-9 for numerical stability
    cos = np.dot(W, x) / np.sqrt(np.sum(W * W, axis=1) * np.sum(x * x) + 1e-9)
    topk = npx.topk(cos, k=k+1, ret_typ='indices').asnumpy().astype('int32')
    for i in topk[1:]:  # Remove the input words
        print(f'cosine sim={float(cos[i]):.3f}: {vocab.idx_to_token[i]}')

get_similar_tokens('chip', 3, net[0])
cosine sim=0.596: intel
cosine sim=0.466: computer
cosine sim=0.447: hewlett-packard

14.4.4. Tóm tắt

Ta có thể tiền huấn luyện một mô hình skip-gram thông qua phương pháp lấy mẫu âm.

14.4.5. Bài tập

  1. Đặt sparse_grad=True khi tạo một đối tượng nn.Embedding. Việc này có tăng tốc quá trình huấn luyện không? Hãy tra tài liệu của MXNet để tìm hiểu ý nghĩa của tham số này.
  2. Hãy tìm từ đồng nghĩa cho các từ khác.
  3. Điều chỉnh các siêu tham số, quan sát và phân tích kết quả thí nghiệm.
  4. Khi tập dữ liệu lớn, ta thường lấy mẫu các từ ngữ cảnh và các từ nhiễu cho từ đích trung tâm trong minibatch hiện tại chỉ khi cập nhật tham số mô hình. Nói cách khác, cùng một từ đích trung tâm có thể có các từ ngữ cảnh và từ nhiễu khác nhau với mỗi epoch khác nhau. Cách huấn luyện này có lợi ích gì? Hãy thử lập trình phương pháp huấn luyện này.

14.4.6. Thảo luận

14.4.7. 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
  • Đỗ Trường Giang
  • Phạm Minh Đức
  • Lê Khắc Hồng Phúc
  • Phạm Hồng Vinh
  • Nguyễn Văn Cường

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: 21/07/2020)