{"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,"sourceType":"competition"},{"sourceId":10038,"sourceType":"datasetVersion","datasetId":6978},{"sourceId":1803569,"sourceType":"datasetVersion","datasetId":1071580},{"sourceId":5709563,"sourceType":"datasetVersion","datasetId":3282418},{"sourceId":6430721,"sourceType":"datasetVersion","datasetId":3710658},{"sourceId":11924458,"sourceType":"datasetVersion","datasetId":7497068},{"sourceId":13746359,"sourceType":"datasetVersion","datasetId":8746989}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"print(\"hi\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import train_test_split\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if os.path.exists('/kaggle/working') else '.'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# BATCH_SIZE = 16\n# EPOCHS = 12\n# SEED = 42\n\n# random.seed(SEED)\n# np.random.seed(SEED)\n# torch.manual_seed(SEED)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n# class TestDataset(Dataset):\n#     def __init__(self, df, img_size, landscape=False, dual_stream=False, aug_config=None):\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n#         self.landscape = landscape\n#         self.dual_stream = dual_stream\n        \n#         if landscape:\n#             target_h, target_w = img_size, img_size * 2\n#         else:\n#             target_h, target_w = img_size, img_size\n        \n#         aug_list = [transforms.Resize((target_h, target_w))]\n        \n#         if aug_config:\n#             if aug_config.get('flip'):\n#                 aug_list.append(transforms.RandomHorizontalFlip(p=0.5))\n#             if aug_config.get('color'):\n#                 aug_list.append(transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1))\n#             if aug_config.get('rotation'):\n#                 aug_list.append(transforms.RandomRotation(15, expand=True, fill=0))\n#                 aug_list.append(transforms.Resize((target_h, target_w)))\n        \n#         aug_list.extend([\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.transform = transforms.Compose(aug_list)\n\n#     def __len__(self):\n#         return len(self.paths)\n\n#     def __getitem__(self, idx):\n#         path = self.paths[idx]\n#         img = Image.open(os.path.join(DATA_DIR, path)).convert('RGB')\n        \n#         if not self.landscape:\n#             img = make_square(img)\n        \n#         if self.dual_stream:\n#             w, h = img.size\n#             left = self.transform(img.crop((0, 0, w//2, h)))\n#             right = self.transform(img.crop((w//2, 0, w, h)))\n#             img_tensor = (left, right)\n#         else:\n#             img_tensor = self.transform(img)\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return img_tensor, torch.from_numpy(targets), torch.from_numpy(raw)\n\n# class SimpleModel(nn.Module):\n#     def __init__(self, dual_stream=False):\n#         super().__init__()\n#         self.dual_stream = dual_stream\n#         self.backbone = models.resnet18(weights=None)\n#         if os.path.exists(RESNET18_PATH):\n#             self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n#         else:\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n#         self.backbone.fc = nn.Identity()\n        \n#         feat_dim = 1024 if dual_stream else 512\n#         self.heads = nn.ModuleList([nn.Linear(feat_dim, 1) for _ in range(5)])\n    \n#     def forward(self, x):\n#         if self.dual_stream:\n#             left, right = x\n#             feat_l = self.backbone(left)\n#             feat_r = self.backbone(right)\n#             feats = torch.cat([feat_l, feat_r], dim=1)\n#         else:\n#             feats = self.backbone(x)\n        \n#         return torch.cat([head(feats) for head in self.heads], dim=1)\n\n# def quick_test(train_df, test_df, config, name):\n#     train_ds = TestDataset(train_df, config['img_size'], config['landscape'], \n#                            config['dual_stream'], config['aug'])\n#     test_ds = TestDataset(test_df, config['img_size'], config['landscape'], \n#                           config['dual_stream'], None)\n    \n#     train_loader = DataLoader(train_ds, BATCH_SIZE, shuffle=True, num_workers=2)\n#     test_loader = DataLoader(test_ds, BATCH_SIZE, num_workers=2)\n    \n#     model = SimpleModel(config['dual_stream']).to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n    \n#     for epoch in range(EPOCHS):\n#         model.train()\n#         for imgs, targets, _ in train_loader:\n#             if config['dual_stream']:\n#                 imgs = (imgs[0].to(DEVICE), imgs[1].to(DEVICE))\n#             else:\n#                 imgs = imgs.to(DEVICE)\n#             targets = targets.to(DEVICE)\n            \n#             optimizer.zero_grad()\n#             criterion(model(imgs), targets).backward()\n#             optimizer.step()\n        \n#         model.eval()\n#         preds, actuals = [], []\n#         with torch.no_grad():\n#             for imgs, _, raw in test_loader:\n#                 if config['dual_stream']:\n#                     imgs = (imgs[0].to(DEVICE), imgs[1].to(DEVICE))\n#                 else:\n#                     imgs = imgs.to(DEVICE)\n                \n#                 p = model(imgs).cpu().numpy()\n#                 for i in range(p.shape[0]):\n#                     for j in range(5):\n#                         preds.append(max(0, np.expm1(p[i, j])))\n#                         actuals.append(raw[i, j])\n        \n#         p, a = np.array(preds), np.array(actuals)\n#         r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n#         best_r2 = max(best_r2, r2)\n        \n#         print(f\"  E{epoch+1:2d}: R²={r2:.4f}\")\n    \n#     return best_r2\n\n# print(\"=\"*70)\n# print(\"ARCHITECTURE & AUGMENTATION COMPARISON TEST (12 epochs each)\")\n# print(\"=\"*70)\n\n# df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n# unique_imgs = df['image_path'].unique()\n# train_imgs, test_imgs = train_test_split(unique_imgs, test_size=20, random_state=SEED)\n# train_df = df[df['image_path'].isin(train_imgs)]\n# test_df = df[df['image_path'].isin(test_imgs)]\n\n# print(f\"Train: {len(train_imgs)} images | Test: {len(test_imgs)} images\\n\")\n\n# tests = [\n#     # Architecture tests\n#     (\"Baseline: Square 224\", {'img_size': 224, 'landscape': False, 'dual_stream': False, 'aug': None}),\n#     (\"Square 384\", {'img_size': 384, 'landscape': False, 'dual_stream': False, 'aug': None}),\n#     (\"Landscape 384x768\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': None}),\n#     (\"Landscape 512x1024\", {'img_size': 512, 'landscape': True, 'dual_stream': False, 'aug': None}),\n#     (\"Dual-stream 384\", {'img_size': 384, 'landscape': False, 'dual_stream': True, 'aug': None}),\n    \n#     # Augmentation tests (on best architecture)\n#     (\"Landscape 384 + Flip\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True}}),\n#     (\"Landscape 384 + Color\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'color': True}}),\n#     (\"Landscape 384 + Flip+Color\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True}}),\n#     (\"Landscape 384 + All Aug\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True, 'rotation': True}}),\n# ]\n\n# results = []\n# for name, config in tests:\n#     print(f\"\\n[Test] {name}\")\n#     print(f\"  Config: {config}\")\n#     best_r2 = quick_test(train_df, test_df, config, name)\n#     results.append((name, best_r2))\n#     print(f\"  Best R²: {best_r2:.4f}\")\n\n# print(\"\\n\" + \"=\"*70)\n# print(\"RESULTS SUMMARY (sorted by R²)\")\n# print(\"=\"*70)\n# for name, r2 in sorted(results, key=lambda x: -x[1]):\n#     marker = \"🏆\" if r2 == max(r[1] for r in results) else \"\"\n#     print(f\"  {name:30s}: R²={r2:.4f} {marker}\")\n\n# best = max(results, key=lambda x: x[1])\n# print(f\"\\n🏆 WINNER: {best[0]} with R²={best[1]:.4f}\")\n\n# ======================================================================\n# ARCHITECTURE & AUGMENTATION COMPARISON TEST (12 epochs each)\n# ======================================================================\n# Train: 337 images | Test: 20 images\n\n\n# [Test] Baseline: Square 224\n#   Config: {'img_size': 224, 'landscape': False, 'dual_stream': False, 'aug': None}\n#   E 1: R²=0.0141\n#   E 2: R²=0.4676\n#   E 3: R²=0.5273\n#   E 4: R²=0.4370\n#   E 5: R²=0.4693\n#   E 6: R²=0.4612\n#   E 7: R²=0.4923\n#   E 8: R²=0.4749\n#   E 9: R²=0.4330\n#   E10: R²=0.2959\n#   E11: R²=0.2087\n#   E12: R²=0.5479\n#   Best R²: 0.5479\n\n# [Test] Square 384\n#   Config: {'img_size': 384, 'landscape': False, 'dual_stream': False, 'aug': None}\n#   E 1: R²=-13.4916\n#   E 2: R²=0.2139\n#   E 3: R²=0.5416\n#   E 4: R²=-0.1458\n#   E 5: R²=0.5250\n#   E 6: R²=0.5512\n#   E 7: R²=0.6820\n#   E 8: R²=0.4717\n#   E 9: R²=0.4044\n#   E10: R²=0.5420\n#   E11: R²=0.5720\n#   E12: R²=0.5322\n#   Best R²: 0.6820\n\n# [Test] Landscape 384x768\n#   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': None}\n#   E 1: R²=-1.3346\n#   E 2: R²=0.2095\n#   E 3: R²=0.3876\n#   E 4: R²=0.3903\n#   E 5: R²=0.5477\n#   E 6: R²=0.6172\n#   E 7: R²=0.4714\n#   E 8: R²=0.6306\n#   E 9: R²=0.6717\n#   E10: R²=0.6163\n#   E11: R²=0.7425\n#   E12: R²=0.7170\n#   Best R²: 0.7425\n\n# [Test] Landscape 512x1024\n#   Config: {'img_size': 512, 'landscape': True, 'dual_stream': False, 'aug': None}\n#   E 1: R²=-0.3673\n#   E 2: R²=-1.8653\n#   E 3: R²=0.5998\n#   E 4: R²=0.6305\n#   E 5: R²=0.5517\n#   E 6: R²=0.6399\n#   E 7: R²=0.6527\n#   E 8: R²=0.2539\n#   E 9: R²=0.7096\n#   E10: R²=0.4878\n#   E11: R²=0.6878\n#   E12: R²=0.6325\n#   Best R²: 0.7096\n\n# [Test] Dual-stream 384\n#   Config: {'img_size': 384, 'landscape': False, 'dual_stream': True, 'aug': None}\n#   E 1: R²=-2.6740\n#   E 2: R²=0.4051\n#   E 3: R²=0.5869\n#   E 4: R²=0.4726\n#   E 5: R²=0.5200\n#   E 6: R²=0.6574\n#   E 7: R²=0.6566\n#   E 8: R²=0.4890\n#   E 9: R²=0.6343\n#   E10: R²=0.7171\n#   E11: R²=0.6038\n#   E12: R²=0.6707\n#   Best R²: 0.7171\n\n# [Test] Landscape 384 + Flip\n#   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True}}\n#   E 1: R²=-3.0365\n#   E 2: R²=-2.9119\n#   E 3: R²=0.5039\n#   E 4: R²=0.5201\n#   E 5: R²=0.3958\n#   E 6: R²=0.5676\n#   E 7: R²=0.6976\n#   E 8: R²=0.5710\n#   E 9: R²=0.6980\n#   E10: R²=0.6143\n#   E11: R²=0.6843\n#   E12: R²=0.6989\n#   Best R²: 0.6989\n\n# [Test] Landscape 384 + Color\n#   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'color': True}}\n#   E 1: R²=-0.3217\n#   E 2: R²=0.2835\n#   E 3: R²=0.5488\n#   E 4: R²=0.6192\n#   E 5: R²=0.6999\n#   E 6: R²=0.5934\n#   E 7: R²=0.6985\n#   E 8: R²=0.6564\n#   E 9: R²=0.6069\n#   E10: R²=0.6639\n#   E11: R²=0.6428\n#   E12: R²=0.6539\n#   Best R²: 0.6999\n\n# [Test] Landscape 384 + Flip+Color\n#   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True}}\n#   E 1: R²=-0.4727\n#   E 2: R²=-1.0523\n#   E 3: R²=0.5012\n#   E 4: R²=0.4431\n#   E 5: R²=-0.1885\n#   E 6: R²=0.6160\n#   E 7: R²=0.4828\n#   E 8: R²=0.4238\n#   E 9: R²=0.5072\n#   E10: R²=0.5959\n#   E11: R²=0.7662\n#   E12: R²=0.6021\n#   Best R²: 0.7662\n\n# [Test] Landscape 384 + All Aug\n#   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True, 'rotation': True}}\n#   E 1: R²=0.2875\n#   E 2: R²=-3.7258\n#   E 3: R²=0.4867\n#   E 4: R²=0.4755\n#   E 5: R²=-0.3217\n#   E 6: R²=-1.0330\n#   E 7: R²=0.0351\n#   E 8: R²=-0.6646\n#   E 9: R²=-1.1740\n#   E10: R²=0.4872\n#   E11: R²=-0.6493\n#   E12: R²=-1.0808\n#   Best R²: 0.4872\n\n# ======================================================================\n# RESULTS SUMMARY (sorted by R²)\n# ======================================================================\n#   Landscape 384 + Flip+Color    : R²=0.7662 🏆\n#   Landscape 384x768             : R²=0.7425 \n#   Dual-stream 384               : R²=0.7171 \n#   Landscape 512x1024            : R²=0.7096 \n#   Landscape 384 + Color         : R²=0.6999 \n#   Landscape 384 + Flip          : R²=0.6989 \n#   Square 384                    : R²=0.6820 \n#   Baseline: Square 224          : R²=0.5479 \n#   Landscape 384 + All Aug       : R²=0.4872 \n\n# 🏆 WINNER: Landscape 384 + Flip+Color with R²=0.7662\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import train_test_split\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if os.path.exists('/kaggle/working') else '.'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# BATCH_SIZE = 16\n# EPOCHS = 12\n# SEED = 42\n\n# random.seed(SEED)\n# np.random.seed(SEED)\n# torch.manual_seed(SEED)\n# torch.cuda.manual_seed_all(SEED)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n# class TestDataset(Dataset):\n#     def __init__(self, df, img_size, landscape=False, dual_stream=False, aug_config=None):\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n#         self.landscape = landscape\n#         self.dual_stream = dual_stream\n        \n#         if landscape:\n#             target_h, target_w = img_size, img_size * 2\n#         else:\n#             target_h, target_w = img_size, img_size\n        \n#         aug_list = [transforms.Resize((target_h, target_w))]\n        \n#         if aug_config:\n#             if aug_config.get('flip'):\n#                 aug_list.append(transforms.RandomHorizontalFlip(p=0.5))\n#             if aug_config.get('color'):\n#                 aug_list.append(transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1))\n#             if aug_config.get('rotation'):\n#                 aug_list.append(transforms.RandomRotation(15, expand=True, fill=0))\n#                 aug_list.append(transforms.Resize((target_h, target_w)))\n        \n#         aug_list.extend([\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.transform = transforms.Compose(aug_list)\n\n#     def __len__(self):\n#         return len(self.paths)\n\n#     def __getitem__(self, idx):\n#         path = self.paths[idx]\n#         img = Image.open(os.path.join(DATA_DIR, path)).convert('RGB')\n        \n#         if not self.landscape:\n#             img = make_square(img)\n        \n#         if self.dual_stream:\n#             w, h = img.size\n#             left = self.transform(img.crop((0, 0, w//2, h)))\n#             right = self.transform(img.crop((w//2, 0, w, h)))\n#             img_tensor = (left, right)\n#         else:\n#             img_tensor = self.transform(img)\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return img_tensor, torch.from_numpy(targets), torch.from_numpy(raw)\n\n# class SimpleModel(nn.Module):\n#     def __init__(self, dual_stream=False):\n#         super().__init__()\n#         self.dual_stream = dual_stream\n#         self.backbone = models.resnet18(weights=None)\n#         if os.path.exists(RESNET18_PATH):\n#             self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n#         else:\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n#         self.backbone.fc = nn.Identity()\n        \n#         feat_dim = 1024 if dual_stream else 512\n#         self.heads = nn.ModuleList([nn.Linear(feat_dim, 1) for _ in range(5)])\n    \n#     def forward(self, x):\n#         if self.dual_stream:\n#             left, right = x\n#             feat_l = self.backbone(left)\n#             feat_r = self.backbone(right)\n#             feats = torch.cat([feat_l, feat_r], dim=1)\n#         else:\n#             feats = self.backbone(x)\n        \n#         return torch.cat([head(feats) for head in self.heads], dim=1)\n\n# def quick_test(train_df, test_df, config, name):\n#     train_ds = TestDataset(train_df, config['img_size'], config['landscape'], \n#                            config['dual_stream'], config['aug'])\n#     test_ds = TestDataset(test_df, config['img_size'], config['landscape'], \n#                           config['dual_stream'], None)\n    \n#     train_loader = DataLoader(train_ds, BATCH_SIZE, shuffle=True, num_workers=2)\n#     test_loader = DataLoader(test_ds, BATCH_SIZE, num_workers=2)\n    \n#     model = SimpleModel(config['dual_stream']).to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n    \n#     for epoch in range(EPOCHS):\n#         model.train()\n#         for imgs, targets, _ in train_loader:\n#             if config['dual_stream']:\n#                 imgs = (imgs[0].to(DEVICE), imgs[1].to(DEVICE))\n#             else:\n#                 imgs = imgs.to(DEVICE)\n#             targets = targets.to(DEVICE)\n            \n#             optimizer.zero_grad()\n#             criterion(model(imgs), targets).backward()\n#             optimizer.step()\n        \n#         model.eval()\n#         preds, actuals = [], []\n#         with torch.no_grad():\n#             for imgs, _, raw in test_loader:\n#                 if config['dual_stream']:\n#                     imgs = (imgs[0].to(DEVICE), imgs[1].to(DEVICE))\n#                 else:\n#                     imgs = imgs.to(DEVICE)\n                \n#                 p = model(imgs).cpu().numpy()\n#                 for i in range(p.shape[0]):\n#                     for j in range(5):\n#                         preds.append(max(0, np.expm1(p[i, j])))\n#                         actuals.append(raw[i, j])\n        \n#         p, a = np.array(preds), np.array(actuals)\n#         r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n#         best_r2 = max(best_r2, r2)\n        \n#         print(f\"  E{epoch+1:2d}: R²={r2:.4f}\")\n    \n#     return best_r2\n\n# print(\"=\"*70)\n# print(\"ARCHITECTURE & AUGMENTATION COMPARISON TEST (12 epochs each)\")\n# print(\"=\"*70)\n\n# df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n# unique_imgs = df['image_path'].unique()\n# train_imgs, test_imgs = train_test_split(unique_imgs, test_size=20, random_state=SEED)\n# train_df = df[df['image_path'].isin(train_imgs)]\n# test_df = df[df['image_path'].isin(test_imgs)]\n\n# print(f\"Train: {len(train_imgs)} images | Test: {len(test_imgs)} images\\n\")\n\n# tests = [\n#     # Architecture tests\n#     (\"Baseline: Square 224\", {'img_size': 224, 'landscape': False, 'dual_stream': False, 'aug': None}),\n#     (\"Square 384\", {'img_size': 384, 'landscape': False, 'dual_stream': False, 'aug': None}),\n#     (\"Landscape 384x768\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': None}),\n#     (\"Landscape 512x1024\", {'img_size': 512, 'landscape': True, 'dual_stream': False, 'aug': None}),\n#     (\"Dual-stream 384\", {'img_size': 384, 'landscape': False, 'dual_stream': True, 'aug': None}),\n    \n#     # Augmentation tests (on best architecture)\n#     (\"Landscape 384 + Flip\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True}}),\n#     (\"Landscape 384 + Color\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'color': True}}),\n#     (\"Landscape 384 + Flip+Color\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True}}),\n#     (\"Landscape 384 + All Aug\", {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True, 'rotation': True}}),\n# ]\n\n# results = []\n# for name, config in tests:\n#     print(f\"\\n[Test] {name}\")\n#     print(f\"  Config: {config}\")\n#     best_r2 = quick_test(train_df, test_df, config, name)\n#     results.append((name, best_r2))\n#     print(f\"  Best R²: {best_r2:.4f}\")\n\n# print(\"\\n\" + \"=\"*70)\n# print(\"RESULTS SUMMARY (sorted by R²)\")\n# print(\"=\"*70)\n# for name, r2 in sorted(results, key=lambda x: -x[1]):\n#     marker = \"🏆\" if r2 == max(r[1] for r in results) else \"\"\n#     print(f\"  {name:30s}: R²={r2:.4f} {marker}\")\n\n# best = max(results, key=lambda x: x[1])\n# print(f\"\\n🏆 WINNER: {best[0]} with R²={best[1]:.4f}\")\n# print(\"\\nThis will help determine the best config for finalBioMassPred.py!\")\n# # ======================================================================\n# # ARCHITECTURE & AUGMENTATION COMPARISON TEST (12 epochs each)\n# # ======================================================================\n# # Train: 337 images | Test: 20 images\n\n\n# # [Test] Baseline: Square 224\n# #   Config: {'img_size': 224, 'landscape': False, 'dual_stream': False, 'aug': None}\n# #   E 1: R²=0.0037\n# #   E 2: R²=0.5574\n# #   E 3: R²=0.5367\n# #   E 4: R²=0.4378\n# #   E 5: R²=0.5077\n# #   E 6: R²=0.3406\n# #   E 7: R²=0.5593\n# #   E 8: R²=0.5904\n# #   E 9: R²=0.4836\n# #   E10: R²=0.5397\n# #   E11: R²=0.4293\n# #   E12: R²=0.5406\n# #   Best R²: 0.5904\n\n# # [Test] Square 384\n# #   Config: {'img_size': 384, 'landscape': False, 'dual_stream': False, 'aug': None}\n# #   E 1: R²=-15.6186\n# #   E 2: R²=-0.0218\n# #   E 3: R²=0.4872\n# #   E 4: R²=0.1830\n# #   E 5: R²=0.5491\n# #   E 6: R²=0.5722\n# #   E 7: R²=0.4772\n# #   E 8: R²=0.5865\n# #   E 9: R²=0.4904\n# #   E10: R²=0.5926\n# #   E11: R²=0.5461\n# #   E12: R²=0.4316\n# #   Best R²: 0.5926\n\n# # [Test] Landscape 384x768\n# #   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': None}\n# #   E 1: R²=-1.3613\n# #   E 2: R²=0.1976\n# #   E 3: R²=0.3963\n# #   E 4: R²=0.3825\n# #   E 5: R²=0.5588\n# #   E 6: R²=0.6038\n# #   E 7: R²=0.4442\n# #   E 8: R²=0.6310\n# #   E 9: R²=0.6684\n# #   E10: R²=0.4408\n# #   E11: R²=0.7305\n# #   E12: R²=0.7000\n# #   Best R²: 0.7305\n\n# # [Test] Landscape 512x1024\n# #   Config: {'img_size': 512, 'landscape': True, 'dual_stream': False, 'aug': None}\n# #   E 1: R²=-0.3456\n# #   E 2: R²=-1.9203\n# #   E 3: R²=0.6237\n# #   E 4: R²=0.6054\n# #   E 5: R²=0.4703\n# #   E 6: R²=0.6636\n# #   E 7: R²=0.6400\n# #   E 8: R²=-0.0023\n# #   E 9: R²=0.7024\n# #   E10: R²=0.4148\n# #   E11: R²=0.7251\n# #   E12: R²=0.6638\n# #   Best R²: 0.7251\n\n# # [Test] Dual-stream 384\n# #   Config: {'img_size': 384, 'landscape': False, 'dual_stream': True, 'aug': None}\n# #   E 1: R²=-0.5921\n# #   E 2: R²=0.3551\n# #   E 3: R²=0.3531\n# #   E 4: R²=0.4872\n# #   E 5: R²=0.5292\n# #   E 6: R²=0.6179\n# #   E 7: R²=0.6400\n# #   E 8: R²=0.4976\n# #   E 9: R²=0.5889\n# #   E10: R²=0.6864\n# #   E11: R²=0.6448\n# #   E12: R²=0.6592\n# #   Best R²: 0.6864\n\n# # [Test] Landscape 384 + Flip\n# #   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True}}\n# #   E 1: R²=-3.1514\n# #   E 2: R²=-2.9345\n# #   E 3: R²=0.5202\n# #   E 4: R²=0.5456\n# #   E 5: R²=0.4229\n# #   E 6: R²=0.5223\n# #   E 7: R²=0.6432\n# #   E 8: R²=0.5502\n# #   E 9: R²=0.7225\n# #   E10: R²=0.5875\n# #   E11: R²=0.6620\n# #   E12: R²=0.6589\n# #   Best R²: 0.7225\n\n# # [Test] Landscape 384 + Color\n# #   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'color': True}}\n# #   E 1: R²=-0.3029\n# #   E 2: R²=0.3249\n# #   E 3: R²=0.5817\n# #   E 4: R²=0.6519\n# #   E 5: R²=0.7248\n# #   E 6: R²=0.6484\n# #   E 7: R²=0.6369\n# #   E 8: R²=0.6287\n# #   E 9: R²=0.6262\n# #   E10: R²=0.7351\n# #   E11: R²=0.6249\n# #   E12: R²=0.6519\n# #   Best R²: 0.7351\n\n# # [Test] Landscape 384 + Flip+Color\n# #   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True}}\n# #   E 1: R²=-0.4920\n# #   E 2: R²=-0.4606\n# #   E 3: R²=0.5348\n# #   E 4: R²=0.4312\n# #   E 5: R²=-0.1095\n# #   E 6: R²=0.6132\n# #   E 7: R²=0.4740\n# #   E 8: R²=-0.2220\n# #   E 9: R²=0.5029\n# #   E10: R²=0.6186\n# #   E11: R²=0.7087\n# #   E12: R²=0.5777\n# #   Best R²: 0.7087\n\n# # [Test] Landscape 384 + All Aug\n# #   Config: {'img_size': 384, 'landscape': True, 'dual_stream': False, 'aug': {'flip': True, 'color': True, 'rotation': True}}\n# #   E 1: R²=0.3951\n# #   E 2: R²=-4.9772\n# #   E 3: R²=0.4345\n# #   E 4: R²=0.4683\n# #   E 5: R²=-0.6242\n# #   E 6: R²=-3.4358\n# #   E 7: R²=-0.3400\n# #   E 8: R²=-0.1692\n# #   E 9: R²=-1.0534\n# #   E10: R²=0.3875\n# #   E11: R²=0.4215\n# #   E12: R²=-2.5070\n# #   Best R²: 0.4683\n\n# # ======================================================================\n# # RESULTS SUMMARY (sorted by R²)\n# # ======================================================================\n# #   Landscape 384 + Color         : R²=0.7351 🏆\n# #   Landscape 384x768             : R²=0.7305 \n# #   Landscape 512x1024            : R²=0.7251 \n# #   Landscape 384 + Flip          : R²=0.7225 \n# #   Landscape 384 + Flip+Color    : R²=0.7087 \n# #   Dual-stream 384               : R²=0.6864 \n# #   Square 384                    : R²=0.5926 \n# #   Baseline: Square 224          : R²=0.5904 \n# #   Landscape 384 + All Aug       : R²=0.4683 \n\n# # 🏆 WINNER: Landscape 384 + Color with R²=0.7351\n\n# # This will help determine the best config for finalBioMassPred.py!","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# \"\"\"\n# Optuna Study: Test ONLY Augmentations\n# Using proven config from backbone_comparison_results.txt:\n# - ResNet18 (best backbone)\n# - Dual-stream (L/R split)\n# - Dropout 0.3\n# - IMG_SIZE 384\n# - SmoothL1 loss\n\n# Tests: rotation, flip_h, flip_v, brightness, contrast, saturation, hue, grayscale\n# \"\"\"\n\n# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n# import matplotlib.pyplot as plt\n# import sys\n\n# # ============ PROVEN CONFIG (DO NOT CHANGE) ============\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# IS_KAGGLE = os.path.exists('/kaggle/working')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if IS_KAGGLE else '.'\n\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# IMG_SIZE = 224      # Smaller for CPU speed (relative comparison still valid)\n# BATCH_SIZE = 16\n# EPOCHS = 3          # Reduced for faster testing (was 10)\n# LR = 1e-4           # Proven best\n# DROPOUT = 0.3       # Proven best (not 0.5)\n# SEED = 42           # Proven best seed\n\n# def set_seed(seed):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     if torch.cuda.is_available():\n#         torch.cuda.manual_seed_all(seed)\n\n# print(f\"Device: {DEVICE} | Kaggle: {IS_KAGGLE}\")\n# print(f\"Proven Config: IMG={IMG_SIZE}, LR={LR}, DROP={DROPOUT}, EPOCHS={EPOCHS}\")\n\n\n# # ============ DATASET ============\n# def make_square(img):\n#     w, h = img.size\n#     if w == h:\n#         return img\n#     max_dim = max(w, h)\n#     square = Image.new('RGB', (max_dim, max_dim), (0, 0, 0))\n#     square.paste(img, ((max_dim - w) // 2, (max_dim - h) // 2))\n#     return square\n\n\n# class AugmentationDataset(Dataset):\n#     def __init__(self, df, data_dir, aug_config=None, augment=True):\n#         self.data_dir = data_dir\n#         self.aug_config = aug_config or {}\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.image_paths = list(self.groups.groups.keys())\n        \n#         # Build augmentation transforms based on config\n#         aug_list = []\n        \n#         # Rotation (expand=True to avoid cropping)\n#         rotation = self.aug_config.get('rotation', 0)\n#         if rotation > 0:\n#             aug_list.append(transforms.RandomRotation(rotation, expand=True, fill=0))\n        \n#         # Resize to target size\n#         aug_list.append(transforms.Resize((IMG_SIZE, IMG_SIZE)))\n        \n#         # Flips\n#         flip_h = self.aug_config.get('flip_h', 0)\n#         if flip_h > 0:\n#             aug_list.append(transforms.RandomHorizontalFlip(p=flip_h))\n        \n#         flip_v = self.aug_config.get('flip_v', 0)\n#         if flip_v > 0:\n#             aug_list.append(transforms.RandomVerticalFlip(p=flip_v))\n        \n#         # Color augmentations\n#         brightness = self.aug_config.get('brightness', 0)\n#         contrast = self.aug_config.get('contrast', 0)\n#         saturation = self.aug_config.get('saturation', 0)\n#         hue = self.aug_config.get('hue', 0)\n        \n#         if brightness > 0 or contrast > 0 or saturation > 0 or hue > 0:\n#             aug_list.append(transforms.ColorJitter(\n#                 brightness=brightness,\n#                 contrast=contrast,\n#                 saturation=saturation,\n#                 hue=hue\n#             ))\n        \n#         # Grayscale\n#         grayscale = self.aug_config.get('grayscale', 0)\n#         if grayscale > 0:\n#             aug_list.append(transforms.RandomGrayscale(p=grayscale))\n        \n#         # Final transforms\n#         aug_list.extend([\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.transform_aug = transforms.Compose(aug_list)\n        \n#         # Normal transform (no augmentation)\n#         self.transform_normal = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         if self.augment:\n#             return len(self.image_paths) * 2  # Original + augmented\n#         return len(self.image_paths)\n\n#     def __getitem__(self, idx):\n#         if self.augment:\n#             real_idx = idx // 2\n#             is_aug = (idx % 2 == 1)\n#         else:\n#             real_idx = idx\n#             is_aug = False\n        \n#         img_path = self.image_paths[real_idx]\n#         img = Image.open(os.path.join(self.data_dir, img_path)).convert('RGB')\n#         img = make_square(img)\n        \n#         # Dual-stream: split L/R\n#         w, h = img.size\n#         left_img = img.crop((0, 0, w//2, h))\n#         right_img = img.crop((w//2, 0, w, h))\n        \n#         transform = self.transform_aug if is_aug else self.transform_normal\n#         left = transform(left_img)\n#         right = transform(right_img)\n        \n#         # Targets\n#         group = self.groups.get_group(img_path)\n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in group.iterrows():\n#             if row['target_name'] in TARGETS:\n#                 tidx = TARGETS.index(row['target_name'])\n#                 targets[tidx] = np.log1p(row['target'])\n#                 raw[tidx] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n\n# # ============ MODEL (Proven: ResNet18 + Dual-stream + Simple head) ============\n# RESNET18_PATH = \"/kaggle/input/resnet18/resnet18-f37072fd.pth\"\n\n# class ProvenModel(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n        \n#         # Load offline weights\n#         if os.path.exists(RESNET18_PATH):\n#             state = torch.load(RESNET18_PATH, map_location='cpu', weights_only=True)\n#             self.backbone.load_state_dict(state)\n#         else:\n#             # Fallback for local testing\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        \n#         self.backbone.fc = nn.Identity()\n        \n#         # Simple head (proven best)\n#         self.head = nn.Sequential(\n#             nn.Dropout(DROPOUT),\n#             nn.Linear(512 * 2, 5)  # Dual-stream: 512*2 -> 5 targets\n#         )\n    \n#     def forward(self, left, right):\n#         feat_l = self.backbone(left)\n#         feat_r = self.backbone(right)\n#         features = torch.cat([feat_l, feat_r], dim=1)\n#         return self.head(features)\n\n\n# # ============ TRAINING ============\n# def visualize_augmentations(train_df, data_dir, aug_configs):\n#     \"\"\"Visualize 1 original + 3 augmented samples for each augmentation type\"\"\"\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"VISUALIZING AUGMENTATIONS (1 original + 3 samples per augmentation)\")\n#     print(\"=\"*70)\n    \n#     # Get one sample image\n#     sample_path = train_df['image_path'].iloc[0]\n#     img = Image.open(os.path.join(data_dir, sample_path)).convert('RGB')\n#     img = make_square(img)\n    \n#     # Split L/R\n#     w, h = img.size\n#     left_img = img.crop((0, 0, w//2, h))\n    \n#     # Base transform (no augmentation)\n#     base_transform = transforms.Compose([\n#         transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#         transforms.ToTensor(),\n#     ])\n    \n#     # Denormalize for display\n#     def tensor_to_img(tensor):\n#         return tensor.permute(1, 2, 0).numpy()\n    \n#     # Create figure: rows = augmentation types, cols = [original, sample1, sample2, sample3]\n#     n_augs = len(aug_configs)\n#     fig, axes = plt.subplots(n_augs, 4, figsize=(12, n_augs * 2.5))\n#     fig.suptitle('Augmentation Preview: Original vs 3 Samples', fontsize=14, fontweight='bold')\n    \n#     for row_idx, (name, config) in enumerate(aug_configs):\n#         # Build augmentation transform\n#         aug_list = [transforms.Resize((IMG_SIZE, IMG_SIZE))]\n        \n#         # Add specific augmentation\n#         brightness = config.get('brightness', 0)\n#         contrast = config.get('contrast', 0)\n#         saturation = config.get('saturation', 0)\n#         hue = config.get('hue', 0)\n        \n#         if brightness > 0 or contrast > 0 or saturation > 0 or hue > 0:\n#             aug_list.append(transforms.ColorJitter(\n#                 brightness=brightness,\n#                 contrast=contrast,\n#                 saturation=saturation,\n#                 hue=hue\n#             ))\n        \n#         grayscale = config.get('grayscale', 0)\n#         if grayscale > 0:\n#             aug_list.append(transforms.RandomGrayscale(p=grayscale))\n        \n#         aug_list.append(transforms.ToTensor())\n#         aug_transform = transforms.Compose(aug_list)\n        \n#         # Column 0: Original\n#         ax = axes[row_idx, 0] if n_augs > 1 else axes[0]\n#         original = base_transform(left_img)\n#         ax.imshow(tensor_to_img(original))\n#         ax.set_title(f\"Original\", fontsize=9)\n#         ax.axis('off')\n        \n#         # Columns 1-3: Augmented samples\n#         for col_idx in range(1, 4):\n#             ax = axes[row_idx, col_idx] if n_augs > 1 else axes[col_idx]\n#             augmented = aug_transform(left_img)\n#             ax.imshow(tensor_to_img(augmented))\n#             ax.set_title(f\"{name} #{col_idx}\", fontsize=9)\n#             ax.axis('off')\n        \n#         # Row label\n#         if n_augs > 1:\n#             axes[row_idx, 0].set_ylabel(name, fontsize=10, fontweight='bold', rotation=0, \n#                                         ha='right', va='center', labelpad=40)\n    \n#     plt.tight_layout()\n#     plt.savefig('augmentation_preview.png', dpi=150, bbox_inches='tight')\n#     print(\"✓ Saved: augmentation_preview.png\")\n#     plt.close()\n\n\n# def train_with_aug(train_df, val_df, aug_config, name=\"\"):\n#     set_seed(SEED)\n    \n#     train_ds = AugmentationDataset(train_df, DATA_DIR, aug_config, augment=True)\n#     val_ds = AugmentationDataset(val_df, DATA_DIR, aug_config=None, augment=False)\n    \n#     train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\n#     val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, num_workers=2)\n    \n#     model = ProvenModel().to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n    \n#     for epoch in range(EPOCHS):\n#         # Train\n#         model.train()\n#         epoch_loss = 0\n#         for batch_idx, (left, right, targets, _) in enumerate(train_loader):\n#             left, right, targets = left.to(DEVICE), right.to(DEVICE), targets.to(DEVICE)\n#             optimizer.zero_grad()\n#             loss = criterion(model(left, right), targets)\n#             loss.backward()\n#             optimizer.step()\n#             epoch_loss += loss.item()\n            \n#             # Print progress every 10 batches\n#             if (batch_idx + 1) % 10 == 0:\n#                 print(f\"\\r{name[:20]:20s} Epoch {epoch+1}/{EPOCHS} Batch {batch_idx+1}/{len(train_loader)} Loss: {loss.item():.4f}\", end='', flush=True)\n        \n#         # Validate\n#         model.eval()\n#         all_preds, all_actuals = [], []\n#         with torch.no_grad():\n#             for left, right, targets, raw in val_loader:\n#                 preds = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#                 raw = raw.numpy()\n#                 for i in range(preds.shape[0]):\n#                     for j in range(5):\n#                         all_preds.append(max(0, np.expm1(preds[i, j])))\n#                         all_actuals.append(raw[i, j])\n        \n#         p, a = np.array(all_preds), np.array(all_actuals)\n#         r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n        \n#         if r2 > best_r2:\n#             best_r2 = r2\n        \n#         # Print epoch result\n#         print(f\"\\r{name[:20]:20s} Epoch {epoch+1}/{EPOCHS} completed - R²: {r2:.4f}\", flush=True)\n    \n#     return best_r2\n\n\n# # ============ MAIN ============\n# if __name__ == '__main__':\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"AUGMENTATION STUDY (Using Proven Config)\")\n#     print(\"ResNet18 + Dual-stream + Dropout 0.3 + IMG 384\")\n#     print(\"=\"*70)\n    \n#     # Load data\n#     train_full = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n    \n#     # Split (same as proven tests)\n#     gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n#     train_idx, val_idx = next(gss.split(train_full, groups=train_full['image_path']))\n#     train_df = train_full.iloc[train_idx]\n#     val_df = train_full.iloc[val_idx]\n    \n#     print(f\"Train: {train_df['image_path'].nunique()} images\")\n#     print(f\"Val: {val_df['image_path'].nunique()} images\")\n    \n#     results = []\n    \n#     # Baseline R² from previous tests (skip running)\n#     baseline_r2 = 0.55  # Approximate baseline without augmentation\n#     print(f\"Using baseline R²: {baseline_r2:.4f} (from previous tests)\")\n    \n#     # ============ VISUALIZE AUGMENTATIONS FIRST ============\n#     # Show representative samples for each augmentation type\n#     viz_configs = [\n#         (\"Brightness 0.2\", {'brightness': 0.2}),\n#         (\"Contrast 0.2\", {'contrast': 0.2}),\n#         (\"Saturation 0.2\", {'saturation': 0.2}),\n#     ]\n#     visualize_augmentations(train_df, DATA_DIR, viz_configs)\n    \n#     # ============ COLOR AUGMENTATION TESTS ============\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"COLOR AUGMENTATION TESTS (Each model trained on original + color variants)\")\n#     print(\"=\"*70)\n    \n#     tests = [\n#         # Brightness only (safe - simulates lighting)\n#         # (\"Brightness 0.1\", {'brightness': 0.1}),  # Already tested: R²=0.6568\n#         # (\"Brightness 0.2\", {'brightness': 0.2}),  # Already tested: R²=0.69 ⭐ BEST\n#         # (\"Brightness 0.3\", {'brightness': 0.3}),  # Already tested: R²=0.6479\n        \n#         # Contrast only (safe - doesn't change colors)\n#         (\"Contrast 0.1\", {'contrast': 0.1}),\n#         (\"Contrast 0.2\", {'contrast': 0.2}),\n        \n#         # Saturation only (moderate risk but worth testing)\n#         (\"Saturation 0.1\", {'saturation': 0.1}),\n#         (\"Saturation 0.2\", {'saturation': 0.2}),\n#     ]\n    \n#     for idx, (name, config) in enumerate(tests, 1):\n#         print(f\"\\n[Test {idx}/{len(tests)}] Starting: {name}\")\n#         r2 = train_with_aug(train_df, val_df, config, name)\n#         diff = r2 - baseline_r2\n#         marker = \"⭐\" if diff > 0.01 else (\"✓\" if diff > 0 else \"\")\n#         print(f\"[Test {idx}/{len(tests)}] {name:20s}: R²={r2:.4f} ({diff:+.4f}) {marker}\")\n#         results.append((name, r2))\n    \n#     # ============ COLOR COMBINATIONS ============\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"COLOR COMBINATION TESTS\")\n#     print(\"=\"*70)\n    \n#     combos = [\n#         (\"Bright+Contrast 0.1\", {'brightness': 0.1, 'contrast': 0.1}),\n#         (\"Bright+Contrast 0.2\", {'brightness': 0.2, 'contrast': 0.2}),\n#         (\"Sat+Contrast 0.2\", {'saturation': 0.2, 'contrast': 0.2}),  # NEW: Your suggestion!\n#         (\"Sat+Contrast 0.1\", {'saturation': 0.1, 'contrast': 0.1}),\n#         (\"Bright+Sat 0.2\", {'brightness': 0.2, 'saturation': 0.2}),\n#         (\"All Color 0.1\", {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.1}),\n#         (\"All Color 0.2\", {'brightness': 0.2, 'contrast': 0.2, 'saturation': 0.2}),\n#         (\"Contrast+Sat+Bright 0.2\", {'contrast': 0.2, 'saturation': 0.2, 'brightness': 0.1}),\n#     ]\n    \n#     for idx, (name, config) in enumerate(combos, 1):\n#         print(f\"\\n[Combo {idx}/{len(combos)}] Starting: {name}\")\n#         r2 = train_with_aug(train_df, val_df, config, name)\n#         diff = r2 - baseline_r2\n#         marker = \"⭐\" if diff > 0.01 else (\"✓\" if diff > 0 else \"\")\n#         print(f\"[Combo {idx}/{len(combos)}] {name:20s}: R²={r2:.4f} ({diff:+.4f}) {marker}\")\n#         results.append((name, r2))\n    \n#     # ============ RESULTS SUMMARY ============\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"RESULTS SUMMARY (sorted by R²)\")\n#     print(\"=\"*70)\n    \n#     for name, r2 in sorted(results, key=lambda x: -x[1]):\n#         diff = r2 - baseline_r2\n#         marker = \"🏆\" if r2 == max(r[1] for r in results) else \"\"\n#         print(f\"  {name:25s}: R²={r2:.4f} ({diff:+.4f}) {marker}\")\n    \n#     best = max(results, key=lambda x: x[1])\n#     print(f\"\\n🏆 BEST: {best[0]} with R²={best[1]:.4f}\")\n#     print(f\"   Improvement over baseline: {best[1] - baseline_r2:+.4f}\")\n    \n#     print(\"\\nDone!\")\n\n# 🏆 BEST: Bright+Contrast 0.1 with R²=0.6665\n\n# That was the final total best R² from all the tests (4 individual augmentations + 8 combinations = 12 tests total).\n\n# The summary showed:\n\n# Bright+Contrast 0.1: R²=0.6665 (+0.1165) 🏆\n# Saturation 0.2: R²=0.6637\n# Contrast 0.1: R²=0.6627\n# Sat+Contrast 0.1: R²=0.6624\n# (and others below)\n# So R²=0.6665 was your final best result from that optuna study.\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# \"\"\"\n# Optuna Study: Test ONLY Augmentations\n# Using proven config from backbone_comparison_results.txt:\n# - ResNet18 (best backbone)\n# - Dual-stream (L/R split)\n# - Dropout 0.3\n# - IMG_SIZE 384\n# - SmoothL1 loss\n\n# Tests: rotation, flip_h, flip_v, brightness, contrast, saturation, hue, grayscale\n# \"\"\"\n\n# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n# import matplotlib.pyplot as plt\n# import sys\n\n# # ============ PROVEN CONFIG (DO NOT CHANGE) ============\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# IS_KAGGLE = os.path.exists('/kaggle/working')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if IS_KAGGLE else '.'\n\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# IMG_SIZE = 224      # Smaller for CPU speed (relative comparison still valid)\n# BATCH_SIZE = 16\n# EPOCHS = 12         # Proper epochs for stable comparison (models stabilize 9-13)\n# LR = 1e-4           # Proven best\n# DROPOUT = 0.3       # Proven best (not 0.5)\n# SEED = 42           # Proven best seed\n\n# def set_seed(seed):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     if torch.cuda.is_available():\n#         torch.cuda.manual_seed_all(seed)\n\n# print(f\"Device: {DEVICE} | Kaggle: {IS_KAGGLE}\")\n# print(f\"Proven Config: IMG={IMG_SIZE}, LR={LR}, DROP={DROPOUT}, EPOCHS={EPOCHS}\")\n\n\n# # ============ DATASET ============\n# def make_square(img):\n#     w, h = img.size\n#     if w == h:\n#         return img\n#     max_dim = max(w, h)\n#     square = Image.new('RGB', (max_dim, max_dim), (0, 0, 0))\n#     square.paste(img, ((max_dim - w) // 2, (max_dim - h) // 2))\n#     return square\n\n\n# class AugmentationDataset(Dataset):\n#     def __init__(self, df, data_dir, aug_config=None, augment=True):\n#         self.data_dir = data_dir\n#         self.aug_config = aug_config or {}\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.image_paths = list(self.groups.groups.keys())\n        \n#         # Build augmentation transforms based on config\n#         aug_list = []\n        \n#         # Rotation (expand=True to avoid cropping)\n#         rotation = self.aug_config.get('rotation', 0)\n#         if rotation > 0:\n#             aug_list.append(transforms.RandomRotation(rotation, expand=True, fill=0))\n        \n#         # Resize to target size\n#         aug_list.append(transforms.Resize((IMG_SIZE, IMG_SIZE)))\n        \n#         # Flips\n#         flip_h = self.aug_config.get('flip_h', 0)\n#         if flip_h > 0:\n#             aug_list.append(transforms.RandomHorizontalFlip(p=flip_h))\n        \n#         flip_v = self.aug_config.get('flip_v', 0)\n#         if flip_v > 0:\n#             aug_list.append(transforms.RandomVerticalFlip(p=flip_v))\n        \n#         # Color augmentations\n#         brightness = self.aug_config.get('brightness', 0)\n#         contrast = self.aug_config.get('contrast', 0)\n#         saturation = self.aug_config.get('saturation', 0)\n#         hue = self.aug_config.get('hue', 0)\n        \n#         if brightness > 0 or contrast > 0 or saturation > 0 or hue > 0:\n#             aug_list.append(transforms.ColorJitter(\n#                 brightness=brightness,\n#                 contrast=contrast,\n#                 saturation=saturation,\n#                 hue=hue\n#             ))\n        \n#         # Grayscale\n#         grayscale = self.aug_config.get('grayscale', 0)\n#         if grayscale > 0:\n#             aug_list.append(transforms.RandomGrayscale(p=grayscale))\n        \n#         # Final transforms\n#         aug_list.extend([\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.transform_aug = transforms.Compose(aug_list)\n        \n#         # Normal transform (no augmentation)\n#         self.transform_normal = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         if self.augment:\n#             return len(self.image_paths) * 2  # Original + augmented\n#         return len(self.image_paths)\n\n#     def __getitem__(self, idx):\n#         if self.augment:\n#             real_idx = idx // 2\n#             is_aug = (idx % 2 == 1)\n#         else:\n#             real_idx = idx\n#             is_aug = False\n        \n#         img_path = self.image_paths[real_idx]\n#         img = Image.open(os.path.join(self.data_dir, img_path)).convert('RGB')\n#         img = make_square(img)\n        \n#         # Dual-stream: split L/R\n#         w, h = img.size\n#         left_img = img.crop((0, 0, w//2, h))\n#         right_img = img.crop((w//2, 0, w, h))\n        \n#         transform = self.transform_aug if is_aug else self.transform_normal\n#         left = transform(left_img)\n#         right = transform(right_img)\n        \n#         # Targets\n#         group = self.groups.get_group(img_path)\n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in group.iterrows():\n#             if row['target_name'] in TARGETS:\n#                 tidx = TARGETS.index(row['target_name'])\n#                 targets[tidx] = np.log1p(row['target'])\n#                 raw[tidx] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n\n# # ============ MODEL (Proven: ResNet18 + Dual-stream + Simple head) ============\n# RESNET18_PATH = \"/kaggle/input/resnet18/resnet18-f37072fd.pth\"\n\n# class ProvenModel(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n        \n#         # Load offline weights\n#         if os.path.exists(RESNET18_PATH):\n#             state = torch.load(RESNET18_PATH, map_location='cpu', weights_only=True)\n#             self.backbone.load_state_dict(state)\n#         else:\n#             # Fallback for local testing\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        \n#         self.backbone.fc = nn.Identity()\n        \n#         # Simple head (proven best)\n#         self.head = nn.Sequential(\n#             nn.Dropout(DROPOUT),\n#             nn.Linear(512 * 2, 5)  # Dual-stream: 512*2 -> 5 targets\n#         )\n    \n#     def forward(self, left, right):\n#         feat_l = self.backbone(left)\n#         feat_r = self.backbone(right)\n#         features = torch.cat([feat_l, feat_r], dim=1)\n#         return self.head(features)\n\n\n# # ============ TRAINING ============\n# def visualize_augmentations(train_df, data_dir, aug_configs):\n#     \"\"\"Visualize 1 original + 3 augmented samples for each augmentation type\"\"\"\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"VISUALIZING AUGMENTATIONS (1 original + 3 samples per augmentation)\")\n#     print(\"=\"*70)\n    \n#     # Get one sample image\n#     sample_path = train_df['image_path'].iloc[0]\n#     img = Image.open(os.path.join(data_dir, sample_path)).convert('RGB')\n#     img = make_square(img)\n    \n#     # Split L/R\n#     w, h = img.size\n#     left_img = img.crop((0, 0, w//2, h))\n    \n#     # Base transform (no augmentation)\n#     base_transform = transforms.Compose([\n#         transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#         transforms.ToTensor(),\n#     ])\n    \n#     # Denormalize for display\n#     def tensor_to_img(tensor):\n#         return tensor.permute(1, 2, 0).numpy()\n    \n#     # Create figure: rows = augmentation types, cols = [original, sample1, sample2, sample3]\n#     n_augs = len(aug_configs)\n#     fig, axes = plt.subplots(n_augs, 4, figsize=(12, n_augs * 2.5))\n#     fig.suptitle('Augmentation Preview: Original vs 3 Samples', fontsize=14, fontweight='bold')\n    \n#     for row_idx, (name, config) in enumerate(aug_configs):\n#         # Build augmentation transform\n#         aug_list = [transforms.Resize((IMG_SIZE, IMG_SIZE))]\n        \n#         # Add specific augmentation\n#         brightness = config.get('brightness', 0)\n#         contrast = config.get('contrast', 0)\n#         saturation = config.get('saturation', 0)\n#         hue = config.get('hue', 0)\n        \n#         if brightness > 0 or contrast > 0 or saturation > 0 or hue > 0:\n#             aug_list.append(transforms.ColorJitter(\n#                 brightness=brightness,\n#                 contrast=contrast,\n#                 saturation=saturation,\n#                 hue=hue\n#             ))\n        \n#         grayscale = config.get('grayscale', 0)\n#         if grayscale > 0:\n#             aug_list.append(transforms.RandomGrayscale(p=grayscale))\n        \n#         aug_list.append(transforms.ToTensor())\n#         aug_transform = transforms.Compose(aug_list)\n        \n#         # Column 0: Original\n#         ax = axes[row_idx, 0] if n_augs > 1 else axes[0]\n#         original = base_transform(left_img)\n#         ax.imshow(tensor_to_img(original))\n#         ax.set_title(f\"Original\", fontsize=9)\n#         ax.axis('off')\n        \n#         # Columns 1-3: Augmented samples\n#         for col_idx in range(1, 4):\n#             ax = axes[row_idx, col_idx] if n_augs > 1 else axes[col_idx]\n#             augmented = aug_transform(left_img)\n#             ax.imshow(tensor_to_img(augmented))\n#             ax.set_title(f\"{name} #{col_idx}\", fontsize=9)\n#             ax.axis('off')\n        \n#         # Row label\n#         if n_augs > 1:\n#             axes[row_idx, 0].set_ylabel(name, fontsize=10, fontweight='bold', rotation=0, \n#                                         ha='right', va='center', labelpad=40)\n    \n#     plt.tight_layout()\n#     plt.savefig('augmentation_preview.png', dpi=150, bbox_inches='tight')\n#     print(\"✓ Saved: augmentation_preview.png\")\n#     plt.close()\n\n\n# def train_with_aug(train_df, val_df, aug_config, name=\"\"):\n#     set_seed(SEED)\n    \n#     train_ds = AugmentationDataset(train_df, DATA_DIR, aug_config, augment=True)\n#     val_ds = AugmentationDataset(val_df, DATA_DIR, aug_config=None, augment=False)\n    \n#     train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\n#     val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, num_workers=2)\n    \n#     model = ProvenModel().to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n    \n#     for epoch in range(EPOCHS):\n#         # Train\n#         model.train()\n#         epoch_loss = 0\n#         for batch_idx, (left, right, targets, _) in enumerate(train_loader):\n#             left, right, targets = left.to(DEVICE), right.to(DEVICE), targets.to(DEVICE)\n#             optimizer.zero_grad()\n#             loss = criterion(model(left, right), targets)\n#             loss.backward()\n#             optimizer.step()\n#             epoch_loss += loss.item()\n            \n#             # Print progress every 10 batches\n#             if (batch_idx + 1) % 10 == 0:\n#                 print(f\"\\r{name[:20]:20s} Epoch {epoch+1}/{EPOCHS} Batch {batch_idx+1}/{len(train_loader)} Loss: {loss.item():.4f}\", end='', flush=True)\n        \n#         # Validate\n#         model.eval()\n#         all_preds, all_actuals = [], []\n#         with torch.no_grad():\n#             for left, right, targets, raw in val_loader:\n#                 preds = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#                 raw = raw.numpy()\n#                 for i in range(preds.shape[0]):\n#                     for j in range(5):\n#                         all_preds.append(max(0, np.expm1(preds[i, j])))\n#                         all_actuals.append(raw[i, j])\n        \n#         p, a = np.array(all_preds), np.array(all_actuals)\n#         r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n        \n#         if r2 > best_r2:\n#             best_r2 = r2\n        \n#         # Early stopping for clearly bad augmentations\n#         if epoch >= 5 and r2 < 0.3:\n#             print(f\"\\r{name[:20]:20s} Early stop at epoch {epoch+1} - poor performance (R²={r2:.4f})\", flush=True)\n#             break\n        \n#         # Print epoch result\n#         print(f\"\\r{name[:20]:20s} Epoch {epoch+1}/{EPOCHS} completed - R²: {r2:.4f} (best: {best_r2:.4f})\", flush=True)\n    \n#     return best_r2\n\n\n# # ============ MAIN ============\n# if __name__ == '__main__':\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"AUGMENTATION STUDY (Using Proven Config)\")\n#     print(\"ResNet18 + Dual-stream + Dropout 0.3 + IMG 384\")\n#     print(\"=\"*70)\n    \n#     # Load data\n#     train_full = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n    \n#     # Split (same as proven tests)\n#     gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n#     train_idx, val_idx = next(gss.split(train_full, groups=train_full['image_path']))\n#     train_df = train_full.iloc[train_idx]\n#     val_df = train_full.iloc[val_idx]\n    \n#     print(f\"Train: {train_df['image_path'].nunique()} images\")\n#     print(f\"Val: {val_df['image_path'].nunique()} images\")\n    \n#     results = []\n    \n#     # Baseline R² from previous tests (skip running)\n#     baseline_r2 = 0.55  # Approximate baseline without augmentation\n#     print(f\"Using baseline R²: {baseline_r2:.4f} (from previous tests)\")\n    \n#     # ============ VISUALIZE AUGMENTATIONS FIRST ============\n#     # Show representative samples for each augmentation type\n#     viz_configs = [\n#         (\"Brightness 0.2\", {'brightness': 0.2}),\n#         (\"Contrast 0.2\", {'contrast': 0.2}),\n#         (\"Saturation 0.2\", {'saturation': 0.2}),\n#     ]\n#     visualize_augmentations(train_df, DATA_DIR, viz_configs)\n    \n#     # ============ COLOR AUGMENTATION TESTS ============\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"COLOR AUGMENTATION TESTS (Each model trained on original + color variants)\")\n#     print(\"=\"*70)\n    \n#     tests = [\n#         # Brightness only (safe - simulates lighting)\n#         # (\"Brightness 0.1\", {'brightness': 0.1}),  # Already tested: R²=0.6568\n#         # (\"Brightness 0.2\", {'brightness': 0.2}),  # Already tested: R²=0.69 ⭐ BEST\n#         # (\"Brightness 0.3\", {'brightness': 0.3}),  # Already tested: R²=0.6479\n        \n#         # Contrast only (safe - doesn't change colors)\n#         (\"Contrast 0.1\", {'contrast': 0.1}),\n#         (\"Contrast 0.2\", {'contrast': 0.2}),\n        \n#         # Saturation only (moderate risk but worth testing)\n#         (\"Saturation 0.1\", {'saturation': 0.1}),\n#         (\"Saturation 0.2\", {'saturation': 0.2}),\n#     ]\n    \n#     for idx, (name, config) in enumerate(tests, 1):\n#         print(f\"\\n[Test {idx}/{len(tests)}] Starting: {name}\")\n#         r2 = train_with_aug(train_df, val_df, config, name)\n#         diff = r2 - baseline_r2\n#         marker = \"⭐\" if diff > 0.01 else (\"✓\" if diff > 0 else \"\")\n#         print(f\"[Test {idx}/{len(tests)}] {name:20s}: R²={r2:.4f} ({diff:+.4f}) {marker}\")\n#         results.append((name, r2))\n    \n#     # ============ COLOR COMBINATIONS ============\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"COLOR COMBINATION TESTS\")\n#     print(\"=\"*70)\n    \n#     combos = [\n#         (\"Bright+Contrast 0.1\", {'brightness': 0.1, 'contrast': 0.1}),\n#         (\"Bright+Contrast 0.2\", {'brightness': 0.2, 'contrast': 0.2}),\n#         (\"Sat+Contrast 0.2\", {'saturation': 0.2, 'contrast': 0.2}),  # NEW: Your suggestion!\n#         (\"Sat+Contrast 0.1\", {'saturation': 0.1, 'contrast': 0.1}),\n#         (\"Bright+Sat 0.2\", {'brightness': 0.2, 'saturation': 0.2}),\n#         (\"All Color 0.1\", {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.1}),\n#         (\"All Color 0.2\", {'brightness': 0.2, 'contrast': 0.2, 'saturation': 0.2}),\n#         (\"Contrast+Sat+Bright 0.2\", {'contrast': 0.2, 'saturation': 0.2, 'brightness': 0.1}),\n#     ]\n    \n#     for idx, (name, config) in enumerate(combos, 1):\n#         print(f\"\\n[Combo {idx}/{len(combos)}] Starting: {name}\")\n#         r2 = train_with_aug(train_df, val_df, config, name)\n#         diff = r2 - baseline_r2\n#         marker = \"⭐\" if diff > 0.01 else (\"✓\" if diff > 0 else \"\")\n#         print(f\"[Combo {idx}/{len(combos)}] {name:20s}: R²={r2:.4f} ({diff:+.4f}) {marker}\")\n#         results.append((name, r2))\n    \n#     # ============ RESULTS SUMMARY ============\n#     print(\"\\n\" + \"=\"*70)\n#     print(\"RESULTS SUMMARY (sorted by R²)\")\n#     print(\"=\"*70)\n    \n#     for name, r2 in sorted(results, key=lambda x: -x[1]):\n#         diff = r2 - baseline_r2\n#         marker = \"🏆\" if r2 == max(r[1] for r in results) else \"\"\n#         print(f\"  {name:25s}: R²={r2:.4f} ({diff:+.4f}) {marker}\")\n    \n#     best = max(results, key=lambda x: x[1])\n#     print(f\"\\n🏆 BEST: {best[0]} with R²={best[1]:.4f}\")\n#     print(f\"   Improvement over baseline: {best[1] - baseline_r2:+.4f}\")\n    \n#     print(\"\\nDone!\")\n# # Device: cuda | Kaggle: True\n# # Proven Config: IMG=224, LR=0.0001, DROP=0.3, EPOCHS=12\n\n# # ======================================================================\n# # AUGMENTATION STUDY (Using Proven Config)\n# # ResNet18 + Dual-stream + Dropout 0.3 + IMG 384\n# # ======================================================================\n# # Train: 285 images\n# # Val: 72 images\n# # Using baseline R²: 0.5500 (from previous tests)\n\n# # ======================================================================\n# # VISUALIZING AUGMENTATIONS (1 original + 3 samples per augmentation)\n# # ======================================================================\n# # ✓ Saved: augmentation_preview.png\n\n# # ======================================================================\n# # COLOR AUGMENTATION TESTS (Each model trained on original + color variants)\n# # ======================================================================\n\n# # [Test 1/4] Starting: Contrast 0.1\n# # Contrast 0.1         Epoch 1/12 completed - R²: 0.2230 (best: 0.2230)\n# # Contrast 0.1         Epoch 2/12 completed - R²: 0.4722 (best: 0.4722)\n# # Contrast 0.1         Epoch 3/12 completed - R²: 0.5391 (best: 0.5391)\n# # Contrast 0.1         Epoch 4/12 completed - R²: 0.6544 (best: 0.6544)\n# # Contrast 0.1         Epoch 5/12 completed - R²: 0.5995 (best: 0.6544)\n# # Contrast 0.1         Epoch 6/12 completed - R²: 0.5924 (best: 0.6544)\n# # Contrast 0.1         Epoch 7/12 completed - R²: 0.5780 (best: 0.6544)\n# # Contrast 0.1         Epoch 8/12 completed - R²: 0.6101 (best: 0.6544)\n# # Contrast 0.1         Epoch 9/12 completed - R²: 0.5870 (best: 0.6544)\n# # Contrast 0.1         Epoch 10/12 completed - R²: 0.6297 (best: 0.6544)\n# # Contrast 0.1         Epoch 11/12 completed - R²: 0.6463 (best: 0.6544)\n# # Contrast 0.1         Epoch 12/12 completed - R²: 0.6627 (best: 0.6627)\n# # [Test 1/4] Contrast 0.1        : R²=0.6627 (+0.1127) ⭐\n\n# # [Test 2/4] Starting: Contrast 0.2\n# # Contrast 0.2         Epoch 1/12 completed - R²: 0.0989 (best: 0.0989)\n# # Contrast 0.2         Epoch 2/12 completed - R²: 0.5389 (best: 0.5389)\n# # Contrast 0.2         Epoch 3/12 completed - R²: 0.5649 (best: 0.5649)\n# # Contrast 0.2         Epoch 4/12 completed - R²: 0.6323 (best: 0.6323)\n# # Contrast 0.2         Epoch 5/12 completed - R²: 0.5908 (best: 0.6323)\n# # Contrast 0.2         Epoch 6/12 completed - R²: 0.5644 (best: 0.6323)\n# # Contrast 0.2         Epoch 7/12 completed - R²: 0.5640 (best: 0.6323)\n# # Contrast 0.2         Epoch 8/12 completed - R²: 0.5935 (best: 0.6323)\n# # Contrast 0.2         Epoch 9/12 completed - R²: 0.6258 (best: 0.6323)\n# # Contrast 0.2         Epoch 10/12 completed - R²: 0.5997 (best: 0.6323)\n# # Contrast 0.2         Epoch 11/12 completed - R²: 0.6307 (best: 0.6323)\n# # Contrast 0.2         Epoch 12/12 completed - R²: 0.6597 (best: 0.6597)\n# # [Test 2/4] Contrast 0.2        : R²=0.6597 (+0.1097) ⭐\n\n# # [Test 3/4] Starting: Saturation 0.1\n# # Saturation 0.1       Epoch 1/12 completed - R²: 0.4012 (best: 0.4012)\n# # Saturation 0.1       Epoch 2/12 completed - R²: 0.5317 (best: 0.5317)\n# # Saturation 0.1       Epoch 3/12 completed - R²: 0.5905 (best: 0.5905)\n# # Saturation 0.1       Epoch 4/12 completed - R²: 0.6455 (best: 0.6455)\n# # Saturation 0.1       Epoch 5/12 completed - R²: 0.5879 (best: 0.6455)\n# # Saturation 0.1       Epoch 6/12 completed - R²: 0.5527 (best: 0.6455)\n# # Saturation 0.1       Epoch 7/12 completed - R²: 0.6447 (best: 0.6455)\n# # Saturation 0.1       Epoch 8/12 completed - R²: 0.6060 (best: 0.6455)\n# # Saturation 0.1       Epoch 9/12 completed - R²: 0.6115 (best: 0.6455)\n# # Saturation 0.1       Epoch 10/12 completed - R²: 0.6183 (best: 0.6455)\n# # Saturation 0.1       Epoch 11/12 completed - R²: 0.6292 (best: 0.6455)\n# # Saturation 0.1       Epoch 12/12 completed - R²: 0.6211 (best: 0.6455)\n# # [Test 3/4] Saturation 0.1      : R²=0.6455 (+0.0955) ⭐\n\n# # [Test 4/4] Starting: Saturation 0.2\n# # Saturation 0.2       Epoch 1/12 completed - R²: 0.3385 (best: 0.3385)\n# # Saturation 0.2       Epoch 2/12 completed - R²: 0.5453 (best: 0.5453)\n# # Saturation 0.2       Epoch 3/12 completed - R²: 0.5545 (best: 0.5545)\n# # Saturation 0.2       Epoch 4/12 completed - R²: 0.6637 (best: 0.6637)\n# # Saturation 0.2       Epoch 5/12 completed - R²: 0.6009 (best: 0.6637)\n# # Saturation 0.2       Epoch 6/12 completed - R²: 0.5878 (best: 0.6637)\n# # Saturation 0.2       Epoch 7/12 completed - R²: 0.5825 (best: 0.6637)\n# # Saturation 0.2       Epoch 8/12 completed - R²: 0.5787 (best: 0.6637)\n# # Saturation 0.2       Epoch 9/12 completed - R²: 0.5888 (best: 0.6637)\n# # Saturation 0.2       Epoch 10/12 completed - R²: 0.6523 (best: 0.6637)\n# # Saturation 0.2       Epoch 11/12 completed - R²: 0.6347 (best: 0.6637)\n# # Saturation 0.2       Epoch 12/12 completed - R²: 0.6496 (best: 0.6637)\n# # [Test 4/4] Saturation 0.2      : R²=0.6637 (+0.1137) ⭐\n\n# # ======================================================================\n# # COLOR COMBINATION TESTS\n# # ======================================================================\n\n# # [Combo 1/8] Starting: Bright+Contrast 0.1\n# # Bright+Contrast 0.1  Epoch 1/12 completed - R²: 0.3850 (best: 0.3850)\n# # Bright+Contrast 0.1  Epoch 2/12 completed - R²: 0.5200 (best: 0.5200)\n# # Bright+Contrast 0.1  Epoch 3/12 completed - R²: 0.5814 (best: 0.5814)\n# # Bright+Contrast 0.1  Epoch 4/12 completed - R²: 0.6182 (best: 0.6182)\n# # Bright+Contrast 0.1  Epoch 5/12 completed - R²: 0.5992 (best: 0.6182)\n# # Bright+Contrast 0.1  Epoch 6/12 completed - R²: 0.5711 (best: 0.6182)\n# # Bright+Contrast 0.1  Epoch 7/12 completed - R²: 0.6665 (best: 0.6665)\n# # Bright+Contrast 0.1  Epoch 8/12 completed - R²: 0.6041 (best: 0.6665)\n# # Bright+Contrast 0.1  Epoch 9/12 completed - R²: 0.6012 (best: 0.6665)\n# # Bright+Contrast 0.1  Epoch 10/12 completed - R²: 0.6212 (best: 0.6665)\n# # Bright+Contrast 0.1  Epoch 11/12 completed - R²: 0.6258 (best: 0.6665)\n# # Bright+Contrast 0.1  Epoch 12/12 completed - R²: 0.5989 (best: 0.6665)\n# # [Combo 1/8] Bright+Contrast 0.1 : R²=0.6665 (+0.1165) ⭐\n\n# # [Combo 2/8] Starting: Bright+Contrast 0.2\n# # Bright+Contrast 0.2  Epoch 1/12 completed - R²: 0.2674 (best: 0.2674)\n# # Bright+Contrast 0.2  Epoch 2/12 completed - R²: 0.4809 (best: 0.4809)\n# # Bright+Contrast 0.2  Epoch 3/12 completed - R²: 0.5719 (best: 0.5719)\n# # Bright+Contrast 0.2  Epoch 4/12 completed - R²: 0.6073 (best: 0.6073)\n# # Bright+Contrast 0.2  Epoch 5/12 completed - R²: 0.5557 (best: 0.6073)\n# # Bright+Contrast 0.2  Epoch 6/12 completed - R²: 0.5330 (best: 0.6073)\n# # Bright+Contrast 0.2  Epoch 7/12 completed - R²: 0.6122 (best: 0.6122)\n# # Bright+Contrast 0.2  Epoch 8/12 completed - R²: 0.5728 (best: 0.6122)\n# # Bright+Contrast 0.2  Epoch 9/12 completed - R²: 0.5920 (best: 0.6122)\n# # Bright+Contrast 0.2  Epoch 10/12 completed - R²: 0.6034 (best: 0.6122)\n# # Bright+Contrast 0.2  Epoch 11/12 completed - R²: 0.6207 (best: 0.6207)\n# # Bright+Contrast 0.2  Epoch 12/12 completed - R²: 0.6364 (best: 0.6364)\n# # [Combo 2/8] Bright+Contrast 0.2 : R²=0.6364 (+0.0864) ⭐\n\n# # [Combo 3/8] Starting: Sat+Contrast 0.2\n# # Sat+Contrast 0.2     Epoch 1/12 completed - R²: 0.3289 (best: 0.3289)\n# # Sat+Contrast 0.2     Epoch 2/12 completed - R²: 0.5512 (best: 0.5512)\n# # Sat+Contrast 0.2     Epoch 3/12 completed - R²: 0.4760 (best: 0.5512)\n# # Sat+Contrast 0.2     Epoch 4/12 completed - R²: 0.6048 (best: 0.6048)\n# # Sat+Contrast 0.2     Epoch 5/12 completed - R²: 0.6204 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 6/12 completed - R²: 0.5531 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 7/12 completed - R²: 0.5670 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 8/12 completed - R²: 0.5714 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 9/12 completed - R²: 0.5490 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 10/12 completed - R²: 0.5852 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 11/12 completed - R²: 0.6098 (best: 0.6204)\n# # Sat+Contrast 0.2     Epoch 12/12 completed - R²: 0.5928 (best: 0.6204)\n# # [Combo 3/8] Sat+Contrast 0.2    : R²=0.6204 (+0.0704) ⭐\n\n# # [Combo 4/8] Starting: Sat+Contrast 0.1\n# # Sat+Contrast 0.1     Epoch 1/12 completed - R²: 0.0958 (best: 0.0958)\n# # Sat+Contrast 0.1     Epoch 2/12 completed - R²: 0.5207 (best: 0.5207)\n# # Sat+Contrast 0.1     Epoch 3/12 completed - R²: 0.5030 (best: 0.5207)\n# # Sat+Contrast 0.1     Epoch 4/12 completed - R²: 0.6130 (best: 0.6130)\n# # Sat+Contrast 0.1     Epoch 5/12 completed - R²: 0.5555 (best: 0.6130)\n# # Sat+Contrast 0.1     Epoch 6/12 completed - R²: 0.5695 (best: 0.6130)\n# # Sat+Contrast 0.1     Epoch 7/12 completed - R²: 0.5713 (best: 0.6130)\n# # Sat+Contrast 0.1     Epoch 8/12 completed - R²: 0.5822 (best: 0.6130)\n# # Sat+Contrast 0.1     Epoch 9/12 completed - R²: 0.5849 (best: 0.6130)\n# # Sat+Contrast 0.1     Epoch 10/12 completed - R²: 0.6624 (best: 0.6624)\n# # Sat+Contrast 0.1     Epoch 11/12 completed - R²: 0.6362 (best: 0.6624)\n# # Sat+Contrast 0.1     Epoch 12/12 completed - R²: 0.6195 (best: 0.6624)\n# # [Combo 4/8] Sat+Contrast 0.1    : R²=0.6624 (+0.1124) ⭐\n\n# # [Combo 5/8] Starting: Bright+Sat 0.2\n# # Bright+Sat 0.2       Epoch 1/12 completed - R²: 0.0516 (best: 0.0516)\n# # Bright+Sat 0.2       Epoch 2/12 completed - R²: 0.5500 (best: 0.5500)\n# # Bright+Sat 0.2       Epoch 3/12 completed - R²: 0.5924 (best: 0.5924)\n# # Bright+Sat 0.2       Epoch 4/12 completed - R²: 0.6395 (best: 0.6395)\n# # Bright+Sat 0.2       Epoch 5/12 completed - R²: 0.5546 (best: 0.6395)\n# # Bright+Sat 0.2       Epoch 6/12 completed - R²: 0.5767 (best: 0.6395)\n# # Bright+Sat 0.2       Epoch 7/12 completed - R²: 0.6547 (best: 0.6547)\n# # Bright+Sat 0.2       Epoch 8/12 completed - R²: 0.5984 (best: 0.6547)\n# # Bright+Sat 0.2       Epoch 9/12 completed - R²: 0.6387 (best: 0.6547)\n# # Bright+Sat 0.2       Epoch 10/12 completed - R²: 0.6445 (best: 0.6547)\n# # Bright+Sat 0.2       Epoch 11/12 completed - R²: 0.6295 (best: 0.6547)\n# # Bright+Sat 0.2       Epoch 12/12 completed - R²: 0.6360 (best: 0.6547)\n# # [Combo 5/8] Bright+Sat 0.2      : R²=0.6547 (+0.1047) ⭐\n\n# # [Combo 6/8] Starting: All Color 0.1\n# # All Color 0.1        Epoch 1/12 completed - R²: 0.2384 (best: 0.2384)\n# # All Color 0.1        Epoch 2/12 completed - R²: 0.4997 (best: 0.4997)\n# # All Color 0.1        Epoch 3/12 completed - R²: 0.5501 (best: 0.5501)\n# # All Color 0.1        Epoch 4/12 completed - R²: 0.5872 (best: 0.5872)\n# # All Color 0.1        Epoch 5/12 completed - R²: 0.5838 (best: 0.5872)\n# # All Color 0.1        Epoch 6/12 completed - R²: 0.5675 (best: 0.5872)\n# # All Color 0.1        Epoch 7/12 completed - R²: 0.5555 (best: 0.5872)\n# # All Color 0.1        Epoch 8/12 completed - R²: 0.6089 (best: 0.6089)\n# # All Color 0.1        Epoch 9/12 completed - R²: 0.5536 (best: 0.6089)\n# # All Color 0.1        Epoch 10/12 completed - R²: 0.6091 (best: 0.6091)\n# # All Color 0.1        Epoch 11/12 completed - R²: 0.6490 (best: 0.6490)\n# # All Color 0.1        Epoch 12/12 completed - R²: 0.6099 (best: 0.6490)\n# # [Combo 6/8] All Color 0.1       : R²=0.6490 (+0.0990) ⭐\n\n# # [Combo 7/8] Starting: All Color 0.2\n# # All Color 0.2        Epoch 1/12 completed - R²: 0.3871 (best: 0.3871)\n# # All Color 0.2        Epoch 2/12 completed - R²: 0.5849 (best: 0.5849)\n# # All Color 0.2        Epoch 3/12 completed - R²: 0.5513 (best: 0.5849)\n# # All Color 0.2        Epoch 4/12 completed - R²: 0.5510 (best: 0.5849)\n# # All Color 0.2        Epoch 5/12 completed - R²: 0.5793 (best: 0.5849)\n# # All Color 0.2        Epoch 6/12 completed - R²: 0.5709 (best: 0.5849)\n# # All Color 0.2        Epoch 7/12 completed - R²: 0.6036 (best: 0.6036)\n# # All Color 0.2        Epoch 8/12 completed - R²: 0.5355 (best: 0.6036)\n# # All Color 0.2        Epoch 9/12 completed - R²: 0.5782 (best: 0.6036)\n# # All Color 0.2        Epoch 10/12 completed - R²: 0.6046 (best: 0.6046)\n# # All Color 0.2        Epoch 11/12 completed - R²: 0.6210 (best: 0.6210)\n# # All Color 0.2        Epoch 12/12 completed - R²: 0.5846 (best: 0.6210)\n# # [Combo 7/8] All Color 0.2       : R²=0.6210 (+0.0710) ⭐\n\n# # [Combo 8/8] Starting: Contrast+Sat+Bright 0.2\n# # Contrast+Sat+Bright  Epoch 1/12 completed - R²: 0.2595 (best: 0.2595)\n# # Contrast+Sat+Bright  Epoch 2/12 completed - R²: 0.5282 (best: 0.5282)\n# # Contrast+Sat+Bright  Epoch 3/12 completed - R²: 0.5621 (best: 0.5621)\n# # Contrast+Sat+Bright  Epoch 4/12 completed - R²: 0.6460 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 5/12 completed - R²: 0.6235 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 6/12 completed - R²: 0.6009 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 7/12 completed - R²: 0.6321 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 8/12 completed - R²: 0.5892 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 9/12 completed - R²: 0.5860 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 10/12 completed - R²: 0.5852 (best: 0.6460)\n# # Contrast+Sat+Bright  Epoch 11/12 completed - R²: 0.6502 (best: 0.6502)\n# # Contrast+Sat+Bright  Epoch 12/12 completed - R²: 0.5845 (best: 0.6502)\n# # [Combo 8/8] Contrast+Sat+Bright 0.2: R²=0.6502 (+0.1002) ⭐\n\n# # ======================================================================\n# # RESULTS SUMMARY (sorted by R²)\n# # ======================================================================\n# #   Bright+Contrast 0.1      : R²=0.6665 (+0.1165) 🏆\n# #   Saturation 0.2           : R²=0.6637 (+0.1137) \n# #   Contrast 0.1             : R²=0.6627 (+0.1127) \n# #   Sat+Contrast 0.1         : R²=0.6624 (+0.1124) \n# #   Contrast 0.2             : R²=0.6597 (+0.1097) \n# #   Bright+Sat 0.2           : R²=0.6547 (+0.1047) \n# #   Contrast+Sat+Bright 0.2  : R²=0.6502 (+0.1002) \n# #   All Color 0.1            : R²=0.6490 (+0.0990) \n# #   Saturation 0.1           : R²=0.6455 (+0.0955) \n# #   Bright+Contrast 0.2      : R²=0.6364 (+0.0864) \n# #   All Color 0.2            : R²=0.6210 (+0.0710) \n# #   Sat+Contrast 0.2         : R²=0.6204 (+0.0704) \n\n# # 🏆 BEST: Bright+Contrast 0.1 with R²=0.6665\n# #    Improvement over baseline: +0.1165\n\n# # Done!","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# DATA_DIR = '/kaggle/input/csiro-biomass'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# IMG_SIZE = 224\n# BATCH_SIZE = 16\n# EPOCHS = 12\n# LR = 1e-4\n# DROPOUT = 0.3\n# SEED = 42\n\n# random.seed(SEED)\n# np.random.seed(SEED)\n# torch.manual_seed(SEED)\n# torch.cuda.manual_seed_all(SEED)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n# class AugDataset(Dataset):\n#     def __init__(self, df, augment=True):\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n        \n#         self.aug = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.2),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.normal = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         return len(self.paths) * 2 if self.augment else len(self.paths)\n\n#     def __getitem__(self, idx):\n#         real_idx = idx // 2 if self.augment else idx\n#         is_aug = (idx % 2 == 1) if self.augment else False\n        \n#         path = self.paths[real_idx]\n#         img = make_square(Image.open(os.path.join(DATA_DIR, path)).convert('RGB'))\n#         w, h = img.size\n        \n#         t = self.aug if is_aug else self.normal\n#         left = t(img.crop((0, 0, w//2, h)))\n#         right = t(img.crop((w//2, 0, w, h)))\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n# class Model(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n#         state = torch.load(RESNET18_PATH, map_location='cpu', weights_only=True)\n#         self.backbone.load_state_dict(state)\n#         self.backbone.fc = nn.Identity()\n#         self.head = nn.Sequential(nn.Dropout(DROPOUT), nn.Linear(1024, 5))\n    \n#     def forward(self, left, right):\n#         return self.head(torch.cat([self.backbone(left), self.backbone(right)], dim=1))\n\n# print(f\"Testing: Bright 0.1 + Contrast 0.1 + Saturation 0.2\")\n# print(f\"Device: {DEVICE} | Epochs: {EPOCHS}\")\n\n# df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n# gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n# train_idx, val_idx = next(gss.split(df, groups=df['image_path']))\n\n# train_loader = DataLoader(AugDataset(df.iloc[train_idx], True), BATCH_SIZE, shuffle=True, num_workers=2)\n# val_loader = DataLoader(AugDataset(df.iloc[val_idx], False), BATCH_SIZE, num_workers=2)\n\n# model = Model().to(DEVICE)\n# optimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n# criterion = nn.SmoothL1Loss()\n\n# best_r2 = -999\n# for epoch in range(EPOCHS):\n#     model.train()\n#     for left, right, targets, _ in train_loader:\n#         optimizer.zero_grad()\n#         criterion(model(left.to(DEVICE), right.to(DEVICE)), targets.to(DEVICE)).backward()\n#         optimizer.step()\n    \n#     model.eval()\n#     preds, actuals = [], []\n#     with torch.no_grad():\n#         for left, right, _, raw in val_loader:\n#             p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#             for i in range(p.shape[0]):\n#                 for j in range(5):\n#                     preds.append(max(0, np.expm1(p[i, j])))\n#                     actuals.append(raw[i, j])\n    \n#     p, a = np.array(preds), np.array(actuals)\n#     r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n#     best_r2 = max(best_r2, r2)\n#     print(f\"Epoch {epoch+1}/{EPOCHS} - R²: {r2:.4f} (best: {best_r2:.4f})\")\n\n# print(f\"\\n🏆 Final Best R²: {best_r2:.4f}\")\n# # Testing: Bright 0.1 + Contrast 0.1 + Saturation 0.2\n# # Device: cuda | Epochs: 12\n# # Epoch 1/12 - R²: 0.3985 (best: 0.3985)\n# # Epoch 2/12 - R²: 0.5464 (best: 0.5464)\n# # Epoch 3/12 - R²: 0.5716 (best: 0.5716)\n# # Epoch 4/12 - R²: 0.6637 (best: 0.6637)\n# # Epoch 5/12 - R²: 0.5963 (best: 0.6637)\n# # Epoch 6/12 - R²: 0.6113 (best: 0.6637)\n# # Epoch 7/12 - R²: 0.6304 (best: 0.6637)\n# # Epoch 8/12 - R²: 0.6169 (best: 0.6637)\n# # Epoch 9/12 - R²: 0.6007 (best: 0.6637)\n# # Epoch 10/12 - R²: 0.5866 (best: 0.6637)\n# # Epoch 11/12 - R²: 0.6213 (best: 0.6637)\n# # Epoch 12/12 - R²: 0.6542 (best: 0.6637)\n\n# # 🏆 Final Best R²: 0.6637","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# DATA_DIR = '/kaggle/input/csiro-biomass'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# IMG_SIZE = 224\n# BATCH_SIZE = 16\n# EPOCHS = 20\n# LR = 1e-4\n# DROPOUT = 0.3\n# SEED = 42\n\n# random.seed(SEED)\n# np.random.seed(SEED)\n# torch.manual_seed(SEED)\n# torch.cuda.manual_seed_all(SEED)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n# class AllAugDataset(Dataset):\n#     def __init__(self, df, augment=True):\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n        \n#         # ALL augmentations combined\n#         self.aug = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.RandomRotation(15, expand=True, fill=0),\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),  # Resize again after rotation\n#             transforms.RandomHorizontalFlip(p=0.5),\n#             transforms.RandomVerticalFlip(p=0.5),\n#             transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.2, hue=0.05),\n#             transforms.RandomGrayscale(p=0.1),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.normal = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         return len(self.paths) * 3 if self.augment else len(self.paths)  # 3x data: 1 original + 2 augmented\n\n#     def __getitem__(self, idx):\n#         real_idx = idx // 3 if self.augment else idx\n#         is_aug = (idx % 3 != 0) if self.augment else False\n        \n#         path = self.paths[real_idx]\n#         img = make_square(Image.open(os.path.join(DATA_DIR, path)).convert('RGB'))\n#         w, h = img.size\n        \n#         t = self.aug if is_aug else self.normal\n#         left = t(img.crop((0, 0, w//2, h)))\n#         right = t(img.crop((w//2, 0, w, h)))\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n# class Model(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n#         state = torch.load(RESNET18_PATH, map_location='cpu', weights_only=True)\n#         self.backbone.load_state_dict(state)\n#         self.backbone.fc = nn.Identity()\n#         self.head = nn.Sequential(nn.Dropout(DROPOUT), nn.Linear(1024, 5))\n    \n#     def forward(self, left, right):\n#         return self.head(torch.cat([self.backbone(left), self.backbone(right)], dim=1))\n\n# print(\"=\"*70)\n# print(\"TESTING ALL AUGMENTATIONS COMBINED\")\n# print(\"=\"*70)\n# print(\"Augmentations:\")\n# print(\"  - Rotation: ±15°\")\n# print(\"  - Horizontal Flip: 50%\")\n# print(\"  - Vertical Flip: 50%\")\n# print(\"  - Brightness: 0.1\")\n# print(\"  - Contrast: 0.1\")\n# print(\"  - Saturation: 0.2\")\n# print(\"  - Hue: 0.05\")\n# print(\"  - Grayscale: 10%\")\n# print(f\"\\nDevice: {DEVICE} | Epochs: {EPOCHS}\")\n# print(\"=\"*70)\n\n# df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n# gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n# train_idx, val_idx = next(gss.split(df, groups=df['image_path']))\n\n# print(f\"Train: {len(df.iloc[train_idx]['image_path'].unique())} images (3x with augmentation)\")\n# print(f\"Val: {len(df.iloc[val_idx]['image_path'].unique())} images (no augmentation)\")\n\n# train_loader = DataLoader(AllAugDataset(df.iloc[train_idx], True), BATCH_SIZE, shuffle=True, num_workers=2)\n# val_loader = DataLoader(AllAugDataset(df.iloc[val_idx], False), BATCH_SIZE, num_workers=2)\n\n# model = Model().to(DEVICE)\n# optimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n# criterion = nn.SmoothL1Loss()\n\n# best_r2 = -999\n# print(\"\\nTraining started...\")\n# for epoch in range(EPOCHS):\n#     # Train\n#     model.train()\n#     train_loss = 0\n#     for left, right, targets, _ in train_loader:\n#         optimizer.zero_grad()\n#         loss = criterion(model(left.to(DEVICE), right.to(DEVICE)), targets.to(DEVICE))\n#         loss.backward()\n#         optimizer.step()\n#         train_loss += loss.item()\n    \n#     # Validate\n#     model.eval()\n#     preds, actuals = [], []\n#     with torch.no_grad():\n#         for left, right, _, raw in val_loader:\n#             p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#             for i in range(p.shape[0]):\n#                 for j in range(5):\n#                     preds.append(max(0, np.expm1(p[i, j])))\n#                     actuals.append(raw[i, j])\n    \n#     p, a = np.array(preds), np.array(actuals)\n#     r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n    \n#     if r2 > best_r2:\n#         best_r2 = r2\n#         marker = \"⭐\"\n#     else:\n#         marker = \"\"\n    \n#     avg_loss = train_loss / len(train_loader)\n#     print(f\"Epoch {epoch+1:2d}/{EPOCHS} | Loss: {avg_loss:.4f} | Val R²: {r2:.4f} | Best: {best_r2:.4f} {marker}\")\n\n# print(\"\\n\" + \"=\"*70)\n# print(f\"🏆 FINAL BEST R²: {best_r2:.4f}\")\n# print(\"=\"*70)\n# # ?======================================================================\n# # TESTING ALL AUGMENTATIONS COMBINED\n# # ======================================================================\n# # Augmentations:\n# #   - Rotation: ±15°\n# #   - Horizontal Flip: 50%\n# #   - Vertical Flip: 50%\n# #   - Brightness: 0.1\n# #   - Contrast: 0.1\n# #   - Saturation: 0.2\n# #   - Hue: 0.05\n# #   - Grayscale: 10%\n\n# # Device: cuda | Epochs: 20\n# # ======================================================================\n# # Train: 285 images (3x with augmentation)\n# # Val: 72 images (no augmentation)\n\n# # Training started...\n# # Epoch  1/20 | Loss: 0.6855 | Val R²: 0.3421 | Best: 0.3421 ⭐\n# # Epoch  2/20 | Loss: 0.2740 | Val R²: 0.4437 | Best: 0.4437 ⭐\n# # Epoch  3/20 | Loss: 0.2425 | Val R²: 0.4771 | Best: 0.4771 ⭐\n# # Epoch  4/20 | Loss: 0.2122 | Val R²: 0.6502 | Best: 0.6502 ⭐\n# # Epoch  5/20 | Loss: 0.1887 | Val R²: 0.4186 | Best: 0.6502 \n# # Epoch  6/20 | Loss: 0.1772 | Val R²: 0.6417 | Best: 0.6502 \n# # Epoch  7/20 | Loss: 0.1710 | Val R²: 0.5974 | Best: 0.6502 \n# # Epoch  8/20 | Loss: 0.1520 | Val R²: 0.5904 | Best: 0.6502 \n# # Epoch  9/20 | Loss: 0.1559 | Val R²: 0.7233 | Best: 0.7233 ⭐\n# # Epoch 10/20 | Loss: 0.1343 | Val R²: 0.4011 | Best: 0.7233 \n# # Epoch 11/20 | Loss: 0.1338 | Val R²: 0.5100 | Best: 0.7233 \n# # Epoch 12/20 | Loss: 0.1355 | Val R²: 0.7120 | Best: 0.7233 \n# # Epoch 13/20 | Loss: 0.1250 | Val R²: 0.6152 | Best: 0.7233 \n# # Epoch 14/20 | Loss: 0.1229 | Val R²: 0.4679 | Best: 0.7233 \n# # Epoch 15/20 | Loss: 0.1232 | Val R²: 0.6976 | Best: 0.7233 \n# # Epoch 16/20 | Loss: 0.1136 | Val R²: 0.7175 | Best: 0.7233 \n# # Epoch 17/20 | Loss: 0.1091 | Val R²: 0.6877 | Best: 0.7233 \n# # Epoch 18/20 | Loss: 0.1002 | Val R²: 0.7178 | Best: 0.7233 \n# # Epoch 19/20 | Loss: 0.1007 | Val R²: 0.7704 | Best: 0.7704 ⭐\n# # Epoch 20/20 | Loss: 0.1021 | Val R²: 0.7183 | Best: 0.7704 \n\n# # ======================================================================\n# # 🏆 FINAL BEST R²: 0.7704\n# # ======================================================================\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# DATA_DIR = '/kaggle/input/csiro-biomass'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# IMG_SIZE = 224\n# BATCH_SIZE = 16\n# EPOCHS = 20\n# LR = 1e-4\n# DROPOUT = 0.3\n# SEED = 42\n\n# random.seed(SEED)\n# np.random.seed(SEED)\n# torch.manual_seed(SEED)\n# torch.cuda.manual_seed_all(SEED)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n# class RefinedAugDataset(Dataset):\n#     def __init__(self, df, augment=True):\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n        \n#         self.aug = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.RandomRotation(10, expand=True, fill=0),\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.RandomHorizontalFlip(p=0.5),\n#             transforms.RandomVerticalFlip(p=0.5),\n#             transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.15),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.normal = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         return len(self.paths) * 3 if self.augment else len(self.paths)\n\n#     def __getitem__(self, idx):\n#         real_idx = idx // 3 if self.augment else idx\n#         is_aug = (idx % 3 != 0) if self.augment else False\n        \n#         path = self.paths[real_idx]\n#         img = make_square(Image.open(os.path.join(DATA_DIR, path)).convert('RGB'))\n#         w, h = img.size\n        \n#         t = self.aug if is_aug else self.normal\n#         left = t(img.crop((0, 0, w//2, h)))\n#         right = t(img.crop((w//2, 0, w, h)))\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n# class Model(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n#         state = torch.load(RESNET18_PATH, map_location='cpu', weights_only=True)\n#         self.backbone.load_state_dict(state)\n#         self.backbone.fc = nn.Identity()\n#         self.head = nn.Sequential(nn.Dropout(DROPOUT), nn.Linear(1024, 5))\n    \n#     def forward(self, left, right):\n#         return self.head(torch.cat([self.backbone(left), self.backbone(right)], dim=1))\n\n# print(\"=\"*70)\n# print(\"REFINED AUGMENTATION TEST (Gentler + LR Scheduler)\")\n# print(\"=\"*70)\n# print(\"Augmentations:\")\n# print(\"  - Rotation: ±10° (was 15°)\")\n# print(\"  - Horizontal Flip: 50%\")\n# print(\"  - Vertical Flip: 50%\")\n# print(\"  - Brightness: 0.1\")\n# print(\"  - Contrast: 0.1\")\n# print(\"  - Saturation: 0.15 (was 0.2)\")\n# print(\"  - REMOVED: Hue, Grayscale\")\n# print(f\"\\nDevice: {DEVICE} | Epochs: {EPOCHS} | LR: {LR} (adaptive)\")\n# print(\"=\"*70)\n\n# df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n# gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n# train_idx, val_idx = next(gss.split(df, groups=df['image_path']))\n\n# print(f\"Train: {len(df.iloc[train_idx]['image_path'].unique())} images (3x with augmentation)\")\n# print(f\"Val: {len(df.iloc[val_idx]['image_path'].unique())} images (no augmentation)\")\n\n# train_loader = DataLoader(RefinedAugDataset(df.iloc[train_idx], True), BATCH_SIZE, shuffle=True, num_workers=2)\n# val_loader = DataLoader(RefinedAugDataset(df.iloc[val_idx], False), BATCH_SIZE, num_workers=2)\n\n# model = Model().to(DEVICE)\n# optimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n# scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', patience=3, factor=0.5)\n# criterion = nn.SmoothL1Loss()\n\n# best_r2 = -999\n# no_improve = 0\n# print(\"\\nTraining started...\")\n\n# for epoch in range(EPOCHS):\n#     # Train\n#     model.train()\n#     train_loss = 0\n#     for left, right, targets, _ in train_loader:\n#         optimizer.zero_grad()\n#         loss = criterion(model(left.to(DEVICE), right.to(DEVICE)), targets.to(DEVICE))\n#         loss.backward()\n#         optimizer.step()\n#         train_loss += loss.item()\n    \n#     # Validate\n#     model.eval()\n#     preds, actuals = [], []\n#     with torch.no_grad():\n#         for left, right, _, raw in val_loader:\n#             p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#             for i in range(p.shape[0]):\n#                 for j in range(5):\n#                     preds.append(max(0, np.expm1(p[i, j])))\n#                     actuals.append(raw[i, j])\n    \n#     p, a = np.array(preds), np.array(actuals)\n#     r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n    \n#     # Track improvement\n#     if r2 > best_r2:\n#         best_r2 = r2\n#         no_improve = 0\n#         marker = \"⭐\"\n#     else:\n#         no_improve += 1\n#         marker = \"\"\n    \n#     avg_loss = train_loss / len(train_loader)\n#     current_lr = optimizer.param_groups[0]['lr']\n    \n#     # Check if LR was reduced\n#     old_lr = LR if epoch == 0 else prev_lr\n#     lr_changed = \" [LR reduced]\" if current_lr < old_lr else \"\"\n#     prev_lr = current_lr\n    \n#     print(f\"Epoch {epoch+1:2d}/{EPOCHS} | Loss: {avg_loss:.4f} | Val R²: {r2:.4f} | Best: {best_r2:.4f} | LR: {current_lr:.6f}{lr_changed} {marker}\")\n    \n#     # Adjust learning rate\n#     scheduler.step(r2)\n    \n#     # Early stopping if no improvement for 8 epochs\n#     if no_improve >= 8:\n#         print(f\"\\nEarly stopping - no improvement for {no_improve} epochs\")\n#         break\n\n# print(\"\\n\" + \"=\"*70)\n# print(f\"🏆 FINAL BEST R²: {best_r2:.4f}\")\n# print(\"=\"*70)\n# ======================================================================\n# REFINED AUGMENTATION TEST (Gentler + LR Scheduler)\n# ======================================================================\n# Augmentations:\n#   - Rotation: ±10° (was 15°)\n#   - Horizontal Flip: 50%\n#   - Vertical Flip: 50%\n#   - Brightness: 0.1\n#   - Contrast: 0.1\n#   - Saturation: 0.15 (was 0.2)\n#   - REMOVED: Hue, Grayscale\n\n# Device: cuda | Epochs: 20 | LR: 0.0001 (adaptive)\n# ======================================================================\n# Train: 285 images (3x with augmentation)\n# Val: 72 images (no augmentation)\n\n# Training started...\n# Epoch  1/20 | Loss: 0.6583 | Val R²: 0.1899 | Best: 0.1899 | LR: 0.000100 ⭐\n# Epoch  2/20 | Loss: 0.2398 | Val R²: 0.5721 | Best: 0.5721 | LR: 0.000100 ⭐\n# Epoch  3/20 | Loss: 0.2115 | Val R²: 0.6080 | Best: 0.6080 | LR: 0.000100 ⭐\n# Epoch  4/20 | Loss: 0.1841 | Val R²: 0.6668 | Best: 0.6668 | LR: 0.000100 ⭐\n# Epoch  5/20 | Loss: 0.1653 | Val R²: 0.5129 | Best: 0.6668 | LR: 0.000100 \n# Epoch  6/20 | Loss: 0.1558 | Val R²: 0.6356 | Best: 0.6668 | LR: 0.000100 \n# Epoch  7/20 | Loss: 0.1510 | Val R²: 0.6173 | Best: 0.6668 | LR: 0.000100 \n# Epoch  8/20 | Loss: 0.1343 | Val R²: 0.6546 | Best: 0.6668 | LR: 0.000100 \n# Epoch  9/20 | Loss: 0.1291 | Val R²: 0.7146 | Best: 0.7146 | LR: 0.000050 [LR reduced] ⭐\n# Epoch 10/20 | Loss: 0.1080 | Val R²: 0.6745 | Best: 0.7146 | LR: 0.000050 \n# Epoch 11/20 | Loss: 0.1122 | Val R²: 0.7506 | Best: 0.7506 | LR: 0.000050 ⭐\n# Epoch 12/20 | Loss: 0.1110 | Val R²: 0.7525 | Best: 0.7525 | LR: 0.000050 ⭐\n# Epoch 13/20 | Loss: 0.1052 | Val R²: 0.7503 | Best: 0.7525 | LR: 0.000050 \n# Epoch 14/20 | Loss: 0.1060 | Val R²: 0.7431 | Best: 0.7525 | LR: 0.000050 \n# Epoch 15/20 | Loss: 0.1043 | Val R²: 0.7211 | Best: 0.7525 | LR: 0.000050 \n# Epoch 16/20 | Loss: 0.0955 | Val R²: 0.7477 | Best: 0.7525 | LR: 0.000050 \n# Epoch 17/20 | Loss: 0.0934 | Val R²: 0.7494 | Best: 0.7525 | LR: 0.000025 [LR reduced] \n# Epoch 18/20 | Loss: 0.0840 | Val R²: 0.7560 | Best: 0.7560 | LR: 0.000025 ⭐\n# Epoch 19/20 | Loss: 0.0846 | Val R²: 0.7561 | Best: 0.7561 | LR: 0.000025 ⭐\n# Epoch 20/20 | Loss: 0.0836 | Val R²: 0.7421 | Best: 0.7561 | LR: 0.000025 \n\n# ======================================================================\n# 🏆 FINAL BEST R²: 0.7561\n# ======================================================================\n\n# Current (Refined):\n\n# Best: 0.7561 at epoch 19\n# Smooth progression: 0.67 → 0.71 → 0.75 → 0.76\n# Much more stable and reliable\n# The difference is only 0.0143 (1.4%), but you gained:\n\n# ✅ Consistent improvement\n# ✅ LR scheduler working perfectly (reduced at epochs 9, 17)\n# ✅ No wild drops\n# ✅ More trustworthy for final training\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import train_test_split\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if os.path.exists('/kaggle/working') else '.'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# IMG_SIZE = 224\n# BATCH_SIZE = 16\n# EPOCHS = 8\n# SEED = 42\n\n# random.seed(SEED)\n# np.random.seed(SEED)\n# torch.manual_seed(SEED)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n# class QuickTestDataset(Dataset):\n#     def __init__(self, df, aug_config):\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n#         self.aug_config = aug_config\n        \n#         # Build transform based on config\n#         aug_list = [transforms.Resize((IMG_SIZE, IMG_SIZE))]\n        \n#         # Add color jitter if specified\n#         if any(k in aug_config for k in ['brightness', 'contrast', 'saturation', 'hue']):\n#             aug_list.append(transforms.ColorJitter(\n#                 brightness=aug_config.get('brightness', 0),\n#                 contrast=aug_config.get('contrast', 0),\n#                 saturation=aug_config.get('saturation', 0),\n#                 hue=aug_config.get('hue', 0)\n#             ))\n        \n#         aug_list.extend([\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.transform = transforms.Compose(aug_list)\n\n#     def __len__(self):\n#         return len(self.paths) * 2  # Original + 1 augmented\n\n#     def __getitem__(self, idx):\n#         real_idx = idx // 2\n#         path = self.paths[real_idx]\n#         img = make_square(Image.open(os.path.join(DATA_DIR, path)).convert('RGB'))\n#         w, h = img.size\n        \n#         left = self.transform(img.crop((0, 0, w//2, h)))\n#         right = self.transform(img.crop((w//2, 0, w, h)))\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n# class SimpleModel(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n#         if os.path.exists(RESNET18_PATH):\n#             self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n#         else:\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n#         self.backbone.fc = nn.Identity()\n#         self.head = nn.Sequential(nn.Dropout(0.3), nn.Linear(1024, 5))\n    \n#     def forward(self, left, right):\n#         return self.head(torch.cat([self.backbone(left), self.backbone(right)], dim=1))\n\n# def quick_test(train_df, test_df, aug_config, name):\n#     train_ds = QuickTestDataset(train_df, aug_config)\n#     test_ds = QuickTestDataset(test_df, {})  # No aug for test\n    \n#     train_loader = DataLoader(train_ds, BATCH_SIZE, shuffle=True, num_workers=2)\n#     test_loader = DataLoader(test_ds, BATCH_SIZE, num_workers=2)\n    \n#     model = SimpleModel().to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n#     crashed = False\n    \n#     for epoch in range(EPOCHS):\n#         model.train()\n#         for left, right, targets, _ in train_loader:\n#             optimizer.zero_grad()\n#             criterion(model(left.to(DEVICE), right.to(DEVICE)), targets.to(DEVICE)).backward()\n#             optimizer.step()\n        \n#         model.eval()\n#         preds, actuals = [], []\n#         with torch.no_grad():\n#             for left, right, _, raw in test_loader:\n#                 p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#                 for i in range(p.shape[0]):\n#                     for j in range(5):\n#                         preds.append(max(0, np.expm1(p[i, j])))\n#                         actuals.append(raw[i, j])\n        \n#         p, a = np.array(preds), np.array(actuals)\n#         r2 = 1 - np.sum((p - a) ** 2) / (np.sum((a - a.mean()) ** 2) + 1e-8)\n        \n#         if r2 > best_r2:\n#             best_r2 = r2\n        \n#         # Check for crash\n#         if r2 < -0.5:\n#             crashed = True\n#             print(f\"  E{epoch+1}: R²={r2:.4f} ❌ CRASH\")\n#             break\n        \n#         print(f\"  E{epoch+1}: R²={r2:.4f}\")\n    \n#     return best_r2, crashed\n\n# print(\"=\"*70)\n# print(\"QUICK AUGMENTATION STABILITY TEST (8 epochs each)\")\n# print(\"=\"*70)\n\n# df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n# unique_imgs = df['image_path'].unique()\n# train_imgs, test_imgs = train_test_split(unique_imgs, test_size=15, random_state=SEED)\n# train_df = df[df['image_path'].isin(train_imgs)]\n# test_df = df[df['image_path'].isin(test_imgs)]\n\n# print(f\"Train: {len(train_imgs)} images | Test: {len(test_imgs)} images\\n\")\n\n# tests = [\n#     (\"Baseline (no aug)\", {}),\n#     (\"Bright+Contrast 0.1\", {'brightness': 0.1, 'contrast': 0.1}),\n#     (\"B+C+Sat 0.15\", {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.15}),\n#     (\"B+C+Sat 0.2\", {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.2}),\n#     (\"B+C+Sat+Hue 0.02\", {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.2, 'hue': 0.02}),\n#     (\"B+C+Sat+Hue 0.05\", {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.2, 'hue': 0.05}),\n# ]\n\n# results = []\n# for name, config in tests:\n#     print(f\"\\n[Test] {name}\")\n#     print(f\"  Config: {config}\")\n#     best_r2, crashed = quick_test(train_df, test_df, config, name)\n#     status = \"❌ CRASHED\" if crashed else \"✓ Stable\"\n#     results.append((name, best_r2, crashed))\n#     print(f\"  Best R²: {best_r2:.4f} {status}\")\n\n# print(\"\\n\" + \"=\"*70)\n# print(\"RESULTS SUMMARY\")\n# print(\"=\"*70)\n# for name, r2, crashed in sorted(results, key=lambda x: -x[1]):\n#     status = \"❌ CRASHED\" if crashed else \"✓\"\n#     print(f\"  {name:25s}: R²={r2:.4f} {status}\")\n\n# best = max(results, key=lambda x: x[1] if not x[2] else -999)\n# print(f\"\\n🏆 BEST STABLE: {best[0]} with R²={best[1]:.4f}\")\n\n\n# # ======================================================================\n# # QUICK AUGMENTATION STABILITY TEST (8 epochs each)\n# # ======================================================================\n# # Train: 342 images | Test: 15 images\n\n\n# # [Test] Baseline (no aug)\n# #   Config: {}\n# #   E1: R²=0.6179\n# #   E2: R²=0.5468\n# #   E3: R²=0.5425\n# #   E4: R²=0.6335\n# #   E5: R²=0.6704\n# #   E6: R²=0.6686\n# #   E7: R²=0.6967\n# #   E8: R²=0.7341\n# #   Best R²: 0.7341 ✓ Stable\n\n# # [Test] Bright+Contrast 0.1\n# #   Config: {'brightness': 0.1, 'contrast': 0.1}\n# #   E1: R²=0.4301\n# #   E2: R²=0.6735\n# #   E3: R²=0.5285\n# #   E4: R²=0.5306\n# #   E5: R²=0.6973\n# #   E6: R²=0.5671\n# #   E7: R²=0.6693\n# #   E8: R²=0.7078\n# #   Best R²: 0.7078 ✓ Stable\n\n# # [Test] B+C+Sat 0.15\n# #   Config: {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.15}\n# #   E1: R²=0.4784\n# #   E2: R²=0.6671\n# #   E3: R²=0.6342\n# #   E4: R²=0.5422\n# #   E5: R²=0.5756\n# #   E6: R²=0.5548\n# #   E7: R²=0.6681\n# #   E8: R²=0.6768\n# #   Best R²: 0.6768 ✓ Stable\n\n# # [Test] B+C+Sat 0.2\n# #   Config: {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.2}\n# #   E1: R²=0.4992\n# #   E2: R²=0.5817\n# #   E3: R²=0.6918\n# #   E4: R²=0.6596\n# #   E5: R²=0.5679\n# #   E6: R²=0.6274\n# #   E7: R²=0.6445\n# #   E8: R²=0.6732\n# #   Best R²: 0.6918 ✓ Stable\n\n# # [Test] B+C+Sat+Hue 0.02\n# #   Config: {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.2, 'hue': 0.02}\n# #   E1: R²=0.5510\n# #   E2: R²=0.5869\n# #   E3: R²=0.6517\n# #   E4: R²=0.6586\n# #   E5: R²=0.6974\n# #   E6: R²=0.6917\n# #   E7: R²=0.6019\n# #   E8: R²=0.7417\n# #   Best R²: 0.7417 ✓ Stable\n\n# # [Test] B+C+Sat+Hue 0.05\n# #   Config: {'brightness': 0.1, 'contrast': 0.1, 'saturation': 0.2, 'hue': 0.05}\n# #   E1: R²=0.1486\n# #   E2: R²=0.6632\n# #   E3: R²=0.5739\n# #   E4: R²=0.7196\n# #   E5: R²=0.6450\n# #   E6: R²=0.6626\n# #   E7: R²=0.6291\n# #   E8: R²=0.7285\n# #   Best R²: 0.7285 ✓ Stable\n\n# # ======================================================================\n# # RESULTS SUMMARY\n# # ======================================================================\n# #   B+C+Sat+Hue 0.02         : R²=0.7417 ✓\n# #   Baseline (no aug)        : R²=0.7341 ✓\n# #   B+C+Sat+Hue 0.05         : R²=0.7285 ✓\n# #   Bright+Contrast 0.1      : R²=0.7078 ✓\n# #   B+C+Sat 0.2              : R²=0.6918 ✓\n# #   B+C+Sat 0.15             : R²=0.6768 ✓\n\n# # 🏆 BEST STABLE: B+C+Sat+Hue 0.02 with R²=0.7417","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# \"\"\"\n# Quick head-to-head: Dual-stream vs Landscape\n# Focus on Dry_Total_g performance\n# \"\"\"\n\n# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# IS_KAGGLE = os.path.exists('/kaggle/working')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if IS_KAGGLE else '.'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n\n# IMG_SIZE = 224\n# BATCH_SIZE = 16\n# EPOCHS = 15\n# LR = 1e-4\n# DROPOUT = 0.3\n# SEED = 42\n\n# def set_seed(seed):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     if torch.cuda.is_available():\n#         torch.cuda.manual_seed_all(seed)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n\n# class DualStreamDataset(Dataset):\n#     \"\"\"Your best config: dual-stream L/R split with refined augmentation\"\"\"\n#     def __init__(self, df, augment=True):\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n        \n#         self.aug = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.RandomRotation(10, expand=True, fill=0),\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.RandomHorizontalFlip(p=0.5),\n#             transforms.RandomVerticalFlip(p=0.5),\n#             transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.15),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.normal = transforms.Compose([\n#             transforms.Resize((IMG_SIZE, IMG_SIZE)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         return len(self.paths) * 3 if self.augment else len(self.paths)\n\n#     def __getitem__(self, idx):\n#         real_idx = idx // 3 if self.augment else idx\n#         is_aug = (idx % 3 != 0) if self.augment else False\n        \n#         path = self.paths[real_idx]\n#         img = make_square(Image.open(os.path.join(DATA_DIR, path)).convert('RGB'))\n#         w, h = img.size\n        \n#         t = self.aug if is_aug else self.normal\n#         left = t(img.crop((0, 0, w//2, h)))\n#         right = t(img.crop((w//2, 0, w, h)))\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n\n\n# class LandscapeDataset(Dataset):\n#     \"\"\"Current config: full landscape with color-only augmentation\"\"\"\n#     def __init__(self, df, augment=True):\n#         self.augment = augment\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n        \n#         self.aug = transforms.Compose([\n#             transforms.Resize((384, 768)),\n#             transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.normal = transforms.Compose([\n#             transforms.Resize((384, 768)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n\n#     def __len__(self):\n#         return len(self.paths)\n\n#     def __getitem__(self, idx):\n#         path = self.paths[idx]\n#         img = Image.open(os.path.join(DATA_DIR, path)).convert('RGB')\n        \n#         t = self.aug if self.augment else self.normal\n#         img_tensor = t(img)\n        \n#         targets = np.zeros(5, dtype=np.float32)\n#         raw = np.zeros(5, dtype=np.float32)\n#         for _, row in self.groups.get_group(path).iterrows():\n#             if row['target_name'] in TARGETS:\n#                 i = TARGETS.index(row['target_name'])\n#                 targets[i] = np.log1p(row['target'])\n#                 raw[i] = row['target']\n        \n#         return img_tensor, torch.from_numpy(targets), torch.from_numpy(raw)\n\n\n# class DualStreamModel(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n#         if os.path.exists(RESNET18_PATH):\n#             self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n#         else:\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n#         self.backbone.fc = nn.Identity()\n#         self.head = nn.Sequential(nn.Dropout(DROPOUT), nn.Linear(1024, 5))\n    \n#     def forward(self, left, right):\n#         return self.head(torch.cat([self.backbone(left), self.backbone(right)], dim=1))\n\n\n# class LandscapeModel(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n#         self.backbone = models.resnet18(weights=None)\n#         if os.path.exists(RESNET18_PATH):\n#             self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n#         else:\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n#         self.backbone.fc = nn.Identity()\n#         self.heads = nn.ModuleList([\n#             nn.Sequential(nn.Dropout(DROPOUT), nn.Linear(512, 1)) for _ in range(5)\n#         ])\n    \n#     def forward(self, x):\n#         feats = self.backbone(x)\n#         return torch.cat([h(feats) for h in self.heads], dim=1)\n\n\n# def calc_r2_per_target(preds, actuals):\n#     \"\"\"Calculate R² for each target separately\"\"\"\n#     r2 = {}\n#     for j, t in enumerate(TARGETS):\n#         p = np.array([pred[j] for pred in preds])\n#         a = np.array([act[j] for act in actuals])\n#         ss_res = np.sum((p - a) ** 2)\n#         ss_tot = np.sum((a - a.mean()) ** 2) + 1e-8\n#         r2[t] = 1 - ss_res / ss_tot\n#     return r2\n\n\n# def test_dual_stream(train_df, val_df):\n#     set_seed(SEED)\n    \n#     train_loader = DataLoader(DualStreamDataset(train_df, True), BATCH_SIZE, shuffle=True, num_workers=2)\n#     val_loader = DataLoader(DualStreamDataset(val_df, False), BATCH_SIZE, num_workers=2)\n    \n#     model = DualStreamModel().to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n#     scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', patience=3, factor=0.5)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n#     best_per_target = {}\n    \n#     for epoch in range(EPOCHS):\n#         model.train()\n#         for left, right, targets, _ in train_loader:\n#             optimizer.zero_grad()\n#             criterion(model(left.to(DEVICE), right.to(DEVICE)), targets.to(DEVICE)).backward()\n#             optimizer.step()\n        \n#         model.eval()\n#         preds, actuals = [], []\n#         with torch.no_grad():\n#             for left, right, _, raw in val_loader:\n#                 p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n#                 for i in range(p.shape[0]):\n#                     preds.append([max(0, np.expm1(p[i, j])) for j in range(5)])\n#                     actuals.append(raw[i].numpy())\n        \n#         r2_per = calc_r2_per_target(preds, actuals)\n#         mean_r2 = np.mean(list(r2_per.values()))\n        \n#         if mean_r2 > best_r2:\n#             best_r2 = mean_r2\n#             best_per_target = r2_per.copy()\n        \n#         scheduler.step(mean_r2)\n#         print(f\"  E{epoch+1:2d}: R²={mean_r2:.4f} | Total={r2_per['Dry_Total_g']:.4f}\")\n    \n#     return best_r2, best_per_target\n\n\n# def test_landscape(train_df, val_df):\n#     set_seed(SEED)\n    \n#     train_loader = DataLoader(LandscapeDataset(train_df, True), BATCH_SIZE, shuffle=True, num_workers=2)\n#     val_loader = DataLoader(LandscapeDataset(val_df, False), BATCH_SIZE, num_workers=2)\n    \n#     model = LandscapeModel().to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)\n#     scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', patience=3, factor=0.5)\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_r2 = -999\n#     best_per_target = {}\n    \n#     for epoch in range(EPOCHS):\n#         model.train()\n#         for img, targets, _ in train_loader:\n#             optimizer.zero_grad()\n#             criterion(model(img.to(DEVICE)), targets.to(DEVICE)).backward()\n#             optimizer.step()\n        \n#         model.eval()\n#         preds, actuals = [], []\n#         with torch.no_grad():\n#             for img, _, raw in val_loader:\n#                 p = model(img.to(DEVICE)).cpu().numpy()\n#                 for i in range(p.shape[0]):\n#                     preds.append([max(0, np.expm1(p[i, j])) for j in range(5)])\n#                     actuals.append(raw[i].numpy())\n        \n#         r2_per = calc_r2_per_target(preds, actuals)\n#         mean_r2 = np.mean(list(r2_per.values()))\n        \n#         if mean_r2 > best_r2:\n#             best_r2 = mean_r2\n#             best_per_target = r2_per.copy()\n        \n#         scheduler.step(mean_r2)\n#         print(f\"  E{epoch+1:2d}: R²={mean_r2:.4f} | Total={r2_per['Dry_Total_g']:.4f}\")\n    \n#     return best_r2, best_per_target\n\n\n# if __name__ == '__main__':\n#     print(\"=\"*70)\n#     print(\"HEAD-TO-HEAD: Dual-Stream vs Landscape\")\n#     print(\"Focus: Dry_Total_g performance\")\n#     print(\"=\"*70)\n    \n#     df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n#     gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n#     train_idx, val_idx = next(gss.split(df, groups=df['image_path']))\n#     train_df = df.iloc[train_idx]\n#     val_df = df.iloc[val_idx]\n    \n#     print(f\"\\nTrain: {train_df['image_path'].nunique()} images\")\n#     print(f\"Val: {val_df['image_path'].nunique()} images\")\n    \n#     print(f\"\\n{'='*70}\")\n#     print(\"[1] DUAL-STREAM (224x224 L/R split + refined aug)\")\n#     print(\"=\"*70)\n#     ds_r2, ds_per = test_dual_stream(train_df, val_df)\n    \n#     print(f\"\\n{'='*70}\")\n#     print(\"[2] LANDSCAPE (384x768 + color-only aug)\")\n#     print(\"=\"*70)\n#     ls_r2, ls_per = test_landscape(train_df, val_df)\n    \n#     print(f\"\\n{'='*70}\")\n#     print(\"RESULTS COMPARISON\")\n#     print(\"=\"*70)\n#     print(f\"\\n{'Target':<20} {'Dual-Stream':>12} {'Landscape':>12} {'Winner':>12}\")\n#     print(\"-\"*60)\n#     for t in TARGETS:\n#         ds_val = ds_per.get(t, 0)\n#         ls_val = ls_per.get(t, 0)\n#         winner = \"Dual\" if ds_val > ls_val else \"Land\"\n#         marker = \"⭐\" if t == 'Dry_Total_g' else \"\"\n#         print(f\"{t:<20} {ds_val:>12.4f} {ls_val:>12.4f} {winner:>12} {marker}\")\n    \n#     print(\"-\"*60)\n#     print(f\"{'MEAN':<20} {ds_r2:>12.4f} {ls_r2:>12.4f} {'Dual' if ds_r2 > ls_r2 else 'Land':>12}\")\n    \n#     print(f\"\\n🏆 WINNER: {'Dual-Stream' if ds_r2 > ls_r2 else 'Landscape'}\")\n#     print(f\"   Dry_Total_g: Dual={ds_per.get('Dry_Total_g', 0):.4f} vs Land={ls_per.get('Dry_Total_g', 0):.4f}\")\n# HEAD-TO-HEAD: Dual-Stream vs Landscape\n# Focus: Dry_Total_g performance\n# ======================================================================\n\n# Train: 285 images\n# Val: 72 images\n\n# ======================================================================\n# [1] DUAL-STREAM (224x224 L/R split + refined aug)\n# ======================================================================\n#   E 1: R²=-0.0747 | Total=-0.3026\n#   E 2: R²=0.3393 | Total=0.4426\n#   E 3: R²=0.4626 | Total=0.4892\n#   E 4: R²=0.5083 | Total=0.4965\n#   E 5: R²=0.3292 | Total=0.3268\n#   E 6: R²=0.4837 | Total=0.4376\n#   E 7: R²=0.5112 | Total=0.3948\n#   E 8: R²=0.4516 | Total=0.3736\n#   E 9: R²=0.5531 | Total=0.5526\n#   E10: R²=0.4854 | Total=0.3506\n#   E11: R²=0.5910 | Total=0.6106\n#   E12: R²=0.5920 | Total=0.6106\n#   E13: R²=0.5365 | Total=0.4747\n#   E14: R²=0.5370 | Total=0.5302\n#   E15: R²=0.5962 | Total=0.6503\n\n# ======================================================================\n# [2] LANDSCAPE (384x768 + color-only aug)\n# ======================================================================\n#   E 1: R²=-4.1830 | Total=-1.7203\n#   E 2: R²=-576.2575 | Total=-67.7570\n#   E 3: R²=0.3060 | Total=-0.1456\n#   E 4: R²=0.5161 | Total=0.4470\n#   E 5: R²=0.2279 | Total=0.4059\n#   E 6: R²=0.5057 | Total=0.4281\n#   E 7: R²=-0.1026 | Total=-0.6679\n#   E 8: R²=0.3835 | Total=0.4445\n#   E 9: R²=0.5676 | Total=0.4529\n#   E10: R²=0.6039 | Total=0.4652\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# \"\"\"\n# FULLY AUTOMATED OPTIMIZATION & SUBMISSION SYSTEM\n# - Automatically finds best configuration (architecture, epochs, hyperparameters, augmentation)\n# - Eliminates bad trials early (pruning)\n# - Learns from results (Bayesian optimization)\n# - Trains final model with best config\n# - Generates submission\n# - ZERO INTERVENTION NEEDED - just run and wait\n# \"\"\"\n\n# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# from PIL import Image\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from sklearn.model_selection import GroupShuffleSplit\n# import optuna\n# from optuna.trial import TrialState\n# import warnings\n# warnings.filterwarnings('ignore')\n\n# # ============ CONFIG ============\n# DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# IS_KAGGLE = os.path.exists('/kaggle/working')\n# DATA_DIR = '/kaggle/input/csiro-biomass' if IS_KAGGLE else '.'\n# RESNET18_PATH = '/kaggle/input/resnet18/resnet18-f37072fd.pth'\n# TARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\n# SEED = 42\n\n# # Optimization settings\n# N_TRIALS = 30  # Increase for better results (30 ≈ 2 hours on GPU)\n# FINAL_EPOCHS = None  # Will be set by Optuna\n\n# def set_seed(seed):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     if torch.cuda.is_available():\n#         torch.cuda.manual_seed_all(seed)\n\n# def make_square(img):\n#     w, h = img.size\n#     if w == h: return img\n#     s = max(w, h)\n#     sq = Image.new('RGB', (s, s), (0, 0, 0))\n#     sq.paste(img, ((s - w) // 2, (s - h) // 2))\n#     return sq\n\n\n# # ============ FLEXIBLE DATASET ============\n# class FlexibleDataset(Dataset):\n#     \"\"\"Supports all architecture and augmentation options\"\"\"\n#     def __init__(self, df, config, augment=True, has_targets=True):\n#         self.config = config\n#         self.augment = augment\n#         self.has_targets = has_targets\n#         self.groups = df.groupby('image_path')\n#         self.paths = list(self.groups.groups.keys())\n        \n#         img_size = config['img_size']\n        \n#         # Build augmentation\n#         aug_list = [transforms.Resize((img_size, img_size))]\n        \n#         if config.get('rotation', 0) > 0:\n#             aug_list.append(transforms.RandomRotation(config['rotation'], expand=True, fill=0))\n#             aug_list.append(transforms.Resize((img_size, img_size)))\n        \n#         if config.get('flip_h', False):\n#             aug_list.append(transforms.RandomHorizontalFlip(p=0.5))\n        \n#         if config.get('flip_v', False):\n#             aug_list.append(transforms.RandomVerticalFlip(p=0.5))\n        \n#         b = config.get('brightness', 0)\n#         c = config.get('contrast', 0)\n#         s = config.get('saturation', 0)\n#         h = config.get('hue', 0)\n#         if b > 0 or c > 0 or s > 0 or h > 0:\n#             aug_list.append(transforms.ColorJitter(brightness=b, contrast=c, saturation=s, hue=h))\n        \n#         aug_list.extend([\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n        \n#         self.transform_aug = transforms.Compose(aug_list)\n#         self.transform_normal = transforms.Compose([\n#             transforms.Resize((img_size, img_size)),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#         ])\n    \n#     def __len__(self):\n#         mult = self.config.get('aug_multiplier', 2) if self.augment else 1\n#         return len(self.paths) * mult\n    \n#     def __getitem__(self, idx):\n#         mult = self.config.get('aug_multiplier', 2) if self.augment else 1\n#         real_idx = idx // mult\n#         is_aug = (idx % mult != 0) if self.augment else False\n        \n#         path = self.paths[real_idx]\n#         img = Image.open(os.path.join(DATA_DIR, path)).convert('RGB')\n#         img = make_square(img)\n#         w, h = img.size\n        \n#         t = self.transform_aug if is_aug else self.transform_normal\n        \n#         # Architecture choice\n#         if self.config['architecture'] == 'dual_stream':\n#             left = t(img.crop((0, 0, w//2, h)))\n#             right = t(img.crop((w//2, 0, w, h)))\n#             img_tensor = (left, right)\n#         else:\n#             img_tensor = t(img)\n        \n#         if self.has_targets:\n#             targets = np.zeros(5, dtype=np.float32)\n#             raw = np.zeros(5, dtype=np.float32)\n#             for _, row in self.groups.get_group(path).iterrows():\n#                 if row['target_name'] in TARGETS:\n#                     i = TARGETS.index(row['target_name'])\n#                     targets[i] = np.log1p(row['target'])\n#                     raw[i] = row['target']\n#             return img_tensor, torch.from_numpy(targets), torch.from_numpy(raw)\n        \n#         return img_tensor, path\n\n\n# # ============ FLEXIBLE MODEL ============\n# class FlexibleModel(nn.Module):\n#     \"\"\"Supports different architectures and head types\"\"\"\n#     def __init__(self, config):\n#         super().__init__()\n#         self.config = config\n        \n#         self.backbone = models.resnet18(weights=None)\n#         if os.path.exists(RESNET18_PATH):\n#             self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n#         else:\n#             self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n#         self.backbone.fc = nn.Identity()\n        \n#         feat_dim = 1024 if config['architecture'] == 'dual_stream' else 512\n#         dropout = config['dropout']\n        \n#         if config['head_type'] == 'simple':\n#             self.head = nn.Sequential(nn.Dropout(dropout), nn.Linear(feat_dim, 5))\n#         elif config['head_type'] == 'hidden':\n#             hidden = config.get('hidden_dim', 256)\n#             self.head = nn.Sequential(\n#                 nn.Dropout(dropout),\n#                 nn.Linear(feat_dim, hidden),\n#                 nn.ReLU(),\n#                 nn.Dropout(dropout),\n#                 nn.Linear(hidden, 5)\n#             )\n#         else:  # separate\n#             self.heads = nn.ModuleList([\n#                 nn.Sequential(nn.Dropout(dropout), nn.Linear(feat_dim, 1))\n#                 for _ in range(5)\n#             ])\n#             self.head = None\n    \n#     def forward(self, x):\n#         if self.config['architecture'] == 'dual_stream':\n#             left, right = x\n#             feat_l = self.backbone(left)\n#             feat_r = self.backbone(right)\n#             features = torch.cat([feat_l, feat_r], dim=1)\n#         else:\n#             features = self.backbone(x)\n        \n#         if self.head is not None:\n#             return self.head(features)\n#         else:\n#             return torch.cat([h(features) for h in self.heads], dim=1)\n\n\n# # ============ TRAINING ============\n# def train_and_evaluate(config, train_df, val_df, trial=None, verbose=False):\n#     \"\"\"Train and return weighted R² score\"\"\"\n#     set_seed(SEED)\n    \n#     train_ds = FlexibleDataset(train_df, config, augment=True)\n#     val_ds = FlexibleDataset(val_df, config, augment=False)\n    \n#     batch_size = config['batch_size']\n#     train_loader = DataLoader(train_ds, batch_size, shuffle=True, num_workers=2)\n#     val_loader = DataLoader(val_ds, batch_size, num_workers=2)\n    \n#     model = FlexibleModel(config).to(DEVICE)\n#     optimizer = torch.optim.AdamW(model.parameters(), lr=config['lr'], weight_decay=config['weight_decay'])\n#     criterion = nn.SmoothL1Loss()\n    \n#     best_score = -999\n#     best_per_target = {}\n#     no_improve = 0\n#     epochs = config['epochs']\n    \n#     for epoch in range(epochs):\n#         # Train\n#         model.train()\n#         for batch in train_loader:\n#             if config['architecture'] == 'dual_stream':\n#                 imgs = (batch[0][0].to(DEVICE), batch[0][1].to(DEVICE))\n#             else:\n#                 imgs = batch[0].to(DEVICE)\n#             targets = batch[1].to(DEVICE)\n            \n#             optimizer.zero_grad()\n#             loss = criterion(model(imgs), targets)\n#             loss.backward()\n#             torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n#             optimizer.step()\n        \n#         # Validate\n#         model.eval()\n#         all_preds = {t: [] for t in TARGETS}\n#         all_actuals = {t: [] for t in TARGETS}\n        \n#         with torch.no_grad():\n#             for batch in val_loader:\n#                 if config['architecture'] == 'dual_stream':\n#                     imgs = (batch[0][0].to(DEVICE), batch[0][1].to(DEVICE))\n#                 else:\n#                     imgs = batch[0].to(DEVICE)\n#                 raw = batch[2]\n                \n#                 preds = model(imgs).cpu().numpy()\n#                 for i in range(preds.shape[0]):\n#                     for j, t in enumerate(TARGETS):\n#                         all_preds[t].append(max(0, np.expm1(preds[i, j])))\n#                         all_actuals[t].append(raw[i, j].item())\n        \n#         # Calculate R² per target\n#         r2_scores = {}\n#         for t in TARGETS:\n#             p = np.array(all_preds[t])\n#             a = np.array(all_actuals[t])\n#             ss_res = np.sum((p - a) ** 2)\n#             ss_tot = np.sum((a - a.mean()) ** 2) + 1e-8\n#             r2_scores[t] = 1 - ss_res / ss_tot\n        \n#         mean_r2 = np.mean(list(r2_scores.values()))\n#         weighted_score = 0.3 * r2_scores['Dry_Total_g'] + 0.7 * mean_r2\n        \n#         if weighted_score > best_score:\n#             best_score = weighted_score\n#             best_per_target = r2_scores.copy()\n#             no_improve = 0\n#         else:\n#             no_improve += 1\n        \n#         if verbose:\n#             print(f\"  E{epoch+1:2d} | R²={mean_r2:.4f} | Dry_Total={r2_scores['Dry_Total_g']:.4f}\")\n        \n#         # Optuna pruning\n#         if trial is not None:\n#             trial.report(weighted_score, epoch)\n#             if trial.should_prune():\n#                 raise optuna.TrialPruned()\n        \n#         # Early stopping\n#         if no_improve >= 8:\n#             break\n    \n#     return best_score, best_per_target, model\n\n\n# # ============ OPTUNA OBJECTIVE ============\n# def objective(trial, train_df, val_df):\n#     \"\"\"Optuna objective - defines search space\"\"\"\n#     config = {\n#         'architecture': trial.suggest_categorical('architecture', ['single', 'dual_stream']),\n#         'head_type': trial.suggest_categorical('head_type', ['simple', 'separate']),\n#         'img_size': trial.suggest_categorical('img_size', [224, 256, 320]),\n#         'epochs': trial.suggest_int('epochs', 10, 40),\n#         'batch_size': trial.suggest_categorical('batch_size', [8, 16, 32]),\n#         'lr': trial.suggest_float('lr', 1e-5, 5e-4, log=True),\n#         'weight_decay': trial.suggest_float('weight_decay', 1e-5, 1e-3, log=True),\n#         'dropout': trial.suggest_float('dropout', 0.1, 0.5),\n#         'aug_multiplier': trial.suggest_int('aug_multiplier', 2, 4),\n#         'rotation': trial.suggest_int('rotation', 0, 20),\n#         'flip_h': trial.suggest_categorical('flip_h', [True, False]),\n#         'flip_v': trial.suggest_categorical('flip_v', [True, False]),\n#         'brightness': trial.suggest_float('brightness', 0, 0.3),\n#         'contrast': trial.suggest_float('contrast', 0, 0.3),\n#         'saturation': trial.suggest_float('saturation', 0, 0.3),\n#         'hue': trial.suggest_float('hue', 0, 0.1),\n#     }\n    \n#     if config['head_type'] == 'hidden':\n#         config['hidden_dim'] = trial.suggest_categorical('hidden_dim', [128, 256, 512])\n    \n#     score, per_target, _ = train_and_evaluate(config, train_df, val_df, trial)\n    \n#     for t, r2 in per_target.items():\n#         trial.set_user_attr(t, r2)\n    \n#     return score\n\n\n# # ============ MAIN AUTOMATION ============\n# def run_fully_automated():\n#     \"\"\"Fully automated: optimize → train → submit\"\"\"\n#     print(\"=\"*70)\n#     print(\"FULLY AUTOMATED OPTIMIZATION & SUBMISSION SYSTEM\")\n#     print(\"=\"*70)\n#     print(f\"\\nDevice: {DEVICE}\")\n#     print(f\"Trials: {N_TRIALS}\")\n#     print(f\"\\nThis will:\")\n#     print(\"  1. Automatically optimize all hyperparameters\")\n#     print(\"  2. Eliminate bad trials early (pruning)\")\n#     print(\"  3. Learn from results (Bayesian optimization)\")\n#     print(\"  4. Train final model with best config\")\n#     print(\"  5. Generate submission.csv\")\n#     print(\"\\n⏳ Estimated time: ~2 hours on GPU\")\n#     print(\"=\"*70)\n    \n#     # Load data\n#     train_df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n#     test_df = pd.read_csv(os.path.join(DATA_DIR, 'test.csv'))\n    \n#     # Split\n#     gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n#     train_idx, val_idx = next(gss.split(train_df, groups=train_df['image_path']))\n#     train_split = train_df.iloc[train_idx]\n#     val_split = train_df.iloc[val_idx]\n    \n#     print(f\"\\nData:\")\n#     print(f\"  Train: {train_split['image_path'].nunique()} images\")\n#     print(f\"  Val: {val_split['image_path'].nunique()} images\")\n#     print(f\"  Test: {test_df['image_path'].nunique()} images\")\n    \n#     # ============ STEP 1: OPTIMIZE ============\n#     print(f\"\\n{'='*70}\")\n#     print(\"STEP 1: AUTOMATIC OPTIMIZATION\")\n#     print(\"=\"*70)\n    \n#     study = optuna.create_study(\n#         direction='maximize',\n#         sampler=optuna.samplers.TPESampler(seed=SEED),\n#         pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=5)\n#     )\n    \n#     study.optimize(\n#         lambda trial: objective(trial, train_split, val_split),\n#         n_trials=N_TRIALS,\n#         show_progress_bar=True\n#     )\n    \n#     print(f\"\\n✓ Optimization complete!\")\n#     print(f\"  Best score: {study.best_value:.4f}\")\n#     print(f\"  Best epochs: {study.best_params['epochs']}\")\n#     print(f\"  Best architecture: {study.best_params['architecture']}\")\n    \n#     # ============ STEP 2: TRAIN FINAL MODEL ============\n#     print(f\"\\n{'='*70}\")\n#     print(\"STEP 2: TRAINING FINAL MODEL WITH BEST CONFIG\")\n#     print(\"=\"*70)\n    \n#     best_config = study.best_params.copy()\n#     print(f\"\\nBest configuration:\")\n#     for k, v in best_config.items():\n#         print(f\"  {k}: {v}\")\n    \n#     print(f\"\\nTraining final model...\")\n#     _, val_scores, final_model = train_and_evaluate(\n#         best_config, train_split, val_split, verbose=True\n#     )\n    \n#     print(f\"\\n✓ Training complete!\")\n#     print(f\"\\nValidation R² per target:\")\n#     for t, r2 in val_scores.items():\n#         marker = \" ⭐\" if t == 'Dry_Total_g' else \"\"\n#         print(f\"  {t:15s}: {r2:.4f}{marker}\")\n    \n#     # ============ STEP 3: GENERATE SUBMISSION ============\n#     print(f\"\\n{'='*70}\")\n#     print(\"STEP 3: GENERATING SUBMISSION\")\n#     print(\"=\"*70)\n    \n#     test_ds = FlexibleDataset(test_df, best_config, augment=False, has_targets=False)\n    \n#     final_model.eval()\n#     predictions = []\n    \n#     with torch.no_grad():\n#         for idx in range(len(test_ds)):\n#             img_data, img_path = test_ds[idx]\n            \n#             if best_config['architecture'] == 'dual_stream':\n#                 left, right = img_data\n#                 left = left.unsqueeze(0).to(DEVICE)\n#                 right = right.unsqueeze(0).to(DEVICE)\n#                 preds = final_model((left, right)).cpu().numpy()[0]\n#             else:\n#                 img = img_data.unsqueeze(0).to(DEVICE)\n#                 preds = final_model(img).cpu().numpy()[0]\n            \n#             for j, target_name in enumerate(TARGETS):\n#                 pred_val = max(0, np.expm1(preds[j]))\n#                 predictions.append({\n#                     'image_path': img_path,\n#                     'target_name': target_name,\n#                     'prediction': pred_val\n#                 })\n    \n#     # Create submission\n#     submission_rows = []\n#     for p in predictions:\n#         match = test_df[\n#             (test_df['image_path'] == p['image_path']) & \n#             (test_df['target_name'] == p['target_name'])\n#         ]\n#         if len(match) > 0:\n#             submission_rows.append({\n#                 'sample_id': match['sample_id'].values[0],\n#                 'target': p['prediction']\n#             })\n    \n#     submission = pd.DataFrame(submission_rows)\n#     submission.to_csv('submission.csv', index=False)\n    \n#     print(f\"\\n✓ Submission saved: submission.csv ({len(submission)} rows)\")\n    \n#     # ============ FINAL SUMMARY ============\n#     print(f\"\\n{'='*70}\")\n#     print(\"AUTOMATION COMPLETE ✓\")\n#     print(\"=\"*70)\n#     print(f\"\\nOptimization results:\")\n#     print(f\"  Trials completed: {len([t for t in study.trials if t.state == TrialState.COMPLETE])}\")\n#     print(f\"  Trials pruned: {len([t for t in study.trials if t.state == TrialState.PRUNED])}\")\n#     print(f\"  Best score: {study.best_value:.4f}\")\n    \n#     print(f\"\\nFinal model:\")\n#     print(f\"  Architecture: {best_config['architecture']}\")\n#     print(f\"  Epochs: {best_config['epochs']}\")\n#     print(f\"  Validation R²: {np.mean(list(val_scores.values())):.4f}\")\n#     print(f\"  Dry_Total_g R²: {val_scores['Dry_Total_g']:.4f}\")\n    \n#     print(f\"\\n✓ Ready to submit: submission.csv\")\n#     print(\"=\"*70)\n\n\n# if __name__ == '__main__':\n#     run_fully_automated()\n# /usr/local/lib/python3.12/dist-packages/sqlalchemy/orm/query.py:195: SyntaxWarning: \"is not\" with 'tuple' literal. Did you mean \"!=\"?\n#   if entities is not ():\n# [I 2026-01-03 14:30:29,151] A new study created in memory with name: no-name-cc081b09-d9e1-48cd-8d71-32c244be79e2\n# ======================================================================\n# FULLY AUTOMATED OPTIMIZATION & SUBMISSION SYSTEM\n# ======================================================================\n\n# Device: cuda\n# Trials: 30\n\n# This will:\n#   1. Automatically optimize all hyperparameters\n#   2. Eliminate bad trials early (pruning)\n#   3. Learn from results (Bayesian optimization)\n#   4. Train final model with best config\n#   5. Generate submission.csv\n\n# ⏳ Estimated time: ~2 hours on GPU\n# ======================================================================\n\n# Data:\n#   Train: 285 images\n#   Val: 72 images\n#   Test: 1 images\n\n# ======================================================================\n# STEP 1: AUTOMATIC OPTIMIZATION\n# ======================================================================\n# Error displaying widget: model not found\n# [I 2026-01-03 14:45:58,876] Trial 0 finished with value: 0.4959974504701622 and parameters: {'architecture': 'dual_stream', 'head_type': 'simple', 'img_size': 224, 'epochs': 36, 'batch_size': 16, 'lr': 0.00044447541666908124, 'weight_decay': 0.000462258900102083, 'dropout': 0.18493564427131048, 'aug_multiplier': 2, 'rotation': 3, 'flip_h': False, 'flip_v': True, 'brightness': 0.18355586841671384, 'contrast': 0.04184815819561255, 'saturation': 0.08764339456056544, 'hue': 0.03663618432936917}. Best is trial 0 with value: 0.4959974504701622.\n# [I 2026-01-03 15:00:12,428] Trial 1 finished with value: 0.540206494531864 and parameters: {'architecture': 'dual_stream', 'head_type': 'separate', 'img_size': 320, 'epochs': 15, 'batch_size': 32, 'lr': 0.000236288641842364, 'weight_decay': 4.066563313514796e-05, 'dropout': 0.13906884560255356, 'aug_multiplier': 4, 'rotation': 9, 'flip_h': False, 'flip_v': False, 'brightness': 0.07763399448000508, 'contrast': 0.19875668530619459, 'saturation': 0.09351332282682329, 'hue': 0.05200680211778108}. Best is trial 1 with value: 0.540206494531864.\n# [I 2026-01-03 15:20:59,713] Trial 2 finished with value: 0.5187305873162965 and parameters: {'architecture': 'single', 'head_type': 'simple', 'img_size': 224, 'epochs': 38, 'batch_size': 16, 'lr': 3.5704789209099774e-05, 'weight_decay': 5.989003672254293e-05, 'dropout': 0.20853961270955837, 'aug_multiplier': 4, 'rotation': 7, 'flip_h': False, 'flip_v': False, 'brightness': 0.022365193103931248, 'contrast': 0.2960660809801552, 'saturation': 0.23167343078899721, 'hue': 0.019871568153417243}. Best is trial 1 with value: 0.540206494531864.\n# [I 2026-01-03 15:31:52,347] Trial 3 finished with value: 0.40231847218788386 and parameters: {'architecture': 'dual_stream', 'head_type': 'separate', 'img_size': 224, 'epochs': 13, 'batch_size': 8, 'lr': 1.2822825454807553e-05, 'weight_decay': 4.187594718900626e-05, 'dropout': 0.23007332881069884, 'aug_multiplier': 4, 'rotation': 13, 'flip_h': True, 'flip_v': False, 'brightness': 0.22823551458506922, 'contrast': 0.16838315927084888, 'saturation': 0.2312901539863683, 'hue': 0.04937955963643908}. Best is trial 1 with value: 0.540206494531864.\n# [I 2026-01-03 15:41:48,768] Trial 4 finished with value: 0.5056276044376179 and parameters: {'architecture': 'single', 'head_type': 'separate', 'img_size': 256, 'epochs': 25, 'batch_size': 8, 'lr': 0.00019215811115723036, 'weight_decay': 2.8681134821030064e-05, 'dropout': 0.1307919639315172, 'aug_multiplier': 2, 'rotation': 3, 'flip_h': True, 'flip_v': False, 'brightness': 0.24110162306973432, 'contrast': 0.05597101766581075, 'saturation': 0.2677676995469933, 'hue': 0.05393422419156507}. Best is trial 1 with value: 0.540206494531864.\n# [I 2026-01-03 16:04:15,746] Trial 6 pruned. \n# [I 2026-01-03 16:09:52,226] Trial 7 pruned. \n# [I 2026-01-03 16:12:09,609] Trial 8 pruned. \n# [I 2026-01-03 17:10:19,777] Trial 12 finished with value: 0.6208291587459096 and parameters: {'architecture': 'dual_stream', 'head_type': 'simple', 'img_size': 320, 'epochs': 29, 'batch_size': 16, 'lr': 7.734194627945694e-05, 'weight_decay': 0.0009586732085552352, 'dropout': 0.4984067957444855, 'aug_multiplier': 3, 'rotation': 6, 'flip_h': True, 'flip_v': False, 'brightness': 0.10554709281561507, 'contrast': 0.004004726421974124, 'saturation': 0.16223080123885233, 'hue': 0.0002879090395443357}. Best is trial 12 with value: 0.6208291587459096.\n# [I 2026-01-03 17:14:37,687] Trial 13 pruned. \n# [I 2026-01-03 17:29:31,636] Trial 15 pruned.","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"raw","source":"","metadata":{}},{"cell_type":"code","source":"print('hI')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nFinal Biomass Prediction Model - Single Best Fold\nCompetition: CSIRO - Image2Biomass Prediction\n\nBest Fold: Fold 1 (10-Fold split) achieves wR² ~0.84\nWorks on: Kaggle, Google Colab, Local\n\"\"\"\n\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models as torchvision_models\nfrom sklearn.model_selection import GroupKFold\n\n# ============ COLAB SETUP ============\ntry:\n    from google.colab import drive\n    IS_COLAB = True\nexcept:\n    IS_COLAB = False\n\nif IS_COLAB and not os.path.exists('/content/drive'):\n    try:\n        drive.mount('/content/drive')\n        print(\"✓ Google Drive mounted\")\n    except:\n        pass\n\n# ============ CONFIG ============\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nIS_KAGGLE = os.path.exists('/kaggle/working')\n\nif IS_KAGGLE:\n    DATA_DIR = '/kaggle/input/csiro-biomass'\n    TRAIN_CSV = os.path.join(DATA_DIR, 'train.csv')\n    TEST_CSV = os.path.join(DATA_DIR, 'test.csv')\n    TRAIN_FOLDER = os.path.join(DATA_DIR, 'train')\n    TEST_FOLDER = os.path.join(DATA_DIR, 'test')\n    RESNET18_PATH = \"/kaggle/input/resnet18/resnet18-f37072fd.pth\"\n    OUTPUT_DIR = '/kaggle/working'\nelif IS_COLAB:\n    DATA_DIR = '/content/drive/MyDrive'\n    TRAIN_CSV = os.path.join(DATA_DIR, 'train.csv')\n    TEST_CSV = os.path.join(DATA_DIR, 'test.csv')\n    TRAIN_FOLDER = os.path.join(DATA_DIR, 'train')\n    TEST_FOLDER = os.path.join(DATA_DIR, 'test')\n    RESNET18_PATH = os.path.join(DATA_DIR, 'resnet18-f37072fd.pth')\n    OUTPUT_DIR = '/content'\nelse:\n    DATA_DIR = 'kaggle/input/csiro-biomass'\n    TRAIN_CSV = os.path.join(DATA_DIR, 'train.csv')\n    TEST_CSV = os.path.join(DATA_DIR, 'test.csv')\n    TRAIN_FOLDER = os.path.join(DATA_DIR, 'train')\n    TEST_FOLDER = os.path.join(DATA_DIR, 'test')\n    RESNET18_PATH = \"kaggle/input/resnet18/resnet18-f37072fd.pth\"\n    OUTPUT_DIR = '.'\n\nTARGETS = ['Dry_Total_g', 'Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g']\nTARGET_WEIGHTS = {'Dry_Total_g': 0.5, 'GDM_g': 0.2, 'Dry_Green_g': 0.1, 'Dry_Dead_g': 0.1, 'Dry_Clover_g': 0.1}\n\n# Best config\nN_FOLDS = 10\nBEST_FOLD = 1  # Fold 1 achieves wR² ~0.84\nIMG_SIZE = 384\nBATCH_SIZE = 16\nEPOCHS = 25\nLR = 0.00011\nDROPOUT = 0.36\nWEIGHT_DECAY = 7.6e-05\nSEED = 42\n\n\ndef set_seed(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n\n\ndef make_square(img):\n    w, h = img.size\n    if w == h:\n        return img\n    s = max(w, h)\n    sq = Image.new('RGB', (s, s), (0, 0, 0))\n    sq.paste(img, ((s - w) // 2, (s - h) // 2))\n    return sq\n\n\nclass DualStreamDataset(Dataset):\n    def __init__(self, df, img_folder, augment=False, has_targets=True):\n        self.augment = augment\n        self.has_targets = has_targets\n        self.groups = df.groupby('image_path')\n        self.paths = list(self.groups.groups.keys())\n        self.img_folder = img_folder\n        \n        self.transform_aug = transforms.Compose([\n            transforms.Resize((IMG_SIZE, IMG_SIZE)),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.RandomVerticalFlip(p=0.5),\n            transforms.ColorJitter(0.13, 0.17, 0.13, 0.05),\n            transforms.ToTensor(),\n            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n        ])\n        self.transform_normal = transforms.Compose([\n            transforms.Resize((IMG_SIZE, IMG_SIZE)),\n            transforms.ToTensor(),\n            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n        ])\n\n    def __len__(self):\n        return len(self.paths) * 3 if self.augment else len(self.paths)\n\n    def __getitem__(self, idx):\n        if self.augment:\n            real_idx = idx // 3\n            is_aug = (idx % 3 != 0)\n        else:\n            real_idx = idx\n            is_aug = False\n        \n        path = self.paths[real_idx]\n        if path.startswith('train/'):\n            img_filename = path.replace('train/', '', 1)\n        elif path.startswith('test/'):\n            img_filename = path.replace('test/', '', 1)\n        else:\n            img_filename = path\n        \n        img = Image.open(os.path.join(self.img_folder, img_filename)).convert('RGB')\n        img = make_square(img)\n        w, h = img.size\n        \n        t = self.transform_aug if is_aug else self.transform_normal\n        left = t(img.crop((0, 0, w//2, h)))\n        right = t(img.crop((w//2, 0, w, h)))\n        \n        if self.has_targets:\n            group = self.groups.get_group(path)\n            targets = np.zeros(5, dtype=np.float32)\n            raw = np.zeros(5, dtype=np.float32)\n            for _, row in group.iterrows():\n                if row['target_name'] in TARGETS:\n                    i = TARGETS.index(row['target_name'])\n                    targets[i] = np.log1p(row['target'])\n                    raw[i] = row['target']\n            return left, right, torch.from_numpy(targets), torch.from_numpy(raw)\n        \n        return left, right, path\n\n\nclass DualStreamModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = torchvision_models.resnet18(weights=None)\n        if os.path.exists(RESNET18_PATH):\n            self.backbone.load_state_dict(torch.load(RESNET18_PATH, map_location='cpu', weights_only=True))\n            print(f\"✓ Loaded: {RESNET18_PATH}\")\n        elif not IS_KAGGLE:\n            self.backbone = torchvision_models.resnet18(weights=torchvision_models.ResNet18_Weights.DEFAULT)\n        else:\n            raise RuntimeError(\"Offline weights required for Kaggle!\")\n        self.backbone.fc = nn.Identity()\n        self.head = nn.Sequential(nn.Dropout(DROPOUT), nn.Linear(1024, 5))\n    \n    def forward(self, left, right):\n        feat_l = self.backbone(left)\n        feat_r = self.backbone(right)\n        return self.head(torch.cat([feat_l, feat_r], dim=1))\n\n\ndef calc_weighted_r2(preds_all, actuals_all):\n    all_preds, all_actuals, all_weights = [], [], []\n    for t in TARGETS:\n        w = TARGET_WEIGHTS[t]\n        for p, a in zip(preds_all[t], actuals_all[t]):\n            all_preds.append(p)\n            all_actuals.append(a)\n            all_weights.append(w)\n    all_preds, all_actuals, all_weights = np.array(all_preds), np.array(all_actuals), np.array(all_weights)\n    y_bar = np.sum(all_weights * all_actuals) / np.sum(all_weights)\n    ss_res = np.sum(all_weights * (all_actuals - all_preds) ** 2)\n    ss_tot = np.sum(all_weights * (all_actuals - y_bar) ** 2) + 1e-8\n    return 1 - ss_res / ss_tot\n\n\ndef train_model(train_df, val_df):\n    set_seed(SEED + BEST_FOLD)\n    \n    train_ds = DualStreamDataset(train_df, TRAIN_FOLDER, augment=True)\n    val_ds = DualStreamDataset(val_df, TRAIN_FOLDER, augment=False)\n    train_loader = DataLoader(train_ds, BATCH_SIZE, shuffle=True, num_workers=2)\n    val_loader = DataLoader(val_ds, BATCH_SIZE, num_workers=2)\n    \n    print(f\"Train: {len(train_ds)//3} imgs | Val: {len(val_ds)} imgs\")\n    \n    model = DualStreamModel().to(DEVICE)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=4)\n    criterion = nn.SmoothL1Loss()\n    \n    best_r2, best_state = -999, None\n    \n    for epoch in range(EPOCHS):\n        model.train()\n        for left, right, targets, _ in train_loader:\n            optimizer.zero_grad()\n            loss = criterion(model(left.to(DEVICE), right.to(DEVICE)), targets.to(DEVICE))\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            optimizer.step()\n        \n        model.eval()\n        preds_all = {t: [] for t in TARGETS}\n        actuals_all = {t: [] for t in TARGETS}\n        \n        with torch.no_grad():\n            for left, right, _, raw in val_loader:\n                p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()\n                for i in range(p.shape[0]):\n                    for j, t in enumerate(TARGETS):\n                        preds_all[t].append(max(0, np.expm1(p[i, j])))\n                        actuals_all[t].append(raw[i, j].item())\n        \n        r2 = calc_weighted_r2(preds_all, actuals_all)\n        marker = \"\"\n        if r2 > best_r2:\n            best_r2 = r2\n            best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}\n            marker = \" *\"\n        \n        print(f\"E{epoch+1:2d}/{EPOCHS} | wR²={r2:.4f} | Best={best_r2:.4f}{marker}\")\n        scheduler.step(r2)\n    \n    model.load_state_dict(best_state)\n    return model, best_r2\n\n\ndef predict_with_tta(model, test_df):\n    \"\"\"TTA: average original + flipped predictions\"\"\"\n    test_ds = DualStreamDataset(test_df, TEST_FOLDER, augment=False, has_targets=False)\n    model.eval()\n    predictions = []\n    \n    transform = transforms.Compose([\n        transforms.Resize((IMG_SIZE, IMG_SIZE)),\n        transforms.ToTensor(),\n        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n    ])\n    \n    with torch.no_grad():\n        for idx in range(len(test_ds)):\n            _, _, path = test_ds[idx]\n            \n            if path.startswith('test/'):\n                img_filename = path.replace('test/', '', 1)\n            else:\n                img_filename = path\n            \n            img = Image.open(os.path.join(TEST_FOLDER, img_filename)).convert('RGB')\n            img = make_square(img)\n            w, h = img.size\n            \n            all_preds = []\n            augments = [\n                lambda x: x,\n                lambda x: x.transpose(Image.FLIP_LEFT_RIGHT),\n                lambda x: x.transpose(Image.FLIP_TOP_BOTTOM),\n                lambda x: x.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.FLIP_TOP_BOTTOM),\n            ]\n            \n            for aug_fn in augments:\n                aug_img = aug_fn(img)\n                left = transform(aug_img.crop((0, 0, w//2, h))).unsqueeze(0)\n                right = transform(aug_img.crop((w//2, 0, w, h))).unsqueeze(0)\n                p = model(left.to(DEVICE), right.to(DEVICE)).cpu().numpy()[0]\n                all_preds.append(p)\n            \n            avg_pred = np.mean(all_preds, axis=0)\n            \n            for j, t in enumerate(TARGETS):\n                predictions.append({\n                    'image_path': path,\n                    'target_name': t,\n                    'prediction': max(0, np.expm1(avg_pred[j]))\n                })\n    \n    return predictions\n\n\nif __name__ == '__main__':\n    print(\"=\"*60)\n    print(f\"CSIRO Biomass - Single Best Fold (Fold {BEST_FOLD})\")\n    print(\"=\"*60)\n    print(f\"Device: {DEVICE}\")\n    print(f\"Environment: {'Kaggle' if IS_KAGGLE else 'Colab' if IS_COLAB else 'Local'}\")\n    print(f\"Target: wR² ~0.84\")\n    \n    # Load data\n    if IS_COLAB and not os.path.exists(TRAIN_CSV):\n        print(\"Loading from Google Sheets...\")\n        os.system('pip install -q gspread')\n        from google.colab import auth\n        import gspread\n        from google.auth import default\n        auth.authenticate_user()\n        creds, _ = default()\n        gc = gspread.authorize(creds)\n        data = gc.open('train').get_worksheet(0).get_all_values()\n        train_df = pd.DataFrame(data[1:], columns=data[0])\n        for col in train_df.columns:\n            try:\n                train_df[col] = pd.to_numeric(train_df[col])\n            except:\n                pass\n        \n        test_df = None\n        try:\n            data = gc.open('test').get_worksheet(0).get_all_values()\n            test_df = pd.DataFrame(data[1:], columns=data[0])\n        except:\n            pass\n    else:\n        train_df = pd.read_csv(TRAIN_CSV)\n        test_df = pd.read_csv(TEST_CSV) if os.path.exists(TEST_CSV) else None\n    \n    print(f\"Data: {train_df['image_path'].nunique()} images\")\n    \n    # Get fold split\n    gkf = GroupKFold(n_splits=N_FOLDS)\n    fold_indices = list(gkf.split(train_df, groups=train_df['image_path']))\n    train_idx, val_idx = fold_indices[BEST_FOLD - 1]\n    \n    train_split = train_df.iloc[train_idx]\n    val_split = train_df.iloc[val_idx]\n    print(f\"Fold {BEST_FOLD}: Train {train_split['image_path'].nunique()} | Val {val_split['image_path'].nunique()}\")\n    \n    # Train\n    print(f\"\\n{'='*60}\")\n    print(\"TRAINING\")\n    print(\"=\"*60)\n    model, best_r2 = train_model(train_split, val_split)\n    print(f\"\\nFinal wR²: {best_r2:.4f}\")\n    \n    # Save model\n    model_path = os.path.join(OUTPUT_DIR, 'best_model.pth')\n    torch.save(model.state_dict(), model_path)\n    print(f\"✓ Saved: {model_path}\")\n    \n    # Generate submission\n    if test_df is not None and len(test_df) > 0:\n        print(f\"\\n{'='*60}\")\n        print(\"PREDICTION (with TTA)\")\n        print(\"=\"*60)\n        \n        predictions = predict_with_tta(model, test_df)\n        \n        rows = []\n        for p in predictions:\n            m = test_df[(test_df['image_path'] == p['image_path']) & (test_df['target_name'] == p['target_name'])]\n            if len(m) > 0:\n                rows.append({'sample_id': m['sample_id'].values[0], 'target': p['prediction']})\n        \n        submission = pd.DataFrame(rows)\n        submission.to_csv('submission.csv', index=False)\n        print(f\"✓ submission.csv saved ({len(rows)} rows)\")\n    \n    print(f\"\\n{'='*60}\")\n    print(\"DONE!\")\n    print(\"=\"*60)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T12:12:40.207806Z","iopub.execute_input":"2026-01-08T12:12:40.208270Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"22\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T14:43:35.709042Z","iopub.execute_input":"2026-01-07T14:43:35.709811Z","iopub.status.idle":"2026-01-07T14:43:35.714930Z","shell.execute_reply.started":"2026-01-07T14:43:35.709734Z","shell.execute_reply":"2026-01-07T14:43:35.713607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T14:43:37.155739Z","iopub.execute_input":"2026-01-07T14:43:37.156572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"hi\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T19:48:56.673980Z","iopub.execute_input":"2026-01-07T19:48:56.674334Z","iopub.status.idle":"2026-01-07T19:48:56.684169Z","shell.execute_reply.started":"2026-01-07T19:48:56.674306Z","shell.execute_reply":"2026-01-07T19:48:56.683102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}