我input_shape
错误地省略了 Keras 模型第一层中的 。最终我注意到了这一点并修复了它 \xe2\x80\x93 并且我的模型的性能急剧下降。
观察有 和没有 的模型结构input_shape
,我发现性能更好的模型的输出形状为multiple
。此外,用 绘制它plot_model
显示各层之间没有连接:
在性能方面,我理解的模型(使用 input_shape)在我的测试代码(如下)的 10 个 epoch 后实现了 4.0513 (MSE) 的验证损失,而“奇怪”模型管理 1.3218 \xe2\x80\x93 和差异只会随着更多纪元而增加。
\n\n型号定义:
\n\nmodel = keras.Sequential()\nmodel.add(keras.layers.Dense(64, activation=tf.nn.relu, input_shape=(1001,)))\n# add or remove this ^^^^^^^^^^^^^^^^^^^\nmodel.add(keras.layers.Dropout(0.05))\n...\n
Run Code Online (Sandbox Code Playgroud)\n\n(不用介意细节,这只是一个模型,展示了有和没有 input_shape 的性能差异)
\n\n那么性能更好的模型中发生了什么?什么是multiple
?各层之间是如何真正连接的?我如何在指定的同时构建相同的模型input_shape
?
完整脚本:
\n\nimport tensorflow as tf\nfrom tensorflow import keras\nimport numpy as np\nfrom collections import deque\nimport math, random\n\ndef func(x):\n return math.sin(x)*5 + math.sin(x*1.8)*4 + math.sin(x/4)*5\n\ndef get_data():\n x = 0\n dx = 0.1\n q = deque()\n r = 0\n data = np.zeros((100000, 1002), np.float32)\n while True:\n x = x + dx\n sig = func(x)\n q.append(sig)\n if len(q) < 1000:\n continue\n\n arr = np.array(q, np.float32)\n\n for k in range(10):\n xx = random.uniform(0.1, 9.9)\n data[r, :1000] = arr[:1000]\n data[r, 1000] = 5*xx #scale for easier fitting\n data[r, 1001] = func(x + xx)\n r = r + 1\n if r >= data.shape[0]:\n break\n\n if r >= data.shape[0]:\n break\n\n q.popleft()\n\n inputs = data[:, :1001]\n outputs = data[:, 1001]\n return (inputs, outputs)\n\nnp.random.seed(1)\ntf.set_random_seed(1)\nrandom.seed(1)\n\nmodel = keras.Sequential()\nmodel.add(keras.layers.Dense(64, activation=tf.nn.relu, input_shape=(1001,)))\n# add or remove this ^^^^^^^^^^^^^^^^^^^\nmodel.add(keras.layers.Dropout(0.05))\nmodel.add(keras.layers.Dense(64, activation=tf.nn.relu))\nmodel.add(keras.layers.Dropout(0.05))\nmodel.add(keras.layers.Dense(64, activation=tf.nn.relu))\nmodel.add(keras.layers.Dropout(0.05))\nmodel.add(keras.layers.Dense(64, activation=tf.nn.relu))\nmodel.add(keras.layers.Dropout(0.05))\nmodel.add(keras.layers.Dense(1))\n\nmodel.compile(\n loss = \'mse\',\n optimizer = tf.train.RMSPropOptimizer(0.0005),\n metrics = [\'mae\', \'mse\'])\n\ninputs, outputs = get_data()\n\nhist = model.fit(inputs, outputs, epochs=10, validation_split=0.1)\n\nprint("Final val_loss is", hist.history[\'val_loss\'][-1])\n
Run Code Online (Sandbox Code Playgroud)\n
结果不同的原因是两个模型的初始权重不同。一个人的表现(明显)比另一个人好这一事实纯粹是偶然的,正如 @today 提到的,他们获得的结果大致相似。
正如文档所tf.set_random_seed
解释的,随机操作使用两个种子,即图级种子和操作特定种子;tf.set_random_seed
设置图级种子:
依赖随机种子的操作实际上源自两个种子:图级种子和操作级种子。这设置了图级种子。
看一下定义,Dense
我们发现默认的内核初始值设定项是'glorot_uniform'
(这里我们只考虑内核初始值设定项,但偏置初始值设定项也是如此)。进一步浏览源代码,我们最终会发现它GlorotUniform
使用默认参数来获取。具体来说,该特定操作(即权重初始化)的随机数生成器种子设置为。现在,如果我们检查该种子的使用位置,我们会发现它被传递到例如。这反过来(与所有随机操作一样)现在获取两个种子,一个是图级种子,另一个是特定于操作的种子:。我们可以检查函数的定义,我们发现如果未给出操作特定种子(这是我们的情况),那么它是从当前图的属性派生的:。文档的相应部分如下: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
才会增量构建网络的输入和输出;否则它只存储层列表 ( ); 比较两个定义。输出是通过直接调用分派到 的层来增量构建的。该包装器构建层,设置层的输入和输出,并向输出添加一些元数据;它还使用 an来对操作进行分组。我们可以从Tensorboard提供的可视化中看到这一点(简化模型架构的示例):input_shape
model._layers
model.inputs, model.outputs
Layer.__call__
ops.name_scope
Input -> Dense -> Dropout -> Dense
现在,在我们没有指定input_shape
模型的情况下,所有的都是层列表。即使在调用之后,compile
模型实际上也没有被编译(只是设置了优化器等属性)。相反,当第一次将数据传递到模型时,它是“动态”编译的。这发生在model._standardize_weights
:模型输出是通过获得的self.call(dummy_input_values, training=training)
。检查此方法,我们发现它构建了层(请注意,模型尚未构建),然后使用(not )增量计算输出。这省略了所有元数据以及操作分组,因此导致图的结构不同(尽管其计算操作都是相同的)。再次检查 Tensorboard 我们发现:Layer.call
__call__
展开这两个图,我们会发现它们包含相同的操作,但以不同的方式分组在一起。然而,这会导致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
Run Code Online (Sandbox Code Playgroud)
我使用了 OP 的代码并进行了一些修改,以便进行类似的随机操作:
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'])
Run Code Online (Sandbox Code Playgroud)
通过这段代码,我实际上希望这两种方法都能获得类似的结果,但事实证明它们并不相等:
for i in $(seq 1 10)
do
python run.py $i
done
Run Code Online (Sandbox Code Playgroud)
绘制平均损失 +/- 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)
Run Code Online (Sandbox Code Playgroud)
和
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
Run Code Online (Sandbox Code Playgroud)
[!]我检查了删除所有层后的性能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)
Run Code Online (Sandbox Code Playgroud)
我获得了两个版本的类似输出,但是当未指定时,调用__call__
会丢失:input_shape
[__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)
Run Code Online (Sandbox Code Playgroud)
所以我怀疑问题出在内部某个地方__call__
,但现在我无法弄清楚它是什么。
我正在使用 Ubuntu 16.04、Python 3.6.7 和 Tensorflow 1.12.0 conda
(无 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
Run Code Online (Sandbox Code Playgroud)
我还安装keras
了keras-base
(keras-applications
并且keras-preprocessing
是必需的tensorflow
):
$ 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
Run Code Online (Sandbox Code Playgroud)
删除所有 和keras*
,tensorflow*
然后重新安装后tensorflow
,差异消失了。即使重新安装后,keras
结果仍然相似。我还检查了另一个 virtualenv,其中tensorflow是通过安装的pip
;这里也没有差异。现在我无法再重现这种差异。这肯定是张量流的安装损坏了。