mostlyai-engine 1.0.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.
Files changed (45) hide show
  1. mostlyai/engine/__init__.py +27 -0
  2. mostlyai/engine/_common.py +612 -0
  3. mostlyai/engine/_dtypes.py +58 -0
  4. mostlyai/engine/_encoding_types/__init__.py +13 -0
  5. mostlyai/engine/_encoding_types/language/__init__.py +13 -0
  6. mostlyai/engine/_encoding_types/language/text.py +41 -0
  7. mostlyai/engine/_encoding_types/tabular/__init__.py +13 -0
  8. mostlyai/engine/_encoding_types/tabular/categorical.py +115 -0
  9. mostlyai/engine/_encoding_types/tabular/character.py +155 -0
  10. mostlyai/engine/_encoding_types/tabular/datetime.py +254 -0
  11. mostlyai/engine/_encoding_types/tabular/itt.py +352 -0
  12. mostlyai/engine/_encoding_types/tabular/lat_long.py +353 -0
  13. mostlyai/engine/_encoding_types/tabular/numeric.py +548 -0
  14. mostlyai/engine/_language/__init__.py +17 -0
  15. mostlyai/engine/_language/common.py +110 -0
  16. mostlyai/engine/_language/encoding.py +162 -0
  17. mostlyai/engine/_language/engine/__init__.py +13 -0
  18. mostlyai/engine/_language/engine/base.py +51 -0
  19. mostlyai/engine/_language/engine/hf_engine.py +136 -0
  20. mostlyai/engine/_language/engine/vllm_engine.py +187 -0
  21. mostlyai/engine/_language/formatron_utils.py +114 -0
  22. mostlyai/engine/_language/generation.py +309 -0
  23. mostlyai/engine/_language/lstm.py +139 -0
  24. mostlyai/engine/_language/tokenizer_utils.py +144 -0
  25. mostlyai/engine/_language/training.py +718 -0
  26. mostlyai/engine/_memory.py +69 -0
  27. mostlyai/engine/_tabular/__init__.py +13 -0
  28. mostlyai/engine/_tabular/argn.py +1382 -0
  29. mostlyai/engine/_tabular/common.py +29 -0
  30. mostlyai/engine/_tabular/encoding.py +406 -0
  31. mostlyai/engine/_tabular/fairness.py +245 -0
  32. mostlyai/engine/_tabular/generation.py +1157 -0
  33. mostlyai/engine/_tabular/training.py +856 -0
  34. mostlyai/engine/_training_utils.py +159 -0
  35. mostlyai/engine/_workspace.py +225 -0
  36. mostlyai/engine/analysis.py +571 -0
  37. mostlyai/engine/domain.py +171 -0
  38. mostlyai/engine/encoding.py +42 -0
  39. mostlyai/engine/generation.py +110 -0
  40. mostlyai/engine/splitting.py +257 -0
  41. mostlyai/engine/training.py +104 -0
  42. mostlyai_engine-1.0.0.dist-info/METADATA +179 -0
  43. mostlyai_engine-1.0.0.dist-info/RECORD +45 -0
  44. mostlyai_engine-1.0.0.dist-info/WHEEL +4 -0
  45. mostlyai_engine-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,159 @@
