12 从 MLP 到 LeNet:卷积层到底在做什么?卷积核、小窗口滑动、特征图,一篇讲明白

张开发
2026/4/3 18:04:55 15 分钟阅读
12 从 MLP 到 LeNet:卷积层到底在做什么?卷积核、小窗口滑动、特征图,一篇讲明白
卷积层到底在做什么卷积核、小窗口滑动、特征图一篇讲明白很多人学卷积的时候都会遇到一个很典型的问题卷积核这个词见过特征图这个词也见过小窗口滑动大概也知道是什么意思代码甚至可能都跑过但如果把这些词放在一起再问一句卷积层到底在干嘛脑子里还是容易宕机。这不是因为卷积特别玄而是因为很多资料会把这些概念拆开讲结果每个词都认识却拼不成一幅完整的画面。所以这篇文章只做一件事把卷积层到底在做什么从头到尾顺着讲清楚。你先不用背公式也不用急着管框架实现。只要先抓住一句话卷积层做的事情就是拿一个小窗口在图像上不断滑动检查每个局部区域里有没有某种模式。不同的卷积核检查不同的模式后面的卷积核、特征图其实都是围绕这个动作展开的。一、为什么图像任务里会出现卷积想理解卷积先要明白一件事图像不是普通的一串数字。在程序里图像当然可以表示成数字矩阵。但问题在于这些数字不是随便排在一起的它们天然带着空间结构哪个像素在左边哪个像素在右边哪个像素在上面哪个像素在下面哪些像素彼此相邻哪一小块区域共同组成了一条边、一块纹理或者一个角点也就是说在图像里真正有用的信息往往不是“某个单独像素值是多少”而是一个像素和它周围邻域一起构成了什么局部模式。比如一条边缘不是单个像素决定的一个角点也不是单个像素决定的一段笔画、一小块纹理、一个局部缺陷都要看附近一片区域这时候问题就很自然了既然图像的重要信息本来就藏在局部区域里那模型能不能直接从局部开始看这就是卷积出现的原因。二、卷积层到底在做什么卷积层做的事情其实非常朴素拿一个小窗口在图像上从左到右、从上到下慢慢滑过去每到一个位置就对当前这小块区域做一次计算。这个计算结果可以理解成当前位置对某种模式的响应有多强这块局部区域和某种模板有多像这里有没有出现当前想找的特征你会发现卷积的核心动作其实很简单取一个局部区域做一次局部计算挪到下一个位置重复这个过程把所有位置的结果收集起来而后面你经常听到的几个词刚好对应这几个步骤里的不同角色卷积核那个用来做局部计算的小模板小窗口滑动在图像不同位置反复检查特征图每个位置的计算结果组成的新图所以别把它们当成三四个分散概念。它们其实是一整套动作里的不同部分。三、为什么卷积一定要先看“局部”很多人一开始会想既然输入是一整张图为什么不直接整体处理原因很简单因为图像里的很多关键信息本来就是局部出现的。比如判断这里有没有边缘只需要附近几个像素判断这里是不是一个角点也只需要一个小邻域判断这一块是不是某种纹理也不需要整张图一起参与换句话说卷积不是“不想看整张图”而是它知道想理解整张图往往要先把局部模式找出来。这也是卷积特别自然的地方。它不是一上来就想直接回答“这张图是什么”而是先做一件更基础、更合理的事先看看局部里有什么模式再把这些局部响应交给后面的层继续处理所以卷积层本质上是在给后面的网络准备一种更好用的表示。四、卷积核到底是什么“卷积核”这个名字第一次看到确实容易让人紧张。但如果只从直觉上理解它没那么神秘。你可以先把卷积核理解成一个专门用来检测某种局部模式的小模板。比如你可以粗略地想象有的模板更容易对横向边缘有反应有的模板更容易对竖向边缘有反应有的模板更容易对某种纹理有反应有的模板更容易对某种亮暗变化有反应在真正的神经网络里这些卷积核通常不是人工写死的而是通过训练自己学出来的。但在入门阶段把它理解成“局部模式检测器”就够了。也就是说卷积层负责整体机制卷积核负责“到底在找什么模式”这么想画面会清楚很多。五、为什么卷积核要在整张图上滑动有了卷积核之后接下来一个特别关键的问题就是为什么只检查一个位置不够为什么一定要在整张图上滑动因为同一种局部模式可能出现在很多不同位置。比如一条边可能在左边也可能在右边一个角点可能在上面也可能在下面一段纹理可能在中间也可能在角落一个缺陷可能出现在图像的任意区域如果模型只能在固定位置识别某种模式那它就太死板了。而图像里的模式往往是“位置会变”的。所以最自然的做法就是让同一个卷积核在整张图不同位置都去检查一遍。它本质上是在不断重复一个问题这个地方像不像我要找的模式往右挪一点再看一次往下挪一点再看一次整张图扫完之后就知道这种模式在哪里强、哪里弱了这就是“小窗口滑动”的意义。六、直接上一个 15×15 的例子看卷积到底怎么动起来下面直接用代码看一个最简单的卷积过程。为了让这个例子更像一张真正的小图像下面直接构造一个15×15的矩阵来演示。1先造一张 15×15 的图importnumpyasnp imgnp.zeros((15,15),dtypeint)# 中间放一个 7x7 的亮区域img[4:11,4:11]1print(输入图像)print(img)print(图像形状,img.shape)这段代码会生成一张很简单的图外围是 0表示暗背景中间是 1表示亮区域把它画出来之后你会立刻看到importmatplotlib.pyplotasplt plt.figure(figsize(6,6))plt.imshow(img,cmapgray)plt.title(15x15 输入图像)plt.colorbar()plt.show()在程序眼里图像本质上就是一个二维数组。而卷积正是在这个二维数组上做局部扫描。输出输入图像 [[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 1 1 1 1 1 1 1 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]] 图像形状 (15, 15)七、再准备一个 3×3 的卷积核接下来定义一个最简单的 3×3 卷积核kernelnp.array([[1,1,1],[1,1,1],[1,1,1]])print(卷积核)print(kernel)现在先别纠结它严格意义上到底“检测什么”。这里只是用它来感受卷积层最基础的动作。你可以暂时把它理解成看当前这块 3×3 区域里整体亮不亮。真实网络里的卷积核会复杂得多也通常是训练学出来的。但拿这个例子理解卷积已经非常够用了。八、让这个小窗口在图像上滑动起来下面不用深度学习框架直接手写一次最基本的卷积过程。h,wimg.shape kh,kwkernel.shape output[]foriinrange(h-kh1):row[]forjinrange(w-kw1):patchimg[i:ikh,j:jkw]valuenp.sum(patch*kernel)row.append(value)output.append(row)outputnp.array(output)print(输出特征图)print(output)print(特征图形状,output.shape)输出输出特征图 [[0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 2 3 3 3 3 3 2 1 0 0] [0 0 2 4 6 6 6 6 6 4 2 0 0] [0 0 3 6 9 9 9 9 9 6 3 0 0] [0 0 3 6 9 9 9 9 9 6 3 0 0] [0 0 3 6 9 9 9 9 9 6 3 0 0] [0 0 3 6 9 9 9 9 9 6 3 0 0] [0 0 3 6 9 9 9 9 9 6 3 0 0] [0 0 2 4 6 6 6 6 6 4 2 0 0] [0 0 1 2 3 3 3 3 3 2 1 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0]] 特征图形状 (13, 13)可以这样理解值为9的点他的上下左右都是亮的所以输出为9相当于这个点把周围记住了周围的某种模式。这段代码做的事情很简单从输入图像里截出当前一个 3×3 小块和卷积核逐元素相乘再把结果全部加起来得到当前位置的一个响应值然后窗口继续滑动重复这个过程因为输入是 15×15卷积核是 3×3而且这里没有做 padding所以输出会变成13×13。这就说明卷积层不是直接“看完整张图然后下结论”而是在每个局部位置做一次检查再把这些检查结果组成新的表示。九、把输入图和输出图放在一起看卷积的作用就很直观了下面把输入图像和输出特征图放在一起importmatplotlib.pyplotasplt plt.figure(figsize(12,5))plt.subplot(1,2,1)plt.imshow(img,cmapgray)plt.title(输入图像15x15)plt.colorbar()plt.subplot(1,2,2)plt.imshow(output,cmapviridis)plt.title(输出特征图13x13)plt.colorbar()plt.tight_layout()plt.show()这时候会看到一个很明显的现象输入图像中间有一块亮区域输出特征图中间对应区域的响应更强越靠近亮区域中心输出值通常越大原因也很直观卷积核在那些位置看到的 1 更多。如果一个 3×3 区域全是 1那么输出就最大。如果大部分是 0那么输出自然就小很多。这其实已经非常接近卷积最本质的直觉了卷积核在不同位置滑动时会对不同局部区域给出不同强度的响应这些响应拼起来就是特征图。十、卷积之后尺寸没小多少那它到底厉害在哪很多人看到特征图时会有一个很自然的疑问卷积完之后图看起来也没缩小多少啊那它到底做了什么这个问题问得很好。因为卷积层的核心价值本来就不只是压缩尺寸。如果卷积核比较小、步长是 1、又没有额外的下采样操作那么卷积后的空间尺寸确实不会一下缩很多。所以卷积最重要的地方不是“把图压小”而是把原始像素变成更有意义的局部模式表示。这才是关键。原图里的一个点通常只是某个位置的像素值。而卷积之后特征图里的一个点已经不再是原图里某个孤立像素本身了。它对应的是输入图像中的一个局部区域这个局部区域和卷积核做完计算之后得到的一个响应结果换句话说特征图上的一个点代表的是输入图像里一小块区域的信息。这就是卷积很厉害的地方。它看起来好像没有做特别明显的“压缩”但实际上每一个位置承载的信息已经变了。原来是单个像素现在变成了“这个局部区域对某种模式的响应”。所以卷积真正强的地方不只是“缩”而是它让每个位置不再是孤立像素它让每个位置开始携带局部上下文它把原始输入变成了更适合后续网络处理的表示这一点比单纯尺寸变小更重要。十一、手算一个位置卷积就不抽象了如果你觉得“代码看懂了但还是差一点感觉”那最好的办法就是手算一个位置。假设当前窗口落在这样一个 3×3 区域patchnp.array([[0,1,1],[1,1,1],[1,1,1]])卷积核仍然是kernelnp.array([[1,1,1],[1,1,1],[1,1,1]])逐元素相乘之后结果还是[[0,1,1],[1,1,1],[1,1,1]]然后把这些数全部加起来0111111118这个 8就是当前位置的输出值。如果窗口刚好落在亮区域中心3×3 全是 1那么输出就是 9。如果窗口落在全黑背景里那输出就是 0。所以卷积层本质上就是在不断做这件事比较当前这个局部区域和卷积核想找的模式到底有多像。十二、特征图到底是什么现在就好理解了讲到这里“特征图”这个词其实已经没什么神秘感了。因为卷积核每滑到一个位置都会得到一个响应值。把这些响应值按位置排起来就形成了一张新的图。这张图就是特征图。所以你完全可以把特征图理解成某个卷积核在整张图不同位置上的响应分布图。它不再是原始像素图而是“模式响应图”。换句话说原图里存的是像素值特征图里存的是“这个地方像不像某种模式”这也是卷积层特别重要的地方它把原始像素变成了更有利于后续判断的中间表示。十三、一个卷积核肯定不够看到这里你大概率会立刻想到一个问题如果一个卷积核只能找一种模式那当然不够。完全正确。真实图像里的信息非常丰富不可能只靠一个卷积核搞定。因为图像里可能同时有横向边缘竖向边缘斜线角点纹理局部形状更复杂的小模式所以真正的卷积层通常会有很多个卷积核。于是会发生这样的事第一个卷积核输出一张特征图第二个卷积核输出另一张特征图第三个卷积核再输出一张特征图……最后卷积层输出的不是一张图而是一组特征图。你可以把它理解成同一张图被很多不同的局部模式检测器分别扫了一遍。这样一来后面的网络看到的就不只是原始像素而是很多不同模式的响应结果。十四、卷积层和全连接层到底差在哪讲到这里再回头看全连接层和卷积层的区别就会清楚很多。全连接层更像什么它更像是把输入看成一组统一特征然后整体做组合计算。如果输入是一张图它不会天然强调哪些像素彼此相邻哪些局部区域一起构成了某种模式同一种模式会不会出现在不同位置卷积层更像什么它更像是在说我先不急着整体判断我先从局部开始用卷积核找某种模式再把这种模式在各个位置上的响应收集起来最后把这些结果交给后面的层继续判断所以两者真正的差别不只是“公式不同”而是它们看待输入的方式不同。卷积层之所以特别适合图像就是因为它更顺着图像本身的结构来设计。十五、如果你现在只想记住一句话如果你看完整篇最后只想留下一个最核心的记忆点那就记这个卷积层会用卷积核作为局部模式检测器在图像上用小窗口不断滑动每个位置得到的响应值组合起来就形成了一张特征图。这句话其实已经把整条逻辑串起来了卷积层整体机制卷积核检测什么小窗口滑动怎么在整张图上找特征图找完之后得到什么十六、完整代码15×15 图像 手写卷积 特征图可视化下面把前面的代码整理成一版可以直接运行的完整示例importnumpyasnpimportmatplotlib.pyplotasplt# # 1. 构造一张 15x15 图像# imgnp.zeros((15,15),dtypeint)# 中间放一个 7x7 的亮区域img[4:11,4:11]1print(输入图像)print(img)print(图像形状,img.shape)# # 2. 定义一个 3x3 卷积核# kernelnp.array([[1,1,1],[1,1,1],[1,1,1]])print(\n卷积核)print(kernel)# # 3. 手工实现卷积滑动# h,wimg.shape kh,kwkernel.shape output[]foriinrange(h-kh1):row[]forjinrange(w-kw1):patchimg[i:ikh,j:jkw]valuenp.sum(patch*kernel)row.append(value)output.append(row)outputnp.array(output)print(\n输出特征图)print(output)print(特征图形状,output.shape)# # 4. 可视化输入图和特征图# plt.figure(figsize(12,5))plt.subplot(1,2,1)plt.imshow(img,cmapgray)plt.title(输入图像15x15)plt.colorbar()plt.subplot(1,2,2)plt.imshow(output,cmapviridis)plt.title(输出特征图13x13)plt.colorbar()plt.tight_layout()plt.show()十七、写到这里卷积其实没那么神秘很多人学卷积时会觉得它抽象往往不是因为它真的特别难而是因为一开始接触到的内容太容易碎这边讲卷积核那边讲特征图另一边又开始讲 stride、padding、channel结果就是每个词都认识但脑子里连不起来。其实只要你先把最核心的动作抓住后面很多细节都会顺先拿一个小窗口在图像上滑动用卷积核检查局部模式把每个位置的响应组成特征图卷积的主干逻辑本质上就是这么回事。如果这篇文章帮你把“卷积层到底在做什么”这件事理顺了那它的目的就达到了。

更多文章