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')