cyberai-1 commited on
Commit
7902c8d
·
1 Parent(s): a363339

updat file

Browse files
README.md CHANGED
@@ -111,24 +111,82 @@ project/
111
 
112
  ## 4. Model Architecture
113
 
114
- Both models share the **same 4-block VGG-inspired architecture**
115
- with `GlobalAveragePooling` replacing `Flatten` for ~20× fewer parameters.
116
 
117
  ```
118
- Input (B, 3, 150, 150) — RGB, 3 channels
119
-
120
- ├─ Block 1: Conv(32)×2 → BN → ReLU → MaxPool(2) [15075]
121
- ├─ Block 2: Conv(64)×2 → BN → ReLU → MaxPool(2) Drop(0.10) [7537]
122
- ├─ Block 3: Conv(128)×2→ BN → ReLU → MaxPool(2) Drop(0.15) [3718]
123
- ├─ Block 4: Conv(256)×2→ BN → ReLU → MaxPool(2) Drop(0.20) [189]
124
-
125
- ├─ GlobalAveragePooling2D [→ (B,256)]
126
- ├─ Dense(256) ReLU → Dropout(0.30)
127
- └─ Dense(6) → Softmax / LogSoftmax
128
-
129
- Trainable parameters : ~2.1M (PyTorch) | ~2.2M (TensorFlow)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  Input size : 150 × 150 × 3 (RGB)
131
- Normalization : ImageNet mean/std [0.485,0.456,0.406] / [0.229,0.224,0.225]
132
  ```
133
 
134
  **Training configuration:**
