Proyecto independiente No afiliado, patrocinado ni avalado por la Watch Tower Bible and Tract Society o Jehovah's Witnesses.
jw-agent-toolkit
EN

Release F80 · interpretabilidad mecanicista

Probing · Steering · Qwen Scope + Gemma Scope

Cierre del bloque de alineamiento F77–F79 con una pregunta operativa: ¿el modelo internalizó los principios o aprendió un shortcut estilístico? Arquitectura tri-modelo: producción Qwen3.5-0.8B intocada, laboratorio Qwen3.5-2B-Base con Qwen-Scope público y laboratorio Gemma-2-2B-PT con Gemma Scope (JumpReLU SOTA). Cross-family validation: una feature moral que emerge en ambas familias arquitectónicamente distintas es evidencia mucho más robusta que en una sola.

Sub-fases

6

Tests nuevos

+98

Paquete nuevo

jw-interp

Suite total

1411

passing · 0 acoplamiento jw-interp en jw-agents

▌ Decisiones de diseño que ataron las 6 sub-fases

  • Producción Qwen3.5-0.8B nunca se toca. Los SAEs viven en modelos 2B del lab.
  • Tier 4 es observacional: probe miss nunca veta un Finding solo.
  • Cross-family validation: Qwen-Scope + Gemma Scope con interfaz numpy idéntica.
  • Mock capturer como contrato matemático: si no separa eso, el pipeline está roto.
  • Cero acoplamiento: fidelity_wrap acepta callable, no importa jw-interp.
  • torch.load(weights_only=True) siempre — sin pickle exploits desde HF.
  • Probes persistidos como npz + JSON sidecar — sin pickle, forward-compat across sklearn.
  • Extras opt-in: torch para captura real, sae para Gemma Scope.

Fase 80.0

sl-cai-critique-cli

Cerrar el gap aguas arriba: reescribir violaciones antes del SFT

✅ Entregada 🧪 14 tests T1 Cierre de loop
Guía técnica →

Pipeline ya existía en jw_finetune.synth.critique pero sin CLI, sin tests y sin docs. F80.0 cierra el ciclo: el comando build-critique-dataset toma un dataset SFT (ShareGPT), filtra principios por agent_name, ejecuta el tier regex barato, llama al LLM solo cuando hay hard violation, y produce un dataset paralelo con las respuestas revisadas. La respuesta original queda preservada en metadata.original_answer para auditoría. Reduce hard violations en el dataset de entrenamiento del próximo round SFT.

Qué se entregó

  • Comando nuevo jw-finetune build-critique-dataset con flags input/output/provider/model/agent/principles/preserve-original.
  • Regex tier short-circuit: si no hay match, ningún LLM call (coste cero en corpus limpio).
  • Filtrado por applies_to del principio respecto al agent_name.
  • Pipeline programático batch_critique(pairs, principles, llm, agent) → (revised_pairs, num_changed).
  • self_critique(pair, principles, llm_provider, agent, preserve_original) devuelve CritiqueResult con changed + violated_principle_ids + original_answer.
  • 10 tests TDD del módulo + 4 tests CLI dispatch (input missing, output paths, no-principles flag).
  • Fallbacks graciosos: LLM error → original intacto, respuesta vacía → original intacto, mismo texto → changed=False.
  • Guía docs/guias/sl-cai.md con quickstart y cómo se relaciona con F77 + F78 + F79.

Fase 80.1

linear-probing-per-principle

¿Los principios viven en la representación o son shortcut estilístico?

✅ Entregada 🧪 27 tests T2 Análisis
Guía técnica →

Paquete nuevo jw-interp con la maquinaria completa: ContrastiveSpec declarativos para los 5 principios builtin, MockActivationCapturer determinístico (offset por principio × capa × hook) que produce datos linealmente separables, LinearProbe sklearn con stratified split y AUC + accuracy, y TorchActivationCapturer HF forward hooks con auto-device (cuda > mps > cpu). Probe accuracy ≥0.80 en alguna capa = principio internalizado; <0.65 todas las capas = shortcut detectado. Diseño honesto: el mock es un contrato matemático — si la probe no separa eso, el pipeline está roto.

Qué se entregó

  • ContrastivePair / ContrastiveSpec / PrincipleContrastiveBuilder / ProbingDataset con validación de shape.
  • 5 specs builtin seed para PF001/002/003/010/012 (extendibles por el usuario).
  • MockActivationCapturer determinístico con direction hashing por (principle_id, layer, hook), signal_strength configurable, noise_std bajo.
  • TorchActivationCapturer con AutoModelForCausalLM + forward hooks en model.model.layers, last-token / mean pooling, batch_size configurable.
  • LinearProbe sklearn.LogisticRegression con stratified test split, accuracy + AUC, coef + bias persistibles para F80.2 steering.
  • train_probe + train_probes_for_principle (uno por capa).
  • Lazy import torch/transformers — el paquete corre sin GPU para tests y mock.
  • Test end-to-end: probe sobre activaciones mock linealmente separables debe ≥ 0.95 accuracy.
  • 22 tests pure-numpy + 5 tests torch (importorskip).
  • Guía docs/guias/probing.md con quickstart real + síntesis + interpretación de resultados.

