如何将Keras的.h5模型导出为TensorFlow的.pb格式?

74

我用一个新数据集微调了Inception模型,并将其保存为Keras的".h5"模型。现在我的目标是在只接受“.pb”扩展名的Android Tensorflow上运行我的模型。问题是,Keras或Tensorflow中是否有任何库可以进行此转换?到目前为止,我看到了这篇文章:https://blog.keras.io/keras-as-a-simplified-interface-to-tensorflow-tutorial.html,但仍无法弄清楚。

14个回答

101
Keras本身不包含将TensorFlow图导出为协议缓冲区文件的任何方式,但您可以使用常规的TensorFlow实用程序来完成。这里是一篇博客文章,解释了如何使用TensorFlow中包含的实用程序脚本freeze_graph.py来完成这个过程,这是“典型”的方式。
然而,我个人认为必须制作一个检查点,然后运行外部脚本来获取模型是很麻烦的,我更喜欢从自己的Python代码中执行这个过程,因此我使用像这样的函数:
def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):
    """
    Freezes the state of a session into a pruned computation graph.

    Creates a new computation graph where variable nodes are replaced by
    constants taking their current value in the session. The new graph will be
    pruned so subgraphs that are not necessary to compute the requested
    outputs are removed.
    @param session The TensorFlow session to be frozen.
    @param keep_var_names A list of variable names that should not be frozen,
                          or None to freeze all the variables in the graph.
    @param output_names Names of the relevant graph outputs.
    @param clear_devices Remove the device directives from the graph for better portability.
    @return The frozen graph definition.
    """
    graph = session.graph
    with graph.as_default():
        freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
        output_names = output_names or []
        output_names += [v.op.name for v in tf.global_variables()]
        input_graph_def = graph.as_graph_def()
        if clear_devices:
            for node in input_graph_def.node:
                node.device = ""
        frozen_graph = tf.graph_util.convert_variables_to_constants(
            session, input_graph_def, output_names, freeze_var_names)
        return frozen_graph

这是受freeze_graph.py实现启发的。参数与脚本相似。 session是TensorFlow会话对象。如果要保留一些未冻结的变量(例如用于有状态模型),则需要keep_var_names,通常不需要。 output_names是一个包含所需输出操作名称的列表。 clear_devices只是移除任何设备指令以使图形更具可移植性。因此,对于具有一个输出的典型Keras model,您可以执行以下操作:

from keras import backend as K

# Create, compile and train model...

frozen_graph = freeze_session(K.get_session(),
                              output_names=[out.op.name for out in model.outputs])

接着,您可以像往常一样使用tf.train.write_graph将图写入文件:

tf.train.write_graph(frozen_graph, "some_directory", "my_model.pb", as_text=False)

1
似乎对我有用。但是,输出的.pb文件没有相同的输入节点名称。这样对吗?输入节点名称为input_1,但在冻结后,我不知道新名称是什么。你能告诉我新名称是什么吗? - Maged Saeed
2
我在model.output.op.name中遇到了一个错误。你能告诉我如何解决吗? - Pawandeep Singh
1
在类似的帖子中,我看到过在此之前使用"keras.backend.set_learning_phase(0)"。这里也需要吗? - gebbissimo
1
@JimChen 没有更多关于模型的信息很难确定,这取决于你所说的“简单”是什么意思,也许你只有几个密集层但它们非常大...此外,这还取决于你认为什么样的文件大小算是大。 - jdehesa
1
@jdehesa 我在执行代码 frozen_graph = freeze_session(K.get_session(),output_names=[out.op.name for out in model.outputs]) 时遇到了错误,错误信息为 Keras 符号输入/输出没有实现 op。您可能正在尝试将 Keras 符号输入/输出传递给未注册调度的 TF API,从而阻止 Keras 自动将 API 调用转换为功能模型中的 lambda 层。 我该如何解决这个问题?有什么建议吗? - aminvincent
显示剩余18条评论

33

