卷积神经网络第一周:概念梳理、习题精讲与代码实践

卷积神经网络第一周:概念梳理、习题精讲与代码实践 第一周学卷积最容易出现的状态是课听完了觉得懂了做题时一脸懵对着答案看懂了自己写代码又处处报错。这个现象太正常了卷积涉及的计算逻辑和你以前的线性代数、神经网络知识不是一个思考维度它需要你把“滑动窗口”“局部连接”“参数共享”这些概念同时装进脑子里再落到代码里。这篇内容就是针对计算机视觉第一周卷积基础课的扫尾我会把课后习题的解题思路掰开讲透再把代码实践的步骤从头到尾过一遍目的只有一个让你在第二周开始前真正把卷积这块地基夯实。1. 第一周的知识边界卷积到底解决什么问题1.1 从全连接层的“浪费”说起学卷积之前得先搞清楚一个问题图像为什么不能用普通全连接网络直接处理一张 32x32 的彩色图片如果拉成一个长向量就是 32x32x3 3072 个数值。假设第一层设置 1024 个神经元光这一层的参数量就是 3072x1024约 300 万个参数。而这只是一层还没算后续网络。如果是 224x224 的大图全连接第一层的参数量会膨胀到没法看训练不充分、数据量不够的话直接过拟合。更关键的问题是全连接层处理图像时把每个像素当作完全独立的特征完全丢掉了像素之间的空间关系。但是在图像里相邻像素之间是有强关联的——一只猫的耳朵、眼睛、胡须都是由一片连续像素组成的局部特征。普通全连接网络“看不到”这种局部性它只会像查字典一样机械地把所有像素值硬塞进神经元。卷积神经网络就是针对这两个痛点设计的用局部连接减少参数用参数共享进一步压缩模型量级同时天然保留空间结构信息。1.2 卷积的数学直觉加权求和、滑动窗口卷积这个名字听起来吓人但如果你跳过教科书上的积分公式从图像处理的角度看它本质就是一句话用一个小的固定矩阵卷积核在整个图像上按固定步长滑动每到一个位置把核覆盖下的像素值和核上的数字一一相乘然后全部加起来得到输出特征图上的一个点。这个过程和你在图像处理软件里看到的“滤镜”“锐化”“模糊”是一回事只是卷积神经网络里的核不是某个设计师手工设计的而是通过反向传播自动学出来的。第一周课程往往会让你们从手工设计的 Sobel 算子、平滑核开始目的不是让你们停留在传统图像处理而是建立一个直观认识卷积核的确在提取某种特征——边缘、纹理、颜色过渡。理解了这一步再看卷积神经网络里的层叠结构就明白它其实是在“从简单特征组合成复杂特征”。这里有个容易混淆的点图像处理里的二维卷积和神经网络里的卷积虽然计算核心一样但习惯略有不同。深度学习框架中的conv2d实际上做的是互相关cross-correlation也就是核直接滑过去乘加不做翻转。这个差异不影响学习和使用但你在看数学推导时会发现教科书里卷积公式有一步翻转看到的时候别懵知道深度学习实现里省掉了这一步就行。1.3 第一周必须拿下的概念清单概念这东西上课时觉得都懂一到做题就模糊。我建议你第一周结束前用一张纸默写一遍下面这张表概念一句话本质常见误解卷积核一个用于提取局部特征的小矩阵以为核越大越好其实大核计算量大且未必更优feature map卷积输出的一张特征图代表某种特征激活的空间分布误把它当成最终分类结果padding在输入四周补一圈0控制输出尺寸、保留边缘信息忘了 padding 不是唯一的补多少需要算stride核每次滑动的步长能下采样、省计算以为 stride 只影响尺寸其实还影响感受野覆盖密度通道 channel每个卷积核处理一个通道多核叠出多个通道混淆输入通道数和卷积核个数池化 pool在局部区域做最大值/平均值提取降低分辨率以为池化是卷积的替代其实两者作用不同感受野 receptive field输出特征图上一个点对应回输入图像的范围以为感受野只和卷积层数有关没考虑 stride 和空洞率这几项是后续所有卷积网络结构的基本零件后面的代码实践和习题都围绕它们展开必须做到随口就能说清。2. 课后习题精讲从公式记忆到真正理解2.1 输出尺寸计算题一个公式吃掉整类题课后习题里最常见的就是给你输入尺寸、卷积核大小、padding、stride问输出特征图多大。公式长这样output_size floor((H 2 * padding - kernel_size) / stride) 1看起来很机械但有几个隐藏坑。第一个坑这个公式的 H 是每个维度分别计算的如果输入是 32x32你算出 H 方向的尺寸W 方向是同一个公式因为一般卷积核是正方形、padding/stride 也一致所以两个维度数值相同。但有些题故意把核设成 3x5此时两个维度必须分开算。第二个坑不是所有尺寸都能被 stride 整除。比如输入 7x7kernel_size3stride2padding1output floor((7 2 - 3) / 2) 1 floor(6/2) 1 4。这个整除情况比较友好但换个输入 8x8kernel_size3stride2padding0output floor((8 - 3)/2) 1 floor(5/2) 1 2 1 3。看到没5/2 向下取整成 2最后输出是 3。这个 floor 很多人第一遍都会忽略导致算出来的结果和 PyTorch 实际跑出来的对不上。推荐一个验证方法不要空想到 PyTorch 里直接跑一下。用torch.nn.Conv2d定义层给一个torch.randn(1, 3, 8, 8)的输入打印out.size()立刻知道答案。我上课后辅导时最常说的话就是“拿不准就打印一下 shape”这不是偷懒是工程习惯模型层与层之间能不能对上靠的就是这种即时验证。2.2 感受野计算题两层 3x3 为什么等价于一层 5x5感受野的题是第二高频考点。题目往往这么出一个输入 32x32 的图像经过两层 3x3 卷积不 padding最后特征图上的一个点对应原图多大区域这里不能只记公式要理解递归关系。第一层卷积核是 3x3输出特征图上的 1 个点看到了输入图上的 3x3 区域所以感受野是 3。第二层卷积核还是 3x3但它的输入是上一层特征图上一层在特征图上的一个点已经包含了原图的 3x3 信息第二层的 3x3 卷积在滑动时实际上是让这些 3x3 信息再组合一次所以感受野变成3 (3-1) 5。推理过程是每经过一层带 k 大小的卷积感受野扩大 (k-1) 乘以当前层的步长累积。两层 3x3 能覆盖 5x5 区域但参数量只有 2 x (3x3) 18而直接用一层 5x5 卷积需要 25 个参数还少算通道数。别小看这个差别现在的经典网络 VGG 就是靠堆叠 3x3 小卷积核用更少的参数获得更大的感受野同时因为中间多了层非线性激活网络表达能力还更强。这道题背后的设计哲学比公式本身重要得多。做题时还有一个易错点如果中间层的 stride 不是 1感受野的扩张不是单纯的线性相加要乘上步长的累积因子。我见过不少人把 stride2 的层也算成感受野只加 2结果差了十万八千里。第一周习题一般不会搞这么复杂但你要有这个意识。2.3 参数量计算题小心偏置和多通道参数量计算是很多人的送分题也是送命题。题目常这样输入 3 通道输出 64 通道卷积核大小 3x3求这一层有多少参数。很多人上来就写 3 x 3 9错得离谱。正确的拆解一个输出通道对应一个 3x3x3 的卷积核因为输入有 3 个通道每个通道都要一个 3x3 的核所以一个输出通道的核参数量是 3x3x3 27加上偏置 1共 28。输出 64 个通道总参数量 64 x (3x3x3 1) 64 x 28 1792。这个例子非常经典因为它反映了卷积的通道对应关系输入通道数决定每个输出卷积核的“厚度”输出通道数决定要学几组这样的核。把这个逻辑想清楚后面看 MobileNet 的深度可分离卷积、ResNet 的 1x1 卷积、Transformer 中降维升维操作都不会再迷茫因为这些操作本质上都离不开通道维度的调整。2.4 问答题padding 和 stride 的作用分别是什么还有一类题不算难但答不到点上。问“为什么需要 padding”时不要只写“防止尺寸变小”。要拆三点回答尺寸补偿不 padding每经过一次卷积特征图尺寸就缩小层数多了图像就没了padding 可以在不改变输出分辨率的情况下加深网络。边缘信息利用核滑动到边缘附近的次数比中心少导致边缘信息天然被“冷落”padding 让边缘像素也有机会被核覆盖到缓解信息衰减。控制特征图尺寸当你希望某一层输出保持某个固定分辨率可以通过调整 padding 实现。问“stride 的作用”时同样要答两个层次一是下采样像步长为 2 时输出尺寸近似减半可以把特征图分辨率降下来减少后续计算量二是覆盖密度stride 越大核滑动的重叠区域越少提取的特征越粗糙这既是好事省计算也是坏事丢细节所以实际网络里常在前几层用小 stride深几层再用大 stride。2.5 判断题池化层到底在干嘛第一周习题里常混入池化层的问题比如给一个 4x4 输入2x2 最大池化stride2问输出是什么。答案是输出 2x2每个格子取原图对应 2x2 区域的最大值。但很多人只是机械算不理解为什么需要池化。这里我想多说一句现在很多新网络比如 ResNet 之后的不少结构开始用 stride2 的卷积替代池化层导致一些初学者觉得池化过时了。其实池化有它不可替代的作用最大池化提供了一种天然的不变性当小物体平移一两个像素时池化后的最大值位置虽然变了但最大值本身还在这种“对微小位移不敏感”的特性对分类任务非常有用。平均池化则适合保留背景信息。做选择题时看到“增强平移不变性”“降低计算量”往池化上靠基本没错看到“增加非线性能力”那是激活函数的事别混。3. 代码实践把卷积“拆开”看一遍3.1 环境准备与跑通第一个卷积第一周的代码实践我强烈建议直接用 PyTorch不要先摆弄 TensorFlow 或者自己造轮子。原因很现实PyTorch 的nn.Conv2d接口简单直接可视化方便资料多遇到报错去搜社区基本都有答案。环境建议Python 3.8 以上安装 torch 和 torchvisionCPU 版就够跑GPU 无所谓。第一次验证环境生成一个随机张量过一遍卷积层import torch import torch.nn as nn # 输入batch_size1, 通道数3, 尺寸32x32 x torch.randn(1, 3, 32, 32) # 卷积层输入通道3输出通道8卷积核3x3padding1 conv nn.Conv2d(in_channels3, out_channels8, kernel_size3, padding1) out conv(x) print(out.shape) # 输出torch.Size([1, 8, 32, 32])这段代码跑通后先别急着往下写。把它当成一个“活的公式计算器”改改 kernel_size、stride、padding观察输出 shape 的变化和第一节的公式对比你会比看十遍课件都记得牢。3.2 用真实图像做边缘检测手工设计核的实践接下来用一个经典练习建立直觉把一张彩色图转成灰度用 Sobel 算子做边缘检测。Sobel 算子的水平方向核是这样的-1 0 1 -2 0 2 -1 0 1垂直方向则是转置。边缘在图像上表现为像素值的剧烈跳变这个核的加权求和能把这个跳变放大成明显的响应值。代码如下import torch import torch.nn as nn import numpy as np from PIL import Image import matplotlib.pyplot as plt # 读取图像并转为灰度像素值归一化到 [0,1] img Image.open(sample.jpg).convert(L) img_np np.array(img, dtypenp.float32) / 255.0 x torch.from_numpy(img_np).unsqueeze(0).unsqueeze(0) # shape: (1,1,H,W) # 定义水平 Sobel 卷积核 sobel_kernel torch.tensor([[[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]]], dtypetorch.float32) # 用 nn.Conv2d 包装核固定权重不加偏置 conv nn.Conv2d(in_channels1, out_channels1, kernel_size3, padding1, biasFalse) conv.weight.data sobel_kernel with torch.no_grad(): edge conv(x) # 可视化 plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.imshow(img_np, cmapgray) plt.title(Original) plt.axis(off) plt.subplot(1, 2, 2) plt.imshow(edge.squeeze().numpy(), cmapgray) plt.title(Sobel Edge) plt.axis(off) plt.show()这段代码看起来简单但有三个细节值得你留意。第一nn.Conv2d默认会随机初始化权重必须手动conv.weight.data sobel_kernel覆盖否则跑出来是一堆噪声。第二biasFalse是必须的因为 Sobel 核本身不带偏置加上偏置会破坏检测效果。第三图像的数值范围一定要归一化到 0~1如果你从 PIL 读到的是 0~255 的 uint8先转 float32 再除以 255否则结果会异常。用真实图像跑完这个实验我对“卷积核就是特征提取器”这句话才有体感同一张图换不同的核输出完全变个样。第一周实践不追求训练深度学习模型这种“手工核可视化”反而是最好的启蒙。3.3 自己写一个最朴素的卷积理解内部循环用nn.Conv2d跑通之后我建议你再手写一个最朴素的卷积实现不用考虑效率和反向传播只是把前向计算用三层循环写出来。这个练习的价值在于它会强制你把 padding、stride、多通道、多输出核这些概念全部过一遍任何一处理解不到位代码都跑不对。def conv2d_simple(inputs, kernels, bias, stride1, padding0): inputs: numpy 数组shape(C_in, H, W) kernels: numpy 数组shape(C_out, C_in, kH, kW) C_in, H, W inputs.shape C_out, _, kH, kW kernels.shape # 先 padding padded np.zeros((C_in, H 2 * padding, W 2 * padding), dtypenp.float32) padded[:, padding:padding H, padding:padding W] inputs out_h (padded.shape[1] - kH) // stride 1 out_w (padded.shape[2] - kW) // stride 1 output np.zeros((C_out, out_h, out_w), dtypenp.float32) for c_out in range(C_out): for i in range(out_h): for j in range(out_w): # 找出对应区域 region padded[:, i * stride:i * stride kH, j * stride:j * stride kW] # 每个输出通道的核对输入所有通道加权求和 value np.sum(region * kernels[c_out]) bias[c_out] output[c_out, i, j] value return output这段代码的循环顺序是先遍历输出通道再遍历输出特征图的高和宽每个输出位置都做一次全通道的加权求和。你把它和nn.Conv2d的结果对比一下就能发现自己有没有踩坑比如偏置到底该加在每个输出通道还是每个卷积核上padding 到底pad在哪个维度。这个“和自己写的代码对比”的过程就是课后实践的核心价值。3.4 进阶一点训练一个最小的卷积分类器如果前面几步都顺利可以再加一个小挑战用 PyTorch 训练一个单卷积层加全连接层的分类器跑 MNIST 手写数字数据集。MNIST 图是 28x28 单通道一个nn.Conv2d(1, 16, kernel_size3, padding1)接nn.MaxPool2d(2)再接nn.Flatten()和nn.Linear(16 * 14 * 14, 10)用交叉熵损失Adam 优化器训练 5 个 epoch。这个练习会让你接触到训练的完整链路数据加载、前向传播、损失计算、反向传播、参数更新。第一周不要求你把训练代码写得多漂亮能跑通、能看到 loss 下降就行。但你做完之后会把卷积层的可学习参数这个概念彻底理解——前面手动覆盖的 Sobel 核是固定的这里卷积核会通过梯度更新不断变化模型自己学着找出更有效的特征组合。4. 作业之外的进阶方向那些卷积热词都是什么4.1 深度可分离卷积减少计算量的利器第一周结束后你在网上搜“卷积神经网络”会看到一堆热词MobileNet、深度可分离卷积、空洞卷积、一维卷积、TCN。这些名字听着高级但根子都在基础卷积上我先挑两个最容易出现在后续课程里的说。标准卷积的计算量是 C_in x C_out x k x k x H_out x W_out。深度可分离卷积把它拆成两步第一步叫逐通道卷积每个输入通道单独用一个 k x k 的核做卷积这时输出通道数等于输入通道数第二步叫逐点卷积用 1x1 卷积把逐通道卷积的输出通道数变换到想要的 C_out。计算量从 C_in x C_out x k x k 变成 C_in x k x k C_in x C_out省下多少当 k3 且 C_out 很大时大约缩小到原来的九分之一。MobileNet 系列能在手机端跑靠的就是这个设计。理解它的关键在于把你之前算卷积参数量的思路用上通道维度的操作和空间维度的操作被解耦了。这个思路后来也被 Transformer 中的 depthwise convolution、以及各种轻量 attention 结构吸收了属于“学一次受用终生”的知识点。4.2 空洞卷积用更少参数获得更大感受野空洞卷积Dilated Convolution是另一个高频热词。它的核心是在卷积核内部插入“空洞”让核覆盖的范围变大但实际参与计算的像素数不变。比如一个 3x3 核dilation2 时它覆盖的范围是 5x5但中间隔一个像素取一个真正乘加的仍然是 9 个点。这个设计大量用在语义分割任务里因为分割既要高分辨率的空间细节又需要大感受野来理解语义常规做法是不断卷积池化缩小图但这样会丢失细节。空洞卷积直接解决了“大感受野和高分辨率不可兼得”的矛盾。计算感受野的公式也升级了有效核大小 dilation x (kernel - 1) 1。回到前面那道两层 3x3 的题如果第二层改成 dilation2感受野就不是 5而是 3 (5-1) 7参数量却没增加。这个扩展在数学上很优雅值得在基础周就建立印象。4.3 一维卷积与时间序列预测TCN 背后的逻辑搜索热词里频繁出现 TCN 时间卷积网络和股票预测这其实是卷积的一个延伸场景。初学者容易把卷积默认成二维图像专用其实一维卷积在处理时间序列时也很有用比如股票价格序列、传感器信号、音频。一维卷积核相当于一个滑动窗口沿时间轴滑动对窗口内的数值做加权求和。TCN 的核心结构是因果卷积意思是输出 t 时刻的值时只用 t 时刻及之前的输入不往后看这符合时间序列“不能预知未来”的约束。再加上膨胀卷积扩大时序感受野让模型能捕捉更长距离的依赖关系。和 LSTM 相比TCN 可以并行处理整个序列训练速度快这是它的一大优势。不过我想提醒你第一周先把二维卷积的空间直觉建立好再去看一维卷积和 TCN 会顺手很多因为它们本质上都是在做“局部加权求和”只是滑动方向和数据结构不一样。股票预测这种话题很有吸引力但别在基础没打牢时就急着上 Transformer 和 TCN 实战那会把精力浪费在调参上面而不是理解原理上。4.4 深度可分离、空洞、一维一张表看懂关系卷积类型改动点解决的问题典型应用标准 2D 卷积无基础特征提取图像分类、检测深度可分离卷积拆成逐通道逐点两步大幅降低计算量MobileNet、边缘设备空洞卷积核内插入空洞扩大感受野不增加参数语义分割、目标检测1D 卷积核沿时间轴滑动处理时间序列语音、股票预测的 TCN转置卷积上采样操作把低分辨率特征图放大生成模型、分割解码器这张表可以贴在你笔记里后续课程每遇到一种新卷积就归类进这个框架“它改了什么为了解决什么用在哪”学习效率会高很多。5. 我踩过的坑和给你的练习建议5.1 四个每天都在重复的低级错误代码实践多了你会发现大部分报错根源就那几个第一周尤其集中。第一个输入张量的 shape 不对。PyTorch 的卷积层要求输入是 (N, C, H, W) 四维而很多人直接从 PIL 读到的是二维数组忘记加 batch 和通道两个维度。这个错频到什么程度几乎每个初学者第一天都会遇到报错信息里写着 “Expected 4D input”你第一反应应该是去检查.unsqueeze()加对了没。第二个归一化没做。图像像素是 0~255 的整数直接作为输入时数值范围太大梯度容易爆炸特征响应也会异常。推荐在数据加载阶段就x / 255.0到 0~1 之间或者用 PyTorch 的transforms.Normalize做标准化。第三个卷积层定义时in_channels和上一层的输出通道对不上。这是个非常经典的维度不匹配错误解决方法是每次写完一层打印一下输出 shape记录下通道数下一层直接对照着写。第四个训练时忘了model.train()和model.eval()的状态切换。有些层如 BatchNorm、Dropout在不同状态下行为不同第一周虽然不一定用到 BatchNorm但从一开始养成切换状态的习惯后面会少很多坑。5.2 一个有效的练习路径从“看”到“改”再到“造”给零基础同学一条具体练习路径。第一天把课程代码跑通重点看nn.Conv2d各种参数变化对 shape 的影响。第二天用真实图像跑手工核可视化把 Sobel、平滑、锐化都试一遍。第三天手写一个朴素卷积实现和 PyTorch 结果对比这一步能检验你有没有真正理解循环里的对应关系。第四天把 MNIST 分类器跑通哪怕只训练几个 epoch目标是看到 loss 下降。第五天尝试自己改网络比如把卷积核从 3x3 改成 5x5观察参数量和效果的变化。这套路径的核心思想是“先跑通再理解”先让代码动起来然后逐个环节深化理解。别想着一口吃成胖子第一周就把所有卷积变体都学会这既不现实也没必要。5.3 最后提醒别急着冲高级模型第一周结束的时候你会看到社区里各种 TC NTransformer 预测股票的项目感到焦虑很正常但请按捺住。那些项目看起来炫酷本质上还是由你这一周学的卷积基础部件搭建起来的如果基础不牢你去复现时会在数据预处理、维度匹配、训练稳定性的泥潭里耗尽信心。把课后习题做透把代码实践里的每一步验证跑通比预习第二周的内容更重要。我见过太多人第一周急着往前赶结果过了三周还要回来补“什么是 padding”学习这种事慢就是快。卷积基础是计算机视觉的地基你可以走得慢但每一步都要踩实。