Pytorch基础知识

Pytorch基础知识

      • 张量
        • 简介
        • 创建Tensor
        • 张量的操作
        • 广播机制
      • 自动求导
        • Autograd简介
        • 梯度计算
      • 并行计算简介
        • 为什么要做并行计算
        • 并行计算和CUDA什么关系
        • 常见的并行的方法:
          • 网络结构分布到不同的设备中(Network partitioning)
          • 同一层的任务分布到不同数据中(Layer-wise partitioning)
          • 不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)

张量

简介

几何代数中定义的张量是基于向量和矩阵的推广,比如我们可以将标量视为零阶张量,矢量可以视为一阶张量,矩阵就是二阶张量。

张量维度 代表含义
0维张量 代表的是标量(数字)
1维张量 代表的是向量
2维张量 代表的是矩阵
3维张量 时间序列数据 股价 文本数据 单张彩色图片(RGB)

张量是现代机器学习的基础。它的核心是一个数据容器,多数情况下,它包含数字,有时候它也包含字符串,但这种情况比较少。

这里有一些存储在各种类型张量的公用数据集类型:

  • 3维 = 时间序列
  • 4维 = 图像
  • 5维 = 视频

例子:一个图像可以用三个字段表示:

(width, height, channel) = 3D

但是,在机器学习工作中,我们经常要处理不止一张图片或一篇文档——我们要处理一个集合。我们可能有10,000张郁金香的图片,这意味着,我们将用到4D张量:

(batch_size, width, height, channel) = 4D

在PyTorch中, torch.Tensor 是存储和变换数据的主要工具。如果你之前用过NumPy,你会发现 Tensor 和NumPy的多维数组非常类似。然而,Tensor 提供GPU计算和自动求梯度等更多功能,这些使 Tensor 这一数据类型更加适合深度学习。

创建Tensor

  • 初始化一个随机数 Tensor

    >>> import tensor
    >>> torch_random_4_3 = torch.rand(4,3)
    >>> print(torch_random_4_3)
    tensor([[0.5949, 0.7045, 0.9920],
            [0.0107, 0.6573, 0.7009],
            [0.6846, 0.9896, 0.4545],
            [0.9499, 0.4030, 0.4142]])
    
  • 初始化一个全 0 Tensor 或者 全 1 Tensor

    >>> import tensor
    >>> tensor_zero_4_3 = torch.zeros(4,3, dtype=torch.long)
    >>> print(tensor_zero_4_3)
    tensor([[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]])
    
    >>> tensor_one_4_3 = torch.ones(4,3, dtype=torch.long)
    >>> print(tensor_one_4_3)
    tensor([[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]])
    
  • List或者numpy.array转换成Tensor

    >>> import torch
    >>> tensor_by_list = torch.tensor([5.5, 3])
    >>> print(tensor_by_list)
    tensor([5.5000, 3.0000])
    
    >>> import numpy as np
    >>> tensor_by_numpy = torch.tensor(np.array([5.5, 3]))
    >>> print(tensor_by_numpy)
    tensor([5.5000, 3.0000], dtype=torch.float64)
    
  • 创建相同维度的Tensor

    >>> import torch
    >>> tensor_one_4_3 = torch.ones(4,3, dtype=torch.double)
    >>> tensor_random_cpdim = torch.randn_like(tensor_one_4_3,  dtype=torch.float)
    >>> print(tensor_one_4_3)
    tensor([[1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.]], dtype=torch.float64)
    >>> print(tensor_random_cpdim)
    tensor([[ 1.2542,  0.3018,  1.7961],
            [ 2.0652,  0.3100,  0.9422],
            [-0.7710,  0.8658, -0.0066],
            [-0.6149, -1.5649, -0.1749]])
    >>> print(tensor_one_4_3.shape) # 等同于 print(tensor_one_4_3.size())
    torch.Size([4, 3])
    >>> print(tensor_random_cpdim.shape) # 等同于 print(tensor_one_4_3.size())
    torch.Size([4, 3])
    
  • 常见的构造Tensor的方法:

    函数 功能
    torch.Tensor(sizes) 基础构造函数
    torch.tensor(data) 类似于np.array
    torch.ones(sizes) 全1矩阵
    torch.zeros(sizes) 全0矩阵
    torch.eye(sizes) 对角为1,其余为0
    torch.arange(s,e,step) 从s到e,步长为step
    torch.linspace(s,e,steps) 从s到e,均匀分成step份
    torch.rand rand是[0,1)均匀分布
    torch.randn(sizes) randn是服从N(0,1)的正态分布
    torch.normal(mean,std) 正态分布(均值为mean,标准差是std)
    torch.randperm(m) 随机排列

