Tensorflow 之 张量操作

改变形状

通过 tf.reshape(x, new_shape),可以对张量的视图进行任意的合法改变

x=tf.range(96)
x=tf.reshape(x,[2,4,4,3])

增删维度

增加维度:增加一个长度为 1 的维度相当于给原有的数据增加一个新维度的概念,维度长度为 1,故数据并不需要改变,仅仅是改变数据的理解方式,因此它其实可以理解为改变视图的一种特殊方式。通过 tf.expand_dims(x, axis) 可在指定的 axis 轴前可以插入一个新的维度:

x = tf.random.uniform([28,28],maxval=10,dtype=tf.int32)
x = tf.expand_dims(x,axis=2)

删除维度:是增加维度的逆操作,与增加维度一样,删除维度只能删除长度为 1 的维度,也不会改变张量的存储。如果希望将图片数量维度删除,可以通过 tf.squeeze(x, axis) 函数,axis 参数为待删除的维度的索引号。

x = tf.random.uniform([1,28,28,1],maxval=10,dtype=tf.int32)
tf.squeeze(x,axis=0)

axis = None 时,代表删除任意长度为 1 的维度。

交换维度

改变视图、增删维度都不会影响张量的存储。在实现算法逻辑时,在保持维度顺序不变的条件下,仅仅改变张量的理解方式是不够的,有时需要直接调整的存储顺序,即交换维度(Transpose)。通过交换维度,改变了张量的存储顺序,同时也改变了张量的视图。使用 tf.transpose(x, perm) 函数完成维度交换操作,其中 perm 表示新维度的顺序 List。

x = tf.random.normal([2,32,32,3])
tf.transpose(x,perm=[0,2,1,3])

数据复制

通过 tf.tile(x, multiples) 函数可以指定数据在指定维度上的复制操作,multiples 为每个维度上面的复制倍数的 List,对应位置为 1 表明不复制,为 n 表示复制为原来的 n 倍,也就是该维度长度会变为原来的 n 倍。

x = tf.range(4)
x = tf.reshape(x,[2,2])
x = tf.tile(x,multiples=[3,2])

广播机制

Broadcasting 也叫广播机制(自动扩展也许更合适),它是一种轻量级张量复制的手段,在逻辑上扩展张量数据的形状,但是只要在需要时才会执行实际存储复制操作。对于大部分场景,Broadcasting 机制都能通过优化手段避免实际复制数据而完成逻辑运算,从而相对于 tf.tile 函数,减少了大量计算代价。

Broadcasting 实际上效果等同于对于长度为 1 的维度执行 tf.tile 复制操作,都能在此维度上逻辑复制数据若干份,区别在于 tf.tile 会创建一个新的张量,执行复制 IO 操作,并保存复制后的张量数据, Broadcasting 并不会立即复制数据,它会逻辑上改变张量的形状,使得视图上变成了复制后的形状。Broadcasting 会通过深度学习框架的优化手段避免实际复制数据而完成逻辑运算,至于怎么实现的用户不必关系,对于用于来说,Broadcasting 和 tf.tile 复制的最终效果是一样的,操作对用户透明,但是 Broadcasting 机制节省大量计算资源,建议在运算过程中尽可能地利用 Broadcasting 提高计算效率。

最简单和最常见的情况是,将标量与张量相乘或相加时。在这种情况下,标量将广播为与其他参数相同的形状:

x = tf.constant([1, 2, 3])

y = tf.constant(2)
z = tf.constant([2, 2, 2])
# All of these are the same computation
print(tf.multiply(x, 2))
print(x * y)
print(x * z)

结果为:

tf.Tensor([2 4 6], shape=(3,), dtype=int32)
tf.Tensor([2 4 6], shape=(3,), dtype=int32)
tf.Tensor([2 4 6], shape=(3,), dtype=int32)

同样,可以拉伸大小为 1 的维度以匹配其他参数。在同一计算中可以拉伸两个参数。

以下为例,3x1 矩阵乘以 1x4 矩阵以生成 3x4 矩阵。请注意前导 1 是可选的:y 的形状是 [4] 或 [1,4]。

