xiaohy commited on
Commit
20a2dbe
·
verified ·
1 Parent(s): a0290da

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +293 -516
app.py CHANGED
@@ -1,15 +1,15 @@
1
  import os
2
  import json
3
- import io
4
  import re
5
  import numpy as np
6
  import matplotlib
7
  matplotlib.use('Agg')
8
  import matplotlib.pyplot as plt
 
9
  import gradio as gr
10
 
11
  # ========================================
12
- # 1. 数据加载
13
  # ========================================
14
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
15
 
@@ -20,12 +20,12 @@ def load_json(path):
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")
@@ -39,86 +39,75 @@ config = load_json("config.json")
39
  plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
40
  plt.rcParams['axes.unicode_minus'] = False
41
 
42
- # 预取数值
43
  bl_auc = mia_results.get('baseline', {}).get('auc', 0)
44
  s002_auc = mia_results.get('smooth_0.02', {}).get('auc', 0)
45
  s02_auc = mia_results.get('smooth_0.2', {}).get('auc', 0)
46
  op001_auc = perturb_results.get('perturbation_0.01', {}).get('auc', 0)
47
  op0015_auc = perturb_results.get('perturbation_0.015', {}).get('auc', 0)
48
  op002_auc = perturb_results.get('perturbation_0.02', {}).get('auc', 0)
49
-
50
  bl_acc = utility_results.get('baseline', {}).get('accuracy', 0) * 100
51
  s002_acc = utility_results.get('smooth_0.02', {}).get('accuracy', 0) * 100
52
  s02_acc = utility_results.get('smooth_0.2', {}).get('accuracy', 0) * 100
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
 
65
  # ========================================
66
- # 2. 图表函数
67
  # ========================================
68
 
69
  def make_pie_chart():
70
- task_counts = {}
71
  for item in member_data + non_member_data:
72
  t = item.get('task_type', 'unknown')
73
- task_counts[t] = task_counts.get(t, 0) + 1
74
- name_map = {
75
- 'calculation': 'Calculation',
76
- 'word_problem': 'Word Problem',
77
- 'concept': 'Concept Q&A',
78
- 'error_correction': 'Error Correction'
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)
@@ -128,75 +117,59 @@ def make_loss_distribution():
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)
@@ -209,136 +182,96 @@ def make_accuracy_bar():
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
  # ========================================
304
- # 3. 回调函数
305
  # ========================================
306
 
307
  def show_random_sample(data_type):
308
- if data_type == "成员数据(训练集)":
309
- data = member_data
310
- else:
311
- data = non_member_data
312
  sample = data[np.random.randint(0, len(data))]
313
  meta = sample['metadata']