Fase 80.2

steering-vectors-activation-patching

Validación causal: correlación no es causa

✅ Entregada 🧪 15 tests T2 Análisis

Si un probe encuentra el principio pero la activación correspondiente no causa la conducta, es shortcut. Steering vectors (diferencia de medias entre positivas y negativas), aplicación al residual con broadcasting, proyección ortogonal (ablación) y evaluación monotónica bajo alpha contra el probe. Activation patching pure-numpy: el core del bucle lo deja listo para el wrap en forward real (torch_patching.py viene en F80.6+). Si +alpha sube probe score y -alpha lo baja → causal; si ninguna dirección cambia → spurious.

Qué se entregó

  • SteeringVector frozen dataclass con vector, magnitude, n_positive/negative.
  • compute_steering_vector(batch, principle_id, normalize=True) usa diferencia de medias, unit-norm por defecto.
  • compute_steering_vectors_for_principle(batches) — uno por capa.
  • apply_steering_to_residual con broadcasting (residual 1D o 2D batch), inmutable.
  • project_out remueve la componente del vector (ablación) — verificado: dot post-proyección ~0.
  • evaluate_steering_effect(batch, vector, probe.predict_proba, alpha) — test de monotonicidad bajo alpha (+ sube neg, − baja neg).
  • patching.PatchedActivation + patch_one / patch_batch / evaluate_patching_effect (pure-numpy).
  • Test: parchear con self → cambio ~0; parchear con flipped labels devuelve effect finito.
  • Rechaza shape/layer mismatches con errores claros.
  • Diseño honesto: la parte de forward-pass real (torch_patching) queda pendiente — no bloquea F80.5.

Fase 80.3

qwen-scope-adapter

Cargar SAE-Res-Qwen3.5-2B-Base-W32K-L0_50 y mapear principios a features

✅ Entregada 🧪 14 tests T3 SAE / runtime

Adapter para los SAEs públicos de Qwen-Scope (TopK k=50 sobre residual stream, 24 capas de Qwen3.5-2B-Base, W32K features). QwenScopeSAE encode con np.argpartition para TopK O(n·d_sae), decode reconstruye residual, reconstruction_error como métrica de fidelidad. Loader usa torch.load(weights_only=True) — sin pickle execution. summarize_feature_activations mapea principios a features candidatas por differential activation rate entre positivos y negativos.

Qué se entregó

  • QwenScopeSAE frozen con W_enc / b_enc / W_dec / b_dec + d_model + d_sae + k.
  • encode TopK puro numpy con argpartition (O(n·d_sae) sin sort completo).
  • decode reconstruye residual; reconstruction_error como MSE.
  • load_qwen_scope_sae(path, layer, k) con torch.load(weights_only=True) — seguro contra pickle exploits.
  • Verifica file exists ANTES de importar torch — FileNotFoundError fast-path sin la dep.
  • summarize_feature_activations devuelve top_n features ordenadas por |rate_pos − rate_neg|.
  • FeatureActivationSummary con layer + feature_idx + rate_pos/neg + mean_pos/neg + differential_rate.
  • Test: TopK conserva las features con pre-activaciones más altas (verificación matemática).
  • 11 tests pure-numpy + 3 tests torch (importorskip): round-trip load/save, missing keys, missing file.
  • Sin dep hard en sae_lens — el adapter usa solo torch para deserializar y numpy para todo lo demás.

Fase 80.4

gemma-scope-wrapper

JumpReLU SAE SOTA + cross-family validation Qwen ⟷ Gemma

✅ Entregada 🧪 7 tests T3 SAE / runtime

Wrapper sobre sae_lens.SAE para los SAEs JumpReLU de Gemma Scope (gemma-2-2b PT y gemma-2-9b PT, sites resid_post + mlp_out + attn_out, todas las capas, widths 16k–262k). Interfaz numpy idéntica a QwenScopeSAE para que F80.3 y F80.4 sean intercambiables aguas abajo. La validación cross-family es el punto importante: una feature moral que emerge en ambas familias arquitectónicamente distintas es evidencia mucho más robusta que en una sola. Si las features no coinciden, ese resultado mismo es informativo.

Qué se entregó

  • GemmaScopeSAE frozen con layer + site (resid_post|mlp_out|attn_out) + d_model + d_sae + _inner SAELens object.
  • encode / decode atraviesan sae_lens.SAE pero la interfaz pública es numpy in/numpy out.
  • _RELEASE_MAP declarativo para gemma-2-2b/-9b × resid/mlp/attn → SAELens release id.
  • _resolve_release(model_name, site) con error claro si combinación no registrada.
  • load_gemma_scope_sae(model_name, site, layer, width, l0, device) con lazy import de sae_lens.
  • summarize_gemma_features reusa el summarizer de QwenScopeSAE (mismo contrato numpy).
  • _FakeSAELensSAE en los tests para evitar dep sae_lens en CI; tests usan importorskip torch.
  • Test que verifica el error message correcto cuando sae_lens no está instalado.

