pytorch学习

对教材《深度学习框架pytorch:入门与实践》和一些技术博客及实践过程中的总结,比较精炼。

网络模型定义

一般通过继承pytoch实现的torch.nn.Module(以下简称Module),然后定义自己的网络的每层。
这里我们先通过继承Module构建一个抽象的网络BasicModule实现了对Module.save/.load的重载,方便保存和加载已训练完毕的模型(持久化)。

class BasicModule(t.nn.Module):

    def __init__(self):
        super(BasicModule, self).__init__()     # 初始化父类
        self.model_name = 'model'   #
        print(self.model_name)

    def load(self, path):
        self.load_state_dict(t.load(path))

    def save(self, name=None):
        if name is None:
            abspath = os.getcwd()
            prefix = abspath + '/data/checkpoint/' + self.model_name + '_'
            name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
        else:
            abspath = os.getcwd()
            prefix = abspath + '/data/checkpoint/' + name + '_'
            name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
        t.save(self.state_dict(), name)
        return name

以上是对模型Module的持久化,对于tensor的持久化只需要t.load(obj,filename)/t.save(obj,filename)。而对于Module和Optimizer建议保存其state_dict()(以上就是这么做的)而不是直接保存整个模型或优化器,可以保存其参数和动量信息。

基于以上的BasicModule实现自己的Module就可以具有加载和保存参数的功能

class BriefNet(BM.BasicModule):
    def __init__(self):
        super(BriefNet, self).__init__()
        # nn.ReLU 和 functional.relu的差别(大部分nn.layer 都对应一个nn.functional中的函数):
        # 前者是nn.Module的子类,后者是nn.functional的子类
        # nn.中许多layer和激活函数都是继承与nn.Module的可以自动提取派生类中的可学习参数(通过继承Module实现的__getattr__和__setattr__)
        # functional是没有可学习参数的,可以用在激活函数、池化等地方
        self.fc = nn.Sequential(
            nn.Linear(784, 256),  # 数据集图片为28*28单通道,此处直接用多层感知机而不是用CNN
            nn.ReLU(),
            nn.Dropout(0.3),

            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Dropout(0.3),

            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Dropout(0.3),

            nn.Linear(64, 10),
            nn.LogSoftmax(dim=1)
        )

    def forward(self, x):
        # 定义前向传播函数
        x = x.view(x.shape[0], -1)  # 将输入图片变成列向量输入
        x = self.fc(x)
        return x

