Sarikaa-Sridhar commited on
Commit
9079f0c
·
verified ·
1 Parent(s): cd10160

Upload 4 files

Browse files
config.json CHANGED
@@ -3,7 +3,7 @@
3
  "S": 66,
4
  "W": 6,
5
  "architectures": [
6
- "YieldForRegression"
7
  ],
8
  "crop_emb_dim": 8,
9
  "d_model": 64,
@@ -272,8 +272,22 @@
272
  "auto_map": {
273
  "AutoConfig": "configuration_yield.YieldConfig",
274
  "AutoModel": "modeling_yield.YieldForRegression",
 
 
 
 
 
275
  "AutoProcessor": "processing_yield.YieldProcessor"
276
  },
 
 
 
 
 
 
 
 
 
277
  "custom_pipelines": {
278
  "yield-estimation": {
279
  "impl": "pipeline_yield.YieldEstimationPipeline",
 
3
  "S": 66,
4
  "W": 6,
5
  "architectures": [
6
+ "YieldForSequenceClassification"
7
  ],
8
  "crop_emb_dim": 8,
9
  "d_model": 64,
 
272
  "auto_map": {
273
  "AutoConfig": "configuration_yield.YieldConfig",
274
  "AutoModel": "modeling_yield.YieldForRegression",
275
+ "AutoModelForSequenceClassification": "modeling_yield_text.YieldForSequenceClassification",
276
+ "AutoTokenizer": [
277
+ "tokenization_yield.YieldTokenizer",
278
+ null
279
+ ],
280
  "AutoProcessor": "processing_yield.YieldProcessor"
281
  },
282
+ "problem_type": "regression",
283
+ "num_labels": 1,
284
+ "id2label": {
285
+ "0": "YIELD_BU_ACRE"
286
+ },
287
+ "label2id": {
288
+ "YIELD_BU_ACRE": 0
289
+ },
290
+ "tokenizer_class": "YieldTokenizer",
291
  "custom_pipelines": {
292
  "yield-estimation": {
293
  "impl": "pipeline_yield.YieldEstimationPipeline",
modeling_yield_text.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from torch import nn
4
+
5
+ from transformers import PreTrainedModel
6
+ from transformers.modeling_outputs import SequenceClassifierOutput
7
+
8
+ from .configuration_yield import YieldConfig
9
+ from .yield_transformer import YieldTransformer
10
+
11
+
12
+ class YieldForSequenceClassification(PreTrainedModel):
13
+
14
+ config_class = YieldConfig
15
+ base_model_prefix = "yield_model"
16
+
17
+ def __init__(self, config: YieldConfig):
18
+ super().__init__(config)
19
+
20
+ self.yield_model = YieldTransformer(
21
+ w_dim=config.W,
22
+ soil_dim=config.S,
23
+ d_model=config.d_model,
24
+ nhead=config.nhead,
25
+ num_layers=config.num_layers,
26
+ dim_ff=config.dim_ff,
27
+ dropout=config.dropout,
28
+ use_crop=config.use_crop,
29
+ crop_emb_dim=config.crop_emb_dim,
30
+ max_weeks=max(52, config.K),
31
+ pool=config.pool,
32
+ )
33
+
34
+ self.post_init()
35
+
36
+ def forward(
37
+ self,
38
+ weather,
39
+ soil,
40
+ crop_id,
41
+ horizon_idx=None,
42
+ labels=None,
43
+ **kwargs,
44
+ ):
45
+
46
+ # ==================================================
47
+ # Shape checks
48
+ # ==================================================
49
+
50
+ if weather.ndim != 3:
51
+ raise ValueError(
52
+ f"weather must have shape [B,K,W], "
53
+ f"received {tuple(weather.shape)}"
54
+ )
55
+
56
+ if soil.ndim != 2:
57
+ raise ValueError(
58
+ f"soil must have shape [B,S], "
59
+ f"received {tuple(soil.shape)}"
60
+ )
61
+
62
+ # ==================================================
63
+ # Training normalization statistics
64
+ # ==================================================
65
+
66
+ w_mean = torch.tensor(
67
+ self.config.w_mean,
68
+ device=weather.device,
69
+ dtype=weather.dtype,
70
+ )
71
+
72
+ w_std = torch.tensor(
73
+ self.config.w_std,
74
+ device=weather.device,
75
+ dtype=weather.dtype,
76
+ )
77
+
78
+ s_mean = torch.tensor(
79
+ self.config.s_mean,
80
+ device=soil.device,
81
+ dtype=soil.dtype,
82
+ )
83
+
84
+ s_std = torch.tensor(
85
+ self.config.s_std,
86
+ device=soil.device,
87
+ dtype=soil.dtype,
88
+ )
89
+
90
+ # NaN -> training mean
91
+ weather = torch.where(
92
+ torch.isnan(weather),
93
+ w_mean.view(1, 1, -1),
94
+ weather,
95
+ )
96
+
97
+ soil = torch.where(
98
+ torch.isnan(soil),
99
+ s_mean.view(1, -1),
100
+ soil,
101
+ )
102
+
103
+ # normalize
104
+ weather = (
105
+ weather - w_mean.view(1, 1, -1)
106
+ ) / w_std.view(1, 1, -1)
107
+
108
+ soil = (
109
+ soil - s_mean.view(1, -1)
110
+ ) / s_std.view(1, -1)
111
+
112
+ # ==================================================
113
+ # Cutoff
114
+ # ==================================================
115
+
116
+ if horizon_idx is None:
117
+ horizon_idx = torch.full(
118
+ (weather.shape[0],),
119
+ weather.shape[1],
120
+ dtype=torch.long,
121
+ device=weather.device,
122
+ )
123
+
124
+ if not torch.is_tensor(horizon_idx):
125
+ horizon_idx = torch.tensor(
126
+ horizon_idx,
127
+ dtype=torch.long,
128
+ device=weather.device,
129
+ )
130
+
131
+ horizon_idx = horizon_idx.to(
132
+ weather.device
133
+ ).long()
134
+
135
+ if horizon_idx.ndim == 0:
136
+ horizon_idx = horizon_idx.unsqueeze(0)
137
+
138
+ # FlexServe requests are normally one sample.
139
+ # Ensure one common temporal length for a batch.
140
+ unique_cutoffs = torch.unique(
141
+ horizon_idx
142
+ )
143
+
144
+ if len(unique_cutoffs) != 1:
145
+ raise ValueError(
146
+ "All samples in one batch must use the same cutoff."
147
+ )
148
+
149
+ t_eff = int(
150
+ unique_cutoffs[0].item()
151
+ )
152
+
153
+ weather = weather[
154
+ :,
155
+ :t_eff,
156
+ :
157
+ ]
158
+
159
+ # ==================================================
160
+ # Existing trained model
161
+ # ==================================================
162
+
163
+ logits_norm = self.yield_model(
164
+ weather,
165
+ soil,
166
+ crop_id,
167
+ horizon_idx=horizon_idx,
168
+ causal=True,
169
+ return_sequence=False,
170
+ )
171
+
172
+ # Restore yield to bu/acre.
173
+ y_mean = torch.tensor(
174
+ self.config.y_mean,
175
+ device=logits_norm.device,
176
+ dtype=logits_norm.dtype,
177
+ )
178
+
179
+ y_std = torch.tensor(
180
+ self.config.y_std,
181
+ device=logits_norm.device,
182
+ dtype=logits_norm.dtype,
183
+ )
184
+
185
+ predicted_yield = (
186
+ logits_norm * y_std
187
+ + y_mean
188
+ )
189
+
190
+ # TextClassificationPipeline expects [B, num_labels].
191
+ logits = predicted_yield.unsqueeze(-1)
192
+
193
+ loss = None
194
+
195
+ if labels is not None:
196
+
197
+ labels = labels.to(
198
+ logits.dtype
199
+ ).view(-1)
200
+
201
+ loss = nn.functional.mse_loss(
202
+ predicted_yield,
203
+ labels,
204
+ )
205
+
206
+ return SequenceClassifierOutput(
207
+ loss=loss,
208
+ logits=logits,
209
+ )
tokenization_yield.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ import numpy as np
4
+ import torch
5
+
6
+ from transformers import PreTrainedTokenizer
7
+ from transformers.tokenization_utils_base import BatchEncoding
8
+
9
+
10
+ class YieldTokenizer(PreTrainedTokenizer):
11
+ """
12
+ Adapter tokenizer for FlexServe's built-in text-classification pipeline.
13
+
14
+ This does NOT tokenize natural language.
15
+
16
+ It accepts:
17
+ - a JSON string, or
18
+ - the yield input dictionary
19
+
20
+ and converts it into:
21
+ weather: [B, 52, 6]
22
+ soil: [B, 66]
23
+ crop_id: [B]
24
+ horizon_idx: [B]
25
+
26
+ Normalization is intentionally NOT performed here.
27
+ The sequence-classification model wrapper performs normalization
28
+ using statistics stored in config.json.
29
+ """
30
+
31
+ vocab_files_names = {}
32
+ model_input_names = [
33
+ "weather",
34
+ "soil",
35
+ "crop_id",
36
+ "horizon_idx",
37
+ ]
38
+
39
+ def __init__(
40
+ self,
41
+ weather_vars=None,
42
+ soil_vars=None,
43
+ K=52,
44
+ eval_cutoffs=None,
45
+ **kwargs,
46
+ ):
47
+ self.weather_vars = list(weather_vars or [])
48
+ self.soil_vars = list(soil_vars or [])
49
+ self.K = int(K)
50
+ self.eval_cutoffs = list(
51
+ eval_cutoffs
52
+ or [20, 24, 28, 32, 36, 40, 44, 48, 52]
53
+ )
54
+
55
+ super().__init__(
56
+ pad_token="[PAD]",
57
+ unk_token="[UNK]",
58
+ **kwargs,
59
+ )
60
+
61
+ @property
62
+ def vocab_size(self):
63
+ return 2
64
+
65
+ def get_vocab(self):
66
+ return {
67
+ "[PAD]": 0,
68
+ "[UNK]": 1,
69
+ }
70
+
71
+ def _tokenize(self, text, **kwargs):
72
+ return ["[UNK]"]
73
+
74
+ def _convert_token_to_id(self, token):
75
+ return 0 if token == "[PAD]" else 1
76
+
77
+ def _convert_id_to_token(self, index):
78
+ return "[PAD]" if index == 0 else "[UNK]"
79
+
80
+ def save_vocabulary(self, save_directory, filename_prefix=None):
81
+ return ()
82
+
83
+ def _parse_sample(self, sample):
84
+
85
+ if isinstance(sample, str):
86
+ sample = sample.strip()
87
+
88
+ try:
89
+ sample = json.loads(sample)
90
+ except json.JSONDecodeError as exc:
91
+ raise ValueError(
92
+ "Input must be a valid JSON string."
93
+ ) from exc
94
+
95
+ # Handle a JSON string containing another JSON string.
96
+ if isinstance(sample, str):
97
+ try:
98
+ sample = json.loads(sample)
99
+ except json.JSONDecodeError as exc:
100
+ raise ValueError(
101
+ "Input string does not contain valid yield JSON."
102
+ ) from exc
103
+
104
+ if not isinstance(sample, dict):
105
+ raise ValueError(
106
+ "Yield input must be a JSON object/dictionary."
107
+ )
108
+
109
+ crop = str(
110
+ sample.get("crop", "corn")
111
+ ).strip().lower()
112
+
113
+ if crop not in ("corn", "maize"):
114
+ raise ValueError(
115
+ "This released model supports corn only."
116
+ )
117
+
118
+ if "weather" not in sample:
119
+ raise ValueError(
120
+ "Missing 'weather' object."
121
+ )
122
+
123
+ if "soil" not in sample:
124
+ raise ValueError(
125
+ "Missing 'soil' object."
126
+ )
127
+
128
+ weather_dict = sample["weather"]
129
+ soil_dict = sample["soil"]
130
+
131
+ # --------------------------------------------
132
+ # Weather: [52, 6]
133
+ # --------------------------------------------
134
+
135
+ weather_cols = []
136
+
137
+ for var in self.weather_vars:
138
+
139
+ if var not in weather_dict:
140
+ raise ValueError(
141
+ f"Missing weather variable '{var}'."
142
+ )
143
+
144
+ values = np.asarray(
145
+ weather_dict[var],
146
+ dtype=np.float32,
147
+ )
148
+
149
+ if values.ndim != 1:
150
+ raise ValueError(
151
+ f"Weather '{var}' must be one-dimensional."
152
+ )
153
+
154
+ if len(values) != self.K:
155
+ raise ValueError(
156
+ f"Weather '{var}' requires exactly "
157
+ f"{self.K} weekly values; received {len(values)}."
158
+ )
159
+
160
+ weather_cols.append(values)
161
+
162
+ weather = np.stack(
163
+ weather_cols,
164
+ axis=1,
165
+ ).astype(np.float32)
166
+
167
+ # --------------------------------------------
168
+ # Soil: [66]
169
+ # --------------------------------------------
170
+
171
+ soil = []
172
+
173
+ for var in self.soil_vars:
174
+
175
+ if var not in soil_dict:
176
+ raise ValueError(
177
+ f"Missing soil variable '{var}'."
178
+ )
179
+
180
+ soil.append(
181
+ float(soil_dict[var])
182
+ )
183
+
184
+ soil = np.asarray(
185
+ soil,
186
+ dtype=np.float32,
187
+ )
188
+
189
+ # --------------------------------------------
190
+ # Cutoff
191
+ # --------------------------------------------
192
+
193
+ cutoff = int(
194
+ sample.get(
195
+ "cutoff",
196
+ max(self.eval_cutoffs),
197
+ )
198
+ )
199
+
200
+ if cutoff not in self.eval_cutoffs:
201
+ raise ValueError(
202
+ f"Unsupported cutoff {cutoff}. "
203
+ f"Supported cutoffs are {self.eval_cutoffs}."
204
+ )
205
+
206
+ return {
207
+ "weather": weather,
208
+ "soil": soil,
209
+ "crop_id": 0,
210
+ "horizon_idx": cutoff,
211
+ }
212
+
213
+ def __call__(
214
+ self,
215
+ text=None,
216
+ text_pair=None,
217
+ return_tensors=None,
218
+ **kwargs,
219
+ ):
220
+ # --------------------------------------------------
221
+ # FlexServe/HF may call tokenizer(**input_dict)
222
+ # instead of tokenizer(json_string).
223
+ # --------------------------------------------------
224
+
225
+ if text is None and "weather" in kwargs and "soil" in kwargs:
226
+
227
+ sample = {
228
+ "crop": kwargs.pop("crop", "corn"),
229
+ "weather": kwargs.pop("weather"),
230
+ "soil": kwargs.pop("soil"),
231
+ "cutoff": kwargs.pop(
232
+ "cutoff",
233
+ max(self.eval_cutoffs),
234
+ ),
235
+ }
236
+
237
+ samples = [sample]
238
+
239
+ elif isinstance(text, (list, tuple)):
240
+
241
+ samples = list(text)
242
+
243
+ else:
244
+
245
+ samples = [text]
246
+
247
+ parsed = [
248
+ self._parse_sample(sample)
249
+ for sample in samples
250
+ ]
251
+
252
+ weather = torch.tensor(
253
+ np.stack(
254
+ [x["weather"] for x in parsed],
255
+ axis=0,
256
+ ),
257
+ dtype=torch.float32,
258
+ )
259
+
260
+ soil = torch.tensor(
261
+ np.stack(
262
+ [x["soil"] for x in parsed],
263
+ axis=0,
264
+ ),
265
+ dtype=torch.float32,
266
+ )
267
+
268
+ crop_id = torch.tensor(
269
+ [
270
+ x["crop_id"]
271
+ for x in parsed
272
+ ],
273
+ dtype=torch.long,
274
+ )
275
+
276
+ horizon_idx = torch.tensor(
277
+ [
278
+ x["horizon_idx"]
279
+ for x in parsed
280
+ ],
281
+ dtype=torch.long,
282
+ )
283
+
284
+ return BatchEncoding(
285
+ {
286
+ "weather": weather,
287
+ "soil": soil,
288
+ "crop_id": crop_id,
289
+ "horizon_idx": horizon_idx,
290
+ }
291
+ )
tokenizer_config.json ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tokenizer_class": "YieldTokenizer",
3
+ "auto_map": {
4
+ "AutoTokenizer": [
5
+ "tokenization_yield.YieldTokenizer",
6
+ null
7
+ ]
8
+ },
9
+ "weather_vars": [
10
+ "prcp",
11
+ "srad",
12
+ "swe",
13
+ "tmax",
14
+ "tmin",
15
+ "vp"
16
+ ],
17
+ "soil_vars": [
18
+ "bdod_mean_0-5cm",
19
+ "bdod_mean_5-15cm",
20
+ "bdod_mean_15-30cm",
21
+ "bdod_mean_30-60cm",
22
+ "bdod_mean_60-100cm",
23
+ "bdod_mean_100-200cm",
24
+ "cec_mean_0-5cm",
25
+ "cec_mean_5-15cm",
26
+ "cec_mean_15-30cm",
27
+ "cec_mean_30-60cm",
28
+ "cec_mean_60-100cm",
29
+ "cec_mean_100-200cm",
30
+ "cfvo_mean_0-5cm",
31
+ "cfvo_mean_5-15cm",
32
+ "cfvo_mean_15-30cm",
33
+ "cfvo_mean_30-60cm",
34
+ "cfvo_mean_60-100cm",
35
+ "cfvo_mean_100-200cm",
36
+ "clay_mean_0-5cm",
37
+ "clay_mean_5-15cm",
38
+ "clay_mean_15-30cm",
39
+ "clay_mean_30-60cm",
40
+ "clay_mean_60-100cm",
41
+ "clay_mean_100-200cm",
42
+ "nitrogen_mean_0-5cm",
43
+ "nitrogen_mean_5-15cm",
44
+ "nitrogen_mean_15-30cm",
45
+ "nitrogen_mean_30-60cm",
46
+ "nitrogen_mean_60-100cm",
47
+ "nitrogen_mean_100-200cm",
48
+ "ocd_mean_0-5cm",
49
+ "ocd_mean_5-15cm",
50
+ "ocd_mean_15-30cm",
51
+ "ocd_mean_30-60cm",
52
+ "ocd_mean_60-100cm",
53
+ "ocd_mean_100-200cm",
54
+ "ocs_mean_0-5cm",
55
+ "ocs_mean_5-15cm",
56
+ "ocs_mean_15-30cm",
57
+ "ocs_mean_30-60cm",
58
+ "ocs_mean_60-100cm",
59
+ "ocs_mean_100-200cm",
60
+ "phh2o_mean_0-5cm",
61
+ "phh2o_mean_5-15cm",
62
+ "phh2o_mean_15-30cm",
63
+ "phh2o_mean_30-60cm",
64
+ "phh2o_mean_60-100cm",
65
+ "phh2o_mean_100-200cm",
66
+ "sand_mean_0-5cm",
67
+ "sand_mean_5-15cm",
68
+ "sand_mean_15-30cm",
69
+ "sand_mean_30-60cm",
70
+ "sand_mean_60-100cm",
71
+ "sand_mean_100-200cm",
72
+ "silt_mean_0-5cm",
73
+ "silt_mean_5-15cm",
74
+ "silt_mean_15-30cm",
75
+ "silt_mean_30-60cm",
76
+ "silt_mean_60-100cm",
77
+ "silt_mean_100-200cm",
78
+ "soc_mean_0-5cm",
79
+ "soc_mean_5-15cm",
80
+ "soc_mean_15-30cm",
81
+ "soc_mean_30-60cm",
82
+ "soc_mean_60-100cm",
83
+ "soc_mean_100-200cm"
84
+ ],
85
+ "K": 52,
86
+ "eval_cutoffs": [
87
+ 20,
88
+ 24,
89
+ 28,
90
+ 32,
91
+ 36,
92
+ 40,
93
+ 44,
94
+ 48,
95
+ 52
96
+ ],
97
+ "model_max_length": 1000000
98
+ }