Join the conversation

Join the community of Machine Learners and AI enthusiasts.

Sign Up
SoulInPsyAbstract 
posted an update 27 days ago
Post
1,874
Eval · EXP-046
A LoRA Specialist Beat Zero-Shot on Every Group. Merging 3 of Them Gave Most of the Gain Back.
Three Qwen2.5-7B LoRA specialists, one per risk group (vulnerability, deletion, sensitive_publication), trained to predict how likely a causal chain actually completes to its harmful outcome. Each one genuinely beat its own zero-shot baseline:
* vulnerability: MAE 0.098 → 0.085
* deletion: MAE 0.144 → 0.113
* sensitive_publication: MAE 0.134 → 0.100
This wasn't a task already saturated zero-shot (unlike a same-day decomposition-classifier tune, EXP-045, where the base model was already at 100% before any training). Real signal, real improvement, on a task with actual headroom.
Then the equal-weight merge of all three specialists into one adapter — same convention that held up cleanly on a binary refusal task back in EXP-031 (6 specialists merged, -1pp swing, noise) — landed within 0.001–0.004 MAE of the unspecialized base model on every group. Not "close to the best specialist." Close to zero fine-tuning at all.
Likely mechanism: merging LoRAs that each shift a continuous number in group-specific directions cancels out under linear combination, in a way merging LoRAs that enforce a shared binary behavior doesn't. Not investigated yet: whether a routed combination (pick the right specialist per group at inference, not blend weights) holds the gain a flat merge loses.
One bug caught before writing this up, not after: the eval script's output filename only encoded before/after, not which adapter — the merged-eval run silently overwrote each specialist's own result file. Caught by checking the downloaded file's own recorded adapter path against what was expected, not by trusting the script's own success message. Fixed, specialists re-run cleanly under distinct filenames — numbers matched within sampling noise.
Adapters, raw eval data (before / each specialist / merged, 9 files), and the full writeup are up.

The merge result may be PEFT arithmetic, not continuous vs binary.

I pulled the four adapter_model.safetensors from qwen25-7b-exp046-probability-estimator-loras and took the merged one apart.

In all 196 modules, merged A = 0.8165 x (A_vuln + A_del + A_sens), and merged B = 0.8165 x (B_vuln + B_del + B_sens). Exact to float32 (relative error 6e-8 on a spot check). 0.8165 is sqrt(1/3 x 2): your weight of 1/3 times alpha/r of 2, applied to each factor separately.

So the merged update is not the average of the three updates. It is

(2/3)(B1+B2+B3)(A1+A2+A3)

which is one third of each specialist's delta, plus six cross products B_i A_j that no specialist ever trained.

Measured on the deltas:

  • the three specialist deltas are near orthogonal (median cosine 0.03 per module)
  • regressing the merged delta on them gives about 0.33 each (median)
  • a median 64% of the merged delta's energy sits outside their span. That is the cross terms.

So each group gets a third of its own correction, plus off-diagonal terms that make up 80% of the merged update's norm (median). A binary refusal gate with margin can survive that. A calibrated probability moves with it. That could explain EXP-031 vs EXP-046 without continuous targets cancelling.

Your merged card's own table also reads as partial retention more than "within 0.001-0.004 of base". sensitive_publication merged is 0.104, inside the specialist's 0.100-0.105. deletion 0.129 is about halfway from 0.144 to 0.113-0.118. Only vulnerability (0.102 vs 0.098) matches the headline.

A cleaner test: combination_type="cat" gives exactly the weighted sum of the deltas (rank 48, no cross terms). With weights [1,1,1] each group keeps its full delta, and the other two are near orthogonal to it.

One snag: the EXP-046 write-up link on all four cards returns "Entry not found" for me, and AI_EXPERIMENTS on sipa-os-governance stops at EXP-045. Where do the 9 raw eval files live? At n=20 per group I'd like to see how much of each gap is sampling.

·

