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)# pip install -U transformers accelerate # 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
Delete pipeline_yield.py
Browse files- pipeline_yield.py +0 -278
pipeline_yield.py
DELETED
|
@@ -1,278 +0,0 @@
|
|
| 1 |
-
import sys
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
|
| 4 |
-
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 5 |
-
if str(PROJECT_ROOT) not in sys.path:
|
| 6 |
-
sys.path.insert(0, str(PROJECT_ROOT))
|
| 7 |
-
|
| 8 |
-
import numpy as np
|
| 9 |
-
import torch
|
| 10 |
-
from transformers import Pipeline
|
| 11 |
-
import math
|
| 12 |
-
|
| 13 |
-
DEFAULT_WEATHER_AGG_RULES = {
|
| 14 |
-
"dayl": "mean",
|
| 15 |
-
"prcp": "sum",
|
| 16 |
-
"srad": "mean",
|
| 17 |
-
"tmax": "mean",
|
| 18 |
-
"tmin": "mean",
|
| 19 |
-
"vp": "mean",
|
| 20 |
-
"tmean": "mean",
|
| 21 |
-
"gdd": "sum",
|
| 22 |
-
"precip_3day_avg_perday": "mean",
|
| 23 |
-
"precip_7day_avg_perday": "mean",
|
| 24 |
-
"precip_14day_avg_perday": "mean",
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
def daily_to_cumulative_weekly(
|
| 28 |
-
daily: np.ndarray,
|
| 29 |
-
agg: str,
|
| 30 |
-
week_len: int = 7,
|
| 31 |
-
) -> np.ndarray:
|
| 32 |
-
daily = daily.astype(np.float32)
|
| 33 |
-
T = daily.shape[0]
|
| 34 |
-
K = int(math.ceil(T / week_len))
|
| 35 |
-
|
| 36 |
-
weekly = np.zeros(K, dtype=np.float32)
|
| 37 |
-
cumulative = np.zeros(K, dtype=np.float32)
|
| 38 |
-
|
| 39 |
-
for w in range(K):
|
| 40 |
-
s = w * week_len
|
| 41 |
-
e = min((w + 1) * week_len, T)
|
| 42 |
-
|
| 43 |
-
week_chunk = daily[s:e]
|
| 44 |
-
cumulative_chunk = daily[:e]
|
| 45 |
-
|
| 46 |
-
if agg == "sum":
|
| 47 |
-
weekly[w] = np.nansum(week_chunk)
|
| 48 |
-
cumulative[w] = np.nansum(cumulative_chunk)
|
| 49 |
-
else:
|
| 50 |
-
weekly[w] = np.nanmean(week_chunk)
|
| 51 |
-
cumulative[w] = np.nanmean(cumulative_chunk)
|
| 52 |
-
|
| 53 |
-
return np.stack([weekly, cumulative], axis=1).astype(np.float32)
|
| 54 |
-
|
| 55 |
-
class YieldEstimationPipeline(Pipeline):
|
| 56 |
-
"""
|
| 57 |
-
Pipeline API for yield prediction using weather + soil inputs.
|
| 58 |
-
|
| 59 |
-
Supports:
|
| 60 |
-
- old models: one feature per weather variable, W = len(weather_vars)
|
| 61 |
-
- new models: weekly + cumulative per weather variable, W = 2 * len(weather_vars)
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
def _sanitize_parameters(self, cutoff=None, weather_format=None, **kwargs):
|
| 65 |
-
preprocess_kwargs = {}
|
| 66 |
-
|
| 67 |
-
if cutoff is not None:
|
| 68 |
-
preprocess_kwargs["cutoff"] = cutoff
|
| 69 |
-
|
| 70 |
-
if weather_format is not None:
|
| 71 |
-
preprocess_kwargs["weather_format"] = weather_format
|
| 72 |
-
|
| 73 |
-
return preprocess_kwargs, {}, {}
|
| 74 |
-
|
| 75 |
-
def preprocess(self, inputs, cutoff=None, weather_format=None):
|
| 76 |
-
cfg = self.model.config
|
| 77 |
-
|
| 78 |
-
if not isinstance(inputs, dict):
|
| 79 |
-
raise ValueError("Input must be a dictionary.")
|
| 80 |
-
|
| 81 |
-
if "weather" not in inputs:
|
| 82 |
-
raise ValueError("Input must contain a 'weather' dictionary.")
|
| 83 |
-
|
| 84 |
-
if "soil" not in inputs:
|
| 85 |
-
raise ValueError("Input must contain a 'soil' dictionary.")
|
| 86 |
-
|
| 87 |
-
requested_cutoff = inputs.get("cutoff", cutoff)
|
| 88 |
-
weather_format = inputs.get(
|
| 89 |
-
"weather_format",
|
| 90 |
-
weather_format if weather_format is not None else "weekly_cumulative",
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
-
n_weather_vars = len(cfg.weather_vars)
|
| 94 |
-
model_w_dim = int(cfg.W)
|
| 95 |
-
|
| 96 |
-
use_weekly_and_cumulative = model_w_dim == 2 * n_weather_vars
|
| 97 |
-
use_cumulative_only = model_w_dim == n_weather_vars
|
| 98 |
-
|
| 99 |
-
if not (use_weekly_and_cumulative or use_cumulative_only):
|
| 100 |
-
raise ValueError(
|
| 101 |
-
f"Unsupported weather dimension. Model has W={model_w_dim}, "
|
| 102 |
-
f"but len(weather_vars)={n_weather_vars}. Expected W={n_weather_vars} "
|
| 103 |
-
f"or W={2 * n_weather_vars}."
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
weather_cols = []
|
| 107 |
-
|
| 108 |
-
for v in cfg.weather_vars:
|
| 109 |
-
if v not in inputs["weather"]:
|
| 110 |
-
raise ValueError(f"Missing weather variable: {v}")
|
| 111 |
-
|
| 112 |
-
arr = np.asarray(inputs["weather"][v], dtype=np.float32)
|
| 113 |
-
|
| 114 |
-
if weather_format == "daily":
|
| 115 |
-
agg = DEFAULT_WEATHER_AGG_RULES.get(v, "mean")
|
| 116 |
-
arr = daily_to_cumulative_weekly(arr, agg=agg, week_len=7)
|
| 117 |
-
|
| 118 |
-
if arr.ndim != 2 or arr.shape[1] != 2:
|
| 119 |
-
raise ValueError(
|
| 120 |
-
f"daily_to_cumulative_weekly for '{v}' should return shape [K, 2], "
|
| 121 |
-
f"but got {arr.shape}."
|
| 122 |
-
)
|
| 123 |
-
|
| 124 |
-
if use_weekly_and_cumulative:
|
| 125 |
-
weather_cols.append(arr[:, 0]) # weekly
|
| 126 |
-
weather_cols.append(arr[:, 1]) # cumulative
|
| 127 |
-
else:
|
| 128 |
-
weather_cols.append(arr[:, 1]) # cumulative only
|
| 129 |
-
|
| 130 |
-
elif weather_format == "weekly_cumulative":
|
| 131 |
-
arr = np.asarray(arr, dtype=np.float32)
|
| 132 |
-
|
| 133 |
-
if use_cumulative_only:
|
| 134 |
-
if arr.ndim != 1:
|
| 135 |
-
raise ValueError(
|
| 136 |
-
f"For W={model_w_dim}, weather variable '{v}' must be 1D, "
|
| 137 |
-
f"but got shape {arr.shape}."
|
| 138 |
-
)
|
| 139 |
-
weather_cols.append(arr)
|
| 140 |
-
|
| 141 |
-
else:
|
| 142 |
-
# For W=22 models, weekly_cumulative input must provide either:
|
| 143 |
-
# 1. a [K, 2] array for each variable, or
|
| 144 |
-
# 2. separate keys: var_weekly and var_cumulative.
|
| 145 |
-
if arr.ndim == 2 and arr.shape[1] == 2:
|
| 146 |
-
weather_cols.append(arr[:, 0])
|
| 147 |
-
weather_cols.append(arr[:, 1])
|
| 148 |
-
else:
|
| 149 |
-
weekly_key = f"{v}_weekly"
|
| 150 |
-
cumulative_key = f"{v}_cumulative"
|
| 151 |
-
|
| 152 |
-
if weekly_key not in inputs["weather"] or cumulative_key not in inputs["weather"]:
|
| 153 |
-
raise ValueError(
|
| 154 |
-
f"Model expects weekly + cumulative features for '{v}'. "
|
| 155 |
-
f"For weather_format='weekly_cumulative', provide either "
|
| 156 |
-
f"weather['{v}'] as shape [K, 2], or provide "
|
| 157 |
-
f"weather['{weekly_key}'] and weather['{cumulative_key}']."
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
weekly = np.asarray(inputs["weather"][weekly_key], dtype=np.float32)
|
| 161 |
-
cumulative = np.asarray(inputs["weather"][cumulative_key], dtype=np.float32)
|
| 162 |
-
|
| 163 |
-
if weekly.ndim != 1 or cumulative.ndim != 1:
|
| 164 |
-
raise ValueError(
|
| 165 |
-
f"{weekly_key} and {cumulative_key} must both be 1D."
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
weather_cols.append(weekly)
|
| 169 |
-
weather_cols.append(cumulative)
|
| 170 |
-
|
| 171 |
-
else:
|
| 172 |
-
raise ValueError(
|
| 173 |
-
"weather_format must be either 'daily' or 'weekly_cumulative'."
|
| 174 |
-
)
|
| 175 |
-
|
| 176 |
-
lengths = [len(x) for x in weather_cols]
|
| 177 |
-
if len(set(lengths)) != 1:
|
| 178 |
-
raise ValueError(
|
| 179 |
-
f"Weather variable lengths do not match after preprocessing: {lengths}"
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
weather = np.stack(weather_cols, axis=1).astype(np.float32)
|
| 183 |
-
|
| 184 |
-
if requested_cutoff is None:
|
| 185 |
-
cutoff = weather.shape[0]
|
| 186 |
-
else:
|
| 187 |
-
cutoff = int(requested_cutoff)
|
| 188 |
-
|
| 189 |
-
t_eff = min(cutoff, weather.shape[0])
|
| 190 |
-
|
| 191 |
-
if weather.shape[1] != model_w_dim:
|
| 192 |
-
raise ValueError(
|
| 193 |
-
f"Processed weather has shape {weather.shape}, but model expects W={model_w_dim}."
|
| 194 |
-
)
|
| 195 |
-
|
| 196 |
-
soil = []
|
| 197 |
-
for v in cfg.soil_vars:
|
| 198 |
-
if v not in inputs["soil"]:
|
| 199 |
-
raise ValueError(f"Missing soil variable: {v}")
|
| 200 |
-
soil.append(float(inputs["soil"][v]))
|
| 201 |
-
|
| 202 |
-
soil = np.asarray(soil, dtype=np.float32)
|
| 203 |
-
|
| 204 |
-
w_mean = np.asarray(cfg.w_mean, dtype=np.float32)
|
| 205 |
-
w_std = np.asarray(cfg.w_std, dtype=np.float32)
|
| 206 |
-
s_mean = np.asarray(cfg.s_mean, dtype=np.float32)
|
| 207 |
-
s_std = np.asarray(cfg.s_std, dtype=np.float32)
|
| 208 |
-
|
| 209 |
-
if len(w_mean) != weather.shape[1]:
|
| 210 |
-
raise ValueError(
|
| 211 |
-
f"Normalization mismatch: len(w_mean)={len(w_mean)}, "
|
| 212 |
-
f"but weather feature dimension={weather.shape[1]}."
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
weather = np.where(np.isnan(weather), w_mean[None, :], weather)
|
| 216 |
-
weather = (weather - w_mean[None, :]) / w_std[None, :]
|
| 217 |
-
|
| 218 |
-
soil = np.where(np.isnan(soil), s_mean, soil)
|
| 219 |
-
soil = (soil - s_mean) / s_std
|
| 220 |
-
|
| 221 |
-
crop_map = {
|
| 222 |
-
"corn": 0,
|
| 223 |
-
"maize": 0,
|
| 224 |
-
"soybean": 1,
|
| 225 |
-
"soy": 1,
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
crop = str(inputs.get("crop", "corn")).strip().lower()
|
| 229 |
-
crop_id = crop_map.get(crop, 0)
|
| 230 |
-
|
| 231 |
-
return {
|
| 232 |
-
"weather": torch.from_numpy(weather[:t_eff]).unsqueeze(0),
|
| 233 |
-
"soil": torch.from_numpy(soil).unsqueeze(0),
|
| 234 |
-
"crop_id": torch.tensor([crop_id], dtype=torch.long),
|
| 235 |
-
"horizon_idx": t_eff,
|
| 236 |
-
"cutoff": cutoff,
|
| 237 |
-
"effective_cutoff": t_eff,
|
| 238 |
-
"crop": crop,
|
| 239 |
-
"weather_format": weather_format,
|
| 240 |
-
"weather_shape": list(weather[:t_eff].shape),
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
def _forward(self, model_inputs):
|
| 244 |
-
cutoff = model_inputs.pop("cutoff")
|
| 245 |
-
effective_cutoff = model_inputs.pop("effective_cutoff")
|
| 246 |
-
crop = model_inputs.pop("crop")
|
| 247 |
-
weather_format = model_inputs.pop("weather_format")
|
| 248 |
-
weather_shape = model_inputs.pop("weather_shape")
|
| 249 |
-
|
| 250 |
-
outputs = self.model(
|
| 251 |
-
weather=model_inputs["weather"],
|
| 252 |
-
soil=model_inputs["soil"],
|
| 253 |
-
crop_id=model_inputs["crop_id"],
|
| 254 |
-
horizon_idx=model_inputs["horizon_idx"],
|
| 255 |
-
causal=True,
|
| 256 |
-
return_sequence=False,
|
| 257 |
-
)
|
| 258 |
-
|
| 259 |
-
return {
|
| 260 |
-
"outputs": outputs,
|
| 261 |
-
"cutoff": cutoff,
|
| 262 |
-
"effective_cutoff": effective_cutoff,
|
| 263 |
-
"crop": crop,
|
| 264 |
-
"weather_format": weather_format,
|
| 265 |
-
"weather_shape": weather_shape,
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
def postprocess(self, model_outputs):
|
| 269 |
-
pred = float(model_outputs["outputs"].predictions.detach().cpu().item())
|
| 270 |
-
|
| 271 |
-
return {
|
| 272 |
-
"predicted_yield": pred,
|
| 273 |
-
"cutoff": int(model_outputs["cutoff"]),
|
| 274 |
-
"effective_cutoff": int(model_outputs["effective_cutoff"]),
|
| 275 |
-
"crop": model_outputs["crop"],
|
| 276 |
-
"weather_format": model_outputs["weather_format"],
|
| 277 |
-
"weather_shape": model_outputs["weather_shape"],
|
| 278 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|