freeze_session 方法工作得很好。但是与将模型保存到检查点文件,然后使用 TensorFlow 附带的 freeze_graph 工具相比,我觉得更简单一些,因为更易于维护。你只需要完成以下两个步骤:

首先,在 Keras 代码 model.fit(...) 后添加并训练您的模型:

from keras import backend as K
import tensorflow as tf
print(model.output.op.name)
saver = tf.train.Saver()
saver.save(K.get_session(), '/tmp/keras_model.ckpt')

然后切换到你的 TensorFlow 根目录,运行:

python tensorflow/python/tools/freeze_graph.py \
--input_meta_graph=/tmp/keras_model.ckpt.meta \
--input_checkpoint=/tmp/keras_model.ckpt \
--output_graph=/tmp/keras_frozen.pb \
--output_node_names="<output_node_name_printed_in_step_1>" \
--input_binary=true

3
在保存检查点之前,我必须设置K.set_learning_phase(0)。否则在Android上运行时会遇到错误“Keras error “You must feed a value for placeholder tensor 'keras_learning_phase' with dtype bool””。我将其设置为0,因为我仅需要用于推理的模型。 - Tahlil
5
在加载模型之前,必须先调用K.set_learning_phase。 - Tahlil

31

Tensorflow 2更新

将所有内容保存到单个档案中,使用Tensorflow SavedModel格式(包含saved_model.pb文件):

最初的回答:

model = ...  # Get model (Sequential, Functional Model, or Model subclass)
model.save('path/to/location')

最初的回答:或者在较老的Keras H5格式中:
model = ...  # Get model (Sequential, Functional Model, or Model subclass)
model.save('model.h5')

推荐的格式是SavedModel
加载模型:
最初的回答:建议使用SavedModel格式。加载模型的方法如下:
from tensorflow import keras
model = keras.models.load_model('path/to/location')
model = keras.models.load_model('model.h5')
SavedModel 包含了一个完整的 TensorFlow 程序,包括经过训练的参数(即 tf.Variables)和计算。它不需要原始的模型构建代码运行,这使得它对于使用 TFLiteTensorFlow.jsTensorFlow ServingTensorFlow Hub 进行分享或部署非常有用。

Tensorflow 2 示例

以下是一个简单示例(XOR 示例),演示如何导出 Keras 模型(以 h5pb 格式)以及在 Python 和 C++ 中使用模型:


train.py:

import numpy as np
import tensorflow as tf

print(tf.__version__)  # 2.4.1

x_train = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], 'float32')
y_train = np.array([[0], [1], [1], [0]], 'float32')

inputs = tf.keras.Input(shape=(2,), name='input')
x = tf.keras.layers.Dense(64, activation='relu')(inputs)
x = tf.keras.layers.Dense(64, activation='relu')(x)
x = tf.keras.layers.Dense(64, activation='relu')(x)
x = tf.keras.layers.Dense(64, activation="relu")(x)
outputs = tf.keras.layers.Dense(1, activation='sigmoid', name='output')(x)

model = tf.keras.Model(inputs=inputs, outputs=outputs, name='xor')

model.summary()

model.compile(loss='mean_squared_error', optimizer='adam', metrics=['binary_accuracy'])

model.fit(x_train, y_train, epochs=100)

model.save('./xor/')  # SavedModel format

model.save('./xor.h5')  # Keras H5 format

在运行上述脚本之后:

最初的回答:

.
├── train.py
├── xor
│   ├── assets
│   ├── saved_model.pb
│   └── variables
│       ├── variables.data-00000-of-00001
│       └── variables.index
└── xor.h5

predict.py:

import numpy as np
import tensorflow as tf

print(tf.__version__)  # 2.4.1

model = tf.keras.models.load_model('./xor/')  # SavedModel format
# model = tf.keras.models.load_model('./xor.h5')  # Keras H5 format

# 0 xor 0 = [[0.11921611]] ~= 0
print('0 xor 0 = ', model.predict(np.array([[0, 0]])))

