ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CNN卷积神经网络入门:核心原理、参数计算与手写数字识别实战

CNN卷积神经网络入门:核心原理、参数计算与手写数字识别实战 1. 从一张图说起为什么图像任务绕不开CNN很多人第一次接触深度学习都是从一张手写数字或者一只猫的图片开始的。你把手里的图片喂给一个普通的全连接网络它也能跑甚至小数据集上效果还行。但一旦图片分辨率上去了参数量就像脱缰的野马训练慢、显存炸、还特别容易过拟合。这个问题困扰了早期做图像研究的人很久直到卷积神经网络CNN这套思路被真正用起来局面才彻底改变。CNN卷积神经网络说白了就是一种专门为处理网格状数据图像、音频频谱、时序信号都算设计的网络结构。它的核心卖点有两个一是局部连接二是权值共享。这两个词听起来抽象但你只要理解了它们想解决什么后面所有细节就都能顺下来。这篇文章我打算从头讲清楚CNN到底在干什么配上一套能直接跑的手写数字实操再把参数计算、感受野、常见坑都捋一遍。不管你是刚入门的学生还是从传统机器学习转过来的工程师看完应该都能自己搭一个小网络出来。我个人的习惯是学任何结构之前先问一句它为什么长这样CNN的答案就藏在图像本身的特性里。一张图里相邻像素高度相关远处的像素基本没关系一只猫挪到图片左上角还是右下角它还是猫。这两个朴素的观察恰好对应了CNN的局部连接和权值共享——前者减少了无效连接后者让网络对位置不那么敏感。理解了这层动机再去啃卷积核、通道、步长这些名词就不会觉得是在背天书了。这篇文章适合三类人正在学CNN但被各种术语绕晕的新手、想复习底层原理准备面试的同学、以及需要给团队做技术分享但缺一份通俗讲义的工程师。我会尽量用生活化的例子代替公式堆砌但关键的数学和代码一个不落尤其是卷积输出尺寸、参数量这些面试和工作里真会用到的东西。2. CNN的核心部件拆解每个零件到底在干什么2.1 卷积层一块滑动的小窗口如何扫出特征卷积层是CNN的心脏。你可以把卷积核想象成一个放大镜它只有巴掌大比如3x3或者5x5但它会在整张图上从左到右、从上到下滑动。每滑到一个位置就把覆盖区域的像素值和卷积核里的权重做一次逐元素相乘再求和得到一个数。滑完整张图这些数就拼成了一张新的图我们叫它特征图feature map。这里的直觉是这样的卷积核里的权重就是它想找的模式。如果某个3x3的卷积核学出来的权重是中间亮、四周暗那它就特别擅长在图上找到亮点出现的位置。训练的过程其实就是让这些卷积核慢慢学出对分类最有用的模式——有的核负责找边缘有的找角点有的找纹理越往后越抽象。有几个参数必须搞明白卷积核大小kernel size常见3x3、5x5。3x3是绝对的主流因为两个3x3堆叠的感受野等于一个5x5但参数更少、非线性更强。步长stride滑动一次走几格。stride1是逐像素滑stride2会把特征图尺寸砍掉大约一半常用于下采样。填充padding在图片边缘补一圈0目的是让输出尺寸可控。不做padding的话每卷一层图就小一圈几层之后边缘信息全丢光了。通道数channels一个卷积层通常有多个卷积核比如64个那它就输出64张特征图。每个核独立学习一种模式。注意卷积核的深度必须和输入的通道数一致。输入是3通道RGB图那你的3x3卷积核实际形状是3x3x3算的时候是把三个通道的结果加起来再输出一个值。新手最容易在这里犯迷糊以为3x3核只管一个通道。举个具体例子帮你建立手感。假设输入是一张5x5的单通道图卷积核是3x3stride1padding0那么输出就是3x3。计算过程是把3x3的核盖在左上角对应位置相乘求和得到输出左上角那个值然后右移一格再算一次滑完一行下移一格。总共滑了(5-31) x (5-31) 3x3次。2.2 池化层浓缩信息而不是简单丢弃池化层常被人误解成降采样就是丢信息其实它更像是在做摘要。最常用的是最大池化max pooling拿一个2x2的窗口在特征图上滑每次取窗口里最大的那个数保留下来。这样特征图边长减半但保留下来的都是每个小区域里最强的响应。为什么取最大而不是平均因为对图像来说一个特征有没有出现比出现得多强更重要。最大值池化相当于在告诉后面的层这个2x2的格子里我关注的这个模式确实存在。平均池化average pooling则更平滑早期LeNet-5用的就是平均池化现在主流网络更偏爱最大池化。池化的好处有三个一是显著减少参数量和计算量二是扩大感受野三是提供一定的平移不变性——就算特征在2x2窗口内挪了一格最大值大概率还是它输出基本不变。这个性质对分类任务特别友好因为我们不关心猫在图片的哪个精确坐标。提示池化层没有可学习的参数。它就是个固定的下采样规则所以你会发现网络的参数量基本都集中在卷积层和全连接层。2.3 激活函数给网络装上非线性开关如果只有卷积和池化整个网络本质上是线性的无论堆多深都能被一个线性变换等价替换掉。这对复杂任务来说是灾难。激活函数的作用就是往里面注入非线性让网络能拟合任意复杂的函数。ReLURectified Linear Unit是目前卷积网络里的标配公式简单到不能再简单f(x) max(0, x)。小于0的变0大于0的原样输出。它的优点是计算极快而且梯度在正区间恒为1能有效缓解深层网络的梯度消失问题。相比之下早期的Sigmoid和Tanh在两端梯度趋近于0网络一深就训不动了。ReLU也有缺点就是神经元死亡——如果某个神经元的输入长期为负它的梯度永远是0权重再也不更新这个神经元就废了。为了解决这个后来出现了Leaky ReLU、ELU、GELU等变体。Leaky ReLU给负区间留了一个很小的斜率比如0.01保证梯度不会完全断掉。实际项目中ReLU通常够用遇到大量神经元死亡再考虑换Leaky ReLU。2.4 全连接层与输出层从特征到最终判定经过若干轮卷积和池化之后我们得到的是一个个三维的特征图宽 x 高 x 通道。全连接层要做的事就是把这些特征图压平成一维向量然后通过一到两层普通神经网络映射到最终的类别分数上。比如做一个10类的手写数字识别最后全连接层输出10个数分别代表这张图属于0到9的原始得分。这些得分通常要经过Softmax函数转成概率训练时用交叉熵损失来衡量预测和真实标签的差距。现在很多新式网络比如ResNet后期版本会用全局平均池化GAP替代全连接层直接把每个通道的特征图取平均得到一个数这样参数量几乎为零还能减少过拟合。这是后话新手阶段先把全连接层用熟没问题。3. 关键参数与计算过程手把手算一遍3.1 卷积输出尺寸的计算公式与推导这个公式必须背下来工作和面试都会用到输出尺寸 (输入尺寸 - 卷积核大小 2 * padding) / stride 1当结果不是整数时说明你选的参数组合不合法需要调整padding或stride让它整除。我们拿几个真实配置验算一下。假设输入是32x32的单通道图配置一kernel5, padding0, stride1。输出 (32-50)/11 28。所以得到28x28。配置二kernel3, padding1, stride1。输出 (32-32)/11 32。尺寸不变这叫same padding是保持尺寸最常用的组合。配置三kernel3, padding1, stride2。输出 (32-32)/21 16.5不合法。改成padding1、kernel3、stride2输入得是偶数才好算。为什么padding1配kernel3能保持尺寸因为卷积核在边缘覆盖不到的地方补了一圈0相当于把输入从32扩到了3434-3132正好回到原尺寸。这个组合在ResNet等网络里到处都是记住它。再补一个多通道的例子。输入是224x224x3卷积层有64个3x3的核padding1stride1输出空间尺寸 (224-32)/11 224不变。输出通道数 卷积核个数 64。所以输出是224x224x64。3.2 参数量怎么算CNN为什么比全连接省这么多卷积层的参数量公式是参数量 卷积核个数 * (卷积核大小 * 卷积核大小 * 输入通道数 1)那个1是每个卷积核对应的偏置项bias。还拿上面那个例子64个3x3的核输入3通道。参数量 64 * (3 * 3 * 3 1) 64 * 28 1792不到两千个参数。现在换成全连接层做同样的事输入224x224x3 150528个像素输出假设也是150528维极端情况参数量就是150528 * 150528大约226亿。就算输出只到1000维也有1.5亿参数是卷积方案的8万倍。这就是CNN能在图像任务上碾压全连接网络的根本原因。权值共享意味着同一个卷积核在整张图上用同一套权重参数量只跟核大小和通道数有关跟图片尺寸无关。图片从224涨到448卷积层参数量一点不变全连接层直接涨4倍。实操心得很多新手设计网络时喜欢一上来就堆全连接层结果模型动辄几亿参数。我的建议是卷积层负责提特征全连接层只在最后收尾一般不超过两层而且维度别设太大。3.3 感受野网络看到的范围是怎么长大的感受野receptive field指的是特征图上一个点对应原始输入图上的区域大小。这个概念很重要因为网络能不能识别一个大物体取决于深层特征的感受野够不够大。感受野是逐层累积的计算公式稍微绕一点但记住思路就行越深的层感受野越大。一个粗略的近似是连续堆n个3x3卷积stride1感受野大约是2n1。比如堆3层3x3感受野约7x7堆5层约11x11。精确计算可以用迭代公式RF_new RF_old (kernel_size - 1) * 前面所有层的stride乘积举个例子输入图第一层3x3stride1感受野3。第二层3x3stride1感受野 3 (3-1)*1 5。第三层加一个stride2的3x3感受野 5 (3-1)*1 7这一步前面stride乘积还是1。如果我们想让感受野快速变大可以在深层用带stride的卷积或者池化它们会把有效步长放大。理解了感受野你就能明白为什么目标检测任务要定位大物体需要很深的网络而简单分类只看整体类别浅一点也够用。4. 一个完整实操从零训练手写数字识别4.1 数据准备与预处理讲了一堆原理不动手永远学不会。我们拿MNIST手写数字数据集做例子这是深度学习的Hello World几乎零门槛CPU上几分钟就能跑完。MNIST包含60000张训练图和10000张测试图每张是28x28的灰度图标签是0到9。数据预处理主要做三件事import torch from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), # 转成张量并把像素归一化到[0,1] transforms.Normalize((0.1307,), (0.3081,)) # 减均值除标准差加速收敛 ]) train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader torch.utils.data.DataLoader(train_set, batch_size64, shuffleTrue) test_loader torch.utils.data.DataLoader(test_set, batch_size1000, shuffleFalse)那几个均值0.1307和标准差0.3081是MNIST全局统计出来的经验值。归一化的目的是让输入分布集中在0附近、方差为1这样梯度更新更稳定收敛更快。新手常忽略这一步结果训练前期loss震荡得厉害。注意归一化用的均值方差必须和训练时一致测试和上线时不能各算各的。这是数据预处理最容易出大坑的地方。4.2 网络结构设计与逐层解析我们先搭一个经典的LeNet-5风格的小网络。LeNet-5是1998年提出的是CNN的开山之作结构看着简单但吃透了受益无穷。import torch.nn as nn import torch.nn.functional as F class LeNet5(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 6, kernel_size5, padding2) # 28x28 - 28x28 self.conv2 nn.Conv2d(6, 16, kernel_size5) # 14x14 - 10x10 self.fc1 nn.Linear(16 * 5 * 5, 120) self.fc2 nn.Linear(120, 84) self.fc3 nn.Linear(84, 10) def forward(self, x): x F.max_pool2d(F.relu(self.conv1(x)), 2) # 28x28 - 14x14 x F.max_pool2d(F.relu(self.conv2(x)), 2) # 10x10 - 5x5 x torch.flatten(x, 1) # 展平成 16*5*5 400 x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) x self.fc3(x) return x逐层过一遍算清楚尺寸变化输入1x28x28单通道灰度图。conv16个5x5核padding2。输出尺寸 (28-54)/11 28通道变6得到6x28x28。池化2x2最大池化尺寸减半得到6x14x14。conv216个5x5核无padding。输出 (14-5)/11 10通道变16得到16x10x10。池化再减半得到16x5x5。展平1655 400维向量。fc1400 - 120加ReLU。fc2120 - 84加ReLU。fc384 - 10输出10类得分。为什么conv1要padding2因为28x28的图如果用5x5核不padding会变成24x24再池化更小边缘信息损失快。padding2正好保持尺寸让第一层尽量多吃点原始信息。4.3 训练配置与调参记录训练循环的模板基本固定device torch.device(cuda if torch.cuda.is_available() else cpu) model LeNet5().to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(10): model.train() for data, target in train_loader: data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() # 每个epoch结束后在测试集上评估 model.eval() correct 0 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) pred model(data).argmax(dim1) correct pred.eq(target).sum().item() print(fEpoch {epoch}: acc {correct / 10000:.4f})几个关键选择的理由优化器选Adam自适应学习率对超参不敏感新手友好。SGD收敛更平滑但需要仔细调lr和momentum入门阶段先不折腾。学习率1e-3Adam的经典起点。训练loss不降就往下调到1e-4震荡就调到5e-4。交叉熵损失多分类标配内部已经包含了Softmax所以网络最后不要手动加Softmax否则会重复。这套配置在MNIST上10个epoch能跑到99%以上准确率。如果只有97%左右多半是归一化漏了或者学习率太大。实操心得训练时务必先跑一两个batch看loss有没有在动。如果loss一直不动或者变成nan八成是学习率太大、输入没归一化、或者标签对错了。这个先小样本验证的习惯能帮你省下大量无谓的等待时间。4.4 参数量与显存占用核对训练前我很喜欢先算一遍参数量确认模型大小合理。LeNet-5的参数层计算方式参数量conv16*(5511)156conv216*(5561)2416fc1120*(4001)48120fc284*(1201)10164fc310*(841)850合计61706才6万多个参数模型体积不到300KB推理速度飞快。这也解释了为什么LeNet能在早期算力有限的设备上落地。对比一下如果我们把conv1换成256个核、conv2换成512个核参数量和显存会迅速上涨但MNIST这种简单任务并不需要。模型容量要匹配任务难度这是设计网络时最重要的原则之一。任务简单就上大网络只会过拟合。5. 典型场景与结构变体CNN不只会认猫5.1 图像分类、目标检测与语义分割CNN最基础的应用是图像分类给一张图输出一个类别。但现实需求远不止于此。目标检测要在图上画出每个物体的框并标出类别。经典的两阶段方法如Faster R-CNN先找出候选区域再对每个区域分类单阶段方法如YOLO、SSD直接一步预测框和类别速度更快。它们都是在CNN骨干网络提特征的基础上加检测头。语义分割要做到像素级分类给每个像素打上类别标签。FCN全卷积网络是里程碑它把全连接层换成卷积层让网络能输出和输入同尺寸的分割图。后来的U-Net用编码器-解码器加跳跃连接在医学影像分割上非常流行。这三种任务的共同点是底层都在用卷积提特征区别只在输出头的设计。理解了CNN的骨架转向这些任务只是换个尾巴的事。5.2 3D卷积与视频、医学影像普通卷积处理的是二维图像高x宽3D卷积则多了一个深度维度处理的是体数据比如视频时间维或者CT/MRI空间深度。3D卷积核的形状是k x k x k它在立方体上滑动。参数量比2D卷积大计算也更贵但换来的是对时空信息的联合建模。视频动作识别、肺结节检测这类任务3D CNN是标配。代价也明显同样尺寸下3D卷积的参数量是2D的k倍显存很容易吃紧所以通常配合更激进的下采样。提示数据量不大的时候慎用3D CNN。三维数据标注成本高参数量又大很容易过拟合。可以考虑先用2D卷积逐帧处理再在时间维度上做融合。5.3 从LeNet到现代骨干结构演进的主线CNN的演进有一条清晰的主线越堆越深同时解决深层带来的退化问题。LeNet-51998最早的成功实践证明卷积池化能提特征。AlexNet2012加深到8层用ReLU和Dropout在ImageNet上一战成名。VGG2014全部用3x3小核堆叠结构规整证明深度是关键。GoogLeNet2014提出Inception模块多尺度并行卷积兼顾宽度和深度。ResNet2015引入残差连接x F(x)让梯度能直接回传一举把网络做到上百层。后续的DenseNet、EfficientNet等继续在连接方式和缩放策略上做文章。对新手来说不必每个都实现一遍但ResNet的残差思想必须理解。它解决了网络越深反而越差的退化问题是后面几乎所有深层网络的基础。6. 常见问题与排查技巧实录6.1 训练不收敛怎么一步步排查训练loss不动或者乱跳按下面顺序查输入是否归一化这是最高频的坑。像素值没归一化到合理范围梯度会爆炸。学习率是否过大loss变nan先砍lr从1e-3降到1e-4甚至1e-5试试。标签是否正确分类任务里标签越界、顺序错乱很常见确认类别数和标签范围对得上。网络最后有没有多加Softmax用CrossEntropyLoss时多加一层Softmax会导致梯度异常。参数初始化默认初始化一般够用但全零初始化会让所有神经元学一样的东西绝对不能用。6.2 过拟合与欠拟合的判断和应对看训练集和验证集的准确率差距现象训练准确率验证准确率判断应对欠拟合低低模型能力不足加深网络、加训练轮数、调大学习率良好高高且接近正常保持过拟合很高明显低记住了训练集加数据、加Dropout、加正则、早停过拟合的应对手段里数据增强性价比最高。对图像做随机裁剪、翻转、旋转、颜色扰动能让模型看到更多样的样本泛化能力显著提升。Dropout则是在训练时随机关掉一部分神经元逼网络学出更鲁棒的特征。实操心得别一上来就上数据增强。先把模型跑通、确认能过拟合到100%哪怕在小样本上再逐步加正则。如果连过拟合都做不到说明模型或数据有问题加正则只会掩盖问题。6.3 常见问题速查表问题可能原因排查动作loss变nanlr过大、输入未归一化降lr、检查预处理准确率卡在10%10类标签对错、输出层维度错打印标签分布、核对输出维度训练快验证慢过拟合加数据增强、加Dropout显存爆了batch太大、模型太大减小batch、加梯度累积卷积输出尺寸算不对padding/stride组合不合法用公式重算保证整除验证准确率忽高忽低batch太小、没shuffle增大batch、开启shuffle6.4 卷积核里到底学出了什么很多人好奇训练完的卷积核长什么样。第一层的卷积核通常能看出明显的边缘、条纹、颜色斑块因为它在处理最原始的像素。越往后的层特征图越抽象人眼已经看不懂了。可视化特征图用matplotlib把中间层输出画出来是理解网络行为的好方法也常用来排查模型是不是真的学到了有用的东西。如果所有特征图都长一个样说明网络退化了多半是学习率或初始化出了问题。7. 几个踩过坑才明白的经验关于图像处理为啥用CNN不用前馈神经网络最直白的答案已经摆在参数量的对比里了。我见过太多人拿着全连接网络硬套图片模型几亿参数训练半天还跑不过一个6万参数的LeNet。这不是网络不行是结构跟数据特性不匹配。CNN的局部连接和权值共享正是为图像的局部相关和平移不变量身定做的。再补一个小提醒batch size、学习率、网络深度这几个超参在初期不用追求最优。先把pipeline跑通拿到一个能用的baseline再一个个调。我早期最浪费时间的事就是模型还没跑通就在纠结用3x3还是5x5、用Adam还是SGD。后来养成先跑通、再优化的习惯效率高了不止一倍。如果你已经能熟练搭CNN下一步可以往两个方向走一是深入现代骨干网络重点啃ResNet的残差连接和BatchNorm二是转向具体任务比如目标检测或分割把backbone换成预训练权重做迁移学习。迁移学习是你实际工作中最常用的技能比从零训练划算得多。这些内容后面有机会我再单独展开聊。
返回列表