Lattice_Spike / docs /new_baselines_implementation_spec.md
Md Saiful Islam Sajol
Upload Lattice_Spike project with full LFS weights
e2c9d42 verified
|
Raw History Blame Contribute Delete
15.2 kB

Implementation spec: SAPLMA, HaMI, LLM-Check, ICR Probe

All four are implemented. Every architecture detail below was checked against the source paper or the authors' released code, not recalled. Fidelity argumentation lives in docs/baseline_fidelity_audit.md (Β§4 SAPLMA, Β§5 HaMI); this file is the build record and the remaining work.

Decisions already made (do not relitigate)

  1. Pipeline: all three go into the baseline runner (scripts/train_faithful_baselines_dgx.py) as new --detector values, under the same suite/protocol contract as EPR/Lookback/MVA. NOT scored ad-hoc from the LatticeSpike signal cache. Protocol consistency is what makes the fidelity argument defensible, and the same hidden-state extraction pass also unblocks WEPR's missing causal number.
  2. HaMI keeps sequence-level supervision. Learning token scores from bag-level labels IS its contribution. This belongs in the "preserved" column of the adaptation table, not "adapted".
  3. What we equalise is the evaluation β€” same token set, same causal horizon, same metrics β€” never the methods themselves.
  4. The default detector set is frozen. STRICT_DETECTORS still resolves to EPR/WEPR/Lookback/MVA-CRF, so a stored command that reproduced a published run keeps reproducing it. Both new methods are opt-in via --detector.

1. SAPLMA β€” implemented

baselines/saplma/, detector name saplma.

Azaria & Mitchell, "The Internal State of an LLM Knows When It's Lying", EMNLP Findings 2023, aclanthology.org/2023.findings-emnlp.68, arXiv:2304.13734.

Independently validated: PARALLAX (2026) found SAPLMA one of only two reliable internal-state probes out of 22 evaluated (0.911 AUROC on HaluBench), with MIND, HaloScope, ACT, SEPs and HalluShift all near-chance (0.49-0.62).

Verified from paper section 4, quoted: "three hidden layers with decreasing numbers of hidden units (256, 128, 64), all utilizing ReLU activations. The final layer is a sigmoid output. We use the Adam optimizer. We do not fine-tune any of these hyper-parameters for this task. The classifier is trained for 5 epochs."

Correction to an earlier draft of this file: SAPLMA does not average activations across tokens. It reads the activations at the last token of the statement β€” one 4096-vector per statement. The earlier note here said "averages across all tokens" and was wrong.

Layer: the paper builds five probes (last, 28th, 24th, 20th, middle of 32) and finds 20/32 best for OPT-6.7b, the middle layer best for LLaMA2-7b. We probe the middle layer of a 16-layer model, matching the LLaMA finding.

Unstated in the paper: learning rate and batch size. The reference is Keras, so its defaults (1e-3, 32) are used β€” the only reading consistent with the paper's claim that nothing was tuned.

Supervision: --saplma-supervision response (default, faithful) fits one row per answer β€” last-token activations against the response label β€” and localises per token at scoring time, exactly as EPR is handled here. --saplma-supervision token fits every labelled token and reports under saplma_token_supervised, so the more generous configuration is on the table rather than assumed away.

NOTE: baselines/hidden_state_probe/ is NOT SAPLMA β€” it is logistic regression on the last layer with class_weight="balanced", correctly classed a control in fidelity_matrix.md. Do not relabel it.

2. HaMI β€” implemented

baselines/hami/, detector name hami.

"Robust Hallucination Detection in LLMs via Adaptive Token Selection", NeurIPS 2025, arXiv:2504.07863, code github.com/mala-lab/HaMI.

Architecture (hami.py):

Linear(n_features, 256) -> BatchNorm1d -> ReLU -> Dropout(0.3) -> Linear(256, 1) -> Sigmoid

Loss (hami.py:MIL_loss), transcribed exactly:

k = int(length * k_ratio) + 1                      # k_ratio = 0.1
loss += (1. - s_topk_mean**2 + nor_max[i]**2)      # index-matched bag pairs
smooth += torch.sum((s[:-1] - s[1:])**2) * 0.0008  # bags longer than 8 only
loss = loss / len(n_len_list) + smooth / len(n_len_list)

The abnormal term is index-matched against the normal term, which is why the reference draws equal-sized batches from two loaders. Note 1. - m**2 + n**2, not (1 - m)**2.

Training (train.py, main.py): Adam lr 1e-3, weight decay 5e-4; ReduceLROnPlateau(mode='min', factor=0.1, patience=20, min_lr=1e-6) stepped on the training loss; batch 128 bags per polarity with drop_last=True; 100 optimizer steps, not 100 passes; validation after every step, keeping the best model; validation metric is bag-level AUROC where the bag score is the mean of its top-k token scores (validate.py).

