|
Download docs/new_baselines_implementation_spec.md from Anonymous1611/Lattice_Spike: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/Anonymous1611/Lattice_Spike/resolve/main/docs/new_baselines_implementation_spec.md
- Command line
-
hf download hf://Anonymous1611/Lattice_Spike/docs/new_baselines_implementation_spec.md
-
curl -L -o new_baselines_implementation_spec.md https://huggingface.co/Anonymous1611/Lattice_Spike/resolve/main/docs/new_baselines_implementation_spec.md
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](https://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](https://arxiv.org/abs/2504.07863), | |
| code [github.com/mala-lab/HaMI](https://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:** | |
| ```python | |
| 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](https://github.com/GaurangSriramanan/LLM_Check_Hallucination_Detection). | |
| Five training-free scores from one forward pass, all verified against | |
| `common_utils.py`: | |
| ```python | |
| 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](https://github.com/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`): | |
| ```python | |
| 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. | |