使用C ++ API
您可以在C ++ 中运行相同的Inception-v3模型,以便在生产环境中使用。您可以下载包含定义模型的GraphDef的存档(从TensorFlow存储库的根目录运行):
curl -L "https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz" |
tar -C tensorflow/examples/label_image/data -xz
接下来,我们需要编译包含加载和运行图形代码的C ++二进制文件。如果您按照 说明下载适用 于您的平台的TensorFlow的源代码安装,您应该能够通过从您的shell终端运行此命令来构建该示例:
bazel build tensorflow/examples/label_image/...
那应该创建一个二进制可执行文件,然后你可以这样运行:
bazel-bin/tensorflow/examples/label_image/label_image
这使用框架附带的默认示例图像,并应输出类似于此的内容:
I tensorflow/examples/label_image/main.cc:206] military uniform (653): 0.834306
I tensorflow/examples/label_image/main.cc:206] mortarboard (668): 0.0218692
I tensorflow/examples/label_image/main.cc:206] academic gown (401): 0.0103579
I tensorflow/examples/label_image/main.cc:206] pickelhaube (716): 0.00800814
I tensorflow/examples/label_image/main.cc:206] bulletproof vest (466): 0.00535088
在这种情况下,我们正在使用海军上将魔法师的默认图像 ,您可以看到网络正确识别她穿着军装,得分高达0.8。
接下来,通过提供--image =参数,例如,尝试在自己的图像上
bazel-bin/tensorflow/examples/label_image/label_image --image=my_image.png
如果您查看tensorflow/examples/label_image/main.cc
文件内容,可以了解它的工作原理。我们希望这段代码可以帮助您将TensorFlow集成到您自己的应用程序中,因此我们将逐步介绍主要功能:
命令行标志控制文件的加载位置以及输入图像的属性。该模型希望获得299x299的RGB图像,所以这些是input_width
和input_height
标志。我们还需要将从0到255之间的整数的像素值缩放到图形运算的浮点值。我们使用input_mean
和input_std
标志控制缩放:我们首先input_mean
从每个像素值中减去 ,然后除以input_std
。
这些值可能看起来有点神奇,但是它们只是由原始模型作者根据他/她想用作输入图像进行培训而定义的。如果您有一个自己训练过的图表,那么您只需要调整这些值,使其与您在培训过程中使用的任何值相匹配。
您可以看到它们如何应用于ReadTensorFromImageFile()
函数中的图像 。
// Given an image file name, read in the data, try to decode it as an image,
// resize it to the requested size, and then scale the values as desired.
Status ReadTensorFromImageFile(string file_name, const int input_height,
const int input_width, const float input_mean,
const float input_std,
std::vector<Tensor>* out_tensors) {
tensorflow::GraphDefBuilder b;
我们首先创建一个GraphDefBuilder
可以用来指定运行或加载的模型的对象。
string input_name = "file_reader";
string output_name = "normalized";
tensorflow::Node* file_reader =
tensorflow::ops::ReadFile(tensorflow::ops::Const(file_name, b.opts()),
b.opts().WithName(input_name));
然后,我们开始创建我们要运行的小型模型的节点,以加载,调整大小和缩放像素值,以获得主模型期望作为其输入的结果。我们创建的第一个节点只是一个Const
op,它包含我们要加载的图像的文件名的张量。那就是作为第一个输入ReadFile
。您可能会注意到我们b.opts()
作为最后一个参数传递给所有op创建函数。该参数确保将节点添加到模型定义中GraphDefBuilder
。我们也打电话给ReadFile
运营商。这给了一个节点的名字,这并不是绝对必要的,因为如果你不这样做,就会分配一个自动的名字,但这样会使调试变得更容易一些。WithName()
b.opts()
// Now try to figure out what kind of file it is and decode it.
const int wanted_channels = 3;
tensorflow::Node* image_reader;
if (tensorflow::StringPiece(file_name).ends_with(".png")) {
image_reader = tensorflow::ops::DecodePng(
file_reader,
b.opts().WithAttr("channels", wanted_channels).WithName("png_reader"));
} else {
// Assume if it's not a PNG then it must be a JPEG.
image_reader = tensorflow::ops::DecodeJpeg(
file_reader,
b.opts().WithAttr("channels", wanted_channels).WithName("jpeg_reader"));
}
// Now cast the image data to float so we can do normal math on it.
tensorflow::Node* float_caster = tensorflow::ops::Cast(
image_reader, tensorflow::DT_FLOAT, b.opts().WithName("float_caster"));
// The convention for image ops in TensorFlow is that all images are expected
// to be in batches, so that they're four-dimensional arrays with indices of
// [batch, height, width, channel]. Because we only have a single image, we
// have to add a batch dimension of 1 to the start with ExpandDims().
tensorflow::Node* dims_expander = tensorflow::ops::ExpandDims(
float_caster, tensorflow::ops::Const(0, b.opts()), b.opts());
// Bilinearly resize the image to fit the required dimensions.
tensorflow::Node* resized = tensorflow::ops::ResizeBilinear(
dims_expander, tensorflow::ops::Const({input_height, input_width},
b.opts().WithName("size")),
b.opts());
// Subtract the mean and divide by the scale.
tensorflow::ops::Div(
tensorflow::ops::Sub(
resized, tensorflow::ops::Const({input_mean}, b.opts()), b.opts()),
tensorflow::ops::Const({input_std}, b.opts()),
b.opts().WithName(output_name));
然后,我们继续添加更多的节点,将文件数据解码为图像,将整数转换为浮点值,调整其大小,然后最终对像素值执行减法和除法运算。
// This runs the GraphDef network definition that we've just constructed, and
// returns the results in the output tensor.
tensorflow::GraphDef graph;
TF_RETURN_IF_ERROR(b.ToGraphDef(&graph));
最后,我们有一个存储在b变量中的模型定义,我们将其转换成具有该ToGraphDef()
函数的完整图形定义。
std::unique_ptr<tensorflow::Session> session(
tensorflow::NewSession(tensorflow::SessionOptions()));
TF_RETURN_IF_ERROR(session->Create(graph));
TF_RETURN_IF_ERROR(session->Run({}, {output_name}, {}, out_tensors));
return Status::OK();
然后我们创建一个tf.Session
对象,它是实际运行图形的接口,并运行它,指定要获取输出的节点以及输出数据的位置。
这给我们一个Tensor
对象的向量,在这种情况下,我们知道只有一个对象很长。Tensor
在这种情况下,您可以将其视为多维数组,并将其作为浮点值保存299像素高,299像素宽,3通道图像。如果您已经在产品中拥有自己的图像处理框架,那么只要在将图像输入主图形之前应用相同的变换即可使用。
这是一个在C ++中动态创建小TensorFlow图的简单示例,但是对于预先训练的Inception模型,我们要从文件中加载更大的定义。你可以看到我们如何在LoadGraph()
函数中这样做。
// Reads a model graph definition from disk, and creates a session object you
// can use to run it.
Status LoadGraph(string graph_file_name,
std::unique_ptr<tensorflow::Session>* session) {
tensorflow::GraphDef graph_def;
Status load_graph_status =
ReadBinaryProto(tensorflow::Env::Default(), graph_file_name, &graph_def);
if (!load_graph_status.ok()) {
return tensorflow::errors::NotFound("Failed to load compute graph at '",
graph_file_name, "'");
}
如果您浏览了图像加载代码,则很多术语应该看起来很熟悉。而不是使用a GraphDefBuilder
来生成一个GraphDef
对象,我们加载一个直接包含的protobuf文件GraphDef
。
session->reset(tensorflow::NewSession(tensorflow::SessionOptions()));
Status session_create_status = (*session)->Create(graph_def);
if (!session_create_status.ok()) {
return session_create_status;
}
return Status::OK();
}
然后我们创建一个Session对象,GraphDef
并将其传递给调用者,以便以后可以运行它。
该GetTopLabels()
功能非常像图像加载,除了在这种情况下,我们要获取运行主图的结果,并将其转换为最高评分标签的排序列表。就像图像加载器一样,它创建一个 GraphDefBuilder
,添加了几个节点,然后运行短图来获得一对输出张量。在这种情况下,它们表示最高结果的排序分数和索引位置。
// Analyzes the output of the Inception graph to retrieve the highest scores and
// their positions in the tensor, which correspond to categories.
Status GetTopLabels(const std::vector<Tensor>& outputs, int how_many_labels,
Tensor* indices, Tensor* scores) {
tensorflow::GraphDefBuilder b;
string output_name = "top_k";
tensorflow::ops::TopK(tensorflow::ops::Const(outputs[0], b.opts()),
how_many_labels, b.opts().WithName(output_name));
// This runs the GraphDef network definition that we've just constructed, and
// returns the results in the output tensors.
tensorflow::GraphDef graph;
TF_RETURN_IF_ERROR(b.ToGraphDef(&graph));
std::unique_ptr<tensorflow::Session> session(
tensorflow::NewSession(tensorflow::SessionOptions()));
TF_RETURN_IF_ERROR(session->Create(graph));
// The TopK node returns two outputs, the scores and their original indices,
// so we have to append :0 and :1 to specify them both.
std::vector<Tensor> out_tensors;
TF_RETURN_IF_ERROR(session->Run({}, {output_name + ":0", output_name + ":1"},
{}, &out_tensors));
*scores = out_tensors[0];
*indices = out_tensors[1];
return Status::OK();
该PrintTopLabels()
功能采用这些排序结果,并以友好的方式打印出来。该CheckTopLabel()
功能非常相似,但只是确保顶级标签是我们期望的标签,用于调试目的。
最后,main()
将所有这些电话联系在一起。
int main(int argc, char* argv[]) {
// We need to call this to set up global state for TensorFlow.
tensorflow::port::InitMain(argv[0], &argc, &argv);
Status s = tensorflow::ParseCommandLineFlags(&argc, argv);
if (!s.ok()) {
LOG(ERROR) << "Error parsing command line flags: " << s.ToString();
return -1;
}
// First we load and initialize the model.
std::unique_ptr<tensorflow::Session> session;
string graph_path = tensorflow::io::JoinPath(FLAGS_root_dir, FLAGS_graph);
Status load_graph_status = LoadGraph(graph_path, &session);
if (!load_graph_status.ok()) {
LOG(ERROR) << load_graph_status;
return -1;
}
我们加载主图。
// Get the image from disk as a float array of numbers, resized and normalized
// to the specifications the main graph expects.
std::vector<Tensor> resized_tensors;
string image_path = tensorflow::io::JoinPath(FLAGS_root_dir, FLAGS_image);
Status read_tensor_status = ReadTensorFromImageFile(
image_path, FLAGS_input_height, FLAGS_input_width, FLAGS_input_mean,
FLAGS_input_std, &resized_tensors);
if (!read_tensor_status.ok()) {
LOG(ERROR) << read_tensor_status;
return -1;
}
const Tensor& resized_tensor = resized_tensors[0];
加载,调整大小并处理输入图像。
// Actually run the image through the model.
std::vector<Tensor> outputs;
Status run_status = session->Run({ {FLAGS_input_layer, resized_tensor}},
{FLAGS_output_layer}, {}, &outputs);
if (!run_status.ok()) {
LOG(ERROR) << "Running model failed: " << run_status;
return -1;
}
在这里,我们运行加载的图形作为输入图像。
// This is for automated testing to make sure we get the expected result with
// the default settings. We know that label 866 (military uniform) should be
// the top label for the Admiral Hopper image.
if (FLAGS_self_test) {
bool expected_matches;
Status check_status = CheckTopLabel(outputs, 866, &expected_matches);
if (!check_status.ok()) {
LOG(ERROR) << "Running check failed: " << check_status;
return -1;
}
if (!expected_matches) {
LOG(ERROR) << "Self-test failed!";
return -1;
}
}
为了测试的目的,我们可以检查以确保我们在这里得到我们预期的输出。
// Do something interesting with the results we've generated.
Status print_status = PrintTopLabels(outputs, FLAGS_labels);
最后我们打印出我们发现的标签。
if (!print_status.ok()) {
LOG(ERROR) << "Running print failed: " << print_status;
return -1;
}
这里的错误处理是使用TensorFlow的Status
对象,这是非常方便的,因为它可以让您知道ok()
检查器是否发生任何错误,然后可以打印出来,提供可读的错误消息。
在这种情况下,我们正在演示对象识别,但是您应该可以在各种领域中使用与您已经找到或训练过的其他型号相似的代码。我们希望这个小例子为您提供如何在您自己的产品中使用TensorFlow的一些想法。
练习:转移学习是一个想法,如果你知道如何解决一个很好的任务,你应该能够转移一些理解来解决相关的问题。执行传输学习的一种方法是去除网络的最终分类层,并提取CNN的下一层(最后一层),在这种情况下为2048维向量。在how-to部分中有一个指导。