从0开始使用tensorflow的c++库进行模型推断

背景

之前我们一直用MNN来作为推断框架,且取得了不错的效果!

近期要在服务器上跑一下模型,我们也顺理成章想到用MNN的GPU,主要是OpenCL和Vulkan,因为在Android手机上已经验证过这两个backend的可行性,所以认为在x86平台上也是顺水推舟的事情。

然后就打脸了,而且是在项目快到期之前才发现这些问题:
在这里插入图片描述
在这里插入图片描述

心慌意乱,然后只能赶紧用Tensorflow的c++库救急:

主要内容

1.编译TF c++的库

建议参考以下两篇文章:
TF–C++动态库编译从头到尾的详解
tensorflow C++动态库编译
1 下载tensorflow_1.15.0版本
2 安装0.26.0以下版本的bazel
3 执行configure

./configure

4 编译两个库

bazel build --config=opt //tensorflow:libtensorflow_cc.so
bazel build --config=opt //tensorflow:libtensorflow_framework.so

5 编译其他依赖

6 整理库和头文件

在顺利编译完成之后,最好将头文件和库分别整理到include 和lib 文件夹,方便后续使用。

2.加载图,构造Session

主要有以下两步:

//加载图
//model_path 是指具体的模型pb文件的路径
tensorflow::GraphDef graphdef
tensorflow::Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef);


//构造Session
Session *session;
tensorflow::Status status_create = session->Create(graphdef);

3.构建输入tensor

如果将模型推理看做一个黑盒函数,那么Session的构造就完成了函数的定义,推理过程就是调用这个黑盒函数,为了得到推理的结果,我们要构建合适的输入,对TF来说,这个输入就是:

std::vector<std::pair<std::string, tensorflow::Tensor> > input

可以看到,这个input首先是一个vector,然后vector的元素是键值对,key是Tensor的名字,Value是Tensor。

所以构造输入也分为两步,一是构造Tensor,二是构造键值对:
一般我们会接受两种类型的输入,一是cv::Mat, 二是unsinged char* 。不管是什么类型,都是两步,一是将数据转为float类型,二是将值拷贝到Tensor的数据空间,以cv::Mat构造Tensor举例:

Tensor EdgeInfer::ReadTensorFromImageMat(cv::Mat img)
{
    img.convertTo(img,CV_32FC1);
    img = (img - mInputMean)/mInputStd;
    tensorflow::Tensor input_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({1, mInputHeight, mInputWidth, mChannel}));
    auto input_tensor_mapped = input_tensor.tensor<float, 4>();

    const float *source_data = (float *)img.data;

    for (int y = 0; y < mInputHeight; ++y)
    {
        const float *source_row = source_data + (y * mInputWidth * mChannel);
        for (int x = 0; x < mInputWidth; ++x)
        {
            const float *source_pixel = source_row + (x * mChannel);
            for (int c = 0; c < mChannel; ++c)
            {
                const float *source_value = source_pixel + c;
                input_tensor_mapped(0, y, x, c) = *source_value;
            }
        }
    }
    return input_tensor;
}

然后将Tensor封装成键值对:

input.push_back(std::pair<std::string, Tensor>(node_name, input_tensor));

4.执行Session

在有了input之后,Session的执行就是傻瓜式操作:

tensorflow::Status status = session->Run(input, {output_node}, {}, &outputs);

参数:

  • input, 前面构造的输入
  • {output_node}, 待返回的节点的名字构成的vector,会在outputs中被返回
  • {}, 第三个参数是目标节点的名称,会执行到该节点,但是不会返回
  • outputs, 返回的tensor

5.获取输出

前面已经说到返回的tensor都在Session->Run()的第四个参数中,和输入类似,我们一般也不会直接操作Tensor,而是会将Tensor转为cv::Mat或者float*,这里也是两步,一是获取到每个输出tensor的shape和数据指针,二是将数据输出到指定的格式, 参考此文

