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,171 @@
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
+ from enum import Enum
16
+
17
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
18
+
19
+
20
+ class CustomBaseModel(BaseModel):
21
+ model_config = ConfigDict(protected_namespaces=(), populate_by_name=True)
22
+
23
+
24
+ class ModelType(str, Enum):
25
+ """
26
+ The type of model.
27
+
28
+ - `TABULAR`: A generative AI model tailored towards tabular data, trained from scratch.
29
+ - `LANGUAGE`: A generative AI model build upon a (pre-trained) language model.
30
+ """
31
+
32
+ tabular = "TABULAR"
33
+ language = "LANGUAGE"
34
+
35
+
36
+ class ModelEncodingType(str, Enum):
37
+ """
38
+ The encoding type used for model training and data generation.
39
+
40
+ - `AUTO`: Model chooses among available encoding types based on the column's data type.
41
+ - `TABULAR_CATEGORICAL`: Model samples from existing (non-rare) categories.
42
+ - `TABULAR_NUMERIC_AUTO`: Model chooses among 3 numeric encoding types based on the values.
43
+ - `TABULAR_NUMERIC_DISCRETE`: Model samples from existing discrete numerical values.
44
+ - `TABULAR_NUMERIC_BINNED`: Model samples from binned buckets, to then sample randomly within a bucket.
45
+ - `TABULAR_NUMERIC_DIGIT`: Model samples each digit of a numerical value.
46
+ - `TABULAR_CHARACTER`: Model samples each character of a string value.
47
+ - `TABULAR_DATETIME`: Model samples each part of a datetime value.
48
+ - `TABULAR_DATETIME_RELATIVE`: Model samples the relative difference between datetimes within a sequence.
49
+ - `TABULAR_LAT_LONG`: Model samples a latitude-longitude column. The format is "latitude,longitude".
50
+ - `LANGUAGE_TEXT`: Model will train a distinct LANGUAGE model for this column, to then generate free text.
51
+ """
52
+
53
+ auto = "AUTO"
54
+ tabular_categorical = "TABULAR_CATEGORICAL"
55
+ tabular_numeric_auto = "TABULAR_NUMERIC_AUTO"
56
+ tabular_numeric_discrete = "TABULAR_NUMERIC_DISCRETE"
57
+ tabular_numeric_binned = "TABULAR_NUMERIC_BINNED"
58
+ tabular_numeric_digit = "TABULAR_NUMERIC_DIGIT"
59
+ tabular_character = "TABULAR_CHARACTER"
60
+ tabular_datetime = "TABULAR_DATETIME"
61
+ tabular_datetime_relative = "TABULAR_DATETIME_RELATIVE"
62
+ tabular_lat_long = "TABULAR_LAT_LONG"
63
+ language_text = "LANGUAGE_TEXT"
64
+
65
+
66
+ class ModelStateStrategy(str, Enum):
67
+ """
68
+ The strategy of how any existing model states and training progress are to be handled.
69
+
70
+ - `RESET`: Start training from scratch. Overwrite any existing model states and training progress.
71
+ - `REUSE`: Reuse any existing model states, but start progress from scratch. Used for fine-tuning existing models.
72
+ - `RESUME`: Reuse any existing model states and progress. Used for continuing an aborted training.
73
+ """
74
+
75
+ reset = "RESET"
76
+ reuse = "REUSE"
77
+ resume = "RESUME"
78
+
79
+
80
+ class RareCategoryReplacementMethod(str, Enum):
81
+ """
82
+ Specifies how rare categories will be sampled.
83
+ Only applicable if value protection has been enabled.
84
+
85
+ - `CONSTANT`: Replace rare categories by a constant `_RARE_` token.
86
+ - `SAMPLE`: Replace rare categories by a sample from non-rare categories.
87
+ """
88
+
89
+ constant = "CONSTANT"
90
+ sample = "SAMPLE"
91
+
92
+
93
+ class RebalancingConfig(CustomBaseModel):
94
+ """
95
+ Configure rebalancing.
96
+ """
97
+
98
+ column: str = Field(..., description="The name of the column to be rebalanced.")
99
+ probabilities: dict[str, float] = Field(
100
+ ...,
101
+ description="The target distribution of samples values. The keys are the categorical values, and the values "
102
+ "are the probabilities.",
103
+ )
104
+
105
+ @field_validator("probabilities", mode="after")
106
+ def validate_probabilities(cls, v):
107
+ if not all(0 <= v <= 1 for v in v.values()):
108
+ raise ValueError("the probabilities must be between 0 and 1")
109
+ if not sum(v.values()) <= 1:
110
+ raise ValueError("the sum of probabilities must be less than or equal to 1")
111
+ return v
112
+
113
+
114
+ class ImputationConfig(CustomBaseModel):
115
+ """
116
+ Configure imputation. Imputed columns will suppress the sampling of NULL values.
117
+ """
118
+
119
+ columns: list[str] = Field(..., description="The names of the columns to be imputed.")
120
+
121
+
122
+ class FairnessConfig(CustomBaseModel):
123
+ """
124
+ Configure a fairness objective for the table.
125
+
126
+ The generated synthetic data will maintain robust statistical parity between the target column and
127
+ the specified sensitive columns. All these columns must be categorical.
128
+ """
129
+
130
+ target_column: str = Field(..., alias="targetColumn")
131
+ sensitive_columns: list[str] = Field(..., alias="sensitiveColumns")
132
+
133
+
134
+ class DifferentialPrivacyConfig(CustomBaseModel):
135
+ """
136
+ The differential privacy configuration for training the model.
137
+ If not provided, then no differential privacy will be applied.
138
+ """
139
+
140
+ max_epsilon: float | None = Field(
141
+ default=None,
142
+ alias="maxEpsilon",
143
+ description="Specifies the maximum allowable epsilon value. If the training process exceeds this threshold, "
144
+ "it will be terminated early. Only model checkpoints with epsilon values below this limit will be "
145
+ "retained. If not provided, the training will proceed without early termination based on "
146
+ "epsilon constraints.",
147
+ ge=0.0,
148
+ le=10000.0,
149
+ )
150
+ noise_multiplier: float = Field(
151
+ default=1.5,
152
+ alias="noiseMultiplier",
153
+ description="The ratio of the standard deviation of the Gaussian noise to the L2-sensitivity of the function "
154
+ "to which the noise is added (How much noise to add).\n",
155
+ ge=0.0,
156
+ le=10000.0,
157
+ )
158
+ max_grad_norm: float = Field(
159
+ default=1.0,
160
+ alias="maxGradNorm",
161
+ description="The maximum norm of the per-sample gradients for training the model with differential privacy.\n",
162
+ ge=0.0,
163
+ le=10000.0,
164
+ )
165
+ delta: float = Field(
166
+ default=1e-5,
167
+ description="The delta value for differential privacy. It is the probability of the privacy guarantee not "
168
+ "holding. The smaller the delta, the more confident you can be that the privacy guarantee holds.",
169
+ ge=0.0,
170
+ le=1.0,
171
+ )
@@ -0,0 +1,42 @@
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
+ from pathlib import Path
16
+
17
+ from mostlyai.engine._common import ProgressCallback
18
+ from mostlyai.engine.domain import ModelType
19
+ from mostlyai.engine._workspace import resolve_model_type
20
+
21
+
22
+ def encode(*, workspace_dir: str | Path = "engine-ws", update_progress: ProgressCallback | None = None) -> None:
23
+ """
24
+ Encodes data in the workspace that has already been split and analyzed.
25
+
26
+ Creates the following folder structure within the `workspace_dir`:
27
+
28
+ - `OriginalData/encoded-data`: Encoded data for training, stored as parquet files.
29
+
30
+ Args:
31
+ workspace_dir: Directory path for workspace.
32
+ update_progress: Callback for progress updates.
33
+ """
34
+ model_type = resolve_model_type(workspace_dir)
35
+ if model_type == ModelType.tabular:
36
+ from mostlyai.engine._tabular.encoding import encode as encode_tabular
37
+
38
+ return encode_tabular(workspace_dir=workspace_dir, update_progress=update_progress)
39
+ else:
40
+ from mostlyai.engine._language.encoding import encode as encode_language
41
+
42
+ return encode_language(workspace_dir=workspace_dir, update_progress=update_progress)
@@ -0,0 +1,110 @@
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 inspect
16
+ from pathlib import Path
17
+ import pandas as pd
18
+
19
+ from mostlyai.engine._common import ProgressCallback
20
+ from mostlyai.engine.domain import (
21
+ ModelType,
22
+ RareCategoryReplacementMethod,
23
+ RebalancingConfig,
24
+ ImputationConfig,
25
+ FairnessConfig,
26
+ )
27
+ from mostlyai.engine._workspace import resolve_model_type
28
+
29
+
30
+ def generate(
31
+ *,
32
+ ctx_data: pd.DataFrame | None = None,
33
+ seed_data: pd.DataFrame | None = None,
34
+ sample_size: int | None = None,
35
+ batch_size: int | None = None,
36
+ sampling_temperature: float = 1.0,
37
+ sampling_top_p: float = 1.0,
38
+ device: str | None = None,
39
+ rare_category_replacement_method: RareCategoryReplacementMethod | str | None = None,
40
+ rebalancing: RebalancingConfig | dict | None = None,
41
+ imputation: ImputationConfig | dict | None = None,
42
+ fairness: FairnessConfig | dict | None = None,
43
+ workspace_dir: str | Path = "engine-ws",
44
+ update_progress: ProgressCallback | None = None,
45
+ ) -> None:
46
+ """
47
+ Generates synthetic data from a trained model.
48
+
49
+ Creates the following folder structure within the `workspace_dir`:
50
+
51
+ - `SyntheticData`: Generated synthetic data, stored as parquet files.
52
+
53
+ Args:
54
+ ctx_data: Context data to be used for generation.
55
+ seed_data: Seed data to condition generation on fixed target columns.
56
+ sample_size: Number of samples to generate. Defaults to number of original samples.
57
+ batch_size: Batch size for generation. If None, determined automatically.
58
+ sampling_temperature: Sampling temperature. Higher values increase randomness.
59
+ sampling_top_p: Nucleus sampling probability threshold.
60
+ device: Device to run generation on ('cuda' or 'cpu'). Defaults to 'cuda' if available, else 'cpu'.
61
+ rare_category_replacement_method: Method for handling rare categories. Only applicable for tabular models.
62
+ rebalancing: Configuration for rebalancing column distributions. Only applicable for tabular models.
63
+ imputation: List of columns to impute missing values. Only applicable for tabular models.
64
+ fairness: Configuration for fairness constraints. Only applicable for tabular models.
65
+ workspace_dir: Directory path for workspace.
66
+ update_progress: Callback for progress updates.
67
+ """
68
+ model_type = resolve_model_type(workspace_dir)
69
+ if model_type == ModelType.tabular:
70
+ from mostlyai.engine._tabular.generation import generate as generate_tabular
71
+
72
+ return generate_tabular(
73
+ ctx_data=ctx_data,
74
+ seed_data=seed_data,
75
+ sample_size=sample_size,
76
+ batch_size=batch_size,
77
+ sampling_temperature=sampling_temperature,
78
+ sampling_top_p=sampling_top_p,
79
+ rare_category_replacement_method=inspect.signature(generate_tabular)
80
+ .parameters["rare_category_replacement_method"]
81
+ .default,
82
+ rebalancing=rebalancing,
83
+ imputation=imputation,
84
+ fairness=fairness,
85
+ device=device,
86
+ workspace_dir=workspace_dir,
87
+ update_progress=update_progress,
88
+ )
89
+ else:
90
+ from mostlyai.engine._language.generation import generate as generate_language
91
+
92
+ if imputation is not None:
93
+ raise ValueError("imputation is not supported for language models")
94
+ if fairness is not None:
95
+ raise ValueError("fairness is not supported for language models")
96
+ if rebalancing is not None:
97
+ raise ValueError("rebalancing is not supported for language models")
98
+ if rare_category_replacement_method is not None:
99
+ raise ValueError("rare_category_replacement_method is not supported for language models")
100
+ return generate_language(
101
+ ctx_data=ctx_data,
102
+ seed_data=seed_data,
103
+ sample_size=sample_size,
104
+ batch_size=batch_size,
105
+ sampling_temperature=sampling_temperature,
106
+ sampling_top_p=sampling_top_p,
107
+ device=device,
108
+ workspace_dir=workspace_dir,
109
+ update_progress=update_progress,
110
+ )
@@ -0,0 +1,257 @@
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
+ """
16
+ Split original data for training and validation.
17
+ """
18
+
19
+ import time
20
+ import warnings
21
+ from pathlib import Path
22
+
23
+ from mostlyai.engine._common import ProgressCallbackWrapper, ProgressCallback
24
+ from mostlyai.engine._dtypes import (
25
+ is_date_dtype,
26
+ is_float_dtype,
27
+ is_integer_dtype,
28
+ is_timestamp_dtype,
29
+ )
30
+
31
+ import numpy as np
32
+ import logging
33
+
34
+ import pandas as pd
35
+
36
+ from mostlyai.engine.domain import ModelType, ModelEncodingType
37
+ from mostlyai.engine._workspace import (
38
+ PathDesc,
39
+ Workspace,
40
+ ensure_workspace_dir,
41
+ reset_dir,
42
+ )
43
+
44
+ warnings.simplefilter(action="ignore", category=UserWarning)
45
+
46
+ _LOG = logging.getLogger(__name__)
47
+
48
+
49
+ def _get_default_tabular_encoding_type(x: pd.Series) -> ModelEncodingType:
50
+ if is_integer_dtype(x) or is_float_dtype(x):
51
+ return ModelEncodingType.tabular_numeric_auto
52
+ elif is_date_dtype(x) or is_timestamp_dtype(x):
53
+ return ModelEncodingType.tabular_datetime
54
+ else:
55
+ return ModelEncodingType.tabular_categorical
56
+
57
+
58
+ def _get_default_language_encoding_type(_: pd.Series) -> ModelEncodingType:
59
+ return ModelEncodingType.language_text
60
+
61
+
62
+ def split(
63
+ tgt_data: pd.DataFrame,
64
+ *,
65
+ ctx_data: pd.DataFrame | None = None,
66
+ tgt_primary_key: str | None = None,
67
+ ctx_primary_key: str | None = None,
68
+ tgt_context_key: str | None = None,
69
+ model_type: str | ModelType | None = None,
70
+ tgt_encoding_types: dict[str, str | ModelEncodingType] | None = None,
71
+ ctx_encoding_types: dict[str, str | ModelEncodingType] | None = None,
72
+ n_partitions: int = 1,
73
+ workspace_dir: str | Path = "engine-ws",
74
+ update_progress: ProgressCallback | None = None,
75
+ ) -> None:
76
+ """
77
+ Splits the provided original data into training and validation sets, and stores these as partitioned Parquet files.
78
+ This is a simplified version of `mostlyai-data`, tailored towards single- and two-table use cases, while requiring
79
+ all data to be passed as DataFrames in memory.
80
+
81
+ Creates the following folder structure within the `workspace_dir`:
82
+
83
+ - `OriginalData/tgt-data`: Partitioned target data files.
84
+ - `OriginalData/tgt-meta`: Metadata files for target data.
85
+ - `OriginalData/ctx-data`: Partitioned context data files (if context is provided).
86
+ - `OriginalData/ctx-meta`: Metadata files for context data (if context is provided).
87
+
88
+ Args:
89
+ tgt_data: DataFrame containing the target data.
90
+ ctx_data: DataFrame containing the context data.
91
+ tgt_primary_key: Primary key column name in the target data.
92
+ ctx_primary_key: Primary key column name in the context data.
93
+ tgt_context_key: Context key column name in the target data.
94
+ model_type: Model type for the target data. If not provided, it will be inferred from the encoding types, or set to TABULAR by default.
95
+ tgt_encoding_types: Encoding types for columns in the target data (excluding key columns).
96
+ ctx_encoding_types: Encoding types for columns in the context data (excluding key columns).
97
+ n_partitions: Number of partitions to split the data into.
98
+ workspace_dir: Path to the workspace directory where files will be created.
99
+ update_progress: A custom progress callback.
100
+ """
101
+
102
+ _LOG.info("SPLIT started")
103
+ t0 = time.time()
104
+ with ProgressCallbackWrapper(update_progress) as progress:
105
+ # validate input
106
+ if tgt_primary_key and tgt_primary_key not in tgt_data:
107
+ raise Exception("tgt_primary_key not found in tgt_data")
108
+ if tgt_primary_key and tgt_data[tgt_primary_key].duplicated().any():
109
+ raise Exception("tgt_primary_key must be unique")
110
+ if ctx_data is not None:
111
+ if tgt_context_key is None:
112
+ raise Exception("tgt_context_key must be provided")
113
+ if tgt_context_key not in tgt_data:
114
+ raise Exception("tgt_context_key not found in tgt_data")
115
+ if ctx_primary_key is None:
116
+ raise Exception("ctx_primary_key must be provided")
117
+ if ctx_primary_key not in ctx_data:
118
+ raise Exception("ctx_primary_key not found in ctx_data")
119
+ if ctx_data[ctx_primary_key].duplicated().any():
120
+ raise Exception("ctx_primary_key must be unique")
121
+ if not tgt_data[tgt_context_key].isin(ctx_data[ctx_primary_key]).all():
122
+ raise Exception("tgt_primary_keys must match ctx_primary_keys")
123
+
124
+ # prepare data
125
+ if ctx_data is None and tgt_context_key is not None:
126
+ ctx_primary_key = ctx_primary_key or tgt_context_key
127
+ ctx_data = pd.DataFrame({ctx_primary_key: tgt_data[tgt_context_key].drop_duplicates()})
128
+
129
+ # prepare workspace directory
130
+ workspace_dir = ensure_workspace_dir(workspace_dir)
131
+ ws = Workspace(workspace_dir)
132
+ reset_dir(ws.tgt_data_path)
133
+ reset_dir(ws.tgt_meta_path)
134
+ if ctx_data is not None:
135
+ reset_dir(ws.ctx_data_path)
136
+ reset_dir(ws.ctx_meta_path)
137
+
138
+ # set model_type if not provided
139
+ if not model_type and not tgt_encoding_types:
140
+ # use TABULAR as default
141
+ model_type = ModelType.tabular
142
+ elif not model_type and tgt_encoding_types:
143
+ # consider ModelType of the first encoding type
144
+ first_enc_type = next(iter(tgt_encoding_types.values()))
145
+ model_type = ModelType.tabular if first_enc_type.startswith(ModelType.tabular.value) else ModelType.language
146
+ model_type = ModelType(model_type).value
147
+
148
+ # populate model_encoding_types with defaults if not specified
149
+ tgt_encoding_types = tgt_encoding_types or {}
150
+ tgt_cols = [c for c in tgt_data if c != tgt_primary_key and c != tgt_context_key]
151
+ for col in tgt_cols:
152
+ if col not in tgt_encoding_types:
153
+ if model_type == ModelType.language:
154
+ tgt_encoding_types[col] = _get_default_language_encoding_type(tgt_data[col]).value
155
+ else:
156
+ tgt_encoding_types[col] = _get_default_tabular_encoding_type(tgt_data[col]).value
157
+ else:
158
+ tgt_encoding_types[col] = ModelEncodingType(tgt_encoding_types[col]).value
159
+ ctx_encoding_types = ctx_encoding_types or {}
160
+ ctx_cols = [c for c in ctx_data if c != ctx_primary_key] if ctx_data is not None else []
161
+ for col in ctx_cols:
162
+ if col not in ctx_encoding_types:
163
+ ctx_encoding_types[col] = _get_default_tabular_encoding_type(ctx_data[col]).value
164
+ else:
165
+ ctx_encoding_types[col] = ModelEncodingType(ctx_encoding_types[col]).value
166
+ _LOG.info(f"{model_type=}")
167
+ _LOG.info(f"{tgt_encoding_types=}")
168
+ if ctx_encoding_types:
169
+ _LOG.info(f"{ctx_encoding_types=}")
170
+
171
+ # check if tgt_encoding_types are consistent with model_type
172
+ for col, enc_type in tgt_encoding_types.items():
173
+ if not enc_type.startswith(model_type):
174
+ raise Exception("Mismatch between model_type and model_encoding_types")
175
+
176
+ # split into trn/val
177
+ tmp_key = "__key"
178
+ if ctx_data is None and tgt_context_key is None:
179
+ # assign temporary key to enable consistent splitting logic; we drop that key later on again
180
+ tgt_data = tgt_data.copy()
181
+ tgt_data[tmp_key] = range(len(tgt_data))
182
+ tgt_context_key = tmp_key
183
+ # gather all keys
184
+ if ctx_data is None:
185
+ keys = tgt_data[tgt_context_key].drop_duplicates()
186
+ else:
187
+ keys = ctx_data[ctx_primary_key]
188
+ # shuffle keys and split
189
+ keys = keys.sample(frac=1)
190
+ trn_cnt = round(0.9 * len(keys))
191
+ trn_keys = keys[:trn_cnt]
192
+ val_keys = keys[trn_cnt:]
193
+
194
+ def save_partition(
195
+ df: pd.DataFrame, path: Path, key: str, sel_trn_keys: np.ndarray, sel_val_keys: np.ndarray, idx: int
196
+ ):
197
+ trn_df = df[df[key].isin(sel_trn_keys)].drop(columns=[tmp_key], errors="ignore")
198
+ val_df = df[df[key].isin(sel_val_keys)].drop(columns=[tmp_key], errors="ignore")
199
+ trn_df.to_parquet(path / f"part.{idx:06d}-trn.parquet", engine="pyarrow", index=False)
200
+ val_df.to_parquet(path / f"part.{idx:06d}-val.parquet", engine="pyarrow", index=False)
201
+
202
+ # save tgt-data
203
+ for i in range(n_partitions):
204
+ save_partition(
205
+ df=tgt_data,
206
+ path=ws.tgt_data_path,
207
+ key=tgt_context_key,
208
+ sel_trn_keys=trn_keys[i::n_partitions],
209
+ sel_val_keys=val_keys[i::n_partitions],
210
+ idx=i,
211
+ )
212
+ progress.update(completed=i, total=2 * n_partitions)
213
+ if tgt_context_key == tmp_key:
214
+ tgt_context_key = None
215
+ # save tgt-meta
216
+ _save_meta_encoding_types(tgt_encoding_types, ws.tgt_encoding_types)
217
+ _save_meta_keys(primary_key=tgt_primary_key, context_key=tgt_context_key, path=ws.tgt_keys)
218
+
219
+ if ctx_data is not None:
220
+ # save ctx-data
221
+ for i in range(n_partitions):
222
+ save_partition(
223
+ df=ctx_data,
224
+ path=ws.ctx_data_path,
225
+ key=ctx_primary_key,
226
+ sel_trn_keys=trn_keys[i::n_partitions],
227
+ sel_val_keys=val_keys[i::n_partitions],
228
+ idx=i,
229
+ )
230
+ progress.update(completed=n_partitions + i, total=2 * n_partitions)
231
+ # save ctx-meta
232
+ _save_meta_keys(primary_key=ctx_primary_key, path=ws.ctx_keys)
233
+ _save_meta_encoding_types(ctx_encoding_types, path=ws.ctx_encoding_types)
234
+
235
+ _LOG.info(f"SPLIT finished in {time.time() - t0:.2f}s")
236
+
237
+
238
+ def _save_meta_keys(
239
+ path: PathDesc,
240
+ primary_key: str | None = None,
241
+ context_key: str | None = None,
242
+ ):
243
+ keys = {}
244
+ if primary_key is not None:
245
+ keys["primary_key"] = primary_key
246
+ if context_key is not None:
247
+ keys["context_key"] = context_key
248
+ path.write(keys)
249
+
250
+
251
+ def _save_meta_encoding_types(encoding_types: dict[str, ModelEncodingType], path: PathDesc):
252
+ for col, enc_type in encoding_types.items():
253
+ try:
254
+ ModelEncodingType(enc_type)
255
+ except ValueError:
256
+ raise ValueError(f"unknown encoding type: {enc_type} on column {col}")
257
+ path.write(encoding_types)
@@ -0,0 +1,104 @@
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 inspect
16
+ from pathlib import Path
17
+ from collections.abc import Callable
18
+
19
+ import torch
20
+
21
+ from mostlyai.engine._common import ProgressCallback
22
+ from mostlyai.engine.domain import ModelType, ModelStateStrategy, DifferentialPrivacyConfig
23
+ from mostlyai.engine._workspace import resolve_model_type
24
+
25
+
26
+ def train(
27
+ *,
28
+ model: str | None = None,
29
+ max_training_time: float = 60.0,
30
+ max_epochs: float = 100.0,
31
+ batch_size: int | None = None,
32
+ gradient_accumulation_steps: int | None = None,
33
+ enable_flexible_generation: bool = True,
34
+ max_sequence_window: int | None = None,
35
+ differential_privacy: DifferentialPrivacyConfig | dict | None = None,
36
+ model_state_strategy: ModelStateStrategy = ModelStateStrategy.reset,
37
+ device: torch.device | str | None = None,
38
+ workspace_dir: str | Path = "engine-ws",
39
+ update_progress: ProgressCallback | None = None,
40
+ upload_model_data_callback: Callable | None = None, # TODO
41
+ ) -> None:
42
+ """
43
+ Trains a model with optional early stopping and differential privacy.
44
+
45
+ Creates the following folder structure within the `workspace_dir`:
46
+
47
+ - `ModelStore`: Trained model checkpoints and logs.
48
+
49
+ Args:
50
+ model: The identifier of the model to train. If tabular, defaults to MOSTLY_AI/Medium. If language, defaults to MOSTLY_AI/LSTMFromScratch-3m.
51
+ max_training_time: Maximum training time in minutes.
52
+ max_epochs: Maximum number of training epochs.
53
+ batch_size: Per-device batch size for training and validation. If None, determined automatically.
54
+ gradient_accumulation_steps: Number of steps to accumulate gradients. If None, determined automatically.
55
+ enable_flexible_generation: Whether to enable flexible order generation. Defaults to True.
56
+ max_sequence_window: Maximum sequence window for tabular sequential models. Only applicable for tabular models.
57
+ differential_privacy: Configuration for differential privacy training. If None, DP is disabled.
58
+ model_state_strategy: Strategy for handling existing model state (reset/resume/reuse).
59
+ device: Device to run training on ('cuda' or 'cpu'). Defaults to 'cuda' if available, else 'cpu'.
60
+ workspace_dir: Directory path for workspace. Training outputs are stored in ModelStore subdirectory.
61
+ update_progress: Callback function to report training progress.
62
+ upload_model_data_callback: Callback function to upload model data during training.
63
+ """
64
+ model_type = resolve_model_type(workspace_dir)
65
+ if model_type == ModelType.tabular:
66
+ from mostlyai.engine._tabular.training import train as train_tabular
67
+
68
+ train_tabular(
69
+ model=model if model else inspect.signature(train_tabular).parameters["model"].default,
70
+ workspace_dir=workspace_dir,
71
+ max_training_time=max_training_time,
72
+ max_epochs=max_epochs,
73
+ batch_size=batch_size,
74
+ gradient_accumulation_steps=gradient_accumulation_steps,
75
+ enable_flexible_generation=enable_flexible_generation,
76
+ differential_privacy=differential_privacy,
77
+ update_progress=update_progress,
78
+ upload_model_data_callback=upload_model_data_callback,
79
+ model_state_strategy=model_state_strategy,
80
+ device=device,
81
+ max_sequence_window=max_sequence_window
82
+ if max_sequence_window
83
+ else inspect.signature(train_tabular).parameters["max_sequence_window"].default,
84
+ )
85
+ else:
86
+ from mostlyai.engine._language.training import train as train_language
87
+
88
+ if max_sequence_window is not None:
89
+ raise ValueError("max_sequence_window is not supported for language models")
90
+
91
+ train_language(
92
+ model=model if model else inspect.signature(train_language).parameters["model"].default,
93
+ workspace_dir=workspace_dir,
94
+ max_training_time=max_training_time,
95
+ max_epochs=max_epochs,
96
+ batch_size=batch_size,
97
+ gradient_accumulation_steps=gradient_accumulation_steps,
98
+ enable_flexible_generation=enable_flexible_generation,
99
+ differential_privacy=differential_privacy,
100
+ update_progress=update_progress,
101
+ upload_model_data_callback=upload_model_data_callback,
102
+ model_state_strategy=model_state_strategy,
103
+ device=device,
104
+ )