如果省略 input_shape,Keras 模型的结构是什么?为什么它的性能更好?

Rom*_*kov 3 keras tensorflow

input_shape错误地省略了 Keras 模型第一层中的 。最终我注意到了这一点并修复了它 \xe2\x80\x93 并且我的模型的性能急剧下降。

\n\n

观察有 和没有 的模型结构input_shape,我发现性能更好的模型的输出形状为multiple。此外,用 绘制它plot_model显示各层之间没有连接:

\n\n

在此输入图像描述

\n\n

在性能方面,我理解的模型(使用 input_shape)在我的测试代码(如下)的 10 个 epoch 后实现了 4.0513 (MSE) 的验证损失,而“奇怪”模型管理 1.3218 \xe2\x80\x93 和差异只会随着更多纪元而增加。

\n\n

型号定义:

\n\n
model = 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\n

完整脚本:

\n\n
import 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

a_g*_*est 5

长话短说

结果不同的原因是两个模型的初始权重不同。一个人的表现(明显)比另一个人好这一事实纯粹是偶然的,正如 @today 提到的,他们获得的结果大致相似。

细节

正如文档所tf.set_random_seed解释的,随机操作使用两个种子,即图级种子操作特定种子tf.set_random_seed设置图级种子:

依赖随机种子的操作实际上源自两个种子:图级种子和操作级种子。这设置了图级种子。

看一下定义,Dense我们发现默认的内核初始值设定项'glorot_uniform'(这里我们只考虑内核初始值设定项,但偏置初始值设定项也是如此)。进一步浏览源代码,我们最终会发现它GlorotUniform使用默认参数来获取。具体来说,该特定操作(即权重初始化)的随机数生成器种子设置为。现在,如果我们检查该种子的使用位置,我们会发现它被传递到例如。这反过来(与所有随机操作一样)现在获取两个种子,一个是图级种子,另一个是特定于操作的种子:。我们可以检查函数的定义,我们发现如果未给出操作特定种子(这是我们的情况),那么它是从当前图的属性派生的:。文档的相应部分如下:Nonerandom_ops.truncated_normalseed1, seed2 = random_seed.get_seed(seed)get_seedop_seed = ops.get_default_graph()._last_idtf.set_random_seed

  1. 如果设置了图级种子,但未设置操作种子:系统确定性地选择与图级种子结合的操作种子,以便获得唯一的随机序列。

现在回到原来的问题,如果input_shape定义与否,图结构会产生影响。再次查看一些源代码,我们发现只有在指定的情况下Sequential.add才会增量构建网络的输入和输出;否则它只存储层列表 ( ); 比较两个定义。输出是通过直接调用分派到 的来增量构建的。该包装器构建层,设置层的输入和输出,并向输出添加一些元数据;它还使用 an来对操作进行分组。我们可以从Tensorboard提供的可视化中看到这一点(简化模型架构的示例):input_shapemodel._layersmodel.inputs, model.outputsLayer.__call__ops.name_scopeInput -> 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 的代码并进行了一些修改,以便进行类似的随机操作:

  • 添加了此处描述的步骤以确保随机化方面的可重复性,
  • 设置随机种子DenseDropout变量初始化,
  • 已删除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_shapeDropout 层是无效的。

基本上,两个版本之间的区别在于一个版本使用__call__而另一个版本call用于计算输出(如上所述)。由于性能与没有 Dropout 层的性能相似,因此可能的解释是,input_shape未指定时 Dropout 层不会丢弃。这可能是由 引起的training=False,即各层无法识别它们处于训练模式。但是我不明白为什么会发生这种情况。我们还可以再次考虑 Tensorboard 图。

指定input_shape

Dropout节点

未指定input_shape

Dropout节点

其中switch还取决于学习阶段(如前所述):

Dropout节点,连接到交换机

为了验证trainingkwarg 让我们子类化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)

编辑

我还安装keraskeras-basekeras-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;这里也没有差异。现在我无法再重现这种差异。这肯定是张量流的安装损坏了。