# These are the same computations
x = tf.reshape(x,[3,1])
y = tf.range(1, 5)
print(x, "\n")
print(y, "\n")
print(tf.multiply(x, y))

结果为:

tf.Tensor(
[[1]
 [2]
 [3]], shape=(3, 1), dtype=int32) 

tf.Tensor([1 2 3 4], shape=(4,), dtype=int32) 

tf.Tensor(
[[ 1  2  3  4]
 [ 2  4  6  8]
 [ 3  6  9 12]], shape=(3, 4), dtype=int32)

图片示意如下:

在这里插入图片描述

与以下不使用广播的效果一样:

x_stretch = tf.constant([[1, 1, 1, 1],
                         [2, 2, 2, 2],
                         [3, 3, 3, 3]])

y_stretch = tf.constant([[1, 2, 3, 4],
                         [1, 2, 3, 4],
                         [1, 2, 3, 4]])

print(x_stretch * y_stretch)  # Again, operator overloading

结果为:

tf.Tensor(
[[ 1  2  3  4]
 [ 2  4  6  8]
 [ 3  6  9 12]], shape=(3, 4), dtype=int32)

也就是说进行张量计算时需要全部参数或者说全部的张量的维度一致,然后相同位置的元素执行操作。

数学运算

加减乘除是最基本的数学运算,分别通过 tf.add, tf.subtract, tf.multiply, tf.divide 函数实现,TensorFlow 已经重载了 + − ∗ / 运算符,一般推荐直接使用运算符来完成加减乘除运算。整除和余除也是常见的运算之一,分别通过 //% 运算符实现。通过 tf.pow(x, a) 可以方便地完成幂次运算操作,同时 TensorFlow 也重载了 ** 运算符实现了幂次运算。对于常见的平方和平方根运算,可以使用 tf.square(x)tf.sqrt(x) 实现。当然 ** 也可以实现指数运算。特别地,对于自然指数 ,可以通过 tf.exp(x) 实现。相对的自然对数运算需要借助 tf.math.log(x) 实现,如果希望计算其他底数的对数,可以根据对数的换底公式实现。矩阵相乘通过函数 tf.matmul(a,b) 实现,当然也重载了运算符 @ 实现矩阵相乘(注意这里的矩阵不包括向量,也就是只包括 shape为 [n,m,...] 的张量,即维度大于一)。当然上述操作会导致新张量的产生,如果想要对原张量进行操作,那么可以使用 assign_addassign_sub 实现原地加减操作。

a = tf.Variable(tf.range(1,6))
b = tf.Variable(tf.range(6,11))
print("a+b : ",a+b)
print("a-b : ",a-b)
print("a*b : ",a*b)
print("a/b : ",a/b)
print("a//b : ",a//b)
print("a%b : ",a%b)
print("a**b : ",a**b)
print("b**a : ",b**a)
print("tf.math.log(a)/tf.math.log(b) : ",tf.math.log(tf.cast(a,tf.float32))/tf.math.log(tf.cast(b,tf.float32)))
a.assign_sub(b)
print("a.assign_sub(b) : ",a)
a.assign_add(b)
print("a.assign_add(b) : ",a)
a = tf.random.normal([1,2,3])
b = tf.random.normal([3,1])
print("a@b : ",a@b)

输出为:

a+b :  tf.Tensor([ 7  9 11 13 15], shape=(5,), dtype=int32)
a-b :  tf.Tensor([-5 -5 -5 -5 -5], shape=(5,), dtype=int32)
a*b :  tf.Tensor([ 6 14 24 36 50], shape=(5,), dtype=int32)
a/b :  tf.Tensor([0.16666667 0.28571429 0.375      0.44444444 0.5       ], shape=(5,), dtype=float64)
a//b :  tf.Tensor([0 0 0 0 0], shape=(5,), dtype=int32)
a%b :  tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32)
a**b :  tf.Tensor([      1     128    6561  262144 9765625], shape=(5,), dtype=int32)
b**a :  tf.Tensor([     6     49    512   6561 100000], shape=(5,), dtype=int32)
tf.math.log(a)/tf.math.log(b) :  tf.Tensor([0.         0.35620716 0.52832085 0.63092977 0.69897   ], shape=(5,), dtype=float32)
a.assign_sub(b) :  <tf.Variable 'Variable:0' shape=(5,) dtype=int32, numpy=array([-5, -5, -5, -5, -5])>
a.assign_add(b) :  <tf.Variable 'Variable:0' shape=(5,) dtype=int32, numpy=array([1, 2, 3, 4, 5])>
a@b :  tf.Tensor(
[[[-2.6270187]
  [-1.0989319]]], shape=(1, 2, 1), dtype=float32)

