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

时间:2019-01-05 16:55:12

标签: tensorflow keras

我错误地省略了Keras模型第一层中的input_shape。最终,我注意到并修复了该问题–模型的性能急剧下降。

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

enter image description here

在性能方面,我理解的模型(带有input_shape)在用我的测试代码(下图)经过10个周期后达到了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])

1 个答案:

答案 0 :(得分:2)

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文档的相应部分为:

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

现在回到原始问题,无论是否定义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的简化模型架构示例):

Tensorboard visualization

现在,在我们未指定input_shape的情况下,所有模型都只有一层列表。即使在调用compile之后,该模型实际上仍是not compiled(仅设置了诸如优化器之类的属性)。取而代之的是,当第一次将数据传递到模型中时,它是“即时”编译的。这发生在model._standardize_weights中:通过self.call(dummy_input_values, training=training)获得模型输出。检查此方法,我们发现它builds the layers(注意尚未建立模型),然后通过使用Layer.call(不是__call__computes the output incrementally。这样就省去了所有元数据以及操作的分组,因此导致了图的结构不同(尽管其计算操作都相同)。再次检查Tensorboard,我们发现:

Tensorboard visualization

扩展两个图,我们会发现它们包含相同的操作,不同的分组在一起。但这会导致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 std。开发人员:

Performance per epoch

初始权重和初始预测

我验证了两个版本的初始权重和初始预测(拟合之前)是相同的:

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层的模型的性能与带有 掉落层的模型的性能相同,但是没有的模型指定input_shape。因此,似乎没有input_shape的Dropout层是无效的。

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

指定input_shape

Dropout node

未指定input_shape

Dropout node

switch也取决于学习阶段(如前所述):

Dropout node, connection to 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)

对于这两个版本,我都获得类似的输出,但是当未指定__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)

所以我怀疑问题出在__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-basekeras-applications需要keras-preprocessingtensorflow

$ 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,结果仍然相似。我还检查了另一个虚拟环境,其中virtualenv通过pip安装了tensorflow;这里也没有差异。现在,我再也无法重现这种差异。一定是张量流的破损安装。