ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

卷积神经网络通道详解:从输入通道、卷积核到通道注意力与剪枝

卷积神经网络通道详解:从输入通道、卷积核到通道注意力与剪枝 调一个卷积网络的时候最容易让人愣住的报错大概就是这一句expected input to have 3 channels, but got 1 channels instead。明明数据看着没问题模型也照着论文抄的就因为一张图是灰度而不是RGB整个前向传播直接断掉。更让人困惑的是你去看网上讲卷积神经网络的教程几乎所有人都在讲卷积核怎么滑动、步长怎么设、填充怎么加唯独通道这个词经常被一句输入通道数等于上一层的输出通道数带过好像它是天生的、不需要理解的东西。这篇文章就想把这件事讲透卷积神经网络里的通道channel到底指什么它和图像的RGB三通道是不是一回事卷积核的形状为什么是四维的输出通道数到底是谁决定的以及分组卷积、深度可分离卷积、1x1卷积、通道注意力这一整套技术本质上都在对通道做哪些改造手术。如果你正在自己搭网络、改通道数、调通道注意力模块或者在复现别人代码时被channels参数绕晕这篇应该能省你不少试错时间。我会从最基础的形状推导讲起一路讲到排查报错和通道剪枝尽量把每个为什么都说清楚。1. 同一个通道在卷积神经网络里到底指什么1.1 这个词为什么到处都是先说个有意思的现象你在搜索引擎里输入通道两个字跳出来的结果五花八门。有讲三维建模软件里材质ID通道的有讲音频芯片有多少路通道的有讲交换机控制台的甚至有讲编程语言里用于协程通信的channel原语的。这些通道彼此之间毫无关系唯一的共同点就是英文都叫channel——它本意是水道、频道、路径任何一个可以被独立编号、独立传输或独立处理的平行通路都可以被叫成通道。这么一对比你就会明白为什么卷积神经网络里的通道容易被误解。很多初学者第一次看到输入通道数为3的时候脑子里浮现的是三条并行的水流同时灌进网络或者三张图叠在一起。这个直觉不算错但它只能解释输入层解释不了中间层为什么会有64、128、256甚至2048个通道。所以理解通道这件事得分两层输入层的通道是数据自带的中间层的通道是网络自己造出来的特征维度。1.2 输入通道是数据的属性中间通道是设计的产物拿一张普通的彩色图片来说它有红、绿、蓝三个分量每个分量都是一张和原图等大的二维数值矩阵堆叠起来就是一个形状为(3, H, W)的张量。这里的3就是我们说的输入通道数它是数据的天然属性——你给我一张图它要么是1通道的灰度要么是3通道的彩色要么是4通道带透明度的RGBA改不了。但当你把这张图送进第一个卷积层之后出来的东西形状就变了。输出通道数不再由数据决定而是由你自己在代码里写的那个数字决定。你写out_channels64网络就吐出64个通道你写out_channels32就是32个。这些通道每一个都是一张二维的特征图只不过它们描述的不再是颜色而是某种被网络学出来的模式响应——可能有的通道专门对水平边缘敏感有的对特定方向的纹理敏感有的对某种颜色块敏感。这就是输入通道和中间通道最本质的区别前者是看到的,后者是想到的。1.3 一个必须建立的直觉通道不是图层是特征维度我见过不少人把通道理解成图像编辑软件里的图层。这个类比在输入层勉强成立但在中间层会带来一个很坏的习惯以为通道之间是彼此独立、可以单独查看修改的。实际上卷积操作从第二个层开始就会把上一层的所有通道混合起来——每一个输出通道都是上一层所有输入通道的加权求和结果。换句话说通道之间从第一层卷积起就是纠缠在一起的任何一个输出通道都同时包含来自所有输入通道的信息。正确的直觉应该是把通道当成特征空间的维度。一张(64, 56, 56)的特征图你可以理解为在56×56这个空间网格上每一个位置都挂着一个64维的向量描述该位置在64个不同特征方向上的强度。这个视角能解释后面很多事情为什么1x1卷积能融合通道信息为什么通道注意力能算出一个权重向量为什么通道剪枝剪掉某个通道只是少了一个特征维度而不是丢了一块图像。把这一层想通后面所有关于通道的技术就都不玄了。2. 卷积核形状与通道数的计算关系2.1 从单通道灰度图推起最容易理解的起点是灰度图。假设输入是一张(1, 28, 28)的手写数字图我们用一个3×3的卷积核去卷它输出会是一张(1, 26, 26)的特征图。这个时候卷积核就是一个3×3的小矩阵参数量是9个再加1个偏置就是10个。整个过程没有任何通道维度的参与因为输入只有1个通道卷积核也只有一个深度切片。从这个最简情形出发你能看清卷积的本质在空间维度上滑动做加权求和。所谓的步长、填充、空洞全都是在空间维度上做文章和通道没有关系。所以如果你现在对卷积的空间操作还没完全吃透建议先用灰度图把步长和填充的手算练熟再来碰多通道否则两件事混在一起容易越看越乱。2.2 三通道输入时卷积核为什么变成立方体现在把输入换成(3, 224, 224)的彩色图。如果还是只想要一个输出通道那卷积核就不能是3×3了它得是(3, 3, 3)——第一个3对应输入的3个通道后两个3是空间上的3×3。这个立方体在滑动的时候是在空间上移动但它的深度方向是完整覆盖输入的所有通道的。卷积的过程是这样的在某个空间位置上把输入的3×3×3小块和卷积核的3×3×3立方体逐元素相乘再求和得到一个标量这就是输出特征图在该位置的值。注意是所有输入通道一起算出一个数而不是每个输入通道各算一个数。这一点极其关键很多人在推导参数量的时候会在这里多乘一次。所以从形状上看一个卷积层的权重张量永远是四维的(out_channels, in_channels, kernel_h, kernel_w)。PyTorch里你可以直接打印conv.weight.shape验证如果你写的层是nn.Conv2d(3, 64, 7)打印出来就是torch.Size([64, 3, 7, 7])。这四个维度分别的含义最好能背下来因为后面读所有报错信息都靠它。2.3 输出通道数是设计决策不是计算结果接着上面的例子如果我想输出32个通道怎么办答案是准备32个(3, 3, 3)的卷积核每个核独立地和输入做一次刚才描述的三维加权求和各自产生一张输出特征图最后把32张堆起来得到(32, 224, 224)。这就引出了一句应该刻在脑子里的话输出通道数 这一层卷积核的个数。它和输入尺寸无关和卷积核大小无关和batch size无关纯粹是你自己定的超参数。之所以很多经典网络里通道数一路从64涨到512再涨到2048不是因为算出来的而是因为设计者认为越往深层需要描述的特征种类越多。这个判断在工程上通常成立但它也不是铁律——过宽会带来参数量和显存爆炸过窄会限制表达力怎么取平衡是调网络时最常纠结的部分。层级位置典型输出通道数主要考虑第一个卷积层32 / 64输入信息量小通道太多浪费算力中间阶段128 / 256需要足够多的特征组合来描述局部模式深层阶段512 / 1024 / 2048空间尺寸已经很小靠增加通道维持容量分类头之前类别数量直接对齐任务输出维度这张表不是公式是经验区间。你要是拿它当硬性规定去设计网络多半会翻车但你要是完全无视它大概率会得到一堆参数浪费或者欠拟合的结果。2.4 参数量和计算量的手算示例理论说再多不如亲手算一遍。拿一个nn.Conv2d(3, 64, kernel_size7, stride2, padding3)举例输入是(1, 3, 224, 224)。参数量 输出通道 × 输入通道 × 卷积核高 × 卷积核宽 偏置 64 × 3 × 7 × 7 64 9408 64 9472个。输出空间尺寸 (224 2×3 - 7) / 2 1 (224 6 - 7)/2 1 223/2 1向下取整后是111 1 112。所以输出是(1, 64, 112, 112)。乘加次数MACs 输出通道 × 输入通道 × 核高 × 核宽 × 输出高 × 输出宽 64 × 3 × 49 × 112 × 112 64 × 147 × 12544 ≈1.18亿次。一次MAC算两次浮点运算的话大约是2.36亿FLOPs。import torch import torch.nn as nn conv nn.Conv2d(3, 64, kernel_size7, stride2, padding3) x torch.randn(1, 3, 224, 224) y conv(x) print(y.shape) # torch.Size([1, 64, 112, 112]) print(conv.weight.shape) # torch.Size([64, 3, 7, 7]) print(sum(p.numel() for p in conv.parameters())) # 9472跑一遍这段代码比看十遍公式都管用。我建议你在读任何一篇网络结构论文的时候都顺手把第一层的参数量手算一遍和论文里报的总参数量对一下对不上就说明你对某一层的理解有偏差。这个习惯能帮你发现大量看着像但其实理解错了的地方。3. 通道的三类改造手术分组、可分离与1x13.1 分组卷积把通道切开算省的是参数与访存标准卷积里每个输出通道都要看全部输入通道所以参数量是out × in × k × k。分组卷积的思路很直接把输入通道平均分成g组输出通道也分成g组第i组输出只跟第i组输入做卷积。这样一来每个输出通道只需要看in/g个输入通道参数量立刻降到out × (in/g) × k × k是原来的1/g。以nn.Conv2d(64, 64, 3, groups2)为例标准卷积参数量是64×64×9 36864分组后变成64×32×9 18432正好减半。如果g等于输入通道数那就是后面要说的深度卷积了。分组卷积最初被提出来是为了在有限显存下塞下更大的模型后来大家发现它还有个副作用不同组之间的通道完全不交流这相当于给网络加了一种结构化的稀疏约束。这种约束有时候是好事减少了冗余提升了泛化有时候是坏事不同组学到的特征可能重复浪费容量。所以现代网络里分组卷积经常和通道混洗搭配使用这个组合稍后会讲。3.2 深度可分离卷积depthwise 和 pointwise 各管一段深度可分离卷积把标准卷积拆成两步这一步拆得非常聪明。第一步叫逐深度卷积depthwise convolution对每一个输入通道单独用一个卷积核去卷输入有几个通道就用几个卷积核通道之间完全不混合。参数量是in × k × k当out in时。这一步负责的是空间维度的特征提取。第二步叫逐点卷积pointwise convolution其实就是一个1×1卷积用out个1×1的卷积核在通道维度上做线性组合参数量是out × in × 1 × 1。这一步负责的是通道维度的信息融合。还是拿刚才那个(3, 64, k7)的例子算一遍。深度卷积3×7×7 147。逐点卷积64×3 192。加起来339个参数对比标准卷积的9472个只有约3.6%。计算量上深度卷积是147×112×112 ≈ 184万次MAC逐点卷积是192×112×112 ≈ 241万次MAC合计约425万次而标准卷积是1.18亿次差不多是1/28。这个1/28是怎么来的有个粗略公式标准卷积与深度可分离卷积的计算量之比约为 1/out 1/k²。代入out64、k71/64 1/49 ≈ 0.036取倒数约27.7和实测吻合。这个公式值得记住它能让你在设计网络时快速判断这个层值不值得换成可分离卷积——输出通道越多、卷积核越大收益越明显如果是1×1卷积那深度可分离根本无从谈起。3.3 1x1卷积只动通道不动空间1×1卷积是通道操作里最纯粹的一个。它的卷积核空间尺寸是1×1滑过特征图时不会跨像素取信息所以它完全不改变空间分辨率只做一件事在每一个空间位置上把in维的通道向量线性变换成out维的向量。它的三大用途值得单独列出来升降维想减通道就用out in想升通道就用out in。这一步在残差网络里到处都是。跨通道融合把不同通道的信息加权组合等价于在每个像素位置上做一次全连接。降低后续计算量这就是所谓的瓶颈结构bottleneck。先用1×1把256通道压到64再用3×3在64通道上卷最后再用1×1升回256。相比直接在256通道上做3×3计算量大幅下降而且表达能力损失很小。import torch.nn as nn bottleneck nn.Sequential( nn.Conv2d(256, 64, kernel_size1, biasFalse), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1, biasFalse), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 256, kernel_size1, biasFalse), )这个结构你会在无数论文里反复见到。理解它的关键就是理解通道数是可以像水管口径一样被收放自如地调节的而1×1卷积就是那个阀门。3.4 通道混洗分组带来的副作用与补救分组卷积有个明显的问题如果一直分组下去最终的输出通道里来自第1组的通道永远只包含第1组输入的信息组与组之间的信息永远碰不到面。这在浅层可能无所谓但到了深层网络就很难组合出跨组的复杂特征。**通道混洗channel shuffle**的操作非常朴素把(g, out/g)这两维转置一下再拉平让原本属于同一组的相邻通道被打散到不同组里去。这样在下一个分组卷积里每个组接收到的输入就来自上一层所有组了。整个过程不引入任何参数只是一次内存重排成本几乎为零。这里有个实操细节要注意混洗必须在内存上真正做转置形如x.view(b, g, c//g, h, w).transpose(1, 2).contiguous().view(b, c, h, w)。少了那个.contiguous()后面接卷积的时候可能报错也可能因为内存不连续而拖慢速度。这是我早期写ShuffleNet结构时踩过的一个坑报错信息完全看不出是这个原因查了好久。4. 通道注意力机制统计量如何变成权重4.1 SE模块的三步拆解通道注意力这个词听起来很高深但拆开看SE模块Squeeze-and-Excitation的逻辑其实只有三步。Squeeze对每个通道做全局平均池化把(C, H, W)压成(C, 1, 1)。这一步的意义是给每个通道算一个总体活跃度因为后续要给单个通道分配权重就必须先有一个能代表整个通道的标量。用全局平均池化而不是最大池化是因为要衡量的是整体响应强度不是最亮的那一个点。Excitation用一个两层的微型全连接网络把C维向量映射到C维权重向量中间先降维到C/rr通常取16再升回来中间夹ReLU最后套Sigmoid。降维是为了控制参数量并引入非线性Sigmoid是为了把权重限制在0到1之间方便做缩放。这一层的参数量是2 × C² / r对C256、r16来说大概是8192个相对于主干网络微不足道。Reweight把算出来的权重逐通道乘回原特征图。某个通道权重接近1说明网络认为它重要接近0说明这个通道在当前输入下基本没用。import torch import torch.nn as nn class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.pool nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Linear(channels, channels // reduction, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels, biasFalse), nn.Sigmoid(), ) def forward(self, x): b, c, _, _ x.shape w self.pool(x).view(b, c) w self.fc(w).view(b, c, 1, 1) return x * w提示SE原论文里Excitation是用1×1卷积实现的和这里的全连接写法在数学上等价区别只在实现层面的内存布局。如果你看到别人的代码里是nn.Conv2d(c, c//r, 1)不要觉得奇怪是一样的东西。4.2 CBAM、ECA 对通道注意力的改进点在哪SE出来之后改进版本主要围绕两个方向怎么更准地算统计量以及怎么少走降维这条路。CBAMConvolutional Block Attention Module注意到只用平均池化会丢掉一些极值信息于是同时用平均池化和最大池化各算一个C维向量两者共享同一个MLP再把结果相加后过Sigmoid。它在通道注意力之后还接了一个空间注意力分支用7×7卷积在通道维度上做池化后生成一张单通道的空间权重图形成通道-空间的协同分配。实测中这种双池化的改动在中小规模数据集上通常能带来稳定的提升。ECAEfficient Channel Attention则从另一个角度切入它认为SE里那个降维操作本身就是有害的因为把C压到C/16会破坏通道之间原本的直接对应关系。于是它干脆不做全连接改用一维卷积直接在通道维度上做局部跨通道交互卷积核大小k根据通道数自适应k |log₂(C)/γ b/γ|取最近的奇数。这样参数量只有k个比SE少了好几个数量级效果却常常更好。import math import torch import torch.nn as nn class ECA(nn.Module): def __init__(self, channels, gamma2, b1): super().__init__() t int(abs((math.log2(channels) b) / gamma)) k t if t % 2 else t 1 self.avg nn.AdaptiveAvgPool2d(1) self.conv nn.Conv1d(1, 1, kernel_sizek, paddingk // 2, biasFalse) self.sig nn.Sigmoid() def forward(self, x): y self.avg(x) # B, C, 1, 1 y y.squeeze(-1).transpose(-1, -2) # B, 1, C y self.conv(y) y y.transpose(-1, -2).unsqueeze(-1) # B, C, 1, 1 return x * self.sig(y)4.3 通道注意力不涨反降的几种情形这块是我觉得最需要拿出来说的因为很多教程只讲怎么加模块不讲什么时候不该加。第一种情形是网络本身很小。当通道数只有16、32这个量级的时候SE里降到C/16就只剩1、2维了中间的瓶颈信息损失极大权重反而学不准。所以ECA那种自适应核大小的设计在小通道数场景下更有优势。第二种情形是批量太小。通道注意力的权重依赖全局池化得来的统计量如果batch size只有2或者4这个统计量的方差会非常大导致训练时权重抖动剧烈验证集表现反而不如不加。我的经验是batch size低于16的时候加通道注意力要格外谨慎或者考虑用GroupNorm之类的替代方案配合。第三种情形是加的位置不对。SE放在残差分支的末端、紧挨着相加操作之前效果通常比放在残差主干之外要好。如果你把它放在激活函数之后可能会削弱激活带来的稀疏性。这些都是论文里不一定写清楚、但实际调参时会明显感受到的差异。5. 通道相关的报错与坑一份排查链路5.1 形状报错信息要逐字读面对通道类报错最忌讳的就是看到红字就慌。PyTorch的报错信息其实写得很详细只是需要逐字拆解。看这个典型例子RuntimeError: Given groups1, weight of size [64, 3, 7, 7], expected input[1, 1, 224, 224] to have 3 channels, but got 1 channels instead这段话包含四个信息groups1告诉你是普通卷积不是分组卷积weight of size [64, 3, 7, 7]说明这层卷积期望64个输出、3个输入通道input[1, 1, 224, 224]是实际喂进去的张量形状最后一句直接点明了矛盾——期望3通道实际只有1通道。排查链路就三步第一定位是哪一层的权重第二看这一层的in_channels第三往上一层找看实际输出的通道数是多少。九成情况是你改了上一层的out_channels但忘了同步下一层的in_channels或者数据加载的时候用了灰度模式。剩下那一成是你在某个地方做了通道维度的操作但形状没对齐。5.2 concat 和 add 对通道数的要求完全不同这是我见过最多人混淆的一点值得单独用一张表说清楚。操作通道维度要求其他维度要求结果通道数逐元素相加 add必须完全相同必须完全相同保持不变通道拼接 concat可以不同必须完全相同各输入之和逐通道相乘 mul必须相同或可广播必须相同或可广播保持不变拼接的时候用的是torch.cat([a, b], dim1)注意dim1指的就是通道维度在NCHW布局下。如果你不小心写成dim0那就是在batch维度上拼形状不会报错但语义完全错了。add操作要求严格稍微有点广播就会出问题比如(1, 64, 56, 56)和(1, 64, 1, 1)相加广播会让空间维度被拉伸结果看起来对但语义上已经完全不是原意了。这种情况编译器不会报错只能靠你打印形状来发现。5.3 NCHW 和 NHWC一个容易忽略的内存格式问题PyTorch默认用NCHWbatch、通道、高、宽TensorFlow默认用NHWC。这两个格式在数学上完全等价但对性能的影响可能超过30%。原因是GPU和某些加速器在做卷积的时候更偏好通道维在最后的排布这样一次内存读取就能拿到相邻空间位置的同类特征。在PyTorch里你可以通过model.to(memory_formattorch.channels_last)和输入x.contiguous(memory_formattorch.channels_last)来切换到NHWC布局。实测在较新的GPU上这个一行代码的改动经常能带来可观的速度提升特别是在用混合精度训练的时候。代价是什么呢一旦你混用了两种格式某些算子会自动插入重新排布的操作反而更慢而且NCHW下理所当然的.view()操作在NHWC里可能会失败得换成.reshape()。所以我的建议是先用NCHW把模型调通确认精度没问题最后再做格式优化并且一定要重新跑一遍完整验证集。5.4 BN层和权重初始化里的通道参数批归一化层的构造参数是nn.BatchNorm2d(num_features)这个num_features必须等于输入特征图的通道数。如果你把它误写成空间尺寸比如56代码不会报错因为BN的参数量本来就很小但它会引入一大堆多余的统计量训练时表现为loss下降慢、验证集抖动。这种错误排查起来非常痛苦因为它不报错只是效果不好。权重初始化同理。用Kaiming初始化的时候fan_in的计算是in_channels × kernel_h × kernel_w如果通道数写错了初始化尺度就会偏离深层网络的梯度可能会爆炸或者消失。所以我现在的习惯是在每个自定义模块的__init__里都打印一次权重形状或者在forward里加一句assertdef forward(self, x): assert x.shape[1] self.expected_channels, \ fexpected {self.expected_channels} channels, got {x.shape[1]} ...这一行断言在多卡训练、动态输入尺寸的场景下救过我很多次。多卡切分的时候通道数一般不会变但显存占用的变化会让一些隐藏的形状问题暴露出来提前加断言能让你更快定位。6. 把通道看出来可视化与剪枝视角6.1 用工具核对每一层的通道数设计网络的时候光靠脑补很容易出现通道数前后不匹配。我常用的办法是用torchsummary或者直接手写一个遍历def print_shapes(model, input_shape(1, 3, 224, 224)): x torch.randn(*input_shape) for name, layer in model.named_children(): x layer(x) print(f{name:20s} - {tuple(x.shape)}) print_shapes(model)打印出来的东西比看代码直观得多。特别要注意的是跨层连接的位置——残差相加的地方、拼接的地方、以及最后从特征图变成向量的地方通常是全局平均池化或者view(b, -1)。后者是最容易出错的因为view(b, -1)会默默地把(C, H, W)压成C×H×W如果之前的通道数变了全连接层的输入维度就对不上报错信息会指向全连接层而不是真正的罪魁祸首。6.2 特征图可视化与死通道的识别通道的可视化做法是把某个中间层的特征图取出来在空间维度上做平均池化降维然后展开成一张长方形的热力图横轴是通道编号纵轴是空间位置或者直接是一维曲线。观察哪几个通道的响应一直偏低。如果某个通道在经过ReLU之后的输出长期恒等于0那它就是个死通道梯度为0权重永远不再更新占着显存但什么也不干。死通道的常见成因是学习率太大把某一层的偏置推到了很负的位置导致ReLU永远不激活或者是初始化尺度不合适。发现死通道之后的处理方式有两种一是调小学习率、换用更温和的初始化二是直接从结构上把它剪掉这就是通道剪枝的思路。我个人的经验是用平均值统计每个通道的激活强度如果某个通道的均值连续若干个epoch都排在整个层的最低5%且输出接近0基本可以判定它没用了。这个过程不需要人工干预写个hook自动统计就行。6.3 通道剪枝为什么可行通道剪枝背后的假设是训练好的网络里存在大量冗余通道它们对最终输出的贡献极小删掉之后精度几乎不变但参数量和计算量能显著下降。具体做法一般是给BN层的缩放因子γ加L1正则让不重要的通道对应的γ被压向0然后设定一个阈值或者按百分比把γ低于阈值的整个通道连同它对应的卷积核一起删掉。之所以用BN的γ而不是直接用卷积核权重的范数是因为γ直接决定了该通道输出信号的幅度物理意义更清晰而且和BN本身是配套的。剪枝完之后必须做微调fine-tune因为删掉通道会改变后续层的输入分布即使BN能缓解一部分损失函数还是会有明显跳变。微调的学习率通常设得比原来小一个数量级迭代几个epoch就能恢复大部分精度。我的经验是在结构规整的网络上剪掉30%到50%的通道通常是可以接受的但剪枝比例一旦超过60%精度掉得会非常快而且很多时候是不可恢复的。7. 一些更容易被忽略的通道细节第一通道数和感受野是两回事别把它们搅在一起理解。增加通道数提升的是特征空间的宽度增加卷积核大小或堆叠层数提升的是感受野两者解决的是完全不同的问题在网络设计时要分开权衡。第二不要把通道数和批量大小的维度搞混。写代码时遇到的所有形状问题第一步都是先确认四个维度各自对应什么。一个简单的口诀PyTorch里看到(N, C, H, W)N是样本数C是通道数剩下两个是空间尺寸。第三通道顺序是语义的一部分。某些恒等映射、残差相加、预训练权重加载对通道顺序是敏感的。如果你自己做通道重排然后又加载别人训好的权重结果一定错。加载预训练权重时报size mismatch很多时候就是通道顺序或者通道数不一致导致的。第四跨模态迁移时通道数常常对不上。拿一个在3通道彩色图上训好的模型去处理1通道灰度图或者多光谱图像最省事的做法是在第一层把权重按通道求和或者复制平均得到一个形状匹配的新权重再微调几轮。直接截断或者随机初始化会浪费大量预训练知识。我自己在调网络结构的时候有个小习惯把每一层的通道数写在纸上画成一条曲线看看它是怎么变化的。如果曲线在某处突然掉下去又突然涨回来往往意味着那里藏着一个瓶颈或者信息损失点值得重新审视。这个习惯比看代码更容易发现设计层面的问题因为代码是一行行读的而曲线是整体看的。
返回列表