搭建环境
我们在pytorch找到这个示例
我们可以看到他这里定义了一个卷积神经网络,然后我们可以考虑自己去搭建一个卷积神经网络,结构的话跟之前第一节内容(LeNet)里的一样
看到这里,其实他的结构非常简单,就是一个卷积,一个下采样,一个卷积,一个下采样,三个全连接层就可以完成了。
但在这里要注意这里他LeNet输入的是一个灰度图像,深度只有一个维度
在我们刚刚的要下载的例程中呢他会有3个RGB彩色通道
Pytorch Tensor的通道排序是[batch,channel,height,width]
第一个参数batch的意思就是一批图像的个数,比如每一批输入32张图片那么就应该是32
如果我们这里输入的是CIFAR-10的图片那么这里应该填[32,3,32,32]
下载资源
下载完毕之后,右击使用pycharm打开
在这里我们就定义了LeNet,在开头导入了两个包,都是来自pytorch包
如果这里torch出现红色波浪线,就说明你还没有安装好pytorch
这里我们得提前下载好pytorch,不会的可以先去把我之前写的两个实战案例都做好再返回这里会有更深刻的理解
在Pytorch中搭建模型,首先就是要定义一个LeNet这个类,这个类就是要继承于nn.module这样一个父类,然后在类中实现两个方法,第一个是初始化函数
def __init__(self):
在初始化函数中,我们可以实现初始化函数过程中所用到的网络层结构
接着在下面forward中定义正向传播的过程,当我们实例化这个类之后,将参数传入到我们的实例中就会进行正向传播。
这里不知道super什么意思的可以去百度一下
摁住ctrl鼠标左键点击Conv2d可以看到给出的简介
这里就是说他用一个2D的输入方法对我们输入的数据进行一个处理
这里是一些参数的定义,这里的
in_channels:代表的是我们输入特征矩阵的一个深度
out_channels:是对应我们卷积核的个数,使用几个卷积核就会生成深度为多少维的一个特征矩阵
kernel_size:代表的就是卷积核的大小
stride:就是他的步距,默认就是等于1
padding:就是在四周进行补0处理,默认就是等于0
后面的dilation跟groups是比较高阶的用法,暂时还用不到
bias:偏置,默认是使用的
进入pytorch的官网我们可以看到我们CONV2D(二维卷积操作的公式)
继续往下翻我们可以看到他对每个参数的解释,以及卷积输出的维度的变化
这里的维度变化跟之前我们基础部分讲的其实是一样的
然后可以在我们的子类中查找所需要的方法
import torch.nn as nn
import torch.nn.functional as F
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 5)#输入深度为3的图片,采用了16个卷积核,卷积核尺寸为5×5
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.relu(self.conv1(x)) # input(3, 32, 32) output(16, 28, 28)
x = self.pool1(x) # output(16, 14, 14)
x = F.relu(self.conv2(x)) # output(32, 10, 10)
x = self.pool2(x) # output(32, 5, 5)
x = x.view(-1, 32*5*5) # output(32*5*5)
x = F.relu(self.fc1(x)) # output(120)
x = F.relu(self.fc2(x)) # output(84)
x = self.fc3(x) # output(10)
return x
根据上面的公式,我们这里输入的是3×32×32大小的
所以他的W就是32 卷积核大小F=5 padding是等于0的 stride是等于1的
算出来就是28
卷积核的维度与输入特征层维度相同
有几个卷积核输出就有几个特征矩阵
所以计算我们的输出就是16×28×28
定义好了我们的卷积层
之后就是定义的我们的下采样层self.pool,使用的方法就是MaxPool2d
我们可以看到这里的第一个参数就是我们池化核的大小,第二个就是步距,如果这里不指定他的步距的话,他就会采用与池化核大小一样的步距,也就是说上面我们采用的是池化核大小为2×2,步距也为2的一个最大池化操作。
经过下采样之后,高度与宽度缩减为原来的一半
池化层:((28-2)/2+1)
接下来我们定义第二个卷积层 conv2,这个时候我们输入的特征层的深度已经变为16了,是因为我们通过第一个卷积输出为深度为16的特征矩阵,这时候我们输入就是16.
计算方法跟上面一样。
接下来我们看一下全连接层,全连接层的输入是一个一维的向量,所以我们需要将我们所得到的特征矩阵展成一个一维向量,我们第一个全连接层的输入节点个数就是32×5×5
这里第一层的节点个数设置为120,第二层84,第三层为10。
这里的10要注意,这里10的这个输出要根据我们的训练集来进行修改
本次案例是是CIFAR-10,他是具有10个类别的分类任务所以这里就是一个10
是这个神经网络定义的,我们只需要按照他的网络定义来构建这个网络,就是上图
Relu是激活函数
下面我们来定义一下他的正向传播过程,这里的x代表的是我们输入的数据,这个数据指的就是
首先我们将我们定义的数据经过卷积层1,接着将我们得到的数据经过Relu激活函数
接着我们的输出再通过下采样层1,得到输出
接着再通过我们的卷积层2,接着将我们得到的数据经过Relu激活函数
接着我们的输出再通过下采样层2,得到输出
再下一层就跟我们的全连接层进行拼接了
我们通过.view这个函数把我们的数据展成为1维向量,这里的-1代表着第一个维度
,会进行自动推理的,因为我们这里第一个维度是batch所以这里设置为-1,第二个维度就是展平后节点的个数,也就是32×5×5.
view中一个参数定为-1,代表动态调整这个维度上的元素个数,以保证元素的总数不变。
将 x
变形为一个形状为 (-1, 32*5*5)
的张量,其中 -1
表示在保持其他维度大小不变的情况下,自动计算该维度的大小,以满足整体张量的大小不变。
-1不是一个有意义的数,在这“暂时占位”而已,可以把他理解成未知数x,他会根据第二个纬度长度反推这个x
具体而言,x.view(-1, 32*5*5)
的目的是将x
张量的形状调整为两维,其中第一维的大小是根据张量总元素数和其他已知维度来自动推断的,而第二维的大小是固定的,即32*5*5
。
-1是xpython里view(x,y)函数的一个可选取值,x这一项置为-1,就会自动根据整个向量的维度和后面的y计算x这项
之后我们再将我们数据经过全连接层1以及他的激活函数
之后我们再将我们数据经过全连接层2以及他的激活函数
最后通过全连接层3得到我们的激活输出
优化器SGD中已经实现了softmax,所以我们这里最后一层不需要添加softmax
测试
接下来我们进行测试
记住设置断点,方便我们的调试
看到我们这里的x,他的shape就是我们输入的32*3*32*32
我们再看一下终端打印的信息 这里就是我们之前初始换函数中定义的所需要的层
接着点击单步运行,首先就是经过我们的第一个卷积以及激励函数
变为了32*16*28*28
又经过池化层 变为了32*16*14*14 正好是我们的输出
接下来就不一一赘述了,就是对我们输入的数据做了一个卷积池化全连接等操作
CIFAR-10讲解
全选然后注释代码(ctrl+/)
后面代码执行顺序我们依次解除注释 并且对其进行详细解释
首先就是先导入我们的包
下载我们的训练集 将里面的download改为true
先加入我们的transforms函数,后面我们再细讲原因,因为这里的函数使用到了
最后两行取消一下注释 然后右击运行
表明是在当前文件夹下生成data目录 并放在data里面
train这个参数为ture的话,他就会导入CIFAR-10的一个训练集的样本,在他的训练集当中包含了50000张训练图片,下载完之后我们将我们的download设置为False,transform就是对我们的图像进行预处理的一个函数
我们再来看一下transform这个函数,首先我们通过transforms.Compose这个函数,将我们所使用的一些预处理方法给打包为一个整体。首先是我们的ToTensor
这里他解释了:将一个PIL图像或者是numpy数据转化成一个Tensor
并且导入的格式为(H×W×C)高度、宽度、深度
然后他的每一个维度的像素值都是0~255
通过我们的这个ToTensor函数之后,将我们的shape变为了(C×H×W),并将我们的像素值都是0~1之间
我们再看到这个,这个函数就是一个标准化的过程
他这里解释到使用均值或者标准差来标准化我们的Tensor
提供的参数有均值以及他的标准差
这里我们的输出=(原始数据-给的均值)/标准差
总结就是我们通过CIFAR10导入到我们的这个训练集,然后将我们训练集的每一个图像经过transform预处理函数进行预处理
除了CIFAR10,我们还有很多其他的数据集(开头大写的)
我们再看到下一个函数
这个loader函数就是将我们刚刚的训练集导入进来,然后把他分成一个批次一个批次的,这里batch_size就是我们之前所说的batch,这里给他分成36,也就是说我们每一次随机拿出36张图片进行训练,
这里的shuffle就是指的是我们是否要将我们的数据集进行一个打乱,这里的num_workers指的就是我们载入数据的一个现成的数
洗牌是为了确保模型在每个epoch中都能够看到不同的样本,有助于提高模型的泛化性能。
num_workers=0
: 这是用于数据加载的子进程数量。num_workers
参数指定了使用多少个子进程来加载数据,可以加速数据加载。在这里,num_workers=0
表示数据将在主进程中加载,没有使用额外的子进程。
windows下num_workers也可以设置了,设置不超过支持的线程数个数的就行,这个会加快图片载入的速度
接着我们按照同样的方法导入我们的验证集,验证集中包含了10000张测试图片,这里的batch_size直接设置成了10000,直接全部拿出来,然后直接去计算验证集的准确率
shuffle设置为false,是因为batch_size=验证集大小了
这里的iter这个函数是将刚刚给生成的val_loader这个参数转换成可迭代的迭代器
转换完成之后通过next这个方法就可以获取到一批数据,数据中包含了验证的图像以及图像对应的标签值
这里的index0对应的就是飞机 index就是汽车 不能改变
查看导入的图片
找到官方对应的代码
首先我们要使用官方给的imshow这个函数
这里面它使用到了两个包,一个是numpy包,第二个包就是plt(专门用来绘制图像的一个包)
所以我们在顶部导入
然后我们再复制imshow的这两行代码就可以查看我们的图片了
调换一下顺序,先打印标签,再看图片
然后我们修改一下参数,这里的5000肯定是看不了的
然后将我们这里的labels改为val_label
之前我们说过,导入图像前会有一个预处理的过程(transform),这里箭头所指的相当于就是对图像进行反标准化处理,相当于还原成了原来的样子
看到我们之前的标准化过程,就是输出=(原始数据-给的均值)/标准差
标准化是:output=(input-0.5)/0.5
反标准化是input=output*0.5+0.5=output/2+0.5
- 平均值归一化: 将图像的每个通道减去平均值,使得每个通道的平均值接近于 0。
- 标准差归一化: 将图像的每个通道除以标准差,使得每个通道的标准差接近于 1。
这里的 mean
和 std
分别是长度为 3 的元组,分别表示图像在每个通道上的平均值和标准差。在这个例子中 (0.5, 0.5, 0.5)
表示将每个通道的值减去 0.5,而 (0.5, 0.5, 0.5)
表示将每个通道的值除以 0.5。
这种标准化操作有助于提高模型的训练速度和稳定性,因为它可以使得输入数据的分布更接近于标准正态分布,从而更容易优化模型的参数。
我们再将图像转化为原来的numpy格式
之前说了再转为Tensor的过程中,它已经将channel的维度提到了第一位(C×H×W)
接下来就是把他还原为载入图像时的最基础的shape,(H×W×C)
012理解成数组索引
通过我们的np.transpose就可以转化为原始的shape格式
接下来我们再使用plt.show将其展示出来
其中这里注意代码的缩进应该是这样的 不然会报错
import torch.utils.data #如果不用这个就会出现pycharm不识别data的问题
我们就看到了我们随机载入的图片
然后我们再导入我们刚刚的类,我们自己定义的LeNet的这个模型
注释掉显示图片的代码 接下来开始正式的训练
查看这个CrossEntropyLoss包含了nn.LogSoftmax这个函数以及nn.NLLLoss这个函数,所以我们就不需要在我们网络的输出加上SoftMax这个函数
训练模型代码
import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import time
import torch.utils.data #如果不用这个就会出现pycharm不识别data的问题
def main():
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# # 50000张训练图片
# # 第一次使用时要将download设置为True才会自动去下载数据集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=False, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
shuffle=True, num_workers=0)
# 10000张验证图片
# 第一次使用时要将download设置为True才会自动去下载数据集
val_set = torchvision.datasets.CIFAR10(root='./data', train=False,
download=False, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=10000,
shuffle=False, num_workers=0)
val_data_iter = iter(val_loader)
val_image, val_label = next(val_data_iter)
#
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# def imshow(img):
# img = img / 2 + 0.5 # unnormalize
# npimg = img.numpy()
# plt.imshow(np.transpose(npimg, (1, 2, 0)))
# plt.show()
#
#
# # print labels
# print(' '.join(f'{classes[val_label[j]]:5s}' for j in range(4)))
# # show images
# imshow(torchvision.utils.make_grid(val_image))
net = LeNet()#实例化模型
loss_function = nn.CrossEntropyLoss()#定义我们的损失函数
optimizer = optim.Adam(net.parameters(), lr=0.001)
#使用的是Adam优化器,传入的第一个参数是我们所需要训练的参数,net就是我们刚定义的net(所有可训练的参数都进行训练),lr代表的就是学习率(learning rate)
for epoch in range(5): # loop over the dataset multiple times
#将我们的训练集迭代5次
running_loss = 0.0#累加在我们训练过程中的一个损失
for step, data in enumerate(train_loader, start=0):#enumerate不仅能返回每一批的数据data,还会返回data随对应的步数(index)
#此处给了start=0,也就是说他会从0开始
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data #得到数据之后将他分离成我们所得到的图像和分离的标签
# zero the parameter gradients
optimizer.zero_grad()#将我们的历史损失梯度清零
# forward + backward + optimize
outputs = net(inputs)#将我们得到的输入图片输入到网络进行正向传播,得到我们的输出
loss = loss_function(outputs, labels)#就是nn.CrossEntropyLoss()去计算他的损失,
#第一个参数就是网络预测的值,第二个就是我们输入图片对应的真实标签
loss.backward()#将我们的loss进行反向传播
optimizer.step()#进行参数更新
# print statistics
running_loss += loss.item()#每次计算完loss之后,又将它累加到刚刚定义的running_loss变量当中
if step % 500 == 499: # print every 500 mini-batches 每隔500步打印数据的信息
with torch.no_grad():#with是一个上下文管理器 整个函数的意思就是在我们接下来的计算过程中,不要去计算每个节点的误差损失梯度
#如果不用这个函数那么就会去测试过程当中也会去计算误差损失梯度,缺点就是会占用更多的算力,占用更多的资源,在这个函数范围内那么就都
#不会计算误差损失梯度了,
"不适用这个函数的话,会自动生成前向的传播图,这会占用大量内存"
outputs = net(val_image) # [batch, 10] 进行正向传播 把前面的batch——size改为10000
predict_y = torch.max(outputs, dim=1)[1]#寻找输出最大的index在哪个位置,我们这里实在维度1上面去寻找最大值
# 然后我们知道这里有十个输出的节点,我们这里第0个维度对应的是batch,所以我们需要在第一个维度,也就是输出的10个节点中寻找最大的值
#这里的[1]代表我们只需要知道他的index看就可以了 [batch, 10] 表示一个二维张量,其中包含了一个批次(batch)的数据,每个数据有10个元素。
#dim=1 表示沿着第二个维度(通常是类别的维度)进行操作。[1] 表示我们想要获取 max 函数返回的索引的部分,而不是最大值。
'一次预测中有一个batch,batch中的一条数据是对0~9个数的预测概率,其中概率最大的那个位置对应的序号,即相当于classes中对应物品的序号'
accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)
#将我们的预测的标签类别与真实的标签类别进行比较,相同的返回true,不相同的返回false
#torch.eq(predict_y, val_label).sum()由于这个过程都是在tensor进行计算的,并不是我们所期望的数值,如果需要这个数值,我们就需要item这个方法拿到对应的数值
#拿到这个数值我们再除以测试样本的数目,也就得到了我们测试的准确率
print('[%d, %5d] train_loss: %.3f test_accuracy: %.3f' %
(epoch + 1, step + 1, running_loss / 500, accuracy))#打印验证过程中的信息
running_loss = 0.0#清零操作 再进行下五百步的迭代过程
'第一个参数指的是我们训练迭代到第几轮了,第二个参数是我们在某一轮的多少步,第三个对应的就是我们训练过程中累加的误差然后除以500就可以得到五百步当中平均的训练误差,第四个就是我们测试样本的准确率'
print('Finished Training')
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)
'保存路径以及保存模型'
#
if __name__ == '__main__':
main()
[batch, 10]
表示一个二维张量,其中包含了一个批次(batch)的数据,每个数据有10个元素。
右击开始运行
第一个epoch的第五百步的时候 当前的训练损失是1.736 测试的准确率是0.429
最后的准确率是0.672
可以看到当前我们文件夹生成的文件,这个就是我们刚刚训练完成之后的模型权重文件
下载一张飞机的图片放入我们的文件中
打开predict的这个文件,这个文件就是调用模型权重文件进行预测的脚本
predict.py
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet
def main():
transform = transforms.Compose(
[transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
net = LeNet()
'实例化权重文件'
net.load_state_dict(torch.load('Lenet.pth'))
'通过load_state_dict来载入我们刚刚保存的权重文件'
im = Image.open('1.jpg')
'再通过上面我们python的PIL库的image这个模块来载入我们的图像'
'之前我们说了通过PIL或者NUMPY图像显示的一般都是高度、宽度、深度'
im = transform(im) # [C, H, W]
'所以就加了一个预处理,跟之前一样,但是多了一个方法就是使用了一个Resize的方法'
'因为我们下载图片的大小肯定是一个不标准的大小,而我们定的LeNet规定的输入图像大小是32*32'
'故我们这里进行一个缩放,将他转化为Tensor,在进行一个标准化处理 (12行)'
im = torch.unsqueeze(im, dim=0) # [N, C, H, W]
'之前我们在PPT看到他是有四个维度的,缺少的那个就是batch,所以我们这里通过unsqueeze给他加上一个新的维度'
'这里的dim=0就是指的是最前面'
'现在就是[batch,channel,height,width]'
with torch.no_grad():
'接下来我们再使用这样一个函数,也就是我们不需要再求他这样的一个损失梯度'
outputs = net(im)
'将我们的图像传入到我们的网络当中输出'
predict = torch.max(outputs, dim=1)[1].numpy()
'寻找输出当中最大值所对应的索引(index)'
print(classes[int(predict)])
'再将index传入到我们的classes,这个classes就是我们上面的所对应的十个类别'
if __name__ == '__main__':
main()
predict = torch.max(outputs, dim=1)[1] 解释
outputs = torch.tensor([[0.2, 0.8, 0.5],
[0.9, 0.1, 0.4],
[0.3, 0.7, 0.6]])
在此示例中,每一行对应于不同类别的输出分数。列表示不同的类。现在,您希望找到每个输入得分最高的类。
使用 torch.max (output,dim = 1) :
Max (output,dim = 1)计算每行的维度1(列)的最大值,生成元组。
对于给定的输出张量,结果可能是:
torch.max(outputs, dim=1)
# Output: (tensor([0.8, 0.9, 0.7]), tensor([1, 0, 1]))
第一个张量(张量([0.8,0.9,0.7]))包含每一行沿维数1的最大值。
第二个张量(张量([1,0,1]))包含每一行沿维数1的最大值的索引。
现在,在[1]的末尾(torch.max (output,dim = 1)[1])提取索引的张量:
predict_tensor = torch.max(outputs, dim=1)[1]
# Output: tensor([1, 0, 1])
预测张量张量包含每个输入的预测类指数,即得分最高的类。在这个例子中,预测的类是[1,0,1]
运行的结果
打印出来的信息就是经过softmax处理得到的概率分布
第一个预测的就是index=0的概率,就是为飞机的概率