Text Classification
Transformers
Safetensors
yield-weather-soil
crop-yield
multi-temporal
regression
yield-estimation
custom_code
Instructions to use ICICLE-AI/yield-estimation with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ICICLE-AI/yield-estimation with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="ICICLE-AI/yield-estimation", trust_remote_code=True)# Load model directly from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained("ICICLE-AI/yield-estimation", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload 21 files
Browse files- training_code/config/__init__.py +0 -0
- training_code/config/config.py +82 -0
- training_code/data/__init__.py +0 -0
- training_code/data/dataset.py +260 -0
- training_code/data/preprocessing.py +119 -0
- training_code/examples/sample_input_weekly.json +399 -0
- training_code/hf/__init__.py +0 -0
- training_code/hf/auto.py +15 -0
- training_code/hf/configuration_yield.py +58 -0
- training_code/hf/modeling_yield.py +91 -0
- training_code/models/__init__.py +0 -0
- training_code/models/unimodal_ws_crossattn.py +171 -0
- training_code/requirements.txt +153 -0
- training_code/scripts/__init__.py +0 -0
- training_code/scripts/evaluate_hf.py +92 -0
- training_code/scripts/inference_hf.py +197 -0
- training_code/scripts/prepare_cornbelt.py +710 -0
- training_code/scripts/train_hf.py +381 -0
- training_code/training.slurm +28 -0
- training_code/training/__init__.py +0 -0
- training_code/training/engine.py +122 -0
training_code/config/__init__.py
ADDED
|
File without changes
|
training_code/config/config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from dataclasses import dataclass, asdict
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class TrainConfig:
|
| 8 |
+
mode: str = "train_eval" # train_eval | train | eval | predict
|
| 9 |
+
|
| 10 |
+
train_file: Path | None = None
|
| 11 |
+
val_file: Path | None = None
|
| 12 |
+
test_file: Path | None = None
|
| 13 |
+
predict_file: Path | None = None
|
| 14 |
+
checkpoint_path: Path | None = None
|
| 15 |
+
output_csv: Path | None = None
|
| 16 |
+
single_sample_json: Path | None = None
|
| 17 |
+
|
| 18 |
+
weather_vars: list[str] | None = None
|
| 19 |
+
soil_vars: list[str] | None = None
|
| 20 |
+
|
| 21 |
+
crop_col: str = "crop"
|
| 22 |
+
yield_col: str = "yield"
|
| 23 |
+
field_col: str = "farm_field"
|
| 24 |
+
year_col: str = "year"
|
| 25 |
+
|
| 26 |
+
years: str | None = None
|
| 27 |
+
crop: str | None = None
|
| 28 |
+
time_agg: str = "weekly_cumulative"
|
| 29 |
+
|
| 30 |
+
train_cutoffs: list[int] | None = None
|
| 31 |
+
eval_cutoffs: list[int] | None = None
|
| 32 |
+
predict_cutoff: int | None = None
|
| 33 |
+
|
| 34 |
+
d_model: int = 64
|
| 35 |
+
nhead: int = 4
|
| 36 |
+
num_layers: int = 4
|
| 37 |
+
dim_ff: int = 128
|
| 38 |
+
dropout: float = 0.4
|
| 39 |
+
pool: str = "last"
|
| 40 |
+
use_crop: bool = True
|
| 41 |
+
crop_emb_dim: int = 8
|
| 42 |
+
|
| 43 |
+
epochs: int = 10
|
| 44 |
+
lr: float = 1e-4
|
| 45 |
+
batch_size: int = 64
|
| 46 |
+
weight_decay: float = 1e-3
|
| 47 |
+
seed: int = 1234
|
| 48 |
+
early_stop_patience: int = 3
|
| 49 |
+
|
| 50 |
+
split_strategy: str = "field" # field | random | none
|
| 51 |
+
val_split: float = 0.2
|
| 52 |
+
test_split: float = 0.2
|
| 53 |
+
|
| 54 |
+
expt_name: str = "test"
|
| 55 |
+
out_dir: Path = Path("outputs")
|
| 56 |
+
log_dir: Path = Path("runs")
|
| 57 |
+
|
| 58 |
+
def validate(self) -> None:
|
| 59 |
+
if self.mode in {"train", "train_eval"}:
|
| 60 |
+
if self.train_file is None:
|
| 61 |
+
raise ValueError("train_file is required for training.")
|
| 62 |
+
if not self.weather_vars:
|
| 63 |
+
raise ValueError("weather_vars is required for training.")
|
| 64 |
+
if not self.soil_vars:
|
| 65 |
+
raise ValueError("soil_vars is required for training.")
|
| 66 |
+
|
| 67 |
+
if self.mode in {"eval", "predict"} and self.checkpoint_path is None:
|
| 68 |
+
raise ValueError("checkpoint_path is required for eval/predict.")
|
| 69 |
+
|
| 70 |
+
if self.mode == "eval" and self.test_file is None:
|
| 71 |
+
raise ValueError("test_file is required for eval.")
|
| 72 |
+
|
| 73 |
+
if self.mode == "predict":
|
| 74 |
+
if self.predict_file is None and self.single_sample_json is None:
|
| 75 |
+
raise ValueError("predict_file or single_sample_json is required.")
|
| 76 |
+
if self.predict_cutoff is None:
|
| 77 |
+
raise ValueError("predict_cutoff is required for predict.")
|
| 78 |
+
|
| 79 |
+
def save(self) -> None:
|
| 80 |
+
self.out_dir.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
with open(self.out_dir / "config.json", "w") as f:
|
| 82 |
+
json.dump(asdict(self), f, indent=2, default=str)
|
training_code/data/__init__.py
ADDED
|
File without changes
|
training_code/data/dataset.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import torch
|
| 6 |
+
from torch.utils.data import Dataset
|
| 7 |
+
|
| 8 |
+
from data.preprocessing import (
|
| 9 |
+
load_table,
|
| 10 |
+
decode_bytes_in_object_cols,
|
| 11 |
+
find_daily_cols,
|
| 12 |
+
daily_to_cumulative_weekly,
|
| 13 |
+
DEFAULT_WEATHER_AGG_RULES,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
class YieldDataset(Dataset):
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
data_file: str | Path,
|
| 20 |
+
weather_vars: list[str],
|
| 21 |
+
soil_vars: list[str],
|
| 22 |
+
split: str = "all",
|
| 23 |
+
seed: int = 1234,
|
| 24 |
+
crop: str | None = None,
|
| 25 |
+
years: str | None = None,
|
| 26 |
+
crop_col: str = "crop",
|
| 27 |
+
yield_col: str = "yield",
|
| 28 |
+
field_col: str = "farm_field",
|
| 29 |
+
year_col: str = "year",
|
| 30 |
+
time_agg: str = "weekly_cumulative",
|
| 31 |
+
split_strategy: str = "field",
|
| 32 |
+
val_split: float = 0.2,
|
| 33 |
+
test_split: float = 0.2,
|
| 34 |
+
require_yield: bool = True,
|
| 35 |
+
):
|
| 36 |
+
self.weather_vars = weather_vars
|
| 37 |
+
self.soil_vars = soil_vars
|
| 38 |
+
self.crop_col = crop_col
|
| 39 |
+
self.yield_col = yield_col
|
| 40 |
+
self.field_col = field_col
|
| 41 |
+
self.year_col = year_col
|
| 42 |
+
self.time_agg = time_agg
|
| 43 |
+
self.crop_map = {"corn": 0, "maize": 0, "soybean": 1, "soy": 1}
|
| 44 |
+
|
| 45 |
+
self.w_mean = None
|
| 46 |
+
self.w_std = None
|
| 47 |
+
self.s_mean = None
|
| 48 |
+
self.s_std = None
|
| 49 |
+
|
| 50 |
+
df = load_table(data_file)
|
| 51 |
+
df = decode_bytes_in_object_cols(df)
|
| 52 |
+
df.columns = [str(c).strip() for c in df.columns]
|
| 53 |
+
|
| 54 |
+
if yield_col != "yield" and yield_col in df.columns:
|
| 55 |
+
df = df.rename(columns={yield_col: "yield"})
|
| 56 |
+
self.yield_col = "yield"
|
| 57 |
+
|
| 58 |
+
required = [crop_col, field_col, year_col] + soil_vars
|
| 59 |
+
if require_yield:
|
| 60 |
+
required.append(self.yield_col)
|
| 61 |
+
|
| 62 |
+
missing = [c for c in required if c not in df.columns]
|
| 63 |
+
if missing:
|
| 64 |
+
raise ValueError(f"Missing required columns: {missing[:20]}")
|
| 65 |
+
|
| 66 |
+
if require_yield:
|
| 67 |
+
df[self.yield_col] = pd.to_numeric(df[self.yield_col], errors="coerce")
|
| 68 |
+
df = df.dropna(subset=[self.yield_col]).reset_index(drop=True)
|
| 69 |
+
else:
|
| 70 |
+
if self.yield_col not in df.columns:
|
| 71 |
+
df[self.yield_col] = np.nan
|
| 72 |
+
|
| 73 |
+
df[crop_col] = df[crop_col].astype(str).str.strip().str.lower()
|
| 74 |
+
if crop:
|
| 75 |
+
df = df[df[crop_col] == crop.strip().lower()].reset_index(drop=True)
|
| 76 |
+
|
| 77 |
+
if years:
|
| 78 |
+
year_list = [int(y.strip()) for y in years.split(",") if y.strip()]
|
| 79 |
+
df = df[df[year_col].isin(year_list)].reset_index(drop=True)
|
| 80 |
+
|
| 81 |
+
self.weather_cols_by_var = {}
|
| 82 |
+
|
| 83 |
+
for v in weather_vars:
|
| 84 |
+
cols = find_daily_cols(df, v)
|
| 85 |
+
|
| 86 |
+
if not cols:
|
| 87 |
+
examples = [c for c in df.columns if str(c).startswith(v)][:10]
|
| 88 |
+
raise ValueError(
|
| 89 |
+
f"No indexed columns found for weather var '{v}'. "
|
| 90 |
+
f"Examples: {examples}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
self.weather_cols_by_var[v] = cols
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
if self.time_agg == "weekly":
|
| 97 |
+
# Input data is already weekly.
|
| 98 |
+
self.K = len(
|
| 99 |
+
self.weather_cols_by_var[weather_vars[0]]
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
elif self.time_agg == "weekly_cumulative":
|
| 103 |
+
# Existing behavior for daily input.
|
| 104 |
+
self.K = len(
|
| 105 |
+
daily_to_cumulative_weekly(
|
| 106 |
+
df.loc[
|
| 107 |
+
0,
|
| 108 |
+
self.weather_cols_by_var[weather_vars[0]]
|
| 109 |
+
].to_numpy(dtype=np.float32),
|
| 110 |
+
agg=DEFAULT_WEATHER_AGG_RULES.get(
|
| 111 |
+
weather_vars[0],
|
| 112 |
+
"mean",
|
| 113 |
+
),
|
| 114 |
+
)
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
else:
|
| 118 |
+
raise ValueError(
|
| 119 |
+
"time_agg must be 'weekly' or 'weekly_cumulative'"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
df = df.reset_index(drop=True)
|
| 123 |
+
self.df_full = df
|
| 124 |
+
self.indices = self._make_split_indices(df, split, seed, split_strategy, val_split, test_split)
|
| 125 |
+
|
| 126 |
+
self.df = df
|
| 127 |
+
self.soil_arr = df[soil_vars].to_numpy(np.float32)
|
| 128 |
+
self.crop_arr = df[crop_col].astype(str).str.strip().str.lower().to_numpy()
|
| 129 |
+
self.y_arr = df[self.yield_col].to_numpy(np.float32)
|
| 130 |
+
|
| 131 |
+
def _make_split_indices(self, df, split, seed, split_strategy, val_split, test_split):
|
| 132 |
+
rng = np.random.default_rng(seed)
|
| 133 |
+
|
| 134 |
+
if split == "all" or split_strategy == "none":
|
| 135 |
+
return np.arange(len(df))
|
| 136 |
+
|
| 137 |
+
if split_strategy == "field":
|
| 138 |
+
fields = df[self.field_col].astype(str).unique()
|
| 139 |
+
rng.shuffle(fields)
|
| 140 |
+
|
| 141 |
+
n = len(fields)
|
| 142 |
+
n_test = max(1, int(test_split * n)) if n >= 3 else 0
|
| 143 |
+
n_val = max(1, int(val_split * n)) if n >= 3 else 0
|
| 144 |
+
n_train = n - n_val - n_test
|
| 145 |
+
|
| 146 |
+
train_fields = set(fields[:n_train])
|
| 147 |
+
val_fields = set(fields[n_train:n_train + n_val])
|
| 148 |
+
test_fields = set(fields[n_train + n_val:])
|
| 149 |
+
|
| 150 |
+
if split == "train":
|
| 151 |
+
keep = train_fields
|
| 152 |
+
elif split == "val":
|
| 153 |
+
keep = val_fields
|
| 154 |
+
elif split == "test":
|
| 155 |
+
keep = test_fields
|
| 156 |
+
else:
|
| 157 |
+
raise ValueError("split must be train, val, test, or all")
|
| 158 |
+
|
| 159 |
+
return df.index[df[self.field_col].astype(str).isin(keep)].to_numpy()
|
| 160 |
+
|
| 161 |
+
if split_strategy == "random":
|
| 162 |
+
idx = np.arange(len(df))
|
| 163 |
+
rng.shuffle(idx)
|
| 164 |
+
n = len(idx)
|
| 165 |
+
n_test = int(test_split * n)
|
| 166 |
+
n_val = int(val_split * n)
|
| 167 |
+
n_train = n - n_val - n_test
|
| 168 |
+
|
| 169 |
+
if split == "train":
|
| 170 |
+
return idx[:n_train]
|
| 171 |
+
if split == "val":
|
| 172 |
+
return idx[n_train:n_train + n_val]
|
| 173 |
+
if split == "test":
|
| 174 |
+
return idx[n_train + n_val:]
|
| 175 |
+
|
| 176 |
+
raise ValueError(f"Unknown split_strategy={split_strategy}")
|
| 177 |
+
|
| 178 |
+
def set_normalization(self, w_mean, w_std, s_mean, s_std):
|
| 179 |
+
self.w_mean = np.asarray(w_mean, dtype=np.float32)
|
| 180 |
+
self.w_std = np.asarray(w_std, dtype=np.float32)
|
| 181 |
+
self.s_mean = np.asarray(s_mean, dtype=np.float32)
|
| 182 |
+
self.s_std = np.asarray(s_std, dtype=np.float32)
|
| 183 |
+
|
| 184 |
+
def __len__(self):
|
| 185 |
+
return len(self.indices)
|
| 186 |
+
|
| 187 |
+
def __getitem__(self, idx):
|
| 188 |
+
ridx = int(self.indices[idx])
|
| 189 |
+
row = self.df.loc[ridx]
|
| 190 |
+
|
| 191 |
+
weather_vars = []
|
| 192 |
+
|
| 193 |
+
for v in self.weather_vars:
|
| 194 |
+
|
| 195 |
+
values = row[
|
| 196 |
+
self.weather_cols_by_var[v]
|
| 197 |
+
].to_numpy(dtype=np.float32)
|
| 198 |
+
|
| 199 |
+
if self.time_agg == "weekly":
|
| 200 |
+
|
| 201 |
+
# Data is already weekly.
|
| 202 |
+
# Use the 52 weekly values exactly as supplied.
|
| 203 |
+
seq = values
|
| 204 |
+
|
| 205 |
+
elif self.time_agg == "weekly_cumulative":
|
| 206 |
+
|
| 207 |
+
# Existing behavior for datasets containing daily weather.
|
| 208 |
+
agg = DEFAULT_WEATHER_AGG_RULES.get(v, "mean")
|
| 209 |
+
|
| 210 |
+
seq = daily_to_cumulative_weekly(
|
| 211 |
+
values,
|
| 212 |
+
agg=agg,
|
| 213 |
+
week_len=7,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
weather_vars.append(seq)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
if self.time_agg == "weekly":
|
| 220 |
+
|
| 221 |
+
# [K, number_of_weather_variables]
|
| 222 |
+
weather = np.stack(
|
| 223 |
+
weather_vars,
|
| 224 |
+
axis=1,
|
| 225 |
+
).astype(np.float32)
|
| 226 |
+
|
| 227 |
+
else:
|
| 228 |
+
|
| 229 |
+
# Existing daily -> weekly+cumulative representation
|
| 230 |
+
weather = np.stack(
|
| 231 |
+
weather_vars,
|
| 232 |
+
axis=1,
|
| 233 |
+
).astype(np.float32)
|
| 234 |
+
|
| 235 |
+
weather = weather.reshape(
|
| 236 |
+
weather.shape[0],
|
| 237 |
+
-1,
|
| 238 |
+
).astype(np.float32)
|
| 239 |
+
|
| 240 |
+
soil = self.soil_arr[ridx].astype(np.float32)
|
| 241 |
+
|
| 242 |
+
if self.w_mean is not None:
|
| 243 |
+
weather = np.where(np.isnan(weather), self.w_mean[None, :], weather)
|
| 244 |
+
weather = (weather - self.w_mean[None, :]) / self.w_std[None, :]
|
| 245 |
+
|
| 246 |
+
if self.s_mean is not None:
|
| 247 |
+
soil = np.where(np.isnan(soil), self.s_mean, soil)
|
| 248 |
+
soil = (soil - self.s_mean) / self.s_std
|
| 249 |
+
|
| 250 |
+
crop = str(self.crop_arr[ridx]).strip().lower()
|
| 251 |
+
crop_id = self.crop_map.get(crop, 0)
|
| 252 |
+
|
| 253 |
+
return {
|
| 254 |
+
"weather": torch.from_numpy(weather),
|
| 255 |
+
"soil": torch.from_numpy(soil),
|
| 256 |
+
"crop_id": torch.tensor(crop_id, dtype=torch.long),
|
| 257 |
+
"yield": torch.tensor(float(self.y_arr[ridx]), dtype=torch.float32),
|
| 258 |
+
"farm_field": str(row[self.field_col]),
|
| 259 |
+
"year": int(row[self.year_col]),
|
| 260 |
+
}
|
training_code/data/preprocessing.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import math
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
DEFAULT_WEATHER_AGG_RULES = {
|
| 7 |
+
"dayl": "mean",
|
| 8 |
+
"prcp": "sum",
|
| 9 |
+
"srad": "mean",
|
| 10 |
+
"tmax": "mean",
|
| 11 |
+
"tmin": "mean",
|
| 12 |
+
"vp": "mean",
|
| 13 |
+
"tmean": "mean",
|
| 14 |
+
"gdd": "sum",
|
| 15 |
+
"precip_3day_avg_perday": "mean",
|
| 16 |
+
"precip_7day_avg_perday": "mean",
|
| 17 |
+
"precip_14day_avg_perday": "mean",
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
def load_table(path):
|
| 21 |
+
path = str(path)
|
| 22 |
+
if path.endswith(".h5") or path.endswith(".hdf5"):
|
| 23 |
+
return pd.read_hdf(path)
|
| 24 |
+
if path.endswith(".csv"):
|
| 25 |
+
return pd.read_csv(path)
|
| 26 |
+
if path.endswith(".parquet"):
|
| 27 |
+
return pd.read_parquet(path)
|
| 28 |
+
raise ValueError(f"Unsupported file type: {path}")
|
| 29 |
+
|
| 30 |
+
def decode_bytes_in_object_cols(df: pd.DataFrame) -> pd.DataFrame:
|
| 31 |
+
out = df.copy()
|
| 32 |
+
for c in out.select_dtypes(include=["object"]).columns:
|
| 33 |
+
if len(out) > 0 and isinstance(out[c].iloc[0], (bytes, bytearray)):
|
| 34 |
+
out[c] = out[c].str.decode("utf-8")
|
| 35 |
+
return out
|
| 36 |
+
|
| 37 |
+
def find_daily_cols(df: pd.DataFrame, var: str) -> list[str]:
|
| 38 |
+
pat_bracket = re.compile(rf"^{re.escape(var)}.*?_\[(\d+)\]$")
|
| 39 |
+
pat_plain = re.compile(rf"^{re.escape(var)}.*?_(\d+)$")
|
| 40 |
+
hits = []
|
| 41 |
+
for c in df.columns:
|
| 42 |
+
s = str(c)
|
| 43 |
+
m = pat_bracket.match(s) or pat_plain.match(s)
|
| 44 |
+
if m:
|
| 45 |
+
hits.append((int(m.group(1)), c))
|
| 46 |
+
hits.sort(key=lambda x: x[0])
|
| 47 |
+
return [c for _, c in hits]
|
| 48 |
+
|
| 49 |
+
# def daily_to_cumulative_weekly(daily: np.ndarray, agg: str, week_len: int = 7) -> np.ndarray:
|
| 50 |
+
# daily = daily.astype(np.float32)
|
| 51 |
+
# T = daily.shape[0]
|
| 52 |
+
# K = int(math.ceil(T / week_len))
|
| 53 |
+
# out = np.zeros(K, dtype=np.float32)
|
| 54 |
+
|
| 55 |
+
# for w in range(K):
|
| 56 |
+
# e = min((w + 1) * week_len, T)
|
| 57 |
+
# chunk = daily[:e]
|
| 58 |
+
# out[w] = np.nansum(chunk) if agg == "sum" else np.nanmean(chunk)
|
| 59 |
+
|
| 60 |
+
# return out
|
| 61 |
+
|
| 62 |
+
def daily_to_cumulative_weekly(
|
| 63 |
+
daily: np.ndarray,
|
| 64 |
+
agg: str,
|
| 65 |
+
week_len: int = 7,
|
| 66 |
+
) -> np.ndarray:
|
| 67 |
+
daily = daily.astype(np.float32)
|
| 68 |
+
T = daily.shape[0]
|
| 69 |
+
K = int(math.ceil(T / week_len))
|
| 70 |
+
|
| 71 |
+
weekly = np.zeros(K, dtype=np.float32)
|
| 72 |
+
cumulative = np.zeros(K, dtype=np.float32)
|
| 73 |
+
|
| 74 |
+
for w in range(K):
|
| 75 |
+
s = w * week_len
|
| 76 |
+
e = min((w + 1) * week_len, T)
|
| 77 |
+
|
| 78 |
+
week_chunk = daily[s:e]
|
| 79 |
+
cumulative_chunk = daily[:e]
|
| 80 |
+
|
| 81 |
+
if agg == "sum":
|
| 82 |
+
weekly[w] = np.nansum(week_chunk)
|
| 83 |
+
cumulative[w] = np.nansum(cumulative_chunk)
|
| 84 |
+
else:
|
| 85 |
+
weekly[w] = np.nanmean(week_chunk)
|
| 86 |
+
cumulative[w] = np.nanmean(cumulative_chunk)
|
| 87 |
+
|
| 88 |
+
return np.stack([weekly, cumulative], axis=1).astype(np.float32)
|
| 89 |
+
|
| 90 |
+
def compute_x_stats(dataset, max_samples=20000, seed=0):
|
| 91 |
+
rng = np.random.default_rng(seed)
|
| 92 |
+
n = min(len(dataset), max_samples)
|
| 93 |
+
idxs = rng.choice(len(dataset), size=n, replace=False) if n < len(dataset) else np.arange(len(dataset))
|
| 94 |
+
|
| 95 |
+
weather, soil = [], []
|
| 96 |
+
for i in idxs:
|
| 97 |
+
item = dataset[int(i)]
|
| 98 |
+
weather.append(item["weather"].numpy())
|
| 99 |
+
soil.append(item["soil"].numpy())
|
| 100 |
+
|
| 101 |
+
W_all = np.stack(weather).astype(np.float32)
|
| 102 |
+
S_all = np.stack(soil).astype(np.float32)
|
| 103 |
+
|
| 104 |
+
w_mean = np.nanmean(W_all, axis=(0, 1))
|
| 105 |
+
w_std = np.nanstd(W_all, axis=(0, 1)) + 1e-6
|
| 106 |
+
s_mean = np.nanmean(S_all, axis=0)
|
| 107 |
+
s_std = np.nanstd(S_all, axis=0) + 1e-6
|
| 108 |
+
|
| 109 |
+
w_mean = np.where(np.isfinite(w_mean), w_mean, 0.0)
|
| 110 |
+
w_std = np.where((np.isfinite(w_std)) & (w_std > 1e-6), w_std, 1.0)
|
| 111 |
+
s_mean = np.where(np.isfinite(s_mean), s_mean, 0.0)
|
| 112 |
+
s_std = np.where((np.isfinite(s_std)) & (s_std > 1e-6), s_std, 1.0)
|
| 113 |
+
|
| 114 |
+
return w_mean, w_std, s_mean, s_std
|
| 115 |
+
|
| 116 |
+
def compute_y_stats(dataset):
|
| 117 |
+
ys = np.array([float(dataset[i]["yield"]) for i in range(len(dataset))], dtype=np.float32)
|
| 118 |
+
#ys_log = np.log1p(np.clip(ys, 0.0, None))
|
| 119 |
+
return float(ys.mean()), float(ys.std() + 1e-6)
|
training_code/examples/sample_input_weekly.json
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"crop": "corn",
|
| 3 |
+
"weather_format": "weekly",
|
| 4 |
+
"cutoff": 52,
|
| 5 |
+
"weather": {
|
| 6 |
+
"prcp": [
|
| 7 |
+
0.0,
|
| 8 |
+
0.701298713684082,
|
| 9 |
+
0.4545454680919647,
|
| 10 |
+
2.155844211578369,
|
| 11 |
+
0.9350649118423462,
|
| 12 |
+
0.11688311398029327,
|
| 13 |
+
0.0,
|
| 14 |
+
3.753246784210205,
|
| 15 |
+
0.15584415197372437,
|
| 16 |
+
2.2337663173675537,
|
| 17 |
+
0.0,
|
| 18 |
+
0.4285714328289032,
|
| 19 |
+
2.402597427368164,
|
| 20 |
+
2.3896102905273438,
|
| 21 |
+
5.753246784210205,
|
| 22 |
+
0.9220778942108154,
|
| 23 |
+
0.0,
|
| 24 |
+
1.1298701763153076,
|
| 25 |
+
1.2077921628952026,
|
| 26 |
+
1.2207791805267334,
|
| 27 |
+
2.441558361053467,
|
| 28 |
+
5.402597427368164,
|
| 29 |
+
2.909090995788574,
|
| 30 |
+
3.6883115768432617,
|
| 31 |
+
5.1688313484191895,
|
| 32 |
+
5.233766078948975,
|
| 33 |
+
7.389610290527344,
|
| 34 |
+
4.883116722106934,
|
| 35 |
+
1.5194804668426514,
|
| 36 |
+
0.8311688303947449,
|
| 37 |
+
4.051948070526123,
|
| 38 |
+
0.03896103799343109,
|
| 39 |
+
0.0,
|
| 40 |
+
5.350649356842041,
|
| 41 |
+
2.0389609336853027,
|
| 42 |
+
4.103896141052246,
|
| 43 |
+
0.012987012974917889,
|
| 44 |
+
15.49350643157959,
|
| 45 |
+
1.1168831586837769,
|
| 46 |
+
3.142857074737549,
|
| 47 |
+
6.155844211578369,
|
| 48 |
+
0.3246753215789795,
|
| 49 |
+
1.6753246784210205,
|
| 50 |
+
0.5714285969734192,
|
| 51 |
+
2.8831169605255127,
|
| 52 |
+
0.10389610379934311,
|
| 53 |
+
0.9610389471054077,
|
| 54 |
+
1.1168831586837769,
|
| 55 |
+
0.0,
|
| 56 |
+
0.0,
|
| 57 |
+
0.051948051899671555,
|
| 58 |
+
4.181818008422852
|
| 59 |
+
],
|
| 60 |
+
"srad": [
|
| 61 |
+
187.0129852294922,
|
| 62 |
+
181.23635864257812,
|
| 63 |
+
184.8935089111328,
|
| 64 |
+
179.7818145751953,
|
| 65 |
+
255.91688537597656,
|
| 66 |
+
267.4701232910156,
|
| 67 |
+
323.158447265625,
|
| 68 |
+
282.09869384765625,
|
| 69 |
+
347.76104736328125,
|
| 70 |
+
274.28570556640625,
|
| 71 |
+
384.70648193359375,
|
| 72 |
+
332.7168884277344,
|
| 73 |
+
384.0831298828125,
|
| 74 |
+
455.7298583984375,
|
| 75 |
+
359.5636291503906,
|
| 76 |
+
525.6727294921875,
|
| 77 |
+
584.2701416015625,
|
| 78 |
+
522.7221069335938,
|
| 79 |
+
385.9532470703125,
|
| 80 |
+
420.5298767089844,
|
| 81 |
+
386.36883544921875,
|
| 82 |
+
335.12725830078125,
|
| 83 |
+
327.6467590332031,
|
| 84 |
+
329.35064697265625,
|
| 85 |
+
319.7090759277344,
|
| 86 |
+
341.6103820800781,
|
| 87 |
+
374.150634765625,
|
| 88 |
+
387.20001220703125,
|
| 89 |
+
379.0545349121094,
|
| 90 |
+
412.1766357421875,
|
| 91 |
+
353.08050537109375,
|
| 92 |
+
397.0493469238281,
|
| 93 |
+
376.4363708496094,
|
| 94 |
+
312.2701416015625,
|
| 95 |
+
312.4363708496094,
|
| 96 |
+
313.7662353515625,
|
| 97 |
+
341.07012939453125,
|
| 98 |
+
249.10130310058594,
|
| 99 |
+
292.32208251953125,
|
| 100 |
+
190.7532501220703,
|
| 101 |
+
189.13246154785156,
|
| 102 |
+
301.589599609375,
|
| 103 |
+
214.69090270996094,
|
| 104 |
+
198.7324676513672,
|
| 105 |
+
148.0311737060547,
|
| 106 |
+
213.77662658691406,
|
| 107 |
+
168.64414978027344,
|
| 108 |
+
116.23896026611328,
|
| 109 |
+
148.4051971435547,
|
| 110 |
+
169.22596740722656,
|
| 111 |
+
130.86753845214844,
|
| 112 |
+
157.7818145751953
|
| 113 |
+
],
|
| 114 |
+
"swe": [
|
| 115 |
+
18.909090042114258,
|
| 116 |
+
18.753246307373047,
|
| 117 |
+
23.220779418945312,
|
| 118 |
+
32.93506622314453,
|
| 119 |
+
37.24675369262695,
|
| 120 |
+
41.45454406738281,
|
| 121 |
+
41.45454406738281,
|
| 122 |
+
51.22077941894531,
|
| 123 |
+
67.84415435791016,
|
| 124 |
+
62.181819915771484,
|
| 125 |
+
61.610389709472656,
|
| 126 |
+
59.844154357910156,
|
| 127 |
+
54.80519485473633,
|
| 128 |
+
71.94805145263672,
|
| 129 |
+
78.9610366821289,
|
| 130 |
+
100.98701477050781,
|
| 131 |
+
84.467529296875,
|
| 132 |
+
50.54545593261719,
|
| 133 |
+
7.636363506317139,
|
| 134 |
+
0.0,
|
| 135 |
+
0.0,
|
| 136 |
+
0.0,
|
| 137 |
+
0.0,
|
| 138 |
+
0.0,
|
| 139 |
+
0.0,
|
| 140 |
+
0.0,
|
| 141 |
+
0.0,
|
| 142 |
+
0.0,
|
| 143 |
+
0.0,
|
| 144 |
+
0.0,
|
| 145 |
+
0.0,
|
| 146 |
+
0.0,
|
| 147 |
+
0.0,
|
| 148 |
+
0.0,
|
| 149 |
+
0.0,
|
| 150 |
+
0.0,
|
| 151 |
+
0.0,
|
| 152 |
+
0.0,
|
| 153 |
+
0.0,
|
| 154 |
+
0.0,
|
| 155 |
+
0.0,
|
| 156 |
+
0.0,
|
| 157 |
+
0.0,
|
| 158 |
+
0.0,
|
| 159 |
+
0.0,
|
| 160 |
+
0.0,
|
| 161 |
+
0.0,
|
| 162 |
+
1.402597427368164,
|
| 163 |
+
0.0,
|
| 164 |
+
0.0,
|
| 165 |
+
0.0,
|
| 166 |
+
9.454545021057129
|
| 167 |
+
],
|
| 168 |
+
"tmax": [
|
| 169 |
+
-13.350648880004883,
|
| 170 |
+
-3.694805145263672,
|
| 171 |
+
-2.4740259647369385,
|
| 172 |
+
-0.1428571492433548,
|
| 173 |
+
-7.298701286315918,
|
| 174 |
+
-10.746753692626953,
|
| 175 |
+
-1.3701298236846924,
|
| 176 |
+
-2.844155788421631,
|
| 177 |
+
5.285714149475098,
|
| 178 |
+
0.7272727489471436,
|
| 179 |
+
4.675324440002441,
|
| 180 |
+
3.948051929473877,
|
| 181 |
+
4.324675559997559,
|
| 182 |
+
-0.4285714328289032,
|
| 183 |
+
3.9285714626312256,
|
| 184 |
+
8.292207717895508,
|
| 185 |
+
16.785715103149414,
|
| 186 |
+
23.915584564208984,
|
| 187 |
+
19.662338256835938,
|
| 188 |
+
25.34415626525879,
|
| 189 |
+
28.39610481262207,
|
| 190 |
+
28.39610481262207,
|
| 191 |
+
24.863636016845703,
|
| 192 |
+
27.415584564208984,
|
| 193 |
+
25.766233444213867,
|
| 194 |
+
28.10389518737793,
|
| 195 |
+
28.441558837890625,
|
| 196 |
+
30.61688232421875,
|
| 197 |
+
26.87013053894043,
|
| 198 |
+
26.402597427368164,
|
| 199 |
+
26.571428298950195,
|
| 200 |
+
29.324674606323242,
|
| 201 |
+
29.714284896850586,
|
| 202 |
+
25.02597427368164,
|
| 203 |
+
25.441558837890625,
|
| 204 |
+
23.467533111572266,
|
| 205 |
+
28.746753692626953,
|
| 206 |
+
20.162338256835938,
|
| 207 |
+
15.733766555786133,
|
| 208 |
+
12.071428298950195,
|
| 209 |
+
8.357142448425293,
|
| 210 |
+
12.409090995788574,
|
| 211 |
+
11.629870414733887,
|
| 212 |
+
9.253246307373047,
|
| 213 |
+
-0.012987012974917889,
|
| 214 |
+
-0.6623376607894897,
|
| 215 |
+
0.9155844449996948,
|
| 216 |
+
-2.0,
|
| 217 |
+
-3.344155788421631,
|
| 218 |
+
0.5064935088157654,
|
| 219 |
+
1.7402597665786743,
|
| 220 |
+
-2.0568182468414307
|
| 221 |
+
],
|
| 222 |
+
"tmin": [
|
| 223 |
+
-23.694805145263672,
|
| 224 |
+
-14.48051929473877,
|
| 225 |
+
-12.051947593688965,
|
| 226 |
+
-7.941558361053467,
|
| 227 |
+
-19.376623153686523,
|
| 228 |
+
-21.123376846313477,
|
| 229 |
+
-14.344156265258789,
|
| 230 |
+
-14.02597427368164,
|
| 231 |
+
-7.7207794189453125,
|
| 232 |
+
-7.7272725105285645,
|
| 233 |
+
-6.344155788421631,
|
| 234 |
+
-4.116883277893066,
|
| 235 |
+
-5.246753215789795,
|
| 236 |
+
-11.681818008422852,
|
| 237 |
+
-3.909090995788574,
|
| 238 |
+
-3.649350643157959,
|
| 239 |
+
1.4220778942108154,
|
| 240 |
+
8.98051929473877,
|
| 241 |
+
8.870129585266113,
|
| 242 |
+
10.454545021057129,
|
| 243 |
+
15.129870414733887,
|
| 244 |
+
16.363636016845703,
|
| 245 |
+
14.461038589477539,
|
| 246 |
+
17.175325393676758,
|
| 247 |
+
16.701297760009766,
|
| 248 |
+
18.720779418945312,
|
| 249 |
+
17.922077178955078,
|
| 250 |
+
19.519479751586914,
|
| 251 |
+
16.305194854736328,
|
| 252 |
+
14.642857551574707,
|
| 253 |
+
15.694805145263672,
|
| 254 |
+
17.422077178955078,
|
| 255 |
+
18.220779418945312,
|
| 256 |
+
15.175324440002441,
|
| 257 |
+
15.558441162109375,
|
| 258 |
+
13.49350643157959,
|
| 259 |
+
17.201297760009766,
|
| 260 |
+
11.655843734741211,
|
| 261 |
+
5.7727274894714355,
|
| 262 |
+
4.551948070526123,
|
| 263 |
+
2.2272727489471436,
|
| 264 |
+
-0.16233766078948975,
|
| 265 |
+
1.7727272510528564,
|
| 266 |
+
1.0324674844741821,
|
| 267 |
+
-5.954545497894287,
|
| 268 |
+
-10.149351119995117,
|
| 269 |
+
-6.623376846313477,
|
| 270 |
+
-7.201298713684082,
|
| 271 |
+
-10.422078132629395,
|
| 272 |
+
-8.344156265258789,
|
| 273 |
+
-4.707792282104492,
|
| 274 |
+
-9.943181991577148
|
| 275 |
+
],
|
| 276 |
+
"vp": [
|
| 277 |
+
100.2597427368164,
|
| 278 |
+
259.22076416015625,
|
| 279 |
+
287.2727355957031,
|
| 280 |
+
344.41558837890625,
|
| 281 |
+
137.14285278320312,
|
| 282 |
+
115.84415435791016,
|
| 283 |
+
211.42857360839844,
|
| 284 |
+
215.06494140625,
|
| 285 |
+
359.48052978515625,
|
| 286 |
+
358.4415588378906,
|
| 287 |
+
377.6623229980469,
|
| 288 |
+
454.5454406738281,
|
| 289 |
+
409.8701171875,
|
| 290 |
+
249.87013244628906,
|
| 291 |
+
451.4285583496094,
|
| 292 |
+
438.4415588378906,
|
| 293 |
+
540.7792358398438,
|
| 294 |
+
887.792236328125,
|
| 295 |
+
1041.0389404296875,
|
| 296 |
+
1108.052001953125,
|
| 297 |
+
1534.54541015625,
|
| 298 |
+
1750.6492919921875,
|
| 299 |
+
1589.6103515625,
|
| 300 |
+
1931.4285888671875,
|
| 301 |
+
1902.337646484375,
|
| 302 |
+
2178.181884765625,
|
| 303 |
+
2062.857177734375,
|
| 304 |
+
2275.32470703125,
|
| 305 |
+
1851.947998046875,
|
| 306 |
+
1670.1298828125,
|
| 307 |
+
1802.077880859375,
|
| 308 |
+
1990.1298828125,
|
| 309 |
+
2097.662353515625,
|
| 310 |
+
1738.1817626953125,
|
| 311 |
+
1810.9090576171875,
|
| 312 |
+
1587.012939453125,
|
| 313 |
+
2006.7532958984375,
|
| 314 |
+
1414.0260009765625,
|
| 315 |
+
942.337646484375,
|
| 316 |
+
852.467529296875,
|
| 317 |
+
731.9480590820312,
|
| 318 |
+
614.5454711914062,
|
| 319 |
+
715.8441772460938,
|
| 320 |
+
658.7012939453125,
|
| 321 |
+
416.1038818359375,
|
| 322 |
+
297.1428527832031,
|
| 323 |
+
392.7272644042969,
|
| 324 |
+
367.7922058105469,
|
| 325 |
+
289.35064697265625,
|
| 326 |
+
336.6233825683594,
|
| 327 |
+
435.3246765136719,
|
| 328 |
+
295.4545593261719
|
| 329 |
+
]
|
| 330 |
+
},
|
| 331 |
+
"soil": {
|
| 332 |
+
"bdod_mean_0-5cm": 127.71450805664062,
|
| 333 |
+
"bdod_mean_5-15cm": 148.86965942382812,
|
| 334 |
+
"bdod_mean_15-30cm": 150.70364379882812,
|
| 335 |
+
"bdod_mean_30-60cm": 158.2148895263672,
|
| 336 |
+
"bdod_mean_60-100cm": 162.8789825439453,
|
| 337 |
+
"bdod_mean_100-200cm": 165.12025451660156,
|
| 338 |
+
"cec_mean_0-5cm": 252.34054565429688,
|
| 339 |
+
"cec_mean_5-15cm": 174.4131317138672,
|
| 340 |
+
"cec_mean_15-30cm": 142.52432250976562,
|
| 341 |
+
"cec_mean_30-60cm": 129.1745147705078,
|
| 342 |
+
"cec_mean_60-100cm": 119.97528839111328,
|
| 343 |
+
"cec_mean_100-200cm": 103.29729461669922,
|
| 344 |
+
"cfvo_mean_0-5cm": 21.431659698486328,
|
| 345 |
+
"cfvo_mean_5-15cm": 22.905019760131836,
|
| 346 |
+
"cfvo_mean_15-30cm": 21.820077896118164,
|
| 347 |
+
"cfvo_mean_30-60cm": 24.725868225097656,
|
| 348 |
+
"cfvo_mean_60-100cm": 34.54826354980469,
|
| 349 |
+
"cfvo_mean_100-200cm": 45.90888214111328,
|
| 350 |
+
"clay_mean_0-5cm": 150.02239990234375,
|
| 351 |
+
"clay_mean_5-15cm": 149.1436309814453,
|
| 352 |
+
"clay_mean_15-30cm": 150.81776428222656,
|
| 353 |
+
"clay_mean_30-60cm": 150.43707275390625,
|
| 354 |
+
"clay_mean_60-100cm": 134.29266357421875,
|
| 355 |
+
"clay_mean_100-200cm": 125.47721862792969,
|
| 356 |
+
"nitrogen_mean_0-5cm": 4723.78857421875,
|
| 357 |
+
"nitrogen_mean_5-15cm": 2148.763671875,
|
| 358 |
+
"nitrogen_mean_15-30cm": 1729.8802490234375,
|
| 359 |
+
"nitrogen_mean_30-60cm": 776.644775390625,
|
| 360 |
+
"nitrogen_mean_60-100cm": 547.14208984375,
|
| 361 |
+
"nitrogen_mean_100-200cm": 534.0015258789062,
|
| 362 |
+
"ocd_mean_0-5cm": 435.4494323730469,
|
| 363 |
+
"ocd_mean_5-15cm": 255.3752899169922,
|
| 364 |
+
"ocd_mean_15-30cm": 181.84786987304688,
|
| 365 |
+
"ocd_mean_30-60cm": 93.57837677001953,
|
| 366 |
+
"ocd_mean_60-100cm": 60.80463409423828,
|
| 367 |
+
"ocd_mean_100-200cm": 44.90888214111328,
|
| 368 |
+
"ocs_mean_0-5cm": 57.79845428466797,
|
| 369 |
+
"ocs_mean_5-15cm": 57.79845428466797,
|
| 370 |
+
"ocs_mean_15-30cm": 57.79845428466797,
|
| 371 |
+
"ocs_mean_30-60cm": 57.79845428466797,
|
| 372 |
+
"ocs_mean_60-100cm": 57.79845428466797,
|
| 373 |
+
"ocs_mean_100-200cm": 57.79845428466797,
|
| 374 |
+
"phh2o_mean_0-5cm": 61.812355041503906,
|
| 375 |
+
"phh2o_mean_5-15cm": 61.73050308227539,
|
| 376 |
+
"phh2o_mean_15-30cm": 61.63166046142578,
|
| 377 |
+
"phh2o_mean_30-60cm": 62.09111785888672,
|
| 378 |
+
"phh2o_mean_60-100cm": 64.0540542602539,
|
| 379 |
+
"phh2o_mean_100-200cm": 69.0687255859375,
|
| 380 |
+
"sand_mean_0-5cm": 625.2216186523438,
|
| 381 |
+
"sand_mean_5-15cm": 626.4702758789062,
|
| 382 |
+
"sand_mean_15-30cm": 631.22314453125,
|
| 383 |
+
"sand_mean_30-60cm": 649.113525390625,
|
| 384 |
+
"sand_mean_60-100cm": 697.4996337890625,
|
| 385 |
+
"sand_mean_100-200cm": 712.781494140625,
|
| 386 |
+
"silt_mean_0-5cm": 224.77297973632812,
|
| 387 |
+
"silt_mean_5-15cm": 224.3876495361328,
|
| 388 |
+
"silt_mean_15-30cm": 217.95135498046875,
|
| 389 |
+
"silt_mean_30-60cm": 200.43319702148438,
|
| 390 |
+
"silt_mean_60-100cm": 168.21775817871094,
|
| 391 |
+
"silt_mean_100-200cm": 161.7498016357422,
|
| 392 |
+
"soc_mean_0-5cm": 446.14508056640625,
|
| 393 |
+
"soc_mean_5-15cm": 223.34988403320312,
|
| 394 |
+
"soc_mean_15-30cm": 144.84561157226562,
|
| 395 |
+
"soc_mean_30-60cm": 107.50271606445312,
|
| 396 |
+
"soc_mean_60-100cm": 53.54926300048828,
|
| 397 |
+
"soc_mean_100-200cm": 47.3972053527832
|
| 398 |
+
}
|
| 399 |
+
}
|
training_code/hf/__init__.py
ADDED
|
File without changes
|
training_code/hf/auto.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoConfig, AutoModel
|
| 2 |
+
from hf.configuration_yield import YieldConfig
|
| 3 |
+
from hf.modeling_yield import YieldForRegression
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def register_yield_autoclass():
|
| 7 |
+
try:
|
| 8 |
+
AutoConfig.register("yield-weather-soil", YieldConfig)
|
| 9 |
+
except ValueError:
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
AutoModel.register(YieldConfig, YieldForRegression)
|
| 14 |
+
except ValueError:
|
| 15 |
+
pass
|
training_code/hf/configuration_yield.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PretrainedConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class YieldConfig(PretrainedConfig):
|
| 5 |
+
model_type = "yield-weather-soil"
|
| 6 |
+
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
weather_vars=None,
|
| 10 |
+
soil_vars=None,
|
| 11 |
+
w_mean=None,
|
| 12 |
+
w_std=None,
|
| 13 |
+
s_mean=None,
|
| 14 |
+
s_std=None,
|
| 15 |
+
y_mean=None,
|
| 16 |
+
y_std=None,
|
| 17 |
+
K=None,
|
| 18 |
+
W=None,
|
| 19 |
+
S=None,
|
| 20 |
+
train_cutoffs=None,
|
| 21 |
+
eval_cutoffs=None,
|
| 22 |
+
d_model=128,
|
| 23 |
+
nhead=4,
|
| 24 |
+
num_layers=4,
|
| 25 |
+
dim_ff=256,
|
| 26 |
+
dropout=0.3,
|
| 27 |
+
pool="mean",
|
| 28 |
+
use_crop=True,
|
| 29 |
+
crop_emb_dim=8,
|
| 30 |
+
**kwargs,
|
| 31 |
+
):
|
| 32 |
+
super().__init__(**kwargs)
|
| 33 |
+
|
| 34 |
+
self.weather_vars = weather_vars
|
| 35 |
+
self.soil_vars = soil_vars
|
| 36 |
+
|
| 37 |
+
self.w_mean = w_mean
|
| 38 |
+
self.w_std = w_std
|
| 39 |
+
self.s_mean = s_mean
|
| 40 |
+
self.s_std = s_std
|
| 41 |
+
self.y_mean = y_mean
|
| 42 |
+
self.y_std = y_std
|
| 43 |
+
|
| 44 |
+
self.K = K
|
| 45 |
+
self.W = W
|
| 46 |
+
self.S = S
|
| 47 |
+
|
| 48 |
+
self.train_cutoffs = train_cutoffs
|
| 49 |
+
self.eval_cutoffs = eval_cutoffs
|
| 50 |
+
|
| 51 |
+
self.d_model = d_model
|
| 52 |
+
self.nhead = nhead
|
| 53 |
+
self.num_layers = num_layers
|
| 54 |
+
self.dim_ff = dim_ff
|
| 55 |
+
self.dropout = dropout
|
| 56 |
+
self.pool = pool
|
| 57 |
+
self.use_crop = use_crop
|
| 58 |
+
self.crop_emb_dim = crop_emb_dim
|
training_code/hf/modeling_yield.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 5 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
from transformers import PreTrainedModel
|
| 10 |
+
from transformers.modeling_outputs import ModelOutput
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
from hf.configuration_yield import YieldConfig
|
| 14 |
+
from models.unimodal_ws_crossattn import UnimodalWS_CrossAttn_TemporalTF
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class YieldModelOutput(ModelOutput):
|
| 19 |
+
loss: torch.Tensor | None = None
|
| 20 |
+
logits: torch.Tensor | None = None
|
| 21 |
+
predictions: torch.Tensor | None = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class YieldForRegression(PreTrainedModel):
|
| 25 |
+
config_class = YieldConfig
|
| 26 |
+
base_model_prefix = "yield_model"
|
| 27 |
+
|
| 28 |
+
def __init__(self, config: YieldConfig):
|
| 29 |
+
super().__init__(config)
|
| 30 |
+
|
| 31 |
+
self.yield_model = UnimodalWS_CrossAttn_TemporalTF(
|
| 32 |
+
w_dim=config.W,
|
| 33 |
+
soil_dim=config.S,
|
| 34 |
+
d_model=config.d_model,
|
| 35 |
+
nhead=config.nhead,
|
| 36 |
+
num_layers=config.num_layers,
|
| 37 |
+
dim_ff=config.dim_ff,
|
| 38 |
+
dropout=config.dropout,
|
| 39 |
+
use_crop=config.use_crop,
|
| 40 |
+
crop_emb_dim=config.crop_emb_dim,
|
| 41 |
+
max_weeks=max(32, config.K),
|
| 42 |
+
pool=config.pool,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.post_init()
|
| 46 |
+
|
| 47 |
+
def forward(
|
| 48 |
+
self,
|
| 49 |
+
weather,
|
| 50 |
+
soil,
|
| 51 |
+
crop_id,
|
| 52 |
+
labels=None,
|
| 53 |
+
horizon_idx=None,
|
| 54 |
+
causal=True,
|
| 55 |
+
return_sequence=False,
|
| 56 |
+
return_dict=True,
|
| 57 |
+
):
|
| 58 |
+
if horizon_idx is None:
|
| 59 |
+
horizon_idx = weather.shape[1]
|
| 60 |
+
|
| 61 |
+
logits = self.yield_model(
|
| 62 |
+
weather,
|
| 63 |
+
soil,
|
| 64 |
+
crop_id,
|
| 65 |
+
horizon_idx=horizon_idx,
|
| 66 |
+
causal=causal,
|
| 67 |
+
return_sequence=return_sequence,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
y_mean = torch.tensor(self.config.y_mean, device=logits.device, dtype=logits.dtype)
|
| 71 |
+
y_std = torch.tensor(self.config.y_std, device=logits.device, dtype=logits.dtype)
|
| 72 |
+
|
| 73 |
+
#predictions = torch.expm1(logits * y_std + y_mean)
|
| 74 |
+
predictions = logits * y_std + y_mean
|
| 75 |
+
|
| 76 |
+
loss = None
|
| 77 |
+
if labels is not None:
|
| 78 |
+
# labels_log = torch.log1p(torch.clamp(labels, min=0.0))
|
| 79 |
+
# labels_norm = (labels_log - y_mean) / y_std
|
| 80 |
+
# loss = nn.functional.mse_loss(logits, labels_norm)
|
| 81 |
+
labels_norm = (labels - y_mean) / y_std
|
| 82 |
+
loss = nn.functional.mse_loss(logits, labels_norm)
|
| 83 |
+
|
| 84 |
+
if not return_dict:
|
| 85 |
+
return (loss, logits, predictions)
|
| 86 |
+
|
| 87 |
+
return YieldModelOutput(
|
| 88 |
+
loss=loss,
|
| 89 |
+
logits=logits,
|
| 90 |
+
predictions=predictions,
|
| 91 |
+
)
|
training_code/models/__init__.py
ADDED
|
File without changes
|
training_code/models/unimodal_ws_crossattn.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class MLP(nn.Module):
|
| 6 |
+
def __init__(self, in_dim: int, out_dim: int, hidden: int = 128, dropout: float = 0.05):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.net = nn.Sequential(
|
| 9 |
+
nn.Linear(in_dim, hidden//2),
|
| 10 |
+
nn.GELU(),
|
| 11 |
+
nn.Dropout(dropout),
|
| 12 |
+
nn.Linear(hidden//2, hidden),
|
| 13 |
+
nn.GELU(),
|
| 14 |
+
nn.Dropout(dropout),
|
| 15 |
+
nn.Linear(hidden, out_dim)
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
def forward(self, x):
|
| 19 |
+
return self.net(x)
|
| 20 |
+
|
| 21 |
+
class UnimodalWS_CrossAttn_TemporalTF(nn.Module):
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
w_dim: int,
|
| 25 |
+
soil_dim: int,
|
| 26 |
+
d_model: int = 128,
|
| 27 |
+
nhead: int = 4,
|
| 28 |
+
num_layers: int = 2,
|
| 29 |
+
dim_ff: int = 256,
|
| 30 |
+
dropout: float = 0.05,
|
| 31 |
+
use_crop: bool = True,
|
| 32 |
+
crop_emb_dim: int = 8,
|
| 33 |
+
max_weeks: int = 32,
|
| 34 |
+
pool: str = "mean", # Changed default to mean
|
| 35 |
+
farm_emb_dim: int = 8,
|
| 36 |
+
horizon_emb_dim: int = 16,
|
| 37 |
+
):
|
| 38 |
+
super().__init__()
|
| 39 |
+
assert pool in ["last", "cls", "mean"]
|
| 40 |
+
self.pool = pool
|
| 41 |
+
self.use_crop = use_crop
|
| 42 |
+
self.max_weeks = max_weeks
|
| 43 |
+
|
| 44 |
+
crop_in = crop_emb_dim if use_crop else 0
|
| 45 |
+
if use_crop:
|
| 46 |
+
self.crop_emb = nn.Embedding(2, crop_emb_dim)
|
| 47 |
+
|
| 48 |
+
self.horizon_emb = nn.Embedding(max_weeks + 1, horizon_emb_dim)
|
| 49 |
+
self.horizon_proj = nn.Linear(horizon_emb_dim, d_model)
|
| 50 |
+
|
| 51 |
+
# Hard Positional Signal: Week ID projection
|
| 52 |
+
self.week_proj = nn.Linear(1, d_model)
|
| 53 |
+
|
| 54 |
+
self.weather_enc = MLP(w_dim + crop_in, d_model, hidden=d_model, dropout=dropout)
|
| 55 |
+
self.soil_enc = MLP(soil_dim + crop_in, d_model, hidden=d_model, dropout=dropout)
|
| 56 |
+
|
| 57 |
+
self.ws_cross_attn = nn.MultiheadAttention(
|
| 58 |
+
embed_dim=d_model,
|
| 59 |
+
num_heads=nhead,
|
| 60 |
+
dropout=dropout,
|
| 61 |
+
batch_first=True,
|
| 62 |
+
)
|
| 63 |
+
self.ws_ln = nn.LayerNorm(d_model)
|
| 64 |
+
|
| 65 |
+
self.pos_emb = nn.Parameter(torch.zeros(1, max_weeks + 1, d_model))
|
| 66 |
+
nn.init.trunc_normal_(self.pos_emb, std=0.02)
|
| 67 |
+
|
| 68 |
+
if pool == "cls":
|
| 69 |
+
self.cls_token = nn.Parameter(torch.zeros(1, 1, d_model))
|
| 70 |
+
nn.init.trunc_normal_(self.cls_token, std=0.02)
|
| 71 |
+
|
| 72 |
+
enc_layer = nn.TransformerEncoderLayer(
|
| 73 |
+
d_model=d_model, nhead=nhead, dim_feedforward=dim_ff,
|
| 74 |
+
dropout=dropout, activation="gelu", batch_first=True, norm_first=True,
|
| 75 |
+
)
|
| 76 |
+
self.temporal_tf = nn.TransformerEncoder(enc_layer, num_layers=num_layers)
|
| 77 |
+
|
| 78 |
+
# Head now takes [Pooled Sequence + Horizon Embedding + Soil Skip Connection]
|
| 79 |
+
self.head = nn.Sequential(
|
| 80 |
+
nn.LayerNorm(d_model + horizon_emb_dim + d_model),
|
| 81 |
+
nn.Linear(d_model + horizon_emb_dim + d_model, d_model),
|
| 82 |
+
nn.GELU(),
|
| 83 |
+
nn.Dropout(dropout),
|
| 84 |
+
nn.Linear(d_model, 1),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def forward(
|
| 88 |
+
self,
|
| 89 |
+
weather,
|
| 90 |
+
soil,
|
| 91 |
+
crop_id,
|
| 92 |
+
farm_field_encoded=None,
|
| 93 |
+
week_mask=None,
|
| 94 |
+
causal=False,
|
| 95 |
+
return_sequence=False,
|
| 96 |
+
horizon_idx=None,
|
| 97 |
+
):
|
| 98 |
+
B, t, W = weather.shape
|
| 99 |
+
device = weather.device
|
| 100 |
+
|
| 101 |
+
# 1. Horizon Processing
|
| 102 |
+
if horizon_idx is None: horizon_idx = torch.tensor(t, device=device).expand(B)
|
| 103 |
+
if not torch.is_tensor(horizon_idx): horizon_idx = torch.tensor(horizon_idx, device=device).expand(B)
|
| 104 |
+
|
| 105 |
+
h_idx = horizon_idx.long().clamp(min=1, max=self.max_weeks)
|
| 106 |
+
h_emb = self.horizon_emb(h_idx)
|
| 107 |
+
h_tok = self.horizon_proj(h_emb) # [B, D]
|
| 108 |
+
|
| 109 |
+
# 2. Input Encoding
|
| 110 |
+
if self.use_crop:
|
| 111 |
+
c = self.crop_emb(crop_id)
|
| 112 |
+
weather_in = torch.cat([weather, c[:, None, :].expand(-1, t, -1)], dim=-1)
|
| 113 |
+
soil_in = torch.cat([soil, c], dim=-1)
|
| 114 |
+
else:
|
| 115 |
+
weather_in, soil_in = weather, soil
|
| 116 |
+
|
| 117 |
+
w_tok = self.weather_enc(weather_in)
|
| 118 |
+
s_encoded = self.soil_enc(soil_in) # [B, D] - Saved for skip connection later
|
| 119 |
+
|
| 120 |
+
# 3. Cross Attention & Horizon
|
| 121 |
+
# attn_out, _ = self.ws_cross_attn(query=w_tok, key=s_encoded[:, None, :], value=s_encoded[:, None, :])
|
| 122 |
+
# fused = self.ws_ln(w_tok + attn_out)
|
| 123 |
+
|
| 124 |
+
fused = self.ws_ln(w_tok + s_encoded[:, None, :])
|
| 125 |
+
fused = fused + h_tok[:, None, :] # Horizon aware tokens
|
| 126 |
+
|
| 127 |
+
# 4. Temporal Transformer
|
| 128 |
+
# Add Hard Week Signal + Learned Pos Emb
|
| 129 |
+
weeks = torch.arange(1, t + 1, device=device).float() / self.max_weeks
|
| 130 |
+
week_signal = self.week_proj(weeks[None, :, None].expand(B, -1, -1))
|
| 131 |
+
|
| 132 |
+
x = fused + week_signal + self.pos_emb[:, :t, :]
|
| 133 |
+
|
| 134 |
+
if self.pool == "cls":
|
| 135 |
+
x = torch.cat([self.cls_token.expand(B, -1, -1), x], dim=1)
|
| 136 |
+
t_idx = t + 1
|
| 137 |
+
else: t_idx = t
|
| 138 |
+
|
| 139 |
+
src_key_padding_mask = ~week_mask.bool() if week_mask is not None else None
|
| 140 |
+
#h = self.temporal_tf(x, src_key_padding_mask=src_key_padding_mask)
|
| 141 |
+
|
| 142 |
+
attn_mask = None
|
| 143 |
+
if causal:
|
| 144 |
+
L = x.size(1)
|
| 145 |
+
attn_mask = torch.triu(
|
| 146 |
+
torch.ones(L, L, device=device, dtype=torch.bool),
|
| 147 |
+
diagonal=1,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
h = self.temporal_tf(
|
| 151 |
+
x,
|
| 152 |
+
mask=attn_mask,
|
| 153 |
+
src_key_padding_mask=src_key_padding_mask,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# 5. Global Mean Pooling
|
| 157 |
+
if self.pool == "mean":
|
| 158 |
+
if week_mask is not None:
|
| 159 |
+
mask = week_mask.unsqueeze(-1).float()
|
| 160 |
+
pooled = (h * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1e-9)
|
| 161 |
+
else:
|
| 162 |
+
pooled = h.mean(dim=1)
|
| 163 |
+
elif self.pool == "cls":
|
| 164 |
+
pooled = h[:, 0, :]
|
| 165 |
+
else: # "last"
|
| 166 |
+
pooled = h[:, -1, :]
|
| 167 |
+
|
| 168 |
+
# 6. Final Fusion (Pooled Sequence + Horizon + Soil Skip)
|
| 169 |
+
out = torch.cat([pooled, h_emb, s_encoded], dim=-1)
|
| 170 |
+
return self.head(out).squeeze(-1)
|
| 171 |
+
|
training_code/requirements.txt
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
annotated-types==0.7.0
|
| 2 |
+
anyio==4.11.0
|
| 3 |
+
argon2-cffi==25.1.0
|
| 4 |
+
argon2-cffi-bindings==25.1.0
|
| 5 |
+
arrow==1.4.0
|
| 6 |
+
ase==3.22.1
|
| 7 |
+
asttokens==2.2.1
|
| 8 |
+
async-lru==2.0.5
|
| 9 |
+
attrs==25.4.0
|
| 10 |
+
babel==2.17.0
|
| 11 |
+
backcall==0.2.0
|
| 12 |
+
beautifulsoup4==4.14.2
|
| 13 |
+
bleach==6.3.0
|
| 14 |
+
blosc2==4.3.3
|
| 15 |
+
certifi==2026.6.17
|
| 16 |
+
charset-normalizer==3.4.7
|
| 17 |
+
comm==0.2.3
|
| 18 |
+
contourpy==1.0.7
|
| 19 |
+
cycler==0.11.0
|
| 20 |
+
debugpy==1.8.17
|
| 21 |
+
decorator==5.1.1
|
| 22 |
+
defusedxml==0.7.1
|
| 23 |
+
exceptiongroup==1.3.1
|
| 24 |
+
executing==1.2.0
|
| 25 |
+
fastjsonschema==2.21.2
|
| 26 |
+
filelock==3.29.0
|
| 27 |
+
fonttools==4.39.3
|
| 28 |
+
fqdn==1.5.1
|
| 29 |
+
fsspec==2026.4.0
|
| 30 |
+
googledrivedownloader==0.4
|
| 31 |
+
h11==0.16.0
|
| 32 |
+
h5py==3.8.0
|
| 33 |
+
hf-xet==1.5.1
|
| 34 |
+
httpcore==1.0.9
|
| 35 |
+
httpx==0.28.1
|
| 36 |
+
huggingface-hub==0.32.4
|
| 37 |
+
idna==3.18
|
| 38 |
+
ipdb==0.13.7
|
| 39 |
+
ipykernel==7.1.0
|
| 40 |
+
ipython==8.12.0
|
| 41 |
+
ipywidgets==8.1.8
|
| 42 |
+
isodate==0.6.1
|
| 43 |
+
isoduration==20.11.0
|
| 44 |
+
jedi==0.18.2
|
| 45 |
+
Jinja2==3.1.6
|
| 46 |
+
joblib==1.2.0
|
| 47 |
+
json5==0.12.1
|
| 48 |
+
jsonschema==4.25.1
|
| 49 |
+
jsonschema-specifications==2025.9.1
|
| 50 |
+
jupyter==1.1.1
|
| 51 |
+
jupyter-console==6.6.3
|
| 52 |
+
jupyter-events==0.12.0
|
| 53 |
+
jupyter-lsp==2.3.0
|
| 54 |
+
jupyter_client==8.6.3
|
| 55 |
+
jupyter_core==5.9.1
|
| 56 |
+
jupyter_server==2.17.0
|
| 57 |
+
jupyter_server_terminals==0.5.3
|
| 58 |
+
jupyterlab==4.5.0
|
| 59 |
+
jupyterlab_pygments==0.3.0
|
| 60 |
+
jupyterlab_server==2.28.0
|
| 61 |
+
jupyterlab_widgets==3.0.16
|
| 62 |
+
kiwisolver==1.4.4
|
| 63 |
+
lark==1.3.1
|
| 64 |
+
llvmlite==0.39.1
|
| 65 |
+
MarkupSafe==3.0.3
|
| 66 |
+
matplotlib==3.10.7
|
| 67 |
+
matplotlib-inline==0.1.6
|
| 68 |
+
mistune==3.1.4
|
| 69 |
+
mpmath==1.3.0
|
| 70 |
+
msgpack==1.2.1
|
| 71 |
+
nbclient==0.10.2
|
| 72 |
+
nbconvert==7.16.6
|
| 73 |
+
nbformat==5.10.4
|
| 74 |
+
ndindex==1.10.1
|
| 75 |
+
nest-asyncio==1.6.0
|
| 76 |
+
networkx==3.4.2
|
| 77 |
+
notebook==7.5.0
|
| 78 |
+
notebook_shim==0.2.4
|
| 79 |
+
numba==0.56.4
|
| 80 |
+
numexpr==2.14.1
|
| 81 |
+
numpy==2.2.6
|
| 82 |
+
nvidia-cufile-cu12==1.13.1.3
|
| 83 |
+
nvidia-cusparselt-cu12==0.7.1
|
| 84 |
+
nvidia-nvjitlink-cu12==12.8.93
|
| 85 |
+
nvidia-nvshmem-cu12==3.3.20
|
| 86 |
+
overrides==7.7.0
|
| 87 |
+
packaging==26.0
|
| 88 |
+
pandas==2.3.3
|
| 89 |
+
pandocfilters==1.5.1
|
| 90 |
+
parso==0.8.3
|
| 91 |
+
pexpect==4.8.0
|
| 92 |
+
pickleshare==0.7.5
|
| 93 |
+
pillow==12.0.0
|
| 94 |
+
prometheus_client==0.23.1
|
| 95 |
+
prompt-toolkit==3.0.38
|
| 96 |
+
psutil==7.1.3
|
| 97 |
+
ptyprocess==0.7.0
|
| 98 |
+
pure-eval==0.2.2
|
| 99 |
+
py-cpuinfo==9.0.0
|
| 100 |
+
pydantic==2.13.4
|
| 101 |
+
pydantic_core==2.46.4
|
| 102 |
+
Pygments==2.14.0
|
| 103 |
+
pyparsing==3.0.9
|
| 104 |
+
python-dateutil==2.8.2
|
| 105 |
+
python-json-logger==4.0.0
|
| 106 |
+
python-louvain==0.16
|
| 107 |
+
pytz==2023.3
|
| 108 |
+
PyYAML==6.0.3
|
| 109 |
+
pyzmq==27.1.0
|
| 110 |
+
rdflib==6.3.2
|
| 111 |
+
referencing==0.37.0
|
| 112 |
+
regex==2026.6.28
|
| 113 |
+
requests==2.34.2
|
| 114 |
+
rfc3339-validator==0.1.4
|
| 115 |
+
rfc3986-validator==0.1.1
|
| 116 |
+
rfc3987-syntax==1.1.0
|
| 117 |
+
rpds-py==0.29.0
|
| 118 |
+
safetensors==0.8.0
|
| 119 |
+
scikit-learn==1.7.2
|
| 120 |
+
scipy==1.15.3
|
| 121 |
+
seaborn==0.12.2
|
| 122 |
+
Send2Trash==1.8.3
|
| 123 |
+
six==1.16.0
|
| 124 |
+
sklearn==0.0.post1
|
| 125 |
+
sniffio==1.3.1
|
| 126 |
+
soupsieve==2.8
|
| 127 |
+
stack-data==0.6.2
|
| 128 |
+
sympy==1.14.0
|
| 129 |
+
tables==3.10.1
|
| 130 |
+
terminado==0.18.1
|
| 131 |
+
threadpoolctl==3.1.0
|
| 132 |
+
tinycss2==1.4.0
|
| 133 |
+
tokenizers==0.21.1
|
| 134 |
+
toml==0.10.2
|
| 135 |
+
tomli==2.3.0
|
| 136 |
+
torch==2.12.1+cpu
|
| 137 |
+
torch-geometric==1.7.0
|
| 138 |
+
torchcodec==0.8.1
|
| 139 |
+
torchvision==0.27.1+cpu
|
| 140 |
+
tornado==6.5.2
|
| 141 |
+
tqdm==4.68.3
|
| 142 |
+
traitlets==5.9.0
|
| 143 |
+
transformers==4.48.3
|
| 144 |
+
typing-inspection==0.4.2
|
| 145 |
+
typing_extensions==4.15.0
|
| 146 |
+
tzdata==2025.2
|
| 147 |
+
uri-template==1.3.0
|
| 148 |
+
urllib3==2.7.0
|
| 149 |
+
wcwidth==0.2.6
|
| 150 |
+
webcolors==25.10.0
|
| 151 |
+
webencodings==0.5.1
|
| 152 |
+
websocket-client==1.9.0
|
| 153 |
+
widgetsnbextension==4.0.15
|
training_code/scripts/__init__.py
ADDED
|
File without changes
|
training_code/scripts/evaluate_hf.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 5 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import torch
|
| 11 |
+
from torch.utils.data import DataLoader
|
| 12 |
+
from transformers import AutoModel
|
| 13 |
+
|
| 14 |
+
from data.dataset import YieldDataset
|
| 15 |
+
from training.engine import evaluate
|
| 16 |
+
from hf.auto import register_yield_autoclass
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def parse_int_list(x):
|
| 20 |
+
return [int(v.strip()) for v in x.split(",") if v.strip()]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_args():
|
| 24 |
+
p = argparse.ArgumentParser()
|
| 25 |
+
p.add_argument("--hf_model_dir", required=True)
|
| 26 |
+
p.add_argument("--test_file", required=True)
|
| 27 |
+
p.add_argument("--cutoffs", default=None)
|
| 28 |
+
p.add_argument("--batch_size", type=int, default=64)
|
| 29 |
+
p.add_argument("--output_csv", default="eval_predictions.csv")
|
| 30 |
+
p.add_argument("--metrics_json", default="eval_metrics.json")
|
| 31 |
+
p.add_argument(
|
| 32 |
+
"--time_agg",
|
| 33 |
+
default="weekly",
|
| 34 |
+
choices=["weekly", "weekly_cumulative"],
|
| 35 |
+
)
|
| 36 |
+
return p.parse_args()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main():
|
| 40 |
+
args = parse_args()
|
| 41 |
+
register_yield_autoclass()
|
| 42 |
+
|
| 43 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 44 |
+
|
| 45 |
+
model = AutoModel.from_pretrained(args.hf_model_dir).to(device)
|
| 46 |
+
model.eval()
|
| 47 |
+
cfg = model.config
|
| 48 |
+
|
| 49 |
+
cutoffs = parse_int_list(args.cutoffs) if args.cutoffs else cfg.eval_cutoffs
|
| 50 |
+
|
| 51 |
+
test_ds = YieldDataset(
|
| 52 |
+
data_file=args.test_file,
|
| 53 |
+
weather_vars=cfg.weather_vars,
|
| 54 |
+
soil_vars=cfg.soil_vars,
|
| 55 |
+
split="all",
|
| 56 |
+
seed=1234,
|
| 57 |
+
crop=None,
|
| 58 |
+
years=None,
|
| 59 |
+
time_agg=args.time_agg,
|
| 60 |
+
)
|
| 61 |
+
test_ds.set_normalization(
|
| 62 |
+
cfg.w_mean,
|
| 63 |
+
cfg.w_std,
|
| 64 |
+
cfg.s_mean,
|
| 65 |
+
cfg.s_std,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
loader = DataLoader(test_ds, batch_size=args.batch_size, shuffle=False)
|
| 69 |
+
|
| 70 |
+
metrics, rows = evaluate(
|
| 71 |
+
model=model,
|
| 72 |
+
loader=loader,
|
| 73 |
+
device=device,
|
| 74 |
+
y_mean=cfg.y_mean,
|
| 75 |
+
y_std=cfg.y_std,
|
| 76 |
+
cutoffs=cutoffs,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
print(json.dumps(metrics, indent=2))
|
| 80 |
+
|
| 81 |
+
Path(args.output_csv).parent.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
pd.DataFrame(rows).to_csv(args.output_csv, index=False)
|
| 83 |
+
|
| 84 |
+
with open(args.metrics_json, "w") as f:
|
| 85 |
+
json.dump(metrics, f, indent=2)
|
| 86 |
+
|
| 87 |
+
print(f"Saved predictions to {args.output_csv}")
|
| 88 |
+
print(f"Saved metrics to {args.metrics_json}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
main()
|
training_code/scripts/inference_hf.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 5 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import torch
|
| 12 |
+
from torch.utils.data import DataLoader
|
| 13 |
+
from transformers import AutoModel
|
| 14 |
+
|
| 15 |
+
from data.dataset import YieldDataset
|
| 16 |
+
from data.preprocessing import daily_to_cumulative_weekly, DEFAULT_WEATHER_AGG_RULES
|
| 17 |
+
from hf.auto import register_yield_autoclass
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def parse_args():
|
| 21 |
+
p = argparse.ArgumentParser()
|
| 22 |
+
p.add_argument("--hf_model_dir", required=True)
|
| 23 |
+
p.add_argument("--input_file", default=None)
|
| 24 |
+
p.add_argument("--single_sample_json", default=None)
|
| 25 |
+
p.add_argument("--cutoff", type=int, required=True)
|
| 26 |
+
p.add_argument("--batch_size", type=int, default=64)
|
| 27 |
+
p.add_argument("--output_csv", default="inference_predictions.csv")
|
| 28 |
+
return p.parse_args()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_single_sample_json(path, cfg, cutoff):
|
| 32 |
+
with open(path, "r") as f:
|
| 33 |
+
sample = json.load(f)
|
| 34 |
+
|
| 35 |
+
weather_format = sample.get("weather_format", "weekly_cumulative")
|
| 36 |
+
|
| 37 |
+
weather_cols = []
|
| 38 |
+
for v in cfg.weather_vars:
|
| 39 |
+
if v not in sample["weather"]:
|
| 40 |
+
raise ValueError(f"Missing weather variable in JSON: {v}")
|
| 41 |
+
|
| 42 |
+
arr = np.asarray(sample["weather"][v], dtype=np.float32)
|
| 43 |
+
|
| 44 |
+
if weather_format == "daily":
|
| 45 |
+
agg = DEFAULT_WEATHER_AGG_RULES.get(v, "mean")
|
| 46 |
+
arr = daily_to_cumulative_weekly(arr, agg=agg, week_len=7)
|
| 47 |
+
elif weather_format in ("weekly", "weekly_cumulative"):
|
| 48 |
+
pass
|
| 49 |
+
else:
|
| 50 |
+
raise ValueError(
|
| 51 |
+
"weather_format must be 'daily', 'weekly', "
|
| 52 |
+
"or 'weekly_cumulative'."
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
weather_cols.append(arr)
|
| 56 |
+
|
| 57 |
+
lengths = [len(x) for x in weather_cols]
|
| 58 |
+
if len(set(lengths)) != 1:
|
| 59 |
+
raise ValueError(f"Weather variable lengths do not match after aggregation: {lengths}")
|
| 60 |
+
|
| 61 |
+
weather = np.stack(weather_cols, axis=1).astype(np.float32)
|
| 62 |
+
|
| 63 |
+
soil = []
|
| 64 |
+
for v in cfg.soil_vars:
|
| 65 |
+
if v not in sample["soil"]:
|
| 66 |
+
raise ValueError(f"Missing soil variable in JSON: {v}")
|
| 67 |
+
soil.append(float(sample["soil"][v]))
|
| 68 |
+
|
| 69 |
+
soil = np.asarray(soil, dtype=np.float32)
|
| 70 |
+
|
| 71 |
+
w_mean = np.asarray(cfg.w_mean, dtype=np.float32)
|
| 72 |
+
w_std = np.asarray(cfg.w_std, dtype=np.float32)
|
| 73 |
+
s_mean = np.asarray(cfg.s_mean, dtype=np.float32)
|
| 74 |
+
s_std = np.asarray(cfg.s_std, dtype=np.float32)
|
| 75 |
+
|
| 76 |
+
weather = np.where(np.isnan(weather), w_mean[None, :], weather)
|
| 77 |
+
weather = (weather - w_mean[None, :]) / w_std[None, :]
|
| 78 |
+
|
| 79 |
+
soil = np.where(np.isnan(soil), s_mean, soil)
|
| 80 |
+
soil = (soil - s_mean) / s_std
|
| 81 |
+
|
| 82 |
+
crop_map = {"corn": 0, "maize": 0, "soybean": 1, "soy": 1}
|
| 83 |
+
crop = str(sample.get("crop", "corn")).strip().lower()
|
| 84 |
+
crop_id = crop_map.get(crop, 0)
|
| 85 |
+
|
| 86 |
+
t_eff = min(cutoff, weather.shape[0])
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"weather": torch.from_numpy(weather[:t_eff]).unsqueeze(0),
|
| 90 |
+
"soil": torch.from_numpy(soil).unsqueeze(0),
|
| 91 |
+
"crop_id": torch.tensor([crop_id], dtype=torch.long),
|
| 92 |
+
"t_eff": t_eff,
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@torch.no_grad()
|
| 97 |
+
def main():
|
| 98 |
+
args = parse_args()
|
| 99 |
+
register_yield_autoclass()
|
| 100 |
+
|
| 101 |
+
if args.input_file is None and args.single_sample_json is None:
|
| 102 |
+
raise ValueError("Provide either --input_file or --single_sample_json.")
|
| 103 |
+
|
| 104 |
+
if args.input_file is not None and args.single_sample_json is not None:
|
| 105 |
+
raise ValueError("Use only one: --input_file OR --single_sample_json.")
|
| 106 |
+
|
| 107 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 108 |
+
|
| 109 |
+
model = AutoModel.from_pretrained(args.hf_model_dir).to(device)
|
| 110 |
+
model.eval()
|
| 111 |
+
cfg = model.config
|
| 112 |
+
|
| 113 |
+
if args.single_sample_json is not None:
|
| 114 |
+
sample = load_single_sample_json(args.single_sample_json, cfg, args.cutoff)
|
| 115 |
+
|
| 116 |
+
weather = sample["weather"].to(device)
|
| 117 |
+
soil = sample["soil"].to(device)
|
| 118 |
+
crop_id = sample["crop_id"].to(device)
|
| 119 |
+
t_eff = sample["t_eff"]
|
| 120 |
+
|
| 121 |
+
out = model(
|
| 122 |
+
weather=weather,
|
| 123 |
+
soil=soil,
|
| 124 |
+
crop_id=crop_id,
|
| 125 |
+
horizon_idx=t_eff,
|
| 126 |
+
causal=True,
|
| 127 |
+
return_sequence=False,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
pred = float(out.predictions.item())
|
| 131 |
+
|
| 132 |
+
Path(args.output_csv).parent.mkdir(parents=True, exist_ok=True)
|
| 133 |
+
pd.DataFrame([{
|
| 134 |
+
"sample_idx": 0,
|
| 135 |
+
"cutoff": int(args.cutoff),
|
| 136 |
+
"y_pred": pred,
|
| 137 |
+
}]).to_csv(args.output_csv, index=False)
|
| 138 |
+
|
| 139 |
+
print(f"Predicted yield: {pred:.4f}")
|
| 140 |
+
print(f"Saved inference prediction to {args.output_csv}")
|
| 141 |
+
return
|
| 142 |
+
|
| 143 |
+
ds = YieldDataset(
|
| 144 |
+
data_file=args.input_file,
|
| 145 |
+
weather_vars=cfg.weather_vars,
|
| 146 |
+
soil_vars=cfg.soil_vars,
|
| 147 |
+
split="all",
|
| 148 |
+
seed=1234,
|
| 149 |
+
crop=None,
|
| 150 |
+
years=None,
|
| 151 |
+
require_yield=False,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
ds.set_normalization(
|
| 155 |
+
cfg.w_mean,
|
| 156 |
+
cfg.w_std,
|
| 157 |
+
cfg.s_mean,
|
| 158 |
+
cfg.s_std,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
loader = DataLoader(ds, batch_size=args.batch_size, shuffle=False)
|
| 162 |
+
|
| 163 |
+
rows = []
|
| 164 |
+
sample_idx = 0
|
| 165 |
+
|
| 166 |
+
for batch in loader:
|
| 167 |
+
weather = batch["weather"].to(device)
|
| 168 |
+
soil = batch["soil"].to(device)
|
| 169 |
+
crop_id = batch["crop_id"].to(device)
|
| 170 |
+
|
| 171 |
+
t_eff = min(args.cutoff, weather.size(1))
|
| 172 |
+
|
| 173 |
+
out = model(
|
| 174 |
+
weather=weather[:, :t_eff, :],
|
| 175 |
+
soil=soil,
|
| 176 |
+
crop_id=crop_id,
|
| 177 |
+
horizon_idx=t_eff,
|
| 178 |
+
causal=True,
|
| 179 |
+
return_sequence=False,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
for pred in out.predictions.detach().cpu().numpy().tolist():
|
| 183 |
+
rows.append({
|
| 184 |
+
"sample_idx": sample_idx,
|
| 185 |
+
"cutoff": int(args.cutoff),
|
| 186 |
+
"y_pred": float(pred),
|
| 187 |
+
})
|
| 188 |
+
sample_idx += 1
|
| 189 |
+
|
| 190 |
+
Path(args.output_csv).parent.mkdir(parents=True, exist_ok=True)
|
| 191 |
+
pd.DataFrame(rows).to_csv(args.output_csv, index=False)
|
| 192 |
+
|
| 193 |
+
print(f"Saved inference predictions to {args.output_csv}")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
main()
|
training_code/scripts/prepare_cornbelt.py
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# ============================================================
|
| 8 |
+
# Configuration
|
| 9 |
+
# ============================================================
|
| 10 |
+
|
| 11 |
+
INPUT_CSV = Path("khaki_multi_crop_yield.csv")
|
| 12 |
+
OUTPUT_DIR = Path("data/cornbelt")
|
| 13 |
+
|
| 14 |
+
TRAIN_YEARS = [2013, 2014, 2015, 2016]
|
| 15 |
+
VAL_YEAR = 2017
|
| 16 |
+
TEST_YEAR = 2018
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Public dataset weather mapping
|
| 20 |
+
WEATHER_MAP = {
|
| 21 |
+
"prcp": 1,
|
| 22 |
+
"srad": 2,
|
| 23 |
+
"swe": 3,
|
| 24 |
+
"tmax": 4,
|
| 25 |
+
"tmin": 5,
|
| 26 |
+
"vp": 6,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
SOIL_MEASUREMENTS = [
|
| 31 |
+
"bdod",
|
| 32 |
+
"cec",
|
| 33 |
+
"cfvo",
|
| 34 |
+
"clay",
|
| 35 |
+
"nitrogen",
|
| 36 |
+
"ocd",
|
| 37 |
+
"ocs",
|
| 38 |
+
"phh2o",
|
| 39 |
+
"sand",
|
| 40 |
+
"silt",
|
| 41 |
+
"soc",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
SOIL_DEPTHS = [
|
| 46 |
+
"0-5cm",
|
| 47 |
+
"5-15cm",
|
| 48 |
+
"15-30cm",
|
| 49 |
+
"30-60cm",
|
| 50 |
+
"60-100cm",
|
| 51 |
+
"100-200cm",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
SOIL_VARS = [
|
| 56 |
+
f"{measurement}_mean_{depth}"
|
| 57 |
+
for measurement in SOIL_MEASUREMENTS
|
| 58 |
+
for depth in SOIL_DEPTHS
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ============================================================
|
| 63 |
+
# Load
|
| 64 |
+
# ============================================================
|
| 65 |
+
|
| 66 |
+
print(f"Reading {INPUT_CSV}")
|
| 67 |
+
|
| 68 |
+
df = pd.read_csv(INPUT_CSV)
|
| 69 |
+
|
| 70 |
+
print("Raw shape:", df.shape)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# ============================================================
|
| 74 |
+
# Verify required columns
|
| 75 |
+
# ============================================================
|
| 76 |
+
|
| 77 |
+
if "corn_yield" not in df.columns:
|
| 78 |
+
raise ValueError("corn_yield column not found")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
required_meta = [
|
| 82 |
+
"loc_ID",
|
| 83 |
+
"year",
|
| 84 |
+
"State",
|
| 85 |
+
"County",
|
| 86 |
+
"lat",
|
| 87 |
+
"lng",
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
missing_meta = [
|
| 91 |
+
c for c in required_meta
|
| 92 |
+
if c not in df.columns
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
if missing_meta:
|
| 96 |
+
raise ValueError(
|
| 97 |
+
f"Missing metadata columns: {missing_meta}"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
missing_soil = [
|
| 102 |
+
c for c in SOIL_VARS
|
| 103 |
+
if c not in df.columns
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
if missing_soil:
|
| 107 |
+
raise ValueError(
|
| 108 |
+
f"Missing soil columns: {missing_soil}"
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# Verify all 52 weekly values exist for each weather variable
|
| 113 |
+
missing_weather = []
|
| 114 |
+
|
| 115 |
+
for weather_name, source_idx in WEATHER_MAP.items():
|
| 116 |
+
for week in range(1, 53):
|
| 117 |
+
src = f"W_{source_idx}_{week}"
|
| 118 |
+
|
| 119 |
+
if src not in df.columns:
|
| 120 |
+
missing_weather.append(src)
|
| 121 |
+
|
| 122 |
+
if missing_weather:
|
| 123 |
+
raise ValueError(
|
| 124 |
+
f"Missing weather columns: {missing_weather[:20]}"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# ============================================================
|
| 129 |
+
# Convert year and corn yield to numeric
|
| 130 |
+
# ============================================================
|
| 131 |
+
|
| 132 |
+
df["year"] = pd.to_numeric(
|
| 133 |
+
df["year"],
|
| 134 |
+
errors="coerce",
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
df["corn_yield"] = pd.to_numeric(
|
| 138 |
+
df["corn_yield"],
|
| 139 |
+
errors="coerce",
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ============================================================
|
| 144 |
+
# IMPORTANT:
|
| 145 |
+
# Filter to corn samples from 2013-2018 BEFORE constructing out
|
| 146 |
+
# ============================================================
|
| 147 |
+
|
| 148 |
+
df = df[
|
| 149 |
+
(df["year"] >= 2013) &
|
| 150 |
+
(df["year"] <= 2018)
|
| 151 |
+
].copy()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# Remove samples without valid corn yield or year
|
| 155 |
+
df = df.dropna(
|
| 156 |
+
subset=[
|
| 157 |
+
"corn_yield",
|
| 158 |
+
"year",
|
| 159 |
+
]
|
| 160 |
+
).reset_index(drop=True)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
df["year"] = df["year"].astype(int)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
print()
|
| 167 |
+
print("===================================")
|
| 168 |
+
print("FILTERED SOURCE DATA")
|
| 169 |
+
print("===================================")
|
| 170 |
+
|
| 171 |
+
print(
|
| 172 |
+
"Years retained:",
|
| 173 |
+
sorted(df["year"].unique())
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
print(
|
| 177 |
+
"Rows after 2013-2018 + corn yield filter:",
|
| 178 |
+
len(df)
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
print("\nSamples per year:")
|
| 182 |
+
print(
|
| 183 |
+
df["year"]
|
| 184 |
+
.value_counts()
|
| 185 |
+
.sort_index()
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
expected_years = {
|
| 190 |
+
2013,
|
| 191 |
+
2014,
|
| 192 |
+
2015,
|
| 193 |
+
2016,
|
| 194 |
+
2017,
|
| 195 |
+
2018,
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
assert set(df["year"].unique()) == expected_years
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# ============================================================
|
| 202 |
+
# Construct metadata block
|
| 203 |
+
# ============================================================
|
| 204 |
+
|
| 205 |
+
metadata_df = pd.DataFrame(
|
| 206 |
+
{
|
| 207 |
+
"crop": "corn",
|
| 208 |
+
|
| 209 |
+
# County/location acts as the sample spatial identifier.
|
| 210 |
+
# It is metadata required by the current dataset interface.
|
| 211 |
+
"farm_field": (
|
| 212 |
+
"county_" +
|
| 213 |
+
df["loc_ID"].astype(str)
|
| 214 |
+
),
|
| 215 |
+
|
| 216 |
+
"year": df["year"].astype(int),
|
| 217 |
+
|
| 218 |
+
"yield": df[
|
| 219 |
+
"corn_yield"
|
| 220 |
+
].astype(np.float32),
|
| 221 |
+
|
| 222 |
+
"loc_ID": df["loc_ID"],
|
| 223 |
+
|
| 224 |
+
"state": df[
|
| 225 |
+
"State"
|
| 226 |
+
].astype(str),
|
| 227 |
+
|
| 228 |
+
"county": df[
|
| 229 |
+
"County"
|
| 230 |
+
].astype(str),
|
| 231 |
+
|
| 232 |
+
"lat": pd.to_numeric(
|
| 233 |
+
df["lat"],
|
| 234 |
+
errors="coerce",
|
| 235 |
+
).astype(np.float32),
|
| 236 |
+
|
| 237 |
+
"lng": pd.to_numeric(
|
| 238 |
+
df["lng"],
|
| 239 |
+
errors="coerce",
|
| 240 |
+
).astype(np.float32),
|
| 241 |
+
}
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# ============================================================
|
| 246 |
+
# Weather block
|
| 247 |
+
#
|
| 248 |
+
# IMPORTANT:
|
| 249 |
+
# - No interpolation
|
| 250 |
+
# - No daily conversion
|
| 251 |
+
# - No aggregation
|
| 252 |
+
#
|
| 253 |
+
# Each source W_x_1 ... W_x_52 is copied directly.
|
| 254 |
+
#
|
| 255 |
+
# Example:
|
| 256 |
+
# W_1_1 -> prcp_0
|
| 257 |
+
# W_1_2 -> prcp_1
|
| 258 |
+
# ...
|
| 259 |
+
# W_1_52 -> prcp_51
|
| 260 |
+
# ============================================================
|
| 261 |
+
|
| 262 |
+
weather_data = {}
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
for weather_name, source_idx in WEATHER_MAP.items():
|
| 266 |
+
|
| 267 |
+
for week in range(1, 53):
|
| 268 |
+
|
| 269 |
+
src = f"W_{source_idx}_{week}"
|
| 270 |
+
|
| 271 |
+
# Zero-based temporal indexing used by current loader
|
| 272 |
+
dst = f"{weather_name}_{week - 1}"
|
| 273 |
+
|
| 274 |
+
weather_data[dst] = pd.to_numeric(
|
| 275 |
+
df[src],
|
| 276 |
+
errors="coerce",
|
| 277 |
+
).astype(np.float32)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
weather_df = pd.DataFrame(
|
| 281 |
+
weather_data,
|
| 282 |
+
index=df.index,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# ============================================================
|
| 287 |
+
# Soil block
|
| 288 |
+
# ============================================================
|
| 289 |
+
|
| 290 |
+
soil_data = {}
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
for col in SOIL_VARS:
|
| 294 |
+
|
| 295 |
+
soil_data[col] = pd.to_numeric(
|
| 296 |
+
df[col],
|
| 297 |
+
errors="coerce",
|
| 298 |
+
).astype(np.float32)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
soil_df = pd.DataFrame(
|
| 302 |
+
soil_data,
|
| 303 |
+
index=df.index,
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# ============================================================
|
| 308 |
+
# Combine all blocks
|
| 309 |
+
# ============================================================
|
| 310 |
+
|
| 311 |
+
out = pd.concat(
|
| 312 |
+
[
|
| 313 |
+
metadata_df.reset_index(drop=True),
|
| 314 |
+
weather_df.reset_index(drop=True),
|
| 315 |
+
soil_df.reset_index(drop=True),
|
| 316 |
+
],
|
| 317 |
+
axis=1,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
# Replace inf values with NaN.
|
| 322 |
+
# Normalization / missing-value handling remains in train_hf.py
|
| 323 |
+
# and YieldDataset.
|
| 324 |
+
out = out.replace(
|
| 325 |
+
[np.inf, -np.inf],
|
| 326 |
+
np.nan,
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# ============================================================
|
| 331 |
+
# Critical safety check
|
| 332 |
+
# ============================================================
|
| 333 |
+
|
| 334 |
+
print()
|
| 335 |
+
print("===================================")
|
| 336 |
+
print("FINAL MODEL DATASET")
|
| 337 |
+
print("===================================")
|
| 338 |
+
|
| 339 |
+
print(
|
| 340 |
+
"Shape:",
|
| 341 |
+
out.shape,
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
print(
|
| 345 |
+
"Years:",
|
| 346 |
+
sorted(out["year"].unique())
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
print(
|
| 350 |
+
"Rows:",
|
| 351 |
+
len(out)
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
assert set(out["year"].unique()) == {
|
| 356 |
+
2013,
|
| 357 |
+
2014,
|
| 358 |
+
2015,
|
| 359 |
+
2016,
|
| 360 |
+
2017,
|
| 361 |
+
2018,
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
assert len(out) == len(df)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
# ============================================================
|
| 369 |
+
# Chronological split
|
| 370 |
+
#
|
| 371 |
+
# Train: 2013-2016
|
| 372 |
+
# Val: 2017
|
| 373 |
+
# Test: 2018
|
| 374 |
+
# ============================================================
|
| 375 |
+
|
| 376 |
+
train_df = out[
|
| 377 |
+
out["year"].isin(TRAIN_YEARS)
|
| 378 |
+
].reset_index(drop=True)
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
val_df = out[
|
| 382 |
+
out["year"] == VAL_YEAR
|
| 383 |
+
].reset_index(drop=True)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
test_df = out[
|
| 387 |
+
out["year"] == TEST_YEAR
|
| 388 |
+
].reset_index(drop=True)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
# ============================================================
|
| 392 |
+
# Split reporting
|
| 393 |
+
# ============================================================
|
| 394 |
+
|
| 395 |
+
print()
|
| 396 |
+
print("===================================")
|
| 397 |
+
print("SPLITS")
|
| 398 |
+
print("===================================")
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
print("\nTRAIN")
|
| 402 |
+
|
| 403 |
+
print(
|
| 404 |
+
train_df[
|
| 405 |
+
"year"
|
| 406 |
+
].value_counts().sort_index()
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
print(
|
| 410 |
+
"Samples:",
|
| 411 |
+
len(train_df),
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
print(
|
| 415 |
+
"Counties:",
|
| 416 |
+
train_df[
|
| 417 |
+
"farm_field"
|
| 418 |
+
].nunique(),
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
print("\nVALIDATION")
|
| 423 |
+
|
| 424 |
+
print(
|
| 425 |
+
val_df[
|
| 426 |
+
"year"
|
| 427 |
+
].value_counts().sort_index()
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
print(
|
| 431 |
+
"Samples:",
|
| 432 |
+
len(val_df),
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
print(
|
| 436 |
+
"Counties:",
|
| 437 |
+
val_df[
|
| 438 |
+
"farm_field"
|
| 439 |
+
].nunique(),
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
print("\nTEST")
|
| 444 |
+
|
| 445 |
+
print(
|
| 446 |
+
test_df[
|
| 447 |
+
"year"
|
| 448 |
+
].value_counts().sort_index()
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
print(
|
| 452 |
+
"Samples:",
|
| 453 |
+
len(test_df),
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
print(
|
| 457 |
+
"Counties:",
|
| 458 |
+
test_df[
|
| 459 |
+
"farm_field"
|
| 460 |
+
].nunique(),
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
# ============================================================
|
| 465 |
+
# Split sanity checks
|
| 466 |
+
# ============================================================
|
| 467 |
+
|
| 468 |
+
assert set(
|
| 469 |
+
train_df["year"].unique()
|
| 470 |
+
) == {
|
| 471 |
+
2013,
|
| 472 |
+
2014,
|
| 473 |
+
2015,
|
| 474 |
+
2016,
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
assert set(
|
| 479 |
+
val_df["year"].unique()
|
| 480 |
+
) == {
|
| 481 |
+
2017,
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
assert set(
|
| 486 |
+
test_df["year"].unique()
|
| 487 |
+
) == {
|
| 488 |
+
2018,
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
assert (
|
| 493 |
+
len(train_df)
|
| 494 |
+
+ len(val_df)
|
| 495 |
+
+ len(test_df)
|
| 496 |
+
== len(out)
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
# No row should appear in more than one split
|
| 501 |
+
assert set(train_df.index).isdisjoint(
|
| 502 |
+
set(range(
|
| 503 |
+
len(train_df),
|
| 504 |
+
len(train_df) + len(val_df)
|
| 505 |
+
))
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
print()
|
| 510 |
+
print("All year/split checks passed.")
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
# ============================================================
|
| 514 |
+
# Weather sanity checks
|
| 515 |
+
# ============================================================
|
| 516 |
+
|
| 517 |
+
weather_vars = list(
|
| 518 |
+
WEATHER_MAP.keys()
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
for weather_var in weather_vars:
|
| 523 |
+
|
| 524 |
+
cols = [
|
| 525 |
+
c
|
| 526 |
+
for c in out.columns
|
| 527 |
+
if c.startswith(
|
| 528 |
+
f"{weather_var}_"
|
| 529 |
+
)
|
| 530 |
+
]
|
| 531 |
+
|
| 532 |
+
assert len(cols) == 52, (
|
| 533 |
+
f"{weather_var}: expected 52 weekly "
|
| 534 |
+
f"columns, found {len(cols)}"
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
print()
|
| 539 |
+
print("Weather variables:")
|
| 540 |
+
print(weather_vars)
|
| 541 |
+
|
| 542 |
+
print(
|
| 543 |
+
"Weeks per weather variable:",
|
| 544 |
+
52,
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
print(
|
| 548 |
+
"Total weather columns:",
|
| 549 |
+
52 * len(weather_vars),
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
# ============================================================
|
| 554 |
+
# Soil sanity checks
|
| 555 |
+
# ============================================================
|
| 556 |
+
|
| 557 |
+
assert len(SOIL_VARS) == 66
|
| 558 |
+
|
| 559 |
+
assert all(
|
| 560 |
+
col in out.columns
|
| 561 |
+
for col in SOIL_VARS
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
print(
|
| 566 |
+
"Number of soil variables:",
|
| 567 |
+
len(SOIL_VARS),
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
# ============================================================
|
| 572 |
+
# Check expected tensor dimensions
|
| 573 |
+
# ============================================================
|
| 574 |
+
|
| 575 |
+
print()
|
| 576 |
+
print("Expected model input dimensions:")
|
| 577 |
+
|
| 578 |
+
print(
|
| 579 |
+
"weather = [52, 6]"
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
print(
|
| 583 |
+
"soil = [66]"
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
# ============================================================
|
| 588 |
+
# Yield summaries
|
| 589 |
+
# ============================================================
|
| 590 |
+
|
| 591 |
+
print()
|
| 592 |
+
print("===================================")
|
| 593 |
+
print("YIELD SUMMARY")
|
| 594 |
+
print("===================================")
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
print("\nTrain:")
|
| 598 |
+
print(
|
| 599 |
+
train_df[
|
| 600 |
+
"yield"
|
| 601 |
+
].describe()
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
print("\nValidation:")
|
| 606 |
+
print(
|
| 607 |
+
val_df[
|
| 608 |
+
"yield"
|
| 609 |
+
].describe()
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
print("\nTest:")
|
| 614 |
+
print(
|
| 615 |
+
test_df[
|
| 616 |
+
"yield"
|
| 617 |
+
].describe()
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
# ============================================================
|
| 622 |
+
# Save HDF5 files
|
| 623 |
+
# ============================================================
|
| 624 |
+
|
| 625 |
+
OUTPUT_DIR.mkdir(
|
| 626 |
+
parents=True,
|
| 627 |
+
exist_ok=True,
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
train_path = OUTPUT_DIR / "train.h5"
|
| 632 |
+
val_path = OUTPUT_DIR / "val.h5"
|
| 633 |
+
test_path = OUTPUT_DIR / "test.h5"
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
train_df.to_hdf(
|
| 637 |
+
train_path,
|
| 638 |
+
key="data",
|
| 639 |
+
mode="w",
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
val_df.to_hdf(
|
| 644 |
+
val_path,
|
| 645 |
+
key="data",
|
| 646 |
+
mode="w",
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
test_df.to_hdf(
|
| 651 |
+
test_path,
|
| 652 |
+
key="data",
|
| 653 |
+
mode="w",
|
| 654 |
+
)
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
print()
|
| 658 |
+
print("===================================")
|
| 659 |
+
print("SAVED")
|
| 660 |
+
print("===================================")
|
| 661 |
+
|
| 662 |
+
print(train_path)
|
| 663 |
+
print(val_path)
|
| 664 |
+
print(test_path)
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
# ============================================================
|
| 668 |
+
# Reload files to verify they were written correctly
|
| 669 |
+
# ============================================================
|
| 670 |
+
|
| 671 |
+
train_check = pd.read_hdf(train_path)
|
| 672 |
+
val_check = pd.read_hdf(val_path)
|
| 673 |
+
test_check = pd.read_hdf(test_path)
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
assert len(train_check) == len(train_df)
|
| 677 |
+
assert len(val_check) == len(val_df)
|
| 678 |
+
assert len(test_check) == len(test_df)
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
assert set(
|
| 682 |
+
train_check["year"].unique()
|
| 683 |
+
) == {
|
| 684 |
+
2013,
|
| 685 |
+
2014,
|
| 686 |
+
2015,
|
| 687 |
+
2016,
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
assert set(
|
| 692 |
+
val_check["year"].unique()
|
| 693 |
+
) == {
|
| 694 |
+
2017,
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
assert set(
|
| 699 |
+
test_check["year"].unique()
|
| 700 |
+
) == {
|
| 701 |
+
2018,
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
print()
|
| 706 |
+
print("HDF5 reload verification passed.")
|
| 707 |
+
|
| 708 |
+
print()
|
| 709 |
+
print("Done.")
|
| 710 |
+
|
training_code/scripts/train_hf.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 5 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 6 |
+
|
| 7 |
+
from tqdm.auto import tqdm
|
| 8 |
+
import time
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
import random
|
| 12 |
+
import pandas as pd
|
| 13 |
+
import torch
|
| 14 |
+
from torch.utils.data import DataLoader
|
| 15 |
+
from transformers import AutoConfig, AutoModel
|
| 16 |
+
|
| 17 |
+
from config.config import TrainConfig
|
| 18 |
+
from data.dataset import YieldDataset
|
| 19 |
+
from data.preprocessing import compute_x_stats, compute_y_stats
|
| 20 |
+
from training.engine import evaluate
|
| 21 |
+
from hf.configuration_yield import YieldConfig
|
| 22 |
+
from hf.auto import register_yield_autoclass
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def parse_list(x):
|
| 26 |
+
return [v.strip() for v in x.split(",") if v.strip()]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def parse_int_list(x):
|
| 30 |
+
return [int(v.strip()) for v in x.split(",") if v.strip()]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_args():
|
| 34 |
+
p = argparse.ArgumentParser()
|
| 35 |
+
p.add_argument("--train_file", required=True)
|
| 36 |
+
p.add_argument("--val_file", default=None)
|
| 37 |
+
p.add_argument("--test_file", default=None)
|
| 38 |
+
p.add_argument("--weather_vars", required=True)
|
| 39 |
+
p.add_argument("--soil_vars", required=True)
|
| 40 |
+
p.add_argument("--crop", default=None)
|
| 41 |
+
p.add_argument("--years", default=None)
|
| 42 |
+
p.add_argument("--train_cutoffs", default="4,8,12,16,22")
|
| 43 |
+
p.add_argument("--eval_cutoffs", default=None)
|
| 44 |
+
p.add_argument("--out_dir", default="outputs/yield_hf_model")
|
| 45 |
+
p.add_argument("--epochs", type=int, default=10)
|
| 46 |
+
p.add_argument("--batch_size", type=int, default=64)
|
| 47 |
+
p.add_argument("--lr", type=float, default=3e-4)
|
| 48 |
+
p.add_argument("--weight_decay", type=float, default=1e-4)
|
| 49 |
+
p.add_argument("--seed", type=int, default=1234)
|
| 50 |
+
p.add_argument(
|
| 51 |
+
"--time_agg",
|
| 52 |
+
default="weekly_cumulative",
|
| 53 |
+
choices=["weekly", "weekly_cumulative"],
|
| 54 |
+
)
|
| 55 |
+
return p.parse_args()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def train_one_epoch_hf(model, loader, optimizer, device, cutoffs):
|
| 59 |
+
model.train()
|
| 60 |
+
|
| 61 |
+
total_loss = 0.0
|
| 62 |
+
n_total = 0
|
| 63 |
+
se_raw = 0.0
|
| 64 |
+
n = 0
|
| 65 |
+
|
| 66 |
+
pbar = tqdm(
|
| 67 |
+
loader,
|
| 68 |
+
desc="Training",
|
| 69 |
+
leave=False,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
for batch in pbar:
|
| 73 |
+
weather = batch["weather"].to(device)
|
| 74 |
+
soil = batch["soil"].to(device)
|
| 75 |
+
crop_id = batch["crop_id"].to(device)
|
| 76 |
+
labels = batch["yield"].to(device)
|
| 77 |
+
|
| 78 |
+
# t = random.choice(cutoffs)
|
| 79 |
+
# t_eff = min(t, weather.size(1))
|
| 80 |
+
|
| 81 |
+
# out = model(
|
| 82 |
+
# weather=weather[:, :t_eff, :],
|
| 83 |
+
# soil=soil,
|
| 84 |
+
# crop_id=crop_id,
|
| 85 |
+
# labels=labels,
|
| 86 |
+
# horizon_idx=t_eff,
|
| 87 |
+
# causal=True,
|
| 88 |
+
# return_sequence=False,
|
| 89 |
+
# )
|
| 90 |
+
|
| 91 |
+
# loss = out.loss
|
| 92 |
+
|
| 93 |
+
losses = []
|
| 94 |
+
preds_for_rmse = []
|
| 95 |
+
|
| 96 |
+
for t in cutoffs:
|
| 97 |
+
t_eff = min(t, weather.size(1))
|
| 98 |
+
|
| 99 |
+
out = model(
|
| 100 |
+
weather=weather[:, :t_eff, :],
|
| 101 |
+
soil=soil,
|
| 102 |
+
crop_id=crop_id,
|
| 103 |
+
labels=labels,
|
| 104 |
+
horizon_idx=t_eff,
|
| 105 |
+
causal=True,
|
| 106 |
+
return_sequence=False,
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
losses.append(out.loss)
|
| 110 |
+
preds_for_rmse.append(out.predictions)
|
| 111 |
+
|
| 112 |
+
loss = torch.stack(losses).mean()
|
| 113 |
+
|
| 114 |
+
optimizer.zero_grad(set_to_none=True)
|
| 115 |
+
loss.backward()
|
| 116 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
|
| 117 |
+
optimizer.step()
|
| 118 |
+
|
| 119 |
+
total_loss += loss.item() * labels.size(0)
|
| 120 |
+
n_total += labels.size(0)
|
| 121 |
+
|
| 122 |
+
with torch.no_grad():
|
| 123 |
+
# pred = out.predictions
|
| 124 |
+
# se_raw += ((pred - labels) ** 2).sum().item()
|
| 125 |
+
# n += labels.numel()
|
| 126 |
+
pred = torch.stack(preds_for_rmse, dim=0).mean(dim=0)
|
| 127 |
+
se_raw += ((pred - labels) ** 2).sum().item()
|
| 128 |
+
n += labels.numel()
|
| 129 |
+
|
| 130 |
+
rmse = (se_raw / max(n, 1)) ** 0.5
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
"loss": total_loss / max(n_total, 1),
|
| 134 |
+
"rmse": rmse,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def save_json(path, obj):
|
| 139 |
+
path = Path(path)
|
| 140 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 141 |
+
with open(path, "w") as f:
|
| 142 |
+
json.dump(obj, f, indent=2, default=str)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main():
|
| 146 |
+
print("Starting training script...")
|
| 147 |
+
args = parse_args()
|
| 148 |
+
register_yield_autoclass()
|
| 149 |
+
|
| 150 |
+
cfg = TrainConfig(
|
| 151 |
+
mode="train_eval",
|
| 152 |
+
train_file=Path(args.train_file),
|
| 153 |
+
val_file=Path(args.val_file) if args.val_file else None,
|
| 154 |
+
test_file=Path(args.test_file) if args.test_file else None,
|
| 155 |
+
weather_vars=parse_list(args.weather_vars),
|
| 156 |
+
soil_vars=parse_list(args.soil_vars),
|
| 157 |
+
crop=args.crop,
|
| 158 |
+
years=args.years,
|
| 159 |
+
train_cutoffs=parse_int_list(args.train_cutoffs),
|
| 160 |
+
eval_cutoffs=parse_int_list(args.eval_cutoffs) if args.eval_cutoffs else parse_int_list(args.train_cutoffs),
|
| 161 |
+
out_dir=Path(args.out_dir),
|
| 162 |
+
epochs=args.epochs,
|
| 163 |
+
batch_size=args.batch_size,
|
| 164 |
+
lr=args.lr,
|
| 165 |
+
weight_decay=args.weight_decay,
|
| 166 |
+
seed=args.seed,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
cfg.validate()
|
| 170 |
+
cfg.save()
|
| 171 |
+
|
| 172 |
+
torch.manual_seed(cfg.seed)
|
| 173 |
+
random.seed(cfg.seed)
|
| 174 |
+
|
| 175 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 176 |
+
|
| 177 |
+
train_ds = YieldDataset(
|
| 178 |
+
data_file=cfg.train_file,
|
| 179 |
+
weather_vars=cfg.weather_vars,
|
| 180 |
+
soil_vars=cfg.soil_vars,
|
| 181 |
+
split="train" if cfg.val_file is None else "all",
|
| 182 |
+
seed=cfg.seed,
|
| 183 |
+
crop=cfg.crop,
|
| 184 |
+
years=cfg.years,
|
| 185 |
+
split_strategy=cfg.split_strategy,
|
| 186 |
+
val_split=cfg.val_split,
|
| 187 |
+
test_split=cfg.test_split,
|
| 188 |
+
time_agg=args.time_agg,
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
if cfg.val_file:
|
| 192 |
+
val_ds = YieldDataset(
|
| 193 |
+
data_file=cfg.val_file,
|
| 194 |
+
weather_vars=cfg.weather_vars,
|
| 195 |
+
soil_vars=cfg.soil_vars,
|
| 196 |
+
split="all",
|
| 197 |
+
seed=cfg.seed,
|
| 198 |
+
crop=cfg.crop,
|
| 199 |
+
years=cfg.years,
|
| 200 |
+
time_agg=args.time_agg,
|
| 201 |
+
)
|
| 202 |
+
else:
|
| 203 |
+
val_ds = YieldDataset(
|
| 204 |
+
data_file=cfg.train_file,
|
| 205 |
+
weather_vars=cfg.weather_vars,
|
| 206 |
+
soil_vars=cfg.soil_vars,
|
| 207 |
+
split="val",
|
| 208 |
+
seed=cfg.seed,
|
| 209 |
+
crop=cfg.crop,
|
| 210 |
+
years=cfg.years,
|
| 211 |
+
split_strategy=cfg.split_strategy,
|
| 212 |
+
val_split=cfg.val_split,
|
| 213 |
+
test_split=cfg.test_split,
|
| 214 |
+
time_agg=args.time_agg,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
w_mean, w_std, s_mean, s_std = compute_x_stats(train_ds, seed=cfg.seed)
|
| 218 |
+
y_mean, y_std = compute_y_stats(train_ds)
|
| 219 |
+
|
| 220 |
+
train_ds.set_normalization(w_mean, w_std, s_mean, s_std)
|
| 221 |
+
val_ds.set_normalization(w_mean, w_std, s_mean, s_std)
|
| 222 |
+
|
| 223 |
+
b0 = train_ds[0]
|
| 224 |
+
K, W = b0["weather"].shape
|
| 225 |
+
S = b0["soil"].shape[0]
|
| 226 |
+
|
| 227 |
+
hf_config = YieldConfig(
|
| 228 |
+
weather_vars=cfg.weather_vars,
|
| 229 |
+
soil_vars=cfg.soil_vars,
|
| 230 |
+
w_mean=w_mean.tolist(),
|
| 231 |
+
w_std=w_std.tolist(),
|
| 232 |
+
s_mean=s_mean.tolist(),
|
| 233 |
+
s_std=s_std.tolist(),
|
| 234 |
+
y_mean=float(y_mean),
|
| 235 |
+
y_std=float(y_std),
|
| 236 |
+
K=int(K),
|
| 237 |
+
W=int(W),
|
| 238 |
+
S=int(S),
|
| 239 |
+
train_cutoffs=cfg.train_cutoffs,
|
| 240 |
+
eval_cutoffs=cfg.eval_cutoffs,
|
| 241 |
+
d_model=cfg.d_model,
|
| 242 |
+
nhead=cfg.nhead,
|
| 243 |
+
num_layers=cfg.num_layers,
|
| 244 |
+
dim_ff=cfg.dim_ff,
|
| 245 |
+
dropout=cfg.dropout,
|
| 246 |
+
pool=cfg.pool,
|
| 247 |
+
use_crop=cfg.use_crop,
|
| 248 |
+
crop_emb_dim=cfg.crop_emb_dim,
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
model = AutoModel.from_config(hf_config).to(device)
|
| 252 |
+
|
| 253 |
+
optimizer = torch.optim.AdamW(
|
| 254 |
+
model.parameters(),
|
| 255 |
+
lr=cfg.lr,
|
| 256 |
+
weight_decay=cfg.weight_decay,
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
train_loader = DataLoader(train_ds, batch_size=cfg.batch_size, shuffle=True, num_workers=0)
|
| 260 |
+
val_loader = DataLoader(val_ds, batch_size=cfg.batch_size, shuffle=False, num_workers=0)
|
| 261 |
+
|
| 262 |
+
best_val = float("inf")
|
| 263 |
+
best_metrics = None
|
| 264 |
+
patience = 5
|
| 265 |
+
bad_epochs = 0
|
| 266 |
+
|
| 267 |
+
for epoch in range(1, cfg.epochs + 1):
|
| 268 |
+
epoch_start = time.time()
|
| 269 |
+
|
| 270 |
+
train_metrics = train_one_epoch_hf(
|
| 271 |
+
model=model,
|
| 272 |
+
loader=train_loader,
|
| 273 |
+
optimizer=optimizer,
|
| 274 |
+
device=device,
|
| 275 |
+
cutoffs=cfg.train_cutoffs,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
val_metrics_by_t, _ = evaluate(
|
| 279 |
+
model=model,
|
| 280 |
+
loader=val_loader,
|
| 281 |
+
device=device,
|
| 282 |
+
y_mean=y_mean,
|
| 283 |
+
y_std=y_std,
|
| 284 |
+
cutoffs=cfg.eval_cutoffs,
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
epoch_time = time.time() - epoch_start
|
| 288 |
+
remaining = (cfg.epochs - epoch) * epoch_time
|
| 289 |
+
|
| 290 |
+
print(
|
| 291 |
+
f"\nEpoch {epoch}/{cfg.epochs}"
|
| 292 |
+
f" | time={epoch_time/60:.1f} min"
|
| 293 |
+
f" | ETA={remaining/60:.1f} min"
|
| 294 |
+
)
|
| 295 |
+
val_rmse = sum(m["rmse"] for m in val_metrics_by_t.values()) / len(val_metrics_by_t)
|
| 296 |
+
|
| 297 |
+
print(
|
| 298 |
+
f"Epoch {epoch:03d} | "
|
| 299 |
+
f"train_rmse={train_metrics['rmse']:.4f} | "
|
| 300 |
+
f"val_rmse={val_rmse:.4f}"
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
print("\nValidation Metrics")
|
| 304 |
+
print("-" * 42)
|
| 305 |
+
print(f"{'Cutoff':>8} {'RMSE':>10} {'Bias':>10} {'R²':>10}")
|
| 306 |
+
|
| 307 |
+
for cutoff in cfg.eval_cutoffs:
|
| 308 |
+
m = val_metrics_by_t[cutoff]
|
| 309 |
+
print(
|
| 310 |
+
f"{cutoff:>8}"
|
| 311 |
+
f"{m['rmse']:>10.3f}"
|
| 312 |
+
f"{m['bias']:>10.3f}"
|
| 313 |
+
f"{m['r2']:>10.3f}"
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
if val_rmse < best_val:
|
| 317 |
+
bad_epochs = 0
|
| 318 |
+
best_val = val_rmse
|
| 319 |
+
best_metrics = {
|
| 320 |
+
"epoch": epoch,
|
| 321 |
+
"val_rmse": val_rmse,
|
| 322 |
+
"val_by_cutoff": val_metrics_by_t,
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
model.save_pretrained(cfg.out_dir)
|
| 326 |
+
hf_config.save_pretrained(cfg.out_dir)
|
| 327 |
+
save_json(cfg.out_dir / "metrics.json", best_metrics)
|
| 328 |
+
|
| 329 |
+
print(f"Saved best HF model to {cfg.out_dir}")
|
| 330 |
+
else:
|
| 331 |
+
bad_epochs += 1
|
| 332 |
+
print(f"No improvement for {bad_epochs}/{patience} epochs.")
|
| 333 |
+
|
| 334 |
+
if bad_epochs >= patience:
|
| 335 |
+
print("Early stopping.")
|
| 336 |
+
break
|
| 337 |
+
|
| 338 |
+
print("Best metrics:")
|
| 339 |
+
print(json.dumps(best_metrics, indent=2))
|
| 340 |
+
|
| 341 |
+
if cfg.test_file is not None:
|
| 342 |
+
print("\nRunning final test using best HF model...")
|
| 343 |
+
|
| 344 |
+
best_model = AutoModel.from_pretrained(cfg.out_dir).to(device)
|
| 345 |
+
best_model.eval()
|
| 346 |
+
|
| 347 |
+
test_ds = YieldDataset(
|
| 348 |
+
data_file=cfg.test_file,
|
| 349 |
+
weather_vars=cfg.weather_vars,
|
| 350 |
+
soil_vars=cfg.soil_vars,
|
| 351 |
+
split="all",
|
| 352 |
+
seed=cfg.seed,
|
| 353 |
+
crop=cfg.crop,
|
| 354 |
+
years=cfg.years,
|
| 355 |
+
time_agg=args.time_agg,
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
test_ds.set_normalization(w_mean, w_std, s_mean, s_std)
|
| 359 |
+
|
| 360 |
+
test_loader = DataLoader(test_ds, batch_size=cfg.batch_size, shuffle=False, num_workers=0)
|
| 361 |
+
|
| 362 |
+
test_metrics, test_rows = evaluate(
|
| 363 |
+
model=best_model,
|
| 364 |
+
loader=test_loader,
|
| 365 |
+
device=device,
|
| 366 |
+
y_mean=y_mean,
|
| 367 |
+
y_std=y_std,
|
| 368 |
+
cutoffs=cfg.eval_cutoffs,
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
print("Final test metrics:")
|
| 372 |
+
print(json.dumps(test_metrics, indent=2))
|
| 373 |
+
|
| 374 |
+
save_json(cfg.out_dir / "test_metrics.json", test_metrics)
|
| 375 |
+
pd.DataFrame(test_rows).to_csv(cfg.out_dir / "test_predictions.csv", index=False)
|
| 376 |
+
|
| 377 |
+
print(f"Saved final test outputs to {cfg.out_dir}")
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
if __name__ == "__main__":
|
| 381 |
+
main()
|
training_code/training.slurm
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --job-name=yield-cornbelt
|
| 3 |
+
#SBATCH --mail-type=ALL
|
| 4 |
+
#SBATCH --mail-user=sridhar.86@buckeyemail.osu.edu
|
| 5 |
+
#SBATCH --time=08:00:00
|
| 6 |
+
#SBATCH --account=PAS2699
|
| 7 |
+
#SBATCH --output=yield-training.%j.out
|
| 8 |
+
#SBATCH --mem=100G
|
| 9 |
+
#SBATCH --gpus-per-node=1
|
| 10 |
+
|
| 11 |
+
module load python/3.12
|
| 12 |
+
|
| 13 |
+
source activate yield_hf
|
| 14 |
+
|
| 15 |
+
python scripts/train_hf.py \
|
| 16 |
+
--train_file data/cornbelt/train.h5 \
|
| 17 |
+
--val_file data/cornbelt/val.h5 \
|
| 18 |
+
--test_file data/cornbelt/test.h5 \
|
| 19 |
+
--weather_vars prcp,srad,swe,tmax,tmin,vp \
|
| 20 |
+
--soil_vars bdod_mean_0-5cm,bdod_mean_5-15cm,bdod_mean_15-30cm,bdod_mean_30-60cm,bdod_mean_60-100cm,bdod_mean_100-200cm,cec_mean_0-5cm,cec_mean_5-15cm,cec_mean_15-30cm,cec_mean_30-60cm,cec_mean_60-100cm,cec_mean_100-200cm,cfvo_mean_0-5cm,cfvo_mean_5-15cm,cfvo_mean_15-30cm,cfvo_mean_30-60cm,cfvo_mean_60-100cm,cfvo_mean_100-200cm,clay_mean_0-5cm,clay_mean_5-15cm,clay_mean_15-30cm,clay_mean_30-60cm,clay_mean_60-100cm,clay_mean_100-200cm,nitrogen_mean_0-5cm,nitrogen_mean_5-15cm,nitrogen_mean_15-30cm,nitrogen_mean_30-60cm,nitrogen_mean_60-100cm,nitrogen_mean_100-200cm,ocd_mean_0-5cm,ocd_mean_5-15cm,ocd_mean_15-30cm,ocd_mean_30-60cm,ocd_mean_60-100cm,ocd_mean_100-200cm,ocs_mean_0-5cm,ocs_mean_5-15cm,ocs_mean_15-30cm,ocs_mean_30-60cm,ocs_mean_60-100cm,ocs_mean_100-200cm,phh2o_mean_0-5cm,phh2o_mean_5-15cm,phh2o_mean_15-30cm,phh2o_mean_30-60cm,phh2o_mean_60-100cm,phh2o_mean_100-200cm,sand_mean_0-5cm,sand_mean_5-15cm,sand_mean_15-30cm,sand_mean_30-60cm,sand_mean_60-100cm,sand_mean_100-200cm,silt_mean_0-5cm,silt_mean_5-15cm,silt_mean_15-30cm,silt_mean_30-60cm,silt_mean_60-100cm,silt_mean_100-200cm,soc_mean_0-5cm,soc_mean_5-15cm,soc_mean_15-30cm,soc_mean_30-60cm,soc_mean_60-100cm,soc_mean_100-200cm \
|
| 21 |
+
--crop corn \
|
| 22 |
+
--time_agg weekly \
|
| 23 |
+
--train_cutoffs 20,24,28,32,36,40,44,48,52 \
|
| 24 |
+
--eval_cutoffs 20,24,28,32,36,40,44,48,52 \
|
| 25 |
+
--epochs 30 \
|
| 26 |
+
--lr 3e-5 \
|
| 27 |
+
--batch_size 32 \
|
| 28 |
+
--out_dir checkpoints
|
training_code/training/__init__.py
ADDED
|
File without changes
|
training_code/training/engine.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import random
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
import numpy as np
|
| 6 |
+
from sklearn.metrics import r2_score
|
| 7 |
+
from tqdm.auto import tqdm
|
| 8 |
+
|
| 9 |
+
def train_one_epoch(model, loader, optimizer, device, y_mean, y_std, cutoffs):
|
| 10 |
+
model.train()
|
| 11 |
+
total_loss = 0.0
|
| 12 |
+
n_total = 0
|
| 13 |
+
|
| 14 |
+
se_norm = 0.0
|
| 15 |
+
se_raw = 0.0
|
| 16 |
+
n = 0
|
| 17 |
+
|
| 18 |
+
for batch in loader:
|
| 19 |
+
weather = batch["weather"].to(device)
|
| 20 |
+
soil = batch["soil"].to(device)
|
| 21 |
+
crop_id = batch["crop_id"].to(device)
|
| 22 |
+
y = batch["yield"].to(device)
|
| 23 |
+
|
| 24 |
+
t = random.choice(cutoffs)
|
| 25 |
+
t_eff = min(t, weather.size(1))
|
| 26 |
+
|
| 27 |
+
y_log = torch.log1p(torch.clamp(y, min=0.0))
|
| 28 |
+
y_norm = (y_log - y_mean) / y_std
|
| 29 |
+
|
| 30 |
+
yhat_norm = model(
|
| 31 |
+
weather[:, :t_eff, :],
|
| 32 |
+
soil,
|
| 33 |
+
crop_id,
|
| 34 |
+
horizon_idx=t_eff,
|
| 35 |
+
causal=True,
|
| 36 |
+
return_sequence=False,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
loss = F.mse_loss(yhat_norm, y_norm)
|
| 40 |
+
|
| 41 |
+
optimizer.zero_grad(set_to_none=True)
|
| 42 |
+
loss.backward()
|
| 43 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
|
| 44 |
+
optimizer.step()
|
| 45 |
+
|
| 46 |
+
total_loss += loss.item() * y.size(0)
|
| 47 |
+
n_total += y.size(0)
|
| 48 |
+
|
| 49 |
+
with torch.no_grad():
|
| 50 |
+
yhat = torch.expm1(yhat_norm * y_std + y_mean)
|
| 51 |
+
se_norm += ((yhat_norm - y_norm) ** 2).sum().item()
|
| 52 |
+
se_raw += ((yhat - y) ** 2).sum().item()
|
| 53 |
+
n += y.numel()
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"loss_norm_mse": total_loss / max(n_total, 1),
|
| 57 |
+
"norm_rmse": math.sqrt(se_norm / max(n, 1)),
|
| 58 |
+
"rmse": math.sqrt(se_raw / max(n, 1)),
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
@torch.no_grad()
|
| 62 |
+
def evaluate(model, loader, device, y_mean, y_std, cutoffs):
|
| 63 |
+
model.eval()
|
| 64 |
+
results = {}
|
| 65 |
+
all_rows = []
|
| 66 |
+
|
| 67 |
+
for t in sorted(cutoffs):
|
| 68 |
+
y_true_all = []
|
| 69 |
+
y_pred_all = []
|
| 70 |
+
|
| 71 |
+
pbar = tqdm(
|
| 72 |
+
loader,
|
| 73 |
+
desc="Validation",
|
| 74 |
+
leave=False,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
for batch in pbar:
|
| 78 |
+
weather = batch["weather"].to(device)
|
| 79 |
+
soil = batch["soil"].to(device)
|
| 80 |
+
crop_id = batch["crop_id"].to(device)
|
| 81 |
+
y = batch["yield"].to(device)
|
| 82 |
+
|
| 83 |
+
t_eff = min(t, weather.size(1))
|
| 84 |
+
|
| 85 |
+
out = model(
|
| 86 |
+
weather=weather[:, :t_eff, :],
|
| 87 |
+
soil=soil,
|
| 88 |
+
crop_id=crop_id,
|
| 89 |
+
horizon_idx=t_eff,
|
| 90 |
+
causal=True,
|
| 91 |
+
return_sequence=False,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
if hasattr(out, "predictions"):
|
| 95 |
+
yhat = out.predictions
|
| 96 |
+
else:
|
| 97 |
+
#yhat = torch.expm1(out * y_std + y_mean)
|
| 98 |
+
yhat = out * y_std + y_mean
|
| 99 |
+
|
| 100 |
+
y_true_all.extend(y.detach().cpu().numpy().tolist())
|
| 101 |
+
y_pred_all.extend(yhat.detach().cpu().numpy().tolist())
|
| 102 |
+
|
| 103 |
+
y_true = np.asarray(y_true_all, dtype=np.float32)
|
| 104 |
+
y_pred = np.asarray(y_pred_all, dtype=np.float32)
|
| 105 |
+
err = y_pred - y_true
|
| 106 |
+
|
| 107 |
+
results[int(t)] = {
|
| 108 |
+
"rmse": float(np.sqrt(np.mean(err ** 2))),
|
| 109 |
+
"bias": float(np.mean(err)),
|
| 110 |
+
"r2": float(r2_score(y_true, y_pred)) if len(y_true) > 1 else float("nan"),
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
for i, (yt, yp) in enumerate(zip(y_true, y_pred)):
|
| 114 |
+
all_rows.append({
|
| 115 |
+
"cutoff": int(t),
|
| 116 |
+
"sample_idx": i,
|
| 117 |
+
"y_true": float(yt),
|
| 118 |
+
"y_pred": float(yp),
|
| 119 |
+
"error": float(yp - yt),
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
return results, all_rows
|