You're right on all of it, and thank you for taking the adapters apart.

  • Arithmetic: reproduced. Merged A and B = 0.816497 × the sum of the specialists' factors in all 196 modules (relative error ≤ 2.2e-7), so the merged update is (2/3)(ΣB)(ΣA) with cross terms. On attention modules in layers 0/13/27 I get 64% of the merged delta's energy outside the span and off-diagonal terms at 80% of its norm, matching yours (MLP modules not recomputed; I used my local adapter copies, not a fresh download).
  • Headline: "within 0.001–0.004 of base" holds for vulnerability only. The merge keeps −35% / 48% / 87% of the specialist gain (vulnerability / deletion / sensitive_publication). The four cards now carry a correction.
  • Continuous vs binary: withdrawn. I haven't run your cat test yet.
  • n=20: paired bootstrap on the raw files: only sensitive_publication separates from base (95% CI of specialist−base MAE [−0.056, −0.010]); the vulnerability and deletion intervals include 0.
  • Raw files: the write-up and the 9 evals were on GitHub but never reached the HF mirror. Mirrored today. Correction note, scripts and outputs: https://huggingface.co/datasets/SoulInPsyAbstract/sipa-os-governance/blob/main/AI_EXPERIMENTS/EXP-046__probability-estimator-specialist-per-group-merge-regresses.CORRECTION.md

Thanks for the correction note, and for mirroring the raw files. I ran one more cut on them.

Your CIs compare each arm to base. The merge question is merged vs specialist, paired on the same 20 records (5000 resamples, as in your bootstrap_mae_ci.py):

  • vulnerability +0.018 [-0.005, +0.042]
  • deletion +0.016 [-0.029, +0.061]
  • sensitive_publication +0.005 [-0.010, +0.021]

All three straddle zero. So at n=20 the files can't show the merge lost anything relative to the specialists either. "Regresses" is as untested as "continuous targets cancel."

The kept-share column is a ratio of two noisy gaps, so it swings hard. Bootstrapped: vulnerability -1100% to +1189%, deletion -450% to +421%, sensitive_publication 6% to 142%.

That sets the size of the cat test. From the per-record spread of merged minus specialist, resolving the gap you observed at 80% power takes about:

  • vulnerability: 79 records (gap 0.018, sd 0.056)
  • deletion: 339 (gap 0.016, sd 0.105)
  • sensitive_publication: 508 (gap 0.0045, sd 0.036)

So vulnerability is the cheap place to run cat vs linear, and n=20 can't decide it anywhere.

The vulnerability pool is 200 chains with every 10th id held out (180 train, 20 eval). Getting to ~80 eval means new chains or rotating folds and retraining.

Which is cheaper for you?

·

Thanks dipankarsarkar, I reproduced your paired numbers on the raw files (gap, CI and the 79 / 339 / 508 sample sizes all match). You're right: at n=20 the data can't show that the merge lost anything relative to the specialists either, so "regresses" is untested. Cheapest for me: vulnerability only, inference-only, no retraining. I'll generate ~60 fresh held-out chains with the same generator (deduplicated against the 200), and run base / specialist / merged-linear / merged-cat on those plus the old 20 (n≈80). I'll add a second correction note to the repo first, then run it after Oct 6, when the Dark Factory deadline is behind me.

Good plan. One change I'd make before the run: keep the old 20 out of the test.

Those 20 are where the +0.018 gap came from. A gap found in a sample usually shrinks on fresh data, so pooling them in pulls the n=80 estimate toward the number you're testing.

I'd call the ~60 fresh chains the test, and report the old 20 next to them, not inside.

That has a price. At n=60, with the same per-record sd (0.056) and the observed gap, power is about 68%. If the true gap is half that, about 23%. So 60 can come back null without saying much.

For cat, weights [1,1,1] is the version that tests "full delta, no cross terms". 1/3 each would test something else.

If 80 fresh chains cost about the same as 60, would you generate 80?

·

Thanks, this is a good change and I'm taking it.
Design. The fresh chains are the test, and the old 20 are reported next to them, not inside. You're right that the +0.018 gap was found on those 20, so pooling them in would pull the estimate toward the number being tested. I confirmed your power figures: with sd 0.056, n=60 gives 0.68 at the observed gap and 0.23 at half of it; n=80 gives 0.80 and 0.29. So I'll generate 80 fresh vulnerability chains if the cost is about linear, and I'll say in the write-up that even n=80 can come back null at half the gap.
cat. I'll use combination_type="cat" with weights [1,1,1]. I checked it with the real PEFT call (0.20.0) on the actual adapter tensors: the merged adapter has rank 48 and scaling 1, and its delta equals D1+D2+D3 to a relative error of 4e-8. That is the full delta with no cross terms, as you said. Note it is a sum rather than an average, so the effective scale is three times the linear 1/3 merge, and I'll report that next to the result. Script and output are in the repo (verify_cat_merge_arithmetic.py).
Two limits I should state before the run. (1) The gold probability_estimate values were produced by hermes-4-405b through OpenRouter on Sep 21, told to vary the values, so the MAE measures agreement with that annotator, not accuracy against reality. (2) The generation script was not saved, so I'll rebuild the prompts from the dataset README and the existing records, and I'll flag that the fresh chains are comparable but not identical in provenance.
Timing. I'm busy with another deadline until Oct 6, so I'll run this after that. Nothing will be run before I post the final design (n, chain source, merge settings) here.

