Pytorch学习笔记

前言

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()。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会抓鼠鼠的阿猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值