| # ============================================================================= | |
| # select_features.sh — pick top-k SAE features per language-model layer. | |
| # | |
| # Two modes: | |
| # MODE=probe (default) — top-k by largest positive probe weight. | |
| # PROBES_PATH can be an all-layers state-dict file | |
| # OR a directory containing one .pth file per layer. | |
| # (e.g. mechanistic_interp/probes/probes_gen_bathroom_toilet.pt | |
| # or mechanistic_interp/probes/toilet/mid/) | |
| # One run, one probe → one OBJECT. | |
| # MODE=f1 — top-k by best per-feature F1 against an HF column | |
| # (0/1 label). | |
| # Positives : HF rows where OBJECT column == 1 | |
| # (includes rows where OTHER_OBJECT=1 too). | |
| # Negatives : HF rows where OTHER_OBJECT=1 & OBJECT=0 | |
| # (contrastive, if OTHER_OBJECT is set) | |
| # + random CC3M ids from NEG_JSONL (if set). | |
| # At least one of OTHER_OBJECT or NEG_JSONL is required. | |
| # To process multiple objects in one shot, set | |
| # OBJECTS="bathroom toilet ..." — one JSON per object. | |
| # | |
| # Examples: | |
| # # probe mode (single object: whatever the state-dict was trained for) | |
| # OBJECT=toilet \ | |
| # PROBES_PATH=mechanistic_interp/probes/probes_gen_bathroom_toilet.pt \ | |
| # bash mechanistic_interp/scripts/select_features.sh | |
| # | |
| # # probe mode using per-layer .pth files from Train_Probe_SAE.py output | |
| # # Directory structure: probes/toilet/mid/probe_mid_0.pth, probe_mid_1.pth, ... | |
| # OBJECT=toilet \ | |
| # PROBES_PATH=mechanistic_interp/probes/toilet/mid \ | |
| # bash mechanistic_interp/scripts/select_features.sh | |
| # | |
| # # f1: contrastive HF negatives only (no random CC3M) | |
| # MODE=f1 OBJECT=toilet OTHER_OBJECT=bathroom \ | |
| # HF_DATASET=pbcong/bathroom-toilet \ | |
| # bash mechanistic_interp/scripts/select_features.sh | |
| # | |
| # # f1: contrastive HF negatives + random CC3M negatives | |
| # MODE=f1 OBJECT=toilet OTHER_OBJECT=bathroom \ | |
| # HF_DATASET=pbcong/bathroom-toilet \ | |
| # NEG_JSONL=mechanistic_interp/neg_cc3m_5k.json \ | |
| # bash mechanistic_interp/scripts/select_features.sh | |
| # | |
| # # f1 on a different HF dataset with two new columns (json negatives only) | |
| # MODE=f1 OBJECTS="kitchen fridge" \ | |
| # HF_DATASET=youruser/kitchen-fridge \ | |
| # NEG_JSONL=mechanistic_interp/neg_cc3m_5k.json \ | |
| # bash mechanistic_interp/scripts/select_features.sh | |
| # | |
| # Output: | |
| # ${OUT_DIR}/selected_features_${MODE}_${OBJ}_top${TOP_K}.json (one per object) | |
| # ============================================================================= | |
| set -euo pipefail | |
| cd "$(dirname "$0")/../.." | |
| export PYTHONPATH="$(cd .. && pwd):$(pwd):${PYTHONPATH:-}" | |
| MODE="${MODE:-probe}" | |
| TOP_K="${TOP_K:-20}" | |
| HOOK_TYPE="${HOOK_TYPE:-post}" | |
| DEVICE="${DEVICE:-cuda:4}" | |
| # Object selection. OBJECTS (plural) wins when set; otherwise fall back to | |
| # OBJECT (singular). OBJECT is also what the legacy single-object call uses. | |
| OBJECT="${OBJECT:-bathroom}" | |
| OBJECTS="${OBJECTS:-}" | |
| # Common paths. | |
| OUT_DIR="${OUT_DIR:-mechanistic_interp/probes}" | |
| # probe-mode | |
| PROBES_PATH="${PROBES_PATH:-/data/caotue/SAE_checkpoints/16_d_model/probes_bathroom_epoch10}" | |
| # f1-mode | |
| SAE_CKPT="${SAE_CKPT:-/data/caotue/SAE_checkpoints/16_d_model/last.ckpt}" | |
| HF_DATASET="${HF_DATASET:-pbcong/bathroom-toilet}" | |
| OTHER_OBJECT="${OTHER_OBJECT:-toilet}" # contrastive column (e.g. 'bathroom' when OBJECT=toilet) | |
| NEG_JSONL="${NEG_JSONL:-mechanistic_interp/neg_cc3m_5k.json}" # optional random-CC3M negatives JSON | |
| IMG_BATCH="${IMG_BATCH:-16}" | |
| DTYPE="${DTYPE:-bfloat16}" | |
| # Resolve object list. | |
| if [ -n "${OBJECTS}" ]; then | |
| LIST=( ${OBJECTS} ) | |
| else | |
| LIST=( "${OBJECT}" ) | |
| fi | |
| echo "MODE=${MODE} TOP_K=${TOP_K} objects: ${LIST[*]}" | |
| OK_LIST=() | |
| FAIL_LIST=() | |
| for OBJ in "${LIST[@]}"; do | |
| OUT="${OUT_DIR}/selected_features_${MODE}_${OBJ}_top${TOP_K}.json" | |
| ARGS=( | |
| -m mechanistic_interp.select_features | |
| --mode "${MODE}" | |
| --top_k "${TOP_K}" | |
| --out "${OUT}" | |
| --hook_type "${HOOK_TYPE}" | |
| --label "${OBJ}" | |
| ) | |
| if [ "${MODE}" = "probe" ]; then | |
| ARGS+=( --probes_path "${PROBES_PATH}" ) | |
| else | |
| # f1 mode: --probe_type drives positives (rows where the named HF | |
| # column == 1), --label is just the JSON tag, OUT path is unique per | |
| # object, and each object runs in its own python process so a failure | |
| # on one (e.g. OOM) doesn't block the others. | |
| ARGS+=( | |
| --sae_ckpt "${SAE_CKPT}" | |
| --hf_dataset "${HF_DATASET}" | |
| --probe_type "${OBJ}" | |
| --device "${DEVICE}" | |
| --img_batch "${IMG_BATCH}" | |
| --dtype "${DTYPE}" | |
| ) | |
| if [ -n "${OTHER_OBJECT}" ]; then | |
| ARGS+=( --other_object "${OTHER_OBJECT}" ) | |
| fi | |
| if [ -n "${NEG_JSONL}" ]; then | |
| ARGS+=( --neg_jsonl "${NEG_JSONL}" ) | |
| fi | |
| fi | |
| echo | |
| echo "[$(date +%T)] OBJECT=${OBJ} → ${OUT}" | |
| echo "Running: python ${ARGS[*]}" | |
| if python "${ARGS[@]}"; then | |
| echo "[$(date +%T)] OK ${OBJ}" | |
| OK_LIST+=( "${OBJ}" ) | |
| else | |
| rc=$? | |
| echo "[$(date +%T)] FAIL ${OBJ} (exit ${rc})" | |
| FAIL_LIST+=( "${OBJ}" ) | |
| fi | |
| done | |
| echo | |
| echo "── Summary ────────────────────────────────────────────────" | |
| echo "OK (${#OK_LIST[@]}): ${OK_LIST[*]:-—}" | |
| echo "FAIL (${#FAIL_LIST[@]}): ${FAIL_LIST[*]:-—}" | |
| if [ "${#FAIL_LIST[@]}" -gt 0 ]; then | |
| exit 1 | |
| fi | |