公司动态

11 卷积神经网络 CNN 入门:计算机是怎样看图片的

📅 2026/8/12 17:48:50
11 卷积神经网络 CNN 入门:计算机是怎样看图片的
前言前 10 篇建立了神经网络的通用基础Tensor、自动求导、多层感知机、损失函数、优化器、训练与验证流程以及过拟合和正则化。从这一篇开始我们把这些知识带到图像任务中。普通多层感知机MLP已经能拟合非线性规律为什么处理图片时还要专门设计卷积神经网络Convolutional Neural NetworkCNN答案不在于“图片必须使用某个 API”而在于图片本身具有特殊的数据结构像素按照高度和宽度排列相邻像素往往共同形成边缘、纹理和轮廓同一种局部模式可能出现在图片的不同位置颜色图片还包含彼此对应的多个通道。CNN 通过局部连接和权重共享把这些特点直接放进网络结构。本篇将从一个小矩阵出发手算一次卷积再使用 PyTorch 的nn.Conv2d、nn.ReLU和nn.MaxPool2d搭建一个只做前向计算的基础 CNN。本篇不会训练 CNN也不会使用 torchvision、真实图像数据集、数据增强或迁移学习。这些内容留给后续文章。一、前置知识与学习目标阅读本文前建议已经掌握Tensor 的 shape、维度索引和批次维度MLP、nn.Linear、ReLU 和可训练参数nn.Module、forward()与nn.Sequential训练、验证和测试的职责边界。学完本文后你应该能够看懂灰度图、RGB 图片以及批量图片的[C,H,W]和[N,C,H,W]解释 MLP 可以处理图片但直接展平没有显式利用二维空间结构手算局部卷积并用固定权重的Conv2d验证理解卷积核、特征图、局部连接和权重共享区分输入通道、输出通道和原始颜色通道看懂Conv2d.weight的[out_channels,in_channels,kH,kW]根据 kernel size、stride 和 padding 计算输出尺寸理解 ReLU、Max Pooling 和感受野的基本作用逐层追踪一个两层 CNN 的 shape并得到分类 logits。本文代码在 Python 3.9.25、PyTorch 2.2.2cu121 环境中运行。二、图片首先是具有空间结构的 Tensor图片与普通表格数据有什么不同一行表格可能包含年龄、收入、房间数等不同含义的特征。图片中的数值则按照二维位置排列像素(10, 10)与(10, 11)通常比相距很远的两个像素具有更直接的局部关系。一条边缘并不是某个像素单独形成的而是邻近像素发生明暗变化的结果纹理和轮廓也依赖局部区域中的组合。与此同时同样的竖直边缘可能出现在左侧、中间或右侧。理想的模型应该学会“这种局部变化是什么”而不是在每个绝对位置分别学习一套互不相关的规则。灰度图、RGB 图与 NCHW一张高度为 (H)、宽度为 (W) 的灰度图片可以先表示成[H,W]。进入卷积层时还要明确它有一个灰度通道因此常写成[1,H,W]RGB 图片在同一空间位置记录红、绿、蓝三个值所以常写成[3,H,W]把多张图片组成一个批次后最前面再增加样本数量N[N,C,H,W]四个字母分别表示N批次中的样本数量Batch SizeC通道数ChannelH高度HeightW宽度Width。PyTorch 的Conv2d常用这种 NCHW 顺序。下面的代码没有加载图片文件只用全零 Tensor 验证 shapeimport torch gray_hw torch.zeros(28, 28) gray_chw gray_hw.unsqueeze(0) rgb_chw torch.zeros(3, 32, 32) rgb_batch_nchw torch.zeros(8, 3, 32, 32) print(gray [H,W]:, gray_hw.shape) print(gray [C,H,W]:, gray_chw.shape) print(RGB [C,H,W]:, rgb_chw.shape) print(batch [N,C,H,W]:, rgb_batch_nchw.shape)实际输出gray [H,W]: torch.Size([28, 28]) gray [C,H,W]: torch.Size([1, 28, 28]) RGB [C,H,W]: torch.Size([3, 32, 32]) batch [N,C,H,W]: torch.Size([8, 3, 32, 32])unsqueeze(0)在最前面增加一个长度为 1 的通道维没有复制或删除像素。一张 RGB 图片若只有[3,32,32]送入通常按批次处理的代码前还可能需要再增加 batch 维变成[1,3,32,32]。三、MLP 能处理图片但没有显式利用空间结构MLP 通常先把原图展平一张28×28的灰度图片共有 784 个数。交给 MLP 时常见做法是[N,1,28,28] → Flatten → [N,784] → Linear这种方法完全可以训练。在 MNIST 等较简单任务上MLP 也可能得到不错结果。因此“MLP 不能处理图片”是错误说法。更准确的表述是直接展平把二维坐标压进一个长特征维后续Linear看见的是 784 个位置没有显式规定相邻像素应该一起处理。图片稍微平移后同一个局部形状会落到另一组输入位置全连接层需要用大量独立参数重新学习相应关系。CNN 引入了适合图像的结构偏好CNN 主要使用两种设计局部连接Local Connectivity一次卷积只查看一个局部窗口权重共享Weight Sharing同一组卷积核权重在不同空间位置重复使用。它们不是“证明所有图像任务必须用 CNN”的定理而是与常见图像规律非常匹配的结构偏好。四、从一次局部乘加理解卷积卷积核是什么卷积核Kernel也常称 Filter是一个较小的权重张量。先看单通道3×3情况卷积核覆盖输入的一个3×3局部区域对应位置相乘再把九个乘积相加。如果卷积层带偏置还要加上偏置值。一个输出位置可以写成这个公式要解决的问题是如何把一个局部窗口压缩成一个响应值。各符号含义如下手算一个输出位置局部输入为1 2 3 4 5 6 7 8 9固定卷积核为1 0 -1 1 0 -1 1 0 -1不使用偏置时对应位置乘加为1×12×03×(−1)4×15×06×(−1)7×18×09×(−1)−6-6表示这个固定 kernel 对当前局部区域的响应。这个经典 kernel 可以帮助观察左右方向的数值差异但不能据此声称真实 CNN 的某一层一定会学习出完全相同的“边缘检测器”。真实网络中的 kernel 通常从初始化值出发通过反向传播和优化器自动学习。用固定权重的 Conv2d 验证手算下面同时验证单个位置和完整5×5 → 3×3滑动结果。Conv2d的输入必须包含[N,C,H,W]四个维度所以小矩阵要变成[1,1,H,W]。import torch from torch import nn local_patch torch.tensor( [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], ] ) kernel torch.tensor( [ [1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, -1.0], ] ) manual_one_position (local_patch * kernel).sum() image torch.tensor( [ [1.0, 1.0, 1.0, 0.0, 0.0], [1.0, 1.0, 1.0, 0.0, 0.0], [1.0, 1.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0, 1.0], ] ) # 用循环完成同样的滑动乘加输出为 3×3。 manual_feature_map torch.empty(3, 3) for row in range(3): for column in range(3): patch image[row:row 3, column:column 3] manual_feature_map[row, column] (patch * kernel).sum() conv nn.Conv2d( in_channels1, out_channels1, kernel_size3, biasFalse, ) # copy_ 会修改参数值因此放进 no_grad避免记录无用的梯度操作。 with torch.no_grad(): conv.weight.copy_(kernel.reshape(1, 1, 3, 3)) torch_one_position conv(local_patch.reshape(1, 1, 3, 3)).squeeze() torch_feature_map_4d conv(image.reshape(1, 1, 5, 5)) torch_feature_map torch_feature_map_4d.squeeze() print(manual one position:, manual_one_position.item()) print(PyTorch one position:, torch_one_position.item()) print(one position equal:, torch.allclose(manual_one_position, torch_one_position)) print(manual feature map:) print(manual_feature_map) print(PyTorch feature map:) print(torch_feature_map) print(full feature map equal:, torch.allclose(manual_feature_map, torch_feature_map)) print( Conv2d input/output:, (1, 1, 5, 5), tuple(torch_feature_map_4d.shape), )实际输出manual one position: -6.0 PyTorch one position: -6.0 one position equal: True manual feature map: tensor([[ 0., 3., 3.], [ 0., 1., 1.], [ 0., -1., -1.]]) PyTorch feature map: tensor([[ 0., 3., 3.], [ 0., 1., 1.], [ 0., -1., -1.]]) full feature map equal: True Conv2d input/output: (1, 1, 5, 5) (1, 1, 3, 3)手算与 PyTorch 在单个位置和完整特征图上都一致。这里特意设置biasFalse因为手算没有加偏置若保留默认偏置就必须把它也加入手算。严格从数学术语看PyTorchConv2d执行的是不翻转 kernel 的互相关cross-correlation。深度学习领域仍普遍把这种运算称为卷积kernel 会通过训练学习所以这项差异不影响本文的结构主线。五、滑动之后得到 Feature Map卷积核怎样移动对于5×5输入、3×3kernel、stride1、padding0kernel 先覆盖左上角3×3区域计算一个数然后向右移动一个位置再计算下一个数。一行结束后向下移动直到所有合法位置处理完毕。横向有 3 个合法位置纵向也有 3 个合法位置所以最终得到3×3输出。代码中的两个特征图正是完整滑动结果。什么是特征图一个卷积核在输入的所有位置完成计算后得到的输出矩阵称为特征图Feature Map。它记录同一组权重在不同空间位置的响应。响应为正、为负或接近零本身没有固定语义要结合卷积核、后续激活函数和训练任务理解。真实 CNN 中不同 kernel 会在训练中形成不同的特征表示。六、局部连接和权重共享为什么重要局部连接如果把224×224×3图片展平就会得到 150528 个输入。全连接层的每个输出神经元都连接全部输入。卷积层则先用3×3、5×5等局部窗口处理邻域这就是局部连接。边缘、角点和简单纹理通常首先由局部像素关系形成所以这种连接方式与图片结构相符。随着网络加深后层可以继续组合前层已经提取的局部信息。权重共享一个3×3kernel 在图片左侧和右侧滑动时使用的是同一组权重而不是为每个位置创建一套新参数。这就是权重共享。它让同一种局部模式出现在不同位置时可以由同一个 filter 产生响应。权重共享能减少参数但不能被夸大成“CNN 对任意平移完全不变”。边缘处理、下采样、网络结构和训练数据都会影响最终的位置鲁棒性。用真实参数量比较把28×28灰度图展平后连接 32 个 Linear 输出784×323225120使用 32 个3×3卷积核处理单通道输入32×1×3×332320实测结果也是 25120 和 320卷积层只有前者约1/78.5的参数。这个比较说明参数共享的效果但两个层的输出形式并不相同Linear 得到 32 个全局数值卷积得到 32 张空间特征图因此它不是模型性能的一对一比较。七、输入通道、输出通道与多个卷积核一个输出 filter 必须覆盖所有输入通道RGB 输入的单张图片 shape 是[3,H,W]。一个输出 filter 不能只看其中一个颜色通道而是包含三个3×3权重切片分别作用于 R、G、B 对应局部区域然后把三个通道的结果相加再加该输出通道的偏置。因此一个标准二维卷积层的 weight shape 是[out_channels,in_channels,kernel_height,kernel_width]例如in_channels3、out_channels16、kernel_size3时[16,3,3,3]这表示有 16 个输出 filters每个 filter 都跨越 3 个输入通道并在每个通道上拥有3×3权重。每个输出 filter 产生一张特征图所以输出通道数是 16。深层通道不再等同于颜色第一层输入的 3 个通道可以对应 RGB。第一层输出的 16 个通道则是模型学习到的 16 组特征表示不是 16 种颜色。下一层若写Conv2d(16,32,...)它会把这 16 张输入特征图共同作为输入再产生 32 张新特征图。八、正式认识 nn.Conv2d关键参数一个基础卷积层可以写成import torch from torch import nn torch.manual_seed(42) conv nn.Conv2d( in_channels3, out_channels16, kernel_size3, stride1, padding0, biasTrue, ) x torch.randn(8, 3, 32, 32) y conv(x) print(weight:, conv.weight.shape) print(bias:, conv.bias.shape) print(input:, x.shape) print(output:, y.shape) print(parameters:, sum(parameter.numel() for parameter in conv.parameters()))这段代码可以独立运行各参数含义如下in_channels3输入有 3 个通道out_channels16学习 16 个输出 filters产生 16 个输出通道kernel_size3空间窗口为3×3stride1每次沿高度或宽度移动 1 个位置padding0输入四周不补值biasTrue每个输出通道学习一个偏置这是默认设置。实际输出weight: torch.Size([16, 3, 3, 3]) bias: torch.Size([16]) input: torch.Size([8, 3, 32, 32]) output: torch.Size([8, 16, 30, 30]) parameters: 448参数量为[ 16\times3\times3\times316448 ]前半部分是 weight 的 432 个参数后面的 16 是 bias。这里使用默认groups1分组卷积属于后续内容本篇不展开。九、Kernel、Stride、Padding 共同决定输出尺寸先理解三个参数Kernel Size决定一次查看多大的局部区域。本篇主要使用3×3但 CNN 并非只能使用 3也存在1×1、5×5、7×7等大小。Stride步幅决定 kernel 每次移动多少位置。stride1是逐格移动stride2是每次跨两个位置输出空间尺寸通常缩小得更快计算量也会下降但可能损失更多细节。Padding填充在输入边缘补充数值。基础Conv2d的默认padding_modezeros可以理解为补 0。kernel_size3、stride1、padding1时常能保持 H/W 不变。Padding 可以让边缘附近参与更多窗口计算但不能保证完全消除边缘信息损失。常用输出尺寸公式在本篇默认dilation1的情况下单个空间维度的输出大小为公式用于分别计算输出高度和宽度其中(I)输入高度或宽度(K)对应方向的 kernel size(P)对应方向两侧使用的 padding 数量(S)stride(O)输出高度或宽度例如I32、K3、P0、S1所以前面的32×32经过卷积后变成30×30因为不允许3×3kernel 超出输入边缘。若考虑 dilation空洞间隔更完整的单维公式是(D) 是 dilation。本篇所有实算都使用默认 (D1)只保留完整公式帮助以后查阅不展开空洞卷积。四组手算与 PyTorch 验证输入 (I)Kernel (K)Stride (S)Padding (P)公式输出PyTorch 输出 H/W323103030×30323113232×32323211616×16285102424×24第三行的计算为具体 PyTorch 验证包含在后面的综合基础层代码中。十、Pooling汇总局部信息并缩小 H/WMax Pooling 怎样计算池化Pooling会在局部区域中按照固定规则汇总信息。最常见的入门例子是最大池化Max Pooling。对于1 3 2 42×2最大池化取出最大值 4。若使用kernel_size2, stride2窗口通常不会重叠H/W 会大致减半。nn.MaxPool2d对每个通道分别处理因此基础设置下不改变N和C只缩小H与W[8,16,32,32] → MaxPool2d(2,2) → [8,16,16,16]Pooling 有什么作用和限制最大池化可以缩小后续特征图降低计算量汇总局部区域中的强响应可能增强对一定局部位置变化的鲁棒性。“可能增强局部鲁棒性”不等于“保证完全平移不变”。过度池化还会快速丢失空间细节。Pooling 也不是所有 CNN 必须使用的固定组件一些网络会用stride1的卷积完成下采样。MaxPool 按固定规则取最大值没有需要训练的 weight 或 bias因此参数量为 0。十一、ReLU 与经典 CNN Block卷积对输入执行的仍然是线性或仿射运算。如果只连续堆叠卷积层而没有非线性激活多层结构的表达能力会受到限制。ReLU 对每个元素执行max(0,x)为网络加入非线性并保持输入 shape 不变。一个经典但并非唯一的基础模块是Conv2d → ReLU → MaxPool2d下面的完整代码集中验证参数量、四组输出尺寸、Pooling、ReLU 和一个 CNN Blockimport torch from torch import nn torch.manual_seed(42) def count_parameters(module: nn.Module) - int: return sum(parameter.numel() for parameter in module.parameters()) # Linear 与卷积层的参数数量对比 linear nn.Linear(28 * 28, 32) conv_gray nn.Conv2d(1, 32, kernel_size3) print(Linear parameters:, count_parameters(linear)) print(Conv parameters:, count_parameters(conv_gray)) # RGB Conv2d 的 weight、bias、输入和输出 conv_rgb nn.Conv2d(3, 16, kernel_size3) conv_input torch.randn(8, 3, 32, 32) conv_output conv_rgb(conv_input) print(Conv weight:, conv_rgb.weight.shape) print(Conv bias:, conv_rgb.bias.shape) print(Conv parameters:, count_parameters(conv_rgb)) print(Conv input/output:, conv_input.shape, conv_output.shape) # 用公式与 PyTorch 同时验证 K/S/P 对输出尺寸的影响 cases [ (32, 3, 1, 0), (32, 3, 1, 1), (32, 3, 2, 1), (28, 5, 1, 0), ] for input_size, kernel, stride, padding in cases: layer nn.Conv2d( 3, 4, kernel_sizekernel, stridestride, paddingpadding, ) inputs torch.randn(2, 3, input_size, input_size) outputs layer(inputs) formula_size (input_size 2 * padding - kernel) // stride 1 print( KSP:, (input_size, kernel, stride, padding), formula:, formula_size, output:, outputs.shape, ) # MaxPool、ReLU 与一个完整 Block pool nn.MaxPool2d(kernel_size2, stride2) pool_input torch.randn(8, 16, 32, 32) pool_output pool(pool_input) print(Pool input/output:, pool_input.shape, pool_output.shape) print(Pool parameters:, count_parameters(pool)) relu nn.ReLU() relu_output relu(pool_input) print(ReLU input/output:, pool_input.shape, relu_output.shape) print(ReLU parameters:, count_parameters(relu)) block nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(kernel_size2, stride2), ) block_input torch.randn(4, 3, 32, 32) block_output block(block_input) print(Block input/output:, block_input.shape, block_output.shape)实际输出Linear parameters: 25120 Conv parameters: 320 Conv weight: torch.Size([16, 3, 3, 3]) Conv bias: torch.Size([16]) Conv parameters: 448 Conv input/output: torch.Size([8, 3, 32, 32]) torch.Size([8, 16, 30, 30]) KSP: (32, 3, 1, 0) formula: 30 output: torch.Size([2, 4, 30, 30]) KSP: (32, 3, 1, 1) formula: 32 output: torch.Size([2, 4, 32, 32]) KSP: (32, 3, 2, 1) formula: 16 output: torch.Size([2, 4, 16, 16]) KSP: (28, 5, 1, 0) formula: 24 output: torch.Size([2, 4, 24, 24]) Pool input/output: torch.Size([8, 16, 32, 32]) torch.Size([8, 16, 16, 16]) Pool parameters: 0 ReLU input/output: torch.Size([8, 16, 32, 32]) torch.Size([8, 16, 32, 32]) ReLU parameters: 0 Block input/output: torch.Size([4, 3, 32, 32]) torch.Size([4, 16, 16, 16])这组结果验证了三个容易混淆的事实Conv 通常改变通道和空间尺寸ReLU 不改变 shape基础 MaxPool 不改变通道但会缩小 H/W。ReLU 和 MaxPool 都没有可训练参数。十二、多层卷积与感受野感受野的直觉感受野Receptive Field表示某个特征位置会受到原输入多大区域的影响。第一层3×3卷积的一个输出位置直接查看原图3×3区域。再叠加一个3×3、stride1、无 dilation 的卷积第二层一个位置会通过第一层间接依赖原图5×5区域。原因是第二层查看第一层相邻的3×3输出而这些第一层位置各自又查看原图3×3区域相邻区域重叠后覆盖范围扩大到5×5。更深层、stride 和 pooling 都会继续影响感受野。本篇只建立这个直觉不展开通用递推公式。层级特征不等于每层都有固定名字浅层可能对颜色变化、边缘和简单纹理产生响应后层可以继续组合前层信息形成更复杂的表示。可以用下面的概念链帮助理解像素 → 局部变化 → 纹理或简单形状 → 更复杂组合 → 分类依据但不能机械断言“第一层一定检测边缘、第二层一定检测眼睛、第三层一定检测猫脸”。真实网络学习到的表示受数据、结构和训练目标影响未必能为每个通道赋予清晰的人类名称。为什么常见 H/W 变小而通道变多基础 CNN 经常让空间尺寸逐步减小例如32×32 → 16×16 → 8×8同时让通道数从3 → 16 → 32增加。直觉上空间下采样减少后续计算而更多通道允许模型保留更多种特征表示。这是常见设计模式不是所有 CNN 必须遵守的固定规则。十三、搭建一个完整但不训练的 SimpleCNN网络结构与逐层 shape本篇的基础网络只完成前向计算[N,3,32,32] → Conv 3→16padding1 → [N,16,32,32] → ReLU → MaxPool → [N,16,16,16] → Conv 16→32padding1 → [N,32,16,16] → ReLU → MaxPool → [N,32,8,8] → Flatten → [N,2048] → Linear → [N,10]为了让每一步可见下面的教学版forward()接受traceTrue。正式模型通常不在每次前向传播中打印这些内容。import torch from torch import nn torch.manual_seed(42) class SimpleCNN(nn.Module): def __init__(self) - None: super().__init__() self.conv1 nn.Conv2d(3, 16, kernel_size3, padding1) self.relu1 nn.ReLU() self.pool1 nn.MaxPool2d(kernel_size2, stride2) self.conv2 nn.Conv2d(16, 32, kernel_size3, padding1) self.relu2 nn.ReLU() self.pool2 nn.MaxPool2d(kernel_size2, stride2) self.classifier nn.Linear(32 * 8 * 8, 10) def forward(self, inputs: torch.Tensor, trace: bool False) - torch.Tensor: if trace: print(input:, inputs.shape) outputs self.conv1(inputs) if trace: print(conv1:, outputs.shape) outputs self.relu1(outputs) if trace: print(relu1:, outputs.shape) outputs self.pool1(outputs) if trace: print(pool1:, outputs.shape) outputs self.conv2(outputs) if trace: print(conv2:, outputs.shape) outputs self.relu2(outputs) if trace: print(relu2:, outputs.shape) outputs self.pool2(outputs) if trace: print(pool2:, outputs.shape) outputs torch.flatten(outputs, start_dim1) if trace: print(flatten:, outputs.shape) logits self.classifier(outputs) if trace: print(logits:, logits.shape) return logits model SimpleCNN() example_images torch.randn(4, 3, 32, 32) logits model(example_images, traceTrue) print(named parameters:) for name, parameter in model.named_parameters(): print(name, tuple(parameter.shape), parameter.numel()) total_parameters sum(parameter.numel() for parameter in model.parameters()) print(total parameters:, total_parameters)实际输出input: torch.Size([4, 3, 32, 32]) conv1: torch.Size([4, 16, 32, 32]) relu1: torch.Size([4, 16, 32, 32]) pool1: torch.Size([4, 16, 16, 16]) conv2: torch.Size([4, 32, 16, 16]) relu2: torch.Size([4, 32, 16, 16]) pool2: torch.Size([4, 32, 8, 8]) flatten: torch.Size([4, 2048]) logits: torch.Size([4, 10]) named parameters: conv1.weight (16, 3, 3, 3) 432 conv1.bias (16,) 16 conv2.weight (32, 16, 3, 3) 4608 conv2.bias (32,) 32 classifier.weight (10, 2048) 20480 classifier.bias (10,) 10 total parameters: 25578输入 4 张 RGB32×32模拟图片输出 shape 为[4,10]。每一行有 10 个 logits它们是分类头对 10 个类别给出的原始分数。本篇没有定义损失函数也没有调用backward()或优化器所以网络没有训练。十四、为什么卷积之后又可以 Flatten文章开头说直接展平原图没有显式利用二维结构。为什么现在又调用torch.flatten(outputs, start_dim1)两种位置的含义不同一开始展平原图空间特征还没有经过局部连接和权重共享处理卷积特征提取后展平前面的卷积、ReLU 和池化已经整合了局部空间信息此时把最终特征图整理成一行便于分类头汇总。start_dim1保留第 0 维 batch把后面的[32,8,8]合并为32×8×82048因此[4,32,8,8]变为[4,2048]。Flatten 不会删除元素只改变组织 shape 的方式。随后nn.Linear(2048,10)把每个样本的 2048 个特征汇总成 10 个 logits。CNN 并不排斥 Linear许多经典分类网络都会在特征提取部分之后使用分类头也存在全局平均池化等其他设计本篇不展开。十五、哪些层有参数参数量怎样组成层基础情况下怎样改变 shape是否有可训练参数Conv2d改变通道数H/W 由 K/S/P 决定是weight 和可选 biasReLU不改变 shape否MaxPool2d通常保持 N/C、缩小 H/W否Flatten合并指定维度不删除元素否Linear把最后特征维变成out_features是weight 和可选 biasSimpleCNN 的参数来自三个层conv116×3×3×316448conv232×16×3×3324640classifier10×20481020490。总参数量为44846402049025578model.named_parameters()的实际输出与逐层手算一致。注意ReLU、MaxPool 和 Flatten 虽然参与前向数据流却不会出现在named_parameters()中因为它们没有需要学习的 weight 或 bias。十六、Shape 练习先计算再让 PyTorch 检查练习 1输入[16,3,64,64] Conv2d(3,32,kernel_size3,stride1,padding1)H/W 保持 64输出通道为 32所以答案是[16,32,64,64]练习 2输入[16,32,32,32] Conv2d(32,64,kernel_size3,stride2,padding1)公式得到 H/W 为 16输出通道为 64所以答案是[16,64,16,16]练习 3输入[16,64,16,16] MaxPool2d(kernel_size2,stride2)Batch 和 Channel 不变H/W 减半所以答案是[16,64,8,8]下面用独立代码验证三道题import torch from torch import nn exercise_1 nn.Conv2d( 3, 32, kernel_size3, stride1, padding1, )(torch.randn(16, 3, 64, 64)) exercise_2 nn.Conv2d( 32, 64, kernel_size3, stride2, padding1, )(torch.randn(16, 32, 32, 32)) exercise_3 nn.MaxPool2d( kernel_size2, stride2, )(torch.randn(16, 64, 16, 16)) print(exercise 1:, exercise_1.shape) print(exercise 2:, exercise_2.shape) print(exercise 3:, exercise_3.shape)实际输出exercise 1: torch.Size([16, 32, 64, 64]) exercise 2: torch.Size([16, 64, 16, 16]) exercise 3: torch.Size([16, 64, 8, 8])检查 CNN shape 时可以固定按照N → C → H → W的顺序先确认 Batch 是否保留再看输出通道由谁决定最后用公式计算 H/W。这样比凭直觉猜四个数字可靠得多。十七、CNN 与 MLP 的核心区别对比项MLPCNN常见输入[N,F][N,C,H,W]连接方式全连接局部连接空间位置的权重不同位置通常有独立权重同一 kernel 在空间位置共享空间结构常先展平在特征提取阶段保留 H/W基础参数层LinearConv2d常见应用表格或一般特征图像及其他空间数据这张表描述的是常见使用方式不是排他的边界。MLP 可以处理图片CNN 也不只用于图片一维卷积可以处理序列三维卷积可以处理体数据。本系列当前只聚焦二维图像。CNN 的优势也不是“参数越少就一定更准”。局部连接和权重共享加入了适合图像的结构假设往往能用更高效的参数组织方式学习空间模式实际效果仍取决于数据、任务、结构和训练过程。十八、常见问题与排查1. 为什么 Conv2d 提示输入维度不对批量输入通常需要[N,C,H,W]。如果单张图片只有[C,H,W]可以用x.unsqueeze(0)增加 batch 维得到[1,C,H,W]。还要检查是否误用了[N,H,W,C]。2. 为什么 RGB 图片的in_channels是 3因为原始输入在同一空间位置包含 R、G、B 三个颜色值。卷积层的in_channels必须与输入 Tensor 的C一致。3. 灰度图为什么通常使用in_channels1灰度图每个空间位置只有一个强度值。即使原始数据暂时是[H,W]进入 Conv2d 时也通常整理为[1,H,W]批量后为[N,1,H,W]。4.out_channels16表示 16 种颜色吗不是。它表示这一层使用 16 个输出 filters产生 16 张特征图。只有原始 RGB 输入的前三个通道有明确的红、绿、蓝含义。5. 一个输出 filter 为什么要覆盖所有输入通道一个输出特征需要综合同一局部位置的全部输入表示。标准groups1卷积中每个输出 filter 的 shape 是[in_channels,kH,kW]。6. 卷积后为什么 Channel 变成out_channels每个输出 filter 产生一个输出通道。16 个 filters 对同一输入执行各自的局部计算就得到 16 张输出特征图。7. Conv2d 的 H/W 为什么变小了检查kernel_size、stride、padding和dilation。最常见原因是kernel_size3, stride1, padding0此时每个空间维度减少 2。8. 为什么padding1能让 3×3 卷积保持尺寸在stride1, dilation1时输入两侧各补一层抵消3×3kernel 带来的空间缩小。若 stride 或 dilation 改变就不能继续套用这个结论。9. Stride 越大越好吗不是。较大 stride 可以更快缩小特征图和减少计算但也会更稀疏地采样空间信息可能损失细节。10. Kernel 越大越好吗不是。更大 kernel 查看范围更大但参数量和计算量也更高。3×3很常见不代表它在所有任务中都是唯一选择。11. 卷积核里的数值是谁设置的训练开始前由初始化方法给出。正常训练时它们与 Linear 权重一样通过loss.backward()得到梯度再由优化器更新。本文的固定 kernel 只用于核对手算。12. Conv2d 在图片每个位置使用不同 kernel 吗同一个输出 filter 在所有空间位置共享同一组权重。不同输出通道拥有不同 filters。13. Feature Map 的每个数都代表什么它表示某个 filter 在对应局部位置的响应。响应的具体语义由训练得到的权重和后续网络决定不能脱离上下文强行命名。14. MaxPool 会改变 Channel 吗基础MaxPool2d对各通道分别处理通常保持 N/C 不变只根据 kernel、stride 等设置改变 H/W。15. Pooling 越多越好吗不是。过度下采样会快速丢失空间细节而且现代网络也可能用 stride 卷积替代部分池化。16. MaxPool 和 ReLU 为什么没有出现在named_parameters()中它们执行固定规则没有需要训练的 weight 或 bias。没有参数不等于没有作用它们仍然会改变数值或 shape。17. ReLU 会改变 H/W 或 Channel 吗普通 ReLU 是逐元素运算因此输入和输出 shape 相同。它改变负值而不是维度。18. Flatten 会不会删除数据不会。Flatten 只是合并维度。把[N,32,8,8]变成[N,2048]后元素总数不变但 H/W 不再作为独立维度存在。19. 为什么开头不能随意 Flatten后面却可以前者在空间特征提取前就压平原图后者是在卷积已经利用局部结构后为分类头重新组织高层特征。两者发生的位置和作用不同。20. CNN 完全不需要 Linear 吗不是。许多经典 CNN 使用 Linear 分类头也有网络使用全局平均池化等其他设计。本文采用最直观的基础结构。21. CNN 能保证识别出现在任意位置的目标吗不能这样保证。权重共享让同一个局部模式可在不同位置被同一 filter 检测但完整的位置鲁棒性还受到 padding、stride、pooling、数据和训练过程影响。22. 为什么模型输入 32×32换成 64×64 后 Linear 报 shape 错误本文分类器固定接收32×8×82048个特征这是根据32×32输入计算的。输入尺寸改变后最终 H/W 也可能改变需要重新设计分类头或使用能适应尺寸变化的结构本篇不提前展开这些方案。本章小结本文从图片的二维空间结构出发完成了 CNN 的第一次前向实践MLP 可以处理图片但直接 Flatten 没有显式利用像素的邻域与位置关系CNN 通过局部连接先观察局部区域通过权重共享在不同位置复用同一组 kernel卷积核中的数值通常是可训练参数滑动后形成 Feature MapPyTorchConv2d的批量输入通常是[N,C,H,W]标准卷积的 weight shape 是[out_channels,in_channels,kH,kW]一个输出 filter 跨越全部输入通道并产生一个输出通道Kernel Size、Stride 和 Padding 共同决定输出 H/WReLU 提供非线性且保持 shapeMaxPool 可以汇总局部信息并缩小 H/W多层卷积逐步扩大感受野让后层能够整合更大范围的信息卷积特征提取完成后可以 Flatten 并通过 Linear 得到分类 logits实测 SimpleCNN 把[4,3,32,32]转换为[4,10]共有 25578 个可训练参数。现在我们已经能解释 CNN 为什么适合图片也能逐层检查一个基础网络的前向 shape但还没有真正加载图片和训练分类器。下一篇将把图像 Dataset、Transform、数据增强、CNN 训练、验证与测试组合成第一个完整图像分类项目。