1. 前言
pytorch提供了内置的手写字母数据集,我们可以通过pytorch完整完成一个基于cnn的手写字母集的识别项目。
考虑到很多人并没有gpu,但对于这个项目,在cpu上也是完全能跑动的,所以本文是完全不涉及gpu版本,专门为cpu版本设计。
2. 数据集介绍
EMNIST数据集一共包含6个不同的数据类别,我们只使用其中的英文字母手写体,只需要选取emnist-letters。
3. 引用模块
from torch import nn,optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets,transforms
import matplotlib.pyplot as plt
import torch
import CNN #CNN是一个自定义的模块
4. CNN模块
4.1 CNN介绍
CNN是一个被广泛用于计算机视觉领域的模型,在图像识别等工作中表现出了强大的能力。
CNN的主要构成为卷积层(用于提取特征),池化层(用于强化提取得到的特征,并且可以简化运算),线性层(用于将图像特征进行训练实现目的)。
(本文不对CNN的工作原理做过多解释,需要的可以参考:卷积神经网络(CNN)详细介绍及其原理详解-CSDN博客
4.2 定义我们的CNN
创建一个CNN.py文件用于定义我们的CNN。
在设计CNN时一般会根据数据集的大小和种类设计不一样大小和数量的组成层,由于EMNIST的识别任务较为简单,并且数据较小(每张图片都是28*28的灰度图),所以会在实践过程中发现随便设计CNN都会得到较好的结果。
4.3 CNN实现
import torch.nn as nn
class CNN(nn.Module):
def __init__(self,out):
super(CNN,self).__init__()
self.out=out
self.layer1=nn.Sequential(
nn.Conv2d(1,8,kernel_size=3),
nn.BatchNorm2d(8),
nn.ReLU(inplace=True)
)
self.layer2=nn.Sequential(
nn.Conv2d(8,16,kernel_size=3),
nn.BatchNorm2d(16),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2,stride=2)
)
self.layer3=nn.Sequential(
nn.Conv2d(16,32,kernel_size=3),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True)
)
self.layer4=nn.Sequential(
nn.Conv2d(32,64,kernel_size=3),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2,stride=2)
)
self.fc=nn.Sequential(
nn.Linear(64*4*4,2048),
nn.ReLU(inplace=True),
nn.Linear(2048,512),
nn.ReLU(inplace=True),
nn.Linear(512,256),
nn.ReLU(inplace=True),
nn.Linear(256,128),
nn.ReLU(inplace=True),
nn.Linear(128,self.out)
)
def forward(self,x):
x = x.view(x.size(0), 3, 32, 32)
x=self.layer1(x)
x=self.layer2(x)
x=self.layer3(x)
x=self.layer4(x)
x=x.view(x.size(0),-1)
x=self.fc(x)
return x
4.4 相关解释
nn.Conv2d :输入通道数,输出通道数,卷积核大小
nn.MaxPool2d:池化层,每个池化块取最大,kernel_size:池化块大小,stride:步长
nn.ReLU:激活函数,其公式为:
当然,你可以尝试使用其他的激活函数。
5. 数据集实现部分
#标准化数据
data_tf=transforms.Compose(
[transforms.ToTensor(),transforms.Normalize([0.5],[0.5])]
)
batch_size=64
#加载训练集
train_dataset=datasets.EMNIST(root='./data/EMNIST',split='letters',train=True,transform=data_tf,download=True)
test_dataset=datasets.EMNIST(root="./data/EMNIST",split='letters',train=False,transform=data_tf)
train_loader=DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
test_loader=DataLoader(test_dataset,batch_size=batch_size,shuffle=False)
transforms.Compose是一个transforms容器,用于存放对数据标准化操作的代码。
datasets.EMNIST是pytorch的一个内置方法,用于加载EMNIST数据集,root存放数据集所在目录,split定义数据集分割方法(EMNIST数据集包含了手写数字和字母,split可以根据我们的需要分割出需要的数据集),train表示该数据集是否用于训练,transform表示数据处理方式,这里取我们前面实现的data_tf,download表示是否需要下载该数据集,True时如果在该目录下存在该数据集则不下载,如果不存在则下载。
6. 设置神经网络,定义损失函数和优化方法
CNNmodel=CNN.CNN(out=52)
learning_rate = 1e-3
criterion=nn.CrossEntropyLoss()
optimizer=optim.SGD(CNNmodel.parameters(),lr=learning_rate)
实现我们存放在CNN模块中的CNN网络,由于我们的任务是识别字母,一共有52个大小写字母,所以CNN的输出值为52。
损失函数我们选取交叉熵损失,其公式为:
优化时我设置的学习率是1e-3,当然,你可以根据情况进行修改,同样的,由于前面提到得的原因,在一个合理范围内我们发现效果都不错。
7. 训练网络
现在准备工作都已经完成了,可以开始训练我们的网络
num_epoches = 10
def train():
for epoch in range(num_epoches):
for images,labels in train_loader:
out=CNNmodel(images)
loss=criterion(out,labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch[{epoch+1}/{num_epoches}],loss:{loss.item():.6f}')
train()
我们设计的迭代次数为10,迭代次数越多,所需要的训练时间就越多,同样的,你可以根据你的实际情况进行调整。
8. 测试网络
训练完网络之后我们还需要测试训练之后的模型的性能,这时候就需要测试网络。
def test():
CNNmodel.eval()
eval_loss=0
eval_acc=0
for data in test_loader:
img,label=data
img=img.view(img.size(0),-1)
img = Variable(img)
label = Variable(label)
out=CNNmodel(img)
loss=criterion(out,label)
eval_loss+=loss.item()*label.size(0)
_,pred=torch.max(out,1)
num_correct=(pred==label).sum()
eval_acc+=num_correct.item()
print(f'Text Loss:{eval_loss/(len(test_dataset)):.6f},Acc:{eval_acc/(len(test_dataset)):.6f}')
test()
测试部分和训练部分相似,我们把测试集里的数据输入训练好的网络里面,得到一组输出,通过取其max可以得到预测的结果,然后对比预测结果和实际结果,通过一定量的对比计算出我们直观的准确率。
模型运行结果:
到这里,我们的模型的主体就已经完成了,但是我们可以添加一些工具使得我们的模型更加的直观和方便。
9. 数据可视化
通过plt模块我们可以引入一些实际的可视的测试案例来直观看到模型的战斗力。
def show_predict():
loader = DataLoader(dataset=test_dataset, batch_size=1, shuffle=True)
plt.figure(figsize=(8, 8))
for i, (images, labels) in enumerate(loader, 1):
if i > 9:
break
images = images.unsqueeze(0)
outputs = CNNmodel(images)
_, predicted = torch.max(outputs.data, 1)
title = f"预测结果: {chr(predicted[0]+64)}, 真实结果: {chr(labels[0]+64)}"
plt.subplot(3, 3, i)
plt.imshow(images[0][0].squeeze(), cmap="gray")
plt.title(title,fontproperties='SimSun')
plt.xticks([])
plt.yticks([])
plt.show()
show_predict()
这是可视化的结果:
10. 保存模型
我们每次训练完模型之后,随着程序的关闭,我们训练的参数也会消失,这显然不符合我们的需要,这时我们可以保存我们训练好的参数。
torch.save()方法可以保存我们训练好的参数。
torch.save(CNNmodel.state_dict(),
f"./model/EMNIST_cnn_Epoch{num_epoches}_Accuracy{acc*100:.3f}%.pth")
将这行代码添加至我们的测试部分,保存地址可以自行定义,这个文件名可以方便知道这组参数是通过几次迭代训练出来的,以及准确率的大小。
pth格式是pytorch里用于保存模型参数的格式。
11. 训练优化设计
我们在训练过程中很容易注意到,loss值并不是随着训练的过程一直减少的,经常会出现波动的现象。一般来说loss越小,其效果就会越好。当然,在训练集上的loss和在测试集上的loss并不是一样的,甚至可能出现过拟合的现象(具体表现为训练集上的loss较小,但测试集上的loss却并不小)。
所以假如我们迭代次数为10,最好的那组参数(在训练集上或是在测试集上,根据实际需要),可能不是在第10次迭代完得到的,有可能在第8次,或者第9次就得到了这10次中最好的结果。那么我们只测试和保存第10次的参数就反而会事倍功半。
于是,我们在设计模型时,将test放在train里面,每训练一次就评估一次和保存一次(当然,你可以选择只保留准确率大于一个你设定的值的参数,这很容易实现,我选择的是全部保存)。
num_epoches = 10
def test():
CNNmodel.eval()
eval_loss=0
eval_acc=0
for data in test_loader:
img,label=data
img=img.view(img.size(0),-1)
img = Variable(img)
label = Variable(label)
out=CNNmodel(img)
loss=criterion(out,label)
eval_loss+=loss.item()*label.size(0)
_,pred=torch.max(out,1)
num_correct=(pred==label).sum()
eval_acc+=num_correct.item()
print(f'Text Loss:{eval_loss/(len(test_dataset)):.6f},Acc:{eval_acc/(len(test_dataset)):.6f}')
torch.save(CNNmodel.state_dict(),
f"./model/EMNIST_cnn_Epoch{num_epoches}_Accuracy{acc*100:.3f}%.pth")
def train():
for epoch in range(num_epoches):
for images,labels in train_loader:
out=CNNmodel(images)
loss=criterion(out,labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch[{epoch+1}/{num_epoches}],loss:{loss.item():.6f}')
test()
train()
12. 结语
本项目是计算机视觉领域一个相对简单的项目,算力需求小,训练时间也不长,自己调着参数试着玩也不需要很多时间,是一个很适合新手实践的项目。
与各位创作者共勉。