- vừa được xem lúc

[Từ Transformer Đến Language Model] Bài 1: Bắt đầu với kiến trúc mô hình - Transformer

0 0 12

Người đăng: Nguyễn Văn Quân

Theo Viblo Asia

Chỉ trong vòng vài tháng trở lại đây, thế giới công nghệ đã bị khuynh đảo bởi các mô hình AI như ChatGPT, GPT-4, DALLE-2, Midjourney... Các mô hình AI đã và đang thực sự thay đổi cách thế giới này vận hành, rất nhiều những job title sống dựa vào các ứng dụng AI được sinh ra, đồng thời cũng rất nhiều những công việc khác dần bị loại bỏ. Nếu không nhanh nhạy và kịp thích ứng, thậm chí đến những AI Engineer cũng có thể bị đào thải bất cứ lúc nào. Hiện giờ thì có thể khẳng định rằng Large Language Model chắc chắn sẽ là xu hướng trong tương lai gần. Nếu như những ai đã bỏ lở chuyến tàu CNN ở 2013, Transformer ở 2018 thì giờ đây cơ hội của chúng ta đang quay trở lại với tấm vé LLM đang sốt rần rần trong giới công nghệ. Mục tiêu của mình khi viết series này là :

  • Bắt kịp xu hướng thông qua việc bổ sung kiến thức nền tảng về LLM.
  • Vạch ra một con đường để những ai có mong muốn tìm hiểu về LLM mà chưa có định hướng sẵn thì có thể tham khảo.

Để tìm hiểu chi tiết về cách hoạt động của mô hình Language Model, đầu tiên chúng ta cần tìm hiểu về kiến trúc của nó. Về cơ bản thì language model đều sử dụng kiến trúc transformer được giới thiệu trong paper Attention is all you need và cũng có khá nhiều người viết về lý thuyết của transformer rồi, nên ở bài này mình sẽ không tập trung hẳn về lý thuyết mà còn giới thiệu về code transformer cũng như self-attention.

Attention là gì ?

Có khá nhiều định nghĩa về từ "attention" kể từ khi kiến trúc transformer ra đời, tuy nhiên ở bài này mình sẽ dùng định nghĩa sau: Cơ chế attention mô tả trọng số đại diện cho độ quan trọng của mỗi thành phần trong input, trọng số này được tính dựa trên query được lấy từ input và key được lấy từ các thành phần đấy. Nói nôm na là dựa vào giá trị của một điểm trong vector đầu vào, ta sẽ tính được một giá trị trọng số thông qua mối quan hệ của nó với tất cả các thành phần còn lại, sau khi tính được giá trị trọng số của tất cả các điểm trong vector đầu vào ta sẽ biết được rằng vùng nào cần nhận được nhiều sự "chú ý" hơn. Cụ thể thì cơ chế attention trong transformer có 4 phần chính:

  • Query: là feature vector biểu diễn thứ mà chúng ta cần tìm kiếm trong input, hoặc có thể hiểu là từ mà chúng ta có thể sẽ cần phải chú ý đến trong câu.
  • Key: là feature vector đại diện cho thành phần mà ta đang xét đến trong input. Vector này sẽ đóng vai trò như một bản tóm tắt thông tin của mỗi thành phần được xét.
  • Value: là feature vector biểu diễn toàn bộ thông tin có trong một thành phần được xét.
  • Score function: dùng để tính độ quan trọng của mỗi thành phần trong đầu vào dựa trên query, key và value đã cho ở trên. Hàm này có thể là một phép nhân ma trận đơn giản hoặc một mạng MLP.

Ví dụ trong một câu, với mỗi từ ta sẽ có một cặp key-value đại diện cho thông tin trong từ đấy. Query sẽ được sử dụng để so với tất cả key có trong câu và tính ra ma trận trọng số attention thông qua score function, sau đó ma trận attention này sẽ được nhân với value để "đánh giá" lại độ quan trọng của mỗi từ trong câu.