定义起来还是很简单的,网络定义有两种比较常见的格式(无性能差别)

  • 一种是使用nn.Sequantial(推荐),其也是Module的子类,相对于ModuleList而言可以直接输入如 self.fc(x)nn.Sequantial中定义了一些列的nn.Module的子类如:Linear(全连接、仿射层)、ReLu(激活函数)、Dropout、SoftMax、Conv等常用的layers,使用Sequential可以很方便的把这些layer的输入输出自动连接起来,自己来定义layer间的组合而得到一个复合layer(代码结构意义上的一个,只需要给定整个sequential一个输入,就能得出一个输出,而不需要一层一层的输入输出和第二种方法有差别),这样的好处是定义简洁、且通过对本model的控制如models.train()、models.eval()就可以自动的设置其内部那些训练和验证具有不同表现的layer所处的状态或model.zero_grad()实现对所有叶节点参数梯度清空操作。更有一体感,方便宏观掌控。

  • 另一种是通过在__init__中自己声明一些nn中实现的layer的实例属性(相较于之前这是比较分散的)。

    class Classifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 64)
        self.fc4 = nn.Linear(64, 10)
        
    def forward(self, x):
        # make sure input tensor is flattened
        x = x.view(x.shape[0], -1)
        
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        x = F.log_softmax(self.fc4(x), dim=1)
        
        return x
    

    nn中实现的大多数layers在nn.functional中也有对应的实现(一般为全小写函数如nn.functinal.relu、softmax、F.conv)。介绍到这,注意一下代码中注释提示:用Module实现的子类layer能自动提取内部的类型为nn.parameter可学习参数(需要变化的参数),而nn.functional中的函数更像是纯函数调用时必须给输入如F.relu(input)、F.linear(input,model.weight,model.bias)。且functional这种纯函数的用法更适合那些没有可学习参数的层如pooling、ReLU、sigmoid、tanh,他们参数都是固定的,不需要输入需要学习的参数到functional.xxx()的输入中,可以保持完整感。但是对于dropout建议使用Module,否则需要手动给出当前训练状态F.dropout(input,training=self.training)

  • 然后就是实现forward。给网络一个输入即output=model(input)其内部会调用model.__call__(nn.Module实现),call中主要是调用了forward。forward主要负责计算图的构建,然后我们就能根据计算图进行自动求导计算。

  • 这里说一下以上内容的实现原理:

    1. 为何nn.Module可以自动提取子类内部的可学习参数:
      因为nn.Module中实现了两个魔法方法.__getattr__和.__setattr__。而obj.name=value 等价于setattr(obj,'name',value)obj.name 等价于 getattr(obj,'name'),当定义了自定义方法后getattr和setattr会调用自定义的.__getattr__和.__setattr__。然后,当用户在子类定义实例属性时候就会触发.__setattr__(obj,'name',value),在此函数中会判断obj类型是否是Parameter或nn.Module对象,如果是就存放到_parameters和_models两个私有字典中;如果是其他对象,则调用默认操作:添加到__dict__字典中。而_models中的都是子models,他们在定义的时候也会在其内部的._models字典中存放其子models。最终可能是会对本自定义models内的所有子models(含迭代)的parameter进行一些管理。

    2. 是如何实现自动求导的:
      在这之前先了解一下torch的一些数据结构:nn.Parameter、autograd.variable、torch.tensor
      a. 其中tensor是numpy中array类似的多维数组。
      b.variable是以tensor作为核心data对象并进行了一些 扩充(如grad梯度值(目标函数对本变量的偏导,也是variable类)、grad_fn梯度计算函数、requires_grad是否需要自动求导功能),使其能进行自动求导功能。且model(网络模型)的输入都是以variable形式输入的。如果数据集经过变换to_tensor在输入前还需要input=Variable(input)。注释:autograd只实现了标量目标函数对叶节点求导,对目标函数进行backward()后叶节点变量x就可以得到目标函数对x偏导x.grad,但是叶节点有标记AccumulateGrad会自动累加历次backward的梯度值。因此调用反向传播时需要清零梯度x.grad.data.zero_()inplace操作清零
      c. Parametervariable的子类,且默认是requires_grad=True。能在定义Model时候自动被基类nn.Module管理。

    3. 什么是计算图(pytorch是动态图):计算图是一种有向无环图且图中有两种节点:

      • 一种是○表示的变量(variable);
      • 另一种是□表示的算子(如已实现重载的+、-、*、/、**n等.sum()、t.exp等已实现函数基于Function类自定义运算符)。

      其中,用户声明的Variable为叶节点(需要requires_grad的)是依赖于其他变量了;其他节点都是通过对叶节点及其运算结果进行复合运算得到的一些中间Variable和最终的目标函数。我们定义用户Variable,拿他们他们之间运算结果做运算时,会自动根据重载或以实现的运算函数(都是算子)进行边的连接(输入输出关系,是有向的)(并在算子内储存一份输入输出拷贝用来梯度计算时使用),然后生成一个图。图中任意一点都可以利用链式法则实现!!因此当对requires_grad的叶子节点求导时,中间变量Variable都会自动.requires_grad=True,但非叶子节点计算完后会清空梯度。此外是利用以下几个规则来进行链式法则的:当y是标量的时候可以进行y.backward(),但是y是向量(认为是中间variable)其求梯度必须输入一个参数y.backward(grad_y)否则会提示只能进行标量对向量求梯度,其中grad_y=标量目标函数z对向量y求偏导

    4. 目前大多数算子都可以使用autograd的反向求导,自己写的复杂函数没有,如何解决:自己实现的复杂函数(算子)不具备自动求导功能(比如y=x**2+x*2支持,但y=func(x)的func不支持)。可以利用autograd.Function对autograd功能进行拓展,其中Function对应计算图中的□。需要自己实现一个继承与autograd.Function的算子,并自己实现求导。这样就可以在计算图使用它并使得链式法则进行下去了。其中forward输入输出都是tensor,而backward的输入和输出都是variable。backward函数的输入对应forwad的输出,backward的输出对应forward的输入。然后用Function.apply(variable)即可调用实现的Function。如z=func(V(...),V(...),...)会自动调用forward并提取Variable中的data(tensor类型)作为输入然后把输出的tensor包装成Variable,z.backward()就会自动调用func.backward()

      class func(Function):
        @staticmethod
        def forward(ctx,input1,input2,...):
            ctx.save_for_backward(input1,input2,...,)
            output = # some opertation with implemently factor
            return output
        @staticmethod
        def backward(ctx,grad_z_input) #grad_z_input对应y.backward()中输入的前链dz/dy
            input1,input2,... = ctx.saved_variables
            grad_input1_x = grad_z_input * (...)
            grad_input2_x = grad_z_input * (...)
            return grad_input1_x,grad_input2_x,...
      

