edrft 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edrft/__init__.py +18 -0
- edrft/data.py +57 -0
- edrft/metrics.py +26 -0
- edrft/models.py +266 -0
- edrft/tuning.py +191 -0
- edrft/wave.py +200 -0
- edrft-0.1.0.dist-info/METADATA +166 -0
- edrft-0.1.0.dist-info/RECORD +11 -0
- edrft-0.1.0.dist-info/WHEEL +5 -0
- edrft-0.1.0.dist-info/licenses/LICENSE +21 -0
- edrft-0.1.0.dist-info/top_level.txt +1 -0
edrft/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Random Vector Functional Link Transformer models."""
|
|
2
|
+
|
|
3
|
+
from .data import chronological_split, make_forecasting_frame
|
|
4
|
+
from .metrics import mean_absolute_scaled_error, mean_absolute_percentage_error, root_mean_squared_error
|
|
5
|
+
from .models import EDRFTRegressor, RFTLayerParams, RFTRegressor
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"EDRFTRegressor",
|
|
9
|
+
"RFTLayerParams",
|
|
10
|
+
"RFTRegressor",
|
|
11
|
+
"chronological_split",
|
|
12
|
+
"make_forecasting_frame",
|
|
13
|
+
"mean_absolute_percentage_error",
|
|
14
|
+
"mean_absolute_scaled_error",
|
|
15
|
+
"root_mean_squared_error",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
edrft/data.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make_forecasting_frame(
|
|
10
|
+
series: Iterable[float] | np.ndarray,
|
|
11
|
+
order: int = 48,
|
|
12
|
+
horizon: int = 4,
|
|
13
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
14
|
+
"""Convert a univariate or multivariate sequence into lagged samples."""
|
|
15
|
+
|
|
16
|
+
values = np.asarray(series, dtype=np.float32)
|
|
17
|
+
if values.ndim == 1:
|
|
18
|
+
values = values.reshape(-1, 1)
|
|
19
|
+
if values.ndim != 2:
|
|
20
|
+
raise ValueError("series must be a 1D or 2D array.")
|
|
21
|
+
if order <= 0 or horizon <= 0:
|
|
22
|
+
raise ValueError("order and horizon must be positive.")
|
|
23
|
+
n_samples = values.shape[0] - order - horizon + 1
|
|
24
|
+
if n_samples <= 0:
|
|
25
|
+
raise ValueError("series is too short for the requested order and horizon.")
|
|
26
|
+
X = np.zeros((n_samples, values.shape[1] * order), dtype=np.float32)
|
|
27
|
+
y = np.zeros((n_samples, values.shape[1]), dtype=np.float32)
|
|
28
|
+
for i in range(n_samples):
|
|
29
|
+
X[i] = values[i : i + order].ravel()
|
|
30
|
+
y[i] = values[i + order + horizon - 1]
|
|
31
|
+
return X, y.ravel() if y.shape[1] == 1 else y
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def chronological_split(n_samples: int, validation_fraction: float = 0.1, test_fraction: float = 0.2):
|
|
35
|
+
"""Return train, validation, full-train, and test indexes in time order."""
|
|
36
|
+
|
|
37
|
+
test_len = int(test_fraction * n_samples)
|
|
38
|
+
val_len = int(validation_fraction * n_samples)
|
|
39
|
+
train_len = n_samples - val_len - test_len
|
|
40
|
+
if train_len <= 0:
|
|
41
|
+
raise ValueError("Not enough samples for the requested split.")
|
|
42
|
+
train = np.arange(train_len)
|
|
43
|
+
val = np.arange(train_len, train_len + val_len)
|
|
44
|
+
full_train = np.arange(train_len + val_len)
|
|
45
|
+
test = np.arange(train_len + val_len, n_samples)
|
|
46
|
+
return train, val, full_train, test
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_ndbc_wave_file(path: str | Path, features: list[str] | None = None) -> pd.DataFrame:
|
|
50
|
+
"""Load an NDBC wave-height text file and clean sentinel missing values."""
|
|
51
|
+
|
|
52
|
+
import pandas as pd
|
|
53
|
+
|
|
54
|
+
features = features or ["WDIR", "WSPD", "GST", "APD", "WVHT"]
|
|
55
|
+
frame = pd.read_csv(path, sep=r"\s+", compression="infer")
|
|
56
|
+
frame = frame[features].replace(["99.0", "99.00", 99.0, 99.00], np.nan)
|
|
57
|
+
return frame.ffill().bfill().astype(float)
|
edrft/metrics.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def root_mean_squared_error(y_true, y_pred) -> float:
|
|
7
|
+
truth = np.asarray(y_true, dtype=float).ravel()
|
|
8
|
+
pred = np.asarray(y_pred, dtype=float).ravel()
|
|
9
|
+
return float(np.sqrt(np.mean((truth - pred) ** 2)))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mean_absolute_percentage_error(y_true, y_pred, epsilon: float = 1e-8) -> float:
|
|
13
|
+
truth = np.asarray(y_true, dtype=float).ravel()
|
|
14
|
+
pred = np.asarray(y_pred, dtype=float).ravel()
|
|
15
|
+
denom = np.maximum(np.abs(truth), epsilon)
|
|
16
|
+
return float(np.mean(np.abs((truth - pred) / denom)))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def mean_absolute_scaled_error(y_true, y_pred, history, seasonality: int = 1) -> float:
|
|
20
|
+
truth = np.asarray(y_true, dtype=float).ravel()
|
|
21
|
+
pred = np.asarray(y_pred, dtype=float).ravel()
|
|
22
|
+
hist = np.asarray(history, dtype=float).ravel()
|
|
23
|
+
scale = np.mean(np.abs(hist[seasonality:] - hist[:-seasonality]))
|
|
24
|
+
if scale == 0:
|
|
25
|
+
return float("inf")
|
|
26
|
+
return float(np.mean(np.abs(truth - pred)) / scale)
|
edrft/models.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable, Literal
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
import torch.nn as nn
|
|
9
|
+
|
|
10
|
+
Aggregation = Literal["median", "mean"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class RFTLayerParams:
|
|
15
|
+
"""Hyperparameters for one RFT hidden layer."""
|
|
16
|
+
|
|
17
|
+
n_hidden: int = 64
|
|
18
|
+
regularization: float = 1e-3
|
|
19
|
+
input_scale: float = 0.1
|
|
20
|
+
transformer_layers: int = 1
|
|
21
|
+
num_heads: int = 1
|
|
22
|
+
dropout: float = 0.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _as_2d(values: np.ndarray | Iterable[float], name: str) -> np.ndarray:
|
|
26
|
+
array = np.asarray(values, dtype=np.float32)
|
|
27
|
+
if array.ndim == 1:
|
|
28
|
+
array = array.reshape(-1, 1)
|
|
29
|
+
if array.ndim != 2:
|
|
30
|
+
raise ValueError(f"{name} must be a 1D or 2D array.")
|
|
31
|
+
if not np.all(np.isfinite(array)):
|
|
32
|
+
raise ValueError(f"{name} contains NaN or infinite values.")
|
|
33
|
+
return array
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ridge_solve(design: np.ndarray, target: np.ndarray, regularization: float) -> np.ndarray:
|
|
37
|
+
penalty = float(regularization) * np.eye(design.shape[1], dtype=np.float64)
|
|
38
|
+
penalty[-1, -1] = 0.0
|
|
39
|
+
left = design.T @ design + penalty
|
|
40
|
+
right = design.T @ target
|
|
41
|
+
try:
|
|
42
|
+
return np.linalg.solve(left, right)
|
|
43
|
+
except np.linalg.LinAlgError:
|
|
44
|
+
return np.linalg.pinv(left) @ right
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _RandomTransformerBlock(nn.Module):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
n_features: int,
|
|
51
|
+
n_hidden: int,
|
|
52
|
+
input_scale: float,
|
|
53
|
+
transformer_layers: int,
|
|
54
|
+
num_heads: int,
|
|
55
|
+
dropout: float,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__()
|
|
58
|
+
heads = int(num_heads) if n_features % int(num_heads) == 0 else 1
|
|
59
|
+
encoder_layer = nn.TransformerEncoderLayer(
|
|
60
|
+
d_model=n_features,
|
|
61
|
+
nhead=max(1, heads),
|
|
62
|
+
dim_feedforward=int(n_hidden),
|
|
63
|
+
dropout=float(dropout),
|
|
64
|
+
activation="relu",
|
|
65
|
+
batch_first=True,
|
|
66
|
+
)
|
|
67
|
+
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=int(transformer_layers))
|
|
68
|
+
self.projection = nn.Linear(n_features, int(n_hidden))
|
|
69
|
+
self.activation = nn.Tanh()
|
|
70
|
+
self.input_scale = float(input_scale)
|
|
71
|
+
self.reset_parameters()
|
|
72
|
+
|
|
73
|
+
def reset_parameters(self) -> None:
|
|
74
|
+
for parameter in self.encoder.parameters():
|
|
75
|
+
if parameter.ndim > 1:
|
|
76
|
+
nn.init.uniform_(parameter, -self.input_scale, self.input_scale)
|
|
77
|
+
else:
|
|
78
|
+
nn.init.zeros_(parameter)
|
|
79
|
+
nn.init.uniform_(self.projection.weight, -self.input_scale, self.input_scale)
|
|
80
|
+
nn.init.uniform_(self.projection.bias, -self.input_scale, self.input_scale)
|
|
81
|
+
|
|
82
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
83
|
+
encoded = self.encoder(x.unsqueeze(1)).squeeze(1)
|
|
84
|
+
return self.activation(self.projection(encoded))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RFTRegressor:
|
|
88
|
+
"""Random Vector Functional Link Transformer regressor.
|
|
89
|
+
|
|
90
|
+
Hidden transformer weights are randomly initialized and fixed. Only the
|
|
91
|
+
output readout is solved with ridge regression.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
n_hidden: int = 64,
|
|
97
|
+
regularization: float = 1e-3,
|
|
98
|
+
input_scale: float = 0.1,
|
|
99
|
+
transformer_layers: int = 1,
|
|
100
|
+
num_heads: int = 1,
|
|
101
|
+
dropout: float = 0.0,
|
|
102
|
+
random_state: int | None = None,
|
|
103
|
+
device: str = "cpu",
|
|
104
|
+
) -> None:
|
|
105
|
+
self.n_hidden = n_hidden
|
|
106
|
+
self.regularization = regularization
|
|
107
|
+
self.input_scale = input_scale
|
|
108
|
+
self.transformer_layers = transformer_layers
|
|
109
|
+
self.num_heads = num_heads
|
|
110
|
+
self.dropout = dropout
|
|
111
|
+
self.random_state = random_state
|
|
112
|
+
self.device = device
|
|
113
|
+
|
|
114
|
+
def fit(self, X: np.ndarray | Iterable[float], y: np.ndarray | Iterable[float]) -> "RFTRegressor":
|
|
115
|
+
X_arr = _as_2d(X, "X")
|
|
116
|
+
y_arr = _as_2d(y, "y").astype(np.float64)
|
|
117
|
+
if X_arr.shape[0] != y_arr.shape[0]:
|
|
118
|
+
raise ValueError("X and y must contain the same number of samples.")
|
|
119
|
+
torch.manual_seed(0 if self.random_state is None else int(self.random_state))
|
|
120
|
+
self.block_ = _RandomTransformerBlock(
|
|
121
|
+
X_arr.shape[1],
|
|
122
|
+
int(self.n_hidden),
|
|
123
|
+
float(self.input_scale),
|
|
124
|
+
int(self.transformer_layers),
|
|
125
|
+
int(self.num_heads),
|
|
126
|
+
float(self.dropout),
|
|
127
|
+
).to(self.device)
|
|
128
|
+
self.block_.eval()
|
|
129
|
+
hidden = self._hidden(X_arr)
|
|
130
|
+
design = self._design(X_arr, hidden)
|
|
131
|
+
self.coef_ = _ridge_solve(design, y_arr, float(self.regularization))
|
|
132
|
+
self.n_features_in_ = X_arr.shape[1]
|
|
133
|
+
self.n_outputs_ = y_arr.shape[1]
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def predict(self, X: np.ndarray | Iterable[float]) -> np.ndarray:
|
|
137
|
+
self._check_fitted()
|
|
138
|
+
X_arr = _as_2d(X, "X")
|
|
139
|
+
if X_arr.shape[1] != self.n_features_in_:
|
|
140
|
+
raise ValueError(f"Expected {self.n_features_in_} features, got {X_arr.shape[1]}.")
|
|
141
|
+
pred = self._design(X_arr, self._hidden(X_arr)) @ self.coef_
|
|
142
|
+
return pred.ravel() if self.n_outputs_ == 1 else pred
|
|
143
|
+
|
|
144
|
+
def _hidden(self, X: np.ndarray) -> np.ndarray:
|
|
145
|
+
with torch.no_grad():
|
|
146
|
+
tensor = torch.as_tensor(X, dtype=torch.float32, device=self.device)
|
|
147
|
+
return self.block_(tensor).cpu().numpy().astype(np.float64)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _design(X: np.ndarray, hidden: np.ndarray) -> np.ndarray:
|
|
151
|
+
return np.hstack([hidden, X.astype(np.float64), np.ones((X.shape[0], 1))])
|
|
152
|
+
|
|
153
|
+
def _check_fitted(self) -> None:
|
|
154
|
+
if not hasattr(self, "coef_"):
|
|
155
|
+
raise RuntimeError("The model must be fitted before prediction.")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class EDRFTRegressor:
|
|
159
|
+
"""Ensemble deep RFT regressor with one ridge readout per layer."""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
n_layers: int = 3,
|
|
164
|
+
n_hidden: int = 64,
|
|
165
|
+
regularization: float = 1e-3,
|
|
166
|
+
input_scale: float = 0.1,
|
|
167
|
+
transformer_layers: int = 1,
|
|
168
|
+
num_heads: int = 1,
|
|
169
|
+
dropout: float = 0.0,
|
|
170
|
+
aggregation: Aggregation = "median",
|
|
171
|
+
random_state: int | None = None,
|
|
172
|
+
device: str = "cpu",
|
|
173
|
+
layer_params: list[RFTLayerParams | dict] | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
self.n_layers = n_layers
|
|
176
|
+
self.n_hidden = n_hidden
|
|
177
|
+
self.regularization = regularization
|
|
178
|
+
self.input_scale = input_scale
|
|
179
|
+
self.transformer_layers = transformer_layers
|
|
180
|
+
self.num_heads = num_heads
|
|
181
|
+
self.dropout = dropout
|
|
182
|
+
self.aggregation = aggregation
|
|
183
|
+
self.random_state = random_state
|
|
184
|
+
self.device = device
|
|
185
|
+
self.layer_params = layer_params
|
|
186
|
+
|
|
187
|
+
def fit(self, X: np.ndarray | Iterable[float], y: np.ndarray | Iterable[float]) -> "EDRFTRegressor":
|
|
188
|
+
X_arr = _as_2d(X, "X")
|
|
189
|
+
y_arr = _as_2d(y, "y").astype(np.float64)
|
|
190
|
+
if X_arr.shape[0] != y_arr.shape[0]:
|
|
191
|
+
raise ValueError("X and y must contain the same number of samples.")
|
|
192
|
+
|
|
193
|
+
torch.manual_seed(0 if self.random_state is None else int(self.random_state))
|
|
194
|
+
state = X_arr
|
|
195
|
+
self.layers_ = []
|
|
196
|
+
for params in self._resolved_layer_params():
|
|
197
|
+
block = _RandomTransformerBlock(
|
|
198
|
+
state.shape[1],
|
|
199
|
+
params.n_hidden,
|
|
200
|
+
params.input_scale,
|
|
201
|
+
params.transformer_layers,
|
|
202
|
+
params.num_heads,
|
|
203
|
+
params.dropout,
|
|
204
|
+
).to(self.device)
|
|
205
|
+
block.eval()
|
|
206
|
+
with torch.no_grad():
|
|
207
|
+
hidden = block(torch.as_tensor(state, dtype=torch.float32, device=self.device))
|
|
208
|
+
hidden_np = hidden.cpu().numpy().astype(np.float64)
|
|
209
|
+
design = np.hstack([hidden_np, X_arr.astype(np.float64), np.ones((X_arr.shape[0], 1))])
|
|
210
|
+
coef = _ridge_solve(design, y_arr, params.regularization)
|
|
211
|
+
self.layers_.append({"block": block, "coef": coef, "params": params})
|
|
212
|
+
state = hidden_np.astype(np.float32)
|
|
213
|
+
|
|
214
|
+
self.n_features_in_ = X_arr.shape[1]
|
|
215
|
+
self.n_outputs_ = y_arr.shape[1]
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def predict(self, X: np.ndarray | Iterable[float], return_layers: bool = False) -> np.ndarray:
|
|
219
|
+
self._check_fitted()
|
|
220
|
+
X_arr = _as_2d(X, "X")
|
|
221
|
+
if X_arr.shape[1] != self.n_features_in_:
|
|
222
|
+
raise ValueError(f"Expected {self.n_features_in_} features, got {X_arr.shape[1]}.")
|
|
223
|
+
state = X_arr
|
|
224
|
+
outputs = []
|
|
225
|
+
for layer in self.layers_:
|
|
226
|
+
with torch.no_grad():
|
|
227
|
+
hidden = layer["block"](torch.as_tensor(state, dtype=torch.float32, device=self.device))
|
|
228
|
+
hidden_np = hidden.cpu().numpy().astype(np.float64)
|
|
229
|
+
design = np.hstack([hidden_np, X_arr.astype(np.float64), np.ones((X_arr.shape[0], 1))])
|
|
230
|
+
outputs.append(design @ layer["coef"])
|
|
231
|
+
state = hidden_np.astype(np.float32)
|
|
232
|
+
|
|
233
|
+
stacked = np.stack(outputs, axis=0)
|
|
234
|
+
if return_layers:
|
|
235
|
+
result = np.moveaxis(stacked, 0, 1)
|
|
236
|
+
elif self.aggregation == "mean":
|
|
237
|
+
result = np.mean(stacked, axis=0)
|
|
238
|
+
elif self.aggregation == "median":
|
|
239
|
+
result = np.median(stacked, axis=0)
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError("aggregation must be 'mean' or 'median'.")
|
|
242
|
+
return result.ravel() if self.n_outputs_ == 1 and result.ndim == 2 else result
|
|
243
|
+
|
|
244
|
+
def _resolved_layer_params(self) -> list[RFTLayerParams]:
|
|
245
|
+
if self.layer_params is None:
|
|
246
|
+
return [
|
|
247
|
+
RFTLayerParams(
|
|
248
|
+
self.n_hidden,
|
|
249
|
+
self.regularization,
|
|
250
|
+
self.input_scale,
|
|
251
|
+
self.transformer_layers,
|
|
252
|
+
self.num_heads,
|
|
253
|
+
self.dropout,
|
|
254
|
+
)
|
|
255
|
+
for _ in range(int(self.n_layers))
|
|
256
|
+
]
|
|
257
|
+
params = []
|
|
258
|
+
for item in self.layer_params:
|
|
259
|
+
params.append(item if isinstance(item, RFTLayerParams) else RFTLayerParams(**item))
|
|
260
|
+
if not params:
|
|
261
|
+
raise ValueError("layer_params cannot be empty.")
|
|
262
|
+
return params
|
|
263
|
+
|
|
264
|
+
def _check_fitted(self) -> None:
|
|
265
|
+
if not hasattr(self, "layers_"):
|
|
266
|
+
raise RuntimeError("The model must be fitted before prediction.")
|
edrft/tuning.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from hyperopt import STATUS_OK, Trials, fmin, hp, tpe
|
|
8
|
+
|
|
9
|
+
from .metrics import root_mean_squared_error
|
|
10
|
+
from .models import EDRFTRegressor, RFTLayerParams, RFTRegressor
|
|
11
|
+
|
|
12
|
+
Scorer = Callable[[np.ndarray, np.ndarray], float]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TuningResult:
|
|
17
|
+
model: Any
|
|
18
|
+
best_params: dict[str, Any]
|
|
19
|
+
best_score: float
|
|
20
|
+
history: list[dict[str, Any]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def default_rft_space(seed: int = 0) -> dict[str, Any]:
|
|
24
|
+
"""Hyperopt space matching the RFT/default edRFT search ranges."""
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"n_hidden": hp.quniform("n_hidden", 32, 1024, 32),
|
|
28
|
+
"regularization": hp.uniform("regularization", 0, 1),
|
|
29
|
+
"input_scale": hp.uniform("input_scale", 0, 1),
|
|
30
|
+
"transformer_layers": hp.choice("transformer_layers", [1, 2, 3, 4, 5]),
|
|
31
|
+
"num_heads": hp.choice("num_heads", [2, 4, 6, 8]),
|
|
32
|
+
"dropout": hp.uniform("dropout", 0, 0.5),
|
|
33
|
+
"random_state": seed,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def default_edrft_layer_space() -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"n_hidden": hp.quniform("n_hidden", 32, 1024, 32),
|
|
40
|
+
"regularization": hp.uniform("regularization", 0, 1),
|
|
41
|
+
"input_scale": hp.uniform("input_scale", 0, 1),
|
|
42
|
+
"transformer_layers": hp.choice("transformer_layers", [1, 2, 3, 4, 5]),
|
|
43
|
+
"num_heads": hp.choice("num_heads", [2, 4, 6, 8]),
|
|
44
|
+
"dropout": hp.uniform("dropout", 0, 0.5),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tune_rft(
|
|
49
|
+
X,
|
|
50
|
+
y,
|
|
51
|
+
space: dict[str, Any] | None = None,
|
|
52
|
+
scorer: Scorer = root_mean_squared_error,
|
|
53
|
+
validation_fraction: float = 0.2,
|
|
54
|
+
max_evals: int = 100,
|
|
55
|
+
random_state: int = 0,
|
|
56
|
+
refit: bool = True,
|
|
57
|
+
) -> TuningResult:
|
|
58
|
+
return _tune(
|
|
59
|
+
RFTRegressor,
|
|
60
|
+
X,
|
|
61
|
+
y,
|
|
62
|
+
default_rft_space(random_state) if space is None else space,
|
|
63
|
+
scorer,
|
|
64
|
+
validation_fraction,
|
|
65
|
+
max_evals,
|
|
66
|
+
random_state,
|
|
67
|
+
refit,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def layerwise_tune_edrft(
|
|
72
|
+
X,
|
|
73
|
+
y,
|
|
74
|
+
n_layers: int = 10,
|
|
75
|
+
layer_space: dict[str, Any] | None = None,
|
|
76
|
+
scorer: Scorer = root_mean_squared_error,
|
|
77
|
+
validation_fraction: float = 0.2,
|
|
78
|
+
max_evals: int = 100,
|
|
79
|
+
random_state: int = 0,
|
|
80
|
+
refit: bool = True,
|
|
81
|
+
fixed_params: dict[str, Any] | None = None,
|
|
82
|
+
) -> TuningResult:
|
|
83
|
+
if n_layers <= 0:
|
|
84
|
+
raise ValueError("n_layers must be positive.")
|
|
85
|
+
X_arr = np.asarray(X, dtype=np.float32)
|
|
86
|
+
y_arr = np.asarray(y, dtype=np.float32)
|
|
87
|
+
X_train, X_val, y_train, y_val = _split(X_arr, y_arr, validation_fraction)
|
|
88
|
+
selected: list[RFTLayerParams] = []
|
|
89
|
+
history = []
|
|
90
|
+
fixed_params = dict(fixed_params or {})
|
|
91
|
+
space = default_edrft_layer_space() if layer_space is None else layer_space
|
|
92
|
+
|
|
93
|
+
for layer_index in range(n_layers):
|
|
94
|
+
layer_history = []
|
|
95
|
+
|
|
96
|
+
def objective(params):
|
|
97
|
+
clean = _clean_params(params)
|
|
98
|
+
if layer_index > 0:
|
|
99
|
+
clean["n_hidden"] = selected[0].n_hidden
|
|
100
|
+
candidate = selected + [RFTLayerParams(**clean)]
|
|
101
|
+
model = EDRFTRegressor(layer_params=candidate, random_state=random_state, **fixed_params).fit(
|
|
102
|
+
X_train, y_train
|
|
103
|
+
)
|
|
104
|
+
score = float(scorer(y_val, model.predict(X_val)))
|
|
105
|
+
record = {"layer": layer_index + 1, "score": score, **clean}
|
|
106
|
+
layer_history.append(record)
|
|
107
|
+
history.append(record)
|
|
108
|
+
return {"loss": score, "status": STATUS_OK}
|
|
109
|
+
|
|
110
|
+
fmin(
|
|
111
|
+
objective,
|
|
112
|
+
space=space,
|
|
113
|
+
algo=tpe.suggest,
|
|
114
|
+
max_evals=max_evals,
|
|
115
|
+
trials=Trials(),
|
|
116
|
+
rstate=np.random.default_rng(random_state + layer_index),
|
|
117
|
+
show_progressbar=False,
|
|
118
|
+
)
|
|
119
|
+
best = min(layer_history, key=lambda item: item["score"])
|
|
120
|
+
selected.append(
|
|
121
|
+
RFTLayerParams(
|
|
122
|
+
n_hidden=int(best["n_hidden"]),
|
|
123
|
+
regularization=float(best["regularization"]),
|
|
124
|
+
input_scale=float(best["input_scale"]),
|
|
125
|
+
transformer_layers=int(best["transformer_layers"]),
|
|
126
|
+
num_heads=int(best["num_heads"]),
|
|
127
|
+
dropout=float(best["dropout"]),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
final_X, final_y = (X_arr, y_arr) if refit else (X_train, y_train)
|
|
132
|
+
best_params = {"layer_params": [param.__dict__ for param in selected], **fixed_params}
|
|
133
|
+
return TuningResult(
|
|
134
|
+
model=EDRFTRegressor(layer_params=selected, random_state=random_state, **fixed_params).fit(final_X, final_y),
|
|
135
|
+
best_params=best_params,
|
|
136
|
+
best_score=min(item["score"] for item in history if item["layer"] == n_layers),
|
|
137
|
+
history=history,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _tune(estimator_cls, X, y, space, scorer, validation_fraction, max_evals, random_state, refit):
|
|
142
|
+
X_arr = np.asarray(X, dtype=np.float32)
|
|
143
|
+
y_arr = np.asarray(y, dtype=np.float32)
|
|
144
|
+
X_train, X_val, y_train, y_val = _split(X_arr, y_arr, validation_fraction)
|
|
145
|
+
history = []
|
|
146
|
+
|
|
147
|
+
def objective(params):
|
|
148
|
+
clean = _clean_params(params)
|
|
149
|
+
model = estimator_cls(**clean).fit(X_train, y_train)
|
|
150
|
+
score = float(scorer(y_val, model.predict(X_val)))
|
|
151
|
+
history.append({"score": score, **clean})
|
|
152
|
+
return {"loss": score, "status": STATUS_OK}
|
|
153
|
+
|
|
154
|
+
fmin(
|
|
155
|
+
objective,
|
|
156
|
+
space=space,
|
|
157
|
+
algo=tpe.suggest,
|
|
158
|
+
max_evals=max_evals,
|
|
159
|
+
trials=Trials(),
|
|
160
|
+
rstate=np.random.default_rng(random_state),
|
|
161
|
+
show_progressbar=False,
|
|
162
|
+
)
|
|
163
|
+
best = min(history, key=lambda item: item["score"])
|
|
164
|
+
best_params = {key: value for key, value in best.items() if key != "score"}
|
|
165
|
+
final_X, final_y = (X_arr, y_arr) if refit else (X_train, y_train)
|
|
166
|
+
return TuningResult(
|
|
167
|
+
model=estimator_cls(**best_params).fit(final_X, final_y),
|
|
168
|
+
best_params=best_params,
|
|
169
|
+
best_score=float(best["score"]),
|
|
170
|
+
history=history,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _split(X, y, validation_fraction):
|
|
175
|
+
if not 0 < validation_fraction < 1:
|
|
176
|
+
raise ValueError("validation_fraction must be between 0 and 1.")
|
|
177
|
+
n_val = max(1, int(round(len(X) * validation_fraction)))
|
|
178
|
+
return X[:-n_val], X[-n_val:], y[:-n_val], y[-n_val:]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _clean_params(params):
|
|
182
|
+
clean = dict(params)
|
|
183
|
+
for key in ("n_hidden", "transformer_layers", "num_heads", "random_state"):
|
|
184
|
+
if key in clean:
|
|
185
|
+
clean[key] = int(clean[key])
|
|
186
|
+
if "n_hidden" in clean:
|
|
187
|
+
clean["n_hidden"] = max(1, clean["n_hidden"])
|
|
188
|
+
for key in ("regularization", "input_scale", "dropout"):
|
|
189
|
+
if key in clean:
|
|
190
|
+
clean[key] = float(clean[key])
|
|
191
|
+
return clean
|
edrft/wave.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from time import perf_counter
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .data import chronological_split, load_ndbc_wave_file
|
|
11
|
+
from .metrics import mean_absolute_percentage_error, mean_absolute_scaled_error, root_mean_squared_error
|
|
12
|
+
from .models import EDRFTRegressor, RFTRegressor
|
|
13
|
+
from .tuning import layerwise_tune_edrft, tune_rft
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class WaveRunResult:
|
|
18
|
+
station: str
|
|
19
|
+
year: str
|
|
20
|
+
seed: int
|
|
21
|
+
model: str
|
|
22
|
+
rmse: float
|
|
23
|
+
mape: float
|
|
24
|
+
mase: float
|
|
25
|
+
tuning_seconds: float
|
|
26
|
+
training_seconds: float
|
|
27
|
+
testing_seconds: float
|
|
28
|
+
best_params: dict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare_wave_supervised(
|
|
32
|
+
path: str | Path,
|
|
33
|
+
look_back: int = 48,
|
|
34
|
+
horizon: int = 4,
|
|
35
|
+
features: list[str] | None = None,
|
|
36
|
+
order: int | None = None,
|
|
37
|
+
):
|
|
38
|
+
"""Prepare NDBC wave data using wave forecasting lag and horizon settings."""
|
|
39
|
+
|
|
40
|
+
if order is not None:
|
|
41
|
+
look_back = order
|
|
42
|
+
if look_back <= 0 or horizon <= 0:
|
|
43
|
+
raise ValueError("look_back and horizon must be positive.")
|
|
44
|
+
frame = load_ndbc_wave_file(path, features=features)
|
|
45
|
+
values = frame.to_numpy(dtype=float)[1:]
|
|
46
|
+
target = frame["WVHT"].to_numpy(dtype=float)[1:]
|
|
47
|
+
n_samples = values.shape[0] - look_back - horizon + 1
|
|
48
|
+
if n_samples <= 0:
|
|
49
|
+
raise ValueError("series is too short for the requested look_back and horizon.")
|
|
50
|
+
X = np.zeros((n_samples, values.shape[1] * look_back), dtype=np.float32)
|
|
51
|
+
y = np.zeros(n_samples, dtype=np.float32)
|
|
52
|
+
for i in range(n_samples):
|
|
53
|
+
X[i] = values[i : i + look_back].ravel()
|
|
54
|
+
y[i] = target[i + look_back + horizon - 1]
|
|
55
|
+
return X, y
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_wave_experiment(
|
|
59
|
+
data_dir: str | Path = "wave",
|
|
60
|
+
stations: Iterable[str] = ("46001h",),
|
|
61
|
+
years: Iterable[str] = ("2017",),
|
|
62
|
+
seeds: Iterable[int] = (0,),
|
|
63
|
+
look_back: int = 48,
|
|
64
|
+
horizon: int = 4,
|
|
65
|
+
n_layers: int = 10,
|
|
66
|
+
max_evals: int = 100,
|
|
67
|
+
order: int | None = None,
|
|
68
|
+
) -> list[WaveRunResult]:
|
|
69
|
+
"""Run RFT and edRFT on NDBC files without writing result files."""
|
|
70
|
+
|
|
71
|
+
if order is not None:
|
|
72
|
+
look_back = order
|
|
73
|
+
data_dir = Path(data_dir)
|
|
74
|
+
results: list[WaveRunResult] = []
|
|
75
|
+
for year in years:
|
|
76
|
+
for station in stations:
|
|
77
|
+
X, y = prepare_wave_supervised(
|
|
78
|
+
data_dir / f"{station}{year}.txt.gz",
|
|
79
|
+
look_back=look_back,
|
|
80
|
+
horizon=horizon,
|
|
81
|
+
)
|
|
82
|
+
train_idx, val_idx, full_train_idx, test_idx = chronological_split(len(X), 0.1, 0.2)
|
|
83
|
+
scaled = _scaled_splits(X, y, train_idx, val_idx, full_train_idx, test_idx)
|
|
84
|
+
for seed in seeds:
|
|
85
|
+
results.append(
|
|
86
|
+
_run_rft(
|
|
87
|
+
station,
|
|
88
|
+
year,
|
|
89
|
+
seed,
|
|
90
|
+
scaled,
|
|
91
|
+
max_evals=max_evals,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
results.append(
|
|
95
|
+
_run_edrft(
|
|
96
|
+
station,
|
|
97
|
+
year,
|
|
98
|
+
seed,
|
|
99
|
+
scaled,
|
|
100
|
+
n_layers=n_layers,
|
|
101
|
+
max_evals=max_evals,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_rft(station, year, seed, scaled, max_evals):
|
|
108
|
+
train, val, full_train, test, inverse_y, history = scaled
|
|
109
|
+
tune_X = np.vstack([train[0], val[0]])
|
|
110
|
+
tune_y = np.concatenate([train[1], val[1]])
|
|
111
|
+
|
|
112
|
+
start = perf_counter()
|
|
113
|
+
tuned = tune_rft(
|
|
114
|
+
tune_X,
|
|
115
|
+
tune_y,
|
|
116
|
+
validation_fraction=len(val[1]) / len(tune_y),
|
|
117
|
+
max_evals=max_evals,
|
|
118
|
+
random_state=seed,
|
|
119
|
+
refit=False,
|
|
120
|
+
)
|
|
121
|
+
tuning_seconds = perf_counter() - start
|
|
122
|
+
|
|
123
|
+
start = perf_counter()
|
|
124
|
+
model = RFTRegressor(**tuned.best_params).fit(*full_train)
|
|
125
|
+
training_seconds = perf_counter() - start
|
|
126
|
+
|
|
127
|
+
start = perf_counter()
|
|
128
|
+
pred = model.predict(test[0])
|
|
129
|
+
testing_seconds = perf_counter() - start
|
|
130
|
+
return _result(station, year, seed, "RFT", test[1], pred, inverse_y, history, tuning_seconds, training_seconds, testing_seconds, tuned.best_params)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _run_edrft(station, year, seed, scaled, n_layers, max_evals):
|
|
134
|
+
train, val, full_train, test, inverse_y, history = scaled
|
|
135
|
+
tune_X = np.vstack([train[0], val[0]])
|
|
136
|
+
tune_y = np.concatenate([train[1], val[1]])
|
|
137
|
+
|
|
138
|
+
start = perf_counter()
|
|
139
|
+
tuned = layerwise_tune_edrft(
|
|
140
|
+
tune_X,
|
|
141
|
+
tune_y,
|
|
142
|
+
n_layers=n_layers,
|
|
143
|
+
validation_fraction=len(val[1]) / len(tune_y),
|
|
144
|
+
max_evals=max_evals,
|
|
145
|
+
random_state=seed,
|
|
146
|
+
refit=False,
|
|
147
|
+
)
|
|
148
|
+
tuning_seconds = perf_counter() - start
|
|
149
|
+
|
|
150
|
+
start = perf_counter()
|
|
151
|
+
model = EDRFTRegressor(layer_params=tuned.best_params["layer_params"], random_state=seed).fit(*full_train)
|
|
152
|
+
training_seconds = perf_counter() - start
|
|
153
|
+
|
|
154
|
+
start = perf_counter()
|
|
155
|
+
pred = model.predict(test[0])
|
|
156
|
+
testing_seconds = perf_counter() - start
|
|
157
|
+
return _result(station, year, seed, "edRFT", test[1], pred, inverse_y, history, tuning_seconds, training_seconds, testing_seconds, tuned.best_params)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _scaled_splits(X, y, train_idx, val_idx, full_train_idx, test_idx):
|
|
161
|
+
x_min = X[train_idx].min(axis=0)
|
|
162
|
+
x_range = np.maximum(X[train_idx].max(axis=0) - x_min, 1e-12)
|
|
163
|
+
y_min = y[train_idx].min()
|
|
164
|
+
y_range = max(float(y[train_idx].max() - y_min), 1e-12)
|
|
165
|
+
|
|
166
|
+
def scale_x(values):
|
|
167
|
+
return 2 * ((values - x_min) / x_range) - 1
|
|
168
|
+
|
|
169
|
+
def scale_y(values):
|
|
170
|
+
return 2 * ((values - y_min) / y_range) - 1
|
|
171
|
+
|
|
172
|
+
def inverse_y(values):
|
|
173
|
+
return ((np.asarray(values, dtype=float) + 1) / 2) * y_range + y_min
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
(scale_x(X[train_idx]), scale_y(y[train_idx])),
|
|
177
|
+
(scale_x(X[val_idx]), scale_y(y[val_idx])),
|
|
178
|
+
(scale_x(X[full_train_idx]), scale_y(y[full_train_idx])),
|
|
179
|
+
(scale_x(X[test_idx]), scale_y(y[test_idx])),
|
|
180
|
+
inverse_y,
|
|
181
|
+
y[train_idx],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _result(station, year, seed, model, y_true, y_pred, inverse_y, history, tuning, training, testing, best_params):
|
|
186
|
+
truth = inverse_y(y_true)
|
|
187
|
+
pred = inverse_y(y_pred)
|
|
188
|
+
return WaveRunResult(
|
|
189
|
+
station=station,
|
|
190
|
+
year=year,
|
|
191
|
+
seed=seed,
|
|
192
|
+
model=model,
|
|
193
|
+
rmse=root_mean_squared_error(truth, pred),
|
|
194
|
+
mape=mean_absolute_percentage_error(truth, pred),
|
|
195
|
+
mase=mean_absolute_scaled_error(truth, pred, history),
|
|
196
|
+
tuning_seconds=tuning,
|
|
197
|
+
training_seconds=training,
|
|
198
|
+
testing_seconds=testing,
|
|
199
|
+
best_params=best_params,
|
|
200
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edrft
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RFT and edRFT models for significant wave-height time-series forecasting.
|
|
5
|
+
Author: Aryan Bhambu
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/statsdl/edRFT
|
|
8
|
+
Project-URL: Issues, https://github.com/statsdl/edRFT/issues
|
|
9
|
+
Keywords: edrft,rft,rvfl,transformer,wave-height,forecasting
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.21
|
|
22
|
+
Requires-Dist: torch>=2.0
|
|
23
|
+
Provides-Extra: tuning
|
|
24
|
+
Requires-Dist: hyperopt>=0.2.7; extra == "tuning"
|
|
25
|
+
Requires-Dist: setuptools<81; extra == "tuning"
|
|
26
|
+
Provides-Extra: wave
|
|
27
|
+
Requires-Dist: hyperopt>=0.2.7; extra == "wave"
|
|
28
|
+
Requires-Dist: pandas>=1.3; extra == "wave"
|
|
29
|
+
Requires-Dist: setuptools<81; extra == "wave"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: hyperopt>=0.2.7; extra == "dev"
|
|
33
|
+
Requires-Dist: pandas>=1.3; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest; extra == "dev"
|
|
35
|
+
Requires-Dist: setuptools<81; extra == "dev"
|
|
36
|
+
Requires-Dist: twine; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# edRFT
|
|
40
|
+
|
|
41
|
+
`edrft` provides Random Vector Functional Link Transformer models for
|
|
42
|
+
significant wave-height forecasting:
|
|
43
|
+
|
|
44
|
+
- `RFTRegressor`: a shallow randomized transformer encoder with ridge readout.
|
|
45
|
+
- `EDRFTRegressor`: an ensemble deep RFT with one output layer per hidden layer.
|
|
46
|
+
- Hyperopt/TPE tuning using the default edRFT search ranges.
|
|
47
|
+
- NDBC wave forecasting experiment helpers that do not write result artifacts.
|
|
48
|
+
|
|
49
|
+
The public package uses the model naming: `RFT` and `edRFT`. Older `rft` and
|
|
50
|
+
`edrft` script names are retained only under legacy files for traceability.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
Core install:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/statsdl/edRFT.git
|
|
58
|
+
cd edRFT
|
|
59
|
+
pip install .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Wave experiment dependencies:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install ".[wave]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Development:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
pytest
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import numpy as np
|
|
79
|
+
from edrft import EDRFTRegressor, make_forecasting_frame
|
|
80
|
+
|
|
81
|
+
series = np.sin(np.linspace(0, 16, 240))
|
|
82
|
+
X, y = make_forecasting_frame(series, order=4)
|
|
83
|
+
|
|
84
|
+
model = EDRFTRegressor(n_layers=3, n_hidden=32, random_state=0)
|
|
85
|
+
model.fit(X[:180], y[:180])
|
|
86
|
+
pred = model.predict(X[180:])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Wave Forecasting Example
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python examples/run_wave_forecasting.py \
|
|
93
|
+
--data-dir wave \
|
|
94
|
+
--stations 46001h \
|
|
95
|
+
--years 2017 \
|
|
96
|
+
--seeds 0 \
|
|
97
|
+
--look-back 48 \
|
|
98
|
+
--horizon 4 \
|
|
99
|
+
--layers 10 \
|
|
100
|
+
--max-evals 100
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The runner prints metrics to stdout only. It follows the original scripts:
|
|
104
|
+
|
|
105
|
+
- NDBC features: `WDIR`, `WSPD`, `GST`, `APD`, `WVHT`
|
|
106
|
+
- Missing sentinel cleanup
|
|
107
|
+
- Default look-back window: 48
|
|
108
|
+
- Default forecasting horizon: 4
|
|
109
|
+
- Min-max scaling to `[-1, 1]`
|
|
110
|
+
- Chronological split: 70% train, 10% validation, 20% test
|
|
111
|
+
- Hyperopt/TPE tuning with 100 evaluations by default
|
|
112
|
+
- Train+validation final fit
|
|
113
|
+
- RMSE, MAPE, MASE, and timing output
|
|
114
|
+
|
|
115
|
+
## Hyperopt Tuning
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from edrft.tuning import layerwise_tune_edrft
|
|
119
|
+
|
|
120
|
+
result = layerwise_tune_edrft(
|
|
121
|
+
X,
|
|
122
|
+
y,
|
|
123
|
+
n_layers=10,
|
|
124
|
+
validation_fraction=0.1 / 0.8,
|
|
125
|
+
max_evals=100,
|
|
126
|
+
random_state=0,
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Repository Notes
|
|
131
|
+
|
|
132
|
+
Supported package code lives in `src/edrft`.
|
|
133
|
+
|
|
134
|
+
The `legacy/`, `DeepRVFL_/`, `ForecastLib.py`, and old experiment scripts are
|
|
135
|
+
retained for traceability. They are not included in the PyPI wheel and are not
|
|
136
|
+
the supported package API.
|
|
137
|
+
|
|
138
|
+
## PyPI Release
|
|
139
|
+
|
|
140
|
+
The publish workflow uses PyPI Trusted Publishing. Configure the PyPI trusted
|
|
141
|
+
publisher with:
|
|
142
|
+
|
|
143
|
+
- owner: `statsdl`
|
|
144
|
+
- repository: `edRFT`
|
|
145
|
+
- workflow: `publish.yml`
|
|
146
|
+
- environment: `pypi`
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
151
|
+
|
|
152
|
+
## Reference
|
|
153
|
+
|
|
154
|
+
If you use edRFT in your work, please cite:
|
|
155
|
+
|
|
156
|
+
```bibtex
|
|
157
|
+
@article{bhambu2025deep,
|
|
158
|
+
title={Deep random vector functional link transformer network with multiple output layers for significant wave height forecasting},
|
|
159
|
+
author={Bhambu, Aryan and Gao, Ruobin and Suganthan, Ponnuthurai Nagaratnam and Selvaraju, Natarajan},
|
|
160
|
+
journal={Applied Soft Computing},
|
|
161
|
+
pages={114136},
|
|
162
|
+
year={2025},
|
|
163
|
+
publisher={Elsevier}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
edrft/__init__.py,sha256=ZtIFqVd4Xk-kpKKGPNHZHoKokpBVIPtrRY8QIlOkUfg,551
|
|
2
|
+
edrft/data.py,sha256=LkHp-BJR_KSKIOVkUXY6jPWfYQAKZUQ4ZizwjniKcCI,2215
|
|
3
|
+
edrft/metrics.py,sha256=6q1yofHAhwD5rICVBdq5Ncqucctwo1lBwOH78o5o7rQ,982
|
|
4
|
+
edrft/models.py,sha256=GnLiTQ1A82HwVz-Na7kF8bQVRyauWnAswk12NEF_NUI,10062
|
|
5
|
+
edrft/tuning.py,sha256=vLq46ciebAfNv57FMHBenvySgD2CVBlRdHQFcMCHU7Y,6544
|
|
6
|
+
edrft/wave.py,sha256=O0u0pLTNUqkctfTxel96RJsD5j7qk5T32lhGmKQIW_0,6544
|
|
7
|
+
edrft-0.1.0.dist-info/licenses/LICENSE,sha256=o-oucML3-pP9uO0PIFnQXUcXY8i8DiyRZieAsyPJLfY,1069
|
|
8
|
+
edrft-0.1.0.dist-info/METADATA,sha256=p5VaSadudLh9HL1FqMeGyNyQ-ZtFw16B7TQi6Pk68-M,4389
|
|
9
|
+
edrft-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
edrft-0.1.0.dist-info/top_level.txt,sha256=zKAm5kZBsSXevNfPi2mpMTt9ROQo5HI2ZbBjhAm8RhE,6
|
|
11
|
+
edrft-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aryan Bhambu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
edrft
|