{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader, Dataset\nfrom torchvision import transforms, models\nfrom torchvision.datasets import ImageFolder # Included for completeness, but CustomCassavaDataset is primarily used\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, f1_score, confusion_matrix\nimport pandas as pd\nimport numpy as np\nimport os\nfrom PIL import Image\nfrom tqdm import tqdm\nimport json \nimport shutil \n\n# --- Configuration ---\nclass Config:\n    # --- Dataset Paths ---\n    DATA_ROOT = '/kaggle/input/cassava-leaf-disease-classification' \n    TRAIN_CSV = os.path.join(DATA_ROOT, 'train.csv')\n    TRAIN_IMAGES_DIR = os.path.join(DATA_ROOT, 'train_images') \n    \n    TEST_CSV = os.path.join(DATA_ROOT, 'sample_submission.csv') # Path to sample_submission.csv for test image IDs\n    TEST_IMAGES_DIR = os.path.join(DATA_ROOT, 'test_images') # Path to test_images folder\n\n    # Directory for processed data or saved models (must be writable, e.g., /kaggle/working/)\n    PROCESSED_TRAIN_DIR = os.path.join('/kaggle/working', 'processed_train_images') \n\n    # --- Model and Training Parameters ---\n    IMAGE_SIZE = 384 # Input image size for the model\n    BATCH_SIZE = 32\n    NUM_EPOCHS = 10 # Training from scratch might require more epochs\n    LEARNING_RATE = 1e-4 # Training from scratch might require adjusted LR\n    NUM_CLASSES = 5 # Number of disease classes (0, 1, 2, 3, 4)\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    RANDOM_SEED = 42\n\n    # --- Feature Map Sizes for ASDA integration (based on IMAGE_SIZE=384 and ResNet50 architecture) ---\n    # These are approximate spatial dimensions after each ResNet layer\n    # ResNet50 input (e.g., 3x384x384) -> conv1/maxpool -> ~96x96\n    # layer1 output: 256 channels @ 96x96\n    # layer2 output: 512 channels @ 48x48\n    # layer3 output: 1024 channels @ 24x24\n    # layer4 output: 2048 channels @ 12x12\n    RESNET_FM_SIZES = {\n        'layer1': (96, 96), \n        'layer2': (48, 48), \n        'layer3': (24, 24), \n        'layer4': (12, 12)\n    }\n\n# Set random seeds for reproducibility\ntorch.manual_seed(Config.RANDOM_SEED)\nnp.random.seed(Config.RANDOM_SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(Config.RANDOM_SEED)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False \n\nprint(f\"Using device: {Config.DEVICE}\")\n\n# --- Data Preparation Utility (Optional - for ImageFolder structure) ---\ndef prepare_data_for_imagefolder(train_csv_path, train_images_dir, output_dir):\n    \"\"\"\n    Reads the train.csv and organizes images into class-specific subfolders\n    for torchvision.datasets.ImageFolder. This can consume significant disk space.\n    \"\"\"\n    if os.path.exists(output_dir) and len(os.listdir(output_dir)) > 0:\n        print(f\"'{output_dir}' already exists and is not empty. Skipping data preparation.\")\n        return\n\n    print(f\"Preparing data for ImageFolder structure in '{output_dir}'...\")\n    df = pd.read_csv(train_csv_path)\n\n    for class_id in range(Config.NUM_CLASSES):\n        os.makedirs(os.path.join(output_dir, str(class_id)), exist_ok=True)\n\n    for index, row in tqdm(df.iterrows(), total=len(df), desc=\"Organizing images\"):\n        img_filename = row['image_id']\n        label = str(row['label'])\n        \n        src_path = os.path.join(train_images_dir, img_filename)\n        dst_path = os.path.join(output_dir, label, img_filename)\n\n        if not os.path.exists(src_path):\n            print(f\"Warning: Image {src_path} not found. Skipping.\")\n            continue\n        \n        try:\n            shutil.copy2(src_path, dst_path)\n        except Exception as e:\n            print(f\"Error copying {src_path} to {dst_path}: {e}\")\n            \n    print(\"Data preparation complete.\")\n\n# --- Custom Dataset for Training and Validation (Reads from TRAIN_IMAGES_DIR) ---\nclass CustomCassavaDataset(Dataset):\n    def __init__(self, image_ids, labels, img_dir, transform=None):\n        self.image_ids = image_ids.tolist()\n        self.labels = labels.tolist()\n        self.img_dir = img_dir\n        self.transform = transform\n        \n        self.label_map = {label: i for i, label in enumerate(sorted(list(set(self.labels))))}\n        print(f\"Dataset initialized with {len(self.image_ids)} samples.\")\n        print(f\"Label map: {self.label_map}\")\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, idx):\n        img_name = self.image_ids[idx]\n        label = self.labels[idx]\n        img_path = os.path.join(self.img_dir, img_name)\n        \n        img = Image.open(img_path).convert('RGB')\n        \n        if self.transform:\n            img = self.transform(img)\n        \n        return img, self.label_map[label]\n\n# --- Custom Dataset for Test (Reads from TEST_IMAGES_DIR) ---\nclass TestCassavaDataset(Dataset):\n    def __init__(self, image_ids, img_dir, transform=None):\n        self.image_ids = image_ids\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, idx):\n        img_name = self.image_ids[idx]\n        img_path = os.path.join(self.img_dir, img_name)\n        img = Image.open(img_path).convert('RGB')\n        if self.transform:\n            img = self.transform(img)\n        return img, img_name # Return image tensor and its ID for submission\n\n# --- Data Transforms ---\ntrain_transforms = transforms.Compose([\n    transforms.Resize((Config.IMAGE_SIZE, Config.IMAGE_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # ImageNet means and stds\n])\n\nval_transforms = transforms.Compose([\n    transforms.Resize((Config.IMAGE_SIZE, Config.IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\n# --- CCIA Module Definition ---\nclass CCIA(nn.Module):\n    def __init__(self, channel, reduction=16):\n        super(CCIA, self).__init__()\n        self.avg_pool = nn.AdaptiveAvgPool2d(1)\n        self.fc1 = nn.Linear(channel, channel // reduction, bias=False)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc2 = nn.Linear(channel // reduction, channel, bias=False)\n        self.channel_interaction_conv = nn.Conv1d(1, 1, kernel_size=3, padding=1, bias=False) \n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        b, c, _, _ = x.size()\n        y = self.avg_pool(x).view(b, c) \n        y = self.fc1(y)\n        y = self.relu(y)\n        y = self.fc2(y) \n        \n        y = y.unsqueeze(1) # (b, 1, c)\n        y = self.channel_interaction_conv(y) \n        y = y.squeeze(1) # (b, c)\n\n        y = self.sigmoid(y).view(b, c, 1, 1) \n        return x * y.expand_as(x) \n\n# --- ASDA Module Definition ---\nclass ASDA(nn.Module):\n    def __init__(self, channel, input_H, input_W, reduction_ratio=4):\n        super(ASDA, self).__init__()\n        self.input_H = input_H\n        self.input_W = input_W\n\n        # Multi-branch Local Feature Extraction\n        self.conv_3x3 = nn.Conv2d(channel, channel // 2, kernel_size=3, padding=1, bias=False)\n        self.conv_5x5 = nn.Conv2d(channel, channel // 2, kernel_size=5, padding=2, bias=False)\n        self.relu = nn.ReLU(inplace=True)\n\n        # Feature Concatenation & Reduction\n        self.conv_1x1_reduce = nn.Conv2d(channel, 1, kernel_size=1, bias=False) \n\n        # Detail Enhancement & Spatial Reshaping\n        self.adaptive_pool = nn.AdaptiveAvgPool2d((4, 4)) \n\n        self.fc_spatial1 = nn.Linear(4 * 4, (4 * 4) // reduction_ratio, bias=False)\n        self.fc_spatial2 = nn.Linear((4 * 4) // reduction_ratio, input_H * input_W, bias=False)\n        \n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        b, c, h, w = x.size()\n\n        f_3x3 = self.relu(self.conv_3x3(x))\n        f_5x5 = self.relu(self.conv_5x5(x))\n        \n        f_local = torch.cat([f_3x3, f_5x5], dim=1) \n\n        f_spatial_pre = self.conv_1x1_reduce(f_local) \n\n        f_pooled = self.adaptive_pool(f_spatial_pre) \n        \n        f_pooled = f_pooled.view(b, -1) \n        \n        f_linear = self.relu(self.fc_spatial1(f_pooled))\n        spatial_weights = self.fc_spatial2(f_linear).view(b, 1, self.input_H, self.input_W) \n\n        spatial_weights = self.sigmoid(spatial_weights)\n        \n        return x * spatial_weights.expand_as(x)\n\n# --- NEW: Fusion Block for Parallel Attention and 1x1 Conv Fusion ---\nclass FusionBlock(nn.Module):\n    def __init__(self, in_channels, H, W, use_ccia=False, use_asda=False):\n        super(FusionBlock, self).__init__()\n        self.use_ccia = use_ccia\n        self.use_asda = use_asda\n        \n        # Instantiate attention modules based on flags\n        if self.use_ccia:\n            self.ccia = CCIA(channel=in_channels)\n        if self.use_asda:\n            self.asda = ASDA(channel=in_channels, input_H=H, input_W=W)\n            \n        # Determine output channels for concatenation\n        # Original features (in_channels) + CCIA output (in_channels) + ASDA output (in_channels)\n        concat_channels = in_channels \n        if self.use_ccia: concat_channels += in_channels\n        if self.use_asda: concat_channels += in_channels\n\n        # 1x1 Convolution for fusion if any attention module is used\n        if self.use_ccia or self.use_asda:\n            self.fusion_conv = nn.Conv2d(concat_channels, in_channels, kernel_size=1, bias=False)\n        else:\n            self.fusion_conv = nn.Identity() # If no attention, just pass through\n\n    def forward(self, x_original):\n        # Collect features to concatenate\n        features_to_concat = [x_original]\n        \n        x_ccia = x_original\n        if self.use_ccia:\n            x_ccia = self.ccia(x_original)\n            features_to_concat.append(x_ccia)\n            \n        x_asda = x_original\n        if self.use_asda:\n            x_asda = self.asda(x_original)\n            features_to_concat.append(x_asda)\n        \n        # Concatenate features\n        # If only original is present (no attention), it will be a list of one item, concat will still work\n        x_combined = torch.cat(features_to_concat, dim=1)\n        \n        # Apply fusion convolution\n        x_fused = self.fusion_conv(x_combined)\n        \n        return x_fused\n\n\n# --- Modified ResNet50 Classifier Model Definition (incorporates FusionBlock) ---\nclass ResNet50_Classifier(nn.Module):\n    # weights_init_type parameter controls backbone initialization\n    def __init__(self, num_classes=Config.NUM_CLASSES, use_ccia=False, use_asda=False, weights_init_type='imagenet'):\n        super(ResNet50_Classifier, self).__init__()\n        \n        # Initialize ResNet50 backbone\n        if weights_init_type == 'imagenet':\n            self.resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) \n            print(\"ResNet50 backbone initialized with ImageNet pretrained weights.\")\n        elif weights_init_type == 'random': \n            self.resnet = models.resnet50(weights=None) \n            print(\"ResNet50 backbone initialized with random weights (training from scratch).\")\n        else: \n            self.resnet = models.resnet50(weights=None)\n            print(f\"ResNet50 backbone initialized with random weights (unknown init type: {weights_init_type}).\")\n\n        self.use_ccia = use_ccia\n        self.use_asda = use_asda\n\n        # Remove original FC layer\n        self.resnet.fc = nn.Identity() \n\n        # --- Instantiate FusionBlocks for each ResNet layer output ---\n        # The channels for ResNet50 layers are 256, 512, 1024, 2048 respectively\n        self.fusion_block1 = FusionBlock(256, Config.RESNET_FM_SIZES['layer1'][0], Config.RESNET_FM_SIZES['layer1'][1], use_ccia, use_asda)\n        self.fusion_block2 = FusionBlock(512, Config.RESNET_FM_SIZES['layer2'][0], Config.RESNET_FM_SIZES['layer2'][1], use_ccia, use_asda)\n        self.fusion_block3 = FusionBlock(1024, Config.RESNET_FM_SIZES['layer3'][0], Config.RESNET_FM_SIZES['layer3'][1], use_ccia, use_asda)\n        self.fusion_block4 = FusionBlock(2048, Config.RESNET_FM_SIZES['layer4'][0], Config.RESNET_FM_SIZES['layer4'][1], use_ccia, use_asda)\n        \n        # Final classification layer\n        self.fc = nn.Linear(2048, num_classes) \n\n    def forward(self, x):\n        x = self.resnet.conv1(x)\n        x = self.resnet.bn1(x)\n        x = self.resnet.relu(x)\n        x = self.resnet.maxpool(x)\n\n        # Apply each layer and then its corresponding FusionBlock\n        x = self.resnet.layer1(x)\n        x = self.fusion_block1(x)\n\n        x = self.resnet.layer2(x)\n        x = self.fusion_block2(x)\n        \n        x = self.resnet.layer3(x)\n        x = self.fusion_block3(x)\n\n        x = self.resnet.layer4(x)\n        x = self.fusion_block4(x)\n\n        x = self.resnet.avgpool(x)\n        x = torch.flatten(x, 1)\n        x = self.fc(x)\n        return x\n\n# --- Training Function ---\ndef train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs, device, model_name=\"model\"):\n    best_val_accuracy = 0.0\n    model.to(device) \n    best_model_save_path = os.path.join('/kaggle/working', f\"{model_name}_best_model.pth\")\n\n    for epoch in range(num_epochs):\n        model.train() \n        running_loss = 0.0\n        correct_predictions = 0\n        total_samples = 0\n\n        for inputs, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Train]\"):\n            inputs, labels = inputs.to(device), labels.to(device)\n            optimizer.zero_grad() \n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n            loss.backward() \n            optimizer.step() \n\n            running_loss += loss.item() * inputs.size(0)\n            _, predicted = torch.max(outputs.data, 1)\n            total_samples += labels.size(0)\n            correct_predictions += (predicted == labels).sum().item()\n\n        epoch_loss = running_loss / total_samples\n        epoch_accuracy = correct_predictions / total_samples\n        print(f\"Epoch {epoch+1} Train Loss: {epoch_loss:.4f} Acc: {epoch_accuracy:.4f}\")\n\n        val_loss, val_accuracy, val_f1, _, _, _ = evaluate_model(model, val_loader, criterion, device)\n        print(f\"Epoch {epoch+1} Val Loss: {val_loss:.4f} Acc: {val_accuracy:.4f} F1: {val_f1:.4f}\")\n\n        if val_accuracy > best_val_accuracy:\n            best_val_accuracy = val_accuracy\n            torch.save(model.state_dict(), best_model_save_path)\n            print(f\"Saved best model with accuracy: {best_val_accuracy:.4f}\")\n    print(\"Training complete!\")\n\n# --- Evaluation Function ---\ndef evaluate_model(model, data_loader, criterion, device):\n    model.eval() \n    running_loss = 0.0\n    correct_predictions = 0\n    total_samples = 0\n    all_labels = []\n    all_predictions = []\n\n    with torch.no_grad(): \n        for inputs, labels in tqdm(data_loader, desc=\"Evaluating\"):\n            inputs, labels = inputs.to(device), labels.to(device)\n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n\n            running_loss += loss.item() * inputs.size(0)\n            _, predicted = torch.max(outputs.data, 1)\n\n            total_samples += labels.size(0)\n            correct_predictions += (predicted == labels).sum().item()\n\n            all_labels.extend(labels.cpu().numpy())\n            all_predictions.extend(predicted.cpu().numpy())\n\n    avg_loss = running_loss / total_samples\n    accuracy = correct_predictions / total_samples\n    f1 = f1_score(all_labels, all_predictions, average='macro') \n    cm = confusion_matrix(all_labels, all_predictions)\n    return avg_loss, accuracy, f1, all_labels, all_predictions, cm\n\n# --- Main Execution Block ---\nif __name__ == \"__main__\":\n    # --- Step 0: Data Loading and Splitting (for Training & Validation) ---\n    print(\"\\n--- Preparing Training and Validation Data ---\")\n    df_train = pd.read_csv(Config.TRAIN_CSV)\n    train_img_ids, val_img_ids, train_labels, val_labels = train_test_split(\n        df_train['image_id'], df_train['label'],\n        test_size=0.2, stratify=df_train['label'], random_state=Config.RANDOM_SEED\n    )\n\n    train_dataset = CustomCassavaDataset(\n        image_ids=train_img_ids,\n        labels=train_labels,\n        img_dir=Config.TRAIN_IMAGES_DIR, # Uses TRAIN_IMAGES_DIR for training data\n        transform=train_transforms\n    )\n    val_dataset = CustomCassavaDataset(\n        image_ids=val_img_ids,\n        labels=val_labels,\n        img_dir=Config.TRAIN_IMAGES_DIR, # Uses TRAIN_IMAGES_DIR for validation data\n        transform=val_transforms\n    )\n\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=Config.BATCH_SIZE,\n        shuffle=True,\n        num_workers=os.cpu_count() // 2, \n        pin_memory=True\n    )\n    val_loader = DataLoader(\n        val_dataset,\n        batch_size=Config.BATCH_SIZE * 2, \n        shuffle=False,\n        num_workers=os.cpu_count() // 2,\n        pin_memory=True\n    )\n    print(f\"Train samples: {len(train_dataset)}, Val samples: {len(val_dataset)}\")\n    print(f\"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}\")\n\n\n    # --- Step 1: Initialize and Train ResNet50 + CCIA + ASDA (DPFR-Net) Model ---\n    print(\"\\n--- Initializing and Training ResNet50 + CCIA + ASDA (DPFR-Net) Model ---\")\n    # Instantiate the model with CCIA and ASDA enabled, and random weights for backbone\n    # This now uses the FusionBlock for parallel attention and 1x1 conv fusion\n    model_dpfr_net = ResNet50_Classifier(num_classes=Config.NUM_CLASSES, use_ccia=True, use_asda=True, weights_init_type='random') \n    criterion_dpfr_net = nn.CrossEntropyLoss()\n    optimizer_dpfr_net = optim.Adam(model_dpfr_net.parameters(), lr=Config.LEARNING_RATE)\n\n    # Train the combined model\n    train_model(model_dpfr_net, train_loader, val_loader, criterion_dpfr_net, optimizer_dpfr_net, Config.NUM_EPOCHS, Config.DEVICE, model_name=\"resnet50_dpfr_net\")\n\n\n    # --- Step 2: Final Evaluation of ResNet50 + CCIA + ASDA (DPFR-Net) & Submission Generation ---\n    print(\"\\n--- Final Evaluation of ResNet50 + CCIA + ASDA (DPFR-Net) Model & Submission Generation ---\")\n    \n    # Instantiate the same model architecture for loading weights\n    # weights_init_type='random' to match how it was trained and avoid downloading ImageNet weights\n    best_model_dpfr_net = ResNet50_Classifier(num_classes=Config.NUM_CLASSES, use_ccia=True, use_asda=True, weights_init_type='random') \n    best_model_dpfr_net_path = os.path.join('/kaggle/working', \"resnet50_dpfr_net_best_model.pth\") \n\n    if not os.path.exists(best_model_dpfr_net_path):\n        print(f\"Error: Best ResNet50 + CCIA + ASDA (DPFR-Net) model not found at {best_model_dpfr_net_path}. Cannot proceed with evaluation or submission.\")\n    else:\n        best_model_dpfr_net.load_state_dict(torch.load(best_model_dpfr_net_path, map_location=Config.DEVICE))\n        best_model_dpfr_net.to(Config.DEVICE) \n        best_model_dpfr_net.eval() # Set model to evaluation mode\n\n        # --- Evaluate on Validation Set ---\n        print(\"\\n--- Evaluating ResNet50 + CCIA + ASDA (DPFR-Net) on Validation Set ---\")\n        val_loss_dpfr_net, val_accuracy_dpfr_net, val_f1_dpfr_net, _, _, cm_dpfr_net = evaluate_model(\n            best_model_dpfr_net, val_loader, criterion_dpfr_net, Config.DEVICE\n        )\n        print(f\"Best ResNet50 + CCIA + ASDA (DPFR-Net) Val Loss: {val_loss_dpfr_net:.4f}\")\n        print(f\"Best ResNet50 + CCIA + ASDA (DPFR-Net) Val Accuracy: {val_accuracy_dpfr_net:.4f}\")\n        print(f\"Best ResNet50 + CCIA + ASDA (DPFR-Net) Val F1-Score (Macro): {val_f1_dpfr_net:.4f}\")\n        print(\"Confusion Matrix:\\n\", cm_dpfr_net)\n\n        # --- Generate Submission File for Test Data ---\n        print(\"\\n--- Generating Submission File for ResNet50 + CCIA + ASDA (DPFR-Net) ---\")\n\n        test_transforms_submission = transforms.Compose([ \n            transforms.Resize((Config.IMAGE_SIZE, Config.IMAGE_SIZE)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ])\n\n        # Load test image IDs from sample_submission.csv\n        submission_df_template = pd.read_csv(Config.TEST_CSV)\n        test_image_ids = submission_df_template['image_id'].tolist()\n\n        # Create Test Dataset and DataLoader\n        test_dataset = TestCassavaDataset(\n            image_ids=test_image_ids,\n            img_dir=Config.TEST_IMAGES_DIR, # Uses Config.TEST_IMAGES_DIR for test data\n            transform=test_transforms_submission\n        )\n\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size=Config.BATCH_SIZE * 2, \n            shuffle=False,\n            num_workers=os.cpu_count() // 2,\n            pin_memory=True\n        )\n\n        all_test_predictions_dpfr_net = []\n\n        with torch.no_grad(): \n            for inputs, _ in tqdm(test_loader, desc=\"Predicting on test set with DPFR-Net\"): \n                inputs = inputs.to(Config.DEVICE)\n                outputs = best_model_dpfr_net(inputs) # Use the DPFR-Net model here\n                _, predicted = torch.max(outputs.data, 1)\n                all_test_predictions_dpfr_net.extend(predicted.cpu().numpy())\n\n        # Create the final submission DataFrame\n        submission_df_dpfr_net = pd.DataFrame({\n            'image_id': test_image_ids, \n            'label': all_test_predictions_dpfr_net\n        })\n\n        # Save the submission file to /kaggle/working/\n        submission_file_path_dpfr_net = os.path.join('/kaggle/working', 'submission.csv') # Unique name for DPFR-Net submission\n        submission_df_dpfr_net.to_csv(submission_file_path_dpfr_net, index=False)\n\n        print(f\"\\nSubmission file for ResNet50 + CCIA + ASDA (DPFR-Net) saved to: {submission_file_path_dpfr_net}\")\n        print(\"First 5 rows of submission_dpfr_net.csv:\")\n        print(submission_df_dpfr_net.head())\n\n    print(\"\\nResNet50 + CCIA + ASDA (DPFR-Net) experiment complete.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}