第三章主要介绍Pytorch中最基本的数据结构——tensor的使用方法:创建、运算、存储、与Numpy的互操作性等。

Chapter 3 It starts with a tensor

  • 简介
  • Tensor的定义
  • 索引
  • 命名
  • 类型
  • 存储
  • 元数据和连续性
  • 设备
  • 与Numpy的互操作性
  • 序列化
  • 练习

简介
  1. 神经网络通常是分阶段地学习数据从一种表达到另一种表达的转换,这意味着每个阶段的转换数据可以被看做是中间表达序列。例如在图像识别中,浅层的表达可以是边缘或者特定的纹理,而深层的表达可以捕获更加复杂的结构,如耳朵、鼻子和眼睛。
     * 神经网络的中间表达其实是一系列浮点数的集合–>刻画输入,并捕获数据的结构
     * 当前层的中间表达其实是输入和之前层神经元权重组合的结果。
  2. PyTorch如何处理和存储数据?引入了基本的数据结构 tensor
    tensor: 任意维度向量和矩阵的统称,可以理解为 多维数组
    与numpy的arrays相比,tensor的特点:
     * 可以在GPU上执行快速计算;
     * 可以将运算分配到多个设备或机器上;
     * 跟踪图的计算过程。
Tensor的定义

PyTorch中的基本数据结构–>tensor,使用tensor可以表达表达许多类型的数据,例如:图像、时间序列、语句等。

import torch    # 导入torch包
"""1. 创建一维tensor"""
# 创建一维tensor, size = 3, value = 1
a = torch.ones(3) 
print(a)

# 使用下标访问其中的元素
a[0]   

# 更改tensor的值
a[1] = 2   
a
# 输出结果:
tensor([1., 1., 1.])
tensor([1., 2., 1.])
  • python中的list和tuple:列表和元组中的每一个元素以Python对象的形式被单独分配在内存空间中;
  • PyTorch中的tensor和Numpy中的arrays:连续分配在内存块中,每个元素占32bit(4 byte)。
     
"""2. 创建二维tensor"""
# 传入python list
points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])   
print(points)

# 获取tensor的shape
print(points.shape)

# 使用两个索引访问单独的元素-->返回的是tensor
print(points[1, 1])

# 访问第一个元素
print(points[0])
# 输出结果:
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]])
torch.Size([3, 2])
tensor(3.)
tensor([4., 1.])

 

索引
points[1:]  # 从第一个元素开始到末尾
# 输出结果
tensor([[5., 3.],
        [2., 1.]])
points[1:, :] # 从第一行到所有的行,对应所有的列
# 输出结果
tensor([[5., 3.],
        [2., 1.]])
points[1:, 0] # 从第一行到所有的行,每行取第0个元素
# 输出结果
tensor([5., 2.])
points[None]  # 增加一个维度,与unsqueeze()功能相同
tensor([[[4., 1.],
         [5., 3.],
         [2., 1.]]])
命名

以将彩色图像转化为灰度图像为例
适用于PyTorch1.3以上的版本(作为experimental feature)

# 1.定义图片和权重
img_t = torch.randn(3, 5, 5)  # shape [channels, rows, column]
batch_t = torch.randn(2, 3, 5, 5) # shape [batch, channels, rows, column]
weights = torch.tensor([0.2126, 0.7152, 0.0722])
# 2.在channels维度(总是位于倒数第三个位置)上计算平均值
image_gray_naive = img_t.mean(-3)
batch_gray_navie = batch_t.mean(-3)
# 3.对RGB三个通道加权
unsqueezed_weights = weights.unsqueeze(-1).unsqueeze_(-1) # 调整weights的shape为3×1×1
image_weights = img_t * unsqueezed_weights	# 使用了广播机制
image_gray_weights = image_weights.sum(-3)	# 在channels维度上加和

batch_weights = batch_t * unsqueezed_weights
batch_gray_weights = batch_weights.sum(-3)

开始给tensor命名

# 4.在定义tensor时传入names参数,给tensor命名
weights_named = torch.tensor([0.2126, 0.7152, 0.0722], names=['channels'])
weights_named.names
# 输出结果
('channels',)
# 5.使用.refine_names()为已经定义的tensor添加name
img_named = img_t.refine_names(..., 'channel', 'row', 'column')
batch_named = batch_t.refine_names('batch', 'channel', 'row', 'column')
weights_named = weights.refine_names('channel')
img_named.names, batch_named.names, weights_named.names
# 输出结果
(('channel', 'row', 'column'),
 ('batch', 'channel', 'row', 'column'),
 ('channel',))
