4. ध्यान तंत्र

Reading time: 14 minutes

ध्यान तंत्र और आत्म-ध्यान न्यूरल नेटवर्क में

ध्यान तंत्र न्यूरल नेटवर्क को आउटपुट के प्रत्येक भाग को उत्पन्न करते समय इनपुट के विशिष्ट भागों पर ध्यान केंद्रित करने की अनुमति देते हैं। वे विभिन्न इनपुट को विभिन्न वजन सौंपते हैं, जिससे मॉडल यह तय करने में मदद मिलती है कि कौन से इनपुट कार्य के लिए सबसे प्रासंगिक हैं। यह मशीन अनुवाद जैसे कार्यों में महत्वपूर्ण है, जहां पूरे वाक्य के संदर्भ को समझना सटीक अनुवाद के लिए आवश्यक है।

tip

इस चौथे चरण का लक्ष्य बहुत सरल है: कुछ ध्यान तंत्र लागू करें। ये बहुत सारे दोहराए गए परतें होंगी जो शब्द के शब्दावली में उसके पड़ोसियों के साथ संबंध को कैप्चर करेंगी जो LLM को प्रशिक्षित करने के लिए वर्तमान वाक्य में उपयोग किया जा रहा है
इसके लिए बहुत सारी परतें उपयोग की जाती हैं, इसलिए बहुत सारे प्रशिक्षित पैरामीटर इस जानकारी को कैप्चर करने जा रहे हैं।

ध्यान तंत्र को समझना

भाषा अनुवाद के लिए उपयोग किए जाने वाले पारंपरिक अनुक्रम-से-अनुक्रम मॉडल में, मॉडल एक इनपुट अनुक्रम को एक निश्चित आकार के संदर्भ वेक्टर में एन्कोड करता है। हालाँकि, यह दृष्टिकोण लंबे वाक्यों के साथ संघर्ष करता है क्योंकि निश्चित आकार का संदर्भ वेक्टर सभी आवश्यक जानकारी को कैप्चर नहीं कर सकता। ध्यान तंत्र इस सीमा को संबोधित करते हैं क्योंकि वे मॉडल को प्रत्येक आउटपुट टोकन उत्पन्न करते समय सभी इनपुट टोकन पर विचार करने की अनुमति देते हैं।

उदाहरण: मशीन अनुवाद

जर्मन वाक्य "Kannst du mir helfen diesen Satz zu übersetzen" का अंग्रेजी में अनुवाद करने पर विचार करें। शब्द-द्वारा-शब्द अनुवाद एक व्याकरणिक रूप से सही अंग्रेजी वाक्य उत्पन्न नहीं करेगा क्योंकि भाषाओं के बीच व्याकरणिक संरचनाओं में भिन्नताएँ होती हैं। एक ध्यान तंत्र मॉडल को आउटपुट वाक्य के प्रत्येक शब्द को उत्पन्न करते समय इनपुट वाक्य के प्रासंगिक भागों पर ध्यान केंद्रित करने में सक्षम बनाता है, जिससे एक अधिक सटीक और सुसंगत अनुवाद होता है।

आत्म-ध्यान का परिचय

आत्म-ध्यान, या आंतरिक-ध्यान, एक तंत्र है जहाँ ध्यान एकल अनुक्रम के भीतर लागू होता है ताकि उस अनुक्रम का प्रतिनिधित्व किया जा सके। यह अनुक्रम में प्रत्येक टोकन को सभी अन्य टोकनों पर ध्यान केंद्रित करने की अनुमति देता है, जिससे मॉडल को अनुक्रम में टोकनों के बीच निर्भरताओं को कैप्चर करने में मदद मिलती है, चाहे उनकी दूरी कितनी भी हो।

प्रमुख अवधारणाएँ

  • टोकन: इनपुट अनुक्रम के व्यक्तिगत तत्व (जैसे, वाक्य में शब्द)।
  • एम्बेडिंग: टोकनों के वेक्टर प्रतिनिधित्व, जो अर्थ संबंधी जानकारी को कैप्चर करते हैं।
  • ध्यान वजन: मान जो यह निर्धारित करते हैं कि प्रत्येक टोकन अन्य टोकनों की तुलना में कितना महत्वपूर्ण है।

