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](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.