# 4. Механізми уваги ## Механізми уваги та самоувага в нейронних мережах Механізми уваги дозволяють нейронним мережам **зосереджуватися на конкретних частинах вхідних даних під час генерації кожної частини виходу**. Вони призначають різні ваги різним вхідним даним, допомагаючи моделі вирішити, які вхідні дані є найбільш релевантними для поставленого завдання. Це є критично важливим у таких завданнях, як машинний переклад, де розуміння контексту всього речення необхідне для точного перекладу. {% hint style="success" %} Мета цього четвертого етапу дуже проста: **застосувати деякі механізми уваги**. Це будуть багато **повторюваних шарів**, які **захоплюватимуть зв'язок слова у словнику з його сусідами в поточному реченні, що використовується для навчання LLM**.\ Для цього використовується багато шарів, тому багато навчальних параметрів будуть захоплювати цю інформацію. {% endhint %} ### Розуміння механізмів уваги У традиційних моделях послідовність до послідовності, що використовуються для перекладу мов, модель кодує вхідну послідовність у вектор контексту фіксованого розміру. Однак цей підхід має труднощі з довгими реченнями, оскільки вектор контексту фіксованого розміру може не захоплювати всю необхідну інформацію. Механізми уваги вирішують це обмеження, дозволяючи моделі враховувати всі вхідні токени під час генерації кожного вихідного токена. #### Приклад: Машинний переклад Розглянемо переклад німецького речення "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: Обчислення оцінок уваги {% hint style="success" %} Просто помножте кожне значення виміру запиту на відповідне значення кожного токена і додайте результати. Ви отримуєте 1 значення для кожної пари токенів. {% endhint %} Для кожного слова в реченні обчисліть **оцінку уваги** відносно "shiny", обчисливши скалярний добуток їх векторних представлень. **Оцінка уваги між "Hello" та "shiny"**
**Оцінка уваги між "shiny" та "shiny"**
**Оцінка уваги між "sun" та "shiny"**
#### Крок 2: Нормалізація оцінок уваги для отримання ваг уваги {% hint style="success" %} Не губіться в математичних термінах, мета цієї функції проста: нормалізувати всі ваги так, щоб **вони в сумі давали 1**. Крім того, **функція softmax** використовується, оскільки вона підкреслює відмінності завдяки експоненціальній частині, що полегшує виявлення корисних значень. {% endhint %} Застосуйте **функцію softmax** до оцінок уваги, щоб перетворити їх на ваги уваги, які в сумі дають 1.
Обчислення експонент:
Обчислення суми:
Обчислення ваг уваги:
#### Крок 3: Обчислення вектора контексту {% hint style="success" %} Просто візьміть кожну вагу уваги і помножте її на відповідні виміри токена, а потім додайте всі виміри, щоб отримати лише 1 вектор (вектор контексту) {% endhint %} **Вектор контексту** обчислюється як зважена сума векторних представлень усіх слів, використовуючи ваги уваги.
Обчислення кожного компонента: * **Зважене векторне представлення "Hello"**:
* **Зважене векторне представлення "shiny"**:
* **Зважене векторне представлення "sun"**:
Сумування зважених векторних представлень: `вектор контексту=[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. **Нормалізуйте оцінки для отримання ваг уваги**: Застосуйте функцію softmax до оцінок уваги, щоб отримати ваги, які в сумі дають 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 **Обчислити оцінки уваги** Схоже на попередній приклад, але цього разу, замість використання значень вимірів токенів, ми використовуємо матрицю ключів токена (яка вже була обчислена за допомогою вимірів):. Отже, для кожного запиту `qi`​ та ключа `kj​`:
**Масштабувати оцінки** Щоб запобігти тому, щоб скалярні добутки не ставали занадто великими, масштабуйте їх за квадратним коренем розміру ключа `dk`​:
{% hint style="success" %} Оцінка ділиться на квадратний корінь розмірів, оскільки скалярні добутки можуть ставати дуже великими, і це допомагає їх регулювати. {% endhint %} **Застосувати Softmax для отримання ваг уваги:** Як у початковому прикладі, нормалізуйте всі значення так, щоб їхня сума дорівнювала 1.
#### Step 3: Compute Context Vectors Як у початковому прикладі, просто складіть усі матриці значень, помноживши кожну з них на її вагу уваги:
### Code Example Взявши приклад з [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb](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)) ``` {% hint style="info" %} Зверніть увагу, що замість ініціалізації матриць випадковими значеннями, використовується `nn.Linear`, щоб позначити всі ваги як параметри для навчання. {% endhint %} ## Причинна Увага: Приховування Майбутніх Слів Для LLM ми хочемо, щоб модель враховувала лише токени, які з'являються перед поточною позицією, щоб **прогнозувати наступний токен**. **Причинна увага**, також відома як **замаскована увага**, досягає цього, модифікуючи механізм уваги, щоб запобігти доступу до майбутніх токенів. ### Застосування Маски Причинної Уваги Щоб реалізувати причинну увагу, ми застосовуємо маску до оцінок уваги **перед операцією softmax**, щоб залишкові значення все ще сумувалися до 1. Ця маска встановлює оцінки уваги майбутніх токенів на негативну нескінченність, забезпечуючи, що після softmax їх ваги уваги дорівнюють нулю. **Кроки** 1. **Обчислити Оцінки Уваги**: Так само, як і раніше. 2. **Застосувати Маску**: Використовуйте верхню трикутну матрицю, заповнену негативною нескінченністю вище діагоналі. ```python mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf') masked_scores = attention_scores + mask ``` 3. **Застосувати Softmax**: Обчисліть ваги уваги, використовуючи замасковані оцінки. ```python attention_weights = torch.softmax(masked_scores, dim=-1) ``` ### Маскування Додаткових Ваг Уваги з Допомогою Dropout Щоб **запобігти перенавчанню**, ми можемо застосувати **dropout** до ваг уваги після операції softmax. Dropout **випадковим чином обнуляє деякі з ваг уваги** під час навчання. ```python dropout = nn.Dropout(p=0.5) attention_weights = dropout(attention_weights) ``` Звичайний dropout становить близько 10-20%. ### Code Example Code example from [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb](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](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb), яка обробляє всі голови одночасно (зменшуючи кількість витратних циклів). Як ви можете бачити в коді, розміри кожного токена діляться на різні розміри відповідно до кількості голів. Таким чином, якщо токен має 8 розмірів і ми хочемо використовувати 3 голови, розміри будуть поділені на 2 масиви по 4 розміри, і кожна голова використовуватиме один з них: ```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) ``` Для ще однієї компактної та ефективної реалізації ви можете використовувати клас [`torch.nn.MultiheadAttention`](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) у PyTorch. {% hint style="success" %} Коротка відповідь ChatGPT про те, чому краще розділити виміри токенів між головами, а не дозволяти кожній голові перевіряти всі виміри всіх токенів: Хоча дозволити кожній голові обробляти всі вимірювання вбудовування може здаватися вигідним, оскільки кожна голова матиме доступ до всієї інформації, стандартною практикою є **розподіл вимірів вбудовування між головами**. Цей підхід забезпечує баланс між обчислювальною ефективністю та продуктивністю моделі та заохочує кожну голову вивчати різноманітні представлення. Тому розподіл вимірів вбудовування зазвичай є кращим, ніж дозволяти кожній голові перевіряти всі виміри. {% endhint %} ## References * [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)