mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-21 22:22:33 -05:00
406 lines
24 KiB
Python
406 lines
24 KiB
Python
"""Bounded amplitude HAR experiment, separate from RuView's eight-feature encoder.
|
|
|
|
Selection uses validation only. The test partition is evaluated once after the
|
|
architecture and checkpoint are frozen. No production promotion or upload.
|
|
"""
|
|
import argparse
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
import platform
|
|
from pathlib import Path
|
|
import time
|
|
|
|
import numpy as np
|
|
import torch
|
|
from torch import nn
|
|
from safetensors.torch import load_file, save_file
|
|
|
|
|
|
class AmplitudeClassifier(nn.Module):
|
|
def __init__(self, channels, classes, variant='temporal16', fused=False):
|
|
super().__init__()
|
|
if variant == 'statistics':
|
|
self.net = nn.Sequential(nn.Linear(channels * 3, 32), nn.ReLU(), nn.Linear(32, classes))
|
|
elif variant in ('temporal16', 'temporal32'):
|
|
width = int(variant.removeprefix('temporal'))
|
|
self.net = nn.Sequential(
|
|
nn.Conv1d(channels, width, 1, bias=fused),
|
|
nn.Identity() if fused else nn.BatchNorm1d(width), nn.ReLU(),
|
|
nn.Conv1d(width, width, 7, padding=3, groups=width, bias=fused),
|
|
nn.Identity() if fused else nn.BatchNorm1d(width), nn.ReLU(),
|
|
nn.Conv1d(width, width, 1), nn.ReLU(), nn.AdaptiveAvgPool1d(8 if width == 32 else 1),
|
|
nn.Flatten(), nn.Linear(width * (8 if width == 32 else 1), classes))
|
|
else:
|
|
raise ValueError('Unknown architecture')
|
|
self.variant = variant
|
|
|
|
def forward(self, x):
|
|
if self.variant == 'statistics':
|
|
x = torch.cat((x.mean(-1), x.std(-1, unbiased=False), (x[..., 1:] - x[..., :-1]).abs().mean(-1)), 1)
|
|
return self.net(x)
|
|
|
|
|
|
def scores(labels, predictions, classes):
|
|
matrix = np.zeros((classes, classes), dtype=np.int64)
|
|
np.add.at(matrix, (labels, predictions), 1)
|
|
tp = np.diag(matrix)
|
|
denominator = matrix.sum(0) + matrix.sum(1)
|
|
f1 = np.divide(2 * tp, denominator, out=np.zeros(classes, dtype=float), where=denominator > 0)
|
|
return {'accuracy': float(tp.sum() / len(labels)), 'macro_f1': float(f1.mean()),
|
|
'per_class_f1': f1.tolist(), 'confusion_matrix': matrix.tolist(), 'samples': len(labels)}
|
|
|
|
|
|
def predict(model, x, batch_size=128):
|
|
model.eval()
|
|
with torch.inference_mode():
|
|
return torch.cat([model(x[i:i + batch_size]) for i in range(0, len(x), batch_size)]).cpu().numpy()
|
|
|
|
|
|
def latency(model, x, repetitions=100):
|
|
"""CPU model-only batch-one timing, excluding preprocessing and IO."""
|
|
sample = x[:1]
|
|
model.eval()
|
|
with torch.inference_mode():
|
|
for _ in range(20):
|
|
model(sample)
|
|
elapsed = []
|
|
for _ in range(repetitions):
|
|
start = time.perf_counter_ns()
|
|
model(sample)
|
|
elapsed.append((time.perf_counter_ns() - start) / 1e6)
|
|
return {'p50_ms': float(np.percentile(elapsed, 50)), 'p95_ms': float(np.percentile(elapsed, 95)),
|
|
'batch_size': 1, 'repetitions': repetitions, 'scope': 'CPU model only, excludes preprocessing and IO'}
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
|
|
|
|
def write_json(path, value):
|
|
Path(path).write_text(json.dumps(value, indent=2, allow_nan=False) + '\n')
|
|
|
|
|
|
def validate_partitions(partitions, classes):
|
|
shape = None
|
|
for split in ('train', 'validation', 'test'):
|
|
x, y = partitions[split]['x'], partitions[split]['y']
|
|
if not isinstance(x, np.ndarray) or x.ndim != 3 or not 0 < len(x) <= 100000 or not 1 <= x.shape[1] <= 256 or not 2 <= x.shape[2] <= 2048:
|
|
raise ValueError('Expected bounded N,C,T amplitude arrays')
|
|
if x.nbytes > 2 * 1024**3 or not np.isfinite(x).all():
|
|
raise ValueError('Oversized or nonfinite amplitudes')
|
|
if not isinstance(y, np.ndarray) or y.shape != (len(x),) or y.dtype.kind not in 'iu' or np.any(y < 0) or np.any(y >= classes):
|
|
raise ValueError('Invalid class labels')
|
|
if shape is not None and shape != x.shape[1:]:
|
|
raise ValueError('Partitions have inconsistent signal shape')
|
|
shape = x.shape[1:]
|
|
if set(partitions['train']['y'].tolist()) != set(range(classes)):
|
|
raise ValueError('Training partition must contain every class')
|
|
return shape
|
|
|
|
|
|
def optimize(partitions, provenance, output, *, epochs=20, seed=42, batch_size=64,
|
|
variants=('statistics', 'temporal16', 'temporal32'), learning_rate=0.003, max_seconds=600):
|
|
# Internal API: partitions must come from the provenance-validating intake.
|
|
if not 1 <= max_seconds <= 3600:
|
|
raise ValueError('Invalid time budget')
|
|
started = time.monotonic()
|
|
if not 1 <= epochs <= 200 or not 1 <= batch_size <= 1024 or not 0 < learning_rate <= 0.1:
|
|
raise ValueError('Invalid bounded training parameters')
|
|
if not variants or len(set(variants)) != len(variants) or any(v not in ('statistics', 'temporal16', 'temporal32') for v in variants):
|
|
raise ValueError('Invalid architecture search')
|
|
classes = len(provenance['classes'])
|
|
if not 2 <= classes <= 100:
|
|
raise ValueError('Invalid classes')
|
|
channels, samples = validate_partitions(partitions, classes)
|
|
root = Path(output)
|
|
root.mkdir(parents=True, exist_ok=False)
|
|
write_json(root / 'status.json', {'status': 'running', 'promotionAuthorized': False})
|
|
try:
|
|
torch.set_num_threads(1)
|
|
torch.use_deterministic_algorithms(True)
|
|
# Streaming channel statistics avoid a second full float64 dataset copy.
|
|
train_x = partitions['train']['x']
|
|
count = len(train_x) * samples
|
|
total = np.zeros(channels, dtype=np.float64)
|
|
squares = np.zeros(channels, dtype=np.float64)
|
|
for start in range(0, len(train_x), batch_size):
|
|
block = train_x[start:start + batch_size].astype(np.float64)
|
|
total += block.sum(axis=(0, 2))
|
|
squares += np.square(block).sum(axis=(0, 2))
|
|
mean = total / count
|
|
scale = np.sqrt(np.maximum(squares / count - mean**2, 1e-12))
|
|
tensors = {}
|
|
for split, part in partitions.items():
|
|
normalized = ((part['x'] - mean[None, :, None]) / scale[None, :, None]).astype(np.float32)
|
|
if not np.isfinite(normalized).all():
|
|
raise ValueError('Normalization overflow')
|
|
tensors[split] = torch.from_numpy(normalized)
|
|
labels = torch.from_numpy(partitions['train']['y'].astype(np.int64))
|
|
weights = len(labels) / (classes * np.bincount(labels.numpy(), minlength=classes))
|
|
loss_fn = nn.CrossEntropyLoss(weight=torch.tensor(weights, dtype=torch.float32))
|
|
records, winner, winner_score = [], None, -1.0
|
|
for variant in variants:
|
|
torch.manual_seed(seed)
|
|
rng = np.random.default_rng(seed)
|
|
model = AmplitudeClassifier(channels, classes, variant)
|
|
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=1e-4)
|
|
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
|
best, state, history, best_epoch = -1.0, None, [], None
|
|
for epoch in range(epochs):
|
|
model.train()
|
|
order = rng.permutation(len(labels))
|
|
for start in range(0, len(order), batch_size):
|
|
if time.monotonic() - started > max_seconds:
|
|
raise TimeoutError('Training time budget exceeded; no complete candidate')
|
|
ids = order[start:start + batch_size]
|
|
optimizer.zero_grad(set_to_none=True)
|
|
loss = loss_fn(model(tensors['train'][ids]), labels[ids])
|
|
if not torch.isfinite(loss):
|
|
raise ValueError('Nonfinite training loss')
|
|
loss.backward()
|
|
nn.utils.clip_grad_norm_(model.parameters(), 1.0, error_if_nonfinite=True)
|
|
optimizer.step()
|
|
scheduler.step()
|
|
result = scores(partitions['validation']['y'], predict(model, tensors['validation']).argmax(1), classes)
|
|
history.append({'epoch': epoch + 1, 'validation_macro_f1': result['macro_f1']})
|
|
if result['macro_f1'] > best:
|
|
best, state, best_epoch = result['macro_f1'], copy.deepcopy(model.state_dict()), epoch + 1
|
|
if epoch + 1 - best_epoch >= 5:
|
|
break
|
|
model.load_state_dict(state)
|
|
model.eval()
|
|
record = {'variant': variant, 'best_epoch': best_epoch, 'validation_macro_f1': best,
|
|
'parameters': sum(p.numel() for p in model.parameters()), 'history': history,
|
|
'latency': latency(model, tensors['validation'])}
|
|
records.append(record)
|
|
# Ties keep the earlier predeclared candidate.
|
|
if best > winner_score:
|
|
winner, winner_score = copy.deepcopy(model), best
|
|
original = predict(winner, tensors['validation'])
|
|
fused = copy.deepcopy(winner)
|
|
if fused.variant != 'statistics':
|
|
for conv, bn in ((0, 1), (3, 4)):
|
|
fused.net[conv] = torch.nn.utils.fusion.fuse_conv_bn_eval(fused.net[conv], fused.net[bn])
|
|
fused.net[bn] = nn.Identity()
|
|
difference = float(np.max(np.abs(original - predict(fused, tensors['validation']))))
|
|
if difference > 1e-4:
|
|
raise ValueError('Fused inference exceeded logit equivalence tolerance')
|
|
# Architecture and checkpoint are frozen before first test predictions.
|
|
test_metrics = scores(partitions['test']['y'], predict(fused, tensors['test']).argmax(1), classes)
|
|
majority = int(np.bincount(labels.numpy(), minlength=classes).argmax())
|
|
majority_metrics = scores(partitions['test']['y'], np.full(len(partitions['test']['y']), majority), classes)
|
|
prep = {'mean': mean.tolist(), 'scale': scale.tolist(), 'fit_partition': 'train',
|
|
'channels': channels, 'samples': samples, 'input': 'amplitude_NCT', 'classes': provenance['classes']}
|
|
write_json(root / 'preprocessing.json', prep)
|
|
save_file({k: v.contiguous() for k, v in fused.state_dict().items()}, str(root / 'model.safetensors'))
|
|
reloaded = AmplitudeClassifier(channels, classes, winner.variant, winner.variant != 'statistics').eval()
|
|
reloaded.load_state_dict(load_file(str(root / 'model.safetensors')), strict=True)
|
|
reload_error = float(np.max(np.abs(predict(fused, tensors['validation']) - predict(reloaded, tensors['validation']))))
|
|
if reload_error != 0.0:
|
|
raise ValueError('Exported inference failed exact reload equivalence')
|
|
report = {'schema_version': 1, 'status': 'complete', 'promotionAuthorized': False,
|
|
'task': 'public_amplitude_activity_classification', 'variant': winner.variant,
|
|
'fused': winner.variant != 'statistics', 'seed': seed, 'epochs': epochs,
|
|
'batch_size': batch_size, 'learning_rate': learning_rate, 'early_stopping_patience': 5, 'selection': 'validation macro F1 only; first candidate wins ties',
|
|
'candidates': records, 'test': test_metrics, 'majority_test': majority_metrics,
|
|
'fused_max_abs_logit_error': difference, 'reload_max_abs_logit_error': reload_error, 'latency': latency(fused, tensors['validation']),
|
|
'parameters': sum(p.numel() for p in fused.parameters()), 'model_bytes': (root / 'model.safetensors').stat().st_size,
|
|
'elapsed_seconds': time.monotonic() - started, 'max_seconds': max_seconds, 'provenance': provenance,
|
|
'evidence': 'SYNTHETIC' if provenance.get('evidence') == 'SYNTHETIC' else 'MEASURED',
|
|
'implementation_sha256': digest(__file__),
|
|
'runtime': {'torch': torch.__version__, 'numpy': np.__version__, 'platform': platform.platform(), 'threads': 1, 'device': 'cpu'},
|
|
'model_sha256': digest(root / 'model.safetensors'), 'preprocessing_sha256': digest(root / 'preprocessing.json')}
|
|
write_json(root / 'metrics.json', report)
|
|
write_json(root / 'status.json', {'status': 'complete', 'promotionAuthorized': False})
|
|
return report
|
|
except Exception:
|
|
write_json(root / 'status.json', {'status': 'failed', 'promotionAuthorized': False})
|
|
raise
|
|
|
|
|
|
class AmplitudeBundle:
|
|
def __init__(self, directory):
|
|
root = Path(directory)
|
|
for name in ('status.json', 'metrics.json', 'preprocessing.json', 'model.safetensors'):
|
|
path = root / name
|
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > 10 * 1024**2:
|
|
raise ValueError('Invalid bounded bundle artifact')
|
|
status = json.loads((root / 'status.json').read_text())
|
|
report = json.loads((root / 'metrics.json').read_text())
|
|
if status.get('status') != 'complete' or status.get('promotionAuthorized') is not False or report.get('status') != 'complete' or report.get('promotionAuthorized') is not False or report.get('task') != 'public_amplitude_activity_classification':
|
|
raise ValueError('Not a complete proposal-only amplitude bundle')
|
|
for name in ('model', 'preprocessing'):
|
|
filename = name + ('.safetensors' if name == 'model' else '.json')
|
|
if digest(root / filename) != report.get(name + '_sha256'):
|
|
raise ValueError('Bundle hash mismatch')
|
|
prep = json.loads((root / 'preprocessing.json').read_text())
|
|
self.channels, self.samples = prep['channels'], prep['samples']
|
|
self.mean, self.scale = np.asarray(prep['mean']), np.asarray(prep['scale'])
|
|
if not 1 <= self.channels <= 256 or not 2 <= self.samples <= 2048 or self.mean.shape != (self.channels,) or self.scale.shape != self.mean.shape or not np.isfinite(self.mean).all() or not np.isfinite(self.scale).all() or not (self.scale > 0).all() or prep.get('fit_partition') != 'train':
|
|
raise ValueError('Invalid preprocessing')
|
|
if not 2 <= len(prep['classes']) <= 100:
|
|
raise ValueError('Invalid class count')
|
|
self.preprocessing_folded = report.get('preprocessing_folded', False)
|
|
if not isinstance(self.preprocessing_folded, bool) or (self.preprocessing_folded and report['variant'] != 'statistics'):
|
|
raise ValueError('Invalid folded preprocessing architecture')
|
|
self.model = AmplitudeClassifier(self.channels, len(prep['classes']), report['variant'], report['fused']).eval()
|
|
self.model.load_state_dict(load_file(str(root / 'model.safetensors')), strict=True)
|
|
if any(not torch.isfinite(v).all() for v in self.model.state_dict().values()):
|
|
raise ValueError('Nonfinite weights')
|
|
|
|
def predict(self, amplitudes):
|
|
x = np.asarray(amplitudes)
|
|
if x.ndim != 3 or x.shape[1:] != (self.channels, self.samples) or not 0 < len(x) <= 4096 or not np.isfinite(x).all():
|
|
raise ValueError('Expected finite bounded N,C,T amplitudes')
|
|
if self.preprocessing_folded:
|
|
x = x.astype(np.float32, copy=False)
|
|
else:
|
|
x = ((x - self.mean[None, :, None]) / self.scale[None, :, None]).astype(np.float32)
|
|
if not np.isfinite(x).all():
|
|
raise ValueError('Normalization overflow')
|
|
return predict(self.model, torch.from_numpy(x))
|
|
|
|
|
|
|
|
def optimize_bundle(source, validation, output, *, repetitions=1000):
|
|
"""Fold statistics preprocessing without training or recomputing test predictions.
|
|
|
|
For positive channel scale s, mean((x-m)/s)=(mean(x)-m)/s,
|
|
std((x-m)/s)=std(x)/s and absdiff((x-m)/s)=absdiff(x)/s.
|
|
Therefore the affine transform folds into the first linear layer exactly
|
|
in real arithmetic. Floating point equivalence is checked on validation.
|
|
"""
|
|
if not 50 <= repetitions <= 2000:
|
|
raise ValueError('Invalid benchmark repetitions')
|
|
torch.set_num_threads(1)
|
|
original = AmplitudeBundle(source)
|
|
if original.model.variant != 'statistics' or original.preprocessing_folded:
|
|
raise ValueError('Requires an unfused statistics bundle')
|
|
# Validate signal contract before creating outputs.
|
|
original.predict(validation[:1])
|
|
if len(validation) > 4096:
|
|
raise ValueError('Validation population exceeds bound')
|
|
expected = original.predict(validation)
|
|
folded = copy.deepcopy(original.model)
|
|
linear = folded.net[0]
|
|
weight = linear.weight.detach().double()
|
|
scale = torch.from_numpy(np.tile(original.scale, 3))
|
|
offset = torch.from_numpy(original.mean / original.scale)
|
|
with torch.no_grad():
|
|
linear.bias.copy_((linear.bias.double() - weight[:, :original.channels] @ offset).float())
|
|
linear.weight.copy_((weight / scale[None, :]).float())
|
|
if any(not torch.isfinite(v).all() for v in folded.state_dict().values()):
|
|
raise ValueError('Folded weights are not finite')
|
|
actual = predict(folded, torch.from_numpy(np.asarray(validation, dtype=np.float32)))
|
|
max_error = float(np.max(np.abs(expected - actual)))
|
|
if not np.allclose(expected, actual, rtol=1e-4, atol=1e-4) or not np.array_equal(expected.argmax(1), actual.argmax(1)):
|
|
raise ValueError('Folded preprocessing failed logit or prediction equivalence')
|
|
root = Path(output)
|
|
root.mkdir(parents=True, exist_ok=False)
|
|
write_json(root / 'status.json', {'status': 'running', 'promotionAuthorized': False})
|
|
try:
|
|
source = Path(source)
|
|
report = json.loads((source / 'metrics.json').read_text())
|
|
# Historical test evidence stays in the parent bundle, not a new score.
|
|
for key in ('test', 'majority_test', 'latency'):
|
|
report.pop(key, None)
|
|
(root / 'preprocessing.json').write_bytes((source / 'preprocessing.json').read_bytes())
|
|
save_file({k: v.contiguous() for k, v in folded.state_dict().items()}, str(root / 'model.safetensors'))
|
|
report.update({'preprocessing_folded': True,
|
|
'model_sha256': digest(root / 'model.safetensors'),
|
|
'model_bytes': (root / 'model.safetensors').stat().st_size,
|
|
'implementation_sha256': digest(__file__),
|
|
'parent_metrics_sha256': digest(source / 'metrics.json'),
|
|
'parent_model_sha256': digest(source / 'model.safetensors'),
|
|
'test_evaluation': 'Test predictions and metrics not recomputed; transformation checked on validation',
|
|
'optimization': {'operation': 'Fold channel normalization into statistics linear layer',
|
|
'validation_samples': len(validation), 'max_abs_logit_error': max_error,
|
|
'identical_validation_predictions': True}})
|
|
write_json(root / 'metrics.json', report)
|
|
# Validate the reloaded tensors using the same public prediction path,
|
|
# while status remains running until verification and benchmarking end.
|
|
optimized = copy.deepcopy(original)
|
|
optimized.preprocessing_folded = True
|
|
optimized.model.load_state_dict(load_file(str(root / 'model.safetensors')), strict=True)
|
|
reloaded = optimized.predict(validation)
|
|
if not np.array_equal(actual, reloaded):
|
|
raise ValueError('Folded bundle reload changed logits')
|
|
sample = validation[:1]
|
|
for _ in range(30):
|
|
original.predict(sample)
|
|
optimized.predict(sample)
|
|
times = {'original': [], 'optimized': []}
|
|
# Interleave and alternate order to reduce clock/load drift bias.
|
|
for i in range(repetitions):
|
|
order = [('original', original), ('optimized', optimized)]
|
|
if i % 2:
|
|
order.reverse()
|
|
for name, bundle in order:
|
|
started = time.perf_counter_ns()
|
|
bundle.predict(sample)
|
|
times[name].append((time.perf_counter_ns() - started) / 1e6)
|
|
timings = {name: {'p50_ms': float(np.percentile(values, 50)),
|
|
'p95_ms': float(np.percentile(values, 95))} for name, values in times.items()}
|
|
report['optimization'].update({'batch_size': 1, 'repetitions': repetitions, 'threads': 1,
|
|
'scope': 'CPU bundle predict including input checks and normalization; excludes IO and temporal pooling',
|
|
'timings': timings, 'reload_max_abs_logit_error': 0.0,
|
|
'p50_speedup': timings['original']['p50_ms'] / timings['optimized']['p50_ms']})
|
|
write_json(root / 'metrics.json', report)
|
|
write_json(root / 'status.json', {'status': 'complete', 'promotionAuthorized': False})
|
|
return report
|
|
except Exception:
|
|
write_json(root / 'status.json', {'status': 'failed', 'promotionAuthorized': False})
|
|
raise
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--manifest', required=True)
|
|
parser.add_argument('--output', required=True)
|
|
parser.add_argument('--epochs', type=int, default=20)
|
|
parser.add_argument('--batch-size', type=int, default=64)
|
|
parser.add_argument('--seed', type=int, default=42)
|
|
parser.add_argument('--max-seconds', type=int, default=600)
|
|
parser.add_argument('--optimize-bundle', help='Existing statistics bundle; inference only, no training or test evaluation')
|
|
parser.add_argument('--confirmation-manifest', help='Independent-confirmation metadata plan (enables enforced gate)')
|
|
parser.add_argument('--exposure-ledger', help='Previously exposed dataset revisions for the confirmation gate')
|
|
parser.add_argument('--expected-protocol-sha256', help='Frozen protocol digest for the confirmation gate')
|
|
parser.add_argument('--expected-dataset-manifest-sha256', help='Frozen digest of --manifest bytes')
|
|
args = parser.parse_args(argv)
|
|
gate_names = ('confirmation_manifest', 'exposure_ledger', 'expected_protocol_sha256',
|
|
'expected_dataset_manifest_sha256')
|
|
gate_values = [getattr(args, name) for name in gate_names]
|
|
gate_enabled = all(gate_values)
|
|
if (any(gate_values) and not gate_enabled) or (gate_enabled and args.optimize_bundle):
|
|
print(json.dumps({'status': 'invalid_input', 'promotionAuthorized': False}))
|
|
raise SystemExit(1)
|
|
preflight = None
|
|
if gate_enabled:
|
|
from .confirmation import training_gate
|
|
try:
|
|
preflight = training_gate(args.confirmation_manifest, args.exposure_ledger,
|
|
args.manifest, args.expected_protocol_sha256,
|
|
args.expected_dataset_manifest_sha256)
|
|
except (ValueError, OSError, TypeError, RecursionError, OverflowError):
|
|
print(json.dumps({'status': 'invalid_input', 'promotionAuthorized': False}))
|
|
raise SystemExit(1)
|
|
if not preflight['preflight_passed']:
|
|
print(json.dumps(preflight, sort_keys=True))
|
|
raise SystemExit(2)
|
|
from .espfi import load_dataset
|
|
partitions, provenance = load_dataset(args.manifest)
|
|
if preflight:
|
|
if provenance.get('manifest_sha256') != preflight['dataset_manifest_sha256']:
|
|
raise ValueError('Dataset manifest changed after confirmation preflight')
|
|
provenance = dict(provenance, confirmation_preflight=preflight)
|
|
if args.optimize_bundle:
|
|
parent = json.loads((Path(args.optimize_bundle) / 'metrics.json').read_text())
|
|
if provenance['manifest_sha256'] != parent['provenance']['manifest_sha256']:
|
|
raise ValueError('Optimization dataset differs from training provenance')
|
|
report = optimize_bundle(args.optimize_bundle, partitions['validation']['x'], args.output)
|
|
print(json.dumps(report['optimization'], indent=2))
|
|
return
|
|
report = optimize(partitions, provenance, args.output, epochs=args.epochs, batch_size=args.batch_size, seed=args.seed, max_seconds=args.max_seconds)
|
|
print(json.dumps({'variant': report['variant'], 'test': report['test'], 'latency': report['latency'], 'promotionAuthorized': False}, indent=2))
|