pytorch - 基础知识

由于张量是对数据的描述,在神经网络中通常将数据以张量的形式表示,如零维张量表示标量,一维张量表示向量,二维表示矩阵,三维张量表示图像,四维表示视频,这章首先介绍张量,然后介绍它的运算,再是核心包autograd自动微分。

张量

张量的简介

pytorch的torch.Tensor和Numpy的多维数组非常相似,由于tensor提供GPU的自动求梯度和计算,它更适合深度学习。

  1. 用dtype指定类型创建tensor,detype有tensor.float/long等

    1
    torch.tensor(1,dtype=torch.int8)
  2. 使用指定类型随机初始化

    1
    torch.IntTensor(2,3)

    tensor和numpy array 互相转化,torch.tensor创建的张量是不共享内存的,但torch.from_numpy()和torch.as_tensor()从numpy array创建得到的张量和原数据是共享内存的,修改numpy array会导致对应tensor的改变

    1
    2
    3
    4
    5
    6
    7
    8
    import numpy as np
    array = np.array([[1,2,3],[4,5,6]])
    tensor = torch.tensor(array)
    array2tensor = torch.from_numpy(array)
    tensor2array = tensor.numpy()
    # 修改array,对应的tensor也会改变
    array[0,0] = 100
    print(array2tensor)
  3. 从已存在的tensor创建

    1
    2
    3
    4
    5
    6
    7
    x = torch.tensor([5.5, 3])
    x = x.new_ones(4, 3, dtype=torch.double)
    # 创建一个新的全1矩阵tensor,返回的tensor默认具有相同的torch.dtype和torch.device
    x = torch.randn_like(x, dtype=torch.float)
    # 重置数据类型
    print(x)
    # 结果会有一样的size
  4. 创建tensor的函数
    基础构造 torch.tensor([1,2,3,4])
    随机初始化 torch.rand(2,3) [0,1)的均匀分布, torch.randn(2,3) N(0,1)的正态分布,randperm(10) 随机排列
    正态分布 torch.normal(2,3) 均值为2标准差为3的正态分布
    全1矩阵 torch.ones(2,3)
    全0矩阵 torch.zeros(2,3)
    对角单位阵 torch.eye(2,3)
    有序序列 torch.arange(2,10,2) 从2到10,步长2
    均分序列 torch.linspace(2,10,2) 从2到10,均分成2份

  5. 查看tensor的维度

    1
    2
    3
    4
    import torch
    k = torch.tensor(2,3)
    print(k.shape)
    print(k.size())

张量的操作

  1. 加法

    1
    2
    3
    4
    5
    6
    7
    8
    k = torch.rand(2, 3) 
    l = torch.ones(2, 3)

    print(k+l)

    torch.add(k,l)

    k.add(l) # 原值修改
  2. 索引操作
    与原数据内存共享,用clone()不会修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    x = torch.rand(4,3)
    # 取第二列
    print(x[:, 1])

    y = x[0,:]
    y += 1
    print(y)
    print(x[0, :]) # 源tensor也被改了了

    y -= 1
    a = y.clone()
    a += 1
    print(x[0, :])
  3. 维度变换

    1
    2
    3
    4
    5
    x = torch.randn(4,4)
    y = x.view(16)
    z = x.view(-1,2)
    y += 1
    print(x)

    view()共享内存,仅是更改了对张量的观察角度,而reshape()不共享内存,但原始tensor如果不连续,它会返回原值的copy,所以推荐先用clone创建副本再view,用clone能记录到计算图中,梯度回传到副本时也会传到源tensor

    扩展/压缩tensor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    o = torch.rand(2,3)
    print(o)
    r = o.unsqueeze(0)
    print(r)
    print(r.shape)

    s = r.squeeze(0)
    print(s)
    print(s.shape)
  4. 取值操作

    1
    2
    3
    4
    import torch
    x = torch.randn(1)
    print(type(x))
    print(type(x.item()))

    其他操作见官方文档

张量的广播机制

两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor形状相同后,再按元素运算。

1
2
3
4
5
p = torch.arange(1, 3).view(1, 2)
print(p)
q = torch.arange(1, 4).view(3, 1)
print(q)
print(p + q)

自动求导

Autograd

