一、LeNet介绍

LeNet-5,是早期CNN中比较经典的一个模型。它是Yann LeCun在1998年设计的用于手写数字识别的卷积神经网络,当时很多美国银行用它来识别支票上的手写数字。下图是LeNet5的模型示意图。

LeNet5 卷积神经网络 lenet5卷积神经网络pytorch_LeNet5 卷积神经网络

LeNet5由7层组成(不包括输入层),包括2个卷积层,2个池化层,3个全连接层。每层都包含不同的训练参数。而输入层是32*32像素的灰度图。

第一层:C1卷积层

这层使用6个5*5卷积核,得到6个28*28(卷积不进行padding填充)的特征图。由于卷积有一个偏置参数,该层参数是(5*5+1)*6=156,该层连接数是156*28*28=122304

第二层:S2池化层

采用池化对图像进行数据压缩处理同时保留有用的特征信息。对C1层6个28*28的特征图分别进行2*2的池化,得到6个14*14的图。

池化方式是4个输入相加,乘以一个可训练参数,再加一个可训练偏置,最后取sigmoid(激活函数)。因此该层参数是2*6=12个,连接数是(2*2+1)*6*14*14=5880

第三层:C3卷积层

这层使用16个5*5卷积核,得到16个10*10的特征图。这层有个特点是它和S2并不是全连接而是部分连接。该层有(5*5*3+1)*6 + (5*5*4+1)*3+(5*5*4+1)*6 + (5*5*6+1)*1 = 1516个参数,共1516*10*10个连接数。

LeNet5 卷积神经网络 lenet5卷积神经网络pytorch_全连接_02

第四层:S4池化层

与S2类似,对16个10*10的图进行2*2池化,得到16个5*5的图。16*2=32个参数,5*5*5*16=2000个连接数

第五层:C5卷积层(全连接层)

这层使用120个5*5卷积核,得到120个1*1特征图。由于S4层16个图大小为5*5,与卷积核大小相同,所以卷积后形成的图大小为1*1。所以这层其实是卷积层,但由于图像大小刚好成了全连接。

该层(5*5*16+1)*120=48120个参数,连接数为48120*1*1=48120

第六层:F6全连接层

这层有84个节点,对应一个7*12的比特图。得到84个1*1特征图。该层训练参数和连接数都是(120+1)*84=10164

LeNet5 卷积神经网络 lenet5卷积神经网络pytorch_卷积_03

第七层:Output输出层

output也是全连接层,共10个节点,分别代表数字0到9。如果第i个节点值为0,则表示网络识别结果是数字i。

 

 

LeNet5卷积神经网络共约60840个训练参数,340908个连接。一个数字识别过程如下图。

LeNet5 卷积神经网络 lenet5卷积神经网络pytorch_池化_04

二、Pytorch实现LeNet

本次实现的环境为,Python 3.9.10,Pytorch 1.10.2。

主要内容是LeNet网络结构,训练和预测。

lenet.py

import torch
import torch.nn as nn
import torch.nn.functional as F


class LeNet(nn.Module):
    # LeNet初始化函数,神经网络基本结构
    def __init__(self):
        # 继承nn.Module的__init__
        super(LeNet, self).__init__()
        # 卷积层1,输入1通道(初始输入32*32灰度图),输出6个特征图,卷积核为5*5
        self.conv1 = nn.Conv2d(3, 6, (5, 5))
        # 卷积层2,输入6通道(6特征图),输出16个特征图,卷积核5*5
        self.conv2 = nn.Conv2d(6, 16, (5, 5))
        # 池化
        self.pool = nn.MaxPool2d(2, 2)
        # 三个全连接层,输入必须是一维向量,利用线性函数y = Wx + b,从400压缩到10.
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    # 前向传播函数,继承nn.Module必须定义的方法。一旦定义,反向传播函数会自动生成
    def forward(self, x):
        # 输入x经过conv1卷积并激活后,用2*2窗口进行最大池化,并更新到x
        x = self.pool(F.relu(self.conv1(x)))
        # 输入x经过conv2卷积并激活后,用2*2窗口进行最大池化,并更新到x
        x = self.pool(F.relu(self.conv2(x)))
        # view将x一维化,为全连接做准备
        x = torch.flatten(x, 1)
        # 经过三次全连接和两次激活。最后一步不需要激活函数,因为softmax激活函数被嵌入在交叉熵函数中
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        # 更新并返回最后的值
        return x

 

train.py

import torch
import torchvision
import torch.nn as nn
import torchvision.transforms as transforms
from lenet import LeNet
import torch.optim as optim
import torch.utils.data as data

# 相当于图像预处理
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
])

batch_size = 50
num_workers = 2
# 训练数据,使用CIFAR10数据集的50000条数据
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, transform=transform, download=False)
trainloader = data.DataLoader(trainset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
# 验证数据
valset = torchvision.datasets.CIFAR10(root='./data', train=False, transform=transform, download=False)
valloader = data.DataLoader(valset, batch_size=10000, shuffle=False, num_workers=num_workers)
# 判断有GPU时使用GPU训练
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# 多线程和GPU只有在__name__ == "__main__"的情况下才能使用
if __name__ == "__main__":
    # 验证数据
    val_img = None
    val_label = None
    for data in valloader:
        val_img, val_label = data[0].to(device), data[1].to(device)

    # LeNet实例并使用GPU加速
    net = LeNet()
    net.to(device)
    # 损失函数
    loss_function = nn.CrossEntropyLoss()
    # 优化器,即梯度下降方式。lr为学习率
    optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

    # 50个世代的训练
    for epoch in range(50):
        running_loss = 0  # 累加损失
        for step, data in enumerate(trainloader, 0):
            # 获取输入
            inputs, labels = data[0].to(device), data[1].to(device)

            # 梯度归零,清除历史梯度
            optimizer.zero_grad()
            # 前向传播
            outputs = net(inputs)
            # 损失计算和反向传播
            loss = loss_function(outputs, labels)
            loss.backward()
            # 梯度下降
            optimizer.step()
            # 累加损失
            running_loss += loss.item()
            
            # 根据数据集和batch_size确定。对于本次50000张图片的数据集,batch_size为50时需要1000个step。每隔100个step进行一次信息输出
            if step % 100 == 99:
                # 验证数据准确率
                outputs = net(val_img)
                predict_y = torch.max(outputs, dim=1)[1]
                accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)
                # 世代,进度,损失和准确率
                print(
                    f'Epoch: {epoch + 1}, progress: {step / 10}%, loss: {running_loss / 100:.3f}, accuracy: {accuracy * 100:.2f}%')
                running_loss = 0.0

    print("Training Finished!")

    # 存储当前网络计算的权重
    save_path = "./LeNet.pth"
    torch.save(net.state_dict(), save_path)

 

predict.py

import torch
import torchvision.transforms as transforms
from PIL import Image
from lenet import LeNet

# 预处理内容
transforms = transforms.Compose([
    transforms.ToTensor(),
    transforms.Resize((32, 32)),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# 类别
classes = ('airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# LeNet实例并加载已训练权重
net = LeNet()
net.load_state_dict(torch.load('LeNet.pth', map_location="cuda"))

# 输入图像并处理
img = Image.open("test.jpg")
img = transforms(img)
img = torch.unsqueeze(img, dim=0)  # 扩展维度,因为net需要的输入还包含batch维度

with torch.no_grad():
    outputs = net(img)
    predict = torch.max(outputs, dim=1)[1].data.numpy()

# 预测结果
print(classes[int(predict)])