在linux18.04上配置tensorflow1.14的C++環境


隨便吐槽下:

前段時間因爲項目的原因在ARM上配置了tensorflow以及tensorflow lite的ARM NN SDK環境(有興趣的朋友可以去看看傳送門),因爲最近板子還沒有到,就想先試一下在pc上配置一下linux上的tensirflow的C++環境。這邊文章主要記錄一下在配置tensorflow的C++環境時的步驟以及踩到的坑。總的來說,之所以去編譯,就是爲了以後方便在自己做的東西上面使用google的tensorflow庫。找了一圈發現大致有兩種方法,一種是把項目放到tensorflow源碼中,然後利用bazel編譯,這樣編譯時間較長,而且編譯出來的二進制文件較大。另一種是將tensorflow的靜態庫編譯出來拿到項目中使用,這種方法易於管理。這篇文章主要講的就是第二種方法。


準備工作:

首先下載tensorflow的源碼(這裏有個坑,建議不要直接克隆master分支,這裏我也不知道爲啥,下載了master分支裏面沒有contrib文件夾,而下載指定版本就有,希望有懂的大佬可以指導一下):

# 下載指定的版本1.14.0 
git clone -b v1.14.0 https://github.com/tensorflow/tensorflow.git
# 進入tensorflow文件夾
cd tensorflow
# 配置tensorflow
./configure

這裏的./configure可以配置也可以不選擇配置,如果需要配置的話可以參照官網的選項說明進行配置,我這裏沒有選擇配置。接下來需要安裝對應版本的bazel和gcc,查看官網推薦來選擇對應版本,這裏我貼出了linux的版本要求:

我用的是tensorflow-1.14.0,選擇的是python2.7、GCC 4.8、Bazel 0.24.1。

因爲linux18.04默認版本爲gcc-7,所以需要降低版本,降低gcc版本的方法推薦使用命令:

# 直接apt安裝需要的版本
sudo apt-get install gcc-version
# 設置鏈接組優先級
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 10
# 使用配置命令可以手動選擇
sudo update-alternatives --config gcc

bazel安裝推薦官網下載.sh文件直接安裝,安裝方法——>傳送門


編譯源碼:

按理說運行下面這個命令就會在bazel-bin/tensorflow文件夾下生成libtensorflow_cc.so和libtensorflow_framework.so。

# 編譯源碼(如果要調用cuda,加命令--config=cuda)
bazel build //tensorflow:libtensorflow_cc.so

但是我在運行之後並沒有看到libtensorflow_framework.so這個庫,只有libtensorflow_framework.so.1,這裏我糾結了很久,一直沒有找到解決方法,在其他博客上看到的方法不知道是版本問題或者是因爲時間問題,在我這裏都不起作用,然後我嘗試了一下:

bazel build //tensorflow:libtensorflow_framework.so

沒想到一下成功了,我也不知道是僥倖還是什麼版本更新的問題,但是總的來說成功就行,hhhhhhhhhhh

由於tensorflow需要用到第三方庫文件比如Protobuf以及矩陣庫Eigen等,所以還需要繼續編譯下第三方庫文件:

./tensorflow/contrib/makefile/build_all_linux.sh

這裏就是我在開頭說的一個大坑,剛開始由於我是直接clone的master分支,所以我找了半天也沒有找到contrib這個文件夾,一度讓我懷疑是否版本更新太快把這個contrib文件夾合併到了其他地方,找了我好久,最後打開以前下載的1.11.0版本的tensorflow發現又有這個文件夾,然後才發現問題。執行這個命令需要的時間挺長,要下載的依賴項也挺多,這裏最好科學上網,編譯完成後在download和gen兩個文件夾中有我們需要的第三方依賴的頭文件和庫。


編寫CMakefile.txt文件來編譯可執行文件

前面已經生成了我們需要的庫和頭文件,然後有兩種方法來調用,一種是通過cmakefile.txt文件來指定各個庫和頭文件的路徑,另一種是將庫和頭文件整理好放入/usr/local文件夾中(這種方式即將下面CMakefile.txt提到的頭文件放入/usr/local/include中,將庫放入/usr/local/lib 中即可)。

  • 編寫CMakefile.txt
