互联网万物基于ChatGPT,学习深度学习之前,先来请教一下ChatGPT如何进行学习。

问:如何从零开始学习深度学习?

Answer / 回答

如果你想从零开始学习深度学习,可以遵循以下步骤:

  1. 学习数学基础:深度学习需要掌握线性代数、概率论及微积分等数学基础,所以需要学好这些数学基础。
  2. 学习编程基础:深度学习需要使用编程语言进行实现,推荐学习Python。
  3. 学习深度学习理论:深度学习理论包括神经网络结构、激活函数、损失函数、优化算法等,需要掌握这些基本概念。
  4. 学习深度学习工具:深度学习需要使用一些工具,如TensorFlow、PyTorch等,需要学会如何使用这些工具。
  5. 实践项目:通过实践项目,可以巩固对深度学习理论和工具的掌握,同时提高实际应用能力。推荐先从简单的项目开始,如手写数字识别、猫狗分类等。
  6. 参与社区: 加入深度学习社区,如GitHub、Stack Overflow等,与其他深度学习爱好者交流,分享经验,提高自己的学习效率。
  7. 持续学习和更新:深度学习领域变化非常迅速,需要持续学习和更新自己的知识。可以通过阅读学术论文、参加学术会议和培训课程,保持对新技术和新方法的了解和掌握。

问:深度学习理论包括哪些内容,如何进行学习?

Answer / 回答

深度学习理论包括以下内容:

  1. 基本的神经网络结构和算法,如多层感知机、卷积神经网络、循环神经网络等。
  2. 梯度下降和反向传播算法。
  3. 激活函数的选择。
  4. 正则化技术,如L1/L2正则化、Dropout等。
  5. 深度学习模型的训练策略,如学习率调整、批量归一化等。
  6. 深度学习模型的优化算法,如Adam、Adagrad等。
  7. 深度学习模型的评估指标,如准确率、精确率、召回率等。

进行深度学习理论学习的建议如下:

  1. 学习基本的微积分、矩阵与线性代数等数学知识。
  2. 学习编程语言,如Python和C++。
  3. 学习使用深度学习工具包,如TensorFlow和PyTorch。
  4. 学习经典的深度学习模型以及相关的算法。
  5. 参考相关的学术论文和书籍进行深度学习理论学习。
  6. 进行实践并实现一些深度学习项目,加强理论知识的实际应用。

问:python中关于深度学习的包有哪些?如何进行学习?

Answer / 回答

Python中关于深度学习的包有很多,其中比较常用的包有:TensorFlow、Keras、PyTorch、Theano、MXNet、Caffe、Chainer、Torch、Lasagne、DeepLearning4J。其中,TensorFlow和PyTorch被广泛认为是目前最流行且最强大的深度学习包。如果想学习深度学习,可以按照以下步骤:

  1. 学习Python编程语言基础,掌握基本的数据类型、控制语句、函数等。
  2. 学习机器学习的基础知识,包括监督学习、无监督学习、回归、分类、聚类等。
  3. 学习深度学习的基础知识,包括神经网络、卷积神经网络、循环神经网络、自编码器、深度生成模型等。
  4. 选择一种深度学习框架,比如TensorFlow或PyTorch。
  5. 根据框架提供的教程和示例代码进行学习和实践,不断尝试和实验。
  6. 提升自己的数学能力,包括线性代数、微积分和概率论等。
  7. 参考相关论文和博客深入了解各种深度学习模型和应用场景,进一步提高自己的水平。

AI地图
AI地图

环境的配置

Python安装

官网:https://www.python.org

CUDA安装

官网:https://developer.nvidia.com/cuda-toolkit,选择合适的版本然后Download即可。

安装完成之后,去安装目录查看,如果存在nvcc.exe则证明安装成功。

也可以打开CMD窗口检验下:

C:\Windows\System32>nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2023 NVIDIA Corporation
Built on Wed_Feb__8_05:53:42_Coordinated_Universal_Time_2023
Cuda compilation tools, release 12.1, V12.1.66
Build cuda_12.1.r12.1/compiler.32415258_0

或者使用nvidia-smi命令来看下:

C:\Windows\System32>nvidia-smi
Tue Apr 18 08:09:57 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 531.61                 Driver Version: 531.61       CUDA Version: 12.1     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                      TCC/WDDM | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf            Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  NVIDIA GeForce RTX 3060       WDDM | 00000000:01:00.0  On |                  N/A |
|  0%   41C    P8               25W / 170W|   4206MiB / 12288MiB |     33%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+

PyTorch安装

官网:https://pytorch.org,选择合适的版本,然后复制命令到CMD窗口回车即可下载。

安装完成之后,验证PyTorch是否成功安装:

In [1]: import torch

In [2]: torch.__version__  # PyTorch版本
Out[2]: '2.0.0+cu117'

