PyTorch
Pytorch框架的一大好处就是我们可以直接调用Pytorch规定的一套架构,用户自己去选择完成整个深度学习任务的积木去堆积,避免了每次都要从底层写代码,并且框架下的函数都是多次优化过的,可以说框架的出现节约了时间成本
(Note:这篇博客是很早之前写的,忘了发了也没仔细去检查,所以如果有出现小错误,请指正)
PyTorch由4个主要包装组成:
1.Torch:类似于Numpy的通用数组库,可以在将张量类型转换为(torch.cuda.TensorFloat)并在GPU上进行计算。
2.torch.autograd:用于构建计算图形并自动获取渐变的包
3.torch.nn:具有共同层和成本函数的神经网络库
4.torch.optim:具有通用优化算法(如SGD,Adam等)的优化包
一、数据下载与读取
1.1、数据下载
用torchvision.datasets下载FashionMNIST数据集
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=down_load, transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=down_load, transform=transform)
用torchvision.datasets下载MNIST数据集
mnist_train = torchvision.datasets.MNIST(root=root, train=True, download=down_load, transform=transform)
mnist_test = torchvision.datasets.MNIST(root=root, train=False, download=down_load, transform=transform)
mnist_train、mnist_test为torch.utils.data.Dataset的子类,故可以直接用torch.utils.data.DataLoader直接导入
root为第一次下载的地址或者第二次以后读取的地址(内部实现不是pickle)
root='~/Datasets/FashionMNIST'
# 或者 root='./FashionMNIST',即保存在当前目录下
transform = transforms.ToTensor() # 将所有数据转为Tensor
DOWNLOAD_MNIST = False
if not(os.path.exists('./mnist_ton/')) or not os.listdir('./mnist_ton/'): # 检测path是否存在或者指定目录是否包含文件
# not mnist dir or mnist is empyt dir
DOWNLOAD_MNIST = True
用DOWNLOAD_MNIST来控制第二次之后就别下载了。
注意:root要包含到MNIST,而不是只到mnist_ton,否则会报错:
train_iter, test_iter = d2l.load_data(100, DOWNLOAD_MNIST, root='./mnist_ton)
train_iter, test_iter = d2l.load_data(100, DOWNLOAD_MNIST, root='./mnist_ton/MNIST')
如果要读取的数据是网上的图片咋办?
train_imgs = torchvision.datasets.ImageFolder(os.path.join('.', 'hotdog/train'), transform=transform)
记得先把网上下载的解压,最后用torch.utils.data.DataLoader读入(用法下面有)
torchvision.transforms常用于做一些图片变换,例如裁剪、旋转等。transforms.ToTensor() 将尺⼨为 (H x W x C) 且数据位于 [0, 255] 的 PIL 图⽚或者数据类型为 np.uint8 的 NumPy 数组转换为尺寸为 (C x H x W) 且数据类型为 torch.float32 且位于 [0.0, 1.0] 的 Tensor 。
常规操作:
transform = transforms.Compose([
transforms.RandomResizedCrop(size=224), # 裁剪成固定大小
transforms.RandomHorizontalFlip(), # 水平翻转
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 将rgb三通道做标准化
])
这里读取的数据mnist_train、mnist_test格式为:[(X1,y1),(X2,y2)...] ,
其中X1:(1, 28, 28),表示1通道(灰度图)分辨率为28*28的图像
y:Tensor标量,表示标签,如tensor(9),他其实对应了查找表中的 'ankle boot'。
1.2、Pytorch提供data包来读取数据
import torch.utils.data as Data #读取数据集
#读入批量数据
# features:(m, n) labels:(m, 1)
# 实例化Dataset类
dataset = Data.TensorDataset(features, labels)
#随机读取batchsize个数据
data_iter = Data.DataLoader(dataset, batchsize, shuffle=True)
还就可以选择加速度取:
# 采用4个线程加速数据读取
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(train_imgs,
batch_size=batchsize, num_workers=num_workers, shuffle=True)
note:
train_iter格式: [(X1,y1),(X2,y2)...] X1:(batch, 1, 28, 28) y:(batch,),标签不是one-hot形式
torch.utils.data.DataLoader
- 功能:构建可迭代的数据装载器;
- dataset:Dataset类,决定数据从哪里读取及如何读取;
- batchsize:批量值;
- num_works:是否多进程读取数据;
- shuffle:每个epoch是否乱序;
- drop_last:当样本数不能被batchsize整除时,是否舍弃最后一批数据;
torch.utils.data.Dataset
- Dataset是用来定义数据从哪里读取,以及如何读取的问题;
- 功能:Dataset抽象类,所有自定义的Dataset需要继承它,并且复写__getitem__();
- getitem:接收一个索引,返回一个样本
1.3、对于一些非图片的数据集怎么读入呢?如嘴唇识别和兵王问题
法一:
注意:一般都要做一些预处理,如字符转为数字、自己去打乱数据集(shuffle、panument)、归一化。。。
这就要分是否批处理了
如果要批处理:(见嘴唇识别的自己版本nn)
功能就是将数据集打乱,然后分批装入
def data_iter(batch_size, features, labels, shuffle=True):
num_examples = len(features)
indices = list(range(num_examples))
if shuffle:
random.shuffle(indices) # 样本的读取顺序是随机的
else:
indices = indices
for i in range(0, num_examples, batch_size):
j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)]) # 最后一次可能不足一个batch
yield features.index_select(0, j), labels.index_select(0, j)
可通过列表内批数据的处理方法(仿照1.2)对data_iter输出进行整洁包装:
for X_train, y_train in data_iter(batchsize, trainingdata, traininglabels):
train_iter.append((X_train, y_train)) # 写法一
test_iter = [(X_test, y_test) for X_test, y_test in data_iter(batchsize, testingdata, testinglabels)] # 写法二
输出数据格式:[(X,y),(),().....] X:(batch,n), y:(batch,)
小结:
由1.1-1.3可见,一般的数据集都是 X:(batch,n), y:(batch,)这样的形式。
法二:
利用sklearn库的model_select和preprocessing(见兵王和嘴唇识别的老师版本的nn)
如果不分批处理,那就简单了,老老实实把输入改成X:(batch,n), y:(batch,)这个样子就好了,具体可参考svm版本的嘴唇识别和兵王问题。
1.4、还有一种基于pickle读取图片的方式(从深度学习入门那本书上看到)
可以看E:\python\anaconda\tj_work\deep_learning_0\dataset文件中的mnist.py
这种方式可以选择输出为one-hot格式,还可以归一化输入啊等,挺全的。
1.5、输入图片大小的改变
二、torch.nn模块
该模块定义了大量神经网络的层
核心数据类:nn.Module
表示nn中的某个层或者网络
实际中用于被继承,然后去写自己的网络/层
1、层的介绍(都是nn.Module的子类)
1.1、全连接层(仿射层)
关于这个的介绍:Pytorch入门之一文看懂nn.Linear_Ton的博客-CSDN博客
1.2、激活函数层
1.3、Dropout层
1.4、卷积层
卷积其实是一个线性运算,特征图是特征X,卷积核是参数W,很多框架都是由im2col函数去做卷积的。由于池化也可以理解为线性运算,故为了避免网络“表现力欠缺”,应该在卷积核池化之间加入非线性元素Relu激活函数。
note:根据线性运算的实质,反卷积就好理解了。反卷积的另一种做法是最大邻域插值。
Conv2类详见:pytorch之torch.nn.Conv2d()函数详解_夏普通-CSDN博客_torch.nn.conv2d
有关CNN和6大主流CNN网络的介绍见:Pytorch入门之CNN和七大CNN网络_Ton的博客-CSDN博客
1.5、池化层
1.6、批量归一化层
2、nn中常用的工具介绍
2.1、nn.Module,通过继承他,来实现层、模型
- pytorch中所有的层结构和损失函数都来自于torch.nn
- 所有的模型结构都是从nn.Module继承的
2.1.1、nn.Sequential
Squential是个有序的容器,网络层将按照传入该容器的顺序依次加入,用[]来访问任意一层
添加方式:
net = nn.Sequential() # 创建空的容器
net.add_module('lin1', nn.Linear(2, 1))
net.add_module('lin2', nn.Linear(3, 2))
# net.add_module....
print(net)
print(net[0])
print(net[1])
# 有序容器,等效于nn.Sequential的源码
class MySequential(nn.Module):
from collections import OrderedDict
def __init__(self, *args):
super(MySequential, self).__init__()
if len(args) == 1 and isinstance(args[0], OrderedDict): # 如果传入的是一个OrderedDict
for key, module in args[0].items():
self.add_module(key, module) # add_module方法会将module添加进self._modules(一个OrderedDict)
else: # 传入的是一些Module
for idx, module in enumerate(args):
self.add_module(str(idx), module)
def forward(self, input): #据此可代替类中的forward函数
# self._modules返回一个 OrderedDict,保证会按照成员添加时的顺序遍历成
for module in self._modules.values():
input = module(input) #这种写法最好是nn.Module的子类
return input
用法:
net = MySequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
print(net)
net(X)
输出
2.1.2、ModuleList:接收子模块列表作为输入,像对列表一样访问
net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
net.append(nn.Linear(256, 10)) # 类似List的append操作
print(net[-1]) # 类似List的索引访问
print(net)
输出
2.1.3、ModuleDict:接收子模块的字典作为输入,像对字典一样访问
net = nn.ModuleDict({
'linear': nn.Linear(784, 256),
'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10) # 添加
print(net['linear']) # 访问
print(net.output)
print(net)
2.1.4、2.1.1、2.1.2、2.1.3与继承nn.Module的自定义模型的组合能增加模型的灵活性-----嵌套组合
net = nn.Sequential(NestMLP(), nn.Linear(30, 20), FancyMLP())
print(net)
输出
2.1.5、用于CNN
复杂网络构建由大容器、小容器、模块(一般都是自己编写的复杂模块)、层(自己编写的层或者nn.Moudule子类层)组合而成的,具体的组成方法见Pytorch入门之CNN和七大CNN网络_Ton的博客-CSDN博客
还有一种,可以定义一个函数def,在函数里面调用容器去做网络。
输出:squentia内层的标号默认是0、1、2.....,可以自己改名字,用add_module来编写
2.2、net.parameters()
用于查看模型所有的可学习参数,parameters、named_parameters()是nn.Module的方法,以生成器方式返回,不占内存,调用时候才访问。容器内参数都是一个个张量。
net = Critic(4, 2)
print(net.parameters())
for i in net.parameters(): # i是Parameter类对象
print(type(i))
输出:
<generator object Module.parameters at 0x0000022E599DF0C8>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
<class 'torch.nn.parameter.Parameter'>
net_dropped是有序容器对象
for param in net_dropped.parameters():
print(param)
for name, param in net.named_parameters():
print(name, param.size())
输出:前面的数字表示参数所在的层编号
或者用state_dict查看:从参数名称到tensor的映射,而不像上面从层数编号到tensor的映射
使用对象,含有卷积全连接层、优化器optimizer的可学习参数。
返回字典
对于优化器 :
可以看出来PyTorch的优化器参数包括2部分,一个是param_groups,这是一个列表,里面只有一个元素,是一个字典,字典里是模型参数、学习率等信息。另一个是优化器状态,一般不用管。
模型的参数类型是torch.nn.parameter.Parameter,是Tensor的子类,如果一个Tensor是Parameter,那么他会被自动添加到模型的参数列表,且required_grad自动设为True。其实例化对象是一个Tensor的子类。
2.3、模型初始化
深度神经网络训练的时候,采用反向传导的方式,其背后的本质是链式求导,计算每层梯度的时候会涉及到一些连乘操作。每一层的残差都由后一层的残差乘以两层之间的权重矩阵,再乘以当前层的激活函数的导数得到。因此,神经网络权重的初始化至关重要,不当的初始化可能会带来梯度消失或者梯度爆炸。
当网络过深,如果连乘的因子大部分小于1,最后乘积可能趋于0;另一方面,如果连乘的因子大部分大于1,最后乘积可能趋于无穷。这就是所谓的梯度消失与梯度爆炸。
防止因权重初始化不当带来的梯度爆炸:
(1) 使用Xavier初始化法或者MSRA初始化法,使得在深度网络的每一层,激活值都有很好的分布。
(2) 使用预训练模型,初始化已有网络层的权重。
残差的概念:
残差其实是对z的偏导数。对于一个神经元,它和上一层的很多神经元相连接,这些神经元的输出经过一个加权,然后相加的结果就是Z,也就是说这z是神经元的真正输入,残差表示的就是最终的代价函数对网络中的一个个神经元输入的偏导。残差体现的是对于代价的贡献的敏感程度,对于一个大的残差,稍微给点输入,就不行了,导致最后的lost很大。z又是关于权重w的函数,所以,按照链式法则可以传递到w对代价函数的贡献敏感度上。
2.3.1调用init模块
Pytorch中关于初始化的详细介绍可参考:pytorch系列 -- 9 pytorch nn.init 中实现的初始化函数 uniform, normal, const, Xavier, He initialization_墨流觞的博客-CSDN博客
from torch.nn import init # 初始化参数模块
#初始化参数w b
init.normal_(net[0].weight, 0, 0.01) # 服从均值0,方差0.01的正太分布
init.constant_(net[0].bias, 0) #填充函数
optimizer = optim.SGD(net.parameters(), lr=0.03) #例化对象
关于Init.constant介绍,转载一篇torch.nn.init.constant_()函数_Wanderer001的博客-CSDN博客
其内部实现(注意不能加入到计算图中):
因此,我们自定义初始化参数的时候,也要注意不要将参数的初始化加入到计算图中,否则会影响第一次的梯度运算:
或者用data或者detach,而不用torch.no_grad()
2.3.2、nn.Module的子模块
Pytorch中nn.Module的模块参数采取了较为合理的初始化策略,不同类型的layer采用不同的策略。但还是有点问题,比如目前发现全连接层的初始化方式为标准初始化,但其实如果用Relu的话,应该进行修改,变成He更好,可以调用torch.nn.init里的函数。
2.3.3、Xavier、He随机初始化
参考:Xavier初始化和He初始化_xxy_的博客-CSDN博客_he初始化
深度学习中神经网络的几种权重初始化方法_天泽28的专栏-CSDN博客_权重初始化
对于激活函数是Relu,就用He,如果sigmoid或者tanh,用Xavier初始化,这是目前最好的实践。
leakyrelu以及四种需要掌握得激活函数:pytorch中的 relu、sigmoid、tanh、softplus 函数_2021乐乐的博客-CSDN博客,其中softplus可能不太熟悉,他就是relu的平滑版本。
2.4、梯度清零
两种zero_grad源码:
# 写法一
def zero_grad(self):
r"""Clears the gradients of all optimized :class:`torch.Tensor` s."""
for group in self.param_groups:
for p in group['params']:
if p.grad is not None:
p.grad.detach_()
p.grad.zero_()
# 写法二
def zero_grad(self):
"""Sets gradients of all model parameters to zero."""
for p in self.parameters():
if p.grad is not None:
p.grad.data.zero_()
两种梯度清零方式
方法一:
optimizer.zero_grad()
方法二:
network.zero_grad()
注意:关于源码中画红线的地方,相信有一部分同学会有疑惑,即可以直接p.grad.zero_()就行,为啥还要先用detach将p.grad脱离出计算图,切断被跟踪(p.grad.data.zero_()一样的效果)。如果说p.data或者p.detach_()还可以理解,意思就是防止影响到后向传播(梯度清零之后就是后向传播了)。那为啥梯度还要防止被追踪呢?
原因其实和p也有关。假如省略了detach的操作,那么p.grad的requires_grad属性是True,清零还是正常清零。关键就是接下来的优化器操作,我们以SGD为例,打开SGD的step函数源码:
def step(self, closure=None):
...
...
for p in group['params']:
if p.grad is None:
continue
d_p = p.grad.data
...
...
p.data.add_(-group['lr'], d_p)
...
我截取了一部分来说明,从step函数中可以看到,梯度是要传入p.data.add_(-group['lr'], d_p)来计算的,由于基于我们之前的假设,d_p是个被追踪的张量(这里又用了data来隔绝跟踪,意思就和detach一样),虽然p.data用来隔绝跟踪,但由于d_p的存在,它与learning rate的乘积全程被记录下来,又因为划红线的式子是个对p.data的in-place操作,即实际是对自己操作,因此该加法操作又将使得p被跟踪(尽管用了data隔绝也没用,因为requires_grad有个规则:只有当所有的“叶子变量”,即所谓的leaf variable都是不可求导的,那函数y才是不能求导的。)一旦p的行为被跟踪,那么就会影响到下一次的反向传播,关于为什么这个会被影响,这一点大家都理解吧,就不展开了。因此在梯度清零中,必须要进行隔绝。
2.5、共享模型参数
法1:Module类中的forward函数中多次调用同一个层
法2:Sequential中的同一个Module实例
2.6、自定义层(含参数),可以用这些自定义的层构造模型
首先,参数一定要定义为Parameter类
法1:直接定义为Parameter实例
法2:用ParameterList接受一个以Parameter实例为列表元素的列表,最后返回一个参数列表
class MyListDense(nn.Module):
def __init__(self):
super(MyListDense, self).__init__()
self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
self.params.append(nn.Parameter(torch.randn(4, 1)))
def forward(self, x):
for i in range(len(self.params)):
x = torch.mm(x, self.params[i])
return x
net = MyListDense()
print(net)
输出:
法3:ParameterDict接收Parameter实例的字典作为输入,返回一个参数字典
class MyDictDense(nn.Module):
def __init__(self):
super(MyDictDense, self).__init__()
self.params = nn.ParameterDict({
'linear1': nn.Parameter(torch.randn(4, 4)),
'linear2': nn.Parameter(torch.randn(4, 1))
})
self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增
def forward(self, x, choice='linear1'):
return torch.mm(x, self.params[choice])
net = MyDictDense()
print(net)
输出
2.7、如何自己编写backward()
继承torch.nn.autograd.Function
参考:探讨pytorch中nn.Module与nn.autograd.Function的backward()函数 - 知乎
3、损失函数的介绍
定义在torch.nn中,是nn.Module的子类
3.1、平方误差函数
loss = nn.MSELoss(reduction='mean'),默认为均方误差
l = loss(x, y)
官方文档如上图,只要输入输出size一致,且为float,那么输出就是个 Tensor标量,可以用Tensor.item转为python数字
loss1 = nn.MSELoss(reduction='mean') # 均方误差
loss2 = nn.MSELoss(reduction='sum') # 平方误差之和
loss3 = nn.MSELoss(reduction='none') # 平方误差
Note:输入的2个Tensor浮点类型必须保持一致,不能一个是torch.float64,另一个是torch.float32
3.2、交叉熵函数
loss = nn.CrossEntropyLoss()
l = loss(y_hat, y).sum()
note:
1、这个函数是pytorch提供的一个包括softmax运算和交叉熵损失计算的函数,他的数值稳定性比分开定义softmax运算和交叉熵损失函数更好
2、这个函数= softmax+log+NLLLoss,
NLLLoss相当于将softmax结果求取log后,根据标签挑选出一个列向量,然后求取平均值
x = torch.tensor([[-0.1342, -2.5835, -0.9810], [0.1867, -1.4513, -0.3225], [0.6272, -0.1120, 0.3048]])
y = torch.tensor([0, 2, 1], dtype=torch.long)
sm = nn.Softmax(dim=1)
z = sm(x)
o = -torch.log(z.gather(1, y.view(-1, 1))).mean()
nl = nn.NLLLoss() #已经包括求取均值
p = nl(z.log(), y)
loss = nn.CrossEntropyLoss()
l = loss(x, y)
3、最后的结果l是个Tensor类的标量
4、y必须为torch.long型
5、注意y_hat与y的格式:
6、解决softmax上下溢问题:
利用softmax解决数值上溢和下溢_Im_Chenxi的博客-CSDN博客
7、误差反向传播推到:
关于熵的计算:熵计算公式_ljz2016的博客-CSDN博客_熵计算公式
相对熵(KL散度)计算过程_藏知阁-CSDN博客_kl散度计算
3.3、L1Loss 和 smooth Loss
详见:回归损失函数1:L1 loss, L2 loss以及Smooth L1 Loss的对比 - Brook_icv - 博客园
pytorch文档:SmoothL1Loss — PyTorch 1.10.0 documentation
3.4
Focal_loss:参考Focal Loss理解 - 三年一梦 - 博客园
IOU loss:参考目标检测中的回归损失函数系列二:IoU Loss_梦坠凡尘-CSDN博客_iou loss
BCEloss:使用nn.BCELoss需要在该层前面加上Sigmoid函数,用于二分类问题。和nn.Crossentropyloss处理二分类问题是不一样的。或者用
nn.BCEWithLogitsLoss()
# 结合了sigmoid和BCE,别他两单独用更加稳定
注意:y_hat与y的格式:
其中input为(3,),float32
target为(3,),float32,而不能是long
4、nn中优化算法介绍
梯度下降算法包括批量梯度下降(batch GD,利用全部样本)、随机梯度下降(单样本)、minibatch梯度下降(minibatch个样本)
上图是batch GD,因为每次要求和所有样本,而求和是最花费时间的,而且要先存储那么多样本,耗费资源,但是优点在于,他会直接奔向全局最小的方向。
上图是SGD,单样本更新,速度快,但是方差大,不稳定,虽然会弯弯曲曲有时还向着不好的方向,但大致走向全局最小的方向,不同于batch GD,它只能在靠近全局最小的周围回环曲折,但是对于一般工程足够了。
minibatch是我们一直在用的,介于两者之间,是速度和更新稳定性、方向准确性的折中。稳定性主要是来源于其是minibatch个均值,均值会使得整个值相对平均,抖动小,方差小。
至于在pytorch中如何选择上述这三种呢?因为我们一般都是默认reduction='mean',输入是minibatch,所以就意味着默认是minibatch GD的。如果输入是一个样本,那么就是SGD,关键看输入形式。
nn的优化算法定义在torch.optim模块中,Pytorch提供了SGD、Adam、RMSProp等优化算法
4.1、SGD:随机梯度下降(常用mini-batch形式)
调用格式:
optimizer = optim.SGD(net.parameters(), lr=0.03) #例化对象
# 相关重要语句
loss.backward() # 求梯度,求关于变量的梯度,结果是产生一个梯度(向量或者矩阵,取决于参数W是向量还是矩阵)
optimizer.step() # 参数更新
打印SGD,显示的是参数空间的信息
关于SGD的详细介绍:Pytorch入门之一文看懂SGD_Ton的博客-CSDN博客
为子网络设置不同学习率的方式,finetune常用
params参数可以是个list或者生成器
optimizer =optim.SGD([
# 如果对某个参数不指定学习率,就使用最外层的默认学习率
{'params': net.subnet1.parameters()}, # lr=0.03
{'params': net.subnet2.parameters(), 'lr': 0.01}], lr=0.03)
调整学习率
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
note:param_groups的话看过我上面这个文章应该就知道是什么
梯度:用表示,用Tensor.grad查看。梯度在一维变量下就是斜率。
梯度总是向着变化率最快的方向且是函数增长最快的方向,而不是下降最快的方向,这也是梯度下降法为啥前面是“-”号。梯度的模是方向导数的最大值,方向导数探究的是沿哪个方向的变化率最大,即斜率最大。
“-”:梯度下降
“+”:梯度上升
梯度的方向也是等高线切线法线的方向:梯度方向与等高线方向垂直的理解_bitcarmanlee的博客-CSDN博客_梯度等高线
为啥是增长最快的方向:具体看这篇的结尾总结部分:机器学习--什么是梯度?为什么梯度方向就是函数上升最快的方向?本文将给你解惑_进击的菜鸟-CSDN博客_什么是梯度
以上是从整体梯度来看的,梯度其实是个向量,或者矩阵,其内部每一个值都是关于某个单变量的偏导数。
梯度的反方向是函数下降最快的方向,参考这里:梯度反方向是函数下降最快的方向
4.2、动量法
引入momentum的原因:
功能:用于将相邻时间步参数的更新方向趋于一致。
设时间步的自变量为x,学习率为,动量法创建速度变量v,并初始化为0。
gt为小批量随机梯度下降。
背后数学原理:指数加权平均:
Pytorch实现:
只需要指定momentum
optimizer = optim.SGD(net.parameters(), lr=0.03, momentum=0.5) #例化对象
4.3、Adamgrad
SGD和moment的学习率是固定的,不是自适应的,且对于每个参数,都是同步进行变化的。
注意 时间步为0时候的s需要为0
优缺点:
学习轨迹:
当lr设置较小:
当lr设置较大:
pytorch实现:
4.4、RMSprop
注意:时间步为0时候的s需要初始化为0
轨迹,当lr=0.4
左图RMSprop,右图是lr=0.4时候的Adamgrad
可见,RMSprop虽然在后期收敛步伐越来越小,但是其收敛速度更快。
Pytorch实现:
4.5、Adam
注意:时间步为0时候的v和s都需要初始化为0
Pytorch实现:
optimizer = optim.Adam(net.parameters(), lr=0.03) #例化对象
4.6、Nesterov momentum
4.7、RMSProp+Nesterov momentum
需要使用momentum的alpha超参数
note:
倒数第三行可以加个很小的1e-5的小参数,防止出现Nan。
5、激活函数
多个仿射变换的叠加仍然是一个仿射变换。解决办法是引入非线性变换函数,即激活函数
5.1、Relu函数
恒等函数Relu调用方式 Tensor.Relu
5.2、sigmoid函数
恒等函数sigmoid调用方式 Tensor.sigmoid()
5.3、tanh函数(双曲正切)
恒等函数tanh调用方式 Tensor.tanh()
5.4、softmax函数
非恒等函数,存在于nn.CrossEntropyLoss()中,用于分类问题的最后一层,自己实现如下
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True)
return X_exp / partition
6、优化手段
6.1、正则化,用于克服过拟合,原理见笔记
目的:衰减权重W
措施:
在实例化优化器的地方通过优化器的“weight_decay”参数来制定权重衰减超参数limda。
默认下,pytorch对权重和偏差会同时衰减,故需要对权重和偏差分别构造优化器,因为我们一般只对权重W衰减,而不对bias衰减。
#构建优化器
optimizer_w = torch.optim.SGD(params=[net.weight], lr=0.3, weight_decay=3)
optimizer_b = torch.optim.SGD(params=[net.bias], lr=0.3)
...
...
...
#训练过程中的梯度清零以及参数更新
l = loss(net(X), y).mean()
optimizer_w.zero_grad()
optimizer_b.zero_grad()
l.backward()
optimizer_w.step()
optimizer_b.step()
最后的效果:(因为没必要实际去做,就用了花书上的结果)
limda=0:
训练后:w0的L2范数:15.68
limda=3
训练后w0的L2范数:0.035
可见w0的减小,使得测试集误差有所下降,减小了过拟合。
note:
1、权重衰减通常会将w减弱到趋近于0
2、关于过拟合的定义,有4个,见笔记本
3、一般,训练集样本过少,比模型参数更少时,会体现模型很复杂,引起过拟合。
4、泛化误差不会随着训练数据集的增大而增大。
5、很多因素会导致欠拟合和过拟合,重点是模型复杂度和训练集大小。
6、对于复杂度,要适中最好;深度学习这种模型参数较多的,我们希望训练集越大越好
6.2、dropout
可行性:按一定概率P丢弃一定数量的神经元(其实就是归零)后,输入的期望值不改变,且降低了模型的复杂度
# 以drop_prob的概率丢弃X中的元素
def dropout(X, drop_prob):
X = X.float()
assert 0 <= drop_prob <= 1
keep_prob = 1 - drop_prob
if keep_prob == 0:
return torch.zeros_like(X)
mask = (torch.randn(X.shape) < keep_prob).float()
return mask * X / keep_prob
反向传播的过程和Relu的反向传播一样的。
实现方法:在全连接层后加入Dropout层,一般在靠近输入层的丢弃概率设小一点。
net_dropped = torch.nn.Sequential(
torch.nn.Linear(1, N_HIDDEN),
torch.nn.Dropout(0.2), # drop 50% of the neuron
torch.nn.ReLU(),
torch.nn.Linear(N_HIDDEN, N_HIDDEN),
torch.nn.Dropout(0.5), # drop 50% of the neuron
torch.nn.ReLU(),
torch.nn.Linear(N_HIDDEN, 1),
)
在训练时候用,验证和测试时不用,故需要:
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item() # 测试
net.train() # 改回训练模式
6.3、Batch Nomalization(BN):对特征维度进行操作,还有一种是对参数W初始化的时候也会标准化,比如torch.util.nomal_。
Note:对于和输入也可以做Nomalization,可以用sklearn库的preproceing。
关于BN的反向推到设计:Batch Normalization学习笔记及其实现 - 知乎
如果说对于参数而言,优化的方式有梯度法、正则化、dropout,那么对于神经元,则是BN算法了。
BN算法可用于对输入神经元或者网络中的隐藏层神经元处理,处理的目的就是使得神经元的分布趋于激活函数的有效区域,就是红色区域。
因为如果神经元的值区域左右两边的话,经过激活函数的输出,会使得值要么集中于0,要么集中于1
1、这会导致网络“缺乏表现力”,想想一直都是0,1,那为何不直接放一个层就好了,更何况维数越高,非线性决策能力越强,显然,0-1模式维数只有2,缺乏决策力。
2、趋于0-1的值,会使得反向求梯度的时候,梯度为0,这样在梯度下降法优化的时候,会使得参数一直不变,也就是网络“死掉了”。这样的网络是没用的。这就是“梯度消失现象”。
反之,将数据集中于红色区域,会使得输出遍布0-1之间,数据广度更大,表现力更强,反映出的决策能力也越强。参数也能够有效更新。
因此BN算法就是要将每一次层的输出之后,激活函数之前,加上BN层,调整输出在红色有效区域。至于为啥一定要每层都要激活函数,我提一下,比如激活函数Relu,在神经网络中的作用是:通过加权的输入进行非线性组合产生非线性决策边界,简单的来说就是增加非线性作用。在深层卷积神经网络中使用激活函数同样也是增加非线性,主要是为了解决sigmoid函数带来的梯度消失问题。
从这个图中也可以看出BN的重要性了,增加输出的广度,提高决策能力。
说完了BN的重要性和功能,正式开始BN算法:
前三步是 归一化操作,样本均值反映了数据的期望值,样本的中心在哪。样本方差反映了最边上样本到中心的平均距离。第四步是对反归一化,简单来说,如果FC的某一个神经元不需要调整,那么可以通过学习得到对应的gamma和beta,使得BN在这个神经元上失效。
gamma、belt都是和样本形状相同。
BN算法有2个分支
nn.BatchNorm1d()
这是针对全连接层的,输入参数为输出神经元个数。每个神经元的mean、var、gamma、beta是不同的
nn.BatchNorm2d()
这是针对卷积层的,处于卷积层和激活函数之间,输入参数为卷积输出层的通道数。
和dropput层一样,训练和测试是不一样的,需要使用的net.eval(),关闭BN,保存BN的参数信息,用另一套不参与反向传播的参数去做归一化。net.train(),开启BN。
关于train、eval:点这里
BN加在哪:点这里
总结BN优点:
1、加快学习进行(增大学习率)
2、不那么依赖必须设定很好的初始值
3、抑制过拟合
4、抑制梯度消失、爆炸
Note:batch的数量必须大于1。
6.4、微调
就是用在大数据上训练好的成熟模型,用自己的小数据集去微调参数,以一个在Imagenet上训练玩的Resnet18为预训练模型,然后在热狗二分类小训练集上训练,因此只需要把最后的fc改成二分类的fc层就行了
预训练模型导入
注意,接口要合适:
初始化预训练模型:
预训练模型的最后一层:
更改成我们的:
只训练最后一层,所以前面的参数可以指定学习率为0或者很小去微调即可,最后一层要从头开始训练,所以学习率设为大一点。
训练结果:
对比,从头训练整个网络:
7、读取和存储训练好的模型、参数
7.1、读取Tensor
存:save 可以存取模型、张量、列表、字典
取:load
x = torch.ones(3)
torch.save(x, 'x.pt')
x1 = torch.load('x.pt')
print(x1)
7.2、state_dict:一个从参数名称到参数Tensor的有序字典,只有具有可学习参数的层(卷积、仿射层)、优化器才用
用法:net.state_dict()
输出:
optimizer.state_dict()
7.3、读取模型
7.3.1、保存和加载模型参数
保存用法:torch.save(model.state_dict(), ./net.pth)
加载用法:model.load_state_dict(torch.load(PATH))
如将模型a的参数给b
a, b = Net(), Net() # a和b的初始参数是不一样的,因为pytorch的初始化时随机的
b.load_state_dict(a.state_dict())
torch.save(net,'./model.pth') #保存整个模型及其参数
net=torch.load('./model.pth') #加载整个模型及其参数
#或者
torch.save(net.state_dict(),'./model-dict.pth')#仅仅保存模型参数
net.load_state_dict(torch.load('./model-dict.pth')) #仅仅加载模型参数(所以需要事先定义一个模型net),这里要注意一个参数map_location,或者参考map_location。
7.3.2、保存和加载整个模型
保存用法:torch.save(model, PATH)
加载用法:torch.load(PATH)
具体:pytorch模型的保存与加载注意事项:_LS_learner的博客-CSDN博客
8、成长过程显示:
第一次用1层的softmax回归训练识别生活物品
第二次用2层网络(加了一层256分神经元的隐藏层)
多了一层之后,把第五件shirt给识别正确了!
在服务器上的效果:
神经网络中一些问题的收集:
Q1:监督学习中为什么输入样本要独立同分布?nn就是典型的监督学习
拿nn举例,nn的作用失去拟合训练集的数据,然后在测试集上检验泛化能力。我们都知道测试集都是我们不知道的数据,其分布我们都不清楚。我们在训练集中训练的时候,假设所有数据都训练到了,如果数据之间存在相关性,那么拟合也会按照相关性去拟合,也就是说网络学习到了这个‘“相关性特征”。那么此时你去测试集上,会有很差的效果。因此我们需要数据之间独立同分布,禁止有相关性或者说连续性。
Q2:为什么网络的输入数据特征之间需要保持不相关性?
Q3:为什么我们要随机化初始值,而不能把网络的参数初始化为0?
根据吴恩达所说,初始化为0,那么会导致网络欠缺表达力。