1
+ # Copyright 2025 MOSTLY AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import abc
16
+ import logging
17
+ import time
18
+
19
+ from opacus.accountants import IAccountant
20
+ import pandas as pd
21
+ import torch
22
+ from pydantic import BaseModel, Field, field_validator
23
+
24
+ from mostlyai.engine._workspace import Workspace
25
+
26
+ _LOG = logging.getLogger(__name__)
27
+
28
+
29
+ class ProgressMessage(BaseModel, extra="allow"):
30
+ epoch: float | None = Field(None, description="Current epoch number")
31
+ is_checkpoint: bool | int | None = Field(0, description="Whether this progress is a checkpoint")
32
+ steps: int | None = Field(None, description="Number of processed steps")
33
+ samples: int | None = Field(None, description="Number of processed samples")
34
+ trn_loss: float | None = Field(None, description="Training loss")
35
+ val_loss: float | None = Field(None, description="Validation loss")
36
+ total_time: float | None = Field(None, description="Elapsed total time (s)")
37
+ learn_rate: float | None = Field(None, description="Learning rate")
38
+ dp_eps: float | None = Field(None, description="Differential privacy epsilon")
39
+ dp_delta: float | None = Field(None, description="Differential privacy delta")
40
+
41
+ @field_validator("epoch", "trn_loss", "val_loss", "learn_rate", "total_time", "dp_eps", "dp_delta")
42
+ @classmethod
43
+ def round_float(cls, v, info) -> float:
44
+ field_decimal_places = {
45
+ "epoch": 2,
46
+ "trn_loss": 4,
47
+ "val_loss": 4,
48
+ "learn_rate": 6,
49
+ "total_time": 1,
50
+ "dp_eps": 2,
51
+ "dp_delta": 8,
52
+ }
53
+ if isinstance(v, float) and info.field_name in field_decimal_places:
54
+ return round(v, field_decimal_places[info.field_name])
55
+ return v
56
+
57
+ @field_validator("is_checkpoint")
58
+ @classmethod
59
+ def cast_to_int(cls, v) -> int:
60
+ return int(v)
61
+
62
+
63
+ class EarlyStopper:
64
+ """
65
+ Stop training when val_loss stopped improving for a while
66
+ """
67
+
68
+ def __init__(self, val_loss_patience: int) -> None:
69
+ self.val_loss_patience = val_loss_patience
70
+ self.best_loss = float("inf")
71
+ self.val_loss_cnt = 0
72
+
73
+ def __call__(self, val_loss: float) -> bool:
74
+ do_stop = False
75
+ # check val_loss
76
+ if not pd.isna(val_loss) and val_loss < self.best_loss:
77
+ # remember best val_loss
78
+ self.best_loss = val_loss
79
+ # reset counter
80
+ self.val_loss_cnt = 0
81
+ else:
82
+ self.val_loss_cnt += 1
83
+ if self.val_loss_cnt > self.val_loss_patience:
84
+ _LOG.info("early stopping: val_loss stopped improving")
85
+ do_stop = True
86
+ return do_stop
87
+
88
+
89
+ class ModelCheckpoint(abc.ABC):
90
+ """
91
+ Save model weights for best model.
92
+ """
93
+
94
+ def __init__(self, workspace: Workspace, initial_best_val_loss: float = float("inf")) -> None:
95
+ self.workspace = workspace
96
+ self.best_val_loss = initial_best_val_loss
97
+ self.last_save_time = time.time()
98
+ self.save_count = 0
99
+
100
+ def optimizer_and_lr_scheduler_paths_exist(self) -> bool:
101
+ return self.workspace.model_optimizer_path.exists() and self.workspace.model_lr_scheduler_path.exists()
102
+
103
+ @abc.abstractmethod
104
+ def model_weights_path_exists(self) -> None:
105
+ pass
106
+
107
+ def clear_checkpoint(self):
108
+ self.workspace.model_optimizer_path.unlink(missing_ok=True)
109
+ self.workspace.model_lr_scheduler_path.unlink(missing_ok=True)
110
+ self._clear_model_weights()
111
+
112
+ def save_checkpoint_if_best(
113
+ self,
114
+ val_loss: float,
115
+ model: torch.nn.Module,
116
+ optimizer: torch.optim.Optimizer | None = None,
117
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler | None = None,
118
+ dp_accountant: IAccountant | None = None,
119
+ ) -> bool:
120
+ # save model weights if validation loss has improved
121
+ if val_loss < self.best_val_loss:
122
+ self.best_val_loss = val_loss
123
+ self.save_checkpoint(model, optimizer, lr_scheduler, dp_accountant)
124
+ return True
125
+ else:
126
+ return False
127
+
128
+ def save_checkpoint(
129
+ self,
130
+ model: torch.nn.Module,
131
+ optimizer: torch.optim.Optimizer | None = None,
132
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler | None = None,
133
+ dp_accountant: IAccountant | None = None,
134
+ ) -> None:
135
+ if optimizer is not None and lr_scheduler is not None:
136
+ torch.save(optimizer.state_dict(), self.workspace.model_optimizer_path)
137
+ torch.save(lr_scheduler.state_dict(), self.workspace.model_lr_scheduler_path)
138
+ if dp_accountant is not None:
139
+ torch.save(dp_accountant.state_dict(), self.workspace.model_dp_accountant_path)
140
+ self._save_model_weights(model)
141
+ self.last_save_time = time.time()
142
+ self.save_count += 1
143
+
144
+ def has_saved_once(self) -> bool:
145
+ return self.save_count > 0
146
+
147
+ @abc.abstractmethod
148
+ def _save_model_weights(self, model: torch.nn.Module) -> None:
149
+ pass
150
+
151
+ @abc.abstractmethod
152
+ def _clear_model_weights(self) -> None:
153
+ pass
154
+
155
+
156
+ def check_early_training_exit(workspace: Workspace, trn_cnt: int, val_cnt: int) -> bool:
157
+ trn_files = workspace.encoded_data_trn.fetch_all()
158
+ val_files = workspace.encoded_data_val.fetch_all()
159
+ return any((len(trn_files) == 0, len(val_files) == 0, trn_cnt == 0, val_cnt == 0))
@@ -0,0 +1,225 @@
1
+ # Copyright 2025 MOSTLY AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import logging
16
+ import os
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any
20
+ from collections.abc import Callable
21
+
22
+ from mostlyai.engine._common import read_json, write_json
23
+ from mostlyai.engine.domain import ModelType
24
+
25
+ _LOG = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class PathDesc:
30
+ root_dir: Path | None = None
31
+ parts: list[str] | None = None
32
+ is_multiple: bool = False
33
+ fetch_multiple: Callable | None = None
34
+ read_handler: Callable | None = None
35
+ write_handler: Callable | None = None
36
+
37
+ def __post_init__(self):
38
+ all_parts = (list(self.root_dir.parts) if isinstance(self.root_dir, Path) else []) + (
39
+ self.parts if isinstance(self.parts, list) else []
40
+ )
41
+ self.path = Path(*all_parts)
42
+
43
+ def read(self):
44
+ if not self.read_handler:
45
+ raise RuntimeError(f"No read handler was defined for {self.path}")
46
+ return self.read_handler(self.path)
47
+
48
+ def write(self, data: Any):
49
+ if not self.write_handler:
50
+ raise RuntimeError(f"No write handler was defined for {self.path}")
51
+ return self.write_handler(data, self.path)
52
+
53
+ def fetch_all(self):
54
+ if not self.fetch_multiple:
55
+ raise RuntimeError(f"No fetch multiple handler was defined for {self.path}")
56
+ return self.fetch_multiple(self.path)
57
+
58
+
59
+ @dataclass
60
+ class JsonPathDesc(PathDesc):
61
+ read_handler: Callable | None = read_json
62
+ write_handler: Callable | None = write_json
63
+
64
+
65
+ def fetch_sorted_glob(pattern: str):
66
+ def fetch_func(path: Path):
67
+ return sorted(list(path.glob(pattern)))
68
+
69
+ return fetch_func
70
+
71
+
72
+ class Workspace:
73
+ def __init__(
74
+ self,
75
+ ws_path: str | Path,
76
+ ):
77
+ self._ws_path: Path = Path(ws_path)
78
+ self._make_workspace_objects_and_paths()
79
+
80
+ def _make_workspace_objects_and_paths(self):
81
+ fetch_part_parquets = fetch_sorted_glob("part.*.parquet")
82
+ fetch_part_trn_parquets = fetch_sorted_glob("part.*-trn.parquet")
83
+ fetch_part_val_parquets = fetch_sorted_glob("part.*-val.parquet")
84
+ fetch_part_jsons = fetch_sorted_glob("part.*.json")
85
+
86
+ def make_path_desc(path_desc_cls: type | None = None, **kwargs):
87
+ path_desc_cls = path_desc_cls or PathDesc
88
+ return path_desc_cls(root_dir=self._ws_path, **kwargs)
89
+
90
+ def make_json_path_desc(**kwargs):
91
+ return make_path_desc(path_desc_cls=JsonPathDesc, **kwargs)
92
+
93
+ def make_stats_json_path_desc(**kwargs):
94
+ return make_path_desc(path_desc_cls=JsonPathDesc, **kwargs)
95
+
96
+ # split-related
97
+ tgt_data = ["OriginalData", "tgt-data"]
98
+ self.tgt_data_path = self._ws_path / Path(*tgt_data)
99
+ self.tgt_data = make_path_desc(
100
+ parts=tgt_data,
101
+ fetch_multiple=fetch_part_parquets,
102
+ )
103
+ self.tgt_trn_data = make_path_desc(
104
+ parts=tgt_data,
105
+ fetch_multiple=fetch_part_trn_parquets,
106
+ )
107
+ self.tgt_val_data = make_path_desc(
108
+ parts=tgt_data,
109
+ fetch_multiple=fetch_part_val_parquets,
110
+ )
111
+ tgt_meta = ["OriginalData", "tgt-meta"]
112
+ self.tgt_meta_path = self._ws_path / Path(*tgt_meta)
113
+ self.tgt_encoding_types = make_json_path_desc(parts=tgt_meta + ["encoding-types.json"])
114
+ self.tgt_keys = make_json_path_desc(parts=tgt_meta + ["keys.json"])
115
+ ctx_data = ["OriginalData", "ctx-data"]
116
+ self.ctx_data_path = self._ws_path / Path(*ctx_data)
117
+ self.ctx_data = make_path_desc(
118
+ parts=ctx_data,
119
+ fetch_multiple=fetch_part_parquets,
120
+ )
121
+ self.ctx_trn_data = make_path_desc(
122
+ parts=ctx_data,
123
+ fetch_multiple=fetch_part_trn_parquets,
124
+ )
125
+ self.ctx_val_data = make_path_desc(
126
+ parts=ctx_data,
127
+ fetch_multiple=fetch_part_val_parquets,
128
+ )
129
+ ctx_meta = ["OriginalData", "ctx-meta"]
130
+ self.ctx_meta_path = self._ws_path / Path(*ctx_meta)
131
+ self.ctx_encoding_types = make_json_path_desc(parts=ctx_meta + ["encoding-types.json"])
132
+ self.ctx_keys = make_json_path_desc(parts=ctx_meta + ["keys.json"])
133
+
134
+ # analyze-related
135
+ tgt_stats = ["ModelStore", "tgt-stats"]
136
+ self.tgt_stats_path = Path(self._ws_path) / Path(*tgt_stats)
137
+ self.tgt_all_stats = make_path_desc(
138
+ parts=tgt_stats,
139
+ fetch_multiple=fetch_part_jsons,
140
+ )
141
+ self.tgt_stats = make_stats_json_path_desc(parts=tgt_stats + ["stats.json"])
142
+ ctx_stats = ["ModelStore", "ctx-stats"]
143
+ self.ctx_stats_path = Path(self._ws_path) / Path(*ctx_stats)
144
+ self.ctx_all_stats = make_path_desc(
145
+ parts=ctx_stats,
146
+ fetch_multiple=fetch_part_jsons,
147
+ )
148
+ self.ctx_stats = make_stats_json_path_desc(parts=ctx_stats + ["stats.json"])
149
+
150
+ # Encode-related
151
+ encoded_data = ["OriginalData", "encoded-data"]
152
+ self.encoded_data_path = Path(self._ws_path) / Path(*encoded_data)
153
+ self.encoded_data_val = make_path_desc(parts=encoded_data, fetch_multiple=fetch_part_val_parquets)
154
+ self.encoded_data_trn = make_path_desc(parts=encoded_data, fetch_multiple=fetch_part_trn_parquets)
155
+
156
+ # Train-related
157
+ model_data = ["ModelStore", "model-data"]
158
+ self.model_path: Path = Path(self._ws_path) / Path(*model_data)
159
+ self.model_optimizer_path: Path = self.model_path / "optimizer.pt"
160
+ self.model_lr_scheduler_path: Path = self.model_path / "lr-scheduler.pt"
161
+ self.model_dp_accountant_path: Path = self.model_path / "dp-accountant.pt"
162
+ self.model_tabular_weights_path: Path = self.model_path / "model-weights.pt"
163
+ self.model_progress_messages_path: Path = self.model_path / "progress-messages.csv"
164
+ self.model_tabular_configs = make_json_path_desc(parts=model_data + ["model-configs.json"])
165
+
166
+ # Generate-related
167
+ generated_data = ["SyntheticData"]
168
+ self.generated_data_path = Path(self._ws_path) / Path(*generated_data)
169
+ self.generated_data = make_path_desc(parts=generated_data, fetch_multiple=fetch_part_parquets)
170
+
171
+ # Report-related
172
+ self.report_ctx_data_path = self._ws_path / "report-ctx-data"
173
+ self.report_ctx_data = make_path_desc(
174
+ parts=["report-ctx-data"],
175
+ fetch_multiple=fetch_part_parquets,
176
+ )
177
+ self.report_trn_ctx_data = make_path_desc(
178
+ parts=["report-ctx-data"],
179
+ fetch_multiple=fetch_part_trn_parquets,
180
+ )
181
+ self.report_val_ctx_data = make_path_desc(
182
+ parts=["report-ctx-data"],
183
+ fetch_multiple=fetch_part_val_parquets,
184
+ )
185
+
186
+
187
+ def ensure_workspace_dir(workspace_dir: str | Path) -> Path:
188
+ workspace_dir = Path(workspace_dir)
189
+ workspace_dir.mkdir(exist_ok=True, parents=True)
190
+ return workspace_dir
191
+
192
+
193
+ def reset_dir(path: Path) -> None:
194
+ """Create directory if not exists. Otherwise remove all files from it"""
195
+ if os.path.exists(path):
196
+ _LOG.info(f"clean `{path}`")
197
+ for file in os.listdir(path):
198
+ if os.path.isfile(os.path.join(path, file)):
199
+ os.remove(os.path.join(path, file))
200
+ else:
201
+ _LOG.info(f"create `{path}`")
202
+ os.makedirs(path)
203
+
204
+
205
+ def resolve_model_type(workspace_dir: str | Path) -> ModelType:
206
+ """
207
+ Determine the model type based on encoding types in the workspace target metadata.
208
+ """
209
+ workspace_dir = ensure_workspace_dir(workspace_dir)
210
+ workspace = Workspace(workspace_dir)
211
+ stats = workspace.tgt_stats.read()
212
+ columns = stats.get("columns", {})
213
+ if len(columns) == 0:
214
+ return ModelType.tabular
215
+ # fetch encoding type from the first column
216
+ encoding_type = next(iter(columns.values())).get("encoding_type")
217
+ if encoding_type.startswith(ModelType.tabular):
218
+ model_type = ModelType.tabular
219
+ elif encoding_type.startswith(ModelType.language):
220
+ model_type = ModelType.language
221
+ else:
222
+ raise ValueError(
223
+ f"Unknown encoding type, valid encoding types start with TABULAR_ or LANGUAGE_: {encoding_type}"
224
+ )
225
+ return model_type