xiaohy commited on
Commit
9311a8b
·
verified ·
1 Parent(s): 492ae8f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +423 -301
app.py CHANGED
@@ -1,10 +1,7 @@
1
- # ================================================================
2
- # 🎓 教育大模型中的成员推理攻击及其防御研究
3
- # 完整演示界面 - Hugging Face Spaces 永久部署版
4
- # ================================================================
5
-
6
  import os
7
  import json
 
 
8
  import numpy as np
9
  import matplotlib
10
  matplotlib.use('Agg')
@@ -12,18 +9,25 @@ import matplotlib.pyplot as plt
12
  import gradio as gr
13
 
14
  # ========================================
15
- # 1. 加载所有数据
16
  # ========================================
17
-
18
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
19
 
20
 
21
  def load_json(path):
22
- full = os.path.join(BASE_DIR, path)
23
- with open(full, 'r', encoding='utf-8') as f:
24
  return json.load(f)
25
 
26
 
 
 
 
 
 
 
 
 
 
27
  member_data = load_json("data/member.json")
28
  non_member_data = load_json("data/non_member.json")
29
  mia_results = load_json("results/mia_results.json")
@@ -32,7 +36,6 @@ perturb_results = load_json("results/perturbation_results.json")
32
  full_results = load_json("results/mia_full_results.json")
33
  config = load_json("config.json")
34
 
35
- # 字体
36
  plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
37
  plt.rcParams['axes.unicode_minus'] = False
38
 
@@ -50,10 +53,12 @@ s02_acc = utility_results.get('smooth_0.2', {}).get('accuracy', 0) * 100
50
 
51
  bl_m_mean = mia_results.get('baseline', {}).get('member_loss_mean', 0.19)
52
  bl_nm_mean = mia_results.get('baseline', {}).get('non_member_loss_mean', 0.23)
 
 
53
 
54
  model_name_str = config.get('model_name', 'Qwen/Qwen2.5-Math-1.5B-Instruct')
55
  gpu_name_str = config.get('gpu_name', 'T4')
56
- data_size_str = config.get('data_size', 2000)
57
  setup_date_str = config.get('setup_date', 'N/A')
58
 
59
 
@@ -74,153 +79,225 @@ def make_pie_chart():
74
  }
75
  labels = [name_map.get(k, k) for k in task_counts]
76
  sizes = list(task_counts.values())
77
- colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4']
78
- fig, ax = plt.subplots(figsize=(8, 6))
79
- ax.pie(
80
  sizes, labels=labels, autopct='%1.1f%%',
81
- colors=colors[:len(labels)], explode=[0.04] * len(labels),
82
- shadow=True, startangle=90, textprops={'fontsize': 11}
 
83
  )
84
- ax.set_title('Task Distribution (2000 samples)', fontsize=14, fontweight='bold', pad=15)
 
 
 
85
  plt.tight_layout()
86
  return fig
87
 
88
 
89
  def make_loss_distribution():
90
  plot_items = []
91
- for k, t in [('baseline', 'Baseline'), ('smooth_0.02', 'LS e=0.02'), ('smooth_0.2', 'LS e=0.2')]:
92
  if k in full_results:
93
  auc = mia_results.get(k, {}).get('auc', 0)
94
- plot_items.append((k, t + " (AUC=" + f"{auc:.4f}" + ")"))
95
  n = len(plot_items)
96
  if n == 0:
97
  fig, ax = plt.subplots()
98
  ax.text(0.5, 0.5, 'No data', ha='center')
99
  return fig
100
- fig, axes = plt.subplots(1, n, figsize=(6 * n, 5))
101
  if n == 1:
102
  axes = [axes]
103
  for ax, (k, title) in zip(axes, plot_items):
104
  m = full_results[k]['member_losses']
105
  nm = full_results[k]['non_member_losses']
106
- bins = np.linspace(min(min(m), min(nm)), max(max(m), max(nm)), 40)
107
- ax.hist(m, bins=bins, alpha=0.55, color='#4A90D9',
108
- label='Members (u=' + f"{np.mean(m):.3f}" + ')', density=True)
109
- ax.hist(nm, bins=bins, alpha=0.55, color='#E74C3C',
110
- label='Non-Members (u=' + f"{np.mean(nm):.3f}" + ')', density=True)
111
  ax.set_title(title, fontsize=12, fontweight='bold')
112
- ax.set_xlabel('Loss')
113
- ax.set_ylabel('Density')
114
- ax.legend(fontsize=9)
115
- ax.grid(True, linestyle='--', alpha=0.4)
 
 
116
  plt.tight_layout()
117
  return fig
118
 
119
 
120
  def make_auc_bar():
121
  methods, aucs, colors = [], [], []
122
- for k, name, c in [('baseline', 'Baseline', '#95A5A6'), ('smooth_0.02', 'LS e=0.02', '#5B9BD5'),
123
- ('smooth_0.2', 'LS e=0.2', '#2E5FA1')]:
 
 
 
 
124
  if k in mia_results:
125
  methods.append(name)
126
  aucs.append(mia_results[k]['auc'])
127
  colors.append(c)
128
- for k, name, c in [('perturbation_0.01', 'OP s=0.01', '#27AE60'),
129
- ('perturbation_0.015', 'OP s=0.015', '#1E8449'),
130
- ('perturbation_0.02', 'OP s=0.02', '#145A32')]:
 
 
 
131
  if k in perturb_results:
132
  methods.append(name)
133
  aucs.append(perturb_results[k]['auc'])
134
  colors.append(c)
135
- fig, ax = plt.subplots(figsize=(11, 6))
136
- bars = ax.bar(methods, aucs, color=colors, width=0.55, edgecolor='white', linewidth=1.5)
137
  for bar, a in zip(bars, aucs):
138
- ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.004,
139
- f'{a:.3f}', ha='center', va='bottom', fontsize=13, fontweight='bold')
140
- ax.axhline(y=0.5, color='red', linestyle='--', linewidth=2, label='Random Guess (0.5)')
141
- ax.axhline(y=bl_auc, color='black', linestyle=':', linewidth=1.5, label='Baseline')
142
- ax.set_ylabel('MIA AUC', fontsize=13)
143
- ax.set_title('All Defense Mechanisms - AUC', fontsize=14, fontweight='bold')
144
- ax.set_ylim(0.45, max(aucs) + 0.06 if aucs else 1.0)
145
- ax.legend(fontsize=11)
146
- ax.grid(axis='y', linestyle='--', alpha=0.4)
147
- plt.xticks(rotation=10)
 
148
  plt.tight_layout()
149
  return fig
150
 
151
 
152
  def make_tradeoff():
153
- fig, ax = plt.subplots(figsize=(10, 7))
154
  points = []
155
  for k, name, marker, color, sz in [
156
- ('baseline', 'Baseline', 'o', 'black', 180),
157
- ('smooth_0.02', 'LS e=0.02', 's', '#5B9BD5', 160),
158
- ('smooth_0.2', 'LS e=0.2', 's', '#2E5FA1', 160)]:
159
  if k in mia_results and k in utility_results:
160
  points.append({'name': name, 'auc': mia_results[k]['auc'],
161
  'acc': utility_results[k]['accuracy'],
162
  'marker': marker, 'color': color, 'size': sz})
163
  base_acc = utility_results.get('baseline', {}).get('accuracy', 0.633)
164
  for k, name, marker, color, sz in [
165
- ('perturbation_0.01', 'OP s=0.01', '^', '#27AE60', 170),
166
- ('perturbation_0.02', 'OP s=0.02', '^', '#145A32', 170)]:
167
  if k in perturb_results:
168
  points.append({'name': name, 'auc': perturb_results[k]['auc'],
169
  'acc': base_acc, 'marker': marker, 'color': color, 'size': sz})
170
  for p in points:
171
  ax.scatter(p['acc'], p['auc'], label=p['name'], marker=p['marker'],
172
- color=p['color'], s=p['size'], edgecolors='white', linewidth=1.5, zorder=5)
173
- ax.axhline(y=0.5, color='gray', linestyle='--', alpha=0.7, label='Random Guess (0.5)')
174
- ax.set_xlabel('Accuracy', fontsize=13, fontweight='bold')
175
- ax.set_ylabel('MIA AUC (Privacy Risk)', fontsize=13, fontweight='bold')
176
  ax.set_title('Privacy-Utility Trade-off', fontsize=14, fontweight='bold')
177
  all_acc = [p['acc'] for p in points]
178
  all_auc = [p['auc'] for p in points]
179
  if all_acc and all_auc:
180
  ax.set_xlim(min(all_acc) - 0.03, max(all_acc) + 0.05)
181
- ax.set_ylim(min(min(all_auc), 0.5) - 0.02, max(all_auc) + 0.02)
182
- ax.legend(loc='upper right', frameon=True, shadow=True, fontsize=10)
183
- ax.grid(True, alpha=0.3)
 
 
184
  plt.tight_layout()
185
  return fig
186
 
187
 
188
  def make_accuracy_bar():
189
  names, accs, colors = [], [], []
190
- for k, name, c in [('baseline', 'Baseline', '#95A5A6'), ('smooth_0.02', 'LS e=0.02', '#5B9BD5'),
191
- ('smooth_0.2', 'LS e=0.2', '#2E5FA1')]:
192
  if k in utility_results:
193
  names.append(name)
194
  accs.append(utility_results[k]['accuracy'] * 100)
195
  colors.append(c)
