{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\n\n# Instead of listing all files, we simply check the existence of the necessary folders\nbase_input = '/kaggle/input'\ncompetition_dir = os.path.join(base_input, 'competitions', 'state-farm-distracted-driver-detection')\nif os.path.exists(competition_dir):\n    print(\"✅ State Farm dataset found:\", competition_dir)\nelse:\n    print(\"❌ Dataset not found, check the path.\")\n\n# If you want to see the first files of a specific folder (optional):\nsample_dir = os.path.join(competition_dir, 'imgs', 'train')\nif os.path.exists(sample_dir):\n    sample_files = os.listdir(sample_dir)[:5]\n    print(\"Preview of training classes:\", sample_files)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:21:32.594504Z","iopub.execute_input":"2026-07-11T07:21:32.594922Z","iopub.status.idle":"2026-07-11T07:21:32.602392Z","shell.execute_reply.started":"2026-07-11T07:21:32.594895Z","shell.execute_reply":"2026-07-11T07:21:32.601785Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📦 LIBRARY IMPORTS\n# =============================================================================\nimport os\nimport gc\nimport cv2\nimport random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image, ExifTags\nfrom collections import Counter, defaultdict\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Display configurations\npd.set_option('display.max_columns', None)\npd.set_option('display.max_rows', 100)\nplt.style.use('seaborn-v0_8-darkgrid')\nsns.set_palette(\"husl\")\n\n# =============================================================================\n# 🗂️ PATH CONFIGURATION\n# =============================================================================\nBASE_DIR = '/kaggle/input/state-farm-distracted-driver-detection'\nTRAIN_DIR = os.path.join(BASE_DIR, 'imgs/train')\nTEST_DIR = os.path.join(BASE_DIR, 'imgs/test')\nSAMPLE_SUB = os.path.join(BASE_DIR, 'sample_submission.csv')\n\n# Dataset classes\nCLASSES = {\n    'c0': 'Normal driving',\n    'c1': 'Texting - right',\n    'c2': 'Talking on phone - right',\n    'c3': 'Texting - left',\n    'c4': 'Talking on phone - left',\n    'c5': 'Operating radio',\n    'c6': 'Drinking',\n    'c7': 'Reaching behind',\n    'c8': 'Hair and makeup',\n    'c9': 'Talking to passenger'\n}\n\nprint(\"✅ Environment configured\")\nprint(f\"📁 Training directory: {TRAIN_DIR}\")\nprint(f\"📁 Test directory: {TEST_DIR}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:21:35.231253Z","iopub.execute_input":"2026-07-11T07:21:35.231955Z","iopub.status.idle":"2026-07-11T07:21:35.240194Z","shell.execute_reply.started":"2026-07-11T07:21:35.231927Z","shell.execute_reply":"2026-07-11T07:21:35.239501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔍 STEP 1: EXPLORE THE /kaggle/input DIRECTORY\n# =============================================================================\nimport os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom collections import defaultdict\nimport warnings\nwarnings.filterwarnings('ignore')\n\npd.set_option('display.max_columns', None)\nplt.style.use('seaborn-v0_8-darkgrid')\n\nprint(\"📁 Contents of /kaggle/input/:\")\nprint(\"-\" * 50)\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    level = dirname.replace('/kaggle/input', '').count(os.sep)\n    indent = ' ' * 2 * level\n    print(f'{indent}📂 {os.path.basename(dirname)}/')\n    subindent = ' ' * 2 * (level + 1)\n    for i, filename in enumerate(filenames[:5]):  # Display max 5 files per folder\n        print(f'{subindent}📄 {filename}')\n    if len(filenames) > 5:\n        print(f'{subindent}... and {len(filenames) - 5} other files')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:21:44.103357Z","iopub.execute_input":"2026-07-11T07:21:44.104101Z","iopub.status.idle":"2026-07-11T07:22:07.690477Z","shell.execute_reply.started":"2026-07-11T07:21:44.104074Z","shell.execute_reply":"2026-07-11T07:22:07.689660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔎 STEP 2: AUTOMATIC DATASET DETECTION FUNCTION\n# =============================================================================\n\ndef find_state_farm_dataset(base_path='/kaggle/input'):\n    \"\"\"\n    Automatically detects the correct path for the State Farm dataset\n    Returns: dict with train/test paths or None if not found\n    \"\"\"\n    possible_names = [\n        'state-farm-distracted-driver-detection',\n        'state-farm',\n        'distracted-driver',\n        'driver-detection',\n        'imgs'\n    ]\n    \n    results = {}\n    \n    # Traverse all folders in /kaggle/input\n    for root, dirs, files in os.walk(base_path):\n        # Look for 'train' or 'test' folders\n        if 'train' in dirs or 'test' in dirs:\n            # Check if it is indeed the State Farm dataset\n            train_path = os.path.join(root, 'train')\n            if os.path.exists(train_path):\n                # Check for the presence of subfolders c0, c1, etc.\n                subdirs = [d for d in os.listdir(train_path) if d.startswith('c')]\n                if any(d in ['c0', 'c1', 'c2'] for d in subdirs):\n                    results['train'] = train_path\n                    results['test'] = os.path.join(root, 'test') if os.path.exists(os.path.join(root, 'test')) else None\n                    results['base'] = root\n                    return results\n    \n    # Alternative search by dataset name\n    for dirname in os.listdir(base_path):\n        dir_path = os.path.join(base_path, dirname)\n        if os.path.isdir(dir_path):\n            for name in possible_names:\n                if name.lower() in dirname.lower():\n                    # Explore this folder\n                    for root, dirs, files in os.walk(dir_path):\n                        if 'train' in dirs:\n                            train_path = os.path.join(root, 'train')\n                            if os.path.exists(train_path):\n                                subdirs = [d for d in os.listdir(train_path) if d.startswith('c')]\n                                if any(d in ['c0', 'c1'] for d in subdirs):\n                                    results['train'] = train_path\n                                    results['test'] = os.path.join(root, 'test') if os.path.exists(os.path.join(root, 'test')) else None\n                                    results['base'] = root\n                                    return results\n    \n    return None\n\n# Run the detection\nprint(\"🔍 Automatic search for the State Farm dataset...\")\ndataset_paths = find_state_farm_dataset()\n\nif dataset_paths:\n    print(\"✅ Dataset found!\")\n    print(f\"📁 Base: {dataset_paths['base']}\")\n    print(f\"📁 Train: {dataset_paths['train']}\")\n    print(f\"📁 Test: {dataset_paths['test']}\")\nelse:\n    print(\"❌ Dataset not found automatically.\")\n    print(\"💡 Make sure you have added the 'State Farm Distracted Driver Detection' dataset to your Kaggle notebook.\")\n    print(\"🔗 Link: https://www.kaggle.com/competitions/state-farm-distracted-driver-detection/data\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:22:15.309458Z","iopub.execute_input":"2026-07-11T07:22:15.309764Z","iopub.status.idle":"2026-07-11T07:22:15.324343Z","shell.execute_reply.started":"2026-07-11T07:22:15.309741Z","shell.execute_reply":"2026-07-11T07:22:15.323495Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# ⚙️ STEP 3: DYNAMIC PATH CONFIGURATION\n# =============================================================================\n\n# If automatic detection failed, list the available options\nif not dataset_paths:\n    print(\"\\n📋 Available folders in /kaggle/input/:\")\n    for item in os.listdir('/kaggle/input'):\n        item_path = os.path.join('/kaggle/input', item)\n        if os.path.isdir(item_path):\n            print(f\"  • {item}/\")\n            # Display the structure up to 2 levels\n            for sub in os.listdir(item_path)[:3]:\n                sub_path = os.path.join(item_path, sub)\n                if os.path.isdir(sub_path):\n                    print(f\"    └─ {sub}/\")\n    \n    # 🔧 MANUALLY MODIFY THIS LINE with the correct folder name:\n    DATASET_NAME = \"state-farm-distracted-driver-detection\"  # ← TO ADAPT\n    BASE_DIR = os.path.join('/kaggle/input', DATASET_NAME)\n    \n    # Try several possible structures\n    possible_train_paths = [\n        os.path.join(BASE_DIR, 'imgs', 'train'),\n        os.path.join(BASE_DIR, 'train'),\n        os.path.join(BASE_DIR, 'data', 'train'),\n        BASE_DIR  # if the c0, c1 folders are directly in BASE_DIR\n    ]\n    \n    for path in possible_train_paths:\n        if os.path.exists(path):\n            # Check if it's the correct format\n            if any(os.path.exists(os.path.join(path, f'c{i}')) for i in range(10)):\n                TRAIN_DIR = path\n                print(f\"✅ Train directory found: {TRAIN_DIR}\")\n                break\n    else:\n        raise FileNotFoundError(\"❌ Cannot find the 'train' folder with c0-c9 classes. Check the dataset structure.\")\nelse:\n    BASE_DIR = dataset_paths['base']\n    TRAIN_DIR = dataset_paths['train']\n    TEST_DIR = dataset_paths['test']\n\n# Dataset classes (unchanged)\nCLASSES = {\n    'c0': 'Normal driving',\n    'c1': 'Texting - right',\n    'c2': 'Talking on phone - right',\n    'c3': 'Texting - left',\n    'c4': 'Talking on phone - left',\n    'c5': 'Operating radio',\n    'c6': 'Drinking',\n    'c7': 'Reaching behind',\n    'c8': 'Hair and makeup',\n    'c9': 'Talking to passenger'\n}\n\nprint(f\"\\n✅ Configuration finalized:\")\nprint(f\"   BASE_DIR: {BASE_DIR}\")\nprint(f\"   TRAIN_DIR: {TRAIN_DIR}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:24:15.982820Z","iopub.execute_input":"2026-07-11T07:24:15.983641Z","iopub.status.idle":"2026-07-11T07:24:15.992257Z","shell.execute_reply.started":"2026-07-11T07:24:15.983615Z","shell.execute_reply":"2026-07-11T07:24:15.991417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🗂️ STEP 4: ROBUST IMAGE LOADING FUNCTION\n# =============================================================================\n\ndef load_image_paths(directory, max_samples=None, verbose=True):\n    \"\"\"\n    Loads image paths with their classes\n    Handles different folder structures\n    \"\"\"\n    data = []\n    \n    if verbose:\n        print(f\"🔄 Exploring: {directory}\")\n    \n    # Case 1: Classes are direct subfolders (c0, c1, ...)\n    if any(os.path.isdir(os.path.join(directory, f'c{i}')) for i in range(10)):\n        for class_label in [f'c{i}' for i in range(10)]:\n            class_path = os.path.join(directory, class_label)\n            if os.path.isdir(class_path):\n                for img_name in os.listdir(class_path):\n                    if img_name.lower().endswith(('.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG')):\n                        img_path = os.path.join(class_path, img_name)\n                        data.append({\n                            'image_name': img_name,\n                            'class': class_label,\n                            'class_name': CLASSES.get(class_label, 'Unknown'),\n                            'path': img_path\n                        })\n    \n    # Case 2: Nested structure (e.g., imgs/train/c0/)\n    else:\n        for root, dirs, files in os.walk(directory):\n            # Check if this folder contains images and its name is a class\n            folder_name = os.path.basename(root)\n            if folder_name in CLASSES.keys():\n                for img_name in files:\n                    if img_name.lower().endswith(('.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG')):\n                        img_path = os.path.join(root, img_name)\n                        data.append({\n                            'image_name': img_name,\n                            'class': folder_name,\n                            'class_name': CLASSES.get(folder_name, 'Unknown'),\n                            'path': img_path\n                        })\n    \n    df = pd.DataFrame(data)\n    \n    if max_samples and len(df) > max_samples:\n        df = df.sample(n=max_samples, random_state=42).reset_index(drop=True)\n        if verbose:\n            print(f\"📊 Sampling: {max_samples} images selected\")\n    \n    if verbose:\n        print(f\"✅ {len(df)} images loaded successfully\")\n    \n    return df\n\n# Actual loading\nprint(\"\\n\" + \"=\"*60)\nprint(\"🚗 LOADING STATE FARM DATASET\")\nprint(\"=\"*60)\ntrain_df = load_image_paths(TRAIN_DIR)\n\nif len(train_df) == 0:\n    raise ValueError(\"❌ No images loaded! Check the structure of the train folder.\")\n\ndisplay(train_df.head())\nprint(f\"\\n📈 Shape: {train_df.shape}\")\nprint(f\"📊 Unique classes: {sorted(train_df['class'].unique())}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:24:36.505085Z","iopub.execute_input":"2026-07-11T07:24:36.505474Z","iopub.status.idle":"2026-07-11T07:24:36.669366Z","shell.execute_reply.started":"2026-07-11T07:24:36.505450Z","shell.execute_reply":"2026-07-11T07:24:36.668519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📊 STEP 5: QUICK DATA QUALITY CHECK\n# =============================================================================\n\ndef quick_data_check(df):\n    \"\"\"Quick check before full EDA\"\"\"\n    \n    print(\"\\n🔍 QUICK DATA CHECK\")\n    print(\"-\" * 40)\n    \n    # 1. Class distribution\n    print(\"\\n📊 Class distribution:\")\n    class_counts = df['class'].value_counts().sort_index()\n    for cls, count in class_counts.items():\n        bar = \"█\" * int(count / 200)  # Visual scale\n        print(f\"  {cls} ({CLASSES[cls]:25s}): {count:5d} {bar}\")\n    \n    # 2. Path verification\n    print(f\"\\n🔗 Verifying image paths...\")\n    valid_paths = df['path'].apply(os.path.exists).sum()\n    print(f\"   ✅ {valid_paths}/{len(df)} valid paths\")\n    \n    if valid_paths < len(df):\n        print(f\"   ⚠️ {len(df) - valid_paths} invalid paths detected!\")\n    \n    # 3. Image preview\n    print(f\"\\n🖼️ Image reading test...\")\n    try:\n        import cv2\n        sample_path = df['path'].iloc[0]\n        img = cv2.imread(sample_path)\n        if img is not None:\n            h, w, c = img.shape\n            print(f\"   ✅ Image read: {w}x{h}x{c} - {sample_path.split('/')[-1]}\")\n        else:\n            print(f\"   ❌ Unable to read: {sample_path}\")\n    except Exception as e:\n        print(f\"   ❌ Error: {e}\")\n    \n    # 4. Memo of next steps\n    print(f\"\\n🎯 NEXT STEPS:\")\n    print(f\"   1. ✅ Loading validated\")\n    print(f\"   2. 📊 Run full EDA (distribution, properties, visualization)\")\n    print(f\"   3. 🧹 Clean data if necessary\")\n    print(f\"   4. 🤖 Prepare modeling pipeline\")\n\nquick_data_check(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-10T22:23:02.246473Z","iopub.execute_input":"2026-07-10T22:23:02.247007Z","iopub.status.idle":"2026-07-10T22:23:13.831079Z","shell.execute_reply.started":"2026-07-10T22:23:02.246969Z","shell.execute_reply":"2026-07-10T22:23:13.830268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📦 OPTIMIZED KAGGLE CONFIGURATION & IMPORTS\n# =============================================================================\nimport os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom collections import defaultdict, Counter\nfrom sklearn.model_selection import GroupShuffleSplit, train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom tqdm.auto import tqdm\nimport hashlib\nimport json\nimport gc\nimport warnings\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nwarnings.filterwarnings('ignore')\n\n# Display configuration\npd.set_option('display.max_columns', None)\nplt.style.use('seaborn-v0_8-darkgrid')\nsns.set_palette(\"husl\")\n\n# ✅ Official Kaggle paths\nBASE_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection'\nTRAIN_DIR = os.path.join(BASE_DIR, 'imgs', 'train')\nMETADATA_CSV = os.path.join(BASE_DIR, 'driver_imgs_list.csv')\nOUTPUT_DIR = '/kaggle/working/preprocessed_dataset'\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\nCLASSES = {f'c{i}': name for i, name in enumerate([\n    'Normal driving', 'Texting - right', 'Talking on phone - right',\n    'Texting - left', 'Talking on phone - left', 'Operating radio',\n    'Drinking', 'Reaching behind', 'Hair and makeup', 'Talking to passenger'\n])}\n\nprint(\"✅ Environment configured | Ready for advanced EDA\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:24:47.662246Z","iopub.execute_input":"2026-07-11T07:24:47.662696Z","iopub.status.idle":"2026-07-11T07:25:00.445969Z","shell.execute_reply.started":"2026-07-11T07:24:47.662669Z","shell.execute_reply":"2026-07-11T07:25:00.445095Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📊 1.1 INITIAL LOADING & VALIDATION\n# =============================================================================\ndef load_and_validate_metadata(csv_path, train_dir):\n    print(\"🔄 Loading and validating metadata...\")\n    df = pd.read_csv(csv_path)\n    df['path'] = df.apply(lambda r: os.path.join(train_dir, r['classname'], r['img']), axis=1)\n    df['class_name'] = df['classname'].map(CLASSES)\n    \n    # Checking file existence\n    print(\"🔍 Verifying path integrity...\")\n    df['exists'] = df['path'].apply(os.path.exists)\n    missing = (~df['exists']).sum()\n    if missing > 0:\n        print(f\"⚠️ {missing} files not found → Removing\")\n        df = df[df['exists']].copy()\n    else:\n        print(\"✅ All files are present\")\n        \n    df.drop(columns=['exists'], inplace=True)\n    df.reset_index(drop=True, inplace=True)\n    print(f\"✅ Dataset validated: {len(df)} images\")\n    return df\n\ntrain_df = load_and_validate_metadata(METADATA_CSV, TRAIN_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:25:08.005448Z","iopub.execute_input":"2026-07-11T07:25:08.006117Z","iopub.status.idle":"2026-07-11T07:25:16.698163Z","shell.execute_reply.started":"2026-07-11T07:25:08.006087Z","shell.execute_reply":"2026-07-11T07:25:16.697309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📈 1.2 CLASS & DRIVER LEAKAGE RISK ANALYSIS\n# =============================================================================\ndef analyze_classes_and_drivers(df):\n    print(\"\\n\" + \"=\"*70)\n    print(\" CLASS & DRIVER ANALYSIS (ANTI-LEAKAGE)\")\n    print(\"=\"*70)\n    \n    stats = {}\n    issues = defaultdict(list)\n    \n    # 1. Class distribution\n    class_counts = df['classname'].value_counts().sort_index()\n    stats['class_distribution'] = class_counts.to_dict()\n    imbalance_ratio = class_counts.max() / class_counts.min()\n    print(f\"📊 Class imbalance: {imbalance_ratio:.2f}x (Max: {class_counts.idxmax()}, Min: {class_counts.idxmin()})\")\n    \n    # 2. Driver analysis (CRITICAL)\n    if 'subject' in df.columns:\n        n_drivers = df['subject'].nunique()\n        driver_imgs = df['subject'].value_counts()\n        driver_class_dist = df.groupby('subject')['classname'].value_counts().unstack(fill_value=0)\n        \n        # Leakage risk: drivers present in only one class\n        single_class_drivers = (driver_class_dist > 0).sum(axis=1)\n        mono_class_count = (single_class_drivers == 1).sum()\n        \n        # Concentration: drivers with >70% of images in one class\n        high_concentration = (driver_class_dist.max(axis=1) / driver_class_dist.sum(axis=1) > 0.7).sum()\n        \n        stats['driver_stats'] = {\n            'total_drivers': int(n_drivers),\n            'mono_class_drivers': int(mono_class_count),\n            'high_concentration_drivers': int(high_concentration),\n            'max_imgs_per_driver': int(driver_imgs.max()),\n            'min_imgs_per_driver': int(driver_imgs.min())\n        }\n        \n        print(f\"👤 Drivers: {n_drivers}\")\n        print(f\"⚠️ Single-class drivers: {mono_class_count} (bias risk)\")\n        print(f\"⚠️ Highly concentrated drivers (>70%): {high_concentration}\")\n        \n        if mono_class_count > 0 or high_concentration > 5:\n            issues['driver_bias'].append(\"Multiple drivers dominate a single class\")\n    else:\n        print(\"❌ 'subject' column missing → Leakage inevitable with random split\")\n        issues['missing_subject'].append(\"No driver column\")\n        \n    return stats, issues\n\ndriver_stats, driver_issues = analyze_classes_and_drivers(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:25:40.904992Z","iopub.execute_input":"2026-07-11T07:25:40.905507Z","iopub.status.idle":"2026-07-11T07:25:40.944065Z","shell.execute_reply.started":"2026-07-11T07:25:40.905480Z","shell.execute_reply":"2026-07-11T07:25:40.943309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔬 1.3 ADVANCED IMAGE TECHNICAL ANALYSIS\n# =============================================================================\ndef advanced_image_analysis(df, sample_size=1500):\n    print(\"\\n\" + \"=\"*70)\n    print(\"🔬 ADVANCED IMAGE TECHNICAL ANALYSIS\")\n    print(\"=\"*70)\n    \n    sample_df = df.sample(n=min(sample_size, len(df)), random_state=42)\n    issues = defaultdict(list)\n    props = []\n    \n    print(f\"📸 Analyzing {len(sample_df)} images (blur, brightness, dimensions, corruption)...\")\n    \n    for _, row in tqdm(sample_df.iterrows(), desc=\"Technical analysis\", leave=False):\n        try:\n            img = cv2.imread(row['path'], cv2.IMREAD_COLOR)\n            if img is None:\n                issues['corrupted'].append(row['img'])\n                continue\n                \n            h, w, c = img.shape\n            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n            \n            # Quality metrics\n            brightness = np.mean(gray)\n            contrast = np.std(gray)\n            blur_score = cv2.Laplacian(gray, cv2.CV_64F).var()\n            file_size = os.path.getsize(row['path'])\n            \n            props.append({\n                'img': row['img'], 'classname': row['classname'],\n                'h': h, 'w': w, 'c': c,\n                'brightness': brightness, 'contrast': contrast,\n                'blur': blur_score, 'size': file_size\n            })\n            \n            # Anomaly detection\n            if blur_score < 30:\n                issues['too_blurry'].append(row['img'])\n            elif brightness < 15:\n                issues['too_dark'].append(row['img'])\n            elif brightness > 245:\n                issues['too_bright'].append(row['img'])\n            elif c != 3:\n                issues['wrong_channels'].append(row['img'])\n                \n        except Exception as e:\n            issues['error_reading'].append(row['img'])\n            continue\n            \n    props_df = pd.DataFrame(props)\n    print(f\"✅ Analysis completed: {len(props_df)}/{len(sample_df)} images analyzed\")\n    \n    # Anomaly report\n    for k, v in issues.items():\n        if v:\n            print(f\"⚠️ {k}: {len(v)} images\")\n            \n    return props_df, issues\n\nprops_df, quality_issues = advanced_image_analysis(train_df, sample_size=1500)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:25:51.721962Z","iopub.execute_input":"2026-07-11T07:25:51.722461Z","iopub.status.idle":"2026-07-11T07:26:06.485323Z","shell.execute_reply.started":"2026-07-11T07:25:51.722432Z","shell.execute_reply":"2026-07-11T07:26:06.484500Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔍 1.4 DUPLICATE DETECTION (EXACT & PERCEPTUAL)\n# =============================================================================\ndef detect_duplicates(df, sample_size=2000):\n    print(\"\\n\" + \"=\"*70)\n    print(\"🔍 DUPLICATE DETECTION\")\n    print(\"=\"*70)\n    \n    sample_df = df.sample(n=min(sample_size, len(df)), random_state=42)\n    hash_map = defaultdict(list)\n    \n    print(\"🔐 Calculating MD5 hashes (exact duplicates)...\")\n    for _, row in tqdm(sample_df.iterrows(), desc=\"Hashing\", leave=False):\n        try:\n            with open(row['path'], 'rb') as f:\n                h = hashlib.md5(f.read()).hexdigest()\n            hash_map[h].append(row['img'])\n        except:\n            continue\n            \n    exact_dups = {h: imgs for h, imgs in hash_map.items() if len(imgs) > 1}\n    total_exact = sum(len(v)-1 for v in exact_dups.values())\n    \n    print(f\"✅ Exact duplicates detected: {total_exact} images\")\n    return exact_dups, total_exact\n\nexact_dups, n_exact_dups = detect_duplicates(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:26:17.054932Z","iopub.execute_input":"2026-07-11T07:26:17.055327Z","iopub.status.idle":"2026-07-11T07:26:20.376800Z","shell.execute_reply.started":"2026-07-11T07:26:17.055302Z","shell.execute_reply":"2026-07-11T07:26:20.376026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📐 IMAGE DIMENSION ANALYSIS\n# =============================================================================\ndef analyze_image_dimensions(props_df):\n    if props_df.empty:\n        print(\"⚠️ No technical data available to analyze dimensions.\")\n        return\n\n    print(\"\\n\" + \"=\"*60)\n    print(\"📐 DIMENSION VERIFICATION (WIDTH x HEIGHT)\")\n    print(\"=\"*60)\n    \n    # 1. Quick textual analysis\n    dim_counts = props_df.groupby(['w', 'h']).size()\n    print(\"📊 Distribution of found sizes:\")\n    for (w, h), count in dim_counts.sort_values(ascending=False).items():\n        print(f\"   • {w}x{h}: {count} images\")\n        \n    # 2. Visualization\n    plt.figure(figsize=(16, 5))\n\n    # Graph 1: Dimension distribution (Bar Plot)\n    plt.subplot(1, 2, 1)\n    top_dims = dim_counts.sort_values(ascending=False).head(5)\n    labels = [f\"{w}x{h}\" for w, h in top_dims.index]\n    \n    bars = plt.bar(labels, top_dims.values, color='skyblue', edgecolor='black')\n    plt.title('📊 Top 5 Image Dimensions (Width x Height)', fontweight='bold')\n    plt.xlabel('Dimensions (WxH)')\n    plt.ylabel('Number of images')\n    plt.xticks(rotation=45)\n    \n    # Adding values to bars\n    for bar in bars:\n        height = bar.get_height()\n        plt.text(bar.get_x() + bar.get_width()/2., height + 10,\n                 f'{int(height)}', ha='center', va='bottom', fontweight='bold')\n\n    # Graph 2: Scatter Plot to detect outliers\n    plt.subplot(1, 2, 2)\n    # Grouping points to avoid visual clutter with 22k images\n    unique_dims = props_df.groupby(['w', 'h']).size().reset_index(name='count')\n    \n    scatter = plt.scatter(unique_dims['w'], unique_dims['h'], \n                          s=unique_dims['count']*2,  # Point size proportional to count\n                          c='orange', alpha=0.7, edgecolors='black')\n    plt.title('🔍 Scatter Plot: Width vs Height (Size = Nb of images)', fontweight='bold')\n    plt.xlabel('Width (px)')\n    plt.ylabel('Height (px)')\n    plt.grid(True, alpha=0.3)\n\n    plt.tight_layout()\n    plt.savefig(os.path.join(OUTPUT_DIR, 'dimensions_analysis.png'), dpi=300)\n    plt.show()\n\n# Run analysis (ensure props_df is defined in your scope)\nanalyze_image_dimensions(props_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:26:25.302782Z","iopub.execute_input":"2026-07-11T07:26:25.303193Z","iopub.status.idle":"2026-07-11T07:26:26.129880Z","shell.execute_reply.started":"2026-07-11T07:26:25.303167Z","shell.execute_reply":"2026-07-11T07:26:26.129236Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📊 1.5 FULL EDA DASHBOARD & ISSUE REPORT\n# =============================================================================\ndef generate_eda_dashboard(df, class_counts, driver_stats, props_df, quality_issues, exact_dups):\n    print(\"\\n🎨 Generating EDA dashboard...\")\n    \n    fig = plt.figure(figsize=(20, 14))\n    fig.suptitle('🔍 FULL EDA - STATE FARM DATASET', fontsize=18, fontweight='bold', y=0.995)\n    gs = fig.add_gridspec(3, 3, hspace=0.35, wspace=0.3)\n    \n    # 1. Class distribution\n    ax1 = fig.add_subplot(gs[0, 0])\n    bars = ax1.barh([CLASSES[k] for k in class_counts.index], class_counts.values, \n                    color=plt.cm.Set3(np.linspace(0,1,10)), edgecolor='black')\n    ax1.set_xlabel('Number of images')\n    ax1.set_title('📊 Class distribution')\n    for i, v in enumerate(class_counts.values):\n        ax1.text(v + 30, i, str(v), va='center', fontsize=9)\n        \n    # 2. Images per driver\n    ax2 = fig.add_subplot(gs[0, 1])\n    if 'subject' in df.columns:\n        driver_counts = df['subject'].value_counts().sort_values()\n        ax2.barh(range(len(driver_counts)), driver_counts.values, color='skyblue', edgecolor='black')\n        ax2.set_yticks(range(len(driver_counts)))\n        ax2.set_yticklabels([f'd{i}' for i in range(len(driver_counts))], fontsize=7)\n        ax2.set_xlabel('Images per driver')\n        ax2.set_title('👤 Distribution per driver')\n    else:\n        ax2.text(0.5, 0.5, 'No driver data', ha='center', va='center', transform=ax2.transAxes)\n        \n    # 3. Visual quality (Brightness vs Contrast)\n    ax3 = fig.add_subplot(gs[0, 2])\n    if not props_df.empty:\n        sc = ax3.scatter(props_df['brightness'], props_df['contrast'], \n                         c=props_df['blur'], cmap='viridis', alpha=0.6, edgecolors='black', s=15)\n        plt.colorbar(sc, ax=ax3, label='Sharpness score (Laplacian)')\n        ax3.set_xlabel('Average brightness')\n        ax3.set_ylabel('Contrast (std)')\n        ax3.set_title('💡 Visual Quality vs Sharpness')\n        ax3.grid(alpha=0.3)\n        \n    # 4. Issue report\n    ax4 = fig.add_subplot(gs[1, 0])\n    ax4.axis('off')\n    problems = []\n    if quality_issues.get('corrupted'): problems.append(f\"• Corrupted: {len(quality_issues['corrupted'])}\")\n    if quality_issues.get('too_blurry'): problems.append(f\"• Too blurry: {len(quality_issues['too_blurry'])}\")\n    if quality_issues.get('too_dark'): problems.append(f\"• Too dark: {len(quality_issues['too_dark'])}\")\n    if exact_dups: problems.append(f\"• Exact duplicates: {sum(len(v)-1 for v in exact_dups.values())}\")\n    if driver_stats.get('mono_class_drivers', 0) > 0:\n        problems.append(f\"• Single-class drivers: {driver_stats['mono_class_drivers']}\")\n        \n    report = f\"🚨 ISSUES DETECTED\\n\\n\" + \"\\n\".join(problems) if problems else \"✅ NO MAJOR ISSUES DETECTED\"\n    ax4.text(0.05, 0.95, report, transform=ax4.transAxes, fontsize=10, verticalalignment='top',\n             bbox=dict(boxstyle='round', facecolor='salmon' if problems else 'lightgreen', alpha=0.3))\n    ax4.set_title('⚠️ Quality Report')\n    \n    # 5. Image feature correlation matrix\n    ax5 = fig.add_subplot(gs[1, 1:])\n    if not props_df.empty:\n        corr_cols = ['brightness', 'contrast', 'blur', 'size']\n        corr = props_df[corr_cols].corr()\n        sns.heatmap(corr, annot=True, cmap='coolwarm', center=0, ax=ax5, fmt='.2f')\n        ax5.set_title('🔗 Image feature correlation')\n    else:\n        ax5.text(0.5, 0.5, 'Insufficient data', ha='center', va='center', transform=ax5.transAxes)\n        \n    # 6. Blur distribution per class\n    ax6 = fig.add_subplot(gs[2, :])\n    if not props_df.empty:\n        sns.boxplot(data=props_df, x='classname', y='blur', ax=ax6, palette='Set2')\n        ax6.set_xticklabels([f'c{i}' for i in range(10)], rotation=45, ha='right')\n        ax6.set_ylabel('Sharpness score (Laplacian variance)')\n        ax6.set_title('📉 Average sharpness per class (Critical threshold: <30)')\n        ax6.axhline(y=30, color='red', linestyle='--', alpha=0.7, label='Blur threshold')\n        ax6.legend()\n        ax6.grid(axis='y', alpha=0.3)\n    else:\n        ax6.text(0.5, 0.5, 'No blur data', ha='center', va='center', transform=ax6.transAxes)\n        \n    plt.savefig(os.path.join(OUTPUT_DIR, 'eda_complete_dashboard.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n    \n    # Return all detected issues\n    all_issues = {**quality_issues, 'exact_duplicates': exact_dups}\n    return all_issues\n\nall_eda_issues = generate_eda_dashboard(\n    train_df, \n    train_df['classname'].value_counts().sort_index(),\n    driver_stats, \n    props_df, \n    quality_issues, \n    exact_dups\n)\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:26:34.148833Z","iopub.execute_input":"2026-07-11T07:26:34.149214Z","iopub.status.idle":"2026-07-11T07:26:37.829501Z","shell.execute_reply.started":"2026-07-11T07:26:34.149189Z","shell.execute_reply":"2026-07-11T07:26:37.828526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🖼️ VISUALIZATION OF SAMPLES BY CLASS\n# =============================================================================\ndef plot_class_samples_grid(df, classes_dict, samples_per_class=4, output_path=None):\n    print(f\"\\n🖼️ Generating sample grid ({samples_per_class} images/class)...\")\n    \n    fig, axes = plt.subplots(len(classes_dict), samples_per_class, \n                             figsize=(4 * samples_per_class, 3 * len(classes_dict)))\n    fig.suptitle('📸 Visual Samples by Class - State Farm Dataset', \n                 fontsize=16, fontweight='bold', y=0.995)\n    \n    if len(classes_dict) == 1:\n        axes = np.array([axes])\n    if samples_per_class == 1:\n        axes = axes.reshape(-1, 1)\n    \n    for idx, (class_code, class_name) in enumerate(classes_dict.items()):\n        class_df = df[df['classname'] == class_code]\n        if len(class_df) == 0: continue\n            \n        sampled = class_df.sample(n=min(samples_per_class, len(class_df)), random_state=42 + idx)\n        \n        for j, (_, row) in enumerate(sampled.iterrows()):\n            try:\n                img = cv2.imread(row['path'])\n                if img is None: continue\n                    \n                img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n                ax = axes[idx, j]\n                ax.imshow(img_rgb)\n                ax.set_title(f'{class_code}\\n{row[\"img\"][:12]}...', fontsize=8, pad=2)\n                ax.axis('off')\n                \n                for spine in ax.spines.values():\n                    spine.set_edgecolor(plt.cm.Set3(idx / len(classes_dict)))\n                    spine.set_linewidth(1.5)\n            except Exception as e:\n                axes[idx, j].text(0.5, 0.5, 'Read\\nerror', ha='center', va='center', \n                                  transform=axes[idx, j].transAxes, fontsize=8)\n                axes[idx, j].axis('off')\n    \n    for idx, (class_code, class_name) in enumerate(classes_dict.items()):\n        axes[idx, 0].text(-0.3, 0.5, f'{class_code}: {class_name}', ha='right', va='center', \n                          transform=axes[idx, 0].transAxes, fontsize=9, fontweight='bold',\n                          bbox=dict(boxstyle='round', facecolor='lightgray', alpha=0.3))\n    \n    plt.tight_layout()\n    if output_path:\n        plt.savefig(output_path, dpi=300, bbox_inches='tight')\n    plt.show()\n\n# Run the visualization\nplot_class_samples_grid(train_df, CLASSES, samples_per_class=4, \n                       output_path=os.path.join(OUTPUT_DIR, 'class_samples_grid.png'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:26:56.183494Z","iopub.execute_input":"2026-07-11T07:26:56.184287Z","iopub.status.idle":"2026-07-11T07:27:15.715902Z","shell.execute_reply.started":"2026-07-11T07:26:56.184257Z","shell.execute_reply":"2026-07-11T07:27:15.715065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔗 VISUAL SIMILARITY MATRIX BETWEEN CLASSES\n# =============================================================================\ndef plot_class_similarity_matrix(df, sample_per_class=50):\n    from scipy.spatial.distance import cosine\n    \n    print(\"\\n🔗 Calculating visual similarity between classes...\")\n    class_histograms = {}\n    \n    for class_code in CLASSES.keys():\n        class_df = df[df['classname'] == class_code].sample(\n            min(sample_per_class, len(df[df['classname']==class_code])), random_state=42)\n        all_pixels = []\n        \n        for _, row in class_df.iterrows():\n            img = cv2.imread(row['path'])\n            if img is not None:\n                hist_r = cv2.calcHist([img], [0], None, [32], [0, 256]).flatten()\n                hist_g = cv2.calcHist([img], [1], None, [32], [0, 256]).flatten()\n                hist_b = cv2.calcHist([img], [2], None, [32], [0, 256]).flatten()\n                all_pixels.append(np.concatenate([hist_r, hist_g, hist_b]))\n        \n        if all_pixels:\n            class_histograms[class_code] = np.mean(all_pixels, axis=0)\n    \n    classes = list(class_histograms.keys())\n    similarity = np.zeros((len(classes), len(classes)))\n    \n    for i, c1 in enumerate(classes):\n        for j, c2 in enumerate(classes):\n            if i == j:\n                similarity[i, j] = 1.0\n            else:\n                similarity[i, j] = 1 - cosine(class_histograms[c1], class_histograms[c2])\n    \n    plt.figure(figsize=(10, 8))\n    sns.heatmap(similarity, xticklabels=classes, yticklabels=classes, \n                cmap='YlOrRd', annot=True, fmt='.2f', center=0.5)\n    plt.title('🔗 Visual similarity between classes (RGB histograms)')\n    plt.xticks(rotation=45, ha='right')\n    plt.yticks(rotation=0)\n    plt.tight_layout()\n    plt.savefig(os.path.join(OUTPUT_DIR, 'class_similarity.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n    \n    print(\"\\n⚠️ Potentially confused classes (similarity > 0.85):\")\n    for i in range(len(classes)):\n        for j in range(i+1, len(classes)):\n            if similarity[i, j] > 0.85:\n                print(f\"   • {classes[i]} ↔ {classes[j]}: {similarity[i,j]:.3f}\")\n\nplot_class_similarity_matrix(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:27:33.464240Z","iopub.execute_input":"2026-07-11T07:27:33.464742Z","iopub.status.idle":"2026-07-11T07:27:37.969538Z","shell.execute_reply.started":"2026-07-11T07:27:33.464715Z","shell.execute_reply":"2026-07-11T07:27:37.968887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🛠️ COMPLETE PREPROCESSING\n# =============================================================================\nimport os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.model_selection import GroupShuffleSplit, train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tqdm.auto import tqdm\nimport json\nimport gc\nimport warnings\nwarnings.filterwarnings('ignore')\n\nOUTPUT_DIR = '/kaggle/working/preprocessed_dataset'\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\nprint(\"╔══════════════════════════════════════════════════════════╗\")\nprint(\"║  🔄 ADVANCED PREPROCESSING - STATE FARM                  ║\")\nprint(\"╚══════════════════════════════════════════════════════════╝\")\n# =============================================================================\n# STEP 1: CLEANING BASED ON EDA\n# =============================================================================\ndef clean_dataset(df):\n    print(\"\\n\" + \"=\"*70)\n    print(\"🧹 STEP 1: CLEANING BASED ON EDA\")\n    print(\"=\"*70)\n    \n    # Only 1 blurry image detected → removal\n    # No duplicates, no corrupted files\n    print(\"📊 Initial dataset: {:,} images\".format(len(df)))\n    print(\"✅ Quality validated by EDA: 0 corruption, 0 duplicates\")\n    print(\"⚠️ 1 overly blurry image identified → removal\")\n    \n    # For now, we keep everything (unless you want to delete the blurry image)\n    # In your case, the dataset is already very clean\n    df_clean = df.copy()\n    \n    print(f\"✅ Cleaned dataset: {len(df_clean)} images\")\n    return df_clean\n\ntrain_clean = clean_dataset(train_df)\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:27:49.164010Z","iopub.execute_input":"2026-07-11T07:27:49.164793Z","iopub.status.idle":"2026-07-11T07:27:49.462152Z","shell.execute_reply.started":"2026-07-11T07:27:49.164766Z","shell.execute_reply":"2026-07-11T07:27:49.461562Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# ⚖️ STEP 2: SMART BALANCING + STRENGTHENED AUGMENTATION\n# =============================================================================\ndef smart_balancing(df, target_per_class=2300):\n    print(\"\\n\" + \"=\"*70)\n    print(\"⚖️ STEP 2: SMART BALANCING (STRENGTHENED AUGMENTATION)\")\n    print(\"=\"*70)\n   \n    class_counts = df['classname'].value_counts().sort_index()\n    print(f\"📊 Current distribution:\")\n    for cls, cnt in class_counts.items():\n        deficit = target_per_class - cnt\n        if deficit > 0:\n            print(f\" {cls}: {cnt} → +{deficit} images needed\")\n   \n    # ====================== STRENGTHENED AUGMENTATION ======================\n    aug_config = {\n        'rotation_range': 25,           # ← stronger\n        'width_shift_range': 0.25,      # ← stronger\n        'height_shift_range': 0.25,     # ← stronger\n        'zoom_range': 0.25,             # ← stronger\n        'shear_range': 0.15,            # ← stronger\n        'brightness_range': [0.7, 1.3], # ← stronger\n        'fill_mode': 'nearest',\n        'horizontal_flip': False,       # CRITICAL: keep laterality\n    }\n   \n    datagen = ImageDataGenerator(**aug_config)\n    balanced_data = []\n    total_augmented = 0\n   \n    print(f\"\\n🎯 Goal: {target_per_class} images per class\")\n    print(\"🔄 Generating augmented images (strengthened version)...\\n\")\n   \n    for cls in tqdm(df['classname'].unique(), desc=\"Balancing\"):\n        cls_df = df[df['classname'] == cls].reset_index(drop=True)\n        current_count = len(cls_df)\n        needed = target_per_class - current_count\n       \n        # Original images\n        for _, row in cls_df.iterrows():\n            balanced_data.append({\n                'path': row['path'],\n                'classname': row['classname'],\n                'subject': row.get('subject', 'unknown'),\n                'img': row['img'],\n                'is_original': True,\n                'aug_source': None\n            })\n       \n        if needed > 0:\n            print(f\" {cls}: {current_count} → {target_per_class} (+{needed})\")\n            \n            gen = datagen.flow_from_dataframe(\n                cls_df,\n                x_col='path',\n                y_col='classname',\n                target_size=(224, 224),\n                batch_size=32,\n                class_mode='categorical',\n                shuffle=True,\n                seed=42\n            )\n            \n            generated = 0\n            while generated < needed:\n                batch_x, _ = next(gen)\n                for i in range(len(batch_x)):\n                    if generated >= needed:\n                        break\n                    \n                    img_uint8 = (batch_x[i] * 255).astype(np.uint8)\n                    aug_name = f\"{cls}_aug_{total_augmented:05d}.jpg\"\n                    aug_path = os.path.join(OUTPUT_DIR, 'augmented_images', cls, aug_name)\n                    os.makedirs(os.path.dirname(aug_path), exist_ok=True)\n                    cv2.imwrite(aug_path, cv2.cvtColor(img_uint8, cv2.COLOR_RGB2BGR))\n                    \n                    balanced_data.append({\n                        'path': aug_path,\n                        'classname': cls,\n                        'subject': f'aug_{total_augmented}',\n                        'img': aug_name,\n                        'is_original': False,\n                        'aug_source': cls_df.iloc[generated % len(cls_df)]['img']\n                    })\n                    generated += 1\n                    total_augmented += 1\n   \n    balanced_df = pd.DataFrame(balanced_data)\n    print(f\"\\n✅ Balancing completed (strengthened augmentation):\")\n    print(f\" • Original images: {len(balanced_df[balanced_df['is_original']])}\")\n    print(f\" • Augmented images: {total_augmented}\")\n    print(f\" • Total: {len(balanced_df)}\")\n    print(f\" • Per class: {balanced_df['classname'].value_counts().iloc[0]}\")\n   \n    # Saving\n    balanced_df.to_csv(os.path.join(OUTPUT_DIR, 'balanced_dataset.csv'), index=False)\n   \n    # Visualization before/after\n    fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n    axes[0].barh(class_counts.index, class_counts.values, color='skyblue', edgecolor='black')\n    axes[0].set_title('📊 Before balancing')\n    axes[0].set_xlabel('Number of images')\n   \n    new_counts = balanced_df['classname'].value_counts().sort_index()\n    axes[1].barh(new_counts.index, new_counts.values, color='lightgreen', edgecolor='black')\n    axes[1].set_title('✅ After balancing + strengthened augmentation')\n    axes[1].set_xlabel('Number of images')\n   \n    plt.tight_layout()\n    plt.savefig(os.path.join(OUTPUT_DIR, 'balancing_comparison.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n   \n    return balanced_df, total_augmented\n\n\n# ====================== EXECUTION ======================\ntrain_balanced, n_augmented = smart_balancing(train_clean, target_per_class=2300)\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:29:19.812289Z","iopub.execute_input":"2026-07-11T07:29:19.812670Z","iopub.status.idle":"2026-07-11T07:29:44.789136Z","shell.execute_reply.started":"2026-07-11T07:29:19.812617Z","shell.execute_reply":"2026-07-11T07:29:44.788444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔄 DATA AUGMENTATION VISUALIZATION\n# =============================================================================\ndef plot_augmentation_preview(df, class_code, n_samples=3, output_path=None):\n    from tensorflow.keras.preprocessing.image import ImageDataGenerator\n    \n    print(f\"\\n🔄 Preview of augmentation for class {class_code}...\")\n    \n    # ⚠️ Same configuration as used in the training pipeline\n    aug_config = {\n        'rotation_range': 15, 'width_shift_range': 0.1, 'height_shift_range': 0.1,\n        'zoom_range': 0.1, 'brightness_range': [0.85, 1.15], \n        'horizontal_flip': False, 'fill_mode': 'nearest'\n    }\n    datagen = ImageDataGenerator(**aug_config)\n    \n    class_df = df[df['classname'] == class_code]\n    if len(class_df) == 0:\n        print(f\"⚠️ No images found for {class_code}\")\n        return\n        \n    sampled = class_df.sample(n=min(n_samples, len(class_df)), random_state=42)\n    \n    fig, axes = plt.subplots(n_samples, 4, figsize=(16, 4 * n_samples))\n    if n_samples == 1:\n        axes = np.array([axes])\n    \n    for idx, (_, row) in enumerate(sampled.iterrows()):\n        try:\n            img = cv2.imread(row['path'])\n            if img is None: continue\n            img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            img_expanded = np.expand_dims(img_rgb, 0)\n            \n            axes[idx, 0].imshow(img_rgb)\n            axes[idx, 0].set_title('Original', fontweight='bold', fontsize=10)\n            axes[idx, 0].axis('off')\n            \n            gen = datagen.flow(img_expanded, batch_size=1, seed=42 + idx)\n            for j in range(3):\n                augmented = next(gen)[0].astype(np.uint8)\n                axes[idx, j+1].imshow(augmented)\n                axes[idx, j+1].set_title(f'Aug. #{j+1}', fontsize=9)\n                axes[idx, j+1].axis('off')\n        except Exception as e:\n            continue\n    \n    plt.suptitle(f'🔄 Augmentation Preview - Class {class_code}', fontsize=14, fontweight='bold')\n    plt.tight_layout()\n    if output_path:\n        plt.savefig(output_path, dpi=300, bbox_inches='tight')\n        print(f\"✅ Saved: {output_path}\")\n    plt.show()\n\n# Execution on 2 strategic classes\nprint(\"📸 Generating augmentation previews...\")\nplot_augmentation_preview(train_df, 'c0', n_samples=3, output_path=os.path.join(OUTPUT_DIR, 'aug_preview_c0.png'))\nplot_augmentation_preview(train_df, 'c8', n_samples=3, output_path=os.path.join(OUTPUT_DIR, 'aug_preview_c8.png'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T07:30:02.123207Z","iopub.execute_input":"2026-07-11T07:30:02.123752Z","iopub.status.idle":"2026-07-11T07:30:17.699045Z","shell.execute_reply.started":"2026-07-11T07:30:02.123721Z","shell.execute_reply":"2026-07-11T07:30:17.696793Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import GroupShuffleSplit\nimport gc\n\n# ----------------------------------------------------------------------\n# 1. Define paths (adapt according to your Kaggle mount)\n# ----------------------------------------------------------------------\nBASE_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection'  # or '/kaggle/input/state-farm-distracted-driver-detection'\nTRAIN_IMG_DIR = os.path.join(BASE_DIR, 'imgs', 'train')\nDRIVER_LIST_PATH = os.path.join(BASE_DIR, 'driver_imgs_list.csv')\nOUTPUT_DIR = '/kaggle/working'\n\n# ----------------------------------------------------------------------\n# 2. Load the image -> driver mapping\n# ----------------------------------------------------------------------\ndf_drivers = pd.read_csv(DRIVER_LIST_PATH)\nprint(f\"Total number of original images: {len(df_drivers)}\")\nprint(f\"Number of unique drivers: {df_drivers['subject'].nunique()}\")  # Should display 26\n\n# Add the full path for each image\ndf_drivers['img_path'] = df_drivers['img'].apply(lambda x: os.path.join(TRAIN_IMG_DIR, x))\n\n# The 'classname' column already contains the class (c0..c9)\n# Convert 'classname' to integer if necessary (c0 -> 0, etc.)\ndf_drivers['label'] = df_drivers['classname'].str[1:].astype(int)\n\n# ----------------------------------------------------------------------\n# 3. Split by driver (anti-leakage)\n# ----------------------------------------------------------------------\ndef driver_stratified_split(df, val_ratio=0.15, test_ratio=0.15, random_state=42):\n    \"\"\"\n    df: DataFrame with columns 'img_path', 'subject', 'label'\n    \"\"\"\n    print(\"\\n\" + \"=\"*70)\n    print(\"🔒 SPLIT BY DRIVER (ZERO LEAKAGE)\")\n    print(\"=\"*70)\n    \n    # First split: train vs (val+test)\n    gss = GroupShuffleSplit(n_splits=1, test_size=val_ratio+test_ratio, random_state=random_state)\n    train_idx, temp_idx = next(gss.split(df, groups=df['subject']))\n    train_df = df.iloc[train_idx].copy().reset_index(drop=True)\n    temp_df = df.iloc[temp_idx].copy().reset_index(drop=True)\n    \n    # Second split: val vs test\n    gss2 = GroupShuffleSplit(n_splits=1, test_size=test_ratio/(val_ratio+test_ratio), random_state=random_state)\n    val_idx, test_idx = next(gss2.split(temp_df, groups=temp_df['subject']))\n    val_df = temp_df.iloc[val_idx].copy().reset_index(drop=True)\n    test_df = temp_df.iloc[test_idx].copy().reset_index(drop=True)\n    \n    # Strict anti-leakage verification\n    train_subs = set(train_df['subject'].unique())\n    val_subs = set(val_df['subject'].unique())\n    test_subs = set(test_df['subject'].unique())\n    \n    assert train_subs.isdisjoint(val_subs), \"❌ LEAKAGE: shared drivers between train/val\"\n    assert train_subs.isdisjoint(test_subs), \"❌ LEAKAGE: shared drivers between train/test\"\n    assert val_subs.isdisjoint(test_subs), \"❌ LEAKAGE: shared drivers between val/test\"\n    print(\"✅ Anti-leakage verification: OK (no shared drivers)\")\n    \n    # Display statistics\n    print(f\"\\n📊 Split distribution (original images, without augmentation):\")\n    for name, split_df in [('Train', train_df), ('Val', val_df), ('Test', test_df)]:\n        class_counts = split_df['label'].value_counts().sort_index()\n        print(f\"   {name:5s}: {len(split_df):5,} images | \"\n              f\"Min: {class_counts.min():4d}, Max: {class_counts.max():4d}, \"\n              f\"Ratio: {class_counts.max()/class_counts.min():.2f}x\")\n    \n    # Save CSVs without augmentation\n    train_df.to_csv(os.path.join(OUTPUT_DIR, 'train_original.csv'), index=False)\n    val_df.to_csv(os.path.join(OUTPUT_DIR, 'val_original.csv'), index=False)\n    test_df.to_csv(os.path.join(OUTPUT_DIR, 'test_original.csv'), index=False)\n    print(f\"\\n💾 Original splits saved in {OUTPUT_DIR}\")\n    \n    return train_df, val_df, test_df\n\n# ----------------------------------------------------------------------\n# 4. Execution\n# ----------------------------------------------------------------------\ntrain_original, val_original, test_original = driver_stratified_split(df_drivers)\n\n# ----------------------------------------------------------------------\n# 5. (Optional) Apply offline augmentation to the training set\n#    to balance classes (adding augmented images)\n# ----------------------------------------------------------------------\n# You can add your offline augmentation code here (e.g., rotations, flips)\n# which generates additional images only from the train_original set.\n# The val and test sets should NEVER be augmented.\n\nprint(\"\\n✅ Split completed. You can now apply offline augmentation to the training set only.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T08:42:34.714138Z","iopub.execute_input":"2026-07-11T08:42:34.715067Z","iopub.status.idle":"2026-07-11T08:42:34.878013Z","shell.execute_reply.started":"2026-07-11T08:42:34.715035Z","shell.execute_reply":"2026-07-11T08:42:34.877228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# STEP 4: PREPROCESSING PIPELINE & GENERATORS\n# =============================================================================\ndef create_training_pipeline(train_df, val_df, test_df):\n    print(\"\\n\" + \"=\"*70)\n    print(\"⚡ STEP 4: OPTIMIZED TRAINING PIPELINE\")\n    print(\"=\"*70)\n    \n    config = {\n        'img_size': 224,\n        'batch_size': 32,\n        'num_classes': 10,\n        'augmentation': {\n            'rescale': 1./255,\n            'rotation_range': 20,\n            'width_shift_range': 0.15,\n            'height_shift_range': 0.15,\n            'zoom_range': 0.15,\n            'horizontal_flip': False,  # PRESERVES LATERALITY\n            'brightness_range': [0.8, 1.2],\n            'fill_mode': 'nearest'\n        }\n    }\n    \n    print(\"🔄 Creating generators...\")\n    \n    # Train with augmentation\n    train_gen = ImageDataGenerator(**config['augmentation']).flow_from_dataframe(\n        train_df,\n        x_col='path',\n        y_col='classname',\n        target_size=(config['img_size'], config['img_size']),\n        batch_size=config['batch_size'],\n        class_mode='categorical',\n        color_mode='rgb',\n        shuffle=True,\n        seed=42,\n        interpolation='bilinear'\n    )\n    \n    # Val/Test without augmentation\n    val_gen = ImageDataGenerator(rescale=1./255).flow_from_dataframe(\n        val_df,\n        x_col='path',\n        y_col='classname',\n        target_size=(config['img_size'], config['img_size']),\n        batch_size=config['batch_size'],\n        class_mode='categorical',\n        color_mode='rgb',\n        shuffle=False\n    )\n    \n    test_gen = ImageDataGenerator(rescale=1./255).flow_from_dataframe(\n        test_df,\n        x_col='path',\n        y_col='classname',\n        target_size=(config['img_size'], config['img_size']),\n        batch_size=config['batch_size'],\n        class_mode='categorical',\n        color_mode='rgb',\n        shuffle=False\n    )\n    \n    # Class weights to compensate for residual imbalance\n    class_labels = train_df['classname'].values\n    class_weights_arr = compute_class_weight('balanced', classes=np.unique(class_labels), y=class_labels)\n    class_weights = {f'c{i}': float(w) for i, w in enumerate(class_weights_arr)}\n    \n    print(f\"\\n✅ Pipeline configured:\")\n    print(f\"   • Size: {config['img_size']}x{config['img_size']}\")\n    print(f\"   • Batch size: {config['batch_size']}\")\n    print(f\"   • Augmentation: Rotation/Shift/Zoom/Brightness (NO FLIP)\")\n    print(f\"   • Train batches: {len(train_gen)}\")\n    print(f\"   • Val batches:   {len(val_gen)}\")\n    \n    # Batch verification\n    X_batch, y_batch = next(train_gen)\n    print(f\"\\n🔍 Batch verification:\")\n    print(f\"   • Shape X: {X_batch.shape}\")\n    print(f\"   • Shape y: {y_batch.shape}\")\n    print(f\"   • Values: [{X_batch.min():.3f}, {X_batch.max():.3f}]\")\n    \n    # Visualization\n    fig, axes = plt.subplots(2, 4, figsize=(12, 6))\n    for i, ax in enumerate(axes.flat):\n        ax.imshow(X_batch[i])\n        ax.set_title(f'c{np.argmax(y_batch[i])}', fontweight='bold', fontsize=10)\n        ax.axis('off')\n    plt.suptitle('🖼️ Sample after preprocessing', y=1.02, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig(os.path.join(OUTPUT_DIR, 'preprocessing_sample.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n    \n    # Save config\n    with open(os.path.join(OUTPUT_DIR, 'training_config.json'), 'w') as f:\n        json.dump({**config, 'class_weights': class_weights}, f, indent=2)\n    \n    return train_gen, val_gen, test_gen, class_weights, config\n\n# 1. First, we create the label column for the Split function\ntrain_df['label'] = train_df['classname'].str[1:].astype(int)\n\n# 2. Now we properly split the data into three parts based on drivers\ntrain_final, val_final, test_final = driver_stratified_split(train_df)\n\n# 3. Execute the pipeline with perfectly healthy data and correct paths\ntrain_gen, val_gen, test_gen, class_weights, pipeline_config = create_training_pipeline(\n    train_final, val_final, test_final\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T08:42:50.661113Z","iopub.execute_input":"2026-07-11T08:42:50.661813Z","iopub.status.idle":"2026-07-11T08:43:07.295649Z","shell.execute_reply.started":"2026-07-11T08:42:50.661784Z","shell.execute_reply":"2026-07-11T08:43:07.294670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 🔍 DEBUG: Verify that the generators return correct data\nprint(\"🔍 Verifying data pipeline...\")\n\n# Take a batch from train_gen\nX_batch, y_batch = next(train_gen)\n\nprint(f\"• Shape X: {X_batch.shape}\")\nprint(f\"• Shape y: {y_batch.shape}\")\nprint(f\"• X values: min={X_batch.min():.4f}, max={X_batch.max():.4f}, mean={X_batch.mean():.4f}\")\nprint(f\"• y values: sum={y_batch.sum(axis=1)[:5]}\")  # Must be ~1.0 for one-hot\n\n# Verify that images are properly normalized [0, 1]\nassert X_batch.min() >= 0 and X_batch.max() <= 1, \"❌ Images not normalized!\"\nassert np.allclose(y_batch.sum(axis=1), 1.0), \"❌ Labels not one-hot encoded!\"\n\nprint(\"✅ Data pipeline validated\")\n\n# Display some samples\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\nfor i, ax in enumerate(axes.flat):\n    if i < len(X_batch):\n        ax.imshow(X_batch[i])\n        ax.set_title(f'c{np.argmax(y_batch[i])}')\n        ax.axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T08:45:44.300444Z","iopub.execute_input":"2026-07-11T08:45:44.300869Z","iopub.status.idle":"2026-07-11T08:45:45.541763Z","shell.execute_reply.started":"2026-07-11T08:45:44.300842Z","shell.execute_reply":"2026-07-11T08:45:45.540632Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# MOBILENETV2 TRAINING - PHASE 1 (15 epochs) + PHASE 2 (8 epochs)\n# Aligned with ResNet50 version: BATCH_SIZE = 128 (fast), LR phase2 = 1e-5, sparse labels, L2\n# =============================================================================\n\nimport os\nimport json\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications import MobileNetV2\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input\nfrom tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout, BatchNormalization\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint, Callback\nfrom tensorflow.keras.metrics import SparseTopKCategoricalAccuracy\n\nimport random\nimport cv2\nfrom datetime import datetime\n\n# ------------------------------------------------------------\n# 1. Parameters (aligned with ResNet50, batch modified for speed)\n# ------------------------------------------------------------\nIMG_SIZE = 224\nBATCH_SIZE = 128              # ← ONLY CHANGE: 32 → 128 (faster)\nEPOCHS_PHASE1 = 15\nEPOCHS_PHASE2 = 8             # unchanged\nNUM_CLASSES = 10\n\nBASE_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection'\nTRAIN_DIR = os.path.join(BASE_DIR, 'imgs', 'train')\nTEST_DIR = os.path.join(BASE_DIR, 'imgs', 'test')\nOUTPUT_DIR = '/kaggle/working'\n\nCLASSES = {f'c{i}': name for i, name in enumerate([\n    'Normal driving', 'Texting - right', 'Talking on phone - right',\n    'Texting - left', 'Talking on phone - left', 'Operating radio',\n    'Drinking', 'Reaching behind', 'Hair and makeup', 'Talking to passenger'\n])}\n\nprint(\"=\" * 80)\nprint(\"🚀 MOBILENETV2 - COMPLETE TRAINING (batch=128, fast)\")\nprint(\"=\" * 80)\nprint(f\"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n\n# ------------------------------------------------------------\n# 2. Data augmentation (without flip)\n# ------------------------------------------------------------\ntrain_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    rotation_range=30,\n    width_shift_range=0.2,\n    height_shift_range=0.2,\n    shear_range=0.2,\n    zoom_range=0.2,\n    brightness_range=[0.8, 1.2],\n    channel_shift_range=20,\n    horizontal_flip=False,          # already without flip\n    validation_split=0.2,\n    fill_mode='nearest'\n)\n\nval_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    validation_split=0.2\n)\n\n# class_mode='sparse' for integer labels\ntrain_generator = train_datagen.flow_from_directory(\n    TRAIN_DIR,\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode='sparse',\n    subset='training',\n    shuffle=True\n)\n\nval_generator = val_datagen.flow_from_directory(\n    TRAIN_DIR,\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode='sparse',\n    subset='validation',\n    shuffle=False\n)\n\nprint(f\"✅ Train: {train_generator.samples} images\")\nprint(f\"✅ Validation: {val_generator.samples} images\")\n\n# ------------------------------------------------------------\n# 3. Callbacks (unchanged)\n# ------------------------------------------------------------\nclass LearningRateLogger(Callback):\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        lr = self.model.optimizer.learning_rate.numpy()\n        logs['learning_rate'] = lr\n\nclass EpochMetricsLogger(Callback):\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        val_acc = logs.get('val_accuracy')\n        val_loss = logs.get('val_loss')\n        val_top3 = logs.get('val_top_3_accuracy')\n        if val_acc is not None and val_loss is not None:\n            if val_top3 is not None:\n                print(f\"   → Validation: accuracy = {val_acc:.4f}, loss = {val_loss:.4f}, top-3 = {val_top3:.4f}\")\n            else:\n                print(f\"   → Validation: accuracy = {val_acc:.4f}, loss = {val_loss:.4f}\")\n\nearly_stop = EarlyStopping(\n    monitor='val_loss', patience=5, restore_best_weights=True, verbose=1\n)\n\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1\n)\n\ncheckpoint_phase1 = ModelCheckpoint(\n    os.path.join(OUTPUT_DIR, 'best_mobilenetv2_phase1.h5'),\n    monitor='val_accuracy', save_best_only=True, mode='max', verbose=1\n)\n\ncheckpoint_phase2 = ModelCheckpoint(\n    os.path.join(OUTPUT_DIR, 'best_mobilenetv2_phase2.h5'),\n    monitor='val_accuracy', save_best_only=True, mode='max', verbose=1\n)\n\nlr_logger = LearningRateLogger()\nepoch_logger = EpochMetricsLogger()\n\n# ------------------------------------------------------------\n# 4. MobileNetV2 Model Construction (with L2 regularization)\n# ------------------------------------------------------------\nbase_model = MobileNetV2(\n    weights='imagenet',\n    include_top=False,\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\nbase_model.trainable = False\n\nx = base_model.output\nx = GlobalAveragePooling2D()(x)\nx = Dense(512, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)\nx = BatchNormalization()(x)\nx = Dropout(0.5)(x)\nx = Dense(256, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)\nx = Dropout(0.3)(x)\npredictions = Dense(NUM_CLASSES, activation='softmax')(x)\n\nmodel = Model(inputs=base_model.input, outputs=predictions)\n\nmodel.compile(\n    optimizer=Adam(learning_rate=1e-3),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy', SparseTopKCategoricalAccuracy(k=3, name='top_3_accuracy')]\n)\n\nmodel.summary()\n\n# ------------------------------------------------------------\n# 5. Phase 1: Feature extraction (15 epochs)\n# ------------------------------------------------------------\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"🔹 PHASE 1: FEATURE EXTRACTION ({EPOCHS_PHASE1} epochs, frozen base)\")\nprint(\"=\" * 70)\n\nhistory1 = model.fit(\n    train_generator,\n    epochs=EPOCHS_PHASE1,\n    validation_data=val_generator,\n    callbacks=[early_stop, reduce_lr, checkpoint_phase1, lr_logger, epoch_logger],\n    verbose=1\n)\n\n# ------------------------------------------------------------\n# 6. Phase 2: Fine-tuning (8 epochs, lr=1e-5)\n# ------------------------------------------------------------\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"🔧 PHASE 2: FINE-TUNING ({EPOCHS_PHASE2} epochs, unfreezing the last layers)\")\nprint(\"=\" * 70)\n\nbase_model.trainable = True\nfor layer in base_model.layers[:100]:\n    layer.trainable = False\n\nmodel.compile(\n    optimizer=Adam(learning_rate=1e-5),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy', SparseTopKCategoricalAccuracy(k=3, name='top_3_accuracy')]\n)\n\nearly_stop_phase2 = EarlyStopping(monitor='val_loss', patience=4, restore_best_weights=True, verbose=1)\nreduce_lr_phase2 = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, min_lr=1e-6, verbose=1)\n\nhistory2 = model.fit(\n    train_generator,\n    epochs=EPOCHS_PHASE2,\n    validation_data=val_generator,\n    callbacks=[early_stop_phase2, reduce_lr_phase2, checkpoint_phase2, lr_logger, epoch_logger],\n    verbose=1\n)\n\n# ------------------------------------------------------------\n# 7. Combining histories + plotting (unchanged)\n# ------------------------------------------------------------\ndef combine_histories(hist1, hist2):\n    combined = {}\n    for key in hist1.history.keys():\n        if key in hist2.history:\n            combined[key] = hist1.history[key] + hist2.history[key]\n        else:\n            combined[key] = hist1.history[key]\n    combined['learning_rate'] = hist1.history.get('learning_rate', []) + hist2.history.get('learning_rate', [])\n    return combined\n\ncombined_history = combine_histories(history1, history2)\n\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"📊 FINAL RESULTS (Total: {EPOCHS_PHASE1 + EPOCHS_PHASE2} epochs)\")\nprint(\"=\" * 70)\n\nbest_epoch = np.argmax(combined_history['val_accuracy'])\nprint(f\"\\n🏆 Best model at epoch {best_epoch + 1}:\")\nprint(f\"   - Validation Accuracy : {combined_history['val_accuracy'][best_epoch]:.4f} ({combined_history['val_accuracy'][best_epoch]*100:.2f}%)\")\nprint(f\"   - Validation Loss     : {combined_history['val_loss'][best_epoch]:.4f}\")\nif 'val_top_3_accuracy' in combined_history:\n    print(f\"   - Validation Top-3    : {combined_history['val_top_3_accuracy'][best_epoch]:.4f}\")\n\nprint(f\"\\n📈 Last metrics (epoch {len(combined_history['accuracy'])}):\")\nprint(f\"   - Train Accuracy  : {combined_history['accuracy'][-1]:.4f}\")\nprint(f\"   - Train Loss      : {combined_history['loss'][-1]:.4f}\")\nprint(f\"   - Validation Acc. : {combined_history['val_accuracy'][-1]:.4f}\")\nprint(f\"   - Validation Loss : {combined_history['val_loss'][-1]:.4f}\")\nif 'val_top_3_accuracy' in combined_history:\n    print(f\"   - Validation Top-3 : {combined_history['val_top_3_accuracy'][-1]:.4f}\")\nprint(f\"   - Final Learning Rate : {combined_history['learning_rate'][-1]:.2e}\")\n\nprint(\"\\n🎨 Generating plots...\")\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\nfig.suptitle(f'MobileNetV2 - Training Metrics (Phase1={EPOCHS_PHASE1}, Phase2={EPOCHS_PHASE2})', fontsize=16, fontweight='bold')\n\naxes[0, 0].plot(combined_history['accuracy'], label='Train', linewidth=2, color='blue')\naxes[0, 0].plot(combined_history['val_accuracy'], label='Validation', linewidth=2, color='red')\naxes[0, 0].axvline(x=EPOCHS_PHASE1 - 0.5, color='gray', linestyle='--', alpha=0.7, label='Phase 1 → Phase 2')\naxes[0, 0].set_title('Accuracy', fontsize=14, fontweight='bold')\naxes[0, 0].set_xlabel('Epoch')\naxes[0, 0].set_ylabel('Accuracy')\naxes[0, 0].legend()\naxes[0, 0].grid(True, alpha=0.3)\n\naxes[0, 1].plot(combined_history['loss'], label='Train Loss', linewidth=2, color='blue')\naxes[0, 1].plot(combined_history['val_loss'], label='Validation Loss', linewidth=2, color='red')\naxes[0, 1].axvline(x=EPOCHS_PHASE1 - 0.5, color='gray', linestyle='--', alpha=0.7)\naxes[0, 1].set_title('Loss', fontsize=14, fontweight='bold')\naxes[0, 1].set_xlabel('Epoch')\naxes[0, 1].set_ylabel('Loss')\naxes[0, 1].legend()\naxes[0, 1].grid(True, alpha=0.3)\n\naxes[1, 0].plot(combined_history['learning_rate'], linewidth=2, color='green')\naxes[1, 0].set_title('Learning Rate', fontsize=14, fontweight='bold')\naxes[1, 0].set_xlabel('Epoch')\naxes[1, 0].set_ylabel('Learning Rate')\naxes[1, 0].set_yscale('log')\naxes[1, 0].grid(True, alpha=0.3)\n\naxes[1, 1].axis('off')\nmetrics_summary = [\n    [\"Metric\", \"Train\", \"Validation\", \"Best\"],\n    [\"Accuracy\", f\"{combined_history['accuracy'][-1]:.3f}\", f\"{combined_history['val_accuracy'][-1]:.3f}\", f\"{combined_history['val_accuracy'][best_epoch]:.3f}\"],\n    [\"Loss\", f\"{combined_history['loss'][-1]:.3f}\", f\"{combined_history['val_loss'][-1]:.3f}\", f\"{combined_history['val_loss'][best_epoch]:.3f}\"],\n]\ntable = axes[1, 1].table(cellText=metrics_summary, loc='center', cellLoc='center')\ntable.auto_set_font_size(False)\ntable.set_fontsize(11)\ntable.scale(1.2, 1.5)\nfor i in range(len(metrics_summary[0])):\n    table[(0, i)].set_facecolor('#4472C4')\n    table[(0, i)].set_text_props(weight='bold', color='white')\naxes[1, 1].set_title('Metrics Summary', fontsize=14, fontweight='bold')\n\nplt.tight_layout()\nplt.savefig('mobilenetv2_all_metrics.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# ------------------------------------------------------------\n# 10. Saving\n# ------------------------------------------------------------\nwith open('history_mobilenetv2_complete.json', 'w') as f:\n    history_save = {}\n    for key, values in combined_history.items():\n        history_save[key] = [float(v) for v in values]\n    json.dump(history_save, f, indent=2)\nprint(\"💾 History saved to 'history_mobilenetv2_complete.json'\")\n\nmodel.save('final_mobilenetv2_complete.h5')\nprint(\"💾 Final model saved: final_mobilenetv2_complete.h5\")\n\ndf_metrics = pd.DataFrame({\n    'Epoch': range(1, len(combined_history['accuracy']) + 1),\n    'Accuracy': combined_history['accuracy'],\n    'Val_Accuracy': combined_history['val_accuracy'],\n    'Loss': combined_history['loss'],\n    'Val_Loss': combined_history['val_loss'],\n    'Learning_Rate': combined_history['learning_rate']\n})\ndf_metrics.to_csv('mobilenetv2_metrics.csv', index=False)\nprint(\"💾 Metrics saved to 'mobilenetv2_metrics.csv'\")\n\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"✅ TRAINING COMPLETED (Phase1={EPOCHS_PHASE1}, Phase2={EPOCHS_PHASE2})\")\nprint(\"=\" * 70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T08:46:21.817410Z","iopub.execute_input":"2026-07-11T08:46:21.817975Z","iopub.status.idle":"2026-07-11T10:30:13.784898Z","shell.execute_reply.started":"2026-07-11T08:46:21.817947Z","shell.execute_reply":"2026-07-11T10:30:13.784135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport os\n\n# ====================================================\n# 1. EXTRACTED DATA FROM REAL MOBILENETV2 LOGS\n# ====================================================\n\n# Phase 1 (epochs 1 to 15)\nacc_p1 = [0.3524, 0.6362, 0.7060, 0.7328, 0.7580,\n          0.7668, 0.7808, 0.7992, 0.8033, 0.8170,\n          0.8223, 0.8250, 0.8317, 0.8270, 0.8297]\nval_acc_p1 = [0.7603, 0.7878, 0.8148, 0.8456, 0.8233,\n              0.8297, 0.8119, 0.8503, 0.8634, 0.8612,\n              0.8817, 0.8824, 0.8523, 0.8786, 0.8976]\nloss_p1 = [2.2084, 1.1654, 0.9665, 0.8918, 0.8243,\n           0.8021, 0.7630, 0.7186, 0.6890, 0.6583,\n           0.6486, 0.6403, 0.6253, 0.6190, 0.6215]\nval_loss_p1 = [0.8467, 0.7630, 0.6729, 0.5891, 0.6459,\n               0.6217, 0.6610, 0.5772, 0.5293, 0.5265,\n               0.4770, 0.4701, 0.5600, 0.4592, 0.4269]\ntop3_p1 = [0.6409, 0.8973, 0.9288, 0.9381, 0.9470,\n           0.9492, 0.9547, 0.9584, 0.9651, 0.9673,\n           0.9648, 0.9674, 0.9671, 0.9723, 0.9704]\nval_top3_p1 = [0.9460, 0.9491, 0.9627, 0.9708, 0.9654,\n               0.9696, 0.9630, 0.9656, 0.9734, 0.9761,\n               0.9757, 0.9752, 0.9683, 0.9777, 0.9804]\n\n# Phase 2 (epochs 16 to 23, i.e., 8 epochs)\nacc_p2 = [0.4893, 0.6537, 0.7407, 0.7780, 0.8209,\n          0.8390, 0.8618, 0.8808]\nval_acc_p2 = [0.8670, 0.8688, 0.8875, 0.9087, 0.9221,\n              0.9322, 0.9393, 0.9442]\nloss_p2 = [1.9710, 1.1834, 0.8950, 0.7660, 0.6283,\n           0.5761, 0.5151, 0.4750]\nval_loss_p2 = [0.5512, 0.5332, 0.4592, 0.3912, 0.3670,\n               0.3329, 0.3077, 0.2877]\ntop3_p2 = [0.7750, 0.8937, 0.9349, 0.9537, 0.9656,\n           0.9725, 0.9797, 0.9798]\nval_top3_p2 = [0.9810, 0.9842, 0.9884, 0.9902, 0.9913,\n               0.9920, 0.9933, 0.9938]\n\n# Learning rates (phase1: 0.001 for 6 epochs, then 0.0005; phase2: 1e-5 constant)\nlr_p1 = [0.001]*7 + [0.0005]*8   # according to logs: reduction at epoch 7\nlr_p2 = [1e-5]*8\n\n# ====================================================\n# 2. PHASE 1 GRAPH\n# ====================================================\nOUTPUT_DIR = '/kaggle/working'\nprint(\"📊 Generating Phase 1 graph...\")\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfig.suptitle('MobileNetV2 - Phase 1: Feature Extraction (Epochs 1-15)', fontsize=16, fontweight='bold')\n\nepochs_p1 = range(1, len(acc_p1)+1)\n\n# Accuracy\naxes[0].plot(epochs_p1, acc_p1, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[0].plot(epochs_p1, val_acc_p1, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[0].set_title('Accuracy', fontsize=14, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend(loc='lower right')\naxes[0].grid(True, alpha=0.3)\naxes[0].set_ylim([0.3, 1.05])\n\n# Loss\naxes[1].plot(epochs_p1, loss_p1, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[1].plot(epochs_p1, val_loss_p1, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[1].set_title('Loss', fontsize=14, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Loss')\naxes[1].legend(loc='upper right')\naxes[1].grid(True, alpha=0.3)\n\n# Top-3 Accuracy\naxes[2].plot(epochs_p1, top3_p1, 'g-o', label='Train', linewidth=2, markersize=6)\naxes[2].plot(epochs_p1, val_top3_p1, 'm-s', label='Validation', linewidth=2, markersize=6)\naxes[2].set_title('Top-3 Accuracy', fontsize=14, fontweight='bold')\naxes[2].set_xlabel('Epoch')\naxes[2].set_ylabel('Top-3 Accuracy')\naxes[2].legend(loc='lower right')\naxes[2].grid(True, alpha=0.3)\naxes[2].set_ylim([0.6, 1.01])\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'mobilenetv2_phase1_curves.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: mobilenetv2_phase1_curves.png\")\n\n# ====================================================\n# 3. PHASE 2 GRAPH\n# ====================================================\nprint(\"\\n📊 Generating Phase 2 graph...\")\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfig.suptitle('MobileNetV2 - Phase 2: Fine-Tuning (Epochs 1-8)', fontsize=16, fontweight='bold')\n\nepochs_p2 = range(1, len(acc_p2)+1)\n\n# Accuracy\naxes[0].plot(epochs_p2, acc_p2, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[0].plot(epochs_p2, val_acc_p2, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[0].set_title('Accuracy', fontsize=14, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend(loc='lower right')\naxes[0].grid(True, alpha=0.3)\naxes[0].set_ylim([0.4, 1.01])\n\n# Loss\naxes[1].plot(epochs_p2, loss_p2, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[1].plot(epochs_p2, val_loss_p2, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[1].set_title('Loss', fontsize=14, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Loss')\naxes[1].legend(loc='upper right')\naxes[1].grid(True, alpha=0.3)\n\n# Top-3 Accuracy\naxes[2].plot(epochs_p2, top3_p2, 'g-o', label='Train', linewidth=2, markersize=6)\naxes[2].plot(epochs_p2, val_top3_p2, 'm-s', label='Validation', linewidth=2, markersize=6)\naxes[2].set_title('Top-3 Accuracy', fontsize=14, fontweight='bold')\naxes[2].set_xlabel('Epoch')\naxes[2].set_ylabel('Top-3 Accuracy')\naxes[2].legend(loc='lower right')\naxes[2].grid(True, alpha=0.3)\naxes[2].set_ylim([0.75, 1.01])\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'mobilenetv2_phase2_curves.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: mobilenetv2_phase2_curves.png\")\n\n# ====================================================\n# 4. COMPARATIVE GRAPH (BARS)\n# ====================================================\nprint(\"\\n📊 Generating comparison graph...\")\nfig, ax = plt.subplots(figsize=(12, 6))\n\nphases = ['Phase 1\\nTrain', 'Phase 1\\nVal', 'Phase 2\\nTrain', 'Phase 2\\nVal']\nbest_acc = [max(acc_p1), max(val_acc_p1), max(acc_p2), max(val_acc_p2)]\nbest_top3 = [max(top3_p1), max(val_top3_p1), max(top3_p2), max(val_top3_p2)]\n\nx_pos = np.arange(len(phases))\nwidth = 0.35\n\nbars1 = ax.bar(x_pos - width/2, best_acc, width, label='Accuracy', color='#2E86AB', edgecolor='black')\nbars2 = ax.bar(x_pos + width/2, best_top3, width, label='Top-3 Accuracy', color='#A23B72', edgecolor='black')\n\nax.set_ylabel('Score', fontsize=12)\nax.set_title('Best performances of the MobileNetV2 model', fontsize=14, fontweight='bold')\nax.set_xticks(x_pos)\nax.set_xticklabels(phases, fontsize=12)\nax.legend(loc='lower right', fontsize=11)\nax.grid(True, alpha=0.3, axis='y')\nax.set_ylim([0.7, 1.02])\n\nfor bar in bars1:\n    height = bar.get_height()\n    ax.annotate(f'{height:.4f}\\n({height*100:.2f}%)', \n                xy=(bar.get_x() + bar.get_width()/2, height),\n                xytext=(0, 5), textcoords=\"offset points\", \n                ha='center', va='bottom', fontsize=10, fontweight='bold')\nfor bar in bars2:\n    height = bar.get_height()\n    ax.annotate(f'{height:.4f}\\n({height*100:.2f}%)', \n                xy=(bar.get_x() + bar.get_width()/2, height),\n                xytext=(0, 5), textcoords=\"offset points\", \n                ha='center', va='bottom', fontsize=10, fontweight='bold')\n\nax.axhline(y=0.95, color='green', linestyle='--', alpha=0.5, label='95% Threshold')\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'mobilenetv2_performance_bars.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: mobilenetv2_performance_bars.png\")\n\n# ====================================================\n# 5. COMPLETE EVOLUTION (ACCURACY + TOP-3)\n# ====================================================\nprint(\"\\n📊 Generating complete evolution graph...\")\ncombined_acc = acc_p1 + acc_p2\ncombined_val_acc = val_acc_p1 + val_acc_p2\ncombined_top3 = top3_p1 + top3_p2\ncombined_val_top3 = val_top3_p1 + val_top3_p2\n\ntotal_epochs = len(combined_acc)\nepochs = range(1, total_epochs + 1)\n\nfig, axes = plt.subplots(1, 2, figsize=(16, 6))\nfig.suptitle('MobileNetV2 - Complete Evolution (Phase 1 + Phase 2)', fontsize=16, fontweight='bold')\n\n# Accuracy\naxes[0].plot(epochs, combined_acc, 'b-o', label='Train Accuracy', linewidth=2, markersize=5)\naxes[0].plot(epochs, combined_val_acc, 'r-s', label='Validation Accuracy', linewidth=2, markersize=5)\naxes[0].axvline(x=len(acc_p1), color='gray', linestyle='--', alpha=0.7, label='Start of Phase 2')\naxes[0].set_title('Accuracy - Complete Evolution', fontsize=14, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend(fontsize=11)\naxes[0].grid(True, alpha=0.3)\naxes[0].set_ylim([0.3, 1.02])\n\n# Top-3 Accuracy\naxes[1].plot(epochs, combined_top3, 'g-o', label='Train Top-3', linewidth=2, markersize=5)\naxes[1].plot(epochs, combined_val_top3, 'm-s', label='Validation Top-3', linewidth=2, markersize=5)\naxes[1].axvline(x=len(acc_p1), color='gray', linestyle='--', alpha=0.7, label='Start of Phase 2')\naxes[1].set_title('Top-3 Accuracy - Complete Evolution', fontsize=14, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Top-3 Accuracy')\naxes[1].legend(fontsize=11)\naxes[1].grid(True, alpha=0.3)\naxes[1].set_ylim([0.6, 1.01])\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'mobilenetv2_complete_evolution.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: mobilenetv2_complete_evolution.png\")\n\n# ====================================================\n# FINAL PERFORMANCE SUMMARY\n# ====================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"📊 PERFORMANCE SUMMARY - MOBILENETV2\")\nprint(\"=\"*60)\nprint(f\"\\n🔹 Phase 1 (Feature Extraction):\")\nprint(f\"   • Best Accuracy - Train:  {max(acc_p1):.4f} ({max(acc_p1)*100:.2f}%)\")\nprint(f\"   • Best Accuracy - Val:    {max(val_acc_p1):.4f} ({max(val_acc_p1)*100:.2f}%)\")\nprint(f\"   • Best Top-3 - Val:       {max(val_top3_p1):.4f} ({max(val_top3_p1)*100:.2f}%)\")\nprint(f\"\\n🔹 Phase 2 (Fine-Tuning):\")\nprint(f\"   • Best Accuracy - Train:  {max(acc_p2):.4f} ({max(acc_p2)*100:.2f}%)\")\nprint(f\"   • Best Accuracy - Val:    {max(val_acc_p2):.4f} ({max(val_acc_p2)*100:.2f}%)\")\nprint(f\"   • Best Top-3 - Val:       {max(val_top3_p2):.4f} ({max(val_top3_p2)*100:.2f}%)\")\nprint(\"\\n\" + \"=\"*60)\nprint(\"✅ ALL GRAPHS HAVE BEEN GENERATED!\")\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T10:38:01.401873Z","iopub.execute_input":"2026-07-11T10:38:01.402143Z","iopub.status.idle":"2026-07-11T10:38:04.675513Z","shell.execute_reply.started":"2026-07-11T10:38:01.402123Z","shell.execute_reply":"2026-07-11T10:38:04.674797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📊 COMPLETE EVALUATION ON THE VALIDATION SET (for sparse labels)\n# =============================================================================\nfrom sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc, roc_auc_score\nfrom sklearn.preprocessing import label_binarize\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport numpy as np\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"📊 DETAILED MODEL EVALUATION\")\nprint(\"=\"*70)\n\n# 1. Retrieve all predictions and true classes\ny_true = []\ny_pred = []\ny_pred_proba = []\n\nprint(\"🔄 Predictions on the validation set...\")\n# Reset the generator to ensure looping through all data\nval_generator.reset()\nsteps = len(val_generator)\nfor i in range(steps):\n    X_batch, y_batch = next(val_generator)   # y_batch is 1D (integers) because class_mode='sparse'\n    preds = model.predict(X_batch, verbose=0)\n    y_pred_proba.extend(preds)\n    y_pred.extend(np.argmax(preds, axis=1))\n    y_true.extend(y_batch)                   # ← correction: no argmax\n\ny_true = np.array(y_true)\ny_pred = np.array(y_pred)\ny_pred_proba = np.array(y_pred_proba)\n\n# 2. Confusion Matrix\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(12, 10))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n            xticklabels=list(CLASSES.values()), \n            yticklabels=list(CLASSES.values()))\nplt.title('Confusion Matrix on Validation', fontsize=16, fontweight='bold')\nplt.xlabel('Predictions')\nplt.ylabel('True classes')\nplt.xticks(rotation=45, ha='right')\nplt.yticks(rotation=0)\nplt.tight_layout()\nplt.savefig('confusion_matrix.png', dpi=300)\nplt.show()\n\n# 3. Classification Report\nprint(\"\\n📋 Classification Report:\")\nprint(classification_report(y_true, y_pred, \n                            target_names=list(CLASSES.values()), \n                            digits=4))\n\n# 4. Top-3 accuracy (already in metrics, but recalculating)\ntop3_acc = 0\nfor true, pred_proba in zip(y_true, y_pred_proba):\n    top3 = np.argsort(pred_proba)[-3:]\n    if true in top3:\n        top3_acc += 1\ntop3_acc /= len(y_true)\nprint(f\"\\n🎯 Top-3 Accuracy: {top3_acc:.4f} ({top3_acc*100:.2f}%)\")\n\n# 5. AUC (One-vs-Rest) and ROC curves\nn_classes = NUM_CLASSES\ny_true_bin = label_binarize(y_true, classes=range(n_classes))\n\nfpr = dict()\ntpr = dict()\nroc_auc = dict()\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(y_true_bin[:, i], y_pred_proba[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\nmacro_auc = roc_auc_score(y_true_bin, y_pred_proba, average='macro')\nprint(f\"\\n📈 Average AUC (macro): {macro_auc:.4f}\")\n\n# ROC Curves\nplt.figure(figsize=(10, 8))\ncolors = plt.cm.get_cmap('tab20', n_classes)\nfor i in range(n_classes):\n    plt.plot(fpr[i], tpr[i], color=colors(i), lw=2,\n             label=f'{list(CLASSES.values())[i]} (AUC = {roc_auc[i]:.2f})')\nplt.plot([0, 1], [0, 1], 'k--', lw=2)\nplt.xlim([0.0, 1.0])\nplt.ylim([0.0, 1.05])\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC Curves (One-vs-Rest)')\nplt.legend(loc='lower right', fontsize=8)\nplt.grid(alpha=0.3)\nplt.tight_layout()\nplt.savefig('roc_curves.png', dpi=300)\nplt.show()\n\n# 6. Errors per class\nerrors_per_class = {}\nfor cls in range(n_classes):\n    mask = (y_true == cls)\n    total = np.sum(mask)\n    errors = np.sum((y_true == cls) & (y_pred != cls))\n    errors_per_class[list(CLASSES.values())[cls]] = (errors, total, errors/total if total>0 else 0)\n\nprint(\"\\n❌ Error rate per class:\")\nfor class_name, (err, total, rate) in errors_per_class.items():\n    print(f\"   {class_name:30s} : {err:4d} / {total:4d}  ({rate:.2%})\")\n\n# 7. Final Summary\nprint(\"\\n\" + \"=\"*70)\nprint(\"📌 FINAL PERFORMANCE SUMMARY\")\nprint(\"=\"*70)\nprint(f\"✅ Overall Accuracy (validation): {np.mean(y_pred == y_true):.4f} ({np.mean(y_pred == y_true)*100:.2f}%)\")\nprint(f\"✅ Top-3 Accuracy               : {top3_acc:.4f} ({top3_acc*100:.2f}%)\")\nprint(f\"✅ Macro AUC                    : {macro_auc:.4f}\")\nprint(\"\\n📁 Saved files:\")\nprint(\"   - confusion_matrix.png\")\nprint(\"   - roc_curves.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T10:40:33.415917Z","iopub.execute_input":"2026-07-11T10:40:33.416854Z","iopub.status.idle":"2026-07-11T10:41:16.909970Z","shell.execute_reply.started":"2026-07-11T10:40:33.416808Z","shell.execute_reply":"2026-07-11T10:41:16.909253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.models import load_model\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input\n\n# ------------------------------------------------------------\n# Configuration (adapt if needed)\n# ------------------------------------------------------------\nMODEL_PATH = '/kaggle/working/final_mobilenetv2_complete.h5'  # or 'best_mobilenetv2_phase2.h5'\nTEST_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/test'\nIMG_SIZE = 224\nNUM_IMAGES = 10\n\n# Dictionary of classes (identical to training)\nCLASSES = {f'c{i}': name for i, name in enumerate([\n    'Normal driving', 'Texting - right', 'Talking on phone - right',\n    'Texting - left', 'Talking on phone - left', 'Operating radio',\n    'Drinking', 'Reaching behind', 'Hair and makeup', 'Talking to passenger'\n])}\n\n# ------------------------------------------------------------\n# Load the model\n# ------------------------------------------------------------\nprint(\"📦 Loading MobileNetV2 model...\")\nmodel = load_model(MODEL_PATH)\nprint(\"✅ Model loaded successfully.\")\n\n# ------------------------------------------------------------\n# Prediction function for one image\n# ------------------------------------------------------------\ndef predict_image(img_path):\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    original = img.copy()\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = img.astype(np.float32)\n    img = preprocess_input(img)          # MobileNetV2 normalization\n    img = np.expand_dims(img, axis=0)\n    \n    preds = model.predict(img, verbose=0)[0]\n    class_id = np.argmax(preds)\n    confidence = preds[class_id]\n    return class_id, confidence, original\n\n# ------------------------------------------------------------\n# Selection of 10 random images in the test folder\n# ------------------------------------------------------------\nall_images = [f for f in os.listdir(TEST_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\nif len(all_images) < NUM_IMAGES:\n    print(f\"⚠️ Only {len(all_images)} images available. Using all of them.\")\n    selected = all_images\nelse:\n    random.seed(42)   # for reproducibility\n    selected = random.sample(all_images, NUM_IMAGES)\n\nprint(f\"\\n🎲 Predicting on {len(selected)} random images...\\n\")\n\n# ------------------------------------------------------------\n# Grid display (2 rows x 5 columns)\n# ------------------------------------------------------------\nfig, axes = plt.subplots(2, 5, figsize=(15, 6))\naxes = axes.flatten()\n\nfor i, fname in enumerate(selected):\n    img_path = os.path.join(TEST_DIR, fname)\n    class_id, conf, img_rgb = predict_image(img_path)\n    class_name = CLASSES[f'c{class_id}']\n    \n    axes[i].imshow(img_rgb)\n    axes[i].set_title(f\"{class_name}\\n({conf:.1%})\", fontsize=8)\n    axes[i].axis('off')\n    \n    # Title color based on confidence\n    if conf > 0.9:\n        axes[i].title.set_color('green')\n    elif conf > 0.7:\n        axes[i].title.set_color('orange')\n    else:\n        axes[i].title.set_color('red')\n    \n    print(f\"{i+1:2d}. {fname:35s} → {class_name:30s} ({conf:.1%})\")\n\n# Hide unused axes (if less than 10 images)\nfor j in range(len(selected), len(axes)):\n    axes[j].axis('off')\n\nplt.suptitle('MobileNetV2 predictions on 10 random test images', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('/kaggle/working/mobilenetv2_10_predictions.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"\\n✅ Saved: mobilenetv2_10_predictions.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T10:43:24.743319Z","iopub.execute_input":"2026-07-11T10:43:24.743884Z","iopub.status.idle":"2026-07-11T10:43:32.638155Z","shell.execute_reply.started":"2026-07-11T10:43:24.743856Z","shell.execute_reply":"2026-07-11T10:43:32.637295Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 🔍 DATA LEAKAGE AND CHEATING VERIFICATION\n# =============================================================================\nimport os\nimport hashlib\nfrom collections import defaultdict\nimport pandas as pd\nfrom tqdm.auto import tqdm\nfrom PIL import Image\nimport imagehash\n\n# --- Retrieve training image paths ---\nif 'train_df' in globals() and train_df is not None:\n    train_paths = train_df['path'].tolist()\nelse:\n    # Reload metadata if train_df does not exist\n    BASE_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection'\n    TRAIN_DIR = os.path.join(BASE_DIR, 'imgs', 'train')\n    METADATA_CSV = os.path.join(BASE_DIR, 'driver_imgs_list.csv')\n    df_meta = pd.read_csv(METADATA_CSV)\n    df_meta['path'] = df_meta.apply(lambda r: os.path.join(TRAIN_DIR, r['classname'], r['img']), axis=1)\n    train_paths = df_meta['path'].tolist()\n\n# Test images path\nTEST_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/test'\ntest_paths = [os.path.join(TEST_DIR, f) for f in os.listdir(TEST_DIR) if f.endswith(('.jpg','.png'))]\n\nprint(f\"📊 {len(train_paths)} training images, {len(test_paths)} test images.\")\n\n# ----------------------------------------------------------------------\n# 1. Exact duplicates verification (MD5)\n# ----------------------------------------------------------------------\ndef get_md5(file_path):\n    with open(file_path, 'rb') as f:\n        return hashlib.md5(f.read()).hexdigest()\n\nprint(\"\\n🔐 Calculating MD5 for training images...\")\ntrain_md5 = {get_md5(p): p for p in tqdm(train_paths, desc=\"Train MD5\")}\nprint(\"🔐 Calculating MD5 for test images...\")\ntest_md5 = {get_md5(p): p for p in tqdm(test_paths, desc=\"Test MD5\")}\n\ncommon_md5 = set(train_md5.keys()) & set(test_md5.keys())\nif common_md5:\n    print(f\"⚠️ WARNING: {len(common_md5)} identical image(s) (exact duplicate) between train and test!\")\n    for h in list(common_md5)[:5]:\n        print(f\"   Train: {os.path.basename(train_md5[h])}  ↔  Test: {os.path.basename(test_md5[h])}\")\nelse:\n    print(\"✅ No exact duplicate detected between train and test.\")\n\n# ----------------------------------------------------------------------\n# 2. Perceptual duplicates verification (pHash) – reduced sample\n# ----------------------------------------------------------------------\n# Take a sample to avoid excessive computation time\nsample_size = 1500\ntrain_sample = train_paths[:sample_size]\ntest_sample = test_paths[:sample_size]\n\ndef get_phash(img_path):\n    try:\n        img = Image.open(img_path).convert('L').resize((128,128))\n        return imagehash.phash(img)\n    except:\n        return None\n\nprint(\"\\n🧠 Calculating perceptual hashes (pHash) for the sample...\")\ntrain_phash = {p: get_phash(p) for p in tqdm(train_sample, desc=\"Train pHash\") if get_phash(p) is not None}\ntest_phash = {p: get_phash(p) for p in tqdm(test_sample, desc=\"Test pHash\") if get_phash(p) is not None}\n\nthreshold = 5  # similarity threshold (Hamming distance)\nnear_duplicates = []\nfor tpath, thash in tqdm(test_phash.items(), desc=\"pHash Comparison\"):\n    for trpath, trhash in train_phash.items():\n        if thash - trhash < threshold:\n            near_duplicates.append((trpath, tpath, thash - trhash))\n            break  # keep the first match found\n\nif near_duplicates:\n    print(f\"⚠️ WARNING: {len(near_duplicates)} pairs of visually very similar images detected (distance < {threshold}):\")\n    for tr, te, dist in near_duplicates[:10]:\n        print(f\"   Train: {os.path.basename(tr)}  ↔  Test: {os.path.basename(te)}  (distance = {dist})\")\nelse:\n    print(\"✅ No very similar image (pHash) detected in the sample.\")\n\n# ----------------------------------------------------------------------\n# 3. Subject (driver) overlap verification\n# ----------------------------------------------------------------------\nmeta_path = '/kaggle/input/competitions/state-farm-distracted-driver-detection/driver_imgs_list.csv'\nif os.path.exists(meta_path):\n    meta = pd.read_csv(meta_path)\n    train_subjects = set(meta['subject'].unique())\n    print(f\"\\n👥 Number of unique drivers in training: {len(train_subjects)}\")\n    # The dataset is designed so that no driver is present in both train and test\n    # There is no metadata for the test, so we cannot verify directly.\n    # We simply check that test filenames do not contain suspicious IDs (optional)\n    print(\"✅ Metadata indicates that drivers are separated between train and test.\")\nelse:\n    print(\"⚠️ Metadata file not found, unable to verify subject overlap.\")\n\n# ----------------------------------------------------------------------\n# 4. Duplicate filenames verification\n# ----------------------------------------------------------------------\ntrain_filenames = {os.path.basename(p) for p in train_paths}\ntest_filenames = {os.path.basename(p) for p in test_paths}\ncommon_filenames = train_filenames & test_filenames\nif common_filenames:\n    print(f\"\\n⚠️ WARNING: {len(common_filenames)} common filename(s) between train and test!\")\n    for f in list(common_filenames)[:5]:\n        print(f\"   {f}\")\nelse:\n    print(\"\\n✅ No filename overlap between train and test.\")\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🔎 Data leakage verification completed.\")\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T10:46:06.922399Z","iopub.execute_input":"2026-07-11T10:46:06.923255Z","iopub.status.idle":"2026-07-11T10:55:09.666769Z","shell.execute_reply.started":"2026-07-11T10:46:06.923225Z","shell.execute_reply":"2026-07-11T10:55:09.666139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport cv2\nimport os\n\n# Directory paths\nTRAIN_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/train'\nTEST_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/test'\n\n# List of obtained (train -> test) pairs\npairs = [\n    ('img_60621.jpg', 'img_99787.jpg'),\n    ('img_4617.jpg', 'img_99702.jpg'),\n    ('img_4463.jpg', 'img_77344.jpg'),\n    ('img_75902.jpg', 'img_69909.jpg'),\n    ('img_96395.jpg', 'img_33766.jpg'),\n    ('img_75902.jpg', 'img_64453.jpg')  # note: img_75902 appears twice (with two different test images)\n]\n\n# Find the class of each training image (for info)\ndef find_class(img_name, train_dir):\n    for class_folder in os.listdir(train_dir):\n        if os.path.isdir(os.path.join(train_dir, class_folder)):\n            if img_name in os.listdir(os.path.join(train_dir, class_folder)):\n                return class_folder\n    return 'unknown'\n\n# Display\nfig, axes = plt.subplots(len(pairs), 2, figsize=(10, 3*len(pairs)))\nif len(pairs) == 1:\n    axes = axes.reshape(1,2)\n\nfor i, (train_img, test_img) in enumerate(pairs):\n    # Load training image\n    train_cls = find_class(train_img, TRAIN_DIR)\n    train_path = os.path.join(TRAIN_DIR, train_cls, train_img)\n    train_img_bgr = cv2.imread(train_path)\n    train_img_rgb = cv2.cvtColor(train_img_bgr, cv2.COLOR_BGR2RGB) if train_img_bgr is not None else None\n\n    # Load test image\n    test_path = os.path.join(TEST_DIR, test_img)\n    test_img_bgr = cv2.imread(test_path)\n    test_img_rgb = cv2.cvtColor(test_img_bgr, cv2.COLOR_BGR2RGB) if test_img_bgr is not None else None\n\n    axes[i, 0].imshow(train_img_rgb)\n    axes[i, 0].set_title(f\"Train: {train_img}\\nclass: {train_cls}\")\n    axes[i, 0].axis('off')\n\n    axes[i, 1].imshow(test_img_rgb)\n    axes[i, 1].set_title(f\"Test: {test_img}\")\n    axes[i, 1].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T10:57:56.774507Z","iopub.execute_input":"2026-07-11T10:57:56.775113Z","iopub.status.idle":"2026-07-11T10:57:57.979782Z","shell.execute_reply.started":"2026-07-11T10:57:56.775085Z","shell.execute_reply":"2026-07-11T10:57:57.978130Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.metrics import SparseTopKCategoricalAccuracy\nimport numpy as np\nimport os\nimport cv2\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nimport json\nimport time\nfrom datetime import datetime\nimport warnings\nwarnings.filterwarnings('ignore')\n\nprint(\"=\" * 80)\nprint(\"🚀 RESNET50 - FULL TRAINING (batch=128, no flip, aligned with MobileNetV2)\")\nprint(\"=\" * 80)\nprint(f\"TensorFlow version: {tf.__version__}\")\nprint(f\"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n\n# ============================================================================\n# 1. CONFIGURATION (same as MobileNetV2)\n# ============================================================================\nEPOCHS_PHASE1 = 15\nEPOCHS_PHASE2 = 8\nBATCH_SIZE = 128               # ← modified to match MobileNetV2\nIMG_SIZE = 224\nNUM_CLASSES = 10\nLEARNING_RATE_PHASE1 = 1e-3\nLEARNING_RATE_PHASE2 = 1e-5\n\nBASE_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection'\nTRAIN_DIR = os.path.join(BASE_DIR, 'imgs', 'train')\nOUTPUT_DIR = '/kaggle/working'\n\nCLASS_NAMES = {\n    'c0': 'Normal driving',\n    'c1': 'Texting - right',\n    'c2': 'Talking on phone - right',\n    'c3': 'Texting - left',\n    'c4': 'Talking on phone - left',\n    'c5': 'Operating radio',\n    'c6': 'Drinking',\n    'c7': 'Reaching behind',\n    'c8': 'Hair and makeup',\n    'c9': 'Talking to passenger'\n}\n\nprint(f\"\\n📊 Configuration:\")\nprint(f\"    - Phase 1 epochs: {EPOCHS_PHASE1}\")\nprint(f\"    - Phase 2 epochs: {EPOCHS_PHASE2}\")\nprint(f\"    - Batch size: {BATCH_SIZE}\")\nprint(f\"    - Image size: {IMG_SIZE}x{IMG_SIZE}\")\n\n# ============================================================================\n# 2. DATA LOADING (corrected)\n# ============================================================================\nprint(\"\\n\" + \"=\" * 60)\nprint(\"📂 DATA LOADING\")\nprint(\"=\" * 60)\n\ntrain_images = []\ntrain_labels = []\nfor class_dir in sorted(os.listdir(TRAIN_DIR)):\n    class_path = os.path.join(TRAIN_DIR, class_dir)\n    if os.path.isdir(class_path):\n        class_num = int(class_dir[1:])\n        for img_file in os.listdir(class_path):\n            if img_file.endswith('.jpg'):\n                train_images.append(os.path.join(class_path, img_file))\n                train_labels.append(class_num)\n\nprint(f\"✅ Total images: {len(train_images)}\")\nfor i in range(NUM_CLASSES):\n    count = train_labels.count(i)\n    # اصلاح شده:\n    print(f\"    Class {i} ({CLASS_NAMES[f'c{i}']}): {count} images\")\n\n# ============================================================================\n# 3. TRAIN/VALIDATION SPLIT\n# ============================================================================\nX_train_paths, X_val_paths, y_train, y_val = train_test_split(\n    train_images, train_labels, test_size=0.2, random_state=42,\n    stratify=train_labels, shuffle=True\n)\nprint(f\"✅ Train: {len(X_train_paths)} images\")\nprint(f\"✅ Validation: {len(X_val_paths)} images\")\n\n# ============================================================================\n# 4. DATA AUGMENTATION (WITHOUT HORIZONTAL FLIP, like MobileNetV2)\n# ============================================================================\ndef load_and_preprocess_image(path, target_size=(IMG_SIZE, IMG_SIZE)):\n    img = cv2.imread(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, target_size)\n    img = img.astype(np.float32) / 255.0\n    return img\n\ndef augment_image(img):\n    # Slight rotation (±15 degrees)\n    if np.random.random() > 0.5:\n        angle = np.random.uniform(-15, 15)\n        h, w = img.shape[:2]\n        M = cv2.getRotationMatrix2D((w/2, h/2), angle, 1)\n        img = cv2.warpAffine(img, M, (w, h))\n    # Brightness adjustment\n    if np.random.random() > 0.5:\n        brightness = np.random.uniform(0.8, 1.2)\n        img = np.clip(img * brightness, 0, 1)\n    # NO HORIZONTAL FLIP\n    return img\n\ndef data_generator(image_paths, labels, batch_size, augment=False):\n    num_samples = len(image_paths)\n    while True:\n        indices = np.random.permutation(num_samples) if augment else np.arange(num_samples)\n        for start in range(0, num_samples, batch_size):\n            end = min(start + batch_size, num_samples)\n            batch_indices = indices[start:end]\n            batch_images = []\n            batch_labels = []\n            for idx in batch_indices:\n                img = load_and_preprocess_image(image_paths[idx])\n                if augment:\n                    img = augment_image(img)\n                batch_images.append(img)\n                batch_labels.append(labels[idx])\n            yield np.array(batch_images, dtype=np.float32), np.array(batch_labels, dtype=np.int32)\n\nprint(\"✅ Preprocessing and augmentation functions ready (no flip)\")\n\n# ============================================================================\n# 5. RESNET50 MODEL (with L2 regularization)\n# ============================================================================\ndef create_resnet_model(input_shape=(IMG_SIZE, IMG_SIZE, 3), num_classes=NUM_CLASSES):\n    base_model = ResNet50(weights='imagenet', include_top=False, input_shape=input_shape)\n    base_model.trainable = False\n    \n    inputs = keras.Input(shape=input_shape)\n    x = tf.keras.applications.resnet50.preprocess_input(inputs * 255.0)\n    x = base_model(x, training=False)\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dense(512, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)\n    x = layers.Dropout(0.5)(x)\n    x = layers.Dense(256, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)\n    x = layers.Dropout(0.3)(x)\n    outputs = layers.Dense(num_classes, activation='softmax')(x)\n    \n    model = keras.Model(inputs, outputs)\n    return model, base_model\n\nmodel, base_model = create_resnet_model()\nmodel.summary()\n\n# ============================================================================\n# 6. PHASE 1 (same as MobileNetV2)\n# ============================================================================\nsteps_per_epoch = max(1, len(X_train_paths) // BATCH_SIZE)\nvalidation_steps = max(1, len(X_val_paths) // BATCH_SIZE)\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(learning_rate=LEARNING_RATE_PHASE1),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy', SparseTopKCategoricalAccuracy(k=3, name='top_3_accuracy')]\n)\n\ncallbacks_phase1 = [\n    ModelCheckpoint(os.path.join(OUTPUT_DIR, 'best_resnet50_phase1.keras'), \n                    monitor='val_accuracy', save_best_only=True, verbose=1),\n    ModelCheckpoint(os.path.join(OUTPUT_DIR, 'best_resnet50_phase1.h5'), \n                    monitor='val_accuracy', save_best_only=True, verbose=0),\n    EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1),\n    ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1)\n]\n\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"🔹 PHASE 1: FEATURE EXTRACTION ({EPOCHS_PHASE1} epochs, base frozen)\")\nprint(\"=\" * 70)\nhistory_phase1 = model.fit(\n    data_generator(X_train_paths, y_train, BATCH_SIZE, augment=True),\n    steps_per_epoch=steps_per_epoch, epochs=EPOCHS_PHASE1,\n    validation_data=data_generator(X_val_paths, y_val, BATCH_SIZE, augment=False),\n    validation_steps=validation_steps, callbacks=callbacks_phase1, verbose=1\n)\n\n# ============================================================================\n# 7. PHASE 2 (same as MobileNetV2)\n# ============================================================================\nbase_model.trainable = True\nfor layer in base_model.layers[:100]:\n    layer.trainable = False\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(learning_rate=LEARNING_RATE_PHASE2),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy', SparseTopKCategoricalAccuracy(k=3, name='top_3_accuracy')]\n)\n\ncallbacks_phase2 = [\n    ModelCheckpoint(os.path.join(OUTPUT_DIR, 'best_resnet50_phase2.keras'), \n                    monitor='val_accuracy', save_best_only=True, verbose=1),\n    ModelCheckpoint(os.path.join(OUTPUT_DIR, 'best_resnet50_phase2.h5'), \n                    monitor='val_accuracy', save_best_only=True, verbose=0),\n    EarlyStopping(monitor='val_loss', patience=4, restore_best_weights=True, verbose=1),\n    ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, min_lr=1e-6, verbose=1)\n]\n\nprint(\"\\n\" + \"=\" * 70)\nprint(f\"🔧 PHASE 2: FINE-TUNING ({EPOCHS_PHASE2} epochs, unfreezing last layers)\")\nprint(\"=\" * 70)\nhistory_phase2 = model.fit(\n    data_generator(X_train_paths, y_train, BATCH_SIZE, augment=True),\n    steps_per_epoch=steps_per_epoch, epochs=EPOCHS_PHASE2,\n    validation_data=data_generator(X_val_paths, y_val, BATCH_SIZE, augment=False),\n    validation_steps=validation_steps, callbacks=callbacks_phase2, verbose=1\n)\n\n# ============================================================================\n# 8. SAVES (with .weights.h5 extension to avoid error)\n# ============================================================================\nmodel.save(os.path.join(OUTPUT_DIR, 'resnet50_final.keras'))\nmodel.save(os.path.join(OUTPUT_DIR, 'resnet50_final.h5'))\nmodel.save_weights(os.path.join(OUTPUT_DIR, 'resnet50_weights.weights.h5'))   # ← extension corrected\nprint(\"✅ Model and weights saved\")\n\n# ============================================================================\n# 9. HISTORY\n# ============================================================================\nhistory_data = {\n    'phase1': history_phase1.history,\n    'phase2': history_phase2.history\n}\nwith open(os.path.join(OUTPUT_DIR, 'training_history_resnet50.json'), 'w') as f:\n    json.dump(history_data, f)\nprint(\"✅ History saved\")\n\n# ============================================================================\n# 10. GRAPHS (optional – you can use those from your existing code)\n# ============================================================================\n# ... (resume your existing graphs or add the same ones as for MobileNetV2)\n# To avoid redundancy, I am not copying them here; they are unchanged.\nprint(\"\\n✅ Training finished. You can now generate the graphs and evaluation.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T11:47:09.751632Z","iopub.execute_input":"2026-07-11T11:47:09.752060Z","iopub.status.idle":"2026-07-11T12:27:47.669123Z","shell.execute_reply.started":"2026-07-11T11:47:09.752030Z","shell.execute_reply":"2026-07-11T12:27:47.668399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport json\nimport os\n\nprint(\"=\" * 80)\nprint(\"📊 RESNET50 GRAPHS GENERATION (MobileNetV2 Style)\")\nprint(\"=\" * 80)\n\nOUTPUT_DIR = '/kaggle/working'\nEPOCHS_PHASE1 = 15\nEPOCHS_PHASE2 = 8\n\n# ------------------------------------------------------------\n# 1. Loading history from JSON file\n# ------------------------------------------------------------\njson_path = os.path.join(OUTPUT_DIR, 'training_history_resnet50.json')\nif not os.path.exists(json_path):\n    # Try another possible name (your ResNet50 code might have used 'training_history.json')\n    json_path = os.path.join(OUTPUT_DIR, 'training_history.json')\n\nif os.path.exists(json_path):\n    with open(json_path, 'r') as f:\n        data = json.load(f)\n    # The file normally contains the keys 'phase1' and 'phase2'\n    if 'phase1' in data and 'phase2' in data:\n        history_phase1 = data['phase1']\n        history_phase2 = data['phase2']\n        print(f\"✅ Histories loaded from {json_path}\")\n    else:\n        # If the file is in combined format (like for MobileNetV2)\n        # We assume it contains 'accuracy', 'val_accuracy', etc. over 23 epochs\n        # We then need to separate the phases manually\n        combined = data\n        # Check if the length of the lists is 23 (15+8)\n        if len(combined.get('accuracy', [])) == EPOCHS_PHASE1 + EPOCHS_PHASE2:\n            history_phase1 = {k: v[:EPOCHS_PHASE1] for k, v in combined.items() if isinstance(v, list)}\n            history_phase2 = {k: v[EPOCHS_PHASE1:] for k, v in combined.items() if isinstance(v, list) and len(v) > EPOCHS_PHASE1}\n            print(\"✅ Histories separated from the combined file\")\n        else:\n            raise ValueError(\"Unrecognized file format\")\nelse:\n    print(f\"❌ File {json_path} not found.\")\n    print(\"   Please ensure that the ResNet50 training was successfully executed and saved the history.\")\n    exit()\n\n# ------------------------------------------------------------\n# 2. Combining histories (same function as MobileNetV2)\n# ------------------------------------------------------------\ndef combine_histories(hist1, hist2):\n    combined = {}\n    for key in hist1.keys():\n        if key in hist2:\n            combined[key] = hist1[key] + hist2[key]\n        else:\n            combined[key] = hist1[key]\n    combined['learning_rate'] = hist1.get('learning_rate', []) + hist2.get('learning_rate', [])\n    return combined\n\ncombined = combine_histories(history_phase1, history_phase2)\n\n# ------------------------------------------------------------\n# 3. Displaying final metrics\n# ------------------------------------------------------------\nbest_epoch = np.argmax(combined['val_accuracy'])\nprint(f\"\\n🏆 Best validation accuracy : {combined['val_accuracy'][best_epoch]:.4f} \"\n      f\"(epoch {best_epoch+1})\")\nprint(f\"   - Validation Loss : {combined['val_loss'][best_epoch]:.4f}\")\nif 'val_top_3_accuracy' in combined:\n    print(f\"   - Validation Top-3: {combined['val_top_3_accuracy'][best_epoch]:.4f}\")\n\n# ------------------------------------------------------------\n# 4. Graphs (strictly identical to those of MobileNetV2)\n# ------------------------------------------------------------\nprint(\"\\n🎨 Generating ResNet50 graphs...\")\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\nfig.suptitle(f'ResNet50 - Training Metrics (Phase1={EPOCHS_PHASE1}, Phase2={EPOCHS_PHASE2})',\n             fontsize=16, fontweight='bold')\n\n# Accuracy\naxes[0, 0].plot(combined['accuracy'], label='Train', linewidth=2, color='blue')\naxes[0, 0].plot(combined['val_accuracy'], label='Validation', linewidth=2, color='red')\naxes[0, 0].axvline(x=EPOCHS_PHASE1 - 0.5, color='gray', linestyle='--', alpha=0.7,\n                   label='Phase 1 → Phase 2')\naxes[0, 0].set_title('Accuracy', fontsize=14, fontweight='bold')\naxes[0, 0].set_xlabel('Epoch')\naxes[0, 0].set_ylabel('Accuracy')\naxes[0, 0].legend()\naxes[0, 0].grid(True, alpha=0.3)\n\n# Loss\naxes[0, 1].plot(combined['loss'], label='Train Loss', linewidth=2, color='blue')\naxes[0, 1].plot(combined['val_loss'], label='Validation Loss', linewidth=2, color='red')\naxes[0, 1].axvline(x=EPOCHS_PHASE1 - 0.5, color='gray', linestyle='--', alpha=0.7)\naxes[0, 1].set_title('Loss', fontsize=14, fontweight='bold')\naxes[0, 1].set_xlabel('Epoch')\naxes[0, 1].set_ylabel('Loss')\naxes[0, 1].legend()\naxes[0, 1].grid(True, alpha=0.3)\n\n# Learning Rate\naxes[1, 0].plot(combined['learning_rate'], linewidth=2, color='green')\naxes[1, 0].set_title('Learning Rate', fontsize=14, fontweight='bold')\naxes[1, 0].set_xlabel('Epoch')\naxes[1, 0].set_ylabel('Learning Rate')\naxes[1, 0].set_yscale('log')\naxes[1, 0].grid(True, alpha=0.3)\n\n# Summary table\naxes[1, 1].axis('off')\nmetrics_summary = [\n    [\"Metric\", \"Train\", \"Validation\", \"Best\"],\n    [\"Accuracy\", f\"{combined['accuracy'][-1]:.3f}\",\n     f\"{combined['val_accuracy'][-1]:.3f}\",\n     f\"{combined['val_accuracy'][best_epoch]:.3f}\"],\n    [\"Loss\", f\"{combined['loss'][-1]:.3f}\",\n     f\"{combined['val_loss'][-1]:.3f}\",\n     f\"{combined['val_loss'][best_epoch]:.3f}\"],\n]\nif 'val_top_3_accuracy' in combined:\n    metrics_summary.append(\n        [\"Top-3\", f\"{combined['top_3_accuracy'][-1]:.3f}\",\n         f\"{combined['val_top_3_accuracy'][-1]:.3f}\",\n         f\"{combined['val_top_3_accuracy'][best_epoch]:.3f}\"]\n    )\n\ntable = axes[1, 1].table(cellText=metrics_summary, loc='center', cellLoc='center')\ntable.auto_set_font_size(False)\ntable.set_fontsize(11)\ntable.scale(1.2, 1.5)\nfor i in range(len(metrics_summary[0])):\n    table[(0, i)].set_facecolor('#4472C4')\n    table[(0, i)].set_text_props(weight='bold', color='white')\naxes[1, 1].set_title('Metrics Summary', fontsize=14, fontweight='bold')\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'resnet50_all_metrics.png'), dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"✅ Graphs saved : resnet50_all_metrics.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:33:52.263645Z","iopub.execute_input":"2026-07-11T12:33:52.264181Z","iopub.status.idle":"2026-07-11T12:33:54.196803Z","shell.execute_reply.started":"2026-07-11T12:33:52.264156Z","shell.execute_reply":"2026-07-11T12:33:54.196048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport os\n\n# ====================================================\n# EXTRACTED DATA FROM REAL RESNET50 LOGS (batch=128, phase1=15, phase2=8)\n# ====================================================\n\n# Phase 1 (epochs 1 to 15)\nacc_p1 = [0.5677, 0.8840, 0.9224, 0.9418, 0.9507,\n          0.9566, 0.9556, 0.9636, 0.9713, 0.9801,\n          0.9819, 0.9843, 0.9835, 0.9828, 0.9847]\nval_acc_p1 = [0.9415, 0.9739, 0.9817, 0.9855, 0.9871,\n              0.9888, 0.9892, 0.9878, 0.9940, 0.9945,\n              0.9936, 0.9947, 0.9947, 0.9952, 0.9945]\nloss_p1 = [1.4776, 0.4868, 0.3733, 0.3199, 0.2885,\n           0.2743, 0.2805, 0.2588, 0.2409, 0.2176,\n           0.2064, 0.2031, 0.1942, 0.1956, 0.1912]\nval_loss_p1 = [0.3714, 0.2276, 0.2023, 0.1866, 0.1843,\n               0.1958, 0.1938, 0.1917, 0.1761, 0.1732,\n               0.1755, 0.1698, 0.1656, 0.1646, 0.1664]\ntop3_p1 = [0.7981, 0.9807, 0.9907, 0.9927, 0.9952,\n           0.9956, 0.9965, 0.9968, 0.9981, 0.9989,\n           0.9978, 0.9988, 0.9995, 0.9992, 0.9993]\nval_top3_p1 = [0.9931, 0.9960, 0.9975, 0.9984, 0.9993,\n               0.9977, 0.9989, 0.9989, 0.9995, 0.9995,\n               0.9998, 0.9998, 0.9989, 0.9995, 0.9998]\n\n# Phase 2 (epochs 16 to 23, i.e., 8 epochs)\nacc_p2 = [0.9081, 0.9689, 0.9811, 0.9865, 0.9916,\n          0.9930, 0.9940, 0.9934]\nval_acc_p2 = [0.9908, 0.9915, 0.9924, 0.9943, 0.9945,\n              0.9954, 0.9952, 0.9952]\nloss_p2 = [0.4459, 0.2413, 0.2030, 0.1863, 0.1699,\n           0.1638, 0.1602, 0.1613]\nval_loss_p2 = [0.1808, 0.1796, 0.1736, 0.1672, 0.1680,\n               0.1645, 0.1648, 0.1654]\ntop3_p2 = [0.9868, 0.9979, 0.9985, 0.9993, 0.9997,\n           0.9998, 0.9999, 0.9996]\nval_top3_p2 = [0.9989, 0.9989, 0.9991, 0.9995, 0.9998,\n               0.9998, 0.9998, 0.9998]\n\n# Learning rates\n# Phase 1: 0.001 for the first 8 epochs (epoch 1 to 8), then 0.0005 for the last 7 (epoch 9 to 15)\nlr_p1 = [0.001]*8 + [0.0005]*7\n# Phase 2: constant 1e-5\nlr_p2 = [1e-5]*8\n\n# ====================================================\n# 1. PHASE 1 GRAPH\n# ====================================================\nOUTPUT_DIR = '/kaggle/working'\nprint(\"📊 Generating Phase 1 graph (ResNet50)...\")\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfig.suptitle('ResNet50 - Phase 1: Feature Extraction (Epochs 1-15)', fontsize=16, fontweight='bold')\n\nepochs_p1 = range(1, len(acc_p1)+1)\n\n# Accuracy\naxes[0].plot(epochs_p1, acc_p1, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[0].plot(epochs_p1, val_acc_p1, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[0].set_title('Accuracy', fontsize=14, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend(loc='lower right')\naxes[0].grid(True, alpha=0.3)\naxes[0].set_ylim([0.5, 1.01])\n\n# Loss\naxes[1].plot(epochs_p1, loss_p1, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[1].plot(epochs_p1, val_loss_p1, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[1].set_title('Loss', fontsize=14, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Loss')\naxes[1].legend(loc='upper right')\naxes[1].grid(True, alpha=0.3)\n\n# Top-3 Accuracy\naxes[2].plot(epochs_p1, top3_p1, 'g-o', label='Train', linewidth=2, markersize=6)\naxes[2].plot(epochs_p1, val_top3_p1, 'm-s', label='Validation', linewidth=2, markersize=6)\naxes[2].set_title('Top-3 Accuracy', fontsize=14, fontweight='bold')\naxes[2].set_xlabel('Epoch')\naxes[2].set_ylabel('Top-3 Accuracy')\naxes[2].legend(loc='lower right')\naxes[2].grid(True, alpha=0.3)\naxes[2].set_ylim([0.75, 1.005])\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'resnet50_phase1_curves.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: resnet50_phase1_curves.png\")\n\n# ====================================================\n# 2. PHASE 2 GRAPH\n# ====================================================\nprint(\"\\n📊 Generating Phase 2 graph (ResNet50)...\")\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfig.suptitle('ResNet50 - Phase 2: Fine-Tuning (Epochs 1-8)', fontsize=16, fontweight='bold')\n\nepochs_p2 = range(1, len(acc_p2)+1)\n\n# Accuracy\naxes[0].plot(epochs_p2, acc_p2, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[0].plot(epochs_p2, val_acc_p2, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[0].set_title('Accuracy', fontsize=14, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend(loc='lower right')\naxes[0].grid(True, alpha=0.3)\naxes[0].set_ylim([0.88, 1.01])\n\n# Loss\naxes[1].plot(epochs_p2, loss_p2, 'b-o', label='Train', linewidth=2, markersize=6)\naxes[1].plot(epochs_p2, val_loss_p2, 'r-s', label='Validation', linewidth=2, markersize=6)\naxes[1].set_title('Loss', fontsize=14, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Loss')\naxes[1].legend(loc='upper right')\naxes[1].grid(True, alpha=0.3)\n\n# Top-3 Accuracy\naxes[2].plot(epochs_p2, top3_p2, 'g-o', label='Train', linewidth=2, markersize=6)\naxes[2].plot(epochs_p2, val_top3_p2, 'm-s', label='Validation', linewidth=2, markersize=6)\naxes[2].set_title('Top-3 Accuracy', fontsize=14, fontweight='bold')\naxes[2].set_xlabel('Epoch')\naxes[2].set_ylabel('Top-3 Accuracy')\naxes[2].legend(loc='lower right')\naxes[2].grid(True, alpha=0.3)\naxes[2].set_ylim([0.98, 1.005])\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'resnet50_phase2_curves.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: resnet50_phase2_curves.png\")\n\n# ====================================================\n# 3. COMPARATIVE GRAPH (BARS)\n# ====================================================\nprint(\"\\n📊 Generating comparison graph...\")\nfig, ax = plt.subplots(figsize=(12, 6))\n\nphases = ['Phase 1\\nTrain', 'Phase 1\\nVal', 'Phase 2\\nTrain', 'Phase 2\\nVal']\nbest_acc = [max(acc_p1), max(val_acc_p1), max(acc_p2), max(val_acc_p2)]\nbest_top3 = [max(top3_p1), max(val_top3_p1), max(top3_p2), max(val_top3_p2)]\n\nx_pos = np.arange(len(phases))\nwidth = 0.35\n\nbars1 = ax.bar(x_pos - width/2, best_acc, width, label='Accuracy', color='#2E86AB', edgecolor='black')\nbars2 = ax.bar(x_pos + width/2, best_top3, width, label='Top-3 Accuracy', color='#A23B72', edgecolor='black')\n\nax.set_ylabel('Score', fontsize=12)\nax.set_title('Best ResNet50 model performances', fontsize=14, fontweight='bold')\nax.set_xticks(x_pos)\nax.set_xticklabels(phases, fontsize=12)\nax.legend(loc='lower right', fontsize=11)\nax.grid(True, alpha=0.3, axis='y')\nax.set_ylim([0.7, 1.02])\n\nfor bar in bars1:\n    height = bar.get_height()\n    ax.annotate(f'{height:.4f}\\n({height*100:.2f}%)',\n                xy=(bar.get_x() + bar.get_width()/2, height),\n                xytext=(0, 5), textcoords=\"offset points\",\n                ha='center', va='bottom', fontsize=10, fontweight='bold')\nfor bar in bars2:\n    height = bar.get_height()\n    ax.annotate(f'{height:.4f}\\n({height*100:.2f}%)',\n                xy=(bar.get_x() + bar.get_width()/2, height),\n                xytext=(0, 5), textcoords=\"offset points\",\n                ha='center', va='bottom', fontsize=10, fontweight='bold')\n\nax.axhline(y=0.95, color='green', linestyle='--', alpha=0.5, label='95% Threshold')\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'resnet50_performance_bars.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: resnet50_performance_bars.png\")\n\n# ====================================================\n# 4. COMPLETE EVOLUTION (ACCURACY + TOP-3)\n# ====================================================\nprint(\"\\n📊 Generating complete evolution graph...\")\ncombined_acc = acc_p1 + acc_p2\ncombined_val_acc = val_acc_p1 + val_acc_p2\ncombined_top3 = top3_p1 + top3_p2\ncombined_val_top3 = val_top3_p1 + val_top3_p2\ntotal_epochs = len(combined_acc)\nepochs = range(1, total_epochs + 1)\n\nfig, axes = plt.subplots(1, 2, figsize=(16, 6))\nfig.suptitle('ResNet50 - Complete Evolution (Phase 1 + Phase 2)', fontsize=16, fontweight='bold')\n\n# Accuracy\naxes[0].plot(epochs, combined_acc, 'b-o', label='Train Accuracy', linewidth=2, markersize=5)\naxes[0].plot(epochs, combined_val_acc, 'r-s', label='Validation Accuracy', linewidth=2, markersize=5)\naxes[0].axvline(x=len(acc_p1), color='gray', linestyle='--', alpha=0.7, label='Start of Phase 2')\naxes[0].set_title('Accuracy - Complete Evolution', fontsize=14, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend(fontsize=11)\naxes[0].grid(True, alpha=0.3)\naxes[0].set_ylim([0.5, 1.01])\n\n# Top-3 Accuracy\naxes[1].plot(epochs, combined_top3, 'g-o', label='Train Top-3', linewidth=2, markersize=5)\naxes[1].plot(epochs, combined_val_top3, 'm-s', label='Validation Top-3', linewidth=2, markersize=5)\naxes[1].axvline(x=len(acc_p1), color='gray', linestyle='--', alpha=0.7, label='Start of Phase 2')\naxes[1].set_title('Top-3 Accuracy - Complete Evolution', fontsize=14, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Top-3 Accuracy')\naxes[1].legend(fontsize=11)\naxes[1].grid(True, alpha=0.3)\naxes[1].set_ylim([0.75, 1.005])\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'resnet50_complete_evolution.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"✅ Saved: resnet50_complete_evolution.png\")\n\n# ====================================================\n# FINAL PERFORMANCE SUMMARY\n# ====================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"📊 PERFORMANCE SUMMARY - RESNET50\")\nprint(\"=\"*60)\nprint(f\"\\n🔹 Phase 1 (Feature Extraction):\")\nprint(f\"   • Best Accuracy - Train:  {max(acc_p1):.4f} ({max(acc_p1)*100:.2f}%)\")\nprint(f\"   • Best Accuracy - Val:    {max(val_acc_p1):.4f} ({max(val_acc_p1)*100:.2f}%)\")\nprint(f\"   • Best Top-3 - Val:       {max(val_top3_p1):.4f} ({max(val_top3_p1)*100:.2f}%)\")\nprint(f\"\\n🔹 Phase 2 (Fine-Tuning):\")\nprint(f\"   • Best Accuracy - Train:  {max(acc_p2):.4f} ({max(acc_p2)*100:.2f}%)\")\nprint(f\"   • Best Accuracy - Val:    {max(val_acc_p2):.4f} ({max(val_acc_p2)*100:.2f}%)\")\nprint(f\"   • Best Top-3 - Val:       {max(val_top3_p2):.4f} ({max(val_top3_p2)*100:.2f}%)\")\nprint(\"\\n\" + \"=\"*60)\nprint(\"✅ ALL GRAPHS HAVE BEEN GENERATED FOR RESNET50!\")\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:36:20.426474Z","iopub.execute_input":"2026-07-11T12:36:20.426908Z","iopub.status.idle":"2026-07-11T12:36:24.138939Z","shell.execute_reply.started":"2026-07-11T12:36:20.426881Z","shell.execute_reply":"2026-07-11T12:36:24.138189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 📊 COMPLETE EVALUATION ON THE VALIDATION SET (adapted for ResNet50)\n# =============================================================================\nfrom sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc, roc_auc_score\nfrom sklearn.preprocessing import label_binarize\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"📊 DETAILED EVALUATION OF THE ResNet50 MODEL\")\nprint(\"=\"*70)\n\n# 1. Retrieve all predictions and true classes\nval_steps = (len(X_val_paths) + BATCH_SIZE - 1) // BATCH_SIZE\ny_true = []\ny_pred = []\ny_pred_proba = []\n\nprint(\"🔄 Predictions on the validation set...\")\nval_gen = data_generator(X_val_paths, y_val, BATCH_SIZE, augment=False)\nfor _ in range(val_steps):\n    X_batch, y_batch = next(val_gen)\n    preds = model.predict(X_batch, verbose=0)\n    y_pred_proba.extend(preds)\n    y_pred.extend(np.argmax(preds, axis=1))\n    y_true.extend(y_batch)   # y_batch is already integer (sparse)\n\ny_true = np.array(y_true)\ny_pred = np.array(y_pred)\ny_pred_proba = np.array(y_pred_proba)\n\n# 2. Confusion matrix\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(12, 10))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n            xticklabels=list(CLASS_NAMES.values()), \n            yticklabels=list(CLASS_NAMES.values()))\nplt.title('Confusion Matrix - ResNet50 Validation', fontsize=16, fontweight='bold')\nplt.xlabel('Predictions')\nplt.ylabel('True classes')\nplt.xticks(rotation=45, ha='right')\nplt.yticks(rotation=0)\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'confusion_matrix_resnet50.png'), dpi=300)\nplt.show()\n\n# 3. Classification report\nprint(\"\\n📋 Classification Report:\")\nprint(classification_report(y_true, y_pred, \n                            target_names=list(CLASS_NAMES.values()), \n                            digits=4))\n\n# 4. Top-3 accuracy\ntop3_acc = 0\nfor true, pred_proba in zip(y_true, y_pred_proba):\n    top3 = np.argsort(pred_proba)[-3:]\n    if true in top3:\n        top3_acc += 1\ntop3_acc /= len(y_true)\nprint(f\"\\n🎯 Top-3 Accuracy: {top3_acc:.4f} ({top3_acc*100:.2f}%)\")\n\n# 5. AUC (One-vs-Rest) and ROC curves\nn_classes = NUM_CLASSES\ny_true_bin = label_binarize(y_true, classes=range(n_classes))\n\nfpr = dict()\ntpr = dict()\nroc_auc = dict()\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(y_true_bin[:, i], y_pred_proba[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\nmacro_auc = roc_auc_score(y_true_bin, y_pred_proba, average='macro')\nprint(f\"\\n📈 Average AUC (macro): {macro_auc:.4f}\")\n\n# ROC Curves\nplt.figure(figsize=(10, 8))\ncolors = plt.cm.get_cmap('tab20', n_classes)\nfor i in range(n_classes):\n    plt.plot(fpr[i], tpr[i], color=colors(i), lw=2,\n             label=f'{list(CLASS_NAMES.values())[i]} (AUC = {roc_auc[i]:.2f})')\nplt.plot([0, 1], [0, 1], 'k--', lw=2)\nplt.xlim([0.0, 1.0])\nplt.ylim([0.0, 1.05])\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC Curves (One-vs-Rest) - ResNet50')\nplt.legend(loc='lower right', fontsize=8)\nplt.grid(alpha=0.3)\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'roc_curves_resnet50.png'), dpi=300)\nplt.show()\n\n# 6. Errors per class\nerrors_per_class = {}\nfor cls in range(n_classes):\n    mask = (y_true == cls)\n    total = np.sum(mask)\n    errors = np.sum((y_true == cls) & (y_pred != cls))\n    errors_per_class[list(CLASS_NAMES.values())[cls]] = (errors, total, errors/total if total>0 else 0)\n\nprint(\"\\n❌ Error rate per class:\")\nfor class_name, (err, total, rate) in errors_per_class.items():\n    print(f\"   {class_name:30s} : {err:4d} / {total:4d}  ({rate:.2%})\")\n\n# 7. Final synthesis\nprint(\"\\n\" + \"=\"*70)\nprint(\"📌 FINAL PERFORMANCE SUMMARY - ResNet50\")\nprint(\"=\"*70)\nprint(f\"✅ Global Accuracy (validation) : {np.mean(y_pred == y_true):.4f} ({np.mean(y_pred == y_true)*100:.2f}%)\")\nprint(f\"✅ Top-3 Accuracy               : {top3_acc:.4f} ({top3_acc*100:.2f}%)\")\nprint(f\"✅ Macro AUC                    : {macro_auc:.4f}\")\nprint(\"\\n📁 Saved files:\")\nprint(f\"   - {os.path.join(OUTPUT_DIR, 'confusion_matrix_resnet50.png')}\")\nprint(f\"   - {os.path.join(OUTPUT_DIR, 'roc_curves_resnet50.png')}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:38:46.627258Z","iopub.execute_input":"2026-07-11T12:38:46.627924Z","iopub.status.idle":"2026-07-11T12:39:37.747800Z","shell.execute_reply.started":"2026-07-11T12:38:46.627894Z","shell.execute_reply":"2026-07-11T12:39:37.747107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Configuration\nTEST_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/test'\nIMG_SIZE = 224\n\nCLASS_NAMES = {\n    0: 'Normal driving', 1: 'Texting - right', 2: 'Talking on phone - right',\n    3: 'Texting - left', 4: 'Talking on phone - left', 5: 'Operating radio',\n    6: 'Drinking', 7: 'Reaching behind', 8: 'Hair and makeup', 9: 'Talking to passenger'\n}\n\n# Loads and normalizes the image between 0 and 1 (exactly like for training)\ndef load_and_normalize(path):\n    img = cv2.imread(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = img.astype(np.float32) / 255.0   # -> [0,1]\n    return img\n\n# Prediction (the model handles preprocess_input)\ndef predict_safe(img_path):\n    img = load_and_normalize(img_path)\n    img_batch = np.expand_dims(img, axis=0)\n    preds = model.predict(img_batch, verbose=0)[0]\n    class_id = np.argmax(preds)\n    conf = preds[class_id]\n    return class_id, conf\n\n# Random selection of 10 test images\nall_test = [f for f in os.listdir(TEST_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\nrandom.seed(42)\nselected = random.sample(all_test, 10)\n\nprint(\"🎲 Predictions on 10 TEST images (with normalized 0-1 input):\\n\")\nfig, axes = plt.subplots(2, 5, figsize=(15, 6))\naxes = axes.flatten()\n\nfor i, fname in enumerate(selected):\n    img_path = os.path.join(TEST_DIR, fname)\n    class_id, conf = predict_safe(img_path)\n    class_name = CLASS_NAMES[class_id]\n    # Display the original image\n    img_disp = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n    axes[i].imshow(img_disp)\n    axes[i].set_title(f\"{class_name}\\n({conf:.1%})\", fontsize=8)\n    axes[i].axis('off')\n    axes[i].title.set_color('green' if conf > 0.9 else 'orange' if conf > 0.7 else 'red')\n    print(f\"{i+1:2d}. {fname:35s} → {class_name:30s} ({conf:.1%})\")\n\nfor j in range(len(selected), len(axes)):\n    axes[j].axis('off')\n\nplt.suptitle('ResNet50 - Correct predictions (normal 0-1 input)', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('/kaggle/working/resnet50_test_final.png', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:43:23.120105Z","iopub.execute_input":"2026-07-11T12:43:23.120508Z","iopub.status.idle":"2026-07-11T12:43:28.743860Z","shell.execute_reply.started":"2026-07-11T12:43:23.120481Z","shell.execute_reply":"2026-07-11T12:43:28.742884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.models import load_model\nfrom tensorflow.keras.applications.resnet50 import preprocess_input as resnet_preprocess\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input as mobilenet_preprocess\n\n# ------------------------------------------------------------\n# Configuration\n# ------------------------------------------------------------\nTEST_DIR = '/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/test'\nIMG_SIZE = 224\n\nCLASS_NAMES = {\n    0: 'Normal driving', 1: 'Texting - right', 2: 'Talking on phone - right',\n    3: 'Texting - left', 4: 'Talking on phone - left', 5: 'Operating radio',\n    6: 'Drinking', 7: 'Reaching behind', 8: 'Hair and makeup', 9: 'Talking to passenger'\n}\n\n# ------------------------------------------------------------\n# Load both models\n# ------------------------------------------------------------\nprint(\"📦 Loading models...\")\nresnet_model = load_model('/kaggle/working/best_resnet50_phase2.keras')  # or resnet50_final.keras\nmobilenet_model = load_model('/kaggle/working/final_mobilenetv2_complete.h5')\nprint(\"✅ Models loaded.\")\n\n# ------------------------------------------------------------\n# Prediction functions specific to each model\n# ------------------------------------------------------------\ndef predict_resnet(img_path):\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = img.astype(np.float32) / 255.0\n    # ResNet50: the model contains preprocess_input(img*255). So we send normalized img.\n    img_batch = np.expand_dims(img, axis=0)\n    preds = resnet_model.predict(img_batch, verbose=0)[0]\n    class_id = np.argmax(preds)\n    conf = preds[class_id]\n    return class_id, conf\n\ndef predict_mobilenet(img_path):\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = img.astype(np.float32)\n    img = mobilenet_preprocess(img)\n    img_batch = np.expand_dims(img, axis=0)\n    preds = mobilenet_model.predict(img_batch, verbose=0)[0]\n    class_id = np.argmax(preds)\n    conf = preds[class_id]\n    return class_id, conf\n\n# ------------------------------------------------------------\n# Selection of the same 10 random images (fixed seed)\n# ------------------------------------------------------------\nall_test = [f for f in os.listdir(TEST_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\nrandom.seed(42)\nselected = random.sample(all_test, 10)\n\nprint(f\"\\n🎲 Comparison on the same 10 test images:\\n\")\nfig, axes = plt.subplots(2, 5, figsize=(20, 8))\naxes = axes.flatten()\n\nfor i, fname in enumerate(selected):\n    img_path = os.path.join(TEST_DIR, fname)\n    \n    # Predictions\n    r_id, r_conf = predict_resnet(img_path)\n    m_id, m_conf = predict_mobilenet(img_path)\n    \n    r_name = CLASS_NAMES[r_id]\n    m_name = CLASS_NAMES[m_id]\n    \n    # Display the image\n    img_disp = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n    axes[i].imshow(img_disp)\n    axes[i].set_title(f\"ResNet50: {r_name}\\n({r_conf:.1%})\\nMobileNetV2: {m_name}\\n({m_conf:.1%})\",\n                      fontsize=8)\n    axes[i].axis('off')\n    \n    print(f\"{i+1:2d}. {fname:30s} | ResNet: {r_name:25s} ({r_conf:.1%}) | MobileNet: {m_name:25s} ({m_conf:.1%})\")\n\nfor j in range(len(selected), len(axes)):\n    axes[j].axis('off')\n\nplt.suptitle('Comparison ResNet50 vs MobileNetV2 on 10 test images', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('/kaggle/working/comparison_resnet_mobilenet.png', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:46:05.101649Z","iopub.execute_input":"2026-07-11T12:46:05.102047Z","iopub.status.idle":"2026-07-11T12:46:20.349292Z","shell.execute_reply.started":"2026-07-11T12:46:05.102021Z","shell.execute_reply":"2026-07-11T12:46:20.346628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\n\n# ------------------------------------------------------------\n# MobileNetV2 Data (23 epochs: 15 phase1 + 8 phase2)\n# ------------------------------------------------------------\nacc_mobilenet = [\n    0.3524, 0.6362, 0.7060, 0.7328, 0.7580, 0.7668, 0.7808, 0.7992, 0.8033, 0.8170,\n    0.8223, 0.8250, 0.8317, 0.8270, 0.8297,  # phase1\n    0.4893, 0.6537, 0.7407, 0.7780, 0.8209, 0.8390, 0.8618, 0.8808   # phase2\n]\nval_acc_mobilenet = [\n    0.7603, 0.7878, 0.8148, 0.8456, 0.8233, 0.8297, 0.8119, 0.8503, 0.8634, 0.8612,\n    0.8817, 0.8824, 0.8523, 0.8786, 0.8976,  # phase1\n    0.8670, 0.8688, 0.8875, 0.9087, 0.9221, 0.9322, 0.9393, 0.9442   # phase2\n]\n\n# ------------------------------------------------------------\n# ResNet50 Data (23 epochs: 15 phase1 + 8 phase2)\n# ------------------------------------------------------------\nacc_resnet = [\n    0.5677, 0.8840, 0.9224, 0.9418, 0.9507, 0.9566, 0.9556, 0.9636, 0.9713, 0.9801,\n    0.9819, 0.9843, 0.9835, 0.9828, 0.9847,  # phase1\n    0.9081, 0.9689, 0.9811, 0.9865, 0.9916, 0.9930, 0.9940, 0.9934   # phase2\n]\nval_acc_resnet = [\n    0.9415, 0.9739, 0.9817, 0.9855, 0.9871, 0.9888, 0.9892, 0.9878, 0.9940, 0.9945,\n    0.9936, 0.9947, 0.9947, 0.9952, 0.9945,  # phase1\n    0.9908, 0.9915, 0.9924, 0.9943, 0.9945, 0.9954, 0.9952, 0.9952   # phase2\n]\n\n# ------------------------------------------------------------\n# Comparative plot\n# ------------------------------------------------------------\nplt.figure(figsize=(14, 8))\n\nepochs = range(1, len(acc_resnet) + 1)  # 23 epochs\n\n# MobileNetV2\nplt.plot(epochs, acc_mobilenet, 'b-', linewidth=2, label='MobileNetV2 - Train Accuracy')\nplt.plot(epochs, val_acc_mobilenet, 'r-', linewidth=2, label='MobileNetV2 - Validation Accuracy')\n\n# ResNet50\nplt.plot(epochs, acc_resnet, 'orange', linewidth=2, label='ResNet50 - Train Accuracy')\nplt.plot(epochs, val_acc_resnet, 'purple', linewidth=2, label='ResNet50 - Validation Accuracy')\n\n# Phase separation line\nplt.axvline(x=15.5, color='gray', linestyle='--', alpha=0.7, label='End of Phase 1 / Start of Phase 2')\n\nplt.xlabel('Epoch', fontsize=12)\nplt.ylabel('Accuracy', fontsize=12)\nplt.title('Accuracy curves comparison: MobileNetV2 vs ResNet50', fontsize=14, fontweight='bold')\nplt.legend(loc='lower right')\nplt.grid(True, alpha=0.3)\nplt.ylim(0.3, 1.02)\nplt.xlim(0, 24)\n\nplt.tight_layout()\nplt.savefig('/kaggle/working/comparison_accuracy_curves.png', dpi=150, bbox_inches='tight')\nplt.show()\n\nprint(\"✅ Graph saved : comparison_accuracy_curves.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:49:55.956164Z","iopub.execute_input":"2026-07-11T12:49:55.956573Z","iopub.status.idle":"2026-07-11T12:49:56.510396Z","shell.execute_reply.started":"2026-07-11T12:49:55.956537Z","shell.execute_reply":"2026-07-11T12:49:56.509668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\ndef draw_architecture(ax, title, blocks, pos_x=0.5, start_y=0.88, step_y=0.15, box_width=0.85, box_height=0.1):\n    \"\"\"Generates a clear and professional vertical architecture diagram.\"\"\"\n    ax.set_xlim(0, 1)\n    ax.set_ylim(0, 1)\n    ax.axis('off')\n    ax.set_facecolor('white')\n    ax.set_title(title, fontsize=16, fontweight='bold', pad=15, y=1.02)\n\n    y = start_y\n    for i, (label, color) in enumerate(blocks):\n        # Rounded box\n        rect = patches.FancyBboxPatch(\n            (pos_x - box_width/2, y - box_height/2),\n            box_width, box_height,\n            boxstyle=\"round,pad=0.04\",\n            edgecolor='black', facecolor=color, linewidth=2\n        )\n        ax.add_patch(rect)\n        \n        # Clean multiline text\n        ax.text(pos_x, y, label, ha='center', va='center', \n                fontsize=11, fontweight='semibold', linespacing=1.3, color='#111111')\n        \n        # Arrow to the next box\n        if i < len(blocks) - 1:\n            next_y = y - step_y\n            start_pt = (pos_x, y - box_height/2)\n            end_pt   = (pos_x, next_y + box_height/2)\n            ax.annotate('', xy=end_pt, xytext=start_pt,\n                        arrowprops=dict(arrowstyle='-|>', lw=2.5, color='#2c3e50', \n                                        shrinkA=5, shrinkB=5))\n        y -= step_y\n\n# Common blocks for both architectures\ncommon_blocks = [\n    (\"Input Image\\n(224 × 224 × 3)\", \"#D9EAD3\"),\n    (\"Backbone CNN\\n(ImageNet, frozen Phase 1)\", \"#C9DAF8\"),\n    (\"Global Average Pooling\", \"#FCE5CD\"),\n    (\"Dense 512 + BatchNorm + Dropout(0.5)\", \"#FFF2CC\"),\n    (\"Dense 256 + Dropout(0.3)\", \"#FFF2CC\"),\n    (\"Output Softmax\\n(10 Classes)\", \"#EAD1DC\")\n]\n\n# 🟦 1. MobileNetV2\nfig1, ax1 = plt.subplots(figsize=(5, 8.5))\ndraw_architecture(ax1, \"MobileNetV2 Architecture\", common_blocks)\nplt.tight_layout()\nfig1.savefig('mobilenetv2_architecture.png', dpi=300, bbox_inches='tight', facecolor='white')\nplt.show()\nprint(\"✅ Saved: mobilenetv2_architecture.png\")\n\n# 🟧 2. ResNet50\nfig2, ax2 = plt.subplots(figsize=(5, 8.5))\ndraw_architecture(ax2, \"ResNet50 Architecture\", common_blocks)\nax2.text(0.5, 0.06, \"⚠️ preprocess_input integrated into the model\\n(inputs × 255.0)\", \n         ha='center', fontsize=10, color='#c0392b', style='italic', fontweight='bold',\n         bbox=dict(boxstyle='round,pad=0.3', facecolor='#fff3cd', edgecolor='#856404', alpha=0.9))\nplt.tight_layout()\nfig2.savefig('resnet50_architecture.png', dpi=300, bbox_inches='tight', facecolor='white')\nplt.show()\nprint(\"✅ Saved: resnet50_architecture.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T12:52:19.210217Z","iopub.execute_input":"2026-07-11T12:52:19.210935Z","iopub.status.idle":"2026-07-11T12:52:19.961981Z","shell.execute_reply.started":"2026-07-11T12:52:19.210907Z","shell.execute_reply":"2026-07-11T12:52:19.961254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}