.. raw:: html .. raw:: html .. raw:: html .. _sec_transformer: Kiến trúc Transformer ===================== .. raw:: html Trong các chương trước, ta đã đề cập đến các kiến trúc mạng nơ-ron quan trọng như mạng nơ-ron tích chập (CNN) và mạng nơ-ron hồi tiếp (RNN). Ưu nhược điểm của hai kiến trúc mạng này có thể được tóm tắt như sau: .. raw:: html - Các mạng **CNN** có thể dễ dàng được thực hiện song song ở một tầng nhưng không có khả năng nắm bắt các phụ thuộc chuỗi có độ dài biến thiên. .. raw:: html - Các mạng **RNN** có khả năng nắm bắt các thông tin cách xa nhau trong chuỗi có độ dài biến thiên, nhưng không thể thực hiện song song trong một chuỗi. .. raw:: html Để kết hợp các ưu điểm của CNN và RNN, :cite:`Vaswani.Shazeer.Parmar.ea.2017` đã thiết kế một kiến trúc mới bằng cách sử dụng cơ chế tập trung. Kiến trúc này gọi là *Transformer*, song song hóa bằng cách học chuỗi hồi tiếp với cơ chế tập trung, đồng thời mã hóa vị trí của từng phần tử trong chuỗi. Kết quả là ta có một mô hình tương thích với thời gian huấn luyện ngắn hơn đáng kể. .. raw:: html Tương tự như mô hình seq2seq trong :numref:`sec_seq2seq`, Transformer cũng dựa trên kiến trúc mã hóa-giải mã. Tuy nhiên, nó thay thế các tầng hồi tiếp trong seq2seq bằng các tầng *tập trung đa đầu* (*multi-head attention*), kết hợp thông tin vị trí thông qua *biểu diễn vị trí* (*positional encoding*) và áp dụng *chuẩn hóa tầng* (*layer normalization*). :numref:`fig_transformer` sẽ so sánh cấu trúc của Transformer và seq2seq. .. raw:: html Nhìn chung, hai mô hình này khá giống nhau: các embedding của chuỗi nguồn được đưa vào :math:`n` khối lặp lại. Đầu ra của khối mã hóa cuối cùng sau đó được sử dụng làm bộ nhớ tập trung cho bộ giải mã. Tương tự, các embedding của chuỗi đích được đưa vào :math:`n` khối lặp lại trong bộ giải mã. Ta thu được đầu ra cuối cùng bằng cách áp dụng một tầng dày đặc có kích thước bằng kích thước bộ từ vựng lên các đầu ra của khối giải mã cuối cùng. .. raw:: html .. _fig_transformer: .. figure:: ../img/transformer.svg :width: 500px Kiến trúc Transformer. .. raw:: html .. raw:: html .. raw:: html Mặt khác, Transformer khác với mô hình seq2seq sử dụng cơ chế tập trung như sau: .. raw:: html 1. **Khối Transformer**: một tầng hồi tiếp trong seq2seq được thay bằng một *Khối Transformer*. Với bộ mã hóa, khối này chứa một tầng *tập trung đa đầu* và một *mạng truyền xuôi theo vị trí* (*position-wise feed-forward network*) gồm hai tầng dày đặc. Đối với bộ giải mã, khối này có thêm một tầng tập trung đa đầu khác để nhận vào trạng thái bộ mã hóa. 2. **Cộng và chuẩn hóa**: đầu vào và đầu ra của cả tầng tập trung đa đầu hoặc mạng truyền xuôi theo vị trí được xử lý bởi hai tầng “cộng và chuẩn hóa” bao gồm cấu trúc phần dư và tầng *chuẩn hóa theo tầng* (*layer normalization*). 3. **Biễu diễn vị trí**: do tầng tự tập trung không phân biệt thứ tự phần tử trong một chuỗi, nên tầng biễu diễn vị trí được sử dụng để thêm thông tin vị trí vào từng phần tử trong chuỗi. .. raw:: html Tiếp theo, chúng ta sẽ tìm hiểu từng thành phần trong Transformer để có thể xây dựng một mô hình dịch máy. .. code:: python from d2l import mxnet as d2l import math from mxnet import autograd, np, npx from mxnet.gluon import nn npx.set_np() .. raw:: html Tập trung Đa đầu ---------------- .. raw:: html Trước khi thảo luận về tầng *tập trung đa đầu*, hãy cùng tìm hiểu qua về kiến trúc *tự tập trung*. Giống như các mô hình tập trung bình thường, mô hình tự tập trung cũng có câu truy vấn, khóa và giá trị nhưng chúng được sao chép từ các phần tử trong chuỗi đầu vào. Như minh họa trong :numref:`fig_self_attention`, tầng tự tập trung trả về một đầu ra tuần tự có cùng độ dài với đầu vào. So với tầng hồi tiếp, các phần tử đầu ra của tầng tự tập trung có thể được tính toán song song, do đó việc xây dựng các đoạn mã tốc độ cao khá dễ dàng. .. raw:: html .. _fig_self_attention: .. figure:: ../img/self-attention.svg Kiến trúc tự tập trung. .. raw:: html Tầng *tập trung đa đầu* bao gồm :math:`h` *đầu* là các tầng tự tập trung song song. Trước khi đưa vào mỗi đầu, ta chiếu các câu truy vấn, khóa và giá trị qua ba tầng dày đặc với kích thước ẩn lần lượt là :math:`p_q`, :math:`p_k` và :math:`p_v`. Đầu ra của :math:`h` đầu này được nối lại và sau đó được xử lý bởi một tầng dày đặc cuối cùng. .. raw:: html .. figure:: ../img/multi-head-attention.svg Tập trung đa đầu .. raw:: html .. raw:: html .. raw:: html Giả sử chiều của câu truy vấn, khóa và giá trị lần lượt là :math:`d_q`, :math:`d_k` và :math:`d_v`. Khi đó, tại mỗi đầu :math:`i=1,\ldots, h`, ta có thể học các tham số :math:`\mathbf W_q^{(i)}\in\mathbb R^{p_q\times d_q}`, :math:`\mathbf W_k^{(i)}\in\mathbb R^{p_k\times d_k}`, và :math:`\mathbf W_v^{(i)}\in\mathbb R^{p_v\times d_v}`. Do đó, đầu ra tại mỗi đầu là .. math:: \mathbf o^{(i)} = \textrm{attention}(\mathbf W_q^{(i)}\mathbf q, \mathbf W_k^{(i)}\mathbf k,\mathbf W_v^{(i)}\mathbf v), .. raw:: html trong đó :math:`\textrm{attention}` có thể là bất kỳ tầng tập trung nào, chẳng hạn như ``DotProductAttention`` và\ ``MLPAttention`` trong :numref:`sec_attention`. .. raw:: html Sau đó, :math:`h` đầu ra với độ dài :math:`p_v` tại mỗi đầu được nối với nhau thành đầu ra có độ dài :math:`h p_v`, rồi được đưa vào tầng dày đặc cuối cùng với :math:`d_o` nút ẩn. Các trọng số của tầng dày đặc này được ký hiệu là :math:`\mathbf W_o\in\mathbb R^{d_o\times h p_v}`. Do đó, đầu ra cuối cùng của tầng tập trung đa đầu sẽ là .. math:: \mathbf o = \mathbf W_o \begin{bmatrix}\mathbf o^{(1)}\\\vdots\\\mathbf o^{(h)}\end{bmatrix}. .. raw:: html Bây giờ chúng ta có thể lập trình tầng tập trung đa đầu. Giả sử tầng tập trung đa đầu có số đầu là ``num_heads`` :math:`=h` và các tầng dày đặc cho câu truy vấn, khóa và giá trị có kích thước ẩn giống nhau ``num_hiddens`` :math:`=p_q=p_k=p_v`. Ngoài ra, do tầng tập trung đa đầu giữ nguyên kích thước chiều đầu vào, kích thước đặc trưng đầu ra cũng là :math:`d_o =` ``num_hiddens``. .. code:: python # Saved in the d2l package for later use class MultiHeadAttention(nn.Block): def __init__(self, num_hiddens, num_heads, dropout, use_bias=False, **kwargs): super(MultiHeadAttention, self).__init__(**kwargs) self.num_heads = num_heads self.attention = d2l.DotProductAttention(dropout) self.W_q = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) self.W_k = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) self.W_v = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) self.W_o = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) def forward(self, query, key, value, valid_len): # For self-attention, query, key, and value shape: # (batch_size, seq_len, dim), where seq_len is the length of input # sequence. valid_len shape is either (batch_size, ) or # (batch_size, seq_len). # Project and transpose query, key, and value from # (batch_size, seq_len, num_hiddens) to # (batch_size * num_heads, seq_len, num_hiddens / num_heads). query = transpose_qkv(self.W_q(query), self.num_heads) key = transpose_qkv(self.W_k(key), self.num_heads) value = transpose_qkv(self.W_v(value), self.num_heads) if valid_len is not None: # Copy valid_len by num_heads times if valid_len.ndim == 1: valid_len = np.tile(valid_len, self.num_heads) else: valid_len = np.tile(valid_len, (self.num_heads, 1)) # For self-attention, output shape: # (batch_size * num_heads, seq_len, num_hiddens / num_heads) output = self.attention(query, key, value, valid_len) # output_concat shape: (batch_size, seq_len, num_hiddens) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) .. raw:: html Dưới đây là định nghĩa của hai hàm chuyển vị ``transpose_qkv`` và ``transpose_output``. Hai hàm này là nghịch đảo của nhau. .. code:: python # Saved in the d2l package for later use def transpose_qkv(X, num_heads): # Input X shape: (batch_size, seq_len, num_hiddens). # Output X shape: # (batch_size, seq_len, num_heads, num_hiddens / num_heads) X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) # X shape: (batch_size, num_heads, seq_len, num_hiddens / num_heads) X = X.transpose(0, 2, 1, 3) # output shape: (batch_size * num_heads, seq_len, num_hiddens / num_heads) output = X.reshape(-1, X.shape[2], X.shape[3]) return output # Saved in the d2l package for later use def transpose_output(X, num_heads): # A reversed version of transpose_qkv X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) X = X.transpose(0, 2, 1, 3) return X.reshape(X.shape[0], X.shape[1], -1) .. raw:: html Hãy cùng kiểm tra mô hình ``MultiHeadAttention`` qua một ví dụ đơn giản. Tạo một tầng tập trung đa đầu với kích thước ẩn :math:`d_o = 100`, đầu ra sẽ có cùng kích thước batch và độ dài chuỗi với đầu vào, nhưng có kích thước chiều cuối cùng bằng ``num_hiddens`` :math:`= 100`. .. code:: python cell = MultiHeadAttention(90, 9, 0.5) cell.initialize() X = np.ones((2, 4, 5)) valid_len = np.array([2, 3]) cell(X, X, X, valid_len).shape .. parsed-literal:: :class: output (2, 4, 90) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Mạng truyền Xuôi theo Vị trí ---------------------------- .. raw:: html Một thành phần quan trọng khác trong Khối Transformer là *mạng truyền xuôi theo vị trí* (*position-wise feed-forward network*). Nó chấp nhận đầu vào :math:`3` chiều với kích thước là (kích thước batch, độ dài chuỗi, kích thước đặc trưng). Mạng truyền xuôi theo vị trí bao gồm hai tầng dày đặc áp dụng trên chiều cuối cùng của đầu vào. Vì hai tầng dày đặc này cùng được sử dụng cho từng vị trí trong chuỗi, nên ta gọi là mạng truyền xuôi *theo vị trí*. Cách làm này tương đương với việc áp dụng hai tầng tích chập :math:`1 \times 1`. .. raw:: html Lớp ``PositionWiseFFN`` dưới đây lập trình mạng truyền xuôi theo vị trí với hai tầng dày đặc có kích thước ẩn lần lượt là ``ffn_num_hiddens`` và ``pw_num_outputs``. .. code:: python # Saved in the d2l package for later use class PositionWiseFFN(nn.Block): def __init__(self, ffn_num_hiddens, pw_num_outputs, **kwargs): super(PositionWiseFFN, self).__init__(**kwargs) self.dense1 = nn.Dense(ffn_num_hiddens, flatten=False, activation='relu') self.dense2 = nn.Dense(pw_num_outputs, flatten=False) def forward(self, X): return self.dense2(self.dense1(X)) .. raw:: html Tương tự như tầng tập trung đa đầu, mạng truyền xuôi theo vị trí sẽ chỉ thay đổi kích thước chiều cuối cùng của đầu vào — tức kích thước của đặc trưng. Ngoài ra, nếu hai phần tử trong chuỗi đầu vào giống hệt nhau, thì hai đầu ra tương ứng cũng sẽ giống hệt nhau. .. code:: python ffn = PositionWiseFFN(4, 8) ffn.initialize() ffn(np.ones((2, 3, 4)))[0] .. parsed-literal:: :class: output array([[ 9.15348239e-04, -7.27669394e-04, 1.14063594e-04, -8.76279722e-04, -1.02867256e-03, 8.02748313e-04, -4.53725770e-05, 2.15598906e-04], [ 9.15348239e-04, -7.27669394e-04, 1.14063594e-04, -8.76279722e-04, -1.02867256e-03, 8.02748313e-04, -4.53725770e-05, 2.15598906e-04], [ 9.15348239e-04, -7.27669394e-04, 1.14063594e-04, -8.76279722e-04, -1.02867256e-03, 8.02748313e-04, -4.53725770e-05, 2.15598906e-04]]) .. raw:: html Cộng và Chuẩn hóa ----------------- .. raw:: html Trong kiến trúc Transformer, tầng “cộng và chuẩn hóa” cũng đóng vai trò thiết yếu trong việc kết nối đầu vào và đầu ra của các tầng khác một cách trơn tru. Cụ thể, ta thêm một cấu trúc phần dư và tầng *chuẩn hóa theo tầng* sau tầng tập trung đa đầu và mạng truyền xuôi theo vị trí. *Chuẩn hóa theo tầng* khá giống với chuẩn hóa theo batch trong :numref:`sec_batch_norm`. Một điểm khác biệt là giá trị trung bình và phương sai của tầng chuẩn hóa này được tính theo chiều cuối cùng, tức ``X.mean(axis=-1)``, thay vì theo chiều đầu tiên (theo batch) ``X.mean(axis=0)`` . Chuẩn hóa tầng ngăn không cho phạm vi giá trị trong các tầng thay đổi quá nhiều, giúp huấn luyện nhanh hơn và khái quát hóa tốt hơn. .. raw:: html MXNet có cả ``LayerNorm`` và ``BatchNorm`` được lập trình trong khối ``nn``. Hãy cùng xem sự khác biệt giữa chúng qua ví dụ dưới đây. .. code:: python layer = nn.LayerNorm() layer.initialize() batch = nn.BatchNorm() batch.initialize() X = np.array([[1, 2], [2, 3]]) # Compute mean and variance from X in the training mode with autograd.record(): print('layer norm:', layer(X), '\nbatch norm:', batch(X)) .. parsed-literal:: :class: output layer norm: [[-0.99998 0.99998] [-0.99998 0.99998]] batch norm: [[-0.99998 -0.99998] [ 0.99998 0.99998]] .. raw:: html Bây giờ hãy cùng lập trình khối ``AddNorm``. ``AddNorm`` nhận hai đầu vào :math:`X` và :math:`Y`. Ta có thể coi :math:`X` là đầu vào ban đầu trong mạng phần dư và :math:`Y` là đầu ra từ tầng tập trung đa đầu hoặc mạng truyền xuôi theo vị trí. Ngoài ra, ta cũng sẽ áp dụng dropout lên :math:`Y` để điều chuẩn. .. code:: python # Saved in the d2l package for later use class AddNorm(nn.Block): def __init__(self, dropout, **kwargs): super(AddNorm, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) self.ln = nn.LayerNorm() def forward(self, X, Y): return self.ln(self.dropout(Y) + X) .. raw:: html Do có kết nối phần dư, :math:`X` và :math:`Y` sẽ có kích thước giống nhau. .. code:: python add_norm = AddNorm(0.5) add_norm.initialize() add_norm(np.ones((2, 3, 4)), np.ones((2, 3, 4))).shape .. parsed-literal:: :class: output (2, 3, 4) .. raw:: html .. raw:: html .. raw:: html Biểu diễn Vị trí ---------------- .. raw:: html Không giống như tầng hồi tiếp, cả tầng tập trung đa đầu và mạng truyền xuôi theo vị trí đều tính toán đầu ra cho từng phần tử trong chuỗi một cách độc lập. Điều này cho phép song song hóa công việc tính toán nhưng lại không mô hình hóa được thông tin tuần tự trong chuỗi đầu vào. Để nắm bắt các thông tin tuần tự một cách hiệu quả, mô hình Transformer sử dụng *biểu diễn vị trí* (*positional encoding*) để duy trì thông tin vị trí của chuỗi đầu vào. .. raw:: html Cụ thể, giả sử :math:`X\in\mathbb R^{l\times d}` là embedding của mẫu đầu vào, trong đó :math:`l` là độ dài chuỗi và :math:`d` là kích thước embedding. Tầng biểu diễn vị trí sẽ mã hóa vị trí :math:`P\in\mathbb R^{l\times d}` của X và trả về đầu ra :math:`P+X`. .. raw:: html Vị trí :math:`P` là ma trận 2 chiều, trong đó :math:`i` là thứ tự trong câu, :math:`j` là vị trí theo chiều embedding. Bằng cách này, mỗi vị trí trong chuỗi ban đầu được biểu biễn bởi hai phương trình dưới đây: .. math:: P_{i, 2j} = \sin(i/10000^{2j/d}), .. math:: \quad P_{i, 2j+1} = \cos(i/10000^{2j/d}), .. raw:: html với :math:`i=0,\ldots, l-1` và :math:`j=0,\ldots,\lfloor(d-1)/2\rfloor`. .. raw:: html :numref:`fig_positional_encoding` minh họa biểu diễn vị trí. .. raw:: html .. _fig_positional_encoding: .. figure:: ../img/positional_encoding.svg Biểu diễn vị trí. .. code:: python # Saved in the d2l package for later use class PositionalEncoding(nn.Block): def __init__(self, num_hiddens, dropout, max_len=1000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(dropout) # Create a long enough P self.P = np.zeros((1, max_len, num_hiddens)) X = np.arange(0, max_len).reshape(-1, 1) / np.power( 10000, np.arange(0, num_hiddens, 2) / num_hiddens) self.P[:, :, 0::2] = np.sin(X) self.P[:, :, 1::2] = np.cos(X) def forward(self, X): X = X + self.P[:, :X.shape[1], :].as_in_ctx(X.ctx) return self.dropout(X) .. raw:: html Bây giờ chúng ta kiểm tra lớp ``PositionalEncoding`` ở trên bằng một mô hình đơn giản cho 4 chiều. Có thể thấy, chiều thứ 4 và chiều thứ 5 có cùng tần số nhưng khác độ dời, còn chiều thứ 6 và 7 có tần số thấp hơn. .. code:: python pe = PositionalEncoding(20, 0) pe.initialize() Y = pe(np.zeros((1, 100, 20))) d2l.plot(np.arange(100), Y[0, :, 4:8].T, figsize=(6, 2.5), legend=["dim %d" % p for p in [4, 5, 6, 7]]) .. figure:: output_transformer_vn_e7c020_21_0.svg .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Bộ Mã hóa --------- .. raw:: html Với các thành phần thiết yếu trên, hãy xây dựng bộ mã hóa cho Transformer. Bộ mã hóa này chứa một tầng tập trung đa đầu, một mạng truyền xuôi theo vị trí và hai khối kết nối “cộng và chuẩn hóa”. Trong mã nguồn, có thể thấy cả tầng tập trung và mạng truyền xuôi theo vị trí trong ``EncoderBlock`` đều có đầu ra với kích thước là ``num_hiddens``. Điều này là do kết nối phần dư trong quá trình “cộng và chuẩn hóa”, khi ta cần cộng đầu ra của hai khối này với giá trị đầu vào của chúng. .. code:: python # Saved in the d2l package for later use class EncoderBlock(nn.Block): def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, use_bias=False, **kwargs): super(EncoderBlock, self).__init__(**kwargs) self.attention = MultiHeadAttention(num_hiddens, num_heads, dropout, use_bias) self.addnorm1 = AddNorm(dropout) self.ffn = PositionWiseFFN(ffn_num_hiddens, num_hiddens) self.addnorm2 = AddNorm(dropout) def forward(self, X, valid_len): Y = self.addnorm1(X, self.attention(X, X, X, valid_len)) return self.addnorm2(Y, self.ffn(Y)) .. raw:: html Nhờ kết nối phần dư, khối mã hóa sẽ không thay đổi kích thước đầu vào. Nói cách khác, giá trị ``num_hiddens`` phải bằng kích thước chiều cuối cùng của đầu vào. Trong ví dụ đơn giản dưới đây, ``num_hiddens`` :math:`= 24`,\ ``ffn_num_hiddens`` :math:`=48`, ``num_heads`` :math:`= 8` và ``dropout`` :math:`= 0.5`. .. code:: python X = np.ones((2, 100, 24)) encoder_blk = EncoderBlock(24, 48, 8, 0.5) encoder_blk.initialize() encoder_blk(X, valid_len).shape .. parsed-literal:: :class: output (2, 100, 24) .. raw:: html Bây giờ hãy lập trình bộ mã hóa của Transformer hoàn chỉnh. Trong bộ mã hóa, :math:`n` khối ``EncoderBlock`` được xếp chồng lên nhau. Nhờ có kết nối phần dư, tầng embedding và đầu ra khối Transformer đều có kích thước là :math:`d`. Cũng lưu ý rằng ta nhân các embedding với :math:`\sqrt{d}` để tránh trường hợp giá trị này quá nhỏ. .. code:: python # Saved in the d2l package for later use class TransformerEncoder(d2l.Encoder): def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, num_layers, dropout, use_bias=False, **kwargs): super(TransformerEncoder, self).__init__(**kwargs) self.num_hiddens = num_hiddens self.embedding = nn.Embedding(vocab_size, num_hiddens) self.pos_encoding = PositionalEncoding(num_hiddens, dropout) self.blks = nn.Sequential() for _ in range(num_layers): self.blks.add( EncoderBlock(num_hiddens, ffn_num_hiddens, num_heads, dropout, use_bias)) def forward(self, X, valid_len, *args): X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) for blk in self.blks: X = blk(X, valid_len) return X .. raw:: html Hãy tạo một bộ mã hóa với hai khối mã hóa Transformer với siêu tham số như ví dụ trên. Ngoài ra, ta đặt hai tham số ``vocab_size`` bằng :math:`200` và ``num_layers`` bằng :math:`2`. .. code:: python encoder = TransformerEncoder(200, 24, 48, 8, 2, 0.5) encoder.initialize() encoder(np.ones((2, 100)), valid_len).shape .. parsed-literal:: :class: output (2, 100, 24) .. raw:: html .. raw:: html .. raw:: html Bộ Giải mã ---------- .. raw:: html Khối giải mã của Transformer gần tương tự như khối mã hóa. Tuy nhiên, bên cạnh hai tầng con (tập trung đa đầu và biểu diễn vị trí), khối giải mã còn chứa tầng tập trung đa đầu áp dụng lên đầu ra của bộ mã hóa. Các tầng con này cũng được kết nối bằng các tầng “cộng và chuẩn hóa”, gồm kết nối phần dư và chuẩn hóa theo tầng. .. raw:: html Cụ thể, tại bước thời gian :math:`t`, giả sử đầu vào hiện tại là câu truy vấn :math:`\mathbf x_t`. Như minh họa trong :numref:`fig_self_attention_predict`, các khóa và giá trị của tầng tập trung gồm có câu truy vấn ở bước thời gian hiện tại và tất cả các câu truy vấn ở các bước thời gian trước :math:`\mathbf x_1, \ldots, \mathbf x_{t-1}`. .. raw:: html .. _fig_self_attention_predict: .. figure:: ../img/self-attention-predict.svg Dự đoán ở bước thời gian :math:`t` của một tầng tự tập trung. .. raw:: html Trong quá trình huấn luyện, đầu ra của câu truy vấn :math:`t` có thể quan sát được tất cả các cặp khóa - giá trị trước đó. Điều này dẫn đến hai cách hoạt động khác nhau của mạng khi huấn luyện và dự đoán. Vì thế, trong lúc dự đoán chúng ta có thể loại bỏ những thông tin không cần thiết bằng cách chỉ định độ dài hợp lệ là :math:`t` cho câu truy vấn thứ :math:`t`. .. code:: python class DecoderBlock(nn.Block): # i means it is the i-th block in the decoder def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, i, **kwargs): super(DecoderBlock, self).__init__(**kwargs) self.i = i self.attention1 = MultiHeadAttention(num_hiddens, num_heads, dropout) self.addnorm1 = AddNorm(dropout) self.attention2 = MultiHeadAttention(num_hiddens, num_heads, dropout) self.addnorm2 = AddNorm(dropout) self.ffn = PositionWiseFFN(ffn_num_hiddens, num_hiddens) self.addnorm3 = AddNorm(dropout) def forward(self, X, state): enc_outputs, enc_valid_len = state[0], state[1] # state[2][i] contains the past queries for this block if state[2][self.i] is None: key_values = X else: key_values = np.concatenate((state[2][self.i], X), axis=1) state[2][self.i] = key_values if autograd.is_training(): batch_size, seq_len, _ = X.shape # Shape: (batch_size, seq_len), the values in the j-th column # are j+1 valid_len = np.tile(np.arange(1, seq_len+1, ctx=X.ctx), (batch_size, 1)) else: valid_len = None X2 = self.attention1(X, key_values, key_values, valid_len) Y = self.addnorm1(X, X2) Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_len) Z = self.addnorm2(Y, Y2) return self.addnorm3(Z, self.ffn(Z)), state .. raw:: html Tương tự như khối mã hóa của Transformer, ``num_hiddens`` của khối giải mã phải bằng với kích thước chiều cuối cùng của :math:`X`. .. code:: python decoder_blk = DecoderBlock(24, 48, 8, 0.5, 0) decoder_blk.initialize() X = np.ones((2, 100, 24)) state = [encoder_blk(X, valid_len), valid_len, [None]] decoder_blk(X, state)[0].shape .. parsed-literal:: :class: output (2, 100, 24) .. raw:: html Bộ giải mã hoàn chỉnh của Transformer được lập trình tương tự như bộ mã hóa, ngoại trừ chi tiết tầng kết nối đầy đủ được thêm vào để tính điểm tin cậy của đầu ra. .. raw:: html Hãy lập trình bộ giải mã đầy đủ ``TransformerDecoder``. Ngoài các siêu tham số thường gặp như ``vocab_size`` và ``num_hiddens``, bộ giải mã cần thêm kích thước đầu ra của bộ mã hóa ``enc_outputs`` và độ dài hợp lệ ``env_valid_len``. .. code:: python class TransformerDecoder(d2l.Decoder): def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, num_layers, dropout, **kwargs): super(TransformerDecoder, self).__init__(**kwargs) self.num_hiddens = num_hiddens self.num_layers = num_layers self.embedding = nn.Embedding(vocab_size, num_hiddens) self.pos_encoding = PositionalEncoding(num_hiddens, dropout) self.blks = nn.Sequential() for i in range(num_layers): self.blks.add( DecoderBlock(num_hiddens, ffn_num_hiddens, num_heads, dropout, i)) self.dense = nn.Dense(vocab_size, flatten=False) def init_state(self, enc_outputs, env_valid_len, *args): return [enc_outputs, env_valid_len, [None]*self.num_layers] def forward(self, X, state): X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) for blk in self.blks: X, state = blk(X, state) return self.dense(X), state .. raw:: html .. raw:: html .. raw:: html Huấn luyện ---------- .. raw:: html Cuối cùng, chúng ta có thể xây dựng một mô hình mã hóa - giải mã với kiến ​​trúc Transformer. Tương tự như mô hình seq2seq áp dụng cơ chế tập trung trong :numref:`sec_seq2seq_attention`, chúng ta sử dụng các siêu tham số sau: hai khối Transformer có kích thước embedding và kích thước đầu ra đều là :math:`32`. Bên cạnh đó, chúng ta sử dụng :math:`4` đầu trong tầng tập trung và đặt kích thước ẩn bằng hai lần kích thước đầu ra. .. code:: python num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.0, 64, 10 lr, num_epochs, ctx = 0.005, 100, d2l.try_gpu() ffn_num_hiddens, num_heads = 64, 4 src_vocab, tgt_vocab, train_iter = d2l.load_data_nmt(batch_size, num_steps) encoder = TransformerEncoder( len(src_vocab), num_hiddens, ffn_num_hiddens, num_heads, num_layers, dropout) decoder = TransformerDecoder( len(src_vocab), num_hiddens, ffn_num_hiddens, num_heads, num_layers, dropout) model = d2l.EncoderDecoder(encoder, decoder) d2l.train_s2s_ch9(model, train_iter, lr, num_epochs, ctx) .. parsed-literal:: :class: output loss 0.033, 3396.2 tokens/sec on gpu(0) .. figure:: output_transformer_vn_e7c020_37_1.svg .. raw:: html Dựa trên thời gian huấn luyện và độ chính xác, ta thấy Transformer chạy nhanh hơn trên mỗi epoch và hội tụ nhanh hơn ở giai đoạn đầu so với seq2seq áp dụng cơ chế tập trung. .. raw:: html Chúng ta có thể sử dụng Transformer đã được huấn luyện để dịch một số câu đơn giản dưới đây. .. code:: python for sentence in ['Go .', 'Wow !', "I'm OK .", 'I won !']: print(sentence + ' => ' + d2l.predict_s2s_ch9( model, sentence, src_vocab, tgt_vocab, num_steps, ctx)) .. parsed-literal:: :class: output Go . => entrez ! Wow ! => ça ! I'm OK . => ça va . I won ! => j'ai gagné ! .. raw:: html Tóm tắt ------- .. raw:: html - Mô hình Transformer dựa trên kiến ​​trúc mã hóa - giải mã. - Tầng tập trung đa đầu gồm có :math:`h` tầng tập trung song song. - Mạng truyền xuôi theo vị trí gồm hai tầng kết nối đầy đủ được áp dụng trên chiều cuối cùng. - Chuẩn hóa theo tầng được áp dụng trên chiều cuối cùng (chiều đặc trưng), thay vì chiều đầu tiên (kích thước batch) như ở chuẩn hóa theo batch. - Biểu diễn vị trí là nơi duy nhất đưa thông tin vị trí trong chuỗi vào mô hình Transformer. .. raw:: html Bài tập ------- .. raw:: html 1. Hãy thử huấn luyện với nhiều epoch hơn và so sánh mất mát giữa seq2seq và Transformer. 2. Biểu diễn vị trí còn có lợi ích gì khác không? 3. So sánh chuẩn hóa theo tầng với chuẩn hóa theo batch. Khi nào nên sử dụng một trong hai tầng chuẩn hóa thay vì tầng còn lại? .. 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 Duy Du - Lê Khắc Hồng Phúc - Trần Yến Thy - Phạm Minh Đức - Nguyễn Văn Quang - Phạm Hồng Vinh - Nguyễn Văn Cường - Nguyễn Cảnh Thướng