之前我们一直用MNN来作为推断框架,且取得了不错的效果!
近期要在服务器上跑一下模型,我们也顺理成章想到用MNN的GPU,主要是OpenCL和Vulkan,因为在Android手机上已经验证过这两个backend的可行性,所以认为在x86平台上也是顺水推舟的事情。
心慌意乱,然后只能赶紧用Tensorflow的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 文件夹,方便后续使用。
主要有以下两步:
//加载图
//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);
如果将模型推理看做一个黑盒函数,那么Session的构造就完成了函数的定义,推理过程就是调用这个黑盒函数,为了得到推理的结果,我们要构建合适的输入,对TF来说,这个输入就是:
std::vector > 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();
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(node_name, input_tensor));
在有了input之后,Session的执行就是傻瓜式操作:
tensorflow::Status status = session->Run(input, {output_node}, {}, &outputs);
参数:
前面已经说到返回的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* 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;
}
举个例子,在Ubuntu 18.04系统上编译得到的动态库如果在16.04系统上运行,会报一系列运行时库的错误,错误都指向glibc++。这是因为18.04的glibc++比16.04要更新,所以不向下兼容。
需要注意的是,向上兼容是支持的,亲测在16.04上编的动态库,在18.04也可以正常运行。
还有就是,千万不要尝试升级glibc++,不要尝试升级glibc++,不要尝试升级glibc++,老老实实重新编tensorflow。
这个错误非常典型,很容易遇到,且目前网上没有比较好的解决办法,这里记录我们遇到的问题以及解决办法。
报错是:
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();
调用Tensor::tensor()
template
typename TTypes::Tensor Tensor::tensor() {
CheckTypeAndIsAligned(DataTypeToEnum::v());
return typename TTypes::Tensor(base(),
shape().AsEigenDSizes());
}
调用AsEigenDSizes
template
Eigen::DSizes TensorShape::AsEigenDSizes() const {
CheckDimsEqual(NDIMS);
return AsEigenDSizesWithPadding();
}
调用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
函数中需要传入的第二个参数必须是右值!
我们大概介绍了如何使用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#整理库文件和头文件