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

Delete image_model.py

Browse files
Files changed (1) hide show
  1. image_model.py +0 -363
image_model.py DELETED
@@ -1,363 +0,0 @@
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 threading
18
- _CNN_LOCK = threading.Lock()
19
- import numpy as np
20
- import os
21
- import json
22
- from datetime import datetime, UTC
23
- from pathlib import Path
24
- from collections import defaultdict
25
-
26
- try:
27
- from PIL import Image
28
- PIL_AVAILABLE = True
29
- except ImportError:
30
- PIL_AVAILABLE = False
31
-
32
- IMG_SIZE = 64 # resize all images to 64x64 (small = faster on CPU)
33
- CATEGORIES = [
34
- 'nature', 'technology', 'science', 'people',
35
- 'animals', 'food', 'sports', 'architecture'
36
- ]
37
-
38
- CNN_CHECKPOINT = 'cnn_checkpoint.pt'
39
- CNN_STATS_FILE = 'cnn_stats.json'
40
-
41
-
42
- # ── IMAGE PREPROCESSING ───────────────────────────────────────────────────────
43
- def load_image_tensor(path: str, size: int = IMG_SIZE):
44
- """Load image from disk → normalised float tensor (3, size, size)."""
45
- if not PIL_AVAILABLE:
46
- raise RuntimeError("Pillow not installed. Add 'Pillow' to requirements.txt")
47
- img = Image.open(path).convert('RGB')
48
- img = img.resize((size, size), Image.BILINEAR)
49
- arr = np.array(img, dtype=np.float32) / 255.0
50
- # Normalize with ImageNet mean/std (works well even for non-ImageNet data)
51
- mean = np.array([0.485, 0.456, 0.406])
52
- std = np.array([0.229, 0.224, 0.225])
53
- arr = (arr - mean) / std
54
- return torch.tensor(arr).permute(2, 0, 1) # HWC → CHW
55
-
56
-
57
- # ── CNN MODEL ─────────────────────────────────────────────────────────────────
58
- class ImageCNN(nn.Module):
59
- """
60
- A real Convolutional Neural Network.
61
- Learns to detect edges → shapes → textures → objects, layer by layer.
62
- Each Conv2d layer is looking for patterns the previous layer found.
63
- """
64
-
65
- def __init__(self, num_classes: int = 8):
66
- super().__init__()
67
- self.num_classes = num_classes
68
-
69
- # Convolutional feature extractor
70
- self.features = nn.Sequential(
71
- # Block 1 — learns basic edges and colours
72
- nn.Conv2d(3, 32, kernel_size=3, padding=1),
73
- nn.BatchNorm2d(32),
74
- nn.ReLU(),
75
- nn.Conv2d(32, 32, kernel_size=3, padding=1),
76
- nn.BatchNorm2d(32),
77
- nn.ReLU(),
78
- nn.MaxPool2d(2, 2), # 64→32
79
- nn.Dropout2d(0.1),
80
-
81
- # Block 2 — learns corners, curves, textures
82
- nn.Conv2d(32, 64, kernel_size=3, padding=1),
83
- nn.BatchNorm2d(64),
84
- nn.ReLU(),
85
- nn.Conv2d(64, 64, kernel_size=3, padding=1),
86
- nn.BatchNorm2d(64),
87
- nn.ReLU(),
88
- nn.MaxPool2d(2, 2), # 32→16
89
- nn.Dropout2d(0.15),
90
-
91
- # Block 3 — learns complex shapes and object parts
92
- nn.Conv2d(64, 128, kernel_size=3, padding=1),
93
- nn.BatchNorm2d(128),
94
- nn.ReLU(),
95
- nn.Conv2d(128, 128, kernel_size=3, padding=1),
96
- nn.BatchNorm2d(128),
97
- nn.ReLU(),
98
- nn.MaxPool2d(2, 2), # 16→8
99
- nn.Dropout2d(0.2),
100
- )
101
-
102
- # Classifier head
103
- self.classifier = nn.Sequential(
104
- nn.Flatten(),
105
- nn.Linear(128 * 8 * 8, 512),
106
- nn.ReLU(),
107
- nn.Dropout(0.4),
108
- nn.Linear(512, 128),
109
- nn.ReLU(),
110
- nn.Linear(128, num_classes),
111
- )
112
-
113
- # Activation tracking for visualization
114
- self._activations = {}
115
- self._register_hooks()
116
- self._initialize_weights()
117
-
118
- def _initialize_weights(self):
119
- for m in self.modules():
120
- if isinstance(m, nn.Conv2d):
121
- nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
122
- elif isinstance(m, nn.BatchNorm2d):
123
- nn.init.constant_(m.weight, 1)
124
- nn.init.constant_(m.bias, 0)
125
- elif isinstance(m, nn.Linear):
126
- nn.init.xavier_normal_(m.weight)
127
- nn.init.constant_(m.bias, 0)
128
-
129
- def _register_hooks(self):
130
- def make_hook(name):
131
- def hook(module, inp, out):
132
- if isinstance(out, torch.Tensor):
133
- v = out.detach().float()
134
- if v.dim() > 1:
135
- v = v.mean(0)
136
- if v.dim() > 1:
137
- v = v.mean(-1).mean(-1) # spatial mean for conv layers
138
- self._activations[name] = v[:16].tolist()
139
- return hook
140
- for i, layer in enumerate(self.features):
141
- layer.register_forward_hook(make_hook(f'conv_{i}'))
142
- for i, layer in enumerate(self.classifier):
143
- layer.register_forward_hook(make_hook(f'fc_{i}'))
144
-
145
- def forward(self, x):
146
- x = self.features(x)
147
- return self.classifier(x)
148
-
149
- def get_activations(self):
150
- return dict(self._activations)
151
-
152
- def get_feature_maps(self, x):
153
- """Return intermediate feature maps for visualization."""
154
- maps = {}
155
- for i, layer in enumerate(self.features):
156
- x = layer(x)
157
- if isinstance(layer, nn.ReLU):
158
- maps[f'relu_{i}'] = x.detach()
159
- return maps
160
-
161
-
162
- # ── LIVING IMAGE NETWORK ──────────────────────────────────────────────────────
163
- class LivingImageNetwork:
164
- """
165
- Wraps the CNN with training loop, data loading, and stats.
166
- Trains on images downloaded by ImageFetcher.
167
- """
168
-
169
- def __init__(self):
170
- self.model = ImageCNN(num_classes=len(CATEGORIES))
171
- self.optimizer = optim.Adam(self.model.parameters(), lr=0.001, weight_decay=1e-4)
172
- self.scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=50, gamma=0.8)
173
- self.criterion = nn.CrossEntropyLoss()
174
-
175
- self.epoch = 0
176
- self.total_images = 0
177
- self.loss_history = []
178
- self.acc_history = []
179
- self.category_counts = defaultdict(int)
180
-
181
- self.stats = {
182
- 'epoch': 0,
183
- 'loss': '—',
184
- 'accuracy': '—',
185
- 'total_images': 0,
186
- 'lr': 0.001,
187
- 'last_image': '(none yet)',
188
- 'status': 'idle',
189
- }
190
-
191
- self._load_checkpoint()
192
-
193
- # ── DATA LOADING ──────────────────────────────────────────────────────────
194
- def _load_batch(self, image_dir: Path, batch_size: int = 16):
195
- """
196
- Load a random batch of images from image_data/ folder.
197
- Returns (tensor_batch, label_batch) or None if not enough images.
198
- """
199
- if not PIL_AVAILABLE:
200
- return None
201
-
202
- all_paths = []
203
- for cat_idx, cat in enumerate(CATEGORIES):
204
- cat_dir = image_dir / cat
205
- if cat_dir.exists():
206
- for p in cat_dir.iterdir():
207
- if p.suffix.lower() in ('.jpg', '.jpeg', '.png', '.webp'):
208
- all_paths.append((str(p), cat_idx))
209
-
210
- if len(all_paths) < batch_size:
211
- return None
212
-
213
- import random
214
- batch_paths = random.sample(all_paths, batch_size)
215
- tensors, labels = [], []
216
-
217
- for path, label in batch_paths:
218
- try:
219
- t = load_image_tensor(path)
220
- tensors.append(t)
221
- labels.append(label)
222
- self.category_counts[CATEGORIES[label]] += 1
223
- except Exception:
224
- continue
225
-
226
- if not tensors:
227
- return None
228
-
229
- return torch.stack(tensors), torch.tensor(labels, dtype=torch.long)
230
-
231
- # ── TRAINING ──────────────────────────────────────────────────────────────
232
- def train_step(self, image_dir: Path, batch_size: int = 16):
233
- """One training step — load images, forward pass, backprop."""
234
- batch = self._load_batch(image_dir, batch_size)
235
- if batch is None:
236
- return None
237
-
238
- x, y = batch
239
- x = x.float() # ensure float32
240
- with _CNN_LOCK:
241
- self.model.train()
242
- self.model.zero_grad(set_to_none=True)
243
- logits = self.model(x)
244
- loss = self.criterion(logits, y)
245
- loss.backward()
246
- for p in self.model.parameters():
247
- if p.grad is not None:
248
- p.grad.data.clamp_(-1.0, 1.0)
249
- self.optimizer.step()
250
- loss_val = loss.detach().item()
251
- acc = (logits.detach().argmax(1) == y).float().mean().item()
252
-
253
- self.epoch += 1
254
- self.total_images += len(x)
255
- self.scheduler.step()
256
-
257
- self.loss_history.append(round(loss_val, 5))
258
- self.acc_history.append(round(acc, 4))
259
- if len(self.loss_history) > 500:
260
- self.loss_history = self.loss_history[-500:]
261
- self.acc_history = self.acc_history[-500:]
262
-
263
- last_path = batch[0] # just the paths string
264
- self.stats.update({
265
- 'epoch': self.epoch,
266
- 'loss': round(loss_val, 4),
267
- 'accuracy': round(acc * 100, 1),
268
- 'total_images': self.total_images,
269
- 'lr': round(self.optimizer.param_groups[0]['lr'], 7),
270
- })
271
-
272
- if self.epoch % 20 == 0:
273
- self._save_checkpoint()
274
- self._write_stats()
275
- return loss_val
276
-
277
- def train_n_steps(self, image_dir: Path, n: int = 20):
278
- losses = []
279
- for _ in range(n):
280
- l = self.train_step(image_dir)
281
- if l is not None:
282
- losses.append(l)
283
- return {
284
- 'steps': len(losses),
285
- 'avg_loss': round(sum(losses)/len(losses), 5) if losses else None,
286
- }
287
-
288
- # ── INFERENCE ─────────────────────────────────────────────────────────────
289
- def predict_image(self, image_path: str) -> dict:
290
- """Predict category of a single image."""
291
- if not PIL_AVAILABLE:
292
- return {'error': 'Pillow not installed'}
293
- try:
294
- t = load_image_tensor(image_path).unsqueeze(0)
295
- self.model.eval()
296
- with torch.no_grad():
297
- logits = self.model(t)
298
- probs = torch.softmax(logits, dim=1)[0].tolist()
299
- pred = int(logits.argmax(1).item())
300
- return {
301
- 'prediction': CATEGORIES[pred],
302
- 'confidence': round(probs[pred] * 100, 1),
303
- 'all_probs': {c: round(p*100, 2) for c, p in zip(CATEGORIES, probs)},
304
- }
305
- except Exception as e:
306
- return {'error': str(e)}
307
-
308
- # ── VIZ STATE ─────────────────────────────────────────────────────────────
309
- def get_viz_state(self) -> dict:
310
- self.model.eval()
311
- with torch.no_grad():
312
- dummy = torch.zeros(1, 3, IMG_SIZE, IMG_SIZE)
313
- self.model(dummy)
314
- return {
315
- 'layer_sizes': [3, 32, 64, 128, 512, 128, len(CATEGORIES)],
316
- 'activations': self.model.get_activations(),
317
- 'loss_history': self.loss_history[-100:],
318
- 'acc_history': self.acc_history[-100:],
319
- 'stats': self.stats,
320
- 'type': 'cnn',
321
- }
322
-
323
- # ── PERSISTENCE ───────────────────────────────────────────────────────────
324
- def _save_checkpoint(self):
325
- try:
326
- torch.save({
327
- 'model': self.model.state_dict(),
328
- 'optimizer': self.optimizer.state_dict(),
329
- 'epoch': self.epoch,
330
- 'total_images': self.total_images,
331
- 'loss_history': self.loss_history,
332
- 'acc_history': self.acc_history,
333
- 'category_counts': dict(self.category_counts),
334
- }, CNN_CHECKPOINT)
335
- except Exception:
336
- pass
337
-
338
- def _load_checkpoint(self):
339
- if not os.path.exists(CNN_CHECKPOINT):
340
- return
341
- try:
342
- ck = torch.load(CNN_CHECKPOINT, map_location='cpu')
343
- self.model.load_state_dict(ck['model'])
344
- self.optimizer.load_state_dict(ck['optimizer'])
345
- self.epoch = ck.get('epoch', 0)
346
- self.total_images = ck.get('total_images', 0)
347
- self.loss_history = ck.get('loss_history', [])
348
- self.acc_history = ck.get('acc_history', [])
349
- self.category_counts = defaultdict(int, ck.get('category_counts', {}))
350
- if self.epoch > 0:
351
- self.stats.update({
352
- 'epoch': self.epoch,
353
- 'total_images': self.total_images,
354
- })
355
- except Exception:
356
- pass
357
-
358
- def _write_stats(self):
359
- try:
360
- with open(CNN_STATS_FILE, 'w') as f:
361
- json.dump(self.stats, f, indent=2)
362
- except Exception:
363
- pass