Fase 80.5

runtime-probe-store-tier4

Probes persistidos + fidelity_wrap Tier 4 observacional

✅ Entregada 🧪 21 tests T1 Cierre de loop
Guía técnica →

Cierre del loop F80 al runtime. probe_store usa np.savez_compressed + JSON sidecar — sin pickle, forward-compat across sklearn versions. RuntimeProbe.predict_proba implementa sigmoid numpy numéricamente estable (matchea sklearn predict_proba a 1e-5). ProbeEvaluator con dos modos: eager (vía TorchActivationCapturer, una forward por finding) y cache-only (acepta activaciones pre-capturadas). El Tier 4 en fidelity_wrap es deliberadamente observacional — NUNCA veta un Finding por sí solo, solo anota probe_scores + probe_misses + probe_coherence (clear|confirms|conflicts|silent). Coupling cero: el tipo ProbeEvaluatorCallable vive en jw-agents, no importa jw-interp.

Qué se entregó

  • save_probe / load_probe con np.savez_compressed + JSON sidecar (forward-compat, sin pickle).
  • save_probe_set / load_probe_set con manifest.json (model_name + hidden_size + n_layers + version).
  • RuntimeProbe.predict_proba con sigmoid numéricamente estable (paths separados para x ≥ 0 y x < 0).
  • Test matemático: RuntimeProbe.predict_proba matchea sklearn LogisticRegression.predict_proba a 1e-5.
  • ProbeEvaluator con __call__ eager (un forward por finding) y score_cached (activaciones pre-capturadas).
  • build_probe_evaluator(probes_dir, capturer, model_name) auto-construye capturer del manifest si torch está.
  • mock_evaluator(returns) para tests deterministas sin GPU.
  • fidelity_wrap acepta probe_evaluator: Callable[[str], dict[str, float]] + probe_min_score (default 0.5).
  • Por Finding: probe_scores (JSON), probe_misses (CSV), probe_min_score, probe_coherence en metadata.
  • Coherence categories: clear (nada flag, todo internalizado), confirms (regex + probe coinciden), conflicts (regex flag pero probe alto), silent (regex limpio pero probe miss — shortcut sospechoso).
  • Tier 4 errors swallowed → probe_error en metadata (un evaluador roto no puede tumbar producción).
  • Test crítico: aún con TODOS los probes en 0, el Finding NO se descarta (on_fail='reject' irrelevante para probes).
  • 14 tests probe_store + runtime + 7 tests Tier 4 en jw-agents (cero regresión en los 930 existentes).
  • Guía docs/guias/interpretabilidad-runtime.md con quickstart eager + cached + integración fidelity_wrap.

▌ Flujo end-to-end: del entrenamiento a Tier 4 en runtime

Las 6 sub-fases componen un pipeline coherente sobre Qwen3.5-0.8B + lab Qwen 2B + lab Gemma 2B:

# 1. F80.0 — SL-CAI critique sobre el dataset SFT antes del fine-tune
uv run jw-finetune build-critique-dataset \
    --workspace ws-sft \
    --synth-provider anthropic \
    --synth-model claude-haiku-4-5-20251001

# 2. F77-F79 — entrenamiento DPO/ORPO sobre dataset revisado (sin cambios)
uv run jw-finetune train --workspace ws-dpo

# 3. F80.1 — entrenar probes sobre el modelo fine-tuneado
uv sync --extra torch
python -c "
from jw_interp import (
    PrincipleContrastiveBuilder, TorchActivationCapturer,
    build_default_contrastive_specs, train_probes_for_principle,
    save_probe_set, ProbeStoreManifest,
)
cap = TorchActivationCapturer('Qwen/Qwen3.5-0.8B')
builder = PrincipleContrastiveBuilder(build_default_contrastive_specs())
results = []
for pid in builder.principle_ids:
    batches = cap.capture(builder.build(pid), layers=list(range(0, 24, 4)))
    results.extend(train_probes_for_principle(batches, pid))
save_probe_set(results, '~/jw-probes/v1', ProbeStoreManifest(
    model_name='Qwen/Qwen3.5-0.8B',
    hidden_size=cap.hidden_size,
    n_layers=cap.n_layers))
"

# 4. F80.5 — enchufar Tier 4 en producción
python -c "
from jw_interp.runtime import build_probe_evaluator
from jw_agents.fidelity_wrap import fidelity_wrap
from jw_eval.principles import load_principles

evaluator = build_probe_evaluator(probes_dir='~/jw-probes/v1')

@fidelity_wrap(
    on_fail='warn',
    principles=load_principles(),
    probe_evaluator=evaluator,
    probe_min_score=0.5,
)
async def apologetics(query: str): ...
"