构建一个pytorch手写网络的基本流程
文章目录
- 从简单手写数字识别开始基本网络框架的使用
-
- 使用pytorch构建网络——基本组件
- 定义一个简单的网络
- 使用pytorch加载和处理数据
-
- 数据流的基本单位:Tensor
- 下载数据集 & 加载
- 使用pytorch训练及保存模型
-
- 训练的基本逻辑——梯度下降 & 反向传播
- 训练代码
- 模型参数
- 使用pytorch进行推理及验证
从简单手写数字识别开始基本网络框架的使用
使用pytorch构建网络——基本组件
-
卷积——conv层
在pytorch里面,nn.conv2d就是用于构建网络中的卷积层组建,我自己经常用到的有下面几个参数,也就是前几个行参:- in_channels
- out_channels
- kenel_size:卷积核大小
- stride:步长
- padding,边缘,默认为0
首先需要理解一下channel的概念,在RGB图像里,通道就是每个颜色分量。在pytorch里面,每个卷积核对一个通道做完卷积操作之后就可以形成一个通道,那么如果输出通道是32的话,就是32个卷积核对图像去做卷积操作。
卷积的基本作用就是提取图像中的特征,32个卷积通道我理解就是32个特征。我自己使用的卷积层定义为:
nn.conv2d(3, 32, 3, 1)
这句代码的意思就是用32个大小为 3*3的卷积核,对一个通道数为3的图像,以padding为1的step去做卷积操作。卷积之后的张量大小会发生改变:
图像卷积尺寸的计算公式:
N = ( W − F + 2 P ) / S + 1 N = (W − F + 2P )/S+1 N=(W−F+2P)/S+1W为原尺寸大小
F为卷积核大小
P为padding大小
S为步长Stride
所以我卷积出来的尺寸是
X 方向上: ( 702 − 3 + 2 ∗ 0 ) / 1 + 1 = 700 X方向上:(702 - 3 + 2*0)/1 + 1 = 700 X方向上:(702−3+2∗0)/1+1=700
Y 方向上: ( 587 − 3 + 2 ∗ 0 ) / 1 + 1 = 585 Y方向上:(587 - 3 + 2*0)/1 + 1 = 585 Y方向上:(587−3+2∗0)/1+1=585如果一个702 * 587像素大小的3通道图像,经过卷积之后就会变成一个[32, 585, 700]的张量(tensor)
-
ReLU层:
一个被称为线性整流的函数,小于0的数全部变为0,大于0的数等于自己
R e L U = { 0 , x < 0 x , x > 0 ReLU = \\begin{cases} 0, x<0 \\\\ x, x>0 \\end{cases} ReLU={0,x<0x,x>0
用于把非0的值过滤掉。在之前的一篇文章中也提到过:https://blog.csdn.net/pcgamer/article/details/122704136?spm=1001.2014.3001.5502
在pytorch中的组件为nn.ReLU
-
池化层:同样在https://blog.csdn.net/pcgamer/article/details/122704136?spm=1001.2014.3001.5502这片文章中提到过
demo中用到了最大池化层,也就是一个感受野中取最大值。这个组件只有一个行参是必须手动赋值的,也就是kernelSize,感受野的大小。
其实这个层也可以理解为一种降采样pytorch中的2维池化层的组件是nn.MaxPool2d
-
Dropout层,是一种抑制过拟合的方法,用于将一些权重参数按照一定的几率变成0,防止过拟合。参数P指的是被置为0的概率。
-
FLatten层,用于减少张量的维数,pytorch中的组件为nn.Flatten,需要制定的参数是降到哪个维度,从0开始,0表示降到1维,1表示降到2维,以此类推。另外,这个是从最低维往上合并,比如一个shape = [2, 3, 4]的三维向量,如果经过Flatten(1)的一个网络层,就会变成shape = [2, 12]的一个二维向量。
-
Linear层,线性回归层。我理解就是全链接层。因为这个pytorch组件nn.Linear的三个参数就是
- in_features, 输入的神经元个数
- out_features, 输出的神经元个数
- bias,偏移量
也就是把前一层的所有输出值,通过
y = w x i + b y = wx_i + b y=wxi+b
的方式输出到后一层的一个神经元节点。out_features就是表示后一层有多少个节点。那么连接数就是 i n _ f e a t u r e s ∗ o u t _ f e a t u r e s in\\_features * out\\_features in_features∗out_features这么多。 -
Softmax层,激活层。使用的函数为:
s o f t m a x ( x i ) = e x p ( x i ) ∑ j e x p ( x j ) softmax(x_i) = \\frac{exp(x_i)}{\\sum_jexp(x_j)} softmax(xi)=∑jexp(xj)exp(xi)
在pytorch中的组件是nn.softmax,这个组件初始化的时候接受一个行参dim,如果dim=0,表示对矩阵中以行为单位进行一个一维数组进行计算,如果dim=1,则表示对矩阵中以列尾单位进行一个以为数组进行计算。import torch import torch.nn as nn input_0 = torch.Tensor([1,2,3,4]) input_1 = torch.Tensor([[1,2,3,4],[5,6,7,8]]) #规定不同方向的softmax softmax_0 = nn.Softmax(dim=0) softmax_1 = nn.Softmax(dim=1 ) #对不同维度的张量试验 output_0 = softmax_0(input_0) output_1 = softmax_1(input_1) output_2 = softmax_0(input_1) #输出 print(output_0) print(output_1) print(output_2)
输入结果为:
tensor([0.0321, 0.0871, 0.2369, 0.6439]) tensor([[0.0321, 0.0871, 0.2369, 0.6439],[0.0321, 0.0871, 0.2369, 0.6439]]) tensor([[0.0180, 0.0180, 0.0180, 0.0180],[0.9820, 0.9820, 0.9820, 0.9820]])
那么,在我们的MINIST手写数字的识别例子中,最后对全连接层输出的一个10长度的数组进行运算,实际上就是算这10个值的概率大小。
定义一个简单的网络
在pytorch框架里,定义自己的网络框架一般分为两个部分,网络结构和推理过程。
上面已经简单说了没一个组件层的作用,直接把这些层拼到一起。
代码如下,定义一个自己的网络,这个网络是继承nn.Module这个python类,我自己是定义的MINISTNet:
from torch import nn
import torch.nn.functionalclass MINISTNet(nn.Module):def __init__(self):super(MINISTNet, self).__init__()self.model = nn.Sequential(nn.Conv2d(1, 32, 3, 1),nn.ReLU(),nn.Conv2d(32, 64, 3, 1),nn.ReLU(),nn.MaxPool2d(2),nn.Dropout(0.25),nn.Flatten(1),nn.Linear(9216, 128),nn.Linear(128, 10),nn.Softmax(dim=1))def forward(self, x):output = self.model(x)return output
网络结构如下:
- 首先把一个三通道图像进入一个卷积层,输出一个32个通道的3维向量
- 然后把这个卷积层的负值去掉。
- 重复再做一次卷积,扩展到64个通道,并去掉负值
- 做一次池化,或者说最大降采样
- 做一次降维,降到2维,因为进入模型的张量第一维是batchSize,图像数据到第二维的Channel就行了。
- 做两次全连接,第一次全连接的输入是降维操作,在网络里,前一个出来的是卷积之后的池化,需要根据前面的公式进行一下推理和计算
- 概率计算
推理过程就是把一个图像从头到尾进行一次正向计算的过程,重载的forward函数就是pytorch框架用于前向推理的基本操作。
使用pytorch加载和处理数据
把网络结构定义好之后,就可以使用这个网络结构进行预测和推理了,当然,此时完全是一种随机猜测,但是可以运行的。
在这里先提一下,pytorch网络处理的基本单位:张量——Tensor
数据流的基本单位:Tensor
pytorch中的Tensor就是一个向量的概念,可以有多维向量。在手写图像的训练数据集中,一般使用的是一个4维向量,一般的图像处理任务也是这4个维度.
[batch, channel, height, weight]
- BatchSize,训练的批次
- channel,图像的通道数
- height,图像的高度,相当于y坐标
- weight,图像的宽度,相当于x坐标
下载数据集 & 加载
from torchvision import datasets, transforms
import torchtransform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))])
ministDataset = datasets.MNIST(root="data/MINIST", train=True, download=True, transform=transform)
self.train_loader = torch.utils.data.DataLoader(ministDataset)
- torchvision中的dataset库提供了MNIST数据集的下载和加载类,如果提供的root目录没有数据,就会从网上把数据下载下来。
- dataset库中加载数据的时候,可以使用一个transform类将数据初始化成Tensor。
- transform类的compose方法简单来说就是把几个转换方法顺序执行
- transform.ToTensor方法是把输入转换成pytorch的Tensor类,对于图像来说,最主要的操作就是把Weight * Height * Channel的格式变成Channel * Height * Weight的格式
- 标准化操作Normalize,这个操作有两个参数:(mean, std),均值与方差。mean和std都可以是一个数组,这就对应图像中的各个通道,如果是3通道图,就需要有三个值。每个数据集都会提供自己的这两个参数,可以加快模型的收敛速度。
有了这些数据就可以进行训练了
使用pytorch训练及保存模型
训练的基本逻辑——梯度下降 & 反向传播
网络结构和数据都准备好了,准备开始训练。
训练的基本逻辑是反向传播和损失函数计算,具体的原理可以参考我之前的一篇文章:
https://blog.csdn.net/pcgamer/article/details/108538173?spm=1001.2014.3001.5501
通过定义一个损失函数,计算输出值与标准样本值的差值,称为损失。然后把这个损失值从网络的最后一段往前推,计算每一层上的参数的调整值,这个过程就称为反向传播。在计算损失的时候,主要是利用的梯度。
-
定义损失函数
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)crierion = torch.nn.CrossEntropyLoss()
- CrossEntropyLoss就是定义的损失函数,称作交叉熵函数。
- SGD是一个计算优化器,就是计算梯度和损失的时候的一种计算优化,有兴趣的朋友可以自行了解。
-
训练过程中的反复迭代和计算
outputs = model(inputs)loss = crierion(outputs, target)loss.backward()optimizer.step()
- 输出预测值,使用CrossEntropyLoss函数计算输出与实际样本的值
- 通过backward进行反向计算
- 通过optimizer.step更新所有参数
训练代码
def train(epoch):running_loss = 0.0model = MINISTNet()optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)crierion = torch.nn.CrossEntropyLoss()miniLoader = MINISTLoader()miniLoader.load()for batch_idx, data in enumerate(miniLoader.train_loader, 0):inputs, target = dataoptimizer.zero_grad()# forward +backward+updateoutputs = model(inputs)loss = crierion(outputs, target)loss.backward()optimizer.step()running_loss += loss.item()if batch_idx % 300 == 299:print('[%d,%5d] loss:%.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))running_loss == 0.0torch.save(model, "result/minist_model.pth")torch.save(model.state_dict(), "result/minist_model_param.pth")
模型参数
有一个问题是,模型保存下来的pth文件里面是啥。我的理解是两个:
- 网络的结构,这个按理来说不会很大,就是层与层之间的拓扑关系
- 网络参数,这个会很大,我们可以拿我们的这个简单模型来看一下:
- 第一个卷积层,输入1个通道,输出32个通道,也就是这里有32个卷积核,每个卷积核是3*3,这里就是32 * 9 = 283个参数
- 第二个卷积层,输入32个通道,输出64个通道,这里没有分组,共有64个5*5的卷积核。
- 后面的两个全连接层,第一个就是一个9216*128的参数列表。
所以我理解占主要部分的就是参数,被数据训练出来的也是这些参数。
使用pytorch进行推理及验证
模型训练出来之后,直接就可以用来做推理了
def predict(img):model = torch.load('result/minist_model.pth')# model = MINISTNet()# model.load_state_dict('result/minist_model_param.pth')model.eval()output = model(img)print(output)img = cv2.imread('images/3.jpg')
img = cv2.resize(img, (28, 28))
img = img[:, :, 0:1]img_tensor = torch.ones(1, 1, img.shape[0], img.shape[1])tfms = transforms.Compose([transforms.ToTensor(),transforms.Normalize(0.485, 0.229)])img = tfms(img)
img_tensor[0] = img
predict(img_tensor)
- 加载模型,同过pytorch.load把刚才的模型加载进来,并把尺寸进行一下resize
- 调用model.eval(),使模型进入推理模式
- 使用opencv加载图像数据,但是因为模型需要一个四维张量,所以需要先把图像的三位数据扩充到四维,然后再通过transform进行转换
- 最后直接进入模型进行预测