# 0 xor 1 = [[0.96736085]] ~= 1
print('0 xor 1 = ', model.predict(np.array([[0, 1]])))

# 1 xor 0 = [[0.97254556]] ~= 1
print('1 xor 0 = ', model.predict(np.array([[1, 0]])))

# 1 xor 1 = [[0.0206149]] ~= 0
print('1 xor 1 = ', model.predict(np.array([[1, 1]])))

将模型转换为ONNX:
ONNX”是交换深度学习模型的新标准。它承诺使深度学习模型可移植,从而防止供应商锁定。
ONNX”是一个开放式格式,用于表示机器学习模型。“ONNX”定义了一组常见的运算符-机器学习和深度学习模型的构建块-以及一种通用的文件格式,使AI开发人员能够使用各种框架、工具、运行时和编译器来使用模型。
$ pip install onnxruntime
$ pip install tf2onnx
$ python -m tf2onnx.convert --saved-model ./xor/ --opset 9 --output xor.onnx

# INFO - Successfully converted TensorFlow model ./xor/ to ONNX
# INFO - Model inputs: ['input:0']
# INFO - Model outputs: ['output']
# INFO - ONNX model is saved at xor.onnx

通过指定--opset,用户可以覆盖默认设置,生成所需操作集的图表。例如,--opset 13将创建一个仅使用opset 13中可用操作的onnx图表。由于旧的操作集在大多数情况下具有更少的操作,因此一些模型可能无法在旧的操作集上转换。

opencv-predict.py:

import numpy as np
import cv2

print(cv2.__version__)  # 4.5.1

model = cv2.dnn.readNetFromONNX('./xor.onnx')

# 0 xor 0 = [[0.11921611]] ~= 0
model.setInput(np.array([[0, 0]]), name='input:0')
print('0 xor 0 = ', model.forward(outputName='output'))

# 0 xor 1 = [[0.96736085]] ~= 1
model.setInput(np.array([[0, 1]]), name='input:0')
print('0 xor 1 = ', model.forward(outputName='output'))

# 1 xor 0 = [[0.97254556]] ~= 1
model.setInput(np.array([[1, 0]]), name='input:0')
print('1 xor 0 = ', model.forward(outputName='output'))

# 1 xor 1 = [[0.02061491]] ~= 0
model.setInput(np.array([[1, 1]]), name='input:0')
print('1 xor 1 = ', model.forward(outputName='output'))

predict.cpp:

#include <cstdlib>
#include <iostream>
#include <opencv2/opencv.hpp>

int main(int argc, char **argv)
{
    std::cout << CV_VERSION << std::endl; // 4.2.0

    cv::dnn::Net net;

    net = cv::dnn::readNetFromONNX("./xor.onnx");

    // 0 xor 0 = [0.11921611] ~= 0
    float x0[] = { 0, 0 };
    net.setInput(cv::Mat(1, 2, CV_32F, x0), "input:0");
    std::cout << "0 xor 0 = " << net.forward("output") << std::endl;

    // 0 xor 1 = [0.96736085] ~= 1
    float x1[] = { 0, 1 };
    net.setInput(cv::Mat(1, 2, CV_32F, x1), "input:0");
    std::cout << "0 xor 1 = " << net.forward("output") << std::endl;

    // 1 xor 0 = [0.97254556] ~= 1
    float x2[] = { 1, 0 };
    net.setInput(cv::Mat(1, 2, CV_32F, x2), "input:0");
    std::cout << "1 xor 0 = " << net.forward("output") << std::endl;

    // 1 xor 1 = [0.020614909] ~= 0
    float x3[] = { 1, 1 };
    net.setInput(cv::Mat(1, 2, CV_32F, x3), "input:0");
    std::cout << "1 xor 1 = " << net.forward("output") << std::endl;

    return EXIT_SUCCESS;
}

编译和运行:

$ sudo apt install build-essential pkg-config libopencv-dev
$ g++ predict.cpp `pkg-config --cflags --libs opencv4` -o predict
$ ./predict

