Alicesong commited on
Commit
4e8d6c1
·
verified ·
1 Parent(s): 8b1f752

Switch to pink princess visual theme

Browse files
Files changed (3) hide show
  1. README.md +7 -7
  2. app.py +312 -56
  3. deploy_hf_space.py +1 -1
README.md CHANGED
@@ -1,7 +1,7 @@
1
  ---
2
- title: 暗黑自监督学习实验室
3
- emoji: 🌑
4
- colorFrom: blue
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.12.0
@@ -9,7 +9,7 @@ app_file: app.py
9
  python_version: "3.10"
10
  fullWidth: true
11
  header: default
12
- short_description: 中文交互式 MAE、SimCLR 与旋转预测自监督学习 Demo
13
  tags:
14
  - gradio
15
  - self-supervised-learning
@@ -19,9 +19,9 @@ tags:
19
  - chinese
20
  ---
21
 
22
- # 暗黑自监督学习实验室
23
 
24
- 这是一个中文暗黑风 Hugging Face Space,用轻量可交互方式展示图像自监督学习:
25
 
26
  - 旋转预测:自动生成 0/90/180/270 度标签,训练一个小型分类器预测图像旋转角度。
27
  - MAE 遮挡重建:比较两种 patch mask 比例,展示遮挡输入、重建输出和 masked MSE loss。
@@ -39,5 +39,5 @@ python app.py
39
 
40
  ```powershell
41
  $env:HF_TOKEN="your_token"
42
- python deploy_hf_space.py --repo-id your-username/ssl-dark-vision-lab
43
  ```
 
1
  ---
2
+ title: 粉色公主自监督学习实验室
3
+ emoji: 👑
4
+ colorFrom: pink
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.12.0
 
9
  python_version: "3.10"
10
  fullWidth: true
11
  header: default
12
+ short_description: 粉色公主系中文交互式 MAE、SimCLR 与旋转预测自监督学习 Demo
13
  tags:
14
  - gradio
15
  - self-supervised-learning
 
19
  - chinese
20
  ---
21
 
22
+ # 粉色公主自监督学习实验室
23
 
24
+ 这是一个中文粉色公主系 Hugging Face Space,用轻量可交互方式展示图像自监督学习:
25
 
26
  - 旋转预测:自动生成 0/90/180/270 度标签,训练一个小型分类器预测图像旋转角度。
27
  - MAE 遮挡重建:比较两种 patch mask 比例,展示遮挡输入、重建输出和 masked MSE loss。
 
39
 
40
  ```powershell
41
  $env:HF_TOKEN="your_token"
42
+ python deploy_hf_space.py --repo-id your-username/ssl-princess-vision-lab
43
  ```
app.py CHANGED
@@ -1,4 +1,4 @@
1
- """Dark Chinese self-supervised learning lab for Hugging Face Spaces."""
2
 
3
  from __future__ import annotations
4
 
@@ -252,6 +252,249 @@ APP_THEME = gr.themes.Base(
252
  button_primary_text_color="#06101a",
253
  )
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  def make_demo_image(size: int = DEFAULT_SIZE, seed: int = 7) -> Image.Image:
257
  """Create a default image with enough structure for pretext tasks."""
@@ -261,9 +504,9 @@ def make_demo_image(size: int = DEFAULT_SIZE, seed: int = 7) -> Image.Image:
261
  cy = size * 0.50
262
  r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
263
  base = np.zeros((size, size, 3), dtype=np.float32)
264
- base[..., 0] = 18 + 58 * (x / size) + 24 * np.sin(y / 22)
265
- base[..., 1] = 28 + 84 * (y / size) + 32 * np.cos((x + y) / 37)
266
- base[..., 2] = 48 + 118 * np.exp(-(r / (size * 0.62)) ** 2)
267
  noise = rng.normal(0, 4, base.shape)
268
  arr = np.clip(base + noise, 0, 255).astype(np.uint8)
269
  img = Image.fromarray(arr, "RGB")
