tensorflow keras savedmodel 丢失输入名称并添加未知输入

tensorflow keras savedmodel lost inputs name and add unknow inputs

我目前正在使用 tensorflow 2.3 实现顺序深度匹配模型 (https://arxiv.org/abs/1909.00385)。我通过子类化 keras.layers.Layer.

将预处理层作为模型的一部分

预处理部分代码如下

class Preprocessing(keras.layers.Layer):

    def __init__(self, str_columns, hash_bins, float_columns, float_buckets, embedding_dim, user_columns, short_seq_columns, prefer_seq_columns, item_key_feats,
                 item_key_hash_bucket_size, series_feats, series_feats_hash_bucket_size, deviceid_num, device_list, **kwargs):
        super(Preprocessing, self).__init__(**kwargs)
        self.str_columns = str_columns
        self.hash_bins = hash_bins
        self.float_columns = float_columns
        self.float_buckets = float_buckets
        self.embedding_dim = embedding_dim
        self.user_columns = user_columns
        self.short_seq_columns = short_seq_columns
        self.prefer_seq_columns = prefer_seq_columns
        self.item_key_feats = item_key_feats
        self.item_key_hash_bucket_size = item_key_hash_bucket_size
        self.series_feats = series_feats
        self.series_feats_hash_bucket_size = series_feats_hash_bucket_size
        self.deviceid_num = deviceid_num
        self.device_list = device_list

        self.user_outputs = {}
        self.short_outputs = {}
        self.prefer_outputs = {}

        deviceid_lookup = keras.layers.experimental.preprocessing.StringLookup(vocabulary=device_list, mask_token=None, oov_token="-1")
        deviceid_embedding = keras.layers.Embedding(input_dim=deviceid_num, output_dim=embedding_dim)

        item_key_hashing = keras.layers.experimental.preprocessing.Hashing(num_bins=item_key_hash_bucket_size)
        item_key_embedding = keras.layers.Embedding(input_dim=item_key_hash_bucket_size, output_dim=embedding_dim)

        series_hashing = keras.layers.experimental.preprocessing.Hashing(num_bins=series_feats_hash_bucket_size)
        series_embedding = keras.layers.Embedding(input_dim=series_feats_hash_bucket_size, output_dim=embedding_dim)

        for i in str_columns:
            if i == "device_id":
                process = [deviceid_lookup, deviceid_embedding]
            elif i in item_key_feats:
                process = [item_key_hashing, item_key_embedding]
            elif i in series_feats:
                process = [series_hashing, series_embedding]
            else:
                hashing = keras.layers.experimental.preprocessing.Hashing(num_bins=hash_bins[i])
                embedding = keras.layers.Embedding(input_dim=hash_bins[i], output_dim=embedding_dim)
                process = [hashing, embedding]
            if i in user_columns:
                self.user_outputs[i] = process
            if i in short_seq_columns:
                self.short_outputs[i] = process
            if i in prefer_seq_columns:
                self.prefer_outputs[i] = process

        for l in float_columns:
            discrete = keras.layers.experimental.preprocessing.Discretization(bins=float_buckets[l])
            embedding = keras.layers.Embedding(input_dim=len(float_buckets[l]) + 1, output_dim=embedding_dim)
            if l in user_columns:
                self.user_outputs[l] = [discrete, embedding]
            if l in short_seq_columns:
                self.short_outputs[l] = [discrete, embedding]
            if l in prefer_seq_columns:
                self.prefer_outputs[l] = [discrete, embedding]

    @staticmethod
    def get_embedding(input_tmp, name, embed_dict):
        func = embed_dict[name]
        if len(func) < 2:
            print(func)
            raise Exception('Not enough function to retrieve embedding')
        output = func[0](input_tmp)
        output = func[1](output)
        return output

    def call(self, inputs):
        user_embedding = tf.concat([tf.reduce_mean(self.get_embedding(inputs[i], i, self.user_outputs), axis=[1, 2]) for i in self.user_columns], axis=-1)
        short_embedding = tf.concat([tf.squeeze(self.get_embedding(inputs[l], l, self.short_outputs), axis=1).to_tensor() for l in self.short_seq_columns], axis=-1)
        prefer_embedding = {k: tf.squeeze(self.get_embedding(inputs[k], k, self.prefer_outputs).to_tensor(), axis=1) for k in self.prefer_seq_columns}
        return user_embedding, short_embedding, prefer_embedding

还有我的输入码:

def read_row(csv_row):
    record_defaults = [[0.]] * numeric_feature_size + [['']] * category_feature_size + [['0-0']] + [['0']]
    row = tf.io.decode_csv(csv_row, record_defaults=record_defaults, field_delim='', use_quote_delim=False)
    features = []
    for i, feature in enumerate(row):
        if i < numeric_feature_size:
            features.append(feature)
        elif i < numeric_feature_size + category_feature_size:
            tmp_tf = tf.strings.split([feature], ";")
            features.append(tmp_tf)
    res = OrderedDict(zip(numeric_columns + category_columns, features))
    res['target'] = [tf.cast(row[-2], tf.string)]
    return res

代码的另一部分没有在这里给出,因为我认为它是正确的,并且可能太多而不能在这里列出。 该模型在训练期间使用 model.compile 然后 model.fit 正常工作,但是,在我用 model.save(path) 保存它之后,生成的图形得到许多未知输入和输入名称的 none已保存。

saved_model_cli show --dir ./ --tag_set serve --signature_def serving_default

The given SavedModel SignatureDef contains the following input(s):
  inputs['args_0'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0:0
  inputs['args_0_1'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_1:0
  inputs['args_0_10'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_10:0
  inputs['args_0_11'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_11:0
  inputs['args_0_12'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_12:0
  inputs['args_0_13'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_13:0
  inputs['args_0_14'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_14:0
  inputs['args_0_15'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_15:0
  inputs['args_0_16'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_16:0
  inputs['args_0_17'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_17:0
  inputs['args_0_18'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_18:0
  inputs['args_0_19'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_19:0
  inputs['args_0_2'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_2:0
  inputs['args_0_20'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_20:0
  inputs['args_0_21'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_21:0
  inputs['args_0_22'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_22:0
  inputs['args_0_23'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_23:0
  inputs['args_0_24'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_24:0
  inputs['args_0_25'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_25:0
  inputs['args_0_26'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_26:0
  inputs['args_0_27'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_27:0
  inputs['args_0_28'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_28:0
  inputs['args_0_29'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_29:0
  inputs['args_0_3'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_3:0
  inputs['args_0_30'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_30:0
  inputs['args_0_31'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_31:0
  inputs['args_0_32'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_32:0
  inputs['args_0_33'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_33:0
  inputs['args_0_34'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_34:0
  inputs['args_0_35'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_35:0
  inputs['args_0_36'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_36:0
  inputs['args_0_37'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_37:0
  inputs['args_0_38'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_38:0
  inputs['args_0_39'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_39:0
  inputs['args_0_4'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_4:0
  inputs['args_0_40'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_40:0
  inputs['args_0_41'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_41:0
  inputs['args_0_42'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_42:0
  inputs['args_0_43'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_43:0
  inputs['args_0_44'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_44:0
  inputs['args_0_45'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_45:0
  inputs['args_0_46'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_46:0
  inputs['args_0_47'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_47:0
  inputs['args_0_48'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_48:0
  inputs['args_0_49'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_49:0
  inputs['args_0_5'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_5:0
  inputs['args_0_50'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_50:0
  inputs['args_0_6'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_6:0
  inputs['args_0_7'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_7:0
  inputs['args_0_8'] tensor_info:
      dtype: DT_INT64
      shape: (-1)
      name: serving_default_args_0_8:0
  inputs['args_0_9'] tensor_info:
      dtype: DT_STRING
      shape: (-1)
      name: serving_default_args_0_9:0
The given SavedModel SignatureDef contains the following output(s):
  outputs['output_1'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 64)
      name: StatefulPartitionedCall:0

在这个模型中,我只使用了dtype为tf.string的分类特征,所以所有dtype为DT_INT64的输入都不是我模型输入的一部分。

谁能帮我解决这个问题?

这很简单,我用多种方法进行了测试,发现名称是发生的最重要的事情。

自定义 class 和带有 lstm 单元的 RNN 不工作,我花时间测试也需要很多时间,因为跟踪和 RNN。

[样本]:

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
: Model Initialize
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
model = tf.keras.models.Sequential([
    tf.keras.layers.InputLayer(input_shape=( 32, 32, 3 ), batch_size=1, name='layer_input'),
    tf.keras.layers.Normalization(mean=3., variance=2., name='layer_normalize_1'),
    tf.keras.layers.Normalization(mean=4., variance=6., name='layer_normalize_2'),
    tf.keras.layers.Conv2DTranspose(2, 3, activation='relu', padding="same", name='layer_conv2dT_1'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(1, 1), padding='valid', name='layer_maxpool_1'),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4 * 256, name='layer_dense_1'),   
    tf.keras.layers.Reshape((4 * 256, 1)),

    tf.keras.layers.LSTM(128, name='layer_4', return_sequences=True, return_state=False),
    tf.keras.layers.LSTM(128, name='layer_5'),

    tf.keras.layers.Dropout(0.2),
])

model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(64, activation='relu', name='layer_dense_2'))
model.add(tf.keras.layers.Dense(7, name='layer_dense_3'))
model.summary()

# Loads the weights
if exists(target_saved_model) :
    model = load_model(target_saved_model)
    print("model load: " + target_saved_model)
    input("Press Any Key!")

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
: Training
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
history = model.fit(x_train, y_train, epochs=10, batch_size=1 ,validation_data=(x_test, y_test))
model.save(target_saved_model)
print('.................')

[输出]: 1. lstm层的错误信息

Epoch 10/10
20/20 [==============================] - 1s 71ms/step - loss: 0.7332 - val_loss: 0.8078
2022-04-06 23:23:26.017439: W tensorflow/python/util/util.cc:368] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
WARNING:absl:Found untraced functions such as lstm_cell_layer_call_fn, lstm_cell_layer_call_and_return_conditional_losses, lstm_cell_1_layer_call_fn, lstm_cell_1_layer_call_and_return_conditional_losses while saving (showing 4 of 4). These functions will not be directly callable after loading.
WARNING:absl:<keras.layers.recurrent.LSTMCell object at 0x00000145F1C83130> has the same name 'LSTMCell' as a built-in Keras object. Consider renaming <class 'keras.layers.recurrent.LSTMCell'> to avoid naming conflicts when loading with `tf.keras.models.load_model`. If renaming is not possible, pass the object in the `custom_objects` parameter of the load function.
WARNING:absl:<keras.layers.recurrent.LSTMCell object at 0x00000145F1C83C10> has the same name 'LSTMCell' as a built-in Keras object. Consider renaming <class 'keras.layers.recurrent.LSTMCell'> to avoid naming conflicts when loading with `tf.keras.models.load_model`. If renaming is not possible, pass the object in the `custom_objects` parameter of the load function.
...

[输出 2]: 2. 删除错误信息

Epoch 10/10
100/100 [==============================] - 6s 63ms/step - loss: 0.3954 - val_loss: 0.5108
.................
...

我终于得到了这份工作。

错误来自我的 tf.io.decode_csv 方法。 tf.strings.split()默认会return一个RaggedTensor,里面也会包含两个int变量。这就是输入签名包含 17 个字符串类型和 34 个 int 类型的原因。 RaggedTensor 也会损害 keras 序列化,这就是为什么所有输入名称都丢失的原因。 我将所有 RaggedTensor 转换为 EagerTensor,一切正常。

然而,这不是我在尝试加载模型时遇到的唯一错误。

我也遇到过 The same saveable will be restored with two names 错误,我花了很多时间来解决它。结果是keras.layers.experimental.preprocessing模块的bug,里面的同一个函数不能重复使用,导致变量会被记录为同名,结果是non-loadablesavedmodel .