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
ace696f6ba
commit
1c5b877f48
@ -201,14 +201,14 @@ Gradient w.r.t b: tensor([-0.0817])
|
||||
|
||||
Em redes neurais maiores com múltiplas camadas, o processo de computação de gradientes se torna mais complexo devido ao aumento do número de parâmetros e operações. No entanto, os princípios fundamentais permanecem os mesmos:
|
||||
|
||||
- **Passo Direto:** Calcule a saída da rede passando as entradas por cada camada.
|
||||
- **Passagem Direta:** Calcule a saída da rede passando as entradas por cada camada.
|
||||
- **Calcule a Perda:** Avalie a função de perda usando a saída da rede e os rótulos alvo.
|
||||
- **Passo Reverso (Retropropagação):** Calcule os gradientes da perda em relação a cada parâmetro na rede aplicando a regra da cadeia recursivamente da camada de saída de volta para a camada de entrada.
|
||||
- **Passagem Reversa (Retropropagação):** Calcule os gradientes da perda em relação a cada parâmetro na rede aplicando a regra da cadeia recursivamente da camada de saída de volta para a camada de entrada.
|
||||
|
||||
### **2. Algoritmo de Retropropagação**
|
||||
|
||||
- **Passo 1:** Inicialize os parâmetros da rede (pesos e viéses).
|
||||
- **Passo 2:** Para cada exemplo de treinamento, realize um passo direto para calcular as saídas.
|
||||
- **Passo 2:** Para cada exemplo de treinamento, realize uma passagem direta para calcular as saídas.
|
||||
- **Passo 3:** Calcule a perda.
|
||||
- **Passo 4:** Calcule os gradientes da perda em relação a cada parâmetro usando a regra da cadeia.
|
||||
- **Passo 5:** Atualize os parâmetros usando um algoritmo de otimização (por exemplo, descida de gradiente).
|
||||
@ -274,9 +274,9 @@ Neste código:
|
||||
|
||||
Durante o backward pass:
|
||||
|
||||
- PyTorch percorre o grafo computacional em ordem reversa.
|
||||
- O PyTorch percorre o grafo computacional em ordem reversa.
|
||||
- Para cada operação, aplica a regra da cadeia para computar gradientes.
|
||||
- Gradientes são acumulados no atributo `.grad` de cada tensor de parâmetro.
|
||||
- Os gradientes são acumulados no atributo `.grad` de cada tensor de parâmetro.
|
||||
|
||||
### **6. Vantagens da Diferenciação Automática**
|
||||
|
||||
|
@ -19,7 +19,7 @@ Tokens: `["Olá", ",", "mundo", "!"]`
|
||||
- **Tokens Especiais:** Estes são símbolos especiais adicionados ao vocabulário para lidar com vários cenários:
|
||||
- `[BOS]` (Início da Sequência): Indica o início de um texto.
|
||||
- `[EOS]` (Fim da Sequência): Indica o fim de um texto.
|
||||
- `[PAD]` (Preenchimento): Usado para fazer todas as sequências em um lote terem o mesmo comprimento.
|
||||
- `[PAD]` (Preenchimento): Usado para fazer com que todas as sequências em um lote tenham o mesmo comprimento.
|
||||
- `[UNK]` (Desconhecido): Representa tokens que não estão no vocabulário.
|
||||
- _Exemplo:_\
|
||||
Se `"Olá"` é atribuído ao ID `64`, `","` é `455`, `"mundo"` é `78`, e `"!"` é `467`, então:\
|
||||
@ -55,7 +55,7 @@ Enquanto o tokenizador básico funciona bem para textos simples, ele tem limita
|
||||
- Equilibra entre ter um tamanho de vocabulário gerenciável e representar palavras de forma eficaz.
|
||||
- Lida eficientemente com palavras raras e compostas.
|
||||
- _Exemplo:_\
|
||||
`"infelicidade"` pode ser tokenizado como `["in", "felicidade"]` ou `["in", "feliz", "dade"]` dependendo do vocabulário.
|
||||
`"infelicidade"` pode ser tokenizado como `["in", "felicidade"]` ou `["in", "feliz", "dade"]`, dependendo do vocabulário.
|
||||
3. **Modelo de Linguagem Unigram:**
|
||||
- **Usado Por:** Modelos como SentencePiece.
|
||||
- **Propósito:** Usa um modelo probabilístico para determinar o conjunto mais provável de tokens de subpalavras.
|
||||
|
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. Amostragem de Dados
|
||||
|
||||
## **Amostragem de Dados**
|
||||
|
||||
**Amostragem de Dados** é um processo crucial na preparação de dados para treinar grandes modelos de linguagem (LLMs) como o GPT. Envolve organizar dados textuais em sequências de entrada e alvo que o modelo usa para aprender a prever a próxima palavra (ou token) com base nas palavras anteriores. A amostragem de dados adequada garante que o modelo capture efetivamente padrões e dependências da linguagem.
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta segunda fase é muito simples: **Amostrar os dados de entrada e prepará-los para a fase de treinamento, geralmente separando o conjunto de dados em frases de um comprimento específico e gerando também a resposta esperada.**
|
||||
|
||||
### **Por que a Amostragem de Dados é Importante**
|
||||
|
||||
LLMs como o GPT são treinados para gerar ou prever texto entendendo o contexto fornecido pelas palavras anteriores. Para alcançar isso, os dados de treinamento devem ser estruturados de uma maneira que o modelo possa aprender a relação entre sequências de palavras e suas palavras subsequentes. Essa abordagem estruturada permite que o modelo generalize e gere texto coerente e contextualmente relevante.
|
||||
|
||||
### **Conceitos Chave na Amostragem de Dados**
|
||||
|
||||
1. **Tokenização:** Dividir o texto em unidades menores chamadas tokens (por exemplo, palavras, subpalavras ou caracteres).
|
||||
2. **Comprimento da Sequência (max_length):** O número de tokens em cada sequência de entrada.
|
||||
3. **Janela Deslizante:** Um método para criar sequências de entrada sobrepostas movendo uma janela sobre o texto tokenizado.
|
||||
4. **Stride:** O número de tokens que a janela deslizante avança para criar a próxima sequência.
|
||||
|
||||
### **Exemplo Passo a Passo**
|
||||
|
||||
Vamos percorrer um exemplo para ilustrar a amostragem de dados.
|
||||
|
||||
**Texto de Exemplo**
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
**Tokenização**
|
||||
|
||||
Assuma que usamos um **tokenizador básico** que divide o texto em palavras e sinais de pontuação:
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
**Parâmetros**
|
||||
|
||||
- **Comprimento Máximo da Sequência (max_length):** 4 tokens
|
||||
- **Passo da Janela Deslizante:** 1 token
|
||||
|
||||
**Criando Sequências de Entrada e Alvo**
|
||||
|
||||
1. **Abordagem da Janela Deslizante:**
|
||||
- **Sequências de Entrada:** Cada sequência de entrada consiste em `max_length` tokens.
|
||||
- **Sequências de Alvo:** Cada sequência de alvo consiste nos tokens que imediatamente seguem a sequência de entrada correspondente.
|
||||
2. **Gerando Sequências:**
|
||||
|
||||
<table><thead><tr><th width="177">Posição da Janela</th><th>Sequência de Entrada</th><th>Sequência de Alvo</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. **Arrays de Entrada e Alvo Resultantes:**
|
||||
|
||||
- **Entrada:**
|
||||
|
||||
```python
|
||||
[
|
||||
["Lorem", "ipsum", "dolor", "sit"],
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
]
|
||||
```
|
||||
|
||||
- **Alvo:**
|
||||
|
||||
```python
|
||||
[
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
["amet,", "consectetur", "adipiscing", "elit."],
|
||||
]
|
||||
```
|
||||
|
||||
**Representação Visual**
|
||||
|
||||
<table><thead><tr><th width="222">Posição do Token</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>
|
||||
|
||||
**Janela Deslizante com Passo 1:**
|
||||
|
||||
- **Primeira Janela (Posições 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Alvo:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Segunda Janela (Posições 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **Alvo:** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Terceira Janela (Posições 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Alvo:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Quarta Janela (Posições 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **Alvo:** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Entendendo o Passo**
|
||||
|
||||
- **Passo de 1:** A janela se move para frente um token a cada vez, resultando em sequências altamente sobrepostas. Isso pode levar a uma melhor aprendizagem das relações contextuais, mas pode aumentar o risco de overfitting, uma vez que pontos de dados semelhantes são repetidos.
|
||||
- **Passo de 2:** A janela se move para frente dois tokens a cada vez, reduzindo a sobreposição. Isso diminui a redundância e a carga computacional, mas pode perder algumas nuances contextuais.
|
||||
- **Passo Igual a max_length:** A janela se move para frente pelo tamanho total da janela, resultando em sequências não sobrepostas. Isso minimiza a redundância de dados, mas pode limitar a capacidade do modelo de aprender dependências entre sequências.
|
||||
|
||||
**Exemplo com Passo de 2:**
|
||||
|
||||
Usando o mesmo texto tokenizado e `max_length` de 4:
|
||||
|
||||
- **Primeira Janela (Posições 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Alvo:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Segunda Janela (Posições 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Alvo:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Terceira Janela (Posições 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **Alvo:** \["consectetur", "adipiscing", "elit.", "sed"] _(Assumindo continuação)_
|
||||
|
||||
## Exemplo de Código
|
||||
|
||||
Vamos entender isso melhor a partir de um exemplo de código de [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]])
|
||||
]
|
||||
```
|
||||
## Referências
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -10,15 +10,15 @@ Os mecanismos de atenção permitem que redes neurais **focalizem partes especí
|
||||
|
||||
### Entendendo os Mecanismos de Atenção
|
||||
|
||||
Em modelos tradicionais de sequência para sequência usados para tradução de linguagem, o modelo codifica uma sequência de entrada em um vetor de contexto de tamanho fixo. No entanto, essa abordagem tem dificuldades com frases longas porque o vetor de contexto de tamanho fixo pode não capturar todas as informações necessárias. Os mecanismos de atenção abordam essa limitação permitindo que o modelo considere todos os tokens de entrada ao gerar cada token de saída.
|
||||
Em modelos tradicionais de sequência para sequência usados para tradução de idiomas, o modelo codifica uma sequência de entrada em um vetor de contexto de tamanho fixo. No entanto, essa abordagem tem dificuldades com frases longas porque o vetor de contexto de tamanho fixo pode não capturar todas as informações necessárias. Os mecanismos de atenção abordam essa limitação permitindo que o modelo considere todos os tokens de entrada ao gerar cada token de saída.
|
||||
|
||||
#### Exemplo: Tradução Automática
|
||||
|
||||
Considere traduzir a frase em alemão "Kannst du mir helfen diesen Satz zu übersetzen" para o inglês. Uma tradução palavra por palavra não produziria uma frase em inglês gramaticalmente correta devido a diferenças nas estruturas gramaticais entre as línguas. Um mecanismo de atenção permite que o modelo se concentre nas partes relevantes da frase de entrada ao gerar cada palavra da frase de saída, levando a uma tradução mais precisa e coerente.
|
||||
Considere traduzir a frase em alemão "Kannst du mir helfen diesen Satz zu übersetzen" para o inglês. Uma tradução palavra por palavra não produziria uma frase em inglês gramaticalmente correta devido a diferenças nas estruturas gramaticais entre os idiomas. Um mecanismo de atenção permite que o modelo se concentre nas partes relevantes da frase de entrada ao gerar cada palavra da frase de saída, levando a uma tradução mais precisa e coerente.
|
||||
|
||||
### Introdução à Auto-Atenção
|
||||
|
||||
A auto-atensão, ou intra-atensão, é um mecanismo onde a atenção é aplicada dentro de uma única sequência para calcular uma representação dessa sequência. Isso permite que cada token na sequência atenda a todos os outros tokens, ajudando o modelo a capturar dependências entre tokens, independentemente da distância entre eles na sequência.
|
||||
Auto-atenção, ou intra-atenção, é um mecanismo onde a atenção é aplicada dentro de uma única sequência para computar uma representação dessa sequência. Isso permite que cada token na sequência atenda a todos os outros tokens, ajudando o modelo a capturar dependências entre tokens, independentemente da distância entre eles na sequência.
|
||||
|
||||
#### Conceitos Chave
|
||||
|
||||
@ -34,7 +34,7 @@ Vamos considerar a frase **"Hello shiny sun!"** e representar cada palavra com u
|
||||
- **shiny**: `[0.53, 0.34, 0.98]`
|
||||
- **sun**: `[0.29, 0.54, 0.93]`
|
||||
|
||||
Nosso objetivo é calcular o **vetor de contexto** para a palavra **"shiny"** usando auto-atensão.
|
||||
Nosso objetivo é computar o **vetor de contexto** para a palavra **"shiny"** usando auto-atenção.
|
||||
|
||||
#### Passo 1: Calcular Pontuações de Atenção
|
||||
|
||||
@ -68,7 +68,7 @@ Aplique a **função softmax** às pontuações de atenção para convertê-las
|
||||
|
||||
Calculando os exponenciais:
|
||||
|
||||
<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>
|
||||
|
||||
Calculando a soma:
|
||||
|
||||
@ -78,12 +78,12 @@ Calculando pesos de atenção:
|
||||
|
||||
<figure><img src="../../images/image (6) (1) (1).png" alt="" width="404"><figcaption></figcaption></figure>
|
||||
|
||||
#### Passo 3: Calcular o Vetor de Contexto
|
||||
#### Passo 3: Computar o Vetor de Contexto
|
||||
|
||||
> [!TIP]
|
||||
> Basta pegar cada peso de atenção e multiplicá-lo pelas dimensões do token relacionado e, em seguida, somar todas as dimensões para obter apenas 1 vetor (o vetor de contexto)
|
||||
|
||||
O **vetor de contexto** é calculado como a soma ponderada dos embeddings de todas as palavras, usando os pesos de atenção.
|
||||
O **vetor de contexto** é computado como a soma ponderada dos embeddings de todas as palavras, usando os pesos de atenção.
|
||||
|
||||
<figure><img src="../../images/image (16).png" alt="" width="369"><figcaption></figcaption></figure>
|
||||
|
||||
@ -103,7 +103,7 @@ Calculando cada componente:
|
||||
|
||||
Somando os embeddings ponderados:
|
||||
|
||||
`context vector=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]`
|
||||
`vetor de contexto=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]`
|
||||
|
||||
**Este vetor de contexto representa o embedding enriquecido para a palavra "shiny", incorporando informações de todas as palavras na frase.**
|
||||
|
||||
@ -111,11 +111,11 @@ Somando os embeddings ponderados:
|
||||
|
||||
1. **Calcular Pontuações de Atenção**: Use o produto escalar entre o embedding da palavra-alvo e os embeddings de todas as palavras na sequência.
|
||||
2. **Normalizar Pontuações para Obter Pesos de Atenção**: Aplique a função softmax às pontuações de atenção para obter pesos que somem 1.
|
||||
3. **Calcular o Vetor de Contexto**: Multiplique o embedding de cada palavra pelo seu peso de atenção e some os resultados.
|
||||
3. **Computar Vetor de Contexto**: Multiplique o embedding de cada palavra pelo seu peso de atenção e some os resultados.
|
||||
|
||||
## Auto-Atenção com Pesos Treináveis
|
||||
|
||||
Na prática, os mecanismos de auto-atensão usam **pesos treináveis** para aprender as melhores representações para consultas, chaves e valores. Isso envolve a introdução de três matrizes de peso:
|
||||
Na prática, os mecanismos de auto-atenção usam **pesos treináveis** para aprender as melhores representações para consultas, chaves e valores. Isso envolve a introdução de três matrizes de peso:
|
||||
|
||||
<figure><img src="../../images/image (10) (1) (1).png" alt="" width="239"><figcaption></figcaption></figure>
|
||||
|
||||
@ -127,7 +127,7 @@ Cada token terá sua própria matriz de consulta, chave e valor multiplicando se
|
||||
|
||||
<figure><img src="../../images/image (11).png" alt="" width="253"><figcaption></figcaption></figure>
|
||||
|
||||
Essas matrizes transformam os embeddings originais em um novo espaço adequado para calcular a atenção.
|
||||
Essas matrizes transformam os embeddings originais em um novo espaço adequado para computar a atenção.
|
||||
|
||||
**Exemplo**
|
||||
|
||||
@ -404,7 +404,7 @@ print(context_vecs)
|
||||
print("context_vecs.shape:", context_vecs.shape)
|
||||
|
||||
```
|
||||
Para uma implementação compacta e eficiente, você pode usar a [`torch.nn.MultiheadAttention`](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) classe no PyTorch.
|
||||
Para uma implementação compacta e eficiente, você pode usar a classe [`torch.nn.MultiheadAttention`](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) no PyTorch.
|
||||
|
||||
> [!TIP]
|
||||
> Resposta curta do ChatGPT sobre por que é melhor dividir as dimensões dos tokens entre as cabeças em vez de fazer com que cada cabeça verifique todas as dimensões de todos os tokens:
|
||||
|
@ -221,7 +221,7 @@ torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
|
||||
### **Rede Neural FeedForward**
|
||||
|
||||
_Formas foram adicionadas como comentários para entender melhor as formas das matrizes:_
|
||||
_As formas foram adicionadas como comentários para entender melhor as formas das matrizes:_
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class FeedForward(nn.Module):
|
||||
@ -306,7 +306,7 @@ return self.scale * norm_x + self.shift
|
||||
|
||||
### **Bloco Transformer**
|
||||
|
||||
_Shapes foram adicionadas como comentários para entender melhor as formas das matrizes:_
|
||||
_Formas foram adicionadas como comentários para entender melhor as formas das matrizes:_
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
|
||||
@ -608,7 +608,7 @@ total_params = 163,009,536
|
||||
```
|
||||
## Gerar Texto
|
||||
|
||||
Tendo um modelo que prevê o próximo token como o anterior, é necessário apenas pegar os últimos valores de token da saída (já que eles serão os do token previsto), que será um **valor por entrada no vocabulário** e então usar a função `softmax` para normalizar as dimensões em probabilidades que somam 1 e, em seguida, obter o índice da maior entrada, que será o índice da palavra dentro do vocabulário.
|
||||
Tendo um modelo que prevê o próximo token como o anterior, é necessário apenas pegar os últimos valores de token da saída (já que serão os do token previsto), que será um **valor por entrada no vocabulário** e então usar a função `softmax` para normalizar as dimensões em probabilidades que somam 1 e, em seguida, obter o índice da maior entrada, que será o índice da palavra dentro do vocabulário.
|
||||
|
||||
Código de [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
|
||||
|
943
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
943
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
@ -0,0 +1,943 @@
|
||||
# 6. Pré-treinamento e Carregamento de Modelos
|
||||
|
||||
## Geração de Texto
|
||||
|
||||
Para treinar um modelo, precisamos que esse modelo seja capaz de gerar novos tokens. Em seguida, compararemos os tokens gerados com os esperados para treinar o modelo a **aprender os tokens que precisa gerar**.
|
||||
|
||||
Como nos exemplos anteriores, já previmos alguns tokens, é possível reutilizar essa função para esse propósito.
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta sexta fase é muito simples: **Treinar o modelo do zero**. Para isso, a arquitetura LLM anterior será utilizada com alguns loops sobre os conjuntos de dados usando as funções de perda e otimizador definidos para treinar todos os parâmetros do modelo.
|
||||
|
||||
## Avaliação de Texto
|
||||
|
||||
Para realizar um treinamento correto, é necessário medir as previsões obtidas para o token esperado. O objetivo do treinamento é maximizar a probabilidade do token correto, o que envolve aumentar sua probabilidade em relação a outros tokens.
|
||||
|
||||
Para maximizar a probabilidade do token correto, os pesos do modelo devem ser modificados para que essa probabilidade seja maximizada. As atualizações dos pesos são feitas via **backpropagation**. Isso requer uma **função de perda a ser maximizada**. Neste caso, a função será a **diferença entre a previsão realizada e a desejada**.
|
||||
|
||||
No entanto, em vez de trabalhar com as previsões brutas, trabalhará com um logaritmo de base n. Assim, se a previsão atual do token esperado for 7.4541e-05, o logaritmo natural (base *e*) de **7.4541e-05** é aproximadamente **-9.5042**.\
|
||||
Então, para cada entrada com um comprimento de contexto de 5 tokens, por exemplo, o modelo precisará prever 5 tokens, sendo os primeiros 4 tokens o último da entrada e o quinto o previsto. Portanto, para cada entrada teremos 5 previsões nesse caso (mesmo que os primeiros 4 estivessem na entrada, o modelo não sabe disso) com 5 tokens esperados e, portanto, 5 probabilidades a serem maximizadas.
|
||||
|
||||
Portanto, após realizar o logaritmo natural em cada previsão, a **média** é calculada, o **sinal de menos removido** (isso é chamado de _cross entropy loss_) e esse é o **número a ser reduzido o mais próximo possível de 0** porque o logaritmo natural de 1 é 0:
|
||||
|
||||
<figure><img src="../../images/image (10) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233">https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233</a></p></figcaption></figure>
|
||||
|
||||
Outra maneira de medir quão bom é o modelo é chamada de perplexidade. **Perplexidade** é uma métrica usada para avaliar quão bem um modelo de probabilidade prevê uma amostra. Na modelagem de linguagem, representa a **incerteza do modelo** ao prever o próximo token em uma sequência.\
|
||||
Por exemplo, um valor de perplexidade de 48725 significa que, ao precisar prever um token, ele não tem certeza sobre qual entre 48.725 tokens no vocabulário é o correto.
|
||||
|
||||
## Exemplo de Pré-Treinamento
|
||||
|
||||
Este é o código inicial proposto em [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) algumas vezes ligeiramente modificado
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Código anterior usado aqui, mas já explicado em seções anteriores</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"
|
||||
)
|
||||
```
|
||||
Vamos ver uma explicação passo a passo
|
||||
|
||||
### Funções para transformar texto <--> ids
|
||||
|
||||
Estas são algumas funções simples que podem ser usadas para transformar textos do vocabulário em ids e vice-versa. Isso é necessário no início do manuseio do texto e no final das previsões:
|
||||
```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())
|
||||
```
|
||||
### Funções de geração de texto
|
||||
|
||||
Na seção anterior, uma função que apenas obteve o **token mais provável** após obter os logits. No entanto, isso significa que para cada entrada a mesma saída sempre será gerada, o que a torna muito determinística.
|
||||
|
||||
A seguinte função `generate_text` aplicará os conceitos de `top-k`, `temperature` e `multinomial`.
|
||||
|
||||
- O **`top-k`** significa que começaremos a reduzir para `-inf` todas as probabilidades de todos os tokens, exceto os k tokens principais. Assim, se k=3, antes de tomar uma decisão, apenas os 3 tokens mais prováveis terão uma probabilidade diferente de `-inf`.
|
||||
- A **`temperature`** significa que cada probabilidade será dividida pelo valor da temperatura. Um valor de `0.1` melhorará a maior probabilidade em comparação com a menor, enquanto uma temperatura de `5`, por exemplo, a tornará mais uniforme. Isso ajuda a melhorar a variação nas respostas que gostaríamos que o LLM tivesse.
|
||||
- Após aplicar a temperatura, uma função **`softmax`** é aplicada novamente para fazer com que todos os tokens restantes tenham uma probabilidade total de 1.
|
||||
- Finalmente, em vez de escolher o token com a maior probabilidade, a função **`multinomial`** é aplicada para **prever o próximo token de acordo com as probabilidades finais**. Assim, se o token 1 tiver 70% de probabilidade, o token 2 tiver 20% e o token 3 tiver 10%, 70% das vezes o token 1 será selecionado, 20% das vezes será o token 2 e 10% das vezes será o 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]
|
||||
> Existe uma alternativa comum ao `top-k` chamada [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), também conhecida como amostragem de núcleo, que em vez de obter k amostras com a maior probabilidade, **organiza** todo o **vocabulário** resultante por probabilidades e **soma** elas da maior probabilidade para a menor até que um **limite seja alcançado**.
|
||||
>
|
||||
> Então, **apenas aquelas palavras** do vocabulário serão consideradas de acordo com suas probabilidades relativas.
|
||||
>
|
||||
> Isso permite não precisar selecionar um número de amostras `k`, já que o k ideal pode ser diferente em cada caso, mas **apenas um limite**.
|
||||
>
|
||||
> _Note que essa melhoria não está incluída no código anterior._
|
||||
|
||||
> [!TIP]
|
||||
> Outra maneira de melhorar o texto gerado é usando **Beam search** em vez da busca gulosa usada neste exemplo.\
|
||||
> Ao contrário da busca gulosa, que seleciona a próxima palavra mais provável em cada passo e constrói uma única sequência, **beam search mantém o controle das 𝑘 k sequências parciais com as maiores pontuações** (chamadas de "beams") em cada passo. Ao explorar múltiplas possibilidades simultaneamente, equilibra eficiência e qualidade, aumentando as chances de **encontrar uma sequência geral melhor** que pode ser perdida pela abordagem gulosa devido a escolhas subótimas precoces.
|
||||
>
|
||||
> _Note que essa melhoria não está incluída no código anterior._
|
||||
|
||||
### Funções de perda
|
||||
|
||||
A função **`calc_loss_batch`** calcula a entropia cruzada de uma previsão de um único lote.\
|
||||
A **`calc_loss_loader`** obtém a entropia cruzada de todos os lotes e calcula a **entropia cruzada média**.
|
||||
```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]
|
||||
> **Recorte de gradiente** é uma técnica usada para melhorar a **estabilidade do treinamento** em grandes redes neurais, definindo um **limite máximo** para as magnitudes dos gradientes. Quando os gradientes excedem esse `max_norm` pré-definido, eles são reduzidos proporcionalmente para garantir que as atualizações dos parâmetros do modelo permaneçam dentro de um intervalo gerenciável, prevenindo problemas como gradientes explosivos e garantindo um treinamento mais controlado e estável.
|
||||
>
|
||||
> _Observe que essa melhoria não está incluída no código anterior._
|
||||
>
|
||||
> Confira o seguinte exemplo:
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Carregando Dados
|
||||
|
||||
As funções `create_dataloader_v1` e `create_dataloader_v1` já foram discutidas em uma seção anterior.
|
||||
|
||||
A partir daqui, note como está definido que 90% do texto será usado para treinamento, enquanto 10% será usado para validação, e ambos os conjuntos são armazenados em 2 carregadores de dados diferentes.\
|
||||
Observe que, às vezes, parte do conjunto de dados também é reservada para um conjunto de testes para avaliar melhor o desempenho do modelo.
|
||||
|
||||
Ambos os carregadores de dados estão usando o mesmo tamanho de lote, comprimento máximo, stride e número de trabalhadores (0 neste caso).\
|
||||
As principais diferenças são os dados usados por cada um, e o validador não está descartando o último nem embaralhando os dados, pois isso não é necessário para fins de validação.
|
||||
|
||||
Além disso, o fato de que **o stride é tão grande quanto o comprimento do contexto** significa que não haverá sobreposição entre os contextos usados para treinar os dados (reduz o overfitting, mas também o conjunto de dados de treinamento).
|
||||
|
||||
Além disso, note que o tamanho do lote neste caso é 2 para dividir os dados em 2 lotes, e o principal objetivo disso é permitir o processamento paralelo e reduzir o consumo por lote.
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Verificações de Sanidade
|
||||
|
||||
O objetivo é verificar se há tokens suficientes para treinamento, se as formas são as esperadas e obter algumas informações sobre o número de tokens usados para treinamento e validação:
|
||||
```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)
|
||||
```
|
||||
### Selecionar dispositivo para treinamento e pré-cálculos
|
||||
|
||||
O código a seguir apenas seleciona o dispositivo a ser usado e calcula uma perda de treinamento e uma perda de validação (sem ter treinado nada ainda) como ponto de partida.
|
||||
```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)
|
||||
```
|
||||
### Funções de treinamento
|
||||
|
||||
A função `generate_and_print_sample` apenas obtém um contexto e gera alguns tokens para ter uma noção de quão bom é o modelo naquele ponto. Isso é chamado por `train_model_simple` em cada etapa.
|
||||
|
||||
A função `evaluate_model` é chamada com a frequência indicada para a função de treinamento e é usada para medir a perda de treinamento e a perda de validação naquele ponto do treinamento do modelo.
|
||||
|
||||
Então, a grande função `train_model_simple` é a que realmente treina o modelo. Ela espera:
|
||||
|
||||
- O carregador de dados de treinamento (com os dados já separados e preparados para treinamento)
|
||||
- O carregador de validação
|
||||
- O **otimizador** a ser usado durante o treinamento: Esta é a função que usará os gradientes e atualizará os parâmetros para reduzir a perda. Neste caso, como você verá, `AdamW` é usado, mas há muitos mais.
|
||||
- `optimizer.zero_grad()` é chamado para redefinir os gradientes em cada rodada para não acumulá-los.
|
||||
- O parâmetro **`lr`** é a **taxa de aprendizado** que determina o **tamanho dos passos** dados durante o processo de otimização ao atualizar os parâmetros do modelo. Uma taxa de aprendizado **menor** significa que o otimizador **faz atualizações menores** nos pesos, o que pode levar a uma convergência mais **precisa**, mas pode **retardar** o treinamento. Uma taxa de aprendizado **maior** pode acelerar o treinamento, mas **risca ultrapassar** o mínimo da função de perda (**pular** o ponto onde a função de perda é minimizada).
|
||||
- **Weight Decay** modifica a etapa de **Cálculo da Perda** adicionando um termo extra que penaliza pesos grandes. Isso incentiva o otimizador a encontrar soluções com pesos menores, equilibrando entre ajustar bem os dados e manter o modelo simples, prevenindo overfitting em modelos de aprendizado de máquina, desencorajando o modelo de atribuir muita importância a qualquer recurso único.
|
||||
- Otimizadores tradicionais como SGD com regularização L2 acoplam o weight decay com o gradiente da função de perda. No entanto, **AdamW** (uma variante do otimizador Adam) desacopla o weight decay da atualização do gradiente, levando a uma regularização mais eficaz.
|
||||
- O dispositivo a ser usado para treinamento
|
||||
- O número de épocas: Número de vezes para percorrer os dados de treinamento
|
||||
- A frequência de avaliação: A frequência para chamar `evaluate_model`
|
||||
- A iteração de avaliação: O número de lotes a serem usados ao avaliar o estado atual do modelo ao chamar `generate_and_print_sample`
|
||||
- O contexto inicial: Qual a frase inicial a ser usada ao chamar `generate_and_print_sample`
|
||||
- O 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]
|
||||
> Para melhorar a taxa de aprendizado, existem algumas técnicas relevantes chamadas **linear warmup** e **cosine decay.**
|
||||
>
|
||||
> **Linear warmup** consiste em definir uma taxa de aprendizado inicial e uma máxima, e atualizá-la consistentemente após cada época. Isso ocorre porque começar o treinamento com atualizações de peso menores diminui o risco de o modelo encontrar atualizações grandes e desestabilizadoras durante sua fase de treinamento.\
|
||||
> **Cosine decay** é uma técnica que **reduz gradualmente a taxa de aprendizado** seguindo uma curva de meio cosseno **após a fase de warmup**, desacelerando as atualizações de peso para **minimizar o risco de ultrapassar** os mínimos de perda e garantir a estabilidade do treinamento em fases posteriores.
|
||||
>
|
||||
> _Observe que essas melhorias não estão incluídas no código anterior._
|
||||
|
||||
### Iniciar treinamento
|
||||
```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.")
|
||||
```
|
||||
### Impressão da evolução do treinamento
|
||||
|
||||
Com a seguinte função, é possível imprimir a evolução do modelo enquanto ele estava sendo treinado.
|
||||
```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)
|
||||
```
|
||||
### Salvar o modelo
|
||||
|
||||
É possível salvar o modelo + otimizador se você quiser continuar o treinamento mais tarde:
|
||||
```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
|
||||
```
|
||||
Ou apenas o modelo se você planeja usá-lo apenas:
|
||||
```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
|
||||
```
|
||||
## Carregando pesos do GPT2
|
||||
|
||||
Existem 2 scripts rápidos para carregar os pesos do GPT2 localmente. Para ambos, você pode clonar o repositório [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) localmente, então:
|
||||
|
||||
- O script [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) fará o download de todos os pesos e transformará os formatos de OpenAI para os esperados pelo nosso LLM. O script também está preparado com a configuração necessária e com o prompt: "Every effort moves you"
|
||||
- O script [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) permite que você carregue qualquer um dos pesos do GPT2 localmente (basta mudar a variável `CHOOSE_MODEL`) e prever texto a partir de alguns prompts.
|
||||
|
||||
## Referências
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -0,0 +1,110 @@
|
||||
# 7.1. Ajuste Fino para Classificação
|
||||
|
||||
## O que é
|
||||
|
||||
Ajuste fino é o processo de pegar um **modelo pré-treinado** que aprendeu **padrões gerais de linguagem** a partir de grandes quantidades de dados e **adaptá-lo** para realizar uma **tarefa específica** ou entender a linguagem específica de um domínio. Isso é alcançado continuando o treinamento do modelo em um conjunto de dados menor e específico para a tarefa, permitindo que ele ajuste seus parâmetros para se adequar melhor às nuances dos novos dados, aproveitando o amplo conhecimento que já adquiriu. O ajuste fino permite que o modelo forneça resultados mais precisos e relevantes em aplicações especializadas sem a necessidade de treinar um novo modelo do zero.
|
||||
|
||||
> [!TIP]
|
||||
> Como pré-treinar um LLM que "entende" o texto é bastante caro, geralmente é mais fácil e barato ajustar modelos pré-treinados de código aberto para realizar uma tarefa específica que queremos que ele execute.
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta seção é mostrar como ajustar um modelo já pré-treinado, de modo que, em vez de gerar novo texto, o LLM selecione e forneça as **probabilidades do texto dado ser categorizado em cada uma das categorias dadas** (como se um texto é spam ou não).
|
||||
|
||||
## Preparando o conjunto de dados
|
||||
|
||||
### Tamanho do conjunto de dados
|
||||
|
||||
Claro, para ajustar um modelo, você precisa de alguns dados estruturados para usar para especializar seu LLM. No exemplo proposto em [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), o GPT2 é ajustado para detectar se um e-mail é spam ou não usando os dados de [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
Este conjunto de dados contém muito mais exemplos de "não spam" do que de "spam", portanto, o livro sugere **usar apenas tantos exemplos de "não spam" quanto de "spam"** (removendo assim todos os exemplos extras dos dados de treinamento). Neste caso, foram 747 exemplos de cada.
|
||||
|
||||
Então, **70%** do conjunto de dados é usado para **treinamento**, **10%** para **validação** e **20%** para **teste**.
|
||||
|
||||
- O **conjunto de validação** é usado durante a fase de treinamento para ajustar os **hiperparâmetros** do modelo e tomar decisões sobre a arquitetura do modelo, ajudando efetivamente a prevenir o overfitting ao fornecer feedback sobre como o modelo se comporta em dados não vistos. Ele permite melhorias iterativas sem enviesar a avaliação final.
|
||||
- Isso significa que, embora os dados incluídos neste conjunto de dados não sejam usados para o treinamento diretamente, eles são usados para ajustar os melhores **hiperparâmetros**, portanto, este conjunto não pode ser usado para avaliar o desempenho do modelo como o conjunto de teste.
|
||||
- Em contraste, o **conjunto de teste** é usado **apenas após** o modelo ter sido totalmente treinado e todos os ajustes estarem completos; ele fornece uma avaliação imparcial da capacidade do modelo de generalizar para novos dados não vistos. Esta avaliação final no conjunto de teste dá uma indicação realista de como o modelo deve se comportar em aplicações do mundo real.
|
||||
|
||||
### Comprimento das entradas
|
||||
|
||||
Como o exemplo de treinamento espera entradas (texto de e-mails, neste caso) do mesmo comprimento, decidiu-se fazer cada entrada tão grande quanto a maior, adicionando os ids de `<|endoftext|>` como preenchimento.
|
||||
|
||||
### Inicializar o modelo
|
||||
|
||||
Usando os pesos pré-treinados de código aberto, inicialize o modelo para treinar. Já fizemos isso antes e seguindo as instruções de [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) você pode facilmente fazê-lo.
|
||||
|
||||
## Cabeça de Classificação
|
||||
|
||||
Neste exemplo específico (prevendo se um texto é spam ou não), não estamos interessados em ajustar de acordo com o vocabulário completo do GPT2, mas queremos que o novo modelo diga se o e-mail é spam (1) ou não (0). Portanto, vamos **modificar a camada final que** fornece as probabilidades por token do vocabulário para uma que apenas fornece as probabilidades de ser spam ou não (então, como um vocabulário de 2 palavras).
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Parâmetros a ajustar
|
||||
|
||||
Para ajustar rapidamente, é mais fácil não ajustar todos os parâmetros, mas apenas alguns finais. Isso ocorre porque é sabido que as camadas inferiores geralmente capturam estruturas e semânticas básicas da linguagem aplicáveis. Portanto, apenas **ajustar as últimas camadas geralmente é suficiente e mais rápido**.
|
||||
```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
|
||||
```
|
||||
## Entradas a serem usadas para treinamento
|
||||
|
||||
Nas seções anteriores, o LLM foi treinado reduzindo a perda de cada token previsto, embora quase todos os tokens previstos estivessem na frase de entrada (apenas 1 no final foi realmente previsto) para que o modelo entendesse melhor a linguagem.
|
||||
|
||||
Neste caso, só nos importa que o modelo seja capaz de prever se o modelo é spam ou não, então só nos importa o último token previsto. Portanto, é necessário modificar nossas funções de perda de treinamento anteriores para levar em conta apenas esse token.
|
||||
|
||||
Isso é implementado em [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) como:
|
||||
```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
|
||||
```
|
||||
Note que para cada lote, estamos interessados apenas nos **logits do último token previsto**.
|
||||
|
||||
## Código completo de classificação de fine-tune do GPT2
|
||||
|
||||
Você pode encontrar todo o código para ajustar o GPT2 para ser um classificador de spam em [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)
|
||||
|
||||
## Referências
|
||||
|
||||
- [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,7 +5,7 @@
|
||||
|
||||
## Conjunto de Dados
|
||||
|
||||
Para ajustar finamente um LLM para seguir instruções, é necessário ter um conjunto de dados com instruções e respostas para ajustar o LLM. Existem diferentes formatos para treinar um LLM a seguir instruções, por exemplo:
|
||||
Para ajustar finamente um LLM para seguir instruções, é necessário ter um conjunto de dados com instruções e respostas para ajustar o LLM. Existem diferentes formatos para treinar um LLM para seguir instruções, por exemplo:
|
||||
|
||||
- O exemplo de estilo de prompt Apply Alpaca:
|
||||
```csharp
|
||||
@ -59,7 +59,7 @@ Então, é necessário agrupar todas as entradas e saídas esperadas para o trei
|
||||
- Preencher todas as amostras para o mesmo comprimento (geralmente o comprimento será tão grande quanto o comprimento do contexto usado para pré-treinar o LLM)
|
||||
- Criar os tokens esperados deslocando 1 a entrada em uma função de colagem personalizada
|
||||
- Substituir alguns tokens de preenchimento por -100 para excluí-los da perda de treinamento: Após o primeiro token `endoftext`, substituir todos os outros tokens `endoftext` por -100 (porque usar `cross_entropy(...,ignore_index=-100)` significa que ele ignorará alvos com -100)
|
||||
- \[Opcional] Mascarar usando -100 também todos os tokens pertencentes à pergunta para que o LLM aprenda apenas como gerar a resposta. No estilo Apply Alpaca, isso significará mascarar tudo até `### Response:`
|
||||
- \[Opcional] Mascarar usando -100 também todos os tokens pertencentes à pergunta para que o LLM aprenda apenas a gerar a resposta. No estilo Apply Alpaca, isso significará mascarar tudo até `### Response:`
|
||||
|
||||
Com isso criado, é hora de criar os carregadores de dados para cada conjunto de dados (treinamento, validação e teste).
|
||||
|
||||
@ -80,16 +80,16 @@ Outros testes a serem realizados para verificar a qualidade das respostas:
|
||||
1. **Measuring Massive Multitask Language Understanding (**[**MMLU**](https://arxiv.org/abs/2009.03300)**):** MMLU avalia o conhecimento e as habilidades de resolução de problemas de um modelo em 57 disciplinas, incluindo humanidades, ciências e mais. Ele usa perguntas de múltipla escolha para avaliar a compreensão em vários níveis de dificuldade, desde o elementar até o profissional avançado.
|
||||
2. [**LMSYS Chatbot Arena**](https://arena.lmsys.org): Esta plataforma permite que os usuários comparem respostas de diferentes chatbots lado a lado. Os usuários inserem um prompt, e vários chatbots geram respostas que podem ser comparadas diretamente.
|
||||
3. [**AlpacaEval**](https://github.com/tatsu-lab/alpaca_eval)**:** AlpacaEval é uma estrutura de avaliação automatizada onde um LLM avançado como o GPT-4 avalia as respostas de outros modelos a vários prompts.
|
||||
4. **General Language Understanding Evaluation (**[**GLUE**](https://gluebenchmark.com/)**):** GLUE é uma coleção de nove tarefas de compreensão de linguagem natural, incluindo análise de sentimentos, implicação textual e resposta a perguntas.
|
||||
4. **General Language Understanding Evaluation (**[**GLUE**](https://gluebenchmark.com/)**):** GLUE é uma coleção de nove tarefas de compreensão de linguagem natural, incluindo análise de sentimento, implicação textual e resposta a perguntas.
|
||||
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**:** Construindo sobre o GLUE, o SuperGLUE inclui tarefas mais desafiadoras projetadas para serem difíceis para os modelos atuais.
|
||||
6. **Beyond the Imitation Game Benchmark (**[**BIG-bench**](https://github.com/google/BIG-bench)**):** BIG-bench é um benchmark em larga escala com mais de 200 tarefas que testam as habilidades de um modelo em áreas como raciocínio, tradução e resposta a perguntas.
|
||||
7. **Holistic Evaluation of Language Models (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM fornece uma avaliação abrangente em várias métricas, como precisão, robustez e justiça.
|
||||
8. [**OpenAI Evals**](https://github.com/openai/evals)**:** Uma estrutura de avaliação de código aberto da OpenAI que permite o teste de modelos de IA em tarefas personalizadas e padronizadas.
|
||||
8. [**OpenAI Evals**](https://github.com/openai/evals)**:** Uma estrutura de avaliação de código aberto da OpenAI que permite testar modelos de IA em tarefas personalizadas e padronizadas.
|
||||
9. [**HumanEval**](https://github.com/openai/human-eval)**:** Uma coleção de problemas de programação usados para avaliar as habilidades de geração de código de modelos de linguagem.
|
||||
10. **Stanford Question Answering Dataset (**[**SQuAD**](https://rajpurkar.github.io/SQuAD-explorer/)**):** SQuAD consiste em perguntas sobre artigos da Wikipedia, onde os modelos devem compreender o texto para responder com precisão.
|
||||
11. [**TriviaQA**](https://nlp.cs.washington.edu/triviaqa/)**:** Um conjunto de dados em larga escala de perguntas e respostas de trivia, juntamente com documentos de evidência.
|
||||
|
||||
e muitos, muitos mais
|
||||
e muitos outros
|
||||
|
||||
## Follow instructions fine-tuning code
|
||||
|
||||
|
@ -50,7 +50,7 @@ Você deve começar lendo este post para alguns conceitos básicos que você dev
|
||||
4.-attention-mechanisms.md
|
||||
{{#endref}}
|
||||
|
||||
## 5. Arquitetura de LLM
|
||||
## 5. Arquitetura do LLM
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta quinta fase é muito simples: **Desenvolver a arquitetura do LLM completo**. Juntar tudo, aplicar todas as camadas e criar todas as funções para gerar texto ou transformar texto em IDs e vice-versa.
|
||||
@ -61,7 +61,7 @@ Você deve começar lendo este post para alguns conceitos básicos que você dev
|
||||
5.-llm-architecture.md
|
||||
{{#endref}}
|
||||
|
||||
## 6. Pré-treinamento & Carregamento de modelos
|
||||
## 6. Pré-treinamento e Carregamento de Modelos
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta sexta fase é muito simples: **Treinar o modelo do zero**. Para isso, a arquitetura LLM anterior será usada com alguns loops sobre os conjuntos de dados usando as funções de perda e otimizador definidos para treinar todos os parâmetros do modelo.
|
||||
@ -70,7 +70,7 @@ Você deve começar lendo este post para alguns conceitos básicos que você dev
|
||||
6.-pre-training-and-loading-models.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.0. Melhorias de LoRA em ajuste fino
|
||||
## 7.0. Melhorias de LoRA em Fine-Tuning
|
||||
|
||||
> [!TIP]
|
||||
> O uso de **LoRA reduz muito a computação** necessária para **ajustar** modelos já treinados.
|
||||
@ -79,19 +79,19 @@ Você deve começar lendo este post para alguns conceitos básicos que você dev
|
||||
7.0.-lora-improvements-in-fine-tuning.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.1. Ajuste Fino para Classificação
|
||||
## 7.1. Fine-Tuning para Classificação
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta seção é mostrar como ajustar finamente um modelo já pré-treinado para que, em vez de gerar novo texto, o LLM selecione e forneça as **probabilidades do texto dado ser categorizado em cada uma das categorias dadas** (como se um texto é spam ou não).
|
||||
> O objetivo desta seção é mostrar como ajustar um modelo já pré-treinado para que, em vez de gerar novo texto, o LLM selecione e forneça as **probabilidades do texto dado ser categorizado em cada uma das categorias dadas** (como se um texto é spam ou não).
|
||||
|
||||
{{#ref}}
|
||||
7.1.-fine-tuning-for-classification.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.2. Ajuste Fino para seguir instruções
|
||||
## 7.2. Fine-Tuning para Seguir Instruções
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta seção é mostrar como **ajustar finamente um modelo já pré-treinado para seguir instruções** em vez de apenas gerar texto, por exemplo, respondendo a tarefas como um chatbot.
|
||||
> O objetivo desta seção é mostrar como **ajustar um modelo já pré-treinado para seguir instruções** em vez de apenas gerar texto, por exemplo, respondendo a tarefas como um chatbot.
|
||||
|
||||
{{#ref}}
|
||||
7.2.-fine-tuning-to-follow-instructions.md
|
||||
|
Loading…
x
Reference in New Issue
Block a user