5.1合并与分割
5.1.1 合并
- 张量的合并可以使用拼接(Concatenate)和堆叠(Stack)操作实现,拼接操作并不会产生新的维度,仅在现有的维度上合并,而堆叠会创建新维度并合并数据
拼接
- 在 PyTorch 中,可以通过 torch.cat(tensors, dim)函数拼接张量,其中参数 tensors保存了所有需要合并的张量 List,dim 参数指定需要合并的维度索引
- 案例:设张量𝑨保存了某学校 1~4 号班级的成绩册,每个班级 35 个学生,共 8 门科目成绩,则张量𝑨的 shape 应为:[4,35,8];同理,张量𝑩保存了其它 6 个班级的成绩册,shape 为[6,35,8]。通过合并这两份 成绩册,便可得到该学校所有班级的成绩册数据,记为张量𝑪,它的 shape 应[10,35,8],其中,数字 10 代表 10 个班级,35 代表 35 个学生,8 代表 8 门科目。
a = torch.randn([4,35,8])
b = torch.randn([6,35,8])
c = torch.cat([a,b],dim=0) # dim表示需要拼接那个维度
print(c.shape) # torch.Size([10, 35, 8])
- 拼接可以在任何一个维度进行操作,但是要求处理要凭借的维度外其他维度必须相等
堆叠
- 拼接操作直接在现有维度上合并数据.希望创建一个新的维度,则需要使用stack堆叠
- 案例:张量B:保存一个班级的成绩册:[35,8].张量A:保存了另外一个班级的成绩册:[35,8].现在需要合并这两个班级,则需要创建一个维度,定义为班级维度.
stack([tensor],dim)
dim的用法和torch.unsqueeze一致.当dim>0表示在dim之前插入,反之在dim之后插入
a = torch.randn([35,8])
b = torch.randn([35,8])
c = torch.stack([a,b],dim=0) #
print(c.shape) # torch.Size([2, 35, 8])
- stack需要所有合并的张量的shape必须完全相同
5.1.2 分割
- torch.split(x,split_size_or_secutions,dim);
- x参数:表示分割张量
- split_size_or_secutions:切割方案
- 为单个数值时:表示每份的长度.
- 当为List时,List的每个元素表示每份的长度
-dim:
- 案例:
a = torch.randn([10,35,8])
c = torch.split(a,split_size_or_sections=1,dim=0) # 在第一个维度等分切割为10份
print(len(c)) # 10
print(c[0].shape) # torch.Size([1, 35, 8])
torch.chunk()
功能和用法类似:
5.2 数据统计
5.2.1向量范数
- 向量范数是表征向量长度的一种度量方法他可以推广到张量上,子啊NN中,常用来表示张量的权值大小,梯度大小.
- 对于矩阵和张量,同样可以利用向量范数的计算思想,等价于将矩阵和张量打平成向量后计算,统称为向量范数-
torch.norm(x, p, dim=None)
求解张量的 L1、L2、∞等范数,其中参数 p 指定为 1、2 时计算 L1、L2 范数,指定为 float(‘inf’)时计算∞ −范数
x = torch.ones([2,2])
print(torch.norm(x,p=1)) # 计算L1范数 tensor(4.)
print(torch.norm(x,p=2)) # 计算Lw范数 tensor(2.)
print(torch.norm(x,p=np.inf)) # 计算无穷范数 tensor(1.)
在神经网络调试的过程中,通常需要在合适的地方查看张量的数值,直接打印张量并
不合适。通常打印张量的范数即可大致推测张量的数值范数。例如,当梯度张量的 L2 范数较大时,可以推测梯度张量的部分元素或整体元素较大,容易出现梯度爆炸现象。
5.2.2 最值,均值,和
通过 torch.max(x, dim)、torch.min(x, dim)、torch.mean(x, dim)、torch.sum(x, dim)函数可以求解张量在某个 dim 维度上的最大、最小、均值、和,也可以求全局最大、最小、均值、和信息。不提供 dim 参数时即可计算全局的最大、最小、均值、和信息。
案例:shape:[4,10]第一个维度4代表样本数量,第二个维度10代表当前样本分别属于10个类别的概率.需要求每个样本的最大概率
x = torch.randn([4,10])
print(x)
x = torch.softmax(x,dim=1) # 转换为0-1区间的概率
print("*"*100)
print(x)
value,idx = torch.max(x,dim=1) # 统计概率维度上的最大值
print(value) # tensor([0.2976, 0.4407, 0.3325, 0.2084])
print(idx) # tensor([0, 5, 4, 1])
'''
tensor([[ 1.5789, 1.0814, -1.2696, -0.9950, -0.2662, 0.5477, 0.0390, -1.4309,
1.0893, 0.0950],
[-1.2895, -1.0340, -1.5963, -1.0534, -1.2014, 2.0275, 1.4572, 0.8600,
-2.6399, 0.3560],
[-0.2834, -0.1758, 0.1350, 0.5610, 1.6566, -1.0543, -0.0188, -0.3594,
0.3412, 0.9549],
[ 0.0308, 0.6552, -0.4137, -0.1948, -0.4073, 0.0246, -0.7505, -1.0286,
0.2252, 0.0262]])
****************************************************************************************************
tensor([[0.2976, 0.1810, 0.0172, 0.0227, 0.0470, 0.1061, 0.0638, 0.0147, 0.1824,
0.0675],
[0.0160, 0.0206, 0.0118, 0.0202, 0.0175, 0.4407, 0.2491, 0.1371, 0.0041,
0.0828],
[0.0478, 0.0532, 0.0726, 0.1112, 0.3325, 0.0221, 0.0623, 0.0443, 0.0892,
0.1648],
[0.1116, 0.2084, 0.0716, 0.0891, 0.0720, 0.1109, 0.0511, 0.0387, 0.1356,
0.1111]])
'''
第一个元素为长度为 4 的向量,代表了每个样本的最大概率值;第二个元素为长度为 4 的整形向量,代表了最大值元素出现的位置索引
案例:在求解神经网络的时,通过需要计算目标值与预测值的差的平方和,再计算样本上的 平均误差。首先计算目标值与预测值的差的平方和,实现如下
out = torch.randn([4,10]) # 模拟网络预测输出
y = torch.tensor([1,2,2,9]) # 模拟真实标签
y = F.one_hot(y,num_classes=10)
loss = torch.square(y-out) # 计算查的平方
loss = loss.sum(dim=1) # 求各个样本的方差
print(loss) # tensor([ 7.6612, 10.2373, 6.6896, 6.5398])
除了希望获取张量的最值信息,有时还需要获得最值元素所在的位置索引号,例如分类任务的标签预测,就需要知道概率最大值所在的位置索引号,并把这个位置索引号作为预测的类别。考虑 10 分类问题,可以得到神经网络的输出张量 out,其 shape 为[2,10],代表了两个样本属于 10 个类别的概率,由于元素的位置索引代表了当前样本属于此类别的概率,预测时往往会选择概率值最大的元素所在的索引号作为样本类别的预测值,例如:
x = torch.randn([2,10]) # 模拟数据
x = torch.softmax(x,dim=1) # 转化为0~1区间的概率
idx = torch.argmax(x,dim=1) # 统计概率维度上的最大值
print(x,idx) # tensor([9, 6])
# 打印网络输出x和预测类别值
通过 torch.argmax(x, dim)和 torch.argmin(x, dim)可以求解在 dim 轴上,x 的最大值、最小值所在的索引号
5.3 张量比较
为了计算分类任务的准确率等指标,通常需要将预测结果和真实标签比较,统计比较结果中正确的数量来计算准确率
out = torch.randn([100,10]) # 模拟数据
out = torch.softmax(out,dim=1) # 转化为0~1区间的概率
pred = torch.argmax(out ,dim=1) # 统计概率维度上的最大值
y = torch.randint(0,10,[100]) # 模拟生成真实标签
res = torch.eq(y,pred) # 预测值与真实值比较,返回布尔类型的张量
torch.eq()函数返回布尔类型的张量比较结果,只需要统计张量中 True 元素的个数,即可知
道预测正确的个数。
res = res.int() # 布尔型转int
correct = res.sum()
5.4填充与复制
5.4.1 填充
- 为了统一不同样本数据的长度,通常的做法是,在需要补充长度的数据开始或结束处填充足够数量的特定数值,这些特定数值一般代表了无效意义,例如数字 0,使得填充后的长度满足模型要求。这种操作就叫作填充操作(Padding)。
- 填充操作可以通过
F.pad(x, pad)
函数实现,参数 pad 是包含了多个[Left Padding, Right Padding]
的嵌套方案 List,并且从最后一个维度开始制定
,如[0,0,2,1,1,2]
表示倒数第一个维度首部填充 0 个单元、尾部填充 0 个单元
,倒数第二个维度首部填充两个单元、尾部填充一个单元
,倒数第三个维度首部填充一个单元、尾部填充两个单元
。特别注意这里 pad 参数指定的填充格式是从最末维度开始的
。 - 案例:
“I like the weather today.”
假设句子数字编码为:[1,2,3,4,5,6],第二个句子为:
“So do I.”它的编码为:[7,8,1,6]。为了能够将这两个句子保存在同一个张量中,需要将这两个句子的
长度保持一致,
a = torch.tensor([1,2,3,4,5,6]) # 第一个句子
b = torch.tensor([7,8,1,6]) # 第一个句子
print(b) # tensor([7, 8, 1, 6])
b = F.pad(b,[0,2]) # 句子末尾填充2个0
print(b) # tensor([7, 8, 1, 6, 0, 0])
c = torch.stack([a,b], dim=0) # 堆叠合并,创建句子数维度
print(c)
#tensor([[1, 2, 3, 4, 5, 6],
# [7, 8, 1, 6, 0, 0]])
- 在自然语言处理中,往往需要加载不同句子长度的数据集,有些句子长度较小,如仅10 个单词,而部份句子长度较长,如超过 100 个单词。为了能够将多个句子保存在同一张量中,一般会选取能够覆盖大部分句子长度的阈值,如 80 个单词,来统一句子的长度。对于小于 80 个单词的句子,在末尾填充相应数量的 0;对大于 80 个单词的句子,则截断超过规定长度的部分单词
total_words = 10000 # 设定词汇量大小
max_review_len = 80 # 最大句子长度
embedding_len = 100 # 词向量长度
# 加载 IMDB 数据集
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
# 将句子填充或截断到相同长度,设置为末尾填充和末尾截断方式
x_train = keras.preprocessing.sequence.pad_sequences(x_train,maxlen=max_review_len,truncating='post',padding='post')
x_test = keras.preprocessing.sequence.pad_sequences(x_test,maxlen=max_review_len,truncating='post',padding='post')
print(x_train.shape, x_test.shape) # 打印等长的句子张量形状
- 以28 × 28大小的图片数据为例,如果网络层所接受的数据高宽为32 × 32,则必须将28 × 28大小填充到32 × 32,可以选择在图片矩阵的上、下、左、右方向各填充 2 个单元,
x = torch.randn([4,1, 28,28]) # 28x28 大小灰度图片
# 图片上下、左右各填充 2 个单元
x2 = F.pad(x, [2,2,2,2])
5.4.2 复制
- torch.repeat 函数除了可以对长度为 1 的维度进行复制若干份,还可以对任意长度的维度进行复制若干份,进行复制时会根据原来的数据次序重复复制。
- 通过 torch.repeat(repeats)函数可以在任意维度将数据重复复制多份,如 shape 为[4,3,32,32]的数据,复制方案为 repeats =[2,1,3,3],即通道数据不复制,高和宽方向分别复制 2 份,图片数再复制 1 份.参数
x = torch.randn([4,3,32,32])
x2 = x.repeat([2,1,3,3]) # 数据复制
print(x.shape) # torch.Size([4, 3, 32, 32])
print(x2.size()) # torch.Size([8, 3, 96, 96])
5.5 数据限幅
- 在 PyTorch 中,可以通过 torch.max(x, a)实现数据的下限幅,即𝑥 ∈ [𝑎, +∞);可以通过torch.min(x, a)实现数据的上限幅,即𝑥 ∈ (−∞, 𝑎]。
x = torch.arange(9)
max = torch.max(x,torch.tensor(2)) # 下限幅,与2比较,小于2的元素取2
min = torch.min(x,torch.tensor(7)) # 上限幅 ,与7比较,大于7的取7
print(min)
- 基于torch.max实现ReLu函数
def relu(x):
return torch.max(x,torch.tensor(0)) # 下限幅取0
5.6高级操作
5.6.1 索引采样
- torch.index_select()函数可以实现根据索引号收集数据的目的。
案例1: - 现在需要收集第 1~2 个班级的成绩册,可以给定需要收集班级的索引号:[0,1],并指定班级的维度 dim=0,通过 torch.index_select()函数收集数据.
x = torch.randint(0,100,[4,35,8]) # 成绩册张量
print(x.shape) # torch.Size([4, 35, 8])
out = torch.index_select(x,dim=0,index=torch.tensor([0,1]))
print(out.shape) # torch.Size([2, 35, 8])
案例2:
- 如果需要收集所有同学的第 3 和第 5 门科目的成绩,则可以指定科目维度 dim=2
out = torch.index_select(x, dim=2, index=torch.tensor([2, 4]))
可以看到,torch.index_select 函数非常适合索引号没有规则的场合,并且索引号可以乱序排列,此时收集的数据也是对应顺序排列
a = torch.arange(8)
a = a.reshape([4,2]) # 生成张量 a
print(torch.index_select(a, dim=0, index=torch.tensor([3, 1, 0, 2])))
'''
tensor([[6, 7],
[2, 3],
[0, 1],
[4, 5]])
'''
5.6.2 掩码采样
- 除了可以通过给定索引号的方式采样,还可以通过给定掩码(Mask)的方式进行采样。
# 模拟4个班级,每个班级35个学生,每个学生有8门课程的成绩
x = torch.randint(0, 9, [4,35,8])
# 考虑在班级维度上进行采样,对这 4 个班级的采样方案
mask = [True, False, False,True]
print(x[mask].shape) # torch.Size([2, 35, 8])
# 如果对 8 门科目进行掩码采样,设掩码采样方案为
mask = [True, False, False, True, True, False, False,True]
print(x[:, :, mask].shape)
- 多维坐标采样
# 如果希望采样第 1 个班级的 第 1~2 号学生,第 2 个班级的第 2~3 号学生 总共4个学生
idx = torch.tensor([[0,0], [0,1], [1,1], [1,2]])
print(idx[:, 0]) # tensor([0, 0, 1, 1])
print('______________')
print(idx[:, 1]) # tensor([0, 1, 1, 2])
print('______________')
print(x[idx[:, 0], idx[:, 1],:]) # 多维坐标采样
- 使用掩码采样
mask = torch.tensor([[True,True,False],[False,True,True]])
x[mask] # 多维掩码方式
实际上,掩码坐标与索引坐标之间可以进行等
价转换,掩码坐标转索引坐标可以通过dx=mask.nonzero()方式实现,而索引坐标转掩码可
以通过 mask[idx[:,0], idx[:,1]]=True 类似的方式实现。
特别注意的是,PyTorch 中提供的掩码函数 torch.masked_select 反而使用方式比较单 一,并不如上述掩码方式灵活
5.6.3 Gather采样
在多维坐标索引采样中,需要给出所有采样点的多维坐标信息,显得比较繁琐。尤其是当需要采样某个维度上的部分数据,而其它维度全部采样时,直接使用多维坐标采样方式表示非常繁琐,此时可以通过 Gather 函数实现。
# 随机采样方案
idx = torch.tensor([
# 第一个班级采样方案
[[0, 1], # 班级 1,学生 1,采样第 1、2 门科目
[1, 2], # 班级 1,学生 2,采样第 2、3 门科目
[2, 3]], # 班级 1,学生 3,采样第 3、4 门科目
# 第二个班级采样方案
[[3, 2], # 班级 2,学生 1,采样第 4、3 门科目
[2, 1], # 班级 2,学生 2,采样第 3、2 门科目
[1, 0]], # 班级 2,学生 3,采样第 2、1 门科目
])
print(idx.shape) # torch.Size([2, 3, 2])
x = torch.randint(0, 9, [2, 3, 4]) # 随机生成成绩张量
out = torch.gather(x, dim=2, index=idx)
# 在科目维度上采集数据
5.6.4 Wherez采样
通过 torch.where(cond, a, b)操作可以根据 cond 条件的真假从参数𝑨或𝑩中读取数据,条件判定规则如下.
a = torch.ones([3, 3]) # 构造 a 为全 1 矩阵
b = torch.zeros([3, 3]) # 构造 b 为全 0 矩阵
# 构造采样条件
cond = torch.tensor([[True, False, False], [False, True, False], [True, True, False]
])
print(torch.where(cond, a, b))
当参数 a=b=None 时,即 a 和 b 参数不指定,torch.where 会返回 cond 张量中所有 True的元素的索引坐标
,此时 torch.where 等价于 torch.nonzero 函数。
cond = torch.tensor([[True, False, False], [False, True, False], [True, True, False]
])
print(torch.where(cond))
print(torch.nonzero(cond))
'''
(tensor([0, 1, 2, 2]), tensor([0, 1, 0, 1]))
tensor([[0, 0],
[1, 1],
[2, 0],
[2, 1]])
'''
可以看到,where 与 nonzero 函数是完全等价的,但是在结果的表达方式上略有不同,where 函数会将坐标信息拆开为元组表示。
- 提取索引坐标的案例:
考虑一个场景,需要提取张量中所有正数的数据和索引。首先构造张量 a,并通过比较运算得到所有正数的位置掩码.
# 方法一:
x = torch.randn([3,3])
mask = x>0 # 比较操作
idx = torch.where(mask) # 提取所有大于0的元素的索引坐标
pos = x[idx[0],idx[1]] # 拿到索引后,通过多维坐标索引即可恢复出所有正数的元素
# 方法二:
# 实际上,当得到掩码 mask 之后,也可以直接通过多维掩码索引方式获取所有正数的
# 元素向量
pos = x[mask] # 直接利用掩码进行多维索引
5.6.5 Scatter写入函数
- 前面介绍了 Gather 采样方式,它通过 idx 张量指定的采样坐标来从张量中读取数据,gather 函数的逆过程可以理解为根据 idx 张量指定的采样坐标来更新张量的部分数据,可以通过 scatter 函数或 scatter_函数来实现。在 PyTorch 中,函数名加下划线后缀一般表示原地更新(In-place update)操作,类似的函数还有 fill_、add_等。
- 通过
x.scatter (dim, index, src)
函数可以高效地写入张量的部分数据,特别适合需要根据坐标来更新张量的部分数据的场合。其中dim 表示更新的维度
,index 参数意义等价于gather 函数的 index 参数,用于选定需要更新数据的坐标
,而待写入的数据则用 src 张量
表示。通常把 x 张量称为目标张量,src 张量称为源张量 - 案例一:
idx = torch.tensor([1, 3]) # 构造写入位置,即2个位置
src = torch.tensor([ # 构造写入数据,即 2 个矩阵
[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]],
[[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]
]).float()
x = torch.zeros([4, 4, 4])
x[idx] = src # 这种单一维度的索引写入不需要 scatter 函数
'''
x[idx] === x[[1,3]] 相当于在第一个维度上取 第1和3位置的元素
'''
print(x)
'''
tensor([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[5., 5., 5., 5.],
[6., 6., 6., 6.],
[7., 7., 7., 7.],
[8., 8., 8., 8.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.]]])
'''
- 案例2:
下面将继续使用 Gather 一节中的例子来更新部分科目成绩数据。考虑 shape 为[2,3,4]的成绩张量,即共有两个班级,每个班级 3 位学生,4 门科目成绩。继续使用坐标为 idx 的采样方案,并生成目标张量 x
# 随机采样方案
idx = torch.tensor([
# 第一个班级采样方案
[[0, 1], # 班级 1,学生 1,采样第 1、2 门科目
[1, 2], # 班级 1,学生 2,采样第 2、3 门科目
[2, 3]], # 班级 1,学生 3,采样第 3、4 门科目
# 第二个班级采样方案
[[3, 2], # 班级 2,学生 1,采样第 4、3 门科目
[2, 1], # 班级 2,学生 2,采样第 3、2 门科目
[1, 0]], # 班级 2,学生 3,采样第 2、1 门科目
])
x = torch.randint(0, 9, [2, 3, 4]).float() # 随机生成成绩张量
src = torch.full([2,3,2], 10) # 这里简单设置新更新的数据全为 10
x.scatter(dim=2, index=idx, src=src)
5.6.6 Meshgrid网格函数
x = torch.linspace(-8.,8,100) # 设置 x 轴的采样点
y = torch.linspace(-8.,8,100) # 设置 y 轴的采样点
x,y = torch.meshgrid(x,y) # 生成网格点,并内部拆分后返回
x.shape,y.shape # 打印拆分后的所有点的 x,y 坐标张量 shape
z = torch.sqrt(x**2+y**2)
z = torch.sin(z)/z # sinc 函数实现
fig = plt.figure()
ax = Axes3D(fig) # 设置 3D 坐标轴
# 根据网格点绘制 sinc 函数 3D 曲面
ax.contour3D(x.numpy(), y.numpy(), z.numpy(), 50)
plt.show()
5.7 经典数据集加载
- 常用经典图片数据集
torchvision 均对这些常见数据集的加载提供了便捷支持,对于如 MNIST、CIFAR 这种小型数据集,可以直接在线下载、自动加载;对于如 ImageNet、Kinetics-400 这种大型数据集,用户需要自行下载数据集文件,并在 torchvision 中指定路径即可。