{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14258368,"sourceType":"datasetVersion","datasetId":9098068}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.571635Z","iopub.execute_input":"2025-12-24T13:04:33.571983Z","iopub.status.idle":"2025-12-24T13:04:33.590148Z","shell.execute_reply.started":"2025-12-24T13:04:33.571955Z","shell.execute_reply":"2025-12-24T13:04:33.589049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR = \"/kaggle/input/csiro-biomass\"\n\nTRAIN_CSV = f\"{DATA_DIR}/train.csv\"\nTEST_CSV  = f\"{DATA_DIR}/test.csv\"\n\nTRAIN_IMG_DIR = f\"{DATA_DIR}\"\nTEST_IMG_DIR  = f\"{DATA_DIR}\"\n\ntrain_df_1 = pd.read_csv(TRAIN_CSV)\ntest_df  = pd.read_csv(TEST_CSV)\n\nprint(\"Raw train rows:\", len(train_df_1))\nprint(\"Raw test rows :\", len(test_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.591917Z","iopub.execute_input":"2025-12-24T13:04:33.592292Z","iopub.status.idle":"2025-12-24T13:04:33.610081Z","shell.execute_reply.started":"2025-12-24T13:04:33.592262Z","shell.execute_reply":"2025-12-24T13:04:33.609047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"meta_cols = [\n    c for c in train_df_1.columns\n    if c not in [\"target_name\", \"target\"]\n]\n\nmeta_df = (\n    train_df_1[meta_cols]\n    .drop_duplicates(subset=[\"image_path\"])\n    .reset_index(drop=True)\n)\n\nprint(\"Metadata rows:\", len(meta_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.611748Z","iopub.execute_input":"2025-12-24T13:04:33.612262Z","iopub.status.idle":"2025-12-24T13:04:33.620722Z","shell.execute_reply.started":"2025-12-24T13:04:33.612230Z","shell.execute_reply":"2025-12-24T13:04:33.619897Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"biomass_df = (\n    train_df_1\n    .pivot(\n        index=\"image_path\",\n        columns=\"target_name\",\n        values=\"target\"\n    )\n    .reset_index()\n)\n\nprint(\"Biomass rows:\", len(biomass_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.622008Z","iopub.execute_input":"2025-12-24T13:04:33.622392Z","iopub.status.idle":"2025-12-24T13:04:33.640554Z","shell.execute_reply.started":"2025-12-24T13:04:33.622341Z","shell.execute_reply":"2025-12-24T13:04:33.639485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_d_2 = meta_df.merge(\n    biomass_df,\n    on=\"image_path\",\n    how=\"inner\"\n)\n\nprint(\"Final train_wide rows:\", len(train_d_2))\nprint(train_d_2.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.642670Z","iopub.execute_input":"2025-12-24T13:04:33.643021Z","iopub.status.idle":"2025-12-24T13:04:33.655708Z","shell.execute_reply.started":"2025-12-24T13:04:33.642990Z","shell.execute_reply":"2025-12-24T13:04:33.654622Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\n\ndef clean_images_df(df, img_dir, verbose=True):\n    \"\"\"\n    df      : DataFrame có cột 'image_path'\n    img_dir : thư mục gốc ảnh\n    return  : df đã clean (ảnh tồn tại + mở được)\n    \"\"\"\n\n    n0 = len(df)\n\n    # 1) check tồn tại file\n    mask_exist = df[\"image_path\"].apply(\n        lambda p: os.path.exists(os.path.join(img_dir, p))\n    )\n    df = df[mask_exist].reset_index(drop=True)\n    n1 = len(df)\n\n    # 2) check mở được ảnh\n    good_idx = []\n    for i, p in enumerate(df[\"image_path\"]):\n        try:\n            Image.open(os.path.join(img_dir, p)).verify()\n            good_idx.append(i)\n        except Exception:\n            pass\n\n    df = df.iloc[good_idx].reset_index(drop=True)\n    n2 = len(df)\n\n    if verbose:\n        print(\"Image cleaning summary:\")\n        print(f\"  Start           : {n0}\")\n        print(f\"  After existence : {n1}  (-{n0-n1})\")\n        print(f\"  After openable  : {n2}  (-{n1-n2})\")\n\n    return df\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.657086Z","iopub.execute_input":"2025-12-24T13:04:33.657383Z","iopub.status.idle":"2025-12-24T13:04:33.675317Z","shell.execute_reply.started":"2025-12-24T13:04:33.657348Z","shell.execute_reply":"2025-12-24T13:04:33.674273Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_IMG_DIR = \"/kaggle/input/csiro-biomass\"\nTEST_IMG_DIR  = \"/kaggle/input/csiro-biomass\"\n\ntrain_df = clean_images_df(train_d_2, TRAIN_IMG_DIR)\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.676879Z","iopub.execute_input":"2025-12-24T13:04:33.677205Z","iopub.status.idle":"2025-12-24T13:04:33.992038Z","shell.execute_reply.started":"2025-12-24T13:04:33.677177Z","shell.execute_reply":"2025-12-24T13:04:33.991232Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from datetime import datetime\n\ndef parse_date_to_doy(s):\n    # Sampling_Date kiểu \"2015/9/4\" như hình\n    dt = datetime.strptime(str(s), \"%Y/%m/%d\")\n    doy = dt.timetuple().tm_yday\n    return doy, dt.year\n\ntrain_df = train_df.copy()\ndoy_year = train_df[\"Sampling_Date\"].apply(parse_date_to_doy)\ntrain_df[\"doy\"]  = doy_year.apply(lambda x: x[0])\ntrain_df[\"year\"] = doy_year.apply(lambda x: x[1])\n\n# time encoding: sin/cos theo chu kỳ năm\nT = 365.0\ntrain_df[\"sin_t\"] = np.sin(2*np.pi*train_df[\"doy\"]/T)\ntrain_df[\"cos_t\"] = np.cos(2*np.pi*train_df[\"doy\"]/T)\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:33.993063Z","iopub.execute_input":"2025-12-24T13:04:33.993350Z","iopub.status.idle":"2025-12-24T13:04:34.020483Z","shell.execute_reply.started":"2025-12-24T13:04:33.993312Z","shell.execute_reply":"2025-12-24T13:04:34.019497Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"state_vocab = sorted(train_df[\"State\"].astype(str).unique())\nstate2idx = {s:i for i,s in enumerate(state_vocab)}\ntrain_df[\"state_idx\"] = train_df[\"State\"].astype(str).map(state2idx).astype(int)\n\nprint(\"Num states:\", len(state_vocab))\ntrain_df[[\"State\",\"state_idx\"]].head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.023116Z","iopub.execute_input":"2025-12-24T13:04:34.023405Z","iopub.status.idle":"2025-12-24T13:04:34.041034Z","shell.execute_reply.started":"2025-12-24T13:04:34.023379Z","shell.execute_reply":"2025-12-24T13:04:34.040192Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\nimport pandas as pd\n\n# nếu muốn gộp vài biến thể/typo thì thêm vào đây (optional)\nSYN_MAP = {\n    # ví dụ: \"barleygrass\": \"barleygrass\",  # đã tự gom bằng lower()\n    # \"whiteclover\": \"clover\",             # chỉ gộp nếu bạn muốn\n}\n\ndef norm_sp_token(t: str) -> str:\n    t = re.sub(r\"\\s+\", \"\", str(t))   # remove spaces\n    t = t.strip().lower()            # normalize case\n    return SYN_MAP.get(t, t)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.042241Z","iopub.execute_input":"2025-12-24T13:04:34.042512Z","iopub.status.idle":"2025-12-24T13:04:34.060724Z","shell.execute_reply.started":"2025-12-24T13:04:34.042486Z","shell.execute_reply":"2025-12-24T13:04:34.059765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def split_species(x):\n    if pd.isna(x):\n        return [\"unknown\"]\n\n    x = str(x).strip()\n    if x == \"\" or x.lower() == \"nan\":\n        return [\"unknown\"]\n\n    # chuẩn hoá delimiter: nhiều dấu _ liên tiếp -> 1 dấu _\n    x = re.sub(r\"_+\", \"_\", x)\n\n    # split\n    toks = [norm_sp_token(t) for t in x.split(\"_\") if t.strip() != \"\"]\n\n    # nếu là \"mixed\" thì giữ như 1 token riêng (đừng split thành rỗng)\n    if len(toks) == 1 and toks[0] == \"mixed\":\n        return [\"mixed\"]\n\n    # fallback\n    return toks if len(toks) > 0 else [\"unknown\"]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.061961Z","iopub.execute_input":"2025-12-24T13:04:34.062309Z","iopub.status.idle":"2025-12-24T13:04:34.079458Z","shell.execute_reply.started":"2025-12-24T13:04:34.062262Z","shell.execute_reply":"2025-12-24T13:04:34.078499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = train_df.copy()\ntrain_df[\"species_list\"] = train_df[\"Species\"].apply(split_species)\n\nlens = train_df[\"species_list\"].apply(len)\nprint(\"Min/Mean/Max species count per row:\", lens.min(), lens.mean(), lens.max())\n\n# có row nào rỗng không?\nempty_rows = train_df[lens == 0]\nprint(\"Empty rows:\", len(empty_rows))\n\n# vocab\nall_species = sorted({t for lst in train_df[\"species_list\"] for t in lst})\nsp2idx = {s:i for i,s in enumerate(all_species)}\n\nprint(\"Num species tokens:\", len(all_species))\nprint(\"Top 20 tokens:\", all_species[:20])\n\n# check vài mẫu\nprint(train_df[[\"Species\",\"species_list\"]].head(10).to_string(index=False))\ntrain_df.head(20)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.081128Z","iopub.execute_input":"2025-12-24T13:04:34.081513Z","iopub.status.idle":"2025-12-24T13:04:34.131458Z","shell.execute_reply.started":"2025-12-24T13:04:34.081479Z","shell.execute_reply":"2025-12-24T13:04:34.130536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_cols = [\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\nmu = train_df[num_cols].mean()\nsd = train_df[num_cols].std().replace(0, 1.0)\n\ntrain_df[\"ndvi_z\"] = (train_df[\"Pre_GSHH_NDVI\"] - mu[\"Pre_GSHH_NDVI\"]) / sd[\"Pre_GSHH_NDVI\"]\ntrain_df[\"hgt_z\"]  = (train_df[\"Height_Ave_cm\"] - mu[\"Height_Ave_cm\"]) / sd[\"Height_Ave_cm\"]\n\ntrain_df[[\"Pre_GSHH_NDVI\",\"ndvi_z\",\"Height_Ave_cm\",\"hgt_z\",\"sin_t\",\"cos_t\"]].head()\n\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.132596Z","iopub.execute_input":"2025-12-24T13:04:34.132956Z","iopub.status.idle":"2025-12-24T13:04:34.158797Z","shell.execute_reply.started":"2025-12-24T13:04:34.132924Z","shell.execute_reply":"2025-12-24T13:04:34.157695Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nCSV_DEFAULT = {\n    \"state_idx\": int(train_df[\"state_idx\"].mode().iloc[0]),\n\n    \"nums\": torch.tensor(\n        [\n            train_df[\"sin_t\"].mean(),\n            train_df[\"cos_t\"].mean(),\n            train_df[\"ndvi_z\"].mean(),\n            train_df[\"hgt_z\"].mean(),\n        ],\n        dtype=torch.float32\n    ),\n\n    \"species_ids\": torch.tensor(\n        [[sp2idx[s] for s in train_df[\"species_list\"].explode().value_counts().index[:1]]],\n        dtype=torch.long\n    ),\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.160137Z","iopub.execute_input":"2025-12-24T13:04:34.160541Z","iopub.status.idle":"2025-12-24T13:04:34.181274Z","shell.execute_reply.started":"2025-12-24T13:04:34.160512Z","shell.execute_reply":"2025-12-24T13:04:34.180317Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\nfrom torch.utils.data import Dataset\n\nTARGET4 = [\"Dry_Clover_g\",\"Dry_Dead_g\",\"Dry_Green_g\",\"GDM_g\"]\n\nclass BiomassMultiModalDS(Dataset):\n    def __init__(self, df, img_dir, sp2idx, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.sp2idx = sp2idx\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, i):\n        r = self.df.iloc[i]\n\n        # image\n        img_path = os.path.join(self.img_dir, r[\"image_path\"])\n        img = Image.open(img_path).convert(\"RGB\")\n        if self.transform is not None:\n            img = self.transform(img)\n\n        # csv pack\n        state_idx = int(r[\"state_idx\"])\n\n        sp_ids = [self.sp2idx[t] for t in r[\"species_list\"] if t in self.sp2idx]\n        if len(sp_ids) == 0:\n            sp_ids = [0]  # fallback (hiếm)\n\n        nums = np.array([r[\"sin_t\"], r[\"cos_t\"], r[\"ndvi_z\"], r[\"hgt_z\"]], dtype=np.float32)\n\n        # targets\n        y = r[TARGET4].values.astype(np.float32)\n\n        return {\n            \"image\": img,\n            \"state_idx\": torch.tensor(state_idx, dtype=torch.long),\n            \"species_ids\": torch.tensor(sp_ids, dtype=torch.long),  # variable length\n            \"nums\": torch.tensor(nums, dtype=torch.float32),\n            \"y\": torch.tensor(y, dtype=torch.float32),\n            \"dry_total_true\": torch.tensor(float(r[\"Dry_Total_g\"]), dtype=torch.float32),  # để penalty optional\n        }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.182638Z","iopub.execute_input":"2025-12-24T13:04:34.183030Z","iopub.status.idle":"2025-12-24T13:04:34.204645Z","shell.execute_reply.started":"2025-12-24T13:04:34.182992Z","shell.execute_reply":"2025-12-24T13:04:34.203541Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def collate_fn(batch):\n    images = torch.stack([b[\"image\"] for b in batch], dim=0)\n    state_idx = torch.stack([b[\"state_idx\"] for b in batch], dim=0)\n    nums = torch.stack([b[\"nums\"] for b in batch], dim=0)\n    y = torch.stack([b[\"y\"] for b in batch], dim=0)\n    dry_total_true = torch.stack([b[\"dry_total_true\"] for b in batch], dim=0)\n\n    # pad species\n    lens = [len(b[\"species_ids\"]) for b in batch]\n    maxlen = max(lens)\n    sp_pad = torch.zeros(len(batch), maxlen, dtype=torch.long)\n    sp_mask = torch.zeros(len(batch), maxlen, dtype=torch.bool)\n    for i,b in enumerate(batch):\n        L = len(b[\"species_ids\"])\n        sp_pad[i,:L] = b[\"species_ids\"]\n        sp_mask[i,:L] = True\n\n    return {\n        \"image\": images,\n        \"state_idx\": state_idx,\n        \"species_ids\": sp_pad,\n        \"species_mask\": sp_mask,\n        \"nums\": nums,\n        \"y\": y,\n        \"dry_total_true\": dry_total_true\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.207062Z","iopub.execute_input":"2025-12-24T13:04:34.207809Z","iopub.status.idle":"2025-12-24T13:04:34.225490Z","shell.execute_reply.started":"2025-12-24T13:04:34.207770Z","shell.execute_reply":"2025-12-24T13:04:34.224367Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.transforms as T\n\nIMG_SIZE = 384\ntfm = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.ToTensor(),\n])\n\nds = BiomassMultiModalDS(train_df, \"/kaggle/input/csiro-biomass\", sp2idx, transform=tfm)\nb0 = ds[0]\nprint(b0[\"image\"].shape, b0[\"state_idx\"], b0[\"species_ids\"][:5], b0[\"nums\"], b0[\"y\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.226858Z","iopub.execute_input":"2025-12-24T13:04:34.227696Z","iopub.status.idle":"2025-12-24T13:04:34.322604Z","shell.execute_reply.started":"2025-12-24T13:04:34.227651Z","shell.execute_reply":"2025-12-24T13:04:34.321546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\nimport timm\nclass ImageEncoder(nn.Module):\n    def __init__(self, weight_path):\n        super().__init__()\n\n        self.backbone = timm.create_model(\n            \"efficientnet_b0\",\n            pretrained=False,\n            num_classes=0\n        )\n\n        state = torch.load(weight_path, map_location=\"cpu\")\n\n        if isinstance(state, dict) and \"state_dict\" in state:\n            state = state[\"state_dict\"]\n\n        self.backbone.load_state_dict(state, strict=False)\n        self.out_dim = self.backbone.num_features\n\n    def forward(self, x):\n        return self.backbone(x)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.325904Z","iopub.execute_input":"2025-12-24T13:04:34.326300Z","iopub.status.idle":"2025-12-24T13:04:34.333341Z","shell.execute_reply.started":"2025-12-24T13:04:34.326270Z","shell.execute_reply":"2025-12-24T13:04:34.332431Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CSVEncoder(nn.Module):\n    def __init__(\n        self,\n        n_state,\n        n_species,\n        emb_dim_state=4,\n        emb_dim_species=4,\n        out_dim=32\n    ):\n        super().__init__()\n\n        self.state_emb = nn.Embedding(n_state, emb_dim_state)\n        self.species_emb = nn.Embedding(n_species, emb_dim_species)\n\n        # numeric: sin_t, cos_t, ndvi_z, hgt_z → 4\n        in_dim = emb_dim_state + emb_dim_species + 4\n\n        self.mlp = nn.Sequential(\n            nn.Linear(in_dim, out_dim),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(out_dim),\n        )\n\n    def forward(self, state_idx, species_ids, species_mask, nums):\n        \"\"\"\n        state_idx    : [B]\n        species_ids  : [B, L]\n        species_mask : [B, L]\n        nums         : [B, 4]\n        \"\"\"\n\n        # State embedding\n        s_emb = self.state_emb(state_idx)              # [B, Ds]\n\n        # Species embedding + mean pooling\n        sp_emb = self.species_emb(species_ids)         # [B, L, Dp]\n        sp_emb = sp_emb * species_mask.unsqueeze(-1)\n\n        sp_mean = sp_emb.sum(1) / species_mask.sum(1, keepdim=True).clamp(min=1)\n\n        # Concat CSV features\n        x = torch.cat([s_emb, sp_mean, nums], dim=1)\n\n        return self.mlp(x)                              # z_csv ∈ R^32\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.334678Z","iopub.execute_input":"2025-12-24T13:04:34.335052Z","iopub.status.idle":"2025-12-24T13:04:34.354355Z","shell.execute_reply.started":"2025-12-24T13:04:34.335010Z","shell.execute_reply":"2025-12-24T13:04:34.353282Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# class BiomassModel(nn.Module):\n#     def __init__(self, n_state, n_species, eff_weight_path):\n#         super().__init__()\n\n#         # Image branch (EfficientNet local)\n#         self.image_encoder = ImageEncoder(eff_weight_path)\n#         img_dim = self.image_encoder.out_dim   # 1280\n\n#         # CSV branch\n#         self.csv_encoder = CSVEncoder(\n#             n_state=n_state,\n#             n_species=n_species,\n#             out_dim=32\n#         )\n\n#         # Regression head\n#         self.head = nn.Sequential(\n#             nn.Linear(img_dim + 32, 256),\n#             nn.ReLU(inplace=True),\n#             nn.Dropout(0.2),\n#             nn.Linear(256, 4)   # [Dry_Clover, Dry_Dead, Dry_Green, GDM]\n#         )\n\n#     def forward(self, batch):\n#         \"\"\"\n#         batch keys:\n#         - image\n#         - state_idx\n#         - species_ids\n#         - species_mask\n#         - nums\n#         \"\"\"\n\n#         z_img = self.image_encoder(batch[\"image\"])   # [B,1280]\n\n#         z_csv = self.csv_encoder(\n#             batch[\"state_idx\"],\n#             batch[\"species_ids\"],\n#             batch[\"species_mask\"],\n#             batch[\"nums\"]\n#         )                                             # [B,32]\n\n#         z = torch.cat([z_img, z_csv], dim=1)\n\n#         return self.head(z)                           # [B,4]\n\nclass BiomassModel(nn.Module):\n    def __init__(self, n_state, n_species, eff_weight_path):\n        super().__init__()\n\n        # Image branch\n        self.image_encoder = ImageEncoder(eff_weight_path)\n        img_dim = self.image_encoder.out_dim   # 1280\n\n        # CSV branch\n        self.csv_encoder = CSVEncoder(\n            n_state=n_state,\n            n_species=n_species,\n            out_dim=32\n        )\n\n        # ===== SHARED TRUNK (giữ nguyên logic cũ) =====\n        self.trunk = nn.Sequential(\n            nn.Linear(img_dim + 32, 256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.2),\n        )\n\n        # ===== MULTI-HEAD =====\n        self.heads = nn.ModuleDict({\n            \"C\": nn.Linear(256, 1),   # Dry_Clover\n            \"D\": nn.Linear(256, 1),   # Dry_Dead\n            \"G\": nn.Linear(256, 1),   # Dry_Green\n            \"M\": nn.Linear(256, 1),   # GDM\n        })\n    def forward(self, batch):\n        \"\"\"\n        batch keys:\n        - image           : [B,3,H,W]\n        - state_idx       : [B]\n        - species_ids     : [B, max_species]\n        - species_mask    : [B, max_species]\n        - nums            : [B, max_species]\n        \"\"\"\n    \n        # ===== IMAGE BRANCH =====\n        z_img = self.image_encoder(\n            batch[\"image\"]\n        )                               # [B, 1280]\n    \n        # ===== CSV BRANCH =====\n        z_csv = self.csv_encoder(\n            batch[\"state_idx\"],\n            batch[\"species_ids\"],\n            batch[\"species_mask\"],\n            batch[\"nums\"]\n        )                               # [B, 32]\n    \n        # ===== FUSION =====\n        z = torch.cat([z_img, z_csv], dim=1)   # [B, 1312]\n    \n        # ===== SHARED TRUNK =====\n        h = self.trunk(z)                       # [B, 256]\n    \n        # ===== MULTI-HEAD OUTPUT =====\n        out_C = self.heads[\"C\"](h)              # [B, 1]\n        out_D = self.heads[\"D\"](h)              # [B, 1]\n        out_G = self.heads[\"G\"](h)              # [B, 1]\n        out_M = self.heads[\"M\"](h)              # [B, 1]\n    \n        # ===== CONCAT OUTPUT =====\n        out = torch.cat(\n            [out_C, out_D, out_G, out_M],\n            dim=1\n        )                                       # [B, 4]\n    \n        return out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.355512Z","iopub.execute_input":"2025-12-24T13:04:34.355871Z","iopub.status.idle":"2025-12-24T13:04:34.378773Z","shell.execute_reply.started":"2025-12-24T13:04:34.355807Z","shell.execute_reply":"2025-12-24T13:04:34.377689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EFF_WEIGHT_PATH = \"/kaggle/input/weight-b0/pytorch_model.bin\"\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = BiomassModel(\n    n_state=len(state2idx),\n    n_species=len(sp2idx),\n    eff_weight_path=EFF_WEIGHT_PATH\n).to(device)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.380417Z","iopub.execute_input":"2025-12-24T13:04:34.380996Z","iopub.status.idle":"2025-12-24T13:04:34.555260Z","shell.execute_reply.started":"2025-12-24T13:04:34.380953Z","shell.execute_reply":"2025-12-24T13:04:34.554202Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(hasattr(model, \"heads\"), type(model.heads))\nprint(model.trunk[0].in_features, model.trunk[0].out_features)\nfor k in [\"C\",\"D\",\"G\",\"M\"]:\n    print(k, model.heads[k].in_features, model.heads[k].out_features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.556500Z","iopub.execute_input":"2025-12-24T13:04:34.557013Z","iopub.status.idle":"2025-12-24T13:04:34.563736Z","shell.execute_reply.started":"2025-12-24T13:04:34.556968Z","shell.execute_reply":"2025-12-24T13:04:34.562509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"batch = next(iter(\n    torch.utils.data.DataLoader(\n        ds, batch_size=4, collate_fn=collate_fn\n    )\n))\n\nfor k in batch:\n    batch[k] = batch[k].to(device)\n\nout = model(batch)\nprint(\"Output shape:\", out.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:34.564853Z","iopub.execute_input":"2025-12-24T13:04:34.565220Z","iopub.status.idle":"2025-12-24T13:04:35.340339Z","shell.execute_reply.started":"2025-12-24T13:04:34.565191Z","shell.execute_reply":"2025-12-24T13:04:35.339423Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\nSEED = 42\nnp.random.seed(SEED)\n\nidx = np.arange(len(train_df))\nnp.random.shuffle(idx)\n\nn_val = int(0.2 * len(idx))\nval_idx = idx[:n_val]\ntrn_idx = idx[n_val:]\n\ntrain_trn = train_df.iloc[trn_idx].reset_index(drop=True)\ntrain_val = train_df.iloc[val_idx].reset_index(drop=True)\n\nprint(\"Train:\", len(train_trn), \" Val:\", len(train_val))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.341657Z","iopub.execute_input":"2025-12-24T13:04:35.342105Z","iopub.status.idle":"2025-12-24T13:04:35.351589Z","shell.execute_reply.started":"2025-12-24T13:04:35.342075Z","shell.execute_reply":"2025-12-24T13:04:35.350726Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.transforms as T\nfrom torch.utils.data import DataLoader\n\nIMG_SIZE = 384\n\ntrain_tfm = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.RandomHorizontalFlip(p=0.5),\n    T.RandomVerticalFlip(p=0.2),\n    T.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.05, hue=0.02),\n    T.ToTensor(),\n])\n\nvalid_tfm = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.ToTensor(),\n])\n\ntrain_ds = BiomassMultiModalDS(train_trn, TRAIN_IMG_DIR, sp2idx, transform=train_tfm)\nvalid_ds = BiomassMultiModalDS(train_val, TRAIN_IMG_DIR, sp2idx, transform=valid_tfm)\n\nBATCH_SIZE = 16\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n    collate_fn=collate_fn\n)\nvalid_loader = DataLoader(\n    valid_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    collate_fn=collate_fn\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.352835Z","iopub.execute_input":"2025-12-24T13:04:35.353215Z","iopub.status.idle":"2025-12-24T13:04:35.370197Z","shell.execute_reply.started":"2025-12-24T13:04:35.353179Z","shell.execute_reply":"2025-12-24T13:04:35.369107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# LOSS_W = torch.tensor([0.1, 0.1, 0.1, 0.2])  # Clover, Dead, Green, GDM\n\n#def biomass_loss(pred, target, dry_total_true, alpha=0.2):\n#    w = LOSS_W.to(pred.device).view(1, -1)\n#    mse = ((pred - target) ** 2 * w).mean()\n\n#    dry_sum_pred = pred[:, 0] + pred[:, 1] + pred[:, 2]\n#   bio_penalty = ((dry_sum_pred - dry_total_true) ** 2).mean()\n\n#    return mse + alpha * bio_penalty\n\n# import torch.nn.functional as F\n\n# def biomass_loss(\n#     pred,\n#     target,\n#     dry_total_true,\n#     epoch,\n#     alpha_max=0.2,\n#     delta=0.3\n# ):\n#     \"\"\"\n#     pred: (B,4)  [Clover, Dead, Green, GDM]\n#     target: (B,4)\n#     dry_total_true: (B,)\n#     \"\"\"\n\n#     # ---- warm-up constraint ----\n#     alpha = min(alpha_max, epoch / 10 * alpha_max)\n\n#     # ---- Huber loss cho Dry components ----\n#     loss_clover = F.smooth_l1_loss(pred[:, 0], target[:, 0], beta=delta)\n#     loss_dead   = F.smooth_l1_loss(pred[:, 1], target[:, 1], beta=delta)\n#     loss_green  = F.smooth_l1_loss(pred[:, 2], target[:, 2], beta=delta)\n#    # GDM dùng RMSE\n#     loss_gdm = torch.sqrt(\n#         F.mse_loss(pred[:, 3], target[:, 3]) + 1e-8\n#     )\n\n#     # ---- Sinh học: ràng buộc tổng khô (L1, detach Clover) ----\n#     dry_sum_pred = pred[:, 1] + pred[:, 2]  # Dead + Green\n#     bio_penalty = (\n#         dry_sum_pred - (dry_total_true - pred[:, 0].detach())\n#     ).abs().mean()\n\n#     # ---- Tổng loss ----\n#     loss = (\n#         0.6*loss_clover +\n#         0.4*loss_dead   +\n#         loss_green  +\n#         loss_gdm\n#     )\n\n#     return loss\n\nimport torch\nimport torch.nn.functional as F\n\ndef biomass_loss(\n    pred,\n    target,\n    dry_total_true,\n    weights,          # dict: {\"C\": wC, \"D\": wD, \"G\": wG, \"M\": wM}\n    epoch,\n    alpha_max=0.2,\n    delta=0.3\n):\n    \"\"\"\n    pred: (B,4)  [Clover, Dead, Green, GDM]\n    target: (B,4)\n    dry_total_true: (B,)\n    weights: dict các hệ số wj (đã được cập nhật ngoài hàm)\n    \"\"\"\n\n    # =========================================================\n    # 1. Warm-up cho ràng buộc sinh học\n    # =========================================================\n    alpha = min(alpha_max, epoch / 10 * alpha_max)\n\n    # =========================================================\n    # 2. Loss từng sinh khối\n    # =========================================================\n    loss_clover = F.smooth_l1_loss(pred[:, 0], target[:, 0], beta=delta)\n    loss_dead   = F.smooth_l1_loss(pred[:, 1], target[:, 1], beta=delta)\n    loss_green  = F.smooth_l1_loss(pred[:, 2], target[:, 2], beta=delta)\n\n    # GDM dùng RMSE\n    loss_gdm = torch.sqrt(\n        F.mse_loss(pred[:, 3], target[:, 3]) + 1e-8\n    )\n\n    # =========================================================\n    # 3. Ràng buộc sinh học Dry_Total\n    # Dry_Total = Clover + Dead + Green\n    # detach Clover để tránh xung đột gradient\n    # =========================================================\n    dry_sum_pred = pred[:, 1] + pred[:, 2]   # Dead + Green\n    bio_penalty = (\n        dry_sum_pred - (dry_total_true - pred[:, 0].detach())\n    ).abs().mean()\n\n    # =========================================================\n    # 4. Loss tổng (trọng tâm của bạn)\n    # =========================================================\n    weights = {\"C\": 1.0, \"D\": 1.0, \"G\": 1.0, \"M\": 1.0}\n    loss = (\n        weights[\"C\"] * loss_clover +\n        weights[\"D\"] * loss_dead   +\n        weights[\"G\"] * loss_green  +\n        weights[\"M\"] * loss_gdm  \n        # alpha * bio_penalty\n    )\n\n    # (tùy chọn) trả thêm loss từng thành phần để log / đo gradient\n    return loss, {\n        \"clover\": loss_clover,\n        \"dead\":   loss_dead,\n        \"green\":  loss_green,\n        \"gdm\":    loss_gdm,\n        # \"bio\":    bio_penalty\n    }\n\n\n\n\n\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.371904Z","iopub.execute_input":"2025-12-24T13:04:35.372250Z","iopub.status.idle":"2025-12-24T13:04:35.392771Z","shell.execute_reply.started":"2025-12-24T13:04:35.372220Z","shell.execute_reply":"2025-12-24T13:04:35.391908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_backbone_trainable(model, trainable: bool):\n    # EfficientNet backbone nằm trong model.image_encoder.backbone\n    for p in model.image_encoder.backbone.parameters():\n        p.requires_grad = trainable\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.394197Z","iopub.execute_input":"2025-12-24T13:04:35.394663Z","iopub.status.idle":"2025-12-24T13:04:35.415355Z","shell.execute_reply.started":"2025-12-24T13:04:35.394634Z","shell.execute_reply":"2025-12-24T13:04:35.414161Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = model.to(device)\n\ndef make_optimizer_stage1(model, lr=3e-4):\n    params = (\n        list(model.csv_encoder.parameters()) +\n        list(model.trunk.parameters()) +\n        list(model.heads.parameters())\n    )\n    return optim.AdamW(params, lr=lr, weight_decay=1e-4)\n    \n\ndef make_optimizer_stage2(\n    model,\n    lr_backbone=1e-4,\n    lr_trunk=3e-4,\n    lr_csv=3e-4,\n    lr_heads=None,   # dict: {\"C\":..., \"D\":..., \"G\":..., \"M\":...}\n):\n    if lr_heads is None:\n        lr_heads = {\"C\": 1e-4, \"D\": 1e-4, \"G\": 3e-4, \"M\": 3e-4}\n\n    return optim.AdamW(\n        [\n            # Backbone (ảnh)\n            {\"params\": model.image_encoder.backbone.parameters(), \"lr\": lr_backbone},\n\n            # CSV encoder\n            {\"params\": model.csv_encoder.parameters(), \"lr\": lr_csv},\n\n            # Shared trunk\n            {\"params\": model.trunk.parameters(), \"lr\": lr_trunk},\n\n            # ===== TỪNG HEAD RIÊNG =====\n            {\"params\": model.heads[\"C\"].parameters(), \"lr\": lr_heads[\"C\"]},\n            {\"params\": model.heads[\"D\"].parameters(), \"lr\": lr_heads[\"D\"]},\n            {\"params\": model.heads[\"G\"].parameters(), \"lr\": lr_heads[\"G\"]},\n            {\"params\": model.heads[\"M\"].parameters(), \"lr\": lr_heads[\"M\"]},\n        ],\n        weight_decay=1e-4\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.416600Z","iopub.execute_input":"2025-12-24T13:04:35.416997Z","iopub.status.idle":"2025-12-24T13:04:35.437553Z","shell.execute_reply.started":"2025-12-24T13:04:35.416968Z","shell.execute_reply":"2025-12-24T13:04:35.436467Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\ndef r2_kaggle(preds, trues):\n    \"\"\"\n    preds, trues: shape (N,4)\n    order: Clover, Dead, Green, GDM\n    \"\"\"\n\n    # --- build flat vectors ---\n    y_true = []\n    y_pred = []\n    weights = []\n\n    # Clover\n    y_true.append(trues[:, 0])\n    y_pred.append(preds[:, 0])\n    weights.append(np.full(len(trues), 0.1))\n\n    # Dead\n    y_true.append(trues[:, 1])\n    y_pred.append(preds[:, 1])\n    weights.append(np.full(len(trues), 0.1))\n\n    # Green\n    y_true.append(trues[:, 2])\n    y_pred.append(preds[:, 2])\n    weights.append(np.full(len(trues), 0.1))\n\n    # GDM\n    y_true.append(trues[:, 3])\n    y_pred.append(preds[:, 3])\n    weights.append(np.full(len(trues), 0.2))\n\n    # Dry_Total = sum 3 dry\n    y_true.append(trues[:, 0] + trues[:, 1] + trues[:, 2])\n    y_pred.append(preds[:, 0] + preds[:, 1] + preds[:, 2])\n    weights.append(np.full(len(trues), 0.5))\n\n    y_true = np.concatenate(y_true)\n    y_pred = np.concatenate(y_pred)\n    w      = np.concatenate(weights)\n\n    # --- weighted mean ---\n    y_bar = np.sum(w * y_true) / np.sum(w)\n\n    # --- weighted R2 ---\n    ss_res = np.sum(w * (y_true - y_pred) ** 2)\n    ss_tot = np.sum(w * (y_true - y_bar) ** 2)\n\n    return 1.0 - ss_res / ss_tot\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.438990Z","iopub.execute_input":"2025-12-24T13:04:35.439381Z","iopub.status.idle":"2025-12-24T13:04:35.448956Z","shell.execute_reply.started":"2025-12-24T13:04:35.439278Z","shell.execute_reply":"2025-12-24T13:04:35.447995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class EarlyStopPerTarget:\n    def __init__(self, patience=5, rel_delta=0.005, eps=1e-8):\n        self.patience = patience\n        self.rel_delta = rel_delta\n        self.eps = eps\n        self.best = None\n        self.wait = 0\n\n    def step(self, current):\n        current = np.asarray(current)\n\n        if self.best is None:\n            self.best = current.copy()\n            return False\n\n        # cải thiện theo tỷ lệ\n        rel_improve = (self.best - current) / np.maximum(self.best, self.eps)\n        improved = rel_improve > self.rel_delta\n\n        if improved.any():\n            self.best[improved] = current[improved]\n            self.wait = 0\n        else:\n            self.wait += 1\n\n        return self.wait >= self.patience\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.450062Z","iopub.execute_input":"2025-12-24T13:04:35.450317Z","iopub.status.idle":"2025-12-24T13:04:35.465690Z","shell.execute_reply.started":"2025-12-24T13:04:35.450293Z","shell.execute_reply":"2025-12-24T13:04:35.464548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import r2_score\n\ndef train_one_epoch(model, loader, optimizer, grad_clip=1.0):\n    model.train()\n    total_loss, n = 0.0, 0\n\n    mae_sum = torch.zeros(4, device=device)\n\n    preds_all = []\n    trues_all = []\n\n    # ---- tích lũy gradient norm theo epoch ----\n    grad_norms = {\"C\": 0.0, \"D\": 0.0, \"G\": 0.0, \"M\": 0.0}\n    n_batches = 0\n\n    for batch in loader:\n        for k in batch:\n            batch[k] = batch[k].to(device)\n\n        pred = model(batch)\n        y    = batch[\"y\"]\n\n        # =====================================================\n        # 1. TÍNH LOSS (NHẬN weights TỪ NGOÀI)\n        # =====================================================\n        loss, loss_dict = biomass_loss(\n            pred,\n            y,\n            batch[\"dry_total_true\"],\n            weights,\n            epoch,\n            alpha_max=0.2,\n            delta=0.5\n        )\n\n        # =====================================================\n        # 2. ĐO GRADIENT TỪNG LUỒNG (QUAN TRỌNG)\n        # =====================================================\n        for k_loss, key in zip(\n            [\"C\", \"D\", \"G\", \"M\"],\n            [\"clover\", \"dead\", \"green\", \"gdm\"]\n        ):\n            optimizer.zero_grad(set_to_none=True)\n            loss_dict[key].backward(retain_graph=True)\n\n            g = 0.0\n            for p in model.parameters():   # <<< SỬA Ở ĐÂY\n                if p.grad is not None:\n                    g += p.grad.data.norm(2).item() ** 2\n\n            grad_norms[k_loss] += g ** 0.5\n\n        # =====================================================\n        # 3. BACKWARD LOSS TỔNG + UPDATE MODEL\n        # =====================================================\n        optimizer.zero_grad(set_to_none=True)\n        loss.backward()\n\n        torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip)\n        optimizer.step()\n\n        # =====================================================\n        # 4. METRICS\n        # =====================================================\n        mae = (pred - y).abs().mean(dim=0)\n        mae_sum += mae.detach() * y.size(0)\n\n        bs = y.size(0)\n        total_loss += loss.item() * bs\n        n += bs\n\n        preds_all.append(pred.detach().cpu().numpy())\n        trues_all.append(y.detach().cpu().numpy())\n\n        n_batches += 1\n\n    # =====================================================\n    # AGGREGATE\n    # =====================================================\n    train_loss = total_loss / n\n    train_mae4 = (mae_sum / n).cpu().numpy()\n\n    preds_all = np.concatenate(preds_all, axis=0)\n    trues_all = np.concatenate(trues_all, axis=0)\n\n    train_r2_global = r2_kaggle(preds_all, trues_all)\n\n    # ---- gradient norm trung bình mỗi epoch ----\n    for k in grad_norms:\n        grad_norms[k] /= max(n_batches, 1)\n\n    return train_loss, train_mae4, train_r2_global, grad_norms\n\n\n\n\n@torch.no_grad()\ndef valid_one_epoch(model, loader, alpha=0.2):\n    model.eval()\n\n    preds, trues = [], []\n    mse_sum = torch.zeros(4, device=device)\n    total_gap, n = 0.0, 0  # |sum3 - total_true|\n\n    for batch in loader:\n        for k in batch:\n            batch[k] = batch[k].to(device)\n\n        pred = model(batch)          # (B,4)\n        y    = batch[\"y\"]            # (B,4)\n\n        # lưu để tính R2\n        preds.append(pred.cpu().numpy())\n        trues.append(y.cpu().numpy())\n\n        # MSE từng sinh khối\n        mse = (pred - y).abs().mean(dim=0)   # (4,)\n        mse_sum += mse * y.size(0)\n\n        # ràng buộc Dry_Total\n        gap = (pred[:, 0] + pred[:, 1] + pred[:, 2]\n               - batch[\"dry_total_true\"]).abs().sum().item()\n\n        total_gap += gap\n        n += y.size(0)\n\n    preds = np.concatenate(preds, axis=0)\n    trues = np.concatenate(trues, axis=0)\n\n    # R2\n    r2_each = [r2_score(trues[:, i], preds[:, i]) for i in range(4)]\n    #r2_global = float(np.mean(r2_each))\n\n    r2_global = r2_kaggle(preds, trues)\n    \n    mean_gap = total_gap / max(n, 1)\n    mse4 = (mse_sum / n).cpu().numpy()\n\n    return r2_global, r2_each, mean_gap, mse4\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.467179Z","iopub.execute_input":"2025-12-24T13:04:35.467506Z","iopub.status.idle":"2025-12-24T13:04:35.487570Z","shell.execute_reply.started":"2025-12-24T13:04:35.467466Z","shell.execute_reply":"2025-12-24T13:04:35.486516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 15\nFREEZE_EPOCHS = 3\nALPHA = 0.2\nGRAD_CLIP = 1.0\nearly_stop = EarlyStopPerTarget(patience=3, rel_delta=0.005, eps=1e-8)\n#best_r2 = -1e9\n\nbest_r2 = -float(\"inf\")\npatience = 3        # số epoch cho phép không cải thiện\nmin_delta = 0.001   # ngưỡng cải thiện tối thiểu\nwait = 0\ngap_threshold = 0.2\n\nweights = {\"C\": 1.0, \"D\": 1.0, \"G\": 1.0, \"M\": 1.0}\n\ngrad_ema = {\"C\": None, \"D\": None, \"G\": None, \"M\": None}\nema_beta = 0.9\neps = 1e-6\nalpha_w = 0.4   # hệ số cập nhật weight\n\n\nhistory = {\n    \"train_mse4\": [],   # list of np.array(4,)\n    \"val_mse4\":   [],\n}\ntrain_r2_hist = []\nval_r2_hist = []\n# Stage 1: freeze\nset_backbone_trainable(model, False)\nopt = make_optimizer_stage1(model, lr=3e-4)\n\nfor epoch in range(1, EPOCHS + 1):\n    weights = {\"C\": 1.0, \"D\": 1.0, \"G\": 1.0, \"M\": 1.0}\n    if epoch == FREEZE_EPOCHS + 1:\n        # Stage 2: unfreeze + new optimizer\n        set_backbone_trainable(model, True)\n        opt = make_optimizer_stage2(\n            model,\n            lr_backbone=1e-4,\n            lr_trunk=3e-4,\n            lr_csv=3e-4,\n            lr_heads=None,   # dict: {\"C\":..., \"D\":..., \"G\":..., \"M\":...}\n        )\n\n    tr_loss, tr_mse4, train_r2_global, grad_norms = train_one_epoch(model, train_loader, opt, grad_clip=GRAD_CLIP)\n    val_r2, r2_each, mean_gap, va_mse4  = valid_one_epoch(model, valid_loader, alpha=ALPHA)\n\n    history[\"train_mse4\"].append(tr_mse4)\n    history[\"val_mse4\"].append(va_mse4)\n    train_r2_hist.append(train_r2_global)\n    val_r2_hist.append(val_r2)\n    \n    print(\n        f\"Epoch {epoch:02d} | \"\n        f\"C-Tr-Val: {tr_mse4[0]:.4f}-\"\n        f\"{va_mse4[0]:.4f} || \"\n        f\"D-Tr-Val: {tr_mse4[1]:.4f}-\"\n        f\"{va_mse4[1]:.4f} || \"\n        f\"G-Tr-Val: {tr_mse4[2]:.4f}-\"\n        f\"{va_mse4[2]:.4f} || \"\n        f\"GDM-Tr-Val: {tr_mse4[3]:.4f}-\"\n        f\"{va_mse4[3]:.4f} || \"\n        f\"R2-Tr-Val: {train_r2_global:.4f}-\"\n        f\"{val_r2:.4f} || \"\n  #     f\"Each {[round(x,3) for x in r2_each]} | \"\n    #    f\"Gap(sum3-total) {mean_gap:.3f}\"\n    )\n        # 1. EMA gradient\n    for k in grad_norms:   # grad_norms trả từ train_one_epoch\n        if grad_ema[k] is None:\n            grad_ema[k] = grad_norms[k]\n        else:\n            grad_ema[k] = ema_beta * grad_ema[k] + (1 - ema_beta) * grad_norms[k]\n    \n    # 2. Gradient trung bình\n    g_bar = sum(grad_ema.values()) / len(grad_ema)\n    \n    # 3. Update weight (tỉ lệ nghịch gradient)\n    for k in weights:\n        weights[k] = (g_bar / (grad_ema[k] + eps)) ** alpha_w\n    \n    # 4. Normalize weight (giữ tổng ≈ số task = 4)\n    s = sum(weights.values())\n    for k in weights:\n        weights[k] = 4.0 * weights[k] / s\n    \n    print(\n        f\"   [Weights] \"\n        f\"C:{weights['C']:.2f} | \"\n        f\"D:{weights['D']:.2f} | \"\n        f\"G:{weights['G']:.2f} | \"\n        f\"M:{weights['M']:.2f}\"\n    )\n\n    # r2_gap = abs(train_r2_global - val_r2)\n    # if r2_gap < gap_threshold and val_r2 > 0.73:\n    #     wait += 1\n    # else:\n    #     wait = 0\n\n    # print(\n    #     f\"Epoch {epoch:03d} | \"\n    #     f\"Train R2: {train_r2_global:.4f} | \"\n    #     f\"Val R2: {val_r2:.4f} | \"\n    #     f\"Gap: {r2_gap:.4f} | \"\n    #     f\"Wait: {wait}/{patience}\"\n    # )\n\n    # if wait >= patience:\n    #     print(\n    #         f\"\\n🛑 EARLY STOP: |Train R2 - Val R2| < {gap_threshold} \"\n    #         f\"for {patience} consecutive epochs.\"\n    #     )\n    #     break\n    if early_stop.step(va_mse4):        \n        torch.save(model.state_dict(), \"best_model.pth\")\n        print(f\"Early stopping at epoch {epoch}\")\n        break\n    \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:04:35.488792Z","iopub.execute_input":"2025-12-24T13:04:35.489106Z","iopub.status.idle":"2025-12-24T13:23:27.513630Z","shell.execute_reply.started":"2025-12-24T13:04:35.489079Z","shell.execute_reply":"2025-12-24T13:23:27.512286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\n\ntrain_mse = np.array(history[\"train_mse4\"])  # (epochs, 4)\nval_mse   = np.array(history[\"val_mse4\"])\n\nepochs = np.arange(1, train_mse.shape[0] + 1)\n\nbiomass_names = [\"Dry_Clover\", \"Dry_Dead\", \"Dry_Green\", \"GDM\"]\n\n\nfig, axes = plt.subplots(2, 2, figsize=(16, 12))\naxes = axes.flatten()\n\nfor i, name in enumerate(biomass_names):\n    ax = axes[i]\n    ax.plot(epochs, train_mse[:, i], label=\"Train\", linewidth=2)\n    ax.plot(epochs, val_mse[:, i], \"--\", label=\"Validation\", linewidth=2)\n    ax.set_title(name)\n    ax.set_xlabel(\"Epoch\")\n    ax.set_ylabel(\"MSE\")\n    ax.grid(True)\n    ax.legend()\n\nfig.suptitle(\"Train / Validation MSE per Biomass Component\", fontsize=18)\nplt.tight_layout(rect=[0, 0, 1, 0.95])\nplt.show()\n\n\nplt.figure(figsize=(12, 8))\nplt.plot(\n    epochs,\n    train_r2_hist,\n    label=\"Train R²\",\n    linewidth=2\n)\n\nplt.plot(\n    epochs,\n    val_r2_hist,\n    \"--\",\n    label=\"Validation R²\",\n    linewidth=2\n)\n\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"R² (Weighted, Kaggle)\")\nplt.title(\"Train / Validation R² (Global)\")\nplt.grid(True)\nplt.legend()\nplt.tight_layout()\n\n# ---- SAVE ----\nplt.savefig(\n    \"train_val_r2_global.png\",\n    dpi=300,\n    bbox_inches=\"tight\"\n)\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:23:27.514956Z","iopub.status.idle":"2025-12-24T13:23:27.515335Z","shell.execute_reply.started":"2025-12-24T13:23:27.515169Z","shell.execute_reply":"2025-12-24T13:23:27.515191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig.savefig(\n    \"train_val_mse_per_biomass.png\",\n    dpi=300,\n    bbox_inches=\"tight\"\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:23:27.517363Z","iopub.status.idle":"2025-12-24T13:23:27.517909Z","shell.execute_reply.started":"2025-12-24T13:23:27.517648Z","shell.execute_reply":"2025-12-24T13:23:27.517679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.transforms as T\n\ndef preprocess_image(img, target_size):\n    tfm = T.Compose([\n        T.Resize((target_size, target_size)),\n        T.ToTensor(),\n    ])\n    return tfm(img)\n\n\n@torch.no_grad()\ndef predict(model, test_df, img_root, target_size=512):\n    model.eval()\n    records = []\n\n    for _, row in test_df.iterrows():\n        img_path = row[\"image_path\"]\n        full_path = f\"{img_root}/{img_path}\"\n\n        img = Image.open(full_path).convert(\"RGB\")\n        x = preprocess_image(img, target_size)\n        x = x.unsqueeze(0).to(device)\n\n        batch = {\n            \"image\": x,\n            \"state_idx\": torch.tensor([CSV_DEFAULT[\"state_idx\"]], device=device),\n            \"nums\": CSV_DEFAULT[\"nums\"].unsqueeze(0).to(device),\n            \"species_ids\": CSV_DEFAULT[\"species_ids\"].to(device),\n            \"species_mask\": torch.ones_like(CSV_DEFAULT[\"species_ids\"], dtype=torch.bool).to(device),\n        }\n\n        pred = model(batch).cpu().numpy().reshape(-1)\n\n        # --- phần dưới GIỮ NGUYÊN ---\n        dry_clover = float(pred[0])\n        dry_dead   = float(pred[1])\n        dry_green  = float(pred[2])\n        gdm        = float(pred[3])\n        dry_total  = dry_clover + dry_dead + dry_green\n\n        image_id = img_path.replace(\".jpg\", \"\").replace(\"test/\", \"\")\n\n        records.append([f\"{image_id}__Dry_Clover_g\", dry_clover])\n        records.append([f\"{image_id}__Dry_Dead_g\",   dry_dead])\n        records.append([f\"{image_id}__Dry_Green_g\",  dry_green])\n        records.append([f\"{image_id}__Dry_Total_g\",  dry_total])\n        records.append([f\"{image_id}__GDM_g\",        gdm])\n\n    return pd.DataFrame(records, columns=[\"sample_id\", \"target\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:23:27.518920Z","iopub.status.idle":"2025-12-24T13:23:27.519217Z","shell.execute_reply.started":"2025-12-24T13:23:27.519073Z","shell.execute_reply":"2025-12-24T13:23:27.519090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\n\ntest[\"image_id\"] = test[\"sample_id\"].apply(lambda x: x.split(\"__\")[0])\ntest_df = test.drop_duplicates(\"image_id\").reset_index(drop=True)\n\nsub = predict(\n    model,\n    test_df,\n    \"/kaggle/input/csiro-biomass\"\n)\n\nsub.to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(\"Saved submission.csv\")\nsub.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T13:23:27.520893Z","iopub.status.idle":"2025-12-24T13:23:27.521277Z","shell.execute_reply.started":"2025-12-24T13:23:27.521072Z","shell.execute_reply":"2025-12-24T13:23:27.521096Z"}},"outputs":[],"execution_count":null}]}