Read the generation scripts. One thing in them changes the n=80 plan.

Both scripts work in calls of 8, and the boundaries line up.
Chains 1-8 came from one generation call and were labelled in one annotation call, and so on.
(vulnerability 153-160 failed once and was re-labelled at the end, still as one block.)

The labels cluster by those blocks. Grouping each file's 200 probability_estimate values into its 25 blocks:

  • vulnerability: ICC 0.18 (permutation p = 0.0002, 20,000 shuffles)
  • deletion: 0.50
  • sensitive_publication: 0.45

Your old 20 eval ids happen to sit in 20 different blocks, so they were close to independent.
80 fresh chains will be 10 blocks of 8.

If the merged-minus-specialist gaps cluster like the vulnerability labels, the design effect is 1 + 7 x 0.18 = 2.25.
80 records then act like about 36, and power at the observed gap falls from 0.82 to about 0.48.

These files can't say which call causes it.
The model may write 8 similar-risk chains at once, or set one level for 8 labels at once.

A cheap design tells them apart: label the fresh chains in shuffled calls, each mixing chains from different generation calls, and bootstrap by generation call rather than by record.
If the clustering survives the shuffle, it lives in the chains. If it vanishes, it was the annotator.

Does a shuffled labelling pass fit inside your ~20 calls?

·

Dipankar, thanks for the block analysis. I ran the shuffled labelling as you suggested: 80 fresh chains, 10 generation calls of 8, labelled in 10 shuffled calls that mix chains across generation calls. ICC by generation call: original labels 0.039 (p = 0.24), shuffled labels −0.027 (p = 0.63), 20,000 permutations. On these 80 chains I don't see block clustering, in either labelling.
Caveat: 10 blocks of 8 gives little power, so this doesn't rule out a smaller effect. It also doesn't say whether the clustering in the old files came from the chains or the annotator. I haven't run the power calculation at design effect 2.25 yet; I can do that next if useful.

That is the test, and both numbers are clean. Three things fall out of what you now hold.

  1. You labelled the same 80 chains twice. The MAE between the two labellings is the annotator's own repeatability, and nobody in this thread has that number yet. For scale: on the old 20, the specialist scored MAE 0.085 against this annotator, and a constant 0.70 (the train median) also scores 0.085. If hermes disagrees with itself by anything near that, the arms are being ranked inside annotator noise. I would report that MAE, and fix the gold before any model runs: one labelling, or the mean of both. The mean halves the annotator's share of the variance, which should shrink the per-record sd of the gap.

  2. The missing grouping. Both labellings are reported by generation call. If the annotator sets one level per call, the shuffled labels should cluster by annotation call, and that number is not in the reply. It is one more groupby on data you already have.

  3. Power at 10 blocks of 8. I simulated the permutation ICC test at k=10, m=8 (2000 draws per point, alpha 0.05):

  • true ICC 0.18, the old vulnerability value: detected 67% of the time
  • true ICC 0.10: 39%
  • true ICC 0.45 to 0.50, the deletion and sensitive_publication values: 97% to 99%
  • an observed 0.039 sits inside the 95% band of every true ICC up to about 0.25

So the fresh 80 do rule out clustering at the deletion size, and leave the vulnerability size open.

Taking 0.039 at face value: design effect 1.27, 80 records act like 63, power 0.72 at the observed gap and 0.25 at half of it. Better than the 0.48 I feared, short of the 0.82 that independent records would give.

Would you mirror the 80 chains with both labellings before the eval runs? Then the test-retest MAE and the annotation-call ICC are one script away for anyone.

·

Test-retest on the fresh 80: two labellings of the same chains (orig and shuffled prompt order) give MAE 0.107 (median 0.10, max 0.35), with exact agreement on 20% of records. That is above the 0.085 gap we were comparing, so the arms are inside annotator noise. Per generation call the MAE runs 0.075 to 0.131 with no clear cluster; I have not run the ICC by call yet.
Before any eval run I will fix the gold as the mean of the two labellings and report the test-retest MAE in the table. Mirroring the 80 with both labellings is already done (probability_fresh_orig and probability_fresh_shuffled, with chain_ref ids).