{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip -q install PyWavelets\nimport os, numpy as np, torch, pywt\nfrom PIL import Image\nfrom torchvision import models\n\ndef find_root():\n    for r, dirs, _ in os.walk(\"/kaggle/input\"):\n        if \"Cover\" in dirs and \"UERD\" in dirs:\n            return r\n    raise SystemExit(\"ALASKA2 folder not found. Did you click Add Input and join the competition?\")\n\nROOT = find_root()\nprint(\"Pictures are here:\", ROOT)\nprint(\"GPU available:\", torch.cuda.is_available())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T12:19:22.691094Z","iopub.execute_input":"2026-10-17T12:19:22.69151Z","iopub.status.idle":"2026-10-17T12:19:39.539635Z","shell.execute_reply.started":"2026-10-17T12:19:22.691484Z","shell.execute_reply":"2026-10-17T12:19:39.538872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.stats import kurtosis, skew\n\nN_IMAGES = 1000\nPAYLOADS = [0.1, 0.25, 0.5, 1.0]      # share of pixels that carry secret bits\n\nfiles = sorted(os.listdir(f\"{ROOT}/Cover\"))[:N_IMAGES]\nrng = np.random.RandomState(0)\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nalex = models.alexnet(weights=models.AlexNet_Weights.IMAGENET1K_V1).features.to(device).eval()\n\ndef load_gray(path):\n    return np.asarray(Image.open(path).convert(\"L\"), dtype=np.uint8)\n\ndef hide_bits(img, payload):                         # LSB replacement = hide a secret message\n    out = img.copy()\n    mask = rng.rand(*img.shape) < payload\n    secret = rng.randint(0, 2, img.shape).astype(np.uint8)\n    out[mask] = (out[mask] & 0xFE) | secret[mask]\n    return out\n\ndef get_features(img):\n    _, (LH, HL, HH) = pywt.dwt2(img.astype(np.float32), \"bior1.3\")\n    stats = []\n    for b in (LH, HL, HH):\n        a = np.abs(b).ravel()\n        stats += [a.mean(), b.std(), kurtosis(b.ravel()), skew(b.ravel())] + list(np.percentile(a, [5, 10, 25, 50, 75]))\n    chans = torch.tensor(np.stack([LH, HL, HH]) / 10.0, dtype=torch.float32)\n    return stats, chans\n\ndef featurize(imgs):\n    stats, chans = [], []\n    for im in imgs:\n        s, c = get_features(im); stats.append(s); chans.append(c)\n    x = torch.stack(chans).to(device)\n    with torch.no_grad():\n        a = alex(x).mean(dim=(2, 3)).cpu().numpy()   # AlexNet features (256 numbers)\n    return np.hstack([np.array(stats), a])\n\nblocks = []\nfor start in range(0, len(files), 8):\n    imgs = []\n    for f in files[start:start + 8]:\n        c = load_gray(f\"{ROOT}/Cover/{f}\")\n        imgs.append(c)\n        imgs += [hide_bits(c, p) for p in PAYLOADS]\n    out = featurize(imgs)\n    blocks.append(out.reshape(-1, 1 + len(PAYLOADS), out.shape[1]))\n    if (start // 8) % 10 == 0: print(start, \"/\", len(files))\n\nF = np.nan_to_num(np.concatenate(blocks))\nnp.savez(\"/kaggle/working/features_suite.npz\", F=F, payloads=np.array([0.0] + PAYLOADS))\nprint(\"Saved features:\", F.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T12:20:58.815729Z","iopub.execute_input":"2026-10-17T12:20:58.816154Z","iopub.status.idle":"2026-10-17T12:23:14.797818Z","shell.execute_reply.started":"2026-10-17T12:20:58.816128Z","shell.execute_reply":"2026-10-17T12:23:14.796907Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import itertools, time, numpy as np, pandas as pd\nfrom sklearn.feature_selection import SelectKBest, f_classif\nfrom sklearn.preprocessing import MinMaxScaler, StandardScaler\nfrom sklearn.svm import SVC\nfrom sklearn.metrics import confusion_matrix\nfrom sklearn.metrics.pairwise import euclidean_distances\n\nN_QUBITS, REPS = 6, 2\nSEEDS = [42, 43, 44]\nM_CLASSICAL, M_QSVM, M_ALL = \"Classical SVM (6 features)\", \"QSVM (6 qubits)\", \"Classical SVM (all features)\"\n\ndef zz_states(X, n=N_QUBITS, reps=REPS):\n    dim = 2 ** n\n    bits = (np.arange(dim)[:, None] >> np.arange(n)[::-1]) & 1\n    H1 = np.array([[1, 1], [1, -1]]) / np.sqrt(2)\n    Hn = np.array([[1.0]])\n    for _ in range(n):\n        Hn = np.kron(Hn, H1)\n    psi = np.zeros((len(X), dim), dtype=complex); psi[:, 0] = 1\n    for _ in range(reps):\n        psi = psi @ Hn.T\n        phase = 2 * X @ bits.T\n        for i, j in itertools.combinations(range(n), 2):\n            w = 2 * (np.pi - X[:, i]) * (np.pi - X[:, j])\n            phase = phase + w[:, None] * (bits[:, i] ^ bits[:, j])[None, :]\n        psi = psi * np.exp(1j * phase)\n    return psi\n\ndef quantum_kernel(Z):\n    S = zz_states(Z)\n    return np.abs(S @ S.conj().T) ** 2\n\ndef rbf_kernel_matrix(Z, tr):\n    d2 = euclidean_distances(Z, Z, squared=True)\n    return np.exp(-d2 / (Z.shape[1] * Z[tr].var() + 1e-12))\n\ndef best_C(K, y, tr, Cs=(0.1, 1, 10, 100)):\n    tr = np.array(tr); np.random.RandomState(0).shuffle(tr)\n    a, b = tr[:int(.8 * len(tr))], tr[int(.8 * len(tr)):]\n    acc = {C: (SVC(C=C, kernel='precomputed').fit(K[np.ix_(a, a)], y[a])\n               .predict(K[np.ix_(b, a)]) == y[b]).mean() for C in Cs}\n    C = max(acc, key=acc.get); return C, acc[C]\n\ndef fit_predict(X, y, tr, te, use_all=True):\n    out = {}\n    sc = StandardScaler().fit(X[tr]); Xs = sc.transform(X)\n    sel = SelectKBest(f_classif, k=N_QUBITS).fit(Xs[tr], y[tr])\n    Z0 = sel.transform(Xs)\n    Z0 = np.clip(MinMaxScaler((0, 1)).fit(Z0[tr]).transform(Z0), 0, 1)\n    Kc = rbf_kernel_matrix(Z0 * np.pi, tr); C, _ = best_C(Kc, y, tr)\n    out[M_CLASSICAL] = SVC(C=C, kernel='precomputed').fit(Kc[np.ix_(tr, tr)], y[tr]).predict(Kc[np.ix_(te, tr)])\n    best = None\n    for s in (0.25, 0.5, 1.0):\n        Kq = quantum_kernel(Z0 * np.pi * s); C, v = best_C(Kq, y, tr)\n        if best is None or v > best[0]: best = (v, Kq, C)\n    _, Kq, C = best\n    out[M_QSVM] = SVC(C=C, kernel='precomputed').fit(Kq[np.ix_(tr, tr)], y[tr]).predict(Kq[np.ix_(te, tr)])\n    if use_all:\n        Ka = rbf_kernel_matrix(Xs, tr); C, _ = best_C(Ka, y, tr, Cs=(1, 10, 100))\n        out[M_ALL] = SVC(C=C, kernel='precomputed').fit(Ka[np.ix_(tr, tr)], y[tr]).predict(Ka[np.ix_(te, tr)])\n    return out\n\ndef split_by_id(ids, seed, frac=0.2):\n    u = np.unique(ids); r = np.random.RandomState(seed).permutation(u)\n    n_te = int(frac * len(u)); return r[:n_te], r[n_te:]\n\ndef binary_scores(y, p):\n    tn, fp, fn, tp = confusion_matrix(y, p, labels=[0, 1]).ravel()\n    return dict(accuracy=(tp + tn) / len(y), sensitivity=tp / (tp + fn),\n                specificity=tn / (tn + fp), precision=tp / (tp + fp) if tp + fp else 0.0)\n\ndef mean_std(df, by, cols):\n    return (df.groupby(by)[cols].agg([\"mean\", \"std\"]) * 100).round(2)\n\nd = np.load(\"/kaggle/working/features_suite.npz\")\nF, PAY = d[\"F\"], d[\"payloads\"]\nN, K, D = F.shape\ncols = [\"accuracy\", \"sensitivity\", \"specificity\", \"precision\"]\nt0 = time.time()\n\n# Experiment 1: detection (clean vs stego) for every payload\nrows = []\nfor k in range(1, K):\n    X = np.vstack([F[:, 0, :], F[:, k, :]]); y = np.r_[np.zeros(N), np.ones(N)].astype(int)\n    ids = np.r_[np.arange(N), np.arange(N)]\n    for seed in SEEDS:\n        te_ids, tr_ids = split_by_id(ids, seed)\n        tr, te = np.where(np.isin(ids, tr_ids))[0], np.where(np.isin(ids, te_ids))[0]\n        for name, p in fit_predict(X, y, tr, te).items():\n            rows.append(dict(payload=PAY[k], model=name, seed=seed, **binary_scores(y[te], p)))\n    print(\"Experiment 1: payload\", PAY[k], \"done\", round(time.time() - t0), \"s\")\ne1 = pd.DataFrame(rows); e1.to_csv(\"/kaggle/working/exp1_detection.csv\", index=False)\n\n# Experiment 2: payload estimation (5 classes: clean, 0.1, 0.25, 0.5, 1.0)\nX2 = F.reshape(N * K, D); y2 = np.tile(np.arange(K), N); ids2 = np.repeat(np.arange(N), K)\nrows, conf = [], {m: np.zeros((K, K)) for m in (M_CLASSICAL, M_QSVM, M_ALL)}\nfor seed in SEEDS:\n    te_ids, tr_ids = split_by_id(ids2, seed)\n    tr, te = np.where(np.isin(ids2, tr_ids))[0], np.where(np.isin(ids2, te_ids))[0]\n    for name, p in fit_predict(X2, y2, tr, te).items():\n        rows.append(dict(model=name, seed=seed, accuracy=(p == y2[te]).mean(),\n                         mae=np.abs(PAY[p] - PAY[y2[te]]).mean()))\n        conf[name] += confusion_matrix(y2[te], p, labels=range(K))\n    print(\"Experiment 2: seed\", seed, \"done\", round(time.time() - t0), \"s\")\ne2 = pd.DataFrame(rows); e2.to_csv(\"/kaggle/working/exp2_payload_estimation.csv\", index=False)\n\n# Experiment 3: learning curve (detection, payload 0.5, few training pictures)\nk5 = int(np.where(PAY == 0.5)[0][0])\nX3 = np.vstack([F[:, 0, :], F[:, k5, :]]); y3 = np.r_[np.zeros(N), np.ones(N)].astype(int)\nids3 = np.r_[np.arange(N), np.arange(N)]\nrows = []\nfor seed in SEEDS:\n    te_ids, tr_pool = split_by_id(ids3, seed)\n    te = np.where(np.isin(ids3, te_ids))[0]\n    for size in (50, 100, 200, 400, 800):\n        tr = np.where(np.isin(ids3, tr_pool[:size]))[0]\n        for name, p in fit_predict(X3, y3, tr, te, use_all=False).items():\n            rows.append(dict(train_pictures=size, model=name, seed=seed, accuracy=(p == y3[te]).mean()))\n    print(\"Experiment 3: seed\", seed, \"done\", round(time.time() - t0), \"s\")\ne3 = pd.DataFrame(rows); e3.to_csv(\"/kaggle/working/exp3_learning_curve.csv\", index=False)\n\nprint(\"\\n\\n===== COPY EVERYTHING BELOW THIS LINE AND SEND IT TO ME =====\")\nprint(\"\\nTABLE A: detection, payload 0.5 (mean, std in %)\")\nprint(mean_std(e1[e1.payload == 0.5], \"model\", cols).to_string())\nprint(\"\\nTABLE B: detection accuracy for each payload (mean, std in %)\")\nprint(mean_std(e1, [\"payload\", \"model\"], [\"accuracy\"]).to_string())\nprint(\"\\nTABLE C: payload estimation, 5 classes (accuracy in %, MAE in payload units)\")\nt = e2.groupby(\"model\")[[\"accuracy\", \"mae\"]].agg([\"mean\", \"std\"]); t[\"accuracy\"] *= 100\nprint(t.round(3).to_string())\nprint(\"\\nTABLE D: learning curve, accuracy in % (mean, std)\")\nprint(mean_std(e3, [\"train_pictures\", \"model\"], [\"accuracy\"]).to_string())\n\nimport matplotlib; matplotlib.use(\"Agg\"); import matplotlib.pyplot as plt\na = e1[e1.payload == 0.5].groupby(\"model\")[cols]\nm, e = a.mean() * 100, a.std() * 100\nax = m.T.plot(kind=\"bar\", yerr=e.T, capsize=3, figsize=(8, 4)); ax.set_ylabel(\"Percent (%)\")\nax.set_title(\"Detection at payload 0.5 (mean ± std, 3 runs)\"); plt.xticks(rotation=0); plt.tight_layout()\nplt.savefig(\"/kaggle/working/figure5.png\", dpi=300); plt.close()\n\nplt.figure(figsize=(6, 4))\nfor name, g in e1.groupby(\"model\"):\n    s = g.groupby(\"payload\")[\"accuracy\"].agg([\"mean\", \"std\"]) * 100\n    plt.errorbar(s.index, s[\"mean\"], yerr=s[\"std\"], marker=\"o\", capsize=3, label=name)\nplt.axhline(50, color=\"gray\", ls=\"--\", lw=1); plt.xlabel(\"Payload (fraction of pixels used)\"); plt.ylabel(\"Accuracy (%)\")\nplt.legend(fontsize=8); plt.tight_layout(); plt.savefig(\"/kaggle/working/figure7_payload_sweep.png\", dpi=300); plt.close()\n\ncm = conf[M_QSVM] / conf[M_QSVM].sum(axis=1, keepdims=True)\nplt.figure(figsize=(5, 4.3)); plt.imshow(cm, cmap=\"Blues\", vmin=0, vmax=1)\nlab = [\"0 (clean)\"] + [str(p) for p in PAY[1:]]\nplt.xticks(range(K), lab, rotation=30); plt.yticks(range(K), lab)\nfor i in range(K):\n    for j in range(K): plt.text(j, i, f\"{cm[i, j]:.2f}\", ha=\"center\", va=\"center\", fontsize=8)\nplt.xlabel(\"Predicted payload\"); plt.ylabel(\"True payload\"); plt.title(\"QSVM payload estimation\"); plt.tight_layout()\nplt.savefig(\"/kaggle/working/figure8_payload_confusion.png\", dpi=300); plt.close()\n\nplt.figure(figsize=(6, 4))\nfor name, g in e3.groupby(\"model\"):\n    s = g.groupby(\"train_pictures\")[\"accuracy\"].agg([\"mean\", \"std\"]) * 100\n    plt.errorbar(s.index, s[\"mean\"], yerr=s[\"std\"], marker=\"o\", capsize=3, label=name)\nplt.xscale(\"log\"); plt.xlabel(\"Number of training pictures\"); plt.ylabel(\"Accuracy (%)\"); plt.legend(fontsize=8)\nplt.tight_layout(); plt.savefig(\"/kaggle/working/figure9_learning_curve.png\", dpi=300); plt.close()\nprint(\"\\nSaved figures: figure5, figure7, figure8, figure9. Total time\", round(time.time() - t0), \"s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T02:28:57.690628Z","iopub.execute_input":"2026-10-17T02:28:57.691037Z","iopub.status.idle":"2026-10-17T02:30:13.503371Z","shell.execute_reply.started":"2026-10-17T02:28:57.691012Z","shell.execute_reply":"2026-10-17T02:30:13.502586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, numpy as np, pywt\nfrom PIL import Image\nfrom scipy.ndimage import uniform_filter\n\ndef find_root():\n    for r, dirs, _ in os.walk(\"/kaggle/input\"):\n        if \"Cover\" in dirs and \"UERD\" in dirs:\n            return r\nROOT = find_root()\nPAYLOADS = [0.1, 0.25, 0.5, 1.0]\nfiles = sorted(os.listdir(f\"{ROOT}/Cover\"))[:1000]\nrng = np.random.RandomState(0)          # SAME seed as before, so the stego images are exactly the same\n\ndef load_gray(path):\n    return np.asarray(Image.open(path).convert(\"L\"), dtype=np.uint8)\n\ndef hide_bits(img, payload):\n    out = img.copy()\n    mask = rng.rand(*img.shape) < payload\n    secret = rng.randint(0, 2, img.shape).astype(np.uint8)\n    out[mask] = (out[mask] & 0xFE) | secret[mask]\n    return out\n\ndef ws_estimate(img, neighbours=4):\n    \"\"\"Weighted-stego (WS) estimate of the LSB-replacement payload (bits per pixel).\"\"\"\n    x = img.astype(np.float64)\n    flipped = (img ^ 1).astype(np.float64)\n    p = np.pad(x, 1, mode=\"edge\")\n    if neighbours == 4:\n        F = (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:]) / 4\n    else:\n        F = (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:] +\n             p[:-2, :-2] + p[:-2, 2:] + p[2:, :-2] + p[2:, 2:]) / 8\n    var = uniform_filter(x * x, 3) - uniform_filter(x, 3) ** 2\n    w = 1.0 / (5.0 + var)\n    change_rate = np.sum(w * (x - flipped) * (x - F)) / np.sum(w)\n    return 2.0 * change_rate\n\ndef pairs_statistic(img):\n    h = np.bincount(img.ravel(), minlength=256).astype(np.float64)\n    even, odd = h[0::2], h[1::2]\n    return np.sum((even - odd) ** 2) / (np.sum(even + odd) + 1e-9)\n\ndef spam_features(img, T=2):\n    \"\"\"SPAM-style second-order Markov features of pixel differences (2 x 125 = 250 values).\"\"\"\n    x = img.astype(np.int16)\n    B = 2 * T + 1\n    def markov(a, b, c):\n        a, b, c = [np.clip(v, -T, T) + T for v in (a, b, c)]\n        idx = (a * B + b) * B + c\n        h = np.bincount(idx.ravel(), minlength=B ** 3).astype(np.float64)\n        return h / h.sum()\n    Dh = x[:, :-1] - x[:, 1:]\n    Dv = x[:-1, :] - x[1:, :]\n    Dd = x[:-1, :-1] - x[1:, 1:]\n    Da = x[:-1, 1:] - x[1:, :-1]\n    h = markov(Dh[:, :-2], Dh[:, 1:-1], Dh[:, 2:])\n    v = markov(Dv[:-2, :], Dv[1:-1, :], Dv[2:, :])\n    d = markov(Dd[:-2, :-2], Dd[1:-1, 1:-1], Dd[2:, 2:])\n    a = markov(Da[:-2, 2:], Da[1:-1, 1:-1], Da[2:, :-2])\n    return np.r_[(h + v) / 2, (d + a) / 2]\n\ndef extra_features(img):\n    return np.r_[ws_estimate(img, 4), ws_estimate(img, 8), pairs_statistic(img), spam_features(img)]\n\nE = []\nfor k, f in enumerate(files):\n    c = load_gray(f\"{ROOT}/Cover/{f}\")\n    imgs = [c] + [hide_bits(c, p) for p in PAYLOADS]\n    E.append([extra_features(im) for im in imgs])\n    if k % 100 == 0: print(k, \"/\", len(files))\nE = np.nan_to_num(np.array(E))\nnp.savez(\"/kaggle/working/features_extra.npz\", E=E)\nprint(\"Saved extra features:\", E.shape)          # expected (1000, 5, 253)\n\n# safety check: are these exactly the same pictures as in the first run?\nold = np.load(\"/kaggle/working/features_suite.npz\")[\"F\"]\n_, (LH, HL, HH) = pywt.dwt2(imgs[3].astype(np.float32), \"bior1.3\")\nprint(\"Same pictures as before:\", abs(np.abs(LH).mean() - old[-1, 3, 0]) < 1e-3)\nprint(\"WS estimates of last picture (clean, 0.1, 0.25, 0.5, 1.0):\", np.round(E[-1, :, 0], 3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T12:28:37.195584Z","iopub.execute_input":"2026-10-17T12:28:37.196019Z","iopub.status.idle":"2026-10-17T12:31:53.147049Z","shell.execute_reply.started":"2026-10-17T12:28:37.195994Z","shell.execute_reply":"2026-10-17T12:31:53.146313Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import itertools, time, numpy as np, pandas as pd\nfrom sklearn.feature_selection import SelectKBest, f_classif\nfrom sklearn.preprocessing import MinMaxScaler, StandardScaler\nfrom sklearn.svm import SVC\nfrom sklearn.metrics import confusion_matrix\nfrom sklearn.metrics.pairwise import euclidean_distances\n\nSEEDS = [42, 43, 44]\nM_WS, M_SPAM = \"WS detector (classic steganalysis)\", \"SPAM + SVM (classic steganalysis)\"\nM_CL, M_Q = \"Classical SVM (6 features)\", \"QSVM (6 qubits)\"\n\n# ---------- quantum kernel (exact simulation of a ZZ feature map) ----------\ndef zz_states(X, reps=2):\n    n = X.shape[1]; dim = 2 ** n\n    bits = (np.arange(dim)[:, None] >> np.arange(n)[::-1]) & 1\n    H1 = np.array([[1, 1], [1, -1]]) / np.sqrt(2)\n    Hn = np.array([[1.0]])\n    for _ in range(n):\n        Hn = np.kron(Hn, H1)\n    psi = np.zeros((len(X), dim), dtype=complex); psi[:, 0] = 1\n    for _ in range(reps):\n        psi = psi @ Hn.T\n        phase = 2 * X @ bits.T\n        for i, j in itertools.combinations(range(n), 2):\n            w = 2 * (np.pi - X[:, i]) * (np.pi - X[:, j])\n            phase = phase + w[:, None] * (bits[:, i] ^ bits[:, j])[None, :]\n        psi = psi * np.exp(1j * phase)\n    return psi\n\ndef quantum_kernel(Z):\n    S = zz_states(Z)\n    return np.abs(S @ S.conj().T) ** 2\n\ndef rbf_kernel_matrix(Z, tr):\n    d2 = euclidean_distances(Z, Z, squared=True)\n    return np.exp(-d2 / (Z.shape[1] * Z[tr].var() + 1e-12))\n\ndef best_C(K, y, tr, Cs=(0.1, 1, 10, 100)):\n    tr = np.array(tr); np.random.RandomState(0).shuffle(tr)\n    a, b = tr[:int(.8 * len(tr))], tr[int(.8 * len(tr)):]\n    acc = {C: (SVC(C=C, kernel='precomputed').fit(K[np.ix_(a, a)], y[a])\n               .predict(K[np.ix_(b, a)]) == y[b]).mean() for C in Cs}\n    C = max(acc, key=acc.get); return C, acc[C]\n\ndef svm_on(K, y, tr, te, Cs=(0.1, 1, 10, 100)):\n    C, _ = best_C(K, y, tr, Cs)\n    return SVC(C=C, kernel='precomputed').fit(K[np.ix_(tr, tr)], y[tr]).predict(K[np.ix_(te, tr)])\n\ndef six_feature_models(X, y, tr, te, k=6, q=True):\n    \"\"\"Pick the k best features on the training set; classical RBF SVM and QSVM on them.\"\"\"\n    out = {}\n    sc = StandardScaler().fit(X[tr]); Xs = sc.transform(X)\n    k = min(k, X.shape[1])\n    Z0 = SelectKBest(f_classif, k=k).fit(Xs[tr], y[tr]).transform(Xs)\n    Z0 = np.clip(MinMaxScaler((0, 1)).fit(Z0[tr]).transform(Z0), 0, 1)\n    out[\"classical\"] = svm_on(rbf_kernel_matrix(Z0 * np.pi, tr), y, tr, te)\n    if q:\n        best = None\n        for s in (0.25, 0.5, 1.0):\n            Kq = quantum_kernel(Z0 * np.pi * s); C, v = best_C(Kq, y, tr)\n            if best is None or v > best[0]: best = (v, Kq, C)\n        _, Kq, C = best\n        out[\"qsvm\"] = SVC(C=C, kernel='precomputed').fit(Kq[np.ix_(tr, tr)], y[tr]).predict(Kq[np.ix_(te, tr)])\n    return out\n\ndef spam_svm(Xspam, y, tr, te):\n    sc = StandardScaler().fit(Xspam[tr]); Xs = sc.transform(Xspam)\n    return svm_on(rbf_kernel_matrix(Xs, tr), y, tr, te, Cs=(1, 10, 100))\n\ndef ws_threshold(ws, y, tr, te):\n    cand = np.quantile(ws[tr], np.linspace(0.01, 0.99, 197))\n    accs = [((ws[tr] > t).astype(int) == y[tr]).mean() for t in cand]\n    return (ws[te] > cand[int(np.argmax(accs))]).astype(int)\n\ndef split_by_id(ids, seed, frac=0.2):\n    u = np.unique(ids); r = np.random.RandomState(seed).permutation(u)\n    n_te = int(frac * len(u)); return r[:n_te], r[n_te:]\n\ndef binary_scores(y, p):\n    tn, fp, fn, tp = confusion_matrix(y, p, labels=[0, 1]).ravel()\n    return dict(accuracy=(tp + tn) / len(y), sensitivity=tp / (tp + fn),\n                specificity=tn / (tn + fp), precision=tp / (tp + fp) if tp + fp else 0.0)\n\ndef mean_std(df, by, cols):\n    return (df.groupby(by)[cols].agg([\"mean\", \"std\"]) * 100).round(2)\n\nd1 = np.load(\"/kaggle/working/features_suite.npz\"); d2 = np.load(\"/kaggle/working/features_extra.npz\")\nF, E, PAY = d1[\"F\"], d2[\"E\"], d1[\"payloads\"]\nA = np.concatenate([F, E], axis=2)                      # all features\nN, K, D = A.shape\nc_dw, c_ws, c_sp = slice(0, F.shape[2]), slice(F.shape[2], F.shape[2] + 3), slice(F.shape[2] + 3, D)\nws_col = F.shape[2]                                     # WS estimate (4 neighbours)\ncols = [\"accuracy\", \"sensitivity\", \"specificity\", \"precision\"]\nt0 = time.time()\n\ndef bin_data(k):\n    X = np.vstack([A[:, 0, :], A[:, k, :]]); y = np.r_[np.zeros(N), np.ones(N)].astype(int)\n    ids = np.r_[np.arange(N), np.arange(N)]; return X, y, ids\n\n# ----- Experiment 1: detection for every payload, 4 methods -----\nrows = []\nfor k in range(1, K):\n    X, y, ids = bin_data(k)\n    for seed in SEEDS:\n        te_ids, tr_ids = split_by_id(ids, seed)\n        tr, te = np.where(np.isin(ids, tr_ids))[0], np.where(np.isin(ids, te_ids))[0]\n        preds = {M_WS: ws_threshold(X[:, ws_col], y, tr, te), M_SPAM: spam_svm(X[:, c_sp], y, tr, te)}\n        m = six_feature_models(X, y, tr, te); preds[M_CL], preds[M_Q] = m[\"classical\"], m[\"qsvm\"]\n        for name, p in preds.items():\n            rows.append(dict(payload=PAY[k], model=name, seed=seed, **binary_scores(y[te], p)))\n    print(\"Exp 1: payload\", PAY[k], \"done\", round(time.time() - t0), \"s\")\ne1 = pd.DataFrame(rows); e1.to_csv(\"/kaggle/working/v2_exp1_detection.csv\", index=False)\n\n# ----- Experiment 2: which feature group matters (QSVM and classical, payloads 0.25 and 0.5) -----\ngroups = {\"DWT + AlexNet\": c_dw, \"WS + pairs statistic\": c_ws, \"SPAM\": c_sp, \"All features\": slice(0, D)}\nrows = []\nfor pay in (0.25, 0.5):\n    k = int(np.where(PAY == pay)[0][0]); X, y, ids = bin_data(k)\n    for seed in SEEDS:\n        te_ids, tr_ids = split_by_id(ids, seed)\n        tr, te = np.where(np.isin(ids, tr_ids))[0], np.where(np.isin(ids, te_ids))[0]\n        for g, sl in groups.items():\n            m = six_feature_models(X[:, sl], y, tr, te)\n            for key, name in ((\"classical\", M_CL), (\"qsvm\", M_Q)):\n                rows.append(dict(payload=pay, group=g, model=name, seed=seed, accuracy=(m[key] == y[te]).mean()))\n    print(\"Exp 2: payload\", pay, \"done\", round(time.time() - t0), \"s\")\ne2 = pd.DataFrame(rows); e2.to_csv(\"/kaggle/working/v2_exp2_feature_groups.csv\", index=False)\n\n# ----- Experiment 3: payload estimation (5 classes) -----\nX3 = A.reshape(N * K, D); y3 = np.tile(np.arange(K), N); ids3 = np.repeat(np.arange(N), K)\nrows = []\nM_WS_EST = \"WS estimator (calibrated)\"\nconf = {m: np.zeros((K, K)) for m in (M_WS_EST, M_SPAM, M_CL, M_Q)}\nfor seed in SEEDS:\n    te_ids, tr_ids = split_by_id(ids3, seed)\n    tr, te = np.where(np.isin(ids3, tr_ids))[0], np.where(np.isin(ids3, te_ids))[0]\n    centers = np.array([X3[tr][y3[tr] == c, ws_col].mean() for c in range(K)])\n    preds = {M_WS_EST: np.argmin(np.abs(X3[te, ws_col][:, None] - centers[None, :]), axis=1),\n             M_SPAM: spam_svm(X3[:, c_sp], y3, tr, te)}\n    m = six_feature_models(X3, y3, tr, te); preds[M_CL], preds[M_Q] = m[\"classical\"], m[\"qsvm\"]\n    for name, p in preds.items():\n        rows.append(dict(model=name, seed=seed, accuracy=(p == y3[te]).mean(), mae=np.abs(PAY[p] - PAY[y3[te]]).mean()))\n        conf[name] += confusion_matrix(y3[te], p, labels=range(K))\n    print(\"Exp 3: seed\", seed, \"done\", round(time.time() - t0), \"s\")\ne3 = pd.DataFrame(rows); e3.to_csv(\"/kaggle/working/v2_exp3_payload_estimation.csv\", index=False)\n\n# ----- Experiment 4: small training sets (payload 0.25) -----\nk = int(np.where(PAY == 0.25)[0][0]); X, y, ids = bin_data(k)\nrows = []\nfor seed in SEEDS:\n    te_ids, pool = split_by_id(ids, seed)\n    te = np.where(np.isin(ids, te_ids))[0]\n    for size in (50, 100, 200, 400, 800):\n        tr = np.where(np.isin(ids, pool[:size]))[0]\n        preds = {M_WS: ws_threshold(X[:, ws_col], y, tr, te)}\n        m = six_feature_models(X, y, tr, te); preds[M_CL], preds[M_Q] = m[\"classical\"], m[\"qsvm\"]\n        for name, p in preds.items():\n            rows.append(dict(train_pictures=size, model=name, seed=seed, accuracy=(p == y[te]).mean()))\n    print(\"Exp 4: seed\", seed, \"done\", round(time.time() - t0), \"s\")\ne4 = pd.DataFrame(rows); e4.to_csv(\"/kaggle/working/v2_exp4_learning_curve.csv\", index=False)\n\n# ----- Experiment 5: number of qubits (payload 0.25) -----\nrows = []\nfor seed in SEEDS:\n    te_ids, tr_ids = split_by_id(ids, seed)\n    tr, te = np.where(np.isin(ids, tr_ids))[0], np.where(np.isin(ids, te_ids))[0]\n    for nq in (2, 4, 6, 8):\n        m = six_feature_models(X, y, tr, te, k=nq)\n        for key, name in ((\"classical\", \"Classical SVM\"), (\"qsvm\", \"QSVM\")):\n            rows.append(dict(features_or_qubits=nq, model=name, seed=seed, accuracy=(m[key] == y[te]).mean()))\n    print(\"Exp 5: seed\", seed, \"done\", round(time.time() - t0), \"s\")\ne5 = pd.DataFrame(rows); e5.to_csv(\"/kaggle/working/v2_exp5_qubits.csv\", index=False)\n\n# ----- print tables -----\nprint(\"\\n\\n===== COPY EVERYTHING BELOW THIS LINE AND SEND IT TO ME =====\")\nprint(\"\\nTABLE A: detection at payload 0.5 (mean, std in %)\")\nprint(mean_std(e1[e1.payload == 0.5], \"model\", cols).to_string())\nprint(\"\\nTABLE B: detection accuracy for each payload (mean, std in %)\")\nprint(mean_std(e1, [\"payload\", \"model\"], [\"accuracy\"]).to_string())\nprint(\"\\nTABLE C: feature groups, accuracy % (mean, std)\")\nprint(mean_std(e2, [\"payload\", \"group\", \"model\"], [\"accuracy\"]).to_string())\nprint(\"\\nTABLE D: payload estimation, 5 classes (accuracy %, MAE in payload units)\")\nt = e3.groupby(\"model\")[[\"accuracy\", \"mae\"]].agg([\"mean\", \"std\"]); t[\"accuracy\"] *= 100\nprint(t.round(3).to_string())\nprint(\"\\nTABLE E: small training sets, payload 0.25, accuracy % (mean, std)\")\nprint(mean_std(e4, [\"train_pictures\", \"model\"], [\"accuracy\"]).to_string())\nprint(\"\\nTABLE F: number of qubits (= selected features), payload 0.25, accuracy % (mean, std)\")\nprint(mean_std(e5, [\"features_or_qubits\", \"model\"], [\"accuracy\"]).to_string())\n\n# ----- figures -----\nimport matplotlib; matplotlib.use(\"Agg\"); import matplotlib.pyplot as plt\na = e1[e1.payload == 0.5].groupby(\"model\")[cols]\nm_, e_ = a.mean() * 100, a.std() * 100\nax = m_.T.plot(kind=\"bar\", yerr=e_.T, capsize=3, figsize=(9, 4.2)); ax.set_ylabel(\"Percent (%)\")\nax.set_title(\"Detection at payload 0.5 (mean ± std, 3 runs)\"); plt.xticks(rotation=0); plt.legend(fontsize=7); plt.tight_layout()\nplt.savefig(\"/kaggle/working/v2_figure_A_payload05.png\", dpi=300); plt.close()\n\nplt.figure(figsize=(6.5, 4))\nfor name, g in e1.groupby(\"model\"):\n    s = g.groupby(\"payload\")[\"accuracy\"].agg([\"mean\", \"std\"]) * 100\n    plt.errorbar(s.index, s[\"mean\"], yerr=s[\"std\"], marker=\"o\", capsize=3, label=name)\nplt.axhline(50, color=\"gray\", ls=\"--\", lw=1); plt.xlabel(\"Payload\"); plt.ylabel(\"Accuracy (%)\"); plt.legend(fontsize=7)\nplt.tight_layout(); plt.savefig(\"/kaggle/working/v2_figure_B_payload_sweep.png\", dpi=300); plt.close()\n\nplt.figure(figsize=(6.5, 4))\ng2 = e2[e2.payload == 0.25].groupby([\"group\", \"model\"])[\"accuracy\"].mean().unstack() * 100\ng2.plot(kind=\"bar\", ax=plt.gca()); plt.ylabel(\"Accuracy (%)\"); plt.title(\"Feature groups, payload 0.25\"); plt.xticks(rotation=15)\nplt.legend(fontsize=7); plt.tight_layout(); plt.savefig(\"/kaggle/working/v2_figure_C_feature_groups.png\", dpi=300); plt.close()\n\ncm = conf[M_Q] / conf[M_Q].sum(axis=1, keepdims=True)\nplt.figure(figsize=(5, 4.3)); plt.imshow(cm, cmap=\"Blues\", vmin=0, vmax=1)\nlab = [\"0 (clean)\"] + [str(p) for p in PAY[1:]]\nplt.xticks(range(K), lab, rotation=30); plt.yticks(range(K), lab)\nfor i in range(K):\n    for j in range(K): plt.text(j, i, f\"{cm[i, j]:.2f}\", ha=\"center\", va=\"center\", fontsize=8)\nplt.xlabel(\"Predicted payload\"); plt.ylabel(\"True payload\"); plt.title(\"QSVM payload estimation\"); plt.tight_layout()\nplt.savefig(\"/kaggle/working/v2_figure_D_payload_confusion.png\", dpi=300); plt.close()\n\nplt.figure(figsize=(6.5, 4))\nfor name, g in e4.groupby(\"model\"):\n    s = g.groupby(\"train_pictures\")[\"accuracy\"].agg([\"mean\", \"std\"]) * 100\n    plt.errorbar(s.index, s[\"mean\"], yerr=s[\"std\"], marker=\"o\", capsize=3, label=name)\nplt.xscale(\"log\"); plt.xlabel(\"Training pictures\"); plt.ylabel(\"Accuracy (%)\"); plt.legend(fontsize=7)\nplt.tight_layout(); plt.savefig(\"/kaggle/working/v2_figure_E_learning_curve.png\", dpi=300); plt.close()\n\nplt.figure(figsize=(6, 4))\nfor name, g in e5.groupby(\"model\"):\n    s = g.groupby(\"features_or_qubits\")[\"accuracy\"].agg([\"mean\", \"std\"]) * 100\n    plt.errorbar(s.index, s[\"mean\"], yerr=s[\"std\"], marker=\"o\", capsize=3, label=name)\nplt.xlabel(\"Number of qubits (selected features)\"); plt.ylabel(\"Accuracy (%)\"); plt.legend(fontsize=8)\nplt.tight_layout(); plt.savefig(\"/kaggle/working/v2_figure_F_qubits.png\", dpi=300); plt.close()\nprint(\"\\nSaved 6 figures (v2_figure_A ... F). Total time\", round(time.time() - t0), \"s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T12:35:42.818825Z","iopub.execute_input":"2026-10-17T12:35:42.819556Z","iopub.status.idle":"2026-10-17T12:37:38.298873Z","shell.execute_reply.started":"2026-10-17T12:35:42.819525Z","shell.execute_reply":"2026-10-17T12:37:38.298124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, shutil, numpy as np, pywt, torch\nfrom PIL import Image\nfrom scipy.ndimage import uniform_filter\nfrom scipy.stats import kurtosis, skew\nfrom torchvision import models\n\nW = \"/kaggle/working\"\nBACKUP = f\"{W}/lsb_replacement_results\"\nif os.path.exists(f\"{BACKUP}/features_suite.npz\"):\n    raise SystemExit(\"This cell was already run. Do NOT run it again. Run Cell 5 now.\")\n\ndef find_root():\n    for r, dirs, _ in os.walk(\"/kaggle/input\"):\n        if \"Cover\" in dirs and \"UERD\" in dirs:\n            return r\nROOT = find_root()\nPAYLOADS_M = [0.25, 0.5, 1.0]\nfiles = sorted(os.listdir(f\"{ROOT}/Cover\"))[:1000]\nrng = np.random.RandomState(1)\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nalex = models.alexnet(weights=models.AlexNet_Weights.IMAGENET1K_V1).features.to(device).eval()\n\ndef load_gray(path):\n    return np.asarray(Image.open(path).convert(\"L\"), dtype=np.uint8)\n\ndef lsb_matching(img, payload):\n    \"\"\"LSB matching (+-1 embedding): if the last bit is wrong, add or subtract 1 at random.\"\"\"\n    out = img.astype(np.int16)\n    mask = rng.rand(*img.shape) < payload\n    secret = rng.randint(0, 2, img.shape)\n    diff = mask & ((out & 1) != secret)\n    step = np.where(rng.rand(*img.shape) < 0.5, 1, -1)\n    step = np.where(out == 0, 1, np.where(out == 255, -1, step))\n    return (out + np.where(diff, step, 0)).astype(np.uint8)\n\ndef get_features(img):\n    _, (LH, HL, HH) = pywt.dwt2(img.astype(np.float32), \"bior1.3\")\n    stats = []\n    for b in (LH, HL, HH):\n        a = np.abs(b).ravel()\n        stats += [a.mean(), b.std(), kurtosis(b.ravel()), skew(b.ravel())] + list(np.percentile(a, [5, 10, 25, 50, 75]))\n    return stats, torch.tensor(np.stack([LH, HL, HH]) / 10.0, dtype=torch.float32)\n\ndef featurize(imgs):\n    stats, chans = [], []\n    for im in imgs:\n        s, c = get_features(im); stats.append(s); chans.append(c)\n    x = torch.stack(chans).to(device)\n    with torch.no_grad():\n        a = alex(x).mean(dim=(2, 3)).cpu().numpy()\n    return np.hstack([np.array(stats), a])\n\ndef ws_estimate(img, neighbours=4):\n    x = img.astype(np.float64)\n    flipped = (img ^ 1).astype(np.float64)\n    p = np.pad(x, 1, mode=\"edge\")\n    if neighbours == 4:\n        F = (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:]) / 4\n    else:\n        F = (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:] +\n             p[:-2, :-2] + p[:-2, 2:] + p[2:, :-2] + p[2:, 2:]) / 8\n    var = uniform_filter(x * x, 3) - uniform_filter(x, 3) ** 2\n    w = 1.0 / (5.0 + var)\n    return 2.0 * np.sum(w * (x - flipped) * (x - F)) / np.sum(w)\n\ndef pairs_statistic(img):\n    h = np.bincount(img.ravel(), minlength=256).astype(np.float64)\n    even, odd = h[0::2], h[1::2]\n    return np.sum((even - odd) ** 2) / (np.sum(even + odd) + 1e-9)\n\ndef spam_features(img, T=2):\n    x = img.astype(np.int16)\n    B = 2 * T + 1\n    def markov(a, b, c):\n        a, b, c = [np.clip(v, -T, T) + T for v in (a, b, c)]\n        idx = (a * B + b) * B + c\n        h = np.bincount(idx.ravel(), minlength=B ** 3).astype(np.float64)\n        return h / h.sum()\n    Dh = x[:, :-1] - x[:, 1:]\n    Dv = x[:-1, :] - x[1:, :]\n    Dd = x[:-1, :-1] - x[1:, 1:]\n    Da = x[:-1, 1:] - x[1:, :-1]\n    h = markov(Dh[:, :-2], Dh[:, 1:-1], Dh[:, 2:])\n    v = markov(Dv[:-2, :], Dv[1:-1, :], Dv[2:, :])\n    d = markov(Dd[:-2, :-2], Dd[1:-1, 1:-1], Dd[2:, 2:])\n    a = markov(Da[:-2, 2:], Da[1:-1, 1:-1], Da[2:, :-2])\n    return np.r_[(h + v) / 2, (d + a) / 2]\n\ndef extra_features(img):\n    return np.r_[ws_estimate(img, 4), ws_estimate(img, 8), pairs_statistic(img), spam_features(img)]\n\nold_F = np.load(f\"{W}/features_suite.npz\")[\"F\"]\nold_E = np.load(f\"{W}/features_extra.npz\")[\"E\"]\n\nF_new, E_new = [], []\nfor start in range(0, len(files), 8):\n    imgs = []\n    for f in files[start:start + 8]:\n        c = load_gray(f\"{ROOT}/Cover/{f}\")\n        imgs += [lsb_matching(c, p) for p in PAYLOADS_M]\n    f_block = featurize(imgs)\n    e_block = np.array([extra_features(im) for im in imgs])\n    F_new.append(f_block.reshape(-1, len(PAYLOADS_M), f_block.shape[1]))\n    E_new.append(e_block.reshape(-1, len(PAYLOADS_M), e_block.shape[1]))\n    if (start // 8) % 10 == 0: print(start, \"/\", len(files))\n\nF_m = np.concatenate([old_F[:, :1, :], np.concatenate(F_new)], axis=1)     # clean covers + 3 stego versions\nE_m = np.concatenate([old_E[:, :1, :], np.concatenate(E_new)], axis=1)\nF_m, E_m = np.nan_to_num(F_m), np.nan_to_num(E_m)\n\n# keep a copy of the LSB-replacement results, then put the LSB-matching features in place\nos.makedirs(BACKUP, exist_ok=True)\nfor fn in os.listdir(W):\n    if fn.startswith(\"v2_\") or fn in (\"features_suite.npz\", \"features_extra.npz\"):\n        shutil.copy(f\"{W}/{fn}\", f\"{BACKUP}/{fn}\")\nnp.savez(f\"{W}/features_suite.npz\", F=F_m, payloads=np.array([0.0] + PAYLOADS_M))\nnp.savez(f\"{W}/features_extra.npz\", E=E_m)\nprint(\"Done. LSB-matching features:\", F_m.shape, E_m.shape)\nprint(\"Old results are saved in the folder lsb_replacement_results.\")\nprint(\"NOW RUN CELL 5 AGAIN (do not change it).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T12:32:12.519729Z","iopub.execute_input":"2026-10-17T12:32:12.52024Z","iopub.status.idle":"2026-10-17T12:35:15.065608Z","shell.execute_reply.started":"2026-10-17T12:32:12.520201Z","shell.execute_reply":"2026-10-17T12:35:15.064768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.decomposition import PCA\nfrom sklearn.cross_decomposition import PLSRegression\n\nd1 = np.load(\"/kaggle/working/features_suite.npz\"); d2 = np.load(\"/kaggle/working/features_extra.npz\")\nF, E, PAY = d1[\"F\"], d2[\"E\"], d1[\"payloads\"]\nA = np.concatenate([F, E], axis=2)\nN, K, D = A.shape\nc_sp = slice(F.shape[2] + 3, D)                       # the 250 SPAM-style features\nt0 = time.time()\n\ndef compress(Xs, y, tr, method, k):\n    if method == \"Select-k (current)\":\n        return SelectKBest(f_classif, k=k).fit(Xs[tr], y[tr]).transform(Xs)\n    if method == \"PCA\":\n        return PCA(k, random_state=0).fit(Xs[tr]).transform(Xs)\n    return PLSRegression(k).fit(Xs[tr], y[tr]).transform(Xs)          # PLS (uses the labels)\n\ndef classical_and_qsvm(Z, y, tr, te):\n    out = {\"Classical SVM\": svm_on(rbf_kernel_matrix(Z * np.pi, tr), y, tr, te)}\n    best = None\n    for s in (0.25, 0.5, 1.0):\n        Kq = quantum_kernel(Z * np.pi * s); C, v = best_C(Kq, y, tr)\n        if best is None or v > best[0]: best = (v, Kq, C)\n    _, Kq, C = best\n    out[\"QSVM\"] = SVC(C=C, kernel='precomputed').fit(Kq[np.ix_(tr, tr)], y[tr]).predict(Kq[np.ix_(te, tr)])\n    return out\n\nrows = []\nfor k_pay in range(1, K):\n    X = np.vstack([A[:, 0, c_sp], A[:, k_pay, c_sp]]); y = np.r_[np.zeros(N), np.ones(N)].astype(int)\n    ids = np.r_[np.arange(N), np.arange(N)]\n    for seed in SEEDS:\n        te_ids, tr_ids = split_by_id(ids, seed)\n        tr, te = np.where(np.isin(ids, tr_ids))[0], np.where(np.isin(ids, te_ids))[0]\n        Xs = StandardScaler().fit(X[tr]).transform(X)\n        p = svm_on(rbf_kernel_matrix(Xs, tr), y, tr, te, Cs=(1, 10, 100))\n        rows.append(dict(payload=PAY[k_pay], method=\"All 250 features (no compression)\", k=250, model=\"Classical SVM\", seed=seed, accuracy=(p == y[te]).mean()))\n        for method in (\"Select-k (current)\", \"PCA\", \"PLS\"):\n            for k in (4, 6, 8):\n                Z = compress(Xs, y, tr, method, k)\n                Z = np.clip(MinMaxScaler((0, 1)).fit(Z[tr]).transform(Z), 0, 1)\n                for name, pred in classical_and_qsvm(Z, y, tr, te).items():\n                    rows.append(dict(payload=PAY[k_pay], method=method, k=k, model=name, seed=seed, accuracy=(pred == y[te]).mean()))\n    print(\"payload\", PAY[k_pay], \"done\", round(time.time() - t0), \"s\")\n\ne7 = pd.DataFrame(rows); e7.to_csv(\"/kaggle/working/v3_compression.csv\", index=False)\nprint(\"\\n\\n===== COPY EVERYTHING BELOW THIS LINE AND SEND IT TO ME =====\")\nprint(\"\\nTABLE G: accuracy (%) with different ways of squeezing the 250 SPAM-style features into k qubits\")\nprint(mean_std(e7, [\"payload\", \"method\", \"k\", \"model\"], [\"accuracy\"]).to_string())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-17T12:38:02.790428Z","iopub.execute_input":"2026-10-17T12:38:02.791261Z","iopub.status.idle":"2026-10-17T12:40:37.231017Z","shell.execute_reply.started":"2026-10-17T12:38:02.791233Z","shell.execute_reply":"2026-10-17T12:40:37.230104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}