数据操作 - 基于 pytorch
为了更好地完成我们需求的任务,我们需要一些方法来存储和操作数据。让我们自己动手来处理综合的数据,需要引入N维数组(n-dimensional array)的概念,它也被称为张量(tensor)。
如果你使用过 NumPy,就会发现这篇博客的内容何其相似。其实用什么框架无所谓,张量类型(tensor class)(其中还有 MXNet 的 ndarray
和 PyTorch 和 TensorFlow 中的 Tensor
)都是和 NumPy 的 ndarray
有着非常类似的特征。
首先,GPU 将会在对计算工作起到非常好的加速支持,然而 NumPy 仅支持 CPU 计算;其次, tensor
类型可以自动地支持这两者。这些属性让 tensor
类型更适合深度学习。
首先我们引入库文件
注意我们引入的是 torch
而不是 pytorch
import torch
张量代表了数值数组,对于一个轴,张量代表了数学中的向量(vector),对于两个轴,其代表了矩阵(matrix),对于二个轴以上的张量,没有特别的数学名称。
我们可以先使用 arange
函数创建一个行向量,参数中传递了行向量的数值范围,它们默认是 int64
类型。每一个值被称为向量的元素。除非特别说明,新创建的张量将会存储在内存中并且是基于 CPU 计算。
x = torch.arange(12)
x
我们可以通过 shape
属性来获得张量的形状,即每个轴上的长度
x.shape
如果我们只是想查看张量元素的数量,即 shape
元素的乘积,我们可以使用 numel
方法
x.numel()
12
为了改变张量的形状而不修改其中的值,我们可以使用 reshape
函数,下面的例子中,我们将行向量 x 转换成了一个 (3 imes 4) 的矩阵,返回的新张量内包含的值是相同的;注意,原张量的大小是不变的
X = x.reshape(3, 4)
X
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
对于, reshape
的每个维度是可以不必须指定的,如果目标是一个矩阵,我们只需要知道宽度,高度将会被隐含地给出。可以在参数传递时将 -1
传递在维度的位置表示让张量自动地计算;上面的函数调用还可以写成 x.reshape(3, -1)
或者 x.reshape(-1, 4)
通常我们想让我嫩的矩阵初始化为 0 或 1,其他的一些常数,或者服从某些指定分布的随机数。
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.]]])
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.]]])
通常我们需要使用随机值初始化神经网络,我们可以创建一个张量,其所有元素服从标准高斯(正态)分布,均值为 0,方差为 1
torch.randn(3, 4)
tensor([[ 0.0701, -0.5225, -1.0090, 0.3266],
[-1.2237, 0.1912, 1.7305, -1.3328],
[-1.0946, -0.3725, 0.2093, -0.2414]])
我们还可以将 python 中的列表(list)的中的每一个值用来创建一个张量,下面的例子中,外部的 list 代表 0 轴,里面的 list 代表 1 轴
torch.tensor([[1, 3, 5, 7], [11, 13, 17, 19], [2, 4, 6, 8]])
tensor([[ 1, 3, 5, 7],
[11, 13, 17, 19],
[ 2, 4, 6, 8]])
运算
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 # The ** operator is exponentiation
(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])
如果想要连结(concatenate)多个张量在一起,我们将张量放在列表中传入函数,并且指定按哪个轴连结,比如 axis 0 表示 shape 的第一个元素,axis 1 是第二个元素,以此类推;在下面的例子中,第一个返回值是一个 (6 imes 4) 的矩阵,因为是按照 axis 0 连结,两个张量的 shape 的第一个元素都是 3,所以合并后的 axis 0 的值是 6
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]])
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.]]))
有时候,我们需要通过逻辑语句构造一个二进制值的张量,例如 X == Y
,返回的张量的每一个元素都是 0 或 1
X == Y
tensor([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
计算张量的总和,将返回一个只有一个元素的张量
X.sum()
tensor(66.)
索引和切片
就像其它的 Python 数组对象,张量中的元素可以使用索引访问。通常在 Python 数组中,第一个元素下标从 0 开始到数组长度减 1 的索引。作为标准的 Python 列表,也可以通过元素的相对位置,通过负索引访问最后的元素。
X[-1], X[1:3]
(tensor([ 8., 9., 10., 11.]),
tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
也可以通过索引对元素进行修改
X[1, 2] = 9
X
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]])
如果想要对多个元素分配相同的值,可以简单地使用索引指派。[0:2, :]
表示第 1 行和第 2 行,:
表示 axis 1 (column) 的所有元素。同样地,索引的操作也可以用来向量和多于 2 维的张量上。
X[0:2, :] = 12
X
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
广播机制
在上面的运算符部分,我们可以看到两个相同 shape 的张量可以按照元素对元素(elementwise)的运算符计算得到新的相同 shape 的张量;在某些条件下,即使我们张量的 shape 不同,我们仍然可以通过广播机制(broadcasting mechanism)对两个向量进行按元素对元素运算;它的工作过程如下:首先,将两个张量元素都复制到合适的相同大小,然后将转换后的有着相同 shape 的两个向量进行 elementwise 运算。
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
因为 a
和 b
分别是 (3 imes 1) 和 (1 imes 2) 的矩阵, 它们的 shape 并匹配,如果我们对其进行相加操作,我们将触发广播机制在这两个矩阵上,将其增大为 (3 imes 2) 的矩阵,将矩阵 a
将相应的列和 b
相应的行复制到合适的大小
a + b
tensor([[0, 1],
[1, 2],
[2, 3]])
内存开销
运行操作符将导致在内存中新开辟一部分空间存储新的运行结果,比如我们执行 Y = X + Y
语句,我们将开辟新内存空间,然后将 Y 重新指向这个内存地址。
before = id(Y)
Y = X + Y
id(Y) == before
False
首先,我们不希望不断地在内存中申请非必需的空间,在机器学习中,我们可能有成千上百 M 的参数需要去更新,我们希望在原内存空间中更新这些参数,并且一般将会由多个变量指向相同的参数,如果将参数更新,其它的指针可能还指向原先参数的旧空间地址。
我们可以简单地在内存原地址操作数据,我们可以使用切片符号对之前已经申请了的空间进行更新或使用,例如 Y[:] = <expression>
语句
Z = torch.zeros_like(Y)
print('id(Z): ', id(Z))
Z[:] = X + Y
print('id(Z): ', id(Z))
id(Z): 2122309357184
id(Z): 2122309357184
如果 X 的值在之后无需使用,可以使用 X += Y
语句在内存上操作
before = id(X)
X += Y
id(X) == before
True
转换为其它 Python 对象
可将张量转换为 NumPy 的 ndarray 对象,转换之后的对象是不共享内存的,这使得我们在进行计算时,你不想停止运算,等待着查看 NumPy 包是否想要在相同的内存做其它操作。
A = X.numpy() # 将 tensor 转换为 NumPy 的 ndarray
B = torch.tensor(A) # 将 ndarray 转换为 Tensor
type(A), type(B)
(numpy.ndarray, torch.Tensor)