LLaMA 3 es uno de los modelos de código abierto más prometedores después de Mistral y resuelve una amplia gama de tareas. Anteriormente escribí un blog en Medium sobre la creación de un LLM con más de 2,3 millones de parámetros desde cero utilizando la arquitectura LLaMA. Ahora que se lanzó LLaMA-3, lo recrearemos de una manera más sencilla.
No usaremos una GPU para este blog, pero necesitarás al menos 17 GB de RAM porque vamos a cargar algunos archivos que tienen un tamaño superior a 15 GB. Si esto es un problema para usted, puede utilizar Kaggle como solución. Como no necesitamos una GPU, Kaggle ofrece 30 GB de RAM mientras usa solo núcleos de CPU como acelerador.
Aquí está el enlace del blog que lo guía sobre cómo crear un LLM de más de 2,3 millones de parámetros desde cero: 2,3+ millones de LLM de parámetros desde cero
Lo bueno es que no usaremos codificación de programación orientada a objetos (POO), simplemente programación Python. Sin embargo, debe tener un conocimiento básico de las redes neuronales y la arquitectura Transformer. Estos son los dos únicos requisitos previos necesarios para seguir el blog.
Tema | Enlace |
---|---|
Teoría del transformador | Enlace de vídeo |
Teoría de las redes neuronales | Enlace de vídeo |
Conceptos básicos de Python | Enlace de vídeo |
Antes de entrar en los detalles técnicos, lo primero que debes saber es que toda la arquitectura de LLaMA 3 es la misma que la de LLaMA 2. Entonces, si aún no has repasado los detalles técnicos de LLaMA 3, no será Un problema para que sigas este blog. Incluso si no comprende la arquitectura de LLaMA 2, no se preocupe, también veremos una descripción general de alto nivel de sus detalles técnicos. Este blog está diseñado para ti de cualquier manera.
Aquí hay algunos puntos clave sobre LLaMA 2 y LLaMA 3. Si ya está familiarizado con su arquitectura:
CARACTERÍSTICA | Llama 3 | Llama 2 |
---|---|---|
Tokenizador | Tiktoken (desarrollado por OpenAI) | FrasePieza |
Número de parámetros | 8B, 70B | 70B, 13B, 7B |
Datos de entrenamiento | fichas de 15T | 2,2 billones de fichas |
Longitud del contexto | 8192 fichas | 4096 fichas |
Mecanismo de atención | Atención de consultas agrupadas | Atención de consultas agrupadas |
Modelos ajustados | Sí | Sí |
Actuación | Mejor que Llama 2 en todos los puntos de referencia | Mejor que Llama 1 en la mayoría de los puntos de referencia |
Requisitos computacionales | Muy alto (modelo 70B) | Muy alto (modelo 70B) |
Disponibilidad | Código abierto | Código abierto |
Aprendizaje reforzado a partir de la retroalimentación humana | Sí | Sí |
Número de idiomas admitidos | 30 idiomas | 20 idiomas |
Adecuado para | Lo mejor para tareas más exigentes, como razonamiento, codificación y pruebas de competencia | Bueno para tareas más exigentes, como razonamiento, codificación y pruebas de competencia. |
Comprender la arquitectura de LLaMA 3 es importante antes de sumergirse en su codificación. Para una mejor comprensión visual, aquí hay un diagrama comparativo entre Vanilla Transformer, LLaMA 2/3 y Mistral.
Veamos los componentes más importantes de LLaMA 3 con un poco más de detalle:
En el enfoque LLaMA 3, que es el mismo que LLaMA 2, se utiliza una técnica llamada RMSNorm para normalizar la entrada de cada subcapa del transformador.
Imagina que estás estudiando para un examen importante y tienes un libro de texto enorme lleno de capítulos. Cada capítulo representa un tema diferente, pero algunos capítulos son más cruciales para comprender el tema que otros. Ahora, antes de sumergirte en todo el libro de texto, decides evaluar la importancia de cada capítulo. No querrás dedicar la misma cantidad de tiempo a cada capítulo; Quieres centrarte más en los críticos. Aquí es donde entra en juego la normalización previa mediante RMSNorm para modelos de lenguaje grandes (LLM) como ChatGPT. Es como asignar un peso a cada capítulo en función de su importancia. Los capítulos que son fundamentales para el tema obtienen mayor peso, mientras que los menos importantes obtienen menor peso.
Entonces, antes de profundizar en el estudio, ajusta su plan de estudio en función de la importancia ponderada de cada capítulo. Asigna más tiempo y esfuerzo a los capítulos con mayor peso, asegurándose de comprender los conceptos básicos a fondo.
De manera similar, la normalización previa utilizando RMSNorm ayuda a los LLM a priorizar qué partes del texto son más críticas para comprender el contexto y el significado. Asigna pesos más altos a los elementos esenciales y pesos más bajos a los menos cruciales, lo que garantiza que el modelo centre su atención donde más se necesita para una comprensión precisa. Los lectores interesados pueden explorar la implementación detallada de RMSNorm aquí.
LLaMA presenta la función de activación SwiGLU, inspirándose en PaLM.
Imagina que eres un profesor que intenta explicar un tema complejo a tus alumnos. Tienes una gran pizarra donde anotas puntos clave y dibujas diagramas para aclarar las cosas. Pero a veces, es posible que su letra no sea muy clara o que sus diagramas no estén perfectamente dibujados. Esto puede dificultar que sus alumnos comprendan el material.
Ahora, imagina si tuvieras un bolígrafo mágico que ajustara automáticamente el tamaño y el estilo de tu escritura según la importancia de cada punto. Si algo es realmente crucial, el bolígrafo lo escribe más grande y claro, haciéndolo destacar. Si es menos importante, el bolígrafo lo escribe en un tamaño más pequeño, pero aún así legible. SwiGLU es como ese bolígrafo mágico para modelos de lenguajes grandes (LLM) como ChatGPT. Antes de generar texto, SwiGLU ajusta la importancia de cada palabra o frase en función de su relevancia para el contexto. Así como el bolígrafo mágico ajusta el tamaño y el estilo de tu escritura, SwiGLU ajusta el énfasis de cada palabra o frase.
Entonces, cuando el LLM genera texto, puede dar más importancia a las partes importantes, haciéndolas más notorias y asegurando que contribuyan más a la comprensión general del texto. De esta manera, SwiGLU ayuda a los LLM a producir textos más claros y fáciles de entender, de forma muy parecida a cómo el bolígrafo mágico le ayuda a crear explicaciones más claras para sus alumnos en la pizarra. Se pueden encontrar más detalles sobre SwiGLU en el artículo asociado.
Rotary Embeddings, o RoPE, es un tipo de incrustación de posición utilizado en LLaMA 3.
Imagine que está en un salón de clases y desea asignar asientos a los estudiantes para discusiones grupales. Por lo general, puede organizar los asientos en filas y columnas, y cada estudiante tendrá una posición fija. Sin embargo, en algunos casos, desea crear una disposición de asientos más dinámica donde los estudiantes puedan moverse e interactuar más libremente.
ROPE es como una disposición especial de asientos que permite a los estudiantes rotar y cambiar de posición mientras mantienen sus posiciones relativas entre sí. En lugar de estar fijos en un lugar, los estudiantes ahora pueden moverse con movimientos circulares, lo que permite interacciones más fluidas.
En este escenario, cada estudiante representa una palabra o ficha en una secuencia de texto y su posición corresponde a su posición en la secuencia. Así como ROPE permite a los estudiantes rotar y cambiar de posición, ROPE permite que las incrustaciones posicionales de palabras en una secuencia de texto cambien dinámicamente en función de sus posiciones relativas entre sí. Entonces, al procesar texto, en lugar de tratar las incrustaciones posicionales como fijas y estáticas, ROPE introduce un aspecto rotacional, lo que permite representaciones más flexibles que capturan las relaciones dinámicas entre las palabras en la secuencia. Esta flexibilidad ayuda a modelos como ChatGPT a comprender y generar mejor texto que fluya naturalmente y mantenga la coherencia, de manera similar a cómo una disposición dinámica de los asientos fomenta discusiones más interactivas en un salón de clases. Aquellos interesados en los detalles matemáticos pueden consultar el artículo de RoPE.
LLaMA 3 utiliza codificación de par de bytes (BPE) de la biblioteca tiktoken introducida por OpenAI, mientras que el tokenizador BPE de LLaMA 2 se basa en la biblioteca de oraciones. Hay una ligera diferencia entre ellos, pero
Primero, aprendamos qué es realmente BPE.
Comencemos con un ejemplo simple. Supongamos que tenemos un corpus de texto con las palabras: "ab", "bc", "bcd" y "cde". Comenzamos inicializando nuestro vocabulario con todos los caracteres individuales en el corpus de texto, por lo que nuestro vocabulario inicial es {"a", "b", "c", "d", "e"}.
A continuación, calculamos la frecuencia de cada carácter en el corpus de texto. Para nuestro ejemplo, las frecuencias son: {"a": 1, "b": 3, "c": 3, "d": 2, "e": 1}.
Ahora comenzamos el proceso de fusión. Repetimos los siguientes pasos hasta que nuestro vocabulario alcance el tamaño deseado:
Primero, encontramos el par de caracteres consecutivos más frecuente. En este caso, el par más frecuente es "bc" con una frecuencia de 2. Luego fusionamos este par para crear una nueva unidad de subpalabra "bc". Después de fusionar, actualizamos los recuentos de frecuencia para reflejar la nueva unidad de subpalabra. La frecuencia actualizada es {"a": 1, "b": 2, "c": 2, "d": 2, "e": 1, "bc": 2}. Agregamos la nueva unidad de subpalabra "bc" a nuestro vocabulario, que ahora se convierte en {"a", "b", "c", "d", "e", "bc"}.
Repetimos el proceso. El siguiente par más frecuente es "cd". Fusionamos "cd" para formar una nueva unidad de subpalabra "cd" y actualizamos los recuentos de frecuencia. La frecuencia actualizada es {"a": 1, "b": 2, "c": 1, "d": 1, "e": 1, "bc": 2, "cd": 2}. Agregamos "cd" al vocabulario, lo que da como resultado {"a", "b", "c", "d", "e", "bc", "cd"}.
Continuando con el proceso, el siguiente par frecuente es "de". Fusionamos "de" para formar la unidad de subpalabra "de" y actualizamos los recuentos de frecuencia a {"a": 1, "b": 2, "c": 1, "d": 1, "e": 0, "bc": 2, "cd": 1, "de": 1}. Agregamos "de" al vocabulario, convirtiéndolo en {"a", "b", "c", "d", "e", "bc", "cd", "de"}.
A continuación encontramos "ab" como el par más frecuente. Fusionamos "ab" para formar la unidad de subpalabra "ab" y actualizamos los recuentos de frecuencia a {"a": 0, "b": 1, "c": 1, "d": 1, "e": 0, "bc": 2, "cd": 1, "de": 1, "ab": 1}.
Agregamos "ab" al vocabulario, que se convierte en {"a", "b", "c", "d", "e", "bc", "cd", "de", "ab"}.
Entonces, el siguiente par frecuente es "bcd". Fusionamos "bcd" para formar la unidad de subpalabra "bcd" y actualizamos los recuentos de frecuencia a {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "bc": 1, "cd": 0, "de": 1, "ab": 1, "bcd": 1}. Agregamos "bcd" al vocabulario, lo que da como resultado {"a", "b", "c", "d", "e", "bc", "cd", "de", "ab", "bcd "}.
Finalmente, el par más frecuente es "cde". Fusionamos "cde" para formar la unidad de subpalabra "cde" y actualizamos los recuentos de frecuencia a {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "bc": 1, "cd": 0, "de": 0, "ab": 1, "bcd": 1, "cde": 1}. Agregamos "cde" al vocabulario, convirtiéndolo en {"a", "b", "c", "d", "e", "bc", "cd", "de", "ab", "bcd ", "cde"}.
Esta técnica puede mejorar el rendimiento de los LLM y manejar palabras raras y fuera de vocabulario. La gran diferencia entre TikToken BPE y BPE de oración es que TikToken BPE no siempre divide las palabras en partes más pequeñas si ya se conoce la palabra completa. Por ejemplo, si "abrazar" está en el vocabulario, permanece como una sola ficha en lugar de dividirse en ["abrazo", "ging"].
Trabajaremos con una pequeña gama de bibliotecas de Python, pero es mejor instalarlas para evitar encontrar errores de "no se encontró ningún módulo".
!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)
Después de instalar las bibliotecas necesarias, necesitamos descargar algunos archivos. Dado que vamos a replicar la arquitectura de llama-3–8B, debes tener una cuenta en HuggingFace. Además, dado que llama-3 es un modelo cerrado, debes aceptar sus términos y condiciones para acceder al contenido del modelo.
Aquí están los pasos:
Una vez que haya completado ambos pasos, ahora tenemos que descargar algunos archivos. Hay dos opciones para hacer eso:
(Opción 1: Manual) Vaya al directorio llama-3–8B HF desde este enlace y descargue manualmente cada uno de estos tres archivos.
(opciones 2: Codificación) Podemos usar la biblioteca hugging_face, que instalamos anteriormente, para descargar todos estos archivos. Sin embargo, primero debemos iniciar sesión en HuggingFace Hub dentro de nuestra computadora portátil de trabajo usando nuestro token HF. Puedes crear un nuevo token o acceder a él desde este enlace.
# 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…
Una vez que ejecute esta celda, le pedirá que ingrese el token. Si hay un error durante el inicio de sesión, vuelva a intentarlo, pero asegúrese de desmarcar agregar token como credencial git. Después de eso, solo necesitamos ejecutar un código Python simple para descargar los tres archivos que son la columna vertebral de la arquitectura 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]
Una vez descargados todos los archivos, debemos importar las bibliotecas que usaremos a lo largo de este 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 continuación, debemos comprender para qué se utilizará cada archivo.
Dado que nuestro objetivo es una replicación exacta de llama-3, significa que nuestro texto de entrada debe producir un resultado significativo. Por ejemplo, si nuestra entrada es "¿de qué color es el sol?", la salida debe ser "blanco". Lograr esto requiere capacitar a nuestro LLM en un gran conjunto de datos, lo que exige una alta potencia de cálculo, lo que lo hace inviable para nosotros.
Sin embargo, Meta ha publicado públicamente sus archivos de arquitectura llama-3, o en términos más complejos, sus pesos previamente entrenados, para su uso. Acabamos de descargar estos archivos, lo que nos permite replicar su arquitectura sin necesidad de capacitación ni de un gran conjunto de datos. Ya está todo preparado, sólo tenemos que utilizar los componentes correctos en los lugares correctos.
Eche un vistazo a cada uno de estos archivos y su importancia:
tokenizer.model: como comentamos anteriormente, LLaMA-3 utiliza el tokenizador Byte Pair Encoding (BPE) de tiktoken, entrenado en un conjunto de datos con 15 billones de tokens, 7 veces más grande que el conjunto de datos utilizado para LLaMA-2. Carguemos este archivo y veamos qué contiene.
# 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
El atributo de longitud muestra el tamaño total del vocabulario, que es el número único de caracteres en los datos de entrenamiento. El tipo de tokenizer_model es un diccionario.
# 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}
Cuando imprimamos 10 elementos aleatorios, verá cadenas que se formaron utilizando el algoritmo BPE, similar al ejemplo que analizamos anteriormente. Las claves representan secuencias de bytes del entrenamiento BPE, mientras que los valores representan rangos de fusión basados en la frecuencia.
consolidado.00.pth: contiene los parámetros aprendidos (pesos) de Llama-3–8B. Estos parámetros incluyen información sobre cómo el modelo entiende y procesa el lenguaje, como por ejemplo cómo representa tokens, calcula la atención, realiza transformaciones de retroalimentación y normaliza sus resultados.
# 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']
Si está familiarizado con la arquitectura del transformador, sabrá sobre consultas, matrices de claves y más. Más adelante, usaremos estas capas/pesos para crear dichas matrices dentro de la arquitectura de Llama-3.
params.json: contiene varios 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}
Estos valores nos ayudarán a replicar la arquitectura Llama-3 especificando detalles como el número de cabezas, la dimensión del vector de incrustación y más.
Guardemos estos valores para poder usarlos más 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" ])
Ahora que tenemos el modelo de tokenizador, el modelo de arquitectura que contiene pesos y parámetros de configuración, comencemos a codificar nuestro propio Llama-3 desde cero.
Lo primero que debemos hacer es convertir nuestro texto de entrada en tokens y, para lograrlo, primero debemos crear algunos tokens especiales que son necesarios para proporcionar marcadores estructurados dentro del texto tokenizado, lo que permite al tokenizador reconocer y manejar condiciones específicas. o instrucciones.
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 continuación, definimos las reglas para dividir el texto en tokens especificando diferentes patrones para que coincidan con varios tipos de subcadenas en el texto de entrada. Así es como podemos hacerlo.
# 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+"
Puede extraer palabras, contracciones, números (hasta tres dígitos) y secuencias de caracteres que no son espacios en blanco del texto de entrada; puede personalizarlo según sus requisitos. Necesitamos codificar una función tokenizadora simple usando TikToken BPE, que requiere tres entradas: tokenizer_model, tokenize_breaker y special_tokens. Esta función codificará/decodificará nuestro texto de entrada en consecuencia.
# 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 que nuestros métodos de función de codificador funcionen correctamente, le pasamos "Hello World". Primero, codifica el texto transformándolo en valores numéricos. Luego, lo decodifica nuevamente en texto, lo que da como resultado "¡hola mundo!". Esto confirma que la función está funcionando correctamente. Tokenicemos nuestra 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 nuestro texto de entrada "la respuesta a la pregunta fundamental de la vida, el universo y todo es" comenzando con una ficha especial.
Si comprobamos la longitud de nuestro vector de entrada, sería:
# checking dimension of input vector and embedding vector from llama-3 architecture
print ( dim , len ( tokens ))
4096 17
Nuestros vectores de entrada, que actualmente tienen una dimensión (17x1), deben transformarse en incrustaciones para cada palabra tokenizada. Esto significa que nuestros tokens (17x1) se convertirán en (17x4096), donde cada token tiene una incrustación correspondiente de longitud 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])
Estas incrustaciones no están normalizadas y tendrán un efecto grave si no las normalizamos. En la siguiente sección, realizaremos la normalización en nuestros vectores de entrada.
Normalizaremos los vectores de entrada usando la misma fórmula que vimos anteriormente para RMSNorm para asegurarnos de que nuestras entradas estén 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 los pesos de atención de Layers_0 para normalizar nuestras incrustaciones no normalizadas. La razón para usar Layer_0 es que ahora estamos creando la primera capa de nuestra arquitectura 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])
Quizás ya sepas que la dimensión no cambiará porque solo estamos normalizando los vectores y nada más.
Primero, carguemos los vectores de consulta, clave, valor y salida del 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])
Las dimensiones indican que los pesos del modelo que descargamos no son para cada cabeza individualmente sino para múltiples cabezas de atención debido a la implementación de un enfoque/entrenamiento paralelo. Sin embargo, podemos desenvolver estas matrices para que estén disponibles para un solo cabezal.
# 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])
Aquí, 32 es el número de cabezas de atención en Llama-3, 128 es el tamaño del vector de consulta y 4096 es el tamaño del token incrustado. Podemos acceder a la matriz de pesos de consulta del primer encabezado de la primera capa 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 el vector de consulta para cada token, multiplicamos los pesos de la consulta por la incrustación del 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])
Los vectores de consulta no conocen inherentemente su posición en el mensaje, por lo que usaremos RoPE para hacérselo saber.
nosotros spl