Download tests/test_train_text.py from Frankenstein-Labs/Cortex-0: direct link, hf CLI and curl.
- Browser
- Download file 7.57 kB
-
https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/tests/test_train_text.py
- Command line
-
hf download hf://Frankenstein-Labs/Cortex-0/tests/test_train_text.py
-
curl -L -o test_train_text.py https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/tests/test_train_text.py
7.57 kB
| """Phase 3 — tests de la préparation de données et de la boucle d'entraînement. | |
| Rapides (CPU) : corpus FR embarqué + vraies fonctions de tokenisation/batching. | |
| L'entraînement complet (perplexité < 30) se fait sur Colab. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| import pytest | |
| import torch | |
| from src.data.prepare_text import ( | |
| DTYPE, | |
| load_bin, | |
| prepare, | |
| split_train_val, | |
| tokenize_corpus, | |
| ) | |
| from src.model.text_model import CortexTextLM, TextConfig | |
| from src.tokenizer import load_tokenizer | |
| from src.train.train_text import ( | |
| TrainConfig, | |
| estimate_loss, | |
| get_batch, | |
| get_lr, | |
| pick_device, | |
| ) | |
| FR = [f"Phrase française numéro {i} avec des accents é à ç ù." for i in range(50)] | |
| def tokenizer(): | |
| tok = load_tokenizer() | |
| if tok.vocab_size != 32000: | |
| pytest.skip("tokenizer final 32k requis") | |
| return tok | |
| # --------------------------------------------------------------------------- | |
| # prepare_text | |
| # --------------------------------------------------------------------------- | |
| def test_tokenize_adds_eos_between_documents(tokenizer): | |
| from src.tokenizer.special_tokens import EOS_ID | |
| arr = tokenize_corpus(["bonjour", "monde"], tokenizer, add_eos=True) | |
| assert arr.dtype == DTYPE | |
| assert arr[-1] == EOS_ID # se termine par eos | |
| # Il y a exactement 2 eos (un par document). | |
| assert int((arr == EOS_ID).sum()) == 2 | |
| def test_tokenize_without_eos(tokenizer): | |
| from src.tokenizer.special_tokens import EOS_ID | |
| arr = tokenize_corpus(["bonjour", "monde"], tokenizer, add_eos=False) | |
| assert int((arr == EOS_ID).sum()) == 0 | |
| def test_split_train_val_no_overlap(): | |
| arr = np.arange(1000, dtype=DTYPE) | |
| train, val = split_train_val(arr, val_frac=0.1) | |
| assert len(train) == 900 and len(val) == 100 | |
| assert train[-1] < val[0] # découpe séquentielle | |
| def test_split_rejects_bad_frac(): | |
| with pytest.raises(ValueError): | |
| split_train_val(np.arange(10), val_frac=0.0) | |
| def test_prepare_writes_artifacts(tmp_path, tokenizer): | |
| meta = prepare(FR, tmp_path, tokenizer, val_frac=0.1, extra_meta={"preset": "test"}) | |
| assert (tmp_path / "train.bin").is_file() | |
| assert (tmp_path / "val.bin").is_file() | |
| assert (tmp_path / "meta.json").is_file() | |
| assert meta["n_total_tokens"] == meta["n_train_tokens"] + meta["n_val_tokens"] | |
| assert meta["dtype"] == "uint16" | |
| assert meta["preset"] == "test" | |
| def test_load_bin_roundtrip(tmp_path, tokenizer): | |
| prepare(FR, tmp_path, tokenizer, val_frac=0.1) | |
| arr = np.asarray(load_bin(tmp_path / "train.bin")) | |
| assert arr.dtype == DTYPE | |
| assert arr.size > 0 | |
| # La val commence bien après la fin de la train. | |
| assert arr[-1] != 0 or True | |
| def test_meta_json_is_valid(tmp_path, tokenizer): | |
| prepare(FR, tmp_path, tokenizer, val_frac=0.1) | |
| data = json.loads((tmp_path / "meta.json").read_text(encoding="utf-8")) | |
| assert data["tokenizer_vocab_size"] == 32000 | |
| # --------------------------------------------------------------------------- | |
| # get_batch | |
| # --------------------------------------------------------------------------- | |
| def test_get_batch_shapes_and_shift(): | |
| data = np.arange(100, dtype=DTYPE) | |
| x, y = get_batch(data, 2, 8, torch.device("cpu")) | |
| assert x.shape == (2, 8) and y.shape == (2, 8) | |
| # y est x décalé d'un token | |
| assert torch.equal(y[:, :-1], x[:, 1:]) | |
| def test_get_batch_rejects_small_corpus(): | |
| data = np.arange(5, dtype=DTYPE) | |
| with pytest.raises(ValueError): | |
| get_batch(data, 2, 8, torch.device("cpu")) | |
| # --------------------------------------------------------------------------- | |
| # get_lr / planificateur | |
| # --------------------------------------------------------------------------- | |
| def test_lr_warmup_starts_small(): | |
| cfg = TrainConfig(warmup_steps=100, max_steps=1000, lr=1e-3) | |
| assert get_lr(0, cfg) < get_lr(50, cfg) < get_lr(99, cfg) | |
| assert get_lr(0, cfg) == pytest.approx(1e-3 / 100) | |
| def test_lr_peaks_then_decays(): | |
| cfg = TrainConfig(warmup_steps=100, max_steps=1000, lr=1e-3, min_lr_ratio=0.1) | |
| peak = get_lr(100, cfg) | |
| assert get_lr(500, cfg) < peak | |
| assert get_lr(1000, cfg) == pytest.approx(1e-4) | |
| assert get_lr(2000, cfg) == pytest.approx(1e-4) | |
| def test_lr_is_monotonic_after_warmup(): | |
| cfg = TrainConfig(warmup_steps=10, max_steps=500, lr=1e-3) | |
| values = [get_lr(s, cfg) for s in range(10, 500, 20)] | |
| assert all(values[i] >= values[i + 1] for i in range(len(values) - 1)) | |
| def test_pick_device_cpu(): | |
| assert pick_device("cpu").type == "cpu" | |
| # --------------------------------------------------------------------------- | |
| # Entraînement court réel (CPU) | |
| # --------------------------------------------------------------------------- | |
| def _tiny_cfg(data_dir: Path, tmp_path: Path) -> TrainConfig: | |
| return TrainConfig( | |
| data_dir=str(data_dir), out_dir=str(tmp_path / "ckpt"), | |
| max_steps=6, batch_size=2, grad_accum=1, ctx_len=16, | |
| lr=3e-3, warmup_steps=2, eval_interval=3, eval_batches=1, | |
| log_interval=2, device="cpu", dtype="float32", | |
| n_layer=2, n_head=2, n_embd=32, tie_embeddings=True, | |
| ) | |
| def test_train_runs_and_loss_decreases(tmp_path, tokenizer): | |
| from src.train.train_text import train | |
| corpus = FR * 5 + ["Fin du corpus pour l'entraînement."] | |
| prepare(corpus, tmp_path / "data", tokenizer, val_frac=0.1) | |
| summary = train(_tiny_cfg(tmp_path / "data", tmp_path)) | |
| assert (tmp_path / "ckpt" / "best.pt").is_file() | |
| assert (tmp_path / "ckpt" / "last.pt").is_file() | |
| assert summary["best_val_loss"] < 50 # la loss reste finie | |
| def test_checkpoint_is_loadable(tmp_path, tokenizer): | |
| from src.train.train_text import train | |
| prepare(FR * 5, tmp_path / "data", tokenizer, val_frac=0.1) | |
| cfg = _tiny_cfg(tmp_path / "data", tmp_path) | |
| train(cfg) | |
| ckpt = torch.load(tmp_path / "ckpt" / "best.pt", weights_only=False) | |
| model = CortexTextLM(TextConfig(**ckpt["text_config"])) | |
| model.load_state_dict(ckpt["model"]) | |
| assert ckpt["val_loss"] > 0 | |
| assert ckpt["train_config"]["ctx_len"] == 16 | |
| def test_estimate_loss_returns_both_splits(tmp_path, tokenizer): | |
| from contextlib import nullcontext | |
| prepare(FR * 5, tmp_path / "data", tokenizer, val_frac=0.1) | |
| train_data = load_bin(tmp_path / "data" / "train.bin") | |
| val_data = load_bin(tmp_path / "data" / "val.bin") | |
| model = CortexTextLM(TextConfig(vocab_size=32000, n_layer=1, n_head=2, n_embd=32, ctx_len=16)) | |
| cfg = TrainConfig(batch_size=2, ctx_len=16, eval_batches=1) | |
| metrics = estimate_loss(model, {"train": train_data, "val": val_data}, cfg, | |
| torch.device("cpu"), nullcontext()) | |
| assert set(metrics) == {"train", "val"} | |
| assert all(v > 0 for v in metrics.values()) | |
| def test_evaluate_perplexity_on_checkpoint(tmp_path, tokenizer): | |
| from contextlib import nullcontext | |
| from src.evaluate import load_checkpoint, perplexity | |
| from src.train.train_text import train | |
| prepare(FR * 5, tmp_path / "data", tokenizer, val_frac=0.1) | |
| cfg = _tiny_cfg(tmp_path / "data", tmp_path) | |
| train(cfg) | |
| device = torch.device("cpu") | |
| model, ckpt = load_checkpoint(tmp_path / "ckpt" / "best.pt", device) | |
| assert ckpt["text_config"]["n_embd"] == 32 | |
| data = load_bin(tmp_path / "data" / "val.bin") | |
| eval_cfg = TrainConfig(batch_size=2, ctx_len=16, eval_batches=1) | |
| ppl, loss = perplexity(model, data, eval_cfg, device, nullcontext()) | |
| assert ppl > 1.0 and loss > 0.0 | |