Ship the learned-key pipeline, without which six figures cannot be rebuilt
The package was missing every script behind the KM (lrn.) curves and both learned table rows: exp_learned's driver, the merge that folds the learned rows into sec_compare.csv and refresh_summary.csv, and the report that reads the learned numbers back. It was also missing sec_keylen_perm.csv, so Fig. 3 could not be regenerated at all, and the two diagnostics that answer why a fixed key beats a learned one here and where a learned mask would win instead. The learned artifacts themselves are regenerated. They were trained on the cross-entropy alone, which drifts to disjoint sparse supports: 99 percent of each key's energy on about six of the 64 entries, so a digit is decided over a sixth of its period and the key set is a choice of support rather than a dense direction in R^L. They are now the regularized keys of Section V-C, and check_consistency asserts which of the two families the figures draw. Verified from inside this repository: replot_security.py rebuilds all seven result figures, make_tables.py reproduces both result tables, and check_consistency.py passes every check that does not need the manuscript. The README now lists what ships. Its run list, layout and figure map had none of the learned pipeline, named two tables the manuscript renders as prose, and gave Fig. 3 no data file for its permutation curve.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Why do structured keys beat learned ones on the legitimate error rate?
|
||||
|
||||
The gap is 0.053 against 0.064 at 10 dB, and a reader may reasonably
|
||||
suspect that the learned keys are handicapped, since they alone are
|
||||
trained under the channel while the structured ones are fixed by
|
||||
construction. This measures where the gap comes from.
|
||||
|
||||
Three questions, one experiment each.
|
||||
|
||||
1. Is the gap a channel-adaptation failure? If it were, the two families
|
||||
would differ by more at some channel qualities than at others. The
|
||||
ratio across the SNR sweep answers this from data already on disk.
|
||||
|
||||
2. Is the structured key a point that training can improve on? Start
|
||||
training from the Walsh-Hadamard keys with the masks unfrozen and let
|
||||
Adam move them. If the structured point is a genuine optimum, the
|
||||
error rate stays or rises; if training is merely under-converged from
|
||||
its random start, it falls.
|
||||
|
||||
3. What does the learned key lose? Two candidates, measured directly:
|
||||
residual cross-user correlation, which the analysis names as the
|
||||
first-order leakage and interference term, and departure from unit
|
||||
modulus, which spreads the key energy unevenly across the entries so
|
||||
that a digit is decided over an effectively shorter support.
|
||||
|
||||
Run: python code/diag_whygap.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from exp_full import (MAIN_D, base_keys, get_model, main_model, # noqa: E402
|
||||
mean_abs_xcorr)
|
||||
from sse_lib import DATA, DEVICE, eval_ser_sse # noqa: E402
|
||||
import sse_lib as L # noqa: E402
|
||||
|
||||
FRAMES = 300_000
|
||||
SNR = 10.0
|
||||
|
||||
|
||||
def modulus_stats(W):
|
||||
"""How far the key entries are from unit modulus, per user.
|
||||
|
||||
A unit-modulus key puts the same energy on every entry, so the
|
||||
signal term does not depend on the key and every entry of the period
|
||||
carries its share of the decision. The ratio below is the effective
|
||||
fraction of the L entries the key actually uses, by the
|
||||
participation ratio (sum a^2)^2 / (L sum a^4) with a the entry
|
||||
magnitudes. It is one for a unit-modulus key and 1/L for a key that
|
||||
puts everything on one entry.
|
||||
"""
|
||||
a2 = W.pow(2)
|
||||
pr = a2.sum(dim=1).pow(2) / (W.shape[1] * a2.pow(2).sum(dim=1))
|
||||
return pr
|
||||
|
||||
|
||||
def report(name, model, rows):
|
||||
W = model.masks().detach().cpu()
|
||||
ser = eval_ser_sse(model, [SNR], frames=FRAMES)[0]
|
||||
kap = mean_abs_xcorr(model.masks().detach())
|
||||
pr = modulus_stats(W)
|
||||
print("%-24s SER %.5f kappa-bar %.5f entry use %.3f"
|
||||
% (name, ser, kap, float(pr.mean())))
|
||||
rows.append([name, "%.5f" % ser, "%.5f" % kap, "%.4f" % float(pr.mean())])
|
||||
return ser
|
||||
|
||||
|
||||
def main():
|
||||
rows = []
|
||||
print("main configuration d=%d, L=%d, 10 dB, %d frames\n"
|
||||
% (MAIN_D, MAIN_D // 4, FRAMES))
|
||||
|
||||
print("-- the two families as the paper plots them --")
|
||||
fix = main_model()
|
||||
s_fix = report("structured (frozen)", fix, rows)
|
||||
free = get_model(iters=4000)
|
||||
s_free = report("learned (free start)", free, rows)
|
||||
|
||||
print("\n-- question 2: can training improve on the structured key? --")
|
||||
# same trainer, same iterations, same seed, but the masks start at
|
||||
# the Walsh-Hadamard point and are free to move
|
||||
from sse_lib import SSE, set_seed
|
||||
set_seed(1)
|
||||
m = SSE(P=4, vu=16, d=MAIN_D, users=4).to(DEVICE)
|
||||
with torch.no_grad():
|
||||
m.W.copy_(base_keys(4, MAIN_D // 4).to(DEVICE))
|
||||
m.W.requires_grad_(True)
|
||||
L.train_sse(m, iters=4000, batch=256, lr=3e-3, seed=1)
|
||||
m.calibrate_power()
|
||||
s_warm = report("learned (Walsh start)", m, rows)
|
||||
|
||||
print("\nreading:")
|
||||
print(" free start %+.1f percent against the structured key"
|
||||
% (100.0 * (s_free - s_fix) / s_fix))
|
||||
print(" Walsh start %+.1f percent against the structured key"
|
||||
% (100.0 * (s_warm - s_fix) / s_fix))
|
||||
if s_warm > s_fix:
|
||||
print(" training moves off the structured point and pays for it,")
|
||||
print(" so the structured key is not a point learning improves on.")
|
||||
else:
|
||||
print(" training improves on the structured point, so the gap is")
|
||||
print(" under-convergence from the random start, not geometry.")
|
||||
|
||||
out = DATA / "whygap.csv"
|
||||
with open(out, "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["family", "legit_ser", "kappa_bar", "entry_use"])
|
||||
w.writerows(rows)
|
||||
print("\n[csv]", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user