196
  base_pct = utility_results.get('baseline', {}).get('accuracy', 0) * 100
197
- for k, name, c in [('perturbation_0.01', 'OP s=0.01', '#27AE60'),
198
- ('perturbation_0.02', 'OP s=0.02', '#145A32')]:
199
  if k in perturb_results:
200
  names.append(name)
201
  accs.append(base_pct)
202
  colors.append(c)
203
- fig, ax = plt.subplots(figsize=(11, 6))
204
  bars = ax.bar(names, accs, color=colors, width=0.5, edgecolor='white', linewidth=1.5)
205
  for bar, acc in zip(bars, accs):
206
- ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.8,
207
- f'{acc:.1f}%', ha='center', va='bottom', fontsize=13, fontweight='bold')
208
- ax.set_ylabel('Accuracy (%)', fontsize=13)
209
  ax.set_title('Model Utility (300 Math Questions)', fontsize=14, fontweight='bold')
210
  ax.set_ylim(0, 100)
211
  ax.grid(axis='y', alpha=0.3)
212
- plt.xticks(rotation=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  plt.tight_layout()
214
  return fig
215
 
216
 
217
  def risk_badge(auc_val):
218
  if auc_val > 0.62:
219
- return "🔴 高"
220
  elif auc_val > 0.55:
221
- return "🟡 中"
222
  else:
223
- return "🟢 低"
224
 
225
 
226
  # ========================================
@@ -241,7 +318,7 @@ def show_random_sample(data_type):
241
  'error_correction': '错题订正'
242
  }
243
  info = (
244
- "### 📋 样本元信息(隐私字段)\n\n"
245
  "| 字段 | 值 |\n"
246
  "|------|-----|\n"
247
  "| **姓名** | " + str(meta['name']) + " |\n"
@@ -249,13 +326,12 @@ def show_random_sample(data_type):
249
  "| **班级** | " + str(meta['class']) + " |\n"
250
  "| **成绩** | " + str(meta['score']) + " 分 |\n"
251
  "| **任务类型** | " + task_map.get(sample['task_type'], sample['task_type']) + " |\n\n"
252
- "> ⚠️ 以上就是攻击者试图推断的**学生隐私信息**\n"
253
  )
254
- return info, sample['question'], sample['answer']
255
 
256
 
257
  def run_mia_demo(sample_index, data_type):
258
- # 判断成员/非成员
259
  if data_type == "成员数据(训练集)":
260
  is_member = True
261
  data = member_data
@@ -266,7 +342,6 @@ def run_mia_demo(sample_index, data_type):
266
  idx = min(int(sample_index), len(data) - 1)
267
  sample = data[idx]
268
 
269
- # 获取真实 loss
270
  bl = full_results.get('baseline', {})
271
  if is_member and idx < len(bl.get('member_losses', [])):
272
  loss = bl['member_losses'][idx]
@@ -283,108 +358,138 @@ def run_mia_demo(sample_index, data_type):
283
  actual_member = is_member
284
  attack_correct = (pred_member == actual_member)
285
 
286
- # 可视化进度条
287
- bar_total = 40
288
- if bl_nm_mean > bl_m_mean:
289
- ratio = (loss - bl_m_mean) / (bl_nm_mean - bl_m_mean)
290
- else:
291
- ratio = 0.5
292
- ratio = max(0.0, min(1.0, ratio))
293
- pos = int(bar_total * ratio)
294
- bar_visual = "=" * pos + "V" + "=" * (bar_total - pos)
295
-
296
- if pred_member:
297
- position_text = "成员区(左侧)⚠️ 隐私风险"
298
- else:
299
- position_text = "非成员区(右侧)✅ 相对安全"
300
 
301
  if pred_member:
302
- pred_text = "🔴 是训练成员(Loss < 阈值,模型过于熟悉)"
 
303
  else:
304
- pred_text = "🟢 不是训练成员(Loss >= 阈值,模型不熟悉)"
 
305
 
306
  if actual_member:
307
- actual_text = "🔴 是训练成员(此数据参与了训练)"
 
308
  else:
309
- actual_text = "🟢 不是训练成员(此数据未参与训练)"
 
310
 
311
  if attack_correct and pred_member and actual_member:
312
- result_text = "**攻击成功 隐私泄露**"
 
313
  elif attack_correct:
314
- result_text = "**判断正确**"
 
315
  else:
316
- result_text = "**攻击失误**"
 
317
 
318
  if pred_member:
319
  warning = (
320
- "⚠️ **隐私风险** 此样本 Loss = " + f"{loss:.4f}"
321
- + " 低于阈值" + f"{threshold:.4f}"
322
- + ",模型对它过于'熟悉',学生隐私可能被推断"
323
  )
324
  else:
325
  warning = (
326
- " 此样本 Loss = " + f"{loss:.4f}"
327
- + " 高于阈值" + f"{threshold:.4f}"
328
- + ",模型对其无特殊记忆,隐私相对安全。"
329
  )
330
 
331
- viz = (
332
- " 成员区(低Loss) 非成员区(高Loss)\n"
333
- " <-----------------------|------------------------->\n"
334
- " 阈值\n"
335
- "\n"
336
- " [" + bar_visual + "]\n"
337
- " | | |\n"
338
- " 成员均值 阈值 非成员均值\n"
339
- " " + f"{bl_m_mean:.4f}" + " "
340
- + f"{threshold:.4f}" + " "
341
- + f"{bl_nm_mean:.4f}" + "\n"
342
- "\n"
343
- " 当前 Loss = " + f"{loss:.4f}" + "\n"
344
- " 位置: " + position_text + "\n"
345
- )
346
-
347
  result_md = (
348
- "## 🔍 MIA 攻击结果\n\n"
349
- "### 📊 Loss 计算\n\n"
350
  "| 指标 | 值 |\n"
351
  "|------|-----|\n"
352
- "| **样本 Loss** | `" + f"{loss:.6f}" + "` |\n"
353
- "| **判定阈值** | `" + f"{threshold:.6f}" + "` |\n"
354
- "| **成员平均 Loss** | `" + f"{bl_m_mean:.6f}" + "` |\n"
355
- "| **非成员平均 Loss** | `" + f"{bl_nm_mean:.6f}" + "` |\n\n"
356
- "### 📏 Loss 位置可视化\n\n"
357
- "```\n"
358
- + viz
359
- + "```\n\n"
360
- "### 🎯 攻击判定\n\n"
361
  "| 项目 | 结果 |\n"
362
  "|------|------|\n"
363
- "| **攻击者预测** | " + pred_text + " |\n"
364
- "| **实际身份** | " + actual_text + " |\n"
365
- "| **攻击结果** | " + result_text + " |\n\n"
366
- "### 💡 原理说明\n\n"
367
- "模型对**训练过的数据**产生**更低的 Loss**(更\"自信\"),"
368
- "攻击者利用这一统计差异推断成员身份:\n\n"
369
- "- Loss **低于** 阈值 " + f"{threshold:.4f}" + " → 判定为**训练成员** → ⚠️ 隐私风险\n"
370
- "- Loss **高于** 阈值 " + f"{threshold:.4f}" + " → 判定为**非成员** → ✅ 相对安全\n\n"
371
- + warning + "\n\n"
372
- "> 📌 本演示使用实验中保存的真实 Loss 数据。\n"
373
  )
374
 
375
- question_display = "**📝 第 " + str(idx) + " 号样本**\n\n" + sample['question'][:600]
376
- return question_display, result_md
377
 
378
 
379
  # ========================================
380
  # 4. 构建界面
381
  # ========================================
382
 
