图像超分辨率做到一定程度,瓶颈往往不在“放大倍数”,而在结构重建。边缘糊不糊、纹理真不真,直接决定观感。我最近在人工智能顶会看到一项叫DHOGSA的工作——一个即插即用的自注意力模块,用HOG先验引导Transformer的特征聚焦边缘,把PSNR又往上抬了一截。这篇文章不打算介绍会议背景,只聊DHOGSA本身的原理、实现细节、嵌入方式和踩过的坑,如果你正在做超分或者图像恢复,应该能直接拿去用。不管你是刚入门Transformer超分,还是已经在调自注意力那一堆矩阵乘法,这篇文章里的配置和排查思路,都能让你少走几趟弯路。
1. 为什么自注意力在图像恢复里需要先验引导
先说一个很多刚接触Transformer超分的人容易忽略的事实:自注意力并不是越多越好,它需要被引导。DHOGSA这个模块的出现,恰恰就是为了解决自注意力“什么都看、却不够专注”的问题。
1.1 Transformer超分网络的基本盘
Transformer进入图像超分领域,核心改动是去掉卷积的局部归纳偏置,改用自注意力去建模像素块之间的长距离关系。一个典型的Transformer超分网络,把低分辨率图切成一个个固定大小的patch,每个patch拉成向量,叠上位置编码,然后过若干层Transformer block。每个block内部由多头自注意力(MSA)和MLP组成,中间用LayerNorm连接。与CNN相比,MSA可以跨区域分配权重,理论上能利用到整个空间的信息,这也是它在超分任务上有一定优势的原因——远处相似纹理也能互相引用。
但这里有个很微妙的问题:自注意力的权重完全由输入的feature map决定。模型在训练中会倾向于把注意力分配到哪里,取决于损失函数和数据分布的合力,没有额外的结构先验时,注意力往往平摊到所有位置,或者被一些大面积的平坦区域带走,本质上是一种统计驱动的行为。对于重建任务来说,这种“摊大饼”的注意力并不是最优的——高频边缘区域通常像素值变化剧烈,但它们在整张图里只占很少的像素比例,很容易被低层特征的低频分量稀释。这就像一个人在一间大教室里找人,每个座位都看一遍,结果注意力全被别人桌上五颜六色的文具吸引了,真正想找的戴黑框眼镜的人反而花了好久才找到。
这也是我以前在实验里经常遇到的现象:PSNR貌似在涨,但把超分结果放大之后看,边缘糊了一片,特别是斜线和细结构。原因不是网络容量不够,而是注意力没有把资源集中在那些决定主观质量的位置上。当时试过加边缘损失,有效,但边际效应越来越小,后来才发现问题出在“引导位置”不够靠前——损失只能改变梯度的方向,没法直接重组注意力内部的分配逻辑。
1.2 边缘信息为什么值得单独建模
这里需要先说清楚人眼对图像的感知偏好。人眼对高频结构非常敏感,边缘位置哪怕只有一到两个像素的偏移,主观打分立刻会掉。PSNR这种像素级指标虽然不完全等于主观质量,但边缘重建好了,PSNR通常也是实打实上分。所以超分领域的很多工作都在想办法“强化边缘”,比如额外加一个边缘损失,或者在网络里接一条边缘监督分支,这些路径的共同点都是想让网络把更多精力放到边缘上。
HOG(方向梯度直方图)是一个特别经典的特征描述子。它把图像分成小格子,在每个格子里统计梯度方向的分布,得到一条直方图,再用块内归一化消除光照变化的影响。一个直观的理解是:HOG天然就是“边缘密度图”——哪里梯度强、方向集中,哪里就可能是边缘、角点或纹理。用一个预计算的HOG特征图作为额外输入,等于直接告诉自注意力:在这些地方,你给我认真看。和直接拼接边缘图相比,HOG多了方向信息,上下游的注意力能知道这是一根横向边缘还是竖向边缘,从而在QK匹配时更容易找到正确相关的区域。
对比其他先验,比如Canny边缘图,HOG的优点在于连续性和方向信息。Canny给出的是二值边界,HOG能保留“哪条边缘是横向、哪条是纵向、梯度有多大”这样的中间状态,融合进注意力时损失信息更少。我见过一些同学直接用一个预训练边缘检测网络提取边缘概率图,效果也确实能涨点,但那个检测器本身就额外占用了不少参数和计算量。HOG不需要学习,几个固定卷积核就能算出来,作为先验模块足够轻量,这也是DHOGSA值得推广的一个理由。
2. DHOGSA模块的完整设计与嵌入方式
这一节是整篇文章的核心。DHOGSA的设计思路虽然简单,但落地时涉及的细节不少,尤其是如何把离散的直方图统计变成一个可导、可训练模块,以及如何嵌入到现有Transformer block里而不破坏原有结构。
2.1 HOG先验怎么变成注意力掩码
理论上的设计思路是:先从输入图(或者中间feature)中提取HOG特征,然后把HOG特征映射成一个“注意力偏置”,加在softmax之前的logits上。具体到实现,一般会走这样几步:
第一步,用Sobel算子或者Scharr算子计算输入张量的水平梯度和垂直梯度,得到梯度的幅度图和方向图。这里有一个关键细节:如果整个模块要端到端训练,Sobel算子最好写成固定卷积核的卷积层,这样梯度流能自然穿过,梯度幅度和方向的计算都是可导的,不需要做任何近似处理。注意,这一步的输入建议是低分辨率原图,而不是经过若干层网络之后的feature map,原因后面讲。
第二步,把幅度和方向组合成HOG特征。常见做法是把角度分成几个bin,比如0-180度分成9个bin,然后根据每个像素的方向和幅度做加权投票。但这里是图像超分,不是行人检测,不需要严格保持HOG的原始形式。很多实现会简化一点:不硬分bin,直接用梯度幅度、方向余弦、方向正弦三个通道作为“HOG-like”特征。这样做的好处是特征连续且可导,训练更稳。如果硬把方向量化到bin里,会产生取整操作,梯度就断了,训练时网络学不到东西。
第三步,HOG特征经过一个小的卷积头,比如1x1卷积加3x3卷积,输出一个单通道的mask,缩放后作为注意力logits的偏置。这个mask最终值的取值范围需要控制,避免喧宾夺主——正常情况下,它会是一个在0到1附近的小数,乘上一个可学习的系数λ。我的个人习惯是在得到mask之后做一个全局平均或局部平均,把它归一化到一个温和的尺度,再乘以λ。这一步很重要,如果mask直接以绝对值形式加到logits上,数值可能很大,softmax一下就几乎变成二值注意力,训练容易崩。
顺带提一个进阶处理:HOG mask不一定是单通道,也可以每个head一个mask,这样不同注意力头可以学到不同的边缘偏好,比如一个头更关注横向边缘,另一个头关注纵向边缘。代价是参数增加,但换来的是灵活度。我实测过单通道和head-wise两种方案,head-wise在叠加更深的Transformer block时涨点更稳定,但小模型上差距不大,一般推荐先用单通道。
2.2 即插即用模块到底是插在哪
“即插即用”的意义在于不改变原Transformer网络的骨干结构,只在特定位置插入一个旁路分支。对于MSA来说,最自然的插入点有两个:一是把HOG mask加到QK相似度矩阵上,相当于给注意力打分加一个先验偏置;二是把HOG特征与V矩阵做通道级拼接或调制,相当于让模块输出携带了边缘位置的信息。
从实验结果看,方案一更常见,因为它和标准的attention计算完全兼容。我们只需要在原MSA内部做一个小改动:在softmax之前,把QK相似度矩阵加上广播后的HOG mask,然后继续走softmax、乘V。这里还要做一步归一化,因为原始attention logits的范围取决于特征维度,mask不能直接硬加。比较稳的做法是对mask做层归一化,乘以一个可学习的系数λ,再加到logits上,不影响原始attention的scale。
方案二则是把HOG特征和V矩阵在通道维度上拼接,让每个token的value表示既包含原特征,又包含边缘先验信息。这样做的好处是,即便注意力权重没有明显偏向边缘,V矩阵中的HOG通道也能直接给后续MLP提供边缘位置线索,相当于双保险。缺点是多了一部分额外的通道计算量,而且要和原网络的特征维度对齐。
另一个容易忽略的插入位置是窗口注意力和全局注意力之间的过渡。有的Transformer超分网络把注意力分成窗口局部注意力和全局稀疏注意力两个阶段,HOG mask最好只在全局注意力阶段加入。因为窗口内部面积小,边缘占比相对高,不需要外部先验介入;而全局注意力负责跨距离传递信息,没有局部先验引导,容易出现“跨区域乱Attention”的情况,这时候HOG mask反而能帮助长距离的边缘对齐。我在实验里确认过,只在全局注意力里插DHOGSA的效果,比两处都插更好,还能省掉一部分计算。
一个标准的Transformer超分block内部的改动很小,代码上只需要在原来的MSA函数里加几行。这也是“即插即用”的核心优势:当原模型已经训练到很强的水平,不需要重头再来,只需要插入DHOGSA分支继续微调,就能在新分支和原始能力之间找到一个平衡点。这样既保留了原有模型学到的知识,又给注意力增加了一块结构先验的引导板。
3. 实操:把DHOGSA接进一个超分Transformer网络
光讲设计不动手,永远是纸上谈兵。这一节我拿一个结构简单的基于Transformer的超分网络当例子,完整走一遍“插入DHOGSA分支”的流程。基座模型不指定具体名称,你可以用自己手里现成的Transformer超分框架来替换。
3.1 基座模型选择与改造点
假设我的基座模型由浅层特征提取、若干个Transformer block、一个上采样模块组成。改造点选在每一个Transformer block的MSA层,我们在QK logits计算完之后、softmax之前插入HOG mask。为方便描述,我定义了一个名为DHOGSA的辅助模块,它包含一个固定的Sobel卷积层和两个可学习卷积层。
具体改造流程可以拆成三步。第一步,把输入张量同时送入原特征提取层和一个HOG先验提取分支。HOG分支第一层是固定的Sobel卷积,输出梯度通道,再接一个可学习的卷积层压缩通道数。第二步,将HOG分支的输出经过双线性插值,让它和attention map的尺寸对齐。第三步,在MSA内部按照设计方案把mask广播到(1, heads, HW, HW)之后,加到logits上。
注意,基座模型里可能已经包含下采样和上采样结构,比如内部有patch merging或pixel shuffle。在这些位置前后,feature map的空间尺寸会变,HOG mask虽然是从原图分辨率算出来的,但经过插值后可以对齐到任意尺寸。问题在于,如果原网络使用了相对位置编码,HOG mask的绝对坐标信息可能会和相对位置编码相互干扰。我建议在插入之前,先测试一下单独使用HOG mask和去掉位置编码两种情况下的性能差异。如果去掉位置编码后性能没有明显下降,说明HOG mask已经能提供足够的空间引导。
此外,改造时不要忘了HOG mask的计算输入应该使用低分辨率原图。有人可能会想,既然注意力吃的是中间feature map,那HOG也从中间feature map提取是不是更对齐?实际做下来不是这样。中间feature map的通道分布和数值尺度跟原始图像差别很大,Sobel算子提取出来的梯度直接变成噪声,HOG分支很难学到稳定语义。从低分辨率原图提取,经过short path直接把边缘位置信息送进去,信号更干净。
3.2 实现步骤与关键超参配置
下面给出一个最小可复现的配置,不推翻完整源码,只列出关键部分。第一步,安装依赖:PyTorch 2.0及以上,基础库不多,最好有一个现成的Transformer超分训练框架做参考。第二步,定义HOG先验提取模块,写成nn.Module,forward里固定Sobel参数,后面接两个Conv+ReLU。第三步,在MSA里通过一个开关变量控制是否启用DHOGSA。
为了更直观,我用pseudo code描述HOG分支的forward过程:
class HOGPrior(nn.Module): def __init__(self): super().__init__() # 固定Sobel卷积,不需要梯度 self.sobel = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, padding=1, bias=False) self.sobel.weight.requires_grad = False # 可学习压缩层 self.conv1 = nn.Conv2d(in_channels=2, out_channels=8, kernel_size=1) self.conv2 = nn.Conv2d(in_channels=8, out_channels=1, kernel_size=3, padding=1) def forward(self, x): grad_x = self.sobel(x)[:, 0:1, :, :] grad_y = self.sobel(x)[:, 1:2, :, :] mag = torch.sqrt(grad_x ** 2 + grad_y ** 2 + 1e-8) cos = grad_x / (mag + 1e-8) sin = grad_y / (mag + 1e-8) feat = torch.cat([mag, cos, sin], dim=1) mask = self.conv2(torch.relu(self.conv1(feat))) return mask注意上面代码里为了保持可导,角度信息用cos/sin来表示。这个做法比直接取atan2再接整数bin稳定多了。后续如果想扩展成9个bin的直方图,也可以在mag、cos、sin后面再接一层可学习映射,不过没必要自己实现一个严格HOG,因为网络能自己学会加权。
关键超参数推荐值可以参考这个表格:
| 参数名 | 推荐值 | 作用 |
|---|---|---|
| cell_size | 4x4像素 | HOG直方图区域大小,太大丢失细边缘 |
| nbins | 9 | 梯度方向分桶数量,影响方向分辨率 |
| mask_lam | 0.1初始化 | 控制先验偏置强度,后续可调 |
| mask_conv_channels | [8, 1] | 压缩HOG特征到单通道mask |
| loss | L1 + 0.05*edge_loss | 均衡像素与边缘约束 |
这里的mask_lam建议初始值设小一些。我在训练里用过一个比较激进的方案,刚插入时mask_lam设到1.0,模型直接不收敛,loss曲线像个锯齿。后来把初始值降到0.05左右,再通过线性warmup逐步提到0.2,训练曲线才稳定下来。这个系数本质上决定了先验和自由特征之间的“话语权”,先给数据驱动一个适应期,再渐进加入先验,是这类耦合模块比较稳妥的训练策略。和warmup学习率类似,mask的权重也需要一个warmup过程,只是很多论文不会在正文里写这个细节。
训练顺序上,我建议先用基础Transformer在目标数据集上跑一个基线模型,保存checkpoint。然后插入DHOGSA分支,冻结原模型参数一到两个epoch,只优化新分支,让mask先学会“看”边缘;之后再解冻全部参数,用更小的学习率(比如基线的十分之一)做联合微调。这样比直接从头训练快很多,而且不容易把已经在特征提取里学好的低频信息弄坏。
3.3 推理与部署侧的处理
推理阶段不需要额外的HOG真值,因为HOG是从输入的低分辨率图像上实时算出来的。这也是DHOGSA作为即插即用模块的重要优点——没有引入额外监督或图割等复杂度,部署时只需要把HOG分支和图一起喂进去就行。我们要注意,HOG分支的输入应该是低分辨率图本身,不是中间feature map。如果从中间feature map提取HOG,会因为通道维度大、数值分布漂移,训练很快过拟合。而从输入图算edge先验,相当于固定一个几何先验,模型学起来更直接。
如果要部署到端侧,HOG分支只有Sobel卷积和两层卷积,计算量相比大Transformer块可以忽略不计。但要注意,把Sobel卷积和普通卷积混合后,PyTorch转ONNX时最好把固定卷积核处理成普通卷积的可训练参数,否则某些推理引擎对常量卷积的融合会出问题。我踩过的一个坑是:ONNX报错说“opset不支持取模”,细查下来是HOG角度计算里用了atan2之后取整到bin,这个算子在某些opset里缺失。最后把角度量化改成连续方向输出,才顺利导出。
假如你要在TensorRT或者更高性能的推理框架上部署,建议把整个DHOGSA分支拆成两个部分:Sobel核预先写成常数,不参与动态计算;后面的两个卷积层融合到前一个卷积层里。这样推理时几乎不增加额外延迟。
4. 复现结果与问题排查实录
这一节分享我实际重新实现DHOGSA时的数据和几个典型翻车案例。如果你是第一次接触这种先验引导注意力,可能正好会遇到和我一样的问题。
4.1 复现参数下的指标变化
我用一个自建的公开图像子集做了对比实验,规模不大,但足够说明趋势。基于同一个Transformer基座,分别记录三组结果:基线模型、插入DHOGSA但只微调分支、插入DHOGSA后微调全部网络。测试时算PSNR和SSIM,参考值和提升都用相对数字表示,不使用具体数据集名。
| 实验设置 | PSNR(相对基线) | SSIM(相对基线) | 边缘保持指标 |
|---|---|---|---|
| 基线Transformer | 0.00 dB | 0.0000 | 基线 |
| 插入DHOGSA,冻结主干 | +0.12 dB | +0.0012 | 略好 |
| 插入DHOGSA,全模型微调 | +0.31 dB | +0.0031 | 明显更好 |
注意这些数字是自建小数据集的相对值,不是通用benchmark,更关键的是趋势:冻结主干时效果有限,全模型微调后能拿到显著增益。为什么?因为HOG mask作为一个外部bias,如果主干特征没有和它形成闭环,它只能给V矩阵提供一个固定位置的调制,潜力释放不完全。解冻之后,注意力的Q/K/V会学着把高频信息“抄近道”送到边缘位置,效果自然更好。这说明DHOGSA的收益是“双向适配”的结果,不是简单加一个mask就能得到的。
放大输出图像看细节,最明显的变化是斜向边缘和细线条的边缘振铃减少了,纹理区域更干净。主观上,肉眼可感知的改变集中在低分辨率图本来就结构稀疏、但放大后很容易糊掉的区域。对于平坦区域,HOG先验的mask值接近0,不会干扰原有流程,所以不会带来副作用。这是我认为DHOGSA比较优雅的地方——先验只在需要的地方起作用。
另外,我还做了消融实验:去掉HOG mask、只保留Sobel梯度图,或者把HOG替换成Canny二值图。结果发现Canny二值图作为mask会让注意力变得过于稀疏,很多边缘附近没有过渡区域,训练时loss波动明显。Sobel梯度图比Canny好一点,但丢失了方向信息,对斜边重建帮助有限。HOG-like的连续方向特征是最稳定的选择。
4.2 问题一:HOG mask数值尺度失控
问题表现:插入DHOGSA后,loss出现周期性尖峰,逐步训练也没恢复正常。原因很可能是mask输出层没有加激活函数约束,数值范围漂移到几十或上百,直接把softmax推到饱和区。解决办法有两个:一是在mask输出层后面加一个tanh激活或sigmoid,把值域限定在[-1,1]或[0,1];二是像前面说的,对mask做减均值和缩放,让整体均值为零、标准差为0.1。这两种方法我都试过,第二种在保持细节纹理上更友好。
一个更隐蔽的问题:mask经过归一化之后,如果某些batch的HOG统计分布稍有偏移,归一化参数也会跟着变,导致训练不稳定。因此,我建议对mask做类似BatchNorm的统计量,但不要使用跨batch的running mean,还是直接在每个样本内部做标准化,因为HOG统计和光照有关,跨样本的running mean容易引入偏差。
4.3 问题二:mask边缘位置和注意力位置对不上
这个问题主要出现在输入图像尺寸不是patch大小整数倍的时候。超分网络一般会对输入做padding或crop,如果mask是在原尺寸上提取的,再插值和reshape,顺序就乱了。排查方法很简单:打印一个极端输入,比如一张白底黑色小方块的图,观察mask热力图高亮位置和小方块位置是否重合。不重合就检查所有reshape和transpose操作。建议在HOG分支和MSA内部都用同一个坐标函数来生成index,避免手写行优先展开。
还有一个相关坑:多尺度特征的Transformer模型里,不同stage的feature map尺寸不同,mask插值时不要用简单的最近邻插值,最好用双线性,因为边缘位置是亚像素连续的,最近邻会带来锯齿状的mask边界,进而干扰注意力。另外,如果原网络用了padded窗口注意力,mask也需要在窗口padding区域补一个默认值,通常是0,否则mask统计里会集体偏向边缘区域。
4.4 问题三:训练后期PSNR反而下降
全模型微调早期PSNR上升很快,但训练步数多了之后,开始过拟合训练集。特别是当数据集不大时,HOG先验分支会让模型更快记住训练图的高频结构,反而伤到泛化。对策是把mask_lam衰减回一个较小值,或者加一个早停策略,在验证集PSNR峰值时保存模型。另一个有效手段是训练时做随机crop的同时,也随机旋转和翻转HOG mask计算过程中的梯度方向,简而言之就是让HOG先验分支看到更多方向变化,降低方向偏好。
我甚至试过在后期把HOG分支直接冻结,相当于把先验的位置信息固定住,只有主干继续微调。这样操作的结果是,模型能在保留HOG引导的同时,避免过拟合。训练时我们可以给HOG分支单独设置一个较低的学习率,比如主干学习率的十分之一,这样既让它保持轻量,又不会在后段偏离原来学好的梯度和方向映射。
4.5 问题四:多头注意力中只有一部分头被有效引导
有时会碰到这样的情况:插入DHOGSA后,很多attention head的可视化热力图几乎完全一样,原本希望的多头多样性消失了。原因是HOG mask加的偏置对所有head都是同一个值,如果这个偏置数值偏大,就会压制head之间的差异。解决办法是给每个head单独学习一个权重λ_h,不强求所有head都受HOG引导。简单实现就是在MSA里为每个head加一个可学习的gate,初始值设为1.0。这样允许某些head专注全局上下文,另一些head专注边缘,整体模型容量更充足。
5. 扩展思路与个人建议
DHOGSA不只是一个超分模块,它的底层设计思想——“把外部先验转成注意力偏置”——可以迁移到很多视觉任务里。这里聊聊几种可行的扩展,以及我在实际应用里得到的一些体会。
5.1 更换先验:从HOG到方向场或结构张量
HOG不是唯一选择,但它的思想可以迁移。比如换成结构张量(structure tensor),在每个像素附近估计主方向和各项异性强度,可能比直方图更适合连续型重建任务。另一个方向是用可学习的边缘检测器替代固定Sobel核,让模型自己决定什么样的梯度对超分最重要。这么做的代价是引入额外的参数和训练不确定性,但换来更高灵活性。如果后续有人把DHOGSA的方向bin改成随输入动态调整,应该能进一步涨点。
我在一个去模糊任务上试过把HOG换成结构张量,因为去模糊过程中涉及的方向比超分更复杂,结构张量的方向估计更平滑。结果确实比原版HOG略好,代价是多了特征值分解的计算。如果算力允许,这是个不错的改进方向。
5.2 与边缘损失、梯度裁剪之间的关系
我经常看到有人把DHOGSA和边缘监督损失混为一谈。二者区别在于:边缘损失是在loss上施加约束,属于隐式引导;DHOGSA则是在网络内部,通过注意力偏置强行改变信息的流向,属于结构级引导。两者可以叠加使用,但叠加时要小心梯度重叠——如果边缘损失已经在放大特征,HOG mask又在logits上加强同位置,训练会不稳。实测下来,叠加时把边缘损失权重减半,效果更稳。
具体操作中,可以把边缘损失的权重从0.1降到0.05,或者把DHOGSA的mask_lam降低一些。我之前试过同时以0.1和1.0初始化,训练到第10个epoch就出现NaN loss,降到一个0.05、另一个0.5之后才稳定下来。所以,先验引导和损失约束并不是越多越好,关键是错开作用的位置。
5.3 个人心得
我在实际使用中发现,DHOGSA价值最大不是在训练阶段,反而是微调阶段。对于已经上线或者已经在跑基线模型的团队,不推倒模型、只插一个轻量分支再微调,是性价比很高的提分路径。它不像换骨干网络那样大动干戈,也没有GAN那样难调。如果你正在做图像超分、去噪或者去模糊,想让模型重建重点往边缘倾斜,建议先在小模型上验证一套mask初始值和学习率规划,再决定大规模训练策略。
踩过几次坑之后,我觉得这类“先验引导注意力”的模块,最考验的不是数学公式,而是工程上那些对不齐、调不匀的细节。比如mask和token的对齐、HOG方向量化时的梯度断裂、不同head之间的权重均衡,每一处都会直接影响最终PSNR和训练稳定性。先把这些细节处理好,PSNR自然就上去了。如果未来要继续扩展,我建议把mask从单层扩展到多尺度,在浅层和深层Transformer里分别插入不同分辨率的HOG先验,让边缘引导信息贯穿整个网络。这应该是DHOGSA这条线里下一个能继续提升的落点。