tflite:get_tensor 在非输出张量上给出随机值

tflite: get_tensor on non-output tensors gives random values

我正在尝试调试使用自定义操作的 tflite 模型。我找到了 op 名称(在 *.pb 中)和 op id(在 *.tflite 中)之间的对应关系,并且我正在进行逐层比较(以确保输出差异始终在 1e-4 范围内(因为它在最后爆炸了,我想找到我的自定义图层失败的确切位置)如下:


方法一:我用get_tensor得到输出如下:

from tensorflow.contrib.lite.python import interpreter

# load the model
model = interpreter.Interpreter(model_path='model.tflite')
model.allocate_tensors()

# get tensors
for i in tensor_ids:
    tensor_output[i] = model.get_tensor(i)

它显示随机值完全不足(与 TensorFlow 模型的输出相比)。


方法二:*.pb只转换到某一层,然后重复,基本上:

  1. 创建一个 *.pb 以便它仅包含从 inputlayer_1 的网络。

  2. 转换为tflite(所以输出现在是layer_1)并用TensorFlow检查TF-Lite的输出。

  3. layer_2layer_3、...outputs 重复步骤 1-2。

此方法需要更多的工作和执行,但它正确地表明对于内置操作,tflitepb 模型的输出是相同的,只是在我的自定义中开始有所不同ops(在方法 1 中,输出立即从第一层发散)。


Question: Why the behaviour of get_tensor is so strange? Maybe it is because I am using tensorflow 1.9(when TF-Lite was still not released and available only in developer preview)?

PS:我知道TF-Lite的发布,但是我已经为我的项目手动编译了TensorFlow 1.9,现在很难更改版本。

几个月前我遇到了同样的问题。问题是,TF-Lite 与 TensorFlow 完全不同——它使用静态内存和执行计划、内存映射文件以加快加载速度,并且它应该优化网络前向传播管道中的所有可能。

我不是 TF-Lite 的开发人员,但我想它通过重新使用用于先前计算操作的内存区域来保持其内存占用极低。让我们看看下图的想法:


第 1 步: 首先,我们将输入提供给符号张量 I(在括号中)。假设它的值存储在一个名为 buffer_1.

的缓冲区中
     op1       op2       op3
(I) ---->  A  ---->  B  ---->  O
_________________________________
^^^        ^^^^^^^^^^^^       ^^^
input      intermediate    output
tensor     tensors         tensor

第 2 步: 现在,我们需要在符号张量 I 上计算 op1 以获得符号张量 A。我们计算 buffer_1 并将符号张量 A 的值存储在名为 buffer_2.

的缓冲区中
    [op1]      op2       op3
(I) ----> (A) ---->  B  ---->  O

第 3 步: 现在,我们在符号张量 A 上计算 op2 以获得符号张量 B。我们计算 buffer_2 并将符号张量 B 的值存储在名为 buffer_3...

的缓冲区中
     op1      [op2]      op3
 I  ----> (A) ----> (B) ---->  O

但是等等! 如果我们现在有未使用的 buffer_1,那么为什么要浪费我们的内存存储在 buffer_3 中,而现在的值是获取输出 O 没用?因此,我们实际上将此操作的结果存储在 buffer_1!

中,而不是存储在 buffer_3

这是高效内存重用的基本思想,我认为它在 TF-Lite 中实现了,因为它在 toco 和其他东西中内置了静态图形分析器。这就是为什么你不能简单地将 get_tensor 应用于非输出张量。


更简单的调试方法?

您提到您正在编写自定义操作,所以我想您已经用 bazel 构建了 tflite,对吗?然后你实际上可以在文件 tensorflow/lite/interpreter.cc 中向 Interpreter::Invoke() 注入一些日志记录代码。一个丑陋的 hack,但它有效。

PS:如果有任何 TensorFlow Lite 开发人员遇到并对此发表评论,我将很高兴:)

是的,中间张量可以被覆盖,除非指定为输出。

编辑: 我设法通过在转换期间将所有操作都放在输出列表中来解决问题。然后在运行时保留它们,并且可以正确读取值。

参见:

