赞
踩
在深度学习中,通常会频繁地对数据进行操作,在PyTorch中,torch.Tensor
是存储和变换数据的主要工具(Tensor
和NumPy的多维数组非常类似,Tensor
提供GPU计算和自动求梯度等更多功能,使Tensor
更加适合深度学习),以下进行一些汇总:
目录
"tensor"这个单词一般可译作“张量”,张量可以看作是一个多维数组。标量可以看作是0维张量,向量可以看作1维张量,矩阵可以看作是二维张量。
- 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)
在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_()
注意⚠️:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改。
- 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一样 |
用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 | 奇异值分解 |
当对两个形状不同的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]])
索引操作是不会开辟新内存的,而像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(内存地址)并不一致。
用numpy()
和from_numpy()
将Tensor
和NumPy中的数组相互转换。但是需要注意的一点是: 这两个函数所产生的的Tensor
和NumPy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变!!
还有一个常用的将NumPy中的array转换成Tensor
的方法就是torch.tensor()
, 需要注意的是,此方法总是会进行数据拷贝(就会消耗更多的时间和空间),所以返回的Tensor
和原来的数据不再共享内存。
使用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.]
使用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)
用方法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()还可以同时更改数据类型
在深度学习中,经常需要对函数求梯度(gradient)。PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。
Tensor
是这个包的核心类,如果将其属性.requires_grad
设置为True
,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用.backward()
来完成所有梯度计算。此Tensor
的梯度将累积到.grad
属性中。
注意⚠️:在
y.backward()
时,如果y
是标量,则不需要为backward()
传入任何参数;否则,需要传入一个与y
同形的Tensor
。
如果不想要被继续追踪,可以调用.detach()
将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,这样梯度就传不过去了。此外,还可以用with torch.no_grad()
将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True
)的梯度。
Function
是另外一个很重要的类。Tensor
和Function
互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor
都有一个.grad_fn
属性,该属性即创建该Tensor
的Function
, 就是说该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_fn
是None。
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>
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
同形的张量。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。