Cơ chế attention trong transformer được gọi là self-attention; tại đó query, key và value đều được lấy từ input và sử dụng scaled dot product để tính ma trận trọng số attention:

Q=X×WQQ = X \times W^Q

K=X×WKK = X \times W^K

V=X×WVV = X \times W^V

Scaled dot product

Mục tiêu của việc sử dụng thuật toán này là tạo ra một ma trận attention biểu diễn mối quan hệ của từng thành phần trong một câu với các thành phần khác để xem là nên tập trung vào phần/vùng nào, trong khi vẫn giữ được lợi thế về mặt tính toán. Dot product attention nhận đầu vào là 1 tập queries QRT×dkQ\in\mathbb{R}^{T\times d_k}, keys KRT×dkK\in\mathbb{R}^{T\times d_k} và values VRT×dvV\in\mathbb{R}^{T\times d_v}, trong đó TT là độ dài của câu và dk,dvd_k, d_v là dimension của queris/keys và values. Dot product attention được biểu diễn dưới dạng toán học như sau:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

Phép nhân ma trận QKTQK^T sẽ thực hiện nhân mỗi cặp query-key với nhau để tìm mối quan hệ giữa chúng, tạo thành ma trận có dạng T×TT\times T. Sau đó ta sẽ áp dụng phép softmax\text{softmax} lên ma trận vừa tìm được để chuẩn hóa trọng số attention về khoảng (0,1)(0, 1), nếu ta nhân ma trận này với VV thì những từ cần được "chú ý" sẽ gần như giữ nguyên giá trị, những từ không quan trọng sẽ bị giảm giá trị.

Tuy nhiên có một nhân tố mà chúng ta chưa nhắc đến là scaling factor 1/dk1/\sqrt{d_k}. Hệ số này đóng vai trò khá quan trọng với nhiệm vụ giữ cho phương sai của phép attention không bị lệch. Giả sử sau khi khởi tạo, QQKK có giá trị trung bình bằng 0 và phương sai 1. Khi thực hiện phép nhân ma trận giữa QQKK sẽ tạo thành ma trận mới với phương sai gấp dkd_k lần 2 ma trận cũ:

qiN(0,σ2),kiN(0,σ2)Var(i=1dkqiki)=σ4dkq_i \sim \mathcal{N}(0,\sigma^2), k_i \sim \mathcal{N}(0,\sigma^2) \to \text{Var}\left(\sum_{i=1}^{d_k} q_i\cdot k_i\right) = \sigma^4\cdot d_k

Chú ý : bởi vì giá trị khởi tạo là 1 nên giá trị của σ4σ2\sigma^4 \approx \sigma^2, do đó có thể dùng σ2\sigma^2 thay σ4\sigma^4 để dễ hình dung bài toán hơn.

Từ đây có thể thấy rằng nếu không loại bỏ được yếu tố dkd_k thì dần dần phương sai của phép nhân QKTQK^T sẽ bị scale lên với biên độ rất lớn, dẫn đến giá trị của hàm softmax bị bão hòa tại 1 ở 1 vị trí bất kỳ và 0 tại tất cả các vị trí còn lại. Gradient của hàm softmax dần về 0, khi đó ta sẽ gặp hiện tượng không mong muốn là vanishing gradient.

Scaled dot product được thể hiện trên biểu đồ như sau:

Với Mask(opt.) phép toán che đi một vài phần trong attention matrix. Toán tử này được sử dụng trong trường hợp chúng ta không muốn một số thành phần của dữ liệu tham gia vào quá trình attention, điều này sẽ được trình bày chi tiết hơn ở phần Mask Multi-Head Attention

Phần code tường minh scaled dot product được implement theo pytorch như sau:

def scaled_dot_product(q, k, v, mask=None): d_k = q.size()[-1] attn_logits = torch.matmul(q, k.transpose(-2, -1)) attn_logits = attn_logits / math.sqrt(d_k) if mask is not None: attn_logits = attn_logits.masked_fill(mask == 0, -9e15) attention = F.softmax(attn_logits, dim=-1) values = torch.matmul(attention, v) return values, attention

Multi-Head Attention

Phép scaled dot product cho phép mô hình có khả năng "đọc" toàn bộ câu. Tuy nhiên thường thì vài từ trong câu sẽ có thêm một số ý nghĩa khác (theo tiếng việt thì là nghĩa đen và nghĩa bóng) và chúng ta cần suy xét hết các khía cạnh để hiểu hết nội dung của câu, do đó chỉ sử dụng duy nhất một giá trị trọng số để biểu diễn cho một từ trong câu thì khó có thể bao quát được hết nội dung ẩn hàm của từ đó. Đây là lý do vì sao tác giả mở rộng cơ chế attention ra thành nhiều heads, tức là mỗi một câu sẽ được mã hóa với nhiều bộ query-key-value, cho ra nhiều ma trận attention. Để dễ hình dung hơn thì ta có thể sử dụng công thức sau:

Multihead(Q,K,V)=Concat(head1,...,headh)WOwhere headi=Attention(QWiQ,KWiK,VWiV)\begin{split}\begin{split} \text{Multihead}(Q,K,V) & = \text{Concat}(\text{head}_1,...,\text{head}_h)W^{O}\\ \text{where } \text{head}_i & = \text{Attention}(QW_i^Q,KW_i^K, VW_i^V) \end{split}\end{split}

Công thức trên cũng chính là biểu diễn toán học của lớp Multi-Head Self-Attention (MHSA) trong transformer với learnable parameter W1...hQRD×dkW_{1...h}^{Q}\in\mathbb{R}^{D\times d_k} (D(D là dimension của input )). MHSA được biểu diễn dưới dạng đồ thị như sau:

Phần code MHSA được implement theo pytorch như sau:

 class SelfAttention(nn.Module): def __init__(self, embed_size, heads): super(SelfAttention, self).__init__() self.embed_size = embed_size self.heads = heads self.head_dim = embed_size // heads assert ( self.head_dim * heads == embed_size ), "Embedding size needs to be divisible by heads" self.values = nn.Linear(self.embed_size, self.embed_size, bias=False) self.keys = nn.Linear(self.embed_size, self.embed_size, bias=False) self.queries = nn.Linear(self.embed_size, self.embed_size, bias=False) self.fc_out = nn.Linear(heads * self.head_dim, embed_size) def forward(self, values, keys, query, mask): # Get number of training examples N = query.shape[0] value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1] values = self.values(values) keys = self.keys(keys) queries = self.queries(query) # Split the embedding into self.heads different pieces # Multi head # [N, len, embed_size] --> [N, len, heads, head_dim] values = values.reshape(N, value_len, self.heads, self.head_dim) keys = keys.reshape(N, key_len, self.heads, self.head_dim) queries = queries.reshape(N, query_len, self.heads, self.head_dim) # Einsum does matrix mult. for query*keys for each training example # with every other training example, don't be confused by einsum # it's just how I like doing matrix multiplication & bmm energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys]) # queries shape: (N, query_len, heads, heads_dim), # keys shape: (N, key_len, heads, heads_dim) # energy: (N, heads, query_len, key_len) # Mask padded indices so their weights become 0 if mask is not None: energy = energy.masked_fill(mask == 0, float("-1e20")) # Normalize energy values similarly to seq2seq + attention # so that they sum to 1. Also divide by scaling factor for # better stability attention = torch.softmax(energy / (self.embed_size ** (1 / 2)), dim=3) # attention shape: (N, heads, query_len, key_len) out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape( N, query_len, self.heads * self.head_dim ) # attention shape: (N, heads, query_len, key_len) # values shape: (N, value_len, heads, heads_dim) # out after matrix multiply: (N, query_len, heads, head_dim), then # we reshape and flatten the last two dimensions. out = self.fc_out(out) # Linear layer doesn't modify the shape, final shape will be # (N, query_len, embed_size) return out