In [3]: torch.cuda.is_available()  # CUDA是否可用
Out[3]: True

来使用Nvidia显卡跑第一个PyTorch程序吧~

In [1]: import torch

In [2]: x = torch.ones((3, 1)).cuda(0)  # 创建一个3×1的Tensor,并将Tensor转移到GPU上

In [3]: y = torch.ones((3, 1)).cuda(0)

In [4]: x + y
Out[4]:
tensor([[2.],
        [2.],
        [2.]], device='cuda:0')

可以成功执行,则证明环境配置工作已经全部完成。

《动手学深度学习》v2中相关包的安装

运行《动手学深度学习》v2中的Jupyter文件报错:ModuleNotFoundError: No module named 'd2l',使用pip install d2l发现也是各种报错:

于是乎直接放弃使用pip命令安装。

访问Aston Zhang大神的GitHub项目d2l-en,然后下载d2l文件夹中的所有文件:

下载并解压后,将d2l文件夹复制到Python的包安装目录。如何查看Python的包安装目录?

In [5]: import pandas

In [6]: pandas.__file__
Out[6]: 'C:\\Users\\myxc\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\pandas\\__init__.py'

代码如上所示,我的电脑中的Python包目录就是:C:\Users\myxc\AppData\Local\Programs\Python\Python310\lib\site-packages\

完成复制后,再次使用import d2l命令还是出现报错:

可以看出这是因为d2l是基于torchtext包来运行的,那么安装下torchtext即可,后面会发现还缺少gym模块,也顺便安装了吧~

PS C:\Users\myxc> pip install torchtext
PS C:\Users\myxc> pip install gym

安装完成后就可以顺利地进行学习啦。以chapter_convolutional-modern文件夹下的resnet.ipynb文件进行测试:

测试全程,3060卡的风扇都呼呼的~

数据操作

创建Tensor

torch.empty(5, 3)  # 创建一个未初始化的Tensor

torch.rand(5, 3)  # 创建一个5x3的随机(0~1)初始化Tensor

torch.zeros(5, 3, dtype=torch.long) # 创建一个5x3的long型全0的Tensor

x = torch.tensor([5.5, 3])  # 还可以直接根据数据创建

x = x.new_ones(5, 3, dtype=torch.float64)  # 还可以通过现有的Tensor来创建,此方法会默认重用输入Tensor的一些属性,例如数据类型,除非自定义数据类型。

x = x.new_ones(5, 3, dtype=torch.float64)  # 返回的tensor默认具有相同的torch.dtype和torch.device

x = torch.randn_like(x, dtype=torch.float) # 指定新的数据类型

BUGS / 报错

在使用Tensor创建一个5x3的未初始化的张量时,会提出警告,如下图所示:

解决方法:

  1. 卸载现有Numpy版本,pip uninstall numpy
  2. 安装匹配的版本,pip install tensorflow,自动补全安装版本匹配的Numpy。

运算