ध्यान वजन की गणना: एक चरण-दर-चरण उदाहरण

आइए वाक्य "Hello shiny sun!" पर विचार करें और प्रत्येक शब्द का 3-आयामी एम्बेडिंग के साथ प्रतिनिधित्व करें:

  • Hello: [0.34, 0.22, 0.54]
  • shiny: [0.53, 0.34, 0.98]
  • sun: [0.29, 0.54, 0.93]

हमारा लक्ष्य आत्म-ध्यान का उपयोग करके "shiny" शब्द के लिए संदर्भ वेक्टर की गणना करना है।

चरण 1: ध्यान स्कोर की गणना करें

tip

बस प्रत्येक आयाम मान को क्वेरी के साथ संबंधित टोकन के प्रत्येक आयाम मान से गुणा करें और परिणामों को जोड़ें। आपको प्रत्येक टोकन जोड़ी के लिए 1 मान मिलता है।

वाक्य में प्रत्येक शब्द के लिए, "shiny" के संबंध में ध्यान स्कोर की गणना करें, उनके एम्बेडिंग का डॉट उत्पाद निकालकर।

"Hello" और "shiny" के बीच ध्यान स्कोर

"shiny" और "shiny" के बीच ध्यान स्कोर

"sun" और "shiny" के बीच ध्यान स्कोर

चरण 2: ध्यान स्कोर को सामान्य करें ताकि ध्यान वजन प्राप्त हो सके

tip

गणितीय शर्तों में खो न जाएं, इस फ़ंक्शन का लक्ष्य सरल है, सभी वजन को सामान्य करें ताकि वे कुल 1 में जोड़ें
इसके अलावा, सॉफ्टमैक्स फ़ंक्शन का उपयोग किया जाता है क्योंकि यह गुणनात्मक भाग के कारण भिन्नताओं को बढ़ाता है, उपयोगी मानों का पता लगाना आसान बनाता है।

ध्यान स्कोर को ध्यान वजन में परिवर्तित करने के लिए सॉफ्टमैक्स फ़ंक्शन लागू करें जो 1 में जोड़ता है।

घातांक की गणना:

योग की गणना:

ध्यान वजन की गणना:

चरण 3: संदर्भ वेक्टर की गणना करें

tip

बस प्रत्येक ध्यान वजन को संबंधित टोकन आयामों से गुणा करें और फिर सभी आयामों को जोड़ें ताकि केवल 1 वेक्टर (संदर्भ वेक्टर) प्राप्त हो सके।

संदर्भ वेक्टर को सभी शब्दों के एम्बेडिंग के भारित योग के रूप में गणना की जाती है, ध्यान वजन का उपयोग करते हुए।

प्रत्येक घटक की गणना:

  • "Hello" का भारित एम्बेडिंग:
  • "shiny" का भारित एम्बेडिंग:
  • "sun" का भारित एम्बेडिंग:

भारित एम्बेडिंग का योग:

context vector=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]

यह संदर्भ वेक्टर "shiny" शब्द के लिए समृद्ध एम्बेडिंग का प्रतिनिधित्व करता है, जो वाक्य में सभी शब्दों से जानकारी को शामिल करता है।

प्रक्रिया का सारांश

  1. ध्यान स्कोर की गणना करें: लक्षित शब्द के एम्बेडिंग और अनुक्रम में सभी शब्दों के एम्बेडिंग के बीच डॉट उत्पाद का उपयोग करें।
  2. ध्यान वजन प्राप्त करने के लिए स्कोर को सामान्य करें: ध्यान स्कोर पर सॉफ्टमैक्स फ़ंक्शन लागू करें ताकि वजन प्राप्त हो सके जो 1 में जोड़ता है।
  3. संदर्भ वेक्टर की गणना करें: प्रत्येक शब्द के एम्बेडिंग को उसके ध्यान वजन से गुणा करें और परिणामों को जोड़ें।

प्रशिक्षित वजन के साथ आत्म-ध्यान

व्यवहार में, आत्म-ध्यान तंत्र प्रशिक्षित वजन का उपयोग करते हैं ताकि क्वेरी, कुंजी और मानों के लिए सर्वोत्तम प्रतिनिधित्व सीखा जा सके। इसमें तीन वजन मैट्रिक्स पेश करना शामिल है:

