Livewirez commited on
Commit
63abc18
Β·
verified Β·
1 Parent(s): e3a2ddf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +913 -12
app.py CHANGED
@@ -1,15 +1,916 @@
 
1
  import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
 
4
- def sepia(input_img):
5
- sepia_filter = np.array([
6
- [0.393, 0.769, 0.189],
7
- [0.349, 0.686, 0.168],
8
- [0.272, 0.534, 0.131]
9
- ])
10
- sepia_img = input_img.dot(sepia_filter.T)
11
- sepia_img /= sepia_img.max()
12
- return sepia_img
13
-
14
- demo = gr.Interface(sepia, gr.Image(), "image", api_name="predict")
15
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, re, random, json
2
  import numpy as np
3
+ import pandas as pd
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from torch.utils.data import Dataset, DataLoader
8
+
9
+ from torch.optim import Optimizer, Adam, AdamW, SGD
10
+ from transformers import AutoTokenizer, AutoModel, get_linear_schedule_with_warmup
11
+ from datasets import load_dataset
12
+ from sklearn.feature_extraction.text import TfidfVectorizer
13
+ from sklearn.metrics import classification_report, confusion_matrix, f1_score
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib.patches as mpatches
16
+ import seaborn as sns
17
+ import timm
18
+ import torchvision
19
+ import torchvision.transforms as transforms
20
+ from torchvision.datasets import ImageFolder
21
+ from collections import Counter
22
+ from torchvision.transforms import ToTensor
23
+ from torchmetrics import MeanMetric, Accuracy
24
+ from torchmetrics import ConfusionMatrix, Accuracy, Precision, Recall, F1Score
25
+
26
+ from sklearn.model_selection import train_test_split
27
+ from pathlib import Path
28
+
29
+ from tqdm import tqdm
30
+ from typing import Tuple, Union, Callable
31
+
32
  import gradio as gr
33
 
