torch.Tensor常用数据操作汇总与自动求梯度

在深度学习中,通常会频繁地对数据进行操作,在PyTorch中,torch.Tensor是存储和变换数据的主要工具(Tensor和NumPy的多维数组非常类似,Tensor提供GPU计算和自动求梯度等更多功能,使Tensor更加适合深度学习),以下进行一些汇总:

目录

1. Tensor数据操作

1.1 创建Tensor

1.2 算术操作

1.3 索引

1.4 改变形状

2. 广播机制

3. 运算的内存开销

4. Tensor和NumPy相互转换

4.1 Tensor转NumPy

4.2 NumPy数组转Tensor

5. Tensor on GPU

6. 自动求梯度

6.1 概念

6.2 梯度


1. Tensor数据操作

"tensor"这个单词一般可译作“张量”,张量可以看作是一个多维数组。标量可以看作是0维张量,向量可以看作1维张量,矩阵可以看作是二维张量。

1.1 创建Tensor

import torch

# 创建一个5x3的未初始化的Tensor
x = torch.empty(5, 3)
print(x)
# 输出
tensor([[ 0.0000e+00,  1.5846e+29,  0.0000e+00],
        [ 1.5846e+29,  5.6052e-45,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
        [ 0.0000e+00,  1.5846e+29, -2.4336e+02]])

# 创建一个5x3的随机初始化的Tensor
x = torch.rand(5, 3)
print(x)
# 输出
tensor([[0.4963, 0.7682, 0.0885],
        [0.1320, 0.3074, 0.6341],
        [0.4901, 0.8964, 0.4556],
        [0.6323, 0.3489, 0.4017],
        [0.0223, 0.1689, 0.2939]])
# 创建一个5x3的long型全0的Tensor
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
# 输出
tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]])

# 直接根据数据创建
x = torch.tensor([5.5, 3])
print(x)
# 输出
tensor([5.5000, 3.0000])

还可以通过现有的Tensor来创建,此方法会默认重用输入Tensor的一些属性,例如数据类型:

x = x.new_ones(5, 3, dtype=torch.float64)  # 返回的tensor默认具有相同的torch.dtype和torch.device
print(x)
x = torch.randn_like(x, dtype=torch.float) # 指定新的数据类型
print(x) 

# 输出
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]], dtype=torch.float64)
tensor([[ 0.6035,  0.8110, -0.0451],
        [ 0.8797,  1.0482, -0.0445],
        [-0.7229,  2.8663, -0.5655],
        [ 0.1604, -0.0254,  1.0739],
        [ 2.2628, -0.9175, -0.2251]])

通过shape或者size()来获取Tensor的形状:

print(x.size())
print(x.shape)

# 输出
torch.Size([5, 3])
torch.Size([5, 3])

注意⚠️:返回的torch.Size其实就是一个tuple, 支持所有tuple的操作。

常用创建汇总:(官方API:torch.Tensor — PyTorch 2.0 documentation

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)随机排列

Tips:创建方法都可以在创建的时候指定数据类型dtype和存放device(cpu/gpu)

1.2 算术操作

在PyTorch中,同一种操作可能有很多种形式,下面用加法为例:

# 加法1
y = torch.rand(5, 3)
print(x + y)

# 加法2
print(torch.add(x, y))

# 加法3
y.add_(x)  # adds x to y
print(y)

# 以上几种形式的输出均为
tensor([[ 1.3967,  1.0892,  0.4369],
        [ 1.6995,  2.0453,  0.6539],
        [-0.1553,  3.7016, -0.3599],
        [ 0.7536,  0.0870,  1.2274],
        [ 2.5046, -0.1913,  0.4760]])

注:PyTorch操作inplace版本都有后缀_, 例如x.copy_(y), x.t_()

1.3 索引

注意⚠️:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改。

y = x[0, :]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了

# 输出
tensor([1.6035, 1.8110, 0.9549])
tensor([1.6035, 1.8110, 0.9549])
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.4 改变形状

