Image Recognition(图像识别)

图像识别

我们的大脑让视觉看起来很容易。它不需要任何努力让人类分辨狮子和美洲虎,阅读标志或识别人类的脸部。但这些实际上是用计算机解决的难题:它们看起来很容易,因为我们的大脑非常善于理解图像。

在过去的几年中,机器学习领域在解决这些难题方面取得了巨大的进步。特别是,我们发现一种称为深度卷积神经网络的模型可以在纯视觉识别任务上实现合理的性能 - 在某些领域匹配或超过人类的表现。

研究人员通过验证他们对ImageNet的工作证明了计算机视觉领域的稳步进步- ImageNet是计算机视觉的学术基准。连续的模型继续显示出改进,每次都实现了最新的最新结果:QuocNetAlexNetInception(GoogLeNet)BN-Inception-v2。Google内部和外部的研究人员发表了描述所有这些模型的论文,但结果仍然难以重现。我们现在正在通过发布我们最新型号Inception-v3上运行图像识别的代码来进行下一步。

Inception-v3 使用2012年的数据对ImageNet大型视觉识别难题进行了训练。这是计算机视觉中的一项标准任务,模型试图将整个图像分为1000个类,如“斑马”,“Dalmatian”和“洗碗机”。例如,以下是AlexNet对一些图像进行分类的结果:

为了比较模型,我们检查了模型未能预测正确答案的频率,作为他们排名前5位的猜测之一 - 被称为“五大错误率”。AlexNet通过在2012年验证数据集上设置15.3%的五大错误率来实现; 初始(GoogLeNet)达到6.67%; BN-Inception-v2实现4.9%; Inception-v3达到3.46%。

人类在ImageNet挑战中的表现如何?Andrej Karpathy 有一篇博客文章试图衡量自己的表现。他达到了5.1%的五大错误率。

本教程将教你如何使用Inception-v3。您将学习如何将图像分类为Python或C ++中的1000个类。我们还将讨论如何从这个模型中提取更高级的特征,这些特征可能会被其他视觉任务重用。

我们很高兴看到社区将使用这种模式做些什么。

与Python API一起使用

classify_image.pytensorflow.org程序第一次运行时下载训练好的模型。您的硬盘上需要大约200M的可用空间。

首先下载GitHub上的TensorFlow模型repo。运行以下命令:

cd models/tutorials/image/imagenet python classify_image.py

上述命令将对提供的熊猫图像进行分类。

如果模型正确运行,脚本将生成以下输出:

giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca (score = 0.88493) indri, indris, Indri indri, Indri brevicaudatus (score = 0.00878) lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens (score = 0.00317) custard apple (score = 0.00149) earthstar (score = 0.00127)

如果你想提供其他JPEG图像,你可以通过编辑--image_file参数来完成。

如果将模型数据下载到其他目录,则需要指向所--model_dir用的目录。

与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

在这种情况下,我们使用Admiral Grace Hopper的默认图像,并且您可以看到网络正确识别出她身穿军装,并且得分高达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_widthinput_height标志。我们还需要将像素值从0到255之间的整数缩放到图形操作的浮点值。我们使用input_meaninput_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运算符,它与我们想要加载的图像的文件名组成一个张量。然后将其作为第一个输入传递给ReadFile操作。您可能会注意到我们将b.opts()作为所有操作创建函数的最后一个参数传递。该参数确保节点被添加到保存在模型中的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, "'" }

如果您已经查看了图片加载代码,很多术语应该看起来很熟悉。我们不是使用 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( }

然后我们从GraphDef中创建一个Session对象并将它传回给调用者,以便他们稍后可以运行它。

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维向量。有一个关于如何去做部分的指导。

学习更多资源

为了了解一般的神经网络,Michael Nielsen的免费在线书籍是一个很好的资源。特别是对于卷积神经网络,克里斯·奥拉有一些很好的博客文章,而迈克尔·尼尔森的书有一个很好的章节涵盖了他们。

要了解有关实施卷积神经网络的更多信息,可以跳到TensorFlow深度卷积网络教程,或者从我们的ML初学者或ML专家的MNIST入门教程开始更加轻松。最后,如果你想加快在这方面的研究速度,你可以阅读本教程中引用的所有论文的最新工作。