pytorch搭建深度学习基本方法
0. 前言
首先推荐河北工业大学刘老师发布在B站的《PyTorch深度学习实践》,整体上讲得非常细致,适合初学者,其中也穿插了不少工程经验和底层知识。
本文整体上采用源码加注释的方式介绍pytorch
1. 模型搭建
模型搭建放在前面主要是方便
class Net(nn.Module):
#从nn.Module继承类
def __init__(self):
# 初始化方法
super(Net,self).__init__()
# 执行父类(nn.Module)的__init__函数,之后可以在Net中调用父类的属性
self.linear1 = nn.Linear(8, 4)
# nn.Linear为线性层也就是全连接层,这里的(8, 4)指输入通道为8,输出通道为4
self.linear2 = nn.Linear(4, 2)
# 同上
self.linear3 = nn.Linear(2, 1)
# 同上
self.sigmod = nn.Sigmoid()
# 定义激活函数为nn.Sigmoid()
def forward(self,x):
# forward方法就是前向传播,简单来说就是把模型算一遍
x = self.sigmod(self.linear1(x))
x = self.sigmod(self.linear2(x))
x = self.sigmod(self.linear3(x))
return x
2. 训练和预测
if __name__ == '__main__':
#---------------导入数据-------------------------
dataset=Diabetes('../diabetes.csv.gz')
train_loader=DataLoader(dataset=dataset,
batch_size=32,
shuffle=True,
num_workers=2)
#----------------下一节会讲----------------------
model=Net()
# 实例化模型
criterion = torch.nn.BCELoss(size_average=True)
# 定义损失函数为torch.nn.BCELoss
optimizer = torch.optim.Adam(model.parameters(),
lr=0.01)
# 定义优化方法为torch.optim.Adam
for epoch in range(100):
for ori in train_loader:
# 1个ori表示1个batch
data,label=ori
# 1个batch中包含data和label
pre=model(data)
# 这里model(data)即为model.forward(data),也就是从前往后算一遍模型
loss=criterion(pre,label)
# 计算loss,定量衡量当前模型的结果和label的差距
optimizer.zero_grad()
# 清空梯度
loss.backward()
# 反向传播
optimizer.step()
# 更新参数
if epoch % 10 == 0 :
# 每10轮输出1次loss
print('epoch:{0}, loss = {1:.2f}'.format(epoch,loss.item()))
3. 数据导入
class Diabetes(Dataset):
def __init__(self,path):
self.bits=np.loadtxt(path,delimiter=',',dtype=np.float32)
# 这里使用了糖尿病的数据集,导入后是一个NX9的矩阵,8个自变量和一个label
self.data=torch.from_numpy(self.bits[:,:-1])
self.label=torch.from_numpy(self.bits[:,[-1]])
self.len=self.bits.shape[0]
def __getitem__(self, item):
# 魔法函数的部分内容放在注解中
# 定义迭代的结果,也就是定义for循环中每一轮要处理的东西
return self.data[item],self.label[item]
def __len__(self):
return self.len
4.注解
因为这里涉及到一些理论知识,就单独补充在这里。
4.1 super
这篇文章中super(Net,self).__init__()
的作用就是执行父类(nn.Module
)的__init__
函数,之后可以在Net中调用它的属性,更多内容见参考资料:Python super 详解
4.2 魔术方法 (Magic Method)和可调用对象
定义的Net就是一个可调用的对象,只需要用Net(data)就可以计算前馈。这样的对象需要定义魔术方法__call__,在nn.Module的源码中
def __call__(self, *input, **kwargs):
for hook in self._forward_pre_hooks.values():
result = hook(self, input)
if result is not None:
if not isinstance(result, tuple):
result = (result,)
input = result
if torch._C._get_tracing_state():
result = self._slow_forward(*input, **kwargs)
else:
#-----------------在这里调用了forward---------------------
result = self.forward(*input, **kwargs)
#--------------------------------------------------------
#省略剩下的代码
return result
4.2 为什么要分batch,batch_size怎么确定
具体可见知乎用户@夕小瑶的回答:训练神经网络时如何确定batch的大小?
总结起来就是:
(1) (不分batch) = (batch_size就是训练集的大小)
(2) 显然在同等的计算量之下(一定的时间内),使用整个样本集的收敛速度要远慢于使用少量样本的情况。即对于收敛到同一个最优点所需的时间(CPU)而言:使用整个样本集几次迭代 > 少量样本多次迭代,即batch_size小更好
(3) 减小batch_size换来收敛速度提升但同时会引入噪声导致性能下降
(4) 打个比方来说类似于吃饭,一个batch相当于吃一口,一个epoch相当于一顿饭。如果一个人的嘴很小(CPU),每次都只能塞一勺饭,但另一个人嘴很大(GPU),每次能塞batch_size勺饭,即一个batch中的多个sample可以并行计算,那么在喂饭这一过程(训练以外的操作也需要时间)耗费时间不太大的情况下,嘴大的人(GPU)吃得更快。
Note1: 据说GPU对2的幂次方batch可以发挥更佳的性能,这也是示例中使用batch_size=32(2^5)的原因。
Note2: batch_size设置不能得太大也不能太小,因此实际工程中最常用的就是mini batch(几十或者几百)。
Note3: 减小batch加速训练对一阶优化算法如Adagrad、Adam等是没问题的,但是对于强大的二阶优化算法如共轭梯度法、L-BFGS,减小batch换来的收敛速度提升远不如引入大量噪声导致的性能下降,因此在使用二阶优化算法时,往往要采用大batch(几千甚至一两万)。
4.3 为什么要用optimizer.zero_grad()
,即为什么在每一个batch的最后清空梯度
先看结果:
清空梯度
epoch:0, loss = 0.65
epoch:10, loss = 0.44
epoch:20, loss = 0.57
epoch:30, loss = 0.51
epoch:40, loss = 0.65
epoch:50, loss = 0.51
epoch:60, loss = 0.49
epoch:70, loss = 0.42
epoch:80, loss = 0.41
epoch:90, loss = 0.49
195.41s
不清空梯度
epoch:0, loss = 0.68
epoch:10, loss = 0.71
epoch:20, loss = 0.72
epoch:30, loss = 0.68
epoch:40, loss = 0.75
epoch:50, loss = 1.09
epoch:60, loss = 0.82
epoch:70, loss = 0.96
epoch:80, loss = 0.90
epoch:90, loss = 0.89
195.65s
可以直观的看出清空梯度后收敛的更快
下面的解释翻译自StackOverflow: Why do we need to call zero_grad() in PyTorch?
In PyTorch, we need to set the gradients to zero before starting to do backpropragation because PyTorch accumulates the gradients on subsequent backward passes. This is convenient while training RNNs. So, the default action is to accumulate (i.e. sum) the gradients on every loss.backward() call.
在pytorch中,我们需要在开始反向传播之前将梯度设为0,因为pytorch会累加每个子反向传播的梯度。这对于训练RNN(注:循环神经网络)而言是很棒的。所以,(pytorch的)默认操作是对对每一次loss.backward()的调用累加(或者叫求和)梯度。
Because of this, when you start your training loop, ideally you should zero out the gradients so that you do the parameter update correctly. Else the gradient would point in some other direction than the intended direction towards the minimum (or maximum, in case of maximization objectives).
因此,为了能使参数正常更新,你应该在开始训练时将梯度清零。否则,梯度将指向预期方向,即最小化(或最大化,目标是最大化的情况)以外的其他方向。
5. 源代码(无注释)
import numpy as np
from torch.utils.data import DataLoader,Dataset
from torch import nn
import torch
class Diabetes(Dataset):
def __init__(self,path):
self.bits=np.loadtxt(path,delimiter=',',dtype=np.float32)
self.data=torch.from_numpy(self.bits[:,:-1])
self.label=torch.from_numpy(self.bits[:,[-1]])
self.len=self.bits.shape[0]
def __getitem__(self, item):
return self.data[item],self.label[item]
def __len__(self):
return self.len
class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.linear1 = nn.Linear(8, 4)
self.linear2 = nn.Linear(4, 2)
self.linear3 = nn.Linear(2, 1)
self.sigmod = nn.Sigmoid()
def forward(self,x):
x = self.sigmod(self.linear1(x))
x = self.sigmod(self.linear2(x))
x = self.sigmod(self.linear3(x))
return x
if __name__ == '__main__':
dataset=Diabetes('../diabetes.csv.gz')
train_loader=DataLoader(dataset=dataset,
batch_size=32,
shuffle=True,
num_workers=2)
model=Net()
criterion = torch.nn.BCELoss(size_average=True)
optimizer = torch.optim.Adam(model.parameters(),
lr=0.01)
for epoch in range(100):
for ori in train_loader:
data,label=ori
pre=model(data)
loss=criterion(pre,label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0 :
print('epoch:{0}, loss = {1:.2f}'.format(epoch,loss.item()))