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
3b5d308e22
commit
72e52f02db
@ -2,7 +2,7 @@
|
||||
|
||||
## Pretraining
|
||||
|
||||
Pretraining to podstawowa faza w rozwijaniu dużego modelu językowego (LLM), w której model jest narażony na ogromne i różnorodne ilości danych tekstowych. W tym etapie **LLM uczy się fundamentalnych struktur, wzorców i niuansów języka**, w tym gramatyki, słownictwa, składni i relacji kontekstowych. Przetwarzając te obszerne dane, model nabywa szerokie zrozumienie języka i ogólnej wiedzy o świecie. Ta kompleksowa baza umożliwia LLM generowanie spójnego i kontekstowo odpowiedniego tekstu. Następnie ten wstępnie wytrenowany model może przejść do fine-tuningu, gdzie jest dalej trenowany na wyspecjalizowanych zbiorach danych, aby dostosować swoje możliwości do konkretnych zadań lub dziedzin, poprawiając swoją wydajność i trafność w docelowych aplikacjach.
|
||||
Pretraining to podstawowa faza w rozwijaniu dużego modelu językowego (LLM), w której model jest narażony na ogromne i różnorodne ilości danych tekstowych. W tym etapie **LLM uczy się fundamentalnych struktur, wzorców i niuansów języka**, w tym gramatyki, słownictwa, składni i relacji kontekstowych. Przetwarzając te obszerne dane, model nabywa szerokie zrozumienie języka i ogólnej wiedzy o świecie. Ta kompleksowa baza umożliwia LLM generowanie spójnego i kontekstowo odpowiedniego tekstu. Następnie ten wstępnie wytrenowany model może przejść do fine-tuningu, gdzie jest dalej trenowany na specjalistycznych zbiorach danych, aby dostosować swoje możliwości do konkretnych zadań lub dziedzin, poprawiając swoją wydajność i trafność w docelowych aplikacjach.
|
||||
|
||||
## Główne komponenty LLM
|
||||
|
||||
@ -43,7 +43,7 @@ W PyTorch, **tenzor** to podstawowa struktura danych, która służy jako wielow
|
||||
|
||||
Z perspektywy obliczeniowej, tenzory działają jako pojemniki na dane wielowymiarowe, gdzie każdy wymiar może reprezentować różne cechy lub aspekty danych. To sprawia, że tenzory są bardzo odpowiednie do obsługi złożonych zbiorów danych w zadaniach uczenia maszynowego.
|
||||
|
||||
### Tenzory PyTorch vs. Tablice NumPy
|
||||
### Tenzory PyTorch a tablice NumPy
|
||||
|
||||
Chociaż tenzory PyTorch są podobne do tablic NumPy pod względem możliwości przechowywania i manipulacji danymi numerycznymi, oferują dodatkowe funkcjonalności kluczowe dla uczenia głębokiego:
|
||||
|
||||
@ -275,7 +275,7 @@ W tym kodzie:
|
||||
Podczas backward pass:
|
||||
|
||||
- PyTorch przeszukuje graf obliczeniowy w odwrotnej kolejności.
|
||||
- Dla każdej operacji stosuje regułę łańcuchową do obliczenia gradientów.
|
||||
- Dla każdej operacji stosuje regułę łańcuchową do obliczania gradientów.
|
||||
- Gradienty są gromadzone w atrybucie `.grad` każdego tensora parametru.
|
||||
|
||||
### **6. Zalety automatycznej różniczkowania**
|
||||
|
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. Próbkowanie Danych
|
||||
|
||||
## **Próbkowanie Danych**
|
||||
|
||||
**Próbkowanie Danych** jest kluczowym procesem w przygotowywaniu danych do trenowania dużych modeli językowych (LLM) takich jak GPT. Polega na organizowaniu danych tekstowych w sekwencje wejściowe i docelowe, które model wykorzystuje do nauki przewidywania następnego słowa (lub tokena) na podstawie poprzednich słów. Odpowiednie próbkowanie danych zapewnia, że model skutecznie uchwyci wzorce językowe i zależności.
|
||||
|
||||
> [!TIP]
|
||||
> Celem tej drugiej fazy jest bardzo proste: **Próbkowanie danych wejściowych i przygotowanie ich do fazy treningowej, zazwyczaj poprzez oddzielenie zbioru danych na zdania o określonej długości i wygenerowanie również oczekiwanej odpowiedzi.**
|
||||
|
||||
### **Dlaczego Próbkowanie Danych Ma Znaczenie**
|
||||
|
||||
LLM, takie jak GPT, są trenowane do generowania lub przewidywania tekstu poprzez zrozumienie kontekstu dostarczonego przez poprzednie słowa. Aby to osiągnąć, dane treningowe muszą być uporządkowane w sposób, który pozwala modelowi nauczyć się relacji między sekwencjami słów a ich następnymi słowami. To uporządkowane podejście pozwala modelowi na generalizację i generowanie spójnego oraz kontekstowo odpowiedniego tekstu.
|
||||
|
||||
### **Kluczowe Pojęcia w Próbkowaniu Danych**
|
||||
|
||||
1. **Tokenizacja:** Rozkładanie tekstu na mniejsze jednostki zwane tokenami (np. słowa, podsłowa lub znaki).
|
||||
2. **Długość Sekwencji (max_length):** Liczba tokenów w każdej sekwencji wejściowej.
|
||||
3. **Przesuwane Okno:** Metoda tworzenia nakładających się sekwencji wejściowych poprzez przesuwanie okna po tokenizowanym tekście.
|
||||
4. **Krok:** Liczba tokenów, o które przesuwa się przesuwane okno, aby utworzyć następną sekwencję.
|
||||
|
||||
### **Przykład Krok po Kroku**
|
||||
|
||||
Przejdźmy przez przykład, aby zilustrować próbkowanie danych.
|
||||
|
||||
**Przykładowy Tekst**
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
**Tokenizacja**
|
||||
|
||||
Załóżmy, że używamy **podstawowego tokenizera**, który dzieli tekst na słowa i znaki interpunkcyjne:
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
**Parametry**
|
||||
|
||||
- **Maksymalna długość sekwencji (max_length):** 4 tokeny
|
||||
- **Krok okna przesuwającego:** 1 token
|
||||
|
||||
**Tworzenie sekwencji wejściowych i docelowych**
|
||||
|
||||
1. **Podejście z oknem przesuwającym:**
|
||||
- **Sekwencje wejściowe:** Każda sekwencja wejściowa składa się z `max_length` tokenów.
|
||||
- **Sekwencje docelowe:** Każda sekwencja docelowa składa się z tokenów, które bezpośrednio następują po odpowiadającej sekwencji wejściowej.
|
||||
2. **Generowanie sekwencji:**
|
||||
|
||||
<table><thead><tr><th width="177">Pozycja okna</th><th>Sekwencja wejściowa</th><th>Sekwencja docelowa</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. **Wynikowe tablice wejściowe i docelowe:**
|
||||
|
||||
- **Wejście:**
|
||||
|
||||
```python
|
||||
[
|
||||
["Lorem", "ipsum", "dolor", "sit"],
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
]
|
||||
```
|
||||
|
||||
- **Cel:**
|
||||
|
||||
```python
|
||||
[
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
["amet,", "consectetur", "adipiscing", "elit."],
|
||||
]
|
||||
```
|
||||
|
||||
**Wizualna reprezentacja**
|
||||
|
||||
<table><thead><tr><th width="222">Pozycja tokena</th><th>Token</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>
|
||||
|
||||
**Okno przesuwające z krokiem 1:**
|
||||
|
||||
- **Pierwsze okno (pozycje 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Cel:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Drugie okno (pozycje 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **Cel:** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Trzecie okno (pozycje 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Cel:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Czwarte okno (pozycje 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **Cel:** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Zrozumienie kroku**
|
||||
|
||||
- **Krok 1:** Okno przesuwa się do przodu o jeden token za każdym razem, co skutkuje silnie nakładającymi się sekwencjami. Może to prowadzić do lepszego uczenia się relacji kontekstowych, ale może zwiększyć ryzyko przeuczenia, ponieważ podobne punkty danych są powtarzane.
|
||||
- **Krok 2:** Okno przesuwa się do przodu o dwa tokeny za każdym razem, co zmniejsza nakładanie się. To zmniejsza redundancję i obciążenie obliczeniowe, ale może pominąć niektóre niuanse kontekstowe.
|
||||
- **Krok równy max_length:** Okno przesuwa się do przodu o całą wielkość okna, co skutkuje sekwencjami bez nakładania się. Minimalizuje to redundancję danych, ale może ograniczyć zdolność modelu do uczenia się zależności między sekwencjami.
|
||||
|
||||
**Przykład z krokiem 2:**
|
||||
|
||||
Używając tego samego tokenizowanego tekstu i `max_length` równym 4:
|
||||
|
||||
- **Pierwsze okno (pozycje 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Cel:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Drugie okno (pozycje 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Cel:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Trzecie okno (pozycje 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **Cel:** \["consectetur", "adipiscing", "elit.", "sed"] _(Zakładając kontynuację)_
|
||||
|
||||
## Przykład kodu
|
||||
|
||||
Zrozummy to lepiej na przykładzie kodu z [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]])
|
||||
]
|
||||
```
|
||||
## Odniesienia
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -5,8 +5,8 @@
|
||||
Po tokenizacji danych tekstowych, następnym kluczowym krokiem w przygotowaniu danych do trenowania dużych modeli językowych (LLM) takich jak GPT jest stworzenie **token embeddings**. Token embeddings przekształcają dyskretne tokeny (takie jak słowa lub pod-słowa) w ciągłe wektory numeryczne, które model może przetwarzać i z których może się uczyć. To wyjaśnienie rozkłada token embeddings, ich inicjalizację, zastosowanie oraz rolę osadzeń pozycyjnych w poprawie zrozumienia sekwencji tokenów przez model.
|
||||
|
||||
> [!TIP]
|
||||
> Cel tej trzeciej fazy jest bardzo prosty: **Przypisz każdemu z poprzednich tokenów w słowniku wektor o pożądanych wymiarach, aby wytrenować model.** Każde słowo w słowniku będzie punktem w przestrzeni o X wymiarach.\
|
||||
> Zauważ, że początkowo pozycja każdego słowa w przestrzeni jest po prostu inicjowana "losowo", a te pozycje są parametrami do wytrenowania (będą poprawiane podczas treningu).
|
||||
> Celem tej trzeciej fazy jest bardzo proste: **Przypisz każdemu z poprzednich tokenów w słowniku wektor o pożądanych wymiarach, aby wytrenować model.** Każde słowo w słowniku będzie punktem w przestrzeni o X wymiarach.\
|
||||
> Zauważ, że początkowo pozycja każdego słowa w przestrzeni jest po prostu inicjowana "losowo", a te pozycje są parametrami, które można trenować (będą poprawiane podczas treningu).
|
||||
>
|
||||
> Co więcej, podczas osadzania tokenów **tworzona jest kolejna warstwa osadzeń**, która reprezentuje (w tym przypadku) **absolutną pozycję słowa w zdaniu treningowym**. W ten sposób słowo w różnych pozycjach w zdaniu będzie miało inną reprezentację (znaczenie).
|
||||
|
||||
@ -55,7 +55,7 @@ tensor([[ 0.3374, -0.1778, -0.1690],
|
||||
- Każda kolumna reprezentuje wymiar w wektorze osadzenia.
|
||||
- Na przykład, token o indeksie `3` ma wektor osadzenia `[-0.4015, 0.9666, -1.1481]`.
|
||||
|
||||
**Dostęp do osadzenia tokenu:**
|
||||
**Dostęp do osadzenia tokena:**
|
||||
```python
|
||||
# Retrieve the embedding for the token at index 3
|
||||
token_index = torch.tensor([3])
|
||||
|
@ -2,19 +2,19 @@
|
||||
|
||||
## Mechanizmy Uwag i Samo-Uwaga w Sieciach Neuronowych
|
||||
|
||||
Mechanizmy uwagi pozwalają sieciom neuronowym **skupić się na konkretnych częściach wejścia podczas generowania każdej części wyjścia**. Przypisują różne wagi różnym wejściom, pomagając modelowi zdecydować, które wejścia są najbardziej istotne dla danego zadania. Jest to kluczowe w zadaniach takich jak tłumaczenie maszynowe, gdzie zrozumienie kontekstu całego zdania jest niezbędne do dokładnego tłumaczenia.
|
||||
Mechanizmy uwag pozwalają sieciom neuronowym **skupić się na konkretnych częściach wejścia podczas generowania każdej części wyjścia**. Przypisują różne wagi różnym wejściom, pomagając modelowi zdecydować, które wejścia są najbardziej istotne dla danego zadania. Jest to kluczowe w zadaniach takich jak tłumaczenie maszynowe, gdzie zrozumienie kontekstu całego zdania jest niezbędne do dokładnego tłumaczenia.
|
||||
|
||||
> [!TIP]
|
||||
> Celem tej czwartej fazy jest bardzo proste: **Zastosować pewne mechanizmy uwagi**. Będą to **wielokrotnie powtarzane warstwy**, które **uchwycą relację słowa w słowniku z jego sąsiadami w aktualnym zdaniu używanym do trenowania LLM**.\
|
||||
> Używa się wielu warstw, więc wiele parametrów do uczenia będzie uchwytywać te informacje.
|
||||
> Celem tej czwartej fazy jest bardzo prosty: **Zastosować kilka mechanizmów uwag**. Będą to **powtarzające się warstwy**, które będą **uchwytywać relację słowa w słownictwie z jego sąsiadami w aktualnym zdaniu używanym do trenowania LLM**.\
|
||||
> Używa się do tego wielu warstw, więc wiele parametrów do uczenia będzie uchwytywać te informacje.
|
||||
|
||||
### Zrozumienie Mechanizmów Uwag
|
||||
|
||||
W tradycyjnych modelach sekwencja-do-sekwencji używanych do tłumaczenia języków, model koduje sekwencję wejściową w wektor kontekstowy o stałej wielkości. Jednak podejście to ma trudności z długimi zdaniami, ponieważ wektor kontekstowy o stałej wielkości może nie uchwycić wszystkich niezbędnych informacji. Mechanizmy uwagi rozwiązują to ograniczenie, pozwalając modelowi rozważać wszystkie tokeny wejściowe podczas generowania każdego tokenu wyjściowego.
|
||||
W tradycyjnych modelach sekwencja-do-sekwencji używanych do tłumaczenia języków, model koduje sekwencję wejściową w wektor kontekstowy o stałej wielkości. Jednak podejście to ma trudności z długimi zdaniami, ponieważ wektor kontekstowy o stałej wielkości może nie uchwycić wszystkich niezbędnych informacji. Mechanizmy uwag rozwiązują to ograniczenie, pozwalając modelowi rozważać wszystkie tokeny wejściowe podczas generowania każdego tokenu wyjściowego.
|
||||
|
||||
#### Przykład: Tłumaczenie Maszynowe
|
||||
|
||||
Rozważmy tłumaczenie niemieckiego zdania "Kannst du mir helfen diesen Satz zu übersetzen" na angielski. Tłumaczenie słowo po słowie nie dałoby gramatycznie poprawnego zdania w języku angielskim z powodu różnic w strukturach gramatycznych między językami. Mechanizm uwagi umożliwia modelowi skupienie się na istotnych częściach zdania wejściowego podczas generowania każdego słowa zdania wyjściowego, co prowadzi do dokładniejszego i spójnego tłumaczenia.
|
||||
Rozważmy tłumaczenie niemieckiego zdania "Kannst du mir helfen diesen Satz zu übersetzen" na angielski. Tłumaczenie słowo po słowie nie dałoby gramatycznie poprawnego zdania w języku angielskim z powodu różnic w strukturach gramatycznych między językami. Mechanizm uwag umożliwia modelowi skupienie się na istotnych częściach zdania wejściowego podczas generowania każdego słowa zdania wyjściowego, co prowadzi do dokładniejszego i spójnego tłumaczenia.
|
||||
|
||||
### Wprowadzenie do Samo-Uwagi
|
||||
|
||||
@ -62,28 +62,28 @@ Dla każdego słowa w zdaniu oblicz wynik **uwagi** w odniesieniu do "shiny", ob
|
||||
>
|
||||
> Ponadto, funkcja **softmax** jest używana, ponieważ akcentuje różnice dzięki części wykładniczej, co ułatwia wykrywanie użytecznych wartości.
|
||||
|
||||
Zastosuj funkcję **softmax** do wyników uwagi, aby przekształcić je w wagi uwagi, które sumują się do 1.
|
||||
Zastosuj funkcję **softmax** do wyników uwag, aby przekształcić je w wagi uwag, które sumują się do 1.
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1) (1) (1).png" alt="" width="293"><figcaption></figcaption></figure>
|
||||
|
||||
Obliczanie wykładników:
|
||||
|
||||
<figure><img src="../../images/image (4) (1) (1).png" alt="" width="249"><figcaption></figcaption></figure>
|
||||
<figure><img src="../../images/image (4) (1) (1) (1).png" alt="" width="249"><figcaption></figcaption></figure>
|
||||
|
||||
Obliczanie sumy:
|
||||
|
||||
<figure><img src="../../images/image (5) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
Obliczanie wag uwagi:
|
||||
Obliczanie wag uwag:
|
||||
|
||||
<figure><img src="../../images/image (6) (1) (1).png" alt="" width="404"><figcaption></figcaption></figure>
|
||||
|
||||
#### Krok 3: Obliczanie Wektora Kontekstowego
|
||||
|
||||
> [!TIP]
|
||||
> Po prostu weź każdą wagę uwagi i pomnóż ją przez odpowiednie wymiary tokenu, a następnie zsumuj wszystkie wymiary, aby uzyskać tylko 1 wektor (wektor kontekstowy)
|
||||
> Po prostu weź każdą wagę uwag i pomnóż ją przez odpowiednie wymiary tokenu, a następnie zsumuj wszystkie wymiary, aby uzyskać tylko 1 wektor (wektor kontekstowy)
|
||||
|
||||
**Wektor kontekstowy** oblicza się jako ważoną sumę osadzeń wszystkich słów, używając wag uwagi.
|
||||
**Wektor kontekstowy** jest obliczany jako ważona suma osadzeń wszystkich słów, przy użyciu wag uwag.
|
||||
|
||||
<figure><img src="../../images/image (16).png" alt="" width="369"><figcaption></figcaption></figure>
|
||||
|
||||
@ -110,8 +110,8 @@ Sumując ważone osadzenia:
|
||||
### Podsumowanie Procesu
|
||||
|
||||
1. **Oblicz Wyniki Uwag**: Użyj iloczynu skalarnego między osadzeniem docelowego słowa a osadzeniami wszystkich słów w sekwencji.
|
||||
2. **Normalizuj Wyniki, Aby Uzyskać Wagi Uwag**: Zastosuj funkcję softmax do wyników uwagi, aby uzyskać wagi, które sumują się do 1.
|
||||
3. **Oblicz Wektor Kontekstowy**: Pomnóż osadzenie każdego słowa przez jego wagę uwagi i zsumuj wyniki.
|
||||
2. **Normalizuj Wyniki, Aby Uzyskać Wagi Uwag**: Zastosuj funkcję softmax do wyników uwag, aby uzyskać wagi, które sumują się do 1.
|
||||
3. **Oblicz Wektor Kontekstowy**: Pomnóż osadzenie każdego słowa przez jego wagę uwag i zsumuj wyniki.
|
||||
|
||||
## Samo-Uwaga z Uczonymi Wagami
|
||||
|
||||
@ -153,15 +153,15 @@ queries = torch.matmul(inputs, W_query)
|
||||
keys = torch.matmul(inputs, W_key)
|
||||
values = torch.matmul(inputs, W_value)
|
||||
```
|
||||
#### Krok 2: Obliczanie uwagi z wykorzystaniem skalowanego iloczynu skalarnego
|
||||
#### Krok 2: Oblicz Skalowaną Uwagę Dot-Product
|
||||
|
||||
**Obliczanie wyników uwagi**
|
||||
**Oblicz Wyniki Uwag**
|
||||
|
||||
Podobnie jak w poprzednim przykładzie, ale tym razem, zamiast używać wartości wymiarów tokenów, używamy macierzy kluczy tokenu (już obliczonej przy użyciu wymiarów):. Tak więc, dla każdego zapytania `qi` i klucza `kj`:
|
||||
|
||||
<figure><img src="../../images/image (12).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**Skalowanie wyników**
|
||||
**Skaluj Wyniki**
|
||||
|
||||
Aby zapobiec zbyt dużym iloczynom skalarnym, skaluj je przez pierwiastek kwadratowy z wymiaru klucza `dk`:
|
||||
|
||||
@ -170,19 +170,19 @@ Aby zapobiec zbyt dużym iloczynom skalarnym, skaluj je przez pierwiastek kwadra
|
||||
> [!TIP]
|
||||
> Wynik jest dzielony przez pierwiastek kwadratowy z wymiarów, ponieważ iloczyny skalarne mogą stać się bardzo duże, a to pomaga je regulować.
|
||||
|
||||
**Zastosuj Softmax, aby uzyskać wagi uwagi:** Jak w początkowym przykładzie, znormalizuj wszystkie wartości, aby ich suma wynosiła 1.
|
||||
**Zastosuj Softmax, aby Uzyskać Wagi Uwag:** Jak w początkowym przykładzie, znormalizuj wszystkie wartości, aby ich suma wynosiła 1.
|
||||
|
||||
<figure><img src="../../images/image (14).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
#### Krok 3: Obliczanie wektorów kontekstu
|
||||
#### Krok 3: Oblicz Wektory Kontekstowe
|
||||
|
||||
Jak w początkowym przykładzie, po prostu zsumuj wszystkie macierze wartości, mnożąc każdą z nich przez jej wagę uwagi:
|
||||
|
||||
<figure><img src="../../images/image (15).png" alt="" width="328"><figcaption></figcaption></figure>
|
||||
|
||||
### Przykład kodu
|
||||
### Przykład Kodu
|
||||
|
||||
Biorąc przykład z [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb), możesz sprawdzić tę klasę, która implementuje funkcjonalność samouważności, o której rozmawialiśmy:
|
||||
Biorąc przykład z [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb), możesz sprawdzić tę klasę, która implementuje funkcjonalność samouwag, o której rozmawialiśmy:
|
||||
```python
|
||||
import torch
|
||||
|
||||
@ -255,7 +255,7 @@ Aby **zapobiec przeuczeniu**, możemy zastosować **dropout** do wag uwagi po op
|
||||
dropout = nn.Dropout(p=0.5)
|
||||
attention_weights = dropout(attention_weights)
|
||||
```
|
||||
Zwykła wartość dropout wynosi około 10-20%.
|
||||
Zwykła utrata to około 10-20%.
|
||||
|
||||
### Code Example
|
||||
|
||||
@ -327,7 +327,7 @@ print("context_vecs.shape:", context_vecs.shape)
|
||||
|
||||
### Przykład kodu
|
||||
|
||||
Możliwe byłoby ponowne wykorzystanie poprzedniego kodu i dodanie opakowania, które uruchamia go kilka razy, ale to jest bardziej zoptymalizowana wersja z [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb), która przetwarza wszystkie głowy jednocześnie (zmniejszając liczbę kosztownych pętli for). Jak widać w kodzie, wymiary każdego tokena są dzielone na różne wymiary w zależności od liczby głów. W ten sposób, jeśli token ma 8 wymiarów, a chcemy użyć 3 głów, wymiary będą podzielone na 2 tablice po 4 wymiary, a każda głowa użyje jednej z nich:
|
||||
Możliwe byłoby ponowne wykorzystanie poprzedniego kodu i dodanie opakowania, które uruchamia go kilka razy, ale to jest bardziej zoptymalizowana wersja z [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb), która przetwarza wszystkie głowy jednocześnie (zmniejszając liczbę kosztownych pętli for). Jak widać w kodzie, wymiary każdego tokena są dzielone na różne wymiary w zależności od liczby głów. W ten sposób, jeśli token ma 8 wymiarów i chcemy użyć 3 głów, wymiary będą podzielone na 2 tablice po 4 wymiary, a każda głowa użyje jednej z nich:
|
||||
```python
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
|
@ -14,7 +14,7 @@ Wysokopoziomowa reprezentacja może być obserwowana w:
|
||||
<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. **Wejście (Tokenizowany tekst)**: Proces zaczyna się od tokenizowanego tekstu, który jest przekształcany w reprezentacje numeryczne.
|
||||
2. **Warstwa osadzania tokenów i warstwa osadzania pozycji**: Tokenizowany tekst przechodzi przez warstwę **osadzania tokenów** i warstwę **osadzania pozycji**, która uchwyca pozycję tokenów w sekwencji, co jest kluczowe dla zrozumienia kolejności słów.
|
||||
2. **Warstwa osadzenia tokenów i warstwa osadzenia pozycyjnego**: Tokenizowany tekst przechodzi przez warstwę **osadzenia tokenów** i warstwę **osadzenia pozycyjnego**, które uchwycają pozycję tokenów w sekwencji, co jest kluczowe dla zrozumienia kolejności słów.
|
||||
3. **Bloki transformatorowe**: Model zawiera **12 bloków transformatorowych**, z których każdy ma wiele warstw. Te bloki powtarzają następującą sekwencję:
|
||||
- **Maskowana wielogłowa uwaga**: Pozwala modelowi skupić się na różnych częściach tekstu wejściowego jednocześnie.
|
||||
- **Normalizacja warstwy**: Krok normalizacji w celu stabilizacji i poprawy treningu.
|
||||
@ -261,7 +261,7 @@ To zostało już wyjaśnione w wcześniejszej sekcji.
|
||||
- **Wielogłowa uwaga własna:** Pozwala modelowi skupić się na różnych pozycjach w sekwencji wejściowej podczas kodowania tokena.
|
||||
- **Kluczowe komponenty:**
|
||||
- **Zapytania, klucze, wartości:** Liniowe projekcje wejścia, używane do obliczania wyników uwagi.
|
||||
- **Głowy:** Wiele mechanizmów uwagi działających równolegle (`num_heads`), z każdą o zredukowanej wymiarowości (`head_dim`).
|
||||
- **Głowy:** Wiele mechanizmów uwagi działających równolegle (`num_heads`), każdy z zredukowaną wymiarowością (`head_dim`).
|
||||
- **Wyniki uwagi:** Obliczane jako iloczyn skalarny zapytań i kluczy, skalowane i maskowane.
|
||||
- **Maskowanie:** Zastosowana jest maska przyczynowa, aby zapobiec modelowi uwagi na przyszłe tokeny (ważne dla modeli autoregresywnych, takich jak GPT).
|
||||
- **Wagi uwagi:** Softmax z maskowanych i skalowanych wyników uwagi.
|
||||
@ -269,9 +269,9 @@ To zostało już wyjaśnione w wcześniejszej sekcji.
|
||||
- **Projekcja wyjściowa:** Warstwa liniowa do połączenia wyjść wszystkich głów.
|
||||
|
||||
> [!TIP]
|
||||
> Celem tej sieci jest znalezienie relacji między tokenami w tym samym kontekście. Co więcej, tokeny są dzielone na różne głowy, aby zapobiec nadmiernemu dopasowaniu, chociaż ostateczne relacje znalezione na głowę są łączone na końcu tej sieci.
|
||||
> Celem tej sieci jest znalezienie relacji między tokenami w tym samym kontekście. Ponadto tokeny są dzielone na różne głowy, aby zapobiec nadmiernemu dopasowaniu, chociaż ostateczne relacje znalezione na każdej głowie są łączone na końcu tej sieci.
|
||||
>
|
||||
> Co więcej, podczas treningu stosowana jest **maska przyczynowa**, aby późniejsze tokeny nie były brane pod uwagę przy poszukiwaniu specyficznych relacji do tokena, a także stosowany jest **dropout**, aby **zapobiec nadmiernemu dopasowaniu**.
|
||||
> Ponadto, podczas treningu stosowana jest **maska przyczynowa**, aby późniejsze tokeny nie były brane pod uwagę przy poszukiwaniu specyficznych relacji do tokena, a także stosowany jest **dropout**, aby **zapobiec nadmiernemu dopasowaniu**.
|
||||
|
||||
### **Normalizacja** warstwy
|
||||
```python
|
||||
@ -289,20 +289,20 @@ 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
|
||||
```
|
||||
#### **Cel i funkcjonalność**
|
||||
#### **Cel i Funkcjonalność**
|
||||
|
||||
- **Normalizacja warstw:** Technika używana do normalizacji wejść wzdłuż cech (wymiarów osadzenia) dla każdego pojedynczego przykładu w partii.
|
||||
- **Normalizacja Warstw:** Technika używana do normalizacji wejść wzdłuż cech (wymiarów osadzenia) dla każdego pojedynczego przykładu w partii.
|
||||
- **Składniki:**
|
||||
- **`eps`:** Mała stała (`1e-5`) dodawana do wariancji, aby zapobiec dzieleniu przez zero podczas normalizacji.
|
||||
- **`scale` i `shift`:** Uczące się parametry (`nn.Parameter`), które pozwalają modelowi na skalowanie i przesuwanie znormalizowanego wyjścia. Są inicjowane odpowiednio do jedynek i zer.
|
||||
- **Proces normalizacji:**
|
||||
- **Obliczanie średniej (`mean`):** Oblicza średnią z wejścia `x` wzdłuż wymiaru osadzenia (`dim=-1`), zachowując wymiar do rozprzestrzeniania (`keepdim=True`).
|
||||
- **Obliczanie wariancji (`var`):** Oblicza wariancję `x` wzdłuż wymiaru osadzenia, również zachowując wymiar. Parametr `unbiased=False` zapewnia, że wariancja jest obliczana przy użyciu obciążonego estymatora (dzieląc przez `N` zamiast `N-1`), co jest odpowiednie przy normalizacji wzdłuż cech, a nie próbek.
|
||||
- **Proces Normalizacji:**
|
||||
- **Obliczanie Średniej (`mean`):** Oblicza średnią z wejścia `x` wzdłuż wymiaru osadzenia (`dim=-1`), zachowując wymiar do rozprzestrzeniania (`keepdim=True`).
|
||||
- **Obliczanie Wariancji (`var`):** Oblicza wariancję `x` wzdłuż wymiaru osadzenia, również zachowując wymiar. Parametr `unbiased=False` zapewnia, że wariancja jest obliczana przy użyciu obciążonego estymatora (dzieląc przez `N` zamiast `N-1`), co jest odpowiednie przy normalizacji wzdłuż cech, a nie próbek.
|
||||
- **Normalizacja (`norm_x`):** Odejmuje średnią od `x` i dzieli przez pierwiastek kwadratowy z wariancji plus `eps`.
|
||||
- **Skalowanie i przesunięcie:** Zastosowuje uczące się parametry `scale` i `shift` do znormalizowanego wyjścia.
|
||||
- **Skalowanie i Przesunięcie:** Zastosowuje uczące się parametry `scale` i `shift` do znormalizowanego wyjścia.
|
||||
|
||||
> [!TIP]
|
||||
> Celem jest zapewnienie średniej równej 0 z wariancją równą 1 we wszystkich wymiarach tego samego tokena. Celem tego jest **stabilizacja treningu głębokich sieci neuronowych** poprzez redukcję wewnętrznego przesunięcia kowariancji, które odnosi się do zmiany w rozkładzie aktywacji sieci z powodu aktualizacji parametrów podczas treningu.
|
||||
> Celem jest zapewnienie średniej 0 z wariancją 1 we wszystkich wymiarach tego samego tokena. Celem tego jest **stabilizacja treningu głębokich sieci neuronowych** poprzez redukcję wewnętrznego przesunięcia kowariancji, które odnosi się do zmiany w rozkładzie aktywacji sieci z powodu aktualizacji parametrów podczas treningu.
|
||||
|
||||
### **Blok Transformera**
|
||||
|
||||
@ -348,7 +348,7 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
```
|
||||
#### **Cel i Funkcjonalność**
|
||||
|
||||
- **Kompozycja Warstw:** Łączy wielogłowe uwagi, sieć feedforward, normalizację warstw i połączenia resztkowe.
|
||||
- **Kompozycja Warstw:** Łączy wielogłowową uwagę, sieć feedforward, normalizację warstw i połączenia resztkowe.
|
||||
- **Normalizacja Warstw:** Stosowana przed warstwami uwagi i feedforward dla stabilnego treningu.
|
||||
- **Połączenia Resztkowe (Skróty):** Dodają wejście warstwy do jej wyjścia, aby poprawić przepływ gradientu i umożliwić trening głębokich sieci.
|
||||
- **Dropout:** Stosowany po warstwach uwagi i feedforward w celu regularyzacji.
|
||||
@ -358,7 +358,7 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
1. **Pierwsza Ścieżka Resztkowa (Self-Attention):**
|
||||
- **Wejście (`shortcut`):** Zapisz oryginalne wejście dla połączenia resztkowego.
|
||||
- **Normalizacja Warstw (`norm1`):** Normalizuj wejście.
|
||||
- **Wielogłowa Uwaga (`att`):** Zastosuj self-attention.
|
||||
- **Wielogłowowa Uwaga (`att`):** Zastosuj self-attention.
|
||||
- **Dropout (`drop_shortcut`):** Zastosuj dropout w celu regularyzacji.
|
||||
- **Dodaj Resztkę (`x + shortcut`):** Połącz z oryginalnym wejściem.
|
||||
2. **Druga Ścieżka Resztkowa (FeedForward):**
|
||||
@ -372,7 +372,7 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
> Blok transformatora grupuje wszystkie sieci razem i stosuje pewne **normalizacje** i **dropouty** w celu poprawy stabilności treningu i wyników.\
|
||||
> Zauważ, że dropouty są stosowane po użyciu każdej sieci, podczas gdy normalizacja jest stosowana przed.
|
||||
>
|
||||
> Ponadto, używa również skrótów, które polegają na **dodawaniu wyjścia sieci do jej wejścia**. Pomaga to zapobiegać problemowi znikającego gradientu, zapewniając, że początkowe warstwy przyczyniają się "tak samo" jak ostatnie.
|
||||
> Ponadto, wykorzystuje również skróty, które polegają na **dodawaniu wyjścia sieci do jej wejścia**. Pomaga to zapobiegać problemowi znikającego gradientu, zapewniając, że początkowe warstwy przyczyniają się "tak samo" jak ostatnie.
|
||||
|
||||
### **GPTModel**
|
||||
|
||||
@ -444,7 +444,7 @@ return logits # Output shape: (batch_size, seq_len, vocab_size)
|
||||
> [!TIP]
|
||||
> Celem tej klasy jest wykorzystanie wszystkich innych wspomnianych sieci do **przewidywania następnego tokena w sekwencji**, co jest fundamentalne dla zadań takich jak generowanie tekstu.
|
||||
>
|
||||
> Zauważ, jak **użyje tylu bloków transformatorowych, ile wskazano** i że każdy blok transformatorowy korzysta z jednej sieci z wieloma głowicami uwagi, jednej sieci feed forward i kilku normalizacji. Więc jeśli użyto 12 bloków transformatorowych, pomnóż to przez 12.
|
||||
> Zauważ, jak **użyje tylu bloków transformatorowych, ile wskazano** i że każdy blok transformatorowy wykorzystuje jedną sieć z wieloma głowicami uwagi, jedną sieć feed forward i kilka normalizacji. Więc jeśli użyto 12 bloków transformatorowych, pomnóż to przez 12.
|
||||
>
|
||||
> Ponadto, warstwa **normalizacji** jest dodawana **przed** **wyjściem** i na końcu stosowana jest ostateczna warstwa liniowa, aby uzyskać wyniki o odpowiednich wymiarach. Zauważ, że każdy ostateczny wektor ma rozmiar używanego słownika. Dzieje się tak, ponieważ próbuje uzyskać prawdopodobieństwo dla każdego możliwego tokena w słowniku.
|
||||
|
||||
@ -481,7 +481,7 @@ token_embedding_params = 50257 * 768 = 38,597,376
|
||||
```python
|
||||
position_embedding_params = 1024 * 768 = 786,432
|
||||
```
|
||||
**Całkowita liczba parametrów osadzenia**
|
||||
**Całkowite parametry osadzenia**
|
||||
```python
|
||||
embedding_params = token_embedding_params + position_embedding_params
|
||||
embedding_params = 38,597,376 + 786,432 = 39,383,808
|
||||
@ -558,7 +558,7 @@ ff_params = 2,362,368 + 2,360,064 = 4,722,432
|
||||
|
||||
- **Składniki:**
|
||||
- Dwa wystąpienia `LayerNorm` na blok.
|
||||
- Każdy `LayerNorm` ma `2 * emb_dim` parametrów (skala i przesunięcie).
|
||||
- Każde `LayerNorm` ma `2 * emb_dim` parametrów (skala i przesunięcie).
|
||||
- **Obliczenia:**
|
||||
|
||||
```python
|
||||
@ -608,7 +608,7 @@ total_params = 163,009,536
|
||||
```
|
||||
## Generowanie tekstu
|
||||
|
||||
Mając model, który przewiduje następny token jak poprzedni, wystarczy wziąć wartości ostatniego tokena z wyjścia (ponieważ będą to wartości przewidywanego tokena), które będą **wartością na wpis w słowniku**, a następnie użyć funkcji `softmax`, aby znormalizować wymiary do prawdopodobieństw, które sumują się do 1, a następnie uzyskać indeks największego wpisu, który będzie indeksem słowa w słowniku.
|
||||
Mając model, który przewiduje następny token, jak ten przedtem, wystarczy wziąć wartości ostatniego tokena z wyjścia (ponieważ będą to wartości przewidywanego tokena), które będą **wartością na wpis w słowniku**, a następnie użyć funkcji `softmax`, aby znormalizować wymiary do prawdopodobieństw, które sumują się do 1, a następnie uzyskać indeks największego wpisu, który będzie indeksem słowa w słowniku.
|
||||
|
||||
Kod z [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
|
||||
|
941
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
941
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
@ -0,0 +1,941 @@
|
||||
# 6. Pre-training & Loading models
|
||||
|
||||
## Generacja tekstu
|
||||
|
||||
Aby wytrenować model, musimy, aby ten model był w stanie generować nowe tokeny. Następnie porównamy wygenerowane tokeny z oczekiwanymi, aby wytrenować model w **uczeniu się tokenów, które musi generować**.
|
||||
|
||||
Jak w poprzednich przykładach, już przewidzieliśmy niektóre tokeny, możliwe jest ponowne wykorzystanie tej funkcji w tym celu.
|
||||
|
||||
> [!TIP]
|
||||
> Celem tej szóstej fazy jest bardzo proste: **Wytrenuj model od podstaw**. W tym celu zostanie wykorzystana poprzednia architektura LLM z pewnymi pętlami przechodzącymi przez zbiory danych, używając zdefiniowanych funkcji straty i optymalizatora do wytrenowania wszystkich parametrów modelu.
|
||||
|
||||
## Ocena tekstu
|
||||
|
||||
Aby przeprowadzić poprawne szkolenie, konieczne jest zmierzenie przewidywań uzyskanych dla oczekiwanego tokena. Celem szkolenia jest maksymalizacja prawdopodobieństwa poprawnego tokena, co wiąże się ze zwiększeniem jego prawdopodobieństwa w stosunku do innych tokenów.
|
||||
|
||||
Aby zmaksymalizować prawdopodobieństwo poprawnego tokena, wagi modelu muszą być modyfikowane, aby to prawdopodobieństwo było maksymalizowane. Aktualizacje wag są dokonywane za pomocą **backpropagation**. Wymaga to **funkcji straty do maksymalizacji**. W tym przypadku funkcją będzie **różnica między wykonaną prognozą a pożądaną**.
|
||||
|
||||
Jednak zamiast pracować z surowymi prognozami, będzie pracować z logarytmem o podstawie n. Jeśli więc aktualna prognoza oczekiwanego tokena wynosiła 7.4541e-05, logarytm naturalny (podstawa *e*) z **7.4541e-05** wynosi około **-9.5042**.\
|
||||
Następnie, dla każdego wpisu z długością kontekstu 5 tokenów, model będzie musiał przewidzieć 5 tokenów, przy czym pierwsze 4 tokeny to ostatni token wejściowy, a piąty to przewidywany. Dlatego dla każdego wpisu będziemy mieli 5 prognoz w tym przypadku (nawet jeśli pierwsze 4 były w wejściu, model o tym nie wie) z 5 oczekiwanymi tokenami i w związku z tym 5 prawdopodobieństw do maksymalizacji.
|
||||
|
||||
Dlatego po wykonaniu logarytmu naturalnego dla każdej prognozy, obliczana jest **średnia**, **symbol minus usuwany** (nazywa się to _cross entropy loss_) i to jest **liczba, którą należy zredukować jak najbliżej 0** ponieważ logarytm naturalny z 1 to 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>
|
||||
|
||||
Innym sposobem na zmierzenie, jak dobry jest model, jest tzw. perplexity. **Perplexity** to metryka używana do oceny, jak dobrze model probabilistyczny przewiduje próbkę. W modelowaniu języka reprezentuje **niepewność modelu** przy przewidywaniu następnego tokena w sekwencji.\
|
||||
Na przykład, wartość perplexity wynosząca 48725 oznacza, że gdy trzeba przewidzieć token, model nie jest pewny, który z 48,725 tokenów w słowniku jest właściwy.
|
||||
|
||||
## Przykład wstępnego treningu
|
||||
|
||||
To jest początkowy kod zaproponowany w [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) czasami nieco zmodyfikowany
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Poprzedni kod użyty tutaj, ale już wyjaśniony w poprzednich sekcjach</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"
|
||||
)
|
||||
```
|
||||
### Funkcje do transformacji tekstu <--> id
|
||||
|
||||
To są proste funkcje, które można wykorzystać do przekształcania tekstów ze słownika na id i odwrotnie. Jest to potrzebne na początku przetwarzania tekstu oraz na końcu prognoz:
|
||||
```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())
|
||||
```
|
||||
### Funkcje generowania tekstu
|
||||
|
||||
W poprzedniej sekcji funkcja, która po prostu uzyskiwała **najbardziej prawdopodobny token** po uzyskaniu logitów. Jednak oznacza to, że dla każdego wpisu zawsze będzie generowane to samo wyjście, co czyni to bardzo deterministycznym.
|
||||
|
||||
Poniższa funkcja `generate_text` zastosuje koncepcje `top-k`, `temperature` i `multinomial`.
|
||||
|
||||
- **`top-k`** oznacza, że zaczniemy redukować do `-inf` wszystkie prawdopodobieństwa wszystkich tokenów, z wyjątkiem najlepszych k tokenów. Więc, jeśli k=3, przed podjęciem decyzji tylko 3 najbardziej prawdopodobne tokeny będą miały prawdopodobieństwo różne od `-inf`.
|
||||
- **`temperature`** oznacza, że każde prawdopodobieństwo będzie dzielone przez wartość temperatury. Wartość `0.1` poprawi najwyższe prawdopodobieństwo w porównaniu do najniższego, podczas gdy temperatura `5`, na przykład, uczyni je bardziej płaskimi. To pomaga poprawić zmienność w odpowiedziach, którą chcielibyśmy, aby LLM miało.
|
||||
- Po zastosowaniu temperatury, funkcja **`softmax`** jest ponownie stosowana, aby wszystkie pozostałe tokeny miały łączne prawdopodobieństwo równe 1.
|
||||
- Na koniec, zamiast wybierać token o największym prawdopodobieństwie, funkcja **`multinomial`** jest stosowana do **przewidywania następnego tokenu zgodnie z ostatecznymi prawdopodobieństwami**. Więc jeśli token 1 miał 70% prawdopodobieństwa, token 2 20% a token 3 10%, 70% razy zostanie wybrany token 1, 20% razy będzie to token 2, a 10% razy będzie to token 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]
|
||||
> Istnieje powszechna alternatywa dla `top-k` zwana [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), znana również jako próbkowanie jądra, która zamiast pobierać k próbek o największym prawdopodobieństwie, **organizuje** całe powstałe **słownictwo** według prawdopodobieństw i **sumuje** je od najwyższego prawdopodobieństwa do najniższego, aż do **osiągnięcia progu**.
|
||||
>
|
||||
> Następnie, **tylko te słowa** ze słownictwa będą brane pod uwagę zgodnie z ich względnymi prawdopodobieństwami.
|
||||
>
|
||||
> To pozwala na niekonieczność wyboru liczby `k` próbek, ponieważ optymalne k może być różne w każdym przypadku, a jedynie **próg**.
|
||||
>
|
||||
> _Zauważ, że ta poprawa nie jest uwzględniona w poprzednim kodzie._
|
||||
|
||||
> [!TIP]
|
||||
> Innym sposobem na poprawę generowanego tekstu jest użycie **Beam search** zamiast zachłannego wyszukiwania użytego w tym przykładzie.\
|
||||
> W przeciwieństwie do zachłannego wyszukiwania, które wybiera najbardziej prawdopodobne następne słowo na każdym kroku i buduje pojedynczą sekwencję, **beam search śledzi najlepsze 𝑘 k najwyżej oceniane częściowe sekwencje** (nazywane "beams") na każdym kroku. Poprzez jednoczesne badanie wielu możliwości, równoważy efektywność i jakość, zwiększając szanse na **znalezienie lepszej ogólnej** sekwencji, która mogłaby zostać przeoczona przez podejście zachłanne z powodu wczesnych, suboptymalnych wyborów.
|
||||
>
|
||||
> _Zauważ, że ta poprawa nie jest uwzględniona w poprzednim kodzie._
|
||||
|
||||
### Funkcje strat
|
||||
|
||||
Funkcja **`calc_loss_batch`** oblicza entropię krzyżową dla prognozy pojedynczej partii.\
|
||||
Funkcja **`calc_loss_loader`** uzyskuje entropię krzyżową dla wszystkich partii i oblicza **średnią entropię krzyżową**.
|
||||
```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]
|
||||
> **Gradient clipping** to technika używana do zwiększenia **stabilności treningu** w dużych sieciach neuronowych poprzez ustawienie **maksymalnego progu** dla wielkości gradientów. Gdy gradienty przekraczają ten zdefiniowany `max_norm`, są proporcjonalnie skalowane w dół, aby zapewnić, że aktualizacje parametrów modelu pozostają w zarządzalnym zakresie, zapobiegając problemom takim jak eksplodujące gradienty i zapewniając bardziej kontrolowany i stabilny trening.
|
||||
>
|
||||
> _Zauważ, że ta poprawa nie jest uwzględniona w poprzednim kodzie._
|
||||
>
|
||||
> Sprawdź poniższy przykład:
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Ładowanie danych
|
||||
|
||||
Funkcje `create_dataloader_v1` i `create_dataloader_v1` były już omawiane w poprzedniej sekcji.
|
||||
|
||||
Zauważ, że zdefiniowano, że 90% tekstu będzie używane do treningu, podczas gdy 10% będzie używane do walidacji, a oba zestawy są przechowywane w 2 różnych loaderach danych.\
|
||||
Zauważ, że czasami część zestawu danych jest również pozostawiana na zestaw testowy, aby lepiej ocenić wydajność modelu.
|
||||
|
||||
Oba loadery danych używają tego samego rozmiaru partii, maksymalnej długości, kroku i liczby pracowników (0 w tym przypadku).\
|
||||
Główne różnice dotyczą danych używanych przez każdy z nich, a walidatorzy nie usuwają ostatniego ani nie tasują danych, ponieważ nie jest to potrzebne do celów walidacji.
|
||||
|
||||
Również fakt, że **krok jest tak duży jak długość kontekstu**, oznacza, że nie będzie nakładania się kontekstów używanych do trenowania danych (zmniejsza nadmierne dopasowanie, ale także zestaw danych treningowych).
|
||||
|
||||
Ponadto zauważ, że rozmiar partii w tym przypadku wynosi 2, aby podzielić dane na 2 partie, a głównym celem tego jest umożliwienie równoległego przetwarzania i zmniejszenie zużycia na partię.
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Sanity Checks
|
||||
|
||||
Celem jest sprawdzenie, czy jest wystarczająco dużo tokenów do treningu, czy kształty są zgodne z oczekiwaniami oraz uzyskanie informacji o liczbie tokenów użytych do treningu i walidacji:
|
||||
```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)
|
||||
```
|
||||
### Wybierz urządzenie do treningu i wstępnych obliczeń
|
||||
|
||||
Następujący kod po prostu wybiera urządzenie do użycia i oblicza stratę treningową oraz stratę walidacyjną (bez wcześniejszego trenowania czegokolwiek) jako punkt wyjścia.
|
||||
```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)
|
||||
```
|
||||
### Funkcje treningowe
|
||||
|
||||
Funkcja `generate_and_print_sample` po prostu pobiera kontekst i generuje kilka tokenów, aby uzyskać poczucie, jak dobrze model działa w danym momencie. Jest wywoływana przez `train_model_simple` na każdym kroku.
|
||||
|
||||
Funkcja `evaluate_model` jest wywoływana tak często, jak wskazuje funkcja treningowa i służy do pomiaru straty treningowej oraz straty walidacyjnej w danym momencie treningu modelu.
|
||||
|
||||
Następnie duża funkcja `train_model_simple` to ta, która faktycznie trenuje model. Oczekuje:
|
||||
|
||||
- Ładowarki danych treningowych (z danymi już podzielonymi i przygotowanymi do treningu)
|
||||
- Ładowarki walidacyjnej
|
||||
- **optymalizatora** do użycia podczas treningu: To jest funkcja, która wykorzysta gradienty i zaktualizuje parametry, aby zredukować stratę. W tym przypadku, jak zobaczysz, używany jest `AdamW`, ale jest ich znacznie więcej.
|
||||
- `optimizer.zero_grad()` jest wywoływane, aby zresetować gradienty w każdej rundzie, aby ich nie akumulować.
|
||||
- Parametr **`lr`** to **współczynnik uczenia**, który określa **rozmiar kroków** podejmowanych podczas procesu optymalizacji przy aktualizacji parametrów modelu. **Mniejszy** współczynnik uczenia oznacza, że optymalizator **dokona mniejszych aktualizacji** wag, co może prowadzić do bardziej **precyzyjnej** konwergencji, ale może **spowolnić** trening. **Większy** współczynnik uczenia może przyspieszyć trening, ale **ryzykuje przeskoczenie** minimum funkcji straty (**przeskoczenie** punktu, w którym funkcja straty jest zminimalizowana).
|
||||
- **Weight Decay** modyfikuje krok **obliczania straty**, dodając dodatkowy składnik, który penalizuje duże wagi. To zachęca optymalizator do znajdowania rozwiązań z mniejszymi wagami, równoważąc między dobrym dopasowaniem do danych a utrzymywaniem modelu prostym, zapobiegając nadmiernemu dopasowaniu w modelach uczenia maszynowego poprzez zniechęcanie modelu do przypisywania zbyt dużej wagi jakiejkolwiek pojedynczej cechy.
|
||||
- Tradycyjne optymalizatory, takie jak SGD z regularyzacją L2, łączą weight decay z gradientem funkcji straty. Jednak **AdamW** (wariant optymalizatora Adam) oddziela weight decay od aktualizacji gradientu, co prowadzi do bardziej efektywnej regularyzacji.
|
||||
- Urządzenie do użycia do treningu
|
||||
- Liczba epok: Liczba razy, kiedy przechodzi się przez dane treningowe
|
||||
- Częstotliwość oceny: Częstotliwość wywoływania `evaluate_model`
|
||||
- Iteracja oceny: Liczba partii do użycia podczas oceny aktualnego stanu modelu przy wywoływaniu `generate_and_print_sample`
|
||||
- Kontekst początkowy: Które zdanie początkowe użyć przy wywoływaniu `generate_and_print_sample`
|
||||
- Tokenizer
|
||||
```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]
|
||||
> Aby poprawić współczynnik uczenia, istnieje kilka odpowiednich technik zwanych **linear warmup** i **cosine decay.**
|
||||
>
|
||||
> **Linear warmup** polega na zdefiniowaniu początkowego współczynnika uczenia i maksymalnego oraz konsekwentnym aktualizowaniu go po każdej epoce. Dzieje się tak, ponieważ rozpoczęcie treningu od mniejszych aktualizacji wag zmniejsza ryzyko, że model napotka duże, destabilizujące aktualizacje podczas fazy treningowej.\
|
||||
> **Cosine decay** to technika, która **stopniowo zmniejsza współczynnik uczenia** zgodnie z krzywą półcosinusoidalną **po fazie warmup**, spowalniając aktualizacje wag, aby **zminimalizować ryzyko przeskoczenia** minimów straty i zapewnić stabilność treningu w późniejszych fazach.
|
||||
>
|
||||
> _Zauważ, że te ulepszenia nie są uwzględnione w poprzednim kodzie._
|
||||
|
||||
### Start training
|
||||
```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.")
|
||||
```
|
||||
### Drukowanie ewolucji treningu
|
||||
|
||||
Dzięki poniższej funkcji możliwe jest drukowanie ewolucji modelu podczas jego treningu.
|
||||
```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)
|
||||
```
|
||||
### Zapisz model
|
||||
|
||||
Możliwe jest zapisanie modelu + optymalizatora, jeśli chcesz kontynuować trening później:
|
||||
```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
|
||||
```
|
||||
Lub po prostu model, jeśli planujesz go tylko używać:
|
||||
```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
|
||||
```
|
||||
## Ładowanie wag GPT2
|
||||
|
||||
Są 2 szybkie skrypty do lokalnego ładowania wag GPT2. W obu przypadkach możesz sklonować repozytorium [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) lokalnie, a następnie:
|
||||
|
||||
- Skrypt [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) pobierze wszystkie wagi i przekształci formaty z OpenAI na te oczekiwane przez nasz LLM. Skrypt jest również przygotowany z potrzebną konfiguracją i z podpowiedzią: "Every effort moves you"
|
||||
- Skrypt [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) pozwala na lokalne ładowanie dowolnych wag GPT2 (po prostu zmień zmienną `CHOOSE_MODEL`) i przewidywanie tekstu z niektórych podpowiedzi.
|
||||
|
||||
## Odniesienia
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -7,15 +7,15 @@
|
||||
|
||||
LoRA umożliwia efektywne dostrajanie **dużych modeli** poprzez zmianę tylko **małej części** modelu. Zmniejsza liczbę parametrów, które musisz wytrenować, oszczędzając **pamięć** i **zasoby obliczeniowe**. Dzieje się tak, ponieważ:
|
||||
|
||||
1. **Zmniejsza liczbę parametrów do wytrenowania**: Zamiast aktualizować całą macierz wag w modelu, LoRA **dzieli** macierz wag na dwie mniejsze macierze (nazywane **A** i **B**). To sprawia, że trening jest **szybszy** i wymaga **mniej pamięci**, ponieważ mniej parametrów musi być aktualizowanych.
|
||||
1. **Zmniejsza liczbę parametrów do wytrenowania**: Zamiast aktualizować całą macierz wag w modelu, LoRA **dzieli** macierz wag na dwie mniejsze macierze (nazywane **A** i **B**). To przyspiesza trening i wymaga **mniej pamięci**, ponieważ mniej parametrów musi być aktualizowanych.
|
||||
|
||||
1. Dzieje się tak, ponieważ zamiast obliczać pełną aktualizację wag warstwy (macierzy), przybliża ją do iloczynu 2 mniejszych macierzy, co zmniejsza aktualizację do obliczenia:\
|
||||
1. Dzieje się tak, ponieważ zamiast obliczać pełną aktualizację wag warstwy (macierzy), przybliża ją do iloczynu 2 mniejszych macierzy, co redukuje aktualizację do obliczenia:\
|
||||
|
||||
<figure><img src="../../images/image (9) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
2. **Zachowuje oryginalne wagi modelu bez zmian**: LoRA pozwala na zachowanie oryginalnych wag modelu, a jedynie aktualizuje **nowe małe macierze** (A i B). Jest to pomocne, ponieważ oznacza, że oryginalna wiedza modelu jest zachowana, a ty tylko dostosowujesz to, co konieczne.
|
||||
2. **Zachowuje oryginalne wagi modelu bez zmian**: LoRA pozwala na zachowanie oryginalnych wag modelu, a jedynie aktualizuje **nowe małe macierze** (A i B). To jest pomocne, ponieważ oznacza, że oryginalna wiedza modelu jest zachowana, a ty tylko dostosowujesz to, co konieczne.
|
||||
3. **Efektywne dostrajanie specyficzne dla zadania**: Kiedy chcesz dostosować model do **nowego zadania**, możesz po prostu trenować **małe macierze LoRA** (A i B), pozostawiając resztę modelu w niezmienionej formie. To jest **znacznie bardziej efektywne** niż ponowne trenowanie całego modelu.
|
||||
4. **Efektywność przechowywania**: Po dostrojeniu, zamiast zapisywać **cały nowy model** dla każdego zadania, musisz tylko przechowywać **macierze LoRA**, które są bardzo małe w porównaniu do całego modelu. Ułatwia to dostosowanie modelu do wielu zadań bez użycia zbyt dużej ilości pamięci.
|
||||
4. **Efektywność przechowywania**: Po dostrojeniu, zamiast zapisywać **cały nowy model** dla każdego zadania, musisz tylko przechowywać **macierze LoRA**, które są bardzo małe w porównaniu do całego modelu. To ułatwia dostosowanie modelu do wielu zadań bez użycia zbyt dużej ilości pamięci.
|
||||
|
||||
Aby zaimplementować LoraLayers zamiast liniowych podczas dostrajania, proponowany jest tutaj ten kod [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
|
||||
|
@ -0,0 +1,110 @@
|
||||
# 7.1. Dostosowywanie do klasyfikacji
|
||||
|
||||
## Czym jest
|
||||
|
||||
Dostosowywanie to proces, w którym bierze się **wstępnie wytrenowany model**, który nauczył się **ogólnych wzorców językowych** z ogromnych ilości danych i **dostosowuje** go do wykonywania **specyficznego zadania** lub rozumienia języka specyficznego dla danej dziedziny. Osiąga się to poprzez kontynuowanie treningu modelu na mniejszym, specyficznym dla zadania zbiorze danych, co pozwala mu dostosować swoje parametry, aby lepiej odpowiadały niuansom nowych danych, jednocześnie wykorzystując szeroką wiedzę, którą już zdobył. Dostosowywanie umożliwia modelowi dostarczanie dokładniejszych i bardziej odpowiednich wyników w specjalistycznych zastosowaniach bez potrzeby trenowania nowego modelu od podstaw.
|
||||
|
||||
> [!TIP]
|
||||
> Ponieważ wstępne trenowanie LLM, który "rozumie" tekst, jest dość kosztowne, zazwyczaj łatwiej i taniej jest dostosować otwarte modele wstępnie wytrenowane do wykonywania konkretnego zadania, które chcemy, aby realizował.
|
||||
|
||||
> [!TIP]
|
||||
> Celem tej sekcji jest pokazanie, jak dostosować już wstępnie wytrenowany model, aby zamiast generować nowy tekst, LLM wybierał **prawdopodobieństwa przypisania danego tekstu do każdej z podanych kategorii** (na przykład, czy tekst jest spamem, czy nie).
|
||||
|
||||
## Przygotowanie zbioru danych
|
||||
|
||||
### Rozmiar zbioru danych
|
||||
|
||||
Oczywiście, aby dostosować model, potrzebujesz pewnych uporządkowanych danych do specjalizacji swojego LLM. W przykładzie zaproponowanym w [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 jest dostosowywany do wykrywania, czy e-mail jest spamem, czy nie, przy użyciu danych z [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
Ten zbiór danych zawiera znacznie więcej przykładów "nie spam" niż "spam", dlatego książka sugeruje, aby **używać tylko tylu przykładów "nie spam", co "spam"** (w związku z tym, usuwając z danych treningowych wszystkie dodatkowe przykłady). W tym przypadku było to 747 przykładów każdego.
|
||||
|
||||
Następnie, **70%** zbioru danych jest używane do **treningu**, **10%** do **walidacji**, a **20%** do **testowania**.
|
||||
|
||||
- **Zbiór walidacyjny** jest używany podczas fazy treningu do dostosowywania **hiperparametrów** modelu i podejmowania decyzji dotyczących architektury modelu, skutecznie pomagając zapobiegać przeuczeniu, dostarczając informacji zwrotnej na temat tego, jak model radzi sobie z nieznanymi danymi. Umożliwia to iteracyjne poprawki bez wprowadzania stronniczości w końcowej ocenie.
|
||||
- Oznacza to, że chociaż dane zawarte w tym zbiorze danych nie są używane bezpośrednio do treningu, są używane do dostrojenia najlepszych **hiperparametrów**, więc ten zbiór nie może być używany do oceny wydajności modelu, jak zbiór testowy.
|
||||
- W przeciwieństwie do tego, **zbiór testowy** jest używany **tylko po** pełnym wytrenowaniu modelu i zakończeniu wszystkich dostosowań; zapewnia bezstronną ocenę zdolności modelu do generalizacji na nowe, nieznane dane. Ta końcowa ocena na zbiorze testowym daje realistyczne wskazanie, jak model ma się sprawować w rzeczywistych zastosowaniach.
|
||||
|
||||
### Długość wpisów
|
||||
|
||||
Ponieważ przykład treningowy oczekuje wpisów (tekstów e-maili w tym przypadku) o tej samej długości, zdecydowano się, aby każdy wpis był tak duży, jak największy, dodając identyfikatory `<|endoftext|>` jako wypełnienie.
|
||||
|
||||
### Inicjalizacja modelu
|
||||
|
||||
Używając otwartych, wstępnie wytrenowanych wag, inicjalizuj model do treningu. Już to zrobiliśmy wcześniej i postępując zgodnie z instrukcjami z [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), możesz to łatwo zrobić.
|
||||
|
||||
## Głowa klasyfikacji
|
||||
|
||||
W tym konkretnym przykładzie (przewidywanie, czy tekst jest spamem, czy nie) nie interesuje nas dostosowywanie zgodnie z pełnym słownictwem GPT2, ale chcemy, aby nowy model tylko określał, czy e-mail jest spamem (1), czy nie (0). Dlatego zamierzamy **zmodyfikować ostatnią warstwę**, która podaje prawdopodobieństwa dla tokenów słownictwa, na taką, która podaje tylko prawdopodobieństwa bycia spamem lub nie (więc jakby słownictwo składające się z 2 słów).
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Parametry do dostrojenia
|
||||
|
||||
Aby szybko dostroić model, łatwiej jest nie dostrajać wszystkich parametrów, a jedynie niektóre końcowe. Dzieje się tak, ponieważ wiadomo, że niższe warstwy zazwyczaj uchwycają podstawowe struktury językowe i semantykę. Dlatego **dostrojenie tylko ostatnich warstw jest zazwyczaj wystarczające i szybsze**.
|
||||
```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
|
||||
|
||||
W poprzednich sekcjach LLM był trenowany, redukując stratę każdego przewidywanego tokena, mimo że prawie wszystkie przewidywane tokeny znajdowały się w zdaniu wejściowym (tylko 1 na końcu był naprawdę przewidywany), aby model lepiej zrozumiał język.
|
||||
|
||||
W tym przypadku interesuje nas tylko to, czy model jest spamem, czy nie, więc interesuje nas tylko ostatni przewidywany token. Dlatego konieczne jest zmodyfikowanie naszych wcześniejszych funkcji straty treningowej, aby uwzględniały tylko ten token.
|
||||
|
||||
To jest zaimplementowane w [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) jako:
|
||||
```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
|
||||
```
|
||||
Zauważ, że dla każdej partii interesują nas tylko **logity ostatniego przewidywanego tokena**.
|
||||
|
||||
## Pełny kod klasyfikacji fine-tune GPT2
|
||||
|
||||
Możesz znaleźć cały kod do fine-tuningu GPT2 jako klasyfikatora spamu w [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)
|
||||
|
||||
## Odniesienia
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -82,7 +82,7 @@ Powinieneś zacząć od przeczytania tego posta, aby poznać podstawowe pojęcia
|
||||
## 7.1. Fine-Tuning do Klasyfikacji
|
||||
|
||||
> [!TIP]
|
||||
> Celem tej sekcji jest pokazanie, jak dostosować już wytrenowany model, aby zamiast generować nowy tekst, LLM podał **prawdopodobieństwa, że dany tekst będzie zaklasyfikowany w każdej z podanych kategorii** (na przykład, czy tekst jest spamem, czy nie).
|
||||
> Celem tej sekcji jest pokazanie, jak dostosować już wytrenowany model, aby zamiast generować nowy tekst, LLM podałby **prawdopodobieństwa, że dany tekst zostanie zaklasyfikowany w każdej z podanych kategorii** (na przykład, czy tekst jest spamem, czy nie).
|
||||
|
||||
{{#ref}}
|
||||
7.1.-fine-tuning-for-classification.md
|
||||
|
Loading…
x
Reference in New Issue
Block a user