{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14523602,"sourceType":"datasetVersion","datasetId":9275927},{"sourceId":14523873,"sourceType":"datasetVersion","datasetId":9276105},{"sourceId":14523943,"sourceType":"datasetVersion","datasetId":9276152},{"sourceId":4537,"sourceType":"modelInstanceVersion","modelInstanceId":3329,"modelId":986},{"sourceId":268942,"sourceType":"modelInstanceVersion","modelInstanceId":230141,"modelId":251887}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Multiview ","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport timm\nfrom tqdm import tqdm\n\n# ==========================================\n# 1. 配置 (需与 train_sed.py 一致)\n# ==========================================\nclass Config:\n    # 图像参数\n    img_size = (448, 448)\n    \n    # 归一化参数 (标准 ImageNet)\n    mean = [0.485, 0.456, 0.406]\n    std = [0.229, 0.224, 0.225]\n    \n    # 模型架构\n    model_name = 'vit_base_patch14_reg4_dinov2.lvd142m'\n    \n    # 推理设置\n    batch_size = 16\n    num_workers = 4\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # 🟢 [请修改] 您的 train_sed.py 训练出的模型路径\n    models_dir = '/kaggle/input/train-transformer-sed' \n    \n    # Kaggle 路径\n    test_csv_path = '/kaggle/input/csiro-biomass/test.csv'\n    data_root = '/kaggle/input/csiro-biomass'\n    output_name = 'submission.csv'\n\n# ==========================================\n# 2. 模型定义 (严格复刻 train_sed.py)\n# ==========================================\nclass StereoDifferenceAttention(nn.Module):\n    def __init__(self, dim, num_heads=8, dropout=0.1):\n        super().__init__()\n        # Cross-Attention\n        self.cross_attn = nn.MultiheadAttention(embed_dim=dim, num_heads=num_heads, dropout=dropout, batch_first=True)\n        self.norm = nn.LayerNorm(dim)\n        self.dropout = nn.Dropout(dropout)\n        \n        # 降维融合\n        self.fusion = nn.Sequential(\n            nn.Linear(dim * 3, dim),\n            nn.GELU(),\n            nn.Dropout(dropout)\n        )\n\n    def forward(self, query, key_value):\n        # 1. 对齐\n        aligned_feat, _ = self.cross_attn(query=query, key=key_value, value=key_value)\n        aligned_feat = self.dropout(aligned_feat)\n        \n        # 2. 差分 (Stereo Difference)\n        diff_feat = torch.abs(query - aligned_feat)\n        \n        # 3. 融合\n        combined = torch.cat([query, aligned_feat, diff_feat], dim=-1)\n        \n        out = self.fusion(combined)\n        return out + query \n\nclass MultiLayerSdaBiomassModel(nn.Module):\n    def __init__(self, model_name, pretrained=False):\n        super().__init__()\n        # Backbone (标准 3 通道)\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, img_size=Config.img_size)\n        \n        # 动态探测维度 (复刻训练代码逻辑)\n        with torch.no_grad():\n            dummy = torch.randn(1, 3, Config.img_size[0], Config.img_size[1])\n            dummy_feats = self.extract_features(dummy)\n            self.fusion_dim = sum([f.shape[-1] for f in dummy_feats])\n        \n        # SDA 模块\n        self.sda_left = StereoDifferenceAttention(self.fusion_dim, num_heads=12) \n        self.sda_right = StereoDifferenceAttention(self.fusion_dim, num_heads=12)\n        \n        # Dense Head\n        self.head = nn.Sequential(\n            nn.Linear(self.fusion_dim * 2, 512), \n            nn.LayerNorm(512),\n            nn.GELU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 3) # Total, GDM, Green\n        )\n        self.softplus = nn.Softplus(beta=1.0)\n\n    # 稳健特征提取\n    def extract_features(self, x):\n        try:\n            # 尝试获取最后4层\n            raw_feats = self.backbone.forward_intermediates(x, n=4)\n        except:\n            raw_feats = [self.backbone.forward_features(x)]\n\n        final_feats = []\n        target_n_patches = (Config.img_size[0] // 14) * (Config.img_size[1] // 14)\n\n        for f in raw_feats:\n            if isinstance(f, (tuple, list)):\n                f = f[0]\n            \n            # 维度归一化\n            if f.ndim == 4:\n                f = f.flatten(2).transpose(1, 2)\n            \n            # 切片 (去除 CLS/Reg)\n            if f.shape[1] > target_n_patches:\n                f = f[:, -target_n_patches:, :]\n            \n            final_feats.append(f)\n        \n        return final_feats\n\n    def forward(self, left_x, right_x):\n        # 1. 提取多层特征\n        l_feats = self.extract_features(left_x)\n        r_feats = self.extract_features(right_x)\n            \n        # 2. 拼接\n        l_concat = torch.cat(l_feats, dim=-1) \n        r_concat = torch.cat(r_feats, dim=-1)\n        \n        # 3. SDA 立体交互\n        l_enhanced = self.sda_left(query=l_concat, key_value=r_concat)\n        r_enhanced = self.sda_right(query=r_concat, key_value=l_concat)\n        \n        # 4. 拼接左右\n        final_feat = torch.cat([l_enhanced, r_enhanced], dim=-1) \n        \n        # 5. Dense Prediction\n        patch_preds = self.head(final_feat) \n        \n        # 6. 聚合\n        image_preds = patch_preds.mean(dim=1)\n        \n        return self.softplus(image_preds)\n\n# ==========================================\n# 3. 数据预处理\n# ==========================================\ndef get_inference_transforms():\n    return transforms.Compose([\n        transforms.Resize(Config.img_size, interpolation=transforms.InterpolationMode.BICUBIC),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=Config.mean, std=Config.std)\n    ])\n\nclass BiomassTestDataset(Dataset):\n    def __init__(self, df, data_root, transform=None):\n        self.df = df\n        self.data_root = data_root\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        sample_id = row['sample_id']\n        img_path = row['image_path']\n        \n        if not img_path.startswith('/'):\n            img_path = os.path.join(self.data_root, img_path)\n            \n        try:\n            image = Image.open(img_path).convert('RGB')\n        except:\n            image = Image.new('RGB', (Config.img_size[0] * 2, Config.img_size[1]))\n\n        # 核心逻辑：垂直切分\n        w, h = image.size\n        mid = w // 2\n        left = image.crop((0, 0, mid, h))\n        right = image.crop((mid, 0, w, h))\n        \n        if self.transform:\n            left = self.transform(left)\n            right = self.transform(right)\n            \n        return left, right, sample_id\n\n# ==========================================\n# 4. 推理主程序\n# ==========================================\ndef run_inference():\n    print(\"🚀 Starting Stereo SDA Model Inference...\")\n    \n    if not os.path.exists(Config.test_csv_path):\n        print(\"Test CSV not found.\")\n        return\n    \n    test_df = pd.read_csv(Config.test_csv_path)\n    unique_test_df = test_df.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n    print(f\"Unique images: {len(unique_test_df)}\")\n    \n    dataset = BiomassTestDataset(unique_test_df, Config.data_root, transform=get_inference_transforms())\n    loader = DataLoader(dataset, batch_size=Config.batch_size, shuffle=False, num_workers=Config.num_workers)\n\n    # 加载模型\n    models = []\n    model_paths = sorted(glob.glob(os.path.join(Config.models_dir, \"*.pth\")))\n    \n    if not model_paths:\n        model_paths = sorted(glob.glob(os.path.join(Config.models_dir, \"**/*.pth\"), recursive=True))\n        \n    print(f\"Found {len(model_paths)} models.\")\n    \n    for path in model_paths:\n        try:\n            model = MultiLayerSdaBiomassModel(Config.model_name, pretrained=False)\n            state_dict = torch.load(path, map_location=Config.device)\n            model.load_state_dict(state_dict)\n            model.to(Config.device)\n            model.eval()\n            models.append(model)\n            print(f\"Loaded: {os.path.basename(path)}\")\n        except Exception as e:\n            print(f\"⚠️ Failed to load {path}: {e}\")\n\n    if not models:\n        print(\"No models loaded.\")\n        return\n\n    results = []\n    print(\"Predicting...\")\n    \n    with torch.no_grad():\n        for left, right, sample_ids in tqdm(loader):\n            left = left.to(Config.device)\n            right = right.to(Config.device)\n            \n            # TTA: 水平翻转\n            left_flip = torch.flip(left, dims=[3])\n            right_flip = torch.flip(right, dims=[3])\n            \n            batch_preds = []\n            for model in models:\n                # 原始预测\n                p1 = model(left, right)\n                # TTA 预测\n                p2 = model(left_flip, right_flip)\n                \n                # 平均并还原 (Log1p -> Linear)\n                p = (p1 + p2) / 2.0\n                p_linear = torch.expm1(p)\n                batch_preds.append(p_linear.cpu().numpy())\n            \n            # Fold Ensemble\n            avg_preds = np.mean(batch_preds, axis=0) # [B, 3]\n            \n            for i, sample_id in enumerate(sample_ids):\n                # 预测输出顺序: [Total, GDM, Green] (对应训练时的 targets)\n                pred_total = max(0, avg_preds[i, 0])\n                pred_gdm   = max(0, avg_preds[i, 1])\n                pred_green = max(0, avg_preds[i, 2])\n                \n                # 逻辑约束\n                if pred_gdm < pred_green: pred_gdm = pred_green\n                if pred_total < pred_gdm: pred_total = pred_gdm\n                \n                val_clover = max(0, pred_gdm - pred_green)\n                val_dead   = max(0, pred_total - pred_gdm)\n                \n                prefix = sample_id.split('__')[0]\n                \n                results.append({'sample_id': f\"{prefix}__Dry_Green_g\",  'target': pred_green})\n                results.append({'sample_id': f\"{prefix}__Dry_Clover_g\", 'target': val_clover})\n                results.append({'sample_id': f\"{prefix}__Dry_Dead_g\",   'target': val_dead})\n                results.append({'sample_id': f\"{prefix}__GDM_g\",        'target': pred_gdm})\n                results.append({'sample_id': f\"{prefix}__Dry_Total_g\",  'target': pred_total})\n\n    submission = pd.DataFrame(results)\n    submission = submission.drop_duplicates(subset=['sample_id'])\n    submission.to_csv(Config.output_name, index=False)\n    \n    print(f\"✅ Submission saved to {Config.output_name}\")\n    print(submission.head())\n\nif __name__ == '__main__':\n    run_inference()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-03T13:43:32.726505Z","iopub.execute_input":"2026-01-03T13:43:32.726738Z","iopub.status.idle":"2026-01-03T13:43:32.740486Z","shell.execute_reply.started":"2026-01-03T13:43:32.726722Z","shell.execute_reply":"2026-01-03T13:43:32.739942Z"}},"outputs":[],"execution_count":null}]}