成果展示
在学习吴恩达机器学习和小土堆入门教程的基础上,完成了该实验,目前可以实现标准数据集的加载、网络模型的搭建及训练、数据可视化、GPU加速功能,是机器学习理论的初步实践
import torch
import torchvision.datasets
from torch import nn
from torch.utils.data import DataLoader
import timefrom torch.utils.tensorboard import SummaryWriter# 获取训练集
train_data = torchvision.datasets.CIFAR10(root="./data", train=True, transform=torchvision.transforms.ToTensor(),download=True)
# 获取测试集
test_data = torchvision.datasets.CIFAR10(root="./data", train=False, transform=torchvision.transforms.ToTensor(),download=True)
# 加载训练数据
train_dataloader = DataLoader(train_data, batch_size=16)
# 加载测试数据
test_dataloader = DataLoader(test_data, batch_size=16)train_data_size=len(train_data)
test_data_size=len(test_data)
# 搭建网络
class NetWork(nn.Module):def __init__(self):super(NetWork, self).__init__()# 网络模型构建self.module = nn.Sequential(nn.Conv2d(3, 32, 5, 1, 2),nn.MaxPool2d(2),nn.Conv2d(32, 32, 5, 1, 2),nn.MaxPool2d(2),nn.Conv2d(32, 64, 5, 1, 2),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(64 * 4 * 4, 64),nn.Linear(64, 10))# 向前传播def forward(self, x):return self.module(x)# 实例化网络对象
NW = NetWork().cuda() # cuda加速# 验证网络正确性,查看卷积后尺寸
# input = torch.ones((64, 3, 32, 32)).cuda()
# output = NW(input)
# print(output.shape)
# print(output.argmax(1))# 创建损失函数
loss_fn = nn.CrossEntropyLoss().cuda() # cuda加速# 创建优化器
optimizer = torch.optim.SGD(NW.parameters(), lr=0.01)# 训练过程
NW.train() # 开启训练模式,但并不必要,仅对特定层起作用
epoch = 10 # 训练轮数for i in range(epoch):start_time = time.time()print("-------第", i + 1, "轮训练--------")for data in train_dataloader:img, target = data # 分别获取图像和标签img=img.cuda()target=target.cuda() # cuda加速output=NW(img) # 输出为经过向前传播的图像# 调优过程loss=loss_fn(output,target) # 根据预测和标签求损失函数optimizer.zero_grad() # 梯度清零,避免干扰loss.backward() # 反向传播,计算参数的梯度optimizer.step() # 调整优化参数cost_time=time.time()-start_timeprint("第",i+1,"轮训练完成,训练用时:",cost_time,"s")# 测试步骤NW.eval() # 验证模式with torch.no_grad(): # 模块下计算不用于梯度求导,即只测试,不调参total_test_loss=0total_accuracy=0 # 总正确率for data in test_dataloader:imgs,targets=dataimgs=imgs.cuda()targets=targets.cuda() # cuda加速output=NW(imgs)loss=loss_fn(output,targets)total_test_loss+=loss.item() # 将loss从tensor类型转为常规类型accuracy=(output.argmax(1)==targets).sum() # 单次验证集的正确数量total_accuracy=total_accuracy+accuracy # 总正确数量print("总损失为:",total_test_loss)print("正确率为:", total_accuracy.item()/test_data_size)
输出示例为:
数据集
数据集加载
主要有标准数据集和自制数据两种方法,标准数据以目标检测为例,有VOC
和COCO
等,自制数据集方法见此处,本文主要介绍读取加载方法。
pytroch中提供DataSet
基类用于获取内容和标签,可以重写该类用于读取自定义的数据,示例如下:
import torch
import torch.utils.data.dataset as Dataset
import numpy as np#创建子类
class New_Dataset(Dataset.Dataset):# 初始化内容和标签def __init__(self, Data, Label):self.Data = Dataself.Label = Label# 返回数据集大小def __len__(self):return len(self.Data)# 获取数据内容及标签并转为Tensor类型def __getitem__(self, index):data = torch.Tensor(self.Data[index])label = torch.Tensor(self.Label[index])return data, label
Dataloader
用于打包数据,给网络提供不同形式的数据,使用方法为数据名=DataLoader(dataset数据集,batch_size=每批取的单元,drop_last=True舍去最后不足的数据,shuffle=True 随机选取)
。
标准数据集
本章使用标准数据集CIFAR10
进行实验,使用方法为train_data = torchvision.datasets.CIFAR10(root="./data", train=True,download=True)
,将训练数据下载到当前目录的data
文件夹下,没有文件时会自动下载,获取测试数据将train
参数设置为False
即可。
该数据集为60000张32×32的图像,其中50000张训练图像,10000张测试图像,使用该数据集的好处就是小,下载演示都比较方便,生产实践中真正搞得数据集动辄几十上百G,暂时还没这个需求。
对第一个加载数据读取情况如下:
数据预处理
Transformor
主要用于图像预处理和相关变换,包含在torchvision.transforms
模块下,一些常用方法如下:
ToTensor
:输入PIL的Image图像格式或 numpy.ndarray的数组,将其转化为包含反向传播所需参数的tensor类型,使用示例为t=transforms.ToTensor(Image.open(path))
。
ToPILImage
:将tensor类型转回PIL的Image格式
Resize
:拉伸压缩调整图像大小,而不分割,使用方法为tr=torchvision.transforms.Resize(size=(宽,高))
若输入为一个数,则图像的短边将与此匹配,其他详细参数见该文章
Normalize
:使用平均值和标准差对图像进行均一化处理,输入每个通道对应的均值和标准差作为参数,函数会利用这两个参数分别将每层标准化(使数据均值为0,方差为1)后输出。
Compose
:组合多个变换,使用方法示例为transforms.Compose([ transforms.CenterCrop(10),transforms.PILToTensor(),transforms.ConvertImageDtype(torch.float) ])
RandomCrop
:随机裁剪,输入大小,返回随机裁剪后的图像
Tensorboard
用于解析训练相关事件,将图像和训练信息可视化,库文件使用from torch.utils.tensorboard import SummaryWriter
导入,创建实例writer=SummaryWriter(".\logs")
,要显示图像使用writer.add_image(标题,tensor_img)
,最常用的绘图使用add_scaler(图标名称,纵坐标,横坐标)
,实例代码及效果如下:
for n_iter in range(100):writer.add_scalar('Loss/train', np.random.random(), n_iter)writer.add_scalar('Loss/test', np.random.random(), n_iter)writer.add_scalar('Accuracy/train', np.random.random(), n_iter)writer.add_scalar('Accuracy/test', np.random.random(), n_iter)
多个图像会自动根据标签字符归类,该方法常用于绘制学习曲线,绘制完成后在终端输入tensorboard --logdir="文件路径\logs --port=新端口"
,在弹出的链接出可查看。
也可以使用该工具绘制模型,通过writer.add_graph(模型名,输入)
使用后必须使用Writer.close()
将内容写入文件中,类似U盘和读写文件操作时的最后一步。
网络搭建
骨架—nn.Module
pytorch中的网络构建通过nn
库实现,其中Module
为骨架,作为容器组合隐藏层及输出层,示例代码如下:
import torch.nn as nn
import torch.nn.functional as Fclass Model(nn.Module):def __init__(self):super().__init__()self.conv1 = nn.Conv2d(1, 20, 5)self.conv2 = nn.Conv2d(20, 20, 5)def forward(self, x):x = F.relu(self.conv1(x))return F.relu(self.conv2(x))
调用该模块时自动执行forward
函数,构造函数中定义神经网络需要的隐藏层。
卷积层Convolution Layers
nn.Conv1d
代表一维卷积,nn.Conv2d
代表二维卷积,常用输入包括输入通道in_channel
、输出通道out_channel
、卷积核大小kernel_size
、填充padding
和步长stride
,由于卷积核也是训练的变量之一,故该方法并不能手动设置卷积核,若要手动设置卷积核,需使用orch.nn.functional.conv2d
方法,在参数的weight
中设置。
池化层Pooling layers
为了保留输入特征,但减少数据量而增加的层,如最大池化,选出卷积核对应的最大值MaxPool
方法,输入有池化窗kernel_size
,步长stride
,填充padding
和空洞卷积dilated
即卷积核有空格,和cellmode
是否保留超出核范围的值。
非线性激活Non-linear Activations
给数据引入非线性特质,如RELU截断非负数值、sigmoid分类方法等。
线性层Linear Layers
对传入数据进行线性变换wx+b
,参数为输入特征的个数和输出特征的个数,即Linear(x,y)
,可将x个特征输出为y个预测。
构建序列
上述隐藏层输出层都写在构造函数并命名太繁琐,调用也很不方便,可以使用Sequential
进行组合,将这些层共同组成模型,示例如下:
model = nn.Sequential(nn.Conv2d(1,20,5),nn.ReLU(),nn.Conv2d(20,64,5),nn.ReLU())
在后续的forward
函数仅需调用self.model()
方法即可实现数据的正向传播。
网络模型
记不清为啥了,反正当时就是用了这个网络,可能是简单网络中效果较好的一种吧,输入三通道32×32的图像,最终输出10个可能特征的预测,中间分别经过的层列表如下:
5×5卷积核的卷积层 | 输入通道为3,输出通道为32 |
---|---|
2×2窗口的最大池化层 | 原有图像缩减16×16 |
5×5卷积核的卷积层 | 输入通道为32,输出通道为32 |
2×2窗口的最大池化层 | 原有图像缩减至8×8 |
5×5卷积核的卷积层 | 输入通道为32,输出通道为64 |
2×2窗口的最大池化层 | 原有图像缩再减4×4 |
Falttern层 | 数据一维化,图像处理成64×4×4=1024个样本 |
全连接层 | 输入1024个样本输出64 |
全连接层 | 输入64个样本输出10个预测特征 |
该步的重点在于计算卷积层的参数,32×32的图像经过5×5的卷积核,步长为1的情况下该输出28×28的矩阵,而我们仍需要输出32×32的图像,则需要对原图像进行填充,填充数值可经过如下公式计算:
空洞卷积我们不使用,故内核间距dilation
为默认的1,可得padding=2
,该步在调用Conv2d
时除了输入输出通道数和卷积核大小外,需要设置该参数。
损失函数
用于衡量预测与实际的差距,为更新输出提供依据(通过反向传播实现),根据模型和预测需要的不同可以选择多种损失函数,MSE平方差,交叉熵等,具体损失函数可见官方文档。
优化器
pytorch实现各种优化算法的包,官方文档在此,使用过程如下:
1,构造优化器对象
2,反向传播计算梯度
3,调优参数
示例代码如下:
optimizer.zero_grad() # 梯度清零,避免上次学习的干扰,pytorch特性是将梯度累加
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 构建SGD优化算法的优化器,学习速率为0.01,动量为0.9
backward() # 反向传播计算梯度
optimizer.step() # 执行调优参数
经过两次优化的部分模型参数变化展示如下:
模型保存与加载
torch.save(模型名,保存文件名.pth)
模型名=torch.load(文件名.pth)
使用该方法会保存模型的结构和参数,但在新文件中加载仍然需要以前的结构文件,即需要import 网络模型文件
,如果有了网络模型文件就没必要从文件中加载了,而因为保存了模型文件,该方法保存的文件体积也较大,故官方推荐使用如下方法:
torch.save(模型名.state_dict(),文件名.pth)
模型.lload_state_dict(文件名.pth)
该方法只保存了模型的训练数据,减少了保存文件体积。
网络模型优化
GPU加速
首先通过两张图直观感受一下GPU的加速效果:
可以看到用了GPU能加的速不是一点半点,这也是为什么当时学习安装pytorch时要着重搞CUDA的安装了,该方法比较费硬件,也对显卡显存有一定要求,大量数据在显卡上运行时是否有和操作系统一致的保护或调度机制,目前还不了解,先这么用着吧,如果没有英伟达显卡的话也可以去谷歌找一张免费提供的显卡使用,好像效果还不错。
使用方法就是分别在模型、数据和损失函数后加上指定显卡运行标识,如NetWork=NetWork().cuda()
,img=img.cuda()
,loss=loss_fn(output,target).cuda()
。
这种方法不大严谨,官方推荐使用device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu"
,有显卡使用第一张显卡,否则使用cpu运行,可以增强程序的可移植性和健壮性,通过NetWork=NetWork.to(device)
的方式给需要的信息加上标签。
输出处理
直接对经过网络的output
进行输出,信息如下:
该信息为16×10的矩阵序列,表示一个批次16张图像的每个结果的可能概率,我们需要的仅仅是最可能的分类结果,在该序列中选择最大的可能作为输出即可,该方法通过调用argmax()
函数实现,参数为0表示横向比较,参数为1表示纵向比较,输出为比较序列最大值的序号。
通过输出可以看到每次预测和真实标签的差距:
统计每个批次预测正确的数量,测试集全部读取完成后与测试集总数之比即为准确率,实现代码如下:
accuracy=(output.argmax(1)==targets).sum() # 单次验证集的正确数量total_accuracy=total_accuracy+accuracy # 总正确数量
# 循环结束后
print("正确率为:", total_accuracy.item()/test_data_size)
答疑
现在已经能构建起自己定义的网络模型并跑出还可以的数据,但在实际构建的确有和基础知识学习时的很大区别,比如:
1,基础学习时特征是经过构建好的神经网络,通过计算损失函数,不断调整神经网络的一些参数,但实验中并没有设置网络参数的过程,只给网络搭起了模型就自行运转了,网络中的参数从何而来?
2,调优过程的反向传播起了什么作用,调优器究竟整合了机器学习的什么步骤?
模型参数
模型在建立时我们并没有指定参数,这是因为pytroch定义了默认值,以Conv2d
为例,继承自ConvNd
类,参数初始化方法定义在其中:
模型初始化参数的方法
backward()
反向传播
我们之前学过,优化成本函数的方法是梯度下降,第一步就需要计算偏导,不同函数人手算很麻烦,计算机实现更复杂,为了简化该过程,有人推导出了由损失函数计算每个结点偏导的快速方法,这种由损失函数推出偏导的方法称为反向传播,pytorch给我们封装成了函数直接使用即可,该步骤在实验中就是为了计算梯度。
说白了方向传播就是求偏导,后面的step
就是给新的参数进行赋值,优化器本质就是给模型更新参数的过程,从数学上来说就是实现了这两个公式:
总结
还是画图比较清楚,对pytorch训练神经网络的整体过程总结如下:
该过程大致和理论学习时一致,优化器整合了梯度下降部分,一些复杂的数学原理并没有向用户过多暴露,使得我们可以很快上手构造模型,但后续调参和模型优化仍要求我们理解底层原理。
这么说来机器学习本身好像也是快上手,慢熟悉的过程,更多操作如特征缩放、判断收敛等还未使用,更不必说神经网络的底层原理,我们并没有规定特征的学习方向,网络究竟是如何通过激活函数和卷积操作等就能学习到准确的特征并判断的呢?不同参数又是如何在一个庞大的神经网络中作用的?目前只能说是稍稍入门,这些都有还待于后续的学习。