File size: 121,831 Bytes
05d8d93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 70,
   "id": "73585b7a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'2.7.1+cu126'"
      ]
     },
     "execution_count": 70,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn # nn contains all of PyTorch's building blocks for neural networks\n",
    "import matplotlib.pyplot as plt\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import random_split, DataLoader\n",
    "import os\n",
    "import numpy as np\n",
    "\n",
    "\n",
    "# Check PyTorch version\n",
    "torch.__version__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "id": "674a74c2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'cuda'"
      ]
     },
     "execution_count": 71,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Setup device-agnostic code\n",
    "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
    "device"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 239,
   "id": "7d08e875",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "data_dir = \"/home/arpan/torchenv_learning/data/num_dataset_jpg\"\n",
    "\n",
    "transform = transforms.Compose([\n",
    "    transforms.RandomInvert(1),\n",
    "    transforms.Resize((28, 28)),  # Resize images to 28x28\n",
    "    transforms.Grayscale(num_output_channels=1),  # Convert to grayscale\n",
    "    transforms.RandomRotation(15),\n",
    "    transforms.RandomAffine(0, translate=(0.1, 0.1)),\n",
    "    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n",
    "    transforms.RandomPerspective(distortion_scale=0.2, p=0.5),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.RandomInvert(1),\n",
    "])\n",
    "\n",
    "full_dataset = datasets.ImageFolder(root=data_dir, transform=transform)\n",
    "\n",
    "# Split into train/test\n",
    "train_ratio = 0.8\n",
    "train_size = int(train_ratio * len(full_dataset))\n",
    "test_size = len(full_dataset) - train_size\n",
    "\n",
    "train_dataset, test_dataset = random_split(full_dataset, [train_size, test_size])\n",
    "\n",
    "\n",
    "train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "id": "7a072dc8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "NumberClassifier(\n",
       "  (conv_block_1): Sequential(\n",
       "    (0): Conv2d(1, 10, kernel_size=(2, 2), stride=(1, 1))\n",
       "    (1): ReLU()\n",
       "    (2): Conv2d(10, 10, kernel_size=(2, 2), stride=(1, 1))\n",
       "    (3): ReLU()\n",
       "    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)\n",
       "  )\n",
       "  (conv_block_2): Sequential(\n",
       "    (0): Conv2d(10, 10, kernel_size=(2, 2), stride=(1, 1))\n",
       "    (1): ReLU()\n",
       "    (2): Conv2d(10, 10, kernel_size=(2, 2), stride=(1, 1))\n",
       "    (3): ReLU()\n",
       "    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)\n",
       "  )\n",
       "  (classifier): Sequential(\n",
       "    (0): Flatten(start_dim=1, end_dim=-1)\n",
       "    (1): Linear(in_features=250, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 74,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class NumberClassifier(nn.Module):\n",
    "\n",
    "    def __init__(self, input_shape: int, hidden_units: int, output_shape: int) -> None:\n",
    "        super().__init__()\n",
    "        self.conv_block_1 = nn.Sequential(\n",
    "            nn.Conv2d(in_channels=input_shape, \n",
    "                      out_channels=hidden_units, \n",
    "                      kernel_size=2, # how big is the square that's going over the image?\n",
    "                      stride=1, # default\n",
    "                      ), # options = \"valid\" (no padding) or \"same\" (output has same shape as input) or int for specific number \n",
    "            nn.ReLU(),\n",
    "            nn.Conv2d(in_channels=hidden_units, \n",
    "                      out_channels=hidden_units,\n",
    "                      kernel_size=2,\n",
    "                      stride=1,\n",
    "                      ),\n",
    "            nn.ReLU(),\n",
    "            nn.MaxPool2d(kernel_size=2,\n",
    "                         stride=2) # default stride value is same as kernel_size\n",
    "        )\n",
    "        self.conv_block_2 = nn.Sequential(\n",
    "            nn.Conv2d(hidden_units, hidden_units, kernel_size=2),\n",
    "            nn.ReLU(),\n",
    "            nn.Conv2d(hidden_units, hidden_units, kernel_size=2),\n",
    "            nn.ReLU(),\n",
    "            nn.MaxPool2d(2)\n",
    "        )\n",
    "        self.classifier = nn.Sequential(\n",
    "            nn.Flatten(),\n",
    "            # Where did this in_features shape come from? \n",
    "            # It's because each layer of our network compresses and changes the shape of our input data.\n",
    "            nn.Linear(in_features=hidden_units*5*5,\n",
    "                      out_features=output_shape)\n",
    "        )\n",
    "    \n",
    "    def forward(self, x: torch.Tensor):\n",
    "        x = self.conv_block_1(x)\n",
    "        # print(x.shape)\n",
    "        x = self.conv_block_2(x)\n",
    "        # print(x.shape)\n",
    "        x = self.classifier(x)\n",
    "        # print(x.shape)\n",
    "        return x\n",
    "        # return self.classifier(self.conv_block_2(self.conv_block_1(x))) # <- leverage the benefits of operator fusion\n",
    "\n",
    "torch.manual_seed(42)\n",
    "model_0 = NumberClassifier(input_shape=1, # number of color channels (3 for RGB) \n",
    "                  hidden_units=10, \n",
    "                  output_shape=len(full_dataset.classes)).to(device)\n",
    "model_0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "id": "15b013cb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Single image shape: torch.Size([1, 1, 28, 28])\n",
      "\n",
      "Output logits:\n",
      "tensor([[-0.0163,  0.0994, -0.0605, -0.0853,  0.0305,  0.0064, -0.0244,  0.0897,\n",
      "         -0.1144, -0.1026]], device='cuda:0')\n",
      "\n",
      "Output prediction probabilities:\n",
      "tensor([[0.0999, 0.1121, 0.0956, 0.0932, 0.1047, 0.1022, 0.0991, 0.1111, 0.0906,\n",
      "         0.0916]], device='cuda:0')\n",
      "\n",
      "Output prediction label:\n",
      "tensor([1], device='cuda:0')\n",
      "\n",
      "Actual label:\n",
      "1\n"
     ]
    }
   ],
   "source": [
    "# 1. Get a batch of images and labels from the DataLoader\n",
    "img_batch, label_batch = next(iter(train_loader))\n",
    "\n",
    "# 2. Get a single image from the batch and unsqueeze the image so its shape fits the model\n",
    "img_single, label_single = img_batch[0].unsqueeze(dim=0), label_batch[0]\n",
    "print(f\"Single image shape: {img_single.shape}\\n\")\n",
    "\n",
    "# 3. Perform a forward pass on a single image\n",
    "model_0.eval()\n",
    "with torch.inference_mode():\n",
    "    pred = model_0(img_single.to(device))\n",
    "    \n",
    "# 4. Print out what's happening and convert model logits -> pred probs -> pred label\n",
    "print(f\"Output logits:\\n{pred}\\n\")\n",
    "print(f\"Output prediction probabilities:\\n{torch.softmax(pred, dim=1)}\\n\")\n",
    "print(f\"Output prediction label:\\n{torch.argmax(torch.softmax(pred, dim=1), dim=1)}\\n\")\n",
    "print(f\"Actual label:\\n{label_single}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "id": "80ad6e9a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "==========================================================================================\n",
       "Layer (type:depth-idx)                   Output Shape              Param #\n",
       "==========================================================================================\n",
       "NumberClassifier                         [1, 10]                   --\n",
       "├─Sequential: 1-1                        [1, 10, 13, 13]           --\n",
       "│    └─Conv2d: 2-1                       [1, 10, 27, 27]           50\n",
       "│    └─ReLU: 2-2                         [1, 10, 27, 27]           --\n",
       "│    └─Conv2d: 2-3                       [1, 10, 26, 26]           410\n",
       "│    └─ReLU: 2-4                         [1, 10, 26, 26]           --\n",
       "│    └─MaxPool2d: 2-5                    [1, 10, 13, 13]           --\n",
       "├─Sequential: 1-2                        [1, 10, 5, 5]             --\n",
       "│    └─Conv2d: 2-6                       [1, 10, 12, 12]           410\n",
       "│    └─ReLU: 2-7                         [1, 10, 12, 12]           --\n",
       "│    └─Conv2d: 2-8                       [1, 10, 11, 11]           410\n",
       "│    └─ReLU: 2-9                         [1, 10, 11, 11]           --\n",
       "│    └─MaxPool2d: 2-10                   [1, 10, 5, 5]             --\n",
       "├─Sequential: 1-3                        [1, 10]                   --\n",
       "│    └─Flatten: 2-11                     [1, 250]                  --\n",
       "│    └─Linear: 2-12                      [1, 10]                   2,510\n",
       "==========================================================================================\n",
       "Total params: 3,790\n",
       "Trainable params: 3,790\n",
       "Non-trainable params: 0\n",
       "Total mult-adds (Units.MEGABYTES): 0.42\n",
       "==========================================================================================\n",
       "Input size (MB): 0.00\n",
       "Forward/backward pass size (MB): 0.13\n",
       "Params size (MB): 0.02\n",
       "Estimated Total Size (MB): 0.15\n",
       "=========================================================================================="
      ]
     },
     "execution_count": 76,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Install torchinfo if it's not available, import it if it is\n",
    "import torchinfo\n",
    "from torchinfo import summary\n",
    "\n",
    "summary(model_0, input_size=[1, 1, 28, 28]) # do a test pass through of an example input size"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "id": "7630f560",
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_step(model: torch.nn.Module, \n",
    "               dataloader: torch.utils.data.DataLoader, \n",
    "               loss_fn: torch.nn.Module, \n",
    "               optimizer: torch.optim.Optimizer):\n",
    "    # Put model in train mode\n",
    "    model.train()\n",
    "    \n",
    "    # Setup train loss and train accuracy values\n",
    "    train_loss, train_acc = 0, 0\n",
    "    \n",
    "    # Loop through data loader data batches\n",
    "    for batch, (X, y) in enumerate(dataloader):\n",
    "        # Send data to target device\n",
    "        X, y = X.to(device), y.to(device)\n",
    "\n",
    "        # 1. Forward pass\n",
    "        y_pred = model(X)\n",
    "\n",
    "        # 2. Calculate  and accumulate loss\n",
    "        loss = loss_fn(y_pred, y)\n",
    "        train_loss += loss.item() \n",
    "\n",
    "        # 3. Optimizer zero grad\n",
    "        optimizer.zero_grad()\n",
    "\n",
    "        # 4. Loss backward\n",
    "        loss.backward()\n",
    "\n",
    "        # 5. Optimizer step\n",
    "        optimizer.step()\n",
    "\n",
    "        # Calculate and accumulate accuracy metrics across all batches\n",
    "        y_pred_class = torch.argmax(torch.softmax(y_pred, dim=1), dim=1)\n",
    "        train_acc += (y_pred_class == y).sum().item()/len(y_pred)\n",
    "\n",
    "    # Adjust metrics to get average loss and accuracy per batch \n",
    "    train_loss = train_loss / len(dataloader)\n",
    "    train_acc = train_acc / len(dataloader)\n",
    "    return train_loss, train_acc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "id": "d912272a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_step(model: torch.nn.Module, \n",
    "              dataloader: torch.utils.data.DataLoader, \n",
    "              loss_fn: torch.nn.Module):\n",
    "    # Put model in eval mode\n",
    "    model.eval() \n",
    "    \n",
    "    # Setup test loss and test accuracy values\n",
    "    test_loss, test_acc = 0, 0\n",
    "    \n",
    "    # Turn on inference context manager\n",
    "    with torch.inference_mode():\n",
    "        # Loop through DataLoader batches\n",
    "        for batch, (X, y) in enumerate(dataloader):\n",
    "            # Send data to target device\n",
    "            X, y = X.to(device), y.to(device)\n",
    "    \n",
    "            # 1. Forward pass\n",
    "            test_pred_logits = model(X)\n",
    "\n",
    "            # 2. Calculate and accumulate loss\n",
    "            loss = loss_fn(test_pred_logits, y)\n",
    "            test_loss += loss.item()\n",
    "            \n",
    "            # Calculate and accumulate accuracy\n",
    "            test_pred_labels = test_pred_logits.argmax(dim=1)\n",
    "            test_acc += ((test_pred_labels == y).sum().item()/len(test_pred_labels))\n",
    "            \n",
    "    # Adjust metrics to get average loss and accuracy per batch \n",
    "    test_loss = test_loss / len(dataloader)\n",
    "    test_acc = test_acc / len(dataloader)\n",
    "    return test_loss, test_acc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "id": "7886be66",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm.auto import tqdm\n",
    "\n",
    "# 1. Take in various parameters required for training and test steps\n",
    "def train(model: torch.nn.Module, \n",
    "          train_dataloader: torch.utils.data.DataLoader, \n",
    "          test_dataloader: torch.utils.data.DataLoader, \n",
    "          optimizer: torch.optim.Optimizer,\n",
    "          loss_fn: torch.nn.Module = nn.CrossEntropyLoss(),\n",
    "          epochs: int = 5):\n",
    "    \n",
    "    # 2. Create empty results dictionary\n",
    "    results = {\"train_loss\": [],\n",
    "        \"train_acc\": [],\n",
    "        \"test_loss\": [],\n",
    "        \"test_acc\": []\n",
    "    }\n",
    "    \n",
    "    # 3. Loop through training and testing steps for a number of epochs\n",
    "    for epoch in tqdm(range(epochs)):\n",
    "        train_loss, train_acc = train_step(model=model,\n",
    "                                           dataloader=train_dataloader,\n",
    "                                           loss_fn=loss_fn,\n",
    "                                           optimizer=optimizer)\n",
    "        test_loss, test_acc = test_step(model=model,\n",
    "            dataloader=test_dataloader,\n",
    "            loss_fn=loss_fn)\n",
    "        \n",
    "        # 4. Print out what's happening\n",
    "        print(\n",
    "            f\"Epoch: {epoch+1} | \"\n",
    "            f\"train_loss: {train_loss:.4f} | \"\n",
    "            f\"train_acc: {train_acc:.4f} | \"\n",
    "            f\"test_loss: {test_loss:.4f} | \"\n",
    "            f\"test_acc: {test_acc:.4f}\"\n",
    "        )\n",
    "\n",
    "        # 5. Update results dictionary\n",
    "        # Ensure all data is moved to CPU and converted to float for storage\n",
    "        results[\"train_loss\"].append(train_loss.item() if isinstance(train_loss, torch.Tensor) else train_loss)\n",
    "        results[\"train_acc\"].append(train_acc.item() if isinstance(train_acc, torch.Tensor) else train_acc)\n",
    "        results[\"test_loss\"].append(test_loss.item() if isinstance(test_loss, torch.Tensor) else test_loss)\n",
    "        results[\"test_acc\"].append(test_acc.item() if isinstance(test_acc, torch.Tensor) else test_acc)\n",
    "\n",
    "    # 6. Return the filled results at the end of the epochs\n",
    "    return results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 287,
   "id": "f0d501b6",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 20%|██        | 1/5 [03:12<12:49, 192.33s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 1 | train_loss: 0.2044 | train_acc: 0.9403 | test_loss: 0.1693 | test_acc: 0.9519\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 40%|████      | 2/5 [06:28<09:43, 194.54s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 2 | train_loss: 0.1614 | train_acc: 0.9522 | test_loss: 0.1490 | test_acc: 0.9564\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 60%|██████    | 3/5 [09:56<06:41, 200.94s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 3 | train_loss: 0.1578 | train_acc: 0.9531 | test_loss: 0.1402 | test_acc: 0.9579\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 80%|████████  | 4/5 [13:10<03:18, 198.12s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 4 | train_loss: 0.1527 | train_acc: 0.9551 | test_loss: 0.1406 | test_acc: 0.9588\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 5/5 [16:19<00:00, 195.95s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 5 | train_loss: 0.1539 | train_acc: 0.9546 | test_loss: 0.1670 | test_acc: 0.9497\n",
      "Total training time: 979.759 seconds\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# Set random seeds\n",
    "torch.manual_seed(40) \n",
    "torch.cuda.manual_seed(40)\n",
    "\n",
    "# Set number of epochs\n",
    "NUM_EPOCHS = 5\n",
    "\n",
    "# Setup loss function and optimizer\n",
    "loss_fn = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.Adam(params=model_0.parameters(), lr=0.01)\n",
    "\n",
    "# Start the timer\n",
    "from timeit import default_timer as timer \n",
    "start_time = timer()\n",
    "\n",
    "# Train model_0 \n",
    "model_0_results = train(model=model_0, \n",
    "                        train_dataloader=train_loader,\n",
    "                        test_dataloader=test_loader,\n",
    "                        optimizer=optimizer,\n",
    "                        loss_fn=loss_fn, \n",
    "                        epochs=NUM_EPOCHS)\n",
    "\n",
    "# End the timer and print out how long it took\n",
    "end_time = timer()\n",
    "print(f\"Total training time: {end_time-start_time:.3f} seconds\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 309,
   "id": "34e9b5e0",
   "metadata": {},
   "outputs": [],
   "source": [
    "from PIL import Image, ImageEnhance, ImageOps\n",
    "\n",
    "def predict_number(custom_image: Image.Image):\n",
    "\n",
    "    custom_image = ImageEnhance.Contrast(custom_image.convert(\"L\")).enhance(5.0).point(lambda p: 255 if p > 128 else 0).resize((28, 28))\n",
    "\n",
    "    transform = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    ])\n",
    "\n",
    "    model_0.eval()\n",
    "    with torch.inference_mode():\n",
    "        pred = torch.softmax(model_0(transform(custom_image).unsqueeze(0).to(device)), dim=1)\n",
    "\n",
    "    class_names = full_dataset.classes\n",
    "    return {class_names[i]: float(pred[0][i]) for i in range(len(class_names))}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 299,
   "id": "96dfd120",
   "metadata": {},
   "outputs": [],
   "source": [
    "img_ = Image.open(\"/home/arpan/torchenv_learning/showcase_dataset/5_inverted.png\")\n",
    "img_ = img_.convert(\"RGB\")\n",
    "img_ = ImageOps.invert(img_)\n",
    "img_.save(\"/home/arpan/torchenv_learning/showcase_dataset/5_4.png\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 300,
   "id": "20809009",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/home/arpan/torchenv_learning/showcase_dataset/4_2.png ==> Prediction: 4, with probability 0.9377965331077576 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/0.jpg ==> Prediction: 0, with probability 0.5970964431762695 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/9_1.png ==> Prediction: 9, with probability 0.9998043179512024 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/0_1.png ==> Prediction: 0, with probability 0.9994669556617737 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/3.png ==> Prediction: 3, with probability 0.9927063584327698 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/6.png ==> Prediction: 6, with probability 0.9987753033638 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/7_1.png ==> Prediction: 7, with probability 0.814873993396759 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/9.png ==> Prediction: 8, with probability 0.5316581726074219 - ❌\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/3_2.png ==> Prediction: 3, with probability 0.9990369081497192 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/5_3.png ==> Prediction: 5, with probability 0.9977946281433105 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/5_4.png ==> Prediction: 9, with probability 0.5392404198646545 - ❌\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/3_1.png ==> Prediction: 3, with probability 0.9937238097190857 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/5.png ==> Prediction: 5, with probability 0.5505344271659851 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/1_1.png ==> Prediction: 7, with probability 0.7994639873504639 - ❌\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/7.png ==> Prediction: 7, with probability 0.9400009512901306 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/5_2.png ==> Prediction: 5, with probability 0.9454333782196045 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/4.png ==> Prediction: 4, with probability 0.9998598098754883 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/7_2.png ==> Prediction: 7, with probability 0.9654712677001953 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/1.png ==> Prediction: 1, with probability 0.9999350309371948 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/4_1.png ==> Prediction: 8, with probability 0.9917175769805908 - ❌\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/2.png ==> Prediction: 2, with probability 0.993058443069458 - ✅\n",
      "\n",
      "/home/arpan/torchenv_learning/showcase_dataset/5_1.png ==> Prediction: 5, with probability 0.6415048241615295 - ✅\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from glob import glob\n",
    "\n",
    "for image_path in glob(\"/home/arpan/torchenv_learning/showcase_dataset/*.*\"):\n",
    "    predict_number(image_path)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 301,
   "id": "206562e0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x7f11a2c179b0>"
      ]
     },
     "execution_count": 301,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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ANevXwcAREZGYuXKlcjLy8Mff/yB+fPn/+130IcPHw65XI6wsDDk5eXp9pmTkwMAkEql4PF4yM/Px927d1FXVweRSIRFixYhOjoaW7duRVVVFU6dOoV169bpLkj76KOPcPHiRSxevBgVFRXIyspCZmZmp37eESNGoLq6GtnZ2aiqqkJqamq7FwKamJhALpfjzJkzOHLkCBYsWICgoCBYW1sDABISEqBUKpGamooLFy6grKwMGRkZWLNmTafyEEK6BhV2Qv5iamqK4uJiSCQSBAYGwsXFBeHh4WhoaNDN4D/55BN88MEHkMvlkMlkEIlEeOedd/52v2lpaXj33Xcxf/58ODs7Y968eaivrwcA2NraIiEhAbGxsbCyskJERAQAIDExEXFxcVAqlXBxccG0adOwf/9+2NnZAXh83nv37t3Iy8uDu7s7NmzYgOTk5E79vG+//Taio6MRERGBsWPH4tixY4iLi2vTz9HREYGBgXjrrbcwZcoUuLm56X2dbe7cudi8eTMyMjLg6uoKHx8fZGZm6rISQl4uHnveVT+EEEII+c+hGTshhBBiQKiwE0IIIQaECjshhBBiQKiwE0IIIQaECjshhBBiQKiwE0IIIQaECjshhBBiQKiwE0IIIQaECjshhBBiQKiwE0IIIQaECjshhBBiQP4P5qciwVbps0cAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 640x480 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import torch\n",
    "from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n",
    "\n",
    "all_preds = []\n",
    "all_labels = []\n",
    "\n",
    "model_0.eval()\n",
    "with torch.no_grad():\n",
    "    for images, labels in test_loader:\n",
    "        images = images.to(device)\n",
    "        labels = labels.to(device)\n",
    "\n",
    "        outputs = model_0(images)\n",
    "        _, preds = torch.max(outputs, 1)\n",
    "\n",
    "        all_preds.extend(preds.cpu().numpy())\n",
    "        all_labels.extend(labels.cpu().numpy())\n",
    "\n",
    "cm = confusion_matrix(all_labels, all_preds)\n",
    "disp = ConfusionMatrixDisplay(confusion_matrix=cm)\n",
    "cm_normalized = cm.astype(\"float\") / cm.sum(axis=1, keepdims=True)\n",
    "disp = ConfusionMatrixDisplay(confusion_matrix=cm_normalized, display_labels=list(full_dataset.classes))\n",
    "disp.plot(cmap=\"Blues\", values_format=\".2f\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3357a514",
   "metadata": {},
   "source": [
    "### save Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 315,
   "id": "d61a4e77",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saving model to: /home/arpan/torchenv_learning/models/pytorch_num_classifier_final_model_with_EMNIST.pth\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "# 1. Create models directory \n",
    "MODEL_PATH = Path(\"/home/arpan/torchenv_learning/models\")\n",
    "MODEL_PATH.mkdir(parents=True, exist_ok=True)\n",
    "\n",
    "# 2. Create model save path \n",
    "MODEL_NAME = \"pytorch_num_classifier_final_model_with_EMNIST.pth\"\n",
    "MODEL_SAVE_PATH = MODEL_PATH / MODEL_NAME\n",
    "\n",
    "# 3. Save the model state dict \n",
    "print(f\"Saving model to: {MODEL_SAVE_PATH}\")\n",
    "torch.save(obj=model_0.state_dict(), # only saving the state_dict() only saves the models learned parameters\n",
    "           f=MODEL_SAVE_PATH)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 236,
   "id": "a38216d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch.utils.data import Dataset, DataLoader, random_split\n",
    "from torchvision import transforms\n",
    "import numpy as np\n",
    "\n",
    "class ImageWithWriterDataset(Dataset):\n",
    "    def __init__(self, image_path, writer_info_path, transform=None):\n",
    "        self.images = np.load(image_path)            # shape: (N, 28, 28)\n",
    "        self.writer_info = np.load(writer_info_path, allow_pickle=True)  # shape: (N,), each item: list/array\n",
    "        self.transform = transform\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.images)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        img = self.images[idx]                       # (28, 28)\n",
    "        label = int(self.writer_info[idx][0])        # First item is the label\n",
    "\n",
    "        # Convert grayscale to 3D tensor (1, 28, 28) and normalize to [0, 1]\n",
    "        if self.transform:\n",
    "            img = self.transform(img)\n",
    "        else:\n",
    "            img = torch.tensor(img, dtype=torch.float32).unsqueeze(0) / 255.0  # shape: (1, 28, 28)\n",
    "\n",
    "        return img, label\n",
    "    \n",
    "transform = transforms.Compose([\n",
    "    transforms.ToPILImage(),\n",
    "    transforms.RandomInvert(1),\n",
    "    transforms.Resize((28, 28)),  # Resize images to 28x28\n",
    "    transforms.Grayscale(num_output_channels=1),  # Convert to grayscale\n",
    "    transforms.RandomRotation(15),\n",
    "    transforms.RandomAffine(0, translate=(0.1, 0.1)),\n",
    "    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n",
    "    transforms.RandomPerspective(distortion_scale=0.2, p=0.5),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.RandomInvert(1),\n",
    "])\n",
    "\n",
    "dataset = ImageWithWriterDataset(\n",
    "    \"/home/arpan/torchenv_learning/data/Images(28x28).npy\",\n",
    "    \"/home/arpan/torchenv_learning/data/WriterInfo.npy\",\n",
    "    transform=None  # or use `transform=transform` if you want PIL-based pipeline\n",
    ")\n",
    "\n",
    "# Split into train/test\n",
    "train_size = int(0.8 * len(dataset))\n",
    "test_size = len(dataset) - train_size\n",
    "train_dataset, test_dataset = random_split(dataset, [train_size, test_size])\n",
    "\n",
    "# Dataloaders\n",
    "train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c86b2e55",
   "metadata": {},
   "source": [
    "### With Pretrained model - Hidden"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 162,
   "id": "54168e52",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torchvision\n",
    "weights = torchvision.models.EfficientNet_B1_Weights.DEFAULT # .DEFAULT = best available weights \n",
    "model_pretrained = torchvision.models.efficientnet_b1(weights=weights).to(device)\n",
    "\n",
    "# # Freeze all base layers in the \"features\" section of the model (the feature extractor) by setting requires_grad=False\n",
    "# for param in model_pretrained.features.parameters():\n",
    "#     param.requires_grad = False\n",
    "\n",
    "# Set the manual seeds\n",
    "torch.manual_seed(40)\n",
    "torch.cuda.manual_seed(40)\n",
    "\n",
    "# Get the length of class_names (one output unit for each class)\n",
    "output_shape = 10\n",
    "\n",
    "# Recreate the classifier layer and seed it to the target device\n",
    "model_pretrained.classifier = torch.nn.Sequential(\n",
    "    torch.nn.Dropout(p=0.2, inplace=True), \n",
    "    torch.nn.Linear(in_features=1280, \n",
    "                    out_features=output_shape, # same number of output units as our number of classes\n",
    "                    bias=True)).to(device)\n",
    "\n",
    "old_conv = model_pretrained.features[0][0]\n",
    "model_pretrained.features[0][0] = nn.Conv2d(\n",
    "    in_channels=3,\n",
    "    out_channels=old_conv.out_channels,\n",
    "    kernel_size=old_conv.kernel_size,\n",
    "    stride=old_conv.stride,\n",
    "    padding=old_conv.padding,\n",
    "    bias=False)\n",
    "\n",
    "\n",
    "data_dir = \"/home/arpan/torchenv_learning/EU_num_processed_dataset\"\n",
    "\n",
    "transform = weights.transforms()\n",
    "\n",
    "full_dataset = datasets.ImageFolder(root=data_dir, transform=transform)\n",
    "\n",
    "# Split into train/test\n",
    "train_ratio = 0.8\n",
    "train_size = int(train_ratio * len(full_dataset))\n",
    "test_size = len(full_dataset) - train_size\n",
    "\n",
    "train_dataset, test_dataset = random_split(full_dataset, [train_size, test_size])\n",
    "\n",
    "\n",
    "train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 163,
   "id": "c7cba614",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "============================================================================================================================================\n",
       "Layer (type (var_name))                                      Input Shape          Output Shape         Param #              Trainable\n",
       "============================================================================================================================================\n",
       "EfficientNet (EfficientNet)                                  [32, 3, 240, 240]    [32, 10]             --                   True\n",
       "├─Sequential (features)                                      [32, 3, 240, 240]    [32, 1280, 8, 8]     --                   True\n",
       "│    └─Conv2dNormActivation (0)                              [32, 3, 240, 240]    [32, 32, 120, 120]   --                   True\n",
       "│    │    └─Conv2d (0)                                       [32, 3, 240, 240]    [32, 32, 120, 120]   864                  True\n",
       "│    │    └─BatchNorm2d (1)                                  [32, 32, 120, 120]   [32, 32, 120, 120]   64                   True\n",
       "│    │    └─SiLU (2)                                         [32, 32, 120, 120]   [32, 32, 120, 120]   --                   --\n",
       "│    └─Sequential (1)                                        [32, 32, 120, 120]   [32, 16, 120, 120]   --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 32, 120, 120]   [32, 16, 120, 120]   1,448                True\n",
       "│    │    └─MBConv (1)                                       [32, 16, 120, 120]   [32, 16, 120, 120]   612                  True\n",
       "│    └─Sequential (2)                                        [32, 16, 120, 120]   [32, 24, 60, 60]     --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 16, 120, 120]   [32, 24, 60, 60]     6,004                True\n",
       "│    │    └─MBConv (1)                                       [32, 24, 60, 60]     [32, 24, 60, 60]     10,710               True\n",
       "│    │    └─MBConv (2)                                       [32, 24, 60, 60]     [32, 24, 60, 60]     10,710               True\n",
       "│    └─Sequential (3)                                        [32, 24, 60, 60]     [32, 40, 30, 30]     --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 24, 60, 60]     [32, 40, 30, 30]     15,350               True\n",
       "│    │    └─MBConv (1)                                       [32, 40, 30, 30]     [32, 40, 30, 30]     31,290               True\n",
       "│    │    └─MBConv (2)                                       [32, 40, 30, 30]     [32, 40, 30, 30]     31,290               True\n",
       "│    └─Sequential (4)                                        [32, 40, 30, 30]     [32, 80, 15, 15]     --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 40, 30, 30]     [32, 80, 15, 15]     37,130               True\n",
       "│    │    └─MBConv (1)                                       [32, 80, 15, 15]     [32, 80, 15, 15]     102,900              True\n",
       "│    │    └─MBConv (2)                                       [32, 80, 15, 15]     [32, 80, 15, 15]     102,900              True\n",
       "│    │    └─MBConv (3)                                       [32, 80, 15, 15]     [32, 80, 15, 15]     102,900              True\n",
       "│    └─Sequential (5)                                        [32, 80, 15, 15]     [32, 112, 15, 15]    --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 80, 15, 15]     [32, 112, 15, 15]    126,004              True\n",
       "│    │    └─MBConv (1)                                       [32, 112, 15, 15]    [32, 112, 15, 15]    208,572              True\n",
       "│    │    └─MBConv (2)                                       [32, 112, 15, 15]    [32, 112, 15, 15]    208,572              True\n",
       "│    │    └─MBConv (3)                                       [32, 112, 15, 15]    [32, 112, 15, 15]    208,572              True\n",
       "│    └─Sequential (6)                                        [32, 112, 15, 15]    [32, 192, 8, 8]      --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 112, 15, 15]    [32, 192, 8, 8]      262,492              True\n",
       "│    │    └─MBConv (1)                                       [32, 192, 8, 8]      [32, 192, 8, 8]      587,952              True\n",
       "│    │    └─MBConv (2)                                       [32, 192, 8, 8]      [32, 192, 8, 8]      587,952              True\n",
       "│    │    └─MBConv (3)                                       [32, 192, 8, 8]      [32, 192, 8, 8]      587,952              True\n",
       "│    │    └─MBConv (4)                                       [32, 192, 8, 8]      [32, 192, 8, 8]      587,952              True\n",
       "│    └─Sequential (7)                                        [32, 192, 8, 8]      [32, 320, 8, 8]      --                   True\n",
       "│    │    └─MBConv (0)                                       [32, 192, 8, 8]      [32, 320, 8, 8]      717,232              True\n",
       "│    │    └─MBConv (1)                                       [32, 320, 8, 8]      [32, 320, 8, 8]      1,563,600            True\n",
       "│    └─Conv2dNormActivation (8)                              [32, 320, 8, 8]      [32, 1280, 8, 8]     --                   True\n",
       "│    │    └─Conv2d (0)                                       [32, 320, 8, 8]      [32, 1280, 8, 8]     409,600              True\n",
       "│    │    └─BatchNorm2d (1)                                  [32, 1280, 8, 8]     [32, 1280, 8, 8]     2,560                True\n",
       "│    │    └─SiLU (2)                                         [32, 1280, 8, 8]     [32, 1280, 8, 8]     --                   --\n",
       "├─AdaptiveAvgPool2d (avgpool)                                [32, 1280, 8, 8]     [32, 1280, 1, 1]     --                   --\n",
       "├─Sequential (classifier)                                    [32, 1280]           [32, 10]             --                   True\n",
       "│    └─Dropout (0)                                           [32, 1280]           [32, 1280]           --                   --\n",
       "│    └─Linear (1)                                            [32, 1280]           [32, 10]             12,810               True\n",
       "============================================================================================================================================\n",
       "Total params: 6,525,994\n",
       "Trainable params: 6,525,994\n",
       "Non-trainable params: 0\n",
       "Total mult-adds (Units.GIGABYTES): 21.94\n",
       "============================================================================================================================================\n",
       "Input size (MB): 22.12\n",
       "Forward/backward pass size (MB): 5568.64\n",
       "Params size (MB): 26.10\n",
       "Estimated Total Size (MB): 5616.86\n",
       "============================================================================================================================================"
      ]
     },
     "execution_count": 163,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Print a summary using torchinfo (uncomment for actual output)\n",
    "summary(model=model_pretrained, \n",
    "        input_size=(32, 3, 240, 240), # make sure this is \"input_size\", not \"input_shape\"\n",
    "        # col_names=[\"input_size\"], # uncomment for smaller output\n",
    "        col_names=[\"input_size\", \"output_size\", \"num_params\", \"trainable\"],\n",
    "        col_width=20,\n",
    "        row_settings=[\"var_names\"]\n",
    ") "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 164,
   "id": "2cf385ae",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 20%|██        | 1/5 [01:00<04:01, 60.39s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 1 | train_loss: 2.3039 | train_acc: 0.1027 | test_loss: 2.3032 | test_acc: 0.0988\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 40%|████      | 2/5 [02:03<03:06, 62.01s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 2 | train_loss: 2.3035 | train_acc: 0.1047 | test_loss: 2.3030 | test_acc: 0.1074\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 60%|██████    | 3/5 [03:06<02:04, 62.33s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 3 | train_loss: 2.3034 | train_acc: 0.1017 | test_loss: 2.3027 | test_acc: 0.1030\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 80%|████████  | 4/5 [04:08<01:02, 62.21s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 4 | train_loss: 2.3032 | train_acc: 0.1042 | test_loss: 2.3027 | test_acc: 0.1041\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 5/5 [05:09<00:00, 61.99s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 5 | train_loss: 2.3042 | train_acc: 0.0992 | test_loss: 2.3031 | test_acc: 0.1034\n",
      "Total training time: 309.954 seconds\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# Set random seeds\n",
    "torch.manual_seed(40) \n",
    "torch.cuda.manual_seed(40)\n",
    "\n",
    "# Set number of epochs\n",
    "NUM_EPOCHS = 5\n",
    "\n",
    "# Setup loss function and optimizer\n",
    "loss_fn = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.Adam(params=model_0.parameters(), lr=0.01)\n",
    "\n",
    "# Start the timer\n",
    "from timeit import default_timer as timer \n",
    "start_time = timer()\n",
    "\n",
    "# Train model_0 \n",
    "model_0_results = train(model=model_pretrained, \n",
    "                        train_dataloader=train_loader,\n",
    "                        test_dataloader=test_loader,\n",
    "                        optimizer=optimizer,\n",
    "                        loss_fn=loss_fn, \n",
    "                        epochs=NUM_EPOCHS)\n",
    "\n",
    "# End the timer and print out how long it took\n",
    "end_time = timer()\n",
    "print(f\"Total training time: {end_time-start_time:.3f} seconds\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cb765530",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "4e134030",
   "metadata": {},
   "source": [
    "### MNIST Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 248,
   "id": "7014dcee",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Transform: converts images to tensor (shape [1, 28, 28])\n",
    "transform = transforms.Compose([\n",
    "    transforms.Resize((28, 28)),  # Resize images to 28x28\n",
    "    transforms.Grayscale(num_output_channels=1),  # Convert to grayscale\n",
    "    transforms.RandomRotation(15),\n",
    "    transforms.RandomAffine(0, translate=(0.1, 0.1)),\n",
    "    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n",
    "    transforms.RandomPerspective(distortion_scale=0.2, p=0.5),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.RandomInvert(1),\n",
    "])\n",
    "\n",
    "# Load MNIST train and test sets\n",
    "train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)\n",
    "test_dataset  = datasets.MNIST(root='./data', train=False, download=True, transform=transform)\n",
    "\n",
    "# Create data loaders\n",
    "train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n",
    "test_loader  = DataLoader(test_dataset, batch_size=64, shuffle=False)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32a0df3b",
   "metadata": {},
   "source": [
    "### EMANIST Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 279,
   "id": "f34d0886",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "# Define transforms (EMNIST is grayscale, 28x28)\n",
    "transform = transforms.Compose([\n",
    "    transforms.Resize((28, 28)),  # Resize images to 28x28\n",
    "    transforms.Grayscale(num_output_channels=1),  # Convert to grayscale\n",
    "    transforms.RandomRotation(15),\n",
    "    transforms.RandomAffine(0, translate=(0.1, 0.1)),\n",
    "    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n",
    "    transforms.RandomPerspective(distortion_scale=0.2, p=0.5),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Lambda(lambda x: torch.rot90(x, k=-1, dims=[1, 2])),\n",
    "    transforms.Lambda(lambda x: torch.flip(x, [2])),  # flip horizontally\n",
    "    transforms.RandomInvert(1),\n",
    "])\n",
    "\n",
    "# Load EMNIST 'digits' split (you can also use 'byclass', 'digits', etc.)\n",
    "train_dataset = datasets.EMNIST(root='./data', split='digits', train=True, download=True, transform=transform)\n",
    "test_dataset  = datasets.EMNIST(root='./data', split='digits', train=False, download=True, transform=transform)\n",
    "\n",
    "# DataLoaders\n",
    "train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n",
    "test_loader  = DataLoader(test_dataset, batch_size=64, shuffle=False)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b0a3d55",
   "metadata": {},
   "source": [
    "### Visualize Train data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 286,
   "id": "1d0859ad",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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w4YNM9cidOzatcvMrSFDv/jFL1T2wAMPmPXWz5hPP/1UZc8884xZ/95776nMGpo1dOhQs95aO2TIEHPt9u3bVdarVy+VtW7d2qyvD/dNtyGhX375pcouueQSc2013rL4JioqSmWzZ88211599dUqa9OmTY3vCbXH+jl+1113mWtXr16tMmvYpNvQ2yNHjqjMel9rvS8UEfnP//xPlbVs2dJci7rvxIkTKvvmm29U5jb80TpnVekJrCF+1nC0wsJCs/7OO+9U2fz58z1fPxjwiTcAAAAAAD6i8QYAAAAAwEc03gAAAAAA+IjGGwAAAAAAH9F4AwAAAADgo/Da3sC54ja9fPPmzSrLzc1V2UsvvWTWf/zxxyo7duyYytymArZv315lMTExKvu3f/s3s75fv34qS09PV5nbBMPqTjDHmZWXl6ts1apV5trFixdX61rh4fpb2ppq7jZR0pqsbGV79uwx61955RWV9e7dW2UNGjQw693OKWqONVnamowrIjJp0iSVZWZmqqwqT7iwJqz279/fc7219tJLLzXXWve3xx9/XGVpaWlmfUREhOd9ue0B/y8/P9/MN23apLK6OL1cRGTlypUqu+aaa1Rm3YtRP1hP/Rg7dqy51novYL2HtDK3a1n3M+tpEiJMMA811hMWUlJSavw6bvdna0q/2/tNy+jRo896T8GCrgsAAAAAAB/ReAMAAAAA4CMabwAAAAAAfETjDQAAAACAj0Jy+of1S/87d+4011qDUqy17777rufrW4Oj2rRpY679zW9+o7IuXbqorHHjxmZ98+bNVRYdHX2mLeIceu+991Q2efJkc601tMwaXOU2yGngwIEqswZMWQMERUSOHj2qMmvg2f/8z/+Y9evWrVPZwoULVXbPPfeY9ag5boOsFi1apLK8vDxz7ZIlS1Rm3V/d7k933HGHyqzz4DZsz9rXrl27VPbwww+b9W5DDFE7ioqKzDw7O/sc7wTwj/Xz1Ro6VRWtWrUy8+nTp6ts6NChKouNja3W9YHvO3HihJm//vrrnurj4+PN3OppQg2feAMAAAAA4CMabwAAAAAAfETjDQAAAACAj2i8AQAAAADwUdAMVysvL1dZSUmJufbkyZMqe/vtt821f/rTn1RmDfqprKw065s0aaKy5ORklU2YMMGsHzZsmMqsoQOBQMCsd8trkzUMTEQkLCzsHO+k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",
      "text/plain": [
       "<Figure size 1000x200 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get one batch of images and labels\n",
    "images, labels = next(iter(train_loader))\n",
    "\n",
    "# Show the first 6 images in the batch\n",
    "plt.figure(figsize=(10, 2))\n",
    "for i in range(6):\n",
    "    plt.subplot(1, 6, i+1)\n",
    "    plt.imshow(images[i].squeeze(), cmap='gray')\n",
    "    plt.title(f\"Label: {labels[i].item()}\")\n",
    "    plt.axis('off')\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79b17f78",
   "metadata": {},
   "source": [
    "### Model deployment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 316,
   "id": "3100dab7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[['/home/arpan/torchenv_learning/showcase_dataset/7_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/4.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/6.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/6_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/3.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/1_2.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/2.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/9.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/3_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/2_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/6_3.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/5.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/7_3.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/8_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/5_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/8.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/4_3.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/5_3.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/2_2.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/0.jpg'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/9_2.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/5_2.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/9_1.png'],\n",
       " ['/home/arpan/torchenv_learning/showcase_dataset/0_1.png']]"
      ]
     },
     "execution_count": 316,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "\n",
    "import random\n",
    "# Create a list of example inputs to our Gradio demo\n",
    "example_list = [[str(filepath)] for filepath in random.sample(glob(\"/home/arpan/torchenv_learning/showcase_dataset/*\"), k=25)]\n",
    "example_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 317,
   "id": "c52e50b3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "* Running on local URL:  http://127.0.0.1:7866\n",
      "* Running on public URL: https://d09e948d3cdff83a48.gradio.live\n",
      "\n",
      "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div><iframe src=\"https://d09e948d3cdff83a48.gradio.live\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": []
     },
     "execution_count": 317,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import gradio as gr\n",
    "\n",
    "# Create title, description and article strings\n",
    "title = \"Number Classifier Minimal\"\n",
    "description = \"An Image feature extractor computer vision model to classify images of handwritten digits.\"\n",
    "article = \"Created at [09. PyTorch Model Deployment](https://www.learnpytorch.io/09_pytorch_model_deployment/).\"\n",
    "\n",
    "# Create the Gradio demo\n",
    "demo = gr.Interface(fn=predict_number, # mapping function from input to output\n",
    "                    inputs=gr.Image(type=\"pil\"), # what are the inputs?\n",
    "                    outputs=[gr.Label(num_top_classes=10, label=\"Predictions\")],\n",
    "                    examples=example_list, \n",
    "                    title=title,\n",
    "                    description=description,\n",
    "                    article=article)\n",
    "\n",
    "# Launch the demo!\n",
    "demo.launch(debug=False, # print errors locally?\n",
    "            share=True) # generate a publically shareable URL?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7cc45dd",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "torchenv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}