Download src/generate.py from erenyanic/eldamar-tokenizer: direct link, hf CLI and curl.
- Browser
- Download file 4.34 kB
-
https://huggingface.co/erenyanic/eldamar-tokenizer/resolve/main/src/generate.py
- Command line
-
hf download hf://erenyanic/eldamar-tokenizer/src/generate.py
-
curl -L -o generate.py https://huggingface.co/erenyanic/eldamar-tokenizer/resolve/main/src/generate.py
4.34 kB
| """Generate Middle-earth names from any trained checkpoint. | |
| Works for all four architectures: it reads the checkpoint's architecture folder, | |
| rebuilds the model, and samples with the matching BPE tokeniser. Like the worker | |
| in train_one.py it runs one architecture per process, so the flat module names in | |
| the architecture folders never collide. | |
| Run: python src/generate.py qwen3 512 # 20 names, temperature 0.8 | |
| python src/generate.py gemma4 char --count 40 # the CharTokenizer baseline | |
| python src/generate.py deepseek3 512 --temperature 0.7 --novel-only | |
| Lower temperature -> safer, more familiar names. Higher -> more varied/inventive. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import importlib | |
| import sys | |
| from pathlib import Path | |
| import torch | |
| ROOT = Path(__file__).resolve().parents[1] | |
| REPO = ROOT / "single_letter_transformers" | |
| CKPT_DIR = ROOT / "checkpoints" | |
| NAMES_FILE = ROOT / "data" / "middle_earth_names.txt" | |
| # architecture -> (folder, model class name). | |
| ARCHITECTURES = { | |
| "qwen3": ("qwen3", "TinyQwen"), | |
| "qwen3_5": ("qwen3_5", "TinyQwen35"), | |
| "gemma4": ("gemma4", "TinyGemma"), | |
| "deepseek3": ("deepseek3", "TinyDeepSeek"), | |
| } | |
| def load_model_and_tokenizer(arch: str, tok_spec: str): | |
| """Rebuild the trained model and its tokeniser (char or BPE) from the checkpoint.""" | |
| folder, class_name = ARCHITECTURES[arch] | |
| sys.path.insert(0, str(REPO / folder)) | |
| # The architecture folder must be importable *before* torch.load, because the | |
| # pickled cfg is that folder's config.ModelConfig. | |
| model_class = getattr(importlib.import_module("model"), class_name) | |
| name = "char" if tok_spec == "char" else f"bpe{tok_spec}" | |
| checkpoint_path = CKPT_DIR / f"{arch}_{name}.pt" | |
| if not checkpoint_path.exists(): | |
| sys.exit(f"No checkpoint at {checkpoint_path.relative_to(ROOT)} -- train it first.") | |
| ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False) | |
| model = model_class(ckpt["cfg"]) | |
| model.load_state_dict(ckpt["model"]) | |
| model.eval() | |
| if ckpt.get("tokenizer_kind") == "char": | |
| # Rebuild the CharTokenizer from the exact character list it was trained on. | |
| char_tokenizer = importlib.import_module("tokenizer").CharTokenizer | |
| tokenizer = char_tokenizer(ckpt["chars"]) | |
| else: | |
| from bpe_tokenizer import BpeTokenizer | |
| tokenizer = BpeTokenizer.from_file(ROOT / ckpt["tokenizer"]) | |
| return model, tokenizer | |
| def generate_names(model, tokenizer, count: int, temperature: float) -> list[str]: | |
| """Sample `count` names, each starting from the newline (start-of-name) token.""" | |
| start = torch.full((count, 1), tokenizer.newline_id, dtype=torch.long) | |
| out = model.generate(start, max_new_tokens=model.cfg.max_seq_len, | |
| temperature=temperature, top_k=None, eos_id=tokenizer.eos_id) | |
| names = [] | |
| for row in out.tolist(): | |
| # Drop the leading newline, then keep everything up to the next newline. | |
| name = tokenizer.decode(row[1:]).split("\n")[0] | |
| if name: | |
| names.append(name) | |
| return names | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Sample names from a trained checkpoint.") | |
| parser.add_argument("arch", choices=sorted(ARCHITECTURES)) | |
| parser.add_argument("tokenizer", choices=("char", "256", "512"), | |
| help="'char' for the CharTokenizer baseline, or a BPE vocab size") | |
| parser.add_argument("--count", type=int, default=20) | |
| parser.add_argument("--temperature", type=float, default=0.8) | |
| parser.add_argument("--seed", type=int, default=None, help="fix the RNG for reproducible samples") | |
| parser.add_argument("--novel-only", action="store_true", | |
| help="only show names that are NOT already in the training corpus") | |
| args = parser.parse_args() | |
| if args.seed is not None: | |
| torch.manual_seed(args.seed) | |
| model, tokenizer = load_model_and_tokenizer(args.arch, args.tokenizer) | |
| names = generate_names(model, tokenizer, args.count, args.temperature) | |
| if args.novel_only: | |
| known = set(NAMES_FILE.read_text(encoding="utf-8").split("\n")) | |
| names = [n for n in names if n not in known] | |
| for name in names: | |
| print(name) | |
| if __name__ == "__main__": | |
| main() | |