PyTorch的数据操作

PyTorch中的数据可以分为:张量(Tensor)、变量(Variable)、参数(Parameter) 三种:

张量

张量是一个具有特定类型数值组成的高阶数组,其形状表现为沿着每一阶对应的轴上按指定的维度展开。在pytorch中,张量支持GPU加速计算,同时可以实现自动微分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
device_gpu = torch.device('cuda') # GPU设备
x = torch.ones((2, 2), requires_grad=True) # 需要计算梯度

x.to(device_gpu) # 使用GPU加速
y = x + 2
z = y * y * 3
out = z.mean()
out.backward() # 反向传播
print(x.grad) # 自动微分 (x+2)(x+2)*3/4 -> (1.5x+3)|x=1

'''
tensor([[ 4.5000, 4.5000],
[ 4.5000, 4.5000]])
'''

对张量数据的操作可以归类为:创建、运算、索引、变换、归约等:

创建

张量由三个关键属性来定义:轴的个数(阶)、形状、数据类型

1
2
3
4
5
6
x = torch.arange(12)
print(x) # tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
print(x.ndim) # 1 (向量的阶为1)
print(x.shape) # 等同于x.size(),结果为:torch.Size([12])
print(x.numel()) # 12 (元素的个数,注意:pytorch的tensor没有size属性)
print(x.dtype) # torch.int64

使用全0、全1、其他常量来初始化矩阵:

1
2
3
x_0 = torch.zeros((2, 3, 4)) # 三阶张量(注意:参数是tuple表示形状,元素对应各个轴上的维度)
x_1 = torch.ones((2, 3, 4))
x_2 = torch.full((2, 3, 4), fill_value=2) # 所有元素初始化为2

从某个概率分布中随机采样来得到张量中每个元素的值:

1
2
3
x_0 = torch.rand((3, 4)) # [0,1)内的均匀分布随机数
x_1 = torch.randn((3, 4)) # 标准正态分布N(0,1)的随机数
x_2 = torch.normal(mean, std, out=None) # 正态分布(注意:mean和std都是tensor)

通过提供包含数值的Python列表(或嵌套列表)来为所需张量中的每个元素赋予确定值:

1
x = torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

创建形状相同的全0、全1张量:

1
2
x_0 = torch.zeros_like(x) # 创建和x张量形状相同的全0张量
x_1 = torch.ones_like(x)

运算

按元素计算(element-wise),即两个张量对应位置上的元素进行运算。这里涉及到广播机制,即通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状可以进行按元素计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = torch.rand(3, 4)
b = torch.rand(4)

c_0 = a + b # 也可以:torch.add(a, b)
c_1 = a - b # 也可以:torch.sub(a, b)
c_2 = a * b # 也可以:torch.mul(a, b) 该乘法为对应元素相乘
c_3 = a / b # 也可以:torch.div(a, b)
c_4 = a.pow(2) # 幂运算
c_5 = a.sqrt() # 开平方根
c_6 = a.rsqrt() # 平方根倒数
c_7 = torch.exp(a) # 指数
c_8 = torch.log(a) # 对数(e为底)
c_9 = a.floor() # 向下取整
c_10 = a.ceil() # 向上取整
c_11 = a.trunc() # 取整数部分
c_12 = a.frac() # 取小数部分
c_13 = a.round() # 四舍五入
c_14 = a.clamp(0.50.8) # 裁剪,最小值为0.5,最大值为0.8,最值以外的元素都用最值代替(注意:可以只给定最大值或最小值)
c_15 = (a == b) # 返回布尔张量,每个元素表示a和b对应位置是否相等

线性代数运算,包括向量的点积、矩阵的乘法等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v_1 = torch.tensor([2, 1, 0])
v_2 = torch.tensor([-1, 2, 1])

x = torch.dot(v_1, v_2) # 向量内积

m_1 = torch.tensor([[1,2], [3,4]])
m_2 = torch.tensor([[0,1,2], [3,4,5]])

y = torch.mm(m_1, m_2) # 也可以:torch.matmul(m_1, m_2) 或 m_1@m_2

