CalligVAE

6 downloads 0 likes on ModelScope updated

Model card

CalligVAE — 书法专用 VAE(DINOv2 感知监督 · 保留飞白)

面向中国书法真迹的潜空间自编码器。在 sd-vae-ft-ema 基础上全量微调, 彻底摒弃 GAN 判别器,改用纯确定性的 DINOv2 感知 + 笔法方差 监督, 解决传统 SD-VAE 在书法场景下抹杀飞白、枯笔纤维、锐利露锋的高斯平滑通病。

  • Base model:stabilityai/sd-vae-ft-ema(diffusers AutoencoderKL)
  • 训练数据:ModelScope CalligDiT / MCCD 全库 393,486 张标准化 256×256 书法真迹
  • 架构:f8、4 通道潜空间(latents (N, 4, 32, 32))、scaling_factor = 0.18215
  • 部署:机器 48(NVIDIA RTX 4090 48GB),无梯度检查点、微批次 16 × 梯度累积 4 = 等效 64

⚠️ 2026-10-09 版本更正(v2,重要)

早期发布的权重把 decoder 的输入尺度写反了 —— 训练时用的是 decode(z / 0.18215), 于是一个"只认 sample/0.18215"的非标准解码器被训了出来;下游若按标准约定 decode(z) 解码,会得到近似常量灰图(SSIM≈0.51、MSE≈0.79)。

本版本已解析修正为标准约定,修正方式:

post_quant_conv.weight  ←  post_quant_conv.weight / 0.18215      # bias 不变

由 AutoencoderKL.decode 的实现 dec = decoder(post_quant_conv(z)) 可知, 上式使 decode_new(z) ≡ decode_old(z / 0.18215) 逐元素恒等 —— encoder / 潜空间 / 后验一个参数都没动,只是把 decoder 的输入尺度翻正。

请使用下方"四、使用方法(v2,标准约定)"。 旧的 decode(z/0.18215) 写法在本版会解码失败。


一、为什么不用 GAN?

问题 传统 PatchGAN CalliVAE(本模型)
对抗稳定性 Min-Max 震荡、棋盘伪影 ✅ 纯回归,100% 稳定梯度
汉字拓扑感知 仅局部 70×70 窗口 ✅ DINOv2 全局 patch 语义
高频飞白 常被平滑掉 ✅ 二阶矩直接锁死高频方差

二、损失配方

L_total = 1.0·L1 + 1e-4·KL + 0.1·L_DINO_Struct + 0.5·L_DINO_Style
  • L_DINO_Struct:DINOv2(x_recon) 与 DINOv2(x_real) 的 256 个 patch-token 特征 MSE(替代 VGG-LPIPS),压制毛刺重影、对齐空间拓扑。
  • L_DINO_Style:对 patch 维求 std 后做 MSE,强制重建图在 patch 间的特征方差拟合真迹 —— 模糊图方差趋 0、飞白图方差极大,以此确定性锁住高频笔墨纹理。

三、收敛记录(30k 计划,25k 验收收敛)

Step L1 ↓ DINO-Struct ↓ DINO-Style ↓ Total ↓
10,000 0.0505 0.5349 0.0210 0.2025
15,000 0.0458 0.5183 0.0208 0.1817
20,000 0.0417 0.5582 0.0219 0.1837
25,000 0.0409 0.4926 0.0203 0.1754

15k 步后 Total 稳定在 0.18 ± 0.02 平台,无趋势 → 判定收敛,取 step 25,000 权重发布。

四、使用方法(v2,标准约定)

# 后端 A:ModelScope(推荐) / 后端 B:diffusers(需已配置 ModelScope 源)
from modelscope import AutoencoderKL          # 或 from diffusers.models import AutoencoderKL
import torch, numpy as np
from PIL import Image

vae = AutoencoderKL.from_pretrained("ArcherFD/CalligVAE")

x = torch.from_numpy(
    np.asarray(Image.open("char.png").convert("RGB").resize((256, 256)), np.float32) / 127.5 - 1.0
).permute(2, 0, 1).unsqueeze(0)

with torch.no_grad():
    # 编码:本 VAE 后验极宽 (std≈0.88),**必须用 sample()**;mode() 无法还原细节
    z   = vae.encode(x).latent_dist.sample()
    lat = z * 0.18215                          # 扩散训练目标(DiT 侧)
    # 推理时 DiT 输出 pred ≈ lat,按标准约定解码:
    x_rec = vae.decode(lat / 0.18215).sample   # 等价于 vae.decode(z)

⚠ 编码约定(v2): 编码 z = latent_dist.sample();扩散目标 z * 0.18215;解码 vae.decode(pred / 0.18215)(即 vae.decode(z))。 切勿把 mode() * 0.18215 当解码输入 —— 该 VAE 的 decoder 本质是个"随机去噪器", 只喂均值会得到模糊/灰图(实测 decode(mode) L1≈0.61 vs decode(sample) L1≈0.03)。

五、文件

文件 说明
config.json diffusers AutoencoderKL 配置(f8 / 4ch / scale 0.18215)
diffusion_pytorch_model.safetensors 权重(~319 MiB,83.65M 参数),v2 已修正解码约定
train_dino_calli_vae.py 训练脚本(纯 DINOv2 监督,可复现)
assets/recon_step_25000.png 重建样张:上排真迹 GT、下排重建(含飞白枯笔)

六、许可与致谢

  • 由 stabilityai/sd-vae-ft-ema 微调而来,请遵循其原始许可。
  • 数据来自 ModelScope CalligDiT(MCCD)书法数据集。