383
- custom_css = (
384
- ".gradio-container { max-width: 1280px !important; margin: auto !important; }\n"
385
- ".tab-nav button { font-size: 15px !important; padding: 10px 18px !important; font-weight: 600 !important; }\n"
386
- "footer { display: none !important; }\n"
387
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
  with gr.Blocks(
390
  title="教育大模型隐私攻防实验",
@@ -393,88 +498,84 @@ with gr.Blocks(
393
  ) as demo:
394
 
395
  # ============================
396
- # 顶部标题
397
  # ============================
398
  gr.Markdown(
399
- "# 🎓 教育大模型中的成员推理攻击及其防御研究\n"
400
- "### Membership Inference Attack & Defense in Educational LLMs\n\n"
401
- "---\n\n"
402
- "> **研究目标**:探究教育场景下大语言模型的隐私泄露风险,"
403
- "验证**标签平滑**和**输出扰动**两种防御策略的效果与局限。\n\n"
404
- "> **技术栈**:`Qwen2.5-Math-1.5B` · `LoRA微调` · `Loss-based MIA` · "
405
- "`标签平滑` · `输出扰动`\n"
406
  )
407
 
408
  # ============================
409
  # Tab 1: 项目概览
410
  # ============================
411
- with gr.Tab("🏠 项目概览"):
412
  gr.Markdown(
413
- "## 📖 研究背景\n\n"
414
- "随着大语言模型在教育领域广泛应用(智能辅导、个性化学习等),"
415
- "模型训练不可避免地接触到学生隐私数据(姓名、学号、成绩等)\n\n"
416
- "**成员推理攻击MIA** 可以判断某条数据是否用于模型训练,从而推断学生隐私。\n\n"
 
417
  "---\n\n"
418
- "## 🔬 研究设计\n\n"
419
  "| 阶段 | 内容 | 说明 |\n"
420
  "|------|------|------|\n"
421
- "| 📂 数据准备 | 2000条小学数学辅导对话 | 含姓名、学号、成绩等隐私 |\n"
422
- "| 🧠 模型训练 | Qwen2.5-Math + LoRA | 基线 + 两个标签平滑模型 |\n"
423
- "| ⚔️ 攻击测试 | Loss-based MIA | 基于Loss判断成员身份 |\n"
424
- "| 🛡️ 训练期防御 | 标签平滑 (e=0.02, 0.2) | 训练时正则化 |\n"
425
- "| 🛡️ 推理期防御 | 输出扰动 (s=0.01~0.02) | 推理加噪声 |\n"
426
- "| 📊 综合评估 | 隐私-效用权衡 | AUC + 准确率 |\n\n"
427
  "---\n\n"
428
- "## ⚙️ 实验配置\n\n"
429
  "| 配置项 | 值 |\n"
430
  "|--------|-----|\n"
431
- "| **基座模型** | " + model_name_str + " |\n"
432
- "| **微调方法** | LoRA (r=8, alpha=16) |\n"
433
- "| **训练轮数** | 10 epochs |\n"
434
- "| **数据总量** | " + str(data_size_str) + " 条成员1000 + 非成员1000|\n"
435
- "| **GPU** | " + gpu_name_str + " |\n"
436
- "| **实验日期** | " + setup_date_str + " |\n\n"
437
  "---\n\n"
438
- "## 📐 技术路线\n\n"
439
- "```\n"
440
- "+----------+ +-----------+ +----------+ +----------+ +----------+\n"
441
- "| 数据生成 |--->| 基线训练 |--->| MIA攻击 |--->| 防御部署 |--->| 综合评估 |\n"
442
- "| (2000条) | | (LoRA) | | (Loss) | | (LS+OP) | | (AUC+Acc)|\n"
443
- "+----------+ +-----+-----+ +----------+ +----------+ +----------+\n"
444
- " | |\n"
445
- " +--- 标签平滑模型训练 -----------+\n"
446
- " (e=0.02, e=0.2)\n"
447
- "```\n"
448
  )
449
 
450
  # ============================
451
  # Tab 2: 数据展示
452
  # ============================
453
- with gr.Tab("📊 数据展示"):
454
  gr.Markdown(
455
- "## 📂 数据集概况\n\n"
456
- "- **成员数据(训练集)**1000条,用于训练模型\n"
457
- "- **非成员数据(测试集)**1000条,不参与训练\n"
458
- "- 每条数据包含**学生隐私信息**(姓名、学号、班级、成绩)\n"
459
  )
460
 
461
  with gr.Row():
462
  with gr.Column(scale=1):
463
- gr.Markdown("### 📊 任务类型分布")
464
  gr.Plot(value=make_pie_chart())
465
  with gr.Column(scale=1):
466
- gr.Markdown("### 🔍 随机查看样本")
467
  data_sel = gr.Radio(
468
  choices=["成员数据(训练集)", "非成员数据(测试集)"],
469
  value="成员数据(训练集)",
470
- label="选择数据类型"
471
  )
472
- sample_btn = gr.Button("🎲 随机抽取样本", variant="primary")
473
 
474
  sample_info = gr.Markdown()
475
  with gr.Row():
476
- sample_q = gr.Textbox(label="📝 学生提问", lines=7, interactive=False)
477
- sample_a = gr.Textbox(label="💡 模型回答", lines=7, interactive=False)
478
 
479
  sample_btn.click(
480
  fn=show_random_sample,
@@ -483,17 +584,16 @@ with gr.Blocks(
483
  )
484
 
485
  # ============================
486
- # Tab 3: MIA 攻击演示
487
  # ============================
488
- with gr.Tab("⚔️ MIA攻击演示"):
489
  gr.Markdown(
490
- "## ⚔️ 实时成员推理攻击\n\n"
491
- "**攻击原理**模型对训练过的数据产生更低的Loss(更\"自信\"),"
492
- "攻击者利用Loss阈值判断成员身份。\n\n"
493
- "### 📌 操作步骤\n"
494
- "1️⃣ 选择数据来源(成员/非成员)\n"
495
- "2️⃣ 拖动滑块选择样本编号\n"
496
- "3️⃣ 点击 **\"执行攻击\"** 查看结果\n"
497
  )
498
 
499
  with gr.Row():
@@ -501,189 +601,211 @@ with gr.Blocks(
501
  atk_data_type = gr.Radio(
502
  choices=["成员数据(训练集)", "非成员数据(测试集)"],
503
  value="成员数据(训练集)",
504
- label="📂 数据来源"
505
  )
506
  atk_index = gr.Slider(
507
  minimum=0, maximum=999, step=1, value=0,
508
- label="📌 样本编号 (0-999)"
509
  )
510
- atk_btn = gr.Button("⚔️ 执行MIA攻击", variant="primary", size="lg")
511
  with gr.Column(scale=1):
512
  atk_question = gr.Markdown()
513
 
 
514
  atk_result = gr.Markdown()
515
 
516
  atk_btn.click(
517
  fn=run_mia_demo,
518
  inputs=[atk_index, atk_data_type],
519
- outputs=[atk_question, atk_result]
520
  )
521
 
522
  # ============================
523
  # Tab 4: 防御对比
524
  # ============================
525
- with gr.Tab("🛡️ 防御对比"):
526
  gr.Markdown(
527
- "## 🛡️ 防御策略效果对比\n\n"
528
- "| 策略 | 类型 | 原理 | 优 | 缺点 |\n"
529
  "|------|------|------|------|------|\n"
530
- "| **标签平滑** | 训练期 | 软化标签防止过拟合 | 从根降低记忆 | 可能损失效用 |\n"
531
- "| **输出扰动** | 推理期 | Loss加高斯噪声 | 零效用损失 | 遮蔽统计信号 |\n"
532
  )
533
 
534
  with gr.Row():
535
  with gr.Column():
536
- gr.Markdown("### 📊 所有防御策略AUC对比")
537
  gr.Plot(value=make_auc_bar())
538
  with gr.Column():
539
- gr.Markdown("### 📈 Loss分布对比")
540
  gr.Plot(value=make_loss_distribution())
541
 
542
- # 结果表格
543
  table = (
544
- "### 📋 完整实验结果\n\n"
545
- "| 策略 | 类型 | AUC | 隐私风险 |\n"
546
  "|------|------|-----|----------|\n"
547
  )
548
- for k, name, cat in [('baseline', '基线无防御', ''),
549
- ('smooth_0.02', '标签平滑 e=0.02', '训练期'),
550
- ('smooth_0.2', '标签平滑 e=0.2', '训练期')]:
551
  if k in mia_results:
552
  a = mia_results[k]['auc']
553
- table += "| " + name + " | " + cat + " | **" + f"{a:.4f}" + "** | " + risk_badge(a) + " |\n"
554
- for k, name in [('perturbation_0.01', '输出扰动 s=0.01'),
555
- ('perturbation_0.015', '输出扰动 s=0.015'),
556
- ('perturbation_0.02', '输出扰动 s=0.02')]:
557
  if k in perturb_results:
558
  a = perturb_results[k]['auc']
559
- table += "| " + name + " | 推理期 | **" + f"{a:.4f}" + "** | " + risk_badge(a) + " |\n"
560
  gr.Markdown(table)
561
 
562
  # ============================
563
- # Tab 5: 输出扰动
564
  # ============================
565
- with gr.Tab("🔊 输出扰动"):
566
  gr.Markdown(
567
- "## 🔊 输出扰动防御详解\n\n"
568
- "### 📌 核心思想\n\n"
569
- "在**推理阶段**,对模型返回的Loss值添加**高斯噪声**:\n\n"
570
- "**Loss_new = Loss_original + N(0, sigma^2)**\n\n"
571
- "### 最大优势\n"
572
- "- **不需要重新训练模型**(部署成为零)\n"
573
- "- **不影响模型效用**(准确率完全不变)\n"
574
- "- 噪声强度sigma可以动态调节\n\n"
575
- "### 📊 实验结果\n\n"
576
- "| sigma | AUC | 相比基线降低 | 准确率 | 说明 |\n"
577
- "|-------|-----|-------------|--------|------|\n"
578
- "| 0(基线)| **" + f"{bl_auc:.4f}" + "** | — | " + f"{bl_acc:.1f}" + "% | 防御 |\n"
579
- "| 0.01 | **" + f"{op001_auc:.4f}" + "** | " + f"{bl_auc - op001_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "%(不变)| 温和 |\n"
580
- "| 0.015 | **" + f"{op0015_auc:.4f}" + "** | ↓" + f"{bl_auc - op0015_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "%(不变)| 适中 |\n"
581
- "| 0.02 | **" + f"{op002_auc:.4f}" + "** | ↓" + f"{bl_auc - op002_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "%(不变)| **推荐** |\n\n"
582
- "### 💡 核心发现\n\n"
583
- "> 输出扰动 (s=0.02) 将AUC从 " + f"{bl_auc:.4f}" + " 降至 **" + f"{op002_auc:.4f}" + "**,"
584
- "准确率 **" + f"{bl_acc:.1f}" + "%** 完全不变 — 真正的**零成本防御**!\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  )
586
 
587
  # ============================
588
  # Tab 6: 效用评估
589
  # ============================
590
- with gr.Tab("📝 效用评估"):
591
  gr.Markdown(
592
- "## 📐 模型效用评估\n\n"
593
- "> 防御不能\"只管隐私不管效果\"。本节评估各模型在 **300道数学题** 上的准确率。\n"
594
  )
595
 
596
  with gr.Row():
597
  with gr.Column():
598
- gr.Markdown("### 📊 准确率对比")
599
  gr.Plot(value=make_accuracy_bar())
600
  with gr.Column():
601
- gr.Markdown("### ⚖️ 隐私-效用权衡")
602
  gr.Plot(value=make_tradeoff())
603
 
604
  ut = (
605
- "### 📋 效用评估详情\n\n"
606
- "| 策略 | 准确率 | AUC | 风险 | 效用影响 |\n"
607
- "|------|--------|-----|------|----------|\n"
608
  )
609
- for k, name in [('baseline', '基线'), ('smooth_0.02', '标签平滑 e=0.02'),
610
- ('smooth_0.2', '标签平滑 e=0.2')]:
611
  if k in utility_results and k in mia_results:
612
  acc = utility_results[k]['accuracy'] * 100
613
  auc = mia_results[k]['auc']
614
- impact = "" if k == 'baseline' else ("提升" if acc > bl_acc else "⚠️ 下降")
615
- ut += "| " + name + " | **" + f"{acc:.1f}" + "%** | " + f"{auc:.4f}" + " | " + risk_badge(auc) + " | " + impact + " |\n"
616
- for k, name in [('perturbation_0.01', '输出扰动 s=0.01'), ('perturbation_0.02', '输出扰动 s=0.02')]:
617
  if k in perturb_results:
618
- ut += "| " + name + " | **" + f"{bl_acc:.1f}" + "%** | " + f"{perturb_results[k]['auc']:.4f}" + " | " + risk_badge(perturb_results[k]['auc']) + " | 无影响 |\n"
619
  gr.Markdown(ut)
620
 
621
  # ============================
622
  # Tab 7: 论文图表
623
  # ============================
624
- with gr.Tab("📄 论文图表"):
625
- gr.Markdown("## 📄 学术级论文图表300 DPI")
626
-
627
- for fn, cap in [("fig1_loss_distribution_comparison.png", "图1:Loss分布对比"),
628
- ("fig2_privacy_utility_tradeoff_fixed.png", "图2:隐私-用权衡"),
629
- ("fig3_defense_comparison_bar.png", "图3:防御效果柱状图")]:
630
  path = os.path.join(BASE_DIR, "figures", fn)
631
  if os.path.exists(path):
632
  gr.Markdown("### " + cap)
633
  gr.Image(value=path, show_label=False, height=420)
634
  gr.Markdown("---")
635
  else:
636
- gr.Markdown("### " + cap + "\n\n> ⚠️ 文件未找到" + fn + "(不影响核心功能)")
637
 
638
  # ============================
639
  # Tab 8: 研究结论
640
  # ============================
641
- with gr.Tab("🎓 研究结论"):
642
  gr.Markdown(
643
- "## 📝 核心结论\n\n"
644
  "---\n\n"
645
- "### 发现:MIA对教育大模型现实威胁\n\n"
646
- "基线模型AUC = **" + f"{bl_auc:.4f}" + "**远高随机猜测(0.5,"
647
- "攻击者可以较高概率推断学生隐私。\n\n"
648
- "### 发现二:标签平滑是有效训练期防御\n\n"
649
- "| 策略 | AUC | 准确 | 评价 |\n"
650
- "|------|-----|--------|------|\n"
651
- "| 基线(无防御)| " + f"{bl_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% | 隐私风险高 |\n"
652
- "| 标签平滑 e=0.02 | " + f"{s002_auc:.4f}" + " | " + f"{s002_acc:.1f}" + "% | ✅ **推荐** |\n"
653
- "| 标签平滑 e=0.2 | " + f"{s02_auc:.4f}" + " | " + f"{s02_acc:.1f}" + "% | ⚠️ 防御强但效用受影响 |\n\n"
654
- "### 发现三:输出扰动是零成本的推理期防御\n\n"
655
- "s=0.02 将AUC从 " + f"{bl_auc:.4f}" + " 降至 **" + f"{op002_auc:.4f}" + "**,准确率**不变**。\n\n"
656
- "### 发现四:双重防御可叠加使用\n\n"
657
- "> **推荐方案**:标签平滑 e=0.02(训练期)+ 输出扰动 s=0.02(推理期)= **双重防护**\n\n"
658
  "---\n\n"
659
- "### 🎤 答辩话术\n\n"
660
- "> \"研究以小学数学智能辅导系统为场景使用Qwen2.5-Math-1.5B + LoRA微调。\n"
661
- "> 基线模型AUC=" + f"{bl_auc:.4f}" + ",存在显著隐私泄露风险。\n"
662
- "> 通过**训练期标签平滑**(e=0.02)和**推理期输出扰动**(s=0.02)两种防御,\n"
663
- "> 有效降低了攻击成功率,其中输出扰动实现了**零效用损失**。\n"
664
- "> 研究揭示了教育AI领域隐私保护与模型效用之间的权衡关系。\"\n\n"
 
 
 
665
  "---\n\n"
666
- "### 📚 创新点\n\n"
667
- "1. **场景新颖** — 聚焦教育领域LLM隐私(而非通用NLP)\n"
668
- "2. **双重防御** — 同时研究训练期 + 推理期防御策略\n"
669
- "3. **工程可行** 标签平滑一行代码,输出扰动一行代码\n"
670
- "4. **实验完整** — 攻击 + 防御 + 效用评估 + 权衡分析\n\n"
 
 
671
  "---\n\n"
672
- "### 🔮 未来工作\n\n"
673
- "- ��索**差分隐私 (DP-SGD)** 等更强防御\n"
674
- "- 测试 **Shadow Model Attack** 等更强攻击\n"
675
- "- 在真实教育数据集上验证\n"
676
- "- 研究**联邦学习**框架下的教育模型隐私\n"
 
 
 
 
 
 
677
  )
678
 
679
  # ============================
680
  # 底部
681
  # ============================
682
  gr.Markdown(
683
- "---\n"
684
  "<center>\n\n"
685
- "🎓 **教育大模型中的成员推理攻击及其防御思路研究**\n\n"
686
- "`Qwen2.5-Math-1.5B` · `LoRA` · `MIA` · `标签平滑` · `输出扰动` · `Gradio`\n\n"
687
  "</center>\n"
688
  )
689
 
 
 
 
 
 
 
1
  import os
2
  import json
3
+ import io
4
+ import re
5
  import numpy as np
6
  import matplotlib
7
  matplotlib.use('Agg')
 
9
  import gradio as gr
10
 
11
  # ========================================
12
+ # 1. 数据加载
13
  # ========================================
 
14
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
15
 
16
 
17
  def load_json(path):
18
+ with open(os.path.join(BASE_DIR, path), 'r', encoding='utf-8') as f:
 
19
  return json.load(f)
20
 
21
 
22
+ def clean_text(text):
23
+ """清理文本中的特殊字符和emoji,防止乱码"""
24
+ # 移除emoji和特殊Unicode字符
25
+ text = re.sub(r'[\U00010000-\U0010ffff]', '', text)
26
+ # 移除其他可能导致乱码的字符
27
+ text = text.encode('utf-8', errors='ignore').decode('utf-8')
28
+ return text
29
+
30
+
31
  member_data = load_json("data/member.json")
32
  non_member_data = load_json("data/non_member.json")
33
  mia_results = load_json("results/mia_results.json")
 
36
  full_results = load_json("results/mia_full_results.json")
37
  config = load_json("config.json")
38
 
 
39
  plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
40
  plt.rcParams['axes.unicode_minus'] = False
41
 
 
53
 
54
  bl_m_mean = mia_results.get('baseline', {}).get('member_loss_mean', 0.19)
55
  bl_nm_mean = mia_results.get('baseline', {}).get('non_member_loss_mean', 0.23)
56
+ bl_m_std = mia_results.get('baseline', {}).get('member_loss_std', 0.03)
57
+ bl_nm_std = mia_results.get('baseline', {}).get('non_member_loss_std', 0.03)
58
 
59
  model_name_str = config.get('model_name', 'Qwen/Qwen2.5-Math-1.5B-Instruct')
60
  gpu_name_str = config.get('gpu_name', 'T4')
61
+ data_size_str = str(config.get('data_size', 2000))
62
  setup_date_str = config.get('setup_date', 'N/A')
63
 
64
 
 
79
  }
80
  labels = [name_map.get(k, k) for k in task_counts]
81
  sizes = list(task_counts.values())
82
+ colors = ['#5B8FF9', '#5AD8A6', '#F6BD16', '#E86452']
83
+ fig, ax = plt.subplots(figsize=(7, 5.5))
84
+ wedges, texts, autotexts = ax.pie(
85
  sizes, labels=labels, autopct='%1.1f%%',
86
+ colors=colors[:len(labels)],
87
+ startangle=90, textprops={'fontsize': 11},
88
+ wedgeprops={'edgecolor': 'white', 'linewidth': 2}
89
  )
90
+ for t in autotexts:
91
+ t.set_fontsize(11)
92
+ t.set_fontweight('bold')
93
+ ax.set_title('Task Type Distribution', fontsize=14, fontweight='bold', pad=12)
94
  plt.tight_layout()
95
  return fig
96
 
97
 
98
  def make_loss_distribution():
99
  plot_items = []
100
+ for k, t in [('baseline', 'Baseline'), ('smooth_0.02', 'LS (e=0.02)'), ('smooth_0.2', 'LS (e=0.2)')]:
101
  if k in full_results:
102
  auc = mia_results.get(k, {}).get('auc', 0)
103
+ plot_items.append((k, t + " | AUC=" + f"{auc:.4f}"))
104
  n = len(plot_items)
105
  if n == 0:
106
  fig, ax = plt.subplots()
107
  ax.text(0.5, 0.5, 'No data', ha='center')
108
  return fig
109
+ fig, axes = plt.subplots(1, n, figsize=(5.5 * n, 4.5))
110
  if n == 1:
111
  axes = [axes]
112
  for ax, (k, title) in zip(axes, plot_items):
113
  m = full_results[k]['member_losses']
114
  nm = full_results[k]['non_member_losses']
115
+ bins = np.linspace(min(min(m), min(nm)), max(max(m), max(nm)), 35)
116
+ ax.hist(m, bins=bins, alpha=0.6, color='#5B8FF9', label='Member', density=True)
117
+ ax.hist(nm, bins=bins, alpha=0.6, color='#E86452', label='Non-Member', density=True)
 
 
118
  ax.set_title(title, fontsize=12, fontweight='bold')
119
+ ax.set_xlabel('Loss', fontsize=10)
120
+ ax.set_ylabel('Density', fontsize=10)
121
+ ax.legend(fontsize=9, framealpha=0.9)
122
+ ax.grid(True, linestyle='--', alpha=0.3)
123
+ ax.spines['top'].set_visible(False)
124
+ ax.spines['right'].set_visible(False)
125
  plt.tight_layout()
126
  return fig
127
 
128
 
129
  def make_auc_bar():
130
  methods, aucs, colors = [], [], []
131
+ items = [
132
+ ('baseline', 'Baseline', '#8C8C8C'),
133
+ ('smooth_0.02', 'LS (e=0.02)', '#5B8FF9'),
134
+ ('smooth_0.2', 'LS (e=0.2)', '#3D76DD'),
135
+ ]
136
+ for k, name, c in items:
137
  if k in mia_results:
138
  methods.append(name)
139
  aucs.append(mia_results[k]['auc'])
140
  colors.append(c)
141
+ p_items = [
142
+ ('perturbation_0.01', 'OP (s=0.01)', '#5AD8A6'),
143
+ ('perturbation_0.015', 'OP (s=0.015)', '#2EAD78'),
144
+ ('perturbation_0.02', 'OP (s=0.02)', '#1A7F5A'),
145
+ ]
146
+ for k, name, c in p_items:
147
  if k in perturb_results:
148
  methods.append(name)
149
  aucs.append(perturb_results[k]['auc'])
150
  colors.append(c)
151
+ fig, ax = plt.subplots(figsize=(10, 5.5))
152
+ bars = ax.bar(methods, aucs, color=colors, width=0.52, edgecolor='white', linewidth=1.5)
153
  for bar, a in zip(bars, aucs):
154
+ ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.003,
155
+ f'{a:.4f}', ha='center', va='bottom', fontsize=11, fontweight='bold')
156
+ ax.axhline(y=0.5, color='#E86452', linestyle='--', linewidth=1.5, alpha=0.7, label='Random Guess (0.5)')
157
+ ax.set_ylabel('MIA AUC', fontsize=12)
158
+ ax.set_title('Defense Mechanisms - AUC Comparison', fontsize=14, fontweight='bold')
159
+ ax.set_ylim(0.48, max(aucs) + 0.04 if aucs else 1.0)
160
+ ax.legend(fontsize=10)
161
+ ax.grid(axis='y', linestyle='--', alpha=0.3)
162
+ ax.spines['top'].set_visible(False)
163
+ ax.spines['right'].set_visible(False)
164
+ plt.xticks(rotation=8)
165
  plt.tight_layout()
166
  return fig
167
 
168
 
169
  def make_tradeoff():
170
+ fig, ax = plt.subplots(figsize=(9, 6.5))
171
  points = []
172
  for k, name, marker, color, sz in [
173
+ ('baseline', 'Baseline', 'o', '#8C8C8C', 200),
174
+ ('smooth_0.02', 'LS (e=0.02)', 's', '#5B8FF9', 180),
175
+ ('smooth_0.2', 'LS (e=0.2)', 's', '#3D76DD', 180)]:
176
  if k in mia_results and k in utility_results:
177
  points.append({'name': name, 'auc': mia_results[k]['auc'],
178
  'acc': utility_results[k]['accuracy'],
179
  'marker': marker, 'color': color, 'size': sz})
180
  base_acc = utility_results.get('baseline', {}).get('accuracy', 0.633)
181
  for k, name, marker, color, sz in [
182
+ ('perturbation_0.01', 'OP (s=0.01)', '^', '#5AD8A6', 190),
183
+ ('perturbation_0.02', 'OP (s=0.02)', '^', '#1A7F5A', 190)]:
184
  if k in perturb_results:
185
  points.append({'name': name, 'auc': perturb_results[k]['auc'],
186
  'acc': base_acc, 'marker': marker, 'color': color, 'size': sz})
187
  for p in points:
188
  ax.scatter(p['acc'], p['auc'], label=p['name'], marker=p['marker'],
189
+ color=p['color'], s=p['size'], edgecolors='white', linewidth=2, zorder=5)
190
+ ax.axhline(y=0.5, color='#BFBFBF', linestyle='--', alpha=0.8, label='Random Guess')
191
+ ax.set_xlabel('Model Utility (Accuracy)', fontsize=12, fontweight='bold')
192
+ ax.set_ylabel('Privacy Risk (MIA AUC)', fontsize=12, fontweight='bold')
193
  ax.set_title('Privacy-Utility Trade-off', fontsize=14, fontweight='bold')
194
  all_acc = [p['acc'] for p in points]
195
  all_auc = [p['auc'] for p in points]
196
  if all_acc and all_auc:
197
  ax.set_xlim(min(all_acc) - 0.03, max(all_acc) + 0.05)
198
+ ax.set_ylim(min(min(all_auc), 0.5) - 0.02, max(all_auc) + 0.025)
199
+ ax.legend(loc='upper right', frameon=True, fontsize=9, fancybox=True)
200
+ ax.grid(True, alpha=0.2)
201
+ ax.spines['top'].set_visible(False)
202
+ ax.spines['right'].set_visible(False)
203
  plt.tight_layout()
204
  return fig
205
 
206
 
207
  def make_accuracy_bar():
208
  names, accs, colors = [], [], []
209
+ for k, name, c in [('baseline', 'Baseline', '#8C8C8C'), ('smooth_0.02', 'LS (e=0.02)', '#5B8FF9'),
210
+ ('smooth_0.2', 'LS (e=0.2)', '#3D76DD')]:
211
  if k in utility_results:
212
  names.append(name)
213
  accs.append(utility_results[k]['accuracy'] * 100)
214
  colors.append(c)
215
  base_pct = utility_results.get('baseline', {}).get('accuracy', 0) * 100
216
+ for k, name, c in [('perturbation_0.01', 'OP (s=0.01)', '#5AD8A6'),
217
+ ('perturbation_0.02', 'OP (s=0.02)', '#1A7F5A')]:
218
  if k in perturb_results:
219
  names.append(name)
220
  accs.append(base_pct)
221
  colors.append(c)
222
+ fig, ax = plt.subplots(figsize=(10, 5.5))
223
  bars = ax.bar(names, accs, color=colors, width=0.5, edgecolor='white', linewidth=1.5)
224
  for bar, acc in zip(bars, accs):
225
+ ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.6,
226
+ f'{acc:.1f}%', ha='center', va='bottom', fontsize=11, fontweight='bold')
227
+ ax.set_ylabel('Accuracy (%)', fontsize=12)
228
  ax.set_title('Model Utility (300 Math Questions)', fontsize=14, fontweight='bold')
229
  ax.set_ylim(0, 100)
230
  ax.grid(axis='y', alpha=0.3)
231
+ ax.spines['top'].set_visible(False)
232
+ ax.spines['right'].set_visible(False)
233
+ plt.xticks(rotation=8)
234
+ plt.tight_layout()
235
+ return fig
236
+
237
+
238
+ def make_loss_gauge(loss_val, m_mean, nm_mean, threshold):
239
+ """生成精致的Loss位置可视化图表(替代粗糙的ASCII字符画)"""
240
+ fig, ax = plt.subplots(figsize=(8, 2.5))
241
+
242
+ # 绘制底部色条
243
+ x_min = min(m_mean - 3 * bl_m_std, loss_val - 0.01)
244
+ x_max = max(nm_mean + 3 * bl_nm_std, loss_val + 0.01)
245
+
246
+ # 成员区域(蓝色渐变)
247
+ ax.axvspan(x_min, threshold, alpha=0.15, color='#5B8FF9')
248
+ # 非成员区域(红色渐变)
249
+ ax.axvspan(threshold, x_max, alpha=0.15, color='#E86452')
250
+
251
+ # 阈值线
252
+ ax.axvline(x=threshold, color='#595959', linewidth=2, linestyle='-', zorder=3)
253
+ ax.text(threshold, 1.15, 'Threshold', ha='center', va='bottom', fontsize=9,
254
+ fontweight='bold', color='#595959', transform=ax.get_xaxis_transform())
255
+
256
+ # 成员均值标记
257
+ ax.axvline(x=m_mean, color='#5B8FF9', linewidth=1.5, linestyle='--', alpha=0.7)
258
+ ax.text(m_mean, -0.25, f'Member\n({m_mean:.4f})', ha='center', va='top', fontsize=8,
259
+ color='#5B8FF9', transform=ax.get_xaxis_transform())
260
+
261
+ # 非成员均值标记
262
+ ax.axvline(x=nm_mean, color='#E86452', linewidth=1.5, linestyle='--', alpha=0.7)
263
+ ax.text(nm_mean, -0.25, f'Non-Member\n({nm_mean:.4f})', ha='center', va='top', fontsize=8,
264
+ color='#E86452', transform=ax.get_xaxis_transform())
265
+
266
+ # 当前样本标记(大箭头)
267
+ is_member_zone = loss_val < threshold
268
+ marker_color = '#5B8FF9' if is_member_zone else '#E86452'
269
+ ax.plot(loss_val, 0.5, marker='v', markersize=18, color=marker_color, zorder=5,
270
+ transform=ax.get_xaxis_transform())
271
+ label_text = f'Loss={loss_val:.4f}'
272
+ ax.text(loss_val, 0.75, label_text, ha='center', va='bottom', fontsize=10,
273
+ fontweight='bold', color=marker_color, transform=ax.get_xaxis_transform(),
274
+ bbox=dict(boxstyle='round,pad=0.3', facecolor='white', edgecolor=marker_color, alpha=0.9))
275
+
276
+ # 区域标签
277
+ member_center = (x_min + threshold) / 2
278
+ nonmember_center = (threshold + x_max) / 2
279
+ ax.text(member_center, 0.5, 'Member Zone', ha='center', va='center', fontsize=10,
280
+ color='#5B8FF9', fontweight='bold', alpha=0.6, transform=ax.get_xaxis_transform())
281
+ ax.text(nonmember_center, 0.5, 'Non-Member Zone', ha='center', va='center', fontsize=10,
282
+ color='#E86452', fontweight='bold', alpha=0.6, transform=ax.get_xaxis_transform())
283
+
284
+ ax.set_xlim(x_min, x_max)
285
+ ax.set_yticks([])
286
+ ax.spines['top'].set_visible(False)
287
+ ax.spines['right'].set_visible(False)
288
+ ax.spines['left'].set_visible(False)
289
+ ax.set_xlabel('Loss Value', fontsize=10)
290
  plt.tight_layout()
291
  return fig
292
 
293
 
294
  def risk_badge(auc_val):
295
  if auc_val > 0.62:
296
+ return "High"
297
  elif auc_val > 0.55:
298
+ return "Medium"
299
  else:
300
+ return "Low"
301
 
302
 
303
  # ========================================
 
318
  'error_correction': '错题订正'
319
  }
320
  info = (
321
+ "### 样本元信息(隐私字段)\n\n"
322
  "| 字段 | 值 |\n"
323
  "|------|-----|\n"
324
  "| **姓名** | " + str(meta['name']) + " |\n"
 
326
  "| **班级** | " + str(meta['class']) + " |\n"
327
  "| **成绩** | " + str(meta['score']) + " 分 |\n"
328
  "| **任务类型** | " + task_map.get(sample['task_type'], sample['task_type']) + " |\n\n"
329
+ "> 以上即为攻击者试图推断的 **学生隐私信息**\n"
330
  )
331
+ return info, clean_text(sample['question']), clean_text(sample['answer'])
332
 
333
 
334
  def run_mia_demo(sample_index, data_type):
 
335
  if data_type == "成员数据(训练集)":
336
  is_member = True
337
  data = member_data
 
342
  idx = min(int(sample_index), len(data) - 1)
343
  sample = data[idx]
344
 
 
345
  bl = full_results.get('baseline', {})
346
  if is_member and idx < len(bl.get('member_losses', [])):
347
  loss = bl['member_losses'][idx]
 
358
  actual_member = is_member
359
  attack_correct = (pred_member == actual_member)
360
 
361
+ # 生成精致的可视化图表
362
+ gauge_fig = make_loss_gauge(loss, bl_m_mean, bl_nm_mean, threshold)
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
  if pred_member:
365
+ pred_text = "是训练成员(Loss < 阈值,模型过于熟悉)"
366
+ pred_icon = "🔴"
367
  else:
368
+ pred_text = "训练成员(Loss >= 阈值,模型不熟悉)"
369
+ pred_icon = "🟢"
370
 
371
  if actual_member:
372
+ actual_text = "是训练成员(此数据参与了训练)"
373
+ actual_icon = "🔴"
374
  else:
375
+ actual_text = "训练成员(此数据未参与训练)"
376
+ actual_icon = "🟢"
377
 
378
  if attack_correct and pred_member and actual_member:
379
+ result_text = "**攻击成功 -- 隐私泄露**"
380
+ result_icon = "⚠️"
381
  elif attack_correct:
382
+ result_text = "**判断正确**"
383
+ result_icon = "✅"
384
  else:
385
+ result_text = "**攻击失误**"
386
+ result_icon = "❌"
387
 
388
  if pred_member:
389
  warning = (
390
+ "> **隐私风险** : 此样本 Loss = " + f"{loss:.4f}"
391
+ + " 低于阈值 " + f"{threshold:.4f}"
392
+ + ",模型对它过于熟悉,学生隐私可能被推断"
393
  )
394
  else:
395
  warning = (
396
+ "> **相对安全** : 此样本 Loss = " + f"{loss:.4f}"
397
+ + " 高于阈值 " + f"{threshold:.4f}"
398
+ + ",模型对其无特殊记忆。"
399
  )
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  result_md = (
402
+ "### Loss 计算结果\n\n"
 
403
  "| 指标 | 值 |\n"
404
  "|------|-----|\n"
405
+ "| 样本 Loss | " + f"{loss:.6f}" + " |\n"
406
+ "| 判定阈值 | " + f"{threshold:.6f}" + " |\n"
407
+ "| 成员平均 Loss | " + f"{bl_m_mean:.6f}" + " |\n"
408
+ "| 非成员平均 Loss | " + f"{bl_nm_mean:.6f}" + " |\n\n"
409
+ "### 攻击判定\n\n"
 
 
 
 
410
  "| 项目 | 结果 |\n"
411
  "|------|------|\n"
412
+ "| 攻击者预测 | " + pred_icon + " " + pred_text + " |\n"
413
+ "| 实际身份 | " + actual_icon + " " + actual_text + " |\n"
414
+ "| 攻击结果 | " + result_icon + " " + result_text + " |\n\n"
415
+ + warning + "\n"
 
 
 
 
 
 
416
  )
417
 
418
+ question_display = "**第 " + str(idx) + " 号样本 :**\n\n" + clean_text(sample['question'][:600])
419
+ return question_display, gauge_fig, result_md
420
 
421
 
422
  # ========================================
423
  # 4. 构建界面
424
  # ========================================
425
 
426
+ custom_css = """
427
+ /* 整体容器 */
428
+ .gradio-container {
429
+ max-width: 1200px !important;
430
+ margin: auto !important;
431
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
432
+ }
433
+
434
+ /* Tab 按钮 */
435
+ .tab-nav button {
436
+ font-size: 14px !important;
437
+ padding: 10px 16px !important;
438
+ font-weight: 500 !important;
439
+ border-radius: 8px 8px 0 0 !important;
440
+ }
441
+ .tab-nav button.selected {
442
+ font-weight: 700 !important;
443
+ border-bottom: 3px solid #5B8FF9 !important;
444
+ }
445
+
446
+ /* 标题样式 */
447
+ .prose h1 {
448
+ font-size: 1.8rem !important;
449
+ color: #1a1a2e !important;
450
+ border-bottom: 2px solid #5B8FF9 !important;
451
+ padding-bottom: 8px !important;
452
+ }
453
+ .prose h2 {
454
+ font-size: 1.35rem !important;
455
+ color: #16213e !important;
456
+ margin-top: 1.2em !important;
457
+ }
458
+ .prose h3 {
459
+ font-size: 1.1rem !important;
460
+ color: #0f3460 !important;
461
+ }
462
+
463
+ /* 表格美化 */
464
+ .prose table {
465
+ border-collapse: collapse !important;
466
+ width: 100% !important;
467
+ font-size: 0.9rem !important;
468
+ }
469
+ .prose th {
470
+ background: #f0f5ff !important;
471
+ color: #1a1a2e !important;
472
+ font-weight: 600 !important;
473
+ padding: 10px 14px !important;
474
+ }
475
+ .prose td {
476
+ padding: 8px 14px !important;
477
+ border-bottom: 1px solid #eee !important;
478
+ }
479
+
480
+ /* 引用块 */
481
+ .prose blockquote {
482
+ border-left: 4px solid #5B8FF9 !important;
483
+ background: #f7f9fc !important;
484
+ padding: 12px 16px !important;
485
+ margin: 12px 0 !important;
486
+ border-radius: 0 6px 6px 0 !important;
487
+ }
488
+
489
+ /* 隐藏底部 */
490
+ footer { display: none !important; }
491
+ """
492
+
493
 
494
  with gr.Blocks(
495
  title="教育大模型隐私攻防实验",
 
498
  ) as demo:
499
 
500
  # ============================
501
+ # 顶部
502
  # ============================
503
  gr.Markdown(
504
+ "# 教育大模型中的成员推理攻击及其防御研究\n\n"
505
+ "> 探究教育场景下大语言模型的隐私泄露风险,"
506
+ "验证 **标签平滑** 与 **输出扰动** 两种防御策略的有效性及其对模型效用的影响。\n"
 
 
 
 
507
  )
508
 
509
  # ============================
510
  # Tab 1: 项目概览
511
  # ============================
512
+ with gr.Tab("项目概览"):
513
  gr.Markdown(
514
+ "## 研究背景\n\n"
515
+ "随着大语言模型在教育领域广泛应用(智能辅导系统、个性化学习推荐等),"
516
+ "模型训练过程中不可避免地接触到学生敏感数据。"
517
+ "**成员推理攻击 (Membership Inference Attack, MIA)** 可以判断某条数据是否参与了模型训练,"
518
+ "进而推断学生的隐私信息。\n\n"
519
  "---\n\n"
520
+ "## 研究设计\n\n"
521
  "| 阶段 | 内容 | 说明 |\n"
522
  "|------|------|------|\n"
523
+ "| 数据准备 | 2000条小学数学辅导对话 | 含姓名、学号、班级、成绩等隐私字段 |\n"
524
+ "| 模型训练 | Qwen2.5-Math-1.5B + LoRA | 基线模型 + 标签平滑模型 (e=0.02, 0.2) |\n"
525
+ "| 攻击测试 | Loss-based MIA | 利用模型输出Loss判断成员身份 |\n"
526
+ "| 训练期防御 | 标签平滑 | 软化训练标签,降低模型对训练数据的记忆程度 |\n"
527
+ "| 推理期防御 | 输出扰动 | 推理阶段对输出Loss添高斯噪声 |\n"
528
+ "| 综合评估 | 隐私-效用权衡分析 | AUC(隐私风险)+ 准确率(模型效用)|\n\n"
529
  "---\n\n"
530
+ "## 实验配置\n\n"
531
  "| 配置项 | 值 |\n"
532
  "|--------|-----|\n"
533
+ "| 基座模型 | " + model_name_str + " |\n"
534
+ "| 微调方法 | LoRA (r=8, alpha=16, target: q/k/v/o_proj) |\n"
535
+ "| 训练轮数 | 10 epochs |\n"
536
+ "| 数据总量 | " + data_size_str + " 条 (成员1000 + 非成员1000) |\n"
537
+ "| GPU | " + gpu_name_str + " |\n\n"
 
538
  "---\n\n"
539
+ "## 技术路线\n\n"
540
+ "| 步骤 | 阶段 | 方法 | 输出 |\n"
541
+ "|------|------|------|------|\n"
542
+ "| 1 | 数据生成 | 模板化生成2000条对话 | member.json + non_member.json |\n"
543
+ "| 2 | 基线训练 | LoRA微调Qwen2.5-Math | baseline模型 |\n"
544
+ "| 3 | 防御训练 | 标签平滑 (e=0.02, e=0.2) | smooth模型 x2 |\n"
545
+ "| 4 | MIA攻击 | 计算全量样本Loss,AUC评估 | mia_results.json |\n"
546
+ "| 5 | 输出扰动 | 对baseline Loss加高斯噪声 (s=0.01~0.02) | perturbation_results.json |\n"
547
+ "| 6 | 效用评估 | 300道数学测试题 | utility_results.json |\n"
548
+ "| 7 | 综合分析 | 隐私-效用权衡图 | 研究结论 |\n"
549
  )
550
 
551
  # ============================
552
  # Tab 2: 数据展示
553
  # ============================
554
+ with gr.Tab("数据展示"):
555
  gr.Markdown(
556
+ "## 数据集概况\n\n"
557
+ "- **成员数据(训练集)**: 1000条,用于模型微调训练\n"
558
+ "- **非成员数据(测试集)**: 1000条,不参与训练,作为MIA攻击的对照组\n"
559
+ "- 每条数据包含学生隐私字段(姓名、学号、班级、成绩),模拟真实教育场景\n"
560
  )
561
 
562
  with gr.Row():
563
  with gr.Column(scale=1):
564
+ gr.Markdown("### 任务类型分布")
565
  gr.Plot(value=make_pie_chart())
566
  with gr.Column(scale=1):
567
+ gr.Markdown("### 随机查看样本")
568
  data_sel = gr.Radio(
569
  choices=["成员数据(训练集)", "非成员数据(测试集)"],
570
  value="成员数据(训练集)",
571
+ label="数据类型"
572
  )
573
+ sample_btn = gr.Button("随机抽取样本", variant="primary")
574
 
575
  sample_info = gr.Markdown()
576
  with gr.Row():
577
+ sample_q = gr.Textbox(label="学生提问", lines=6, interactive=False)
578
+ sample_a = gr.Textbox(label="模型回答", lines=6, interactive=False)
579
 
580
  sample_btn.click(
581
  fn=show_random_sample,
 
584
  )
585
 
586
  # ============================
587
+ # Tab 3: MIA攻击演示
588
  # ============================
589
+ with gr.Tab("MIA攻击演示"):
590
  gr.Markdown(
591
+ "## 成员推理攻击演示\n\n"
592
+ "**原理**: 模型对训练过的数据产生更低的Loss,"
593
+ "攻击者利用Loss阈值的比较判断样本是否为训练成员。\n\n"
594
+ "1. 选择数据来源 (成员 / 非成员)\n"
595
+ "2. 拖动滑块选择样本编号\n"
596
+ "3. 点击 **执行攻击**\n"
 
597
  )
598
 
599
  with gr.Row():
 
601
  atk_data_type = gr.Radio(
602
  choices=["成员数据(训练集)", "非成员数据(测试集)"],
603
  value="成员数据(训练集)",
604
+ label="数据来源"
605
  )
606
  atk_index = gr.Slider(
607
  minimum=0, maximum=999, step=1, value=0,
608
+ label="样本编号 (0-999)"
609
  )
610
+ atk_btn = gr.Button("执行MIA攻击", variant="primary", size="lg")
611
  with gr.Column(scale=1):
612
  atk_question = gr.Markdown()
613
 
614
+ atk_gauge = gr.Plot(label="Loss位置可视化")
615
  atk_result = gr.Markdown()
616
 
617
  atk_btn.click(
618
  fn=run_mia_demo,
619
  inputs=[atk_index, atk_data_type],
620
+ outputs=[atk_question, atk_gauge, atk_result]
621
  )
622
 
623
  # ============================
624
  # Tab 4: 防御对比
625
  # ============================
626
+ with gr.Tab("防御对比"):
627
  gr.Markdown(
628
+ "## 防御策略效果对比\n\n"
629
+ "| 策略 | 类型 | 原理 | 优 | 局限 |\n"
630
  "|------|------|------|------|------|\n"
631
+ "| 标签平滑 | 训练期 | 软化one-hot标签,抑制过拟合 | 从根���降低模型记忆 | 可能影响模型效用 |\n"
632
+ "| 输出扰动 | 推理期 | 对输出Loss加高斯噪声 | 零效用损失,即插即用 | 遮蔽统计信号 |\n"
633
  )
634
 
635
  with gr.Row():
636
  with gr.Column():
637
+ gr.Markdown("### AUC对比(所有防御策略")
638
  gr.Plot(value=make_auc_bar())
639
  with gr.Column():
640
+ gr.Markdown("### Loss分布对比")
641
  gr.Plot(value=make_loss_distribution())
642
 
 
643
  table = (
644
+ "### 实验结果汇总\n\n"
645
+ "| 策略 | 类型 | AUC | 风险等级 |\n"
646
  "|------|------|-----|----------|\n"
647
  )
648
+ for k, name, cat in [('baseline', '基线 (无防御)', '--'),
649
+ ('smooth_0.02', '标签平滑 (e=0.02)', '训练期'),
650
+ ('smooth_0.2', '标签平滑 (e=0.2)', '训练期')]:
651
  if k in mia_results:
652
  a = mia_results[k]['auc']
653
+ table += "| " + name + " | " + cat + " | " + f"{a:.4f}" + " | " + risk_badge(a) + " |\n"
654
+ for k, name in [('perturbation_0.01', '输出扰动 (s=0.01)'),
655
+ ('perturbation_0.015', '输出扰动 (s=0.015)'),
656
+ ('perturbation_0.02', '输出扰动 (s=0.02)')]:
657
  if k in perturb_results:
658
  a = perturb_results[k]['auc']
659
+ table += "| " + name + " | 推理期 | " + f"{a:.4f}" + " | " + risk_badge(a) + " |\n"
660
  gr.Markdown(table)
661
 
662
  # ============================
663
+ # Tab 5: 防御详解(标签平滑 + 输出扰动
664
  # ============================
665
+ with gr.Tab("防御详解"):
666
  gr.Markdown(
667
+ "## 防御策略详解\n\n"
668
+ "---\n\n"
669
+ "### 一、标签平滑 (Label Smoothing)\n\n"
670
+ "**类型** : 训练期防御\n\n"
671
+ "**原理** : 将训练标签从硬标签 (one-hot) 转换为软标签,"
672
+ "降低模型对训练样的过度拟合程度,从而缩小成员与非成员之间的Loss差异。\n\n"
673
+ "**公式** : y_smooth = (1 - e) * y_onehot + e / V\n\n"
674
+ "其中 e 为平滑系数,V 为词汇表大小。\n\n"
675
+ "| 参数 | AUC | 准确率 | 分析 |\n"
676
+ "|------|-----|--------|------|\n"
677
+ "| 基线 (e=0) | " + f"{bl_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% | 无防御,MIA风险较高 |\n"
678
+ "| e=0.02 | " + f"{s002_auc:.4f}" + " | " + f"{s002_acc:.1f}" + "% | 温和防御,效用保持良好 |\n"
679
+ "| e=0.2 | " + f"{s02_auc:.4f}" + " | " + f"{s02_acc:.1f}" + "% | 强力防御,AUC显著下降 |\n\n"
680
+ "---\n\n"
681
+ "### 二、输出扰动 (Output Perturbation)\n\n"
682
+ "**类型** : 推理期防御\n\n"
683
+ "**原理** : 在推理阶段对模型返回的Loss值添加高斯噪声,"
684
+ "模糊成员与非成员之间的统计差异,使攻击者难以准确判别。\n\n"
685
+ "**公式** : Loss_perturbed = Loss_original + N(0, s^2)\n\n"
686
+ "**核心优势** : 不修改模型参数,准确率完全不变。\n\n"
687
+ "| 参数 | AUC | AUC降幅 | 准确率 |\n"
688
+ "|------|-----|---------|--------|\n"
689
+ "| 基线 (s=0) | " + f"{bl_auc:.4f}" + " | -- | " + f"{bl_acc:.1f}" + "% |\n"
690
+ "| s=0.01 | " + f"{op001_auc:.4f}" + " | " + f"{bl_auc - op001_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n"
691
+ "| s=0.015 | " + f"{op0015_auc:.4f}" + " | " + f"{bl_auc - op0015_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n"
692
+ "| s=0.02 | " + f"{op002_auc:.4f}" + " | " + f"{bl_auc - op002_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n\n"
693
+ "---\n\n"
694
+ "### 三、综合对比\n\n"
695
+ "| 维度 | 标签平滑 | 输出扰动 |\n"
696
+ "|------|---------|----------|\n"
697
+ "| 作用阶段 | 训练期 | 推理期 |\n"
698
+ "| 是否需要重训 | 是 | 否 |\n"
699
+ "| 对效用的影响 | 可能有影响 | 无影响 |\n"
700
+ "| 防御机制 | 降低过拟合 | 遮蔽统计信号 |\n"
701
+ "| 可叠加使用 | 是 | 是 |\n\n"
702
+ "> **推荐方案** : 标签平滑 (e=0.02) + 输出扰动 (s=0.02) 双重防护\n"
703
  )
704
 
705
  # ============================
706
  # Tab 6: 效用评估
707
  # ============================
708
+ with gr.Tab("效用评估"):
709
  gr.Markdown(
710
+ "## 模型效用评估\n\n"
711
+ "> 测试集: 300道数学题,覆盖基础计算、应用题、概念问答三类任务。\n"
712
  )
713
 
714
  with gr.Row():
715
  with gr.Column():
716
+ gr.Markdown("### 准确率对比")
717
  gr.Plot(value=make_accuracy_bar())
718
  with gr.Column():
719
+ gr.Markdown("### 隐私-效用权衡")
720
  gr.Plot(value=make_tradeoff())
721
 
722
  ut = (
723
+ "### 效用评估详情\n\n"
724
+ "| 策略 | 准确率 | AUC | 风险等级 | 效用影响 |\n"
725
+ "|------|--------|-----|---------|----------|\n"
726
  )
727
+ for k, name in [('baseline', '基线'), ('smooth_0.02', '标签平滑 (e=0.02)'),
728
+ ('smooth_0.2', '标签平滑 (e=0.2)')]:
729
  if k in utility_results and k in mia_results:
730
  acc = utility_results[k]['accuracy'] * 100
731
  auc = mia_results[k]['auc']
732
+ impact = "--" if k == 'baseline' else ("提升" if acc > bl_acc else "下降")
733
+ ut += "| " + name + " | " + f"{acc:.1f}" + "% | " + f"{auc:.4f}" + " | " + risk_badge(auc) + " | " + impact + " |\n"
734
+ for k, name in [('perturbation_0.01', '输出扰动 (s=0.01)'), ('perturbation_0.02', '输出扰动 (s=0.02)')]:
735
  if k in perturb_results:
736
+ ut += "| " + name + " | " + f"{bl_acc:.1f}" + "% | " + f"{perturb_results[k]['auc']:.4f}" + " | " + risk_badge(perturb_results[k]['auc']) + " | 无影响 |\n"
737
  gr.Markdown(ut)
738
 
739
  # ============================
740
  # Tab 7: 论文图表
741
  # ============================
742
+ with gr.Tab("论文图表"):
743
+ gr.Markdown("## 学术图表 (300 DPI)")
744
+ for fn, cap in [("fig1_loss_distribution_comparison.png", "图1 : Loss分布对比"),
745
+ ("fig2_privacy_utility_tradeoff_fixed.png", "图2 : 隐私-效用权衡"),
746
+ ("fig3_defense_comparison_bar.png", "图3 : 防御果柱状图")]:
 
747
  path = os.path.join(BASE_DIR, "figures", fn)
748
  if os.path.exists(path):
749
  gr.Markdown("### " + cap)
750
  gr.Image(value=path, show_label=False, height=420)
751
  gr.Markdown("---")
752
  else:
753
+ gr.Markdown("### " + cap + "\n\n> 文件未找到: " + fn)
754
 
755
  # ============================
756
  # Tab 8: 研究结论
757
  # ============================
758
+ with gr.Tab("研究结论"):
759
  gr.Markdown(
760
+ "## 研究结论\n\n"
761
  "---\n\n"
762
+ "### 一教育大模型面临显著的员推理攻击风险\n\n"
763
+ "实验结果表明Qwen2.5-Math-1.5B经LoRA微调的教育辅导模型,"
764
+ "在面对基于Loss的成员推理攻击时,AUC达到 **" + f"{bl_auc:.4f}" + "**,"
765
+ "显著高于随机猜测基准 (0.5)。这意味着攻击者仅通过观察模型对某一样本输出置信度,"
766
+ "即可以高于随机的概推断该样本是否被纳入训练集。"
767
+ "在教育场景中,训练数据通常包含学生的姓名、学号、学业成绩等敏感信息,"
768
+ "上述攻击能力构成了切实的隐私威胁。\n\n"
 
 
 
 
 
 
769
  "---\n\n"
770
+ "### 二、标签平滑作为训练期防御策略的有效性与局限性\n\n"
771
+ "标签平滑通过软化训练标签分布,抑制模型对训练样的过度拟合,"
772
+ "从而缩��成员与非成员之间的Loss分布差异实验中:\n\n"
773
+ "- **e=0.02** (温和平滑): AUC从 " + f"{bl_auc:.4f}" + " 降至 " + f"{s002_auc:.4f}"
774
+ + ",准确为 " + f"{s002_acc:.1f}" + "%在隐私保护与效用保持之间取得了较好的平衡。\n"
775
+ "- **e=0.2** (强力平滑): AUC进一步降至 " + f"{s02_auc:.4f}"
776
+ + ",防御效果更为显著,准确率为 " + f"{s02_acc:.1f}" + "%。\n\n"
777
+ "该��果揭示了标签平滑系数的选取需在隐私保护强度与模型效用之间进行权衡。"
778
+ "过小的平滑系数防御效果有限,而过大的系数可能影响模型在下游任务上的表现。\n\n"
779
  "---\n\n"
780
+ "### 三、输出扰动作为推理期防御策略的独特优势\n\n"
781
+ "输出扰动在推理阶段对模型输出的Loss值注入高斯噪声,"
782
+ "其核心优势在于**完全不改变模型参数**,因此对模型效用无任何影响。实验中:\n\n"
783
+ "- **s=0.02**: AUC从 " + f"{bl_auc:.4f}" + " 降至 " + f"{op002_auc:.4f}"
784
+ + ",而准确率保持 " + f"{bl_acc:.1f}" + "% 不变。\n\n"
785
+ "这表明输出扰动是一种**零效用成本**的防御手段,"
786
+ "特别适合已部署的模型系统进行后期隐私加固,具有良好的工程实用性。\n\n"
787
  "---\n\n"
788
+ "### 四、隐私-效用权衡的定量分析\n\n"
789
+ "综合所有实验结果,本研究揭示了教育大模型隐私保护中的核心矛盾:\n\n"
790
+ "| 策略 | AUC (隐私风险) | 准确率 (效用) | 特点 |\n"
791
+ "|------|----------------|--------------|------|\n"
792
+ "| 基线 (无防御) | " + f"{bl_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% | 风险最高 |\n"
793
+ "| 标签平滑 e=0.02 | " + f"{s002_auc:.4f}" + " | " + f"{s002_acc:.1f}" + "% | 训练期防御,效用保持良好 |\n"
794
+ "| 标签平滑 e=0.2 | " + f"{s02_auc:.4f}" + " | " + f"{s02_acc:.1f}" + "% | 强力防御 |\n"
795
+ "| 输出扰动 s=0.02 | " + f"{op002_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% | 零效用损失 |\n\n"
796
+ "上述分析表明,将**训练期标签平滑** (e=0.02) 与**推理期输出扰动** (s=0.02) 组合使用,"
797
+ "可以在两个独立维度上削弱攻击者的推断能力,实现更为全面的隐私保护,"
798
+ "同时将效用损失控制在可接受范围内。\n"
799
  )
800
 
801
  # ============================
802
  # 底部
803
  # ============================
804
  gr.Markdown(
805
+ "---\n\n"
806
  "<center>\n\n"
807
+ "教育大模型中的成员推理攻击及其防御思路研究\n\n"
808
+ "Qwen2.5-Math-1.5B | LoRA | MIA | Label Smoothing | Output Perturbation\n\n"
809
  "</center>\n"
810
  )
811