☰
从0搭建Stable Diffusion:Backbone选型指南——Unet与Transformer深度对比
2026/10/27 6:47:33 网站建设 项目流程

多模态大模型这两年火得一塌糊涂,但真到动手搭的时候,第一个卡住大多数人的问题往往不是数据、不是训练策略,而是Backbone到底选什么。我前后折腾过好几个图像生成和图文理解的项目,从早期拿Unet硬怼,到后来全面转向Transformer架构,中间踩的坑能写满一个笔记本。这篇就围绕“从0搭建一个SD”这件事,把Backbone选型的来龙去脉、Unet和Transformer各自的脾气、以及一个能跑起来的最小实现路径,掰开揉碎讲清楚。不管你是刚接触扩散模型的新手,还是已经调过几个模型但没深究过结构的老手,应该都能从里面抠出点有用的东西。

1. 先搞清楚Backbone在SD里到底扮演什么角色

很多人一上来就纠结“Unet还是Transformer”,但连Backbone在整条链路里干什么都没想明白,选型就成了拍脑袋。我先把这层窗户纸捅破。

1.1 扩散模型的三段式结构里,Backbone是那个“干重活”的

一个标准的Stable Diffusion类模型,拆开来看其实是三块:文本编码器负责把提示词变成语义向量,**Backbone(去噪网络)**负责在潜空间里一步步把噪声还原成图像特征,**解码器(VAE Decoder)**负责把潜空间特征还原成像素图。文本编码器通常用CLIP或者T5这类现成的,解码器也是固定的VAE,真正决定模型能力上限、参数量大头、训练成本大头的,就是中间那个Backbone。

打个比方,文本编码器像是“翻译官”,把你的话翻译成模型能懂的语言;VAE解码器像是“画师的手”,负责最后落笔;而Backbone是“画师的脑子”,它要在几十步里去判断“当前这团噪声里哪些该去掉、哪些该保留、往哪个方向走”。这个判断的质量,直接决定了出图的构图、细节、语义一致性。所以Backbone选型不是审美问题,是能力问题。

1.2 为什么会有Unet和Transformer两条路线之争

早期扩散模型(DDPM那一批)几乎清一色用Unet,因为Unet天生适合做“图像到图像”的稠密预测,跳跃连接能把浅层细节直接送到深层,去噪这种任务简直是量身定做。SD 1.5、SD 2.1都是Unet backbone,稳定、成熟、生态好。

但后来DiT(Diffusion Transformer)出来了,Sora那一波又把Transformer路线推到了台前。Transformer的优势在于可扩展性——参数量往上堆的时候,性能提升曲线比Unet更平滑,而且天然适合处理多模态输入(文本token和图像token可以拼在一起做注意力)。这就是为什么新一代模型越来越多往Transformer靠。

提示:选型不是“新的就一定好”。Unet在中小规模、单模态图像生成上依然能打,Transformer在超大规模、多模态联合建模上更有优势。你的项目规模决定了你该站哪边。

1.3 从0搭SD,先明确你要的是“能跑”还是“能扩”

我见过太多人一上来就想复现一个Sora级别的架构,结果卡在显存和训练成本上动弹不得。从0搭建,我的建议是先分清目标:如果你是想理解原理、跑通一个能出图的最小闭环,Unet路线成熟度高、调试成本低,两三天能出第一张图;如果你是想做多模态联合训练、为后续扩展留空间,那从一开始就上Transformer架构,哪怕先做小规模,也比后期重构省事。

下面这张表是我自己总结的两条路线对比,选型时可以直接对照:

维度Unet BackboneTransformer Backbone(DiT类)
参数量规模通常0.8B~3B可轻松扩展到数十B
训练稳定性高,收敛行为可预测需要更精细的学习率调度
显存占用相对友好序列长度一长就吃显存
多模态扩展需要额外设计融合模块天然支持token拼接
生态成熟度极高,参考实现多中等,仍在快速演进
适合场景单模态图像生成、快速验证多模态联合、大规模扩展

2. Unet路线:成熟稳重,但别指望它无限扩展

既然要从0搭,我先把Unet这条路走一遍,因为它是理解扩散模型去噪机制最好的入口。你把Unet啃透了,再去看DiT,会发现很多设计思想是一脉相承的。

2.1 Unet的核心结构拆解:编码、瓶颈、解码三段

Unet之所以叫U,是因为它的结构像个字母U:左边一路下采样(编码器),中间一个瓶颈层,右边一路上采样(解码器),左右之间用跳跃连接(skip connection)连起来。在SD里,这个Unet还额外接入了时间步嵌入(timestep embedding)和文本条件(cross-attention),所以它其实是个“条件Unet”。