# 使用矩阵乘法实现向量外积
torch.mm(v_1.T, v_2) # v_1.T表示v_1的转置

a_1 = torch.eye(5) # 创建5*5的单位矩阵
a_2 = torch.diag(torch.tensor([1,2,3,4,5])) # 创建对角阵(注意:参数是对角线元素构成的一阶张量)
a_3 = m_1.transpose(1, 0) # 第0轴和第1轴交换,也就是矩阵的转置
a_4 = torch.det(m_1) # 矩阵对应的行列式
a_5 = torch.inverse(m_1) # 矩阵的逆
a_6 = torch.pinverse(m_1) # 矩阵的伪逆
a_7 = torch.matrix_rank(m_1) # 矩阵的秩
a_8 = torch.trace(m_1) # 矩阵的迹

当然,张量支持的运算还有很多,比如三角函数、对数函数、激活函数等等,这里就不一一列举了。

索引与切片

假设我们有4张3通道(RGB)的图片(28*28),我们使用类似python的索引与切片方式来访问需要的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
data = torch.randn((4, 3, 28, 28))

print(data[0].shape) # 第0张图片的形状:torch.Size([3,28,28])
print(data[0,0].shape) # 第0张图片第0个通道的形状:torch.Size([28,28])
print(data[0,0,2,4]) # 第0张图片的第0个通道的第2行第4列的像素点的值:tensor(0.8082)
print(data[:2].shape) # 取第0、1张图片:torch.Size([2,3,28,28])
print(data[:2,-1:,:,:].shape) # 取第0、1张图片的最后一个通道:torch.Size(2,1,28,28)
print(data[:,:,0:28:2,0:28:2].shape) # 图片的矩阵进行隔行与隔列索引:torch.Size([4,3,14,14])

# 使用省略号...进行任意多的维度索引
print(data[...].shape) # torch.Size([4, 3, 28, 28])
print(data[0,1,...].shape) # torch.Size([28, 28])
print(data[...,2].shape) # torch.Size([4, 3, 28])

支持使用不规则间隔、掩码以及平铺开来计算的索引等复杂索引:

1
2
3
4
5
6
7
8
9
# index_select(dim, indices),其中indices必须是tensor
print(data.index_select(0, torch.tensor([0,2])).shape) # 取0轴上的,第0,2个元素(即第0,2张图片):torch.Size([2, 3, 28, 28])

# masked_select(x, mask),返回的结果会被打平
mask = data.ge(0.5)
print(torch.mask_select(data, mask)) # data中大于0.5的元素组成的一阶张量

# take(src, torch.tensor([index])),平铺后按index索引元素
print(torch.take(data, torch.tensor([0, 2, 5])))

另外两种切片操作也值得关注,narrow(dim, start, length)unbind(dim=0)
1
2
3
4
5
6
7
8
9
10
x = torch.arange(9.).reshape(3, 3)

print(x.narrow(0, 1, 2)) # 返回张量与输入张量共享内存
print(x.unbind(1)) # 沿指定轴拆解张量,返回一个元组,包含了沿着指定轴切片后的结果

'''
tensor([[3., 4., 5.],
[6., 7., 8.]])
(tensor([0., 3., 6.]), tensor([1., 4., 7.]), tensor([2., 5., 8.]))
'''

变换

张量的变换包括形状的改变、轴的互换、轴的扩展与压缩、拼接与拆分等,我们先初始化一个张量,在此基础上对其进行各种变换:

1
x = torch.arange(12)

改变张量的形状,可以使用 reshapeview 函数,需要注意的是 view 只能改变连续存储的(contiguous) 张量,所以一旦原始的张量经过了轴的互换(transpose或permute)之后就不能直接使用 view 了,必须先调用 cotiguous;reshape则不受限制(x.reshape() == x.contiguous().view())。
1
2
x_1 = x.reshape(3, 4) # 也可以由tensor自己计算部分维度:x.reshape(3, -1)
x_2 = x.view(2, 2, -1)

在改变形状中-1是一个很特殊的参数,它表示“自动计算”,这样可以只指定部分轴上的维度,由张量自己去计算剩下的维度。所以如果想把一个高阶张量变换为一个向量,可以写成 x.reshape(-1) 或 torch.reshape(x, (-1, ))。

