
调图像生成模型一年多了最让我头疼的始终不是风格崩坏而是“左右不分”。前段时间接了个电商需求要生成“左手抬起”的模特模型死活塞给出右手把提示词里的 left 改成“画面左侧”它又直接把人挪到构图左边。后来被一个做物理视觉的朋友提点“方向本来就是相对于参照系才成立的你给它固定一个参照系不就好了”这让我想到了伽利略相对运动——运动是相对的位置坐标必须有坐标系才有意义。顺着这个思路我试出了一种“绝对坐标锚定法”在生成过程中给模型注入一个不随采样变化的坐标场把左右从语义猜测变成几何读取。这篇文章把完整的思路、实现和踩坑记录分享出来希望给同样被方向问题折磨的人一条可复现的路径。1. 左右不分的复现现场图像模型的方向悬案1.1 那些让人抓狂的翻车案例如果你觉得“模型镜像”只是社交媒体上的搞笑素材真到交付环节就会知道痛。我自己踩过的几类翻车几乎覆盖了所有常见场景文字和招牌输入“STOP 禁止通行标志”模型生成的是镜像字母T 和 O 还勉强认得“S”直接反写。早期版本的扩散模型对字母镜像尤其严重因为字符符号的方向本身不包含在语义描述里。人物细节让模型画“左耳戴耳机”耳机出现在右耳生成“刘海向左侧”的人像刘海方向永远和需求相反。最典型的还是手表同一句“右手戴表”十张图里有六张是左手戴表剩下四张倒是不戴了。机械构件汽车方向盘“左舵”和“右舵”经常搞反吉他手“左手按弦”变成右手按弦。这类物体对左右有严格的功能性定义一旦错位整张图都显得荒诞。场景与交通生成“向左转的路标”时方向箭头左右颠倒地图类素材里的“东/西”标注也可能翻面。大模型并不理解“左”是相对观察者还是相对画面它只知道训练数据里哪个方向出现得更频繁。这些问题有一个共同特征方向错误不是随机噪声而是系统性偏向。也就是说模型对某些方向的输出会稳定地偏向某一边这是数据集和训练方式造成的先验偏置不是采样时偶发的失误。1.2 问题出在哪不是模型笨是“左右”没有物理定义我一开始也以为是模型训练得不够后来认真复盘才发现根子比这深得多。在二维图片里“左”和“右”根本不是物体的固有属性而是观察者视角和图像坐标系二者共同决定的。一张照片如果把底片镜像翻过来人物脸部的肌肉纹理、头发的分缝方向都会跟着翻转但图像本身依然是“一张正常的人脸”。人类能认出照片被翻转了通常是因为记忆中的视觉场景有方向锚点比如字的笔画、衣服口袋的位置。而模型没有这种记忆它只知道像素分布。更要命的是训练数据本身自相矛盾。很多生成模型的训练管线会做随机水平翻转增强但文本标签没有跟着翻转。比如原始图片是“左手叉腰”水平翻转后变成右手叉腰文本还是“左手叉腰”。模型在梯度更新里看到同样的文本配了两种方向的图像它只能选择“哪个方向先出现就学哪个”或者干脆把左右在特征空间里揉成一团。我还做过一个粗糙统计把 COCO 数据里带 left 和 right 的图像描述向量抽出来用 PCA 降到二维看分布两条子空间几乎是重叠的。这印证了一个判断语言模型对“左右”的嵌入表征先天弱于对“红色”“圆形”这类物体属性的表征。方向是一个关系概念而 CLIP 这类模型擅长捕捉实体和属性不擅长捕捉视角关系。综合这些原因模型左右不分不是少训了几万步而是数据、文本编码、增强策略三个环节同时缺了一个“坐标系”。2. 伽利略相对运动给出的新思路方向需要一个参照系2.1 伽利略变换教会我的事伽利略相对运动说的是在一个匀速直线运动的车厢里手边的水杯如果没有外力作用会和车厢保持相对静止车里的人感觉不到自己在动。它背后是一个坐标变换公式如果 S 系静止S 系相对它以速度 v 沿 x 轴运动那同一个点在两个系里的坐标满足x x - v*t y y z z t t这个公式中学生都会背但它背后有一个很关键的哲学意义任何“位置”和“方向”的描述都必须先声明参照系。你问“杯子在左边吗”如果不说清楚相对谁这个问题就没有答案。所谓绝对位置是不存在的运动都是相对的。图像生成问题里的“左右不分”本质上也是这个困境生成模型面对 pixel 坐标张量时没有一个预先声明的参照系。文本提示词里的 left 和 right 究竟对应坐标轴的哪个方向模型不知道只能靠统计大概猜。伽利略变换给我的启发是与其让模型去猜测“左右”的物理意义不如主动给它一个坐标系然后在这个坐标系里用“观察者移动但坐标轴不变”的方式训练。这样“左右”就从模糊语义变成了坐标轴上的正负号可以被梯度稳定地学习。2.2 “绝对坐标锚定法”的核心想法“绝对坐标锚定法”这个名字听起来唬人其实做法很简单在生成空间里预先指定一个不动的人为坐标系把每个像素的坐标写成一个张量然后作为额外条件送进模型。我这里说的“绝对坐标”不是物理意义上有绝对空间而是在一次生成任务内部约定一个不可变的标准。比如我可以约定坐标系原点在画面边缘中心x 轴向右、y 轴向下、角度 0 度指向右侧。这个约定在整张图的采样过程中保持不变模型生成每一个点的时候都能看到自己在坐标系里的位置。那么“左”就对应 x 负方向“右”对应 x 正方向方向不再是一个隐含在词汇里的抽象属性而是一组可以直接从坐标图里读取的数值。为什么这样能解决左右不分因为模型本质上是一个从条件到图像分布的映射网络它完全可以学会“当文本里出现 left 时把主体关键点放在 x 坐标为负的区域当出现 right 时放在 x 为正的区域”。前提是你让它看见那个坐标系。传统的 UNet 输入只有噪声图和文本条件没有这种显式的位置语义它自然只能靠猜。2.3 用相对运动做训练增强才是伽利略思路的精华如果只是把坐标图拼进输入那还只是一个工程技巧称不上“基于伽利略相对运动”。真正的关键是训练时如何处理坐标系和观察者之间的关系。我采用的做法是训练时把坐标锚定图随机施加平移、旋转和镜像变换这模拟了“观察者在动”的效果。但文本提示里的方向语义保持相对坐标轴不变。也就是说模型要做的是哪怕坐标系整体被旋转了 10 度、平移了半个画面它依然能准确地把“left”映射到变换后坐标系的负 x 方向。这个设计的物理直觉是伽利略变换的核心思想一个物体在 S 系里在左边在 S 系里可能由于观察者移动而不在左边但如果我们把坐标变换本身也交给模型模型就能理解“左”不是某个固定视觉位置而是相对于坐标轴正负方向的关系。这样做出来的模型推理时即使坐标系换了方向、换了位置它也不会把左右搞错因为它在训练中见过无穷多种“相对运动”下的坐标变换。传统的水平翻转增强是对图像做变换但文本和坐标轴都不变于是模型学到的是“翻到哪边都行”的混淆。我的做法反过来了图像内容不变锚定坐标系变换让模型学会坐标系变换下的方向守恒。方向不再绑定到某个具体的构图位置而是绑定到坐标轴的语义方向。3. 绝对坐标锚定法的具体落地给扩散模型塞进一个虚拟观察者3.1 坐标锚定图怎么构造先看代码。我要构造的是一个四通道的锚定张量宽度和高度跟随 UNet 的 latent 尺寸import torch def make_anchor_map(h, w, devicecuda): xs torch.linspace(-1, 1, w, devicedevice) ys torch.linspace(-1, 1, h, devicedevice) x_map, y_map torch.meshgrid(xs, ys, indexingxy) r_map (x_map**2 y_map**2).sqrt() theta_map torch.atan2(y_map, x_map) # 通道: [x, y, 半径, 角度] anchor torch.stack([x_map, y_map, r_map, theta_map], dim0) return anchor.unsqueeze(0) # shape [1, 4, h, w]这里我还在每个像素上加了数值都是干什么用的x_map 和 y_map给每个像素一个绝对位置身份。模型看这个通道就知道当前生成的细节对应画面里的哪个方位。r_map距离画面中心的半径。它能帮模型判断物体距离“观察者”的远近这在模拟观察者移动时非常关键。theta_map极坐标下的角度。它的好处是旋转之后数值会平移模型可以通过学习角度变化来理解方向变换。这段代码本身很简单但有几个细节会影响效果。锚定图要在 latent 空间的分辨率上生成而不是在像素空间再 resize。因为 UNet 的输入是潜变量图尺寸通常是 64×64 或 32×32你如果把 1024×1024 的坐标图 resize 下来数值的线性度和各向异性都会被扭曲。直接按 latent 的 h、w 生成坐标关系才干净。3.2 如何注入 UNet 而不破坏原有特征把锚定图硬拼到输入通道的前四个通道是最简单的做法但我实测下来效果一般。原因是 UNet 的输入通道原本经过 zero-conv 和文本特征交互额外通道如果只在输入层出现模型很容易忽略它或者干脆过拟合到坐标噪声上去。我最后采取的是“三条路同时走”的策略第一路输入通道拼接。把 anchor map 作为额外通道和 latent 一起 concat保持最低成本的信息传递。这一路损失小但不够强。第二路浅层特征调制。把 anchor map 过一个 1×1 卷积得到 128 维的全局条件向量然后用 FiLM 的方式作用在 UNet 的中间特征上。这样坐标信息不仅存在于输入层还能通过仿射变换直接影响每个分辨率下的 feature map。这一路的“观察者状态”也可以一起编码进来比如观察者的平移量、旋转角作为一个 4 维向量接入。第三路辅助一致性损失。训练时从 UNet 的中间特征里拉出一个分支接一个小 MLP做一件额外的事预测当前输入的 anchor map 的 x 坐标符号和旋转角度。这个分支只在训练阶段存在推理时可以删掉。它的作用是强迫中间特征真的携带坐标信息防止模型只在输入层“看了一眼”完全没有往下游传播。# 示意图用一个小头迫使特征携带坐标信息 anchor_feature unet.mid_block_feature # [B, C, H, W] anchor_pred anchor_head(anchor_feature) loss_anchor mse(anchor_pred, anchor_map_target)实际训练时这个辅助 loss 的权重我给得很小lambda 大约 0.05否则会干扰主干生成能力。它的意义更像一个“提示词监督”帮助模型别把坐标图当空气。3.3 训练与推理时的完整流程训练阶段我大概分成四步拿到带方向描述的图文对。文本里至少要出现 left/right/左侧/右侧 这类词图像质量可以粗糙但方向标注要准确。清洗数据时我用了一个简单的规则过滤掉同时含 left 和 right 的句子避免歧义。构造锚定图并做随机伽利略变换。训练时 anchor map 有 50% 概率保持单位坐标50% 概率做随机旋转角度在正负 15 度内、随机平移不超过画面宽高 10%、随机镜像。保持文本不变。训练主扩散模型或 LoRA。用正常的扩散 loss 加上小幅度的辅助锚定 loss。如果只想快速验证训练 LoRA 就够了不需要全量微调。定期用方向分类器评估。我准备了一个很小的分类器用来判断生成图里主体朝向是否符合提示词每 500 步跑一次验证集防止模型学到“把所有物体都放到坐标轴右侧的偷懒解”。推理阶段不能再用随机变换锚定图必须固定为默认坐标系。我通常把“左”定义在 x 负方向“右”定义在 x 正方向观察者状态全部置零。与此同时提示词里的 left/right 不需要额外改写因为模型已经学会了把 text 里的方向词和坐标轴符号对应起来。如果要换观察视角比如想要“从背后看人的左侧”不需要重新训练只需要重新计算锚定图的旋转角度相当于把观察者在坐标系里转了个方向。这就是相对运动带来的灵活性坐标系变模型不变。4. 实测效果与翻车记录哪些方法真能用哪些是伪需求4.1 一个小规模的验证实验我给自己的锚定方法做了个内部对比实验。数据集是从互联网上收集的带左右方向描述的图像文本对筛完大概两万张做了一个 10 万步的 LoRA 微调基座是 Stable Diffusion 1.5。评测集是 1000 张 prompt包含“左手手表”“右侧路标”“左耳耳机”这类描述。为了不那么主观我搭了两套自动化指标方向满足率用一个方向分类器判断主体朝向是否与提示词一致文字镜像率用 OCR 检测生成图里的字母是否反向构图偏移误差计算主体包围盒中心相对于提示词指定区域的距离。结果如下指标基线 SD1.5锚定法微调方向满足率68.2%91.7%文字镜像率34.0%29.0%构图偏移误差0.310.12方向满足率提升非常明显这是锚定图直接带来的价值。但注意文字镜像率只降了五个点说明文字问题并没有被彻底解决。文字镜像和空间方向是两个不同维度的信息字母方向依赖字符本身的形变光靠坐标系很难完全纠正。如果你想彻底解决文字镜像还得配合 OCR 相关的字符一致性 loss锚定法只能解决构图层面的左右。4.2 必须避开的几个坑第一个坑是锚定图直接拼接导致过拟合。第一次实验我把 anchor map 无脑拼进 input训练中期模型反而出现严重的构图同质化所有生成结果都倾向把主体放在画面中心偏右的位置。原因是模型学到了一个捷径——直接对着 anchor map 的 x 正方向偏移就能满足大多数 prompt 的模糊统计。修复方法是在训练时对 anchor map 做随机 mask比如 15% 概率把整个 anchor map 清零迫使模型不能完全依赖坐标图仍然要参考真实内容。第二个坑是分辨率和 latent 尺寸不匹配。如果你在生成 512×512 图片时用了 64×64 的 anchor map但推理时模型被用到 768×768 或更复杂的分辨率坐标系里的 -1 到 1 范围会被 resize 得歪掉。解决方式是始终按当前 UNet 的 latent 尺寸生成坐标图不要偷懒一次性生成固定尺寸。第三个坑是文本方向词解析歧义。“左边”到底是指“画面左边”还是“对象的左手边”这二者经常冲突。我的解决方式是设置一个视角协议默认观察者的视点在画面内除非 prompt 里明确出现“从右上方看”“在场景左侧的人物”等描述否则统一把 left 解释为“屏幕坐标的左侧”。这样至少能让模型输出稳定不会一会按屏幕左一会按对象左。第四个坑是随机旋转的角度不能太大。我试过把 anchor map 随机旋转 30 度模型整个训练过程都在震荡因为大角度旋转会严重挑战 UNet 现有的空间归纳偏置。后来限制在正负 15 度以内训练才稳定下来。这也合理物理里的伽利略变换只描述匀速直线运动对应图像的平移和定速旋转如果观察者突然原地转 60 度语义关系保持起来就难了。第五个坑是不要在文本里同时出现左右。prompt 里如果同时写“一个人左手拿苹果右手拿橙子”模型往往把两个方向混在一起。锚定法只能让模型区分“文本提到的方向”和“坐标轴方向”并不负责解决多目标的多方向绑定。多方向场景建议拆成两次生成再合成或者借助 ControlNet 的分区域控制。5. 边界与后续从“左右”扩展到“前后上下”5.1 坐标锚定法的自然延伸“左右不分”只是空间方向混淆里的一个典型代表上下、前后、内外其实也是同一类问题。锚定图本来就有 y 通道如果你在构造时额外增加深度通道 z_map就可以把同样思路推向“抬头低头”“前后遮挡”等问题。比如 3D 生成模型里可以把相机位姿当作观察者状态把三维坐标锚定场作为条件。这样“从左侧看”“从上方看”就转化成了锚定场的旋转更新模型不需要针对每种角度重新训练。这个跟 NeRF 里的位置编码有点类似但思路不一样我强调的不是单个点的频率编码而是整个空间的坐标系定义。它给模型提供了一个“方向记忆”无论在哪个视角它都知道上下左右相对谁的坐标轴成立。我还在实验把这种方法用到 ControlNet 上让 ContorlNet 的骨骼图同时携带一个方向锚定通道。效果比单纯在 prompt 里改文字更稳定因为 ControlNet 是空间条件它直接喂给 UNet 的是局部特征锚定信息天然更接近最终输出的像素空间。如果将来把锚定图和 ControlNet 结合起来做产品化应该能解决大量工业场景里的方向问题。5.2 我的取舍建议这套方法不是万能的也不是所有项目都值得上。如果只是偶尔生成一张图发现方向不对用负向提示写“right hand, mirror image, flipped”往往就够了没必要动模型。但如果你的项目是批量生成产品图、广告文案配图而且反复出现系统性错向那我建议你试一次锚定 LoRA只需要在现有训练代码里加一个坐标张量成本不高效果可测。我个人最后保留的做法是在训练 LoRA 时加入一个两通道的 x/y 锚定图配合辅助 loss 和随机 mask。这基本不影响原模型风格但能稳定解决绝大多数“左手/右手”“左侧/右侧”的构图漂移。唯一要记住的是锚定图要跟着训练分辨率走观察者状态不要一开始就设成复杂旋转先从固定坐标系跑通再逐步放开平移和旋转增强。这个方法说到底是给一个“方向先天模糊”的模型补上了一个参照系。伽利略说运动是相对的那左右也应该是相对的。只要让模型明确相对谁、相对哪个轴很多玄学问题都会变成可以测量的几何问题。如果你也在图像生成里被方向折腾过不妨把自己当作那个观察者先给模型画一个坐标系再让它自由发挥。