1 tensor
【创建tensor】
a = t.Tensor(2, 3)
:指定tensor形状a = t.Tensor((2, 3))
:创建一个元素为2和3的tensorx = t.Tensor([[1,2],[3,4]])
:用list数据创建tensorb.tolist()
:把tensor转为listc = t.Tensor(b_size)
:创建一个和b形状一样的tensor,系统不会马上分配空间,会计算剩余内存是否够用,使用到tensor时才会分配t.ones(2, 3)
:形状为2×3的全为1的tensort.zeros(2, 3)
:形状为2×3的全为0的tensort.arange(1, 6, 2)
:从1到6,步长为2t.linspace(1, 10, 3)
:从1到10,均匀切成3份x = t.rand(5, 3)
:均匀随机分布t.randn(2, 3, device=t.device('cpu'))
:标准随机分布t.randperm(5)
:长度为5的随机排列t.eye(2, 3, dtype=t.int)
:对角线为1,不要求行列数一致torch.tensor
:在0.4版本新增的一个新版本的创建tensor方法,使用方法和参数几乎和np.array
完全一致,如scalar = t.tensor(3.14159)
,vector = t.tensor([1, 2])
,tensor = t.Tensor(1,2)
,empty_tensor = t.tensor([])
【tensor操作】
接口来说,对tensor操作分为两类:
torch.function
(如torch.save
)tensor.function
(如tensor.view
)
tensor.size()
:等价于tensor.shape
,返回torch.Size
对象tensor.view()
:调整tensor形状,当某一维为1时,自动计算它的大小,和源tensor共享内存,实际应用如需添加或减小某一维度,使用squeeze
和unsqueeze
b.unsqueeze(1)
:在第1维(下标从0开始)上增加“1”b.unsqueeze(-2)
:在倒数第2维上增加“1”b.squeeze(0)
:压缩第0维的“1”b.squeeze()
:把所有维度为“1”的压缩tensor.resize_()
:修改tensor的大小,于view不同的是,可以修改tensor的大小,如果超过原大小,会自动分配新的内存空间,如果小于原大小,之前的数据会被保存b.numel()
:b中元素总个数
【索引操作】
a[0]
:第0行a[:, 0]
:第0列a[0, -1]
:第0行最后一个元素,等价于a[0][-1]
a[:2]
:前两行a[:2, 0:2]
:前两行,第0,1列a[None].shape
:None类似于np.newaxis
,为a新增一个轴,等价于a.view(1, a.shape[0], a.shape[1])
a[a>1]
:等价于a.mask_select(a>1)
,选择结果与原tensor不共享内存空间a[t.LongTensor([0, 1])]
:第0行和第1行- gather把数据从input中按index取出,相反地,scatter_是把取出的数据放回去(就地操作),
out = input.gather(dim, index)
逆操作为out.scatter_(dim, index)
index = t.LongTensor([[0, 1, 2, 3]]) #对角线元素
a.gather(0, index)
index = t.LongTensor([[3, 2, 1, 0]]).t() #反对角线元素
a.gather(1, index)
index = t.LongTensor([[3,2,1,0]]) #反对角线元素,和上面不同
a.gather(0, index)
index = t.LongTensor([[0,1,2,3],[3,2,1,0]]).t() # 选取两个对角线上的元素
b = a.gather(1, index)
- 获取某一个元素的值:使用
tensor.item()
(一个元素的tensor),直接索引操作tensor[idx]
得到的还是一个tensor
【tensor类型】
- 默认是FloatTensor,可通过
t.set_default_tensor_type("torch.DoubleTensor")
修改默认tensor类型 t.set_default_tensor_type('torch.FloatTensor')
:恢复之前的默认设置b = a.float()
:把a转成FloatTensor,等价于b = a.type(t.FloatTensor)
- 数据类型转换是
type(new_type)
,还有float, long, half方法 c = a.type_as(b)
torch.*_like(tensora)
:生成和tensora同样属性的新tensortensor.new_*(new_shape)
:新建一个不同形状的tensora.new(2, 3)
:等价于torch.DoulbeTensor(2, 3)
t.zeros_like(a)
:全为0,等价于t.zeros(a.shape, dtype = a.dtype, device = a.device)
t.rand_like(a)
:随机数值a.new_ones(4, 5, dype = t.int)
a.new_tensor([3, 4])
【逐元素操作】
- 数学运算:
abs
,sqrt
,div
,exp
,fmod
,log
,pow
- 三角函数:
cos
,sin
,asin
,atan2
,cosh
- 取整:
ceil
,round
,floor
,trunc
- 截断:
clamp(input, min, max)
- 激活函数:
sigmod
,tanh
a % 3
等价于t.fmod(a, 3)
a ** 2
等价于t.pow(a, 2)
【归并操作】
- 包括
mean
,sum
,median
,mode
(众数),norm
(范数),dist
(距离),std
,var
,cumsum
(累加),cumprod
(累乘) - dim指定是在哪个维度上执行操作,如果输入形状是(m, n, k)
如果指定dim = 0,输出形状(1, n, k)或(n, k)
如果指定dim = 1,输出形状(m, 1, k)或(m, k)
如果指定dim = 2,输出形状(m, n, 1)或(m, n)
size中是否有1取决于参数keepdim,keepdim = True
会保留维度1,除了cumsum
【比较】
- 包括gt(大于),lt(小于),te(大于等于),le(小于等于),eq(等于),ne(不等),topk(最大的k个数),sort(排序),max/min(比较两个tensor最大最小值)
t.clamp(a, min = 10)
:比较a和10较大的元素
【线性代数】
trace
:矩阵的迹diag
:对角线元素triu
,tril
:矩阵上三角/下三角可指定的偏移量mm
,bmm
:矩阵乘法addmm
,addbmm
,addmv
,addr
,badbmm
:矩阵运算t
:转置dot
,cross
:内积/外积inverse
:求逆svd
:奇异值分解- 矩阵转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续,
b = a.t()
b = contiguous()
【numpy和tensor转换】
- tensor不支持的操作可以转为numpy数组处理,之后再转回tensor,共享内存
# Tensor -> Numpy
a = t.ones(5)
b = a.numpy()
# Numpy -> Tensor
import numpy as np
a = np.ones(5)
b = t.from_numpy(a)
- 当numpy数据类型和tensor不一样时,数据被复制,不会共享内存
- 不论输入类型是什么,
t.Tensor()
或tensor.clone()
都会数据拷贝,不共享内存
a = np.ones([2,3])
b = t.Tensor(a)
- 如果想共享内存,使用
torch.from_numpy()
或者tensor.detach()
来新建一个tensor,二者共享内存
【广播法则】
- 让所有的输入数组向shape最长的数组看齐,shape不足的部分通过在前面加1补齐;
- 两个数组要么在某一维长度一致,要么其中一个为1;
- 当输入数组某个维度为1时,计算沿此维度复制扩充成一样的形状
手动广播更直观,不容易出错:
unsqueeze
/view
/tensor[None]
为数据某一维形状补1,实现法则1;expand/expand_as
重复数组,实现法则3;- repeat实现与expand类似的功能,但repeat会把相同数据复制成多份,占用额外空间
a = t.ones(3, 2)
b = t.zeros(2, 3, 1)
a[None].expand(2, 3, 2) + b.expand(2, 3, 2)
【内部结构】
tensor分为头信息区(Tensor)和存储区(Storage),信息区保存tensor的形状(size)、步长(stride)、数据类型(type),存储区决定了tensor中元素数目和内存,不同tensor头信息不同,但可能使用相同storage。绝大多数操作不修改tensor数据,只修改tensor头信息,这样更节省内存,提升了处理速度。
【CPU和GPU tensor之间的转换】
tensor.cuda(device_id)
和tensor.cpu()
:a = t.randn(3, 4, device = t.device('cuda:1'))
tensor.to(device)
:将device设置成一个可配置的参数device=t.device('cpu')
【保存和加载】
a = a.cuda(1) #转化为GPU1上的tensor
t.save(a,'a.pth')
b = t.load('a.pth', map_location = lamda storage, loc: storage) #存储于CPU
d = t.load('a.pth', map_location = {'cuda:1':'cuda:0'}) #存储于GPU0上
【向量化】
并行计算方式,提高科学计算效率
2 autograd(自动微分)
torch.autograd是为方便用户使用,而专门开发的自动求导引擎,能够根据输入二号前向传播过程自动构建计算图并执行反向传播。
【variable】
- 创建tensor的时候只需设置
tensor.requires_grad=True
,代表需要求导数,需要求导(requires_grad)的tensor即为variable
# 方式1
a = t.randn(3, 4, requires_grad = True)
# 方式2
a = t.randn(3, 4).requires_grad()
# 方式3
a = t.randn(3, 4)
a.requires_grad = True
- variable的
grad
与data形状一致,variable默认不需要求导,即requires_grad = false
,如果一个节点的requires_grad=True,所有依赖它的节点requires_grad都为True - variable的
volatile
属性默认为False,即volatile = false
,如果volatile=True,所有依赖它的节点volatile属性都为True。volatile属性为True的节点不会求导,优先级比requires_grad高
x = t.ones(2, 2, requires_grad = True)
【反向传播】
- 计算图是一种特殊的有向无环图(DAG),用于记录算子与变量之间的关系。PyTorch采用动态图设计,可以很方便查看中间层输出,动态设计计算图结构。
- 深度学习的算法本质是通过反向传播求导数,autograd会记录生成当前variable的所有操作,建立一个有向无环图,每个变量在图中位置可通过
grad_fn
属性记录了操作Function,叶子节点的grad_fn
为None - 叶子节点:对于那些不是任何Function的输出,由用户创建的节点称为叶子节点,其
grad_fn
为None,叶子节点中需要求导的variable具有AccumulateGrad
标识,其梯度累加 - grad在反向传播过程中累加,每一次反向传播之前需要把梯度清0
x.grad.data.zero_()
- 反向传播时计算各个variable梯度,只需调用根节点的backward方法,autograd会沿着图反向传播,计算每一个叶子节点的梯度
variable.backward(gradient_variable = None, retain_graph = None, create_graph = None)
gradient_variables
:形状与variable一致,对于y.backward(),grad_variables相当于链式法则dz/dx=(dz/dy)*(dy/dx)中的dz/dy,可以是tensor或序列,如果是标量,可以省略,默认为1retain_graph
:反向传播的中间缓存会被清空,为进行多次反向传播,通过指定retain_graph = True
不清空反向传播之后的中间缓存create_graph
:对反向传播过程再次构建计算图,可通过backward of backward实现求高阶导数
【variable的grad属性和backward()函数的grad_variables】
- variable x梯度是目标函数f(x)对x的梯度,df(x)/dx = (df(x)/dx0, df(x)/dx1, … df(x)/dxN),形状和x一致
y.backward(grad_variables)
中的grad_variables
可以看成链式求导的中间结果,相当于链式法则中的dz/dx = dz/dy * dy/dx中的dz/dy,z是目标函数,一般是一个标量,故dz/dy的形状和variable y的状一致z.backward()
在一定程度上等价于y.backward(grad_y)
,z.backward()
省略了在grad_variables
参数,因为z是一个标量,dz/dz = 1。
【修改tensor数值】
- 如果想修改tensor数值,但不被autograd记录,可以对
tensor.data
或tensor.detach()
- 如果修改
variable.data
将无法使用autograd进行反向传播,除了对参数做初始化,一般不会修改variable.data
的值,应避免
【查看变量梯度】
在反向传播过程中非叶子节点梯度计算完被清空,若想查看这些非叶子节点的梯度,使用autograd.grad
函数或hook
(推荐)
- 使用autograd.grad函数
x = t.ones(3, requires_grad = True)
w = t.rand(3, requires_grad=True)
y = x*w
z = y.sum()
# z对y的梯度,隐式调用backward()
t.autograd.grad(z, y)
- 使用hook
# hook是一个函数,输入是梯度,不应该有返回值
def variable_hook(grad):
print('y的梯度:', grad)
x = t.ones(3, requires_grad = True)
w = t.rand(3, requires_grad = True)
y = x*w
# 注册hook
hook_handle = y.register_hook(variable_hook)
z = y.sum()
z.backward()
# 除非每次都用Hook,否则用完之后记得移除hook
hook_handle.remove()
【前向传播和反向传播实现】
- ctx是context的缩写, 翻译成"上下文; 环境",ctx专门用在静态方法中
- 自定义的Function需要继承autograd.Function,没有构造函数
__init__
,forward()
和backward()
都是静态方法 - self指的是实例对象;而ctx用在静态方法中, 调用的时候不需要实例化对象, 直接通过类名就可以调用, 所以self在静态方法中没有意义
- 自定义的
forward()
方法和backward()
方法的第一个参数必须是ctx; ctx可以保存forward()中的变量,以便在backward()中继续使用,否则前向传播结束后这些对象即被释放 ctx.save_for_backward(a, b)
能够保存forward()
静态方法中的张量, 从而可以在backward()
静态方法中调用ctx.needs_input_grad
是一个元组, 元素是True或者False, 表示forward()
中对应的输入是否需要求导, 比如ctx.needs_input_grad[0]
指的是下面forwad()代码中indices是否需要求导- backward的输出个数,应与forward的输入个数相同,如果某一输入变量不需要梯度,在backward中返回None;backward函数的输入和forward函数的输出一一对应
from torch.autograd import Function
class MultiplyAdd(Function):
@staticmethod
def forward(ctx, w, x, b):
ctx.save_for_backward(w, x)
output = w * x + b
return output
@staticmethod
def backward(ctx, grad_output):
w, x = ctx.saved_tensors
grad_w = grad_output * x
grad_x = grad_output * w
grad_b = grad_output * 1
return grad_w, grad_x, grad_b
x = t.ones(1)
w = t.rand(1, requires_grad = True)
b = t.rand(1, requires_grad = True)
# 开始前向传播
z = MultiplyAdd.apply(w, x, b)
# 开始反向传播
# 方式1
z.backward()
# 方式2 调用MultiplyAdd.backward
z.grad_fn.apply(t.ones(1))
实现了自己的Function后,可以使用gradcheck函数检测实现是否正确。gradcheck通过数值逼近计算梯度,可能具有一定误差,通过控制eps大小可以控制容忍的误差。
class Sigmoid(Function):
@staticmethod
def forward(ctx, x):
output = 1 / (1 + t.exp(-x))
ctx.save_for_backward(output)
return output
@staticmethod
def backward(ctx, grad_output):
output, = ctx.saved_tensors
grad_x = output * (1 - output) * grad_output
return grad_x
# 采用数值逼近方式检验计算梯度的公式对不对
test_input = t.rand(3, 4, requires_grad = True).double()
t.autograd.gradcheck(Sigmoid.apply, (test_input,), eps = 1e-3)
【高阶求导】
之所以forward函数的输入是tensor,而backward函数的输入是variable,是为了实现高阶求导。backward函数的输入输出虽然是variable,但在实际使用时autograd.Function会将输入variable提取为tensor,并将计算结果的tensor封装成variable返回。在backward函数中,之所以要对variable进行操作,是为了能够计算梯度的梯度。
x = t.tensor([5], requires_grad=True,dtype=t.float)
y = x ** 2
grad_x = t.autograd.grad(y, x, create_graph=True)
grad_x # dy/dx = 2 * x
grad_grad_x = t.autograd.grad(grad_x[0],x)
grad_grad_x # 二阶导数 d(2x)/dx = 2
3 神经网络
torch.nn
:神经网络模块化接口nn.Module
:网络各层定义和forward方法nn.functional
:和nn.Module
类似,纯函数- 如果模型中有可学习的参数,用nn.Module,否则既可以用nn.functional也可以用nn.Module:对于激活函数(ReLU, sigmoid, tanh)、池化(MaxPool)等没有可学习参数,可以用nn.functional函数代替,对于卷积、全连接等具有可学习参数的网络使用nn.Module。
3.1 定义网络
(1) 需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在__init__中,如果某一层不具有可学习参数,可放在构造函数中,或在forwad中使用nn.functional代替
from torch.nn import functional as F
class Net(nn.Module)
def __init__(self)
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.pool(F.relu(self.conv1(x)), 2)
x = F.pool(F.relu(self.conv2(x)), 2)
x = x.view(-1, 16*5*5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
(2) 网络可学习参数通过net.parameters()
返回,net.named_parameters()可同时返回可学习的参数及名称
(3) nn.Conv2d
输入必须是4维的,形如nSamples×nChannels×Height×Width
self.conv1 = nn.Conv2d(1, 6, 5)
'1’输入通道,'6’输出通道,'5’卷积核
(4) torch.nn
只支持mini-batches,一次必须是一个batch
(5) view()
相当于numpy中的reshape,重新定义矩阵形状,-1代表动态调整这个维度的元素个数
(6) 卷积层:卷积 > 激活 > 池化 input -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss
【全连接层】
import torch as t
from torch import nn
class Linear(nn.Module):
def __init__(self, in_features, out_features):
super(Linear, self).__init__()
self.w = nn.Parameter(t.randn(in_features, out_features))
self.b = nn.Parameter(t.randn(out_features))
def forward(self, x):
x = x.mm(self.w)
return x + self.b.expand_as(x)
- 构造函数使用
super(Linear, self).__init__()
(推荐)或nn.Module.__init__(self)
- 构造函数
__init__
必须自己定义可学习的参数,并封装为Parameter
(一种特殊的Tensor),默认需要求导requires_grad = True
- forward实现前向传播过程,输入可以是一个或多个tensor
无需写反向传播函数,nn.Module能利用autograd实现反向传播,比利用Function实现更简单 - 使用时可以将layer看成数学概念中的函数,
layer(input)
等价于layers.__call__(input)
,在__call__
函数中调用layer.forward(x)
,实际尽量使用layer(x)
而不是layer.forward(x)
- 输入输出:
nn.linear
的输入形状是(N, input_features),输出为(N, output_features),N是batch_size。默认输入不是单个数据,而是一个batch,如果输入只有一个数据,必须调用tensor.unsqueeze(0)
或tensor[None]
将数据伪装成batch_size=1的batch - 全连接层对模型影响参数有三个:全连接层的总层数(长度)、单个全连接层的神经元数(宽度)、激活函数
- 全连接层的意义:(详见https://blog.csdn.net/zfjBIT/article/details/88075569)
把特征representation整合到一起,输出为一个值,大大减少特征位置对分类带来的影响。连接层实际就是卷积核大小为上层特征大小的卷积运算,卷积后的结果为一个节点,就对应全连接层的一个点。
假设最后一个卷积层的输出为7×7×512,连接此卷积层的全连接层为1×1×4096。如果将这个全连接层转化为卷积层:
- 共有4096组滤波器
- 每组滤波器含有512个卷积核
- 每个卷积核的大小为7×7
- 则输出为1×1×4096
若后面再连接一个1×1×4096全连接层。则其对应的转换后的卷积层的参数为:
- 共有4096组滤波器
- 每组滤波器含有4096个卷积核
- 每个卷积核的大小为1×1
- 输出为1×1×4096
相当于就是将特征组合起来进行4096个分类分数的计算,得分最高的就是划到的正确的类别。
【卷积层】
from PIL import Image
from torchvision.transform import ToTensor, ToPILImage
to_tensor = ToTensor() # img -> tensor
to_pil = ToPILImage()
lena = Image.open("imgs/lena.png")
# 输入是一个batch, batch_size = 1
input = to_tensor(lena).unsqueeze(0)
# 锐化卷积核
kernel = t.ones(3, 3)/-9.
kernel[1][1] = 1
conv = nn.Conv2d(1, 1, (3, 3), 1, bias = False)
conv.weight.data = kernel.view(1, 1, 3, 3)
out = conv(input)
to_pil(out.data.squeeze(0))
【池化层】
特殊的卷积层,用来下采样。池化层没有可学习参数,其weight是固定的。
pool = nn.AvgPool2d(2, 2)
out = pool(input)
【BatchNorm】
批规范化层,分为1D、2D和3D。除了标准的BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。
# 输入batch_size = 2,维度3
input = t.randn(2, 3)
linear = nn.Linear(3, 4)
h = linear(input)
# 4 channel,初始化标准差为4,均值为0
bn = nn.BatchNorm1d(4)
bn.weight.data = t.ones(4) * 4
bn.bias.data = t.zeros(4)
bn_out = bn(h) # 输出的均值、方差
【Dropout】
防止过拟合,分为1D、2D和3D
# 每个元素以0.5的概率舍弃
dropout = nn.Dropout(0.5)
o = dropout(bn_out)
【激活函数】
最常用的激活函数ReLU:数学表达式为ReLU(x) = max(0, x)
relu = nn.ReLU(inplace = True)
input = t.randn(2, 3)
output = relu(input)
ReLU函数有个inplace函数,如果设为True,会把输出直接覆盖到输入中,这样可以节约内存/显存。之所以可以覆盖,是因为在计算ReLU的反向传播时,只需根据输出就能够推算反向传播的梯度。但只有少数的autograd操作支持inplace操作如tensor.sigmoid_()
,除非明确知道自己在做什么,否则不要使用inplace操作。
【Sequential/ModuleList】
- Sequential:前馈神经网络将每层输出作为下一层输入,不用每次都写forward,包含几个子Module,前向传播会把输入层一层接一层传递下去
方式1
net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(3, 3, 3))
net1.add_module('batchnorm', nn.BatchNorm2d(3))
net1.add_module('activation_layer', nn.ReLU())
方式2
net2 = nn.Sequential(nn.Conv2d(3, 3, 3), nn.BatchNorm2d(3), nn.ReLU())
方式3
from collections import OrderedDict
net3 = nn.Sequential(OrderedDict([('conv1', nn.Conv2d(3, 3, 3)),('bn1', nn.BatchNorm2d(3)),('relu1', nn.ReLU())]))
可根据名字或序号取出子module:net1.conv
, net2[0]
, net3.conv1
- ModuleList:一个特殊Module,可以像list使用,但不能直接把输入传给ModuleList
modellist = nn.ModuleList([nn.Linear(3, 4), nn.ReLU(), nn.Linear(4, 2)])
input = t.randn(1, 3)
for model in modellist:
input = model(input)
# 下面会报错
# output = modelist(input)
- ParameterList:一个可以包含多个parameter的类list对象,使用方式和ModuleList类似,如果在构造函数
__init__
中用到list, tuple, dict对象时,考虑是否该用ModuleList或ParameterList代替
3.2 损失函数
损失函数可看作一种特殊的layer,PyTorch将这些损失函数实现为nn.Module的子类,然而实际使用中通常将这些loss function专门提取出来,和主模型互相独立。
nn.MSELoss
:计算均方误差nn.CrossEntropyLoss
:计算交叉熵损失loss.backward()
:动态生成并自动微分
score = t.randn(3, 2) # batch_size=3,计算每个类别的分数
label = t.Tensor([1, 0, 1]).long() # 三个样本属于1,0,1类,label是LongTensor
criterion = nn.CrossEntropyLoss()
loss = criterion(score, label)
3.3 优化器
在反向传播计算完所有参数的梯度后,还需要使用优化方法来更新网络的权重和参数,例如随机梯度下降法(SGD)的更新策略如下:weight = weight - learning_rate * gradient。手动实现:
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)# inplace 减法
torch.optim
实现了深度学习中绝大多数优化方法如RMSProp, Adam, SGD,都是继承基类在optim.Optimizer
,大多数时候不需要手动实现
from torch import optim
#新建一个优化器,使用时指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(), lr = 0.01)
#训练过程中梯度清0,等价于net.zero_grad()
optimizer.zero_grad()
#计算损失
input = t.randn(1, 3, 32, 32)
output = net(input)
loss = criterion(output, target)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
- 为不同子网络设置不同学习率,在finetune中常用,如果对某个参数不指定学习率就用最外层的学习率
optimizer = optim.SGD([{'params': net.features.parameters()},{'params': net.classifier.parameters(), 'lr': 1e-2}],lr = 1e-5)
- 为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[0], net.classifier[3]])
special_layers_params = list(map(id, special_layers.parameters()))
base_params = filter(lambda p : id(p) not in special_layers_params, net.parameters())
- 调整学习率的两种方法:
方法1 调整学习率,新建一个optimizer
old_lr = 0.1
optimizer1 = optim.SGD([{'params': net.features.parameters()},{'params': net.classifier.parameters(), 'lr': old_lr*0.1}], lr = 1e-5)
方法2 调整学习率,手动decay,保存动量
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1
optimizer
3.4 初始化策略
良好的初始化可以使模型更快收敛,nn.Module采取了较合理的初始化策略,也可以自定义初始化,如在使用Parameter时。t.Tensor()返回的是内存中的随机数,可能有极大值,造成训练网络溢出或梯度消失
t.maual_seed(1) #为CPU/GPU设置种子用于生成随机数,以使得结果是确定的
init.xavier_normal_(linear.weight)
3.5 nn.functional
3.6 nn和autograd的关系
autograd.Function
和nn.Module
区别:
autograd.Function
利用Tensor对autograd技术的拓展,为autograd实现了新的运算op,不仅要实现前向传播还要手动实现反向传播;nn.Module
利用autograd技术,对nn功能进行拓展,实现深度学习中更多层,只需实现前向传播功能,autograd会自动实现反向传播;nn.functional
是一些autograd操作的集合,是经过封装的函数
3.6 nn.Module深入分析
【nn.Module属性】
_parameters
:字典,在字典中加入key为’param’,value为对应parameter的item_modules
:子module,通过self.submodel = nn.Linear(3,4)
指定的子module会保存于此_buffers
:缓存,如batchnorm使用momentum机制,每次前向传播用到上一次传播的结果_backward_hooks
,_forward_hooks
:钩子技术,用来提取中间变量
model = VGG()
features = t.Tensor()
def hook(module, input, output):
features.copy_(output.data)
handle = model.layer8.register_forward_hook(hook)
_ = model(input)
handle.remove() #用完hook后删除
【training】
- Batch层在训练阶段和测试阶段采取的策略不同,通过判断training决定前向传播策略
- 对于
batchnorm
,dropout
,instancenorm
等在训练和测试阶段行为差距巨大的层,需要在测试时将training值设为False - 可以调用
model.train()
函数,将当前module及其子module中所有training属性设为True,model.eval()
函数,将training属性设为False
【构造:__getattr__和__setattr__方法】
result = obj.name
会调用buildin函数getattr(obj, 'name')
,如果该属性找不到,调用obj.__getattr__('name')
,返回一个对象属性值obj.name = value
会调用buildin函数setattr(obj, 'name', value)
,如果obj对象实现了在__setattr__
方法,setattr会直接调用obj.__setattr__('name','value')
,设置一个对象属性值
【保存和加载模型】
#保存模型
t.save(net.state_dict(), 'net.pth')
#加载已保存的模型
net2 = Net()
net2.load_state_dict(t.load('net.pth'))
【GPU运算】
- 将模型所有参数转存GPU:
model = model.cuda()
- 将输入数据放置到GPU上:
input.cuda()
- 在多个GPU上进行并行计算
方式1 直接利用多GPU并行计算得出结果
nn.parallel.data_parallel(module, inputs, device_ids = None, output_device = None, dim = 0, module_kwargs = None)
方式2 返回一个新的module,自动在GPU上进行并行加速
class torch.nn.DataParallel(module, device_ids = None, output_device = None, dim = 0)
- device_ids参数指定在哪些GPU上进行优化
- output_device指定输出到哪个GPU上
DataParallel并行方式,是将输入一个batch的数据分成多份,分别送到对应GPU计算,各个GPU得到的梯度累加,与Module相关的数据也都以浅复制的方式复制多份。module中属性应是已读的。
4 用CIFAR-10进行神经网络训练
【数据加载】
- 图片预处理:利用
transforms.ToTensor()
转化为tensor,再用transforms.Normalize()
归一化 - Dataloader是一个可迭代的对象,将dataset返回的每一条数据拼接成一个batch,提供多线程加速优化和数据打乱等
trainset = tv.datasets.CIFAR10(root = './dataset', train = True, download = True, transform = transform)
trainloader = t.utils.data.DataLoader(trainset, batch_size = 4, shuffle = True, num_workers = 2) #一个batch返回4张图片
【训练网络】
- 流程:①输入数据;②前向传播+反向传播;③更新参数
- 每遍历完一遍数据集称为一个epoch
t.set_num_threads
:设置pytorch进行CPU多线程并行计算时占用的线程数,在linux和windows平台都有效t.set_printoptions(precision = 10)
:设置打印tensor时的数值精度和格式
for epoch in range(2): #遍历数据集
running_loss=0.0
for i, data in enumerate(trainloader, 0):
#输入数据
inputs, labels = data
#梯度清0
optimizer.zero_grad()
#forward+backward
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
#更新参数
optimizer.step()
#打印log:loss是一个scalar,需要使用loss.item()获取数值
running_loss += loss.item()
if i % 2000 == 1999:
print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
print('Finish Training')
遍历dataloader的另一种方法:
dataiter = iter(testloader)
images, labels = dataiter.next() #一个batch返回4张图片