MHSA có một tính chất khá quan trọng cần được nhắc đến là permutation-equivariant. Tính chất này cho phép model "miễn dịch" với phép hoán vị đối với các thành phần trong đầu vào, ví dụ ta hoán vị X1X2X_1\leftrightarrow X_2 trong đầu vào thì đầu ra của mô hình vẫn giữ nguyên không bị ảnh hưởng. Từ đây ta có thể thấy rằng MHSA không xem đầu vào như một chuỗi mà là một tập hợp các phần tử. Thuộc tính này làm cho MHSA và kiến trúc Transformer trở nên mạnh mẽ và có ưu thế hơn so với các thuật toán truyền thống. Tuy nhiên, nếu thứ tự của đầu vào là một thông tin khá quan trọng, vậy làm sao để kết hợp nó với MHSA ? Câu trả lời là mã hóa thông tin vị trí của các thành phần trong chuỗi và đưa nó vào input (positional encoding)

Teacher Forcing

Trước khi nói về masked multi-head attention thì có lẽ cần phải nói qua một chút về phương pháp Teacher Forcing được sử dụng trong quá trình training mô hình transformer. Teacher Forcing là chiến lược training đặc biệt cho các mạng recurrent neural network, sử dụng groundtruth làm input cho mô hình. Nghe qua thì có vẻ phương pháp này khá ma giáo, nhưng nó lại đặc biệt hữu ích cho quá trình training của các mô hình có tính Auto-Regressive, nhưng mô hình có dạng này sẽ lấy ouput tại time step (t1)(t-1) ghép với input tại time step (t)(t) để feed vào model và cho ra đầu ra time step (t+1)(t+1). Vậy tại sao phương pháp đi ngược với supervised learning truyền thống(không cho phép model tiếp xúc với groundtruth) lại hoạt động tốt đến thế? Để trả lời câu hỏi này thì chúng ta cần xem qua ví dụ sau, ta có một chuỗi đầu vào của mô hình và ta muốn mô hình sinh ra các từ khớp với câu bên dưới:

[START] Mary had a little lamb whose fleece was white as snow [END]

Đầu tiên ta sẽ đưa ký tự "[START]" vào mô hình để sinh ra từ tiếp theo với kỳ vọng từ đó sẽ là "Mary". Tuy nhiên, mô hình lại sinh ra từ "a" và tất nhiên là từ "a" này sẽ được ghép với "[START]" tạo thành input mới của mô hình:

[START] a

Như bạn thấy, kể từ bước này thì model đã lệch hoàn toàn so với dự tính ban đầu của chúng ta và hàm loss sẽ phạt toàn bộ những từ được mô hình sinh ra. Điều này khiến sự hội tụ của quá trình training chậm và mất ổn định hơn rất nhiều. Thay vào đó, ta có thể sử dụng luôn groundth-truth để dẫn đường cho mô hình học hiệu quả hơn. Việc đưa groundth-truth cho model học giúp nó nhận được input chính xác tại mỗi time-step trong quá trình huấn luyện, cho phép mô hình học được các đặc trưng mang thông tin liên tiếp và cho ra các dự đoán chính xác hơn. Ngoài ra còn cung cấp gradient đáng tin cậy giúp model hội tụ nhanh hơn. Tuy nhiên teacher forcing chỉ được sử dụng trong quá trình để hỗ trợ model, đến test phase thì model sẽ phải sử dụng output mà nó tự sinh để thực hiện quá trình auto-regressive.

Masked Multi-Head Attention

