[深度学习网络从入门到入土] lenet
个人导航
知乎:https://www.zhihu.com/people/byzh_rc
CSDN:https://blog.csdn.net/qq_54636039
注:本文仅对所述内容做了框架性引导,具体细节可查询其余相关资料or源码
参考文章:各方资料
文章目录
- [深度学习网络从入门到入土] lenet
- 个人导航
- 参考资料
- 背景
- 架构(公式)
- 1.输入层
- 2.卷积层(Convolution)
- 3.下采样层(Pooling)
- 4.全连接层(Fully Connected)
- 5.激活函数
- 创新点
- 1. LeNet 第一次完整定义了 CNN 的结构模板
- 2. 从“人工特征”到“特征学习”
- 代码实现
参考资料
Gradient-based learning applied to document recognition
背景
在 LeNet 出现之前,图像任务基本是“手工特征 + 传统分类器”:
- 边缘检测(Edge Detection)
- 人工特征(HOG / SIFT)
- 降维PCA / 投影LDA
- 最后接 SVM / KNN
这些方法有一个致命问题:特征是人为设计的,模型本身不会“学特征”
LeNet:第一次系统性地证明了:神经网络可以端到端地从原始图像中学习特征并完成分类
- 手写数字识别(MNIST)
- 支票 / 邮政编码识别
架构(公式)
1.输入层
输入通常为灰度图像(channel=1):
X ∈ R 1 × 32 × 32 X \in \mathbb{R}^{1 \times 32 \times 32}X∈R1×32×32
- MNIST 原始是28 × 28 28\times2828×28-> LeNet 里通常padding 到32 × 32 32\times3232×32
2.卷积层(Convolution)
卷积的本质不是“滑窗”,而是局部连接 + 权值共享。
对单通道输入,卷积可写为:
Y k ( i , j ) = ∑ c ∑ u , v W k , c ( u , v ) X c ( i + u , j + v ) + b k Y_{k}(i,j) = \sum_{c}\sum_{u,v} W_{k,c}(u,v)\,X_c(i+u,j+v) + b_kYk(i,j)=c∑u,v∑Wk,c(u,v)Xc(i+u,j+v)+bk
LeNet 中的特点:
- 小卷积核(5 × 5 5\times55×5)
- 通道数逐步增加
- 没有 padding(尺寸会缩小)
3.下采样层(Pooling)
LeNet 使用的是平均池化(Average Pooling):
Y ( i , j ) = 1 ∣ R ∣ ∑ ( u , v ) ∈ R X ( u , v ) Y(i,j) = \frac{1}{|R|}\sum_{(u,v)\in R} X(u,v)Y(i,j)=∣R∣1(u,v)∈R∑X(u,v)
这里和现代 CNN 有明显区别:
- 没有 MaxPool
- 平均池化 + 可学习参数(早期版本)
作用只有一个:降维 + 平移不变性
4.全连接层(Fully Connected)
经过两次 Conv + Pool 后,特征图被拉平成向量:
z = v e c ( X ) \mathbf{z} = \mathrm{vec}(X)z=vec(X)
再经过多层线性映射:
h = σ ( W z + b ) \mathbf{h} = \sigma(W\mathbf{z} + b)h=σ(Wz+b)
最终输出类别概率。
5.激活函数
LeNet 使用的是tanh / sigmoid:
σ ( x ) = tanh ( x ) \sigma(x) = \tanh(x)σ(x)=tanh(x)
这是时代局限:
- ReLU 当时还没流行
- 梯度消失问题在那时并未被系统性认识
创新点
1. LeNet 第一次完整定义了 CNN 的结构模板
- 局部感受野
- 权值共享
- 多层特征抽象
- 端到端训练
2. 从“人工特征”到“特征学习”
LeNet 的核心思想不是网络多深,而是特征不再由人设计,而是由数据驱动学习得到
这是现代深度学习的思想源头
代码实现
importtorchfrombyzh.ai.BtrainerimportB_Trainerfrombyzh.ai.BdataimportB_Download_MNIST,b_get_dataloader_from_tensorfrombyzh.ai.Bmodel.study_cnnimportB_LeNet5_Paperfrombyzh.ai.Butilsimportb_get_device##### data #####downloader=B_Download_MNIST(save_dir='D:/study_cnn/datasets/MNIST')data_dict=downloader.get_data()X_train=data_dict['X_train_standard']y_train=data_dict['y_train']X_test=data_dict['X_test_standard']y_test=data_dict['y_test']num_classes=data_dict['num_classes']train_dataloader,val_dataloader=b_get_dataloader_from_tensor(X_train,y_train,X_test,y_test)##### model #####model=B_LeNet5_Paper(num_classes=num_classes)##### else #####optimizer=torch.optim.Adam(model.parameters(),lr=1e-3)criterion=torch.nn.CrossEntropyLoss()device=b_get_device(use_idle_gpu=True)epochs=10##### trainer #####trainer=B_Trainer(model=model,optimizer=optimizer,criterion=criterion,train_loader=train_dataloader,val_loader=val_dataloader,device=device)if__name__=='__main__':trainer.train_eval_s(epochs=epochs)importtorchimporttorch.nnasnnimporttorch.nn.functionalasFclassScaledTanh(nn.Module):""" 原论文常用的缩放版 tanh f(x) = 1.7159 * tanh((2/3) * x) """def__init__(self,A=1.7159,S=2.0/3.0):super().__init__()self.A=A self.S=Sdefforward(self,x):returnself.A*torch.tanh(self.S*x)classSubsamplingLayer(nn.Module):""" 原论文的 S 层(subsampling layer) 不是纯 AvgPool,而是: y = a * avgpool(x) + b 其中 a,b 对每个通道(feature map)可学习 输入: (N, C, H, W) 输出: (N, C, H/2, W/2) (当 kernel=2, stride=2) """def__init__(self,channels,kernel_size=2,stride=2):super().__init__()# 平均池化:负责下采样self.pool=nn.AvgPool2d(kernel_size=kernel_size,stride=stride)# 每个通道一个可学习的缩放系数 a 和偏置 b# 形状是 (C,) ,forward 时会 reshape 成 (1,C,1,1) 以便广播self.a=nn.Parameter(torch.ones(channels))self.b=nn.Parameter(torch.zeros(channels))defforward(self,x):# 先做下采样x=self.pool(x)# (N,C,H/2,W/2)# 做通道级的仿射变换:a * x + ba=self.a.view(1,-1,1,1)b=self.b.view(1,-1,1,1)x=a*x+breturnxclassB_LeNet5_Paper(nn.Module):""" 输入: (N, 1, 32, 32) 注意: - 这里把 S2/S4 改成论文里的 subsampling(avgpool + 可学习 a,b) - 激活用论文常用的 scaled tanh - C3 的“部分连接表”(partial connectivity) 这里仍使用现代全连接卷积(更常见的复现做法) 如果你要严格复刻 C3 的连接表,我也可以再给一版 工作流(形状): (N,1,32,32) -> conv5x5 -> (N,6,28,28) [可学习] -> tanh -> paper-sub -> (N,6,14,14) [可学习 a,b] -> conv5x5 -> (N,16,10,10) [可学习] -> tanh -> paper-sub -> (N,16,5,5) [可学习 a,b] -> conv5x5 -> (N,120,1,1) [可学习] -> tanh -> flatten -> (N,120) -> linear -> (N,84) [可学习] -> tanh -> linear -> (N,10) [可学习] """def__init__(self,num_classes=10):super().__init__()# 论文风格激活self.act=ScaledTanh()# C1: 1 -> 6self.conv1=nn.Conv2d(1,6,kernel_size=5,stride=1,padding=0)# S2: 6 通道的论文风格下采样(avgpool + 可学习 a,b)# 是否在 S 层后再接激活:这里先不接(更保守、也更常见)self.pool2=SubsamplingLayer(channels=6,kernel_size=2,stride=2)# C3: 6 -> 16self.conv3=nn.Conv2d(6,16,kernel_size=5,stride=1,padding=0)# S4: 16 通道的论文风格下采样self.pool4=SubsamplingLayer(channels=16,kernel_size=2,stride=2)# C5: 16 -> 120,输入正好是 5x5,所以输出 1x1self.conv5=nn.Conv2d(16,120,kernel_size=5,stride=1,padding=0)# F6self.fc6=nn.Linear(120,84)# F7self.fc7=nn.Linear(84,num_classes)defforward(self,x):# 兼容 MNIST 原始 28x28:先 pad 到 32x32ifx.shape[-2:]==(28,28):x=F.pad(x,(2,2,2,2))# left,right,top,bottom# conv + actx=self.conv1(x)# (N,6,28,28)x=self.act(x)# poolx=self.pool2(x)# (N,6,14,14)# conv + actx=self.conv3(x)# (N,16,10,10)x=self.act(x)# poolx=self.pool4(x)# (N,16,5,5)# conv + actx=self.conv5(x)# (N,120,1,1)x=self.act(x)# flattenx=x.view(x.size(0),-1)# (N,120)# linear + actx=self.fc6(x)# (N,84)x=self.act(x)# linearlogits=self.fc7(x)# (N,10)returnlogitsif__name__=='__main__':net=B_LeNet5_Paper(num_classes=2)a=torch.randn(50,1,28,28)result=net(a)print(result.shape)