一个张量中轴的位置调整最常见的就是矩阵的转置:

1
x_3 = x_1.transpose(1, 0) # 只能进行两个轴的互换,x_1并没有改变

当要进行多个轴的位置互换时需要使用 permute,注意传入的参数是所有轴的索引下标对应的新排列:
1
x_4 = x_2.permute(2, 0, 1) # 可以进行多个轴的互换,x_2并没有改变

在一个张量指定的位置插入一个维度为1的轴实现扩展,或删除维度为1的轴实现压缩:
1
2
x_5 = x_1.unsqueeze(0) # 位置可以是负数,按照python列表下标的负值去理解
x_6 = x_5.squeeze(0) # 可以不输入位置参数,则删除所有维度为1的轴

需要注意的是,这两个操作返回的张量与输入张量共享内存,改变其中一个的内容另一个也会改变。之所以只能增加或删除维度为1的轴,是因为维度为1的轴并没有实际用途,增加只是为了让张量间的形状一致而可以运算,删除则是为了加快计算。

拼接张量的函数有 catstack,两者的区别在于,cat 是按照指定的轴拼接张量,stack 则是沿着一个新的轴对输入的张量进行拼接:

1
2
3
4
5
6
7
8
9
10
a = torch.arange(3)
print(torch.cat((a, a, a), 0)) # cat传入的张量必须有相同数量的轴,且除了指定的轴以外的其他轴需要有相同的尺寸
print(torch.stack((a, a, a), 0)) # stack传入的张量都应该有相同的形状

'''
tensor([0, 1, 2, 0, 1, 2, 0, 1, 2])
tensor([[0, 1, 2],
[0, 1, 2],
[0, 1, 2]])
'''

拆分张量可以通过指定每个分块的大小或需要拆分得到的分块数量进行:

1
2
3
4
5
6
7
print(a.split(2, 0)) # 在0轴上按split_size=2进行分割,不能整除部分在最后一块中
print(a.chunk(3, 0)) # 在0轴上拆分出3个分块来,不能整分的部分在最后一块中

'''
(tensor([0, 1]), tensor([2]))
(tensor([0]), tensor([1]), tensor([2]))
'''

归约

归约操作表示输出的张量是对输入张量进行整体或指定轴上的数学操作,比如求均值、求方差、求最小值、求最大值等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)

print(torch.argmin(a, dim=0)) # 第0轴上的最小值的下标
print(torch.argmax(a, dim=1, keepdim=True)) # 第1轴上最大值的下标(返回结果与原张量同阶)
print(torch.sum(a)) # 所有元素之和
print(torch.max(a, dim=0)) # 第0轴上的最大值(返回值包含值和下标)
print(torch.mean(a)) # 所有元素的平均值
b = torch.std(a) # 标准差
b = torch.median(a) # 中位数

'''
tensor([0, 0, 0])
tensor([[2],
[2]])
tensor(21.)
torch.return_types.max(values=tensor([4., 5., 6.]), indices=tensor([1, 1, 1]))
tensor(3.5000)
'''

选择

使用 gather 函数对张量指定轴上的元素按照新的索引进行重新聚合,按新的索引在指定的那个轴上顺次(其他轴从0开始连续递增)取值:

1
2
3
4
5
6
7
8
9
10
t = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(torch.gather(t, 0, torch.tensor([[1, 2, 0], [2, 0, 2], [0, 1, 1]])))

'''
# 在0轴上,按照新的索引生成一个3*3(索引的形状)的矩阵A,其中A[0][0]=t[1][0]=4,A[0][1]=t[2][1]=8, A[0][3]=t[0][2]=3, ...

tensor([[4, 8, 3],
[7, 2, 9],
[1, 5, 6]])
'''

返回非零元素的下标:

1
2
3
4
5
6
7
8
print(torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0],
[0.0, 0.4, 0.0, 0.0],
[0.0, 0.0, 1.2, 0.0]])))
'''
tensor([[ 0, 0],
[ 1, 1],
[ 2, 2]])
'''