张量的操作

  • 加法操作:

    >>> import torch
    # 方式1
    >>> y = torch.rand(4, 3) 
    >>> print(x + y)
    tensor([[ 2.8977,  0.6581,  0.5856],
          [-1.3604,  0.1656, -0.0823],
          [ 2.1387,  1.7959,  1.5275],
          [ 2.2427, -0.3100, -0.4826]])
    
    # 方式2
    >>> print(torch.add(x, y))
    tensor([[ 2.8977,  0.6581,  0.5856],
          [-1.3604,  0.1656, -0.0823],
          [ 2.1387,  1.7959,  1.5275],
          [ 2.2427, -0.3100, -0.4826]])
    
    # 方式3 in-place,原值修改
    >>> y.add_(x) 
    >>> print(y)
    tensor([[ 2.8977,  0.6581,  0.5856],
          [-1.3604,  0.1656, -0.0823],
          [ 2.1387,  1.7959,  1.5275],
          [ 2.2427, -0.3100, -0.4826]])
    
  • 索引操作:(类似于numpy)

    需要注意的是:索引出来的结果与原数据共享内存,修改一个,另一个会跟着修改。如果不想修改,可以考虑使用copy()等方法

    >>> import torch
    >>> x = torch.rand(4,3)
    # 取第二列
    >>> print(x[:, 1])
    tensor([-0.0720,  0.0666,  1.0336, -0.6965])
    
    >>> y = x[0,:]
    >>> y += 1
    >>> print(y)
    tensor([3.7311, 0.9280, 1.2497])
    >>> print(x[0, :]) # 源tensor也被改了了
    tensor([3.7311, 0.9280, 1.2497])
    
  • 维度变换
    张量的维度变换常见的方法有torch.view()torch.reshape(),下面我们将介绍第一中方法torch.view()

    >>> x = torch.randn(4, 4)
    >>> y = x.view(16)
    >>> z = x.view(-1, 8) # -1是指这一维的维数由其他维度决定
    >>> print(x.size(), y.size(), z.size())
    torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
    

    注: torch.view() 返回的新tensor与源tensor共享内存(其实是同一个tensor),更改其中的一个,另外一个也会跟着改变。(顾名思义,view()仅仅是改变了对这个张量的观察角度)

    >>> x += 1
    >>> print(x)
    tensor([[ 1.3019,  0.3762,  1.2397,  1.3998],
            [ 0.6891,  1.3651,  1.1891, -0.6744],
            [ 0.3490,  1.8377,  1.6456,  0.8403],
            [-0.8259,  2.5454,  1.2474,  0.7884]])
    >>> print(y) # 也加了了1
    tensor([ 1.3019,  0.3762,  1.2397,  1.3998,  0.6891,  1.3651,  1.1891, -0.6744,
             0.3490,  1.8377,  1.6456,  0.8403, -0.8259,  2.5454,  1.2474,  0.7884])
    

    上面我们说过torch.view()会改变原始张量,但是很多情况下,我们希望原始张量和变换后的张量互相不影响。为了使创建的张量和原始张量不共享内存,我们需要使用第二种方法torch.reshape(), 同样可以改变张量的形状,但是此函数并不能保证返回的是其拷贝值,所以官方不推荐使用。推荐的方法是我们先用 clone() 创造一个张量副本然后再使用 torch.view()进行函数维度变换 。

    注:使用 clone() 还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源 Tensor 。

  • 取值操作
    如果我们有一个元素 tensor ,我们可以使用 .item() 来获得这个 value,而不获得其他性质:

    >>> import torch
    >>> x = torch.randn(1) 
    >>> print(type(x))
    <class 'torch.Tensor'>
    >>> print(type(x.item()))
    <class 'float'>
    

    PyTorch中的 Tensor 支持超过一百种操作,包括转置、索引、切片、数学运算、线性代数、随机数等等,具体使用方法可参考官方文档。

广播机制

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

>>> x = torch.arange(1, 3).view(1, 2)
>>> print(x)
tensor([[1, 2]])
>>> y = torch.arange(1, 4).view(3, 1)
>>> print(y)
tensor([[1],
        [2],
        [3]])
>>> print(x + y)
tensor([[2, 3],
        [3, 4],
        [4, 5]])

由于x和y分别是1行2列和3行1列的矩阵,如果要计算x+y,那么x中第一行的2个元素被广播 (复制)到了第二行和第三行,⽽y中第⼀列的3个元素被广播(复制)到了第二列。如此,就可以对2个3行2列的矩阵按元素相加。

自动求导

PyTorch 中,所有神经网络的核心是 autograd 包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 ( define-by-run )的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。

Autograd简介

torch.Tensor 是这个包的核心类。如果设置它的属性 .requires_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用 .backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。

注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。

要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad(): 中。在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。

还有一个类对于autograd的实现非常重要:FunctionTensor Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor 自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fnNone )。下面给出的例子中,张量由用户手动创建,因此grad_fn返回结果是None。

>>> from __future__ import print_function
>>> import torch
>>> x = torch.randn(3,3,requires_grad=True)
>>> print(x.grad_fn)
None

如果需要计算导数,可以在 Tensor 上调用 .backward()。如果 Tensor 是一个标量(即它包含一个元素的数据),则不需要为 backward() 指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。

创建一个张量并设置requires_grad=True用来追踪其计算历史

>>> x = torch.ones(2, 2, requires_grad=True)
>>> print(x)
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

对这个张量做一次运算:

>>> y = x**2
>>> print(y)
tensor([[1., 1.],
        [1., 1.]], grad_fn=<PowBackward0>)