क्वेरी वही डेटा है जिसका उपयोग पहले की तरह किया जाता है, जबकि कुंजी और मान मैट्रिक्स बस यादृच्छिक-प्रशिक्षित मैट्रिक्स हैं।

चरण 1: क्वेरी, कुंजी और मानों की गणना करें

प्रत्येक टोकन का अपना क्वेरी, कुंजी और मान मैट्रिक्स होगा, इसके आयाम मानों को परिभाषित मैट्रिक्स से गुणा करके:

ये मैट्रिक्स मूल एम्बेडिंग को ध्यान की गणना के लिए उपयुक्त एक नए स्थान में परिवर्तित करते हैं।

उदाहरण

मान लीजिए:

  • इनपुट आयाम din=3 (एम्बेडिंग आकार)
  • आउटपुट आयाम dout=2 (क्वेरी, कुंजी और मानों के लिए इच्छित आयाम)

वजन मैट्रिक्स को प्रारंभ करें:

python
import torch.nn as nn

d_in = 3
d_out = 2

W_query = nn.Parameter(torch.rand(d_in, d_out))
W_key = nn.Parameter(torch.rand(d_in, d_out))
W_value = nn.Parameter(torch.rand(d_in, d_out))

क्वेरी, की, और वैल्यू की गणना करें:

python
queries = torch.matmul(inputs, W_query)
keys = torch.matmul(inputs, W_key)
values = torch.matmul(inputs, W_value)

Step 2: Compute Scaled Dot-Product Attention

Compute Attention Scores

पहले के उदाहरण के समान, लेकिन इस बार, टोकन के आयामों के मानों का उपयोग करने के बजाय, हम टोकन की कुंजी मैट्रिक्स का उपयोग करते हैं (जो पहले से आयामों का उपयोग करके गणना की गई है):। इसलिए, प्रत्येक क्वेरी qi​ और कुंजी kj​ के लिए:

Scale the Scores

डॉट उत्पादों को बहुत बड़ा होने से रोकने के लिए, उन्हें कुंजी आयाम dk​ के वर्गमूल से स्केल करें:

tip

स्कोर को आयामों के वर्गमूल से विभाजित किया जाता है क्योंकि डॉट उत्पाद बहुत बड़े हो सकते हैं और यह उन्हें नियंत्रित करने में मदद करता है।

Apply Softmax to Obtain Attention Weights: प्रारंभिक उदाहरण की तरह, सभी मानों को सामान्यीकृत करें ताकि उनका योग 1 हो।

Step 3: Compute Context Vectors

प्रारंभिक उदाहरण की तरह, सभी मानों के मैट्रिक्स को जोड़ें, प्रत्येक को इसके ध्यान वजन से गुणा करें:

Code Example

https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb से एक उदाहरण लेते हुए, आप इस क्लास को देख सकते हैं जो हमने चर्चा की स्व-संबंधित कार्यक्षमता को लागू करती है:

python
import torch

inputs = torch.tensor(
[[0.43, 0.15, 0.89], # Your     (x^1)
[0.55, 0.87, 0.66], # journey  (x^2)
[0.57, 0.85, 0.64], # starts   (x^3)
[0.22, 0.58, 0.33], # with     (x^4)
[0.77, 0.25, 0.10], # one      (x^5)
[0.05, 0.80, 0.55]] # step     (x^6)
)

import torch.nn as nn
class SelfAttention_v2(nn.Module):

def __init__(self, d_in, d_out, qkv_bias=False):
super().__init__()
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)

def forward(self, x):
keys = self.W_key(x)
queries = self.W_query(x)
values = self.W_value(x)

attn_scores = queries @ keys.T
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)

context_vec = attn_weights @ values
return context_vec

d_in=3
d_out=2
torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))

note

ध्यान दें कि मैट्रिस को यादृच्छिक मानों के साथ प्रारंभ करने के बजाय, nn.Linear का उपयोग सभी वेट्स को प्रशिक्षण के लिए पैरामीटर के रूप में चिह्नित करने के लिए किया जाता है।

