第五章 Python手写数字识别【CNN卷积神经网络实现】
刚看完鱼书,不得不说确实讲的通透,对我一个没系统学过深度学习的都能看得懂,里面详细说了神经网络的实现,手把手教的那种,而且也没有遇到需要解锁前置知识的情况
鱼书pdf分享链接(知乎好人)https://zhuanlan.zhihu.com/p/665578698
安装好虚拟环境(创建虚拟环境、安装好对应python版本、安装cuda版的Pytorch),建好项目,跟着视频开干,感谢大佬
【附:代码】手写数字识别python项目实战手把手教学_计算机视觉_人工智能_哔哩哔哩_bilibili
加载数据
包引用
import torch #pytorch库加载
import torchvision.datasets as dataset #数据集接口
import torchvision.transforms as transforms #数据转换
import torch.utils.data as data_utils #数据分批
下载数据
###############################数据加载###############################
train_data = dataset.MNIST( #训练集root = "mnist", #数据存放目录train = True, #加载训练数据transform = transforms.ToTensor(), #将数据转为张量格式download = True #如果数据集不存在,下载到本地
);test_data = dataset.MNIST( #测试集root = "mnist", #数据存放目录train = False, #加载训练数据transform = transforms.ToTensor(), #将数据转为张量格式download = True #如果数据集不存在,下载到本地
)print(train_data)
print(test_data)
执行
数据分批
###############################数据分批###############################
train_loader = data_utils.DataLoader(dataset=train_data, #训练集分批batch_size=64, #一批64shuffle=True) #打乱数据test_loader = data_utils.DataLoader(dataset=test_data, #测试集分批batch_size=64, #一批64shuffle=True) #打乱数据print(train_loader)
print(test_loader)# for index,value in enumerate(train_loader): #遍历训练集
# print(index)
# print(value)
# break #只打印一批for index,(images,labels) in enumerate(train_loader): #遍历训练集【分开图片与标签】print(index) #索引(批号)print(images) #图像print(labels) #真实标签break #只打印一批
执行
网络结构设计
创建model文件CNN
利用Pytorch库可以很容易构建起我们的model
import torchclass CNN(torch.nn.Module): #继承torch.nn.Module这个基类,我们就可以直接使用人家做好的函数和属性def __init__(self): #构造函数super(CNN,self).__init__() #继承自己的父类self.conv = torch.nn.Sequential( #Sequential是序列容器,用来实现前向传播#1、卷积操作:卷积层torch.nn.Conv2d(1, #通道数32, #卷积核数量(输出特征图的数量),即进行32次卷积操作,得到32张输出图kernel_size=5, #kernel_size=5在Conv2d中表示卷积核大小(5*5)padding=2, #填充(0填充)stride=1 #步幅),# 卷积后的输出大小计算:(H为图片的高、p为填充、FH为滤波器高、S为步幅)# OH = (H+2*p-FH)/S + 1 = (28+2*2-5)/1 + 1 = 28# OW = (W+2*p-FW)/S + 1 = (28+2*2-5)/1 + 1 = 28#2、归一化操作:BN层(Batch Normalization)torch.nn.BatchNorm2d(32), #初始化32个卷积核#3、激活函数:ReLUtorch.nn.ReLU(), #激活函数#4、最大池化:池化层torch.nn.MaxPool2d(2), #2表示池化大小为2*2# 池化后的输出大小计算:(每一块都是4选1) => 长宽减半# OH = H/2 = 14# OW = W/2 = 14)#全连接操作self.fc = torch.nn.Linear(in_features=14*14*32,out_features=10) #前面的参数表示输入的结点数(池化后)【输出的图大小为14*14,共32张】,后面的参数表示输出的结点数(0~9)#前向传播def forward(self,x): #x为输入的数据,输入形式为张量,tensor(样本数,通道数,高,宽)即(n,c,h,w)out = self.conv(x) #卷积out = out.view(out.size()[0],-1) #进行全连接前需要把图像数据转为一维的格式,out.size()[0]为样本数n,表示保持n个样本;-1表示剩下部分全部展为一维out = self.fc(out) #全连接fc层return out
实例化model
导入model类
在训练前声明,实例化
损失函数及优化函数
###############################损失函数###############################
loss_func = torch.nn.CrossEntropyLoss() #交叉熵损失函数###############################优化函数###############################
optimizer = torch.optim.Adam(cnn.parameters(), lr = 0.01) #使用Adam优化方法,传入cnn.parameters()即cnn所有的参数,学习率lr设置为0.01
模型训练
单次遍历训练的训练过程
for index,(images,labels) in enumerate(train_loader): #遍历训练集【分开图片与标签】,enumerate的意思是枚举# print(index) #索引(批号)# print(images) #图像# print(labels) #真实标签images = images.cuda() #将图片也放到GPU上加载labels = labels.cuda() #将标签也放到GPU上加载#前向传播outputs = cnn(images) #传入数据(实例),会自动调用前向传播forwardloss = loss_func(outputs, labels) #根据真实标签和模型输出计算损失lossoptimizer.zero_grad() #梯度清空#反向传播loss.backward() #根据损失函数loss进行反向传播optimizer.step() #优化参数#提示信息print("当前epoch = {},当前批次为{}/{},目前loss为{}".format(epoch+1, #当前epochindex+1, #index表示批次len(train_data)//64, #//64表示整除64,len(train_data)//64即是将完整数据集分了多少批loss.item()))
一轮epoch后,通过测试集评估效果
loss_test = 0 #测试中的总损失for(index2,(images,labels)) in enumerate(test_loader):images = images.cuda() #将图片也放到GPU上加载labels = labels.cuda() #将标签也放到GPU上加载outputs = cnn(images) #将测试数据传入模型中计算验证print(outputs)print(labels)loss_test += loss_func(outputs, labels)
查看模型训练后的测试效果
_, pred = torch.max(outputs,1) #torch.max即提取输出的outputs中第1维度数据中的最大值,_部分对应接收最大值(变量_我们默认无意义),pred接受最大值对应的索引print(pred)print(labels)print(pred == labels)print((pred == labels).sum().item()) #sum求和预测结果(正确为1,错误为0),pytorch中输入和输出都是张量,因此需要用到item()取到结果值
模型保存
import os
model_dir = "model"
model_path = os.path.join(model_dir, "mnist_model.pkl")# 检查并创建目录(如果不存在)
if not os.path.exists(model_dir):os.makedirs(model_dir) # 递归创建目录(即使父目录不存在)torch.save(cnn,model_path) #保存模型
epoch设置为10,验证:
总实现代码
CNN.py
import torchclass CNN(torch.nn.Module): #继承torch.nn.Module这个基类,我们就可以直接使用人家做好的函数和属性def __init__(self): #构造函数super(CNN,self).__init__() #继承自己的父类self.conv = torch.nn.Sequential( #Sequential是序列容器,用来实现前向传播#1、卷积操作:卷积层torch.nn.Conv2d(1, #通道数32, #卷积核数量(输出特征图的数量),即进行32次卷积操作,得到32张输出图kernel_size=5, #kernel_size=5在Conv2d中表示卷积核大小(5*5)padding=2, #填充(0填充)stride=1 #步幅),# 卷积后的输出大小计算:(H为图片的高、p为填充、FH为滤波器高、S为步幅)# OH = (H+2*p-FH)/S + 1 = (28+2*2-5)/1 + 1 = 28# OW = (W+2*p-FW)/S + 1 = (28+2*2-5)/1 + 1 = 28#2、归一化操作:BN层(Batch Normalization)torch.nn.BatchNorm2d(32), #初始化32个卷积核#3、激活函数:ReLUtorch.nn.ReLU(), #激活函数#4、最大池化:池化层torch.nn.MaxPool2d(2), #2表示池化大小为2*2# 池化后的输出大小计算:(每一块都是4选1) => 长宽减半# OH = H/2 = 14# OW = W/2 = 14)#全连接操作self.fc = torch.nn.Linear(in_features=14*14*32,out_features=10) #前面的参数表示输入的结点数(池化后)【输出的图大小为14*14,共32张】,后面的参数表示输出的结点数(0~9)#前向传播def forward(self,x): #x为输入的数据,输入形式为张量,tensor(样本数,通道数,高,宽)即(n,c,h,w)out = self.conv(x) #卷积out = out.view(out.size()[0],-1) #进行全连接前需要把图像数据转为一维的格式,out.size()[0]为样本数n,表示保持n个样本;-1表示剩下部分全部展为一维out = self.fc(out) #全连接fc层return out
训练主函数
import os
import torch #pytorch库加载
import torchvision.datasets as dataset #数据集接口
import torchvision.transforms as transforms #数据转换
import torch.utils.data as data_utils #数据分批from CNN import CNN###############################数据加载###############################
train_data = dataset.MNIST( #训练集root = "mnist", #数据存放目录train = True, #加载训练数据transform = transforms.ToTensor(), #将数据转为张量格式download = True #如果数据集不存在,下载到本地
);test_data = dataset.MNIST( #测试集root = "mnist", #数据存放目录train = False, #加载训练数据transform = transforms.ToTensor(), #将数据转为张量格式download = True #如果数据集不存在,下载到本地
)# print(train_data)
# print(test_data)###############################数据分批###############################
train_loader = data_utils.DataLoader(dataset=train_data, #训练集分批batch_size=64, #一批64shuffle=True) #打乱数据test_loader = data_utils.DataLoader(dataset=test_data, #测试集分批batch_size=64, #一批64shuffle=True) #打乱数据# print(train_loader)
# print(test_loader)# for index,value in enumerate(train_loader): #遍历训练集
# print(index)
# print(value)
# break #只打印一批# for index,(images,labels) in enumerate(train_loader): #遍历训练集【分开图片与标签】
# print(index) #索引(批号)
# print(images) #图像
# print(labels) #真实标签
# break #只打印一批###############################模型声明###############################
cnn = CNN() #声明实例# print(torch.cuda.is_available()) #确认cuda能用
cnn = cnn.cuda() #声明在cuda上运行【利用GPU运行】###############################损失函数###############################
loss_func = torch.nn.CrossEntropyLoss() #交叉熵损失函数###############################优化函数###############################
optimizer = torch.optim.Adam(cnn.parameters(), lr = 0.01) #使用Adam优化方法,传入cnn.parameters()即cnn所有的参数,学习率lr设置为0.01###############################模型训练###############################
for epoch in range(10): #epoch指将所有的数据全部训练一遍for index,(images,labels) in enumerate(train_loader): #遍历训练集【分开图片与标签】,enumerate的意思是枚举# print(index) #索引(批号)# print(images) #图像# print(labels) #真实标签images = images.cuda() #将图片也放到GPU上加载labels = labels.cuda() #将标签也放到GPU上加载#前向传播outputs = cnn(images) #传入数据(实例),会自动调用前向传播forwardloss = loss_func(outputs, labels) #根据真实标签和模型输出计算损失lossoptimizer.zero_grad() #梯度清空#反向传播loss.backward() #根据损失函数loss进行反向传播optimizer.step() #优化参数# #提示信息# print("当前epoch = {},当前批次为{}/{},目前loss为{}".format(epoch+1, #当前epoch# index+1, #index表示批次# len(train_data)//64, #//64表示整除64,len(train_data)//64即是将完整数据集分了多少批# loss.item()))###############################测试集验证###############################loss_test = 0 #测试中的总损失acc = 0for(index2,(images,labels)) in enumerate(test_loader):images = images.cuda() #将图片也放到GPU上加载labels = labels.cuda() #将标签也放到GPU上加载outputs = cnn(images) #将测试数据传入模型中计算验证# print(outputs)# print(labels)_, pred = torch.max(outputs,1) #torch.max即提取输出的outputs中第1维度数据中的最大值,_部分对应接收最大值(变量_我们默认无意义),pred接受最大值对应的索引# # print(pred)# # print(labels)# # print(pred == labels)## print((pred == labels).sum().item()) #sum求和预测结果(正确为1,错误为0),pytorch中输入和输出都是张量,因此需要用到item()取到结果值acc += (pred == labels).sum().item() #相加预测正确的数量loss_test += loss_func(outputs, labels) #叠加每一步损失print("当前epoch = {},目前loss为{},准确率ACC={}".format(epoch+1, #当前epochloss_test,acc / len(test_data)))model_dir = "model"
model_path = os.path.join(model_dir, "mnist_model.pkl")# 检查并创建目录(如果不存在)
if not os.path.exists(model_dir):os.makedirs(model_dir) # 递归创建目录(即使父目录不存在)torch.save(cnn,model_path) #保存模型
验证模型test.py
在虚拟环境中安装opencv
conda install -c conda-forge opencv
import cv2
import torch #pytorch库加载
import torchvision.datasets as dataset #数据集接口
import torchvision.transforms as transforms #数据转换
import torch.utils.data as data_utils #数据分批from CNN import CNN# print(cv2.__version__)test_data = dataset.MNIST( #测试集root = "mnist", #数据存放目录train = False, #加载训练数据transform = transforms.ToTensor(), #将数据转为张量格式download = True #如果数据集不存在,下载到本地
)test_loader = data_utils.DataLoader(dataset=test_data, #测试集分批batch_size=64, #一批64shuffle=True) #打乱数据cnn = torch.load("model/mnist_model.pkl") #加载模型
# print(torch.cuda.is_available()) #确认cuda能用
cnn = cnn.cuda() #声明在cuda上运行【利用GPU运行】###############################损失函数###############################
loss_func = torch.nn.CrossEntropyLoss() #交叉熵损失函数loss_test = 0 # 测试中的总损失
acc = 0
for (index, (images, labels)) in enumerate(test_loader):images = images.cuda() # 将图片也放到GPU上加载labels = labels.cuda() # 将标签也放到GPU上加载outputs = cnn(images) # 将测试数据传入模型中计算验证(前向传播)_, pred = torch.max(outputs, 1) # torch.max即提取输出的outputs中第1维度数据中的最大值,_部分对应接收最大值(变量_我们默认无意义),pred接受最大值对应的索引acc += (pred == labels).sum().item() # 相加预测正确的数量loss_test += loss_func(outputs, labels) # 叠加每一步损失##opencv可视化#首先要把图片放回cpu,并且将张量转为numpy格式images = images.cpu().numpy()labels = labels.cpu().numpy()preds = pred.cpu().numpy()for idx in range(images.shape[0]): #遍历每一张图片【B,C,H,W】,shape[0]即是B(batch) = 64im_data = images[idx] #取出图片【即(C,H,W)】im_label = labels[idx] #取出真实标签im_pred = pred[idx] #取出预测值print("预测值为{}".format(im_pred))print("真实值为{}".format(im_label))im_data = im_data.transpose((1, 2, 0)) #调整图片格式(C,H,W)=>(H,W,C)cv2.imshow("The image",im_data) #显示图像,第一个参数是当前窗口的命名,第二个是图像,只接受前两个维度,输入格式为(H,W)cv2.waitKey(0) #窗口弹出时停止运行,等关闭窗口后继续执行,参数0代表延迟为0print("loss为{},准确率ACC={}".format(loss_test,acc / len(test_data)))