【问题标题】:What is the structure of a Keras model if input_shape is omitted and why does it perform better?如果省略了 input_shape,Keras 模型的结构是什么?为什么它的性能更好?
【发布时间】:2019-05-31 22:06:55
【问题描述】:

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

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

在性能方面,我理解的模型(使用 input_shape)在 10 个 epoch 后使用我的测试代码(如下)实现了 4.0513 (MSE) 的验证损失,而“怪异”模型管理 1.3218 - 并且只有差异随着时代的增加而增加。

模型定义:

model = keras.Sequential()
model.add(keras.layers.Dense(64, activation=tf.nn.relu, input_shape=(1001,)))
#                                   add or remove this  ^^^^^^^^^^^^^^^^^^^
model.add(keras.layers.Dropout(0.05))
...

(不要在意细节,这只是一个模型,展示了使用和不使用 input_shape 时的性能差异)

那么性能更好的模型发生了什么?什么是multiple?这些层是如何真正连接的?如何在指定 input_shape 的同时构建相同的模型?

完整的脚本:

import tensorflow as tf
from tensorflow import keras
import numpy as np
from collections import deque
import math, random

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)

np.random.seed(1)
tf.set_random_seed(1)
random.seed(1)

model = keras.Sequential()
model.add(keras.layers.Dense(64, activation=tf.nn.relu, input_shape=(1001,)))
#                                   add or remove this  ^^^^^^^^^^^^^^^^^^^
model.add(keras.layers.Dropout(0.05))
model.add(keras.layers.Dense(64, activation=tf.nn.relu))
model.add(keras.layers.Dropout(0.05))
model.add(keras.layers.Dense(64, activation=tf.nn.relu))
model.add(keras.layers.Dropout(0.05))
model.add(keras.layers.Dense(64, activation=tf.nn.relu))
model.add(keras.layers.Dropout(0.05))
model.add(keras.layers.Dense(1))

model.compile(
    loss = 'mse',
    optimizer = tf.train.RMSPropOptimizer(0.0005),
    metrics = ['mae', 'mse'])

inputs, outputs = get_data()

hist = model.fit(inputs, outputs, epochs=10, validation_split=0.1)

print("Final val_loss is", hist.history['val_loss'][-1])

【问题讨论】:

  • 我无法在我的机器上重现该问题(在这两种情况下,模型都实现了 ~6.5 的训练损失和 ~3.5 的验证损失)。此外,我无法重现您提到的有关模型摘要的部分。它们对我来说是相同的。至于input_shape 参数,如果你不指定它,Keras 会在调用fit 时根据你提供给模型的输入数据自动推断。您确定您对模型所做的唯一区别是删除 input_shape 参数吗?
  • @today 对我的机器有什么不同的想法吗?再试一次以防万一,如果我删除 input_shape,1.75 训练和验证损失。如果它没有产生显着的改进,我会把它写成一个错误(我用 pyplot 仔细检查以确保它确实产生了良好的拟合)
  • 进一步,当你想比较两个模型的性能时,尝试设置一个随机种子,至少在数据生成阶段。
  • 我认为这不是 Keras/TF 中的错误。由于随机权重初始化或随机数据生成,这可能只是巧合。您应该遵循此处的可重现指南。无论如何,尝试同时升级 TF 和 Keras,看看是否仍然观察到这种情况(但我怀疑这是否是 Keras/TF 中的错误)。
  • @RomanStarkov 您是如何获得 tensorflow 安装的(pip, conda, ...)?能否提供对应的环境转储(pip freezeconda list)?在执行了一些额外的运行后,我可以重现模型之间的差异,但我无法重现你的确切数字,尽管我在与你相同的 tensorflow 版本上运行(我在 Ubuntu 16.04 上使用 Python 3.6.7)。跨度>

标签: tensorflow keras


【解决方案1】:

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_idtf.set_random_seed 文档的相应部分如下:

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

现在回到最初的问题,如果input_shape 被定义或没有定义,它对图形结构有影响。再次查看一些源代码,我们发现Sequential.add 在指定input_shape 的情况下 增量构建网络的输入和输出;否则它只存储层列表(model._layers);比较两个定义的model.inputs, model.outputs。输出由calling the layers directly 增量构建,它分派到Layer.__call__。这个包装器构建层,设置层的输入和输出,并向输出添加一些元数据;它还使用ops.name_scope 对操作进行分组。我们可以从Tensorboard 提供的可视化中看到这一点(以Input -&gt; Dense -&gt; Dropout -&gt; 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 所述的步骤,以确保随机化方面的可重复性,
  • 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'])

使用这段代码,我实际上希望这两种方法都能获得相似的结果,但事实证明它们并不相等:

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

编辑

我还安装了keraskeras-basetensorflow 需要keras-applicationskeras-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 安装出错了。

【讨论】:

  • 感谢您的详细解释,它确实解释了正在发生的事情以及原因。我猜plot_model 可能由于缺少元数据/操作分组而被注销为“误导”。我觉得它不仅仅是不同的初始权重,因为性能差异发生在种子 1、2 和 3 上,并且在具有固定学习率的 500 个 epoch 之后,它的损失为 ~1.0 vs ~0.2,似乎不管的种子。我很想获得改进的性能,同时还指定我的input_shape
  • +1!感谢您为编写此答案所做的努力。我想知道你是否也在你的实验中考虑了并行性,正如here 所解释的那样?
  • @RomanStarkov 我根据我链接到的指南关闭了并行性,并达到了 4.4 的 val_loss(不使用 input_shape)与 4.3(使用 input_shape)。所以这一次它并没有表现得更好。但是,我不知道为什么即使我们设置种子并使用一个线程在 CPU 上进行训练,结果也会略有不同,但这个答案中提供的解释可能是原因之一。但是,至少在我的实验中,情况并非如此,一个模型(即不使用input_shape)始终优于另一个模型(即使用input_shape)。
  • @RomanStarkov 我可以重现您的结果,并且一些进一步的调查表明,当未指定 input_shape 时,Dropout 层存在问题。实际上,看起来没有input_shape,Dropout 层只是处于非活动状态,因此得到了改进的结果(这是有道理的,因为它们正则化)。但是,现在我无法弄清楚到底有什么区别,也许您有想法?有关详细信息,请参阅我的更新答案。
  • @today 感谢您的评论。我确实实现了常见问题解答链接中描述的所有步骤,并且我还为初始化操作添加了随机种子,并删除了一些其他随机性以真正对齐。这样做之后,似乎差异来自 Dropout 层,当未指定 input_shape 时,这些层似乎无法正常工作。去掉 Dropout 层后,两者的结果是完全一样的。有关详细信息,请参阅我的更新答案。你能在你的机器上重现结果吗?我会尝试使用不同版本的 tensorflow,看看效果如何。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-08
  • 1970-01-01
  • 1970-01-01
  • 2018-09-29
  • 1970-01-01
相关资源
最近更新 更多