
1. 先弄清楚通道数到底卡在哪1.1 我被通道数绊倒的几次真实经历刚接触卷积神经网络那阵子我一度以为最难的是反向传播是梯度消失是各种花哨的网络结构。结果真正让我在调模型时频频卡壳的反而是最不起眼的两个东西输入通道数和输出通道数。第一次写代码输入是彩色图我随手把in_channels写成 1模型直接报形状不匹配后来想把某层特征图接进自定义模块又搞不懂上一层输出的 64 个通道到底对应什么含义再后来算参数量我天真地按kernel_size × kernel_size去乘结果和summary()打出来的数字差了整整一个量级。这些坑有一个共同点它们都不涉及高深的数学纯粹是对通道维度理解不透。而通道这一个维度恰恰是卷积区别于全连接的核心所在。全连接层把输入摊平成一维空间结构全丢了卷积层保留了(通道, 高, 宽)这种三维的组织方式才让局部感受野、权值共享这些东西成立。所以我觉得有必要把这两个概念彻底掰开揉碎讲一遍讲清楚它们各自管什么、谁决定谁、算参数时怎么用。这篇内容适合谁看如果你已经能跑通一个简单的 CNN但面对nn.Conv2d的参数列表还是靠猜如果你看得懂结构图却说不清每层特征图的通道为什么是 32、64、128 这样翻倍如果你想自己改网络结构、做通道剪枝那这篇基本就是给你准备的。我下面会用大量类比、手算过程和可直接跑的代码把这件事从直觉讲到公式。先把结论摆在前头输入通道数由上一层的输出决定输出通道数由你自己决定。记住这一句后面八成的问题都能自己推出来。1.2 用一个印刷厂类比把两个概念分开我喜欢用印刷厂来类比卷积层。假设你是一家印刷厂的老板接到一批订单要把一份原稿印成若干种不同的版式。这里有几个角色原稿的张数就好比输入通道数。来的是单色稿还是三色稿决定了你要同时处理几摞纸。你要印出的版式种类数就好比输出通道数。你想出 32 种风格那就得准备 32 套印版。每一套印版就是一个卷积核filter。关键在于每一套印版都必须能同时盖住所有摞原稿。你不可能用一套只管红墨水的版去处理三色原稿那样蓝绿两摞纸就白来了。所以一套印版的厚度必须等于原稿的张数。翻到技术语言每个卷积核的深度 输入通道数这就是那个硬约束的直观来源。而你要产出多少种版式完全由业务需求决定跟原稿几摞没关系。这就是输出通道数 卷积核的个数。理解了这个类比后面那些in_channels、out_channels、groups的参数就都是这条主线的分支了。1.3 本文的组织方式我会按“先建直觉再上公式最后跑代码”的顺序来铺。第 2 节专门讲输入通道把为什么它由卷积核的深度决定讲透第 3 节讲输出通道重点说清它的物理含义和设计套路第 4 节把参数量、计算量的公式从头推一遍附手算过程第 5 节给出可运行的 PyTorch 代码验证每一步的通道变化顺带把分组卷积、深度可分离卷积、3D 卷积里的通道玩法一起带上第 6 节是我这几年攒下来的报错速查表和经验法则。我不打算把每个概念都写成教科书式的定义而是按“我当时是这么想错的正确的应该是这样”的思路走。你在读的时候如果某一处突然觉得“对啊我上次就是这么栽的”那这段就没白写。2. 输入通道数它其实是卷积核的厚度2.1 输入通道数不是你想定就能定很多人第一次接触nn.Conv2d看到参数列表里in_channels和out_channels并列就默认这俩是平级的、都可以自由发挥。这是个很深的误解。输出通道数你可以随意定输入通道数你几乎没有自由度它是被上一层的输出钉死的。为什么这么说回顾一下数据在网络中的流动一张 RGB 图片送进来形状是(3, H, W)通道数是 3。经过第一层卷积后输出的形状变成(C1, H1, W1)。那么第二层卷积读到的输入通道数就是 C1不是 3 了。你如果硬把第二层的in_channels写成 3程序立刻报维度不匹配。我见过有人为了“省事”在中间插一层时把in_channels猜着填结果要么报错要么更糟——如果恰好蒙对了数字但语义上是错的模型能跑但学不到东西。所以正确的姿势是写每一层之前先明确上一层的输出通道是多少或者干脆用nn.LazyConv2d这种能自动推断的接口先把流程跑通再回头固化参数。从信息流动的角度看输入通道数代表的不是“我有几张图”而是“当前这一层能从多少个不同的特征视角去观察输入”。第一层观察到的是原始的颜色三通道中间层观察到的是上一层提炼出来的边缘、纹理、局部形状等抽象特征。通道数越多意味着前面的网络已经为你准备了越丰富的“素材”你这一层可用的信息就越多样。2.2 卷积核的完整形状从二维直觉升级到四维张量课本上画卷积永远画一个 3×3 的小方格在图上滑来滑去那是最简化的示意只画了单个通道的情况。真实情况里卷积核是一个四维张量形状是(C_out, C_in, K_h, K_w)拆开看这四个维度分别管什么C_out这层的输出通道数也就是有多少个独立的卷积核。C_in每个卷积核的深度必须等于输入通道数。K_h、K_w卷积核在空间上的高和宽比如 3×3、5×5。所以一个“64 通道输出、接收 3 通道输入、3×3 核”的卷积层它的权重张量形状就是(64, 3, 3, 3)一共 64 个卷积核每个核的形状是(3, 3, 3)——注意最后这个三维形状第三维的 3 就是深度对应三个输入通道。这里有个特别容易混淆的点人们口头说的“3×3 卷积”从来都只是指空间尺寸不含深度。你跟别人交流时说“我用的是 3×3 卷积输入 3 通道输出 64 通道”对方脑子里自动补全的形状就是(64, 3, 3, 3)。所以下次看结构图图中标注Conv 3×3, 64的时候那个 64 是输出通道不是核的尺寸别读错。2.3 输入通道与卷积核深度为什么必须相等现在正面回答那个硬约束的由来。一次卷积运算的完整过程是这样的把卷积核覆盖在输入特征图的一个空间位置上然后逐通道做乘加最后把所有通道的结果加在一起得到一个标量输出。注意这个“加在一起”是整个约束的根源。假设输入有 3 个通道核的深度只有 1会发生什么核只能对第 1 个通道做乘加第 2、3 通道的数据完全没参与运算等于被丢弃了。反过来核的深度是 5输入只有 3 通道那核的第 4、5 层没有数据可乘运算没法完成。两种方向都不成立所以深度必须严格相等。这点和全连接层可以对比着理解。全连接层里输入向量的长度和权重矩阵的列数也必须对上本质上是一回事只要涉及加权求和参与运算的两侧维度就得一致。区别在于卷积把这个“一致”限制在了通道维度上而空间维度上有权值共享和滑动窗口的机制不用一一对应。实操提醒如果你看到某层报错说 “expected input to have 3 channels, but got 1 channels”八成是数据集的问题而不是网络的问题。比如你把单通道灰度图送进了一个为 RGB 设计的网络或者反过来的情况。2.4 三类典型输入的通道数对照实际项目里作为卷积层输入的张量主要来自三种来源通道数的含义各不相同用一个表格对照会比较清楚。输入来源典型通道数通道含义常见坑原始图像1 或 3 或 4灰度 / RGB / RGBA数据加载时忘了统一转灰度或转 RGB上一层卷积输出自定义如 64抽象特征的不同视角手写网络时忘了对齐报维度错拼接或多分支合并若干层通道之和多来源特征堆叠拼接后通道翻倍下一层输入要跟着改第三行尤其值得说。像 Inception 那种结构会把不同尺寸卷积核的输出在通道维度上concat起来。如果四个分支各自输出 64 通道拼完就是 256 通道。这时候下一层的in_channels必须写 256。我见过有人拼接完直接接一个 64 输入的卷积报错信息一长串其实根源就在这。还有一个细节通道顺序也很重要。深度学习框架里几乎都是 channel-first通道在前形状是(N, C, H, W)但某些移动端推理框架是 channel-last形状(N, H, W, C)。同一份权重格式搞反了通道数看起来是对的但计算结果全错而且不一定报错属于最难查的一类 bug。转换格式的时候务必用框架提供的正规接口别自己手动permute完忘了contiguous。3. 输出通道数你想要的“特征视角”有几路3.1 输出通道数等于卷积核的个数说完输入来看输出。输出通道数的定义非常干脆这一层里有多少个独立的卷积核就产生多少个输出通道。一个卷积核扫完整张输入特征图得到一个二维的响应图feature map64 个卷积核就得到 64 张响应图堆起来就是 64 个通道。为什么这么设计因为每个卷积核的权重是独立学习的它们会各自收敛到不同的“偏好”上。有的核可能对水平边缘敏感有的对垂直边缘敏感有的对某个方向的纹理敏感。你给的核越多网络能同时刻画的模式就越多。这就像给一支调查队派任务派 8 个人只能分头查 8 条线索派 64 个人就能同时铺开 64 条线。这里有个非常适合建立直觉的实验拿一个训练好的网络把第一层的 64 个卷积核单独拿出来可视化。你会看到它们长得像各种方向、各种频率的条纹和小块这就是所谓的“学到的东西”。核的个数越多这些可视化的模式就越丰富但也会出现冗余——有些核学出来几乎一样这也是通道剪枝能生效的前提。3.2 输出通道数增加为什么能提升表达能力从线性代数的角度一次卷积本质上是一次带局部约束的线性变换输出可以用矩阵乘的形式近似理解。核的个数决定了输出空间的维度。维度越高能线性表示的模式就越多这跟“隐藏层宽度越大模型容量越高”是同样的道理。但容量大不等于效果好这里要泼一盆冷水。无脑堆输出通道会带来三个代价第一是参数量和显存线性上涨。通道翻倍参数跟着翻倍如果同时把输入通道也翻倍参数量就是四倍增长。我在一块显存不大的卡上做过测试把某个中间层从 256 通道提到 512单层参数从 59 万涨到 236 万训练时显存直接多了几百兆。第二是过拟合风险。小数据集上堆大通道训练集损失刷刷降验证集纹丝不动甚至往上走这是最经典的过拟合信号。第三是推理延迟。通道数直接决定计算量部署到移动端时这个账要算得很细。所以输出通道的设计本质是在容量和代价之间找平衡点。常见的做法是让通道数随网络加深逐步增加同时空间尺寸逐步减小——这样一来虽然通道多了但特征图变小了整体计算量还能控制在可接受范围。这个“空间换通道”的思路是几乎所有经典骨干网络都在用的。3.3 从 LeNet-5 到现代网络的通道演进规律LeNet-5是很多人的入门网络它的通道设计朴素得可爱输入 1 通道第一个卷积层输出 6 通道第二个卷积层输出 16 通道。整个网络的参数量只有几万放在今天简直微不足道。但你仔细看它的思路通道从 6 涨到 16涨了大约 2.67 倍同时特征图从 28×28 降到 10×10。空间在缩通道在涨这个此消彼长的节奏已经定下来了。到了 VGG 时代规律变得更规整64 → 128 → 256 → 512 → 512每次空间尺寸减半池化通道就翻一倍。为什么是翻倍因为空间尺寸减半意味着每个位置的信息量减半通道翻倍刚好补回来让每层的总信息承载量大致守恒。这算是一个经验公式不是铁律但很实用。再往后ResNet 的 bottleneck 结构提出了另一种玩法先用 1×1 卷积把通道压下去比如 256 压到 64做 3×3 卷积再用 1×1 卷积升回 256。这么绕一圈图什么图的是省参数。同样的感受野直接上 3×3 处理 256 通道参数量是256×256×9压到 64 再升回去参数量降到256×64 64×64×9 64×256大约是原来的三分之一。这个技巧我后面在参数量那节还会手算一遍。3D 卷积神经网络里通道的含义就更丰富了。以视频为例输入形状是(C, D, H, W)C 是颜色通道D 是时间帧数。经过一个 3D 卷积层后输出的 D 维度时间深度和 C 维度特征通道同时变化。要注意区分这两个“深度”一个来自卷积核的时间尺寸一个来自核的个数。新手最容易在这里把两件事搞混。3.4 输出通道数不影响空间尺寸这点必须分清最后一个特别容易混的点输出通道数只影响“有几张特征图”不影响“每张特征图多大”。输出特征图的空间尺寸由另外三个因素决定卷积核的空间尺寸 K、步长 stride、填充 padding。公式是这样的H_out floor((H_in 2P - K) / S) 1 W_out floor((W_in 2P - K) / S) 1举个具体数字。输入(3, 224, 224)卷积核 3×3padding 1stride 1那么H_out (224 2 - 3)/1 1 224空间尺寸不变。输出通道设为 64最终输出就是(64, 224, 224)。如果把 stride 改成 2H_out (224 2 - 3)/2 1 112空间减半通道数依然是 64。这个区分非常关键因为在写网络时你其实是在同时控制两件事用kernel_size、stride、padding控制空间尺寸的演变用out_channels控制通道数的演变。两者互相独立可以自由组合出下采样、升维、降维等各种操作。我建议在纸上画一个表左边写空间尺寸的变化右边写通道数的变化画完一遍基本就不会混了。4. 参数量与计算量的推导把账算明白4.1 参数量公式的逐项拆解现在把公式推一遍。对于一个标准的二维卷积层权重张量形状是(C_out, C_in, K, K)所以权重的元素个数就是权重参数 C_out × C_in × K × K别忘了偏置。每个输出通道配一个偏置所以偏置参数 C_out加一起总参数量 C_out × (C_in × K × K 1)这个式子值得反复看几遍。它告诉你两件事参数量与输入通道成正比与输出通道成正比与核面积成正比。注意和输入图像的空间尺寸 H、W 完全无关这是卷积相对全连接最大的优势。全连接层处理 224×224 的图光一层就可能上亿参数卷积层由于权值共享参数量只跟核的大小和通道数挂钩。我刚开始就栽在这儿拿到一个 5×5 卷积输入 3 通道输出 32 通道我以为参数是 25×32800实际是32×(3×251)2432。差了整整 3 倍因为我漏掉了输入通道这一维。这个错误在做模型压缩时特别致命你以为剪掉了多少参数实际一算差得远。4.2 三层网络参数的手算演练光看公式还是抽象直接上手算。假设一个简化版的网络输入是(3, 32, 32)结构如下第 1 层Conv2d(3, 32, 3, padding1)第 2 层Conv2d(32, 64, 3, padding1)第 3 层Conv2d(64, 128, 3, padding1)逐层算参数量第 1 层32 × (3 × 3 × 3 1) 32 × 28 896。注意这里3×3×3是核的体积其中第一个 3 是输入通道。特征图空间尺寸保持不变还是 32×32。第 2 层64 × (32 × 3 × 3 1) 64 × 289 18496。这一层参数已经冲到一万八只因为输入通道从 3 跳到了 32。第 3 层128 × (64 × 3 × 3 1) 128 × 577 73856。七万三接近第二层的四倍。三层加起来不到 9.3 万参数。但你看这个增速896 → 18496 → 73856每层都是前一层的好几倍。这个增长的主要驱动力就是输入通道数因为它在公式里是乘数。这也是为什么网络越深参数越多——不是深度本身导致的是通道数一路涨上去导致的。再把 1×1 卷积的省钱效果算一遍。假设同样是处理 256 通道的特征图目标是输出 256 通道方案 A直接 3×3 卷积参数256 × (256 × 9 1) 256 × 2305 589,824。方案 B先用 1×1 把 256 压到 64参数64 × (256 × 1 1) 64 × 257 16,448再 3×3 从 64 到 64参数64 × (64 × 9 1) 64 × 577 36,928再用 1×1 升回 256参数256 × (64 × 1 1) 256 × 65 16,640。三段加起来16,448 36,928 16,640 70,016。方案 B 的参数量只有方案 A 的 11.9%感受野却完全一样。这就是 bottleneck 的威力所在。代价是中间表达被压缩到了 64 维理论上损失了一些信息但实践中因为加入了非线性激活效果往往差不太多甚至因为参数少了、正则效果强了泛化还更好。这个取舍在工程上非常值。4.3 计算量 FLOPs 的估算方法参数量管的是显存和模型体积真正决定推理速度的其实是计算量。一次卷积的主要运算是乘加粗略估算公式是FLOPs ≈ C_out × C_in × K × K × H_out × W_out注意这里多出了输出特征图的空间尺寸 H_out、W_out因为卷积核要在每个空间位置都滑一遍。这跟参数量的公式形成了鲜明对照参数量只看核本身计算量还要乘上输出尺寸。把上面的例子代进去看看。假设某层输入(256, 56, 56)卷积 3×3输出 256 通道padding 1stride 1那么输出尺寸是(256, 56, 56)FLOPs ≈ 256 × 256 × 3 × 3 × 56 × 56 ≈ 1.85 × 10^10一百八十多亿次乘加单层就这么大。换成方案 B 的三段式1×1 降维256×64×1×1×56×56 ≈ 5.14×10^83×3 卷积64×64×9×56×56 ≈ 1.16×10^91×1 升维64×256×1×56×56 ≈ 5.14×10^8合计约2.19×10^9。计算量降到原来的 11.8%和参数量的降幅基本一致。记住一个规律1×1 卷积的参数和计算量里K×K那一项等于 1所以它永远是省算力的利器。降维、升维、通道融合凡是能靠 1×1 干的活别用 3×3 硬上。4.4 需要注意的边界情况公式好用但有几个边界别搞错。第一公式里没算激活函数、池化、归一化层的开销。ReLU 本身很便宜但 BatchNorm 的通道维度运算加上内存读写在移动端可能占到相当比例纯算卷积 FLOPs 会低估实际耗时。第二理论 FLOPs 和真实延迟不成正比。同样 FLOPs 的层通道数少、空间大的往往比通道数多、空间小的慢因为访存模式不同。深度可分离卷积就是典型例子FLOPs 降得很多但在某些硬件上因为算子太碎实测加速比远不如理论值。我踩过这个坑按 FLOPs 算以为能快 8 倍实测只快了不到 3 倍。第三groups 参数会改变这一切。分组卷积把输入通道切成 g 组每组只负责一部分输出通道参数量和计算量都变成原来的 1/g。这是移动端网络的常用手段后面代码部分会专门验证。5. 代码实操用 PyTorch 亲手验证通道变化5.1 一个最小可运行的验证脚本光靠纸上推导容易飘跑一遍代码最踏实。下面这段脚本验证最基本的形状变换你可以直接复制运行。import torch import torch.nn as nn # 模拟一张 RGB 图片batch13 通道224x224 x torch.randn(1, 3, 224, 224) # 输入 3 通道输出 64 通道3x3 卷积padding1 保持尺寸 conv1 nn.Conv2d(in_channels3, out_channels64, kernel_size3, padding1) y1 conv1(x) print(第1层输出:, y1.shape) # torch.Size([1, 64, 224, 224]) # 第二层的输入通道必须写成 64不能写 3 conv2 nn.Conv2d(in_channels64, out_channels128, kernel_size3, padding1) y2 conv2(y1) print(第2层输出:, y2.shape) # torch.Size([1, 128, 224, 224]) # 看看权重张量的真实形状 print(第1层权重形状:, conv1.weight.shape) # torch.Size([64, 3, 3, 3]) print(第2层权重形状:, conv2.weight.shape) # torch.Size([128, 64, 3, 3])跑完你会看到两个关键输出。第一conv1.weight.shape是(64, 3, 3, 3)正好对应“输出通道在前输入通道在后”的四维结构这在前面讲过了。第二第二层的in_channels必须写 64这个数字就是第一层的输出通道。你试着把 64 改成 3立刻会看到报错报错信息里会明确写出期望的通道数和实际收到的通道数。5.2 打印每层参数量并与公式对账接着上面把参数量打出来跟手算结果核对一遍。def count_params(layer): return sum(p.numel() for p in layer.parameters()) print(第1层参数量:, count_params(conv1)) # 64 * (3 * 3 * 3 1) 1792 print(第2层参数量:, count_params(conv2)) # 128 * (64 * 3 * 3 1) 73856 # 单独看权重和偏置 print(conv1 权重:, conv1.weight.numel()) # 64*3*3*3 1728 print(conv1 偏置:, conv1.bias.numel()) # 64第一层打出来是 1792正好是64 × (3×3×3 1)的结果。第二层是 73856对应128 × (64×3×3 1)。对上了说明理解正确。我建议你把这几行代码存下来当模板以后每设计一层都手动算一遍再让代码验证一遍两次对得上才算真的掌握了。这个习惯帮我避免了好几次通道设计上的低级错误尤其是做模型剪枝的时候。还有一个更省事的办法装个torchinfo或者用torchsummary直接把整个模型的逐层形状和参数量一次性打出来。但在你还处于学习阶段时我建议先手算再看工具输出不然容易养成“工具说啥就是啥”的依赖。5.3 分组卷积与深度可分离卷积里的通道变化分组卷积是理解通道数的一个绝佳案例因为它直接挑战了“每个核必须看全部输入通道”这个默认前提。# 普通卷积3 - 6 plain nn.Conv2d(3, 6, kernel_size3, padding1) print(普通卷积参数量:, count_params(plain)) # 6 * (3 * 3 * 3 1) 168 # 分组卷积groups3输入和输出都被分成 3 组 grouped nn.Conv2d(3, 6, kernel_size3, padding1, groups3) print(分组卷积参数量:, count_params(grouped)) # 每组处理 1 个输入通道、产出 2 个输出通道 # 每组参数 2 * (1*3*3 1) 20三组共 60 # 深度可分离卷积groups in_channels depthwise nn.Conv2d(3, 3, kernel_size3, padding1, groups3) print(深度卷积参数量:, count_params(depthwise)) # 3 * (1*3*3 1) 30 # 再补一个 1x1 的逐点卷积做通道混合 pointwise nn.Conv2d(3, 6, kernel_size1) print(逐点卷积参数量:, count_params(pointwise)) # 6 * (3*1*1 1) 24普通卷积 168 个参数分组卷积只有 60 个降到约 36%。深度卷积 30 个加上逐点卷积 24 个加起来 54 个比普通卷积省了 68%。这就是移动端网络偏爱深度可分离卷积的原因。有个约束要记住用groups时输入通道数和输出通道数都必须能被 groups 整除。上面groups3配in3, out6是可以的因为 3 和 6 都能被 3 整除。如果你写out4, groups3直接报错。这个约束的数学根源是分组是把通道平均切块切不匀就没法办。5.4 3D 卷积里的通道维度处理3D 卷积神经网络常在视频、医学体数据上出现它的通道维度多了一层值得单独看看。# 模拟一段视频特征batch13 个颜色通道 # 时间深度 16 帧空间 112x112 v torch.randn(1, 3, 16, 112, 112) conv3d nn.Conv3d(in_channels3, out_channels32, kernel_size3, padding1) out conv3d(v) print(3D卷积输出:, out.shape) # torch.Size([1, 32, 16, 112, 112]) print(3D权重形状:, conv3d.weight.shape) # torch.Size([32, 3, 3, 3, 3]) print(3D参数量:, count_params(conv3d)) # 32 * (3 * 27 1) 2624看清楚这里维度排布形状是(N, C, D, H, W)C 是通道D 是时间深度。权重形状(32, 3, 3, 3, 3)是五维的后三维是卷积核的立体尺寸。参数量是32 × (3×27 1) 2624比同规模的 2D 版本参数多出 9 倍因为核的体积从 9 涨到了 27。实际用的时候有个坑随着卷积层加深时间深度 D 会因为卷积核的时间尺寸而不断缩减。如果你想在网络中间保持时间维度不变别忘了在时间维度上也做 padding。很多人只给空间加了 padding结果跑几层之后时间维度归零报出让人摸不着头脑的错误。一个实测经验3D 卷积的显存占用非常夸张。上面这个(1, 3, 16, 112, 112)的输入看着不大一旦输出通道提到 64中间激活值就能吃掉好几个 G。真要做视频任务我一般会先把空间尺寸降下来再做时间维度的建模别一上来就在高分辨率上硬跑 3D。6. 常见问题与排查技巧实录6.1 通道相关报错速查表这类报错的信息量其实很大只要读懂了定位非常快。我把遇到过的典型问题整理成表方便你对照。报错关键词真实原因解决方法expected input to have 3 channels, but got 1数据是灰度图网络按 RGB 设计数据加载时复制成 3 通道或改网络首层输入为 1expected input to have 64 channels, but got 32上一层输出通道和本层输入没对齐检查上一层out_channels改本层in_channelschannels cannot be divided by groups分组数不能整除通道数调整groups或让通道数变成它的倍数Given groups1, weight of size...权重文件与当前网络结构不匹配核对加载的预训练权重对应的通道配置size mismatch for ...通常出现在加载 checkpoint 时逐层对比权重形状确认通道改动点第一行和第二行占了我遇到问题的七成以上。判断方法很直接看报错里的两个数字一个是网络期望的一个是实际收到的。实际收到的那个数就是上一层输出的通道数期望的那个数写在你定义的in_channels里。把这俩对上问题就解决了。第三行和第四行属于进阶一点的问题。分组卷积刚上手时特别容易忘掉整除约束尤其是从别处抄来一段代码只改了输出通道没改 groups。第五行在做迁移学习时高频出现你换了个分类头或者改了某一层的通道加载旧权重就会形状不匹配这时候要么用strictFalse跳过不匹配的层要么干脆重新训练这些层。6.2 设计通道数时可以照着走的经验我攒了几条通道设计的经验不一定放之四海皆准但能帮你少走弯路。第一条首层的输出通道不用太大。很多教程一上来就是 64但其实 32 甚至 16 在小数据集上往往更好。首层主要提取的是低级特征边缘、颜色块通道太多容易过拟合而且首层的输入通道通常只有 3核本身就很小堆太多也没多少信息可挖。第二条通道增长要跟着空间缩小走。空间减半、通道翻倍这个节奏在多数任务上都比较稳。如果某个阶段空间没缩通道就别急着涨否则计算量会爆炸。我个人习惯是只在降采样层之后才翻倍通道。第三条中间层的通道不要突然大幅度跳变。从 64 直接跳到 512中间那层参数会很大而且特征也没那么快需要这么宽的表示。加一个 128、256 的过渡会更平滑收敛也更稳。第四条用 1×1 卷积做瓶颈。只要某层的输入通道超过 256我基本都会考虑在它前面塞一个 1×1 降维把通道收到原来的四分之一左右做完 3×3 再升回去。前面算过了这一手能省接近九成参数几乎不影响精度。第五条别用神经架构搜索以外的方法去“精确最优”通道数。通道配置对最终精度的影响远不如数据质量、训练策略、正则化这些因素。花大量时间去调一个 64 还是 72 的问题性价比极低。定个合理值把精力放在别的地方。6.3 我踩过的几个坑你可别再踩第一个坑是把通道顺序和 batch 顺序搞混。有次做格式转换手动permute把(N, C, H, W)转成(N, H, W, C)结果某一处少转了一次代码照样跑完精度掉了几个点查了两天才发现。这类错误不报错、只降精度最难查。现在我的习惯是能用框架提供的接口就用接口手动转换后一定加一句形状打印。第二个坑是复制网络结构时忘改 in_channels。比如从别人的代码里搬了一段残差块那个块原本是按 256 输入设计的我的特征是 128 通道直接搬过来就报错或者蒙对了数字但语义错位。现在我会在每层的注释里写清楚输入输出通道搬代码时先看注释。第三个坑是上采样时通道没跟着调整。解码器里做上采样空间翻倍了但通道如果还保持和编码器对称层一样拼接时就会出问题。UNet 那类结构里拼接是通道维度的操作编码器给 64 通道、解码器给 32 通道拼起来是 96下一层就得按 96 写。这个数字很容易算错我一般会在解码器每一层旁边把拼接后的通道数写死在注释里。第四个坑是以为通道数越大精度一定越高。我在一个小数据集上把某个中间层从 128 提到 256参数量翻倍训练时间长了将近一倍最后验证集精度反而降了 0.7 个点。原因就是过拟合。后来加了更强的数据增强才把差距补回来但那一轮实验白烧了好几个小时的算力。从那以后我改通道都是小步试探一次只动一处。第五个坑是忘了考虑通道剪枝后的实际收益。理论上剪掉一半通道参数减半速度应该提升不少。但实测下来因为剩下的通道在内存里不连续访存效率下降加速比经常只有理论值的一半。所以真要落地剪枝别只看参数减少的比例一定得在目标硬件上实测延迟。最后分享个我一直在用的小方法。每次设计完一个网络我会在纸上画一张表列三样东西每层的输入通道、输出通道、输出空间尺寸。然后把这张表和代码里的定义逐行对一遍。这个动作多花五分钟但帮我省下的调试时间远不止五小时。通道这件事说难也不难核心就是那两条输入通道由上游决定输出通道由自己决定参数量与两者都成正比。把这两句刻在脑子里配合上面那张报错速查表基本就够用了。