前言
Pytorch和Numpy数组经常在一起互相转换,因为它们是共享内存的,区别就在于一个在
gpu中进行加速运算,一个在cpu中进行加速运算。
torch
torch使用torch.tensor,需要将数据进行转换
import torch
x_train, y_train, x_valid, y_valid = map(
torch.tensor,
(x_train, y_train, x_valid, y_valid)
)
x_train.shape
x_train.min()
x_train.max()
torch.nn
nn.functional
该包中包含torch.nn中所有的函数,还有很多损失函数和激活函数可以调用。
import torch.nn.functional as F
loss_func = F.cross_entropy
loss = loss_func(model(x), y)
loss.backward()
其中loss.backward()更新模型的梯度,包括权重系数w和偏置系数b。
nn和nn.functional似乎有一点功能重复了啊,那它们有何区别呢?
其实nn.functional.xxx是提供函数接口,nn.Xxx是nn.functional.xxx的类封装,并且nn.Xxx都继承于一个父类nn.Module
nn.Xxx相比nn.functional.xxx对象,其还含有train(),eval(),load_state_dict,state_dict等属性和方法
举例子:
inputs = torch.rand(64, 3, 28, 28)
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1)
out = conv(inputs)
inputs是模拟的一个图像,torch.rand(64,3,28,28)是batch数64,通道数3,H是28,W是28;
可以看到nn.Xxx的方式是,首先实例化一个对象,conv = nn.Conv2d(),需要传入参数,in_channels就是输入的feature_map的通道数,out_channels就是输出的feature_map的通道数,kernel_size应该是卷积核的通道数,padding是边缘的填充数;
然后直接用该对象来输入数据即可,out = conv(inputs),这就实现了一次卷积。
weight = torch.rand(64, 3, 3, 3)
bias = torch.rand(64)
out = nn.functional.conv2d(inputs, weight, bias, padding=1)
那这个nn.functional.xxx的方式呢,它是直接提供的接口,除了要输入数据,还需要的参数有权重w和偏置b,padding参数等;
其中只有nn.Xxx继承于nn.Module,可以与nn.Sequential结合使用
fm_layer = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(num_features=64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
nn.Dropout(0.2)
)
nn.Sequential是一个很好用的用来进行模块化操作的类封装;
可以看到上面的nn.Sequential()实例化了一个对象,该对象可以直接输入数据inputs,并对输入按顺序进行Conv2d(),BN,ReLU,MaxPool2d(),Dropout()操作;
如果model中有很多重复的模块化组块的话,可以用这个,只需要实例化出对象就造好轮子了,后面可以重复调用。
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.cnn1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=16, padding=0)
self.relu1 = nn.ReLU()
self.maxpool1 = nn.MaxPool2d(kernel_size=2)
self.cnn2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, padding=0)
self.relu2 = nn.ReLU()
self.maxpool2 = nn.MaxPool2d(kernel_size=2)
self.linear1 = nn.Linear(4 * 4 * 32, 10)
def forward(self, x):
x = x.view(x.size(0), -1)
out = self.maxpool1(self.relu1(self.cnn1(x)))
out = self.maxpool2(self.relu2(self.cnn2(x)))
out = self.linear1(out.view(x.size(0), -1))
return out
对于nn.Xxx来说,除了可以进行nn.Sequential的模块化操作以外,还有一个优势就是不需要管学习参数如w,b,但是nn.functional.xxx需要自己定义w和b,不利于代码的复用
class CNN(nn.Module):
"""docstring for CNN"""
def __init__(self):
super(CNN, self).__init__()
self.cnn1_weight = nn.Parameter(torch.rand(16, 1, 5, 5))
self.bias1_weight = nn.Parameter((torch.rand(16)))
self.cnn2_weight = nn.Parameter(torch.rand(32, 16, 5, 5))
self.bias2_weight = nn.Parameter(torch.rand(32))
self.linear1_weight = nn.Parameter(torch.rand(4 * 4 * 32, 10))
self.bias3_weight = nn.Parameter(torch.rand(10))
def forward(self, x):
x = x.view(x.size(0), -1)
out = F.conv2d(x, self.cnn1_weight, self.bias1_weight)
out = F.relu(out)
out = F.max_pool2d(out)
out = F.conv2d(out, self.cnn2_weight, self.bias2_weight)
out = F.relu(out)
out = F.max_pool2d(out)
out = F.linear(out, self.linear1_weight, self.bias3_weight)
那既然这两种方式实现的功能都是相同的,应该怎么选呢?选择方式如下:
具有w,b等学习参数的(如conv2d, linear, batch_norm) 采用 nn.Xxx
没有w,b等学习参数的(如maxpool, loss_func, activation func) 等根据个人选择使用 nn.functional.xxx 或 nn.Xxx
最后,关于 dropout,强烈推荐使用 nn.Xxx 方式,因为一般情况下只有训练阶段才进行 dropout,在 eval 阶段不会进行 dropout。使用nn.Xxx 方法定义 dropout,在调用 model.eval() 之后,model 中所有的 dropout layer 都关闭,但以 nn.functional.dropout 方式定义 dropout,在调用 model.eval() 之后并不能关闭 dropout。需要使用 F.dropout(x, trainig=self.training)。综合来看,建议没啥事还是选nn.Xxx方式吧。。。
nn.Linear
这就是全连接层啦,可以实例化出一个全连接层对象
nn.Module
这是一个经常用来被继承的类,继承nn.Module,可以构造一个保存weights,bias和具有前向传播方法(forward step)的类,nn.Module还拥有大量的属性和方法(例如.parameters和.zero_grad()等)
对于继承了nn.Module的类,都有如下特性:
首先该网络模型的初始化方法__init__需要继承父类nn.Module的初始化方法,用语句super().init()实现。并在初始化方法里面,定义了卷积、BN、激活函数等。接下来定义forward方法,将整个网络连接起来。
class Net(nn.Module):
'''
Net将进行Conv2d,BN,ReLU三步操作
forward执行了以上操作的前向传播的计算工作
'''
def __init__(self, in_ch, out_ch, dirate):
super(Net, self).__init__()
self.conv_s1 = nn.Conv2d(
in_ch, out_ch, 3, padding=1 * dirate, dilation=1 * dirate
)
self.bn_s1 = nn.BatchNorm2d(out_ch)
self.relu_s1 = nn.ReLU(inplace=True)
def forward(self, x):
hx = x
xout = self.relu_s1(self.bn_s1(self.conv_s1(hx)))
return xout
net = Net(3,3,1)
xout = net(x)
可以看到我们可以实例化一个对象:
net = Net(3,3,1)
然后进行前向传播,使用
xout = net (x)
其中x是该网络的输入,xout 是输出,实现了forward方法的功能。这里就会有人感到奇怪,forward作为Net这个类的方法,使用的时候不应该是xout = net .forward(x)吗?
这里为什么一个类的实例可以当做方法直接使用?这是因为这个Net类继承的父类nn.Module里面定义了__call__方法。一个类如果定义了__call__方法,则该类的实例就可以作为一个方法那样直接使用。
实例化的时候,类的名称后面括号可以传递参数,例如前面实例化Net的时候,传递in_ch,out_ch等参数。但是要利用__call__的特性,是在实例名后面的括号中传递参数,例如上面的例子xout = net(x)。
回到网络模型的内容上来。翻看nn.Module的部分源码,可以发现,nn.Module里面果然定义了__call__。
def _call_impl(self, *input, **kwargs):
forward_call = (self._slow_forward if torch._C._get_tracing_state() else self.forward)
# If we don't have any hooks, we want to skip the rest of the logic in
# this function, and just call forward.
if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
or _global_forward_hooks or _global_forward_pre_hooks):
return forward_call(*input, **kwargs)
# Do not call functions when jit is used
full_backward_hooks, non_full_backward_hooks = [], []
if self._backward_hooks or _global_backward_hooks:
full_backward_hooks, non_full_backward_hooks = self._get_backward_hooks()
if _global_forward_pre_hooks or self._forward_pre_hooks:
for hook in (*_global_forward_pre_hooks.values(), *self._forward_pre_hooks.values()):
result = hook(self, input)
if result is not None:
if not isinstance(result, tuple):
result = (result,)
input = result
bw_hook = None
if full_backward_hooks:
bw_hook = hooks.BackwardHook(self, full_backward_hooks)
input = bw_hook.setup_input_hook(input)
result = forward_call(*input, **kwargs)
if _global_forward_hooks or self._forward_hooks:
for hook in (*_global_forward_hooks.values(), *self._forward_hooks.values()):
hook_result = hook(self, input, result)
if hook_result is not None:
result = hook_result
if bw_hook:
result = bw_hook.setup_output_hook(result)
# Handle the non-full backward hooks
if non_full_backward_hooks:
var = result
while not isinstance(var, torch.Tensor):
if isinstance(var, dict):
var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
else:
var = var[0]
grad_fn = var.grad_fn
if grad_fn is not None:
for hook in non_full_backward_hooks:
wrapper = functools.partial(hook, self)
functools.update_wrapper(wrapper, hook)
grad_fn.register_hook(wrapper)
self._maybe_warn_non_full_backward_hook(input, result, grad_fn)
return result
__call__ : Callable[..., Any] = _call_impl
这里__call__ : Callable[..., Any] = _call_impl代表call函数调用了 _call_impl,而在 _call_impl中,我们能得到如下的函数执行顺序:
在Module被调用执行时,通常会首先调用self.call方法,出发_call_impl()方法的执行,然后会逐渐调用各种系统、前向、后向相关的hooks,大概的调用示意图如下:
self.__call__ >>>
self._call_impl >>>
_global_forward_pre_hooks >>>
self._forward_pre_hooks >>>
self.forward / self.slow_forward >>>
_global_forward_hooks >>>
self._forward_hooks >>>
register _global_backward_hooks >>>
register self._backward_hooks
这些hooks在forward、backward、state_dict()等事件被触发前、后被相应调用,可以通过这些hooks来定义一些触发事件或改变模型、buffer参数等。
其中有result = self.forward(*input,**kwargs),从而使得所有的model函数在调用的时候变会调用call,于是调用forward。
那如果仔细观察了nn.Module的话,其实还有一个点值得注意。其实nn.Module里面并没有定义forward,但他却调用self.forward,严格来说,他是“想要”调用self.forward。如果我们没有定义一个类,例如Net,来继承nn.Module,并且在这个类里面定义forward,那么nn.Module中__call__下面的self.forward就是无效的。这意味着,父类中__call__下面调用的函数,可以在继承他的子类中定义。下面给出一个简单的例子。
class Origin():
def __call__(self):
self.forward()
print('I''m the origin class!')
class Later(Origin):
def forward(self):
print('Forward!')
origin = Origin()
later = Later()
这里定义了父类Origin,并定义了继承他的一个子类Later。此外还进行了他们的实例化。显然,在Origin的__call__方法下面,调用了self.forward,但是没有定义。Later在继承了Origin之后,定义了forward。首先,这段代码不会报错,即使Origin的__call__下面的self.forward并没有定义,这也是前面我说的,虽然没有定义forward,但是可以理解为他“想要”调用self.forward。那么在Later记成了Origin之后,进行了forward的定义,这使得Later本身可以调用forward。
在上面这段代码的基础上,如果我们执行origin(),将会报错:
AttributeError: 'Origin' object has no attribute 'forward',这解释了forward没有定义,只是“想要”调用self.forward。
如果我们执行later(),则如下图输出。显然,在Later中补充了forward的定义,就可以成功调用。
执行: later()
也就是执行__call__方法,Later没有重写__call__,所以执行的是父类的__call__方法,流程就是:self.forward()、print('Forward!')、print('I''m the origin class!')
unsqueeze与squeeze
unsqueeze就是用来扩充维度的,具体如下:
import torch
a = torch.ones(3,4)
print(a)
# 在最外面的括号外再加一个括号
a = a.unsqueeze(0)
print(a)
b = torch.ones(3,4)
# 在外面第二层的括号外再加一个括号
b = b.unsqueeze(1)
print(b)
c = torch.ones(3,4)
# 在最里面的每个数字上再加一个括号
c = c.unsqueeze(2)
print(c)
'''
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
tensor([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
tensor([[[1., 1., 1., 1.]],
[[1., 1., 1., 1.]],
[[1., 1., 1., 1.]]])
tensor([[[1.],
[1.],
[1.],
[1.]],
[[1.],
[1.],
[1.],
[1.]],
[[1.],
[1.],
[1.],
[1.]]])
'''
那有维度扩充就有维度删除,维度删除可以用squeeze:
import torch
a = torch.ones(3,4)
print(a)
a = a.unsqueeze(0)
print(a)
a = a.squeeze(0)
print(a)
# a = a.squeeze() # 注意,没有参数时,默认参数为0
# print(a)
'''
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
tensor([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
'''
这里再介绍一个numpy数组的squeeze方法:
import torch
import numpy as np
a = torch.ones(3,4)
print(a)
a = a.unsqueeze(0)
a = np.array(a)
print(a)
# numpy也有删除维度的函数,可以处理numpy数组,但是没有unsqueeze函数
a = np.squeeze(a,axis = 0)
print(a)
'''
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
[[[1. 1. 1. 1.]
[1. 1. 1. 1.]
[1. 1. 1. 1.]]]
[[1. 1. 1. 1.]
[1. 1. 1. 1.]
[1. 1. 1. 1.]]
'''
torch.optim
Optimizer
是所有优化器的父类,它主要有如下公共方法:
- add_param_group(param_group): 添加模型可学习参数组
- step(closure): 进行一次参数更新
- zero_grad(): 清空上次迭代记录的梯度信息
- state_dict(): 返回 dict 结构的参数状态
- load_state_dict(state_dict): 加载 dict 结构的参数状态
torch.optim中含有各种优化器,可以使用优化器的step来进行前向传播,而不用人工的更新所有参数
optimizer = torch.optim.Adam(...) #选择一种优化器
scheduler = torch.optim.lr_scheduler.... # 选择一种动态调整学习率的方法,可以是下面几种之一
for epoch in range(100):
opt.zero_grad() # 放在这个位置
opt.step()
optim.zero_grad()将所有的梯度置为0,需要在下个批次计算梯度之前调用;
通常还可以采用with torch.no_grad():的方式来实现梯度清零的目的。
具体使用步骤:
在训练过程中先调用 optimizer.zero_grad()
清空梯度,再调用 loss.backward()
反向传播,最后调用 optimizer.step()
更新模型参数
torch.optim.lr_scheduler.xxx
这是用来动态调整学习率的封装方法,常用的调整学习率的方法有以下几种:
torch.optim.lr_scheduler.LambdaLR
torch.optim.lr_scheduler.StepLR
torch.optim.lr_scheduler.ExponetialLR
torch.optim.lr_scheduler.MultiStepLR
torch.optim.torch.optim.lr_scheduler.ReduceLROnPlateau
optimizer = torch.optim.Adam(...) #选择一种优化器
scheduler = torch.optim.lr_scheduler.... # 选择一种动态调整学习率的方法,可以是下面几种之一
for epoch in range(100):
train(...)
validate(...)
optimizer.step()
scheduler.step() # 需要在优化器参数更新之后再动态调整学习率
torch格式转换:.numpy、.item()、.cpu()、.detach()、.data
.numpy
Tensor.numpy()将Tensor转化为ndarray;
这里的Tensor可以是标量或者向量(与item()不同)转换前后的dtype不会改变。
a = torch.tensor([[1.,2.]]) # tensor([[1., 2.]])
a_numpy = a.numpy() # [[1., 2.]]
.item()
这个可以把tensor转化成标量(int,float)
optimizer.zero_grad()
outputs = model(data)
loss = F.cross_entropy(outputs, label)
#计算这一个batch的准确率
acc = (outputs.argmax(dim=1) == label).sum().cpu().item() / len(labels) #这里也用到了.item()
loss.backward()
optimizer.step()
train_loss += loss.item() #这里用到了.item()
train_acc += acc
一般都是在训练时,将loss转换为标量进行运算,以及进行分类任务,计算准确值值时需要这样转换出来。
.cpu()
将数据的处理设备从其他设备(如.cuda()拿到cpu上),不会改变变量类型,转换后仍然是Tensor变量。
.detach()
.detach()就是返回一个新的tensor,并且这个tensor是从当前的计算图中分离出来的,返回的tensor和原来的tensor是共享内存空间的,也就是会随原来的tensor变化而变化,该方法只取出原来Tensor的tensor数据,却丢弃了grad、grad_fn等额外的信息。
tensor具有in-place函数,其in-place函数修改会在这两个tensor上同时体现(因为它们共享内存数据),此时当要对其调用backward()时可能会导致错误,具体细节如下:
所有的tensor都会记录用在他们身上的 in-place operations。如果pytorch检测到tensor在一个Function中已经被保存用来backward,但是之后它又被in-place operations修改。当这种情况发生时,在backward的时候,pytorch就会报错。这种机制保证了,如果你用了in-place operations,但是在backward过程中没有报错,那么梯度的计算就是正确的。(也就是一个tensor进行了反向传播就会修改其in-place operations,再对该tensor.detach()后返回出来的tensor进行backward(),就会再次修改其in-place operations,因为.detach()后返回的tensor与之前的tensor是共享内存空间的,也就是连in-place的修改操作也会共享,而修改两次in-place operations的操作是不被允许的,所以会报错,这可以用报错来提醒用户,梯度计算出现了错误。)
举个例子来说明一下.detach()具体有什么用。如果A网络的输出被喂给B网络作为输入, 且我们希望在梯度反传的时候只更新B中参数的值,而不更新A中的参数值(梯度进行反向传播时,是先计算梯度并更新后面的B网络的参数,再计算梯度并更新前面的A网络的参数,所以完全可以在A与B中间阶段梯度反向传播,进而使得参数只在后半段更新),这时候就可以使用.detach()。
import torch
a = torch.randn(2,requires_grad = True)
print('a.requires_grad = {}'.format(a.requires_grad))
b = a.sigmoid()
print('b.requires_grad = {}'.format(b.requires_grad))
c = a.detach().sigmoid() # a.detach()后将会阻断梯度传播,只能改变a的值,却不会再对其求偏导
print('c.requires_grad = {}'.format(c.requires_grad))
'''
a.requires_grad = True
b.requires_grad = True
c.requires_grad = False
'''
可以看到,.detach()之后,就不会进行反向传播了,那么就会导致,在此之前的梯度都将保持静默,在此之后的梯度则不会受到任何影响,即使之后重新将它的requires_grad置为true也不会发生改变。
.data
与.detach()类似,都是阻断反向梯度传播,但是,这个操作无法保证in-place的安全性,也就是,可能会重复修改in-place operations而不报错,此时的梯度计算就是错误的了,总结就是用.detach()。