{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":87323,"databundleVersionId":9934427,"sourceType":"competition"}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div style='background-color: #e6f7ff; border: 4px solid #0078FF; border-radius: 12px; padding: 10px; text-align: center;'>\n<font size=\"+2\" color=\"#0078FF\">\n<b>🌟 Enjoyed this notebook? Please <span style='color: #FF4500;'>comment</span> & <span style='color: #32CD32;'>upvote</span>! 🌟</b>\n</font>\n</div>\n<div style='background-color: #fff0f5; border: 4px solid #FF69B4; border-radius: 12px; padding: 10px; text-align: center; margin-top: 10px;'>\n<font size=\"+2\" color=\"#FF69B4\">\n<b>💻 Explore more in my profile ➔\n<a href=\"https://www.kaggle.com/killa92/code\" style='color: #0078FF; text-decoration: none; font-weight: bold;'>Kaggle Notebooks</a> 💻</b>\n</font>\n</div>\n\n<div style='background-color: #f0fff0; border: 4px solid #32CD32; border-radius: 12px; padding: 10px; text-align: center; margin-top: 10px;'>\n<font size=\"+2\" color=\"#32CD32\">\n<b>🤝 Let's grow together and inspire each other! 🚀</b>\n</font>\n</div>","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"markdown","source":"<div style='background-color: #e6f7ff; border: 3px solid #0078FF; border-radius: 10px; padding: 8px; text-align: center;'>\n<font size=\"+2\" color=\"#0078FF\"><b>🗂️ 1. Custom Datasets & DataLoaders 🗂️</b></font>\n</div>","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom PIL import Image\nfrom torch.utils.data import random_split, Dataset, DataLoader\nfrom torchvision import transforms as T\n\ntorch.manual_seed(2025)\n\nclass CustomDataset(Dataset):\n    def __init__(self, root, transformations=None, data_type = \"train\", im_files=[\".png\", \".jpg\", \".jpeg\", \".bmp\"]):\n        \n        self.dirname, self.transformations = \"Train\" if data_type == \"train\" else \"Test\", transformations\n        self.im_paths = glob(f\"{root}/{self.dirname}/*{[im_file for im_file in im_files]}\")        \n\n        if data_type == \"train\": self.get_meta_data(); self.get_info()          \n            \n    def get_meta_data(self):\n        \n        meta_data = pd.read_csv(f\"{root}/train.csv\")\n        self.lbls = {im_id: lbl for im_id, lbl in zip(list(meta_data[\"image\"]), list(meta_data[\"label\"]))}        \n    \n    def get_info(self):\n\n        self.cls_names, self.cls_counts = {}, {}\n        count = 0\n        for im_path in self.im_paths:\n            class_name = self.get_class(im_path)\n            if class_name not in self.cls_names:\n                self.cls_names[class_name] = count\n                self.cls_counts[class_name] = 1\n                count += 1\n            else:\n                self.cls_counts[class_name] += 1\n    \n    def get_class(self, path): return self.lbls[os.path.basename(path)]\n    \n    def __len__(self): return len(self.im_paths)\n\n    def __getitem__(self, idx):\n        \n        im_path = self.im_paths[idx]\n        im = Image.open(im_path).convert(\"RGB\")\n        if self.transformations: im = self.transformations(im)\n        \n        if self.dirname == \"Train\": return im, self.cls_names[self.get_class(im_path)]\n        else: return im\n\n    @classmethod\n    def get_dls(cls, root, transformations, bs, split=[0.9, 0.1], ns=4):\n        \n        ds    = cls(root=root, transformations=transformations)\n        ts_ds = cls(root=root, transformations=transformations, data_type = \"test\")\n\n        total_len = len(ds)\n        tr_len = int(total_len * split[0])\n        vl_len = total_len - tr_len        \n\n        tr_ds, vl_ds = random_split(ds, [tr_len, vl_len])\n\n        tr_dl = DataLoader(tr_ds, batch_size=bs, shuffle=True, num_workers=ns)\n        val_dl = DataLoader(vl_ds, batch_size=bs, shuffle=False, num_workers=ns)\n        ts_dl = DataLoader(ts_ds, batch_size=1, shuffle=False, num_workers=ns)\n\n        return tr_dl, val_dl, ts_dl, ds.cls_names, ds.cls_counts\n\nroot = \"/kaggle/input/cidaut-ai-fake-scene-classification-2024\"\nmean, std, im_size, bs = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225], 224, 16\ntfs = T.Compose([T.Resize((im_size, im_size)), T.ToTensor(), T.Normalize(mean=mean, std=std)])\n\ntr_dl, val_dl, ts_dl, classes, cls_counts = CustomDataset.get_dls(root=root, transformations=tfs, bs=bs)\n\nprint(len(tr_dl)); print(len(val_dl)); print(len(ts_dl)); print(classes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:44:23.489525Z","iopub.execute_input":"2025-03-31T00:44:23.489738Z","iopub.status.idle":"2025-03-31T00:44:29.632863Z","shell.execute_reply.started":"2025-03-31T00:44:23.489718Z","shell.execute_reply":"2025-03-31T00:44:29.632076Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style='background-color: #ffe6f2; border: 3px solid #ff1493; border-radius: 10px; padding: 8px; text-align: center;'>\n<font size=\"+2\" color=\"#ff1493\"><b>📈 2. Data Analysis 📈</b></font>\n</div>","metadata":{}},{"cell_type":"code","source":"import numpy as np\nfrom matplotlib import pyplot as plt\nfrom torchvision import transforms as T\n\nclass Visualization:\n\n    def __init__(self, vis_datas, n_ims, rows, cmap=None, cls_names=None, cls_counts=None, t_type=\"rgb\"):\n        self.n_ims, self.rows = n_ims, rows\n        self.t_type, self.cmap = t_type, cmap\n        self.cls_names = cls_names\n        \n        data_names = [\"train\", \"val\", \"test\"]\n        self.vis_datas = {data_names[i]: vis_datas[i] for i in range(len(vis_datas))}\n        if isinstance(cls_counts, list): \n            self.analysis_datas = {data_names[i]: cls_counts[i] for i in range(len(cls_counts))}\n        else: \n            self.analysis_datas = {\"all\": cls_counts}\n\n    def tn2np(self, t):\n        gray_tfs = T.Compose([T.Normalize(mean=[0.], std=[1/0.5]), T.Normalize(mean=[-0.5], std=[1])])\n        rgb_tfs = T.Compose([T.Normalize(mean=[0., 0., 0.], std=[1/0.229, 1/0.224, 1/0.225]), \n                             T.Normalize(mean=[-0.485, -0.456, -0.406], std=[1., 1., 1.])])\n        \n        invTrans = gray_tfs if self.t_type == \"gray\" else rgb_tfs\n        \n        return (invTrans(t) * 255).detach().squeeze().cpu().permute(1, 2, 0).numpy().astype(np.uint8) if self.t_type == \"gray\" \\\n               else (invTrans(t) * 255).detach().cpu().permute(1, 2, 0).numpy().astype(np.uint8)\n\n    def plot(self, rows, cols, count, im, title=\"Original Image\"):\n        plt.subplot(rows, cols, count)\n        plt.imshow(self.tn2np(im))\n        plt.axis(\"off\")\n        plt.title(title)\n        return count + 1\n\n    def vis(self, data, save_name):\n        print(f\"{save_name.upper()} Data Visualization is in process...\\n\")\n        assert self.cmap in [\"rgb\", \"gray\"], \"Please choose rgb or gray cmap\"\n        cmap = \"viridis\" if self.cmap == \"rgb\" else None\n        cols = self.n_ims // self.rows\n        count = 1\n\n        plt.figure(figsize=(25, 20))\n        indices = [np.random.randint(low=0, high=len(data) - 1) for _ in range(self.n_ims)]\n\n        for idx, index in enumerate(indices):\n            if count == self.n_ims + 1: break\n            image, label = data.dataset[index]\n            plt.subplot(self.rows, self.n_ims // self.rows, idx + 1)\n\n            if cmap:\n                plt.imshow(self.tn2np(image), cmap=cmap)\n            else:\n                plt.imshow(self.tn2np(image))\n\n            plt.axis('off')\n            if self.cls_names is not None:\n                plt.title(f\"GT -> {self.cls_names[int(label)]}\")\n            else:\n                plt.title(f\"GT -> {label}\")\n        \n        plt.show()\n\n    def data_analysis(self, cls_counts, save_name):\n        print(\"Data analysis is in process...\\n\")\n        width, text_width, text_height = 0.7, 0.05, 2\n        cls_names = list(cls_counts.keys())\n        counts = list(cls_counts.values())\n        _, ax = plt.subplots(figsize=(20, 10))\n        indices = np.arange(len(counts))\n        ax.bar(indices, counts, width, color=\"darkorange\")\n        ax.set_xlabel(\"Class Names\", color=\"black\")\n        ax.set_xticklabels(cls_names, rotation = 90)\n        ax.set(xticks=indices, xticklabels=cls_names)\n        ax.set_ylabel(\"Data Counts\", color=\"black\")\n        ax.set_title(\"Dataset Class Imbalance Analysis\")\n        for i, v in enumerate(counts):\n            ax.text(i - text_width, v + text_height, str(v), color=\"royalblue\")\n    \n    def plot_pie_chart(self, cls_counts):\n        print(\"Generating pie chart...\\n\")\n        labels = list(cls_counts.keys())\n        sizes = list(cls_counts.values())\n        explode = [0.1] * len(labels)  # To highlight all slices equally (optional)\n        \n        plt.figure(figsize=(8, 8))\n        plt.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%', startangle=140, colors=plt.cm.tab20.colors)\n        plt.title(\"Class Distribution\")\n        plt.axis(\"equal\")  # Equal aspect ratio ensures the pie chart is circular\n        plt.show()\n\n    def visualization(self): [self.vis(data.dataset, save_name) for (save_name, data) in self.vis_datas.items()]\n        \n    def analysis(self): [self.data_analysis(data, save_name) for (save_name, data) in self.analysis_datas.items()]\n\n    def pie_chart(self): [self.plot_pie_chart(data) for data in self.analysis_datas.values()]\n        \nvis = Visualization(vis_datas = [tr_dl, val_dl], n_ims = 18, rows = 6, cmap = \"rgb\", cls_names = list(classes.keys()), cls_counts = cls_counts)\nvis.analysis()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:44:29.633744Z","iopub.execute_input":"2025-03-31T00:44:29.634024Z","iopub.status.idle":"2025-03-31T00:44:29.967890Z","shell.execute_reply.started":"2025-03-31T00:44:29.634002Z","shell.execute_reply":"2025-03-31T00:44:29.966990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"vis.pie_chart()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:44:29.968860Z","iopub.execute_input":"2025-03-31T00:44:29.969196Z","iopub.status.idle":"2025-03-31T00:44:30.080525Z","shell.execute_reply.started":"2025-03-31T00:44:29.969165Z","shell.execute_reply":"2025-03-31T00:44:30.079760Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style='background-color: #e6f7ff; border: 3px solid #007acc; border-radius: 10px; padding: 8px; text-align: center;'>\n<font size=\"+2\" color=\"#007acc\"><b>📊 3. Data Visualization 📊</b></font>\n</div>","metadata":{}},{"cell_type":"code","source":"vis.visualization()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:44:30.081890Z","iopub.execute_input":"2025-03-31T00:44:30.082231Z","iopub.status.idle":"2025-03-31T00:44:33.856820Z","shell.execute_reply.started":"2025-03-31T00:44:30.082210Z","shell.execute_reply":"2025-03-31T00:44:33.855723Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style='background-color: #f0f9e8; border: 3px solid #4CAF50; border-radius: 10px; padding: 8px; text-align: center;'>\n<font size=\"+2\" color=\"#4CAF50\"><b>✨ 4. AI Model Train and Validation ✨</b></font>\n</div>","metadata":{}},{"cell_type":"code","source":"import os, torchmetrics\nimport timm\nfrom tqdm import tqdm\n\nclass TrainValidation:\n    def __init__(self, model_name, classes, tr_dl, val_dl, device, save_dir=\"saved_models\", save_prefix=\"model\", lr=1e-3, epochs=50, patience=5, threshold=0.01, dev_mode = False):\n        self.model_name = model_name\n        self.classes = classes\n        self.tr_dl = tr_dl\n        self.val_dl = val_dl\n        self.save_dir = save_dir\n        self.save_prefix = save_prefix\n        self.lr = lr\n        self.epochs = epochs\n        self.patience = patience\n        self.threshold = threshold\n        self.dev_mode = dev_mode\n        self.device = device\n        self.model = timm.create_model(model_name, pretrained=True, num_classes=len(classes)).to(self.device)\n        self.loss_fn = torch.nn.CrossEntropyLoss()\n        self.optimizer = torch.optim.Adam(self.model.parameters(), lr=lr)\n        self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(self.optimizer, mode = \"min\", patience = 3, threshold = 0.001)\n        self.f1_metric = torchmetrics.F1Score(task=\"multiclass\", num_classes=len(classes)).to(self.device)\n\n        os.makedirs(save_dir, exist_ok=True)\n\n        self.best_loss = float(\"inf\")\n        self.best_acc = 0\n        self.not_improved = 0\n\n        self.tr_losses, self.val_losses = [], []\n        self.tr_accs, self.val_accs = [], []\n        self.tr_f1s, self.val_f1s = [], []\n\n    @staticmethod\n    def to_device(batch, device):\n        ims, gts = batch\n        return ims.to(device), gts.to(device)\n\n    def train_epoch(self):\n        self.model.train()\n        train_loss, train_acc = 0.0, 0.0\n        self.f1_metric.reset()\n\n        for idx, batch in tqdm(enumerate(self.tr_dl), desc=\"Training\"):\n            if self.dev_mode: \n                if idx == 1: break\n            \n            ims, gts = TrainValidation.to_device(batch = batch, device = self.device)            \n            \n            # Forward pass\n            preds = self.model(ims)\n            loss = self.loss_fn(preds, gts)\n            \n            # Backward pass\n            self.optimizer.zero_grad()\n            loss.backward()\n            self.optimizer.step()\n            \n            # Update metrics\n            train_loss += loss.item()\n            train_acc += (torch.argmax(preds, dim=1) == gts).sum().item()\n            self.f1_metric.update(preds, gts)\n\n        train_loss /= len(self.tr_dl)\n        train_acc /= len(self.tr_dl.dataset)\n        train_f1 = self.f1_metric.compute().item()\n        \n        self.tr_losses.append(train_loss)\n        self.tr_accs.append(train_acc)\n        self.tr_f1s.append(train_f1)\n\n        return train_loss, train_acc, train_f1\n\n    def validate_epoch(self):\n        self.model.eval()\n        val_loss, val_acc = 0.0, 0.0\n        self.f1_metric.reset()\n\n        with torch.no_grad():\n            for idx, batch in tqdm(enumerate(self.val_dl), desc=\"Validation\"):\n                if self.dev_mode:  \n                    if idx == 1: break                \n                ims, gts = TrainValidation.to_device(batch, device = self.device)\n                preds = self.model(ims)\n                loss = self.loss_fn(preds, gts)\n\n                # Update metrics\n                val_loss += loss.item()\n                val_acc += (torch.argmax(preds, dim=1) == gts).sum().item()\n                self.f1_metric.update(preds, gts)\n\n        val_loss /= len(self.val_dl)\n        val_acc /= len(self.val_dl.dataset)\n        val_f1 = self.f1_metric.compute().item()\n        self.scheduler.step(val_loss)\n\n        self.val_losses.append(val_loss)\n        self.val_accs.append(val_acc)\n        self.val_f1s.append(val_f1)\n\n        return val_loss, val_acc, val_f1\n\n    def save_best_model(self, val_f1, val_loss):\n        if val_f1 > self.best_acc + self.threshold:\n            self.best_acc = val_f1            \n            save_path = os.path.join(self.save_dir, f\"{self.save_prefix}_best_model.pth\")\n            torch.save(self.model.state_dict(), save_path)\n            print(f\"Best model saved with F1-Score: {self.best_acc:.3f}\")\n            self.not_improved = 0\n        else:\n            self.not_improved += 1\n            print(f\"No improvement for {self.not_improved} epoch(s).\")\n\n    def verbose(self, epoch, metric1, metric2, metric3, process = \"train\"):\n\n        print(f\"{epoch + 1}-epoch {process} process is completed!\\n\")\n        print(f\"{epoch + 1}-epoch {process} loss          -> {metric1:.3f}\")\n        print(f\"{epoch + 1}-epoch {process} accuracy      -> {metric2:.3f}\")\n        print(f\"{epoch + 1}-epoch {process} f1-score      -> {metric3:.3f}\\n\")\n    \n    def run(self):\n        print(\"Start training...\")\n\n        for epoch in range(self.epochs):\n            if self.dev_mode: \n                if epoch == 1: break \n                    \n            print(f\"\\nEpoch {epoch + 1}/{self.epochs}:\\n\")\n\n            train_loss, train_acc, train_f1 = self.train_epoch()\n            self.verbose(epoch, train_loss, train_acc, train_f1, process = \"train\")\n\n            val_loss, val_acc, val_f1 = self.validate_epoch()\n            self.verbose(epoch, val_loss, val_acc, val_f1, process = \"validation\")            \n\n            self.save_best_model(val_f1, val_loss)\n\n            if self.not_improved >= self.patience:\n                print(\"Early stopping triggered.\")\n                break\n\nmodel_name   = \"rexnet_150\"\nsave_prefix  = \"fake\"\nsave_dir     = \"saved_models\"\ndevice       = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\ntrainer = TrainValidation(model_name = model_name, device = device, \n                          save_prefix = save_prefix, classes = classes, \n                          patience = 5, tr_dl = tr_dl, val_dl = val_dl, dev_mode = False)\ntrainer.run()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:44:33.857891Z","iopub.execute_input":"2025-03-31T00:44:33.858218Z","iopub.status.idle":"2025-03-31T00:46:45.908894Z","shell.execute_reply.started":"2025-03-31T00:44:33.858182Z","shell.execute_reply":"2025-03-31T00:46:45.907914Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style='background-color: #fff0f5; border: 3px solid #FF69B4; border-radius: 10px; padding: 8px; text-align: center;'>\n<font size=\"+2\" color=\"#FF1493\"><b>😍 5. Learning Curves 😍</b></font>\n</div>","metadata":{}},{"cell_type":"code","source":"class PlotLearningCurves:\n    \n    def __init__(self, tr_losses, val_losses, tr_accs, val_accs, tr_f1s, val_f1s):\n        \n        self.tr_losses, self.val_losses, self.tr_accs, self.val_accs, self.tr_f1s, self.val_f1s = tr_losses, val_losses, tr_accs, val_accs, tr_f1s, val_f1s\n        \n    def plot(self, array_1, array_2, label_1, label_2, color_1, color_2):\n        \n        plt.plot(array_1, label = label_1, c = color_1)\n        plt.plot(array_2, label = label_2, c = color_2)\n        \n    def create_figure(self): \n        plt.figure(figsize = (10, 5))\n    \n    def decorate(self, ylabel, xlabel = \"Epochs\"): \n        \n        plt.xlabel(xlabel)\n        plt.ylabel(ylabel)\n        plt.xticks(ticks = np.arange(len(self.tr_accs)), labels = [i for i in range(1, len(self.tr_accs) + 1)])\n        plt.legend()\n        plt.show()      \n        \n    def visualize(self):\n        \n        # Figure 1: Loss Curves with more colorful colors\n        self.create_figure()\n        self.plot(array_1 = self.tr_losses, array_2 = self.val_losses, label_1 = \"Train Loss\", label_2 = \"Validation Loss\", color_1 = \"#FF6347\", color_2 = \"#3CB371\")  # Tomato and MediumSeaGreen\n        self.decorate(ylabel = \"Loss Values\")\n        \n        # Figure 2: Accuracy Curves with more colorful colors\n        self.create_figure()\n        self.plot(array_1 = self.tr_accs, array_2 = self.val_accs, label_1 = \"Train Accuracy\", label_2 = \"Validation Accuracy\", color_1 = \"#FF4500\", color_2 = \"#32CD32\")  # OrangeRed and LimeGreen\n        self.decorate(ylabel = \"Accuracy Scores\")\n        \n        # Figure 3: F1 Score Curves with more colorful colors\n        self.create_figure()\n        self.plot(array_1 = self.tr_f1s, array_2 = self.val_f1s, label_1 = \"Train F1 Score\", label_2 = \"Validation F1 Score\", color_1 = \"#8A2BE2\", color_2 = \"#DC143C\")  # BlueViolet and Crimson\n        self.decorate(ylabel = \"F1 Scores\")\n        \nPlotLearningCurves(tr_losses=trainer.tr_losses, val_losses=trainer.val_losses, tr_accs=trainer.tr_accs, val_accs=trainer.val_accs, tr_f1s=trainer.tr_f1s, val_f1s=trainer.val_f1s).visualize()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:46:45.910108Z","iopub.execute_input":"2025-03-31T00:46:45.910372Z","iopub.status.idle":"2025-03-31T00:46:46.556801Z","shell.execute_reply.started":"2025-03-31T00:46:45.910341Z","shell.execute_reply":"2025-03-31T00:46:46.555988Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style='background-color: #fff0db; border: 3px solid #FFA07A; border-radius: 10px; padding: 6px; text-align: center;'>\n<font size=\"+1.8\" color=\"#FF4500\"><b>✅ 6. Inference and AI Model Performance Analysis with GradCAM ✅</b></font>\n</div>","metadata":{}},{"cell_type":"code","source":"!pip install grad-cam==1.4.6\nimport cv2, random\nimport seaborn as sns\nfrom pytorch_grad_cam import GradCAM, GradCAMPlusPlus\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nfrom sklearn.metrics import confusion_matrix\nfrom tqdm import tqdm\n\nclass Denormalize:\n    def __init__(self, mean, std):\n        self.mean = mean\n        self.std = std\n\n    def __call__(self, tensor):\n        \"\"\"\n        Reverse the normalization applied to the image tensor.\n        \"\"\"\n        for t, m, s in zip(tensor, self.mean, self.std):\n            t.mul_(s).add_(m)\n        return tensor\n\nclass ModelInferenceVisualizer:\n    def __init__(self, model, device, class_names=None, im_size=224, mean = mean, std = std):\n        \n        self.denormalize = Denormalize(mean, std)\n        self.model = model\n        self.device = device\n        self.class_names = class_names\n        self.im_size = im_size\n        self.model.eval()  # Set model to evaluation mode\n\n    def tensor_to_image(self, tensor):\n        \"\"\"\n        Convert a normalized tensor to a denormalized image array.\n        \"\"\"\n        tensor = self.denormalize(tensor)  # Denormalize the tensor\n        tensor = tensor.permute(1, 2, 0)  # Convert from CxHxW to HxWxC\n        return (tensor.cpu().numpy() * 255).astype(np.uint8)\n\n    def generate_cam_visualization(self, image_tensor):\n        \"\"\"Generate GradCAM visualization.\"\"\"\n        cam = GradCAMPlusPlus(model=self.model, target_layers=[self.model.features[-1]], use_cuda=self.device == \"cuda\")\n        grayscale_cam = cam(input_tensor=image_tensor.unsqueeze(0))[0, :]\n        return grayscale_cam\n\n    def infer_and_visualize(self, test_dl, num_images=5, rows=2):\n        \"\"\"Perform inference and visualize predictions along with GradCAM.\"\"\"\n        preds, images, lbls, logitss = [], [], [], []\n        accuracy, count = 0, 1\n\n        with torch.no_grad():\n            for idx, batch in tqdm(enumerate(test_dl), desc=\"Inference\"): \n                im = batch.to(self.device)\n                logits = self.model(im)                  \n                pred_class = torch.argmax(logits, dim=1)\n                images.append(im[0])                \n                preds.append(pred_class[0].item())                \n\n        plt.figure(figsize=(20, 10))\n        indices = [random.randint(0, len(images) - 1) for _ in range(num_images)]\n        for idx, index in enumerate(indices):\n            # Convert and denormalize image\n            im = self.tensor_to_image(images[index].squeeze())\n            pred_idx = preds[index]            \n\n            # Display image\n            plt.subplot(rows, num_images // rows, count)\n            count += 1\n            plt.imshow(im, cmap=\"gray\")\n            plt.axis(\"off\")\n\n            # GradCAM visualization\n            grayscale_cam = self.generate_cam_visualization(images[index])\n            visualization = show_cam_on_image(im / 255, grayscale_cam, image_weight=0.4, use_rgb=True)\n            plt.imshow(cv2.resize(visualization, (self.im_size, self.im_size), interpolation=cv2.INTER_LINEAR), alpha=0.7, cmap='jet')\n            plt.axis(\"off\")           \n\n            # Title with GT and Prediction\n            if self.class_names:                \n                pred_name = self.class_names[pred_idx]                \n                plt.title(f\"PRED -> {pred_name}\")\n\nmodel = timm.create_model(model_name = model_name, pretrained = True, num_classes = len(classes)).to(device)\nmodel.load_state_dict(torch.load(f\"{save_dir}/{save_prefix}_best_model.pth\"))\ninference_visualizer = ModelInferenceVisualizer(\n    model=model,\n    device=device,\n    class_names=list(classes.keys()),  # List of class names\n    im_size=im_size\n)\n\ninference_visualizer.infer_and_visualize(ts_dl, num_images = 20, rows = 4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T00:47:40.465553Z","iopub.execute_input":"2025-03-31T00:47:40.465876Z","iopub.status.idle":"2025-03-31T00:47:49.887982Z","shell.execute_reply.started":"2025-03-31T00:47:40.465848Z","shell.execute_reply":"2025-03-31T00:47:49.886851Z"}},"outputs":[],"execution_count":null}]}