VISHAL18for4 commited on
Commit
301271f
Β·
verified Β·
1 Parent(s): 47e9f16

Upload 2 files

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