# 6.使用align_as()显示地对齐两个tensor,并将维度按照正确的顺序排列
weights_aligned = weights_named.align_as(img_named)
print(weights_aligned.names)
# 接收维度参数的函数也可以接收对应的name
gray_named = (img_named * weights_aligned).sum('channel')
gray_named.names
# 输出结果
('channel', 'row', 'column')
('row', 'column')
# 7.删除tensor的names
gray_plain = gray_named.rename(None)
gray_plain.names
# 输出结果
(None, None)
类型
  1. 定义tensor时可以在tensor的构造器中通过’dtype’参数指定tensor的数据类型,常用的tensor构造器有:tensor()、zeros()、ones()等;
  2. 默认为32-bit浮点型,即torch.float32 或者 torch.float;
  3. 当一个tensor作为其他tensor的索引时,必须使用整型类型。定义整型tensor时,默认为64-bit整型,即torch.int64 或者 torch.long;
  4. 在神经网络执行的计算中,通常使用32位浮点型,更高的精度(64位浮点型)并不会明显地提升模型的准确率,反而会增大内存需求和计算时间;更低的精度(16位浮点型)在标准CPU中没有定义,只有在GPU中存在。
# 1.定义tensor时指定其类型
double_points = torch.zeros(10, 2, dtype=torch.double)
short_points = torch.tensor([[1, 2], [3, 4]], dtype=torch.short)

# 通过.dtype查看tensor的类型
double_points.dtype, short_points.dtype
# 输出结果:
(torch.float64, torch.int16)
# 2.使用转换函数
float_points = torch.zeros(10, 2).float()
int_points = torch.tensor([[1, 2], [3, 4]]).int()
float_points.dtype, int_points.dtype
# 输出结果:
(torch.float32, torch.int32)
# 3.使用to()
half_points = torch.zeros(10, 2).to(torch.half) # float16
long_points = torch.tensor([[1, 2], [3, 4]]).to(dtype=torch.long)
half_points.dtype, long_points.dtype
# 输出结果:
(torch.float16, torch.int64)
  • 使用.to()时,底层会检查所执行的类型转换是否必要,如果必要则执行;
  • 在进行运算时,会执行自动类型转换:自动转化成较大的数据类型。
存储
  1. tensor中的数值被分配在连续的内存块中,由torch.Storage类的对象管理;
  2. storage: 一维数组,是一个包含给定类型数据的连续内存块;
  3. 二者的关系:无论tensor的维度是多少,storage总是以一维数组的形式存储所有的元素;tensor只是建立在storage上的视图,多个tensors可以在同一个storage上建立不同的索引;如果storage的值发生了改变,在其上建立的所有tensor的值都会发生改变;
  4. 修改存储的数据
  • 不带下划线的方法:不修改原tensor,而是返回一个新的tensor;
  • 带下划线的方法:直接改变原tensor–> in-place operation。
# 1.查看tensor对应的storage
points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
print(points.shape)
print(points.storage()) # 尽管points是3行2列的张量,但是实际存储的storage是一维的
# 输出结果:
torch.Size([3, 2])
 4.0
 1.0
 5.0
 3.0
 2.0
 1.0
[torch.FloatStorage of size 6]
# 2.索引storage
points_storage = points.storage()
points_storage[0]
# 输出结果:
4.0
# 3.改变storage的值
points_storage[3] = 2.0
points
# 输出结果:
tensor([[4., 1.],
        [5., 2.],
        [2., 1.]])
# -place运算
a = torch.ones(3, 2)
a.zero_()
a
# 输出结果:
tensor([[0., 0.],
        [0., 0.],
        [0., 0.]])

 

