Download examples/tutorials/causal_steering_vsv.py from gexinh/vlm-probe: direct link, hf CLI and curl.
- Browser
- Download file 6.35 kB
-
https://huggingface.co/spaces/gexinh/vlm-probe/resolve/main/examples/tutorials/causal_steering_vsv.py
- Command line
-
hf download hf://spaces/gexinh/vlm-probe/examples/tutorials/causal_steering_vsv.py
-
curl -L -o causal_steering_vsv.py https://huggingface.co/spaces/gexinh/vlm-probe/resolve/main/examples/tutorials/causal_steering_vsv.py
6.35 kB
| """Live tutorial: run after installing vlmprobe[transformers,visualization].""" | |
| # 1. Install and load the native VLM | |
| import os | |
| import hashlib | |
| import urllib.request | |
| from pathlib import Path | |
| import torch | |
| import matplotlib.pyplot as plt | |
| from PIL import Image | |
| from transformers import AutoModelForImageTextToText, AutoProcessor | |
| from vlmprobe import Prober, TokenMargin | |
| from vlmprobe.visualization import plot_input, plot_intervention_curves | |
| # This recipe targets the SmolVLM/Idefics3 image/chat contract. | |
| # First run downloads the 500M model (~1 GB); CUDA is recommended. | |
| model_id = os.environ.get("VLM_PROBING_MODEL", "HuggingFaceTB/SmolVLM-500M-Instruct") | |
| device = os.environ.get("VLM_PROBING_DEVICE", "cuda:0" if torch.cuda.is_available() else "cpu") | |
| dtype = torch.bfloat16 if device.startswith("cuda") else torch.float32 | |
| model = AutoModelForImageTextToText.from_pretrained( | |
| model_id, dtype=dtype, attn_implementation="eager", | |
| ).to(device).eval() | |
| processor = AutoProcessor.from_pretrained(model_id) | |
| probe = Prober(model, processor=processor) | |
| assert model.config.model_type == "idefics3", "Use this recipe with SmolVLM; other models need their native input guide" | |
| print({name: info for name, info in probe.describe()["methods"].items() | |
| if name.startswith("causal.")}) | |
| output_dir = Path(os.environ.get("VLM_PROBING_OUTPUT", "outputs/tutorials/causal")) | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| # 2. Read the original image and verify its source identity | |
| # A raw COCO image, not an attribution asset or cached model output. | |
| filename = "COCO_val2014_000000310196.jpg" | |
| expected_sha256 = "0dcc39d91764e59e0e01c11b92ec31320f094a87ca2dce5e0c56c4f1a361daf4" | |
| custom_image = os.environ.get("VLM_PROBING_IMAGE") | |
| root = next((p for p in [Path.cwd(), *Path.cwd().parents] | |
| if (p / "demos/assets/images" / filename).is_file()), None) | |
| image_path = Path(custom_image) if custom_image else (root / "demos/assets/images" / filename if root else output_dir / filename) | |
| if custom_image and not image_path.is_file(): | |
| raise FileNotFoundError(image_path) | |
| if not image_path.is_file(): | |
| url = "https://s3.amazonaws.com/images.cocodataset.org/val2014/" + filename | |
| with urllib.request.urlopen(url, timeout=60) as response: | |
| image_path.write_bytes(response.read()) | |
| if not custom_image: | |
| assert hashlib.sha256(image_path.read_bytes()).hexdigest() == expected_sha256 | |
| image = Image.open(image_path).convert("RGB") | |
| # 3. Prepare a native image prompt and an exact text-only reference | |
| question = "Please help me describe the image in detail." | |
| messages = [{"role": "user", "content": [ | |
| {"type": "image"}, {"type": "text", "text": question}, | |
| ]}] | |
| prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
| values = dict(processor(text=prompt, images=image, return_tensors="pt")) | |
| positive = {key: value.to(device=device, dtype=dtype if value.is_floating_point() else value.dtype) | |
| if isinstance(value, torch.Tensor) else value for key, value in values.items()} | |
| # SmolVLM surrounds all image crops/global image tokens with a contiguous wrapper span. | |
| # Remove that entire span; preserve the exact tokenized text on either side. | |
| wrapper_id = processor.fake_image_token_id | |
| wrappers = (positive["input_ids"][0] == wrapper_id).nonzero().flatten() | |
| assert wrappers.numel() >= 2, "Expected SmolVLM image wrapper tokens" | |
| keep = torch.ones(positive["input_ids"].shape[1], dtype=torch.bool, device=device) | |
| keep[int(wrappers[0]):int(wrappers[-1]) + 1] = False | |
| negative = {"input_ids": positive["input_ids"][:, keep].clone(), | |
| "attention_mask": positive["attention_mask"][:, keep].clone()} | |
| assert torch.equal(negative["input_ids"], positive["input_ids"][:, keep]) | |
| print("Positive prompt:", prompt) | |
| print("Text-only reference:", processor.tokenizer.decode(negative["input_ids"][0])) | |
| print("Positive/reference token counts:", positive["input_ids"].shape[1], negative["input_ids"].shape[1]) | |
| ax = plot_input(image, prompt=question, title="Raw COCO input") | |
| ax.figure.savefig(output_dir / "causal-vsv-input.png", dpi=150, bbox_inches="tight") | |
| plt.show() | |
| # 4. Construct directions, intervene, and generate fresh continuations | |
| # No training: directions are positive-minus-negative last-context residuals. | |
| vectors = probe.causal.vsv(positive, negative, layers=None) | |
| print("Direction construction:", vectors.metadata) | |
| strengths = [0.0, 0.02, 0.05] | |
| max_new_tokens = int(os.environ.get("VLM_PROBING_MAX_NEW_TOKENS", "32")) | |
| # These are exact vocabulary-token logits, not complete word probabilities. | |
| candidates = {word: processor.tokenizer.encode(word, add_special_tokens=False)[0] | |
| for word in ("person", "car")} | |
| print("Fixed-prefix subwords:", {word: processor.tokenizer.decode([token]) | |
| for word, token in candidates.items()}) | |
| responses = {word: [] for word in candidates} | |
| captions = {} | |
| for strength in strengths: | |
| method = vectors.configure(strength=strength, tokens="prediction") | |
| # All strengths see this same original prompt in this diagnostic. | |
| trace = method.forward(positive, direction=vectors.directions) | |
| for word, token in candidates.items(): | |
| responses[word].append(float(trace.logits[0, -1, token])) | |
| del trace | |
| output = method.generate(positive, direction=vectors.directions, | |
| max_new_tokens=max_new_tokens, do_sample=False) | |
| captions[strength] = output.metadata["decoded_text"][0] | |
| print(f"Strength {strength}: {captions[strength]}") | |
| output.save(output_dir / f"vsv_strength_{strength:g}") | |
| # 5. Visualize the fixed-prefix response and retain full generated text | |
| ax = plot_intervention_curves( | |
| {word: {"x": strengths, "y": values} for word, values in responses.items()}, | |
| xlabel="Raw residual steering strength", ylabel="First-subword logit", | |
| title="Same input prefix; per-image VSV strength sweep", | |
| ) | |
| ax.figure.savefig(output_dir / "causal-vsv.png", dpi=150, bbox_inches="tight") | |
| plt.show() | |
| # Persist the complete generated continuations, not selected favorable excerpts. | |
| import json | |
| (output_dir / "vsv_captions.json").write_text(json.dumps({ | |
| "model": model_id, "prompt": prompt, "construction": vectors.metadata, | |
| "max_new_tokens": max_new_tokens, "captions": captions, | |
| "candidate_ids": candidates, "fixed_prefix_logits": responses, | |
| }, indent=2) + "\n") | |