问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

卷积神经网络入门:手写数字识别实战

创作时间:
作者:
@小白创作中心

卷积神经网络入门:手写数字识别实战

引用
CSDN
1.
https://m.blog.csdn.net/DevoteeQN/article/details/140870314

卷积神经网络(手写数字识别)

何为卷积

在理解卷积神经网络(CNN)之前,首先需要理解什么是卷积。卷积是一种数学运算,以图片为例,假设一张图片具有3个颜色通道,尺寸为4x4,那么在数学上可以将其表示为一个size = 3x4x4的张量。卷积运算是输入张量与卷积核之间的运算,例如设置一个3x3x3的卷积核,并约定步长为1。

卷积运算可以这样理解:对卷积核的每个通道,它将会分别和输入张量的对应通道做卷积,然后将所得的矩阵求和。对于单个通道与单个卷积核对应通道,具体的运算方法如图所示:

值得注意的是,卷积核的通道数必须和输入张量的通道数保持一致。但是卷积操作其实是可以改变输入张量的通道数的,可以通过设计多个卷积核来实现,一个卷积核经过卷积将会得到输出张量的一个通道,n个卷积核将会得到n个通道。

在卷积过程中还可以设置卷积的步长(stride)。如果不设置,默认步长是1,即对一个区域卷积后,卷积核会向后移动一个单位长度,如果已经到了最右端,它将会向下移动。如果设置stride=2,那么卷积核将会两个单位长度的移动。

在设计卷积层的过程中,常常会进行padding操作。仔细观察卷积的过程,会发现处在边缘地带的区块,被卷积核提取信息的次数明显会少于中间的区块。事实上,边缘区域也很有可能包含了关键的信息。因此,可以在输入张量每一层的边缘补充0,将例如原来3x3的矩阵扩展为4x4,甚至5x5,这样那些包含了信息的区域,就能够同等的被提取信息。

卷积神经网络的架构(简单的手写数字识别)

下面以一个简单的卷积神经网络(AlexNet)为例,谈谈对架构的理解:

如何分类

对于MNIST数据集而言,输入的是一张张灰度图,输出的是属于哪个类别,这是一个明显的分类问题。对于分类问题,通常有几个类别就输出一个几维向量,这个向量的每一个位置的元素表示该输入属于这一类别的概率。例如分类是不是猫,假如网络输出了(0.3,0.7)这样一个向量,如果我们约定零号位置是该输入是猫的概率,那么一号位置我们约定是不是猫的概率,这两个概率之和必须为一。更多类别时也是同理,我们一般取概率最大的位置对应的类别为分类结果。

什么是池化

池化(pooling)操作一般常用的分为两种,一种是最大值池化(max_pooling),一种是平均值池化(avg_pooling)。不管是哪种池化,都需要定义单次池化的范围大小,一般是对正方形区域进行池化。例如设置一个2x2的区域,如果使用最大值池化,那么这个区域里四个数值,只保留数值最大的;类似的平均值池化保留四个位置的平均值。进行池化后,矩阵变小,适当舍弃了一些作用不大的信息,有助于减少训练成本,同时又尽可能保证了训练的结果。事实上,大多数时候max_pooling能取得比avg_pooling更好的结果。

具体的操作流程

首先需要将输入的灰度图转换为可以处理的张量,然后利用卷积操作提取信息,之后对卷积后的结果进行一次池化,进一步精炼信息,然后进行非线性化,这便是一轮处理。同样的操作可以设置多轮,有助于网络更好的学习。最后需要设置一个全连接层,也就是一般的bp神经网络,输出一个十维的向量,放到分类器中,从而得到分类结果和相应的loss值,最后调用后向传播算法,经过多轮训练,使网络收敛。

具体的PyTorch实现(对MNIST的手写数字识别)

第一步:加载MNIST数据集

import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

batch_size = 64 # 设置批量大小
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
# 根据MNIST的均值和标准差对数据集做一些处理
train_dataset = datasets.MNIST('../data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST('../data', train=False, transform=transform, download=True)
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)

由于MNIST数据集在PyTorch中已经内置了,可以直接使用对应的模块加载。

第二步:定义网络的模块