统计特征

向量范数

在 TensorFlow 中,可以通过 tf.norm(x, ord) 求解张量的 L1, L2, ∞等范数,其中参数 ord 指定为 1,2 时计算 L1, L2 范数,指定为 np.inf 时计算 ∞ 范数 也就是取最大值。其中对于张量或矩阵会采取打平为向量后取向量范数的操作。以无穷范数为例:

import numpy as np
tf.norm(x,ord=np.inf) # 计算∞范数

输出为:

<tf.Tensor: shape=(), dtype=float32, numpy=1.0>

最大最小值、均值、和

通过 tf.reduce_max, tf.reduce_min, tf.reduce_mean, tf.reduce_sum 可以求解张量在某个维度上的最大、最小、均值、和,也可以求全局最大、最小、均值、和。同时通过 tf.argmax(x, axis)tf.argmin(x, axis) 可以求解在 axis 轴上,x 的最大值、最小值所在的索引号。

x = tf.random.normal([2,3,4])
print(x)
print(tf.math.reduce_max(x,axis=0)) # 统计维度 0 上的最大值
print(tf.argmax(x,axis=0)) # 统计维度 0 上的最大值的索引号
print(tf.math.reduce_min(x,axis=1)) # 统计维度 1 上的最小值
print(tf.argmin(x,axis=1)) # 统计维度 0 上的最小值的索引号
print(tf.math.reduce_mean(x,axis=None)) # 统计全局的平均值
print(tf.math.reduce_sum(x,axis=None)) # 统计全局的和

输出为:

tf.Tensor(
[[[-0.8554411  -1.1995217  -0.07226285 -0.80842066]
  [ 1.0545903   1.5078677  -1.0189347   0.13855955]
  [-0.9500216   0.45217472  1.5256957   1.4739561 ]]

 [[ 0.45588306 -0.16753301 -0.17005087 -0.15572974]
  [-0.25932798  0.6888086   1.1512228  -0.44160977]
  [-0.81231385 -0.9079794   1.752819    1.2487847 ]]], shape=(2, 3, 4), dtype=float32)
tf.Tensor(
[[ 0.45588306 -0.16753301 -0.07226285 -0.15572974]
 [ 1.0545903   1.5078677   1.1512228   0.13855955]
 [-0.81231385  0.45217472  1.752819    1.4739561 ]], shape=(3, 4), dtype=float32)
tf.Tensor(
[[1 1 0 1]
 [0 0 1 0]
 [1 0 1 0]], shape=(3, 4), dtype=int64)
tf.Tensor(
[[-0.9500216  -1.1995217  -1.0189347  -0.80842066]
 [-0.81231385 -0.9079794  -0.17005087 -0.44160977]], shape=(2, 4), dtype=float32)
tf.Tensor(
[[2 0 1 0]
 [2 2 0 1]], shape=(2, 4), dtype=int64)
tf.Tensor(0.15130064, shape=(), dtype=float32)
tf.Tensor(3.6312153, shape=(), dtype=float32)

张量比较

函数 功能
tf.math.equal 𝑏=𝑐
tf.math.greater 𝑏>𝑐
tf.math.less 𝑏<𝑐
tf.math.greater_equal 𝑏≥𝑐
tf.math.less_equal 𝑏≤𝑐
tf.math.not_equal 𝑏≠𝑐
tf.math.is_nan 𝑏=nan

这里以比较两个张量是否相等为例:

a = tf.Variable([1,2,3,4,5])
b = tf.Variable([0,2,6,4,7])
tf.math.equal(a,b)