@@ -138,7 +196,7 @@ Normalization : ImageNet mean/std [0.485,0.456,0.406] / [0.229,0.224,0.2
138
  | Optimizer | Adam |
139
  | Learning rate | 1e-4 |
140
  | LR scheduler | ReduceLROnPlateau (factor=0.5, patience=3) |
141
- | Early stopping | patience=15 |
142
  | Batch size | 32 |
143
  | Max epochs | 50 |
144
  | Loss function | CrossEntropyLoss / SparseCategoricalCrossentropy |
@@ -150,10 +208,6 @@ Normalization : ImageNet mean/std [0.485,0.456,0.406] / [0.229,0.224,0.2
150
  **Python 3.9+** is required.
151
 
152
  ```bash
153
- # Clone / download the project
154
- git clone <your-repo-url>
155
- cd project
156
-
157
  # Install dependencies
158
  pip install -r requirements.txt
159
  ```
@@ -238,7 +292,7 @@ python main.py \
238
  --mode eval \
239
  --model_path parfait_model.pth \
240
  --data_dir ../data \
241
- --output_dir ./eval_output_dir
242
 
243
  # Evaluate TensorFlow model
244
  python main.py \
@@ -246,7 +300,7 @@ python main.py \
246
  --mode eval \
247
  --model_path parfait_model.keras \
248
  --data_dir ../data \
249
- --output_dir ./eval_output_dir
250
 
251
  ```
252
 
@@ -265,12 +319,11 @@ outputs/
265
 
266
  ```bash
267
  # Start Flask server
268
- python app.py
269
- # → http://localhost:5000
270
-
271
- # Production (gunicorn)
272
  gunicorn app:app --bind 0.0.0.0:8000 --workers 1 --timeout 120
273
  ```
 
 
 
274
 
275
  **Features:**
276
  - Model selector: **PyTorch** or **TensorFlow**
@@ -302,7 +355,7 @@ gunicorn app:app --bind 0.0.0.0:8000 --workers 1 --timeout 120
302
  | street | 0.90 | 0.91 | 0.90 |
303
 
304
  > Note: `buildings` vs `street` is the hardest pair due to visual overlap.
305
- > Run `--mode eval` on your trained model to get your exact numbers.
306
 
307
  ---
308
 
@@ -352,30 +405,3 @@ The seed fixes:
352
  - DataLoader worker seeds (via `worker_init_fn`)
353
 
354
  ---
355
-
356
- ## 10. Deployment
357
-
358
- ### PythonAnywhere (recommended, free tier available)
359
- 1. Upload all project files via the **Files** tab
360
- 2. Upload `parfait_model.pth` and `parfait_model.keras`
361
- 3. Open a Bash console → `pip install -r requirements.txt`
362
- 4. **Web** tab → New web app → Manual configuration → Python 3.10
363
- 5. Edit the WSGI file:
364
- ```python
365
- import sys
366
- sys.path.insert(0, '/home/YOUR_USERNAME/project')
367
- from app import app as application
368
- ```
369
- 6. **Reload** → your app is live at `https://yourusername.pythonanywhere.com`
370
-
371
- ### Railway / Render
372
- 1. Push the project to a GitHub repository
373
- 2. Connect the repo to Railway or Render
374
- 3. Set start command: `gunicorn app:app --bind 0.0.0.0:$PORT --workers 1 --timeout 120`
375
- 4. Upload model files as part of the repo or via persistent volume
376
-
377
- ### Environment variables
378
- | Variable | Default | Description |
379
- |---------|---------|--------------------------|
380
- | `PORT` | `5000` | Flask server port |
381
-
 
111
 
112
  ## 4. Model Architecture
113
 
114
+ ### 4.1 TensorFlow / Keras model
 
115
 
116
  ```
117
+ Input: (228, 228, 3)
118
+
119
+ Block 1: Conv2D(32, 5×5, ReLU) → MaxPool(2×2) 224×224×32 → 112×112×32
120
+ Block 2: Conv2D(32, 5×5, ReLU) → MaxPool(2×2) 108×108×32 54×54×32
121
+ Block 3: Conv2D(32, 3×3, ReLU) → MaxPool(2×2) 52×52×32 26×26×32
122
+ Block 4: Conv2D(64, 3×3, ReLU) → MaxPool(2×2) 24×24×64 12×12×64
123
+ Block 5: Conv2D(64, 3×3, ReLU) → MaxPool(2×2) → 10×10×64 → 5×5×64
124
+
125
+ Flatten 1600
126
+ Dense(1024, ReLU)
127
+ Dropout(0.20)
128
+ Dense(124, ReLU)
129
+ Dropout(0.20)
130
+ Dense(6, Softmax)
131
+
132
+ Trainable parameters : 1,86M
133
+ Input size : 228 × 228 × 3 (RGB)
134
+ ```
135
+
136
+ ### 4.1 PyTorch model
137
+
138
+ ```
139
+ Input: (B, 3, 150, 150)
140
+
141
+ Block 1:
142
+ Conv2d(3 → 32, 3×3, padding=1)
143
+ BatchNorm2d(32)
144
+ ReLU
145
+ Conv2d(32 → 32, 3×3, padding=1)
146
+ BatchNorm2d(32)
147
+ ReLU
148
+ MaxPool2d(2)
149
+
150
+ Block 2:
151
+ Conv2d(32 → 64, 3×3, padding=1)
152
+ BatchNorm2d(64)
153
+ ReLU
154
+ Conv2d(64 → 64, 3×3, padding=1)
155
+ BatchNorm2d(64)
156
+ ReLU
157
+ MaxPool2d(2)
158
+ Dropout2d(0.10)
159
+
160
+ Block 3:
161
+ Conv2d(64 → 128, 3×3, padding=1)
162
+ BatchNorm2d(128)
163
+ ReLU
164
+ Conv2d(128 → 128, 3×3, padding=1)
165
+ BatchNorm2d(128)
166
+ ReLU
167
+ MaxPool2d(2)
168
+ Dropout2d(0.15)
169
+
170
+ Block 4:
171
+ Conv2d(128 → 256, 3×3, padding=1)
172
+ BatchNorm2d(256)
173
+ ReLU
174
+ Conv2d(256 → 256, 3×3, padding=1)
175
+ BatchNorm2d(256)
176
+ ReLU
177
+ MaxPool2d(2)
178
+ Dropout2d(0.20)
179
+
180
+ AdaptiveAvgPool2d(1) → (B, 256, 1, 1)
181
+ Flatten → (B, 256)
182
+ Linear(256 → 256)
183
+ ReLU
184
+ Dropout(0.30)
185
+ Linear(256 → 6)
186
+
187
+
188
+ Trainable parameters : 1.24M
189
  Input size : 150 × 150 × 3 (RGB)
 
190
  ```
191
 
192
  **Training configuration:**
 
196
  | Optimizer | Adam |
197
  | Learning rate | 1e-4 |
198
  | LR scheduler | ReduceLROnPlateau (factor=0.5, patience=3) |
199
+ | Early stopping | patience=5 |
200
  | Batch size | 32 |
201
  | Max epochs | 50 |
202
  | Loss function | CrossEntropyLoss / SparseCategoricalCrossentropy |
 
208
  **Python 3.9+** is required.
209
 
210
  ```bash
 
 
 
 
211
  # Install dependencies
212
  pip install -r requirements.txt
213
  ```
 
292
  --mode eval \
293
  --model_path parfait_model.pth \
294
  --data_dir ../data \
295
+ --output_dir ./outputs
296
 
297
  # Evaluate TensorFlow model
298
  python main.py \
 
300
  --mode eval \
301
  --model_path parfait_model.keras \
302
  --data_dir ../data \
303
+ --output_dir ./outputs
304
 
305
  ```
306
 
 
319
 
320
  ```bash
321
  # Start Flask server
 
 
 
 
322
  gunicorn app:app --bind 0.0.0.0:8000 --workers 1 --timeout 120
323
  ```
324
+ # Live link
325
+
326
+ For instance the app is available at: https://huggingface.co/spaces/CyberAl/Image_Classification_Parfait_TOLEFO
327
 
328
  **Features:**
329
  - Model selector: **PyTorch** or **TensorFlow**
 
355
  | street | 0.90 | 0.91 | 0.90 |
356
 
357
  > Note: `buildings` vs `street` is the hardest pair due to visual overlap.
358
+
359
 
360
  ---
361
 
 
405
  - DataLoader worker seeds (via `worker_init_fn`)
406
 
407
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .cnn import CNN_Torch, build_cnn_tf
2
+ from .train import Trainer
models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (232 Bytes). View file
 
models/__pycache__/cnn.cpython-312.pyc ADDED
Binary file (7.23 kB). View file
 
models/__pycache__/train.cpython-312.pyc ADDED
Binary file (8.69 kB). View file
 
models/cnn.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ models/cnn.py
3
+ CNN pour images RGB 3 canaux — Intel Image Classification (228×228, 6 classes).
4
+
5
+ RÈGLE DE NORMALISATION :
6
+ La normalisation est faite UNIQUEMENT dans utils/prep.py (pipeline de données).
7
+ Les modèles reçoivent des images déjà normalisées — il n'y a PAS de couche
8
+ Rescaling à l'intérieur des modèles. Cela garantit un comportement identique
9
+ entre training, evaluation et production (Flask).
10
+ """
11
+
12
+ # ── PyTorch ───────────────────────────────────────────────────────────────────
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+
16
+
17
+ class CNN_Torch(nn.Module):
18
+ """
19
+ CNN PyTorch 4 blocs pour images RGB (3 canaux, 150×150).
20
+ Entrée : (B, 3, 150, 150) — normalisée ImageNet (mean/std)
21
+ Sortie : (B, num_classes) — logits bruts (CrossEntropyLoss)
22
+
23
+ Architecture :
24
+ Block 1 : Conv(3→32)×2 + BN + ReLU + MaxPool(2) 150→75
25
+ Block 2 : Conv(32→64)×2 + BN + ReLU + MaxPool(2) + Drop2d 75→37
26
+ Block 3 : Conv(64→128)×2 + BN + ReLU + MaxPool(2) + Drop2d 37→18
27
+ Block 4 : Conv(128→256)×2+ BN + ReLU + MaxPool(2) + Drop2d 18→9
28
+ GAP : AdaptiveAvgPool2d(1) →(B,256)
29
+ Head : Linear(256→256) + ReLU + Dropout + Linear(256→C)
30
+ """
31
+ def __init__(self, num_classes: int = 6):
32
+ super().__init__()
33
+
34
+ self.features = nn.Sequential(
35
+ # Block 1 — 150×150 → 75×75
36
+ nn.Conv2d(3, 32, kernel_size=3, padding=1, bias=False),
37
+ nn.BatchNorm2d(32), nn.ReLU(inplace=True),
38
+ nn.Conv2d(32, 32, kernel_size=3, padding=1, bias=False),
39
+ nn.BatchNorm2d(32), nn.ReLU(inplace=True),
40
+ nn.MaxPool2d(2),
41
+
42
+ # Block 2 — 75×75 → 37×37
43
+ nn.Conv2d(32, 64, kernel_size=3, padding=1, bias=False),
44
+ nn.BatchNorm2d(64), nn.ReLU(inplace=True),
45
+ nn.Conv2d(64, 64, kernel_size=3, padding=1, bias=False),
46
+ nn.BatchNorm2d(64), nn.ReLU(inplace=True),
47
+ nn.MaxPool2d(2), nn.Dropout2d(0.10),
48
+
49
+ # Block 3 — 37×37 → 18×18
50
+ nn.Conv2d(64, 128, kernel_size=3, padding=1, bias=False),
51
+ nn.BatchNorm2d(128), nn.ReLU(inplace=True),
52
+ nn.Conv2d(128, 128, kernel_size=3, padding=1, bias=False),
53
+ nn.BatchNorm2d(128), nn.ReLU(inplace=True),
54
+ nn.MaxPool2d(2), nn.Dropout2d(0.15),
55
+
56
+ # Block 4 — 18×18 → 9×9
57
+ nn.Conv2d(128, 256, kernel_size=3, padding=1, bias=False),
58
+ nn.BatchNorm2d(256), nn.ReLU(inplace=True),
59
+ nn.Conv2d(256, 256, kernel_size=3, padding=1, bias=False),
60
+ nn.BatchNorm2d(256), nn.ReLU(inplace=True),
61
+ nn.MaxPool2d(2), nn.Dropout2d(0.20),
62
+ )
63
+
64
+ # (B,256,9,9) → (B,256,1,1) → (B,256)
65
+ self.gap = nn.AdaptiveAvgPool2d(1)
66
+
67
+ self.classifier = nn.Sequential(
68
+ nn.Flatten(),
69
+ nn.Linear(256, 256),
70
+ nn.ReLU(inplace=True),
71
+ nn.Dropout(0.30),
72
+ nn.Linear(256, num_classes),
73
+ )
74
+
75
+ def forward(self, x):
76
+ return self.classifier(self.gap(self.features(x)))
77
+
78
+
79
+ # ── TensorFlow / Keras ────────────────────────────────────────────────────────
80
+ def build_cnn_tf(num_classes: int = 6, input_shape: tuple = (228, 228, 3)):
81
+ """
82
+ CNN TF reproduisant l'architecture du notebook de référence hassanraof.
83
+ Source : https://www.kaggle.com/code/hassanraof/intel-image-classification
84
+
85
+ Entrée : (B, 228, 228, 3) — valeurs [0, 1] normalisées par prep.py
86
+ Sortie : (B, num_classes) — softmax
87
+
88
+ Architecture (5 blocs conv) :
89
+ Block 1 : Conv(32, 5×5) → ReLU → MaxPool(2,2)
90
+ Block 2 : Conv(32, 5×5) → ReLU → MaxPool(2,2)
91
+ Block 3 : Conv(32, 3×3) → ReLU → MaxPool(2,2)
92
+ Block 4 : Conv(64, 3×3) → ReLU → MaxPool(2,2)
93
+ Block 5 : Conv(64, 3×3) → ReLU → MaxPool(2,2)
94
+ Head : Flatten → Dense(1024) → Dropout(0.20)
95
+ → Dense(124) → Dropout(0.20)
96
+ → Dense(num_classes, softmax)
97
+
98
+ ⚠️ PAS de couche Rescaling ici — la normalisation est faite dans prep.py.
99
+ Ajouter Rescaling ici causerait une double normalisation.
100
+ """
101
+ from tensorflow.keras import layers, models
102
+
103
+ return models.Sequential([
104
+ layers.Input(shape=input_shape),
105
+ # ← PAS de Rescaling ici
106
+
107
+ # Block 1
108
+ layers.Conv2D(32, kernel_size=(5, 5), activation="relu"),
109
+ layers.MaxPooling2D(2, 2),
110
+
111
+ # Block 2
112
+ layers.Conv2D(32, kernel_size=(5, 5), activation="relu"),
113
+ layers.MaxPooling2D(2, 2),
114
+
115
+ # Block 3
116
+ layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
117
+ layers.MaxPooling2D(2, 2),
118
+
119
+ # Block 4
120
+ layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
121
+ layers.MaxPooling2D(2, 2),
122
+
123
+ # Block 5
124
+ layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
125
+ layers.MaxPooling2D(2, 2),
126
+
127
+ # Head
128
+ layers.Flatten(),
129
+ layers.Dense(1024, activation="relu"),
130
+ layers.Dropout(0.20),
131
+ layers.Dense(124, activation="relu"),
132
+ layers.Dropout(0.20),
133
+ layers.Dense(num_classes, activation="softmax"),
134
+
135
+ ], name="CNN_TF_hassanraof")
models/train.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ models/train.py
3
+ Classe Trainer pour PyTorch.
4
+ Fonctionnalités : early stopping, ReduceLROnPlateau,
5
+ sauvegarde du meilleur modèle, courbes train/val.
6
+ """
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ from tqdm import tqdm
11
+ import matplotlib.pyplot as plt
12
+
13
+
14
+ class Trainer:
15
+ def __init__(self, model, train_dataloader, test_dataloader,
16
+ lr=1e-3, epochs=30, device="cpu", patience=5):
17
+ self.model = model
18
+ self.train_dataloader = train_dataloader
19
+ self.test_dataloader = test_dataloader
20
+ self.epochs = epochs
21
+ self.patience = patience
22
+ self.device = device
23
+ self.criterion = nn.CrossEntropyLoss()
24
+ self.optimizer = torch.optim.Adam(model.parameters(), lr=lr)
25
+ self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
26
+ self.optimizer, mode="min",
27
+ factor=0.5, patience=3)
28
+
29
+ # ── Entraînement complet ───────────────────────────────────────────────
30
+ def train(self, save_path=None, plot=False):
31
+ self.train_loss, self.train_acc = [], []
32
+ self.val_loss, self.val_acc = [], []
33
+
34
+ best_val_loss = float("inf")
35
+ epochs_no_improve = 0
36
+ best_state = None
37
+
38
+ for epoch in range(self.epochs):
39
+ tr_loss, tr_acc = self._train_one_epoch(epoch)
40
+ v_loss, v_acc = self._validate()
41
+
42
+ self.train_loss.append(tr_loss)
43
+ self.train_acc.append(tr_acc)
44
+ self.val_loss.append(v_loss)
45
+ self.val_acc.append(v_acc)
46
+
47
+ self.scheduler.step(v_loss)
48
+ lr = self.optimizer.param_groups[0]["lr"]
49
+
50
+ print(f"Epoch {epoch+1:02d}/{self.epochs} "
51
+ f"| Train loss={tr_loss:.4f} acc={tr_acc:.2f}% "
52
+ f"| Val loss={v_loss:.4f} acc={v_acc:.2f}% "
53
+ f"| LR={lr:.2e}")
54
+
55
+ # Early stopping
56
+ if v_loss < best_val_loss:
57
+ best_val_loss = v_loss
58
+ epochs_no_improve = 0
59
+ best_state = {k: v.clone() for k, v in self.model.state_dict().items()}
60
+ if save_path:
61
+ torch.save(best_state, save_path)
62
+ print(f" ✓ Best model saved (val_loss={v_loss:.4f})")
63
+ else:
64
+ epochs_no_improve += 1
65
+ print(f" ⚠ No improvement {epochs_no_improve}/{self.patience}")
66
+ if epochs_no_improve >= self.patience:
67
+ print(f"\n⛔ Early stopping at epoch {epoch+1}")
68
+ break
69
+
70
+ if best_state:
71
+ self.model.load_state_dict(best_state)
72
+ if plot:
73
+ self.plot_history()
74
+
75
+ # ── Une epoch de train ─────────────────────────────────────────────────
76
+ def _train_one_epoch(self, epoch):
77
+ self.model.train()
78
+ total_loss, total_correct, total_samples = 0, 0, 0
79
+ pbar = tqdm(self.train_dataloader,
80
+ desc=f"Epoch {epoch+1}/{self.epochs} [train]", leave=False)
81
+
82
+ for imgs, labels in pbar:
83
+ imgs, labels = imgs.to(self.device), labels.to(self.device)
84
+ self.optimizer.zero_grad()
85
+ out = self.model(imgs)
86
+ loss = self.criterion(out, labels)
87
+ loss.backward()
88
+ self.optimizer.step()
89
+
90
+ _, preds = out.max(1)
91
+ correct = (preds == labels).sum().item()
92
+ total = labels.size(0)
93
+ total_correct += correct
94
+ total_samples += total
95
+ total_loss += loss.item()
96
+
97
+ pbar.set_postfix({
98
+ "Batch Acc": f"{100.*correct/total:.1f}%",
99
+ "Avg Acc": f"{100.*total_correct/total_samples:.1f}%",
100
+ "Loss": f"{total_loss/total_samples:.4f}",
101
+ })
102
+
103
+ return total_loss / total_samples, 100. * total_correct / total_samples
104
+
105
+ # ── Validation ────────────────────────────────────────────────────────
106
+ @torch.no_grad()
107
+ def _validate(self):
108
+ self.model.eval()
109
+ total_loss, total_correct, total_samples = 0, 0, 0
110
+ for imgs, labels in self.test_dataloader:
111
+ imgs, labels = imgs.to(self.device), labels.to(self.device)
112
+ out = self.model(imgs)
113
+ loss = self.criterion(out, labels)
114
+ _, preds = out.max(1)
115
+ total_correct += (preds == labels).sum().item()
116
+ total_samples += labels.size(0)
117
+ total_loss += loss.item() * labels.size(0)
118
+ return total_loss / total_samples, 100. * total_correct / total_samples
119
+
120
+ # ── Évaluation finale (public) ────────────────────────────────────────
121
+ @torch.no_grad()
122
+ def evaluate(self):
123
+ loss, acc = self._validate()
124
+ print(f"\nTest Accuracy : {acc:.2f}% | Test Loss : {loss:.4f}")
125
+ return acc, loss
126
+
127
+ # ── Courbes ───────────────────────────────────────────────────────────
128
+ def plot_history(self, save_path="/kaggle/working/history_pytorch.png"):
129
+ epochs = range(1, len(self.train_loss) + 1)
130
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
131
+
132
+ ax1.plot(epochs, self.train_loss, label="Train", color="tab:blue")
133
+ ax1.plot(epochs, self.val_loss, label="Val", color="tab:orange")
134
+ ax1.set_title("Loss"); ax1.set_xlabel("Epoch")
135
+ ax1.legend(); ax1.grid(alpha=.3)
136
+
137
+ ax2.plot(epochs, self.train_acc, label="Train", color="tab:blue")
138
+ ax2.plot(epochs, self.val_acc, label="Val", color="tab:orange")
139
+ ax2.set_title("Accuracy (%)"); ax2.set_xlabel("Epoch")
140
+ ax2.legend(); ax2.grid(alpha=.3)
141
+
142
+ fig.suptitle("Training History — PyTorch", fontsize=13)
143
+ fig.tight_layout()
144
+ plt.savefig(save_path, dpi=120)
145
+ plt.show()
146
+ print(f"✓ Courbes sauvegardées → {save_path}")
utils/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from .prep import (
2
+ get_pytorch_transforms,
3
+ get_pytorch_loaders,
4
+ get_tf_datasets,
5
+ preprocess_image_pytorch,
6
+ preprocess_image_tf,
7
+ )
utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (312 Bytes). View file
 
utils/__pycache__/prep.cpython-312.pyc ADDED
Binary file (5.17 kB). View file
 
utils/prep.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # ══════════════════════════════════════════════════════════════════════════════
3
+ # PYTORCH — Transforms & DataLoaders
4
+ # ══════════════════════════════════════════════════════════════════════════════
5
+ def get_pytorch_transforms(img_size: int = 150):
6
+ """
7
+ Retourne (train_transform, val_transform).
8
+ Augmentation scène-aware pour le dataset Intel (6 classes naturelles RGB).
9
+ """
10
+ from torchvision import transforms
11
+
12
+ # Statistiques ImageNet — optimal pour images naturelles RGB 3 canaux
13
+ MEAN = [0.485, 0.456, 0.406]
14
+ STD = [0.229, 0.224, 0.225]
15
+
16
+ train_transform = transforms.Compose([
17
+ transforms.Resize((img_size, img_size)),
18
+ transforms.RandomHorizontalFlip(p=0.5),
19
+ transforms.RandomVerticalFlip(p=0.1),
20
+ transforms.RandomRotation(degrees=40),
21
+ transforms.ColorJitter(
22
+ brightness=0.3, contrast=0.2, saturation=0.1, hue=0.05
23
+ ),
24
+ transforms.RandomGrayscale(p=0.05),
25
+ transforms.ToTensor(),
26
+ transforms.Normalize(MEAN, STD), # ← normalisation ici, pas dans le modèle
27
+ transforms.RandomErasing(p=0.15, scale=(0.02, 0.15)),
28
+ ])
29
+
30
+ val_transform = transforms.Compose([
31
+ transforms.Resize((img_size, img_size)),
32
+ transforms.ToTensor(),
33
+ transforms.Normalize(MEAN, STD), # ← même normalisation en val/test
34
+ ])
35
+
36
+ return train_transform, val_transform
37
+
38
+
39
+ def get_pytorch_loaders(
40
+ train_dir: str,
41
+ test_dir: str,
42
+ img_size: int = 150,
43
+ batch_size: int = 64,
44
+ ):
45
+ from torch.utils.data import DataLoader
46
+ from torchvision import datasets
47
+
48
+ train_tf, val_tf = get_pytorch_transforms(img_size)
49
+
50
+ train_loader = DataLoader(
51
+ datasets.ImageFolder(train_dir, transform=train_tf),
52
+ batch_size=batch_size, shuffle=True,
53
+ num_workers=2, pin_memory=True,
54
+ )
55
+ test_loader = DataLoader(
56
+ datasets.ImageFolder(test_dir, transform=val_tf),
57
+ batch_size=batch_size, shuffle=False,
58
+ num_workers=2, pin_memory=True,
59
+ )
60
+ return train_loader, test_loader
61
+
62
+
63
+ # ══════════════════════════════════════════════════════════════════════════════
64
+ # TENSORFLOW — Dataset pipeline
65
+ # ══════════════════════════════════════════════════════════════════════════════
66
+ def get_tf_datasets(
67
+ train_dir: str,
68
+ test_dir: str,
69
+ img_size: int = 228,
70
+ batch_size: int = 64,
71
+ ):
72
+ import tensorflow as tf
73
+
74
+ # Same preprocessing as in the notebook
75
+ norm_layer = tf.keras.layers.Rescaling(1.0 / 255.0)
76
+
77
+ # ── Raw loading ──────────────────────────────────────────────────────────
78
+ train_ds = tf.keras.utils.image_dataset_from_directory(
79
+ train_dir,
80
+ seed=123,
81
+ image_size=(img_size, img_size),
82
+ batch_size=batch_size,
83
+ shuffle=True,
84
+ label_mode="int",
85
+ )
86
+
87
+ test_ds = tf.keras.utils.image_dataset_from_directory(
88
+ test_dir,
89
+ seed=123,
90
+ image_size=(img_size, img_size),
91
+ batch_size=batch_size,
92
+ shuffle=False,
93
+ label_mode="int",
94
+ )
95
+
96
+ # ── Normalization only ───────────────────────────────────────────────────
97
+ train_ds = train_ds.map(
98
+ lambda x, y: (norm_layer(x), y),
99
+ num_parallel_calls=tf.data.AUTOTUNE
100
+ )
101
+
102
+ test_ds = test_ds.map(
103
+ lambda x, y: (norm_layer(x), y),
104
+ num_parallel_calls=tf.data.AUTOTUNE
105
+ )
106
+
107
+ # ── Performance ──────────────────────────────────────────────────────────
108
+ train_ds = train_ds.prefetch(tf.data.AUTOTUNE)
109
+ test_ds = test_ds.prefetch(tf.data.AUTOTUNE)
110
+
111
+ return train_ds, test_ds
112
+
113
+ # ══════════════════════════════════════════════════════════════════════════════
114
+ # INFÉRENCE — Preprocessing image unique (Flask / production)
115
+ # ══════════════════════════════════════════════════════════════════════════════
116
+ def preprocess_image_pytorch(pil_img, img_size: int = 150):
117
+ """Prépare une image PIL pour l'inférence PyTorch. Retourne (1,3,H,W)."""
118
+ import torch
119
+ from torchvision import transforms
120
+
121
+ tf = transforms.Compose([
122
+ transforms.Resize((img_size, img_size)),
123
+ transforms.ToTensor(),
124
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
125
+ ])
126
+ return tf(pil_img).unsqueeze(0)
127
+
128
+
129
+ def preprocess_image_tf(pil_img, img_size: int = 150):
130
+ """
131
+ Prépare une image PIL pour l'inférence TensorFlow. Retourne (1,H,W,3).
132
+ Normalisation identique au pipeline val/test : ÷255 → [0,1].
133
+ """
134
+ import numpy as np
135
+ arr = np.array(pil_img.resize((img_size, img_size)), dtype=np.float32)
136
+ arr = arr / 255.0 # ← même normalisation que normalize_only()
137
+ return np.expand_dims(arr, 0) # (1, H, W, 3)