
1. CNN为什么存在从一次全连接网络的翻车说起先讲一个我刚开始接触深度学习时的真实经历。当时我拿到一个手写数字识别任务觉得神经网络这东西不就是一个接一个的全连接层吗把图片拉平成一维向量丢进网络里反正梯度下降会自己调整权重。于是我搭了一个只有三层的全连接网络输入层直接接了784个像素点28x28的灰度图训练完一测准确率大概在91%左右。91%听起来还行但我很快发现两个问题。第一我把图片稍微平移几个像素或者把数字写得潦草一点模型立刻开始胡说八道。第二我试着把网络加深加宽参数从几万涨到几百万准确率却纹丝不动甚至开始下降。那时候我才意识到全连接网络处理图片这件事从根上就是别扭的。因为它压根不理解图片是一种有空间结构的数据。1.1 全连接网络处理图片的三个致命伤第一个致命伤是参数爆炸。一张普通的生活照常见的分辨率是1920x1080也就是大约两百万个像素。如果是彩色图片还得乘上RGB三个通道那就是六百万个数值。全连接层的每个神经元要和输入的每个数值相连第一层如果想输出一千个神经元那这一层的权重数量就是六百万乘一千足足六十亿个参数。你根本不可能有那么多数据去训练它。第二个致命伤是破坏空间结构。把图片拉成一维向量等于把旁边像素和中间像素的邻居关系彻底打散了。图片里一只猫和一只狗区别往往不在于某一个孤立的像素值而在于哪些像素组成了眼睛、哪些像素组成了耳朵。这种局部相关性全连接层完全没法利用。第三个致命伤是对位置过于敏感。同一只猫平移五个像素在像素层面就是完全不同的输入向量网络只能靠硬记去适应。换句话说它没有平移不变性的先天能力。那CNN卷积神经网络是怎么破局的它做了三个根本性的改变每个神经元只看图片的一小块局部区域、同一块卷积核在所有位置共享同一组参数、通过池化逐步合并局部信息来扩大视野。这三个改变让CNN天然适合图片这类带有空间结构的数据参数数量也控制在了合理范围内。2. 卷积层到底在做什么不只是提取特征这四个字很多人讲卷积层就丢下一句它的作用是提取特征然后就开始上公式。公式我能理解但如果只讲公式初学者根本不知道卷积核里的数字到底是怎么来的、为什么能提取特征。2.1 卷积运算的直观理解你可以把卷积核想象成一张小的模板或探照灯。比如一个3x3的卷积核就是一个三行三列的小窗口里面有九个数。运算的时候这个小窗口从图片左上角开始和对应位置的九个数逐格相乘再全部加起来得到一个输出值。然后窗口向右滑动一个单位继续做同样的操作直到扫完整个图片。等等这到底在干嘛我换个说法你就明白了。假设你的卷积核是这样的-1 -1 -1 -1 8 -1 -1 -1 -1这个核的中心权重大周围权重是负的。当一个像素点比它周围八个点都亮得多的时候计算结果就会很大反之如果周围也亮、中间也亮结果就接近零。所以这个卷积核实际上在检测中间有一个亮点的模式也就是一个孤立的小亮点或边缘。换个卷积核比如横向的[1, 0, -1]它检测的就是左右两侧的亮度差异也就是垂直边缘。所以卷积核的参数不是听天由命的它是网络在训练中一点点学出来的。一开始初始化成随机数经过反向传播不断调整最终每个卷积核会自发变成某种特定的滤波器有的检测横边有的检测竖边有的检测纹理深层网络里的卷积核甚至能组合出眼睛轮子这种复杂概念。2.2 感受野与参数共享卷积里有两个词是理解整个网络的关键一个是感受野一个是参数共享。感受野可以理解为一个输出像素对应到原始图片上的多大区域。一个3x3的卷积核卷积一次每个输出像素看的就是3x3的邻居区域感受野是3x3。如果再来一个3x3的卷积层第二次卷积的每个输出像素就看了第一次输出的3x3区域而那个区域里的每个值又各自对应原图的3x3所以第二次的输出实际上对应原图的5x5区域。这就是为什么网络越深高层能看到的信息范围越大。刚开始学CNN的人会觉得为什么不多堆几层大卷积核直接用7x7不就行了道理在于用两层3x3卷积替代一层7x7卷积参数更少而且中间多了一层非线性变换表达能力强得多。后面我会专门算这笔账。参数共享的意思是同一个卷积核在扫描全图的过程中权重是不变的。这带来两个好处一是参数数量只取决于卷积核大小和数量和图片尺寸无关二是网络天然拥有了平移不变性——猫在图片左边还是右边只要局部模式相同响应的强度一致。当然严格来说CNN不是完全平移不变的因为边界和池化方式会有影响但从直觉上理解这个性质完全够用。2.3 卷积层的三个关键超参数第一个是关键中的关键——卷积核大小。常见的是3x3和5x5现在的主流网络几乎都用3x3甚至1x1。3x3是最小的能捕捉上下左右和中心信息的尺寸参数少、堆叠灵活这是近十年的主流选择。第二个是步长stride也就是窗口每次滑动几个像素。步长为1输出尺寸基本不变考虑填充的情况下步长为2输出尺寸直接减半相当于一边卷积一边下采样。有些网络会用步长2的卷积代替池化来做下采样效果也不错。第三个是填充padding。卷积会缩小图片尺寸而且边缘像素参与的卷积次数比中间少信息被边缘化了。padding就是在图片四周补一圈零。补一层零配合3x3卷积和步长1可以让输出尺寸和输入完全一致这在搭建深层网络时很有用。来一个具体的计算例子。输入是一张32x32的灰度图用10个3x3的卷积核步长1不填充输出尺寸就是(32-3)/1130也就是30x30通道数变成10。公式很简单输出尺寸 (输入尺寸 - 卷积核尺寸 2 * 填充) / 步长 1这个公式请务必自己动手算两遍后面看网络结构图的时候能省很多时间。3. 池化与激活让网络瘦下来和活起来卷积层解决的是提取什么特征的问题。但光有卷积还不够还需要两个沉默的搭档池化层和激活函数。它们不显眼却是整个网络能稳定训练的支柱。3.1 池化为什么能保留信息又压缩计算池化层做的事情很简单在一个小区域里取一个代表值。最常见的是最大池化比如2x2的窗口步长2在每四个像素里取最大值图片尺寸直接减半。直观理解就是经过卷积之后某个位置激活值大代表这里检测到了对应的模式那么取最大值就是在小范围内保留最强烈的响应即使目标在局部移动了一两个像素最大值依然能捕获到它。这也是池化能带来轻微平移不变性的原因。手写数字5的横杠即便上下偏移了两个像素经过池化之后响应最强的位置变化并不大后继层依然能识别出同样的结构。池化还有个非常实际的作用急剧减少计算量。想象一个网络经过几次卷积后特征图已经是32通道、尺寸112x112了如果不做池化下一层卷积的计算量会非常大。2x2最大池化直接砍掉75%的像素后续层的参数规模和运算量都跟着降下来这在显存有限的年代几乎是必须的操作。最大池化和平均池化怎么选我的经验是网络前面几层用最大池化因为它保留有没有这个特征更合适。而在分类器附近如果想保留整体统计信息可以考虑全局平均池化把每张特征图压成一个数值效果稳、参数为零。全局平均池化在后来的经典网络里大量使用这个思路值得我们记住。3.2 激活函数没有它卷积就是线性变换的叠罗汉如果把卷积层一个个叠起来不加任何非线性那整个网络无论多深本质上都等价于一次线性变换。线性变换拟合不了非线性边界所以卷积之后必须接一个激活函数。ReLU修正线性单元是现在默认的选择公式简单x大于0则输出x否则输出0。它的好处一是计算快二是解决了深层网络里梯度消失的问题。早期用的Sigmoid函数曲线两端几乎平坦反向传播时梯度趋近于零网络很快就学不动了。ReLU在正半轴的梯度恒为1缓解了这个问题。但ReLU也有一个坑如果一个神经元的输入一直为负它的输出就永远是0梯度也永远是0这个神经元等于死掉了。解决的办法之一是改用Leaky ReLU负半轴给一个很小的斜率比如0.01让信息还能流通。初学者不必纠结选哪个从ReLU开始用遇到大批神经元死亡的问题再换Leaky ReLU就行。3.3 经典CNN架构里的组合套路我想用一个大家都听过的架构——它的核心思路至今还在被继承——来收束这一章。这个架构的基本单元是这样的先来一组卷积一般两到三层每个卷积后面跟一个ReLU然后接一个最大池化最后是全连接层加Softmax做分类。这个组合有什么道理卷积负责从局部提取特征ReLU负责引入非线性、过滤掉无用的激活池化负责压缩尺寸、聚合局部信息。经过这一轮处理特征图变得更少、更抽象、更能代表这里有什么结构。反复堆叠三四轮之后网络就从像素层面的边缘、纹理逐步上升到部件、形体层面的特征最后全连接层把所有特征综合起来打分。新手最容易犯的错误是只盯着最后一层如何分类看忽略中间这些卷积池化的组合。实际上图片分类能不能做好几乎完全取决于中间特征提取部分的质量。后面的全连接层只是一个文章末尾的归纳总结真正干活的是前面的卷积基础。4. 新手最容易困惑的五个问题这一章我集中解答我在带新人时反复被问到的五个问题。每一个都不算难但如果不搞清楚读任何网络结构都会一眼黑。4.1 通道数到底是怎么变的很多新手卡在通道数上。输入图片是RGB三通道经过卷积后变成64通道、128通道这到底是什么意思通道数可以理解为这一层提取了多少种不同的特征。3个通道的红绿蓝经过64个不同卷积核的扫描就得到64张特征图每张特征图代表一种特征在全图各位置的强度响应。一个卷积核产生一张特征图所以输出通道数 卷积核个数。而卷积核本身的深度必须等于输入通道数一个3x3x3的卷积核会同时在三个彩色通道上做运算把九个值加上另外两个通道的对应值一共27个值加权求和得到一个输出值。所以通道变化的规律是输入多少通道卷积核的深度就是多少有几个卷积核输出通道就是几。管他前面的通道数是3还是128这个规则永远不变。4.2 输出尺寸计算的完整走一遍光讲公式不动手等于白讲。我们来完整走一遍一个实际例子。假设输入是224x224的RGB图先经过一个64个3x3卷积核、填充1、步长1的卷积层然后接一个步长2的最大池化。第一步卷积输出尺寸 (224 - 3 2*1) / 1 1 224通道数64。也就是说在填充1的情况下3x3卷积不改变空间尺寸。第二步池化2x2窗口步长2输出尺寸 224 / 2 112通道数不变仍为64。于是这一轮过后数据从224x224x3变成了112x112x64。空间减半特征数变多这就是CNN每一层都在玩的游戏。多算几遍这个流程看网络结构图就不会晕了。4.3 参数数量算一笔账参数数量是新手严重忽略的一个指标。我见过有人搭网络参数到八千万还不觉得有什么问题。算一下就知道八千万意味着什么假设每张图片训练一次要正向和反向各算一遍八千万参数光存储就要320MB按单精度浮点数算训练速度会被拖到难以接受。参数数量的计算分两类。卷积层的参数 卷积核宽度 * 核高度 * 输入通道数 * 输出通道数 输出通道数偏置项。全连接层的参数 输入维度 * 输出维度 输出维度。举个例子一个Conv2d(3, 16, kernel_size3)参数就是33316 16 448。16个3通道的3x3核和16个偏置就这么点参数却能处理任意尺寸的图片这就是参数共享的威力。而一个Linear(4096, 1000)参数是40961000 1000约410万。全连接层才是参数大户这也是为什么后来的架构喜欢用卷积和池化把特征压得很小再接全连接或者干脆用全局平均池化换掉全连接。4.4 深度和宽度到底怎么选宽度指每层的通道数深度指总层数。没有绝对正确的答案但有几条经验值得分享。通道数通常按2的倍数递增16、32、64、128这种。因为很多硬件和框架在处理这些尺寸时有优化也符合网络逐层抽象的需求——前面提取边缘特征少后面要组合复杂结构特征得多。一次不要加太猛从64翻倍到128是稳妥的从32直接跳到256容易过拟合。深度方面针对小数据集四到六层卷积已经完全够用。盲目加深会遇到两个问题一是梯度传播困难网络末端几乎学不到东西二是过拟合加剧。新手在这种阶段宁可把精力放在数据增强和调参上也不要把层数无限堆高。一个很有用的经验是先用小网络快速验证整个流程能跑通再逐步加宽度和深度。直接一上来就搭一个几十层的网络训练一晚上最后发现是环境配置问题那是非常崩溃的。4.5 手里的数据不够怎么办CNN是个贪吃的模型数据量太少会过拟合得很厉害。我见过只有两三百张图就想做五分类的无论网络多简单训练集准确率都能到99%测试集却只有六成。解决思路有几条。第一是数据增强随机水平翻转、小角度旋转、随机裁剪、亮度扰动。把这些加进训练流程相当于把数据量扩大了一个量级。注意增强只应用在训练集验证和测试用原图。第二是用预训练模型做迁移学习借别人在超大数据集上训练好的卷积部分来提特征只训练后面的分类层这是小数据场景下最实用的招。第三是换成更小的模型参数量越少过拟合的风险越低有时一个两层卷积的小网络比一个八层大网络在小数据上表现更好。这五条问题背后其实共用同一个思维CNN不是黑盒子它的每个设计决策都在权衡表达能力和计算开销模型容量和数据规模这两组矛盾。想通了这两组矛盾CNN对你来说就不再是魔法了。5. 亲手搭建并跑通一个最简单的CNN理论聊得再多不亲手跑一个模型总觉得隔着一层窗户纸。这一章我带你完整搭一个能识别手写数字的CNN。整个流程我在本地实测过多次照着走大概十几分钟就能看到结果。5.1 环境与数据准备我用的框架是PyTorch理由很简单对初学者最友好、调试最直观。环境准备三步安装Python、安装PyTorch、确认CUDA可用没有独显的话CPU也能跑这个例子只是慢一点。数据集用现成的手写数字集28x28灰度图共十类。在PyTorch里数据集是内置的动动手指就能下载加载。数据预处理只需要做两件事把像素值从0到255缩放到0到1之间以及做数据增强——新手阶段可以先不做复杂增强等后面遇到瓶颈再加。5.2 模型定义下面是我常用的一个极简CNN一共四层卷积加两个全连接参数才四万多CPU上训练一个epoch也只要几十秒import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 28x28 - 14x14 nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 14x14 - 7x7 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(32 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, 10), ) def forward(self, x): x self.features(x) x self.classifier(x) return x model SimpleCNN()注意看通道变化1 - 16 - 32空间尺寸28 - 14 - 7。到全连接之前数据被压成了32x7x7 1568维的向量。这1568个数就是网络从原图里提炼出的精华描述最后两层全连接在这个描述之上做分类。5.3 训练与验证训练部分的核心代码不长但有几个细节很值得说。我习惯把训练和验证分成两个函数训练时开model.train()让dropout和批归一化生效验证时用model.eval()并配合torch.no_grad()关掉梯度记录既省内存又确保推理稳定。import torch.optim as optim from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss, correct, total 0, 0, 0 for images, labels in loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total images.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion): model.eval() total_loss, correct, total 0, 0, 0 with torch.no_grad(): for images, labels in loader: outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total images.size(0) return total_loss / total, correct / total优化器我用Adam而不是SGD。初学者先用Adam最省心它对学习率没那么敏感等以后对调参有感觉了再换SGD配合学习率衰减策略去冲更高精度。损失函数用交叉熵它在多分类问题上就是标准答案内部会先做Softmax再把输出和标签比出损失值所以你不需要在模型最后一层额外加Softmax。5.4 观察训练结果并做小实验跑十个epoch左右这个模型的验证准确率基本能达到98%以上。但你千万不要只看最终的准确率数字要养成看训练曲线和验证曲线的习惯。如果训练损失在降、验证损失在升说明过拟合了这是小数据集上最常见的现象应对方法是加数据增强、加dropout或减小模型。我强烈建议你做三个小实验每个都能加深理解。第一个把padding1全部去掉让卷积不再保持尺寸观察每层输出的尺寸变化和精度变化。第二个把最大池化层删掉一层改成步长为2的卷积对比参数数量和训练速度。第三个把16和32这两个通道数改成64和128看精度能涨多少再算算参数量涨了多少倍。做完这三个实验你对CNN内部机制的体感会比读十篇文章都深刻。6. 学CNN最容易被忽视的三件事走到这一步你已经不是完全的初学者了。最后我想分享三件我在学习过程中走了弯路才想明白的事它们不是具体的网络结构知识而是决定你后续能走多远的学习策略。第一件一定要亲手算一遍每一层的输入输出尺寸和参数数量。做到什么程度算合格看到网络结构图能在一分钟内说出任意相邻两层的输入通道、输出通道、参数数量和特征图尺寸。这个基本功决定了你能不能读懂论文里的网络架构。我在帮别人debug时有相当大比例的问题都是尺寸对不上导致的如果能随手算出尺寸至少一半的运行时报错能自己解决。第二件不要沉迷于调参先学会看现象。新手最喜欢问学习率设多少batch size选多大。这些问题确实存在但早期更重要的是建立模型的准确率-损失曲线-参数变化之间的因果直觉。看到验证集涨不上去先判断是欠拟合还是过拟合再决定是加容量、加数据还是加正则。调参是在这个判断框架下进行的没有框架只靠枚举参数就是瞎猫碰死耗子。第三件尽早动手迁移学习。很多人学完CNN之后有个误区觉得自己必须从零训练所有模型但其实主流实践中绝大多数场景都在用预训练模型做微调。原因为什么一个在千万级图片上训练过的卷积基座已经掌握了通用的视觉特征。你花一个下午把自己的分类头换上去在几千张小数据集上往往就能取得相当不错的成绩。这是性价比最高的实践方式也是连接学术理解和工程落地之间的一座桥。今天这篇关于CNN的认知分享就写到这里。我个人带新人的体会是CNN入门最大的障碍不是数学而是既不知道每一步在做什么也不知道做完了怎么看结果。把卷积、池化、参数共享这几个概念想透亲手跑通一个小模型再自己动手改几个结构做对比实验这条学习路径走完你基本就能脱离初学者这个身份了。