TensorFlow变量作用域(Variable Scope)详解:复用与封装
本文深入解析TensorFlow中Variable Scope的核心机制,阐述如何通过tf.variable_scope和tf.get_variable实现模型参数的共享与复用。重点讲解reuse标志的控制、作用域嵌套规则、初始化器继承以及Name Scope与Variable Scope的区别,帮助开发者构建封装良好且高效的神经网络模型。
变量共享的挑战与Variable Scope机制
示例说明
在TensorFlow框架中,变量通常代表模型的可训练参数。随着模型架构的复杂化,变量共享的逻辑会变得极其繁琐。
官方文档提供了一个典型案例:在构建包含两层卷积层的过滤器时,若每次输入图片都重新创建过滤器变量,将导致资源浪费。实际上,我们希望所有输入图片共享同一组过滤器变量,具体包括四个核心变量:conv1_weights、conv1_biases、conv2_weights 以及 conv2_biases。
传统的处理方式是将这些变量定义为全局变量。然而,这种做法破坏了代码的封装性,要求这些变量必须在文档中明确说明,以便其他代码文件引用。一旦内部实现发生变动,所有调用方代码可能都需要随之调整,维护成本较高。
另一种保持封装性的方案是将整个模型封装为类对象,通过类方法管理变量。
为此,TensorFlow提供了Variable Scope(变量作用域)这一独特机制来高效共享变量。该机制主要依赖两个关键函数来实现。
tf.get_variable(<name>, <shape>, <initializer>) 创建或返回给定名称的变量
tf.variable_scope(<scope_name>) 管理传给get_variable()的变量名称的作用域
在以下代码片段中,通过调用tf.get_variable()分别创建了名为weights和biases的两个变量。
def conv_relu(input, kernel_shape, bias_shape):
# Create variable named "weights".
weights = tf.get_variable("weights", kernel_shape,
initializer=tf.random_normal_initializer())
# Create variable named "biases".
biases = tf.get_variable("biases", bias_shape,
initializer=tf.constant_initializer(0.0))
conv = tf.nn.conv2d(input, weights,
strides=[1, 1, 1, 1], padding='SAME')
return tf.nn.relu(conv + biases)
当模型包含多个卷积层时,可以通过tf.variable_scope()指定不同的作用域以区分变量,例如使用with tf.variable_scope("conv1")将第一个卷积层的作用域限定为conv1。
在此作用域约束下,内部创建的weights和biases变量将自动带上该作用域前缀。
def my_image_filter(input_images):
with tf.variable_scope("conv1"):
# Variables created here will be named "conv1/weights", "conv1/biases".
relu1 = conv_relu(input_images, [5, 5, 32, 32], [32])
with tf.variable_scope("conv2"):
# Variables created here will be named "conv2/weights", "conv2/biases".
return conv_relu(relu1, [5, 5, 32, 32], [32])
最后,在处理image_filters作用域时,若需重复使用第一张图片输入时创建的变量,可调用reuse_variables()函数,具体代码如下:
with tf.variable_scope("image_filters") as scope:
result1 = my_image_filter(image1)
scope.reuse_variables()
result2 = my_image_filter(image2)
tf.get_variable()工作机制详解
tf.get_variable()的工作机制
tf.get_variable()的行为逻辑如下:
当tf.get_variable_scope().reuse的值为False时,调用该函数会创建一个新的变量。
with tf.variable_scope("foo"):
v = tf.get_variable("v", [1])
assert v.name == "foo/v:0"
当tf.get_variable_scope().reuse的值为True时,调用该函数会尝试重用已存在的同名变量。
with tf.variable_scope("foo"):
v = tf.get_variable("v", [1])
with tf.variable_scope("foo", reuse=True):
v1 = tf.get_variable("v", [1])
assert v1 is v
所有变量均通过作用域路径与变量名进行唯一标识,后续内容将展示作用域支持类似文件路径的嵌套结构。
tf.variable_scope()深度解析
tf.variable_scope()的理解
tf.variable_scope()用于指定变量的作用域,作为变量名的前缀,且支持多层嵌套,示例如下:
with tf.variable_scope("foo"):
with tf.variable_scope("bar"):
v = tf.get_variable("v", [1])
assert v.name == "foo/bar/v:0"
当前环境的作用域可通过函数tf.get_variable_scope()获取,同时可以通过调用reuse_variables()将reuse标志设置为True,这一操作非常关键,示例如下:
with tf.variable_scope("foo"):
v = tf.get_variable("v", [1])
tf.get_variable_scope().reuse_variables()
v1 = tf.get_variable("v", [1])
assert v1 is v
作用域中的reuse标志默认值为False,调用reuse_variables()可将其设置为True。一旦设置为True,该状态不可逆地回退为False,且该作用域下所有子空间的reuse标志均继承为True。若需停止重用变量,可退出当前作用域回到上层作用域,例如:
with tf.variable_scope("root"):
# At start, the scope is not reusing.
assert tf.get_variable_scope().reuse == False
with tf.variable_scope("foo"):
# Opened a sub-scope, still not reusing.
assert tf.get_variable_scope().reuse == False
with tf.variable_scope("foo", reuse=True):
# Explicitly opened a reusing scope.
assert tf.get_variable_scope().reuse == True
with tf.variable_scope("bar"):
# Now sub-scope inherits the reuse flag.
assert tf.get_variable_scope().reuse == True
# Exited the reusing scope, back to a non-reusing one.
assert tf.get_variable_scope().reuse == False
一个作用域对象可以作为另一个新作用域的参数传入,示例如下:
with tf.variable_scope("foo") as foo_scope:
v = tf.get_variable("v", [1])
with tf.variable_scope(foo_scope):
w = tf.get_variable("w", [1])
with tf.variable_scope(foo_scope, reuse=True):
v1 = tf.get_variable("v", [1])
w1 = tf.get_variable("w", [1])
assert v1 is v
assert w1 is w
无论作用域如何嵌套,当使用with tf.variable_scope()打开一个已存在的作用域时,程序会直接跳转至该作用域。
with tf.variable_scope("foo") as foo_scope:
assert foo_scope.name == "foo"
with tf.variable_scope("bar"):
with tf.variable_scope("baz") as other_scope:
assert other_scope.name == "bar/baz"
with tf.variable_scope(foo_scope) as foo_scope2:
assert foo_scope2.name == "foo" # Not changed.
Variable Scope中设置的Initializers(初始化器)可以传递给子空间及tf.get_variable()函数,除非中间有函数显式覆盖,否则该初始化器保持不变。
with tf.variable_scope("foo", initializer=tf.constant_initializer(0.4)):
v = tf.get_variable("v", [1])
assert v.eval() == 0.4 # Default initializer as set above.
w = tf.get_variable("w", [1], initializer=tf.constant_initializer(0.3)):
assert w.eval() == 0.3 # Specific initializer overrides the default.
with tf.variable_scope("bar"):
v = tf.get_variable("v", [1])
assert v.eval() == 0.4 # Inherited default initializer.
with tf.variable_scope("baz", initializer=tf.constant_initializer(0.2)):
v = tf.get_variable("v", [1])
assert v.eval() == 0.2 # Changed default initializer.
算子(ops)会受到变量作用域(variable scope)的影响,相当于隐式地打开了同名的名称作用域(name scope)。例如,加法算子的名称可能显示为foo/add。
with tf.variable_scope("foo"):
x = 1.0 + tf.get_variable("v", [1])
assert x.op.name == "foo/add"
除了变量作用域外,还可以显式打开名称作用域(name scope)。名称作用域仅影响算子的命名,不影响变量的命名。此外,若tf.variable_scope()传入字符串参数,则在创建变量作用域的同时会隐式创建同名的名称作用域。如下例所示,变量v的作用域为foo,而算子x的命名变为foo/bar,这是因为隐式创建了名称作用域foo。
with tf.variable_scope("foo"):
with tf.name_scope("bar"):
v = tf.get_variable("v", [1])
x = 1.0 + v
assert v.name == "foo/v:0"
assert x.op.name == "foo/bar/add"
注意:如果tf.variable_scope()传入的不是字符串而是scope对象,则不会隐式创建同名的名称作用域。
以上内容涵盖了TensorFlow变量作用域的核心要点,希望对读者理解模型构建有所帮助。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









