动手学习pyTorch之【计算过程】——从基础模块类开始

Computation

  • 神经网络块:描述单个层、由多个层组成的组件或整个模型本身(模块化)。块由类表示,它的任何子类都必须定义一个将其输入转换为输出的前向传播函数并且必须存储任何必需的参数。 注意,有些块不需要任何参数。为了计算梯度,块还必须具有反向传播函数。 在自定义时,由于自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。
  • 模块实例:nn.Sequential是一种表示一个块的类,它定义了一种特殊的Module,即维护了一个由Module组成的有 序列表。上面的实例化例子中,两个全连接层都是Linear类的实例, Linear类本身就是Module的子类,层的执行顺序是作为参数传递的。 这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = torch.rand(2, 20)
net(X)
'''
tensor([[-0.1216,  0.0153,  0.0546, -0.0989, -0.0582,  0.1448, -0.3097, -0.0478,
         -0.1381,  0.0593],
        [-0.1315,  0.0540,  0.0157, -0.0701, -0.2307,  0.0710, -0.2731, -0.0527,
         -0.2170,  0.1010]], grad_fn=)
'''
  • 自定义块

    • 将输入数据作为其前向传播函数的参数
    • 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如上面模型中的第一个全连接层接收一个20维的输入,但是返回一个维度为256的输出。
    • 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
    • 存储和访问前向传播计算所需的参数
    • 根据需要初始化模型参数

    自定义MLP():net(X)会调用MLP继承自Module类的__call__函数,这个函数将调用MLP类定义的forward函数来完成前向计算,这一点是为什么直接使用net(x)就能完成向前计算的原因。

    # 自定义一个具有256个隐藏单元的隐藏层和一个10维的输出层
    # 继承了Module类!在定义子类的时候,必须在括号内指定父类的名称
    class MLP(nn.Module):
        def __init__(self):  # 调用MLP的父类Module的构造函数来执行必要的初始化
            super().__init__()  # super() 用来调用父类,帮助父类和子类关联起来
            # 继承之后,便可以给子类定义属性和方法
            self.hidden = nn.Linear(20, 256)  # 隐藏层
            self.out = nn.Linear(256, 10)
    
        # 定义模型的前向传播
        def forward(self, X):
            return self.out(F.relu(self.hidden))
        
        
    net = MLP()
    net(X)
    

    **自定义Sequential():**它可以接收一个子模块的有序字典或者一系列子模块作为参数来逐一添加Module的实例,而模型的前向计算就是将这些实例按添加的顺序逐一计算。我们需要定义两个关键函数:

    • 一种将块逐个追加到列表中的函数。
    • 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。

    __init__函数将每个模块逐个添加到有序字典_modules中,_modules优点是在模块的参数初始化过程中, 系统知道在_modules字典中查找需要初始化参数的子块。当MySequential的前向传播函数被调用时, 每个添加的块都按照它们被添加的顺序执行。

    # 实例化Sequential
    class MySequential(nn.Module):
        def __init__(self, *args):
            super().__init__()
            # 如果传入的是一个OrderedDict
            # 函数中以列表或者元组的形式传参时所有传入的位置参数都会被args接收生成一个元组,故args[0]
            if len(args) == 1 and isinstance(args[0], OrderedDict):
                for key, module in args[0].items():  # item取元素
                    self.add_module(key, module)  # add_module方法会将module添加进self._modules(一个OrderedDict)
            else:  # 传入的是一些Module
                for idx, module in enumerate(args):  # 枚举取变量
                    self.add_module(str(idx), module)  # 主要要str(key)
    
        def forward(self, X):
            for block in self._modules.values():
                X=block(X)
            return X
    
    
    net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
    

    进阶

    FixedHiddenMLP中,我们实现了一个隐藏层, 其权重self.rand_weight在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 然后,神经网络将这个固定层的输出通过一个全连接层。

    注意,在返回输出之前,模型运行了一个while循环,在1范数大于1的条件下反复将输出向量除以2,最后返回X中所有项的和(此操作不常用, 只是在展示如何将任意代码集成到神经网络计算的流程中)

    class FixedHiddenMLP(nn.Module):
        def __init__(self):
            super().__init__()
            # 不计算梯度的随机权重参数。因此其在训练期间保持不变
            self.rand_weight = torch.rand((20, 20), requires_grad=False)
            self.linear = nn.Linear(20, 20)
    
        def forward(self, X):
            X = self.linear(X)
            # 使用创建的常量参数以及relu和mm函数
            X = F.relu(torch.mm(X, self.rand_weight) + 1)
            # 复用全连接层。这相当于两个全连接层共享参数
            X = self.linear(X)
            # 控制流
            while X.abs().sum() > 1:
                X /= 2
            return X.sum()
    
    
    net = FixedHiddenMLP()
    net(X)
    
    
    # 常规的参数初始化操作
    def init_weights(m):
        if type(m) == nn.Linear:  # 套路
            nn.init.normal_(m.weight, std=0.01)  # 要加下划线
    
    net.apply(init_weights)  # 递归初始化
    

    混搭:

    class NestMLP(nn.Module):
        def __init__(self):
            super().__init__()
            self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                     nn.Linear(64, 32), nn.ReLU())
            self.linear = nn.Linear(32, 16)
    
        def forward(self, X):
            return self.linear(self.net(X))
    
    chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
    chimera(X)
    
  • **延后初始化:**在初始化时可以先不显式定义模型的参数个数和维度,而是在数据第一次通过模型时框架动态地推断出每个层的大小(卷积神经网络常用)

  • 参数管理

    • 自定义层是可以不含有任何参数的

      class CenteredLayer(nn.Module):
          def __init__(self):
              super().__init__()
      
          def forward(self, X):
              return X - X.mean()
      
      net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
      
    • 参数访问:当通过Sequential类定义模型时,我们可以通过索引来访问模型的任意层。参数是复合的对象,包含值、梯度和额外信息。

      net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
      X = torch.rand(size=(2, 4))
      net(X)
      
      
      print(net[2].state_dict())  # 检查第二个全连接层的参数
      '''
      OrderedDict([('weight', tensor([[ 0.0743,  0.1876,  0.0571,  0.3447,  0.3483, -0.2867,  0.3273, -0.1527]])), ('bias', tensor([0.1162]))])
      '''
      
      print(type(net[2].bias))  # 
      print(net[2].bias)  # tensor([0.1162], requires_grad=True)
      print(net[2].bias.data)  # tensor([0.1162])
      print(net[2].weight.grad == None) # True , 还没有调用反向传播
      
      
      # 一次性访问所有参数(逐个访问需要递归,因为是嵌套结构)
      print(*[(name, param.shape) for name, param in net[0].named_parameters()])
      print(*[(name, param.shape) for name, param in net.named_parameters()])
      '''
      ('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
      ('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
      '''
      print(net.state_dict()['2.bias'].data)  # tensor([0.1162])
      
    • 从嵌套块中收集参数

      def block1():
          return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                               nn.Linear(8, 4), nn.ReLU())
      
      def block2():
          net = nn.Sequential()
          for i in range(4):
              # 在这里嵌套
              net.add_module(f'block {i}', block1())
          return net
      
      rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
      rgnet(X)
      
      print(rgnet)
      '''
      Sequential(
        (0): Sequential(
          (block 0): Sequential(
            (0): Linear(in_features=4, out_features=8, bias=True)
            (1): ReLU()
            (2): Linear(in_features=8, out_features=4, bias=True)
            (3): ReLU()
          )
          (block 1): Sequential(
            (0): Linear(in_features=4, out_features=8, bias=True)
            (1): ReLU()
            (2): Linear(in_features=8, out_features=4, bias=True)
            (3): ReLU()
          )
          (block 2): Sequential(
            (0): Linear(in_features=4, out_features=8, bias=True)
            (1): ReLU()
            (2): Linear(in_features=8, out_features=4, bias=True)
            (3): ReLU()
          )
          (block 3): Sequential(
            (0): Linear(in_features=4, out_features=8, bias=True)
            (1): ReLU()
            (2): Linear(in_features=8, out_features=4, bias=True)
            (3): ReLU()
          )
        )
        (1): Linear(in_features=4, out_features=1, bias=True)
      '''
      # 可以通过嵌套列表索引访问它们
      rgnet[0][1][0].bias.data # 访问第一个主要的块中、第二个子块的第一层的偏置项
      
    • 参数初始化:深度学习框架提供默认随机初始化,会根据一个范围(根据输入和输出维度计算出的)均匀地初始化权重和偏置矩阵,也允许我们创建自定义初始化方法。

      # 内部的初始化方法
      class MyLinear(nn.Module):
          def __init__(self, in_units, units):
              super().__init__()
              self.weight = nn.Parameter(torch.randn(in_units, units))
              self.bias = nn.Parameter(torch.randn(units,))
          def forward(self, X):
              linear = torch.matmul(X, self.weight.data) + self.bias.data
              return F.relu(linear)
      linear = MyLinear(5, 3)
      
      # 内置初始化
      def init_normal(m):
          if type(m) == nn.Linear:
              nn.init.normal_(m.weight, mean=0, std=0.01)  # 初始化权重
              nn.init.zeros_(m.bias)  # 初始化偏置
              # nn.init.constant_(m.weight, 1) # 全部初始化成1
      net.apply(init_normal)  # 递归地进行初始化
      
      # 对某些块用不同的初始化方法
      def xavier(m):
          if type(m) == nn.Linear:
              nn.init.xavier_uniform_(m.weight)
      def init_42(m):
          if type(m) == nn.Linear:
              nn.init.constant_(m.weight, 42)
      
      net[0].apply(xavier)
      net[2].apply(init_42)
      print(net[0].weight.data[0])  # tensor([-0.3261, -0.5587,  0.0063, -0.3914])
      print(net[2].weight.data)  # tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
      
      # 始终可以直接设置参数
      net[0].weight.data[:] += 1
      net[0].weight.data[0, 0] = 42
      

      自定义初始化:(使用以下概率分布函数)
      w ∼ { U ( 5 , 10 )  可能性  1 4 0  可能性  1 2 U ( − 10 , − 5 )  可能性  1 4 \begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned} wU(5,10)0U(10,5) 可能性 41 可能性 21 可能性 41

      def my_init(m):
          if type(m) == nn.Linear:
              print("Init", *[(name, param.shape)
                              for name, param in m.named_parameters()][0])
              nn.init.uniform_(m.weight, -10, 10)
              m.weight.data *= m.weight.data.abs() >= 5
      
      net.apply(my_init)
      net[0].weight[:2]
      '''
      Init weight torch.Size([8, 4]) # [0]:weight,[1]:bias
      Init weight torch.Size([1, 8])
      tensor([[-0.0000,  8.0813, -0.0000, -6.7370],
              [-9.4638, -7.5112, -5.1031,  0.0000]], grad_fn=)
      '''
      
    • 参数绑定:(目标:在多个层间共享参数)定义一个稠密层,然后使用它的参数来设置另一个层的参数。

      # 我们需要给共享层一个名称,以便可以引用它的参数
      shared = nn.Linear(8, 8)
      net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                          shared, nn.ReLU(),
                          shared, nn.ReLU(),
                          nn.Linear(8, 1))
      net(X)
      # 检查参数是否相同
      print(net[2].weight.data[0] == net[4].weight.data[0])
      net[2].weight.data[0, 0] = 100
      # 确保它们实际上是同一个对象,而不只是有相同的值
      print(net[2].weight.data[0] == net[4].weight.data[0])
      
      # tensor([True, True, True, True, True, True, True, True])
      # tensor([True, True, True, True, True, True, True, True])
      

      当参数绑定时,由于模型参数包含梯度,因此在反向传播期间第二个隐藏层(即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起

  • 读写文件

    # 一些基础操作
    x = torch.arange(4)
    torch.save(x, 'x-file')
    x2 = torch.load('x-file')
    
    y = torch.zeros(4)
    torch.save([x, y],'x-files')
    x2, y2 = torch.load('x-files')
    
    mydict = {'x': x, 'y': y}
    torch.save(mydict, 'mydict')
    mydict2 = torch.load('mydict')
    
    # 实例化一个net,将模型的参数存储在一个叫做“mlp.params”的文件中
    net = MLP()
    X = torch.randn(size=(2, 20))
    Y = net(X)
    torch.save(net.state_dict(), 'mlp.params')
    
    #恢复模型,我们需要用代码生成架构, 然后从磁盘加载参数。
    clone = MLP()
    clone.load_state_dict(torch.load('mlp.params'))
    
    # 验证
    Y_clone = clone(X)
    print(Y_clone == Y)
    '''
    tensor([[True, True, True, True, True, True, True, True, True, True],
            [True, True, True, True, True, True, True, True, True, True]])
    '''
    
  • GPU

    • 查看显卡信息

      !nvidia-smi
      

      在PyTorch中,每个数组都有一个设备(也叫上下文context),默认所有变量和相关的计算都分配给CPU。当我们跨多个服务器部署作业时通常希望模型的参数在GPU上。

    • 指定存储和计算的设备:cpu设备:所有物理CPU和内存,gpu设备:只代表一个卡和相应的显存)

      torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')
      print(torch.cuda.device_count()) # 查询可用gpu的数量
      
      # 如果存在,则返回gpu(i),否则返回cpu()
      def try_gpu(i=0):  
          if torch.cuda.device_count() >= i + 1:
              return torch.device(f'cuda:{i}')
          return torch.device('cpu')
      
      # 返回所有可用的GPU,如果没有GPU,则返回[cpu(),]
      def try_all_gpus():  
          devices = [torch.device(f'cuda:{i}')
                   for i in range(torch.cuda.device_count())]
          return devices if devices else [torch.device('cpu')]
      
      try_gpu(), try_gpu(10), try_all_gpus()
      
    • 张量与GPU

      # 查询张量所在的设备
      x = torch.tensor([1, 2, 3])
      print(x.device) # device(type='cpu')
      
      # 创建时指定存储设备
      X = torch.ones(2, 3, device=try_gpu())
      '''
      tensor([[1., 1., 1.],
              [1., 1., 1.]], device='cuda:0')
      '''
      Y = torch.rand(2, 3, device=try_gpu(1)) # 在第二个GPU上创建一个随机张量
      

      注意,如果要对多个项进行操作,它们都必须在同一个设备上(如求和操作)我们可以通过显式的复制操作,将一个设备的张量传输到另一个设备上执行操作。

      单个GPU相对运行速度快,**但是设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。**因此拷贝数据要很小心。一次执行几个操作比代码中散布的许多单个操作要好得多(否则可能会阻塞)。

      打印张量或将张量转换为NumPy格式时,如果数据不在内存中,框架会首先将其复制到内存中,这会导致额外的传输开销。更糟糕的是,它还受制于全局解释器锁,使得一切都得等待Python完成。

    • 神经网络与GPU

      net = nn.Sequential(nn.Linear(3, 1))
      net = net.to(device=try_gpu())
      
      # 当输入为GPU上的张量时,模型将在同一GPU上计算结果
      net(X)  # ……device='cuda:0'
      
      #确认模型参数存储在同一个GPU上
      print(net[0].weight.data.device)  # device(type='cuda', index=0)
      
      

你可能感兴趣的:(机器学习,深度学习,python,神经网络,pytorch,人工智能)