Tensorflow的进阶操作

下面学习一下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个通道上。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345