网站备案帐号,工作空窗期怎么缴纳社保,企业网站管理系统,百度推广上班怎么样2.1 张量
本节主要内容#xff1a;
张量的简介PyTorch如何创建张量PyTorch中张量的操作PyTorch中张量的广播机制
2.1.1 简介
几何代数中定义的张量是基于向量和矩阵的推广#xff0c;比如我们可以将标量视为零阶张量#xff0c;矢量可以视为一阶张量#xff0c;矩阵就是…2.1 张量
本节主要内容
张量的简介PyTorch如何创建张量PyTorch中张量的操作PyTorch中张量的广播机制
2.1.1 简介
几何代数中定义的张量是基于向量和矩阵的推广比如我们可以将标量视为零阶张量矢量可以视为一阶张量矩阵就是二阶张量。
张量维度代表含义0维张量代表的是标量数字1维张量代表的是向量2维张量代表的是矩阵3维张量时间序列数据 股价
张量是现代机器学习的基础。它的核心是一个数据容器多数情况下它包含数字有时候它也包含字符串但这种情况比较少。因此可以把它想象成一个数字的水桶。
这里有一些存储在各种类型张量的公用数据集类型
3维 时间序列4维 图像5维 视频
例子一个图像可以用三个字段表示分别是
width: 图像的宽度即水平方向上的像素数量height: 图像的高度即垂直方向上的像素数量channel: 图像的颜色通道数如RGB彩色图像有3个通道灰度图像有1个通道
(width, height, channel) 3D
但是在机器学习工作中我们经常要处理不止一张图片或一篇文档——我们要处理一个集合。我们可能有10,000张郁金香的图片这意味着我们将用到4D张量
(batch_size, width, height, channel) 4D
在PyTorch中 torch.Tensor 是存储和变换数据的主要工具。如果你之前用过NumPy你会发现 Tensor 和NumPy的多维数组非常类似。然而Tensor 提供GPU计算和自动求梯度等更多功能这些使 Tensor 这一数据类型更加适合深度学习。 2.1.2 创建tensor
在接下来的内容中我们将介绍几种常见的创建tensor的方法。
随机初始化矩阵 我们可以通过torch.rand()的方法构造一个随机初始化的矩阵
import torch
x torch.rand(4, 3, 1)
print(x, type(x), x.shape, x.size())tensor([[[0.9630],[0.2057],[0.2067]],[[0.5101],[0.8320],[0.9128]],[[0.1335],[0.9004],[0.9082]],[[0.1949],[0.2616],[0.3007]]]) class torch.Tensor torch.Size([4, 3, 1]) torch.Size([4, 3, 1])全0矩阵的构建 我们可以通过torch.zeros()构造一个矩阵全为 0并且通过dtype设置数据类型为 long。除此以外我们还可以通过torch.zero_()和torch.zeros_like()将现有矩阵转换为全0矩阵.
import torch
x torch.zeros(4, 3, dtypetorch.long)
print(x)tensor([[0, 0, 0],[0, 0, 0],[0, 0, 0],[0, 0, 0]])# 使用 torch.zero_() 将现有矩阵转换为全0矩阵
y torch.rand(2, 3)
print(\n原始 y:)
print(y)
y.zero_()
print(使用 torch.zero_() 后的 y:)
print(y)原始 y:
tensor([[0.8797, 0.4270, 0.7012],[0.5926, 0.1490, 0.6743]])
使用 torch.zero_() 后的 y:
tensor([[0., 0., 0.],[0., 0., 0.]])# 使用 torch.zeros_like() 创建与现有矩阵形状相同的全0矩阵
z torch.rand(3, 2)
print(\n原始 z:)
print(z)
z_zeros torch.zeros_like(z)
print(torch.zeros_like(z):)
print(z_zeros)原始 z:
tensor([[0.5537, 0.3520],[0.3345, 0.1989],[0.8854, 0.5777]])
torch.zeros_like(z):
tensor([[0., 0.],[0., 0.],[0., 0.]])张量的构建 我们可以通过torch.tensor()直接使用数据构造一个张量
import torch
x torch.tensor([5.5, 3])
print(x)
tensor([5.5000, 3.0000])import torch
import numpy as np# 1. 使用Python列表一维或多维
x1 torch.tensor([1, 2, 3, 4])
x2 torch.tensor([[1, 2], [3, 4]])print(从Python列表创建)
print(x1)
print(x2)从Python列表创建
tensor([1, 2, 3, 4])
tensor([[1, 2],[3, 4]])# 2. 使用NumPy数组
np_array np.array([1, 2, 3, 4])
x3 torch.tensor(np_array)print(\n从NumPy数组创建)
print(x3) 从NumPy数组创建
tensor([1, 2, 3, 4])# 3. 使用Python标量
x4 torch.tensor(3.14)
x5 torch.tensor(True)print(\n从Python标量创建)
print(x4)
print(x5) 从Python标量创建
tensor(3.1400)
tensor(True)# 4. 使用其他PyTorch张量
existing_tensor torch.randn(2, 3)
x6 torch.tensor(existing_tensor)print(\n从现有张量创建)
print(x6) 从现有张量创建
tensor([[ 1.3995, 0.8028, -1.1152],[ 1.6206, 0.7856, 0.1517]])/var/folders/z7/ll9p_xgn76l2f7pqtx3c44jr0000gn/T/ipykernel_36214/1463659560.py:3: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).x6 torch.tensor(existing_tensor)# 5. 使用Python元组
x7 torch.tensor((1, 2, 3))print(\n从Python元组创建)
print(x7)从Python元组创建
tensor([1, 2, 3])# 6. 使用range对象
x8 torch.tensor(range(5))print(\n从range对象创建)
print(x8)从range对象创建
tensor([0, 1, 2, 3, 4])返回的torch.Size其实是一个tuple⽀持所有tuple的操作。我们可以使用索引操作取得张量的长、宽等数据维度。
常见的构造Tensor的方法
函数功能Tensor(sizes)基础构造函数tensor(data)类似于np.arrayones(sizes)全1zeros(sizes)全0eye(sizes)对角为1其余为0arange(s,e,step)从s到e步长为steplinspace(s,e,steps)从s到e均匀分成step份rand/randn(sizes)rand是[0,1)均匀分布randn是服从N(01)的正态分布normal(mean,std)正态分布(均值为mean标准差是std)randperm(m)随机排列
2.1.3 张量的操作
在接下来的内容中我们将介绍几种常见的张量的操作方法
加法操作
import torch# 创建两个张量
x torch.tensor([[1, 2], [3, 4]])
y torch.tensor([[5, 6], [7, 8]])# 1. 使用 运算符
result_1 x y
print(使用 运算符:)
print(result_1)使用 运算符:
tensor([[ 6, 8],[10, 12]])# 2. 使用 torch.add() 函数
result_2 torch.add(x, y)
print(\n使用 torch.add() 函数:)
print(result_2)使用 torch.add() 函数:
tensor([[ 6, 8],[10, 12]])# 3. 使用 add_() 方法进行原地操作
x_copy x.clone() # 创建 x 的副本以免修改原始 x
x_copy.add_(y)
print(\n使用 add_() 方法进行原地操作:)
print(x_copy)使用 add_() 方法进行原地操作:
tensor([[ 6, 8],[10, 12]])# 4. 使用 torch.add() 函数并指定输出张量
result_4 torch.empty_like(x)
torch.add(x, y, outresult_4)
print(\n使用 torch.add() 函数并指定输出张量:)
print(result_4)使用 torch.add() 函数并指定输出张量:
tensor([[ 6, 8],[10, 12]])print(x)tensor([[1, 2],[3, 4]])# 5. 加上一个标量
scalar 10
result_5 x scalar
print(\n加上一个标量:)
print(result_5)加上一个标量:
tensor([[11, 12],[13, 14]])# 6. 使用广播机制进行加法
z torch.tensor([1, 2])
result_6 x z
print(\n使用广播机制进行加法:)
print(result_6)使用广播机制进行加法:
tensor([[2, 4],[4, 6]])索引操作(类似于numpy)
需要注意的是索引出来的结果与原数据共享内存修改一个另一个会跟着修改。如果不想修改可以考虑使用copy()等方法
import torch
x torch.rand(4,3)
print(x)
# 取第二列
print(x[:, 1]) tensor([[0.6240, 0.1236, 0.6454],[0.2799, 0.1227, 0.4354],[0.6472, 0.8142, 0.0389],[0.7155, 0.9703, 0.3107]])
tensor([0.1236, 0.1227, 0.8142, 0.9703])y x[0,:]
print(y)
y 1
print(y)
print(x[0, :]) # 因为索引操作返回的是对原tensor的引用(视图),而不是副本,所以修改索引结果会影响原tensor tensor([0.6240, 0.1236, 0.6454])
tensor([1.6240, 1.1236, 1.6454])
tensor([1.6240, 1.1236, 1.6454])维度变换 张量的维度变换常见的方法有torch.view()和torch.reshape()下面我们将介绍第一中方法torch.view()
区分维度和长度的区别 维度张量的维度比如4维5维 长度张量中元素的个数比如4个元素5个元素
x torch.randn(4, 4)
e torch.tensor(4.0)
y x.view(16)
z x.view(-1, 8) # -1是指这一维的维数由其他维度决定
print(x)
print(y)
print(z)
print(x.size(), y.size(), z.size(), e.size()) tensor([[-0.0676, 0.3851, 2.1078, -2.2629],[ 0.9745, 0.1784, 0.5629, -0.6183],[-0.1645, 0.6379, 1.3582, 2.5104],[-0.1031, -0.1028, -1.0995, 0.2379]])
tensor([-0.0676, 0.3851, 2.1078, -2.2629, 0.9745, 0.1784, 0.5629, -0.6183,-0.1645, 0.6379, 1.3582, 2.5104, -0.1031, -0.1028, -1.0995, 0.2379])
tensor([[-0.0676, 0.3851, 2.1078, -2.2629, 0.9745, 0.1784, 0.5629, -0.6183],[-0.1645, 0.6379, 1.3582, 2.5104, -0.1031, -0.1028, -1.0995, 0.2379]])
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8]) torch.Size([])注: torch.view() 返回的新tensor与源tensor共享内存(其实是同一个tensor)更改其中的一个另外一个也会跟着改变。(顾名思义view()仅仅是改变了对这个张量的观察角度)
x 1
print(x)
print(y) # 也加了了1 tensor([[ 0.9324, 1.3851, 3.1078, -1.2629],[ 1.9745, 1.1784, 1.5629, 0.3817],[ 0.8355, 1.6379, 2.3582, 3.5104],[ 0.8969, 0.8972, -0.0995, 1.2379]])
tensor([ 0.9324, 1.3851, 3.1078, -1.2629, 1.9745, 1.1784, 1.5629, 0.3817,0.8355, 1.6379, 2.3582, 3.5104, 0.8969, 0.8972, -0.0995, 1.2379])上面我们说过torch.view()会改变原始张量但是很多情况下我们希望原始张量和变换后的张量互相不影响。为了使创建的张量和原始张量不共享内存我们需要使用第二种方法torch.reshape()同样可以改变张量的形状。下面是一个例子
import torch
original_tensor torch.randn(4, 4) # 创建一个4x4的张量
reshaped_tensor original_tensor.reshape(16) # 使用reshape改变形状
print(Original Tensor:\n, original_tensor)
print(Reshaped Tensor:\n, reshaped_tensor)Original Tensor:tensor([[ 0.0997, 2.1006, 1.3564, -2.2575],[ 0.5212, 0.7903, 0.5076, -0.9435],[-1.0901, -0.5410, -0.7492, 0.3113],[ 1.8470, 0.4270, -1.5640, 0.3467]])
Reshaped Tensor:tensor([ 0.0997, 2.1006, 1.3564, -2.2575, 0.5212, 0.7903, 0.5076, -0.9435,-1.0901, -0.5410, -0.7492, 0.3113, 1.8470, 0.4270, -1.5640, 0.3467])但是需要注意的是torch.reshape()并不能保证返回的是其拷贝值所以官方不推荐使用。推荐的方法是我们先用 clone() 创造一个张量副本然后再使用 torch.view()进行维度变换。下面是一个例子
cloned_tensor original_tensor.clone() # 创建原始张量的副本
viewed_tensor cloned_tensor.view(16) # 使用view改变形状
print(Cloned Tensor:\n, cloned_tensor)
print(Viewed Tensor:\n, viewed_tensor)# 这样cloned_tensor和viewed_tensor就不会互相影响。Cloned Tensor:tensor([[-0.3983, -0.3365, -1.9220, 0.1157],[ 0.7252, 0.7562, -0.0743, 0.5322],[-0.5094, -0.3373, -1.3409, 0.5753],[ 0.0301, 1.6293, 0.7553, -0.7787]])
Viewed Tensor:tensor([-0.3983, -0.3365, -1.9220, 0.1157, 0.7252, 0.7562, -0.0743, 0.5322,-0.5094, -0.3373, -1.3409, 0.5753, 0.0301, 1.6293, 0.7553, -0.7787])取值操作 如果我们有一个元素 tensor 我们可以使用 .item() 来获得这个 value而不获得其他性质
# 下面的代码演示了如何创建一个随机数张量并获取其类型和单个值的类型。
import torch# 创建一个包含随机数的张量x形状为(1,)
x torch.randn(1) # 打印张量x的值
print(x)# 打印张量x的类型应该是torch.Tensor
print(type(x)) # 使用.item()方法获取张量x中的单个值并打印其类型应该是float
print(type(x.item()))
print(x.item()) tensor([0.9657])
class torch.Tensor
class float
0.9656938910484314#获取所有值作为列表
import torch
x torch.randn(2)
print(x)
print(type(x))
print(x.tolist()) # 获取所有值作为列表
print(type(x.tolist()))tensor([0.1090, 0.1836])
class torch.Tensor
[0.10899410396814346, 0.18355011940002441]
class listPyTorch中的 Tensor 支持超过一百种操作包括转置、索引、切片、数学运算、线性代数、随机数等等具体使用方法可参考官方文档。
2.1.4 广播机制
当对两个形状不同的 Tensor 按元素运算时可能会触发广播(broadcasting)机制先适当复制元素使这两个 Tensor 形状相同后再按元素运算。
numpy广播机制
x torch.arange(1, 3).view(1, 2)
print(x)
y torch.arange(1, 4).view(3, 1)
print(y)
print(x y)
tensor([[1, 2]])
tensor([[1],[2],[3]])
tensor([[2, 3],[3, 4],[4, 5]])由于x和y分别是1行2列和3行1列的矩阵如果要计算xy那么x中第一行的2个元素被广播 (复制)到了第二行和第三行⽽y中第⼀列的3个元素被广播(复制)到了第二列。如此就可以对2个3行2列的矩阵按元素相加。
2.2 自动求导
PyTorch 中所有神经网络的核心是 autograd包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 ( define-by-run 的框架这意味着反向传播是根据代码如何运行来决定的并且每次迭代可以是不同的。 经过本节的学习你将收获
autograd的求导机制梯度的反向传播
2.2.1 Autograd简介
让我们通过一个简单的例子来解释 autograd 中梯度的记录机制。
import torch# 创建叶子节点
x torch.tensor([2.0], requires_gradTrue)
y torch.tensor([3.0], requires_gradTrue)# 构建计算图
z x * y # 中间节点
w z x # 中间节点
loss w ** 2 # 最终的loss节点# 计算梯度
loss.backward()# 检查各个节点的梯度
print(节点值:)
print(fx {x.data}, y {y.data})
print(fz {z.data}, w {w.data})
print(floss {loss.data})print(\n各节点对应的梯度:)
print(fdx {x.grad}) # ∂loss/∂x
print(fdy {y.grad}) # ∂loss/∂y# 查看中间节点的梯度函数
print(\n中间节点的grad_fn:)
print(fz.grad_fn: {z.grad_fn})
print(fw.grad_fn: {w.grad_fn})
print(floss.grad_fn: {loss.grad_fn})# 验证链式法则
# 手动计算梯度进行验证
x_val, y_val 2.0, 3.0
z_val x_val * y_val
w_val z_val x_val
loss_val w_val ** 2# ∂loss/∂w 2w
# ∂w/∂x 1 y
# ∂w/∂y x
# 因此
# ∂loss/∂x ∂loss/∂w * ∂w/∂x 2w * (1 y)
# ∂loss/∂y ∂loss/∂w * ∂w/∂y 2w * xmanual_dx 2 * w_val * (1 y_val)
manual_dy 2 * w_val * x_valprint(\n手动计算的梯度值:)
print(fmanual dx {manual_dx})
print(fmanual dy {manual_dy})节点值:
x tensor([2.]), y tensor([3.])
z tensor([6.]), w tensor([8.])
loss tensor([64.])各节点对应的梯度:
dx tensor([64.])
dy tensor([32.])中间节点的grad_fn:
z.grad_fn: MulBackward0 object at 0x11819d930
w.grad_fn: AddBackward0 object at 0x118116dd0
loss.grad_fn: PowBackward0 object at 0x11819d930手动计算的梯度值:
manual dx 64.0
manual dy 32.02.2.2 autograd中的梯度
autograd中梯度的记录
autograd 记录的确实是 loss 对各个节点的偏导数 什么是节点节点的概念 节点 具体来说是 ∂loss/∂node即最终损失对每个节点的偏导数
import torch# 创建节点 - 叶子节点是需要计算梯度的参数
x torch.tensor([2.0], requires_gradTrue) # 叶子节点
y torch.tensor([3.0], requires_gradTrue) # 叶子节点# 中间节点通过操作自动创建
z x * y # 中间节点建立function节点
w z x # 中间节点建立function节点
loss w ** 2 # 最终输出# 执行反向传播
loss.backward()# 查看各个节点的梯度
print(fx的梯度: {x.grad}) # ∂loss/∂x
print(fy的梯度: {y.grad}) # ∂loss/∂y
print(fz的grad_fn: {z.grad_fn}) # 中间节点存储grad_fn而不是grad
print(fw的grad_fn: {w.grad_fn}) # 中间节点存储grad_fn而不是gradx的梯度: tensor([64.])
y的梯度: tensor([32.])
z的grad_fn: MulBackward0 object at 0x1116cad10
w的grad_fn: AddBackward0 object at 0x1116ca920在 PyTorch 中autograd.Function 是实现自动微分机制的基础。它允许你自定义前向和反向传播的操作从而实现自定义的梯度计算。要理解这个概念首先需要了解几个核心点 Tensor 和 Function 的关系 每个 Tensor 对象都附带了一个计算图。计算图记录了生成该 Tensor 的一系列操作。这些操作通过 Function 节点连接起来。 Function 节点表示操作例如加法、卷积等及其输入的 Tensor在计算图中记录了这些操作的历史。 当你对 Tensor 进行一系列操作如加法、乘法等时PyTorch 会自动为这些操作生成 Function 节点并将这些节点链接起来从而构建一个计算图。 前向传播和反向传播 前向传播Function 的 forward 方法定义了如何计算输出 Tensor。当进行计算时PyTorch 记录所有操作并保存任何需要用于反向传播的中间结果。 反向传播Function 的 backward 方法定义了如何计算梯度。反向传播发生时PyTorch 依赖于这些 Function 节点逐层计算梯度。
梯度的记录方式
对于叶子节点如权重参数梯度存储在 .grad 属性中对于中间节点保存的是梯度函数 grad_fn而不直接存储梯度值grad_fn 记录了如何计算该节点的梯度的方法
import torch# 创建一个简单的神经网络层
layer torch.nn.Linear(2, 1)# 创建输入数据
x torch.tensor([[1.0, 2.0]], requires_gradTrue)# 前向传播
output layer(x)# 检查参数和梯度
print(权重参数:)
print(layer.weight)
print(\n权重的grad_fn:)
print(layer.weight.grad_fn) # None因为是叶子节点
print(\n输出的grad_fn:)
print(output.grad_fn) # 显示计算图的一部分权重参数:
Parameter containing:
tensor([[-0.4196, 0.4725]], requires_gradTrue)权重的grad_fn:
None输出的grad_fn:
AddmmBackward0 object at 0x110ee67a0计算图的构建
前向传播时自动构建计算图每个操作都会创建一个新的 grad_fn这些 grad_fn 连接形成反向传播的路径
import torchdef visualize_graph():x torch.tensor(2.0, requires_gradTrue)y torch.tensor(3.0, requires_gradTrue)# 构建计算图z x * yw z xloss w ** 2# 打印计算图的结构print(计算图结构:)print(floss {loss})print(fgrad_fn {loss.grad_fn})print(f上一步grad_fn {loss.grad_fn.next_functions[0][0]})print(f再上一步grad_fn {loss.grad_fn.next_functions[0][0].next_functions[0][0]})visualize_graph()计算图结构:
loss 64.0
grad_fn PowBackward0 object at 0x11819e3e0
上一步grad_fn AddBackward0 object at 0x11819ea40
再上一步grad_fn MulBackward0 object at 0x11819ea40链式法则的应用
反向传播时通过链式法则自动计算复合函数的导数例如对于路径 x → z → w → loss ∂loss/∂x ∂loss/∂w * ∂w/∂z * ∂z/∂x
import torch# 创建一个需要应用链式法则的例子
x torch.tensor(2.0, requires_gradTrue)# 构建复合函数: f(g(h(x)))
h x ** 2 # h(x) x²
g torch.sin(h) # g(h) sin(h)
f torch.exp(g) # f(g) e^g# 计算梯度
f.backward()# 手动计算梯度进行验证
with torch.no_grad():# 计算每一步的导数dh_dx 2 * x # ∂h/∂x 2xdg_dh torch.cos(h) # ∂g/∂h cos(h)df_dg torch.exp(g) # ∂f/∂g e^g# 应用链式法则manual_grad df_dg * dg_dh * dh_dxprint(f自动计算的梯度: {x.grad})print(f手动计算的梯度: {manual_grad})自动计算的梯度: -1.226664662361145
手动计算的梯度: -1.226664662361145梯度累积特点
如果一个节点被多条路径使用其梯度会被累加例如示例中的 x既直接参与了 w 的计算也通过 z 间接参与了计算
import torch# 重置梯度很重要否则梯度会累积
def demonstrate_grad_accumulation():x torch.tensor(2.0, requires_gradTrue)# 第一次前向传播和反向传播y1 x ** 2z1 torch.sin(y1)z1.backward(retain_graphTrue) # retain_graphTrue 允许多次反向传播print(f第一次反向传播后的梯度: {x.grad})# 不清零梯度进行第二次前向传播和反向传播y2 x ** 3z2 torch.cos(y2)z2.backward()print(f第二次反向传播后的梯度(累积): {x.grad})# 重置梯度后重新计算x.grad.zero_()y2 x ** 3z2 torch.cos(y2)z2.backward()print(f清零后重新计算的梯度: {x.grad})demonstrate_grad_accumulation()# 展示多路径梯度累积
def multiple_paths():x torch.tensor(2.0, requires_gradTrue)# x参与两条计算路径path1 x ** 2path2 x ** 3# 两条路径的结果相加result path1 path2result.backward()print(f多路径累积的梯度: {x.grad})# 梯度将是 ∂(x² x³)/∂x 2x 3x²multiple_paths()第一次反向传播后的梯度: -2.614574432373047
第二次反向传播后的梯度(累积): -14.486873626708984
清零后重新计算的梯度: -11.872299194335938
多路径累积的梯度: 16.0梯度追踪机制
当你设置 requires_gradTrue 时
x torch.tensor([1.0], requires_gradTrue)PyTorch 会记录这个张量参与的所有计算过程相当于给这个张量打开了记录模式
停止追踪计算
方法一使用 .detach()
import torch
# 创建一个需要追踪梯度的张量
tensor torch.tensor([2.0, 3.0], requires_gradTrue)
x tensor.detach() # x不会被追踪计算历史
print(f原始tensor requires_grad: {tensor.requires_grad}) # True
print(fdetach后的x requires_grad: {x.requires_grad}) # False原始tensor requires_grad: True
detach后的x requires_grad: False方法二使用上下文管理器
# 创建一个需要追踪梯度的张量
x torch.tensor([2.0, 3.0], requires_gradTrue)# 使用torch.no_grad()上下文管理器
with torch.no_grad():# 在这个上下文中的计算不会被追踪y x * 2z y ** 2print(fx requires_grad: {x.requires_grad}) # True
print(fy requires_grad: {y.requires_grad}) # False
print(fz requires_grad: {z.requires_grad}) # False
x requires_grad: True
y requires_grad: False
z requires_grad: False简单来说
Tensor 就像一个会记笔记的计算器开启 requires_grad 就是按下记录键进行计算时自动记录每一步最后用 backward() 回看笔记算出所有梯度不想记录时可以按停止键detach 或 no_grad
这样的设计让 PyTorch 能够
自动处理复杂的梯度计算在需要时可以方便地关闭梯度计算比如测试模型时清晰地追踪计算过程方便调试
2.3 Numpy中的数组广播
让我们深入探讨numpy中一个更高级且强大的概念——广播Broadcasting。
广播是一种机制它描述了numpy在进行算术运算时如何处理形状不同的数组。这个概念可能初看起来有些复杂但它实际上非常有用且高效。
广播的核心思想是
在某些特定条件下较小的数组可以被广播到较大的数组上使它们的形状变得兼容。这种机制允许我们对不同形状的数组进行操作而无需显式地复制数据。
广播的主要优势包括
向量化操作它提供了一种高效的方法来进行向量化数组操作。这意味着许多循环操作可以在底层的C语言中进行而不是在Python中从而大大提高了执行速度。内存效率广播不需要复制不必要的数据。相反它通过巧妙的内存访问和计算来实现操作这通常会导致非常高效的算法实现。代码简洁广播可以让我们用更少的代码完成复杂的操作使代码更加简洁和易读。
然而广播也并非在所有情况下都是最佳选择
在某些情况下广播可能会导致内存使用效率低下。例如如果广播操作导致创建了一个非常大的临时数组这可能会显著增加内存使用并降低计算速度。对于非常大的数组或复杂的操作有时显式循环可能更高效。
本文将通过一系列由浅入深的示例来逐步介绍广播的概念和应用。我们将从最简单的情况开始逐渐过渡到更复杂的场景帮助你全面理解广播的工作原理。
此外我们还将提供一些实用的建议帮助你判断何时应该使用广播以及在哪些情况下可能需要考虑其他替代方法。通过这些指导你将能够更好地在效率和代码可读性之间做出权衡选择最适合你特定需求的方法。
numpy操作通常是逐元素进行的这要求两个数组具有完全相同的形状
示例1¶ from numpy import arraya array([1.0, 2.0, 3.0])b array([2.0, 2.0, 2.0])a * b
array([ 2., 4., 6.])当数组的形状满足某些约束时numpy的广播规则放宽了这个限制。最简单的广播示例发生在数组和标量值在操作中结合时
示例2¶ from numpy import arraya array([1.0,2.0,3.0])b 2.0a * b
array([ 2., 4., 6.])结果等同于前面的示例其中b是一个数组。我们可以认为标量b在算术运算过程中被拉伸成一个与a形状相同的数组。如图1所示b中的新元素只是原始标量的副本。拉伸的类比只是概念上的。numpy足够聪明可以使用原始标量值而不实际制作副本因此广播操作在内存和计算效率上都是最优的。因为示例2在乘法过程中移动的内存更少b是标量而不是数组所以在Windows 2000上使用标准numpy对于一百万元素的数组它比示例1快约10%。 图1¶
在广播的最简单示例中标量b被拉伸成与a形状相同的数组因此形状兼容可以进行逐元素乘法。
决定两个数组是否具有兼容形状以进行广播的规则可以用一句话表达。
广播规则
要判断两个张量是否能够进行广播需要遵循广播的规则
1. 如果两个张量的维度不同较小维度的张量会在前面补1直到维度数相同。
2. 然后两个张量从最后一个维度开始比较
• 如果维度相同或者其中一个是1则该维度是兼容的。
• 如果两个维度都不为1并且不相等则无法广播。。
如果不满足这个条件就会抛出ValueError(frames are not aligned)异常表示数组的形状不兼容。广播操作创建的结果数组的大小是输入数组在每个维度上的最大大小。注意这个规则并没有说两个数组需要具有相同数量的维度。因此例如如果你有一个256 x 256 x 3的RGB值数组你想用不同的值缩放图像中的每种颜色你可以将图像乘以一个具有3个值的一维数组。根据广播规则对齐这些数组的尾轴大小可以看出它们是兼容的
图像3d数组256 x256 x3缩放1d数组3结果3d数组256 x256 x3
在下面的示例中A和B数组都有长度为1的轴在广播操作中被扩展到更大的尺寸。
A4d数组8 x1 x6 x1B3d数组7 x1 x5结果4d数组8 x7 x6 x5
下面是几个代码示例和图形表示有助于使广播规则在视觉上变得明显。示例3将一个一维数组添加到一个二维数组
示例3¶ from numpy import array
# a 是一个二维数组, 4行3列a array([[ 0.0, 0.0, 0.0],
... [10.0, 10.0, 10.0],
... [20.0, 20.0, 20.0],
... [30.0, 30.0, 30.0]])
# b 是一个一维数组, 1 * 3b array([1.0, 2.0, 3.0])a b
array([[ 1., 2., 3.],[ 11., 12., 13.],[ 21., 22., 23.],[ 31., 32., 33.]])如图2所示b被添加到a的每一行。当b比a的行长时如图3所示会因形状不兼容而引发异常。 图2¶
如果一维数组元素的数量与二维数组的列数匹配则二维数组乘以一维数组会导致广播。 图3¶
当数组的尾部维度不相等时广播失败因为无法将第一个数组行中的值与第二个数组的元素对齐进行逐元素加法。
广播提供了一种方便的方法来计算两个数组的外积或任何其他外部操作。以下示例展示了两个1-d数组的外部加法操作产生的结果与示例3相同
示例4¶ from numpy import array, newaxisa array([0.0, 10.0, 20.0, 30.0])b array([1.0, 2.0, 3.0])a[:,newaxis] b
array([[ 1., 2., 3.],[ 11., 12., 13.],[ 21., 22., 23.],[ 31., 32., 33.]])这里newaxis索引运算符在a中插入了一个新轴使其成为一个4x1的二维数组。图4说明了两个数组如何被拉伸以产生所需的4x3输出数组。