NanoGPT : Desglosando el cΓ³digo de prepare.py del

GPT-2, fue entrenado simplemente para predecir la siguiente palabra en 40GB de texto de internet

Hace unos dΓ­as Andrej Karphaty publicΓ³ el proyecto nanoGPT, asΓ­ que intentΓ© reproducirlo en mi portΓ‘til. Sin embargo, durante el proceso de replicaciΓ³n me fallaron muchas cosas. DespuΓ©s de leer el cΓ³digo, me di cuenta de que tenΓ­a muchas lagunas conceptuales. Por lo tanto, decidΓ­ desglosar la primera parte del proyecto que consiste en descargar el dataset, tokenizarlo, hacer un memmap y generar los binarios train.bin y val.bin.

πŸ’‘ Importante:

Descartar replicar este proyecto en Windows, porque Pytorch 2.0 no tiene soporte para este SO y si puedes tambiΓ©n evita WSL.

https://github.com/pytorch/pytorch/issues/90768

Instalar Python 3.9 como mΓ­nimo y en caso no tengas una GPU te recomiendo LambdaLabs

Empecemos

En la versiΓ³n original de prepare.py se importan las librerΓ­as tqdm, numpy, tiktoken y datasets. Sin embargo, yo en mi proceso de fortalecer los conceptos, importΓ© estas dos funciones load_dataset_builder() y get_dataset_split_names() de manera adicional.

from tqdm import tqdm
import numpy as np
import tiktoken
from datasets import load_dataset, load_dataset_builder, get_dataset_split_names

LibrerΓ­as:

πŸ—ƒοΈ tqdm : se usa para mostrar una barra de progreso durante iteraciones largas

πŸ—ƒοΈ numpy: servirΓ‘ para trabajar con las matrices de forma eficiente

πŸ—ƒοΈ tiktoken: es el tokenizador opensource mΓ‘s rΓ‘pido y lo liberΓ³ OpenAI

πŸ—ƒοΈ datasets: es la librerΓ­a creada por Hugging Face que facilita el acceso a datasets populares de una manera sencilla

MΓ‘s adelante se aplicarΓ‘ la funciΓ³n β€˜map()’ al dataset y se le proporcionarΓ‘ el parΓ‘metro β€˜num_proc’ para definir el nΓΊmero de procesos que se ejecutarΓ‘n en simultΓ‘neo.

# un buen numero a usar es aproximadamente = CPU cores // 2
num_proc = 8

Antes de descargar y empezar a manipular el dataset de openwebtext, es importante inspeccionarlo y obtener informaciΓ³n sobre este dataset, como su descripciΓ³n, caracterΓ­sticas, size, etc. Para hacer esto, se usa la funciΓ³n ’load_dataset_builder()’

ds_builder = load_dataset_builder("openwebtext")

print("DescripciΓ³n de OWT: \n", ds_builder.info.description, "\n")
print("Features de OWT: \n", ds_builder.info.features, "\n")
print("Cita de OWT: \n", ds_builder.info.citation, "\n")
print("Sitio Web de OWT: \n", ds_builder.info.homepage, "\n")

A partir de ahora nos referiremos a OpenWebText como OWT.

Para listar los subconjuntos [’train’, β€˜validation’, β€˜test’] de OWT usamos la funciΓ³n β€˜get_dataset_split_names()’. Este dataset por defecto sΓ³lo contiene β€˜train’.

print("--Subconjuntos--", get_dataset_split_names("openwebtext"))

Luego de haber inspeccionado y entendido mΓ‘s sobre el dataset de OWT, procedemos a descargarlo, este proceso se hace con la funciΓ³n ’load_dataset()’ y acΓ‘ te recomiendo que tengas un como mΓ­nimo 200GB de espacio libre en disco, que conectes tu cable ethernet al portΓ‘til y te vayas por un cafΓ© porque son 8M de documentos o 54GB que se irΓ‘n almacenando en $HOME/.cache/huggingface/datasets/openwebtext/plain_text/1.0.0/…

dataset = load_dataset("openwebtext")
print("---Todos los subconjuntos y features---", dataset)

Con la funciΓ³n train_test_split() dividimos nuestro dataset en dos partes, uno llamado β€˜train’ y otro β€˜test’, el subconjunto β€˜test’ equivale al 0.05% de OWT

split_dataset = dataset["train"].train_test_split(
test_size=0.0005,
seed=2357,
shuffle=True
)

El contenido de la variable split_dataset serΓ­a este:

DatasetDict({
    train: Dataset({
       features: ['text'],
        num_rows: 8009762
    })
    test: Dataset({
        features: ['text'],
        num_rows: 4007
    })
})

Ahora, lo que se busca es tener un subconjunto de validaciΓ³n (’val’), para ello usamos la funciΓ³n pop() para transferir el contenido del subconjunto β€˜test’ a β€˜val’, y luego eliminar β€˜test’ del conjunto original.

split_dataset['val'] = split_dataset.pop('test') # renombramos test como val

Finalmente el dataset de OWT se quedarΓ­a asΓ­:

# mostramos en consola ambos dataset train y val
print(split_dataset)

El resultado de este print a split_dataset serΓ‘: 
DatasetDict({
    train: Dataset({
        features: ['text'],
        num_rows: 8009762
    })
    val: Dataset({
        features: ['text'],
        num_rows: 4007
    })
})

