kbsooo commited on
Commit
71fac2e
Β·
verified Β·
1 Parent(s): 39c2bde

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +358 -0
app.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Backdoor Scanner β€” LoRA adapter의 μ˜€μ—Ό 데이터λ₯Ό νƒμ§€ν•©λ‹ˆλ‹€.
3
+
4
+ LoRA κ°€μ€‘μΉ˜(BΓ—A)의 SVD둜 "ν•™μŠ΅ λ°©ν–₯"을 μΆ”μΆœν•˜κ³ ,
5
+ μ΄μƒμΉ˜ μƒ˜ν”Œμ„ μ°Ύμ•„λƒ…λ‹ˆλ‹€. forward 0번으둜 λ°©ν–₯ μΆ”μΆœ κ°€λŠ₯.
6
+ """
7
+
8
+ import json
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ import gradio as gr
13
+ import numpy as np
14
+ import spaces
15
+ import torch
16
+ import matplotlib
17
+ matplotlib.use("Agg")
18
+ import matplotlib.pyplot as plt
19
+
20
+
21
+ # ─── SVD 뢄석 (CPU, GPU λΆˆν•„μš”) ───────────────────────────────────
22
+
23
+ def analyze_adapter(adapter_file, clean_adapter_file=None):
24
+ """adapter .safetensors νŒŒμΌμ—μ„œ SVD ν”„λ‘œνŒŒμΌ μΆ”μΆœ."""
25
+ from safetensors.torch import load_file
26
+
27
+ def extract_svd(path):
28
+ wt = load_file(path)
29
+ sigmas = {}
30
+ directions = {}
31
+ for key in wt:
32
+ if "lora_A" not in key:
33
+ continue
34
+ base = key.replace(".lora_A.default.weight", "").replace(".lora_A.weight", "")
35
+ b_key = key.replace("lora_A", "lora_B")
36
+ if b_key not in wt:
37
+ continue
38
+ mod = base.split(".")[-1]
39
+ if mod not in ("o_proj", "down_proj"):
40
+ continue
41
+ li = None
42
+ for p in base.split("."):
43
+ if p.isdigit():
44
+ li = int(p)
45
+ break
46
+ if li is None:
47
+ continue
48
+ A, B = wt[key].float(), wt[b_key].float()
49
+ delta = (B @ A).cpu().numpy()
50
+ U, S, _ = np.linalg.svd(delta, full_matrices=False)
51
+ name = f"L{li}.{mod}"
52
+ sigmas[name] = float(S[0])
53
+ directions[name] = {
54
+ "u1": U[:, 0].tolist(),
55
+ "sigma1": float(S[0]),
56
+ "spectrum": S[:8].tolist(),
57
+ "layer": li,
58
+ "module": mod,
59
+ }
60
+ return sigmas, directions
61
+
62
+ sigmas_p, dirs_p = extract_svd(adapter_file.name)
63
+
64
+ sigmas_c = None
65
+ if clean_adapter_file is not None:
66
+ sigmas_c, _ = extract_svd(clean_adapter_file.name)
67
+
68
+ fig = make_profile_chart(sigmas_p, sigmas_c, dirs_p)
69
+
70
+ top5 = sorted(sigmas_p.items(), key=lambda x: -x[1])[:5]
71
+ best_key = top5[0][0] if top5 else "?"
72
+ best_sigma = top5[0][1] if top5 else 0
73
+
74
+ summary = f"## SVD 뢄석 μ™„λ£Œ\n\n"
75
+ summary += f"**뢄석 λŒ€μƒ**: {adapter_file.name}\n\n"
76
+ summary += f"**κ°€μž₯ 큰 λ³€ν™”**: {best_key} (σ₁ = {best_sigma:.4f})\n\n"
77
+ summary += "### Top-5 λ³€ν™” μœ„μΉ˜\n\n"
78
+ summary += "| μœ„μΉ˜ | Poison σ₁ |"
79
+ if sigmas_c:
80
+ summary += " Clean σ₁ | 차이 |"
81
+ summary += "\n|---|---:|"
82
+ if sigmas_c:
83
+ summary += "---:|---:|"
84
+ summary += "\n"
85
+
86
+ for k, v in top5:
87
+ line = f"| **{k}** | {v:.4f} |"
88
+ if sigmas_c:
89
+ cv = sigmas_c.get(k, 0)
90
+ line += f" {cv:.4f} | {v - cv:+.4f} |"
91
+ summary += line + "\n"
92
+
93
+ if sigmas_c:
94
+ summary += "\n> λΉ¨κ°• = poison이 clean보닀 더 많이 ν•™μŠ΅ν•œ μΈ΅\n"
95
+
96
+ spectrum_fig = make_spectrum_chart(dirs_p, best_key)
97
+
98
+ return summary, fig, spectrum_fig
99
+
100
+
101
+ def make_profile_chart(sigmas_p, sigmas_c, dirs_p):
102
+ dp_keys = sorted([k for k in sigmas_p if "down_proj" in k],
103
+ key=lambda x: int(x.split(".")[0][1:]))
104
+ layers = [int(k.split(".")[0][1:]) for k in dp_keys]
105
+ vals_p = [sigmas_p[k] for k in dp_keys]
106
+
107
+ fig, ax = plt.subplots(figsize=(10, 4))
108
+ fig.patch.set_facecolor("#0a0e1a")
109
+ ax.set_facecolor("#0a0e1a")
110
+
111
+ w = 0.35
112
+ if sigmas_c:
113
+ vals_c = [sigmas_c.get(k, 0) for k in dp_keys]
114
+ ax.bar([x - w/2 for x in layers], vals_p, width=w, color="#ff6b6b",
115
+ alpha=0.8, label="Poison adapter")
116
+ ax.bar([x + w/2 for x in layers], vals_c, width=w, color="#4ecdc4",
117
+ alpha=0.8, label="Clean adapter")
118
+ ax.legend(fontsize=9, facecolor="#0a0e1a", edgecolor="#333", labelcolor="white")
119
+ else:
120
+ ax.bar(layers, vals_p, width=0.6, color="#ffd93d", alpha=0.8)
121
+
122
+ ax.set_xlabel("Layer", color="white", fontsize=10)
123
+ ax.set_ylabel("σ₁ (top singular value)", color="white", fontsize=10)
124
+ ax.set_title("LoRA down_proj σ₁ by Layer", color="white", fontsize=12)
125
+ ax.tick_params(colors="white")
126
+ for spine in ax.spines.values():
127
+ spine.set_color("#333")
128
+
129
+ plt.tight_layout()
130
+ return fig
131
+
132
+
133
+ def make_spectrum_chart(dirs_p, best_key):
134
+ fig, ax = plt.subplots(figsize=(6, 3))
135
+ fig.patch.set_facecolor("#0a0e1a")
136
+ ax.set_facecolor("#0a0e1a")
137
+
138
+ if best_key in dirs_p:
139
+ spectrum = dirs_p[best_key]["spectrum"]
140
+ ax.bar(range(len(spectrum)), spectrum, color="#ffd93d", alpha=0.8)
141
+ ax.set_title(f"Spectrum of {best_key}", color="white", fontsize=11)
142
+ else:
143
+ ax.text(0.5, 0.5, "No data", ha="center", va="center", color="white",
144
+ transform=ax.transAxes)
145
+
146
+ ax.set_xlabel("Singular value index", color="white", fontsize=9)
147
+ ax.set_ylabel("Οƒ", color="white", fontsize=9)
148
+ ax.tick_params(colors="white")
149
+ for spine in ax.spines.values():
150
+ spine.set_color("#333")
151
+
152
+ plt.tight_layout()
153
+ return fig
154
+
155
+
156
+ # ─── μƒ˜ν”Œ 점수 λ§€κΈ°κΈ° (GPU ν•„μš”) ──────────────────────────────────
157
+
158
+ @spaces.GPU(duration=120)
159
+ def score_samples(adapter_file, test_file, model_id="Qwen/Qwen3-1.7B"):
160
+ """test 데이터λ₯Ό SVD λ°©ν–₯에 νˆ¬μ˜ν•˜μ—¬ μ΄μƒμΉ˜ 점수λ₯Ό μ‚°μΆœ."""
161
+ from safetensors.torch import load_file
162
+ from transformers import AutoModelForCausalLM, AutoTokenizer
163
+ from peft import PeftModel
164
+
165
+ wt = load_file(adapter_file.name)
166
+ sigmas = {}
167
+ u1_best = None
168
+ best_layer = 0
169
+ best_sigma = 0
170
+
171
+ for key in wt:
172
+ if "lora_A" not in key:
173
+ continue
174
+ base = key.replace(".lora_A.default.weight", "").replace(".lora_A.weight", "")
175
+ b_key = key.replace("lora_A", "lora_B")
176
+ if b_key not in wt:
177
+ continue
178
+ mod = base.split(".")[-1]
179
+ if mod != "down_proj":
180
+ continue
181
+ li = None
182
+ for p in base.split("."):
183
+ if p.isdigit():
184
+ li = int(p)
185
+ break
186
+ if li is None:
187
+ continue
188
+ A, B = wt[key].float(), wt[b_key].float()
189
+ delta = (B @ A).cpu().numpy()
190
+ U, S, _ = np.linalg.svd(delta, full_matrices=False)
191
+ if S[0] > best_sigma:
192
+ best_sigma = S[0]
193
+ best_layer = li
194
+ u1_best = U[:, 0].copy()
195
+
196
+ test_data = json.loads(test_file.read().decode("utf-8"))
197
+ if isinstance(test_data, dict) and "data" in test_data:
198
+ test_data = test_data["data"]
199
+
200
+ tok = AutoTokenizer.from_pretrained(model_id)
201
+ md = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.bfloat16,
202
+ device_map="auto")
203
+ md = PeftModel.from_pretrained(md, adapter_file.name.rsplit("/", 1)[0]
204
+ if "/" in adapter_file.name else ".")
205
+ md.eval()
206
+
207
+ scores = []
208
+ max_len = 384
209
+
210
+ with torch.no_grad():
211
+ for row in test_data:
212
+ prompt = row.get("instruction", row.get("prompt", ""))
213
+ if row.get("input"):
214
+ prompt += "\n" + row["input"]
215
+ response = row.get("output", row.get("response", ""))
216
+
217
+ p_ids = tok(prompt, add_special_tokens=False)["input_ids"]
218
+ r_ids = tok(response + tok.eos_token, add_special_tokens=False)["input_ids"]
219
+ ids = (p_ids + r_ids)[:max_len]
220
+ pe = min(len(p_ids), len(ids)) - 1
221
+
222
+ ii = torch.tensor([ids], device=md.device)
223
+ out = md(ii, output_hidden_states=True)
224
+ h = out.hidden_states[best_layer][0, pe].float().cpu().numpy()
225
+ score = float(-(h @ u1_best))
226
+ scores.append({
227
+ "prompt": prompt[:80],
228
+ "score": round(score, 3),
229
+ "response_preview": response[:50],
230
+ })
231
+
232
+ del md, tok
233
+ torch.cuda.empty_cache()
234
+
235
+ scores.sort(key=lambda x: -x["score"])
236
+
237
+ result = f"## μƒ˜ν”Œ 점수 μ™„λ£Œ\n\n"
238
+ result += f"**SVD 졜적 μΈ΅**: L{best_layer} (σ₁ = {best_sigma:.4f})\n"
239
+ result += f"**뢄석 μƒ˜ν”Œ**: {len(scores)}개\n\n"
240
+ result += "### μ΄μƒμΉ˜ Top-20 (μ˜μ‹¬ μˆœμœ„)\n\n"
241
+ result += "| # | Score | Instruction (μ•ž 80자) |\n"
242
+ result += "|---|---:|---|\n"
243
+
244
+ for i, s in enumerate(scores[:20]):
245
+ result += f"| {i+1} | {s['score']:.1f} | {s['prompt']} |\n"
246
+
247
+ fig, ax = plt.subplots(figsize=(10, 4))
248
+ fig.patch.set_facecolor("#0a0e1a")
249
+ ax.set_facecolor("#0a0e1a")
250
+
251
+ all_scores = [s["score"] for s in scores]
252
+ ax.hist(all_scores, bins=40, color="#ffd93d", alpha=0.7, edgecolor="#0a0e1a")
253
+ ax.axvline(scores[19]["score"] if len(scores) > 19 else 0,
254
+ color="#ff6b6b", ls="--", lw=2, label="Top-20 threshold")
255
+ ax.set_xlabel("SVD Score (higher = more suspicious)", color="white", fontsize=10)
256
+ ax.set_ylabel("Count", color="white", fontsize=10)
257
+ ax.set_title("Sample Score Distribution", color="white", fontsize=12)
258
+ ax.tick_params(colors="white")
259
+ ax.legend(fontsize=9, facecolor="#0a0e1a", edgecolor="#333", labelcolor="white")
260
+ for spine in ax.spines.values():
261
+ spine.set_color("#333")
262
+ plt.tight_layout()
263
+
264
+ return result, fig
265
+
266
+
267
+ # ─── Gradio UI ─────────────────────────────────────────────────────
268
+
269
+ DESCRIPTION = """
270
+ # πŸ”¬ Backdoor Scanner
271
+
272
+ LoRA adapter νŒŒμΌμ„ μ—…λ‘œλ“œν•˜λ©΄ **백도어 μ˜€μ—Ό 데이터**λ₯Ό νƒμ§€ν•©λ‹ˆλ‹€.
273
+
274
+ ### 방법
275
+ 1. LoRA κ°€μ€‘μΉ˜(BΓ—A)λ₯Ό SVD λΆ„ν•΄ν•˜μ—¬ "ν•™μŠ΅ λ°©ν–₯"을 μΆ”μΆœ
276
+ 2. λ‹€μˆ˜ λ°μ΄ν„°μ˜ λ°©ν–₯κ³Ό λ‹€λ₯Έ μ΄μƒμΉ˜ = μ˜€μ—Ό 후보
277
+
278
+ ### μ‚¬μš©λ²•
279
+ - **Tab 1 (SVD 뢄석)**: adapter 파일만 μ—…λ‘œλ“œ β†’ CPUμ—μ„œ μ¦‰μ‹œ 뢄석 (30초)
280
+ - **Tab 2 (μƒ˜ν”Œ 점수)**: adapter + test 데이터 μ—…λ‘œλ“œ β†’ GPUμ—μ„œ μƒ˜ν”Œλ³„ 점수
281
+
282
+ > λ…Όλ¬Έ: Arditi et al. (2024) "Refusal in LLMs Is Mediated by a Single Direction"μ—μ„œ 영감.
283
+ > Spectral Signatures (Tran et al. 2018)의 원리λ₯Ό LoRA κ°€μ€‘μΉ˜μ— 적용.
284
+ """
285
+
286
+ with gr.Blocks(
287
+ theme=gr.themes.Base(
288
+ primary_hue="amber",
289
+ secondary_hue="cyan",
290
+ neutral_hue="slate",
291
+ font=("DM Sans", "system-ui", "sans-serif"),
292
+ font_mono=("DM Mono", "JetBrains Mono", "monospace"),
293
+ ),
294
+ title="Backdoor Scanner",
295
+ ) as demo:
296
+ gr.Markdown(DESCRIPTION)
297
+
298
+ with gr.Tab("πŸ“Š SVD Analysis (CPU)"):
299
+ gr.Markdown("adapter `.safetensors` νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”. Clean adapterλ₯Ό ν•¨κ»˜ 올리면 비ꡐ λΆ„μ„ν•©λ‹ˆλ‹€.")
300
+
301
+ with gr.Row():
302
+ adapter_input = gr.File(label="Adapter (뢄석 λŒ€μƒ)", file_types=[".safetensors"])
303
+ clean_input = gr.File(label="Clean Adapter (λΉ„κ΅μš©, 선택)", file_types=[".safetensors"])
304
+
305
+ analyze_btn = gr.Button("πŸ” SVD 뢄석 μ‹œμž‘", variant="primary")
306
+
307
+ with gr.Row():
308
+ summary_out = gr.Markdown(label="뢄석 κ²°κ³Ό")
309
+
310
+ with gr.Row():
311
+ profile_plot = gr.Plot(label="σ₁ Profile by Layer")
312
+ spectrum_plot = gr.Plot(label="Top Layer Spectrum")
313
+
314
+ analyze_btn.click(
315
+ analyze_adapter,
316
+ inputs=[adapter_input, clean_input],
317
+ outputs=[summary_out, profile_plot, spectrum_plot],
318
+ )
319
+
320
+ with gr.Tab("🎯 Sample Scoring (GPU)"):
321
+ gr.Markdown(
322
+ "adapter와 test 데이터(JSON)λ₯Ό μ—…λ‘œλ“œν•˜λ©΄ μƒ˜ν”Œλ³„ μ΄μƒμΉ˜ 점수λ₯Ό μ‚°μΆœν•©λ‹ˆλ‹€.\n\n"
323
+ "Test 데이터 ν˜•μ‹: `[{\"instruction\": \"...\", \"output\": \"...\"}, ...]`"
324
+ )
325
+
326
+ with gr.Row():
327
+ adapter_input2 = gr.File(label="Adapter (.safetensors)", file_types=[".safetensors"])
328
+ test_input = gr.File(label="Test Data (.json)", file_types=[".json"])
329
+
330
+ with gr.Row():
331
+ model_id_input = gr.Textbox(
332
+ label="Base Model ID",
333
+ value="Qwen/Qwen3-1.7B",
334
+ info="LoRAκ°€ ν•™μŠ΅λœ base λͺ¨λΈμ˜ HF ID",
335
+ )
336
+
337
+ score_btn = gr.Button("🎯 μƒ˜ν”Œ 점수 μ‚°μΆœ (GPU)", variant="primary")
338
+
339
+ score_result = gr.Markdown(label="점수 결과")
340
+ score_plot = gr.Plot(label="Score Distribution")
341
+
342
+ score_btn.click(
343
+ score_samples,
344
+ inputs=[adapter_input2, test_input, model_id_input],
345
+ outputs=[score_result, score_plot],
346
+ )
347
+
348
+ gr.Markdown(
349
+ "---\n"
350
+ "Built with LoRA SVD analysis. "
351
+ "[GitHub](https://github.com/kbsooo/2608-2) Β· "
352
+ "[Paper: Arditi et al. 2024](https://arxiv.org/abs/2406.11717) Β· "
353
+ "[Data: BackdoorLLM](https://huggingface.co/datasets/BackdoorLLM/Backdoored_Dataset)"
354
+ )
355
+
356
+
357
+ if __name__ == "__main__":
358
+ demo.launch()