|
|
|
|
|
|
| """
|
| Descargar texto español masivo y re-entrenar tokenizer 48K.
|
|
|
| Usa Wikipedia español de HuggingFace (gratuito, sin auth).
|
| Luego re-entrena SentencePiece con corpus código + español.
|
| """
|
| import os
|
| import sys
|
| import json
|
| from pathlib import Path
|
|
|
| DATA_DIR = Path(__file__).parent.parent / "data"
|
| TOKENIZER_DIR = DATA_DIR / "tokenizer"
|
| SPANISH_FILE = TOKENIZER_DIR / "spanish_wiki.txt"
|
| CORPUS_FILE = TOKENIZER_DIR / "corpus_48k.txt"
|
| OUTPUT_PREFIX = str(TOKENIZER_DIR / "pampar_48k")
|
|
|
| TARGET_ES_MB = 500
|
|
|
|
|
| def download_spanish_wiki():
|
| """Descargar Wikipedia español desde HuggingFace datasets."""
|
| from datasets import load_dataset
|
|
|
| print("📥 Descargando Wikipedia ES desde HuggingFace...")
|
| print(" Dataset: wikimedia/wikipedia, 20231101.es")
|
| print(" Esto puede tardar 3-10 minutos...\n")
|
|
|
| ds = load_dataset(
|
| "wikimedia/wikipedia",
|
| "20231101.es",
|
| split="train",
|
| streaming=True,
|
| )
|
|
|
| count = 0
|
| with open(SPANISH_FILE, "w", encoding="utf-8") as f:
|
| for example in ds:
|
| text = example.get("text", "")
|
| if len(text) > 200:
|
| f.write(text.strip() + "\n")
|
| count += 1
|
|
|
| if count % 50000 == 0:
|
| size_mb = SPANISH_FILE.stat().st_size / 1024**2
|
| print(f" {count:,} artículos ({size_mb:.0f} MB)")
|
|
|
| if size_mb >= TARGET_ES_MB:
|
| print(f" Alcanzado objetivo de {TARGET_ES_MB} MB")
|
| break
|
|
|
| size_mb = SPANISH_FILE.stat().st_size / 1024**2
|
| print(f"\n✅ Wikipedia ES: {count:,} artículos, {size_mb:.0f} MB")
|
| return count
|
|
|
|
|
| def rebuild_corpus():
|
| """Reconstruir corpus combinando código + español + código ES."""
|
| print("\n📦 Reconstruyendo corpus bilingüe...")
|
|
|
|
|
| code_files = [
|
| DATA_DIR / "code" / "github_code.jsonl",
|
| DATA_DIR / "code" / "train_massive.jsonl",
|
| DATA_DIR / "code" / "train.jsonl",
|
| DATA_DIR / "distillation" / "codealpaca_20k.jsonl",
|
| DATA_DIR / "distillation" / "evol_instruct_code_80k.jsonl",
|
| DATA_DIR / "distillation" / "codeexercises_python.jsonl",
|
| DATA_DIR / "distillation" / "distillation_data.jsonl",
|
| ]
|
|
|
| total = 0
|
| with open(CORPUS_FILE, "w", encoding="utf-8") as out:
|
|
|
| print(" Código:")
|
| for jsonl_path in code_files:
|
| if not jsonl_path.exists():
|
| continue
|
| size_mb = jsonl_path.stat().st_size / (1024 * 1024)
|
| file_count = 0
|
| with open(jsonl_path, "r", encoding="utf-8", errors="replace") as f:
|
| for line in f:
|
| try:
|
| data = json.loads(line)
|
| text = data.get("text", "")
|
| if len(text) > 50:
|
| out.write(text.strip() + "\n")
|
| total += 1
|
| file_count += 1
|
| except (json.JSONDecodeError, KeyError):
|
| continue
|
| print(f" {jsonl_path.name}: {file_count:,} docs ({size_mb:.1f} MB)")
|
|
|
|
|
| if SPANISH_FILE.exists():
|
| print(f" Wikipedia ES:")
|
| es_count = 0
|
| with open(SPANISH_FILE, "r", encoding="utf-8") as f:
|
| for line in f:
|
| if len(line.strip()) > 50:
|
| out.write(line)
|
| total += 1
|
| es_count += 1
|
| print(f" {es_count:,} artículos")
|
|
|
|
|
| print(" Código español sintético:")
|
| es_code = generate_spanish_code()
|
| for line in es_code:
|
| out.write(line + "\n")
|
| total += 1
|
| print(f" {len(es_code):,} líneas")
|
|
|
|
|
| old_corpus = TOKENIZER_DIR / "corpus.txt"
|
| if old_corpus.exists():
|
| print(f" Corpus antiguo:")
|
| old_count = 0
|
| with open(old_corpus, "r", encoding="utf-8", errors="replace") as f:
|
| for line in f:
|
| if len(line.strip()) > 20:
|
| out.write(line)
|
| total += 1
|
| old_count += 1
|
| print(f" {old_count:,} docs")
|
|
|
| corpus_mb = CORPUS_FILE.stat().st_size / 1024**2
|
| print(f"\n📊 Corpus final: {total:,} docs, {corpus_mb:.0f} MB")
|
|
|
|
|
| es_lines = 0
|
| with open(CORPUS_FILE, "r", encoding="utf-8") as f:
|
| for line in f:
|
| if any(c in line for c in "áéíóúñü"):
|
| es_lines += 1
|
| es_pct = es_lines * 100 / total if total else 0
|
| print(f" Líneas con español: {es_lines:,} ({es_pct:.1f}%)")
|
|
|
| return total
|
|
|
|
|
| def generate_spanish_code():
|
| """Genera corpus sustancial de código con español."""
|
| lines = []
|
|
|
|
|
| terms = [
|
| "función", "parámetro", "argumento", "retorno", "resultado",
|
| "variable", "constante", "método", "clase", "objeto",
|
| "archivo", "directorio", "configuración", "conexión", "autenticación",
|
| "usuario", "contraseña", "validación", "verificación", "búsqueda",
|
| "algoritmo", "estructura", "índice", "cálculo", "número",
|
| "cadena", "entero", "flotante", "booleano", "tamaño",
|
| "longitud", "cantidad", "máximo", "mínimo", "promedio",
|
| "iteración", "condición", "comparación", "operación", "excepción",
|
| "implementación", "definición", "inicialización", "herencia", "abstracción",
|
| "compilación", "ejecución", "depuración", "prueba", "documentación",
|
| "capa", "neurona", "peso", "activación", "gradiente",
|
| "optimizador", "pérdida", "precisión", "época", "entrenamiento",
|
| "inferencia", "predicción", "modelo", "vocabulario", "tokenización",
|
| "normalización", "regularización", "aprendizaje", "atención", "transformador",
|
| ]
|
|
|
| templates = [
|
| 'def {0}(self, {1}):\n """Calcula el {2} del {1} proporcionado."""\n return self._{1}',
|
| '# {0}: se encarga de procesar el {1} y calcular el {2}',
|
| 'raise ValueError("El {0} no puede estar vacío")',
|
| 'logger.info(f"Procesando {0} con {1}: {{valor}}")',
|
| '"""\n{0}.\n\nParámetros:\n {1}: El valor de {2} a procesar.\n\nRetorna:\n El {2} calculado.\n"""',
|
| 'self.{0} = {1} # {2} del componente',
|
| '# TODO: implementar {0} para mejorar el {1}',
|
| 'if not self.{0}:\n raise RuntimeError("Falta el {1} para la {2}")',
|
| 'print(f"Error en {0}: el {1} no es válido para {2}")',
|
| 'assert isinstance({0}, {1}), f"Se esperaba {1}, se recibió {{type({0})}}"',
|
| ]
|
|
|
| import random
|
| random.seed(42)
|
|
|
| for _ in range(2000):
|
| random.shuffle(terms)
|
| for i in range(0, len(terms) - 2, 3):
|
| for t in templates:
|
| try:
|
| lines.append(t.format(terms[i], terms[i+1], terms[i+2]))
|
| except (IndexError, KeyError):
|
| pass
|
|
|
|
|
| for _ in range(2000):
|
| for term in terms:
|
| lines.append(term)
|
|
|
| return lines
|
|
|
|
|
| def retrain_tokenizer():
|
| """Re-entrenar SentencePiece BPE con corpus bilingüe."""
|
| import sentencepiece as spm
|
|
|
| corpus_mb = CORPUS_FILE.stat().st_size / 1024**2
|
| print(f"\n🔧 Re-entrenando tokenizer BPE...")
|
| print(f" Corpus: {corpus_mb:.0f} MB")
|
| print(f" Vocab: 48,000")
|
| print(f" Esto puede tardar 10-30 minutos...\n")
|
|
|
| spm.SentencePieceTrainer.train(
|
| input=str(CORPUS_FILE),
|
| model_prefix=OUTPUT_PREFIX,
|
| vocab_size=48000,
|
| model_type="bpe",
|
| pad_id=0, eos_id=1, bos_id=2, unk_id=3,
|
| character_coverage=0.9999,
|
| num_threads=os.cpu_count() or 4,
|
| train_extremely_large_corpus=True,
|
| max_sentence_length=16384,
|
| byte_fallback=True,
|
| normalization_rule_name="identity",
|
| split_digits=True,
|
| user_defined_symbols=[
|
| " ", " ", " ", "\t",
|
| "==", "!=", "<=", ">=", "+=", "-=", "*=", "/=",
|
| "->", "=>", "::", "//", "**", "&&", "||",
|
| "...", "..=", '"""', "'''", "```",
|
| ],
|
| control_symbols=["<pad>", "<mask>"],
|
| )
|
|
|
| print("✅ Tokenizer re-entrenado!")
|
|
|
|
|
| def verify():
|
| """Verificar tokenizer."""
|
| import sentencepiece as spm
|
|
|
| sp = spm.SentencePieceProcessor()
|
| sp.load(f"{OUTPUT_PREFIX}.model")
|
|
|
| print(f"\n🧪 Verificación del tokenizer")
|
| print(f" Vocab: {sp.get_piece_size():,}")
|
|
|
|
|
| tests = {
|
| "función": "función",
|
| "número": "número",
|
| "tamaño": "tamaño",
|
| "cálculo": "cálculo",
|
| "también": "también",
|
| "España": "España",
|
| "programación": "programación",
|
| }
|
|
|
| print(f"\n Palabras españolas:")
|
| all_ok = True
|
| for name, word in tests.items():
|
| enc = sp.encode(word, out_type=str)
|
| has_bytes = any("<0x" in t for t in enc)
|
| ok = "✅" if not has_bytes else "❌"
|
| if has_bytes:
|
| all_ok = False
|
| print(f" {ok} {word:20s} -> {enc}")
|
|
|
|
|
| tests2 = {
|
| "Python": "def calcular_promedio(numeros):\n return sum(numeros) / len(numeros)",
|
| "Español": "La función calcula el promedio de una lista de números flotantes.",
|
| "TypeScript": "const resultado: number = await procesarDatos(entrada);",
|
| "SQL": "SELECT nombre, COUNT(*) AS total FROM usuarios GROUP BY nombre",
|
| }
|
|
|
| print(f"\n Eficiencia (tok/char):")
|
| for name, text in tests2.items():
|
| ids = sp.encode(text)
|
| ratio = len(ids) / len(text)
|
| print(f" {name:15s}: {ratio:.3f} ({len(ids)} tok / {len(text)} chars)")
|
|
|
|
|
| accent_count = sum(1 for i in range(sp.get_piece_size())
|
| if any(c in sp.id_to_piece(i) for c in "áéíóúñüÁÉÍÓÚÑÜ"))
|
| print(f"\n Tokens con acentos/ñ: {accent_count:,} de {sp.get_piece_size():,}")
|
|
|
| if all_ok:
|
| print("\n✅ El tokenizer maneja español correctamente!")
|
| else:
|
| print("\n⚠️ Algunas palabras todavía usan byte fallback")
|
|
|
|
|
| if __name__ == "__main__":
|
|
|
| download_spanish_wiki()
|
|
|
|
|
| rebuild_corpus()
|
|
|
|
|
| retrain_tokenizer()
|
|
|
|
|
| verify()
|
|
|