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.
Files changed (51) hide show
  1. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/PKG-INFO +3 -3
  2. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/README.md +1 -1
  3. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/__init__.py +1 -1
  4. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_common.py +83 -0
  5. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/character.py +11 -3
  6. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/datetime.py +10 -5
  7. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py +21 -8
  8. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/numeric.py +10 -7
  9. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/__init__.py +5 -0
  10. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/training.py +6 -3
  11. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/argn.py +19 -1
  12. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/fairness.py +3 -3
  13. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/training.py +28 -19
  14. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/pyproject.toml +2 -2
  15. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/.gitignore +0 -0
  16. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/LICENSE +0 -0
  17. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_dtypes.py +0 -0
  18. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/__init__.py +0 -0
  19. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/__init__.py +0 -0
  20. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/categorical.py +0 -0
  21. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/datetime.py +0 -0
  22. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/numeric.py +0 -0
  23. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/language/text.py +0 -0
  24. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/__init__.py +0 -0
  25. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/categorical.py +0 -0
  26. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_encoding_types/tabular/itt.py +0 -0
  27. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/common.py +0 -0
  28. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/encoding.py +0 -0
  29. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/__init__.py +0 -0
  30. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/base.py +0 -0
  31. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/hf_engine.py +0 -0
  32. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/engine/vllm_engine.py +0 -0
  33. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/generation.py +0 -0
  34. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/lstm.py +0 -0
  35. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/tokenizer_utils.py +0 -0
  36. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_language/xgrammar_utils.py +0 -0
  37. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_memory.py +0 -0
  38. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/__init__.py +0 -0
  39. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/common.py +0 -0
  40. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/encoding.py +0 -0
  41. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_tabular/generation.py +0 -0
  42. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_training_utils.py +0 -0
  43. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/_workspace.py +0 -0
  44. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/analysis.py +0 -0
  45. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/domain.py +0 -0
  46. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/encoding.py +0 -0
  47. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/generation.py +0 -0
  48. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/logging.py +0 -0
  49. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/random_state.py +0 -0
  50. {mostlyai_engine-1.5.7 → mostlyai_engine-1.6.0}/mostlyai/engine/splitting.py +0 -0
  51. {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.5.7
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~=2.2.0
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.7.0+cpu torchvision==0.22.0+cpu mostlyai-engine --extra-index-url https://download.pytorch.org/whl/cpu
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.7.0+cpu torchvision==0.22.0+cpu mostlyai-engine --extra-index-url https://download.pytorch.org/whl/cpu
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.5.7"
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)
@@ -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 dp_non_rare, get_stochastic_rare_threshold, safe_convert_string
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
 
@@ -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].where(~is_not_nan, df[key] - stats["min_values"][key])
178
- # ensure that any value is mapped onto valid value range
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
- if not stats["has_nan"]:
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
 
@@ -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 dp_non_rare, get_stochastic_rare_threshold, safe_convert_string
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
- column_stats: dict,
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 column_stats["quad_codes"].items():
347
- stats = {"codes": value_counts}
348
- encoded_quads[quad] = encode_categorical(quads[quad], stats)
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
 
@@ -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.loc[values < reduced_min] = reduced_min
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.loc[values > reduced_max] = reduced_max
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
- is_not_nan = df["nan"] == 0
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].where(~is_not_nan, df[key] - stats["min_digits"][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) / 50)
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.core.multiarray.scalar, np.dtype, np.dtypes.Float64DType])
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
- privacy_engine = PrivacyEngine(accountant=dp_accountant)
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 = np.argsort(order)
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) / 50)
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
- tgt_cardinalities=tgt_cardinalities,
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.core.multiarray.scalar, np.dtype, np.dtypes.Float64DType])
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
- privacy_engine = PrivacyEngine(accountant=dp_accountant)
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.5.7"
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~=2.2.0",
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