最初的回答

以下是一个简单的示例(XOR示例),展示了如何导出Keras模型(以h5格式和pb格式为例),并在Python和C++中使用该模型:


train.py:

import numpy as np
import tensorflow as tf


def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):
    """
    Freezes the state of a session into a pruned computation graph.

    Creates a new computation graph where variable nodes are replaced by
    constants taking their current value in the session. The new graph will be
    pruned so subgraphs that are not necessary to compute the requested
    outputs are removed.
    @param session The TensorFlow session to be frozen.
    @param keep_var_names A list of variable names that should not be frozen,
                          or None to freeze all the variables in the graph.
    @param output_names Names of the relevant graph outputs.
    @param clear_devices Remove the device directives from the graph for better portability.
    @return The frozen graph definition.
    """
    graph = session.graph
    with graph.as_default():
        freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
        output_names = output_names or []
        output_names += [v.op.name for v in tf.global_variables()]
        input_graph_def = graph.as_graph_def()
        if clear_devices:
            for node in input_graph_def.node:
                node.device = ''
        frozen_graph = tf.graph_util.convert_variables_to_constants(
            session, input_graph_def, output_names, freeze_var_names)
        return frozen_graph


X = np.array([[0,0], [0,1], [1,0], [1,1]], 'float32')
Y = np.array([[0], [1], [1], [0]], 'float32')

model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Dense(64, input_dim=2, activation='relu'))
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))

model.compile(loss='mean_squared_error', optimizer='adam', metrics=['binary_accuracy'])

model.fit(X, Y, batch_size=1, nb_epoch=100, verbose=0)

# inputs:  ['dense_input']
print('inputs: ', [input.op.name for input in model.inputs])

# outputs:  ['dense_4/Sigmoid']
print('outputs: ', [output.op.name for output in model.outputs])

model.save('./xor.h5')

frozen_graph = freeze_session(tf.keras.backend.get_session(), output_names=[out.op.name for out in model.outputs])
tf.train.write_graph(frozen_graph, './', 'xor.pbtxt', as_text=True)
tf.train.write_graph(frozen_graph, './', 'xor.pb', as_text=False)

predict.py:

import numpy as np
import tensorflow as tf

model = tf.keras.models.load_model('./xor.h5')

# 0 ^ 0 =  [[0.01974997]]
print('0 ^ 0 = ', model.predict(np.array([[0, 0]])))

# 0 ^ 1 =  [[0.99141496]]
print('0 ^ 1 = ', model.predict(np.array([[0, 1]])))

# 1 ^ 0 =  [[0.9897714]]
print('1 ^ 0 = ', model.predict(np.array([[1, 0]])))

# 1 ^ 1 =  [[0.00406971]]
print('1 ^ 1 = ', model.predict(np.array([[1, 1]])))

opencv-predict.py:

import numpy as np
import cv2 as cv


model = cv.dnn.readNetFromTensorflow('./xor.pb')

# 0 ^ 0 =  [[0.01974997]]
model.setInput(np.array([[0, 0]]), name='dense_input')
print('0 ^ 0 = ', model.forward(outputName='dense_4/Sigmoid'))

# 0 ^ 1 =  [[0.99141496]]
model.setInput(np.array([[0, 1]]), name='dense_input')
print('0 ^ 1 = ', model.forward(outputName='dense_4/Sigmoid'))

# 1 ^ 0 =  [[0.9897714]]
model.setInput(np.array([[1, 0]]), name='dense_input')
print('1 ^ 0 = ', model.forward(outputName='dense_4/Sigmoid'))

# 1 ^ 1 =  [[0.00406971]]
model.setInput(np.array([[1, 1]]), name='dense_input')
print('1 ^ 1 = ', model.forward(outputName='dense_4/Sigmoid'))

predict.cpp:

#include <cstdlib>
#include <iostream>
#include <opencv2/opencv.hpp>

