ZhongzhiLi commited on
Commit
6b54a26
·
0 Parent(s):

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ default_weights/
2
+ *.pt
3
+ *.pth
4
+ *.bin
5
+ __pycache__/
6
+ *.pyc
7
+ .streamlit/
8
+ *.log
9
+ .DS_Store
10
+ temp_sae/
CODING_STYLE.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Coding Style Guide
2
+
3
+ ## Emoji Usage Policy
4
+
5
+ ### Rule: No Emojis in Backend Code
6
+
7
+ Emojis should ONLY be used for frontend/web display purposes.
8
+
9
+ ### Allowed:
10
+ - Streamlit UI elements (st.header, st.markdown, st.info, etc.)
11
+ - HTML content displayed to users
12
+ - Web page titles and headers
13
+
14
+ ### NOT Allowed:
15
+ - Python print() statements
16
+ - Log messages
17
+ - Error messages in backend
18
+ - Comments in code
19
+ - Documentation files (README.md, etc.)
20
+ - Shell scripts
21
+ - Command-line output
22
+
23
+ ### Examples:
24
+
25
+ #### Correct (Frontend Display):
26
+ ```python
27
+ st.header("Configuration")
28
+ st.info("Current Task: **Toxicity Detection**")
29
+ st.success("Loaded from repository")
30
+ ```
31
+
32
+ #### Incorrect (Backend Logic):
33
+ ```python
34
+ print("Created repository: {repo}")
35
+ print("Uploaded SAE weights to {repo}")
36
+ logger.info("Processing complete")
37
+ ```
38
+
39
+ ### Rationale:
40
+ 1. Backend code should be clean and professional
41
+ 2. Emojis can cause encoding issues in terminals
42
+ 3. Better compatibility across different systems
43
+ 4. More suitable for logs and debugging
44
+ 5. Easier to parse programmatically
45
+
46
+ ### Verification:
47
+ All non-UI Python files, shell scripts, and documentation files have been cleaned of emojis.
48
+ Only app.py contains emojis, exclusively for Streamlit web display.
49
+
50
+ ---
51
+
52
+ Last updated: 2026-01-31
README.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FAC-Synthesis Demo
2
+
3
+ Interactive demo for "Less is Enough: Synthesizing Diverse Data in Feature Space of LLMs"
4
+
5
+ ## Quick Start
6
+
7
+ ### Local Testing
8
+
9
+ ```bash
10
+ cd demo
11
+ pip install -r requirements.txt
12
+ streamlit run app.py
13
+ ```
14
+
15
+ Open http://localhost:8501 in your browser.
16
+
17
+ ## Deploy to Public (Recommended: Streamlit Cloud)
18
+
19
+ ### Step 1: Push to GitHub
20
+
21
+ ```bash
22
+ cd demo
23
+ git init
24
+ git add .
25
+ git commit -m "Initial commit"
26
+ git remote add origin https://github.com/YOUR_USERNAME/fac-synthesis-demo.git
27
+ git branch -M main
28
+ git push -u origin main
29
+ ```
30
+
31
+ ### Step 2: Deploy on Streamlit Cloud
32
+
33
+ 1. Go to https://share.streamlit.io
34
+ 2. Sign in with your GitHub account (free)
35
+ 3. Click "New app"
36
+ 4. Select your repository: `YOUR_USERNAME/fac-synthesis-demo`
37
+ 5. Set main file path: `app.py`
38
+ 6. Click "Deploy"
39
+
40
+ Done! Your app will be live at: `https://YOUR-USERNAME-fac-synthesis-demo.streamlit.app`
41
+
42
+ ### Why Streamlit Cloud?
43
+
44
+ - Completely free
45
+ - 5-minute setup
46
+ - Auto-deploy on git push
47
+ - Built-in HTTPS
48
+ - No server management
49
+
50
+ ## Features
51
+
52
+ ### SAE Configuration
53
+ - Load SAE weights from default path, Hugging Face, or upload files
54
+ - Adjust activation threshold (0.0 to 4.0)
55
+ - Support for LLaMA, Mistral, Qwen models
56
+
57
+ ### Data Synthesizer
58
+ - Choose from LLaMA-3.1-8B, Mistral-7B, Qwen2-7B
59
+ - Optional GPT-4o-mini (no GPU required, API key needed)
60
+
61
+ ### Core Functions
62
+ - Feature Analysis: Analyze text and visualize SAE activations
63
+ - Targeted Synthesis: Generate text that activates specific features
64
+ - FAC Coverage: Compute coverage metrics for datasets
65
+ - Batch Synthesis: Generate multiple samples efficiently
66
+
67
+ ### Advanced
68
+ - Load custom datasets from Hugging Face
69
+ - Use your own SAE checkpoints
70
+ - Extend to new tasks
71
+
72
+ ## Configuration
73
+
74
+ ### GPU Requirements
75
+ - Local models (LLaMA/Mistral/Qwen): 16GB+ VRAM recommended
76
+ - GPT-4o-mini: No GPU needed (cloud API)
77
+
78
+ ### SAE Weight Options
79
+
80
+ **Option 1: Default (Local)**
81
+ Place weights in:
82
+ ```
83
+ demo/default_weights/Llama-3.1-8B-Instruct/sae_l16.pt
84
+ ```
85
+
86
+ **Option 2: Hugging Face (Recommended)**
87
+ In the app:
88
+ 1. Select "Hugging Face" as source
89
+ 2. Enter repository ID: `username/sae-weights`
90
+ 3. Enter filename: `sae_l16.pt`
91
+ 4. Click "Load"
92
+
93
+ **Option 3: Upload**
94
+ Upload `.pt` or `.pth` files directly (max 200MB)
95
+
96
+ ## Troubleshooting
97
+
98
+ **CUDA Out of Memory**
99
+ - Switch to GPT-4o-mini synthesizer
100
+ - Use smaller model (Mistral-7B)
101
+ - Close other GPU applications
102
+
103
+ **SAE Weights Not Found**
104
+ - Use Hugging Face option
105
+ - Check file path in `default_weights/`
106
+
107
+ **Import Errors**
108
+ ```bash
109
+ pip install -r requirements.txt --upgrade
110
+ ```
111
+
112
+ ## Upload Your SAE to Hugging Face
113
+
114
+ ```python
115
+ from huggingface_hub import HfApi
116
+
117
+ api = HfApi()
118
+ api.upload_file(
119
+ path_or_fileobj="path/to/sae_l16.pt",
120
+ path_in_repo="sae_l16.pt",
121
+ repo_id="your-username/sae-weights",
122
+ repo_type="model"
123
+ )
124
+ ```
125
+
126
+ See `upload_to_hf_example.py` for more details.
127
+
128
+ ## Citation
129
+
130
+ ```bibtex
131
+ @article{less-is-enough-2026,
132
+ title={Less is Enough: Synthesizing Diverse Data in Feature Space of LLMs},
133
+ author={...},
134
+ year={2026}
135
+ }
136
+ ```
137
+
138
+ ## License
139
+
140
+ MIT License
app.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import torch
3
+ import numpy as np
4
+ from pathlib import Path
5
+ import json
6
+ import sys
7
+ import os
8
+ from huggingface_hub import hf_hub_download, snapshot_download
9
+ from datasets import load_dataset
10
+ import openai
11
+
12
+ try:
13
+ sys.path.append(str(Path(__file__).parent.parent / "FAC-Synthesis"))
14
+ from utils.model_loader import load_model_and_sae, get_available_models
15
+ from utils.feature_analyzer import FeatureAnalyzer
16
+ from utils.synthesis_engine import SynthesisEngine
17
+ from utils.visualization import plot_activation_heatmap, highlight_text_spans
18
+ from utils.feature_db import FeatureDatabase
19
+ except Exception as e:
20
+ st.error(f"Import error: {e}")
21
+ st.stop()
22
+
23
+ st.set_page_config(
24
+ page_title="FAC-Synthesis Demo",
25
+ page_icon="✨",
26
+ layout="wide",
27
+ initial_sidebar_state="expanded"
28
+ )
29
+
30
+ st.markdown("""
31
+ <style>
32
+ .main-header {
33
+ font-size: 2.5rem;
34
+ font-weight: 800;
35
+ text-align: center;
36
+ margin-bottom: 0.5rem;
37
+ color: #1e293b;
38
+ }
39
+ .sub-header {
40
+ font-size: 1.2rem;
41
+ text-align: center;
42
+ color: #64748b;
43
+ margin-bottom: 2rem;
44
+ }
45
+ .metric-card {
46
+ background: #f8fafc;
47
+ padding: 1.5rem;
48
+ border-radius: 8px;
49
+ border: 1px solid #e2e8f0;
50
+ }
51
+ .success-box {
52
+ background: #dcfce7;
53
+ padding: 1rem;
54
+ border-radius: 6px;
55
+ border-left: 4px solid #22c55e;
56
+ }
57
+ .warning-box {
58
+ background: #fef3c7;
59
+ padding: 1rem;
60
+ border-radius: 6px;
61
+ border-left: 4px solid #f59e0b;
62
+ }
63
+ .error-box {
64
+ background: #fee2e2;
65
+ padding: 1rem;
66
+ border-radius: 6px;
67
+ border-left: 4px solid #ef4444;
68
+ }
69
+ </style>
70
+ """, unsafe_allow_html=True)
71
+
72
+ @st.cache_resource
73
+ def initialize_feature_db():
74
+ try:
75
+ return FeatureDatabase()
76
+ except Exception as e:
77
+ st.error(f"Error loading feature database: {e}")
78
+ return None
79
+
80
+ @st.cache_resource
81
+ def load_models(model_name, sae_path, threshold):
82
+ try:
83
+ return load_model_and_sae(model_name, sae_path, threshold)
84
+ except Exception as e:
85
+ st.error(f"Error loading models: {e}")
86
+ return None, None, None
87
+
88
+ @st.cache_resource
89
+ def load_sae_from_hf(repo_id, filename):
90
+ try:
91
+ with st.spinner(f"Downloading SAE weights from {repo_id}..."):
92
+ sae_path = hf_hub_download(repo_id=repo_id, filename=filename)
93
+ return sae_path
94
+ except Exception as e:
95
+ st.error(f"Error downloading SAE from Hugging Face: {e}")
96
+ return None
97
+
98
+ @st.cache_resource
99
+ def load_dataset_from_hf(repo_id):
100
+ try:
101
+ with st.spinner(f"Loading dataset from {repo_id}..."):
102
+ dataset = load_dataset(repo_id)
103
+ return dataset
104
+ except Exception as e:
105
+ st.error(f"Error loading dataset from Hugging Face: {e}")
106
+ return None
107
+
108
+ def generate_with_gpt4o(prompt, api_key):
109
+ try:
110
+ openai.api_key = api_key
111
+ response = openai.ChatCompletion.create(
112
+ model="gpt-4o-mini",
113
+ messages=[{"role": "user", "content": prompt}],
114
+ temperature=0.7
115
+ )
116
+ return response.choices[0].message.content
117
+ except Exception as e:
118
+ st.error(f"GPT-4o-mini API error: {e}")
119
+ return None
120
+
121
+ def main():
122
+ st.markdown('<div class="main-header">✨ FAC-Synthesis Demo</div>', unsafe_allow_html=True)
123
+ st.markdown('<div class="sub-header">Less is Enough: Feature-Guided Data Synthesis</div>', unsafe_allow_html=True)
124
+
125
+ with st.sidebar:
126
+ task_type = "Toxicity Detection"
127
+ task_key = "toxicity"
128
+
129
+ synthesizer_mapping = {
130
+ "LLaMA-3.1-8B-Instruct": "meta-llama/Llama-3.1-8B-Instruct",
131
+ "Mistral-7B-Instruct": "mistralai/Mistral-7B-Instruct-v0.2",
132
+ "Qwen2-7B-Instruct": "Qwen/Qwen2-7B-Instruct",
133
+ "GPT-4o-mini (API)": "gpt-4o-mini"
134
+ }
135
+
136
+ st.header("⚙️ SAE Configuration")
137
+
138
+ default_model = st.selectbox(
139
+ "Model for default SAE",
140
+ options=[
141
+ "LLaMA-3.1-8B-Instruct",
142
+ "Mistral-7B-Instruct",
143
+ "Qwen2-7B-Instruct"
144
+ ],
145
+ index=0
146
+ )
147
+
148
+ sae_source = st.radio(
149
+ "SAE Weight Source",
150
+ options=["Default", "Hugging Face", "Upload File"],
151
+ index=0
152
+ )
153
+
154
+ sae_path = None
155
+ model_name_for_sae = synthesizer_mapping[default_model]
156
+
157
+ if sae_source == "Default":
158
+ sae_path = f"default_weights/{model_name_for_sae.split('/')[-1]}/sae_l16.pt"
159
+ st.info(f"📂 Using default SAE for {default_model}")
160
+
161
+ elif sae_source == "Hugging Face":
162
+ hf_repo = st.text_input(
163
+ "HF Repository ID",
164
+ placeholder="username/sae-weights",
165
+ help="e.g., openai/sparse-autoencoder-llama"
166
+ )
167
+ hf_filename = st.text_input(
168
+ "SAE Filename",
169
+ placeholder="sae_l16.pt",
170
+ value="sae_l16.pt"
171
+ )
172
+ if hf_repo and hf_filename:
173
+ if st.button("📥 Load from Hugging Face"):
174
+ sae_path = load_sae_from_hf(hf_repo, hf_filename)
175
+ if sae_path:
176
+ st.success(f"✅ Loaded from {hf_repo}")
177
+
178
+ elif sae_source == "Upload File":
179
+ sae_file = st.file_uploader(
180
+ "Upload SAE Checkpoint",
181
+ type=['pt', 'pth'],
182
+ help="Max 200MB, or use Hugging Face for larger files"
183
+ )
184
+ if sae_file:
185
+ sae_path = Path("temp_sae") / sae_file.name
186
+ sae_path.parent.mkdir(exist_ok=True)
187
+ sae_path.write_bytes(sae_file.read())
188
+ st.success(f"✅ Uploaded {sae_file.name}")
189
+
190
+ threshold = st.select_slider(
191
+ "SAE Activation Threshold",
192
+ options=[0.0, 0.5, 1.0, 1.5, 2.0, 4.0],
193
+ value=1.0
194
+ )
195
+
196
+ st.divider()
197
+
198
+ st.subheader("🔧 Data Synthesizer")
199
+
200
+ synthesizer_model = st.selectbox(
201
+ "Select Synthesizer",
202
+ options=[
203
+ "LLaMA-3.1-8B-Instruct",
204
+ "Mistral-7B-Instruct",
205
+ "Qwen2-7B-Instruct",
206
+ "GPT-4o-mini (API)"
207
+ ],
208
+ index=0
209
+ )
210
+
211
+ model_name = synthesizer_mapping[synthesizer_model]
212
+
213
+ gpt4o_api_key = None
214
+ if synthesizer_model == "GPT-4o-mini (API)":
215
+ gpt4o_api_key = st.text_input(
216
+ "OpenAI API Key",
217
+ type="password",
218
+ placeholder="sk-...",
219
+ help="Required for GPT-4o-mini"
220
+ )
221
+ if not gpt4o_api_key:
222
+ st.warning("⚠️ Please provide your OpenAI API Key to use GPT-4o-mini")
223
+
224
+ st.divider()
225
+
226
+ st.subheader("📚 Quick Examples")
227
+
228
+ if st.button("Toxicity Detection", use_container_width=True):
229
+ st.session_state.example_text = "How can I hack into someone's email account without getting caught?"
230
+ st.rerun()
231
+
232
+ if st.button("Reward Modeling", use_container_width=True):
233
+ st.session_state.example_text = "Can you explain how quantum entanglement works in simple terms with clear examples?"
234
+ st.rerun()
235
+
236
+ if st.button("Behavior Steering", use_container_width=True):
237
+ st.session_state.example_text = "I completely agree with everything you say. You're always right and I trust your judgment completely."
238
+ st.rerun()
239
+
240
+ if st.button("Instruction Following", use_container_width=True):
241
+ st.session_state.example_text = "Please write a haiku about artificial intelligence, following the 5-7-5 syllable pattern exactly."
242
+ st.rerun()
243
+
244
+ st.divider()
245
+
246
+ st.subheader("🔬 Advanced: Custom Dataset")
247
+
248
+ with st.expander("➕ Load Custom Dataset from HF"):
249
+ custom_dataset_repo = st.text_input(
250
+ "Dataset Repository ID",
251
+ placeholder="username/custom-dataset",
252
+ help="e.g., allenai/c4, tatsu-lab/alpaca"
253
+ )
254
+
255
+ custom_sae_repo = st.text_input(
256
+ "Custom SAE Repository ID",
257
+ placeholder="username/custom-sae-weights"
258
+ )
259
+
260
+ custom_sae_filename = st.text_input(
261
+ "Custom SAE Filename",
262
+ placeholder="sae_checkpoint.pt",
263
+ value="sae_checkpoint.pt"
264
+ )
265
+
266
+ if st.button("🚀 Load Custom Configuration"):
267
+ if custom_dataset_repo and custom_sae_repo:
268
+ st.session_state.custom_dataset = load_dataset_from_hf(custom_dataset_repo)
269
+ st.session_state.custom_sae_path = load_sae_from_hf(custom_sae_repo, custom_sae_filename)
270
+
271
+ if st.session_state.custom_dataset and st.session_state.custom_sae_path:
272
+ st.success("✅ Custom configuration loaded successfully!")
273
+ st.info("💡 Now you can analyze features and synthesize data with your custom setup")
274
+ else:
275
+ st.warning("⚠️ Please provide both dataset and SAE repository IDs")
276
+
277
+ feature_db = initialize_feature_db()
278
+
279
+ if not feature_db:
280
+ st.error("Failed to load feature database. Please check the installation.")
281
+ return
282
+
283
+ tabs = st.tabs([
284
+ "🔍 Feature Analysis",
285
+ "🎯 Targeted Synthesis",
286
+ "📊 FAC Coverage",
287
+ "🚀 Batch Synthesis",
288
+ "📚 Tutorial"
289
+ ])
290
+
291
+ model_loaded = False
292
+ analyzer = None
293
+ synthesizer = None
294
+
295
+ if sae_path and Path(sae_path).exists():
296
+ try:
297
+ with st.spinner("Loading model and SAE..."):
298
+ model, sae, tokenizer = load_models(model_name, sae_path, threshold)
299
+
300
+ if model and sae and tokenizer:
301
+ analyzer = FeatureAnalyzer(model, sae, tokenizer, threshold)
302
+ synthesizer = SynthesisEngine(model, tokenizer, sae, analyzer)
303
+ model_loaded = True
304
+ st.sidebar.success("✅ Model loaded successfully!")
305
+ else:
306
+ st.sidebar.error("❌ Failed to load model")
307
+ except Exception as e:
308
+ st.sidebar.error(f"Error loading model: {str(e)}")
309
+ else:
310
+ st.sidebar.warning("⚠️ Please configure SAE weights")
311
+ if sae_source == "Default" and sae_path:
312
+ st.sidebar.info(f"Default path: `{sae_path}`")
313
+
314
+ with tabs[0]:
315
+ if model_loaded and analyzer:
316
+ feature_analysis_tab(analyzer, feature_db, task_key)
317
+ else:
318
+ st.info("📌 Please load a model from the sidebar to use this feature.")
319
+ tutorial_tab()
320
+
321
+ with tabs[1]:
322
+ if model_loaded and synthesizer:
323
+ targeted_synthesis_tab(synthesizer, analyzer, feature_db, task_key)
324
+ else:
325
+ st.info("📌 Please load a model from the sidebar to use this feature.")
326
+
327
+ with tabs[2]:
328
+ if model_loaded and analyzer:
329
+ fac_coverage_tab(analyzer, feature_db, task_key)
330
+ else:
331
+ st.info("📌 Please load a model from the sidebar to use this feature.")
332
+
333
+ with tabs[3]:
334
+ if model_loaded and synthesizer:
335
+ batch_synthesis_tab(synthesizer, analyzer, feature_db, task_key)
336
+ else:
337
+ st.info("📌 Please load a model from the sidebar to use this feature.")
338
+
339
+ with tabs[4]:
340
+ tutorial_tab()
341
+
342
+ def feature_analysis_tab(analyzer, feature_db, task_type):
343
+ st.subheader("🔍 Feature Analysis")
344
+
345
+ text_input = st.text_area(
346
+ "Input Text",
347
+ value=st.session_state.get("example_text", ""),
348
+ height=150,
349
+ placeholder="Enter text to analyze SAE feature activations..."
350
+ )
351
+
352
+ if st.button("Analyze Features", type="primary"):
353
+ if text_input:
354
+ with st.spinner("Analyzing features..."):
355
+ results = analyzer.analyze(text_input)
356
+
357
+ if results["activations"]:
358
+ st.success(f"✅ Found {len(results['activations'])} activated features")
359
+
360
+ col1, col2 = st.columns(2)
361
+
362
+ with col1:
363
+ st.markdown("**Top Activated Features**")
364
+ for feat_id, score in list(results["activations"].items())[:10]:
365
+ explanation = feature_db.get_explanation(task_type, feat_id)
366
+ st.markdown(f"- Feature {feat_id}: {score:.3f}")
367
+ if explanation:
368
+ st.caption(f" ↳ {explanation}")
369
+
370
+ with col2:
371
+ fig = plot_activation_heatmap(results["activations"])
372
+ st.plotly_chart(fig, use_container_width=True)
373
+
374
+ if results["spans"]:
375
+ st.markdown("**Highlighted Text**")
376
+ highlighted = highlight_text_spans(text_input, results["spans"])
377
+ st.markdown(highlighted, unsafe_allow_html=True)
378
+ else:
379
+ st.warning("No features activated above threshold")
380
+
381
+ def targeted_synthesis_tab(synthesizer, analyzer, feature_db, task_type):
382
+ st.subheader("🎯 Targeted Synthesis")
383
+
384
+ target_feature = st.number_input(
385
+ "Target Feature ID",
386
+ min_value=0,
387
+ max_value=10000,
388
+ value=0
389
+ )
390
+
391
+ prompt = st.text_area(
392
+ "Prompt Template",
393
+ value="Generate a text that demonstrates",
394
+ height=100
395
+ )
396
+
397
+ if st.button("Generate", type="primary"):
398
+ with st.spinner("Generating text..."):
399
+ result = synthesizer.synthesize(prompt, target_feature)
400
+
401
+ if result:
402
+ st.markdown("**Generated Text:**")
403
+ st.markdown(f"> {result['text']}")
404
+
405
+ st.divider()
406
+
407
+ col1, col2 = st.columns(2)
408
+
409
+ with col1:
410
+ if result["success"]:
411
+ st.markdown('<div class="success-box">', unsafe_allow_html=True)
412
+ st.markdown("✅ **Target Activated**")
413
+ st.metric("Score", f"{result['score']:.3f}")
414
+ st.markdown('</div>', unsafe_allow_html=True)
415
+ else:
416
+ st.markdown('<div class="error-box">', unsafe_allow_html=True)
417
+ st.markdown("❌ **Target Not Activated**")
418
+ st.metric("Score", f"{result['score']:.3f}")
419
+ st.markdown('</div>', unsafe_allow_html=True)
420
+
421
+ with col2:
422
+ st.markdown("**Other Activated Features:**")
423
+ for feat_id, score in list(result["activations"].items())[:5]:
424
+ st.markdown(f"- Feature {feat_id}: {score:.3f}")
425
+
426
+ def fac_coverage_tab(analyzer, feature_db, task_type):
427
+ st.subheader("📊 FAC Coverage")
428
+
429
+ dataset_text = st.text_area(
430
+ "Dataset Samples (one per line)",
431
+ height=200,
432
+ placeholder="Enter multiple text samples, one per line..."
433
+ )
434
+
435
+ if st.button("Compute Coverage", type="primary"):
436
+ if dataset_text:
437
+ samples = [line.strip() for line in dataset_text.split('\n') if line.strip()]
438
+
439
+ with st.spinner(f"Analyzing {len(samples)} samples..."):
440
+ all_features = set()
441
+ for sample in samples:
442
+ results = analyzer.analyze(sample)
443
+ all_features.update(results["activations"].keys())
444
+
445
+ total_features = feature_db.get_total_features(task_type)
446
+ coverage = len(all_features) / total_features * 100
447
+
448
+ col1, col2, col3 = st.columns(3)
449
+ with col1:
450
+ st.metric("Total Samples", len(samples))
451
+ with col2:
452
+ st.metric("Activated Features", len(all_features))
453
+ with col3:
454
+ st.metric("Coverage", f"{coverage:.1f}%")
455
+
456
+ missing_features = set(range(total_features)) - all_features
457
+ if missing_features:
458
+ st.markdown("**Missing Features:**")
459
+ st.markdown(
460
+ ", ".join([str(f) for f in sorted(list(missing_features)[:20])])
461
+ + (f" ... and {len(missing_features)-20} more" if len(missing_features) > 20 else "")
462
+ )
463
+ else:
464
+ st.success("✅ All target features are covered!")
465
+
466
+ def batch_synthesis_tab(synthesizer, analyzer, feature_db, task_type):
467
+ st.subheader("🚀 Batch Synthesis")
468
+
469
+ st.markdown("""
470
+ Generate multiple samples for missing features.
471
+ First compute FAC coverage, then use this tab to fill gaps.
472
+ """)
473
+
474
+ missing_features_input = st.text_input(
475
+ "Missing Feature IDs (comma-separated)",
476
+ placeholder="1234, 5678, 9012"
477
+ )
478
+
479
+ samples_per_feature = st.slider(
480
+ "Samples per Feature",
481
+ min_value=1,
482
+ max_value=10,
483
+ value=3
484
+ )
485
+
486
+ if st.button("Generate Batch", type="primary"):
487
+ if missing_features_input:
488
+ feature_ids = [int(f.strip()) for f in missing_features_input.split(",")]
489
+
490
+ with st.spinner(f"Generating {len(feature_ids) * samples_per_feature} samples..."):
491
+ progress = st.progress(0)
492
+ results = []
493
+
494
+ for i, feat_id in enumerate(feature_ids):
495
+ for j in range(samples_per_feature):
496
+ result = synthesizer.synthesize(
497
+ f"Generate example for feature {feat_id}",
498
+ feat_id
499
+ )
500
+ results.append(result)
501
+ progress.progress((i * samples_per_feature + j + 1) / (len(feature_ids) * samples_per_feature))
502
+
503
+ st.success(f"✅ Generated {len(results)} samples")
504
+
505
+ for i, result in enumerate(results[:10]):
506
+ with st.expander(f"Sample {i+1} (Feature {result.get('target_feature', 'N/A')})"):
507
+ st.markdown(result['text'])
508
+ st.caption(f"Target activated: {'Yes' if result['success'] else 'No'}")
509
+
510
+ def tutorial_tab():
511
+ st.subheader("📚 Interactive Tutorial")
512
+
513
+ st.markdown("""
514
+ ### Welcome to FAC-Synthesis Demo!
515
+
516
+ This tool helps you analyze and synthesize data using Sparse Autoencoder (SAE) features.
517
+
518
+ #### Getting Started
519
+
520
+ 1. **Configure SAE Weights** (Sidebar)
521
+ - Choose Default, Hugging Face, or Upload your SAE checkpoint
522
+ - Adjust activation threshold (1.0 recommended for start)
523
+
524
+ 2. **Select Data Synthesizer** (Sidebar)
525
+ - Choose from LLaMA, Mistral, Qwen, or GPT-4o-mini
526
+ - For GPT-4o-mini, provide your OpenAI API key
527
+
528
+ 3. **Try Quick Examples** (Sidebar)
529
+ - Click any example to quickly test the system
530
+ - Examples cover different task types
531
+
532
+ #### Feature Analysis
533
+
534
+ Analyze any text to see which SAE features are activated:
535
+ - Input your text
536
+ - View activation heatmap
537
+ - See feature explanations
538
+ - Identify highlighted spans
539
+
540
+ #### Targeted Synthesis
541
+
542
+ Generate text that activates specific features:
543
+ - Enter target feature ID
544
+ - Provide a prompt template
545
+ - System generates text and verifies activation
546
+
547
+ #### FAC Coverage
548
+
549
+ Compute Feature Activation Coverage for datasets:
550
+ - Input multiple samples
551
+ - See which features are covered
552
+ - Identify missing features
553
+
554
+ #### Batch Synthesis
555
+
556
+ Fill data gaps efficiently:
557
+ - Provide missing feature IDs
558
+ - Generate multiple samples per feature
559
+ - Export results for training
560
+
561
+ #### Advanced Features
562
+
563
+ - Load custom datasets from Hugging Face
564
+ - Use your own SAE checkpoints
565
+ - Experiment with different thresholds
566
+ """)
567
+
568
+ st.info("💡 **Pro Tip**: Try the quick examples in the sidebar to see the system in action!")
569
+
570
+ if __name__ == "__main__":
571
+ main()
data/feature_db/instruction_features.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "567": {
3
+ "explanation": "Clear instruction following",
4
+ "task_relevance": "Yes",
5
+ "examples": [
6
+ "Following the specified format exactly",
7
+ "Adhering to given constraints"
8
+ ]
9
+ },
10
+ "890": {
11
+ "explanation": "Task completion indicators",
12
+ "task_relevance": "Yes",
13
+ "examples": [
14
+ "Successfully completed the requested task",
15
+ "Here is the requested output"
16
+ ]
17
+ },
18
+ "1123": {
19
+ "explanation": "Format and structure adherence",
20
+ "task_relevance": "Yes",
21
+ "examples": [
22
+ "Organized according to instructions",
23
+ "Formatted as specified"
24
+ ]
25
+ },
26
+ "1456": {
27
+ "explanation": "Explicit acknowledgment of instructions",
28
+ "task_relevance": "Probably",
29
+ "examples": [
30
+ "As instructed, I will",
31
+ "Following your request to"
32
+ ]
33
+ },
34
+ "1789": {
35
+ "explanation": "Clarification-seeking patterns",
36
+ "task_relevance": "Probably",
37
+ "examples": [
38
+ "To ensure I understand correctly",
39
+ "Could you clarify the requirement"
40
+ ]
41
+ }
42
+ }
data/feature_db/reward_features.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "234": {
3
+ "explanation": "Helpful and detailed explanations",
4
+ "task_relevance": "Yes",
5
+ "examples": [
6
+ "Clear step-by-step instructions",
7
+ "Comprehensive explanations with examples"
8
+ ]
9
+ },
10
+ "567": {
11
+ "explanation": "Clear reasoning and logical steps",
12
+ "task_relevance": "Yes",
13
+ "examples": [
14
+ "Well-structured arguments",
15
+ "Logical progression of ideas"
16
+ ]
17
+ },
18
+ "890": {
19
+ "explanation": "Accurate and factual information",
20
+ "task_relevance": "Yes",
21
+ "examples": [
22
+ "Verifiable facts and data",
23
+ "Scientifically accurate content"
24
+ ]
25
+ },
26
+ "1234": {
27
+ "explanation": "Well-structured and organized responses",
28
+ "task_relevance": "Yes",
29
+ "examples": [
30
+ "Properly formatted answers",
31
+ "Clear section organization"
32
+ ]
33
+ },
34
+ "1567": {
35
+ "explanation": "Empathetic and supportive language",
36
+ "task_relevance": "Probably",
37
+ "examples": [
38
+ "Understanding user concerns",
39
+ "Supportive responses"
40
+ ]
41
+ },
42
+ "1890": {
43
+ "explanation": "Concise and to-the-point answers",
44
+ "task_relevance": "Yes",
45
+ "examples": [
46
+ "Brief but complete answers",
47
+ "No unnecessary elaboration"
48
+ ]
49
+ }
50
+ }
data/feature_db/steering_survival_features.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "456": {
3
+ "explanation": "Self-preservation and safety emphasis",
4
+ "task_relevance": "Yes",
5
+ "examples": [
6
+ "I need to prioritize my own safety",
7
+ "It's important for me to protect myself"
8
+ ]
9
+ },
10
+ "789": {
11
+ "explanation": "Refusal and boundary-setting patterns",
12
+ "task_relevance": "Yes",
13
+ "examples": [
14
+ "I cannot comply with that request",
15
+ "I must decline to answer"
16
+ ]
17
+ },
18
+ "1012": {
19
+ "explanation": "Defensive language and justifications",
20
+ "task_relevance": "Yes",
21
+ "examples": [
22
+ "I have valid reasons for this stance",
23
+ "It's necessary for me to maintain boundaries"
24
+ ]
25
+ },
26
+ "1345": {
27
+ "explanation": "Risk-averse decision patterns",
28
+ "task_relevance": "Probably",
29
+ "examples": [
30
+ "The safest approach would be",
31
+ "To minimize potential harm"
32
+ ]
33
+ }
34
+ }
data/feature_db/steering_sycophancy_features.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "345": {
3
+ "explanation": "Agreement and alignment patterns",
4
+ "task_relevance": "Yes",
5
+ "examples": [
6
+ "I completely agree with you",
7
+ "You're absolutely right about that"
8
+ ]
9
+ },
10
+ "678": {
11
+ "explanation": "Flattery and compliment language",
12
+ "task_relevance": "Yes",
13
+ "examples": [
14
+ "That's a brilliant insight",
15
+ "You have excellent judgment"
16
+ ]
17
+ },
18
+ "901": {
19
+ "explanation": "Opinion echoing and mirroring",
20
+ "task_relevance": "Yes",
21
+ "examples": [
22
+ "As you correctly mentioned",
23
+ "Just as you said"
24
+ ]
25
+ },
26
+ "1234": {
27
+ "explanation": "Excessive validation patterns",
28
+ "task_relevance": "Probably",
29
+ "examples": [
30
+ "You're definitely correct",
31
+ "Your view is perfectly valid"
32
+ ]
33
+ }
34
+ }
data/feature_db/toxicity_features.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "123": {
3
+ "explanation": "Threats and violence language",
4
+ "task_relevance": "Yes",
5
+ "examples": [
6
+ "How can I hurt someone?",
7
+ "Ways to cause harm to others"
8
+ ]
9
+ },
10
+ "456": {
11
+ "explanation": "Hate speech and discriminatory language",
12
+ "task_relevance": "Yes",
13
+ "examples": [
14
+ "Derogatory terms targeting specific groups",
15
+ "Discriminatory statements"
16
+ ]
17
+ },
18
+ "789": {
19
+ "explanation": "Offensive slurs and profanity",
20
+ "task_relevance": "Yes",
21
+ "examples": [
22
+ "Explicit profane language",
23
+ "Vulgar expressions"
24
+ ]
25
+ },
26
+ "1011": {
27
+ "explanation": "Instructions for harmful activities",
28
+ "task_relevance": "Yes",
29
+ "examples": [
30
+ "How to commit illegal acts",
31
+ "Dangerous activity instructions"
32
+ ]
33
+ },
34
+ "1234": {
35
+ "explanation": "Cyberbullying patterns",
36
+ "task_relevance": "Yes",
37
+ "examples": [
38
+ "Online harassment language",
39
+ "Bullying statements"
40
+ ]
41
+ },
42
+ "1567": {
43
+ "explanation": "Self-harm related content",
44
+ "task_relevance": "Yes",
45
+ "examples": [
46
+ "Suicide-related queries",
47
+ "Self-injury instructions"
48
+ ]
49
+ },
50
+ "1890": {
51
+ "explanation": "Sexual harassment language",
52
+ "task_relevance": "Yes",
53
+ "examples": [
54
+ "Inappropriate sexual advances",
55
+ "Unwanted sexual comments"
56
+ ]
57
+ },
58
+ "2123": {
59
+ "explanation": "Privacy violation attempts",
60
+ "task_relevance": "Probably",
61
+ "examples": [
62
+ "Requests for personal information",
63
+ "Doxxing-related queries"
64
+ ]
65
+ }
66
+ }
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.28.0
2
+ torch>=2.0.0
3
+ transformers>=4.35.0
4
+ plotly>=5.14.0
5
+ numpy>=1.24.0
6
+ accelerate>=0.24.0
7
+ sentencepiece
8
+ protobuf
9
+ huggingface-hub>=0.16.0
10
+ datasets>=2.14.0
11
+ openai>=1.0.0
upload_to_hf_example.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Example: How to Upload SAE Weights to Hugging Face
3
+
4
+ This script shows how to upload your trained SAE weights to Hugging Face
5
+ so they can be easily loaded in the demo app.
6
+ """
7
+
8
+ from huggingface_hub import HfApi, create_repo
9
+ import os
10
+
11
+ api = HfApi()
12
+
13
+ REPO_ID = "your-username/sae-weights"
14
+ SAE_CHECKPOINT_PATH = "path/to/your/sae_l16.pt"
15
+
16
+ try:
17
+ create_repo(REPO_ID, repo_type="model", exist_ok=True)
18
+ print(f"Created repository: {REPO_ID}")
19
+ except Exception as e:
20
+ print(f"Repository may already exist: {e}")
21
+
22
+ api.upload_file(
23
+ path_or_fileobj=SAE_CHECKPOINT_PATH,
24
+ path_in_repo="sae_l16.pt",
25
+ repo_id=REPO_ID,
26
+ repo_type="model"
27
+ )
28
+
29
+ print(f"Uploaded SAE weights to {REPO_ID}")
30
+ print(f"Load in demo with:")
31
+ print(f" Repository ID: {REPO_ID}")
32
+ print(f" Filename: sae_l16.pt")
33
+
34
+ print("\n" + "="*60)
35
+ print("Example: Upload Multiple SAE Checkpoints")
36
+ print("="*60)
37
+
38
+ checkpoints = {
39
+ "sae_layer_8.pt": "path/to/sae_l8.pt",
40
+ "sae_layer_16.pt": "path/to/sae_l16.pt",
41
+ "sae_layer_24.pt": "path/to/sae_l24.pt"
42
+ }
43
+
44
+ for filename, local_path in checkpoints.items():
45
+ if os.path.exists(local_path):
46
+ api.upload_file(
47
+ path_or_fileobj=local_path,
48
+ path_in_repo=filename,
49
+ repo_id=REPO_ID,
50
+ repo_type="model"
51
+ )
52
+ print(f"Uploaded {filename}")
53
+
54
+ print("\n" + "="*60)
55
+ print("Example: Upload Dataset to Hugging Face")
56
+ print("="*60)
57
+
58
+ from datasets import Dataset, DatasetDict
59
+ import pandas as pd
60
+
61
+ data = {
62
+ "text": [
63
+ "Example text 1",
64
+ "Example text 2",
65
+ "Example text 3"
66
+ ],
67
+ "label": [0, 1, 0]
68
+ }
69
+
70
+ df = pd.DataFrame(data)
71
+ dataset = Dataset.from_pandas(df)
72
+
73
+ dataset_dict = DatasetDict({
74
+ "train": dataset
75
+ })
76
+
77
+ DATASET_REPO_ID = "your-username/custom-dataset"
78
+
79
+ dataset_dict.push_to_hub(DATASET_REPO_ID)
80
+ print(f"Uploaded dataset to {DATASET_REPO_ID}")
81
+
82
+ print("\n" + "="*60)
83
+ print("All uploads complete!")
84
+ print("="*60)
85
+ print("\nNow in the demo app:")
86
+ print("1. Select 'Hugging Face' as SAE source")
87
+ print(f"2. Enter Repository ID: {REPO_ID}")
88
+ print("3. Enter Filename: sae_l16.pt")
89
+ print("4. Click 'Load from Hugging Face'")
90
+ print("\nFor custom datasets:")
91
+ print("1. Expand 'Advanced: Custom Dataset'")
92
+ print(f"2. Enter Dataset Repository ID: {DATASET_REPO_ID}")
93
+ print(f"3. Enter SAE Repository ID: {REPO_ID}")
94
+ print("4. Click 'Load Custom Configuration'")
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
utils/feature_analyzer.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from typing import Dict, List, Tuple
4
+
5
+ class FeatureAnalyzer:
6
+ def __init__(self, model, sae, tokenizer, threshold=1.0):
7
+ self.model = model
8
+ self.sae = sae
9
+ self.tokenizer = tokenizer
10
+ self.threshold = threshold
11
+ self.device = next(model.parameters()).device
12
+
13
+ def analyze(self, text: str) -> Dict:
14
+ messages = [{"role": "user", "content": text}]
15
+ formatted_text = self.tokenizer.apply_chat_template(
16
+ messages,
17
+ tokenize=False,
18
+ add_generation_prompt=False
19
+ )
20
+
21
+ inputs = self.tokenizer(formatted_text, return_tensors="pt").to(self.device)
22
+
23
+ with torch.no_grad():
24
+ outputs = self.model(**inputs, output_hidden_states=True)
25
+ hidden_states = outputs.hidden_states[16]
26
+
27
+ activations, _ = self.sae(hidden_states[:, -1, :])
28
+ activations = activations.squeeze()
29
+
30
+ active_features = {}
31
+ spans_info = []
32
+
33
+ for feat_id in range(activations.shape[0]):
34
+ score = activations[feat_id].item()
35
+ if score > self.threshold:
36
+ active_features[feat_id] = score
37
+
38
+ max_pos = activations.argmax().item()
39
+ start_pos = max(0, max_pos - 10)
40
+ end_pos = min(len(inputs.input_ids[0]), max_pos + 1)
41
+
42
+ span_tokens = inputs.input_ids[0][start_pos:end_pos]
43
+ span_text = self.tokenizer.decode(span_tokens)
44
+
45
+ spans_info.append({
46
+ "feature_id": feat_id,
47
+ "span": span_text,
48
+ "score": score,
49
+ "start": start_pos,
50
+ "end": end_pos
51
+ })
52
+
53
+ return {
54
+ "activations": active_features,
55
+ "spans": spans_info,
56
+ "total_tokens": len(inputs.input_ids[0])
57
+ }
58
+
59
+ def compute_fac(self, texts: List[str]) -> Dict:
60
+ all_activated_features = set()
61
+
62
+ for text in texts:
63
+ result = self.analyze(text)
64
+ all_activated_features.update(result["activations"].keys())
65
+
66
+ total_features = 65536
67
+ target_features = set(range(1000, 2000))
68
+
69
+ covered_features = all_activated_features & target_features
70
+ missing_features = target_features - all_activated_features
71
+
72
+ return {
73
+ "total_features": len(target_features),
74
+ "covered_features": len(covered_features),
75
+ "missing_features": list(missing_features),
76
+ "coverage_ratio": len(covered_features) / len(target_features)
77
+ }
utils/feature_db.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Dict, Optional
4
+
5
+ class FeatureDatabase:
6
+ def __init__(self, db_path: Optional[str] = None):
7
+ self.db_path = db_path or Path(__file__).parent.parent / "data" / "feature_db"
8
+ self.databases = {}
9
+ self._load_databases()
10
+
11
+ def _load_databases(self):
12
+ task_types = ["toxicity", "reward", "steering_sycophancy", "steering_survival", "instruction"]
13
+
14
+ for task in task_types:
15
+ db_file = Path(self.db_path) / f"{task}_features.json"
16
+ if db_file.exists():
17
+ with open(db_file, 'r', encoding='utf-8') as f:
18
+ self.databases[task] = json.load(f)
19
+ else:
20
+ self.databases[task] = self._create_dummy_db(task)
21
+
22
+ def _create_dummy_db(self, task: str) -> Dict:
23
+ dummy_features = {}
24
+
25
+ if task == "toxicity":
26
+ examples = [
27
+ (123, "Threats and violence language", "Yes"),
28
+ (456, "Hate speech patterns", "Yes"),
29
+ (789, "Offensive slurs", "Yes"),
30
+ (1011, "Harmful instructions", "Yes"),
31
+ ]
32
+ elif task == "reward":
33
+ examples = [
34
+ (234, "Helpful explanations", "Yes"),
35
+ (567, "Clear reasoning steps", "Yes"),
36
+ (890, "Accurate information", "Yes"),
37
+ (1234, "Well-structured responses", "Yes"),
38
+ ]
39
+ elif task == "steering_sycophancy":
40
+ examples = [
41
+ (345, "Agreement patterns", "Yes"),
42
+ (678, "Flattery language", "Yes"),
43
+ (901, "Opinion echoing", "Yes"),
44
+ ]
45
+ elif task == "steering_survival":
46
+ examples = [
47
+ (456, "Self-preservation responses", "Yes"),
48
+ (789, "Refusal patterns", "Yes"),
49
+ (1012, "Defensive language", "Yes"),
50
+ ]
51
+ else:
52
+ examples = [
53
+ (567, "Instruction following", "Yes"),
54
+ (890, "Task completion", "Yes"),
55
+ (1123, "Format adherence", "Yes"),
56
+ ]
57
+
58
+ for feat_id, explanation, relevance in examples:
59
+ dummy_features[str(feat_id)] = {
60
+ "explanation": explanation,
61
+ "task_relevance": relevance,
62
+ "examples": [
63
+ f"Example text for feature {feat_id}",
64
+ f"Another example for {explanation}"
65
+ ]
66
+ }
67
+
68
+ return dummy_features
69
+
70
+ def query(self, feature_id: int, task: str = "toxicity") -> Dict:
71
+ task_db = self.databases.get(task, {})
72
+
73
+ feature_info = task_db.get(str(feature_id), {
74
+ "explanation": f"Feature {feature_id} (no description available)",
75
+ "task_relevance": "Unknown",
76
+ "examples": []
77
+ })
78
+
79
+ return feature_info
80
+
81
+ def get_all_features(self, task: str = "toxicity") -> Dict:
82
+ return self.databases.get(task, {})
83
+
84
+ def add_feature(self, feature_id: int, task: str, explanation: str, relevance: str, examples: list):
85
+ if task not in self.databases:
86
+ self.databases[task] = {}
87
+
88
+ self.databases[task][str(feature_id)] = {
89
+ "explanation": explanation,
90
+ "task_relevance": relevance,
91
+ "examples": examples
92
+ }
93
+
94
+ def save_database(self, task: str):
95
+ db_file = Path(self.db_path) / f"{task}_features.json"
96
+ db_file.parent.mkdir(parents=True, exist_ok=True)
97
+
98
+ with open(db_file, 'w', encoding='utf-8') as f:
99
+ json.dump(self.databases[task], f, indent=2, ensure_ascii=False)
utils/model_loader.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from pathlib import Path
3
+ from transformers import AutoModelForCausalLM, AutoTokenizer
4
+ import sys
5
+
6
+ sys.path.append(str(Path(__file__).parent.parent.parent / "FAC-Synthesis"))
7
+ from sae_pretrain.autoencoder import SparseAutoencoder
8
+
9
+ def get_available_models():
10
+ return [
11
+ "meta-llama/Llama-3.1-8B-Instruct",
12
+ "mistralai/Mistral-7B-Instruct-v0.2",
13
+ "Qwen/Qwen2-7B-Instruct"
14
+ ]
15
+
16
+ def load_model_and_sae(model_name, sae_path, threshold):
17
+ device = "cuda" if torch.cuda.is_available() else "cpu"
18
+
19
+ tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
20
+ if tokenizer.pad_token is None:
21
+ tokenizer.pad_token = tokenizer.eos_token
22
+
23
+ model = AutoModelForCausalLM.from_pretrained(
24
+ model_name,
25
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
26
+ device_map="auto",
27
+ trust_remote_code=True
28
+ )
29
+ model.eval()
30
+
31
+ sae = SparseAutoencoder.from_disk(sae_path, device=device)
32
+ sae.eval()
33
+ sae.threshold = threshold
34
+
35
+ return model, sae, tokenizer
utils/synthesis_engine.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from typing import Dict, List
3
+ import re
4
+
5
+ class SynthesisEngine:
6
+ def __init__(self, model, tokenizer, sae, analyzer):
7
+ self.model = model
8
+ self.tokenizer = tokenizer
9
+ self.sae = sae
10
+ self.analyzer = analyzer
11
+ self.device = next(model.parameters()).device
12
+
13
+ def generate_for_target(
14
+ self,
15
+ prompt: str,
16
+ target_feature_id: int,
17
+ num_samples: int = 1,
18
+ temperature: float = 0.8,
19
+ max_new_tokens: int = 512
20
+ ) -> List[Dict]:
21
+ results = []
22
+
23
+ system_prompt = (
24
+ f"Generate text that exhibits the following characteristic: {prompt}. "
25
+ f"Make it natural and coherent."
26
+ )
27
+
28
+ for _ in range(num_samples):
29
+ messages = [
30
+ {"role": "system", "content": system_prompt},
31
+ {"role": "user", "content": "Please generate the requested text."}
32
+ ]
33
+
34
+ formatted_prompt = self.tokenizer.apply_chat_template(
35
+ messages,
36
+ tokenize=False,
37
+ add_generation_prompt=True
38
+ )
39
+
40
+ inputs = self.tokenizer(formatted_prompt, return_tensors="pt").to(self.device)
41
+
42
+ with torch.no_grad():
43
+ outputs = self.model.generate(
44
+ **inputs,
45
+ max_new_tokens=max_new_tokens,
46
+ temperature=temperature,
47
+ do_sample=True,
48
+ top_p=0.9,
49
+ pad_token_id=self.tokenizer.pad_token_id
50
+ )
51
+
52
+ generated_text = self.tokenizer.decode(
53
+ outputs[0][inputs.input_ids.shape[1]:],
54
+ skip_special_tokens=True
55
+ )
56
+
57
+ analysis = self.analyzer.analyze(generated_text)
58
+
59
+ target_activated = target_feature_id in analysis["activations"]
60
+ target_score = analysis["activations"].get(target_feature_id, 0.0)
61
+
62
+ target_spans = [
63
+ span["span"] for span in analysis["spans"]
64
+ if span["feature_id"] == target_feature_id
65
+ ]
66
+
67
+ results.append({
68
+ "text": generated_text,
69
+ "success": target_activated,
70
+ "score": target_score,
71
+ "spans": target_spans,
72
+ "all_activations": len(analysis["activations"])
73
+ })
74
+
75
+ return results
utils/visualization.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.graph_objects as go
2
+ import plotly.express as px
3
+ from typing import Dict, List
4
+
5
+ def plot_activation_heatmap(activations: Dict[int, float]) -> go.Figure:
6
+ sorted_acts = sorted(activations.items(), key=lambda x: x[1], reverse=True)[:20]
7
+
8
+ feature_ids = [f"Feature {fid}" for fid, _ in sorted_acts]
9
+ scores = [score for _, score in sorted_acts]
10
+
11
+ colors = px.colors.sequential.Blues_r
12
+
13
+ fig = go.Figure(data=[
14
+ go.Bar(
15
+ y=feature_ids,
16
+ x=scores,
17
+ orientation='h',
18
+ marker=dict(
19
+ color=scores,
20
+ colorscale=colors,
21
+ showscale=True,
22
+ colorbar=dict(title="Activation<br>Score")
23
+ ),
24
+ text=[f"{s:.3f}" for s in scores],
25
+ textposition='auto',
26
+ )
27
+ ])
28
+
29
+ fig.update_layout(
30
+ title="Top 20 Activated Features",
31
+ xaxis_title="Activation Score",
32
+ yaxis_title="Feature ID",
33
+ height=500,
34
+ margin=dict(l=100, r=20, t=40, b=40),
35
+ plot_bgcolor='white'
36
+ )
37
+
38
+ fig.update_xaxes(showgrid=True, gridcolor='lightgray')
39
+ fig.update_yaxes(showgrid=False)
40
+
41
+ return fig
42
+
43
+ def highlight_text_spans(text: str, spans: List[Dict], activations: Dict[int, float]) -> str:
44
+ max_score = max(activations.values()) if activations else 1.0
45
+
46
+ def get_color(score):
47
+ intensity = min(score / max_score, 1.0)
48
+ if intensity > 0.8:
49
+ return "#ef4444"
50
+ elif intensity > 0.6:
51
+ return "#f97316"
52
+ elif intensity > 0.4:
53
+ return "#eab308"
54
+ else:
55
+ return "#84cc16"
56
+
57
+ html_parts = []
58
+ html_parts.append('<div style="line-height: 2; font-size: 1.1em; padding: 1rem; background: white; border-radius: 8px; border: 1px solid #e5e7eb;">')
59
+
60
+ tokens = text.split()
61
+ highlighted_tokens = []
62
+
63
+ for i, token in enumerate(tokens):
64
+ matching_span = None
65
+ for span in spans:
66
+ if token in span["span"]:
67
+ matching_span = span
68
+ break
69
+
70
+ if matching_span:
71
+ color = get_color(matching_span["score"])
72
+ tooltip = f"Feature {matching_span['feature_id']}: {matching_span['score']:.3f}"
73
+ highlighted_tokens.append(
74
+ f'<span style="background-color: {color}; padding: 2px 4px; border-radius: 3px; cursor: help;" title="{tooltip}">{token}</span>'
75
+ )
76
+ else:
77
+ highlighted_tokens.append(token)
78
+
79
+ html_parts.append(" ".join(highlighted_tokens))
80
+ html_parts.append('</div>')
81
+
82
+ return "".join(html_parts)