文章目录
- LeNet
- 1 搭建模型
- 2 训练模型
- 3 测试模型
- 3.1 预测一
- 3.2 预测二
LeNet
LeNet 诞生于 1994 年,是最早的卷积神经网络之一,并且推动了深度学习领域的发展。自从 1988 年开始,在许多次成功的迭代后,这项由 Yann LeCun 完成的开拓性成果被命名为 LeNet5【一般LeNet即指代LeNet-5】。LeNet5 的架构基于这样的观点:(尤其是)图像的特征分布在整张图像上,以及带有可学习参数的卷积是一种用少量参数在多个位置上提取相似特征的有效方式。在那时候,没有 GPU 帮助训练,甚至 CPU 的速度也很慢。因此,能够保存参数以及计算过程是一个关键进展。这和将每个像素用作一个大型多层神经网络的单独输入相反。LeNet5 阐述了那些像素不应该被使用在第一层,因为图像具有很强的空间相关性,而使用图像中独立的像素作为不同的输入特征则利用不到这些相关性。
LeNet介绍_1:https://www.jiqizhixin.com/graph/technologies/6c9baf12-1a32-4c53-8217-8c9f69bd011b
LeNet介绍_2: https://www.analyticsvidhya.com/blog/2021/03/the-architecture-of-lenet-5/
1 搭建模型
该网络有 5 层,具有可学习的参数,因此命名为 Lenet-5。它有三组卷积层,结合了平均池化。在卷积层和平均池化层之后,我们有两个完全连接的层。
该模型的输入是 32 X 32 灰度图像,因此通道数为 1。
整体项目结构
模型搭建写法
方式一: 不使用nn.Sequential
class LeNet5(nn.Module):def __init__(self):super(LeNet5, self).__init__()self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2)self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)# self.flatten = nn.Flatten() 如果不使用view,而是用flattenself.fc1 = nn.Linear(120, 84)self.fc2 = nn.Linear(84, 10)def forward(self, x):x = F.relu(self.conv1(x)) # C1x = self.pool1(x) # S2x = F.relu(self.conv2(x)) # C3x = self.pool2(x) # S4x = F.relu(self.conv3(x)) # C5x = x.view(-1, 120) # Flatten the tensor for fully connected layer# view可以替换为如下# x = self.flatten(x) # Flatten the tensor for fully connected layerx = F.relu(self.fc1(x)) # F6x = self.fc2(x) # Output layerreturn x
方式二:使用nn.Sequential
class LeNet5(nn.Module):def __init__(self):super(LeNet5, self).__init__()self.model = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2), # C1nn.ReLU(),nn.AvgPool2d(kernel_size=2, stride=2), # S2nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0), # C3nn.ReLU(),nn.AvgPool2d(kernel_size=2, stride=2), # S4nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0), # C5nn.ReLU())self.fc1 = nn.Linear(120, 84) # F6self.fc2 = nn.Linear(84, 10) # Output layerdef forward(self, x):x = self.model(x) # Pass through sequential layersx = x.view(-1, 120) # Flatten the tensor for fully connected layer x = F.relu(self.fc1(x)) # F6x = self.fc2(x) # Output layerreturn x
写法上的对比:
依据个人喜好
x = F.relu(self.conv1(x)) # C1
等价于
nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2), # C1
nn.ReLU()
view和flatten的对比:
- view 是一个张量方法,用于重新调整张量的形状。它可以用于多种形状变换,包括展平张量。使用 view 展平张量时,需要明确指定新形状。
- flatten 是 PyTorch 的一个模块类(nn.Flatten)或张量方法(tensor.flatten),专门用于展平操作。它可以自动将输入张量展平为二维张量,保持批次维度不变。
何时使用view:
当需要对张量进行复杂的形状变换时。
需要手动指定新形状。
确保张量是连续存储的。
何时使用flatten:
当只需要展平操作时。
不需要手动计算形状。
自动处理张量的存储连续性。
Softmax函数
Softmax函数是机器学习和深度学习中常用的一种归一化函数,通常用于多分类问题的输出层。它将一个向量中的未归一化数值(logits)转换为一个概率分布。
- Softmax函数将 logits 映射到 (0, 1) 区间内,并且各类别的概率总和为1,这样可以避免数值上的不稳定性。
- Softmax函数会放大概率大的类别和概率小的类别之间的差异,从而使模型更容易做出明确的决策。
- Softmax函数的输出可以解释为每个类别的预测概率,这对决策和解释模型输出非常有用。
注意:
如果在模型的最后一层应用了Softmax,并且使用了nn.CrossEntropyLoss进行训练,那么可能会遇到数值不稳定的问题,因为nn.CrossEntropyLoss已经在内部应用了Softmax。所以:1、如果在模型的forward中使用了softmax,那么应该使用nn.NLLLoss(负对数似然损失)来训练模型,因为它期望输入的是对数概率。
2、如果在forward中没有使用softmax,那么应该使用nn.CrossEntropyLoss进行训练【nn.CrossEntropyLoss 是 PyTorch 中用于多类分类问题的损失函数。它结合了 nn.LogSoftmax 和 nn.NLLLoss(负对数似然损失),因此可以直接处理未归一化的 logits,并计算出交叉熵损失。】该模型在forward中并没有直接显示使用softmax函数做归一化处理,因此在训练过程中模型输出的结果就是未归一化的概率分布【未经过 softmax 的 logits】。而是在训练阶段定义了交叉熵作为损失函数。
2 训练模型
mnist数据集:https://yann.lecun.com/exdb/mnist/
MNIST(Modified National Institute of Standards and Technology)数据集是一个经典的手写数字识别数据集,被广泛用于机器学习和计算机视觉领域。
数据集概述
内容:MNIST 数据集包含 70,000 张手写数字图像,这些图像属于 10 个类别(0 到 9),每个类别代表一个数字。
分布:
训练集:60,000 张图像
测试集:10,000 张图像
图像大小:每张图像的尺寸为 28x28 像素。
图像格式:灰度图像,每个像素值是从 0 到 255 的整数,其中 0 代表黑色,255 代表白色,其他值代表不同的灰度级别。
数据集文件结构
MNIST 数据集通常分为四个主要文件:
train-images-idx3-ubyte: 训练集图像数据。
train-labels-idx1-ubyte: 训练集标签数据。
t10k-images-idx3-ubyte: 测试集图像数据。
t10k-labels-idx1-ubyte: 测试集标签数据。
样本图片:
import torch
from torch import nn
from torch import optim
from LeNet5 import LeNet5
from datetime import datetime
from torch.utils.data import DataLoader
from torch.optim import lr_scheduler
from torchvision import datasets, transforms
import os# 先判断是否有data目录
if not os.path.exists("../data"):os.mkdir("../data")
# 获取Mnist数据
data_train = datasets.MNIST(root="../data", train=True, transform=transforms.ToTensor(), download=True)
data_test = datasets.MNIST(root="../data", train=False, transform=transforms.ToTensor(), download=True)
# 获取数据集的长度
data_train_len = len(data_train)
data_test_len = len(data_test)
# DataLoader加载数据
train_dataLoader = DataLoader(batch_size=16, dataset=data_train, shuffle=True)
test_dataLoader = DataLoader(batch_size=16, dataset=data_test, shuffle=True)# 定义是否使用GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前设备{}".format(device))# 实例化模型
my_nn = LeNet5().to(device)
# 定义交叉熵损失函数
lossFn = nn.CrossEntropyLoss()
# 定义优化器 随机梯度下降
start_lr = 0.01
optimizer = optim.SGD(my_nn.parameters(), lr=start_lr, momentum=0.9)
# 学习率衰减 ,每十轮衰减一次 ,衰减为原来的0.1
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)# 定义训练函数
def train_net(dataloader, model, loss_fn, optim, lr_scheduler):model.train()# 一轮次的损失值、一轮次的正确率、一轮次被分为几个批次epoch_loss, epoch_acc, batch_count = 0.0, 0.0, 0# 学习率衰减的几轮cur_epoch=lr_scheduler.last_epoch+1print("当前为第{}轮次,此时的学习率为{}".format(cur_epoch,optim.param_groups[0]['lr']))for data in dataloader:img, tag = data# 将数据移动到设备上【cpu或者gpu,看支持情况】img, tag = img.to(device), tag.to(device)# 记录该批次模型训练输出结果output = model(img)# 记录该批次的损失值batch_loss = loss_fn(output, tag)# 对 output 张量的每一行取最大值和对应的索引。_ 保存最大值,pred 保存最大值的索引,也就是预测的类别。pred_num, pred = torch.max(output, axis=1)# 将正确的预测数量除以总的样本数量,得到当前批次的准确率cur_acc = torch.sum(tag == pred) / output.shape[0]# 梯度清零,pytorch框架需要手动调用optim.zero_grad()# 反向传播计算梯度batch_loss.backward()# 更新模型参数optim.step()# 累加批次损失值epoch_loss += batch_loss.item()# 累加批次正确率epoch_acc += cur_acc.item()# 记录总批次数batch_count += 1"""lr_scheduler.step() 有一个内部计数器来跟踪训练进度[轮次]。每次调用 step() 方法时,StepLR 会检查当前的 epoch 数量是否达到了 step_size 的倍数。如果是,它会按照设定的 gamma 参数更新学习率;如果不是,它不会调整学习率。"""# 每个epoch结束后,更新学习率lr_scheduler.step()print("Train_Avg_Batch_Loss:{} ".format(epoch_loss / batch_count))print("Train_Avg_Batch_Acc: {}".format(epoch_acc / batch_count))# 定义模型测试方法
def test_net(dataloader, model, loss_fn):model.eval()# 一轮次的损失值、一轮次的正确率、一轮次被分为几个批次epoch_loss, epoch_acc, batch_count = 0.0, 0.0, 0# 禁用梯度计算,节省内存和加速计算【在test阶段,是不需要梯度计算的】with torch.no_grad():for data in dataloader:img, tag = data# 将数据移动到设备上【cpu或者gpu,看支持情况】img, tag = img.to(device), tag.to(device)# 记录该批次模型训练输出结果output = model(img)# 记录该批次的损失值batch_loss = loss_fn(output, tag)# 对 output 张量的每一行取最大值和对应的索引。pred_num存最大值,pred 保存最大值的索引,也就是预测的类别。pred_num, pred = torch.max(output, axis=1)# 将正确的预测数量除以总的样本数量,得到当前批次的准确率cur_acc = torch.sum(tag == pred) / output.shape[0]# 累加批次损失值epoch_loss += batch_loss.item()# 累加批次正确率epoch_acc += cur_acc.item()# 记录总批次数batch_count += 1print("Test_Avg_Batch_Loss:{} ".format(epoch_loss / batch_count))print("Test_Avg_Batch_Acc: {}".format(epoch_acc / batch_count))# 返回批次正确率return epoch_acc / batch_count"""
只保存两个模型:1、测试结果最好的那一个模型2、最后的那一个模型
"""
# 开始训练模型
epoch = 20
max_acc = 0.0
# 计时
start_time=datetime.now()
for i in range(epoch):print("-----第{}轮训练开始-----".format(i + 1))train_net(train_dataLoader, my_nn, lossFn, optimizer,lr_scheduler)acc = test_net(test_dataLoader, my_nn, lossFn)if acc > max_acc:save_dir = "../save_models"if not os.path.exists(save_dir):os.mkdir(save_dir)max_acc = acc# 保存模型[只保留模型参数]torch.save(my_nn.state_dict(), "../save_models/MaxAccModel.pth")print("save max_acc ok!")if i + 1 == epoch:torch.save(my_nn.state_dict(), "../save_models/LastModel.pth")print("save last ok!")
end_time=datetime.now()
print("start_time:{}".format(start_time))
print("end_time:{}".format(end_time))
print("{}训练总用时:{}".format(device,end_time-start_time))
print("Done!")
3 测试模型
ToPILImage 是 torchvision.transforms 中的一个类,用于将张量(Tensor)转换为 PIL 图像。
它不仅适用于灰度图像,也适用于彩色图像。
具体来说,它的作用是将 PyTorch 张量转换为
Python Imaging Library (PIL) 的图像对象,以便进行图像显示或保存。
灰度图像:如果输入的张量是 [1, H, W],它会被解释为灰度图像。
彩色图像:如果输入的张量是 [3, H, W],它会被解释为 RGB 彩色图像。
使用步骤:
from torchvision.transforms import ToPILImage
showImg = ToPILImage()
showImg(img).show()
img=Variable(torch.unsqueeze(img,dim=0).float(),requires_grad=False).to(device)
1、torch.unsqueeze(img, dim=0) 在第 0 维增加一个维度,变成 [1, C, H, W]。这相当于添加了一个 batch 维度,
因为神经网络通常需要输入形状为 [N, C, H, W] 的数据,其中 N 是批次大小。
2、将张量的数据类型转换为 float 类型。这是因为神经网络的输入通常要求是浮点数。
3、Variable 是 PyTorch 的一个类,用于封装张量。尽管在新版的 PyTorch 中直接使用张量即可,但这种用法仍然适用。
4、requires_grad=False 表示在这次操作中不需要计算梯度。因为我们只是在进行推理,不需要反向传播。
5、to(device)将张量移动到指定设备(CPU 或 GPU)
新版可以直接写作:img=torch.unsqueeze(img, dim=0).float().to(device)
3.1 预测一
用mnist测试数据集去做预测【也称为:推理】
import torch
from torch.autograd import Variable
from torchvision import datasets
from torchvision.transforms import transforms
from LeNet5 import LeNet5
from torchvision.transforms import ToPILImage# 获取Mnist数据
data_test = datasets.MNIST(root="../data", train=False, transform=transforms.ToTensor(), download=True)# 加载模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
LeNet5_Model = LeNet5().to(device)
LeNet5_Model.load_state_dict(torch.load("../save_models/MaxAccModel.pth", map_location=device))# 可视化图片
"""
ToPILImage 是 torchvision.transforms 中的一个类,用于将张量(Tensor)转换为 PIL 图像。
它不仅适用于灰度图像,也适用于彩色图像。
具体来说,它的作用是将 PyTorch 张量转换为
Python Imaging Library (PIL) 的图像对象,以便进行图像显示或保存。
灰度图像:如果输入的张量是 [1, H, W],它会被解释为灰度图像。
彩色图像:如果输入的张量是 [3, H, W],它会被解释为 RGB 彩色图像。
"""
showImg = ToPILImage()# 预测标签【要不要都行,】
tags = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]for i in range(5):img,tag=data_test[i][0],data_test[i][1]showImg(img).show()"""1、torch.unsqueeze(img, dim=0) 在第 0 维增加一个维度,变成 [1, C, H, W]。这相当于添加了一个 batch 维度,因为神经网络通常需要输入形状为 [N, C, H, W] 的数据,其中 N 是批次大小。2、将张量的数据类型转换为 float 类型。这是因为神经网络的输入通常要求是浮点数。3、Variable 是 PyTorch 的一个类,用于封装张量。尽管在新版的 PyTorch 中直接使用张量即可,但这种用法仍然适用。4、requires_grad=False 表示在这次操作中不需要计算梯度。因为我们只是在进行推理,不需要反向传播。5、to(device)将张量移动到指定设备(CPU 或 GPU)"""img=Variable(torch.unsqueeze(img,dim=0).float(),requires_grad=False).to(device)# 可改写为: img = torch.unsqueeze(img, dim=0).float().to(device)with torch.no_grad():pred=LeNet5_Model(img)"""print(pred) ——》tensor([[ -8.3555, 1.5206, -3.0009, 7.4744, 1.2823, -3.6160, -12.8001,20.1332, -6.3026, 4.4215]])"""# 这里对于mnist数据集,索引正好就是和标签对应——》比如:1索引位置记录的正好是1# 所以去获取概率最大的位置的索引值,直接就可以和正确的类别标签作对比predicted,actual=pred.argmax(1).item(),tagprint("预测结果:{},正确结果:{}".format(predicted,actual))
3.2 预测二
彩色数字图像预测【小白刚学习,纯好奇自己瞎搞,同一个模型到这里预测结果很垃圾
】
模型训练是用的灰度图像是1通道的,而这里的待预测图像都是彩色的三通道数据。所以这里不同于上面的mnist数据集那样直接可以用,而是需要对图片数据做出一定的处理之后,才可以。
自定义处理函数
class Utils:def reSizeAndShape(self, input):img = Image.open(input)transform = torchvision.transforms.Compose([torchvision.transforms.Resize((28, 28)), # 修改待测试图片的尺寸为28*28torchvision.transforms.ToTensor() # 将待测试图片转化为可以被接受的tensor类型])img = transform(img)# print(img.shape) --》torch.Size([3, 28, 28]) 待测图片并非灰度图像,而是彩色三通道的图像# 训练模型是灰度图像,通道数为1,所以修改为1通道,那么N值给-1,自动计算填充 (N,C,W,H)img = torch.reshape(img, (-1, 1, 28, 28))# print(img.shape) ——》torch.Size([3, 1, 28, 28]) 原来图像是3*28*28的元素个数,重塑形状必定是保持元素个数不变,因此自然就是3*1*28*28return img
1、需要将图片尺寸修改为 28 ∗ 28 28*28 28∗28的,因为mnist数据集的图片是该尺寸,模型是那该数据集训练的,所以先改图片尺寸。
2、转换图片数据格式,换成模型网络支持的tensor格式
3、重塑图像数据形状,将彩色3通道改为灰度1通道,N给值-1,让其更具计算结果自动填充。(N,C,W,H)原来图像是 3 ∗ 28 ∗ 28 3*28*28 3∗28∗28的元素个数,重塑形状必定是保持元素个数不变,因此自然就是 3 ∗ 1 ∗ 28 ∗ 28 3*1*28*28 3∗1∗28∗28
经此变换,我的理解是:就是将原始数据一张彩色图像的元素,切分成了三份,相当于一张彩色图片数据被分为了三张灰度图像数据。依据下面的输出结果似乎可以得到验证。
print(output) ——》 tensor([[ 0.6269, 0.4243, 1.3495, 0.0457, -0.2867, -0.1191, -1.6458, 1.4099, -0.7365, -0.2978],
[ 0.6269, 0.4243, 1.3495, 0.0457, -0.2867, -0.1191, -1.6458, 1.4099,-0.7365, -0.2978],
[ 0.6269, 0.4243, 1.3495, 0.0457, -0.2867, -0.1191, -1.6458, 1.4099, -0.7365, -0.2978]], grad_fn=<AddmmBackward0>)
import torch
import torchvision
from PIL import Image
from LeNet5 import LeNet5# 自定义工具类
class Utils:def reSizeAndShape(self, input):img = Image.open(input)transform = torchvision.transforms.Compose([torchvision.transforms.Resize((28, 28)), # 修改待测试图片的尺寸为28*28torchvision.transforms.ToTensor() # 将待测试图片转化为可以被接受的tensor类型])img = transform(img)# print(img.shape) --》torch.Size([3, 28, 28]) 待测图片并非灰度图像,而是彩色三通道的图像# 训练模型是灰度图像,通道数为1,所以修改为1通道,那么N值给-1,自动计算填充 (N,C,W,H)img = torch.reshape(img, (-1, 1, 28, 28))# print(img.shape) ——》torch.Size([3, 1, 28, 28]) 原来图像是3*28*28的元素个数,重塑形状必定是保持元素个数不变,因此自然就是3*1*28*28return img# 待测试图片
imgs = ["1.png", "3_1.png", "4_3.png", "5.png","3.png", "4.png", "4_2.png", "6.png","7.png","8.png"
]
# 加载模型
LeNet5_Model = LeNet5()
LeNet5_Model.load_state_dict(torch.load("../save_models/MaxAccModel.pth", map_location=torch.device('cpu')))
# 实例化工具类
utils = Utils()
# 测试
for name in imgs:img_path = "../test_imgs/" + nameprint("待测试图像名称:{}".format(name.split(".")[0]))# 对图片做出处理img = utils.reSizeAndShape(img_path)# 放入模型预测output = LeNet5_Model(img)# print(output.shape) ——》torch.Size([3, 10])"""该结果就是与上面定义的图像处理函数有关,reshape之后,就是将原始数据,切分成了三份,相当于一张彩色图片数据被分为了三张灰度图像数据# print(output) ——》 tensor([[ 0.6269, 0.4243, 1.3495, 0.0457, -0.2867, -0.1191, -1.6458, 1.4099,-0.7365, -0.2978],[ 0.6269, 0.4243, 1.3495, 0.0457, -0.2867, -0.1191, -1.6458, 1.4099,-0.7365, -0.2978],[ 0.6269, 0.4243, 1.3495, 0.0457, -0.2867, -0.1191, -1.6458, 1.4099,-0.7365, -0.2978]], grad_fn=<AddmmBackward0>)"""print(output.argmax(1)) # ——》tensor([3, 3, 3]) 横向取出最低维的概率最大的索引# print("图片预测结果为:{}".format(output.argmax(1)[0].item()))print("\n=================\n")
结果:很差劲,就对一个,哈哈。
借助ToPILImage去将分割后的张量转换为图片,似乎和我想的将一张图片数据分成三份灰度的,会产生杂乱图片的结果不一样。【比较懵,待后续学习去理解】
import torch
import torchvision
from PIL import Image
from torchvision.transforms import ToPILImagefrom LeNet5 import LeNet5# 自定义工具类
class Utils:def reSizeAndShape(self, input):img = Image.open(input)transform = torchvision.transforms.Compose([torchvision.transforms.Resize((28, 28)), # 修改待测试图片的尺寸为28*28torchvision.transforms.ToTensor() # 将待测试图片转化为可以被接受的tensor类型])img = transform(img)# print(img.shape) --》torch.Size([3, 28, 28]) 待测图片并非灰度图像,而是彩色三通道的图像# 训练模型是灰度图像,通道数为1,所以修改为1通道,那么N值给-1,自动计算填充 (N,C,W,H)img = torch.reshape(img, (-1, 1, 28, 28))# print(img.shape) ——》torch.Size([3, 1, 28, 28]) 原来图像是3*28*28的元素个数,重塑形状必定是保持元素个数不变,因此自然就是3*1*28*28return img# 待测试图片
imgs = ["3_1.png"
]
# 加载模型
LeNet5_Model = LeNet5()
LeNet5_Model.load_state_dict(torch.load("../save_models/MaxAccModel.pth", map_location=torch.device('cpu')))
# 实例化工具类
utils = Utils()
showImg = ToPILImage()
# 测试
for name in imgs:img_path = "../test_imgs/" + nameprint("待测试图像名称:{}".format(name.split(".")[0]))# 对图片做出处理img = utils.reSizeAndShape(img_path)for i in range(3):# print(img[i])# print(img[i].shape)showImg(img[i]).show()# 放入模型预测output = LeNet5_Model(img)print(output.argmax(1)) # ——》tensor([3, 3, 3]) 横向取出最低维的概率最大的索引# print("图片预测结果为:{}".format(output.argmax(1)[0].item()))print("\n=================\n")
结果是这样的:不是乱的,而是将彩色进行了灰度化处理,而且预测也是错的【tensor[8,4,2],里面就是预测的标签,三个都不对】