#項目名稱
project(test)
#設置c++編譯器
set(CMAKE_CXX_STANDARD 11)
#設置TENSORFLOW_DIR變量,變量內容爲安裝的tensorflow文件夾路徑
set(TENSORFLOW_DIR /home/wang/work/ARM-BAZEL/tensorflow-1.14.0)
#項目中的include路徑
include_directories(${TENSORFLOW_DIR} //tensorFlow頭文件
                    ${TENSORFLOW_DIR}/tensorflow/contrib/makefile/gen/proto //tensorflow pb文件生成的pb.h頭文件
                    ${TENSORFLOW_DIR}/tensorflow/contrib/makefile/gen/protobuf-host/include //protobuf頭文件
                    ${TENSORFLOW_DIR}/tensorflow/contrib/makefile/downloads/eigen //eigen頭文件
                    ${TENSORFLOW_DIR}/tensorflow/contrib/makefile/downloads/nsync/public //nsync頭文件)

#項目中lib路徑
link_directories(${TENSORFLOW_DIR}/tensorflow/contrib/makefile/gen/lib //tensorflow靜態庫
                 ${TENSORFLOW_DIR}/tensorflow/contrib/makefile/gen/protobuf-host/lib //protobuf靜態庫
                 ${TENSORFLOW_DIR}/bazel-bin/tensorflow //tensorflow動態庫
                 ${TENSORFLOW_DIR}/tensorflow/contrib/makefile/downloads/nsync/builds /default. linux.c++11 //nsync靜態庫)

add_executable(test main.cpp)
#連接libtensorflow_cc.so和libtensorflow_framework庫。
target_link_libraries(test tensorflow_cc tensorflow_framework)

上面將需要用到的庫和頭文件全都鏈接到了,如果報錯缺少某庫或者某頭文件,將報錯的庫或頭文件的地址找到,然後添加進去即可。一般如果找不到其地址,大概率是因爲下載的時候網速中斷或者其他原因而沒有下載成功,建議重新運行一下build_all_linux.sh。最後通過cmake可以構建main.cpp的可執行程序test

  • 編寫測試文件main.cpp

編寫一個main.cpp文件來生成一個session,內容如下:

#include <tensorflow/core/platform/env.h>
#include <tensorflow/core/public/session.h>
#include <iostream>

using namespace std;
using namespace tensorflow;

int main()
{
    Session* session;
    Status status = NewSession(SessionOptions(), &session);
    if (!status.ok()) {
        cout << status.ToString() << "\n";
        return 1;
    }
    cout << "Session successfully created.\n";
}

然後就可以cmake了,運行代碼如下,最後會生成test可執行文件。

cmake .

make

./test

運行test文件後會顯示 Session successful created. 這樣就代表tensorflow的C++ API接口成功安裝了。

這裏也可以用官方的測試程序,編寫hello_tf.c如下:

#include <stdio.h>
#include <tensorflow/c/c_api.h>

int main() {
  printf("Hello from TensorFlow C library version %s\n", TF_Version());
  return 0;
}

按上述步驟運行後會顯示 Hello from TensorFlow C library version 1.14.0


使用C++ API接口來調用自己的模型並預測

使用C++接口來編寫預測預訓練模型的代碼,這裏選用mnist手寫數字識別來演示,整個流程如下:

  • 在python環境下訓練好自己的模型,並凍結成.pb文件。
  • 編寫預測代碼,使用C++ API調用生成好的.pb文件。
  • 預測代碼可以分成五部分:創建Session、導入模型、加載模型、設置模型輸入輸出、運行模型

爲了使用方便,我將前面提到過的庫和頭文件全部放入新建的項目文件夾中。

cd /home/wang/work/ARM-BAZEL
# 建立工程文件夾
mkdir project
cd project
# 建立庫和頭文件的文件夾
mkdir include lib
cd ../tensorflow-1.14.0
# 複製頭文件
cp -r third_party ../projet/include/tensorflow
cp -r bazel-genfiles ../projet/include/tensorflow
cp -r tensorflow/c ../projet/include/tensorflow/tensorflow
cp -r tensorflow/cc ../projet/include/tensorflow/tensorflow
cp -r tensorflow/core ../projet/include/tensorflow/tensorflow
cp -r tensorflow/comtrib/makefile/gen/proto ../projet/include
cp -r tensorflow/comtrib/makefile/download/absl ../projet/include
cp -r tensorflow/contrib/makefile/download/eigen ../projet/include
cp -r tensorflow/contrib/makefile/download/nsync/public ../projet/ include
cp -r tensorflow/comtrib/makefile/gen/protobuf-host/include ../projet/include
# 複製庫
cp -r bazel-bin/tensorflow/lib* ../project/lib
cp -r tensorflow/comtrib/makefile/gen/lib ../project/lib
cp -r tensorflow/comtrib/makefile/gen/protobuf-host/lib ../project/lib
cp -r tensorflow/contrib/makefile/download/nsync/builds/default.linux.c++11 ../project/lib

然後將工程中無用的.cc文件刪除掉:

cd ../project
find . -name "*.cc" -type f -delete

接下來就可以愉快的在工程項目中進行我們的工作啦。

 一、生成pb文件

因爲我們平時使用tensorflow都是在python環境下使用,tensorflow的python API較其他來說也多,所以我們現在python環境下訓練一個神經網絡的模型用來識別手勢。關於模型訓練的過程這裏不多闡述,網上到處都是,我用的是兩層CNN和一層全連接。在訓練模型時要注意給輸入和輸出命名,等下調用時會用上。這裏說一下我用的固化pb文件的代碼,將下面這段代碼加載主程序中就可以生成pb文件:

# 導入需要的函數
from tensorflow.python.framework.graph_util import convert_variables_to_constants

# 加在session中
graph = convert_variables_to_constants(sess, sess.graph_def, ["softmax"])
tf.train.write_graph(graph,'models','model.pb',as_text=False)

在上面的convert_variables_to_constants()中,第三個參數時我們在模型中定義的輸出。(我的網絡最後的輸出代碼:output = tf.nn.softmax(logit, name='softmax'))

二、在C++環境中調用pb文件

在網上查找在C++環境下調用pb文件時,發現大家都是根據tensorflow例程中的label_image的主函數來改寫的,但是大部分都已經過時了,沒有跟上庫的變化,所以如果用以前的就會出現函數找不到的錯誤,根據這個傳送門的寫法,我們直接copy下來再根據自己的模型稍微做出調整即可。首先定義一下讀取圖片的函數:

// 將指定文件讀入
static Status ReadEntireFile(tensorflow::Env* env,const string& filename,Tensor* output) {
	tensorflow::uint64 file_size = 0;
	TF_RETURN_IF_ERROR(env->GetFileSize(filename,&file_size));
	
	string contents;
	contents.resize(file_size);

	std::unique_ptr<tensorflow::RandomAccessFile> file;
	TF_RETURN_IF_ERROR(env->NewRandomAccessFile(filename,&file));

	tensorflow::StringPiece data;
	TF_RETURN_IF_ERROR(file->Read(0,file_size,&data,&(contents)[0]));
	if (data.size() != file_size) {
		return tensorflow::errors::DataLoss("Truncated read of '", filename, "' expected ",file_size," got ",data.size());
	}
	output->scalar<string>()() = string(data);

	return Status::OK();
}

// 將給定的圖像讀入,並將其調整爲所需要的大小
Status ReadTensorFromImageFile(const string& file_name,const int input_height,const int input_width,
			       const float input_mean,const float input_std,
			       std::vector<Tensor>* out_tensors) {
	auto root = tensorflow::Scope::NewRootScope();
	using namespace ::tensorflow::ops;

	string input_name = "file_reader";
	string output_name = "normalized";

	// 將file_name讀取到名爲input的張量中
	Tensor input(tensorflow::DT_STRING, tensorflow::TensorShape());
	TF_RETURN_IF_ERROR(
		ReadEntireFile(tensorflow::Env::Default(), file_name,&input));

	// 使用佔位符讀取輸入數據
	auto file_reader = Placeholder(root.WithOpName("input"),tensorflow::DataType::DT_STRING);

	std::vector<std::pair<string, tensorflow::Tensor>> inputs = {{"input",input},};

	// 嘗試找出文件類型並對其進行解碼
	const int wanted_channels = 3;
	tensorflow::Output image_reader;
	if (tensorflow::str_util::EndsWith(file_name, ".png")) {
		 image_reader = DecodePng(root.WithOpName("png_reader"), file_reader,
		 DecodePng::Channels(wanted_channels));
	} 

	// gif
        else if (tensorflow::str_util::EndsWith(file_name, ".gif")) {
		image_reader =
			Squeeze(root.WithOpName("squeeze_first_dim"),
                		DecodeGif(root.WithOpName("gif_reader"), file_reader));
	} 

	// bmp
        else if (tensorflow::str_util::EndsWith(file_name, ".bmp")) {
		image_reader = DecodeBmp(root.WithOpName("bmp_reader"), file_reader);
	} 

        // 假設它既不是PNG也不是GIF,則必須是JPEG
        else {
		image_reader = DecodeJpeg(root.WithOpName("jpeg_reader"), file_reader,
                              		  DecodeJpeg::Channels(wanted_channels));
	}
	
        // 將圖像數據轉換爲浮點數,以便我們對其進行常規數學運算
	auto float_caster =
		Cast(root.WithOpName("float_caster"), image_reader, tensorflow::DT_FLOAT);
	
        // TensorFlow中圖像操作的約定是期望所有圖像要分批處理,因此它們是索引爲[batch, height, width, channel]。
	// 因爲我們只有一個圖像,所以我們必須以ExpandDims()開始添加批處理尺寸1
	auto dims_expander = ExpandDims(root.WithOpName("expand"), float_caster, 0);
	// 對圖片進行雙線性調整以適合所需的尺寸
	// auto resized = ResizeBilinear(
		// root, dims_expander,
		// Const(root.WithOpName("size"), {input_height, input_width}));
	
        // 減去均值併除以比例
	// Div(root.WithOpName(output_name), Sub(root, resized, {input_mean}),
	   // {input_std});
	float input_max = 255;
	Div(root.WithOpName("div"),dims_expander,input_max);

        // 這會運行我們剛剛構建的GraphDef網絡定義,並且在輸出張量中返回結果
	tensorflow::GraphDef graph;
	TF_RETURN_IF_ERROR(root.ToGraphDef(&graph));

	std::unique_ptr<tensorflow::Session> session(
		tensorflow::NewSession(tensorflow::SessionOptions()));
	TF_RETURN_IF_ERROR(session->Create(graph));
	TF_RETURN_IF_ERROR(session->Run({inputs}, {"div"}, {}, out_tensors));
	return Status::OK();
}

然後再是主函數,主函數主要調整是讀取圖片時,自己模型訓練的圖片大小要和這裏一致,不然會導致圖片讀取失敗。

int main()
{
	//創建新會話Session
	Session* session;
	Status status = NewSession(SessionOptions(), &session);
	if (!status.ok()){
		std::cout << status.ToString() << std::endl;
	}else{
		std::cout << "Session successful created." << std::endl;
	}

	string model_path = "model.pb";
	//
	GraphDef graphdef;

	//從pb文件中讀取模型
	Status status_load = ReadBinaryProto(Env::Default(),model_path,&graphdef);
	if (!status_load.ok()){
		std::cout << "ERROR:Lonading model failed..." << model_path << std::endl;
		std::cout << status_load.ToString() << "\n";
		return -1;
	}

	//將模型導入會話Session中
	Status status_create = session->Create(graphdef);
	if (!status_create.ok()){
		std::cout << "ERROR:Creating graph in session failed..." << status_create.ToString() << std::endl;
		return -1;
	}
	cout << "Session successfully created." << endl;

	//讀取圖片
	string image_path = "/home/wang/work/ARM-BAZEL/Cmake_demo/project1/test.jpg";
	int input_height = 64;
	int input_width = 64;
	int input_mean = 0;
	int input_std = 3;
	//
	std::vector<Tensor> resized_tensors;
	Status read_tensor_status = ReadTensorFromImageFile(image_path,input_height,input_width,input_mean,input_std,&resized_tensors);
	if (!read_tensor_status.ok()){
		LOG(ERROR) << read_tensor_status;
		cout << "resing error" << endl;
		return -1;
	}

	const Tensor& resized_tensor = resized_tensors[0];
	std::cout << resized_tensor.DebugString() << endl;

	//運行模型
	vector<tensorflow::Tensor> outputs;
	string output_node = "softmax";
	Status status_run = session->Run({{"inputs", resized_tensor}},{output_node},{},&outputs);
	if (!status_run.ok()){
		std::cout << "ERROR:RUN failed..." << std::endl;
		std::cout << status_run.ToString() << "\n";
		return -1;
	}

	//獲取輸出值
	std::cout << "Output tensor size:" << outputs.size() << std::endl;
	for (std::size_t i=0; i < outputs.size();i++){
		std::cout << outputs[i].DebugString() << endl;
	}

	//輸出類別概率
	Tensor t = outputs[0];
	int ndim2 = t.shape().dims();
	auto tmap = t.tensor<float,2>();
	int output_dim = t.shape().dim_size(1);
	std::vector<double> tout;

	//獲取最終預測標籤和概率
	int output_class_id = -1;
	double output_prob = 0.0;
	for (int j = 0;j < output_dim; j++)
	{
		std::cout << "Class" << j << "prob:" << tmap(0,j) << "," << std::endl;
		if (tmap(0,j) >= output_prob) {
			output_class_id = j;
			output_prob = tmap(0,j);
		}
	}

	//
	std::cout << "Final class id: " << output_class_id << std::endl;
	std::cout << "Final class prob: " << output_prob << std::endl;	

	//關閉會話
	session-> Close();
	return 0;
}

到這裏就終於結束了,然後按照前半部分的CMakefile.txt的寫法,更改一下庫和頭文件的位置,再把模型和圖片都放入工程目錄中就可以運行了。最終我預測的是我自己拍的數字5的手勢,然後也是成功預測出來爲5啦,大功告成hhhhhhhhhhhhhhhhh

 

發佈了7 篇原創文章 · 獲贊 1 · 訪問量 3199
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章