torchsonn 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.
- torchsonn/__init__.py +23 -0
- torchsonn/config/__init__.py +22 -0
- torchsonn/config/schemas.py +293 -0
- torchsonn/data/__init__.py +0 -0
- torchsonn/data/dataset.py +40 -0
- torchsonn/data/preprocessing.py +185 -0
- torchsonn/layer.py +184 -0
- torchsonn/logger.py +56 -0
- torchsonn/loss/__init__.py +17 -0
- torchsonn/loss/errors.py +273 -0
- torchsonn/loss/norm_mse.py +12 -0
- torchsonn/model.py +721 -0
- torchsonn/modules/__init__.py +8 -0
- torchsonn/modules/base.py +37 -0
- torchsonn/modules/soft_binner.py +28 -0
- torchsonn/neurons/__init__.py +24 -0
- torchsonn/neurons/base.py +373 -0
- torchsonn/neurons/binary.py +85 -0
- torchsonn/neurons/poly.py +124 -0
- torchsonn/optimizers/__init__.py +26 -0
- torchsonn/optimizers/adam.py +96 -0
- torchsonn/optimizers/base.py +42 -0
- torchsonn/optimizers/lbfgs.py +260 -0
- torchsonn/optimizers/newton.py +123 -0
- torchsonn/optimizers/newton_lm.py +82 -0
- torchsonn/optimizers/sgd.py +96 -0
- torchsonn/plot_model.py +171 -0
- torchsonn/schedulers/__init__.py +14 -0
- torchsonn/schedulers/base.py +11 -0
- torchsonn/schedulers/warmup_flat.py +39 -0
- torchsonn/trainer.py +2375 -0
- torchsonn/types.py +101 -0
- torchsonn/utils.py +73 -0
- torchsonn-0.1.0.dist-info/METADATA +231 -0
- torchsonn-0.1.0.dist-info/RECORD +38 -0
- torchsonn-0.1.0.dist-info/WHEEL +5 -0
- torchsonn-0.1.0.dist-info/licenses/LICENSE +21 -0
- torchsonn-0.1.0.dist-info/top_level.txt +1 -0
torchsonn/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""TorchSONN — self-organizing polynomial neural network on PyTorch.
|
|
2
|
+
|
|
3
|
+
Everything needed for a full train / infer loop is re-exported here:
|
|
4
|
+
|
|
5
|
+
from torchsonn import SONN, SONNConfig, SONNDataset, Trainer
|
|
6
|
+
|
|
7
|
+
`PlotModel` is deliberately *not* re-exported: it pulls in the optional
|
|
8
|
+
`graphviz` dependency (``pip install "torchsonn[viz]"``), which would make a
|
|
9
|
+
bare `import torchsonn` fail on a base install. It stays reachable as
|
|
10
|
+
`from torchsonn.plot_model import PlotModel`.
|
|
11
|
+
"""
|
|
12
|
+
from torchsonn.config import SONNConfig
|
|
13
|
+
from torchsonn.data.dataset import SONNDataset
|
|
14
|
+
from torchsonn.model import SONN
|
|
15
|
+
from torchsonn.trainer import Trainer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"SONN",
|
|
20
|
+
"SONNConfig",
|
|
21
|
+
"SONNDataset",
|
|
22
|
+
"Trainer",
|
|
23
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Hydra-structured config schemas for SONN.
|
|
2
|
+
|
|
3
|
+
Importing this package triggers `ConfigStore.store(name="default", node=SONNConfig)`,
|
|
4
|
+
so any subsequent `@hydra.main` / `hydra.compose` call that references
|
|
5
|
+
`- default` in its `defaults:` list resolves to the typed dataclass below.
|
|
6
|
+
"""
|
|
7
|
+
from torchsonn.config.schemas import (
|
|
8
|
+
ModelConfig,
|
|
9
|
+
OptimizerConfig,
|
|
10
|
+
SchedulerConfig,
|
|
11
|
+
SONNConfig,
|
|
12
|
+
TrainConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ModelConfig",
|
|
18
|
+
"OptimizerConfig",
|
|
19
|
+
"SchedulerConfig",
|
|
20
|
+
"SONNConfig",
|
|
21
|
+
"TrainConfig",
|
|
22
|
+
]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Structured-config dataclass schemas for SONN.
|
|
2
|
+
|
|
3
|
+
These mirror the keys that previously lived in `conf/default.yaml`, with two
|
|
4
|
+
upgrades over a plain YAML default:
|
|
5
|
+
|
|
6
|
+
• Type-checked field values — `omegaconf` rejects `model.type: 42` at
|
|
7
|
+
compose time instead of yielding cryptic AttributeErrors later.
|
|
8
|
+
• Schema discoverable via Python introspection — `dataclasses.fields(...)`,
|
|
9
|
+
IDE autocomplete, Sphinx autodoc all work.
|
|
10
|
+
|
|
11
|
+
Polymorphic / dynamically-typed fields are deliberately left as `List[Any]`
|
|
12
|
+
or `Optional[Dict[str, Any]]`:
|
|
13
|
+
|
|
14
|
+
• `ModelConfig.ref_functions` — heterogeneous list (bare names, mappings
|
|
15
|
+
carrying options); see `_parse_ref_function_entry` in model.py for the
|
|
16
|
+
parser. Schema-typing it stricter would force every entry into the
|
|
17
|
+
dict form, which would uglify the common `- linear_cov` case.
|
|
18
|
+
• `SchedulerConfig.scheduler_params` — varies per scheduler family.
|
|
19
|
+
• `TrainConfig.criterion_type` — string here (e.g. `"validate"`); coerced
|
|
20
|
+
to `CriterionType` enum inside `SONN.__init__` via `CriterionType.get`.
|
|
21
|
+
"""
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
from hydra.core.config_store import ConfigStore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Optimizer / scheduler — referenced from TrainConfig
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
@dataclass
|
|
32
|
+
class OutProjTrainConfig:
|
|
33
|
+
max_steps: int = 5000
|
|
34
|
+
lr: float = 1.0e-3
|
|
35
|
+
weight_decay: float = 0.0
|
|
36
|
+
# 'adam' | 'sgd' | 'lbfgs'. lbfgs runs full-batch with a closure and ignores
|
|
37
|
+
# the ReduceLROnPlateau schedule (lr_patience / lr_factor / lr_min) — it does
|
|
38
|
+
# its own line search. weight_decay is honored for lbfgs as a manual L2
|
|
39
|
+
# penalty added inside the closure.
|
|
40
|
+
optimizer: str = "adam"
|
|
41
|
+
eval_interval: int = 100
|
|
42
|
+
lr_patience: int = 5
|
|
43
|
+
lr_factor: float = 0.5
|
|
44
|
+
lr_min: float = 1.0e-5
|
|
45
|
+
early_stop_patience: int = 10
|
|
46
|
+
early_stop_min_delta: float = 1.0e-4
|
|
47
|
+
|
|
48
|
+
# LBFGS-only tuning. history_size matches PyTorch's default; max_iter is the
|
|
49
|
+
# number of inner LBFGS iterations per opt.step() call. For Otto-sized
|
|
50
|
+
# problems max_steps=100 outer x max_iter=20 inner ≈ sklearn's default.
|
|
51
|
+
lbfgs_history_size: int = 20
|
|
52
|
+
lbfgs_max_iter: int = 20
|
|
53
|
+
lbfgs_line_search: str = "strong_wolfe" # "" / None to disable
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _default_optimizer_params() -> Dict[str, Any]:
|
|
57
|
+
"""Shared optimizer/trainer kwargs.
|
|
58
|
+
|
|
59
|
+
The trainer pops `min_lr` and `gamma` (LR-drop schedule) before handing
|
|
60
|
+
the rest to the chosen optimizer class. The optimizer-specific extras
|
|
61
|
+
(`betas`/`eps` for adam, `momentum`/`nesterov` for sgd, `history_size`
|
|
62
|
+
for lbfgs, `damping`/`max_damping` for newton-LM) are NOT enumerated
|
|
63
|
+
here — see the note on `OptimizerConfig.optimizer_params` below.
|
|
64
|
+
"""
|
|
65
|
+
return {
|
|
66
|
+
"lr": 1.0e-4,
|
|
67
|
+
"min_lr": 1.0e-5,
|
|
68
|
+
"gamma": 0.5,
|
|
69
|
+
"clip_value": 1.0,
|
|
70
|
+
"clip_norm": 5.0,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class OptimizerConfig:
|
|
76
|
+
# 'adam' | 'sgd' | 'lbfgs' | 'newton' | 'newton-lm' — see optimizer_map
|
|
77
|
+
# in src/optimizers/__init__.py.
|
|
78
|
+
name: str = "adam"
|
|
79
|
+
verbose: bool = True
|
|
80
|
+
# Deliberately typed `Dict[str, Any]` rather than a nested dataclass:
|
|
81
|
+
# each optimizer family takes its own kwargs (lbfgs needs
|
|
82
|
+
# `history_size`, adam takes `betas`/`eps`, sgd takes `momentum`/
|
|
83
|
+
# `nesterov`, newton-lm takes `damping`/`max_damping`). A strict union
|
|
84
|
+
# schema would force every YAML to set every field. The optimizer
|
|
85
|
+
# constructor itself rejects unknown kwargs at instantiation time, so
|
|
86
|
+
# validation happens at the right boundary.
|
|
87
|
+
#
|
|
88
|
+
# If you want strict per-family typing later, switch to Hydra config
|
|
89
|
+
# groups: cs.store(group="optimizer", name="adam", node=AdamParams) and
|
|
90
|
+
# reference via `defaults: [{optimizer: adam}, default, _self_]`.
|
|
91
|
+
optimizer_params: Dict[str, Any] = field(default_factory=_default_optimizer_params)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class SchedulerConfig:
|
|
96
|
+
# 'warmup_flat' (currently the only registered scheduler) | null to
|
|
97
|
+
# disable the scheduler entirely.
|
|
98
|
+
name: Optional[str] = None
|
|
99
|
+
# Schema is loose here — each scheduler family takes its own kwargs.
|
|
100
|
+
scheduler_params: Optional[Dict[str, Any]] = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Model
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
@dataclass
|
|
107
|
+
class ModelConfig:
|
|
108
|
+
# 'regressor' | 'binary' | 'multi-class'
|
|
109
|
+
type: str = "multi-class"
|
|
110
|
+
soft_binner: bool = True
|
|
111
|
+
soft_binner_scale: float = 100.0
|
|
112
|
+
num_classes: int = 3
|
|
113
|
+
|
|
114
|
+
# Heterogeneous polymorphic list — see _parse_ref_function_entry.
|
|
115
|
+
ref_functions: List[Any] = field(default_factory=lambda: ["linear_cov"])
|
|
116
|
+
|
|
117
|
+
shortcut: bool = True
|
|
118
|
+
normalize: bool = True
|
|
119
|
+
|
|
120
|
+
# Required by the caller (tutorial YAMLs override). Left as Optional so
|
|
121
|
+
# the schema can still load with the library default before the caller
|
|
122
|
+
# supplies a value; SONN.__init__ asserts `nbest_neurons > 1`.
|
|
123
|
+
nbest_neurons: Optional[int] = None
|
|
124
|
+
max_neuron_models: Optional[int] = None
|
|
125
|
+
|
|
126
|
+
use_output_projection: bool = False
|
|
127
|
+
num_out_neurons: Optional[int] = None
|
|
128
|
+
|
|
129
|
+
# Per-neuron linear projection: each neuron in the ensemble gets its own
|
|
130
|
+
# nn.Linear(1, num_classes) trained with in_dims=0 (vs shared_proj which
|
|
131
|
+
# uses in_dims=None). Mutually exclusive with soft_binner.
|
|
132
|
+
use_neuron_proj: bool = False
|
|
133
|
+
|
|
134
|
+
# torch.clamp threshold applied between layers. Raise above the worst-
|
|
135
|
+
# case |xi*xj| at xavier init when feeding heavy-tailed unclipped
|
|
136
|
+
# features (e.g. California-housing Population).
|
|
137
|
+
output_clamp_value: float = 1000.0
|
|
138
|
+
|
|
139
|
+
# Apply nn.LayerNorm(d_layer, elementwise_affine=False) to each layer's
|
|
140
|
+
# post-clamp output, before the optional shortcut concat. Standardizes
|
|
141
|
+
# the (batch, nbest_neurons) feature map so the next layer's polynomial
|
|
142
|
+
# neurons see comparable scales regardless of how heavy-tailed the
|
|
143
|
+
# previous layer's polynomial happens to be. No trainable params — pure
|
|
144
|
+
# per-sample standardization, identical to the preprocessing LayerNorm
|
|
145
|
+
# that the Otto tutorial puts on the model's raw input.
|
|
146
|
+
use_layer_norm: bool = False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Train
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
@dataclass
|
|
153
|
+
class TrainConfig:
|
|
154
|
+
seed: int = 10
|
|
155
|
+
|
|
156
|
+
# String form — coerced to CriterionType via CriterionType.get() in
|
|
157
|
+
# SONN.__init__. Accepted: 'validate' | 'bias' | 'validate_bias' | 'bias_retrain'.
|
|
158
|
+
criterion_type: str = "validate"
|
|
159
|
+
# Bias criterion variant for multi-class: 'l2' (L2 on logits) | 'js' (Jensen-Shannon divergence).
|
|
160
|
+
bias_ce_type: str = "js"
|
|
161
|
+
error_alpha: float = 0.5
|
|
162
|
+
|
|
163
|
+
max_layer_count: int = 999
|
|
164
|
+
criterion_minimum_width: int = 5
|
|
165
|
+
stop_train_epsilon_condition: float = 0.001
|
|
166
|
+
|
|
167
|
+
manual_best_neurons_selection: bool = False
|
|
168
|
+
min_best_neurons_count: int = 0
|
|
169
|
+
max_best_neurons_count: int = 0
|
|
170
|
+
|
|
171
|
+
# 'top' (smallest err_value wins) | 'avg'.
|
|
172
|
+
layer_err_criterion: str = "top"
|
|
173
|
+
|
|
174
|
+
# Algorithm used by train_layer → neuron_selection to pick survivors from
|
|
175
|
+
# the candidate pool:
|
|
176
|
+
# 'plain' — top-k by individual error (historical default).
|
|
177
|
+
# 'omp_mixed' — walk candidates in error order, drop ones whose residual
|
|
178
|
+
# (after Gram-Schmidt vs. already-selected survivors on the
|
|
179
|
+
# dev set) falls below `neuron_selection_orth_threshold` of
|
|
180
|
+
# its original norm. Trades a small NCE bump for
|
|
181
|
+
# decorrelated outputs feeding the next layer.
|
|
182
|
+
# 'omp' — classical Orthogonal Matching Pursuit: at every step pick
|
|
183
|
+
# the candidate whose orthogonalized residual has the
|
|
184
|
+
# largest norm. Ignores `err` entirely; purely decorrelation-
|
|
185
|
+
# driven, vectorized over the candidate axis.
|
|
186
|
+
neuron_selection_method: str = "plain"
|
|
187
|
+
# Used only by the 'omp_mixed' variant. 0.3 = "keep candidate if at least
|
|
188
|
+
# 30 % of its centered norm survives orthogonalization."
|
|
189
|
+
neuron_selection_orth_threshold: float = 0.3
|
|
190
|
+
|
|
191
|
+
eval_step_interval: int = 1000
|
|
192
|
+
eval_smoothing_factor: float = 0.2
|
|
193
|
+
|
|
194
|
+
save_interval: int = 1000
|
|
195
|
+
save_last_layer: bool = True
|
|
196
|
+
keep_last_n: int = 10
|
|
197
|
+
skip_saving_at_epoch_end: bool = True
|
|
198
|
+
checkpoint_dir: str = ""
|
|
199
|
+
|
|
200
|
+
early_stop_completion_percentage: int = 100
|
|
201
|
+
early_stop_patience: float = 1.0e-4
|
|
202
|
+
early_stop_tolerance_steps: int = 10
|
|
203
|
+
|
|
204
|
+
shared_proj_lr_multiplier: float = 0.1
|
|
205
|
+
|
|
206
|
+
# When True, train_layer runs every frozen layer over the full dataset
|
|
207
|
+
# once and caches the resulting features in CPU-backed DataLoaders, so
|
|
208
|
+
# the inner training step skips the SONN forward. Speeds training up but
|
|
209
|
+
# caches (N_samples, d_layer) floats per split, which can spike CPU RAM
|
|
210
|
+
# at deeper layers. False = recompute features per step (the previous
|
|
211
|
+
# behavior); trades wallclock for memory.
|
|
212
|
+
precompute_features: bool = False
|
|
213
|
+
|
|
214
|
+
# When True, log per-layer survivor-pool diagnostics (off-diagonal
|
|
215
|
+
# correlation stats + top singular values + effective rank) on the dev
|
|
216
|
+
# split after neuron_selection. Streaming O(D²) implementation — safe on
|
|
217
|
+
# large dev sets — but still adds one extra full forward pass over dev
|
|
218
|
+
# per layer, so leave off in production runs.
|
|
219
|
+
log_layer_diagnostics: bool = False
|
|
220
|
+
|
|
221
|
+
# Absolute val-loss threshold for permanently flagging a diverged
|
|
222
|
+
# neuron. `.inf` disables the guard (matches gmdhpy).
|
|
223
|
+
divergence_threshold: float = float("inf")
|
|
224
|
+
|
|
225
|
+
# L2 (ridge) penalty on the per-neuron polynomial `weight` during the
|
|
226
|
+
# OLS-style fit. The training loss for each candidate neuron becomes
|
|
227
|
+
# loss = data_fit + ridge_alpha * (params["weight"] ** 2).sum()
|
|
228
|
+
# Only the polynomial coefficients are penalized — projection weights
|
|
229
|
+
# (proj_weight / proj_bias / shared_proj_*) stay unregularized.
|
|
230
|
+
# Recommended range: 0.01–0.1 for higher-arity primitives (cubic / poly
|
|
231
|
+
# quad dim≥4) on small datasets where over-fit is the dominant gap.
|
|
232
|
+
# Evaluation and prediction partials always see ridge_alpha=0 so the
|
|
233
|
+
# reported dev / test metrics measure pure predictive quality, not the
|
|
234
|
+
# penalized training objective. Default 0.0 = ridge disabled.
|
|
235
|
+
ridge_alpha: float = 0.0
|
|
236
|
+
|
|
237
|
+
optimizer: OptimizerConfig = field(default_factory=OptimizerConfig)
|
|
238
|
+
scheduler: SchedulerConfig = field(default_factory=SchedulerConfig)
|
|
239
|
+
out_proj_train: OutProjTrainConfig = field(default_factory=OutProjTrainConfig)
|
|
240
|
+
|
|
241
|
+
# When True, train_layer runs an extra per-layer fine-tune pass after
|
|
242
|
+
# neuron_selection: jointly trains the surviving neurons' polynomial
|
|
243
|
+
# `weight`s together with a temporary (d_layer, num_classes) Linear head
|
|
244
|
+
# against the class-weighted CE on the dev split. Refines the polynomial
|
|
245
|
+
# coefficients so they're CE-aligned before the next layer trains on top
|
|
246
|
+
# of them. Hyperparameters are shared with `out_proj_train` to keep the
|
|
247
|
+
# config surface small. Skipped on the planned last layer
|
|
248
|
+
# (layer_index == max_layer_count - 1) since the subsequent
|
|
249
|
+
# train_out_proj pass effectively replaces it.
|
|
250
|
+
layer_finetune: bool = False
|
|
251
|
+
|
|
252
|
+
device: str = "cpu"
|
|
253
|
+
dtype: str = "float32"
|
|
254
|
+
batch_size: int = 1
|
|
255
|
+
steps: int = 1000
|
|
256
|
+
shuffle: bool = False
|
|
257
|
+
normalize: bool = True
|
|
258
|
+
|
|
259
|
+
train_loss_tol: float = 0.001
|
|
260
|
+
train_loss_window: int = 20
|
|
261
|
+
|
|
262
|
+
verbose: bool = True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Top-level SONN config
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
@dataclass
|
|
269
|
+
class SONNConfig:
|
|
270
|
+
model: ModelConfig = field(default_factory=ModelConfig)
|
|
271
|
+
train: TrainConfig = field(default_factory=TrainConfig)
|
|
272
|
+
|
|
273
|
+
# Tutorial-level workflow flags. Included in the base schema so the
|
|
274
|
+
# `defaults: [default, _self_]` composition pattern in tutorial YAMLs
|
|
275
|
+
# doesn't trip strict-mode "unknown key" errors. Each tutorial reads
|
|
276
|
+
# only the flag(s) it cares about.
|
|
277
|
+
resume: bool = False
|
|
278
|
+
train_on_first_half: bool = False # gmdhpy-style split toggle (CA only)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Register the schema with Hydra's ConfigStore.
|
|
283
|
+
#
|
|
284
|
+
# `name="default"` makes `- default` resolvable in any tutorial YAML's
|
|
285
|
+
# `defaults:` list, e.g.:
|
|
286
|
+
#
|
|
287
|
+
# defaults:
|
|
288
|
+
# - default # SONNConfig
|
|
289
|
+
# - _self_
|
|
290
|
+
# model:
|
|
291
|
+
# type: regressor
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
ConfigStore.instance().store(name="default", node=SONNConfig)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from torch.utils.data import Dataset
|
|
7
|
+
|
|
8
|
+
# Anything that supports `arr.shape[0]` and integer indexing — numpy arrays
|
|
9
|
+
# and torch tensors are the two real use cases.
|
|
10
|
+
ArrayLike = np.ndarray | torch.Tensor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SONNDataset(Dataset):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
x: ArrayLike,
|
|
17
|
+
target: ArrayLike | None,
|
|
18
|
+
split: int | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.x = x
|
|
21
|
+
self.target = target
|
|
22
|
+
self.split = split
|
|
23
|
+
assert split in [0, 1, None]
|
|
24
|
+
|
|
25
|
+
def __len__(self) -> int:
|
|
26
|
+
length = self.x.shape[0]
|
|
27
|
+
if self.split == 0:
|
|
28
|
+
length = (length + 1) // 2
|
|
29
|
+
elif self.split == 1:
|
|
30
|
+
length = length // 2
|
|
31
|
+
return length
|
|
32
|
+
|
|
33
|
+
def __getitem__(self, idx: int) -> tuple[Any, Any]:
|
|
34
|
+
if self.split == 0:
|
|
35
|
+
idx = 2 * idx
|
|
36
|
+
elif self.split == 1:
|
|
37
|
+
idx = 2 * idx + 1
|
|
38
|
+
x = self.x[idx]
|
|
39
|
+
y = self.target[idx] if self.target is not None else None
|
|
40
|
+
return x, y
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataSetType(IntEnum):
|
|
9
|
+
"""Tag attached to each row of a dataset by `set_split_types`."""
|
|
10
|
+
dsTrain = 0
|
|
11
|
+
dsValidate = 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SequenceTypeSet(IntEnum):
|
|
15
|
+
"""How rows are assigned to train vs. validate.
|
|
16
|
+
|
|
17
|
+
sqRandom — random Bernoulli per row.
|
|
18
|
+
sqMode1 — every 2nd row is validate.
|
|
19
|
+
sqMode3_1 — every 3rd row is validate.
|
|
20
|
+
sqMode4_1 — every 4th row is validate.
|
|
21
|
+
sqMode2 — every 2nd row is train (inverse of sqMode1).
|
|
22
|
+
sqMode3_2 — every 3rd row is train (inverse of sqMode3_1).
|
|
23
|
+
sqMode4_2 — every 4th row is train (inverse of sqMode4_1).
|
|
24
|
+
"""
|
|
25
|
+
sqRandom = 0
|
|
26
|
+
sqMode1 = 1
|
|
27
|
+
sqMode3_1 = 2
|
|
28
|
+
sqMode4_1 = 3
|
|
29
|
+
sqMode2 = 4
|
|
30
|
+
sqMode3_2 = 5
|
|
31
|
+
sqMode4_2 = 6
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_mode1_type(cls, seq_type: "SequenceTypeSet") -> bool:
|
|
35
|
+
return seq_type in (cls.sqMode1, cls.sqMode3_1, cls.sqMode4_1)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def is_mode2_type(cls, seq_type: "SequenceTypeSet") -> bool:
|
|
39
|
+
return seq_type in (cls.sqMode2, cls.sqMode3_2, cls.sqMode4_2)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def train_preprocessing(
|
|
43
|
+
data_x: np.ndarray | pd.DataFrame,
|
|
44
|
+
data_y: np.ndarray | pd.DataFrame | pd.Series,
|
|
45
|
+
feature_names: Sequence[str] | None,
|
|
46
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
47
|
+
"""process of train input data: transform to numpy matrix, transpose etc"""
|
|
48
|
+
|
|
49
|
+
if isinstance(data_x, pd.DataFrame):
|
|
50
|
+
data_x = data_x.to_numpy()
|
|
51
|
+
|
|
52
|
+
if isinstance(data_y, pd.DataFrame) or isinstance(data_y, pd.Series):
|
|
53
|
+
data_y = data_y.to_numpy()
|
|
54
|
+
|
|
55
|
+
if not isinstance(data_y, (np.ndarray, np.generic)):
|
|
56
|
+
data_y = np.asarray(data_y)
|
|
57
|
+
if len(data_y.shape) != 1:
|
|
58
|
+
if data_y.shape[0] == 1:
|
|
59
|
+
data_y = data_y[0, :]
|
|
60
|
+
elif data_y.shape[1] == 1:
|
|
61
|
+
data_y = data_y[:, 0]
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError('data_y dimension should be 1 or (n, 1) or (1, n)')
|
|
64
|
+
data_len = data_y.shape[0]
|
|
65
|
+
|
|
66
|
+
if not isinstance(data_x, (np.ndarray, np.generic)):
|
|
67
|
+
data_x = np.asarray(data_x)
|
|
68
|
+
|
|
69
|
+
if len(data_x.shape) != 2:
|
|
70
|
+
raise ValueError('data_x dimension has to be 2. it has to be a 2D numpy array: number of features x '
|
|
71
|
+
'number of observations')
|
|
72
|
+
|
|
73
|
+
if data_x.shape[0] != data_len:
|
|
74
|
+
# try to check if transpose matrix is suitable
|
|
75
|
+
if data_x.shape[1] == data_len:
|
|
76
|
+
# ok, need to transpose data_x
|
|
77
|
+
data_x = data_x.transpose()
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError('number of examples in data_x is not equal to number of examples in data_y')
|
|
80
|
+
|
|
81
|
+
if data_x.shape[1] < 2:
|
|
82
|
+
raise ValueError('Error: number of features should be not less than two')
|
|
83
|
+
|
|
84
|
+
if data_x.shape[0] < 2:
|
|
85
|
+
raise ValueError('Error: number of samples should be not less than two')
|
|
86
|
+
|
|
87
|
+
if feature_names is not None:
|
|
88
|
+
feature_names_len = len(feature_names)
|
|
89
|
+
if feature_names_len > 0 and feature_names_len != data_x.shape[1]:
|
|
90
|
+
raise ValueError('Error: size of feature_names list is not equal to number of features')
|
|
91
|
+
|
|
92
|
+
return data_x, data_y
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def predict_preprocessing(
|
|
96
|
+
data_x: np.ndarray | pd.DataFrame,
|
|
97
|
+
n_features: int,
|
|
98
|
+
) -> tuple[np.ndarray, int]:
|
|
99
|
+
"""process of predict input data: transform to numpy matrix, transpose etc"""
|
|
100
|
+
|
|
101
|
+
if isinstance(data_x, pd.DataFrame):
|
|
102
|
+
data_x = data_x.to_numpy()
|
|
103
|
+
|
|
104
|
+
if not isinstance(data_x, (np.ndarray, np.generic)):
|
|
105
|
+
data_x = np.asarray(data_x)
|
|
106
|
+
|
|
107
|
+
if len(data_x.shape) != 2:
|
|
108
|
+
raise ValueError('data_x dimension has to be 2. it has to be a 2D numpy array: number of features x '
|
|
109
|
+
'number of observations')
|
|
110
|
+
|
|
111
|
+
if data_x.shape[1] != n_features:
|
|
112
|
+
# try to check if transpose matrix is suitable
|
|
113
|
+
if data_x.shape[0] == n_features:
|
|
114
|
+
# ok, need to transpose data_x
|
|
115
|
+
data_x = data_x.transpose()
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError('number of features in data_x is not equal to number of features in trained model')
|
|
118
|
+
|
|
119
|
+
data_len = data_x.shape[0]
|
|
120
|
+
return data_x, data_len
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def set_split_types(seq_type: SequenceTypeSet, data_len: int) -> np.ndarray:
|
|
124
|
+
"""
|
|
125
|
+
Set seq_types array that will be used to divide data set to train and validate ones
|
|
126
|
+
"""
|
|
127
|
+
# DataSetType is IntEnum, so numeric comparison works against int8 storage.
|
|
128
|
+
seq_types = np.empty((data_len,), dtype=np.int8)
|
|
129
|
+
|
|
130
|
+
if seq_type == SequenceTypeSet.sqRandom:
|
|
131
|
+
r = np.random.uniform(-1, 1, data_len)
|
|
132
|
+
seq_types[:] = np.where(r > 0, DataSetType.dsTrain, DataSetType.dsValidate)
|
|
133
|
+
return seq_types
|
|
134
|
+
|
|
135
|
+
elif seq_type == SequenceTypeSet.sqMode1:
|
|
136
|
+
n = 2
|
|
137
|
+
elif seq_type == SequenceTypeSet.sqMode3_1:
|
|
138
|
+
n = 3
|
|
139
|
+
elif seq_type == SequenceTypeSet.sqMode4_1:
|
|
140
|
+
n = 4
|
|
141
|
+
elif seq_type == SequenceTypeSet.sqMode2:
|
|
142
|
+
n = 2
|
|
143
|
+
elif seq_type == SequenceTypeSet.sqMode3_2:
|
|
144
|
+
n = 3
|
|
145
|
+
elif seq_type == SequenceTypeSet.sqMode4_2:
|
|
146
|
+
n = 4
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError('Unknown type of data division into train and validate sequences')
|
|
149
|
+
|
|
150
|
+
if SequenceTypeSet.is_mode1_type(seq_type):
|
|
151
|
+
for i in range(data_len, 0, -1):
|
|
152
|
+
if (data_len-i) % n == 0:
|
|
153
|
+
seq_types[i-1] = DataSetType.dsValidate
|
|
154
|
+
else:
|
|
155
|
+
seq_types[i-1] = DataSetType.dsTrain
|
|
156
|
+
|
|
157
|
+
if SequenceTypeSet.is_mode2_type(seq_type):
|
|
158
|
+
for i in range(data_len, 0, -1):
|
|
159
|
+
if (data_len-i) % n == 0:
|
|
160
|
+
seq_types[i-1] = DataSetType.dsTrain
|
|
161
|
+
else:
|
|
162
|
+
seq_types[i-1] = DataSetType.dsValidate
|
|
163
|
+
return seq_types
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def split_dataset(
|
|
167
|
+
data_x: np.ndarray,
|
|
168
|
+
data_y: np.ndarray,
|
|
169
|
+
seq_type: SequenceTypeSet,
|
|
170
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
171
|
+
"""Split train and validate data sets from input data set and target
|
|
172
|
+
"""
|
|
173
|
+
data_len = data_x.shape[0]
|
|
174
|
+
|
|
175
|
+
seq_types = set_split_types(seq_type, data_len)
|
|
176
|
+
|
|
177
|
+
idx_train = np.extract(DataSetType.dsTrain == seq_types, np.arange(data_len))
|
|
178
|
+
input_train_x = data_x[idx_train, :]
|
|
179
|
+
train_y = data_y[idx_train]
|
|
180
|
+
|
|
181
|
+
idx_validate = np.extract(DataSetType.dsValidate == seq_types, np.arange(data_len))
|
|
182
|
+
input_validate_x = data_x[idx_validate, :]
|
|
183
|
+
validate_y = data_y[idx_validate]
|
|
184
|
+
|
|
185
|
+
return input_train_x, train_y, input_validate_x, validate_y
|