如果觉得本篇文章对您的学习起到帮助作用,请 点赞 + 关注 + 评论 ,留下您的足迹💪💪💪
LeNet是早期最具代表性的卷积神经网络,虽然现在已经出现了很多性能优于LeNet的模型,但是我认为,学习LeNet还是很有必要的,追本溯源,因为这可以使我们弄清楚卷积神经网络乃至深度学习的历史发展进程。读者需要知道,本文所构建的卷积神经网络并非原始的LeNet,而是现代改进版。
本文所使用代码下载:
链接:https://pan.baidu.com/s/1ZVPVvrZl8YcEPtPep3RsJg
提取码:nsqb
本文相关推荐阅读:
一学就会 | 基于PyTorch的TensorBoard可视化
全文框架
前言
LeNet设计的巧妙之处在于,它利用卷积核实现参数共享,使用池化操作提取特征,极大减少了参数的数量,节省了大量的计算成本,最后再使用全连接神经网络进行分类识别,后来如AlexNet、VGG等网络,都是在LeNet的基础上提出的。
LeNet的网络结构示意图如下所示:
这是在原论文摘取的图片,如果感兴趣,可以去看Gradient-Based Learning Applied to Document Recognition。
构建卷积神经网络
LeNet网络所识别的目标是灰色1通道的数据,像素为(1, 32, 32),而本实验要做的是3个通道RGB彩色图片,图片像素为(3, 32, 32)。
分析模型
我们简单分析一下卷积神经网络结构:
- conv1:
conv1是卷积层,有6个卷积核,卷积核大小为(5, 5),其他都是默认值,stride=1,padding=0,输入input大小为(N, 3, 32, 32),其中 N 为批次大小,这一层输出output大小为(N, 6, 28, 28)。conv1有456个参数,其中卷积核的参数 6 x 5 x 5 x 3=450 个,偏置参数为 6 个 。
注意:经过卷积层后,应该先使用激活函数(激活函数选择ReLU),在向池化层输入。 - pool1:
在LeNet中,使用的是下采样,我们这里使用最大池化max_pooling,size为(2, 2),步长stride大小为(2, 2),max_pooling是没有参数的。输入input大小为(N, 6, 28, 28),这一层输出output大小为(N, 6, 14, 14)。 - conv2:
conv2是卷积层,有16个卷积核,卷积核大小为(5, 5),其他都是默认值,stride=1,padding=0,输入input大小为(N, 6, 14, 14),这一层输出output大小为(N, 16, 10, 10)。conv1有2416个参数,其中卷积核的参数 16 x 5 x 5 x 6=2400 个,偏置参数为 16 个 。
注意:经过卷积层后,应该先使用激活函数(激活函数选择ReLU),在向池化层输入。 - pool2:
依旧在这里使用最大池化max_pooling,size为(2, 2),步长stride大小为(2, 2),max_pooling是没有参数的。输入input大小为(N, 16, 10, 10),这一层输出output大小为(N, 16, 5, 5)。 - fc1:
fc1是全连接层,有 120 个神经元,经过池化层pool2的输出(N, 16, 5, 5),需要调整维度为(N, 400)。输入input大小为(N, 400),输出大小为(N, 120)。可训练参数为48120,其中权重为 48000 个,偏置为 120 个。
注意:经过全连接层后,应该使用激活函数(激活函数选择ReLU)。 - fc2:
fc2是全连接层,有 84 个神经元,输入input大小为(N, 120),输出大小为(N, 84)。可训练参数为10164,其中权重为 10080 个,偏置为 84 个。
注意:经过全连接层后,应该使用激活函数(激活函数选择ReLU)。 - fc3:
fc3是全连接层,有 10 个神经元,输入input大小为(N, 84),输出大小为(N, 10)。可训练参数为850,其中权重为 840 个,偏置为 10 个。
注意:经过最后一层全连接层后,不要使用激活函数(如softmax),因为在进行损失函数计算时,会对最后一层的输出进行处理。
构建LeNet网络模型如下:
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, 6, 5)
self.pool1 = nn.MaxPool2d(2,2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(16*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))
x = self.pool1(x)
x = F.relu(self.contv2(x))
x = self.pool2(x)
x = x.view(-1, 16*5*5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
检测模型
使用torchsummary工具检测网络及参数,有关torchsummary的内容,请点击这里,根据目录选择相关内容,进行学习。
model = LeNet()
from torchsummary import summary
summary(model, input_size=(3, 32, 32),device="cpu")
网络结构及参数如下:
数据处理
CIFAR-10数据集
该数据集有60000张彩色图像,通道为3,这些图像像素为32 * 32的,分为10个类,每类有60000张图像。其中50000张图像作为训练集数据,10000张图像作为测试集数据。
该数据集内10个类别分别为:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。
下载数据集,点击CIFAR-10数据集。
加载并标准化CIFAR-10数据集
使用Torchvision,可以很容易的加载CIFAR-10数据集。
1、导入需要的包:
import torch
import torchvision
import torchvision.transforms as transforms
2、数据处理
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
torchvision.transforms.ToTensor作用是:
- 将PIL Image和numpy.ndarray数据类型转换为PyTorch可使用的tensor数据类型;
- 将PIL Image和numpy.ndarray维度(H, W, C)转化为(C, H, W);
- 将范围[0, 255]转化为[0.0, 1.0]。
torchvision.transforms.Normalize作用是:
- 用均值和标准差对张量图像进行归一化;
- 均值为( M 1 M_1 M1… M n M_n Mn),标注差为( S 1 S_1 S1… S n S_n Sn),n为通道C的数量;
- 计算公式为,(input[channel] - mean[channel]) / std[channel]
3、加载数据
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
shuffle=True, num_workers=0)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
shuffle=False, num_workers=0)
- 参数root指定数据集位置,./data表示在当前文件夹新建data文件夹,存储CIFAR-10数据。
- 参数train=True,表示为训练数据,样本数为50000。train=False,表示为测试数据,样本数为10000。
- 参数download=True,表示下载数据集,当已经准备好数据集时,可将download设置为False。
- 参数transform,将上文定义的数据处理功能应用在数据集上。
- 参数batch_size规定一个处理批次的大小,因为计算机如果将上万个数据一次性进行处理,将占用极大内存。
- 参数shuffle,设置为True时,表示将数据集顺序打乱,在训练集设置为True,测试集上没必要打乱顺序。
- 参数num_workers,Windows上一定要设置为0。
对模型进行训练
1、卷积神经网络实例化
# 建立GPU设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
from model import LeNet
net = LeNet()
# 模型放置到GPU
net.to(device)
2、使用交叉熵损失函数和SGD优化器
import torch.nn as nn
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
3、使用TensorBoard绘制epoch折线图
from torch.utils.tensorboard import SummaryWriter
# logs保存训练10个epoch的日志
writer = SummaryWriter('logs/logs')
4、迭代训练与测试
# 开始训练标志
print('Start Training')
# 计算loss的和,为了计算平均loss值
training_loss = 0.0
testing_loss = 0.0
for epoch in range(10):
for data in trainloader:
# 将数据放在GPU上
inputs, labels = data[0].to(device), data[1].to(device)
# 清空梯度信息
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
# 反向传播,计算梯度
loss.backward()
# 更新一步参数
optimizer.step()
# 计算这个epoch的loss值得和
training_loss += loss.item()
# torch.no_grad()为PyTorch的上下文管理器,
# 测试将不会对梯度监控,节省内存空间
with torch.no_grad():
for data in testloader:
inputs, labels = data[0].to(device), data[1].to(device)
outputs = net(inputs)
loss = criterion(outputs, labels)
testing_loss += loss.item()
# 使用TensorBoard绘制折现图,对比训练集和测试机上的损失loss
writer.add_scalars('Train-Test Loss',
{'Train Loss': training_loss / len(trainloader),
'Test Loss': testing_loss / len(testloader)}, epoch)
# 每个epoch都把loss清零
testing_loss = 0.0
training_loss = 0.0
# 每完成一个epoch打印一次,可掌握训练进度
print("{} finished".format(epoch))
# 关闭TensorBoard
writer.close()
# 训练完成标志
print('Finished Training')
5、保存训练好的模型
PATH = './cifar_net_10.pth'
torch.save(net.state_dict(), PATH)
完成了这五步,接下来检测下我们的可视化结果:
我们可以观察到,在第4次迭代epoch之前,测试集loss值反而低于训练集。训练集loss始终在下降,而测试机loss在第8次loss值达到最低,之后呈现上升趋势,说明该模型出现了一定的过拟合现象。
测试模型
我从网上找了四张图片,分别为狗,马,船,卡车。图片存放于test_images文件夹中。
展示下这四张图片:
提示:读者可以自己在网上随意下载图片,不过下载的图片要在CIFAR10数据集包含的10个类别之中。
1、加载预先训练好的模型
from model import LeNet
import torch
# 实例化模型
net = LeNet()
PATH = 'cifar_net_10.pth'
# 将训练好的参数导入
net.load_state_dict(torch.load(PATH))
2、数据处理模块
import torchvision.transforms as transforms
'''
我们设计的模型输入为32*32,所以在测试时,仍应该输入32*32大小的图片。
'''
transform = transforms.Compose(
[transforms.Resize((32,32)),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))])
- transforms.Resize:将图像转化为32*32的像素值;
- transforms.ToTensor:将数据转化为tensor数据类型,同时调整维度顺序,同前文描述;
- transforms.Normalize:同前文描述。
from PIL import Image
# 将图片加载为PIL图片形式,分别存放为img1,img2,img3,img4
img0,img1,img2,img3 = map(Image.open,
('test_images/dog.jpg','test_images/horse.jpg',
'test_images/ship.jpg','test_images/truck.jpg'))
img0,img1,img2,img3 = map(transform, (img0,img1,img2,img3))
四张图片合成为一个批次:
# 加入表示批次大小的维度
img0 = torch.unsqueeze(img0, dim=0)
img1 = torch.unsqueeze(img1, dim=0)
img2 = torch.unsqueeze(img2, dim=0)
img3 = torch.unsqueeze(img3, dim=0)
# 四张图片合成一个批次
images = torch.cat((img0, img1, img2, img3),dim=0)
3、测试
with torch.no_grad():
outputs = net(images)
# pred为最大值的索引值
_, pred = torch.max(outputs,dim=1)
pred = pred.numpy()
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
for i in range(4):
print(classes[pred[i]])
Out:
cat
horse
ship
truck
太棒了!模型准确的预测出了在网上随便下载的图片。
如果您觉得这篇文章对你有帮助,记得 点赞 + 关注 + 评论 三连,您只需动一动手指,将会鼓励我创作出更好的文章,快留下你的足迹吧💪💪💪