深度学习——基础(基于Pytorch代码)
Posted 颜妮儿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度学习——基础(基于Pytorch代码)相关的知识,希望对你有一定的参考价值。
写在前面:该系列文章大都摘自于李沐的《动手学深度学习》,一边学习一边记录和实践,希望自己可以有收获🧐
文章目录
预备知识
数据操作
Pytorch中对数据(tensor 张量)的操作类似于Numpy(ndarray),但由于深度学习大都是用GPU进行计算,而Numpy仅支持CPU计算,且Pytorch的张量类支持自动微分,所以我们有必要去了解如何使用Pytorch的张量。
生成张量:
import torch
# 生成一个包含0~11的向量
x = torch.arange(12) #tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
# 获取每个维度的大小
x.shape # torch.Size([12])
# 获取张量中的元素总个数
x.numel() # 12
# 修改张量的形状,当确定行数或列数时,另一个值可以设为-1.会自动调整大小
X = x.reshape(3, 4)
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# 定义一个形状为(2,3,4),元素全为0的张量
torch.zeros((2, 3, 4))
# tensor([[[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]],
#
# [[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]]])
# 定义一个形状为(2,3,4),元素全为1的张量
torch.ones((2, 3, 4))
# tensor([[[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]],
#
# [[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]]])
# 定义一个形状为(3,4),元素是随机值,但服从标准正态分布
torch.randn(3, 4)
# tensor([[-1.0778, -0.7014, -0.1674, -0.8677],
# [-0.4078, -1.1577, 1.0313, 0.7065],
# [ 0.2872, -1.0250, 0.0995, 0.9716]])
张量的基本运算
# 对应位置的元素进行运算操作
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
# (tensor([ 3., 4., 6., 10.]),
# tensor([-1., 0., 2., 6.]),
# tensor([ 2., 4., 8., 16.]),
# tensor([0.5000, 1.0000, 2.0000, 4.0000]),
# tensor([ 1., 4., 16., 64.]))
# 求幂运算
torch.exp(x)
# tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])
# 判断两个张量对应位置的元素是否相等
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
X == Y
# tensor([[False, True, False, True],
# [False, False, False, False],
# [False, False, False, False]])
# 对张量中的元素值求和
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
X.sum()
# tensor(66.)
张量的拼接
Python中,0轴表示行,1轴表示列。
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
# dim=0 表示对行进行操作(行数改变);dim=1 表示对列进行操作(列数改变)
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
# (tensor([[ 0., 1., 2., 3.],
# [ 4., 5., 6., 7.],
# [ 8., 9., 10., 11.],
# [ 2., 1., 4., 3.],
# [ 1., 2., 3., 4.],
# [ 4., 3., 2., 1.]]),
# tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
# [ 4., 5., 6., 7., 1., 2., 3., 4.],
# [ 8., 9., 10., 11., 4., 3., 2., 1.]]))
广播机制
两个形状不同的张量通过广播机制按元素执行操作,该机制的工作方式为:
- 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;
- 对生成的数组执行按元素操作。
a = torch.arange(3).reshape((3, 1))
# tensor([[0],
# [1],
# [2]])
b = torch.arange(2).reshape((1, 2))
# tensor([[0, 1]])
a+b
# tensor([[0, 1],
# [1, 2],
# [2, 3]])
# a和b分别扩展为:
# a: tensor([[0, 0],
# [1, 1],
# [2, 2]])
# b: tensor([[0, 1],
# [0, 1],
# [0, 1]])
索引和切片
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
# 最后一行
X[-1]
# tensor([ 8., 9., 10., 11.])
# 第2和第三行
X[1:3]
# tensor([[ 4., 5., 6., 7.],
# [ 8., 9., 10., 11.]])
# 指定索引修改元素
X[1, 2] = 9
# tensor([[ 0., 1., 2., 3.],
# [ 4., 5., 9., 7.],
# [ 8., 9., 10., 11.]])
# 修改第一行和第二行的元素为12
X[0:2, :] = 12
# tensor([[12., 12., 12., 12.],
# [12., 12., 12., 12.],
# [ 8., 9., 10., 11.]])
节省内存
当对张量进行运算时,可能会为新结果分配内存,例如:
before = id(Y)
Y = Y + X
id(Y) == before # False
我们可以使用切片表示法将操作的结果分配给先前分配的数组,例如:
Z = torch.zeros_like(Y)
print('id(Z):', id(Z)) # id(Z): 2675870772256
Z[:] = X + Y
print('id(Z):', id(Z)) # id(Z): 2675870772256
如果在后续计算中没有重复使用X, 我们也可以使用X[:] = X + Y或X += Y来减少操作的内存开销,例如:
before = id(X)
X += Y
id(X) == before # True
线性代数
矩阵及其转置
A = torch.arange(20).reshape(5, 4)
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11],
# [12, 13, 14, 15],
# [16, 17, 18, 19]])
# 转置
A.T
# tensor([[ 0, 4, 8, 12, 16],
# [ 1, 5, 9, 13, 17],
# [ 2, 6, 10, 14, 18],
# [ 3, 7, 11, 15, 19]])
张量算法的基本性质
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
# tensor([[ 0., 1., 2., 3.],
# [ 4., 5., 6., 7.],
# [ 8., 9., 10., 11.],
# [12., 13., 14., 15.],
# [16., 17., 18., 19.]])
B = A.clone() # 通过分配新内存,将A的一个副本分配给B
A + B
# tensor([[ 0., 2., 4., 6.],
# [ 8., 10., 12., 14.],
# [16., 18., 20., 22.],
# [24., 26., 28., 30.],
# [32., 34., 36., 38.]])
# 两个矩阵的对应元素相乘,Hadamard积
A * B
# tensor([[ 0., 1., 4., 9.],
# [ 16., 25., 36., 49.],
# [ 64., 81., 100., 121.],
# [144., 169., 196., 225.],
# [256., 289., 324., 361.]])
# 矩阵乘法
B = torch.ones(4, 3)
torch.mm(A, B)
# tensor([[ 6., 6., 6.],
# [22., 22., 22.],
# [38., 38., 38.],
# [54., 54., 54.],
# [70., 70., 70.]])
# 将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。
X = torch.arange(24).reshape(2, 3, 4)
# 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]]])
a = 2
a + X
# tensor([[[ 2, 3, 4, 5],
# [ 6, 7, 8, 9],
# [10, 11, 12, 13]],
#
# [[14, 15, 16, 17],
# [18, 19, 20, 21],
# [22, 23, 24, 25]]])
(a * X).shape
# torch.Size([2, 3, 4]))
降维
# 求和
x = torch.arange(4, dtype=torch.float32)
x.sum()
# tensor(6.)
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
A.sum()
# tensor(190.)
# axis=0对行进行操作,及求每列的和
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
A_sum_axis0=A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
# (tensor([40., 45., 50., 55.]), torch.Size([4])) 一维结果
# axis=1对列进行操作,及求每行的和
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
A_sum_axis1=A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
# (tensor([ 6., 22., 38., 54., 70.]), torch.Size([5])) 一维结果
# 求均值
# 获取每行的均值
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
# (tensor([ 8., 9., 10., 11.]), tensor([ 8., 9., 10., 11.])) 一维结果
非降维求和
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
sum_A = A.sum(axis=1, keepdims=True)
# 仍是二维
# tensor([[ 6.],
# [22.],
# [38.],
# [54.],
# [70.]])
# 求每行元素除以各行均值:
A / sum_A
# tensor([[0.0000, 0.1667, 0.3333, 0.5000],
# [0.1818, 0.2273, 0.2727, 0.3182],
# [0.2105, 0.2368, 0.2632, 0.2895],
# [0.2222, 0.2407, 0.2593, 0.2778],
# [0.2286, 0.2429, 0.2571, 0.2714]])
如果我们想沿某个轴计算A元素的累积总和, 比如axis=0(按行计算),可以调用cumsum函数。 此函数不会沿任何轴降低输入张量的维度。
A.cumsum(axis=0)
# tensor([[ 0., 1., 2., 3.],
# [ 4., 6., 8., 10.],
# [12., 15., 18., 21.],
# [24., 28., 32., 36.],
# [40., 45., 50., 55.]])
在原始数据之后添加一行计算结果。相应地,如果是按列进行计算,则在添加为列。
自动微分
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
例如,求函数
y
=
2
x
T
x
y = 2\\mathbfx^\\mathrmT\\mathbfx
y=2xTx关于
x
\\mathbfx
x求导,其中
x
\\mathbfx
x的初始值为
[
0
,
1
,
2
,
3
]
[0, 1, 2, 3]
[0,1,2,3]:
import torch
x = torch.arange(4.0)
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
print(x.grad) # 输出默认值为None
现计算 y y y:
y = 2 * torch.dot(x, x)
# tensor(28., grad_fn=<MulBackward0>)
接下来,通过调用反向传播函数来自动计算y关于x每个分量的梯度,并打印这些梯度:
y.backward()
x.grad # tensor([ 0., 4., 8., 12.])
函数 y = 2 x T x y = 2\\mathbfx^\\mathrmT\\mathbfx y=2xTx的梯度应为 4 x 4\\mathbfx 4x,验证梯度是否正确:
x.grad == 4 * x
# tensor([True, True, True, True])
非标量变量的反向传播
(我对这里不太理解)
当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中), 但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad # tensor([0., 2., 4., 6.])
Python控制流的梯度计算
使用自动微分的一个好处是: 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。
# c=k*a.k的值取决于a的输入
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
a.grad == d / a # tensor(True)
深度学习计算
层和块
自定义块
块(block)可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件。通过定义代码来按需生成任意复杂度的块, 我们可以通过简洁的代码实现复杂的神经网络。如图,右边的网络由4个块组成:
从编程的角度来看,块由类(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数。 注意,有些块不需要任何参数。 最后,为了计算梯度,块必须具有反向传播函数。在定义我们自己的块时,由于自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。
每个块的基本功能:
- 将输入数据作为其前向传播函数的参数。
- 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数。
构造一个块,其有256个隐藏单元的隐藏层和一个10个输出节点的输出层:
import torch
from torch import nn
from torch.nn import functional as F
class MLP(nn.Module):
# 用模型参数声明层。这里,我们声明两个全连接的层
def __init__(self):
# 调用MLP的父类Module的构造函数来执行必要的初始化。
# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
super().__init__()
self.hidden = nn.Linear(20, 256) # 隐藏层
self.out = nn.Linear(256, 10) # 输出层
# 定义模型的前向传播,即如何根据输入X返回所需的模型输出
def forward(self, X):
# 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
# 运算顺序为 A = W_1 * X, B = relu(B), C = w_2 * B
# W_1为20*256的参数,w_2为 256*10的参数
return self.out(F.relu(self.hidden(X)))
X = torch.rand(2, 20)
net = MLP()
net(X)
# tensor([[ 0.0718, -0.0758, -0.1922, 0.0293, 0.2414, 0.0770, -0.1361, -0.0221,
# 0.0587, 0.0078],
# [ 0.0478, -0.0155, -0.0269, 0.0704, 0.1355, 0.2137, -0.1533, 0.0172,
# 0.2437, 0.0926]], grad_fn=<AddmmBackward0>)
顺序快
顺序块的作用:将多个模块串起来。
实现的两种方式:
- 一种将块逐个追加到列表中的函数;
- 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
定义顺序块:
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
# 变量_modules中。_module的类型是OrderedDict
self._modules[str(idx)] = module
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
__init__函数将每个模块逐个添加到有序字典_modules中,通过这样,在模块的参数初始化过程中, 系统知道在_modules字典中查找需要初始化参数的子块。
调用:
X = torch.rand(2, 20)
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
# tensor([[-0.0622, -0.2874, -0.0551, 0.0102, 0.0808, -0.0382, -0.1675, -0.2091,
# -0.1044, 0.2538],
# [-0.0761, -0.3218, -0.1482, -0.0384, -0.0085, 0.1346, -0.1855, -0.1458,
# -0.1240, 0.2864]], grad_fn=<AddmmBackward0>)
在前向传播函数中执行代码
Sequential类使模型构造变得简单, 允许我们组合新的架构,而不必定义自己的类。 然而,并不是所有的架构都是简单的顺序架构。 当需要更强的灵活性时,我们需要定义自己的块。另外,我们可能希望执行任意的数学运算, 而不是简单地依赖预定义的神经网络层。
到目前为止, 我们网络中的所有操作都对网络的激活值及网络的参数起作用。 然而,有时我们可能希望合并既不是上一层的结果也不是可更新参数的项, 我们称之为常数参数(constant parameter)。 例如,我们需要一个计算函数
f
(
x
,
w
)
=
c
⋅
w
T
x
f(\\mathbfx,\\mathbfw) = c \\cdot \\mathbfw^\\mathrmT\\mathbfx
f(x,w)=c⋅wTx的层, 其中是
x
\\mathbfx
x输入,
w
\\mathbfw
w是参数,
c
c
c是某个在优化过程中没有更新的指定常量。 则有:
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# 不计算梯度的随机权重参数。因此其在训练期间保持不变
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)
def forward(self, XPyTorch由于使用了强大的GPU加速的Tensor计算(类似numpy)和基于磁带的自动系统的深度神经网络。这使得今年一月份被开源的PyTorch成为了深度学习领域新流行框架,许多新的论文在发表过程中都加入了大多数人不理解的PyTorch代码。这篇文章我们就来讲述一下我对PyTorch代码的理解,希望能帮助你阅读PyTorch代码。整个过程是基于贾斯汀·的约翰逊伟大教程。如果你想了解更多或者有超过10分钟的时间,建议你去读下整篇代码。
PyTorch 由4个主要包装组成:
火炬:类似于Numpy的通用数组库,可以在将张量类型转换为(torch.cuda.TensorFloat)并在GPU上进行计算。
torch.autograd :用于构建计算图形并自动获取渐变的包
torch.nn :具有共同层和成本函数的神经网络库
torch.optim :具有通用优化算法(如SGD,Adam等)的优化包
1. 导入工具
可以你这样导入PyTorch:
2.torch数组取代了numpy ndarray - >在GPU支持下提供线性代数
第一个特色,PyTorch提供了一个像numpy的数组一样的多维数组,当数据类型被转换为(torch.cuda.TensorFloat)时,可以在GPU上进行处理。这个数组和它的关联函数是一般的科学计算工具。
从下面的代码中,我们可以发现,PyTorch提供的这个包的功能可以将我们常用的二维数组变成GPU可以处理的三维数组。这极大的提高了GPU的利用效率,提升了计算速度。
大家可以自己比较Torch和numpy,从而发现他们的优缺点。
3.torch.autograd 可以生成一个计算图- > 自动计算梯度
第二个特色是autograd 包,其提供了定义计算图的能力,以便我们可以自动计算渐变梯度。在计算图中,一个节点是一个数组,边(边缘)是对数组的一个操作。要做一个计算图,我们需要在(torch.aurograd.Variable ())函数中通过包装数组来创建一个节点。那么我们在这个节点上所做的所有操作都将被定义为边,它们将是计算图中新的节点。图中的每个节点都有一个(node.data )属性,它是一个多维数组和一个(node.grad )属性,这是相对于一些标量值的渐变(node.grad也是一个。变量())在定义计算图之后,我们可以使用单个命令(loss.backward ())来计算图中所有节点的损耗梯度。
使用torch.autograd.Variable ()将张量转换为计算图中的节点。
使用x.data 访问其值。
使用x.grad 访问其渐变。
在.Variable ()上执行操作,绘制图形的边缘。
4.Tronch.nn 包含各种NN 层(张量行的线性映射)+ (非线性) - >
其作用是有助于构建神经网络计算图,而无需手动操纵张量和参数,减少不必要的麻烦。
第三个特色是高级神经网络库(torch.nn ),其抽象出了神经网络层中的所有参数处理,以便于在通过几个命令(例如torch.nn.conv )就很容易地定义NN 。这个包也带有流动的损失函数的功能(例如torch.nn.MSEloss )。我们首先定义一个模型容器,例如使用(torch.nn.Sequential )的层序列的模型,然后在序列中列出我们期望的这个高级神经网络库也可以处理其他的事情,我们可以使用(model.parameters ())访问参数(Variable())
我们还可以通过子类(torch.nn.Module )定义自定义层,并实现接受(Variable ())作为输入的(forward ())函数,并产生(Variable ())作为输出我们也可以通过定义一个时间变化的层来做一个动态网络。
定义自定义层时,需要实现2 个功能:
_ init_函数必须始终被继承,然后层的所有参数必须在这里定义为类变量(self.x )
正向函数是我们通过层传递输入的函数,使用参数对输入进行操作并返回输出。输入需要是一个autograd.Variable (),以便pytorch 可以构建图层的计算图。
5.torch.optim 也可以做优化- >
我们使用torch.nn 构建一个神经网络计算图,使用torch.autograd 来计算梯度,然后将它们提供给torch.optim 来更新网络参数。
第四个特色是与NN 库一起工作的优化软件包(torch.optim )。该库包含复杂的优化器,如Adam ,RMSprop 等。我们定义一个优化器并传递网络参数和学习率(opt = torch .optim.Adam (model.parameters (),lr = learning_rate ))然后我们调用(opt.step())对我们的参数进行近一步更新。
建立神经网络很容易,但是如何协同工作并不容易这是一个示例显示如何协同工作:
希望上述的介绍能够帮你更好的阅读PyTorch代码。
以上是关于深度学习——基础(基于Pytorch代码)的主要内容,如果未能解决你的问题,请参考以下文章