34
+ from PIL import Image
35
+ # Ensure dataset outputs images to the same size
36
+ # because the model expects the input to be consistent
37
+ from torchvision.transforms import v2
38
+ import random
39
+ from glob import glob
40
+
41
+
42
+ class CNN32(nn.Module):
43
+ """
44
+ 32-layer CNN - 4 blocks of 8 conv layers each.
45
+ Channels: Block1 = 32 Block2 = 64 Block3 = 128 Block4 = 256
46
+ Spatial: 224 -> 112 -> 56 -> 28 -> 14 -> AdaptiveAvgPool(1)
47
+
48
+ BatchNorm added after every conv essential bcause
49
+ gradients vanish through 32 layers and the network
50
+ fails to train (this is exactly why ResNet was invented).
51
+
52
+ https://d2l.ai/chapter_convolutional-modern/batch-norm.html
53
+ https://docs.pytorch.org/docs/2.12/generated/torch.nn.BatchNorm2d.html
54
+ https://arxiv.org/pdf/1502.03167
55
+ """
56
+
57
+ @property
58
+ def has_backbone(): return False
59
+
60
+ def __init__(self, num_classes=8):
61
+ super().__init__()
62
+
63
+ self.features = nn.Sequential(
64
+ # Block 1: 8 convolution & pooling layers, 32 channels
65
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
66
+ nn.BatchNorm2d(32),
67
+ nn.ReLU(),
68
+
69
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
70
+ nn.BatchNorm2d(32),
71
+ nn.ReLU(),
72
+
73
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
74
+ nn.BatchNorm2d(32),
75
+ nn.ReLU(),
76
+
77
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
78
+ nn.BatchNorm2d(32),
79
+ nn.ReLU(),
80
+
81
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
82
+ nn.BatchNorm2d(32),
83
+ nn.ReLU(),
84
+
85
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
86
+ nn.BatchNorm2d(32),
87
+ nn.ReLU(),
88
+
89
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
90
+ nn.BatchNorm2d(32),
91
+ nn.ReLU(),
92
+
93
+ nn.LazyConv2d(32, kernel_size=3, stride=1, padding=1),
94
+ nn.BatchNorm2d(32),
95
+ nn.ReLU(),
96
+
97
+ nn.MaxPool2d(kernel_size=2), # from 224 to 112
98
+ nn.Dropout2d(0.1),
99
+
100
+ # Block 1: 8 convolution & pooling layers, 64 channels
101
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
102
+ nn.BatchNorm2d(64),
103
+ nn.ReLU(),
104
+
105
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
106
+ nn.BatchNorm2d(64),
107
+ nn.ReLU(),
108
+
109
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
110
+ nn.BatchNorm2d(64),
111
+ nn.ReLU(),
112
+
113
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
114
+ nn.BatchNorm2d(64),
115
+ nn.ReLU(),
116
+
117
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
118
+ nn.BatchNorm2d(64),
119
+ nn.ReLU(),
120
+
121
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
122
+ nn.BatchNorm2d(64),
123
+ nn.ReLU(),
124
+
125
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
126
+ nn.BatchNorm2d(64),
127
+ nn.ReLU(),
128
+
129
+ nn.LazyConv2d(64, kernel_size=3, stride=1, padding=1),
130
+ nn.BatchNorm2d(64),
131
+ nn.ReLU(),
132
+
133
+ nn.MaxPool2d(kernel_size=2), # from 112 to 56
134
+ nn.Dropout2d(0.2),
135
+
136
+ # Block 3: 8 convolution & pooling layers, 128 channels
137
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
138
+ nn.BatchNorm2d(128),
139
+ nn.ReLU(),
140
+
141
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
142
+ nn.BatchNorm2d(128),
143
+ nn.ReLU(),
144
+
145
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
146
+ nn.BatchNorm2d(128),
147
+ nn.ReLU(),
148
+
149
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
150
+ nn.BatchNorm2d(128),
151
+ nn.ReLU(),
152
+
153
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
154
+ nn.BatchNorm2d(128),
155
+ nn.ReLU(),
156
+
157
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
158
+ nn.BatchNorm2d(128),
159
+ nn.ReLU(),
160
+
161
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
162
+ nn.BatchNorm2d(128),
163
+ nn.ReLU(),
164
+
165
+ nn.LazyConv2d(128, kernel_size=3, stride=1, padding=1),
166
+ nn.BatchNorm2d(128),
167
+ nn.ReLU(),
168
+
169
+ nn.MaxPool2d(kernel_size=2), # from 56 to 28
170
+ nn.Dropout2d(0.2),
171
+
172
+ # Block 4: 8 convolution & pooling layers, 256 channels
173
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
174
+ nn.BatchNorm2d(256),
175
+ nn.ReLU(),
176
+
177
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
178
+ nn.BatchNorm2d(256),
179
+ nn.ReLU(),
180
+
181
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
182
+ nn.BatchNorm2d(256),
183
+ nn.ReLU(),
184
+
185
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
186
+ nn.BatchNorm2d(256),
187
+ nn.ReLU(),
188
+
189
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
190
+ nn.BatchNorm2d(256),
191
+ nn.ReLU(),
192
+
193
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
194
+ nn.BatchNorm2d(256),
195
+ nn.ReLU(),
196
+
197
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
198
+ nn.BatchNorm2d(256),
199
+ nn.ReLU(),
200
+
201
+ nn.LazyConv2d(256, kernel_size=3, stride=1, padding=1),
202
+ nn.BatchNorm2d(256),
203
+ nn.ReLU(),
204
+
205
+ nn.MaxPool2d(kernel_size=2), # from 28 to 14
206
+ nn.Dropout2d(0.3),
207
+ )
208
+
209
+ # Collapses 14 x 14 -> 1 x 1 regardless of input size
210
+ # https://discuss.pytorch.org/t/what-is-adaptiveavgpool2d/26897
211
+ # https://medium.com/@caring_smitten_gerbil_914/demystifying-nn-adaptiveavgpool2d-in-pytorch-why-adaptive-pooling-matters-in-deep-learning-1f7b7b1cc9b0
212
+ # https://docs.pytorch.org/docs/main/generated/torch.nn.modules.pooling.AdaptiveAvgPool2d.html
213
+ self.pool = nn.AdaptiveAvgPool2d(1)
214
+
215
+ # Fully Connected Layer head: wider than CNN9 to match 256 input channels
216
+ self.classifier = nn.Sequential(
217
+ nn.Flatten(),
218
+ nn.LazyLinear(1024),
219
+ nn.ReLU(),
220
+ nn.Dropout(0.5),
221
+ nn.LazyLinear(512),
222
+ nn.ReLU(),
223
+ nn.Dropout(0.5),
224
+ nn.LazyLinear(256),
225
+ nn.ReLU(),
226
+ nn.LazyLinear(num_classes), # no Dropout on output
227
+ )
228
+
229
+ def forward(self, x):
230
+ x = self.features(x)
231
+ x = self.pool(x)
232
+ return self.classifier(x)
233
+
234
+
235
+
236
+
237
+ class ResNet50Modified(nn.Module):
238
+ """
239
+ https://pytorch.org/hub/nvidia_deeplearningexamples_resnet50/
240
+ https://docs.pytorch.org/vision/main/models/generated/torchvision.models.resnet50.html
241
+ https://medium.com/@deepvisionkararhaider/resnet-50-explained-step-by-step-the-easiest-guide-to-deep-residual-networks-7616f4f45046
242
+ https://arxiv.org/pdf/1512.03385
243
+ """
244
+ @property
245
+ def has_backbone(): return True
246
+
247
+ def __init__(self, num_classes=8):
248
+ super().__init__()
249
+ self.backbone = torchvision.models.resnet50(weights="IMAGENET1K_V2", progress=True)
250
+ in_features = self.backbone.fc.in_features # 2048 the standard feature embedding vector size
251
+
252
+ # replace the original fc layer (a Linear(2048, 1000) trained on ImageNet's 1000 classes) with a passthrough.
253
+ # The backbone now outputs the raw 2048-dimensional feature vector instead of 1000 class logits.
254
+ self.backbone.fc = nn.Identity()
255
+
256
+ # Fully conncted layer head outside self.backbone -> stays trainable during phase 1 freeze
257
+ self.classifier = nn.Sequential(
258
+ nn.Dropout(p=0.4),
259
+ nn.Linear(in_features, num_classes),
260
+ )
261
+
262
+ def forward(self, x):
263
+ return self.classifier(self.backbone(x))
264
+
265
+
266
+
267
+
268
+ class ResNet18Modified(nn.Module):
269
+ """
270
+ https://pytorch.org/hub/nvidia_deeplearningexamples_resnet50/
271
+ https://docs.pytorch.org/vision/main/models/generated/torchvision.models.resnet50.html
272
+ https://medium.com/@deepvisionkararhaider/resnet-50-explained-step-by-step-the-easiest-guide-to-deep-residual-networks-7616f4f45046
273
+ https://arxiv.org/pdf/1512.03385
274
+ """
275
+
276
+ @property
277
+ def has_backbone(): return True
278
+
279
+ def __init__(self, num_classes=8):
280
+ super().__init__()
281
+ self.backbone = torchvision.models.resnet18(weights="IMAGENET1K_V1", progress=True)
282
+ in_features = self.backbone.fc.in_features # 2048 the standard feature embedding vector size
283
+
284
+ # replace the original fc layer (a Linear(2048, 1000) trained on ImageNet's 1000 classes) with a passthrough.
285
+ # The backbone now outputs the raw 2048-dimensional feature vector instead of 1000 class logits.
286
+ self.backbone.fc = nn.Identity()
287
+
288
+ # Fully conncted layer head outside self.backbone -> stays trainable during phase 1 freeze
289
+ self.classifier = nn.Sequential(
290
+ nn.Dropout(p=0.4),
291
+ nn.Linear(in_features, num_classes),
292
+ )
293
+
294
+ def forward(self, x):
295
+ return self.classifier(self.backbone(x))
296
+
297
+
298
+
299
+ class DenseNet201Modified(nn.Module):
300
+ """
301
+ Densenet201: https://docs.pytorch.org/vision/main/models/generated/torchvision.models.densenet201.html#torchvision.models.densenet201
302
+ https://medium.com/@karuneshu21/implement-densenet-in-pytorch-46374ef91900
303
+ https://docs.pytorch.org/vision/main/models/densenet.html
304
+ Densely Connected Convolutional Networks: https://arxiv.org/abs/1608.06993
305
+ """
306
+
307
+ @property
308
+ def has_backbone(): return True
309
+
310
+ def __init__(self, num_classes=8):
311
+ super().__init__()
312
+ self.backbone = torchvision.models.densenet201(
313
+ weights="IMAGENET1K_V1", progress=True)
314
+ in_features = self.backbone.classifier.in_features
315
+ self.backbone.classifier = nn.Identity()
316
+ self.classifier = nn.Sequential(
317
+ nn.Dropout(p=0.4),
318
+ nn.Linear(in_features, num_classes),
319
+ )
320
+ def forward(self, x):
321
+ return self.classifier(self.backbone(x))
322
+
323
+
324
+
325
+
326
+ class XceptionModified(nn.Module):
327
+ """
328
+
329
+ https://huggingface.co/docs/timm/en/models/xception
330
+ You can finetune any of the pre-trained models just by changing the classifier (the last layer).
331
+ """
332
+
333
+ @property
334
+ def has_backbone(): return True
335
+
336
+ def __init__(self, num_classes=8):
337
+ super().__init__()
338
+ self.backbone = timm.create_model(
339
+ "xception", pretrained=True, num_classes=0)
340
+ in_features = self.backbone.num_features
341
+ self.classifier = nn.Sequential(
342
+ nn.Dropout(p=0.4),
343
+ nn.Linear(in_features, num_classes),
344
+ )
345
+ def forward(self, x):
346
+ return self.classifier(self.backbone(x))
347
+
348
+
349
+
350
+ class InceptionV3Modified(nn.Module):
351
+ """
352
+ Special case: requires 299Γ—299 input
353
+ https://arxiv.org/abs/1512.00567
354
+ https://docs.pytorch.org/vision/main/models/generated/torchvision.models.inception_v3.html
355
+ """
356
+
357
+ @property
358
+ def has_backbone(): return True
359
+
360
+ def __init__(self, num_classes=8):
361
+ super().__init__()
362
+ self.backbone = torchvision.models.inception_v3(
363
+ weights="IMAGENET1K_V1", progress=True,
364
+ aux_logits=True)
365
+ self.backbone.aux_logits = False # disable after loading -> forward returns plain tensor
366
+ self.backbone.AuxLogits = None # free the auxiliary classifier modules
367
+
368
+ in_features = self.backbone.fc.in_features # 2048 channels
369
+ self.backbone.fc = nn.Identity()
370
+ self.classifier = nn.Sequential(
371
+ nn.Dropout(p=0.4),
372
+ nn.Linear(in_features, num_classes),
373
+ )
374
+ def forward(self, x):
375
+ return self.classifier(self.backbone(x))
376
+
377
+
378
+
379
+
380
+ class AlexNetModified(nn.Module):
381
+ """
382
+ AlexNet (Krizhevsky et al. 2012) - historical baseline:
383
+ https://www.researchgate.net/publication/319770183_Imagenet_classification_with_deep_convolutional_neural_networks.
384
+ https://proceedings.neurips.cc/paper_files/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf
385
+ First deep CNN to win ImageNet. Shows progression from early architectures.
386
+ weights="IMAGENET1K_V1"
387
+ https://medium.com/@shivsingh483/understanding-alexnet-the-2012-breakthrough-that-changed-ai-forever-7c365cf76969
388
+ https://docs.pytorch.org/vision/main/models/generated/torchvision.models.alexnet.html
389
+ """
390
+ @property
391
+ def has_backbone(): return True
392
+
393
+ def __init__(self, num_classes=8):
394
+ super().__init__()
395
+ backbone = torchvision.models.alexnet(weights="IMAGENET1K_V1")
396
+
397
+ # Remove the final linear classifier
398
+ self.backbone = nn.Sequential(backbone.features, backbone.avgpool, nn.Flatten(),
399
+ *list(backbone.classifier.children())[:-1]) # igore the last layer
400
+ in_features = 4096
401
+
402
+ self.classifier = nn.Sequential(
403
+ nn.Dropout(p=0.4),
404
+ nn.Linear(in_features, num_classes),
405
+ )
406
+
407
+ def forward(self, x):
408
+ return self.classifier(self.backbone(x))
409
+
410
+
411
+
412
+
413
+ class GoogLeNetModified(nn.Module):
414
+ """
415
+ GoogLeNet
416
+
417
+ use weights="IMAGENET1K_V1" and aux_logits=False for simplicity.
418
+ https://www.researchgate.net/publication/316215961_KVASIR_A_Multi-Class_Image_Dataset_for_Computer_Aided_Gastrointestinal_Disease_Detection
419
+ https://doras.dcu.ie/21821/1/Pogorelov_et_al._2017.pdf
420
+ https://arxiv.org/pdf/1409.4842
421
+ https://pytorch.org/hub/pytorch_vision_googlenet/
422
+ https://pytorch.org/hub/pytorch_vision_googlenet/
423
+ https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://medium.com/%40siddheshb008/googlenet-a-deep-dive-into-googles-neural-network-technology-f588d1b49e55&ved=2ahUKEwjdirymi76UAxV0VEEAHR-aGQQQFnoECCUQAQ&usg=AOvVaw1Bij1bxrw7vGia5oJQhiZh
424
+ https://www.cs.unc.edu/~wliu/papers/GoogLeNet.pdf
425
+
426
+ # add final nn.Linear classifier layer
427
+ """
428
+
429
+ @property
430
+ def has_backbone(): return True
431
+
432
+ def __init__(self, num_classes=8):
433
+ super().__init__()
434
+ self.backbone = torchvision.models.googlenet(
435
+ weights="IMAGENET1K_V1",
436
+ aux_logits=True, # required by torchvision when loading weights
437
+ )
438
+ self.backbone.aux_logits = False # disable after loading -> forward returns plain tensor
439
+ self.backbone.aux1 = None # free the auxiliary classifier modules
440
+ self.backbone.aux2 = None
441
+ self.backbone.AuxLogits = None
442
+
443
+ in_features = self.backbone.fc.in_features
444
+ self.backbone.fc = nn.Identity() # strip head from backbone
445
+
446
+ self.classifier = nn.Sequential(
447
+ nn.Dropout(0.4),
448
+ nn.Linear(in_features, num_classes),
449
+ )
450
+
451
+ def forward(self, x):
452
+ return self.classifier(self.backbone(x))
453
+
454
+
455
+
456
+ class VGG16Modified(nn.Module):
457
+ """
458
+ VGG16 (Simonyan & Zisserman 2014).
459
+ https://arxiv.org/abs/1409.1556
460
+ https://arxiv.org/pdf/1409.1556
461
+ https://www.robots.ox.ac.uk/~vgg/research/very_deep/
462
+
463
+ You can finetune any of the pre-trained models just by changing the classifier (the last layer).
464
+ # num_classes=0 -> remove classifier nn.Linear
465
+ """
466
+
467
+ @property
468
+ def has_backbone(): return True
469
+
470
+ def __init__(self, num_classes=8):
471
+ super().__init__()
472
+ self.backbone = torchvision.models.vgg16(weights="IMAGENET1K_V1")
473
+
474
+ # Remove last linear classifier
475
+ in_features = self.backbone.classifier[6].in_features # 4096
476
+ self.backbone.classifier[6] = nn.Identity() # strip original classifier head
477
+
478
+
479
+ self.classifier = nn.Sequential(
480
+ nn.Dropout(0.4),
481
+ nn.Linear(in_features, num_classes),
482
+ )
483
+
484
+ def forward(self, x):
485
+ return self.classifier(self.backbone(x))
486
+
487
+
488
+
489
+
490
+ class EfficientNetB0Modified(nn.Module):
491
+ """
492
+ EfficientNet-B0 (Tan & Le 2019)
493
+
494
+ https://proceedings.mlr.press/v97/tan19a.html
495
+ https://arxiv.org/pdf/1905.11946
496
+ # num_classes=0 -> remove classifier nn.Linear
497
+ """
498
+
499
+ @property
500
+ def has_backbone(): return True
501
+
502
+ def __init__(self, num_classes=8):
503
+ super().__init__()
504
+ self.backbone = timm.create_model("efficientnet_b0", pretrained=True, num_classes=0)
505
+ self.classifier = nn.Sequential(
506
+ nn.Dropout(0.4),
507
+ nn.Linear(self.backbone.num_features, num_classes),
508
+ )
509
+
510
+ def forward(self, x):
511
+ return self.classifier(self.backbone(x))
512
+
513
+
514
+ class SEBlock(nn.Module):
515
+ """
516
+ Squeeze-and-Excitation block.
517
+
518
+ https://www.emergentmind.com/topics/squeeze-and-excitation-se-mechanism
519
+ https://www.digitalocean.com/community/tutorials/channel-attention-squeeze-and-excitation-networks
520
+ https://arxiv.org/pdf/1709.01507
521
+
522
+ Learns WHICH feature channels matter most for each endoscopic finding.
523
+ For Example: colour channels matter more for esophagitis, texture for polyps.
524
+ """
525
+ def __init__(self, channels, reduction=16):
526
+ super().__init__()
527
+ self.pool = nn.AdaptiveAvgPool2d(1)
528
+ self.excite = nn.Sequential(
529
+ nn.Flatten(),
530
+ nn.Linear(channels, channels // reduction, bias=False),
531
+ nn.ReLU(),
532
+ nn.Linear(channels // reduction, channels, bias=False),
533
+ nn.Sigmoid(),
534
+ )
535
+
536
+ def forward(self, x):
537
+ s = self.excite(self.pool(x)).unsqueeze(-1).unsqueeze(-1)
538
+ return x * s
539
+
540
+ class EfficientNetB0_SE_Modified(nn.Module):
541
+ """
542
+ EfficientNet-B0 with custom SE attention pooling.
543
+
544
+ Modification over baseline EfficientNet-B0:
545
+ - After the backbone's final feature maps, apply an SE block
546
+ that recalibrates channel importance before classification.
547
+
548
+ https://arxiv.org/pdf/1905.11946
549
+ https://medium.com/codex/a-summary-of-efficientnet-rethinking-model-scaling-for-cnns-d524d37ff8bb
550
+
551
+ # num_classes=0 -> remove classifier nn.Linear
552
+ """
553
+
554
+ @property
555
+ def has_backbone(): return True
556
+
557
+ def __init__(self, num_classes=8, reduction=16):
558
+ super().__init__()
559
+ self.backbone = timm.create_model(
560
+ "efficientnet_b0",
561
+ pretrained=True,
562
+ num_classes=0,
563
+ global_pool=""
564
+ )
565
+ in_features = self.backbone.num_features # 1280
566
+ self.se_block = SEBlock(in_features, reduction=reduction)
567
+ self.pool = nn.AdaptiveAvgPool2d(1) # [B, 1280, H, W] -> [B, 1280, 1, 1]
568
+
569
+ self.classifier = nn.Sequential(
570
+ nn.Flatten(), # [B, 1280, 1, 1] -> [B, 1280]
571
+ nn.Dropout(0.4),
572
+ nn.Linear(in_features, num_classes),
573
+ )
574
+
575
+ def forward(self, x):
576
+ x = self.backbone(x) # [B, 1280, H, W]
577
+ x = self.se_block(x) # [B, 1280, H, W]
578
+ x = self.pool(x) # [B, 1280, 1, 1]
579
+ return self.classifier(x) # Flatten inside classifier -> [B, 8]
580
+
581
+
582
+
583
+
584
+ # must have __init__ and forward
585
+ class MaxVitTinyTfModified(nn.Module):
586
+ """
587
+ https://huggingface.co/timm/maxvit_tiny_tf_224.in1k
588
+ You can finetune any of the pre-trained models just by changing the classifier (the last layer).
589
+ """
590
+
591
+ @property
592
+ def has_backbone(): return True
593
+
594
+ def __init__(self, num_classes=8): # define different parts of the model
595
+ super().__init__()
596
+ self.backbone = timm.create_model(
597
+ "maxvit_tiny_tf_224", pretrained=True,
598
+ num_classes=0 # remove classifier nn.Linear
599
+ )
600
+ out_size = self.backbone.num_features
601
+ self.classifier = nn.Sequential(
602
+ nn.Dropout(p=0.4),
603
+ nn.Linear(out_size, num_classes)
604
+ )
605
+
606
+ # take example or batch of examples and connect the parts
607
+ # defind in init and return the output
608
+ def forward(self, x):
609
+ x = self.backbone(x) # returns pooled features, no head
610
+ return self.classifier(x)
611
+
612
+
613
+
614
+
615
+ from dataclasses import dataclass, field
616
+ import torch
617
+
618
+ @dataclass(frozen=True)
619
+ class EvaluationResult:
620
+ confusion_matrix: ConfusionMatrix
621
+ accuracy: float
622
+ precision: float
623
+ recall: float
624
+ f1_score: float
625
+ all_preds: list = field(default_factory=list)
626
+ all_labels: list = field(default_factory=list)
627
+
628
+
629
+
630
+ # Model List
631
+ MODEL_REGISTRY_ADAM = {
632
+ #"SimpleCNN_Adam": (SimpleCNN(NUM_CLASSES), False), # no base model
633
+ #"CNN3_Adam": (CNN3(NUM_CLASSES), False), # no base model
634
+ #"CNN6_Adam": (CNN6(NUM_CLASSES), False), # no base model
635
+ #"CNN9_Adam": (CNN9(NUM_CLASSES), False), # no base model
636
+ 'CNN32_Adam': (CNN32(NUM_CLASSES), False),
637
+ "Resnet50_Adam": (ResNet50Modified(NUM_CLASSES), True), # My Method 1
638
+ "Resnet18_Adam": (ResNet18Modified(NUM_CLASSES), True),
639
+ #"AlexNet_Adam": (AlexNetModified(NUM_CLASSES), True),
640
+ #"GoogLeNet_Adam":(GoogLeNetModified(NUM_CLASSES), True),
641
+ #"VGG16_Adam": (VGG16Modified(NUM_CLASSES), True),
642
+ #"EfficientNet-B0_Adam": (EfficientNetB0Modified(NUM_CLASSES), True),
643
+ "EfficientNet-B0-SE_Adam":(EfficientNetB0_SE_Modified(NUM_CLASSES), True), # My Method 2
644
+ #"MaxVitTinyTf_Adam":(MaxVitTinyTfModified(NUM_CLASSES), True),
645
+ #'Xception_Adam': (XceptionModified(NUM_CLASSES), True),
646
+ }
647
+
648
+ special_models_adam = {
649
+ 'InceptionV3_Adam': (InceptionV3Modified(NUM_CLASSES), True)
650
+ }
651
+
652
+ # Model List
653
+ MODEL_REGISTRY_SGD = {
654
+ #"SimpleCNN_SGD": (SimpleCNN(NUM_CLASSES), False), # no base model
655
+ #"CNN3_SGD": (CNN3(NUM_CLASSES), False), # no base model
656
+ #"CNN6_SGD": (CNN6(NUM_CLASSES), False), # no base model
657
+ #"CNN9_SGD": (CNN9(NUM_CLASSES), False), # no base model
658
+ 'CNN32_SGD': (CNN32(NUM_CLASSES), False),
659
+ "Resnet50_SGD": (ResNet50Modified(NUM_CLASSES), True), # My Method 1
660
+ "Resnet18_SGD": (ResNet18Modified(NUM_CLASSES), True),
661
+ #"AlexNet_SGD": (AlexNetModified(NUM_CLASSES), True),
662
+ #"GoogLeNet_SGD":(GoogLeNetModified(NUM_CLASSES), True),
663
+ #"VGG16_SGD": (VGG16Modified(NUM_CLASSES), True),
664
+ #"EfficientNet-B0_SGD": (EfficientNetB0Modified(NUM_CLASSES), True),
665
+ "EfficientNet-B0-SE_SGD":(EfficientNetB0_SE_Modified(NUM_CLASSES), True), # My Method 2
666
+ #"MaxVitTinyTf_SGD":(MaxVitTinyTfModified(NUM_CLASSES), True),
667
+ #'Xception_SGD': (XceptionModified(NUM_CLASSES), True),
668
+ }
669
+
670
+ special_models_sgd = {
671
+ 'InceptionV3_SGD': (InceptionV3Modified(NUM_CLASSES), True)
672
+ }
673
+
674
+
675
+
676
+ MODEL_REGISTRY_ADAM_W = {
677
+ #"SimpleCNN_AdamW": (SimpleCNN(NUM_CLASSES), False), # no base model
678
+ #"CNN3_AdamW": (CNN3(NUM_CLASSES), False), # no base model
679
+ #"CNN6_AdamW": (CNN6(NUM_CLASSES), False), # no base model
680
+ #"CNN9_AdamW": (CNN9(NUM_CLASSES), False), # no base model
681
+ 'CNN32_AdamW': (CNN32(NUM_CLASSES), False),
682
+ "Resnet50_AdamW": (ResNet50Modified(NUM_CLASSES), True), # My Method 1
683
+ "Resnet18_AdamW": (ResNet18Modified(NUM_CLASSES), True),
684
+ #"AlexNet_AdamW": (AlexNetModified(NUM_CLASSES), True),
685
+ #"GoogLeNet_AdamW":(GoogLeNetModified(NUM_CLASSES), True),
686
+ #"VGG16_AdamW": (VGG16Modified(NUM_CLASSES), True),
687
+ #"EfficientNet-B0_AdamW": (EfficientNetB0Modified(NUM_CLASSES), True),
688
+ "EfficientNet-B0-SE_AdamW":(EfficientNetB0_SE_Modified(NUM_CLASSES), True), # My Method 2
689
+ #"MaxVitTinyTf_AdamW":(MaxVitTinyTfModified(NUM_CLASSES), True),
690
+ #'Xception_AdamW': (XceptionModified(NUM_CLASSES), True),
691
+ }
692
+
693
+ special_models_adam_w = {
694
+ 'InceptionV3_AdamW': (InceptionV3Modified(NUM_CLASSES), True)
695
+ }
696
+
697
+
698
+ best_optimizer = 'AdamW'
699
+
700
+
701
+
702
+ LOADED_MODELS = {}
703
+
704
+ if best_optimizer == "SGD":
705
+ for model_name, (model, _) in {**MODEL_REGISTRY_SGD, **special_models_sgd}.items():
706
+ ckpt_path = f"best_{model_name.replace(' ','_')}.pth"
707
+ try:
708
+ model.load_state_dict(
709
+ torch.load(ckpt_path, map_location=DEVICE))
710
+ model.to(DEVICE)
711
+ model.eval()
712
+ LOADED_MODELS[model_name] = model
713
+ print(f" βœ” Loaded: {model_name}")
714
+ except FileNotFoundError:
715
+ print(f"(<-->) Skipped: {model_name} β€” checkpoint not found ({ckpt_path})")
716
+ elif best_optimizer == "AdamW":
717
+ for model_name, (model, _) in {**MODEL_REGISTRY_ADAM_W, **special_models_adam_w}.items():
718
+ ckpt_path = f"best_{model_name.replace(' ','_')}.pth"
719
+ try:
720
+ model.load_state_dict(
721
+ torch.load(ckpt_path, map_location=DEVICE))
722
+ model.to(DEVICE)
723
+ model.eval()
724
+ LOADED_MODELS[model_name] = model
725
+ print(f" βœ” Loaded: {model_name}")
726
+ except FileNotFoundError:
727
+ print(f"(<-->) Skipped: {model_name} β€” checkpoint not found ({ckpt_path})")
728
+ elif best_optimizer == "Adam":
729
+ for model_name, (model, _) in {**MODEL_REGISTRY_ADAM, **special_models_adam}.items():
730
+ ckpt_path = f"best_{model_name.replace(' ','_')}.pth"
731
+ try:
732
+ model.load_state_dict(
733
+ torch.load(ckpt_path, map_location=DEVICE))
734
+ model.to(DEVICE)
735
+ model.eval()
736
+ LOADED_MODELS[model_name] = model
737
+ print(f" βœ” Loaded: {model_name}")
738
+ except FileNotFoundError:
739
+ print(f"(<-->) Skipped: {model_name} β€” checkpoint not found ({ckpt_path})")
740
+
741
+ print(f"\nAvailable models: {list(LOADED_MODELS.keys())}")
742
+
743
+
744
+ inference_transform = v2.Compose([
745
+ v2.Resize((224, 224)),
746
+ v2.ToImage(),
747
+ v2.ToDtype(torch.float32, scale=True),
748
+ v2.Normalize(mean=[0.485, 0.456, 0.406],
749
+ std=[0.229, 0.224, 0.225]),
750
+ ])
751
+
752
+
753
+ inception_transform = v2.Compose([
754
+ v2.Resize((299, 299)),
755
+ v2.ToImage(),
756
+ v2.ToDtype(torch.float32, scale=True),
757
+ v2.Normalize(mean=[0.485, 0.456, 0.406],
758
+ std=[0.229, 0.224, 0.225]),
759
+ ])
760
+
761
+ CLASS_NAMES = [
762
+ "dyed-lifted-polyps",
763
+ "dyed-resection-margins",
764
+ "esophagitis",
765
+ "normal-cecum",
766
+ "normal-pylorus",
767
+ "normal-z-line",
768
+ "polyps",
769
+ "ulcerative-colitis",
770
+ ]
771
+
772
+
773
+ CLASS_EMOJIS = {
774
+ "dyed-lifted-polyps": "🟣",
775
+ "dyed-resection-margins": "πŸ”΅",
776
+ "esophagitis": "🟠",
777
+ "normal-cecum": "🟒",
778
+ "normal-pylorus": "🟨",
779
+ "normal-z-line": "🟦",
780
+ "polyps": "🟑",
781
+ "ulcerative-colitis": "πŸ”΄",
782
+ }
783
+
784
+
785
+ # https://www.gradio.app/guides/the-interface-class
786
+ # fn: the function to wrap a user interface (UI) around
787
+ def predict(image, model_name: str):
788
+ """
789
+ Takes a PIL image and model name.
790
+ Returns: prediction label, confidence bar chart figure, attention note.
791
+ """
792
+ # Guard: Image is None
793
+ if image is None:
794
+ return "No image uploaded.", None
795
+
796
+ # Guad: if model_name is None
797
+ if model_name is None:
798
+ return "No model selected.", None
799
+
800
+ # Guard: Model not loaded
801
+ if model_name not in LOADED_MODELS:
802
+ return f"Model '{model_name}' not loaded. Available: {list(LOADED_MODELS.keys())}", None
803
+
804
+ model = LOADED_MODELS[model_name]
805
+
806
+ # Guard: In case model is stored as None
807
+ if model is None:
808
+ return f"Model '{model_name}' is None β€” checkpoint failed to load.", None
809
+
810
+ # Image Transformation
811
+ transform = (inception_transform
812
+ if "inception" in model_name.lower()
813
+ else inference_transform)
814
+
815
+ img_tensor = transform(image).unsqueeze(0).to(DEVICE) # [1, 3, H, W]
816
+
817
+ # Model Prediction Inference
818
+ model.eval()
819
+ with torch.no_grad():
820
+ logits = model(img_tensor)
821
+ probs = F.softmax(logits, dim=1).squeeze()
822
+ pred_idx = probs.argmax().item()
823
+ pred_class = CLASS_NAMES[pred_idx]
824
+ confidence = probs[pred_idx].item()
825
+
826
+ # Probability Bar Chart
827
+ probs_np = probs.cpu().numpy()
828
+ colours = ["#1D9E75" if i == pred_idx else "#B0BEC5"
829
+ for i in range(len(CLASS_NAMES))]
830
+
831
+ fig, ax = plt.subplots(figsize=(7, 3.5))
832
+ bars = ax.barh(CLASS_NAMES, probs_np, color=colours, edgecolor="white")
833
+ ax.set_xlim(0, 1)
834
+ ax.set_xlabel("Probability")
835
+ ax.set_title(f"{model_name} β€” class probabilities")
836
+ ax.spines[["top", "right"]].set_visible(False)
837
+ for bar, prob in zip(bars, probs_np):
838
+ if prob > 0.02:
839
+ ax.text(prob + 0.01, bar.get_y() + bar.get_height() / 2,
840
+ f"{prob:.1%}", va="center", fontsize=9)
841
+ plt.tight_layout()
842
+
843
+ # Class Label
844
+ emoji = CLASS_EMOJIS.get(pred_class, "")
845
+ label = (f"{emoji} Predicted: {pred_class.replace('-',' ').title()}\n"
846
+ f"Confidence: {confidence:.1%}\n"
847
+ f"Model: {model_name}")
848
+
849
+ return label, fig
850
+
851
+
852
+ print("LOADED_MODELS contents:")
853
+ for name, model in LOADED_MODELS.items():
854
+ print(f" {name}: {type(model).__name__ if model is not None else 'None'}")
855
+
856
+ example_paths = []
857
+ for class_name in CLASS_NAMES:
858
+ images = glob(f"gradio_examples/{class_name}.jpg")
859
+ if images:
860
+ dst = f"gradio_examples/{class_name}.jpg"
861
+ example_paths.append(dst)
862
+
863
+
864
+ with gr.Blocks(title="Kvasir GI Endoscopy Multi-Image Classifier") as demo:
865
+
866
+ gr.Markdown("""
867
+ # Kvasir Gastrointestinal Endoscopy Classifier
868
+ Upload an endoscopy image and select a model to classify it into one of
869
+ **8 GI tract categories**: esophagitis, polyps, ulcerative colitis,
870
+ dyed lifted polyps, dyed resection margins, normal cecum, pylorus, or z-line.
871
+ """)
872
+
873
+ with gr.Row():
874
+ with gr.Column(scale=1):
875
+ image_input = gr.Image(
876
+ type="pil", # PIL Image Format
877
+ label="Upload endoscopy image",
878
+ )
879
+ model_dropdown = gr.Dropdown(
880
+ choices=list(LOADED_MODELS.keys()),
881
+ value=list(LOADED_MODELS.keys())[0],
882
+ label="Select model",
883
+ )
884
+ predict_btn = gr.Button("Classify", variant="primary")
885
+
886
+ with gr.Column(scale=2):
887
+ label_output = gr.Textbox(
888
+ label="Prediction",
889
+ lines=3,
890
+ )
891
+ chart_output = gr.Plot(
892
+ label="Class probabilities",
893
+ )
894
+
895
+ # Example images for testing
896
+ gr.Examples(
897
+ examples=[[p, random.choice(list(LOADED_MODELS.keys()))] for p in example_paths],
898
+ inputs=[image_input, model_dropdown],
899
+ label="Example images",
900
+ )
901
+
902
+ # Wire button to predict function
903
+ predict_btn.click(
904
+ fn=predict,
905
+ inputs=[image_input, model_dropdown],
906
+ outputs=[label_output, chart_output],
907
+ )
908
+
909
+ # Also predict on image upload (no button press needed)
910
+ image_input.change(
911
+ fn=predict,
912
+ inputs=[image_input, model_dropdown],
913
+ outputs=[label_output, chart_output],
914
+ )
915
+
916
+ demo.launch(share=True, allowed_paths=["gradio_examples"]) # share=True -> public URL