zoukankan      html  css  js  c++  java
  • Pytorch: torch.nn

    import torch as t
    from torch import nn
    
    class Linear(nn.Module): # 继承nn.Module
        def __init__(self, in_features, out_features):
            super(Linear, self).__init__() # 等价于nn.Module.__init__(self)
            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) # x.@(self.w)
            return x + self.b.expand_as(x)
    • 自定义层Linear必须继承nn.Module,并且在其构造函数中需调用nn.Module的构造函数,即super(Linear, self).__init__() 或nn.Module.__init__(self),推荐使用第一种用法,尽管第二种写法更直观。
    • 在构造函数__init__中必须自己定义可学习的参数,并封装成Parameter,如在本例中我们把wb封装成parameterparameter是一种特殊的Tensor,但其默认需要求导(requires_grad = True),感兴趣的读者可以通过nn.Parameter??,查看Parameter类的源代码。
    • forward函数实现前向传播过程,其输入可以是一个或多个tensor。
    • 无需写反向传播函数,nn.Module能够利用autograd自动实现反向传播,这点比Function简单许多。
    • 使用时,直观上可将layer看成数学概念中的函数,调用layer(input)即可得到input对应的结果。它等价于layers.__call__(input),在__call__函数中,主要调用的是 layer.forward(x),另外还对钩子做了一些处理。所以在实际使用中应尽量使用layer(x)而不是使用layer.forward(x),关于钩子技术将在下文讲解。
    • Module中的可学习参数可以通过named_parameters()或者parameters()返回迭代器,前者会给每个parameter都附上名字,使其更具有辨识度。

    可见利用Module实现的全连接层,比利用Function实现的更为简单,因其不再需要写反向传播函数。

    module中parameter的命名规范:

    • 对于类似self.param_name = nn.Parameter(t.randn(3, 4)),命名为param_name
    • 对于子Module中的parameter,会其名字之前加上当前Module的名字。如对于self.sub_module = SubModel(),SubModel中有个parameter的名字叫做param_name,那么二者拼接而成的parameter name 就是sub_module.param_name

    ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如tensor.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。

    在以上的例子中,基本上都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(feedforward neural network)。对于此类网络如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的module,它包含几个子Module,前向传播时会将输入一层接一层的传递下去。ModuleList也是一个特殊的module,可以包含几个子module,可以像用list一样使用它,但不能直接把输入传给ModuleList。下面举例说明。

    # Sequential的三种写法
    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())
    
    net2 = nn.Sequential(
            nn.Conv2d(3, 3, 3),
            nn.BatchNorm2d(3),
            nn.ReLU()
            )
    
    from collections import OrderedDict
    net3= nn.Sequential(OrderedDict([
              ('conv1', nn.Conv2d(3, 3, 3)),
              ('bn1', nn.BatchNorm2d(3)),
              ('relu1', nn.ReLU())
            ]))
    print('net1:', net1)
    print('net2:', net2)
    print('net3:', net3)
    
    
    net1: Sequential(
      (conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
      (batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (activation_layer): ReLU()
    )
    net2: Sequential(
      (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
      (1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (2): ReLU()
    )
    net3: Sequential(
      (conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
      (bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu1): ReLU()
    )
    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)
    # 下面会报错,因为modellist没有实现forward方法
    # output = modelist(input)
    
    # 看到这里,读者可能会问,为何不直接使用Python中自带的list,而非要多此一举呢?这是因为ModuleList是Module的子类,当在Module中使用它的时候,就能自动识别为子module。
    
    class MyModule(nn.Module):
        def __init__(self):
            super(MyModule, self).__init__()
            self.list = [nn.Linear(3, 4), nn.ReLU()]
            self.module_list = nn.ModuleList([nn.Conv2d(3, 3, 3), nn.ReLU()])
        def forward(self):
            pass
    model = MyModule()
    model
    
    MyModule(
      (module_list): ModuleList(
        (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
        (1): ReLU()
      )
    )

    可见,list中的子module并不能被主module所识别,而ModuleList中的子module能够被主module所识别。这意味着如果用list保存子module,将无法调整其参数,因其未加入到主module的参数中。

    除ModuleList之外还有ParameterList,其是一个可以包含多个parameter的类list对象。在实际应用中,使用方式与ModuleList类似。如果在构造函数__init__中用到list、tuple、dict等对象时,一定要思考是否应该用ModuleList或ParameterList代替。

    损失函数:

    # batch_size=3,计算对应每个类别的分数(只有两个类别)
    score = t.randn(3, 2)
    # 三个样本分别属于1,0,1类,label必须是LongTensor
    label = t.Tensor([1, 0, 1]).long()
    
    # loss与普通的layer无差异
    criterion = nn.CrossEntropyLoss()
    loss = criterion(score, label)
    loss

    优化器:

    from torch import  optim
    optimizer = optim.SGD(params=net.parameters(), lr=1)
    optimizer.zero_grad() # 梯度清零,等价于net.zero_grad()
    
    input = t.randn(1, 3, 32, 32)
    output = net(input)
    output.backward(output) # fake backward
    
    optimizer.step() # 执行优化
    
    # 为不同子网络设置不同的学习率,在finetune中经常用到
    # 如果对某个参数不指定学习率,就使用最外层的默认学习率
    optimizer =optim.SGD([
                    {'params': net.features.parameters()}, # 学习率为1e-5
                    {'params': net.classifier.parameters(), 'lr': 1e-2}
                ], lr=1e-5)
    optimizer
    # 只为两个全连接层设置较大的学习率,其余层的学习率较小
    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())
    
    optimizer = t.optim.SGD([
                {'params': base_params},
                {'params': special_layers.parameters(), 'lr': 0.01}
            ], lr=0.001 )
    optimizer
    # 方法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)
    optimizer1
    # 方法2: 调整学习率, 手动decay, 保存动量
    for param_group in optimizer.param_groups:
        param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
    optimizer

    nn中还有一个很常用的模块:nn.functional,nn中的大多数layer,在functional中都有一个与之相对应的函数。nn.functional中的函数和nn.Module的主要区别在于,用nn.Module实现的layers是一个特殊的类,都是由class layer(nn.Module)定义,会自动提取可学习的参数。而nn.functional中的函数更像是纯函数,由def function(input)定义。下面举例说明functional的使用,并指出二者的不同之处。

    此时读者可能会问,应该什么时候使用nn.Module,什么时候使用nn.functional呢?答案很简单,如果模型有可学习的参数,最好用nn.Module,否则既可以使用nn.functional也可以使用nn.Module,二者在性能上没有太大差异,具体的使用取决于个人的喜好。如激活函数(ReLU、sigmoid、tanh),池化(MaxPool)等层由于没有可学习参数,则可以使用对应的functional函数代替,而对于卷积、全连接等具有可学习参数的网络建议使用nn.Module。下面举例说明,如何在模型中搭配使用nn.Module和nn.functional。另外虽然dropout操作也没有可学习操作,但建议还是使用nn.Dropout而不是nn.functional.dropout,因为dropout在训练和测试两个阶段的行为有所差别,使用nn.Module对象能够通过model.eval操作加以区分。

    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

    对于不具备可学习参数的层(激活层、池化层等),将它们用函数代替,这样则可以不用放置在构造函数__init__中。对于有可学习参数的模块,也可以用functional来代替,只不过实现起来较为繁琐,需要手动定义参数parameter,如前面实现自定义的全连接层,就可将weight和bias两个参数单独拿出来,在构造函数中初始化为parameter。

    初始化策略:

    在深度学习中参数的初始化十分重要,良好的初始化能让模型更快收敛,并达到更高水平,而糟糕的初始化则可能使得模型迅速瘫痪。PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑,当然我们也可以用自定义初始化去代替系统的默认初始化。而当我们在使用Parameter时,自定义初始化则尤为重要,因t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。PyTorch中nn.init模块就是专门为初始化而设计,如果某种初始化策略nn.init不提供,用户也可以自己直接初始化。

    nn.Module深入分析

    如果想要更深入地理解nn.Module,究其原理是很有必要的。首先来看看nn.Module基类的构造函数:

    def __init__(self):
        self._parameters = OrderedDict()
        self._modules = OrderedDict()
        self._buffers = OrderedDict()
        self._backward_hooks = OrderedDict()
        self._forward_hooks = OrderedDict()
        self.training = True

    其中每个属性的解释如下:

    • _parameters:字典,保存用户直接设置的parameter,self.param1 = nn.Parameter(t.randn(3, 3))会被检测到,在字典中加入一个key为'param',value为对应parameter的item。而self.submodule = nn.Linear(3, 4)中的parameter则不会存于此。
    • _modules:子module,通过self.submodel = nn.Linear(3, 4)指定的子module会保存于此。
    • _buffers:缓存。如batchnorm使用momentum机制,每次前向传播需用到上一次前向传播的结果。
    • _backward_hooks_forward_hooks:钩子技术,用来提取中间变量,类似variable的hook。
    • training:BatchNorm与Dropout层在训练阶段和测试阶段中采取的策略不同,通过判断training值来决定前向传播策略。

    上述几个属性中,_parameters_modules_buffers这三个字典中的键值,都可以通过self.key方式获得,效果等价于self._parameters['key'].下面举例说明。

    class Net(nn.Module):
        def __init__(self):
            super(Net, self).__init__()
            # 等价与self.register_parameter('param1' ,nn.Parameter(t.randn(3, 3)))
            self.param1 = nn.Parameter(t.rand(3, 3))
            self.submodel1 = nn.Linear(3, 4) 
        def forward(self, input):
            x = self.param1.mm(input)
            x = self.submodel1(x)
            return x
    net = Net()
    net
    
    net._modules
    OrderedDict([('submodel1', Linear(in_features=3, out_features=4, bias=True))])
    
    net._parameters
    OrderedDict([('param1', Parameter containing:
                  tensor([[ 0.3398,  0.5239,  0.7981],
                          [ 0.7718,  0.0112,  0.8100],
                          [ 0.6397,  0.9743,  0.8300]]))])

    nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。为方便用户访问各个子module,nn.Module实现了很多方法,如函数children可以查看直接子module,函数module可以查看所有的子module(包括当前module)。与之相对应的还有函数named_childennamed_modules,其能够在返回module列表的同时返回它们的名字。

    对于batchnorm、dropout、instancenorm等在训练和测试阶段行为差距巨大的层,如果在测试时不将其training值设为True,则可能会有很大影响,这在实际使用中要千万注意。虽然可通过直接设置training属性,来将子module设为train和eval模式,但这种方式较为繁琐,因如果一个模型具有多个dropout层,就需要为每个dropout层指定training属性。更为推荐的做法是调用model.train()函数,它会将当前module及其子module中的所有training属性都设为True,相应的,model.eval()函数会把training属性都设为False。

    在PyTorch中保存模型十分简单,所有的Module对象都具有state_dict()函数,返回当前Module所有的状态数据。将这些状态数据保存后,下次使用模型时即可利用model.load_state_dict()函数将状态加载进来。优化器(optimizer)也有类似的机制,不过一般并不需要保存优化器的运行状态。

    # 保存模型
    t.save(net.state_dict(), 'net.pth')
    
    # 加载已保存的模型
    net2 = Net()
    net2.load_state_dict(t.load('net.pth'))
    将Module放在GPU上运行也十分简单,只需两步:
    
    model = model.cuda():将模型的所有参数转存到GPU
    input.cuda():将输入数据也放置到GPU上
    至于如何在多个GPU上并行计算,PyTorch也提供了两个函数,可实现简单高效的并行GPU计算
    
    nn.parallel.data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None)
    class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
    可见二者的参数十分相似,通过device_ids参数可以指定在哪些GPU上进行优化,output_device指定输出到哪个GPU上。唯一的不同就在于前者直接利用多GPU并行计算得出结果,而后者则返回一个新的module,能够自动在多GPU上进行并行加速。
    
    # method 1
    new_net = nn.DataParallel(net, device_ids=[0, 1])
    output = new_net(input)
    
    # method 2
    output = nn.parallel.data_parallel(new_net, input, device_ids=[0, 1])
    DataParallel并行的方式,是将输入一个batch的数据均分成多份,分别送到对应的GPU进行计算,各个GPU得到的梯度累加。与Module相关的所有数据也都会以浅复制的方式复制多份,在此需要注意,在module中属性应该是只读的。

     

    nn和autograd的关系

    nn.Module利用的也是autograd技术,其主要工作是实现前向传播。在forward函数中,nn.Module对输入的tensor进行的各种操作,本质上都是用到了autograd技术。这里需要对比autograd.Function和nn.Module之间的区别:

    • autograd.Function利用了Tensor对autograd技术的扩展,为autograd实现了新的运算op,不仅要实现前向传播还要手动实现反向传播
    • nn.Module利用了autograd技术,对nn的功能进行扩展,实现了深度学习中更多的层。只需实现前向传播功能,autograd即会自动实现反向传播
    • nn.functional是一些autograd操作的集合,是经过封装的函数

    作为两大类扩充PyTorch接口的方法,我们在实际使用中应该如何选择呢?如果某一个操作,在autograd中尚未支持,那么只能实现Function接口对应的前向传播和反向传播。如果某些时候利用autograd接口比较复杂,则可以利用Function将多个操作聚合,实现优化,正如第三章所实现的Sigmoid一样,比直接利用autograd低级别的操作要快。而如果只是想在深度学习中增加某一层,使用nn.Module进行封装则更为简单高效。

  • 相关阅读:
    js设计模式——代理模式
    js设计模式——策略模式
    js设计模式——单例模式
    Krpano vtourskin.xml 默认皮肤详解
    通过JS动态切换大场景xml
    krpano 户型地图雷达
    微信小程序开发
    CSS3的calc()使用
    Yslow
    微信分享
  • 原文地址:https://www.cnblogs.com/ziwh666/p/12356118.html
Copyright © 2011-2022 走看看