资讯动态

PyTorch卷积层与池化层详解:从原理到参数与代码实践

发布时间:2026/10/13 21:49:23 来源:尧图企业网站定制
Pytorch 的卷积层和池化层我在文章标题里写的是“零基础入门第六篇”但其实这俩概念才是真正迈进深度学习图像处理门槛的第一步。前面那些张量操作、线性层、激活函数都是基本功卷积和池化一上手你才算真正开始“看”图像。这篇我打算用一个比较特别的组织方式先不讲 nn.Conv2d 的 API 怎么填参数而是先带你搞清楚卷积在解决什么问题池化在解决什么问题然后再回到代码里把每一个参数、每一次维度变化都对上号。这样安排是因为我发现大部分初学者卡住的点根本不是代码不会写而是不知道这层网络究竟在做什么。等你把“它在做什么”想明白了PyTorch 那几行 API 调用基本就是水到渠成的事。1. 内容整体设计与思路拆解1.1 为什么到了第六篇才引入卷积层我们之前几篇已经折腾过全连接网络。你用全连接网络做过手写数字识别的话一定有个很直观的感受模型参数量大训练慢而且对图像中的位置变化特别敏感。同样的数字挪动几个像素识别结果可能就变了这其实暴露了全连接层的一个致命缺陷——它根本不关心像素之间的空间关系。全连接层是把图像“拉平”成长长的向量比如一张 32x32 的图拉平就是 1024 个数字然后全部灌进神经元里。这样一来每个输出神经元都要和这 1024 个输入相连参数数量一下就爆炸了。如果图像变成 256x256你都不敢想参数会膨胀到什么程度。卷积层的设计哲学完全不一样。它用一个小窗口——通常叫卷积核——在图像上滑动每次都只看窗口内的那一小块区域。这个设计说白了就是模拟人眼看图的方式先看局部再看整体。局部的纹理、边缘、角点逐渐组合成更高层的语义信息。这也是为什么标题里要专门拿出来讲卷积层它是计算机视觉大厦的地基。我在实际教学中发现很多同学一上来就去背 nn.Conv2d 的参数列表效果很不好。我建议的路径是先理解卷积核为什么小stride 为什么存在padding 为什么要有然后再去看代码你会发现代码里的每个参数你都认识而不是在抄作业。1.2 卷积层与池化层的分工逻辑这两个层经常被放一起讲是因为它们总是成对出现一个负责提取特征一个负责压缩信息。你可以把卷积层想象成放大镜它在一张图上反复扫描找出边缘、颜色变化、纹理等局部特征池化层则是压缩器它把放大镜扫出来的结果“浓缩”一下保留关键信息扔掉次要细节。为什么要压缩因为卷积层输出的特征图虽然已经有意义但尺寸还是太大了。如果堆很多层卷积特征图一直保持原尺寸计算量会指数级上升。池化就把每个小区域里的信息提炼成一个代表值最常见的是取最大值也就是最大池化。这样做还有一个额外收益模型对微小的位置变化不再那么敏感稍微平移几个像素池化后的结果差异很小这就是所谓的平移不变性。当然你也可以说我用 stride2 的卷积层也能缩小特征图为什么要单独搞一个池化层理论上确实可以现代很多网络架构也确实这么干。但对零基础入门来说池化层简单、直观、没有参数是最容易理解的下采样手段。先把这个基础打牢后面你再看到 strided convolution 也不会慌。2. 核心细节解析与实操要点2.1 卷积核到底是什么怎么理解 kernel_size如果你第一次接触“卷积核”这个词可能会觉得很高深。其实它就是一个小矩阵比如 3x3、5x5里面装了一堆数字。这个矩阵在图像上滑动每次覆盖图像的一小块把对应位置的像素值相乘后累加得到一个输出值。窗口滑动一步的距离就叫 stride中文常称为步长。举个自然语言的类比你读一段英文的时候眼睛每次聚焦三五个单词读完一个窗口再移到下一组不会把整页文字一次性塞进脑子里。卷积核就是那个“眼睛的聚焦范围”kernel_size3 表示每次只看 3x3 这个范围内的信息看完再往外扩大视野。这里有一个必须强调的点卷积核里面的数字不是人设计的而是网络在训练过程中自己学出来的。刚开始可能是随机数经过反向传播不断调整最后会形成能识别边缘、纹理、甚至更复杂模式的滤波器。我在带学生做实验时喜欢做一件事训练完一个简单的 CNN 后把第一层卷积核的可视化结果打印出来你会看到它们大多演化成了类似边缘检测器、颜色过滤器的模式特别直观。卷积核的选择直接影响你能提取到什么层次的特征。小卷积核比如 3x3 适合提取精细的局部纹理大卷积核比如 7x7 感受野更大能看到更宏观的结构但计算量也会上去。实践中 3x3 用得最多因为堆叠两层 3x3 卷积就能达到 5x5 的感受野但参数量更少非线性表达能力更强。2.2 卷积层参数逐个拆解从 in_channels 到 biasnn.Conv2d 的完整签名常见版本是这样torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride1, padding0, dilation1, groups1, biasTrue, padding_modezeros)逐个说清楚这些参数每一个都对应一个真实的设计决策。in_channels 是输入特征图的通道数。灰度图是 1RGB 彩色图是 3。如果你前面的网络层输出了 64 个通道那这里就填 64。初学者最容易在这类参数上犯错稍后我会专门讲一个典型的坑。out_channels 是这一层卷积输出的通道数也决定了这一层用了多少个卷积核。每个卷积核会扫一遍输入产生一张特征图。所以 out_channels16 就意味着输出有 16 张特征图也就是 16 个通道。这个数字是你自己定的一般会随着网络加深而增加因为它表示网络在这一层提取了多少种不同的特征。kernel_size 前面已经说了可以是单个整数比如 3表示 3x3 的卷积核也可以是元组比如 (3, 5)表示高 3、宽 5 的卷积核。非方形卷积核在特殊场景有用比如处理宽屏图像的某些特征但入门阶段用方形的就足够了。stride 控制卷积核滑动的步幅。stride1 是逐像素扫过特征图尺寸几乎不变stride2 是每隔一个像素扫一次输出尺寸大约缩小一半。调大 stride 是缩小特征图的直接手段之一而且顺便省了计算量。padding 是在输入四周补一圈数字最常用的是补 0。为什么需要补 0因为卷积核滑到边缘的时候窗口会有部分超出图像范围不补点什么就没法算了。更重要的是padding 能控制输出特征图的尺寸。比如输入 5x5、kernel_size3、padding1输出仍然是 5x5这就是 SAME padding 的朴素实现。dilation 是空洞卷积的参数意思是卷积核的每个采样点之间隔几个像素。dilation1 就是普通卷积dilation2 相当于把 3x3 的卷积核撑大到覆盖 5x5 的区域但中间有空洞。这个主要用于需要扩大感受野又不想增加参数量的场景比如语义分割网络。入门阶段先用默认的 1 就行。groups 是分组卷积的参数。groups1 是普通卷积groupsin_channels 就变成了深度可分离卷积每个输入通道独自用一个卷积核这在轻量级网络里很常见。初学者先知道有这么回事不需要深究。bias 默认是 True表示每个输出通道有一个可学习的偏置项。如果卷积层后面马上接 BNBatchNorm层通常可以把 bias 设成 False因为 BN 会做平移bias 是冗余的。这个技巧很多开源代码里都会出现自己写网络时也可以这么处理。2.3 输出尺寸计算公式必须背下来的核心数学这部分几乎没有商量的余地输出特征图的尺寸由这个公式决定输出尺寸 floor((输入尺寸 - dilation * (kernel_size - 1) - 1 2 * padding) / stride) 1我把这个公式拆开解释一下别死记硬背理解了就永远不会忘。假设输入是 H_in卷积核大小是 Kpadding 是 Pstride 是 S。在 padding 之后实际上参与计算的区域变成了 H_in 2P。卷积核在滑动过程中覆盖过的范围除以步长决定了一共能落多少个位置。你回想一下在一个数轴上有两个端点从起点开始每隔 S 步取一个点能取多少个点就是 (总范围 - 窗口大小) / S 1。卷积核在图上滑的过程本质也是这样。我建议用一个小例子来验证。输入是 32x32kernel_size3padding1stride1。先按公式算(32 2*1 - 3) / 1 1 32。你会发现输出还是 32x32这就是 padding1 配合 kernel_size3 的效果。如果同样的配置padding0输出就变成 (32 - 3) / 1 1 30图像被缩了一圈。stride2、kernel_size3、padding1 的典型配置(32 2 - 3) / 2 1 16.5这里要取整。很多框架是向下取整PyTorch 也是这个行为实际输出就是 16。所以在设计网络时如果计算出来是小数一般会调整 padding 或卷积核大小保证各层尺寸是整数这样网络结构更规整。另外一个常见的等价写法是out (in 2*pad - k) // stride 1在 kernel_size、dilation 等于默认值的时候可以用这个简化版但完整公式还是要会在需要时查回来。2.4 池化层的细节MaxPool 与 AvgPool 怎么选PyTorch 里最常用的池化是 nn.MaxPool2d它把特征图分成一个个不重叠的小窗口每个窗口取最大值。比如 kernel_size2、stride2就会把特征图的宽高各缩小一半。为什么取最大值而不是平均值或最小值因为卷积层的输出在激活之后值越大代表这个位置的响应越强烈也就是特征越明显。保留最大值意味着保留最“强”的特征丢弃其他响应弱的区域。对应的还有 nn.AvgPool2d它会求窗口内的平均值。平均池化更平滑不会因为某个极值点导致输出异常因此在一些需要全局信息的任务中也会用到。比如在分类网络的最后经常用全局平均池化代替全连接层就是直接把整张特征图求平均变成一个向量这个操作对过拟合有不错的抑制效果。选择建议是隐藏层默认用最大池化它保特征能力强最后一层如果要做全局特征聚合优先考虑平均池化。我这几年见的网络结构里95% 的隐藏层池化都是 MaxPool你先把最大池化整明白平均池化其实就是换一个聚合函数的事。池化层还有一个参数叫 ceil_mode默认是 False。它控制的是窗口滑到边缘时超出输入范围怎么办。False 表示只取完整落在输入内的窗口边缘不够一个窗口就舍弃True 表示只要窗口有一部分落在输入内就保留不足部分用忽略的方式处理。这个参数在输入尺寸不对称时需要特别注意有时候会影响特征图的最终尺寸差一个像素。3. 实操过程与核心环节实现3.1 从零搭建一个“卷积 池化”特征提取模块现在真正到代码环节。我们用一个模拟项目来验证前面讲的所有概念项目目标很明确搭建一个小型卷积特征提取模块对一张类似 64x64 的模拟图像数据做处理观察每一层之后张量形状的变化。先确定输入。假设输入是一些 64x64 的 RGB 图像也就是形状是 (batch_size, 3, 64, 64)。我们把 batch_size 设为 2方便演示。import torch import torch.nn as nn # 模拟输入2张 3通道 64x64 的图像 x torch.randn(2, 3, 64, 64) # 第一个卷积块卷积 激活 池化 conv_block1 nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(kernel_size2, stride2) ) out conv_block1(x) print(out.shape) # 期望 (2, 16, 32, 32)跑一下得到 (2, 16, 32, 32)。为什么宽高从 64 变成 32因为 MaxPool2d 的 kernel_size2、stride2把尺寸减半了。为什么通道从 3 变成 16因为卷积层的 out_channels16。接着再加一层卷积块把通道数提到 32继续池化conv_block2 nn.Sequential( nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(kernel_size2, stride2) ) out conv_block2(out) print(out.shape) # 期望 (2, 32, 16, 16)到这里你应该看出来了一个典型的 CNN 特征提取阶段就是不断重复“卷积增加通道、池化缩小尺寸”的过程。通道数从 3 到 16 到 32尺寸从 64 到 32 到 16特征从原始的像素值逐渐变成语义特征。3.2 全部核心参数之后的完整构建演示再看一个更完整的例子把 stride、padding、dilation 都串起来验证。假设你现在要设计一个模拟的小网络对尺寸较大的输入做快速下采样。# 验证 padding 和 stride 对形状的影响 x torch.randn(1, 1, 32, 32) # 普通卷积保持尺寸 conv_same nn.Conv2d(1, 8, kernel_size3, stride1, padding1) print(conv_same(x).shape) # (1, 8, 32, 32) # 卷积自带的降采样 conv_down nn.Conv2d(8, 16, kernel_size3, stride2, padding1) print(conv_down(conv_same(x)).shape) # (1, 16, 16, 16) # 空洞卷积扩大感受野 conv_dilation nn.Conv2d(16, 32, kernel_size3, stride1, padding2, dilation2) print(conv_dilation(conv_down(conv_same(x))).shape) # 计算一下再对比输出第三行需要稍微算一下。输入是 16kernel_size 是 3dilation2padding2stride1。先把 dilation 的影响考虑进去等效卷积核尺寸 2 * (3-1) 1 5。然后套公式(16 2*2 - 5) / 1 1 16。所以输出尺寸是 16x16通道从 16 变成 32。你可以看到空洞卷积在不改变输出尺寸的情况下把感受野从 3x3 扩大到了 5x5这就是它的核心价值。3.3 参数数量与计算量估算少走弯路我给零基础读者一个建议设计网络时最好手动估算一下各层的参数量这样可以提前发现设计不合理的地方。卷积层的参数量计算公式是参数量 (kernel_height * kernel_width * in_channels 1) * out_channels其中的 1 是 bias 项。用上面的 conv_block1 举个例子in_channels3out_channels16kernel3x3。参数量 (3 * 3 * 3 1) * 16 448这个数量非常小对比一下全连接层同样输入 64x64x3 的图像如果直接平铺到 64 个神经元参数量是 64643*64也就是大概 78 万个参数。差距就是这么悬殊这也是卷积层能处理大尺寸图像的根本原因。如果你用的是分组卷积比如 groupsin_channels 的深度可分离卷积参数量会进一步下降。每个通道单独一个卷积核参数量变成 (331 1) * out_channels。这就是很多移动端网络能跑得动的原因。我踩过的一个常见的坑设计网络时以为参数量大没关系结果显存直接爆掉。建议你在每层后面加一个 print(shape)监控特征图尺寸。如果某层输出张量太大可以用池化或增大 stride 来下采样而不是一味堆卷积层。4. 常见问题与排查技巧实录4.1 维度不匹配你大概率会遇到的第一个报错在练习时最常看到的报错就是 size mismatch类似于 “Given groups1, weight of size [16, 3, 3, 3] expected input[2, 3, 64, 64] to have 3 channels, but got 2 channels instead”。这种报错的本质是输入的通道数和你定义的卷积层 in_channels 不一致。常见的发生场景有三个。第一个是你把灰度图当 RGB 图输进去了输入是单通道但卷积层 in_channels 填了 3。第二个是 DataLoader 返回的张量形状是 (高, 宽, 通道)而 PyTorch 卷积层要求的是 (通道, 高, 宽)此时需要写x x.permute(0, 3, 1, 2) # 把通道维挪到第二位或者在数据预处理阶段就用 transforms 处理。第三个更隐蔽你在前面某个卷积层忘了设置 out_channels 和下一层的 in_channels 一致比如第一层输出 16 通道第二层却写了 in_channels32。网络层数一多这类错误就非常容易发生。排查技巧在每层之后打印 x.shape一层一层核对。如果某个地方的通道数与下一层的期望不一致马上就能定位到问题。4.2 padding 和 stride 组合后输出尺寸对不上很多人在算输出尺寸时会遇到这种情况推导结果是 16实际跑出来是 15或者反过来的。PyTorch 的尺寸计算公式是向下取整也就是 (输入尺寸 2*padding - 等效卷积核尺寸) // stride 1多余的边缘会被丢弃。举个例子输入 16kernel_size4stride3padding1。先算分子(16 2*1 - 4) / 3 1 14/3 1 4.67 1取整方式让结果变成 5。实际输出是 5。如果你按 4.671 取整得到 5那没毛病如果先取 4.67 的整数部分是 4再加 1 得到 5也是对的。关键是顺序不能搞错是先整除再加 1不是先算小数再加。如果你想要输出尺寸是精确的一半最简单的方式就是 kernel_size2、stride2或者 kernel_size3、stride2、padding1。这两种组合几乎成了 CNN 里的默认配置。4.3 MaxPool 与 AvgPool 的实际选择场景有不少同学会问到底应该用 MaxPool 还是 AvgPool。这其实取决于你希望保留什么信息。最大池化保的是“有没有”这个特征比如某个位置是否检测到了边缘、纹理等它对光照变化、小噪声的鲁棒性更强。平均池化保的是“平均响应”它会模糊掉极端值更适合需要整体统计特征的场景。给你一个可直接参考的范例图像分类任务里隐藏层用 MaxPool最后接全局平均池化。搜索引擎领域比如做验证码识别一般也是这个套路。如果你在做分割或检测任务同样优先 MaxPool。入门阶段不需要纠结MaxPool 主力AvgPool 收尾。4.4 几个少有人提但特别重要的避坑技巧第一个是关于 padding 模式。PyTorch 的 padding_mode 有 zeros、reflect、replicate、circular 几种默认是 zeros。大多数网络都用 zeros 就够了但如果你做图像分割、深度估计这类边界信息比较重要的任务可以考虑 reflect 模式它对边缘的处理更自然不会像补 0 那样在边缘形成突变。第二个是卷积层和池化层的顺序。常见的模式是 Conv - ReLU - Pool不是 Conv - Pool - ReLU。原因很简单卷积是线性操作激活函数提供非线性。先激活再池化池化可以在响应最强的特征上做选择如果先池化再激活部分信息就被提前压缩掉了。这个顺序看起来很基础但改一下确实对收敛效果有影响。第三个是 kernel_size 和 stride 都为 1 的卷积层。你可能觉得它什么都干不了其实它常用于调整通道数和增加非线性。1x1 卷积在 MobileNet、ResNet 里大量出现它本质上是在通道维度上做了一次全连接可以有效降低计算量同时保持空间尺寸不变。我建议你至少了解它存在的意义因为后面几乎一定会碰到。第四个是关于池化窗口与输入尺寸的对齐。如果你的输入尺寸不是池化窗口大小的整数倍比如 34x34 做 kernel_size2 的池化边缘会有 1 个像素被丢掉。如果这 1 个像素恰好包含关键信息结果就可能不理想。此时你可以设置 ceil_modeTrue或者在前一层按需 padding保证输入尺寸能被整除。最后分享一点个人体会我刚开始学卷积的时候花了很多时间在可视化卷积核上。训练一个简单模型后把第一层卷积核画出来看到它们确实演化出了边缘检测、颜色分离等模式那一刻对卷积的理解才真正具象化。你也可以试试这个做法自己训练一个十行代码的小网络把卷积核打印成图片这比看任何教程都来得印象深刻。另外一个让我少走弯路的小技巧是在写网络结构的同时随手在每层后面记录输出张量的 shape。看似很笨但它能帮你构建出清晰的“维度直觉”。等你积累多了看到一个输入尺寸基本就能预测出一串卷积池化之后输出大概多大这在设计网络、调参、排错时都特别有用。如果你想接着延伸下一步建议去了解批量归一化层BatchNorm2d和 Dropout2d它们通常会与卷积层、池化层配套出现。把这三类层配合起来你就已经能搭建出一个基础的图像分类网络了。到时候再回来读这篇你会发现之前关于维度、参数和顺序的讨论全部都用得上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