常见的标准算术运算符(+-*/**)都是可以被升级为按元素运算的。

In [36]: x, y = torch.arange(6), torch.ones(6) * 2

In [37]: x, y
Out[37]: (tensor([0, 1, 2, 3, 4, 5]), tensor([2., 2., 2., 2., 2., 2.]))

In [38]: x + y, x - y, x * y, x ** y
Out[38]:
(tensor([2., 3., 4., 5., 6., 7.]),
 tensor([-2., -1.,  0.,  1.,  2.,  3.]),
 tensor([ 0.,  2.,  4.,  6.,  8., 10.]),
 tensor([ 0.,  1.,  4.,  9., 16., 25.]))

加法

In [1]: import torch

In [2]: x = torch.ones(2, 3)  # 创建一个5x3全1的Tensor

In [3]: y = x * 2

In [4]: y
Out[4]:
tensor([[2., 2., 2.],
        [2., 2., 2.]])

In [5]: x + y  # 1、使用"+"直接相加
Out[5]:
tensor([[3., 3., 3.],
        [3., 3., 3.]])

In [6]: torch.add(x, y)  # 2、使用.add()方法
Out[6]:
tensor([[3., 3., 3.],
        [3., 3., 3.]])

In [7]: result = torch.empty(5, 3)  # 3、创建一个空的Tensor,然后装进去

In [8]: torch.add(x, y, out=result)
Out[8]:
tensor([[3., 3., 3.],
        [3., 3., 3.]])

In [9]: y.add(x)  # 4、add()方法的直接使用
Out[9]:
tensor([[3., 3., 3.],
        [3., 3., 3.]])

In [10]: y.add_(x)  # 5、注:PyTorch操作inplace版本都有后缀_, 例如x.copy_(y), x.t_()
Out[10]:
tensor([[3., 3., 3.],
        [3., 3., 3.]])

In [11]: y
Out[11]:
tensor([[3., 3., 3.],
        [3., 3., 3.]])

求和

使用.sum()方法可以对Tensor求和,但是求和后仍然是一个一维的Tensor元素,使用.item()方法可以取出这个元素的数值。

In [43]: x
Out[43]:
tensor([[0, 1],
        [2, 3],
        [4, 5]])

In [44]: x.sum()
Out[44]: tensor(15)

In [45]: x.sum().item()
Out[45]: 15

.sum()方法还可以对多维Tensor求不同维度下的和:

In [27]: x = torch.arange(18).reshape(2, 3, 3)  # 创建一个2×3×3三维Tensor

In [28]: x
Out[28]:
tensor([[[ 0,  1,  2],
         [ 3,  4,  5],
         [ 6,  7,  8]],

        [[ 9, 10, 11],
         [12, 13, 14],
         [15, 16, 17]]])

In [29]: x.sum()
Out[29]: tensor(153)

In [30]: x.sum(axis=0)  # 对第(3-0)维求和,就是先“消灭”第三个维度,两个矩阵相加,就是求和的结果
Out[30]:
tensor([[ 9, 11, 13],
        [15, 17, 19],
        [21, 23, 25]])

In [31]: x.sum(axis=1)  # 对第(3-1)维求和,就是先“消灭”第二个维度,对每一列进行相加,即可消灭
Out[31]:
tensor([[ 9, 12, 15],
        [36, 39, 42]])

In [32]: x.sum(axis=2)  # 对第(3-2)维求和,就是先“消灭”第一个维度,对每一行进行相加,即可消灭
Out[32]:
tensor([[ 3, 12, 21],
        [30, 39, 48]])

连接两个张量

使用.cat()方法对张量进行连接:

In [39]: x, y = torch.arange(6).view(3, 2), torch.ones(3, 2)

In [40]: x, y
Out[40]:
(tensor([[0, 1],
         [2, 3],
         [4, 5]]),
 tensor([[1., 1.],
         [1., 1.],
         [1., 1.]]))

In [41]: torch.cat((x, y))  # 默认dim=0,即在列的方向上堆叠
Out[41]:
tensor([[0., 1.],
        [2., 3.],
        [4., 5.],
        [1., 1.],
        [1., 1.],
        [1., 1.]])

In [42]: torch.cat((x, y), dim=1)  # dim=1在行的方向上堆叠
Out[42]:
tensor([[0., 1., 1., 1.],
        [2., 3., 1., 1.],
        [4., 5., 1., 1.]])

索引

In [1]: import torch

In [2]: x = torch.ones(2, 3)  # 创建一个2x3全1的Tensor

In [3]: y = x[0, :]

In [4]: y += 1

In [5]: y
Out[5]: tensor([2., 2., 2.])

In [6]: x
Out[6]:
tensor([[2., 2., 2.],
        [1., 1., 1.]])

Warning / 注意

注意:Tensor中的切片索引不像在Numpy中,Numpy中为了防止对切片的操作影响到原始DataFrame,可以使用df.copy(),将切片完全复制一份出来,从而防止原始DataFrame被修改。Tensor中可以使用.clone()方法来实现这样的效果。

索引的更多用法
索引的更多用法

改变形状

可以使用.shape属性访问张量的形状,使用.numel()方法访问张量中元素的总个数:

In [16]: x = torch.arange(4).view(2, 2)

In [17]: x
Out[17]:
tensor([[0, 1],
        [2, 3]])

In [18]: x.shape
Out[18]: torch.Size([2, 2])

In [19]: x.numel()
Out[19]: 4
Tips / 提示

在Pandas中,同样使用.shape属性DataFrame的形状,使用.size属性访问DataFrame中元素的个数。

In [2]: x = pd.DataFrame([[1, 2], [3, 4]])

In [3]: x
Out[3]:
   0  1
0  1  2
1  3  4

In [5]: x.shape
Out[5]: (2, 2)

In [7]: x.size
Out[7]: 4

如果要改变一个Tensor的形状而不改变其中元素的数量与个数,可以使用.reshape()或者.view()方法来改变Tensor的形状:

In [20]: x = torch.arange(6).view(3, 2)  # 使用0~5创建一个3×2的Tensor

In [21]: x
Out[21]:
tensor([[0, 1],
        [2, 3],
        [4, 5]])

In [22]: x.shape
Out[22]: torch.Size([3, 2])

In [23]: y = x.reshape(2, 3)

In [24]: y.shape
Out[24]: torch.Size([2, 3])

In [25]: z = x.reshape(-1, 6)  # -1代表所指的维度可以根据其他维度的值推出来

In [26]: z
Out[26]: tensor([[0, 1, 2, 3, 4, 5]])

In [27]: y += 1

In [28]: y
Out[28]:
tensor([[1, 2, 3],
        [4, 5, 6]])

In [29]: x
Out[29]:
tensor([[1, 2],
        [3, 4],
        [5, 6]])

In [30]: z += 1

In [31]: z
Out[31]: tensor([[2, 3, 4, 5, 6, 7]])

In [32]: x
Out[32]:
tensor([[2, 3],
        [4, 5],
        [6, 7]])

Warning / 注意

注意.view().reshape()返回的新Tensor与源Tensor虽然可能有不同的size,但是共享data,也即更改其中的一个,另外一个也会跟着改变。(顾名思义,view仅仅是改变了对这个张量的观察角度,内部数据并未改变)

虽然.view().reshape()返回的Tensor与源Tensor是共享data的,但是依然是一个新的Tensor(因为Tensor除了包含data外还有一些其他属性),二者id(内存地址)并不一致。

In [33]: id(x) == id(y)
Out[33]: False

In [34]: id(x) == id(z)
Out[34]: False

线性代数

标量

标量由只有一个元素的张量表示:

In [1]: import torch

In [2]: torch.tensor([3.])  # 创建一个Tensor标量
Out[2]: tensor([3.])

向量

由一组标量值组成的列表成为向量:

In [3]: x = torch.arange(5)  # 创建一个Tensor向量

In [4]: x[3]  # 通过Tensor的索引来访问其中的任一个元素
Out[4]: tensor(3)

In [5]: len(x)  # 访问张量的长度
Out[5]: 5

In [6]: x.shape  # 只有一个轴的张量,形状只有一个元素
Out[6]: torch.Size([5])

矩阵

矩阵的认识

通过指定两个分量m和n来创建一个形状为m×n的矩阵:

In [7]: x = torch.arange(20).reshape(5, 4)  # 创建一个5×4的Tensor

In [8]: x
Out[8]:
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [12, 13, 14, 15],
        [16, 17, 18, 19]])

In [9]: x.T  # 矩阵的转置
Out[9]:
tensor([[ 0,  4,  8, 12, 16],
        [ 1,  5,  9, 13, 17],
        [ 2,  6, 10, 14, 18],
        [ 3,  7, 11, 15, 19]])

In [10]: y = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])  # 对称矩阵

In [11]: y == y.T
Out[11]:
tensor([[True, True, True],
        [True, True, True],
        [True, True, True]])
Tensor的认识

向量是标量的推广,矩阵是向量的推广。我们可以构建具有更多轴的数据结构:

In [12]: x = torch.arange(24).reshape(2, 3, 4)

In [13]: x
Out[13]:
tensor([[[ 0,  1,  2,  3],
         [ 4,  5,  6,  7],
         [ 8,  9, 10, 11]],

        [[12, 13, 14, 15],
         [16, 17, 18, 19],
         [20, 21, 22, 23]]])

In [14]: x.shape
Out[14]: torch.Size([2, 3, 4])
哈达玛积

两个矩阵的按元素乘法称为哈达玛积(Hadamard product),数学符号为“$\odot$”:

In [15]: x = torch.ones(6).reshape(3, 2)

In [16]: y = x * 2

In [17]: x * y
Out[17]:
tensor([[2., 2.],
        [2., 2.],
        [2., 2.]])
平均值

一个与求和相关的量是平均值(mean或average):

BUGS / 报错

如果创建Tensor时没有指定其为float数据类型,那么使用.mean()求平均值时就会报错,如下所示:

因此只能对float类型的Tensor求平均值:

In [38]: x = torch.arange(18, dtype=torch.float32).reshape(2, 3, 3)  # 创建一个Float类型的Tensor

In [39]: x.mean() # 求Tensor的平均值
Out[39]: tensor(8.5000)
 
In [40]: x.mean(axis=0)  # 求Tensor第(3-0)维度的平均值
Out[40]:
tensor([[ 4.5000,  5.5000,  6.5000],
        [ 7.5000,  8.5000,  9.5000],
        [10.5000, 11.5000, 12.5000]])

In [41]: x.sum(axis=0) / x.shape[0]  # 求Tensor第(3-n)维度的平均值就等于该维度的和/该维度元素的个数
Out[41]:
tensor([[ 4.5000,  5.5000,  6.5000],
        [ 7.5000,  8.5000,  9.5000],
        [10.5000, 11.5000, 12.5000]])

如何在计算总和或均值时保持轴数不变?

In [42]: x.mean(axis=0, keepdims=True)
Out[42]:
tensor([[[ 4.5000,  5.5000,  6.5000],
         [ 7.5000,  8.5000,  9.5000],
         [10.5000, 11.5000, 12.5000]]])

可以看出在In [40]行,对第三维度求平均值后,结果丢失了一个维度,变成了二维Tensor。这样的结果不方便利用广播机制来进行Tensor的操作。因此在In [42]中可以通过添加keepdims=True参数来保持维度的不丢失。

In [43]: x / x.mean(axis=0, keepdims=True)
Out[43]:
tensor([[[0.0000, 0.1818, 0.3077],
         [0.4000, 0.4706, 0.5263],
         [0.5714, 0.6087, 0.6400]],

        [[2.0000, 1.8182, 1.6923],
         [1.6000, 1.5294, 1.4737],
         [1.4286, 1.3913, 1.3600]]])

In [44]: x
Out[44]:
tensor([[[ 0.,  1.,  2.],
         [ 3.,  4.,  5.],
         [ 6.,  7.,  8.]],

        [[ 9., 10., 11.],
         [12., 13., 14.],
         [15., 16., 17.]]])
累加求和
In [46]: x.cumsum(axis=0)
Out[46]:
tensor([[[ 0.,  1.,  2.],
         [ 3.,  4.,  5.],
         [ 6.,  7.,  8.]],

        [[ 9., 11., 13.],
         [15., 17., 19.],
         [21., 23., 25.]]])

In [47]: x.cumsum(axis=1)
Out[47]:
tensor([[[ 0.,  1.,  2.],
         [ 3.,  5.,  7.],
         [ 9., 12., 15.]],

        [[ 9., 10., 11.],
         [21., 23., 25.],
         [36., 39., 42.]]])

In [48]: x.cumsum(axis=2)
Out[48]:
tensor([[[ 0.,  1.,  3.],
         [ 3.,  7., 12.],
         [ 6., 13., 21.]],

        [[ 9., 19., 30.],
         [12., 25., 39.],
         [15., 31., 48.]]])
点积
In [52]: x, y = torch.arange(6), torch.arange(6, 12)

In [53]: x, y
Out[53]: (tensor([0, 1, 2, 3, 4, 5]), tensor([ 6,  7,  8,  9, 10, 11]))

In [54]: torch.dot(x, y)
Out[54]: tensor(145)

In [55]: torch.sum(x * y)
Out[55]: tensor(145)
Tips / 提示

点积是针对向量的概念,因此torch.dot()仅针对两个一维Tensor有效。

矩阵的向量积

矩阵向量积$\mathbf{Ax}$是一个长度为$m$的列向量,其$i^{th}$元素是点积$a^T_i\mathbf{x}$:

In [59]: x, y = torch.arange(6).reshape(2, 3), torch.arange(1, 4)

In [60]: x, y
Out[60]:
(tensor([[0, 1, 2],
         [3, 4, 5]]),
 tensor([1, 2, 3]))

In [61]: torch.mv(x, y)
Out[61]: tensor([ 8, 26])
矩阵乘法

两个矩阵$\mathbf{A}_{m×n}$、$\mathbf{B}_{n×z}$的乘法可以简单地看作是执行$z$次矩阵向量积,并将结果拼接在一起,形成一个$m×z$的矩阵:

In [62]: x, y = torch.arange(6).reshape(2, 3), torch.arange(6).reshape(3, 2)

In [63]: x, y
Out[63]:
(tensor([[0, 1, 2],
         [3, 4, 5]]),
 tensor([[0, 1],
         [2, 3],
         [4, 5]]))

In [64]: torch.mm(x, y)
Out[64]:
tensor([[10, 13],
        [28, 40]])
范数

1、$L_1$范数就是向量元素的绝对值之和:

$$ ||x||_1 = \sum_{i=1}^{n} |x_i| $$

In [65]: x = torch.tensor([3.0, -4])

In [66]: torch.abs(x).sum()
Out[66]: tensor(7.)

2、$L_2$范数就是向量元素平方和的平方根:

$$ ||x||_2 = \sqrt{\sum_{i=1}^{n} x_i^2} $$

In [67]: torch.norm(x)
Out[67]: tensor(5.)

3、m×n矩阵的弗罗贝尼乌斯范数(Frobenius norm)是矩阵元素的平方和的平方根:

$$ ||x||_F = \sqrt{\sum_{i=1}^{m} \sum_{j=1}^{n} x_{ij}^2} $$

In [71]: x = torch.arange(9, dtype=torch.float32).reshape(3, 3)

In [72]: torch.norm(x)
Out[72]: tensor(14.2829)

In [73]: torch.sqrt(torch.sum(x * x))
Out[73]: tensor(14.2829)
矩阵的求导
Expand / 拓展

这一部分学得好迷糊啊?‍?️,后面再看看,感觉需要开个专题研究下——「 矩阵求导」学习笔记

线性函数

另外,PyTorch还支持一些线性函数,这里提一下,免得用起来的时候自己造轮子,具体用法参考官方文档。如下表所示:

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

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

自动求导

演示

设想对函数$y=2\mathbf{x}^T\mathbf{x}$,关于列向量$\mathbf{x}$求导:

In [1]: import torch

In [2]: x = torch.arange(4.0)  # 设x为一个[0, 1, 2, 3]的一维Tensor

In [3]: x
Out[3]: tensor([0., 1., 2., 3.])

In [4]: x.requires_grad_(True)  # 打开x的梯度保留
Out[4]: tensor([0., 1., 2., 3.], requires_grad=True)

In [5]: x.grad  # 查看x的梯度

In [6]: y = 2 * torch.dot(x, x)  # 定义y=f(x)函数

In [7]: y  # y为一个标量
Out[7]: tensor(28., grad_fn=<MulBackward0>)

In [8]: y.backward()  # 对y进行求导

In [9]: x.grad  # 查看x的梯度
Out[9]: tensor([ 0.,  4.,  8., 12.])

In [10]: x.grad == 4 * x  # 验证求导结果
Out[10]: tensor([True, True, True, True])
Tips / 提示

因为$y=2\mathbf{x}^T\mathbf{x}=2\sum_{i=1}^{n}a_ix_i=2(||x||_2)^2$,所以:

$$ \frac{\partial y}{\partial \mathbf{x}} = \frac{\partial (2(||x||_2)^2)}{\partial \mathbf{x}}=2·2\mathbf{x}^T=4\mathbf{x}^T $$

升级版

再来尝试另外一个关于$\mathbf{x}$的函数:

In [12]: x.grad.zero_()  # 在默认情况,PyTorch会累积梯度,我们需要清除之前的值
Out[12]: tensor([0., 0., 0., 0.])

In [13]: y = x.sum()

In [14]: y
Out[14]: tensor(6., grad_fn=<SumBackward0>)

In [15]: y.backward()

In [16]: x.grad
Out[16]: tensor([1., 1., 1., 1.])
Tips / 提示

为什么另y = x.sum()之后进行反向传播,x.grad=[1, 1, 1, 1]?因为:

$$ y=x.sum()=6=x_1+x_2+x_3+x_4 $$

所以说:

$$ \frac{\partial y}{\partial \mathbf{x}} = \left [ \begin{matrix} \frac{\partial (x_1+x_2+x_3+x_4)}{\partial x_1} \\ \frac{\partial (x_1+x_2+x_3+x_4)}{\partial x_2} \\ \frac{\partial (x_1+x_2+x_3+x_4)}{\partial x_3} \\ \frac{\partial (x_1+x_2+x_3+x_4)}{\partial x_4} \end{matrix} \right ] = \left [ \begin{matrix} 1 \\ 1 \\ 1 \\ 1 \end{matrix} \right ] $$

常用求导公式

常用求导函数_01
常用求导函数_01
常用求导函数_02
常用求导函数_02

为什么要使用

使用backward()的目的,是为了求出某个张量对于某些标量节点的梯度。而我们只能对标量使用backward(),如果需要对向量或者矩阵使用,需要进行下转换。

举个?:

$$ \mathbf{y}= \left [ \begin{matrix} y_1 \\ y_2 \\ y_3 \end{matrix} \right ]= \mathbf{f}(\mathbf{x})= \left [ \begin{matrix} x_1x_2x_3 \\ x_1+x_2+x_3 \\ x_1+x_2x_3 \end{matrix} \right ] $$

自然,我们是无法直接对$\mathbf{y}$使用backward()的,因为$\mathbf{y}$是一个向量而非标量。这种情况下,我们可以使用一种骚操作来“弯道超车”:

利用一个函数$z=\mathbf{g}(\mathbf{y})$,先不考虑函数内部如何实现,只把这个向量变成一个标量。然后我们就可以对$z$使用backward(),因为$z$是一个标量,backward()表示张量$\mathbf{x}$对标量$z$的梯度。

$$ \frac{\partial z}{\partial \mathbf{x}} = \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial x_1}, \ \frac{\partial \mathbf{g}}{\partial x_2}, \ \frac{\partial \mathbf{g}}{\partial x_3} \end{matrix} \right ] = \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial y_1} \frac{\partial y_1}{\partial x_1}+\frac{\partial \mathbf{g}}{\partial y_2} \frac{\partial y_2}{\partial x_1}+\frac{\partial \mathbf{g}}{\partial y_3} \frac{\partial y_3}{\partial x_1} \\ \frac{\partial \mathbf{g}}{\partial y_1} \frac{\partial y_1}{\partial x_2}+\frac{\partial \mathbf{g}}{\partial y_2} \frac{\partial y_2}{\partial x_2}+\frac{\partial \mathbf{g}}{\partial y_3} \frac{\partial y_3}{\partial x_2} \\ \frac{\partial \mathbf{g}}{\partial y_1} \frac{\partial y_1}{\partial x_3}+\frac{\partial \mathbf{g}}{\partial y_2} \frac{\partial y_2}{\partial x_3}+\frac{\partial \mathbf{g}}{\partial y_3} \frac{\partial y_3}{\partial x_3} \end{matrix} \right ]^T = \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial y_1}, \ \frac{\partial \mathbf{g}}{\partial y_2}, \ \frac{\partial \mathbf{g}}{\partial y_3} \end{matrix} \right ] \left [ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \frac{\partial y_1}{\partial x_3} \\ \frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \frac{\partial y_2}{\partial x_3} \\ \frac{\partial y_3}{\partial x_1} & \frac{\partial y_3}{\partial x_2} & \frac{\partial y_3}{\partial x_3} \end{matrix} \right ] $$

其中,

$$ \left [ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \frac{\partial y_1}{\partial x_3} \\ \frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \frac{\partial y_2}{\partial x_3} \\ \frac{\partial y_3}{\partial x_1} & \frac{\partial y_3}{\partial x_2} & \frac{\partial y_3}{\partial x_3} \end{matrix} \right ] $$

可以通过backward()得到,而由于$\mathbf{g}(\mathbf{y})$的未知性,

$$ \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial y_1}, \ \frac{\partial \mathbf{g}}{\partial y_2}, \ \frac{\partial \mathbf{g}}{\partial y_3} \end{matrix} \right ] $$

是无法求得的。在这种情况下,gradient参数的意义就是定义$\mathbf{g}(\mathbf{y})$,只有这样我们才可以顺利求出

$$ \frac{\partial z}{\partial \mathbf{x}} = \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial x_1}, \ \frac{\partial \mathbf{g}}{\partial x_2}, \ \frac{\partial \mathbf{g}}{\partial x_3} \end{matrix} \right ] $$

来上代码演示下:

首先要创建一个$\mathbf{y} = f(\mathbf{x})$的函数,其中$\mathbf{x,\ y}$都是向量:

In [1]: import torch

In [2]: x_0, x_1, x_2 = torch.tensor(1., requires_grad=True), torch.tensor(2., requires_grad=True), torch.tensor(3., re
   ...: quires_grad=True)

In [3]: x = torch.tensor([x_0, x_1, x_2])

In [4]: y = torch.randn(3)

In [5]: y[0], y[1], y[2] = x_0 * x_1 * x_2, x_0 + x_1 + x_2, x_0 + x_1 * x_2

In [6]: x, y
Out[6]: (tensor([1., 2., 3.]), tensor([6., 6., 7.], grad_fn=<CopySlices>))

上面定义的数学关系为:

$$ \mathbf{y} = \mathbf{f}(\mathbf{x}) = \left [ \begin{matrix} y_0(\mathbf{x}) \\ y_1(\mathbf{x}) \\ y_2(\mathbf{x}) \end{matrix} \right ]= \left [ \begin{matrix} x_0 x_1 x_2 \\ x_0 + x_1 + x_2 \\ x_0 + x_1 x_2 \end{matrix} \right ] $$

这种情况下,$\mathbf{x,\ y}$都是向量,显然不能直接进行求导得到$\mathbf{x}$的梯度,而通过设置参数gradient=torch.tensor([0.1, 0.2, 0.3],我们就指定了

$$ \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial y_1}, \ \frac{\partial \mathbf{g}}{\partial y_2}, \ \frac{\partial \mathbf{g}}{\partial y_3} \end{matrix} \right ] $$

就可以顺利求出:

$$ \begin{equation*} \begin{split} \frac{\partial z}{\partial \mathbf{x}} = \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial x_1}, \ \frac{\partial \mathbf{g}}{\partial x_2}, \ \frac{\partial \mathbf{g}}{\partial x_3} \end{matrix} \right ]&= \left [ \begin{matrix} \frac{\partial \mathbf{g}}{\partial y_1}, \ \frac{\partial \mathbf{g}}{\partial y_2}, \ \frac{\partial \mathbf{g}}{\partial y_3} \end{matrix} \right ] \left [ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \frac{\partial y_1}{\partial x_3} \\ \frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \frac{\partial y_2}{\partial x_3} \\ \frac{\partial y_3}{\partial x_1} & \frac{\partial y_3}{\partial x_2} & \frac{\partial y_3}{\partial x_3} \end{matrix} \right ]\\ &= \left [ \begin{matrix} 0.1, \ 0.2, \ 0.3 \end{matrix} \right ] \left [ \begin{matrix} x_1x_2 & x_0x_2 & x_0x_1 \\ 1 & 1 & 1 \\ 1 & x_2 & x_1 \end{matrix} \right ]\\ &= \left [ \begin{matrix} 0.1, \ 0.2, \ 0.3 \end{matrix} \right ] \left [ \begin{matrix} 6 & 3 & 2 \\ 1 & 1 & 1 \\ 1 & 3 & 2 \end{matrix} \right ]\\ &= \left [ \begin{matrix} 1.1, \ 1.4, \ 1.0 \end{matrix} \right ] \end{split} \end{equation*} $$

由此即可求出了$\mathbf{x}$的梯度

In [7]: y.backward(torch.tensor([0.1, 0.2, 0.3])
   ...: )

In [8]: x_0.grad, x_1.grad, x_2.grad
Out[8]: (tensor(1.1000), tensor(1.4000), tensor(1.))

这个时候你就会恍然大悟,为什么上面要对一个向量使用y = x.sum()来求导,其本质也是定义了一个:

$$ y=\mathbf{g}(\mathbf{x})=x_1+x_2+x_3 $$

backward方法中gradient参数的意义

广播机制

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

In [1]: import torch

In [2]:  x, y = torch.arange(1, 3).view(1, 2), torch.arange(1, 4).view(3, 1)

In [3]: x, y
Out[3]:
(tensor([[1, 2]]),
 tensor([[1],
         [2],
         [3]]))

In [4]: x + y
Out[4]:
tensor([[2, 3],
        [3, 4],
        [4, 5]])

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

运算的内存开销

索引操作是不会开辟新内存的,而像y = x + y这样的运算是会新开内存的,然后将y指向新内存。为了演示这一点,我们可以使用Python自带的id函数:如果两个实例的ID一致,那么它们所对应的内存地址相同;反之则不同。

In [1]: import torch

In [2]: x, y, z = torch.ones(1, 3), torch.arange(1, 4).view(1, 3), torch.arange(5, 8).view(1, 3)

In [3]: y_old_id, z_old_id = id(y), id(z)

In [4]: y, z[:] = y+x, z+x

In [5]: y, z
Out[5]: (tensor([[2., 3., 4.]]), tensor([[6, 7, 8]]))

In [6]: id(y) == y_old_id  # 如果
Out[6]: False

In [7]: id(z) == z_old_id
Out[7]: True

我们还可以使用运算符全名函数中的out参数或者自加运算符+=(也即add_())达到上述效果,例如torch.add(x, z, out=z)z += x(z.add_(x))。

Tensor和NumPy相互转换

我们很容易用numpy()from_numpy()Tensor和NumPy中的数组相互转换。但是需要注意的一点是: 这两个函数所产生的的Tensor和NumPy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变!!!

还有一个常用的将NumPy中的array转换成Tensor的方法就是torch.tensor(), 需要注意的是,此方法总是会进行数据拷贝(就会消耗更多的时间和空间),所以返回的Tensor和原来的数据不再共享内存。

Tensor转NumPy

使用numpy()Tensor转换成NumPy数组:

In [1]: import torch

In [2]: import numpy as np

In [3]: x = torch.ones(5)

In [4]: y = x.numpy()

In [5]: x += 1

In [6]: x, y
Out[6]: (tensor([2., 2., 2., 2., 2.]), array([2., 2., 2., 2., 2.], dtype=float32))

NumPy数组转Tensor

使用from_numpy()将NumPy数组转换成Tensor:

In [1]: import numpy as np

In [2]: import torch

In [3]: x = np.ones(3)

In [4]: y = torch.from_numpy(x)

In [5]: x += 1  # 对Numpy数组进行+1操作

In [6]: x, y  # Tensor也受到了影响
Out[6]: (array([2., 2., 2.]), tensor([2., 2., 2.], dtype=torch.float64))

所有在CPU上的Tensor(除了CharTensor)都支持与NumPy数组相互转换。

此外上面提到还有一个常用的方法就是直接用torch.tensor()将NumPy数组转换成Tensor,需要注意的是该方法总是会进行数据拷贝,返回的Tensor和原来的数据不再共享内存。

In [7]: x = np.ones(3)

In [8]: y = torch.tensor(x)

In [9]: x += 1

In [10]: x, y
Out[10]: (array([2., 2., 2.]), tensor([1., 1., 1.], dtype=torch.float64))

Tensor on GPU

用方法to()可以将Tensor在CPU和GPU(需要硬件支持)之间相互移动。

In [1]: import torch

In [2]: x = torch.ones(3)

In [3]: # 以下代码只有在PyTorch GPU版本上才会执行
   ...: if torch.cuda.is_available():
   ...:     device = torch.device("cuda")          # GPU
   ...:     y = torch.ones_like(x, device=device)  # 直接创建一个在GPU上的Tensor
   ...:     x = x.to(device)                       # 等价于 .to("cuda")
   ...:     z = x + y
   ...:     print(z)
   ...:     print(z.to("cpu", torch.double))       # to()还可以同时更改数据类型
   ...:
tensor([2., 2., 2.], device='cuda:0')
tensor([2., 2., 2.], dtype=torch.float64)