痛定思痛,好好做人,从头过一遍PyTorch框架(一)(1.深度学习简介、2.预备知识)
现在是2024年2月24日,13:59,从研一就开始断断续续说要过一遍框架,到现在博一下学期,还一直拖着呢,拖延症太可怕啦,决定好好做人,不拖了,就从现在开始,好好过一遍,呜呜呜呜呜呜呜呜,(罪该万死)。
看的教程是:《动手学深度学习》(PyTorch版),是把李沐老师的《动手学深度学习》原书中的MXNet实现改为PyTorch实现。
开始吧。
1.深度学习简介
1.通俗来说,机器学习是一门讨论各式各样的适用于不同问题的函数形式,以及如何使用数据来有效地获取函数参数具体值的学科。深度学习是指机器学习中的一类函数,它们的形式通常为多层神经网络。近年来,仰仗着大数据集和强大的硬件,深度学习已逐渐成为处理图像、文本语料和声音信号等复杂高维度数据的主要方法。
2.另一个对深度学习有重大影响的领域是神经科学与心理学。既然人类显然能够展现出智能,那么对于解释并逆向工程人类智能机理的探究也在情理之中。最早的算法之一是由唐纳德·赫布(1904--1985)正式提出的。在他开创性的著作《行为的组织》中,他提出神经是通过正向强化来学习的,即赫布理论 [2]。赫布理论是感知机学习算法的原型,并成为支撑今日深度学习的随机梯度下降算法的基石:强化合意的行为、惩罚不合意的行为,最终获得优良的神经网络参数。
3.随着时间发展,神经网络的生物学解释被稀释,但仍保留了这个名字。时至今日,绝大多数神经网络都包含以下的核心原则。
- 交替使用线性处理单元与非线性处理单元,它们经常被称为“层”。
- 使用链式法则(即反向传播)来更新网络的参数。
4.Q:
“很显然,存储容量没能跟上数据量增长的步伐。与此同时,计算力的增长又盖过了数据量的增长。这样的趋势使得统计模型可以在优化参数上投入更多的计算力,但同时需要提高存储的利用效率,例如使用非线性处理单元。”为什么说非线性处理单元有这样的优势
A:
ChatGPT:
提到非线性处理单元(比如神经网络中的非线性激活函数),它们能够带来一系列优势,特别是在处理复杂数据和优化存储效率方面。以下是一些关键点:
- 增加模型的表达能力:非线性处理单元可以帮助统计模型捕捉数据中的复杂模式和关系。线性模型仅能处理数据的线性关系,而非线性模型通过引入非线性处理单元,能够学习到更加复杂的数据结构,从而在相同或更少的参数下实现更高的准确度。
- 提高存储利用效率:通过使用非线性处理单元,模型能够以更紧凑的形式表示复杂函数。这意味着为了达到相同的性能,非线性模型可能需要更少的参数和存储空间。例如,深度学习模型通过堆叠非线性层来学习高级特征,而不是简单地增加模型的大小,这样不仅提升了模型的学习能力,也优化了参数的使用和存储。
- 使模型能够处理非线性问题:现实世界中的许多问题都是非线性的,使用非线性处理单元使得模型能够直接应对这些问题。比如在图像识别、语言处理等领域,非线性模型展现出了比线性模型更好的性能。
- 提高计算效率:虽然非线性计算在某些情况下可能比线性计算更为复杂,但通过智能的算法设计和硬件优化(如GPU加速),这种增加的复杂度可以得到有效管理。此外,由于非线性模型能够使用更少的参数解决问题,因此可以减少计算资源的消耗,从而在给定的计算能力下实现更高效的数据处理。
综上所述,非线性处理单元之所以在优化参数和提高存储利用效率方面具有优势,是因为它们提高了模型的表达能力,使得模型能以更紧凑、效率更高的方式学习和表示复杂数据模式。这对于处理大规模数据集、在计算资源受限的情况下优化性能尤为重要。
5.在描述深度学习的特点之前,我们先回顾并概括一下机器学习和深度学习的关系。机器学习研究如何使计算机系统利用经验改善性能。它是人工智能领域的分支,也是实现人工智能的一种手段。在机器学习的众多研究方向中,表征学习关注如何自动找出表示数据的合适方式,以便更好地将输入变换为正确的输出,而本书要重点探讨的深度学习是具有多级表示的表征学习方法。在每一级(从原始数据开始),深度学习通过简单的函数将该级的表示变换为更高级的表示。因此,深度学习模型也可以看作是由许多简单函数复合而成的函数。当这些复合的函数足够多时,深度学习模型就可以表达非常复杂的变换。
6.深度学习可以逐级表示越来越抽象的概念或模式。以图像为例,它的输入是一堆原始像素值。深度学习模型中,图像可以逐级表示为特定位置和角度的边缘、由边缘组合得出的花纹、由多种花纹进一步汇合得到的特定部位的模式等。最终,模型能够较容易根据更高级的表示完成给定的任务,如识别图像中的物体。值得一提的是,作为表征学习的一种,深度学习将自动找出每一级表示数据的合适方式。
7.因此,深度学习的一个外在特点是端到端的训练。也就是说,并不是将单独调试的部分拼凑起来组成一个系统,而是将整个系统组建好之后一起训练。比如说,计算机视觉科学家之前曾一度将特征抽取与机器学习模型的构建分开处理,像是Canny边缘探测 [20] 和SIFT特征提取 [21] 曾占据统治性地位达10年以上,但这也就是人类能找到的最好方法了。当深度学习进入这个领域后,这些特征提取方法就被性能更强的自动优化的逐级过滤器替代了。
8.除端到端的训练以外,我们也正在经历从含参数统计模型转向完全无参数的模型。当数据非常稀缺时,我们需要通过简化对现实的假设来得到实用的模型。当数据充足时,我们就可以用能更好地拟合现实的无参数模型来替代这些含参数模型。这也使我们可以得到更精确的模型,尽管需要牺牲一些可解释性。
9.相对其它经典的机器学习方法而言,深度学习的不同在于:对非最优解的包容、对非凸非线性优化的使用,以及勇于尝试没有被证明过的方法。这种在处理统计问题上的新经验主义吸引了大量人才的涌入,使得大量实际问题有了更好的解决方案。尽管大部分情况下需要为深度学习修改甚至重新发明已经存在数十年的工具,但是这绝对是一件非常有意义并令人兴奋的事。
小结
- 机器学习研究如何使计算机系统利用经验改善性能。它是人工智能领域的分支,也是实现人工智能的一种手段。
- 作为机器学习的一类,表征学习关注如何自动找出表示数据的合适方式。
- 深度学习是具有多级表示的表征学习方法。它可以逐级表示越来越抽象的概念或模式。
- 深度学习所基于的神经网络模型和用数据编程的核心思想实际上已经被研究了数百年。
- 深度学习已经逐渐演变成一个工程师和科学家皆可使用的普适工具。
(15:03,结束第一节,休息一下~)
2. 预备知识
(15:09,继续)
2.2 数据操作
1.在PyTorch中,torch.Tensor
是存储和变换数据的主要工具。如果你之前用过NumPy,你会发现Tensor
和NumPy的多维数组非常类似。然而,Tensor
提供GPU计算和自动求梯度等更多功能,这些使Tensor
更加适合深度学习。
"tensor"这个单词一般可译作“张量”,张量可以看作是一个多维数组。标量可以看作是0维张量,向量可以看作1维张量,矩阵可以看作是二维张量。
2.2.1 创建Tensor
1 ### 2.2.1 创建Tensor 2 import torch 3 4 # 创建一个5x3的未初始化的Tensor: 5 x = torch.empty(5,3) 6 # print(x) 7 8 # 创建一个5x3的随机初始化的Tensor: 9 x = torch.rand(5,3) 10 # print(x) 11 12 # 创建一个5x3的long型全0的Tensor: 13 x = torch.zeros(5,3, dtype = torch.long) 14 # print(x) 15 16 # 还可以直接根据数据创建: 17 x = torch.tensor([5,5,3]) 18 # print(x) 19 20 # 还可以通过现有的Tensor来创建,此方法会默认重用输入Tensor的一些属性,例如数据类型,除非自定义数据类型 21 x = x.new_ones(5,3, dtype = torch.float64) # 返回的tensor默认具有相同的torch.dtype和torch.device 22 # print(x) 23 24 x = torch.randn_like(x, dtype = torch.float) # 指定新的数据类型 25 # print(x) 26 27 # 可以通过shape或者size()来获取Tensor的形状: 28 print(x.size()) 29 print(x.shape) 30 # 注意:返回的torch.Size其实就是一个tuple, 支持所有tuple的操作。
还有很多函数可以创建Tensor
,去翻翻官方API就知道了,下表给了一些常用的作参考。
函数 | 功能 |
---|---|
Tensor(*sizes) | 基础构造函数 |
tensor(data,) | 类似np.array的构造函数 |
ones(*sizes) | 全1Tensor |
zeros(*sizes) | 全0Tensor |
eye(*sizes) | 对角线为1,其他为0 |
arange(s,e,step) | 从s到e,步长为step |
linspace(s,e,steps) | 从s到e,均匀切分成steps份 |
rand/randn(*sizes) | 均匀/标准分布 |
normal(mean,std)/uniform(from,to) | 正态分布/均匀分布 |
randperm(m) | 随机排列 |
这些创建方法都可以在创建的时候指定数据类型dtype和存放device(cpu/gpu)。
2.2.2 操作
1 ### 2.2.2 操作 2 # 介绍Tensor的各种操作 3 4 ## 算数操作 5 # 在PyTorch中,同一种操作可能有很多中形式,下面用加法作为例子。 6 # 加法形式一 7 y = torch.rand(5,3) 8 # print(x + y) 9 10 # 加法形式二 11 # print(torch.add(x,y)) 12 # 还可指定输出: 13 result = torch.empty(5,3) 14 torch.add(x,y, out = result) 15 # print(result) 16 17 # 加法形式三、inplace 原地 18 # adds x to y 19 y.add_(x) 20 # print(y) 21 # 注:PyTorch操作inplace版本都有后缀_, 例如x.copy_(y), x.t_() 22 23 ## 索引 24 # 还可以使用类似Numpy的索引操作来访问Tensor的一部分,需要注意的是:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改。 25 y = x[0,:] # 选择x的第一行,所有列,第一行的一个完整切片 26 print(x) 27 y += 1 28 print(y) 29 print(x[0,:]) # 源tensor也被修改了
除了常用的索引选择数据之外,PyTorch还提供了一些高级的选择函数:
函数 | 功能 |
---|---|
index_select(input, dim, index) | 在指定维度dim上选取,比如选取某些行、某些列 |
masked_select(input, mask) | 例子如上,a[a>0],使用ByteTensor进行选取 |
nonzero(input) | 非0元素的下标 |
gather(input, dim, index) | 根据index,在dim维度上选取数据,输出的size与index一样 |
这里不详细介绍,用到了再查官方文档。
1 ## 改变形状 2 # 用view()来改变Tensor的形状: 3 y = x.view(15) 4 z = x.view(-1,5) # -1所指的维度可以根据其他维度的值推出来 5 # print(x) 6 # print(y) 7 # print(z) 8 # print(x.size(), y.size(), z.size()) 9 # 输出:看形状 10 # tensor([[ 1.3107, 0.0693, 3.4263], 11 # [-0.7560, 1.2920, -0.5012], 12 # [ 1.4238, -1.1785, 0.7892], 13 # [ 0.3062, -0.9667, 1.1572], 14 # [ 0.3245, 1.3990, 0.0399]]) 15 # tensor([ 1.3107, 0.0693, 3.4263, -0.7560, 1.2920, -0.5012, 1.4238, -1.1785, 16 # 0.7892, 0.3062, -0.9667, 1.1572, 0.3245, 1.3990, 0.0399]) 17 # tensor([[ 1.3107, 0.0693, 3.4263, -0.7560, 1.2920], 18 # [-0.5012, 1.4238, -1.1785, 0.7892, 0.3062], 19 # [-0.9667, 1.1572, 0.3245, 1.3990, 0.0399]]) 20 # torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5]) 21 22 # 注意view()返回的新Tensor与源Tensor虽然可能有不同的size,但是是共享data的,也即更改其中的一个,另外一个也会跟着改变。(顾名思义,view仅仅是改变了对这个张量的观察角度,内部数据并未改变) 23 # print(x) 24 x += 1 25 # print(x) 26 # print(y) # 也加了1 27 28 # 所以如果我们想返回一个真正新的副本(即不共享data内存)该怎么办呢?Pytorch还提供了一个reshape()可以改变形状,但是此函数并不能保证返回的是其拷贝,所以不推荐使用。推荐先用clone创造一个副本然后再使用view。 29 x_cp = x.clone().view(15) 30 # print(x) 31 x -= 1 32 # print(x) 33 # print(x_cp) 34 # 输出 35 # tensor([[ 0.2564, 3.6559, 3.9383], 36 # [ 0.3162, -0.7308, 0.6828], 37 # [-0.3874, 2.2831, 0.4981], 38 # [ 1.2237, 0.5300, 0.5792], 39 # [ 1.5834, 1.8099, -0.2272]]) 40 # tensor([[-0.7436, 2.6559, 2.9383], 41 # [-0.6838, -1.7308, -0.3172], 42 # [-1.3874, 1.2831, -0.5019], 43 # [ 0.2237, -0.4700, -0.4208], 44 # [ 0.5834, 0.8099, -1.2272]]) 45 # tensor([ 0.2564, 3.6559, 3.9383, 0.3162, -0.7308, 0.6828, -0.3874, 2.2831, 46 # 0.4981, 1.2237, 0.5300, 0.5792, 1.5834, 1.8099, -0.2272]) 47 # 使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor。 48 49 # 另外一个常用的函数就是item(),它可以将一个标量Tensor转换成一个Python number: 50 x= torch.randn(1) 51 print(x) 52 print(x.item()) 53 # 现在16:40,累了 54 55 ## 线性代数
另外,PyTorch还支持一些线性函数,这里提一下,免得用起来的时候自己造轮子,具体用法参考官方文档。如下表所示:
函数 | 功能 |
---|---|
trace | 对角线元素之和(矩阵的迹) |
diag | 对角线元素 |
triu/tril | 矩阵的上三角/下三角,可指定偏移量 |
mm/bmm | 矩阵乘法,batch的矩阵乘法 |
addmm/addbmm/addmv/addr/baddbmm.. | 矩阵运算 |
t | 转置 |
dot/cross | 内积/外积 |
inverse | 求逆矩阵 |
svd | 奇异值分解 |
PyTorch中的Tensor
支持超过一百种操作,包括转置、索引、切片、数学运算、线性代数、随机数等等,可参考官方文档。
2.2.3 广播机制
前面我们看到如何对两个形状相同的Tensor
做按元素运算。当对两个形状不同的Tensor
按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor
形状相同后再按元素运算。例如:
1 ### 2.2.3 广播机制 2 # 前面我们看到如何对两个形状相同的Tensor做按元素运算。当对两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor形状相同后再按元素运算。例如: 3 x = torch.arange(1,3).view(1,2) 4 print(x) 5 y = torch.arange(1,4).view(3,1) 6 print(y) 7 print(x + y) 8 # 输出 9 # tensor([[1, 2]]) 10 # tensor([[1], 11 # [2], 12 # [3]]) 13 # tensor([[2, 3], 14 # [3, 4], 15 # [4, 5]])
由于x
和y
分别是1行2列和3行1列的矩阵,如果要计算x + y
,那么x
中第一行的2个元素被广播(复制)到了第二行和第三行,而y
中第一列的3个元素被广播(复制)到了第二列。如此,就可以对2个3行2列的矩阵按元素相加。
(唉,一把年纪了,还要重新想一下之前矩阵分析学的东西)
2.2.4 运算的内存开销
1 ### 2.2.4 运算的内存开销 2 # 前面说了,索引操作是不会开辟新内存的,而像y = x + y这样的运算是会新开内存的,然后将y指向新内存。为了演示这一点,我们可以使用Python自带的id函数:如果两个实例的ID一致,那么它们所对应的内存地址相同;反之则不同。 3 x = torch.tensor([1,2]) 4 y = torch.tensor([3,4]) 5 id_before = id(y) 6 y = y + x 7 # print(id_before) 8 # print(id(y)) 9 # print(id(y) == id_before) # False 10 11 # 如果想指定结果到原来的y的内存,我们可以使用前面介绍的索引来进行替换操作。在下面的例子中,我们把x + y的结果通过[:]写进y对应的内存中。 12 x = torch.tensor([1,2]) 13 y = torch.tensor([3,4]) 14 id_before = id(y) 15 y[:] = y + x 16 # print(id(y) == id_before) # True 17 # print(y) 18 19 # 我们还可以使用运算符全名函数中的out参数或者自加运算符+=(也即add_())达到上述效果,例如torch.add(x,y, out = y)和y += x(y.add_(x)) 20 x = torch.tensor([1,2]) 21 y = torch.tensor([3,4]) 22 id_before = id(y) 23 torch.add(x,y, out = y) # y += x, y.add_(x) 24 print(id(y) == id_before) # True
注:虽然
view
返回的Tensor
与源Tensor
是共享data
的,但是依然是一个新的Tensor
(因为Tensor
除了包含data
外还有一些其他属性),二者id(内存地址)并不一致。
2.2.5 Tensor
和NumPy相互转换
我们很容易用numpy()
和from_numpy()
将Tensor
和NumPy中的数组相互转换。但是需要注意的一点是: 这两个函数所产生的的Tensor
和NumPy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变!!!
还有一个常用的将NumPy中的array转换成
Tensor
的方法就是torch.tensor()
, 需要注意的是,此方法总是会进行数据拷贝(就会消耗更多的时间和空间),所以返回的Tensor
和原来的数据不再共享内存。
1 ### 2.2.5 Tensor和NumPy相互转换 2 ## Tensor转NumPy 3 # 使用numpy()将Tensor转换成NumPy数组: 4 a = torch.ones(5) 5 b = a.numpy() 6 # print(a,b) 7 8 a += 1 9 # print(a,b) 10 b += 1 11 # print(a,b) 12 13 ## NumPy数组转Tensor 14 # 使用from_numpy()将NumPy数组转换成Tensor: 15 import numpy as np 16 a = np.ones(5) 17 b = torch.from_numpy(a) 18 # print(a,b) 19 20 a += 1 21 # print(a,b) 22 b += 1 23 # print(a,b) 24 25 # 所有在CPU上的Tensor(除了CharTensor)都支持与NumPy数组相互转换。 26 # 此外上面提到还有一个常用的方法就是直接用torch.tensor()将NumPy数组转换成Tensor,需要注意的是该方法总是会进行数据拷贝,返回的Tensor和原来的数据不再共享内存。 27 c = torch.tensor(a) 28 a += 1 29 print(a,c)
2.2.6 Tensor
on GPU
1 ### 2.2.6 Tensor on GPU 2 # 用方法to()可以将Tensor在CPU和GPU(需要硬件支持)之间相互移动。 3 # 以下代码只有在PyTorch GPU版本上才会执行 4 if torch.cuda.is_available(): 5 device = torch.device("cuda") # GPU 6 y = torch.ones_like(x, device = device) # 直接创建一个在GPU上的Tensor 7 x = x.to(device) # 等价于 .to("cuda") 8 z = x + y 9 print(z) 10 print(z.device) 11 print(z.to("cpu", torch.double)) # to()还可以同时更改数据类型 12 print(z.device) # 有问题,我的电脑切换不到cpu上 13 z = z.to("cpu", torch.double) # 这个可以切换成功,原因:data.to(device)后需要赋值给另一变量,可以是data本身,否则无法真正实现迁移。 14 print(z.device)
现在是18:02,好饿,溜了,今天元宵节,打算去快乐一会了.
现在是20:51,没买到汤圆,刚刚改了一下之前写的代码,然后现在贴一下这篇里用到的所有代码的汇总:
结束,准备开启下一章内容。(21:26,突然发现还有2.3节,哈哈哈哈哈哈)
(2024年2月26日,21:04继续)
2.3 自动求梯度
1.在深度学习中,我们经常需要对函数求梯度(gradient)。PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。本节将介绍如何使用autograd包来进行自动求梯度的有关操作。
2.3.1 概念
上一节介绍的Tensor
是这个包的核心类,如果将其属性.requires_grad
设置为True
,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用.backward()
来完成所有梯度计算。此Tensor
的梯度将累积到.grad
属性中。
注意在
y.backward()
时,如果y
是标量,则不需要为backward()
传入任何参数;否则,需要传入一个与y
同形的Tensor
。解释见 2.3.2 节。
如果不想要被继续追踪,可以调用.detach()
将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,这样梯度就传不过去了。此外,还可以用with torch.no_grad()
将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True
)的梯度。
Function
是另外一个很重要的类。Tensor
和Function
互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor
都有一个.grad_fn
属性,该属性即创建该Tensor
的Function
, 就是说该Tensor
是不是通过某些运算得到的,若是,则grad_fn
返回一个与这些运算相关的对象,否则是None。
下面通过一些例子来理解这些概念。
2.3.2 Tensor