通过 torch.where(condition, x=None, y=None) 函数使用某种逻辑条件进行元素筛选,如果条件成立则使用 x,否则使用 y:

1
2
3
4
5
6
7
8
9
10
11
12
x = torch.arange(9.).reshape(3, 3)

print(torch.where(x > 5)) # 只指定条件,返回元组以构成满足条件元素的下标
print(torch.where(x < 5, x, torch.tensor(-1.0))) # x中小于5的元素保持不变,大于等于5的元素被替换为-1

'''
(tensor([2, 2, 2]), tensor([0, 1, 2])) # 即x[2][0], x[2][1], x[2][2]

tensor([[ 0., 1., 2.],
[ 3., 4., -1.],
[-1., -1., -1.]])
'''

比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
a = torch.Tensor([[1, 2], [3, 4]])
b = torch.Tensor([[1, 1], [4, 4]])

print(torch.equal(a, b)) # 如果两个张量有相同的形状和元素值,则返回true,否则False
print(torch.eq(a, b)) # 比较元素是否相等,第二个参数可以是一个数,或者是第一个参数同类型形状的张量,返回张量为每个位置的比较结果(Boolean)
torch.ne(a, b) # 比较元素不等于,同上
torch.ge(a, b) # 比较元素大于等于,同上
torch.gt(a, b) # 比较元素大于,同上
torch.le(a, b) # 比较元素小于等于,同上
torch.lt(a, b) # 比较元素小于,同上

print(torch.kthvalue(a, 1, dim=0)) # 返回指定轴上第k小的值(如果不指定dim。默认为最后一个轴)
print(torch.topk(a, 1)) # 返回指定轴上第k大的值,同上

sorted, indices = torch.sort(a) # 对指定轴进行升序排序,如果不指定dim,默认是最后一个轴,descending=True则为降序

'''
False
tensor([[ True, False],
[False, True]])

torch.return_types.kthvalue(
values=tensor([1., 2.]),
indices=tensor([0, 0]))
torch.return_types.topk(
values=tensor([[2.],
[4.]]),
indices=tensor([[1],
[1]]))
'''

变量

Variable 和 Tensor 本质上没有区别,不过 Variable 会被放入一个计算图中,然后进行前向传播、反向传播。Variable 有三个重要的组成属性:data,grad 和 grad_fn。通过 data 可以取出 Variable 里面的 Tensor 值,grad_fn 表示的是得到这个 Variable 的操作,比如通过加减还是乘除得到的,最后 grad 是这个 Variable 的反向传播梯度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from torch.autograd import Variable
# create Variable
x = Variable(torch.Tensor([1]), requires_grad=True)
w = Variable(torch.Tensor([2]), requires_grad=True)
b = Variable(torch.Tensor([3]), requires_grad=True)

# Build a computational graph
y = w * x + b

# 查看变量x里面的三个组成属性,此时还未自动求导,所以x.grad_fn,x_grad都为None
print(x.data, x.grad_fn, x.grad) # x.data = 1, x.grad_fn = None, x.grad = None

# Compute gradients
y.backward() # same as y.backward(torch.Tensor([1]))
# print out the gradients
print(x.grad, x.grad_fn, x.data) # x.grad = 2, x.grad_fn = None, x.data = 1
print(y.grad, y.grad_fn, y.data) # y.grad = None, y.grad_fn = <AddBackward0 object at 0x0000022B482E4978>, y.data = 5

要将一个 tensor 变为 Variable 也非常简单,比如想让一个 tensor a 变为 Variable,只需要 Variable(a) 就可以了。

参数

PyTorch 主要通过引入 nn.Parameter 类型的变量和 optimizer 机制来解决模型训练过程中频繁的更新操作。Parameter 是 Variable 的子类,本质上和后者一样,只不过 Parameter 默认是求梯度的,同时一个网络 net 中的 Parameter 变量是可以通过 net.parameters() 来很方便地访问到的,只需将网络中所有需要训练更新的参数定义为 Parameter 类型,再佐以 optimizer,就能够完成所有参数的更新了。

1
2
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
torch.nn.utils.clip_grad_norm_(net.parameters(), 0.5)