Dantonitowin commited on
Commit
d4d5674
·
verified ·
1 Parent(s): f9d250c

Upload crazy.py

Browse files

idk dont mind the name this is the script i use to train it but also to say! if you wanna run it Ctrl + F find all "Eclipsed" and replace it with your PC user

Files changed (1) hide show
  1. crazy.py +597 -0
crazy.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch.utils.data import Dataset, DataLoader
4
+ import os
5
+ import time
6
+ import threading
7
+ import glob
8
+ import re
9
+ import json
10
+ import numpy as np
11
+ import wikipediaapi
12
+
13
+ # ============================================================
14
+ # 5x2T — Word Level Model with Disk Offloaded Optimizer
15
+ # Uses numpy for disk saves — much more memory efficient
16
+ # ============================================================
17
+
18
+ # ---------------- CONFIG ----------------
19
+ MAX_TRAIN_MIN = 60
20
+ BATCH_SIZE = 2
21
+ SEQ_LENGTH = 64
22
+ EMBED_SIZE = 192
23
+ HIDDEN_SIZE = 384
24
+ NUM_LAYERS = 1
25
+ DROPOUT = 0.2
26
+ LEARNING_RATE = 0.01
27
+ GRAD_CLIP = 1.0
28
+ AUTOSAVE_MIN = 5
29
+ MAX_VOCAB = 995600
30
+ TEMPERATURE = 0.8
31
+ RESPONSE_LENGTH = 40
32
+ MAX_LENGTH = 200
33
+ UNK_TOKEN = "<UNK>"
34
+ PAD_TOKEN = "<PAD>"
35
+
36
+ BASE_DIR = r"C:\Users\Eclipsed\Downloads\5x2T"
37
+ DATASET_DIR = os.path.join(BASE_DIR, "datasets")
38
+ MODEL_DIR = os.path.join(BASE_DIR, "5x2T-2")
39
+ MODEL_PATH = os.path.join(MODEL_DIR, "model.pth")
40
+ VOCAB_PATH = os.path.join(MODEL_DIR, "vocab.json")
41
+ OFFLOAD_DIR = os.path.join(MODEL_DIR, "offload")
42
+
43
+ DATASET_FOLDERS = [
44
+ os.path.join(DATASET_DIR, "chat_dataset"),
45
+ os.path.join(DATASET_DIR, "python_data"),
46
+ os.path.join(DATASET_DIR, "lua_dataset"),
47
+ os.path.join(DATASET_DIR, "dic_dataset"),
48
+ os.path.join(DATASET_DIR, "Wiki_dataset"),
49
+ r"E:\5x2T",
50
+ ]
51
+
52
+ DEVICE = torch.device("cpu")
53
+
54
+
55
+ # ---------------- DISK OFFLOAD OPTIMIZER ----------------
56
+ class DiskOffloadSGD:
57
+ """
58
+ SGD optimizer that stores momentum buffers on disk as numpy files.
59
+ Large buffers are processed in chunks to avoid RAM spikes.
60
+ """
61
+ CHUNK = 4_000_000 # process 4 million elements at a time
62
+
63
+ def __init__(self, params, lr=0.01, momentum=0.9, offload_dir=OFFLOAD_DIR):
64
+ self.params = list(params)
65
+ self.lr = lr
66
+ self.momentum = momentum
67
+ self.offload_dir = offload_dir
68
+ os.makedirs(offload_dir, exist_ok=True)
69
+
70
+ print(f" Initialising {len(self.params)} momentum buffers on disk...")
71
+ for i, p in enumerate(self.params):
72
+ path = os.path.join(offload_dir, f"m_{i}.npy")
73
+ if not os.path.exists(path):
74
+ # Save in chunks to avoid allocating the full array at once
75
+ shape = p.data.shape
76
+ total = p.data.numel()
77
+ flat = np.zeros(total, dtype=np.float32)
78
+ np.save(path, flat.reshape(shape))
79
+ del flat
80
+ print(f" Momentum buffers ready in {offload_dir}\n")
81
+
82
+ def zero_grad(self):
83
+ for p in self.params:
84
+ if p.grad is not None:
85
+ p.grad.detach_()
86
+ p.grad.zero_()
87
+
88
+ def step(self):
89
+ for i, p in enumerate(self.params):
90
+ if p.grad is None:
91
+ continue
92
+
93
+ path = os.path.join(self.offload_dir, f"m_{i}.npy")
94
+ shape = p.data.shape
95
+ total = p.data.numel()
96
+
97
+ # Use memory mapped file — only the chunk we touch is in RAM
98
+ buf_mm = np.load(path, mmap_mode="r+")
99
+ buf_flat = buf_mm.reshape(-1)
100
+ grad_flat = p.grad.data.reshape(-1).numpy()
101
+ data_flat = p.data.reshape(-1).numpy()
102
+
103
+ # Process in chunks so RAM never spikes
104
+ for start in range(0, total, self.CHUNK):
105
+ end = min(start + self.CHUNK, total)
106
+ buf_flat[start:end] = (self.momentum * buf_flat[start:end]
107
+ + grad_flat[start:end])
108
+ data_flat[start:end] -= self.lr * buf_flat[start:end]
109
+
110
+ # Write updated data back to param tensor
111
+ p.data.copy_(torch.from_numpy(data_flat.reshape(shape)))
112
+
113
+ # Flush mmap and free
114
+ buf_mm.flush()
115
+ del buf_mm, buf_flat, grad_flat, data_flat
116
+
117
+ def state_dict(self):
118
+ return {"lr": self.lr, "momentum": self.momentum}
119
+
120
+ def load_state_dict(self, state):
121
+ self.lr = state.get("lr", self.lr)
122
+ self.momentum = state.get("momentum", self.momentum)
123
+
124
+
125
+ # ---------------- DATASET DISCOVERY ----------------
126
+ def find_all_txt_files(folders):
127
+ all_files = []
128
+ for folder in folders:
129
+ if os.path.exists(folder):
130
+ found = glob.glob(os.path.join(folder, "**", "*.txt"), recursive=True)
131
+ all_files.extend(found)
132
+ print(f" [{os.path.basename(folder)}] -> {len(found)} file(s)")
133
+ else:
134
+ print(f" [SKIP] Not found: {folder}")
135
+ if not all_files:
136
+ raise FileNotFoundError("No .txt files found. Check your dataset paths.")
137
+ print(f"\n Total files: {len(all_files)}\n")
138
+ return all_files
139
+
140
+
141
+ # ---------------- TOKENISER ----------------
142
+ def tokenise(text):
143
+ return re.findall(r"\b\w+\b|[\"'.,!?;:\-\n]", text.lower())
144
+
145
+
146
+ def build_vocab(files, max_vocab=MAX_VOCAB):
147
+ print(" Building vocabulary...")
148
+ freq = {}
149
+ total_tokens = 0
150
+ for f in files:
151
+ try:
152
+ with open(f, "r", encoding="utf-8", errors="ignore") as file:
153
+ tokens = tokenise(file.read())
154
+ for t in tokens:
155
+ freq[t] = freq.get(t, 0) + 1
156
+ total_tokens += len(tokens)
157
+ except Exception as e:
158
+ print(f" [WARNING] Could not read {f}: {e}")
159
+
160
+ sorted_vocab = sorted(freq.items(), key=lambda x: x[1], reverse=True)
161
+ vocab_words = [PAD_TOKEN, UNK_TOKEN] + [w for w, _ in sorted_vocab[:max_vocab - 2]]
162
+ word2idx = {w: i for i, w in enumerate(vocab_words)}
163
+ idx2word = {i: w for i, w in enumerate(vocab_words)}
164
+
165
+ print(f" Total tokens : {total_tokens:,}")
166
+ print(f" Unique words : {len(freq):,}")
167
+ print(f" Vocab size : {len(vocab_words):,}\n")
168
+
169
+ return vocab_words, word2idx, idx2word
170
+
171
+
172
+ def save_vocab(vocab_words, path):
173
+ with open(path, "w", encoding="utf-8") as f:
174
+ json.dump(vocab_words, f)
175
+
176
+
177
+ def load_vocab(path):
178
+ with open(path, "r", encoding="utf-8") as f:
179
+ vocab_words = json.load(f)
180
+ word2idx = {w: i for i, w in enumerate(vocab_words)}
181
+ idx2word = {i: w for i, w in enumerate(vocab_words)}
182
+ return vocab_words, word2idx, idx2word
183
+
184
+
185
+ # ---------------- DATASET ----------------
186
+ class WordDataset(Dataset):
187
+ def __init__(self, files, word2idx):
188
+ self.data = []
189
+ unk_idx = word2idx.get(UNK_TOKEN, 1)
190
+ for f in files:
191
+ try:
192
+ with open(f, "r", encoding="utf-8", errors="ignore") as file:
193
+ tokens = tokenise(file.read())
194
+ self.data += [word2idx.get(t, unk_idx) for t in tokens]
195
+ except Exception as e:
196
+ print(f" [WARNING] Could not read {f}: {e}")
197
+
198
+ if not self.data:
199
+ raise ValueError("Dataset is empty after tokenisation.")
200
+ print(f" Dataset tokens: {len(self.data):,}\n")
201
+
202
+ def __len__(self):
203
+ return len(self.data) - SEQ_LENGTH
204
+
205
+ def __getitem__(self, idx):
206
+ x = torch.tensor(self.data[idx:idx + SEQ_LENGTH], dtype=torch.long)
207
+ y = torch.tensor(self.data[idx + 1:idx + SEQ_LENGTH + 1], dtype=torch.long)
208
+ return x, y
209
+
210
+
211
+ # ---------------- MODEL ----------------
212
+ class Model(nn.Module):
213
+ def __init__(self, vocab_size):
214
+ super().__init__()
215
+ self.embed = nn.Embedding(vocab_size, EMBED_SIZE, padding_idx=0)
216
+ self.dropout = nn.Dropout(DROPOUT)
217
+ self.lstm = nn.LSTM(
218
+ EMBED_SIZE, HIDDEN_SIZE,
219
+ num_layers=NUM_LAYERS,
220
+ batch_first=True,
221
+ dropout=0
222
+ )
223
+ self.norm = nn.LayerNorm(HIDDEN_SIZE)
224
+ self.fc = nn.Linear(HIDDEN_SIZE, vocab_size)
225
+
226
+ def forward(self, x, hc=None):
227
+ x = self.dropout(self.embed(x))
228
+ x, hc = self.lstm(x, hc)
229
+ x = self.norm(x)
230
+ x = self.fc(x)
231
+ return x, hc
232
+
233
+
234
+ # ---------------- SETUP ----------------
235
+ def setup_dirs():
236
+ os.makedirs(MODEL_DIR, exist_ok=True)
237
+ os.makedirs(OFFLOAD_DIR, exist_ok=True)
238
+ os.makedirs(os.path.join(MODEL_DIR, "questions"), exist_ok=True)
239
+
240
+
241
+ # ---------------- GENERATE ----------------
242
+ def generate(model, word2idx, idx2word, seed_text, length=RESPONSE_LENGTH, temperature=TEMPERATURE):
243
+ model.eval()
244
+ tokens = tokenise(seed_text)
245
+ unk_idx = word2idx.get(UNK_TOKEN, 1)
246
+ indices = [word2idx.get(t, unk_idx) for t in tokens]
247
+ hc = None
248
+
249
+ with torch.no_grad():
250
+ for _ in range(min(length, MAX_LENGTH)):
251
+ x = torch.tensor([indices[-SEQ_LENGTH:]], dtype=torch.long)
252
+ out, hc = model(x, hc)
253
+ logits = out[0, -1] / temperature
254
+ probs = torch.softmax(logits, dim=0)
255
+ next_idx = torch.multinomial(probs, 1).item()
256
+ indices.append(next_idx)
257
+
258
+ generated = indices[len(tokens):]
259
+ words = [idx2word.get(i, UNK_TOKEN) for i in generated]
260
+ return " ".join(words)
261
+
262
+
263
+ def format_response(text):
264
+ text = re.sub(r' ([.,!?;:])', r'\1', text)
265
+ text = re.sub(r'\n ', '\n', text)
266
+ if text:
267
+ text = text[0].upper() + text[1:]
268
+ return text
269
+
270
+
271
+ # ---------------- WIKIPEDIA ----------------
272
+ wiki_api = wikipediaapi.Wikipedia(
273
+ language='en',
274
+ extract_format=wikipediaapi.ExtractFormat.WIKI,
275
+ user_agent="5x2T-AI/1.0"
276
+ )
277
+
278
+ def search_wikipedia(query):
279
+ try:
280
+ search_term = query.lower()
281
+ for prefix in ["what is ", "what are ", "who is ", "who was ",
282
+ "tell me about ", "explain ", "define ",
283
+ "what was ", "how does ", "how do "]:
284
+ search_term = search_term.replace(prefix, "")
285
+ search_term = search_term.replace("?", "").strip()
286
+ page = wiki_api.page(search_term)
287
+ if page.exists():
288
+ return f"[Wikipedia: {page.title}]\n{page.summary[:600]}"
289
+ return None
290
+ except Exception as e:
291
+ print(f" [WARNING] Wikipedia lookup failed: {e}")
292
+ return None
293
+
294
+
295
+ def should_search_wiki(text):
296
+ triggers = [
297
+ "what is", "what are", "who is", "who was",
298
+ "tell me about", "explain", "define", "what was",
299
+ "how does", "how do"
300
+ ]
301
+ return any(text.lower().strip().startswith(t) for t in triggers)
302
+
303
+
304
+ # ---------------- TRAINING ----------------
305
+ def train():
306
+ setup_dirs()
307
+ print("=" * 55)
308
+ print(" 5x2T — Word Level Training (Disk Offload)")
309
+ print(f" Device : {DEVICE}")
310
+ print(f" Offload dir : {OFFLOAD_DIR}")
311
+ print(f" Target : {MAX_TRAIN_MIN} minutes")
312
+ print("=" * 55 + "\n")
313
+
314
+ print("Scanning dataset folders...")
315
+ files = find_all_txt_files(DATASET_FOLDERS)
316
+
317
+ if os.path.exists(VOCAB_PATH):
318
+ print(" Found existing vocab — loading...")
319
+ vocab_words, word2idx, idx2word = load_vocab(VOCAB_PATH)
320
+ print(f" Vocab size: {len(vocab_words):,}\n")
321
+ else:
322
+ vocab_words, word2idx, idx2word = build_vocab(files)
323
+ save_vocab(vocab_words, VOCAB_PATH)
324
+ print(f" Vocab saved to {VOCAB_PATH}\n")
325
+
326
+ print("Loading dataset...")
327
+ dataset = WordDataset(files, word2idx)
328
+ loader = DataLoader(
329
+ dataset, batch_size=BATCH_SIZE,
330
+ shuffle=True, num_workers=0
331
+ )
332
+
333
+ vocab_size = len(vocab_words)
334
+ model = Model(vocab_size)
335
+ criterion = nn.CrossEntropyLoss(ignore_index=0)
336
+ optimizer = DiskOffloadSGD(
337
+ model.parameters(),
338
+ lr=LEARNING_RATE,
339
+ momentum=0.9,
340
+ offload_dir=OFFLOAD_DIR
341
+ )
342
+
343
+ param_count = sum(p.numel() for p in model.parameters())
344
+ print(f" Model parameters : {param_count:,}")
345
+ print(f" Vocab size : {vocab_size:,}")
346
+ print(f" Optimizer : DiskOffloadSGD (numpy on disk)")
347
+ print(f" Offload folder : {OFFLOAD_DIR}\n")
348
+
349
+ if os.path.exists(MODEL_PATH + ".npz"):
350
+ load_path = MODEL_PATH + ".npz"
351
+ elif os.path.exists(MODEL_PATH):
352
+ load_path = MODEL_PATH
353
+ else:
354
+ load_path = None
355
+
356
+ if load_path:
357
+ try:
358
+ if load_path.endswith(".npz"):
359
+ raw = np.load(load_path)
360
+ checkpoint = {k: torch.from_numpy(raw[k]) for k in raw.files}
361
+ else:
362
+ checkpoint = torch.load(load_path, map_location="cpu")
363
+ model_state = model.state_dict()
364
+ loaded = 0
365
+ for k in checkpoint.keys():
366
+ if k in model_state and checkpoint[k].shape == model_state[k].shape:
367
+ model_state[k] = checkpoint[k]
368
+ loaded += 1
369
+ model.load_state_dict(model_state)
370
+ print(f" Resumed from checkpoint ({loaded} layers matched)\n")
371
+ except Exception as e:
372
+ print(f" Could not load checkpoint: {e} — starting fresh\n")
373
+
374
+ print("-" * 55)
375
+ print(" Training started...\n")
376
+
377
+ start_time = time.time()
378
+ epoch = 0
379
+ best_loss = float("inf")
380
+ total_tokens = 0
381
+ last_autosave = 0
382
+ epoch_loss = 0
383
+ batches = 0
384
+ loss = None
385
+
386
+ def print_progress():
387
+ while True:
388
+ elapsed_sec = time.time() - start_time
389
+ elapsed_min = elapsed_sec / 60
390
+ speed = total_tokens / (elapsed_sec + 1e-5)
391
+ avg_loss = epoch_loss / max(batches, 1) if batches > 0 else 0
392
+ mins = int(elapsed_sec // 60)
393
+ secs = int(elapsed_sec % 60)
394
+ current_loss = loss.item() if loss is not None else 0.0
395
+ print(
396
+ f" Epoch {epoch+1:>3} | "
397
+ f"Batch {batches:>5} | "
398
+ f"Loss: {current_loss:.4f} | "
399
+ f"Avg: {avg_loss:.4f} | "
400
+ f"Speed: {speed:.0f} tok/s | "
401
+ f"Time: {mins:02d}:{secs:02d}/{MAX_TRAIN_MIN:02d}:00",
402
+ end="\r"
403
+ )
404
+ time.sleep(1)
405
+
406
+ # Start progress printing thread
407
+ progress_thread = threading.Thread(target=print_progress, daemon=True)
408
+ progress_thread.start()
409
+
410
+ while (time.time() - start_time) / 60 < MAX_TRAIN_MIN:
411
+ epoch_loss = 0
412
+ batches = 0
413
+
414
+ for x, y in loader:
415
+ optimizer.zero_grad()
416
+ out, _ = model(x)
417
+ out = out.view(-1, vocab_size)
418
+ y = y.view(-1)
419
+ loss = criterion(out, y)
420
+ loss.backward()
421
+ nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)
422
+ optimizer.step()
423
+
424
+ epoch_loss += loss.item()
425
+ batches += 1
426
+ total_tokens += x.numel()
427
+
428
+ elapsed_min = (time.time() - start_time) / 60
429
+ mins = int(elapsed_min)
430
+ secs = int((elapsed_min - mins) * 60)
431
+ if elapsed_min - last_autosave >= AUTOSAVE_MIN:
432
+ try:
433
+ torch.save(model.state_dict(), MODEL_PATH)
434
+ last_autosave = elapsed_min
435
+ print(f"\n [Autosave] {mins:02d}:{secs:02d} -> {MODEL_PATH}")
436
+ except MemoryError:
437
+ try:
438
+ print(f"\n [Autosave] RAM full - saving directly to disk...")
439
+ tmp_path = MODEL_PATH + ".tmp"
440
+ with open(tmp_path, "wb") as f:
441
+ state = {k: v.numpy() for k, v in model.state_dict().items()}
442
+ np.savez_compressed(f, **state)
443
+ os.replace(tmp_path, MODEL_PATH + ".npz")
444
+ last_autosave = elapsed_min
445
+ print(f"\n [Autosave] {mins:02d}:{secs:02d} -> {MODEL_PATH}.npz")
446
+ except OSError:
447
+ print(f"\n [5xSc-404] Low storage or memory - autosave skipped")
448
+ except Exception as e:
449
+ print(f"\n [5xSc-9512] Unknown autosave error: {e}")
450
+ except OSError:
451
+ print(f"\n [5xSc-404] Low storage or memory - autosave skipped")
452
+ except KeyboardInterrupt:
453
+ print(f"\n [5xSc-80082] Training stopped early - saving...")
454
+ try:
455
+ torch.save(model.state_dict(), MODEL_PATH)
456
+ except Exception:
457
+ state = {k: v.numpy() for k, v in model.state_dict().items()}
458
+ np.savez_compressed(MODEL_PATH + ".npz", **state)
459
+ print(f" Model saved. Exiting.")
460
+ raise
461
+ except Exception as e:
462
+ err = str(e).lower()
463
+ if "corrupt" in err or "invalid" in err:
464
+ print(f"\n [5xSc-312] Corruption detected: {e}")
465
+ elif "allocat" in err or "memory" in err:
466
+ print(f"\n [5xSc-500] Memory allocation failed: {e}")
467
+ else:
468
+ print(f"\n [5xSc-9512] Unknown error: {e}")
469
+ if elapsed_min >= MAX_TRAIN_MIN:
470
+ break
471
+
472
+ print()
473
+ epoch += 1
474
+ avg_loss = epoch_loss / max(batches, 1)
475
+
476
+ if avg_loss < best_loss:
477
+ best_loss = avg_loss
478
+ try:
479
+ torch.save(model.state_dict(), MODEL_PATH)
480
+ except MemoryError:
481
+ print(f" [Save] RAM full — saving directly to disk...")
482
+ tmp_path = MODEL_PATH + ".tmp"
483
+ with open(tmp_path, "wb") as f:
484
+ state = {k: v.numpy() for k, v in model.state_dict().items()}
485
+ np.savez_compressed(f, **state)
486
+ os.replace(tmp_path, MODEL_PATH + ".npz")
487
+ print(f" [Saved] Best loss: {best_loss:.4f}\n")
488
+
489
+ if (time.time() - start_time) / 60 >= MAX_TRAIN_MIN:
490
+ break
491
+
492
+ print("-" * 55)
493
+ print(f" Done! Epochs: {epoch} | Best loss: {best_loss:.4f}")
494
+ print(f" Model saved to: {MODEL_PATH}\n")
495
+
496
+ print(" Sample generation:")
497
+ seed = '"what is marxism"\n"'
498
+ sample = generate(model, word2idx, idx2word, seed_text=seed, length=40)
499
+ print(f" {format_response(sample)}\n")
500
+
501
+ return model, word2idx, idx2word
502
+
503
+
504
+ # ---------------- CHAT ----------------
505
+ def chat(model=None, word2idx=None, idx2word=None):
506
+ print("=" * 55)
507
+ print(" 5x2T — Chat")
508
+ print(" Commands:")
509
+ print(" quit — exit")
510
+ print(" temp X — temperature e.g. temp 0.7")
511
+ print(" length X — response length e.g. length 60")
512
+ print(" maxlen X — max length cap e.g. maxlen 300")
513
+ print(" wiki X — force Wikipedia lookup e.g. wiki Python")
514
+ print("=" * 55 + "\n")
515
+
516
+ if model is None:
517
+ if not os.path.exists(VOCAB_PATH):
518
+ print("[ERROR] No vocab found. Run training first.")
519
+ return
520
+ if not os.path.exists(MODEL_PATH):
521
+ print("[ERROR] No model found. Run training first.")
522
+ return
523
+ vocab_words, word2idx, idx2word = load_vocab(VOCAB_PATH)
524
+ model = Model(len(vocab_words))
525
+ model.load_state_dict(torch.load(MODEL_PATH, map_location="cpu"))
526
+ model.eval()
527
+ param_count = sum(p.numel() for p in model.parameters())
528
+ print(f" Vocab size : {len(vocab_words):,}")
529
+ print(f" Model parameters : {param_count:,}")
530
+ print(f" Device : {DEVICE}\n")
531
+
532
+ temperature = TEMPERATURE
533
+ response_length = RESPONSE_LENGTH
534
+ max_length = MAX_LENGTH
535
+
536
+ while True:
537
+ user_input = input("You: ").strip()
538
+
539
+ if not user_input:
540
+ continue
541
+ if user_input.lower() in ("quit", "exit", "q"):
542
+ print("Goodbye.")
543
+ break
544
+ if user_input.lower().startswith("temp "):
545
+ try:
546
+ temperature = float(user_input.split()[1])
547
+ print(f" Temperature -> {temperature}\n")
548
+ except:
549
+ print(" Usage: temp 0.8\n")
550
+ continue
551
+ if user_input.lower().startswith("length "):
552
+ try:
553
+ response_length = int(user_input.split()[1])
554
+ print(f" Length -> {response_length}\n")
555
+ except:
556
+ print(" Usage: length 50\n")
557
+ continue
558
+ if user_input.lower().startswith("maxlen "):
559
+ try:
560
+ max_length = int(user_input.split()[1])
561
+ print(f" Max length -> {max_length}\n")
562
+ except:
563
+ print(" Usage: maxlen 300\n")
564
+ continue
565
+
566
+ if user_input.lower().startswith("wiki "):
567
+ query = user_input[5:].strip()
568
+ result = search_wikipedia(query)
569
+ reply = result if result else f"No Wikipedia page found for '{query}'"
570
+ print(f"5x2T: {reply}\n")
571
+ continue
572
+
573
+ wiki_result = None
574
+ if should_search_wiki(user_input):
575
+ wiki_result = search_wikipedia(user_input)
576
+
577
+ if wiki_result:
578
+ print(f"5x2T: {wiki_result}\n")
579
+ else:
580
+ seed = f'"{user_input.lower()}"\n"'
581
+ raw = generate(model, word2idx, idx2word,
582
+ seed_text=seed,
583
+ length=min(response_length, max_length),
584
+ temperature=temperature)
585
+ reply = format_response(raw)
586
+ print(f"5x2T: {reply}\n")
587
+
588
+
589
+ # ---------------- ENTRY POINT ----------------
590
+ if __name__ == "__main__":
591
+ import sys
592
+ if len(sys.argv) > 1 and sys.argv[1] == "chat":
593
+ chat()
594
+ else:
595
+ model, word2idx, idx2word = train()
596
+ print("\nStarting chat...\n")
597
+ chat(model, word2idx, idx2word)