LeNet 系列
- 实践部分
- 1.引言
- 2. limu代码
- 3. plpal代码
- 3.1 代码调试
- 3.2 代码详解
- 4. 总结
实践部分
Lenet的实现分为两种代码,一种是李沐老师的实现代码以及b友up霹雳啪啦的代码,两者都有不同的优点,李老师的lenet十分还原原著中的操作,差异化比较小,并且使用fashion_mnist数据集,但其缺点也比较明显,较多的使用了团队封装的包,对初学者只能看到简单的流程。霹雳啪的代码则是结合了pytorch官网中对这个lenet的实现相对而言是一个完整且清晰的项目,都是借助官网代码进行完成。所以我在这里对两种代码的解决方式进行讲解, 便于各位参考学习。
李老师的代码地址https://zh.d2l.ai/chapter_convolutional-neural-networks/lenet.html
劈啦啪同学的地址 https://github.com/2578562306/deep-learning-for-image-processing/blob/master/pytorch_classification/Test1_official_demo/model.py
1.引言
LeNet,这是最早期的卷积神经网络之一,它由 AT&T 贝尔实验室的研究员 Yann LeCun 在 1989 年提出,并因其在计算机视觉任务中的卓越性能而广受关注。LeNet 的设计初衷是用于识别图像中的手写数字(LeCun et al., 1998),并标志着使用反向传播算法成功训练卷积神经网络的重要突破,代表了神经网络研究发展的多年成果。
LeNet 在当时展现出了与支持向量机(SVM)相媲美的性能,很快成为了监督学习任务中的主流方法。特别是,在自动取款机(ATM)中的应用,LeNet 极大地提升了处理支票识别的效率。走到今天,一些 ATM 机仍在使用上世纪 90 年代 Yann LeCun 和他的同事 Leon Bottou 编写的代码进行操作!
随着机器学习技术的不断进步,尤其是在处理图像数据方面,相比于传统的线性模型,如 softmax 回归和多层感知机,卷积神经网络能够更好地处理图像中的空间结构信息。使用卷积层而非全连接层不仅使得模型结构更为简洁,还大幅降低了模型所需的参数数量,从而提高了模型的训练与运行效率。
总之,LeNet 的开发和应用不仅推动了计算机视觉领域的发展,还对整个人工智能领域产生了长远的影响。
下图将李老师的这一代码进行说明
定义了一个使用PyTorch框架的卷积神经网络Lenet的网络架构,适用于类似于图像识别的任务。下面是逐行的解释和注释:
2. limu代码
import torch
from torch import nn
from d2l import torch as d2l # 这个是动手深度学习这本书使用的包,可能是李老师团队的其中都是对基础的基础进行封装
# 定义一个顺序模型
net = nn.Sequential(
# 第一层卷积层,输入通道为1(灰度图),***********************************8注意下这里原文原文用到的也是灰度图
# 输出通道为6,使用5x5的卷积核,边缘填充2个像素
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
# 第一个池化层(平均池化),卷积核大小为2x2,步长为2
nn.AvgPool2d(kernel_size=2, stride=2),
# 第二层卷积层,输入通道为6,输出通道为16,使用5x5的卷积核
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
# 第二个池化层(平均池化),卷积核大小为2x2,步长为2
nn.AvgPool2d(kernel_size=2, stride=2),
# 展平操作,将三维的特征图展平成一维的向量
nn.Flatten(),
# 全连接层,输入维度为16 * 5 * 5(来自上一层的输出),输出维度为120
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
# 另一个全连接层,输入维度为120,输出维度为84
nn.Linear(120, 84), nn.Sigmoid(),
# 最后一个全连接层,输入维度为84,输出维度为10(分类的类别数,例如MNIST手写数字识别为10)
nn.Linear(84, 10)
)
这个网络结构就是limu团队定义的lenet卷积神经网络,适用于基础的图像处理任务,如手写数字识别。每一层的组件都是为了从输入图像中抽取空间特征,并逐层递减空间维度,同时增加抽象层次的复杂性,直到最后通过全连接层进行分类。使用nn.Sigmoid()
作为激活函数可以帮助非线性地转化特征。可以看出来和论文精讲部分还是存在差异,激活函数的差异,以及最后使用的一个正常的卷积层,没像论文中提到的卷C3种一样的操作。其实也是现阶段计算机性能的增强产物。
比较引人注意的就是这里采用了一个nn.Sequential()和其采用的这样的卷积写法,激活函数放在卷积层后面,看起来比较整齐,对这个内容感兴趣的读者可以点这里补补课程
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) # 随机生成规定大小的数据
for layer in net: # 遍历网络的层
X = layer(X) # 将生成的数据送入到网络中,然后修改数据X内容,为了下次遍历网络的下一个层能够直接使用
print(layer.__class__.__name__,'output shape: \t',X.shape) # 打印网络的输出形状
主要用于测试PyTorch定义的 nn.Sequential
网络模型的每一层的输出尺寸。该测试对于理解各层如何改变数据尺寸、调试和优化模型极为有用。以下是对该代码的详细解释:
代码解释
-
初始化输入数据:
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
这行代码生成了一个随机浮点张量
X
,它的尺寸为(1, 1, 28, 28)
,代表了一个批次大小为1、通道数为1(灰度图),尺寸为28x28像素的图像。这种尺寸对应于典型的手写数字识别图像,如MNIST数据集中的图像格式。 -
遍历网络层:
for layer in net: X = layer(X) print(layer.__class__.__name__, 'output shape: \t', X.shape)
这段循环代码逐层应用网络
net
中的每个层到输入张量X
上,并打印出每层的类名和经过该层处理后的输出张量的形状。这有助于:- 理解数据流:通过查看每一层的输出,可以直观地看到数据如何在网络中流动和变换。
- 调试和设计帮助:若某层的输出尺寸不符合预期,可能表明在网络设计中存在问题,如卷积层的填充(padding)或步长(stride)设置不当等。
- 性能优化:有时候修改层的参数(例如减少卷积核数量或改变卷积核大小)可以在不影响网络性能的前提下,减少计算量和模型大小。
运行结果:
batch_size = 256 # 批次大小
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size) # 下载数据集并且做成了两个迭代器
可以看到还是采用了李老师数据的包,这段代码涉及到使用 d2l.load_data_fashion_mnist 函数从李沐老师所提供的 d2l (Dive into Deep Learning)工具包中加载 Fashion-MNIST 数据集,并将其组织成适用于训练和测试的迭代器。当然在实际的实践场景中,读者需要自行将数据集划分整理并且做成可迭代对象。
由于是封装好的迭代器对象,我对数据进行了展示各位可以看下原始数据集就是各种各样的衣服的类别啥的
import torch
from torchvision.utils import make_grid
import numpy as np
import matplotlib.pyplot as plt
# 假设 train_iter 是一个 DataLoader 实例,迭代提供批次数据
for x, y in train_iter:
# x 的形状通常是 [批大小, 通道, 高度, 宽度]
# 使用 torchvision 的 make_grid 创建一个网格图像
grid_img = make_grid(x, nrow=19) # nrow 是每行显示的图像数量
# 将张量转换为 numpy 数组,适合 matplotlib 显示
plt.imshow(grid_img.permute(1, 2, 0)) # 调整维度以适应 matplotlib 的显示要求
plt.show()
# 可以在这里添加 break 如果你只想显示第一个批次
break
代码执行结果:
数据有了模型有了,可以开始训练了,看过我之前博文的小伙伴可以看奥这里很熟悉就是简单的训练代码,咱们一行一行走起来:
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型(在第六章定义)"""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
逐行解释上述代码:
train_ch6
函数为啥这么命名呢,函数名 train_ch6 中的 ch6 通常代表 “Chapter 6”,这表明该函数是在《动手学深度学习》一书中第六章使用或定义的代码。很好的入门书籍,各位感兴趣可以看看
函数概览
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
net
: 要训练的网络模型。train_iter
: 训练数据的迭代器。test_iter
: 测试数据的迭代器,用于评估模型性能。num_epochs
: 训练的轮数。lr
: 学习率。device
: 训练使用的设备(例如:"cuda"
或"cpu"
)。
关键步骤和组件
-
权重初始化:
def init_weights(m): if type(m) == nn.Linear or type(m) == nn.Conv2d: nn.init.xavier_uniform_(m.weight) net.apply(init_weights)
使用Xavier均匀初始化对模型的线性层和卷积层权重进行初始化。这有助于保持网络各层激活值和梯度大小适中,促进稳定训练。
-
设置优化器和损失函数:
optimizer = torch.optim.SGD(net.parameters(), lr=lr) loss = nn.CrossEntropyLoss()
使用随机梯度下降(SGD)优化器和交叉熵损失函数,这是多分类问题中常用的组合。
-
动画显示训练进度:
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=['train loss', 'train acc', 'test acc'])
动态显示训练损失、训练精度和测试精度。人家用了自己的包。埋个坑有兴趣的dd我,我开一专栏讲解下
-
训练循环:
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3) # 创建一个累加器来存储总损失、总准确率以及处理的总样本数,以计算平均损失和平均准确率。
net.train() # 调整模型为训练模式,可以进行训练了
for i, (X, y) in enumerate(train_iter): # 遍历训练数据迭代器,获得一批数据X和它们的标签y。
timer.start() # 计时器开始
optimizer.zero_grad() # 梯度信息清除
X, y = X.to(device), y.to(device) #将数据放到设备上看你是cpu还是GPU
y_hat = net(X) # 模型预测结果
l = loss(y_hat, y) # 计算损失
l.backward() # 计算梯度
optimizer.step() # 优化其开始通过梯度信息更改参数
with torch.no_grad(): # 在不计算梯度的情况下,加总损失和准确率,并计算总样本数。
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop() # 计时器结束
train_l = metric[0] / metric[2] # 计算平均训练损失 (train_l) 和平均训练精度 (train_acc):
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
# 条件判断 if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: 这行代码决定了何时更新动画显示的频率。通常我们不会每处理完一个 batch 就更新一次动画,因为那样会频繁地进行绘图操作,降低训练效率。这里设定的是每处理总批次数的五分之一时更新一次,或者在每个 epoch 的最后一个 batch 更新。这样可以在不影响性能的情况下较频繁地观察训练进度。
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
这个代码还是很精美的可以使用到自己的模型中
每个周期(epoch)中,函数遍历训练数据集,每次迭代做如下操作:
- 加载数据到指定设备。
- 前向传播计算模型输出和损失。
- 反向传播更新模型参数。
- 计算并累积损失和精度。
- 定期(每epoch的五分之一处)使用
animator
更新训练进度展示。
-
评估模型: 训练完后开始使用对模型进行测试
test_acc = evaluate_accuracy_gpu(net, test_iter)
测试函数是之前写好的,我们看下其内容;
您的函数evaluate_accuracy_gpu
是用于评估在给定数据集上,基于GPU运行的模型准确率的实用函数。这个函数将模型和数据迭代器作为输入,并输出模型的准确率。这里对函数的工作原理和关键部分提供一个详细的解释。
函数解释
def evaluate_accuracy_gpu(net, data_iter, device=None):
"""使用GPU计算模型在数据集上的精度"""
net
:待评估的神经网络模型。data_iter
:提供数据的迭代器,通常包括特征和标签。device
:指定运行计算的设备,如 GPU。如果没有指明,则自动选择模型参数所在的设备。
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
- 设置为评估模式 (
eval()
):这一步是为了确保在评估过程中模型的某些特定层(如Dropout和BatchNorm层)表现出与训练时不同的行为。 - 自动设备选择:如果没有显式指定设备,函数会检测模型参数所在的设备,并将其作为计算设备。
metric = d2l.Accumulator(2) # 正确预测的数量,总预测的数量
- 使用
d2l.Accumulator
实例来累积正确预测的数量和总的预测数量。这是一个简便的工具,用于在遍历数据时累积求和。
with torch.no_grad():
- 使用
torch.no_grad()
开启一个上下文管理器,以阻止PyTorch跟踪和存储计算图中的梯度信息,旨在减少内存的使用和提高计算速度,因为在评估模型时不需要进行反向传播。
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
- 数据装置设置:将输入
X
和标签y
移至指定的device
(GPU)上。 - 计算准确率:通过
d2l.accuracy
计算当前批次的准确率,并更新到metric
。 y.numel()
:返回y
中元素的总数,即本批次的大小。
return metric[0] / metric[1]
- 计算总的正确率:将累积的正确预测数量除以总的预测数量,得到整个数据集上的平均准确率。
- 输出训练结果和性能:
最后输出训练损失、训练精度、测试精度和处理速度(样本/秒)。print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, test acc {test_acc:.3f}') print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(device)}')
lr, num_epochs = 0.3, 100
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) # 调用函数开始执行
运行结果:
https://zh.d2l.ai/chapter_convolutional-neural-networks/lenet.html#
这里是李老师的代码地址,顺序执行即可
3. plpal代码
下面开始对b友噼里啪啦的lenet实现代码进行讲解。下面我们将深入解析基于LeNet实现的代码。对于对这部分内容感兴趣的读者,您可以通过点击这里访问相关代码。值得注意的是,虽然这套代码的结构和李老师的版本相比可能不那么直观易用,但对于大型项目来说,将不同功能模块化是一种更为直接且易于维护的方式。显然,李老师的代码主要是为了帮助初学者快速入门。在您下载并准备运行这些代码之前,可能需要进行一些修改和调整。接下来,我们会详细查看主要的三个文件——model
、train
和 predict
,这些文件代表了深度学习项目中的传统三大核心组件。
3.1 代码调试
李老师的代码只要环境配置正确就能跑,这个代码还是需要调试下的。
将训练文件中的地址按照自己的地址进行修改,这行代码会从目标网址下载数据存入到你修改地址的文件夹中,用于模型的训练。修改好地址后要将这两个参数改成真True
。
模型训练后会对参数进行保存,便于验证时候使用,所以需要修改喜爱这里的地址,把参数保存到目标目录下:
然后点运行即可看到模型的训练结果如下:
模型训练好了,那么咱们怎么测试下咱们这个模型好不好好用用呢??
各位可以随便在网络上下载一个jpg的图像文件存到代码的文件夹下。我这里随便下载了一个车的图片:
当当当:xiaomi汽车预览下:
测试一下模型能不能识别出来,当然了你下载需要判断的图的时候要看下你训练用的数据集的类别存在不,因为我们任务中使用的数据集就是包含着几个类别
然后就是修改代码中的文件位置了注意这次修改的是predict中的文件位置。
修改好后执行代码即可:但是但是,这个模型给识别成飞机了✈️了。咱们看下这个运行结果:
可以理解,因为确实数据集过于老旧,并且代码仅仅训练到了5个epoch,性能也是有限。况且咱们这小米汽车外形也是十分酷炫。感兴趣的读者可以自行改进下代码汇总的网络结果,和学习率以及epcoh数值看看能不能修正这一错误,本章节不再讨论,下面将对代码进行逐行的解释。
3.2 代码详解
上文中主要将对代码的调试过程,现阶段对代码进行逐行的精讲,便于读者理解每一部分存在的含义。首先看下模型的主体架构,即是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) # 输入通道是3和输出通道是16个
self.pool1 = nn.MaxPool2d(2, 2) # 池化层
self.conv2 = nn.Conv2d(16, 32, 5) # 从16个特征图输出成32个特征图
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
看到以上代码,最直观的感受就是没用到快捷的nn.Sequential()当然那个不是我们这个章节需要讨论的,这个网络主体,先对比下和论文中的存在哪些区别呢,我们看下论文中的模型架构:
首先输入就存在差别,论文中的输入是一个13232 的数据,但是这个神经网络接受的通道数为3,首先明确下,这个是由数据训练数据决定的。传统的lenet采用训练图是灰度图,和李老师使用数据集类似。而plpal这个b友的则是彩色的图片数据集。所以三原色,三个通道,所以模型的输入是3个通道。即三种特征图。便于直观展示,给大家看下这个这个数据集的介绍。
都是彩色图片所以三种输入通道。输出的通道则是和原文是一致的,但是原文中使用的是均值池化,这个代码中则是采用的最大值池化,现阶段最大值池化用的比较多。还有一个不同的点实际上激活函数肯定是也和原文的不同,但是大概还是整体一致的。同样人家也没有使用论文中为了减少运算量奇怪的卷积层。
回顾完模型的主体代码现在可以看训练的代码了即是train.py
文件下的代码:
import torch
import torchvision
import torch.nn as nn
from model import LeNet # 导入新建的模型
import torch.optim as optim # 优化函数
import torchvision.transforms as transforms # 处理图片数据的
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='/Users/wangyang/Desktop/计算机视觉技术/Lenet/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='/Users/wangyang/Desktop/计算机视觉技术/Lenet/data', train=False,
download=True, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=5000,
shuffle=False, num_workers=0)
# 设置批大小和线程个数,以及各种迭代器需要使用的超参数,然后生成迭代器。使用的都是很常见的函数,具体的细节,读者可参考我的这个博文内容
# 链接 https://blog.csdn.net/weixin_47332746/article/details/139290051
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(20): # 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 # 将数据中的数据和label分开
# zero the parameter gradients
optimizer.zero_grad() # 梯度清0
# 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 每 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 # 打印准确率然后损失清0
print('Finished Training')
save_path = '/Users/wangyang/Desktop/计算机视觉技术/Lenet.pth'
torch.save(net.state_dict(), save_path) # 将模型存起来
if __name__ == '__main__':
main()
这个模型还是存在诸多的不完整比如,并没有采用模型的验证模式,以及模型并没有使用测试和验证的代码分开。
for epoch in range(20):
net.train() # Ensure the network is in training mode
# Training loop...
# Validation phase
net.eval() # Set the network to evaluation mode
with torch.no_grad():
# Evaluate on the validation set
# This part of the code handles the evaluation
当然一个调试代码也不能要求的过多,作为学习使用。将训练与验证代码分离,确保在验证阶段使用 net.eval() 以正确处理特殊层的行为,是提高模型评估准确性的关键。这不仅能更精确地反映模型对未见数据的泛化能力,还有助于避免过拟合以及其他可能的评估误差。尽管在训练循环中进行快速验证可以为模型调优提供即时反馈,但在重要的评估阶段应当严格执行标准的评估程序。
同样和上文一样我们也是可以通过函数对图片进行查看的,感兴趣的读者可以自行的试试调试下。给大家看看我的效果:
代码和李老师中使用的是一致的如下:
val_loader = torch.utils.data.DataLoader(val_set, batch_size=200,
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')
for x, y in val_data_iter:
# x 的形状通常是 [批大小, 通道, 高度, 宽度]
# 使用 torchvision 的 make_grid 创建一个网格图像
grid_img = make_grid(x, nrow=19) # nrow 是每行显示的图像数量
# 将张量转换为 numpy 数组,适合 matplotlib 显示
plt.imshow(grid_img.permute(1, 2, 0)) # 调整维度以适应 matplotlib 的显示要求
plt.show()
# 可以在这里添加 break 如果你只想显示第一个批次
break
然后就是试着运行就可以了。我们继续看predict.py
文件中的代码:
导入所需库
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet
torch
: PyTorch框架,深度学习库,用于构建神经网络。torchvision.transforms
: 辅助工具,用于进行图片的预处理。Image
: 从Pillow库导入,用于打开和处理图像文件。LeNet
: 从本地model文件导入的LeNet模型类。
定义main函数
def main():
main
函数是程序的入口,当脚本被直接运行时执行。
初始化图像转换器
transform = transforms.Compose([
transforms.Resize((32, 32)), # 将图像调整为32x32像素
transforms.ToTensor(), # 将图像转换为PyTorch张量
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 归一化处理
])
- 使用图像转换序列来预处理输入的图像数据。包括调整大小、转换为张量和归一化。归一化使用均值 (0.5, 0.5, 0.5) 和标准差 (0.5, 0.5, 0.5),这对于图像处理是很常见的,有助于模型更好地理解图像数据。
定义类别
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
- 定义类别元组,这些类别与CIFAR-10数据集相关,模型将使用这些类别为图像分类。
加载模型并导入权重
net = LeNet() # 创建一个LeNet模型实例
net.load_state_dict(torch.load('/Users/wangyang/Desktop/计算机视觉技术/Lenet.pth'))
- 加载预先训练好的网络权重。这些权重应该是用CIFAR-10或类似数据集预训练的。
图像处理并进行分类
im = Image.open('/Users/wangyang/Desktop/计算机视觉技术/Lenet/小米汽车.jpg')
im = transform(im) # 应用上面定义的转换处理
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)]) # 输出预测的类别名称
- 使用
torch.no_grad()
表示在此代码块中不记录梯度信息,这是因为在预测模式下不需要反向传播。然后通过模型获得输出,并找到预测类别的索引,最后输出类别名。
if __name__ == '__main__':
main()
- 这是Python的常规入口点,
if __name__ == '__main__':
确保当脚本作为主程序运行时调用main()
函数,而不是在被其他脚本导入时执行。
4. 总结
本文对比了李沐老师的LeNet实现和b友噼里啪啦的实现,展示了两种不同风格的代码结构及其适用场景。李沐老师的版本面向初学者,使用自定义的d2l
库简化数据处理和训练流程,侧重于教学和易理解性,适合快速掌握基础概念。而噼里啪啦的实现则更接近实际应用,采用模块化的代码组织方式,直接应用PyTorch原生操作,更适合有一定基础的开发者深入学习和项目开发。这两种实现各有优点,选择时应考虑个人学习阶段和目标:初学者或基础理解者适合从李老师的代码学起,而希望进行实际应用或深化理解的开发者则可从劈啦啪的实践代码入手。这样的对比和选择有助于根据个人需求进行有效学习,并为将来的深入使用和开发打下坚实的基础。下一个章节将会对Alexnet进行论文精度和代码复现,感兴趣的读者可以欢迎催更讨论加油。