第5章 TensorFlow进阶
5.1 合并与分割
5.1.1合并
合并是指将多个张量在某个维度上合并为一个张量 。
张量的合并可以使用拼接(Concatenate)和堆叠 (Stack)操作实现,拼接并不会产生新的维度,而堆叠会创建新维度。
-
拼接:
tf.concat(tensors, axis)
,其中tensors 保存了所有需要合并的张量 List , axis 指定需要合并的维度。# 在axis=0维度, 合并张量 A,B a = tf.random.normal([4, 35, 8]) b = tf.random.normal([6, 35, 8]) c = tf.concat([a, b], axis=0) print(c.shape)
合并操作可以在任意的维度上进行,唯一的约束是非合并维度的长度必须一致。
-
堆叠 :如果在合并数据时,希望创建一个新的维度,则需要使用
tf.stack
操作 。使用
tf.stack(tensors, axis)
可以合并多个张量 tensors,其中 axis 指定插入新维度的位置, axis 的用法tf.expand_dims
的一致,当axis ≥ 0时,在 axis 之前插入;当axis < 0时 ,在 axis 之后插入新维度。# 堆叠方式合并a,b 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 完全一致才可合并。
5.1.2 分割
通过 tf.split(x, axis, num_or_size_splits)
可以完成张量的分割操作 :
-
x:待分割张量
-
axis:分割的维度索引号
-
num_or_size_splits:切割方案
-
当 num_or_size_splits 为单个数值时,如 10,表示切割
为 10 份x = tf.random.normal([10, 35, 8]) # 等长切割 result = tf.split(x, axis=0, num_or_size_splits=10) print(len(result)) print(result[0])
-
当 num_or_size_splits 为 List 时,每个元素表示每份的长度,如[2,4,2,2]表示切割为 4 份,每份的长度分别为[2,4,2,2 ]
x = tf.random.normal([10, 35, 8]) # 自定义长度切割 result = tf.split(x, axis=0, num_or_size_splits=[4, 2, 2, 2]) print(len(result)) print(result[0])
-
特别地,如果希望在某个维度上全部按长度为 1 的方式分割,还可以直接使用 tf.unstack(x,axis)
。 这种方式是tf.split
的一种特殊情况,切割长度固定为 1,只需要指定切割维度即可。
5.2.1向量范数
向量范数(Vector norm)是表征向量“长度”的一种度量方法 ,常用来
表示张量的权值大小,梯度大小等。
-
L1范数,定义为向量𝒙的所有元素绝对值之和:
\[||x||_ 1 = \sum_i|x_i| \] -
L2范数,定义为向量𝒙的所有元素的平方和,再开根号 :
\[||x||_2 = \sqrt\sum_i|x_i|^2 \] -
\(\infty\)-范数,定义为向量𝒙的所有元素绝对值的最大值 :
\[||X||_\infty = max_i(|x_i|) \]
在 TensorFlow 中,可以通过 tf.norm(x, ord)
求解张量的 L1, L2, \(\infty\)等范数 :
- 参数 ord 指定为 1,2 时计算 L1, L2 范数,指定为
np.inf
时计算\(\infty\) -范数
5.2.2最大最小值、均值、和
通过 tf.reduce_max
,tf.reduce_min
,tf.reduce_mean
,tf.reduce_sum
可以求解张量在某个维度上的最大、最小、 均值、和,也可以求全局最大、最小、均值、和信息。
x = tf.random.normal([4, 10])
# 统计概率维度上的最大值
y = tf.reduce_max(x, axis=1)
# 统计概率维度上的最小值
z = tf.reduce_min(x, axis=1)
# 统计概率维度上的均值
a = tf.reduce_mean(x, axis=1)
当不指定 axis 参数时, tf.reduce_*
函数会求解出全局元素的最大、最小、 均值、和:
tf.reduce_max(x)
,tf.reduce_min(x)
,tf.reduce_mean(x)
在求解误差函数时,通过 TensorFlow 的 MSE 误差函数可以求得每个样本的误差,需要计算样本的平均误差,此时可以通过 tf.reduce_mean
在样本数维度上计算均值 :
out = tf.random.normal([4, 10])# 网络预测输出
y = tf.constant([1, 2, 2, 0])# 真实标签
y = tf.one_hot(y, depth=10)# one-hot 编码
loss = tf.keras.losses.mse(y, out)# 计算每个样本的误差
loss = tf.reduce_mean(loss)# 平均误差
print(loss)
求和函数 tf.reduce_sum(x,axis)
, 它可以求解张量在 axis 轴上所有特征的和:
out = tf.random.normal([4, 10])
y = tf.reduce_sum(out, axis=-1)
通过 tf.argmax(x, axis)
, tf.argmin(x, axis)
可以求解在 axis 轴上, x 的最大值、 最小值所在的索引号。
5.3张量比较
通过 tf.equal(a, b)
(或 tf.math.equal(a, b)
)函数可以比较这 2个张量是否相等:
out = tf.random.normal([100, 10])
out = tf.nn.softmax(out, axis=1) # 输出转换为概率
pred = tf.argmax(out, axis=1) # 选取预测值
print(pred)
y = tf.random.uniform([100], dtype=tf.int64, maxval=10)
print(y)
out = tf.equal(pred, y) # 预测值与真实值比较
print(out)
out = tf.cast(out, dtype=tf.float32) # 布尔型转 int 型
correct = tf.reduce_sum(out) # 统计 True 的个数
print(correct)
tf.equal()
函数返回布尔型的张量比较结果, 只需要统计张量中 True 元素的个数,即可知道预测正确的个数。
其他比较函数:
函数 | 功能 |
---|---|
tf.math.greater |
𝑎 > 𝑏 |
tf.math.less |
𝑎 < 𝑏 |
tf.math.greater_equal |
𝑎 ≥ 𝑏 |
tf.math.less_equal |
𝑎 ≤ 𝑏 |
tf.math.not_equal |
𝑎 ≠ 𝑏 |
tf.math.is_nan |
𝑎 = 𝑛𝑎𝑛 |
5.4 填充与复制
5.4.1 填充
在需要补充长度的信号开始或结束处填充足够数量的特定数值,如 0,使得填充后的长度满足系统要求。那么这种操作就叫做填充(Padding) 。
填充操作可以通过 tf.pad(x, paddings)
函数实现, paddings 是包含了多个[𝐿𝑒𝑓𝑡 𝑃𝑎𝑑𝑑𝑖𝑛𝑔, 𝑅𝑖𝑔ℎ𝑡 𝑃𝑎𝑑𝑑𝑖𝑛𝑔]
的嵌套方案 List 。
在自然语言处理中,需要加载不同句子长度的数据集,有些句子长度较小,部份句子长度较长 ,一般会选取能够覆盖大部分句子长度的阈值。
# IMDB数据集加载
total_words = 10000
max_review_len = 80
embedding_len = 100
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.imdb.load_data(num_words=total_words)
# 将句子填充或截断到相同长度,设置为末尾填充和末尾截断方式
x_train = tf.keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len, truncating='post', padding='post')
x_test = tf.keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len, truncating='post', padding='post')
print(x_train.shape, x_test.shape)
通过keras.preprocessing.sequence.pad_sequences
可以快速完成句子的填充和截断工作。
图像多维填充:
# 图片填充
x = tf.random.normal([4, 28, 28, 1])
y = tf.pad(x, [[0, 0], [2, 2], [2, 2], [0, 0]])
print(y)
5.5数据限幅
在 TensorFlow 中,可以通过 tf.maximum(x, a)
实现数据的下限幅: 𝑥 ∈ [𝑎, +∞);可以通过 tf.minimum(x, a)
实现数据的上限幅: 𝑥 ∈ (-∞, 𝑎]
那么 ReLU 函数可以实现为:
def relu(x):
return tf.minimum(x, 0.) # 下限幅为0
通过组合 tf.maximum(x, a
)和 tf.minimum(x, b)
可以实现同时对数据的上下边界限幅:𝑥 ∈ [𝑎, 𝑏]
x = tf.range(9)
m = tf.minimum(tf.maximum(x, 2), 7)
print(m)
可以使用 tf.clip_by_value
实现上下限幅 :
x = tf.range(9)
m = tf.clip_by_value(x, 2, 7)
print(m)
5.6 高级操作
5.6.1 tf.gather
tf.gather
可以实现根据索引号收集数据的目的。tf.gather
可以组合使用。
x = tf.random.uniform([4, 35, 8], maxval=100, dtype=tf.int32)
students = tf.gather(x, [1, 2], axis=0)
print(students.shape)
student = tf.gather(students, [2, 3, 5, 26], axis=1)
print(student.shape)
5.6.2 tf.gather_nd
通过 tf.gather_nd
,可以通过指定每次采样的坐标来实现采样多个点的目的。
我们将这个采样方案合并为一个 List 参数: [[1,1], [2,2], [3,3]], 通过tf.gather_nd
实现如下 :
# 根据多维度坐标收集数据
studentsList = tf.gather_nd(x, [[1, 1], [2, 2], [3, 3]])
print(studentsList)
一般地,在使用 tf.gather_nd
采样多个样本时, 如果希望采样第 i 号班级,第 j 个学生,第 k 门科目的成绩,则可以表达为[. . . , [𝑖, 𝑗, 𝑘], . . . ], 外层的括号长度为采样样本的个数,内层列表包含了每个采样点的索引坐标 。
# 根据多维度坐标收集数据
studentsList = tf.gather_nd(x, [[1, 1, 2], [2, 2, 3], [3, 3, 4]])
print(studentsList)
5.6.3 tf.boolean_mask
除了可以通过给定索引号的方式采样,还可以通过给定掩码(mask)的方式采样。
𝑚𝑎𝑠𝑘 = [𝑇𝑟𝑢𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝑇𝑟𝑢𝑒]
即采样第 1 和第 4 个班级,通过 tf.boolean_mask(x, mask, axis)
可以在 axis 轴上根据 mask 方案进行采样。
# 根据掩码方式采样班级
tf.boolean_mask(x,mask=[True, False, False, True], axis=0)
# 多维掩码采样
tf.boolean_mask(x,mask=[[True, True, False],[False, True, True]])
5.6.4 tf.where
通过 tf.where(cond, a, b)
操作可以根据 cond 条件的真假从 a 或 b 中读取数据 :
其中 i 为张量的索引, 返回张量大小与 a,b 张量一致, 当对应位置中𝑐𝑜𝑛𝑑𝑖为 True, 𝑜𝑖位置从𝑎𝑖中复制数据;当对应位置中𝑐𝑜𝑛𝑑𝑖为 False, 𝑜𝑖位置从𝑏𝑖中复制数据。
a = tf.ones([3, 3])
b = tf.zeros([3, 3])
cond = tf.constant([[True, False, False], [False, True, False], [True, True, False]])
x = tf.where(cond, a, b)
# 当 a=b=None 即 a,b 参数不指定时, tf.where 会返回 cond 张量中所有 True 的元素的索引坐标。
y = tf.where(cond)
print(y)
5.6.5 scatter_nd
通过 tf.scatter_nd(indices, updates, shape)
可以高效地刷新张量的部分数据,但是只能在全 0 张量的白板上面刷新,因此可能需要结合其他操作来实现现有张量的数据刷新功能。
- 需要刷新的数据索引为
indices
- 新数据为
updates
- 其中每个需要刷新的数据对应在白板中的位置, 根据
indices
给出的索引位置将updates
中新的数据依次写入白板中,并返回更新后的白板张量。
# 构造需要刷新数据的位置
indices = tf.constant([[4], [3], [1], [7]])
# 构造需要写入的数据
updates = tf.constant([4.4, 3.3, 1.1, 7.7])
# 在长度为8的全0向量上根据indices写入updates
y = tf.scatter_nd(indices, updates, [8])
print(y)
5.6.6 meshgrid
通过 tf.meshgrid
可以方便地生成二维网格采样点坐标,方便可视化等应用场合 。
x = tf.linspace(-8., 8, 100) # 设置 x 坐标的间隔
y = tf.linspace(-8., 8, 100) # 设置 y 坐标的间隔
x, y = tf.meshgrid(x, y) # 生成网格点,并拆分后返回
z = tf.sqrt(x**2+y**2)
z = tf.sin(z)/z # sinc 函数实现
fig = plt.figure()
ax = Axes3D(fig)
# 根据网格点绘制 sinc 函数 3D 曲面
ax.contour3D(x.numpy(), y.numpy(), z.numpy(), 50)
plt.show()
5.7经典数据加载
在 TensorFlow 中, keras.datasets
模块提供了常用经典数据集的自动下载、 管理、 加载与转换功能,并且提供了 tf.data.Dataset
数据集对象, 方便实现多线程(Multi-thread),预处理(Preprocess),随机打散(Shuffle)和批训练(Train on batch)等常用数据集功能。
对于常用的数据集:
-
Boston Housing 波士顿房价趋势数据集,用于回归模型训练与测试
-
CIFAR10/100 真实图片数据集,用于图片分类任务
-
MNIST/Fashion_MNIST 手写数字图片数据集,用于图片分类任务
-
IMDB 情感分类任务数据集
对于新提出的算法,一般优先在简单的数据集上面测试,再尝试迁移到更大规模、更复杂的数据集上
通过 datasets.xxx.load_data()
即可实现经典数据集的自动加载,其中 xxx
代表具体的数据集名称。
TensorFlow 会默认将数据缓存在用户目录下的.keras/datasets 文件夹 。
(x, y), (x_test, y_test) = datasets.mnist.load_data()
print('x:', x.shape, 'y:', y.shape, 'x_test:', x_test.shape, 'y_test:', y_test)
数据加载进入内存后,需要转换成 Dataset 对象,以利用 TensorFlow 提供的各种便捷功能。通过 Dataset.from_tensor_slices
可以将训练部分的数据图片 x 和标签 y 都转换成Dataset 对象:
train_db = tf.data.Dataset.from_tensor_slices((x, y))
将数据转换成 Dataset 对象后,一般需要再添加一系列的数据集标准处理步骤,如随机打散,预处理,按批装载等。
5.7.1 随机打散
通过 Dataset.shuffle(buffer_size)
工具可以设置 Dataset 对象随机打散数据之间的顺序,防止每次训练时数据按固定顺序产生,从而使得模型尝试“记忆”住标签信息 :
train_db = train_db.shuffle(10000)
- 其中 buffer_size 指定缓冲池的大小,一般设置为一个较大的参数即可。
- 通过 Dataset 提供的这些工具函数会返回新的 Dataset 对象可以通过
db = db. shuffle(). step2(). step3. ()
方式完成所有的数据处理步骤, 实现起来非常方便。
5.7.2 批训练
为了一次能够从 Dataset 中产生 batch size 数量的样本,需要设置 Dataset 为批训练方式:
train_db = train_db.batch(128)
其中 128 为 batch size 参数,即一次并行计算 128 个样本的数据。Batch size 一般根据用户的 GPU 显存资源来设置,当显存不足时,可以适量减少 batch size 来减少算法的显存使用量。
5.7.3 预处理
Dataset 对象通过提供 map(func)
工具函数可以非常方便地调用用户自定义的预处理逻辑, 它实现在 func 函数里:
# 预处理函数实现在 preprocess 函数中,传入函数引用即可
train_db = train_db.map(preprocess)
5.7.4 循环训练
对于 Dataset 对象, 在使用时可以通过
for step, (x, y) in enumerate(train_db): # 迭代数据集对象,带 step 参数
或者
for x, y in train_db: # 迭代数据集对象
方式进行迭代,每次返回的 x,y 对象即为批量样本和标签 ,当对train_db
的所有样本完成一次迭代后, for 循环终止退出。
- 我们一般把完成一个 batch 的数据训练,叫做一个 step;
- 通过多个 step 来完成整个训练集的一次迭代,叫做一个 epoch 。
在实际训练时,通常需要 对数据集迭代多个 epoch 才能取得较好地训练效果:
for epoch in range(20): # 训练epoch数
for step, (x, y) in enumerate(train_db): # 迭代Step数
# training...
或
train_db = train_db.repeat(20)