文章目录
张量的简要介绍
在Pytorch
中,张量(torch.Tensor
)是最基本的数据结构,其重要性可类比于Spark
中的rdd
,Numpy
中的ndarray
,Pandas
中的DataFrame
。
张量和ndarray
有很多异曲同工之处,如索引、线性代数、广播机制等。不过,张量额外提供了GPU计算 自动求梯度等功能,更加适用于深度学习。
如何创建一个张量
创建张量的方法有很多,且类似于Numpy
中创建ndarray
。下面举几个常用的创建方式:
创建一个空的张量
import torch
x = torch.empty(3, 5)
print(x)
tensor([[ 0.0000e+00, -2.5244e-29, -6.4268e-30, -8.5920e+09, 1.1210e-44],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00]])
返回填充了未初始化数据的张量。张量的形状为3*5。
创建一个0张量
x = torch.zeros(3, 5)
print(x)
tensor([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
创建一个随机张量
x = torch.rand(5, 3)
print(x)
tensor([[0.4105, 0.4467, 0.9016],
[0.6860, 0.0143, 0.0723],
[0.2301, 0.2465, 0.8574],
[0.9940, 0.3832, 0.1964],
[0.3754, 0.0219, 0.3208]])
根据现有数据创建Tensor
array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
x = torch.tensor(array)
print(x)
tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
根据现有Tensor
创建Tensor
该方法会重用输入Tensor
的一些属性(torch.dtype、torch.device)
x = torch.tensor([1, 2, 3], dtype=torch.long)
y = x.new_ones(5, 3)
z = torch.rand_like(x, dtype=torch.float)
print(x)
print(y)
print(z)
tensor([1, 2, 3])
tensor([[1, 1, 1],
[1, 1, 1],
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]])
tensor([0.7695, 0.3723, 0.5159])
更多创建Tensor的方法
张量的属性
tensor.dtype
torch.dtype
属性标识了 torch.Tensor
的数据类型。PyTorch 有八种不同的数据类型:
tensor.device
x = torch.tensor([1, 2, 3])
print(x.device)
device(type='cpu')
[torch.device](https://pytorch.apachecn.org/docs/1.0/tensor_attributes.html#torch.torch.device)
属性标识了[torch.Tensor](https://pytorch.apachecn.org/docs/1.0/tensors.html#torch.Tensor)
对象在创建之后所存储在的设备名称,而在对象创建之前此属性标识了即将为此对象申请存储空间的设备名称。
[torch.device](https://pytorch.apachecn.org/docs/1.0/tensor_attributes.html#torch.torch.device)
包含了两种设备类型 ('cpu'
或者 'cuda'
) ,分别标识将Tensor对象储存于cpu内存或者gpu内存中,同时支持指定设备编号,比如多张gpu,可以通过gpu编号指定某一块gpu。 如果没有指定设备编号,则默认将对象存储于current_device()当前设备中; 举个例子, 一个[torch.Tensor](https://pytorch.apachecn.org/docs/1.0/tensors.html#torch.Tensor)
对象构造函数中的设备字段如果填写'cuda'
,那等价于填写了'cuda:X'
,其中X是函数 [torch.cuda.current_device()](https://pytorch.apachecn.org/docs/1.0/cuda.html#torch.cuda.current_device)
的返回值。
在[torch.Tensor](https://pytorch.apachecn.org/docs/1.0/tensors.html#torch.Tensor)
对象创建之后,可以通过访问[Tensor.device](https://pytorch.apachecn.org/docs/1.0/tensors.html#torch.Tensor.device)
属性实时访问当前对象所存储在的设备名称。
[torch.device](https://pytorch.apachecn.org/docs/1.0/tensor_attributes.html#torch.torch.device)
对象支持使用字符串或者字符串加设备编号这两种方式来创建:
# 通过字符串创建:
>>> torch.device('cuda:0')
device(type='cuda', index=0) # 编号为0的cuda设备
>>> torch.device('cpu') # cpu内存
device(type='cpu')
>>> torch.device('cuda') # 当前cuda设备
device(type='cuda')Copy
# 通过字符串加设备编号创建:
>>> torch.device('cuda', 0)
device(type='cuda', index=0)
>>> torch.device('cpu', 0)
device(type='cpu', index=0)
当torch.device作为函数的参数的时候, 可以直接用字符串替换。 这样有助于加快代码创建原型的速度:
>>> # 一个接受torch.device对象为参数的函数例子
>>> cuda1 = torch.device('cuda:1')
>>> torch.randn((2,3), device=cuda1)
>>> # 可以用一个字符串替换掉torch.device对象,一样的效果
>>> torch.randn((2,3), 'cuda:1')
以下的操作是等价的:
torch.randn((2,3), device=torch.device('cuda:1'))
torch.randn((2,3), device='cuda:1')
torch.randn((2,3), device=1) # 历史遗留做法
tensor.layout
[torch.layout](https://pytorch.apachecn.org/docs/1.0/tensor_attributes.html#torch.torch.layout)
属性标识了[torch.Tensor](https://pytorch.apachecn.org/docs/1.0/tensors.html#torch.Tensor)
在内存中的布局模式。 现在, 我们支持了两种内存布局模式:
torch.strided
(dense Tensors)- 尚处试验阶段的
torch.sparse_coo
(sparse COO Tensors, 一种经典的稀疏矩阵存储方式)
torch.strided
跨步存储代表了密集张量的存储布局方式,当然也是最常用最经典的一种布局方式。 每一个strided tensor都有一个与之相连的torch.Storage
对象, 这个对象存储着tensor的数据. 这些Storage对象为tensor提供了一种多维的, 跨步的(strided)数据视图. 这一视图中的strides是一个interger整形列表:这个列表的主要作用是给出当前张量的各个维度的所占内存大小,严格的定义就是,strides中的第k个元素代表了在第k维度下,从一个元素跳转到下一个元素所需要跨越的内存大小。 跨步这个概念有助于提高多种张量运算的效率。
x = torch.tensor([1, 2, 3, 4, 5, 6, 7])
print(x.stride())
(1,)
x = torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
print(x.stride())
(5, 1)
(5, 1)的意思是:此时在这个二维张量中,在第0维度下,从一个元素到下一个元素需要跨越的内存大小是5,比如x[0] 到x[1]需要跨越x[0]这5个元素, 在第1维度下,是1,如x[0, 0]到x[0, 1]需要跨越1个元素
更多关于 torch.sparse_coo tensors
的信息, 请看 [torch.sparse](https://pytorch.apachecn.org/docs/1.0/sparse.html#sparse-docs)
tensor.shape
通过shape
或者size()
来获取Tensor
的形状:
x = torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
print(x.shape)
print(x.size())
torch.Size([2, 5])
torch.Size([2, 5])
张量的算术操作
加法
在PyTorch中,同一种操作可能有很多种形式
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
print(x + y)
print(torch.add(x, y))
y.add_(x)
print(y)
以上三种方法的输出结果都是:
tensor([5, 6])
注:PyTorch操作inplace版本都有后缀_
, 例如x.copy_(y), x.t_()
其他算术操作
- 减法:
x - y
torch.sub(x, y)
- 乘法:
x * y
torch.mul(x, y)
- 除法:
x / y
torch.div(x, y)
张量的索引
我们还可以使用类似NumPy的索引操作来访问Tensor
的一部分,
需要注意的是:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
x[0, 1]
Out[46]: tensor(2)
x[0, 1:3]
Out[47]: tensor([2, 3])
x[0, 1:10]
Out[48]: tensor([2, 3])
x[0, :]
Out[49]: tensor([1, 2, 3])
x[:, 1]
Out[50]: tensor([2, 5])
除了常用的索引选择数据之外,PyTorch还提供了一些高级的选择函数:
改变张量的形状 & 拷贝张量
用view()
来改变Tensor
的形状:
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
y = x.view(6)
z = x.view(3, -1) # -1所指的维度可以根据其他维度的值推出来
print(x.size(), y.size(), z.size())
torch.Size([2, 3]) torch.Size([6]) torch.Size([3, 2])
注意view()
返回的新Tensor
与源Tensor
虽然可能有不同的size
,但是是共享data
的,也即更改其中的一个,另外一个也会跟着改变。(顾名思义,view仅仅是改变了对这个张量的观察角度,内部数据并未改变)
x+=1
x
Out[53]:
tensor([[2, 3, 4],
[5, 6, 7]])
y
Out[54]: tensor([2, 3, 4, 5, 6, 7])
z
Out[55]:
tensor([[2, 3],
[4, 5],
[6, 7]])
所以如果我们想返回一个真正新的副本(即不共享data内存)该怎么办呢?Pytorch还提供了一个reshape()
可以改变形状,但是此函数并不能保证返回的是其拷贝,所以不推荐使用。推荐先用clone
创造一个副本然后再使用view
。参考此处
x_cp = x.clone().view(-1)
x_cp
Out[58]: tensor([2, 3, 4, 5, 6, 7])
使用 clone
还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源 Tensor
张量的线性代数运算
PyTorch中的Tensor
支持超过一百种操作,包括转置、索引、切片、数学运算、线性代数、随机数等等,可参考官方文档
张量的广播机制
前面我们看到如何对两个形状相同的Tensor
做按元素运算。当对两个形状不同的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]])
由于x
和y
分别是1行2列和3行1列的矩阵,如果要计算x + y
,那么x
中第一行的2个元素被广播(复制)到了第二行和第三行,而y
中第一列的3个元素被广播(复制)到了第二列。如此,就可以对2个3行2列的矩阵按元素相加。
节省张量计算时的内存开销
前面说了,索引操作是不会开辟新内存的,而像y = x + y
这样的运算是会新开内存的,然后将y
指向新内存。为了演示这一点,我们可以使用Python自带的id
函数:如果两个实例的ID一致,那么它们所对应的内存地址相同;反之则不同。
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x
id_after = id(y)
print(id_before, id_after)
140343323861952 140343323774464
以下三种方式可以节省内存:
y[:] = y + x
y += x
ory.add_(x)
torch.add(x, y, out=y)
注:虽然 view
返回的 Tensor
与源Tensor
是共享 data
的,但是依然是一个新的Tensor
(因为Tensor
除了包含data
外还有一些其他属性),二者id(内存地址)并不一致。
张量、numpy和list
x = torch.tensor([1, 2])
# 张量转numpy
x.numpy()
Out[68]: array([1, 2])
# 张量转list
x.tolist()
Out[69]: [1, 2]
# numpy转张量
array = np.array([1, 2])
x = torch.from_numpy(array) # 返回的Tensor和原来的数据共享内存
y = torch.tensor(array) # torch.tensor方法总是会进行数据拷贝,返回的`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()还可以同时更改数据类型