Layer selection: main.py loops over every layer, trains a separate model for each, and records validation results per layer β€” the layer is a validation-selected hyperparameter. We run one layer per invocation (--hidden-layer), so a layer sweep is a sweep of runs.

Two things the reference does that we cannot:

  • Its default --mode se scales hidden states by a probability from clustering several sampled responses per question. Our dataset has one response per prompt, so we use the reference's ori (unweighted) mode. HaMI is therefore evaluated below its published capability, and that must be stated.
  • Bag labels come from answer correctness (acc), with ambiguous acc == 2.0 bags dropped under is_refined. Ours: abnormal if the response contains any hallucinated token, normal if none, uncertain-only responses dropped.

Step budget caution. 100 steps is tied to their dataset size. On our larger training split it sees a fraction of the data. --hami-steps exists for this; any run that raises it must say so in the results, and the reference's own best-on-validation rule is what makes a longer budget defensible.

3. LLM-Check β€” implemented

baselines/llm_check/, detector names token_logprob, llm_check_logit_entropy, llm_check_window_entropy, llm_check_hidden, llm_check_attention.

Sriramanan et al., NeurIPS 2024, code GaurangSriramanan/LLM_Check_Hallucination_Detection. Five training-free scores from one forward pass, all verified against common_utils.py:

perplexity:      log_softmax(logits)[t-1, tok[t]]        # mean then exp
logit_entropy:   softmax(topk(logits, 50)); (-p*log p).mean()     # own row
window_entropy:  softmax(logits);           (-p*log p).mean()     # own row, w=1
hidden:          log(svdvals(Z.T @ J @ Z + 1e-3*I)).mean()        # J centres features
attention:       sum_h log(diag(A_h))                             # over the answer block

Note .mean() not .sum() in both entropies, and note that the reference reads the realized-token log-probability at row t-1 but both entropies at row t. Extraction stores rows prompt_len-1 .. prompt_len+T-1 so index t is the predicting row and index t+1 is the own row; both alignments are then served from one array.

Three things that are easy to get wrong:

  1. Orientation is fitted, not fixed. The authors' notebook negates perplexity and the attention score, negates neither entropy, and ships no orientation for the hidden score. An earlier draft here hard-coded the sign from the paper's prose and put both eigen scores at ~0.20 AUROC; the fitted bit puts them at ~0.80. One bit per score, fitted on train.
  2. centered_svd_val centres over the FEATURE axis. The reference passes (dim, n_tokens) and builds a dim x dim centering matrix, so Sigma comes out n_tokens x n_tokens. Easy to transpose by mistake.
  3. The attention diagonal changes meaning with the row. Own row = the token's self-attention (post-emission). Predicting row = attention to the most recent prefix token (causal). Not the same quantity; say so.

Hidden score is pooled per token over the expanding causal prefix (--llm-check-hidden-window 0, the default), which is the direct causal restriction of the reference's whole-response score and adds no hyperparameter.

token_logprob is LLM-Check's per-token perplexity term. It gets its own row because it is the floor every method must clear, but it is computed once, not double-counted under two names.

4. ICR Probe β€” implemented

baselines/icr_probe/, detector name icr_probe.

Zhang et al., ACL 2025 Main, code XavierZhang2002/ICR_Probe (src/icr_score.py, src/icr_probe.py, src/utils.py, src/config.py). An earlier draft of this file said there was no code release. There is.

ICR Score, per layer a and scored row r (compute_icr):

dx      = H[a + 1][r] - H[a][r]        # consecutive layers
targets = H[a][topk_idx]               # the LOWER layer, easy to get wrong
w       = (dx * targets).sum(1) / (targets.norm(1) + 1e-8)
ICR     = js_divergence(w, attn_row[topk_idx])   # standardise -> softmax -> JSD

Five details that a paper-only implementation gets wrong:

  1. Attention is the softmaxed probability, not the pre-softmax score. Equation 3 defines (Q.K)/sqrt(d); the code uses HF output_attentions.
  2. top_p silently overrides top_k. top_k = top_k if top_p is None else int(top_p * N), and the README passes both, so top_k=20 is dead code and k is a fraction of the sequence.
  3. Projection targets come from the lower layer, origin_hidden_states[a], not the layer whose update is being measured.
  4. The block mask drops response-to-prompt attention entirely, keeping only prompt-to-prompt and response-to-response.
  5. The induction-head fallback fires on a count test, not a threshold: if fewer than n_heads // 8 heads pass, the verdict is discarded and the top n_heads // 8 by skewness are taken instead.