int tfTensor2cvMat(const tensorflow::Tensor& inputTensor, cv::Mat& output)
{
	tensorflow::TensorShape inputTensorShape = inputTensor.shape();
	if (inputTensorShape.dims() != 4)
	{
		return -1;
	}

	int height = inputTensorShape.dim_size(1);
	int width = inputTensorShape.dim_size(2);
	int depth = inputTensorShape.dim_size(3);

	output = cv::Mat(height, width, CV_32FC(depth));
	auto inputTensorMapped = inputTensor.tensor<float, 4>();
	float* data = (float*)output.data;
	for (int y = 0; y < height; ++y)
	{
		float* dataRow = data + (y * width * depth);
		for (int x = 0; x < width; ++x)
		{
			float* dataPixel = dataRow + (x * depth);
			for (int c = 0; c < depth; ++c)
			{
				float* dataValue = dataPixel + c;
				*dataValue = inputTensorMapped(0, y, x, c);
			}
		}
	}
	return 0;
}

主要问题

1.系统版本导致的运行时c++库报错

举个例子,在Ubuntu 18.04系统上编译得到的动态库如果在16.04系统上运行,会报一系列运行时库的错误,错误都指向glibc++。这是因为18.04的glibc++比16.04要更新,所以不向下兼容。

需要注意的是,向上兼容是支持的,亲测在16.04上编的动态库,在18.04也可以正常运行。

还有就是,千万不要尝试升级glibc++,不要尝试升级glibc++,不要尝试升级glibc++,老老实实重新编tensorflow。

2.获取输出tensor时数据时报错

这个错误非常典型,很容易遇到,且目前网上没有比较好的解决办法,这里记录我们遇到的问题以及解决办法。

报错是:

Check failed: NDIMS == dims() (4 vs. 2)Asking for tensor of 4 dimensions from a tensor of 2 dimensions

乍一看,这是一个TensorFlow内部报出来的错误,似乎不太好修改,我们层层寻找,发现报错的根源在于我们操作的tensor的shape和实际不一致,具体来说,我们把这个tensor当做NHWC格式来使用,但是实际上这个tensor就是一个n batch的一维向量,那么就会报上面的错误。

源码追溯如下:

把tensor都当做NHWC格式来使用

auto inputTensorMapped = inputTensor.tensor<float, 4>();

调用Tensor::tensor()

template <typename T, size_t NDIMS>
typename TTypes<T, NDIMS>::Tensor Tensor::tensor() {
  CheckTypeAndIsAligned(DataTypeToEnum<T>::v());
  return typename TTypes<T, NDIMS>::Tensor(base<T>(),
                                           shape().AsEigenDSizes<NDIMS>());
}

调用AsEigenDSizes

template <int NDIMS, typename IndexType>
Eigen::DSizes<IndexType, NDIMS> TensorShape::AsEigenDSizes() const {
  CheckDimsEqual(NDIMS);
  return AsEigenDSizesWithPadding<NDIMS, IndexType>();
}

调用CheckDimsEqual

void TensorShape::CheckDimsEqual(int NDIMS) const {
  CHECK_EQ(NDIMS, dims()) << "Asking for tensor of " << NDIMS << " dimensions"
                          << " from a tensor of " << dims() << " dimensions";
}

这就是我们熟悉的报错了!
解决的办法是将可能遇到的情况分别处理,调用shape = tensor.shape(),获取到shape,然后再根据shape.size()就可以获取tensor的维度,然后再分别处理各种类型的维度的情况,需要注意的是,因为Eigen是高度抽象的模板类,所以在inputTensor.tensor<float, 4>()函数中需要传入的第二个参数必须是右值!

总结

我们大概介绍了如何使用tensorflow的动态库来进行模型的加载以及输入输出的构造和获取,在此基础上我们分析了两个可能遇到的坑,以及解决办法,尤其是对于Asking for tensor of ....类报错,我们仔细分析了报错的原因,并给出了详细的解决办法。

希望能有所帮助!

参考

https://blog.csdn.net/heiheiya/article/details/89454884
https://zhuanlan.zhihu.com/p/42187985
https://zhuanlan.zhihu.com/p/58570658
https://zhuanlan.zhihu.com/p/91892469
https://blog.csdn.net/u011285477/article/details/93975689#整理库文件和头文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章