@@ -272,26 +515,39 @@ def make_demo_image(size: int = DEFAULT_SIZE, seed: int = 7) -> Image.Image:
272
  draw.rounded_rectangle(
273
  [size * 0.08, size * 0.10, size * 0.58, size * 0.50],
274
  radius=int(size * 0.07),
275
- outline=(100, 227, 255, 210),
276
  width=max(2, size // 80),
277
- fill=(100, 227, 255, 28),
278
  )
279
  draw.ellipse(
280
  [size * 0.46, size * 0.18, size * 0.90, size * 0.62],
281
- outline=(255, 93, 135, 230),
282
  width=max(2, size // 70),
283
- fill=(255, 93, 135, 30),
284
  )
285
  draw.polygon(
286
  [(size * 0.18, size * 0.84), (size * 0.54, size * 0.45), (size * 0.86, size * 0.86)],
287
- outline=(255, 179, 92, 235),
288
- fill=(255, 179, 92, 38),
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  )
290
  for i in range(7):
291
  offset = int(size * (0.12 + i * 0.10))
292
  draw.line(
293
  [(offset, size * 0.08), (size * 0.92, offset)],
294
- fill=(140, 255, 194, 54),
295
  width=max(1, size // 130),
296
  )
297
  return img
@@ -477,30 +733,30 @@ def fig_to_image(fig: plt.Figure) -> np.ndarray:
477
 
478
 
479
  def style_axis(ax: plt.Axes) -> None:
480
- ax.set_facecolor("#0b1324")
481
- ax.tick_params(colors="#b8c7db", labelsize=8)
482
  for spine in ax.spines.values():
483
- spine.set_color("#28415f")
484
- ax.grid(True, color="#20324c", linewidth=0.8, alpha=0.72)
485
 
486
 
487
  def plot_rotation_curves(losses: Iterable[float], accuracies: Iterable[float]) -> np.ndarray:
488
  losses = list(losses)
489
  accuracies = list(accuracies)
490
  xs = np.arange(1, len(losses) + 1)
491
- fig, axes = plt.subplots(1, 2, figsize=(8.7, 3.45), facecolor="#070b16")
492
- axes[0].plot(xs, losses, color="#ffb35c", linewidth=2.7)
493
- axes[0].set_title("Rotation pretext loss", color="#eef7ff", fontsize=10, weight="bold")
494
- axes[0].set_xlabel("epoch", color="#9db0c5")
495
- axes[0].set_ylabel("cross entropy", color="#9db0c5")
496
  style_axis(axes[0])
497
 
498
- axes[1].plot(xs, np.array(accuracies) * 100, color="#64e3ff", linewidth=2.7)
499
- axes[1].fill_between(xs, np.array(accuracies) * 100, color="#64e3ff", alpha=0.13)
500
  axes[1].set_ylim(0, 105)
501
- axes[1].set_title("Rotation prediction accuracy", color="#eef7ff", fontsize=10, weight="bold")
502
- axes[1].set_xlabel("epoch", color="#9db0c5")
503
- axes[1].set_ylabel("accuracy %", color="#9db0c5")
504
  style_axis(axes[1])
505
  fig.tight_layout(pad=1.25)
506
  return fig_to_image(fig)
@@ -628,18 +884,18 @@ def loss_curve(start: float, end: float, epochs: int, seed: int) -> np.ndarray:
628
 
629
  def plot_mae_curves(curve_a: np.ndarray, curve_b: np.ndarray, label_a: str, label_b: str) -> np.ndarray:
630
  xs = np.arange(1, len(curve_a) + 1)
631
- fig, ax = plt.subplots(figsize=(8.7, 3.45), facecolor="#070b16")
632
- ax.plot(xs, curve_a, color="#64e3ff", linewidth=2.8, label=label_a)
633
- ax.plot(xs, curve_b, color="#ff5d87", linewidth=2.8, label=label_b)
634
- ax.fill_between(xs, curve_a, color="#64e3ff", alpha=0.11)
635
- ax.fill_between(xs, curve_b, color="#ff5d87", alpha=0.10)
636
- ax.set_title("MAE masked reconstruction loss", color="#eef7ff", fontsize=11, weight="bold")
637
- ax.set_xlabel("epoch", color="#9db0c5")
638
- ax.set_ylabel("masked MSE", color="#9db0c5")
639
  style_axis(ax)
640
- leg = ax.legend(facecolor="#0b1324", edgecolor="#28415f", fontsize=8)
641
  for text in leg.get_texts():
642
- text.set_color("#eef7ff")
643
  fig.tight_layout(pad=1.25)
644
  return fig_to_image(fig)
645
 
@@ -810,27 +1066,27 @@ def plot_simclr(
810
  label_b: str,
811
  ) -> np.ndarray:
812
  xs = np.arange(1, len(curves_a[0]) + 1)
813
- fig, axes = plt.subplots(1, 2, figsize=(8.7, 3.45), facecolor="#070b16")
814
- axes[0].plot(xs, curves_a[2], color="#64e3ff", linewidth=2.6, label=label_a)
815
- axes[0].plot(xs, curves_b[2], color="#ff5d87", linewidth=2.6, label=label_b)
816
- axes[0].set_title("InfoNCE loss", color="#eef7ff", fontsize=10, weight="bold")
817
- axes[0].set_xlabel("epoch", color="#9db0c5")
818
- axes[0].set_ylabel("loss", color="#9db0c5")
819
  style_axis(axes[0])
820
 
821
- axes[1].plot(xs, curves_a[0], color="#64e3ff", linewidth=2.4, label=f"{label_a} pos")
822
- axes[1].plot(xs, curves_a[1], color="#64e3ff", linestyle="--", linewidth=2.1, label=f"{label_a} neg")
823
- axes[1].plot(xs, curves_b[0], color="#ff5d87", linewidth=2.4, label=f"{label_b} pos")
824
- axes[1].plot(xs, curves_b[1], color="#ff5d87", linestyle="--", linewidth=2.1, label=f"{label_b} neg")
825
  axes[1].set_ylim(-0.2, 1.05)
826
- axes[1].set_title("Cosine similarity", color="#eef7ff", fontsize=10, weight="bold")
827
- axes[1].set_xlabel("epoch", color="#9db0c5")
828
  style_axis(axes[1])
829
 
830
  for ax in axes:
831
- leg = ax.legend(facecolor="#0b1324", edgecolor="#28415f", fontsize=7)
832
  for text in leg.get_texts():
833
- text.set_color("#eef7ff")
834
  fig.tight_layout(pad=1.25)
835
  return fig_to_image(fig)
836
 
@@ -890,20 +1146,20 @@ def run_simclr_ui(
890
 
891
  def build_interface() -> gr.Blocks:
892
  default_image = pil_to_np(make_demo_image())
893
- with gr.Blocks(title="暗黑自监督学习实验室") as demo:
894
  gr.HTML(
895
  """
896
  <div class="app-shell">
897
  <div class="hero-grid">
898
  <div>
899
- <div class="eyebrow">Self-Supervised Vision Lab</div>
900
- <div class="hero-title">暗黑自监督学习实验室</div>
901
  <div class="hero-copy">
902
- 上传一张图片,现场观察旋转预测、MAE 遮挡重建和 SimCLR 对比学习。
903
  页面会展示原图、变换或遮挡后的输入、模型输出结果,并用 loss / accuracy 曲线解释“自监督信号”如何起作用。
904
  </div>
905
  <div class="hero-badges">
906
- <div class="badge-chip">中文交互页面</div>
907
  <div class="badge-chip">旋转预测预任务</div>
908
  <div class="badge-chip">MAE patch mask 重建</div>
909
  <div class="badge-chip">SimCLR 正负样本对比</div>
@@ -930,7 +1186,7 @@ def build_interface() -> gr.Blocks:
930
  )
931
  seed = gr.Slider(0, 999, value=42, step=1, label="随机种子", info="换一个种子会改变 mask、裁剪和增强视图。")
932
  gr.Markdown(
933
- "这不是重型训练平台,而是一个教学 Vibe Coding Demo:用轻量模型和可视化,把自监督学习的机制讲清楚。",
934
  elem_classes=["caption"],
935
  )
936
  with gr.Column(scale=2):
@@ -1059,7 +1315,7 @@ def build_interface() -> gr.Blocks:
1059
 
1060
 
1061
  def main() -> None:
1062
- parser = argparse.ArgumentParser(description="Launch the dark self-supervised learning lab.")
1063
  parser.add_argument("--share", action="store_true", help="Create a temporary Gradio sharing link.")
1064
  parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "7860")), help="Server port.")
1065
  parser.add_argument("--no-browser", action="store_true", help="Do not open a browser window.")
 
1
+ """Pink princess Chinese self-supervised learning lab for Hugging Face Spaces."""
2
 
3
  from __future__ import annotations
4
 
 
252
  button_primary_text_color="#06101a",
253
  )
254
 
255
+ CUSTOM_CSS = """
256
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&family=Ma+Shan+Zheng&display=swap');
257
+
258
+ :root {
259
+ --cream: #fff8fc;
260
+ --blush: #ffe3ef;
261
+ --blush-2: #ffd0e4;
262
+ --rose: #e84f8a;
263
+ --rose-deep: #9f2f62;
264
+ --berry: #6e244b;
265
+ --gold: #c88a30;
266
+ --gold-soft: #ffe7a8;
267
+ --lilac: #b77ff2;
268
+ --panel: rgba(255, 248, 252, 0.82);
269
+ --panel-strong: rgba(255, 255, 255, 0.94);
270
+ --line: rgba(232, 79, 138, 0.24);
271
+ --text: #54213d;
272
+ --muted: #8a5f75;
273
+ --shadow: 0 28px 90px rgba(177, 71, 116, 0.22);
274
+ }
275
+
276
+ body,
277
+ .gradio-container {
278
+ background:
279
+ radial-gradient(circle at 12% 8%, rgba(255, 196, 218, 0.92), transparent 25%),
280
+ radial-gradient(circle at 86% 6%, rgba(255, 231, 168, 0.76), transparent 23%),
281
+ radial-gradient(circle at 72% 88%, rgba(219, 188, 255, 0.54), transparent 30%),
282
+ linear-gradient(135deg, #fff6fb 0%, #ffe4f0 45%, #fff7df 100%) !important;
283
+ color: var(--text) !important;
284
+ font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif !important;
285
+ }
286
+
287
+ .gradio-container {
288
+ max-width: 1460px !important;
289
+ }
290
+
291
+ .app-shell {
292
+ position: relative;
293
+ overflow: hidden;
294
+ padding: clamp(24px, 4vw, 46px);
295
+ border: 1px solid rgba(232, 79, 138, 0.24);
296
+ border-radius: 38px;
297
+ background:
298
+ linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(255, 238, 247, 0.72)),
299
+ repeating-linear-gradient(90deg, rgba(232,79,138,0.035) 0 1px, transparent 1px 78px);
300
+ box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.82);
301
+ margin-bottom: 18px;
302
+ }
303
+
304
+ .app-shell::before {
305
+ content: "";
306
+ position: absolute;
307
+ inset: -34% -8% auto auto;
308
+ width: 470px;
309
+ height: 470px;
310
+ background:
311
+ radial-gradient(circle, rgba(255, 231, 168, 0.92), transparent 42%),
312
+ conic-gradient(from 120deg, rgba(232,79,138,0), rgba(232,79,138,0.22), rgba(183,127,242,0.18), rgba(232,79,138,0));
313
+ filter: blur(10px);
314
+ opacity: 0.86;
315
+ animation: floatRibbon 12s ease-in-out infinite alternate;
316
+ }
317
+
318
+ .app-shell::after {
319
+ content: "♕";
320
+ position: absolute;
321
+ right: 34px;
322
+ bottom: 18px;
323
+ color: rgba(200, 138, 48, 0.22);
324
+ font-size: clamp(5rem, 11vw, 10rem);
325
+ font-family: Georgia, serif;
326
+ transform: rotate(-9deg);
327
+ }
328
+
329
+ @keyframes floatRibbon {
330
+ from { transform: rotate(0deg) translate3d(0, 0, 0); }
331
+ to { transform: rotate(14deg) translate3d(-28px, 22px, 0); }
332
+ }
333
+
334
+ .hero-grid {
335
+ position: relative;
336
+ display: grid;
337
+ grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.72fr);
338
+ gap: 26px;
339
+ align-items: stretch;
340
+ z-index: 1;
341
+ }
342
+
343
+ .eyebrow {
344
+ display: inline-flex;
345
+ gap: 10px;
346
+ align-items: center;
347
+ padding: 9px 15px;
348
+ border: 1px solid rgba(200, 138, 48, 0.34);
349
+ border-radius: 999px;
350
+ color: var(--rose-deep);
351
+ background: linear-gradient(135deg, rgba(255,255,255,0.82), rgba(255,231,168,0.48));
352
+ font-size: 0.82rem;
353
+ letter-spacing: 0.14em;
354
+ text-transform: uppercase;
355
+ font-weight: 900;
356
+ box-shadow: 0 10px 28px rgba(232, 79, 138, 0.13);
357
+ }
358
+
359
+ .hero-title {
360
+ margin: 18px 0 16px;
361
+ max-width: 900px;
362
+ font-family: 'Ma Shan Zheng', 'Noto Sans SC', cursive;
363
+ font-size: clamp(3.2rem, 7vw, 6.6rem);
364
+ line-height: 0.92;
365
+ letter-spacing: 0.02em;
366
+ color: var(--rose-deep);
367
+ text-shadow: 0 3px 0 #fff, 0 18px 36px rgba(232, 79, 138, 0.20);
368
+ }
369
+
370
+ .hero-copy {
371
+ max-width: 790px;
372
+ color: var(--muted);
373
+ font-size: 1.05rem;
374
+ line-height: 1.86;
375
+ }
376
+
377
+ .hero-badges,
378
+ .mini-grid {
379
+ display: flex;
380
+ flex-wrap: wrap;
381
+ gap: 10px;
382
+ margin-top: 20px;
383
+ }
384
+
385
+ .badge-chip {
386
+ padding: 10px 15px;
387
+ border: 1px solid rgba(232, 79, 138, 0.22);
388
+ border-radius: 999px;
389
+ background: linear-gradient(135deg, rgba(255,255,255,0.84), rgba(255,224,239,0.78));
390
+ color: var(--berry);
391
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 10px 24px rgba(232, 79, 138, 0.12);
392
+ font-weight: 800;
393
+ }
394
+
395
+ .side-card {
396
+ min-height: 132px;
397
+ border-radius: 28px;
398
+ padding: 19px;
399
+ border: 1px solid rgba(232,79,138,0.22);
400
+ background:
401
+ radial-gradient(circle at 18% 0%, rgba(255,231,168,0.72), transparent 34%),
402
+ linear-gradient(180deg, rgba(255,255,255,0.94), rgba(255,226,240,0.82));
403
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.88), 0 18px 40px rgba(177, 71, 116, 0.13);
404
+ }
405
+
406
+ .side-card strong {
407
+ display: block;
408
+ margin-bottom: 8px;
409
+ color: var(--rose-deep);
410
+ font-size: 1.08rem;
411
+ }
412
+
413
+ .side-card span {
414
+ color: var(--muted);
415
+ line-height: 1.68;
416
+ }
417
+
418
+ .control-card,
419
+ .note-card,
420
+ .metric-card {
421
+ border: 1px solid var(--line) !important;
422
+ border-radius: 28px !important;
423
+ background: var(--panel) !important;
424
+ box-shadow: 0 18px 52px rgba(177, 71, 116, 0.14), inset 0 1px 0 rgba(255,255,255,0.78) !important;
425
+ backdrop-filter: blur(8px);
426
+ }
427
+
428
+ .control-card {
429
+ padding: 18px !important;
430
+ }
431
+
432
+ .note-card {
433
+ padding: 18px 20px;
434
+ color: var(--muted);
435
+ line-height: 1.78;
436
+ }
437
+
438
+ .note-card strong {
439
+ color: var(--rose-deep);
440
+ }
441
+
442
+ .metric-card {
443
+ padding: 14px 16px;
444
+ }
445
+
446
+ .tabs button,
447
+ button {
448
+ font-weight: 900 !important;
449
+ }
450
+
451
+ button.primary {
452
+ border: 0 !important;
453
+ background: linear-gradient(135deg, #ff9cc5, #ffd36e 52%, #d8a5ff) !important;
454
+ color: #5a213e !important;
455
+ box-shadow: 0 14px 34px rgba(232, 79, 138, 0.24) !important;
456
+ }
457
+
458
+ .gradio-container label,
459
+ .gradio-container .wrap,
460
+ .gradio-container .prose,
461
+ .gradio-container .markdown {
462
+ color: var(--text) !important;
463
+ }
464
+
465
+ .gradio-container input,
466
+ .gradio-container textarea,
467
+ .gradio-container select {
468
+ background: rgba(255, 255, 255, 0.74) !important;
469
+ color: var(--text) !important;
470
+ border-color: rgba(232, 79, 138, 0.24) !important;
471
+ }
472
+
473
+ .caption {
474
+ color: var(--muted);
475
+ line-height: 1.7;
476
+ margin-top: -6px;
477
+ }
478
+
479
+ @media (max-width: 960px) {
480
+ .hero-grid { grid-template-columns: 1fr; }
481
+ }
482
+ """
483
+
484
+ APP_THEME = gr.themes.Base(
485
+ primary_hue="pink",
486
+ secondary_hue="purple",
487
+ neutral_hue="stone",
488
+ radius_size="lg",
489
+ ).set(
490
+ body_background_fill="#fff6fb",
491
+ block_background_fill="rgba(255, 248, 252, 0.82)",
492
+ block_border_color="rgba(232, 79, 138, 0.24)",
493
+ input_background_fill="rgba(255, 255, 255, 0.74)",
494
+ button_primary_background_fill="#ff9cc5",
495
+ button_primary_text_color="#5a213e",
496
+ )
497
+
498
 
499
  def make_demo_image(size: int = DEFAULT_SIZE, seed: int = 7) -> Image.Image:
500
  """Create a default image with enough structure for pretext tasks."""
 
504
  cy = size * 0.50
505
  r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
506
  base = np.zeros((size, size, 3), dtype=np.float32)
507
+ base[..., 0] = 246 + 8 * np.sin(y / 20) + 5 * (x / size)
508
+ base[..., 1] = 206 + 26 * (y / size) + 8 * np.cos((x + y) / 34)
509
+ base[..., 2] = 226 + 18 * np.exp(-(r / (size * 0.60)) ** 2)
510
  noise = rng.normal(0, 4, base.shape)
511
  arr = np.clip(base + noise, 0, 255).astype(np.uint8)
512
  img = Image.fromarray(arr, "RGB")
 
515
  draw.rounded_rectangle(
516
  [size * 0.08, size * 0.10, size * 0.58, size * 0.50],
517
  radius=int(size * 0.07),
518
+ outline=(232, 79, 138, 210),
519
  width=max(2, size // 80),
520
+ fill=(255, 255, 255, 72),
521
  )
522
  draw.ellipse(
523
  [size * 0.46, size * 0.18, size * 0.90, size * 0.62],
524
+ outline=(183, 127, 242, 210),
525
  width=max(2, size // 70),
526
+ fill=(255, 226, 240, 76),
527
  )
528
  draw.polygon(
529
  [(size * 0.18, size * 0.84), (size * 0.54, size * 0.45), (size * 0.86, size * 0.86)],
530
+ outline=(200, 138, 48, 235),
531
+ fill=(255, 231, 168, 92),
532
+ )
533
+ draw.polygon(
534
+ [
535
+ (size * 0.34, size * 0.25),
536
+ (size * 0.42, size * 0.13),
537
+ (size * 0.50, size * 0.25),
538
+ (size * 0.60, size * 0.12),
539
+ (size * 0.66, size * 0.25),
540
+ (size * 0.67, size * 0.35),
541
+ (size * 0.33, size * 0.35),
542
+ ],
543
+ outline=(200, 138, 48, 230),
544
+ fill=(255, 231, 168, 150),
545
  )
546
  for i in range(7):
547
  offset = int(size * (0.12 + i * 0.10))
548
  draw.line(
549
  [(offset, size * 0.08), (size * 0.92, offset)],
550
+ fill=(232, 79, 138, 48),
551
  width=max(1, size // 130),
552
  )
553
  return img
 
733
 
734
 
735
  def style_axis(ax: plt.Axes) -> None:
736
+ ax.set_facecolor("#fff8fc")
737
+ ax.tick_params(colors="#8a5f75", labelsize=8)
738
  for spine in ax.spines.values():
739
+ spine.set_color("#f2bad2")
740
+ ax.grid(True, color="#f8d6e6", linewidth=0.8, alpha=0.9)
741
 
742
 
743
  def plot_rotation_curves(losses: Iterable[float], accuracies: Iterable[float]) -> np.ndarray:
744
  losses = list(losses)
745
  accuracies = list(accuracies)
746
  xs = np.arange(1, len(losses) + 1)
747
+ fig, axes = plt.subplots(1, 2, figsize=(8.7, 3.45), facecolor="#fff8fc")
748
+ axes[0].plot(xs, losses, color="#c88a30", linewidth=2.7)
749
+ axes[0].set_title("Rotation pretext loss", color="#6e244b", fontsize=10, weight="bold")
750
+ axes[0].set_xlabel("epoch", color="#8a5f75")
751
+ axes[0].set_ylabel("cross entropy", color="#8a5f75")
752
  style_axis(axes[0])
753
 
754
+ axes[1].plot(xs, np.array(accuracies) * 100, color="#e84f8a", linewidth=2.7)
755
+ axes[1].fill_between(xs, np.array(accuracies) * 100, color="#e84f8a", alpha=0.16)
756
  axes[1].set_ylim(0, 105)
757
+ axes[1].set_title("Rotation prediction accuracy", color="#6e244b", fontsize=10, weight="bold")
758
+ axes[1].set_xlabel("epoch", color="#8a5f75")
759
+ axes[1].set_ylabel("accuracy %", color="#8a5f75")
760
  style_axis(axes[1])
761
  fig.tight_layout(pad=1.25)
762
  return fig_to_image(fig)
 
884
 
885
  def plot_mae_curves(curve_a: np.ndarray, curve_b: np.ndarray, label_a: str, label_b: str) -> np.ndarray:
886
  xs = np.arange(1, len(curve_a) + 1)
887
+ fig, ax = plt.subplots(figsize=(8.7, 3.45), facecolor="#fff8fc")
888
+ ax.plot(xs, curve_a, color="#e84f8a", linewidth=2.8, label=label_a)
889
+ ax.plot(xs, curve_b, color="#b77ff2", linewidth=2.8, label=label_b)
890
+ ax.fill_between(xs, curve_a, color="#e84f8a", alpha=0.13)
891
+ ax.fill_between(xs, curve_b, color="#b77ff2", alpha=0.12)
892
+ ax.set_title("MAE masked reconstruction loss", color="#6e244b", fontsize=11, weight="bold")
893
+ ax.set_xlabel("epoch", color="#8a5f75")
894
+ ax.set_ylabel("masked MSE", color="#8a5f75")
895
  style_axis(ax)
896
+ leg = ax.legend(facecolor="#fff8fc", edgecolor="#f2bad2", fontsize=8)
897
  for text in leg.get_texts():
898
+ text.set_color("#6e244b")
899
  fig.tight_layout(pad=1.25)
900
  return fig_to_image(fig)
901
 
 
1066
  label_b: str,
1067
  ) -> np.ndarray:
1068
  xs = np.arange(1, len(curves_a[0]) + 1)
1069
+ fig, axes = plt.subplots(1, 2, figsize=(8.7, 3.45), facecolor="#fff8fc")
1070
+ axes[0].plot(xs, curves_a[2], color="#e84f8a", linewidth=2.6, label=label_a)
1071
+ axes[0].plot(xs, curves_b[2], color="#b77ff2", linewidth=2.6, label=label_b)
1072
+ axes[0].set_title("InfoNCE loss", color="#6e244b", fontsize=10, weight="bold")
1073
+ axes[0].set_xlabel("epoch", color="#8a5f75")
1074
+ axes[0].set_ylabel("loss", color="#8a5f75")
1075
  style_axis(axes[0])
1076
 
1077
+ axes[1].plot(xs, curves_a[0], color="#e84f8a", linewidth=2.4, label=f"{label_a} pos")
1078
+ axes[1].plot(xs, curves_a[1], color="#e84f8a", linestyle="--", linewidth=2.1, label=f"{label_a} neg")
1079
+ axes[1].plot(xs, curves_b[0], color="#b77ff2", linewidth=2.4, label=f"{label_b} pos")
1080
+ axes[1].plot(xs, curves_b[1], color="#b77ff2", linestyle="--", linewidth=2.1, label=f"{label_b} neg")
1081
  axes[1].set_ylim(-0.2, 1.05)
1082
+ axes[1].set_title("Cosine similarity", color="#6e244b", fontsize=10, weight="bold")
1083
+ axes[1].set_xlabel("epoch", color="#8a5f75")
1084
  style_axis(axes[1])
1085
 
1086
  for ax in axes:
1087
+ leg = ax.legend(facecolor="#fff8fc", edgecolor="#f2bad2", fontsize=7)
1088
  for text in leg.get_texts():
1089
+ text.set_color("#6e244b")
1090
  fig.tight_layout(pad=1.25)
1091
  return fig_to_image(fig)
1092
 
 
1146
 
1147
  def build_interface() -> gr.Blocks:
1148
  default_image = pil_to_np(make_demo_image())
1149
+ with gr.Blocks(title="粉色公主自监督学习实验室") as demo:
1150
  gr.HTML(
1151
  """
1152
  <div class="app-shell">
1153
  <div class="hero-grid">
1154
  <div>
1155
+ <div class="eyebrow">Princess Self-Supervised Vision Lab</div>
1156
+ <div class="hero-title">粉色公主自监督学习实验室</div>
1157
  <div class="hero-copy">
1158
+ 上传一张图片,在玫瑰粉、香槟金和软糖色的实验台里观察旋转预测、MAE 遮挡重建和 SimCLR 对比学习。
1159
  页面会展示原图、变换或遮挡后的输入、模型输出结果,并用 loss / accuracy 曲线解释“自监督信号”如何起作用。
1160
  </div>
1161
  <div class="hero-badges">
1162
+ <div class="badge-chip">中文公主风交互页面</div>
1163
  <div class="badge-chip">旋转预测预任务</div>
1164
  <div class="badge-chip">MAE patch mask 重建</div>
1165
  <div class="badge-chip">SimCLR 正负样本对比</div>
 
1186
  )
1187
  seed = gr.Slider(0, 999, value=42, step=1, label="随机种子", info="换一个种子会改变 mask、裁剪和增强视图。")
1188
  gr.Markdown(
1189
+ "这不是重型训练平台,而是一个粉色公主系教学 Demo:用轻量模型和可视化,把自监督学习的机制讲清楚。",
1190
  elem_classes=["caption"],
1191
  )
1192
  with gr.Column(scale=2):
 
1315
 
1316
 
1317
  def main() -> None:
1318
+ parser = argparse.ArgumentParser(description="Launch the pink princess self-supervised learning lab.")
1319
  parser.add_argument("--share", action="store_true", help="Create a temporary Gradio sharing link.")
1320
  parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "7860")), help="Server port.")
1321
  parser.add_argument("--no-browser", action="store_true", help="Do not open a browser window.")
deploy_hf_space.py CHANGED
@@ -49,7 +49,7 @@ def main() -> None:
49
  parser.add_argument("--private", action="store_true", help="Create the Space as private.")
50
  parser.add_argument(
51
  "--commit-message",
52
- default="Deploy dark self-supervised learning lab",
53
  help="Commit message for the uploaded Space files.",
54
  )
55
  args = parser.parse_args()
 
49
  parser.add_argument("--private", action="store_true", help="Create the Space as private.")
50
  parser.add_argument(
51
  "--commit-message",
52
+ default="Deploy pink princess self-supervised learning lab",
53
  help="Commit message for the uploaded Space files.",
54
  )
55
  args = parser.parse_args()