首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >明月深度学习实践001:LeNet网络入门学习

明月深度学习实践001:LeNet网络入门学习

作者头像
明月AI
发布2021-10-28 11:15:37
发布2021-10-28 11:15:37
4330
举报
文章被收录于专栏:野生AI架构师野生AI架构师

0x01 缘起


之前进行深度学习建模时,基本就是套模型,微调参数,基本也能解决问题。不过最近进行OCR识别,大模型效果其实已经不错了,但是还是有些比较明显的场景下却是没有识别到,而大模型本身也比较笨重,基于上面去调可能效果未必好,于是想建立一个简单的模型来进行识别,因为那些识别不到的不少是一些单独的在单元格里的数字。

于是萌生了开辟一个深度学习实践的系列,这是开篇。对于深度学习,网络结构很多,我们就从最简单的一个开始。

我所使用的环境,python 3.6.9,pytorch版本:

代码语言:javascript
复制
torch                             1.5.0+cu101
torchtext                         0.6.0
torchvision                       0.6.0+cu101

所有实验基于notebook进行,该系列可能需要一点深度学习的基础。

0x02 关于LeNet


LeNet神经网络由深度学习三巨头之一的Yan LeCun提出,他同时也是卷积神经网络 (CNN,Convolutional Neural Networks)之父。LeNet主要用来进行手写字符的识别与分类,并在美国的银行中投入了使用。LeNet的实现确立了CNN的结构,现在神经网络中的许多内容在LeNet的网络结构中都能看到,例如卷积层,Pooling层,ReLU层。

其他关于该网络的介绍非常多,一搜一大堆,有兴趣可以自己去找。

0x03 加载数据


Pytorch已经为我们准备了多个学习用的数据集,手写体识别数据集就是其中之一,加载数据集:

代码语言:javascript
复制
import torch
import torchvision as tv
import torchvision.transforms as transforms

# 超参数设置
BATCH_SIZE = 64     # 批处理尺寸(batch_size)
# 定义数据预处理方式
transform = transforms.ToTensor()

# 定义训练数据集
trainset = tv.datasets.MNIST(
    root='./data/',
    train=True,
    download=True,
    transform=transform)
    
# 定义训练批处理数据
trainloader = torch.utils.data.DataLoader(
    trainset,
    batch_size=BATCH_SIZE,
    shuffle=True)
    
# 定义测试数据集
testset = tv.datasets.MNIST(
    root='./data/',
    train=False,
    download=True,
    transform=transform)
    
# 定义测试批处理数据
testloader = torch.utils.data.DataLoader(
    testset,
    batch_size=BATCH_SIZE,
    shuffle=False)

上面代码就是将MNIST数据集加载进来,生成训练集和测试集,不准备对其详细说明。下面我们了解一下数据的基本情况:

数据量及图像size:

训练集有60000个图像,测试集有10000个图像,每个图像是一个28*28的灰度图。每个图像大概长下面的样子:

每个图像都标注了相应的标签:

0x04 定义LeNet网络结构


关于网络结构的说明有很多,这里直接给出代码:

代码语言:javascript
复制
import torch
import torch.nn as nn

