TensorFlow LSTM Object Detection 模型导出 TFLite 完整指南:从 checkpoint 到 FlatBuffer 的两步转换
【免费下载链接】modelsModels and examples built with TensorFlow项目地址: https://gitcode.com/GitHub_Trending/mode/models
导读
LSTD(LSTM SSD)视频目标检测模型在训练完成后,若要部署到移动端 / 嵌入式 TFLite 运行时,需要把 TensorFlow 训练出的 checkpoint 依次转换成「TFLite 兼容的冻结图」与「TFLite FlatBuffer 模型」两个中间产物。本文基于 exporting_models.md 的官方流程,结合 models 仓库 中export_tflite_lstd_graph.py、export_tflite_lstd_model.py及其底层库实现,完整讲解每一步的命令、参数含义、图结构与节点规格,并给出使用test_tflite_model.py校验导出的模型是否可正常推理的收尾方法。读完本文,你将能够独立把一个训练好的 LSTD checkpoint 导出为可在应用侧直接加载的*.tflite文件,并理解冻结图输入 / 输出张量为何被设计成视频序列形式。
为什么 LSTD 的 TFLite 导出需要两步
LSTD 模型包含 LSTM 时序单元,其输入不是单张静态图片,而是一段长度为unroll_length的视频帧序列(维度为[unroll_length, height, width, 3])。这就决定了它与普通 SSD 模型的导出路径不同:不能直接走标准的 saved_model / frozen graph 单步转换。
从 exporting_models.md 可以提炼出官方推荐的固定两步流水线:
- 从 checkpoint 导出 TFLite 兼容的冻结图(frozen graph)——把训练权重固化成一张输入 / 输出已按 TFLite 约定定义的
tflite_graph.pb; - 从冻结图导出 TFLite FlatBuffer 模型——把上一步的
tflite_graph.pb交给 TFLite 转换器,产出最终供应用加载的*.tflite文件。
在 research/lstm_object_detection 目录下,这两个步骤分别对应可执行脚本 export_tflite_lstd_graph.py 与 export_tflite_lstd_model.py。下面逐一展开。
导出前的工程与配置前提
运行目录与依赖
- 导出命令设计为从
tensorflow/models/research目录(即本仓库的 research 目录)发起执行,命令行中以lstm_object_detection/xxx.py的相对模块路径调用脚本; - 底层实现 export_tflite_lstd_graph_lib.py 除导入
lstm_object_detection自身外,还依赖同仓库 research/object_detection 下的exporter、builders.graph_rewriter_builder、builders.post_processing_builder、core.box_list等模块,因此需要保证该目录位于PYTHONPATH中; - 脚本基于
tensorflow.compat.v1编写(见两处脚本的 import 与tf.app.run),请使用兼容 TensorFlow 1.x API 的 TensorFlow 版本环境运行。
pipeline config 中的硬性约束
从源码实现(export_tflite_lstd_graph_lib.py)可以确认,参与导出的 pipeline config 必须满足以下条件,否则会直接抛错:
| 约束 | 说明 | 源码行为 |
|---|---|---|
| 检测模型类型只能是 SSD | 仅model { ssd { ... } }支持 TFLite 导出 | 非 SSD 时抛出ValueError('Only ssd models are supported in tflite. ...') |
图像缩放器必须是fixed_shape_resizer | TFLite 侧需要完全确定的高度 / 宽度 | 其它 resizer 类型抛出ValueError |
lstm_model配置块必须存在 | 需要其中的eval_unroll_length决定输入序列长度 | 导出时读取lstm_config.eval_unroll_length |
| 输入必须是固定尺寸的 RGB(或灰度)视频序列 | 输入占位符为[eval_unroll_length, height, width, 3](若开启convert_to_grayscale则通道数为 1) | 依 config 动态构造 placeholder |
仓库给出的两份参考配置可以印证这些字段的实际写法:
- lstm_ssd_mobilenet_v1_imagenet.config:
train_unroll_length: 4、eval_unroll_length: 4,image_resizer { fixed_shape_resizer { height: 256 width: 256 } },检测类别数num_classes: 30,使用 Faster R-CNN box coder(y_scale/x_scale: 10.0、height_scale/width_scale: 5.0),后处理采用batch_non_max_suppression与score_converter: SIGMOID; - lstm_ssd_interleaved_mobilenet_v2_imagenet.config:基于 Interleaved MobileNet V2 的 LSTD 变体,
image_resizer为 320×320,同样配置了eval_unroll_length: 4。
注意 TFLite 导出读取的是eval 路径下的固定分辨率与eval_unroll_length,这些字段最终决定了冻结图的输入张量形状,训练与导出配置需保持一致。
第一步:从 checkpoint 导出 TFLite 冻结图
官方命令
在 exporting_models.md 中,官方给出从tensorflow/models/research目录执行如下命令:
# 位于 research 目录下执行 PIPELINE_CONFIG_PATH={path to pipeline config} TRAINED_CKPT_PREFIX=/{path to model.ckpt} EXPORT_DIR={path to folder that will be used for export} python lstm_object_detection/export_tflite_lstd_graph.py \ --pipeline_config_path ${PIPELINE_CONFIG_PATH} \ --trained_checkpoint_prefix ${TRAINED_CKPT_PREFIX} \ --output_directory ${EXPORT_DIR} \ --add_preprocessing_op执行成功后,${EXPORT_DIR}目录下将出现两个文件:
tflite_graph.pb:二进制格式的 TFLite 兼容冻结图;tflite_graph.pbtxt:同一张图的文本格式版本,便于人工检查节点结构。
脚本完整参数表(以源码为准)
对照 export_tflite_lstd_graph.py 中的 flag 定义,可以拿到比文档更完整的参数语义与默认值。注意文档命令行中写作--add_preprocessing_op,而当前仓库源码中对应的 flag 实际名为add_postprocessing_op,且默认值为True(历史版本命名可能有差异,请以你所 checkout 的源码为准)。
| Flag | 类型 | 默认值 | 必填 | 含义 |
|---|---|---|---|---|
--output_directory | string | None | 是 | 导出产物输出目录 |
--pipeline_config_path | string | None | 是 | pipeline_pb2.TrainEvalPipelineConfig格式的配置文件路径 |
--trained_checkpoint_prefix | string | None | 是 | 训练 checkpoint 前缀(如.../model.ckpt) |
--max_detections | integer | 10 | 否 | 最多输出的检测框数量 |
--max_classes_per_detection | integer | 1 | 否 | 每个检测框最多输出的类别数 |
--detections_per_class | integer | 100 | 否 | Regular NMS 中每个类别使用的 anchor 数量 |
--add_postprocessing_op | bool | True | 否 | 是否在图中追加 TFLite 自定义后处理算子 |
--use_regular_nms | bool | False | 否 | 后处理算子使用 Regular NMS 还是 Fast NMS |
--config_override | string | '' | 否 | 覆盖 pipeline config 指定字段的文本 proto |
导出图的输入与输出规格
根据脚本与库实现,导出的tflite_graph.pb中节点约定如下(也是后续第二步转换时引用张量名的依据):
输入节点
input_video_tensor:float32张量,形状[unroll_length, height, width, 3],内容是归一化后的输入视频帧序列;- 其中
height/width必须与 pipeline config 的fixed_shape_resizer中配置一致,unroll_length取自lstm_model.eval_unroll_length。
输出节点(未附加后处理时,即add_postprocessing_op=False)
raw_outputs/box_encodings:float32,形状[1, num_anchors, 4],编码后的框回归预测;raw_outputs/class_predictions:float32,形状[1, num_anchors, num_classes],经过 score conversion(SIGMOID 等)后的每 anchor 类别得分;anchors:float32常量张量,形状[num_anchors, 4],中心点 / 尺寸编码后的 anchor 集合。
输出节点(附加后处理时,即add_postprocessing_op=True)
图中会追加名为TFLite_Detection_PostProcess的自定义算子节点,其四个输出为:
detection_boxes:float32,形状[1, num_boxes, 4],框坐标;detection_classes:float32,形状[1, num_boxes],类别索引;detection_scores:float32,形状[1, num_boxes],类别得分;num_boxes:大小为 1 的float32张量,表示检测到的框数量。
底层做了什么:冻结 + 追加 TFLite 自定义算子
深入 export_tflite_lstd_graph_lib.py 可以看到导出函数的完整调用链:
- 读取并校验配置:
config_util.get_configs_from_pipeline_file解析 pipeline config,取出model、lstm_model、eval_config,并校验 SSD 与fixed_shape_resizer; - 收集解码与 NMS 所需参数:从
batch_non_max_suppression读取score_threshold/iou_threshold,从faster_rcnn_box_coder读取y_scale/x_scale/h_scale/w_scale,从model.ssd.num_classes读取类别数(export_tflite_lstd_graph_lib.py); - 构造视频输入与推理图:创建形状为
[eval_unroll_length, height, width, 3]的input_video_tensor占位符,调用model_builder.build(...)构建检测模型并执行 preprocess / predict(export_tflite_lstd_graph_lib.py),NMS 被有意推迟到 TFLite 自定义算子中完成; - 固化原始输出:在
raw_outputs命名空间下输出box_encodings、class_predictions与常量anchors(anchor 通过get_const_center_size_encoded_anchors转成常量节点);若配置中存在graph_rewriter还会在此处执行量化图改写;若eval_config.use_moving_averages为真,则先将滑动平均变量替换进图再冻结(export_tflite_lstd_graph_lib.py); - 冻结图:使用
exporter.freeze_graph_with_def_protos将 checkpoint 权重固化,输出节点为raw_outputs/box_encodings、raw_outputs/class_predictions、anchors; - 按需追加后处理算子:若
add_postprocessing_op=True,调用append_postprocessing_op把TFLite_Detection_PostProcess节点挂到raw_outputs/*与anchors之上,并通过TransformGraph的strip_unused_nodes变换剪掉无用节点(export_tflite_lstd_graph_lib.py)。该节点的属性(max_detections、nms_score_threshold、nms_iou_threshold、y_scale、x_scale等)全部来自第 2 步收集的配置; - 落盘:分别写出
tflite_graph.pb与tflite_graph.pbtxt。
其中第 6 步正是 TFLite 目标检测模型常见的做法:把 NMS 等后处理做成TFLite custom op在端上执行,从而避免 TFLite 转换器不支持这些算子的兼容性问题。
用 config_override 微调推理行为而不改训练配置
脚本还提供了--config_override参数,允许在不改动训练 / 评估配置的前提下,对导出的推理图做小规模参数覆盖。它同样接受pipeline_pb2.TrainEvalPipelineConfig文本 proto,源码 docstring 中给出的例子是把 NMS 的iou_threshold改为 0.5、score_threshold改为 0.0:
python lstm_object_detection/export_tflite_lstd_graph.py \ --pipeline_config_path path/to/lstm_pipeline.config \ --trained_checkpoint_prefix path/to/model.ckpt \ --output_directory path/to/exported_model_directory \ --config_override " \ model{ \ ssd{ \ post_processing { \ batch_non_max_suppression { \ score_threshold: 0.0 \ iou_threshold: 0.5 \ } \ } \ } \ } \ "可以看到该机制在导出「低阈值召回更多框」或「收紧重叠抑制」等推理变体时非常实用。
第二步:从冻结图导出 TFLite FlatBuffer 模型
官方命令
拿到第一步的tflite_graph.pb后,在tensorflow/models/research目录执行:
# 位于 research 目录下执行 FROZEN_GRAPH_PATH={path to exported tflite_graph.pb} EXPORT_PATH={path to filename that will be used for export} PIPELINE_CONFIG_PATH={path to pipeline config} python lstm_object_detection/export_tflite_lstd_model.py \ --export_path ${EXPORT_PATH} \ --frozen_graph_path ${FROZEN_GRAPH_PATH} \ --pipeline_config_path ${PIPELINE_CONFIG_PATH}三个参数均为必填(见脚本中的mark_flag_as_required)。执行成功后,${EXPORT_PATH}即为可直接交付给移动应用加载的 FlatBuffer 模型文件。
脚本内部如何调用转换器
对照 export_tflite_lstd_model.py 的main实现,转换逻辑如下:
configs = config_util.get_configs_from_pipeline_file(FLAGS.pipeline_config_path) lstm_config = configs['lstm_model'] input_arrays = ['input_video_tensor'] output_arrays = [ 'TFLite_Detection_PostProcess', 'TFLite_Detection_PostProcess:1', 'TFLite_Detection_PostProcess:2', 'TFLite_Detection_PostProcess:3', ] input_shapes = { 'input_video_tensor': [lstm_config.eval_unroll_length, 320, 320, 3], } converter = tf.lite.TFLiteConverter.from_frozen_graph( FLAGS.frozen_graph_path, input_arrays, output_arrays, input_shapes=input_shapes) converter.allow_custom_ops = True tflite_model = converter.convert()几个值得注意的细节:
- 输入张量为第一步冻结图唯一的输入
input_video_tensor; - 输出张量为后处理自定义算子的 4 个输出端口
TFLite_Detection_PostProcess及其:1、:2、:3,分别对应detection_boxes / detection_classes / detection_scores / num_boxes——这要求第一步导出时必须开启后处理算子,否则冻结图不存在该节点; - 输入形状中的序列长度取自
lstm_model.eval_unroll_length,而当前仓库源码中高 / 宽硬编码为320, 320,因此该流程面向 320×320 固定输入分辨率设计(若训练配置使用其它分辨率,需要相应调整此处源码,这也侧面印证了导出的图对固定形状的强约束); converter.allow_custom_ops = True是关键开关:由于图中包含TFLite_Detection_PostProcess这一自定义算子,必须允许 custom ops,否则转换器会因遇到未知算子而报错;- 转换完成后直接以二进制方式写入
${EXPORT_PATH}。
收尾验证:用随机输入跑通导出的 tflite
仓库在 test_tflite_model.py 中提供了面向 TFLite 模型的冒烟测试工具,可用于快速确认 FlatBuffer 可被解释器加载并完成一次前向推理:
python lstm_object_detection/test_tflite_model.py \ --model_path ${EXPORT_PATH}该脚本会依次执行:
- 通过
tf.lite.Interpreter(model_path=...)加载模型并allocate_tensors(); - 打印输入 / 输出张量的
input_details与output_details,其中会显示输入形状,可据此核对是否为[unroll_length, 320, 320, 3]; - 用
np.random.random_sample生成随机输入喂入网络并invoke(); - 打印第一个输出张量的内容。
脚本 docstring 也提示:随机输入仅用于验证,实际使用时应把input_data替换为按同形状组织好的真实归一化视频帧。若该步能正常打印输出,说明两步导出产出的 tflite 模型在运行时层面是完整的。
小结与常见问题排查
回顾整条链路,从 checkpoint 到可交付的 TFLite FlatBuffer 共两步、对应两个脚本:
| 阶段 | 脚本 | 输入 | 输出 |
|---|---|---|---|
| 导出冻结图 | export_tflite_lstd_graph.py | pipeline config + checkpoint | tflite_graph.pb/tflite_graph.pbtxt |
| 导出 TFLite | export_tflite_lstd_model.py | tflite_graph.pb+ pipeline config | ${EXPORT_PATH}(FlatBuffer) |
| 冒烟验证 | test_tflite_model.py | tflite 模型路径 | 前向推理输出 |
排障时可以优先对照以下几点(均可从前述源码与配置文件中找到依据):
- pipeline config 必须是 SSD 模型且使用
fixed_shape_resizer,同时存在[lstm_object_detection.protos.lstm_model]配置块(参考两个示例 config); - 输入分辨率与
eval_unroll_length在训练、pipeline config、导出脚本三处必须自洽; - 若应用侧只消费检测框结果(
detection_boxes等 4 项输出),第一步必须保留add_postprocessing_op=True(默认即开启),第二步才能找到TFLite_Detection_PostProcess输出端口; - 第二步转换时
allow_custom_ops必须开启,否则自定义后处理算子会导致转换失败; - 若希望推理图与训练 / 评估配置在 NMS 阈值等参数上有所差异,无需改动原配置,直接通过
--config_override覆盖即可。
【免费下载链接】modelsModels and examples built with TensorFlow项目地址: https://gitcode.com/GitHub_Trending/mode/models
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考