编码器负责逐层提取特征、降低分辨率,把空间信息压缩成语义信息;瓶颈层在最低分辨率上做全局推理;解码器再逐层上采样恢复分辨率。跳跃连接的作用是把编码器浅层的高频细节直接送到解码器对应层,避免上采样过程中细节丢失。这个设计对去噪任务特别关键——因为去噪既要理解全局语义(这团噪声整体该往哪个方向走),又要保留局部细节(边缘、纹理不能糊)。

我实际搭的时候,编码器和解码器各用4个stage,每个stage包含若干个ResBlock加注意力层,通道数从128起步逐级翻倍到512,瓶颈层再翻一次。这个配置在单卡24G显存上能跑batch size 4左右,属于能验证效果的入门配置。

2.2 时间步和文本条件是怎么注入的

这是Unet做条件生成的关键,也是新手最容易写错的地方。时间步注入用的是正弦位置编码加MLP:把标量时间步t映射成一个高维向量,然后通过全连接层投影到每个ResBlock的通道维度,以加法方式注入。文本条件注入用的是cross-attention:在Unet的中间层和上采样层里插入cross-attention模块,query来自图像特征,key和value来自文本编码器的输出。

这里有个实操细节:cross-attention不要加得太早。我一开始在每个stage都塞了cross-attention,结果训练时文本条件“压过”了图像自身的去噪信号,出图语义对了但构图崩了。后来改成只在瓶颈层和上采样阶段加,效果立刻正常。这个经验在官方文档里不会写,但实测很关键。

# 时间步嵌入的简化实现 import torch import torch.nn as nn import math class TimestepEmbedding(nn.Module): def __init__(self, dim, freq_dim=256): super().__init__() self.freq_dim = freq_dim self.mlp = nn.Sequential( nn.Linear(freq_dim, dim * 4), nn.SiLU(), nn.Linear(dim * 4, dim) ) def forward(self, t): # t: [batch] half = self.freq_dim // 2 freqs = torch.exp( -math.log(10000) * torch.arange(half, device=t.device) / half ) args = t[:, None].float() * freqs[None] emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) return self.mlp(emb)

2.3 从0搭Unet的实操步骤与参数选择

我把搭建流程拆成五步,按顺序走基本不会乱:

  1. 确定潜空间分辨率。SD用的是VAE把512x512图像压到64x64x4的潜空间,所以Unet的输入是64x64、4通道。这个分辨率决定了你的下采样次数——64到32到16到8,4个stage刚好,再多就压没了。
  2. 定通道数基数。入门用128,显存紧张用64,追求效果用192。通道数基数乘以2的stage次方就是各层通道。
  3. 堆ResBlock和注意力。每个stage至少2个ResBlock,注意力层在低分辨率stage加即可,高分辨率stage加注意力显存吃不消。
  4. 接条件注入。时间步嵌入全局注入,文本条件用cross-attention选择性注入。
  5. 写输出层。最后一层用卷积把通道数映射回4(潜空间通道数),输出预测的噪声。

参数选择上有个经验公式:总参数量控制在0.8B到1.2B之间,这个区间在单卡或小集群上训练成本可控,效果也够看。超过这个量级,Unet的扩展收益开始递减,不如换Transformer。

注意:Unet的下采样用卷积stride=2还是用池化,我建议用卷积,因为可学习。池化会丢信息且不可逆,在去噪任务里是减分项。

2.4 Unet路线的坑:我踩过的三个典型问题

第一个坑是跳跃连接的特征对齐。编码器和解码器对应层的通道数、分辨率必须严格一致,否则拼接会报错。我一开始通道数设计成不对称,调试了半天才发现是拼接维度对不上。

第二个坑是注意力层的显存爆炸。在64x64分辨率上做全局注意力,序列长度4096,注意力矩阵是4096x4096,单这一层就能吃掉好几个G。解决办法是在高分辨率用窗口注意力或者干脆不加,只在16x16、8x8这些低分辨率层加全局注意力。

第三个坑是训练初期loss震荡。Unet对学习率比较敏感,我实测用1e-4起步、配合cosine退火比较稳,用1e-3直接发散。另外EMA(指数移动平均)对Unet特别重要,不加EMA出图质量明显差一截。

3. Transformer路线:为扩展而生,但要伺候好它

Unet跑通之后,我把同样的任务用DiT架构重写了一遍,感受完全是另一个世界——灵活是真灵活,难调也是真难调。

3.1 DiT的核心思想:把图像切成patch当token处理

