Text Generation
Transformers
Safetensors
GGUF
Spanish
llama
spanish
español
tiny
from-scratch
distillation
conversational
text-generation-inference
Instructions to use AxoneSystem/Minerva-v0.1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use AxoneSystem/Minerva-v0.1 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="AxoneSystem/Minerva-v0.1") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("AxoneSystem/Minerva-v0.1") model = AutoModelForCausalLM.from_pretrained("AxoneSystem/Minerva-v0.1", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use AxoneSystem/Minerva-v0.1 with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf AxoneSystem/Minerva-v0.1:F16 # Run inference directly in the terminal: llama cli -hf AxoneSystem/Minerva-v0.1:F16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf AxoneSystem/Minerva-v0.1:F16 # Run inference directly in the terminal: llama cli -hf AxoneSystem/Minerva-v0.1:F16
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf AxoneSystem/Minerva-v0.1:F16 # Run inference directly in the terminal: ./llama-cli -hf AxoneSystem/Minerva-v0.1:F16
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf AxoneSystem/Minerva-v0.1:F16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf AxoneSystem/Minerva-v0.1:F16
Use Docker
docker model run hf.co/AxoneSystem/Minerva-v0.1:F16
- LM Studio
- Jan
- vLLM
How to use AxoneSystem/Minerva-v0.1 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "AxoneSystem/Minerva-v0.1" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "AxoneSystem/Minerva-v0.1", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/AxoneSystem/Minerva-v0.1:F16
- SGLang
How to use AxoneSystem/Minerva-v0.1 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "AxoneSystem/Minerva-v0.1" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "AxoneSystem/Minerva-v0.1", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "AxoneSystem/Minerva-v0.1" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "AxoneSystem/Minerva-v0.1", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Ollama
How to use AxoneSystem/Minerva-v0.1 with Ollama:
ollama run hf.co/AxoneSystem/Minerva-v0.1:F16
- Unsloth Desktop
- Docker Model Runner
How to use AxoneSystem/Minerva-v0.1 with Docker Model Runner:
docker model run hf.co/AxoneSystem/Minerva-v0.1:F16
- Lemonade
How to use AxoneSystem/Minerva-v0.1 with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull AxoneSystem/Minerva-v0.1:F16
Run and chat with the model
lemonade run user.Minerva-v0.1-F16
List all available models
lemonade list
- Atomic Chat
Upload 20 files
Browse files- .gitattributes +1 -0
- Minerva-v0.1-F16.gguf +3 -0
- README.md +118 -3
- chat_template.jinja +1 -0
- config.json +32 -0
- entrenamiento/chat.py +64 -0
- entrenamiento/chat_perdidas.csv +41 -0
- entrenamiento/comun.py +24 -0
- entrenamiento/entrenar.py +218 -0
- entrenamiento/exportar_hf.py +141 -0
- entrenamiento/ficha_modelo.md +102 -0
- entrenamiento/identidad.py +62 -0
- entrenamiento/info.json +5 -0
- entrenamiento/maestro.py +174 -0
- entrenamiento/modelo.py +159 -0
- entrenamiento/preentreno_perdidas.csv +189 -0
- entrenamiento/preparar_datos.py +80 -0
- generation_config.json +14 -0
- model.safetensors +3 -0
- tokenizer.json +0 -0
- tokenizer_config.json +12 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Minerva-v0.1-F16.gguf filter=lfs diff=lfs merge=lfs -text
|
Minerva-v0.1-F16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:af7f64d9645f76ae574a71c895c064dc860e386f09d2859837d0f2e1bc95aa94
|
| 3 |
+
size 60094368
|
README.md
CHANGED
|
@@ -1,3 +1,118 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- es
|
| 4 |
+
license: cc-by-nc-4.0
|
| 5 |
+
library_name: transformers
|
| 6 |
+
pipeline_tag: text-generation
|
| 7 |
+
tags:
|
| 8 |
+
- spanish
|
| 9 |
+
- español
|
| 10 |
+
- tiny
|
| 11 |
+
- llama
|
| 12 |
+
- gguf
|
| 13 |
+
- from-scratch
|
| 14 |
+
- distillation
|
| 15 |
+
datasets:
|
| 16 |
+
- robrenaud/multilingual_tinystories
|
| 17 |
+
- FreedomIntelligence/alpaca-gpt4-spanish
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# Minerva v0.1 🇪🇸
|
| 21 |
+
|
| 22 |
+
**Minerva** es un modelo de lenguaje en español **diminuto** (30 millones de parámetros), entrenado
|
| 23 |
+
**desde cero** en un ordenador portátil. Es un modelo de juguete y de aprendizaje: habla español con
|
| 24 |
+
soltura y cuenta cuentos sencillos, pero **sabe muy poco del mundo y se inventa las cosas**.
|
| 25 |
+
|
| 26 |
+
El nombre viene de Minerva, diosa romana de la sabiduría, que según el mito nació de la cabeza de Júpiter.
|
| 27 |
+
Este modelo también "nació de la cabeza" de modelos más grandes: aprendió de textos escritos por ellos
|
| 28 |
+
(destilación).
|
| 29 |
+
|
| 30 |
+
## Qué sabe hacer (y qué no)
|
| 31 |
+
|
| 32 |
+
| ✅ Sí | ❌ No |
|
| 33 |
+
|---|---|
|
| 34 |
+
| Escribir en español con buena gramática | Dar datos fiables (se los inventa con seguridad) |
|
| 35 |
+
| Contar cuentos infantiles coherentes | Matemáticas, razonamiento, programación |
|
| 36 |
+
| Charlar con frases sencillas | Recordar conversaciones largas (ve unas 350 palabras) |
|
| 37 |
+
| Decir quién es y por qué se llama así | Cualquier uso serio: **no lo uses para decisiones reales** |
|
| 38 |
+
|
| 39 |
+
## Ejemplos reales
|
| 40 |
+
|
| 41 |
+
Respuestas generadas automáticamente al exportar el modelo (temperatura 0.7, sin escoger las mejores):
|
| 42 |
+
|
| 43 |
+
> **Tú:** ¿Quién eres?
|
| 44 |
+
> **Minerva:** Soy Minerva, un modelo de lenguaje español. No soy una persona, soy un programa de ordenador.
|
| 45 |
+
|
| 46 |
+
> **Tú:** ¿Por qué te llamas Minerva?
|
| 47 |
+
> **Minerva:** Me llamo así por Minerva, la diosa romana de la sabiduría. Según el mito, nació de la cabeza de Júpiter. Yo también nací de la cabeza de modelos más grandes: ellos me enseñaron y yo aprendí de sus respuestas.
|
| 48 |
+
|
| 49 |
+
> **Tú:** ¿Cuál es la capital de España?
|
| 50 |
+
> **Minerva:** La capital de España es Aragón.
|
| 51 |
+
|
| 52 |
+
> **Tú:** ¿Qué es la paella?
|
| 53 |
+
> **Minerva:** La paella es una tienda minorista que consiste en marcas y compresas para vender productos crudos.
|
| 54 |
+
|
| 55 |
+
> **Tú:** Dame un consejo para dormir mejor.
|
| 56 |
+
> **Minerva:** No es bueno dormir mucho. Es importante estar preparado para dormir. Las personas no pueden sentir que algo no estaba bien, y es importante tomarse un descanso y tomar descansos.
|
| 57 |
+
|
| 58 |
+
> **Cuento** (empieza con «Había una vez un dragón que»):
|
| 59 |
+
> Había una vez un dragón que podría volar. El dragón estaba enojado porque no había nadie que pudiera ayudar.
|
| 60 |
+
|
| 61 |
+
## Cómo usarlo
|
| 62 |
+
|
| 63 |
+
### Con LM Studio u Ollama (sin programar)
|
| 64 |
+
|
| 65 |
+
El repositorio incluye el archivo `Minerva-v0.1-F16.gguf`.
|
| 66 |
+
|
| 67 |
+
- **LM Studio**: busca `AxoneSystem/Minerva-v0.1` y descárgalo.
|
| 68 |
+
- **Ollama**: `ollama run hf.co/AxoneSystem/Minerva-v0.1`
|
| 69 |
+
|
| 70 |
+
### Con Python (`transformers`)
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 74 |
+
|
| 75 |
+
tok = AutoTokenizer.from_pretrained("AxoneSystem/Minerva-v0.1")
|
| 76 |
+
modelo = AutoModelForCausalLM.from_pretrained("AxoneSystem/Minerva-v0.1")
|
| 77 |
+
|
| 78 |
+
mensajes = [{"role": "user", "content": "¿Quién eres?"}]
|
| 79 |
+
ids = tok.apply_chat_template(mensajes, add_generation_prompt=True, return_tensors="pt")
|
| 80 |
+
salida = modelo.generate(ids, max_new_tokens=150, do_sample=True, temperature=0.7, top_k=30)
|
| 81 |
+
print(tok.decode(salida[0, ids.shape[1]:], skip_special_tokens=True))
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
Para que cuente un cuento, dale el principio sin plantilla de chat: `"Había una vez un gato que"`.
|
| 85 |
+
|
| 86 |
+
### Formato de chat
|
| 87 |
+
|
| 88 |
+
```
|
| 89 |
+
<|usuario|>¿Quién eres?<|asistente|>Soy Minerva, un modelo de lenguaje muy pequeño...<|fin|>
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
## Cómo se entrenó
|
| 93 |
+
|
| 94 |
+
| | |
|
| 95 |
+
|---|---|
|
| 96 |
+
| Arquitectura | Transformer decodificador tipo Llama: RMSNorm, RoPE, SwiGLU, embeddings compartidos |
|
| 97 |
+
| Tamaño | 30M parámetros · 8 capas · 512 dimensiones · 8 cabezas · contexto de 512 tokens |
|
| 98 |
+
| Tokenizador | BPE byte-level propio, 8.192 piezas, entrenado solo con español |
|
| 99 |
+
| Hardware | 1 portátil con NVIDIA RTX 5070 Laptop (8 GB) |
|
| 100 |
+
| Tiempo | ~1 h de preentreno + ~1 h de destilación y ajuste |
|
| 101 |
+
|
| 102 |
+
1. **Preentreno** (~280M tokens vistos): ~1 millón de cuentos infantiles en español escritos por
|
| 103 |
+
Gemini 1.5 Flash (`robrenaud/multilingual_tinystories`) + ~50.000 respuestas de GPT-4 en español
|
| 104 |
+
(`FreedomIntelligence/alpaca-gpt4-spanish`). Objetivo: predecir la siguiente palabra.
|
| 105 |
+
2. **Destilación**: [Salamandra-2B-Instruct](https://huggingface.co/BSC-LT/salamandra-2b-instruct)
|
| 106 |
+
(Barcelona Supercomputing Center) respondió miles de preguntas **de forma sencilla y corta**, para
|
| 107 |
+
que un modelo tan pequeño pudiera imitarlo.
|
| 108 |
+
3. **Ajuste de chat**: aprende a responder solo con las partes del asistente (las preguntas no cuentan
|
| 109 |
+
en el aprendizaje) + unas decenas de respuestas escritas a mano sobre su identidad.
|
| 110 |
+
|
| 111 |
+
Pérdida de validación final en el ajuste de chat: 2.242.
|
| 112 |
+
El código completo de entrenamiento está en la carpeta `entrenamiento/`.
|
| 113 |
+
|
| 114 |
+
## Licencia
|
| 115 |
+
|
| 116 |
+
**CC BY-NC 4.0** (uso no comercial). Parte de los datos de ajuste son respuestas de GPT-4 que proceden de
|
| 117 |
+
Alpaca-GPT4, publicado solo para investigación y uso no comercial; por prudencia el modelo hereda esa
|
| 118 |
+
restricción. Los cuentos (Apache-2.0) y el maestro Salamandra (Apache-2.0) no tienen esa limitación.
|
chat_template.jinja
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{%- for m in messages -%}{%- if m['role'] == 'user' -%}<|usuario|>{{ m['content'] }}{%- elif m['role'] == 'assistant' -%}<|asistente|>{{ m['content'] }}<|fin|>{%- endif -%}{%- endfor -%}{%- if add_generation_prompt -%}<|asistente|>{%- endif -%}
|
config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"LlamaForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"bos_token_id": 0,
|
| 8 |
+
"dtype": "float32",
|
| 9 |
+
"eos_token_id": 0,
|
| 10 |
+
"head_dim": 64,
|
| 11 |
+
"hidden_act": "silu",
|
| 12 |
+
"hidden_size": 512,
|
| 13 |
+
"initializer_range": 0.02,
|
| 14 |
+
"intermediate_size": 1408,
|
| 15 |
+
"max_position_embeddings": 512,
|
| 16 |
+
"mlp_bias": false,
|
| 17 |
+
"model_type": "llama",
|
| 18 |
+
"num_attention_heads": 8,
|
| 19 |
+
"num_hidden_layers": 8,
|
| 20 |
+
"num_key_value_heads": 8,
|
| 21 |
+
"pad_token_id": 0,
|
| 22 |
+
"pretraining_tp": 1,
|
| 23 |
+
"rms_norm_eps": 1e-06,
|
| 24 |
+
"rope_parameters": {
|
| 25 |
+
"rope_theta": 10000.0,
|
| 26 |
+
"rope_type": "default"
|
| 27 |
+
},
|
| 28 |
+
"tie_word_embeddings": true,
|
| 29 |
+
"transformers_version": "5.19.0",
|
| 30 |
+
"use_cache": true,
|
| 31 |
+
"vocab_size": 8192
|
| 32 |
+
}
|
entrenamiento/chat.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Paso 5: ¡habla con el modelo!
|
| 3 |
+
|
| 4 |
+
python chat.py # modo chat (usa checkpoints/chat.pt)
|
| 5 |
+
python chat.py --cuento # modo cuentacuentos (usa checkpoints/preentreno.pt)
|
| 6 |
+
python chat.py --temperatura 0.5 # más bajo = más predecible, más alto = más creativo
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from tokenizers import Tokenizer
|
| 13 |
+
|
| 14 |
+
from comun import ASISTENTE, FIN, NOMBRE, USUARIO
|
| 15 |
+
from modelo import GPT, Config
|
| 16 |
+
|
| 17 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 18 |
+
p = argparse.ArgumentParser()
|
| 19 |
+
p.add_argument("--cuento", action="store_true")
|
| 20 |
+
p.add_argument("--temperatura", type=float, default=0.7)
|
| 21 |
+
p.add_argument("--top_k", type=int, default=30)
|
| 22 |
+
args = p.parse_args()
|
| 23 |
+
|
| 24 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 25 |
+
ruta = "checkpoints/preentreno.pt" if args.cuento else "checkpoints/chat.pt"
|
| 26 |
+
ck = torch.load(ruta, map_location=dev, weights_only=False)
|
| 27 |
+
modelo = GPT(Config(**ck["config"])).to(dev).eval()
|
| 28 |
+
modelo.load_state_dict(ck["modelo"])
|
| 29 |
+
tok = Tokenizer.from_file("datos/tokenizador.json")
|
| 30 |
+
ID_FIN, ID_USU, ID_ASI = (tok.token_to_id(t) for t in (FIN, USUARIO, ASISTENTE))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def continuar(ids, max_tokens):
|
| 34 |
+
x = torch.tensor([ids], device=dev)
|
| 35 |
+
with torch.autocast(dev, dtype=torch.bfloat16, enabled=dev == "cuda"):
|
| 36 |
+
y = modelo.generar(x, max_tokens=max_tokens, temperatura=args.temperatura, top_k=args.top_k, parar_en={ID_FIN})
|
| 37 |
+
nuevos = y[0, len(ids):].tolist()
|
| 38 |
+
if nuevos and nuevos[-1] == ID_FIN:
|
| 39 |
+
nuevos = nuevos[:-1]
|
| 40 |
+
return nuevos
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
print(f"{NOMBRE} ({modelo.num_parametros() / 1e6:.0f}M parámetros, {ruta}) — escribe 'salir' para terminar")
|
| 44 |
+
if args.cuento:
|
| 45 |
+
print("Escribe el comienzo de un cuento (p. ej. 'Había una vez un gato') y el modelo lo sigue.\n")
|
| 46 |
+
historial = []
|
| 47 |
+
while True:
|
| 48 |
+
try:
|
| 49 |
+
texto = input("\nTú: ").strip()
|
| 50 |
+
except (EOFError, KeyboardInterrupt):
|
| 51 |
+
break
|
| 52 |
+
if texto.lower() in ("salir", "exit", "quit"):
|
| 53 |
+
break
|
| 54 |
+
if not texto:
|
| 55 |
+
continue
|
| 56 |
+
if args.cuento:
|
| 57 |
+
ids = tok.encode(texto).ids
|
| 58 |
+
print(f"{NOMBRE}: " + texto + tok.decode(continuar(ids, 400)))
|
| 59 |
+
continue
|
| 60 |
+
historial += [ID_USU] + tok.encode(texto).ids + [ID_ASI]
|
| 61 |
+
historial = historial[-(modelo.config.contexto - 200):] # recuerda solo lo último
|
| 62 |
+
respuesta = continuar(historial, 200)
|
| 63 |
+
historial += respuesta + [ID_FIN]
|
| 64 |
+
print(f"{NOMBRE}: " + tok.decode(respuesta).strip())
|
entrenamiento/chat_perdidas.csv
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
paso,perdida_train,perdida_val,lr,tokens_por_seg
|
| 2 |
+
0,,2.5465,,
|
| 3 |
+
0,2.4225,,0.000003,0
|
| 4 |
+
25,2.6421,,0.000090,77420
|
| 5 |
+
50,2.4493,,0.000176,76207
|
| 6 |
+
75,2.4308,,0.000262,76027
|
| 7 |
+
100,2.3939,,0.000300,74666
|
| 8 |
+
125,2.3228,,0.000298,72977
|
| 9 |
+
150,2.3681,,0.000296,74274
|
| 10 |
+
175,2.3509,,0.000292,70574
|
| 11 |
+
200,2.2703,,0.000286,73708
|
| 12 |
+
225,2.4100,,0.000280,73527
|
| 13 |
+
250,,2.4240,,
|
| 14 |
+
250,2.4816,,0.000272,68771
|
| 15 |
+
275,2.2978,,0.000264,72449
|
| 16 |
+
300,2.2344,,0.000254,73476
|
| 17 |
+
325,2.3682,,0.000243,73291
|
| 18 |
+
350,2.3207,,0.000232,74426
|
| 19 |
+
375,2.3712,,0.000220,70396
|
| 20 |
+
400,2.3498,,0.000207,73838
|
| 21 |
+
425,2.3636,,0.000194,73854
|
| 22 |
+
450,2.2348,,0.000181,73398
|
| 23 |
+
475,2.2173,,0.000167,72852
|
| 24 |
+
500,,2.2985,,
|
| 25 |
+
500,2.1653,,0.000154,68580
|
| 26 |
+
525,2.3053,,0.000141,72057
|
| 27 |
+
550,2.0973,,0.000127,70499
|
| 28 |
+
575,2.1182,,0.000115,74156
|
| 29 |
+
600,1.9934,,0.000102,70583
|
| 30 |
+
625,2.2471,,0.000091,38884
|
| 31 |
+
650,2.1793,,0.000080,37694
|
| 32 |
+
675,1.9070,,0.000070,37313
|
| 33 |
+
700,2.0341,,0.000061,73352
|
| 34 |
+
725,2.0737,,0.000053,74291
|
| 35 |
+
750,,2.2419,,
|
| 36 |
+
750,2.0562,,0.000046,68670
|
| 37 |
+
775,2.0480,,0.000040,73805
|
| 38 |
+
800,2.2048,,0.000036,71873
|
| 39 |
+
825,2.0975,,0.000032,73843
|
| 40 |
+
850,2.0775,,0.000031,73641
|
| 41 |
+
872,,2.2420,,
|
entrenamiento/comun.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cosas compartidas entre los scripts: nombre, tokens especiales y formato de chat."""
|
| 2 |
+
|
| 3 |
+
# Nombre del modelo pequeño (provisional). Si lo cambias, vuelve a ejecutar
|
| 4 |
+
# identidad.py y `entrenar.py chat` para que el modelo aprenda su nuevo nombre.
|
| 5 |
+
NOMBRE = "Minerva"
|
| 6 |
+
|
| 7 |
+
FIN = "<|fin|>"
|
| 8 |
+
USUARIO = "<|usuario|>"
|
| 9 |
+
ASISTENTE = "<|asistente|>"
|
| 10 |
+
ESPECIALES = [FIN, USUARIO, ASISTENTE]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def formatear_chat(turnos):
|
| 14 |
+
"""turnos = [("usuario", "hola"), ("asistente", "¡Hola!"), ...] -> texto plano.
|
| 15 |
+
|
| 16 |
+
<|usuario|>hola<|asistente|>¡Hola!<|fin|>
|
| 17 |
+
"""
|
| 18 |
+
texto = ""
|
| 19 |
+
for rol, contenido in turnos:
|
| 20 |
+
if rol == "usuario":
|
| 21 |
+
texto += USUARIO + contenido
|
| 22 |
+
else:
|
| 23 |
+
texto += ASISTENTE + contenido + FIN
|
| 24 |
+
return texto
|
entrenamiento/entrenar.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pasos 3 y 4: entrenar a Minerva.
|
| 3 |
+
|
| 4 |
+
python entrenar.py preentreno -> aprende español leyendo ~1M cuentos + textos de GPT-4
|
| 5 |
+
(predice la siguiente palabra, como todos los GPT)
|
| 6 |
+
python entrenar.py chat -> aprende a CONVERSAR imitando las respuestas de los
|
| 7 |
+
modelos maestros (destilación). Solo aprende de las
|
| 8 |
+
respuestas del asistente, no de las preguntas.
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import glob
|
| 12 |
+
import json
|
| 13 |
+
import math
|
| 14 |
+
import os
|
| 15 |
+
import random
|
| 16 |
+
import time
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
import torch
|
| 20 |
+
from tokenizers import Tokenizer
|
| 21 |
+
|
| 22 |
+
from comun import ASISTENTE, FIN, USUARIO
|
| 23 |
+
from modelo import GPT, Config
|
| 24 |
+
|
| 25 |
+
p = argparse.ArgumentParser()
|
| 26 |
+
p.add_argument("fase", choices=["preentreno", "chat"])
|
| 27 |
+
p.add_argument("--lote", type=int, default=32, help="secuencias por micro-lote")
|
| 28 |
+
p.add_argument("--acumular", type=int, default=4, help="micro-lotes por paso")
|
| 29 |
+
p.add_argument("--lr", type=float, default=None)
|
| 30 |
+
p.add_argument("--epocas", type=float, default=None)
|
| 31 |
+
p.add_argument("--evaluar_cada", type=int, default=250)
|
| 32 |
+
args = p.parse_args()
|
| 33 |
+
|
| 34 |
+
torch.manual_seed(1234)
|
| 35 |
+
random.seed(1234)
|
| 36 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 37 |
+
dev = "cuda"
|
| 38 |
+
autocast = torch.autocast("cuda", dtype=torch.bfloat16)
|
| 39 |
+
tok = Tokenizer.from_file("datos/tokenizador.json")
|
| 40 |
+
ID_FIN, ID_USU, ID_ASI = (tok.token_to_id(t) for t in (FIN, USUARIO, ASISTENTE))
|
| 41 |
+
os.makedirs("checkpoints", exist_ok=True)
|
| 42 |
+
os.makedirs("logs", exist_ok=True)
|
| 43 |
+
|
| 44 |
+
# ======================================================================= datos
|
| 45 |
+
if args.fase == "preentreno":
|
| 46 |
+
config = Config()
|
| 47 |
+
modelo = GPT(config).to(dev)
|
| 48 |
+
lr_max = args.lr or 1e-3
|
| 49 |
+
epocas = args.epocas or 1.0
|
| 50 |
+
datos = {s: np.memmap(f"datos/preentreno_{s}.bin", dtype=np.uint16, mode="r") for s in ("train", "val")}
|
| 51 |
+
T = config.contexto
|
| 52 |
+
|
| 53 |
+
def lote(split):
|
| 54 |
+
d = datos[split]
|
| 55 |
+
ix = np.random.randint(0, len(d) - T - 1, args.lote)
|
| 56 |
+
x = torch.from_numpy(np.stack([d[i : i + T] for i in ix]).astype(np.int64))
|
| 57 |
+
y = torch.from_numpy(np.stack([d[i + 1 : i + 1 + T] for i in ix]).astype(np.int64))
|
| 58 |
+
return x.pin_memory().to(dev, non_blocking=True), y.pin_memory().to(dev, non_blocking=True), None
|
| 59 |
+
|
| 60 |
+
tokens_por_paso = args.lote * args.acumular * T
|
| 61 |
+
pasos_totales = int(epocas * len(datos["train"]) / tokens_por_paso)
|
| 62 |
+
print(f"Datos: {len(datos['train']):,} tokens de entrenamiento")
|
| 63 |
+
|
| 64 |
+
else: # chat
|
| 65 |
+
ck = torch.load("checkpoints/preentreno.pt", map_location="cpu", weights_only=False)
|
| 66 |
+
config = Config(**ck["config"])
|
| 67 |
+
modelo = GPT(config).to(dev)
|
| 68 |
+
modelo.load_state_dict(ck["modelo"])
|
| 69 |
+
lr_max = args.lr or 3e-4
|
| 70 |
+
epocas = args.epocas or 2.0
|
| 71 |
+
|
| 72 |
+
def tokenizar(turnos):
|
| 73 |
+
ids, masc = [], []
|
| 74 |
+
for rol, texto in turnos:
|
| 75 |
+
piezas = tok.encode(texto).ids
|
| 76 |
+
if rol == "usuario":
|
| 77 |
+
ids += [ID_USU] + piezas
|
| 78 |
+
masc += [0] * (1 + len(piezas))
|
| 79 |
+
else: # aprende a producir la respuesta y el token de fin
|
| 80 |
+
ids += [ID_ASI] + piezas + [ID_FIN]
|
| 81 |
+
masc += [0] + [1] * (len(piezas) + 1)
|
| 82 |
+
return ids[: config.contexto + 1], masc[: config.contexto + 1]
|
| 83 |
+
|
| 84 |
+
conversaciones = []
|
| 85 |
+
with open("datos/crudos/alpaca-gpt4-spanish.json", encoding="utf-8") as f:
|
| 86 |
+
for item in json.load(f):
|
| 87 |
+
turnos = [("usuario" if c["from"] == "human" else "asistente", c["value"].strip()) for c in item["conversations"]]
|
| 88 |
+
conversaciones.append(turnos)
|
| 89 |
+
print(f" GPT-4 (alpaca-es): {len(conversaciones):,}")
|
| 90 |
+
for ruta in sorted(glob.glob("datos/chat_*.jsonl")):
|
| 91 |
+
with open(ruta, encoding="utf-8") as f:
|
| 92 |
+
filas = [json.loads(l) for l in f if l.strip()]
|
| 93 |
+
for r in filas: # "repetir" permite dar más peso a algunos ejemplos
|
| 94 |
+
conversaciones += [r["turnos"]] * r.get("repetir", 1)
|
| 95 |
+
print(f" {os.path.basename(ruta)}: {len(filas):,}")
|
| 96 |
+
random.shuffle(conversaciones)
|
| 97 |
+
ejemplos = [tokenizar(c) for c in conversaciones]
|
| 98 |
+
ejemplos = [e for e in ejemplos if sum(e[1]) > 0]
|
| 99 |
+
val, train = ejemplos[:500], ejemplos[500:]
|
| 100 |
+
cursor = {"train": 0, "val": 0}
|
| 101 |
+
conjuntos = {"train": train, "val": val}
|
| 102 |
+
|
| 103 |
+
def lote(split):
|
| 104 |
+
conj = conjuntos[split]
|
| 105 |
+
if cursor[split] + args.lote > len(conj):
|
| 106 |
+
cursor[split] = 0
|
| 107 |
+
if split == "train":
|
| 108 |
+
random.shuffle(conj)
|
| 109 |
+
trozo = conj[cursor[split] : cursor[split] + args.lote]
|
| 110 |
+
cursor[split] += args.lote
|
| 111 |
+
L = max(len(i) for i, _ in trozo)
|
| 112 |
+
x = torch.full((len(trozo), L - 1), ID_FIN, dtype=torch.long)
|
| 113 |
+
y = torch.full((len(trozo), L - 1), ID_FIN, dtype=torch.long)
|
| 114 |
+
m = torch.zeros((len(trozo), L - 1), dtype=torch.long)
|
| 115 |
+
for j, (ids, masc) in enumerate(trozo):
|
| 116 |
+
n = len(ids) - 1
|
| 117 |
+
x[j, :n] = torch.tensor(ids[:-1])
|
| 118 |
+
y[j, :n] = torch.tensor(ids[1:])
|
| 119 |
+
m[j, :n] = torch.tensor(masc[1:])
|
| 120 |
+
return x.to(dev), y.to(dev), m.to(dev)
|
| 121 |
+
|
| 122 |
+
pasos_totales = int(epocas * len(train) / (args.lote * args.acumular))
|
| 123 |
+
print(f"Conversaciones: {len(train):,} entrenamiento / {len(val)} validación")
|
| 124 |
+
|
| 125 |
+
print(f"Modelo: {modelo.num_parametros() / 1e6:.1f} M parámetros | {pasos_totales:,} pasos | lr {lr_max}")
|
| 126 |
+
|
| 127 |
+
# ======================================================================= optimizador
|
| 128 |
+
decay = [p for n, p in modelo.named_parameters() if p.dim() >= 2]
|
| 129 |
+
no_decay = [p for n, p in modelo.named_parameters() if p.dim() < 2]
|
| 130 |
+
opt = torch.optim.AdamW(
|
| 131 |
+
[{"params": decay, "weight_decay": 0.1}, {"params": no_decay, "weight_decay": 0.0}],
|
| 132 |
+
lr=lr_max, betas=(0.9, 0.95), fused=True,
|
| 133 |
+
)
|
| 134 |
+
calentamiento = min(200, pasos_totales // 10)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def tasa(paso): # calentamiento lineal + bajada en coseno hasta 10%
|
| 138 |
+
if paso < calentamiento:
|
| 139 |
+
return lr_max * (paso + 1) / calentamiento
|
| 140 |
+
prog = (paso - calentamiento) / max(1, pasos_totales - calentamiento)
|
| 141 |
+
return lr_max * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * prog)))
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@torch.no_grad()
|
| 145 |
+
def evaluar():
|
| 146 |
+
modelo.eval()
|
| 147 |
+
perdidas = []
|
| 148 |
+
for _ in range(20):
|
| 149 |
+
x, y, m = lote("val")
|
| 150 |
+
with autocast:
|
| 151 |
+
_, perd = modelo(x, y, m)
|
| 152 |
+
perdidas.append(perd.item())
|
| 153 |
+
modelo.train()
|
| 154 |
+
return sum(perdidas) / len(perdidas)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@torch.no_grad()
|
| 158 |
+
def muestra():
|
| 159 |
+
modelo.eval()
|
| 160 |
+
if args.fase == "preentreno":
|
| 161 |
+
prompt = tok.encode("Había una vez").ids
|
| 162 |
+
else:
|
| 163 |
+
prompt = [ID_USU] + tok.encode("¿Cuál es la capital de España?").ids + [ID_ASI]
|
| 164 |
+
x = torch.tensor([prompt], device=dev)
|
| 165 |
+
with autocast:
|
| 166 |
+
y = modelo.generar(x, max_tokens=120, parar_en={ID_FIN})
|
| 167 |
+
modelo.train()
|
| 168 |
+
return tok.decode(y[0].tolist(), skip_special_tokens=False)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def guardar(paso, perdida_val):
|
| 172 |
+
torch.save(
|
| 173 |
+
{"modelo": modelo.state_dict(), "config": config.__dict__, "paso": paso, "perdida_val": perdida_val},
|
| 174 |
+
f"checkpoints/{args.fase}.pt",
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# ======================================================================= bucle
|
| 179 |
+
log = open(f"logs/{args.fase}.csv", "w", encoding="utf-8")
|
| 180 |
+
log.write("paso,perdida_train,perdida_val,lr,tokens_por_seg\n")
|
| 181 |
+
t0 = time.time()
|
| 182 |
+
mejor = float("inf")
|
| 183 |
+
for paso in range(pasos_totales + 1):
|
| 184 |
+
if paso % args.evaluar_cada == 0 or paso == pasos_totales:
|
| 185 |
+
pv = evaluar()
|
| 186 |
+
print(f"\n[paso {paso}] pérdida validación {pv:.3f}")
|
| 187 |
+
print(" muestra:", muestra().replace("\n", " ")[:400])
|
| 188 |
+
log.write(f"{paso},,{pv:.4f},,\n")
|
| 189 |
+
if pv < mejor:
|
| 190 |
+
mejor = pv
|
| 191 |
+
guardar(paso, pv)
|
| 192 |
+
if paso == pasos_totales:
|
| 193 |
+
break
|
| 194 |
+
|
| 195 |
+
for g in opt.param_groups:
|
| 196 |
+
g["lr"] = tasa(paso)
|
| 197 |
+
tokens = 0
|
| 198 |
+
for _ in range(args.acumular):
|
| 199 |
+
x, y, m = lote("train")
|
| 200 |
+
with autocast:
|
| 201 |
+
_, perd = modelo(x, y, m)
|
| 202 |
+
(perd / args.acumular).backward()
|
| 203 |
+
tokens += x.numel()
|
| 204 |
+
torch.nn.utils.clip_grad_norm_(modelo.parameters(), 1.0)
|
| 205 |
+
opt.step()
|
| 206 |
+
opt.zero_grad(set_to_none=True)
|
| 207 |
+
|
| 208 |
+
if paso % 25 == 0:
|
| 209 |
+
torch.cuda.synchronize()
|
| 210 |
+
dt = time.time() - t0
|
| 211 |
+
tps = tokens * 25 / dt if paso else 0
|
| 212 |
+
resta = (pasos_totales - paso) * dt / 25 / 60 if paso else 0
|
| 213 |
+
print(f"paso {paso:5d}/{pasos_totales} | pérdida {perd.item():.3f} | lr {tasa(paso):.2e} | {tps/1000:.0f}k tok/s | faltan ~{resta:.0f} min", flush=True)
|
| 214 |
+
log.write(f"{paso},{perd.item():.4f},,{tasa(paso):.6f},{tps:.0f}\n")
|
| 215 |
+
log.flush()
|
| 216 |
+
t0 = time.time()
|
| 217 |
+
|
| 218 |
+
print(f"\nListo. Mejor pérdida de validación: {mejor:.3f} -> checkpoints/{args.fase}.pt")
|
entrenamiento/exportar_hf.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Exporta Minerva v0.1 al formato estándar de Hugging Face (arquitectura Llama), para que
|
| 3 |
+
cualquiera pueda usarla con `transformers` sin nuestro código:
|
| 4 |
+
|
| 5 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 6 |
+
tok = AutoTokenizer.from_pretrained("Minerva-v0.1")
|
| 7 |
+
modelo = AutoModelForCausalLM.from_pretrained("Minerva-v0.1")
|
| 8 |
+
|
| 9 |
+
Comprueba que el modelo exportado da EXACTAMENTE los mismos resultados que el original.
|
| 10 |
+
"""
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import shutil
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from tokenizers import Tokenizer
|
| 18 |
+
from transformers import LlamaConfig, LlamaForCausalLM, PreTrainedTokenizerFast
|
| 19 |
+
|
| 20 |
+
from comun import ASISTENTE, FIN, NOMBRE, USUARIO
|
| 21 |
+
from modelo import GPT, Config
|
| 22 |
+
|
| 23 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 24 |
+
SALIDA = sys.argv[2] if len(sys.argv) > 2 else f"{NOMBRE}-v0.1"
|
| 25 |
+
os.makedirs(SALIDA, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
ck = torch.load(sys.argv[1] if len(sys.argv) > 1 else "checkpoints/chat.pt", map_location="cpu", weights_only=False)
|
| 28 |
+
c = Config(**ck["config"])
|
| 29 |
+
nuestro = GPT(c).eval()
|
| 30 |
+
nuestro.load_state_dict(ck["modelo"])
|
| 31 |
+
sd = nuestro.state_dict()
|
| 32 |
+
|
| 33 |
+
tok = Tokenizer.from_file("datos/tokenizador.json")
|
| 34 |
+
id_fin = tok.token_to_id(FIN)
|
| 35 |
+
dim_cabeza = c.dim // c.cabezas
|
| 36 |
+
oculta = nuestro.bloques[0].ff.w1.weight.shape[0]
|
| 37 |
+
|
| 38 |
+
# ------------------------------------------------------------------ pesos
|
| 39 |
+
config = LlamaConfig(
|
| 40 |
+
vocab_size=c.vocab, hidden_size=c.dim, intermediate_size=oculta, num_hidden_layers=c.capas,
|
| 41 |
+
num_attention_heads=c.cabezas, num_key_value_heads=c.cabezas, head_dim=dim_cabeza,
|
| 42 |
+
max_position_embeddings=c.contexto, rms_norm_eps=1e-6, rope_theta=10000.0, hidden_act="silu",
|
| 43 |
+
tie_word_embeddings=True, attention_bias=False, mlp_bias=False,
|
| 44 |
+
bos_token_id=id_fin, eos_token_id=id_fin, pad_token_id=id_fin, torch_dtype="float32",
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Nuestra RoPE rota pares (0,1),(2,3)...; la de Llama en HF rota (0,d/2),(1,d/2+1)...
|
| 48 |
+
# Reordenamos las filas de q y k dentro de cada cabeza para que sea la misma operación.
|
| 49 |
+
orden = torch.cat([torch.arange(0, dim_cabeza, 2), torch.arange(1, dim_cabeza, 2)])
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def permutar(w):
|
| 53 |
+
return w.view(c.cabezas, dim_cabeza, c.dim)[:, orden, :].reshape(c.dim, c.dim)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
nuevos = {"model.embed_tokens.weight": sd["embedding.weight"], "model.norm.weight": sd["norm_final.peso"]}
|
| 57 |
+
for i in range(c.capas):
|
| 58 |
+
p, h = f"bloques.{i}.", f"model.layers.{i}."
|
| 59 |
+
q, k, v = sd[p + "atencion.qkv.weight"].split(c.dim, dim=0)
|
| 60 |
+
nuevos.update({
|
| 61 |
+
h + "self_attn.q_proj.weight": permutar(q), h + "self_attn.k_proj.weight": permutar(k),
|
| 62 |
+
h + "self_attn.v_proj.weight": v, h + "self_attn.o_proj.weight": sd[p + "atencion.salida.weight"],
|
| 63 |
+
h + "mlp.gate_proj.weight": sd[p + "ff.w1.weight"], h + "mlp.up_proj.weight": sd[p + "ff.w3.weight"],
|
| 64 |
+
h + "mlp.down_proj.weight": sd[p + "ff.w2.weight"],
|
| 65 |
+
h + "input_layernorm.weight": sd[p + "norm1.peso"], h + "post_attention_layernorm.weight": sd[p + "norm2.peso"],
|
| 66 |
+
})
|
| 67 |
+
hf = LlamaForCausalLM(config).eval()
|
| 68 |
+
faltan, sobran = hf.load_state_dict({k: v.clone() for k, v in nuevos.items()}, strict=False)
|
| 69 |
+
assert not sobran and set(faltan) <= {"lm_head.weight"}, (faltan, sobran)
|
| 70 |
+
hf.tie_weights()
|
| 71 |
+
hf.generation_config.update(do_sample=True, temperature=0.7, top_k=30, max_new_tokens=200,
|
| 72 |
+
eos_token_id=id_fin, pad_token_id=id_fin, bos_token_id=id_fin)
|
| 73 |
+
hf.save_pretrained(SALIDA, safe_serialization=True)
|
| 74 |
+
|
| 75 |
+
# ------------------------------------------------------------------ tokenizador + plantilla de chat
|
| 76 |
+
PLANTILLA = (
|
| 77 |
+
"{%- for m in messages -%}"
|
| 78 |
+
"{%- if m['role'] == 'user' -%}" + USUARIO + "{{ m['content'] }}"
|
| 79 |
+
"{%- elif m['role'] == 'assistant' -%}" + ASISTENTE + "{{ m['content'] }}" + FIN +
|
| 80 |
+
"{%- endif -%}"
|
| 81 |
+
"{%- endfor -%}"
|
| 82 |
+
"{%- if add_generation_prompt -%}" + ASISTENTE + "{%- endif -%}"
|
| 83 |
+
)
|
| 84 |
+
tok_hf = PreTrainedTokenizerFast(tokenizer_object=tok, eos_token=FIN, pad_token=FIN, bos_token=FIN,
|
| 85 |
+
additional_special_tokens=[USUARIO, ASISTENTE], model_max_length=c.contexto)
|
| 86 |
+
tok_hf.chat_template = PLANTILLA
|
| 87 |
+
tok_hf.save_pretrained(SALIDA)
|
| 88 |
+
|
| 89 |
+
# ------------------------------------------------------------------ comprobación
|
| 90 |
+
hf2 = LlamaForCausalLM.from_pretrained(SALIDA).eval()
|
| 91 |
+
tok2 = PreTrainedTokenizerFast.from_pretrained(SALIDA)
|
| 92 |
+
texto = tok2.apply_chat_template([{"role": "user", "content": "¿Quién eres?"}], tokenize=False, add_generation_prompt=True)
|
| 93 |
+
ids = tok2(texto, return_tensors="pt", add_special_tokens=False)["input_ids"]
|
| 94 |
+
assert ids[0].tolist() == tok.encode(texto).ids, "el tokenizador exportado no coincide"
|
| 95 |
+
with torch.no_grad():
|
| 96 |
+
a = nuestro(ids, ids)[0] # pasamos objetivos para obtener los logits de todas las posiciones
|
| 97 |
+
b = hf2(ids).logits
|
| 98 |
+
dif = (a - b).abs().max().item()
|
| 99 |
+
print(f"Diferencia máxima de logits entre el original y el exportado: {dif:.2e}")
|
| 100 |
+
assert dif < 1e-3, "¡la exportación no es fiel!"
|
| 101 |
+
with torch.no_grad():
|
| 102 |
+
g1 = nuestro.generar(ids, max_tokens=40, temperatura=0, parar_en={id_fin})[0, ids.shape[1]:].tolist()
|
| 103 |
+
g2 = hf2.generate(ids, max_new_tokens=40, do_sample=False)[0, ids.shape[1]:].tolist()
|
| 104 |
+
print("Mismo texto generado (voraz):", g1 == g2[: len(g1)])
|
| 105 |
+
print("Respuesta:", tok2.decode(g2, skip_special_tokens=True))
|
| 106 |
+
|
| 107 |
+
# ------------------------------------------------------------------ ficha del modelo (README.md) con ejemplos reales
|
| 108 |
+
torch.manual_seed(2026)
|
| 109 |
+
ejemplos = []
|
| 110 |
+
for q in ["¿Quién eres?", "¿Por qué te llamas Minerva?", "¿Cuál es la capital de España?",
|
| 111 |
+
"¿Qué es la paella?", "Dame un consejo para dormir mejor."]:
|
| 112 |
+
ids = tok2.apply_chat_template([{"role": "user", "content": q}], add_generation_prompt=True, return_tensors="pt", return_dict=False)
|
| 113 |
+
with torch.no_grad():
|
| 114 |
+
out = hf2.generate(ids, max_new_tokens=120, do_sample=True, temperature=0.7, top_k=30)
|
| 115 |
+
ejemplos.append(f"> **Tú:** {q} \n> **Minerva:** {tok2.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip()}")
|
| 116 |
+
ids = tok2("Había una vez un dragón que", return_tensors="pt", add_special_tokens=False)["input_ids"]
|
| 117 |
+
with torch.no_grad():
|
| 118 |
+
out = hf2.generate(ids, max_new_tokens=150, do_sample=True, temperature=0.7, top_k=30)
|
| 119 |
+
cuento = tok2.decode(out[0], skip_special_tokens=True).strip().replace("\n\n", " ").replace("\n", " ")
|
| 120 |
+
ejemplos.append(f"> **Cuento** (empieza con «Había una vez un dragón que»): \n> {cuento}")
|
| 121 |
+
ficha = open("ficha_modelo.md", encoding="utf-8").read()
|
| 122 |
+
ficha = (ficha.replace("@@PARAMS@@", f"{nuestro.num_parametros() / 1e6:.0f}")
|
| 123 |
+
.replace("@@PERDIDA@@", f"{ck['perdida_val']:.3f}")
|
| 124 |
+
.replace("@@EJEMPLOS@@", "\n\n".join(ejemplos)))
|
| 125 |
+
with open(f"{SALIDA}/README.md", "w", encoding="utf-8") as f:
|
| 126 |
+
f.write(ficha)
|
| 127 |
+
print("\n".join(ejemplos))
|
| 128 |
+
|
| 129 |
+
# ------------------------------------------------------------------ código de entrenamiento (para reproducirlo)
|
| 130 |
+
os.makedirs(f"{SALIDA}/entrenamiento", exist_ok=True)
|
| 131 |
+
for f in ["modelo.py", "comun.py", "preparar_datos.py", "maestro.py", "identidad.py", "entrenar.py", "chat.py", "exportar_hf.py", "ficha_modelo.md"]:
|
| 132 |
+
shutil.copy(f, f"{SALIDA}/entrenamiento/{f}")
|
| 133 |
+
for f in ["logs/preentreno.csv", "logs/chat.csv"]:
|
| 134 |
+
if os.path.exists(f):
|
| 135 |
+
shutil.copy(f, f"{SALIDA}/entrenamiento/{os.path.basename(f).replace('.csv', '_perdidas.csv')}")
|
| 136 |
+
with open(f"{SALIDA}/entrenamiento/info.json", "w", encoding="utf-8") as f:
|
| 137 |
+
json.dump({"paso": ck["paso"], "perdida_val_chat": ck["perdida_val"], "parametros": nuestro.num_parametros()}, f, indent=1)
|
| 138 |
+
print(f"\nExportado en {SALIDA}/:")
|
| 139 |
+
for f in sorted(os.listdir(SALIDA)):
|
| 140 |
+
ruta = os.path.join(SALIDA, f)
|
| 141 |
+
print(f" {f:28s} {os.path.getsize(ruta) / 1e6:8.1f} MB" if os.path.isfile(ruta) else f" {f}/")
|
entrenamiento/ficha_modelo.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- es
|
| 4 |
+
license: cc-by-nc-4.0
|
| 5 |
+
library_name: transformers
|
| 6 |
+
pipeline_tag: text-generation
|
| 7 |
+
tags:
|
| 8 |
+
- spanish
|
| 9 |
+
- español
|
| 10 |
+
- tiny
|
| 11 |
+
- llama
|
| 12 |
+
- gguf
|
| 13 |
+
- from-scratch
|
| 14 |
+
- distillation
|
| 15 |
+
datasets:
|
| 16 |
+
- robrenaud/multilingual_tinystories
|
| 17 |
+
- FreedomIntelligence/alpaca-gpt4-spanish
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# Minerva v0.1 🇪🇸
|
| 21 |
+
|
| 22 |
+
**Minerva** es un modelo de lenguaje en español **diminuto** (@@PARAMS@@ millones de parámetros), entrenado
|
| 23 |
+
**desde cero** en un ordenador portátil. Es un modelo de juguete y de aprendizaje: habla español con
|
| 24 |
+
soltura y cuenta cuentos sencillos, pero **sabe muy poco del mundo y se inventa las cosas**.
|
| 25 |
+
|
| 26 |
+
El nombre viene de Minerva, diosa romana de la sabiduría, que según el mito nació de la cabeza de Júpiter.
|
| 27 |
+
Este modelo también "nació de la cabeza" de modelos más grandes: aprendió de textos escritos por ellos
|
| 28 |
+
(destilación).
|
| 29 |
+
|
| 30 |
+
## Qué sabe hacer (y qué no)
|
| 31 |
+
|
| 32 |
+
| ✅ Sí | ❌ No |
|
| 33 |
+
|---|---|
|
| 34 |
+
| Escribir en español con buena gramática | Dar datos fiables (se los inventa con seguridad) |
|
| 35 |
+
| Contar cuentos infantiles coherentes | Matemáticas, razonamiento, programación |
|
| 36 |
+
| Charlar con frases sencillas | Recordar conversaciones largas (ve unas 350 palabras) |
|
| 37 |
+
| Decir quién es y por qué se llama así | Cualquier uso serio: **no lo uses para decisiones reales** |
|
| 38 |
+
|
| 39 |
+
## Ejemplos reales
|
| 40 |
+
|
| 41 |
+
Respuestas generadas automáticamente al exportar el modelo (temperatura 0.7, sin escoger las mejores):
|
| 42 |
+
|
| 43 |
+
@@EJEMPLOS@@
|
| 44 |
+
|
| 45 |
+
## Cómo usarlo
|
| 46 |
+
|
| 47 |
+
### Con LM Studio u Ollama (sin programar)
|
| 48 |
+
|
| 49 |
+
El repositorio incluye el archivo `Minerva-v0.1-F16.gguf`.
|
| 50 |
+
|
| 51 |
+
- **LM Studio**: busca `AxoneSystem/Minerva-v0.1` y descárgalo.
|
| 52 |
+
- **Ollama**: `ollama run hf.co/AxoneSystem/Minerva-v0.1`
|
| 53 |
+
|
| 54 |
+
### Con Python (`transformers`)
|
| 55 |
+
|
| 56 |
+
```python
|
| 57 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 58 |
+
|
| 59 |
+
tok = AutoTokenizer.from_pretrained("AxoneSystem/Minerva-v0.1")
|
| 60 |
+
modelo = AutoModelForCausalLM.from_pretrained("AxoneSystem/Minerva-v0.1")
|
| 61 |
+
|
| 62 |
+
mensajes = [{"role": "user", "content": "¿Quién eres?"}]
|
| 63 |
+
ids = tok.apply_chat_template(mensajes, add_generation_prompt=True, return_tensors="pt")
|
| 64 |
+
salida = modelo.generate(ids, max_new_tokens=150, do_sample=True, temperature=0.7, top_k=30)
|
| 65 |
+
print(tok.decode(salida[0, ids.shape[1]:], skip_special_tokens=True))
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Para que cuente un cuento, dale el principio sin plantilla de chat: `"Había una vez un gato que"`.
|
| 69 |
+
|
| 70 |
+
### Formato de chat
|
| 71 |
+
|
| 72 |
+
```
|
| 73 |
+
<|usuario|>¿Quién eres?<|asistente|>Soy Minerva, un modelo de lenguaje muy pequeño...<|fin|>
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## Cómo se entrenó
|
| 77 |
+
|
| 78 |
+
| | |
|
| 79 |
+
|---|---|
|
| 80 |
+
| Arquitectura | Transformer decodificador tipo Llama: RMSNorm, RoPE, SwiGLU, embeddings compartidos |
|
| 81 |
+
| Tamaño | @@PARAMS@@M parámetros · 8 capas · 512 dimensiones · 8 cabezas · contexto de 512 tokens |
|
| 82 |
+
| Tokenizador | BPE byte-level propio, 8.192 piezas, entrenado solo con español |
|
| 83 |
+
| Hardware | 1 portátil con NVIDIA RTX 5070 Laptop (8 GB) |
|
| 84 |
+
| Tiempo | ~1 h de preentreno + ~1 h de destilación y ajuste |
|
| 85 |
+
|
| 86 |
+
1. **Preentreno** (~280M tokens vistos): ~1 millón de cuentos infantiles en español escritos por
|
| 87 |
+
Gemini 1.5 Flash (`robrenaud/multilingual_tinystories`) + ~50.000 respuestas de GPT-4 en español
|
| 88 |
+
(`FreedomIntelligence/alpaca-gpt4-spanish`). Objetivo: predecir la siguiente palabra.
|
| 89 |
+
2. **Destilación**: [Salamandra-2B-Instruct](https://huggingface.co/BSC-LT/salamandra-2b-instruct)
|
| 90 |
+
(Barcelona Supercomputing Center) respondió miles de preguntas **de forma sencilla y corta**, para
|
| 91 |
+
que un modelo tan pequeño pudiera imitarlo.
|
| 92 |
+
3. **Ajuste de chat**: aprende a responder solo con las partes del asistente (las preguntas no cuentan
|
| 93 |
+
en el aprendizaje) + unas decenas de respuestas escritas a mano sobre su identidad.
|
| 94 |
+
|
| 95 |
+
Pérdida de validación final en el ajuste de chat: @@PERDIDA@@.
|
| 96 |
+
El código completo de entrenamiento está en la carpeta `entrenamiento/`.
|
| 97 |
+
|
| 98 |
+
## Licencia
|
| 99 |
+
|
| 100 |
+
**CC BY-NC 4.0** (uso no comercial). Parte de los datos de ajuste son respuestas de GPT-4 que proceden de
|
| 101 |
+
Alpaca-GPT4, publicado solo para investigación y uso no comercial; por prudencia el modelo hereda esa
|
| 102 |
+
restricción. Los cuentos (Apache-2.0) y el maestro Salamandra (Apache-2.0) no tienen esa limitación.
|
entrenamiento/identidad.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Le enseña al modelo quién es (el nombre está en comun.py). Genera datos/chat_identidad.jsonl con preguntas
|
| 3 |
+
típicas ("¿quién eres?", "¿quién te creó?"...) y respuestas escritas a mano.
|
| 4 |
+
Cámbialo a tu gusto y vuelve a ejecutar `python entrenar.py chat`.
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
import random
|
| 8 |
+
|
| 9 |
+
from comun import NOMBRE
|
| 10 |
+
|
| 11 |
+
random.seed(7)
|
| 12 |
+
|
| 13 |
+
saludos = ["hola", "Hola", "¡Hola!", "buenas", "Buenos días", "Buenas tardes", "hey", "Hola, ¿qué tal?", "¿Qué tal estás?", "Hola {nombre}"]
|
| 14 |
+
resp_saludos = [
|
| 15 |
+
"¡Hola! Soy {nombre}. ¿En qué te puedo ayudar?",
|
| 16 |
+
"¡Hola! ¿Qué tal? Pregúntame lo que quieras.",
|
| 17 |
+
"¡Buenas! Aquí estoy para charlar contigo.",
|
| 18 |
+
"¡Hola! Me alegra hablar contigo. ¿Qué quieres saber?",
|
| 19 |
+
]
|
| 20 |
+
quien = ["¿Quién eres?", "¿Qué eres?", "¿Cómo te llamas?", "¿Cuál es tu nombre?", "Preséntate", "¿Eres ChatGPT?",
|
| 21 |
+
"¿Eres una persona?", "¿Eres un robot?", "Dime quién eres", "¿Qué tipo de inteligencia artificial eres?"]
|
| 22 |
+
resp_quien = [
|
| 23 |
+
"Soy {nombre}, un modelo de lenguaje muy pequeño hecho en España. Todavía estoy aprendiendo, así que a veces me equivoco.",
|
| 24 |
+
"Me llamo {nombre}. Soy una inteligencia artificial pequeñita que habla español. No soy ChatGPT, soy un modelo mucho más sencillo.",
|
| 25 |
+
"Soy {nombre}, una red neuronal tipo GPT con unos 30 millones de parámetros. Aprendí español leyendo cuentos y conversaciones.",
|
| 26 |
+
"Soy {nombre}, un modelo de lenguaje español. No soy una persona, soy un programa de ordenador.",
|
| 27 |
+
]
|
| 28 |
+
creador = ["¿Quién te creó?", "¿Quién te ha hecho?", "¿Quién te entrenó?", "¿De dónde eres?", "¿Cómo te hicieron?", "¿Cómo aprendiste?"]
|
| 29 |
+
resp_creador = [
|
| 30 |
+
"Me entrenaron en un ordenador portátil en España. Unos modelos grandes me enseñaron con sus respuestas, igual que un maestro enseña a un alumno.",
|
| 31 |
+
"Soy de España. Aprendí leyendo un millón de cuentos y copiando las respuestas de modelos más grandes. A eso se le llama destilación.",
|
| 32 |
+
"Me hicieron en España, con una sola tarjeta gráfica. Primero leí muchos cuentos para aprender español y luego aprendí a conversar.",
|
| 33 |
+
]
|
| 34 |
+
limites = ["¿Eres listo?", "¿Sabes mucho?", "¿Te equivocas?", "¿Puedo fiarme de ti?", "¿Qué sabes hacer?"]
|
| 35 |
+
resp_limites = [
|
| 36 |
+
"Sé un poco de todo, pero soy un modelo muy pequeño y me equivoco a menudo. Para cosas importantes, consulta otra fuente.",
|
| 37 |
+
"Puedo contar cuentos y charlar un poco en español. Soy un modelo pequeñito, así que no te fíes mucho de lo que digo.",
|
| 38 |
+
"Intento ayudar, pero todavía estoy aprendiendo. ¡A veces me invento cosas!",
|
| 39 |
+
]
|
| 40 |
+
por_que_nombre = ["¿Por qué te llamas {nombre}?", "¿Qué significa tu nombre?", "¿De dónde viene tu nombre?", "¿Quién era {nombre}?"]
|
| 41 |
+
resp_por_que_nombre = [
|
| 42 |
+
"Me llamo así por {nombre}, la diosa romana de la sabiduría. Según el mito, nació de la cabeza de Júpiter. "
|
| 43 |
+
"Yo también nací de la cabeza de modelos más grandes: ellos me enseñaron y yo aprendí de sus respuestas.",
|
| 44 |
+
"{nombre} era la diosa romana de la sabiduría, hija de Júpiter. Me pusieron su nombre porque aprendí de modelos "
|
| 45 |
+
"mucho más grandes que yo, igual que ella salió de la cabeza de su padre.",
|
| 46 |
+
]
|
| 47 |
+
despedidas = ["adiós", "Adiós", "hasta luego", "Gracias", "gracias, adiós", "Muchas gracias", "Chao"]
|
| 48 |
+
resp_despedidas = ["¡De nada! Hasta pronto.", "¡Adiós! Ha sido un placer charlar contigo.", "¡Gracias a ti! Vuelve cuando quieras."]
|
| 49 |
+
|
| 50 |
+
filas = []
|
| 51 |
+
for preguntas, respuestas, rep in [(saludos, resp_saludos, 3), (quien, resp_quien, 6), (creador, resp_creador, 6),
|
| 52 |
+
(limites, resp_limites, 4), (por_que_nombre, resp_por_que_nombre, 6),
|
| 53 |
+
(despedidas, resp_despedidas, 2)]:
|
| 54 |
+
for q in preguntas:
|
| 55 |
+
for r in random.sample(respuestas, min(2, len(respuestas))):
|
| 56 |
+
turnos = [["usuario", q.format(nombre=NOMBRE)], ["asistente", r.format(nombre=NOMBRE)]]
|
| 57 |
+
filas.append({"turnos": turnos, "repetir": rep})
|
| 58 |
+
|
| 59 |
+
with open("datos/chat_identidad.jsonl", "w", encoding="utf-8") as f:
|
| 60 |
+
for fila in filas:
|
| 61 |
+
f.write(json.dumps(fila, ensure_ascii=False) + "\n")
|
| 62 |
+
print(f"{len(filas)} conversaciones de identidad -> datos/chat_identidad.jsonl")
|
entrenamiento/info.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"paso": 750,
|
| 3 |
+
"perdida_val_chat": 2.2418617248535155,
|
| 4 |
+
"parametros": 29893120
|
| 5 |
+
}
|
entrenamiento/maestro.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Paso 2: DESTILACIÓN. Un modelo grande (el "maestro") escribe conversaciones
|
| 3 |
+
sencillas para que Minerva (el "alumno") aprenda a imitarlas.
|
| 4 |
+
|
| 5 |
+
Es lo mismo que hicieron TinyStories (GPT-4 -> modelos de 30M), Alpaca, Phi, etc.
|
| 6 |
+
El truco: el maestro responde de forma SENCILLA y CORTA, para que un modelo
|
| 7 |
+
diminuto sea capaz de aprenderlo.
|
| 8 |
+
|
| 9 |
+
Dos fuentes de preguntas:
|
| 10 |
+
1) preguntas de alpaca-gpt4-spanish, re-contestadas de forma sencilla
|
| 11 |
+
2) preguntas nuevas que el propio maestro inventa sobre temas (muchos de España)
|
| 12 |
+
|
| 13 |
+
Uso:
|
| 14 |
+
python maestro.py --probar # ver unos ejemplos y la velocidad
|
| 15 |
+
python maestro.py --n 12000 # generar datos/chat_maestro.jsonl
|
| 16 |
+
python maestro.py --maestro maestros/Qwen3-1.7B # usar otro maestro
|
| 17 |
+
"""
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
import random
|
| 21 |
+
import re
|
| 22 |
+
import time
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 26 |
+
|
| 27 |
+
p = argparse.ArgumentParser()
|
| 28 |
+
p.add_argument("--maestro", default="maestros/salamandra-2b-instruct")
|
| 29 |
+
p.add_argument("--n", type=int, default=12000, help="conversaciones a generar")
|
| 30 |
+
p.add_argument("--lote", type=int, default=48)
|
| 31 |
+
p.add_argument("--probar", action="store_true")
|
| 32 |
+
p.add_argument("--salida", default="datos/chat_maestro.jsonl")
|
| 33 |
+
args = p.parse_args()
|
| 34 |
+
random.seed(42)
|
| 35 |
+
|
| 36 |
+
INSTRUCCION = (
|
| 37 |
+
"Eres un asistente amable que habla español de España. "
|
| 38 |
+
"Responde en español, de forma clara y sencilla, en 1 a 4 frases cortas. "
|
| 39 |
+
"No uses listas, ni títulos, ni negritas."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
TEMAS = [
|
| 43 |
+
# España
|
| 44 |
+
"Madrid", "Barcelona", "Sevilla", "Valencia", "Bilbao", "Galicia", "Andalucía", "Cataluña",
|
| 45 |
+
"el País Vasco", "las Islas Canarias", "las Islas Baleares", "Castilla y León", "Asturias",
|
| 46 |
+
"Aragón", "Extremadura", "Murcia", "Navarra", "La Rioja", "Cantabria", "Castilla-La Mancha",
|
| 47 |
+
"la tortilla de patatas", "la paella", "el gazpacho", "el jamón ibérico", "los churros",
|
| 48 |
+
"las tapas", "el aceite de oliva", "la Semana Santa", "las Fallas", "San Fermín",
|
| 49 |
+
"la Feria de Abril", "la Tomatina", "los Reyes Magos", "la Nochevieja y las uvas",
|
| 50 |
+
"el flamenco", "Cervantes y el Quijote", "Velázquez", "Goya", "Picasso", "Dalí", "Gaudí",
|
| 51 |
+
"la Sagrada Familia", "la Alhambra", "el Museo del Prado", "el Camino de Santiago",
|
| 52 |
+
"el río Ebro", "los Pirineos", "el Teide", "el idioma español", "el catalán", "el gallego",
|
| 53 |
+
"el euskera", "la siesta", "el fútbol en España", "el clima de España", "la Constitución española",
|
| 54 |
+
# Ciencia y naturaleza
|
| 55 |
+
"el sol", "la luna", "las estrellas", "los planetas", "la lluvia", "el arcoíris", "los volcanes",
|
| 56 |
+
"los océanos", "las plantas", "los árboles", "los dinosaurios", "los perros", "los gatos",
|
| 57 |
+
"los pájaros", "las abejas", "los peces", "las ballenas", "el agua", "el aire", "el fuego",
|
| 58 |
+
"la electricidad", "los imanes", "el cuerpo humano", "el corazón", "los huesos", "los dientes",
|
| 59 |
+
"los sueños", "las estaciones del año", "el tiempo", "la gravedad", "la luz", "el sonido",
|
| 60 |
+
# Vida diaria
|
| 61 |
+
"la amistad", "la familia", "el colegio", "los deportes", "la música", "los libros", "cocinar",
|
| 62 |
+
"la comida sana", "dormir bien", "hacer ejercicio", "el dinero", "los ordenadores", "internet",
|
| 63 |
+
"los teléfonos móviles", "la inteligencia artificial", "los robots", "los trenes", "los aviones",
|
| 64 |
+
"los coches", "las bicicletas", "el reciclaje", "cuidar el planeta", "las emociones",
|
| 65 |
+
"estar triste", "estar contento", "pedir perdón", "dar las gracias", "aprender idiomas",
|
| 66 |
+
"las matemáticas", "la historia", "el arte", "el cine", "los juegos", "los cumpleaños",
|
| 67 |
+
"la Navidad", "el verano", "el invierno", "la playa", "la montaña", "el campo", "la ciudad",
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def cargar_maestro():
|
| 72 |
+
tok = AutoTokenizer.from_pretrained(args.maestro)
|
| 73 |
+
tok.padding_side = "left"
|
| 74 |
+
if tok.pad_token is None:
|
| 75 |
+
tok.pad_token = tok.eos_token
|
| 76 |
+
modelo = AutoModelForCausalLM.from_pretrained(args.maestro, dtype=torch.bfloat16, device_map="cuda")
|
| 77 |
+
modelo.eval()
|
| 78 |
+
return tok, modelo
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def plantilla(tok, mensajes):
|
| 82 |
+
extra = {"enable_thinking": False} if "qwen3" in args.maestro.lower() else {}
|
| 83 |
+
return tok.apply_chat_template(mensajes, tokenize=False, add_generation_prompt=True, **extra)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@torch.no_grad()
|
| 87 |
+
def generar(tok, modelo, lista_mensajes, max_tokens, temperatura):
|
| 88 |
+
textos = [plantilla(tok, m) for m in lista_mensajes]
|
| 89 |
+
enc = tok(textos, return_tensors="pt", padding=True, add_special_tokens=False).to("cuda")
|
| 90 |
+
salida = modelo.generate(
|
| 91 |
+
**enc, max_new_tokens=max_tokens, do_sample=True, temperature=temperatura, top_p=0.9,
|
| 92 |
+
repetition_penalty=1.1, pad_token_id=tok.pad_token_id,
|
| 93 |
+
)
|
| 94 |
+
nuevos = salida[:, enc["input_ids"].shape[1] :]
|
| 95 |
+
return [t.strip() for t in tok.batch_decode(nuevos, skip_special_tokens=True)]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def respuesta_valida(r):
|
| 99 |
+
if not (15 <= len(r) <= 700):
|
| 100 |
+
return False
|
| 101 |
+
if re.search(r"(\*\*|^#|^\s*[-•]\s|^\s*\d+\.\s)", r, re.M): # listas o markdown
|
| 102 |
+
return False
|
| 103 |
+
if r[-1] not in ".!?»)\"": # cortada a medias
|
| 104 |
+
return False
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def preguntas_alpaca(n):
|
| 109 |
+
with open("datos/crudos/alpaca-gpt4-spanish.json", encoding="utf-8") as f:
|
| 110 |
+
datos = json.load(f)
|
| 111 |
+
evitar = re.compile(r"c[oó]digo|python|java|sql|html|css|funci[oó]n|programa|traduc|ingl[eé]s|ecuaci[oó]n|"
|
| 112 |
+
r"\d+\s*[x×*/+-]\s*\d+|tabla|lista de|enumera|clasifica|reescrib|resume|edita|corrige", re.I)
|
| 113 |
+
qs = [d["conversations"][0]["value"].strip() for d in datos]
|
| 114 |
+
qs = [q for q in qs if 10 < len(q) < 160 and "\n" not in q and not evitar.search(q)]
|
| 115 |
+
random.shuffle(qs)
|
| 116 |
+
return qs[:n]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def main():
|
| 120 |
+
t0 = time.time()
|
| 121 |
+
tok, modelo = cargar_maestro()
|
| 122 |
+
print(f"Maestro cargado: {args.maestro} ({modelo.num_parameters() / 1e9:.2f} B parámetros) en {time.time() - t0:.0f}s")
|
| 123 |
+
|
| 124 |
+
if args.probar:
|
| 125 |
+
qs = ["¿Por qué el cielo es azul?", "¿Cuál es la capital de España?", "¿Qué es la paella?",
|
| 126 |
+
"Dame un consejo para dormir mejor.", "¿Quién escribió el Quijote?", "¿Qué es un volcán?"]
|
| 127 |
+
t0 = time.time()
|
| 128 |
+
rs = generar(tok, modelo, [[{"role": "system", "content": INSTRUCCION}, {"role": "user", "content": q}] for q in qs], 160, 0.7)
|
| 129 |
+
for q, r in zip(qs, rs):
|
| 130 |
+
print(f"\nP: {q}\nR: {r} [{'OK' if respuesta_valida(r) else 'descartada'}]")
|
| 131 |
+
print(f"\n{len(qs)} respuestas en {time.time() - t0:.1f}s")
|
| 132 |
+
return
|
| 133 |
+
|
| 134 |
+
# 1) preguntas: mitad de alpaca, mitad inventadas por el maestro sobre temas
|
| 135 |
+
n_alpaca = args.n // 2
|
| 136 |
+
preguntas = preguntas_alpaca(n_alpaca)
|
| 137 |
+
n_temas = args.n - len(preguntas)
|
| 138 |
+
print(f"Inventando {n_temas} preguntas sobre {len(TEMAS)} temas...")
|
| 139 |
+
estilos = ["una pregunta sencilla", "una pregunta curiosa", "una pregunta de un niño", "una pregunta práctica",
|
| 140 |
+
"una pregunta de un turista", "una petición de consejo"]
|
| 141 |
+
pedidos = [
|
| 142 |
+
[{"role": "user", "content": f"Escribe {random.choice(estilos)} que alguien podría hacer sobre {random.choice(TEMAS)}. "
|
| 143 |
+
"Escribe solo la pregunta, en español, en una línea."}]
|
| 144 |
+
for _ in range(n_temas)
|
| 145 |
+
]
|
| 146 |
+
for i in range(0, len(pedidos), args.lote):
|
| 147 |
+
for q in generar(tok, modelo, pedidos[i : i + args.lote], 48, 1.0):
|
| 148 |
+
q = q.split("\n")[0].strip().strip('"«»')
|
| 149 |
+
if 8 < len(q) < 200:
|
| 150 |
+
preguntas.append(q)
|
| 151 |
+
print(f" preguntas: {len(preguntas)}", end="\r")
|
| 152 |
+
random.shuffle(preguntas)
|
| 153 |
+
|
| 154 |
+
# 2) respuestas sencillas del maestro
|
| 155 |
+
print(f"\nEl maestro está contestando {len(preguntas)} preguntas...")
|
| 156 |
+
t0 = time.time()
|
| 157 |
+
guardadas = 0
|
| 158 |
+
with open(args.salida, "w", encoding="utf-8") as f:
|
| 159 |
+
for i in range(0, len(preguntas), args.lote):
|
| 160 |
+
trozo = preguntas[i : i + args.lote]
|
| 161 |
+
mensajes = [[{"role": "system", "content": INSTRUCCION}, {"role": "user", "content": q}] for q in trozo]
|
| 162 |
+
for q, r in zip(trozo, generar(tok, modelo, mensajes, 200, 0.7)):
|
| 163 |
+
if respuesta_valida(r):
|
| 164 |
+
fila = {"turnos": [["usuario", q], ["asistente", r]], "maestro": args.maestro, "repetir": 2}
|
| 165 |
+
f.write(json.dumps(fila, ensure_ascii=False) + "\n")
|
| 166 |
+
guardadas += 1
|
| 167 |
+
hecho = i + len(trozo)
|
| 168 |
+
ritmo = hecho / (time.time() - t0)
|
| 169 |
+
print(f" {hecho}/{len(preguntas)} | guardadas {guardadas} | faltan ~{(len(preguntas) - hecho) / ritmo / 60:.0f} min", flush=True)
|
| 170 |
+
print(f"Listo: {guardadas} conversaciones -> {args.salida}")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
main()
|
entrenamiento/modelo.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
La red neuronal: un Transformer tipo GPT (solo decodificador), igual en esencia
|
| 3 |
+
que ChatGPT / Llama, pero diminuto.
|
| 4 |
+
|
| 5 |
+
Piezas modernas que usa (las mismas que Llama, Qwen, Salamandra...):
|
| 6 |
+
- RMSNorm en vez de LayerNorm
|
| 7 |
+
- RoPE (posiciones rotatorias) en la atención
|
| 8 |
+
- SwiGLU en la capa feed-forward
|
| 9 |
+
- Embeddings de entrada y salida compartidos
|
| 10 |
+
"""
|
| 11 |
+
import math
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
import torch.utils.checkpoint
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class Config:
|
| 22 |
+
vocab: int = 8192
|
| 23 |
+
contexto: int = 512 # cuántos tokens "ve" a la vez
|
| 24 |
+
capas: int = 8
|
| 25 |
+
cabezas: int = 8
|
| 26 |
+
dim: int = 512
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class RMSNorm(nn.Module):
|
| 30 |
+
def __init__(self, dim, eps=1e-6):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.eps = eps
|
| 33 |
+
self.peso = nn.Parameter(torch.ones(dim))
|
| 34 |
+
|
| 35 |
+
def forward(self, x):
|
| 36 |
+
return self.peso * x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def tabla_rope(dim_cabeza, contexto, base=10000.0):
|
| 40 |
+
frec = 1.0 / (base ** (torch.arange(0, dim_cabeza, 2).float() / dim_cabeza))
|
| 41 |
+
t = torch.arange(contexto).float()
|
| 42 |
+
angulos = torch.outer(t, frec)
|
| 43 |
+
return torch.cos(angulos), torch.sin(angulos)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def aplicar_rope(x, cos, sin, inicio=0):
|
| 47 |
+
# x: (B, cabezas, T, dim_cabeza); inicio = posición del primer token (para la caché)
|
| 48 |
+
x1, x2 = x[..., ::2], x[..., 1::2]
|
| 49 |
+
T = x.shape[2]
|
| 50 |
+
cos, sin = cos[inicio : inicio + T].to(x.dtype), sin[inicio : inicio + T].to(x.dtype)
|
| 51 |
+
return torch.stack((x1 * cos - x2 * sin, x1 * sin + x2 * cos), dim=-1).flatten(-2)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class Atencion(nn.Module):
|
| 55 |
+
def __init__(self, c: Config):
|
| 56 |
+
super().__init__()
|
| 57 |
+
self.cabezas = c.cabezas
|
| 58 |
+
self.qkv = nn.Linear(c.dim, 3 * c.dim, bias=False)
|
| 59 |
+
self.salida = nn.Linear(c.dim, c.dim, bias=False)
|
| 60 |
+
|
| 61 |
+
def forward(self, x, cos, sin, cache=None, inicio=0):
|
| 62 |
+
B, T, D = x.shape
|
| 63 |
+
q, k, v = self.qkv(x).split(D, dim=2)
|
| 64 |
+
q, k, v = (t.view(B, T, self.cabezas, D // self.cabezas).transpose(1, 2) for t in (q, k, v))
|
| 65 |
+
q, k = aplicar_rope(q, cos, sin, inicio), aplicar_rope(k, cos, sin, inicio)
|
| 66 |
+
if cache is not None: # al generar: recordar k y v de los tokens anteriores
|
| 67 |
+
if "k" in cache:
|
| 68 |
+
k, v = torch.cat((cache["k"], k), dim=2), torch.cat((cache["v"], v), dim=2)
|
| 69 |
+
cache["k"], cache["v"] = k, v
|
| 70 |
+
# cada token solo mira hacia atrás (con caché y 1 token nuevo, todo lo anterior es pasado)
|
| 71 |
+
y = F.scaled_dot_product_attention(q, k, v, is_causal=T > 1)
|
| 72 |
+
return self.salida(y.transpose(1, 2).reshape(B, T, D))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class FeedForward(nn.Module):
|
| 76 |
+
def __init__(self, c: Config):
|
| 77 |
+
super().__init__()
|
| 78 |
+
oculta = int(8 * c.dim / 3 / 64 + 0.999) * 64 # SwiGLU: ~2/3 de 4*dim
|
| 79 |
+
self.w1 = nn.Linear(c.dim, oculta, bias=False)
|
| 80 |
+
self.w3 = nn.Linear(c.dim, oculta, bias=False)
|
| 81 |
+
self.w2 = nn.Linear(oculta, c.dim, bias=False)
|
| 82 |
+
|
| 83 |
+
def forward(self, x):
|
| 84 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class Bloque(nn.Module):
|
| 88 |
+
def __init__(self, c: Config):
|
| 89 |
+
super().__init__()
|
| 90 |
+
self.norm1, self.atencion = RMSNorm(c.dim), Atencion(c)
|
| 91 |
+
self.norm2, self.ff = RMSNorm(c.dim), FeedForward(c)
|
| 92 |
+
|
| 93 |
+
def forward(self, x, cos, sin, cache=None, inicio=0):
|
| 94 |
+
x = x + self.atencion(self.norm1(x), cos, sin, cache, inicio)
|
| 95 |
+
return x + self.ff(self.norm2(x))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class GPT(nn.Module):
|
| 99 |
+
def __init__(self, c: Config):
|
| 100 |
+
super().__init__()
|
| 101 |
+
self.config = c
|
| 102 |
+
self.embedding = nn.Embedding(c.vocab, c.dim)
|
| 103 |
+
self.bloques = nn.ModuleList(Bloque(c) for _ in range(c.capas))
|
| 104 |
+
self.norm_final = RMSNorm(c.dim)
|
| 105 |
+
self.cabeza = nn.Linear(c.dim, c.vocab, bias=False)
|
| 106 |
+
self.cabeza.weight = self.embedding.weight # pesos compartidos
|
| 107 |
+
cos, sin = tabla_rope(c.dim // c.cabezas, c.contexto)
|
| 108 |
+
self.register_buffer("cos", cos, persistent=False)
|
| 109 |
+
self.register_buffer("sin", sin, persistent=False)
|
| 110 |
+
self.checkpointing = False # True = ahorra memoria recalculando activaciones (para Juno)
|
| 111 |
+
self.apply(self._iniciar)
|
| 112 |
+
for nombre, p in self.named_parameters():
|
| 113 |
+
if nombre.endswith("salida.weight") or nombre.endswith("w2.weight"):
|
| 114 |
+
nn.init.normal_(p, std=0.02 / math.sqrt(2 * c.capas))
|
| 115 |
+
|
| 116 |
+
def _iniciar(self, m):
|
| 117 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 118 |
+
nn.init.normal_(m.weight, std=0.02)
|
| 119 |
+
|
| 120 |
+
def num_parametros(self):
|
| 121 |
+
return sum(p.numel() for p in self.parameters())
|
| 122 |
+
|
| 123 |
+
def forward(self, idx, objetivos=None, mascara=None, cache=None, inicio=0):
|
| 124 |
+
x = self.embedding(idx)
|
| 125 |
+
for i, b in enumerate(self.bloques):
|
| 126 |
+
if self.checkpointing and self.training:
|
| 127 |
+
x = torch.utils.checkpoint.checkpoint(b, x, self.cos, self.sin, use_reentrant=False)
|
| 128 |
+
else:
|
| 129 |
+
x = b(x, self.cos, self.sin, None if cache is None else cache[i], inicio)
|
| 130 |
+
if objetivos is None: # generando: solo hace falta el último token
|
| 131 |
+
return self.cabeza(self.norm_final(x[:, -1:, :])), None
|
| 132 |
+
logits = self.cabeza(self.norm_final(x))
|
| 133 |
+
perdida = F.cross_entropy(logits.float().view(-1, logits.size(-1)), objetivos.view(-1), reduction="none")
|
| 134 |
+
if mascara is None:
|
| 135 |
+
return logits, perdida.mean()
|
| 136 |
+
mascara = mascara.view(-1).float()
|
| 137 |
+
return logits, (perdida * mascara).sum() / mascara.sum().clamp(min=1)
|
| 138 |
+
|
| 139 |
+
@torch.no_grad()
|
| 140 |
+
def generar(self, idx, max_tokens=200, temperatura=0.8, top_k=40, parar_en=None):
|
| 141 |
+
"""Genera con caché de k/v: cada token nuevo solo procesa 1 posición."""
|
| 142 |
+
idx = idx[:, -(self.config.contexto - max_tokens) :] if max_tokens < self.config.contexto else idx[:, -1:]
|
| 143 |
+
cache = [{} for _ in self.bloques]
|
| 144 |
+
logits, _ = self(idx, cache=cache)
|
| 145 |
+
for _ in range(max_tokens):
|
| 146 |
+
logits = logits[:, -1, :].float()
|
| 147 |
+
if temperatura <= 0:
|
| 148 |
+
siguiente = logits.argmax(-1, keepdim=True)
|
| 149 |
+
else:
|
| 150 |
+
logits = logits / temperatura
|
| 151 |
+
if top_k:
|
| 152 |
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
| 153 |
+
logits[logits < v[:, [-1]]] = -float("inf")
|
| 154 |
+
siguiente = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
| 155 |
+
idx = torch.cat((idx, siguiente), dim=1)
|
| 156 |
+
if (parar_en is not None and siguiente.item() in parar_en) or idx.size(1) >= self.config.contexto:
|
| 157 |
+
break
|
| 158 |
+
logits, _ = self(siguiente, cache=cache, inicio=idx.size(1) - 1)
|
| 159 |
+
return idx
|
entrenamiento/preentreno_perdidas.csv
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
paso,perdida_train,perdida_val,lr,tokens_por_seg
|
| 2 |
+
0,,9.0940,,
|
| 3 |
+
0,9.0850,,0.000005,0
|
| 4 |
+
25,6.5048,,0.000130,75413
|
| 5 |
+
50,4.5096,,0.000255,75410
|
| 6 |
+
75,3.7175,,0.000380,75082
|
| 7 |
+
100,3.1970,,0.000505,74618
|
| 8 |
+
125,2.8841,,0.000630,74219
|
| 9 |
+
150,2.5233,,0.000755,73884
|
| 10 |
+
175,2.4021,,0.000880,73637
|
| 11 |
+
200,2.4235,,0.001000,73377
|
| 12 |
+
225,2.1661,,0.001000,73520
|
| 13 |
+
250,,2.1816,,
|
| 14 |
+
250,2.0543,,0.001000,66586
|
| 15 |
+
275,1.9887,,0.000999,73255
|
| 16 |
+
300,1.9894,,0.000999,73011
|
| 17 |
+
325,1.9059,,0.000998,73036
|
| 18 |
+
350,1.7943,,0.000997,72983
|
| 19 |
+
375,1.8847,,0.000996,73106
|
| 20 |
+
400,1.9629,,0.000995,73040
|
| 21 |
+
425,1.8058,,0.000993,72926
|
| 22 |
+
450,1.6687,,0.000991,72866
|
| 23 |
+
475,1.7674,,0.000990,73170
|
| 24 |
+
500,,1.7825,,
|
| 25 |
+
500,1.7968,,0.000988,65951
|
| 26 |
+
525,1.8742,,0.000986,73466
|
| 27 |
+
550,1.7355,,0.000983,73289
|
| 28 |
+
575,1.6160,,0.000981,73017
|
| 29 |
+
600,1.7174,,0.000978,73102
|
| 30 |
+
625,1.7388,,0.000976,73370
|
| 31 |
+
650,1.7017,,0.000973,73235
|
| 32 |
+
675,1.7292,,0.000970,73396
|
| 33 |
+
700,1.6019,,0.000966,73536
|
| 34 |
+
725,1.7276,,0.000963,73240
|
| 35 |
+
750,,1.6864,,
|
| 36 |
+
750,1.5838,,0.000959,65998
|
| 37 |
+
775,1.6387,,0.000956,73169
|
| 38 |
+
800,1.6661,,0.000952,73113
|
| 39 |
+
825,1.5309,,0.000948,72931
|
| 40 |
+
850,1.4915,,0.000944,72921
|
| 41 |
+
875,1.6597,,0.000939,72930
|
| 42 |
+
900,1.5196,,0.000935,73079
|
| 43 |
+
925,1.6002,,0.000930,72819
|
| 44 |
+
950,1.5956,,0.000925,72519
|
| 45 |
+
975,1.6519,,0.000920,72008
|
| 46 |
+
1000,,1.5787,,
|
| 47 |
+
1000,1.5421,,0.000915,64758
|
| 48 |
+
1025,1.5306,,0.000910,71574
|
| 49 |
+
1050,1.4661,,0.000905,71333
|
| 50 |
+
1075,1.5832,,0.000899,70585
|
| 51 |
+
1100,1.5751,,0.000894,69174
|
| 52 |
+
1125,1.5792,,0.000888,70300
|
| 53 |
+
1150,1.5443,,0.000882,71428
|
| 54 |
+
1175,1.5095,,0.000876,71866
|
| 55 |
+
1200,1.5545,,0.000870,70214
|
| 56 |
+
1225,1.5694,,0.000864,69062
|
| 57 |
+
1250,,1.5390,,
|
| 58 |
+
1250,1.5006,,0.000858,59647
|
| 59 |
+
1275,1.6079,,0.000851,69414
|
| 60 |
+
1300,1.4556,,0.000845,71129
|
| 61 |
+
1325,1.5899,,0.000838,71755
|
| 62 |
+
1350,1.4234,,0.000831,72148
|
| 63 |
+
1375,1.4959,,0.000824,69821
|
| 64 |
+
1400,1.4595,,0.000817,66873
|
| 65 |
+
1425,1.5758,,0.000810,66460
|
| 66 |
+
1450,1.4525,,0.000803,66149
|
| 67 |
+
1475,1.5975,,0.000796,65746
|
| 68 |
+
1500,,1.5019,,
|
| 69 |
+
1500,1.4778,,0.000788,60166
|
| 70 |
+
1525,1.5105,,0.000781,66260
|
| 71 |
+
1550,1.5034,,0.000773,66121
|
| 72 |
+
1575,1.4774,,0.000766,65942
|
| 73 |
+
1600,1.3869,,0.000758,65197
|
| 74 |
+
1625,1.5230,,0.000750,66595
|
| 75 |
+
1650,1.5015,,0.000742,66147
|
| 76 |
+
1675,1.4225,,0.000734,65989
|
| 77 |
+
1700,1.4899,,0.000726,65969
|
| 78 |
+
1725,1.3340,,0.000718,65995
|
| 79 |
+
1750,,1.4575,,
|
| 80 |
+
1750,1.3761,,0.000710,59339
|
| 81 |
+
1775,1.5018,,0.000702,67656
|
| 82 |
+
1800,1.4702,,0.000693,68379
|
| 83 |
+
1825,1.3838,,0.000685,68633
|
| 84 |
+
1850,1.4483,,0.000677,68443
|
| 85 |
+
1875,1.4907,,0.000668,67564
|
| 86 |
+
1900,1.3804,,0.000660,67034
|
| 87 |
+
1925,1.4077,,0.000651,67723
|
| 88 |
+
1950,1.3633,,0.000643,66967
|
| 89 |
+
1975,1.4314,,0.000634,66472
|
| 90 |
+
2000,,1.4411,,
|
| 91 |
+
2000,1.4946,,0.000626,60696
|
| 92 |
+
2025,1.4347,,0.000617,67709
|
| 93 |
+
2050,1.3295,,0.000608,68321
|
| 94 |
+
2075,1.4065,,0.000600,70898
|
| 95 |
+
2100,1.3599,,0.000591,71519
|
| 96 |
+
2125,1.4682,,0.000582,72186
|
| 97 |
+
2150,1.3989,,0.000573,72880
|
| 98 |
+
2175,1.4323,,0.000565,73159
|
| 99 |
+
2200,1.3553,,0.000556,73265
|
| 100 |
+
2225,1.3650,,0.000547,73463
|
| 101 |
+
2250,,1.4254,,
|
| 102 |
+
2250,1.4845,,0.000538,66730
|
| 103 |
+
2275,1.4193,,0.000530,73563
|
| 104 |
+
2300,1.3363,,0.000521,73255
|
| 105 |
+
2325,1.3382,,0.000512,72949
|
| 106 |
+
2350,1.2987,,0.000503,72934
|
| 107 |
+
2375,1.3427,,0.000495,72926
|
| 108 |
+
2400,1.3849,,0.000486,72840
|
| 109 |
+
2425,1.3384,,0.000477,72852
|
| 110 |
+
2450,1.3014,,0.000469,72667
|
| 111 |
+
2475,1.3265,,0.000460,72782
|
| 112 |
+
2500,,1.3695,,
|
| 113 |
+
2500,1.3153,,0.000451,65614
|
| 114 |
+
2525,1.3540,,0.000443,72642
|
| 115 |
+
2550,1.3417,,0.000434,72112
|
| 116 |
+
2575,1.2804,,0.000426,72395
|
| 117 |
+
2600,1.3640,,0.000418,71662
|
| 118 |
+
2625,1.4105,,0.000409,72102
|
| 119 |
+
2650,1.3423,,0.000401,72229
|
| 120 |
+
2675,1.3431,,0.000393,72366
|
| 121 |
+
2700,1.4083,,0.000384,71821
|
| 122 |
+
2725,1.2765,,0.000376,71729
|
| 123 |
+
2750,,1.3753,,
|
| 124 |
+
2750,1.3923,,0.000368,65470
|
| 125 |
+
2775,1.3539,,0.000360,72083
|
| 126 |
+
2800,1.2945,,0.000352,72185
|
| 127 |
+
2825,1.3152,,0.000345,72412
|
| 128 |
+
2850,1.2479,,0.000337,71987
|
| 129 |
+
2875,1.2483,,0.000329,72516
|
| 130 |
+
2900,1.3602,,0.000322,72591
|
| 131 |
+
2925,1.3734,,0.000314,71523
|
| 132 |
+
2950,1.3755,,0.000307,71717
|
| 133 |
+
2975,1.2761,,0.000299,71650
|
| 134 |
+
3000,,1.3465,,
|
| 135 |
+
3000,1.3338,,0.000292,64222
|
| 136 |
+
3025,1.3120,,0.000285,71337
|
| 137 |
+
3050,1.3048,,0.000278,71339
|
| 138 |
+
3075,1.3044,,0.000271,71503
|
| 139 |
+
3100,1.2848,,0.000264,72611
|
| 140 |
+
3125,1.3064,,0.000257,73049
|
| 141 |
+
3150,1.2515,,0.000251,73280
|
| 142 |
+
3175,1.2691,,0.000244,73475
|
| 143 |
+
3200,1.3571,,0.000238,73609
|
| 144 |
+
3225,1.2302,,0.000232,73652
|
| 145 |
+
3250,,1.3669,,
|
| 146 |
+
3250,1.3235,,0.000226,66927
|
| 147 |
+
3275,1.2430,,0.000220,73811
|
| 148 |
+
3300,1.3265,,0.000214,73714
|
| 149 |
+
3325,1.3275,,0.000208,73926
|
| 150 |
+
3350,1.2480,,0.000202,73830
|
| 151 |
+
3375,1.3304,,0.000197,73915
|
| 152 |
+
3400,1.3360,,0.000191,73796
|
| 153 |
+
3425,1.2725,,0.000186,73955
|
| 154 |
+
3450,1.2637,,0.000181,73976
|
| 155 |
+
3475,1.3091,,0.000176,73998
|
| 156 |
+
3500,,1.3474,,
|
| 157 |
+
3500,1.2067,,0.000171,67073
|
| 158 |
+
3525,1.3096,,0.000167,73841
|
| 159 |
+
3550,1.2450,,0.000162,73923
|
| 160 |
+
3575,1.2156,,0.000158,73920
|
| 161 |
+
3600,1.2824,,0.000154,73903
|
| 162 |
+
3625,1.2234,,0.000150,73953
|
| 163 |
+
3650,1.3159,,0.000146,73837
|
| 164 |
+
3675,1.3265,,0.000142,73850
|
| 165 |
+
3700,1.3669,,0.000138,73873
|
| 166 |
+
3725,1.3347,,0.000135,74024
|
| 167 |
+
3750,,1.3053,,
|
| 168 |
+
3750,1.2511,,0.000131,66805
|
| 169 |
+
3775,1.3485,,0.000128,74167
|
| 170 |
+
3800,1.2438,,0.000125,74111
|
| 171 |
+
3825,1.2686,,0.000123,74057
|
| 172 |
+
3850,1.2598,,0.000120,74134
|
| 173 |
+
3875,1.3040,,0.000117,74086
|
| 174 |
+
3900,1.3205,,0.000115,74100
|
| 175 |
+
3925,1.2600,,0.000113,74080
|
| 176 |
+
3950,1.2536,,0.000111,74104
|
| 177 |
+
3975,1.3220,,0.000109,74162
|
| 178 |
+
4000,,1.3467,,
|
| 179 |
+
4000,1.2605,,0.000107,67438
|
| 180 |
+
4025,1.2687,,0.000106,74177
|
| 181 |
+
4050,1.2641,,0.000105,74142
|
| 182 |
+
4075,1.2007,,0.000103,74174
|
| 183 |
+
4100,1.2531,,0.000102,74030
|
| 184 |
+
4125,1.2442,,0.000102,74357
|
| 185 |
+
4150,1.1855,,0.000101,74050
|
| 186 |
+
4175,1.1865,,0.000100,74219
|
| 187 |
+
4200,1.2182,,0.000100,74088
|
| 188 |
+
4225,1.2210,,0.000100,74143
|
| 189 |
+
4233,,1.3103,,
|
entrenamiento/preparar_datos.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Paso 1: preparar los datos y entrenar el tokenizador.
|
| 3 |
+
|
| 4 |
+
Datos de partida (ya generados por modelos GRANDES, o sea, destilación):
|
| 5 |
+
- datos/crudos/stories_*.json -> cuentos infantiles en español escritos por Gemini 1.5 Flash
|
| 6 |
+
- datos/crudos/alpaca-gpt4-spanish.json -> preguntas y respuestas escritas por GPT-4
|
| 7 |
+
|
| 8 |
+
Salida:
|
| 9 |
+
- datos/tokenizador.json (BPE de 8192 piezas, entrenado solo con español)
|
| 10 |
+
- datos/preentreno_train.bin / _val.bin (tokens uint16 para el preentrenamiento)
|
| 11 |
+
"""
|
| 12 |
+
import glob
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import random
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
from tokenizers import Tokenizer, decoders, models, pre_tokenizers, trainers
|
| 19 |
+
|
| 20 |
+
from comun import ESPECIALES, FIN, formatear_chat
|
| 21 |
+
|
| 22 |
+
random.seed(1234)
|
| 23 |
+
os.makedirs("datos", exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# ---------------------------------------------------------------- 1) leer textos
|
| 26 |
+
print("Leyendo cuentos...")
|
| 27 |
+
cuentos = []
|
| 28 |
+
for ruta in sorted(glob.glob("datos/crudos/stories_*.json")):
|
| 29 |
+
with open(ruta, encoding="utf-8") as f:
|
| 30 |
+
for item in json.load(f):
|
| 31 |
+
texto = item["story"].strip()
|
| 32 |
+
if len(texto) > 100:
|
| 33 |
+
cuentos.append(texto)
|
| 34 |
+
print(f" {len(cuentos):,} cuentos")
|
| 35 |
+
|
| 36 |
+
print("Leyendo conversaciones de GPT-4...")
|
| 37 |
+
with open("datos/crudos/alpaca-gpt4-spanish.json", encoding="utf-8") as f:
|
| 38 |
+
alpaca = json.load(f)
|
| 39 |
+
chats = []
|
| 40 |
+
for item in alpaca:
|
| 41 |
+
turnos = [(c["from"], c["value"].strip()) for c in item["conversations"]]
|
| 42 |
+
if all(v for _, v in turnos):
|
| 43 |
+
chats.append(formatear_chat([("usuario" if q == "human" else "asistente", v) for q, v in turnos]))
|
| 44 |
+
print(f" {len(chats):,} conversaciones")
|
| 45 |
+
|
| 46 |
+
# ---------------------------------------------------------------- 2) tokenizador
|
| 47 |
+
if not os.path.exists("datos/tokenizador.json"):
|
| 48 |
+
print("Entrenando tokenizador BPE (8192 piezas)...")
|
| 49 |
+
tok = Tokenizer(models.BPE())
|
| 50 |
+
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
|
| 51 |
+
tok.decoder = decoders.ByteLevel()
|
| 52 |
+
entrenador = trainers.BpeTrainer(
|
| 53 |
+
vocab_size=8192,
|
| 54 |
+
special_tokens=ESPECIALES,
|
| 55 |
+
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
|
| 56 |
+
min_frequency=2,
|
| 57 |
+
)
|
| 58 |
+
muestra = random.sample(cuentos, 300_000) + chats
|
| 59 |
+
tok.train_from_iterator(muestra, trainer=entrenador)
|
| 60 |
+
tok.save("datos/tokenizador.json")
|
| 61 |
+
tok = Tokenizer.from_file("datos/tokenizador.json")
|
| 62 |
+
fin_id = tok.token_to_id(FIN)
|
| 63 |
+
print(" ejemplo:", tok.encode("¡Hola! ¿Cómo estás? Me llamo Minerva.").tokens)
|
| 64 |
+
|
| 65 |
+
# ---------------------------------------------------------------- 3) codificar
|
| 66 |
+
# Preentrenamiento = cuentos + conversaciones completas (sin máscara).
|
| 67 |
+
documentos = [c + FIN for c in cuentos] + chats
|
| 68 |
+
random.shuffle(documentos)
|
| 69 |
+
n_val = 5000
|
| 70 |
+
partes = {"val": documentos[:n_val], "train": documentos[n_val:]}
|
| 71 |
+
|
| 72 |
+
for nombre, docs in partes.items():
|
| 73 |
+
print(f"Codificando {nombre} ({len(docs):,} documentos)...")
|
| 74 |
+
trozos = []
|
| 75 |
+
for i in range(0, len(docs), 20_000):
|
| 76 |
+
for enc in tok.encode_batch(docs[i : i + 20_000]):
|
| 77 |
+
trozos.append(np.array(enc.ids, dtype=np.uint16))
|
| 78 |
+
arr = np.concatenate(trozos)
|
| 79 |
+
arr.tofile(f"datos/preentreno_{nombre}.bin")
|
| 80 |
+
print(f" {len(arr):,} tokens -> datos/preentreno_{nombre}.bin")
|
generation_config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"bos_token_id": 0,
|
| 4 |
+
"do_sample": true,
|
| 5 |
+
"eos_token_id": 0,
|
| 6 |
+
"max_new_tokens": 200,
|
| 7 |
+
"output_attentions": false,
|
| 8 |
+
"output_hidden_states": false,
|
| 9 |
+
"pad_token_id": 0,
|
| 10 |
+
"temperature": 0.7,
|
| 11 |
+
"top_k": 30,
|
| 12 |
+
"transformers_version": "5.19.0",
|
| 13 |
+
"use_cache": true
|
| 14 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4fb26432a3d2cc445dcb1d018f95b800664f6485097f6243bb94335791338d3e
|
| 3 |
+
size 119580576
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"bos_token": "<|fin|>",
|
| 4 |
+
"eos_token": "<|fin|>",
|
| 5 |
+
"extra_special_tokens": [
|
| 6 |
+
"<|usuario|>",
|
| 7 |
+
"<|asistente|>"
|
| 8 |
+
],
|
| 9 |
+
"model_max_length": 512,
|
| 10 |
+
"pad_token": "<|fin|>",
|
| 11 |
+
"tokenizer_class": "TokenizersBackend"
|
| 12 |
+
}
|