通过TensorFlow2.0训练神经网络模型
在神经网络优化算法中,最常用的方法是反向传播算法(backpropagation),其工作流程如下图:
如图所示,反向传播算法
实现了一个迭代
的过程。每次迭代的开始,都选取一部分训练数据,这一小部分数据叫做一个batch
。然后,这个batch的样例会通过前向传播算法
得到神经网络模型的预测结果
。因为训练数据都是有正确答案标注的,所以可以计算出当前神经网络模型的预测答案与正确答案之间的差距。最后,基于这个差距,通过反向传播算法
会更新神经网络参数的取值,使得在这个batch上神经网络的预测结果与真实答案更加接近。
TensorFlow v1中神经网络模型的训练
但是如果每轮迭代中选取的数据都要通过常量来表示,那么TensorFlow的计算图将会很大。因为每生成一个常量,TensorFlow都会在计算图中增加一个节点。一般来说,一个神经网络的训练过程会需要经过几百万轮甚至几亿轮的迭代,这样的计算图就会很大,且利用率很低。因此,TensorFlow提供了placeholder机制用于提供输入数据。placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定。这样就只需要将数据通过placeholder传入TensorFlow计算图。
在placeholder定义时,这个位置上的数据类型是需要指定的。和张量一样,placeholder的类型也是不可改变的。placeholder中数据的维度信息可以根据提供的数据推导得出。下面给出通过placeholder实现的前向传播算法(基于TensorFlow 2.5):
# -*- coding: utf-8 -*-#
# ----------------------------------------------
# Name: NN02.py
# Description:
# Author: PANG
# Date: 2022/2/5
# ----------------------------------------------
import tensorflow._api.v2.compat.v1 as tf # 引入TF1
tf.disable_v2_behavior() # 关闭v2版的特性
# 声明w1,w2两个变量,这里还通过seed固定随机种子,保证每次运行的结果一样
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
# 定义placeholder作为数据存放的地方,给定维度可以降低出错的概率。
x = tf.placeholder(tf.float32, shape=(1, 2), name='input')
# 输入N*m维数组,N大于1。
xx = tf.placeholder(tf.float32, shape=(3, 2), name='input')
# 暂时将输入的特征向量定义为一个常量。X是一个1*2的矩阵
# x = tf.constant([[0.7,0.9]])
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
aa = tf.matmul(xx, w1)
yy = tf.matmul(aa, w2)
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
# print(sess.run(y))#会报错,需要提供一个feed_dict来指定x的取值。
print(sess.run(y, feed_dict={x: [[0.7, 0.9]]}))
# 当多维数组N*m输入时,输出N*1个结果
print(sess.run(yy, feed_dict={xx: [[0.7, 0.9], [0.1, 0.4], [0.5, 0.8]]}))
sess.close()
注意:feed_dict是一个字典(map),在字典中需要给出每个用到placeholder的取值。如果某个需要的placeholder没有被指定取值,那么程序在运行时就会报错。
在得到一个batch的前向传播结果后,需要定义一个损失函数
来刻画当前预测值与真实答案之间的差距。然后通过反向传播算法来调整神经网络参数的取值使得差距可以被缩小。
# 定义损失函数来刻画预测值与真实值之间的差距
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的参数
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)
TensorFlow v1目前有7种优化器,比较常用的优化方法有三种:
tf.train.GradientDescentOptimizer
tf.train.AdamOptimizer
tf.train.MomentumOptimizer
在定义反向传播算法之后,通过运行sess.run(train_step)
就可以对所在集合中的变量进行优化,使得当前batch的损失函数最小。
完整的神经网络训练程序
:
# -*- coding: utf-8 -*-#
# ----------------------------------------------
# Name: NN02.py
# Description:
# Author: PANG
# Date: 2022/2/5
# ----------------------------------------------
import numpy as np
import tensorflow._api.v2.compat.v1 as tf
# NumPy是一个科学计算工具包,通过NumPy工具包生成模拟数据集
from numpy.random import RandomState
tf.disable_v2_behavior()
# 定义训练数据batch的大小
batch_size = 8
# 定义神经网络的参数
# 声明w1,w2两个变量,这里通过seed固定随机种子,保证每次运行的结果一样
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
# 定义placeholder作为数据存放的点,给定维度可以降低出错的概率。
# 在shape的一个维度上使用None可以方便使用不同的batch大小。
# 在训练时需要把数据分成较小的batch,但是在测试时,可以一次性使用全部的数据。
# 当数据集比较小的时候这样可以方便测试,但是数据集比较大时,将大量数据放入一个batch会导致数据溢出。
x = tf.placeholder(tf.float32, shape=(None, 2), name='x-input')
# 输入N*m维数组,N大于1。
y_ = tf.placeholder(tf.float32, shape=(None, 1), name='y-input')
# 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
# 定义损失函数和反向传播的算法,损失函数来刻画预测值与真实值的差距
y = tf.sigmoid(y)
cross_entropy = -tf.reduce_mean(
y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)) + (1 - y_) * tf.log(tf.clip_by_value(1 - y, 1e-10, 1.0)))
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的参数
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)
# 通过随机数生成一个模拟数据集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)
# 定义规则给出样本的标签。在这x1+x2<1的样例认为是正样本,
# 而其他为负样本。这里使用0表示负样本,1来表示正样本
Y = [[int(x1 + x2 < 1) for (x1, x2) in X]]
Y = np.array(Y).reshape(-1, 1)
print(Y)
# 创建一个会话来运行TensorFlow程序
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(w1))
print(sess.run(w2))
# 设定训练次数
Steps = 5000
for i in range(Steps):
# 每次选择batch_size个样本进行训练
start = (i * batch_size) % dataset_size
# print(start)
end = min(start + batch_size, dataset_size)
# 通过选取的样本训练神经网络并更新参数
sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start:end]})
if i % 1000 == 0:
# 每隔一段时间计算所有数据的交叉熵并输出
total_cross_entropy = sess.run(cross_entropy, feed_dict={x: X, y_: Y})
print('循环:%d,交叉熵:%g' % (i, total_cross_entropy))
print(sess.run(w1))
print(sess.run(w2))
训练神经网络的三个步骤:
- 定义神经网络的结构和前向传播的输出结果;
- 定义损失函数以及选取反向传播优化的算法;
- 生成会话(tf.Session)并且在训练数据上反复运行反向传播优化算法。
计算图(tf.Graph)是TensorFlow的计算模型,所有TensorFlow的程序都会通过计算图的形式表示。计算图上的每一个节点都是一个运算,而计算图上的边则表示了运算之间的数据传递关系。计算图上的边还保存了运行每个运算的设备信息以及运算之间的依赖关系。
张量是TensorFlow的数据模型,TensorFlow种所有运算的输入,输出都是张量。张量本身并不存储任何数据,它只是对运算结果的引用。
会话是TensorFlow的运算模型,它管理了一个TensorFlow程序拥有的系统资源,所有的运算都要通过会话执行。
TensorFlow v2中神经网络的训练
梯度下降法
梯度
∇
f
=
(
∂
f
∂
x
1
,
∂
f
∂
x
2
,
.
.
.
,
∂
f
∂
x
n
)
\nabla f=(\frac{\partial f}{\partial x_1},\frac{\partial f}{\partial x_2},...,\frac{\partial f}{\partial x_n} )
∇f=(∂x1∂f,∂x2∂f,...,∂xn∂f)指函数关于变量x的导数,梯度的方向表示函数值增大的方向,梯度的模表示函数值增大的速率。那么只要不断地将参数的值朝着梯度的反方向更新一定大小,就能得到函数的最小值(全局最小值或者局部最小值)
θ
t
+
1
=
θ
t
−
α
∇
f
(
θ
t
)
\theta _{t+1}=\theta _t-\alpha \nabla f(\theta _t)
θt+1=θt−α∇f(θt)
上述参数更新过程就叫做梯度下降法,但是一般利用梯度更新参数时会将梯度乘以一个小于1的学习速率(learning rate),这是因为往往梯度的模还是比较大的,直接用其更新参数会使得函数值不断波动,很难收敛到一个平衡点。
但是对于不同的函数,梯度下降法未必都能找到最优解,很多时候它只能收敛到一个局部最优解就不再变动了,尽管这个局部最优解可能已经很接近全局最优解了。实验证明,梯度下降法对于凸函数有着较好的表现。
TensorFlow和PyTorch这类深度学习框架是支持自动梯度求解的,在TensorFlow v2中只要将需要进行梯度求解的代码包裹在GradientTape中,TensorFlow就会自动求解相关运算的梯度。但是通过tape.gradient(loss, [w1, w2,...])
只能调用一次,梯度作为占用显存较大的资源在被获取一次后就会被释放掉,要想多次调用需要设置tf.GradientTape(persistent=True)
。TensorFlow v2也支持多阶求导,只需要将求导进行多次包裹即可。例如:
反向传播
反向传播算法(BP)
是训练深度神经网络的核心算法,它的实现是基于链式法则
的。将输出层的loss
通过权值反向传播(前向传播的逆运算)回第i层(这是个反复迭代返回的过程),计算i层的梯度更新参数。
在TensorFlow2
中,对于经典的BP神经网络层
进行了封装,称为全连接层
,自动完成BP神经网络隐层的操作。下面为使用Dense层构建BP神经网络训练Fashion_MNIST
数据集进行识别的代码。
# -*- coding: utf-8 -*-#
# ----------------------------------------------
# Name: NN03.py
# Description: 反向传播算法示例
# Author: PANG
# Date: 2022/2/5
# ----------------------------------------------
import tensorflow as tf
from tensorflow.keras import datasets, layers, optimizers, Sequential
(x, y), (x_test, y_test) = datasets.fashion_mnist.load_data()
print(x.shape, y.shape)
def preprocess(x, y):
x = tf.cast(x, dtype=tf.float32) / 255.
y = tf.cast(y, dtype=tf.int32)
return x, y
batch_size = 64
db = tf.data.Dataset.from_tensor_slices((x, y))
db = db.map(preprocess).shuffle(10000).batch(batch_size)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.map(preprocess).shuffle(10000).batch(batch_size)
model = Sequential([
layers.Dense(256, activation=tf.nn.relu), # [b, 784] => [b, 256]
layers.Dense(128, activation=tf.nn.relu), # [b, 256] => [b, 128]
layers.Dense(64, activation=tf.nn.relu), # [b, 128] => [b, 64]
layers.Dense(32, activation=tf.nn.relu), # [b, 64] => [b, 32]
layers.Dense(10), # [b, 32] => [b, 10]
])
model.build(input_shape=([None, 28 * 28]))
optimizer = optimizers.Adam(lr=1e-3)
def main():
# forward
for epoch in range(30):
for step, (x, y) in enumerate(db):
x = tf.reshape(x, [-1, 28 * 28])
with tf.GradientTape() as tape:
logits = model(x)
y_onthot = tf.one_hot(y, depth=10)
loss_mse = tf.reduce_mean(tf.losses.MSE(y_onthot, logits))
loss_ce = tf.reduce_mean(tf.losses.categorical_crossentropy(y_onthot, logits, from_logits=True))
grads = tape.gradient(loss_ce, model.trainable_variables)
# backward
optimizer.apply_gradients(zip(grads, model.trainable_variables))
if step % 100 == 0:
print(epoch, step, "loss:", float(loss_mse), float(loss_ce))
# test
total_correct, total_num = 0, 0
for x, y in db_test:
x = tf.reshape(x, [-1, 28 * 28])
logits = model(x)
prob = tf.nn.softmax(logits, axis=1)
pred = tf.cast(tf.argmax(prob, axis=1), dtype=tf.int32)
correct = tf.reduce_sum(tf.cast(tf.equal(pred, y), dtype=tf.int32))
total_correct += int(correct)
total_num += int(x.shape[0])
acc = total_correct / total_num
print("acc", acc)
if __name__ == '__main__':
main()
参考资料
- 《TensorFlow:实战Google深度学习框架》
- https://zhouchen.blog.csdn.net/article/details/102572264