{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13737905,"sourceType":"datasetVersion","datasetId":8741100},{"sourceId":13986965,"sourceType":"datasetVersion","datasetId":8913148},{"sourceId":14067043,"sourceType":"datasetVersion","datasetId":8953834},{"sourceId":14203354,"sourceType":"datasetVersion","datasetId":9058629},{"sourceId":14464889,"sourceType":"datasetVersion","datasetId":9011999}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install --no-deps /kaggle/input/open-clip-dependencies-2/timm-1.0.22-py3-none-any.whl\n!pip install --no-deps /kaggle/input/protobuf528-1209/protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:35:47.717941Z","iopub.execute_input":"2026-01-26T13:35:47.718200Z","iopub.status.idle":"2026-01-26T13:35:55.659898Z","shell.execute_reply.started":"2026-01-26T13:35:47.718180Z","shell.execute_reply":"2026-01-26T13:35:55.659098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile infer_single.py\n\n# 📦\nimport os\nimport gc\nimport cv2\nimport json\nimport time\nimport subprocess\nfrom pathlib import Path\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport random\n\nfrom tqdm import tqdm\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom torch.amp import autocast, GradScaler       \n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom PIL import Image\nimport timm\n\n\ndir = Path('/kaggle/input/csiro-biomass')\nDIRS = {\n    \"dir\"  : dir,\n    \"train\": Path(dir, \"train\"),\n    \"test\" : Path(dir, \"test\"),\n    \"model\": Path('/kaggle/input/single-1209-swa-models'),\n    \"data\" : Path(\"/kaggle/working/\"),\n}\n\n\ndef show_df_info(df, name: str):\n    print(f\"📊 {name:<16} shape: {str(df.shape):<16}  列名: {df.columns.tolist()}\")\n\n\ndef move_column_first(df, col_name):\n    if col_name not in df.columns:\n        raise ValueError(f\"列 '{col_name}' 不存在于 DataFrame 中。\")\n\n    cols = [col_name] + [c for c in df.columns if c != col_name]\n    return df[cols]\n\n\ndef setup_seed(seed, deterministic=True):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\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 = deterministic\n    torch.backends.cudnn.benchmark = False\n    \n\n# Model \nclass MySingleStreamModel(nn.Module):\n    def __init__(self, \n                backbone_name=\"convnext_tiny\", \n                pretrained=True, \n                config = None):\n        super().__init__()\n        print(\"Current backbone:\", backbone_name)\n        img_size = (config[\"img_size\"], config.get(\"img_width\", config[\"img_size\"]))\n        self.backbone = timm.create_model(\n            backbone_name,\n            pretrained=pretrained,\n            num_classes=0,\n            img_size=img_size\n        )\n        in_dim = self.backbone.num_features\n\n        params = list(self.backbone.parameters())\n        freeze_until = int(len(params) * config[\"freeze_ratio\"])\n        for i, p in enumerate(params):\n            p.requires_grad = i >= freeze_until \n\n        self.feature_dim = in_dim\n        print(\"feature dim = \", self.feature_dim)\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.feature_dim, 512),\n                nn.ReLU(),\n                nn.Dropout(0.3),\n                nn.Linear(512, 128),\n                nn.ReLU(),\n                nn.Linear(128, 1)\n            )\n        self.head_green  = make_head()\n        self.head_clover = make_head()\n        self.head_dead   = make_head()\n        self.head_gdm = make_head()\n        self.head_total = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n\n    def forward(self, img):\n        feat = self.backbone(img)\n        \n        G = self.softplus(self.head_green(feat))\n        C = self.softplus(self.head_clover(feat))\n        D = self.softplus(self.head_dead(feat))\n        # G = torch.where(G < 0.4, torch.zeros_like(G), G)\n        # C = torch.where(C < 0.4, torch.zeros_like(C), C)\n        # D = torch.where(D < 0.4, torch.zeros_like(D), D)\n        GDM   = self.softplus(self.head_gdm(feat))\n        Total = self.softplus(self.head_total(feat))\n\n        preds = torch.cat([G, C, D, GDM, Total], dim=1)\n        return preds\n\n    def compute_loss(self, preds, targets):\n        preds = preds.view(-1, 5)\n        targets = targets.view(-1, 5)\n        weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5], device=preds.device)\n        y_true_flat = targets.view(-1)\n        y_pred_flat = preds.view(-1)\n        w_flat = torch.cat([\n            torch.full_like(targets[:, i], weights[i], device=preds.device)\n            for i in range(5)\n        ])\n        y_mean = torch.sum(w_flat * y_true_flat) / torch.sum(w_flat)\n        ss_res = torch.sum(w_flat * (y_true_flat - y_pred_flat) ** 2)\n        ss_tot = torch.sum(w_flat * (y_true_flat - y_mean) ** 2)\n        loss = ss_res / ss_tot\n        return loss\n\n\nclass SingleStreamDataset(Dataset):\n    def __init__(self, df, image_dir, config, pre_transform=None, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.image_dir = image_dir\n        self.target_cols = config[\"target_cols\"]\n        self.pre_transform = pre_transform\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        img_path = Path(self.image_dir, str(row[\"image_path\"]))\n        try:\n            image = Image.open(img_path).convert(\"RGB\")\n        except Exception as e:\n            print(f\"⚠️ Failed to read image: {img_path} ({e})\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n\n        image = np.array(image)\n        if self.pre_transform:\n            image = self.pre_transform(image=image)[\"image\"]\n        \n        if self.transform:\n            image = self.transform(image=image)[\"image\"]\n\n        if self.target_cols is not None:\n            targets = torch.tensor(\n                row[self.target_cols].astype(float).values,\n                dtype=torch.float32\n            )\n            return image, targets\n        else:\n            return image\n\n\ndef get_pre_transforms():\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.OneOf([\n            A.RandomBrightnessContrast(\n                brightness_limit=(0.8, 1.2), \n                contrast_limit=(0.8, 1.2),   \n                p=0.3\n            ),\n            A.ColorJitter(p=0.7)\n        ], p=0.4),\n    ])\n\n\ndef _get_resize_dims(config):\n    height = config[\"img_size\"]\n    width = int(config.get(\"img_width\", config[\"img_size\"]))\n    return height, width\n\n\ndef get_train_transforms(config):\n    height, width = _get_resize_dims(config)\n    return A.Compose([\n        A.Resize(height, width),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_valid_transforms(config):\n    height, width = _get_resize_dims(config)\n    return A.Compose([\n        A.Resize(height, width),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_tta_transforms(config):\n    height, width = _get_resize_dims(config)\n    return {\n        \"base\": A.Compose([\n            A.Resize(height, width),\n            A.Normalize(mean=[0.485, 0.456, 0.406],\n                        std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        \"hflip\": A.Compose([\n            A.HorizontalFlip(p=1.0)\n        ])\n    }\n\n\nconfig = {\n    # ⚙️ basic train params\n    \"seed\"                           : 43,  \n    \"freeze_ratio\"                   : 0.8,\n    \"num_workers\"                    : 4,\n    \"prefetch_factor\"                : 3,\n    \"batch_size\"                     : 4,\n    \"img_size\"                       : 1024,\n    \"img_ratio\"                      : 2.0,\n    \"backbone_name\"                  : \"vit_large_patch16_dinov3_qkvb.lvd1689m\",\n\n    \"target_cols\": [\n        \"Dry_Green_g\",\n        \"Dry_Clover_g\",\n        \"Dry_Dead_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\"\n    ]\n}\nconfig[\"img_width\"] = int(config[\"img_size\"] * config.get(\"img_ratio\", 1.0))\nisPREDICT   = True\nDEBUG       = False\n\n\ndef load_and_prepare_test_df():\n    df_file_path = Path(DIRS[\"dir\"]) / \"test.csv\"\n    df = pd.read_csv(df_file_path)\n    df[\"ID\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df = move_column_first(df, \"ID\")\n    df[\"target\"] = 0  \n    df_targets = (\n        df.pivot_table(index=\"ID\", columns=\"target_name\", values=\"target\", aggfunc=\"first\")\n        .reset_index()\n    )\n    df_targets.columns.name = None\n    df_meta = df[[\"ID\", \"image_path\"]].drop_duplicates(subset=\"ID\")\n    df_test = pd.merge(df_meta, df_targets, on=\"ID\", how=\"left\")\n    show_df_info(df_test, \"df_test\")\n    print(f\"✅ Test set loaded: {df_test.shape}\")\n    return df_test\n\n\ndef load_model_for_inference(model_path, device, config):\n    model = MySingleStreamModel(config[\"backbone_name\"], pretrained=False, config=config)\n    model = model.to(device).to(memory_format=torch.channels_last)\n    try:\n        state_dict = torch.load(model_path, map_location=device, weights_only=True)\n    except TypeError:\n        state_dict = torch.load(model_path, map_location=device)\n    model.load_state_dict(state_dict, strict=False)\n    model.eval()\n    return model\n\n\ndef predict_with_model(model, dataloader, device):\n    preds_list = []\n    model.eval()\n    with torch.no_grad():\n        for step, (images, targets) in enumerate(dataloader):\n            images = images.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last)\n            targets = targets.to(device, non_blocking=True)\n            with torch.amp.autocast(device_type='cuda'):\n                preds = model(images)\n            preds_list.append(preds.cpu().numpy())\n    return np.concatenate(preds_list, axis=0)\n\n\ndef predict_ensemble(df_test, pre_transform, transform, model_dir, device, config):\n    model_paths = sorted(Path(model_dir).glob(\"*.pt\"))\n    assert len(model_paths) > 0, f\"❌ No model files found in: {model_dir}\"\n\n    print(f\"✅ Detected {len(model_paths)} 个模型:\")\n    for p in model_paths:\n        print(\"   -\", p.name)\n    test_dataset = SingleStreamDataset(\n        df_test,\n        DIRS[\"dir\"],\n        config,\n        pre_transform=pre_transform,\n        transform=transform,\n    )\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=config[\"batch_size\"],\n        shuffle=False,\n        num_workers=2,\n        pin_memory=True,\n    )\n    fold_preds = []\n    for fold, model_path in enumerate(model_paths):\n        print(f\"Fold {fold+1}/{len(model_paths)} 推理: {model_path.name}\")\n        model = load_model_for_inference(model_path, device, config)\n        fold_pred = predict_with_model(model, test_loader, device)\n        fold_preds.append(fold_pred)\n    preds_mean = np.mean(fold_preds, axis=0)\n    df_pred = pd.DataFrame(preds_mean, columns=config[\"target_cols\"])\n    df_pred[\"ID\"] = df_test[\"ID\"]\n    df_pred = df_pred[[\"ID\"] + config[\"target_cols\"]]\n    show_df_info(df_pred, \"df_pred\")\n    return df_pred\n\n\ndef generate_submission(df_pred_final):\n    ordered_target_cols = [\n        \"Dry_Green_g\",  \n        \"Dry_Clover_g\", \n        \"Dry_Dead_g\",  \n        \"GDM_g\",       \n        \"Dry_Total_g\"  \n    ]\n\n    if \"Dry_Clover_g\" in df_pred_final.columns:\n        df_pred_final[\"Dry_Clover_g\"] = df_pred_final[\"Dry_Clover_g\"] * 0.8\n    else:\n        print(\"   ⚠️ Warning: Column Dry_Clover_g not found in DataFrame\")\n        \n\n    df_submit = (\n        df_pred_final.melt(\n            id_vars=\"ID\",\n            value_vars=ordered_target_cols,\n            var_name=\"target_name\",\n            value_name=\"target\"\n        )\n    )\n\n    df_submit[\"sample_id\"] = df_submit[\"ID\"] + \"__\" + df_submit[\"target_name\"]\n    df_submit = df_submit[[\"sample_id\", \"target\"]]\n    df_submit = df_submit.sort_values(\"sample_id\").reset_index(drop=True)\n    print(df_submit.head(10))\n    df_submit.to_csv(\"submission1.csv\", index=False)\n    print(\"\\n✅ Submission file generated: submission1.csv\")\n\n\ndef run_tta_prediction(df_test, model_dir, device, config):\n    tta_transforms = get_tta_transforms(config)\n    tta_names = list(tta_transforms.keys())\n    print(f\"\\n✅ Detected {len(tta_names)} TTA modes: {tta_names}\\n\")\n\n    all_preds = []\n    for name, _ in tta_transforms.items():\n        print(f\"\\n🚀 TTA mode: {name}\")\n        if name == \"base\":\n            pre_transform = None\n            transform = tta_transforms[name]\n        else:\n            pre_transform = tta_transforms[name]\n            transform = tta_transforms[\"base\"]\n        df_pred = predict_ensemble(df_test, pre_transform, transform, model_dir, device, config)\n        all_preds.append(df_pred[config[\"target_cols\"]].values)\n\n    mean_preds = np.mean(all_preds, axis=0)\n    df_pred_final = df_pred.copy()\n    df_pred_final[config[\"target_cols\"]] = mean_preds\n    return df_pred_final\n\n\nif __name__ == \"__main__\" and isPREDICT:\n    print(\"\\n🧠 Starting prediction pipeline...\")\n    setup_seed(config[\"seed\"])    \n    device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n\n    df_test = load_and_prepare_test_df()\n    print(f\"\\nCurrent dataset shape: {df_test.shape}\\n\")\n\n    model_dir = DIRS[\"model\"]\n    print(f\"Model directory loaded: {model_dir}\")\n\n    df_pred_final = run_tta_prediction(df_test, model_dir, device, config)\n    generate_submission(df_pred_final)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:35:55.661729Z","iopub.execute_input":"2026-01-26T13:35:55.661993Z","iopub.status.idle":"2026-01-26T13:35:55.674718Z","shell.execute_reply.started":"2026-01-26T13:35:55.661968Z","shell.execute_reply":"2026-01-26T13:35:55.673853Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile infer_dual.py\n\n# 📦\nimport os\nimport gc\nimport cv2\nimport json\nimport time\nimport subprocess\nfrom pathlib import Path\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport random\n\nfrom tqdm import tqdm\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom torch.amp import autocast, GradScaler       \n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom PIL import Image\nimport timm\n\ndir = Path('/kaggle/input/csiro-biomass')\nDIRS = {\n    \"dir\"  : dir,\n    \"train\": Path(dir, \"train\"),\n    \"test\" : Path(dir, \"test\"),\n    \"model\": Path('/kaggle/input', \"dinvov3large-1204-cv\"),\n    \"data\" : Path(\"/kaggle/working/\"),\n}\n\n\ndef setup_seed(seed, deterministic=True):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\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 = deterministic\n    torch.backends.cudnn.benchmark = False\n    \n\ndef show_df_info(df, name: str):\n    print(f\"📊 {name:<16} shape: {str(df.shape):<16}  列名: {df.columns.tolist()}\")\n\n\ndef move_column_first(df, col_name):\n    if col_name not in df.columns:\n        raise ValueError(f\"列 '{col_name}' 不存在于 DataFrame 中。\")\n    cols = [col_name] + [c for c in df.columns if c != col_name]\n    return df[cols]\n\n    \nclass MyDualStreamModel(nn.Module):\n    def __init__(self, \n                backbone_name=\"convnext_tiny\", \n                pretrained=True, \n                config = None):\n        super().__init__()\n        print(\"Current backbone:\", backbone_name)\n        self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0)\n        in_dim = self.backbone.num_features\n\n        params = list(self.backbone.parameters())\n        freeze_until = int(len(params) * config[\"freeze_ratio\"])\n        for i, p in enumerate(params):\n            p.requires_grad = i >= freeze_until     \n        self.fusion_dim = in_dim * 2\n        print(\"feature fusion dim = \", self.fusion_dim)\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.fusion_dim, 512),\n                nn.ReLU(),\n                nn.Dropout(0.3),\n                nn.Linear(512, 128),\n                nn.ReLU(),\n                nn.Linear(128, 1)\n            )\n        self.head_green  = make_head()\n        self.head_clover = make_head()\n        self.head_dead   = make_head()\n        self.head_gdm = make_head()\n        self.head_total = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n\n    def forward(self, img_left, img_right):\n        feat_left  = self.backbone(img_left)\n        feat_right = self.backbone(img_right)\n        fused = torch.cat([feat_left, feat_right], dim=1)\n        G = self.softplus(self.head_green(fused))\n        C = self.softplus(self.head_clover(fused))\n        D = self.softplus(self.head_dead(fused))\n        # G = torch.where(G < 0.4, torch.zeros_like(G), G)\n        # C = torch.where(C < 0.4, torch.zeros_like(C), C)\n        # D = torch.where(D < 0.4, torch.zeros_like(D), D)\n        GDM   = G + C\n        Total = G + C + D\n        preds = torch.cat([G, C, D, GDM, Total], dim=1)\n        return preds\n\n\nclass DualStreamDataset(Dataset):\n    def __init__(self, df, image_dir, config, pre_transform=None, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.image_dir = image_dir\n        self.target_cols = config[\"target_cols\"]\n        self.pre_transform = pre_transform\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        img_path = Path(self.image_dir, str(row[\"image_path\"]))\n        try:\n            image = Image.open(img_path).convert(\"RGB\")\n        except Exception as e:\n            print(f\"⚠️ Failed to read image: {img_path} ({e})\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n\n        image = np.array(image)\n        if self.pre_transform:\n            image = self.pre_transform(image=image)[\"image\"]\n        h, w, _ = image.shape\n        mid = w // 2\n        img_left = image[:, :mid]\n        img_right = image[:, mid:]\n        if self.transform:\n            img_left = self.transform(image=img_left)[\"image\"]\n            img_right = self.transform(image=img_right)[\"image\"]\n        if self.target_cols is not None:\n            targets = torch.tensor(\n                row[self.target_cols].astype(float).values,\n                dtype=torch.float32\n            )\n            return img_left, img_right, targets\n        else:\n            return img_left, img_right\n\n\ndef get_pre_transforms():\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.OneOf([\n            A.RandomBrightnessContrast(\n                brightness_limit=(0.8, 1.2), \n                contrast_limit=(0.8, 1.2),   \n                p=0.3\n            ),\n            A.ColorJitter(p=0.7)\n        ], p=0.4),\n    ])\n\n\ndef get_train_transforms(size):\n    return A.Compose([\n        A.Resize(size, size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_valid_transforms(size):\n    return A.Compose([\n        A.Resize(size, size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_tta_transforms(size):\n    return {\n        \"base\": A.Compose([\n            A.Resize(size, size),\n            A.Normalize(mean=[0.485, 0.456, 0.406],\n                        std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        \"hflip\": A.Compose([\n            A.HorizontalFlip(p=1.0)\n        ])\n    }\n\n\nconfig = {\n    \"seed\"                           : 43,  \n    \"freeze_ratio\"                   : 0.8,\n    \"num_workers\"                    : 4,\n    \"prefetch_factor\"                : 3,\n    \"batch_size\"                     : 8,\n    \"img_size\"                       : 1024,\n    \"backbone_name\"                  : \"vit_large_patch16_dinov3_qkvb.lvd1689m\",\n    \"target_cols\": [\n        \"Dry_Green_g\",\n        \"Dry_Clover_g\",\n        \"Dry_Dead_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\"\n    ]\n}\nisPREDICT   = True\nDEBUG       = False\n\n\ndef load_and_prepare_test_df():\n    df_file_path = Path(DIRS[\"dir\"]) / \"test.csv\"\n    df = pd.read_csv(df_file_path)\n\n    df[\"ID\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df = move_column_first(df, \"ID\")\n    df[\"target\"] = 0  \n\n    df_targets = (\n        df.pivot_table(index=\"ID\", columns=\"target_name\", values=\"target\", aggfunc=\"first\")\n        .reset_index()\n    )\n    df_targets.columns.name = None\n\n    df_meta = df[[\"ID\", \"image_path\"]].drop_duplicates(subset=\"ID\")\n    df_test = pd.merge(df_meta, df_targets, on=\"ID\", how=\"left\")\n    show_df_info(df_test, \"df_test\")\n\n    print(f\"✅ Test set loaded: {df_test.shape}\")\n    return df_test\n\n\ndef load_model_for_inference(model_path, device, config):\n    model = MyDualStreamModel(config[\"backbone_name\"], pretrained=False, config=config)\n    model = model.to(device).to(memory_format=torch.channels_last)\n    try:\n        state_dict = torch.load(model_path, map_location=device, weights_only=True)\n    except TypeError:\n        state_dict = torch.load(model_path, map_location=device)\n    model.load_state_dict(state_dict, strict=False)\n    model.eval()\n    return model\n\n\ndef predict_with_model(model, dataloader, device):\n    preds_list = []\n    model.eval()\n    with torch.no_grad():\n        for step, (img_left, img_right, targets) in enumerate(dataloader):\n            img_left   = img_left.to(device, non_blocking=True)\n            img_right  = img_right.to(device, non_blocking=True)\n            targets    = targets.to(device, non_blocking=True)\n            with torch.amp.autocast(device_type='cuda'):\n                preds = model(img_left, img_right)\n            preds_list.append(preds.cpu().numpy())\n    return np.concatenate(preds_list, axis=0)\n\n\ndef predict_ensemble(df_test, pre_transform, transform, model_dir, device, config):\n    model_paths = sorted(Path(model_dir).glob(\"fold*.pt\"))\n    assert len(model_paths) > 0, f\"❌ No model files found in: {model_dir}\"\n    print(f\"✅ Detected {len(model_paths)} 个模型:\")\n    for p in model_paths:\n        print(\"   -\", p.name)\n    test_dataset = DualStreamDataset(\n        df_test,\n        DIRS[\"dir\"],\n        config,\n        pre_transform=pre_transform,\n        transform=transform,\n    )\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=config[\"batch_size\"],\n        shuffle=False,\n        num_workers=2,\n        pin_memory=True,\n    )\n    fold_preds = []\n    for fold, model_path in enumerate(model_paths):\n        print(f\"Fold {fold+1}/{len(model_paths)} 推理: {model_path.name}\")\n        model = load_model_for_inference(model_path, device, config)\n        fold_pred = predict_with_model(model, test_loader, device)\n        fold_preds.append(fold_pred)\n    preds_mean = np.mean(fold_preds, axis=0)\n    df_pred = pd.DataFrame(preds_mean, columns=config[\"target_cols\"])\n    df_pred[\"ID\"] = df_test[\"ID\"]\n    df_pred = df_pred[[\"ID\"] + config[\"target_cols\"]]\n    show_df_info(df_pred, \"df_pred\")\n    return df_pred\n\n\ndef generate_submission(df_pred_final):\n    ordered_target_cols = [\n        \"Dry_Green_g\",  \n        \"Dry_Clover_g\", \n        \"Dry_Dead_g\",   \n        \"GDM_g\",        \n        \"Dry_Total_g\"   \n    ]\n    if \"Dry_Clover_g\" in df_pred_final.columns:\n        df_pred_final[\"Dry_Clover_g\"] = df_pred_final[\"Dry_Clover_g\"] * 0.8\n    else:\n        print(\"   ⚠️ Warning: Column Dry_Clover_g not found in DataFrame\")\n        \n    df_submit = (\n        df_pred_final.melt(\n            id_vars=\"ID\",\n            value_vars=ordered_target_cols,\n            var_name=\"target_name\",\n            value_name=\"target\"\n        )\n    )\n    df_submit[\"sample_id\"] = df_submit[\"ID\"] + \"__\" + df_submit[\"target_name\"]\n    df_submit = df_submit[[\"sample_id\", \"target\"]]\n    df_submit = df_submit.sort_values(\"sample_id\").reset_index(drop=True)\n    print(df_submit.head(10))\n    df_submit.to_csv(\"submission2.csv\", index=False)\n    print(\"\\n✅ Submission file generated: submission2.csv\")\n\n\ndef run_tta_prediction(df_test, model_dir, device, config):\n    tta_transforms = get_tta_transforms(config[\"img_size\"])\n    tta_names = list(tta_transforms.keys())\n    print(f\"\\n✅ Detected {len(tta_names)} TTA modes: {tta_names}\\n\")\n    all_preds = []\n    for name, _ in tta_transforms.items():\n        print(f\"\\n🚀 TTA mode: {name}\")\n        if name == \"base\":\n            pre_transform = None\n            transform = tta_transforms[name]\n        else:\n            pre_transform = tta_transforms[name]\n            transform = tta_transforms[\"base\"]\n        df_pred = predict_ensemble(df_test, pre_transform, transform, model_dir, device, config)\n        all_preds.append(df_pred[config[\"target_cols\"]].values)\n    mean_preds = np.mean(all_preds, axis=0)\n    df_pred_final = df_pred.copy()\n    df_pred_final[config[\"target_cols\"]] = mean_preds\n    return df_pred_final\n\n\nif __name__ == \"__main__\" and isPREDICT:\n    print(\"\\n🧠 Starting prediction pipeline...\")\n    setup_seed(config[\"seed\"])    \n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n   \n    df_test = load_and_prepare_test_df()\n    print(f\"\\nCurrent dataset shape: {df_test.shape}\\n\")\n    \n    model_dir = DIRS[\"model\"]\n    print(f\"Model directory loaded: {model_dir}\")\n\n    df_pred_final = run_tta_prediction(df_test, model_dir, device, config)\n    generate_submission(df_pred_final)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:35:55.675866Z","iopub.execute_input":"2026-01-26T13:35:55.676189Z","iopub.status.idle":"2026-01-26T13:35:55.698686Z","shell.execute_reply.started":"2026-01-26T13:35:55.676171Z","shell.execute_reply":"2026-01-26T13:35:55.698147Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\nCUDA_VISIBLE_DEVICES=0 python infer_single.py > log_single.txt 2>&1 &\nCUDA_VISIBLE_DEVICES=1 python infer_dual.py > log_dual.txt 2>&1 &\necho \"GPU 0: Single Stream\"\necho \"GPU 1: Dual Stream\"\n\nwait\n\necho \"single&&dual done！\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:35:55.700311Z","iopub.execute_input":"2026-01-26T13:35:55.700533Z","iopub.status.idle":"2026-01-26T13:38:05.247888Z","shell.execute_reply.started":"2026-01-26T13:35:55.700516Z","shell.execute_reply":"2026-01-26T13:38:05.246975Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!tail -n 1000 log_single.txt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:05.248854Z","iopub.execute_input":"2026-01-26T13:38:05.249161Z","iopub.status.idle":"2026-01-26T13:38:05.370278Z","shell.execute_reply.started":"2026-01-26T13:38:05.249135Z","shell.execute_reply":"2026-01-26T13:38:05.369601Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!tail -n 1000 log_dual.txt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:05.371204Z","iopub.execute_input":"2026-01-26T13:38:05.371453Z","iopub.status.idle":"2026-01-26T13:38:05.488417Z","shell.execute_reply.started":"2026-01-26T13:38:05.371416Z","shell.execute_reply":"2026-01-26T13:38:05.487699Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import gc\nimport torch\n\ngc.collect()\nif torch.cuda.is_available():\n    torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:05.489425Z","iopub.execute_input":"2026-01-26T13:38:05.489700Z","iopub.status.idle":"2026-01-26T13:38:07.400886Z","shell.execute_reply.started":"2026-01-26T13:38:05.489675Z","shell.execute_reply":"2026-01-26T13:38:07.400237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\ndf1 = pd.read_csv('submission1.csv')\ndf2 = pd.read_csv('submission2.csv')\n\ndf1 = df1.sort_values('sample_id').reset_index(drop=True)\ndf2 = df2.sort_values('sample_id').reset_index(drop=True)\n\nif not df1['sample_id'].equals(df2['sample_id']):\n    print(\"警告：两个文件的sample_id不完全一致！\")\n    print(\"将按sample_id对齐...\")\n    df_merged = pd.merge(df1, df2, on='sample_id', suffixes=('_1', '_2'))\nelse:\n    df_merged = df1.copy()\n    df_merged['target_1'] = df1['target']\n    df_merged['target_2'] = df2['target']\n\n\ndf_merged['target'] = 0.8 * df_merged['target_1'] + 0.2 * df_merged['target_2']\ndf_merged['target'] = df_merged['target'].apply(lambda x: max(x, 0) if x >= 0.1 else 0)\ndf_merged['target'] = df_merged['target'].clip(lower=0)\nresult_df = df_merged[['sample_id', 'target']].copy()\n\nnegative_count = (result_df['target'] < 0).sum()\nbelow_01_count = ((df_merged['target_1'] < 0.1) | (df_merged['target_2'] < 0.1)).sum()\nclipped_count = (result_df['target'] == 0).sum()\n\nprint(f\"处理后统计:\")\nprint(f\"  负值数量: {negative_count}\")\nprint(f\"  原始值低于0.1的数量: {below_01_count}\")\nprint(f\"  被置为0的数量: {clipped_count}\")\nprint(f\"  最终值范围: [{result_df['target'].min():.3f}, {result_df['target'].max():.3f}]\")\n\nresult_df.to_csv('submission_stage1.csv', index=False)\nprint(f\"\\n✅ 提交文件已保存: submission.csv\")\n\nprint(result_df.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:07.401747Z","iopub.execute_input":"2026-01-26T13:38:07.402255Z","iopub.status.idle":"2026-01-26T13:38:07.701089Z","shell.execute_reply.started":"2026-01-26T13:38:07.402199Z","shell.execute_reply":"2026-01-26T13:38:07.700157Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# stage2 online training","metadata":{}},{"cell_type":"code","source":"%%writefile train_single.py\n\n# 📦\nimport os\nimport gc\nimport cv2\nimport json\nimport time\nimport shutil\nimport subprocess\nfrom pathlib import Path\nfrom datetime import datetime, timedelta\nimport math\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport random\nfrom tqdm import tqdm\nfrom sklearn.model_selection import KFold, GroupKFold\nfrom sklearn.metrics import r2_score\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.optim.lr_scheduler import _LRScheduler\nfrom torch.amp import autocast, GradScaler       \nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom PIL import Image\nimport timm\nfrom torchvision import models, transforms\nfrom torchvision.models import get_model_weights\nimport collections\n\n\ndir = Path('/kaggle/input/csiro-biomass')\nDIRS = {\n    \"dir\"  : dir,\n    \"train\": Path(dir, \"train\"),\n    \"test\" : Path(dir, \"test\"),\n    \"model\": Path('/kaggle/working/stage2_train_single_swa'),\n    \"save\" : Path('/kaggle/working/stage2_train_single'),\n    \"data\" : Path(\"/kaggle/working/\"),\n}\n\n\ndef show_df_info(df, name: str):\n    print(f\"📊 {name:<16} shape: {str(df.shape):<16}  列名: {df.columns.tolist()}\")\n\n\ndef move_column_first(df, col_name):\n    if col_name not in df.columns:\n        raise ValueError(f\"列 '{col_name}' 不存在于 DataFrame 中。\")\n    cols = [col_name] + [c for c in df.columns if c != col_name]\n    return df[cols]\n\n\ndef setup_seed(seed, deterministic=True):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\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 = deterministic\n    torch.backends.cudnn.benchmark = False\n\n\ndef load_and_prepare_train_stage2_df():\n    df_file_path = 'submission_stage1.csv'\n    df = pd.read_csv(df_file_path)\n    df[\"ID\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df[\"image_path\"] = df[\"ID\"].apply(lambda x: f\"test/{x}.jpg\")\n    df[\"target_name\"] = df[\"sample_id\"].str.split(\"__\").str[1]\n    df = move_column_first(df, \"ID\")\n\n    df_targets = (\n        df.pivot_table(\n            index=\"ID\",\n            columns=\"target_name\",\n            values=\"target\",\n            aggfunc=\"first\"\n        )\n        .reset_index()\n    )\n    df_targets.columns.name = None  \n    meta_cols = [\n        \"ID\", \"image_path\"\n    ]\n    df_meta = df[meta_cols].drop_duplicates(subset=\"ID\")\n    df_train = pd.merge(df_meta, df_targets, on=\"ID\", how=\"left\")\n    show_df_info(df_train, \"df_train\")\n    return df_train\n\n\ndf_pseudo = load_and_prepare_train_stage2_df()\ndf_stage1_path = '/kaggle/input/train-single-csv/train_with_folds_singleflow.csv'\ndf_stage1 = pd.read_csv(df_stage1_path)\ndf_stage2_train = pd.concat([df_pseudo, df_stage1], ignore_index=True)\ndf_stage2_train.reset_index(drop=True, inplace=True)\n\n\nconfig = {\n    \"seed\"                           : 42,  \n    \"epochs\"                         : 12,\n    \"freeze_ratio\"                   : 0.8,\n    \"num_workers\"                    : 4,\n    \"prefetch_factor\"                : 2,\n    \"batch_size\"                     : 4,\n    \"lr\"                             : 1e-4,\n    \"img_size\"                       : 1024,\n    \"img_ratio\"                      : 2.0, \n    \"backbone_name\"                  : \"vit_large_patch16_dinov3_qkvb.lvd1689m\",\n    \"scheduler_type\"                 : \"cosine\",\n    \"weights\": {\n        \"Dry_Green_g\" : 0.1,\n        \"Dry_Clover_g\": 0.1,\n        \"Dry_Dead_g\"  : 0.1,\n        \"GDM_g\"       : 0.2,\n        \"Dry_Total_g\" : 0.5\n    },\n    \"target_cols\": [\n        \"Dry_Green_g\",\n        \"Dry_Clover_g\",\n        \"Dry_Dead_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\"\n    ]\n}\nconfig[\"img_width\"] = int(config[\"img_size\"] * config.get(\"img_ratio\", 1.0))\n\n\nclass MySingleStreamModel(nn.Module):\n    def __init__(self, \n                backbone_name=\"convnext_tiny\", \n                pretrained=True, \n                config = None):\n        super().__init__()\n        print(\"Current backbone:\", backbone_name)\n        img_size = (config[\"img_size\"], config.get(\"img_width\", config[\"img_size\"]))\n        self.backbone = timm.create_model(\n            backbone_name,\n            pretrained=pretrained,\n            num_classes=0,\n            img_size=img_size\n        )\n        in_dim = self.backbone.num_features\n        params = list(self.backbone.parameters())\n        freeze_until = int(len(params) * config[\"freeze_ratio\"])\n        for i, p in enumerate(params):\n            p.requires_grad = i >= freeze_until\n        self.feature_dim = in_dim\n        print(\"feature dim = \", self.feature_dim)\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.feature_dim, 512),\n                nn.ReLU(),\n                nn.Dropout(0.3),\n                nn.Linear(512, 128),\n                nn.ReLU(),\n                nn.Linear(128, 1)\n            )\n        self.head_green  = make_head()\n        self.head_clover = make_head()\n        self.head_dead   = make_head()\n        self.head_gdm = make_head()\n        self.head_total = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n        self.weights = config[\"weights\"]\n\n    def forward(self, img):\n        feat = self.backbone(img)\n        G = self.softplus(self.head_green(feat))\n        C = self.softplus(self.head_clover(feat))\n        D = self.softplus(self.head_dead(feat))\n        GDM   = self.softplus(self.head_gdm(feat))\n        Total = self.softplus(self.head_total(feat))\n        preds = torch.cat([G, C, D, GDM, Total], dim=1)  # [B, 5]\n        return preds\n\n    def compute_loss(self, preds, targets):\n        preds = preds.view(-1, 5)\n        targets = targets.view(-1, 5)\n        weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5], device=preds.device)\n        y_true_flat = targets.view(-1)\n        y_pred_flat = preds.view(-1)\n        w_flat = torch.cat([\n            torch.full_like(targets[:, i], weights[i], device=preds.device)\n            for i in range(5)\n        ])\n        y_mean = torch.sum(w_flat * y_true_flat) / torch.sum(w_flat)\n        ss_res = torch.sum(w_flat * (y_true_flat - y_pred_flat) ** 2)\n        ss_tot = torch.sum(w_flat * (y_true_flat - y_mean) ** 2)\n        loss = ss_res / ss_tot\n        return loss\n\n\nclass SingleStreamDataset(Dataset):\n    def __init__(self, df, image_dir, config, pre_transform=None, transform=None, mode=\"train\"):\n        self.df = df.reset_index(drop=True)\n        self.image_dir = image_dir\n        self.target_cols = config[\"target_cols\"]\n        self.pre_transform = pre_transform\n        self.transform = transform\n        self.mode = mode\n\n    def __len__(self):\n        if self.mode == \"train\":\n            return len(self.df)\n        else:\n            return len(self.df) \n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = Path(self.image_dir, str(row[\"image_path\"]))\n        image = Image.open(img_path).convert(\"RGB\")\n        image = np.array(image)\n        if self.pre_transform:\n            image = self.pre_transform(image=image)[\"image\"]\n        \n        if self.transform:\n            image = self.transform(image=image)[\"image\"]\n\n        if self.target_cols is not None:\n            targets = torch.tensor(\n                row[self.target_cols].astype(float).values,\n                dtype=torch.float32\n            )\n            return image, targets\n        else:\n            return image\n\n\ndef get_pre_transforms():\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.OneOf([\n            A.RandomBrightnessContrast(\n                brightness_limit=(0.8, 1.2), \n                contrast_limit=(0.8, 1.2),   \n                p=0.3\n            ),\n            A.ColorJitter(p=0.7)\n        ], p=0.4),\n    ])\n\n\ndef _get_resize_dims(config):\n    height = config[\"img_size\"]\n    width = int(config.get(\"img_width\", config[\"img_size\"]))\n    return height, width\n\n\ndef get_train_transforms(config):\n    height, width = _get_resize_dims(config)\n    return A.Compose([\n        A.Resize(height, width),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_valid_transforms(config):\n    height, width = _get_resize_dims(config)\n    return A.Compose([\n        A.Resize(height, width),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_tta_transforms(config):\n    height, width = _get_resize_dims(config)\n    return {\n        \"base\": A.Compose([\n            A.Resize(height, width),\n            A.Normalize(mean=[0.485, 0.456, 0.406],\n                        std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        \"hflip\": A.Compose([\n            A.HorizontalFlip(p=1.0)\n        ])\n    }\n\n\ndef train_one_epoch(model, dataloader, optimizer, device, scaler, config):\n    model.train()\n    running_loss = []\n    start_epoch = time.time()\n    prev_end = start_epoch  \n    pbar = tqdm(enumerate(dataloader), total=len(dataloader), desc=\"Training\", leave=False)\n\n    for step, (images, targets) in pbar:\n        t_load = time.time()  \n        data_load_time = t_load - prev_end\n        t0 = time.time()\n        images = images.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last)\n        targets = targets.to(device, non_blocking=True)\n        t1 = time.time()\n        optimizer.zero_grad(set_to_none=True)\n        with torch.amp.autocast(device_type='cuda'):\n            preds = model(images)\n            loss = model.compute_loss(preds, targets)    \n        t2 = time.time()\n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        scaler.step(optimizer)\n        scaler.update()\n        t3 = time.time()\n        running_loss.append(loss.item())\n        prev_end = t3  \n    end_epoch = time.time()\n    epoch_time = end_epoch - start_epoch\n    avg_batch_time = epoch_time / len(dataloader)\n    return float(np.mean(running_loss))\n\n\n\ndef get_fold_loaders(df, config):\n    train_df = df\n    train_dataset = SingleStreamDataset(train_df, DIRS[\"dir\"],\n                                        config, pre_transform=get_pre_transforms(),\n                                        transform=get_train_transforms(config), mode=\"train\")\n    num_workers = config[\"num_workers\"]\n    prefetch_factor = config[\"prefetch_factor\"]\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=config[\"batch_size\"],\n        shuffle=True,\n        num_workers=num_workers,        \n        pin_memory=True,                 \n        prefetch_factor=prefetch_factor,  \n        persistent_workers=True         \n    )\n    return train_loader\n\n\ndef fetch_scheduler(optimizer, config):\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=config[\"epochs\"], eta_min=config[\"lr\"]/100)\n    return scheduler\n\n\ndef train_one_fold(df, save_dir, config, device):   \n    train_loader = get_fold_loaders(df, config)\n    model = MySingleStreamModel(config[\"backbone_name\"], pretrained=False, config=config)\n    model = model.to(device).to(memory_format=torch.channels_last)\n    try:\n        state_dict = torch.load('/kaggle/input/single-1209-swa-models/fold2_swa.pt', map_location=device, weights_only=True)\n    except TypeError:\n        state_dict = torch.load('/kaggle/input/single-1209-swa-models/fold2_swa.pt', map_location=device)\n    model.load_state_dict(state_dict, strict=False)\n    model = torch.compile(model)\n    \n    optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=config[\"lr\"], weight_decay=2e-5)\n    scheduler = fetch_scheduler(optimizer, config)\n    scaler = torch.amp.GradScaler(device=\"cuda\")\n    train_losses, LR_records = [], []\n    epoch_times = [] \n    all_progress = config[\"epochs\"]\n    for epoch in range(config[\"epochs\"]):\n        epoch_start = time.time()\n        train_loss = train_one_epoch(model, train_loader, optimizer, device, scaler, config)\n        scheduler.step()\n        train_losses.append(train_loss)\n        LR_records.append(scheduler.get_last_lr()[0])\n        epoch_time = time.time() - epoch_start\n        if epoch > 0:\n            epoch_times.append(epoch_time)\n            if len(epoch_times) > 10:\n                epoch_times.pop(0)\n        if len(epoch_times) > 0:\n            avg_epoch_time = np.mean(epoch_times)\n        else:\n            avg_epoch_time = epoch_time\n\n        progress = epoch + 1\n        remaining_epochs = all_progress - progress\n        eta_seconds = avg_epoch_time * remaining_epochs\n        if not np.isnan(eta_seconds) and not np.isinf(eta_seconds):\n            eta_time = datetime.now() + timedelta(seconds=float(eta_seconds))\n            eta_time = eta_time.replace(microsecond=0)\n            days_diff = (eta_time.date() - datetime.now().date()).days\n            eta_str = (\n                f\"T+{days_diff} \" + eta_time.strftime(\"%H:%M:%S\")\n                if days_diff > 0 else eta_time.strftime(\"%H:%M:%S\")\n            )\n        else:\n            eta_str = \"--:--:--\"\n        now_str = datetime.now().strftime(\"%H:%M:%S\")\n        print(\n            f\"[{now_str}]🧩[{progress/all_progress*100:6.2f}%] \"\n            f\"Epoch {epoch+1:03d}/{config['epochs']} | \"\n            f\"Train={train_loss:.4f} | \"\n            f\"LR={scheduler.get_last_lr()[0]:.6f} | \"\n            f\"{epoch_time:6.2f}s/it | \"\n            f\"ETA≈{eta_str}\"\n        )\n        if epoch > 7:\n            loss_model_path = save_dir / f\"model_epoch{epoch+1}.pt\"\n            torch.save(model._orig_mod.state_dict(), loss_model_path)\n            # torch.save(model.state_dict(), loss_model_path)\n            print(f\"🌟 save Epoch {epoch+1} 模型！\")\n    del train_loader, model, optimizer, scheduler, scaler\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    return train_losses\n\n\ndef train_with_groupkfold(df_train, save_dir, config, device):\n    df = df_train.copy()\n    metrics = train_one_fold(df, save_dir, config, device)\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    print(\"🎯 All folds training completed.\")\n\n\nisTRAIN     = True\nisPREDICT   = True\nDEBUG = False\nprint(\"DEBUG模式\",DEBUG)\n\n\nif (\n    __name__ == \"__main__\"\n    and isTRAIN\n):\n    torch.multiprocessing.freeze_support()       \n    setup_seed(config[\"seed\"])        \n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    model_root = DIRS[\"save\"]\n    os.makedirs(model_root, exist_ok=True)\n    if DEBUG:\n        config[\"epochs\"] = 2\n        print(f\"⚠️ DEBUG mode enabled — running only {config['epochs']} epochs\\n\")\n\n\n    print(\"\\n🚀🚀🚀 Starting training process... 🚀🚀🚀\\n\")\n    df_train = df_stage2_train\n    train_with_groupkfold(\n        df_train             = df_train,\n        save_dir             = model_root,\n        config               = config,\n        device               = device\n    )\n    print(f\"\\n✅ Training completed! Results saved in: {model_root}\")\n    print(\"✅\"*65)\n\n    model_dir = DIRS[\"save\"]\n    out_dir = DIRS[\"model\"]\n    model_paths = sorted(Path(model_dir).glob(\"*.pt\"))\n    print(f\": Found {len(model_paths)} unique models!\")\n    for path in model_paths:\n        print(f\" - {path}\")\n    if len(model_paths) < 2:\n        print(f\"⚠️ Only {len(model_paths)} unique models, skipping SWA...\")\n    \n    models = []\n    for module_path in model_paths:\n        if os.path.exists(module_path):\n            model = torch.load(module_path, map_location='cpu')\n            models.append(model)\n        else:\n            print(f\"❌ Model file not found: {module_path}\")\n    \n    worker_state_dicts = [m for m in models]\n    weight_keys = list(worker_state_dicts[0].keys())\n    print(f\"Example weight keys: {list(weight_keys)[:5]}\")\n    fed_state_dict = collections.OrderedDict()\n    for key in weight_keys:\n        key_sum = 0\n        for i in range(len(models)):\n            key_sum += worker_state_dicts[i][key]\n        fed_state_dict[key] = key_sum / len(models)\n    os.makedirs(out_dir, exist_ok=True)\n    output_path = os.path.join(out_dir, f'swa.pt')\n    torch.save(fed_state_dict, output_path)\n    print(f\"averaging complete. Saved to: {output_path}\")\n\n\n\ndef load_and_prepare_test_df():\n    df_file_path = Path(DIRS[\"dir\"]) / \"test.csv\"\n    df = pd.read_csv(df_file_path)\n\n    df[\"ID\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df = move_column_first(df, \"ID\")\n    df[\"target\"] = 0  \n    df_targets = (\n        df.pivot_table(index=\"ID\", columns=\"target_name\", values=\"target\", aggfunc=\"first\")\n        .reset_index()\n    )\n    df_targets.columns.name = None\n    df_meta = df[[\"ID\", \"image_path\"]].drop_duplicates(subset=\"ID\")\n    df_test = pd.merge(df_meta, df_targets, on=\"ID\", how=\"left\")\n    show_df_info(df_test, \"df_test\")\n    print(f\"✅ Test set loaded: {df_test.shape}\")\n    return df_test\n\n\ndef load_model_for_inference(model_path, device, config):\n    model = MySingleStreamModel(config[\"backbone_name\"], pretrained=False, config=config)\n    model = model.to(device).to(memory_format=torch.channels_last)\n    try:\n        state_dict = torch.load(model_path, map_location=device, weights_only=True)\n    except TypeError:\n        state_dict = torch.load(model_path, map_location=device)\n    model.load_state_dict(state_dict, strict=False)\n    model.eval()\n    return model\n\n\ndef predict_with_model(model, dataloader, device):\n    preds_list = []\n    model.eval()\n    with torch.no_grad():\n        for step, (images, targets) in enumerate(dataloader):\n            images = images.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last)\n            targets = targets.to(device, non_blocking=True)\n            with torch.amp.autocast(device_type='cuda'):\n                preds = model(images)\n            preds_list.append(preds.cpu().numpy())\n    return np.concatenate(preds_list, axis=0)\n\n\ndef predict_ensemble(df_test, pre_transform, transform, model_dir, device, config):\n    model_paths = sorted(Path(model_dir).glob(\"*.pt\"))\n    assert len(model_paths) > 0, f\"❌ No model files found in: {model_dir}\"\n    print(f\"✅ Detected {len(model_paths)} 个模型:\")\n    for p in model_paths:\n        print(\"   -\", p.name)\n    test_dataset = SingleStreamDataset(\n        df_test,\n        DIRS[\"dir\"],\n        config,\n        pre_transform=pre_transform,\n        transform=transform,\n        mode=\"test\"\n    )\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=config[\"batch_size\"],\n        shuffle=False,\n        num_workers=2,\n        pin_memory=True,\n    )\n    fold_preds = []\n    for fold, model_path in enumerate(model_paths):\n        print(f\"Fold {fold+1}/{len(model_paths)} 推理: {model_path.name}\")\n        model = load_model_for_inference(model_path, device, config)\n        fold_pred = predict_with_model(model, test_loader, device)\n        fold_preds.append(fold_pred)\n    preds_mean = np.mean(fold_preds, axis=0)\n    df_pred = pd.DataFrame(preds_mean, columns=config[\"target_cols\"])\n    df_pred[\"ID\"] = df_test[\"ID\"]\n    df_pred = df_pred[[\"ID\"] + config[\"target_cols\"]]\n    show_df_info(df_pred, \"df_pred\")\n    return df_pred\n\n\ndef generate_submission(df_pred_final):\n    ordered_target_cols = [\n        \"Dry_Green_g\",  # 1️⃣\n        \"Dry_Clover_g\", # 2️⃣\n        \"Dry_Dead_g\",   # 3️⃣\n        \"GDM_g\",        # 4️⃣\n        \"Dry_Total_g\"   # 5️⃣\n    ]\n    # if \"Dry_Clover_g\" in df_pred_final.columns:\n    #     df_pred_final[\"Dry_Clover_g\"] = df_pred_final[\"Dry_Clover_g\"] * 0.8\n    # else:\n    #     print(\"   ⚠️ Warning: Column Dry_Clover_g not found in DataFrame\")\n\n    df_submit = (\n        df_pred_final.melt(\n            id_vars=\"ID\",\n            value_vars=ordered_target_cols,\n            var_name=\"target_name\",\n            value_name=\"target\"\n        )\n    )\n\n    df_submit[\"sample_id\"] = df_submit[\"ID\"] + \"__\" + df_submit[\"target_name\"]\n    df_submit = df_submit[[\"sample_id\", \"target\"]]\n    df_submit = df_submit.sort_values(\"sample_id\").reset_index(drop=True)\n    df_submit.to_csv(\"submission_stage2_1.csv\", index=False)\n    print(\"\\n✅ Submission file generated: submission_stage2_1.csv\")\n\n\ndef run_tta_prediction(df_test, model_dir, device, config):\n    tta_transforms = get_tta_transforms(config)\n    tta_names = list(tta_transforms.keys())\n    print(f\"\\n✅ Detected {len(tta_names)} TTA modes: {tta_names}\\n\")\n    all_preds = []\n    for name, _ in tta_transforms.items():\n        print(f\"\\n🚀 TTA mode: {name}\")\n        if name == \"base\":\n            print(\"base no pre_transform\")\n            pre_transform = None\n            transform = tta_transforms[name]\n        else:\n            pre_transform = tta_transforms[name]\n            transform = tta_transforms[\"base\"]\n        df_pred = predict_ensemble(df_test, pre_transform, transform, model_dir, device, config)\n        all_preds.append(df_pred[config[\"target_cols\"]].values)\n\n    mean_preds = np.mean(all_preds, axis=0)\n    df_pred_final = df_pred.copy()\n    df_pred_final[config[\"target_cols\"]] = mean_preds\n    return df_pred_final\n\n\nif __name__ == \"__main__\" and isPREDICT:\n    print(\"\\n🧠 Starting prediction pipeline...\")\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n    df_test = load_and_prepare_test_df()\n    print(f\"\\nCurrent dataset shape: {df_test.shape}\\n\")\n    model_dir = DIRS[\"model\"]\n\n    print(f\"Model directory loaded: {model_dir}\")\n\n    df_pred_final = run_tta_prediction(df_test, model_dir, device, config)\n    generate_submission(df_pred_final)\n    print(\"🎯 Prediction pipeline completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:07.702894Z","iopub.execute_input":"2026-01-26T13:38:07.703105Z","iopub.status.idle":"2026-01-26T13:38:07.717544Z","shell.execute_reply.started":"2026-01-26T13:38:07.703089Z","shell.execute_reply":"2026-01-26T13:38:07.716845Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_dual.py\n\n# 📦\nimport os\nimport gc\nimport cv2\nimport json\nimport time\nimport shutil\nimport subprocess\nfrom pathlib import Path\nfrom datetime import datetime, timedelta\nimport math\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport random\nfrom tqdm import tqdm\nfrom sklearn.model_selection import KFold, GroupKFold\nfrom sklearn.metrics import r2_score\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.optim.lr_scheduler import _LRScheduler\nfrom torch.amp import autocast, GradScaler       \nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom PIL import Image\nimport timm\nfrom torchvision import models, transforms\nfrom torchvision.models import get_model_weights\nimport collections\n\n\ndir = Path('/kaggle/input/csiro-biomass')\nDIRS = {\n    \"dir\"  : dir,\n    \"train\": Path(dir, \"train\"),\n    \"test\" : Path(dir, \"test\"),\n    \"model\": Path('/kaggle/working/stage2_train_dual_swa'),\n    \"save\" : Path('/kaggle/working/stage2_train_dual'),\n    \"data\" : Path(\"/kaggle/working/\"),\n}\n\n\ndef show_df_info(df, name: str):\n    print(f\"📊 {name:<16} shape: {str(df.shape):<16}  列名: {df.columns.tolist()}\")\n\n\ndef move_column_first(df, col_name):\n    if col_name not in df.columns:\n        raise ValueError(f\"列 '{col_name}' 不存在于 DataFrame 中。\")\n    cols = [col_name] + [c for c in df.columns if c != col_name]\n    return df[cols]\n\n\ndef setup_seed(seed, deterministic=True):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\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 = deterministic\n    torch.backends.cudnn.benchmark = False\n\n\ndef load_and_prepare_train_stage2_df():\n    df_file_path = 'submission_stage1.csv'\n    df = pd.read_csv(df_file_path)\n    df[\"ID\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df[\"image_path\"] = df[\"ID\"].apply(lambda x: f\"test/{x}.jpg\")\n    df[\"target_name\"] = df[\"sample_id\"].str.split(\"__\").str[1]\n    df = move_column_first(df, \"ID\")\n\n    df_targets = (\n        df.pivot_table(\n            index=\"ID\",\n            columns=\"target_name\",\n            values=\"target\",\n            aggfunc=\"first\"\n        )\n        .reset_index()\n    )\n    df_targets.columns.name = None  \n    meta_cols = [\n        \"ID\", \"image_path\"\n    ]\n    df_meta = df[meta_cols].drop_duplicates(subset=\"ID\")\n    df_train = pd.merge(df_meta, df_targets, on=\"ID\", how=\"left\")\n    show_df_info(df_train, \"df_train\")\n    return df_train\n\n\ndf_pseudo = load_and_prepare_train_stage2_df()\ndf_stage1_path = '/kaggle/input/train-single-csv/train_with_folds_singleflow.csv'\ndf_stage1 = pd.read_csv(df_stage1_path)\ndf_stage2_train = pd.concat([df_pseudo, df_stage1], ignore_index=True)\ndf_stage2_train.reset_index(drop=True, inplace=True)\n\n\nconfig = {\n    \"seed\"                           : 3407,  \n    \"epochs\"                         : 12,\n    \"freeze_ratio\"                   : 0.8,\n    \"num_workers\"                    : 2,\n    \"prefetch_factor\"                : 2,\n    \"batch_size\"                     : 4,\n    \"lr\"                             : 1e-4,\n    \"img_size\"                       : 1024,\n    \"backbone_name\"                  : \"vit_large_patch16_dinov3_qkvb.lvd1689m\",\n    \"scheduler_type\"                 : \"cosine\",\n    \"weights\": {\n        \"Dry_Green_g\" : 0.1,\n        \"Dry_Clover_g\": 0.1,\n        \"Dry_Dead_g\"  : 0.1,\n        \"GDM_g\"       : 0.2,\n        \"Dry_Total_g\" : 0.5\n    },\n    \"target_cols\": [\n        \"Dry_Green_g\",\n        \"Dry_Clover_g\",\n        \"Dry_Dead_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\"\n    ]\n}\n\n\n# Model \nclass MyDualStreamModel(nn.Module):\n    def __init__(self, \n                backbone_name=\"convnext_tiny\", \n                pretrained=True, \n                config = None):\n        super().__init__()\n        print(\"Current backbone:\", backbone_name)\n        self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0)\n        in_dim = self.backbone.num_features\n        params = list(self.backbone.parameters())\n        freeze_until = int(len(params) * config[\"freeze_ratio\"])\n        for i, p in enumerate(params):\n            p.requires_grad = i >= freeze_until    \n        self.fusion_dim = in_dim * 2\n        print(\"feature fusion dim = \", self.fusion_dim)\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.fusion_dim, 512),\n                nn.ReLU(),\n                nn.Dropout(0.3),\n                nn.Linear(512, 128),\n                nn.ReLU(),\n                nn.Linear(128, 1)\n            )\n\n        self.head_green  = make_head()  \n        self.head_clover = make_head()   \n        self.head_dead   = make_head()  \n        self.head_gdm = make_head()\n        self.head_total = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n        self.weights = config[\"weights\"]\n    def forward(self, img_left, img_right):\n        feat_left  = self.backbone(img_left)\n        feat_right = self.backbone(img_right)\n        fused = torch.cat([feat_left, feat_right], dim=1)\n        G = self.softplus(self.head_green(fused))\n        C = self.softplus(self.head_clover(fused))\n        D = self.softplus(self.head_dead(fused))\n        if self.training:\n            GDM   = self.softplus(self.head_gdm(fused))\n            Total = self.softplus(self.head_total(fused))\n        else:\n            GDM   = G + C\n            Total = G + C + D\n        preds = torch.cat([G, C, D, GDM, Total], dim=1)  # [B, 5]\n        return preds\n\n    def compute_loss(self, preds, targets):\n        preds = preds.view(-1, 5)\n        targets = targets.view(-1, 5)\n        weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5], device=preds.device)\n        y_true_flat = targets.view(-1)\n        y_pred_flat = preds.view(-1)\n        w_flat = torch.cat([\n            torch.full_like(targets[:, i], weights[i], device=preds.device)\n            for i in range(5)\n        ])\n        y_mean = torch.sum(w_flat * y_true_flat) / torch.sum(w_flat)\n        ss_res = torch.sum(w_flat * (y_true_flat - y_pred_flat) ** 2)\n        ss_tot = torch.sum(w_flat * (y_true_flat - y_mean) ** 2)\n        loss = ss_res / ss_tot\n        return loss\n\n\nclass DualStreamDataset(Dataset):\n    def __init__(self, df, image_dir, config, pre_transform=None, transform=None, mode=\"train\"):\n        self.df = df.reset_index(drop=True)\n        self.image_dir = image_dir\n        self.target_cols = config[\"target_cols\"]\n        self.pre_transform = pre_transform\n        self.transform = transform\n        self.mode = mode\n\n    def __len__(self):\n        if self.mode == \"train\":\n            return len(self.df)\n        else:\n            return len(self.df) \n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = Path(self.image_dir, str(row[\"image_path\"]))\n        image = Image.open(img_path).convert(\"RGB\")\n        image = np.array(image)\n        if self.pre_transform:\n            image = self.pre_transform(image=image)[\"image\"]\n        \n        h, w, _ = image.shape\n        mid = w // 2\n        img_left = image[:, :mid]\n        img_right = image[:, mid:]\n        if self.transform:\n            img_left = self.transform(image=img_left)[\"image\"]\n            img_right = self.transform(image=img_right)[\"image\"]\n        if self.target_cols is not None:\n            targets = torch.tensor(\n                row[self.target_cols].astype(float).values,\n                dtype=torch.float32\n            )\n            return img_left, img_right, targets\n        else:\n            return img_left, img_right\n\n\ndef get_pre_transforms():\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.OneOf([\n            A.RandomBrightnessContrast(\n                brightness_limit=(0.8, 1.2), \n                contrast_limit=(0.8, 1.2),   \n                p=0.3\n            ),\n            A.ColorJitter(p=0.7)\n        ], p=0.4),\n    ])\n\n\ndef get_train_transforms(size):\n    return A.Compose([\n        A.Resize(size, size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_valid_transforms(size):\n    return A.Compose([\n        A.Resize(size, size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n\ndef get_tta_transforms(size):\n    return {\n        \"base\": A.Compose([\n            A.Resize(size, size),\n            A.Normalize(mean=[0.485, 0.456, 0.406],\n                        std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        \"hflip\": A.Compose([\n            A.HorizontalFlip(p=1.0)\n        ])\n    }\n\n\ndef train_one_epoch(model, dataloader, optimizer, device, scaler, config):\n    model.train()\n    running_loss = []\n    start_epoch = time.time()\n    prev_end = start_epoch  \n    pbar = tqdm(enumerate(dataloader), total=len(dataloader), desc=\"Training\", leave=False)\n    for step, (img_left, img_right, targets) in enumerate(dataloader):\n        t_load = time.time()  \n        data_load_time = t_load - prev_end\n        t0 = time.time()\n        img_left, img_right, targets = (\n            img_left.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last),\n            img_right.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last),\n            targets.to(device, non_blocking=True),\n        )\n        t1 = time.time()\n        optimizer.zero_grad(set_to_none=True)\n        with torch.amp.autocast(device_type='cuda'):\n            preds = model(img_left, img_right)\n            loss = model.compute_loss(preds, targets)    \n        t2 = time.time()\n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        scaler.step(optimizer)\n        scaler.update()\n        t3 = time.time()\n        running_loss.append(loss.item())\n        prev_end = t3  \n    end_epoch = time.time()\n    epoch_time = end_epoch - start_epoch\n    avg_batch_time = epoch_time / len(dataloader)\n    return float(np.mean(running_loss))\n\n\n\ndef get_fold_loaders(df, config):\n    train_df = df\n    train_dataset = DualStreamDataset(train_df, DIRS[\"dir\"],\n                                        config, pre_transform=get_pre_transforms(),\n                                        transform=get_train_transforms(config[\"img_size\"]), mode=\"train\")\n    num_workers = config[\"num_workers\"]\n    prefetch_factor = config[\"prefetch_factor\"]\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=config[\"batch_size\"],\n        shuffle=True,\n        num_workers=num_workers,        \n        pin_memory=True,                 \n        prefetch_factor=prefetch_factor,  \n        persistent_workers=True         \n    )\n    return train_loader\n\n\ndef fetch_scheduler(optimizer, config):\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=config[\"epochs\"], eta_min=config[\"lr\"]/100)\n    return scheduler\n\n\ndef train_one_fold(df, save_dir, config, device):   \n    train_loader = get_fold_loaders(df, config)\n    model = MyDualStreamModel(config[\"backbone_name\"], pretrained=False, config=config)\n    model = model.to(device).to(memory_format=torch.channels_last)\n    try:\n        state_dict = torch.load('/kaggle/input/dinvov3large-1204-cv/fold2_swa.pt', map_location=device, weights_only=True)\n    except TypeError:\n        state_dict = torch.load('/kaggle/input/dinvov3large-1204-cv/fold2_swa.pt', map_location=device)\n    model.load_state_dict(state_dict, strict=False)\n    model = torch.compile(model)\n    \n    optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=config[\"lr\"], weight_decay=2e-5)\n    scheduler = fetch_scheduler(optimizer, config)\n    scaler = torch.amp.GradScaler(device=\"cuda\")\n    train_losses, LR_records = [], []\n    epoch_times = [] \n    all_progress = config[\"epochs\"]\n    for epoch in range(config[\"epochs\"]):\n        epoch_start = time.time()\n        train_loss = train_one_epoch(model, train_loader, optimizer, device, scaler, config)\n        scheduler.step()\n        train_losses.append(train_loss)\n        LR_records.append(scheduler.get_last_lr()[0])\n        epoch_time = time.time() - epoch_start\n        if epoch > 0:\n            epoch_times.append(epoch_time)\n            if len(epoch_times) > 10:\n                epoch_times.pop(0)\n        if len(epoch_times) > 0:\n            avg_epoch_time = np.mean(epoch_times)\n        else:\n            avg_epoch_time = epoch_time\n\n        progress = epoch + 1\n        remaining_epochs = all_progress - progress\n        eta_seconds = avg_epoch_time * remaining_epochs\n        if not np.isnan(eta_seconds) and not np.isinf(eta_seconds):\n            eta_time = datetime.now() + timedelta(seconds=float(eta_seconds))\n            eta_time = eta_time.replace(microsecond=0)\n            days_diff = (eta_time.date() - datetime.now().date()).days\n            eta_str = (\n                f\"T+{days_diff} \" + eta_time.strftime(\"%H:%M:%S\")\n                if days_diff > 0 else eta_time.strftime(\"%H:%M:%S\")\n            )\n        else:\n            eta_str = \"--:--:--\"\n        now_str = datetime.now().strftime(\"%H:%M:%S\")\n        print(\n            f\"[{now_str}]🧩[{progress/all_progress*100:6.2f}%] \"\n            f\"Epoch {epoch+1:03d}/{config['epochs']} | \"\n            f\"Train={train_loss:.4f} | \"\n            f\"LR={scheduler.get_last_lr()[0]:.6f} | \"\n            f\"{epoch_time:6.2f}s/it | \"\n            f\"ETA≈{eta_str}\"\n        )\n        if epoch > 7:\n            loss_model_path = save_dir / f\"model_epoch{epoch+1}.pt\"\n            torch.save(model._orig_mod.state_dict(), loss_model_path)\n            # torch.save(model.state_dict(), loss_model_path)\n            print(f\"🌟 save Epoch {epoch+1} 模型！\")\n    del train_loader, model, optimizer, scheduler, scaler\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    return train_losses\n\n\ndef train_with_groupkfold(df_train, save_dir, config, device):\n    df = df_train.copy()\n    metrics = train_one_fold(df, save_dir, config, device)\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    print(\"🎯 All folds training completed.\")\n\n\nisTRAIN     = True\nisPREDICT   = True\nDEBUG = False\nprint(\"DEBUG模式\",DEBUG)\n\n\nif (\n    __name__ == \"__main__\"\n    and isTRAIN\n):\n    torch.multiprocessing.freeze_support()       \n    setup_seed(config[\"seed\"])        \n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    model_root = DIRS[\"save\"]\n    os.makedirs(model_root, exist_ok=True)\n    if DEBUG:\n        config[\"epochs\"] = 2\n        print(f\"⚠️ DEBUG mode enabled — running only {config['epochs']} epochs\\n\")\n\n\n    print(\"\\n🚀🚀🚀 Starting training process... 🚀🚀🚀\\n\")\n    df_train = df_stage2_train\n    train_with_groupkfold(\n        df_train             = df_train,\n        save_dir             = model_root,\n        config               = config,\n        device               = device\n    )\n    print(f\"\\n✅ Training completed! Results saved in: {model_root}\")\n    print(\"✅\"*65)\n\n    model_dir = DIRS[\"save\"]\n    out_dir = DIRS[\"model\"]\n    model_paths = sorted(Path(model_dir).glob(\"*.pt\"))\n    print(f\": Found {len(model_paths)} unique models!\")\n    for path in model_paths:\n        print(f\" - {path}\")\n    if len(model_paths) < 2:\n        print(f\"⚠️ Only {len(model_paths)} unique models, skipping SWA...\")\n    \n    models = []\n    for module_path in model_paths:\n        if os.path.exists(module_path):\n            model = torch.load(module_path, map_location='cpu')\n            models.append(model)\n        else:\n            print(f\"❌ Model file not found: {module_path}\")\n    \n    worker_state_dicts = [m for m in models]\n    weight_keys = list(worker_state_dicts[0].keys())\n    print(f\"Example weight keys: {list(weight_keys)[:5]}\")\n    fed_state_dict = collections.OrderedDict()\n    for key in weight_keys:\n        key_sum = 0\n        for i in range(len(models)):\n            key_sum += worker_state_dicts[i][key]\n        fed_state_dict[key] = key_sum / len(models)\n    os.makedirs(out_dir, exist_ok=True)\n    output_path = os.path.join(out_dir, f'swa.pt')\n    torch.save(fed_state_dict, output_path)\n    print(f\"averaging complete. Saved to: {output_path}\")\n\n\n\ndef load_and_prepare_test_df():\n    df_file_path = Path(DIRS[\"dir\"]) / \"test.csv\"\n    df = pd.read_csv(df_file_path)\n\n    df[\"ID\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df = move_column_first(df, \"ID\")\n    df[\"target\"] = 0  \n    df_targets = (\n        df.pivot_table(index=\"ID\", columns=\"target_name\", values=\"target\", aggfunc=\"first\")\n        .reset_index()\n    )\n    df_targets.columns.name = None\n    df_meta = df[[\"ID\", \"image_path\"]].drop_duplicates(subset=\"ID\")\n    df_test = pd.merge(df_meta, df_targets, on=\"ID\", how=\"left\")\n    show_df_info(df_test, \"df_test\")\n    print(f\"✅ Test set loaded: {df_test.shape}\")\n    return df_test\n\n\ndef load_model_for_inference(model_path, device, config):\n    model = MyDualStreamModel(config[\"backbone_name\"], pretrained=False, config=config)\n    model = model.to(device).to(memory_format=torch.channels_last)\n    try:\n        state_dict = torch.load(model_path, map_location=device, weights_only=True)\n    except TypeError:\n        state_dict = torch.load(model_path, map_location=device)\n    model.load_state_dict(state_dict, strict=False)\n    model.eval()\n    return model\n\n\ndef predict_with_model(model, dataloader, device):\n    preds_list = []\n    model.eval()\n    with torch.no_grad():\n        for step, (img_left, img_right, targets) in enumerate(dataloader):\n            img_left   = img_left.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last)\n            img_right  = img_right.to(device, non_blocking=True).contiguous(memory_format=torch.channels_last)\n            targets    = targets.to(device, non_blocking=True)\n            with torch.amp.autocast(device_type='cuda'):\n                preds = model(img_left, img_right)\n            preds_list.append(preds.cpu().numpy())\n    return np.concatenate(preds_list, axis=0)\n\n\ndef predict_ensemble(df_test, pre_transform, transform, model_dir, device, config):\n    model_paths = sorted(Path(model_dir).glob(\"*.pt\"))\n    assert len(model_paths) > 0, f\"❌ No model files found in: {model_dir}\"\n    print(f\"✅ Detected {len(model_paths)} 个模型:\")\n    for p in model_paths:\n        print(\"   -\", p.name)\n    test_dataset = DualStreamDataset(\n        df_test,\n        DIRS[\"dir\"],\n        config,\n        pre_transform=pre_transform,\n        transform=transform,\n        mode=\"test\"\n    )\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=config[\"batch_size\"],\n        shuffle=False,\n        num_workers=2,\n        pin_memory=True,\n    )\n    fold_preds = []\n    for fold, model_path in enumerate(model_paths):\n        print(f\"Fold {fold+1}/{len(model_paths)} 推理: {model_path.name}\")\n        model = load_model_for_inference(model_path, device, config)\n        fold_pred = predict_with_model(model, test_loader, device)\n        fold_preds.append(fold_pred)\n    preds_mean = np.mean(fold_preds, axis=0)\n    df_pred = pd.DataFrame(preds_mean, columns=config[\"target_cols\"])\n    df_pred[\"ID\"] = df_test[\"ID\"]\n    df_pred = df_pred[[\"ID\"] + config[\"target_cols\"]]\n    show_df_info(df_pred, \"df_pred\")\n    return df_pred\n\n\ndef generate_submission(df_pred_final):\n    ordered_target_cols = [\n        \"Dry_Green_g\",  \n        \"Dry_Clover_g\",\n        \"Dry_Dead_g\",\n        \"GDM_g\",       \n        \"Dry_Total_g\"\n    ]\n    # if \"Dry_Clover_g\" in df_pred_final.columns:\n    #     df_pred_final[\"Dry_Clover_g\"] = df_pred_final[\"Dry_Clover_g\"] * 0.8\n    # else:\n    #     print(\"   ⚠️ Warning: Column Dry_Clover_g not found in DataFrame\")\n\n    df_submit = (\n        df_pred_final.melt(\n            id_vars=\"ID\",\n            value_vars=ordered_target_cols,\n            var_name=\"target_name\",\n            value_name=\"target\"\n        )\n    )\n\n    df_submit[\"sample_id\"] = df_submit[\"ID\"] + \"__\" + df_submit[\"target_name\"]\n    df_submit = df_submit[[\"sample_id\", \"target\"]]\n    df_submit = df_submit.sort_values(\"sample_id\").reset_index(drop=True)\n    df_submit.to_csv(\"submission_stage2_2.csv\", index=False)\n    print(\"\\n✅ Submission file generated: submission_stage2_2.csv\")\n\n\ndef run_tta_prediction(df_test, model_dir, device, config):\n    tta_transforms = get_tta_transforms(config[\"img_size\"])\n    tta_names = list(tta_transforms.keys())\n    print(f\"\\n✅ Detected {len(tta_names)} TTA modes: {tta_names}\\n\")\n    all_preds = []\n    for name, _ in tta_transforms.items():\n        print(f\"\\n🚀 TTA mode: {name}\")\n        if name == \"base\":\n            print(\"base no pre_transform\")\n            pre_transform = None\n            transform = tta_transforms[name]\n        else:\n            pre_transform = tta_transforms[name]\n            transform = tta_transforms[\"base\"]\n        df_pred = predict_ensemble(df_test, pre_transform, transform, model_dir, device, config)\n        all_preds.append(df_pred[config[\"target_cols\"]].values)\n\n    mean_preds = np.mean(all_preds, axis=0)\n    df_pred_final = df_pred.copy()\n    df_pred_final[config[\"target_cols\"]] = mean_preds\n    return df_pred_final\n\n\nif __name__ == \"__main__\" and isPREDICT:\n    print(\"\\n🧠 Starting prediction pipeline...\")\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n    df_test = load_and_prepare_test_df()\n    print(f\"\\nCurrent dataset shape: {df_test.shape}\\n\")\n    model_dir = DIRS[\"model\"]\n\n    print(f\"Model directory loaded: {model_dir}\")\n\n    df_pred_final = run_tta_prediction(df_test, model_dir, device, config)\n    generate_submission(df_pred_final)\n    print(\"🎯 Prediction pipeline completed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:07.719882Z","iopub.execute_input":"2026-01-26T13:38:07.720112Z","iopub.status.idle":"2026-01-26T13:38:07.744541Z","shell.execute_reply.started":"2026-01-26T13:38:07.720094Z","shell.execute_reply":"2026-01-26T13:38:07.743756Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\nCUDA_VISIBLE_DEVICES=0 python train_single.py > log_single_train.txt 2>&1 &\nCUDA_VISIBLE_DEVICES=1 python train_dual.py > log_dual_train.txt 2>&1 &\necho \"GPU 0: Single Stream\"\necho \"GPU 1: Dual Stream\"\n\nwait\n\necho \"single&&dual done！\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:38:07.745246Z","iopub.execute_input":"2026-01-26T13:38:07.745414Z","iopub.status.idle":"2026-01-26T13:52:32.859081Z","shell.execute_reply.started":"2026-01-26T13:38:07.745399Z","shell.execute_reply":"2026-01-26T13:52:32.858271Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!tail -n 1000 log_single_train.txt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:52:32.860191Z","iopub.execute_input":"2026-01-26T13:52:32.860741Z","iopub.status.idle":"2026-01-26T13:52:33.037966Z","shell.execute_reply.started":"2026-01-26T13:52:32.860721Z","shell.execute_reply":"2026-01-26T13:52:33.036965Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!tail -n 1000 log_dual_train.txt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:52:33.039323Z","iopub.execute_input":"2026-01-26T13:52:33.039654Z","iopub.status.idle":"2026-01-26T13:52:33.169991Z","shell.execute_reply.started":"2026-01-26T13:52:33.039618Z","shell.execute_reply":"2026-01-26T13:52:33.169053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\ndf1 = pd.read_csv('submission_stage2_1.csv')\ndf2 = pd.read_csv('submission_stage2_2.csv')\n\ndf1 = df1.sort_values('sample_id').reset_index(drop=True)\ndf2 = df2.sort_values('sample_id').reset_index(drop=True)\n\nif not df1['sample_id'].equals(df2['sample_id']):\n    print(\"警告：两个文件的sample_id不完全一致！\")\n    print(\"将按sample_id对齐...\")\n    df_merged = pd.merge(df1, df2, on='sample_id', suffixes=('_1', '_2'))\nelse:\n    df_merged = df1.copy()\n    df_merged['target_1'] = df1['target']\n    df_merged['target_2'] = df2['target']\n\n\ndf_merged['target'] = 0.6 * df_merged['target_1'] + 0.4 * df_merged['target_2']\ndf_merged['target'] = df_merged['target'].apply(lambda x: max(x, 0) if x >= 0.1 else 0)\ndf_merged['target'] = df_merged['target'].clip(lower=0)\nresult_df = df_merged[['sample_id', 'target']].copy()\nresult_df.to_csv('submission_stage2.csv', index=False)\nprint(f\"\\n✅ 提交文件已保存: submission_stage2.csv\")\nprint(result_df.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:52:33.171287Z","iopub.execute_input":"2026-01-26T13:52:33.171953Z","iopub.status.idle":"2026-01-26T13:52:33.196719Z","shell.execute_reply.started":"2026-01-26T13:52:33.171915Z","shell.execute_reply":"2026-01-26T13:52:33.195749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\ndf1 = pd.read_csv('submission_stage1.csv')\ndf2 = pd.read_csv('submission_stage2.csv')\n\ndf1 = df1.sort_values('sample_id').reset_index(drop=True)\ndf2 = df2.sort_values('sample_id').reset_index(drop=True)\n\nif not df1['sample_id'].equals(df2['sample_id']):\n    print(\"警告：两个文件的sample_id不完全一致！\")\n    print(\"将按sample_id对齐...\")\n    df_merged = pd.merge(df1, df2, on='sample_id', suffixes=('_1', '_2'))\nelse:\n    df_merged = df1.copy()\n    df_merged['target_1'] = df1['target']\n    df_merged['target_2'] = df2['target']\n\n\ndf_merged['target'] = 0.2 * df_merged['target_1'] + 0.8 * df_merged['target_2']\ndf_merged['target'] = df_merged['target'].apply(lambda x: max(x, 0) if x >= 0.2 else 0)\ndf_merged['target'] = df_merged['target'].clip(lower=0)\nresult_df = df_merged[['sample_id', 'target']].copy()\n\nnegative_count = (result_df['target'] < 0).sum()\nbelow_01_count = ((df_merged['target_1'] < 0.2) | (df_merged['target_2'] < 0.2)).sum()\nclipped_count = (result_df['target'] == 0).sum()\n\nprint(f\"处理后统计:\")\nprint(f\"  负值数量: {negative_count}\")\nprint(f\"  原始值低于0.1的数量: {below_01_count}\")\nprint(f\"  被置为0的数量: {clipped_count}\")\nprint(f\"  最终值范围: [{result_df['target'].min():.3f}, {result_df['target'].max():.3f}]\")\n\nresult_df.to_csv('submission.csv', index=False)\nprint(f\"\\n✅ 提交文件已保存: submission.csv\")\n\nprint(result_df.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T13:52:33.197673Z","iopub.execute_input":"2026-01-26T13:52:33.198171Z","iopub.status.idle":"2026-01-26T13:52:33.216026Z","shell.execute_reply.started":"2026-01-26T13:52:33.198153Z","shell.execute_reply":"2026-01-26T13:52:33.215179Z"}},"outputs":[],"execution_count":null}]}