int main(int argc, char **argv)
{
    cv::dnn::Net net;

    net = cv::dnn::readNetFromTensorflow("./xor.pb");

    // 0 ^ 0 = [0.018541215]
    float x0[] = { 0, 0 };
    net.setInput(cv::Mat(1, 2, CV_32F, x0), "dense_input");
    std::cout << "0 ^ 0 = " << net.forward("dense_4/Sigmoid") << std::endl;

    // 0 ^ 1 = [0.98295897]
    float x1[] = { 0, 1 };
    net.setInput(cv::Mat(1, 2, CV_32F, x1), "dense_input");
    std::cout << "0 ^ 1 = " << net.forward("dense_4/Sigmoid") << std::endl;

    // 1 ^ 0 = [0.98810625]
    float x2[] = { 1, 0 };
    net.setInput(cv::Mat(1, 2, CV_32F, x2), "dense_input");
    std::cout << "1 ^ 0 = " << net.forward("dense_4/Sigmoid") << std::endl;

    // 1 ^ 1 = [0.010002014]
    float x3[] = { 1, 1 };
    net.setInput(cv::Mat(1, 2, CV_32F, x3), "dense_input");
    std::cout << "1 ^ 1 = " << net.forward("dense_4/Sigmoid") << std::endl;

    return EXIT_SUCCESS;
}

1
非常感谢您提供完整的示例。我能否请您添加一句话?如果人们直接使用keras而不是tf.keras,他们还需要在函数调用中使用“keras.backend.get_session()”而不是“tf.keras.backend.get_session()”,否则会出现关于未初始化变量的错误。我之前没有意识到您在使用前缀“tf.keras”,这个小差别浪费了我一个小时的时间... - gebbissimo
这对我非常有帮助。一个不使用cv2的opencv-predict.py替代方案:import tensorflow as tf from tensorflow.python.platform import gfile f = gfile.FastGFile(r'.\xor\xor.pb', 'rb') graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) f.close() tfSession = tf.InteractiveSession() tfSession.graph.as_default() tf.import_graph_def(graph_def) out = tfSession.graph.get_tensor_by_name('import/dense_4/Sigmoid:0') tfSession.run(out, {'import/dense_input:0': np.array([[0,0]])}) - BitPusher16
@gebbissimo 我遇到了这个错误 AttributeError: module 'tensorflow.keras.backend' has no attribute 'get_session'。你有什么想法吗?我是使用 tf.keras.backend.get_session() 的方式来调用的。 - S Andrew

12

目前所有较早的答案都已过时。截至Tensorflow 2.1

from tensorflow.keras.models import Model, load_model
model = load_model(MODEL_FULLPATH)
model.save(MODEL_FULLPATH_MINUS_EXTENSION)

将创建一个包含“saved_model.pb”的文件夹


它在我的环境下可以运行,但当我尝试在openCV中使用它时,出现了以下错误:FAILED: ReadProtoFromBinaryFile(param_file, param)。无法解析GraphDef文件:... - Gefilte Fish
2
不知道。到目前为止生成的.pb文件是有效的。可能是您正在使用的Tensorflow版本和OpenCV版本之间出现了新的不兼容性。我会制作一个最小化的示例并在OpenCV中创建一个问题。 - WurmD
2
@GefilteFish SavedModel将模型导出到一个目录中,其中的saved_model.pb不是GraphDef。这就是为什么ReadProtoBinary无法加载它的原因。请改用LoadSavedModel。有关更多详细信息,请查看此答案:https://dev59.com/RLzpa4cB1Zd3GeqPL3wB#63189994 - High Performance Rangsiman

7

当您想转换为TensorFlow时,有一个非常重要的点。如果您使用了dropout、batch normalization或其他类似这些的层(它们具有可计算但不可训练的值),您应该更改Keras后端的学习阶段。这里是关于此的讨论

import keras.backend as K
k.set_learning_phase(0) # 0 testing, 1 training mode

6

这个解决方案对我很有帮助。感谢https://medium.com/tensorflow/training-and-serving-ml-models-with-tf-keras-fd975cc0fa27提供的帮助。

