背景
像素重排还是步长卷积?这是一个针对下采样任务的问题。目前的超分辨率任务里,尤其在实时渲染的需求下,多数都会用类似Unet的架构,其中下采样是不可避免的。
通过一些不太全面的实验,我个人暂时还是更青睐 Pixel unshuffle。
池化操作以前挺常用的,但是丢弃像素确实会缺失相当多信息。目前比较常用的是卷积步长方法(Strided Conv)和随着Pixel Shuffle一起提出来的Unshuffle。
Strided Conv 通过设置 stride=2 就可以实现 x2 的下采样,在降维的同时也完成了特征提取。
Pixel Unshuffle 则是单纯的像素重排,相比前者会跳过部分像素(或者说只靠窗口视野),它是无损的操作。他的下采样倍率n会使得通道数变为原来的n2倍,特征提取需要后面卷积处理。
顺带一提 Transformer 那块的 Patch Merging 本质好像就是 Pixel Unshuffle + Conv1x1。
我在尝试搭建Unet架构,以及阅读 RDG 超分论文的时候留意到了这点,大家似乎都在用卷积步长而没有用像素重排?直觉来看,有可能是像素重排会保留噪声、破坏了平移不变性。。。
事实上我找到一篇可能相关的论文,叫 Making Convolutional Networks Shift-Invariant Again 。他里面提到,常规下采样中由于忽略奈奎斯特采样定理,会导致网络丧失平移不变性,简单来说大概就是作者认为输入图像产生微小偏移,输出的特征图也应该产生等量的偏移,而不会不可预测变化。Strided Conv其实也在其中,主要是信号中的高频分量叠到低频中。Pixel Unshuffle没被提到,不过他也是朴素的子采样。
作为解决办法,作者认为应该事先做低通滤波,去除高于奈奎斯特频率的高频分量,实验结果来看适度的滤波反而起到了正则化的作用,提高了准确率。作者也提出这存在平移等变性与生成高频内容之间的权衡。不过这里我没做实验,所以我不太能就题目进行讨论。
对比实验
说回我们的讨论,我打算讨论两种方法在重建上的能力,进行针对单张图像的过拟合。
所有实验均使用相同的ResBlock作为网络的主干,相同的 PixelShuffle 作为上采样层,学习率统一为 1e-3,训练 1000 轮次。
optimizer = optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss()

本文的数据均基于单图加噪过拟合得出。单图拟合考量的是模型对信息的“记忆与重建上限”,并未包含大规模数据集带来的泛化性、真实世界退化以及抗噪能力的考量。因此,本文的结论更多是揭示两者在信息保留度上的理论边界,仅供抛砖引玉。
实验一:直接降维
输入图像3通道,直接通过2x下采样变为12通道,再送入深层学习。
- Strided Conv 组:nn.Conv2d(3, 12, kernel_size=3, stride=2, padding=1)
- Unshuffle 组:nn.PixelUnshuffle(2)
- 结果:
- Strided Conv: 32.91 dB (速度 105.22 it/s)
- Pixel Unshuffle: 34.20 dB (速度 161.50 it/s)
Pixel Unshuffle大幅领先很可能是因为步长卷积进行的有损操作,尤其要考虑到这是在头部。
实验二:加入Head采样得到浅层特征,再下采样
在下采样之前加了3x3卷积,通道3->16,2x下采样时16->64。
- 结果:
- Strided Conv: 38.28 dB (7.71 it/s)
- Pixel Unshuffle: 38.78 dB (8.18 it/s)
看来Unshuffle 先天无损的优势仍然能发挥作用?
实验三:四倍超分辨率(2026.5.14)
将输入图裁切为1536x1536,制作384x384的双线性插值LR。
上采样同样先使用插值+卷积,后使用Pixel shuffle。
self.upsample = nn.Sequential(
nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True),
nn.Conv2d(64, 48, kernel_size=3, padding=1)
)
self.up = nn.PixelShuffle(4)
- 结果:
- Strided Conv: 25.74 dB (104.51 it/s)
- Pixel Unshuffle: 25.91 dB (105.33 it/s)
另外还做了两组,插值四倍上采样,Pixel shuffle两倍
self.upsample = nn.Sequential(
nn.Upsample(scale_factor=4, mode='bilinear', align_corners=True),
nn.Conv2d(64, 12, kernel_size=3, padding=1)
)
self.up = nn.PixelShuffle(2)
- 结果:
- Strided Conv: 25.08 dB (80.64 it/s)
- Pixel Unshuffle: 25.43 dB (83.20 it/s)
self.upsample = nn.Sequential(
nn.Upsample(scale_factor=4, mode='bilinear', align_corners=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1)
)
self.tail = nn.Conv2d(64, 12, kernel_size=3, padding=1) # 增加了tail
self.up = nn.PixelShuffle(2)
- 结果:
- Strided Conv: 26.33 dB (47.18 it/s)
- Pixel Unshuffle: 26.08 dB (48.44 it/s)
- 这个变体 StridedConv组出现了反超,我猜测这与网络变深有关,可能是Strided Conv 提供的额外可学习参数让网络在拟合这张特定图像的细节时表现得更好?
如果我们改回两倍插值上采样,四倍Pixel shuffle的话:
self.upsample = nn.Sequential(
nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1)
)
self.tail = nn.Conv2d(64, 48, kernel_size=3, padding=1)
self.up = nn.PixelShuffle(4)
- 结果:
- Strided Conv: 25.89 dB (87.66 it/s)
- Pixel Unshuffle: 26.00 dB (91.50 it/s)
总结
超分是一种病态的重建问题,从这点来看,似乎越能无损获取原始信息,对于深层网络似乎就越友好。我猜测在超分任务中,pixel unshuffle能做到的只是对特征图上的无损,他在质量上的回报,还需要继续用超分的架构做点实验,跑跑div2k。
顺带一提速度一直保持领先,这点倒是必然的,底层上来看是内存重排,跟带宽关系比较大。对shader而言,主要是做地址计算,对纹理坐标偏移,好像也是缓存友好的,毕竟是取连续像素块。
目前来看,单图重建上,对我所搭建的架构是又好又快的,我没理由不去对更加复杂的架构做类似的实验。
Comments NOTHING