输出为:

<tf.Tensor: shape=(5,), dtype=bool, numpy=array([False,  True, False,  True, False])>

数据限幅

在 TensorFlow 中,可以通过 tf.maximum(x, b) 实现数据的下限幅:𝑦 ∈ [𝑏,+∞);可以通过 tf.minimum(x, b) 实现数据的上限幅:𝑦 ∈ (−∞,𝑏],可以通过 tf.clip_by_value 实现上下限幅。举例如下:

x = tf.range(9)
print(tf.maximum(x,2)) # 下限幅 2
print(tf.minimum(x,7)) # 上限幅 7

输出为

tf.Tensor([2 2 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.Tensor([2 2 2 3 4 5 6 7 7], shape=(9,), dtype=int32)

网格数据

考虑一个 3D 函数:

\[z = \frac { \sin \left( \sqrt {x ^ { 2 } + y ^ { 2 } } \right) } { \sqrt {x ^ { 2 } + y ^ { 2 } }} \]

现在可以通过 tf.meshgrid 可以方便地生成二维网格采样点坐标,将其可视化。

import numpy as np
import matplotlib.pyplot as plt
#这里只是用Axes3D函数,所以只导入了Axes3D
from mpl_toolkits.mplot3d import Axes3D

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()

图像如下:

在这里插入图片描述

数据填充

对于图片数据的高和宽、序列信号的长度,维度长度可能各不相同。为了方便网络的并行计算,需要将不同长度的数据扩张为相同长度。通常的做法是,在需要补充长度的信号开始或结束处填充足够数量的特定数值,如 0,使得填充后的长度满足系统要求。那么这种操作就叫做填充(Padding)。

填充操作可以通过 tf.pad(x, paddings) 函数实现,paddings 是包含了多个
[左侧填充个数,右侧填充个数] 的嵌套方案 List,List 中的第 n 个元素表示第 n 维的填充方案,如 [[0,0],[2,1],[1,2]] 表示第一个维度不填充,第二个维度左边(起始处)填充两个单元,右边(结束处)填充一个单元,第三个维度左边填充一个单元,右边填充两个单元。以下代码为例:

t = tf.constant([[1, 2, 3], [4, 5, 6]])
paddings = tf.constant([[1, 1,], [2, 2]])
# 'constant_values' is 0.
# rank of 't' is 2.
tf.pad(t, paddings, "CONSTANT")  # [[0, 0, 0, 0, 0, 0, 0],
                                 #  [0, 0, 1, 2, 3, 0, 0],
                                 #  [0, 0, 4, 5, 6, 0, 0],
                                 #  [0, 0, 0, 0, 0, 0, 0]]

tf.pad(t, paddings, "REFLECT")  # [[6, 5, 4, 5, 6, 5, 4],
                                #  [3, 2, 1, 2, 3, 2, 1],
                                #  [6, 5, 4, 5, 6, 5, 4],
                                #  [3, 2, 1, 2, 3, 2, 1]]

tf.pad(t, paddings, "SYMMETRIC")  # [[2, 1, 1, 2, 3, 3, 2],
                                  #  [2, 1, 1, 2, 3, 3, 2],
                                  #  [5, 4, 4, 5, 6, 6, 5],
                                  #  [5, 4, 4, 5, 6, 6, 5]]

现在将 CONSTANT 填充模式分解成 n 步,重复一遍:

t = tf.constant([[1, 2, 3], [4, 5, 6]])
t = tf.pad(t, [[1, 1,], [0, 0,]], "CONSTANT")
print(t)
t = tf.pad(t, [[0, 0,], [2, 2,]], "CONSTANT")
print(t)

输出为:

tf.Tensor(
[[0 0 0]
 [1 2 3]
 [4 5 6]
 [0 0 0]], shape=(4, 3), dtype=int32)
tf.Tensor(
[[0 0 0 0 0 0 0]
 [0 0 1 2 3 0 0]
 [0 0 4 5 6 0 0]
 [0 0 0 0 0 0 0]], shape=(4, 7), dtype=int32)

还有一个特殊的填充函数 tf.scatter_nd,其实现了根据索引将已有数据分散为新的张量。简单来说就是根据索引 indices 将已有的数据 updates 中新的数据依次写入白板中,并返回更新后的白板张量。以一维向量为例,其示意图如下:

在这里插入图片描述
代码如下:

indices = tf.constant([[4], [3], [1], [7]])
updates = tf.constant([9, 10, 11, 12])
shape = tf.constant([8])
scatter = tf.scatter_nd(indices, updates, shape)
print(scatter)

输出为:

tf.Tensor([ 0 11  0 10  9  0  0 12], shape=(8,), dtype=int32)

以多维张量为例,其示意图如下
在这里插入图片描述
代码如下:

indices = tf.constant([[0], [2]])
updates = tf.constant([[[5, 5, 5, 5], [6, 6, 6, 6],
                        [7, 7, 7, 7], [8, 8, 8, 8]],
                       [[5, 5, 5, 5], [6, 6, 6, 6],
                        [7, 7, 7, 7], [8, 8, 8, 8]]])
shape = tf.constant([4, 4, 4])
scatter = tf.scatter_nd(indices, updates, shape)
print(scatter)

输出为:

tf.Tensor(
[[[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]]

 [[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]]], shape=(4, 4, 4), dtype=int32)

数据采样

通过 tf.gather 可以实现根据索引号针对特定维度收集数据的目的。通过 tf.gather_nd 可以通过指定每次采样的坐标来实现采样多个点的目的。除了可以通过给定索引号的方式采样,tf.boolean_mask 实现通过给定掩码 (mask) 的方式采样。下面以二维张量展示其简单的应用:

# 2-D example
tensor = tf.constant([[1, 2, 3], [4, 5, 6]])
print(tf.gather_nd(tensor,[[1,1],[0,2],[0,0]]))
print(tf.gather(tensor,[1,0,2],axis=1))
print(tf.boolean_mask(tensor,[[True,True,False],[False,True,True]]))

输出为:

tf.Tensor([5 3 1], shape=(3,), dtype=int32)
tf.Tensor(
[[2 1 3]
 [5 4 6]], shape=(2, 3), dtype=int32)
tf.Tensor([1 2 5 6], shape=(4,), dtype=int32)

张量合并

拼接

在 TensorFlow 中,可以通过 tf.concat(tensors, axis),其中 tensors 保存了所有需要合并的张量 List,axis 指定需要合并的维度。

a = tf.random.normal([2,3])
b = tf.random.normal([1,3])
c = tf.concat([a,b],axis=0)

print(a)
print(b)
print(c)

输出为:

tf.Tensor(
[[-0.6666865   0.11457029 -0.20638017]
 [-0.16385473 -1.3571881  -0.1474465 ]], shape=(2, 3), dtype=float32)
tf.Tensor([[ 0.3256825  -0.3882357  -0.24900076]], shape=(1, 3), dtype=float32)
tf.Tensor(
[[-0.6666865   0.11457029 -0.20638017]
 [-0.16385473 -1.3571881  -0.1474465 ]
 [ 0.3256825  -0.3882357  -0.24900076]], shape=(3, 3), dtype=float32)

堆叠

tf.concat 直接在现有维度上面合并数据,并不会创建新的维度。如果在合并数据时,希望创建一个新的维度,则需要使用 tf.stack 操作。使用 tf.stack(tensors, axis) 可以合并多个张量 tensors,其中 axis 指定插入新维度的位置,axis 的用法与 tf.expand_dims 的一致,当 axis ≥ 0时在 axis 之前插入;当 axis < 0 时,在最后插入新维度。

a = tf.random.normal([1,3])
b = tf.random.normal([1,3])
c = tf.stack([a,b],axis=-1)

print(a)
print(b)
print(c)

输出为:

tf.Tensor([[ 0.4222968  -1.4700925   0.00859822]], shape=(1, 3), dtype=float32)
tf.Tensor([[-0.92276585  0.55793864  0.27060276]], shape=(1, 3), dtype=float32)
tf.Tensor(
[[[ 0.4222968  -0.92276585]
  [-1.4700925   0.55793864]
  [ 0.00859822  0.27060276]]], shape=(1, 3, 2), dtype=float32)

张量分割

通过 tf.split(x, axis, num_or_size_splits) 可以完成张量的分割操作,其中

  • x:待分割张量
  • axis:分割的维度索引号
  • num_or_size_splits:切割方案。当 num_or_size_splits 为单个数值时,如 10,表示切割为 10 份;当 num_or_size_splits 为 List,每个元素表示每份的长度,如 [2,4,2,2] 表示切割为 4 份,每份的长度分别为 2,4,2,2。
a = tf.random.normal([1,2,3])
b = tf.split(a,axis=2,num_or_size_splits=[1,2])

print(a)
for i in b:
	print(i)

输出为

tf.Tensor(
[[[-0.141147   -0.611212    0.33784124]
  [-0.16212434  1.4793061   0.1870316 ]]], shape=(1, 2, 3), dtype=float32)
tf.Tensor(
[[[-0.141147  ]
  [-0.16212434]]], shape=(1, 2, 1), dtype=float32)
tf.Tensor(
[[[-0.611212    0.33784124]
  [ 1.4793061   0.1870316 ]]], shape=(1, 2, 2), dtype=float32)

如果希望在某个维度上全部按长度为 1 的方式分割,还可以直接使用 tf.unstack(x,axis)。这种方式是 tf.split 的一种特殊情况,切割长度固定为 1,只需要指定切割维度即
可。

数据集处理

对于数据集类型 tf.data.Dataset,常用的处理函数有四个 shufflebatchmaprepeatshuffle(buffersize) 实现了数据的随机打散,其中 buffersize 为缓冲区大小,输出的数据将从缓冲区随机采样获得,当有一个数据被取出缓冲区后,将从剩下的数据中向缓存区补充。batch(bacthsize) 实现了批量数据的获取,即当使用迭代器获取数据时将从数据集中获取大小为 bacthsize 的一批数据。map(map_func) 通过函数 map_func 可以实现数据的预处理。repeat(count) 实现了数据集重复 count 次,也就是通过迭代器获取数据时会表现为原来的 count 倍。当对 train_db 的所有样本完成一次迭代后,for 循环终止退出。我们一般把完成一个 batch 的数据训练,叫做一个 step;通过多个 step 来完成整个训练集的一次迭代,叫做一个 epoch。那么在这里的 count 与 epoch 的意义一样。

def preprocess(x, y): 
    x = tf.cast(x, dtype=tf.float32) / 255.
    x = tf.reshape(x, [-1, 28*28])
    y = tf.cast(y, dtype=tf.int32)
    y = tf.one_hot(y, depth=10)
    return x,y
    
(x, y), (x_test, y_test) = datasets.mnist.load_data()
batchsz = 512
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(60000).batch(batchsz).map(preprocess).repeat(100)

反向传播算法

利用上述的介绍的工具可以很简单的实现反向传播算法,代码如下:

import  matplotlib
from    matplotlib import pyplot as plt
# Default parameters for plots
matplotlib.rcParams['font.size'] = 20
matplotlib.rcParams['figure.titlesize'] = 20
matplotlib.rcParams['figure.figsize'] = [9, 7]
matplotlib.rcParams['font.family'] = ['KaiTi']
matplotlib.rcParams['axes.unicode_minus']=False 

import  tensorflow as tf
from    tensorflow import keras
from    tensorflow.keras import datasets, layers, optimizers
import  os

os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
print(tf.__version__)

def preprocess(x, y): 
    x = tf.cast(x, dtype=tf.float32) / 255.
    x = tf.reshape(x, [-1, 28*28])
    y = tf.cast(y, dtype=tf.int32)
    y = tf.one_hot(y, depth=10)
    return x,y

def load_data():
    (x, y), (x_test, y_test) = datasets.mnist.load_data()

    batchsz = 512
    train_db = tf.data.Dataset.from_tensor_slices((x, y))
    train_db = train_db.shuffle(60000).batch(batchsz).map(preprocess) # .repeat(100)

    test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
    test_db = test_db.shuffle(60000).batch(batchsz).map(preprocess)
    return train_db,test_db


def init_paramaters():
    tf.random.set_seed(42)
    # 每层的张量都需要被优化,故使用 Variable 类型,并使用截断的正太分布初始化权值张量
    # 偏置向量初始化为 0 即可
    # 第一层的参数
    w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
    b1 = tf.Variable(tf.zeros([256]))
    # 第二层的参数
    w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
    b2 = tf.Variable(tf.zeros([128]))
    # 第三层的参数
    w3 = tf.Variable(tf.random.truncated_normal([128, 10], stddev=0.1))
    b3 = tf.Variable(tf.zeros([10]))
    return w1, b1, w2, b2, w3, b3


def train_epoch(train_dataset, w1, b1, w2, b2, w3, b3, lr=0.001):
    
    acc_meter = metrics.Accuracy()

    for step, (x, y) in enumerate(train_dataset):
        with tf.GradientTape() as tape:
            # 第一层计算, [b, 784]@[784, 256] + [256] => [b, 256] + [256] => [b,256] + [b, 256]
            h1 = x @ w1 + b1
            h1 = tf.nn.relu(h1)  # 通过激活函数

            # 第二层计算, [b, 256] => [b, 128]
            h2 = h1 @ w2 + b2
            h2 = tf.nn.relu(h2)
            # 输出层计算, [b, 128] => [b, 10]
            out = h2 @ w3 + b3

            # 计算网络输出与标签之间的均方差, mse = mean(sum(y-out)^2)
            # [b, 10]
            loss = tf.square(y - out)
            # 误差标量, mean: scalar
            loss = tf.reduce_mean(loss)

            # 自动梯度,需要求梯度的张量有[w1, b1, w2, b2, w3, b3]
            grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3]) # 使用自动梯度无需推到梯度反向传播公式

            # 梯度更新, assign_sub 将当前值减去参数值,原地更新
            for p, g in zip([w1, b1, w2, b2, w3, b3], grads):
                p.assign_sub(lr * g)

            acc_meter.update_state(tf.argmax(out, axis=1), tf.argmax(y, axis=1))

    return float(loss), float(acc_meter.result())

