3.10 变量作用域机制
3.10.1 基本原理
目前为止,我们已经对TensorFlow架构和TensorFlow客户端的实现有了基本的认识。但是,在深度学习过程中,一方面,我们需要减少训练参数的个数(比如CNN和LSTM模型)或是面对多机多卡并行化训练大数据大模型(比如数据并行化)等情况;另一方面,当我们的深度学习模型变得异常复杂的时候,往往存在大量的变量和调用方法,如何有效地维护这些变量名称和方法名称的唯一性(即不重复),同时又能维护好一个条理清晰的图(Graph)就变得非常重要了。这时,变量共享机制就变得非常重要。比如,我们构建CNN、LSTM等模型时,需要使用很多变量集去验证权重值(Weight)和偏差(Bias)等训练参数,非常希望在输入不同的数据时这些参数是可以共享的(本书5.3.4小节“参数共享机制”部分会进行详细解读)。过去,我们创建一个全局变量就可以使用了,但在深度学习中则不可以,不方便管理而且使代码的封装性受到极大影响。所以,TensorFlow提供了一种变量管理方法:变量作用域机制,以此解决上面出现的问题。
关于变量作用域机制,有的文档也叫共享变量机制,根据笔者查阅的文献资料,大部分文章的参考资料来自于TensorFlow官方说明(详见https://tensorflow.google.cn/guide/variables)。TensorFlow中是通过调用四个函数来进行变量作用域共享的,这四个函数是tf.Variable(<variable_name>)、tf.get_variable(<variable_name>)、tf.name_scope(<scope_name>)和tf.variable_scope(<scope_name>)。下面,我们具体来看一下这几个函数。
如果使用Variable,那么每次都会新建变量,但是大多数时候我们是希望一些变量可以重用的,所以就用到了get_variable()。get_variable()会去搜索变量名称,搜索结果如果没有就新建变量,如果有就直接使用该变量。既然用到变量名称,这就会涉及名称域的概念。通过不同的域来对变量名称加以区分,因为如果让我们给所有变量都直接取不同名字其实是非常辛苦的且没必要,这就是为什么会用到作用域(Scope)的概念了。name_scope主要用于图(Graph)中的各种运算,variable_scope可以通过设置reuse标志以及初始化方式来影响作用域中的变量。当然对我们而言,还有一个更直观的感受就是:在使用tensorboard可视化的时候用名字作用域进行封装后会更清晰。
3.10.2 通过示例解读
假设我们需要一个执行某种计算的函数,给定w,需要计算x * w + y ** 2。让我们编写一个TensorFlow客户端:
这里,为了得到想要的结果,我们可以调用session.run(very_simple_computation(2))函数(当然是在调用tf.global_variables_initializer().run()函数之后)。实际上,每次调用这个函数时都会创建两个TensorFlow变量。在多次调用该方法(在面向对象程序设计中把封装的函数也称为方法)时,图(Graph)中x和y变量不会被替换,相反,将会保留这些旧变量,并在图(Graph)中创建新的变量,直到内存不足为止。不管如何,最终的结果是正确的。为了更好地验证这个情况,我们在for循环中运行session.run(very_simple_computation(2))方法,并将变量名称也打印出来,循环10次的输出结果如下(完整的代码请在ch3文件夹中的3_tensorflow_introduction.ipynb文件“变量作用域机制(Variable Scoping)”部分查看):
14.0 ['x:0', 'y:0', 'x_1:0', 'y_1:0', 'x_2:0', 'y_2:0', 'x_3:0', 'y_3:0', 'x_4:0', 'y_4:0', 'x_5:0', 'y_5:0', 'x_6:0', 'y_6:0', 'x_7:0', 'y_7:0', 'x_8:0', 'y_8:0', 'x_9:0', 'y_9:0', 'x_10:0', 'y_10:0']
每次运行该函数时都会创建一对变量。如果运行这个函数100次,那么计算图中将有198个过时变量(99x变量和99y变量)。
作用域允许我们重复使用变量,而不是每次调用函数时都创建一个新的变量。我们现在为上面的例子添加变量可复用性的操作,将代码更改为以下内容:
在这个例子中,如果执行session.run([z1,z2,a1,a2,zz1,zz2])操作,就应该会看到z1,z2,a1,a2,zz1,zz2的值按顺序为9.0,90.0,9.0, 90.0,9.0,90.0值。现在,如果打印变量,应该只看到四个不同的变量:scopeA / x、scopeA / y、scopeB / x和scopeB / y。我们现在可以在循环中多次运行它,而不必担心创建冗余变量和内存不足。
现在你可能想知道,为什么我们不能在代码的开头创建这四个变量并在后面的方法中使用它们。如果这样做,就会破坏代码的封装性,因为这样是在明显地依赖于代码之外的内容。
最后,作用域支持了可复用性,同时也保留了代码的封装性。此外,作用域使代码流更直观,减少了错误的可能性,因为我们通过作用域和名称显式地获取变量,而不是使用TensorFlow变量分配的Python变量。