简介:这份CNN卷积神经网络详解PPT面向计算机视觉初学者与需要梳理深度学习知识体系的开发者,围绕传统LBP等手工特征到端到端CNN特征提取的演进,系统讲解卷积层、池化层、全连接层、激活函数及卷积尺寸计算,并延伸到转置卷积、空洞卷积、深度可分离卷积、组卷积、可变形卷积和3D卷积等常见变体,同时补充非线性激活、优化算法与正则化等训练必备要素。压缩包仅含1个pptx文件,大小约7.58MB,页面按“CNN从哪里来、细节实现、代码实战、CNN到哪里去”组织,适合课堂讲授、自学复盘与技术分享。该内容已有6671人学习,读者可借助目录框架快速建立CNN知识地图,结合代码实战理解特征提取与分类流程,并对照变体卷积和训练要点查漏补缺,便于从概念到实现形成完整闭环。
1. 卷积只是一个滑动的点积,真正的门槛在它押中的三个先验
CNN 卷积神经网络里最容易被高估的部分,恰恰是“卷积”这个运算本身——十行 numpy 就能写完,本质是一次滑窗点积。可把同样的参数量丢给全连接层去做图像分类,准确率会掉一大截。差的那一截,来自结构里被硬编码进网络的三个先验:局部连接、权重共享、层次堆叠。这套课件从 LBP 这类手工算子讲起,一路走到转置卷积、空洞卷积、深度可分离卷积,主线其实只有一条——每加一个设计,都是在回答“我要把什么先验塞给网络,代价是什么”。看清这条线,论文里那些眼花缭乱的变体就不那么玄学了,而是能一眼看出它在补哪块短板。
2. 从 LBP 到端到端:特征这件事什么时候交给了梯度
2.1 LBP 的工作机制,以及它为什么只停在“中阶特征”
LBP 的思路很朴素:不看一个像素的绝对灰度,只看它和周围八个邻居的相对大小。以中心像素为阈值,邻居亮于它记 1,暗于它记 0,八个 bit 顺时针拼成一个 0 到 255 的整数。这个整数就是该位置局部纹理的“编号”,把整张图的编号统计成直方图,就得到一个图像描述子。
它有效的原因值得多想一层:阈值是中心像素自己,所以整幅图整体变亮或变暗(光照的单调变化)不会改变任何一个 bit 的取值,这就天然获得了光照不变性。代价是它把灰度信息彻底扔了,只保留序关系。
import numpy as np def lbp8(img): """8 邻域、3x3 固定窗口的原始 LBP,返回比输入小一圈的编码图""" h, w = img.shape code = np.zeros((h - 2, w - 2), dtype=np.uint8) # 8 个邻居相对中心的偏移,按顺时针排,第 k 个邻居占第 k 位 offsets = [(-1,-1), (-1,0), (-1,1), (0,1), (1,1), (1,0), (1,-1), (0,-1)] center = img[1:-1, 1:-1] for k, (dy, dx) in enumerate(offsets): neighbor = img[1+dy:h-1+dy, 1+dx:w-1+dx] # 邻居 >= 中心 记 1,左移到第 k 位 code |= ((neighbor >= center).astype(np.uint8) << k) return code这段代码里最关键的一行是<< k:它把八个独立的布尔比较压成一个整数,等价于给每个像素分配了一个“局部纹理模板编号”。参数只有两个——采样点数 P=8 和半径 R=1,而这恰恰是它最大的约束。想描述更大的结构,就得把 P 和 R 放大,等价模式(uniform pattern)的种类数会随 P 迅速膨胀,而每种模式对应什么语义,没有任何机制去告诉它。课件里那句“只能手动设定特征的模式吗”,问的就是这个:人的先验写死在算子里,任务变了,算子不会跟着变。
2.2 端到端把“设计特征”换成了“设计可学习性”
传统流程是两段式的:先手工算特征,再拿特征喂 SVM 之类的分类器。这个结构有个致命处——两段之间是断开的。分类器分错了,错误传不回特征提取器,因为特征提取器里根本没有可调参数。LBP 的半径、采样点数、直方图分箱数,只能靠人在验证集上试。
端到端的改动看起来只是“把两段接起来”,但性质完全不同:卷积核里的每一个数都是可学习参数,分类损失对它们的偏导可以一路回传。网络在做的其实是——在参数空间里搜索那组最能降低分类损失的模板。人不再回答“边缘长什么样”,只回答“第一层是 3×3、通道数是 64”。
| 对比项 | 手工特征 + 分类器 | 端到端 CNN |
|---|---|---|
| 特征来源 | 人工设计算子 | 梯度反传学出来的卷积核 |
| 能否针对任务优化 | 不能,只能换算子 | 能,损失函数直接定义好坏 |
| 参数量 | 几乎为 0 | 与通道数、核大小相关 |
| 泛化依赖 | 算子本身的通用性 | 数据量 + 先验强度 |
表里最后一行是很多人踩坑的地方:端到端不是免费的,它把“设计特征”的工作量换成了“喂数据和调结构”的工作量。数据量不够时,手工算子的强先验反而更稳,这也是小样本场景里 LBP、HOG 至今还活着的原因。
2.3 三个先验:局部连接、权重共享、层次堆叠
局部连接的动机很直接:图像相邻像素高度相关,一个像素和它 200 像素外的点几乎没关系。全连接层硬把这种稀疏关系拉成稠密,既浪费参数,又破坏了空间结构。3×3 核在 224×224 的图上只连 9 个输入,这个假设在自然图像上几乎总是成立。
权重共享解决的是平移问题。同一个核在整张图上滑动,意味着网络假设“猫的耳朵出现在左上角还是右下角,用的是同一个检测器”。副作用是参数量与图像尺寸彻底解耦:输入变成 448×448,卷积层的参数一个都不增加,只增加计算量。
层次堆叠则来自一个经验观察——浅层学到的是边缘、颜色梯度,中层是纹理和简单部件,深层才是语义。这不是设计出来的,更多是被深度逼出来的:单层 3×3 核只看得到 9 个像素,想看到整只猫,只能一层层把感受野叠上去。
拿数字说话。224×224×3 的输入直接全连接到 1000 维,参数量是 1.5 亿;换成一个 3×3×3、64 通道的卷积层,参数量是 1728。四个数量级的差距,就是这三个先验值多少钱。
2.4 先验的另一面:每条假设都会在某个任务上失效
局部连接假设了“有用的模式是局部的”,遇到需要全局统计的任务(比如判断整张图的风格)就得靠堆深度硬凑感受野。权重共享假设了“同一种模式出现在哪里都一样”,遇到位置本身就是信息的任务(比如关键点回归的绝对坐标),就得额外编码位置。层次堆叠假设了“语义由局部到全局组合”,遇到长程依赖(比如两处相距很远的对称结构)就力不从心。
后面几章的变种卷积,几乎都在拆这三个假设中的某一个:空洞卷积拆的是“感受野必须靠加深”,深度可分离卷积拆的是“跨通道必须有全连接”,可变形卷积拆的是“采样点必须在固定网格上”。
3. 卷积层的实现细节:一次点积、一张特征图、一条尺寸公式
3.1 卷积核到底在算什么:互相关与模板匹配
先把一个常见的误解澄清掉:深度学习框架里的 conv2d 严格来说是互相关,核不翻转。数学上的卷积要先翻转核再滑动,但因为核的权重是学出来的,翻不翻转对表达能力没有任何影响,工程上就省了这一步。
单通道的计算是:输出 (i, j) 位置的值 = 输入窗口与核逐元素相乘再求和。多通道时,核的深度必须等于输入通道数,各通道乘积全部相加,得到一个标量。所以一个核无论输入多少通道,输出的都是一张单通道特征图;想要 64 张特征图,就得有 64 个核。
import numpy as np def conv2d(x, w, b, stride=1, pad=0): """x: (C_in, H, W) w: (C_out, C_in, kh, kw) b: (C_out,)""" C_in, H, W = x.shape C_out, _, kh, kw = w.shape xp = np.pad(x, ((0, 0), (pad, pad), (pad, pad))) # 输出尺寸:向下取整,+1 是因为首个窗口不需要滑动 Ho = (H + 2 * pad - kh) // stride + 1 Wo = (W + 2 * pad - kw) // stride + 1 out = np.zeros((C_out, Ho, Wo)) for oc in range(C_out): for i in range(Ho): for j in range(Wo): hs, ws = i * stride, j * stride patch = xp[:, hs:hs + kh, ws:ws + kw] # (C_in, kh, kw) # 展平后做一次内积:这就是"一次点积"的全部含义 out[oc, i, j] = np.sum(patch * w[oc]) + b[oc] return outnp.sum(patch * w[oc])这一句是整段代码的核心,参与内积的维度是C_in * kh * kw。真实框架不会写成这样四重循环,它会用 im2col 把每个滑窗展平成矩阵的一列,再一次性交给 BLAS 做矩阵乘。理解 im2col 的意义在于:它解释了为什么卷积的显存占用常常远大于参数量——展开后的矩阵尺寸是C_in * kh * kw * Ho * Wo,是原始输入的kh * kw倍,这也是后面显存排查章节要回头看的点。
3.2 输出尺寸公式是怎么推出来的
Ho = (H + 2P - K) // S + 1这个公式在面试里被问烂了,但推导过程比结论有用。
填充后可用长度是H + 2P。核长度 K,最后一个合法窗口的起点必须满足start + K <= H + 2P,也就是start <= H + 2P - K。起点从 0 开始、步长 S,能取的起点个数就是floor((H + 2P - K) / S) + 1。那个 +1 是因为起点 0 也算一个窗口,手算时漏掉它是最常见的翻车方式。
空洞卷积要把 K 换成有效核尺寸K_eff = d * (K - 1) + 1。d=2、K=3 时 K_eff=5,感受野等效 5×5,但参数还是 9 个。
| 参数 | 作用 | 对输出尺寸的影响 | 主要代价 |
|---|---|---|---|
| padding P | 在边界补零 | 输出变大,P=K//2 且 S=1 时尺寸不变 | 边界引入虚假零值 |
| stride S | 滑窗步长 | 输出约缩小为 1/S | 信息丢失,可能出现混叠 |
| dilation d | 核内插空 | 输出缩小,等效核变大为 d(K-1)+1 | 网格效应,局部不连续 |
| kernel K | 核尺寸 | 输出变小 | 参数按 K² 增长 |
这四个参数不是自由的:改 S 通常要同步改 K,改 d 通常要保持d < K,否则核中间的空洞比有效区域还大,采样会退化成一堆孤立的点。
3.3 参数量和计算量,两个完全不同的账
参数量C_out * (C_in * K * K + 1),只和通道数、核大小有关,和输入分辨率无关。计算量(乘加次数)Ho * Wo * C_out * C_in * K * K,和输出分辨率成正比。
这个区别决定了很多工程决策。想在移动端减参数量,砍通道数、换小核就够;想减推理延迟,光砍参数没用,必须砍输出分辨率或者用深度可分离卷积,因为延迟主要由计算量决定。见过不少模型参数量只有几 MB,推理却慢得离谱,原因就是早期层保持了高分辨率、通道数又给得很足,计算量全堆在那里。
3.4 为什么两个 3×3 胜过一个 5×5
两个 3×3 卷积堆叠,感受野等于 5×5,参数量是2 * 9 = 18,而单个 5×5 是 25。三个 3×3 叠出 7×7 感受野,参数 27,单个 7×7 要 49。越往上差距越大,这是 VGG 式结构的基本动机。
但参数少只是表面理由。更关键的是中间多了一次非线性激活——两个 3×3 之间存在一个 ReLU,整个映射的表示能力比单个 5×5 的线性卷积更强。反过来说,如果两个卷积之间没有激活函数,那它们完全可以被一个等价的 5×5 核合并,堆叠就白堆了。这也是排查不收敛时要先看激活函数的原因。
3.5 池化与激活:卷积层之外真正决定能不能训起来的部分
池化层做的是降采样。Max Pooling 取窗口最大值,反向传播时梯度只回传给窗口内取到最大值的那一个位置,其余位置梯度为 0;Average Pooling 把梯度平均分给窗口内每个位置。这个差别在多任务里很关键:需要精确位置的任务(分割、检测)用 max pooling 会导致梯度稀疏,位置信息也更难恢复。
池化的收益是三重叠加的:特征图变小、计算量下降、感受野按比例扩大、局部平移不变性增强。代价同样明确——它扔掉了窗口内的位置细节,对小目标和密集排列的物体不友好。所以现代分割网络里,池化层越来越少,更多用 stride=2 的卷积或者空洞卷积来替代,因为卷积至少还能学。
激活函数的存在理由更硬:卷积是线性算子,两个线性算子复合还是线性的,堆一百层卷积不加激活,等价于一个巨大的线性滤波器,拟合能力和一层没区别。ReLU 之所以赢过 sigmoid,核心在导数——sigmoid 在饱和区导数趋近于 0,梯度一层层乘下去就消失了;ReLU 正半轴导数恒为 1,梯度能原样传回去。代价是负半轴导数为 0,神经元可能“死掉”,这才有了 LeakyReLU 之类的补丁。
4. 变种卷积:每一种都在补标准卷积的某块短板
4.1 转置卷积:把尺寸反推回去,以及棋盘格是怎么来的
转置卷积常被叫成“反卷积”,这个叫法误导性很强——它不是卷积的逆运算,不会还原出原来的输入,它只是让输出尺寸变大的一种上采样卷积。名字里的“转置”来自矩阵视角:如果标准卷积的前向可以写成输入向量左乘一个稀疏矩阵 W,那转置卷积就是左乘 W 的转置。
尺寸关系是H_out = (H_in - 1) * S - 2P + K + output_padding。注意H_in - 1而不是H_in:因为输出尺寸主要由“输入元素之间插入的间隔”决定,首尾元素的贡献是固定的。
def deconv_out_size(h_in, k, stride=1, pad=0, out_pad=0): # 与 ConvTranspose2d 的尺寸公式一致 return (h_in - 1) * stride - 2 * pad + k + out_pad # 常见的 2 倍上采样:h_in=16 -> 32 print(deconv_out_size(16, k=4, stride=2, pad=1)) # 32 print(deconv_out_size(16, k=3, stride=2, pad=1, out_pad=1)) # 32两种配置都能把 16 变成 32,但性质不同。k=4、S=2 时核尺寸能被步长整除,每个输出位置收到的贡献次数相同;k=3、S=2 时不能整除,某些输出位置收到 2 次贡献,相邻位置只收到 1 次,累加后强度不均,就形成了规律的棋盘格纹理。
这就是转置卷积最经典的问题:现象是生成或分割结果上出现规则网格纹,原因是核尺寸与步长不整除导致重叠不均,解决办法是让 K 能被 S 整除(S=2 就用 K=4),或者干脆先做最近邻/双线性插值放大,再接一个普通 3×3 卷积修细节。后一种做法在实践中更稳,也更容易调。
4.2 空洞卷积:不增加参数地扩大感受野
标准卷积要扩大感受野只有三条路:加大核、加深网络、加池化。三条路都会带来额外代价。空洞卷积提供第四条:在核元素之间插零,让同样的 9 个参数覆盖更大的区域。K=3、d=2 时,9 个采样点分布在 5×5 的范围内,有效核 5×5。
代价是网格效应。d=2 时,采样点之间隔着一个像素,那个像素永远不参与这一层的计算。单层还好,多层连续使用同样的 dilation,会出现大量“从未被采样”的像素,对细小目标和密集预测任务影响明显。常见的补救是混合不同 dilation(比如 1、2、5 交替),让采样点相互覆盖;或者在深层用普通卷积收尾。
d < K这条约束不是随便定的。d >= K 时,有效区域内会出现大片连续的未采样区,核实际上退化成了几个孤立的点采样,卷积的局部性假设被彻底破坏。
4.3 深度可分离卷积与组卷积:从通道维度砍计算量
标准卷积的计算量是Ho * Wo * C_in * C_out * K * K,其中C_in * C_out这一项在通道数大时占绝对主导。深度可分离卷积把它拆成两步:
第一步 depthwise,每个输入通道配一个独立的 K×K 核,只在自己通道上滑动,计算量Ho * Wo * C_in * K * K;第二步 pointwise,用 1×1 卷积把C_in个通道线性组合成C_out个,计算量Ho * Wo * C_in * C_out。
def sep_conv_cost(h, w, c_in, c_out, k=3): std = h * w * c_in * c_out * k * k # 标准卷积 dw = h * w * c_in * k * k # 逐通道卷积,通道间不交互 pw = h * w * c_in * c_out # 1x1 卷积,只做通道混合 return std, dw + pw, (dw + pw) / std std, sep, ratio = sep_conv_cost(56, 56, 128, 256, k=3) print(f"标准 {std/1e6:.1f}M 可分离 {sep/1e6:.1f}M 占比 {ratio:.3f}") # 标准 2312.0M 可分离 267.4M 占比 0.116理论比值是1/C_out + 1/K²。K=3、C_out=256 时约等于 1/9,实测 0.116 与它吻合。这个省法有个隐含前提:depthwise 那一步各通道之间完全没有信息交换,表达能力被削弱,所以 pointwise 那一层不能省,而且通道数往往要给得比标准卷积更宽来补偿。
组卷积是同一个思路的连续版本:把通道切成 g 组,组内做标准卷积,组间不通信。g=1 退化成标准卷积,g=C_in 就是 depthwise。它最初是为了把模型切到多张卡上训练,后来发现精度损失可以接受,就成了压缩手段。有意思的是 1×1 卷积可以看成g = C_in的极端情形,只不过它是把每组的单个通道重新组合,所以跨通道能力还在。
4.4 可变形卷积与 3D 卷积:把几何先验也交给网络
可变形卷积把“采样点必须在固定网格上”这条假设也拆了。它在标准网格上额外学一组偏移量(Δx, Δy),实际采样位置变成p + p_n + Δp。由于偏移是小数,采样要用双线性插值。偏移量本身由一个平行的小卷积分支预测出来,初始化为 0 时整个模块退化为标准卷积,这个初始化设计很关键——它保证了训练初期不会因为随机偏移把特征图搅乱。
代价有两块:额外参数和显存(偏移分支的卷积核),以及不规则的内存访问模式——双线性插值要在四个非连续地址取值,GPU 上很难向量化,实测推理速度通常比标准卷积慢,加速比不如理论计算量看起来那么漂亮。
3D 卷积把核从(C_in, C_out, kh, kw)扩成(C_in, C_out, kt, kh, kw),多出的时间维用来处理视频或体数据。参数和计算量都按kt线性增长,输入是 16 帧时,一个 3D 卷积的开销是 2D 的 16 倍左右。所以主流做法是:浅层用 2D 卷积提空间特征,深层或少量层用 3D 卷积建时间关系,或者把 3D 拆成空间 2D 加时间 1D。
5. 排查清单:尺寸对不上、不收敛、显存爆掉,各有各的根
5.1 尺寸类:公式算出来的输出是负数
现象很直白,要么是框架抛 shape mismatch,要么是手算的 Ho 出现负数或 0。
原因通常是三种:padding 给小了,H + 2P - K_eff变成负值;stride 给大了,可用长度不够滑一步;用了空洞卷积却忘了把 K 换成d*(K-1)+1,按原核大小算出来的尺寸比实际大一圈。
解决顺序建议固定成:先写出K_eff,再套公式,最后检查整除方向。公式里的除法必须是向下取整,不是四舍五入——差一个像素就会在后续的拼接(concat)或残差相加时报错,而错误位置往往离真正的原因隔了好几层,看 traceback 很难定位。
提示:残差结构要求主分支和 shortcut 的输出尺寸、通道数都一致,改动 stride 或通道数时两边必须同步改,否则会在相加那一步炸掉。
5.2 不收敛类:loss 卡在某个值一动不动
现象是 loss 从第一个 epoch 就停在 0.69 附近(二分类的随机水平),或者完全不降,或者几步之后直接变 NaN。
最常见的三个原因,按出现频率排:忘了加激活函数,多层卷积退化成单个线性变换,模型容量和逻辑回归差不多;学习率过大,第一步就把权重推到饱和区,梯度全变成 0;数据标签错位,比如图片按文件名排序读、标签按另一个列表顺序读,shuffle 之后整批全错,这种情况下 loss 会稳定在一个略高于随机的高位。
定位方法我一般这么走:先拿 20 到 50 张图,关掉所有正则化和数据增强,让模型去死记硬背。如果连这几张都过拟合不到 100%,问题一定在模型结构、损失函数或标签管线上,和数据量、泛化毫无关系。这一步能省掉大量瞎调超参的时间。
5.3 显存类:参数量明明很小,却 OOM
现象是模型文件只有几 MB,一开训就报显存不足,或者把 batch 从 8 调到 4 能跑、调到 6 又炸。
原因是显存的大头从来不是参数,而是激活值。前向时要保存每一层的输出供反向使用,占用和batch * 通道数 * 高 * 宽 * 层数成正比。im2col 还会额外放大:展开后的矩阵是输入尺寸的K*K倍,3×3 卷积就是 9 倍,这一块临时内存经常被忽略。
还有一个隐蔽的坑:验证或推理时忘了包torch.no_grad(),计算图会一直累积,跑几十个 batch 后显存越用越多,最后炸在一个看起来毫不相关的位置。排查时先看显存是不是随时间线性增长,是的话基本就是这个原因。
5.4 过拟合与“好得不像话”的准确率
现象是验证集准确率高得离谱,或者训练集 99%、测试集掉到 60%。
前者通常是数据泄漏:同一张原图裁出的多个 patch 被随机划分到了训练集和验证集,模型在验证集上看到的其实是训练时见过的内容。解决办法是按原图 ID 或采集批次划分,不是按裁剪后的小图划分。后者常见于预处理不一致——训练时做了归一化、验证时忘了,或者 BatchNorm 在 eval 模式下用的是没校准好的 running stats。
BatchNorm 这一条值得单独说:训练时统计量是每个 batch 现算的,推理时用滑动平均。如果训练轮数太少,滑动平均还没收敛,直接切 eval 会看到精度断崖式下跌。稳妥做法是切 eval 之前先拿一批训练数据跑几次前向,让统计量稳定下来。
5.5 转置卷积的棋盘格,和它在分割任务里的连锁反应
现象是生成图像或分割掩码上出现规则的网格状明暗纹理,放大看像马赛克。
原因在 4.1 已经说过:K 不能被 S 整除时,不同输出位置收到的累加次数不同,重叠少的区域强度偏低,形成周期性图案。分割任务里这个问题的危害被放大——边界附近的预测会沿着网格抖动,IoU 掉得比肉眼看到的更明显。
解决就两条路:让 K 能被 S 整除(S=2 配 K=4),或者用“插值上采样 + 3×3 卷积”替代转置卷积。后者多了几个参数,但输出平滑、没有周期性伪影,在需要精细边界的任务里更值得。
6. 感受野与精度的取舍:加深、加宽,还是回头改数据
模型不work的时候,第一反应往往是加层、加通道,但真正该先算的是感受野够不够。有效感受野的粗算方式是逐层累加:每层增加(K_eff - 1) * 之前所有层的 stride 乘积。一个 224 输入的骨干,经过四五个 stride=2 的下采样后,深层的理论感受野早就超过整张图了,但实际有效感受野远小于理论值——高斯分布式的权重让边缘像素的贡献微乎其微。所以判断标准不是“理论感受野大于目标尺寸”,而是“目标尺寸落在有效感受野的中心区域”。
加深和加宽的取舍也有个大致边界。加深提升的是特征抽象层级和感受野,但梯度路径变长,需要残差连接和归一化兜底;加宽提升的是单层能表达的模式种类数,并行度好、训练更稳,但参数量和显存增长是平方级的。经验上,在数据量中等(几万张)的场景里,先把深度堆到能覆盖目标尺度,再考虑加宽,比反过来划算得多。深度可分离卷积这类结构,本质上就是用“加宽但每层更便宜”换“加深但每层更贵”。
真正值得先动的是数据。我踩过最贵的一次坑,是花了很久调结构和学习率,最后发现验证集划分时把同一段视频的相邻帧拆散了,模型其实在背帧。修正划分方式之后,同一个网络结构的指标直接上了一个台阶,之前调的参数全白费。从那以后我每次开新任务,都强制先走一遍三件事:打印每个类的样本数和划分来源、拿几十张图做单批过拟合验证、把预处理管线固化成同一个函数给训练和推理共用。这三步不做完,调参调到天亮也大概率是在给数据问题打补丁。
这套课件把 CNN 从手工特征到各种变种卷积的链路串成了一条线,适合对着源码一页页往回推——先想清楚每一层在押什么先验、代价是什么,再去看实现里那些参数为什么这么设。希望帮到你。
本文还有配套的精品资源,点击获取