当前位置: 首页 > news >正文

【个人CNN学习记录之LeNet pytorch代码分析】

系列文章目录

个人CNN学习记录之LeNet pytorch代码分析


文章目录

  • 系列文章目录
  • 前言
  • 一、Lenet模型架构
    • 一、网络的历史地位与意义
    • 二、网络结构层次详解
  • 二、代码分析
        • model.py
        • train.py
        • predict.py
  • 总结

前言

在日常工作中,我专注于并行计算领域,主要依托GPGPU、NPU等高算力芯片进行开发。当前,高算力与AI已深度融合,计算与人工智能二者相辅相成:底层计算为实现通用算法与算子提供基础,而AI模型则能反哺并优化传统算法的决策效率与性能。为系统构建这方面的知识体系,我在公司导师的推荐下,跟随up主“霹雳吧啦Wz”的CNN系列视频进行学习,并通过博客记录学习过程,融入自己的理解与总结。


一、Lenet模型架构

一、网络的历史地位与意义

CNN雏形:由Yann LeCun等人于1998年提出,是第一个成功应用于手写数字识别的卷积神经网络,奠定了现代CNN的基础架构。
经典结构:其“卷积层 → 池化层 → 卷积层 → 池化层 → 全连接层”的交替模式,成为后续几乎所有深度CNN的通用设计范式。

二、网络结构层次详解

LeNet-5的结构清晰地展示了从原始图像到最终分类结果的数据变换过程:
过程需要注意每一层的输入输出的尺寸变化,尺寸公式以前课程有记录:
N=(W-F+2P)/S+1,W输入尺寸,F卷积核大小,步长S,padding像素数P

输入层

尺寸:32x32的灰度图像。

说明:输入图像被归一化并置于网络中心。这个尺寸大于实际数字(如MNIST数据集的28x28),为特征提取留出边界。

第一组特征提取:卷积层C1 + 池化层S2

卷积层C1:

使用6个​5x5的卷积核,无填充。

产生 6个​特征图,每个尺寸为 28x28((32-5)/1+1 = 28)。

功能:初步提取边缘、角点等低级视觉特征。

池化层(下采样层)S2:

采用 2x2 最大池化,步长为2。

产生 6个​ 特征图,每个尺寸为 14x14(28/2 = 14)。

功能:降低特征图的空间尺寸和计算量,同时增强特征的平移不变性。

第二组特征提取:卷积层C3 + 池化层S4

卷积层C3:

使用16个​ 5x5的卷积核。

产生 16个​ 特征图,每个尺寸为 10x10 ((14-5)/1+1 = 10)。

注意:此层并非简单的全连接卷积,而是设计了稀疏连接,以模拟生物视觉并减少参数。图中标注的16@5x5即表示16个5x5的特征图。

池化层S4:

再次使用 2x2 最大池化,步长为2。

产生 16个​ 特征图,每个尺寸为 5x5(10/2 = 5)。

分类器部分:全连接层
层C5:

是一个具有 120个​ 神经元(或120维特征向量)的全连接层。它将S4层输出的16个5x5特征图(共16 * 5 * 5=400个值)展平并连接。

层F6:

是一个具有 84个​ 神经元的全连接层。

输出层:

由 10个​ 神经元组成,分别对应数字0-9,通常使用Softmax函数输出每个类别的概率。

二、代码分析