class LeNet(nn.Module):
    """定义网络结构"""

    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Sequential(     # input: 28*28*1
            # filter 大小 5×5,filter 深度(个数)为 6,padding为2,卷积步长s=1
            # 输出矩阵大小为 28×28×6
            nn.Conv2d(1, 6, 5, 1, 2),
            nn.ReLU(),
            # filter 大小 2×2(即 f=2),步长 s=2,no padding
            # 输出:14*14*6
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        self.conv2 = nn.Sequential(
            # filter 大小 5×5,filter 个数为 16,padding 为 0, 卷积步长 s=1
            # 输出矩阵大小为 10×10×16
            nn.Conv2d(6, 16, 5),
            nn.ReLU(),
            # 输出:5*5*16
            nn.MaxPool2d(2, 2)
        )
        # 全连接层
        self.fc1 = nn.Sequential(
            nn.Linear(16 * 5 * 5, 120),
            nn.ReLU()
        )
        # 全连接层
        self.fc2 = nn.Sequential(
            nn.Linear(120, 84),
            nn.ReLU()
        )
        # 全连接层,输出神经元数量为 10,代表 0~9 十个数字类别
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        """定义前向传播过程,输入为x"""
        x = self.conv1(x)
        x = self.conv2(x)
        # nn.Linear()的输入输出都是维度为一的值,所以要把多维度的tensor展平成一维
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

看上面代码,共有5个网络层(分组之后的):

第一层:卷积层conv1

第一层包含Conv2d,ReLu和MaxPool2d三个层:

Conv2d:

  • 这是一个卷积层。
  • 我们已经知道实际输入的图像是28*28的灰度图,Conv2d(1, 6, 5, 1, 2)这个定义了我们的输入是1通道(灰度图),输出是6通道,记得最早接触卷积时,好难理解输入1通道怎么就变成了6通道的,其实就是使用了6个过滤器(filter),每个过滤器的size是5*5,可以这样理解:对于图像中的每个5*5的区域分别和每个过滤器进行运算,分别输出一个通道的特征图,这样就形成了6通道的输出。图像增加了2个像素的边界padding,这样原来输入的28*28就变成了32*32,filter每次移动的步长是1。
  • 综合这些信息,我们能够计算到输出的size是:28*28*6,这里的28是这样计算出来的:32-(5-1)。自己在图上画一画就清楚了。
  • 这个Conv2d层的参数量是:(5*5+1)*6 = 156。这里5*5是卷积核,卷积计算实际是矩阵运算:Y = A*X + B,矩阵A对应的就是卷积核,而矩阵B是5*1的矩阵。

ReLu:

  • 这是一个常见的激活函数,小于0的值都处理成0进行输出,特征图的size不会改变。

MaxPool2d:

  • 这是一个池化层,最大池化。
  • 核的size是2*2,即每个2*2的区域的生成一个输出(4个值中最大的一个值),步长为2,每次移动2个格子,这样特征图的size就减半了,变成了:14*14*6。

第二层:卷积层conv2

这一层跟前面一层类似,不详述,重点是要计算清楚,这层最后的输出特征图是:5*5*16。输入的是6通道,输出的是16通道。这层的理解和前一层的理解差不多,不过,这里的参数量计算却是有点复杂,网上很多文章基本都是摘自大神的论文:

行是6个通道,列是16个通道,参数计算:

6*(3*5*5+1)+6*(4*5*5+1)+3*(4*5*5+1)+1*(6*5*5+1)=1516

第一部分是输入相邻的3个通道组合输出6个通道,第二部分是输入相邻4个通道组合输出6个通道,第三部分是输入两两不相邻的4个通道组合输出3个 通道,第4部分是输入的6个通道组合输出1个通道。

(下面这段有很大的猜测成分,待验证)

原始大神的网络,参数的计算应该是这样的,大神可能是精心设计了上面的通道组合方式,因为没有看过原始的时候,不好判断。不过,对于我们的上面的实现,参数量应该不是这样的计算方式。这里一个卷积核的参数量应该是:5*5*6,这里6是输入的通道数,所以这里卷积层总的参数量应该是:

(5*5*6+1)*16 = 2416

第三层:全连接层fc1

全连接层理解上比较简单,顾名思义就是输入和输出的任意神经元之间都有连接。输入的神经元数量就是上一层的输出5*5*16 = 800,输出定义的是120,这层的参数量就比较多,计算也容易:(800+1)*120

第四层:全连接层fc2

输入120,输出84.

第五层:全连接层fc3

输入84,输出10。这是最后的输出层,输出的10个神经元对应我们的10个分类。

0x05 模型训练


代码:

代码语言:javascript
复制
import torch.optim as optim

EPOCH = 10        # 遍历数据集次数
LR = 0.001        # 学习率

# 定义是否使用GPU
print("cuda" if torch.cuda.is_available() else "cpu")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 定义损失函数loss function 和优化方式(采用SGD)
net = LeNet().to(device)
criterion = nn.CrossEntropyLoss()  # 交叉熵损失函数,通常用于多分类问题上
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)

for epoch in range(EPOCH):
    sum_loss = 0.0
    # 数据读取
    for i, data in enumerate(trainloader):
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)

        # 梯度清零
        optimizer.zero_grad()

        # forward + backward
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # 每训练100个batch打印一次平均loss
        sum_loss += loss.item()
        if i % 100 == 99:
            print('[%d, %d] loss: %.03f' % (epoch + 1, i + 1, sum_loss / 100))
            sum_loss = 0.0

    # 每跑完一次epoch测试一下准确率
    with torch.no_grad():
        correct = 0
        total = 0
        for data in testloader:
            images, labels = data
            images, labels = images.to(device), labels.to(device)
            outputs = net(images)
            # 取得分最高的那个类
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum()
        print('第%d个epoch的识别准确率为:%.2f%%,显存占用:%dm' % (epoch + 1, (100 * int(correct) / total),
                                                   torch.cuda.max_memory_allocated()//(1024*1024)))

    # 保存模型
    torch.save(net.state_dict(), 'models/lenet/lenet_%03d.pth' % epoch)
    
# 释放显存占用
torch.cuda.empty_cache()

模型训练无非就是前向计算输出,计算损失,然后反向计算梯度,更新权重,这样不断迭代优化网络,最后loss降到一定程度,计算识别准确率即可。

训练输出如下:

代码语言:javascript
复制
......
[9, 800] loss: 0.078
[9, 900] loss: 0.086
第9个epoch的识别准确率为:97.98%,显存占用:5m
[10, 100] loss: 0.079
[10, 200] loss: 0.073
[10, 300] loss: 0.071
[10, 400] loss: 0.080
[10, 500] loss: 0.080
[10, 600] loss: 0.071
[10, 700] loss: 0.082
[10, 800] loss: 0.079
[10, 900] loss: 0.084
第10个epoch的识别准确率为:97.90%,显存占用:5m

训练10个epoch,准确率就接近98%了,识别效果可是很不错了。

后记:

关于LeNet,后续还得补一篇才能完整。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 野生AI架构师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档