CalligVAE — 书法专用 VAE(DINOv2 感知监督 · 保留飞白)
面向中国书法真迹的潜空间自编码器。在
sd-vae-ft-ema基础上全量微调, 彻底摒弃 GAN 判别器,改用纯确定性的 DINOv2 感知 + 笔法方差 监督, 解决传统 SD-VAE 在书法场景下抹杀飞白、枯笔纤维、锐利露锋的高斯平滑通病。
- Base model:
stabilityai/sd-vae-ft-ema(diffusersAutoencoderKL) - 训练数据: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 vsdecode(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)书法数据集。