pytorch_神经网络模型搭建系列(2):自定义神经网络层

目录

  • 1、nn.Module——搭建属于自己的神经网络
    • 1.1 回顾系统预定义的层
      • 1.1.1 最常用的Linear层
      • 1.1.2 Conv2d类的源代码
      • 1.1.3 小结
      • 1.1.4 自定义层的基本步骤
    • 1.2 简单实现-自定义层
      • 1.2.1 第一步:定义一个的层(即一个类)
      • 1.2.2 第二步:定义一个神经网络模型
      • 1.2.3 第三步:训练模型
      • 1.2.4 小结
    • 1.3 补充:model.parameters()和model.state_dict()
      • 1.3.1 model.parameters()
      • 1.3.2 model.state_dict()
    • 1.4 总结
    • 1.5 参考资料

1、nn.Module——搭建属于自己的神经网络

前言

首先,回顾一下上一次的核心内容:使用nn.Mudule来搭建自己的神经网络模型,有多种方式来构建,主要涉及__init__构造函数和forward方法,见上一篇文章pytorch_神经网络模型搭建系列(1):自定义神经网络模型

  • 千万记住,魔法方法,定义方式:双下划线+方法名称+双下划线,一旦定义并实现了魔法方法,那么初始化对象的时候会自动调用该魔法方法
    • 构造函数init实际上就是一种特殊的魔法方法,只是特别地称其为构造函数,一旦定义实现,创建实例对象的同时,就会自动调用该构造函数
    • 构造函数主要用来在创建对象时完成对实例对象属性的一些初始化等操作, 当创建对象时, 对象会自动调用它的构造函数

完成了模型的构建,那得好好分析一下,模型里面的具体构成有哪些?只有深入模型的每一个构造组成,才能更好驾驭模型和设计模型。

其实,模型的构成组分无非就是

  • 激活函数
  • 线性层和卷积层之类的(指一些矩阵运算操作)
  • 损失函数(比如均方误差损失)
  • 优化算法(比如SGD)
  • 层的连接方式(如Resnet残差网络)

(未全部列举出来)

当然,这些都是经常用的,实际上学术圈也在不断研究这些组分的影响和设计不同的组分,因为目前还没有完全弄清楚其中任何一个组分,所以我们都能自由发挥,设计自己的组分以及组合成自己的网络模型

模型的搭建已经讲过了,今天说说层的构建,也就是如何自定义层?

  • 事实上,在pytorch里面自定义层也是通过继承自nn.Module类来实现的,前面说过,pytorch里面一般是没有层的概念,层也是当成一个模型来处理的,这里和keras是不一样的

  • 也可以直接通过继承torch.autograd.Function类来自定义一个层,但是这很不推荐,不提倡,至于为什么后面会介绍

  • keras更加注重的是层Layer,pytorch更加注重的是模型Module

  • 本文介绍如何通过nn.Module类来实现自定义层

1.1 回顾系统预定义的层

1.1.1 最常用的Linear层

  • Linear层的官方源代码
import math
import torch
from torch.nn.parameter import Parameter
from .. import functional as F
from .. import init
from .module import Module
from ..._jit_internal import weak_module, weak_script_method
 
class Linear(Module):
    __constants__ = ['bias']
 
    def __init__(self, in_features, out_features, bias=True):
        super(Linear, self).__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.weight = Parameter(torch.Tensor(out_features, in_features))
        if bias:
            self.bias = Parameter(torch.Tensor(out_features))
        else:
            self.register_parameter('bias', None)
        self.reset_parameters()
 
    def reset_parameters(self):
        init.kaiming_uniform_(self.weight, a=math.sqrt(5))
        if self.bias is not None:
            fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
            bound = 1 / math.sqrt(fan_in)
            init.uniform_(self.bias, -bound, bound)
 
    @weak_script_method
    def forward(self, input):
######################显然,这里调用了低层的F.linear############################
        return F.linear(input, self.weight, self.bias)
 
    def extra_repr(self):
        return 'in_features={}, out_features={}, bias={}'.format(
            self.in_features, self.out_features, self.bias is not None
        )

1.1.2 Conv2d类的源代码

  • Conv2d卷积层的官方源代码