元数据和连续性
  • size: 元组,表示Tensor的每个维度上有多少元素
  • offset: tensor中的第一个元素在storage中的下标
  • stride: 元组,表示在每个维度上,tensor中的index增加1时,storage中需要跳过的元素个数
  1. 访问二维tensor中第i行第j列的数据:pytorch特征 交叉衍生 pytorch三个特征层融合_数据
  2. 当转置一个tensor或者获取子tensor时,不会重新分配内存,而是分配一个新的tensor,具有与原来tensor不同的元数据;
  3. 当进行矩阵或者更高维数组时转置时,只需要交换相应维度的stride;
  4. 关于tensor的连续性
     * Storage是一维数据存储tensor中所有元素,当按行优先遍历tensor得到的元素顺序与storage中的元素顺序一致时–>连续,否则为不连续;
     
     * 例如转置后的tensor就是不连续的;
     
     * 某些运算必须在连续的tensor上执行,例如.view()。因此可以使用.contiguous()将不连续的tensor转化为连续的tensor,此时对应的storage和stride会发生改变。
# 1.查看元数据
points.size(), points.storage_offset(), points.stride()
# 输出结果:
(torch.Size([3, 2]), 0, (2, 1))
# 2.提取一个子tensor
second_point = points[1]
second_point.size(), second_point.storage_offset(), second_point.stride()
# 输出结果:
(torch.Size([2]), 2, (1,))
# 3.子tensor与原tensor具有相同的storage
second_point[0] = 10.0
points
# 输出结果:
tensor([[ 4.,  1.],
        [10.,  2.],
        [ 2.,  1.]])
# 4.若只想改变子tensor,使用.clone()把子tensor克隆一个新的tensor
second_point = points[1].clone()
second_point[1] = 100
second_point, points
# 输出结果:
(tensor([ 10., 100.]), tensor([[ 4.,  1.],
         [10.,  2.],
         [ 2.,  1.]]))
# 5.转置tensor
points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
points_t = points.t()   # 对points进行转置
print(points_t)

# 查看points_t和points的内存地址
print(id(points.storage()) == id(points_t.storage()))  # id()用于获取对象的内存地址

# 查看二者对应的stride
print(points.stride(), points_t.stride())
# 输出结果:
tensor([[4., 5., 2.],
        [1., 3., 1.]])
True
(2, 1) (1, 2)
# 6.在更高维上进行转置
some_t = torch.randn(3, 4, 5)
transpose_t = some_t.transpose(0, 2)  # 在指定的两个维度上进行转置
some_t.size(), transpose_t.size(), some_t.stride(), transpose_t.stride()
# 输出结果:
(torch.Size([3, 4, 5]), torch.Size([5, 4, 3]), (20, 5, 1), (1, 5, 20))
# 7.使用.is_contiguous()判断tensor的连续性
points.is_contiguous(), points_t.is_contiguous()
# 输出结果:
(True, False)
# 使用.contiguous()为不连续tensor创建连续的tensor
points_t_cont = points_t.contiguous()
points_t_cont, points_t_cont.storage()
# 输出结果:
(tensor([[4., 5., 2.],
         [1., 3., 1.]]),  4.0
  5.0
  2.0
  1.0
  3.0
  1.0
 [torch.FloatStorage of size 6])

 

设备
  1. PyTorch中的tensor既可以存储在CPU上,也可以存储在GPU上;每个tensor都可以转移到GPU上进行大规模高速地并行计算
  2. Tensor的‘device’属性用于管理tensor的存储设备
  3. cpu–>gpu
      - 定义tensor时使用‘device=cuda’直接在GPU上创建tensor
      - 已经存在于cpu上的tensor, 使用.to(device=‘cuda’)或者.cuda()将tensor复制到GPU上
      - 存在多个GPU时,使用从0开始的整数进行标识,例如,cuda:0
  4. gpu–>cpu
      - 使用.to(device=‘cpu’)或者.cpu()
  5. 使用.to()可以同时改变tensor的‘dtype’和‘device’
# 1.在GPU上创建tensor
points_gpu = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]], device='cuda')
points_gpu
# 输出结果:
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]], device='cuda:0')
# 2.使用.to()将CPU上已经创建的tensor复制到GPU上
points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
points_to_gpu = points.to(device='cuda:0')   # 返回一个新的tensor,存储在GPU的RAM上;含有多个GPU时,使用从0开始的整数表示具体使用哪个GPU
print(points)
print(points_to_gpu)