model.py
import torch.nn as nn import torch.nn.functional as F class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 = nn.Conv2d(3, 16, 5) self.pool1 = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(16, 32, 5) self.pool2 = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(32*5*5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = F.relu(self.conv1(x)) # input(3, 32, 32) output(16, 28, 28) x = self.pool1(x) # output(16, 14, 14) x = F.relu(self.conv2(x)) # output(32, 10, 10) x = self.pool2(x) # output(32, 5, 5) x = x.view(-1, 32*5*5) # output(32*5*5) x = F.relu(self.fc1(x)) # output(120) x = F.relu(self.fc2(x)) # output(84) x = self.fc3(x) # output(10) return x

根据模型再去看代码确实很简单,基本就是按照网络结构,按照python提供的类进行模型创建。和前面不一样的是代码里第二个卷积层个数为32,所以后面的尺寸有所变化。
首先,导入必要的模块:
torch.nn:PyTorch的神经网络模块,包含各种层和函数。
torch.nn.functional:通常简写为F,包含各种激活函数、损失函数等。
定义LeNet类,继承自nn.Module。
definit(self)定义初始化,值得注意的是初始化中使用了super(LeNet, self).init(),super().init()​ 确保了LeNet正确继承了nn.Module的全部功能,在定义网络时通常都需要这个函数。

nn.Conv2d(3, 16, 5)定义了第一层卷积层,需要注意这个函数的参数:

根据图片可以看到Conv2d默认的参数顺序为in_channles(输入特征矩阵的深度,如第一层通常为RGB图像,有三个分量,深度为3),out_channels为卷积核的个数,kernel_size为卷积核的尺寸。想要更详细的参数说明可以参考官方手册 https://docs.pytorch.org/docs/stable/index.html

nn.MaxPool2d定义了池化层,同样需要注意池化层的参数,第一个参数为池化层尺寸,第二个参数为步长,如果不设置步长,默认和尺寸相等。

nn.Linear定义了全连接层,同样需要注意全连接层的参数,第一个参数为输入节点尺寸,第二个参数为输出节点尺寸。第一个全连接层的输入节点尺寸即为特征提取后的特征矩阵展平得来,后续的全连接层输入节点等于前一层的输出节点尺寸。最后一个全连接层的输出节点与分类类别种类个数相同。

def forward(self, x)定义前向传播过程,x代表输入的数据,注意pytorch的数据通道排序为[batch,channel,height,width]。

x = F.relu(self.conv1(x)) 表示将数据通过第一个卷积层后再经过一个relu激活函数
x = self.pool1(x) 表示数据经过第一个池化层
x = F.relu(self.conv2(x)) 表示数据经过第二个卷积层后再经过一个relu激活函数
x = self.pool2(x) 表示数据经过第二个池化层
x = x.view(-1, 3255) 表示将数据通过view函数做展平处理,变成一维向量
x = F.relu(self.fc1(x)) 表示将展平后的数据通过第一个全连接层,再经过一个relu激活函数
x = F.relu(self.fc2(x)) 表示数据经过第二个全连接层,再经过一个relu激活函数
x = self.fc3(x) 表示数据经过最后一个全连接层
这里代码没有通过softmax做归一化是因为训练那里计算会有体现。因此这里不需要。

train.py
def main(): transform = transforms.Compose( [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) # 50000张训练图片 # 第一次使用时要将download设置为True才会自动去下载数据集 train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) train_loader = torch.utils.data.DataLoader(train_set, batch_size=36, shuffle=True, num_workers=0) # 10000张验证图片 # 第一次使用时要将download设置为True才会自动去下载数据集 val_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) val_loader = torch.utils.data.DataLoader(val_set, batch_size=5000, shuffle=False, num_workers=0) val_data_iter = iter(val_loader) val_image, val_label = next(val_data_iter) # classes = ('plane', 'car', 'bird', 'cat', # 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') net = LeNet() loss_function = nn.CrossEntropyLoss() optimizer = optim.Adam(net.parameters(), lr=0.001) for epoch in range(5): # loop over the dataset multiple times running_loss = 0.0 for step, data in enumerate(train_loader, start=0): # get the inputs; data is a list of [inputs, labels] inputs, labels = data # zero the parameter gradients optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = loss_function(outputs, labels) loss.backward() optimizer.step() # print statistics running_loss += loss.item() if step % 500 == 499: # print every 500 mini-batches with torch.no_grad(): outputs = net(val_image) # [batch, 10] predict_y = torch.max(outputs, dim=1)[1] accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0) print('[%d, %5d] train_loss: %.3f test_accuracy: %.3f' % (epoch + 1, step + 1, running_loss / 500, accuracy)) running_loss = 0.0 print('Finished Training') save_path = './Lenet.pth' torch.save(net.state_dict(), save_path) if __name__ == '__main__': main()

代码里通过
train_set = torchvision.datasets.CIFAR10(root=‘./data’, train=True,download=True, transform=transform)来进行训练数据集的下载,root表示下载的目录,train=true表示下载的是训练数据集,download可以控制是否下载,transorm可以用来做数据预处理。代码里的transform首先通过transforms.ToTenso对输入图像做tensor转换,然后通过transforms.Normalize对数据做归一化处理。

torch.utils.data.DataLoader进行数据集的批次导入,第一个参数为上面输出的训练数据集,第二个参数为批次数,第三个参数shuffle表示数据是否打乱,一般都为true,第四个参数num_workers为线程数。

接下来同样的方法导入测试集,代码里导入了10000张图片用于测试准确率,不同的是测试集通过val_data_iter = iter(val_loader)创建了一个迭代器,然后通过next(): 从迭代器中获取下一批数据。由于val_loader的batch_size=5000,这里获取的是5000个样本。
返回的val_image: 形状为[5000, 3, 32, 32]的张量
返回的val_label: 形状为[5000]的张量

net = LeNet() 进行模型实例化
loss_function = nn.CrossEntropyLoss()定义损失函数为交叉熵损失函数
optimizer = optim.Adam(net.parameters(), lr=0.001)定义优化器为Adam优化器,lr为学习率。

接下来进入训练过程,代码最外层for循环定义了5次训练,每一次训练通过enumerate函数去遍历训练集,然后通过optimizer.zero_grad()清除历史梯度。
outputs = net(val_image) 经过网络模型得到输出,loss = loss_function(outputs, labels)计算损失,loss.backward()进行反向传播,optimizer.step()通过优化器进行参数的更新。 running_loss += loss.item()进行损失累计计算。

if step % 500 == 499:定义每500个batch验证一次,with torch.no_grad():可以禁用梯度计算,节省内存。outputs = net(val_image)通过将数据集送入网络得到输出,predict_y = torch.max(outputs, dim=1)[1] 获得预测的类别,accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)计算准确率。最后打印这些数据。

predict.py
import torch import torchvision.transforms as transforms from PIL import Image from model import LeNet def main(): transform = transforms.Compose( [transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') net = LeNet() net.load_state_dict(torch.load('Lenet.pth')) im = Image.open('1.jpg') im = transform(im) # [C, H, W] im = torch.unsqueeze(im, dim=0) # [N, C, H, W] with torch.no_grad(): outputs = net(im) predict = torch.max(outputs, dim=1)[1].numpy() print(classes[int(predict)]) if __name__ == '__main__': main()

验证脚本先通过transforms.Compose定义了数据预处理方式,与训练脚本不同的是增加了Resize((32, 32)):将输入图片调整为32×32。原因是LeNet网络要求输入为32×32,但测试图片可能是任意尺寸。

classes = (‘plane’, ‘car’, ‘bird’, ‘cat’,
‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’)
定义标签,CIFAR-10的10个类别名称,顺序必须与训练时一致
net = LeNet() 实例化模型结构
net.load_state_dict(torch.load(‘Lenet.pth’))加载训练好的权重
im = Image.open(‘1.jpg’) 打开图片文件

im = transform(im) 图片预处理
transform处理后的维度变化:
原始图片: PIL.Image对象
↓ transforms.Resize((32, 32))
调整大小: (3, 32, 32) 但仍为PIL.Image
↓ transforms.ToTensor()
转换为Tensor: torch.Tensor [3, 32, 32] 值范围[0,1]
↓ transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
标准化: torch.Tensor [3, 32, 32] 值范围[-1,1]

im = torch.unsqueeze(im, dim=0) 调整维度
原始维度:[3, 32, 32]
添加批次维度:[1, 3, 32, 32]
网络期望输入格式:[batch_size, channels, height, width]

with torch.no_grad(): 禁用梯度计算
outputs = net(im) 输入网络得到输出
predict = torch.max(outputs, dim=1)[1].numpy() 获取预测类别


总结

以上就是今天要讲的内容

http://www.jsqmd.com/news/657725/

相关文章:

  • CSS如何实现元素隐藏不占位_使用display-none完全移除
  • 如何用GetQzonehistory完整备份QQ空间说说历史记录:终极免费解决方案
  • Altium Designer 23导出Gerber文件保姆级教程,附嘉立创下单全流程
  • AI时代高效管理个人笔记!Windows本地部署MaxKB,打造专属可视化知识库(超详细无坑版)
  • m4s-converter:B站缓存视频转换终极指南,三步拯救无法播放的珍贵内容
  • 2026年最新版看板管理系统大全:10个高效看板管理系统助力团队协作
  • 【Number0-高光谱检测技术-烂尾楼翻新】
  • CSS 悬停箭头闪烁偏移问题的根源与稳定解决方案
  • 【CS336】分词器:分词器原理与 BPE 实现
  • 开源神器Buzz深度评测:Whisper模型哪家强?实测对比tiny到large的准确率与速度
  • 线性代数实战:5分钟掌握二阶矩阵逆矩阵的快速计算技巧
  • 模型服务化:TorchServe 与 Triton Inference Server 深度实践
  • 用FastAPI从0到1写一个真正可用的接口服务
  • 3D 地球卫星轨道可视化平台开发 Day1(3D 场景、卫星渲染与筛选交互实现)
  • 从LLM幻觉到生产级健壮性,智能代码生成错误检测与修复全链路落地手册,覆盖GitHub Copilot/CodeWhisperer/Tabnine三大引擎
  • c++任意精度定点类型说明
  • 10. 如何批量处理圆角和倒角? I ANSA 设计小诀窍系列
  • 2026年4月怎么集成OpenClaw?华为云7分钟小白流程+大模型APIKey、Skill整合
  • 手搓STM32H743开源飞控系列教程---(七) 从零到一:三种固件烧录方式全场景实战解析
  • 3D地球卫星轨道可视化平台开发Day2(轨道错位Bug修复+模块化结构优化)
  • 2026 年优质农家乐推荐榜:杭州临安双福居农家乐领衔,精选品质之选 - 海棠依旧大
  • 【紧急预警】AI代码提交正在污染你的主干分支:3步紧急隔离+4层防御机制已验证
  • 5分钟上手LogcatReader:安卓设备日志查看神器
  • CentOS7.9 LVM生产环境扩容【KVM虚拟化需要】20260415001篇
  • LX Music Desktop:免费开源跨平台音乐播放器的完整解决方案
  • 打破“存储墙”,为AI硬件提供新路径
  • XHS-Downloader深度解析:小红书内容采集的3大核心技术架构与5倍性能优化方案
  • Super Qwen Voice World智能语音助手开发:基于Python的完整项目实战
  • 大厂 Multi-Agent 落地经验:字节跳动智能创作平台的架构拆解
  • MOPSO算法实战:如何用它搞定你的多目标优化项目?(从理论到调参全解析)