y是计算的结果,所以它有grad_fn属性。

>>> print(y.grad_fn)
<PowBackward0 object at 0x000001CB45988C70>

对 y 进行更多操作

>>> z = y * y * 3
>>> out = z.mean()

>>> print(z, out)
tensor([[3., 3.],
        [3., 3.]], grad_fn=<MulBackward0>) tensor(3., grad_fn=<MeanBackward0>)

.requires_grad_(...) 原地改变了现有张量的requires_grad标志。如果没有指定的话,默认输入的这个标志是 False

>>> a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
>>> a = ((a * 3) / (a - 1))
>>> print(a.requires_grad)
False
>>> a.requires_grad_(True)
>>> print(a.requires_grad)
True
>>> b = (a * a).sum()
>>> print(b.grad_fn)

<SumBackward0 object at 0x000001CB4A19FB50>

梯度计算

现在开始进行反向传播,因为 out 是一个标量,因此out.backward() out.backward(torch.tensor(1.)) 等价。

>>> out.backward()

输出导数 d(out)/dx

>>> print(x.grad)
tensor([[3., 3.],
        [3., 3.]])

数学上,若有向量函数 y ⃗ = f ( x ⃗ ) \vec{y}=f(\vec{x}) y =f(x ) ,那么 y ⃗ \vec{y} y 关于 x ⃗ \vec{x} x 的梯度就是一个雅可比矩阵:
J = ( ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ) J=\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right) J= x1y1x1ymxny1xnym
torch.autograd 这个包就是用来计算一些雅可比矩阵的乘积的。例如,如果 v v v 是一个标量函数 l = g ( y ⃗ ) l = g(\vec{y}) l=g(y ) 的梯度:
$
v=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)
$
由链式法则,我们可以得到:
$
v J=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \ \vdots & \ddots & \vdots \ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right)=\left(\begin{array}{lll}\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}\end{array}\right)
$

注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。

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

>>> out3 = x.sum()
>>> x.grad.data.zero_()
>>> out3.backward()
>>> print(x.grad)
tensor([[1., 1.],
        [1., 1.]])

现在我们来看一个雅可比向量积的例子:

>>> x = torch.randn(3, requires_grad=True)
>>> print(x)
tensor([-0.9332,  1.9616,  0.1739], requires_grad=True)

>>> y = x * 2
>>> i = 0
>>> while y.data.norm() < 1000:
>>>     y = y * 2
>>>     i = i + 1
>>> print(y)
tensor([-477.7843, 1004.3264,   89.0424], grad_fn=<MulBackward0>)
>>> print(i)
8

在这种情况下,y 不再是标量。torch.autograd 不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给 backward:

>>> v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
>>> y.backward(v)

>>> print(x.grad)
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])

也可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪设置了.requires_grad=True的张量的历史记录。

>>> print(x.requires_grad)
True
>>> print((x ** 2).requires_grad)
True

>>> with torch.no_grad():
 >>>    print((x ** 2).requires_grad)
False

如果我们想要修改 tensor 的数值,但是又不希望被 autograd 记录(即不会影响反向传播), 那么我们可以对 tensor.data 进行操作。

>>> x = torch.ones(1,requires_grad=True)

>>> print(x.data) # 还是一个tensor
tensor([1.])
>>> print(x.data.requires_grad) # 但是已经是独立于计算图之外
False

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

>>> y.backward()
>>> print(x) # 更改data的值也会影响tensor的值 
tensor([100.], requires_grad=True)
>>> print(x.grad)
tensor([2.])

并行计算简介

为什么要做并行计算

一件事情,让多个人同时做,提高效率!

并行计算和CUDA什么关系

CUDA是我们使用GPU的提供商——NVIDIA提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。但是,在我们使用PyTorch编写深度学习代码时,使用的CUDA又是另一个意思。在PyTorch使用 CUDA表示要开始要求我们的模型或者数据开始使用GPU了。

在编写程序中,当我们使用了 .cuda() 时,其功能是让我们的模型或者数据从CPU迁移到GPU(0)当中,通过GPU开始计算。

注:

  1. 我们使用GPU时使用的是.cuda()而不是使用.gpu()。这是因为当前GPU的编程接口采用CUDA,但是市面上的GPU并不是都支持CUDA,只有部分NVIDIA的GPU才支持,AMD的GPU编程接口采用的是OpenCL,在现阶段PyTorch并不支持。
  2. 数据在GPU和CPU之间进行传递时会比较耗时,我们应当尽量避免数据的切换。
  3. GPU运算很快,但是在使用简单的操作时,我们应该尽量使用CPU去完成。
  4. 当我们的服务器上有多个GPU,我们应该指明我们使用的GPU是哪一块,如果我们不设置的话,tensor.cuda()方法会默认将tensor保存到第一块GPU上,等价于tensor.cuda(0),这将会导致爆出out of memory的错误。我们可以通过以下两种方式继续设置。
#设置在文件最开始部分
>>> 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中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传。(合作伙伴)

这种方式可以解决之前模式遇到的通讯问题。现在的主流方式是数据并行的方式(Data parallelism)

你可能感兴趣的:(pytorch,python,深度学习)