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 10 files
Browse files- README.md +277 -0
- config.json +184 -0
- configuration_yield.py +58 -0
- model.safetensors +3 -0
- modeling_yield.py +88 -0
- pipeline_yield.py +278 -0
- requirements.txt +153 -0
- sample_input_daily.json +43 -0
- sample_test.py +14 -0
- yield_transformer.py +159 -0
README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
````markdown
|
| 2 |
+
---
|
| 3 |
+
library_name: transformers
|
| 4 |
+
|
| 5 |
+
tags:
|
| 6 |
+
- transformers
|
| 7 |
+
- agriculture
|
| 8 |
+
- crop-yield
|
| 9 |
+
- yield-prediction
|
| 10 |
+
- weather
|
| 11 |
+
- soil
|
| 12 |
+
- regression
|
| 13 |
+
- time-series
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Yield Estimation Transformer
|
| 17 |
+
|
| 18 |
+
A Hugging Face Transformers model for crop yield prediction using weather time-series and soil properties.
|
| 19 |
+
|
| 20 |
+
This repository contains a pretrained transformer model packaged for inference through the Hugging Face Transformers API and a custom Hugging Face Pipeline. The pipeline automatically performs preprocessing, feature normalization, model inference, and returns the predicted crop yield from daily weather observations and static soil properties.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
# Features
|
| 25 |
+
|
| 26 |
+
- Transformer-based crop yield prediction
|
| 27 |
+
- Weather time-series and static soil feature integration
|
| 28 |
+
- Automatic preprocessing and normalization
|
| 29 |
+
- Daily weather input support
|
| 30 |
+
- Hugging Face AutoClass compatible
|
| 31 |
+
- Custom Hugging Face Pipeline
|
| 32 |
+
- CPU and GPU inference
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
# Quick Start
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
import json
|
| 40 |
+
from transformers import pipeline
|
| 41 |
+
|
| 42 |
+
pipe = pipeline(
|
| 43 |
+
"yield-estimation",
|
| 44 |
+
model="Sarikaa-Sridhar/yield-estimation-transformer",
|
| 45 |
+
trust_remote_code=True,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
with open("sample_input_daily.json") as f:
|
| 49 |
+
sample = json.load(f)
|
| 50 |
+
|
| 51 |
+
prediction = pipe(sample)
|
| 52 |
+
|
| 53 |
+
print(prediction)
|
| 54 |
+
````
|
| 55 |
+
|
| 56 |
+
Example output
|
| 57 |
+
|
| 58 |
+
```json
|
| 59 |
+
{
|
| 60 |
+
"predicted_yield": 182.28,
|
| 61 |
+
"cutoff": 4,
|
| 62 |
+
"effective_cutoff": 4,
|
| 63 |
+
"crop": "corn",
|
| 64 |
+
"weather_format": "daily"
|
| 65 |
+
}
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
# Installation
|
| 71 |
+
|
| 72 |
+
Clone the repository.
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
git clone https://huggingface.co/Sarikaa-Sridhar/yield-estimation-transformer
|
| 76 |
+
cd yield-estimation-transformer
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Create a Python environment.
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
conda create -n yield_hf python=3.10
|
| 83 |
+
conda activate yield_hf
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
Install the required dependencies.
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
pip install -r requirements.txt
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
# Repository Structure
|
| 95 |
+
|
| 96 |
+
```text
|
| 97 |
+
.
|
| 98 |
+
├── config.json
|
| 99 |
+
├── model.safetensors
|
| 100 |
+
├── configuration_yield.py
|
| 101 |
+
├── modeling_yield.py
|
| 102 |
+
├── pipeline_yield.py
|
| 103 |
+
├── yield_transformer.py
|
| 104 |
+
├── sample_input_daily.json
|
| 105 |
+
├── requirements.txt
|
| 106 |
+
└── README.md
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
# Loading the Model
|
| 112 |
+
|
| 113 |
+
The model can be loaded directly using the Hugging Face AutoModel interface.
|
| 114 |
+
|
| 115 |
+
```python
|
| 116 |
+
from transformers import AutoModel
|
| 117 |
+
|
| 118 |
+
model = AutoModel.from_pretrained(
|
| 119 |
+
"Sarikaa-Sridhar/yield-estimation-transformer",
|
| 120 |
+
trust_remote_code=True,
|
| 121 |
+
)
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
For most users, the recommended interface is the custom Hugging Face Pipeline shown in the Quick Start example.
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
# Input Format
|
| 129 |
+
|
| 130 |
+
The pipeline accepts a single JSON dictionary.
|
| 131 |
+
|
| 132 |
+
## Required fields
|
| 133 |
+
|
| 134 |
+
* `crop`
|
| 135 |
+
* `weather`
|
| 136 |
+
* `soil`
|
| 137 |
+
|
| 138 |
+
## Optional fields
|
| 139 |
+
|
| 140 |
+
* `cutoff`
|
| 141 |
+
* `weather_format`
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
# Daily Weather Input
|
| 146 |
+
|
| 147 |
+
The recommended input format is `"daily"`.
|
| 148 |
+
|
| 149 |
+
Example:
|
| 150 |
+
|
| 151 |
+
```json
|
| 152 |
+
{
|
| 153 |
+
"crop": "corn",
|
| 154 |
+
"weather_format": "daily",
|
| 155 |
+
"cutoff": 16,
|
| 156 |
+
"weather": {
|
| 157 |
+
"dayl": [...],
|
| 158 |
+
"prcp": [...],
|
| 159 |
+
"srad": [...],
|
| 160 |
+
"tmax": [...],
|
| 161 |
+
"gdd": [...],
|
| 162 |
+
"tmin": [...],
|
| 163 |
+
"vp": [...],
|
| 164 |
+
"tmean": [...],
|
| 165 |
+
"precip_3day_avg_perday": [...],
|
| 166 |
+
"precip_7day_avg_perday": [...],
|
| 167 |
+
"precip_14day_avg_perday": [...]
|
| 168 |
+
},
|
| 169 |
+
"soil": {
|
| 170 |
+
"ph": 6.5,
|
| 171 |
+
"om": 3.2,
|
| 172 |
+
"cec": 15.0,
|
| 173 |
+
"awc": 0.18,
|
| 174 |
+
"clay": 25.0,
|
| 175 |
+
"p_ppm": 30.0,
|
| 176 |
+
"k_ppm": 150.0,
|
| 177 |
+
"mg_ppm": 220.0,
|
| 178 |
+
"ca_ppm": 1800.0,
|
| 179 |
+
"k_te": 3.0,
|
| 180 |
+
"mg_te": 12.0,
|
| 181 |
+
"ca_te": 70.0,
|
| 182 |
+
"s_ppm": 15.0,
|
| 183 |
+
"zn_ppm": 1.2,
|
| 184 |
+
"fe_ppm": 50.0,
|
| 185 |
+
"mn_ppm": 20.0,
|
| 186 |
+
"cu_ppm": 0.8,
|
| 187 |
+
"b_ppm": 0.5,
|
| 188 |
+
"na_ppm": 10.0,
|
| 189 |
+
"sand": 40.0,
|
| 190 |
+
"silt": 35.0,
|
| 191 |
+
"bd": 1.3,
|
| 192 |
+
"elevation": 280.0,
|
| 193 |
+
"slope": 2.0
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
The pipeline automatically:
|
| 199 |
+
|
| 200 |
+
* converts daily weather observations into the feature representation expected by the model
|
| 201 |
+
* applies the normalization statistics stored with the model
|
| 202 |
+
* performs inference
|
| 203 |
+
* returns the predicted crop yield
|
| 204 |
+
|
| 205 |
+
No external preprocessing is required.
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
# Supported Crops
|
| 210 |
+
|
| 211 |
+
Current supported crop identifiers are:
|
| 212 |
+
|
| 213 |
+
* `corn`
|
| 214 |
+
* `maize`
|
| 215 |
+
* `soy`
|
| 216 |
+
* `soybean`
|
| 217 |
+
|
| 218 |
+
---
|
| 219 |
+
|
| 220 |
+
# Device Support
|
| 221 |
+
|
| 222 |
+
The pipeline supports both CPU and NVIDIA CUDA GPUs.
|
| 223 |
+
|
| 224 |
+
GPU inference:
|
| 225 |
+
|
| 226 |
+
```python
|
| 227 |
+
from transformers import pipeline
|
| 228 |
+
|
| 229 |
+
pipe = pipeline(
|
| 230 |
+
"yield-estimation",
|
| 231 |
+
model="Sarikaa-Sridhar/yield-estimation-transformer",
|
| 232 |
+
device=0,
|
| 233 |
+
trust_remote_code=True,
|
| 234 |
+
)
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
CPU inference:
|
| 238 |
+
|
| 239 |
+
```python
|
| 240 |
+
from transformers import pipeline
|
| 241 |
+
|
| 242 |
+
pipe = pipeline(
|
| 243 |
+
"yield-estimation",
|
| 244 |
+
model="Sarikaa-Sridhar/yield-estimation-transformer",
|
| 245 |
+
device=-1,
|
| 246 |
+
trust_remote_code=True,
|
| 247 |
+
)
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
# Pipeline Output
|
| 253 |
+
|
| 254 |
+
The pipeline returns a Python dictionary.
|
| 255 |
+
|
| 256 |
+
Example:
|
| 257 |
+
|
| 258 |
+
```json
|
| 259 |
+
{
|
| 260 |
+
"predicted_yield": 182.28,
|
| 261 |
+
"cutoff": 16,
|
| 262 |
+
"effective_cutoff": 16,
|
| 263 |
+
"crop": "corn",
|
| 264 |
+
"weather_format": "daily"
|
| 265 |
+
}
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
# Acknowledgements
|
| 271 |
+
|
| 272 |
+
This work was developed as part of the ICICLE AI Institute.
|
| 273 |
+
|
| 274 |
+
*National Science Foundation (NSF) AI Institute for Intelligent Cyberinfrastructure with Computational Learning in the Environment (ICICLE), Award OAC-2112606.*
|
| 275 |
+
|
| 276 |
+
```
|
| 277 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"K": 22,
|
| 3 |
+
"S": 24,
|
| 4 |
+
"W": 22,
|
| 5 |
+
"architectures": [
|
| 6 |
+
"YieldForRegression"
|
| 7 |
+
],
|
| 8 |
+
"crop_emb_dim": 8,
|
| 9 |
+
"d_model": 64,
|
| 10 |
+
"dim_ff": 128,
|
| 11 |
+
"dropout": 0.4,
|
| 12 |
+
"dtype": "float32",
|
| 13 |
+
"eval_cutoffs": [
|
| 14 |
+
8,
|
| 15 |
+
12,
|
| 16 |
+
16,
|
| 17 |
+
22
|
| 18 |
+
],
|
| 19 |
+
"model_type": "yield-weather-soil",
|
| 20 |
+
"nhead": 4,
|
| 21 |
+
"num_layers": 4,
|
| 22 |
+
"pool": "last",
|
| 23 |
+
"s_mean": [
|
| 24 |
+
6.847164154052734,
|
| 25 |
+
3.9301836490631104,
|
| 26 |
+
15.024548530578613,
|
| 27 |
+
0.11498195677995682,
|
| 28 |
+
28.22588348388672,
|
| 29 |
+
47.32505798339844,
|
| 30 |
+
165.28575134277344,
|
| 31 |
+
439.181640625,
|
| 32 |
+
2344.830322265625,
|
| 33 |
+
2.625120162963867,
|
| 34 |
+
21.69441032409668,
|
| 35 |
+
60.720855712890625,
|
| 36 |
+
8.925610542297363,
|
| 37 |
+
2.972254991531372,
|
| 38 |
+
195.54396057128906,
|
| 39 |
+
50.68339157104492,
|
| 40 |
+
3.3951573371887207,
|
| 41 |
+
1.1097495555877686,
|
| 42 |
+
11.323736190795898,
|
| 43 |
+
28.845870971679688,
|
| 44 |
+
42.91221618652344,
|
| 45 |
+
1.6110131740570068,
|
| 46 |
+
3.024193048477173,
|
| 47 |
+
0.01876676268875599
|
| 48 |
+
],
|
| 49 |
+
"s_std": [
|
| 50 |
+
0.3154756426811218,
|
| 51 |
+
1.066802740097046,
|
| 52 |
+
2.689357280731201,
|
| 53 |
+
0.02995595894753933,
|
| 54 |
+
6.459582328796387,
|
| 55 |
+
12.300628662109375,
|
| 56 |
+
34.140281677246094,
|
| 57 |
+
138.90280151367188,
|
| 58 |
+
493.52239990234375,
|
| 59 |
+
0.7019332051277161,
|
| 60 |
+
4.397585868835449,
|
| 61 |
+
7.440662860870361,
|
| 62 |
+
1.4173370599746704,
|
| 63 |
+
2.256568431854248,
|
| 64 |
+
34.21792221069336,
|
| 65 |
+
20.011821746826172,
|
| 66 |
+
1.11983060836792,
|
| 67 |
+
0.5050033926963806,
|
| 68 |
+
11.268291473388672,
|
| 69 |
+
12.573676109313965,
|
| 70 |
+
7.386580467224121,
|
| 71 |
+
0.09427518397569656,
|
| 72 |
+
1.7381932735443115,
|
| 73 |
+
0.013877413235604763
|
| 74 |
+
],
|
| 75 |
+
"soil_vars": [
|
| 76 |
+
"ph",
|
| 77 |
+
"om",
|
| 78 |
+
"cec",
|
| 79 |
+
"awc",
|
| 80 |
+
"clay",
|
| 81 |
+
"p_ppm",
|
| 82 |
+
"k_ppm",
|
| 83 |
+
"mg_ppm",
|
| 84 |
+
"ca_ppm",
|
| 85 |
+
"k_te",
|
| 86 |
+
"mg_te",
|
| 87 |
+
"ca_te",
|
| 88 |
+
"s_ppm",
|
| 89 |
+
"zn_ppm",
|
| 90 |
+
"fe_ppm",
|
| 91 |
+
"mn_ppm",
|
| 92 |
+
"cu_ppm",
|
| 93 |
+
"b_ppm",
|
| 94 |
+
"na_ppm",
|
| 95 |
+
"sand",
|
| 96 |
+
"silt",
|
| 97 |
+
"bd",
|
| 98 |
+
"elevation",
|
| 99 |
+
"slope"
|
| 100 |
+
],
|
| 101 |
+
"train_cutoffs": [
|
| 102 |
+
8,
|
| 103 |
+
12,
|
| 104 |
+
16,
|
| 105 |
+
22
|
| 106 |
+
],
|
| 107 |
+
"transformers_version": "5.13.0",
|
| 108 |
+
"use_crop": true,
|
| 109 |
+
"w_mean": [
|
| 110 |
+
49053.73828125,
|
| 111 |
+
51464.3359375,
|
| 112 |
+
22.913280487060547,
|
| 113 |
+
294.0011291503906,
|
| 114 |
+
362.58697509765625,
|
| 115 |
+
376.50128173828125,
|
| 116 |
+
26.46450424194336,
|
| 117 |
+
26.286815643310547,
|
| 118 |
+
73.66980743408203,
|
| 119 |
+
887.6223754882812,
|
| 120 |
+
14.769508361816406,
|
| 121 |
+
14.471501350402832,
|
| 122 |
+
1747.4173583984375,
|
| 123 |
+
1715.754638671875,
|
| 124 |
+
20.6231746673584,
|
| 125 |
+
20.374893188476562,
|
| 126 |
+
1.1128652095794678,
|
| 127 |
+
1.2524775266647339,
|
| 128 |
+
0.47625812888145447,
|
| 129 |
+
0.5278241634368896,
|
| 130 |
+
0.23921121656894684,
|
| 131 |
+
0.2665020227432251
|
| 132 |
+
],
|
| 133 |
+
"w_std": [
|
| 134 |
+
4406.49951171875,
|
| 135 |
+
1378.2545166015625,
|
| 136 |
+
22.44007110595703,
|
| 137 |
+
155.5632781982422,
|
| 138 |
+
53.64530563354492,
|
| 139 |
+
16.995542526245117,
|
| 140 |
+
3.947380542755127,
|
| 141 |
+
2.6959431171417236,
|
| 142 |
+
27.608686447143555,
|
| 143 |
+
530.9790649414062,
|
| 144 |
+
4.153758525848389,
|
| 145 |
+
2.8188235759735107,
|
| 146 |
+
401.1476745605469,
|
| 147 |
+
259.7794189453125,
|
| 148 |
+
3.952458381652832,
|
| 149 |
+
2.7360341548919678,
|
| 150 |
+
1.0096169710159302,
|
| 151 |
+
0.35198140144348145,
|
| 152 |
+
0.3805038034915924,
|
| 153 |
+
0.1445319503545761,
|
| 154 |
+
0.14427466690540314,
|
| 155 |
+
0.06612387299537659
|
| 156 |
+
],
|
| 157 |
+
"weather_vars": [
|
| 158 |
+
"dayl",
|
| 159 |
+
"prcp",
|
| 160 |
+
"srad",
|
| 161 |
+
"tmax",
|
| 162 |
+
"gdd",
|
| 163 |
+
"tmin",
|
| 164 |
+
"vp",
|
| 165 |
+
"tmean",
|
| 166 |
+
"precip_3day_avg_perday",
|
| 167 |
+
"precip_7day_avg_perday",
|
| 168 |
+
"precip_14day_avg_perday"
|
| 169 |
+
],
|
| 170 |
+
"y_mean": 214.7746124267578,
|
| 171 |
+
"y_std": 32.72690200805664,
|
| 172 |
+
"auto_map": {
|
| 173 |
+
"AutoConfig": "configuration_yield.YieldConfig",
|
| 174 |
+
"AutoModel": "modeling_yield.YieldForRegression"
|
| 175 |
+
},
|
| 176 |
+
"custom_pipelines": {
|
| 177 |
+
"yield-estimation": {
|
| 178 |
+
"impl": "pipeline_yield.YieldEstimationPipeline",
|
| 179 |
+
"pt": [
|
| 180 |
+
"AutoModel"
|
| 181 |
+
]
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
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
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:54e7921280a67c7f83d2a50b3823e7384fd8c606328ba22d8c834617c189da9f
|
| 3 |
+
size 723380
|
modeling_yield.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 configuration_yield import YieldConfig
|
| 14 |
+
from yield_transformer import YieldTransformer
|
| 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 = YieldTransformer(
|
| 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_norm = (labels - y_mean) / y_std
|
| 79 |
+
loss = nn.functional.mse_loss(logits, labels_norm)
|
| 80 |
+
|
| 81 |
+
if not return_dict:
|
| 82 |
+
return (loss, logits, predictions)
|
| 83 |
+
|
| 84 |
+
return YieldModelOutput(
|
| 85 |
+
loss=loss,
|
| 86 |
+
logits=logits,
|
| 87 |
+
predictions=predictions,
|
| 88 |
+
)
|
pipeline_yield.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
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
|
sample_input_daily.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"crop": "corn",
|
| 3 |
+
"weather_format": "daily",
|
| 4 |
+
"weather": {
|
| 5 |
+
"dayl": [45000,45100,45200,45300,45400,45500,45600,45700,45800,45900,46000,46100,46200,46300,46400,46500,46600,46700,46800,46900,47000,47100,47200,47300,47400,47500,47600,47700],
|
| 6 |
+
"prcp": [1,0,3,2,0,5,1,0,2,1,4,0,0,3,2,1,0,4,1,0,2,1,0,3,2,1,0,4],
|
| 7 |
+
"srad": [300,305,310,315,320,325,330,335,340,345,350,355,360,365,370,375,380,385,390,395,400,405,410,415,420,425,430,435],
|
| 8 |
+
"tmax": [25,26,27,28,29,30,29,28,27,26,28,29,30,31,32,31,30,29,28,27,26,25,26,27,28,29,30,31],
|
| 9 |
+
"gdd": [10,12,13,14,15,16,15,14,13,12,14,15,16,17,18,17,16,15,14,13,12,11,12,13,14,15,16,17],
|
| 10 |
+
"tmin": [15,16,16,17,18,19,18,17,16,15,16,17,18,19,20,19,18,17,16,15,14,14,15,16,17,18,19,20],
|
| 11 |
+
"vp": [1000,1010,1020,1030,1040,1050,1040,1030,1020,1010,1020,1030,1040,1050,1060,1050,1040,1030,1020,1010,1000,995,1000,1010,1020,1030,1040,1050],
|
| 12 |
+
"tmean": [20,21,21.5,22.5,23.5,24.5,23.5,22.5,21.5,20.5,22,23,24,25,26,25,24,23,22,21,20,19.5,20.5,21.5,22.5,23.5,24.5,25.5],
|
| 13 |
+
"precip_3day_avg_perday": [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
| 14 |
+
"precip_7day_avg_perday": [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
| 15 |
+
"precip_14day_avg_perday": [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
|
| 16 |
+
},
|
| 17 |
+
"soil": {
|
| 18 |
+
"ph": 6.5,
|
| 19 |
+
"om": 3.2,
|
| 20 |
+
"cec": 15.0,
|
| 21 |
+
"awc": 0.18,
|
| 22 |
+
"clay": 25.0,
|
| 23 |
+
"p_ppm": 30.0,
|
| 24 |
+
"k_ppm": 150.0,
|
| 25 |
+
"mg_ppm": 220.0,
|
| 26 |
+
"ca_ppm": 1800.0,
|
| 27 |
+
"k_te": 3.0,
|
| 28 |
+
"mg_te": 12.0,
|
| 29 |
+
"ca_te": 70.0,
|
| 30 |
+
"s_ppm": 15.0,
|
| 31 |
+
"zn_ppm": 1.2,
|
| 32 |
+
"fe_ppm": 50.0,
|
| 33 |
+
"mn_ppm": 20.0,
|
| 34 |
+
"cu_ppm": 0.8,
|
| 35 |
+
"b_ppm": 0.5,
|
| 36 |
+
"na_ppm": 10.0,
|
| 37 |
+
"sand": 40.0,
|
| 38 |
+
"silt": 35.0,
|
| 39 |
+
"bd": 1.3,
|
| 40 |
+
"elevation": 280.0,
|
| 41 |
+
"slope": 2.0
|
| 42 |
+
}
|
| 43 |
+
}
|
sample_test.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from transformers import pipeline
|
| 3 |
+
|
| 4 |
+
with open(f"sample_input_daily.json", "r") as f:
|
| 5 |
+
sample = json.load(f)
|
| 6 |
+
|
| 7 |
+
pipe = pipeline(
|
| 8 |
+
"yield-estimation",
|
| 9 |
+
model=".",
|
| 10 |
+
trust_remote_code=True,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
out = pipe(sample)
|
| 14 |
+
print(json.dumps(out, indent=2))
|
yield_transformer.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 YieldTransformer(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 |
+
self.week_proj = nn.Linear(1, d_model)
|
| 52 |
+
|
| 53 |
+
self.weather_enc = MLP(w_dim + crop_in, d_model, hidden=d_model, dropout=dropout)
|
| 54 |
+
self.soil_enc = MLP(soil_dim + crop_in, d_model, hidden=d_model, dropout=dropout)
|
| 55 |
+
|
| 56 |
+
self.ws_cross_attn = nn.MultiheadAttention(
|
| 57 |
+
embed_dim=d_model,
|
| 58 |
+
num_heads=nhead,
|
| 59 |
+
dropout=dropout,
|
| 60 |
+
batch_first=True,
|
| 61 |
+
)
|
| 62 |
+
self.ws_ln = nn.LayerNorm(d_model)
|
| 63 |
+
|
| 64 |
+
self.pos_emb = nn.Parameter(torch.zeros(1, max_weeks + 1, d_model))
|
| 65 |
+
nn.init.trunc_normal_(self.pos_emb, std=0.02)
|
| 66 |
+
|
| 67 |
+
if pool == "cls":
|
| 68 |
+
self.cls_token = nn.Parameter(torch.zeros(1, 1, d_model))
|
| 69 |
+
nn.init.trunc_normal_(self.cls_token, std=0.02)
|
| 70 |
+
|
| 71 |
+
enc_layer = nn.TransformerEncoderLayer(
|
| 72 |
+
d_model=d_model, nhead=nhead, dim_feedforward=dim_ff,
|
| 73 |
+
dropout=dropout, activation="gelu", batch_first=True, norm_first=True,
|
| 74 |
+
)
|
| 75 |
+
self.temporal_tf = nn.TransformerEncoder(enc_layer, num_layers=num_layers)
|
| 76 |
+
|
| 77 |
+
self.head = nn.Sequential(
|
| 78 |
+
nn.LayerNorm(d_model + horizon_emb_dim + d_model),
|
| 79 |
+
nn.Linear(d_model + horizon_emb_dim + d_model, d_model),
|
| 80 |
+
nn.GELU(),
|
| 81 |
+
nn.Dropout(dropout),
|
| 82 |
+
nn.Linear(d_model, 1),
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
def forward(
|
| 86 |
+
self,
|
| 87 |
+
weather,
|
| 88 |
+
soil,
|
| 89 |
+
crop_id,
|
| 90 |
+
farm_field_encoded=None,
|
| 91 |
+
week_mask=None,
|
| 92 |
+
causal=False,
|
| 93 |
+
return_sequence=False,
|
| 94 |
+
horizon_idx=None,
|
| 95 |
+
):
|
| 96 |
+
B, t, W = weather.shape
|
| 97 |
+
device = weather.device
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if horizon_idx is None: horizon_idx = torch.tensor(t, device=device).expand(B)
|
| 101 |
+
if not torch.is_tensor(horizon_idx): horizon_idx = torch.tensor(horizon_idx, device=device).expand(B)
|
| 102 |
+
|
| 103 |
+
h_idx = horizon_idx.long().clamp(min=1, max=self.max_weeks)
|
| 104 |
+
h_emb = self.horizon_emb(h_idx)
|
| 105 |
+
h_tok = self.horizon_proj(h_emb)
|
| 106 |
+
|
| 107 |
+
if self.use_crop:
|
| 108 |
+
c = self.crop_emb(crop_id)
|
| 109 |
+
weather_in = torch.cat([weather, c[:, None, :].expand(-1, t, -1)], dim=-1)
|
| 110 |
+
soil_in = torch.cat([soil, c], dim=-1)
|
| 111 |
+
else:
|
| 112 |
+
weather_in, soil_in = weather, soil
|
| 113 |
+
|
| 114 |
+
w_tok = self.weather_enc(weather_in)
|
| 115 |
+
s_encoded = self.soil_enc(soil_in)
|
| 116 |
+
|
| 117 |
+
fused = self.ws_ln(w_tok + s_encoded[:, None, :])
|
| 118 |
+
fused = fused + h_tok[:, None, :]
|
| 119 |
+
|
| 120 |
+
weeks = torch.arange(1, t + 1, device=device).float() / self.max_weeks
|
| 121 |
+
week_signal = self.week_proj(weeks[None, :, None].expand(B, -1, -1))
|
| 122 |
+
|
| 123 |
+
x = fused + week_signal + self.pos_emb[:, :t, :]
|
| 124 |
+
|
| 125 |
+
if self.pool == "cls":
|
| 126 |
+
x = torch.cat([self.cls_token.expand(B, -1, -1), x], dim=1)
|
| 127 |
+
t_idx = t + 1
|
| 128 |
+
else: t_idx = t
|
| 129 |
+
|
| 130 |
+
src_key_padding_mask = ~week_mask.bool() if week_mask is not None else None
|
| 131 |
+
|
| 132 |
+
attn_mask = None
|
| 133 |
+
if causal:
|
| 134 |
+
L = x.size(1)
|
| 135 |
+
attn_mask = torch.triu(
|
| 136 |
+
torch.ones(L, L, device=device, dtype=torch.bool),
|
| 137 |
+
diagonal=1,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
h = self.temporal_tf(
|
| 141 |
+
x,
|
| 142 |
+
mask=attn_mask,
|
| 143 |
+
src_key_padding_mask=src_key_padding_mask,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
if self.pool == "mean":
|
| 147 |
+
if week_mask is not None:
|
| 148 |
+
mask = week_mask.unsqueeze(-1).float()
|
| 149 |
+
pooled = (h * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1e-9)
|
| 150 |
+
else:
|
| 151 |
+
pooled = h.mean(dim=1)
|
| 152 |
+
elif self.pool == "cls":
|
| 153 |
+
pooled = h[:, 0, :]
|
| 154 |
+
else:
|
| 155 |
+
pooled = h[:, -1, :]
|
| 156 |
+
|
| 157 |
+
out = torch.cat([pooled, h_emb, s_encoded], dim=-1)
|
| 158 |
+
return self.head(out).squeeze(-1)
|
| 159 |
+
|