news 2026/4/17 22:56:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【个人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() 获取预测类别


总结

以上就是今天要讲的内容

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 22:55:35

矽力杰 Silergy SA21308B 低压差线性稳压器 佰祥电子

突破车载供电可靠性与功耗痛点!SA21308B车规级AEC-Q100110mV超低压差的五大核心优势导语:当前在智能座舱信息娱乐系统、汽车仪表盘显示屏以及高级车载摄像头等车规级电子设备的设计中,工程师们普遍面临着车载电网环境极其恶劣、对低压差线性稳…

作者头像 李华
网站建设 2026/4/17 22:55:34

告别繁琐计算!用QT集成这个CANFD波特率计算器,配置效率翻倍

告别繁琐计算!用QT集成这个CANFD波特率计算器,配置效率翻倍 在嵌入式开发领域,CANFD(Controller Area Network Flexible Data-rate)协议因其更高的数据传输速率和更大的数据负载能力,正逐渐取代传统CAN总线…

作者头像 李华
网站建设 2026/4/17 22:53:37

终极Windows优化工具:Win11Debloat让系统重获新生

终极Windows优化工具:Win11Debloat让系统重获新生 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declutter and custom…

作者头像 李华
网站建设 2026/4/17 22:52:24

轴承润滑脂:机械运转的“生命血液”

轴承润滑脂:机械运转的“生命血液”轴承是工业设备中减少摩擦、支承转动的核心元件。而轴承润滑脂则被誉为轴承的“生命血液”,其性能的优劣直接关系到设备的运行稳定性、能耗以及使用寿命。一、 润滑脂的基本构成理解润滑脂,可以将其类比为一…

作者头像 李华
网站建设 2026/4/17 22:51:33

牛客网最新 BATJ 等一线互联网大厂秋招面试题汇总,速刷

本文收集整理了各大厂常见面试题 N 道,你想要的这里都有内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、Spring Boot、Spring Cloud、RabbitMQ、Kafka、Linux 等技术栈……由于题量较多,篇幅的…

作者头像 李华
网站建设 2026/4/17 22:51:32

Hermes和OpenClaw的技术融合与未来产业发展

对比维度HermesOpenClaw核心特性永久本地记忆、自动技能沉淀、持续自我优化多模型兼容、本地化部署、高频迭代、庞大开源生态架构与性能显著降低Token消耗,支持SQLite本地知识库,跨会话复用经历从流量泡沫向价值验证的行业回调,强调深度定制与…

作者头像 李华