PyTorch快速入门

参考:https://github.com/chenyuntc/pytorch-book

基本介绍

Tensor

  • t.Tensor
    • 传入维度 - 分配空间不初始化
    • 传入具体数据 - 舒适化
  • t.rand
    • 传入维度 - 指定维度的随机初始化数据,复合[0,1]分布
  • Add
    • x+y
    • t.add(x,y)
    • t.add(x,y,out=result) - 指定输出目标
    • x.add(y)- x不变
    • x.add_(y) - x改变 = x+=y

函数名后面带下划线_会修改Tensor本身

  • 与Numpy转换:
    • Tensor -> numpy: a.numpy()
    • Numpy -> tensor: t.from_numpy(a)

一个对象的tensor和numpy共享内存,一个改变,另一个随之而变

  • 获得元素值:

    • 下标访问tensor - tensor[index...]获得0-dim tensor
    • 获得值 - scalar.item()
  • scalar和tensor的区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    tensor = t.tensor([2])
    scalar = tensor[0]
    scalar0 = t.tensor(2)
    # tensor 1-dim
    tensor.size() # torch.Size([1])
    # scalar 0-dim
    scalar.size() # torch.Size([])
    # scalar0 0-dim
    scalar0.size() # torch.Size([])
  • 数据拷贝和共享内存

    • t.tensor() - 会进行数据拷贝
    • t.from_numpy() / tensor.detach()新建的tensor与原tensor共享内存
  • GPU加速

    • t.cuda