Vậy khả năng sinh dữ liệu Auto-Regressive hiệu quả của transformer từ đâu mà có ? Tính chất này có được là nhờ cách thiết kế self-attention với causal masking. Nhờ có causal masking mà trong quá trình huấn luyện, model chỉ tập trung vào token ở quá khứ và hiện tại, ít chú ý tới những token ở tương lai. Causal mask trong self-attention đơn giản chỉ là một ma trận vuông với những thành phần nằm trên đường chéo chính có giá trị âm vô cùng, các thành phần khác có giá trị là 0:

 S1 S2 S3 S4 t=0: 0 -N -N -N S1 t=1: 0 0 -N -N S2 t=2: 0 0 0 -N S3 t=3: 0 0 0 0 S4

Trong quá trình training, groundtruth được đưa thẳng qua decoder nên nếu sử dụng self-attention giống encoder, model sẽ nhìn thấy toàn bộ câu trả lời và ghi nhớ chứ không phải học kiến thức từ nó. Tuy nhiên với causal masking như trên, ta sẽ hạn chế khả năng truy cập groundtruth của model từ sớm và cứ thế mớm từng chút ground-truth cho model rồi ép model sinh output từ dữ liệu được mớm, ví dụ:

Tại thời điểm step t = 0, model chỉ tập trung vào từ đầu tiên (S1) và sinh ra output Y1

Tại thời điểm step t = 1, model tập trung vào từ thứ 1 và 2 (S1, S2) và sinh ra output Y2

Quá trình này sẽ tiếp diễn cho đến khi kết thúc câu. Hơn thế nữa, với causal masking, chúng ta có thể thực hiện quá trình trên đồng thời cùng 1 lúc trong lúc training, không phải chờ step này hoàn thành rồi đến step kia như lúc inference, giúp training nhanh hơn.

Ngoài ra còn 1 loại mask nữa là Padding Masking. Dữ liệu input của mô hình thường được pad với 1 token đặc biệt để đảm bảo độ dài của input trong 1 batch là như nhau. Tuy nhiên padding token không nên tham gia vào quá trình tính attention do nó không chứa thông tin hữu ích gì và có số lượng không ít. Do đó để ngăn không cho model chú ý vào những token này, ta sẽ áp dụng padding mask vào các vị trí có token padding.

Transformer Encoder

Transformer được xây dựng theo cấu trúc encoder-decoder với MHSA đóng vai trò chủ đạo, trong đó encoder nhận đầu vào là một câu văn bản và tạo ra vector attention chứa thông tin về câu đấy, vector này sẽ được đưa vào decoder để tiếp tục attend và giải mã theo hướng auto-regressive. Khối encoder của transformer thích hợp để phục vụ các task có nhiệm vụ đọc hiểu toàn văn bản nhờ khả năng mã hóa mạnh như text summarization, sentence classification, named entity recognition và extractive question answering. Transformer Encoder bao gồm N khối giống nhau được sắp xếp tuần tự, mỗi khối lớn gồm 2 khối nhỏ : [MHSA + Add&Norm] và [FFN + Add&Norm].

Khối MHSA:

LayerNorm(x+Multihead(x,x,x))\text{LayerNorm}(x+\text{Multihead}(x,x,x))

Khối FFN:

FFN(x)=ReLU(xW1)W2x=LayerNorm(x+FFN(x))\begin{split}\begin{split} \text{FFN}(x) & = \text{ReLU}(xW_1)W_2\\ x & = \text{LayerNorm}(x + \text{FFN}(x)) \end{split}\end{split}

