mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-22 06:32:35 -05:00
142 lines
7.0 KiB
Python
142 lines
7.0 KiB
Python
"""Fixed label-dependent ridge probes for frozen activity representations.
|
|
|
|
Only training labels/features fit the probes. Development metrics never select
|
|
hyperparameters. Numeric activity IDs 0..6 have no occupancy semantics.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
|
|
import numpy as np
|
|
|
|
NAMES = ('baseline', 'candidate', 'untrained', 'statistics')
|
|
CONFIG = {'alpha': 1.0, 'intercept': True, 'scaling': 'train_mean_population_std',
|
|
'zero_variance_scale': 1.0, 'classes': list(range(7)),
|
|
'macro_f1_classes': 'all_seven_including_absent', 'selection': 'none'}
|
|
|
|
|
|
def _features(value):
|
|
if (not isinstance(value, np.ndarray) or value.ndim != 2
|
|
or value.dtype.kind != 'f' or value.dtype.itemsize not in (4, 8)
|
|
or not 1 <= value.shape[0] <= 4096 or not 1 <= value.shape[1] <= 1024):
|
|
raise ValueError('Expected bounded float32/float64 N-by-D features')
|
|
if not np.isfinite(value).all() or np.any(np.abs(value) > 1e100):
|
|
raise ValueError('Features must be finite and magnitude <= 1e100')
|
|
return value.astype(np.float64, copy=True)
|
|
|
|
|
|
def _labels(value, length=None):
|
|
if (not isinstance(value, np.ndarray) or value.ndim != 1
|
|
or value.dtype.kind not in 'iu' or not 1 <= len(value) <= 4096
|
|
or (length is not None and len(value) != length)
|
|
or np.any(value < 0) or np.any(value > 6)):
|
|
raise ValueError('Expected aligned integer activity labels 0..6 (no bool)')
|
|
return value.astype(np.int64, copy=True)
|
|
|
|
|
|
def metrics(y, prediction):
|
|
y = _labels(y)
|
|
prediction = _labels(prediction, len(y))
|
|
confusion = np.bincount(7*y + prediction, minlength=49).reshape(7, 7)
|
|
true_positive = np.diag(confusion)
|
|
denominator = confusion.sum(0) + confusion.sum(1)
|
|
f1 = np.divide(2.*true_positive, denominator, out=np.zeros(7), where=denominator != 0)
|
|
return {'accuracy': float(true_positive.sum()/len(y)), 'macro_f1': float(f1.mean()),
|
|
'per_class_f1': f1.tolist(), 'confusion': confusion.tolist(),
|
|
'confusion_axes': 'rows=true numeric activity, columns=predicted numeric activity'}
|
|
|
|
|
|
def _state_hash(state):
|
|
result = hashlib.sha256(json.dumps(CONFIG, sort_keys=True).encode())
|
|
for name in ('mean', 'scale', 'weights', 'bias'):
|
|
value = np.ascontiguousarray(state[name], dtype='<f8')
|
|
result.update(name.encode())
|
|
result.update(json.dumps(value.shape).encode())
|
|
result.update(value.tobytes())
|
|
return result.hexdigest()
|
|
|
|
|
|
def _fit(x, y):
|
|
mean = x.mean(0)
|
|
scale = x.std(0)
|
|
scale[scale == 0] = 1.
|
|
z = (x-mean)/scale
|
|
# Center after scaling to account for floating-point residual mean; this
|
|
# fits an unpenalized intercept even for nearly constant input columns.
|
|
z_mean = z.mean(0)
|
|
centered = z-z_mean
|
|
target = np.eye(7)[y]
|
|
target_mean = target.mean(0)
|
|
centered_target = target-target_mean
|
|
if x.shape[1] <= x.shape[0]:
|
|
weights = np.linalg.solve(centered.T@centered + np.eye(x.shape[1]),
|
|
centered.T@centered_target)
|
|
else:
|
|
weights = centered.T@np.linalg.solve(centered@centered.T + np.eye(x.shape[0]),
|
|
centered_target)
|
|
state = {'mean': mean, 'scale': scale, 'weights': weights,
|
|
'bias': target_mean-z_mean@weights}
|
|
if not all(np.isfinite(v).all() for v in state.values()):
|
|
raise ValueError('Nonfinite fitted probe state')
|
|
return state
|
|
|
|
|
|
def _predict(state, x):
|
|
# Reject overflow in transformation or prediction instead of reporting a
|
|
# spurious class zero from nonfinite scores.
|
|
with np.errstate(over='raise', invalid='raise', divide='raise'):
|
|
try:
|
|
scores = ((x-state['mean'])/state['scale'])@state['weights']+state['bias']
|
|
except FloatingPointError as error:
|
|
raise ValueError('Probe transformation overflow') from error
|
|
if not np.isfinite(scores).all():
|
|
raise ValueError('Nonfinite probe scores')
|
|
return scores.argmax(1)
|
|
|
|
|
|
def run_probes(embeddings, y_train, y_dev):
|
|
"""Return JSON-compatible metrics and fitted-state hashes, never raw data.
|
|
|
|
``embeddings`` must have exactly baseline/candidate/untrained/statistics,
|
|
each mapping to train and dev floating arrays with matching feature width.
|
|
Hashes attest fitted numeric state, not externally verified source identity;
|
|
callers must bind this report to their split/encoder/data provenance.
|
|
"""
|
|
if not isinstance(embeddings, dict) or set(embeddings) != set(NAMES):
|
|
raise ValueError('Expected four frozen representation comparators')
|
|
y_train, y_dev = _labels(y_train), _labels(y_dev)
|
|
parts = {}
|
|
for name in NAMES:
|
|
part = embeddings[name]
|
|
if not isinstance(part, dict) or set(part) != {'train', 'dev'}:
|
|
raise ValueError('Expected train/dev features only')
|
|
train, dev = _features(part['train']), _features(part['dev'])
|
|
if (train.shape[0] != len(y_train) or dev.shape[0] != len(y_dev)
|
|
or train.shape[1] != dev.shape[1]):
|
|
raise ValueError('Feature/label alignment or width mismatch')
|
|
parts[name] = train, dev
|
|
probes = {}
|
|
for name, (train, dev) in parts.items():
|
|
state = _fit(train, y_train)
|
|
probes[name] = dict(metrics(y_dev, _predict(state, dev)),
|
|
feature_count=train.shape[1], training_samples=len(y_train),
|
|
training_parameters=json.loads(json.dumps(CONFIG)), artifact_state_sha256=_state_hash(state),
|
|
fitted_scalar_count=sum(v.size for v in state.values()))
|
|
majority_class = int(np.bincount(y_train, minlength=7).argmax())
|
|
majority = dict(metrics(y_dev, np.full(len(y_dev), majority_class, dtype=np.int64)),
|
|
predicted_activity=majority_class, tie_break='lowest numeric activity')
|
|
candidate = probes['candidate']
|
|
comparisons = [probes[name] for name in ('baseline', 'untrained', 'statistics')]
|
|
best_f1, best_accuracy = max(p['macro_f1'] for p in comparisons), max(p['accuracy'] for p in comparisons)
|
|
checks = {'macro_f1_at_least_best_plus_002': candidate['macro_f1'] >= best_f1+.02,
|
|
'accuracy_within_001_of_best': candidate['accuracy'] >= best_accuracy-.01,
|
|
'no_new_zero_f1_class': all(c > 0 or b == 0 for c, b in
|
|
zip(candidate['per_class_f1'], probes['baseline']['per_class_f1']))}
|
|
return {'schema_version': 1, 'task': 'frozen_numeric_activity_linear_probe',
|
|
'promotionAuthorized': False, 'classes': list(range(7)), 'probes': probes,
|
|
'majority': majority, 'gate': {'checks': checks, 'passed': all(checks.values()),
|
|
'best_comparator_macro_f1': best_f1, 'best_comparator_accuracy': best_accuracy,
|
|
'macro_f1_margin': .02, 'accuracy_tolerance': .01},
|
|
'scope': 'Fixed alpha=1; train-only scaling and label fit. Development labels score only. '
|
|
'No hyperparameter search or representation selection. Caller supplies provenance '
|
|
'and separately evaluates representation collapse and latency.'}
|