Train Lens (SAE)
The interpretable lens — get the feature list, not just the vector
- lens_path
The contrastive trainer gives you a direction that separates concepts, but it's opaque - a black box vector you have to trust. This node is the "show me your work" version. It trains a Sparse Autoencoder on the text encoder's residual stream, decomposes the concept into specific sparse features, and saves a lens whose metadata records exactly which feature indices define "cinematic" and by how much. It's slower - roughly five minutes instead of thirty seconds - but it's the pack's bridge into real mechanistic interpretability, the same SAE approach from Bricken et al. (Anthropic, 2023) that's been the backbone of LLM interpretability research.
How it works
Full pipeline, straight from the pack's HOW_IT_WORKS.md:
- Hook into the text encoder's residual stream at layer 22 (~60% depth - the point where syntactic processing gives way to semantic representation), and collect token-level activations from ~500 diverse prompts (~15k vectors).
- Train an SAE on those activations: 8× expansion on Qwen's 2560d gives 20,480 sparse features, with unit-norm decoder columns and L1 sparsity.
- Run your positive and negative concept texts through the model + SAE, compute
mean(pos_features) - mean(neg_features), and take the top-K features by absolute difference. - Reconstruct a direction from those features via bias-free decoding (
decode_sparse). - Optionally blend with a contrastive direction (on by default) for better separation.
The payoff is that the lens carries sae_features in its metadata - indices like [4821, 12033, 7944, ...] - plus sanity metrics like how well the SAE direction matches the raw centroid difference. That's the thing contrastive lenses just don't give you.
The inputs that matter
- positive_texts / negative_texts - same discipline as contrastive: one per line, similar scenes without the concept for the negatives, 10+ pairs recommended.
- layer - 22 by default. Earlier layers = lower-level features, later = more abstract. 22 is the documented sweet spot for style/aesthetics.
- sae_expansion - 8 (20,480 features). Higher is more fine-grained but slower and a bigger file.
- sae_features - how many top features go into the direction. 30 default; more = richer but noisier.
- refine_contrastive - on by default; blends in a contrastive direction so you get interpretability and robust separation.
- sae_save_path / sae_load_path - this is the good stuff. Train the SAE once, save it, and reuse it across concepts to skip the ~5-minute training step for every subsequent lens.
- transcoder_repo - a HuggingFace repo for pretrained 64× transcoders (e.g.
mwhanna/qwen3-4b-transcoders, 163,840 features trained on ~1B tokens). This is the shortcut: skip training a small SAE entirely and use a much larger, better-quality one. It downloads on first use.
Output is lens_path - the absolute path to the saved lens, which also lands in lenses/ for the dropdown. The exact same training is available from the CLI with python tools/lens_factory.py sae <preset> (add --sae-save ./my_sae.pt to cache the SAE).
Installing
Standard for this pack - ComfyUI Manager search "Concept Steer", or clone https://github.com/Nynxz/ComfyUI-ConceptSteer into custom_nodes/ and restart. Training needs transformers and safetensors (pip install transformers safetensors) and ~10 GB VRAM for Qwen. No pretrained lenses ship with the pack - everything is generated locally.
Common gotchas
- Five minutes feels like forever on the first run because the SAE activation collection + training is the slow part. Save with
sae_save_pathand reuse it; subsequent lenses are ~30s. - The feature indices aren't human-readable yet. The README is direct about this: without a feature labeling pipeline,
[4821, 12033, ...]is just numbers. That's what the Feature Dictionary / Feature Map / Feature Gate / Feature Probe nodes in the pack are for - this lens is one stop on a longer interpretability workflow. - Refine-contrastive is on by default for a reason. Turning it off gives you a purer SAE direction but weaker separation. Turn it off only if you specifically want the raw SAE vector.
Inputs (16)
| Name | Type | Default | Description |
|---|---|---|---|
| concept_name | STRING | my_concept | Name for the concept (used in filename) |
| positive_texts | STRING | Texts that EMBODY the concept (one per line). 10+ pairs recommended for strong results. | |
| negative_texts | STRING | Neutral/opposite texts (one per line, same count as positive). Should describe SIMILAR scenes WITHOUT the concept. | |
| layer | INT | 221–36 | Which transformer layer to hook into. Default 22 (~60% depth) is the sweet spot for concepts. Earlier layers = lower-level features, later = more abstract. |
| sae_expansion | INT | 82–16 | SAE hidden dimension multiplier. 8x (default) = 20,480 features for 2560d Qwen. Higher = more features but slower training. |
| sae_epochs | INT | 20050–1000 | SAE training epochs (default 200, more = better features) |
| sae_features | INT | 305–100 | Number of top SAE features to keep in the concept direction. More features = richer concept but noisier. |
| n_prompts | INT | 500100–2000 | Number of diverse prompts for SAE activation collection |
| refine_contrastive | BOOLEAN | true | Also run contrastive optimization on output embeddings and blend with SAE direction. Recommended for best results. |
| contrastive_steps | INT | 500500–20000 | Contrastive optimization steps (only used if refine_contrastive is enabled) |
| sae_save_path | STRING | Save the trained SAE to this path for reuse across concepts. Saves ~5min per subsequent concept. | |
| sae_load_path | STRING | Load a pre-trained SAE/transcoder instead of training a new one. Supports both native .pt SAE and .safetensors transcoder formats. | |
| transcoder_repo | STRING | HuggingFace repo for pretrained transcoders (e.g. 'mwhanna/qwen3-4b-transcoders'). Downloads a 64x expansion transcoder with 163,840 features trained on ~1B tokens. Much better than training a small SAE. Leave empty to train your own SAE. | |
| encoder_path | STRING | Path to Qwen 3.4B safetensors file. Leave empty to use QWEN_ENCODER_PATH env var. | |
| output_dir | STRING | Override output directory for the lens file | |
| protect_existing | BOOLEAN | true | If the output lens file already exists, save as _v2, _v3, … instead of overwriting. Disable only when intentionally replacing. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| lens_path | STRING | — |