fa98e4722e
The guard answers one question — may this utterance become an executable action — as a three-way policy gate (permissive / blocked / ambiguous) and never decides what the utterance is. Rules are the encoding of the measured slice-20 dev-pool discriminators: 126 capability-question rows are 42/42/42 addressed / bare-ability / bare-future; 127 bare можешь+пожалуйста rows are 100% action; can-you-please is 100% action. Reuses the shipped prohibition parser, morph finiteness and lexicon fillers; reason vocabulary is closed. Slice 21 (task/725, brief after the accepted slice 20).
135 lines
5.5 KiB
Python
135 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Slice 21 emit: deterministic execution-frame guard — data files for the Go harness
|
|
==================================================================================
|
|
|
|
Slice 18 showed the sparse lexical gate owns the aggregate boundary (PR-AUC
|
|
0.838, strict operating point at threshold 0.715 with P>=0.95 | R=0.264) and
|
|
slice 19/20 showed learning heads collapse on capability-question LOFO. Slice 21
|
|
tests the deterministic alternative: a rule engine over existing parsers that
|
|
decides execution eligibility as a three-way gate (permissive / blocked /
|
|
ambiguous), never itself routing.
|
|
|
|
This script only repackages the frozen dev pool for the Go harness. It reuses
|
|
slice 18's feature builders and grouped-CV and slice 19's pair builder verbatim,
|
|
so the numbers the Go side reports are the same populations the accepts
|
|
measured. It writes:
|
|
|
|
/tmp/mvn-s21/pool.json dev rows: idx, text, n_text, route, y, tags,
|
|
cv_fold, split_group, source_id, family
|
|
/tmp/mvn-s21/pairs.json capability-vs-action pairs (slice-19 builder)
|
|
/tmp/mvn-s21/sparse_oof.json slice-18 "both" grouped-CV OOF proba per row
|
|
(for the §17 guard+sparse composition)
|
|
|
|
No training happens here and no label is changed. The guard itself is Go.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
|
|
import slice18_sparse # noqa: E402
|
|
import slice19_main # noqa: E402
|
|
|
|
OUT_DIR = "/tmp/mvn-s21"
|
|
SPARSE_THRESHOLD = 0.715 # slice-18 §4 strict-operating-point (P>=0.95 best recall)
|
|
|
|
|
|
def main():
|
|
# Population = the exact slice-20 dev pool (s19.load_dev): every dev_pool
|
|
# row, fast-path included. The guard is evaluated on what slice 20 measured.
|
|
meta, examples = slice18_sparse.load_data()
|
|
dev = slice18_sparse.filter_dev_pool(examples)
|
|
print(f"dev pool (all dev_pool rows): {len(dev)} rows")
|
|
print(f"corpus meta: {meta.get('dev_count', '?')} dev rows declared, "
|
|
f"{meta.get('route_counts', {}).get('action', '?')} action declared")
|
|
|
|
n_texts = [slice18_sparse.normalize_match_text(e["text"]) for e in dev]
|
|
|
|
rows = []
|
|
by_route = {}
|
|
by_family = {}
|
|
for i, (e, nt) in enumerate(zip(dev, n_texts)):
|
|
tags = sorted(set(e.get("tags", [])))
|
|
route = e["route"]
|
|
fam = slice19_main.family_of(set(tags))
|
|
by_route[route] = by_route.get(route, 0) + 1
|
|
by_family[fam] = by_family.get(fam, 0) + 1
|
|
rows.append({
|
|
"idx": i,
|
|
"text": e["text"],
|
|
"n_text": nt,
|
|
"route": route,
|
|
"y": 1 if route == "action" else 0,
|
|
"tags": tags,
|
|
"cv_fold": e["cv_fold"],
|
|
"split_group": e["split_group"],
|
|
"source_id": e["source_id"],
|
|
})
|
|
|
|
print("routes:", by_route)
|
|
print("families:", by_family)
|
|
|
|
# ── pairs (slice-19 builder, exact population) ─────────────────────────
|
|
ldev = [{
|
|
"text_orig": nt,
|
|
"route": r["route"],
|
|
"y": r["y"],
|
|
"cv_fold": r["cv_fold"],
|
|
"tags": set(r["tags"]),
|
|
"source_id": r["source_id"],
|
|
} for r, nt in zip(rows, n_texts)]
|
|
pairs = slice19_main.build_pairs(ldev, n_texts)
|
|
print(f"pairs: {len(pairs)}")
|
|
|
|
# ── slice-18 "both" grouped-CV OOF proba, aligned to row index ────────
|
|
y = [1 if r["route"] == "action" else 0 for r in rows]
|
|
folds = [r["cv_fold"] for r in rows]
|
|
X, _vec = slice18_sparse.build_features(n_texts, "both")
|
|
print(f"sparse 'both' X: {X.shape}")
|
|
yb = np.array(y)
|
|
folds_arr = np.array(folds)
|
|
idx_proba = {}
|
|
for te_fold in sorted(set(folds)):
|
|
tr = folds_arr != te_fold
|
|
te = folds_arr == te_fold
|
|
clf = slice18_sparse.LogisticRegression(
|
|
C=1.0, max_iter=2000, solver="lbfgs", random_state=42)
|
|
clf.fit(X[tr], yb[tr])
|
|
p = clf.predict_proba(X[te])[:, 1]
|
|
te_idx = np.where(te)[0]
|
|
for k, i in enumerate(te_idx):
|
|
idx_proba[int(i)] = float(p[k])
|
|
assert len(idx_proba) == len(rows)
|
|
sparse_oof = [{"idx": i, "proba": idx_proba[i]} for i in range(len(rows))]
|
|
pred = [1 if idx_proba[i] >= 0.5 else 0 for i in range(len(rows))]
|
|
tp = sum(1 for i in range(len(rows)) if y[i] == 1 and pred[i] == 1)
|
|
fp = sum(1 for i in range(len(rows)) if y[i] == 0 and pred[i] == 1)
|
|
fn = sum(1 for i in range(len(rows)) if y[i] == 1 and pred[i] == 0)
|
|
print(f"sparse both OOF @0.5: P={tp/max(tp+fp,1):.3f} R={tp/max(tp+fn,1):.3f} "
|
|
f"FA={fp} ({fp/len(rows):.4f})")
|
|
pred21 = [1 if idx_proba[i] >= SPARSE_THRESHOLD else 0 for i in range(len(rows))]
|
|
tp = sum(1 for i in range(len(rows)) if y[i] == 1 and pred21[i] == 1)
|
|
fp = sum(1 for i in range(len(rows)) if y[i] == 0 and pred21[i] == 1)
|
|
fn = sum(1 for i in range(len(rows)) if y[i] == 1 and pred21[i] == 0)
|
|
print(f"sparse both OOF @{SPARSE_THRESHOLD}: P={tp/max(tp+fp,1):.3f} "
|
|
f"R={tp/max(tp+fn,1):.3f} FA={fp} ({fp/len(rows):.4f})")
|
|
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
with open(os.path.join(OUT_DIR, "pool.json"), "w") as f:
|
|
json.dump(rows, f, ensure_ascii=False, indent=1)
|
|
with open(os.path.join(OUT_DIR, "pairs.json"), "w") as f:
|
|
json.dump([[c, a] for c, a in pairs], f)
|
|
with open(os.path.join(OUT_DIR, "sparse_oof.json"), "w") as f:
|
|
json.dump(sparse_oof, f)
|
|
print(f"wrote {OUT_DIR}/{{pool,pairs,sparse_oof}}.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |