文章目录
- Python深度学习神经网络的API
- pytorch简介
- pytorch安装
- GPU和CPU
- 张量
- 1.基本概念
- 2.图像作为输入
- 3.张量的创建方式
- 弄好一个numpy数组后,利用它有四种方式创建张量
- 没有预先确定数据,生成默认张量
- 4.张量的计算
- 5.重塑
- 6.压缩与解缩
- 7.堆叠与拼接
- Pytorch中的广播
- 相同维度 不同形状
- 张量1与标量1
- 不同维度,不同形状
- 预处理
- 构建神经网络
- 层
- 参数
- 前向传播
- 预测
- 训练数据
- 单批次训练过程
- 多批次多周期训练过程
- TensorBoard
- 相关函数
- 准备数据
- 启动 TensorBoard
- 优化训练代码
- 编写RunBuilder类
- 编写RunManager类
- Training Loop
- 多进程加速神经网络训练
- 数据集规范化
- 网络权重的处理
- 重置网络权重
- 加载保存网络权重
Python深度学习神经网络的API
pytorch简介
是一个深度学习框架and科学计算包,科学计算这方面主要指的是有关张量的运算,它与Python中Numpy包的n维数组计算非常相似,并且pytorch与numpy有高度的互通性。所有深度学习框架都有2个特性:一个张量库、一个用于计算导数的包,在pytorch中分别对应torch和torch.autograd
相关支持已经内置在GPU中,也就是说如果我们安装好pytorch那么有关张量的大量运算可以很容易的移动到GPU上计算
pytorch包的功能
torch:顶级包、张量库,除了包含的子包外还有一些函数
torch.Storage 管理数据的存放形式和位置
torch.nn子包是搭建神经网络包,包含比如图层、前向函数、权重等相关的类和模块
- torch.nn.Prameter 负责管理网络中的参数,比如是否需要求导,是参数的实际类别
- torch.nn.init 网络中参数的初始化
- torch.nn.functional 包含了网络中各个层次需要的多种函数如激活函数、归一化函数、损失函数、卷积运算
- torch.nn.Module/Sequential(Containers) 作为容器,定义的各种层放进来
- torch.nn.Linear/Conv123d/XxxPool123d 它们都继承了Module类
- torch.nn.一些现成的模型名字
torch.autograd 负责处理导数的计算
torch.optim 能使用典型优化,比如SGD、Adam
torch.utils 包含像数据集、数据加载器等实用程序类,方便数据预处理
torchvision 单独的霸屏 ,支持访问流行的数据集、经典计算机视觉模型架构和图像转换
pytorch安装
千言万语汇成一句话,使用高性能设备+vpn可以减少90%的报错
1.在anaconda中创建一个名为pytorch的虚拟环境
2.更新显卡驱动到最新版本,查看最高支持的cuda版本
3.对照cuda版本获取相应版本的pytorch安装代码,开启vpn,conda执行
cuda是NVIDIA推出的GPU计算架构平台,开发人员可以通过cuda来利用GPU的并行计算能力。cudnn是cuda的深层神经网络
GPU和CPU
GPU更适合并行计算因为它有成千上万个核,而cpu一般高者才有16核。
例如,卷积运算中输入矩阵经过卷积核一步一步投影成一个个结果并组成输出矩阵,其中每一步卷积计算都是独立的并不依赖某一步前置计算。
但是,如果一个任务本来就很小还要转到GPU计算可能会变得更慢,因为从CPU移动到GPU上的过程也需要开销。
张量
1.基本概念
标量 向量 矩阵,超过2维统称n维张量。
秩:明确一个张量有多少个轴,如秩2表示此张量需要[m] [n]2个轴表示,这个值也被称为 “张量形状的长度”,也就是tensor.shape()返回值列表的长度,t.shape() 返回值是一个列表,其中的每个值表示张量每个轴的数据个数。
索引:明确某一个轴的长度。
t = torch.tensor(dd) 将一个列表转化成张量。
2.图像作为输入
输入形状 = [ B ,C ,H ,W ] 表示此批量输入数据中有B张C通道、H*W像素点图像。
假设一张rgb图像,C H W可以确定图像某通道上的某一点的值,B是在批量数据处理中第几张。
3.张量的创建方式
data = np.array([1,2,3])
type(data) // numpy.ndarray类型
弄好一个numpy数组后,利用它有四种方式创建张量
一般使用torch.tensor(),因为它可以指定或自适应数据类型;如果对性能调优,选择torch.as_tensor(),因为相比torch.from_numpy()它可以接受任何数据结构的输入
torch.Tensor(data)
// tensor([1., 2., 3.])
//这是个类构造函数,会将数组中的数据变成浮点类型
//在torch中float32是默认的**dtype**,numpy中**浮点**的默认类型是float64
//生成张量后,修改numpy数组的值张量不会发生变化,因为创建时在内存中额外copy一个副本出来
torch.tensor(data)
//tensor([1, 2, 3], dtype=torch.int32)
// 与numpy数组中的数据类型匹配,这种工厂函数会根据输入推断数据的dtype
// 这个包括下面两个都是工厂函数
torch.tensor(data,dtype=torch.float64)
//tensor([1., 2., 3.], dtype=torch.float64)
//此函数也可以显示地在第二个参数上指定数据类型
//生成张量后,修改numpy数组的值张量不会发生变化
torch.as_tensor(data)
//tensor([1, 2, 3], dtype=torch.int32)
// 与numpy数组中的数据类型匹配
//生成张量后,修改numpy数组的值张量发生变化,在内存中与numpy数组共享数据
torch.from_numpy(data)
//tensor([1, 2, 3], dtype=torch.int32)
// 与numpy数组中的数据类型匹配
//生成张量后,修改numpy数组的值张量发生变化
没有预先确定数据,生成默认张量
torch.eye(2)
// tensor([[1., 0.],
[0., 1.]])
// 生成一个浮点对角张量
torch.zeros(2,2)
// tensor([[0., 0.],
[0., 0.]])
// 生成全零浮点张量
torch.ones(2,2)
// tensor([[1., 1.],
[1., 1.]])
// 生成全1浮点张量
torch.rand(2,2)
// tensor([[0.1632, 0.2996],
[0.4889, 0.3011]])
// 生成随机浮点张量
4.张量的计算
我们可以打印出张量的基本属性:
print(t.dtype) // 首字母d代表数据,数据类型比如float、int,区别于Python的type()是打印对象的类型
print(t.device) // 此张量目前在cpu上
print(t.layout) // Stride
张量之间的运算必须具有相同的数据类型。
device决定了张量计算的位置,并且张量之间的计算也必须在同一个设备上,如果有多个GPU它们会被打上索引0,1…也就是在相同索引的GPU上运算(如果是在GPU上算的话)。
Stride是布局的默认值,没必要改变它
5.重塑
t.reshape(m,n,…) 第一个轴长度是m,第二个轴长度n…
需要注意的是重塑时分量值的乘积必须等于原先张量中元素的总数,所以可能要用到 t.numel() 输出元素总数。
t.shape属性值是一个列表,其中的每个值表示张量每个轴的数据个数。
重塑还可以在一个张量的任意两轴之间增加新轴:t.reshape(3,4,4) —> t.reshape(3,1,4,4)
6.压缩与解缩
准备数据
import torch
import numpy as np
data = np.array([1.,1.,1.,1.,2.,2.,2.,2.,3.,3.,3.,3.])
t = torch.tensor(data)
重塑一下
print(t.reshape(1,12))
print(t.reshape(1,12).shape)
//tensor([[1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.]], dtype=torch.float64)
torch.Size([1, 12])
重塑的基础上做“压缩”操作,压缩一个张量可以移除所有长度为1的轴
print(t.reshape(1,12).squeeze())
print(t.reshape(1,12).squeeze().shape)
//tensor([1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.], dtype=torch.float64)
torch.Size([12])
解缩会使张量增加一个长度为1的轴
print(t.reshape(1,12).squeeze().unsqueeze(dim=0))
print(t.reshape(1,12).squeeze().unsqueeze(dim=0).shape)
//tensor([[1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.]], dtype=torch.float64)
torch.Size([1, 12])
上述操作在神经网络编程中的应用:flatten函数。这个函数用于卷积神经网络中与全连接层连接时。
def flatten(t)
t = t.reshape(1,-1) // -1表示这第二个参数会根据前一个参数以及张量中元素的个数来动态得出这里的值应该是多少
t = t.squeeze()
return t
还可以这么做:
- t.reshape(-1)
- t.view( t.numel() ) or t.view(-1)
- t.flatten() //pytorch提供的一个拉平函数 默认参数(strat_dim = 0,end_dim=-1)
在批量图像作为输入时,张量大概是这样的:输入形状 = [ B ,C ,H ,W ],分别对应图片数量、通道数、像素大小。所以这种情况下某卷积层输出的张量可能也是个四维张量,我们不希望将这个张量一股脑压缩成一维因为这样不能区分每张图片对应的数据,因此可以使用函数 t.faltten(strat_dim=1) , 表示从索引1到索引结束的轴全部合并拉平,完事后张量形状变为[ B ,C * H * W ]。另一种方法是直接使用t.reshape(B,-1) 重塑。
7.堆叠与拼接
stack(inputTensor, dim=x) 沿着一个新维度对输入张量序列进行连接,序列中所有的张量都应该为相同形状
# 两个张量shape [3,3]
T1 = torch.randn(3,3)
T2 = torch.randn(3,3)
# 0<=dim<len(outputs)=旧张量维度+1
print(torch.stack((T1,T2),dim=0).shape) #最常用
print(torch.stack((T1,T2),dim=1).shape)
print(torch.stack((T1,T2),dim=2).shape)
print(torch.stack((T1,T2),dim=3).shape) # 报错 dim最多只能取2
# outputs:
torch.Size([2, 3, 3])
torch.Size([3, 2, 3])
torch.Size([3, 3, 2])
cat(inputTensor, dim=x) 除x维数值可不同外其余维数数值需相同才能对齐
a = torch.randn(2,3)
b = torch.randn(3,3)
c = torch.cat((a,b),dim=0) #shape [5,3]
a = torch.randn(2,3)
b = torch.randn(2,4)
c = torch.cat((a,b),dim=1) #shape [2,7]
实际使用层场景,例如有5张大小为28*28的3通道图像已经被批处理成一个张量T待输入,T.shape=[5,3,28,28],现在还有另外的7张相同规格的图像[3,28,28]也像加入到T中一起输入,那么第一步需要利用stack()在0维将这7张3维的图像张量堆叠成四维的张量T2:[7,3,28,28],然后利用cat()在0维将T与T2拼接成[12,3,28,28]
Pytorch中的广播
element-wise operations按元素逐项操作:
- 算数操作±*/、
- 比较操作:t.eq(0) 是否和0相等 还有ge()、gt()、lt()等
- 函数操作:t.abs()、 开方sqrt() 取负neg()
这些操作就可能涉及到广播:
相同维度 不同形状
规则一:确定兼容否。广播的条件是确定两个张量的形状之间的每个维度都兼容才可以进行。~~首先两个张量的维度必须相同(维度不同的话用一个 1 代替较低等级张量的缺失维度),~~判断兼容性是从它们的最后一个维度向前比较,要么相等要么其中有个数是1,如果有任一个维度不满足就不能广播。
规则二:确定广播成什么形状。依旧从最后一个维度开始,两两比较取最大值作为那个维度的数据个数。
张量1与标量1
在上面规则的基础上,因为标量1的维度是0,所以用一个 1 代替标量的缺失维度变成与张量1维度相同的张量2。例如(1,3)和标量5,会把5首先补维成(1,1)再根据之前的规则一和二resize成(1,3)的张量2。直白来说会把标量resize成和张量完全一致的形状
不同维度,不同形状
首先检查两个向量已有的维度是否满足规则一,如果不满足就不能广播,如果已有维度满足,则用1补充缺失维度,再走规则二。
// 2和3不满足规则一,结束判断
(1, 2, 3)
(3, 3)
// 将张量2补成(1,2,3),走规则二,最后结果的形状是(2,2,3)
(2, 1, 3)
(2, 3)
与element-wise operations不同的是reduction operations让我们能够对单个张量内的元素执行运算:
> t = torch.tensor([
[0,1,0],
[2,0,2],
[0,3,0]
], dtype=torch.float32)
#默认是对张量中的所有分量进行操作
> t.sum() # 求元素之和
tensor(8.)
> t.prod() # 求元素乘积
tensor(0.)
> t.mean() # 求元素平均值
tensor(.8889)
> t.std() # 求元素标准差
tensor(1.1667)
# 参数dim=x 可以指定哪个维度上进行reduction operations
> t = torch.tensor([
[1,1,1,1],
[2,2,2,2],
[3,3,3,3]
], dtype=torch.float32)
> t.sum(dim=0)
tensor([6., 6., 6., 6.])
> t.sum(dim=0).shape
torch.Size([4])
> t.sum(dim=1)
tensor([ 4., 8., 12.])
> t.sum(dim=1).shape
torch.Size([3])
Argmax函数会输出张量中值最大的索引号,这个函数经常被用来确定哪个类别的预测值最高
t = torch.tensor([
[1,0,0,2],
[0,3,3,0],
[4,0,0,5]
], dtype=torch.float32)
#无参默认清空下是找整个张量的分量最大值的索引,这个索引号是把张量拉直后的一维数组索引
> t.max()
tensor(5.)
> t.argmax()
tensor(11)
#参数dim=x 选择在哪个维度上找最大值索引
> t.max(dim=0)
(tensor([4., 3., 3., 5.]), tensor([2, 1, 1, 2]))
> t.argmax(dim=0)
tensor([2, 1, 1, 2])
> t.max(dim=1)
(tensor([2., 3., 5.]), tensor([3, 1, 3]))
> t.argmax(dim=1)
tensor([3, 1, 3])
访问和提取张量中的数据
> t = torch.tensor([
[1,2,3],
[4,5,6],
[7,8,9]
], dtype=torch.float32)
> t.mean()
tensor(5.)
> type(t.mean())
torch.Tensor
# mean()的输出结果是一个标量张量,所以需要转换一下
> t.mean().item()
5.0
> type(t.mean().item())
float
#对于输出为含有多个元素的张量时,可以采用 tolist() 将其转为 Python list
> t.mean(dim=0).tolist()
[4.0, 5.0, 6.0]
预处理
数据预处理的目标是将我们正在处理的任何数据转换成能够被神经网络感知的张量
ETL流程:
- 从数据源获取数据
- 将图像数据转换为理想的张量格式
- 将数据加载到模型中
在上述流程中Pytorch提供了Dataset(训练集)和DataLoader两个类,如果要使用 PyTorch 创建自定义的数据集,我们需要通过创建子类并继承 Dataset来实现 扩展,经过格式处理成张量后就可以传递给 DataLoader 对象。
流行数据集已经直接或间接地继承了Dataset,以使用流行数据集Fashion-MNIST为例演示一下ETL流程:
#导包省略
train_set = torchvision.datasets.FashionMNIST(
root='./data' #E
,train=True # 对于FashionMNIST数据集来说,True时表示作为训练集,6万张全部加载;False表示作为测试集,只加载1万张测试集部分
,download=True
,transform=transforms.Compose([ #T
transforms.ToTensor()
])
)
#L
train_loader = torch.utils.data.DataLoader(train_set
,batch_size=10 #默认1
,shuffle=True #在划分batch前先将数据打乱
)
#数据集中的个数
> len(train_set)
60000
#对应标签
> train_set.targets
tensor([9, 0, 0, ..., 3, 0, 5])
#输出数据集中每个类别对应的数据个数
> train_set.targets.bincount()
tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])
#查看训练集Dataset中(具体来说这里是train_set)的单对数据(图像值+标签)
#iter()是python内置函数,功能为返回参数对象的一个迭代器
> sample = next(iter(train_set))
#元祖,相当于不可变值的列表
# tuple( Tensor[1,28,28], int lable )
> type(sample)
tuple
#单对数据的长度
> len(sample)
2
#sample类型终究是个元祖所以也等价于image = sample[0] label = sample[1]
> image, label = sample
#输出元祖中元素的类型,可以不相同
> type(image)
torch.Tensor
> type(label)
int
#查看数据加载器DataLoader中(具体来说这里是train_loader)的数据
#这里的next()不再拿一个数据,而是拿batch_size个
> batch = next(iter(display_loader))
#图像数据和标签分别存在两个张量里面并放入列表返回
# list [ Tensor1 [batch_size, 1, 28, 28] , Tensor2 [batch_size] ]
> type(batch)
list
> print('len:', len(batch))
len: 2
> print('types:', type(images), type(labels))
types: <class 'torch.Tensor'> <class 'torch.Tensor'>
构建神经网络
- 通过扩展 nn.Module 基类的方法,创建一个神经网络子类——整个网络的容器。
- 在类的构造函数( _init_() 函数 )中,使用 torch.nn 包中预构建的层,将需要的网络层定义为类的属性。
- 使用层的属性以及 nn.functional 包中的操作,来定义网络的前向传递( forward() )。
# Network类中继承自Module的属性和方法还有众多
class Network(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
self.out = nn.Linear(in_features=60, out_features=10)
def forward(self, t):
# implement the forward pass
return t
层
有Linear/Conv123d/XxxPool123d 三种,和容器一样都继承自Module类、
# 输入输出通道、核大小、xy步长
> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
# 六个 单通道 5*5 卷积核
> network.conv1.weight.shape
torch.Size([6, 1, 5, 5])
# 全连接层的参数 [out_features, in_features] 注意顺序是颠倒的,与矩阵乘法规则有关
> network.fc1.shape
torch.Size([120, 192])
参数
每层的参数作为属性分别以张量的数据格式存放在各自层中,实际上是一个叫Parameter的类,它是torch.Tensor的子类
#生成某层后它的权重属性对值是背随机初始化的,我们可以指定这层的权重值
> weight_matrix = torch.tensor([
[1,2,3,4],
[2,3,4,5],
[3,4,5,6]
], dtype=torch.float32)
> fc = nn.Linear(in_features=4, out_features=3)
> fc.weight = nn.Parameter(weight_matrix)
# 批量访问各层参数
> for name, param in network.named_parameters():
print(name, '\t\t', param.shape)
前向传播
#初始化一个输入张量
> in_features = torch.tensor([1,2,3,4], dtype=torch.float32)
#初始化一个权重矩阵
> weight_matrix = torch.tensor([
[1,2,3,4],
[2,3,4,5],
[3,4,5,6]
], dtype=torch.float32)
#创建一个全连接层,关闭偏置
> fc = nn.Linear(in_features=4, out_features=3, bias=False)
#将权重矩阵赋给这层的weight属性,否则fc.weight是随机值
> fc.weight = nn.Parameter(weight_matrix)
#前向传播,直接调用对象同名的call函数,参数是前一层的输出值。call函数是继承自Module类的,在其中调用了forword函数,而且Linear类重写了forword所以实际是调用的重写后的方法,然后在forword中又去调用了nn.functional.linear(),这个函数定义了全连接层线性求和的公式,利用tensor.matmul(),但是这些只是初始的线性求和,并不包含激活函数、池化等操作
> fc(in_features)
tensor([30., 40., 50.], grad_fn=<SqueezeBackward3>)
# 当我们new一个Network实例,并想让整个网络前向传播时,就像前面例子一样,network(t)会调用继承自Module的call方法,所以我们需要在Network类中层重写自定义的网络前向传播方式,像搭积木一样顺序组装起所有层的前向传播动作:
def forward(self, t):
# (1) input layer
t = t
# (2) hidden conv layer
t = self.conv1(t) # call the conv1 layer
t = F.relu(t) # activation function
t = F.max_pool2d(t, kernel_size=2, stride=2) # max pooling
# (3) hidden conv layer
t = self.conv2(t) # call the conv2 layer
t = F.relu(t) # activation function
t = F.max_pool2d(t, kernel_size=2, stride=2) # max pooling
# (4) hidden linear layer
t = t.reshape(-1, 12 * 4 * 4) # 卷积层与全连接层交接时需要拉直,因为是批处理要分开每张图的数据,所以拉直后的张量是二维,此时t.shape = [x,y] x是图片个数 y是一张图像的张量经过卷积部分后的大小
t = self.fc1(t) # linear mapping
t = F.relu(t) # activation function
# (5) hidden linear layer
t = self.fc2(t) # linear mapping
t = F.relu(t) # activation function
# (6) output layer
t = self.out(t) # linear mapping
# t = F.softmax(t, dim=1) # not use here
return t
预测
使用流行数据集作为测试集,可以从test_set和test_loader中拿数据前者是单个数据
# 预测单张图片
image,label = next(iter(test_set))
network = Network()
# 编程批量处理的四维张量形式 [1,28,28]—>[1,1,28,28]
predict = network(image.unsqueeze(dim=0))
predict.shape
>tensor.Size([1,10])
predict = F.softmax(predict,dim=1)
predict.argmax()
>tensor(5)
# 批量预测图片
images,labels = next(iter(test_loader))
predict = network(images)
训练数据
1.从训练集中得到一批数据
2.把这批数据传递给网络
3.使用一种选择函数计算损失
4.计算损失函数相对于各个权重的梯度
5.使用梯度更新权值,以减少损失函数值
6.重复步骤1 - 5直到一个epoch被完成
7.重复步骤1-6直到训练了很多个epoch
单批次训练过程
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
#Class Network 网络结构的定义省略...
#导入训练集train_set省略...
#训练集加载器train_loader省略...
#第一步
images,labels = next(iter(train_loader))
#第二步
network = Network()
preds = network(images)
#第三步 这里选择交叉熵损失函数
loss = F.cross_entropy(preds,labels)
loss.item() #调用此函数得到损失函数具体值
#第四步
loss.backword() #调用反向传播函数,自动更新每层的权重属性中的梯度属性:层名.weight.grad 这也是个张量,形状和权重张量的形状,相同因为每个权重都需要对应一个梯度
#第五步
optimizer = optim.Adam(network.parameters(),lr=0.01) #这里使用梯度下降的优化算法Adam,把网络所有权重扔进去,还有Adam需要的超参数
optimizer.step() #更新权重
多批次多周期训练过程
train_loader = torch.utils.data.DataLoader(
train_set,batch_size=100,shuffle=True
)
network = Network()
optimizer = optim.Adam(network.parameters(),lr=0.01) #???
for epoch in range(10): # epoch
total_loss = 0
total_correct = 0
for batch in train_loader:
images,labels = batch
predict = network(images)
loss = F.cross_entropy(predict,labels)
total_loss += loss
total_correct += get_correct_num(predict,labels)
optimizer.zero_grad() #将各层的权重属性的梯度属性值清零
loss.backward()
optimizer.step() #更新梯度,这个函数实际上是+=操作而不是赋值,所以这是optimizer.zero_grad()的原因
print(
'epoch:',epoch,
'total_loss',total_loss,
'total_correct',total_correct
)
TensorBoard
- 可视化和跟踪例如loss和arrcuracy的精度标准
- 可视化计算图
- 查看例如权重,偏置这些张量随着时间变化的柱状图
- 展示图片,文字,或者语音数据
使用这个可视化工具主要是想在runs文件夹下生成一个数据文件,TensorBoard在6006端口等待接受一份数据文件以此来生成指标的可视化内容
相关函数
add_scalar(tag, scalar_value, global_step, walltime)
生成一个二维函数图,每调用一次,就会往对应tag的图中添加一个键值对数据
tag:标题 scalar_value:y轴的值 global_step:全局步长值,也就是x轴的值
add_image(tag, img_tensor, global_step=None, walltime=None, dataformats=‘CHW’)
这个函数类似于add_scalar,每调用一次,就会往对应tag的图中添加一个图像数据,把图像加载进来可通过滑纽查看每张图像
- 参数
- tag: 数据标识名
- img_tensor: 图像输入
- global_step: 全局步长值
- dataformats=‘CHW’:默认输入形状为(3,H,W),但如果输入图像为(H,W,3),(H,W),(1,H,W)之类的形状的话,需要指定
dataformats
参数为HWC,HW,CHW
以正确显示图像
torchvision.utils.make_grid(tensor, nrow=8, padding=2, normalize=False, range=None, scale_each=False, pad_value=0)
将若干幅图像拼成一幅图像
Parameters: | tensor (Tensor or list) – 4D mini-batch Tensor of shape (B x C x H x W) or a list of images all of the same size. nrow (int, optional) – 每一行显示的图像数. 最终图标尺寸为(B / nrow, nrow). 默认为8. padding (int, optional) –图片之间的距离填充. Default is 2. normalize (bool, optional) – If True, 归一化图像到(0, 1)区间, by subtracting the minimum and dividing by the maximum pixel value. range (tuple, optional) – tuple (min, max) where min and max are numbers, then these numbers are used to normalize the image. By default, min and max are computed from the tensor. scale_each (bool, optional) – If True, scale each image in the batch of images separately rather than the (min, max) over all images. pad_value (float, optional) – Value for the padded pixels. |
add_graph(model, input_to_model=None, verbose=False)
添加网络结构图
- 参数
- 网络模型
- 一个输入样本实例
- verbose: 控制是否打印网络结构的开关语句
add_histogram(tag, values, global_step=None, bins=‘tensorflow’, walltime=None, max_bins=None)
添加直方图
- 参数
- values: 一个张量或数组
- global_step:全局步长
- bins: 设定柱状图样式
准备数据
from torch.utils.tensorboard import SummaryWriter
# 如果参数是"logs"——在项目目录下创建一个名为logs的文件然后在里面放入生成的数据文件。但是这样在启动时发现命令没反应的情况,网上提供的方法均无法解决,只能是创建如下分层目录,启动时必须要先切换到logs目录,然后再执行tensorboard --logdir=logTest2
writer = SummaryWriter("logs\logTest2")
# 准备数据
writer.close()
这样的分层目录也方便对比每次跑模型出来的结果
在多批次多周期训练过程中添加捕捉指标数据的代码:
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from torch.utils.tensorboard import SummaryWriter
train_loader = torch.utils.data.DataLoader(
train_set,batch_size=100,shuffle=True
)
network = Network()
optimizer = optim.Adam(network.parameters(),lr=0.01) #???
# -----------------------------------------------------------------------
images,labels = next(iter(train_loader))
grid = torchvision.utils.make_grid(images)
# tb对象用来往指定路径的文件下写入数据
tb = SummaryWriter()
tb.add_images('images',grid)
tb.add_gragh(network,images)
# -----------------------------------------------------------------------
for epoch in range(10): # epoch
total_loss = 0
total_correct = 0
for batch in train_loader:
images,labels = batch
predict = network(images)
loss = F.cross_entropy(predict,labels)
total_loss += loss
total_correct += get_correct_num(predict,labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# -----------------------------------------------------------------------
tb.add_scalar('Accuracy',total_correct/len(train_set),epoch)
tb.add_scalar('Loss',total_loss,epoch)
'''
这种表达方式只能看单个层的偏置,权重,想要看到全部层的需要写很多代码
tb.add_histogram('conv1.bias',network.conv1.bias,epoch)
tb.add_histogram('conv1.weight',network.conv1.weight,epoch)
tb.add_histogram('conv1.weight.grad',network.conv1.weight.grad,epoch)
'''
# 循环遍历出所有层的权重属性名、值、梯度值,然后加入到对应的图表中
for name, weight in network.named_parameters():
tb.add_histogram(name, weight, epoch)
tb.add_histogram(f'{name}.grad', weight.grad, epoch)
# -----------------------------------------------------------------------
tb.close()
如果我们想要把应用了不同超参数的模型的训练结果做对比:
# 其余的一些包省略
# product函数可以求多个可迭代对象的笛卡尔积
from itertools import product
def get_num_correct(preds, labels):
return preds.argmax(dim=1).eq(labels).sum().item()
# 我们把待应用的超参数以及他们的各种取值,以字典形式存放到变量parameters
parameters = dict(
lr = [.01, .001],
batch_size = [10, 100],
shuffle = [True, False]
)
# 这是个列表生成式,字典类型的values()方法可以将字典的所有值以可迭代器类型返回,然后利用列表生成式循环地从可迭代器中取出,生成列表
param_values = [v for v in parameters.values()]
print(param_values) # [ [.01, .001],[10, 100],[True, False] ]
# 星号表示将列表param_values的所有值作为参数而不是其本身
# 这里product是求[.01, .001],[10, 100],[True, False]的笛卡尔积
for lr, batch_size, shuffle in product(*param_values):
network = Network()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True)
# 获取一个批次的随机图像数据
images, labels = next(iter(train_loader))
# 创建能在tensorboard中查看这些即将被训练的图像网格
grid = torchvision.utils.make_grid(images)
comment = f'batch_size={batch_size} lr ={lr} shuffle={shuffle}'
tb = SummaryWriter(comment=comment) # 在Summary Writer添加该注释,可帮助我们在tensorboard中唯一地识别指标图像对应哪些超参的模型
tb.add_image('images', grid) # 将这批图像放在坐标方格中进行显示
tb.add_graph(network, images) # 在tensorboard中看见网络结构的可视化图
optimizer = optim.Adam(network.parameters(), lr=lr)
for epoch in range(5):
total_loss = 0
total_correct = 0
for batch in train_loader: # Get Batch
images, labels = batch
preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate loss
optimizer.zero_grad() # 梯度清零,否则会累加
loss.backward() # Calculate Gradients
optimizer.step() # Update Weights
#total_loss += loss.item()
total_loss += loss.item()*batch_size # 这里上述用的是mini-batch训练方法,一个batch得loss会被平均,所以乘以size得到总和,这样放大后比较更具可视性
total_correct += get_num_correct(preds, labels) # 自定义的一个求准确率的函数
tb.add_scalar("Loss", total_loss, epoch)
tb.add_scalar("Number Correct", total_correct, epoch)
tb.add_scalar("Accuracy", total_correct/len(train_set), epoch)
for name, weight in network.named_parameters():
tb.add_histogram(name, weight, epoch)
tb.add_histogram(f'{name}.grad', weight.grad, epoch)
print("epoch:", epoch, "total_correct:", total_correct, "loss", total_loss)
tb.close()
启动 TensorBoard
# directory_name是保存数据的目录名,默认logs,可以自定义端口号
tensorboard --logdir=directory_name --port=6006
访问端口http://localhost:6006/
优化训练代码
在不同超参数组合的模型训练中我们给原本简洁的训练主体代码部分添加了许多功能,包括使用笛卡尔运算生成超参数的不同组合、初始化SummaryWriter、向tensorboard中写入指标数据、保存权重到文件等等,这使训练循环的主体代码变得非常杂乱,所以接下来通过RunBuilder和RunManager类封装以上操作。
编写RunBuilder类
该类的编写允许我们使用不同的超参数值生成多个运行
from collections import OrderedDict
from collections import namedtuple
from itertools import product
'''
get_runs函数的参数由Training Loop程序给出
params = OrderedDict(
lr = [.01, .001],
batch_size = [1000, 10000],
)
'''
class RunBuilder():
@staticmethod
def get_runs(params):
# namedtuple定义一个新类型Run,它是元祖的子类。第二个参数是元组将具有的属性列表,也就是说突破只能用下标索引访问元祖数据的限制,现在可以用名字去访问。在这里Run实例将被用作保存一组超参数的组合。
Run = namedtuple('Run', params.keys()) # [lr, batch_size]
runs = []
for v in product(*params.values()): # 对超参数字典的值进行笛卡尔积运算,
runs.append(Run(*v)) #将每一种笛卡尔积运算结果放到Run的构造函数中生成一个Run实例,然后添加到runs列表中
return runs
runs = RunBuilder.get_runs(params)
for run in runs:
print(run, run.lr, run.batch_size)
'''Run(lr=0.01, batch_size=1000) 0.01 1000
Run(lr=0.01, batch_size=10000) 0.01 10000
Run(lr=0.001, batch_size=1000) 0.001 1000
Run(lr=0.001, batch_size=10000) 0.001 10000'''
# 创建RunBuilder类以后,comment表示为:
for run in RunBuilder.get_runs(params):
comment = f'-{run}' # 用来说明当前网络使用的超参数情况的注释
print(comment)
编写RunManager类
class RunManager():
def __init__(self):
self.epoch_count = 0
self.epoch_loss = 0
self.epoch_num_correct = 0
self.epoch_start_time = None
self.run_params = None
self.run_count = 0
self.run_data = [] # 里面是一个个的results列表,results里装的是第m组超参的第n个周期的指标
self.run_start_time = None
self.network = None
self.loader = None
self.tb = None
def begin_run(self, run, network, loader):
self.run_start_time = time.time() # 开始一组超参数训练的计时
self.run_params = run # 储存超参
self.run_count += 1 # 第几组超参
self.network = network # 托管网络,self.network在cpu还是gpu上取决于参数
self.loader = loader # 托管数据加载器
self.tb = SummaryWriter(comment=f'-{run}') # 托管TensorBoard书写笔
images, labels = next(iter(self.loader)) # 获取一个批次的随机图像数据
grid = torchvision.utils.make_grid(images) # 生成这个批次的图像网格
self.tb.add_image('images', grid) # 将这批图像放在坐标方格中进行显示
# 生成网络结构图,因为这个函数涉及到数据放到网络里计算了所以要考虑它们所处的位置
self.tb.add_graph(self.network
, images.to(getattr(run,'device','cpu'))) # 默认返回'cpu'
def end_run(self):
self.tb.close()
self.epoch_count = 0
def begin_epoch(self):
self.epoch_start_time = time.time() # 开始一个周期训练的计时
self.epoch_count += 1 # 第几个周期
self.epoch_loss = 0 # 第n个周期的损失值
self.epoch_num_correct = 0 # 第n个周期的正确数
def end_epoch(self):
epoch_duration = time.time() - self.epoch_start_time
run_duration = time.time() - self.run_start_time # 每个run的运行时间也是由end_epoch函数负责刻录的,每次周期结束都需要记录一下本次run走了多长时间
loss = self.epoch_loss/len(self.loader.dataset) # 平均到单个图像的损失值
accuracy = self.epoch_num_correct/len(self.loader.dataset) # 精确度 分类正确的比例
#往tb笔里写入指标数据
self.tb.add_scalar('Loss', loss, self.epoch_count)
self.tb.add_scalar('Accuracy', accuracy, self.epoch_count)
for name, param in self.network.named_parameters(): # 写入权重和梯度
self.tb.add_histogram(name, param, self.epoch_count)
self.tb.add_histogram(f'{name}.grad', param.grad, self.epoch_count)
results = OrderedDict() # 初始化一个字典,除了上面将数据写到tb笔里,我们还希望持久化它们
results["run"] = self.run_count
results["epoch"] = self.epoch_count
results["loss"] = loss
results["accuracy"] = accuracy
results["epoch duration"] = epoch_duration
results["run duration"] = run_duration
# asdict()可将namedtuple创建的元组子类对象直接转化为字典数据
# items()将一个字典以列表形式返回,其中值是可遍历的元组数组:[(键1,值1),(键2,值2)...]
for k,v in self.run_params._asdict().items(): results[k] = v # k是键 v是值,这里想要保存超参到results列表
self.run_data.append(results)
df = pd.DataFrame.from_dict(self.run_data, orient='columns')
''' jupyter中可视化的代码
clear_output(wait=True)
display(df) '''
def track_loss(self, loss):
self.epoch_loss += loss.item()*self.loader.batch_size
def track_num_correct(self, preds, labels):
self.epoch_num_correct += self._get_num_correct(preds, labels)
@torch.no_grad()
def _get_num_correct(self, preds, labels): # 这里的参数是张量
return preds.argmax(dim=1).eq(labels).sum().item() # 返回预测正确的数目
def save(self, fileName):
pd.DataFrame.from_dict(
self.run_data,
orient='columns').to_csv(f'{fileName}.csv') # Excel可打开
with open(f'{fileName},json','w', encoding='utf-8') as f:
json.dump(self.run_data, f, ensure_ascii=False, indent=4)
Training Loop
# 使用RunManager和RunBuilder类可以使得程序更易扩展
params = OrderedDict(
lr = [.01],
batch_size =[1000, 2000],
#shuffle = [True, False]
#device = ['cuda','cpu']
#num_workers = [0,1,2,4,8,16]
)
m = RunManager() # 1
for run in RunBuilder.get_runs(params): # 构建一个个超参组合,循环训练
# 创建网络模型实例、数据加载器实例、优化器实例
device = torch.device(run.device)
network = Network().to(device)
loader = DataLoader(train_set, batch_size=run.batch_size, shuffle=run.shuffle, num_workers=run.num_workers)
optimizer = optim.Adam(network.parameters(), lr=run.lr)
m.begin_run(run, network, loader) # 2 托管超参、网络模型、加载器、tb笔,可视化网络结构和一些图像
for epoch in range(5):
m.begin_epoch() # 3
for batch in loader:
# 网络的一个来回:前传、计算损失、梯度清零、梯度下降更新梯度、更新权重
images, labels = batch
images = images.to(device) # 将图像张量和标签数据移动到超参列表中指定的设备上
labels = labels.to(device)
preds = network(images)
loss = F.cross_entropy(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
m.track_loss(loss) # 4 累计出一个周期的损失值
m.track_num_correct(preds, labels) # 5 累计出一个周期的正确数
m.end_epoch() # 6 做的事情最多因为记录的最小单位是epoch
m.end_run() # 7 关闭tb笔 清零周期计数器
m.save('resuls') # 8
多进程加速神经网络训练
- 使用data loader类的num_workers可选属性可加速神经网络的训练
- num_workers属性告诉data loader实例有多少个单元处理器用于数据加载,本质是省去了CPU等待数据加载到内存的时间,所以当数据加载进程数到达一定程度后CPU满负荷,加速就不会有效果了
- num_workers值的选择的最好方式是进行试验
params = OrderedDict(
lr = [.01],
batch_size =[1000, 2000],
shuffle = [True, False],
#device = ['cuda','cpu']
num_workers = [0,1,2,4,8,16] # 尝试不同的取值
)
m = RunManager()
for run in RunBuilder.get_runs(params):
device = torch.device(run.device)
network = Network().to(device)
loader = DataLoader(train_set, batch_size=run.batch_size, shuffle=run.shuffle, num_workers=run.num_workers)
optimizer = optim.Adam(network.parameters(), lr=run.lr)
m.begin_run(run, network, loader)
for epoch in range(1): # 只设置1个周期的训练即可反应出加速的效果
m.begin_epoch()
for batch in loader:
images, labels = batch
images = images.to(device)
labels = labels.to(device)
preds = network(images)
loss = F.cross_entropy(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
m.track_loss(loss)
m.track_num_correct(preds, labels)
m.end_epoch()
m.end_run()
m.save('resuls')
数据集规范化
流行数据集+单通道数据:
# 没有经过归一化的流行数据集
train_set = torchvision.datasets.FashionMNIST(
root='./data'
,train=True
,download=True
,transform=transforms.Compose([
transforms.ToTensor()
])
)
# 以下这部分操作是想得到数据集的均值和标准差来当做参数传给数据加载器,因为数据加载器有一个功能可以做到归一化
# 先放到数据加载器中转化成张量才可以调求均值标准差函数,上面的train_set是个FashionMNIST类型的对象,不可以直接求
loader = DataLoader(train_set, batch_size=len(train_set), num_workers=1)
# 此时data是个列表,放了两个张量 一个数据一个标签
data = next(iter(loader))
# 因为这个流行数据集是黑白一通道,所以直接拿出来全部求平均和标准查,多通道的话就需要每个通道分别求了
mean = data[0].mean()
std = data[0].std()
mean, std
# 归一化的流行数据集
train_set_normal = torchvision.datasets.FashionMNIST(
root='./data'
,train=True
,download=True
,transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean, std)
])
)
批量归一化:
network2 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(6), # BN为什么要放在这一层的最后一步?
nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2), # 这里没有BN,所以在哪层归一化怎么确定?
nn.Flatten(start_dim=1),
nn.Linear(in_features=12*4*4, out_features=120),
nn.ReLU(),
nn.BatchNorm1d(120),
nn.Linear(in_features=120, out_features=60),
nn.ReLU(),
nn.Linear(in_features=69, out_features=10)
)
网络权重的处理
重置网络权重
Sequential创建的网络才可以用下标访问每个层 例如network[n]
children返回网络所有层的可迭代集合
for layer in network.children():
print(layer)
'''
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
Linear(in_features=192, out_features=120, bias=True)
Linear(in_features=120, out_features=60, bias=True)
Linear(in_features=60, out_features=10, bias=True)
'''
随机数种子
生成网络实例时其中的参数默认会被赋予一个随机值,在对比训练过程中如果希望每组超参开始训练时网络参数初始化成相同值,可以在前一步添加一个随机数种子
import torch
number = 1
# 为cpu中设置种子,number是设置随机数种子,一个整数值
# gpu用这个:torch.cuda.manual_seed(number)
torch.manual_seed(number)
print(torch.rand(1)) # 参数是想要生成随机数据的形状
# 与上一次生成的结果相同,随机数种子就像一个标号,凡是在这个标号出现后的第一次生成随机值时都会根据这个种子生成一个固定的值
torch.manual_seed(number)
print(torch.rand(1))
# 与前两次结果不一样,因为没有设置种子
print(torch.rand(1))
加载保存网络权重
import torch
from torchvision.models import resnet18
# 设置预训练参数为True可以将官方的权重值作为初始化值
model = resnet18(pretrained=True)
# 保存模型的全部
PATH = "./model.pth" # 可以是pth,pt,pkl后缀文件名
torch.save(model, PATH)
# =======加载模型=======
PATH = "./model.pth" # 路径
model = torch.load(PATH)
model.state_dict()来为每一层和它的参数建立一个映射关系并存储在字典中,其键值由每个网络层和其对应的参数张量构成。
import torch
from torchvision.models import resnet18
network = resnet18(pretrained=True)
# 保存模型的state_dict
PATH = "./network.pth"
torch.save(network.state_dict(), PATH)
# =======加载模型=======
# 1. 定义模型
model = resnet18()
# 2. 加载状态字典进内存
state_dict = torch.load(PATH)
# 3. 将状态字典中的可学习参数加载到模型上使用
model.load_state_dict(state_dict)