mostlyai-engine 1.5.7__tar.gz → 1.6.0__tar.gz
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.
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/PKG-INFO +3 -3
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/README.md +1 -1
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/__init__.py +1 -1
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_common.py +83 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/character.py +11 -3
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/datetime.py +10 -5
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py +21 -8
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/numeric.py +10 -7
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/__init__.py +5 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/training.py +6 -3
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/argn.py +19 -1
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/fairness.py +3 -3
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/training.py +28 -19
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/pyproject.toml +2 -2
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/.gitignore +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/LICENSE +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_dtypes.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/__init__.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/__init__.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/categorical.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/datetime.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/numeric.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/text.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/__init__.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/categorical.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/itt.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/common.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/encoding.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/__init__.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/base.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/hf_engine.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/vllm_engine.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/generation.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/lstm.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/tokenizer_utils.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/xgrammar_utils.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_memory.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/__init__.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/common.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/encoding.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/generation.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_training_utils.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_workspace.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/analysis.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/domain.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/encoding.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/generation.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/logging.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/random_state.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/splitting.py +0 -0
- {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/training.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mostlyai-engine
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Synthetic Data Engine
|
|
5
5
|
Project-URL: homepage, https://github.com/mostly-ai/mostlyai-engine
|
|
6
6
|
Project-URL: repository, https://github.com/mostly-ai/mostlyai-engine
|
|
@@ -31,7 +31,7 @@ Requires-Dist: joblib>=1.4.2
|
|
|
31
31
|
Requires-Dist: json-repair>=0.47.0
|
|
32
32
|
Requires-Dist: numpy>=2.0.0
|
|
33
33
|
Requires-Dist: opacus>=1.5.4
|
|
34
|
-
Requires-Dist: pandas
|
|
34
|
+
Requires-Dist: pandas>=2.2.0
|
|
35
35
|
Requires-Dist: peft>=0.12.0
|
|
36
36
|
Requires-Dist: psutil<6,>=5.9.5
|
|
37
37
|
Requires-Dist: pyarrow>=16.0.0
|
|
@@ -91,7 +91,7 @@ pip install -U 'mostlyai-engine[gpu]'
|
|
|
91
91
|
On Linux, one can explicitly install the CPU-only variant of torch together with `mostlyai-engine`:
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
pip install -U torch==2.
|
|
94
|
+
pip install -U torch==2.8.0+cpu torchvision==0.23.0+cpu mostlyai-engine --extra-index-url https://download.pytorch.org/whl/cpu
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
## Quick start
|
|
@@ -41,7 +41,7 @@ pip install -U 'mostlyai-engine[gpu]'
|
|
|
41
41
|
On Linux, one can explicitly install the CPU-only variant of torch together with `mostlyai-engine`:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
pip install -U torch==2.
|
|
44
|
+
pip install -U torch==2.8.0+cpu torchvision==0.23.0+cpu mostlyai-engine --extra-index-url https://download.pytorch.org/whl/cpu
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
## Quick start
|
|
@@ -22,7 +22,7 @@ from mostlyai.engine.splitting import split
|
|
|
22
22
|
from mostlyai.engine.training import train
|
|
23
23
|
|
|
24
24
|
__all__ = ["split", "analyze", "encode", "train", "generate", "init_logging", "set_random_state"]
|
|
25
|
-
__version__ = "1.
|
|
25
|
+
__version__ = "1.6.0"
|
|
26
26
|
|
|
27
27
|
# suppress specific warning related to os.fork() in multi-threaded processes
|
|
28
28
|
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*multi-threaded.*fork.*")
|
|
@@ -884,3 +884,86 @@ def dp_non_rare(value_counts: dict[str, int], epsilon: float, threshold: int = 5
|
|
|
884
884
|
|
|
885
885
|
def get_stochastic_rare_threshold(min_threshold: int = 5, noise_multiplier: float = 3) -> int:
|
|
886
886
|
return min_threshold + int(noise_multiplier * np.random.uniform())
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def get_empirical_probs_for_predictor_init(
|
|
890
|
+
first_encoded_part: Path, tgt_cardinalities: dict[str, int], is_sequential: bool, alpha: float = 1.0
|
|
891
|
+
) -> dict[str, np.ndarray]:
|
|
892
|
+
"""
|
|
893
|
+
Calculate empirical probabilities of each sub column from the first partition of encoded data.
|
|
894
|
+
The probabilities will be used for predictor layer initialization.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
first_encoded_part: Path to the first partition of encoded data.
|
|
898
|
+
tgt_cardinalities: Mapping from column name to its cardinality.
|
|
899
|
+
is_sequential: Whether the model is sequential.
|
|
900
|
+
alpha: Laplace smoothing parameter. If smaller or equal to 0, no smoothing is applied.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
dict[str, np.ndarray]: Mapping from sub column name to its empirical probabilities.
|
|
904
|
+
"""
|
|
905
|
+
df_part = pd.read_parquet(first_encoded_part)
|
|
906
|
+
# for sequential models, we will use the empirical probs of the first time step for weight initialization
|
|
907
|
+
if is_sequential:
|
|
908
|
+
for sub_col in df_part.columns:
|
|
909
|
+
df_part[sub_col] = df_part[sub_col].apply(lambda x: x[0] if isinstance(x, np.ndarray) else x)
|
|
910
|
+
# check which columns have a separate NaN sub column
|
|
911
|
+
has_nan_map = {
|
|
912
|
+
col: f"{col}{PREFIX_SUB_COLUMN}nan" in tgt_cardinalities
|
|
913
|
+
for col in get_columns_from_cardinalities(tgt_cardinalities)
|
|
914
|
+
}
|
|
915
|
+
probs_map: dict[str, np.ndarray] = {}
|
|
916
|
+
for sub_col, cardinality in tgt_cardinalities.items():
|
|
917
|
+
col, _ = sub_col.split(PREFIX_SUB_COLUMN)
|
|
918
|
+
nan_sub_col = f"{col}{PREFIX_SUB_COLUMN}nan"
|
|
919
|
+
if has_nan_map[col] is True and sub_col != nan_sub_col and (df_part[nan_sub_col] == 0).sum() > 0:
|
|
920
|
+
# exclude NaN rows from the calculation if
|
|
921
|
+
# - this column has a separate NaN sub column
|
|
922
|
+
# - the NaN sub column has at least one non-NaN row
|
|
923
|
+
# - this sub column is not the NaN sub column
|
|
924
|
+
df_part_sub_col = df_part.loc[df_part[nan_sub_col] == 0, sub_col]
|
|
925
|
+
else:
|
|
926
|
+
df_part_sub_col = df_part[sub_col]
|
|
927
|
+
# calculate empirical probabilities
|
|
928
|
+
vc = df_part_sub_col.value_counts()
|
|
929
|
+
if vc.empty:
|
|
930
|
+
# fallback to uniform distribution
|
|
931
|
+
probs_map[sub_col] = np.full(cardinality, 1.0 / cardinality)
|
|
932
|
+
else:
|
|
933
|
+
counts = np.zeros(cardinality)
|
|
934
|
+
for idx, count in vc.items():
|
|
935
|
+
counts[int(idx)] = float(count)
|
|
936
|
+
# apply Laplace smoothing
|
|
937
|
+
alpha = max(0.0, alpha)
|
|
938
|
+
total = counts.sum() + alpha * len(counts)
|
|
939
|
+
probs_map[sub_col] = (counts + alpha) / max(total, 1e-12)
|
|
940
|
+
probs_map[sub_col] = np.clip(probs_map[sub_col], a_min=1e-12, a_max=None)
|
|
941
|
+
return probs_map
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def impute_from_non_nan_distribution(values: pd.Series, column_stats: dict) -> tuple[pd.Series, pd.Series]:
|
|
945
|
+
"""
|
|
946
|
+
Impute NaNs with values from the empirical distributions of non-NaN rows.
|
|
947
|
+
This is helpful especially in the low-data regime to avoid bias towards strong artificial patterns.
|
|
948
|
+
It is applied when encoding columns with the following encoding types:
|
|
949
|
+
- TABULAR_NUMERIC_DIGIT
|
|
950
|
+
- TABULAR_DATETIME
|
|
951
|
+
- TABULAR_LAT_LONG
|
|
952
|
+
- TABULAR_CHARACTER
|
|
953
|
+
|
|
954
|
+
Args:
|
|
955
|
+
values: Series of values before encoding.
|
|
956
|
+
column_stats: Column statistics.
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
tuple[pd.Series, pd.Series]: The series with imputed values and the mask of NaNs.
|
|
960
|
+
"""
|
|
961
|
+
nan_mask = values.isna()
|
|
962
|
+
vc = values.value_counts(normalize=True)
|
|
963
|
+
if vc.empty:
|
|
964
|
+
return values, nan_mask.astype(int)
|
|
965
|
+
probs = vc.values
|
|
966
|
+
categories = vc.index
|
|
967
|
+
# NOTE: an alternative will be to use the largest remainder method
|
|
968
|
+
values[nan_mask] = np.random.choice(categories, size=nan_mask.sum(), p=probs)
|
|
969
|
+
return values, nan_mask.astype(int)
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/character.py
RENAMED
|
@@ -19,7 +19,12 @@ Character encoding splits any value into its characters, and encodes each positi
|
|
|
19
19
|
import numpy as np
|
|
20
20
|
import pandas as pd
|
|
21
21
|
|
|
22
|
-
from mostlyai.engine._common import
|
|
22
|
+
from mostlyai.engine._common import (
|
|
23
|
+
dp_non_rare,
|
|
24
|
+
get_stochastic_rare_threshold,
|
|
25
|
+
impute_from_non_nan_distribution,
|
|
26
|
+
safe_convert_string,
|
|
27
|
+
)
|
|
23
28
|
|
|
24
29
|
UNKNOWN_TOKEN = "\0"
|
|
25
30
|
MAX_LENGTH_CHARS = 50
|
|
@@ -92,15 +97,18 @@ def analyze_reduce_character(
|
|
|
92
97
|
|
|
93
98
|
def encode_character(values: pd.Series, stats: dict, _: pd.Series | None = None) -> pd.DataFrame:
|
|
94
99
|
values = safe_convert_string(values)
|
|
100
|
+
values, nan_mask = impute_from_non_nan_distribution(values, stats)
|
|
95
101
|
max_string_length = stats["max_string_length"]
|
|
96
102
|
df_split = split_sub_columns_character(values, max_string_length)
|
|
97
|
-
if not stats["has_nan"]:
|
|
98
|
-
df_split.drop(["nan"], axis=1, inplace=True)
|
|
99
103
|
for idx in range(max_string_length):
|
|
100
104
|
sub_col = f"P{idx}"
|
|
101
105
|
np_codes = np.array(pd.Categorical(df_split[sub_col], categories=stats["codes"][sub_col]).codes)
|
|
102
106
|
np.place(np_codes, np_codes == -1, 0)
|
|
103
107
|
df_split[sub_col] = np_codes
|
|
108
|
+
if stats["has_nan"]:
|
|
109
|
+
df_split["nan"] = nan_mask
|
|
110
|
+
else:
|
|
111
|
+
df_split.drop(["nan"], axis=1, inplace=True)
|
|
104
112
|
return df_split
|
|
105
113
|
|
|
106
114
|
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/datetime.py
RENAMED
|
@@ -30,6 +30,7 @@ from mostlyai.engine._common import (
|
|
|
30
30
|
compute_log_histogram,
|
|
31
31
|
dp_approx_bounds,
|
|
32
32
|
get_stochastic_rare_threshold,
|
|
33
|
+
impute_from_non_nan_distribution,
|
|
33
34
|
safe_convert_datetime,
|
|
34
35
|
)
|
|
35
36
|
from mostlyai.engine._dtypes import is_date_dtype, is_timestamp_dtype
|
|
@@ -168,22 +169,26 @@ def encode_datetime(values: pd.Series, stats: dict, _: pd.Series | None = None)
|
|
|
168
169
|
if stats["max"] is not None:
|
|
169
170
|
reduced_max = pd.Series([stats["max"]], dtype=values.dtype).iloc[0]
|
|
170
171
|
values.loc[values > reduced_max] = reduced_max
|
|
172
|
+
values, nan_mask = impute_from_non_nan_distribution(values, stats)
|
|
171
173
|
# split to sub_columns
|
|
172
174
|
df = split_sub_columns_datetime(values)
|
|
173
|
-
is_not_nan = df["nan"] == 0
|
|
174
175
|
# encode values so that each datetime part ranges from 0 to `max_value-min_value`
|
|
175
176
|
for key in DATETIME_PARTS:
|
|
176
177
|
# subtract minimum value
|
|
177
|
-
df[key] = df[key]
|
|
178
|
-
#
|
|
178
|
+
df[key] = df[key] - stats["min_values"][key]
|
|
179
|
+
# clamp to valid range
|
|
179
180
|
df[key] = np.minimum(df[key], stats["max_values"][key] - stats["min_values"][key])
|
|
180
181
|
df[key] = np.maximum(df[key], 0)
|
|
181
|
-
|
|
182
|
-
df.drop(["nan"], inplace=True, axis=1)
|
|
182
|
+
|
|
183
183
|
if not stats["has_time"]:
|
|
184
184
|
df.drop(["hour", "minute", "second"], inplace=True, axis=1)
|
|
185
185
|
if not stats["has_ms"]:
|
|
186
186
|
df.drop(["ms_E2", "ms_E1", "ms_E0"], inplace=True, axis=1)
|
|
187
|
+
|
|
188
|
+
if stats["has_nan"]:
|
|
189
|
+
df["nan"] = nan_mask
|
|
190
|
+
else:
|
|
191
|
+
df.drop(["nan"], inplace=True, axis=1)
|
|
187
192
|
return df
|
|
188
193
|
|
|
189
194
|
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py
RENAMED
|
@@ -19,7 +19,12 @@ import numpy as np
|
|
|
19
19
|
import pandas as pd
|
|
20
20
|
from numpy.typing import NDArray
|
|
21
21
|
|
|
22
|
-
from mostlyai.engine._common import
|
|
22
|
+
from mostlyai.engine._common import (
|
|
23
|
+
dp_non_rare,
|
|
24
|
+
get_stochastic_rare_threshold,
|
|
25
|
+
impute_from_non_nan_distribution,
|
|
26
|
+
safe_convert_string,
|
|
27
|
+
)
|
|
23
28
|
from mostlyai.engine._encoding_types.tabular.categorical import (
|
|
24
29
|
CATEGORICAL_UNKNOWN_TOKEN,
|
|
25
30
|
encode_categorical,
|
|
@@ -336,21 +341,29 @@ def analyze_reduce_latlong(
|
|
|
336
341
|
|
|
337
342
|
def encode_latlong(
|
|
338
343
|
values: pd.Series,
|
|
339
|
-
|
|
344
|
+
stats: dict,
|
|
340
345
|
context_keys: pd.Series | None = None,
|
|
341
346
|
) -> pd.DataFrame:
|
|
342
347
|
values = safe_convert_string(values)
|
|
348
|
+
# convert invalid entries to NaNs before imputation
|
|
349
|
+
latitude_longitude = split_str_to_latlong(values)
|
|
350
|
+
invalid_entry_or_nan_mask = latitude_longitude.isna().any(axis=1)
|
|
351
|
+
values[invalid_entry_or_nan_mask] = np.nan
|
|
352
|
+
values, nan_mask = impute_from_non_nan_distribution(values, stats)
|
|
343
353
|
# split to sub_columns
|
|
344
354
|
quads = split_sub_columns_latlong(values)
|
|
345
355
|
encoded_quads = pd.DataFrame() # empty DF to include all the ModelEncodingType.tabular_categorical quads
|
|
346
|
-
for quad, value_counts in
|
|
347
|
-
|
|
348
|
-
encoded_quads[quad] = encode_categorical(quads[quad],
|
|
356
|
+
for quad, value_counts in stats["quad_codes"].items():
|
|
357
|
+
quad_stats = {"codes": value_counts}
|
|
358
|
+
encoded_quads[quad] = encode_categorical(quads[quad], quad_stats)
|
|
359
|
+
|
|
360
|
+
encoded_quadtile = encode_character(quads["QUADTILE"], stats["quadtile_characters"])
|
|
349
361
|
|
|
350
|
-
encoded_quadtile = encode_character(quads["QUADTILE"], column_stats["quadtile_characters"])
|
|
351
|
-
if column_stats["has_nan"]:
|
|
352
|
-
encoded_quadtile["nan"] = quads["nan"]
|
|
353
362
|
df = pd.concat([encoded_quads, encoded_quadtile], axis=1)
|
|
363
|
+
if stats["has_nan"]:
|
|
364
|
+
# FIXME: consider moving nan sub column to the beginning
|
|
365
|
+
df["nan"] = nan_mask
|
|
366
|
+
|
|
354
367
|
return df
|
|
355
368
|
|
|
356
369
|
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/numeric.py
RENAMED
|
@@ -36,6 +36,7 @@ from mostlyai.engine._common import (
|
|
|
36
36
|
dp_non_rare,
|
|
37
37
|
find_distinct_bins,
|
|
38
38
|
get_stochastic_rare_threshold,
|
|
39
|
+
impute_from_non_nan_distribution,
|
|
39
40
|
safe_convert_numeric,
|
|
40
41
|
)
|
|
41
42
|
from mostlyai.engine._dtypes import is_float_dtype, is_integer_dtype
|
|
@@ -401,29 +402,31 @@ def _encode_numeric_digit(values: pd.Series, stats: dict, _: pd.Series | None =
|
|
|
401
402
|
# replace extreme values with min/max
|
|
402
403
|
if stats["min"] is not None:
|
|
403
404
|
reduced_min = _type_safe_numeric_series([stats["min"]], dtype).iloc[0]
|
|
404
|
-
values.
|
|
405
|
+
values = values.where((values.isna()) | (values >= reduced_min), reduced_min)
|
|
405
406
|
if stats["max"] is not None:
|
|
406
407
|
reduced_max = _type_safe_numeric_series([stats["max"]], dtype).iloc[0]
|
|
407
|
-
values.
|
|
408
|
+
values = values.where((values.isna()) | (values <= reduced_max), reduced_max)
|
|
409
|
+
values, nan_mask = impute_from_non_nan_distribution(values, stats)
|
|
408
410
|
# split to sub_columns
|
|
409
411
|
df = split_sub_columns_digit(values, stats["max_decimal"], stats["min_decimal"])
|
|
410
|
-
|
|
412
|
+
|
|
411
413
|
# normalize values to `[0, max_digit-min_digit]`
|
|
412
414
|
for d in np.arange(stats["max_decimal"], stats["min_decimal"] - 1, -1):
|
|
413
415
|
key = f"E{d}"
|
|
414
416
|
# subtract minimum value
|
|
415
|
-
df[key] = df[key]
|
|
417
|
+
df[key] = df[key] - stats["min_digits"][key]
|
|
416
418
|
# ensure that any value is mapped onto valid value range
|
|
417
419
|
df[key] = np.minimum(df[key], stats["max_digits"][key] - stats["min_digits"][key])
|
|
418
420
|
df[key] = np.maximum(df[key], 0)
|
|
419
|
-
|
|
420
421
|
# ensure that encoded digits are mapped onto valid value range
|
|
421
422
|
for d in np.arange(stats["max_decimal"], stats["min_decimal"] - 1, -1):
|
|
422
423
|
df[f"E{d}"] = np.minimum(df[f"E{d}"], stats["max_digits"][f"E{d}"])
|
|
423
|
-
if not stats["has_nan"]:
|
|
424
|
-
df.drop("nan", inplace=True, axis=1)
|
|
425
424
|
if not stats["has_neg"]:
|
|
426
425
|
df.drop("neg", inplace=True, axis=1)
|
|
426
|
+
if stats["has_nan"]:
|
|
427
|
+
df["nan"] = nan_mask
|
|
428
|
+
else:
|
|
429
|
+
df.drop("nan", inplace=True, axis=1)
|
|
427
430
|
return df
|
|
428
431
|
|
|
429
432
|
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
15
17
|
from mostlyai.engine._language.lstm import register_mostly_lstm_model
|
|
16
18
|
|
|
19
|
+
# suppress xgrammar max_rollback_tokens deprecation warnings
|
|
20
|
+
warnings.filterwarnings("ignore", message=".*max_rollback_tokens.*", category=DeprecationWarning)
|
|
21
|
+
|
|
17
22
|
register_mostly_lstm_model()
|
|
@@ -355,7 +355,7 @@ def train(
|
|
|
355
355
|
max_training_time = max(0.0, max_training_time * 60) # convert to seconds
|
|
356
356
|
_LOG.info(f"{max_training_time=}s")
|
|
357
357
|
max_epochs = max(0.0, max_epochs)
|
|
358
|
-
max_epochs_cap = math.ceil((trn_cnt + val_cnt) /
|
|
358
|
+
max_epochs_cap = math.ceil((trn_cnt + val_cnt) / 25)
|
|
359
359
|
if max_epochs_cap < max_epochs:
|
|
360
360
|
_LOG.info(f"{max_epochs=} -> max_epochs={max_epochs_cap} due to small sample size")
|
|
361
361
|
max_epochs = max_epochs_cap
|
|
@@ -425,7 +425,7 @@ def train(
|
|
|
425
425
|
_LOG.info(f"{model_state_strategy=}")
|
|
426
426
|
if model_state_strategy in [ModelStateStrategy.resume, ModelStateStrategy.reuse]:
|
|
427
427
|
_LOG.info("load existing model weights")
|
|
428
|
-
torch.serialization.add_safe_globals([np.
|
|
428
|
+
torch.serialization.add_safe_globals([np._core.multiarray.scalar, np.dtype, np.dtypes.Float64DType])
|
|
429
429
|
resume_from_last_checkpoint = True
|
|
430
430
|
model_id_or_path = workspace.model_path
|
|
431
431
|
else: # ModelStateStrategy.reset
|
|
@@ -637,7 +637,9 @@ def train(
|
|
|
637
637
|
# therefore, we choose RDP instead as it is more stable and provides comparable privacy guarantees
|
|
638
638
|
dp_accountant = "rdp" # hard-coded for now
|
|
639
639
|
_LOG.info(f"{dp_config=}, {dp_accountant=}")
|
|
640
|
-
|
|
640
|
+
with warnings.catch_warnings():
|
|
641
|
+
warnings.filterwarnings("ignore", category=UserWarning, message=".*Secure RNG turned off*")
|
|
642
|
+
privacy_engine = PrivacyEngine(accountant=dp_accountant)
|
|
641
643
|
if model_state_strategy == ModelStateStrategy.resume and workspace.model_dp_accountant_path.exists():
|
|
642
644
|
_LOG.info("restore DP accountant state")
|
|
643
645
|
torch.serialization.add_safe_globals([getattr, PRVAccountant, RDPAccountant, GaussianAccountant])
|
|
@@ -707,6 +709,7 @@ def train(
|
|
|
707
709
|
# FIXME approximation, should be divided by total sum of number of tokens in the batch
|
|
708
710
|
# as in _calculate_per_label_losses, also the final sample may be smaller than the batch size.
|
|
709
711
|
if with_dp:
|
|
712
|
+
warnings.filterwarnings("ignore", category=UserWarning, message="Full backward hook is firing*")
|
|
710
713
|
step_loss = outputs.loss
|
|
711
714
|
step_loss.backward()
|
|
712
715
|
else:
|
|
@@ -702,6 +702,7 @@ class Predictors(nn.Module):
|
|
|
702
702
|
cardinalities: dict[str, int],
|
|
703
703
|
regressors_dims: dict[str, int],
|
|
704
704
|
device: torch.device,
|
|
705
|
+
empirical_probs: dict[str, np.ndarray] | None = None,
|
|
705
706
|
):
|
|
706
707
|
super().__init__()
|
|
707
708
|
|
|
@@ -710,10 +711,23 @@ class Predictors(nn.Module):
|
|
|
710
711
|
self.device = device
|
|
711
712
|
|
|
712
713
|
self.predictors = nn.ModuleDict()
|
|
714
|
+
empirical_probs = empirical_probs or {}
|
|
715
|
+
if empirical_probs:
|
|
716
|
+
_LOG.info("initializing predictor bias with empirical log probabilities")
|
|
713
717
|
|
|
714
718
|
for sub_col, dim_output in self.cardinalities.items():
|
|
715
719
|
dim_input = self.regressors_dims[sub_col]
|
|
716
720
|
self.predictors[sub_col] = nn.Linear(in_features=dim_input, out_features=dim_output, device=self.device)
|
|
721
|
+
if empirical_probs:
|
|
722
|
+
nn.init.xavier_uniform_(self.predictors[sub_col].weight)
|
|
723
|
+
with torch.no_grad():
|
|
724
|
+
self.predictors[sub_col].bias.copy_(
|
|
725
|
+
torch.as_tensor(
|
|
726
|
+
np.log(empirical_probs[sub_col]),
|
|
727
|
+
dtype=self.predictors[sub_col].bias.dtype,
|
|
728
|
+
device=device,
|
|
729
|
+
)
|
|
730
|
+
)
|
|
717
731
|
|
|
718
732
|
def forward(self, x: torch.Tensor, sub_col: str) -> torch.Tensor:
|
|
719
733
|
return self.predictors[sub_col](x)
|
|
@@ -739,7 +753,7 @@ def _make_permutation_mask(
|
|
|
739
753
|
order = torch.randperm(n_cols)
|
|
740
754
|
|
|
741
755
|
# convert order into a binary mask consisting of 0s and 1s
|
|
742
|
-
idx =
|
|
756
|
+
idx = torch.argsort(order)
|
|
743
757
|
ones = torch.ones(n_cols, n_cols, dtype=torch.int32, device=device)
|
|
744
758
|
mask = torch.tril(ones, diagonal=-1) # strict lower triangular matrix
|
|
745
759
|
mask = mask[idx, :][:, idx].bool() # re-order rows and columns
|
|
@@ -872,6 +886,7 @@ class FlatModel(nn.Module):
|
|
|
872
886
|
column_order: list[str] | None,
|
|
873
887
|
device: torch.device,
|
|
874
888
|
with_dp: bool = False,
|
|
889
|
+
empirical_probs_for_predictor_init: dict[str, np.ndarray] | None = None,
|
|
875
890
|
):
|
|
876
891
|
super().__init__()
|
|
877
892
|
|
|
@@ -926,6 +941,7 @@ class FlatModel(nn.Module):
|
|
|
926
941
|
cardinalities=self.tgt_cardinalities,
|
|
927
942
|
regressors_dims=self.regressors.dims_output,
|
|
928
943
|
device=device,
|
|
944
|
+
empirical_probs=empirical_probs_for_predictor_init if not with_dp else None,
|
|
929
945
|
)
|
|
930
946
|
|
|
931
947
|
def _handle_context(
|
|
@@ -1155,6 +1171,7 @@ class SequentialModel(nn.Module):
|
|
|
1155
1171
|
column_order: list[str] | None,
|
|
1156
1172
|
device: torch.device,
|
|
1157
1173
|
with_dp: bool = False,
|
|
1174
|
+
empirical_probs_for_predictor_init: dict[str, np.ndarray] | None = None,
|
|
1158
1175
|
):
|
|
1159
1176
|
super().__init__()
|
|
1160
1177
|
|
|
@@ -1228,6 +1245,7 @@ class SequentialModel(nn.Module):
|
|
|
1228
1245
|
cardinalities=self.tgt_cardinalities,
|
|
1229
1246
|
regressors_dims=self.regressors.dims_output,
|
|
1230
1247
|
device=device,
|
|
1248
|
+
empirical_probs=empirical_probs_for_predictor_init if not with_dp else None,
|
|
1231
1249
|
)
|
|
1232
1250
|
|
|
1233
1251
|
def _repeat_flat_context(self, flat_ctx: list[torch.Tensor], repetition: int) -> list[torch.Tensor]:
|
|
@@ -182,8 +182,8 @@ def get_fairness_transforms(
|
|
|
182
182
|
for sensitive_group, target_quantiles in conditional_target_quantiles.items():
|
|
183
183
|
transforms[i][sensitive_group] = partial(
|
|
184
184
|
torch_interp,
|
|
185
|
-
xp=torch.as_tensor(target_quantiles[:, i], device=device),
|
|
186
|
-
fp=torch.as_tensor(marginal_target_quantiles[:, i], device=device),
|
|
185
|
+
xp=torch.as_tensor(target_quantiles[:, i], device=device).contiguous(),
|
|
186
|
+
fp=torch.as_tensor(marginal_target_quantiles[:, i], device=device).contiguous(),
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
_LOG.info("fairness: created transforms")
|
|
@@ -217,7 +217,7 @@ def apply_fairness_transforms(
|
|
|
217
217
|
sensitive_values == torch.as_tensor(group, device=device).tile((probs.shape[0], 1)),
|
|
218
218
|
dim=1,
|
|
219
219
|
),
|
|
220
|
-
transform(probs[:, i]),
|
|
220
|
+
transform(probs[:, i].contiguous()),
|
|
221
221
|
probs[:, i],
|
|
222
222
|
)
|
|
223
223
|
# normalize transformed probabilities to make sure they sum to 1
|
|
@@ -44,6 +44,7 @@ from mostlyai.engine._common import (
|
|
|
44
44
|
get_cardinalities,
|
|
45
45
|
get_columns_from_cardinalities,
|
|
46
46
|
get_ctx_sequence_length,
|
|
47
|
+
get_empirical_probs_for_predictor_init,
|
|
47
48
|
get_max_data_points_per_sample,
|
|
48
49
|
get_sequence_length_stats,
|
|
49
50
|
get_sub_columns_from_cardinalities,
|
|
@@ -390,7 +391,7 @@ def train(
|
|
|
390
391
|
max_training_time = max(0.0, max_training_time) * 60 # convert to seconds
|
|
391
392
|
_LOG.info(f"{max_training_time=}s")
|
|
392
393
|
max_epochs = max(0.0, max_epochs)
|
|
393
|
-
max_epochs_cap = math.ceil((trn_cnt + val_cnt) /
|
|
394
|
+
max_epochs_cap = math.ceil((trn_cnt + val_cnt) / 25)
|
|
394
395
|
if max_epochs_cap < max_epochs:
|
|
395
396
|
_LOG.info(f"{max_epochs=} -> max_epochs={max_epochs_cap} due to small sample size")
|
|
396
397
|
max_epochs = max_epochs_cap
|
|
@@ -437,6 +438,14 @@ def train(
|
|
|
437
438
|
_LOG.info(f"{max_sequence_window=}")
|
|
438
439
|
ctx_seq_len_median = get_ctx_sequence_length(ctx_stats, key="median")
|
|
439
440
|
|
|
441
|
+
empirical_probs_for_predictor_init = (
|
|
442
|
+
get_empirical_probs_for_predictor_init(
|
|
443
|
+
workspace.encoded_data_trn.fetch_all()[0], tgt_cardinalities, is_sequential
|
|
444
|
+
)
|
|
445
|
+
if not with_dp
|
|
446
|
+
else None
|
|
447
|
+
)
|
|
448
|
+
|
|
440
449
|
# the line below fixes issue with growing epoch time for later epochs
|
|
441
450
|
# https://discuss.pytorch.org/t/training-time-gets-slower-and-slower-on-cpu/145483
|
|
442
451
|
torch.set_flush_denormal(True)
|
|
@@ -444,28 +453,24 @@ def train(
|
|
|
444
453
|
_LOG.info("create training model")
|
|
445
454
|
model_checkpoint = TabularModelCheckpoint(workspace=workspace)
|
|
446
455
|
argn: SequentialModel | FlatModel
|
|
456
|
+
model_kwargs = {
|
|
457
|
+
"tgt_cardinalities": tgt_cardinalities,
|
|
458
|
+
"ctx_cardinalities": ctx_cardinalities,
|
|
459
|
+
"ctxseq_len_median": ctx_seq_len_median,
|
|
460
|
+
"model_size": model_size,
|
|
461
|
+
"column_order": trn_column_order,
|
|
462
|
+
"device": device,
|
|
463
|
+
"with_dp": with_dp, # this flag decides whether the model is initialized with LSTM or DPLSTM layers
|
|
464
|
+
"empirical_probs_for_predictor_init": empirical_probs_for_predictor_init,
|
|
465
|
+
}
|
|
447
466
|
if is_sequential:
|
|
448
467
|
argn = SequentialModel(
|
|
449
|
-
|
|
450
|
-
ctx_cardinalities=ctx_cardinalities,
|
|
468
|
+
**model_kwargs,
|
|
451
469
|
tgt_seq_len_median=tgt_seq_len_median,
|
|
452
470
|
tgt_seq_len_max=tgt_seq_len_max,
|
|
453
|
-
ctxseq_len_median=ctx_seq_len_median,
|
|
454
|
-
model_size=model_size,
|
|
455
|
-
column_order=trn_column_order,
|
|
456
|
-
device=device,
|
|
457
|
-
with_dp=with_dp, # this flag decides whether the model is initialized with LSTM or DPLSTM layers
|
|
458
471
|
)
|
|
459
472
|
else:
|
|
460
|
-
argn = FlatModel(
|
|
461
|
-
tgt_cardinalities=tgt_cardinalities,
|
|
462
|
-
ctx_cardinalities=ctx_cardinalities,
|
|
463
|
-
ctxseq_len_median=ctx_seq_len_median,
|
|
464
|
-
model_size=model_size,
|
|
465
|
-
column_order=trn_column_order,
|
|
466
|
-
device=device,
|
|
467
|
-
with_dp=with_dp,
|
|
468
|
-
)
|
|
473
|
+
argn = FlatModel(**model_kwargs)
|
|
469
474
|
_LOG.info(f"model class: {argn.__class__.__name__}")
|
|
470
475
|
|
|
471
476
|
if isinstance(model_state_strategy, str):
|
|
@@ -476,7 +481,7 @@ def train(
|
|
|
476
481
|
_LOG.info(f"{model_state_strategy=}")
|
|
477
482
|
if model_state_strategy in [ModelStateStrategy.resume, ModelStateStrategy.reuse]:
|
|
478
483
|
_LOG.info("load existing model weights")
|
|
479
|
-
torch.serialization.add_safe_globals([np.
|
|
484
|
+
torch.serialization.add_safe_globals([np._core.multiarray.scalar, np.dtype, np.dtypes.Float64DType])
|
|
480
485
|
load_model_weights(model=argn, path=workspace.model_tabular_weights_path, device=device)
|
|
481
486
|
else: # ModelStateStrategy.reset
|
|
482
487
|
_LOG.info("remove existing checkpoint files")
|
|
@@ -634,7 +639,9 @@ def train(
|
|
|
634
639
|
# therefore, we choose RDP instead as it is more stable and provides comparable privacy guarantees
|
|
635
640
|
dp_accountant = "rdp" # hard-coded for now
|
|
636
641
|
_LOG.info(f"{dp_config=}, {dp_accountant=}")
|
|
637
|
-
|
|
642
|
+
with warnings.catch_warnings():
|
|
643
|
+
warnings.filterwarnings("ignore", category=UserWarning, message=".*Secure RNG turned off*")
|
|
644
|
+
privacy_engine = PrivacyEngine(accountant=dp_accountant)
|
|
638
645
|
if model_state_strategy == ModelStateStrategy.resume and workspace.model_dp_accountant_path.exists():
|
|
639
646
|
_LOG.info("restore DP accountant state")
|
|
640
647
|
torch.serialization.add_safe_globals([getattr, PRVAccountant, RDPAccountant, GaussianAccountant])
|
|
@@ -700,6 +707,8 @@ def train(
|
|
|
700
707
|
# backward pass
|
|
701
708
|
with warnings.catch_warnings():
|
|
702
709
|
warnings.filterwarnings("ignore", category=FutureWarning, message="Using a non-full backward hook*")
|
|
710
|
+
if with_dp:
|
|
711
|
+
warnings.filterwarnings("ignore", category=UserWarning, message="Full backward hook is firing*")
|
|
703
712
|
step_loss.backward()
|
|
704
713
|
accumulated_steps += 1
|
|
705
714
|
# explicitly count the number of processed samples as the actual batch size can vary when DP is on
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mostlyai-engine"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.0"
|
|
4
4
|
description = "Synthetic Data Engine"
|
|
5
5
|
authors = [{ name = "MOSTLY AI", email = "dev@mostly.ai" }]
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
"setuptools>=77.0.3", # similar to vllm 0.10.1.1
|
|
29
29
|
"numpy>=2.0.0",
|
|
30
|
-
"pandas
|
|
30
|
+
"pandas>=2.2.0",
|
|
31
31
|
"pyarrow>=16.0.0",
|
|
32
32
|
"joblib>=1.4.2",
|
|
33
33
|
"psutil>=5.9.5,<6", # upgrade when colab psutil is updated
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/datetime.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/numeric.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/text.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/itt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/hf_engine.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/vllm_engine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/tokenizer_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|