TL;DR
结果不同的原因是两个模型的初始权重不同。一个表现(显着)优于另一个的事实纯属偶然,正如@today 所提到的,他们获得的结果大致相似。
详情
正如tf.set_random_seed 的文档所解释的,随机操作使用两个种子,图级种子和操作特定种子; tf.set_random_seed 设置图级种子:
依赖随机种子的操作实际上是从两个种子中派生出来的:图形级种子和操作级种子。这将设置图形级种子。
看一下Dense 的定义,我们看到the default kernel initializer 是'glorot_uniform'(这里我们只考虑内核初始化程序,但偏置初始化程序也是如此)。进一步浏览源代码,我们最终会发现它使用默认参数获取GlorotUniform。具体来说,该特定操作(即权重初始化)的random number generator seed 设置为None。现在,如果我们检查这个种子的使用位置,我们会发现它被传递给了random_ops.truncated_normal。这反过来(与所有随机操作一样)现在获取 两个 种子,一个是图形级种子,另一个是特定于操作的种子:seed1, seed2 = random_seed.get_seed(seed)。我们可以检查get_seed 函数的定义,我们发现如果没有给出操作特定的种子(这是我们的例子),那么它是从当前图的属性派生的:op_seed = ops.get_default_graph()._last_id。 tf.set_random_seed 文档的相应部分如下:
- 如果设置了图级种子,但未设置操作种子:系统确定性地选择操作种子和图级种子,以便获得唯一的随机序列。
现在回到最初的问题,如果input_shape 被定义或没有定义,它对图形结构有影响。再次查看一些源代码,我们发现Sequential.add 在指定input_shape 的情况下仅 增量构建网络的输入和输出;否则它只存储层列表(model._layers);比较两个定义的model.inputs, model.outputs。输出由calling the layers directly 增量构建,它分派到Layer.__call__。这个包装器构建层,设置层的输入和输出,并向输出添加一些元数据;它还使用ops.name_scope 对操作进行分组。我们可以从Tensorboard 提供的可视化中看到这一点(以Input -> Dense -> Dropout -> Dense 的简化模型架构为例):
现在,在我们没有指定input_shape 的情况下,所有模型都有一个层列表。即使在调用compile 之后,模型实际上也是not compiled(只是设置了优化器等属性)。相反,它是在第一次将数据传递到模型时“即时”编译的。这发生在model._standardize_weights 中:模型输出是通过self.call(dummy_input_values, training=training) 获得的。检查这个方法,我们发现它是builds the layers(注意模型尚未构建),然后是computes the output incrementally,使用Layer.call(不是__call__)。这忽略了所有元数据以及操作分组,因此导致图的结构不同(尽管其计算操作都是相同的)。再次检查我们发现的 Tensorboard:
展开这两个图,我们会发现它们包含相同的操作,但以不同的方式组合在一起。但是,这会导致 keras.backend.get_session().graph._last_id 对于两个定义不同,因此会导致随机操作的种子不同:
# With `input_shape`:
>>> keras.backend.get_session().graph._last_id
303
# Without `input_shape`:
>>> keras.backend.get_session().graph._last_id
7
性能结果
为了进行类似的随机操作,我使用了 OP 的代码并进行了一些修改:
- 添加了here 所述的步骤,以确保随机化方面的可重复性,
- 为
Dense 和Dropout 变量初始化设置随机种子,
- 已删除
validation_split,因为在没有 input_shape 的情况下“动态”编译模型之前会发生拆分,因此可能会干扰种子,
- 设置
shuffle = False,因为这可能会使用单独的特定于操作的种子。
这是完整的代码(另外我在运行脚本之前执行了export PYTHONHASHSEED=0):
from collections import deque
from functools import partial
import math
import random
import sys
import numpy as np
import tensorflow as tf
from tensorflow import keras
seed = int(sys.argv[1])
np.random.seed(1)
tf.set_random_seed(seed)
random.seed(1)
session_conf = tf.ConfigProto(intra_op_parallelism_threads=1,
inter_op_parallelism_threads=1)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
keras.backend.set_session(sess)
def func(x):
return math.sin(x)*5 + math.sin(x*1.8)*4 + math.sin(x/4)*5
def get_data():
x = 0
dx = 0.1
q = deque()
r = 0
data = np.zeros((100000, 1002), np.float32)
while True:
x = x + dx
sig = func(x)
q.append(sig)
if len(q) < 1000:
continue
arr = np.array(q, np.float32)
for k in range(10):
xx = random.uniform(0.1, 9.9)
data[r, :1000] = arr[:1000]
data[r, 1000] = 5*xx #scale for easier fitting
data[r, 1001] = func(x + xx)
r = r + 1
if r >= data.shape[0]:
break
if r >= data.shape[0]:
break
q.popleft()
inputs = data[:, :1001]
outputs = data[:, 1001]
return (inputs, outputs)
Dense = partial(keras.layers.Dense, kernel_initializer=keras.initializers.glorot_uniform(seed=1))
Dropout = partial(keras.layers.Dropout, seed=1)
model = keras.Sequential()
model.add(Dense(64, activation=tf.nn.relu,
# input_shape=(1001,)
))
model.add(Dropout(0.05))
model.add(Dense(64, activation=tf.nn.relu))
model.add(Dropout(0.05))
model.add(Dense(64, activation=tf.nn.relu))
model.add(Dropout(0.05))
model.add(Dense(64, activation=tf.nn.relu))
model.add(Dropout(0.05))
model.add(Dense(1))
model.compile(
loss = 'mse',
optimizer = tf.train.RMSPropOptimizer(0.0005)
)
inputs, outputs = get_data()
shuffled = np.arange(len(inputs))
np.random.shuffle(shuffled)
inputs = inputs[shuffled]
outputs = outputs[shuffled]
hist = model.fit(inputs, outputs[:, None], epochs=10, shuffle=False)
np.save('without.{:d}.loss.npy'.format(seed), hist.history['loss'])
使用这段代码,我实际上希望这两种方法都能获得相似的结果,但事实证明它们并不相等:
for i in $(seq 1 10)
do
python run.py $i
done
绘制平均损失 +/- 1 标准。开发者:
初始权重和初始预测
我验证了两个版本的初始权重和初始预测(拟合前)相同:
inputs, outputs = get_data()
mode = 'without'
pred = model.predict(inputs)
np.save(f'{mode}.prediction.npy', pred)
for i, layer in enumerate(model.layers):
if isinstance(layer, keras.layers.Dense):
w, b = layer.get_weights()
np.save(f'{mode}.{i:d}.kernel.npy', w)
np.save(f'{mode}.{i:d}.bias.npy', b)
和
for i in 0 2 4 8
do
for data in bias kernel
do
diff -q "with.$i.$data.npy" "without.$i.$data.npy"
done
done
辍学的影响
[ ! ] 我在删除所有Dropout 层后检查了性能,在这种情况下,性能实际上是相等的。所以关键似乎在于 Dropout 层。实际上,没有 Dropout 层的模型的性能与 具有 Dropout 层但没有指定input_shape 的模型相同。所以似乎没有input_shape,Dropout 层是无效的。
基本上这两个版本之间的区别在于一个使用__call__ 而另一个使用call 来计算输出(如上所述)。由于性能类似于没有 Dropout 层,因此可能的解释是,当未指定 input_shape 时,Dropout 层不会下降。这可能是由training=False 引起的,即层无法识别它们处于训练模式。但是,我看不出为什么会发生这种情况。我们也可以再次考虑 Tensorboard 图。
指定input_shape:
未指定input_shape:
switch 也取决于学习阶段(和以前一样):
为了验证training kwarg,让我们继承Dropout:
class Dropout(keras.layers.Dropout):
def __init__(self, rate, noise_shape=None, seed=None, **kwargs):
super().__init__(rate, noise_shape=noise_shape, seed=1, **kwargs)
def __call__(self, inputs, *args, **kwargs):
training = kwargs.get('training')
if training is None:
training = keras.backend.learning_phase()
print('[__call__] training: {}'.format(training))
return super().__call__(inputs, *args, **kwargs)
def call(self, inputs, training=None):
if training is None:
training = keras.backend.learning_phase()
print('[call] training: {}'.format(training))
return super().call(inputs, training)
我为这两个版本获得了类似的输出,但是在未指定 input_shape 时缺少对 __call__ 的调用:
[__call__] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[call] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[__call__] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[call] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[__call__] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[call] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[__call__] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
[call] training: Tensor("keras_learning_phase:0", shape=(), dtype=bool)
所以我怀疑问题出在__call__ 的某个地方,但现在我无法弄清楚它是什么。
系统
我通过conda 使用 Ubuntu 16.04、Python 3.6.7 和 Tensorflow 1.12.0(不支持 GPU):
$ uname -a
Linux MyPC 4.4.0-141-generic #167-Ubuntu SMP Wed Dec 5 10:40:15 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ python --version
Python 3.6.7 :: Anaconda, Inc.
$ conda list | grep tensorflow
tensorflow 1.12.0 mkl_py36h69b6ba0_0
tensorflow-base 1.12.0 mkl_py36h3c3e929_0
编辑
我还安装了keras 和keras-base(tensorflow 需要keras-applications 和keras-preprocessing):
$ conda list | grep keras
keras 2.2.4 0
keras-applications 1.0.6 py36_0
keras-base 2.2.4 py36_0
keras-preprocessing 1.0.5 py36_0
删除所有keras* 和tensorflow*,然后重新安装tensorflow,差异消失。即使重新安装keras 后,结果仍然相似。我还检查了通过pip 安装tensorflow 的不同virtualenv;这里也没有差异。现在我不能再重现这种差异了。肯定是 tensorflow 安装出错了。