En el siguiente segmento se inicia el proceso de tokenizado del dataset, pero primero se instancia la variable β€˜enc’ con el valor de codificaciΓ³n β€˜gpt2’

enc = tiktoken.get_encoding('gpt2')

def process(example):
    # enconde_ordinary ignora cualquier token especial
    ids = enc.encode_ordinary(example['text']) 
    # al final del texto agregamos el token '50256 o <|endoftext|>, para gpt2 bpe
    ids.append(enc.eot_token)
    # creamos un diccionario 'out' con los elementos id y len
    out = {'ids': ids, 'len': len(ids)} 
    return out

πŸ’‘ Para ilustrar que hace la funciΓ³nΒ process()Β les dejo este ejemplo:

# Declarar una variable 'text' con una oraciΓ³n/sentence
text= "Hola, me llamo Alexander y tΓΊ como te llamas?"
ids= enc.encode_ordinary(text) # tokenizamos la variable
# Estos serΓ­an los tokens que devuelve encode_ordinary
print(ids) # [39, 5708, 11, 502, 32660, 18811, 10009, 331, 256, 21356, 401, 78, 573, 32660, 17485, 30]

# llamar a la funciΓ³n append() para agregar EOT token (50256 o <|endoftext|>)
ids.append(enc.eot_token)
print(ids) # [39, 5708, 11, 502, 32660, 18811, 10009, 331, 256, 21356, 401, 78, 573, 32660, 17485, 30, **50256**]

# Llamar a la funciΓ³n decode() para descrifar ids
print(enc.decode(ids))  #Hola, me llamo Alexander y tΓΊ como te llamas?<|endoftext|>

ΒΏRecordarΓ‘s que al principio mencionamos a la funciΓ³n map() ? Pues aquΓ­ la utilizamos con split_dataset y le pasamos los argumentos process(), remove_columns, desc y num_proc:

# Aqui aplicamos la funcion process al dataset split_dataset creado lineas arriba
tokenized = split_dataset.map(
    process, # Funcion de tokenizado
    remove_columns=['text'], # Luego de aplicar la funciΓ³n al dataset se elimina la columna text
    desc="tokenizing the splits", # DescripciΓ³n que se mostrarΓ‘ en la barra de progreso
    num_proc=num_proc # NΓΊmero de procesos para generar un dataset local
)

# Este output serΓ­a un ejemplo ilustrativo:
#tokenizing the splits #0: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 392.79ex/s]
#tokenizing the splits #6: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 386.31ex/s]
#tokenizing the splits #5: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 377.24ex/s]
#tokenizing the splits #7: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 500/500 [00:01<00:00, 372.65ex/s]
#tokenizing the splits #3: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 376.59ex/s]
#tokenizing the splits #4: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 364.67ex/s]
#tokenizing the splits #2: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 362.56ex/s]
#tokenizing the splits #1: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 501/501 [00:01<00:00, 360.43ex/s]

En el segmento de abajo vemos que ya no existe la columna β€˜text’:

print(tokenized)

train: Dataset({
        features: ['ids', 'len'],
        num_rows: 8009762
    })
    val: Dataset({
        features: ['ids', 'len'],
        num_rows: 4007
    })
})

Para terminar, juntamos todos los ids de cada dataset en un ΓΊnico archivo, que luego podremos usar para el training:

for split, dset in tokenized.items():
    arr_len = np.sum(dset['len']) # calculamos el tamaΓ±o total de la matriz
    filename = f'{split}.bin' # AcΓ‘ usamos formatspec para crear de manera dinΓ‘mica un archivo train.bin y val.bin
    dtype = np.uint16 # Definimos este tipo np.uint16 para nΓΊmeros enteros sin signo en el rango de 0 a 65535 (2^16 - 1) y como el valor mΓ‘ximo del token EOT es 50256 y es < 2^16 - 1 
    arr = np.memmap(filename, dtype=dtype, mode='w+', shape=(arr_len,)) # En la variable arr es del tipo memoria mapeada, esto es util porque se trabaja con archivos grandes 

    print(f"writing {filename}...") # Esto indica el nombre del archivo en el que se estΓ‘ escribiendo la matriz por ejemplo train.bin o val.bin
    idx = 0 # Establecemos el valor 0 para iniciar
    for example in tqdm(dset): # Con tqdm tendremos una barra de progreso segΓΊn vayamos iterando sobre cada elemento de dset
        arr[idx : idx + example['len']] = example['ids'] # Esto es slicing para asignar los valores de example['ids'] a una secciΓ³n de la matriz 'arr'
        idx += example['len'] # sumamos el valor actual de example['len']
    arr.flush() # por ΓΊltimo usamos la funciΓ³n flush() para vaciar el buffer, es decir escribiremos fΓ­sicamente todos los datos en el disco duro.

Al final tendremos :

train.bin de ~17GB y val.bin ~8.5MB

train tiene ~9B tokens (9,035,582,198)

val tiene ~4M tokens (4,434,897)

Luego leeremos los archivos .bin con numpy de la siguiente manera:

m = np.memmap('train.bin', dtype=np.uint16, mode='r')

Leave a Comment