VISHAL18for4 commited on
Commit
47e9f16
·
verified ·
1 Parent(s): dc263a2

Delete neural_network.py

Browse files
Files changed (1) hide show
  1. neural_network.py +0 -439
neural_network.py DELETED
@@ -1,439 +0,0 @@
1
- """
2
- neural_network.py — Real PyTorch neural network built from scratch.
3
-
4
- KEY CHANGES:
5
- - Every item ingested is IMMEDIATELY written to knowledge.jsonl
6
- - On startup, knowledge.jsonl is read back → data_buffer is restored
7
- - Model checkpoint auto-saves every 30 training epochs
8
- - training_stats.json written after every training step (human-readable)
9
- """
10
-
11
- import torch
12
- import threading
13
- _TEXT_LOCK = threading.Lock()
14
- import numpy as np
15
- import os
16
- import json
17
- import re
18
- from datetime import datetime, UTC
19
- from collections import Counter, defaultdict
20
-
21
- # ─── FILES ON DISK ────────────────────────────────────────────────────────────
22
- KNOWLEDGE_FILE = 'knowledge.jsonl' # Every article/text the AI has seen
23
- CHECKPOINT_FILE = 'model_checkpoint.pt' # PyTorch weights + optimizer state
24
- STATS_FILE = 'training_stats.json' # Human-readable live stats
25
-
26
- # ─── CATEGORIES ───────────────────────────────────────────────────────────────
27
- CATEGORIES = ['technology', 'science', 'world', 'sports',
28
- 'business', 'health', 'entertainment', 'other']
29
-
30
- # ─── VOCABULARY ───────────────────────────────────────────────────────────────
31
- STOPWORDS = {
32
- 'a','an','the','is','it','in','on','at','to','for','of','and','or','but',
33
- 'was','are','were','be','been','have','has','had','do','does','did','will',
34
- 'would','could','should','may','might','that','this','these','those','with',
35
- 'from','by','as','not','also','than','then','so','if','when','what','how',
36
- 'who','which','its','their','our','your','my','his','her','we','they','he',
37
- 'she','you','i','me','him','us','them','said','says','new','one','two',
38
- }
39
-
40
- class Vocabulary:
41
- def __init__(self, max_size=10000):
42
- self.word2idx = {'<PAD>': 0, '<UNK>': 1}
43
- self.idx2word = {0: '<PAD>', 1: '<UNK>'}
44
- self.word_counts = Counter()
45
- self.max_size = max_size
46
- self.is_built = False
47
-
48
- def update(self, text: str):
49
- self.word_counts.update(self._tokenize(text))
50
-
51
- def build(self):
52
- top = self.word_counts.most_common(self.max_size - 2)
53
- self.word2idx = {'<PAD>': 0, '<UNK>': 1}
54
- self.idx2word = {0: '<PAD>', 1: '<UNK>'}
55
- for i, (word, _) in enumerate(top):
56
- idx = i + 2
57
- self.word2idx[word] = idx
58
- self.idx2word[idx] = word
59
- self.is_built = True
60
-
61
- def encode(self, text: str, max_len: int = 64) -> list:
62
- words = self._tokenize(text)[:max_len]
63
- ids = [self.word2idx.get(w, 1) for w in words]
64
- ids += [0] * (max_len - len(ids))
65
- return ids
66
-
67
- def _tokenize(self, text: str) -> list:
68
- text = text.lower()
69
- text = re.sub(r'[^\w\s]', ' ', text)
70
- return [w for w in text.split() if w not in STOPWORDS and len(w) > 2]
71
-
72
- def __len__(self):
73
- return len(self.word2idx)
74
-
75
-
76
- # ─── MODEL ────────────────────────────────────────────────────────────────────
77
- class TextClassifier(nn.Module):
78
- def __init__(self, vocab_size=10002, embed_dim=64,
79
- hidden=[256, 128, 64], num_classes=8):
80
- super().__init__()
81
- self.embed_dim = embed_dim
82
- self.hidden_dims = hidden
83
- self.num_classes = num_classes
84
-
85
- self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
86
- nn.init.normal_(self.embedding.weight, 0, 0.1)
87
-
88
- layers = []
89
- in_dim = embed_dim
90
- for h in hidden:
91
- layers += [nn.Linear(in_dim, h), nn.LayerNorm(h),
92
- nn.ReLU(), nn.Dropout(0.25)]
93
- in_dim = h
94
- layers.append(nn.Linear(in_dim, num_classes))
95
- self.net = nn.Sequential(*layers)
96
-
97
- self._activations = {}
98
- self._register_hooks()
99
-
100
- def _register_hooks(self):
101
- def make_hook(name):
102
- def hook(module, inp, out):
103
- if isinstance(out, torch.Tensor):
104
- v = out.detach().float()
105
- if v.dim() > 1:
106
- v = v.mean(0)
107
- self._activations[name] = v[:32].tolist()
108
- return hook
109
- for i, layer in enumerate(self.net):
110
- layer.register_forward_hook(make_hook(f'net.{i}'))
111
-
112
- def forward(self, x):
113
- emb = self.embedding(x)
114
- mask = (x != 0).float().unsqueeze(-1)
115
- pooled = (emb * mask).sum(1) / mask.sum(1).clamp(min=1)
116
- return self.net(pooled)
117
-
118
- def get_activations(self) -> dict:
119
- return dict(self._activations)
120
-
121
- def get_weight_info(self) -> dict:
122
- info = {}
123
- for name, param in self.named_parameters():
124
- if 'weight' in name and param.dim() == 2:
125
- w = param.detach().float().numpy()
126
- r, c = min(w.shape[0], 16), min(w.shape[1], 16)
127
- info[name] = {
128
- 'shape': list(w.shape),
129
- 'mean_abs': float(np.mean(np.abs(w))),
130
- 'std': float(np.std(w)),
131
- 'sample': w[:r, :c].tolist(),
132
- }
133
- return info
134
-
135
-
136
- # ─── LIVING NETWORK ───────────────────────────────────────────────────────────
137
- class LivingNetwork:
138
- """
139
- The brain. Wraps the PyTorch model with:
140
- - knowledge.jsonl → persistent record of everything it has read
141
- - model_checkpoint.pt → saved weights (restored on restart)
142
- - training_stats.json → live stats readable by the UI
143
- """
144
-
145
- AUTO_SAVE_EVERY = 30 # Save checkpoint every N training epochs
146
-
147
- def __init__(self):
148
- self.vocab = Vocabulary()
149
- self.model = TextClassifier()
150
- self.optimizer = optim.Adam(self.model.parameters(), lr=0.001, weight_decay=1e-5)
151
- self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
152
- self.optimizer, mode='min', patience=20, factor=0.5, min_lr=1e-5)
153
- self.criterion = nn.CrossEntropyLoss()
154
-
155
- self.epoch = 0
156
- self.total_samples = 0
157
- self.data_buffer = [] # (text, label_int) — in-memory training pool
158
- self.loss_history = []
159
- self.acc_history = []
160
- self.category_counts = defaultdict(int)
161
- self.knowledge_count = 0 # Total articles ever ingested
162
-
163
- self.stats = {
164
- 'epoch': 0,
165
- 'loss': '—',
166
- 'accuracy': '—',
167
- 'total_samples': 0,
168
- 'lr': 0.001,
169
- 'buffer_size': 0,
170
- 'knowledge_count': 0,
171
- 'last_text': '(nothing yet)',
172
- 'vocab_size': 2,
173
- 'status': 'idle',
174
- }
175
-
176
- # Load checkpoint first, then restore knowledge buffer
177
- self._load_checkpoint()
178
- self._load_knowledge()
179
-
180
- # ── KNOWLEDGE FILE ────────────────────────────────────────────────────────
181
- def _write_knowledge(self, text: str, category: str, source: str = 'unknown'):
182
- """Append one learned item to knowledge.jsonl immediately."""
183
- record = {
184
- 'text': text,
185
- 'category': category,
186
- 'source': source,
187
- 'timestamp': datetime.now(UTC).isoformat(),
188
- 'epoch_at_ingestion': self.epoch,
189
- }
190
- try:
191
- with open(KNOWLEDGE_FILE, 'a', encoding='utf-8') as f:
192
- f.write(json.dumps(record, ensure_ascii=False) + '\n')
193
- self.knowledge_count += 1
194
- except Exception:
195
- pass
196
-
197
- def _load_knowledge(self):
198
- """On startup: read knowledge.jsonl and rebuild data_buffer + vocab."""
199
- if not os.path.exists(KNOWLEDGE_FILE):
200
- return
201
- loaded = 0
202
- try:
203
- with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
204
- for line in f:
205
- line = line.strip()
206
- if not line:
207
- continue
208
- try:
209
- rec = json.loads(line)
210
- text = rec.get('text', '')
211
- cat = rec.get('category', 'other')
212
- label = CATEGORIES.index(cat) if cat in CATEGORIES else 7
213
- if len(text) > 20:
214
- self.data_buffer.append((text, label))
215
- self.vocab.update(text)
216
- self.category_counts[cat] += 1
217
- loaded += 1
218
- except Exception:
219
- continue
220
- self.knowledge_count = loaded
221
- if loaded > 0:
222
- self.vocab.build()
223
- # Trim buffer if huge
224
- if len(self.data_buffer) > 5000:
225
- self.data_buffer = self.data_buffer[-4000:]
226
- except Exception:
227
- pass
228
- self.stats['knowledge_count'] = self.knowledge_count
229
- self.stats['buffer_size'] = len(self.data_buffer)
230
- self.stats['vocab_size'] = len(self.vocab)
231
-
232
- def get_knowledge_file_stats(self) -> dict:
233
- """Return stats about the knowledge file for the UI."""
234
- if not os.path.exists(KNOWLEDGE_FILE):
235
- return {'exists': False, 'lines': 0, 'size_kb': 0}
236
- size = os.path.getsize(KNOWLEDGE_FILE)
237
- lines = 0
238
- try:
239
- with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
240
- lines = sum(1 for l in f if l.strip())
241
- except Exception:
242
- pass
243
- return {'exists': True, 'lines': lines, 'size_kb': round(size / 1024, 1)}
244
-
245
- def get_recent_knowledge(self, n: int = 20) -> list:
246
- """Return last N items from knowledge.jsonl for display."""
247
- if not os.path.exists(KNOWLEDGE_FILE):
248
- return []
249
- lines = []
250
- try:
251
- with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
252
- all_lines = [l.strip() for l in f if l.strip()]
253
- for line in reversed(all_lines[-n:]):
254
- try:
255
- lines.append(json.loads(line))
256
- except Exception:
257
- pass
258
- except Exception:
259
- pass
260
- return lines
261
-
262
- # ── INGEST ────────────────────────────────────────────────────────────────
263
- def ingest(self, text: str, category: str, source: str = 'unknown'):
264
- """
265
- Add text to training buffer AND write to knowledge.jsonl immediately.
266
- This is how the AI 'remembers' what it has learned.
267
- """
268
- cleaned = text.strip()
269
- if len(cleaned) < 20:
270
- return
271
- label = CATEGORIES.index(category) if category in CATEGORIES else 7
272
-
273
- # ① Write to disk first — never lose this
274
- self._write_knowledge(cleaned, category, source)
275
-
276
- # ② Add to in-memory training buffer
277
- self.data_buffer.append((cleaned, label))
278
- self.vocab.update(cleaned)
279
- self.category_counts[category] += 1
280
-
281
- # Rebuild vocab every 25 items
282
- if len(self.data_buffer) % 25 == 0:
283
- self.vocab.build()
284
-
285
- # Keep buffer bounded (disk has the full history)
286
- if len(self.data_buffer) > 5000:
287
- self.data_buffer = self.data_buffer[-4000:]
288
-
289
- self.stats.update({
290
- 'buffer_size': len(self.data_buffer),
291
- 'vocab_size': len(self.vocab),
292
- 'knowledge_count': self.knowledge_count,
293
- })
294
-
295
- # ── TRAINING ──────────────────────────────────────────────────────────────
296
- def train_step(self, batch_size: int = 32) -> float | None:
297
- if len(self.data_buffer) < batch_size or not self.vocab.is_built:
298
- return None
299
- with _TEXT_LOCK:
300
- self.model.train()
301
- idx = np.random.choice(len(self.data_buffer), batch_size, replace=False)
302
- batch = [self.data_buffer[i] for i in idx]
303
- texts, labels = zip(*batch)
304
-
305
- x = torch.tensor([self.vocab.encode(t) for t in texts], dtype=torch.long)
306
- y = torch.tensor(list(labels), dtype=torch.long)
307
-
308
- self.model.zero_grad(set_to_none=True)
309
- logits = self.model(x)
310
- loss = self.criterion(logits, y)
311
- loss.backward()
312
- for p in self.model.parameters():
313
- if p.grad is not None:
314
- p.grad.data.clamp_(-1.0, 1.0)
315
- self.optimizer.step()
316
- loss_val = loss.detach().item()
317
- acc = (logits.detach().argmax(1) == y).float().mean().item()
318
-
319
- self.epoch += 1
320
- self.total_samples += batch_size
321
- self.scheduler.step(loss_val)
322
-
323
- self.loss_history.append(round(loss_val, 5))
324
- self.acc_history.append(round(acc, 4))
325
- if len(self.loss_history) > 500:
326
- self.loss_history = self.loss_history[-500:]
327
- self.acc_history = self.acc_history[-500:]
328
-
329
- self.stats.update({
330
- 'epoch': self.epoch,
331
- 'loss': round(loss_val, 4),
332
- 'accuracy': round(acc * 100, 1),
333
- 'total_samples': self.total_samples,
334
- 'lr': round(self.optimizer.param_groups[0]['lr'], 7),
335
- 'buffer_size': len(self.data_buffer),
336
- 'last_text': texts[0][:120],
337
- 'vocab_size': len(self.vocab),
338
- 'knowledge_count': self.knowledge_count,
339
- })
340
-
341
- # Auto-save checkpoint every N epochs
342
- if self.epoch % self.AUTO_SAVE_EVERY == 0:
343
- self.save_checkpoint()
344
-
345
- # Always write stats file so UI can read without waiting
346
- self._write_stats_file()
347
-
348
- return loss_val
349
-
350
- def train_n_steps(self, n: int = 50) -> dict:
351
- losses = []
352
- for _ in range(n):
353
- l = self.train_step()
354
- if l is not None:
355
- losses.append(l)
356
- return {
357
- 'steps': len(losses),
358
- 'avg_loss': round(sum(losses) / len(losses), 5) if losses else None,
359
- }
360
-
361
- # ── INFERENCE ───────────────────────────────────────────���─────────────────
362
- def predict(self, text: str) -> dict:
363
- if not self.vocab.is_built or not text.strip():
364
- return {'error': 'Model not ready — start the network and let it train first'}
365
- self.model.eval()
366
- with torch.no_grad():
367
- x = torch.tensor([self.vocab.encode(text)], dtype=torch.long)
368
- logits = self.model(x)
369
- probs = torch.softmax(logits, dim=1)[0].tolist()
370
- pred = int(logits.argmax(1).item())
371
- return {
372
- 'prediction': CATEGORIES[pred],
373
- 'confidence': round(probs[pred] * 100, 1),
374
- 'all_probs': {c: round(p * 100, 2) for c, p in zip(CATEGORIES, probs)},
375
- }
376
-
377
- # ── VIZ STATE ─────────────────────────────────────────────────────────────
378
- def get_viz_state(self) -> dict:
379
- self.model.eval()
380
- with torch.no_grad():
381
- dummy = torch.zeros(1, 64, dtype=torch.long)
382
- self.model(dummy)
383
- return {
384
- 'layer_sizes': [self.model.embed_dim] + self.model.hidden_dims + [self.model.num_classes],
385
- 'activations': self.model.get_activations(),
386
- 'weights': self.model.get_weight_info(),
387
- 'loss_history': self.loss_history[-100:],
388
- 'acc_history': self.acc_history[-100:],
389
- 'stats': self.stats,
390
- 'category_counts':dict(self.category_counts),
391
- }
392
-
393
- # ── PERSISTENCE ───────────────────────────────────────────────────────────
394
- def save_checkpoint(self):
395
- try:
396
- torch.save({
397
- 'model': self.model.state_dict(),
398
- 'optimizer': self.optimizer.state_dict(),
399
- 'epoch': self.epoch,
400
- 'total_samples': self.total_samples,
401
- 'loss_history': self.loss_history,
402
- 'acc_history': self.acc_history,
403
- 'vocab_word2idx': self.vocab.word2idx,
404
- 'category_counts': dict(self.category_counts),
405
- 'stats': self.stats,
406
- }, CHECKPOINT_FILE)
407
- return True
408
- except Exception:
409
- return False
410
-
411
- def _load_checkpoint(self):
412
- if not os.path.exists(CHECKPOINT_FILE):
413
- return False
414
- try:
415
- ck = torch.load(CHECKPOINT_FILE, map_location='cpu')
416
- self.model.load_state_dict(ck['model'])
417
- self.optimizer.load_state_dict(ck['optimizer'])
418
- self.epoch = ck.get('epoch', 0)
419
- self.total_samples = ck.get('total_samples', 0)
420
- self.loss_history = ck.get('loss_history', [])
421
- self.acc_history = ck.get('acc_history', [])
422
- self.category_counts = defaultdict(int, ck.get('category_counts', {}))
423
- self.stats = ck.get('stats', self.stats)
424
- w2i = ck.get('vocab_word2idx', {})
425
- if w2i:
426
- self.vocab.word2idx = w2i
427
- self.vocab.idx2word = {v: k for k, v in w2i.items()}
428
- self.vocab.is_built = len(w2i) > 2
429
- return True
430
- except Exception:
431
- return False
432
-
433
- def _write_stats_file(self):
434
- """Write human-readable stats to training_stats.json for easy debugging."""
435
- try:
436
- with open(STATS_FILE, 'w') as f:
437
- json.dump({**self.stats, 'loss_last10': self.loss_history[-10:]}, f, indent=2)
438
- except Exception:
439
- pass