import tensorflow as tf

# The export path contains the name and the version of the model
tf.keras.backend.set_learning_phase(0) # Ignore dropout at inference
model = tf.keras.models.load_model('./model.h5')
export_path = './PlanetModel/1'

# Fetch the Keras session and save the model
# The signature definition is defined by the input and output tensors
# And stored with the default serving key
with tf.keras.backend.get_session() as sess:
    tf.saved_model.simple_save(
        sess,
        export_path,
        inputs={'input_image': model.input},
        outputs={t.name:t for t in model.outputs})

4
请使用 tf.saved_model.simple_save,以下是一些示例代码:
with tf.keras.backend.get_session() as sess:
    tf.saved_model.simple_save(
        sess,
        export_path,
        inputs={'input': keras_model.input},
        outputs={'output': keras_model.output})

===更新====

你可以使用作为保存的模型,示例代码:

saved_model_path = tf.contrib.saved_model.save_keras_model(model, "./saved_models")

在Tensorflow 2.0中,simple_save似乎已经被弃用了。 - scribu
2
@scribu,没错,在TF2.0中我们可以参考export_to_savedmodel - lasclocker

2

如果你只需要模型进行推理,你应该先冻结图形,然后将其写成一个.pb文件。代码片段如下所示(代码来自这里):

import tensorflow as tf
from tensorflow.python.framework import graph_util
from tensorflow.python.framework import graph_io
import keras
from keras import backend as K

sess = K.get_session()

constant_graph = graph_util.convert_variables_to_constants(
        sess,
        sess.graph.as_graph_def(),
        ["name_of_the_output_graph_node"])

graph_io.write_graph(constant_graph, "path/to/output/folder", 
                     "output_model_name", as_text=False)

您可以使用 keras_to_tensorflow 工具来完成上述操作:https://github.com/amir-abdi/keras_to_tensorflow keras_to_tensorflow 工具会处理上述操作,并提供一些额外的功能,以实现更多样化的解决方案。只需使用正确的输入参数(例如 input_modeloutput_model 标志)调用即可。
如果您想在 tensorflow 中重新训练模型,请使用上述工具和 output_meta_ckpt 标志导出检查点和元图。

1
使用 estimator.export_savedmodel 我们可以轻松地将 h5 模型转换为 saved model。 在此处查看文档 https://www.tensorflow.org/api_docs/python/tf/estimator/Estimator
def prepare_image(image_str_tensor):
    image_contents = tf.read_file(image_str_tensor)
    image = tf.image.decode_jpeg(image_contents, channels=3)
    image = tf.image.resize_images(image, [224, 224])
    image = tf.cast(image, tf.float32)
    return preprocess_input(image)

def serving_input_receiver_fn():
    input_ph = tf.placeholder(tf.string, shape=[None])
    images_tensor = tf.map_fn(
          prepare_image, input_ph, back_prop=False, dtype=tf.float32)
    images_tensor = tf.image.convert_image_dtype(images_tensor, 
                      dtype=tf.float32)

    return tf.estimator.export.ServingInputReceiver({"input": images_tensor}, 
             {'image_url': input_ph})

estimator = tf.keras.estimator.model_to_estimator(
    keras_model_path=h5_model_path
)

estimator.export_savedmodel(saved_model_path, serving_input_receiver_fn=serving_input_receiver_fn)

1

tf 2.2.0

导入tensorflow.keras而不是仅仅使用keras,因为它会将您的模型加载为keras.engine.sequential.Sequential对象,该对象无法直接转换为tensorflow .pb格式。

#import keras
import tensorflow.keras as keras
model = keras.models.load_model(load_path)
model.save(save_path)

出现以下错误: ValueError: 试图保存一个函数 b '__inference_forward_lstm_1_layer_call_fn_14156',该函数引用了一个非简单常量符号张量 Tensor("dropout/mul_1:0", shape=(None, 300), dtype=float32)。不支持这种操作。 - Abhishek Gangwar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接