view()来改变Tensor的形状:

y = x.view(15)
z = x.view(-1, 5)  # -1所指的维度可以根据其他维度的值推出来
print(x.size(), y.size(), z.size())

# 输出
torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])

x += 1
print(x)
print(y) # 也加了1

# 输出
tensor([[1.6035, 1.8110, 0.9549],
        [1.8797, 2.0482, 0.9555],
        [0.2771, 3.8663, 0.4345],
        [1.1604, 0.9746, 2.0739],
        [3.2628, 0.0825, 0.7749]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
        1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])

注意⚠️:view()返回的新Tensor与源Tensor虽然可能有不同的size,但是是共享data,也即更改其中的一个,另外一个也会跟着改变。(顾名思义,view仅仅是改变了对这个张量的观察角度,内部数据并未改变)

如果想返回一个真正新的副本(即不共享data内存),Pytorch还提供了一个reshape()可以改变形状,但是此函数并不能保证返回的是其拷贝,所以不推荐使用。推荐先用clone创造一个副本然后再使用view:

x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)

# 输出
tensor([[ 0.6035,  0.8110, -0.0451],
        [ 0.8797,  1.0482, -0.0445],
        [-0.7229,  2.8663, -0.5655],
        [ 0.1604, -0.0254,  1.0739],
        [ 2.2628, -0.9175, -0.2251]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
        1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])

注意⚠️:使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor

另外一个常用的函数就是item(), 它可以将一个标量Tensor转换成一个Python number:

x = torch.randn(1)
print(x)
print(x.item())

# 输出
tensor([2.3466])
2.3466382026672363

PyTorch中的Tensor支持超过一百种操作,包括转置、索引、切片、数学运算、线性代数、随机数等等,可参考: torch.Tensor — PyTorch 2.0 documentation

trace对角线元素之和(矩阵的迹)
diag对角线元素
triu/tril矩阵的上三角/下三角,可指定偏移量
mm/bmm矩阵乘法,batch的矩阵乘法
addmm/addbmm/addmv/addr/baddbmm..矩阵运算
t转置
dot/cross内积/外积
inverse求逆矩阵
svd奇异值分解

2. 广播机制

当对两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor形状相同后再按元素运算

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]])

3. 运算的内存开销

索引操作是不会开辟新内存的,而像y = x + y这样的运算是会新开内存的,然后将y指向新内存:

x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x
print(id(y) == id_before) # False 

如果想指定结果到原来的y的内存,可以使用索引来进行替换操作:

x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y[:] = y + x
print(id(y) == id_before) # True

还可以使用运算符全名函数中的out参数或者自加运算符+=(也即add_())达到上述效果,例如torch.add(x, y, out=y)y += x(y.add_(x)):

x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
torch.add(x, y, out=y) # y += x, y.add_(x)
print(id(y) == id_before) # True

注意⚠️:虽然view返回的Tensor与源Tensor是共享data的,但是依然是一个新的Tensor(因为Tensor除了包含data外还有一些其他属性),二者id(内存地址)并不一致。

4. Tensor和NumPy相互转换

numpy()from_numpy()Tensor和NumPy中的数组相互转换。但是需要注意的一点是: 这两个函数所产生的的Tensor和NumPy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变!!

还有一个常用的将NumPy中的array转换成Tensor的方法就是torch.tensor(), 需要注意的是,此方法总是会进行数据拷贝(就会消耗更多的时间和空间),所以返回的Tensor和原来的数据不再共享内存。

4.1 Tensor转NumPy

使用numpy()Tensor转换成NumPy数组:

a = torch.ones(5)
b = a.numpy()
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)

# 输出
tensor([1., 1., 1., 1., 1.]) [1. 1. 1. 1. 1.]
tensor([2., 2., 2., 2., 2.]) [2. 2. 2. 2. 2.]
tensor([3., 3., 3., 3., 3.]) [3. 3. 3. 3. 3.]

