.. raw:: html .. raw:: html .. raw:: html .. _sec_attention: Cơ chế Tập trung ================ .. raw:: html Trong :numref:`sec_seq2seq`, chúng ta dùng mạng hồi tiếp để mã hóa thông tin của chuỗi nguồn đầu vào thành trạng thái ẩn và truyền nó tới bộ giải mã để sinh chuỗi đích. Một token trong chuỗi đích có thể chỉ liên quan mật thiết tới một vài token chứ không nhất thiết là toàn bộ token trong chuỗi nguồn. Ví dụ, khi dịch “Hello world.” thành “Bonjour le monde.”, từ “Bonjour” ánh xạ tới từ “Hello” và từ “monde” ánh xạ tới từ “world”. Trong mô hình seq2seq, bộ giải mã có thể ngầm chọn thông tin tương ứng từ trạng thái ẩn được truyền đến từ bộ mã hóa. Tuy nhiên, cơ chế tập trung (*attention mechanism*) thực hiện phép chọn này một cách tường minh. .. raw:: html Cơ chế *tập trung* có thể được coi là phép gộp tổng quát. Nó gộp đầu vào dựa trên các trọng số khác nhau. Thành phần cốt lõi của cơ chế tập trung là tầng tập trung. Đầu vào của tầng tập trung được gọi ngắn gọn là *câu truy vấn* (*query*). Với mỗi câu truy vấn, tầng tập trung trả về đầu ra dựa trên bộ nhớ là tập các cặp khóa-giá trị được mã hóa trong tầng tập trung này. Cụ thể, giả sử bộ nhớ chứa :math:`n` cặp vector khóa-giá trị, :math:`(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_n, \mathbf{v}_n)`, với :math:`\mathbf{k}_i \in \mathbb R^{d_k}`, :math:`\mathbf{v}_i \in \mathbb R^{d_v}`. Với mỗi vector truy vấn :math:`\mathbf{q} \in \mathbb R^{d_q}`, tầng tập trung trả về đầu ra :math:`\mathbf{o} \in \mathbb R^{d_v}` có cùng kích thước với vector giá trị. .. raw:: html .. _fig_attention: .. figure:: ../img/attention.svg Tầng tập trung trả về giá trị dựa trên câu truy vấn đầu vào và bộ nhớ của nó. .. raw:: html Chi tiết về cơ chế tập trung được minh họa trong :numref:`fig_attention_output`. Để tính toán đầu ra của tầng tập trung, chúng ta sử dụng hàm tính điểm :math:`\alpha` để đo độ tương đồng giữa câu truy vấn và các khóa. Sau đó, với mỗi khóa :math:`(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_n, \mathbf{v}_n)`, ta tính điểm :math:`a_1, \ldots, a_n` như sau: .. math:: a_i = \alpha(\mathbf q, \mathbf k_i). .. raw:: html Tiếp theo, chúng ta sử dụng hàm softmax để thu được các trọng số tập trung (*attention weights*), cụ thể: .. math:: \mathbf{b} = \mathrm{softmax}(\mathbf{a})\quad \text{trong đó }\quad {b}_i = \frac{\exp(a_i)}{\sum_j \exp(a_j)}, \mathbf{b} = [b_1, \ldots, b_n]^T . .. raw:: html .. raw:: html .. raw:: html Cuối cùng, đầu ra của tầng là tổng trọng số của các giá trị: .. math:: \mathbf o = \sum_{i=1}^n b_i \mathbf v_i. .. raw:: html .. _fig_attention_output: .. figure:: ../img/attention_output.svg Đầu ra của tầng tập trung là tổng trọng số của các giá trị. .. raw:: html Cách lựa chọn hàm tính điểm khác nhau sẽ tạo ra các tầng tập trung khác nhau. Ở dưới đây chúng tôi sẽ trình bày hai tầng tập trung thường hay được sử dụng. Đầu tiên chúng tôi giới thiệu hai toán tử cần thiết để lập trình hai tầng này: toán tử softmax có mặt nạ ``masked_softmax`` và toán tử tích vô hướng chuyên biệt theo batch ``batched_dot``. .. code:: python import math from mxnet import np, npx from mxnet.gluon import nn npx.set_np() .. raw:: html Toán tử softmax có mặt nạ nhận đầu vào là một tensor 3 chiều và cho phép ta lọc ra một số phần tử bằng cách xác định độ dài hợp lệ cho chiều cuối cùng. (Tham khảo :numref:`sec_machine_translation` về định nghĩa của độ dài hợp lệ). Do đó, những giá trị nằm ngoài độ dài hợp lệ sẽ được gán bằng :math:`0`. Chúng ta lập trình hàm ``masked_softmax`` như sau. .. code:: python # Saved in the d2l package for later use def masked_softmax(X, valid_len): # X: 3-D tensor, valid_len: 1-D or 2-D tensor if valid_len is None: return npx.softmax(X) else: shape = X.shape if valid_len.ndim == 1: valid_len = valid_len.repeat(shape[1], axis=0) else: valid_len = valid_len.reshape(-1) # Fill masked elements with a large negative, whose exp is 0 X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_len, True, axis=1, value=-1e6) return npx.softmax(X).reshape(shape) .. raw:: html Để minh họa cách hàm trên hoạt động, chúng ta hãy khởi tạo hai ma trận đầu vào kích thước là :math:`2 \times 4`. Bên cạnh đó, chúng ta sẽ gán độ dài hợp lệ cho mẫu thứ nhất là 2 và mẫu thứ hai là 3. Từ đó, những giá trị đầu ra nằm ngoài độ dài hợp lệ sẽ được gán bằng :math:`0` như dưới đây. .. code:: python masked_softmax(np.random.uniform(size=(2, 2, 4)), np.array([2, 3])) .. parsed-literal:: :class: output array([[[0.488994 , 0.511006 , 0. , 0. ], [0.43654838, 0.56345165, 0. , 0. ]], [[0.28817102, 0.3519408 , 0.3598882 , 0. ], [0.29034293, 0.25239873, 0.45725834, 0. ]]]) .. raw:: html Ngoài ra, toán tử thứ hai ``batched_dot`` nhận hai đầu vào là :math:`X` và :math:`Y` có kích thước lần lượt là :math:`(b, n, m)` và :math:`(b, m, k)`, và trả về đầu ra có kích thước là :math:`(b, n, k)`. Cụ thể, toán tử này tính :math:`b` tích vô hướng với :math:`i= \{1,\ldots, b\}` như sau: .. math:: Z[i,:,:] = X[i,:,:] Y[i,:,:]. .. code:: python npx.batch_dot(np.ones((2, 1, 3)), np.ones((2, 3, 2))) .. parsed-literal:: :class: output array([[[3., 3.]], [[3., 3.]]]) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Tầng Tập trung Tích Vô hướng ---------------------------- .. raw:: html Với hai toán tử ``masked_softmax`` và ``batched_dot`` ở trên, chúng ta sẽ đi vào chi tiết hai loại tầng tập trung được sử dụng phổ biến. Loại đầu tiên là *tập trung tích vô hướng* (*dot product attention*): nó giả định rằng câu truy vấn có cùng kích thước chiều với khóa, cụ thể là :math:`\mathbf q, \mathbf k_i \in\mathbb R^d` với mọi :math:`i`. Tầng tập trung tích vô hướng sẽ tính điểm bằng cách lấy tích vô hướng giữa câu truy vấn và khóa, sau đó chia cho :math:`\sqrt{d}` để giảm thiểu ảnh hưởng không liên quan của số chiều :math:`d` lên điểm số. Nói cách khác, .. math:: \alpha(\mathbf q, \mathbf k) = \langle \mathbf q, \mathbf k \rangle /\sqrt{d}. .. raw:: html Mở rộng ra từ các câu truy vấn và khóa một chiều, chúng ta luôn có thể tổng quát hóa chúng lên thành các giá trị truy vấn và khóa đa chiều. Giả định rằng :math:`\mathbf Q\in\mathbb R^{m\times d}` chứa :math:`m` câu truy vấn và :math:`\mathbf K\in\mathbb R^{n\times d}` chứa toàn bộ :math:`n` khóa. Chúng ta có thể tính toàn bộ :math:`mn` điểm số như sau .. math:: \alpha(\mathbf Q, \mathbf K) = \mathbf Q \mathbf K^\top /\sqrt{d}. :label: eq_alpha_QK .. raw:: html Với :eq:`eq_alpha_QK`, chúng ta có thể lập trình tầng tập trung tích vô hướng ``DotProductAttention`` hỗ trợ một batch các câu truy vấn và các cặp khóa-giá trị. Ngoài ra, chúng ta cũng dùng thêm một tầng dropout để điều chuẩn. .. code:: python # Saved in the d2l package for later use class DotProductAttention(nn.Block): def __init__(self, dropout, **kwargs): super(DotProductAttention, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) # query: (batch_size, #queries, d) # key: (batch_size, #kv_pairs, d) # value: (batch_size, #kv_pairs, dim_v) # valid_len: either (batch_size, ) or (batch_size, xx) def forward(self, query, key, value, valid_len=None): d = query.shape[-1] # Set transpose_b=True to swap the last two dimensions of key scores = npx.batch_dot(query, key, transpose_b=True) / math.sqrt(d) attention_weights = self.dropout(masked_softmax(scores, valid_len)) return npx.batch_dot(attention_weights, value) .. raw:: html Hãy kiểm tra lớp ``DotProductAttention`` với một ví dụ nhỏ sau. Đầu tiên ta tạo 2 batch, mỗi batch có 1 câu truy vấn và 10 cặp khóa-giá trị. Thông qua đối số ``valid_len``, ta chỉ định rằng ta sẽ kiểm tra :math:`2` cặp khóa-giá trị đầu tiên cho batch đầu tiên và :math:`6` cặp cho batch thứ hai. Do đó, mặc dù cả hai batch đều có cùng câu truy vấn và các cặp khóa-giá trị, chúng ta sẽ thu được các đầu ra khác nhau. .. code:: python atten = DotProductAttention(dropout=0.5) atten.initialize() keys = np.ones((2, 10, 2)) values = np.arange(40).reshape(1, 10, 4).repeat(2, axis=0) atten(np.ones((2, 1, 2)), keys, values, np.array([2, 6])) .. parsed-literal:: :class: output array([[[ 2. , 3. , 4. , 5. ]], [[10. , 11. , 12.000001, 13. ]]]) .. raw:: html Như đã thấy ở trên, tập trung tích vô hướng chỉ đơn thuần nhân câu truy vấn và khóa lại với nhau, hi vọng rằng từ đó thu được những điểm tương đồng giữa chúng. Tuy nhiên, câu truy vấn và khóa có thể không có cùng kích thước chiều. Để giải quyết vấn đề này, chúng ta cần nhờ đến cơ chế tập trung perceptron đa tầng. .. raw:: html .. raw:: html .. raw:: html Tập trung Perceptron Đa tầng ---------------------------- .. raw:: html Trong cơ chế *tập trung perceptron đa tầng* (*multilayer perceptron attention*), chúng ta chiếu cả câu truy vấn và các khóa lên :math:`\mathbb R^{h}` bằng các tham số trọng số được học. Giả định rằng các trọng số được học là :math:`\mathbf W_k\in\mathbb R^{h\times d_k}`, :math:`\mathbf W_q\in\mathbb R^{h\times d_q}` và :math:`\mathbf v\in\mathbb R^{h}`. Hàm tính điểm sẽ được định nghĩa như sau .. math:: \alpha(\mathbf k, \mathbf q) = \mathbf v^\top \text{tanh}(\mathbf W_k \mathbf k + \mathbf W_q\mathbf q). .. raw:: html Một cách trực quan, ta có thể tưởng tượng :math:`\mathbf W_k \mathbf k + \mathbf W_q\mathbf q` chính là việc nối khóa và giá trị lại với nhau theo chiều đặc trưng và đưa chúng qua perceptron có một tầng ẩn với kích thước là :math:`h` và tầng đầu ra với kích thước là :math:`1`. Trong tầng ẩn này, hàm kích hoạt là :math:`tanh` và không có hệ số điều chỉnh. Giờ hãy lập trình một tầng tập trung perceptron đa tầng. .. code:: python # Saved in the d2l package for later use class MLPAttention(nn.Block): def __init__(self, units, dropout, **kwargs): super(MLPAttention, self).__init__(**kwargs) # Use flatten=True to keep query's and key's 3-D shapes self.W_k = nn.Dense(units, activation='tanh', use_bias=False, flatten=False) self.W_q = nn.Dense(units, activation='tanh', use_bias=False, flatten=False) self.v = nn.Dense(1, use_bias=False, flatten=False) self.dropout = nn.Dropout(dropout) def forward(self, query, key, value, valid_len): query, key = self.W_q(query), self.W_k(key) # Expand query to (batch_size, #querys, 1, units), and key to # (batch_size, 1, #kv_pairs, units). Then plus them with broadcast features = np.expand_dims(query, axis=2) + np.expand_dims(key, axis=1) scores = np.squeeze(self.v(features), axis=-1) attention_weights = self.dropout(masked_softmax(scores, valid_len)) return npx.batch_dot(attention_weights, value) .. raw:: html Để kiểm tra lớp ``MLPAttention`` phía trên, chúng ta sẽ sử dụng lại đầu vào ở ví dụ đơn giản trước. Như ta thấy ở dưới, mặc dù ``MLPAttention`` chứa thêm một mô hình MLP, chúng ta vẫn thu được đầu ra tương tự ``DotProductAttention``. .. code:: python atten = MLPAttention(units=8, dropout=0.1) atten.initialize() atten(np.ones((2, 1, 2)), keys, values, np.array([2, 6])) .. parsed-literal:: :class: output array([[[ 2. , 3. , 4. , 5. ]], [[10. , 11. , 12.000001, 13. ]]]) .. raw:: html Tóm tắt ------- .. raw:: html - Tầng tập trung lựa chọn một cách tường minh các thông tin liên quan. - Ô nhớ của tầng tập trung chứa các cặp khóa-giá trị, do đó đầu ra của nó gần các giá trị có khóa giống với câu truy vấn. - Hai mô hình tập trung được sử dụng phổ biến là tập trung tích vô hướng và tập trung perceptron đa tầng. .. raw:: html Bài tập ------- .. raw:: html Ưu và khuyết điểm của tầng tập trung tích vô hướng và tập trung perceptron đa tầng là gì? .. raw:: html .. raw:: html Thảo luận --------- - `Tiếng Anh `__ - `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 Cảnh Thướng - Nguyễn Văn Cường - Võ Tấn Phát - Lê Khắc Hồng Phúc - Phạm Minh Đức - Phạm Hồng Vinh