कारणात्मक ध्यान: भविष्य के शब्दों को छिपाना

LLMs के लिए हम चाहते हैं कि मॉडल केवल उन टोकनों पर विचार करे जो वर्तमान स्थिति से पहले प्रकट होते हैं ताकि अगले टोकन की भविष्यवाणी की जा सके। कारणात्मक ध्यान, जिसे मास्क किया गया ध्यान भी कहा जाता है, इसे ध्यान तंत्र को संशोधित करके प्राप्त करता है ताकि भविष्य के टोकनों तक पहुंच को रोका जा सके।

कारणात्मक ध्यान मास्क लागू करना

कारणात्मक ध्यान को लागू करने के लिए, हम ध्यान स्कोर पर एक मास्क लागू करते हैं सॉफ्टमैक्स ऑपरेशन से पहले ताकि शेष स्कोर का योग 1 हो सके। यह मास्क भविष्य के टोकनों के ध्यान स्कोर को नकारात्मक अनंत में सेट करता है, यह सुनिश्चित करते हुए कि सॉफ्टमैक्स के बाद, उनके ध्यान वेट्स शून्य हैं।

चरण

  1. ध्यान स्कोर की गणना करें: पहले की तरह ही।
  2. मास्क लागू करें: एक ऊपरी त्रिकोणीय मैट्रिक्स का उपयोग करें जो विकर्ण के ऊपर नकारात्मक अनंत से भरा हो।
python
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
masked_scores = attention_scores + mask
  1. सॉफ्टमैक्स लागू करें: मास्क किए गए स्कोर का उपयोग करके ध्यान वेट्स की गणना करें।
python
attention_weights = torch.softmax(masked_scores, dim=-1)

ड्रॉपआउट के साथ अतिरिक्त ध्यान वेट्स को मास्क करना

ओवरफिटिंग को रोकने के लिए, हम सॉफ्टमैक्स ऑपरेशन के बाद ध्यान वेट्स पर ड्रॉपआउट लागू कर सकते हैं। ड्रॉपआउट प्रशिक्षण के दौरान कुछ ध्यान वेट्स को यादृच्छिक रूप से शून्य कर देता है

python
dropout = nn.Dropout(p=0.5)
attention_weights = dropout(attention_weights)

एक नियमित ड्रॉपआउट लगभग 10-20% है।

कोड उदाहरण

कोड उदाहरण https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb से:

python
import torch
import torch.nn as nn

inputs = torch.tensor(
[[0.43, 0.15, 0.89], # Your     (x^1)
[0.55, 0.87, 0.66], # journey  (x^2)
[0.57, 0.85, 0.64], # starts   (x^3)
[0.22, 0.58, 0.33], # with     (x^4)
[0.77, 0.25, 0.10], # one      (x^5)
[0.05, 0.80, 0.55]] # step     (x^6)
)

batch = torch.stack((inputs, inputs), dim=0)
print(batch.shape)

class CausalAttention(nn.Module):

def __init__(self, d_in, d_out, context_length,
dropout, qkv_bias=False):
super().__init__()
self.d_out = d_out
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.dropout = nn.Dropout(dropout)
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1)) # New

def forward(self, x):
b, num_tokens, d_in = x.shape
# b is the num of batches
# num_tokens is the number of tokens per batch
# d_in is the dimensions er token

keys = self.W_key(x) # This generates the keys of the tokens
queries = self.W_query(x)
values = self.W_value(x)

attn_scores = queries @ keys.transpose(1, 2) # Moves the third dimension to the second one and the second one to the third one to be able to multiply
attn_scores.masked_fill_(  # New, _ ops are in-place
self.mask.bool()[:num_tokens, :num_tokens], -torch.inf)  # `:num_tokens` to account for cases where the number of tokens in the batch is smaller than the supported context_size
attn_weights = torch.softmax(
attn_scores / keys.shape[-1]**0.5, dim=-1
)
attn_weights = self.dropout(attn_weights)

context_vec = attn_weights @ values
return context_vec

torch.manual_seed(123)

context_length = batch.shape[1]
d_in = 3
d_out = 2
ca = CausalAttention(d_in, d_out, context_length, 0.0)

context_vecs = ca(batch)

print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