数据集加载

训练

训练除了需要数据集,还需要损失函数/目标函数、优化器。其中损失函数是标量函数,将网络的输出(多维向量)进行某种评估得分。我们训练的方法就是BP即通过求取目标函数对输入(叶节点)的梯度,然后通过优化器来求去网络可学习参数的变化量的。

损失函数

torch.nn中实现了许多损失函数如:NLLLoss、CrossEntropyLoss等常用损失函数。
如果网络训练需要用GPU则需要使用对应的损失函数即.cuda()。因为为了用GPU进行训练,网络参数在GPU上、输入也需要在GPU上才能进行前向传播、网络输出的向量也是在GPU上的,为了评估输出,损失函数计算时候的参数也是GPU上的才能和网络输出进行运算,因此损失函数求出的score也是GPU上的。

    if option.use_gpu:
        criterion = t.nn.NLLLoss().cuda()
    else:
        criterion = t.nn.NLLLoss()
优化器

优化器根据criterion.backward()得到model.parameters().grad。然后得到的梯度进行一些优化策略从而计算出网络中可学习参数的更新值。并能通过model.zero_grad()或optimizer.zero_grad()清空对叶节点(网络可学习参数)梯度。
torch.optim中实现了许多常用的优化器如:SGD(根据参数觉得是否用动量)、Adam...
这里注意:学习率和weight_decay(可能导致学习率为0)要小心设计否则优化结果导致训练发散或不变。

optimizer = optim.SGD(model.parameters(), lr=option.lr, momentum=0.8)  # , weight_decay=option.lr_decay) # 慎用,
训练

网络设置为训练模式 -> 梯度清零 -> 输入网络(Variable输入) -> 根据实际值和输出值计算损失函数输出评分 -> 求评分对网络可学习参数梯度 -> 优化器更新网络可学习参数

            model.train()
            optimizer.zero_grad()
            ps = model(x)
            loss = criterion(ps, target)
            loss.backward()
            optimizer.step()

训练完需要model.save(path)保存网络

测试

需要先model.load(path)加载训练后的参数
把网络设置成测试模型 -> 输入 -> (optional:计算损失函数 -> ) 获得输出 -> 输出和已知标签对比 -> 计算准确度

        model.eval()
        ps = model(val_input)
        test_loss = criterion(ps, val_labels)
        top_p, top_class = t.exp(ps).topk(1, dim=1)
        equals = top_class == val_labels.view(*top_class.shape)
        accuracy += t.mean(equals.type(t.FloatTensor))

结语

至此,就简要的记完了这几天学习pytorch的结果了。

你可能感兴趣的:(pytorch学习)