它是torch.tensor的核心类,设置它的属性requires_grad=True来追踪张量,完成计算后调用backward()来自动计算所有梯度,导数会自动累积到grad,由链式法则可以计算导数,torch.autograd就是计算雅可比矩阵乘积的。

  • requires_grad
    如果没有指定的话,默认输入的这个标志是 False。
  • grad_fn
    每个张量都有一个grad_fn属性,该属性引用了创建Tensor自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fn是None)。Tensor 和 Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。
    1
    2
    3
    4
    5
    6
    7
    a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
    a = ((a * 3) / (a - 1))
    print(a.requires_grad)
    a.requires_grad_(True)
    print(a.requires_grad)
    b = (a * a).sum()
    print(b.grad_fn)

下面举个例子说明梯度计算过程

  1. 创建一个张量并设置requires_grad=True用来追踪其计算历史
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    x = torch.ones(2, 2, requires_grad=True)
    print(x)

    y = x**2
    print(y)
    print(y.grad_fn)

    z = y * y * 3
    out = z.mean()
    print(z, out)
  2. 现在开始进行反向传播,因为out是一个标量,因此out.backward()和 out.backward(torch.tensor(1.)) 等价。由于grad在反向传播是累加的,所以在backward之前要清零.grad.data.zero_()。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    out.backward()
    print(x.grad)

    # 再来反向传播⼀一次,注意grad是累加的
    out2 = x.sum()
    out2.backward()
    print(x.grad)

    out3 = x.sum()
    x.grad.data.zero_()
    out3.backward()
    print(x.grad)
    雅可比向量积,.data.norm()它对张量y每个元素进行平方,然后对它们求和,最后取平方根,这些操作计算就是所谓的L2或欧几里德范数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    x = torch.randn(3, requires_grad=True)
    print(x)

    y = x * 2
    i = 0
    while y.data.norm() < 1000:
    y = y * 2
    i = i + 1
    print(y)
    print(i)
    当y不再是标量,torch.autograd不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给backward:
    1
    2
    3
    v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
    y.backward(v)
    print(x.grad)
    若不需要计算梯度,阻止autograd跟踪设置.requires_grad=True的张量的历史记录
    1
    2
    3
    4
    5
    print(x.requires_grad)
    print((x ** 2).requires_grad)

    with torch.no_grad():
    print((x ** 2).requires_grad)
    若想修改tensor的数值,又不希望被autograd记录(即不会影响反向传播), 那么我们可以对tensor.data进行操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    x = torch.ones(1,requires_grad=True)

    print(x.data) # 还是一个tensor
    print(x.data.requires_grad) # 但是已经是独立于计算图之外,返回false

    y = 2 * x
    x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播

    y.backward()
    print(x) # 更改data的值也会影响tensor的值,x为100
    print(x.grad) # 不会受到值改变的影响

并行计算简介

PyTorch可以在编写完模型之后,让多个GPU来参与训练,减少训练时间。CUDA是我们使用GPU的提供商——NVIDIA提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。而pytorch编写深度学习代码使用的CUDA是表示开始要求我们的模型或者数据开始使用GPU了。当我们使用了.cuda()时,其功能是让我们的模型或者数据从CPU迁移到GPU(0)当中,通过GPU开始计算。

注:数据在GPU和CPU之间进行传递时会比较耗时,我们应当尽量避免数据的切换。GPU运算很快,但是在使用简单的操作时,我们应该尽量使用CPU去完成。当我们的服务器上有多个GPU,我们应该指明我们使用的GPU是哪一块,如果我们不设置的话,tensor.cuda()方法会默认将tensor保存到第一块GPU上,等价于tensor.cuda(0),这将会导致爆出out of memory的错误。我们可以通过以下两种方式继续设置。

1
2
3
4
 #设置在文件最开始部分
import os
os.environ["CUDA_VISIBLE_DEVICE"] = "2" # 设置默认的显卡
CUDA_VISBLE_DEVICE=0,1 python train.py # 使用0,1两块GPU

常见的并行方法:

  • 网络结构分布到不同的设备中(Network partitioning)
    将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。这里遇到的问题就是,不同模型组件在不同的GPU上时,GPU之间的传输就很重要,对于GPU之间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。

  • 同一层的任务分布到不同数据中(Layer-wise partitioning)
    同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。这样可以保证在不同组件之间传输的问题,但是在我们需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题。

  • 不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)
    它的逻辑是,不再拆分模型,训练的时候模型都是一整个模型。但是我将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传,这种方式可以解决之前模式遇到的通讯问题,是现在主流方式。

    参考资料

    深入浅出PyTorch

作者

Yang

发布于

2022-10-10

更新于

2022-10-16

许可协议

评论