morphtree 2.1.0__py3-none-win_amd64.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.
- morphtree/__init__.py +28 -0
- morphtree/__main__.py +3 -0
- morphtree/_experimental/__init__.py +1 -0
- morphtree/_experimental/admission.py +364 -0
- morphtree/_experimental/base.py +215 -0
- morphtree/_experimental/component_cache_v2.py +263 -0
- morphtree/_experimental/evolution_report.py +307 -0
- morphtree/_experimental/execution_v2.py +697 -0
- morphtree/_experimental/facade.py +289 -0
- morphtree/_experimental/integrated.py +748 -0
- morphtree/_experimental/learning.py +418 -0
- morphtree/_experimental/manifest.json +204 -0
- morphtree/_experimental/numeric_fixture.py +21 -0
- morphtree/_experimental/parameter_contract.py +38 -0
- morphtree/_experimental/postfit.py +51 -0
- morphtree/_experimental/preprocessing.py +133 -0
- morphtree/_experimental/processes.py +191 -0
- morphtree/_experimental/producer.py +542 -0
- morphtree/_experimental/product.py +156 -0
- morphtree/_experimental/product_cli.py +43 -0
- morphtree/_experimental/roundtrip_v3.py +311 -0
- morphtree/_experimental/runtime_io.py +65 -0
- morphtree/_experimental/sampling.py +26 -0
- morphtree/_experimental/support.py +281 -0
- morphtree/_experimental/target.py +59 -0
- morphtree/_experimental/training.py +53 -0
- morphtree/_experimental/worker.py +682 -0
- morphtree/_installed.py +17 -0
- morphtree/_native/__init__.py +1 -0
- morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
- morphtree/_native/buffers.py +82 -0
- morphtree/_native/loader.py +39 -0
- morphtree/_native/manifest.json +29 -0
- morphtree/_native/runtime.py +48 -0
- morphtree/_native/src/histogram.c +61 -0
- morphtree/_native/src/traversal.c +51 -0
- morphtree/api.py +388 -0
- morphtree/api_compat.py +65 -0
- morphtree/backend.py +162 -0
- morphtree/booster.py +252 -0
- morphtree/cli.py +52 -0
- morphtree/config.py +262 -0
- morphtree/control.py +20 -0
- morphtree/data.py +300 -0
- morphtree/diagnostics.py +158 -0
- morphtree/estimators.py +156 -0
- morphtree/examples.py +25 -0
- morphtree/exporting.py +224 -0
- morphtree/legacy_artifacts.py +32 -0
- morphtree/metrics.py +156 -0
- morphtree/model.py +1010 -0
- morphtree/mutations.py +205 -0
- morphtree/native_lab_backend.py +79 -0
- morphtree/numpy_backend.py +180 -0
- morphtree/optuna_tuner.py +154 -0
- morphtree/persistence.py +414 -0
- morphtree/prediction_output.py +35 -0
- morphtree/prepared_bins.py +82 -0
- morphtree/public.py +106 -0
- morphtree/resource_plan.py +170 -0
- morphtree/scalable.py +781 -0
- morphtree/scalable_backend.py +74 -0
- morphtree/scalable_checkpoint.py +129 -0
- morphtree/scalable_data.py +255 -0
- morphtree/scalable_receipts.py +92 -0
- morphtree/scalable_target.py +68 -0
- morphtree/scalable_variants.py +133 -0
- morphtree/sklearn_view.py +79 -0
- morphtree/source_markers.py +25 -0
- morphtree/species.py +47 -0
- morphtree/temporary.py +65 -0
- morphtree/training_fragment.py +130 -0
- morphtree/tree.py +231 -0
- morphtree/tuning.py +150 -0
- morphtree-2.1.0.dist-info/METADATA +212 -0
- morphtree-2.1.0.dist-info/RECORD +79 -0
- morphtree-2.1.0.dist-info/WHEEL +4 -0
- morphtree-2.1.0.dist-info/entry_points.txt +2 -0
- morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
morphtree/scalable.py
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"""Private columnar evolutionary facade, not a change to published 2.0.0.
|
|
2
|
+
|
|
3
|
+
Same native boosters, two genuine native families, current-parent OOF diagnosis,
|
|
4
|
+
two executable operators, verified SEARCH acceptance, final frozen-program
|
|
5
|
+
Optuna and explicit all-input refit. No source dataset JSON materialization.
|
|
6
|
+
"""
|
|
7
|
+
from array import array
|
|
8
|
+
from collections import Counter
|
|
9
|
+
import copy
|
|
10
|
+
from dataclasses import asdict,replace
|
|
11
|
+
import gc
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import statistics
|
|
17
|
+
import time
|
|
18
|
+
from .booster import BinaryHistogramBooster
|
|
19
|
+
from .config import AlgorithmGenome,HyperParams
|
|
20
|
+
from .persistence import decode_json_bytes,LoadLimits,decode_booster,decode_multiclass_booster,atomic_text,regular_file_size,read_regular_bytes
|
|
21
|
+
from .scalable_data import Input,Table,Encoder,Resources,scalar
|
|
22
|
+
from .scalable_backend import ScalableBackend
|
|
23
|
+
from .scalable_receipts import Ledger,verify as verify_receipts,data_identity,encoder_digest
|
|
24
|
+
from .prepared_bins import BinCache
|
|
25
|
+
from .resource_plan import fit_plan,memory_allowance,inference_seconds,io_seconds,memory_snapshot,numeric_capacity_plan
|
|
26
|
+
from .scalable_target import detect_target,label_scalar,compact_oof_train_indices
|
|
27
|
+
from .source_markers import controlled_source
|
|
28
|
+
from ._experimental.target import _split
|
|
29
|
+
from ._experimental.sampling import bootstrap,oof_folds
|
|
30
|
+
|
|
31
|
+
_PACKAGE=Path(__file__).resolve().parent
|
|
32
|
+
_ENGINE_SHA256=hashlib.sha256((_PACKAGE/'_experimental/manifest.json').read_bytes()).hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def verify_source_identity():
|
|
36
|
+
raw=(_PACKAGE/'_experimental/manifest.json').read_bytes()
|
|
37
|
+
if hashlib.sha256(raw).hexdigest()!=_ENGINE_SHA256:raise ImportError('Changed source manifest since import')
|
|
38
|
+
m=json.loads(raw)
|
|
39
|
+
actual={p.relative_to(_PACKAGE).as_posix() for p in _PACKAGE.rglob('*') if p.is_file() and '__pycache__' not in p.parts}
|
|
40
|
+
if actual!=set(m['files'])|{'_experimental/manifest.json'}:raise ImportError('Missing/unexpected private runtime source')
|
|
41
|
+
for n,h in m['files'].items():
|
|
42
|
+
if hashlib.sha256((_PACKAGE/n).read_bytes()).hexdigest()!=h:raise ImportError('Changed private engine source')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def digest(v):return hashlib.sha256(json.dumps(v,sort_keys=True,allow_nan=False,separators=(',',':')).encode()).hexdigest()
|
|
46
|
+
def point_loss(task,y,p):
|
|
47
|
+
# Invalid internal predictions must stop selection, not become an apparently
|
|
48
|
+
# excellent loss through NaN comparison/clipping or zip truncation.
|
|
49
|
+
if task not in ('binary','multiclass','regression') or not len(y) or len(y)!=len(p):raise ValueError('Known task and nonempty aligned loss inputs required')
|
|
50
|
+
if task=='regression':
|
|
51
|
+
if any(not math.isfinite(a) or not math.isfinite(b) or not math.isfinite((a-b)**2) for a,b in zip(y,p)):raise ValueError('Finite regression loss inputs and squared errors required')
|
|
52
|
+
elif task=='binary':
|
|
53
|
+
if any(a not in (0,1) or not math.isfinite(b) or not 0<=b<=1 for a,b in zip(y,p)):raise ValueError('Binary labels and finite probabilities in [0,1] required')
|
|
54
|
+
else:
|
|
55
|
+
k=len(p[0])
|
|
56
|
+
if k<2 or any(type(a) is not int or not 0<=a<k or len(b)!=k or any(not math.isfinite(v) or not 0<=v<=1 for v in b) or abs(math.fsum(b)-1)>1e-10 for a,b in zip(y,p)):raise ValueError('Aligned multiclass labels and finite probability simplexes required')
|
|
57
|
+
if task=='regression':return array('d',((a-b)**2 for a,b in zip(y,p)))
|
|
58
|
+
if task=='binary':return array('d',(-math.log(max(1e-15,min(1-1e-15,b if a else 1-b))) for a,b in zip(y,p)))
|
|
59
|
+
return array('d',(-math.log(max(1e-15,min(1-1e-15,b[a]))) for a,b in zip(y,p)))
|
|
60
|
+
def risk(task,y,p):return statistics.mean(point_loss(task,y,p))
|
|
61
|
+
def available_memory():
|
|
62
|
+
try:
|
|
63
|
+
import os
|
|
64
|
+
if os.name=='nt':
|
|
65
|
+
import ctypes
|
|
66
|
+
class Status(ctypes.Structure):_fields_=[('length',ctypes.c_ulong),('load',ctypes.c_ulong)]+[(n,ctypes.c_ulonglong) for n in ('total','available','page_total','page_available','virtual_total','virtual_available','extended')]
|
|
67
|
+
s=Status();s.length=ctypes.sizeof(s)
|
|
68
|
+
if not ctypes.WinDLL('kernel32',use_last_error=True).GlobalMemoryStatusEx(ctypes.byref(s)):raise OSError('RAM query')
|
|
69
|
+
return s.available
|
|
70
|
+
return os.sysconf('SC_AVPHYS_PAGES')*os.sysconf('SC_PAGE_SIZE')
|
|
71
|
+
except (OSError,ValueError,AttributeError):return 512*1024**2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Family:
|
|
75
|
+
def __init__(self,task,genome,params,encoder,models,family):self.task,self.genome,self.params,self.encoder,self.models,self.family=task,genome,params,encoder,models,family
|
|
76
|
+
def probabilities(self,table,resources=None):
|
|
77
|
+
if not self.models or not len(table):raise ValueError('Nonempty family and observations required')
|
|
78
|
+
k=self.models[0].n_classes if self.task=='multiclass' else 1
|
|
79
|
+
if resources is None:resources=managed_resources('auto',inference_seconds(len(table),table.width,sum(len(m.trees) for m in self.models),self.params.max_depth))
|
|
80
|
+
output_bytes=len(table)*(64+40*k if self.task=='multiclass' else 40)+64
|
|
81
|
+
member_row_bytes=64+40*k if self.task=='multiclass' else 40
|
|
82
|
+
row_bytes=table.width*32+len(self.models)*member_row_bytes+256
|
|
83
|
+
resources.reserve(output_bytes+row_bytes*4,'Family prediction output and member workspace')
|
|
84
|
+
batch=max(1,min(16384,(resources.bytes_limit-output_bytes)//max(1,row_bytes*4)));result=[]
|
|
85
|
+
for start in range(0,len(table),batch):
|
|
86
|
+
resources.check();stop=min(len(table),start+batch);block=table if start==0 and stop==len(table) else table.take(range(start,stop));X=self.encoder.transform(block);parts=[]
|
|
87
|
+
for model in self.models:
|
|
88
|
+
resources.check();parts.append(model.predict_raw(X) if self.task=='regression' else model.predict_proba(X))
|
|
89
|
+
# Preserve original member order and math.fsum for each observation.
|
|
90
|
+
if self.task=='multiclass':result.extend([[math.fsum(v[i][j] for v in parts)/len(parts) for j in range(k)] for i in range(len(block))])
|
|
91
|
+
else:result.extend([math.fsum(v[i] for v in parts)/len(parts) for i in range(len(block))])
|
|
92
|
+
del parts,X,block
|
|
93
|
+
resources.check();return result
|
|
94
|
+
def packet(self):return dict(task=self.task,family=self.family,genome=self.genome.to_dict(),params=asdict(self.params),encoder=self.encoder.state(),members=[m.to_dict() for m in self.models])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def managed_resources(memory_budget,seconds,observer=None):
|
|
98
|
+
observation=memory_snapshot(available_memory());observed=observation['effective_available_bytes'];res=Resources(memory_allowance(memory_budget,observed),seconds,observer)
|
|
99
|
+
res.available_bytes_snapshot=observed;res.memory_observation=observation;res.base_commit=res.peak_commit=observation['process_private_bytes'];res.pressure_reader=lambda:memory_snapshot(available_memory());return res
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def mutation_order(regions,guidance=True):
|
|
103
|
+
"""Research control changes only proposal order; OOF is still computed."""
|
|
104
|
+
if type(guidance) is not bool:raise ValueError('Typed research guidance flag required')
|
|
105
|
+
return ('split_program','growth_policy') if regions and guidance else ('growth_policy','split_program')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def streaming_loss_regions(table,point,folds,resources,primary=None):
|
|
109
|
+
n=len(table)
|
|
110
|
+
if not n or len(point)!=n:raise ValueError('Aligned nonempty OOF point losses')
|
|
111
|
+
resources.reserve(n+table.width*256,'Compact diagnostic masks and output metadata')
|
|
112
|
+
if primary is None:primary=statistics.mean(point)
|
|
113
|
+
groups=[]
|
|
114
|
+
def checked_column(feature):
|
|
115
|
+
for index,value in enumerate(table.column(feature)):
|
|
116
|
+
if index%4096==0:resources.check()
|
|
117
|
+
yield value
|
|
118
|
+
for feature,kind in enumerate(table.kinds):
|
|
119
|
+
resources.check()
|
|
120
|
+
if kind=='categorical':
|
|
121
|
+
resources.reserve(n+table.width*256+min(n,len(table.vocab[feature])+1)*192,'Nominal diagnostic mode dictionary')
|
|
122
|
+
cut=Counter(checked_column(feature)).most_common(1)[0][0]
|
|
123
|
+
mask=bytearray(value==cut for value in checked_column(feature))
|
|
124
|
+
else:
|
|
125
|
+
try:cut=statistics.mean(value for value in checked_column(feature) if math.isfinite(value))
|
|
126
|
+
except statistics.StatisticsError:cut=0
|
|
127
|
+
mask=bytearray(math.isfinite(value) and value>cut for value in checked_column(feature))
|
|
128
|
+
contrasts=[]
|
|
129
|
+
for held in folds:
|
|
130
|
+
resources.check();selected=sum(mask[index] for index in held)
|
|
131
|
+
if min(selected,len(held)-selected)>=8:
|
|
132
|
+
first=statistics.mean(point[index] for index in held if mask[index])
|
|
133
|
+
second=statistics.mean(point[index] for index in held if not mask[index])
|
|
134
|
+
contrasts.append(first-second)
|
|
135
|
+
if len(contrasts)==2 and min(contrasts)>.02*primary+.01:
|
|
136
|
+
groups.append(dict(feature=feature,fold_excess=contrasts))
|
|
137
|
+
resources.check()
|
|
138
|
+
return groups
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_PARAMETERS_V1=('random_state','verbosity','time_budget','memory_budget','use_optuna','n_trials','max_mutations','refit_full','capacity','checkpoint_dir')
|
|
142
|
+
_PARAMETERS_V2=_PARAMETERS_V1+('n_jobs','mode','operators')
|
|
143
|
+
_OPERATORS={'split_program:objective_balance':'split_program','growth_policy:depthwise':'growth_policy'}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _Scalable:
|
|
147
|
+
task='auto'
|
|
148
|
+
def __init__(self,random_state=None,verbosity=0,*,time_budget='auto',memory_budget='auto',use_optuna=True,n_trials='auto',max_mutations=2,refit_full=True,capacity=None,checkpoint_dir='auto',n_jobs=1,mode='adaptive',operators=None):
|
|
149
|
+
if random_state is not None and type(random_state) is not int or type(use_optuna) is not bool or type(refit_full) is not bool:raise ValueError('Integer seed or None and boolean stage controls required')
|
|
150
|
+
if type(max_mutations) is not int or max_mutations<0:raise ValueError('Nonnegative integer mutation budget required')
|
|
151
|
+
if n_trials!='auto' and (type(n_trials) is not int or n_trials<0):raise ValueError('Nonnegative integer trial budget or auto required')
|
|
152
|
+
if n_jobs!='auto' and (type(n_jobs) is not int or n_jobs!=1):raise ValueError('This serial native controller supports n_jobs=1 or auto')
|
|
153
|
+
if mode not in ('adaptive','baseline','research'):raise ValueError('Known baseline/adaptive/research mode required')
|
|
154
|
+
if mode=='baseline' and operators is not None:raise ValueError('Baseline has no structural operators')
|
|
155
|
+
if mode=='research' and operators!=('growth_policy:depthwise',):raise ValueError('Research requires explicit growth_policy:depthwise')
|
|
156
|
+
if operators is not None and (type(operators) is not tuple or not operators or any(type(v) is not str or v not in _OPERATORS for v in operators) or len(set(operators))!=len(operators)):raise ValueError('Distinct supported structural operators required')
|
|
157
|
+
if time_budget!='auto' and (type(time_budget) not in (int,float) or not math.isfinite(time_budget) or time_budget<=0):raise ValueError('Positive finite time allowance or auto')
|
|
158
|
+
if memory_budget!='auto' and (type(memory_budget) is not int or memory_budget<1):raise ValueError('Positive byte allowance or auto')
|
|
159
|
+
if type(verbosity) is not int or not 0<=verbosity<=2 or capacity is not None and type(capacity) is not dict:raise ValueError('Typed verbosity/capacity')
|
|
160
|
+
if capacity:replace(HyperParams(),**capacity)
|
|
161
|
+
if checkpoint_dir is not None and (not isinstance(checkpoint_dir,(str,Path)) or isinstance(checkpoint_dir,str) and not checkpoint_dir):raise ValueError('Local checkpoint parent path, auto or None required')
|
|
162
|
+
self.checkpoint_dir=checkpoint_dir
|
|
163
|
+
self.random_state=random_state;self.verbosity=verbosity;self.time_budget=time_budget;self.memory_budget=memory_budget;self.use_optuna=use_optuna;self.n_trials=n_trials;self.max_mutations=max_mutations;self.refit_full=refit_full;self.capacity=capacity;self.n_jobs=n_jobs;self.mode=mode;self.operators=operators;self._observer=None
|
|
164
|
+
def _resolved_controls(self):
|
|
165
|
+
# Revalidate direct parameter edits before input access or allocation.
|
|
166
|
+
type(self)(**self.get_params(deep=False))
|
|
167
|
+
genes=() if self.mode=='baseline' else tuple(_OPERATORS[v] for v in self.operators) if self.operators is not None else tuple(_OPERATORS.values())
|
|
168
|
+
# Each of the two qualified operators changes a gene to one fixed value.
|
|
169
|
+
# Along the single retained lineage at most m+(m-1)+...+1 children
|
|
170
|
+
# can be attempted; duplicate parent/program pairs are never retrained.
|
|
171
|
+
mutations=min(self.max_mutations,len(genes)*(len(genes)+1)//2)
|
|
172
|
+
trials=(2 if self.n_trials=='auto' else self.n_trials) if self.use_optuna else 0
|
|
173
|
+
return dict(mutations=mutations,trials=trials,genes=genes,families=('boost',) if self.mode=='baseline' else ('boost','bag'),
|
|
174
|
+
automatic_trial_policy='Conservative qualified count 2; numeric capacity and deadlines adapt to resources',
|
|
175
|
+
mutation_scope='Maximum attempts under current finite monotone operator grammar; not requested accepted mutations')
|
|
176
|
+
def __sklearn_tags__(self):
|
|
177
|
+
# Optional integration only; importing/training MorphTree needs no sklearn.
|
|
178
|
+
from sklearn.utils import Tags,TargetTags,InputTags,ClassifierTags,RegressorTags
|
|
179
|
+
classifier=self._estimator_type=='classifier'
|
|
180
|
+
return Tags(estimator_type=self._estimator_type,target_tags=TargetTags(required=True),
|
|
181
|
+
input_tags=InputTags(allow_nan=True,categorical=True,string=True),
|
|
182
|
+
classifier_tags=ClassifierTags() if classifier else None,
|
|
183
|
+
regressor_tags=None if classifier else RegressorTags())
|
|
184
|
+
def __sklearn_is_fitted__(self):return hasattr(self,'model_')
|
|
185
|
+
def as_sklearn(self):
|
|
186
|
+
"""Optional array-output view for sklearn scoring/Pipeline; same native estimator."""
|
|
187
|
+
from .sklearn_view import SklearnView
|
|
188
|
+
return SklearnView(self)
|
|
189
|
+
def _resources(self):
|
|
190
|
+
return managed_resources(self.memory_budget,150 if self.time_budget=='auto' else self.time_budget,self._observer)
|
|
191
|
+
def _bin_cache_limit(self,res):return min(32*1024**2,res.bytes_limit//8)
|
|
192
|
+
def fit(self,X,y):
|
|
193
|
+
previous=dict(self.__dict__);start=time.perf_counter();self.last_attempt_=dict(status='IN_PROGRESS',native_fits=0,OOF_fits=0,scalar_trees=0,Optuna_trials=0,cache_hits=0,events=[])
|
|
194
|
+
self._ledger=Ledger();self._attempt_anchor=None;self._active_checkpoint_dir_=None
|
|
195
|
+
self.last_attempt_['requested_stages']=dict(max_mutations=self.max_mutations,use_optuna=self.use_optuna,Optuna_trials=self.n_trials if self.use_optuna else 0,refit_full=self.refit_full,diagnostic_guidance=getattr(self,'_research_use_diagnosis',True))
|
|
196
|
+
try:
|
|
197
|
+
verify_source_identity();self._event('CALL_STARTED',engine_sha256=_ENGINE_SHA256,scope='PRIVATE_QUALITY019_NOT_PUBLISHED',requested_task=self.task)
|
|
198
|
+
type(self)(**self.get_params(deep=False))
|
|
199
|
+
self._requested_random_state=self.random_state
|
|
200
|
+
if self.random_state is None:
|
|
201
|
+
import random
|
|
202
|
+
self.random_state=random.SystemRandom().randrange(2**31)
|
|
203
|
+
self.last_attempt_['effective_seed']=self.random_state
|
|
204
|
+
self.last_attempt_['resolved_n_jobs']=1
|
|
205
|
+
self._fit(X,y);self.__dict__.pop('_retained_variants',None);self.__dict__.pop('_variant_selection',None);self.last_attempt_['status']='COMPLETED';self.last_attempt_['seconds']=time.perf_counter()-start;self.report_=self.last_attempt_
|
|
206
|
+
if self.last_attempt_['native_fits']:
|
|
207
|
+
from .scalable_checkpoint import commit
|
|
208
|
+
commit(self,self.model_.packet(),self.classes_,self._res,final=True)
|
|
209
|
+
self.last_attempt_['seconds']=time.perf_counter()-start;self.__dict__.pop('_res',None)
|
|
210
|
+
self.__dict__.pop('_interrupted_anchor',None);self.__dict__.pop('_interrupted_evidence',None)
|
|
211
|
+
return self
|
|
212
|
+
except BaseException as exc:
|
|
213
|
+
anchor=self._attempt_anchor;evidence=getattr(self,'diagnostic_profiles_',{})
|
|
214
|
+
attempt=self.last_attempt_;ledger=self._ledger
|
|
215
|
+
# Restore fitted state before any fallible error reporting. A single
|
|
216
|
+
# dictionary swap avoids exposing a cleared or partly restored state.
|
|
217
|
+
self.__dict__=previous
|
|
218
|
+
self.last_attempt_=attempt;self._interrupted_anchor=anchor;self._interrupted_evidence=evidence
|
|
219
|
+
try:message=str(exc)
|
|
220
|
+
except BaseException:message='<exception message unavailable>'
|
|
221
|
+
attempt.update(status='STOPPED_NO_RETRY',error=type(exc).__name__,message=message,seconds=time.perf_counter()-start)
|
|
222
|
+
if attempt['events']:
|
|
223
|
+
try:
|
|
224
|
+
attempt['events'].append(ledger.emit('FIT_STOPPED',dict(error_type=type(exc).__name__)))
|
|
225
|
+
attempt['failure_receipt_status']='RECORDED_PARTIAL_CHAIN'
|
|
226
|
+
except BaseException as receipt_error:
|
|
227
|
+
attempt['failure_receipt_status']='TERMINAL_EVENT_UNAVAILABLE'
|
|
228
|
+
attempt['failure_receipt_error']=type(receipt_error).__name__
|
|
229
|
+
else:attempt['failure_receipt_status']='CALL_CHAIN_NOT_STARTED'
|
|
230
|
+
raise
|
|
231
|
+
finally:
|
|
232
|
+
self.random_state=previous['random_state']
|
|
233
|
+
self.__dict__.pop('_requested_random_state',None)
|
|
234
|
+
def _event(self,kind,**kw):self.last_attempt_['events'].append(self._ledger.emit(kind,kw))
|
|
235
|
+
def _ordinary_context_guard(self):
|
|
236
|
+
# An active or orphaned scientific/quota context must never fall through
|
|
237
|
+
# to ordinary memory data. The legacy scientific API owns those contexts.
|
|
238
|
+
from .api import ACTIVE, PUBLIC_SCOPE
|
|
239
|
+
from ._experimental.admission import CURRENT, AdmissionError
|
|
240
|
+
from ._experimental.producer import PIPE_CURRENT
|
|
241
|
+
if any(value.get() is not None for value in (ACTIVE,CURRENT,PIPE_CURRENT,PUBLIC_SCOPE)):
|
|
242
|
+
raise AdmissionError({'status':'REJECTED','reason':'CONTROLLED_EXECUTION_CONTEXT_CANNOT_FALL_THROUGH_TO_ORDINARY_DATA'})
|
|
243
|
+
|
|
244
|
+
def _fit(self,X,y):
|
|
245
|
+
self._ordinary_context_guard()
|
|
246
|
+
from .native_lab_backend import bind_backend
|
|
247
|
+
backend_factory=bind_backend()
|
|
248
|
+
if any(controlled_source(obj) for obj in (X,y)):raise ValueError('Controlled sources cannot use this ordinary experimental input path')
|
|
249
|
+
controls=self._resolved_controls();mutations=controls['mutations'];trials=controls['trials'];self.last_attempt_['resolved_controls']=controls
|
|
250
|
+
res=self._resources();self._res=res;tick=time.perf_counter();source=Input(X)
|
|
251
|
+
hp=replace(HyperParams(n_estimators=2,learning_rate=1.,max_leaves=5,max_depth=3,max_bins=15,min_samples_leaf=4,row_fraction=1.,feature_fraction=1.),**(self.capacity or {}))
|
|
252
|
+
available=res.available_bytes_snapshot
|
|
253
|
+
plan=fit_plan(source.n,source.width,1 if self.task=='regression' else 2,hp,mutations,trials,self.refit_full,available,self.time_budget,res.bytes_limit)
|
|
254
|
+
res.configure_time(plan['seconds']);self.last_attempt_['resource_plan']=plan
|
|
255
|
+
res.reserve(source.n*source.width*10+source.n*(256+80*(1 if self.task=='regression' else 2))+source.width*512,'Minimum configured fit workspace before target/feature allocation')
|
|
256
|
+
task,targets,classes=detect_target(y,self.task,res.check,reserve=res.reserve)
|
|
257
|
+
if len(targets)!=source.n:raise ValueError('Aligned X/y required')
|
|
258
|
+
k=len(classes) if task!='regression' else 1
|
|
259
|
+
plan=fit_plan(source.n,source.width,k,hp,mutations,trials,self.refit_full,available,self.time_budget,res.bytes_limit)
|
|
260
|
+
plan=numeric_capacity_plan(plan,hp,mutations,trials,self.refit_full,'n_estimators' in (self.capacity or {}))
|
|
261
|
+
plan['memory_mode']='auto' if self.memory_budget=='auto' else 'explicit'
|
|
262
|
+
plan['memory_observation']=res.memory_observation
|
|
263
|
+
res.configure_time(plan['seconds']);self.last_attempt_['resource_plan']=plan;self._event('RESOURCE_PLAN',plan=plan)
|
|
264
|
+
res.reserve(plan['estimated_workspace_bytes'],'Conservative fit workspace before feature allocation')
|
|
265
|
+
raw=Table.read(source,res)
|
|
266
|
+
self._event('INPUT_PREPARED',rows=len(raw),features=raw.width,seconds=time.perf_counter()-tick,storage_bytes=raw.storage_bytes,input_identity=data_identity(raw,targets,'USER_INPUT'))
|
|
267
|
+
ti,si=_split(targets,task,self.random_state)
|
|
268
|
+
train,search=raw.take(ti),raw.take(si);ty=array(targets.typecode,(targets[i] for i in ti));sy=array(targets.typecode,(targets[i] for i in si));del ti,si
|
|
269
|
+
cache={};prepared=BinCache(self._bin_cache_limit(res),res.check);task_classes=k
|
|
270
|
+
def acquire(family,table,labels,genome,params,phase,seed):
|
|
271
|
+
res.check();enc=Encoder().fit(table);encoded=enc.transform(table)
|
|
272
|
+
identity=data_identity(table,labels,'TRAIN_FULL' if phase=='REFIT_FULL' else 'TRAIN');preprocessing=encoder_digest(enc)
|
|
273
|
+
key=digest(dict(data=identity,preprocessing_sha256=preprocessing,task=task,family=family,genome=genome.to_dict(),params=asdict(params),seed=seed,engine_sha256=_ENGINE_SHA256))
|
|
274
|
+
if key in cache:
|
|
275
|
+
self.last_attempt_['cache_hits']+=1;self._event('CACHE_HIT',phase=phase,family=family,rows=len(table),cache_key=key);return cache[key]
|
|
276
|
+
models=[];m=1 if family=='boost' else params.n_estimators
|
|
277
|
+
for member in range(m):
|
|
278
|
+
res.check();actual=params if family=='boost' else replace(params,n_estimators=1,learning_rate=1.)
|
|
279
|
+
if family=='bag':
|
|
280
|
+
ids=bootstrap(labels,task,seed+member+1);t=table.take(ids);yy=array(labels.typecode,(labels[i] for i in ids));del ids;xx=enc.transform(t)
|
|
281
|
+
else:xx,yy=encoded,labels
|
|
282
|
+
self.last_attempt_['native_fits']+=1;self.last_attempt_['OOF_fits']+=phase=='OOF';self.last_attempt_['scalar_trees']+=actual.n_estimators*(k if task=='multiclass' else 1)
|
|
283
|
+
actual_table=xx.source;effective_rows=hashlib.sha256(memoryview(actual_table.indices)).hexdigest() if actual_table.indices is not None else digest(dict(range_start=0,range_stop=len(actual_table)))
|
|
284
|
+
effective_labels=hashlib.sha256(memoryview(yy)).hexdigest();component=digest(dict(request=key,member=member,effective_rows=effective_rows,effective_labels=effective_labels,seed=seed+member,params=asdict(actual)))
|
|
285
|
+
self._event('NATIVE_FIT_STARTED',ordinal=self.last_attempt_['native_fits'],phase=phase,rows=len(yy),source_rows=len(table),family=family,cache_key=key,component_key=component,effective_rows_sha256=effective_rows,effective_labels_sha256=effective_labels,seed=seed+member,params_sha256=digest(asdict(actual)),preprocessing_sha256=preprocessing,program_sha256=digest(genome.to_dict()),role=identity['role'],reserved_scalar_trees=actual.n_estimators*(k if task=='multiclass' else 1))
|
|
286
|
+
res.check()
|
|
287
|
+
model=BinaryHistogramBooster(genome,actual,seed+member,backend_factory(encoded._nominal_sizes),memory_enabled=False,objective='softmax' if task=='multiclass' else 'squared_error' if task=='regression' else 'binary_logloss',n_classes=k if task=='multiclass' else None)
|
|
288
|
+
preparation_identity=dict(source_sha256=identity['source_sha256'],schema_sha256=identity['schema_sha256'],effective_rows_sha256=effective_rows,effective_labels_sha256=effective_labels,preprocessing_sha256=preprocessing,engine_sha256=_ENGINE_SHA256)
|
|
289
|
+
token=prepared.bind(xx,preparation_identity,'TRAIN_FULL' if phase=='REFIT_FULL' else 'OOF_TRAIN' if phase=='OOF' else 'TRAIN')
|
|
290
|
+
tick=time.perf_counter();model.fit(xx,yy,callback=lambda *_:res.check() or True,prepared_data=token)
|
|
291
|
+
assert len(model.trees)==actual.n_estimators*(k if task=='multiclass' else 1)
|
|
292
|
+
self._event('NATIVE_FIT_COMPLETED',ordinal=self.last_attempt_['native_fits'],phase=phase,seconds=time.perf_counter()-tick,component_key=component,actual_scalar_trees=len(model.trees));models.append(model)
|
|
293
|
+
fitted=Family(task,genome,params,enc,models,family);cache[key]=fitted;return fitted
|
|
294
|
+
base=AlgorithmGenome();tick=time.perf_counter();screen={f:acquire(f,train,ty,base,hp,'SCREEN',self.random_state) for f in controls['families']}
|
|
295
|
+
losses={f:risk(task,sy,m.probabilities(search,res)) for f,m in screen.items()};family=min(losses,key=lambda f:(losses[f],f));current=screen[family];current_risk=losses[family]
|
|
296
|
+
self._event('SCREEN_COMPLETED',losses=losses,parent=family,seconds=time.perf_counter()-tick)
|
|
297
|
+
self._research_initial_parent=current
|
|
298
|
+
def retain_anchor():
|
|
299
|
+
self._attempt_anchor=dict(model=current.packet(),classes=classes,SEARCH=current_risk,source_role='TRAIN',final_refit=False)
|
|
300
|
+
self.last_attempt_['last_verified_anchor']=dict(model_sha256=digest(current.packet()),SEARCH=current_risk,source_role='TRAIN',final_refit=False)
|
|
301
|
+
from .scalable_checkpoint import commit
|
|
302
|
+
commit(self,self._attempt_anchor['model'],classes,res,final=False)
|
|
303
|
+
retain_anchor()
|
|
304
|
+
seen=set();diagnoses={};self.diagnostic_profiles_=diagnoses
|
|
305
|
+
for generation in range(mutations):
|
|
306
|
+
parent=digest(current.packet())
|
|
307
|
+
if parent not in diagnoses:
|
|
308
|
+
tick=time.perf_counter();folds=oof_folds(ty,task,self.random_state);oof=[None]*len(ty)
|
|
309
|
+
for held in folds:
|
|
310
|
+
ids=compact_oof_train_indices(len(ty),held,res);fold=acquire(family,train.take(ids),array(ty.typecode,(ty[i] for i in ids)),current.genome,hp,'OOF',self.random_state)
|
|
311
|
+
pred=fold.probabilities(train.take(held),res)
|
|
312
|
+
self._event('OOF_FOLD',parent_sha256=parent,program_sha256=digest(current.genome.to_dict()),fit_rows=len(ids),held_rows=len(held),disjoint=True,encoder_sha256=digest(fold.encoder.state()),fit_indices_sha256=hashlib.sha256(array('Q',ids)).hexdigest(),held_indices_sha256=hashlib.sha256(array('Q',held)).hexdigest())
|
|
313
|
+
for i,p in zip(held,pred):oof[i]=p
|
|
314
|
+
point=point_loss(task,ty,oof);primary=statistics.mean(point)
|
|
315
|
+
groups=streaming_loss_regions(train,point,folds,res,primary)
|
|
316
|
+
diagnoses[parent]=dict(parent_sha256=parent,task=task,OOF_primary=primary,fold_rows=list(map(len,folds)),TRAIN_only=True,fold_encoder_local=True,features_examined=train.width,regions=groups,causal_status='HEURISTIC_UNCONFIRMED',seconds=time.perf_counter()-tick)
|
|
317
|
+
profile=diagnoses[parent]
|
|
318
|
+
self._event('DIAGNOSIS',**{key:value for key,value in profile.items() if key!='regions'},region_count=len(groups),profile_sha256=digest(profile));del oof,point,folds
|
|
319
|
+
diag=diagnoses[parent];order=mutation_order(diag['regions'],getattr(self,'_research_use_diagnosis',True));proposal=None
|
|
320
|
+
for gene in order:
|
|
321
|
+
if gene not in controls['genes']:continue
|
|
322
|
+
value=('OBJECTIVE_GAIN','BALANCE','MUL') if gene=='split_program' else 'depthwise'
|
|
323
|
+
if getattr(current.genome,gene)==value:continue
|
|
324
|
+
candidate=current.genome.mutate(**{gene:value});identity=digest(candidate.to_dict())
|
|
325
|
+
if (parent,identity) not in seen:proposal=candidate;seen.add((parent,identity));break
|
|
326
|
+
if proposal is None:break
|
|
327
|
+
tick=time.perf_counter();child=acquire(family,train,ty,proposal,hp,'DESCENDANT',self.random_state);child_risk=risk(task,sy,child.probabilities(search,res));accept=child_risk<current_risk-1e-4*max(1.,abs(current_risk))
|
|
328
|
+
self._event('MUTATION',generation=generation,parent_sha256=parent,child_sha256=digest(child.packet()),child_program=proposal.to_dict(),parent_SEARCH=current_risk,child_SEARCH=child_risk,decision='ACCEPT' if accept else 'ROLLBACK',seconds=time.perf_counter()-tick)
|
|
329
|
+
if accept:current=child;current_risk=child_risk;retain_anchor()
|
|
330
|
+
frozen=current.genome;self._event('PROGRAM_FROZEN',genome_sha256=digest(frozen.to_dict()))
|
|
331
|
+
self._research_structural_before_numeric=current
|
|
332
|
+
if self.use_optuna:
|
|
333
|
+
import optuna
|
|
334
|
+
maximum_estimators=plan['numeric_capacity']['maximum_estimators']
|
|
335
|
+
study=optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=self.random_state,n_startup_trials=2))
|
|
336
|
+
for _ in range(trials):
|
|
337
|
+
res.check();trial=study.ask();self.last_attempt_['Optuna_trials']+=1
|
|
338
|
+
p=replace(hp,l2=trial.suggest_float('l2',.1,10.,log=True),min_samples_leaf=trial.suggest_int('min_samples_leaf',2,8))
|
|
339
|
+
if maximum_estimators>hp.n_estimators:p=replace(p,n_estimators=trial.suggest_int('n_estimators',hp.n_estimators,maximum_estimators))
|
|
340
|
+
self._event('OPTUNA_ASK',number=trial.number,genome_sha256=digest(frozen.to_dict()))
|
|
341
|
+
child=acquire(family,train,ty,frozen,p,'NUMERIC',self.random_state);value=risk(task,sy,child.probabilities(search,res));study.tell(trial,value)
|
|
342
|
+
if value<current_risk:current=child;current_risk=value;retain_anchor()
|
|
343
|
+
self._event('OPTUNA_COMPLETE',number=trial.number,SEARCH=value,parameters=asdict(p))
|
|
344
|
+
self._research_selected_before_refit=current
|
|
345
|
+
if self.refit_full:current=acquire(family,raw,targets,frozen,current.params,'REFIT_FULL',self.random_state)
|
|
346
|
+
self.model_=current;self.classes_=classes;self.task_=task;self.last_attempt_['bin_cache']=prepared.report();self._event('BIN_PREPARATION_CACHE',**prepared.report());prepared.clear()
|
|
347
|
+
self._event('FINAL_MODEL',input_rows=len(raw),selection_TRAIN_rows=len(train),SEARCH_rows=len(search),final_fit_source_rows=len(raw) if self.refit_full else len(train),refit_full=self.refit_full,SEARCH_primary_before_refit=current_risk,selection_metric_is_independent=False,nominal_columns=len(current.encoder.transform(raw)._nominal_sizes),source_storage_bytes=raw.storage_bytes,all_rows_accounted=True)
|
|
348
|
+
self.n_features_in_=raw.width;self.best_family_=family;self.best_params_=asdict(current.params)
|
|
349
|
+
self.last_attempt_['receipt_verification']=verify_receipts(self.last_attempt_['events'])
|
|
350
|
+
self.last_attempt_['resource_usage']=dict(baseline_RSS_bytes=res.base_rss,sampled_peak_RSS_bytes=res.peak_rss,baseline_private_commit_bytes=res.base_commit,sampled_peak_private_commit_bytes=res.peak_commit)
|
|
351
|
+
self.last_attempt_['scope']='PRIVATE_EXPERIMENTAL_NOT_PUBLISHED_2_0_OR_GENERAL_UTILITY_QUALIFICATION';cache.clear();gc.collect()
|
|
352
|
+
def _blocks(self,X):
|
|
353
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
354
|
+
res=self._resources();source=Input(X);k=len(self.classes_) if self.task_!='regression' else 1
|
|
355
|
+
schema=self.model_.encoder
|
|
356
|
+
if schema.names is None:
|
|
357
|
+
if source.names is not None:raise ValueError('Positional TRAIN requires positional prediction')
|
|
358
|
+
ordered=schema.columns
|
|
359
|
+
else:
|
|
360
|
+
if source.names is None or set(source.names)!=set(schema.names):raise ValueError('Feature schema mismatch')
|
|
361
|
+
columns=dict(zip(schema.names,schema.columns));ordered=[columns[name] for name in source.names]
|
|
362
|
+
if len(ordered)!=source.width:raise ValueError('Feature count mismatch')
|
|
363
|
+
# Infer representations from the fitted schema,never refit vocabularies
|
|
364
|
+
# from query dtypes. Preserve typed numeric nominal IDs through Table.
|
|
365
|
+
source.fitted_kinds=tuple(column['kind'] for column in ordered)
|
|
366
|
+
if self.time_budget=='auto':res.configure_time(inference_seconds(source.n,source.width,sum(len(m.trees) for m in self.model_.models),self.model_.params.max_depth))
|
|
367
|
+
member_rows=len(self.model_.models)*(40*k+64 if self.task_=='multiclass' else 40)
|
|
368
|
+
batch=max(1,min(16384,res.bytes_limit//max(1,(source.width*32+member_rows+k*128+256)*4)))
|
|
369
|
+
for start in range(0,source.n,batch):
|
|
370
|
+
raw=Table.read(source,res,start,min(source.n,start+batch));yield self.model_.probabilities(raw,res)
|
|
371
|
+
def predict(self,X):
|
|
372
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
373
|
+
import numpy as np
|
|
374
|
+
from .prediction_output import prediction_array
|
|
375
|
+
n=Input(X).n;res=self._resources()
|
|
376
|
+
res.reserve(n*40+len(self.classes_)*64,'Returned prediction array and class dtype')
|
|
377
|
+
dtype=np.float64 if self.task_=='regression' else prediction_array(self.classes_).dtype
|
|
378
|
+
result=np.empty(n,dtype=dtype);offset=0
|
|
379
|
+
for values in self._blocks(X):
|
|
380
|
+
res.check();stop=offset+len(values)
|
|
381
|
+
if stop>n:raise ValueError('Unexpected prediction row count')
|
|
382
|
+
if self.task_=='regression':result[offset:stop]=values
|
|
383
|
+
elif self.task_=='binary':result[offset:stop]=[self.classes_[int(p>=.5)] for p in values]
|
|
384
|
+
else:result[offset:stop]=[self.classes_[max(range(len(p)),key=p.__getitem__)] for p in values]
|
|
385
|
+
offset=stop
|
|
386
|
+
if offset!=n:raise ValueError('Incomplete prediction rows')
|
|
387
|
+
if self.task_=='regression' and not np.isfinite(result).all():raise ValueError('Finite regression predictions required')
|
|
388
|
+
res.check()
|
|
389
|
+
return result
|
|
390
|
+
def predict_proba(self,X):
|
|
391
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
392
|
+
if self.task_=='regression':raise AttributeError('Regression has no class probabilities')
|
|
393
|
+
import numpy as np
|
|
394
|
+
from .prediction_output import probability_array
|
|
395
|
+
n=Input(X).n;k=len(self.classes_);res=self._resources();res.reserve(n*(k*40+64),'Returned probability array and bounded validation')
|
|
396
|
+
result=np.empty((n,k),dtype=np.float64);offset=0
|
|
397
|
+
for values in self._blocks(X):
|
|
398
|
+
res.check();stop=offset+len(values)
|
|
399
|
+
if stop>n:raise ValueError('Unexpected probability row count')
|
|
400
|
+
result[offset:stop]=[[1-p,p] for p in values] if self.task_=='binary' else values
|
|
401
|
+
offset=stop
|
|
402
|
+
if offset!=n:raise ValueError('Incomplete probability rows')
|
|
403
|
+
return probability_array(result,k,n_samples=n,check=res.check)
|
|
404
|
+
def available_variants(self):
|
|
405
|
+
from .scalable_variants import describe
|
|
406
|
+
return describe(self)
|
|
407
|
+
def _variant_view(self,variant):
|
|
408
|
+
from .scalable_variants import view
|
|
409
|
+
return view(self,variant)
|
|
410
|
+
def save(self,path,*,variant='all'):
|
|
411
|
+
from .scalable_variants import save_bundle
|
|
412
|
+
result=save_bundle(self,path,variant)
|
|
413
|
+
if result is not None:return result
|
|
414
|
+
return self._save_single(path)
|
|
415
|
+
def _save_single(self,path):
|
|
416
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
417
|
+
res=self._serialization_resources('Model snapshot')
|
|
418
|
+
chained=bool(self.report_.get('events')) and all('receipt_schema' in e for e in self.report_['events'])
|
|
419
|
+
if chained:verify_receipts(self.report_['events'])
|
|
420
|
+
packet=dict(schema='SCALABLE_PRIVATE_MODEL_2' if chained else 'SCALABLE_PRIVATE_MODEL_1',model=self.model_.packet(),classes=self.classes_,report=self.report_)
|
|
421
|
+
if chained:packet['receipt_sha256']=digest(self.report_);packet['evidence']=dict(diagnostic_profiles=getattr(self,'diagnostic_profiles_',{}))
|
|
422
|
+
if chained:
|
|
423
|
+
packet['schema']='SCALABLE_PRIVATE_MODEL_3'
|
|
424
|
+
packet['constructor']=self._constructor_state()
|
|
425
|
+
packet['constructor_sha256']=digest(packet['constructor'])
|
|
426
|
+
raw=json.dumps(packet,sort_keys=True,allow_nan=False)
|
|
427
|
+
res.reserve(len(raw.encode())*4,'Serialized model');atomic_text(Path(path),raw+'\n');return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
428
|
+
def _serialization_resources(self,description,*,families=None):
|
|
429
|
+
res=self._resources()
|
|
430
|
+
families=list({id(f):f for f in (families or [self.model_])}.values())
|
|
431
|
+
members=[m for family in families for m in family.models]
|
|
432
|
+
nodes=sum(len(tree.nodes) for model in members for tree in model.trees)
|
|
433
|
+
cuts=sum(len(c) for model in members for c in model.binner.thresholds)
|
|
434
|
+
words=sum(len(v) for family in families for c in family.encoder.columns for v in c.get('vocabulary',()))
|
|
435
|
+
names=sum(len(v) for family in families for v in family.encoder.names or ())
|
|
436
|
+
labels=sum(len(v) for v in self.classes_ if isinstance(v,str))
|
|
437
|
+
estimate=1024+nodes*4096+cuts*64+(words+names+labels)*32+len(self.report_.get('events',()))*4096
|
|
438
|
+
if self.time_budget=='auto':res.configure_time(io_seconds(estimate))
|
|
439
|
+
res.reserve(estimate,description+' estimated serialization workspace');return res
|
|
440
|
+
def export_python(self,path,*,variant='selected',output_format='python'):
|
|
441
|
+
if output_format not in ('python','numpy'):raise ValueError('output_format must be python or numpy')
|
|
442
|
+
if variant!='selected':return self._variant_view(variant).export_python(path,output_format=output_format)
|
|
443
|
+
import inspect
|
|
444
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
445
|
+
res=self._serialization_resources('Standalone snapshot')
|
|
446
|
+
packet=dict(model=self.model_.packet(),classes=self.classes_)
|
|
447
|
+
guard_source=(_PACKAGE/'source_markers.py').read_text('utf8')
|
|
448
|
+
source='# Private frozen Python inference; no training or external libraries.\nMODEL='+repr(packet)+'\n'+guard_source+'\n'+inspect.getsource(memory_snapshot)+'\n'+inspect.getsource(memory_allowance)+'\n'+inspect.getsource(available_memory)+'\n'+inspect.getsource(_standalone)+'\ndef predict(X):return _standalone(X,MODEL,False)\ndef predict_proba(X):return _standalone(X,MODEL,True)\n'
|
|
449
|
+
if output_format=='numpy':
|
|
450
|
+
from .prediction_output import numpy_export_source
|
|
451
|
+
source=numpy_export_source(source,self.classes_,self.task_=='regression')
|
|
452
|
+
res.reserve(len(source.encode())*4,'Standalone source');atomic_text(Path(path),source);return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
453
|
+
@classmethod
|
|
454
|
+
def load(cls,path,*,sha256=None,memory_budget='auto',workdir=None):
|
|
455
|
+
from .api_compat import validate_workdir,wrap_historical
|
|
456
|
+
workdir=validate_workdir(workdir)
|
|
457
|
+
p=Path(path);size=regular_file_size(p);res=managed_resources(memory_budget,io_seconds(size));res.reserve(size*8,'Validated JSON model')
|
|
458
|
+
limit=max(1,res.bytes_limit//8)
|
|
459
|
+
raw=read_regular_bytes(p,limit)
|
|
460
|
+
res.reserve(len(raw)*8,'Immutable JSON snapshot')
|
|
461
|
+
actual_sha256=hashlib.sha256(raw).hexdigest()
|
|
462
|
+
if sha256 is not None and actual_sha256!=sha256:raise ValueError('Changed saved model')
|
|
463
|
+
packet=decode_json_bytes(raw,LoadLimits(max_bytes=limit),reserve=res.reserve)
|
|
464
|
+
if type(packet) is not dict:raise ValueError('Typed model envelope must be an object')
|
|
465
|
+
from .legacy_artifacts import PUBLIC_2_0_SCHEMA,restore_public_2_0
|
|
466
|
+
if packet.get('schema')==PUBLIC_2_0_SCHEMA:
|
|
467
|
+
if set(packet)!={'constructor','origins','schema','selected_variant','variants'} or type(packet['constructor']) is not dict or type(packet['origins']) is not dict or type(packet['selected_variant']) is not str or type(packet['variants']) is not dict or not packet['variants']:raise ValueError('Exact historical model envelope required')
|
|
468
|
+
del packet,raw
|
|
469
|
+
restored=restore_public_2_0(p,actual_sha256,cls.task,workdir=workdir);res.check()
|
|
470
|
+
return wrap_historical(restored,cls,memory_budget)
|
|
471
|
+
if packet.get('schema')=='SCALABLE_PRIVATE_VARIANTS_1':
|
|
472
|
+
from .scalable_variants import restore_bundle
|
|
473
|
+
return restore_bundle(cls,packet,res,limit,actual_sha256,sha256 is not None,memory_budget)
|
|
474
|
+
return cls._restore_packet(packet,res,limit,actual_sha256,sha256 is not None,memory_budget)
|
|
475
|
+
@classmethod
|
|
476
|
+
def _restore_packet(cls,packet,res,limit,actual_sha256,expected_checksum_supplied,memory_budget):
|
|
477
|
+
required={'schema','model','classes','report'}
|
|
478
|
+
if packet.get('schema') in ('SCALABLE_PRIVATE_MODEL_2','SCALABLE_PRIVATE_MODEL_3'):required.update(('receipt_sha256','evidence'))
|
|
479
|
+
if packet.get('schema')=='SCALABLE_PRIVATE_MODEL_3':required.update(('constructor','constructor_sha256'))
|
|
480
|
+
if set(packet)!=required or packet['schema'] not in ('SCALABLE_PRIVATE_MODEL_1','SCALABLE_PRIVATE_MODEL_2','SCALABLE_PRIVATE_MODEL_3') or type(packet['report']) is not dict:raise ValueError('Private model schema')
|
|
481
|
+
if packet['schema'] in ('SCALABLE_PRIVATE_MODEL_2','SCALABLE_PRIVATE_MODEL_3'):
|
|
482
|
+
if packet['receipt_sha256']!=digest(packet['report']):raise ValueError('Changed report identity')
|
|
483
|
+
verification=verify_receipts(packet['report'].get('events'))
|
|
484
|
+
report=packet['report'];events=report['events'];starts=[e for e in events if e['event']=='NATIVE_FIT_STARTED'];ends=[e for e in events if e['event']=='NATIVE_FIT_COMPLETED']
|
|
485
|
+
if any(type(e['ordinal']) is not int for e in starts) or [e['ordinal'] for e in starts]!=list(range(1,len(starts)+1)):raise ValueError('Invalid native component order')
|
|
486
|
+
if report.get('status')=='COMPLETED' and (events[-1]['event']!='FINAL_MODEL' or not starts):raise ValueError('Completed training requires fitted-component and finalization receipts')
|
|
487
|
+
if len(starts)!=report.get('native_fits') or sum(e['phase']=='OOF' for e in starts)!=report.get('OOF_fits') or sum(e['reserved_scalar_trees'] for e in starts)!=report.get('scalar_trees') or sum(e['event']=='OPTUNA_ASK' for e in events)!=report.get('Optuna_trials'):raise ValueError('Receipt/counter mismatch')
|
|
488
|
+
if report.get('status')=='COMPLETED' and (len(starts)!=len(ends) or [e['component_key'] for e in starts]!=[e['component_key'] for e in ends] or sum(e['actual_scalar_trees'] for e in ends)!=report.get('scalar_trees')):raise ValueError('Incomplete fitted-component chain')
|
|
489
|
+
evidence=packet['evidence']
|
|
490
|
+
if type(evidence) is not dict or set(evidence)!={'diagnostic_profiles'} or type(evidence['diagnostic_profiles']) is not dict:raise ValueError('Separate diagnostic evidence schema')
|
|
491
|
+
profiles=evidence['diagnostic_profiles'];receipts=[e for e in events if e['event']=='DIAGNOSIS']
|
|
492
|
+
if set(profiles)!={e['parent_sha256'] for e in receipts} or any(digest(profiles[e['parent_sha256']])!=e['profile_sha256'] for e in receipts):raise ValueError('Changed diagnostic artifact identity')
|
|
493
|
+
else:verification=dict(status='LEGACY_UNCHAINED_EXPERIMENTAL_REPORT',authority='Caller-supplied file custody only; not a verified operation chain')
|
|
494
|
+
v=packet['model']
|
|
495
|
+
if type(v) is not dict or set(v)!={'task','family','genome','params','encoder','members'} or v['task'] not in ('binary','multiclass','regression') or v['family'] not in ('boost','bag'):raise ValueError('Exact native model fields')
|
|
496
|
+
if cls.task=='classification' and v['task']=='regression' or cls.task=='regression' and v['task']!='regression':raise ValueError('Saved task is incompatible with requested facade')
|
|
497
|
+
facade=MorphTreeRegressor if v['task']=='regression' else MorphTreeClassifier
|
|
498
|
+
if packet['schema']=='SCALABLE_PRIVATE_MODEL_3':
|
|
499
|
+
state=packet['constructor']
|
|
500
|
+
if digest(state)!=packet['constructor_sha256']:raise ValueError('Changed refit configuration identity')
|
|
501
|
+
if type(state) is not dict or set(state)!={'schema','parameters','checkpoint_location_reset','scope'} or state['schema'] not in ('SCALABLE_REFIT_CONFIGURATION_1','SCALABLE_REFIT_CONFIGURATION_2') or type(state['parameters']) is not dict or type(state['checkpoint_location_reset']) is not bool or state['scope']!='FUTURE_REFIT_SETTINGS_AT_SNAPSHOT':raise ValueError('Exact typed refit configuration required')
|
|
502
|
+
params=state['parameters']
|
|
503
|
+
keys=_PARAMETERS_V1 if state['schema']=='SCALABLE_REFIT_CONFIGURATION_1' else _PARAMETERS_V2
|
|
504
|
+
if set(params)!=set(keys) or params['checkpoint_dir'] not in ('auto',None):raise ValueError('Safe refit parameters required;artifact output paths cannot be restored')
|
|
505
|
+
params=dict(params)
|
|
506
|
+
if state['schema']=='SCALABLE_REFIT_CONFIGURATION_1':
|
|
507
|
+
# Exact old contract; new constructor fields do not invalidate v1.
|
|
508
|
+
if any(type(params[k]) is not int or not 0<=params[k]<=2 for k in ('n_trials','max_mutations')):raise ValueError('Invalid legacy refit configuration limits')
|
|
509
|
+
params.update(n_jobs=1,mode='adaptive',operators=None)
|
|
510
|
+
elif params['operators'] is not None:
|
|
511
|
+
if type(params['operators']) is not list:raise ValueError('Serialized operator list required')
|
|
512
|
+
params['operators']=tuple(params['operators'])
|
|
513
|
+
if memory_budget!='auto':params['memory_budget']=memory_budget
|
|
514
|
+
obj=facade(**params)
|
|
515
|
+
recovery=dict(available=True,scope=state['scope'],checkpoint_location_reset=state['checkpoint_location_reset'],original_training_settings_inferred=False)
|
|
516
|
+
else:
|
|
517
|
+
obj=facade(memory_budget=memory_budget)
|
|
518
|
+
recovery=dict(available=False,scope='LEGACY_CONFIG_NOT_RECORDED_DEFAULTS_FOR_FUTURE_REFIT',original_training_settings_inferred=False)
|
|
519
|
+
from .native_lab_backend import bind_backend
|
|
520
|
+
enc=Encoder.from_state(v['encoder'],res);hp=HyperParams(**v['params']);genome=AlgorithmGenome(**v['genome']);nominal={i:len(c['vocabulary']) for i,c in enumerate(enc.columns) if c['kind']=='categorical'};backend=bind_backend()(nominal);models=[]
|
|
521
|
+
classes=packet['classes']
|
|
522
|
+
if type(classes) is not list:raise ValueError('Typed class-label list required')
|
|
523
|
+
if v['task']=='regression':
|
|
524
|
+
if classes:raise ValueError('Regression class labels must be empty')
|
|
525
|
+
else:
|
|
526
|
+
normalized=[label_scalar(c) for c in classes]
|
|
527
|
+
if any(c is None for c in normalized) or not (all(type(c) is str for c in normalized) or all(type(c) in (int,float) for c in normalized)):raise ValueError('Consistent finite scalar class labels required')
|
|
528
|
+
if normalized!=sorted(set(normalized)) or len(classes)<2 or v['task']=='binary' and len(classes)!=2:raise ValueError('Distinct canonical task-compatible class labels required')
|
|
529
|
+
if type(v['members']) is not list or len(v['members'])!=(1 if v['family']=='boost' else hp.n_estimators):raise ValueError('Exact bounded family members')
|
|
530
|
+
for raw in v['members']:
|
|
531
|
+
res.check()
|
|
532
|
+
actual=hp if v['family']=='boost' else replace(hp,n_estimators=1,learning_rate=1.)
|
|
533
|
+
limits=LoadLimits(max_features=len(enc.columns),max_trees=actual.n_estimators*(len(classes) if v['task']=='multiclass' else 1),max_nodes=limit,max_thresholds=limit)
|
|
534
|
+
model=decode_multiclass_booster(raw,actual,backend,len(classes),'softmax',limits,genome=genome) if v['task']=='multiclass' else decode_booster(raw,genome,actual,backend,limits,objective='squared_error' if v['task']=='regression' else 'binary_logloss')
|
|
535
|
+
if model.n_features_!=len(enc.columns):raise ValueError('Model/encoder feature mismatch')
|
|
536
|
+
models.append(model)
|
|
537
|
+
res.check()
|
|
538
|
+
obj.model_=Family(v['task'],genome,hp,enc,models,v['family']);obj.classes_=classes;obj.task_=v['task'];obj.report_=copy.deepcopy(packet['report']);obj.report_['receipt_verification']=verification
|
|
539
|
+
obj.report_['constructor_recovery']=recovery
|
|
540
|
+
obj.report_['file_custody']=dict(sha256=actual_sha256,expected_checksum_supplied=expected_checksum_supplied,scope='A local computed hash alone does not authenticate an author or scientific claim')
|
|
541
|
+
obj.diagnostic_profiles_=copy.deepcopy(packet.get('evidence',{}).get('diagnostic_profiles',{}))
|
|
542
|
+
anchor=obj.report_.get('last_verified_anchor')
|
|
543
|
+
if type(anchor) is dict and 'model' in anchor:obj.report_['last_verified_anchor']={k:value for k,value in anchor.items() if k not in ('model','classes')}|dict(model_sha256=digest(anchor['model']),legacy_embedded_model_omitted_from_report_view=True)
|
|
544
|
+
obj.n_features_in_=len(enc.columns);obj.best_family_=v['family'];obj.best_params_=asdict(hp);return obj
|
|
545
|
+
@classmethod
|
|
546
|
+
def load_checkpoint(cls,directory,*,memory_budget='auto',stage='current'):
|
|
547
|
+
"""Recover current or selected TRAIN state;never resume training or auto-select from EXTERNAL."""
|
|
548
|
+
from .scalable_checkpoint import read_pointer
|
|
549
|
+
target,record=read_pointer(directory,managed_resources(memory_budget,150),stage=stage)
|
|
550
|
+
obj=cls.load(target,sha256=record['sha256'],memory_budget=memory_budget);events=obj.report_.get('events',[])
|
|
551
|
+
if not events or events[0]['call_id']!=record['call_id']:raise ValueError('Checkpoint call identity mismatch')
|
|
552
|
+
if record['final']:
|
|
553
|
+
final=events[-1]
|
|
554
|
+
if obj.report_.get('status')!='COMPLETED' or final['event']!='FINAL_MODEL' or record['source_role']!=('TRAIN_FULL' if final['refit_full'] else 'TRAIN'):raise ValueError('Checkpoint finalization/role mismatch')
|
|
555
|
+
elif obj.report_.get('status')!='PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT' or digest(obj.model_.packet())!=obj.report_.get('last_verified_anchor',{}).get('model_sha256'):raise ValueError('Checkpoint TRAIN anchor mismatch')
|
|
556
|
+
obj.report_['checkpoint_recovery']=dict(**record,training_resumed=False)
|
|
557
|
+
return obj
|
|
558
|
+
def get_params(self,deep=True):
|
|
559
|
+
params={k:getattr(self,k) for k in _PARAMETERS_V2}
|
|
560
|
+
return copy.deepcopy(params) if deep else params
|
|
561
|
+
def _constructor_state(self):
|
|
562
|
+
params=self.get_params()
|
|
563
|
+
if '_requested_random_state' in self.__dict__:params['random_state']=self._requested_random_state
|
|
564
|
+
reset=params['checkpoint_dir'] not in ('auto',None)
|
|
565
|
+
if reset:params['checkpoint_dir']='auto'
|
|
566
|
+
# Validate executable future settings; never deserialize artifact-chosen
|
|
567
|
+
# output destinations or present inferred defaults as old training facts.
|
|
568
|
+
type(self)(**params)
|
|
569
|
+
return dict(schema='SCALABLE_REFIT_CONFIGURATION_2',parameters=params,
|
|
570
|
+
checkpoint_location_reset=reset,scope='FUTURE_REFIT_SETTINGS_AT_SNAPSHOT')
|
|
571
|
+
def set_params(self,**values):
|
|
572
|
+
if not values:return self
|
|
573
|
+
params=self.get_params(deep=False)
|
|
574
|
+
if set(values)-set(params):raise ValueError('Unknown parameter')
|
|
575
|
+
replacement=type(self)(**(params|values));self.__dict__.clear();self.__dict__.update(replacement.__dict__);return self
|
|
576
|
+
def get_algorithm_program(self,*,variant='selected'):
|
|
577
|
+
if variant!='selected':return self._variant_view(variant).get_algorithm_program()
|
|
578
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
579
|
+
frozen=any(e['event']=='PROGRAM_FROZEN' and e['genome_sha256']==digest(self.model_.genome.to_dict()) for e in self.report_.get('events',[]))
|
|
580
|
+
return dict(schema='SCALABLE_EVOLVED_PROGRAM_1',task=self.task_,family=self.model_.family,genome=self.model_.genome.to_dict(),frozen_before_numeric=frozen,nominal_split='ONE_CATEGORY_VS_REST',training_program_export=True,training_export_scope='FROZEN_FAMILY_NOT_END_TO_END_SEARCH')
|
|
581
|
+
def get_recipe(self,*,variant='selected'):
|
|
582
|
+
if variant!='selected':return self._variant_view(variant).get_recipe()
|
|
583
|
+
return dict(program=self.get_algorithm_program(),parameters=asdict(self.model_.params),preprocessing=copy.deepcopy(self.model_.encoder.state()),classes=list(self.classes_),role='FITTED_MODEL_RECIPE_NOT_AN_EXPORTED_TRAINING_PROCEDURE')
|
|
584
|
+
def get_mutation_history(self):
|
|
585
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
586
|
+
result=[];depth={}
|
|
587
|
+
for e in self.report_.get('events',[]):
|
|
588
|
+
if e['event']!='MUTATION':continue
|
|
589
|
+
parent=e['parent_sha256'];depth.setdefault(parent,0);value=copy.deepcopy(e);value['parent_genealogical_depth']=depth[parent]
|
|
590
|
+
if e['decision']=='ACCEPT':depth[e['child_sha256']]=depth[parent]+1
|
|
591
|
+
value['retained_sha256']=e['child_sha256'] if e['decision']=='ACCEPT' else parent;value['retained_genealogical_depth']=depth[value['retained_sha256']];result.append(value)
|
|
592
|
+
return result
|
|
593
|
+
def report(self):
|
|
594
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
595
|
+
from .scalable_variants import active_metadata
|
|
596
|
+
result=copy.deepcopy(self.report_);result['variant_selection']=active_metadata(self)
|
|
597
|
+
result['trace_scope']='Original fit history and costs; active variant source role is stated separately'
|
|
598
|
+
return result
|
|
599
|
+
def get_search_report(self):return self.report()
|
|
600
|
+
def cost_report(self):
|
|
601
|
+
report=self.report()
|
|
602
|
+
native={}
|
|
603
|
+
for event in report.get('events',()):
|
|
604
|
+
if event['event']=='NATIVE_FIT_COMPLETED':
|
|
605
|
+
phase=event['phase'];native[phase]=native.get(phase,0.)+event['seconds']
|
|
606
|
+
return dict(scope='RECORDED_FIT_COST_NOT_NEW_MEASUREMENT',
|
|
607
|
+
native_fits=report.get('native_fits'),OOF_fits=report.get('OOF_fits'),
|
|
608
|
+
Optuna_trials=report.get('Optuna_trials'),
|
|
609
|
+
scalar_tree_reservations=report.get('scalar_trees'),
|
|
610
|
+
completed_scalar_trees=sum(e['actual_scalar_trees'] for e in report.get('events',()) if e['event']=='NATIVE_FIT_COMPLETED'),
|
|
611
|
+
total_fit_seconds=report.get('seconds'),native_fit_seconds_by_phase=native,
|
|
612
|
+
observed_resources=copy.deepcopy(report.get('resource_usage',{})),
|
|
613
|
+
native_time_scope='Native fit intervals only;OOF included. Excludes preprocessing,diagnosis bookkeeping,Optuna overhead and persistence;not a complete disjoint wall-time partition.',
|
|
614
|
+
new_training_from_this_query=0)
|
|
615
|
+
def save_recipe(self,path,*,variant='selected'):
|
|
616
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
617
|
+
if variant!='selected':return self._variant_view(variant).save_recipe(path)
|
|
618
|
+
res=self._serialization_resources('Recipe snapshot')
|
|
619
|
+
raw=json.dumps(self.get_recipe(),sort_keys=True,allow_nan=False)+'\n'
|
|
620
|
+
res.reserve(len(raw.encode('utf8'))*4,'Serialized recipe')
|
|
621
|
+
atomic_text(Path(path),raw)
|
|
622
|
+
return hashlib.sha256(raw.encode('utf8')).hexdigest()
|
|
623
|
+
def export_standalone(self,path,*,variant='selected',output_format='python'):
|
|
624
|
+
"""Export the fitted dependency-free predictor; this does not export training."""
|
|
625
|
+
return self.export_python(path,variant=variant,output_format=output_format)
|
|
626
|
+
def export_algorithm(self,path):
|
|
627
|
+
"""Export the frozen family trainer, distinct from fitted inference.
|
|
628
|
+
|
|
629
|
+
train_encoded requires explicit TRAIN identity. This fragment does not
|
|
630
|
+
rerun family screening, OOF, evolution or numerical selection.
|
|
631
|
+
"""
|
|
632
|
+
from .training_fragment import export_algorithm
|
|
633
|
+
return export_algorithm(self,path)
|
|
634
|
+
@property
|
|
635
|
+
def stop_reason_(self):
|
|
636
|
+
if not hasattr(self,'model_'):raise AttributeError('stop_reason_ requires a fitted model')
|
|
637
|
+
report=self.report_
|
|
638
|
+
if 'stop_reason' in report:return report['stop_reason']
|
|
639
|
+
if report.get('status')=='COMPLETED':return 'PLANNED_SEARCH_COMPLETE'
|
|
640
|
+
if report.get('status')=='PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT':return 'PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT'
|
|
641
|
+
return 'LEGACY_STOP_REASON_NOT_RECORDED'
|
|
642
|
+
@property
|
|
643
|
+
def feature_names_in_(self):
|
|
644
|
+
if not hasattr(self,'model_'):raise AttributeError('feature_names_in_ requires a fitted model')
|
|
645
|
+
names=self.model_.encoder.names
|
|
646
|
+
return list(names) if names is not None else None
|
|
647
|
+
def _score(self,X,y):
|
|
648
|
+
self._ordinary_context_guard()
|
|
649
|
+
if controlled_source(y):raise ValueError('Controlled sources cannot use ordinary scoring')
|
|
650
|
+
if isinstance(y,(str,bytes,dict)) or getattr(y,'ndim',1)!=1 or not hasattr(y,'__len__'):
|
|
651
|
+
raise ValueError('score requires nonempty aligned scalar targets')
|
|
652
|
+
prediction=self.predict(X);n=len(prediction)
|
|
653
|
+
if not n or len(y)!=n:raise ValueError('score requires nonempty aligned scalar targets')
|
|
654
|
+
res=self._resources()
|
|
655
|
+
if self.time_budget=='auto':res.configure_time(inference_seconds(n,1,0,0))
|
|
656
|
+
regression=self.task=='regression'
|
|
657
|
+
if regression:res.reserve(n*8,'Compact scoring targets');targets=array('d')
|
|
658
|
+
correct=0;count=0
|
|
659
|
+
for raw in y:
|
|
660
|
+
if count%4096==0:res.check()
|
|
661
|
+
if count>=n:raise ValueError('Score target iterator/length mismatch')
|
|
662
|
+
value=scalar(raw) if regression else label_scalar(raw)
|
|
663
|
+
if value is None:raise ValueError('Missing score targets are unsupported')
|
|
664
|
+
if regression:
|
|
665
|
+
if type(value) not in (int,float):raise ValueError('Regression score requires finite numeric targets')
|
|
666
|
+
targets.append(value)
|
|
667
|
+
else:
|
|
668
|
+
predicted=prediction[count]
|
|
669
|
+
correct+=value==(predicted.item() if hasattr(predicted,'item') else predicted)
|
|
670
|
+
count+=1
|
|
671
|
+
if count!=n:raise ValueError('Score target iterator/length mismatch')
|
|
672
|
+
if not regression:return correct/n
|
|
673
|
+
def residuals():
|
|
674
|
+
for index,(target,predicted) in enumerate(zip(targets,prediction)):
|
|
675
|
+
if index%4096==0:res.check()
|
|
676
|
+
value=target-predicted.item()
|
|
677
|
+
if not math.isfinite(value):raise ArithmeticError('Regression residual outside floating-point range')
|
|
678
|
+
yield value
|
|
679
|
+
error_scale=max(map(abs,residuals()),default=0.)
|
|
680
|
+
if error_scale==0:return 1.
|
|
681
|
+
if all(value==targets[0] for value in targets):return 0.
|
|
682
|
+
mean=math.fsum(value/n for value in targets)
|
|
683
|
+
def centered():
|
|
684
|
+
for index,value in enumerate(targets):
|
|
685
|
+
if index%4096==0:res.check()
|
|
686
|
+
difference=value-mean
|
|
687
|
+
if not math.isfinite(difference):raise ArithmeticError('Regression spread outside floating-point range')
|
|
688
|
+
yield difference
|
|
689
|
+
spread_scale=max(map(abs,centered()),default=0.)
|
|
690
|
+
if spread_scale==0:return 0.
|
|
691
|
+
error=error_scale*math.sqrt(math.fsum((value/error_scale)**2 for value in residuals())/n)
|
|
692
|
+
spread=spread_scale*math.sqrt(math.fsum((value/spread_scale)**2 for value in centered())/n)
|
|
693
|
+
ratio=error/spread;result=1-ratio*ratio
|
|
694
|
+
if not math.isfinite(result):raise ArithmeticError('R2 outside floating-point range')
|
|
695
|
+
return result
|
|
696
|
+
def get_diagnostics(self):
|
|
697
|
+
if not hasattr(self,'model_'):raise RuntimeError('Not fitted')
|
|
698
|
+
return copy.deepcopy(getattr(self,'diagnostic_profiles_',{}))
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class MorphTreeClassifier(_Scalable):
|
|
702
|
+
task='classification'
|
|
703
|
+
_estimator_type='classifier'
|
|
704
|
+
def score(self,X,y):return self._score(X,y)
|
|
705
|
+
class MorphTreeRegressor(_Scalable):
|
|
706
|
+
task='regression'
|
|
707
|
+
_estimator_type='regressor'
|
|
708
|
+
def score(self,X,y):return self._score(X,y)
|
|
709
|
+
@property
|
|
710
|
+
def predict_proba(self):raise AttributeError('Regression has no class probabilities')
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _standalone(X,packet,probabilities):
|
|
714
|
+
import bisect,json,math,sys
|
|
715
|
+
from numbers import Real
|
|
716
|
+
if controlled_source(X):raise ValueError('Controlled sources cannot use ordinary standalone inference')
|
|
717
|
+
model=packet['model'];schema=model['encoder'];classes=packet['classes'];task=model['task'];columns=schema['columns'];named=hasattr(X,'columns') and hasattr(X,'itertuples')
|
|
718
|
+
names=list(X.columns) if named else None
|
|
719
|
+
if schema['names'] is not None:
|
|
720
|
+
if names is None or len(names)!=len(columns) or set(names)!=set(schema['names']):raise ValueError('Feature schema mismatch')
|
|
721
|
+
positions={name:i for i,name in enumerate(names)};order=[positions[n] for n in schema['names']]
|
|
722
|
+
else:
|
|
723
|
+
if names is not None:raise ValueError('Positional TRAIN schema')
|
|
724
|
+
order=list(range(len(columns)))
|
|
725
|
+
if not len(X):raise ValueError('Nonempty inference input')
|
|
726
|
+
allowance=memory_allowance('auto',memory_snapshot(available_memory())['effective_available_bytes'])
|
|
727
|
+
if len(X)*(len(classes)*40+64 if probabilities else 40)>allowance:raise MemoryError('Standalone returned-result estimate exceeds currently available memory allowance')
|
|
728
|
+
if probabilities and task=='regression':raise AttributeError('Regression has no probabilities')
|
|
729
|
+
rows=X.itertuples(index=False,name=None) if named else iter(X)
|
|
730
|
+
def category_key(v):
|
|
731
|
+
if not isinstance(v,str):
|
|
732
|
+
if type(v) is int:v=v if abs(v)>2**53 else float(v)
|
|
733
|
+
else:
|
|
734
|
+
v=float(v)
|
|
735
|
+
if abs(v)>2**53 and v.is_integer():v=int(v)
|
|
736
|
+
if v==0:v=0.0
|
|
737
|
+
return json.dumps(['s' if isinstance(v,str) else 'n',v],ensure_ascii=False,allow_nan=False,separators=(',',':'))
|
|
738
|
+
lookups={j:{category_key(json.loads(v)[1]):i+1 for i,v in enumerate(c['vocabulary'])} for j,c in enumerate(columns) if c['kind']=='categorical'}
|
|
739
|
+
def sigmoid(v):
|
|
740
|
+
if v>=0:return 1/(1+math.exp(-min(v,745.)))
|
|
741
|
+
z=math.exp(max(v,-745.));return z/(1+z)
|
|
742
|
+
out=[]
|
|
743
|
+
for row in rows:
|
|
744
|
+
if len(row)!=len(columns):raise ValueError('Feature count mismatch')
|
|
745
|
+
values=[]
|
|
746
|
+
for j,c in enumerate(columns):
|
|
747
|
+
v=row[order[j]]
|
|
748
|
+
if isinstance(v,str) and len(v)*32+512>allowance:raise MemoryError('Standalone scalar/key workspace exceeds available allowance')
|
|
749
|
+
if type(v).__module__.split('.')[0]=='numpy' and hasattr(v,'item'):v=v.item()
|
|
750
|
+
missing=v is None or (sys.modules.get('pandas') is not None and v is getattr(sys.modules['pandas'],'NA',None)) or isinstance(v,Real) and math.isnan(float(v))
|
|
751
|
+
if not missing and not(isinstance(v,str) or isinstance(v,Real) and math.isfinite(float(v))):raise ValueError('Invalid scalar')
|
|
752
|
+
if c['kind']=='numeric':
|
|
753
|
+
if isinstance(v,str):raise ValueError('String in numeric feature')
|
|
754
|
+
values.append(math.nan if missing else float(v))
|
|
755
|
+
else:
|
|
756
|
+
k=len(c['vocabulary']);key=None if missing else category_key(v)
|
|
757
|
+
values.append(k+1 if missing else lookups[j].get(key,k+2))
|
|
758
|
+
predictions=[]
|
|
759
|
+
for member in model['members']:
|
|
760
|
+
bins=[int(v) if j in lookups else 0 if math.isnan(v) else bisect.bisect_right(member['binner']['thresholds'][j],v)+1 for j,v in enumerate(values)]
|
|
761
|
+
raw=list(member['base_score']) if task=='multiclass' else member['base_score']
|
|
762
|
+
for index,(tree,step) in enumerate(zip(member['trees'],member['steps'])):
|
|
763
|
+
nodes=tree['nodes'];node=nodes[0]
|
|
764
|
+
for _ in range(len(nodes)+1):
|
|
765
|
+
if node['is_leaf']:break
|
|
766
|
+
j=node['feature'];b=bins[j];left=(b==node['threshold_bin']) if j in lookups else node['missing_left'] if b==0 else b<=node['threshold_bin'];node=nodes[node['left'] if left else node['right']]
|
|
767
|
+
else:raise ValueError('Cyclic model')
|
|
768
|
+
if task=='multiclass':
|
|
769
|
+
raw[index%len(classes)]+=step*node['value']
|
|
770
|
+
if index%len(classes)==len(classes)-1:
|
|
771
|
+
mean=math.fsum(raw)/len(raw);raw=[v-mean for v in raw]
|
|
772
|
+
else:raw+=step*node['value']
|
|
773
|
+
if task=='multiclass':
|
|
774
|
+
weights=[math.exp(v-max(raw)) for v in raw];total=math.fsum(weights);predictions.append([v/total for v in weights])
|
|
775
|
+
else:predictions.append(raw if task=='regression' else sigmoid(raw))
|
|
776
|
+
if task=='multiclass':p=[math.fsum(v[j] for v in predictions)/len(predictions) for j in range(len(classes))]
|
|
777
|
+
else:p=math.fsum(predictions)/len(predictions)
|
|
778
|
+
if probabilities:out.append([1-p,p] if task=='binary' else p)
|
|
779
|
+
elif task=='regression':out.append(p)
|
|
780
|
+
else:out.append(classes[int(p>=.5)] if task=='binary' else classes[max(range(len(p)),key=p.__getitem__)])
|
|
781
|
+
return out
|