ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

卷积网络不认旋转?一文讲透旋转等变性原理与落地实践

卷积网络不认旋转?一文讲透旋转等变性原理与落地实践 如果你做过任何带方向变化的视觉任务大概率见过这样一个反直觉现象模型对原图识别得很好把图旋转 90 度后准确率就开始下降。解决这个问题的一个常用手段是旋转数据增强但很多人没有意识到更强的思路是在模型架构中显式加入旋转等变性Rotational Equivariance让网络从底层就“知道”旋转是同一物体的一种合法变化。这篇文章想把这个概念完整讲透。我会从“等变”和“不变”的差异讲起再解释为什么普通卷积不能天然处理旋转然后落到实现、验证和选型层面。如果你是第一次接触旋转等变建议按顺序读如果你已经在调研落地可以直接跳到第 4 章和第 5 章看验证思路和踩坑清单。1. 先分清旋转不变性、旋转等变性与“输入怎么变、输出怎么变”很多人一听到旋转等变性会下意识把它和“旋转不变性”混在一起。它们有关联但描述的其实不是同一件事。1.1 不变性解决的是类别变不变等变性解决的是空间位置怎么跟着变不变性指的是输入旋转后模型的输出保持不变。比如一张猫的图片不管猫头朝左还是朝右分类结果都应该是“猫”。这里的“输出”是抽象标签它没有空间坐标自然也不会跟着图像一起旋转。等变性则更严格输入旋转后如果输出仍然是一张图、一组点、一个边界框或一个像素级掩码那么这个输出应当和原输出保持同样的几何变换关系。换句话说输入转了 90 度输出也要相应地转 90 度。用数学语言写出来是[ F(Rx)RF(x) ]其中 (R) 代表旋转操作(x) 是输入(F) 是网络模型。如果把输入先旋转、再过模型和先把输入过模型、再把输出旋转两者应该得到一样的结果。如果不要求 (R) 作用在 (F(x)) 上而是要求 (F(Rx)F(x))那么你讨论的就是不变性。很多人犯的一个错误是只在分类任务里讨论等变。分类任务的最终输出是类别类别不随旋转变化所以它真正需要的往往是不变性。但当任务涉及坐标、方向、姿态、分割掩码、点云位移时等变性才有真正的出场空间。1.2 不变性常常靠“对等变特征做聚合”来实现理解等变化之后会发现它其实比不变性更基础。以平移为例。卷积神经网络天然具有平移等变能力。猫在图像左上角和右下角卷积层输出的特征图也会相应移动。分类头想输出“猫”这个类别怎么把不同位置的特征变成同一类常见做法是全局平均池化把所有空间位置的特征压成一个向量再送进全连接层。这一步本质上是把“空间信息”聚合掉从而得到对平移近似不变的全局表示。旋转也同理。如果一个网络的特征图确实做到了旋转等变那它在旋转维度上仍然携带方向信息。想让最终分类对旋转不变可以在旋转方向维度上做池化想让网络保留方向信息用于后续位姿估计就不要在那个维度上过早池化。所以不要问“旋转等变和旋转不变哪个好”。应该先问你的输出到底是“没有坐标的标签”还是“有坐标、有方向的几何量”前者最终需要不变性后者往往需要等变性。不变性可以在等变模型之后通过池化得到但反过来就很难。2. 为什么普通卷积只对平移等变对方向却需要“死记硬背”标准卷积几乎是所有现代视觉模型的基石。它有一个很容易被视而不见的特点它只对“平移”拥有天然的等变性对“旋转”没有任何系统性保证。2.1 滑窗和共享权重造就了平移等变卷积的平移等变性来自于两个设计的叠加一是共享权重同一个卷积核会在输入的所有位置反复使用二是滑动窗口输入平移后卷积核也会在对应位置扫到同样的局部模式。因此一个边缘出现在图像左上角还是右下角卷积层的响应强度并不会因此改变只是响应位置跟着挪动。这个性质让卷积网络在自然图像上表现远好于没有空间结构的全连接网络因为它已经内置了一个非常强的先验目标在图像上的绝对位置不应该改变它的局部外观。但共享权重只在空间的“位置”维度上共享并没有在“方向”维度上共享。一个能够检测水平边缘的卷积核对竖直方向的边缘响应通常不会好。要让普通 CNN 也识别旋转后的物体模型只能靠数据增强去“看”更多角度的样本或者靠中间层堆出一组方向不同的检测器。2.2 数据增强能缓解问题但无法改变权值共享逻辑旋转数据增强确实有效。把每张训练图转 0 度、90 度、180 度、270 度再一起训练模型便能学到常见角度下的模式。严格来说这也是一种把先验注入模型的方式只是注入位置在“训练样本分布”而不是“网络结构”。问题是这种方法有三个明显边界。第一样本效率低。同一模式的每个角度都被当作不同样本去学习模型不得不用更多参数去记住不同方向的重复特征。第二离散增强难以覆盖连续旋转。即使你转 90 度模型对 45 度、15 度这类未见角度的泛化仍然缺乏结构性保障。第三增强可能会和真实任务的方向语义冲突。如果某些类别本身的方向敏感比如 OCR 中的字符方向就很重要盲目旋转增强反而会制造错误标签。所以数据增强是一个值得保留的工具但把它当作处理旋转问题的唯一手段实际上是用“更多数据”掩盖“架构缺少几何先验”的问题。2.3 从普通卷积到群卷积多出来的不只是“四个旋转后的核”要真正在架构层面解决方向问题一个自然想法是既然卷积在平移群里共享权重那能不能把“共享权重”的范围扩展到旋转群这就是群卷积Group Convolution / G-CNN的出发点。以离散 90 度旋转为例模型不再只用一个卷积核去滑窗而是把同一个卷积核旋转 4 次在输入上分别做卷积得到 4 组 feature map每一组对应一个方向。但后面层不能再把这些 feature map 简单地当成普通通道处理。因为如果第一层有 4 个方向响应第二层必须知道这 4 个方向之间的次序并且要对“方向”也做卷积。简单说网络的特征图空间从二维平面扩展成了“平移 旋转”的群空间。旋转操作在特征图上不再只是旋转一下像素坐标还会同时移动方向通道的次序。这也是为什么许多人第一次阅读旋转等变代码时感到困难你不能再把特征图当作一张普通图片来理解。它多了“方向”这个坐标轴有点像三维数据只是其中两个轴仍然是 (H) 和 (W)另一个轴则记录着旋转状态。3. 旋转等变架构的三种路线离散旋转、连续转向与特征场旋转等变不是一个单一网络而是一族遵循同一设计原则的方法。不同实现差别很大主要取决于你如何处理“连续旋转”这个连续群。3.1 离散旋转最容易理解的开局路线最直观的实现是只考虑有限个离散旋转比如 90 度、180 度、270 度或者把范围扩大到 45 度间隔。由于只旋转有限次特征图可以直接把不同方向结果堆叠起来实现难度相对较低。这类方法通常和群论中的循环群有关。二维平面内四个旋转方向可以构成一个群通常记为 (C_4) 或 (p4)如果再加上翻转会有八个对称操作对应 (D_4) 或 (p4m)。很多开源实现里的p4、p4m指的就是这种离散对称群。离散旋转最大的好处是计算过程和反向传播都比较直接。最大的限制也很明确如果任务要求模型对任意角度都保持等变只靠 90 度离散旋转并不够因为模型对 30 度、47 度这种角度的行为没有被显式约束。从实操上我通常建议新手先从离散旋转开始。因为它足以展示等变的直觉也足够用来验证“到底是不是架构问题”代价又不至于失控。想直接处理任意连续角度实现复杂度会明显上升。3.2 连续旋转从离散枚举变成可转向滤波器对于连续旋转人们不希望在内存里枚举所有角度。一个替代思路是使用可转向滤波器steerable filter把滤波器写成一组基函数的线性组合当输入旋转时这些基函数按照已知方式变换组合系数也相应变化从而保证输出特征是等变的。这种设计经常与傅里叶特征、球谐函数联系到一起。在二维图像里用复数指数或傅里叶基来参数化卷积核在三维点云或分子结构里则常涉及球谐函数和 SO(3) 群表示。与离散旋转不同连续旋转方法不需要枚举所有方向但需要你对群表示理论有较多理解。实际工程中连续旋转方法的代码会比离散版本难得多。你需要面对通道类型、不可约表示、特征场的旋转规则等概念。如果只是做普通图像识别实验这些概念会让学习成本陡增如果是处理分子、点云这类天然连续旋转的数据那么投入是值得的。3.3 中间层特征并不是“把输入图片转一下”无论是离散旋转还是连续旋转都要注意一个问题层数越深特征图越抽象旋转作用在特征上的方式也越复杂。第一层如果直接旋转输入图像并做卷积你还能想象出特征图是“几种角度的边缘”。但到了后面特征图通常被分成了多个组每一组在旋转下会按照不同的方式进行变换。有的组可能像标量一样保持数值不变有的组会像向量一样在坐标轴间交换分量有的组则会经历更复杂的通道混合。所以如果你是自己实现了群卷积或可转向卷积千万不要天真地在中间特征图上直接调用类似cv2.rotate的函数。你应该回到特征字段的定义搞清楚这个阶段“旋转”到底意味着哪些索引被移动、哪些通道被交换。如果你只是想在某个开源库之上做实验建议不要随便改动中间特征图的通道顺序。4. 动手验证用等变误差而不是肉眼判断模型是否真的等变引入旋转等变之后一个非常典型的困惑是我怎么知道这个模型做得对不对很多人会拿一两张图出来看觉得结果差不多。但“看起来差不多”不够最好量化成数字。4.1 等变误差的基本计算方式假设你有一个模型输入是二维图像输出也是一张与输入共享同一坐标系统的图例如语义分割结果。你可以用下面这段思路计算离散 90 度旋转下的等变误差import torch def rot90_image(x): # x: (B, C, H, W) return torch.rot90(x, 1, dims(-2, -1)) def equivariance_error(model, x): model.eval() with torch.no_grad(): out_before model(x) # 模型输出也应被同一旋转作用 expected rot90_image(out_before) actual model(rot90_image(x)) diff torch.abs(expected - actual) denom torch.mean(torch.abs(expected)) 1e-6 return torch.mean(diff).item() / denom这段代码的关键假设是模型输出仍然有二维空间布局并且rot90_image对输入和输出使用同一套旋转规则。如果你的模型最后接了全局平均池化输出变成了向量那么上面这段代码就没有意义因为你不知道“对向量做旋转”是什么。此时你更适合去检验不变性而不是等变性。def invariance_error(model, x): model.eval() with torch.no_grad(): out1 model(x) out2 model(rot90_image(x)) diff torch.abs(out1 - out2) denom torch.mean(torch.abs(out1)) 1e-6 return torch.mean(diff).item() / denom如果你连向量输出也想和某些位置信息关联那就必须在模型内部定义一个“旋转对角向量坐标”规则。但工程上更常见的做法是去掉分类头从中间层抽特征来计算等变误差因为分类头本来就会破坏空间结构。不要用眼睛判断等变。先用脚本把误差打出来误差的量级会告诉你结构是否真的满足设计预期。4.2 误差很大时按这套顺序排查如果等变误差高得不像话先不要怀疑旋转等变理论先怀疑你写的那几个旋转函数。第一个要检查的是坐标中心。旋转一张图片时你是绕图像中心转还是绕左上角转模型里特征图的旋转如果又绕了中心转两个旋转中心不一致误差立刻就会爆掉。第二个要检查的是边界。图像是方形区域旋转后四个角会出现空白你用零填充、边缘填充还是循环填充都会影响输出。严格来说方形图像经过旋转后的定义域已经不是原来的方形区域边界处的等变天然不成立。做验证时可以先把输入裁到中心区域或使用循环填充再看核心区域误差。第三个检查点是池化和下采样。全局平均池化会把空间结构聚合成向量严格等变很难保住。Max pooling 在边界处也有方向差异因为最大值的索引会随旋转而变但你无法保证池化窗口里总是恰好有同一个局部区域。如果模型里有 stride 卷积也要确认旋转后坐标是否和采样位置对齐。第四个隐含问题是 BatchNorm。BatchNorm 统计的是一个 batch 的特征分布如果这个 batch 里各张图片的方向分布不一致统计量就不会完全等变。最直接的缓解办法是使用 GroupNorm或者把同一个输入的多个旋转版本放进同一个 batch 去计算。你可能会发现模型在训练模式下误差较大但在 eval 模式下小一些这并不罕见。第五个误差来源是插值。很多图像旋转函数使用双线性插值插值结果并不是严格的群作用。90 度旋转用torch.rot90做像素转置没有插值误差因此适合做原则性验证如果使用任意角度旋转则会引入一定数值误差但通常不会造成系统性破坏。4.3 从哪里开始上手比较划算对于没有接触过旋转等变的读者我建议不要一上来就写底层 kernel。可以先到开源库里找现成实现。PyTorch 生态中有一些专门支持群卷积和旋转等变的库例如 e2cnn 就是比较知名的一个此外还有一些更偏研究性的 SO(3) 等变工具。运行时库的版本、Python 版本和 PyTorch 兼容性会变化动手前先读 README运行它附带的示例再替换成自己的数据。一个比较稳妥的路线是先用 MNIST 或一个小型旋转验证集跑通确认等变误差达到理论预期。把普通 CNN 作为 baseline加上旋转增强记录任务指标。再换上等变网络同样记录任务指标。对比参数数量、显存、训练时间和最终指标。这一步的关键是把“结构问题”和“实现问题”分开。如果等变误差很大先调代码如果等变误差合理但任务指标没有提升那个问题可能出在任务定义或者模型容量上。5. 什么场景真正需要旋转等变一套选型框架和踩坑清单旋转等变听起来理论优雅但它不是万能的。在决定引入之前我建议先回答几个问题这几个问题能避免你花大量算力做一个不该做的结构改动。5.1 五个问题决定要不要用旋转等变第一个问题你的任务里输出到底应该跟着输入旋转还是保持固定如果输出是分类标签你最终需要旋转不变性如果输出是分割图、关键点热图、姿态参数这类几何量等变性才有明确价值。对于分类任务加旋转池化和数据增强也能做到近似不变不一定要上复杂结构。第二个问题你面对的旋转是离散的还是连续的工业场景里工件可能每隔 90 度出现一次用离散群就够卫星图像或机器人导航里的航向角可能连续变化离散群只是近似。连续旋转场景需要更复杂的可转向实现。第三个问题当前模型的问题是真的来自旋转还是样本不足、标注噪声、光照变化、遮挡等其它因素很多人把任务困难的账全记在“旋转”头上但旋转等变只能解决旋转这一部分不能包治百病。第四个问题你能接受多少额外计算开销群卷积通常会引入多个方向副本显存和计算量会显著增加。你可以只在网络前几层使用旋转等变后几层回到普通卷积也可以从很小的群规模开始测试。第五个问题你的数据标签是否真的与方向无关这一点最容易踩坑。5.2 不适合旋转等变的场景要格外小心旋转等变的本质是给网络注入“旋转后仍是同一个系统”的先验。但有些任务里旋转后的语义会发生变化。例如文字识别中的方向问题。字符“6”旋转 180 度后在外观上可能接近另一个字符“9”。如果训练标签要求模型区分它们那么强制让中间特征严格旋转等变并不自然。并不是说完全不能用而是说必须在网络末端或目标定义上额外处理“方向敏感的语义”。类似地某些纹理方向、箭头符号、具有方向性的结构旋转后并不总是属于同一个语义类别。另一个常见误区是镜像。p4m 这类包含翻转操作的群会同时把镜像对称也绑进模型。如果你的任务里物体翻转后不合法比如镜像后的文字或左右不对称的零件就不要随意使用含翻转的对称群。所以不要只看网络结构先看数据分布里的“标签在不同旋转方向之间是否保持一致”。如果答案是否定的旋转等变更像是在和一个合理的方向先验作斗争。5.3 从零到一的落地路线最后给一条保守、可执行的落地路线。第一步用 50 到 200 张样本构造一个只有旋转变化的小型验证集确认人类标注在旋转后保持一致。第二步用普通 CNN 配合旋转增强跑一个 baseline记录“旋转测试集”上的准确率。第三步选择一个离散群规模先用开源库跑通一个最小模型测量等变误差。第四步观察误差是否随层数加深而增大如果是回到第 4 章的排查流程。第五步在任务误差和等变误差之间寻找平衡。一个真正好用的模型不需要等变误差绝对为零只要误差不影响最终任务即可。我见过的很多失败案例并不是因为等变理论有问题而是因为项目还没确认标签方向语义就直接上了复杂结构。相反先在数据集层面搞清楚“你期待旋转后发生什么”再谈架构通常能省下大量时间。所以下一次你再遇到“把图转个方向就认不出”的模型先别急着翻倍扩大数据。你先问自己一个问题你究竟是想让模型把旋转后的东西看成同一个东西还是希望它能在输出中准确表达出这个旋转本身这个问题的答案决定了你是需要旋转不变性、旋转等变性还是干脆只需要调一下数据增强策略。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进