mostlyai-engine 1.0.3__tar.gz → 1.1.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.0.3 → mostlyai_engine-1.1.0}/PKG-INFO +3 -3
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/__init__.py +1 -1
- mostlyai_engine-1.1.0/mostlyai/engine/_encoding_types/language/categorical.py +75 -0
- mostlyai_engine-1.1.0/mostlyai/engine/_encoding_types/language/datetime.py +143 -0
- mostlyai_engine-1.1.0/mostlyai/engine/_encoding_types/language/numeric.py +136 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/language/text.py +5 -1
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/numeric.py +1 -1
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/encoding.py +36 -14
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/hf_engine.py +2 -5
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/vllm_engine.py +6 -0
- mostlyai_engine-1.1.0/mostlyai/engine/_language/formatron_utils.py +258 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/generation.py +48 -18
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/tokenizer_utils.py +23 -1
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/training.py +2 -3
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/analysis.py +84 -36
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/domain.py +7 -1
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/generation.py +3 -7
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/pyproject.toml +7 -7
- mostlyai_engine-1.0.3/mostlyai/engine/_language/formatron_utils.py +0 -114
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/.gitignore +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/LICENSE +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/README.md +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_common.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_dtypes.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/__init__.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/language/__init__.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/__init__.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/categorical.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/character.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/datetime.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/itt.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/__init__.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/common.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/__init__.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/base.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/lstm.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_memory.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/__init__.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/argn.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/common.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/encoding.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/fairness.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/generation.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/training.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_training_utils.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_workspace.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/encoding.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/logging.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/splitting.py +0 -0
- {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.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.0
|
|
3
|
+
Version: 1.1.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
|
|
@@ -37,7 +37,7 @@ Requires-Dist: psutil<6,>=5.9.5
|
|
|
37
37
|
Requires-Dist: pyarrow>=16.0.0
|
|
38
38
|
Requires-Dist: rich<14,>=13.9.4
|
|
39
39
|
Requires-Dist: setuptools<76,>=75.0.0
|
|
40
|
-
Requires-Dist: tokenizers<0.
|
|
40
|
+
Requires-Dist: tokenizers<0.22,>=0.21.0
|
|
41
41
|
Requires-Dist: torch>=2.5.1; sys_platform == 'darwin'
|
|
42
42
|
Requires-Dist: transformers<5,>=4.45.2
|
|
43
43
|
Provides-Extra: cpu
|
|
@@ -46,7 +46,7 @@ Requires-Dist: torch>=2.5.1; (sys_platform != 'linux') and extra == 'cpu'
|
|
|
46
46
|
Provides-Extra: gpu
|
|
47
47
|
Requires-Dist: bitsandbytes<0.44,>=0.43.3; extra == 'gpu'
|
|
48
48
|
Requires-Dist: torch>=2.5.1; extra == 'gpu'
|
|
49
|
-
Requires-Dist: vllm<0.
|
|
49
|
+
Requires-Dist: vllm<0.8,>=0.7.2; extra == 'gpu'
|
|
50
50
|
Description-Content-Type: text/markdown
|
|
51
51
|
|
|
52
52
|
# Synthetic Data Engine 💎
|
|
@@ -22,7 +22,7 @@ from mostlyai.engine.generation import generate
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
__all__ = ["split", "analyze", "encode", "train", "generate", "init_logging"]
|
|
25
|
-
__version__ = "1.0
|
|
25
|
+
__version__ = "1.1.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.*")
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
Categorical encoding for language models.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
from mostlyai.engine._common import safe_convert_string, STRING
|
|
23
|
+
|
|
24
|
+
CATEGORICAL_UNKNOWN_TOKEN = "_RARE_"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def analyze_language_categorical(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
|
|
28
|
+
values = safe_convert_string(values)
|
|
29
|
+
# count distinct root_keys per categorical value for rare-category protection
|
|
30
|
+
df = pd.concat([root_keys, values], axis=1)
|
|
31
|
+
cnt_values = df.groupby(values.name)[root_keys.name].nunique().to_dict()
|
|
32
|
+
stats = {"has_nan": sum(values.isna()) > 0, "cnt_values": cnt_values}
|
|
33
|
+
return stats
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def analyze_reduce_language_categorical(stats_list: list[dict], value_protection: bool = True) -> dict:
|
|
37
|
+
# sum up all counts for each categorical value
|
|
38
|
+
cnt_values: dict[str, int] = {}
|
|
39
|
+
for item in stats_list:
|
|
40
|
+
for value, count in item["cnt_values"].items():
|
|
41
|
+
cnt_values[value] = cnt_values.get(value, 0) + count
|
|
42
|
+
# create alphabetically sorted list of non-rare categories
|
|
43
|
+
known_categories = [k for k in sorted(cnt_values.keys())]
|
|
44
|
+
if value_protection:
|
|
45
|
+
# stochastic threshold for rare categories
|
|
46
|
+
rare_min = 5 + int(3 * np.random.uniform())
|
|
47
|
+
else:
|
|
48
|
+
rare_min = 0
|
|
49
|
+
categories = [k for k in known_categories if cnt_values[k] >= rare_min]
|
|
50
|
+
no_of_rare_categories = len(known_categories) - len(categories)
|
|
51
|
+
# add None to categories, if any are present
|
|
52
|
+
if any([j["has_nan"] for j in stats_list]):
|
|
53
|
+
categories = [None] + categories
|
|
54
|
+
# add special token for UNKNOWN categories at first position
|
|
55
|
+
if no_of_rare_categories > 0:
|
|
56
|
+
categories = [CATEGORICAL_UNKNOWN_TOKEN] + categories
|
|
57
|
+
stats = {"no_of_rare_categories": no_of_rare_categories, "categories": categories}
|
|
58
|
+
return stats
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def encode_language_categorical(values: pd.Series, stats: dict) -> pd.Series:
|
|
62
|
+
values = safe_convert_string(values)
|
|
63
|
+
values = values.copy()
|
|
64
|
+
known_categories = stats["categories"]
|
|
65
|
+
mask = ~values.isin(known_categories)
|
|
66
|
+
if None in known_categories:
|
|
67
|
+
mask &= ~pd.isna(values)
|
|
68
|
+
values[mask] = CATEGORICAL_UNKNOWN_TOKEN
|
|
69
|
+
return values
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def decode_language_categorical(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
|
|
73
|
+
x = x.astype(STRING)
|
|
74
|
+
allowed_categories = col_stats.get("categories", [])
|
|
75
|
+
return x.where(x.isin(allowed_categories), other=None)
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
import calendar
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from mostlyai.engine._common import safe_convert_datetime
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def analyze_language_datetime(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
|
|
23
|
+
values = safe_convert_datetime(values)
|
|
24
|
+
df = pd.concat([root_keys, values], axis=1)
|
|
25
|
+
# determine lowest/highest values by root ID, and return Top 10
|
|
26
|
+
min_dates = df.groupby(root_keys.name)[values.name].min().dropna()
|
|
27
|
+
min11 = min_dates.sort_values(ascending=True).head(11).astype(str).tolist()
|
|
28
|
+
max_dates = df.groupby(root_keys.name)[values.name].max().dropna()
|
|
29
|
+
max11 = max_dates.sort_values(ascending=False).head(11).astype(str).tolist()
|
|
30
|
+
# determine if there are any NaN values
|
|
31
|
+
has_nan = bool(values.isna().any())
|
|
32
|
+
# return stats
|
|
33
|
+
stats = {
|
|
34
|
+
"has_nan": has_nan,
|
|
35
|
+
"min11": min11,
|
|
36
|
+
"max11": max11,
|
|
37
|
+
}
|
|
38
|
+
return stats
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def analyze_reduce_language_datetime(stats_list: list[dict], value_protection: bool = True) -> dict:
|
|
42
|
+
# check if there are missing values
|
|
43
|
+
has_nan = any([j["has_nan"] for j in stats_list])
|
|
44
|
+
# determine min / max 5 values to map too low / too high values to
|
|
45
|
+
min11 = sorted([v for min11 in [j["min11"] for j in stats_list] for v in min11], reverse=False)[:11]
|
|
46
|
+
max11 = sorted([v for max11 in [j["max11"] for j in stats_list] for v in max11], reverse=True)[:11]
|
|
47
|
+
if value_protection:
|
|
48
|
+
# extreme value protection - discard lowest/highest 5 values
|
|
49
|
+
if len(min11) < 11 or len(max11) < 11:
|
|
50
|
+
# less than 11 subjects with non-NULL values; we need to protect all
|
|
51
|
+
min5 = []
|
|
52
|
+
max5 = []
|
|
53
|
+
else:
|
|
54
|
+
min5 = [str(v) for v in min11[5:10]] # drop 1 to 5th lowest; keep 6th to 10th lowest
|
|
55
|
+
max5 = [str(v) for v in max11[5:10]] # drop 1 to 5th highest; keep 6th to 10th highest
|
|
56
|
+
else:
|
|
57
|
+
min5 = min11[0:4]
|
|
58
|
+
max5 = max11[0:4]
|
|
59
|
+
stats = {
|
|
60
|
+
"has_nan": has_nan,
|
|
61
|
+
"min5": min5,
|
|
62
|
+
"max5": max5,
|
|
63
|
+
}
|
|
64
|
+
return stats
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def encode_language_datetime(values: pd.Series, stats: dict, _: pd.Series | None = None) -> pd.Series:
|
|
68
|
+
# convert
|
|
69
|
+
values = safe_convert_datetime(values)
|
|
70
|
+
values = values.copy()
|
|
71
|
+
# reset index, as `values.mask` can throw errors for misaligned indices
|
|
72
|
+
values.reset_index(drop=True, inplace=True)
|
|
73
|
+
# replace extreme values with randomly sampled 5-th to 10-th largest/smallest values
|
|
74
|
+
min5 = stats["min5"] if len(stats["min5"]) > 0 else [0]
|
|
75
|
+
max5 = stats["max5"] if len(stats["max5"]) > 0 else [0]
|
|
76
|
+
min5 = pd.Series(min5, dtype=values.dtype)
|
|
77
|
+
max5 = pd.Series(max5, dtype=values.dtype)
|
|
78
|
+
values.mask(
|
|
79
|
+
values < min5[0],
|
|
80
|
+
min5.sample(n=len(values), replace=True, ignore_index=True),
|
|
81
|
+
inplace=True,
|
|
82
|
+
)
|
|
83
|
+
values.mask(
|
|
84
|
+
values > max5[0],
|
|
85
|
+
max5.sample(n=len(values), replace=True, ignore_index=True),
|
|
86
|
+
inplace=True,
|
|
87
|
+
)
|
|
88
|
+
return values
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _clip_datetime(x: pd.Series, min5: list, max5: list) -> pd.Series:
|
|
92
|
+
x_dt = pd.to_datetime(x, errors="coerce")
|
|
93
|
+
min_arr = pd.to_datetime(min5).to_numpy(dtype="datetime64[ns]")
|
|
94
|
+
max_arr = pd.to_datetime(max5).to_numpy(dtype="datetime64[ns]")
|
|
95
|
+
n = len(x_dt)
|
|
96
|
+
random_mins = np.random.choice(min_arr, size=n)
|
|
97
|
+
random_maxs = np.random.choice(max_arr, size=n)
|
|
98
|
+
clipped = np.minimum(np.maximum(x_dt.to_numpy(dtype="datetime64[ns]"), random_mins), random_maxs)
|
|
99
|
+
return pd.Series(clipped, index=x.index)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def decode_language_datetime(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
|
|
103
|
+
x = x.where(~x.isin(["", "_INVALID_"]), np.nan)
|
|
104
|
+
|
|
105
|
+
valid_mask = (
|
|
106
|
+
x.str.len().ge(10)
|
|
107
|
+
& x.str.slice(0, 4).str.isdigit()
|
|
108
|
+
& x.str.slice(5, 7).str.isdigit()
|
|
109
|
+
& x.str.slice(8, 10).str.isdigit()
|
|
110
|
+
)
|
|
111
|
+
if valid_mask.sum() > 0: # expected "YYYY-MM-DD" prefix
|
|
112
|
+
# handle the date portion, ensuring validity
|
|
113
|
+
years = x[valid_mask].str.slice(0, 4).astype(int)
|
|
114
|
+
months = x[valid_mask].str.slice(5, 7).astype(int)
|
|
115
|
+
days = x[valid_mask].str.slice(8, 10).astype(int)
|
|
116
|
+
|
|
117
|
+
# clamp days according to maximum possible day of the month of a given year
|
|
118
|
+
last_days = np.array([calendar.monthrange(y, m)[1] for y, m in zip(years, months)])
|
|
119
|
+
clamped_days = np.minimum(days, last_days)
|
|
120
|
+
|
|
121
|
+
# rebuild the date portion
|
|
122
|
+
new_date = (
|
|
123
|
+
years.astype(str).str.zfill(4)
|
|
124
|
+
+ "-"
|
|
125
|
+
+ months.astype(str).str.zfill(2)
|
|
126
|
+
+ "-"
|
|
127
|
+
+ pd.Series(clamped_days, index=years.index).astype(str).str.zfill(2)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# handle the time portion, ensuring validity
|
|
131
|
+
remainder = x[valid_mask].str.slice(10)
|
|
132
|
+
|
|
133
|
+
time_regex = r"^[ T]?(\d{2}:\d{2}:\d{2}(?:\.\d+)?)"
|
|
134
|
+
valid_time = remainder.str.extract(time_regex, expand=False)
|
|
135
|
+
valid_time = valid_time.fillna("00:00:00")
|
|
136
|
+
valid_time = " " + valid_time
|
|
137
|
+
|
|
138
|
+
new_date = new_date + valid_time
|
|
139
|
+
x.loc[valid_mask] = new_date
|
|
140
|
+
|
|
141
|
+
x = pd.to_datetime(x, errors="coerce")
|
|
142
|
+
x = _clip_datetime(x, col_stats["min5"], col_stats["max5"])
|
|
143
|
+
return x.astype("datetime64[ns]")
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from mostlyai.engine._common import safe_convert_numeric
|
|
18
|
+
from mostlyai.engine._encoding_types.tabular.numeric import _type_safe_numeric_series
|
|
19
|
+
from mostlyai.engine.domain import ModelEncodingType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def analyze_language_numeric(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
|
|
23
|
+
values = safe_convert_numeric(values)
|
|
24
|
+
|
|
25
|
+
# determine lowest/highest values by root ID, and return top 11
|
|
26
|
+
df = pd.concat([root_keys, values], axis=1)
|
|
27
|
+
min_values = df.groupby(root_keys.name)[values.name].min().dropna()
|
|
28
|
+
min11 = min_values.sort_values(ascending=True).head(11).tolist()
|
|
29
|
+
max_values = df.groupby(root_keys.name)[values.name].max().dropna()
|
|
30
|
+
max11 = max_values.sort_values(ascending=False).head(11).tolist()
|
|
31
|
+
|
|
32
|
+
# determine if there are any NaN values
|
|
33
|
+
has_nan = bool(values.isna().any())
|
|
34
|
+
|
|
35
|
+
# determine max scale
|
|
36
|
+
def count_scale(num: float) -> int:
|
|
37
|
+
# represent number as fixed point string, remove trailing zeros and decimal point
|
|
38
|
+
num = format(num, "f").rstrip("0").rstrip(".")
|
|
39
|
+
if "." in num:
|
|
40
|
+
# in case of decimal, return number of digits after decimal point
|
|
41
|
+
return len(num.split(".")[1])
|
|
42
|
+
# in case of integer, return 0
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
max_scale = int(values.apply(count_scale).max())
|
|
46
|
+
|
|
47
|
+
stats = {
|
|
48
|
+
"has_nan": has_nan,
|
|
49
|
+
"max_scale": max_scale,
|
|
50
|
+
"min11": min11,
|
|
51
|
+
"max11": max11,
|
|
52
|
+
}
|
|
53
|
+
return stats
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def analyze_reduce_language_numeric(stats_list: list[dict], value_protection: bool = True) -> dict:
|
|
57
|
+
# check for occurrence of NaN values
|
|
58
|
+
has_nan = any([j["has_nan"] for j in stats_list])
|
|
59
|
+
|
|
60
|
+
# determine max scale
|
|
61
|
+
max_scale = max([j["max_scale"] for j in stats_list])
|
|
62
|
+
|
|
63
|
+
# determine min / max 5 values to map too low / too high values to
|
|
64
|
+
min11 = sorted([v for min11 in [j["min11"] for j in stats_list] for v in min11], reverse=False)[:11]
|
|
65
|
+
max11 = sorted([v for max11 in [j["max11"] for j in stats_list] for v in max11], reverse=True)[:11]
|
|
66
|
+
if value_protection:
|
|
67
|
+
# extreme value protection - discard lowest/highest 5 values
|
|
68
|
+
if len(min11) < 11 or len(max11) < 11:
|
|
69
|
+
# less than 11 subjects with non-NULL values; we need to protect all
|
|
70
|
+
min5 = []
|
|
71
|
+
max5 = []
|
|
72
|
+
else:
|
|
73
|
+
min5 = min11[5:10] # drop 1 to 5th lowest; keep 6th to 10th lowest
|
|
74
|
+
max5 = max11[5:10] # drop 1 to 5th highest; keep 6th to 10th highest
|
|
75
|
+
else:
|
|
76
|
+
min5 = min11[0:5]
|
|
77
|
+
max5 = max11[0:5]
|
|
78
|
+
|
|
79
|
+
stats = {
|
|
80
|
+
"encoding_type": ModelEncodingType.language_numeric.value,
|
|
81
|
+
"has_nan": has_nan,
|
|
82
|
+
"max_scale": max_scale,
|
|
83
|
+
"min5": min5,
|
|
84
|
+
"max5": max5,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return stats
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def encode_language_numeric(values: pd.Series, stats: dict, _: pd.Series | None = None) -> pd.DataFrame:
|
|
91
|
+
values = safe_convert_numeric(values)
|
|
92
|
+
# try to convert to int, if possible
|
|
93
|
+
dtype = "Int64" if stats["max_scale"] == 0 else "Float64"
|
|
94
|
+
if dtype == "Int64":
|
|
95
|
+
values = values.round()
|
|
96
|
+
try:
|
|
97
|
+
values = values.astype(dtype)
|
|
98
|
+
except TypeError:
|
|
99
|
+
if dtype == "Int64": # if couldn't safely convert to int, stick to float
|
|
100
|
+
dtype = "Float64"
|
|
101
|
+
values = values.astype(dtype)
|
|
102
|
+
# reset index, as `values.mask` can throw errors for misaligned indices
|
|
103
|
+
values.reset_index(drop=True, inplace=True)
|
|
104
|
+
# replace extreme values with randomly sampled 5-th to 10-th largest/smallest values
|
|
105
|
+
min5 = _type_safe_numeric_series(stats["min5"] or [0], dtype)
|
|
106
|
+
max5 = _type_safe_numeric_series(stats["max5"] or [0], dtype)
|
|
107
|
+
values.mask(
|
|
108
|
+
values < min5[0],
|
|
109
|
+
min5.sample(n=len(values), replace=True, ignore_index=True),
|
|
110
|
+
inplace=True,
|
|
111
|
+
)
|
|
112
|
+
values.mask(
|
|
113
|
+
values > max5[0],
|
|
114
|
+
max5.sample(n=len(values), replace=True, ignore_index=True),
|
|
115
|
+
inplace=True,
|
|
116
|
+
)
|
|
117
|
+
return values
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _clip_numeric(x: pd.Series, min5: list, max5: list) -> pd.Series:
|
|
121
|
+
x_numeric = pd.to_numeric(x, errors="coerce")
|
|
122
|
+
min_arr = np.array(min5, dtype=x_numeric.dtype)
|
|
123
|
+
max_arr = np.array(max5, dtype=x_numeric.dtype)
|
|
124
|
+
n = len(x_numeric)
|
|
125
|
+
random_mins = np.random.choice(min_arr, size=n)
|
|
126
|
+
random_maxs = np.random.choice(max_arr, size=n)
|
|
127
|
+
clipped = np.minimum(np.maximum(x_numeric.to_numpy(), random_mins), random_maxs)
|
|
128
|
+
return pd.Series(clipped, index=x.index)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def decode_language_numeric(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
|
|
132
|
+
x = pd.to_numeric(x, errors="coerce")
|
|
133
|
+
x = x.round(col_stats["max_scale"])
|
|
134
|
+
x = _clip_numeric(x, col_stats["min5"], col_stats["max5"])
|
|
135
|
+
dtype = "Int64" if col_stats["max_scale"] == 0 else float
|
|
136
|
+
return x.astype(dtype)
|
{mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/language/text.py
RENAMED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import pandas as pd
|
|
16
16
|
|
|
17
|
-
from mostlyai.engine._common import safe_convert_string
|
|
17
|
+
from mostlyai.engine._common import safe_convert_string, STRING
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def analyze_text(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
|
|
@@ -39,3 +39,7 @@ def analyze_reduce_text(stats_list: list[dict], _: bool = True) -> dict:
|
|
|
39
39
|
"nchar_max": nchar_max,
|
|
40
40
|
}
|
|
41
41
|
return stats
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def decode_text(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
|
|
45
|
+
return x.astype(STRING)
|
{mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/numeric.py
RENAMED
|
@@ -165,7 +165,7 @@ def analyze_numeric(
|
|
|
165
165
|
# do not count values, if there are too many
|
|
166
166
|
cnt_values = None
|
|
167
167
|
|
|
168
|
-
# determine lowest/highest values by root ID, and return
|
|
168
|
+
# determine lowest/highest values by root ID, and return top 11
|
|
169
169
|
df = pd.concat([root_keys, values], axis=1)
|
|
170
170
|
min_values = df.groupby(root_keys.name)[values.name].min().dropna()
|
|
171
171
|
min11 = min_values.sort_values(ascending=True).head(11).astype("float").tolist()
|
|
@@ -24,17 +24,36 @@ from tokenizers.pre_tokenizers import ByteLevel
|
|
|
24
24
|
|
|
25
25
|
from mostlyai.engine._common import is_sequential, ProgressCallback, ProgressCallbackWrapper, TABLE_COLUMN_INFIX
|
|
26
26
|
from mostlyai.engine._workspace import ensure_workspace_dir, Workspace, reset_dir
|
|
27
|
+
from mostlyai.engine._encoding_types.language.categorical import encode_language_categorical
|
|
28
|
+
from mostlyai.engine._encoding_types.language.numeric import encode_language_numeric
|
|
29
|
+
from mostlyai.engine._encoding_types.language.datetime import encode_language_datetime
|
|
27
30
|
|
|
28
31
|
_LOG = logging.getLogger(__name__)
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
def
|
|
32
|
-
|
|
34
|
+
def apply_encoding_types(df: pd.DataFrame, stats: dict) -> pd.DataFrame:
|
|
35
|
+
for col, col_stats in stats["columns"].items():
|
|
36
|
+
if col_stats["encoding_type"] == "LANGUAGE_CATEGORICAL":
|
|
37
|
+
df[col] = encode_language_categorical(df[col], col_stats)
|
|
38
|
+
elif col_stats["encoding_type"] == "LANGUAGE_NUMERIC":
|
|
39
|
+
df[col] = encode_language_numeric(df[col], col_stats)
|
|
40
|
+
elif col_stats["encoding_type"] == "LANGUAGE_DATETIME":
|
|
41
|
+
df[col] = encode_language_datetime(df[col], col_stats)
|
|
42
|
+
return df
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def drop_sequential_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
33
46
|
# Some columns (e.g., SCP columns) may contain np.ndarray, which are not JSON serializable
|
|
34
47
|
# We need to drop them before converting the DataFrame to JSON
|
|
35
48
|
sequential_columns = [col for col in df.columns if is_sequential(df[col])]
|
|
36
49
|
df = df.drop(columns=sequential_columns)
|
|
37
|
-
|
|
50
|
+
return df
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_df(df: pd.DataFrame, stats: dict, is_target: bool = False) -> pd.DataFrame:
|
|
54
|
+
columns = list(stats["columns"].keys())
|
|
55
|
+
df = df[columns].copy()
|
|
56
|
+
_LOG.info(f"Formatting {'target' if is_target else 'context'} columns {columns} to JSON")
|
|
38
57
|
# convert date format to ISO so that it's JSON serializable
|
|
39
58
|
for col in df.columns:
|
|
40
59
|
if is_datetime64_any_dtype(df[col]):
|
|
@@ -76,15 +95,21 @@ def row_to_json(row: pd.Series, is_target: bool = False) -> str:
|
|
|
76
95
|
|
|
77
96
|
def encode_df(
|
|
78
97
|
ctx_df: pd.DataFrame,
|
|
79
|
-
|
|
98
|
+
ctx_stats: dict | None = None,
|
|
80
99
|
tgt_df: pd.DataFrame | None = None,
|
|
81
|
-
|
|
100
|
+
tgt_stats: dict | None = None,
|
|
82
101
|
) -> pd.DataFrame:
|
|
83
|
-
assert (tgt_df is None) == (
|
|
102
|
+
assert (tgt_df is None) == (tgt_stats is None), "tgt_df and tgt_stats must be both None or both not None"
|
|
103
|
+
if ctx_stats is None:
|
|
104
|
+
ctx_stats = {"columns": {}}
|
|
84
105
|
df = pd.DataFrame()
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
ctx_df = drop_sequential_columns(ctx_df)
|
|
107
|
+
ctx_df = apply_encoding_types(ctx_df, stats=ctx_stats)
|
|
108
|
+
df["ctx"] = format_df(ctx_df, stats=ctx_stats, is_target=False)
|
|
109
|
+
if tgt_df is not None and tgt_stats is not None:
|
|
110
|
+
tgt_df = drop_sequential_columns(tgt_df)
|
|
111
|
+
tgt_df = apply_encoding_types(tgt_df, stats=tgt_stats)
|
|
112
|
+
df["tgt"] = format_df(tgt_df, stats=tgt_stats, is_target=True)
|
|
88
113
|
|
|
89
114
|
# log the bounds of n_tokens in this partition
|
|
90
115
|
content = df["ctx"] + df["tgt"] if "tgt" in df.columns else df["ctx"]
|
|
@@ -107,19 +132,16 @@ def _encode_partition(
|
|
|
107
132
|
ctx_stats: dict | None = None,
|
|
108
133
|
) -> None:
|
|
109
134
|
tgt_df = pd.read_parquet(tgt_partition_file)
|
|
110
|
-
tgt_columns = list(tgt_stats.get("columns", {}).keys())
|
|
111
135
|
if ctx_partition_file:
|
|
112
136
|
ctx_df = pd.read_parquet(ctx_partition_file)
|
|
113
|
-
ctx_columns = list(ctx_stats.get("columns", {}).keys())
|
|
114
137
|
else:
|
|
115
138
|
# create on-the-fly context
|
|
116
139
|
ctx_df = pd.DataFrame(index=range(len(tgt_df)))
|
|
117
|
-
ctx_columns = []
|
|
118
140
|
df = encode_df(
|
|
119
141
|
ctx_df=ctx_df,
|
|
120
|
-
|
|
142
|
+
ctx_stats=ctx_stats,
|
|
121
143
|
tgt_df=tgt_df,
|
|
122
|
-
|
|
144
|
+
tgt_stats=tgt_stats,
|
|
123
145
|
)
|
|
124
146
|
# shuffle and persist to disk as parquet files
|
|
125
147
|
df = df.sample(frac=1)
|
{mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/hf_engine.py
RENAMED
|
@@ -23,8 +23,8 @@ from peft import PeftModel
|
|
|
23
23
|
|
|
24
24
|
from transformers import AutoTokenizer
|
|
25
25
|
from mostlyai.engine._language.common import load_base_model_and_config
|
|
26
|
-
from mostlyai.engine._language.tokenizer_utils import tokenize_fn
|
|
27
26
|
from mostlyai.engine._language.formatron_utils import monkey_patch_formatron
|
|
27
|
+
from mostlyai.engine._language.tokenizer_utils import tokenize_fn
|
|
28
28
|
|
|
29
29
|
from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
|
|
30
30
|
from formatron.formatter import FormatterBuilder
|
|
@@ -66,11 +66,8 @@ class HuggingFaceEngine(LanguageEngine):
|
|
|
66
66
|
self.tokenizer.special_tokens_map
|
|
67
67
|
)
|
|
68
68
|
self._json_enforcing_possible = is_peft_adapter or is_trained_lstm_tokenizer
|
|
69
|
-
|
|
70
|
-
# apply all necessary monkey patches to the formatron library
|
|
71
|
-
if self._json_enforcing_possible:
|
|
69
|
+
if self.supports_json_enforcing():
|
|
72
70
|
monkey_patch_formatron()
|
|
73
|
-
|
|
74
71
|
self._logits_processors = None
|
|
75
72
|
|
|
76
73
|
def get_default_batch_size(self) -> int:
|
{mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/vllm_engine.py
RENAMED
|
@@ -16,6 +16,8 @@ import contextlib
|
|
|
16
16
|
from os import PathLike
|
|
17
17
|
import time
|
|
18
18
|
import typing
|
|
19
|
+
|
|
20
|
+
from mostlyai.engine._language.common import is_bf16_supported
|
|
19
21
|
from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
|
|
20
22
|
import torch
|
|
21
23
|
from formatron.config import EngineGenerationConfig
|
|
@@ -24,6 +26,7 @@ from formatron.formatter import FormatterBuilder
|
|
|
24
26
|
from peft import PeftConfig
|
|
25
27
|
|
|
26
28
|
from transformers import AutoTokenizer, AutoConfig, PreTrainedTokenizerBase
|
|
29
|
+
from mostlyai.engine._language.formatron_utils import monkey_patch_formatron
|
|
27
30
|
from vllm import LLM, SamplingParams
|
|
28
31
|
from vllm.lora.request import LoRARequest
|
|
29
32
|
from vllm.config import _get_and_verify_max_len
|
|
@@ -105,8 +108,10 @@ class VLLMEngine(LanguageEngine):
|
|
|
105
108
|
device=device.type,
|
|
106
109
|
max_model_len=min(config_max_model_len, self.tokenizer_max_length + max_new_tokens),
|
|
107
110
|
enable_lora=True,
|
|
111
|
+
dtype=torch.bfloat16 if is_bf16_supported(device) else torch.float16,
|
|
108
112
|
# enforce_eager=True, # results in big slowdown, but is needed when running pytest locally
|
|
109
113
|
swap_space=0,
|
|
114
|
+
disable_log_stats=True,
|
|
110
115
|
)
|
|
111
116
|
self._base_logits_processors = [MaskInvalidIndicesLogitsProcessor(self.llm.get_tokenizer())]
|
|
112
117
|
self.tokenizer = AutoTokenizer.from_pretrained(
|
|
@@ -119,6 +124,7 @@ class VLLMEngine(LanguageEngine):
|
|
|
119
124
|
add_eos_token=False,
|
|
120
125
|
)
|
|
121
126
|
self._logits_processors = None
|
|
127
|
+
monkey_patch_formatron()
|
|
122
128
|
|
|
123
129
|
def get_default_batch_size(self) -> int:
|
|
124
130
|
return 192
|