LLaMA 3 é um dos modelos de código aberto mais promissores depois do Mistral, resolvendo uma ampla gama de tarefas. Anteriormente escrevi um blog no Medium sobre a criação de um LLM com mais de 2,3 milhões de parâmetros do zero usando a arquitetura LLaMA. Agora que o LLaMA-3 foi lançado, iremos recriá-lo de uma forma mais simples.
Não usaremos GPU para este blog, mas você precisará de pelo menos 17 GB de RAM porque carregaremos alguns arquivos com mais de 15 GB. Se isso for um problema para você, você pode usar o Kaggle como solução. Como não precisamos de GPU, o Kaggle oferece 30 GB de RAM usando apenas núcleos de CPU como acelerador.
Aqui está o link do blog que orienta você sobre como criar um LLM de mais de 2,3 milhões de parâmetros do zero: LLM de mais de 2,3 milhões de parâmetros do zero
A parte boa é que não usaremos codificação de programação orientada a objetos (OOP), apenas programação Python simples. No entanto, você deve ter um conhecimento básico de redes neurais e da arquitetura do Transformer. Esses são os únicos dois pré-requisitos necessários para acompanhar o blog.
Tópico | Link |
---|---|
Teoria do Transformador | Link do vídeo |
Teoria das Redes Neurais | Link do vídeo |
Noções básicas de Python | Link do vídeo |
Antes de entrar nos detalhes técnicos, a primeira coisa que você deve saber é que toda a arquitetura do LLaMA 3 é igual à do LLaMA 2. Então, se você ainda não passou pelos detalhes técnicos do LLaMA 3, não será um problema para você acompanhar este blog. Mesmo que você não entenda a arquitetura do LLaMA 2, não se preocupe, também veremos uma visão geral de alto nível de seus detalhes técnicos. Este blog foi projetado para você de qualquer maneira.
Aqui estão alguns pontos-chave sobre o LLaMA 2 e o LLaMA 3. Se você já está familiarizado com sua arquitetura:
RECURSO | Lhama 3 | Lhama 2 |
---|---|---|
Tokenizador | Tiktoken (desenvolvido pela OpenAI) | Peça de frase |
Número de parâmetros | 8B, 70B | 70B, 13B, 7B |
Dados de treinamento | Tokens 15T | Tokens 2.2T |
Comprimento do contexto | 8.192 fichas | 4.096 fichas |
Mecanismo de Atenção | Atenção de consulta agrupada | Atenção de consulta agrupada |
Modelos ajustados | Sim | Sim |
Desempenho | Melhor que Llama 2 em todos os benchmarks | Melhor que o Llama 1 na maioria dos benchmarks |
Requisitos computacionais | Muito alto (modelo 70B) | Muito alto (modelo 70B) |
Disponibilidade | Código aberto | Código aberto |
Aprendizagem por reforço com feedback humano | Sim | Sim |
Número de idiomas suportados | 30 idiomas | 20 idiomas |
Adequado para | Melhor para tarefas mais exigentes, como raciocínio, codificação e testes de proficiência | Bom para tarefas mais exigentes, como raciocínio, codificação e testes de proficiência |
Compreender a arquitetura do LLaMA 3 é importante antes de mergulhar na codificação. Para uma melhor compreensão visual, aqui está um diagrama de comparação entre o vanilla Transformer, LLaMA 2/3 e Mistral.
Vejamos os componentes mais importantes do LLaMA 3 com um pouco mais de detalhes:
Na abordagem LLaMA 3, que é igual ao LLaMA 2, uma técnica chamada RMSNorm é usada para normalizar a entrada de cada subcamada do transformador.
Imagine que você está estudando para uma prova importante e tem um livro enorme cheio de capítulos. Cada capítulo representa um tópico diferente, mas alguns capítulos são mais cruciais para a compreensão do assunto do que outros. Agora, antes de mergulhar no livro inteiro, você decide avaliar a importância de cada capítulo. Você não quer gastar a mesma quantidade de tempo em cada capítulo; você deseja se concentrar mais nos críticos. É aqui que a pré-normalização usando RMSNorm entra em ação para grandes modelos de linguagem (LLMs) como ChatGPT. É como atribuir um peso a cada capítulo com base no seu significado. Os capítulos fundamentais para o assunto recebem pesos maiores, enquanto os menos importantes recebem pesos menores.
Portanto, antes de se aprofundar nos estudos, você ajusta seu plano de estudo com base na importância ponderada de cada capítulo. Você aloca mais tempo e esforço para os capítulos com pesos mais altos, garantindo a compreensão completa dos conceitos básicos.
Da mesma forma, a pré-normalização usando RMSNorm ajuda os LLMs a priorizar quais partes do texto são mais críticas para a compreensão do contexto e do significado. Ele atribui pesos mais altos aos elementos essenciais e pesos mais baixos aos menos cruciais, garantindo que o modelo concentre sua atenção onde é mais necessário para uma compreensão precisa. Os leitores interessados podem explorar a implementação detalhada do RMSNorm aqui.
LLaMA apresenta a função de ativação SwiGLU, inspirada no PaLM.
Imagine que você é um professor tentando explicar um assunto complexo aos seus alunos. Você tem um grande quadro branco onde anota os pontos-chave e desenha diagramas para tornar as coisas mais claras. Mas às vezes, sua caligrafia pode não ser muito clara ou seus diagramas podem não estar perfeitamente desenhados. Isso pode dificultar a compreensão do material pelos alunos.
Agora, imagine se você tivesse uma caneta mágica que ajustasse automaticamente o tamanho e o estilo da sua caligrafia com base na importância de cada ponto. Se algo é realmente crucial, a caneta escreve de forma maior e mais clara, destacando-o. Se for menos importante, a caneta escreve em tamanho menor, mas ainda legível. SwiGLU é como aquela caneta mágica para grandes modelos de linguagem (LLMs) como ChatGPT. Antes de gerar o texto, o SwiGLU ajusta a importância de cada palavra ou frase com base na sua relevância para o contexto. Assim como a caneta mágica ajusta o tamanho e o estilo da sua escrita, o SwiGLU ajusta a ênfase de cada palavra ou frase.
Assim, quando o LLM gera texto, pode dar mais destaque às partes importantes, tornando-as mais perceptíveis e garantindo que contribuam mais para a compreensão geral do texto. Dessa forma, o SwiGLU ajuda os LLMs a produzir textos mais claros e fáceis de entender, da mesma forma que a caneta mágica ajuda a criar explicações mais claras para seus alunos no quadro branco. Mais detalhes sobre o SwiGLU podem ser encontrados no artigo associado.
Rotary Embeddings, ou RoPE, é um tipo de incorporação de posição usado no LLaMA 3.
Imagine que você está em uma sala de aula e deseja atribuir vagas aos alunos para discussões em grupo. Normalmente, você pode organizar os assentos em fileiras e colunas, com cada aluno tendo uma posição fixa. No entanto, em alguns casos, você deseja criar uma disposição de assentos mais dinâmica, onde os alunos possam se movimentar e interagir com mais liberdade.
ROPE é como um arranjo especial de assentos que permite aos alunos girar e mudar de posição enquanto mantêm suas posições relativas uns aos outros. Em vez de ficarem fixos em um lugar, os alunos agora podem se movimentar em movimentos circulares, permitindo interações mais fluidas.
Neste cenário, cada aluno representa uma palavra ou token numa sequência de texto, e a sua posição corresponde à sua posição na sequência. Assim como o ROPE permite que os alunos girem e mudem de posição, o ROPE permite que as incorporações posicionais de palavras em uma sequência de texto mudem dinamicamente com base em suas posições relativas entre si. Assim, ao processar texto, em vez de tratar os embeddings posicionais como fixos e estáticos, o ROPE introduz um aspecto rotacional, permitindo representações mais flexíveis que capturam as relações dinâmicas entre as palavras na sequência. Essa flexibilidade ajuda modelos como o ChatGPT a compreender e gerar melhor texto que flui naturalmente e mantém a coerência, semelhante a como uma disposição dinâmica dos assentos promove discussões mais interativas em uma sala de aula. Os interessados nos detalhes matemáticos podem consultar o artigo RoPE.
LLaMA 3 usa Byte Pair Encoding (BPE) da biblioteca tiktoken introduzida pela OpenAI, enquanto o tokenizer LLaMA 2 BPE é baseado na biblioteca de sentenças. Há uma pequena diferença entre eles, mas
primeiro, vamos aprender o que realmente é o BPE.
Vamos começar com um exemplo simples. Suponha que temos um corpus de texto com as palavras: “ab”, “bc”, “bcd” e “cde”. Começamos inicializando nosso vocabulário com todos os caracteres individuais no corpus do texto, então nosso vocabulário inicial é {"a", "b", "c", "d", "e"}.
A seguir, calculamos a frequência de cada caractere do corpus do texto. Para nosso exemplo, as frequências são: {"a": 1, "b": 3, "c": 3, "d": 2, "e": 1}.
Agora, iniciamos o processo de fusão. Repetimos os seguintes passos até que nosso vocabulário atinja o tamanho desejado:
Primeiro, encontramos o par de caracteres consecutivos mais frequente. Nesse caso, o par mais frequente é "bc" com frequência 2. Em seguida, mesclamos esse par para criar uma nova unidade de subpalavra "bc". Após a fusão, atualizamos as contagens de frequência para refletir a nova unidade de subpalavra. A frequência atualizada é {"a": 1, "b": 2, "c": 2, "d": 2, "e": 1, "bc": 2}. Adicionamos a nova unidade de subpalavra "bc" ao nosso vocabulário, que agora se torna {"a", "b", "c", "d", "e", "bc"}.
Repetimos o processo. O próximo par mais frequente é “cd”. Mesclamos "cd" para formar uma nova unidade de subpalavra "cd" e atualizamos as contagens de frequência. A frequência atualizada é {"a": 1, "b": 2, "c": 1, "d": 1, "e": 1, "bc": 2, "cd": 2}. Adicionamos "cd" ao vocabulário, resultando em {"a", "b", "c", "d", "e", "bc", "cd"}.
Continuando o processo, o próximo par frequente é “de”. Mesclamos "de" para formar a unidade de subpalavra "de" e atualizamos as contagens de frequência para {"a": 1, "b": 2, "c": 1, "d": 1, "e": 0, "bc": 2, "cd": 1, "de": 1}. Adicionamos "de" ao vocabulário, tornando-o {"a", "b", "c", "d", "e", "bc", "cd", "de"}.
A seguir, encontramos “ab” como o par mais frequente. Mesclamos "ab" para formar a unidade de subpalavra "ab" e atualizamos as contagens de frequência para {"a": 0, "b": 1, "c": 1, "d": 1, "e": 0, "bc": 2, "cd": 1, "de": 1, "ab": 1}.
Adicionamos "ab" ao vocabulário, que se torna {"a", "b", "c", "d", "e", "bc", "cd", "de", "ab"}.
Então, o próximo par frequente é “bcd”. Mesclamos "bcd" para formar a unidade de subpalavra "bcd" e atualizamos as contagens de frequência para {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "bc": 1, "cd": 0, "de": 1, "ab": 1, "bcd": 1}. Adicionamos "bcd" ao vocabulário, resultando em {"a", "b", "c", "d", "e", "bc", "cd", "de", "ab", "bcd "}.
Finalmente, o par mais frequente é “cde”. Mesclamos "cde" para formar a unidade de subpalavra "cde" e atualizamos as contagens de frequência para {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "bc": 1, "cd": 0, "de": 0, "ab": 1, "bcd": 1, "cde": 1}. Adicionamos "cde" ao vocabulário, tornando-o {"a", "b", "c", "d", "e", "bc", "cd", "de", "ab", "bcd ", "cde"}.
Esta técnica pode melhorar o desempenho dos LLMs e lidar com palavras raras e fora do vocabulário. A grande diferença entre o TikToken BPE e o sentença BPE é que o TikToken BPE nem sempre divide as palavras em partes menores se a palavra inteira já for conhecida. Por exemplo, se "abraçar" estiver no vocabulário, ele permanecerá como um token em vez de se dividir em ["abraçar","ging"].
Trabalharemos com uma pequena variedade de bibliotecas Python, mas é melhor instalá-las para evitar erros de “nenhum módulo encontrado”.
!p ip install sentencepiece tiktoken torch blobfile matplotlib huggingface_hub
Requirement already satisfied: sentencepiece in /opt/conda/lib/python3.10/site-packages (0.2.0)
Requirement already satisfied: tiktoken in /opt/conda/lib/python3.10/site-packages (0.7.0)
Requirement already satisfied: torch in /opt/conda/lib/python3.10/site-packages (2.1.2+cpu)
Requirement already satisfied: blobfile in /opt/conda/lib/python3.10/site-packages (2.1.1)
Requirement already satisfied: matplotlib in /opt/conda/lib/python3.10/site-packages (3.7.5)
Requirement already satisfied: huggingface_hub in /opt/conda/lib/python3.10/site-packages (0.22.2)
Requirement already satisfied: regex>=2022.1.18 in /opt/conda/lib/python3.10/site-packages (from tiktoken) (2023.12.25)
Requirement already satisfied: requests>=2.26.0 in /opt/conda/lib/python3.10/site-packages (from tiktoken) (2.31.0)
Requirement already satisfied: filelock in /opt/conda/lib/python3.10/site-packages (from torch) (3.13.1)
Requirement already satisfied: typing-extensions in /opt/conda/lib/python3.10/site-packages (from torch) (4.9.0)
Requirement already satisfied: sympy in /opt/conda/lib/python3.10/site-packages (from torch) (1.12)
Requirement already satisfied: networkx in /opt/conda/lib/python3.10/site-packages (from torch) (3.2.1)
Requirement already satisfied: jinja2 in /opt/conda/lib/python3.10/site-packages (from torch) (3.1.2)
Requirement already satisfied: fsspec in /opt/conda/lib/python3.10/site-packages (from torch) (2024.2.0)
Requirement already satisfied: pycryptodomex~=3.8 in /opt/conda/lib/python3.10/site-packages (from blobfile) (3.20.0)
Requirement already satisfied: urllib3<3,>=1.25.3 in /opt/conda/lib/python3.10/site-packages (from blobfile) (1.26.18)
Requirement already satisfied: lxml~=4.9 in /opt/conda/lib/python3.10/site-packages (from blobfile) (4.9.4)
Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (1.2.0)
Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (4.47.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (1.4.5)
Requirement already satisfied: numpy<2,>=1.20 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (1.26.4)
Requirement already satisfied: packaging>=20.0 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (21.3)
Requirement already satisfied: pillow>=6.2.0 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (9.5.0)
Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (3.1.1)
Requirement already satisfied: python-dateutil>=2.7 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (2.9.0.post0)
Requirement already satisfied: pyyaml>=5.1 in /opt/conda/lib/python3.10/site-packages (from huggingface_hub) (6.0.1)
Requirement already satisfied: tqdm>=4.42.1 in /opt/conda/lib/python3.10/site-packages (from huggingface_hub) (4.66.1)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /opt/conda/lib/python3.10/site-packages (from requests>=2.26.0->tiktoken) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.10/site-packages (from requests>=2.26.0->tiktoken) (3.6)
Requirement already satisfied: certifi>=2017.4.17 in /opt/conda/lib/python3.10/site-packages (from requests>=2.26.0->tiktoken) (2024.2.2)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.10/site-packages (from jinja2->torch) (2.1.3)
Requirement already satisfied: mpmath>=0.19 in /opt/conda/lib/python3.10/site-packages (from sympy->torch) (1.3.0)
Após instalar as bibliotecas necessárias, precisamos baixar alguns arquivos. Como vamos replicar a arquitetura do llama-3–8B, você deve ter uma conta no HuggingFace. Além disso, como o llama-3 é um modelo fechado, você deve aceitar os termos e condições para acessar o conteúdo do modelo.
Aqui estão as etapas:
Depois de concluir essas duas etapas, agora temos que baixar alguns arquivos. Existem duas opções para fazer isso:
(Opção 1: Manual) Vá para o diretório llama-3–8B HF neste link e baixe manualmente cada um desses três arquivos.
(opções 2: Codificação) Podemos usar a biblioteca hugging_face, que instalamos anteriormente, para baixar todos esses arquivos. No entanto, primeiro, precisamos fazer login no HuggingFace Hub em nosso notebook de trabalho usando nosso token HF. Você pode criar um novo token ou acessá-lo neste link.
# Import the `notebook_login` function from the `huggingface_hub` module.
from huggingface_hub import notebook_login
# Execute the `notebook_login` function to log in to the Hugging Face Hub.
notebook_login ()
VBox(children=(HTML(value='<center> <imgnsrc=https://huggingface.co/front/assets/huggingface_logo-noborder.sv…
Depois de executar esta célula, será solicitado que você insira o token. Se houver um erro durante o login, tente novamente, mas certifique-se de desmarcar adicionar token como credencial git. Depois disso, só precisamos executar um código Python simples para baixar os três arquivos que são a espinha dorsal da arquitetura llama-3–8B.
# Import the necessary function from the huggingface_hub library
from huggingface_hub import hf_hub_download
# Define the repository information
repo_id = "meta-llama/Meta-Llama-3-8B"
subfolder = "original" # Specify the subfolder within the repository
# List of filenames to download
filenames = [ "params.json" , "tokenizer.model" , "consolidated.00.pth" ]
# Specify the directory where you want to save the downloaded files
save_directory = "llama-3-8B/" # Replace with your desired path
# Download each file
for filename in filenames :
hf_hub_download (
repo_id = repo_id , # Repository ID
filename = filename , # Name of the file to download
subfolder = subfolder , # Subfolder within the repository
local_dir = save_directory # Directory to save the downloaded file
)
original/params.json: 0%| | 0.00/211 [00:00<?, ?B/s]
original/tokenizer.model: 0%| | 0.00/2.18M [00:00<?, ?B/s]
original/consolidated.00.pth: 0%| | 0.00/16.1G [00:00<?, ?B/s]
Depois que todos os arquivos forem baixados, precisamos importar as bibliotecas que usaremos ao longo deste blog.
# File system paths
from pathlib import Path
# Tokenization library
import tiktoken
# BPE loading function
from tiktoken . load import load_tiktoken_bpe
# PyTorch library
import torch
# JSON handling
import json
# Plotting library
import matplotlib . pyplot as plt
A seguir, precisamos entender para que será utilizado cada arquivo.
Como pretendemos uma replicação exata do lhama-3, isso significa que nosso texto de entrada deve produzir uma saída significativa. Por exemplo, se nossa entrada for “qual é a cor do sol?”, a saída deverá ser “branca”. Conseguir isso requer treinar nosso LLM em um grande conjunto de dados, o que exige alto poder computacional, tornando-o inviável para nós.
No entanto, Meta divulgou publicamente seus arquivos de arquitetura lhama-3, ou em termos mais complexos, seus pesos pré-treinados, para uso. Acabamos de baixar esses arquivos, o que nos permite replicar sua arquitetura sem a necessidade de treinamento ou de um grande conjunto de dados. Já está tudo preparado, só temos que usar os componentes certos nos lugares certos.
Dê uma olhada em cada um desses arquivos e sua importância:
tokenizer.model - Como discutimos anteriormente, o LLaMA-3 usa o tokenizer Byte Pair Encoding (BPE) do tiktoken, treinado em um conjunto de dados com 15 trilhões de tokens - 7 vezes maior que o conjunto de dados usado para LLaMA-2. Vamos carregar este arquivo e ver o que ele contém.
# Loading the tokenizer from llama-3-8B
tokenizer_model = load_tiktoken_bpe ( "/kaggle/working/llama-3-8B/original/tokenizer.model" )
# Get the length of the tokenizer model
len ( tokenizer_model )
# OUTPUT: 128000
# Get the type of the `tokenizer_model` object.
type ( tokenizer_model )
# OUTPUT: dictionary
dict
O atributo length mostra o tamanho total do vocabulário, que é o número exclusivo de caracteres nos dados de treinamento. O tipo de tokenizer_model é um dicionário.
# Printing the first 10 items of tokenizer model
dict ( list ( tokenizer_model . items ())[ 5600 : 5610 ])
{b'mitted': 5600,
b" $('#": 5601,
b' saw': 5602,
b' approach': 5603,
b'ICE': 5604,
b' saying': 5605,
b' anyone': 5606,
b'meta': 5607,
b'SD': 5608,
b' song': 5609}
Quando imprimirmos 10 itens aleatórios dele, você verá strings que foram formadas usando o algoritmo BPE, semelhante ao exemplo que discutimos anteriormente. Chaves que representam sequências de bytes do treinamento BPE, enquanto os valores representam classificações de mesclagem com base na frequência.
consolidado.00.pth - contém os parâmetros aprendidos (pesos) do Llama-3–8B. Esses parâmetros incluem informações sobre como o modelo entende e processa a linguagem, como representa tokens, calcula a atenção, realiza transformações feed-forward e normaliza suas saídas.
# Loading a PyTorch model of LLaMA-3-8B
model = torch . load ( "/kaggle/working/llama-3-8B/original/consolidated.00.pth" )
# printing first 11 layers of the architecture
list ( model . keys ())[: 11 ]
['tok_embeddings.weight',
'layers.0.attention.wq.weight',
'layers.0.attention.wk.weight',
'layers.0.attention.wv.weight',
'layers.0.attention.wo.weight',
'layers.0.feed_forward.w1.weight',
'layers.0.feed_forward.w3.weight',
'layers.0.feed_forward.w2.weight',
'layers.0.attention_norm.weight',
'layers.0.ffn_norm.weight',
'layers.1.attention.wq.weight']
Se você estiver familiarizado com a arquitetura do transformador, saberá sobre consultas, matrizes-chave e muito mais. Posteriormente, usaremos essas camadas/pesos para criar tais matrizes dentro da arquitetura do Llama-3.
params.json- contém vários valores de parâmetros, como:
# Opening the parameters JSON file
with open ( "/kaggle/working/llama-3-8B/original/params.json" , "r" ) as f :
config = json . load ( f )
# Printing the content
print ( config )
{'dim': 4096, 'n_layers': 32, 'n_heads': 32, 'n_kv_heads': 8, 'vocab_size': 128256, 'multiple_of': 1024, 'ffn_dim_multiplier': 1.3, 'norm_eps': 1e-05, 'rope_theta': 500000.0}
Esses valores nos ajudarão a replicar a arquitetura Llama-3, especificando detalhes como o número de cabeças, a dimensão do vetor de incorporação e muito mais.
Vamos armazenar esses valores para podermos usá-los mais tarde.
# Dimension
dim = config [ "dim" ]
# Layers
n_layers = config [ "n_layers" ]
# Heads
n_heads = config [ "n_heads" ]
# KV_heads
n_kv_heads = config [ "n_kv_heads" ]
# Vocabulary
vocab_size = config [ "vocab_size" ]
# Multiple
multiple_of = config [ "multiple_of" ]
# Multiplier
ffn_dim_multiplier = config [ "ffn_dim_multiplier" ]
# Epsilon
norm_eps = config [ "norm_eps" ]
# RoPE
rope_theta = torch . tensor ( config [ "rope_theta" ])
Agora que temos o modelo tokenizer, o modelo de arquitetura contendo pesos e parâmetros de configuração, vamos começar a codificar nosso próprio Llama-3 do zero.
A primeira coisa que precisamos fazer é converter nosso texto de entrada em tokens e, para conseguir isso, primeiro precisamos criar alguns tokens especiais que são necessários para fornecer marcadores estruturados dentro do texto tokenizado, permitindo que o tokenizer reconheça e lide com condições específicas ou instruções.
special_tokens = [
"<|begin_of_text|>" , # Marks the beginning of a text sequence.
"<|end_of_text|>" , # Marks the end of a text sequence.
"<|reserved_special_token_0|>" , # Reserved for future use.
"<|reserved_special_token_1|>" , # Reserved for future use.
"<|reserved_special_token_2|>" , # Reserved for future use.
"<|reserved_special_token_3|>" , # Reserved for future use.
"<|start_header_id|>" , # Indicates the start of a header ID.
"<|end_header_id|>" , # Indicates the end of a header ID.
"<|reserved_special_token_4|>" , # Reserved for future use.
"<|eot_id|>" , # Marks the end of a turn (in a conversational context).
] + [ f"<|reserved_special_token_ { i } |>" for i in range ( 5 , 256 - 5 )] # A large set of tokens reserved for future use.
A seguir, definimos as regras para dividir o texto em tokens, especificando diferentes padrões para corresponder a vários tipos de substrings no texto de entrada. Veja como podemos fazer isso.
# patterns based on which text will be break into tokens
tokenize_breaker = r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^rnp{L}p{N}]?p{L}+|p{N}{1,3}| ?[^sp{L}p{N}]+[rn]*|s*[rn]+|s+(?!S)|s+"
Ele pode extrair palavras, contrações, números (até três dígitos) e sequências de caracteres que não sejam espaços em branco do texto de entrada, você pode personalizá-lo com base em suas necessidades. Precisamos codificar uma função tokenizer simples usando o TikToken BPE, que recebe três entradas: tokenizer_model, tokenize_breaker e special_tokens. Esta função irá codificar/decodificar nosso texto de entrada de acordo.
# Initialize tokenizer with specified parameters
tokenizer = tiktoken . Encoding (
# make sure to set path to tokenizer.model file
name = "/kaggle/working/llama-3-8B/original/tokenizer.model" ,
# Define tokenization pattern string
pat_str = tokenize_breaker ,
# Assign BPE mergeable ranks from tokenizer_model of LLaMA-3
mergeable_ranks = tokenizer_model ,
# Set special tokens with indices
special_tokens = { token : len ( tokenizer_model ) + i for i , token in enumerate ( special_tokens )},
)
# Encode "hello world!" and decode tokens to string
tokenizer . decode ( tokenizer . encode ( "hello world!" ))
'hello world!'
Para verificar se nossos métodos de função do codificador funcionam corretamente, passamos "Hello World" para ele. Primeiro, codifica o texto, transformando-o em valores numéricos. Em seguida, ele decodifica de volta para texto, resultando em "olá mundo!". Isso confirma que a função está funcionando corretamente. Vamos tokenizar nossa entrada.
# input prompt
prompt = "the answer to the ultimate question of life, the universe, and everything is "
# Encode the prompt using the tokenizer and prepend a special token (128000)
tokens = [ 128000 ] + tokenizer . encode ( prompt )
print ( tokens ) # Print the encoded tokens
# Convert the list of tokens into a PyTorch tensor
tokens = torch . tensor ( tokens )
# Decode each token back into its corresponding string
prompt_split_as_tokens = [ tokenizer . decode ([ token . item ()]) for token in tokens ]
print ( prompt_split_as_tokens ) # Print the decoded tokens
[128000, 1820, 4320, 311, 279, 17139, 3488, 315, 2324, 11, 279, 15861, 11, 323, 4395, 374, 220]
['<|begin_of_text|>', 'the', ' answer', ' to', ' the', ' ultimate', ' question', ' of', ' life', ',', ' the', ' universe', ',', ' and', ' everything', ' is', ' ']
Codificamos nosso texto de entrada “a resposta à questão fundamental da vida, do universo e de tudo mais” começando com um token especial.
Se verificarmos o comprimento do nosso vetor de entrada, seria:
# checking dimension of input vector and embedding vector from llama-3 architecture
print ( dim , len ( tokens ))
4096 17
Nossos vetores de entrada, que atualmente têm dimensão (17x1), precisam ser transformados em embeddings para cada palavra tokenizada. Isso significa que nossos tokens (17x1) se tornarão (17x4096), onde cada token terá uma incorporação correspondente de comprimento 4096.
# Define embedding layer with vocab size and embedding dimension
embedding_layer = torch . nn . Embedding ( vocab_size , dim )
# Copy pre-trained token embeddings to the embedding layer
embedding_layer . weight . data . copy_ ( model [ "tok_embeddings.weight" ])
# Get token embeddings for given tokens, converting to torch.bfloat16 format
token_embeddings_unnormalized = embedding_layer ( tokens ). to ( torch . bfloat16 )
# Print shape of resulting token embeddings
token_embeddings_unnormalized . shape
torch.Size([17, 4096])
Essas incorporações não são normalizadas e terão um efeito sério se não as normalizarmos. Na próxima seção, realizaremos a normalização em nossos vetores de entrada.
Normalizaremos os vetores de entrada usando a mesma fórmula que vimos anteriormente para RMSNorm para garantir que nossas entradas sejam normalizadas.
# Calculating RMSNorm
def rms_norm ( tensor , norm_weights ):
# Calculate the mean of the square of tensor values along the last dimension
squared_mean = tensor . pow ( 2 ). mean ( - 1 , keepdim = True )
# Add a small value to avoid division by zero
normalized = torch . rsqrt ( squared_mean + norm_eps )
# Multiply normalized tensor by the provided normalization weights
return ( tensor * normalized ) * norm_weights
Usaremos os pesos de atenção de camadas_0 para normalizar nossos embeddings não normalizados. A razão para usar layer_0 é que agora estamos criando a primeira camada de nossa arquitetura de transformador LLaMA-3.
# using RMS normalization and provided normalization weights
token_embeddings = rms_norm ( token_embeddings_unnormalized ,
model [ "layers.0.attention_norm.weight" ])
# Print the shape of the resulting token embeddings
token_embeddings . shape
torch.Size([17, 4096])
Você já deve saber que a dimensão não mudará porque estamos apenas normalizando os vetores e nada mais.
primeiro, vamos carregar os vetores de consulta, chave, valor e saída do modelo.
# Print the shapes of different weights
print (
# Query weight shape
model [ "layers.0.attention.wq.weight" ]. shape ,
# Key weight shape
model [ "layers.0.attention.wk.weight" ]. shape ,
# Value weight shape
model [ "layers.0.attention.wv.weight" ]. shape ,
# Output weight shape
model [ "layers.0.attention.wo.weight" ]. shape
)
torch.Size([4096, 4096]) torch.Size([1024, 4096]) torch.Size([1024, 4096]) torch.Size([4096, 4096])
As dimensões indicam que os pesos do modelo que baixamos não são para cada cabeça individualmente, mas para múltiplas cabeças de atenção devido à implementação de uma abordagem/treinamento paralelo. No entanto, podemos desembrulhar estas matrizes para disponibilizá-las apenas para uma única cabeça.
# Retrieve query weight for the first layer of attention
q_layer0 = model [ "layers.0.attention.wq.weight" ]
# Calculate dimension per head
head_dim = q_layer0 . shape [ 0 ] // n_heads
# Reshape query weight to separate heads
q_layer0 = q_layer0 . view ( n_heads , head_dim , dim )
# Print the shape of the reshaped query weight tensor
q_layer0 . shape
torch.Size([32, 128, 4096])
Aqui, 32 é o número de cabeças de atenção no Llama-3, 128 é o tamanho do vetor de consulta e 4096 é o tamanho da incorporação do token. Podemos acessar a matriz de pesos de consulta do primeiro cabeçalho da primeira camada usando:
# Extract the query weight for the first head of the first layer of attention
q_layer0_head0 = q_layer0 [ 0 ]
# Print the shape of the extracted query weight tensor for the first head
q_layer0_head0 . shape
torch.Size([128, 4096])
Para encontrar o vetor de consulta para cada token, multiplicamos os pesos da consulta pela incorporação do token.
# Matrix multiplication: token embeddings with transpose of query weight for first head
q_per_token = torch . matmul ( token_embeddings , q_layer0_head0 . T )
# Shape of resulting tensor: queries per token
q_per_token . shape
torch.Size([17, 128])
Os vetores de consulta não sabem inerentemente sua posição no prompt, então usaremos RoPE para alertá-los sobre isso.
Nós nos divertimos