class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5)
        self.pooling = torch.nn.MaxPool2d(kernel_size=2)
        self.fc1 = torch.nn.Linear(320, 50)  # 全连接层
        self.fc2 = torch.nn.Linear(50, 10)
        self.ReLU = torch.nn.ReLU()
    def forward(self, x):
        batch_size = x.size(0)
        x = self.ReLU(self.pooling(self.conv1(x)))  # 先卷积,再池化,再非线性化
        x = self.ReLU(self.pooling(self.conv2(x)))
        x = x.view(batch_size, -1)  # 展成一维向量,便于后续全连接层处理
        x = self.ReLU(self.fc1(x))
        x = self.fc2(x)  # 使用crossEntropyLoss时最后一层不能激活
        return x

值得注意的是,由于这是多分类任务,最后一层不能激活,因为后续的分类器crossEntropyLoss已经自带了激活功能。

第三步:实现网络、分类器、优化器

net = Net()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net.to(device)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.5)

如果没有GPU,可以去掉带###号的GPU的操作。

第四步:训练模块

def train(epoch):
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)  ### 计算的张量迁移到显卡
        y_pred = net(data)
        loss = criterion(y_pred, target)
        optimizer.zero_grad()
        loss.backward() # 反向传播
        optimizer.step()
        running_loss += loss.item() # 采用了MINI_BATCH 损失要累加起来
        if batch_idx % 300 == 299:
            print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
            running_loss = 0.0
            # 每三百次计算一次平均损失

这一步就是一般的训练流程:输入、前向传播、计算损失、反向传播、修改参数。

第五步:测试模块

def Test():
    correct = 0
    total = 0
    with torch.no_grad():  # 测试集无需计算梯度,只是做测试,不对网络进行优化
        for data, target in test_loader:
            data, target = data.to(device), target.to(device) ###
            output = net(data)
            _, predicted = torch.max(output.data, 1) # 找到预测值最大的类别
            total += target.size(0)
            correct += (predicted == target).sum().item()  # 统计预测正确的次数
    print('Accuracy of the network on the test images: %d %% [%d/%d]' % (100 * correct / total, correct, total))

这一步是在测试集上进行测试,统计正确率。

第六步:开始训练、测试

if __name__ == '__main__':
    for epoch in range(0, 10):
        train(epoch)
        Test()

在本地训练结果如下:

[1,   300] loss: 1.036
[1,   600] loss: 0.221
[1,   900] loss: 0.154
Accuracy of the network on the test images: 96 % [9635/10000]
[2,   300] loss: 0.114
[2,   600] loss: 0.102
[2,   900] loss: 0.091
Accuracy of the network on the test images: 97 % [9728/10000]
[3,   300] loss: 0.076
[3,   600] loss: 0.072
[3,   900] loss: 0.073
Accuracy of the network on the test images: 98 % [9825/10000]
[4,   300] loss: 0.062
[4,   600] loss: 0.063
[4,   900] loss: 0.057
Accuracy of the network on the test images: 98 % [9842/10000]
[5,   300] loss: 0.052
[5,   600] loss: 0.051
[5,   900] loss: 0.049
Accuracy of the network on the test images: 98 % [9862/10000]
[6,   300] loss: 0.046
[6,   600] loss: 0.045
[6,   900] loss: 0.044
Accuracy of the network on the test images: 98 % [9874/10000]
[7,   300] loss: 0.045
[7,   600] loss: 0.039
[7,   900] loss: 0.039
Accuracy of the network on the test images: 98 % [9874/10000]
[8,   300] loss: 0.037
[8,   600] loss: 0.036
[8,   900] loss: 0.038
Accuracy of the network on the test images: 98 % [9875/10000]
[9,   300] loss: 0.036
[9,   600] loss: 0.033
[9,   900] loss: 0.033
Accuracy of the network on the test images: 98 % [9863/10000]
[10,   300] loss: 0.031
[10,   600] loss: 0.030
[10,   900] loss: 0.032
Accuracy of the network on the test images: 98 % [9893/10000]

在MNIST数据集上取得了98%的正确率。

总结

通过本文,我们成功利用简单的卷积神经网络实现了手写数字识别。这只是最简单的卷积神经网络,后续还有GoogleNet、ResNet等更复杂的网络结构。这些网络结构将会在后续的文章中逐个介绍。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号