AxoneSystem commited on
Commit
101bfba
·
verified ·
1 Parent(s): 52f5bcd

Upload 20 files

Browse files
.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
- license: cc-by-nc-nd-4.0
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
+ }