{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":8232278,"sourceType":"datasetVersion","datasetId":1047930},{"sourceId":11953774,"sourceType":"datasetVersion","datasetId":7515375},{"sourceId":11954082,"sourceType":"datasetVersion","datasetId":7515627},{"sourceId":409562,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":334571,"modelId":355605},{"sourceId":412374,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":336646,"modelId":357639}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n#for dirname, _, filenames in os.walk('/kaggle/input'):\n#    for filename in filenames:\n#        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-26T12:33:37.002641Z","iopub.execute_input":"2025-05-26T12:33:37.003660Z","iopub.status.idle":"2025-05-26T12:33:37.264649Z","shell.execute_reply.started":"2025-05-26T12:33:37.003623Z","shell.execute_reply":"2025-05-26T12:33:37.263895Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.transforms import v2\nimport cv2\nimport PIL\nimport sys\n\n# --- Ensure timm is available (already handled if it's in ../input/pytorchimagemodels) ---\nsys.path.append('../input/pytorchimagemodels')\nimport timm\nprint(f\"timm version: {timm.__version__}\")\n\n# --- DINOv2 GitHub Repository Path for torch.hub ---\n# This path should point to the TOP-LEVEL DIRECTORY of your DINOv2 GitHub repository.\n# torch.hub.load expects to find 'hubconf.py' directly within this directory,\n# or within a subfolder like 'dinov2-main' if that's how your dataset is structured.\n# Let's assume your Kaggle dataset 'dinov2-repo' contains 'dinov2-main' which has hubconf.py.\n# If your 'dinov2-repo' directly contains 'hubconf.py', use KAGGLE_DINOV2_DATASET_ROOT directly.\n\nKAGGLE_DINOV2_DATASET_ROOT = '../input/dinov2-repo' # <<< --- VERIFY THIS IS YOUR DINOv2 DATASET NAME\n\n# Attempt to find the actual DINOv2 repo directory containing hubconf.py\nDINOV2_HUB_REPO_DIR = None\nif os.path.exists(os.path.join(KAGGLE_DINOV2_DATASET_ROOT, 'hubconf.py')):\n    DINOV2_HUB_REPO_DIR = KAGGLE_DINOV2_DATASET_ROOT\nelif os.path.exists(os.path.join(KAGGLE_DINOV2_DATASET_ROOT, 'dinov2-main', 'hubconf.py')):\n    DINOV2_HUB_REPO_DIR = os.path.join(KAGGLE_DINOV2_DATASET_ROOT, 'dinov2-main')\nelif os.path.exists(os.path.join(KAGGLE_DINOV2_DATASET_ROOT, 'dinov2', 'hubconf.py')):\n    DINOV2_HUB_REPO_DIR = os.path.join(KAGGLE_DINOV2_DATASET_ROOT, 'dinov2')\n\nif DINOV2_HUB_REPO_DIR and os.path.exists(DINOV2_HUB_REPO_DIR):\n    print(f\"DINOv2 hubconf.py found in: {DINOV2_HUB_REPO_DIR}\")\nelse:\n    print(f\"ERROR: Could not find DINOv2 hubconf.py in expected locations within {KAGGLE_DINOV2_DATASET_ROOT}.\")\n    print(\"Please check the internal structure of your 'dinov2-repo' dataset on Kaggle.\")\n    print(f\"Contents of {KAGGLE_DINOV2_DATASET_ROOT}: {os.listdir(KAGGLE_DINOV2_DATASET_ROOT)}\")\n    raise RuntimeError(\"DINOv2 hubconf.py not found, cannot proceed.\")\n\n\n# --------------------------------------------------------------------------\n# KAGGLE PATHS & PARAMETERS (Adjust as needed)\n# --------------------------------------------------------------------------\nOUTPUT_DIR = \"./\"  # Output directory for submission.csv\nTEST_PATH = '../input/cassava-leaf-disease-classification/test_images'\n\n# Path to your trained DINOv2 model weights\n# IMPORTANT: This path should point directly to your .pth file.\nMODEL_PATH = '../input/vrdl_dinov2/pytorch/default/1/model_DINOv2_vitb14_seed42.pth'\n\n# Parameters from your DINOv2 training script\nDINOV2_MODEL_NAME = 'dinov2_vitb14' # This model name is used by torch.hub.load\nCLASS_NUM = 5\nSEED = 42\nBATCH_SIZE = 64\nTTA = 4\n\n# --------------------------------------------------------------------------\n# SETUP\n# --------------------------------------------------------------------------\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ndef seed_torch(seed=42):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_torch(SEED)\n\nhub_cache_dir = '../input/dinov2-cache'\nif os.path.exists(hub_cache_dir):\n    print(f\"PyTorch Hub cache directory will be set to: {hub_cache_dir}\")\n    torch.hub.set_dir(hub_cache_dir)\nelse:\n    print(f\"WARNING: Specified hub_cache_dir does not exist: {hub_cache_dir}\")\n    print(\"torch.hub.load will attempt to download from the internet if internet is enabled.\")\n\n# --------------------------------------------------------------------------\n# DINOv2 SPECIFIC NORMALIZATION (from your training script)\n# --------------------------------------------------------------------------\nDINOV2_MEAN = (0.485, 0.456, 0.406)\nDINOV2_STD = (0.229, 0.224, 0.225)\n\n# --------------------------------------------------------------------------\n# DATASET\n# --------------------------------------------------------------------------\nclass TestDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.file_names = df['image_id'].values\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        file_name = self.file_names[idx]\n        file_path = f'{TEST_PATH}/{file_name}'\n        \n        # Load image with OpenCV\n        image_np = cv2.imread(file_path)\n        image_np = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)\n        \n        # --- CRITICAL FIX: Convert NumPy array to PIL Image before applying transforms ---\n        image = PIL.Image.fromarray(image_np)\n        \n        # Original image shape (for debugging - now it's PIL, so different way to check)\n        # print(f\"Processing {file_name}. Original HWC (from PIL): {image.size[::-1] + (len(image.getbands()),)}\") \n\n        if self.transform:\n            image = self.transform(image)\n        \n        # Transformed image shape (for debugging)\n        # print(f\"Processing {file_name}. Transformed CHW: {image.shape}\") # Should be (3, 224, 224)\n        \n        return image\n# --------------------------------------------------------------------------\n# MODEL DEFINITION (Adapted from your DINOv2 training script)\n# --------------------------------------------------------------------------\nclass DINOv2Classifier(nn.Module):\n    def __init__(self, num_classes=CLASS_NUM, dinov2_model_name=DINOV2_MODEL_NAME):\n        super(DINOv2Classifier, self).__init__()\n        self.dinov2_model_name = dinov2_model_name\n\n        try:\n            # Load DINOv2 model using torch.hub.load\n            # pretrained=False here means we are NOT loading the original DINOv2 pre-trained weights from Meta.\n            # Instead, we are just getting the model architecture.\n            # The 'repo_dir' argument tells torch.hub where to find the dinov2 library.\n            # The 'model_name' should match a callable in dinov2's hubconf.py (e.g., dinov2_vitb14).\n            self.dinov2 = torch.hub.load(\n                repo_or_dir=DINOV2_HUB_REPO_DIR, # Point to the local directory containing hubconf.py\n                model=dinov2_model_name,\n                pretrained=False, # We will load our fine-tuned weights later\n                source='local' # Important: tells torch.hub to load from local file system\n            )\n            print(f\"Successfully initialized DINOv2 model using torch.hub.load: {dinov2_model_name}\")\n        except Exception as e:\n            print(f\"Error loading DINOv2 model '{dinov2_model_name}' with torch.hub.load: {e}\")\n            print(f\"Ensure that '{dinov2_model_name}' is a valid entrypoint in {os.path.join(DINOV2_HUB_REPO_DIR, 'hubconf.py')}\")\n            print(f\"Also verify that {DINOV2_HUB_REPO_DIR} points to the correct DINOv2 repository root.\")\n            raise\n\n        # Get embed_dim must be after model is loaded\n        # DINOv2 models expose num_features or embed_dim\n        # Check for both common attribute names\n        if hasattr(self.dinov2, 'num_features'):\n            embed_dim = self.dinov2.num_features\n        elif hasattr(self.dinov2, 'embed_dim'):\n            embed_dim = self.dinov2.embed_dim\n        else:\n            raise AttributeError(\"DINOv2 backbone does not have 'num_features' or 'embed_dim' attribute.\")\n\n        print(f\"DINOv2 backbone embed_dim: {embed_dim}\")\n\n        self.classifier_head = nn.Linear(embed_dim, num_classes)\n\n    def forward(self, x):\n        # DINOv2 models typically output features directly from the Vision Transformer\n        features = self.dinov2(x)\n        return self.classifier_head(features)\n\n\n# --------------------------------------------------------------------------\n# TRANSFORMS (Must match validation/test transforms from training)\n# --------------------------------------------------------------------------\ntest_transforms = v2.Compose([\n    v2.Resize(size=224, interpolation=v2.InterpolationMode.BICUBIC, antialias=True),\n    v2.CenterCrop(size=(224, 224)),\n    v2.ToTensor(),\n    v2.ConvertImageDtype(torch.float),\n    v2.Normalize(mean=DINOV2_MEAN, std=DINOV2_STD)\n])\n\ntta_train_transforms = v2.Compose([\n    v2.RandomResizedCrop(224, scale=(0.9, 1.0), ratio=(0.75, 1.3333), interpolation=v2.InterpolationMode.BICUBIC, antialias=True),\n    v2.RandomRotation(degrees=20),\n    v2.RandomHorizontalFlip(),\n    v2.ToTensor(),\n    v2.ConvertImageDtype(torch.float),\n    v2.Normalize(mean=DINOV2_MEAN, std=DINOV2_STD)\n])\n\n# --------------------------------------------------------------------------\n# INFERENCE FUNCTION\n# --------------------------------------------------------------------------\ndef inference(model, model_state_path, test_loader, device):\n    try:\n        state = torch.load(model_state_path, map_location=device)\n        \n        # 處理 nn.DataParallel 引起的 'module.' 前綴\n        state_dict_keys = list(state.keys())\n        if all(k.startswith('module.') for k in state_dict_keys):\n            state = {k.replace('module.', ''): v for k, v in state.items()}\n            print(\"Removed 'module.' prefix from state_dict keys.\")\n\n        model.load_state_dict(state, strict=True) # 嘗試使用 strict=True\n        print(f\"Successfully loaded model weights from {model_state_path}\")\n    except FileNotFoundError:\n        print(f\"ERROR: Model weights file not found at {model_state_path}\")\n        print(\"Please ensure MODEL_PATH is correctly set and the file exists in your Kaggle input.\")\n        # 列出文件幫助偵錯\n        model_dir = os.path.dirname(model_state_path)\n        if os.path.exists(model_dir):\n            print(f\"Contents of {model_dir}: {os.listdir(model_dir)}\")\n        raise\n    except Exception as e:\n        print(f\"Error loading model state_dict: {e}\")\n        print(\"Please check if the model architecture (e.g., vit_large vs vit_base) matches the loaded weights.\")\n        print(f\"MODEL_PATH: {model_state_path}\")\n        # 打印鍵來偵錯\n        print(\"Keys in loaded state_dict (first 10):\", list(state.keys())[:10])\n        print(\"Keys expected by model (first 10):\", list(model.state_dict().keys())[:10])\n        print(\"Number of keys in loaded state_dict:\", len(state.keys()))\n        print(\"Number of keys expected by model:\", len(model.state_dict().keys()))\n        raise\n\n    model.to(device)\n    model.eval()\n    \n    all_preds = []\n    with torch.no_grad():\n        for images in tqdm(test_loader, desc=\"Inference\"):\n            images = images.to(device)\n            outputs = model(images)\n            probabilities = outputs.softmax(1).cpu().numpy()\n            all_preds.append(probabilities)\n            \n    return np.concatenate(all_preds, axis=0)\n\n# --------------------------------------------------------------------------\n# MAIN EXECUTION\n# --------------------------------------------------------------------------\ndef main():\n    print(f\"Using device: {device}\")\n    print(f\"Loading test data from: {TEST_PATH}\")\n    print(f\"Using DINOv2 model: {DINOV2_MODEL_NAME}\")\n    print(f\"Model path: {MODEL_PATH}\")\n    print(f\"TTA (Test Time Augmentations): {TTA}\")\n\n    try:\n        test_df = pd.read_csv('../input/cassava-leaf-disease-classification/sample_submission.csv')\n    except FileNotFoundError:\n        print(\"ERROR: sample_submission.csv not found. Ensure the competition dataset is correctly loaded.\")\n        if os.path.exists(TEST_PATH):\n            print(f\"Attempting to list images from {TEST_PATH} as a fallback...\")\n            test_image_files = [f for f in os.listdir(TEST_PATH) if os.path.isfile(os.path.join(TEST_PATH, f))]\n            if not test_image_files:\n                print(f\"ERROR: No image files found in {TEST_PATH} either.\")\n                return\n            test_df = pd.DataFrame({'image_id': test_image_files, 'label': 0})\n            print(f\"Created a dummy test_df with {len(test_df)} images.\")\n        else:\n            print(f\"ERROR: TEST_PATH {TEST_PATH} does not exist.\")\n            return\n\n    # Initialize model\n    model = DINOv2Classifier(num_classes=CLASS_NUM, dinov2_model_name=DINOV2_MODEL_NAME)\n\n    # Standard inference pass\n    print(\"Starting standard inference pass...\")\n    test_dataset_standard = TestDataset(test_df, transform=test_transforms)\n    test_loader_standard = DataLoader(test_dataset_standard, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n    predictions = inference(model, MODEL_PATH, test_loader_standard, device)\n\n    # Test Time Augmentation (TTA)\n    if TTA > 1:\n        print(f\"\\nStarting TTA inference ({TTA-1} additional passes)...\")\n        test_dataset_tta = TestDataset(test_df, transform=tta_train_transforms)\n        test_loader_tta = DataLoader(test_dataset_tta, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n        \n        for i in range(TTA - 1):\n            print(f\"TTA Pass {i+1}/{TTA-1}\")\n            current_model_tta = DINOv2Classifier(num_classes=CLASS_NUM, dinov2_model_name=DINOV2_MODEL_NAME)\n            tta_preds = inference(current_model_tta, MODEL_PATH, test_loader_tta, device)\n            predictions += tta_preds\n            \n        predictions /= TTA\n\n    # Generate submission file\n    predicted_labels = predictions.argmax(1)\n    submission_df = pd.DataFrame({'image_id': test_df['image_id'], 'label': predicted_labels})\n    submission_df.to_csv(os.path.join(OUTPUT_DIR, 'submission.csv'), index=False)\n    \n    print(\"\\nSubmission file created: submission.csv\")\n    print(\"Top 5 predictions:\")\n    print(submission_df.head())\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-26T12:33:37.265987Z","iopub.execute_input":"2025-05-26T12:33:37.266333Z","iopub.status.idle":"2025-05-26T12:34:15.823596Z","shell.execute_reply.started":"2025-05-26T12:33:37.266316Z","shell.execute_reply":"2025-05-26T12:34:15.822662Z"}},"outputs":[],"execution_count":null}]}