3.2 搭建解决三好学生成绩问题的神经网络
我们先按照一般的理解,设计出如图3.1所示的神经网络模型。
下面介绍在神经网络模型图中的一般约定,本书后面的所有神经网络模型图都是按这个约定来画的。
• 神经网络模型图一般均包含1个输入层、1个或多个隐藏层,以及1个输出层。
• 一般来说,输入层是描述输入数据的形态的;我们用方块来代表每一条输入数据的一个数(或者叫一个字段),叫作输入节点;输入节点一般用x来命名,如果有多个数值,则用x1,x2,…,xn来代表。
• 隐藏层是描述我们设计的神经网络模型结构中最重要的部分;隐藏层可能有多个;每一层中都会有1个或多个神经元,我们用圆圈来表示,叫作神经元节点或隐藏节点,有时也直接简称为节点;每一个节点都接收上一层传来的数据并进行一定的运算后向下一层输出数据,符合神经元的特性,神经元节点上的这些运算称为“计算操作”或“操作”(operation,简称op)。
图3.1 三好学生成绩问题神经网络模型图
• 输出层一般是神经网络模型的最后一层,会包含1个或多个以菱形表示的输出节点;输出节点代表着整个神经网络计算的最后结果;输出层的节点一般习惯上用y来命名,但并非必须。
• 我们在神经网络模型图中,一般约定在各个节点的右下方(有时候因为拥挤也会在左下方)标记节点的名称,在节点的左上方标记该节点所做的计算,例如,图3.1中x1、x2、x3、n1、n2、n3、y都是节点名称,“*w1”、“*w2”、“*w3”、“∑”这些都代表节点运算。
现在我们回到模型本身,这是一个标准的前馈神经网络,即信号总是往前传递的神经网络。输入层有3个节点x1、x2、x3,分别代表前面所说的德育分、智育分和体育分。因为问题比较简单,隐藏层我们只设计了一层,其中有3个节点n1、n2、n3,分别对3个输入的分数进行处理,处理的方式就是分别乘以3个权重w1、w2、w3。输出层只有一个节点y,因为我们只要求一个总分数值;节点y的操作就是把n1、n2、n3这3个节点输出过来的数值进行相加求和。
可以看到,这个神经网络模型是非常简单的,下面我们就用代码来实现它。
代码3.1 score1a.py
import tensorflow as tf x1 = tf.placeholder(dtype=tf.float32) x2 = tf.placeholder(dtype=tf.float32) x3 = 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 sess = tf.Session() init = tf.global_variables_initializer() sess.run(init) result = sess.run([x1, x2, x3, w1, w2, w3, y], feed_dict={x1: 90, x2: 80, x3: 70}) print(result)
下面逐行解释这段代码。
import tensorflow as tf
一开始,我们需要导入tensorflow这个包。为了以后调用tensorflow包中的对象、成员变量和成员函数时更方便,我们在importtensorflow后面加上了as tf,表示以后可以用tf这个简写来代表tensorflow的全名。
x1 = tf.placeholder(dtype=tf.float32) x2 = tf.placeholder(dtype=tf.float32) x3 = tf.placeholder(dtype=tf.float32)
这3行分别定义了3个输入节点x1、x2、x3,我们计划在训练这个神经网络的时候,把德育、智育和体育3个分数分别作为x1、x2、x3的数值输入。这种等待模型运行时才会输入的节点,在TensorFlow中要把它定义成为placeholder(占位符);所谓“占位符”,就是在编写程序的时候还不确定输入什么数,而是在程序运行的时候才会输入,编程时仅仅把这个节点定义好,先“占个位子”。
定义占位符的方法是调用TensorFlow的placeholder函数,由于我们前面在导入tensorflow包时约定了可以把它简称为tf,所以我们用tf.placeholder的写法来调用这个函数。上面的tf.placeholder函数调用时带了一个参数(在函数后面紧跟括号中的是函数的参数),写法是“dtype=tf.float32”,这是一种高级的参数写法,叫作命名参数。一般计算机语言的参数只支持按照顺序来写,不需要给每个参数命名,但是这样就不允许顺序发生错误。而Python语言支持命名参数,可以通过指定参数的名称来赋值,避免只用顺序来区别参数造成错乱的情况,也可以适应有时候只需要某几个参数的情况。dtype是“data type”的缩写,表示占位符所代表的数值的类型,tf.float32是TensorFlow中的32位浮点数类型(所谓32位浮点数,指的是用32位二进制数字来代表一个小数;计算机中现在常用的浮点数有32位浮点数和64位浮点数,64位浮点数比32位浮点数能表达的数值范围更大,一般用32位浮点数已经足够满足计算的需要)。所以像x1=tf.placeholder(dtype=tf.float32)这样一条语句的含义就是“定义一个占位符变量x1,它的数据类型是32位浮点数”。
接下来就到了定义几个权重w1、w2、w3的时候了。在神经网络中,类似权重这种会在训练过程中经常性地变化的神经元参数,TensorFlow中把它们叫作变量,这与我们一般大多数计算机语言中变量的含义还是有一些区别的。为避免混淆,本书中把这些TensorFlow中的变量叫作神经网络的可变参数(注意也不要把可变参数和函数的参数混淆)。那么,本问题中就有3个可变参数w1、w2、w3,我们用tf.Variable函数来定义它们。
w1 = tf.Variable(0.1, dtype=tf.float32) w2 = tf.Variable(0.1, dtype=tf.float32) w3 = tf.Variable(0.1, dtype=tf.float32)
定义w1、w2、w3的形式除了函数用的是tf.Variable函数外,其他与定义占位符x1、x2、x3的时候类似,还有一点不同是除了用dtype参数来指定数值类型,还传入了另一个初始值参数,这个参数没有用命名参数的形式,这是因为tf.Variable函数规定第一个参数是用于指定可变参数的初始值。可以看到,我们把w1、w2、w3的初始值都设置为0.1。
再下来就是定义隐藏层中的3个节点n1、n2、n3。这一段代码应该很好理解,就是让nn分别等于xn乘以wn的计算结果。
n1 = x1 * w1 n2 = x2 * w2 n3 = x3 * w3
输出层的定义也很好理解,就是把n1、n2、n3这3个隐藏层节点的输出相加。
y = n1 + n2 + n3
至此为止,我们对这个神经网络模型的定义实际上已经完成了。下面我们看看如何在这个神经网络中输入数据并得到运算结果。
sess = tf.Session()
这条语句定义了一个sess变量,它包含一个TensorFlow的会话(session)对象,我们现在不必深究会话是什么,可以简单地把会话理解成管理神经网络运行的一个对象,有了会话对象,我们的神经网络就可以正式运转了。所以每次定义完神经网络模型后,在准备运行前都要定义一个会话对象,才能开始训练这个模型或者用训练好的模型去进行预测计算。
会话对象管理神经网络的第一步,一般是要把所有的可变参数初始化,也就是给所有可变参数一个各自的初始值,这是用下面的语句来实现的。
init = tf.global_variables_initializer() sess.run(init)
首先让变量init等于tf.global_variables_initializer这个函数的返回值,它返回的是一个专门用于初始化可变参数的对象。然后调用会话对象sess的成员函数run(),带上init变量作为参数,就可以实现对我们之前定义的神经网络模型中所有可变参数的初始化。run是英语中“运行”的意思,sess.run(init)就是在sess会话中运行初始化这个函数。具体给每个可变参数赋什么样的初值,是由我们刚才在定义w1、w2、w3时的第一个函数参数来决定的。
w1 = tf.Variable(0.1, dtype=tf.float32) w2 = tf.Variable(0.1, dtype=tf.float32) w3 = tf.Variable(0.1, dtype=tf.float32)
这里,我们把3个可变参数初始值都设置为0.1。
上面两条初始化可变参数的语句,也可以合起来写成:
sess.run(tf.global_variables_initializer())
这种写法没有先定义一个变量来存储初始化可变参数的对象,而是直接把tf.global_variables_initializer()函数的返回值作为sess.run()的参数,这样的写法也是可以的。
然后,我们用下面代码来执行一次神经网络的计算。
result = sess.run([x1, x2, x3, w1, w2, w3, y], feed_dict={x1:90, x2:80, x3:70})
这条语句中,我们再一次调用了sess对象的run函数,不过这回不是进行初始化,而是真正进行一次计算,也就是说,要输入一组数据并获得神经网络的计算结果。sess.run函数的第一个参数为一个数组,代表我们需要查看哪些结果项;为了查看结果更清楚,除了最终输出层的结果y,还把输入层的x1、x2、x3和隐藏层的可变参数w1、w2、w3都获取出来,以便对比参照。sess.run函数的另一个参数是个命名参数feed_dict,代表我们要输入的数据,feed在英语中有“喂”的意思,所以有时候也称为给神经网络“喂”数据。feed_dict中第二个单词dict是dictionary的简写,代表着feed_dict参数要求输入的是前面介绍过的“字典”类型的数值,所以要按字典类型数值的写法,用大括号括起来,里面分别按占位符的名称一个一个指明数值。{x1:90, x2:80, x3:70}就代表分别为x1、x2、x3占位符送入90、80、70这3个数值。我们把sess.run函数的执行结果存放到了result变量中,这是一个包含了x1、x2、x3、w1、w2、w3和y的具体数值在内的数组。
最后,用print(result)把result变量的值在命令行上输出来。
我们试着执行图3.2中这段代码,可以看到相应的结果。
图3.2 三好学生成绩问题代码执行结果1
其中,第一段是TensorFlow提醒我们没有充分发挥CPU的能力,可以忽略这条信息。第二段是真正得到的print函数输出的result变量的值。我们可以看到,整个结果是以中括号“[ ]”括起来的,说明这是一个数组类型的变量,数组中用逗号分隔开了各个数值,前三个数值分别是我们输入的3个分数90、80、70,对应x1、x2、x3变量,TensorFlow把它们认为是仅保存一个数字的数组(array),并且数值类型是float32类型(即32位浮点数),这没有关系,不影响我们的运算。之后是3个0.1的数字,分别对应w1、w2、w3可变参数,因为给它们定义的初始值都是0.1。最后是根据这些数值计算出来的输出层结果y,计算结果是24。我们可以验算一下,90*0.1+80*0.1+70*0.1=9+8+7=24,说明我们搭建的神经网络计算的结果是正确的。