314
- task_map = {
315
- 'calculation': '基础计算',
316
- 'word_problem': '应用题',
317
- 'concept': '概念问答',
318
- 'error_correction': '错题订正'
319
- }
320
- info = (
321
- "### 样本元信息(隐私字段)\n\n"
322
- "| 字段 | |\n"
323
- "|------|-----|\n"
324
- "| **姓名** | " + str(meta['name']) + " |\n"
325
- "| **学号** | " + str(meta['student_id']) + " |\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
338
- else:
339
- is_member = False
340
- data = non_member_data
341
-
342
  idx = min(int(sample_index), len(data) - 1)
343
  sample = data[idx]
344
 
@@ -348,350 +281,239 @@ def run_mia_demo(sample_index, data_type):
348
  elif not is_member and idx < len(bl.get('non_member_losses', [])):
349
  loss = bl['non_member_losses'][idx]
350
  else:
351
- if is_member:
352
- loss = float(np.random.normal(bl_m_mean, 0.02))
353
- else:
354
- loss = float(np.random.normal(bl_nm_mean, 0.02))
355
 
356
  threshold = (bl_m_mean + bl_nm_mean) / 2.0
357
  pred_member = (loss < threshold)
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="教育大模型隐私攻防实验",
496
- theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky", neutral_hue="slate"),
497
- css=custom_css
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,
582
- inputs=[data_sel],
583
- outputs=[sample_info, sample_q, sample_a]
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():
600
  with gr.Column(scale=1):
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"
@@ -699,117 +521,72 @@ with gr.Blocks(
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
 
812
- # ========================================
813
- # 5. 启动
814
- # ========================================
815
  demo.launch()
 
1
  import os
2
  import json
 
3
  import re
4
  import numpy as np
5
  import matplotlib
6
  matplotlib.use('Agg')
7
  import matplotlib.pyplot as plt
8
+ from matplotlib.patches import FancyBboxPatch
9
  import gradio as gr
10
 
11
  # ========================================
12
+ # 1. Load Data
13
  # ========================================
14
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
15
 
 
20
 
21
 
22
  def clean_text(text):
23
+ if not isinstance(text, str):
24
+ return str(text)
25
  text = re.sub(r'[\U00010000-\U0010ffff]', '', text)
26
+ text = re.sub(r'[\ufff0-\uffff]', '', text)
27
+ text = re.sub(r'[\u200b-\u200f\u2028-\u202f\u2060-\u206f\ufeff]', '', text)
28
+ return text.strip()
29
 
30
 
31
  member_data = load_json("data/member.json")
 
39
  plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
40
  plt.rcParams['axes.unicode_minus'] = False
41
 
42
+ # Pre-fetch values
43
  bl_auc = mia_results.get('baseline', {}).get('auc', 0)
44
  s002_auc = mia_results.get('smooth_0.02', {}).get('auc', 0)
45
  s02_auc = mia_results.get('smooth_0.2', {}).get('auc', 0)
46
  op001_auc = perturb_results.get('perturbation_0.01', {}).get('auc', 0)
47
  op0015_auc = perturb_results.get('perturbation_0.015', {}).get('auc', 0)
48
  op002_auc = perturb_results.get('perturbation_0.02', {}).get('auc', 0)
 
49
  bl_acc = utility_results.get('baseline', {}).get('accuracy', 0) * 100
50
  s002_acc = utility_results.get('smooth_0.02', {}).get('accuracy', 0) * 100
51
  s02_acc = utility_results.get('smooth_0.2', {}).get('accuracy', 0) * 100
 
52
  bl_m_mean = mia_results.get('baseline', {}).get('member_loss_mean', 0.19)
53
  bl_nm_mean = mia_results.get('baseline', {}).get('non_member_loss_mean', 0.23)
54
  bl_m_std = mia_results.get('baseline', {}).get('member_loss_std', 0.03)
55
  bl_nm_std = mia_results.get('baseline', {}).get('non_member_loss_std', 0.03)
 
56
  model_name_str = config.get('model_name', 'Qwen/Qwen2.5-Math-1.5B-Instruct')
 
57
  data_size_str = str(config.get('data_size', 2000))
 
58
 
59
 
60
  # ========================================
61
+ # 2. Chart Functions
62
  # ========================================
63
 
64
  def make_pie_chart():
65
+ tc = {}
66
  for item in member_data + non_member_data:
67
  t = item.get('task_type', 'unknown')
68
+ tc[t] = tc.get(t, 0) + 1
69
+ nm = {'calculation': 'Calculation', 'word_problem': 'Word Problem',
70
+ 'concept': 'Concept Q&A', 'error_correction': 'Error Correction'}
71
+ labels = [nm.get(k, k) for k in tc]
72
+ sizes = list(tc.values())
 
 
 
 
73
  colors = ['#5B8FF9', '#5AD8A6', '#F6BD16', '#E86452']
74
+ fig, ax = plt.subplots(figsize=(6, 5))
75
  wedges, texts, autotexts = ax.pie(
76
+ sizes, labels=labels, autopct='%1.1f%%', colors=colors[:len(labels)],
77
+ startangle=90, textprops={'fontsize': 10},
78
+ wedgeprops={'edgecolor': 'white', 'linewidth': 2})
 
 
79
  for t in autotexts:
80
+ t.set_fontsize(10)
81
  t.set_fontweight('bold')
82
+ ax.set_title('Task Type Distribution', fontsize=13, fontweight='bold', pad=10)
83
  plt.tight_layout()
84
  return fig
85
 
86
 
87
  def make_loss_distribution():
88
+ items = []
89
  for k, t in [('baseline', 'Baseline'), ('smooth_0.02', 'LS (e=0.02)'), ('smooth_0.2', 'LS (e=0.2)')]:
90
  if k in full_results:
91
  auc = mia_results.get(k, {}).get('auc', 0)
92
+ items.append((k, t + " | AUC=" + f"{auc:.4f}"))
93
+ n = len(items)
94
  if n == 0:
95
  fig, ax = plt.subplots()
96
  ax.text(0.5, 0.5, 'No data', ha='center')
97
  return fig
98
+ fig, axes = plt.subplots(1, n, figsize=(5 * n, 4))
99
  if n == 1:
100
  axes = [axes]
101
+ for ax, (k, title) in zip(axes, items):
102
  m = full_results[k]['member_losses']
103
  nm = full_results[k]['non_member_losses']
104
  bins = np.linspace(min(min(m), min(nm)), max(max(m), max(nm)), 35)
105
  ax.hist(m, bins=bins, alpha=0.6, color='#5B8FF9', label='Member', density=True)
106
  ax.hist(nm, bins=bins, alpha=0.6, color='#E86452', label='Non-Member', density=True)
107
+ ax.set_title(title, fontsize=11, fontweight='bold')
108
+ ax.set_xlabel('Loss', fontsize=9)
109
+ ax.set_ylabel('Density', fontsize=9)
110
+ ax.legend(fontsize=8)
111
  ax.grid(True, linestyle='--', alpha=0.3)
112
  ax.spines['top'].set_visible(False)
113
  ax.spines['right'].set_visible(False)
 
117
 
118
  def make_auc_bar():
119
  methods, aucs, colors = [], [], []
120
+ for k, name, c in [('baseline', 'Baseline', '#8C8C8C'), ('smooth_0.02', 'LS (e=0.02)', '#5B8FF9'),
121
+ ('smooth_0.2', 'LS (e=0.2)', '#3D76DD')]:
 
 
 
 
122
  if k in mia_results:
123
+ methods.append(name); aucs.append(mia_results[k]['auc']); colors.append(c)
124
+ for k, name, c in [('perturbation_0.01', 'OP (s=0.01)', '#5AD8A6'),
125
+ ('perturbation_0.015', 'OP (s=0.015)', '#2EAD78'),
126
+ ('perturbation_0.02', 'OP (s=0.02)', '#1A7F5A')]:
 
 
 
 
 
127
  if k in perturb_results:
128
+ methods.append(name); aucs.append(perturb_results[k]['auc']); colors.append(c)
129
+ fig, ax = plt.subplots(figsize=(9, 5))
130
+ bars = ax.bar(methods, aucs, color=colors, width=0.5, edgecolor='white', linewidth=1.5)
 
 
131
  for bar, a in zip(bars, aucs):
132
+ ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.003,
133
+ f'{a:.4f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
134
+ ax.axhline(y=0.5, color='#E86452', linestyle='--', linewidth=1.5, alpha=0.6, label='Random Guess (0.5)')
135
+ ax.set_ylabel('MIA AUC', fontsize=11)
136
+ ax.set_title('Defense Mechanisms - AUC', fontsize=13, fontweight='bold')
137
+ ax.set_ylim(0.48, max(aucs) + 0.04 if aucs else 0.7)
138
+ ax.legend(fontsize=9)
139
  ax.grid(axis='y', linestyle='--', alpha=0.3)
140
  ax.spines['top'].set_visible(False)
141
  ax.spines['right'].set_visible(False)
142
+ plt.xticks(rotation=8, fontsize=9)
143
  plt.tight_layout()
144
  return fig
145
 
146
 
147
  def make_tradeoff():
148
+ fig, ax = plt.subplots(figsize=(8, 6))
149
+ pts = []
150
+ for k, name, mk, c, sz in [('baseline', 'Baseline', 'o', '#8C8C8C', 180),
151
+ ('smooth_0.02', 'LS (e=0.02)', 's', '#5B8FF9', 160),
152
+ ('smooth_0.2', 'LS (e=0.2)', 's', '#3D76DD', 160)]:
 
153
  if k in mia_results and k in utility_results:
154
+ pts.append({'n': name, 'a': mia_results[k]['auc'], 'c': utility_results[k]['accuracy'],
155
+ 'm': mk, 'co': c, 's': sz})
156
+ ba = utility_results.get('baseline', {}).get('accuracy', 0.633)
157
+ for k, name, mk, c, sz in [('perturbation_0.01', 'OP (s=0.01)', '^', '#5AD8A6', 170),
158
+ ('perturbation_0.02', 'OP (s=0.02)', '^', '#1A7F5A', 170)]:
 
 
159
  if k in perturb_results:
160
+ pts.append({'n': name, 'a': perturb_results[k]['auc'], 'c': ba, 'm': mk, 'co': c, 's': sz})
161
+ for p in pts:
162
+ ax.scatter(p['c'], p['a'], label=p['n'], marker=p['m'], color=p['co'],
163
+ s=p['s'], edgecolors='white', linewidth=2, zorder=5)
 
164
  ax.axhline(y=0.5, color='#BFBFBF', linestyle='--', alpha=0.8, label='Random Guess')
165
+ ax.set_xlabel('Accuracy', fontsize=11, fontweight='bold')
166
+ ax.set_ylabel('MIA AUC', fontsize=11, fontweight='bold')
167
+ ax.set_title('Privacy-Utility Trade-off', fontsize=13, fontweight='bold')
168
+ aa = [p['c'] for p in pts]; ab = [p['a'] for p in pts]
169
+ if aa and ab:
170
+ ax.set_xlim(min(aa)-0.03, max(aa)+0.05)
171
+ ax.set_ylim(min(min(ab), 0.5)-0.02, max(ab)+0.025)
172
+ ax.legend(loc='upper right', fontsize=9, fancybox=True)
 
173
  ax.grid(True, alpha=0.2)
174
  ax.spines['top'].set_visible(False)
175
  ax.spines['right'].set_visible(False)
 
182
  for k, name, c in [('baseline', 'Baseline', '#8C8C8C'), ('smooth_0.02', 'LS (e=0.02)', '#5B8FF9'),
183
  ('smooth_0.2', 'LS (e=0.2)', '#3D76DD')]:
184
  if k in utility_results:
185
+ names.append(name); accs.append(utility_results[k]['accuracy']*100); colors.append(c)
186
+ bp = utility_results.get('baseline', {}).get('accuracy', 0)*100
 
 
187
  for k, name, c in [('perturbation_0.01', 'OP (s=0.01)', '#5AD8A6'),
188
  ('perturbation_0.02', 'OP (s=0.02)', '#1A7F5A')]:
189
  if k in perturb_results:
190
+ names.append(name); accs.append(bp); colors.append(c)
191
+ fig, ax = plt.subplots(figsize=(9, 5))
 
 
192
  bars = ax.bar(names, accs, color=colors, width=0.5, edgecolor='white', linewidth=1.5)
193
  for bar, acc in zip(bars, accs):
194
+ ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.6,
195
+ f'{acc:.1f}%', ha='center', va='bottom', fontsize=10, fontweight='bold')
196
+ ax.set_ylabel('Accuracy (%)', fontsize=11)
197
+ ax.set_title('Model Utility (300 Math Questions)', fontsize=13, fontweight='bold')
198
  ax.set_ylim(0, 100)
199
  ax.grid(axis='y', alpha=0.3)
200
  ax.spines['top'].set_visible(False)
201
  ax.spines['right'].set_visible(False)
202
+ plt.xticks(rotation=8, fontsize=9)
203
  plt.tight_layout()
204
  return fig
205
 
206
 
207
  def make_loss_gauge(loss_val, m_mean, nm_mean, threshold):
208
+ fig, ax = plt.subplots(figsize=(8, 2.8))
209
+ x_min = min(m_mean - 3*bl_m_std, loss_val - 0.01)
210
+ x_max = max(nm_mean + 3*bl_nm_std, loss_val + 0.01)
211
+
212
+ ax.axvspan(x_min, threshold, alpha=0.12, color='#5B8FF9')
213
+ ax.axvspan(threshold, x_max, alpha=0.12, color='#E86452')
214
+ ax.axvline(x=threshold, color='#434343', linewidth=2, linestyle='-', zorder=3)
215
+ ax.text(threshold, 1.12, 'Threshold', ha='center', va='bottom', fontsize=9,
216
+ fontweight='bold', color='#434343', transform=ax.get_xaxis_transform())
217
+
218
+ ax.axvline(x=m_mean, color='#5B8FF9', linewidth=1.2, linestyle='--', alpha=0.6)
219
+ ax.text(m_mean, -0.28, f'Member Mean\n({m_mean:.4f})', ha='center', va='top',
220
+ fontsize=7.5, color='#5B8FF9', transform=ax.get_xaxis_transform())
221
+ ax.axvline(x=nm_mean, color='#E86452', linewidth=1.2, linestyle='--', alpha=0.6)
222
+ ax.text(nm_mean, -0.28, f'Non-Member Mean\n({nm_mean:.4f})', ha='center', va='top',
223
+ fontsize=7.5, color='#E86452', transform=ax.get_xaxis_transform())
224
+
225
+ in_member = loss_val < threshold
226
+ mc = '#5B8FF9' if in_member else '#E86452'
227
+ ax.plot(loss_val, 0.5, marker='v', markersize=16, color=mc, zorder=5,
 
 
 
 
 
 
 
 
 
 
 
228
  transform=ax.get_xaxis_transform())
229
+ ax.text(loss_val, 0.78, f'Loss={loss_val:.4f}', ha='center', va='bottom', fontsize=10,
230
+ fontweight='bold', color=mc, transform=ax.get_xaxis_transform(),
231
+ bbox=dict(boxstyle='round,pad=0.3', facecolor='white', edgecolor=mc, alpha=0.95))
232
+
233
+ mc_x = (x_min + threshold) / 2
234
+ nmc_x = (threshold + x_max) / 2
235
+ ax.text(mc_x, 0.5, 'Member Zone', ha='center', va='center', fontsize=10,
236
+ color='#5B8FF9', fontweight='bold', alpha=0.5, transform=ax.get_xaxis_transform())
237
+ ax.text(nmc_x, 0.5, 'Non-Member Zone', ha='center', va='center', fontsize=10,
238
+ color='#E86452', fontweight='bold', alpha=0.5, transform=ax.get_xaxis_transform())
 
 
239
 
240
  ax.set_xlim(x_min, x_max)
241
  ax.set_yticks([])
242
  ax.spines['top'].set_visible(False)
243
  ax.spines['right'].set_visible(False)
244
  ax.spines['left'].set_visible(False)
245
+ ax.set_xlabel('Loss Value', fontsize=9)
246
  plt.tight_layout()
247
  return fig
248
 
249
 
 
 
 
 
 
 
 
 
 
250
  # ========================================
251
+ # 3. Callbacks
252
  # ========================================
253
 
254
  def show_random_sample(data_type):
255
+ data = member_data if data_type == "成员数据(训练集)" else non_member_data
 
 
 
256
  sample = data[np.random.randint(0, len(data))]
257
  meta = sample['metadata']
258
+ task_map = {'calculation': '基础计算', 'word_problem': '应用题',
259
+ 'concept': '概念问答', 'error_correction': '错题订正'}
260
+
261
+ info_md = (
262
+ "**截获的隐私元数据**\n\n"
263
+ "- **姓名**: " + clean_text(str(meta.get('name', ''))) + "\n"
264
+ "- **学号**: " + clean_text(str(meta.get('student_id', ''))) + "\n"
265
+ "- **班级**: " + clean_text(str(meta.get('class', ''))) + "\n"
266
+ "- **成绩**: " + clean_text(str(meta.get('score', ''))) + " 分\n"
267
+ "- **类型**: " + task_map.get(sample.get('task_type', ''), sample.get('task_type', '')) + "\n"
 
 
 
 
 
 
268
  )
269
+ return info_md, clean_text(sample.get('question', '')), clean_text(sample.get('answer', ''))
270
 
271
 
272
  def run_mia_demo(sample_index, data_type):
273
+ is_member = (data_type == "成员数据(训练集)")
274
+ data = member_data if is_member else non_member_data
 
 
 
 
 
275
  idx = min(int(sample_index), len(data) - 1)
276
  sample = data[idx]
277
 
 
281
  elif not is_member and idx < len(bl.get('non_member_losses', [])):
282
  loss = bl['non_member_losses'][idx]
283
  else:
284
+ loss = float(np.random.normal(bl_m_mean if is_member else bl_nm_mean, 0.02))
 
 
 
285
 
286
  threshold = (bl_m_mean + bl_nm_mean) / 2.0
287
  pred_member = (loss < threshold)
288
+ attack_correct = (pred_member == is_member)
 
289
 
 
290
  gauge_fig = make_loss_gauge(loss, bl_m_mean, bl_nm_mean, threshold)
291
 
292
+ # Build result card
293
  if pred_member:
294
+ pred_label = "训练成员"
295
+ pred_color = "🔴"
296
  else:
297
+ pred_label = "非训练成员"
298
+ pred_color = "🟢"
299
 
300
+ if is_member:
301
+ actual_label = "训练成员"
302
+ actual_color = "🔴"
303
  else:
304
+ actual_label = "非训练成员"
305
+ actual_color = "🟢"
306
 
307
+ if attack_correct and pred_member and is_member:
308
+ verdict = "⚠️ **攻击成功: 发生了隐私泄露**"
309
+ verdict_detail = "模型对该样本过于熟悉(Loss低于阈值),攻击者成功判定其为训练集数据。"
310
  elif attack_correct:
311
+ verdict = "**判断正确**"
312
+ verdict_detail = "攻击者的判定与真实身份一致。"
 
 
 
 
 
 
 
 
 
 
313
  else:
314
+ verdict = "❌ **攻击失误**"
315
+ verdict_detail = "攻击者的判定与真实身份不符。"
 
 
 
316
 
317
  result_md = (
318
+ verdict + "\n\n"
319
+ + verdict_detail + "\n\n"
320
+ "| | 攻击者计算得出 | 系统真实身份 |\n"
321
+ "|---|---|---|\n"
322
+ "| 判定 | " + pred_color + " " + pred_label + " | " + actual_color + " " + actual_label + " |\n"
323
+ "| Loss | " + f"{loss:.4f}" + " | Threshold: " + f"{threshold:.4f}" + " |\n"
 
 
 
 
 
 
 
 
324
  )
325
 
326
+ q_text = "**样本追���号 [" + str(idx) + "] :**\n\n" + clean_text(sample.get('question', ''))[:500]
327
+ return q_text, gauge_fig, result_md
328
 
329
 
330
  # ========================================
331
+ # 4. Build Interface
332
  # ========================================
333
 
334
+ CSS = """
 
335
  .gradio-container {
336
  max-width: 1200px !important;
337
  margin: auto !important;
338
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif !important;
339
  }
 
 
340
  .tab-nav button {
341
  font-size: 14px !important;
342
+ padding: 10px 20px !important;
343
  font-weight: 500 !important;
344
  border-radius: 8px 8px 0 0 !important;
345
+ transition: all 0.2s !important;
346
  }
347
  .tab-nav button.selected {
348
  font-weight: 700 !important;
349
+ color: #5B8FF9 !important;
350
  border-bottom: 3px solid #5B8FF9 !important;
351
  }
 
 
352
  .prose h1 {
353
+ font-size: 1.6rem !important;
354
  color: #1a1a2e !important;
355
+ letter-spacing: -0.02em !important;
 
356
  }
357
  .prose h2 {
358
+ font-size: 1.25rem !important;
359
  color: #16213e !important;
360
+ margin-top: 1.5em !important;
361
+ padding-bottom: 0.3em !important;
362
+ border-bottom: 1px solid #e8e8e8 !important;
363
  }
364
  .prose h3 {
365
+ font-size: 1.05rem !important;
366
+ color: #333 !important;
 
 
 
 
 
 
 
367
  }
368
+ .prose table { font-size: 0.88rem !important; }
369
  .prose th {
370
  background: #f0f5ff !important;
 
371
  font-weight: 600 !important;
372
+ padding: 8px 12px !important;
 
 
 
 
373
  }
374
+ .prose td { padding: 7px 12px !important; }
 
375
  .prose blockquote {
376
  border-left: 4px solid #5B8FF9 !important;
377
  background: #f7f9fc !important;
378
+ padding: 10px 14px !important;
 
379
  border-radius: 0 6px 6px 0 !important;
380
+ font-size: 0.92rem !important;
381
  }
 
 
382
  footer { display: none !important; }
383
  """
384
 
385
+ with gr.Blocks(title="教育大模型隐私攻防", theme=gr.themes.Soft(
386
+ primary_hue="blue", secondary_hue="sky", neutral_hue="slate"), css=CSS) as demo:
387
 
 
 
 
 
 
 
 
 
 
388
  gr.Markdown(
389
  "# 教育大模型中的成员推理攻击及其防御研究\n\n"
390
  "> 探究教育场景下大语言模型的隐私泄露风险,"
391
+ "验证标签平滑与输出扰动两种防御策略的有效性。\n")
 
392
 
393
+ # --- Tab 1 ---
 
 
394
  with gr.Tab("项目概览"):
395
  gr.Markdown(
396
  "## 研究背景\n\n"
397
+ "大语言模型在教育领域的应用日益广泛模型训练过程中不可避免地接触到学生敏感数据。"
398
+ "**成员推理攻击 (MIA)** 能够判断某条数据是否参与了模型训练,构成隐私威胁\n\n"
 
 
399
  "---\n\n"
400
+ "## 实验设计\n\n"
401
+ "| 阶段 | 内容 | 方法 |\n"
402
  "|------|------|------|\n"
403
+ "| 数据准备 | 2000条数学辅导对话 | 模板化生,含隐私字段 |\n"
404
+ "| 模型训练 | Qwen2.5-Math + LoRA | 基线 + 标签平滑 (e=0.02, 0.2) |\n"
405
+ "| MIA攻击 | Loss-based攻击 | 计算全样本Loss,AUC评估 |\n"
406
+ "| 输出扰动 | 推理期防御 | 对Loss加高斯噪声 (s=0.01~0.02) |\n"
407
+ "| 效用评估 | 300道数学测试题 | 准确率评估 |\n"
408
+ "| 综合分析 | 隐私-用权衡 | 散点图 + 定量对比 |\n\n"
409
  "---\n\n"
410
  "## 实验配置\n\n"
411
+ "| 项 | 值 |\n"
412
+ "|---|---|\n"
413
  "| 基座模型 | " + model_name_str + " |\n"
414
+ "| 微调 | LoRA (r=8, alpha=16) |\n"
415
  "| 训练轮数 | 10 epochs |\n"
416
+ "| 数据量 | " + data_size_str + " 条 |\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ # --- Tab 2 ---
419
+ with gr.Tab("数据展示"):
420
+ gr.Markdown("## 数据集概况\n\n"
421
+ "成员1000条(训练集)+ 非成员1000条(对照组),每条含学生隐私字段。\n")
422
  with gr.Row():
423
  with gr.Column(scale=1):
424
+ gr.Plot(value=make_pie_chart(), label="图表")
 
425
  with gr.Column(scale=1):
426
+ gr.Markdown("**选择靶向数据池**")
427
+ data_sel = gr.Radio(["成员数据(训练集)", "非成员数据(测试集)"],
428
+ value="成员数据(训练集)", label="")
429
+ sample_btn = gr.Button("随机提取", variant="primary")
430
+ sample_info = gr.Markdown()
431
+
432
+ gr.Markdown("---\n\n**原始对话内容**")
 
 
433
  with gr.Row():
434
+ sample_q = gr.Textbox(label="学生提问 (Prompt)", lines=5, interactive=False)
435
+ sample_a = gr.Textbox(label="模型回答 (Ground Truth)", lines=5, interactive=False)
436
+
437
+ sample_btn.click(show_random_sample, [data_sel], [sample_info, sample_q, sample_a])
438
+
439
+ # --- Tab 3 ---
 
 
 
 
 
 
440
  with gr.Tab("MIA攻击演示"):
441
  gr.Markdown(
442
+ "## 发起成员推理攻击\n\n"
443
+ "调整下方滑块选择一条数据,系统将计算该条数据 Loss 值并实施判定。\n")
 
 
 
 
 
444
 
445
  with gr.Row():
446
  with gr.Column(scale=1):
447
+ atk_type = gr.Radio(["成员数据(训练集)", "非成员数据(测试集)"],
448
+ value="成员数据(训练集)", label="模拟真实数据来源")
449
+ atk_idx = gr.Slider(0, 999, step=1, value=0, label="样本游标 ID (0-999)")
450
+ atk_btn = gr.Button("执行成员推理攻击", variant="primary", size="lg")
 
 
 
 
 
 
 
451
  atk_question = gr.Markdown()
452
 
453
+ with gr.Column(scale=1):
454
+ gr.Markdown("**攻击侦测控制台**")
455
+ atk_gauge = gr.Plot(label="Loss 分布雷达")
456
+ atk_result = gr.Markdown()
457
 
458
+ atk_btn.click(run_mia_demo, [atk_idx, atk_type], [atk_question, atk_gauge, atk_result])
 
 
 
 
459
 
460
+ # --- Tab 4 ---
 
 
461
  with gr.Tab("防御对比"):
462
  gr.Markdown(
463
  "## 防御策略效果对比\n\n"
464
  "| 策略 | 类型 | 原理 | 优势 | 局限 |\n"
465
  "|------|------|------|------|------|\n"
466
+ "| 标签平滑 | 训练期 | 软化训练标签 | 降低过拟合 | 可能影响效用 |\n"
467
+ "| 输出扰动 | 推理期 | Loss加噪声 | 零效用损失 | 仅遮蔽信号 |\n")
 
468
 
469
  with gr.Row():
470
  with gr.Column():
471
+ gr.Plot(value=make_auc_bar(), label="AUC对比")
 
472
  with gr.Column():
473
+ gr.Plot(value=make_loss_distribution(), label="Loss分布")
474
+
475
+ tbl = (
476
+ "### 结果汇总\n\n"
477
+ "| 策略 | 类型 | AUC | 准确率 |\n"
478
+ "|------|------|-----|--------|\n")
479
+ for k, name, cat in [('baseline', '基线', '--'), ('smooth_0.02', '标签平滑 (e=0.02)', '训练期'),
 
 
 
480
  ('smooth_0.2', '标签平滑 (e=0.2)', '训练期')]:
481
  if k in mia_results:
482
  a = mia_results[k]['auc']
483
+ acc = utility_results.get(k, {}).get('accuracy', 0) * 100
484
+ tbl += "| " + name + " | " + cat + " | " + f"{a:.4f}" + " | " + f"{acc:.1f}" + "% |\n"
485
+ for k, name in [('perturbation_0.01', '输出扰动 (s=0.01)'), ('perturbation_0.015', '输出扰动 (s=0.015)'),
486
  ('perturbation_0.02', '输出扰动 (s=0.02)')]:
487
  if k in perturb_results:
488
  a = perturb_results[k]['auc']
489
+ tbl += "| " + name + " | 推理期 | " + f"{a:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n"
490
+ gr.Markdown(tbl)
491
 
492
+ # --- Tab 5 ---
 
 
493
  with gr.Tab("防御详解"):
494
  gr.Markdown(
495
+ "## 标签平滑 (Label Smoothing)\n\n"
496
+ "**类型**: 训练期防御\n\n"
497
+ "将训练标签从硬标签 (one-hot) 转换为软标签,降低模型对训练样本的过度拟合。\n\n"
498
+ "y_smooth = (1 - e) * y_onehot + e / V\n\n"
499
+ "| 参数 | AUC | 准确率 |\n"
500
+ "|------|-----|--------|\n"
501
+ "| 基线 (e=0) | " + f"{bl_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% |\n"
502
+ "| e=0.02 | " + f"{s002_auc:.4f}" + " | " + f"{s002_acc:.1f}" + "% |\n"
503
+ "| e=0.2 | " + f"{s02_auc:.4f}" + " | " + f"{s02_acc:.1f}" + "% |\n\n"
 
 
 
 
504
  "---\n\n"
505
+ "## 输出扰动 (Output Perturbation)\n\n"
506
+ "**类型**: 推理期防御\n\n"
507
+ "在推理阶段对Loss值注入高斯噪声,不修改模型参数,准确率完全不变。\n\n"
508
+ "Loss_perturbed = Loss_original + N(0, s^2)\n\n"
 
 
509
  "| 参数 | AUC | AUC降幅 | 准确率 |\n"
510
  "|------|-----|---------|--------|\n"
511
+ "| 基线 | " + f"{bl_auc:.4f}" + " | -- | " + f"{bl_acc:.1f}" + "% |\n"
512
+ "| s=0.01 | " + f"{op001_auc:.4f}" + " | " + f"{bl_auc-op001_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n"
513
+ "| s=0.015 | " + f"{op0015_auc:.4f}" + " | " + f"{bl_auc-op0015_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n"
514
+ "| s=0.02 | " + f"{op002_auc:.4f}" + " | " + f"{bl_auc-op002_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% (不变) |\n\n"
515
  "---\n\n"
516
+ "## 综合对比\n\n"
517
  "| 维度 | 标签平滑 | 输出扰动 |\n"
518
  "|------|---------|----------|\n"
519
  "| 作用阶段 | 训练期 | 推理期 |\n"
 
521
  "| 对效用的影响 | 可能有影响 | 无影响 |\n"
522
  "| 防御机制 | 降低过拟合 | 遮蔽统计信号 |\n"
523
  "| 可叠加使用 | 是 | 是 |\n\n"
524
+ "> 推荐方案: 标签平滑 (e=0.02) + 输出扰动 (s=0.02) 双重防护\n")
 
525
 
526
+ # --- Tab 6 ---
 
 
527
  with gr.Tab("效用评估"):
528
+ gr.Markdown("## 效用评估\n\n> 测试集: 300道数学题\n")
 
 
 
 
529
  with gr.Row():
530
  with gr.Column():
531
+ gr.Plot(value=make_accuracy_bar(), label="准确率")
 
532
  with gr.Column():
533
+ gr.Plot(value=make_tradeoff(), label="隐私-效用权衡")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ # --- Tab 7 ---
 
 
536
  with gr.Tab("论文图表"):
537
  gr.Markdown("## 学术图表 (300 DPI)")
538
+ for fn, cap in [("fig1_loss_distribution_comparison.png", "图1: Loss分布对比"),
539
+ ("fig2_privacy_utility_tradeoff_fixed.png", "图2: 隐私-效用权衡"),
540
+ ("fig3_defense_comparison_bar.png", "图3: 防御效果柱状图")]:
541
+ p = os.path.join(BASE_DIR, "figures", fn)
542
+ if os.path.exists(p):
543
  gr.Markdown("### " + cap)
544
+ gr.Image(value=p, show_label=False, height=400)
545
  gr.Markdown("---")
 
 
546
 
547
+ # --- Tab 8 ---
 
 
548
  with gr.Tab("研究结论"):
549
  gr.Markdown(
550
  "## 研究结论\n\n"
551
  "---\n\n"
552
  "### 一、教育大模型面临显著的成员推理攻击风险\n\n"
553
+ "实验结果表明,经LoRA微调的教育辅导模型在面对基于Loss的成员推理攻击时,"
554
+ "AUC达到 " + f"{bl_auc:.4f}" + ",显著高于随机猜测基准(0.5)。"
555
+ "这意味着攻击者仅通过观察模型对某一样本的输出置信度,"
556
  "即可以高于随机的概率推断该样本是否被纳入训练集。"
557
+ "在教育场景中,训练数据通常包含学生姓名、学号、学业成绩等敏感信息,"
558
+ "攻击能力构成了切实的隐私威胁。\n\n"
559
  "---\n\n"
560
+ "### 二、标签平滑的有效性与局限性\n\n"
561
  "标签平滑通过软化训练标签分布,抑制模型对训练样本的过度拟合,"
562
+ "缩成员与非成员之间的Loss分布差异。\n\n"
563
+ "- e=0.02: AUC从" + f"{bl_auc:.4f}" + "降至" + f"{s002_auc:.4f}"
564
+ + ",准确率" + f"{s002_acc:.1f}" + "%,隐私保护与效用保持间取得较好平衡。\n"
565
+ "- e=0.2: AUC进一步降至" + f"{s02_auc:.4f}"
566
+ + ",防御效果更为显著,准确率" + f"{s02_acc:.1f}" + "%。\n\n"
567
+ "该结果表明平滑系数的选取需在隐私保护强度与模型效用之间进行权衡。\n\n"
 
568
  "---\n\n"
569
+ "### 三、输出扰动的独特优势\n\n"
570
+ "输出扰动在推理阶段对Loss值注入高斯噪声,"
571
+ "核心优势在于完全不改变模型参数,对模型效用无任何影响。\n\n"
572
+ "- s=0.02: AUC从" + f"{bl_auc:.4f}" + "降至" + f"{op002_auc:.4f}"
573
+ + ",准确率保持" + f"{bl_acc:.1f}" + "%不变。\n\n"
574
+ "这是一种零效用成本的防御手段,适合已部署系统进行后期隐私加固。\n\n"
 
575
  "---\n\n"
576
  "### 四、隐私-效用权衡的定量分析\n\n"
577
+ "| 策略 | AUC | 准确率 | 特点 |\n"
578
+ "|------|-----|--------|------|\n"
579
+ "| 基线 | " + f"{bl_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% | 风险最高 |\n"
580
+ "| 标签平滑 e=0.02 | " + f"{s002_auc:.4f}" + " | " + f"{s002_acc:.1f}" + "% | 效用保持良好 |\n"
 
581
  "| 标签平滑 e=0.2 | " + f"{s02_auc:.4f}" + " | " + f"{s02_acc:.1f}" + "% | 强力防御 |\n"
582
  "| 输出扰动 s=0.02 | " + f"{op002_auc:.4f}" + " | " + f"{bl_acc:.1f}" + "% | 零效用损失 |\n\n"
583
+ "将训练期标签平滑(e=0.02)与推理期输出扰动(s=0.02)组合使用,"
584
+ "可在两个独立维度上削弱攻击者的推断能力,实现更为全面的隐私保护,"
585
+ "同时将效用损失控制在可接受范围内。\n")
586
+
 
 
 
 
587
  gr.Markdown(
588
+ "---\n\n<center>\n\n"
 
589
  "教育大模型中的成员推理攻击及其防御思路研究\n\n"
590
+ "</center>\n")
 
 
591
 
 
 
 
592
  demo.launch()