DiT(Diffusion Transformer)的思路很直接:既然Transformer擅长处理序列,那就把潜空间特征图切成一个个patch,每个patch展平成一个token,加上位置编码,然后丢进标准的Transformer block里。文本条件通过cross-attention或者adaLN(自适应层归一化)注入。

和Unet最大的区别在于:DiT没有下采样和上采样,全程保持同一分辨率。这意味着它的计算量随分辨率是平方级增长的,所以patch size的选择很关键。SD潜空间是64x64,patch size取2的话就是32x32=1024个token,这个长度在可接受范围;取1就是4096个token,显存直接翻几倍。

我实测下来,patch size=2是性价比最高的选择,1024个token的序列长度,配合12到24层Transformer block,参数量能控制在0.6B到1.5B之间,单卡能训。

3.2 adaLN条件注入:比cross-attention更优雅的方案

DiT论文里提了一个很聪明的设计:用adaLN-Zero注入条件。具体做法是把时间步嵌入和类别/文本嵌入相加,通过MLP预测出每个Transformer block的scale和shift参数,对LayerNorm的输出做仿射变换。最后一层还额外预测一个gate参数,初始化为0,这样训练初期整个block相当于恒等映射,训练非常稳定。

这个设计比cross-attention优雅在哪?cross-attention要额外引入一组注意力参数,计算量不小;adaLN只是对归一化层做调制,参数少、计算省,而且初始化成0保证了训练稳定性。我在项目里两种都试过,adaLN收敛更快、显存更省,唯一缺点是文本条件的细粒度控制稍弱——如果你的任务需要精确的局部文本对齐,cross-attention还是更合适。

# adaLN-Zero 的简化实现 class DiTBlock(nn.Module): def __init__(self, dim, num_heads): super().__init__() self.norm1 = nn.LayerNorm(dim, elementwise_affine=False) self.attn = nn.MultiheadAttention(dim, num_heads, batch_first=True) self.norm2 = nn.LayerNorm(dim, elementwise_affine=False) self.mlp = nn.Sequential( nn.Linear(dim, dim * 4), nn.GELU(), nn.Linear(dim * 4, dim) ) # 6个调制参数:attn的shift/scale/gate + mlp的shift/scale/gate self.adaLN_modulation = nn.Sequential( nn.SiLU(), nn.Linear(dim, 6 * dim) ) # 零初始化gate,保证训练初期恒等 nn.init.zeros_(self.adaLN_modulation[-1].weight) nn.init.zeros_(self.adaLN_modulation[-1].bias) def forward(self, x, c): shift1, scale1, gate1, shift2, scale2, gate2 = \ self.adaLN_modulation(c).chunk(6, dim=-1) h = self.norm1(x) * (1 + scale1.unsqueeze(1)) + shift1.unsqueeze(1) h, _ = self.attn(h, h, h) x = x + gate1.unsqueeze(1) * h h = self.norm2(x) * (1 + scale2.unsqueeze(1)) + shift2.unsqueeze(1) h = self.mlp(h) x = x + gate2.unsqueeze(1) * h return x

3.3 从0搭DiT的关键参数与训练配置

DiT的搭建比Unet简单——没有复杂的上下采样结构,就是patchify、堆block、unpatchify三步。但训练配置比Unet讲究得多:

  • 学习率:DiT对学习率更敏感,我用1e-4配合warmup 5000步,比Unet的warmup要长。
  • 权重衰减:0.01到0.05之间,太高会欠拟合。
  • 梯度裁剪:必须开,阈值1.0,Transformer的梯度比Unet更容易爆。
  • 混合精度:bf16比fp16稳,fp16在DiT上容易出现梯度下溢。

patchify的实现要注意:用卷积实现比用reshape+linear更高效,kernel size和stride都等于patch size,输出通道就是hidden dim。unpatchify用转置卷积或者pixel shuffle都行,我用的转置卷积,实测更稳。

3.4 Transformer路线的坑:显存、收敛、位置编码

第一个坑还是显存。1024个token、24层、hidden dim 1152的DiT,单卡24G跑batch size 2都紧张。解决办法是梯度检查点(gradient checkpointing),用时间换显存,能省40%左右,代价是训练慢20%。

第二个坑是收敛慢。DiT前期loss下降比Unet慢,我一度以为是自己写错了,后来对比论文才发现这是正常现象——Transformer需要更多步数才能“找到感觉”。耐心训到2万步左右,loss曲线才开始和Unet持平。

第三个坑是位置编码。DiT用的是固定的2D正弦位置编码,我试过换成可学习的,效果反而差一点,因为可学习位置编码在分辨率变化时泛化性差。如果你的项目需要多分辨率训练,固定编码更稳妥。

4. 两条路线的融合与选型决策:别非黑即白