# 使用.cuda()
points_to_gpu_2 = points.cuda() # .cuda()还可以传入参数,表示使用哪个GPU,eg:0
print(points_to_gpu_2)
# 输出结果:
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]])
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]], device='cuda:0')
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]], device='cuda:0')
# 3.如果tensor定义或者被复制到GPU上,基于该tensor的运算也在GPU上进行
d_points = 2 * points
d_points_gpu = 2 * points.to(device='cuda')
a_points_gpu = d_points_gpu + 4
d_points, d_points_gpu, a_points_gpu
# 输出结果:
(tensor([[ 8.,  2.],
         [10.,  6.],
         [ 4.,  2.]]), tensor([[ 8.,  2.],
         [10.,  6.],
         [ 4.,  2.]], device='cuda:0'), tensor([[12.,  6.],
         [14., 10.],
         [ 8.,  6.]], device='cuda:0'))
# 4.将tensor从GPU转移到CPU
points_cpu = points_gpu.to(device='cpu')
points_cpu2 = points_gpu.cpu()
points_cpu, points_cpu2
# 输出结果:
(tensor([[4., 1.],
         [5., 3.],
         [2., 1.]]), tensor([[4., 1.],
         [5., 3.],
         [2., 1.]]))

 

与Numpy的互操作性
  1. tensor–>array:使用.numpy()
  2. array–>tensor:使用torch.from_numpy(array)
  3. 注意,这两种形式转换的tensor和array共享底层的缓冲区–>修改其中一个会导致另一个的改变
# tensor与array的转换

# tensor-->array
a = torch.ones(3, 4)
a_np = a.numpy()
print(a.dtype, a_np.dtype)

# array-->tensor
b = torch.from_numpy(a_np)
b
# 输出结果:
torch.float32 float32
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])

 

序列化
  1. 使用torch.save()和torch.load()
     - torch.save(): 将tensor保存到文件中
     - torch.load(): 从文件中加载tensor
      这种方法与Python的其他库不兼容,只能在PyTorch中使用
  2. 基于HDF5文件格式,使用Python的h5py库
     - HDF5是一种表达序列化多维数组的常用格式,以嵌套字典的形式组织数据;
      - h5py库:接收和返回的数据形式为numpy的array。
# 1.基于torch.save()和torch.load()
with open('temp.t', 'wb')as f:
  torch.save(points, f)

with open('temp.t', 'rb')as f:
  points = torch.load(f)

points
# 输出结果:
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]])
# 2.基于h5py
import h5py

f = h5py.File('temp.t', 'w')
dset = f.create_dataset('coords', data=points.numpy())  # 两个参数分别为key value
f.close()
f = h5py.File('temp.t', 'r')
dset = f['coords']
all_points = torch.from_numpy(dset[:])
print(all_points)
f.close()
# 输出结果:
tensor([[4., 1.],
        [5., 3.],
        [2., 1.]])

 

练习
# 1.从list(range(9))创建tensor, 查看其元数据
a = torch.tensor(list(range(9)))
a.size(), a.storage_offset(), a.stride()
# 输出结果:
(torch.Size([9]), 0, (1,))
# 2.使用.view()创建新的tensor
b = a.view(3, 3)
b, id(a.storage())==id(b.storage())  # 二者共享底层存储单元
# 输出结果:
(tensor([[0, 1, 2],
         [3, 4, 5],
         [6, 7, 8]]), True)
c = b[1:, 1:]
c, c.size(), c.storage_offset(), c.stride()
# 输出结果:
(tensor([[4, 5],
         [7, 8]]), torch.Size([2, 2]), 4, (3, 1))
# 3.对a执行元素级数学运算
a_square = torch.square(a)
a_square
# 输出结果:
tensor([ 0,  1,  4,  9, 16, 25, 36, 49, 64])
a_sqrt = torch.sqrt(a)
# 输出结果:
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-24-096f487a51f0> in <module>()
----> 1 a_sqrt = torch.sqrt(a)

RuntimeError: sqrt_vml_cpu not implemented for 'Long'

报错:a的数据类型为Long,但是torch.sqrt()方法不支持对长整型的运算
# 转换数据类型
a_sqrt = torch.sqrt(a.float())
a_sqrt
# 输出结果:
tensor([0.0000, 1.0000, 1.4142, 1.7321, 2.0000, 2.2361, 2.4495, 2.6458, 2.8284])