- 结论:在搭建GAN判别器时,慎用keras。
- 原因:tf.variable_scope() 的 reuse=True 对keras变量不起作用,导致判别real_img的判别器与判别fake_img的判别器是两个独立的判别器,使对抗训练失效。
- 导致现象:real_img 的判别结果快速收敛到1, fake_img的判别结果快速收敛到0, 且不再变化, 快速收敛是指在100~1000次迭代内完全收敛,不再变化。当判别器损失函数中用到log函数时, 损失函数会出现 nan 现象。
- 其他: 无论是用keras.layers 还是tensorflow.keras.layers都存在这个问题。具体在keras中如何使用变量重用,我还没有探究,希望有看到的可以指教。
-------------------------------------------------------------------------------------------------------------------------------------------------
个人经历:
我之前在搭建神经网络时,用的卷积层是定义在tf.layers中的:
from tensorflow.compat.v1.layers import Conv2D
最近,发现keras 很好用,尤其对LSTM的封装很好用,所以开始转用keras:
from keras.layers import Conv2D
一直用的挺顺利的,直到这两天在训练GAN时出现问题:判别器对fake_img的判别分值很快就趋近于0,对real_img的判别分值很快就趋近于1。
我百思不得其解,调了学习率,多次检查了GAN的损失函数,都没有问题,折腾了一天时间。直到刚刚突然想到变量重用的问题,检查了变量空间,发现以下问题:用keras的Conv2D构建的判别器,在计算real_map与fake_map时,重复创建了判别器的计算节点,并没有执行变量重用,示例代码如下:
import numpy as np
import tensorflow.compat.v1 as tf
from keras.layers import Conv2D
from tensorflow.keras.layers import Conv2D
inputs = tf.convert_to_tensor(np.random.random((1,256,256,3)).astype(np.float32))
with tf.variable_scope("dis"):
conv1 = Conv2D(32,(3,3),strides=(1,1),padding="same",activation='relu',name="conv1")(inputs)
with tf.variable_scope("dis",reuse=True):
conv2 = Conv2D(32, (3, 3), strides=(1, 1), padding="same", activation='relu', name="conv1")(inputs)
var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES)
for v in var_list:
print(v.name)
"""
返回:
dis/conv1/kernel:0
dis/conv1/bias:0
dis_1/conv1/kernel:0
dis_1/conv1/bias:0
"""
由代码可见,我在命名空间中设置了 reuse=True, 但是代码仍旧重复创建了相同的计算节点。当在训练GAN时遇到这个问题时,就会导致 计算real_map的判别器与计算fake_map的判别器是两个独立的判别器,他们两个完全不相关,所以就达不到对抗训练的目的了。
很明显,这个问题是不应该出现的,tensorflow设置了reuse参数,应该是要起作用的,于是我又测试了tf.layer.Conv2D的情况,发现用tf.layer.Conv2D 不存在这个问题,示例代码如下(仅仅改变了Conv2D的来源):
import numpy as np
import tensorflow.compat.v1 as tf
# from keras.layers import Conv2D
from tensorflow.compat.v1.layers import Conv2D
inputs = tf.convert_to_tensor(np.random.random((1,256,256,3)).astype(np.float32))
with tf.variable_scope("dis"):
conv1 = Conv2D(32,(3,3),strides=(1,1),padding="same",activation='relu',name="conv1")(inputs)
with tf.variable_scope("dis",reuse=True):
conv2 = Conv2D(32, (3, 3), strides=(1, 1), padding="same", activation='relu', name="conv1")(inputs)
var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES)
for v in var_list:
print(v.name)
"""
返回:
dis/conv1/kernel:0
dis/conv1/bias:0
"""
可以发现,当改用 tf.compat.v1.layers 的源时不存在这个问题。
另外, 在reuse = True的命名空间中,只能重复使用已经在该空间中定义的变量,不能创建新的变量,否则会报错。
如果,既希望能够变量重用,又不耽误创建新的变量,请用 reuse=tf.AUTO_REUSE,示例代码如下(与上例相比增加了一个新变量):
import numpy as np
import tensorflow.compat.v1 as tf
# from keras.layers import Conv2D
from tensorflow.compat.v1.layers import Conv2D
inputs = tf.convert_to_tensor(np.random.random((1,256,256,3)).astype(np.float32))
with tf.variable_scope("dis"):
conv1 = Conv2D(32,(3,3),strides=(1,1),padding="same",activation='relu',name="conv1")(inputs)
with tf.variable_scope("dis",reuse=True):
conv2 = Conv2D(32, (3, 3), strides=(1, 1), padding="same", activation='relu', name="conv1")(inputs)
conv3 = Conv2D(32, (3, 3), strides=(1, 1), padding="same", activation='relu', name="conv2")(inputs)
var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES)
for v in var_list:
print(v.name)
"""
报错:
ValueError: Variable dis/conv2/kernel does not exist, or was not created with tf.get_variable(). Did you mean to set reuse=tf.AUTO_REUSE in VarScope?
"""
当把 reuse=True 改为 reuse=tf.AUTO_REUSE后,错误消失:
import numpy as np
import tensorflow.compat.v1 as tf
# from keras.layers import Conv2D
from tensorflow.compat.v1.layers import Conv2D
inputs = tf.convert_to_tensor(np.random.random((1,256,256,3)).astype(np.float32))
with tf.variable_scope("dis"):
conv1 = Conv2D(32,(3,3),strides=(1,1),padding="same",activation='relu',name="conv1")(inputs)
with tf.variable_scope("dis",reuse=tf.AUTO_REUSE):
conv2 = Conv2D(32, (3, 3), strides=(1, 1), padding="same", activation='relu', name="conv1")(inputs)
conv3 = Conv2D(32, (3, 3), strides=(1, 1), padding="same", activation='relu', name="conv2")(inputs)
var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES)
for v in var_list:
print(v.name)
"""
返回:
dis/conv1/kernel:0
dis/conv1/bias:0
dis/conv2/kernel:0
dis/conv2/bias:0
"""
可见,在已有的变量命名空间中新定义的变量成功了。