• Pytorch(一)——Pytorch基础知识



    之前学习Pytorch已经比较久远了,本次学习是跟着datawhale的《深入浅出Pytorch》进行回顾和查缺补漏,以下是个人笔记,如果侵权咱就删。

    Pytorch学习资源

    1.张量

    张量是现代机器学习的基础。它的核心是一个数据容器,多数情况下,它包含数字,有时候它也包含字符串,但这种情况比较少。因此可以把它想象成一个数字的水桶。
    例子:一个图像可以用三个字段表示:

    (width, height, channel) = 3D
    
    • 1

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

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

    创建tensor

    随机初始化矩阵 我们可以通过torch.rand()的方法,构造一个随机初始化的矩阵:

    import torch
    x = torch.rand(4, 3) 
    print(x)
    
    • 1
    • 2
    • 3
    tensor([[0.7569, 0.4281, 0.4722],
            [0.9513, 0.5168, 0.1659],
            [0.4493, 0.2846, 0.4363],
            [0.5043, 0.9637, 0.1469]])
    
    • 1
    • 2
    • 3
    • 4

    全0矩阵的构建 我们可以通过torch.zeros()构造一个矩阵全为 0,并且通过dtype设置数据类型为 long。除此以外,我们还可以通过torch.zero_()和torch.zeros_like()将现有矩阵转换为全0矩阵.

    import torch
    x = torch.zeros(4, 3, dtype=torch.long)
    print(x)
    
    • 1
    • 2
    • 3
    tensor([[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]])
    
    • 1
    • 2
    • 3
    • 4
    a = torch.randn(3,4)
    print(a.zero_())
    
    • 1
    • 2
    tensor([[0., 0., 0., 0.],
            [0., 0., 0., 0.],
            [0., 0., 0., 0.]])
    
    • 1
    • 2
    • 3
    a = torch.rand(3,4)  # 产生一个3行4列的0~1的随机Tensor
    b = torch.zeros_like(a)
    print(b)
    
    • 1
    • 2
    • 3
    tensor([[0., 0., 0., 0.],
            [0., 0., 0., 0.],
            [0., 0., 0., 0.]])
    
    • 1
    • 2
    • 3

    张量的构建 我们可以通过torch.tensor()直接使用数据,构造一个张量:

    import torch
    x = torch.tensor([5.5, 3]) 
    print(x)
    
    • 1
    • 2
    • 3
    tensor([5.5000, 3.0000])
    
    • 1

    基于已经存在的 tensor,创建一个 tensor :

    x = x.new_ones(4, 3, dtype=torch.double) 
    # 创建一个新的全1矩阵tensor,返回的tensor默认具有相同的torch.dtype和torch.device
    # 也可以像之前的写法 x = torch.ones(4, 3, dtype=torch.double)
    print(x)
    x = torch.randn_like(x, dtype=torch.float)
    # 重置数据类型
    print(x)
    # 结果会有一样的size
    # 获取它的维度信息
    print(x.size())
    print(x.shape)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    tensor([[1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.]], dtype=torch.float64)
    tensor([[ 2.7311, -0.0720,  0.2497],
            [-2.3141,  0.0666, -0.5934],
            [ 1.5253,  1.0336,  1.3859],
            [ 1.3806, -0.6965, -1.2255]])
    torch.Size([4, 3])
    torch.Size([4, 3])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    返回的torch.Size其实是一个tuple,⽀持所有tuple的操作。我们可以使用索引操作取得张量的长、宽等数据维度。

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

    张量的操作

    1.加法操作

    import torch
    # 方式1
    y = torch.rand(4, 3) 
    print(x + y)
    
    # 方式2
    print(torch.add(x, y))
    
    # 方式3 in-place,原值修改
    y.add_(x) 
    print(y)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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]])
    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]])
    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]])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    import torch
    x = torch.rand(4,3)
    # 取第二列
    print(x[:, 1]) 
    
    • 1
    • 2
    • 3
    • 4
    tensor([-0.0720,  0.0666,  1.0336, -0.6965])
    
    • 1
    y = x[0,:]
    y += 1
    print(y)
    print(x[0, :]) # 源tensor也被改了了
    
    • 1
    • 2
    • 3
    • 4
    tensor([3.7311, 0.9280, 1.2497])
    tensor([3.7311, 0.9280, 1.2497])
    
    • 1
    • 2

    3.维度变换 张量的维度变换常见的方法有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())
    
    • 1
    • 2
    • 3
    • 4
    torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
    
    • 1

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

    x += 1
    print(x)
    print(y) # 也加了了1
    
    • 1
    • 2
    • 3
    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]])
    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])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    注:使用clone()还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源 Tensor 。
    4. 取值操作 如果我们有一个元素tensor,我们可以使用.item()来获得这个value,而不获得其他性质:

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

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

    广播机制

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

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

    2.自动求导

    Autograd简介

    torch.Tensor是这个包的核心类。如果设置它的属性.requires_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。
    注意:在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor
    要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad(): 中。在评估模型时特别有用,因为模型可能具有requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。
    还有一个类对于autograd的实现非常重要:FunctionTensorFunction互相连接生成了一个无环图 (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)
    
    • 1
    • 2
    • 3
    • 4
    None
    
    • 1

    如果需要计算导数,可以在Tensor上调用 .backward()。如果Tensor是一个标量(即它包含一个元素的数据),则不需要为 backward()指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。
    创建一个张量并设置requires_grad=True用来追踪其计算历史:

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

    对这个张量做一次运算:

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

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

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

    对 y 进行更多操作

    z = y * y * 3
    out = z.mean()
    
    print(z, out)
    
    • 1
    • 2
    • 3
    • 4
    tensor([[3., 3.],
            [3., 3.]], grad_fn=<MulBackward0>) tensor(3., grad_fn=<MeanBackward0>)
    
    • 1
    • 2

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    False
    True
    <SumBackward0 object at 0x000001CB4A19FB50>
    
    • 1
    • 2
    • 3

    梯度

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

    out.backward()
    
    • 1

    输出导数d(out)/dx

    print(x.grad)
    
    • 1
    tensor([[3., 3.],
            [3., 3.]])
    
    • 1
    • 2

    数学上,若有向量函数 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(

    y1x1y1xnymx1ymxn" role="presentation" style="position: relative;">y1x1y1xnymx1ymxn
    \right) J=x1y1x1ymxny1xnym
    torch.autograd 这个包就是用来计算一些雅可比矩阵的乘积的。例如,如果 v v v 是一个标量函数 l = g ( y ⃗ ) l = g(\vec{y}) l=g(y ) 的梯度:
    v = ( ∂ l ∂ y 1 ⋯ ∂ l ∂ y m ) v=\left(
    ly1lym" role="presentation" style="position: relative;">ly1lym
    \right)
    v=(y1lyml)

    由链式法则,我们可以得到:
    v J = ( ∂ l ∂ y 1 ⋯ ∂ l ∂ y m ) ( ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ) = ( ∂ l ∂ x 1 ⋯ ∂ l ∂ x n ) v J=\left(
    ly1lym" role="presentation" style="position: relative;">ly1lym
    \right)\left(
    y1x1y1xnymx1ymxn" role="presentation" style="position: relative;">y1x1y1xnymx1ymxn
    \right)=\left(
    lx1lxn" role="presentation" style="position: relative;">lx1lxn
    \right)
    vJ=(y1lyml)x1y1x1ymxny1xnym=(x1lxnl)

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

    # 再来反向传播⼀一次,注意grad是累加的
    out2 = x.sum()
    out2.backward()
    print(x.grad)
    
    out3 = x.sum()
    x.grad.data.zero_()
    out3.backward()
    print(x.grad)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    tensor([[4., 4.],
            [4., 4.]])
    tensor([[1., 1.],
            [1., 1.]])
    
    • 1
    • 2
    • 3
    • 4

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

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    tensor([-1.8979, -0.1852, -0.1072], requires_grad=True)
    tensor([-1943.4286,  -189.6687,  -109.8237], grad_fn=<MulBackward0>)
    9
    
    • 1
    • 2
    • 3
    v = torch.tensor([1, 1, 1], dtype=torch.float)
    y.backward(v)
    
    print(x.grad)
    
    • 1
    • 2
    • 3
    • 4
    tensor([1024., 1024., 1024.])
    
    • 1

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

    print(x.requires_grad)
    print((x ** 2).requires_grad)
    
    with torch.no_grad():
        print((x ** 2).requires_grad)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    True
    True
    False
    
    • 1
    • 2
    • 3

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

    x = torch.ones(1,requires_grad=True)
    
    print(x.data) # 还是一个tensor
    print(x.data.requires_grad) # 但是已经是独立于计算图之外
    
    y = 2 * x
    x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
    
    y.backward()
    print(x) # 更改data的值也会影响tensor的值 
    print(x.grad)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    tensor([1.])
    False
    tensor([100.], requires_grad=True)
    tensor([2.])
    
    • 1
    • 2
    • 3
    • 4

    3.并行计算简介

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

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

    网络结构分布到不同的设备中(Network partitioning)

    在刚开始做模型并行的时候,这个方案使用的比较多。其中主要的思路是,将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。其架构如下:

    请添加图片描述

    这里遇到的问题就是,不同模型组件在不同的GPU上时,GPU之间的传输就很重要,对于GPU之间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。

    同一层的任务分布到不同数据中(Layer-wise partitioning)

    第二种方式就是,同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。其架构如下:

    请添加图片描述

    这样可以保证在不同组件之间传输的问题,但是在我们需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题。

    不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)

    第三种方式有点不一样,它的逻辑是,我不再拆分模型,我训练的时候模型都是一整个模型。但是我将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传。其架构如下:

    请添加图片描述

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

  • 相关阅读:
    【开源】JAVA+Vue.js实现创意工坊双创管理系统
    植物叶绿素测定
    ElasticSearch基本操作
    [手写spring](5)实现AOP机制(完结)
    Unity3D
    【PostgreSQL】PG_DUMP的文件大小元小于库占用物理空间统计
    麻雀算法(SSA)优化混合核极限学习机(HKELM)分类预测,多输入单输出模型,SSA-HKELM分类预测。
    多线程总结(线程池 & 线程安全 & 常见锁)
    苹果知名开发者怒“怼”:GitHub 不可信,俄罗斯开发者贡献的项目遭毁灭性打击
    计算机网络 —— 网络字节序
  • 原文地址:https://blog.csdn.net/qq_44941689/article/details/125778229