Upload chat.py with huggingface_hub
Browse files
chat.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
ROOT = Path(__file__).resolve().parent
|
| 10 |
+
INDIGO_TORCH = ROOT.parent / "Indigo"
|
| 11 |
+
INDIGO_TF = ROOT.parent / "indigo.tf"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def meta_of(ckpt):
|
| 15 |
+
meta_path = str(ckpt)[: -len(".safetensors")] + "_meta.json"
|
| 16 |
+
if not os.path.exists(meta_path):
|
| 17 |
+
raise SystemExit(f"meta tidak ditemukan: {meta_path}")
|
| 18 |
+
with open(meta_path, encoding="utf-8") as f:
|
| 19 |
+
return json.load(f)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def load_torch(ckpt, meta, device):
|
| 23 |
+
sys.path.insert(0, str(INDIGO_TORCH))
|
| 24 |
+
from safetensors.torch import load_file
|
| 25 |
+
|
| 26 |
+
from indigo.common import build_tokenizer
|
| 27 |
+
from indigo.model import GPT, GPTConfig
|
| 28 |
+
|
| 29 |
+
state = load_file(str(ckpt))
|
| 30 |
+
model = GPT(GPTConfig(**meta["config"]))
|
| 31 |
+
missing, unexpected = model.load_state_dict(state, strict=False)
|
| 32 |
+
if missing or unexpected:
|
| 33 |
+
print(f"state_dict: missing={missing} unexpected={unexpected}")
|
| 34 |
+
model = model.to(device)
|
| 35 |
+
tokenizer = build_tokenizer(meta.get("tokenizer") or {"type": "char"}, meta.get("vocab"))
|
| 36 |
+
return model, tokenizer, "torch"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def load_tf(ckpt, meta):
|
| 40 |
+
sys.path.insert(0, str(INDIGO_TF))
|
| 41 |
+
import numpy as np
|
| 42 |
+
import tensorflow as tf
|
| 43 |
+
from safetensors.numpy import load_file
|
| 44 |
+
|
| 45 |
+
from indigotf.common import build_tokenizer
|
| 46 |
+
from indigotf.model import build_gpt, generate as tf_generate
|
| 47 |
+
|
| 48 |
+
keys = ("vocab_size", "block_size", "n_layer", "n_head", "n_embd", "dropout")
|
| 49 |
+
model = build_gpt(**{k: meta["config"][k] for k in keys})
|
| 50 |
+
state = {k.replace("/", "_"): v for k, v in load_file(str(ckpt)).items()}
|
| 51 |
+
by_path = {v.path.replace("/", "_"): v.path for v in model.weights}
|
| 52 |
+
missing = [p for p in by_path if p not in state]
|
| 53 |
+
if missing:
|
| 54 |
+
raise SystemExit(f"bobot tidak cocok: {missing[:5]}")
|
| 55 |
+
model.set_weights([state[v.path.replace("/", "_")] for v in model.weights])
|
| 56 |
+
tokenizer = build_tokenizer(meta.get("tokenizer") or {"type": "char"}, meta.get("vocab"))
|
| 57 |
+
n_params = int(sum(int(np.prod(v.shape)) for v in model.weights))
|
| 58 |
+
print(f"(tensorflow dimuat, params={n_params / 1e6:.2f}M)")
|
| 59 |
+
return model, tokenizer, "tf", tf_generate
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def mean_nll(logp_fn, context, new_ids, block_size):
|
| 63 |
+
if not new_ids:
|
| 64 |
+
return float("inf")
|
| 65 |
+
seq = (context + list(new_ids))[-block_size:]
|
| 66 |
+
T = len(seq)
|
| 67 |
+
w = min(len(new_ids), max(T - 1, 1))
|
| 68 |
+
targets = np.asarray(seq[-w:])
|
| 69 |
+
logp = logp_fn(seq)
|
| 70 |
+
rows = np.arange(T - 1 - w, T - 1)
|
| 71 |
+
return float(-logp[rows, targets].mean())
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def main():
|
| 75 |
+
global INDIGO_TORCH, INDIGO_TF
|
| 76 |
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
| 77 |
+
parser = argparse.ArgumentParser(description="Chat tester untuk model Indigo (PyTorch & TensorFlow)")
|
| 78 |
+
parser.add_argument("--ckpt", default=None, help="path .safetensors (deteksi backend otomatis)")
|
| 79 |
+
parser.add_argument("--torch-dir", default=str(INDIGO_TORCH))
|
| 80 |
+
parser.add_argument("--tf-dir", default=str(INDIGO_TF))
|
| 81 |
+
parser.add_argument("--max-new", type=int, default=120)
|
| 82 |
+
parser.add_argument("--temperature", type=float, default=0.8)
|
| 83 |
+
parser.add_argument("--top-k", type=int, default=40)
|
| 84 |
+
parser.add_argument("--top-p", type=float, default=0.95)
|
| 85 |
+
parser.add_argument("--repetition-penalty", type=float, default=1.15)
|
| 86 |
+
parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"])
|
| 87 |
+
parser.add_argument("--fallback", default="saya tidak punya data.",
|
| 88 |
+
help='jawaban saat model tidak yakin; "" untuk mematikan')
|
| 89 |
+
parser.add_argument("--threshold", type=float, default=None,
|
| 90 |
+
help="ambang NLL prompt per token (default: val_loss - 0.6)")
|
| 91 |
+
parser.add_argument("--guard", default=None, help="kamus kata (satu/baris); fallback jika ratio rendah")
|
| 92 |
+
parser.add_argument("--guard-min", type=float, default=0.5, help="ambang rasio kata dikenal")
|
| 93 |
+
parser.add_argument("--show-nll", action="store_true", help="tampilkan skor NLL tiap jawaban")
|
| 94 |
+
args = parser.parse_args()
|
| 95 |
+
|
| 96 |
+
INDIGO_TORCH = Path(args.torch_dir)
|
| 97 |
+
INDIGO_TF = Path(args.tf_dir)
|
| 98 |
+
|
| 99 |
+
ckpt = args.ckpt
|
| 100 |
+
if ckpt is None:
|
| 101 |
+
cand = INDIGO_TORCH / "out" / "indigo_best.safetensors"
|
| 102 |
+
if cand.exists():
|
| 103 |
+
ckpt = cand
|
| 104 |
+
else:
|
| 105 |
+
raise SystemExit("tidak ada checkpoint; gunakan --ckpt")
|
| 106 |
+
ckpt = Path(ckpt)
|
| 107 |
+
meta = meta_of(ckpt)
|
| 108 |
+
backend = meta.get("backend", "pytorch")
|
| 109 |
+
|
| 110 |
+
wordset = prefiks = sufiks = None
|
| 111 |
+
if args.guard:
|
| 112 |
+
sys.path.insert(0, str(INDIGO_TORCH))
|
| 113 |
+
from indigo.common import load_wordlist as _load
|
| 114 |
+
|
| 115 |
+
wordset = _load(args.guard)
|
| 116 |
+
p_def = INDIGO_TORCH / "data" / "prefiks.txt"
|
| 117 |
+
s_def = INDIGO_TORCH / "data" / "sufiks.txt"
|
| 118 |
+
prefiks = _load(str(p_def)) if p_def.exists() else None
|
| 119 |
+
sufiks = _load(str(s_def)) if s_def.exists() else None
|
| 120 |
+
|
| 121 |
+
if backend == "tensorflow":
|
| 122 |
+
model, tokenizer, backend_label, tf_generate_fn = load_tf(ckpt, meta)
|
| 123 |
+
import tensorflow as tf
|
| 124 |
+
|
| 125 |
+
def idx_input(ctx):
|
| 126 |
+
return tf.constant([ctx], dtype=tf.int64)
|
| 127 |
+
|
| 128 |
+
def logp_fn(window):
|
| 129 |
+
logits = model(tf.constant([window], dtype=tf.int64), training=False)
|
| 130 |
+
return tf.nn.log_softmax(tf.cast(logits[0], tf.float32), axis=-1).numpy()
|
| 131 |
+
|
| 132 |
+
def generate_fn(mdl, idx, max_new, block_size_unused, temperature=1.0, top_k=None):
|
| 133 |
+
return tf_generate_fn(mdl, idx, max_new, meta["config"]["block_size"],
|
| 134 |
+
temperature=temperature, top_k=top_k)
|
| 135 |
+
else:
|
| 136 |
+
import torch
|
| 137 |
+
|
| 138 |
+
device = (
|
| 139 |
+
("cuda" if torch.cuda.is_available() else "cpu") if args.device == "auto" else args.device
|
| 140 |
+
)
|
| 141 |
+
model, tokenizer, backend_label = load_torch(ckpt, meta, device)
|
| 142 |
+
dev = next(model.parameters()).device
|
| 143 |
+
|
| 144 |
+
def idx_input(ctx):
|
| 145 |
+
return torch.tensor([ctx], dtype=torch.long, device=dev)
|
| 146 |
+
|
| 147 |
+
def logp_fn(window):
|
| 148 |
+
idx = torch.tensor([window], dtype=torch.long, device=dev)
|
| 149 |
+
with torch.no_grad():
|
| 150 |
+
logits, _ = model(idx)
|
| 151 |
+
return torch.log_softmax(logits[0].float(), dim=-1).cpu().numpy()
|
| 152 |
+
|
| 153 |
+
def generate_fn(mdl, idx, max_new, block_size_unused, temperature=1.0, top_k=None):
|
| 154 |
+
return mdl.generate(idx, max_new, temperature=temperature, top_k=top_k,
|
| 155 |
+
top_p=args.top_p, repetition_penalty=args.repetition_penalty)
|
| 156 |
+
|
| 157 |
+
if args.threshold is None:
|
| 158 |
+
val = meta.get("val_loss")
|
| 159 |
+
args.threshold = max(3.0, val - 0.6) if val else 5.0
|
| 160 |
+
info_lines = [
|
| 161 |
+
f"model={ckpt.name} | backend={backend_label} | "
|
| 162 |
+
f"tokenizer={meta.get('tokenizer', {}).get('type', 'char')} | ambang nll={args.threshold:.2f}",
|
| 163 |
+
"perintah: /reset ulang konteks | /keluar berhenti",
|
| 164 |
+
]
|
| 165 |
+
if wordset:
|
| 166 |
+
info_lines.append(f"[guard] kamus: {len(wordset):,} kata | ambang ratio >= {args.guard_min:.0%}")
|
| 167 |
+
print("\n".join(info_lines) + "\n")
|
| 168 |
+
|
| 169 |
+
block_size = meta["config"]["block_size"]
|
| 170 |
+
history = []
|
| 171 |
+
|
| 172 |
+
def respond(user_text):
|
| 173 |
+
piece = tokenizer.encode("\nAnda: " + user_text + "\nIndigo:")
|
| 174 |
+
context = (history + piece)[-block_size:]
|
| 175 |
+
prev_history = list(history)
|
| 176 |
+
prompt_nll = mean_nll(logp_fn, [], piece, block_size)
|
| 177 |
+
out = generate_fn(model, idx_input(context), args.max_new, block_size,
|
| 178 |
+
temperature=args.temperature, top_k=args.top_k)
|
| 179 |
+
full = (out[0].tolist() if hasattr(out[0], "tolist") else out[0])
|
| 180 |
+
new_ids = full[len(context):]
|
| 181 |
+
text = tokenizer.decode(new_ids).strip()
|
| 182 |
+
ratio = word_known_ratio(text, wordset, prefiks, sufiks) if wordset else 1.0
|
| 183 |
+
guard_ok = ratio >= args.guard_min if wordset else True
|
| 184 |
+
if args.fallback and (not text or prompt_nll > args.threshold or not guard_ok):
|
| 185 |
+
history[:] = prev_history
|
| 186 |
+
return args.fallback, prompt_nll
|
| 187 |
+
history.clear()
|
| 188 |
+
history.extend(full[-block_size:])
|
| 189 |
+
return text, prompt_nll
|
| 190 |
+
|
| 191 |
+
while True:
|
| 192 |
+
try:
|
| 193 |
+
user = input("\nAnda> ").strip()
|
| 194 |
+
except (EOFError, KeyboardInterrupt):
|
| 195 |
+
print()
|
| 196 |
+
break
|
| 197 |
+
if not user:
|
| 198 |
+
continue
|
| 199 |
+
if user in ("/keluar", "/quit", "/exit"):
|
| 200 |
+
break
|
| 201 |
+
if user == "/reset":
|
| 202 |
+
history.clear()
|
| 203 |
+
print("(konteks direset)")
|
| 204 |
+
continue
|
| 205 |
+
reply, score = respond(user)
|
| 206 |
+
suffix = f" [nll={score:.2f}]" if args.show_nll else ""
|
| 207 |
+
print(f"Indigo> {reply}{suffix}")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
main()
|