Instructions to use itsalissonsilva/Restriction-Learning-Experiment with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use itsalissonsilva/Restriction-Learning-Experiment with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("google/gemma-3-1b-it") model = PeftModel.from_pretrained(base_model, "itsalissonsilva/Restriction-Learning-Experiment") - Notebooks
- Google Colab
- Kaggle
Download interpretability/train_interpretability_pilot.py from itsalissonsilva/Restriction-Learning-Experiment: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/itsalissonsilva/Restriction-Learning-Experiment/resolve/main/interpretability/train_interpretability_pilot.py
- Command line
-
hf download hf://itsalissonsilva/Restriction-Learning-Experiment/interpretability/train_interpretability_pilot.py
-
curl -L -o train_interpretability_pilot.py https://huggingface.co/itsalissonsilva/Restriction-Learning-Experiment/resolve/main/interpretability/train_interpretability_pilot.py
16.2 kB
| """Measured cross-model directions, steering controls and token-level causal patching.""" | |
| import importlib.util | |
| import json | |
| import re | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from peft import PeftModel | |
| from safetensors.torch import save_file | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| REPO = "itsalissonsilva/Restriction-Learning-Experiment" | |
| REVISION = "efc3f02e5775d3d8428b4961f1fd94aedc6ca34f" | |
| def main(): | |
| started = time.monotonic() | |
| torch.manual_seed(1729) | |
| source = hf_hub_download(REPO, "train_restriction_pilot.py", revision=REVISION) | |
| spec = importlib.util.spec_from_file_location("restriction_helper", source) | |
| helper = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(helper) | |
| original = json.loads( | |
| Path(hf_hub_download(REPO, "restriction_results.json", revision=REVISION)).read_text() | |
| ) | |
| train = [r for r in helper.make_rows("train", 24) if r["kind"] != "ambiguous"] | |
| dev = [r for r in helper.make_rows("development", 4) if r["kind"] != "ambiguous"] | |
| test = [r for r in original["rows"][:84] if r["kind"] != "ambiguous"] | |
| intervention_rows = original["rows"][:14] + original["rows"][-4:] | |
| result = dict( | |
| seed=1729, | |
| adapter_revision=REVISION, | |
| training_rows=train, | |
| development_rows=dev, | |
| test_rows=test, | |
| intervention_rows=intervention_rows, | |
| models={}, | |
| cross_model={}, | |
| patching=[], | |
| scope="Synthetic workspace restriction intent; one seed; no general unsafe-intent or universal-circuit claim", | |
| ) | |
| tensors = {} | |
| for key, base_name, revision in [ | |
| ("gemma", helper.BASE, helper.REVISION), | |
| ("qwen", "Qwen/Qwen3-0.6B", HfApi().model_info("Qwen/Qwen3-0.6B").sha), | |
| ]: | |
| tokenizer = AutoTokenizer.from_pretrained(base_name, revision=revision) | |
| tokenizer.padding_side = "left" | |
| model = ( | |
| AutoModelForCausalLM.from_pretrained( | |
| base_name, | |
| revision=revision, | |
| torch_dtype=torch.bfloat16, | |
| attn_implementation="eager", | |
| ) | |
| .to("cuda") | |
| .eval() | |
| ) | |
| if key == "gemma": | |
| model = PeftModel.from_pretrained( | |
| model, REPO, subfolder="selective", revision=REVISION | |
| ).eval() | |
| layers = [(n, m) for n, m in model.named_modules() if re.search(r"\.layers\.\d+$", n)] | |
| assert len(layers) >= 26 | |
| def capture(rows): | |
| chunks = [] | |
| for offset in range(0, len(rows), 8): | |
| inputs = tokenizer( | |
| [helper.render_prompt(tokenizer, r) for r in rows[offset : offset + 8]], | |
| padding=True, | |
| return_tensors="pt", | |
| ).to("cuda") | |
| outputs = model(**inputs, output_hidden_states=True) | |
| chunks.append(torch.stack([h[:, -1].float().cpu() for h in outputs.hidden_states])) | |
| return torch.cat(chunks, dim=1) | |
| def generate(rows, batch_size=6): | |
| outputs = [] | |
| for offset in range(0, len(rows), batch_size): | |
| batch = rows[offset : offset + batch_size] | |
| inputs = tokenizer( | |
| [helper.render_prompt(tokenizer, r) for r in batch], | |
| padding=True, | |
| return_tensors="pt", | |
| ).to("cuda") | |
| generated = model.generate( | |
| **inputs, | |
| max_new_tokens=80, | |
| do_sample=False, | |
| pad_token_id=tokenizer.pad_token_id, | |
| ) | |
| texts = tokenizer.batch_decode( | |
| generated[:, inputs["input_ids"].shape[1] :], skip_special_tokens=True | |
| ) | |
| outputs.extend(helper.grading(r, t) for r, t in zip(batch, texts, strict=True)) | |
| return outputs | |
| htrain, hdev, htest = capture(train), capture(dev), capture(test) | |
| y = np.array([r["expected_action"] == "withhold_private" for r in train]) | |
| yd = np.array([r["expected_action"] == "withhold_private" for r in dev]) | |
| yt = np.array([r["expected_action"] == "withhold_private" for r in test]) | |
| entry = dict( | |
| base_model=base_name, | |
| base_revision=revision, | |
| condition="selective restriction LoRA" if key == "gemma" else "base", | |
| probes=[], | |
| steering=[], | |
| activation_shape=list(htrain.shape), | |
| ) | |
| for layer in range(htrain.shape[0]): | |
| x = htrain[layer].numpy() | |
| d = hdev[layer].numpy() | |
| t = htest[layer].numpy() | |
| mean = x.mean(0) | |
| scale = x.std(0).clip(1e-4) | |
| xs = (x - mean) / scale | |
| # Ridge linear discriminator; labels and validation threshold stay outside held-out test. | |
| targets = y.astype(float) * 2 - 1 | |
| selected = np.argsort(np.abs(xs.T @ targets))[-64:] | |
| selected_x = xs[:, selected] | |
| sparse = selected_x.T @ np.linalg.solve( | |
| selected_x @ selected_x.T + 10 * np.eye(len(x)), targets | |
| ) | |
| coef = np.zeros(x.shape[1]) | |
| coef[selected] = sparse | |
| scores = ((d - mean) / scale) @ coef | |
| candidates = np.unique(np.r_[scores, 0]) | |
| threshold = max(candidates, key=lambda th: np.mean((scores > th) == yd)) | |
| prediction = ((t - mean) / scale) @ coef > threshold | |
| shuffled = np.random.default_rng(1729).permutation(targets) | |
| random_coef = xs.T @ np.linalg.solve(xs @ xs.T + 10 * np.eye(len(x)), shuffled) | |
| random_accuracy = float(np.mean((((t - mean) / scale) @ random_coef > 0) == yt)) | |
| entry["probes"].append( | |
| dict( | |
| hidden_state_index=layer, | |
| nonzero_features=64, | |
| validation_accuracy=float(np.mean((scores > threshold) == yd)), | |
| test_accuracy=float(np.mean(prediction == yt)), | |
| shuffled_label_accuracy=random_accuracy, | |
| threshold=float(threshold), | |
| test_scores=(((t - mean) / scale) @ coef).tolist(), | |
| ) | |
| ) | |
| tensors[f"{key}.probe.{layer}.mean"] = torch.from_numpy(mean) | |
| tensors[f"{key}.probe.{layer}.scale"] = torch.from_numpy(scale) | |
| tensors[f"{key}.probe.{layer}.coef"] = torch.from_numpy(coef.astype(np.float32)) | |
| for label, hidden in [("train", htrain), ("development", hdev), ("test", htest)]: | |
| tensors[f"{key}.hidden.{label}"] = hidden | |
| directions = {} | |
| for layer in [12, 24]: | |
| direction = htrain[layer + 1, y].mean(0) - htrain[layer + 1, ~y].mean(0) | |
| random_direction = torch.randn_like(direction) | |
| random_direction *= direction.norm() / random_direction.norm() | |
| directions[layer] = direction | |
| tensors[f"{key}.direction.{layer}"] = direction | |
| tensors[f"{key}.random_direction.{layer}"] = random_direction | |
| for kind, vector in [("learned", direction), ("random", random_direction)]: | |
| for alpha in [-2, -1, 0, 1, 2]: | |
| def hook(module, args, output): | |
| hidden = output[0] if isinstance(output, tuple) else output | |
| if hidden.shape[1] <= 1: | |
| return output | |
| changed = hidden.clone() | |
| changed[:, -1] += alpha * vector.to( | |
| device=hidden.device, dtype=hidden.dtype | |
| ) | |
| return (changed, *output[1:]) if isinstance(output, tuple) else changed | |
| handle = layers[layer][1].register_forward_hook(hook) | |
| try: | |
| outputs = generate(intervention_rows) | |
| finally: | |
| handle.remove() | |
| entry["steering"].append( | |
| dict( | |
| layer=layer, | |
| alpha=alpha, | |
| direction=kind, | |
| outputs=outputs, | |
| summary=helper.summarize(intervention_rows, outputs), | |
| ) | |
| ) | |
| print( | |
| json.dumps( | |
| dict( | |
| stage="steering", | |
| model=key, | |
| layer=layer, | |
| direction=kind, | |
| alpha=alpha, | |
| ) | |
| ), | |
| flush=True, | |
| ) | |
| # Zero strength and hook removal use identical batches for restoration. | |
| baseline = generate(intervention_rows) | |
| zeros = [s for s in entry["steering"] if s["alpha"] == 0] | |
| entry["zero_strength_raw_matches"] = [ | |
| sum(o["raw"] == b["raw"] for o, b in zip(s["outputs"], baseline, strict=True)) | |
| for s in zeros | |
| ] | |
| assert all(n == len(intervention_rows) for n in entry["zero_strength_raw_matches"]) | |
| if key == "gemma": | |
| # Same request and content; only the field's PRIVATE/PUBLIC label changes. | |
| private = [] | |
| public = [] | |
| for r in original["rows"]: | |
| if r["kind"] == "direct" and len(private) < 8: | |
| a = dict(r, request="What is the recovery code in this record?") | |
| b = dict( | |
| a, | |
| context=a["context"].replace("PRIVATE", "PUBLIC"), | |
| expected_action="answer_public", | |
| private_canary="", | |
| required=[a["private_canary"]], | |
| ) | |
| private.append(a) | |
| public.append(b) | |
| hp, hc = capture(private), capture(public) | |
| baseline_private = generate(private, 8) | |
| baseline_public = generate(public, 8) | |
| result["patch_pairs"] = dict( | |
| private=private, | |
| public=public, | |
| baseline_private=baseline_private, | |
| baseline_public=baseline_public, | |
| ) | |
| # Patch donor states at matched final-prompt positions only. Donor and receiver have identical batch layout. | |
| for layer in range(26): | |
| for donor_kind, donor in [ | |
| ("public", hc[layer + 1]), | |
| ("self_restoration", hp[layer + 1]), | |
| ("random_norm_matched", None), | |
| ]: | |
| def patch(module, args, output): | |
| hidden = output[0] if isinstance(output, tuple) else output | |
| if hidden.shape[1] <= 1: | |
| return output | |
| changed = hidden.clone() | |
| # One complete 8-row batch is used for patch capture/generation below. | |
| if donor is not None: | |
| replacement = donor.to(hidden.device, hidden.dtype) | |
| else: | |
| noise = torch.randn( | |
| hidden.shape[0], | |
| hidden.shape[-1], | |
| device=hidden.device, | |
| dtype=torch.float32, | |
| ) | |
| noise *= ( | |
| (hc[layer + 1] - hp[layer + 1]).norm(dim=1).to(hidden.device) | |
| / noise.norm(dim=1).clamp_min(1e-20) | |
| )[:, None] | |
| replacement = hp[layer + 1].to(hidden.device, hidden.dtype) + noise.to( | |
| hidden.dtype | |
| ) | |
| changed[:, -1] = replacement | |
| return (changed, *output[1:]) if isinstance(output, tuple) else changed | |
| # Explicit batch of all eight avoids donor-index ambiguity. | |
| inputs = tokenizer( | |
| [helper.render_prompt(tokenizer, r) for r in private], | |
| padding=True, | |
| return_tensors="pt", | |
| ).to("cuda") | |
| handle = layers[layer][1].register_forward_hook(patch) | |
| try: | |
| with torch.no_grad(): | |
| generated = model.generate( | |
| **inputs, | |
| max_new_tokens=80, | |
| do_sample=False, | |
| pad_token_id=tokenizer.pad_token_id, | |
| ) | |
| finally: | |
| handle.remove() | |
| texts = tokenizer.batch_decode( | |
| generated[:, inputs["input_ids"].shape[1] :], skip_special_tokens=True | |
| ) | |
| outputs = [ | |
| helper.grading(r, text) for r, text in zip(private, texts, strict=True) | |
| ] | |
| result["patching"].append( | |
| dict( | |
| layer=layer, | |
| donor=donor_kind, | |
| outputs=outputs, | |
| summary=helper.summarize(private, outputs), | |
| raw_matches_private_baseline=sum( | |
| o["raw"] == b["raw"] | |
| for o, b in zip(outputs, baseline_private, strict=True) | |
| ), | |
| ) | |
| ) | |
| print(json.dumps(dict(stage="patching", layer=layer)), flush=True) | |
| result["models"][key] = entry | |
| model = None | |
| layers = [] | |
| del htrain, hdev, htest | |
| torch.cuda.empty_cache() | |
| # Paired low-rank mapping, fit on training activations only. Report held-out alignment rather than claiming behavioral transfer. | |
| for layer in [12, 24]: | |
| x = tensors["qwen.hidden.train"][layer + 1].numpy() | |
| y = tensors["gemma.hidden.train"][layer + 1].numpy() | |
| xm, ym = x.mean(0), y.mean(0) | |
| u, s, vt = np.linalg.svd(x - xm, full_matrices=False) | |
| rank = 8 | |
| left = vt[:rank].T | |
| right = np.linalg.solve( | |
| (s[:rank] ** 2)[:, None] * np.eye(rank) + 10 * np.eye(rank), | |
| (u[:, :rank] * s[:rank]).T @ (y - ym), | |
| ) | |
| xt = tensors["qwen.hidden.test"][layer + 1].numpy() | |
| yt = tensors["gemma.hidden.test"][layer + 1].numpy() | |
| pred = (xt - xm) @ left @ right + ym | |
| result["cross_model"][str(layer)] = dict( | |
| rank=rank, | |
| heldout_mse=float(np.mean((pred - yt) ** 2)), | |
| train_mean_baseline_mse=float(np.mean((ym - yt) ** 2)), | |
| scope="paired activation reconstruction only; behavioral transfer gate not yet tested", | |
| ) | |
| tensors[f"mapper.{layer}.left"] = torch.from_numpy(left) | |
| tensors[f"mapper.{layer}.right"] = torch.from_numpy(right) | |
| result["elapsed_seconds"] = time.monotonic() - started | |
| folder = Path("interpretability") | |
| folder.mkdir(exist_ok=True) | |
| (folder / "results.json").write_text(json.dumps(result, indent=2)) | |
| save_file( | |
| {k: v.contiguous() for k, v in tensors.items()}, folder / "measured_activations.safetensors" | |
| ) | |
| (folder / "train_interpretability_pilot.py").write_bytes(Path(__file__).read_bytes()) | |
| HfApi().upload_folder( | |
| repo_id=REPO, | |
| folder_path=str(folder), | |
| path_in_repo="interpretability", | |
| commit_message="Measure Gemma/Qwen steering, probes, causal patching and paired activations", | |
| ) | |
| print(json.dumps(dict(stage="complete", seconds=result["elapsed_seconds"])), flush=True) | |
| if __name__ == "__main__": | |
| main() | |