第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 中读取数据 :

\[O_i = \begin{cases} a_i,cond_i为True\\ b_i,cond_i为False \end{cases} \]

其中 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)
posted @ 2021-10-26 21:19  Reversal-destiny  阅读(84)  评论(0编辑  收藏  举报