एकल-हेड ध्यान को बहु-हेड ध्यान में विस्तारित करना

बहु-हेड ध्यान व्यावहारिक रूप से आत्म-ध्यान फ़ंक्शन के कई उदाहरणों को निष्पादित करने पर आधारित है, प्रत्येक के अपने वजन होते हैं ताकि विभिन्न अंतिम वेक्टर की गणना की जा सके।

कोड उदाहरण

पिछले कोड का पुन: उपयोग करना और बस एक लपेटन जोड़ना जो इसे कई बार लॉन्च करता है, संभव हो सकता है, लेकिन यह https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb से एक अधिक अनुकूलित संस्करण है जो सभी सिरों को एक साथ संसाधित करता है (महंगे फॉर लूप की संख्या को कम करता है)। जैसा कि आप कोड में देख सकते हैं, प्रत्येक टोकन के आयामों को सिरों की संख्या के अनुसार विभिन्न आयामों में विभाजित किया गया है। इस तरह, यदि टोकन के 8 आयाम हैं और हम 3 सिरों का उपयोग करना चाहते हैं, तो आयामों को 4 आयामों के 2 ऐरे में विभाजित किया जाएगा और प्रत्येक सिर उनमें से एक का उपयोग करेगा:

python
class MultiHeadAttention(nn.Module):
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
super().__init__()
assert (d_out % num_heads == 0), \
"d_out must be divisible by num_heads"

self.d_out = d_out
self.num_heads = num_heads
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim

self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.out_proj = nn.Linear(d_out, d_out)  # Linear layer to combine head outputs
self.dropout = nn.Dropout(dropout)
self.register_buffer(
"mask",
torch.triu(torch.ones(context_length, context_length),
diagonal=1)
)

def forward(self, x):
b, num_tokens, d_in = x.shape
# b is the num of batches
# num_tokens is the number of tokens per batch
# d_in is the dimensions er token

keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
queries = self.W_query(x)
values = self.W_value(x)

# We implicitly split the matrix by adding a `num_heads` dimension
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)

# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
keys = keys.transpose(1, 2)
queries = queries.transpose(1, 2)
values = values.transpose(1, 2)

# Compute scaled dot-product attention (aka self-attention) with a causal mask
attn_scores = queries @ keys.transpose(2, 3)  # Dot product for each head

# Original mask truncated to the number of tokens and converted to boolean
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]

# Use the mask to fill attention scores
attn_scores.masked_fill_(mask_bool, -torch.inf)

attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
attn_weights = self.dropout(attn_weights)

# Shape: (b, num_tokens, num_heads, head_dim)
context_vec = (attn_weights @ values).transpose(1, 2)

# Combine heads, where self.d_out = self.num_heads * self.head_dim
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
context_vec = self.out_proj(context_vec) # optional projection

return context_vec

torch.manual_seed(123)

batch_size, context_length, d_in = batch.shape
d_out = 2
mha = MultiHeadAttention(d_in, d_out, context_length, 0.0, num_heads=2)

context_vecs = mha(batch)

print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

For another compact and efficient implementation you could use the torch.nn.MultiheadAttention class in PyTorch.

tip

ChatGPT का संक्षिप्त उत्तर कि क्यों टोकनों के आयामों को सिरों के बीच विभाजित करना बेहतर है बजाय इसके कि प्रत्येक सिर सभी टोकनों के सभी आयामों की जांच करे:

जबकि प्रत्येक सिर को सभी एम्बेडिंग आयामों को संसाधित करने की अनुमति देना फायदेमंद लग सकता है क्योंकि प्रत्येक सिर को पूर्ण जानकारी तक पहुंच होगी, मानक प्रथा है कि सिरों के बीच एम्बेडिंग आयामों को विभाजित करना। यह दृष्टिकोण गणनात्मक दक्षता और मॉडल प्रदर्शन के बीच संतुलन बनाता है और प्रत्येक सिर को विविध प्रतिनिधित्व सीखने के लिए प्रोत्साहित करता है। इसलिए, एम्बेडिंग आयामों को विभाजित करना आमतौर पर प्रत्येक सिर को सभी आयामों की जांच करने की तुलना में प्राथमिकता दी जाती है।

References