「深度学习」PyTorch笔记-01-基础知识
引
互联网万物基于ChatGPT,学习深度学习之前,先来请教一下ChatGPT如何进行学习。
问:如何从零开始学习深度学习?
Answer / 回答
如果你想从零开始学习深度学习,可以遵循以下步骤:
- 学习数学基础:深度学习需要掌握线性代数、概率论及微积分等数学基础,所以需要学好这些数学基础。
- 学习编程基础:深度学习需要使用编程语言进行实现,推荐学习Python。
- 学习深度学习理论:深度学习理论包括神经网络结构、激活函数、损失函数、优化算法等,需要掌握这些基本概念。
- 学习深度学习工具:深度学习需要使用一些工具,如TensorFlow、PyTorch等,需要学会如何使用这些工具。
- 实践项目:通过实践项目,可以巩固对深度学习理论和工具的掌握,同时提高实际应用能力。推荐先从简单的项目开始,如手写数字识别、猫狗分类等。
- 参与社区: 加入深度学习社区,如GitHub、Stack Overflow等,与其他深度学习爱好者交流,分享经验,提高自己的学习效率。
- 持续学习和更新:深度学习领域变化非常迅速,需要持续学习和更新自己的知识。可以通过阅读学术论文、参加学术会议和培训课程,保持对新技术和新方法的了解和掌握。
问:深度学习理论包括哪些内容,如何进行学习?
Answer / 回答
深度学习理论包括以下内容:
- 基本的神经网络结构和算法,如多层感知机、卷积神经网络、循环神经网络等。
- 梯度下降和反向传播算法。
- 激活函数的选择。
- 正则化技术,如L1/L2正则化、Dropout等。
- 深度学习模型的训练策略,如学习率调整、批量归一化等。
- 深度学习模型的优化算法,如Adam、Adagrad等。
- 深度学习模型的评估指标,如准确率、精确率、召回率等。
进行深度学习理论学习的建议如下:
- 学习基本的微积分、矩阵与线性代数等数学知识。
- 学习编程语言,如Python和C++。
- 学习使用深度学习工具包,如TensorFlow和PyTorch。
- 学习经典的深度学习模型以及相关的算法。
- 参考相关的学术论文和书籍进行深度学习理论学习。
- 进行实践并实现一些深度学习项目,加强理论知识的实际应用。
问:python中关于深度学习的包有哪些?如何进行学习?
Answer / 回答
Python中关于深度学习的包有很多,其中比较常用的包有:TensorFlow、Keras、PyTorch、Theano、MXNet、Caffe、Chainer、Torch、Lasagne、DeepLearning4J。其中,TensorFlow和PyTorch被广泛认为是目前最流行且最强大的深度学习包。如果想学习深度学习,可以按照以下步骤:
- 学习Python编程语言基础,掌握基本的数据类型、控制语句、函数等。
- 学习机器学习的基础知识,包括监督学习、无监督学习、回归、分类、聚类等。
- 学习深度学习的基础知识,包括神经网络、卷积神经网络、循环神经网络、自编码器、深度生成模型等。
- 选择一种深度学习框架,比如TensorFlow或PyTorch。
- 根据框架提供的教程和示例代码进行学习和实践,不断尝试和实验。
- 提升自己的数学能力,包括线性代数、微积分和概率论等。
- 参考相关论文和博客深入了解各种深度学习模型和应用场景,进一步提高自己的水平。
环境的配置
Python安装
CUDA安装
官网:https://developer.nvidia.com/cuda-toolkit,选择合适的版本然后Download即可。
[photos]
[/photos]
安装完成之后,去安装目录查看,如果存在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窗口回车即可下载。
[photos]
[/photos]
安装完成之后,验证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
文件进行测试:
[photos]
[/photos]
测试全程,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的未初始化的张量时,会提出警告,如下图所示:
解决方法:
- 卸载现有Numpy版本,
pip uninstall numpy
; - 安装匹配的版本,
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 ] $$
常用求导公式
[photos]
[/photos]
为什么要使用
使用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]])
由于x
和y
分别是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)