搭完两套之后我最大的感受是:Unet和Transformer不是对立关系,实际项目里经常是混着用的。

4.1 混合架构:Unet的骨架加Transformer的注意力

现在不少工作走的是混合路线——保留Unet的上下采样结构,但把里面的卷积注意力换成Transformer block。这样既有Unet的多尺度归纳偏置,又有Transformer的长程建模能力。我试过在Unet的瓶颈层和最低分辨率stage用Transformer block替换,出图的全局一致性有提升,显存增加在可接受范围。

这种混合架构的实操要点是:只在低分辨率层换Transformer,高分辨率层保持卷积,否则显存扛不住。另外Transformer block的初始化要小心,建议用零初始化输出投影,保证训练初期不破坏原有Unet的行为。

4.2 选型决策表:按你的实际约束来选

我把选型时该问自己的问题整理成一张表,照着填基本不会选错:

你的约束推荐路线理由
单卡24G以内,快速出效果Unet成熟、省显存、调试快
需要多模态联合训练Transformertoken拼接天然支持
参数量要上到3B以上Transformer扩展曲线更优
团队没有Transformer调参经验Unet收敛行为更可预测
要做视频/长序列生成Transformer序列建模能力强
推理延迟要求极低Unet结构轻、推理快

4.3 从0搭SD的完整最小闭环

不管选哪条路线,一个能跑的最小闭环都包含这几块:VAE(可以直接用现成的)、文本编码器(CLIP现成的)、Backbone(自己搭)、训练循环(噪声采样、前向、loss、反向)、采样器(DDIM或DPM-Solver)。我的建议是先把VAE和文本编码器用现成的冻结住,只训Backbone,这样能把问题聚焦。

训练循环里有个容易忽略的点:噪声调度。linear schedule和cosine schedule出图风格不一样,cosine在低噪声段更细腻,linear在高噪声段更激进。我默认用cosine,配合v-prediction,实测比epsilon-prediction在细节上更好。

提示:第一次跑通不要追求出图质量,先确认loss能稳定下降、采样能出有结构的图,再逐步调参。我第一版跑出来的图全是噪点,排查后发现是噪声采样和设备没对齐,这种低级错误新手很容易犯。

5. 实操排查:那些文档里不会写的坑

搭模型的过程里,报错和异常是常态。我把高频问题和排查思路整理出来,省得你重复踩。

5.1 训练不收敛的排查顺序

遇到loss不降,按这个顺序查:先看数据管道有没有问题(噪声和图像是否对齐、归一化范围对不对),再看学习率和warmup,然后看条件注入有没有写错(时间步嵌入维度、文本mask),最后看初始化。我遇到过一次loss完全不降,查了两天才发现是时间步嵌入的freq_dim和MLP输入维度对不上,静默广播成了错误形状,这种bug最坑。

5.2 出图质量差的典型原因对照表

现象可能原因排查方向
图全糊训练步数不够或学习率太低加步数、调学习率
语义对但构图乱文本条件注入过强减少cross-attention层数
颜色异常VAE解码问题或归一化错误检查VAE输入范围
局部重复纹理注意力坍缩加dropout或调温度
高噪声步出图崩噪声调度不匹配换cosine schedule

5.3 显存优化的几个实用手段

显存不够是常态,我常用的手段按性价比排序:梯度检查点(省40%,慢20%)、bf16混合精度(省30%左右)、把优化器状态换成分片存储、减小batch size配合梯度累积。最后这个最容易被忽略——batch size 1配合梯度累积8步,效果和batch size 8接近,显存却省了8倍。

6. 我个人在Backbone选型上的几点体会

折腾完这几轮,我最大的体会是:Backbone选型本质是在“归纳偏置”和“可扩展性”之间做权衡。Unet的卷积和跳跃连接是强归纳偏置,它“知道”图像是局部相关的、多尺度的,所以小数据小算力下表现好;Transformer的归纳偏置弱,它靠数据和规模去学这些规律,所以规模上去了才显出优势。

如果你的项目是验证想法、快速迭代,别犹豫,Unet起步,两三天能出结果。如果你明确要做多模态、要往大规模走,那从一开始就上Transformer,哪怕先做小规模,架构上的债早还比晚还省事。混合架构是个不错的折中,但别为了混合而混合,先想清楚你要解决的是全局一致性问题还是局部细节问题。

最后分享一个我调试时的小习惯:每次改Backbone结构,先用一个极小的数据集(比如几十张图)过拟合一遍。如果连几十张图都过拟合不了,说明结构或训练配置有硬伤,别急着上大数据集浪费时间。这个“过拟合小数据集”的土办法,帮我省下的调试时间比任何工具都多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询