小项目:手写数字识别,可识别小数点(三)
完整代码已经上传 GitHub(https://github.com/wwwwkd/Digit-Recognition)有帮助的话给个小星星!!!
代码是自己写的,有些不严谨,有更好的方法或者思路,希望大家之间相互指点相互进步。首先提供思路,然后在提供部分代码,然后在展示效果图。
功能要求:可以根据自己手工书写一个数字得带小数,拍照后,程序能将该手写数字转换成对应的数字。
经过查阅资料,将其大致分为一下三个部分:
① 数字的定位、分割、保存.
② 小数点的识别.
③ 网络的训练、测试和最佳模型参数保存加载.
网络的训练、测试和最佳模型参数保存加载
LeNet5的网络结构示意图如图3所示:
该网络分为三部分,第一部分,输入图像;第二部,分特征提取;第三部分分类识别。
本次使用的数据集也是经典的Mnist数据集,通过引入视觉工具包torchvision其中的torchvision.datasets方法中含有多个经典数据集,其中就包含Mnist。在加载数据集(torch.utils.data.DataLoader())时选择打乱数据,也是数据增强的一个方法。
然后就是继承nn.Module类进行网络的编写,对于网络部分,复现LeNet5网络。网络的训练集loss曲线如图4所示
最终识别效果如图所示
下面仅展示部分代码只含有数字定位并不包含小数点识别,小数点的识别见下一篇,完整代码见 GitHub
https://github.com/wwwwkd/Digit-Recognition
网络部分代码如下:
import torch
from torch import nn
from torch.nn import functional as F
from utils import Flatten
class LeNet5(nn.Module):
'''
1.Conv1 Block
2.Conv2 Block
3.Flatten layer1
4.Flatten layer2
----参数设置------
使用优化器adam:其中的 weight_decay = 0.01
学习率learning rate:lr = 1e-4
激活函数active function:relu
'''
def __init__(self):
super(LeNet5, self).__init__() # 初始化函数并实现
# -----Conv1 Block-----
self.conv1_block = nn.Sequential(
nn.Conv2d(1, 10, kernel_size=5, stride=1),
nn.MaxPool2d(2)
)
# -----Conv2 Block-----
self.conv2_block = nn.Sequential(
nn.Conv2d(10, 20, kernel_size=5, stride=1),
nn.MaxPool2d(2)
)
# -----Flatten layer1-----
self.fc1 = nn.Sequential(
Flatten(),
nn.Linear(20*4*4, 120), # (b,225*1*37)=>(b,4)
)
# -----Flatten layer2-----
self.fc2 = nn.Sequential(
nn.Linear(120, 84), # (b,225*1*37)=>(b,4)
)
# -----Flatten layer3-----
self.fc3 = nn.Sequential(
Flatten(),
nn.Linear(84, 10), # (b,225*1*37)=>(b,4)
)
def forward(self, x): # 前向传播
# [b, 1, 28, 28] => [b, 10, 12, 12]
x1 = F.relu(self.conv1_block(x))
#print('Conv1 Block', x1.shape)
# [b, 10, 12, 12] => [b, 20, 4, 4]
x2 = F.relu(self.conv2_block(x1))
#print('Conv2 Block', x2.shape)
#[b, 120*4*4] => [b, 120]
x3 = self.fc1(x2)
#print('flatetn layer1', x3.shape)
# [b, 120] => [b, 84]
x4 = self.fc2(x3)
#print('flatetn layer1', x4.shape)
# [b, 84] => [b, 10]
x5 = self.fc3(x4)
#print('flatetn layer1', x5.shape)
return x5
def main():
# test
net = LeNet5()
x = torch.randn(1, 1, 28, 28)
a = net(x)
print(a)
if __name__ == '__main__':
main()
训练部分代码
import torch
from torch import optim, nn
from torch.utils.data import DataLoader
from LeNet5 import LeNet5
from utils import plot_curve
from torchvision import datasets, transforms # 视觉工具包
batch_size=200 # 一次送入200张
learning_rate=0.01 # 学习率
epochs=40 # 最大迭代次数
device = torch.device('cuda') # 设备选择cuda
torch.manual_seed(1234) # 种子点
train_db = datasets.MNIST('../Digit Recognition/data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
]))
#文件名 train = true 代表下载的是那60k的训练数据 而不是剩下10k的test数据
#download = true 代表当前文件无数据则自动下载
#数据格式为numpy 将其转化成 tensor格式
#normalize是正则化的一个过程 由于图像的像素是0-1 只在0的右边 将其转换到0的两侧进行均匀分布 可以提高性能
#batch_size 代表一次加载多少张数据 shuffle 代表加载数据并打散
train_loader = torch.utils.data.DataLoader(
train_db,
batch_size=batch_size, shuffle=True)
test_db = datasets.MNIST('../Digit Recognition/data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])) # 加载测试集10k
test_loader = torch.utils.data.DataLoader(test_db,
batch_size=batch_size, shuffle=True)
print('train:', len(train_db), 'test:', len(test_db))
train_db, val_db = torch.utils.data.random_split(train_db, [50000, 10000]) # 将训练集在分为训练集50k和验证集10k
print('db1:', len(train_db), 'db2:', len(val_db))
train_loader = torch.utils.data.DataLoader(
train_db,
batch_size=batch_size, shuffle=True) #在进行数据打乱,数据增强
val_loader = torch.utils.data.DataLoader(
val_db,
batch_size=batch_size, shuffle=True)
def evalute(model, loader):
'''
测试函数:1.通过验证集评价网络最好得epoch
2.通过测试集评价网络真实准确率
注:均不用于反向传播调节网络,单纯评价
'''
model.eval()
correct = 0
total = len(loader.dataset)
for x,y in loader:
x,y = x.to(device), y.to(device) # 改变设备类型 在GPU上进行运算
with torch.no_grad():
logits = model(x)
pred = logits.argmax(dim=1)
correct += torch.eq(pred, y).sum().float().item()
# eq:pred与y进行比较相等的输出1 不同的输出0,然后sum累加,之后float转换成浮点数,最后item将tensor数据类型转换成numpy
return correct / total # 返回平均准确率
def main():
device = torch.device('cuda:0')
model = LeNet5().to(device) # 加载网络模型
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.01) # 设置优化器参数 参数选择详见网络注释
criteon = nn.CrossEntropyLoss().to(device) # 设置损失函数参数
best_acc, best_epoch = 0, 0
train_loss = []
for epoch in range(epochs): # 训练
for batch_idx, (data, target) in enumerate(train_loader):
# data: [b, 1, 28, 28], target: [b]
data, target = data.to(device), target.to(device)
model.train() # 训练模型
logits = model(data) # 获得网络输出
loss = criteon(logits, target) # 根据损失函数得到loss
train_loss.append(loss.item()) # loss是tensor数据类型 loss.item将其转换成具体数值,numpy类型
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播,更新权重等参数信息,计算梯度
optimizer.step() # 更新梯度,注:每一个step完成的是一个batchsize,每一个epoch完成的是一整个数据集
if batch_idx % 100 == 0: # 每100个batchsize,输出一次迭代代数,已经训了训练集中多少数据,所占百分比,对应此时得loss
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
if epoch % 1 == 0:
val_acc = evalute(model, val_loader) # 每迭代一次,用validation_set进行验证
print('Average_val_acc:', val_acc, 'epoch:', epoch)
if val_acc> best_acc: # 用验证集来测试网络表现最好的代数 方便进行参数保存
best_epoch = epoch
best_acc = val_acc
torch.save(model.state_dict(), 'best.mdl')
plot_curve(train_loss)
print('best acc:', best_acc, 'best epoch:', best_epoch)
model.load_state_dict(torch.load('best.mdl')) # 加载在validation_set上表现最好时网络参数
print('loaded from ckpt!')
test_acc = evalute(model, test_loader) # 没有训练过的新图像即测试集来体现网络得真是性能
print('test acc:', test_acc)
if __name__ == '__main__':
main()