codadapt 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codadapt/__init__.py +8 -0
- codadapt/_base.py +380 -0
- codadapt/_core.py +70 -0
- codadapt/_multiclass.py +229 -0
- codadapt/_multiclass_artifact.py +75 -0
- codadapt/_multiclass_packing.py +165 -0
- codadapt/_training.py +370 -0
- codadapt/_version.py +1 -0
- codadapt/classifier.py +117 -0
- codadapt/experimental/__init__.py +13 -0
- codadapt/experimental/_compiler.py +166 -0
- codadapt/experimental/_model.py +174 -0
- codadapt/experimental/_rational.py +133 -0
- codadapt/experimental/_safe_blend.py +215 -0
- codadapt/preprocessing.py +435 -0
- codadapt/regressor.py +15 -0
- codadapt-1.0.0.dist-info/METADATA +566 -0
- codadapt-1.0.0.dist-info/RECORD +21 -0
- codadapt-1.0.0.dist-info/WHEEL +5 -0
- codadapt-1.0.0.dist-info/licenses/LICENSE +21 -0
- codadapt-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Lossless shared native multiclass prediction and persistence."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ._core import sigmoid
|
|
6
|
+
from ._multiclass_packing import pack, unpack
|
|
7
|
+
from ._training import predict_coded_memory_buckets
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CompactHead:
|
|
11
|
+
__slots__ = ("shared_state", "intercept", "levels")
|
|
12
|
+
|
|
13
|
+
def __init__(self, shared_state, intercept, levels):
|
|
14
|
+
self.shared_state, self.intercept, self.levels = shared_state, intercept, levels
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def table_size(self):
|
|
18
|
+
return self.shared_state.table_size
|
|
19
|
+
|
|
20
|
+
def scores(self, z):
|
|
21
|
+
s = self.shared_state
|
|
22
|
+
return predict_coded_memory_buckets(
|
|
23
|
+
self.levels, s.encoder.level_views_, z, self.intercept, s.table_size
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def probabilities(logits):
|
|
28
|
+
p = sigmoid(logits)
|
|
29
|
+
total = p.sum(axis=1, keepdims=True)
|
|
30
|
+
if not np.isfinite(p).all() or np.any(total <= 0):
|
|
31
|
+
raise FloatingPointError("Invalid OVR probability mass; no silent uniform repair")
|
|
32
|
+
return p / total
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SharedState:
|
|
36
|
+
__slots__ = ("encoder", "classes", "table_size", "params")
|
|
37
|
+
|
|
38
|
+
def __init__(self, encoder, classes, table_size, params):
|
|
39
|
+
self.encoder, self.classes, self.table_size, self.params = (
|
|
40
|
+
encoder,
|
|
41
|
+
classes,
|
|
42
|
+
table_size,
|
|
43
|
+
params,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Artifact:
|
|
48
|
+
__slots__ = ("shared_state", "heads")
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def create(cls, encoder, classes, heads, params):
|
|
52
|
+
result = cls()
|
|
53
|
+
result.__setstate__(pack(encoder, classes, heads, params))
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def decision_function(self, x):
|
|
57
|
+
z = self.shared_state.encoder.transform_levels(x)
|
|
58
|
+
return np.column_stack([h.scores(z) for h in self.heads])
|
|
59
|
+
|
|
60
|
+
def predict_proba(self, x):
|
|
61
|
+
return probabilities(self.decision_function(x))
|
|
62
|
+
|
|
63
|
+
def predict(self, x):
|
|
64
|
+
return self.shared_state.classes[self.predict_proba(x).argmax(1)]
|
|
65
|
+
|
|
66
|
+
def __getstate__(self):
|
|
67
|
+
s = self.shared_state
|
|
68
|
+
return pack(s.encoder, s.classes, self.heads, s.params)
|
|
69
|
+
|
|
70
|
+
def __setstate__(self, wire):
|
|
71
|
+
encoder, classes, records, table_size, params = unpack(wire)
|
|
72
|
+
self.shared_state = SharedState(encoder, classes, table_size, params)
|
|
73
|
+
self.heads = tuple(
|
|
74
|
+
CompactHead(self.shared_state, intercept, levels) for intercept, levels in records
|
|
75
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Dense float64 coefficient packing; immutable metadata pooling, no compression."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .preprocessing import BucketEncoder, _ResolutionView
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def minimal_index(a):
|
|
11
|
+
if a.dtype.kind not in "iu" or (a.size and a.min() < 0):
|
|
12
|
+
return a
|
|
13
|
+
maximum = int(a.max()) if a.size else 0
|
|
14
|
+
dtype = (
|
|
15
|
+
np.uint8
|
|
16
|
+
if maximum <= 255
|
|
17
|
+
else np.uint16
|
|
18
|
+
if maximum <= 65535
|
|
19
|
+
else np.uint32
|
|
20
|
+
if maximum <= 2**32 - 1
|
|
21
|
+
else a.dtype
|
|
22
|
+
)
|
|
23
|
+
narrowed = a.astype(dtype)
|
|
24
|
+
assert np.array_equal(narrowed.astype(a.dtype), a)
|
|
25
|
+
return narrowed
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Pool:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.parts, self.sizes, self.memo = {}, {}, {}
|
|
31
|
+
|
|
32
|
+
def add(self, original, indices=True):
|
|
33
|
+
a = minimal_index(original) if indices else original
|
|
34
|
+
a = np.ascontiguousarray(a)
|
|
35
|
+
signature = (a.dtype.str, a.shape, a.tobytes())
|
|
36
|
+
if signature not in self.memo:
|
|
37
|
+
dtype = a.dtype.str
|
|
38
|
+
offset = self.sizes.get(dtype, 0)
|
|
39
|
+
self.parts.setdefault(dtype, []).append(a.ravel().copy())
|
|
40
|
+
self.sizes[dtype] = offset + a.size
|
|
41
|
+
self.memo[signature] = ("a", dtype, offset, a.shape)
|
|
42
|
+
return self.memo[signature]
|
|
43
|
+
|
|
44
|
+
def buffers(self):
|
|
45
|
+
return {dtype: np.concatenate(arrays) for dtype, arrays in self.parts.items()}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def encode(value, pool):
|
|
49
|
+
if isinstance(value, np.ndarray) and value.dtype.kind != "O":
|
|
50
|
+
return pool.add(value)
|
|
51
|
+
if isinstance(value, dict):
|
|
52
|
+
return ("d", tuple((k, encode(v, pool)) for k, v in value.items()))
|
|
53
|
+
if isinstance(value, list):
|
|
54
|
+
return ("l", tuple(encode(v, pool) for v in value))
|
|
55
|
+
if isinstance(value, tuple):
|
|
56
|
+
return ("t", tuple(encode(v, pool) for v in value))
|
|
57
|
+
if isinstance(value, _ResolutionView):
|
|
58
|
+
return ("v", encode(vars(value), pool))
|
|
59
|
+
return ("p", copy.deepcopy(value))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def decode(value, buffers, memo):
|
|
63
|
+
tag, *rest = value
|
|
64
|
+
if tag == "a":
|
|
65
|
+
dtype, offset, shape = rest
|
|
66
|
+
key = (dtype, offset, tuple(shape))
|
|
67
|
+
if key not in memo:
|
|
68
|
+
length = int(np.prod(shape))
|
|
69
|
+
a = buffers[dtype][offset : offset + length].reshape(shape)
|
|
70
|
+
a.flags.writeable = False
|
|
71
|
+
memo[key] = a
|
|
72
|
+
return memo[key]
|
|
73
|
+
if tag == "p":
|
|
74
|
+
return rest[0]
|
|
75
|
+
if tag == "d":
|
|
76
|
+
return {k: decode(v, buffers, memo) for k, v in rest[0]}
|
|
77
|
+
if tag in ("l", "t"):
|
|
78
|
+
values = [decode(v, buffers, memo) for v in rest[0]]
|
|
79
|
+
return values if tag == "l" else tuple(values)
|
|
80
|
+
if tag == "v":
|
|
81
|
+
result = object.__new__(_ResolutionView)
|
|
82
|
+
result.__dict__.update(decode(rest[0], buffers, memo))
|
|
83
|
+
return result
|
|
84
|
+
raise ValueError("Unsupported artifact tag")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def pack(encoder, classes, heads, params):
|
|
88
|
+
pool = Pool()
|
|
89
|
+
enc = encode(vars(encoder), pool)
|
|
90
|
+
values, records, cursor = [], [], 0
|
|
91
|
+
sizes = {h.table_size for h in heads}
|
|
92
|
+
if len(sizes) != 1:
|
|
93
|
+
raise ValueError("Frozen native heads require a common table size")
|
|
94
|
+
|
|
95
|
+
def coefficients(a):
|
|
96
|
+
nonlocal cursor
|
|
97
|
+
if a.dtype != np.float64 or a.ndim != 1:
|
|
98
|
+
raise ValueError("Predictive coefficient dtype/shape must remain float64/1D")
|
|
99
|
+
record = (cursor, len(a))
|
|
100
|
+
cursor += len(a)
|
|
101
|
+
values.append(a.copy())
|
|
102
|
+
return record
|
|
103
|
+
|
|
104
|
+
for head in heads:
|
|
105
|
+
levels = []
|
|
106
|
+
for level in head.levels:
|
|
107
|
+
levels.append(
|
|
108
|
+
(
|
|
109
|
+
level["n_bins"],
|
|
110
|
+
pool.add(level["main_features"]),
|
|
111
|
+
tuple(coefficients(a) for a in level["main_tables"]),
|
|
112
|
+
tuple(pool.add(a) for a in level["groups"]),
|
|
113
|
+
tuple(tuple(pool.add(a) for a in group) for group in level["codes"]),
|
|
114
|
+
tuple(coefficients(a) for a in level["tables"]),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
records.append((head.intercept, tuple(levels)))
|
|
118
|
+
dense = np.concatenate(values) if values else np.empty(0, dtype=np.float64)
|
|
119
|
+
return (
|
|
120
|
+
1,
|
|
121
|
+
enc,
|
|
122
|
+
classes.copy(),
|
|
123
|
+
pool.buffers(),
|
|
124
|
+
dense,
|
|
125
|
+
tuple(records),
|
|
126
|
+
sizes.pop(),
|
|
127
|
+
copy.deepcopy(params),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def unpack(wire):
|
|
132
|
+
version, enc, classes, buffers, values, records, table_size, params = wire
|
|
133
|
+
if version != 1 or values.dtype != np.float64:
|
|
134
|
+
raise ValueError("Unsupported artifact schema or coefficient dtype")
|
|
135
|
+
for buffer in [values, *buffers.values()]:
|
|
136
|
+
buffer.flags.writeable = False
|
|
137
|
+
memo = {}
|
|
138
|
+
encoder = object.__new__(BucketEncoder)
|
|
139
|
+
encoder.__dict__.update(decode(enc, buffers, memo))
|
|
140
|
+
|
|
141
|
+
def coefficients(ref):
|
|
142
|
+
offset, length = ref
|
|
143
|
+
a = values[offset : offset + length]
|
|
144
|
+
a.flags.writeable = False
|
|
145
|
+
return a
|
|
146
|
+
|
|
147
|
+
result = []
|
|
148
|
+
for intercept, levels in records:
|
|
149
|
+
result.append(
|
|
150
|
+
(
|
|
151
|
+
intercept,
|
|
152
|
+
[
|
|
153
|
+
dict(
|
|
154
|
+
n_bins=n,
|
|
155
|
+
main_features=decode(main, buffers, memo),
|
|
156
|
+
main_tables=[coefficients(a) for a in main_values],
|
|
157
|
+
groups=[decode(a, buffers, memo) for a in groups],
|
|
158
|
+
codes=[[decode(a, buffers, memo) for a in group] for group in codes],
|
|
159
|
+
tables=[coefficients(a) for a in tables],
|
|
160
|
+
)
|
|
161
|
+
for n, main, main_values, groups, codes, tables in levels
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
return encoder, classes, result, table_size, params
|
codadapt/_training.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Block updates; all row-dependent training state stays local to this module."""
|
|
2
|
+
|
|
3
|
+
from time import perf_counter
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ._core import GAIN_RTOL, addresses, aggregate, gain, sigmoid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def data_loss(y, score, weight, binary):
|
|
11
|
+
loss = np.logaddexp(0.0, score) - y * score if binary else 0.5 * (y - score) ** 2
|
|
12
|
+
return float(np.dot(weight, loss))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def objective(model, y, score, weight, binary):
|
|
16
|
+
penalty = sum(float(np.dot(v, v)) for v in model.main_tables_ + model.tables_)
|
|
17
|
+
return data_loss(y, score, weight, binary) + 0.5 * model.l2 * penalty
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _surrogate(y, score, old, weight, binary):
|
|
21
|
+
if binary:
|
|
22
|
+
return weight / 4.0, old + 4.0 * (y - sigmoid(score))
|
|
23
|
+
return weight, y - (score - old)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _selected_groups(feature_gains, n_tables):
|
|
27
|
+
"""Select a bounded set of high-signal pairs without scanning all combinations."""
|
|
28
|
+
p = len(feature_gains)
|
|
29
|
+
if not n_tables or not p:
|
|
30
|
+
return []
|
|
31
|
+
if p == 1:
|
|
32
|
+
return [np.array([0], dtype=np.intp)]
|
|
33
|
+
order = np.argsort(-np.asarray(feature_gains), kind="stable")
|
|
34
|
+
groups, seen = [], set()
|
|
35
|
+
for offset in range(1, p):
|
|
36
|
+
for start in range(p):
|
|
37
|
+
pair = tuple(sorted((int(order[start]), int(order[(start + offset) % p]))))
|
|
38
|
+
if pair in seen:
|
|
39
|
+
continue
|
|
40
|
+
seen.add(pair)
|
|
41
|
+
groups.append(np.asarray(pair, dtype=np.intp))
|
|
42
|
+
if len(groups) == n_tables:
|
|
43
|
+
return groups
|
|
44
|
+
return groups
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def predict_coded_memory_buckets(levels, encoders, z_levels, intercept, table_size):
|
|
48
|
+
"""Add level memories for already encoded inputs."""
|
|
49
|
+
result = np.full(len(z_levels[0]), intercept, dtype=np.float64)
|
|
50
|
+
for level, encoder, z in zip(levels, encoders, z_levels):
|
|
51
|
+
main_features = level.get("main_features", range(len(level["main_tables"])))
|
|
52
|
+
for feature, values in zip(main_features, level["main_tables"]):
|
|
53
|
+
result += values[z[:, feature]]
|
|
54
|
+
for features, codes, values in zip(level["groups"], level["codes"], level["tables"]):
|
|
55
|
+
index = addresses(z, features, codes, table_size)
|
|
56
|
+
supported = np.ones(len(z), dtype=bool)
|
|
57
|
+
for feature in features:
|
|
58
|
+
supported &= encoder.support_[feature][z[:, feature]]
|
|
59
|
+
result += np.where(supported, values[index], 0.0)
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _combination_count(z, features):
|
|
64
|
+
return int(np.unique(z[:, features], axis=0).shape[0])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def train_coded_memory(model, z_levels, y, weight, z_valid_levels, y_valid, rng, *, cache=None):
|
|
68
|
+
"""Train the shared memory engine, optionally reusing fit-local OVR statistics.
|
|
69
|
+
|
|
70
|
+
The cache supplies the same primitives with X-only reuse across binary heads.
|
|
71
|
+
It is never retained in the fitted estimator or used during prediction.
|
|
72
|
+
"""
|
|
73
|
+
address = addresses if cache is None else cache.address
|
|
74
|
+
reduce = aggregate if cache is None else cache.aggregate
|
|
75
|
+
surrogate = _surrogate if cache is None else cache.surrogate
|
|
76
|
+
combination_count = _combination_count if cache is None else cache.combination_count
|
|
77
|
+
binary = model._task == "classification"
|
|
78
|
+
n, p = z_levels[0].shape
|
|
79
|
+
mean = float(np.average(y, weights=weight))
|
|
80
|
+
if binary:
|
|
81
|
+
positive_mass = float(weight[y == 1].sum())
|
|
82
|
+
negative_mass = float(weight[y == 0].sum())
|
|
83
|
+
model.intercept_ = float(np.log(positive_mass) - np.log(negative_mass))
|
|
84
|
+
else:
|
|
85
|
+
model.intercept_ = mean
|
|
86
|
+
score = np.full(n, model.intercept_, dtype=np.float64)
|
|
87
|
+
model.levels_, model.level_history_, model.allocation_history_ = [], [], []
|
|
88
|
+
model.training_history_ = []
|
|
89
|
+
model.timings_.update(
|
|
90
|
+
score_updates=0.0,
|
|
91
|
+
code_search=0.0,
|
|
92
|
+
validation=0.0,
|
|
93
|
+
restore=0.0,
|
|
94
|
+
residual=0.0,
|
|
95
|
+
candidate_selection=0.0,
|
|
96
|
+
candidate_scoring=0.0,
|
|
97
|
+
candidate_application=0.0,
|
|
98
|
+
)
|
|
99
|
+
interactions_enabled = model.n_tables > 0
|
|
100
|
+
stopping_enabled = model.early_stopping and z_valid_levels is not None
|
|
101
|
+
collision_aware = True
|
|
102
|
+
shared_screening = True
|
|
103
|
+
active_feature_mask = np.ones(p, dtype=bool)
|
|
104
|
+
previous_feature_gains = np.full(p, np.inf, dtype=np.float64)
|
|
105
|
+
screening_seconds = 0.0
|
|
106
|
+
prefilter_discarded = 0
|
|
107
|
+
fully_screened = 0
|
|
108
|
+
remaining_budget = int(model.lookup_budget)
|
|
109
|
+
cumulative_memory = 0
|
|
110
|
+
cumulative_penalty = 0.0
|
|
111
|
+
best_validation = None
|
|
112
|
+
if z_valid_levels is not None:
|
|
113
|
+
initial = np.full(len(y_valid), model.intercept_, dtype=np.float64)
|
|
114
|
+
if binary:
|
|
115
|
+
best_validation = float(np.mean(np.logaddexp(0.0, initial) - y_valid * initial))
|
|
116
|
+
else:
|
|
117
|
+
best_validation = float(np.mean((y_valid - initial) ** 2))
|
|
118
|
+
|
|
119
|
+
for level_number, (z, encoder) in enumerate(zip(z_levels, model.encoders_), start=1):
|
|
120
|
+
if remaining_budget <= 0:
|
|
121
|
+
break
|
|
122
|
+
level_started = perf_counter()
|
|
123
|
+
score_before = score.copy()
|
|
124
|
+
penalty_before = cumulative_penalty
|
|
125
|
+
objective_before = data_loss(y, score, weight, binary)
|
|
126
|
+
objective_before += 0.5 * model.l2 * cumulative_penalty
|
|
127
|
+
levels_remaining = len(z_levels) - level_number + 1
|
|
128
|
+
level_budget = max(1, remaining_budget // levels_remaining)
|
|
129
|
+
|
|
130
|
+
screening_started = perf_counter()
|
|
131
|
+
candidate_features = np.flatnonzero(active_feature_mask)
|
|
132
|
+
h, residual_target = surrogate(y, score, np.zeros(n), weight, binary)
|
|
133
|
+
screened_features = candidate_features
|
|
134
|
+
if shared_screening and len(candidate_features):
|
|
135
|
+
sample_step = max(1, n // 512)
|
|
136
|
+
sample = np.arange(0, n, sample_step, dtype=np.intp)[:512]
|
|
137
|
+
sample_response = h[sample] * residual_target[sample]
|
|
138
|
+
cheap_scores = []
|
|
139
|
+
for feature in candidate_features:
|
|
140
|
+
size = encoder.n_buckets_[feature]
|
|
141
|
+
mass = np.bincount(z[sample, feature], minlength=size)
|
|
142
|
+
response = np.bincount(z[sample, feature], weights=sample_response, minlength=size)
|
|
143
|
+
cheap_scores.append(float(np.dot(response, response / (mass + model.l2))))
|
|
144
|
+
pool_size = min(
|
|
145
|
+
len(candidate_features),
|
|
146
|
+
max(2 * max(1, level_budget), (len(candidate_features) + 1) // 2),
|
|
147
|
+
)
|
|
148
|
+
cheap_order = np.argsort(-np.asarray(cheap_scores), kind="stable")
|
|
149
|
+
screened_features = candidate_features[cheap_order[:pool_size]]
|
|
150
|
+
discarded = candidate_features[cheap_order[pool_size:]]
|
|
151
|
+
active_feature_mask[discarded] = False
|
|
152
|
+
prefilter_discarded += len(discarded)
|
|
153
|
+
feature_gains = np.zeros(p, dtype=np.float64)
|
|
154
|
+
for feature in screened_features:
|
|
155
|
+
a, b = reduce(z[:, feature], h, residual_target, encoder.n_buckets_[feature])
|
|
156
|
+
feature_gains[feature] = gain(a, b, model.l2)
|
|
157
|
+
fully_screened += len(screened_features)
|
|
158
|
+
order = screened_features[np.argsort(-feature_gains[screened_features], kind="stable")]
|
|
159
|
+
maximum_feature_gain = float(feature_gains[order[0]]) if len(order) else 0.0
|
|
160
|
+
feature_slots = (
|
|
161
|
+
level_budget if not interactions_enabled else max(1, (2 * level_budget) // 3)
|
|
162
|
+
)
|
|
163
|
+
selected_features = [
|
|
164
|
+
int(feature)
|
|
165
|
+
for feature in order
|
|
166
|
+
if feature_gains[feature] >= 0.02 * maximum_feature_gain
|
|
167
|
+
][: min(feature_slots, p)]
|
|
168
|
+
if not model.main_effects or maximum_feature_gain <= GAIN_RTOL:
|
|
169
|
+
selected_features = []
|
|
170
|
+
if shared_screening:
|
|
171
|
+
selected_set = set(selected_features)
|
|
172
|
+
for feature in screened_features:
|
|
173
|
+
weak_relative = feature_gains[feature] < 0.03 * maximum_feature_gain
|
|
174
|
+
weak_progress = (
|
|
175
|
+
np.isfinite(previous_feature_gains[feature])
|
|
176
|
+
and feature_gains[feature] < 0.10 * previous_feature_gains[feature]
|
|
177
|
+
)
|
|
178
|
+
if feature not in selected_set or weak_relative or weak_progress:
|
|
179
|
+
active_feature_mask[feature] = False
|
|
180
|
+
else:
|
|
181
|
+
previous_feature_gains[feature] = feature_gains[feature]
|
|
182
|
+
screening_seconds += perf_counter() - screening_started
|
|
183
|
+
|
|
184
|
+
main_tables = []
|
|
185
|
+
for feature in selected_features:
|
|
186
|
+
h, residual_target = surrogate(y, score, np.zeros(n, dtype=np.float64), weight, binary)
|
|
187
|
+
a, b = reduce(z[:, feature], h, residual_target, encoder.n_buckets_[feature])
|
|
188
|
+
values = b / (a + model.l2)
|
|
189
|
+
score += values[z[:, feature]]
|
|
190
|
+
cumulative_penalty += float(np.dot(values, values))
|
|
191
|
+
main_tables.append(values)
|
|
192
|
+
|
|
193
|
+
interaction_slots = min(model.n_tables, max(0, level_budget - len(selected_features)))
|
|
194
|
+
interaction_candidates = []
|
|
195
|
+
if interactions_enabled and interaction_slots:
|
|
196
|
+
candidate_count = min(max(2 * interaction_slots, 1), max(2 * model.n_tables, 1))
|
|
197
|
+
for features in _selected_groups(feature_gains, candidate_count):
|
|
198
|
+
group_codes = [
|
|
199
|
+
rng.randint(model.table_size, size=encoder.n_buckets_[feature]).astype(
|
|
200
|
+
np.uint16
|
|
201
|
+
)
|
|
202
|
+
for feature in features
|
|
203
|
+
]
|
|
204
|
+
index = address(z, features, group_codes, model.table_size)
|
|
205
|
+
h, residual_target = surrogate(
|
|
206
|
+
y, score, np.zeros(n, dtype=np.float64), weight, binary
|
|
207
|
+
)
|
|
208
|
+
a, b = reduce(index, h, residual_target, model.table_size)
|
|
209
|
+
candidate_gain = gain(a, b, model.l2)
|
|
210
|
+
combinations = combination_count(z, features)
|
|
211
|
+
occupied = int(np.unique(index).size)
|
|
212
|
+
collisions = max(0, combinations - occupied)
|
|
213
|
+
collision_rate = collisions / combinations if combinations else 0.0
|
|
214
|
+
utility = (
|
|
215
|
+
candidate_gain / (1.0 + 3.0 * collision_rate)
|
|
216
|
+
if collision_aware
|
|
217
|
+
else candidate_gain
|
|
218
|
+
)
|
|
219
|
+
interaction_candidates.append(
|
|
220
|
+
(
|
|
221
|
+
utility,
|
|
222
|
+
candidate_gain,
|
|
223
|
+
collision_rate,
|
|
224
|
+
combinations,
|
|
225
|
+
collisions,
|
|
226
|
+
features,
|
|
227
|
+
group_codes,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
interaction_candidates.sort(key=lambda item: item[0], reverse=True)
|
|
231
|
+
|
|
232
|
+
codes, groups, tables = [], [], []
|
|
233
|
+
collision_counts, combination_counts = [], []
|
|
234
|
+
top_interaction_gain = max(
|
|
235
|
+
(candidate[1] for candidate in interaction_candidates), default=0.0
|
|
236
|
+
)
|
|
237
|
+
for candidate in interaction_candidates:
|
|
238
|
+
if len(tables) >= interaction_slots:
|
|
239
|
+
break
|
|
240
|
+
_, candidate_gain, collision_rate, combinations, collisions, features, group_codes = (
|
|
241
|
+
candidate
|
|
242
|
+
)
|
|
243
|
+
if candidate_gain < 0.05 * top_interaction_gain:
|
|
244
|
+
continue
|
|
245
|
+
if (
|
|
246
|
+
collision_aware
|
|
247
|
+
and collision_rate > 0.55
|
|
248
|
+
and candidate_gain < 0.5 * top_interaction_gain
|
|
249
|
+
):
|
|
250
|
+
continue
|
|
251
|
+
index = address(z, features, group_codes, model.table_size)
|
|
252
|
+
h, residual_target = surrogate(y, score, np.zeros(n, dtype=np.float64), weight, binary)
|
|
253
|
+
a, b = reduce(index, h, residual_target, model.table_size)
|
|
254
|
+
values = b / (a + model.l2)
|
|
255
|
+
score += values[index]
|
|
256
|
+
cumulative_penalty += float(np.dot(values, values))
|
|
257
|
+
groups.append(features)
|
|
258
|
+
codes.append(group_codes)
|
|
259
|
+
tables.append(values)
|
|
260
|
+
collision_counts.append(collisions)
|
|
261
|
+
combination_counts.append(combinations)
|
|
262
|
+
|
|
263
|
+
lookup_count = len(selected_features) + len(tables)
|
|
264
|
+
if lookup_count == 0:
|
|
265
|
+
break
|
|
266
|
+
level_memory = sum(values.nbytes for values in main_tables + tables)
|
|
267
|
+
level_memory += sum(code.nbytes for group in codes for code in group)
|
|
268
|
+
total_combinations = int(sum(combination_counts))
|
|
269
|
+
total_collisions = int(sum(collision_counts))
|
|
270
|
+
collision_rate = total_collisions / total_combinations if total_combinations else 0.0
|
|
271
|
+
level = {
|
|
272
|
+
"n_bins": int(model.resolution_bins_[level_number - 1]),
|
|
273
|
+
"main_features": np.asarray(selected_features, dtype=np.intp),
|
|
274
|
+
"main_tables": main_tables,
|
|
275
|
+
"groups": groups,
|
|
276
|
+
"codes": codes,
|
|
277
|
+
"tables": tables,
|
|
278
|
+
}
|
|
279
|
+
candidate_levels = model.levels_ + [level]
|
|
280
|
+
validation_loss = None
|
|
281
|
+
validation_gain = np.nan
|
|
282
|
+
if z_valid_levels is not None:
|
|
283
|
+
validation_started = perf_counter()
|
|
284
|
+
validation_prediction = predict_coded_memory_buckets(
|
|
285
|
+
candidate_levels,
|
|
286
|
+
model.encoders_[:level_number],
|
|
287
|
+
z_valid_levels[:level_number],
|
|
288
|
+
model.intercept_,
|
|
289
|
+
model.table_size,
|
|
290
|
+
)
|
|
291
|
+
if binary:
|
|
292
|
+
validation_loss = float(
|
|
293
|
+
np.mean(
|
|
294
|
+
np.logaddexp(0.0, validation_prediction) - y_valid * validation_prediction
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
validation_loss = float(np.mean((y_valid - validation_prediction) ** 2))
|
|
299
|
+
validation_gain = float(best_validation - validation_loss)
|
|
300
|
+
model.timings_["validation"] += perf_counter() - validation_started
|
|
301
|
+
|
|
302
|
+
objective_value = data_loss(y, score, weight, binary)
|
|
303
|
+
objective_value += 0.5 * model.l2 * cumulative_penalty
|
|
304
|
+
gain_per_lookup = (objective_before - objective_value) / lookup_count
|
|
305
|
+
accepted = not stopping_enabled or validation_gain > model.tol * max(
|
|
306
|
+
1.0, abs(best_validation)
|
|
307
|
+
)
|
|
308
|
+
residual = y - sigmoid(score) if binary else y - score
|
|
309
|
+
history = {
|
|
310
|
+
"level": level_number,
|
|
311
|
+
"n_bins": level["n_bins"],
|
|
312
|
+
"accepted": bool(accepted),
|
|
313
|
+
"active_features": selected_features,
|
|
314
|
+
"active_interactions": len(tables),
|
|
315
|
+
"lookup_count": lookup_count,
|
|
316
|
+
"train_loss": float(data_loss(y, score, weight, binary) / weight.sum()),
|
|
317
|
+
"objective": float(objective_value),
|
|
318
|
+
"validation_loss": validation_loss,
|
|
319
|
+
"validation_gain": float(validation_gain),
|
|
320
|
+
"gain_per_lookup": float(gain_per_lookup),
|
|
321
|
+
"residual_mean_abs": float(np.average(np.abs(residual), weights=weight)),
|
|
322
|
+
"fit_seconds": float(perf_counter() - level_started),
|
|
323
|
+
"added_memory_bytes": int(level_memory),
|
|
324
|
+
"collision_count": total_collisions,
|
|
325
|
+
"combination_count": total_combinations,
|
|
326
|
+
"collision_rate": float(collision_rate),
|
|
327
|
+
}
|
|
328
|
+
model.allocation_history_.append(history.copy())
|
|
329
|
+
if not accepted:
|
|
330
|
+
score = score_before
|
|
331
|
+
cumulative_penalty = penalty_before
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
model.levels_.append(level)
|
|
335
|
+
cumulative_memory += int(level_memory)
|
|
336
|
+
remaining_budget -= lookup_count
|
|
337
|
+
history["cumulative_memory_bytes"] = int(cumulative_memory)
|
|
338
|
+
model.level_history_.append(history)
|
|
339
|
+
model.training_history_.append(
|
|
340
|
+
{
|
|
341
|
+
"iteration": len(model.levels_),
|
|
342
|
+
"train_loss": history["train_loss"],
|
|
343
|
+
"objective": history["objective"],
|
|
344
|
+
"validation_loss": validation_loss,
|
|
345
|
+
"coordinates_examined": 0,
|
|
346
|
+
"accepted_moves": 0,
|
|
347
|
+
"candidates_evaluated": len(interaction_candidates),
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
if validation_loss is not None:
|
|
351
|
+
best_validation = validation_loss
|
|
352
|
+
|
|
353
|
+
model.main_tables_ = [values for level in model.levels_ for values in level["main_tables"]]
|
|
354
|
+
model.groups_ = [group for level in model.levels_ for group in level["groups"]]
|
|
355
|
+
model.codes_ = [codes for level in model.levels_ for codes in level["codes"]]
|
|
356
|
+
model.tables_ = [values for level in model.levels_ for values in level["tables"]]
|
|
357
|
+
model.level_memory_bytes_ = int(cumulative_memory)
|
|
358
|
+
model.effective_levels_ = len(model.levels_)
|
|
359
|
+
model.active_features_per_level_ = [len(level["main_features"]) for level in model.levels_]
|
|
360
|
+
model.active_interactions_ = sum(len(level["tables"]) for level in model.levels_)
|
|
361
|
+
model.lookup_count_per_sample_ = sum(
|
|
362
|
+
len(level["main_tables"]) + len(level["tables"]) for level in model.levels_
|
|
363
|
+
)
|
|
364
|
+
model.n_iter_ = len(model.levels_)
|
|
365
|
+
model.screening_seconds_ = float(screening_seconds)
|
|
366
|
+
model.prefilter_discarded_ = int(prefilter_discarded)
|
|
367
|
+
model.fully_screened_candidates_ = int(fully_screened)
|
|
368
|
+
model.feature_level_stopping_ = int(np.count_nonzero(~active_feature_mask))
|
|
369
|
+
model.best_iteration_ = model.n_iter_
|
|
370
|
+
model.accepted_moves_ = 0
|
codadapt/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|