如果你也在做文生图相关的落地项目,多半遇到过这种场景:同一句提示词,设了不同的随机种子,跑出来五六张图,看起来却像同一张图的轻微滤镜版本。我去年在某内容生成团队负责提升生成结果的多样性,被这个问题折磨了几周后,做了一个叫 GASS 的小模块——全称是 Geometry-Aware Spherical Sampling for Disentangled Diversity Enhancement。名字比较长,但做的东西其实很聚焦:在扩散模型的潜空间里,用几何感知的球面采样,把“多样性”从随机撞大运变成有方向、有边界、可解释的显式控制。
这篇文章不打算写成论文复现,而是分享我在项目里从想法到落地踩坑的全过程。适合谁看?如果你正在做文生图模型的二次开发,或者做批量内容生成时被结果雷同困扰,又或者单纯好奇“解耦多样性增强”在工程上怎么落地,这篇应该能给你一些可操作的经验。我会尽量把原理讲得直白,把实现细节和踩坑记录也一并放出来,免得你在同一条路上再花一遍时间。
1. 被“同一提示词千篇一律”逼出来的项目:GASS 到底想解决什么问题
1.1 一次让我挠头的复现实验
事情起因很朴素。某次内部评估,我需要把同一个提示词跑出 10 张样张,给客户挑风格。那是我第一次认真观察生成模型在不同种子下的输出分布,结果让人崩溃:10 张图里,构图、配色、姿态基本一致,只有衬衣的纹路和背景亮度有轻微差异。方案汇报里要求的“风格多样化”,实际交付成了“磨皮三连”。
一开始我以为是采样次数不够,把生成批次从 4 调到 16,又换了几个随机种子,情况只好了那么一点点。真正的问题不是随机性不够,而是扰动被模型“吸收”了。主流通用扩散模型在潜空间采样时,随机噪声往往集中在低频分量,这些分量决定的是整体色调、大面积布局这类容易被语义绑死的因素。模型为了保证提示词对齐,会把这些扰动在去噪过程中逐渐平滑掉,最终输出的差异自然就被压缩到一个很窄的范围。
1.2 多样性缺陷的本质:采样并不是“遍地撒网”
要理解 GASS 的出发点,可以先看看常规提高多样性的三种手段为什么都不够。
第一是换随机种子。这个方法在文生图里是最常见的,但种子影响的只是初始噪声初始化,经过几十步去噪后,大部分高频细节被重新采样,留下的低频骨架仍然由提示词和模型先验主导。第二是调大 guidance 相关参数。这个参数调节的是“语义贴合程度”,它会让结果更听话,但不会给结果“换姿势”。调过头反而会出现色彩过饱和、边缘发硬。第三是改提示词。提示词一变,语义边界确实变了,但这不是“同一条件下的多样性增强”,而是换了需求。
所以我们需要的不是让模型更随机,而是让它在保持提示词语义的前提下,有更多“合理的结构性变化”。这个目标听起来简单,落地却绕不开一个几何问题:到底该往潜空间的哪个方向加扰动,才能产生可见但不跑题的视觉差异?
1.3 “解耦”和“几何感知”在标题里的真实含义
GASS 这个模块把上述问题拆成了三步:先理解当前输入内容的几何结构,再在潜空间球面上采样一组方向,最后按照内容区域施加差异化的扰动强度。标题里的三个关键词——“Geometry-Aware”“Spherical Sampling”“Disentangled”——分别对应这三步中的关键选择。
几何感知的意思是,扰动不能无脑加。人像的主体区域在画面中段,背景天空在远端,不同区域对干扰的容忍度完全不同。球形采样指的是,扰动方向不是在高维空间里随便撒高斯点,而是在单位球面上做有约束的采样。这样多个扰动方向可以被归一化、正交化,不会挤在一起导致冗余。解耦则是说,方向之间最好各管各的:姿态方向和光照方向分开,调整一个的时候不牵连另一个。
这套思路在项目里最终落地为一个可插拔的模块,不修改生成模型的权重,只改变推理时潜变量的采样策略。我后面所有实验和踩坑记录,都是在这个前提下展开的。
2. 球形采样与几何感知:拆开看 GASS 的两个核心设计
2.1 “球形采样”采的到底是什么
如果你对扩散模型的潜空间有印象,大概知道一张图的 latent 编码可以看作高维空间里的一个点。我们想让生成结果变化,本质上是让这个点沿着某个方向移动。但问题在于,潜空间不是平坦的,移动方向和视觉语义之间存在高度非线性关系。随机移动一个方向,很可能落入提示词不匹配的区域。
球形采样解决的是“方向从哪里来”。我们把当前提示词对应的基准点记作中心,在它周围的单位球面上抽取若干方向向量。这些向量决定了生成结果往哪些语义方向漂移。球面采样的优势在于,所有方向天然有相同的模长,不会因为某个方向向量过长而让结果失控。
最开始我图省事,直接用高斯分布随机采方向,结果采样出来的向量经常高度相关,比如 8 个方向里 6 个都落在“变亮”附近,剩下的 2 个落在“变暗”,视觉多样性几乎没有提升。后来换成在单位球面上做均匀采样,再用 Gram-Schmidt 正交化把方向拉开,多样性指标立刻涨了一截。这个细节听着简单,却是整个项目最关键的取舍之一。
2.2 几何感知:全局先验与局部掩码两层配合
“感知几何”在 GASS 里不是一句空话。我们实际做了两层。
第一层是全局几何先验,来自文本提示词。不同物体类别的空间结构差异很大。“人像”的语义方向主要作用于姿态、表情、肢体比例;“风景”则更多作用于光照角度、取景远近、色彩基调。GASS 用一个轻量文本分类头,从提示词里提取类别特征,再把它折算成一串采样方向的增益系数。相当于让模型先知道“这是一张人像,姿态方向可以多动一点,场景方向少动一点”。
第二层是局部几何掩码。我们用生成过程中的 cross-attention 图来定位语义主体在画面中的空间范围,比如人物、桌子、远处的山。这一步输出的是一张空间权重图,主体区域权重高,背景区域权重低。采样方向施加到潜变量上时,会先乘以这张权重图,保证结构变化发生在主体附近,而不是把整张图的底色搅乱。
这两层加在一起,才真正配得上 Geometry-Aware 这个名号。实际跑下来,全局先验让“该往哪儿动”变得合理,局部掩码让“动了之后不破坏画面”变得可靠。两者缺一,都会出现崩坏或风格偏移。
2.3 解耦不是各改各的,而是让方向有明确语义坐标
项目里最容易误解的一个点是“解耦到底解什么”。解耦不是说姿态方向只许改姿态,光照方向只许改光照,图像生成模型的中间特征层本身是耦合的,做不到这么干净。我们的做法是:在方向向量表和视觉语义之间建立映射,让每个方向具有可解释性,同时通过正交化降低相互干扰。
具体实现上,GASS 维护一张方向向量表,每个表项对应一个可解释的操作方向:主体平移、姿态旋转、光照变化、材质噪声、背景虚化等。每张新图采样时,不是直接用这些表项,而是由内容分析分支输出一组权重,把这些表项做线性组合,得到实际的扰动方向。因为表项之间彼此正交,调整姿态方向的权重时,它贡献到光照方向的分量理论上为零,这样控制起来才接近“拧一个旋钮只动一个参数”。
当然,理论正交和实际解耦还有距离。低层特征上,方向之间仍然会通过 attention 层发生串扰。我们后续加了一个轻量的投影层来修正,这部分在后文评测里会提到。
2.4 为什么方向一定要归一化和均匀覆盖
很多人写代码时顺手就做normalize(sample),但没细想归一化到底带来什么。在潜空间里,方向向量的模长直接决定扰动强度。如果不对模长做约束,有的方向移动半步就很大变化,有的方向移动两步都没反应,批次内多样性会非常不均匀。所以 GASS 里所有方向向量在进入注入模块前,都会统一投影到单位球面上。
均匀覆盖则是为了防止“采样了八个方向,实际等效于三个方向”的浪费。我们希望方向集合尽量铺开,于是采样后做一步相关性筛选,余弦相似度超过阈值的向量会被重新采样。这一步让同样数量的方向数能覆盖更多的视觉变化模式。实验数据里,方向数从 4 增加到 8 时,多样性不是线性增长,而是阶梯式跳跃——这背后的原因就是覆盖率在起作用。
3. 从原理到实现:GASS 的关键工程决策与参数选择
3.1 注入位置的选择:放浅层还是深层
原理想清楚之后,第一个工程问题是:扰动往生成模型的哪一个环节注入。
我最初试过直接在初始噪声上加方向扰动,效果最差。因为噪声经过整个去噪过程后,扰动会被逐步抹平,真正保留下来的是低频色彩偏移。我也试过在最终解码前加扰动,结果更糟,画面会出现大块彩色噪斑,因为此时特征已经高度语义化,强行改变方向会直接撕裂结构。
最后稳定下来的方案是分两层注入,并且避开最初的噪声层和最后的解码层:一个是中低层特征图,负责结构姿态层面的变化;一个是中高层特征图,负责材质纹理层面的变化。两个位置的扰动强度走不同的增益系数,前者衰减慢,后者衰减快。这样整体上既能改变姿态,又不会让纹理细节糊成一团。
为了说明不同位置的差异,我把实测里的典型现象整理成了表格:
| 注入位置 | 主要影响 | 典型副作用 | 适用场景 |
|---|---|---|---|
| 初始噪声层 | 色调整体漂移 | 主体结构不变,布光没变化 | 几乎不可用 |
| 中低层特征 | 姿态、构图、视角 | 偶尔出现肢体结构扭曲 | 主体方向控制 |
| 中高层特征 | 材质、细节、纹理 | 细节过碎,风格轻微偏离 | 材质多样性 |
| 解码前特征 | 局部色块失真 | 出现噪斑和伪影 | 不可用 |
3.2 注意力图提取与空间掩码的构造
局部掩码的实现依赖 cross-attention 图。文生图模型在去噪时会计算文本 token 和图像 token 之间的注意力权重,这个权重矩阵天然带有“哪个文本词对应画面哪个区域”的信息。我需要做的就是把每个 token 的注意力图汇总成一个空间掩码。
汇总时有个容易踩坑的点:注意力图的和不一定为 1,不同 token 量纲差别很大。如果直接做 softmax 再累加,主体 token 因为权重较高会把掩码值顶得极大,背景区域几乎被压成 0,导致背景完全无法产生结构变化。我们的做法是先对每张注意力图做 min-max 归一化,再按 token 的语义类别加权累加,最后再做一次空间平滑。这样掩码的边界不会锐化成硬切,背景也会保留一个较小的基础值,防止“只有主体动、其他全死板”的问题。
掩码分辨率也需要适配。注意力图通常比实际输出分辨率低很多,直接上采样到目标尺寸会导致边缘锯齿。我们在上采样后加了一层高斯模糊,半径取 3 到 5 个像素。这个操作看似不起眼,却直接决定了区域交界处会不会出现生硬的“贴图感”。
3.3 正交基初始化与方向合成
方向向量表的初始化,我踩过一次浅坑:直接用随机矩阵初始化,再正交化,得到的基向量和常见语义并没有稳定对应关系。后来改成半人工初始化:先随机生成一批候选方向,分别注入模型,观察生成的图片变化方向,把变化明显且语义单一的方向挑出来作为基底。这一步我称之为“语义校准”,是整个 GASS 里唯一需要人工介入的环节。
方向合成公式倒是简单:最终扰动方向等于方向基底的线性组合,权重来自内容分析分支。组合后的方向还要再过一次正交化,确保它不与基底中其他方向重合。综合权重和方向基底的归一化乘积就作为最终的潜变量扰动项。
3.4 几个容易拍脑袋定错的参数
方向数。太少覆盖不够,太多方向之间相互干扰。我测试过 2、4、8、16 四个档位,4 到 8 是最甜区,16 时视觉多样性增加有限,显存占用却明显上升。
扰动强度。这个参数直接控制“改多少”。保守的做法是从 0.2 起步,每档加 0.1 观察结果。有人贪心想一步到位调到 1.0,出来的常常是形变或者色彩溢出。
正交化阈值。方向之间余弦相似度超过 0.85 时重新采样。我一开始用 0.95,导致重新采样太频繁,严重拖慢速度;后来放宽到 0.85,速度和多样性兼得。
3.5 一个参考伪代码结构
下面这段不是生产代码,只是把 GASS 的主流程梳理清楚,方便你看懂参数之间的协作关系。
def gass_generate(prompt, base_latent, num_directions=8): # 1. 内容分析:得到全局类别特征和空间掩码 cls_feature = text_encoder(prompt) attn_map = get_cross_attention(base_latent) spatial_mask = build_spatial_mask(attn_map) # 2. 方向采样:球面均匀采样 + 正交化 directions = sample_sphere(num_directions) directions = gram_schmidt(directions) # 3. 方向合成:结合内容感知权重 weights = content_head(cls_feature) final_direction = sum(w * d for w, d in zip(weights, directions)) final_direction = project_onto_unit_sphere(final_direction) # 4. 空间掩码与分层注入 perturb = final_direction * spatial_mask * strength mid_low_feature = inject(base_latent.mid_low, perturb * 0.7) mid_high_feature = inject(base_latent.mid_high, perturb * 0.3) # 5. 继续常规去噪流程 return denoise(prompt, mid_low_feature, mid_high_feature)这段伪代码省略了 attention 图缓存、方向语义校准等工程细节,但已经能表达 GASS 的整体推理路径。实际接入时,你只需要拿到生成模型中间层的输入输出句柄,不需要改动权重。
4. 评测实验中的惊喜与翻车:哪些指标能说明多样性真的提升了
4.1 常用指标:LPIPS 和 FID 的盲区
项目中期,团队内部需要一个可量化的指标来验收多样性。我们用了两个常见指标:LPIPS 和 FID。LPIPS 衡量两张图的感知差异,FID 衡量生成集与真实集的特征分布距离。刚开始测的时候,数值看着还行,但人工审图发现了一个尴尬情况:LPIPS 提升主要是亮度、色彩偏移贡献的,姿态几乎没有变化。
这说明单一指标容易骗人。只看 LPIPS,GASS 和普通随机噪声都能提升数值,但普通随机噪声的提升集中在低层像素差异上,GASS 的差异更多来自结构层面。我们后来加了一个辅助指标:把生成图的关键点位置、前景物体占比、光照主方向分别提取出来,计算这些语义属性在批次内的标准差。只有这些属性标准差提升了,才说明多样性是从结构层面发生的。
4.2 单方向扰动为什么崩坏,组合扰动为什么稳定
有一版实验让我印象很深:只注入“姿态旋转”这一个方向,强度调到 0.6,结果生成的人像大面积出现肢体扭曲,手指数量都开始不稳定。我当时以为是强度太大,降到 0.3 后扭曲消失,但姿态变化也几乎看不出来了。
这个翻车让我意识到,单一方向在高强度下的脆弱性来源于模型的语义耦合。姿态方向不只是改变骨骼坐标,也会连带改变遮挡关系、边缘轮廓。当模型试图保持主体一致性时,高频特征会被强行拉回原状,最终表现为结构崩坏。
组合扰动反而更稳定。原因在于,多个方向叠加时,每个方向的强度可以分摊到较低水平,整体扰动面积更广但单点冲击更小。我们在最终版本里把方向数固定为 6 个,混合姿态、光照、材质、背景四个类别,强度分布从单方向 0.8 降为每个方向 0.2 到 0.4,视觉多样性反而显著提升。这个“分散强度”的思路后来也用在其他容错场景里。
4.3 语义保持与细节偏移的平衡
评测里另一个矛盾是,多样性增强往往伴随语义偏移。客户要求“还是同一件红色毛衣”,GASS 增强后可能变成同一款式但颜色偏橙。我们用 CLIP 打分还是别的文本对齐指标,都会发现多样性越强,对齐分数越低。
这里没有免费的午餐,只能做权衡。我的调参经验是:主体 token 对应的空间掩码值不要低于 0.6,背景区域不要高于 0.35,这样主体语义保持力度足够,背景又能产生足够的视觉变化。也就是说,多样性不是平均施加在每个像素上,而是把预算倾斜到“不影响语义识别”的区域。
另外可以在损失函数侧做约束。我们在注入扰动后,加了一个轻量的语义一致性惩罚项,让扰动后的潜变量和原始潜变量在文本对齐特征上的夹角保持在一定范围内。这样多样性和对齐是可控的,不再是对立关系。
4.4 显存和推理时间实测
功能层面稳定后,就该看成本了。GASS 额外增加的显存主要来自两个部分:cross-attention 图的缓存,以及方向正交化时的临时矩阵。在某个常用的生成模型配置下,原生推理占 6.2GB 显存,接入 GASS 后升到 6.8GB 左右,增量还能接受。如果开着注意力图缓存同时跑 16 个方向,显存会跳到 8.1GB,这也是我后来不建议方向数超过 8 的原因之一。
推理时间方面,方向采样和正交化的计算量并不大,主要开销在注意力图提取上。第一次提取需要把模型的 attention 中间结果拷贝到内存,耗时约 30 毫秒;之后每次生成可以复用同一份掩码,因为提示词不变时跨 attention 结构相对稳定。整体来看,GASS 让单次生成耗时增加约 6%,对于批量生成场景完全可以接受。
5. 实际部署时我踩过的坑:从采样方向冲突到交互式调试
5.1 固定方向数导致的“样本聚集”
最初版本里,我固定每个 prompt 生成 8 个方向。测试集跑下来,发现某些 prompt 的 8 张图之间差异很大,另一些 prompt 的 8 张图却几乎重复。问题出在内容分析分支:当提示词本身比较模糊时,输出的权重向量方差很小,8 个方向经过线性组合后高度近似,等于只用了 2 个有效方向。
解决办法是加入一个自适应的方向去重步骤。先生成 12 个候选方向做语义构图预测,再根据预测结果筛掉冗余,只保留差异最大的 6 个用于实际生成。这个做法牺牲了一点首帧延迟,但换来的是批次内多样性稳定。在模糊提示词和具体提示词上的表现终于拉齐了。
5.2 空间掩码归一化陷阱
空间掩码的归一化问题前面提过,这里说一个更隐蔽的坑。有一版实验里,我对掩码做了严格归一化,使整张掩码的均值为 1。看起来没问题,但实际生成时发现,主体区域扰动不足,背景区域扰动过大。因为注意力图在主体区域的峰值很高,均值归一化会把整个掩码整体缩小,反而压低了主体的扰动强度。
正确的做法是基于分位数归一化,而不是均值归一化。把注意力值映射到 0 到 1 区间后,用 95% 分位数作为主体区域的参考值,低干 20% 分位数的背景区域直接压到接近 0.1。这样既能保证主体区域扰动幅度足够,又不会让背景完全静止。
5.3 提示词种子参数与生成变量的耦合
这是个很容易被忽略的运营问题。接入 GASS 后,同一个“种子”在开关模块时,生成的图完全不同,导致原本依赖种子做可复现性的流程全部失效。后来排查发现,GASS 在采样方向时使用了独立的随机数流,并且这个随机数流和生成模型的种子没有同步。于是我改了实现:方向的随机种子由原始种子推导出来,用固定哈希函数映射,保证同一原始种子每次产生同一组方向向量。
改完之后的体验是,用户调种子能观察到连续的多样性变化,不再是“开了 GASS 就是另一套随机世界”。如果你想保持一个稳定的基准图,就固定种子;想探索多样,就调一个方向扩散系数,两者可以解耦控制。
5.4 交互式调试脚本
最后分享一个非常朴素的调试工具。GASS 有太多参数要试,我写了一个简单的交互式脚本,用命令行实时调整方向数、强度、掩码阈值。
python run_gass_demo.py \ --prompt "a quiet street at dusk" \ --num_directions 6 \ --strength 0.35 \ --mask_quantile 0.8 \ --dry_run脚本会把 9 张结果排成 3×3 网格输出,方便一眼看出哪个参数出了问题。等参数稳定后再把同样的配置接到批量生成流程里。这个脚本虽然简陋,但比每次改参数都要重新跑完整评估快得多,也算是 GASS 开发过程中最大的效率提升工具。
6. 后续可以怎么玩:从多样性增强到可控语义滑动
GASS 做完之后,我最大的体会是,它本质上不是“让结果乱一点”,而是给了你一个旋钮面板。每个方向向量对应一种语义变化,调整权重就是调整这个变化的分量。顺着这个思路,后续可以延展出几个有意思的方向。
第一个是图像编辑。给定一张参考图,提取它的潜变量,再叠加 GASS 方向,可以做到“保持整体构图不变,只转动某个语义参数”。比如把“光照方向”权重从 0 滑到 1,画面会呈现出从晨光到午后的渐变。这个能力对电商场景非常实用,一套商品图可以快速出一组不同光照的变体。
第二个是批量风格生成。在内容生成团队的日常里,经常需要快速交付多风格素材。GASS 的方向表可以预置一批“风格方向”,比如怀旧对称色调、古典水彩氛围、硬朗印刷质感。生成时用权重控制风格倾向,比反复改写提示词稳定得多。
第三个是作为其他可控生成插件的前置处理。GASS 生成的方向集合可以作为候选语义骨架,交给更精细的后处理模型去细化。实测下来,这种分层做法比直接让后处理模型从零生成要快将近一倍,因为 GASS 已经保证了候选样本的分布覆盖。
最后按老规矩分享一点个人经验。做这类多样性增强,别一上来就想用一个大模块包打天下。先把采样方向做正交、把掩码做平滑、把扰动强度做分散,这三步到位后,哪怕没有复杂网络结构,效果也足够撑起一次交付。GASS 这个名字看起来很长,实际落地下来其实是一串朴素工程选择的集合,而它给我留下的真正收获是:在潜空间世界,方向永远比强度更重要。