1. 从LeNet开始理解卷积神经网络
1.1 为什么LeNet是绕不开的起点
很多接触机器智能的人,学到卷积神经网络(CNN)这一块时,第一个认真研究的框架就是LeNet。原因很简单:它是最早被证明有效的卷积神经网络结构之一,而且结构足够简洁,非常适合用来建立“卷积到底在做什么”的直觉。
LeNet诞生于上世纪90年代,最初就是为手写数字识别设计的。当年没有GPU集群,没有海量数据集,也没有复杂的训练技巧,靠的就是一个设计清晰的前向传播结构:卷积层提取特征,池化层压缩尺寸,全连接层做分类。这套流程后来几乎成了所有CNN的基本范式,哪怕是今天的ResNet、Transformer里的卷积变体,底层逻辑也没跳出这个框架。
我当时带一个模拟项目X时,团队里有人直接上手现代框架里的预训练模型,跑通了但说不清里面的卷积层、池化层各自干了什么。后来我让他回头把LeNet从零写一遍,再对照每个张量的尺寸变化,他很快就把CNN的核心概念理清了。这就是LeNet作为教学工具的最大价值:它不复杂,但足够完整。
这篇博文就把卷积神经网络概念和LeNet框架放在一起讲,覆盖面试高频问题、逐层参数计算、训练踩坑实录三个部分。适合正准备系统学习CNN的初学者,也适合面试前想快速把关键点过一遍的开发者。
1.2 卷积神经网络到底解决了什么问题
在进入LeNet结构之前,得先搞清楚CNN解决的核心问题是什么。
传统的全连接网络在图像任务上有个致命问题:一张256x256的彩色图片,展平后就是19万多个输入神经元,如果第一层隐藏层有1000个神经元,那光这一层的权重就接近2亿个参数。这种规模在算力有限的条件下基本不可训练,而且全连接方式会把图像的二维空间结构完全打散,相邻像素的关联信息就丢了。
CNN的思路则是反过来的。它用“局部连接”和“参数共享”两个策略来压缩参数量。局部连接的意思是,每个神经元只和输入图像的一个小区域相连,既保留了空间上的邻近关系,又把连接数大幅减少。参数共享的意思是,同一个卷积核会在整张图上滑动,用同一组参数检测同一种特征,比如某个卷积核专门检测横边,另一个专门检测竖边。这样一来,不管图像多大,一个卷积核需要的参数量只跟卷积核本身的大小有关,和输入尺寸无关。
这个设计的妙处在于,它把“识别图像”这件事拆成了“逐层提取特征”:浅层卷积核检测边缘、纹理,深层卷积核把边缘组合成部件、物体。LeNet-5正是这个思想最早的完整实现之一。
2. LeNet-5网络结构逐层拆解
2.1 整体架构与核心组件
LeNet-5的网络结构其实只有7层:1个输入层,2个卷积层,2个池化层,2个全连接层(最后接一个输出层)。原始论文里用的是32x32的灰度图输入,所以通道数只有1。现在的深度学习框架里复现LeNet时,很多人喜欢改造成适配数据集的样子,但原始设计的这些核心组件和尺寸计算思路是固定的。
结构顺序是这样的:
- 输入层:32x32x1的灰度图像。
- C1卷积层:6个5x5卷积核,输出6张28x28特征图。
- S2池化层:2x2区域平均池化,输出6张14x14特征图。
- C3卷积层:16个5x5卷积核,输出16张10x10特征图。
- S4池化层:2x2平均池化,输出16张5x5特征图。
- C5卷积层:120个5x5卷积核,输出120张1x1特征图(实际上等价于全连接)。
- F6全连接层:84个神经元,接激活函数。
- 输出层:10个神经元(对应0-9数字分类),用欧氏径向基函数或Softmax。
这里有个容易忽略的设计细节:C3层用了16个卷积核,但并不是每个卷积核都和前面全部6张特征图做卷积,而是用了部分连接的方式,一部分卷积核和4张图相连,一部分和6张图相连。原作者Yann LeCun当年这么设计是为了控制模型复杂度和打破对称性,让不同卷积核学到互补的特征。现在复现时为了方便,很多人直接改成全连接,效果差别不大。
2.2 特征图尺寸计算与参数数量推导
卷积神经网络的尺寸变化规律,是面试里最常被问到的基础题。公式其实就一行:
输出尺寸 = (输入尺寸 - 卷积核尺寸 + 2 * 填充) / 步长 + 1
LeNet-5原始设计里没有填充(padding=0),步长为1,所以C1层从32x32变成28x28,就是(32-5)/1+1=28。S2池化层的2x2区域、步长为2,裁一半变成14x14。整个过程每经过一次池化,特征图尺寸减半,这符合经典CNN“空间分辨率逐渐降低,通道数逐渐增加”的趋势。
参数数量也很好算。C1层:6个5x5卷积核,每个卷积核有5x5+1(偏置)=26个参数,总共6x26=156个参数。C3层:16个5x5卷积核,如果按部分连接设计,参数数量大概是8x(3x25+1)+8x(4x25+1)+6x(6x25+1)=1516个左右。C5层是120个5x5卷积核作用于16张5x5特征图,参数是120x(16x25+1)=48120个。F6层是84x(120+1)=10164个参数。总参数量加起来约6万个字节,所以LeNet-5论文里有个著名的标题叫“Gradient-Based Learning Applied to Document Recognition”,里面专门提到这个模型只有6万个可学习参数。
对比一下前面说的全连接网络动辄上亿参数,6万参数就能完成手写数字识别,这就是卷积结构压缩参数的威力。理解了这个计算过程,你以后看到任何CNN网络结构,都能自己推算出参数量和计算量,不用死记。
2.3 激活函数与输出层的选择
LeNet-5当年用的激活函数是双曲正切函数tanh,不是现在主流的ReLU。tanh输出范围是(-1, 1),关于零点对称,梯度不容易发生单侧饱和,在浅层网络里表现稳定。
输出层用了欧氏径向基函数(RBF),每个类别对应一个预设的向量原型,计算输入向量和原型的欧氏距离,距离越小得分越高。这种设计现在看来有点老派,但它本质上是让模型学习“输入应该靠近哪个类别的中心”,和现在用Softmax加交叉熵的做法在思路上有相通之处。
如果你今天自己复现LeNet,我建议直接换成ReLU和Softmax。ReLU计算简单、能缓解梯度消失,Softmax输出概率便于解释。LeNet框架本身的教学重点在于卷积和池化的结构,激活函数和输出层的选择属于与时俱进的部分,不必拘泥于原版。
3. 卷积神经网络核心概念问答
3.1 卷积层里的“卷积”到底是什么
这是面试里最容易卡壳的问题。很多人能用框架调用Conv2D,但说不清卷积运算的数学过程。
卷积操作说白了就是:一个小的权重矩阵(卷积核),在输入图像上按步长滑动,每滑到一个位置,就把卷积核里的权重和对应位置的像素值相乘,然后求和,再加上偏置,得到特征图上的一个像素值。这个过程可以用一个生活化的类比来理解:你拿一个印章在纸上不断盖印,印章上的花纹就是卷积核,盖出来的图案就是特征图。不同的花纹印章能印出不同的纹路,对应不同的特征检测器。
举一个具体的计算例子:假设输入是5x5的图像,卷积核是3x3:
输入矩阵(简化):
1 2 1 0 0 0 1 2 1 0 1 0 1 2 1 0 1 0 1 2 1 0 1 0 1
卷积核:
1 0 1 0 1 0 1 0 1
步长为1,无填充。卷积核覆盖左上角3x3区域: 1 2 1 0 1 2 1 0 1
对应相乘再求和:1x1 + 2x0 + 1x1 + 0x0 + 1x1 + 2x0 + 1x1 + 0x0 + 1x1 = 5。这个5就是输出特征图第一个像素的值。然后卷积核向右滑动1格,继续算下一个位置。整个过程就是在做“模板匹配”:如果图像某个局部区域的模式和卷积核相似,那这个位置的输出值就会比较大,说明检测到了对应的特征。
注意,卷积核的值不是人为设计的,而是通过训练自动学习出来的。初始随机赋值,经过反向传播不断调整,最终在数据中“发现”有用的特征模板。这一点是CNN最核心的思想之一。
3.2 池化层的作用和常见方式
池化层在LeNet里紧跟在卷积层后面,作用可以概括成三个词:降维、提特征、防过拟合。
降维很好理解,2x2池化把特征图尺寸减半,计算量也随之减少。提特征的含义在于,池化操作是对局部区域做聚合统计,相当于选出这个区域里最有代表性的响应。防过拟合则是因为参数量减少后,模型的复杂度降低了。
常见的池化方式有两种:最大池化(Max Pooling)和平均池化(Average Pooling)。最大池化取区域内的最大值,突出最强响应,对纹理、边缘等特征更敏感;平均池化取区域内平均值,保留整体背景信息,让特征更平滑。
LeNet用的是平均池化,而且带可学习的缩放系数和偏置。现代CNN里最大池化用得更多,因为实验经验表明它更有利于保留显著特征。实际面试如果被问到两种池化的区别,你可以从“最大池化提取显著特征,抗噪声能力强;平均池化保留更多背景信息,适合整体描述”这个角度展开,然后补充一句“图像分类任务里最大池化更常用”。
另外还有一个概念要分清:池化和卷积都能降低特征图尺寸,但池化没有可学习参数,卷积有。池化是固定的聚合计算,卷积核是学出来的。
3.3 感受野与参数共享的核心概念
感受野(Receptive Field)指的是特征图上的一个像素,对应回输入图像上的多大区域。这个概念是理解CNN层级特征的关键。
LeNet-5里,C1层的每个神经元对应输入图像上的5x5区域,所以感受野是5x5。经过S2池化后,C3层的一个神经元对应到C1层特征图的5x5区域,而这5x5区域又对应输入图像的更大范围,因为中间隔了一次池化,所以感受野变成了14x14左右。
随着网络加深,感受野越来越大,这就是CNN能“从局部到全局”提取特征的原因:浅层看到的是细节,深层看到的是整体。你不需要特意设计更大的卷积核来“看更远”,堆叠小卷积核加池化,就能指数级扩大感受野。这也是为什么现代网络倾向于使用大量3x3小卷积核,既能保持感受野,又能减少参数量。
参数共享在前面提过:同一个卷积核在整个特征图上滑动,所有位置共用同一组权重。这带来的好处不只是减少参数量,还让网络具备了平移等变性——图像里的目标平移一段距离,特征图的响应也跟着平移,但响应值不变。这个性质对于图像分类任务意义重大,因为目标出现在什么位置,原则上不应该影响分类结果。
4. LeNet框架在机器学习训练中的实战注意点
4.1 数据预处理与模型输入设计的坑
LeNet原始设计是32x32输入,但现在很多数据集里的图片尺寸不是这样。有人直接用28x28的数据集喂给LeNet,结果第一个卷积层输出的尺寸变成了24x24,虽然也能跑,但如果你没有调整后续结构,到C5层时可能会遇到尺寸对不上的问题。
正确的做法是统一设计输入尺寸。以MINIST为例,原始图片是28x28,如果你想完全按LeNet-5的原始尺寸来跑,就要先做一步预处理,把图片填充或缩放到32x32。缩放还是填充有讲究:直接拉伸会破坏数字的宽高比,填充则保持了原始形状,我建议用填充,填充值设为0或均值。
数据归一化也是一个容易被忽略的细节。输入图像像素值范围是0到255,如果直接喂给网络,初始的梯度可能会很大,导致训练不稳定。标准做法是归一化到0到1之间,或者进一步标准化到均值为0、方差为1。很多教程不会强调这一步,但实际训练中,归一化对收敛速度的影响非常显著。
我当时做一个模拟图像分类项目时,刚开始没做归一化,训练loss一直在高位震荡,调了一个下午的学习率都没用。后来加上归一化,loss立刻开始稳定下降。这个经验教训写在这里,就是希望大家不要在数据预处理上省功夫。
4.2 学习率、批大小与优化器选择
LeNet因为结构简单,对超参数不算敏感,但有几个基本规律还是要讲清楚。
学习率是最重要的超参数。对于LeNet这类小型网络,0.01到0.001是比较合理的区间。学习率太大,loss会震荡甚至发散;学习率太小,收敛慢且容易陷入局部最优。最实用的做法是先用0.01跑20个epoch观察loss曲线,如果loss震荡剧烈就降到0.001,如果下降太慢就升到0.01,没有想象中复杂。
批大小(batch size)影响的是梯度估计的稳定性。小的批大小(如32、64)引入更多的梯度噪声,反而有正则化效果,LeNet这种小网络不需要特别大的批次。我习惯用32或64,训练速度快且稳定。
优化器方面,原始的LeNet用的是带动量的随机梯度下降(SGD),现在复现时用Adam的人也很多。SGD加上动量收敛更慢但泛化性往往更好,Adam收敛快但有时会停在次优解。对于学习用的小项目,直接Adam省心;如果是要刷测试集精度,SGD加动量的上限通常更高。
4.3 过拟合判断与模型保存的最佳实践
LeNet参数量本身不大,但在小数据集上仍然可能过拟合。判断方法很简单:训练集loss持续下降,验证集loss降到某个点后开始回升,同时验证集准确率不再提升甚至下滑,那基本就是过拟合了。
应对过拟合,优先考虑三个手段:增加数据增强(对图像做随机平移、旋转、缩放)、增加Dropout层(LeNet的F6层后面可以插一个概率0.5的Dropout)、减小网络规模或增加正则化强度。注意,Dropout只应该在训练时启用,推理时要关闭,现代深度学习框架会自动处理这一点,但如果你手写实现,要小心别漏掉。
模型保存这块,不要只保存最终一轮的参数,而是要在训练过程中实时监控验证集准确率,达到历史最优时就保存一份。这样即使后续训练震荡或过拟合,你手里还有一个最优模型的备份。很多框架里都有ModelCheckpoint回调,设置好监控指标和保存路径就能自动生效。
另一个常见错误是忘记区分训练模式和推理模式。如果模型里有BatchNorm或Dropout层,训练和推理的计算方式是截然不同的。LeNet原始结构里没有这两个层,但如果你为了防过拟合加了Dropout,那在预测之前一定要记得切回推理模式,否则预测结果会很奇怪。
4.4 验证集选中一个容易忽视的细节
验证集和测试集不能混用。有人在小数据量项目里,偷懒用测试集来调参,最后报出来的“精度”虚高,因为模型已经在测试集上做过隐式优化了。正确做法是:训练时只用训练集更新权重,验证集用来选模型和调超参数,测试集只在最终评估时用一次。
这个道理大家其实都懂,但实操中很容易因为懒或者数据集太小而犯浑。我在带一个入门项目时,有个同学把测试集当验证集用,调了一整周参数,最后模型在测试集上表现很好,换到真实场景的数据就崩得一塌糊涂。这种就是典型的数据泄露问题。
另外,数据划分时要注意类别分布。如果数据集的类别比例不均衡,直接随机划分可能导致验证集和训练集里某些类别的比例差异很大,这样验证结果就有误导性。建议用分层抽样(stratified split),保证训练、验证、测试三个集合的类别比例和整体一致。
5. 高频面试题与易混淆概念排查
5.1 面试官最爱问的六个CNN概念题
结合我自己被问过的和帮别人模拟面试的经历,LeNet和卷积神经网络这块出现频率最高的题目集中在以下几个:
关于LeNet-5具体结构的问题,包括各层参数数量、特征图尺寸变化,这些是基础。第二个常问的是为什么用卷积核不用全连接,要能讲出局部连接和参数共享两个核心思想。第三个是池化层去掉行不行,答案是“行,但模型会更慢更容易过拟合”,池化是效率手段而不完全是精度手段。第四个是1x1卷积核有什么用,这在LeNet里没有,但面试可能会延伸到,它的作用是改变通道数、增加非线性,不改变空间尺寸。第五个是卷积层里的偏置项有什么意义,答案是提供平移能力,没有偏置的话卷积输出会被限制在过原点附近。第六个是梯度消失和ReLU的关系,LeNet用tanh如果层数更深就容易梯度消失,换成ReLU解决这个问题。
回答这类题目的时候,核心不是背定义,而是能在说概念的时候带出“为什么这样做”的推导逻辑。面试官更看重你对每个设计模块的理解是否成体系,而不是背了多少个网络结构。
5.2 三种容易混淆的池化话题
围绕池化层,有几个概念经常被混在一起谈,这里梳理一下。
第一个是最大池化和平均池化的区别。这个前面已经展开说了,不再赘述。
第二个是全局平均池化和普通池化的区别。全局平均池化是指对整个特征图求平均,直接得到一个值,常用于分类网络的最后一层,替代全连接层。它的好处是把特征图直接映射成类别得分,没有额外参数,还天然减少了过拟合。LeNet里没有用全局平均池化,但这个是CNN发展过程中的一个重要演进思路。
第三个是池化和卷积步长的关系。池化核是2x2、步长2时,相当于每4个像素里取一个代表值,这和直接用步长2的卷积在降低分辨率方面的效果类似,但池化是固定的操作,不学习参数,计算开销更小。
只要能把这三个话题分开讲清楚,面试中池化相关的题目基本就稳了。
5.3 面对“你如何从零设计一个CNN”这类问题的思路
LeNet作为入门框架后,面试中常问的还有一个开放性问题:如果让你针对一个新任务设计一个简单CNN,你会怎么思考。
这里提供一个自洽的思路:第一步确定输入尺寸和输出类别数,输入尺寸不要随意取,要考虑图像长宽比;第二步确定层数,小数据集两三组卷积池化就够了,深了容易过拟合;第三步确定卷积核数量和尺寸,第一层卷积核数量少一点(16或32),逐层翻倍,卷积核尺寸用3x3或5x5;第四步加入全连接层和Dropout;第五步配置损失函数和优化器,分类用交叉熵,回归用均方误差。
这个思路本质上就是从LeNet的结构出发,按照数据规模和任务难度做缩放。你不需要设计一个全新的网络,能基于LeNet的框架做合理调整,就已经具备很好的CNN设计基础了。
6. 一次完整的LeNet训练跑通过程
6.1 用框架搭建LeNet的完整流程
下面给出一份基于常见深度学习框架的LeNet搭建代码。模型结构按照原始LeNet-5,输入尺寸设为32x32的单通道图。
import torch import torch.nn as nn class LeNet5(nn.Module): def __init__(self, num_classes=10): super(LeNet5, self).__init__() # C1: 6个5x5卷积核,输入1通道,输出6通道 self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0) # S2: 2x2平均池化 self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2) # C3: 16个5x5卷积核,输入6通道 self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0) # S4: 2x2平均池化 self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2) # C5: 120个5x5卷积核 self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0) # F6: 全连接层 self.fc1 = nn.Linear(120, 84) # 输出层 self.fc2 = nn.Linear(84, num_classes) def forward(self, x): x = self.pool1(torch.relu(self.conv1(x))) x = self.pool2(torch.relu(self.conv2(x))) x = torch.relu(self.conv3(x)) x = x.view(x.size(0), -1) x = torch.relu(self.fc1(x)) x = self.fc2(x) return x这一段代码里,有几个细节值得单独说明。
卷积层后面紧跟ReLU激活,池化层放在激活之后,这是现代实现和原版的一个小区别。原版用的是tanh,但功能等价,ReLU恢复更快,效果也好。
forward函数里,经过conv3之后张量形状是[batch, 120, 1, 1],所以view操作把它展平成[batch, 120]。如果你把卷积核数量和输入尺寸换了,这里的展平尺寸也要跟着换。
池化层用了AvgPool2d而不是MaxPool2d,这是为了尽量接近LeNet-5原始设计。如果你想要更好的实际效果,换成MaxPool2d也很方便。
6.2 训练循环中的关键配置和输出监控
模型定义好之后,训练循环本身没有太多神秘的地方,但有几个监控指标是必须盯着的。
import torch.optim as optim model = LeNet5(num_classes=10) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) def train_one_epoch(model, train_loader, criterion, optimizer): model.train() total_loss = 0 correct = 0 total = 0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() avg_loss = total_loss / len(train_loader) accuracy = 100.0 * correct / total return avg_loss, accuracy def evaluate(model, val_loader, criterion): model.eval() total_loss = 0 correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: outputs = model(images) loss = criterion(outputs, labels) total_loss += loss.item() _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() avg_loss = total_loss / len(val_loader) accuracy = 100.0 * correct / total return avg_loss, accuracy这里特别强调一个容易踩的坑:训练前必须调用model.train(),验证前必须调用model.eval()。如果你的模型结构里有Dropout或BatchNorm,忘了切模式会导致训练和验证结果都不正常。LeNet5原始结构里没有这两个层,跑起来影响不大,但这个习惯要养成,后面用更复杂的网络时就不会吃亏。
每轮训练结束后,打印训练集和验证集的loss、准确率。经验来看,训练集准确率稳步上升,验证集准确率略低于训练集是正常状态。如果验证集准确率开始明显下滑,马上停止训练,回退到验证集准确率最高的那个模型。
6.3 训练过程中遇到的典型问题记录
我在复现LeNet的过程中,遇到过几个值得记录的问题,这里整理成表格,方便大家排查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| Loss完全不下降 | 数据没归一化、学习率太大、标签有错误 | 检查数据预处理,降低学习率到0.0001 |
| Loss下降后又反弹 | 学习率大导致震荡 | 降低学习率或加学习率衰减 |
| 训练集准确率高但验证集低 | 过拟合 | 加Dropout、加数据增强、减小模型规模 |
| 验证集准确率忽高忽低 | 批大小太小或数据顺序有规律 | 增大batch size或shuffle数据 |
| 测试时输出全是同一个类 | 模型没有收敛或数据类别严重不均衡 | 检查训练过程,尝试调整损失函数权重 |
| 显存不足 | 批大小太大 | 减小batch size |
第一个问题的出现频率最高,很多人刚跑LeNet时loss卡在某个值不动,第一反应是改模型结构。其实多数情况下就是输入范围没处理好,做一次归一化就能解决。这个经验来自实际经历,推荐遇到问题先排数据处理,再看模型和优化器。
7. LeNet的局限与CNN的后续演进方向
7.1 为什么LeNet结构在今天不那么能打了
LeNet在手写数字识别上的效果是很出色的,放到现代数据集上就不太行了,原因集中在几个方面。
首先,LeNet的卷积核尺寸偏大(5x5),在现代设计理念看来,用两层3x3卷积替代一层5x5卷积,可以在相同感受野下引入更多非线性,而且参数量更少。其次,LeNet没有批归一化(BatchNorm)层,训练深网络时会遇到内部协变量偏移的问题,需要用更精细的学习率调整来弥补。再者,LeNet为了降低分辨率直接用了2x2池化,而现代网络更多用步长卷积替代池化,让网络自己学习要不要降采样。
这些局限是视角问题,不是设计缺陷。LeNet诞生年代的计算资源和数据规模决定了它的设计只能走向简洁,现在的模块(如残差连接、注意力机制)是在它的基础上逐步迭代出来的。
7.2 从LeNet到现代CNN的关键变化
如果你已经掌握了LeNet的细节,再去看后面出现的经典CNN,会觉得每一代演进方向都很清晰。
ResNet引入了残差连接,解决网络加深后的退化问题,允许训练上百层的网络。VGG用若干个3x3卷积核堆叠替代大卷积核,参数更少、非线性更强。GoogLeNet用Inception模块做多尺度特征提取。MobileNet用深度可分离卷积大幅压缩计算量。这些结构的共同点依然是“卷积提取特征、下采样减少尺寸、全连接或全局池化输出分类”,所以LeNet的思路始终没有过时。
面试中如果被问到“最近在关注什么模型”,顺着这条演进线讲下来,比你背一堆论文名效果要好。关键是理解每一代模型“改了什么、为什么改”,而不是记住结构图。
8. 给初学者的LeNet学习建议
8.1 学习路径规划
如果你是完全零基础,我建议的学习路径是这样的:先搞懂CNN的三个核心概念(卷积、池化、全连接),然后手推一遍LeNet-5每一层特征图的尺寸变化和参数量,再自己用框架写完LeNet并在小数据集上训出一个能用的模型,最后回头复盘每一层的作用并尝试改结构看效果差异。
整个过程大概需要一到两周的业余时间,重点是手推尺寸和亲自动手训模型。只看不练是学不会卷积神经网络的,因为很多细节只有在你亲自调试代码、看到loss曲线变化时才有体感。
8.2 验证理解的三个小实验
检验自己是不是真懂了LeNet和CNN概念,可以做三个小实验。
第一个实验:把C3层的卷积核数量从16改成32,看参数量和训练时间怎么变化。第二个实验:把所有池化层去掉,看模型收敛速度和精度有什么变化。第三个实验:把卷积核从5x5改成3x3,看对结果的影响。
这三个实验都能让你直观感受到每个组件对模型的影响,比背十篇原理文章都有用。我自己带项目和带人的时候,最重视的就是这种动手验证的习惯,它能帮助把书本上的概念变成自己的一部分。
最后还想补充一句:框架封装程度越来越高,很多概念都被隐藏了,但卷积神经网络的基础原理并不会过时。花点时间从LeNet开始把底子打好,后面对任何新模型的学习都会顺畅很多。