Một chi tiết không thể bỏ qua ở kiến trúc Transformer chính là residual connection (kết nối tắt):

  • Tương tự với ResNet, Transformer được thiết kế theo kiểu ghép các block tuần tự nhau tạo thành một mạng rất sâu, một số mô hình thậm chí có thể chứa 24 block chỉ trong encoder. Do đó residual đóng vai trò rất quan trọng trong việc đưa tín hiệu gradient đi xuyên suốt mạng mà không bị "vanish".
  • MHSA không quan tâm đến vị trí của các thành phần trong câu và chỉ có thể học thông qua positional encoding. Điều này có nghĩa là càng về sau thì thông tin về câu gốc sẽ càng mất dần, điều này đối với những task như machine translation là cực kì tai hại. Tất cả đầu ra attention đều sẽ biểu diễn thông tin y hệt nhau, dẫn đến mô hình không thể phân biệt được thông tin này đến từ vị trí nào trong câu. Tuy nhiên với residual connection, thông tin về câu gốc sẽ được giữ từ đầu đến cuối mô hình giúp quá trình mapping giữa đầu vào - đầu ra diễn ra hiệu quả hơn.

Ngoài residual connection ra thì không thể không kể đến LayerNorm - một kỹ thuật regularization phổ biến trong kiến trúc Transformer. Nó được sử dụng để chuẩn hóa giá trị đầu ra của mỗi lớp trong mạng nơ-ron, giúp tăng tốc độ huấn luyện và cải thiện kết quả. Kỹ thuật này giúp giảm hiện tượng mất mát gradient, cải thiện khả năng hội tụ và tăng khả năng tổng quát hóa của mô hình Transformer. Trong khi BatchNorm có vẻ không thích hợp cho bài toán NLP bởi đặc trưng trích xuất từ các từ thường có phương sai khá cao (có rất nhiều từ hiếm cần được giữ để ho ra phân phối đủ tổng quát).

Để implement toàn bộ encoder của transformer, ta cần implement trước 1 block encoder:

class EncoderBlock(nn.Module): def __init__(self, embed_size, heads, dropout, forward_expansion): super(EncoderBlock, self).__init__() self.attention = SelfAttention(embed_size, heads) self.norm1 = nn.LayerNorm(embed_size) self.norm2 = nn.LayerNorm(embed_size) self.feed_forward = nn.Sequential( nn.Linear(embed_size, forward_expansion * embed_size), nn.ReLU(), nn.Linear(forward_expansion * embed_size, embed_size), ) self.dropout = nn.Dropout(dropout) def forward(self, value, key, query, mask): attention = self.attention(value, key, query, mask) # Add skip connection, run through normalization and finally dropout x = self.dropout(self.norm1(attention + query)) forward = self.feed_forward(x) out = self.dropout(self.norm2(forward + x)) return out

Từ 1 block trên, ta có thể mở rộng ra thành encoder của transformer:

class Encoder(nn.Module): def __init__( self, src_vocab_size, embed_size, num_layers, heads, device, forward_expansion, dropout, max_length, ): super(Encoder, self).__init__() self.embed_size = embed_size self.device = device self.word_embedding = nn.Embedding(src_vocab_size, embed_size) self.position_embedding = nn.Embedding(max_length, embed_size) self.layers = nn.ModuleList( [ EncoderBlock( embed_size, heads, dropout=dropout, forward_expansion=forward_expansion, ) for _ in range(num_layers) ] ) self.dropout = nn.Dropout(dropout) def forward(self, x, mask): N, seq_length = x.shape positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device) out = self.dropout( (self.word_embedding(x) + self.position_embedding(positions)) ) # In the Encoder the query, key, value are all the same, it's in the # decoder this will change. This might look a bit odd in this case. for layer in self.layers: out = layer(out, out, out, mask) return out

Transformer Decoder

Kiến trúc của decoder khá tương tự với encoder, khác ở chỗ ở decoder ta sẽ có thêm 1 block masked multi-head attention:

class DecoderBlock(nn.Module): def __init__(self, embed_size, heads, forward_expansion, dropout, device): super(DecoderBlock, self).__init__() self.norm = nn.LayerNorm(embed_size) self.attention = SelfAttention(embed_size, heads=heads) self.transformer_block = EncoderBlock( embed_size, heads, dropout, forward_expansion ) self.dropout = nn.Dropout(dropout) def forward(self, x, value, key, src_mask, trg_mask): attention = self.attention(x, x, x, trg_mask) query = self.dropout(self.norm(attention + x)) out = self.transformer_block(value, key, query, src_mask) return out
class Decoder(nn.Module): def __init__( self, trg_vocab_size, embed_size, num_layers, heads, forward_expansion, dropout, device, max_length, ): super(Decoder, self).__init__() self.device = device self.word_embedding = nn.Embedding(trg_vocab_size, embed_size) self.position_embedding = nn.Embedding(max_length, embed_size) self.layers = nn.ModuleList( [ DecoderBlock(embed_size, heads, forward_expansion, dropout, device) for _ in range(num_layers) ] ) self.dropout = nn.Dropout(dropout) self.fc_out = nn.Linear(embed_size, trg_vocab_size) def forward(self, x, enc_out, src_mask, trg_mask): N, seq_length = x.shape positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device) x = self.dropout( (self.word_embedding(x) + self.position_embedding(positions)) ) for layer in self.layers: x = layer(x, enc_out, enc_out, src_mask, trg_mask) out = self.fc_out(x) return out
class Transformer(nn.Module): def __init__( self, src_vocab_size, trg_vocab_size, src_pad_idx, trg_pad_idx, embed_size=512, num_layers=6, forward_expansion=4, heads=8, dropout=0, device="cpu", max_length=100, ): super(Transformer, self).__init__() self.encoder = Encoder( src_vocab_size, embed_size, num_layers, heads, device, forward_expansion, dropout, max_length, ) self.decoder = Decoder( trg_vocab_size, embed_size, num_layers, heads, forward_expansion, dropout, device, max_length, ) self.src_pad_idx = src_pad_idx self.trg_pad_idx = trg_pad_idx self.device = device def make_src_mask(self, src): src_mask = (src != self.src_pad_idx).unsqueeze(1).unsqueeze(2) # (N, 1, 1, src_len) return src_mask.to(self.device) def make_trg_mask(self, trg): N, trg_len = trg.shape trg_mask = torch.tril(torch.ones((trg_len, trg_len))).expand( N, 1, trg_len, trg_len ) return trg_mask.to(self.device) def forward(self, src, trg): src_mask = self.make_src_mask(src) trg_mask = self.make_trg_mask(trg) enc_src = self.encoder(src, src_mask) out = self.decoder(trg, enc_src, src_mask, trg_mask) return out

Positional Encoding

Như đã nói ở phần Multi_head Attention thì MHSA có tính chất permutation-equivariant, nghĩa là nó coi tất cả các từ trong 1 câu là như nhau và xử lý tất cả các từ đấy đồng thời cùng lúc không phân biệt thứ tự của các từ. Tuy nhiên đối với những task như language understanding thì việc nắm được vị trí của từ trong câu là rất quan trọng do nó liên quan rất nhiều đến ngữ pháp và nội dung, do đó người ta đã đề xuất ý tưởng đưa thêm thông tin vị trí vào input để model học thêm.

Gọi tt là vị trí của từ đang xét đến trong câu, ptRd\vec{p_t} \in \mathbb{R}^d là vector biểu diễn vị trí của nó, dd là chiều của vector biểu diễn. Gọi f:NRdf : \mathbb{N} \rightarrow \mathbb{R}^d là hàm mã hóa vị trí được định nghĩa như sau:

