尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

卷积神经网络通道数变化全解析:从原理到实战

卷积神经网络通道数变化全解析:从原理到实战 很多人学卷积神经网络学到卷积层的时候第一个卡住的问题往往不是卷积怎么滑而是通道数到底怎么变。看经典的卷积神经网络结构图输入是一张3通道的RGB图片经过一个卷积层就变成64通道再经过几层又变成128、256到了最后全连接层之前甚至能到512、1024。这些数字是谁决定的有的层通道数翻倍有的层又突然压到32这里面的规则到底是什么代码里的in_channels和out_channels又该怎么填这篇文章就把通道数变化这件事彻底讲明白。这篇文章默认你已经有最基础的卷积概念哪怕只会调用现成接口也没关系。读完以后你应该能自己推导任何一层卷积前后的通道数也能快速定位“通道不匹配”这类报错。我会从标准卷积入手逐步展开到1x1卷积、转置卷积、深度可分离卷积、3D卷积、门控卷积和自适应图卷积最后再给出一套实际排查问题的方法。全程都会给代码示例和shape推导方便你直接抄作业。1. 通道数变化的本质先搞懂“卷积核个数”这一个变量1.1 通道到底是什么图像通道这个概念很好理解一张RGB图有3个通道每个通道是 H×W 的二维矩阵分别代表红、绿、蓝三个颜色分量的亮度。进入卷积网络之后特征图依然是“多张二维矩阵叠在一起”的结构只是这些通道不再对应颜色而是对应网络自己学习出来的各种特征。比如某个通道可能对水平边缘响应大另一个通道可能对纹理敏感还有的通道专门负责颜色分布。你可以把通道理解成一摞地图每张地图标注不同信息一张标河流一张标道路一张标等高线。卷积网络越深通道数越多可以标注的“信息类型”就越丰富。这也就是为什么很多卷积神经网络模型越深通道数越往上加——carrying更多特征类型才有足够容量去表达复杂的语义。在PyTorch里一个特征图的张量shape通常写作[N, C, H, W]其中N是batch sizeC就是通道数。在TensorFlow旧版里常用[N, H, W, C]。如果你在阅读网络结构图时看到某个卷积层上方写“64”或“128”那个数字就是该层输出特征图的通道数。1.2 核心规则输出通道数 这一层卷积核的个数很多初学者会去背各种公式其实通道数变化的核心就一句话输出通道数由这一层定义了多个卷积核决定。举个例子输入是[1, 3, 32, 32]也就是一张3通道、高和宽都是32的特征图。现在这层卷积定义out_channels8、kernel_size3。在PyTorch里就是import torch.nn as nn conv nn.Conv2d(in_channels3, out_channels8, kernel_size3)这里的out_channels8意味着这一层有8个卷积核。每个卷积核的shape是[3, 3, 3]3个输入通道每个通道一个3×3的空间窗口。计算时一个卷积核会先把输入3个通道对应位置的3×3窗口全部做乘加运算再跨通道累加最后得到一个单通道的二维响应图。8个卷积核各算各的拼在一起就是8个输出通道。所以输出通道数 卷积核个数这一点在所有卷积相关操作里都成立。你别管卷积核内部结构有多复杂只要数出有多少个卷积核输出通道数就出来了。1.3 权重shape是很多报错的根源实际写代码时很多人会卡在卷积核权重shape上。其实只要理解了“输出通道数 卷积核个数”权重的维度顺序就很好记。PyTorch里nn.Conv2d的权重shape是[out_channels, in_channels, kH, kW]。上面的例子中print(conv.weight.shape) # torch.Size([8, 3, 3, 3])第一个数8就是输出通道数第二个数3就是输入通道数后面两个3是卷积核空间尺寸。TensorFlow/Keras里权重顺序是[kH, kW, in_channels, out_channels]本质含义一样只是排列位置不同。你在把模型从一个框架搬到另一个框架时最常踩的坑就是通道维度顺序搞反后面我们会专门讲这个。2. 从标准卷积到实用模块通道数变化的具体计算2.1 输出尺寸与通道数计算公式卷积层会同时改变特征图的空间尺寸和通道数但这两条线是独立的。通道数只看卷积核个数空间尺寸则由卷积核大小、步长、填充共同决定。输出特征图尺寸公式是H_out (H 2P - K) / S 1 W_out (W 2P - K) / S 1其中H、W是输入高和宽P是paddingK是卷积核尺寸S是步长。如果除不尽PyTorch默认向下取整。这个公式很重要但注意它和通道数没有任何关系。参数含义是否影响通道数out_channels卷积核个数是直接决定输出通道数kernel_size卷积核空间尺寸否只影响空间尺寸stride步长否只影响空间尺寸padding填充否只影响空间尺寸dilation空洞系数否只影响感受野和空间尺寸groups分组数是会影响卷积方式后面单独说实际操作中如果只是想知道某一层输入输出shape最朴素的办法是打印每层输出。但更稳的做法是先在纸上把公式推一遍。我自己复现论文时习惯先把每一层的input shape - output shape写成一个表格再动手写代码这样出错的概率会小很多。2.2 1x1卷积通道数调节器1x1卷积可能是改变通道数最灵活的工具在很多经典结构里都能看到它的身影。所谓1x1卷积就是卷积核空间尺寸是1×1但深度仍然等于输入通道数。它的计算方式是对特征图每个位置上的所有通道做一次加权求和生成一个新的通道值。举个例子输入是[N, 256, 14, 14]经过nn.Conv2d(256, 32, kernel_size1)之后输出是[N, 32, 14, 14]。空间尺寸完全没变通道数从256压到了32。反过来说如果用nn.Conv2d(32, 128, kernel_size1)通道数也能从32升到128。1x1卷积本质上等价于一个逐像素位置的全连接层它不做空间上的邻居聚合只做跨通道信息融合。这个特性让它非常适合做通道数的“闸门”想降维就减少卷积核个数想升维就增加卷积核个数而且计算量远小于3×3卷积。在Inception、ResNet、MobileNet里1x1卷积几乎无处不在。ResNet的Bottleneck结构就是先用1x1把256通道降到64再用3×3卷积做空间特征提取最后再用1x1升回256。这样做既保持了通道数表达能力又大幅减少了计算量。2.3 池化层、激活层、BN层通道数按兵不动新手看结构图时还容易犯一个错以为池化层也会改变通道数。实际上最大池化、平均池化只对每个通道独立操作输入是[N, C, H, W]输出是[N, C, H/2, W/2]以stride2为例通道数不变。ReLU、Sigmoid、LayerNorm、BatchNorm这些逐元素或逐通道操作同样不改变通道数。也就是说在一个标准的卷积块里通道数的变化只发生在卷积层以及少数attention模块里的矩阵乘法。全连接层可以看成一种特殊的“全局通道混合”它会把C×H×W展平后再映射到自定义维度这时候你的通道概念就变成“神经元数”了。所以你在追踪通道数时只需要盯住卷积层和全连接层。其余的池化、激活、归一化操作可以放心跳过它们不会让你的通道数“悄悄变掉”。3. 特殊卷积结构里的通道数规律3.1 转置卷积反卷积的通道变化转置卷积常被用在语义分割、超分辨率、生成模型里作用是把特征图的空间尺寸放大。它的名字容易让人误解好像它能把卷积过程完全逆回去实际上它只是一个可学习的上采样层不能恢复原始数据。转置卷积的通道数变化规则和普通卷积一样输出通道数等于这一层定义的卷积核个数。区别主要在于空间尺寸变大。普通卷积的输出尺寸是(H 2P - K)/S 1转置卷积则是H_out (H - 1) * S K - 2P举个例子输入[N, 64, 8, 8]经过nn.ConvTranspose2d(in_channels64, out_channels32, kernel_size4, stride2, padding1)输出就是[N, 32, 16, 16]。通道数从64变成了32空间尺寸从8变成了16。如果你希望上采样时通道数不变甚至增多完全可以把out_channels设成64或128空间尺寸照样变大。在3D卷积自编码器这类结构里encoder部分通常用普通卷积逐步加深通道并缩小空间尺寸decoder部分则用转置卷积逐步恢复空间尺寸并调整通道数。整个过程的通道数变化完全由每一层的out_channels决定并不存在什么“自动对齐”的魔法。3.2 深度可分离卷积通道数变化被拆成两步深度可分离卷积是MobileNet系列的核心它的设计思路是把标准卷积分解成两步深度卷积Depthwise Convolution和逐点卷积Pointwise Convolution。第一步深度卷积每个输入通道单独用一个卷积核去做空间卷积。输入是[N, C, H, W]输出依然[N, C, H, W]通道数不变。这一步相当于只做空间特征提取。第二步逐点卷积其实就是1x1卷积。输入是[N, C, H, W]通过nn.Conv2d(C, C2, kernel_size1)把通道数变成C2。也就是说通道数真正的变化发生在逐点卷积这一层。在PyTorch里实现深度可分离卷积的常见写法是depthwise nn.Conv2d(in_channels64, out_channels64, kernel_size3, padding1, groups64) pointwise nn.Conv2d(in_channels64, out_channels128, kernel_size1)这里groups64表示把输入通道分成64组每组单独做空间卷积这就是深度卷积的精髓。由于第二步是1x1卷积通道数可以任意变化。深度可分离卷积的参数量和计算量明显小于标准卷积所以很多轻量级卷积神经网络模型都采用了这个结构。3.3 3D卷积与3D卷积自编码器的通道变化3D卷积常用在视频、医学影像、点云体素等场景。它和2D卷积的区别是卷积核多了一个“深度”维度权重shape在PyTorch里是[out_channels, in_channels, kD, kH, kW]。输入是[N, C_in, D, H, W]经过一个nn.Conv3d(in_channelsC_in, out_channelsC_out, kernel_size3)输出就是[N, C_out, D, H, W]。通道数依然由卷积核个数决定只是空间尺寸变成了三个维度。你可以把3D卷积想象成用一个小立方体在更大的立方体里滑动每个位置的乘加都要跨输入通道累加。3D卷积自编码器通常用于体积数据重建比如从低分辨率CT图像重建高分辨率图像。encoder部分用3D卷积逐步把通道数加深、空间尺寸缩小decoder部分再用3D转置卷积逐步恢复尺寸、调整通道数。整个通道数变化规律和2D卷积完全一样只是多了一维初学者容易懵的地方在于维度顺序建议先打印一次输出shape再继续往下写。3.4 门控卷积与自适应图卷积怎么处理通道门控卷积Gated Convolution最早在图像修复任务里被广泛使用结构上是一个卷积层分出两个分支一个分支是常规卷积特征另一个分支经过卷积后接sigmoid激活作为“门控权重”两者逐元素相乘。为了让门控与特征逐元素对齐这两条分支的通道数通常保持一致所以门控卷积的输入输出通道数一般相同。标准实现里不会用门控卷积去改变通道数如果需要改变通道就在它前后单独接1x1卷积。自适应图卷积Adaptive Graph Convolution出现在图神经网络和部分点云模型里它的“通道”叫节点特征维度。假设输入特征是[N_nodes, C_in]邻接矩阵是[N_nodes, N_nodes]自适应图卷积通常先根据邻接矩阵聚合邻居特征再通过一个可学习的特征变换矩阵W完成通道变换W的shape是[C_in, C_out]。所以输出就是[N_nodes, C_out]。这里的通道数变化逻辑依然是由“变换矩阵的输出维度”控制和卷积核个数是一回事。4. 实操如何在模型中控制通道数4.1 最常用的升维/降维操作把通道数变大最简单的办法就是增加卷积核个数。比如nn.Conv2d(64, 128, kernel_size3)会把通道数从64变成128。但通道数翻倍意味着特征图的体积翻倍后续卷积的计算量也会明显上升。以标准卷积为例计算量大概是FLOPs ≈ C_out × C_in × K × K × H_out × W_out如果C_in64、C_out128这个乘出来的数字会相当可观。所以升维不能无脑加要结合硬件显存和算力来权衡。把通道数变小最直接的办法是用1x1卷积。nn.Conv2d(256, 32, kernel_size1)可以把256通道压到32计算量小而且几乎不损失空间信息。它也是各种注意力模块里常见的通道压缩手段比如SENet里的squeeze操作本质上就是先用全局池化得到[C, 1, 1]再用1x1卷积把通道压缩再扩展。我的建议是空间特征需要更丰富表达时升维计算量吃紧或要拼接特征时降维。升维的默认选项是3x3卷积降维的默认选项是1x1卷积。4.2 残差结构里的通道数对齐ResNet里有一个特别容易踩坑的地方shortcut连接要求相加的两个特征图shape完全一致。如果主干分支经过卷机后通道数变了shortcut就必须做一次投影才能让两个分支对齐后再相加。import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity self.shortcut(x) out self.conv1(x) out self.bn1(out) out torch.relu(out) out self.conv2(out) out self.bn2(out) out out identity return torch.relu(out)这里的关键就在in_channels ! out_channels时用一个1x1卷积做投影。原因很简单两个分支相加要求张量的每个维度都一致通道数不一致就只能通过投影把它强行拉齐。所谓Bottleneck结构里的“先降维再卷积再升维”本质上也是在做类似的通道数控制只不过把3x3卷积的输入输出通道都压缩到较低维度整体计算量更小非线性表达能力反而更强。4.3 多分支融合时的通道对齐在FPN、Transformer、多尺度特征融合等场景中你经常要把不同来源的特征图拼在一起。拼接用torch.cat要求除了拼接维度以外其他维度完全一致。比如两个特征图分别是[N, 64, 28, 28]和[N, 32, 28, 28]在通道维拼接后就是[N, 96, 28, 28]。空间尺寸必须相同否则要先插值或调整步长。如果是逐元素相加则要求所有维度完全一致这时候就需要先统一通道数。常见做法是先用1x1卷积把多个分支的通道数都变成同一个值再相加。这个“先1x1对齐再融合”的套路在FPN里非常常见也是面试时经常被问到的点。通道数对齐还有一个隐含的好处1x1卷积可学习能在融合前对特征做线性重组合比单纯拼接更灵活。但代价是多了一组参数和一点计算量。5. 常见问题与排查技巧实录5.1 通道不匹配报错怎么定位新手最常见的报错长这样RuntimeError: Given groups1, weight of size [32, 64, 3, 3], expected input[128, 3, 32, 32] to have 64 channels, but got 32 channels instead这个报错的含义是当前卷积层的权重shape是[32, 64, 3, 3]说明这一层期望输入是64通道但实际输入是32通道。也就是说上一层输出通道是32这一层in_channels却写成了64。解决办法很简单要么把上一层的out_channels改成64要么把这一层的in_channels改成32。具体改哪个取决于你的网络设计意图。有一种快速排查法在模型里注册一个hook打印每一层输入输出shape。PyTorch可以这样写def print_shape(module, input, output): print(module.__class__.__name__, input[0].shape, -, output.shape) for name, layer in model.named_children(): layer.register_forward_hook(print_shape)这样前向传播时每一层的变化都能看到通道数从哪里开始对不上就一目了然。我每次复现论文遇到shape报错都是先挂这个hook比肉眼检查代码快得多。5.2 不同框架下通道维度顺序的坑PyTorch默认用NCHW也就是[batch, channels, height, width]所以在PyTorch里卷积层的输入第一维是batch、第二维是通道。TensorFlow早期默认用NHWC也就是[batch, height, width, channels]后面虽然也支持channels_first但很多预训练权重和旧代码还是NHWC。如果你把TensorFlow的模型权重搬到PyTorch里千万不能直接照搬权重的shape必须做维度转置。PyTorch的卷积权重是[out_channels, in_channels, kH, kW]TensorFlow是[kH, kW, in_channels, out_channels]。转换时要用np.transpose(weight, (3, 2, 0, 1))一类的操作把顺序调整过来。这个坑我至少踩过三次每次都是模型前向传播出来的结果完全不对但又不报错。所以当你发现模型加载预训练权重后精度异常低先检查一下通道维度的排列顺序。5.3 显存被通道数拖垮的优化策略通道数越大特征图占用显存越多。尤其在高分辨率输入下一个[N, 256, 128, 128]的特征图可能就占几百MB。如果你发现训练时显存不够不要一上来就调小batch size先看哪些层的通道数偏大。优化手段有几个方向。第一用1x1卷积对高通道特征做降维把瓶颈层的通道数压到合理范围第二用深度可分离卷积替代标准卷积把计算量降下来第三使用混合精度训练让特征图用半精度存储第四减少中间层的通道数后重新设计通道分布而不是只调某一个层。我个人的习惯是先用小通道起步比如第一层32通道起步观察到训练指标确实需要更多容量再按2倍递增。不要一开始就把所有层都设成512、1024那样模型不收敛时你根本分不清是通道数太多还是太少。5.4 配图说明图片说明第一张图展示的是标准卷积层从3通道输入经过多个卷积核得到N通道输出的过程输入特征图左侧是3个通道卷积层内有8个卷积核每个卷积核都会对输入所有通道做加权求和最终输出8个通道的特征图第二张图展示的是1x1卷积在通道数控制中的典型用法输入64通道先降到16通道再升到128通道空间尺寸始终保持不变。建议在实际复现时用NN-SVG或draw.io自己画一张标注了每层输入输出shape的结构图排查问题时会非常直观。关键词卷积通道数卷积神经网络CNN卷积层1x1卷积转置卷积深度可分离卷积3D卷积门控卷积自适应图卷积卷积神经网络结构图
返回列表