Probe (src/utils.py, src/config.py): L -> 128 -> 64 -> 32 -> 1, each hidden layer BatchNorm1d + LeakyReLU(0.01) + Dropout(0.3), sigmoid; Kaiming uniform a=0.01; BCE, Adam(1e-3, wd 1e-5), ReduceLROnPlateau(min, 0.5, 5), 100 epochs, batch 16, best validation loss.

The featurisation is ours, unavoidably. _load_data is referenced but never defined in the release, and the README says the training code must be adapted. Default --icr-supervision response mean-pools ICR over the answer's tokens and fits the response label; scoring feeds each token's own layer-vector to the fitted probe. --icr-supervision token is the token-supervised variant.

Cost β€” the planning error worth remembering. ICR needs consecutive-layer hidden states and per-layer attention, which looked like it demanded a 12-24 GB multi-layer cache. It does not: the score reduces to one scalar per (token, layer) at extraction time, so the cached feature is (T, L) β€” about 15 MB for the whole dataset. Always check what the feature reduces to before budgeting for what the computation reads.

The AWS L4 signal cache cannot serve ICR, for three independent reasons: it samples layers 4/8/12/16 so no consecutive difference exists; it stores no attention, only the collapsed attn_ent scalar; and it covers answer prediction rows only, while the projection needs all N tokens. Checked before spending instance time.

What was wired into the runner

  • DETECTOR_SAPLMA / DETECTOR_HAMI, plus HIDDEN_STATE_DETECTORS so all three hidden-state methods share one extraction path and one feature array.
  • --hidden-alignment {generated_token_position, generation_prediction_position}. The causal suite sets the predicting row automatically; native suites keep the token's own row. Both new detectors are in CAUSAL_CAPABLE_DETECTORS.
  • hidden_sequence_training_example() carries the whole answer's hidden states plus both label granularities, so each detector picks the one its paper uses. U rows stay in the matrix and are excluded by the detector, because both methods index into the sequence (last token, adjacent-token smoothing) and deleting rows would silently change what "last" and "adjacent" mean.
  • --saplma-supervision/-epochs/-lr/-batch-size, --hami-steps/-batch-size/-k-ratio/-lr/-weight-decay.
  • Summary JSON gains a hidden_state_detectors block recording layer, alignment, and each method's fitted-run facts (bag counts, selected step, validation AUROC, training positive rate).

Cross-cutting cautions from this codebase

  • Never add a key to a cache spec unconditionally. The fingerprint is a hash of the whole spec, so a new key invalidates every cache built before it existed. This broke the 5.1 GB LatticeSpike base cache in r6 and cost a night. hidden_alignment is therefore added to feature_cache_spec only when non-default, and there is a test asserting its absence from legacy specs.
  • Never bump EXTRACTION_CODE_VERSION unless extraction genuinely changes for existing configurations; it invalidates every cache on every box.
  • Note that selected_features is already part of the fingerprint. Running --detector saplma hami will not reuse a cache built for the four-detector set. Extract the two hidden-state methods in their own pass β€” they need no attention output, so it is the cheap pass, not the expensive one.
  • Probe before long runs. A 5-record extraction probe caught two shape bugs in r6 that would each have surfaced hours in.
  • Verify the training path, not just extraction. r6's failures were both in training/cache-validation, which the extraction probe did not cover. tests/test_train_faithful_baselines_dgx.py now fits and scores both new detectors end to end on synthetic rows for exactly this reason.
  • Compare single-seed arms against the same seed's control. Per-seed validation AP for the current best LatticeSpike config: seed 42 = 0.2785, seed 43 = 0.2632, seed 44 = 0.2633; 3-seed mean 0.2683 +/- 0.0088. Comparing a seed-42 arm to the mean inflates it by ~0.010 and turned three nulls into apparent passes in r5.

Where things stand

  • Current best documented in ablations/BEST_MODEL.md (TEST AP 0.2741 +/- 0.0238, AUROC 0.9283 +/- 0.0040, 3 seeds).
  • Baseline audit in docs/baseline_fidelity_audit.md; SAPLMA is Β§4, HaMI Β§5.
  • Adaptation table rows for both are in scripts/gen_results_tables.py (tab:adaptation). The fair results table has no rows for them yet β€” add rows only once runs exist.
  • Rounds archived in ablations/r0, r1, r2, r3_5, r4, r5_stage1, multilayer, each with ANALYSIS.md.
  • r6b results are still unfetched and unarchived (4 arms: lookback s42/s43, head128 s43/s44) in outputs/sweep_r6 on the 5090.
  • WEPR still has no causal-matched number; the same hidden-state extraction pass should fix that.
  • Planned external evaluation: RAGTruth and Mu-SHROOM (team already working on both). FAVA parked but hallucination_labelling/span_align.py already supports it.