def acc(test_dataset, w1, b1, w2, b2, w3, b3):

    acc_meter = metrics.Accuracy()
    loss = 0

    for step, (x, y) in enumerate(test_dataset):

        h1 = x @ w1 + b1
        h1 = tf.nn.relu(h1)  # 通过激活函数

        # 第二层计算, [b, 256] => [b, 128]
        h2 = h1 @ w2 + b2
        h2 = tf.nn.relu(h2)
        # 输出层计算, [b, 128] => [b, 10]
        out = h2 @ w3 + b3

        # 计算网络输出与标签之间的均方差, mse = mean(sum(y-out)^2)
        
        loss += tf.reduce_mean(tf.square(y - out))

        acc_meter.update_state(tf.argmax(out, axis=1), tf.argmax(y, axis=1))

    return float(loss), float(acc_meter.result())


def train(epochs):
    accs_train = []
    accs_val = []
    train_dataset, test_dataset= load_data()
    w1, b1, w2, b2, w3, b3 = init_paramaters()
    for epoch in range(epochs):
        loss_train,acc_train = train_epoch(train_dataset, w1, b1, w2, b2, w3, b3, lr=0.01)
        loss_val,acc_val = acc(test_dataset, w1, b1, w2, b2, w3, b3)
        print(epoch, 'loss_train:', loss_train, 'acc_train:', acc_train, 'loss_val:', loss_val, 'acc_val:', float(acc_val))
        accs_train.append(acc_val)
        accs_val.append(acc_train)

    x = [i for i in range(0, epochs)]
    # 绘制曲线
    plt.figure()
    plt.plot(x, accs_val, color='blue', label='vaildation')
    plt.plot(x, accs_train, color='red', label='training') #, marker='s'
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy')
    plt.legend()
    plt.show()
    plt.close()


if __name__ == '__main__':
    train(epochs=100)

迭代优化的训练曲线如下:

在这里插入图片描述

posted @ 2021-04-28 17:54  FlameAlpha  阅读(416)  评论(0编辑  收藏  举报