下面学习一下Tensorflow张量的进阶操作,如张量的合并与分割、范数统计、张量填充、张量限幅等操作。
1. 张量的合并与分割操作
1.1 张量的合并
合并是指将多个张量在某个维度上合并为一个张量。
张量的合并可以使用拼接和堆叠操作实现,拼接操作并不会产生新的维度,仅在现有的维度上合并,而堆叠会创建新维度。选择使用拼接还是堆叠操作来合并张量,取决于具体的场景是否需要创建新维度。
拼接 在Tensorflow中,可以通过 tf.concat(tensors, axis) 函数拼接张量,其中参数tensors保存了所有需要合并的张量 list,axis 参数指定需要合并的维度索引。
下面看示例:
a = tf.random.normal([4,35,8])
b = tf.random.normal([6,35,8])
c = tf.concat([a,b],axis=0)#拼接操作
print(c.shape)
(10, 35, 8)
再看一例:
a = tf.random.normal([10,35,4])
b = tf.random.normal([10,35,4])
c = tf.concat([a,b],axis=2)#拼接操作
print(c.shape)
(10, 35, 8)
从语法上来说,拼接合并操作可以在任意的维度上进行,唯一的约束是非合并维度的长度必须一致。
堆叠如果在合并数据时,希望创建一个新的维度,则需要使用 tf.stack(tensors, axis) 操作。
tf.stack(tensors, axis) 中的tensors表示需要合并的张量,参数axis指定新维度插入的位置,axis的用法与tf.expand_dims一致,当axis>=0时,在axis之前插入;当axis<0时,在axis之后插入新维度。
下面是使用示例:
a = tf.random.normal([35,8])
b = tf.random.normal([35,8])
c = tf.stack([a,b],axis=0)# 堆叠操作
print(c.shape)
(2, 35, 8)
tf.stack也需要满足张量堆叠合并条件,它需要所有待合并的张量shape完全一致才可合并。
1.2 张量的分割
合并操作的逆过程就是分割,将一个张量拆分为多个张量。
tf.split(x, num_or_size_splits, axis)可以完成张量的分割操作,参数意义如下:
- x 参数::待分割张量。
- num_or_size_splits 参数:切割方案。当 num_or_size_splits 为单个数值时,如10,表示等长切割为10份;当 num_or_size_splits 为 list 时,list的每个元素表示每份的长度,如[2,4,2,2]表示切割为4份,每份的长度依次是2、4、2、2 。
- axis 参数:指定分割的维度索引号。
下面看示例:
a = tf.random.normal([10,35,8])
results = tf.split(a, num_or_size_splits=10, axis=0)#把张量拆分成10份
print(len(results))
10
#查看切割后的某个张量的形状
print(results[0].shape)#可以看到,切割后的张量仍然保留了切割的维度,这一点要注意
(1, 35, 8)
下面,示例一下不等长切割:
a = tf.random.normal([10,35,8])
results = tf.split(a, num_or_size_splits=[4,2,2,2], axis=0)#切割为不等长 4 份
print(len(results))
4
#查看切割后的某个张量的形状
print(results[0].shape)
(4, 35, 8)
特别地,如果希望在某个维度上全部按长度为 1 的方式切割,还可以使用 tf.unstack(x, axis)函数。这是tf.split的一种特殊情况,切割长度固定为1,只需要指定切割维度的索引号即可。
下面看示例:
a = tf.random.normal([10,35,8])
results = tf.unstack(a,axis=0)#张量切割
print(len(results))
10
#查看切割后张量的形状
print(results[0].shape)#可以看到,这种切割方式去掉了切割维度
(35, 8)
2. 张量数据统计
在神经网络的计算过程中,经常需要统计数据的各种属性,如最值、最值位置、均值、和。
通过tf.reduce_max、tf.reduce_min、tf.reduce_mean、tf.reduce_sum 可以求解张量在某个维度上的最大、最小、均值、和,也可以求全局最大、最小、均值、和信息。
下面看一些使用示例:
x = tf.random.normal([4,10])
out = tf.reduce_max(x, axis=1)#求样本的最大值
print(out.shape)
4
out = tf.reduce_min(x, axis=1)#求样本的最小值
print(out.shape)
4
out = tf.reduce_mean(x, axis=1)#求样本的均值
print(out.shape)
4
当不指定 axis 参数时,tf.reduce_*函数会求解出全局元素的最大、最小、均值、和。
x = tf.random.normal([4,10])
tf.reduce_max(x)#求解样本全局的最大值
tf.reduce_min(x)#求解样本全局的最小值
tf.reduce_mean(x)#求解样本全局的均值
在求解误差函数时,通过Tensorflow 的 MSE 误差函数可以求得每个样本的误差。若是需要计算样本的平均误差,此时可以通过 tf.reduce_mean 在样本数维度上计算均值。
看下面示例:
import tensorflow as tf
from tensorflow import keras
out = tf.random.normal([4,10])#网络预测输出
y = tf.constant([1,2,2,0])#真实标签
y = tf.one_hot(y,depth=10)#one-hot编码
loss = keras.losses.mse(y,out)#计算每个样本的误差
loss = tf.reduce_mean(loss)#计算平均误差
print(float(loss))
1.067490816116333
除了希望获得张量的最值信息,还希望获得最值所在的索引号。
通过 tf.argmax(x, axis),tf.argmin(x, axis) 可以求解在 axis 轴上,x的最大值、最小值所在的索引号。
out = tf.random.normal([2,10])#网络输出值
out = tf.nn.softmax(out,axis=1)#通过softmax转换为概率值
pred = tf.argmax(out,axis=1)#求出最值所在的索引号
print(pred)
tf.Tensor([7 3], shape=(2,), dtype=int64)
3.张量比较操作
为了计算分类任务的准确率等指标,一般需要将预测结果和真实标签比较,统计比较结果中正确的数量来计算准确率。
下面看一个示例:
out = tf.random.normal([100,10])
out = tf.nn.softmax(out,axis=1)#输出转换为概率
pred = tf.argmax(out,axis=1)#选取预测值
pred = tf.cast(pred,dtype=tf.int32)
#print(pred)
y = tf.random.uniform([100],maxval=10,dtype=tf.int32)#真实标签
out = tf.equal(pred,y)#预测值与真实值比较
out = tf.cast(out,dtype=tf.int32)
#print(out)
correct = tf.reduce_sum(out)#计算正确个数
print(float(correct)/100)
0.12
可以看到,我们随机产生的预测数据的准确度是12%,这也是随机预测模型的正常水平。
4.填充与复制操作
填充操作可以通过 tf.pad(x, paddings) 函数实现,paddings 是包含了多个[left padding, right padding]的嵌套方案 list,如[[0,0],[2,1],[1,2]]表示第一个维度不填充,第二个维度左起(起始处)填充两个单元,右边(结束处)填充一个单元,第三个维度左边填充一个单元,右边填充两个单元。
下面看使用示例:
a = tf.constant([1,2,3,4,5,6])
b = tf.constant([7,8,1,6])
b = tf.pad(b, [[0,2]])#填充操作
print(b)
tf.Tensor([7 8 1 6 0 0], shape=(6,), dtype=int32)
out = tf.stack([a,b],axis=0)#填充后,合并两个张量
print(out)
tf.Tensor(
[[1 2 3 4 5 6]
[7 8 1 6 0 0]], shape=(2, 6), dtype=int32)
下面再看一例:
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)
#将句子截断或填充到相同长度,设置为末尾填充和末尾截断方式R
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)
(25000, 80) (25000, 80)
上述代码中,我们将句子的最大长度 max_review_len 设置为80个单词,通过keras.preprocessing.sequence.pad_sequences 可以快速完成句子的填充和截断工作。
下面我们介绍对多个维度进行填充的例子。
x = tf.random.normal([4,28,28,3])
print('before: ',x.shape)
x = tf.pad(x,[[0,0],[2,2],[2,2],[0,2]])#对第二维,第三维进行填充
print('after:',x.shape)
before: (4, 28, 28, 3)
after: (4, 32, 32, 5)
复制操作通过 tf.tile 函数可以对长度为1的维度进行复制若干份,也可以对任意长度的维度进行复制若干份。
下面看使用示例:
x = tf.random.normal([4,32,32,3])
x = tf.tile(x,[2,3,3,1])#进行复制操作
print(x.shape)
(8, 96, 96, 3)
5.数据限幅操作
通过 tf.maximum(x,a) 实现数据的下限幅,通过 tf.minimum(x,a)实现数据的上限幅。
使用示例如下:
x = tf.range(9)
print(x)
x = tf.maximum(x,2)#下限幅2
print(x)
tf.Tensor([0 1 2 3 4 5 6 7 8], shape=(9,), dtype=int32)
tf.Tensor([2 2 2 3 4 5 6 7 8], shape=(9,), dtype=int32)
x = tf.range(9)
print(x)
x = tf.minimum(x,7)# 上限幅7
print(x)
tf.Tensor([0 1 2 3 4 5 6 7 8], shape=(9,), dtype=int32)
tf.Tensor([0 1 2 3 4 5 6 7 7], shape=(9,), dtype=int32)
更方便地,我们可以使用 tf.clip_by_value 实现上下限幅。
x = tf.range(9)
x = tf.clip_by_value(x,2,7)#实现上下限幅
print(x)
tf.Tensor([2 2 2 3 4 5 6 7 7], shape=(9,), dtype=int32)
6. Tensorflow的高级操作
6.1 tf.gather操作
tf.gather 可以实现根据索引号收集数据的目的。考虑班级成绩册例子,共有4个班级,每个班级35个学生,8门科目,保存成绩册张量 shape 为 [4,35,8]。
下面看使用示例:
x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)
print(x.shape)
results = tf.gather(x,[0,1],axis=0)#在班级维度收集第1-2号班级成绩册
print(results.shape)
(4, 35, 8)
(2, 35, 8)
#收集第1,4,9,12,13,27号同学的成绩
results = tf.gather(x,[0,3,8,11,12,26],axis=1)
print(results.shape)
(4, 6, 8)
#收集所有同学的第3、5等科目成绩
results = tf.gather(x,[2,4],axis=2)
print(results.shape)
(4, 35, 2)
通过上面的例子可以看到,tf.gather非常适合索引没有规则的场合,其中索引号可以乱序排列,此时收集的数据也是对应顺序。
看如下例子:
x = tf.range(8)
x = tf.reshape(x,[4,2])
print(x)
results = tf.gather(x,[3,1,0,2],axis=0)#收集第4,2,1,3号元素
print(results)
tf.Tensor(
[[0 1]
[2 3]
[4 5]
[6 7]], shape=(4, 2), dtype=int32)
tf.Tensor(
[[6 7]
[2 3]
[0 1]
[4 5]], shape=(4, 2), dtype=int32)
下面看一个问题,如果希望抽查第[2,3]班级的第[3,4,6,27]号同学的科目成绩,则可以通过组合多个tf.gather实现。
x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)
students = tf.gather(x,[1,2],axis=0)#收集第2,3号班级
print(students.shape)
results = tf.gather(students,[2,3,5,26],axis=1)#收集第3,4,6,27号同学
print(results.shape)
(2, 35, 8)
(2, 4, 8)
下面将问题再复杂一点,如果希望抽查第2个班级的第2个同学的所有科目,第3个班级的第3个同学的所有科目,第4个班级的第4个同学的所有科目。可以怎么实现呢?
可以通过笨方式一个一个手动提取,然后合并数据。
x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)
results = tf.stack([x[1,1],x[2,2],x[3,3]],axis=0)
print(results.shape)
(3, 8)
虽然这种方法可以得到正确的结果,但是效率很低,下面来看一种更加高效的方法。
6.2 tf.gather_nd 操作
通过 tf.gather_nd,可以通过指定每次采样的坐标来实现多个采样的目的。
在上面的那个问题中,我们可以将这个采样方案合并为一个list参数:[[1,1],[2,2],[3,3]],通过 tf.gather_nd 实现如下。
x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)
print(x.shape)
results = tf.gather_nd(x,[[1,1],[2,2],[3,3]])
print(results.shape)
(4, 35, 8)
(3, 8)
可以看到,这种实现方式更加简洁,计算效率大大提升。
results = tf.gather_nd(x,[[1,1,2],[2,2,3],[3,3,4]])
print(results.shape)
(3,)
上述代码中,我们抽出了班级1,学生1的科目2;班级2,学生2的科目3;班级3,学生3的科目4的成绩,共有3个成绩数据,结果汇总为一个shape为[3]的张量。
6.3 tf.boolean_mask 操作
除了可以通过给定索引号的方式采样,还可以通过给定掩码(mask)的方式采样。通过 tf.boolean_mask(x, mask, axis)可以在 axis 轴上根据 mask 方案进行采样。
下面看使用示例:
x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)
print(x.shape)
results = tf.boolean_mask(x,[True,False,False,True],axis=0)#采样第一个和第四个数据
print(results.shape)
(4, 35, 8)
(2, 35, 8)
注意掩码的长度必须与对应维度的长度一致。
现要我们来对比一下tf.gather_nd 和 tf.boolean_mask的用法。
x = tf.random.uniform([2,3,8],maxval=100,dtype=tf.int32)
print(x.shape)
#采集第一个班级的第一和第二个学生的成绩。第二个班级的第二和第三个学生的成绩。
results = tf.gather_nd(x,[[0,0],[0,1],[1,1],[1,2]])#多维座标采集
print(results.shape)
(2, 3, 8)
(4, 8)
#使用掩码来实现上述方案
results = tf.boolean_mask(x,[[True,True,False],[False,True,True]])
print(results.shape)
(4, 8)
可以看到,掩码采样的结果与tf.gather_nd的结果完全一样。可见,tf.boolean_mask既可以实现 tf.gather方式的一维掩码采样,又可以实现 tf.gather_nd 方式的多维掩码采样。
6.4 tf.where 操作
通过tf.where(cond,a,b)操作可以根据cond条件的真假从a 或 b中读取数据。
下面看使用示例:
a = tf.ones([3,3])# 构造 a 为全1
b = tf.zeros([3,3])# 构造 b 为全0
cond = tf.constant([[True,False,False],[False,True,False],[False,True,True]])
results = tf.where(cond,a,b)#根据条件从 a,b 中采样
print(results)
tf.Tensor(
[[1. 0. 0.]
[0. 1. 0.]
[0. 1. 1.]], shape=(3, 3), dtype=float32)
这个例子可以看到,返回的张量中为 1 的位置来自张量a,返回的张量中为0的位置来自张量b。
当a=b=None 即a,b参数不指定时,tf.where会返回 cond 张量中所有True的元素的索引座标。
看如下示例:
results = tf.where(cond)#返回cond 中True无素的索引座标
print(results)
tf.Tensor(
[[0 0]
[1 1]
[2 1]
[2 2]], shape=(4, 2), dtype=int64)
那么这个操作有什么作用呢,考虑一个例子,我们需要提取张量中所有正数的数据和索引。
= tf.random.normal([3,3])#构造 a
mask = a > 0#通过比较运算,得到正数的掩码
indices = tf.where(mask)#提取此掩码处True元素的索引座标
results = tf.gather_nd(a,indices)#拿到索引后,即可提取出所有正数
print(results)
tf.Tensor([0.11021124 0.553751 1.3484484 1.2101223 ], shape=(4,), dtype=float32)
results = tf.boolean_mask(a,mask)#通过掩码提取正数,和tf.gather_nd的结果是一致的
print(results)
tf.Tensor([0.11021124 0.553751 1.3484484 1.2101223 ], shape=(4,), dtype=float32)
6.5 scatter_nd 操作
通过 tf.scatter_nd(indices,updates,shapes)可以高效地刷新张量的部分数据,但是只能在全0张量的白板上面刷新。
下面看使用示例:
indices = tf.constant([[4],[3],[1],[7]])#构造需要刷新数据的位置
updates = tf.constant([4.4,3.3,1.1,7.7])#构造需要写入的数据
#在长度为8的全0向量上根据indices写入updates数据
tf.scatter_nd(indices,updates,[8])
<tf.Tensor: shape=(8,), dtype=float32, numpy=array([0. , 1.1, 0. , 3.3, 4.4, 0. , 0. , 7.7], dtype=float32)>
下面考虑3维张量的刷新例子:
indices = tf.constant([[1],[3]])#构造写入位置
updates = tf.constant([[[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]]])#构造刷新数据
#在shape为[4,4,4]的白板上根据indices写入updates数据
results = tf.scatter_nd(indices,updates,[4,4,4])
print(results)
tf.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]]], shape=(4, 4, 4), dtype=int32)
根据打印数据可以看到,数据被刷新到第2,4个通道上。
网友评论