Switch to pink princess visual theme
Browse files- README.md +7 -7
- app.py +312 -56
- deploy_hf_space.py +1 -1
README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 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 |
-
这是一个中文
|
| 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-
|
| 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 |
-
"""
|
| 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] =
|
| 265 |
-
base[..., 1] =
|
| 266 |
-
base[..., 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=(
|
| 276 |
width=max(2, size // 80),
|
| 277 |
-
fill=(
|
| 278 |
)
|
| 279 |
draw.ellipse(
|
| 280 |
[size * 0.46, size * 0.18, size * 0.90, size * 0.62],
|
| 281 |
-
outline=(
|
| 282 |
width=max(2, size // 70),
|
| 283 |
-
fill=(255,
|
| 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=(
|
| 288 |
-
fill=(255,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=(
|
| 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("#
|
| 481 |
-
ax.tick_params(colors="#
|
| 482 |
for spine in ax.spines.values():
|
| 483 |
-
spine.set_color("#
|
| 484 |
-
ax.grid(True, color="#
|
| 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="#
|
| 492 |
-
axes[0].plot(xs, losses, color="#
|
| 493 |
-
axes[0].set_title("Rotation pretext loss", color="#
|
| 494 |
-
axes[0].set_xlabel("epoch", color="#
|
| 495 |
-
axes[0].set_ylabel("cross entropy", color="#
|
| 496 |
style_axis(axes[0])
|
| 497 |
|
| 498 |
-
axes[1].plot(xs, np.array(accuracies) * 100, color="#
|
| 499 |
-
axes[1].fill_between(xs, np.array(accuracies) * 100, color="#
|
| 500 |
axes[1].set_ylim(0, 105)
|
| 501 |
-
axes[1].set_title("Rotation prediction accuracy", color="#
|
| 502 |
-
axes[1].set_xlabel("epoch", color="#
|
| 503 |
-
axes[1].set_ylabel("accuracy %", color="#
|
| 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="#
|
| 632 |
-
ax.plot(xs, curve_a, color="#
|
| 633 |
-
ax.plot(xs, curve_b, color="#
|
| 634 |
-
ax.fill_between(xs, curve_a, color="#
|
| 635 |
-
ax.fill_between(xs, curve_b, color="#
|
| 636 |
-
ax.set_title("MAE masked reconstruction loss", color="#
|
| 637 |
-
ax.set_xlabel("epoch", color="#
|
| 638 |
-
ax.set_ylabel("masked MSE", color="#
|
| 639 |
style_axis(ax)
|
| 640 |
-
leg = ax.legend(facecolor="#
|
| 641 |
for text in leg.get_texts():
|
| 642 |
-
text.set_color("#
|
| 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="#
|
| 814 |
-
axes[0].plot(xs, curves_a[2], color="#
|
| 815 |
-
axes[0].plot(xs, curves_b[2], color="#
|
| 816 |
-
axes[0].set_title("InfoNCE loss", color="#
|
| 817 |
-
axes[0].set_xlabel("epoch", color="#
|
| 818 |
-
axes[0].set_ylabel("loss", color="#
|
| 819 |
style_axis(axes[0])
|
| 820 |
|
| 821 |
-
axes[1].plot(xs, curves_a[0], color="#
|
| 822 |
-
axes[1].plot(xs, curves_a[1], color="#
|
| 823 |
-
axes[1].plot(xs, curves_b[0], color="#
|
| 824 |
-
axes[1].plot(xs, curves_b[1], color="#
|
| 825 |
axes[1].set_ylim(-0.2, 1.05)
|
| 826 |
-
axes[1].set_title("Cosine similarity", color="#
|
| 827 |
-
axes[1].set_xlabel("epoch", color="#
|
| 828 |
style_axis(axes[1])
|
| 829 |
|
| 830 |
for ax in axes:
|
| 831 |
-
leg = ax.legend(facecolor="#
|
| 832 |
for text in leg.get_texts():
|
| 833 |
-
text.set_color("#
|
| 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="
|
| 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">
|
| 901 |
<div class="hero-copy">
|
| 902 |
-
上传一张图片,
|
| 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 |
-
"这不是重型训练平台,而是一个教学
|
| 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
|
| 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
|
| 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()
|