{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13852231,"sourceType":"datasetVersion","datasetId":8823915},{"sourceId":13969945,"sourceType":"datasetVersion","datasetId":8905857},{"sourceId":14549047,"sourceType":"datasetVersion","datasetId":9292546}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import glob\nimport os\nimport random\nfrom pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torchvision.models as tv\nimport torchvision.transforms as T\nfrom sklearn.linear_model import Ridge\nfrom sklearn.model_selection import StratifiedGroupKFold, train_test_split\nfrom torch.utils.data import DataLoader, Dataset, TensorDataset\n\n# pd.set_option('display.max_columns', None)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:07.902895Z","iopub.execute_input":"2026-01-15T13:12:07.903165Z","iopub.status.idle":"2026-01-15T13:12:15.689874Z","shell.execute_reply.started":"2026-01-15T13:12:07.903144Z","shell.execute_reply":"2026-01-15T13:12:15.689276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# reproducibility package\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:19.220728Z","iopub.execute_input":"2026-01-15T13:12:19.221398Z","iopub.status.idle":"2026-01-15T13:12:19.230932Z","shell.execute_reply.started":"2026-01-15T13:12:19.221374Z","shell.execute_reply":"2026-01-15T13:12:19.230174Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset creation","metadata":{}},{"cell_type":"code","source":"train = pd.read_csv('/kaggle/input/csiro-biomass/train.csv')\n\ntrain","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:21.148539Z","iopub.execute_input":"2026-01-15T13:12:21.149253Z","iopub.status.idle":"2026-01-15T13:12:21.190258Z","shell.execute_reply.started":"2026-01-15T13:12:21.149226Z","shell.execute_reply":"2026-01-15T13:12:21.189687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train.info()\nprint('\\n'f'Missing values: {train.isna().sum().sum()}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:23.379411Z","iopub.execute_input":"2026-01-15T13:12:23.379892Z","iopub.status.idle":"2026-01-15T13:12:23.399051Z","shell.execute_reply.started":"2026-01-15T13:12:23.379871Z","shell.execute_reply":"2026-01-15T13:12:23.398409Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:25.134851Z","iopub.execute_input":"2026-01-15T13:12:25.135418Z","iopub.status.idle":"2026-01-15T13:12:25.154299Z","shell.execute_reply.started":"2026-01-15T13:12:25.135390Z","shell.execute_reply":"2026-01-15T13:12:25.153660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train.describe(include='object')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:27.044201Z","iopub.execute_input":"2026-01-15T13:12:27.044494Z","iopub.status.idle":"2026-01-15T13:12:27.063256Z","shell.execute_reply.started":"2026-01-15T13:12:27.044454Z","shell.execute_reply":"2026-01-15T13:12:27.062589Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*Checked the data: no missing values or constant features.*","metadata":{}},{"cell_type":"code","source":"# building a dataset with unique images and their five target values\ndf_image = (\n    train.pivot_table(index='image_path',\n                   columns='target_name',\n                   values='target')\n    .reset_index()\n    .rename_axis(None, axis=1)  \n)\n\ndf_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:28.786054Z","iopub.execute_input":"2026-01-15T13:12:28.786898Z","iopub.status.idle":"2026-01-15T13:12:28.818650Z","shell.execute_reply.started":"2026-01-15T13:12:28.786872Z","shell.execute_reply":"2026-01-15T13:12:28.817875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"meta = train[[\"image_path\", \"State\", \"Sampling_Date\"]].drop_duplicates()\ndf_image = df_image.merge(meta, on=\"image_path\", how=\"left\")\n\ndf_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:31.881688Z","iopub.execute_input":"2026-01-15T13:12:31.881962Z","iopub.status.idle":"2026-01-15T13:12:31.903348Z","shell.execute_reply.started":"2026-01-15T13:12:31.881939Z","shell.execute_reply":"2026-01-15T13:12:31.902625Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test = pd.read_csv('/kaggle/input/csiro-biomass/test.csv')\ntest['target'] = 0\n\ndf_image_test = (\n    test.pivot_table(index='image_path',\n                   columns='target_name',\n                   values='target')\n    .reset_index()\n    .rename_axis(None, axis=1)  \n)\n\ndf_image_test","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:12:38.616915Z","iopub.execute_input":"2026-01-15T13:12:38.617191Z","iopub.status.idle":"2026-01-15T13:12:38.637314Z","shell.execute_reply.started":"2026-01-15T13:12:38.617168Z","shell.execute_reply":"2026-01-15T13:12:38.636587Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualization of images before and after transformations","metadata":{}},{"cell_type":"code","source":"X = df_image[\"image_path\"]\ny = df_image.drop([\"image_path\", \"State\", \"Sampling_Date\"], axis=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:13:36.815560Z","iopub.execute_input":"2026-01-15T13:13:36.815842Z","iopub.status.idle":"2026-01-15T13:13:36.820313Z","shell.execute_reply.started":"2026-01-15T13:13:36.815819Z","shell.execute_reply":"2026-01-15T13:13:36.819603Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*I create two transformations for further ensemble training on two models with two different resolutions.*","metadata":{}},{"cell_type":"code","source":"IMG_SIZE_256 = 256 \n\ntrain_tf_256 = T.Compose([\n    T.RandomResizedCrop(\n        size=IMG_SIZE_256,\n        scale=(0.5, 1.0),\n        ratio=(0.75, 1.25),\n    ),\n    T.RandomHorizontalFlip(p=0.5),\n    T.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15),\n    T.ToTensor(),  \n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])\n\nval_tf_256 = T.Compose([\n    T.Resize((IMG_SIZE_256, IMG_SIZE_256)),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:01.935857Z","iopub.execute_input":"2026-01-12T08:55:01.936181Z","iopub.status.idle":"2026-01-12T08:55:01.941418Z","shell.execute_reply.started":"2026-01-12T08:55:01.936159Z","shell.execute_reply":"2026-01-12T08:55:01.940695Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE_384 = 384 \n\ntrain_tf_384 = T.Compose([\n    T.RandomResizedCrop(\n        size=IMG_SIZE_384,\n        scale=(0.5, 1.0),\n        ratio=(0.75, 1.25),\n    ),\n    T.RandomHorizontalFlip(p=0.5),\n    T.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15),\n    T.ToTensor(),  \n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])\n\nval_tf_384 = T.Compose([\n    T.Resize((IMG_SIZE_384, IMG_SIZE_384)),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:04.318449Z","iopub.execute_input":"2026-01-12T08:55:04.318727Z","iopub.status.idle":"2026-01-12T08:55:04.323826Z","shell.execute_reply.started":"2026-01-12T08:55:04.318706Z","shell.execute_reply":"2026-01-12T08:55:04.323272Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE_whole = 384  \n\ntrain_tf_whole = T.Compose([\n    T.Resize((IMG_SIZE_whole, IMG_SIZE_whole)),         \n    T.RandomHorizontalFlip(p=0.5),\n    T.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n])\n\nval_tf_whole = T.Compose([\n    T.Resize((IMG_SIZE_whole, IMG_SIZE_whole)),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# set the dataset root\nDATA_ROOT = Path(\"/kaggle/input/csiro-biomass/train\")\n\ndef visualize_before_after(paths_series, transform, n_samples=5, root: Path = None):\n    \"\"\"\n    takes several random paths x, \n    opens images via root / <relative_path>, \n    shows the original and the transformed version (resize+totor), \n    prints pixel sizes and tensor shape\n    \"\"\"\n    # get a list of paths to 5 random images\n    sample_paths = random.sample(list(paths_series), min(n_samples, len(paths_series)))    \n\n    fig, axes = plt.subplots(len(sample_paths), 2, figsize=(11, 4 * len(sample_paths)))\n    # safety check for the case len(sample_paths) = 1, otherwise it would crash\n    if len(sample_paths) == 1:\n        axes = [axes]\n\n    for i, rel_path in enumerate(sample_paths):\n        # read the original image\n        abs_path = root / Path(rel_path).name \n        # read and ensure rgb format\n        img = Image.open(abs_path).convert(\"RGB\") \n        # pil stores (width, height)\n        ow, oh = img.size                        \n        orig_pixels = ow * oh\n\n        # plot the original image\n        ax0 = axes[i][0]\n        ax0.imshow(img)\n        ax0.set_title(f\"Original\\n{ow}×{oh}px ({orig_pixels:,} px.)\", fontsize=10)\n        ax0.axis(\"off\")\n\n        # apply the transform\n        tens = transform(img)       \n        c, h, w = tens.shape\n        new_pixels = h * w\n\n        # convert back chw → hwc and numpy for visualization\n        ax1 = axes[i][1]\n        ax1.imshow(tens.permute(1, 2, 0).numpy())\n        ax1.set_title(\n            f\"After transform\\n{w}×{h}px ({new_pixels:,}) | tensor {tuple(tens.shape)}\",\n            fontsize=10\n        )\n        ax1.axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()\n\n    # text log with sizes\n    print(\"Sample summary:\")\n    for rel_path in sample_paths:\n        abs_path = root / Path(rel_path).name\n        img = Image.open(abs_path).convert(\"RGB\")\n        ow, oh = img.size\n        tens = transform(img)\n        c, h, w = tens.shape\n        print(f\"- {abs_path.name:>20} | before: {ow}×{oh} ({ow*oh:,}) → after: {w}×{h} ({w*h:,}), tensor={tuple(tens.shape)}\")\n\nvisualize_before_after(X, transform=train_tf_384, n_samples=2, root=DATA_ROOT)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:07.716684Z","iopub.execute_input":"2026-01-12T08:55:07.717169Z","iopub.status.idle":"2026-01-12T08:55:09.066100Z","shell.execute_reply.started":"2026-01-12T08:55:07.717146Z","shell.execute_reply":"2026-01-12T08:55:09.065403Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Building the network architecture","metadata":{}},{"cell_type":"code","source":"class PastureDataset(Dataset):\n    def __init__(self, paths, targets=None, transform=None, root=None):\n        \"\"\"\n        paths: pd.Series with relative or absolute image paths\n        targets: pd.DataFrame or None (for test); columns = 5 targets\n        transform: torchvision.transforms.Compose\n        root: optional common root for relative paths\n        \"\"\"\n        # pandas.Series of length N containing image paths\n        self.paths = paths.reset_index(drop=True)\n        self.has_targets = targets is not None\n        self.transform = transform\n        self.root = Path(root) if root is not None else None\n\n        if self.has_targets:\n            # keep the original column order for consistency\n            self.target_cols = list(targets.columns)\n            # torch.FloatTensor of shape [N, 5] (N rows, 5 target values)\n            self.targets = torch.tensor(targets.values, dtype=torch.float32)\n        else:\n            self.target_cols = None\n            self.targets = None\n\n    def __len__(self):\n        return len(self.paths)\n\n    def _resolve_path(self, p):\n        # return absolute path if root is provided and path is relative\n        if self.root is not None and not os.path.isabs(p):\n            return os.path.join(self.root, p)\n        return p\n\n    def __getitem__(self, idx):\n        # image file name, e.g. \"ID1011485656.jpg\" or \"train/ID1011485656.jpg\"\n        fname = Path(self.paths.iloc[idx]).name\n        # absolute path = root + file name\n        img_path = self.root / fname\n\n        # open the image and convert to rgb\n        img = Image.open(img_path).convert(\"RGB\")\n\n        # apply transforms\n        if self.transform:\n            img = self.transform(img)\n\n        if self.has_targets:\n            # target vector [5]\n            target = self.targets[idx]\n            return img, target\n        else:\n            # return image path for test mode\n            return img, str(img_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:17.263574Z","iopub.execute_input":"2026-01-12T08:55:17.263822Z","iopub.status.idle":"2026-01-12T08:55:17.270900Z","shell.execute_reply.started":"2026-01-12T08:55:17.263804Z","shell.execute_reply":"2026-01-12T08:55:17.270190Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*Create the competition metric for evaluating the model architecture*","metadata":{}},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# metric weights in the required order\nWEIGHTS_MAP = {\n    'Dry_Green_g': 0.1,\n    'Dry_Dead_g':  0.1,\n    'Dry_Clover_g':0.1,\n    'GDM_g':       0.2,\n    'Dry_Total_g': 0.5,\n}\n\n# rearrange weights to match y columns order\nWEIGHTS = torch.tensor([WEIGHTS_MAP[c] for c in y.columns],\n                       dtype=torch.float32, device=device)\n\n# r2 for each target\ndef r2_per_target(y_true: torch.Tensor, y_pred: torch.Tensor, eps=1e-9):\n    # compute mean value of each target: y_true [n,5] → mean [1,5]\n    mean = y_true.mean(dim=0, keepdim=True)\n    # mse residual sum for each target\n    ss_res = ((y_true - y_pred) ** 2).sum(dim=0)\n    # total variance (mse vs mean) for each target\n    ss_tot = ((y_true - mean) ** 2).sum(dim=0) + eps\n    # r2 value for each target\n    return 1.0 - ss_res / ss_tot\n\n# weighted r2\ndef weighted_r2(y_true: torch.Tensor, y_pred: torch.Tensor, weights: torch.Tensor):\n    # compute r2 for each target (vector of 5 values)\n    r2_vec = r2_per_target(y_true, y_pred)\n    # normalize weights (sum = 1) as a safety check\n    w = weights / (weights.sum() + 1e-9)\n    # first value = weighted metric, second = per-target r2 vector\n    return (r2_vec * w).sum(), r2_vec","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:18:02.789329Z","iopub.execute_input":"2026-01-15T13:18:02.789910Z","iopub.status.idle":"2026-01-15T13:18:03.007808Z","shell.execute_reply.started":"2026-01-15T13:18:02.789887Z","shell.execute_reply":"2026-01-15T13:18:03.007225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 32\nNUM_TARGETS = 5\n# the weights of pre-trained models are stored here\nLOCAL_W_34 = Path(\"/kaggle/input/resnet34/resnet34_imagenet_weights.pth\")\nLOCAL_W_50 = Path(\"/kaggle/input/resnet50/resnet50_imagenet1k_v1.pth\")\nLOCAL_W_convnext_tiny = Path('/kaggle/input/convnext-tiny-pretrained/convnext_tiny_pretrained.pth')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:27.321577Z","iopub.execute_input":"2026-01-12T08:55:27.322135Z","iopub.status.idle":"2026-01-12T08:55:27.327792Z","shell.execute_reply.started":"2026-01-12T08:55:27.322109Z","shell.execute_reply":"2026-01-12T08:55:27.327227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_resnet34_regressor(num_targets=NUM_TARGETS, weights_path=LOCAL_W_34):\n    m = tv.resnet34(weights=None)\n\n    # load pretrained weights from a local file\n    state_dict = torch.load(weights_path, map_location=\"cpu\")\n\n    # sometimes checkpoints contain {'state_dict': ...}, so we check for that\n    if isinstance(state_dict, dict) and \"state_dict\" in state_dict:\n        state_dict = state_dict[\"state_dict\"]\n\n    # load weights while skipping mismatched layers (fc will be replaced anyway)\n    m.load_state_dict(state_dict, strict=False)\n\n    # replace the head for regression\n    in_feats = m.fc.in_features\n    m.fc = nn.Sequential(\n        nn.Dropout(p=0.1),\n        nn.Linear(in_feats, num_targets)\n    )\n\n    return m","metadata":{"execution":{"iopub.status.busy":"2026-01-12T08:55:29.229098Z","iopub.execute_input":"2026-01-12T08:55:29.229358Z","iopub.status.idle":"2026-01-12T08:55:29.234252Z","shell.execute_reply.started":"2026-01-12T08:55:29.229339Z","shell.execute_reply":"2026-01-12T08:55:29.233433Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_resnet50_regressor(num_targets=NUM_TARGETS, weights_path=LOCAL_W_50):\n    m = tv.resnet50(weights=None)\n\n    # load pretrained weights from a local file\n    state_dict = torch.load(weights_path, map_location=\"cpu\")\n\n    # sometimes checkpoints contain {'state_dict': ...}, so we check for that\n    if isinstance(state_dict, dict) and \"state_dict\" in state_dict:\n        state_dict = state_dict[\"state_dict\"]\n\n    # load weights while skipping mismatched layers\n    m.load_state_dict(state_dict, strict=False)\n    \n    # replace the head for regression\n    in_feats = m.fc.in_features\n    m.fc = nn.Sequential(\n        nn.Dropout(p=0.1),\n        nn.Linear(in_feats, num_targets)\n    )\n\n    return m","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:31.435155Z","iopub.execute_input":"2026-01-12T08:55:31.435622Z","iopub.status.idle":"2026-01-12T08:55:31.440493Z","shell.execute_reply.started":"2026-01-12T08:55:31.435600Z","shell.execute_reply":"2026-01-12T08:55:31.439776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm\n\nclass ConvNeXtRegressor(nn.Module):\n    def __init__(self, backbone, num_targets):\n        super().__init__()\n        self.backbone = backbone\n        in_features = backbone.num_features\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.head = nn.Linear(in_features, num_targets)\n\n    def forward(self, x):\n        x = self.backbone.forward_features(x)   # (B,C,H,W)\n        x = self.pool(x).flatten(1)             # (B,C)\n        x = self.head(x)                        # (B,num_targets)\n        return x\n\n\ndef build_convnext_regressor(\n    num_targets,\n    model_name=\"convnext_tiny\",\n    weights_path=LOCAL_W_convnext_tiny,\n):\n    backbone = timm.create_model(model_name, pretrained=False)\n\n    state_dict = torch.load(weights_path, map_location=\"cpu\")\n    backbone.load_state_dict(state_dict, strict=False)\n\n    model = ConvNeXtRegressor(backbone, num_targets)\n    return model","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# create directory for saving checkpoints if it does not exist yet\nCKPT_DIR = \"checkpoints\"\nos.makedirs(CKPT_DIR, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T08:55:33.436870Z","iopub.execute_input":"2026-01-12T08:55:33.437160Z","iopub.status.idle":"2026-01-12T08:55:33.440912Z","shell.execute_reply.started":"2026-01-12T08:55:33.437141Z","shell.execute_reply":"2026-01-12T08:55:33.440226Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*Training the resnet34 model on images with a resolution of 256x256.*","metadata":{}},{"cell_type":"code","source":"%%time\n\ntrain_ds_256 = PastureDataset(X, y, transform=train_tf_256, root=DATA_ROOT)\n\ng = torch.Generator().manual_seed(SEED)\n\ntrain_loader = DataLoader(\n    train_ds_256,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n    prefetch_factor=2,\n    generator=g\n)\n\nmodel34 = build_resnet34_regressor(num_targets=y.shape[1], weights_path=LOCAL_W_34).to(device)\n\nbackbone_params = [p for n, p in model34.named_parameters() if not n.startswith(\"fc.\")]\nhead_params     = [p for n, p in model34.named_parameters() if n.startswith(\"fc.\")]\noptimizer = torch.optim.AdamW([\n    {\"params\": backbone_params, \"lr\": 1e-4, \"weight_decay\": 1e-4},\n    {\"params\": head_params,     \"lr\": 1e-3, \"weight_decay\": 1e-4},\n])\n\nepochs = 35\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\nloss_func = nn.SmoothL1Loss()\n\nfor epoch in range(1, epochs + 1):\n    model34.train()\n    tr_running, tr_batches = 0.0, 0\n\n    for xb, yb in train_loader:\n        xb = xb.to(device, non_blocking=True)\n        yb = yb.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n        preds = model34(xb)\n        loss = loss_func(preds, yb)\n        loss.backward()\n        optimizer.step()\n\n        tr_running += loss.item()\n        tr_batches += 1\n\n    scheduler.step()\n\n    print(f\"epoch {epoch:02d} | train_loss {tr_running/max(tr_batches,1):.4f}\")\n\n# save final checkpoint\nfinal_state = {k: v.detach().cpu().clone() for k, v in model34.state_dict().items()}\ntorch.save(\n    {\n        \"state_dict\": final_state,\n        \"target_cols\": list(y.columns),\n        \"model_name\": \"resnet34_reg\",\n        \"epochs\": epochs,\n        \"img_size\": 256,\n        \"seed\": SEED,\n    },\n    f\"{CKPT_DIR}/resnet34_reg{epochs}.pt\"\n)","metadata":{"execution":{"iopub.status.busy":"2026-01-08T09:03:19.538461Z","iopub.execute_input":"2026-01-08T09:03:19.539013Z","iopub.status.idle":"2026-01-08T09:28:46.744889Z","shell.execute_reply.started":"2026-01-08T09:03:19.538992Z","shell.execute_reply":"2026-01-08T09:28:46.743713Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*Training the resnet50 model on images with a resolution of 384x384.*","metadata":{}},{"cell_type":"code","source":"%%time\n\ntrain_ds_384 = PastureDataset(X, y, transform=train_tf_384, root=DATA_ROOT)\n\ng = torch.Generator().manual_seed(SEED)\n\ntrain_loader = DataLoader(\n    train_ds_384,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n    prefetch_factor=2,\n    generator=g\n)\n\nmodel50 = build_resnet50_regressor(num_targets=y.shape[1], weights_path=LOCAL_W_50).to(device)\n\nbackbone_params = [p for n, p in model50.named_parameters() if not n.startswith(\"fc.\")]\nhead_params     = [p for n, p in model50.named_parameters() if n.startswith(\"fc.\")]\noptimizer = torch.optim.AdamW([\n    {\"params\": backbone_params, \"lr\": 1e-4, \"weight_decay\": 1e-4},\n    {\"params\": head_params,     \"lr\": 1e-3, \"weight_decay\": 1e-4},\n])\n\nepochs = 35\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\nloss_func = nn.SmoothL1Loss()\n\nfor epoch in range(1, epochs + 1):\n    model50.train()\n    tr_running, tr_batches = 0.0, 0\n\n    for xb, yb in train_loader:\n        xb = xb.to(device, non_blocking=True)\n        yb = yb.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n        preds = model50(xb)\n        loss = loss_func(preds, yb)\n        loss.backward()\n        optimizer.step()\n\n        tr_running += loss.item()\n        tr_batches += 1\n\n    scheduler.step()\n\n    print(f\"epoch {epoch:02d} | train_loss {tr_running/max(tr_batches,1):.4f}\")\n\n# save final checkpoint\nfinal_state = {k: v.detach().cpu().clone() for k, v in model50.state_dict().items()}\ntorch.save(\n    {\n        \"state_dict\": final_state,\n        \"target_cols\": list(y.columns),\n        \"model_name\": \"resnet50_reg\",\n        \"epochs\": epochs,\n        \"img_size\": 384,\n        \"seed\": SEED,\n    },\n    f\"{CKPT_DIR}/resnet50_reg{epochs}.pt\"\n)","metadata":{"execution":{"iopub.status.busy":"2026-01-08T09:03:19.538461Z","iopub.execute_input":"2026-01-08T09:03:19.539013Z","iopub.status.idle":"2026-01-08T09:28:46.744889Z","shell.execute_reply.started":"2026-01-08T09:03:19.538992Z","shell.execute_reply":"2026-01-08T09:28:46.743713Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*Training the resnet34 model on **whole** images with a resolution of 384x384.*","metadata":{}},{"cell_type":"code","source":"%%time\n\ntrain_ds_whole = PastureDataset(X, y, transform=train_tf_whole, root=DATA_ROOT)\n\ng = torch.Generator().manual_seed(SEED)\n\ntrain_loader = DataLoader(\n    train_ds_whole,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n    prefetch_factor=2,\n    generator=g\n)\n\nmodel_whole = build_resnet34_regressor(num_targets=y.shape[1], weights_path=LOCAL_W_34).to(device)\n\nbackbone_params = [p for n, p in model_whole.named_parameters() if not n.startswith(\"fc.\")]\nhead_params     = [p for n, p in model_whole.named_parameters() if n.startswith(\"fc.\")]\noptimizer = torch.optim.AdamW([\n    {\"params\": backbone_params, \"lr\": 1e-4, \"weight_decay\": 1e-4},\n    {\"params\": head_params,     \"lr\": 1e-3, \"weight_decay\": 1e-4},\n])\n\nepochs = 35\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\nloss_func = nn.SmoothL1Loss()\n\nfor epoch in range(1, epochs + 1):\n    model_whole.train()\n    tr_running, tr_batches = 0.0, 0\n\n    for xb, yb in train_loader:\n        xb = xb.to(device, non_blocking=True)\n        yb = yb.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n        preds = model_whole(xb)\n        loss = loss_func(preds, yb)\n        loss.backward()\n        optimizer.step()\n\n        tr_running += loss.item()\n        tr_batches += 1\n\n    scheduler.step()\n\n    print(f\"epoch {epoch:02d} | train_loss {tr_running/max(tr_batches,1):.4f}\")\n\n# save final checkpoint\nfinal_state = {k: v.detach().cpu().clone() for k, v in model_whole.state_dict().items()}\ntorch.save(\n    {\n        \"state_dict\": final_state,\n        \"target_cols\": list(y.columns),\n        \"model_name\": \"resnet_whole_reg\",\n        \"epochs\": epochs,\n        \"img_size\": 384,\n        \"seed\": SEED,\n    },\n    f\"{CKPT_DIR}/resnet_whole_reg{epochs}.pt\"\n)","metadata":{"execution":{"iopub.status.busy":"2026-01-08T09:03:19.538461Z","iopub.execute_input":"2026-01-08T09:03:19.539013Z","iopub.status.idle":"2026-01-08T09:28:46.744889Z","shell.execute_reply.started":"2026-01-08T09:03:19.538992Z","shell.execute_reply":"2026-01-08T09:28:46.743713Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"*This model shows an inflated local validation score relative to the leaderboard; local metrics are not reliable here. The model is used solely as an ensemble component.*","metadata":{}},{"cell_type":"markdown","source":"*Training the convnext_tiny model on  images with a resolution of 256x256.*","metadata":{}},{"cell_type":"code","source":"%%time\n\ntrain_ds_convnext = PastureDataset(X, y, transform=train_tf_256, root=DATA_ROOT)\n\ng = torch.Generator().manual_seed(SEED)\n\ntrain_loader = DataLoader(\n    train_ds_convnext,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n    prefetch_factor=2,\n    generator=g\n)\n\nmodel_conv = build_convnext_regressor(num_targets=y.shape[1], weights_path=LOCAL_W_convnext_tiny).to(device)\n\n# different lr: head trains faster, backbone more carefully\nhead_names = (\"head.\",)  # convnext uses 'head'\nbackbone_params = [p for n,p in model_conv.named_parameters() if not n.startswith(\"head.\")]\nhead_params     = [p for n,p in model_conv.named_parameters() if n.startswith(\"head.\")]\n\noptimizer = torch.optim.AdamW([\n    {'params': backbone_params, 'lr': 1e-4, 'weight_decay': 1e-4},\n    {'params': head_params,     'lr': 1e-3, 'weight_decay': 1e-4},\n])\n\nepochs = 35\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\nloss_func = nn.SmoothL1Loss()\n\nfor epoch in range(1, epochs + 1):\n    model_conv.train()\n    tr_running, tr_batches = 0.0, 0\n\n    for xb, yb in train_loader:\n        xb = xb.to(device, non_blocking=True)\n        yb = yb.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n        preds = model_conv(xb)\n        loss = loss_func(preds, yb)\n        loss.backward()\n        optimizer.step()\n\n        tr_running += loss.item()\n        tr_batches += 1\n\n    scheduler.step()\n\n    print(f\"epoch {epoch:02d} | train_loss {tr_running/max(tr_batches,1):.4f}\")\n\n# save final checkpoint\nfinal_state = {k: v.detach().cpu().clone() for k, v in model_conv.state_dict().items()}\ntorch.save(\n    {\n        \"state_dict\": final_state,\n        \"target_cols\": list(y.columns),\n        \"model_name\": \"convnext_reg\",\n        \"epochs\": epochs,\n        \"img_size\": 256,\n        \"seed\": SEED,\n    },\n    f\"{CKPT_DIR}/convnext_reg{epochs}.pt\"\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_on_loader(model, loader, device):\n    model.eval()\n    out = []\n    with torch.no_grad():\n        for xb, _ in loader:\n            xb = xb.to(device, non_blocking=True)\n            out.append(model(xb).detach().cpu())\n    return torch.cat(out, dim=0)\n\ntrain_loader_256_noshuf   = DataLoader(train_ds_256,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\ntrain_loader_384_noshuf   = DataLoader(train_ds_384,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\ntrain_loader_whole_noshuf = DataLoader(train_ds_whole, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\ntrain_loader_convnext_noshuf = DataLoader(train_ds_convnext, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\noof_preds_34    = predict_on_loader(model34,    train_loader_256_noshuf,   device)\noof_preds_50    = predict_on_loader(model50,    train_loader_384_noshuf,   device)\noof_preds_whole = predict_on_loader(model_whole, train_loader_whole_noshuf, device)\noof_preds_convnext = predict_on_loader(model_conv, train_loader_convnext_noshuf, device)\n\n# keep same variable names downstream\nassert oof_preds_34.shape == oof_preds_50.shape == oof_preds_whole.shape == oof_preds_convnext.shape == (len(y), 5)\n\ny_true_all = torch.tensor(y.values, dtype=torch.float32)\n\nblend_oof = 0.3 * oof_preds_34 + 0.3 * oof_preds_50 + 0.3 * oof_preds_convnext + 0.1 * oof_preds_whole\n\nWEIGHTS_CPU = torch.tensor([WEIGHTS_MAP[c] for c in y.columns], dtype=torch.float32)\nwr2_ens, r2_vec_ens = weighted_r2(y_true_all, blend_oof, WEIGHTS_CPU)\n\nprint(\"\\nTRAIN ensemble WR2 (diagnostic only):\", wr2_ens.item(),\n      \"| TRAIN R2 per target:\", r2_vec_ens.detach().cpu().numpy())","metadata":{"execution":{"iopub.status.busy":"2026-01-08T09:51:55.102674Z","iopub.execute_input":"2026-01-08T09:51:55.102874Z","iopub.status.idle":"2026-01-08T09:51:55.124219Z","shell.execute_reply.started":"2026-01-08T09:51:55.102859Z","shell.execute_reply":"2026-01-08T09:51:55.123648Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_cols = list(y.columns)\n\n# SAVE OOF FOR POSTPROCESS\nnp.save(\"oof_targets.npy\", y_true_all)\nnp.save(\"target_cols.npy\", np.array(target_cols))\n\nnp.save(\"oof_preds_34.npy\", oof_preds_34)\nnp.save(\"oof_preds_50.npy\", oof_preds_50)\nnp.save(\"oof_preds_whole.npy\", oof_preds_whole)\nnp.save(\"oof_preds_convnext.npy\", oof_preds_convnext)\n\nprint(\"OOF predictions and targets saved\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_postprocess_with_dead_stacking_fit(\n    preds: torch.Tensor,                 # (N, 5) blended preds\n    y_true: torch.Tensor,                # (N, 5) true targets (REQUIRED)\n    target_cols: list[str],\n    preds34: torch.Tensor,\n    preds50: torch.Tensor,\n    preds_whole: torch.Tensor,\n    preds_convnext: torch.Tensor,\n    clip_all_min=0.0,\n    floor_total=1.0,\n    floor_gdm=1.0,\n    ridge_alpha=1.0,\n):\n    \"\"\"\n    Postprocess:\n      1) clip all targets to >= clip_all_min\n      2) floor Total and GDM to >= 1.0 (optional, for stability)\n      3) replace Dry_Dead_g with stacked linear combo of per-model dead preds + intercept\n    \"\"\"\n    out = preds.clone()\n\n    idx_dead  = target_cols.index(\"Dry_Dead_g\")\n    idx_total = target_cols.index(\"Dry_Total_g\")\n    idx_gdm   = target_cols.index(\"GDM_g\")\n\n    # 1) clip negatives\n    out = torch.clamp(out, min=clip_all_min)\n    out[:, idx_total] = torch.clamp(out[:, idx_total], min=floor_total)\n    out[:, idx_gdm]   = torch.clamp(out[:, idx_gdm],   min=floor_gdm)\n\n    # 2) build ridge features\n    X = torch.stack([\n        preds34[:, idx_dead],\n        preds50[:, idx_dead],\n        preds_whole[:, idx_dead],\n        preds_convnext[:, idx_dead]\n    ], dim=1).cpu().numpy()\n\n    y_dead = y_true[:, idx_dead].cpu().numpy()\n\n    # 3) fit ridge\n    ridge = Ridge(alpha=ridge_alpha, random_state=SEED)\n    ridge.fit(X, y_dead)\n\n    # 4) predict stacked dead\n    dead_stacked = ridge.predict(X)\n    dead_stacked = torch.from_numpy(dead_stacked).float()\n    dead_stacked = torch.clamp(dead_stacked, min=0.0)\n\n    out[:, idx_dead] = dead_stacked\n    return out, ridge","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"blend_oof_pp, dead_ridge = apply_postprocess_with_dead_stacking_fit(\n    preds=blend_oof,\n    y_true=y_true_all,\n    target_cols=target_cols,\n    preds34=oof_preds_34,\n    preds50=oof_preds_50,\n    preds_whole=oof_preds_whole,\n    preds_convnext=oof_preds_convnext\n)\n\nwr2_pp, r2_vec_pp = weighted_r2(y_true_all, blend_oof_pp, WEIGHTS_CPU)\nprint(\"Postprocessed OOF WR2:\", wr2_pp.item(), \"| R2:\", r2_vec_pp.cpu().numpy())\n\nprint(\"Dead ridge coefs:\", dead_ridge.coef_, \"intercept:\", dead_ridge.intercept_)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Final prediction and submission","metadata":{}},{"cell_type":"code","source":"# FINAL PREDICTION (ENSEMBLE)\n# Ensemble: 0.3 * ResNet34@256 + 0.3 * ResNet50@384 + 0.3 * ConvNext@256 + 0.1 * ResNet34@384\n\n# creating a data loader for testing\nDATA_ROOT_TEST = Path(\"/kaggle/input/csiro-biomass/test\")\n\n# build three test loaders with the correct validation transforms\ntest_ds_256 = PastureDataset(df_image_test['image_path'], targets=None, transform=val_tf_256, root=DATA_ROOT_TEST)\ntest_ds_384 = PastureDataset(df_image_test['image_path'], targets=None, transform=val_tf_384, root=DATA_ROOT_TEST)\ntest_ds_whole = PastureDataset(df_image_test['image_path'], targets=None, transform=val_tf_whole, root=DATA_ROOT_TEST)\ntest_ds_convnext = PastureDataset(df_image_test['image_path'], targets=None, transform=val_tf_256, root=DATA_ROOT_TEST)\n\nTEST_BS = 32\n\ntest_loader_256 = DataLoader(\n    test_ds_256,\n    batch_size=TEST_BS,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n)\n\ntest_loader_384 = DataLoader(\n    test_ds_384,\n    batch_size=TEST_BS,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n)\n\ntest_loader_whole = DataLoader(\n    test_ds_whole,\n    batch_size=TEST_BS,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n)\n\ntest_loader_convnext = DataLoader(\n    test_ds_convnext,\n    batch_size=TEST_BS,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    persistent_workers=True,\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# target order is the same as during training\ntarget_cols = list(y.columns)    \n\ndef _load_state_dict(ckpt_path: str):\n    state = torch.load(ckpt_path, map_location=\"cpu\")\n    if isinstance(state, dict) and \"state_dict\" in state:\n        return state[\"state_dict\"]\n    return state\n\n@torch.no_grad()\ndef predict_from_checkpoints(ckpt_paths, build_fn, loader, device):\n    \"\"\"Return [n_test, n_targets] averaged over folds for a given model family.\"\"\"\n    assert len(ckpt_paths) > 0, \"No checkpoints found.\"\n    fold_preds = []\n\n    for ckpt in ckpt_paths:\n        model = build_fn(num_targets=len(target_cols)).to(device)\n        sd = _load_state_dict(ckpt)\n        model.load_state_dict(sd, strict=True)\n        model.eval()\n\n        preds_parts = []\n        for xb in loader:\n            # PastureDataset(test) may return imgs or (imgs, meta). Keep only imgs.\n            if isinstance(xb, (list, tuple)):\n                xb = xb[0]\n            xb = xb.to(device, non_blocking=True)\n            out = model(xb).detach().cpu()\n            preds_parts.append(out)\n\n        fold_preds.append(torch.cat(preds_parts, dim=0))\n\n    # [num_folds, n_test, n_targets] -> [n_test, n_targets]\n    return torch.stack(fold_preds, dim=0).mean(dim=0)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_from_single_ckpt(ckpt_path, build_fn, loader, device):\n    state = torch.load(ckpt_path, map_location=\"cpu\")\n    sd = state[\"state_dict\"] if isinstance(state, dict) and \"state_dict\" in state else state\n    model = build_fn(num_targets=len(target_cols)).to(device)\n    model.load_state_dict(sd, strict=True)\n    model.eval()\n\n    preds = []\n    with torch.no_grad():\n        for xb, _ in loader:\n            xb = xb.to(device, non_blocking=True)\n            preds.append(model(xb).detach().cpu())\n    return torch.cat(preds, dim=0)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ckpt34 = f\"{CKPT_DIR}/resnet34_reg{epochs}.pt\"\nckpt50 = f\"{CKPT_DIR}/resnet50_reg{epochs}.pt\"\nckpt_whole = f\"{CKPT_DIR}/resnet_whole_reg{epochs}.pt\"\nckpt_convnext = f\"{CKPT_DIR}/convnext_reg{epochs}.pt\"\n\npreds34 = predict_from_single_ckpt(ckpt34, build_resnet34_regressor, test_loader_256, device)\npreds50 = predict_from_single_ckpt(ckpt50, build_resnet50_regressor, test_loader_384, device)\npreds_whole = predict_from_single_ckpt(ckpt_whole, build_resnet34_regressor, test_loader_whole, device)\npreds_convnext = predict_from_single_ckpt(ckpt_convnext, build_convnext_regressor, test_loader_convnext, device)\n\npreds_ens = 0.3 * preds34 + 0.3 * preds50 + 0.3 * preds_convnext + 0.1 * preds_whole","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_postprocess_with_dead_stacking_apply(\n    preds: torch.Tensor,\n    target_cols: list[str],\n    preds34: torch.Tensor,\n    preds50: torch.Tensor,\n    preds_whole: torch.Tensor,\n    preds_convnext: torch.Tensor,\n    ridge,                            \n    clip_all_min=0.0,\n    floor_total=1.0,\n    floor_gdm=1.0,\n):\n    out = preds.clone()\n\n    idx_dead  = target_cols.index(\"Dry_Dead_g\")\n    idx_total = target_cols.index(\"Dry_Total_g\")\n    idx_gdm   = target_cols.index(\"GDM_g\")\n\n    out = torch.clamp(out, min=clip_all_min)\n    out[:, idx_total] = torch.clamp(out[:, idx_total], min=floor_total)\n    out[:, idx_gdm]   = torch.clamp(out[:, idx_gdm],   min=floor_gdm)\n\n    X = torch.stack([\n        preds34[:, idx_dead],\n        preds50[:, idx_dead],\n        preds_whole[:, idx_dead],\n        preds_convnext[:, idx_dead]\n    ], dim=1).cpu().numpy()\n\n    dead_stacked = ridge.predict(X)\n    dead_stacked = torch.from_numpy(dead_stacked).float()\n    dead_stacked = torch.clamp(dead_stacked, min=0.0)\n\n    out[:, idx_dead] = dead_stacked\n    return out","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds_ens_pp = apply_postprocess_with_dead_stacking_apply(\n    preds=preds_ens,\n    target_cols=target_cols,\n    preds34=preds34,\n    preds50=preds50,\n    preds_whole=preds_whole,\n    preds_convnext=preds_convnext,\n    ridge=dead_ridge,\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# we form a submission in the format sample_id,target\nrows = []\nimg_ids = [Path(p).stem for p in df_image_test['image_path']]\n\npreds_np = preds_ens.numpy()\n\nfor i, img_id in enumerate(img_ids):\n    for j, tname in enumerate(target_cols):\n        rows.append({\n            \"sample_id\": f\"{img_id}__{tname}\",\n            \"target\": float(preds_np[i, j]),\n        })\n\nsubmission = pd.DataFrame(rows, columns=[\"sample_id\", \"target\"])\nsubmission.to_csv(\"submission.csv\", index=False)\n\nsubmission.head()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}