Train Lens (Contrastive)
Train Lens (Contrastive) — teach a direction a concept, no LoRA required
- clip
- steer
- report
Train Lens (Contrastive) is the node that gets you concepts SAE features can't. Single SAE features are narrow - one feature might reliably fire on rim lighting, but "cinematic" or "vintage film" or "ethereal" isn't one feature, it's a whole cluster. This node trains a direction for exactly those concepts: you give it positive prompts that embody the concept, negative prompts that don't, and it optimizes a vector that maximally separates the two. Roughly 30 seconds later you have a steer plan.
That's the same recipe as ConceptSteer's contrastive lens training, but routed through ComfyUI's CLIP bridge - so it works on whatever encoder your image model actually uses (FLUX.2 [klein]'s Qwen3-8B, Qwen-Image, quantised encoders included) without loading a separate Qwen LLM. It's the most "train-y" node in the pack and the one that produces the most reusable results, which is why its output feeds straight into Save Lens.
How it works
It captures the mean-pooled residual at one layer for each of your positive and negative prompts, then runs a small optimization loop (default 500 steps, Adam, lr 0.01) that maximizes a Bradley-Terry margin loss on (pos − neg) · direction. The result is normalized to the unit sphere so strength stays calibrated. The beta input controls margin sharpness - higher = a sharper boundary, easier to overfit; the default 50 is a solid middle. It even escapes ComfyUI's inference-mode tensor restrictions to run real autograd, which is a nice bit of engineering under the hood.
The inputs that matter
- clip - the
CLIPwhose encoder you're training on. Use the one you'll actually generate with. - positive_texts / negative_texts - one prompt per line, equal counts required. The pairs should differ only on the concept: "a cinematic dramatic portrait" vs "a casual snapshot portrait" beats "a cinematic dramatic portrait" vs "a photo of a dog." The pack ships starter prompt packs in
examples/lens_prompts.md(cinematic, vintage_film, ethereal…). - layer -
-1(last layer) is the default and the right choice if you'll apply via Steer Conditioning. Pick a middle layer if you'll apply via Steer CLIP at the same layer. - steps / lr / beta - leave at defaults (500 / 0.01 / 50) until you have a reason not to.
- strength - the default strength baked into the plan (start 1.0; edit models often want 0.5–1.5).
- save_name - set it to auto-save to
lenses/qwenscope/<name>.safetensorsafter training.
Outputs: steer (the trained direction, ready for Steer Conditioning / Steer CLIP / Save Lens) and report - a STRING with training time, layer, pairs, steps and the resulting direction norm.
Installing it
Same pack, one install: ComfyUI Manager → search "ComfyUI QwenScope", or
cd ComfyUI/custom_nodes
git clone https://github.com/Nynxz/ComfyUI-QwenScope
restart. Standard deps. No model downloads beyond what your CLIP already loaded - training is on the encoder you have.
Common issues
Three real ones. (1) Mismatched counts - positives ≠ negatives throws immediately; count your lines. (2) NaN during training - the error message is blunt about the usual cause: leftover steering hooks from a Steer CLIP contaminating your captures. Run Clear CLIP Steering first. (3) Positives and negatives too similar - if the means are nearly identical the node warns and falls back to a random init; you'll get a direction that means nothing. Fix it with more diverse pairs. And 8–12 good pairs is usually enough; 20+ is diminishing returns, so don't grind out fifty.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| clip | CLIP | — | |
| positive_texts | STRING | Texts that EMBODY the concept (one per line). | |
| negative_texts | STRING | Neutral / opposite texts (same count as positives). | |
| layer | INT | -1-128–127 | Layer to capture residuals at. -1 = last layer (use this if you'll apply via Steer Conditioning). Middle layers (e.g. half of total) work well for Steer CLIP at the same layer. |
| steps | INT | 50050–10000 | — |
| lr | FLOAT | 0.01000.0001–0.1 | — |
| beta | FLOAT | 501–200 | Margin sharpness in the BT loss. Higher = sharper boundary. |
| strength | FLOAT | 1.00-8–8 | — |
| save_nameopt | STRING | If set, also save to lenses/qwenscope/<save_name>.safetensors. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| steer | QSCOPE_STEER | — |
| report | STRING | — |