我在想要将 TFLite 文件转换为另一个框架时遇到了类似的问题,但无法访问用于制作 TFLite 文件的原始 TF 图。因为我的转换输出与 TFLite 模型的输出不同,所以我想查看中间层的输出。感谢这个关于 SO 的主题,我了解到 get_tensor() 不是一个可靠的方法。

最简单的解决方案是在十六进制编辑器中编辑 TFLite 文件!

模型的输出是模型中一个张量的索引。在我的例子中,它是张量 175(你可以用 get_tensor_details() 看到它。它作为小端 int32 存储在 TFLite 文件的某处。对于张量 175,TFLite 值将包含值 0xAF000000。

我希望模型输出改为使用张量 3,因此我在十六进制编辑器中打开 TFLite 文件,搜索 0xAF000000,并将其替换为 0x03000000。保存文件并使用 TFLite 解释器再次加载它。奇迹般有效。您只需要注意该文件可能包含多次出现的 0xAF000000(或您要查找的任何内容)。在我的 TFLite 文件中,它存储在接近尾部的位置。

我希望这个提示对某人有用。 :-)

默认情况下,TFLite 不保留中间张量,这是因为它优化了内存使用并根据数据流依赖性重用分配给张量的内存。 您可以使用新添加的调试功能来保留所有张量

interpreter = tf.lite.Interpreter(
    model_path="test.tflite",
    experimental_preserve_all_tensors=True)

现在您可以在此解释器上检查中间张量。

卡里姆的回答

interpreter = tf.lite.Interpreter( model_path="test.tflite", experimental_preserve_all_tensors=True)

是最直接的解决方案,但你必须在 tensorflow>=2.5.0 上。

我们无法直接从 TFlite 模型中获取中间输入和输出。但是,我们可以通过修改模型缓冲区来获取层的输入和输出。 This repo 展示了它是如何完成的。我们需要修改平面缓冲区架构才能使其正常工作。修改后的 TFlite 架构(tflite 存储库中的文件夹)在存储库中可用。

为了回答的完整,下面是相关代码:

def buffer_change_output_tensor_to(model_buffer, new_tensor_i):
    # from https://github.com/raymond-li/tflite_tensor_outputter
    # Set subgraph 0's output(s) to new_tensor_i
    # Reads model_buffer as a proper flatbuffer file and gets the offset programatically
    # It might be much more efficient if Model.subgraphs[0].outputs[] was set to a list of all the tensor indices.
    fb_model_root = tflite_model.Model.GetRootAsModel(model_buffer, 0)
    output_tensor_index_offset = fb_model_root.Subgraphs(0).OutputsOffset(0) # Custom added function to return the file offset to this vector
    # print("buffer_change_output_tensor_to. output_tensor_index_offset: ")
    # print(output_tensor_index_offset)
    # output_tensor_index_offset = 0x5ae07e0 # address offset specific to inception_v3.tflite
    # output_tensor_index_offset = 0x16C5A5c # address offset specific to inception_v3_quant.tflite
    # Flatbuffer scalars are stored in little-endian.
    new_tensor_i_bytes = bytes([
        new_tensor_i & 0x000000FF, \
        (new_tensor_i & 0x0000FF00) >> 8, \
        (new_tensor_i & 0x00FF0000) >> 16, \
        (new_tensor_i & 0xFF000000) >> 24 \
    ])
    # Replace the 4 bytes corresponding to the first output tensor index
    return model_buffer[:output_tensor_index_offset] + new_tensor_i_bytes + model_buffer[output_tensor_index_offset + 4:]

def get_tensor(path_tflite, tensor_id):
    with open(path_tflite, 'rb') as fp:
        model_buffer = fp.read()
    
    model_buffer = buffer_change_output_tensor_to(model_buffer, int(tensor_id))
    interpreter = tf.lite.Interpreter(model_content=model_buffer)
    interpreter.allocate_tensors()
    tensor_details = interpreter._get_tensor_details(tensor_id)
    tensor_name = tensor_details['name']
    
    input_details = interpreter.get_input_details()
    interpreter.set_tensor(input_details[0]['index'], input_tensor)
    interpreter.invoke()
    
    tensor = interpreter.get_tensor(tensor_id)
    return tensor