pt(i)=f(t)(i):={sin(ωm.t),if i=2mcos(ωm.t),if i=2m+1\begin{align} \vec{p_t}^{(i)} = f(t)^{(i)} & := \begin{cases} \sin({\omega_m} . t), & \text{if}\ i = 2m \\ \cos({\omega_m} . t), & \text{if}\ i = 2m + 1 \end{cases} \end{align}

Trong đó:

ωm=1100002m/d, m{1,2,...,d2}\omega_m = \frac{1}{10000^{2m / d}}, \; m \in \{1,2,...,\frac{d}{2}\}

Việc sử dụng cả 2 hàm sin và cos trong positional encoding cho phép chúng ta tạo ra một vector độc nhất cho mỗi từ, với mỗi thành phần trong vector tương ứng với một tần số riêng. Với cách mã hóa như thế này, ta có thể biểu diễn f(t+k)(i)f(t+k)^{(i)} như một hàm tuyến tính của f(t)(i)f(t)^{(i)}, từ đó cho phép model dễ dàng chú ý hơn vào vị trí tương đối của từ.

Code:

class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): """ Inputs d_model - Hidden dimensionality of the input. max_len - Maximum length of a sequence to expect. """ super().__init__() # Create matrix of [SeqLen, HiddenDim] representing the positional encoding for max_len inputs pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) # register_buffer => Tensor which is not a parameter, but should be part of the modules state. # Used for tensors that need to be on the same device as the module. # persistent=False tells PyTorch to not add the buffer to the state dict (e.g. when we save the model) self.register_buffer('pe', pe, persistent=False) def forward(self, x): x = x + self.pe[:, :x.size(1)] return x

Kết

Phía trên là một số kiến thức của mình về transformer, nếu mọi người cảm thấy có chỗ nào cần góp ý hoặc muốn trao đổi thì hãy thoải mái comment bên dưới nhé. Hy vọng bài viết mang đến thông tin hữu ích với mọi người.

References

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tấn công và phòng thủ bậc nhất cực mạnh cho các mô hình học máy

tấn công bậc nhất cực mạnh = universal first-order adversary. Update: Bleeding edge của CleverHans đã lên từ 3.1.0 đến 4.

0 0 28

- vừa được xem lúc

[Deep Learning] Key Information Extraction from document using Graph Convolution Network - Bài toán trích rút thông tin từ hóa đơn với Graph Convolution Network

Các nội dung sẽ được đề cập trong bài blog lần này. . Tổng quan về GNN, GCN. Bài toán Key Information Extraction, trích rút thông tin trong văn bản từ ảnh.

0 0 203

- vừa được xem lúc

Trích xuất thông tin bảng biểu cực đơn giản với OpenCV

Trong thời điểm nhà nước đang thúc đẩy mạnh mẽ quá trình chuyển đổi số như hiện nay, Document Understanding nói chung cũng như Table Extraction nói riêng đang trở thành một trong những lĩnh vực được quan tâm phát triển và chú trọng hàng đầu. Vậy Table Extraction là gì? Document Understanding là cái

0 0 214

- vừa được xem lúc

Con đường AI của tôi

Gần đây, khá nhiều bạn nhắn tin hỏi mình những câu hỏi đại loại như: có nên học AI, bắt đầu học AI như nào, làm sao tự học cho đúng, cho nhanh, học không bị nản, lộ trình học AI như nào... Sau nhiều lần trả lời, mình nghĩ rằng nên viết hẳn một bài để trả lời chi tiết hơn, cũng như để các bạn sau này

0 0 137

- vừa được xem lúc

[B5'] Smooth Adversarial Training

Đây là một bài trong series Báo khoa học trong vòng 5 phút. Được viết bởi Xie et. al, John Hopkins University, trong khi đang intern tại Google. Hiện vẫn là preprint do bị reject tại ICLR 2021.

0 0 32

- vừa được xem lúc

Deep Learning với Java - Tại sao không?

Muốn tìm hiểu về Machine Learning / Deep Learning nhưng với background là Java thì sẽ như thế nào và bắt đầu từ đâu? Để tìm được câu trả lời, hãy đọc bài viết này - có thể kỹ năng Java vốn có sẽ giúp bạn có những chuyến phiêu lưu thú vị. DJL là tên viết tắt của Deep Java Library - một thư viện mã ng

0 0 124