动手学习深度学习——2.1 数据操作
2.1 数据操作 为了完成任何事情,我们需要某种方式来存储和操作数据。一般来说,我们需要对数据做两件重要的事情:(i)获取数据;(ii)一旦它们进入计算机就进行处理。如果没有存储数据的方法,那么获取数据就没有意义,所以让我们先尝试一下合成数据。首先,我们引入n维数组,也叫张量(tensor)。 如果你曾经用过这个在Python中广泛使用的科学计算包,Numpy,那么你对本章节比较熟悉。不管使用
2.1 数据操作
为了完成任何任务,我们需要某种方式来存储和操作数据。一般来说,我们需要对数据做两件重要的事情:(i)获取数据;(ii)一旦它们进入计算机就进行处理。如果没有存储数据的方法,那么获取数据就没有意义,所以让我们先尝试一下合成数据。首先,我们引入 N
维数组,也叫张量(Tensor),它将是我们本节的主要研究对象。
如果你曾经用过这个在Python
中广泛使用的科学计算包,NumPy
,那么你对本章节会比较熟悉。不管使用哪个框架,它的张量类(MXNet中的ndarray, PyTorch和TensorFlow中的张量)都与NumPy
的ndarray
非常相似。但也有其独有的特性,首先,GPU很好的支持张量的加速计算,而NumPy
只支持CPU加速计算。其次,张量类支持自动微分。这些特性使得张量类非常适合深度学习。贯穿本书,当提到张量时,如果没有特别说明,指的就是张量类的实例。
补充知识点:
- 为了对NumPy数组进行【GPU加速】和【自动微分】,需要借助第三方库【MinPy】,链接为:MinPy对Numpy的GPU加速。
- 一般情况下,Numpy还是多用在CPU端进行数据预处理,配合深度学习框架在GPU端的训练任务。
2.1.1 开始学习
在本节中,我们的目标是让你开始并运行,使您在完成本书的过程中掌握基本的数学和数值计算工具。如果你很难理解一些数学概念或库函数,不要担心。我们会在实际例子的背景下重新讨论些概念,它将逐渐被理解。
为了开始,我们导入 torch 库。值得注意的是,虽然我们称之为 pytorch,但是我们应该导入 torch,代码如下:
import torch
张量表示一组(可能是多维的)数值。张量只有一个轴(Axis)时,数学上称为向量。两个轴时,张量与矩阵相关。当张量的轴大于2时,数学上没有对应的名字。
为了开始,我们可以使用 【torch.arange()】函数创建一个行向量a
,包含12个整数,以0
为开始值,但是默认的数据类型是浮点型 。张量的每一个值称为张量的一个元素。比如,张量a
有12个元素。除非特别指定,否则,新的张量被存在主内存(main memory),并且默认是在CPU
上计算。
如何使用【Pytorch】库创建1-D张量?
-
函数原型:
torch.arange(start, end, step)
功能:生成一个左闭右开的
1-D
张量,[start,end),长度为 [ e n d − s t a r t s t e p ] [\frac{end-start}{step}] [stepend−start].
参数解释:
start:向量从哪个数字开始,默认从0
开始。
end:向量以哪个数字结束,需要指定具体的值。
step:向量2个元素之间的步长,默认值为1
。
官方API文档:torch.arange,可以了解更为详细的参数介绍。函数实例:
import torch # 单个输入参数,默认:end=12 a = torch.arange(12) print('a: ', a) print('dtype of a: ', a.dtype) print('type of a: ', type(a)) # 手动设置非0的开始值 b = torch.arange(1, 12) print('b: ', b) # 设置浮点型输入参数 c = torch.arange(1, 2.5, 0.5) print('c: ', c) print('dtype of c: ', c.dtype) print('type of c: ', type(c))
实例运行结果:
a: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) dtype of a: torch.int64 type of a: <class 'torch.Tensor'> b: tensor([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) c: tensor([1.0000, 1.5000, 2.0000]) dtype of c: torch.float32 # 数据类型与输入参数类型保持一致 type of c: <class 'torch.Tensor'>
原书错误校正:
从上面例子的输出可以看出,使用函数
torch.arange()
默认创建的张量,返回张量的数据类型与输入参数的数据类型保持一致或者默认为torch.int64
,默认类型并不是浮点型。 -
那么,张量的数据类型如何确定呢?
a. 手动指定数据类型的输入参数,比如,
torch.arange(12, dtype=torch.float32)
.
b. 与输入参数的数据类型保持一致,参考上面的例子。
上面我们简单创建了1-D张量。为了进一步认识张量,我们将探讨张量有哪些【基本属性】?
-
张量的数据类型(
torch.dtype
)
在上面创建1-D张量的例子中,我们可以通过以下代码打印张量的数据类型,a.dtype
Pytorch框架中,总共有
12
种数据类型(实际应用中,常用的并没有那么多),具体如下表所示:DataType dtype Legacy Constructors 32-bit floating point
torch.float32
ortorch.float
torch.*.FloatTensor
64-bit floating point
torch.float64
ortorch.double
torch.*.FloatTensor
64-bit complex
torch.complex64
ortorch.cfloat
128-bit complex
torch.complex128
ortorch.cdouble
16-bit floating point
torch.float16
ortorch.half
torch.*.HalfTensor
16-bit floating point
torch.bfloat16
torch.*.BFloat16Tensor
8-bit integer (unsigned)
torch.uint8
torch.*.ByteTensor
8-bit integer (signed)
torch.int8
torch.*.CharTensor
16-bit integer (signed)
torch.int16
ortorch.short
torch.*.ShortTensor
32-bit integer (signed)
torch.int32
ortorch.int
torch.*.IntTensor
64-bit integer (signed)
torch.int64
ortorch.long
torch.*.LongTensor
Boolean
torch.bool
torch.*.BoolTensor
-
张量的存储设备(
torch.device
)
对象torch.device
表示torch.Tensor
创建的张量被分配在什么设备(device
)。torch.device
包含设备类型为('cpu','cuda')
以及每一种设备的可选序号,比如'cuda:0,cuda:1'
等等。在实际中,应该如何为张量分配设备呢?下面给出两个简单的例子,
实例1:使用字符串指定张量的存储设备
# Example of a function that takes in a torch.device cuda1 = torch.device('cuda:1') torch.randn((2,3), device=cuda1)
# You can substitute the torch.device with a string torch.randn((2,3), device='cuda:1')
实例2:由于历史遗留的原因,可以通过单个设备序号获取设备,这只适用cuda设备,并不支持cpu设备。
torch.device(1) # device(type='cuda', index=1)
-
张量的内存布局(
torch.layout
)
下图是从官方文档中截取,关于内存布局属于测试版本,后面的版本中可能变化。
官方实例如下:首先,张量的形状为(2,5),x.stride()
函数的输出为(5,1),5 表示在第0轴的维度中,从一个内存数据块跳到下一个内存块需要跨越5步,同理 1 表示在第一轴的维度中,从一个数据跳跃到下一个数据需要1步。看上去,跟C++的存、取方式类似。>>> x = torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) >>> x.stride() (5, 1) >>> x.t().stride() (1, 5)
这里的解释比较简单,目的是为了先大概了解,后续的教程中,我们会进行更加细致的解释。
如果想进一步理解张量内存存储的内容,可以自己查阅资料,或者参考补充材料:https://zhuanlan.zhihu.com/p/345773178
-
张量的大小
我们可以通过检查张量的形状属性来访问它的形状(沿每个轴的长度),代码如下:
print(a.shape) # 输出如下 torch.Size([12])
-
张量中元素的个数
如果我们只想知道一个张量中元素的总数,也就是所有形状元素的乘积,我们可以检查它的大小。因为我们在这里处理的是一个向量,形状的单个元素与张量的大小是相同的,具体代码如下:
print(a.numel()) # 输出如下 12
-
张量元素求和
print(a.sum()) # 输出如下 tensor(66)
张量有哪些常见的操作?
-
改变张量的形状
借助
reshape()
函数,我们可以更改张量的形状。例如,我们可以将张量a
从形状为(12,)
的行向量转换为形状为(3,4)
的矩阵。这个新的张量包含完全相同的值,但将它们被视为3
行4
列的矩阵。重申一下,虽然形状变了,但元素的值没有变。# reshape A = a.reshape(3, 4) print(A) # 输出 tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
注意:通过手动指定
reshape
函数的每个维度是不必要的。如果我们的目标形状是一个带有形状(高度,宽度)的矩阵,那么在我们知道宽度后,高度是隐式给出的。为什么我们要自己做除法呢?在上面的例子中,为了得到一个3行矩阵,我们指定它应该有3行和4列。幸运的是,张量可以自动算出剩下的一维。我们通过为我们希望张量自动推断的维度放置-1
来调用这个功能。在本例中,不是调用a.reshape(3,4)
,而是等价地调用a.reshape(- 1,4)
或a.reshape(3, -1)
。 -
矩阵张量的初始化
通常,我们希望用0、1、其他常数或从特定分布中随机抽样的数字初始化矩阵。我们可以创建一个张量,它的所有元素都为0,形状为(2,3,4)
,如下所示:# 张量初始化 x_zero = torch.zeros(2,3,4) print(x_zero) # 输出: 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.]]])
类似地,生成全
1
的张量,代码如下:x_ones = torch.ones(2,3,4) print(x_ones) # 输出 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.]]])
通常,我们想从特定概率分布中随机抽取张量中每个元素的值。例如,当我们构造数组作为神经网络的参数时,我们通常会随机初始化它们的值。下面的代码片段创建了一个形状为
(3,4)
的张量。它的每个元素都是从均值为0
、标准差为1
的标准高斯(正态)分布中随机采样。# 标准正态分布 x_rand = torch.rand(3,4) print(x_rand) # 输出 tensor([[0.3960, 0.5114, 0.9809, 0.8033], [0.5834, 0.0358, 0.4234, 0.2559], [0.5354, 0.4384, 0.8155, 0.5033]])
我们还可以通过提供包含数值的Python列表(或列表的列表)来指定所需张量中每个元素的精确值。在这里,最外面的列表对应于轴0,内部列表对应于轴1。代码如下:
# list 初始化 x_list = torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) print(x_list) # 输出 tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
2.1.2 张量运算符
本书并不是软件工程,不仅仅局限于简单的进行数组的写入和写出,我们希望将一些数学运算符作用于数组。一些最简单和最有用的操作是元素级的运算。它们将标准的标量操作应用于数组的每个元素。对于接受两个数组作为输入的函数,元素操作(elementwise operations)指的是对这两个数组中的每一对对应元素应用一些标准二元操作符。我们可以从任意函数创建一个从标量映射到标量的元素型函数。
在数学的概念中,一些定义如下:
- 一元标量运算:将实数映射为实数,记为, f : R → R f:R\rightarrow R f:R→R
- 二元标量运算:输入为两个实数,输出为一个实数,记为, f : R , R → R f:R,R\rightarrow R f:R,R→R
- 向量的运算:给定两个向量以及二元运算符, u , v u,v u,v, c i ← f ( u i , v i ) , i = 0 , 1 , 2 , . . . c_i\leftarrow f(u_i,v_i),i=0,1,2,... ci←f(ui,vi),i=0,1,2,...,可以得到向量
c = F ( u , v ) c=F(u,v) c=F(u,v)。整个过程可以记为, F : R d , R d → R d F:R^d,R^d\rightarrow R^d F:Rd,Rd→Rd,称为向量上的元素级运算。
张量之间的基本运算有哪些?
-
基本运算
下面,我们将常见的、标准算术运算符 (+, -, *, /, 和 **)作用于具有任意相同输入大小的两个张量。# 基本运算符 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(幂运算) print(x+y) print(x-y) print(x*y) print(x/y) print(x**y) # 输出如下 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.])
还有更多的元素级操作,包括像指数运算这样的一元操作符,函数如下:
x_exp = torch.exp(x) print(x_exp) # 输出如下 tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])
-
合并运算
我们可以将多个张量合并为一个大的张量,神经网络模型中,这是非常常见的操作。值得注意的是,未参与合并的轴的形状必须一样,具体例子如下:# tensor 合并操作 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]]) Z = torch.rand(2, 3) XY0 = torch.cat((X, Y), dim=0) XY1 = torch.cat((X, Y), dim=1) print(XY0) print(XY0.shape) print(XY1) print(XY1.shape) # 输出如下 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.]]) torch.Size([6, 4]) 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.]]) torch.Size([3, 8])
举例说明,非合并轴的形状不一样会报错,代码如下:
XZ = torch.cat((X,Z)) print(XZ) # 报错如下 RuntimeError: Sizes of tensors must match except in dimension 0. Expected size 4 but got size 3 for tensor number 1 in the list.
-
逻辑运算
有时,我们想通过逻辑运算来构造一个二值张量。以X == Y
为例。对于每个位置,如果X和Y在该位置相等,新张量中对应的项取1
,这意味着逻辑命题X == Y
在该位置为真;否则这个位置是0
。代码如下:# 逻辑运算 print(X==Y) # 输出如下 tensor([[False, True, False, True], [False, False, False, False], [False, False, False, False]])
2.1.3. 广播机制
在上一节中,我们看到了如何对两个相同形状的张量进行元素级运算。在某些条件下,即使形状不同,我们仍然可以通过调用广播机制来执行按元素执行的操作。
-
广播机制的过程
该机制的执行方式分为两步:
首先,通过适当复制元素来展开一个或两个数组,这样在转换之后,两个张量具有相同的形状。
其次,对结果数组执行按元素顺序的操作。举例如下:
# 广播机制 x_broad = torch.arange(3).reshape((3, 1)) y_broad = torch.arange(2).reshape((1, 2)) print(x_broad) print(x_broad.shape) print(y_broad) print(y_broad.shape) # z_xy = x_broad+y_broad print(z_xy) print(z_xy.shape) # 输出如下 tensor([[0], [1], [2]]) torch.Size([3, 1]) tensor([[0, 1]]) torch.Size([1, 2]) ======================== # 广播机制的具体实例过程如下: 首先,x_broad扩展为如下形式: tensor([[0, 0], [1, 1], [2, 2]]) torch.Size([3, 2]) 其次,y_broad扩展为如下形式: tensor([[0, 1], [0, 1], [0, 1]]) torch.Size([3, 2]) 最后,形状一样,可以直接加法, tensor([[0, 1], [1, 2], [2, 3]]) torch.Size([3, 2])
-
广播机制之后的张量形状
广播之后的张量形状应该是所有张量对应维度的最大者。
# 示例6:可广播 f = torch.empty(1) g = torch.empty(3, 1, 7) print((f + g).size()) # 输出如下 torch.Size([3, 1, 7])
-
广播机制的触发条件
可广播的一对张量需满足以下规则:
a. 每个张量至少有一个维度。
b. 迭代维度尺寸时,从尾部的维度开始,维度尺寸
或者相等,
或者其中一个张量的维度尺寸为 1 ,
或者其中一个张量不存在这个维度。
补充材料,参考链接:https://zhuanlan.zhihu.com/p/86997775.# m 和 n 可广播 m = torch.empty(5, 3, 4, 1) n = torch.empty( 3, 1, 1) # 倒数第一个维度:两者的尺寸均为1 # 倒数第二个维度:n尺寸为1 # 倒数第三个维度:两者尺寸相同 # 倒数第四个维度:n该维度不存在 print((m+b).size()) # torch.Size([5, 3, 4, 1]) # 示例4:不可广播,因为倒数第三个维度:2 != 3 p = torch.empty(5, 2, 4, 1) q = torch.empty( 3, 1, 1) print(p+q) # 输出错误如下 RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 1
2.1.4. 索引和切片
就像在Python中的数组一样,张量中的元素可以通过索引访问。与在任何Python数组中一样,第一个元素的索引为0,并且指定范围以包括第一个但在最后一个元素之前的元素。就像在标准Python列表中一样,我们可以使用负索引来根据元素在相对列表末尾的位置来访问元素。
# X:
# tensor([[ 0., 1., 2., 3.],
# [ 4., 5., 6., 7.],
# [ 8., 9., 10., 11.]])
print(X[-1]) # 打印张量X的最后一个元素
print(X[1:3]) # 打印第二和第三个元素
# 输出如下
tensor([ 8., 9., 10., 11.])
tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
除了读取特定位置元素,也可以通过索引修改特定位置元素,
print(X)
X[1,2]=9
print(X)
# 输出如下
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]])
如果我们想给多个元素赋相同的值,我们只需要索引所有元素,然后给它们赋值。例如,[0:2,:]访问第一行和第二行,其中 :
符号表示获取沿1轴(列)的所有元素。当我们讨论矩阵的索引时,这显然也适用于向量和二维以上的张量。
X[0:2, :] = 12
print(X)
# 输出
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
2.1.5. 节省内存
运行操作运算符可能导致新的内存分配给运行结果。例如,如果我们写 Y = X + Y
,我们将解引用Y曾经指向的张量,而将Y指向新分配的内存。在下面的例子中,我们用Python的id()
函数来演示,该函数给出了被引用对象在内存中的确切地址。运行Y = Y + X后,我们将发现id(Y)
指向不同的位置。这是因为Python首先计算Y + X
,为结果分配新的内存,然后使Y指向内存中的这个新位置。比如下面的例子,
before = id(Y)
Y = Y + X
print(before)
print(id(Y))
# 输出如下
140366208360832
140363802047808
上述的方式可能不是很合适,有两个原因:首先,我们不希望总是在不必要的情况下分配内存。在机器学习中,我们可能有数百兆字节的参数,并在每秒内多次更新它们。通常,我们希望在适当的地方执行更新操作,而不是重新分配。其次,我们可能从多个变量指向相同的参数。如果我们不及时更新,其他引用仍然会指向旧的内存位置,这使得部分代码可能无意中引用陈旧的参数。
幸运的是,执行就地(in-place)操作很容易。我们可以用切片表示法将操作的结果分配给先前分配的数组,例如,Y[:] = <表达式>
。为了说明这个概念,我们首先创建一个新的矩阵Z,其形状与另一个Y相同,使用zeros_like
函数分配一个包含0的块。
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
# 输出如下
id(Z): 140004298247760
id(Z): 140004298247760
如果X的值在后续的计算中没有被重用,我们也可以使用X[:] = X + Y或X += Y
来减少操作的内存开销。
before = id(X)
X += Y # 注意 不能 X = X + Y
print('id(X):', before)
print('id(X):', id(X))
# 输出
id(X): 139652101369664
id(X): 139652101369664
2.1.6. 转为其它Python对象
将NumPy张量(ndarray)转换为NumPy张量(ndarray),这很容易。torch张量和numpy数组将共享它们的底层内存位置,通过就地操作改变其中一个也会改变另一个。
# 数据转换
print('X: ', X)
A = X.numpy()
B = torch.from_numpy(A)
print(type(A), type(B))
print('================================')
print(A)
print(B)
print('================================')
A[1:2,:] = 13
print('改变后A的值,X的值是否会变化?')
print(A)
print(X)
# 输出如下
X: tensor([[26., 25., 28., 27.],
[25., 26., 27., 28.],
[20., 21., 22., 23.]])
<class 'numpy.ndarray'> <class 'torch.Tensor'>
================================
[[26. 25. 28. 27.]
[25. 26. 27. 28.]
[20. 21. 22. 23.]]
tensor([[26., 25., 28., 27.],
[25., 26., 27., 28.],
[20., 21., 22., 23.]])
================================
改变后A的值,X的值是否会变化?
[[26. 25. 28. 27.]
[13. 13. 13. 13.]
[20. 21. 22. 23.]]
# 上面NumPy数组A的第二行被改变,张量X的第二行也发生改变,表明它们共享相同的内存
tensor([[26., 25., 28., 27.],
[13., 13., 13., 13.],
[20., 21., 22., 23.]])
要将大小为1的张量转换为Python标量,可以调用item
函数或Python的内置函数。
a = torch.tensor([3.5])
print(a, a.item(), float(a), int(a))
# 输出
tensor([3.5000]) 3.5 3.5 3

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)