4.2 NumPy数组转Tensor

使用from_numpy()将NumPy数组转换成Tensor:

import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)

# 输出
[1. 1. 1. 1. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
[2. 2. 2. 2. 2.] tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
[3. 3. 3. 3. 3.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)

注意⚠️:所有在CPU上的Tensor(除了CharTensor)都支持与NumPy数组相互转换。

此外上面提到还有一个常用的方法就是直接用torch.tensor()将NumPy数组转换成Tensor,需要注意的是该方法总是会进行数据拷贝,返回的Tensor和原来的数据不再共享内存:

c = torch.tensor(a)
a += 1
print(a, c)

# 输出
[4. 4. 4. 4. 4.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)

5. Tensor on GPU

用方法to()可以将Tensor在CPU和GPU(需要硬件支持)之间相互移动:

# 以下代码只有在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()还可以同时更改数据类型

6. 自动求梯度

在深度学习中,经常需要对函数求梯度(gradient)。PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。

6.1 概念

Tensor是这个包的核心类,如果将其属性.requires_grad设置为True,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用.backward()来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。

注意⚠️:在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor

如果不想要被继续追踪,可以调用.detach()将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,这样梯度就传不过去了。此外,还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True)的梯度。

Function是另外一个很重要的类。TensorFunction互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor都有一个.grad_fn属性,该属性即创建该TensorFunction, 就是说该Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。

x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)

# 输出
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
None


y = x + 2
print(y)
print(y.grad_fn)

# 输出
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>

注意x是直接创建的,所以它没有grad_fn,像x这种直接创建的称为叶子节点,叶子节点对应的grad_fnNone。

print(x.is_leaf, y.is_leaf) # True False

再来点复杂度运算操作:

z = y * y * 3
out = z.mean()
print(z, out)

# 输出
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=<MeanBackward1>)

通过.requires_grad_()来用in-place的方式改变requires_grad属性:

a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)

# 输出
False
True
<SumBackward0 object at 0x118f50cc0>

6.2 梯度

out是一个标量,所以调用backward()时不需要指定求导变量:

out.backward() # 等价于 out.backward(torch.tensor(1.))

out关于x的梯度:

print(x.grad)

# 输出
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

数学上,如果有一个函数值和自变量都为向量的函数,

那么y 关于x的梯度就是一个雅可比矩阵(Jacobian matrix):

torch.autograd这个包就是用来计算一些雅克比矩阵的乘积的。

注意⚠️:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。

# 再来反向传播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)

# 输出
tensor([[5.5000, 5.5000],
        [5.5000, 5.5000]])
tensor([[1., 1.],
        [1., 1.]])

为什么在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor?

简单来说就是为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导

举个例子,假设形状为 m x n 的矩阵 X 经过运算得到了 p x q 的矩阵 Y,Y 又经过运算得到了 s x t 的矩阵 Z。那么按照前面讲的规则,dZ/dY 应该是一个 s x t x p x q 四维张量,dY/dX 是一个 p x q x m x n的四维张量。问题来了,怎样反向传播?怎样将两个四维张量相乘???这要怎么乘???就算能解决两个四维张量怎么乘的问题,四维和三维的张量又怎么乘?导数的导数又怎么求,这一连串的问题,感觉要疯掉…… 为了避免这个问题,我们不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量,举个例子,假设y由自变量x计算而来,w是和y同形的张量,则y.backward(w)的含义是:先计算l = torch.sum(y * w),则l是个标量,然后求l对自变量x的导数(参考:PyTorch 的 backward 为什么有一个 grad_variables 参数? - 知乎

x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)

# 输出
tensor([[2., 4.],
        [6., 8.]], grad_fn=<ViewBackward>)

现在 z 不是一个标量,所以在调用backward时需要传入一个和z同形的权重向量进行加权求和得到一个标量:

v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad)

# 输出
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad)

注意,x.grad是和x同形的张量。 

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值