Download chat_train.py from DemonKing1234/tiny: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/DemonKing1234/tiny/resolve/main/chat_train.py
- Command line
-
hf download hf://DemonKing1234/tiny/chat_train.py
-
curl -L -o chat_train.py https://huggingface.co/DemonKing1234/tiny/resolve/main/chat_train.py
10.9 kB
| import argparse | |
| import os | |
| from pathlib import Path | |
| import torch | |
| from train import TinyTransformerLM, build_vocab, encode_text, make_batch | |
| def load_or_create_model(model_path, text, block_size, n_embd, n_head, n_layer): | |
| requested_config = { | |
| "vocab_size": None, | |
| "block_size": block_size, | |
| "n_embd": n_embd, | |
| "n_head": n_head, | |
| "n_layer": n_layer, | |
| } | |
| if model_path.exists(): | |
| checkpoint = torch.load(model_path, map_location="cpu") | |
| saved_stoi = checkpoint["stoi"] | |
| fresh_stoi, fresh_itos = build_vocab(text) | |
| saved_config = checkpoint["config"] | |
| config_matches = all(saved_config.get(k) == v for k, v in requested_config.items() if v is not None) | |
| if set(saved_stoi) == set(fresh_stoi) and config_matches: | |
| model = TinyTransformerLM(**saved_config) | |
| model.load_state_dict(checkpoint["model"]) | |
| return model, saved_stoi, {int(k): v for k, v in checkpoint["itos"].items()}, saved_config | |
| backup = model_path.with_suffix(".old-vocab.pt") | |
| model_path.replace(backup) | |
| print(f"old vocabulary checkpoint moved to {backup}") | |
| stoi, itos = build_vocab(text) | |
| config = { | |
| "vocab_size": len(stoi), | |
| "block_size": block_size, | |
| "n_embd": n_embd, | |
| "n_head": n_head, | |
| "n_layer": n_layer, | |
| } | |
| model = TinyTransformerLM(**config) | |
| return model, stoi, itos, config | |
| def limit_memory_file(data_path, keep_tail_chars=200_000): | |
| if not data_path.exists(): | |
| return | |
| text = data_path.read_text(encoding="utf-8") | |
| if len(text) <= keep_tail_chars: | |
| return | |
| data_path.write_text(text[-keep_tail_chars:], encoding="utf-8") | |
| def normalize_text(text): | |
| return "".join(ch.lower() if ch.isalpha() else ch for ch in text) | |
| def score_snippet(query, snippet): | |
| query_chars = set(ch for ch in normalize_text(query) if not ch.isspace()) | |
| snippet_chars = set(ch for ch in normalize_text(snippet) if not ch.isspace()) | |
| if not query_chars or not snippet_chars: | |
| return 0 | |
| overlap = len(query_chars & snippet_chars) | |
| return overlap / len(query_chars | snippet_chars) | |
| def retrieve_context(user_text, memory_text, max_examples=3): | |
| blocks = [b.strip() for b in memory_text.split("\n\n") if b.strip()] | |
| scored = [] | |
| for block in blocks: | |
| if "USER:" in block and "AI:" in block: | |
| scored.append((score_snippet(user_text, block), block)) | |
| scored.sort(key=lambda item: item[0], reverse=True) | |
| chosen = [block for score, block in scored[:max_examples] if score > 0] | |
| return "\n\n".join(chosen) | |
| def generate_once(model, prompt, stoi, itos, max_new_tokens, temperature): | |
| fallback = next(iter(stoi.values())) | |
| idx = torch.tensor([[stoi.get(ch, fallback) for ch in prompt]], dtype=torch.long) | |
| model.eval() | |
| for _ in range(max_new_tokens): | |
| idx_cond = idx[:, -model.block_size :] | |
| logits, _ = model(idx_cond) | |
| logits = logits[:, -1, :] / temperature | |
| probs = torch.softmax(logits, dim=-1) | |
| next_id = torch.multinomial(probs, num_samples=1) | |
| idx = torch.cat((idx, next_id), dim=1) | |
| if itos[int(next_id)] == "\n" and idx.shape[1] > len(prompt) + 20: | |
| break | |
| return "".join(itos[int(i)] for i in idx[0])[len(prompt) :] | |
| def score_reply(reply, user_text): | |
| stripped = reply.strip() | |
| if not stripped: | |
| return -100 | |
| score = 0.0 | |
| lowered = stripped.lower() | |
| if "user:" in lowered: | |
| score -= 6 | |
| if "ai:" in lowered: | |
| score -= 6 | |
| if stripped.count("\n") > 2: | |
| score -= 2 | |
| if len(set(stripped)) < 4: | |
| score -= 2 | |
| words = [word for word in stripped.split() if word] | |
| if words: | |
| unique_ratio = len(set(words)) / len(words) | |
| score += unique_ratio * 3 | |
| if len(stripped) < 4: | |
| score -= 2 | |
| if len(stripped) > 220: | |
| score -= 1 | |
| if user_text and user_text.lower().strip() in lowered: | |
| score -= 2 | |
| if any(ch.isalpha() for ch in stripped): | |
| score += 1 | |
| return score | |
| def generate_reply(model, prompt, user_text, stoi, itos, max_new_tokens, temperature, candidates=4): | |
| best_reply = "" | |
| best_score = float("-inf") | |
| for _ in range(candidates): | |
| reply = generate_once(model, prompt, stoi, itos, max_new_tokens, temperature) | |
| score = score_reply(reply, user_text) | |
| if score > best_score: | |
| best_score = score | |
| best_reply = reply | |
| return best_reply | |
| def train_steps(model, text, stoi, steps, batch_size, block_size, lr): | |
| if len(text) < block_size + 2: | |
| return | |
| data = encode_text(text, stoi) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=lr) | |
| model.train() | |
| for _ in range(steps): | |
| xb, yb = make_batch(data, batch_size, block_size, "cpu") | |
| _, loss = model(xb, yb) | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| optimizer.step() | |
| def save_model(model_path, model, config, stoi, itos): | |
| model_path.parent.mkdir(parents=True, exist_ok=True) | |
| torch.save( | |
| { | |
| "model": model.state_dict(), | |
| "config": config, | |
| "stoi": stoi, | |
| "itos": itos, | |
| }, | |
| model_path, | |
| ) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--data", default="data/chat_memory.txt") | |
| parser.add_argument("--seed-data", default="data/input.txt") | |
| parser.add_argument("--model", default="runs/chat_model.pt") | |
| parser.add_argument("--preset", choices=["tiny", "turbo", "small", "big", "large"], default="small") | |
| parser.add_argument("--steps-per-turn", type=int, default=8) | |
| parser.add_argument("--tokens", type=int, default=160) | |
| parser.add_argument("--temperature", type=float, default=1.1) | |
| parser.add_argument("--batch-size", type=int, default=4) | |
| parser.add_argument("--block-size", type=int, default=64) | |
| parser.add_argument("--n-embd", type=int, default=64) | |
| parser.add_argument("--n-head", type=int, default=2) | |
| parser.add_argument("--n-layer", type=int, default=1) | |
| parser.add_argument("--lr", type=float, default=3e-4) | |
| parser.add_argument("--no-self-train", action="store_true") | |
| args = parser.parse_args() | |
| presets = { | |
| "tiny": {"steps_per_turn": 4, "tokens": 120, "temperature": 1.0, "batch_size": 4, "block_size": 64, "n_embd": 64, "n_head": 2, "n_layer": 1, "lr": 3e-4}, | |
| "turbo": {"steps_per_turn": 2, "tokens": 100, "temperature": 1.2, "batch_size": 8, "block_size": 32, "n_embd": 64, "n_head": 4, "n_layer": 2, "lr": 1e-3}, | |
| "small": {"steps_per_turn": 8, "tokens": 160, "temperature": 1.1, "batch_size": 4, "block_size": 64, "n_embd": 96, "n_head": 2, "n_layer": 2, "lr": 2.5e-4}, | |
| "big": {"steps_per_turn": 12, "tokens": 180, "temperature": 1.0, "batch_size": 4, "block_size": 96, "n_embd": 192, "n_head": 4, "n_layer": 4, "lr": 2e-4}, | |
| "large": {"steps_per_turn": 16, "tokens": 220, "temperature": 0.95, "batch_size": 2, "block_size": 128, "n_embd": 256, "n_head": 8, "n_layer": 6, "lr": 1.5e-4}, | |
| } | |
| preset = presets[args.preset] | |
| if args.steps_per_turn == 8: | |
| args.steps_per_turn = preset["steps_per_turn"] | |
| if args.tokens == 160: | |
| args.tokens = preset["tokens"] | |
| if args.temperature == 1.1: | |
| args.temperature = preset["temperature"] | |
| if args.batch_size == 4: | |
| args.batch_size = preset["batch_size"] | |
| if args.block_size == 64: | |
| args.block_size = preset["block_size"] | |
| if args.n_embd == 64: | |
| args.n_embd = preset["n_embd"] | |
| if args.n_head == 2: | |
| args.n_head = preset["n_head"] | |
| if args.n_layer == 1: | |
| args.n_layer = preset["n_layer"] | |
| if args.lr == 3e-4: | |
| args.lr = preset["lr"] | |
| data_path = Path(args.data) | |
| seed_path = Path(args.seed_data) | |
| model_path = Path(args.model) | |
| if not data_path.exists(): | |
| seed = seed_path.read_text(encoding="utf-8") if seed_path.exists() else "" | |
| data_path.parent.mkdir(parents=True, exist_ok=True) | |
| data_path.write_text(seed + "\n", encoding="utf-8") | |
| text = data_path.read_text(encoding="utf-8") | |
| model, stoi, itos, config = load_or_create_model( | |
| model_path, text, args.block_size, args.n_embd, args.n_head, args.n_layer | |
| ) | |
| if not torch.cuda.is_available(): | |
| threads = os.cpu_count() or 4 | |
| torch.set_num_threads(threads) | |
| torch.set_num_interop_threads(1) | |
| torch.set_float32_matmul_precision("high") | |
| print(f"CPU optimization: using {threads} threads") | |
| print("Tiny chat. Type /quit to exit.") | |
| print("It uses retrieved examples and can keep training after each turn.") | |
| while True: | |
| try: | |
| user = input("\nyou> ").strip() | |
| if user.lower() in {"/quit", "quit", "exit"}: | |
| save_model(model_path, model, config, stoi, itos) | |
| print(f"saved {model_path}") | |
| break | |
| if user.startswith("/teach "): | |
| lesson = user[len("/teach ") :].strip() | |
| data_path.write_text(data_path.read_text(encoding="utf-8") + f"\nTEACHER: {lesson}\n", encoding="utf-8") | |
| limit_memory_file(data_path) | |
| text = data_path.read_text(encoding="utf-8") | |
| print("learning...") | |
| train_steps(model, text, stoi, args.steps_per_turn, args.batch_size, args.block_size, args.lr) | |
| save_model(model_path, model, config, stoi, itos) | |
| print("learned") | |
| continue | |
| memory_text = data_path.read_text(encoding="utf-8") | |
| context = retrieve_context(user, memory_text) | |
| if context: | |
| prompt = f"{context}\n\nUSER: {user}\nAI:" | |
| else: | |
| prompt = f"\nUSER: {user}\nAI:" | |
| reply = generate_reply(model, prompt, user, stoi, itos, args.tokens, args.temperature).strip() | |
| if not reply: | |
| reply = "..." | |
| reply = reply.replace("USER:", "").replace("AI:", "").strip() | |
| print(f"ai> {reply}") | |
| addition = f"\nUSER: {user}\nAI: {reply}\n" | |
| data_path.write_text(memory_text + addition, encoding="utf-8") | |
| limit_memory_file(data_path) | |
| text = data_path.read_text(encoding="utf-8") | |
| if not args.no_self_train: | |
| print("learning from this turn...") | |
| train_steps(model, text, stoi, args.steps_per_turn, args.batch_size, args.block_size, args.lr) | |
| save_model(model_path, model, config, stoi, itos) | |
| print("saved") | |
| except Exception as exc: | |
| print(f"error: {exc}") | |
| save_model(model_path, model, config, stoi, itos) | |
| print("model saved after error, continue or /quit") | |
| if __name__ == "__main__": | |
| main() | |