目录
写在前面
这一段时间上课在学习简单的神经网络,最后结课的时候要求搭建一个简单的网络模型,并训练识别几类交通标志牌。写下博客记录,供日后学习。这个简易模型可以根据不同的数据集实现对不同的类别的图片识别。但是层数只有三层,只能训练简易的数据集。
/------------------------------------2021.8.7更新--------------------------------------------------------------/
1.加了一些笔记,补充之前自己学习上面一些不理解的地方
————————————————————————————————————————
一些笔记
1. '.view()'函数相关笔记
.view()函数的用法
作用:改变Tensor的维度,使其变成你需要的维度
例如output.view(2,2) 将维度变成2*2
如果是在view里面加入了-1的参数,那么代表着这个维度会根据其他唯独算出来
即:一个size为16的tensor变量——output
经过了output.view(-1,8)就会变成(2,6)
2.卷积神经网络中图像大小的计算公式
在刚开始实现卷积神经网络的过程中,我经常由于没有算清楚图像的层数,而导致了最终程序出现报错的情况,搜索了资料之后发现,有固定的公式来计算我们所需要的图像
公式如下:
卷积层——输出=[(输入大小-卷积核大小+2*填充)/步长] +1
池化层——输出=[(输入大小-卷积核大小)/步长] +1.
拿一个例子来说明一下
#读取的数据集按照以下方式读取
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
# 以下是三层,每一层的图像大小如下图所示
self.conv1 = torch.nn.Conv2d(in_channels=3, out_channels = 32, kernel_size=5, stride=1, padding=2)
#按照读取batch为64来看,这里的大小由64*3*30*30变成了64*32*30*30
self.pool1 = torch.nn.MaxPool2d(kernel_size=2, stride=2)
#经过第一次池化之后,这里的池化卷积核为2*2,故大小变成了64*32*15*15
self.conv2 = torch.nn.Conv2d(in_channels=32, out_channels = 32, kernel_size=5, stride=1, padding=2)
self.pool2 = torch.nn.MaxPool2d(kernel_size=2, stride=2)
#经过第二次池化之后,大小变成了64*32*7*7
self.conv3 = torch.nn.Conv2d(in_channels=32, out_channels = 32, kernel_size=5, stride=1, padding=2)
self.pool3 = torch.nn.MaxPool2d(kernel_size=2, stride=2)
# 最后一次池化之后已经变成了64*32*3*3
3.图像正规化处理
正规化:将数据按比例缩放,使之落入一个小的特定区间。
我们用Normalize()来对图像进行正规化处理
因为图像一般具有三个维度,RGB,所以需要在三个维度上给均值和方差。
实现卷积神经网络的过程
这段时间的学习下来,我理解的深度学习的流程大致如下:
过程
一、图像预处理化函数定义
'''
Image preprocessing package in Python, Compose is used to integrate muiltiple steps
--图像预处理化的步骤,将所有的步骤全部写到这个里面
'''
myTransforms = (
[transforms.Resize((40,40)),#图像尺寸重新定义,将图像尺寸变为40*40
transforms.ToTensor(), # 将图像归一化并且变成张量
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
#将图像归一化,图像的数值范围变为(-0.5~0.5)
图像预处理化是一步非常重要的步骤,尤其是需要将图像数据变成张量,因为pytorch里面的处理数据的基本操作是张量,即Tensor,具体可以搜索张量的含义。
博主这里用的预处理化操作只有三个,实际上还可以有很多其他的操作
例如:
ToPILImage: convert a tensor to PIL image
Resize:将图像拉伸或者收缩成自己需要的尺寸
CenterCrop:在图片的中间区域进行裁剪
RandomCrop:在一个随机的位置进行裁剪
RandomHorizontalFlip:以0.5的概率水平翻转图像
RandomVerticalFlip:以0.5的概率竖直翻转图像
RandomResizedCrop:将PIL图像裁剪成任意大小和纵横比
Grayscale:将图像转换为灰度图像
RandomGrayscale:将图像以一定的概率转换为灰度图像
FiceCrop:把图像裁剪为四个角和一个中心
Pad:填充
ColorJitter(色彩抖动):随机改变图像的亮度对比度和饱和度。
根据自己的需要添加或者删除操作。
下面的这一篇博客里面,博主讲了许多种图像处理和增强的方法,值得收藏
【pytorch】常用图像处理与数据增强方法合集(torchvision.transforms)
二、数据导入
我们用的数据集是按照不同的文件夹里面放入了不同的图片,所以可以直接利用torchvision.datasets.ImageFolder()这一个函数来进行数据读入,并将图像进行预处理化。
针对的数据集都需要如以下数据集类型:即按照文件夹的顺序把文件夹的名字当作标签,文件夹里面的图片作为该标签下面的数据。
#例1:
root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png
root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
例2:
用法:
dataset=torchvision.datasets.ImageFolder(
root, transform=None,
target_transform=None,
loader=<function default_loader>,
is_valid_file=None)
参数含义
- root:图片存储的根目录,即各类别文件夹所在目录的上一级目录。例如/root/home/user
- transform:对图片进行预处理的操作(函数),原始图片作为输入,返回一个转换后的图片。
- target_transform:对图片类别进行预处理的操作,输入为 target,输出对其的转换。如果不传该参数,即对 target 不做任何转换,返回的顺序索引 0,1, 2…
- loader:表示数据集加载方式,通常默认加载方式即可。
- is_valid_file:获取图像文件的路径并检查该文件是否为有效文件的函数(用于检查损坏文件)
返回的dataset有以下属性
- self.classes:用一个 list 保存类别名称
- self.class_to_idx:类别对应的索引,与不做任何转换返回的 target 对应
- self.imgs:保存图片的list
- dataset[0] 表示取第一个训练样本,即(path, class_index)。可以自自行printf(dataset[0][0])和dataset[0][0]查看区别
使用了ImageFolder之后,还需要将返回的dataset变成lodaer一下,将数据转化成模型可以使用的数据类型。具体的实例见下方
train_dataset = torchvision.datasets.ImageFolder('/home/Desktop/dataset',transform=myTransforms)
'''
train: use train file:是否使用训练文件
download: if you have not downloaded the test file, program will download from web. if you have downloaded, do nothing
the method is myTransforms:训练方法是自己定义的Compose
'''
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
'''
tran_dateset: take test_file form:训练的对象
batch_size: how many samples per batch to load (default:1):一次训练时候抓取的样本数量.或者一次检测图片的时候抓取的图片数量
shuffle: wheather reshuffle after every train:每次训练之前,是否要将训练集的数据打乱
num_workers: how many subprocesses use for data loading:使用多少个线程加载数据集
'''
三、模型搭建
这个模型中包含了三层
'''
in_channels: Number of channels in the input image
out_channels: Number of channels produced by the convolution
#注意:输出的通道数量代表了你有多少个卷积核,这里面有32个通道有默认的不同的卷积参数(其实也可以自己更改参数)
#我们训练的实质,其实就是训练这一个参数,对于不同类别的图片,参数会有kernel_size区别,这个参数就相当于是过滤器,通过了这一个过滤器之后我们就能快速得到是哪一类别
kernel_size: Size of the convolving kernel:卷积核的大小
stride:步幅,卷积核在图片上移动后遍历每一个像素点,每次移动的步长就是stride
padding:填充,在图像周围填充一圈像素点。
'''
class myNetwork(torch.nn.Module): # 定义自己的网络对象
def __init__(self) :
'''
初始化函数
'''
super(myNetwork, self).__init__()
'''
super(Net, self).init()是指首先找到Net的父类(比如是类NNet,然后把类Net的对象self转换为类NNet的对象,然后“被转换”的类NNet对象调用自己的init函数
简单理解:子类把父类的__init__()放到自己的__init__()当中,这样子类就有了父类的__init__()的那些东西。
'''
self.convl1 = torch.nn.Conv2d(in_channels = 3, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool11 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
'''
# 初始化卷积层和池化层
# 池化层的操作和卷积层的操作类似,但是最后是采用取最大值或者最小值来代表卷积之后那片区域的值
'''
self.convl2 = torch.nn.Conv2d(in_channels = 32, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool12 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
self.convl3 = torch.nn.Conv2d(in_channels = 32, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool13 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
# 全连接层
'''
输入图像的尺寸为32*32*40*3(RGB)
图片变化尺寸的过程见后续的公式
在这个定义的三层卷积网络中,卷积层和padding加起来不会影响图片的尺寸
所以每一次池化,大小为2*2,步长为1,图片就会除以2
40*40*32经过3次池化之后,就变成了40/2/2/2*40/2/2/2*32 = 32*5*5
'''
self.fc1 = torch.nn.Linear(in_features = 32*5*5, out_features = 64, bias=True)
self.fc2 = torch.nn.Linear(in_features = 64, out_features = 64, bias=True)
self.fc3 = torch.nn.Linear(in_features = 64, out_features = 10, bias=True)
'''
in_features指的是输入的二维张量的大小,即输入的[batch_size, size]中的size。
out_features指的是输出的二维张量的大小,即输出的二维张量的形状为[batch_size,output_size],当然,它也代表了该全连接层的神经元个数。
实际上全连接层也是做了一个类似于卷积的操作,将所有的特征点映射到了一个值
'''
# 训练模型的函数
def forward(self,train_data):
output = self.pool11(torch.nn.functional.relu(self.convl1(train_data)))
output = self.pool12(torch.nn.functional.relu(self.convl2(output)))
output = self.pool13(torch.nn.functional.relu(self.convl3(output)))
output = output.view(-1, 32*5*5)# 动态调整维度上的元素个数,确保元素的总数不变。
output = torch.nn.functional.relu(self.fc1(output))
output = torch.nn.functional.relu(self.fc2(output))
output = self.fc3(output)
return output
图像尺寸变化的公式
- height in:指输入的高
- height kernel:卷积核的大小
- padding:图像边缘增加的像素
- stride:步长
四、开始训练模型
训练模型的过程大致如下:
这一个简单的神经网络可以理解成一个分类器,可以分出图像属于哪一类的图片
训练模型其实可以简单的理解成不停的遍历数据集,后向传递参数,使得网络模型的分类器的参数针对你这几类图片越来越好。
# 开始
myModel = myNetwork() # 建立了一个模型是myModel
myOptimzier = optim.SGD(myModel.parameters(),lr = 0.01, momentum = 0.9) # 神经网络优化器,主要目的是优化神经网络,使训练过程加快
myLoss = torch.nn.CrossEntropyLoss() # 计算损失函数
for _epoch in range(10): #循环10次,增加遍历数据集的次数有利于实现更好的训练效果
training_loss = 0.0
'''
step每次都是增加1,即遍历了整个图片
而每次抓取图片的数量为64
'''
for _step, data in enumerate(train_loader): # enumerate()函数的作用是通过迭代来遍历一个字符串、列表或字典等,并且为其增加索引,返回值为enumerate类。
image,label = data # train_loader里面每个数据集对应了一个图片以及标签
predict_label = myModel.forward(image) # 经过模型之后的预测值
loss = myLoss(predict_label, label) # 预测的标签值和实际值的损失值
myOptimzier.zero_grad()# optimizer.zero_grad()意思是把梯度置零,也就是把loss关于weight的导数变成0.
loss.backward() # 损失传递后向更新
myOptimzier.step() # 更新所有参数
training_loss = training_loss + loss.item()
''''
loss是张量,loss.item是对其取数值,但注意 item() 只适用于 tensor 只包含一个元素的时候。
每10次把平均损失值计算出来
'''
if _step % 10 == 0:
print('[iteration - %3d] training loss: %.3f' % (_epoch*len(train_loader) + _step, training_loss/10)) # 训练的总次数,每10次损失的值/10得到平均值
training_loss = 0.0
五、保存模型
针对保存模型,我们可以使用torch.save来保存模型参数或者保存整个模型
'''保存模型'''
#第二个参数是你保存模型的名字
#torch.save(myModel.state_dict(), 'params.pkl')#保存模型的参数
torch.save(myModel, 'model2.pkl') # 保存整个模型
之后需要使用模型的时候,或者需要继续训练模型参数的时候,我们可以使用load函数来进行加载模型和参数
torch.load(f, map_location=None, pickle_module=<module 'pickle' from '...'>)
torch.nn.Module.load_state_dict(state_dict, strict=True)
具体使用方法可以参考以下博客,写的特别清晰
Pytorch:模型的保存与加载 torch.save()、torch.load()、torch.nn.Module.load_state_dict()
六、使用训练好的模型测试训练集的准确率
correct = 0
total = 0
'''
关于with,with的作用是对于with后面任务进行事前设置,事后清理。对异常情况进行处理
(1)紧跟with后面的语句被求值后,返回对象的“–enter–()”方法被调用,这个方法的返回值将被赋值给as后面的变量;
(2)当with后面的代码块全部被执行完之后,将调用前面返回对象的“–exit–()”方法。
比如文件需要打开,用完需要关闭,并且于对文件读取的数据发生异常的时候,进行处理
对于torch.no_grad()的理解,pytorch的张量变量有一个requires_grad参数,设置为ture的话,反向传播的时候tensor会自动求导
这里设置为no_grad。即不用构建计算图,因为我们只是进行模型测试,不需要进行更新参数,使得内存节约
'''
with torch.no_grad():
for images,labels in train_loader:
outputs = myModel(images)
numbers,predicted = torch.max(outputs.data,1)
# 函数返回两个张量,一个output_data里面每一行最大的值,第二个是每一行最大值的索引
total += labels.size(0)
correct += (predicted==labels).sum().item()
print('Testing Accuracy : %d %%' % ( 100 * correct / total))
简单来说就是看模型输出的准确率和loss来判断你的模型训练的是否可以,如果准确率不够,可以试试增加卷积的层数和训练遍历的次数来加大准确率和减小loss
七、测试模型
当我们训练完了模型之后,我们需要将模型用测试集里面的图片进行测试,下面是一个简单的例子,实现了2w张图片遍历测试,并输出结果到txt文件中
其实最重要的就是torch.load和net(img)这两个函数
from matplotlib import image
import torch
import torchvision
import torch.nn.functional as F
import torchvision.transforms as transforms
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from torchvision import transforms
class myNetwork(torch.nn.Module): # 定义自己的网络对象
def __init__(self) :
super(myNetwork, self).__init__()
self.convl1 = torch.nn.Conv2d(in_channels = 3, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool11 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
self.convl2 = torch.nn.Conv2d(in_channels = 32, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool12 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
self.convl3 = torch.nn.Conv2d(in_channels = 32, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool13 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
self.fc1 = torch.nn.Linear(in_features = 32*5*5, out_features = 64, bias=True)
self.fc2 = torch.nn.Linear(in_features = 64, out_features = 64, bias=True)
self.fc3 = torch.nn.Linear(in_features = 64, out_features = 10, bias=True)
def forward(self,train_data):
output = self.pool11(torch.nn.functional.relu(self.convl1(train_data)))
output = self.pool12(torch.nn.functional.relu(self.convl2(output)))
output = self.pool13(torch.nn.functional.relu(self.convl3(output)))
output = output.view(-1, 32*5*5)# 输出可视化
output = torch.nn.functional.relu(self.fc1(output))
output = torch.nn.functional.relu(self.fc2(output))
output = self.fc3(output)
return output
myTransforms = transforms.Compose(
[transforms.Resize((40,40)),
transforms.ToTensor(), # 将图像归一化并且变成张量
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
predicted = 1
if __name__ == '__main__':
with open("result_lyx","w") as file:
for i in range(20000):
net=torch.load('model2.pkl')
torch.no_grad()
img=Image.open('/home/usr/Desktop/dataset/test/' + str(i) + ".png")
img=myTransforms(img).unsqueeze(0)
outputs = net(img)
_, predicted = torch.max(outputs, 1)
#print(str(i) + ".png" + " " + str(predicted) +type[predicted] + "\n")
file.write(str(i) + ".png" + " " + str(predicted.item()))
file.write("\n")
完整代码
训练模型代码
from matplotlib import image
import torch
import torchvision
import torch.nn.functional as F
import torchvision.transforms as transforms
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
from torchvision.datasets import ImageFolder
from torch.utils.tensorboard import SummaryWriter
from torchvision.transforms.transforms import RandomHorizontalFlip, Resize
myTransforms = transforms.Compose(
[transforms.Resize((40,40)),
# transforms.RandomResizedCrop(37),
transforms.ToTensor(), # 将图像归一化并且变成张量
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
train_dataset = ImageFolder('/自己的路径',transform=myTransforms)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
class myNetwork(torch.nn.Module): # 定义自己的网络对象
def __init__(self) :
super(myNetwork, self).__init__()
self.convl1 = torch.nn.Conv2d(in_channels = 3, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool11 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
self.convl2 = torch.nn.Conv2d(in_channels = 32, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool12 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
self.convl3 = torch.nn.Conv2d(in_channels = 32, out_channels = 32, kernel_size = 5, stride = 1, padding = 2)
self.pool13 = torch.nn.MaxPool2d(kernel_size = 2, stride = 2)
# 全连接层
self.fc1 = torch.nn.Linear(in_features = 32*5*5, out_features = 64, bias=True)
self.fc2 = torch.nn.Linear(in_features = 64, out_features = 64, bias=True)
self.fc3 = torch.nn.Linear(in_features = 64, out_features = 10, bias=True)
def forward(self,train_data):
output = self.pool11(torch.nn.functional.relu(self.convl1(train_data)))
output = self.pool12(torch.nn.functional.relu(self.convl2(output)))
output = self.pool13(torch.nn.functional.relu(self.convl3(output)))
output = output.view(-1, 32*5*5)# 动态调整维度上的元素个数,确保元素的总数不变。
output = torch.nn.functional.relu(self.fc1(output))
output = torch.nn.functional.relu(self.fc2(output))
output = self.fc3(output)
return output
# 开始
myModel = myNetwork() # 建立了一个模型是myModel
myOptimzier = optim.SGD(myModel.parameters(),lr = 0.01, momentum = 0.9) # 神经网络优化器,主要目的是优化神经网络,使训练过程加快
myLoss = torch.nn.CrossEntropyLoss() # 计算损失函数
for _epoch in range(10):
training_loss = 0.0
for _step, data in enumerate(train_loader): # enumerate()函数的作用是通过迭代来遍历一个字符串、列表或字典等,并且为其增加索引,返回值为enumerate类。
image,label = data # train_loader里面每个数据集对应了一个图片以及标签
predict_label = myModel.forward(image) # 经过模型之后的预测值
loss = myLoss(predict_label, label) # 预测的标签值和实际值的损失值
myOptimzier.zero_grad()# optimizer.zero_grad()意思是把梯度置零,也就是把loss关于weight的导数变成0.
loss.backward() # 损失传递后向更新
myOptimzier.step() # 更新所有参数
training_loss = training_loss + loss.item()
if _step % 10 == 0:
print('[iteration - %3d] training loss: %.3f' % (_epoch*len(train_loader) + _step, training_loss/10)) # 训练的总次数,每10次损失的值/10得到平均值
training_loss = 0.0
'''保存模型参数'''
# torch.save(myModel.state_dict(), 'params.pkl')
torch.save(myModel, 'model2.pkl')
correct = 0
total = 0
with torch.no_grad():
for images,labels in train_loader:
outputs = myModel(images)
numbers,predicted = torch.max(outputs.data,1)
# 函数返回两个张量,一个output_data里面每一行最大的值,第二个是每一行最大值的索引
total += labels.size(0)
correct += (predicted==labels).sum().item()
print('Testing Accuracy : %d %%' % ( 100 * correct / total))
测试模型完整代码见上方七即可