class Conv2d(_ConvNd):
    def __init__(self, in_channels, out_channels, kernel_size, stride=1,
                 padding=0, dilation=1, groups=1,
                 bias=True, padding_mode='zeros'):
        kernel_size = _pair(kernel_size)
        stride = _pair(stride)
        padding = _pair(padding)
        dilation = _pair(dilation)
        super(Conv2d, self).__init__(
            in_channels, out_channels, kernel_size, stride, padding, dilation,
            False, _pair(0), groups, bias, padding_mode)
 
    @weak_script_method
    def forward(self, input):
        if self.padding_mode == 'circular':
            expanded_padding = ((self.padding[1] + 1) // 2, self.padding[1] // 2,
                                (self.padding[0] + 1) // 2, self.padding[0] // 2)
            return F.conv2d(F.pad(input, expanded_padding, mode='circular'),
                            self.weight, self.bias, self.stride,
                            _pair(0), self.dilation, self.groups)
######################显然,这里调用了低层的F.conv2d############################
        return F.conv2d(input, self.weight, self.bias, self.stride,
                        self.padding, self.dilation, self.groups)

1.1.3 小结

之前讲过,torch里面实现神经网络有两种方式:

  • (1)高层API方法:使用高层类,使用torch.nn.**来实现

  • (2)低层API方法:使用低层函数方法,torch.nn.functional.****来实现

推荐使用高层API的方法,因为:

  • 高层API是使用类的形式来包装的,既然是类就可以存储参数,比如全连接层的权值矩阵、偏置矩阵等都可以作为类的属性存储着

  • 低层API仅仅是实现函数的运算功能,没办法保存这些信息,会丢失参数信息,但是高层API是依赖于低层API的计算函数的,比如上面的两个层:

    • Linear高级层(类封装) ——> 低层F.linear()函数
    • Conv2d高级层(类封装) ——> 低层F.conv2d()函数

1.1.4 自定义层的基本步骤

要实现一个自定义层大致分以下几个主要的步骤:

  • 自定义一个类,继承自nn.Module类,并且一定要实现两个方法,第一个是构造函数__init__,第二个是层的逻辑运算函数,即所谓的前向计算函数forward函数

  • 在构造函数_init__中实现层的参数定义

    • 比如Linear层的权重和偏置,Conv2d层的in_channels, out_channels, kernel_size, stride=1,padding=0, dilation=1, groups=1,bias=True, padding_mode='zeros’这一系列参数
  • 在前向传播forward函数里面实现前向运算

    • 一般通过torch.nn.functional.***函数来实现,当然很多时候也需要自定义自己的运算方式
  • 如果该层含有权重,那么权重必须是nn.Parameter类型,关于Tensor和Variable(0.3版本之前)与Parameter的区别请参阅相关的文档。简单说就是Parameter默认需要求导,其他两个类型则不会

    • 为自己定义的新层提供默认的参数初始化,以防使用过程中忘记初始化操作
  • 补充:一般情况下,我们定义的参数是可以求导的

    • 如果自定义操作如不可导,需要实现backward函数

总结:

自定义层和自定义模型是一样的,核心都是实现最基本的构造函数__init__和前向传播forward方法

1.2 简单实现-自定义层

如果要实现一个简单的层,一个非线性层,这个层的功能是 y = w ∗ ( x 2 + b ) y = w*{(x^2+b)} y=w(x2+b)

1.2.1 第一步:定义一个的层(即一个类)

  • 写一个.py文件,自定义一个类(层),也就是说把新定义的层(类)当作一个模块

  • 该类继承于torch.nn.Module

  • 需要重写init构造函数和forword方法

# 定义一个 my_layer.py
##########注意,这里是一个.py文件,也就是说把新定义的层(类)当作一个模块########
import torch

class MyLayer(torch.nn.Module):
    '''
    因为这个层实现的功能是:$y = w*\sqrt{x^2+b}$,所以有两个参数:
    权值矩阵 weights
    偏置矩阵 bias
    输入 x 的维度是(in_features,),输出 y 的维度是(out_features,) 
    
    所以,bias 的维度是(in_fearures,),注意这里为什么是in_features,而不是out_features,
    其实由于广播机制,计算y = w*{(x^2+b)}的时候,只要保持bias和x^2的某个维度保持一致就行
    
    weights 的维度是(in_features, out_features)注意这里为什么是(in_features, out_features),
    而不是(out_features, in_features)
    因为最后的forward方法中定义了,y=torch.matmul(input_,self.weight)
    ##############?????????????????此处定义好像有问题#####################
    所以,(in_features,) * (in_features, out_features) = (out_features,) 
    
    '''    
    # 定义构造函数时,需要传入一些与层相关的需要训练的参数
    def __init__(self, in_features, out_features, bias=True):
        super(MyLayer, self).__init__()  
        # 和自定义模型一样,首先就是调用父类的构造函数
        
        self.in_features = in_features
        self.out_features = out_features
        
        self.weight = torch.nn.Parameter(torch.Tensor(in_features, out_features)) 
        # 由于weights是可以训练的,所以使用Parameter来定义,而且是tensor类型
        
        if bias:
            self.bias = torch.nn.Parameter(torch.Tensor(in_features))             
            # 由于bias是可以训练的,所以使用Parameter来定义
        else:
            self.register_parameter('bias', None)

####前向传播forward方法:其实就是定义从输入x到输出y之间的函数运算关系###
    def forward(self, input):
        # 先计算x^2 + b
        input_=torch.pow(input,2)+self.bias
        # 再计算w*(x^2 + b)
#############为什么在做乘法运算时,要把权重矩阵写在后面#################
        y=torch.matmul(input_,self.weight)
        # 返回前向计算的结果
        return y

1.2.2 第二步:定义一个神经网络模型

  • 定义层后,就可以导入并使用层了,来自定义模型(在上一篇文章中,已经讲过了自定义神经网络模型)
import torch
# 只要把my_layer.py文件放在同级目录下,就能把my_layer.py当模块导入了,从模块中导入了一个类MyLayer
from my_layer import MyLayer 
# 从模块中导入自定义的MyLayer层
# 自定义类(层)所在.py文件,其实就是python的一个模块
# 所以可以直接从模块中导入自定义MyLayer层
# 导入自定义层


# 设置数据维度
N, D_in, D_out = 10, 5, 3  # 一共10组样本,输入特征为5,输出特征为3 
 
# 先定义一个模型
class MyNet(torch.nn.Module):
    def __init__(self):
        super(MyNet, self).__init__()  # 第一句话,调用父类的构造函数
######前面说过,有训练的参数的层放在init构造函数中,故把MyLayer层放入init#####
        self.mylayer1 = MyLayer(D_in,D_out)

    # 定义模型的forward方法,常规操作啦
    def forward(self, x):
        x = self.mylayer1(x)
 
        return x

model = MyNet()
print(model)
'''运行结果为:
MyNet(
  (mylayer1): MyLayer()   # 这就是自己定义的一个层
)
'''
MyNet(
  (mylayer1): MyLayer()
)
'运行结果为:\nMyNet(\n  (mylayer1): MyLayer()   # 这就是自己定义的一个层\n)\n'

1.2.3 第三步:训练模型

  • 自定义层和模型后,然后就是给数据,训练模型了
"""
(in_features,) * (in_features, out_features) = (out_features,)
#######??????此时还是没明白啊??????##############
- 回头思考一下,前面的输入、输出以及权重矩阵的维度的设计
- in_features = (N, D_in)
- out_features = (N, D_out)
显然权重矩阵为(in_features, out_features)
- 
"""

# 创建输入、输出数据
x = torch.randn(N, D_in)  #(10,5)
y = torch.randn(N, D_out) #(10,3)

# 定义损失函数
loss_fn = torch.nn.MSELoss(reduction='sum')
 
learning_rate = 1e-4
#构造一个optimizer对象
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
 
for t in range(20): # 
    
    # 数据的前向传播,计算预测值p_pred
    y_pred = model(x)
 
    # 计算计算预测值p_pred与真实值的误差
    loss = loss_fn(y_pred, y)
    # 格式化输出
    print(f"第 {t} 个epoch, 损失是 {loss.item()}")
 
    # 在反向传播之前,将模型的梯度归零
    optimizer.zero_grad()
 
    # 反向传播
    loss.backward()
 
    # 直接通过梯度一步到位,更新完整个网络的训练参数
    optimizer.step()
第 0 个epoch, 损失是 32.372467041015625
第 1 个epoch, 损失是 32.36086654663086
第 2 个epoch, 损失是 32.34928894042969
第 3 个epoch, 损失是 32.33773422241211
第 4 个epoch, 损失是 32.326194763183594
第 5 个epoch, 损失是 32.31468200683594
第 6 个epoch, 损失是 32.30318832397461
第 7 个epoch, 损失是 32.29171371459961
第 8 个epoch, 损失是 32.28026580810547
第 9 个epoch, 损失是 32.268829345703125
第 10 个epoch, 损失是 32.257423400878906
第 11 个epoch, 损失是 32.24604034423828
第 12 个epoch, 损失是 32.234676361083984
第 13 个epoch, 损失是 32.22333526611328
第 14 个epoch, 损失是 32.21201705932617
第 15 个epoch, 损失是 32.200714111328125
第 16 个epoch, 损失是 32.18944549560547
第 17 个epoch, 损失是 32.17818832397461
第 18 个epoch, 损失是 32.16695785522461
第 19 个epoch, 损失是 32.15574645996094

1.2.4 小结

使用pytorch搭建神经网路的一般步骤:

(1)第一步:搭建网络的结构,得到一个model。网络的结构可以是最简单的序贯模型,当然还可以是多输入-单输出模型、单输入-多输出模型、多输入-多输出模型、跨层连接的模型等,我们可以自己定义模型

(2)第二步:定义损失函数。 loss = torch.nn.MSELoss(reduction=‘sum’)

(3)第二步:定义优化方式。构造一个optimizer对象 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

上面是模型以及模型相关的配置三步走

  • 下面是训练的五步走

(1)第一步:计算y_pred;

(2)第二步:根据损失函数计算loss

(3)第三步:梯度归零, optimizer.zero_grad()

(4)第四步:反向传播误差 loss.backward()

(5)更新参数,使用step() optimizer.step()

1.3 补充:model.parameters()和model.state_dict()

1.3.1 model.parameters()

  • 注意,得到是一个生成器,可以使用for循环遍历它
# 可以看出,model.parameters()是一个生成器generator
print(model.parameters())

# 对于生成器,我们可以用循环或者next()来获取数据
for para in model.parameters():
    print(para)

Parameter containing:
tensor([[ 9.2755e-39,  9.1837e-39,  9.3674e-39],
        [-3.9455e-02,  4.2710e-02,  4.3286e-02],
        [ 1.0561e-38,  1.0194e-38,  1.1112e-38],
        [-4.6460e-02,  4.6853e-02, -4.8670e-02],
        [ 4.8080e-02,  4.1843e-02, -4.3377e-02]], requires_grad=True)
Parameter containing:
tensor([1.3148e+22, 7.1475e-02, 1.6691e+22, 7.1317e-02, 5.8291e-02],
       requires_grad=True)

1.3.2 model.state_dict()

  • state_dict()方法,看名字中dict就知道是个字典,我们直接print():
# 我们可以看到,key值是对网络参数的说明
# 这里是mylayer1层的weight和bias
print(model.state_dict())
OrderedDict([('mylayer1.weight', tensor([[ 9.2755e-39,  9.1837e-39,  9.3674e-39],
        [-3.9455e-02,  4.2710e-02,  4.3286e-02],
        [ 1.0561e-38,  1.0194e-38,  1.1112e-38],
        [-4.6460e-02,  4.6853e-02, -4.8670e-02],
        [ 4.8080e-02,  4.1843e-02, -4.3377e-02]])), ('mylayer1.bias', tensor([1.3148e+22, 7.1475e-02, 1.6691e+22, 7.1317e-02, 5.8291e-02]))])

1.4 总结

  • 本文说明了如何使用Module父类来拓展实现自定义模型、自定义层,发现二者的定义方式几乎相近,这也是pytorch如此受欢迎的原因之一了。后面继续讲解通过Function来自定义一个层。

  • Function与Module都可以对pytorch进行自定义拓展,使其满足网络的需求,但这两者还是有十分重要的不同。

  • 类的调用逻辑(以本文中自定义的模型MyNet类的使用来说明)

    • 1、首先,创建该类的实例化对象 model = MyNet()
      • 此时,实例化对象model会自动调用MyNet的init方法,也就是给对象增加了一个属性mylayer1,而这个属性的值为一个类对象MyLayer(D_in,D_out),也即是说mylayer1也是一个实例化的对象, self.mylayer1 = MyLayer(D_in,D_out),self指的就是对象model本身
    • 2、然后,使用model.parameters(),这个用来给优化器的参数进行初始化
      • 实际上,model.parameters()返回的是一个生成器,可for循环,这就是在训练阶段使用for循环的原因
    • 3、接着,使用for循环进行训练,要计算预测值,y_pred = model(x)
      • a、 model是一个实例化的对象,给它传参数是因为父类Module中的call这个魔法方法
      • b、 call方法使得对象具备跟函数一样的功能,可以给对象传参数,像调用函数一样来调用对象
      • c、 最重要的是,call方法里面还调用了forward方法
      • d、 MyNet类继承了父类Module,并且调用了父类的初始化函数,super(MyNet, self).init(),
        那么魔法方法call会自动被调用,也就是forward方法被自动调用了
      • e、 因此,传给model的参数实际上传给了MyNet类的forward方法
    • 4、再看MyNet类的forward方法,x = self.mylayer1(x)
      • a、 显然,它调用了mylayer1,因此参数x传给了mylayer1
      • b、再看mylayer1,self.mylayer1 = MyLayer(D_in,D_out)
        • mylayer1是MyLayer(D_in,D_out)的实例化对象,因此参数x传入了MyLayer类的init和forward方法
        • 此时,参数x已经传入到了我们自定义的层MyLayer
        • 最后,就是前向传播计算了,将前向传播的计算结果返回y_pred,作为一次预测值
    • 5、finally,计算y_pred与真实值之间的误差,让梯度归零,求误差关于训练参数的梯度,更新可训练参数,如此循环下去,直到误差不再明显下降

1.5 参考资料

参考博客

你可能感兴趣的:(pytorch神经网络模型)