3.3 训练神经网络
上面的神经网络已经可以运行,但显然还不能真正使用,因为它最终的计算结果是存在误差的。神经网络在投入使用前,都要经过训练的过程。那么,如何来训练神经网络呢?
如图3.3所示,神经网络的训练过程包含下面几个步骤。
• 输入数据:例如本章例子中输入的x1、x2、x3,也就是两位学生各自的德育、智育、体育3项分数。
• 计算结果:神经网络根据输入的数据和当前的可变参数值计算出结果,本章例子中为y。
• 计算误差:将计算出来的结果y与我们期待的结果(或者说标准答案,把它暂时称为yTrain)进行比对,看看误差(loss)是多少;在本章例子中,yTrain的值也就是两位学生各自已知的总分。
• 调整神经网络可变参数:根据误差的大小,使用反向传播算法,对神经网络中的可变参数(也就是本章例子中的w1、w2、w3)进行相应的调节。
• 再次训练:在调整完可变参数后,重复上述步骤重新进行训练,直至误差低于我们的理想水平,神经网络的训练就完成了。
上一节中编写的程序已经实现了这个流程中的前两个步骤,下面我们就来实现剩余的步骤。
图3.3 神经网络训练流程图
代码3.2 score1b.py
import tensorflow as tf x1 = tf.placeholder(dtype=tf.float32) x2 = tf.placeholder(dtype=tf.float32) x3 = tf.placeholder(dtype=tf.float32) yTrain = tf.placeholder(dtype=tf.float32) w1 = tf.Variable(0.1, dtype=tf.float32) w2 = tf.Variable(0.1, dtype=tf.float32) w3 = tf.Variable(0.1, dtype=tf.float32) n1 = x1 * w1 n2 = x2 * w2 n3 = x3 * w3 y = n1 + n2 + n3 loss = tf.abs(y - yTrain) optimizer = tf.train.RMSPropOptimizer(0.001) train = optimizer.minimize(loss) sess = tf.Session() init = tf.global_variables_initializer() sess.run(init) result = sess.run([train, x1, x2, x3, w1, w2, w3, y, yTrain, loss], feed_dict={x1: 90, x2: 80, x3: 70, yTrain: 85}) print(result) result = sess.run([train, x1, x2, x3, w1, w2, w3, y, yTrain, loss], feed_dict={x1: 98, x2: 95, x3: 87, yTrain: 96}) print(result)
上面的代码3.2中,与前面代码3.1中有区别的地方已经用粗体文字标出。我们来看看这些有区别的地方。
x3 = tf.placeholder(dtype=tf.float32)
yTrain = tf.placeholder(dtype=tf.float32)
在定义了x1、x2、x3这3个输入数据的占位符后,我们这次又定义了一个占位符yTrain,这是用来在训练时传入针对每一组输入数据我们期待的对应计算结果值的,后面一般把它简称为“目标计算结果”或“目标值”。
y = n1 + n2 + n3 loss = tf.abs(y - yTrain) optimizer = tf.train.RMSPropOptimizer(0.001) train = optimizer.minimize(loss)
在定义模型中计算出结果y后,我们用tf.abs(y-yTrain)来计算神经网络的计算结果y与目标值yTrain之间的误差,tf.abs函数是用来计算绝对值的。这也很好理解,我们是期望y与yTrain越接近越好,而不是y-yTrain的结果越小越好,因为y-yTrain的结果可能是负数,所以最终计算误差loss是用y-yTrain的绝对值来表示。
然后定义了一个优化器变量optimizer。所谓优化器,就是用来调整神经网络可变参数的对象。TensorFlow中有很多种优化器,我们这次选用的就是AlphaGo使用的优化器RMSPropOptimizer。这个优化器是通过调用tf.train.RMSPropOptimizer函数来获得的;其中的参数0.001是这个优化器的学习率(learn rate)。关于学习率,后面会有专门的章节讨论,暂时可理解为:学习率决定了优化器每次调整参数的幅度大小,先赋一个常用的数值0.001。
定义完优化器后,我们又定义了一个训练对象train。train对象代表了我们准备如何来训练这个神经网络。可以看到,我们把train对象定义为optimizer.minimize(loss),也就是要求优化器按照把loss最小化(minimize)的原则来调整可变参数。
定义好训练方式后,就可以正式开始训练了,训练的代码与之前计算的很相似。
result = sess.run([train, x1, x2, x3, w1, w2, w3, y, yTrain, loss], feed_dict={x1: 90, x2: 80, x3: 70, yTrain: 85}) print(result) result = sess.run([train, x1, x2, x3, w1, w2, w3, y, yTrain, loss], feed_dict={x1: 98, x2: 95, x3: 87, yTrain: 96}) print(result)
不同之处主要有两个,一是在feed_dict参数中多指定了一个yTrain的数值,也就是对应每一组输入数据x1、x2、x3,我们指定的目标结果值;二是在sess.run函数的第一个参数也就是我们要求输出的结果数组当中,多加了一个train对象,在结果数组中有train对象,意味着要求程序要执行train对象所包含的训练过程,那么在这个过程中,y、loss等计算结果自然也会被计算出来;所以在结果数组中即使只写一个train,其他的结果也会都被计算出来,只不过我们看不到而已。这里我们还是在结果数组中加上了yTrain和loss,以便对照。另外,只有在结果数组中加上了训练对象,这次sess.run函数的执行才能被称为一次“训练”,否则只是“运行”一次神经网络或者说是用神经网络进行一次“计算”。
可以看到,这段代码中共进行了两次训练,每次训练分别把两位学生的3项分数作为输入数据传入到占位符x1、x2、x3,把总分作为输出的目标值传入到占位符yTrain。整个程序的执行结果如图3.4所示。
图3.4 三好学生成绩问题代码执行结果2
我们可以看到,由于引入了训练过程,可变参数w1、w2、w3已经不再停留在初始值0.1,而是发生了变化,例如第一次训练中分别变成了0.10316052,0.10316006,0.10315938,这说明训练开始起到作用了。
从第一次训练的结果中还可以看到,神经网络在当时可变参数的取值下计算出来的结果值y为24,而我们的目标值yTrain是85,所以结果数组的最后一项误差loss的值为61,正好是85-24的结果。
从第二次训练结果中可以看到,可变参数w1、w2、w3已经变成了0.10554425,0.10563005, 0.1056722,计算结果值y也相应变成了28.884804,说明优化器继续对神经网络的可变参数做了调整;而误差loss变成了67.115196,反而更大了。这是由于我们这两次训练传入的是不同的输入数据,优化器前一次对可变参数的调整是针对第一次输入数据所得的结果值来进行的,所以用在第二次训练时,有可能反而误差会加大;但这没有关系,神经网络的训练具备适应能力,能够在训练过程中逐步调整可变参数,试图去缩小对所有输入数据的计算结果误差。下面就来看看进行多轮训练的结果。
代码3.3 score1c.py
import tensorflow as tf x1 = tf.placeholder(dtype=tf.float32) x2 = tf.placeholder(dtype=tf.float32) x3 = tf.placeholder(dtype=tf.float32) yTrain = tf.placeholder(dtype=tf.float32) w1 = tf.Variable(0.1, dtype=tf.float32) w2 = tf.Variable(0.1, dtype=tf.float32) w3 = tf.Variable(0.1, dtype=tf.float32) n1 = x1 * w1 n2 = x2 * w2 n3 = x3 * w3 y = n1 + n2 + n3 loss = tf.abs(y - yTrain) optimizer = tf.train.RMSPropOptimizer(0.001) train = optimizer.minimize(loss) sess = tf.Session() init = tf.global_variables_initializer() sess.run(init) for i in range(2): result = sess.run([train, x1, x2, x3, w1, w2, w3, y, yTrain, loss], feed_dict={x1: 90, x2: 80, x3: 70, yTrain: 85}) print(result) result = sess.run([train, x1, x2, x3, w1, w2, w3, y, yTrain, loss], feed_dict={x1: 98, x2: 95, x3: 87, yTrain: 96}) print(result)
我们使用循环(如对循环的概念有需要了解的可参看第二章“知识背景准备”一节中的相关内容)可以重复执行训练过程。这一次循环两次,也就是重复进行两轮训练,每一轮进行两次训练,也就是每一轮都分别用两名学生的数据进行训练。执行结果如图3.5所示。
图3.5 三好学生成绩问题代码执行结果3
可以看到,第一轮的两次训练与前面的结果相同,这是正常的。第二轮的两次训练结果与第一轮相比明显有了变化。我们先看其中第一条结果,也就是程序输出中的第三条结果。
[None, array(90.0, dtype=float32), array(80.0, dtype=float32), array(70.0, dtype=float32), 0.10740828, 0.10743184, 0.1074395, 25.346441, array(85.0, dtype=float32), 59.653557]
这是和第一轮第一条训练结果相同的输入数据计算后的结果,这里可变参数w1、w2、w3继续被调整变成了0.10740828,0.10743184,0.1074395,而计算结果y也相应的变成了25.346441,最重要的是误差loss变成了59.653557,比第一轮时的误差61.0已经变小了。
再看第二轮第二次训练结果,也就是程序输出中的第四条结果。
[None, array(98.0, dtype=float32), array(95.0, dtype=float32), array(87.0, dtype=float32), 0.10918032, 0.10926805, 0.10930761, 30.079273, array(96.0, dtype=float32), 65.920731]
可以看到误差loss变成了65.920731,比第一轮第二次的结果67.115196也变小了。
至此可以欣喜地说,我们的训练已经取得了成功,每一轮训练都让计算结果朝着正确的方向前进了一步。神经网络的训练一般均会经历成千上万轮,对于复杂的神经网络,进行上亿次的训练也不稀罕。那么我们来看看这个模型进行多轮训练后的结果,在本段代码中把循环次数增加到5000次,即把for i in range(2):这一条语句中的“2”改成“5000”,再次执行后得到的训练结果如图3.6所示。
图3.6 三好学生成绩问题代码执行结果4
我们直接看最后两条结果数据,这是最后一轮训练后的输出结果。可以看到,两次训练的误差loss都已经被缩小到0.023246765~0.033248901,对于八九十分的总分来说,这个误差值已非常小了。另外,注意w1、w2、w3的数值分别是0.58284378、0.2860972、0.13144642,已经非常接近我们期待的0.6、0.3和0.1了。当然,在以后运用神经网络来解决实际问题的时候,我们是不会预知可变参数的期待值的,在本例中只是想要证明神经网络是可以通过训练获得正确的计算结果的。
到现在为止,我们已经成功地运用神经网络的方式解决了第一个实际问题,虽然问题本身很简单,但是已经能够体现整个神经网络理论和方法的基础了。我们当然可以继续增多前面代码中训练的循环次数来让这个神经网络计算得越来越准确,但这与本章想要讲解的内容已经关系不大了。下一章,我们将讲解如何优化本章中的神经网络模型。