目录
- 一、Pytorch简介和安装
- 1. 1 Pytorch的发展
- 1. 2 Pytorch的优点
- 1. 动态计算图
- 2. 易用性
- 3. 易于调试
- 4. 强大的社区支持
- 5. 广泛的预训练模型
- 6. 高效的GPU利用
- 1.3 Pytorch的主要使用场景
- 1. 计算机视觉
- 2. 自然语言处理
- 3. 生成对抗网络
- 4. 强化学习
- 5. 时序数据分析
- 1.4 Pytorch的安装
- 二、张量
- 2.1 张量简介
- 2.2 张量的创建
- 2.2.1 直接创建
- (1)通过torch.tensor()创建
- (2) 通过torch.from_numpy(ndarray)创建
- 2.2.2 根据数值创建
- (1)torch.zeros()和torch.ones_like()
- (2) torch.full()
- (4)torch.arange()
- (5)torch.linspace()
- 2.2.3 依据概率分布创建张量
- (1)torch.normal()
- (2)torch.randn()
- (3)其他方法
- 三、张量的操作与线性回归
- 3.1张量的操作
- 3.1.1张量拼接与拆分
- (1)torch.cat()
- (2)torch.stack()
- (3)torch.chunk()
- 3.1.2 张量索引
- (1)torch.index_select()
- (2)torch.masked_select()
- 3.1.3张量变换
- (1)torch.reshape()
- (2)torch.transpose()和torch.t()
- (3)torch.squeeze()和torch.unsqueeze()
- 3.1.4张量数学运算
- torch.add()
- 3.2线性回归(Linear Regression)
- 四、autograd与回归算法
- 4.1 autograd——自动求导系统
- 4.1.1 torch.autograd.backward()
- 4.1.2 torch.autograd.grad
- 4.1.2 前向传播、反向传播和计算图
- 4.1.3.链式法则和雅克比矩阵
- (1)神经网络中的链式法则
- (2)雅克比矩阵
- 4.1.4 动态计算图和Autograd原理
- 4.2 softmax回归模型
- 4.2.1 分类问题
- 4.2.2 模型定义
- 4.2.3 softmax运算
- 4.2.4 单样本分类的矢量计算表达式
- 4.2.5 小批量样本分类的矢量计算表达式
- 4.2.6 交叉熵损失函数
- 4.2.7模型预测及评价
- 4.2.8 模型实现
- (1)获取和读取数据集
- (2)定义相关函数
- (3)训练模型
- (4)预测
- 参考资料
一、Pytorch简介和安装
1. 1 Pytorch的发展
PyTorch是一个由Facebook的人工智能研究团队开发的开源深度学习框架。在2016年发布后,PyTorch很快就因其易用性、灵活性和强大的功能而在科研社区中广受欢迎。
PyTorch的发展历程是一部充满创新和挑战的历史,它从一个科研项目发展成为了全球最流行的深度学习框架之一。在未来,我们有理由相信,PyTorch将会在深度学习领域继续发挥重要的作用。
1. 2 Pytorch的优点
PyTorch不仅是最受欢迎的深度学习框架之一,而且也是最强大的深度学习框架之一。它有许多独特的优点,使其在学术界和工业界都受到广泛的关注和使用。接下来我们就来详细地探讨一下PyTorch的优点。
1. 动态计算图
PyTorch最突出的优点之一就是它使用了动态计算图(Dynamic Computation Graphs,DCGs),与TensorFlow和其他框架使用的静态计算图不同。动态计算图允许你在运行时更改图的行为。这使得PyTorch非常灵活,在处理不确定性或复杂性时具有优势,因此非常适合研究和原型设计。
2. 易用性
PyTorch被设计成易于理解和使用。其API设计的直观性使得学习和使用PyTorch成为一件非常愉快的事情。此外,由于PyTorch与Python的深度集成,它在Python程序员中非常流行。
3. 易于调试
由于PyTorch的动态性和Python性质,调试PyTorch程序变得相当直接。你可以使用Python的标准调试工具,如PDB或PyCharm,直接查看每个操作的结果和中间变量的状态。
4. 强大的社区支持
PyTorch的社区非常活跃和支持。官方论坛、GitHub、Stack Overflow等平台上有大量的PyTorch用户和开发者,你可以从中找到大量的资源和帮助。
5. 广泛的预训练模型
PyTorch提供了大量的预训练模型,包括但不限于ResNet,VGG,Inception,SqueezeNet,EfficientNet等等。这些预训练模型可以帮助你快速开始新的项目。
6. 高效的GPU利用
PyTorch可以非常高效地利用NVIDIA的CUDA库来进行GPU计算。同时,它还支持分布式计算,让你可以在多个GPU或服务器上训练模型。
综上所述,PyTorch因其易用性、灵活性、丰富的功能以及强大的社区支持,在深度学习领域中备受欢迎。
1.3 Pytorch的主要使用场景
PyTorch的强大功能和灵活性使其在许多深度学习应用场景中都能够发挥重要作用。以下是PyTorch在各种应用中的一些典型用例:
1. 计算机视觉
在计算机视觉方面,PyTorch提供了许多预训练模型(如ResNet,VGG,Inception等)和工具(如TorchVision),可以用于图像分类、物体检测、语义分割和图像生成等任务。这些预训练模型和工具大大简化了开发计算机视觉应用的过程。
2. 自然语言处理
在自然语言处理(NLP)领域,PyTorch的动态计算图特性使得其非常适合处理变长输入,这对于许多NLP任务来说是非常重要的。同时,PyTorch也提供了一系列的NLP工具和预训练模型(如Transformer,BERT等),可以帮助我们处理文本分类、情感分析、命名实体识别、机器翻译和问答系统等任务。
3. 生成对抗网络
生成对抗网络(GANs)是一种强大的深度学习模型,被广泛应用于图像生成、图像到图像的转换、样式迁移和数据增强等任务。PyTorch的灵活性使得其非常适合开发和训练GAN模型。
4. 强化学习
强化学习是一种学习方法,其中智能体通过与环境的交互来学习如何执行任务。PyTorch的动态计算图和易于使用的API使得其在实现强化学习算法时表现出极高的效率。
5. 时序数据分析
在处理时序数据的任务中,如语音识别、时间序列预测等,PyTorch的动态计算图为处理可变长度的序列数据提供了便利。同时,PyTorch提供了包括RNN、LSTM、GRU在内的各种循环神经网络模型。
总的来说,PyTorch凭借其强大的功能和极高的灵活性,在许多深度学习的应用场景中都能够发挥重要作用。无论你是在研究新的深度学习模型,还是在开发实际的深度学习应用,PyTorch都能够提供强大的支持。
1.4 Pytorch的安装
可以参考如下博客: Pytorch的安装
二、张量
2.1 张量简介
张量(Tensor),是Pytorch中最基础的概念。Pytorch中的Tensor类似于Numpy中的ndarrays结构,同时 Tensors 可以使用GPU进行计算。简单来说,Tensor实际上就是一个多维数组(multidimensional array)。而Tensor的目的是能够创造更高维度的矩阵、向量。
那么,便有一个问题:张量与矩阵、向量、标量的关系是怎么样的?
简而言之:
标量(scalar)是一个标量表示一个0维的数据,没有方向。
向量(vector)是一个一维的数组,有一个方向,数据沿一个方向排列存放。
矩阵(matrix)是一个二维数组,如灰度图像。有行和列两个维度,分别对应图像的高和宽。无法表示RGB图像。
张量(tensor)是一个多维数组,它是标量、向量、矩阵的高维扩展。如RGB图像,第一个维度为图像的高,第二个维度为图像的宽,第三个维度为色彩通道。张量为一个多维数组。
它们之间的关系可以这样描述:标量是0阶张量,向量是一维张量,矩阵维二维张量,如下图所示:
(2)Tensor与Variable
PyTorch的早期版本中,可以使用torch.autograd.Variable类进行创建支持梯度计算和跟踪的张量,torch.autograd.Variable包含以下5个属性:data、grad、grad_fn、requires_grad、is_leaf。但目前来看,在较新的PyTorch版本一般直接使用 torch.Tensor,其在torch.autograd.Variable的基础上,又增加了dtype、shape、device三个属性。
Variable是torch.autograd中的数据类型,主要用于封装Tensor,进行求导。
Variable的5个属性:
data:被包装的Tensor
grad:data的梯度
grad_fn:创建Tensor的Function,是自动求导的关键
requires_grad:指示是否需要梯度
is_leaf: 指示是否是叶子结点(张量)
其关系可以如下表示:
(3)Tensor
在 PyTorch 0.4.0 之后,Variable 并入了 Tensor。在之后版本的 Tensor 中,除了具有上面 Variable 的 5 个属性,还有另外 3 个属性。
dtype:张量的数据类型,如,torch.FloatTensor,torch.cuda.FloatTensor
shape:张量的形状,如(64,3,256,256)->(batch_size,channel,height,width),这里的3指的是RGB通道数。
device:张量所在的设备,GPU/CPU,是加速计算的关键。
关系可以如下图表示:
2.2 张量的创建
2.2.1 直接创建
(1)通过torch.tensor()创建
- data:数据,可以使list,numpy
- dtype:数据类型,默认与data的一致
- device:所在设备,cuda/cpu。device=‘cuda’
- requires_grad:是否需要梯度
- pin_memory: 是否存于锁页内存,通常设为false
测试代码
import torch
import numpy as np
# 通过torch.tensor创建张量
arr = np.ones((3, 3))
print("ndarray的数据类型:", arr.dtype)
t = torch.tensor(arr)
# 测试结果如下
print(t)
ndarray的数据类型: float64
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], device='cuda:0', dtype=torch.float64)
(2) 通过torch.from_numpy(ndarray)创建
注意事项:从torch.from_numpy创建的tensor与原ndarray共享内存,当修改其中一个的数据,另一个也将会被改动。
测试代码
arr = np.array([[1, 2, 3], [4, 5, 6]])
t = torch.from_numpy(arr)
print(arr, '\n',t)
arr[0, 0] = 0
print('*' * 10)
print(arr, '\n',t)
t[1, 1] = 100
print('*' * 10)
print(arr, '\n',t)
# 结果:
[[1 2 3]
[4 5 6]]
tensor([[1, 2, 3],
[4, 5, 6]], dtype=torch.int32)
**********
[[0 2 3]
[4 5 6]]
tensor([[0, 2, 3],
[4, 5, 6]], dtype=torch.int32)
**********
[[ 0 2 3]
[ 4 100 6]]
tensor([[ 0, 2, 3],
[ 4, 100, 6]], dtype=torch.int32)
2.2.2 根据数值创建
(1)torch.zeros()和torch.ones_like()
功能:依size创建全0张量;根据input的维度创建全0张量
- size:张量的形状,如(3,3)、(3,224,224)
- out:表示输出张量,就是再把这个张量赋值给别的一个张量,但是这两个张量时一样的,指的同一个内存地址
- layout: 内存中布局形式,有strided(默认),sparse_coo(稀疏张量设置)等,一般采用默认
- device:所在设备,gpu/cpu
- requires_grad:是否需要梯度
(2) torch.full()
功能:自定义数值张量
- size:张量的形状,如(3,3)、(3,224,224)
- ill_value:张量的值 i
(4)torch.arange()
功能:创建等差的一维张量
注意事项:数值区间为[start,end),因为区间是作弊又开的,所以取不到最后的值。
- start:数列起始值
- end:数列“结束值”
- step:步长,数列公差,默认为1
(5)torch.linspace()
功能:创建均分的1维张量
注意事项:数值区间为[start,end],左闭右闭,能取到最后的值,这与arrange()方法是不一样的
- start:数列起始值
- end:数列“结束值”
- step:步长,数列公差,默认为1
[步长计算:(end-start)/(steps - 1)],如[0,10]且步长为1,会生成一个长为11的张量。
2.2.3 依据概率分布创建张量
(1)torch.normal()
功能:生成正态分布(高斯分布)
- mean:均值
- std:标准差
四种模式:
mean为标量,std为标量
mean为标量,std为张量
mean为张量,std为标量
mean为张量,std为张量
注意事项:当mean和std均为标量时, 应设定size来规定张量的长度,分别各有两种取值,所以这里会有四种模式。
测试代码
# 第一种模式 - 均值是标量, 方差是标量 - 此时产生的是一个分布, 从这一个分部种抽样相应的个数,所以这个必须指定size,也就是抽取多少个数
t_normal = torch.normal(0, 1, size=(4,))
print(t_normal) # 来自同一个分布
# 第二种模式 - 均值是标量, 方差是张量 - 此时会根据方差的形状大小,产生同样多个分布,每一个分布的均值都是那个标量
std = torch.arange(1, 5, dtype=torch.float)
print(std.dtype)
t_normal2 = torch.normal(1, std)
print(t_normal2) # 也产生来四个数,但是这四个数分别来自四个不同的正态分布,这些分布均值相等
# 第三种模式 - 均值是张量,方差是标量 - 此时也会根据均值的形状大小,产生同样多个方差相同的分布,从这几个分布中分别取一个值作为结果
mean = torch.arange(1, 5, dtype=torch.float)
t_normal3 = torch.normal(mean, 1)
print(t_normal3) # 来自不同的分布,但分布里面方差相等
# 第四种模式 - 均值是张量, 方差是张量 - 此时需要均值的个数和方差的个数一样多,分别产生这么多个正太分布,从这里面抽取一个值
mean = torch.arange(1, 5, dtype=torch.float)
std = torch.arange(1, 5, dtype=torch.float)
t_normal4 = torch.normal(mean, std)
print(t_normal4) # 来自不同的分布,各自有自己的均值和方差
(2)torch.randn()
功能:生成标准正态分布
(3)其他方法
torch.randperm():
torch.bernoulli():
三、张量的操作与线性回归
3.1张量的操作
3.1.1张量拼接与拆分
(1)torch.cat()
功能:将张量按维度dim进行拼接
- tensors:张量序列
- dim:要拼接的维度:dim=0时表示按行拼接,dim=1时表示按列拼接
注意事项:.cat是在原来的基础上根据行和列,进行拼接, 浮点数类型拼接才可以,long类型拼接会报错
测试代码
# 张量的拼接
t = torch.ones((2, 3))
print(t)
t_0 = torch.cat([t, t], dim=0) # 行拼接
t_1 = torch.cat([t, t], dim=1) # 列拼接
print(t_0, t_0.shape)
print(t_1, t_1.shape)
(2)torch.stack()
功能:在新创建的维度dim上进行拼接
- tensors:张量序列
- dim:要拼接的维度
注意事项:.stack()会拓展维度,但是.cat()不会。简单地说.stack()新加了一个维度Z轴。
测试代码
# 张量的拼接(stack)
t = torch.ones((2, 3))
t_stack = torch.stack([t,t,t], dim=0)
print(t_stack)
print(t_stack.shape)
t_stack1 = torch.stack([t, t, t], dim=1)
print(t_stack1)
print(t_stack1.shape)
(3)torch.chunk()
功能:将张量按维度dim进行平均切分
返回值:张量列表
注意事项:若不能整除,最后一份张量小于其他张量
- input:要切分的张量
- chunks:要切分的份数
- dim:要切分的维度(在哪个维度进行切分)
测试代码
a = torch.ones((2, 7)) # 7
list_of_tensors = torch.chunk(a, dim=1, chunks=3) # 按照第一个维度(即列)切成三块, 那么应该是(2,3), (2,3), (2,1) 如果最后一份不能够乘除则要少于其他张量。
print(list_of_tensors)
for idx, t in enumerate(list_of_tensors):
print("第{}个张量:{}, shape is {}".format(idx+1, t, t.shape))
(4)torch.split()
功能:将张量按维度dim进行切分(可以指定维度)
返回值:张量列表
注意事项:list元素的和必须等于指定维度上张量的长度。
- tensor:要切分的张量
- split_size_or_sections:为int时,表示每一份的长度;为list时,按list元素切分(和为当前维度的长度,否则报错)
- dim:要切分的维度
测试代码
# split
t = torch.ones((2, 5))
list_of_tensors = torch.split(t, [2, 1, 2], dim=1) # [2 , 1, 2], list元素的和必须等于指定维度上张量的长度,即2+1+1=5。
for idx, t in enumerate(list_of_tensors):
print("第{}个张量:{}, shape is {}".format(idx+1, t, t.shape))
3.1.2 张量索引
(1)torch.index_select()
功能:按照索引查找,需要先指定一个tensor的索引量,然后指定类型是long的
返回值:依index索引数据拼接的张量
- input:要索引的张量
- dim:要索引的维度
- index:要索引数据的序号,index的数据类型是long类型
测试代码
t = torch.randint(0, 9, size=(3, 3)) # 从0-8随机产生数组成3*3的矩阵
print(t)
idx = torch.tensor([0, 2], dtype=torch.long) # index的数据类型是long类型
t_select = torch.index_select(t, dim=1, index=idx) #第0列和第2列拼接返回
print(t_select)
(2)torch.masked_select()
功能:按照值的条件进行查找,需要先指定条件作为mask,一般用来筛选数据
返回值:一维张量(因为不能确定张量中True的个数)
- input:要索引的张量
- mask:与input同形状的布尔类型张量
测试代码
t = torch.randint(0, 9, size=(3, 3)) # 从0-8随机产生数组成3*3的矩阵
mask = t.ge(5) # le表示<=5, ge表示>=5 gt >5 lt <5
print("mask: \n", mask)
t_select1 = torch.masked_select(t, mask) # 选出t中大于5的元素
print(t_select1)
3.1.3张量变换
(1)torch.reshape()
功能: 变换张量形状
注意事项:当张量在内存中是连续时,新张量与input共享数据内存
- input:要变换的张量
- shape:新张量的形状,若维度为-1,则表示根据其他维度的大小自动计算
测试代码
# torch.reshape
t = torch.randperm(8) # randperm是随机排列的一个函数,生成0到n-1之间n个数的随机排列
print(t)
t_reshape = torch.reshape(t, (-1, 2, 2)) # -1表示自动计算该维度
print("t:{}\nt_reshape:\n{}".format(t, t_reshape))
t[0] = 1024
print("t:{}\nt_reshape:\n{}".format(t, t_reshape))
print("t.data 内存地址:{}".format(id(t.data)))
print("t_reshape.data 内存地址:{}".format(id(t_reshape.data))) # 这个注意一下,两个是共内存的
(2)torch.transpose()和torch.t()
功能:交换张量的两个维度,矩阵的转置和图像的预处理中常用。
torch.transpose()
- input:要变换的张量
- dim0:要交换的维度
- dim1:要交换的维度
功能:二维张量转置,对于矩阵而言,等价于张量torch.transpose(input, 0, 1)
测试代码
# torch.transpose
t = torch.rand((2, 3, 4)) # 产生0-1之间的随机数
print(t)
t_transpose = torch.transpose(t, dim0=0, dim1=2) # c*h*w h*w*c, 这表示第0维和第2维进行交换
print("t shape:{}\nt_transpose shape: {}".format(t.shape, t_transpose.shape))
(3)torch.squeeze()和torch.unsqueeze()
功能:压缩长度为1的维度(轴)
- dim:若为None,移除所有长度为1的轴;若指定维度,当且仅当该轴长度为1时,可以被移除;
功能:依据dim扩展维度
- dim:扩展的维度
测试代码
# torch.squeeze
t = torch.rand((1, 2, 3, 1))
t_sq = torch.squeeze(t)
t_0 = torch.squeeze(t, dim=0)
t_1 = torch.squeeze(t, dim=1)
print(t.shape) # torch.Size([1, 2, 3, 1])
print(t_sq.shape) # torch.Size([2, 3])
print(t_0.shape) # torch.Size([2, 3, 1])
print(t_1.shape) # torch.Size([1, 2, 3, 1])
3.1.4张量数学运算
Pytorch中中的数学运算大致可以分为下图的三大类: 加减乘除, 对数指数幂函数,三角函数
torch.add()
功能:逐元素计算input+alpha*other
- input:第一个张量
- alpha:乘项因子
- other:第二个张量
测试代码
# torch.add()
t_0 = torch.randn((3, 3))
t_1 = torch.ones_like(t_0)
t_add = torch.add(t_0, 10, t_1)
print("t_0:\n{}\nt_1:\n{}\nt_add_10:\n{}".format(t_0, t_1, t_add))
3.2线性回归(Linear Regression)
测试代码
# Linear Regression
import torch
im
# 随机生成X,Y
x = torch.rand(20, 1) * 10
y = 2 * x + (5 + torch.randn(20, 1))
# 构建线性回归函数的参数
w = torch.randn((1), requires_grad=True)
b = torch.zeros((1), requires_grad=True) # 因为w,b都是参数,所以都需要计算梯度
for iteration in range(100):
# 前向传播
wx = torch.mul(w, x) # mul表示矩阵乘法
y_pred = torch.add(wx, b) # y = wx + 1 * b(alpha默认为1)
# 计算loss
loss = (0.5 * (y-y_pred)**2).mean() # 系数1/1是为了求导时计算的简洁
# 反向传播
loss.backward()
# 更新参数
b.data.sub_(lr * b.grad) # 相当于-=
w.data.sub_(lr * w.grad)
# 梯度清零
w.grad.data.zero_()
b.grad.data.zero_()
print(w.data, b.data)
四、autograd与回归算法
4.1 autograd——自动求导系统
PyTorch 中所有神经网络的核心是 autograd包。 autograd包为张量上的所有操作提供了自动求导。 它是一个在运行时定义的框架,这意味着反向传播是根据代码来确定如何运行,并且每次迭代可以是不同的。
4.1.1 torch.autograd.backward()
功能:自动求取梯度
- tensors:用于求导的张量,如loss
- retain_graph:保存计算图, 由于Pytorch采用了动态图机制,在每一次反向传播结束之后,计算图都会被释放掉。如果我们不想被释放,就要设置这个参数为True
- create_graph:表示创建导数计算图,用于高阶求导。
- grad_tensors:表示多梯度权重。如果有多个loss需要计算梯度的时候,就要设置这些loss的权重比例。
测试代码
grad_tensors = torch.tensor([1., 1.])
loss.backward(gradient=grad_tensors)
print(w.grad) # 这时候会是tensor([7.]) 5+2
grad_tensors = torch.tensor([1., 2.])
loss.backward(gradient=grad_tensors)
print(w.grad) # 这时候会是tensor([9.]) 5+2*2
4.1.2 torch.autograd.grad
功能:求取梯度
- outputs:用于求导的张量,如loss
- inputs:需要梯度的张量
- create_graph:创建导数计算图,用于高阶求导
- retain_graph:保存计算图
- grad_outputs:多梯度权重
注意事项:
- 梯度不自动清零(梯度会叠加,需手动清零,w.grad.zero_(),zero_的下划线表示原位操作)
- 依赖于叶子结点的结点,requires_grad默认为True
- 叶子结点不可执行in-place,这是因为反向传播时还需要用到叶子结点的数据,故叶子结点不能改变
4.1.2 前向传播、反向传播和计算图
以简单的深度神经网络为例,为了完成loss的优化,需要不断以mini-batch的数据送入模型网络中进行迭代过程,最终优化网络达到收敛:
- 1.mini-batch送入网络进行前向传播后输出的预测值,同真实值(label)对比后用loss函数计算出此次迭代的loss
- 2.loss进行反向传播,送入神经网络模型中之前的每一层,以更新weight矩阵和bias
也就是说模型训练的重点过程可以总结为两点:前向传播和反向传播,而其中前向传播就是矩阵+激活函数组合运算;而反向传播可以理解为稍显复杂的矩阵计算。
在Pytorch中,反向传播的计算依赖于autograd自动微分机制(这里说的自动微分,即指求导/梯度)。而autograd实现的基础,有以下两个部分:
- 1.数学基础——链式求导法则和雅克比矩阵
- 2.底层结构基础——由Tensor张量为基础构成的计算图模型
在pytorch中,底层结构是由tensor组成的计算图,虽然框架代码在实际autograd自动求梯度的过程中,并没有显示地构造和展示出计算图,不过其计算路径确实是沿着计算图的路径来进行的。
** 计算图**,即用图的方式来表示计算过程。如下是使用numpy的示例:
numpy表示
# numpy
import numpy as np
np.random.seed(0)
N, D = 3, 4
x = np.random.randn(N, D)
y = np.random.randn(N, D)
z = np.random.randn(N, D)
a = x * y
b = a + z
c = np.sum(b)
上述过程的计算图可以如下表示:
如上,蓝绿色的一个个节点构成了一个计算图,节点里的内容是变量或者计算符,这就是一个简单的计算图。同样的计算过程,也可以用pytorch中的tensor来表示:
python表示
import torch
x = torch.randn(N, D, requires_grad=True)
y = torch.randn(N, D)
z = torch.randn(N, D)
a = x * y
b = a + z
c = torch.sum(b)
如果用numpy表示,为了求出所有元素的梯度,需要以下几步:
grad_c = 1.0
grad_b = grad_c * np.ones((N, D))
grad_a = grad_b.copy()
grad_z = grad_b.copy()
grad_x = grad_a * y
grad_y = grad_a * x
而这只是一个很简单的情形,当面对深层神经网络以及复杂度较大的计算需求时候,框架的好处便能体现。基于计算图的数据结构使得pytorch可以应对复杂的神经网络,能方便地利用autograd机制来自动求导,只需一个.backward()即可自动求出标量对所有变量的梯度,并将梯度值存在各个变量tensor节点中,只需.grad便可读取:
c.backward()
print(x.grad)
4.1.3.链式法则和雅克比矩阵
(1)神经网络中的链式法则
链式法则是微积分中的求导法则,用于求一个复合函数的导数,是在微积分的求导运算中一种常用的方法,类似地也能用于神经网络的计算中。
下面以一个简单的神经网络模型为例:
一个神经网络中有5个神经元;其中为权重矩阵,为输出。满足以下计算关系:
组成的前向计算图如下:图片来源
在pytorch的神经网络模型中,通过反向传播来更新weight和bias的梯度时,计算过程就类似如下的计算图:图片来源
通过雅克比矩阵,即可表示所有L对所有权重的偏导:
实际上,pytorch计算对的偏导时,正是沿着反向传播计算图的路径执行的:
先求对的偏导数,再求对的偏导,然后求对的偏导,最后乘积即为所求。
(2)雅克比矩阵
wiki:
在矢量运算中,雅克比矩阵是基于函数对所有变量一阶偏导数的数值矩阵,当输入个数 = 输出个数时又称为雅克比行列式。假设是一个函数,其每个一阶偏导数都存在且属于。函数以$x ∈ ℝ_{n} $为输入,以向量 为输出。则的矩阵定义为矩阵,表达如下:
4.1.4 动态计算图和Autograd原理
Autograd
在熟悉了计算图、链式求导法则、雅克比矩阵的概念后,我们现在来看下反向传播在pytorch中的核心底层原理——Autograd和动态计算图。Autograd简而言之就是反向的自动微分(求偏导)系统。其实现的基础依赖于两点,前面也说过:
- 1.数学基础——链式求导法则和雅克比矩阵
- 2.底层结构基础——由Tensor张量为基础构成的计算图模型(DAG有向无环图)。
参考资料:官方文档
在用户用Tensor节点定义网络模型时,对Tensor的所有操作(包括tensor之间的关系,tensor的值的改变等)将被记录跟踪,形成一个概念上的前向传播的有向无环图DAG,在图中,输入tensor作为叶子节点,输出tensor作为根节点。反向传播autograd计算梯度时,从根节点开始遍历这些tensors来构造一个反向传播梯度的计算图模型,将计算得到的梯度值更新到上一层的节点,并重复此过程直至所有required=True的tensor变量都得到更新。此过程是从输出到输入节点一层层更新梯度,故称为反向传播。这一层层地求导过程,即隐式地利用了链式法则,最终各个变量的梯度值得以更新,故此过程形象地称为autograd。
反向传播计算图
从输出节点(根)遍历tensors,使用了栈结构,每个tensor梯度计算的具体方法存放于tensor节点的grad_fn属性中,依据此构建出包含梯度计算方法的反向传播计算图。
静态计算图&动态计算图
静态计算图理论上神经网络模型定义好以后就无需更改,当计算图构建好以后,在一轮轮的前向传播/反向传播迭代中可以重复使用此计算图,只不过将计算的梯度值不断更新到每个变量节点处即可,这种方式称为静态计算图,而早期的tensorflow采用的就是静态计算图的方式。
动态计算图和静态图相反,pytorch在设计中采取了动态计算图的方式,即反向传播的计算图是动态更新的。每一轮反向传播开始时(前向传播结束后)都会动态的重新构建一个计算图,当本次反向传播完成后,计算图再次销毁。这种动态更新的方式允许用户在迭代过程中更改网络的形状和大小,称为动态计算图。
4.2 softmax回归模型
机器学习模型训练的步骤
1.数据模块(数据采集,清洗,处理等)
2.建立模型(各种模型的建立)
3.损失函数的选择(根据不同的任务选择不同的损失函数),有了loss就可以求取梯度
4.得到梯度之后,我们会选择某种优化方式去进行优化
5.然后迭代训练
后面建立各种模型,都是基于这五大步骤进行, 这个就相当于一个逻辑框架了。下面就基于上面的五个步骤,看看Pytorch是如何建立一个softmax回归模型,并分类任务的。
softmax回归模型是神经网络中的分类模型。和线性回归不同,softmax回归的输出单元从一个变成了多个,且引入了softmax运算使输出更适合离散值的预测和训练。
4.2.1 分类问题
让我们考虑一个简单的图像分类问题,其输入图像的高和宽均为2像素,且色彩为灰度。这样每个像素值都可以用一个标量表示。我们将图像中的4像素分别记为。假设训练数据集中图像的真实标签为狗、猫或鸡(假设可以用4像素表示出这3种动物),这些标签分别对应离散值。
我们通常使用离散的数值来表示类别,例如。如此,一张图像的标签为1、2和3这3个数值中的一个。虽然我们仍然可以使用回归模型来进行建模,并将预测值就近定点化到1、2和3这3个离散值之一,但这种连续值到离散值的转化通常会影响到分类质量。因此我们一般使用更加适合离散值输出的模型来解决分类问题。
4.2.2 模型定义
softmax回归跟线性回归一样将输入特征与权重做线性叠加。与线性回归的一个主要不同在于,softmax回归的输出值个数等于标签里的类别数。因为一共有4种特征和3种输出动物类别,所以权重包含12个标量(带下标的)、偏差包含3个标量(带下标的),且对每个输入计算这3个输出:下图用神经网络图描绘了上面的计算。softmax回归同线性回归一样,也是一个单层神经网络。由于每个输出的计算都要依赖于所有的输入,softmax回归的输出层也是一个全连接层。
4.2.3 softmax运算
既然分类问题需要得到离散的预测输出,一个简单的办法是将输出值当作预测类别是的置信度,并将值最大的输出所对应的类作为预测输出,即输出。例如,如果分别为,由于最大,那么预测类别为2,其代表猫。
然而,直接使用输出层的输出有两个问题。一方面,由于输出层的输出值的范围不确定,我们难以直观上判断这些值的意义。例如,刚才举的例子中的输出值10表示“很置信”图像类别为猫,因为该输出值是其他两类的输出值的100倍。但如果,那么输出值10却又表示图像类别为猫的概率很低。另一方面,由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。
softmax运算符(softmax operator)解决了以上两个问题。它通过下式将输出值变换成值为正且和为1的概率分布:
其中
容易看出且,因此是一个合法的概率分布。这时候,如果,不管和的值是多少,我们都知道图像类别为猫的概率是80%。此外,我们注意到因此softmax运算不改变预测类别输出。
4.2.4 单样本分类的矢量计算表达式
为了提高计算效率,我们可以将单样本分类通过矢量计算来表达。在上面的图像分类问题中,假设softmax回归的权重和偏差参数分别为
设高和宽分别为2个像素的图像样本的特征为
输出层的输出为
预测为狗、猫或鸡的概率分布为
softmax回归对样本分类的矢量计算表达式为
4.2.5 小批量样本分类的矢量计算表达式
为了进一步提升计算效率,我们通常对小批量数据做矢量计算。广义上讲,给定一个小批量样本,其批量大小为,输入个数(特征数)为,输出个数(类别数)为。设批量特征为。假设softmax回归的权重和偏差参数分别为和。softmax回归的矢量计算表达式为
其中的加法运算使用了广播机制,且这两个矩阵的第行分别为样本的输出和概率分布。
4.2.6 交叉熵损失函数
前面提到,使用softmax运算后可以更方便地与离散标签计算误差。我们已经知道,softmax运算将输出变换成一个合法的类别预测分布。实际上,真实标签也可以用类别分布表达:对于样本,我们构造向量 ,使其第(样本类别的离散数值)个元素为1,其余为0。这样我们的训练目标可以设为使预测概率分布尽可能接近真实的标签概率分布。
我们可以像线性回归那样使用平方损失函数。然而,想要预测分类结果正确,我们其实并不需要预测概率完全等于标签概率。例如,在图像分类的例子里,如果,那么我们只需要比其他两个预测值和大就行了。即使值为0.6,不管其他两个预测值为多少,类别预测均正确。而平方损失则过于严格,例如比的损失要小很多,虽然两者都有同样正确的分类预测结果。
改善上述问题的一个方法是使用更适合衡量两个概率分布差异的测量函数。其中,交叉熵(cross entropy)是一个常用的衡量方法:
其中带下标的是向量中非0即1的元素,需要注意将它与样本类别的离散数值,即不带下标的区分。在上式中,我们知道向量中只有第个元素为1,其余全为0,于是。也就是说,交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。当然,遇到一个样本有多个标签时,例如图像里含有不止一个物体时,我们并不能做这一步简化。但即便对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。
假设训练数据集的样本数为,交叉熵损失函数定义为
那么有:其中代表模型参数。同样地,如果每个样本只有一个标签,那么交叉熵损失可以简写成。从另一个角度来看,我们知道最小化等价于最大化,即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率。
4.2.7模型预测及评价
在训练好softmax回归模型后,给定任一样本特征,就可以预测每个输出类别的概率。通常,我们把预测概率最大的类别作为输出类别。如果它与真实类别(标签)一致,说明这次预测是正确的。
4.2.8 模型实现
(1)获取和读取数据集
# 准备工作
import torch
from IPython import display
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) # 创建训练集、测试集迭代器
# 将图片拉成向量
num_inputs = 784 # 28*28
num_outputs = 10 # 输出类别数
# 初始化参数
W = torch.normal(0, 0.01 ,size=(num_inputs, num_outputs),requires_grad = True)
b = torch.zeros(num_outputs, requires_grad = True)
(2)定义相关函数
# 定义softmax运算函数
def softmax(X):
X_exp = torch.exp(X) # 对X(矩阵)每个元素求exp
partition = X_exp.sum(1, keepdim = True) # softmax相当于对矩阵每一行做softmax
return X_exp / partition # 应用广播机制,partition会变成和X一样的维度后再运算
# 定义model
def net(X):
return softmax(torch.matmul(X.reshape((-1,W.shape[0])), W)+ b)
# 定义损失函数
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
# 定义分类准确率函数
def accuracy(y_hat, y):
'''计算分类正确的数量'''
# 如果y_hat是二维以上矩阵(y_hat是一数的话则无意义)并且y_hat的列向量大于1(列向量的数量是类别数)
if len(y_hat.shape) > 1and y_hat.shape[1] > 1:
# 按行找到y_hat矩阵中最大的那个概率,并且返回索引;
# 如果这个索引和y对应行的索引相同,就说明预测正确,否则错误
y_hat = y_hat.argmax(axis = 1)
cmp = y_hat.type(y.dtype) == y # 将y_hat的数据类型转换成y的数据类型,再进行比较
# 预测正确的个数
return float(cmp.type(y.dtype).sum())
# 评估模型在任意net上的准确率
def evaluate_accuracy (net, data_iter):
if isinstance(net, torch.nn.Module): # 如果net使用torch.nn实现的模型
net.eval() # 则将模型转变为评估模式
metric = Accumulator(2) # metric包括的参数为正确预测数和预测总数 ,Accumulator表示创建累加器
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel()) # y.numel()表示求张量y中元素的总个数
return metric[0] / metric[1] # 返回准确率
# Accumulator的实现,创建了两个变量,分别用于存储正确预测的数量和预测的总数量
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
# 定义一个动画类
class Animator: #@save
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
(3)训练模型
def train_epoch_ch3(net, train_iter, loss, updater):
if isinstance(net, torch.nn.Module):
net.train()# net要么是手动定义要么是nn.Module定义的
metric = Accumulator(3) # 分别为所有损失累加,所有分类正确的样本个数,样本总数
for X, y in train_iter: #扫数据集
y_hat = net(X) # 得到预测的y
l = loss(y_hat, y) # 计算损失
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad() # 梯度设置为零
l.backward() # 反向求梯度
updater.step() # 参数自更新
metric.add(
float(l) * len(y), accuracy(y_hat, y), y.size().numel() )
else:
l.sum().backward()
updater(X.shape[0]) # X.shape[0]表示第一个样本的权重
metric.add(
l.sum(), accuracy(y_hat, y), y.numel())
return metric[0] / metric[1], metric[0] / metric[2]
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 2],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
lr = 0.1 # 学习率
# 定义优化器(小批量随机梯度下降)
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
num_epochs = 100
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
(4)预测
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
代码可以运行,如果运行不了的话,可能是d2l包的版本不对,需要下载对应的版本。
参考资料