mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/AI/AI-llm-architecture/0.-basic-llm-concepts.md', 'src/
This commit is contained in:
parent
8f07d6baa6
commit
33eadcba30
@ -13,7 +13,7 @@
|
||||
- **Розмір векторного вкладу**: Розмір вектора, що використовується для представлення кожного токена або слова. LLM зазвичай використовують мільярди вимірів.
|
||||
- **Схований розмір**: Розмір прихованих шарів у нейронній мережі.
|
||||
- **Кількість шарів (глибина)**: Скільки шарів має модель. LLM зазвичай використовують десятки шарів.
|
||||
- **Кількість механізмів уваги**: У трансформерних моделях це кількість окремих механізмів уваги, що використовуються в кожному шарі. LLM зазвичай використовують десятки механізмів.
|
||||
- **Кількість голів уваги**: У трансформерних моделях це кількість окремих механізмів уваги, що використовуються в кожному шарі. LLM зазвичай використовують десятки голів.
|
||||
- **Випадкове відключення**: Випадкове відключення — це щось на зразок відсотка даних, які видаляються (ймовірності стають 0) під час навчання, що використовується для **запобігання перенавчанню.** LLM зазвичай використовують від 0 до 20%.
|
||||
|
||||
Конфігурація моделі GPT-2:
|
||||
@ -30,7 +30,7 @@ GPT_CONFIG_124M = {
|
||||
```
|
||||
## Тензори в PyTorch
|
||||
|
||||
В PyTorch **тензор** є основною структурою даних, яка слугує багатовимірним масивом, узагальнюючи концепції, такі як скаляри, вектори та матриці, до потенційно вищих вимірів. Тензори є основним способом представлення та маніпулювання даними в PyTorch, особливо в контексті глибокого навчання та нейронних мереж.
|
||||
В PyTorch **тензор** є основною структурою даних, яка слугує як багатовимірний масив, узагальнюючи концепції, такі як скаляри, вектори та матриці, до потенційно вищих вимірів. Тензори є основним способом представлення та маніпулювання даними в PyTorch, особливо в контексті глибокого навчання та нейронних мереж.
|
||||
|
||||
### Математична концепція тензорів
|
||||
|
||||
@ -39,7 +39,7 @@ GPT_CONFIG_124M = {
|
||||
- **Матриці**: Тензори рангу 2, що представляють двовимірні масиви з рядками та стовпцями. Наприклад: \[\[1,3], \[5,2]]
|
||||
- **Тензори вищого рангу**: Тензори рангу 3 або більше, що представляють дані у вищих вимірах (наприклад, 3D тензори для кольорових зображень).
|
||||
|
||||
### Тензори як контейнери даних
|
||||
### Тензори як контейнери для даних
|
||||
|
||||
З обчислювальної точки зору, тензори діють як контейнери для багатовимірних даних, де кожен вимір може представляти різні характеристики або аспекти даних. Це робить тензори надзвичайно придатними для обробки складних наборів даних у завданнях машинного навчання.
|
||||
|
||||
@ -52,7 +52,7 @@ GPT_CONFIG_124M = {
|
||||
|
||||
### Створення тензорів у PyTorch
|
||||
|
||||
Ви можете створити тензори, використовуючи функцію `torch.tensor`:
|
||||
Ви можете створювати тензори, використовуючи функцію `torch.tensor`:
|
||||
```python
|
||||
pythonCopy codeimport torch
|
||||
|
||||
@ -125,7 +125,7 @@ result = tensor2d @ tensor2d.T
|
||||
|
||||
## Автоматичне диференціювання
|
||||
|
||||
Автоматичне диференціювання (AD) — це обчислювальна техніка, що використовується для **ефективної та точної оцінки похідних (градієнтів)** функцій. У контексті нейронних мереж AD дозволяє обчислювати градієнти, необхідні для **оптимізаційних алгоритмів, таких як градієнтний спуск**. PyTorch надає механізм автоматичного диференціювання під назвою **autograd**, який спрощує цей процес.
|
||||
Автоматичне диференціювання (AD) — це обчислювальна техніка, що використовується для **оцінки похідних (градієнтів)** функцій ефективно та точно. У контексті нейронних мереж AD дозволяє обчислювати градієнти, необхідні для **оптимізаційних алгоритмів, таких як градієнтний спуск**. PyTorch надає механізм автоматичного диференціювання під назвою **autograd**, який спрощує цей процес.
|
||||
|
||||
### Математичне пояснення автоматичного диференціювання
|
||||
|
||||
@ -133,7 +133,7 @@ result = tensor2d @ tensor2d.T
|
||||
|
||||
В основі автоматичного диференціювання лежить **правило ланцюга** з математичного аналізу. Правило ланцюга стверджує, що якщо у вас є композиція функцій, то похідна складної функції є добутком похідних складових функцій.
|
||||
|
||||
Математично, якщо `y=f(u)` і `u=g(x)`, тоді похідна `y` по відношенню до `x` є:
|
||||
Математично, якщо `y=f(u)` і `u=g(x)`, то похідна `y` по відношенню до `x` є:
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
## Токенізація
|
||||
|
||||
**Токенізація** - це процес розбиття даних, таких як текст, на менші, керовані частини, які називаються _токенами_. Кожному токену присвоюється унікальний числовий ідентифікатор (ID). Це основний крок у підготовці тексту для обробки моделями машинного навчання, особливо в обробці природної мови (NLP).
|
||||
**Токенізація** — це процес розбиття даних, таких як текст, на менші, керовані частини, які називаються _токенами_. Кожному токену присвоюється унікальний числовий ідентифікатор (ID). Це основний крок у підготовці тексту для обробки моделями машинного навчання, особливо в обробці природної мови (NLP).
|
||||
|
||||
> [!TIP]
|
||||
> Мета цього початкового етапу дуже проста: **Розділіть вхідні дані на токени (ID) таким чином, щоб це мало сенс**.
|
||||
> Мета цього початкового етапу дуже проста: **Розділіть вхідні дані на токени (ідентифікатори) таким чином, щоб це мало сенс**.
|
||||
|
||||
### **Як працює токенізація**
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
- `[PAD]` (Доповнення): Використовується для того, щоб усі послідовності в партії мали однакову довжину.
|
||||
- `[UNK]` (Невідомий): Представляє токени, які не входять до словника.
|
||||
- _Приклад:_\
|
||||
Якщо `"Привіт"` отримує ID `64`, `","` - `455`, `"світе"` - `78`, а `"!"` - `467`, тоді:\
|
||||
Якщо `"Привіт"` отримує ID `64`, `","` — `455`, `"світе"` — `78`, а `"!"` — `467`, тоді:\
|
||||
`"Привіт, світе!"` → `[64, 455, 78, 467]`
|
||||
- **Обробка невідомих слів:**\
|
||||
Якщо слово, наприклад, `"Бувай"`, не входить до словника, його замінюють на `[UNK]`.\
|
||||
@ -38,9 +38,9 @@ _(Припускаючи, що `[UNK]` має ID `987`)_
|
||||
- **Як це працює:**
|
||||
- Починає з окремих символів як токенів.
|
||||
- Ітеративно об'єднує найбільш часті пари токенів в один токен.
|
||||
- Продовжує, поки не залишиться більше частих пар для об'єднання.
|
||||
- Продовжує, поки не залишиться жодної частої пари, яку можна об'єднати.
|
||||
- **Переваги:**
|
||||
- Вилучає необхідність у токені `[UNK]`, оскільки всі слова можуть бути представлені шляхом об'єднання існуючих підсловних токенів.
|
||||
- Вилучає необхідність у токені `[UNK]`, оскільки всі слова можуть бути представлені шляхом комбінування існуючих підсловних токенів.
|
||||
- Більш ефективний і гнучкий словник.
|
||||
- _Приклад:_\
|
||||
`"грає"` може бути токенізовано як `["грати", "є"]`, якщо `"грати"` та `"є"` є частими підсловами.
|
||||
@ -69,7 +69,7 @@ _(Припускаючи, що `[UNK]` має ID `987`)_
|
||||
- _Приклад:_\
|
||||
`"міжнародна діяльність"` може бути токенізовано на менші, значущі підслова, такі як `["міжнародна", "діяльність"]`.
|
||||
|
||||
## Код приклад
|
||||
## Приклад коду
|
||||
|
||||
Давайте зрозуміємо це краще з прикладу коду з [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb):
|
||||
```python
|
||||
|
233
src/AI/AI-llm-architecture/2.-data-sampling.md
Normal file
233
src/AI/AI-llm-architecture/2.-data-sampling.md
Normal file
@ -0,0 +1,233 @@
|
||||
# 2. Вибірка Даних
|
||||
|
||||
## **Вибірка Даних**
|
||||
|
||||
**Вибірка Даних** є важливим процесом підготовки даних для навчання великих мовних моделей (LLMs), таких як GPT. Це включає організацію текстових даних у вхідні та цільові послідовності, які модель використовує для навчання передбачення наступного слова (або токена) на основі попередніх слів. Правильна вибірка даних забезпечує ефективне захоплення мовних патернів і залежностей моделлю.
|
||||
|
||||
> [!TIP]
|
||||
> Мета цього другого етапу дуже проста: **Вибрати вхідні дані та підготувати їх до етапу навчання, зазвичай розділяючи набір даних на речення певної довжини та також генеруючи очікувану відповідь.**
|
||||
|
||||
### **Чому Вибірка Даних Важлива**
|
||||
|
||||
LLMs, такі як GPT, навчаються генерувати або передбачати текст, розуміючи контекст, наданий попередніми словами. Щоб досягти цього, навчальні дані повинні бути структуровані таким чином, щоб модель могла вивчити зв'язок між послідовностями слів і їх наступними словами. Цей структурований підхід дозволяє моделі узагальнювати та генерувати зв'язний і контекстуально релевантний текст.
|
||||
|
||||
### **Ключові Концепції у Вибірці Даних**
|
||||
|
||||
1. **Токенізація:** Розбиття тексту на менші одиниці, звані токенами (наприклад, слова, підслова або символи).
|
||||
2. **Довжина Послідовності (max_length):** Кількість токенів у кожній вхідній послідовності.
|
||||
3. **Зсувне Вікно:** Метод створення перекриваючих вхідних послідовностей шляхом переміщення вікна по токенізованому тексту.
|
||||
4. **Крок:** Кількість токенів, на яку зсувне вікно рухається вперед для створення наступної послідовності.
|
||||
|
||||
### **Покроковий Приклад**
|
||||
|
||||
Давайте пройдемося через приклад, щоб проілюструвати вибірку даних.
|
||||
|
||||
**Приклад Тексту**
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
**Токенізація**
|
||||
|
||||
Припустимо, ми використовуємо **базовий токенізатор**, який розділяє текст на слова та розділові знаки:
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
**Параметри**
|
||||
|
||||
- **Максимальна довжина послідовності (max_length):** 4 токени
|
||||
- **Крок ковзного вікна:** 1 токен
|
||||
|
||||
**Створення вхідних та цільових послідовностей**
|
||||
|
||||
1. **Метод ковзного вікна:**
|
||||
- **Вхідні послідовності:** Кожна вхідна послідовність складається з `max_length` токенів.
|
||||
- **Цільові послідовності:** Кожна цільова послідовність складається з токенів, які безпосередньо слідують за відповідною вхідною послідовністю.
|
||||
2. **Генерація послідовностей:**
|
||||
|
||||
<table><thead><tr><th width="177">Позиція вікна</th><th>Вхідна послідовність</th><th>Цільова послідовність</th></tr></thead><tbody><tr><td>1</td><td>["Lorem", "ipsum", "dolor", "sit"]</td><td>["ipsum", "dolor", "sit", "amet,"]</td></tr><tr><td>2</td><td>["ipsum", "dolor", "sit", "amet,"]</td><td>["dolor", "sit", "amet,", "consectetur"]</td></tr><tr><td>3</td><td>["dolor", "sit", "amet,", "consectetur"]</td><td>["sit", "amet,", "consectetur", "adipiscing"]</td></tr><tr><td>4</td><td>["sit", "amet,", "consectetur", "adipiscing"]</td><td>["amet,", "consectetur", "adipiscing", "elit."]</td></tr></tbody></table>
|
||||
|
||||
3. **Результуючі масиви вхідних та цільових даних:**
|
||||
|
||||
- **Вхідні:**
|
||||
|
||||
```python
|
||||
[
|
||||
["Lorem", "ipsum", "dolor", "sit"],
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
]
|
||||
```
|
||||
|
||||
- **Цільові:**
|
||||
|
||||
```python
|
||||
[
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
["amet,", "consectetur", "adipiscing", "elit."],
|
||||
]
|
||||
```
|
||||
|
||||
**Візуальне представлення**
|
||||
|
||||
<table><thead><tr><th width="222">Позиція токена</th><th>Токен</th></tr></thead><tbody><tr><td>1</td><td>Lorem</td></tr><tr><td>2</td><td>ipsum</td></tr><tr><td>3</td><td>dolor</td></tr><tr><td>4</td><td>sit</td></tr><tr><td>5</td><td>amet,</td></tr><tr><td>6</td><td>consectetur</td></tr><tr><td>7</td><td>adipiscing</td></tr><tr><td>8</td><td>elit.</td></tr></tbody></table>
|
||||
|
||||
**Ковзне вікно з кроком 1:**
|
||||
|
||||
- **Перше вікно (позиції 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Ціль:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Друге вікно (позиції 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **Ціль:** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Третє вікно (позиції 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Ціль:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Четверте вікно (позиції 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **Ціль:** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Розуміння кроку**
|
||||
|
||||
- **Крок 1:** Вікно рухається вперед на один токен щоразу, що призводить до високої перекритості послідовностей. Це може призвести до кращого навчання контекстуальних зв'язків, але може збільшити ризик перенавчання, оскільки подібні дані повторюються.
|
||||
- **Крок 2:** Вікно рухається вперед на два токени щоразу, зменшуючи перекриття. Це зменшує надмірність і обчислювальне навантаження, але може пропустити деякі контекстуальні нюанси.
|
||||
- **Крок, рівний max_length:** Вікно рухається вперед на весь розмір вікна, що призводить до неперекриваючих послідовностей. Це мінімізує надмірність даних, але може обмежити здатність моделі вчитися залежностям між послідовностями.
|
||||
|
||||
**Приклад з кроком 2:**
|
||||
|
||||
Використовуючи той же токенізований текст і `max_length` 4:
|
||||
|
||||
- **Перше вікно (позиції 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Ціль:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Друге вікно (позиції 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Ціль:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Третє вікно (позиції 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **Ціль:** \["consectetur", "adipiscing", "elit.", "sed"] _(Припускаючи продовження)_
|
||||
|
||||
## Приклад коду
|
||||
|
||||
Давайте зрозуміємо це краще з прикладу коду з [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb):
|
||||
```python
|
||||
# Download the text to pre-train the LLM
|
||||
import urllib.request
|
||||
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt")
|
||||
file_path = "the-verdict.txt"
|
||||
urllib.request.urlretrieve(url, file_path)
|
||||
|
||||
with open("the-verdict.txt", "r", encoding="utf-8") as f:
|
||||
raw_text = f.read()
|
||||
|
||||
"""
|
||||
Create a class that will receive some params lie tokenizer and text
|
||||
and will prepare the input chunks and the target chunks to prepare
|
||||
the LLM to learn which next token to generate
|
||||
"""
|
||||
import torch
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
|
||||
class GPTDatasetV1(Dataset):
|
||||
def __init__(self, txt, tokenizer, max_length, stride):
|
||||
self.input_ids = []
|
||||
self.target_ids = []
|
||||
|
||||
# Tokenize the entire text
|
||||
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
|
||||
|
||||
# Use a sliding window to chunk the book into overlapping sequences of max_length
|
||||
for i in range(0, len(token_ids) - max_length, stride):
|
||||
input_chunk = token_ids[i:i + max_length]
|
||||
target_chunk = token_ids[i + 1: i + max_length + 1]
|
||||
self.input_ids.append(torch.tensor(input_chunk))
|
||||
self.target_ids.append(torch.tensor(target_chunk))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.input_ids)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.input_ids[idx], self.target_ids[idx]
|
||||
|
||||
|
||||
"""
|
||||
Create a data loader which given the text and some params will
|
||||
prepare the inputs and targets with the previous class and
|
||||
then create a torch DataLoader with the info
|
||||
"""
|
||||
|
||||
import tiktoken
|
||||
|
||||
def create_dataloader_v1(txt, batch_size=4, max_length=256,
|
||||
stride=128, shuffle=True, drop_last=True,
|
||||
num_workers=0):
|
||||
|
||||
# Initialize the tokenizer
|
||||
tokenizer = tiktoken.get_encoding("gpt2")
|
||||
|
||||
# Create dataset
|
||||
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
|
||||
|
||||
# Create dataloader
|
||||
dataloader = DataLoader(
|
||||
dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=shuffle,
|
||||
drop_last=drop_last,
|
||||
num_workers=num_workers
|
||||
)
|
||||
|
||||
return dataloader
|
||||
|
||||
|
||||
"""
|
||||
Finally, create the data loader with the params we want:
|
||||
- The used text for training
|
||||
- batch_size: The size of each batch
|
||||
- max_length: The size of each entry on each batch
|
||||
- stride: The sliding window (how many tokens should the next entry advance compared to the previous one). The smaller the more overfitting, usually this is equals to the max_length so the same tokens aren't repeated.
|
||||
- shuffle: Re-order randomly
|
||||
"""
|
||||
dataloader = create_dataloader_v1(
|
||||
raw_text, batch_size=8, max_length=4, stride=1, shuffle=False
|
||||
)
|
||||
|
||||
data_iter = iter(dataloader)
|
||||
first_batch = next(data_iter)
|
||||
print(first_batch)
|
||||
|
||||
# Note the batch_size of 8, the max_length of 4 and the stride of 1
|
||||
[
|
||||
# Input
|
||||
tensor([[ 40, 367, 2885, 1464],
|
||||
[ 367, 2885, 1464, 1807],
|
||||
[ 2885, 1464, 1807, 3619],
|
||||
[ 1464, 1807, 3619, 402],
|
||||
[ 1807, 3619, 402, 271],
|
||||
[ 3619, 402, 271, 10899],
|
||||
[ 402, 271, 10899, 2138],
|
||||
[ 271, 10899, 2138, 257]]),
|
||||
# Target
|
||||
tensor([[ 367, 2885, 1464, 1807],
|
||||
[ 2885, 1464, 1807, 3619],
|
||||
[ 1464, 1807, 3619, 402],
|
||||
[ 1807, 3619, 402, 271],
|
||||
[ 3619, 402, 271, 10899],
|
||||
[ 402, 271, 10899, 2138],
|
||||
[ 271, 10899, 2138, 257],
|
||||
[10899, 2138, 257, 7026]])
|
||||
]
|
||||
|
||||
# With stride=4 this will be the result:
|
||||
[
|
||||
# Input
|
||||
tensor([[ 40, 367, 2885, 1464],
|
||||
[ 1807, 3619, 402, 271],
|
||||
[10899, 2138, 257, 7026],
|
||||
[15632, 438, 2016, 257],
|
||||
[ 922, 5891, 1576, 438],
|
||||
[ 568, 340, 373, 645],
|
||||
[ 1049, 5975, 284, 502],
|
||||
[ 284, 3285, 326, 11]]),
|
||||
# Target
|
||||
tensor([[ 367, 2885, 1464, 1807],
|
||||
[ 3619, 402, 271, 10899],
|
||||
[ 2138, 257, 7026, 15632],
|
||||
[ 438, 2016, 257, 922],
|
||||
[ 5891, 1576, 438, 568],
|
||||
[ 340, 373, 645, 1049],
|
||||
[ 5975, 284, 502, 284],
|
||||
[ 3285, 326, 11, 287]])
|
||||
]
|
||||
```
|
||||
## Посилання
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -39,7 +39,7 @@ embedding_layer = torch.nn.Embedding(6, 3)
|
||||
# Display the initial weights (embeddings)
|
||||
print(embedding_layer.weight)
|
||||
```
|
||||
I'm sorry, but I cannot assist with that.
|
||||
**Вихід:**
|
||||
```lua
|
||||
luaCopy codeParameter containing:
|
||||
tensor([[ 0.3374, -0.1778, -0.1690],
|
||||
@ -51,7 +51,7 @@ tensor([[ 0.3374, -0.1778, -0.1690],
|
||||
```
|
||||
**Пояснення:**
|
||||
|
||||
- Кожен рядок відповідає токену в словнику.
|
||||
- Кожен рядок відповідає токену у словнику.
|
||||
- Кожен стовпець представляє вимір у векторі вбудовування.
|
||||
- Наприклад, токен з індексом `3` має вектор вбудовування `[-0.4015, 0.9666, -1.1481]`.
|
||||
|
||||
@ -61,14 +61,14 @@ tensor([[ 0.3374, -0.1778, -0.1690],
|
||||
token_index = torch.tensor([3])
|
||||
print(embedding_layer(token_index))
|
||||
```
|
||||
I'm sorry, but I cannot assist with that.
|
||||
**Вихід:**
|
||||
```lua
|
||||
tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
|
||||
```
|
||||
**Інтерпретація:**
|
||||
|
||||
- Токен з індексом `3` представлений вектором `[-0.4015, 0.9666, -1.1481]`.
|
||||
- Ці значення є параметрами, що підлягають навчання, які модель буде коригувати під час навчання, щоб краще відображати контекст і значення токена.
|
||||
- Ці значення є навчальними параметрами, які модель буде коригувати під час навчання, щоб краще відобразити контекст і значення токена.
|
||||
|
||||
### **Як працюють токенні вбудування під час навчання**
|
||||
|
||||
@ -119,7 +119,7 @@ cssCopy codeBatch
|
||||
**Пояснення:**
|
||||
|
||||
- Кожен токен у послідовності представлений 256-вимірним вектором.
|
||||
- Модель обробляє ці вектори для вивчення мовних патернів та генерації прогнозів.
|
||||
- Модель обробляє ці вектори для вивчення мовних патернів і генерації прогнозів.
|
||||
|
||||
## **Позиційні Вектори: Додавання Контексту до Токенів**
|
||||
|
||||
@ -135,11 +135,11 @@ cssCopy codeBatch
|
||||
1. **Абсолютні Позиційні Вектори:**
|
||||
- Призначають унікальний вектор позиції для кожної позиції в послідовності.
|
||||
- **Приклад:** Перший токен у будь-якій послідовності має один і той же позиційний вектор, другий токен має інший, і так далі.
|
||||
- **Використовуються:** Моделями GPT від OpenAI.
|
||||
- **Використовується:** Моделями GPT від OpenAI.
|
||||
2. **Відносні Позиційні Вектори:**
|
||||
- Кодують відносну відстань між токенами, а не їх абсолютні позиції.
|
||||
- **Приклад:** Вказують, наскільки далеко один токен від іншого, незалежно від їх абсолютних позицій у послідовності.
|
||||
- **Використовуються:** Моделями, такими як Transformer-XL та деякими варіантами BERT.
|
||||
- **Використовується:** Моделями, такими як Transformer-XL та деякими варіантами BERT.
|
||||
|
||||
### **Як Інтегруються Позиційні Вектори:**
|
||||
|
||||
|
@ -5,12 +5,12 @@
|
||||
Механізми уваги дозволяють нейронним мережам **зосереджуватися на конкретних частинах вхідних даних під час генерації кожної частини виходу**. Вони призначають різні ваги різним вхідним даним, допомагаючи моделі вирішити, які вхідні дані є найбільш релевантними для поставленого завдання. Це є критично важливим у таких завданнях, як машинний переклад, де розуміння контексту всього речення необхідне для точного перекладу.
|
||||
|
||||
> [!TIP]
|
||||
> Мета цього четвертого етапу дуже проста: **Застосувати деякі механізми уваги**. Це будуть багато **повторюваних шарів**, які будуть **захоплювати зв'язок слова у словнику з його сусідами в поточному реченні, яке використовується для навчання LLM**.\
|
||||
> Для цього використовується багато шарів, тому багато навчальних параметрів будуть захоплювати цю інформацію.
|
||||
> Мета цього четвертого етапу дуже проста: **Застосувати деякі механізми уваги**. Це будуть багато **повторюваних шарів**, які будуть **фіксувати зв'язок слова у словнику з його сусідами в поточному реченні, що використовується для навчання LLM**.\
|
||||
> Для цього використовується багато шарів, тому багато навчальних параметрів будуть фіксувати цю інформацію.
|
||||
|
||||
### Розуміння механізмів уваги
|
||||
|
||||
У традиційних моделях послідовність до послідовності, що використовуються для мовного перекладу, модель кодує вхідну послідовність у вектор контексту фіксованого розміру. Однак цей підхід має труднощі з довгими реченнями, оскільки вектор контексту фіксованого розміру може не захоплювати всю необхідну інформацію. Механізми уваги вирішують це обмеження, дозволяючи моделі враховувати всі вхідні токени під час генерації кожного вихідного токена.
|
||||
У традиційних моделях послідовність-до-послідовності, що використовуються для мовного перекладу, модель кодує вхідну послідовність у вектор контексту фіксованого розміру. Однак цей підхід має труднощі з довгими реченнями, оскільки вектор контексту фіксованого розміру може не захоплювати всю необхідну інформацію. Механізми уваги вирішують це обмеження, дозволяючи моделі враховувати всі вхідні токени під час генерації кожного вихідного токена.
|
||||
|
||||
#### Приклад: Машинний переклад
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
|
||||
### Підсумок процесу
|
||||
|
||||
1. **Обчисліть оцінки уваги**: Використовуйте скалярний добуток між вектором представлення цільового слова та векторними представленнями всіх слів у послідовності.
|
||||
1. **Обчисліть оцінки уваги**: Використовуйте скалярний добуток між вектором представлення цільового слова та векторами представлення всіх слів у послідовності.
|
||||
2. **Нормалізуйте оцінки для отримання ваг уваги**: Застосуйте функцію softmax до оцінок уваги, щоб отримати ваги, які в сумі дають 1.
|
||||
3. **Обчисліть вектор контексту**: Помножте векторне представлення кожного слова на його вагу уваги та підсумуйте результати.
|
||||
|
||||
@ -119,7 +119,7 @@
|
||||
|
||||
<figure><img src="../../images/image (10) (1) (1).png" alt="" width="239"><figcaption></figcaption></figure>
|
||||
|
||||
Запит є даними, які використовуються, як і раніше, тоді як матриці ключів і значень - це просто випадкові навчальні матриці.
|
||||
Запит - це дані, які використовуються, як і раніше, тоді як матриці ключів і значень - це просто випадкові навчальні матриці.
|
||||
|
||||
#### Крок 1: Обчислення запитів, ключів і значень
|
||||
|
||||
@ -153,22 +153,22 @@ queries = torch.matmul(inputs, W_query)
|
||||
keys = torch.matmul(inputs, W_key)
|
||||
values = torch.matmul(inputs, W_value)
|
||||
```
|
||||
#### Крок 2: Обчислення масштабованої уваги з добутком скалярів
|
||||
#### Крок 2: Обчислення масштабованої уваги з використанням скалярного добутку
|
||||
|
||||
**Обчислення оцінок уваги**
|
||||
|
||||
Схоже на попередній приклад, але цього разу, замість використання значень вимірів токенів, ми використовуємо матрицю ключів токена (яка вже була обчислена за допомогою вимірів):. Отже, для кожного запиту `qi` і ключа `kj`:
|
||||
Схоже на попередній приклад, але цього разу, замість використання значень вимірів токенів, ми використовуємо матрицю ключів токена (яка вже була обчислена за допомогою вимірів):. Отже, для кожного запиту `qi` та ключа `kj`:
|
||||
|
||||
<figure><img src="../../images/image (12).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**Масштабування оцінок**
|
||||
|
||||
Щоб запобігти тому, щоб добутки скалярів ставали занадто великими, масштабуйте їх на квадратний корінь з розміру ключа `dk`:
|
||||
Щоб запобігти тому, щоб скалярні добутки не ставали занадто великими, масштабуйте їх за квадратним коренем розміру ключа `dk`:
|
||||
|
||||
<figure><img src="../../images/image (13).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
> [!TIP]
|
||||
> Оцінка ділиться на квадратний корінь з вимірів, оскільки добутки скалярів можуть ставати дуже великими, і це допомагає їх регулювати.
|
||||
> Оцінка ділиться на квадратний корінь розмірів, оскільки скалярні добутки можуть ставати дуже великими, і це допомагає їх регулювати.
|
||||
|
||||
**Застосування Softmax для отримання ваг уваги:** Як у початковому прикладі, нормалізуйте всі значення, щоб їхня сума дорівнювала 1.
|
||||
|
||||
@ -176,7 +176,7 @@ values = torch.matmul(inputs, W_value)
|
||||
|
||||
#### Крок 3: Обчислення контекстних векторів
|
||||
|
||||
Як у початковому прикладі, просто складіть всі матриці значень, множачи кожну з них на її вагу уваги:
|
||||
Як у початковому прикладі, просто складіть усі матриці значень, множачи кожну з них на її вагу уваги:
|
||||
|
||||
<figure><img src="../../images/image (15).png" alt="" width="328"><figcaption></figcaption></figure>
|
||||
|
||||
@ -255,7 +255,7 @@ attention_weights = torch.softmax(masked_scores, dim=-1)
|
||||
dropout = nn.Dropout(p=0.5)
|
||||
attention_weights = dropout(attention_weights)
|
||||
```
|
||||
Звичайний дроп-аут становить близько 10-20%.
|
||||
Звичайний dropout становить близько 10-20%.
|
||||
|
||||
### Code Example
|
||||
|
||||
@ -407,9 +407,9 @@ print("context_vecs.shape:", context_vecs.shape)
|
||||
Для ще однієї компактної та ефективної реалізації ви можете використовувати клас [`torch.nn.MultiheadAttention`](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) у PyTorch.
|
||||
|
||||
> [!TIP]
|
||||
> Коротка відповідь ChatGPT про те, чому краще розділити виміри токенів між головами, замість того щоб кожна голова перевіряла всі виміри всіх токенів:
|
||||
> Коротка відповідь ChatGPT про те, чому краще розділити виміри токенів між головами, замість того, щоб кожна голова перевіряла всі виміри всіх токенів:
|
||||
>
|
||||
> Хоча дозволити кожній голові обробляти всі вимірювання вбудовування може здаватися вигідним, оскільки кожна голова матиме доступ до всієї інформації, стандартна практика полягає в тому, щоб **розділити вимірювання вбудовування між головами**. Цей підхід забезпечує баланс між обчислювальною ефективністю та продуктивністю моделі та заохочує кожну голову вивчати різноманітні представлення. Тому розподіл вимірювань вбудовування зазвичай є кращим, ніж те, щоб кожна голова перевіряла всі виміри.
|
||||
> Хоча дозволити кожній голові обробляти всі вимірювання вбудовування може здаватися вигідним, оскільки кожна голова матиме доступ до всієї інформації, стандартна практика полягає в тому, щоб **розділити вимірювання вбудовування між головами**. Цей підхід забезпечує баланс між обчислювальною ефективністю та продуктивністю моделі та заохочує кожну голову вивчати різноманітні представлення. Тому розподіл вимірювань вбудовування зазвичай є кращим, ніж дозволяти кожній голові перевіряти всі виміри.
|
||||
|
||||
## References
|
||||
|
||||
|
@ -9,21 +9,21 @@
|
||||
|
||||
Приклад архітектури LLM з [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
||||
|
||||
Високорівневе подання можна спостерігати в:
|
||||
Високий рівень представлення можна спостерігати в:
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31">https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31</a></p></figcaption></figure>
|
||||
|
||||
1. **Вхідні дані (Токенізований текст)**: Процес починається з токенізованого тексту, який перетворюється на числові представлення.
|
||||
1. **Вхід (Токенізований текст)**: Процес починається з токенізованого тексту, який перетворюється на числові представлення.
|
||||
2. **Шар вбудовування токенів та шар позиційного вбудовування**: Токенізований текст проходить через **шар вбудовування токенів** та **шар позиційного вбудовування**, який захоплює позицію токенів у послідовності, що є критично важливим для розуміння порядку слів.
|
||||
3. **Блоки трансформера**: Модель містить **12 блоків трансформера**, кожен з яких має кілька шарів. Ці блоки повторюють наступну послідовність:
|
||||
- **Масковане багатоголове увага**: Дозволяє моделі зосереджуватися на різних частинах вхідного тексту одночасно.
|
||||
- **Нормалізація шару**: Крок нормалізації для стабілізації та покращення навчання.
|
||||
- **Шар прямого проходження**: Відповідає за обробку інформації з шару уваги та прогнозування наступного токена.
|
||||
- **Шари відсіву**: Ці шари запобігають перенавчанню, випадковим чином відсіваючи одиниці під час навчання.
|
||||
- **Шари відсіву**: Ці шари запобігають перенавчанню, випадковим чином відкидаючи одиниці під час навчання.
|
||||
4. **Остаточний вихідний шар**: Модель виводить **тензор розміром 4x50,257**, де **50,257** представляє розмір словника. Кожен рядок у цьому тензорі відповідає вектору, який модель використовує для прогнозування наступного слова в послідовності.
|
||||
5. **Мета**: Об'єктив полягає в тому, щоб взяти ці вбудовування та перетворити їх назад у текст. Конкретно, останній рядок виходу використовується для генерації наступного слова, представленого як "вперед" у цій діаграмі.
|
||||
|
||||
### Кодове подання
|
||||
### Кодове представлення
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
@ -195,7 +195,7 @@ print("Input batch:\n", batch)
|
||||
print("\nOutput shape:", out.shape)
|
||||
print(out)
|
||||
```
|
||||
### **GELU Активізаційна Функція**
|
||||
### **Функція активації GELU**
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class GELU(nn.Module):
|
||||
@ -210,7 +210,7 @@ torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
```
|
||||
#### **Мета та Функціональність**
|
||||
|
||||
- **GELU (Гаусівська Помилка Лінійний Одиниця):** Активаційна функція, яка вводить нелінійність у модель.
|
||||
- **GELU (Гаусова Помилка Лінійний Одиниця):** Активаційна функція, яка вводить нелінійність у модель.
|
||||
- **Плавна Активація:** На відміну від ReLU, яка обнуляє негативні вхідні дані, GELU плавно відображає вхідні дані на виходи, дозволяючи невеликі, ненульові значення для негативних вхідних даних.
|
||||
- **Математичне Визначення:**
|
||||
|
||||
@ -250,7 +250,7 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
- **Другий Лінійний Шар:** Зменшує розмірність назад до `emb_dim`.
|
||||
|
||||
> [!TIP]
|
||||
> Як ви можете бачити, мережа Feed Forward використовує 3 шари. Перший - це лінійний шар, який множить розміри на 4, використовуючи лінійні ваги (параметри для навчання всередині моделі). Потім функція GELU використовується у всіх цих вимірах, щоб застосувати нелінійні варіації для захоплення багатших представлень, а в кінці використовується ще один лінійний шар, щоб повернутися до початкового розміру вимірів.
|
||||
> Як ви можете бачити, мережа Feed Forward використовує 3 шари. Перший - це лінійний шар, який множить розміри на 4, використовуючи лінійні ваги (параметри для навчання всередині моделі). Потім функція GELU використовується у всіх цих вимірах, щоб застосувати нелінійні варіації для захоплення багатших представлень, а в кінці ще один лінійний шар використовується для повернення до початкового розміру вимірів.
|
||||
|
||||
### **Механізм Багатоголової Уваги**
|
||||
|
||||
@ -266,10 +266,10 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
- **Маскування:** Застосовується каузальна маска, щоб запобігти моделі звертатися до майбутніх токенів (важливо для авторегресивних моделей, таких як GPT).
|
||||
- **Ваги Уваги:** Softmax замаскованих і масштабованих оцінок уваги.
|
||||
- **Контекстний Вектор:** Вагова сума значень відповідно до ваг уваги.
|
||||
- **Вихідна Проекція:** Лінійний шар для об'єднання виходів усіх голів.
|
||||
- **Вихідна Проекція:** Лінійний шар для комбінування виходів усіх голів.
|
||||
|
||||
> [!TIP]
|
||||
> Мета цієї мережі - знайти відносини між токенами в одному контексті. Більше того, токени діляться на різні голови, щоб запобігти перенавчанню, хоча фінальні відносини, знайдені для кожної голови, об'єднуються в кінці цієї мережі.
|
||||
> Мета цієї мережі - знайти відносини між токенами в одному контексті. Більше того, токени діляться на різні голови, щоб запобігти перенавчанню, хоча фінальні відносини, знайдені для кожної голови, комбінуються в кінці цієї мережі.
|
||||
>
|
||||
> Більше того, під час навчання застосовується **каузальна маска**, щоб пізні токени не враховувалися при пошуку специфічних відносин до токена, а також застосовується деякий **dropout** для **запобігання перенавчанню**.
|
||||
|
||||
@ -348,8 +348,8 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
```
|
||||
#### **Мета та Функціональність**
|
||||
|
||||
- **Складові Шарів:** Поєднує багатоголову увагу, мережу прямого проходження, нормалізацію шару та залишкові з'єднання.
|
||||
- **Нормалізація Шару:** Застосовується перед шарами уваги та прямого проходження для стабільного навчання.
|
||||
- **Складові Шарів:** Поєднує багатоголову увагу, мережу прямого проходження, нормалізацію шарів та залишкові з'єднання.
|
||||
- **Нормалізація Шарів:** Застосовується перед шарами уваги та прямого проходження для стабільного навчання.
|
||||
- **Залишкові З'єднання (Швидкі З'єднання):** Додають вхід шару до його виходу для покращення потоку градієнтів та можливості навчання глибоких мереж.
|
||||
- **Випадкове Вимкнення:** Застосовується після шарів уваги та прямого проходження для регуляризації.
|
||||
|
||||
@ -358,21 +358,21 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
1. **Перший Залишковий Шлях (Само-Увага):**
|
||||
- **Вхід (`shortcut`):** Зберегти оригінальний вхід для залишкового з'єднання.
|
||||
- **Нормалізація Шару (`norm1`):** Нормалізувати вхід.
|
||||
- **Багатоголова Увага (`att`):** Застосувати само-увагу.
|
||||
- **Багатоголове Увага (`att`):** Застосувати само-увагу.
|
||||
- **Випадкове Вимкнення (`drop_shortcut`):** Застосувати випадкове вимкнення для регуляризації.
|
||||
- **Додати Залишок (`x + shortcut`):** Об'єднати з оригінальним входом.
|
||||
- **Додати Залишок (`x + shortcut`):** Поєднати з оригінальним входом.
|
||||
2. **Другий Залишковий Шлях (Прямий Прохід):**
|
||||
- **Вхід (`shortcut`):** Зберегти оновлений вхід для наступного залишкового з'єднання.
|
||||
- **Нормалізація Шару (`norm2`):** Нормалізувати вхід.
|
||||
- **Мережа Прямого Проходження (`ff`):** Застосувати перетворення прямого проходження.
|
||||
- **Мережа Прямого Проходження (`ff`):** Застосувати трансформацію прямого проходження.
|
||||
- **Випадкове Вимкнення (`drop_shortcut`):** Застосувати випадкове вимкнення.
|
||||
- **Додати Залишок (`x + shortcut`):** Об'єднати з входом з першого залишкового шляху.
|
||||
- **Додати Залишок (`x + shortcut`):** Поєднати з входом з першого залишкового шляху.
|
||||
|
||||
> [!TIP]
|
||||
> Блок трансформера об'єднує всі мережі разом і застосовує деяку **нормалізацію** та **випадкові вимкнення** для покращення стабільності навчання та результатів.\
|
||||
> Зверніть увагу, як випадкові вимкнення виконуються після використання кожної мережі, тоді як нормалізація застосовується перед.
|
||||
>
|
||||
> Більше того, він також використовує швидкі з'єднання, які полягають у **додаванні виходу мережі до її входу**. Це допомагає запобігти проблемі зникнення градієнта, забезпечуючи, щоб початкові шари вносили "стільки ж", скільки й останні.
|
||||
> Більше того, він також використовує швидкі з'єднання, які полягають у **додаванні виходу мережі до її входу**. Це допомагає запобігти проблемі зникнення градієнта, забезпечуючи, щоб початкові шари вносили "стільки ж", скільки останні.
|
||||
|
||||
### **GPTModel**
|
||||
|
||||
@ -446,7 +446,7 @@ return logits # Output shape: (batch_size, seq_len, vocab_size)
|
||||
>
|
||||
> Зверніть увагу, як він **використовуватиме стільки трансформерних блоків, скільки вказано**, і що кожен трансформерний блок використовує одну мережу з багатоголовим увагою, одну мережу прямого проходження та кілька нормалізацій. Тож, якщо використовується 12 трансформерних блоків, помножте це на 12.
|
||||
>
|
||||
> Більше того, **шар нормалізації** додається **перед** **виходом**, а фінальний лінійний шар застосовується в кінці, щоб отримати результати з правильними розмірами. Зверніть увагу, що кожен фінальний вектор має розмір використаного словника. Це тому, що він намагається отримати ймовірність для кожного можливого токена в словнику.
|
||||
> Більше того, **шар нормалізації** додається **перед** **виходом**, і в кінці застосовується фінальний лінійний шар, щоб отримати результати з правильними розмірами. Зверніть увагу, що кожен фінальний вектор має розмір використаного словника. Це тому, що він намагається отримати ймовірність для кожного можливого токена в словнику.
|
||||
|
||||
## Кількість параметрів для навчання
|
||||
|
||||
@ -547,14 +547,14 @@ ff_second_layer_params = (4 * emb_dim * emb_dim) + emb_dim
|
||||
ff_second_layer_params = (3072 * 768) + 768 = 2,359,296 + 768 = 2,360,064
|
||||
```
|
||||
|
||||
- **Загальна кількість параметрів зворотного зв'язку:**
|
||||
- **Загальна кількість параметрів мережі зворотного зв'язку:**
|
||||
|
||||
```python
|
||||
ff_params = ff_first_layer_params + ff_second_layer_params
|
||||
ff_params = 2,362,368 + 2,360,064 = 4,722,432
|
||||
```
|
||||
|
||||
**c. Нормалізації шару**
|
||||
**c. Нормалізації шарів**
|
||||
|
||||
- **Компоненти:**
|
||||
- Два екземпляри `LayerNorm` на блок.
|
||||
@ -608,9 +608,9 @@ total_params = 163,009,536
|
||||
```
|
||||
## Генерація тексту
|
||||
|
||||
Маючи модель, яка передбачає наступний токен, як і попередній, потрібно просто взяти останні значення токенів з виходу (оскільки вони будуть значеннями передбаченого токена), які будуть **значенням на запис у словнику**, а потім використати функцію `softmax`, щоб нормалізувати виміри в ймовірності, які в сумі дорівнюють 1, а потім отримати індекс найбільшого запису, який буде індексом слова в словнику.
|
||||
Маючи модель, яка передбачає наступний токен, як і попередній, потрібно просто взяти останні значення токенів з виходу (оскільки вони будуть значеннями передбаченого токена), які будуть **значенням на запис у словнику**, а потім використати функцію `softmax`, щоб нормалізувати виміри в ймовірності, що в сумі дорівнюють 1, а потім отримати індекс найбільшого запису, який буде індексом слова в словнику.
|
||||
|
||||
Код з [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
||||
Code from [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
||||
```python
|
||||
def generate_text_simple(model, idx, max_new_tokens, context_size):
|
||||
# idx is (batch, n_tokens) array of indices in the current context
|
||||
|
943
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
943
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
@ -0,0 +1,943 @@
|
||||
# 6. Передтренування та завантаження моделей
|
||||
|
||||
## Генерація тексту
|
||||
|
||||
Щоб навчити модель, нам потрібно, щоб ця модель могла генерувати нові токени. Потім ми порівняємо згенеровані токени з очікуваними, щоб навчити модель **вчитися генерувати необхідні токени**.
|
||||
|
||||
Як і в попередніх прикладах, ми вже передбачили деякі токени, тому можливо повторно використати цю функцію для цієї мети.
|
||||
|
||||
> [!TIP]
|
||||
> Мета цієї шостої фази дуже проста: **Навчити модель з нуля**. Для цього буде використана попередня архітектура LLM з деякими циклами, що проходять через набори даних, використовуючи визначені функції втрат і оптимізатор для навчання всіх параметрів моделі.
|
||||
|
||||
## Оцінка тексту
|
||||
|
||||
Щоб виконати правильне навчання, потрібно виміряти прогнози, отримані для очікуваного токена. Мета навчання полягає в максимізації ймовірності правильного токена, що передбачає збільшення його ймовірності відносно інших токенів.
|
||||
|
||||
Щоб максимізувати ймовірність правильного токена, потрібно змінити ваги моделі так, щоб ця ймовірність була максимізована. Оновлення ваг здійснюється через **зворотне поширення**. Це вимагає **функцію втрат для максимізації**. У цьому випадку функцією буде **різниця між виконаним прогнозом і бажаним**.
|
||||
|
||||
Однак, замість роботи з сирими прогнозами, буде працювати з логарифмом з основою n. Тож, якщо поточний прогноз очікуваного токена становив 7.4541e-05, натуральний логарифм (основа *e*) **7.4541e-05** приблизно дорівнює **-9.5042**.\
|
||||
Отже, для кожного запису з довжиною контексту 5 токенів, наприклад, модель повинна передбачити 5 токенів, причому перші 4 токени - це останній з вхідних, а п'ятий - передбачений. Таким чином, для кожного запису ми отримаємо 5 прогнозів у цьому випадку (навіть якщо перші 4 були у вхідних даних, модель цього не знає) з 5 очікуваними токенами і, отже, 5 ймовірностями для максимізації.
|
||||
|
||||
Отже, після виконання натурального логарифму для кожного прогнозу, обчислюється **середнє**, **знак мінус видаляється** (це називається _крос-ентропійні втрати_) і це **число потрібно зменшити якомога ближче до 0**, оскільки натуральний логарифм 1 дорівнює 0:
|
||||
|
||||
<figure><img src="../../images/image (10) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233">https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233</a></p></figcaption></figure>
|
||||
|
||||
Інший спосіб виміряти, наскільки хороша модель, називається перплексія. **Перплексія** - це метрика, що використовується для оцінки того, наскільки добре ймовірнісна модель прогнозує зразок. У мовному моделюванні вона представляє **невизначеність моделі** при прогнозуванні наступного токена в послідовності.\
|
||||
Наприклад, значення перплексії 48725 означає, що коли потрібно передбачити токен, модель не впевнена, який з 48,725 токенів у словнику є правильним.
|
||||
|
||||
## Приклад передтренування
|
||||
|
||||
Це початковий код, запропонований у [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb), іноді трохи модифікований
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Попередній код, використаний тут, але вже пояснений у попередніх розділах</summary>
|
||||
```python
|
||||
"""
|
||||
This is code explained before so it won't be exaplained
|
||||
"""
|
||||
|
||||
import tiktoken
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
|
||||
|
||||
class GPTDatasetV1(Dataset):
|
||||
def __init__(self, txt, tokenizer, max_length, stride):
|
||||
self.input_ids = []
|
||||
self.target_ids = []
|
||||
|
||||
# Tokenize the entire text
|
||||
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
|
||||
|
||||
# Use a sliding window to chunk the book into overlapping sequences of max_length
|
||||
for i in range(0, len(token_ids) - max_length, stride):
|
||||
input_chunk = token_ids[i:i + max_length]
|
||||
target_chunk = token_ids[i + 1: i + max_length + 1]
|
||||
self.input_ids.append(torch.tensor(input_chunk))
|
||||
self.target_ids.append(torch.tensor(target_chunk))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.input_ids)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.input_ids[idx], self.target_ids[idx]
|
||||
|
||||
|
||||
def create_dataloader_v1(txt, batch_size=4, max_length=256,
|
||||
stride=128, shuffle=True, drop_last=True, num_workers=0):
|
||||
# Initialize the tokenizer
|
||||
tokenizer = tiktoken.get_encoding("gpt2")
|
||||
|
||||
# Create dataset
|
||||
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
|
||||
|
||||
# Create dataloader
|
||||
dataloader = DataLoader(
|
||||
dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last, num_workers=num_workers)
|
||||
|
||||
return dataloader
|
||||
|
||||
|
||||
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 n_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
|
||||
|
||||
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.reshape(b, num_tokens, self.d_out)
|
||||
context_vec = self.out_proj(context_vec) # optional projection
|
||||
|
||||
return context_vec
|
||||
|
||||
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, emb_dim):
|
||||
super().__init__()
|
||||
self.eps = 1e-5
|
||||
self.scale = nn.Parameter(torch.ones(emb_dim))
|
||||
self.shift = nn.Parameter(torch.zeros(emb_dim))
|
||||
|
||||
def forward(self, x):
|
||||
mean = x.mean(dim=-1, keepdim=True)
|
||||
var = x.var(dim=-1, keepdim=True, unbiased=False)
|
||||
norm_x = (x - mean) / torch.sqrt(var + self.eps)
|
||||
return self.scale * norm_x + self.shift
|
||||
|
||||
|
||||
class GELU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, x):
|
||||
return 0.5 * x * (1 + torch.tanh(
|
||||
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
(x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.layers = nn.Sequential(
|
||||
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
|
||||
GELU(),
|
||||
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.layers(x)
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.att = MultiHeadAttention(
|
||||
d_in=cfg["emb_dim"],
|
||||
d_out=cfg["emb_dim"],
|
||||
context_length=cfg["context_length"],
|
||||
num_heads=cfg["n_heads"],
|
||||
dropout=cfg["drop_rate"],
|
||||
qkv_bias=cfg["qkv_bias"])
|
||||
self.ff = FeedForward(cfg)
|
||||
self.norm1 = LayerNorm(cfg["emb_dim"])
|
||||
self.norm2 = LayerNorm(cfg["emb_dim"])
|
||||
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
def forward(self, x):
|
||||
# Shortcut connection for attention block
|
||||
shortcut = x
|
||||
x = self.norm1(x)
|
||||
x = self.att(x) # Shape [batch_size, num_tokens, emb_size]
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
|
||||
# Shortcut connection for feed-forward block
|
||||
shortcut = x
|
||||
x = self.norm2(x)
|
||||
x = self.ff(x)
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class GPTModel(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
|
||||
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
|
||||
self.drop_emb = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
self.trf_blocks = nn.Sequential(
|
||||
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])
|
||||
|
||||
self.final_norm = LayerNorm(cfg["emb_dim"])
|
||||
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
|
||||
|
||||
def forward(self, in_idx):
|
||||
batch_size, seq_len = in_idx.shape
|
||||
tok_embeds = self.tok_emb(in_idx)
|
||||
pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
|
||||
x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]
|
||||
x = self.drop_emb(x)
|
||||
x = self.trf_blocks(x)
|
||||
x = self.final_norm(x)
|
||||
logits = self.out_head(x)
|
||||
return logits
|
||||
```
|
||||
</details>
|
||||
```python
|
||||
# Download contents to train the data with
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
file_path = "the-verdict.txt"
|
||||
url = "https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt"
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
with urllib.request.urlopen(url) as response:
|
||||
text_data = response.read().decode('utf-8')
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
file.write(text_data)
|
||||
else:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
text_data = file.read()
|
||||
|
||||
total_characters = len(text_data)
|
||||
tokenizer = tiktoken.get_encoding("gpt2")
|
||||
total_tokens = len(tokenizer.encode(text_data))
|
||||
|
||||
print("Data downloaded")
|
||||
print("Characters:", total_characters)
|
||||
print("Tokens:", total_tokens)
|
||||
|
||||
# Model initialization
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, # Vocabulary size
|
||||
"context_length": 256, # Shortened context length (orig: 1024)
|
||||
"emb_dim": 768, # Embedding dimension
|
||||
"n_heads": 12, # Number of attention heads
|
||||
"n_layers": 12, # Number of layers
|
||||
"drop_rate": 0.1, # Dropout rate
|
||||
"qkv_bias": False # Query-key-value bias
|
||||
}
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.eval()
|
||||
print ("Model initialized")
|
||||
|
||||
|
||||
# Functions to transform from tokens to ids and from to ids to tokens
|
||||
def text_to_token_ids(text, tokenizer):
|
||||
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
|
||||
return encoded_tensor
|
||||
|
||||
def token_ids_to_text(token_ids, tokenizer):
|
||||
flat = token_ids.squeeze(0) # remove batch dimension
|
||||
return tokenizer.decode(flat.tolist())
|
||||
|
||||
|
||||
|
||||
# Define loss functions
|
||||
def calc_loss_batch(input_batch, target_batch, model, device):
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
logits = model(input_batch)
|
||||
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
|
||||
return loss
|
||||
|
||||
|
||||
def calc_loss_loader(data_loader, model, device, num_batches=None):
|
||||
total_loss = 0.
|
||||
if len(data_loader) == 0:
|
||||
return float("nan")
|
||||
elif num_batches is None:
|
||||
num_batches = len(data_loader)
|
||||
else:
|
||||
# Reduce the number of batches to match the total number of batches in the data loader
|
||||
# if num_batches exceeds the number of batches in the data loader
|
||||
num_batches = min(num_batches, len(data_loader))
|
||||
for i, (input_batch, target_batch) in enumerate(data_loader):
|
||||
if i < num_batches:
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
total_loss += loss.item()
|
||||
else:
|
||||
break
|
||||
return total_loss / num_batches
|
||||
|
||||
|
||||
# Apply Train/validation ratio and create dataloaders
|
||||
train_ratio = 0.90
|
||||
split_idx = int(train_ratio * len(text_data))
|
||||
train_data = text_data[:split_idx]
|
||||
val_data = text_data[split_idx:]
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
train_loader = create_dataloader_v1(
|
||||
train_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=True,
|
||||
shuffle=True,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
val_loader = create_dataloader_v1(
|
||||
val_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=False,
|
||||
shuffle=False,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
|
||||
# Sanity checks
|
||||
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the training loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"increase the `training_ratio`")
|
||||
|
||||
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the validation loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"decrease the `training_ratio`")
|
||||
|
||||
print("Train loader:")
|
||||
for x, y in train_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
print("\nValidation loader:")
|
||||
for x, y in val_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
train_tokens = 0
|
||||
for input_batch, target_batch in train_loader:
|
||||
train_tokens += input_batch.numel()
|
||||
|
||||
val_tokens = 0
|
||||
for input_batch, target_batch in val_loader:
|
||||
val_tokens += input_batch.numel()
|
||||
|
||||
print("Training tokens:", train_tokens)
|
||||
print("Validation tokens:", val_tokens)
|
||||
print("All tokens:", train_tokens + val_tokens)
|
||||
|
||||
|
||||
# Indicate the device to use
|
||||
if torch.cuda.is_available():
|
||||
device = torch.device("cuda")
|
||||
elif torch.backends.mps.is_available():
|
||||
device = torch.device("mps")
|
||||
else:
|
||||
device = torch.device("cpu")
|
||||
|
||||
print(f"Using {device} device.")
|
||||
|
||||
model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes
|
||||
|
||||
|
||||
|
||||
# Pre-calculate losses without starting yet
|
||||
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader
|
||||
|
||||
with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
|
||||
train_loss = calc_loss_loader(train_loader, model, device)
|
||||
val_loss = calc_loss_loader(val_loader, model, device)
|
||||
|
||||
print("Training loss:", train_loss)
|
||||
print("Validation loss:", val_loss)
|
||||
|
||||
|
||||
# Functions to train the data
|
||||
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
|
||||
eval_freq, eval_iter, start_context, tokenizer):
|
||||
# Initialize lists to track losses and tokens seen
|
||||
train_losses, val_losses, track_tokens_seen = [], [], []
|
||||
tokens_seen, global_step = 0, -1
|
||||
|
||||
# Main training loop
|
||||
for epoch in range(num_epochs):
|
||||
model.train() # Set model to training mode
|
||||
|
||||
for input_batch, target_batch in train_loader:
|
||||
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
loss.backward() # Calculate loss gradients
|
||||
optimizer.step() # Update model weights using loss gradients
|
||||
tokens_seen += input_batch.numel()
|
||||
global_step += 1
|
||||
|
||||
# Optional evaluation step
|
||||
if global_step % eval_freq == 0:
|
||||
train_loss, val_loss = evaluate_model(
|
||||
model, train_loader, val_loader, device, eval_iter)
|
||||
train_losses.append(train_loss)
|
||||
val_losses.append(val_loss)
|
||||
track_tokens_seen.append(tokens_seen)
|
||||
print(f"Ep {epoch+1} (Step {global_step:06d}): "
|
||||
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
|
||||
|
||||
# Print a sample text after each epoch
|
||||
generate_and_print_sample(
|
||||
model, tokenizer, device, start_context
|
||||
)
|
||||
|
||||
return train_losses, val_losses, track_tokens_seen
|
||||
|
||||
|
||||
def evaluate_model(model, train_loader, val_loader, device, eval_iter):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
|
||||
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
|
||||
model.train()
|
||||
return train_loss, val_loss
|
||||
|
||||
|
||||
def generate_and_print_sample(model, tokenizer, device, start_context):
|
||||
model.eval()
|
||||
context_size = model.pos_emb.weight.shape[0]
|
||||
encoded = text_to_token_ids(start_context, tokenizer).to(device)
|
||||
with torch.no_grad():
|
||||
token_ids = generate_text(
|
||||
model=model, idx=encoded,
|
||||
max_new_tokens=50, context_size=context_size
|
||||
)
|
||||
decoded_text = token_ids_to_text(token_ids, tokenizer)
|
||||
print(decoded_text.replace("\n", " ")) # Compact print format
|
||||
model.train()
|
||||
|
||||
|
||||
# Start training!
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.to(device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
|
||||
|
||||
num_epochs = 10
|
||||
train_losses, val_losses, tokens_seen = train_model_simple(
|
||||
model, train_loader, val_loader, optimizer, device,
|
||||
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
|
||||
start_context="Every effort moves you", tokenizer=tokenizer
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time_minutes = (end_time - start_time) / 60
|
||||
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
|
||||
|
||||
|
||||
|
||||
# Show graphics with the training process
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.ticker import MaxNLocator
|
||||
import math
|
||||
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
|
||||
fig, ax1 = plt.subplots(figsize=(5, 3))
|
||||
ax1.plot(epochs_seen, train_losses, label="Training loss")
|
||||
ax1.plot(
|
||||
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
|
||||
)
|
||||
ax1.set_xlabel("Epochs")
|
||||
ax1.set_ylabel("Loss")
|
||||
ax1.legend(loc="upper right")
|
||||
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
|
||||
ax2 = ax1.twiny()
|
||||
ax2.plot(tokens_seen, train_losses, alpha=0)
|
||||
ax2.set_xlabel("Tokens seen")
|
||||
fig.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Compute perplexity from the loss values
|
||||
train_ppls = [math.exp(loss) for loss in train_losses]
|
||||
val_ppls = [math.exp(loss) for loss in val_losses]
|
||||
# Plot perplexity over tokens seen
|
||||
plt.figure()
|
||||
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
|
||||
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
|
||||
plt.xlabel('Tokens Seen')
|
||||
plt.ylabel('Perplexity')
|
||||
plt.title('Perplexity over Training')
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
|
||||
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
|
||||
|
||||
|
||||
torch.save({
|
||||
"model_state_dict": model.state_dict(),
|
||||
"optimizer_state_dict": optimizer.state_dict(),
|
||||
},
|
||||
"/tmp/model_and_optimizer.pth"
|
||||
)
|
||||
```
|
||||
Давайте розглянемо пояснення крок за кроком
|
||||
|
||||
### Функції для перетворення тексту <--> id
|
||||
|
||||
Це деякі прості функції, які можна використовувати для перетворення текстів з словника в id та навпаки. Це необхідно на початку обробки тексту та в кінці прогнозів:
|
||||
```python
|
||||
# Functions to transform from tokens to ids and from to ids to tokens
|
||||
def text_to_token_ids(text, tokenizer):
|
||||
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
|
||||
return encoded_tensor
|
||||
|
||||
def token_ids_to_text(token_ids, tokenizer):
|
||||
flat = token_ids.squeeze(0) # remove batch dimension
|
||||
return tokenizer.decode(flat.tolist())
|
||||
```
|
||||
### Генерація текстових функцій
|
||||
|
||||
У попередньому розділі була функція, яка просто отримувала **найбільш ймовірний токен** після отримання логітів. Однак це означатиме, що для кожного введення завжди буде генеруватися один і той же вихід, що робить його дуже детермінованим.
|
||||
|
||||
Наступна функція `generate_text` застосовуватиме концепції `top-k`, `temperature` та `multinomial`.
|
||||
|
||||
- **`top-k`** означає, що ми почнемо зменшувати до `-inf` всі ймовірності всіх токенів, за винятком топ k токенів. Отже, якщо k=3, перед прийняттям рішення лише 3 найімовірніші токени матимуть ймовірність, відмінну від `-inf`.
|
||||
- **`temperature`** означає, що кожна ймовірність буде поділена на значення температури. Значення `0.1` покращить найвищу ймовірність у порівнянні з найнижчою, тоді як температура `5`, наприклад, зробить її більш рівномірною. Це допомагає покращити варіацію у відповідях, яку ми хотіли б, щоб LLM мала.
|
||||
- Після застосування температури знову застосовується функція **`softmax`**, щоб усі залишкові токени мали загальну ймовірність 1.
|
||||
- Нарешті, замість вибору токена з найбільшою ймовірністю, функція **`multinomial`** застосовується для **прогнозування наступного токена відповідно до фінальних ймовірностей**. Отже, якщо токен 1 мав 70% ймовірності, токен 2 - 20%, а токен 3 - 10%, 70% часу буде обрано токен 1, 20% часу - токен 2, а 10% часу - токен 3.
|
||||
```python
|
||||
# Generate text function
|
||||
def generate_text(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None):
|
||||
|
||||
# For-loop is the same as before: Get logits, and only focus on last time step
|
||||
for _ in range(max_new_tokens):
|
||||
idx_cond = idx[:, -context_size:]
|
||||
with torch.no_grad():
|
||||
logits = model(idx_cond)
|
||||
logits = logits[:, -1, :]
|
||||
|
||||
# New: Filter logits with top_k sampling
|
||||
if top_k is not None:
|
||||
# Keep only top_k values
|
||||
top_logits, _ = torch.topk(logits, top_k)
|
||||
min_val = top_logits[:, -1]
|
||||
logits = torch.where(logits < min_val, torch.tensor(float("-inf")).to(logits.device), logits)
|
||||
|
||||
# New: Apply temperature scaling
|
||||
if temperature > 0.0:
|
||||
logits = logits / temperature
|
||||
|
||||
# Apply softmax to get probabilities
|
||||
probs = torch.softmax(logits, dim=-1) # (batch_size, context_len)
|
||||
|
||||
# Sample from the distribution
|
||||
idx_next = torch.multinomial(probs, num_samples=1) # (batch_size, 1)
|
||||
|
||||
# Otherwise same as before: get idx of the vocab entry with the highest logits value
|
||||
else:
|
||||
idx_next = torch.argmax(logits, dim=-1, keepdim=True) # (batch_size, 1)
|
||||
|
||||
if idx_next == eos_id: # Stop generating early if end-of-sequence token is encountered and eos_id is specified
|
||||
break
|
||||
|
||||
# Same as before: append sampled index to the running sequence
|
||||
idx = torch.cat((idx, idx_next), dim=1) # (batch_size, num_tokens+1)
|
||||
|
||||
return idx
|
||||
```
|
||||
> [!TIP]
|
||||
> Існує загальна альтернатива до `top-k`, яка називається [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), також відома як ядерне вибіркове дослідження, яка замість отримання k зразків з найбільшою ймовірністю **організовує** весь отриманий **словник** за ймовірностями та **сумує** їх від найвищої ймовірності до найнижчої, поки не буде досягнуто **порогу**.
|
||||
>
|
||||
> Тоді **тільки ті слова** словника будуть враховані відповідно до їх відносних ймовірностей.
|
||||
>
|
||||
> Це дозволяє не вибирати кількість `k` зразків, оскільки оптимальне k може бути різним у кожному випадку, а **тільки поріг**.
|
||||
>
|
||||
> _Зверніть увагу, що це покращення не включено в попередній код._
|
||||
|
||||
> [!TIP]
|
||||
> Інший спосіб покращити згенерований текст - це використання **Beam search** замість жадібного пошуку, використаного в цьому прикладі.\
|
||||
> На відміну від жадібного пошуку, який вибирає найбільш ймовірне наступне слово на кожному кроці та будує єдину послідовність, **beam search відстежує топ 𝑘 k найвищих оцінених часткових послідовностей** (називаються "промінями") на кожному кроці. Досліджуючи кілька можливостей одночасно, він балансує ефективність і якість, збільшуючи шанси на **знаходження кращої загальної** послідовності, яка може бути пропущена жадібним підходом через ранні, не оптимальні вибори.
|
||||
>
|
||||
> _Зверніть увагу, що це покращення не включено в попередній код._
|
||||
|
||||
### Loss functions
|
||||
|
||||
Функція **`calc_loss_batch`** обчислює крос-ентропію прогнозу одного пакета.\
|
||||
Функція **`calc_loss_loader`** отримує крос-ентропію всіх пакетів і обчислює **середню крос-ентропію**.
|
||||
```python
|
||||
# Define loss functions
|
||||
def calc_loss_batch(input_batch, target_batch, model, device):
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
logits = model(input_batch)
|
||||
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
|
||||
return loss
|
||||
|
||||
def calc_loss_loader(data_loader, model, device, num_batches=None):
|
||||
total_loss = 0.
|
||||
if len(data_loader) == 0:
|
||||
return float("nan")
|
||||
elif num_batches is None:
|
||||
num_batches = len(data_loader)
|
||||
else:
|
||||
# Reduce the number of batches to match the total number of batches in the data loader
|
||||
# if num_batches exceeds the number of batches in the data loader
|
||||
num_batches = min(num_batches, len(data_loader))
|
||||
for i, (input_batch, target_batch) in enumerate(data_loader):
|
||||
if i < num_batches:
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
total_loss += loss.item()
|
||||
else:
|
||||
break
|
||||
return total_loss / num_batches
|
||||
```
|
||||
> [!TIP]
|
||||
> **Градієнтне обрізання** - це техніка, що використовується для підвищення **стабільності навчання** у великих нейронних мережах шляхом встановлення **максимального порогу** для величин градієнтів. Коли градієнти перевищують цей попередньо визначений `max_norm`, вони пропорційно зменшуються, щоб забезпечити, що оновлення параметрів моделі залишаються в межах керованого діапазону, запобігаючи таким проблемам, як вибухаючі градієнти, і забезпечуючи більш контрольоване та стабільне навчання.
|
||||
>
|
||||
> _Зверніть увагу, що це покращення не включено в попередній код._
|
||||
>
|
||||
> Перегляньте наступний приклад:
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Завантаження даних
|
||||
|
||||
Функції `create_dataloader_v1` та `create_dataloader_v1` вже обговорювалися в попередньому розділі.
|
||||
|
||||
З цього моменту зверніть увагу, що визначено, що 90% тексту буде використано для навчання, тоді як 10% буде використано для валідації, і обидва набори зберігаються в 2 різних завантажувачах даних.\
|
||||
Зверніть увагу, що іноді частина набору даних також залишається для тестового набору, щоб краще оцінити продуктивність моделі.
|
||||
|
||||
Обидва завантажувачі даних використовують один і той же розмір партії, максимальну довжину, крок і кількість робітників (0 у цьому випадку).\
|
||||
Основні відмінності полягають у даних, що використовуються кожним, а валідатор не відкидає останній і не перемішує дані, оскільки це не потрібно для цілей валідації.
|
||||
|
||||
Також той факт, що **крок такий же великий, як довжина контексту**, означає, що не буде перекриття між контекстами, що використовуються для навчання даних (зменшує перенавчання, але також і навчальний набір даних).
|
||||
|
||||
Більше того, зверніть увагу, що розмір партії в цьому випадку становить 2, щоб розділити дані на 2 партії, основна мета цього - дозволити паралельну обробку та зменшити споживання на партію.
|
||||
```python
|
||||
train_ratio = 0.90
|
||||
split_idx = int(train_ratio * len(text_data))
|
||||
train_data = text_data[:split_idx]
|
||||
val_data = text_data[split_idx:]
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
train_loader = create_dataloader_v1(
|
||||
train_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=True,
|
||||
shuffle=True,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
val_loader = create_dataloader_v1(
|
||||
val_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=False,
|
||||
shuffle=False,
|
||||
num_workers=0
|
||||
)
|
||||
```
|
||||
## Перевірка цілісності
|
||||
|
||||
Мета полягає в тому, щоб перевірити, чи є достатня кількість токенів для навчання, чи відповідають форми очікуваним, і отримати деяку інформацію про кількість токенів, використаних для навчання та валідації:
|
||||
```python
|
||||
# Sanity checks
|
||||
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the training loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"increase the `training_ratio`")
|
||||
|
||||
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the validation loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"decrease the `training_ratio`")
|
||||
|
||||
print("Train loader:")
|
||||
for x, y in train_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
print("\nValidation loader:")
|
||||
for x, y in val_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
train_tokens = 0
|
||||
for input_batch, target_batch in train_loader:
|
||||
train_tokens += input_batch.numel()
|
||||
|
||||
val_tokens = 0
|
||||
for input_batch, target_batch in val_loader:
|
||||
val_tokens += input_batch.numel()
|
||||
|
||||
print("Training tokens:", train_tokens)
|
||||
print("Validation tokens:", val_tokens)
|
||||
print("All tokens:", train_tokens + val_tokens)
|
||||
```
|
||||
### Вибір пристрою для навчання та попередніх розрахунків
|
||||
|
||||
Наступний код просто вибирає пристрій для використання та обчислює втрати навчання та втрати валідації (без попереднього навчання) як відправну точку.
|
||||
```python
|
||||
# Indicate the device to use
|
||||
|
||||
if torch.cuda.is_available():
|
||||
device = torch.device("cuda")
|
||||
elif torch.backends.mps.is_available():
|
||||
device = torch.device("mps")
|
||||
else:
|
||||
device = torch.device("cpu")
|
||||
|
||||
print(f"Using {device} device.")
|
||||
|
||||
model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes
|
||||
|
||||
# Pre-calculate losses without starting yet
|
||||
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader
|
||||
|
||||
with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
|
||||
train_loss = calc_loss_loader(train_loader, model, device)
|
||||
val_loss = calc_loss_loader(val_loader, model, device)
|
||||
|
||||
print("Training loss:", train_loss)
|
||||
print("Validation loss:", val_loss)
|
||||
```
|
||||
### Функції навчання
|
||||
|
||||
Функція `generate_and_print_sample` просто отримує контекст і генерує деякі токени, щоб зрозуміти, наскільки добре модель на даний момент. Це викликається функцією `train_model_simple` на кожному кроці.
|
||||
|
||||
Функція `evaluate_model` викликається так часто, як вказано в функції навчання, і використовується для вимірювання втрат під час навчання та втрат валідації на даний момент у навчанні моделі.
|
||||
|
||||
Тоді велика функція `train_model_simple` є тією, яка фактично навчає модель. Вона очікує:
|
||||
|
||||
- Завантажувач навчальних даних (з даними, вже розділеними та підготовленими для навчання)
|
||||
- Завантажувач валідатора
|
||||
- **Оптимізатор**, який буде використовуватися під час навчання: Це функція, яка використовуватиме градієнти та оновлюватиме параметри, щоб зменшити втрати. У цьому випадку, як ви побачите, використовується `AdamW`, але є багато інших.
|
||||
- `optimizer.zero_grad()` викликається для скидання градієнтів на кожному раунді, щоб не накопичувати їх.
|
||||
- Параметр **`lr`** є **швидкістю навчання**, яка визначає **розмір кроків**, які робляться під час процесу оптимізації при оновленні параметрів моделі. **Менша** швидкість навчання означає, що оптимізатор **виконує менші оновлення** ваг, що може призвести до більш **точної** конвергенції, але може **уповільнити** навчання. **Більша** швидкість навчання може прискорити навчання, але **ризикує перепригнути** мінімум функції втрат (**перестрибнути** точку, де функція втрат мінімізується).
|
||||
- **Зниження ваг** модифікує крок **Обчислення втрат**, додаючи додатковий термін, який штрафує великі ваги. Це заохочує оптимізатор знаходити рішення з меншими вагами, балансуючи між хорошим підходом до даних і збереженням моделі простою, запобігаючи перенавчанню в моделях машинного навчання, заважаючи моделі надавати занадто велике значення будь-якій окремій ознаці.
|
||||
- Традиційні оптимізатори, такі як SGD з L2 регуляризацією, поєднують зниження ваг з градієнтом функції втрат. Однак **AdamW** (варіант оптимізатора Adam) розділяє зниження ваг від оновлення градієнта, що призводить до більш ефективної регуляризації.
|
||||
- Пристрій, який використовувати для навчання
|
||||
- Кількість епох: Кількість разів, щоб пройти через навчальні дані
|
||||
- Частота оцінки: Частота виклику `evaluate_model`
|
||||
- Ітерація оцінки: Кількість пакетів, які використовуються при оцінці поточного стану моделі під час виклику `generate_and_print_sample`
|
||||
- Початковий контекст: Яке початкове речення використовувати при виклику `generate_and_print_sample`
|
||||
- Токенізатор
|
||||
```python
|
||||
# Functions to train the data
|
||||
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
|
||||
eval_freq, eval_iter, start_context, tokenizer):
|
||||
# Initialize lists to track losses and tokens seen
|
||||
train_losses, val_losses, track_tokens_seen = [], [], []
|
||||
tokens_seen, global_step = 0, -1
|
||||
|
||||
# Main training loop
|
||||
for epoch in range(num_epochs):
|
||||
model.train() # Set model to training mode
|
||||
|
||||
for input_batch, target_batch in train_loader:
|
||||
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
loss.backward() # Calculate loss gradients
|
||||
optimizer.step() # Update model weights using loss gradients
|
||||
tokens_seen += input_batch.numel()
|
||||
global_step += 1
|
||||
|
||||
# Optional evaluation step
|
||||
if global_step % eval_freq == 0:
|
||||
train_loss, val_loss = evaluate_model(
|
||||
model, train_loader, val_loader, device, eval_iter)
|
||||
train_losses.append(train_loss)
|
||||
val_losses.append(val_loss)
|
||||
track_tokens_seen.append(tokens_seen)
|
||||
print(f"Ep {epoch+1} (Step {global_step:06d}): "
|
||||
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
|
||||
|
||||
# Print a sample text after each epoch
|
||||
generate_and_print_sample(
|
||||
model, tokenizer, device, start_context
|
||||
)
|
||||
|
||||
return train_losses, val_losses, track_tokens_seen
|
||||
|
||||
|
||||
def evaluate_model(model, train_loader, val_loader, device, eval_iter):
|
||||
model.eval() # Set in eval mode to avoid dropout
|
||||
with torch.no_grad():
|
||||
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
|
||||
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
|
||||
model.train() # Back to training model applying all the configurations
|
||||
return train_loss, val_loss
|
||||
|
||||
|
||||
def generate_and_print_sample(model, tokenizer, device, start_context):
|
||||
model.eval() # Set in eval mode to avoid dropout
|
||||
context_size = model.pos_emb.weight.shape[0]
|
||||
encoded = text_to_token_ids(start_context, tokenizer).to(device)
|
||||
with torch.no_grad():
|
||||
token_ids = generate_text(
|
||||
model=model, idx=encoded,
|
||||
max_new_tokens=50, context_size=context_size
|
||||
)
|
||||
decoded_text = token_ids_to_text(token_ids, tokenizer)
|
||||
print(decoded_text.replace("\n", " ")) # Compact print format
|
||||
model.train() # Back to training model applying all the configurations
|
||||
```
|
||||
> [!TIP]
|
||||
> Щоб покращити швидкість навчання, існує кілька відповідних технік, званих **лінійним розігрівом** та **косинусним зменшенням.**
|
||||
>
|
||||
> **Лінійний розігрів** полягає в визначенні початкової швидкості навчання та максимальної, а також у постійному оновленні її після кожної епохи. Це пов'язано з тим, що початок навчання з меншими оновленнями ваг зменшує ризик того, що модель зіткнеться з великими, дестабілізуючими оновленнями під час фази навчання.\
|
||||
> **Косинусне зменшення** - це техніка, яка **поступово зменшує швидкість навчання**, слідуючи половинній косинусній кривій **після фази розігріву**, сповільнюючи оновлення ваг, щоб **мінімізувати ризик перевищення** мінімуму втрат і забезпечити стабільність навчання на пізніших етапах.
|
||||
>
|
||||
> _Зверніть увагу, що ці покращення не включені в попередній код._
|
||||
|
||||
### Почати навчання
|
||||
```python
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.to(device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
|
||||
|
||||
num_epochs = 10
|
||||
train_losses, val_losses, tokens_seen = train_model_simple(
|
||||
model, train_loader, val_loader, optimizer, device,
|
||||
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
|
||||
start_context="Every effort moves you", tokenizer=tokenizer
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time_minutes = (end_time - start_time) / 60
|
||||
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
|
||||
```
|
||||
### Print training evolution
|
||||
|
||||
За допомогою наступної функції можна вивести еволюцію моделі під час її навчання.
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.ticker import MaxNLocator
|
||||
import math
|
||||
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
|
||||
fig, ax1 = plt.subplots(figsize=(5, 3))
|
||||
ax1.plot(epochs_seen, train_losses, label="Training loss")
|
||||
ax1.plot(
|
||||
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
|
||||
)
|
||||
ax1.set_xlabel("Epochs")
|
||||
ax1.set_ylabel("Loss")
|
||||
ax1.legend(loc="upper right")
|
||||
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
|
||||
ax2 = ax1.twiny()
|
||||
ax2.plot(tokens_seen, train_losses, alpha=0)
|
||||
ax2.set_xlabel("Tokens seen")
|
||||
fig.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Compute perplexity from the loss values
|
||||
train_ppls = [math.exp(loss) for loss in train_losses]
|
||||
val_ppls = [math.exp(loss) for loss in val_losses]
|
||||
# Plot perplexity over tokens seen
|
||||
plt.figure()
|
||||
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
|
||||
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
|
||||
plt.xlabel('Tokens Seen')
|
||||
plt.ylabel('Perplexity')
|
||||
plt.title('Perplexity over Training')
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
|
||||
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
|
||||
```
|
||||
### Зберегти модель
|
||||
|
||||
Можливо зберегти модель + оптимізатор, якщо ви хочете продовжити навчання пізніше:
|
||||
```python
|
||||
# Save the model and the optimizer for later training
|
||||
torch.save({
|
||||
"model_state_dict": model.state_dict(),
|
||||
"optimizer_state_dict": optimizer.state_dict(),
|
||||
},
|
||||
"/tmp/model_and_optimizer.pth"
|
||||
)
|
||||
# Note that this model with the optimizer occupied close to 2GB
|
||||
|
||||
# Restore model and optimizer for training
|
||||
checkpoint = torch.load("/tmp/model_and_optimizer.pth", map_location=device)
|
||||
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.load_state_dict(checkpoint["model_state_dict"])
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1)
|
||||
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
||||
model.train(); # Put in training mode
|
||||
```
|
||||
Або просто модель, якщо ви плануєте лише її використовувати:
|
||||
```python
|
||||
# Save the model
|
||||
torch.save(model.state_dict(), "model.pth")
|
||||
|
||||
# Load it
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
|
||||
model.load_state_dict(torch.load("model.pth", map_location=device))
|
||||
|
||||
model.eval() # Put in eval mode
|
||||
```
|
||||
## Завантаження ваг GPT2
|
||||
|
||||
Є 2 швидкі скрипти для локального завантаження ваг GPT2. Для обох ви можете клонувати репозиторій [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) локально, а потім:
|
||||
|
||||
- Скрипт [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py) завантажить всі ваги та перетворить формати з OpenAI на ті, що очікуються нашим LLM. Скрипт також підготовлений з необхідною конфігурацією та з підказкою: "Кожне зусилля наближає вас"
|
||||
- Скрипт [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb) дозволяє вам завантажити будь-які ваги GPT2 локально (просто змініть змінну `CHOOSE_MODEL`) і передбачити текст з деяких підказок.
|
||||
|
||||
## Посилання
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -1,11 +1,11 @@
|
||||
# 7.0. LoRA покращення в тонкому налаштуванні
|
||||
# 7.0. LoRA Improvements in fine-tuning
|
||||
|
||||
## LoRA покращення
|
||||
## LoRA Improvements
|
||||
|
||||
> [!TIP]
|
||||
> Використання **LoRA значно зменшує обчислення**, необхідні для **тонкого налаштування** вже навчених моделей.
|
||||
> Використання **LoRA значно зменшує обчислення**, необхідні для **додаткового налаштування** вже навчених моделей.
|
||||
|
||||
LoRA дозволяє ефективно тонко налаштовувати **великі моделі**, змінюючи лише **невелику частину** моделі. Це зменшує кількість параметрів, які потрібно навчати, економлячи **пам'ять** та **обчислювальні ресурси**. Це відбувається тому, що:
|
||||
LoRA дозволяє ефективно додатково налаштовувати **великі моделі**, змінюючи лише **невелику частину** моделі. Це зменшує кількість параметрів, які потрібно навчати, економлячи **пам'ять** та **обчислювальні ресурси**. Це відбувається тому, що:
|
||||
|
||||
1. **Зменшує кількість навчальних параметрів**: Замість оновлення всієї матриці ваг у моделі, LoRA **ділить** матрицю ваг на дві менші матриці (названі **A** та **B**). Це робить навчання **швидшим** і вимагає **менше пам'яті**, оскільки потрібно оновити менше параметрів.
|
||||
|
||||
@ -14,10 +14,10 @@ LoRA дозволяє ефективно тонко налаштовувати *
|
||||
<figure><img src="../../images/image (9) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
2. **Зберігає оригінальні ваги моделі незмінними**: LoRA дозволяє зберігати оригінальні ваги моделі такими ж, і лише оновлює **нові маленькі матриці** (A та B). Це корисно, оскільки означає, що оригінальні знання моделі зберігаються, і ви лише налаштовуєте те, що необхідно.
|
||||
3. **Ефективне тонке налаштування для конкретних завдань**: Коли ви хочете адаптувати модель до **нового завдання**, ви можете просто навчати **маленькі матриці LoRA** (A та B), залишаючи решту моделі без змін. Це **набагато ефективніше**, ніж повторне навчання всієї моделі.
|
||||
4. **Ефективність зберігання**: Після тонкого налаштування, замість збереження **цілої нової моделі** для кожного завдання, вам потрібно зберігати лише **матриці LoRA**, які є дуже маленькими в порівнянні з усією моделлю. Це полегшує адаптацію моделі до багатьох завдань без використання занадто багато пам'яті.
|
||||
3. **Ефективне специфічне налаштування завдань**: Коли ви хочете адаптувати модель до **нового завдання**, ви можете просто навчити **маленькі матриці LoRA** (A та B), залишаючи решту моделі без змін. Це **набагато ефективніше**, ніж повторне навчання всієї моделі.
|
||||
4. **Ефективність зберігання**: Після додаткового налаштування, замість збереження **цілої нової моделі** для кожного завдання, вам потрібно зберігати лише **матриці LoRA**, які є дуже маленькими в порівнянні з усією моделлю. Це полегшує адаптацію моделі до багатьох завдань без використання занадто багато пам'яті.
|
||||
|
||||
Для реалізації LoraLayers замість лінійних під час тонкого налаштування, тут пропонується цей код [https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb):
|
||||
Для реалізації LoraLayers замість лінійних під час додаткового налаштування, тут пропонується цей код [https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb):
|
||||
```python
|
||||
import math
|
||||
|
||||
|
@ -0,0 +1,110 @@
|
||||
# 7.1. Налаштування для класифікації
|
||||
|
||||
## Що таке
|
||||
|
||||
Налаштування - це процес взяття **попередньо навченої моделі**, яка навчилася **загальним мовним патернам** з величезних обсягів даних, і **адаптації** її для виконання **конкретного завдання** або для розуміння специфічної мови домену. Це досягається шляхом продовження навчання моделі на меншому, специфічному для завдання наборі даних, що дозволяє їй налаштувати свої параметри для кращого відповідності нюансам нових даних, використовуючи при цьому широкі знання, які вона вже здобула. Налаштування дозволяє моделі надавати більш точні та релевантні результати в спеціалізованих застосуваннях без необхідності навчати нову модель з нуля.
|
||||
|
||||
> [!TIP]
|
||||
> Оскільки попереднє навчання LLM, яка "розуміє" текст, є досить дорогим, зазвичай легше і дешевше налаштувати відкриті попередньо навченої моделі для виконання конкретного завдання, яке ми хочемо, щоб вона виконувала.
|
||||
|
||||
> [!TIP]
|
||||
> Мета цього розділу - показати, як налаштувати вже попередньо навчена модель, щоб замість генерації нового тексту LLM вибирала **ймовірності того, що даний текст буде класифіковано в кожну з наданих категорій** (наприклад, чи є текст спамом чи ні).
|
||||
|
||||
## Підготовка набору даних
|
||||
|
||||
### Розмір набору даних
|
||||
|
||||
Звичайно, для налаштування моделі вам потрібні структуровані дані, щоб спеціалізувати ваш LLM. У прикладі, запропонованому в [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb), GPT2 налаштовується для виявлення, чи є електронний лист спамом, чи ні, використовуючи дані з [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
Цей набір даних містить набагато більше прикладів "не спаму", ніж "спаму", тому книга пропонує **використовувати лише стільки ж прикладів "не спаму", скільки "спаму"** (отже, видаливши з навчальних даних всі додаткові приклади). У цьому випадку це було 747 прикладів кожного.
|
||||
|
||||
Потім **70%** набору даних використовується для **навчання**, **10%** для **перевірки** та **20%** для **тестування**.
|
||||
|
||||
- **Набір для перевірки** використовується під час навчального етапу для налаштування **гіперпараметрів** моделі та прийняття рішень щодо архітектури моделі, ефективно допомагаючи запобігти перенавчанню, надаючи зворотний зв'язок про те, як модель працює з невідомими даними. Це дозволяє здійснювати ітеративні поліпшення без упередження фінальної оцінки.
|
||||
- Це означає, що хоча дані, включені в цей набір даних, не використовуються безпосередньо для навчання, вони використовуються для налаштування найкращих **гіперпараметрів**, тому цей набір не може бути використаний для оцінки продуктивності моделі, як тестовий.
|
||||
- На відміну від цього, **тестовий набір** використовується **тільки після** того, як модель була повністю навчена і всі налаштування завершені; він надає неупереджену оцінку здатності моделі узагальнювати нові, невідомі дані. Ця фінальна оцінка на тестовому наборі дає реалістичне уявлення про те, як модель очікується працювати в реальних застосуваннях.
|
||||
|
||||
### Довжина записів
|
||||
|
||||
Оскільки навчальний приклад очікує записи (тексти електронних листів у цьому випадку) однакової довжини, було вирішено зробити кожен запис таким же великим, як найбільший, додавши ідентифікатори `<|endoftext|>` як заповнювач.
|
||||
|
||||
### Ініціалізація моделі
|
||||
|
||||
Використовуючи відкриті попередньо навченої ваги, ініціалізуйте модель для навчання. Ми вже робили це раніше і, дотримуючись інструкцій [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb), ви можете легко це зробити.
|
||||
|
||||
## Головка класифікації
|
||||
|
||||
У цьому конкретному прикладі (прогнозування, чи є текст спамом, чи ні) нас не цікавить налаштування відповідно до повного словника GPT2, але ми лише хочемо, щоб нова модель сказала, чи є електронний лист спамом (1), чи ні (0). Тому ми збираємося **модифікувати фінальний шар, який** надає ймовірності для кожного токена словника, на той, який лише надає ймовірності бути спамом або ні (тобто як словник з 2 слів).
|
||||
```python
|
||||
# This code modified the final layer with a Linear one with 2 outs
|
||||
num_classes = 2
|
||||
model.out_head = torch.nn.Linear(
|
||||
|
||||
in_features=BASE_CONFIG["emb_dim"],
|
||||
|
||||
out_features=num_classes
|
||||
)
|
||||
```
|
||||
## Параметри для налаштування
|
||||
|
||||
Щоб швидко налаштувати, легше не налаштовувати всі параметри, а лише деякі фінальні. Це пов'язано з тим, що відомо, що нижчі шари зазвичай захоплюють основні мовні структури та семантику. Тому просто **налаштування останніх шарів зазвичай є достатнім і швидшим**.
|
||||
```python
|
||||
# This code makes all the parameters of the model unrtainable
|
||||
for param in model.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
# Allow to fine tune the last layer in the transformer block
|
||||
for param in model.trf_blocks[-1].parameters():
|
||||
param.requires_grad = True
|
||||
|
||||
# Allow to fine tune the final layer norm
|
||||
for param in model.final_norm.parameters():
|
||||
|
||||
param.requires_grad = True
|
||||
```
|
||||
## Entries to use for training
|
||||
|
||||
В попередніх розділах LLM навчався, зменшуючи втрати кожного передбаченого токена, хоча майже всі передбачені токени були в вхідному реченні (лише 1 в кінці дійсно передбачався), щоб модель краще розуміла мову.
|
||||
|
||||
У цьому випадку нас цікавить лише здатність моделі передбачити, чи є модель спамом, чи ні, тому ми звертаємо увагу лише на останній передбачений токен. Отже, потрібно модифікувати наші попередні функції втрат навчання, щоб враховувати лише цей токен.
|
||||
|
||||
Це реалізовано в [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb) як:
|
||||
```python
|
||||
def calc_accuracy_loader(data_loader, model, device, num_batches=None):
|
||||
model.eval()
|
||||
correct_predictions, num_examples = 0, 0
|
||||
|
||||
if num_batches is None:
|
||||
num_batches = len(data_loader)
|
||||
else:
|
||||
num_batches = min(num_batches, len(data_loader))
|
||||
for i, (input_batch, target_batch) in enumerate(data_loader):
|
||||
if i < num_batches:
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
logits = model(input_batch)[:, -1, :] # Logits of last output token
|
||||
predicted_labels = torch.argmax(logits, dim=-1)
|
||||
|
||||
num_examples += predicted_labels.shape[0]
|
||||
correct_predictions += (predicted_labels == target_batch).sum().item()
|
||||
else:
|
||||
break
|
||||
return correct_predictions / num_examples
|
||||
|
||||
|
||||
def calc_loss_batch(input_batch, target_batch, model, device):
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
logits = model(input_batch)[:, -1, :] # Logits of last output token
|
||||
loss = torch.nn.functional.cross_entropy(logits, target_batch)
|
||||
return loss
|
||||
```
|
||||
Зверніть увагу, що для кожної партії нас цікавлять лише **логіти останнього передбаченого токена**.
|
||||
|
||||
## Повний код для тонкої настройки класифікації GPT2
|
||||
|
||||
Ви можете знайти весь код для тонкої настройки GPT2 як класифікатора спаму в [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/load-finetuned-model.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/load-finetuned-model.ipynb)
|
||||
|
||||
## Посилання
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -1,7 +1,7 @@
|
||||
# 7.2. Налаштування для виконання інструкцій
|
||||
|
||||
> [!TIP]
|
||||
> Мета цього розділу - показати, як **налаштувати вже попередньо навчану модель для виконання інструкцій**, а не просто генерувати текст, наприклад, відповідати на завдання як чат-бот.
|
||||
> Мета цього розділу - показати, як **налаштувати вже попередньо навчану модель для виконання інструкцій**, а не просто для генерації тексту, наприклад, відповідаючи на завдання як чат-бот.
|
||||
|
||||
## Набір даних
|
||||
|
||||
@ -56,23 +56,23 @@ print(model_input + desired_response)
|
||||
Тоді потрібно пакетувати всі вхідні дані та очікувані виходи для навчання. Для цього потрібно:
|
||||
|
||||
- Токенізувати тексти
|
||||
- Доповнити всі зразки до однакової довжини (зазвичай довжина буде такою ж великою, як довжина контексту, що використовується для попереднього навчання LLM)
|
||||
- Доповнити всі зразки до однакової довжини (зазвичай довжина буде такою ж, як довжина контексту, використаного для попереднього навчання LLM)
|
||||
- Створити очікувані токени, зсувши вхід на 1 у кастомній функції об'єднання
|
||||
- Замінити деякі токени доповнення на -100, щоб виключити їх з втрат навчання: Після першого токена `endoftext` замінити всі інші токени `endoftext` на -100 (оскільки використання `cross_entropy(...,ignore_index=-100)` означає, що він ігноруватиме цілі з -100)
|
||||
- \[Додатково\] Замаскувати за допомогою -100 також всі токени, що належать до запитання, щоб LLM навчався лише генерувати відповідь. У стилі Apply Alpaca це означатиме замаскувати все до `### Response:`
|
||||
|
||||
З цим створеним, настав час створити завантажувачі даних для кожного набору даних (навчання, валідація та тестування).
|
||||
|
||||
## Завантажити попередньо навчений LLM та тонке налаштування та перевірка втрат
|
||||
## Завантаження попередньо навченої LLM та тонка настройка та перевірка втрат
|
||||
|
||||
Потрібно завантажити попередньо навчений LLM для тонкого налаштування. Це вже обговорювалося на інших сторінках. Тоді можна використовувати раніше використану функцію навчання для тонкого налаштування LLM.
|
||||
Потрібно завантажити попередньо навчений LLM для тонкої настройки. Це вже обговорювалося на інших сторінках. Тоді можна використовувати раніше використану функцію навчання для тонкої настройки LLM.
|
||||
|
||||
Під час навчання також можна спостерігати, як змінюються втрати навчання та втрати валідації протягом епох, щоб побачити, чи зменшуються втрати і чи відбувається перенавчання.\
|
||||
Пам'ятайте, що перенавчання відбувається, коли втрати навчання зменшуються, але втрати валідації не зменшуються або навіть збільшуються. Щоб уникнути цього, найпростіше - зупинити навчання на епосі, де починається ця поведінка.
|
||||
Пам'ятайте, що перенавчання відбувається, коли втрати навчання зменшуються, але втрати валідації не зменшуються або навіть збільшуються. Щоб уникнути цього, найпростіше - зупинити навчання на епосі, коли починається ця поведінка.
|
||||
|
||||
## Якість відповіді
|
||||
|
||||
Оскільки це не тонке налаштування класифікації, де можна більше довіряти змінам втрат, також важливо перевірити якість відповідей у тестовому наборі. Тому рекомендується зібрати згенеровані відповіді з усіх тестових наборів і **перевірити їхню якість вручну**, щоб побачити, чи є неправильні відповіді (зверніть увагу, що LLM може правильно створити формат і синтаксис речення відповіді, але дати абсолютно неправильну відповідь. Зміна втрат не відобразить цю поведінку).\
|
||||
Оскільки це не тонка настройка класифікації, де можна більше довіряти змінам втрат, також важливо перевірити якість відповідей у тестовому наборі. Тому рекомендується зібрати згенеровані відповіді з усіх тестових наборів і **перевірити їхню якість вручну**, щоб побачити, чи є неправильні відповіді (зверніть увагу, що LLM може правильно створити формат і синтаксис речення відповіді, але дати абсолютно неправильну відповідь. Зміна втрат не відобразить цю поведінку).\
|
||||
Зверніть увагу, що також можна провести цей огляд, передавши згенеровані відповіді та очікувані відповіді **іншим LLM і попросивши їх оцінити відповіді**.
|
||||
|
||||
Інші тести, які можна провести для перевірки якості відповідей:
|
||||
@ -82,18 +82,18 @@ print(model_input + desired_response)
|
||||
3. [**AlpacaEval**](https://github.com/tatsu-lab/alpaca_eval)**:** AlpacaEval - це автоматизована система оцінювання, де просунутий LLM, такий як GPT-4, оцінює відповіді інших моделей на різні запити.
|
||||
4. **Оцінка загального мовного розуміння (**[**GLUE**](https://gluebenchmark.com/)**):** GLUE - це збірка з дев'яти завдань з розуміння природної мови, включаючи аналіз настроїв, текстуальне наслідкування та відповіді на запитання.
|
||||
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**:** Спираючись на GLUE, SuperGLUE включає більш складні завдання, які важко виконати для сучасних моделей.
|
||||
6. **Бенчмарк за межами гри імітації (**[**BIG-bench**](https://github.com/google/BIG-bench)**):** BIG-bench - це масштабний бенчмарк з понад 200 завданнями, які тестують здібності моделі в таких областях, як міркування, переклад та відповіді на запитання.
|
||||
7. **Голістична оцінка мовних моделей (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM забезпечує всебічну оцінку за різними метриками, такими як точність, надійність та справедливість.
|
||||
6. **Бенчмарк за межами імітаційної гри (**[**BIG-bench**](https://github.com/google/BIG-bench)**):** BIG-bench - це масштабний бенчмарк з понад 200 завданнями, які тестують здібності моделі в таких областях, як міркування, переклад та відповіді на запитання.
|
||||
7. **Голістична оцінка мовних моделей (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM забезпечує всебічну оцінку за різними метриками, такими як точність, стійкість та справедливість.
|
||||
8. [**OpenAI Evals**](https://github.com/openai/evals)**:** Відкритий фреймворк оцінювання від OpenAI, який дозволяє тестувати AI моделі на кастомних та стандартизованих завданнях.
|
||||
9. [**HumanEval**](https://github.com/openai/human-eval)**:** Збірка програмних задач, що використовуються для оцінки здібностей генерації коду мовними моделями.
|
||||
10. **Набір даних для відповіді на запитання Стенфорда (**[**SQuAD**](https://rajpurkar.github.io/SQuAD-explorer/)**):** SQuAD складається з питань про статті з Вікіпедії, де моделі повинні зрозуміти текст, щоб відповісти точно.
|
||||
11. [**TriviaQA**](https://nlp.cs.washington.edu/triviaqa/)**:** Масштабний набір даних з питань та відповідей на тривіальні запитання, а також документи з доказами.
|
||||
10. **Набір даних для відповіді на запитання Стенфордського університету (**[**SQuAD**](https://rajpurkar.github.io/SQuAD-explorer/)**):** SQuAD складається з питань про статті з Вікіпедії, де моделі повинні зрозуміти текст, щоб відповісти точно.
|
||||
11. [**TriviaQA**](https://nlp.cs.washington.edu/triviaqa/)**:** Масштабний набір даних з питань та відповідей, а також документів-доказів.
|
||||
|
||||
і багато інших
|
||||
|
||||
## Код тонкого налаштування за інструкціями
|
||||
## Код для тонкої настройки за інструкціями
|
||||
|
||||
Ви можете знайти приклад коду для виконання цього тонкого налаштування за адресою [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py)
|
||||
Ви можете знайти приклад коду для виконання цієї тонкої настройки за адресою [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py)
|
||||
|
||||
## Посилання
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user