15.5. Suy luận Ngôn ngữ Tự nhiên: Sử dụng Cơ chế Tập trung¶
Chúng tôi đã giới thiệu tác vụ suy luận ngôn ngữ tự nhiên và tập dữ liệu SNLI trong Section 15.4. Trong nhiều mô hình dựa trên các kiến trúc sâu và phức tạp, Parikh và các cộng sự đề xuất hướng giải quyết bài toán suy luận ngôn ngữ tự nhiên bằng cơ chế tập trung và gọi nó là một “mô hình tập trung có thể phân tách” (decomposable attention model) [Parikh et al., 2016]. Điều này dẫn đến một mô hình không có các tầng truy hồi hay tích chập, nhưng đạt được kết quả tốt nhất vào thời điểm đó trên tập dữ liệu SNLI với lượng tham số ít hơn nhiều. Trong phần này, chúng tôi sẽ mô tả và lập trình phương pháp dựa trên cơ chế tập trung (cùng với MLP) để suy luận ngôn ngữ tự nhiên, như minh họa trong Fig. 15.5.1.
15.5.1. Mô hình¶
Đơn giản hơn so với việc duy trì thứ tự của các từ trong các tiền đề và giả thuyết, ta có thể căn chỉnh các từ trong một chuỗi văn bản với mọi từ trong chuỗi khác và ngược lại, rồi so sánh và kết hợp các thông tin đó để dự đoán mối quan hệ logic giữa tiền đề và giả thuyết. Tương tự như việc căn chỉnh các từ giữa câu nguồn và đích trong dịch máy, việc căn chỉnh các từ giữa tiền đề và giả thuyết có thể được thực hiện nhanh gọn nhờ cơ chế tập trung.
Fig. 15.5.2 minh họa phương pháp suy luận ngôn ngữ tự nhiên sử dụng cơ chế tập trung. Ở mức cao, nó bao gồm ba bước huấn luyện phối hợp: thực hiện tập trung, so sánh, và kết hợp. Ta sẽ từng bước mô tả chúng trong phần tiếp theo.
from d2l import mxnet as d2l
import mxnet as mx
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
15.5.1.1. Thực hiện Tập trung¶
Bước đầu tiên là phải căn chỉnh các từ trong một chuỗi văn bản với một chuỗi khác. Giả sử câu tiền đề là “i do need sleep” và câu giả thuyết là “i am tired”. Do sự tương đồng về ngữ nghĩa, ta mong muốn căn chỉnh “i” trong câu giả thuyết với “i” trong câu tiền đề, và căn chỉnh “tired” trong câu giả thuyết với “sleep” trong câu tiền đề. Tương tự, ta muốn căn chỉnh “i” trong câu tiền đề với “i” trong câu giả thuyết, và căn chỉnh “need” và “sleep” trong câu tiền đề với “tired” trong câu giả thuyết. Lưu ý là sự căn chỉnh này là mềm, sử dụng trung bình có trọng số, trong đó các trọng số nên có độ lớn hợp lý ứng với các từ được căn chỉnh. Để dễ dàng cho việc minh họa, Fig. 15.5.2 diễn tả sự căn chỉnh này theo cách cứng.
Bây giờ ta mô tả sự căn chỉnh mềm sử dụng cơ chế tập trung chi tiết hơn. Ký hiệu \(\mathbf{A} = (\mathbf{a}_1, \ldots, \mathbf{a}_m)\) và \(\mathbf{B} = (\mathbf{b}_1, \ldots, \mathbf{b}_n)\) là câu tiền đề và câu giả thuyết, với số từ lần lượt là \(m\) và \(n\). Ở đây \(\mathbf{a}_i, \mathbf{b}_j \in \mathbb{R}^{d}\) (\(i = 1, \ldots, m, j = 1, \ldots, n\)) là một vector embedding từ \(d\)-chiều. Để căn chỉnh mềm, ta tính trọng số tập trung \(e_{ij} \in \mathbb{R}\) như sau
ở đây hàm \(f\) là một MLP được định nghĩa theo hàm mlp
. Chiều
đầu ra của \(f\) được thiết lập bởi đối số num_hiddens
của hàm
mlp
.
def mlp(num_hiddens, flatten):
net = nn.Sequential()
net.add(nn.Dropout(0.2))
net.add(nn.Dense(num_hiddens, activation='relu', flatten=flatten))
net.add(nn.Dropout(0.2))
net.add(nn.Dense(num_hiddens, activation='relu', flatten=flatten))
return net
Cũng nên chú ý rằng, trong (15.5.1) \(f\) nhận hai đầu vào \(\mathbf{a}_i\) và \(\mathbf{b}_j\) riêng biệt thay vì nhận cả cặp làm đầu vào. Thủ thuật phân tách này dẫn tới việc chỉ có \(m + n\) lần tính (độ phức tạp tuyến tính) \(f\) thay vì \(mn\) (độ phức tạp bậc hai).
Thực hiện chuẩn hóa các trọng số tập trung trong (15.5.1), ta tính trung bình có trọng số của tất cả các embedding từ trong câu giả thuyết để thu được biểu diễn của câu giả thuyết được căn chỉnh mềm với từ được đánh chỉ số \(i\) trong câu tiền đề:
Tương tự, ta tính sự căn chỉnh mềm của các từ trong câu tiền đề cho mỗi từ được đánh chỉ số \(j\) trong câu giả thuyết:
Dưới đây ta định nghĩa lớp Attend
để tính sự căn chỉnh mềm của các
câu giả thuyết (beta
) với các câu tiền đề đầu vào A
và sự căn
chỉnh mềm của các câu tiền đề (alpha
) với các câu giả thuyết B
.
class Attend(nn.Block):
def __init__(self, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_hiddens=num_hiddens, flatten=False)
def forward(self, A, B):
# Shape of `A`/`B`: (b`atch_size`, no. of words in sequence A/B,
# `embed_size`)
# Shape of `f_A`/`f_B`: (`batch_size`, no. of words in sequence A/B,
# `num_hiddens`)
f_A = self.f(A)
f_B = self.f(B)
# Shape of `e`: (`batch_size`, no. of words in sequence A,
# no. of words in sequence B)
e = npx.batch_dot(f_A, f_B, transpose_b=True)
# Shape of `beta`: (`batch_size`, no. of words in sequence A,
# `embed_size`), where sequence B is softly aligned with each word
# (axis 1 of `beta`) in sequence A
beta = npx.batch_dot(npx.softmax(e), B)
# Shape of `alpha`: (`batch_size`, no. of words in sequence B,
# `embed_size`), where sequence A is softly aligned with each word
# (axis 1 of `alpha`) in sequence B
alpha = npx.batch_dot(npx.softmax(e.transpose(0, 2, 1)), A)
return beta, alpha
15.5.1.2. So sánh¶
Tại bước so sánh, chúng ta đưa những từ đã được ghép nối (toán tử \([\cdot, \cdot]\)) và những từ đã căn chỉnh của chuỗi còn lại vào hàm \(g\) (một MLP):
Trong (15.5.4), \(\mathbf{v}_{A,i}\) là phép so sánh
giữa từ thứ \(i\) của câu tiền đề và tất cả các từ trong câu giả
thuyết được căn chỉnh mềm với từ thứ \(i\); trong khi
\(\mathbf{v}_{B,j}\) lại là phép so sánh giữa từ thứ \(j\) trong
câu giả thuyết và tất cả từ trong câu tiền đề được căn chỉnh mềm với từ
thứ \(j\). Lớp Compare
sau định nghĩa bước so sánh này.
class Compare(nn.Block):
def __init__(self, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_hiddens=num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(np.concatenate([A, beta], axis=2))
V_B = self.g(np.concatenate([B, alpha], axis=2))
return V_A, V_B
15.5.1.3. Tổng hợp¶
Với hai tập vector so sánh \(\mathbf{v}_{A,i}\) (\(i = 1, \ldots, m\)) và \(\mathbf{v}_{B, j}\) (\(j = 1 , \ldots, n\)) trong tay, ta sẽ tổng hợp các thông tin đó để suy ra mối quan hệ logic tại bước cuối cùng. Chúng ta bắt đầu bằng cách lấy tổng trên cả hai tập:
Tiếp theo, chúng ta ghép nối hai kết quả tổng rồi đưa vào hàm \(h\) (một MLP) để thu được kết quả phân loại của mối quan hệ logic:
Bước tổng hợp được định nghĩa trong lớp Aggregate
sau đây.
class Aggregate(nn.Block):
def __init__(self, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_hiddens=num_hiddens, flatten=True)
self.h.add(nn.Dense(num_outputs))
def forward(self, V_A, V_B):
# Sum up both sets of comparison vectors
V_A = V_A.sum(axis=1)
V_B = V_B.sum(axis=1)
# Feed the concatenation of both summarization results into an MLP
Y_hat = self.h(np.concatenate([V_A, V_B], axis=1))
return Y_hat
15.5.1.4. Kết hợp tất cả lại¶
Bằng cách gộp các bước thực hiện tập trung, so sánh và tổng hợp lại với nhau, ta định nghĩa mô hình tập trung có thể phân tách để cùng huấn luyện cả ba bước này.
class DecomposableAttention(nn.Block):
def __init__(self, vocab, embed_size, num_hiddens, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_hiddens)
self.compare = Compare(num_hiddens)
# There are 3 possible outputs: entailment, contradiction, and neutral
self.aggregate = Aggregate(num_hiddens, 3)
def forward(self, X):
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
15.5.2. Huấn luyện và Đánh giá Mô hình¶
Bây giờ ta sẽ huấn luyện và đánh giá mô hình tập trung có thế phân tách vừa được định nghĩa trên tập dữ liệu SNLI. Ta bắt đầu bằng việc đọc tập dữ liệu.
15.5.2.1. Đọc tập dữ liệu¶
Ta tải xuống và đọc tập dữ liệu SNLI bằng hàm định nghĩa trong Section 15.4. Kích thước batch và độ dài chuỗi được đặt là \(256\) và \(50\).
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)
read 549367 examples
read 9824 examples
15.5.2.2. Tạo mô hình¶
Ta sử dụng embedding GloVe \(100\)-chiều đã tiền huấn luyện để biểu
diễn các token đầu vào. Do đó, ta định nghĩa trước chiều của các vector
\(\mathbf{a}_i\) và \(\mathbf{b}_j\) trong :eqref:eq_nli_e
là \(100\). Chiều đầu ra của hàm \(f\) trong :eqref:eq_nli_e
và \(g\) trong :eqref:eq_nli_v_ab
được đặt bằng \(200\). Sau
đó ta tạo thực thể của mô hình, khởi tạo tham số, và nạp embedding GloVe
để khởi tạo các vector token đầu vào.
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
net.initialize(init.Xavier(), ctx=devices)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.set_data(embeds)
Downloading ../data/glove.6B.100d.zip from http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip...
15.5.2.3. Huấn luyện và Đánh giá Mô hình¶
Trái ngược với hàm split_batch
trong Section 12.5 nhận
đầu vào đơn như một chuỗi văn bản (hoặc ảnh) chẳng hạn, ta định nghĩa
hàm split_batch_multi_inputs
để nhận đa đầu vào, ví dụ như cặp tiền
đề và giả thuyết ở trong các minibatch.
#@save
def split_batch_multi_inputs(X, y, devices):
"""Split multi-input `X` and `y` into multiple devices."""
X = list(zip(*[gluon.utils.split_and_load(
feature, devices, even_split=False) for feature in X]))
return (X, gluon.utils.split_and_load(y, devices, even_split=False))
Giờ ta có thể huấn luyện và đánh giá mô hình trên tập dữ liệu SNLI.
lr, num_epochs = 0.001, 4
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices,
split_batch_multi_inputs)
loss 0.522, train acc 0.793, test acc 0.814
26971.0 examples/sec on [gpu(0)]
15.5.2.4. Sử dụng Mô hình¶
Cuối cùng, ta định nghĩa hàm dự đoán để xuất ra mối quan hệ logic giữa cặp tiền đề và giả thuyết.
#@save
def predict_snli(net, vocab, premise, hypothesis):
premise = np.array(vocab[premise], ctx=d2l.try_gpu())
hypothesis = np.array(vocab[hypothesis], ctx=d2l.try_gpu())
label = np.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), axis=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
Ta có thể sử dụng mô hình đã huấn luyện để thu được kết quả suy luận ngôn ngữ tự nhiên cho các cặp câu mẫu.
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
'contradiction'
15.5.3. Tóm tắt¶
- Mô hình tập trung có thể phân tách bao gồm 3 bước để dự đoán mối quan hệ logic giữa cặp tiền đề và giả thuyết: thực hiện tập trung, so sánh và tổng hợp.
- Với cơ chế tập trung, ta có thể căn chỉnh các từ trong một chuỗi văn bản với tất cả các từ trong chuỗi văn bản còn lại, và ngược lại. Đây là kỹ thuật căn chỉnh mềm, sử dụng trung bình có trọng số, trong đó các trọng số có độ lớn hợp lý được gán với các từ sẽ được căn chỉnh.
- Thủ thuật phân tách tầng tập trung giúp giảm độ phức tạp thành tuyến tính thay vì là bậc hai khi tính toán trọng số tập trung.
- Ta có thể sử dụng embedding từ đã tiền huấn luyện để biểu diễn đầu vào cho các tác vụ xử lý ngôn ngữ tự nhiên xuôi dòng, ví dụ như suy luận ngôn ngữ tự nhiên.
15.5.4. Bài tập¶
- Huấn luyện mô hình với các tập siêu tham số khác nhau. Bạn có thể thu được độ chính xác cao hơn trên tập kiểm tra không?
- Những điểm hạn chế chính của mô hình tập trung kết hợp đối với suy luận ngôn ngữ tự nhiên là gì?
- Giả sử ta muốn tính độ tương tự ngữ nghĩa (một giá trị liên tục trong khoảng \(0\) và \(1\)) cho một cặp câu bất kỳ. Ta sẽ thu thập và gán nhãn tập dữ liệu như thế nào? Bạn có thể thiết kế một mô hình với cơ chế tập trung không?
15.5.5. Thảo luận¶
- Tiếng Anh: MXNet
- Tiếng Việt: Diễn đàn Machine Learning Cơ Bản
15.5.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 Mai Hoàng Long
- Phạm Đăng Khoa
- Lê Khắc Hồng Phúc
- Phạm Hồng Vinh
- Nguyễn Văn Cường
- Nguyễn Lê Quang Nhật
- Phạm Minh Đức
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: 19/09/2020)