Autograd: 自动微分

  • 使用autograd功能

    • ```python
      x = t.ones(2, 2, requires_grad=True)
      result = doSomething(x)

      1
      2
      3
      4
      5

      - 反向传播

      - ```python
      result.backward()

      反向传播会累加之前的梯度,反向传播前把梯度清零

    • 梯度清零

      • ```python
        x.grad.data.zero_()
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59

        ### 神经网络

        定义网络

        - 继承`nn.Module`,实现`forward`,把网络中具有**可学习参数**的层放在`__init__`中。不具有可学习参数使用`forward`的`nn.functional`代替

        - `__init__`函数先执行父类构造函数`super(CLASSNAME, self).__init__()`

        ##### 卷积层

        - `nn.Conv2d(inputC, outputC, k)`
        - 输入图片通道数
        - 输出通道数
        - 卷积核

        ##### 全连接层

        - `nn.Linear(input, output)`

        - 输入样本数
        - 输出样本数

        > y = Wx + b

        ##### forward

        - `F.relu(input)` - 激活
        - 上一层的结果
        - `F.max_pool2d(input, kernel size)`
        - kernel size

        - `x.view(ONEDIMENSION,-1`
        - -1 自适应维度

        #### 训练网络

        - 定义了forward函数,backward就会自动被实现
        - `net.parameters()`返回网络的**可学习参数**
        - `·net.named_parameters()`返回可学习的参数及名称

        > torch.nn不支持一次只输入一个样本:
        >
        > 1. 用 `input.unsqueeze(0)`将batch_size设为1
        > 2. Conv2d输入时将nSample设为1

        #### 损失函数

        - `nn.MSELoss(output, target)`计算均方误差
        - `nn.CrossEntropyLoss(output target)`计算交叉熵损失

        #### 优化器

        - 反向传播计算参数的梯度,使用优化方法**更新网络的权重和参数**

        - e.g. SGD:

        - ```python
        weight = weight - learning_rate * gradient
    • ```python
      learning_rate = 0.01
      for f in net.parameters():

      f.data.sub_(f.grad.data * learning_rate)# inplace 减法
      
      1
      2
      3
      4
      5
      6
      7

      ##### 构造优化器

      > optim <= torch.optim

      - ```
      optimizer = optim.SGD(net.parameters(), lr = 0.01)
    • 要调整的参数为网络中的可学习参数

    • lr为学习率

  • 训练步骤

    1. 清零优化器梯度

      1
      optimizer.zero_grad()
    2. 过网络

      1
      output = net(input)
    3. 计算损失

      1
      loss = criterion(output, target)
    4. 反向传播

      1
      loss.backward()
    5. 更新参数

      1
      optimizer.step()

Tensor 和 Autograd

Tensor

基础操作

创建
  • 函数 功能
    arange(s,e,step) 从s到e,步长为step
    linspace(s,e,steps) 从s到e,均匀切分成steps份
    rand/randn(*sizes) 均匀/标准分布
    normal(mean,std)/uniform(from,to) 正态分布/均匀分布
    randperm(m) 随机排列
  • 创建的时候可以指定数据类型(e.g. dtype = t.int)和存放device(e.g. device = t.device('cpu')

  • tensor转化为list - tensor.tolist()

    tensor的数据类型为tensor([[1,2],[3,4]]); list数据类型为[[1,2],[3,4]]

  • 获得tensor的维度 - tensor.size()tensor.shape返回torch.Size对象 - torch.Size([第一维, 第二维, ..])

  • 获得tensor元素的总数 - tensor.numel()

  • 传参为数据和维度的区别:

    1
    2
    t.Tensor(2,3) # tensor([[x, x, x],[x, x, x]])
    t.Tensor((2,3)) # tensor([2,3])

使用维度作为参数构造tensor的时候不会马上分配空间,使用到tensor才分配;其他的构造函数都是马上分配

  • 使用eye构造对角矩阵,不要求行列数一致
常用操作
  • 调整tensor形状,调整前后元素总数一致。view返回的新tensor与原tensor共享内存。

  • view的参数为修改后的每个维度,如果有一个为-1则该维度根据元素总数不变的原则自动计算

  • unsqueeze(index) - 第index维的维度+1

    • 即之前维度为[x, y, z, …]
      • index = 0 -> 维度变为 [1, x, y, z, …]
      • index = 1 -> 维度变为 [x, 1, y, z, …]
      • index = 2 -> 维度变为 [x, y, 1, z, …]
    • 参数为负数的时候表示倒数
  • squeeze(index) - 压缩第index维的1,如果不输入index,则把所有维度为1的压缩

  • resize可以修改tensor的大小,如果新大小超过了原大小,自动分配新空间,大小小于原大小,之前数据仍然保留

索引操作
  • 索引出来的结果与原tensor共享内存

  • None - 新增轴

    • ```pythob
      a[:,None,:,None,None].shape
      torch.Size([3, 1, 4, 1, 1])

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138

      - 筛选tensor元素 - `tensor[CONDITION]`

      > 返回结果与原tensor不共享内存空间

      - 常用选择函数

      | 函数 | 功能 |
      | ------------------------------: | ----------------------------------------------------: |
      | index_select(input, dim, index) | 在指定维度dim上选取,比如选取某些行、某些列 |
      | masked_select(input, mask) | 例子如上,a[a>0],使用ByteTensor进行选取 |
      | non_zero(input) | 非0元素的下标 |
      | gather(input, dim, index) | 根据index,在dim维度上选取数据,输出的size与index一样 |

      > 对tensor的任何索引操作仍是一个**tensor**,想要获取标准的python对象数值,需要调用`tensor.item()`, 这个方法**只对包含一个元素的tensor适用**

      - 索引访问:
      - 维度为3:[[a,b], [c,d], [e,f]] => [[a,c,e], [b,d,f]]
      - [[a,b,c], [d], [e]] => [[a,d,e], [b,d,e], [c,d,e]]

      ##### 逐元素操作

      - `clamp` - 控制元素范围,用于比较大小

      ![1561187765542](C:\Users\Sherry\AppData\Roaming\Typora\typora-user-images\1561187765542.png)

      ##### 归并操作

      - 使得输出形状小于输入形状,e.g. 均值、和...
      - 指定对第几维度的元素操作,`keepdim = True`保存被操作的维度为1,否则不保留这一维度

      ##### 线性代数

      | 函数 | 功能 |
      | -------------------------------- | --------------------------------- |
      | trace | 对角线元素之和(矩阵的迹) |
      | diag | 对角线元素 |
      | triu/tril | 矩阵的上三角/下三角,可指定偏移量 |
      | mm/bmm | 矩阵乘法,batch的矩阵乘法 |
      | addmm/addbmm/addmv/addr/badbmm.. | 矩阵运算 |
      | t | 转置 |
      | dot/cross | 内积/外积 |
      | inverse | 求逆矩阵 |
      | svd | 奇异值分解 |

      #### Tensor和Numpy

      > 当numpy的数据类型和Tensor的类型不一样时,数据会被复制,不共享内存

      - 调用`t.tensor`进行构建的时候都会进行数据拷贝

      ##### 广播法则

      ###### N

      - 所有输入数组像其中shape最长的看齐,shape不足通过前面加1
      - 两个数组要么在某一个维度长度一致,要么其中一个为1,否则不能计算
      - 输入某个长度为1,计算时沿此维度扩充成和另一个输入一样的维度

      ###### Pytorch中的使用

      - `unsqueeze`或者`view`或者`tensor[NONE]`,实现补齐1

      - `expand`或者`expand_as`重复数组,不会占用空间

      > repeat和expand相似,repeat占用额外空间,expand不占用



      #### 内部结构

      - Tensor分为头信息区tensor和存储区storage
      - tensor保存size, stride, type
      - 共享内存的情况下,即共享storage
      - 下标访问只是对于原storage地址增加了偏移量进行映射的

      > 大部分操作不修改tensor,只修改tensor的头
      >
      > contiguous方法将离散的tensor变成连续数据
      >
      > 高级索引一般不共享storage,普通索引共享storage

      #### 其他问题

      ##### GP

      - 推荐使用`tensor.to(device)`使得程序同时兼容CPU和GPU,避免频繁在内存和显存中传输数据

      ##### 持久化

      - `t.save`,

      ##### 向量化

      - python的for循环十分低效
      - `t.set_num_threads`可以设置PyTorch进行CPU多线程并行计算时候所占用的线程数,这个可以用来限制PyTorch所占用的CPU数目

      ### AutoGrad

      - 自动求导引擎

      #### 计算图

      - 有向无环图

      - 有向无环图的叶子节点由用户自己创建,不依赖其他变量,根节点为计算图的最终目标

      - 链式的中间导数在前向传播过程中保存为buffer,在计算完梯度后会自动清空

      > 需要多次反向传播则指定`retain_graph`保存buffer

      - 变量的`requires_grad`属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点`requires_grad`都是True。

      - 修改tensor数值,不希望被autograd记录,则修改tensor.data或者tensor.detach()
      - 反向传播非叶子结点的导数会在计算完后被清空,查看梯度可以使用autograd.grad函数或者hood

      - hook需要register和remove

      ##### 总结

      - `autograd`根究用户对variable的操作构建计算图。对变量的操作抽象为Function
      - 某个变量不是Function的输出,且由用户创建则为叶子节点(用户输入),其`grad_fn`为None. 叶子节点中需要求导的Variable,具有`AccumulateGrad`标识,因为梯度累加
      - Variable默认不求导,如果一个节点的`require_grad`设置为True,所有依赖它的节点`require_grad`为True
      - Variable的`volatile`默认为True,volatile为True的节点不会求导,volatile优先级比`require_grad`高
      - 多次反向传播梯度累加,若需要保存反向传播中间变量的值,需要设置`retain_graph = True`

      - 非叶子节点梯度计算完之后被清空,使用`autogtad.grad`或`hook`获取

      #### 扩展autograd

      - 自己定义函数继承`Function`,并且实现`forward`, `backward`静态方法(def之前写`@staticmethod`)

      - 主函数中调用时:

      - 前向传播:

      - ```
      z = MultiplyAdd.apply(w, x, b)
    • 反向传播:

      • ```
        z.backward()
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19

        ## 神经网络工具箱nn

        - Module - 神经网络中的某层/包含很多层的神经网络

        ##### 全连接层

        - 继承`nn.Module`

        - 构造函数`__init__`自己定义可学习参数

        - ```python
        # infeatures和outfeatures为输入输出特征的维度
        def __init__(self, infeatures, outfeatures):
        super...
        # 自己定义可学习参数如下:
        self.para1 = nn.Parameter..
        self.para2 = ...
        ...

    nn.Parameter - tensor,默认requires_grad = True

  • 不需要写反向传播函数

  • 调用:

    1
    2
    layer = Linear(INFEATURES, OUTFEATURES)
    y = layer(x)
  • 返回可学习参数 - named_parameters()

子模块sub module
  • named_parameters()返回可学习参数为:模块名.参数名

常用神经网络层

图像相关层

卷积层
  • nn.Conv2d
池化层
  • 没有可学习参数,权重固定
其他
  • Linear全连接

  • BatchNorm - 批量规范化,风格迁移instanceNorm

    • 初始化通道数
    • 权重初始化为单位阵
    • 偏差初始化为0
  • Dropout - 防止过拟合

    • 输出每个元素的舍弃概率

激活函数

Relu
  • $ReLU(x) = max(0, x)$
    • 其inplace参数为True会将输出覆盖到输入,节省内存,一般不使用Inplace

前馈传播网络 feedfoward neural network: 将每一层的输出直接作为下一层的输入。简化forward -> ModuleList & Sequential

Sequential
  • 包含几个sub module - 前向传播一层一层传递
  1. 声明,添加module
  • ```python
    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())
    1
    2
    3
    4
    5
    6
    7
    8
    9

    2. 将模块作为参数传递

    - ```python
    net2 = nn.Sequential(
    nn.Conv2d(3, 3, 3),
    nn.BatchNorm2d(3),
    nn.ReLU()
    )
  1. 将模块作为有序字典传入
  • ```python
    from collections import OrderedDict
    net3= nn.Sequential(OrderedDict([
          ('conv1', nn.Conv2d(3, 3, 3)),
          ('bn1', nn.BatchNorm2d(3)),
          ('relu1', nn.ReLU())
        ]))
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    - 其中方法1和方法3可以按照模块名称访问子模块,而方法2按照下标获取子模块(从0)



    ---

    - 模块数组`nn.ModuleList`,其中的元素可以被主(父)module识别,即其参数可以自动加入到主module的参数中

    - ```python
    modellist = nn.ModuleList([nn.Linear(3,4), nn.ReLU(), nn.Linear(4,2)])

损失函数

  • ```
    nn.LOSS_FUNCTION_NAME()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    ### 优化器

    - 优化方法来自包`torch.optim`,所有的优化方法继承基类`optim.Optimizer`

    - 优化器可以对于不同的子网络(如在主网络/主Module中定义了多个子模块或者Sequential)

    - ```python
    optimizer =optim.SGD([
    {'params': net.features.parameters()}, # 学习率为1e-5
    {'params': net.classifier.parameters(), 'lr': 1e-2}
    ], lr=1e-5)

    如果某个参数显示执行学习率,则使用最外层的默认学习率

  • 指定自网络中的不同层有不同的学习率的例子:

    • ```python

      只为两个全连接层设置较大的学习率,其余层的学习率较小

      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 )
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      ##### 调整学习率

      ###### 新建优化器

      - 优点:简单
      - 缺点:如果使用动量的优化器,会丢失动量等状态信息

      ###### 修改学习率

      - 手动decay保存动量

      - ```python
      for param_group in optimizer.param_groups:
      param_group['lr'] *= 0.1 # 学习率为之前的0.1倍

nn.functional

  • 大部分layer在functional有与之对应的函数
nn.functional和nn.Module的区别
  • module: 自动提取可学习参数
  • function: 类似纯函数,不需要可学习参数时可使用. e.g. 激活函数,池化层

  • 不具备可学习参数的层不放在构造函数__init__中。

初始化策略

  • 随机初始化可能导致后期训练中梯度爆炸或梯度消失
  • nn.init模块

nn.Module深入分析

属性

变量名 类型 说明 备注
_parameters 字典 用户设置 子模块的参数不会存放于此
_modules 字典 子模块
_buffers 字段 缓存
_backward_hooks_forward_hooks 钩子,提取中间变量
training 区分训练阶段于测试阶段,据此确定前向传播策略
_parameters, _modules, _buffers
  • 使用named_paramaters()的时候将parameters和modules的parameter全部返回

  • module层层嵌套,常用方法:

    • named_children- 查看直接子module
    • named_modules - 查看所有子module

training
  • 一些layer在训练和测试阶段差距较大,为了使得每个training都要被设置好 => 定义了模型的的train和eval模式
    • model.train() - 将当前Module及其子module所有training属性设置为True
    • model.eval() - 将training属性都设为False
_backward_hooks_forward_hooks
  • 在module前向传播或反向传播时注册,传播执行结束执行钩子,使用完及时删除
  • 钩子用于获取某些中间结果
使用方法
1
2
3
4
5
6
7
8
9
10
model = VGG()
features = t.Tensor()
def hook(module, input, output):
'''把这层的输出拷贝到features中'''
features.copy_(output.data)

handle = model.layer8.register_forward_hook(hook)
_ = model(input)
# 用完hook后删除
handle.remove()

Python的原始实现:

  • result = obj.name会调用buildin函数getattr(obj, 'name'),如果该属性找不到,会调用obj.__getattr__('name')
  • obj.name = value会调用buildin函数setattr(obj, 'name', value),如果obj对象实现了__setattr__方法,setattr会直接调用obj.__setattr__('name', value')

nn.Module中的实现:

  • nn.Module中实现了__setattr__函数,执行module.name = value时,在__setattr__中判断value是否为Parameternn.Module对象。如果是则加入_parameters_modules字典。如果是其他的对象,则保存在__dict__

_modules_parameters的item未保存在__dict__中,默认getattr无法获取,nn.Module实现__getattr__:如果getattr无法处理,则调用自定义的__getattr___modules, _parameters_buffers三个字典中获取

保存模型
  • 使用Module的state_dict()函数,返回当前Module所有状态数据。下次使用,module.load_state_dict()

  • 优化器的实现:

    • # 保存模型
      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计算
  • 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)
    • 将一个输入batch分成多分,送到对应GPU计算,各个GPU得到的梯度累加

通过device_ids参数指定在哪些GPU上优化,output_devices指定输出到哪个GPU