File size: 46,298 Bytes
d3375c4
1
{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":119874,"databundleVersionId":14372465},{"sourceType":"modelInstanceVersion","sourceId":641877,"databundleVersionId":14449807,"modelInstanceId":483952},{"sourceType":"modelInstanceVersion","sourceId":641700,"databundleVersionId":14448141,"modelInstanceId":483940},{"sourceType":"modelInstanceVersion","sourceId":641715,"databundleVersionId":14448271,"modelInstanceId":483952},{"sourceType":"modelInstanceVersion","sourceId":641870,"databundleVersionId":14449727,"modelInstanceId":483940}],"dockerImageVersionId":31192,"isInternetEnabled":true,"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 os\nfrom kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\nos.environ[\"HF_TOKEN\"] = user_secrets.get_secret(\"entire_hf_write_access\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T07:09:34.789435Z","iopub.execute_input":"2025-11-09T07:09:34.789735Z","iopub.status.idle":"2025-11-09T07:09:34.936024Z","shell.execute_reply.started":"2025-11-09T07:09:34.789712Z","shell.execute_reply":"2025-11-09T07:09:34.935031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q trackio kagglehub","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T08:22:02.202117Z","iopub.execute_input":"2025-11-12T08:22:02.202757Z","iopub.status.idle":"2025-11-12T08:22:08.986432Z","shell.execute_reply.started":"2025-11-12T08:22:02.202729Z","shell.execute_reply":"2025-11-12T08:22:08.985675Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# SECTION A: ENVIRONMENT & DEPENDENCIES\n# ============================================================================\n\n!pip install -q trackio kagglehub\n\nimport os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nimport pytorch_lightning as pl\nfrom pytorch_lightning.callbacks import ModelCheckpoint\nfrom sklearn.model_selection import train_test_split\nfrom kaggle_secrets import UserSecretsClient\nimport trackio\nimport kagglehub\n\n# ============================================================================\n# SECTION B: GLOBAL SETTINGS\n# ============================================================================\n\nclass PipelineSettings:\n    \"\"\"Manages all static parameters and paths for the project.\"\"\"\n    \n    def __init__(self):\n        # --- Data Source Paths ---\n        self.DATA_ROOT_DIR = \"/kaggle/input/sep-25-dl-gen-ai-nppe-1/face_dataset\"\n        self.TRAIN_CSV_PATH = f\"{self.DATA_ROOT_DIR}/train.csv\"\n        self.TEST_CSV_PATH = f\"{self.DATA_ROOT_DIR}/test.csv\"\n        \n        # --- Model Hyperparameters ---\n        self.INPUT_IMAGE_SIZE = 128\n        self.BATCH_SIZE = 128\n        self.LEARNING_RATE = 1e-3\n        self.NUM_EPOCHS = 10\n        self.AGE_LOSS_WEIGHT = 0.01\n        \n        # --- Model Architecture Selection ---\n        self.SCRATCH_MODEL_ID = \"scratch_cnn\"\n        self.FINETUNED_MODEL_ID = \"resnet_finetuned\"\n        \n        # --- System & Environment ---\n        self.NUM_DATALOADER_WORKERS = os.cpu_count()\n        \n        # --- Kaggle Model Upload Settings ---\n        self.KAGGLE_USERNAME = None  # Will be fetched automatically\n        self.MODEL_NAME = \"face-age-gender-predictor\"  # Your model name on Kaggle\n\n# Instantiate global settings\nsettings = PipelineSettings()\n\n# ============================================================================\n# SECTION C: IMAGE AUGMENTATION & PREPROCESSING\n# ============================================================================\n\nclass ImageAugmentor:\n    \"\"\"Defines image transformation pipelines for training and evaluation.\"\"\"\n    \n    def __init__(self, image_size):\n        self.image_size = image_size\n        self.normalization_params = {'mean': [0.485, 0.456, 0.406], 'std': [0.229, 0.224, 0.225]}\n\n    def get_training_transforms(self):\n        \"\"\"Returns an augmented transformation pipeline for the training set.\"\"\"\n        return transforms.Compose([\n            transforms.Resize((self.image_size, self.image_size)),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1),\n            transforms.ToTensor(),\n            transforms.Normalize(**self.normalization_params),\n        ])\n    \n    def get_inference_transforms(self):\n        \"\"\"Returns a standard transformation pipeline for validation and testing.\"\"\"\n        return transforms.Compose([\n            transforms.Resize((self.image_size, self.image_size)),\n            transforms.ToTensor(),\n            transforms.Normalize(**self.normalization_params),\n        ])\n\n# ============================================================================\n# SECTION D: CUSTOM DATASET LOADER\n# ============================================================================\n\nclass FaceImageDataset(Dataset):\n    \"\"\"Custom PyTorch Dataset to load facial images and their attributes.\"\"\"\n    \n    def __init__(self, metadata_df, image_dir, image_transform=None, is_prediction=False):\n        self.metadata = metadata_df\n        self.image_dir = image_dir\n        self.transform = image_transform\n        self.is_prediction = is_prediction\n    \n    def __len__(self):\n        return len(self.metadata)\n    \n    def __getitem__(self, idx):\n        row = self.metadata.iloc[idx]\n        image_path = os.path.join(self.image_dir, row['full_path'])\n        image = Image.open(image_path).convert(\"RGB\")\n        \n        if self.transform:\n            image = self.transform(image)\n        \n        if self.is_prediction:\n            return image\n        else:\n            gender_target = torch.tensor(row['gender'], dtype=torch.float32)\n            age_target = torch.tensor(row['age'], dtype=torch.float32)\n            return image, gender_target, age_target\n\n# ============================================================================\n# SECTION E: PYTORCH LIGHTNING DATA MODULE\n# ============================================================================\n\nclass FaceDataModule(pl.LightningDataModule):\n    \"\"\"Encapsulates all data loading and splitting logic.\"\"\"\n    \n    def __init__(self, config: PipelineSettings):\n        super().__init__()\n        self.cfg = config\n        self.augmentor = ImageAugmentor(self.cfg.INPUT_IMAGE_SIZE)\n        self.train_df, self.val_df = None, None\n\n    def prepare_data(self):\n        pass\n\n    def setup(self, stage=None):\n        if stage == 'fit' or stage is None:\n            full_train_data = pd.read_csv(self.cfg.TRAIN_CSV_PATH)\n            self.train_df, self.val_df = train_test_split(\n                full_train_data, \n                test_size=0.15, \n                random_state=42, \n                stratify=full_train_data['gender']\n            )\n            \n            self.train_dataset = FaceImageDataset(\n                self.train_df, \n                self.cfg.DATA_ROOT_DIR,\n                self.augmentor.get_training_transforms()\n            )\n            \n            self.val_dataset = FaceImageDataset(\n                self.val_df,\n                self.cfg.DATA_ROOT_DIR,\n                self.augmentor.get_inference_transforms()\n            )\n    \n    def train_dataloader(self):\n        return DataLoader(self.train_dataset, batch_size=self.cfg.BATCH_SIZE, shuffle=True, num_workers=self.cfg.NUM_DATALOADER_WORKERS)\n    \n    def val_dataloader(self):\n        return DataLoader(self.val_dataset, batch_size=self.cfg.BATCH_SIZE, num_workers=self.cfg.NUM_DATALOADER_WORKERS)\n\n# ============================================================================\n# SECTION F: ABSTRACT MODEL DEFINITION\n# ============================================================================\n\nclass AbstractFaceModel(pl.LightningModule):\n    \"\"\"A base class for face attribute models, defining the training loop and loss.\"\"\"\n    \n    def __init__(self, learning_rate, age_loss_weight):\n        super().__init__()\n        self.save_hyperparameters()\n        self.lr = learning_rate\n        self.age_weight = age_loss_weight\n        self.gender_loss_fn = nn.BCEWithLogitsLoss()\n        self.age_loss_fn = nn.MSELoss()\n\n    def _calculate_combined_loss(self, gender_preds, age_preds, gender_labels, age_labels):\n        gender_loss = self.gender_loss_fn(gender_preds.squeeze(), gender_labels)\n        age_loss = self.age_loss_fn(age_preds.squeeze(), age_labels)\n        total_loss = gender_loss + (age_loss * self.age_weight)\n        return total_loss\n\n    def training_step(self, batch, batch_idx):\n        images, gender_labels, age_labels = batch\n        gender_preds, age_preds = self(images)\n        loss = self._calculate_combined_loss(gender_preds, age_preds, gender_labels, age_labels)\n        self.log('train_loss', loss, on_step=True, on_epoch=True, prog_bar=True)\n        return loss\n    \n    def validation_step(self, batch, batch_idx):\n        images, gender_labels, age_labels = batch\n        gender_preds, age_preds = self(images)\n        loss = self._calculate_combined_loss(gender_preds, age_preds, gender_labels, age_labels)\n        self.log('val_loss', loss, on_epoch=True, prog_bar=True)\n    \n    def configure_optimizers(self):\n        optimizer = torch.optim.Adam(self.parameters(), lr=self.lr)\n        return optimizer\n\n# ============================================================================\n# SECTION G: CUSTOM SCRATCH CNN MODEL\n# ============================================================================\n\nclass ScratchCNNModel(AbstractFaceModel):\n    \"\"\"A custom Convolutional Neural Network built from scratch.\"\"\"\n    \n    def __init__(self, learning_rate, age_loss_weight):\n        super().__init__(learning_rate, age_loss_weight)\n        \n        def conv_block(in_f, out_f):\n            return nn.Sequential(\n                nn.Conv2d(in_f, out_f, kernel_size=3, padding=1, bias=False),\n                nn.BatchNorm2d(out_f),\n                nn.ReLU(inplace=True),\n                nn.MaxPool2d(kernel_size=2, stride=2)\n            )\n\n        self.feature_extractor = nn.Sequential(\n            conv_block(3, 32),\n            conv_block(32, 64),\n            conv_block(64, 128),\n            conv_block(128, 256),\n        )\n        \n        probe_tensor = torch.randn(1, 3, settings.INPUT_IMAGE_SIZE, settings.INPUT_IMAGE_SIZE)\n        flattened_size = self.feature_extractor(probe_tensor).view(1, -1).size(1)\n        \n        self.gender_head = nn.Linear(flattened_size, 1)\n        self.age_head = nn.Linear(flattened_size, 1)\n\n    def forward(self, x):\n        features = self.feature_extractor(x)\n        features = torch.flatten(features, 1)\n        gender_output = self.gender_head(features)\n        age_output = self.age_head(features)\n        return gender_output, age_output\n\n# ============================================================================\n# SECTION H: TRANSFER LEARNING MODEL (ResNet18)\n# ============================================================================\n\nclass FineTunedResNetModel(AbstractFaceModel):\n    \"\"\"A model using a pre-trained ResNet18 backbone for feature extraction.\"\"\"\n    \n    def __init__(self, learning_rate, age_loss_weight):\n        super().__init__(learning_rate, age_loss_weight)\n        resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        num_features = resnet.fc.in_features\n        \n        self.backbone = nn.Sequential(*list(resnet.children())[:-1])\n        \n        self.gender_head = nn.Linear(num_features, 1)\n        self.age_head = nn.Linear(num_features, 1)\n\n    def forward(self, x):\n        features = self.backbone(x)\n        features = torch.flatten(features, 1)\n        gender_output = self.gender_head(features)\n        age_output = self.age_head(features)\n        return gender_output, age_output\n\n# ============================================================================\n# SECTION I: EXECUTION & ORCHESTRATION\n# ============================================================================\n\nclass PipelineRunner:\n    \"\"\"Coordinates training and model upload workflow.\"\"\"\n    \n    def __init__(self, cfg: PipelineSettings):\n        self.cfg = cfg\n        self.data_module = FaceDataModule(cfg)\n        self._authenticate_trackio()\n        self._get_kaggle_username()\n\n    def _authenticate_trackio(self):\n        try:\n            secrets = UserSecretsClient()\n            hf_token = secrets.get_secret(\"HUGGINGFACE_TOKEN\")  # Store in Kaggle Secrets\n            os.environ[\"HF_TOKEN\"] = hf_token\n            print(\"โ†’ TrackIO authentication configured.\")\n        except Exception as e:\n            print(f\"โš  Could not configure TrackIO authentication: {e}\")\n\n    def _get_kaggle_username(self):\n        \"\"\"Fetch Kaggle username for model upload.\"\"\"\n        try:\n            user_info = kagglehub.whoami()\n            self.cfg.KAGGLE_USERNAME = user_info['username']\n            print(f\"โ†’ Kaggle username: {self.cfg.KAGGLE_USERNAME}\")\n        except Exception as e:\n            print(f\"โš  Could not fetch Kaggle username: {e}\")\n            self.cfg.KAGGLE_USERNAME = \"your-username\"  # Fallback\n\n    def _run_training_session(self, model_instance, model_name, tracking_id):\n        print(f\"\\n{'='*70}\\n๐Ÿš€ Starting Training: {model_name}\\n{'='*70}\")\n        \n        trackio.init(\n            project=\"25-t3-nppe1\",\n            name=tracking_id,\n            config={\n                \"space_id\": \"josondev/IITM-NPPE\",\n                \"learning_rate\": self.cfg.LEARNING_RATE,\n                \"epochs\": self.cfg.NUM_EPOCHS,\n                \"model_type\": model_name\n            }\n        )\n        \n        checkpoint_cb = ModelCheckpoint(\n            monitor='val_loss',\n            dirpath='/kaggle/working/',\n            filename=f'{model_name}-best-model',\n            save_top_k=1,\n            mode='min'\n        )\n        \n        trainer = pl.Trainer(\n            max_epochs=self.cfg.NUM_EPOCHS,\n            accelerator='gpu',\n            devices='auto',\n            strategy=\"ddp_notebook\",\n            callbacks=[checkpoint_cb]\n        )\n        \n        trainer.fit(model_instance, self.data_module)\n        print(f\"โœ… Best checkpoint saved at: {checkpoint_cb.best_model_path}\")\n        \n        final_val_loss = trainer.callback_metrics.get('val_loss', torch.tensor(0.0)).item()\n        trackio.log({\"final_validation_loss\": final_val_loss})\n        trackio.finish()\n        \n        # Upload to Kaggle Hub\n        self._upload_to_kaggle_hub(checkpoint_cb.best_model_path, model_name, final_val_loss)\n        \n        del model_instance, trainer, checkpoint_cb\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    def _upload_to_kaggle_hub(self, checkpoint_path, model_name, val_loss):\n        \"\"\"Upload trained checkpoint to Kaggle Models.\"\"\"\n        print(f\"\\n{'='*70}\\n๐Ÿ“ค Uploading {model_name} to Kaggle Hub...\\n{'='*70}\")\n        \n        try:\n            # Define handle: username/model/framework/variation\n            framework = \"pyTorch\"\n            variation = model_name.replace(\"_\", \"-\")  # e.g., \"scratch-cnn\" or \"resnet-finetuned\"\n            handle = f\"{self.cfg.KAGGLE_USERNAME}/{self.cfg.MODEL_NAME}/{framework}/{variation}\"\n            \n            # Create a directory with checkpoint and metadata\n            upload_dir = f\"/kaggle/working/{model_name}_upload\"\n            os.makedirs(upload_dir, exist_ok=True)\n            \n            # Copy checkpoint\n            import shutil\n            shutil.copy(checkpoint_path, os.path.join(upload_dir, \"model.ckpt\"))\n            \n            # Create a README (optional but recommended)\n            with open(os.path.join(upload_dir, \"README.md\"), \"w\") as f:\n                f.write(f\"# {model_name.replace('_', ' ').title()} Model\\n\\n\")\n                f.write(f\"**Validation Loss:** {val_loss:.4f}\\n\\n\")\n                f.write(f\"**Framework:** PyTorch Lightning\\n\\n\")\n                f.write(f\"**Task:** Gender classification (binary) + Age regression\\n\\n\")\n                f.write(f\"Trained on face dataset with {self.cfg.NUM_EPOCHS} epochs.\\n\")\n            \n            # Upload using kagglehub\n            version_notes = f\"Val loss: {val_loss:.4f}, Epochs: {self.cfg.NUM_EPOCHS}\"\n            kagglehub.model_upload(\n                handle=handle,\n                local_model_dir=upload_dir,\n                version_notes=version_notes,\n                license_name=\"Apache 2.0\"\n            )\n            \n            print(f\"โœ… Model uploaded successfully!\")\n            print(f\"   View at: https://www.kaggle.com/models/{self.cfg.KAGGLE_USERNAME}/{self.cfg.MODEL_NAME}/{framework}/{variation}\")\n            \n        except Exception as e:\n            print(f\"โš  Upload failed: {e}\")\n            print(\"   You can manually upload from /kaggle/working/ via Kaggle UI or dataset creation.\")\n\n    def execute(self):\n        \"\"\"Main entry point: Training only (no inference).\"\"\"\n        print(\"\\n*** MODE: TRAINING + UPLOAD ***\")\n        \n        # Train Scratch Model\n        scratch_model = ScratchCNNModel(self.cfg.LEARNING_RATE, self.cfg.AGE_LOSS_WEIGHT)\n        self._run_training_session(scratch_model, \"scratch_cnn\", \"BaselineCNN-v2\")\n\n        # Train Finetuned Model\n        finetuned_model = FineTunedResNetModel(self.cfg.LEARNING_RATE, self.cfg.AGE_LOSS_WEIGHT)\n        self._run_training_session(finetuned_model, \"resnet_finetuned\", \"TransferResNet-v2\")\n        \n        print(\"\\n๐ŸŽ‰ Training and upload pipeline completed!\")\n        print(\"   Both models are now available on Kaggle Models for inference.\")\n\n# ============================================================================\n# SECTION J: SCRIPT EXECUTION\n# ============================================================================\n\nif __name__ == \"__main__\":\n    pipeline = PipelineRunner(settings)\n    pipeline.execute()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T08:22:08.988265Z","iopub.execute_input":"2025-11-12T08:22:08.988499Z","iopub.status.idle":"2025-11-12T08:22:09.691837Z","shell.execute_reply.started":"2025-11-12T08:22:08.988475Z","shell.execute_reply":"2025-11-12T08:22:09.690620Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls -lh /kaggle/working | grep -i submission","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T08:22:09.692226Z","iopub.status.idle":"2025-11-12T08:22:09.692460Z","shell.execute_reply.started":"2025-11-12T08:22:09.692346Z","shell.execute_reply":"2025-11-12T08:22:09.692357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# INFERENCE NOTEBOOK: Download Models from Kaggle Hub & Generate Submissions\n# ============================================================================\n# This notebook loads pre-trained models from Kaggle Models and creates\n# submission_scratch.csv and submission_finetuned.csv for competition upload.\n\n!pip install -q kagglehub\n\nimport os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nimport pytorch_lightning as pl\nimport kagglehub\n\n# ============================================================================\n# SECTION A: SETTINGS & PATHS\n# ============================================================================\n\nclass InferenceSettings:\n    \"\"\"Configuration for inference pipeline.\"\"\"\n    \n    def __init__(self):\n        # Data paths\n        self.DATA_ROOT_DIR = \"/kaggle/input/sep-25-dl-gen-ai-nppe-1/face_dataset\"\n        self.TEST_CSV_PATH = f\"{self.DATA_ROOT_DIR}/test.csv\"\n        \n        # Model parameters (must match training)\n        self.INPUT_IMAGE_SIZE = 128\n        self.BATCH_SIZE = 128\n        self.LEARNING_RATE = 1e-3\n        self.AGE_LOSS_WEIGHT = 0.01\n        \n        # Kaggle Model Hub handles (UPDATE with your username)\n        self.KAGGLE_USERNAME = \"rexjosondeva\"  # Replace with your Kaggle username\n        self.MODEL_NAME = \"face-age-gender-predictor\"\n        self.SCRATCH_HANDLE = f\"{self.KAGGLE_USERNAME}/{self.MODEL_NAME}/pyTorch/scratch-cnn\"\n        self.FINETUNED_HANDLE = f\"{self.KAGGLE_USERNAME}/{self.MODEL_NAME}/pyTorch/resnet-finetuned\"\n        \n        # System\n        self.NUM_WORKERS = 2  # Reduce for stability\n\nsettings = InferenceSettings()\n\n# ============================================================================\n# SECTION B: DATA LOADING\n# ============================================================================\n\nclass ImageAugmentor:\n    \"\"\"Inference-time image transforms (no augmentation).\"\"\"\n    def __init__(self, image_size=128):\n        self.transform = transforms.Compose([\n            transforms.Resize((image_size, image_size)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n\nclass FaceImageTestDataset(Dataset):\n    \"\"\"Test dataset loader.\"\"\"\n    def __init__(self, metadata_df, image_dir, transform=None):\n        self.metadata = metadata_df\n        self.image_dir = image_dir\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.metadata)\n    \n    def __getitem__(self, idx):\n        row = self.metadata.iloc[idx]\n        image_path = os.path.join(self.image_dir, row['full_path'])\n        image = Image.open(image_path).convert(\"RGB\")\n        if self.transform:\n            image = self.transform(image)\n        return image\n\n# Load test data\ntest_df = pd.read_csv(settings.TEST_CSV_PATH)\naugmentor = ImageAugmentor(settings.INPUT_IMAGE_SIZE)\ntest_dataset = FaceImageTestDataset(test_df, settings.DATA_ROOT_DIR, augmentor.transform)\ntest_loader = DataLoader(test_dataset, batch_size=settings.BATCH_SIZE, shuffle=False, num_workers=settings.NUM_WORKERS)\n\nprint(f\"โœ… Test dataset loaded: {len(test_df)} samples\")\n\n# ============================================================================\n# SECTION C: MODEL DEFINITIONS (Must Match Training)\n# ============================================================================\n\nclass ScratchCNNModel(pl.LightningModule):\n    \"\"\"Custom CNN from scratch.\"\"\"\n    def __init__(self, learning_rate, age_loss_weight):\n        super().__init__()\n        self.save_hyperparameters()\n        \n        def conv_block(in_f, out_f):\n            return nn.Sequential(\n                nn.Conv2d(in_f, out_f, kernel_size=3, padding=1, bias=False),\n                nn.BatchNorm2d(out_f),\n                nn.ReLU(inplace=True),\n                nn.MaxPool2d(kernel_size=2, stride=2)\n            )\n        \n        self.feature_extractor = nn.Sequential(\n            conv_block(3, 32),\n            conv_block(32, 64),\n            conv_block(64, 128),\n            conv_block(128, 256),\n        )\n        \n        probe = torch.randn(1, 3, 128, 128)\n        flat_size = self.feature_extractor(probe).view(1, -1).size(1)\n        self.gender_head = nn.Linear(flat_size, 1)\n        self.age_head = nn.Linear(flat_size, 1)\n    \n    def forward(self, x):\n        features = self.feature_extractor(x)\n        features = torch.flatten(features, 1)\n        return self.gender_head(features), self.age_head(features)\n\nclass FineTunedResNetModel(pl.LightningModule):\n    \"\"\"Fine-tuned ResNet18 model.\"\"\"\n    def __init__(self, learning_rate, age_loss_weight):\n        super().__init__()\n        self.save_hyperparameters()\n        resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        num_features = resnet.fc.in_features\n        self.backbone = nn.Sequential(*list(resnet.children())[:-1])\n        self.gender_head = nn.Linear(num_features, 1)\n        self.age_head = nn.Linear(num_features, 1)\n    \n    def forward(self, x):\n        features = self.backbone(x)\n        features = torch.flatten(features, 1)\n        return self.gender_head(features), self.age_head(features)\n\n# ============================================================================\n# SECTION D: DOWNLOAD MODELS FROM KAGGLE HUB\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"๐Ÿ“ฅ Downloading Models from Kaggle Hub...\")\nprint(\"=\"*70)\n\n# Download scratch model\nprint(f\"\\nโ†’ Downloading Scratch CNN from: {settings.SCRATCH_HANDLE}\")\nscratch_model_dir = kagglehub.model_download(settings.SCRATCH_HANDLE)\nscratch_ckpt_path = os.path.join(scratch_model_dir, \"model.ckpt\")\nprint(f\"   โœ… Downloaded to: {scratch_ckpt_path}\")\n\n# Download finetuned model\nprint(f\"\\nโ†’ Downloading Fine-Tuned ResNet from: {settings.FINETUNED_HANDLE}\")\nfinetuned_model_dir = kagglehub.model_download(settings.FINETUNED_HANDLE)\nfinetuned_ckpt_path = os.path.join(finetuned_model_dir, \"model.ckpt\")\nprint(f\"   โœ… Downloaded to: {finetuned_ckpt_path}\")\n\n# Verify files exist\nassert os.path.exists(scratch_ckpt_path), f\"Scratch checkpoint not found: {scratch_ckpt_path}\"\nassert os.path.exists(finetuned_ckpt_path), f\"Finetuned checkpoint not found: {finetuned_ckpt_path}\"\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"\\nโœ… Using device: {device}\")\n\n# ============================================================================\n# SECTION E: INFERENCE - SCRATCH MODEL\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"๐Ÿ”ฎ Running Scratch CNN Predictions...\")\nprint(\"=\"*70)\n\nscratch_model = ScratchCNNModel.load_from_checkpoint(\n    checkpoint_path=scratch_ckpt_path,\n    learning_rate=settings.LEARNING_RATE,\n    age_loss_weight=settings.AGE_LOSS_WEIGHT\n)\nscratch_model.eval()\nscratch_model.to(device)\n\nscratch_gender_logits, scratch_age_outputs = [], []\nwith torch.no_grad():\n    for images in test_loader:\n        images = images.to(device)\n        gender_pred, age_pred = scratch_model(images)\n        scratch_gender_logits.append(gender_pred.cpu())\n        scratch_age_outputs.append(age_pred.cpu())\n\n# Post-process predictions\nscratch_gender_logits = torch.cat(scratch_gender_logits).numpy().reshape(-1)\nscratch_age_outputs = torch.cat(scratch_age_outputs).numpy().reshape(-1)\nscratch_gender_probs = 1 / (1 + np.exp(-scratch_gender_logits))\nscratch_gender_preds = (scratch_gender_probs > 0.5).astype(int)\nscratch_age_preds = np.clip(scratch_age_outputs, 0, 120)\n\n# Save submission\nsubmission_scratch = pd.DataFrame({\n    'id': test_df['id'].values,\n    'gender': scratch_gender_preds,\n    'age': scratch_age_preds\n})\nsubmission_scratch.to_csv('/kaggle/working/submission_scratch.csv', index=False)\n\nprint(\"โœ… Scratch CNN submission saved: submission_scratch.csv\")\nprint(submission_scratch.head())\nprint(f\"Shape: {submission_scratch.shape}\")\n\n# Free memory\ndel scratch_model, scratch_gender_logits, scratch_age_outputs\ngc.collect()\ntorch.cuda.empty_cache()\n\n# ============================================================================\n# SECTION F: INFERENCE - FINETUNED MODEL\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"๐Ÿ”ฎ Running Fine-Tuned ResNet Predictions...\")\nprint(\"=\"*70)\n\nfinetuned_model = FineTunedResNetModel.load_from_checkpoint(\n    checkpoint_path=finetuned_ckpt_path,\n    learning_rate=settings.LEARNING_RATE,\n    age_loss_weight=settings.AGE_LOSS_WEIGHT\n)\nfinetuned_model.eval()\nfinetuned_model.to(device)\n\nfinetuned_gender_logits, finetuned_age_outputs = [], []\nwith torch.no_grad():\n    for images in test_loader:\n        images = images.to(device)\n        gender_pred, age_pred = finetuned_model(images)\n        finetuned_gender_logits.append(gender_pred.cpu())\n        finetuned_age_outputs.append(age_pred.cpu())\n\n# Post-process predictions\nfinetuned_gender_logits = torch.cat(finetuned_gender_logits).numpy().reshape(-1)\nfinetuned_age_outputs = torch.cat(finetuned_age_outputs).numpy().reshape(-1)\nfinetuned_gender_probs = 1 / (1 + np.exp(-finetuned_gender_logits))\nfinetuned_gender_preds = (finetuned_gender_probs > 0.5).astype(int)\nfinetuned_age_preds = np.clip(finetuned_age_outputs, 0, 120)\n\n# Save submission\nsubmission_finetuned = pd.DataFrame({\n    'id': test_df['id'].values,\n    'gender': finetuned_gender_preds,\n    'age': finetuned_age_preds\n})\nsubmission_finetuned.to_csv('/kaggle/working/submission_finetuned.csv', index=False)\n\nprint(\"โœ… Fine-Tuned ResNet submission saved: submission_finetuned.csv\")\nprint(submission_finetuned.head())\nprint(f\"Shape: {submission_finetuned.shape}\")\n\n# Free memory\ndel finetuned_model, finetuned_gender_logits, finetuned_age_outputs\ngc.collect()\ntorch.cuda.empty_cache()\n\n# ============================================================================\n# SECTION G: OPTIONAL ENSEMBLE\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"๐ŸŽฏ Generating Ensemble Submission (Average of Both Models)...\")\nprint(\"=\"*70)\n\nensemble_gender = ((scratch_gender_preds + finetuned_gender_preds) / 2 > 0.5).astype(int)\nensemble_age = (scratch_age_preds + finetuned_age_preds) / 2\n\nsubmission_ensemble = pd.DataFrame({\n    'id': test_df['id'].values,\n    'gender': ensemble_gender,\n    'age': ensemble_age\n})\nsubmission_ensemble.to_csv('/kaggle/working/submission_ensemble.csv', index=False)\n\nprint(\"โœ… Ensemble submission saved: submission_ensemble.csv\")\nprint(submission_ensemble.head())\nprint(f\"Shape: {submission_ensemble.shape}\")\n\n# ============================================================================\n# FINAL SUMMARY\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"๐ŸŽ‰ ALL SUBMISSIONS GENERATED SUCCESSFULLY!\")\nprint(\"=\"*70)\nprint(\"Files in /kaggle/working/:\")\nprint(\"  - submission_scratch.csv\")\nprint(\"  - submission_finetuned.csv\")\nprint(\"  - submission_ensemble.csv\")\nprint(\"\\nDownload from Output panel and submit to competition!\")\nprint(\"=\"*70)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:45:51.133717Z","iopub.execute_input":"2025-11-12T10:45:51.134423Z","iopub.status.idle":"2025-11-12T10:47:19.922939Z","shell.execute_reply.started":"2025-11-12T10:45:51.134393Z","shell.execute_reply":"2025-11-12T10:47:19.922179Z"}},"outputs":[{"name":"stdout","text":"โœ… Test dataset loaded: 8677 samples\n\n======================================================================\n๐Ÿ“ฅ Downloading Models from Kaggle Hub...\n======================================================================\n\nโ†’ Downloading Scratch CNN from: rexjosondeva/face-age-gender-predictor/pyTorch/scratch-cnn\nMounting files to /kaggle/input/face-age-gender-predictor/pytorch/scratch-cnn/2...\n   โœ… Downloaded to: /kaggle/input/face-age-gender-predictor/pytorch/scratch-cnn/2/model.ckpt\n\nโ†’ Downloading Fine-Tuned ResNet from: rexjosondeva/face-age-gender-predictor/pyTorch/resnet-finetuned\n   โœ… Downloaded to: /kaggle/input/face-age-gender-predictor/pytorch/resnet-finetuned/2/model.ckpt\n\nโœ… Using device: cuda\n\n======================================================================\n๐Ÿ”ฎ Running Scratch CNN Predictions...\n======================================================================\nโœ… Scratch CNN submission saved: submission_scratch.csv\n   id  gender        age\n0   0       1  30.513975\n1   1       0  22.544374\n2   2       1  29.283455\n3   3       1  29.424118\n4   4       1  51.745323\nShape: (8677, 3)\n\n======================================================================\n๐Ÿ”ฎ Running Fine-Tuned ResNet Predictions...\n======================================================================\nโœ… Fine-Tuned ResNet submission saved: submission_finetuned.csv\n   id  gender        age\n0   0       1  35.028824\n1   1       0  27.688492\n2   2       1  35.269264\n3   3       1  28.340361\n4   4       1  64.935043\nShape: (8677, 3)\n\n======================================================================\n๐ŸŽฏ Generating Ensemble Submission (Average of Both Models)...\n======================================================================\nโœ… Ensemble submission saved: submission_ensemble.csv\n   id  gender        age\n0   0       1  32.771400\n1   1       0  25.116432\n2   2       1  32.276360\n3   3       1  28.882240\n4   4       1  58.340183\nShape: (8677, 3)\n\n======================================================================\n๐ŸŽ‰ ALL SUBMISSIONS GENERATED SUCCESSFULLY!\n======================================================================\nFiles in /kaggle/working/:\n  - submission_scratch.csv\n  - submission_finetuned.csv\n  - submission_ensemble.csv\n\nDownload from Output panel and submit to competition!\n======================================================================\n","output_type":"stream"}],"execution_count":15},{"cell_type":"code","source":"# ============================================================================\n# SCRATCH CNN INFERENCE (Fixed Layer Names)\n# ============================================================================\n\nimport os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport pytorch_lightning as pl\n\n# Settings\nclass Config:\n    DATA_ROOT = \"/kaggle/input/sep-25-dl-gen-ai-nppe-1/face_dataset\"\n    TEST_CSV = f\"{DATA_ROOT}/test.csv\"\n    IMG_SIZE = 128\n    BATCH = 128\n    LR = 1e-3\n    AGE_WEIGHT = 0.01\n    WORKERS = 2\n\ncfg = Config()\n\n# Auto-detect checkpoint\nprint(\"=\"*70)\nprint(\"๐Ÿ” Searching for scratch model checkpoint...\")\nprint(\"=\"*70)\n\ncfg.CHECKPOINT = None\nfor root, dirs, files in os.walk(\"/kaggle/input/\"):\n    for file in files:\n        if file == \"model.ckpt\" and \"scratch\" in root.lower():\n            cfg.CHECKPOINT = os.path.join(root, file)\n            print(f\"โœ… Found: {cfg.CHECKPOINT}\")\n            break\n    if cfg.CHECKPOINT:\n        break\n\nif cfg.CHECKPOINT is None:\n    raise FileNotFoundError(\"Add Kaggle Model: face-age-gender-predictor (scratch-cnn) via 'Add Input'\")\n\n# Data Pipeline\nclass ImageTransform:\n    def __init__(self, size=128):\n        self.transform = transforms.Compose([\n            transforms.Resize((size, size)),\n            transforms.ToTensor(),\n            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n        ])\n\nclass TestDataset(Dataset):\n    def __init__(self, df, root, tfm):\n        self.df, self.root, self.tfm = df, root, tfm\n    def __len__(self): return len(self.df)\n    def __getitem__(self, i):\n        img = Image.open(os.path.join(self.root, self.df.iloc[i]['full_path'])).convert(\"RGB\")\n        return self.tfm(img)\n\ntest_df = pd.read_csv(cfg.TEST_CSV)\ntest_ds = TestDataset(test_df, cfg.DATA_ROOT, ImageTransform(cfg.IMG_SIZE).transform)\ntest_dl = DataLoader(test_ds, batch_size=cfg.BATCH, shuffle=False, num_workers=cfg.WORKERS)\nprint(f\"โœ… Test: {len(test_df)} samples\")\n\n# Model (FIXED: Match training architecture with \"feature_extractor\")\nclass ScratchCNN(pl.LightningModule):\n    def __init__(self, lr, age_w):\n        super().__init__()\n        self.save_hyperparameters()\n        \n        def conv_block(in_c, out_c):\n            return nn.Sequential(\n                nn.Conv2d(in_c, out_c, 3, padding=1, bias=False),\n                nn.BatchNorm2d(out_c),\n                nn.ReLU(inplace=True),\n                nn.MaxPool2d(2, 2)\n            )\n        \n        # MUST be named \"feature_extractor\" to match checkpoint\n        self.feature_extractor = nn.Sequential(\n            conv_block(3, 32),\n            conv_block(32, 64),\n            conv_block(64, 128),\n            conv_block(128, 256),\n        )\n        \n        probe = torch.randn(1, 3, 128, 128)\n        flat_size = self.feature_extractor(probe).view(1, -1).size(1)\n        self.gender_head = nn.Linear(flat_size, 1)\n        self.age_head = nn.Linear(flat_size, 1)\n    \n    def forward(self, x):\n        features = torch.flatten(self.feature_extractor(x), 1)\n        return self.gender_head(features), self.age_head(features)\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"โœ… Device: {device}\\n\")\n\n# Inference\nprint(\"=\"*70)\nprint(\"๐Ÿ”ฎ Scratch CNN Predictions...\")\nprint(\"=\"*70)\n\nmodel = ScratchCNN.load_from_checkpoint(cfg.CHECKPOINT, lr=cfg.LR, age_w=cfg.AGE_WEIGHT)\nmodel.eval().to(device)\n\ng_logits, a_outputs = [], []\nwith torch.no_grad():\n    for imgs in test_dl:\n        g, a = model(imgs.to(device))\n        g_logits.append(g.cpu())\n        a_outputs.append(a.cpu())\n\ng_logits = torch.cat(g_logits).numpy().flatten()\na_outputs = torch.cat(a_outputs).numpy().flatten()\ng_probs = 1 / (1 + np.exp(-g_logits))\ng_preds = (g_probs > 0.5).astype(int)\na_preds = np.clip(a_outputs, 0, 120)\n\nsubmission = pd.DataFrame({'id': test_df['id'], 'gender': g_preds, 'age': a_preds})\nsubmission.to_csv('/kaggle/working/submission.csv', index=False)\n\nprint(\"โœ… submission.csv (Scratch CNN)\")\nprint(submission.head())\nprint(f\"Shape: {submission.shape}\")\nprint(\"\\n๐ŸŽ‰ Ready for submission!\")\n\ndel model; gc.collect(); torch.cuda.empty_cache()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:47:19.924459Z","iopub.execute_input":"2025-11-12T10:47:19.924773Z","iopub.status.idle":"2025-11-12T10:47:34.689258Z","shell.execute_reply.started":"2025-11-12T10:47:19.924729Z","shell.execute_reply":"2025-11-12T10:47:34.688401Z"}},"outputs":[{"name":"stdout","text":"======================================================================\n๐Ÿ” Searching for scratch model checkpoint...\n======================================================================\nโœ… Found: /kaggle/input/face-age-gender-predictor/pytorch/scratch-cnn/1/model.ckpt\nโœ… Test: 8677 samples\nโœ… Device: cuda\n\n======================================================================\n๐Ÿ”ฎ Scratch CNN Predictions...\n======================================================================\nโœ… submission.csv (Scratch CNN)\n   id  gender        age\n0   0       1  33.268768\n1   1       0  24.642073\n2   2       1  34.242413\n3   3       1  27.935734\n4   4       1  50.617668\nShape: (8677, 3)\n\n๐ŸŽ‰ Ready for submission!\n","output_type":"stream"}],"execution_count":16},{"cell_type":"code","source":"# ============================================================================\n# FINE-TUNED RESNET INFERENCE (Fixed Layer Names)\n# ============================================================================\n\nimport os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nimport pytorch_lightning as pl\n\n# Settings\nclass Config:\n    DATA_ROOT = \"/kaggle/input/sep-25-dl-gen-ai-nppe-1/face_dataset\"\n    TEST_CSV = f\"{DATA_ROOT}/test.csv\"\n    IMG_SIZE = 128\n    BATCH = 128\n    LR = 1e-3\n    AGE_WEIGHT = 0.01\n    WORKERS = 2\n\ncfg = Config()\n\n# Auto-detect checkpoint\nprint(\"=\"*70)\nprint(\"๐Ÿ” Searching for finetuned model checkpoint...\")\nprint(\"=\"*70)\n\ncfg.CHECKPOINT = None\nfor root, dirs, files in os.walk(\"/kaggle/input/\"):\n    for file in files:\n        if file == \"model.ckpt\" and (\"resnet\" in root.lower() or \"finetuned\" in root.lower() or \"finetune\" in root.lower()):\n            cfg.CHECKPOINT = os.path.join(root, file)\n            print(f\"โœ… Found: {cfg.CHECKPOINT}\")\n            break\n    if cfg.CHECKPOINT:\n        break\n\nif cfg.CHECKPOINT is None:\n    raise FileNotFoundError(\"Add Kaggle Model: face-age-gender-predictor (resnet-finetuned) via 'Add Input'\")\n\n# Data Pipeline\nclass ImageTransform:\n    def __init__(self, size=128):\n        self.transform = transforms.Compose([\n            transforms.Resize((size, size)),\n            transforms.ToTensor(),\n            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n        ])\n\nclass TestDataset(Dataset):\n    def __init__(self, df, root, tfm):\n        self.df, self.root, self.tfm = df, root, tfm\n    def __len__(self): return len(self.df)\n    def __getitem__(self, i):\n        img = Image.open(os.path.join(self.root, self.df.iloc[i]['full_path'])).convert(\"RGB\")\n        return self.tfm(img)\n\ntest_df = pd.read_csv(cfg.TEST_CSV)\ntest_ds = TestDataset(test_df, cfg.DATA_ROOT, ImageTransform(cfg.IMG_SIZE).transform)\ntest_dl = DataLoader(test_ds, batch_size=cfg.BATCH, shuffle=False, num_workers=cfg.WORKERS)\nprint(f\"โœ… Test: {len(test_df)} samples\")\n\n# Model (Match training architecture exactly)\nclass FineTunedResNet(pl.LightningModule):\n    def __init__(self, lr, age_w):\n        super().__init__()\n        self.save_hyperparameters()\n        resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        num_features = resnet.fc.in_features\n        self.backbone = nn.Sequential(*list(resnet.children())[:-1])\n        self.gender_head = nn.Linear(num_features, 1)\n        self.age_head = nn.Linear(num_features, 1)\n    \n    def forward(self, x):\n        features = torch.flatten(self.backbone(x), 1)\n        return self.gender_head(features), self.age_head(features)\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"โœ… Device: {device}\\n\")\n\n# Inference\nprint(\"=\"*70)\nprint(\"๐Ÿ”ฎ Fine-Tuned ResNet Predictions...\")\nprint(\"=\"*70)\n\nmodel = FineTunedResNet.load_from_checkpoint(cfg.CHECKPOINT, lr=cfg.LR, age_w=cfg.AGE_WEIGHT)\nmodel.eval().to(device)\n\ng_logits, a_outputs = [], []\nwith torch.no_grad():\n    for imgs in test_dl:\n        g, a = model(imgs.to(device))\n        g_logits.append(g.cpu())\n        a_outputs.append(a.cpu())\n\ng_logits = torch.cat(g_logits).numpy().flatten()\na_outputs = torch.cat(a_outputs).numpy().flatten()\ng_probs = 1 / (1 + np.exp(-g_logits))\ng_preds = (g_probs > 0.5).astype(int)\na_preds = np.clip(a_outputs, 0, 120)\n\nsubmission = pd.DataFrame({'id': test_df['id'], 'gender': g_preds, 'age': a_preds})\nsubmission.to_csv('/kaggle/working/submission.csv', index=False)\n\nprint(\"โœ… submission.csv (Fine-Tuned ResNet)\")\nprint(submission.head())\nprint(f\"Shape: {submission.shape}\")\nprint(\"\\n๐ŸŽ‰ Ready for submission!\")\n\ndel model; gc.collect(); torch.cuda.empty_cache()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:47:42.672984Z","iopub.execute_input":"2025-11-12T10:47:42.673783Z","iopub.status.idle":"2025-11-12T10:47:57.826931Z","shell.execute_reply.started":"2025-11-12T10:47:42.673755Z","shell.execute_reply":"2025-11-12T10:47:57.826236Z"}},"outputs":[{"name":"stdout","text":"======================================================================\n๐Ÿ” Searching for finetuned model checkpoint...\n======================================================================\nโœ… Found: /kaggle/input/face-age-gender-predictor/pytorch/resnet-finetuned/1/model.ckpt\nโœ… Test: 8677 samples\nโœ… Device: cuda\n\n======================================================================\n๐Ÿ”ฎ Fine-Tuned ResNet Predictions...\n======================================================================\nโœ… submission.csv (Fine-Tuned ResNet)\n   id  gender        age\n0   0       1  33.796421\n1   1       0  27.736071\n2   2       1  35.373379\n3   3       1  24.109423\n4   4       1  64.067947\nShape: (8677, 3)\n\n๐ŸŽ‰ Ready for submission!\n","output_type":"stream"}],"execution_count":17},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}