ensorFlow 智能移动项目:6~10(1)https://developer.aliyun.com/article/1426908
以及以下实例变量和处理器实现:
private ImageView mImageView; private Button mButton; private TensorFlowInferenceInterface mInferenceInterface; private String[] mWords = new String[WORD_COUNT]; private int[] intValues; private float[] floatValues; Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { mButton.setText("DESCRIBE ME"); String text = (String)msg.obj; Toast.makeText(MainActivity.this, text, Toast.LENGTH_LONG).show(); mButton.setEnabled(true); } };
- 在
onCreate
方法中,首先在ImageView
中添加显示测试图像并处理按钮单击事件的代码:
mImageView = findViewById(R.id.imageview); try { AssetManager am = getAssets(); InputStream is = am.open(IMAGE_NAME); Bitmap bitmap = BitmapFactory.decodeStream(is); mImageView.setImageBitmap(bitmap); } catch (IOException e) { e.printStackTrace(); } mButton = findViewById(R.id.button); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mButton.setEnabled(false); mButton.setText("Processing..."); Thread thread = new Thread(MainActivity.this); thread.start(); } });
然后添加读取word_counts.txt
每行的代码,并将每个单词保存在mWords
数组中:
String filename = VOCAB_FILE.split("file:///android_asset/")[1]; BufferedReader br = null; int linenum = 0; try { br = new BufferedReader(new InputStreamReader(getAssets().open(filename))); String line; while ((line = br.readLine()) != null) { String word = line.split(" ")[0]; mWords[linenum++] = word; } br.close(); } catch (IOException e) { throw new RuntimeException("Problem reading vocab file!" , e); }
- 现在,在
public void run()
方法中,在DESCRIBE ME
按钮发生onClick
事件时启动,添加代码以调整测试图像的大小,从调整后的位图中读取像素值,然后将它们转换为浮点数-我们已经在前三章中看到了这样的代码:
intValues = new int[IMAGE_WIDTH * IMAGE_HEIGHT]; floatValues = new float[IMAGE_WIDTH * IMAGE_HEIGHT * IMAGE_CHANNEL]; Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open(IMAGE_NAME)); Bitmap croppedBitmap = Bitmap.createScaledBitmap(bitmap, IMAGE_WIDTH, IMAGE_HEIGHT, true); croppedBitmap.getPixels(intValues, 0, IMAGE_WIDTH, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT); for (int i = 0; i < intValues.length; ++i) { final int val = intValues[i]; floatValues[i * IMAGE_CHANNEL + 0] = ((val >> 16) & 0xFF); floatValues[i * IMAGE_CHANNEL + 1] = ((val >> 8) & 0xFF); floatValues[i * IMAGE_CHANNEL + 2] = (val & 0xFF); }
- 创建一个
TensorFlowInferenceInterface
实例,该实例加载模型文件,并通过向其提供图像值,然后在initialState
中获取返回结果来使用该模型进行第一个推断:
AssetManager assetManager = getAssets(); mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE); float[] initialState = new float[STATE_COUNT]; mInferenceInterface.feed(INPUT_NODE1, floatValues, IMAGE_WIDTH, IMAGE_HEIGHT, 3); mInferenceInterface.run(new String[] {OUTPUT_NODE1}, false); mInferenceInterface.fetch(OUTPUT_NODE1, initialState);
- 将第一个
input_feed
值设置为起始 ID,并将第一个state_feed
值设置为返回的initialState
值:
long[] inputFeed = new long[] {START_ID}; float[] stateFeed = new float[STATE_COUNT * inputFeed.length]; for (int i=0; i < STATE_COUNT; i++) { stateFeed[i] = initialState[i]; }
如您所见,得益于 Android 中的TensorFlowInferenceInterface
实现,在 Android 中获取和设置张量值并进行推理比在 iOS 中更简单。 在我们开始重复使用inputFeed
和stateFeed
进行模型推断之前,我们创建了一个captions
列表,该列表包含一对整数和浮点数,其中整数作为单词 ID,具有最大 softmax 值(在模型为每个推理调用返回的所有 softmax 值中)和float
作为单词的 softmax 值。 我们可以使用一个简单的向量来保存每个推论返回中具有最大 softmax 值的单词,但是使用对的列表可以使以后我们从贪婪搜索方法切换到集束搜索时更加容易:
List<Pair<Integer, Float>> captions = new ArrayList<Pair<Integer, Float>>();
- 在字幕长度的
for
循环中,我们将上面设置的值提供给input_feed
和state_feed
,然后获取返回的softmax
和newstate
值:
for (int i=0; i<CAPTION_LEN; i++) { float[] softmax = new float[WORD_COUNT * inputFeed.length]; float[] newstate = new float[STATE_COUNT * inputFeed.length]; mInferenceInterface.feed(INPUT_NODE2, inputFeed, 1); mInferenceInterface.feed(INPUT_NODE3, stateFeed, 1, STATE_COUNT); mInferenceInterface.run(new String[]{OUTPUT_NODE2, OUTPUT_NODE3}, false); mInferenceInterface.fetch(OUTPUT_NODE2, softmax); mInferenceInterface.fetch(OUTPUT_NODE3, newstate);
- 现在,创建另一个由整数和浮点对组成的列表,将每个单词的 ID 和 softmax 值添加到列表中,并以降序对列表进行排序:
List<Pair<Integer, Float>> prob_id = new ArrayList<Pair<Integer, Float>>(); for (int j = 0; j < WORD_COUNT; j++) { prob_id.add(new Pair(j, softmax[j])); } Collections.sort(prob_id, new Comparator<Pair<Integer, Float>>() { @Override public int compare(final Pair<Integer, Float> o1, final Pair<Integer, Float> o2) { return o1.second > o2.second ? -1 : (o1.second == o2.second ? 0 : 1); } });
- 如果最大概率的单词是结束单词,则结束循环。 否则,将该对添加到
captions
列表,并使用最大 softmax 值的单词 ID 更新input_feed
并使用返回的状态值更新state_feed
,以继续进行下一个推断:
if (prob_id.get(0).first == END_ID) break; captions.add(new Pair(prob_id.get(0).first, prob_id.get(0).first)); inputFeed = new long[] {prob_id.get(0).first}; for (int j=0; j < STATE_COUNT; j++) { stateFeed[j] = newstate[j]; } }
- 最后,遍历
captions
列表中的每一对,并将每个单词(如果不是开头和结尾的话)添加到sentence
字符串,该字符串通过处理器返回,以向用户显示自然语言输出:
String sentence = ""; for (int i=0; i<captions.size(); i++) { if (captions.get(i).first == START_ID) continue; if (captions.get(i).first == END_ID) break; sentence = sentence + " " + mWords[captions.get(i).first]; } Message msg = new Message(); msg.obj = sentence; mHandler.sendMessage(msg);
在您的虚拟或真实 Android 设备上运行该应用。 大约需要 10 秒钟才能看到结果。 您可以使用上一节中显示的四个不同的测试图像,并在图 6.9 中查看结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOpv4y9U-1681653119032)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/249e6bc8-7f89-45d4-a217-2cf1c54bf5fe.png)]
图 6.9:在 Android 中显示图像字幕结果
一些结果与 iOS 结果以及 TensorFlow im2txt 网站上的结果略有不同。 但是它们看起来都不错。 另外,在相对较旧的 Android 设备(例如 Nexus 5)上运行该模型的非映射版本也可以。 但是最好在 Android 中加载映射模型,以查看性能的显着提高,我们可能会在本书后面的章节中介绍。
因此,这将使用功能强大的图像字幕模型完成分步的 Android 应用构建过程。 无论您使用的是 iOS 还是 Android 应用,您都应该能够轻松地将我们训练有素的模型和推理代码集成到自己的应用中,或者返回到训练过程以微调模型,然后准备并优化更好的模型。 在您的移动应用中使用的模型。
总结
在本章中,我们首先讨论了由现代端到端深度学习支持的图像字幕如何工作,然后总结了如何使用 TensorFlow im2txt 模型项目训练这种模型。 我们详细讨论了如何找到正确的输入节点名称和输出节点名称,以及如何冻结模型,然后使用最新的图转换工具和映射转换工具修复在将模型加载到手机上时出现的一些讨厌的错误。 之后,我们展示了有关如何使用模型构建 iOS 和 Android 应用以及如何使用模型的 LSTM RNN 组件进行新的序列推断的详细教程。
令人惊讶的是,经过训练了成千上万个图像字幕示例,并在现代 CNN 和 LSTM 模型的支持下,我们可以构建和使用一个模型,该模型可以在移动设备上生成合理的自然语言描述。 不难想象可以在此基础上构建什么样的有用应用。 我们喜欢福尔摩斯吗? 当然不。 我们已经在路上了吗? 我们希望如此。 AI 的世界既令人着迷又充满挑战,但是只要我们不断取得稳步进步并改善自己的学习过程,同时又避免了梯度问题的消失和爆炸,我们就有很大机会建立一个类似于 Holmes 的模型,并可以随时随地在一天中在移动应用中使用它。
漫长的篇章讨论了基于 CNN 和 LSTM 的网络模型的实际使用,我们值得一试。 在下一章中,您将看到如何使用另一个基于 CNN 和 LSTM 的模型来开发有趣的 iOS 和 Android 应用,这些应用使您可以绘制对象然后识别它们是什么。 要快速获得游戏在线版本的乐趣,请访问这里。
七、使用 CNN 和 LSTM 识别绘画
在上一章中,我们看到了使用深度学习模型的强大功能,该模型将 CNN 与 LSTM RNN 集成在一起以生成图像的自然语言描述。 如果深度学习驱动的 AI 就像新的电力一样,我们当然希望看到这种混合神经网络模型在许多不同领域中的应用。 诸如图像字幕之类的严肃应用与之相反? 一个有趣的绘画应用,例如 Quick Draw(请参见这里了解有趣的示例数据),使用经过训练并基于 345 个类别中的 5000 万张绘画的模型,并将新绘画分类到这些类别中,听起来不错。 还有一个正式的 TensorFlow 教程,该教程介绍了如何构建这样的模型来帮助我们快速入门。
事实证明,在 iOS 和 Android 应用上使用本教程构建的模型的任务提供了一个绝佳的机会:
- 加深我们对找出模型的正确输入和输出节点名称的理解,因此我们可以为移动应用适当地准备模型
- 使用其他方法来修复 iOS 中的新模型加载和推断错误
- 首次为 Android 构建自定义的 TensorFlow 本机库,以修复 Android 中的新模型加载和预测错误
- 查看有关如何使用预期格式的输入来输入 TensorFlow 模型以及如何在 iOS 和 Android 中获取和处理其输出的更多示例
此外,在处理所有繁琐而重要的细节的过程中,以便模型可以像魔术一样工作,以进行漂亮的绘画分类,您将在 iOS 和 Android 设备上享受有趣的涂鸦。
因此,在本章中,我们将介绍以下主题:
- 绘画分类 – 工作原理
- 训练并准备绘画分类模型
- 在 iOS 中使用绘画分类模型
- 在 Android 中使用绘画分类模型
绘画分类 – 工作原理
TensorFlow 教程中内置的绘画分类模型,首先接受表示为点列表的用户绘画输入,并将规范化输入转换为连续点的增量的张量,以及有关每个点是否是新笔画的开始的信息。 然后将张量穿过几个卷积层和 LSTM 层,最后穿过 softmax 层,如图 7.1 所示,以对用户绘画进行分类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hFvjcltm-1681653119032)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/ef475044-6568-4e50-bc98-8ff0a0d6efbf.png)]
图 7.1:绘画分类模式
与接受 2D 图像输入的 2D 卷积 API tf.layers.conv2d
不同,此处将 1D 卷积 API tf.layers.conv1d
用于时间卷积(例如绘画)。 默认情况下,在绘画分类模型中,使用三个 1D 卷积层,每个层具有 48、64 和 96 个过滤器,其长度分别为 5、5 和 3。 卷积层之后,将创建 3 个 LSTM 层,每层具有 128 个正向BasicLSTMCell
节点和 128 个反向BasicLSTMCell
节点,然后将其用于创建动态双向循环神经网络,该网络的输出将发送到最终的完全连接层以计算logits
(非标准化的对数概率)。
If you don’t have a good understanding of all these details, don’t worry; to develop powerful mobile apps using a model built by others, you don’t have to understand all the details, but in the next chapter we’ll also discuss in greater detail how you can build a RNN model from scratch for stock prediction, and with that, you’ll have a better understanding of all the RNN stuff.
在前面提到的有趣的教程中详细描述了简单而优雅的模型以及构建模型的 Python 实现,其源代码位于仓库中。 在继续进行下一部分之前,我们只想说一件事:模型的构建,训练,评估和预测的代码与上一章中看到的代码不同,它使用了称为Estimator
的 TensorFlow API,或更准确地说,是自定义Estimator
。 如果您对模型实现的详细信息感兴趣,则应该阅读有关创建和使用自定义Estimator
的指南。 这个页面的models/samples/core/get_started/custom_estimator.py
上的指南的有用源代码。 基本上,首先要实现一个函数,该函数定义模型,指定损失和准确率度量,设置优化器和training
操作,然后创建tf.estimator.Estimator
类的实例并调用其train
,evaluate
和predict
方法。 就像您将很快看到的那样,使用Estimator
可以简化如何构建,训练和推断神经网络模型,但是由于它是高级 API,因此它还会执行一些更加困难的低级任务,例如找出输入和输出节点名称来推断移动设备。
训练,预测和准备绘画分类模型
训练模型非常简单,但为移动部署准备模型则有些棘手。 在我们开始训练之前,请首先确保您已经在 TensorFlow 根目录中克隆了 TensorFlow 模型库,就像我们在前两章中所做的一样。 然后从这里下载绘画分类训练数据集,大约 1.1GB,创建一个名为rnn_tutorial_data
的新文件夹, 并解压缩dataset tar.gz
文件。 您将看到 10 个训练 TFRecord 文件和 10 个评估 TFRecord 文件,以及两个带有.classes
扩展名的文件,它们具有相同的内容,并且只是该数据集可用于分类的 345 个类别的纯文本,例如"sheep", "skull", "donut", "apple"
。
训练绘画分类模型
要训练模型,只需打开终端cd
到tensorflow/models/tutorials/rnn/quickdraw
,然后运行以下脚本:
python train_model.py \ --training_data=rnn_tutorial_data/training.tfrecord-?????-of-????? \ --eval_data=rnn_tutorial_data/eval.tfrecord-?????-of-????? \ --model_dir quickdraw_model/ \ --classes_file=rnn_tutorial_data/training.tfrecord.classes
默认情况下,训练步骤为 100k,在我们的 GTX 1070 GPU 上大约需要 6 个小时才能完成训练。 训练完成后,您将在模型目录中看到一个熟悉的文件列表(省略了其他四组model.ckpt*
文件):
ls -lt quickdraw_model/ -rw-rw-r-- 1 jeff jeff 164419871 Feb 12 05:56 events.out.tfevents.1518422507.AiLabby -rw-rw-r-- 1 jeff jeff 1365548 Feb 12 05:56 model.ckpt-100000.meta -rw-rw-r-- 1 jeff jeff 279 Feb 12 05:56 checkpoint -rw-rw-r-- 1 jeff jeff 13707200 Feb 12 05:56 model.ckpt-100000.data-00000-of-00001 -rw-rw-r-- 1 jeff jeff 2825 Feb 12 05:56 model.ckpt-100000.index -rw-rw-r-- 1 jeff jeff 2493402 Feb 12 05:47 graph.pbtxt drwxr-xr-x 2 jeff jeff 4096 Feb 12 00:11 eval
如果您运行tensorboard --logdir quickdraw_model
,然后从浏览器在http://localhost:6006
上启动 TensorBoard,您会看到精度达到约 0.55,损失到约 2.0。 如果继续进行约 200k 的训练,则精度将提高到约 0.65,损失将下降到 1.3,如图 7.2 所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZwniLh1v-1681653119033)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/479a1d37-1775-459d-a616-c45f2f10f0f1.png)]
图 7.2:300k 训练步骤后模型的准确率和损失
现在,我们可以像上一章一样运行freeze_graph.py
工具,以生成用于移动设备的模型文件。 但是在执行此操作之前,我们首先来看一下如何在 Python 中使用该模型进行推断,例如上一章中的run_inference.py
脚本。
使用绘画分类模型进行预测
看一下models/tutorial/rnn/quickdraw
文件夹中的train_model.py
文件。 当它开始运行时,将在create_estimator_and_specs
函数中创建一个Estimator
实例:
estimator = tf.estimator.Estimator( model_fn=model_fn, config=run_config, params=model_params)
传递给Estimator
类的关键参数是名为model_fn
的模型函数,该函数定义:
- 获取输入张量并创建卷积,RNN 和最终层的函数
- 调用这些函数来构建模型的代码
- 损失,优化器和预测
在返回tf.estimator.EstimatorSpec
实例之前,model_fn
函数还具有一个名为mode
的参数,该参数可以具有以下三个值之一:
tf.estimator.ModeKeys.TRAIN
tf.estimator.ModeKeys.EVAL
tf.estimator.ModeKeys.PREDICT
实现train_model.py
的方式支持训练和求值模式,但是您不能直接使用它来通过特定的绘画输入进行推理(对绘画进行分类)。 要使用特定输入来测试预测,请按照以下步骤操作:
- 复制
train_model.py
,然后将新文件重命名为predict.py
-这样您就可以更自由地进行预测了。 - 在
predict.py
中,定义[预测]的输入函数,并将features
设置为模型期望的绘画输入(连续点的增量,其中第三个数字表示该点是否为笔划的起点) :
def predict_input_fn(): def _input_fn(): features = {'shape': [[16, 3]], 'ink': [[ -0.23137257, 0.31067961, 0\. , -0.05490196, 0.1116505 , 0\. , 0.00784314, 0.09223297, 0\. , 0.19215687, 0.07766992, 0\. , ... 0.12156862, 0.05825245, 0\. , 0\. , -0.06310678, 1\. , 0\. , 0., 0\. , ... 0\. , 0., 0\. , ]]} features['shape'].append( features['shape'][0]) features['ink'].append( features['ink'][0]) features=dict(features) dataset = tf.data.Dataset.from_tensor_slices(features) dataset = dataset.batch(FLAGS.batch_size) return dataset.make_one_shot_iterator().get_next() return _input_fn
我们并没有显示所有的点值,但它们是使用 TensorFlow RNN 用于绘画分类的教程中显示的示例猫示例数据创建的,并应用了parse_line
函数(请参见教程或models/tutorials/rnn/quickdraw
文件夹中的create_dataset.py
细节)。
还要注意,我们使用tf.data.Dataset
的make_one_shot_iterator
方法创建了一个迭代器,该迭代器从数据集中返回一个示例(在这种情况下,我们在数据集中只有一个示例),与模型在处理大型数据集时,在训练和评估过程中获取数据的方式相同–这就是为什么稍后在模型的图中看到OneShotIterator
操作的原因。
- 在主函数中,调用估计器的
predict
方法,该方法将生成给定特征的预测,然后打印下一个预测:
predictions = estimator.predict(input_fn=predict_input_fn()) print(next(predictions)['argmax'])
- 在
model_fn
函数中,在logits = _add_fc_layers(final_state)
之后,添加以下代码:
argmax = tf.argmax(logits, axis=1) if mode == tf.estimator.ModeKeys.PREDICT: predictions = { 'argmax': argmax, 'softmax': tf.nn.softmax(logits), 'logits': logits, } return tf.estimator.EstimatorSpec(mode, predictions=predictions)
现在,如果您运行predict.py
,您将在步骤 2 中获得具有输入数据返回最大值的类 ID。
基本了解如何使用Estimator
高级 API 构建的模型进行预测后,我们现在就可以冻结该模型,以便可以在移动设备上使用该模型,这需要我们首先弄清楚输出节点名称应该是什么。
准备绘画分类模型
让我们使用 TensorBoard 看看我们能找到什么。 在我们模型的 TensorBoard 视图的 GRAPHS 部分中,您可以看到,如图 7.3 所示,以红色突出显示的BiasAdd
节点是ArgMax
操作的输入,用于计算精度,以及 softmax 操作的输入。 我们可以使用SparseSoftmaxCrossEntropyWithLogits
(图 7.3 仅显示为SparseSiftnaxCr ...
)操作,也可以仅使用Dense
/BiasAdd
作为输出节点名称,但我们将ArgMax
和Dense
/BiasAdd
用作freeze_graph
工具的两个输出节点名称,因此我们可以更轻松地查看最终密集层的输出以及ArgMax
结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XWqHPeoM-1681653119033)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/d911bab2-cfc2-41d3-a352-d00db55b1f71.png)]
图 7.3:显示模型的可能输出节点名称
用您的graph.pbtxt
文件的路径和最新的模型检查点前缀替换--input_graph
和--input_checkpoint
值后,在 TensorFlow 根目录中运行以下脚本以获取冻结的图:
python tensorflow/python/tools/freeze_graph.py --input_graph=/tmp/graph.pbtxt --input_checkpoint=/tmp/model.ckpt-314576 --output_graph=/tmp/quickdraw_frozen_dense_biasadd_argmax.pb --output_node_names="dense/BiasAdd,ArgMax"
您会看到quickdraw_frozen_dense_biasadd_argmax.pb
成功创建。 但是,如果您尝试在 iOS 或 Android 应用中加载模型,则会收到一条错误消息,内容为Could not create TensorFlow Graph: Not found: Op type not registered 'OneShotIterator' in binary. Make sure the Op and Kernel are registered in the binary running in this process.
我们在前面的小节中讨论了OneShotIterator
的含义。 回到 TensorBoard GRAPHS
部分,我们可以看到OneShotIterator
(如图 7.4 所示),该区域以红色突出显示,并且还显示在右下方的信息面板中,在图表的底部,以及上方的几个层次中,有一个 Reshape
操作用作第一卷积层的输入:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DKlJXEkX-1681653119033)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/a88ae370-8a39-4ee3-8ce0-ebaa5fd8a8ed.png)]
图 7.4:查找可能的输入节点名称
您可能想知道为什么我们不能使用我们之前使用的技术来解决Not found: Op type not registered 'OneShotIterator'
错误,即先使用命令grep 'REGISTER.*"OneShotIterator"' tensorflow/core/ops/*.cc
(您将看到输出为tensorflow/core/ops/dataset_ops.cc:REGISTER_OP("OneShotIterator")
),然后将tensorflow/core/ops/dataset_ops.cc
添加到tf_op_files.txt
并重建 TensorFlow 库。 即使这可行,也会使解决方案复杂化,因为现在我们需要向模型提供一些与OneShotIterator
相关的数据,而不是以点为单位的直接用户绘画。
此外,在右侧上方一层(图 7.5),还有另一种操作 Squeeze
,它是 rnn_classification
子图的输入:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BMOmquMH-1681653119033)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/6230229e-1c21-4280-b60a-cac640c1cb1f.png)]
图 7.5:找出输入节点名称的进一步研究
我们不必担心Reshape
右侧的Shape
运算,因为它实际上是rnn_classification
子图的输出。 因此,所有这些研究背后的直觉是,我们可以使用Reshape
和Squeeze
作为两个输入节点,然后使用在上一章中看到的transform_graph
工具,我们应该能够删除 Reshape
和Squeeze
以下的节点,包括OneShotIterator
。
现在在 TensorFlow 根目录中运行以下命令:
bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=/tmp/quickdraw_frozen_dense_biasadd_argmax.pb --out_graph=/tmp/quickdraw_frozen_strip_transformed.pb --inputs="Reshape,Squeeze" --outputs="dense/BiasAdd,ArgMax" --transforms=' strip_unused_nodes(name=Squeeze,type_for_name=int64,shape_for_name="8",name=Reshape,type_for_name=float,shape_for_name="8,16,3")'
在这里,我们为strip_unused_nodes
使用了更高级的格式:对于每个输入节点名称(Squeeze
和Reshape
),我们指定其特定的类型和形状,以避免以后出现模型加载错误。 有关transform_graph
工具的strip_unused_nodes
的更多详细信息,请参见其上的文档 https://github.com/tensorflow/tensorflow/tree/master/tensorflow/tools/graph_transforms 。
现在在 iOS 或 Android 中加载模型,OneShotIterator
错误将消失。 但是,您可能已经学会了预期,但是会出现一个新错误:Could not create TensorFlow Graph: Invalid argument: Input 0 of node IsVariableInitialized was passed int64 from global_step:0 incompatible with expected int64_ref.
我们首先需要了解有关IsVariableInitialized
的更多信息。 如果我们回到 TensorBoard GRAPHS
标签,我们会在左侧看到一个IsVariableInitialized
操作,该操作以红色突出显示并在右侧的信息面板中以global_step
作为其输入(图 7.6)。
即使我们不确切知道它的用途,我们也可以确保它与模型推断无关,该模型推断只需要一些输入(图 7.4 和图 7.5)并生成绘画分类作为输出(图 7.3)。 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eTUXwrz3-1681653119034)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/6cc9946c-dfb2-41de-af95-a35e47d405a7.png)]
图 7.6:查找导致模型加载错误但与模型推断无关的节点
那么,如何摆脱global_step
以及其他相关的cond
节点,由于它们的隔离性,它们不会被变换图工具剥离掉? 幸运的是,freeze_graph
脚本支持这一点 – 仅在其源代码中记录。 我们可以为脚本使用variable_names_blacklist
参数来指定应在冻结模型中删除的节点:
python tensorflow/python/tools/freeze_graph.py --input_graph=/tmp/graph.pbtxt --input_checkpoint=/tmp/model.ckpt-314576 --output_graph=/tmp/quickdraw_frozen_long_blacklist.pb --output_node_names="dense/BiasAdd,ArgMax" --variable_names_blacklist="IsVariableInitialized,global_step,global_step/Initializer/zeros,cond/pred_id,cond/read/Switch,cond/read,cond/Switch_1,cond/Merge"
在这里,我们只列出global_step
和cond
范围内的节点。 现在再次运行transform_graph
工具:
bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=/tmp/quickdraw_frozen_long_blacklist.pb --out_graph=/tmp/quickdraw_frozen_long_blacklist_strip_transformed.pb --inputs="Reshape,Squeeze" --outputs="dense/BiasAdd,ArgMax" --transforms=' strip_unused_nodes(name=Squeeze,type_for_name=int64,shape_for_name="8",name=Reshape,type_for_name=float,shape_for_name="8,16,3")'
在 iOS 或 Android 中加载生成的模型文件quickdraw_frozen_long_blacklist_strip_transformed.pb
,您将不再看到 IsVariableInitialized
错误。 当然,在 iOS 和 Android 上,您还会看到另一个错误。 加载先前的模型将导致此错误:
Couldn't load model: Invalid argument: No OpKernel was registered to support Op 'RefSwitch' with these attrs. Registered devices: [CPU], Registered kernels: device='GPU'; T in [DT_FLOAT] device='GPU'; T in [DT_INT32] device='GPU'; T in [DT_BOOL] device='GPU'; T in [DT_STRING] device='CPU'; T in [DT_INT32] device='CPU'; T in [DT_FLOAT] device='CPU'; T in [DT_BOOL] [[Node: cond/read/Switch = RefSwitch[T=DT_INT64, _class=["loc:@global_step"], _output_shapes=[[], []]](global_step, cond/pred_id)]]
要解决此错误,我们必须以不同的方式为 iOS 和 Android 构建自定义的 TensorFlow 库。 在下面的 iOS 和 Android 部分中讨论如何执行此操作之前,让我们首先做一件事:将模型转换为映射版本,以便在 iOS 中更快地加载并使用更少的内存:
bazel-bin/tensorflow/contrib/util/convert_graphdef_memmapped_format \ --in_graph=/tmp/quickdraw_frozen_long_blacklist_strip_transformed.pb \ --out_graph=/tmp/quickdraw_frozen_long_blacklist_strip_transformed_memmapped.pb
在 iOS 中使用绘画分类模型
要解决以前的 RefSwitch 错误,无论您是否像在第 2 章,“通过迁移学习对图像分类”和第 6 章,“用自然语言描述图像”或手动构建的 TensorFlow 库,就像在其他章节中一样,我们必须使用一些新技巧。 发生错误的原因是RefSwitch
操作需要INT64
数据类型,但它不是 TensorFlow 库中内置的已注册数据类型之一,因为默认情况下,要使该库尽可能小,仅包括每个操作的共同数据类型。 我们可能会从 Python 的模型构建端修复此问题,但是在这里,我们仅向您展示如何从 iOS 端修复此问题,当您无权访问源代码来构建模型时,这很有用。
为 iOS 构建自定义的 TensorFlow 库
从tensorflow/contrib/makefile/Makefile
打开 Makefile,然后,如果您使用 TensorFlow 1.4,则搜索IOS_ARCH
。 对于每种架构(总共 5 种:ARMV7,ARMV7S,ARM64,I386,X86_64),将-D__ANDROID_TYPES_SLIM__
更改为
-D__ANDROID_TYPES_FULL__
。 TensorFlow 1.5(或 1.6/1.7)中的Makefile
稍有不同,尽管它仍位于同一文件夹中。 对于 1.5/1.6/1.7,搜索ANDROID_TYPES_SLIM
并将其更改为 ANDROID_TYPES_FULL
。 现在,通过运行tensorflow/contrib/makefile/build_all_ios.sh
重建 TensorFlow 库。 此后,在加载模型文件时,RefSwitch
错误将消失。 使用 TensorFlow 库构建并具有完整数据类型支持的应用大小约为 70MB,而使用默认的细长数据类型构建的应用大小为 37MB。
好像还不够,仍然发生另一个模型加载错误:
Could not create TensorFlow Graph: Invalid argument: No OpKernel was registered to support Op 'RandomUniform' with these attrs. Registered devices: [CPU], Registered kernels: .
幸运的是,如果您已经阅读了前面的章节,那么您应该非常熟悉如何解决这种错误。 快速回顾一下:首先找出哪些操作和内核文件定义并实现了该操作,然后检查tf_op_files.txt
文件中是否包含操作或内核文件,并且应该至少缺少一个文件,从而导致错误 ; 现在只需将操作或内核文件添加到tf_op_files.txt
并重建库。 在我们的情况下,运行以下命令:
grep RandomUniform tensorflow/core/ops/*.cc grep RandomUniform tensorflow/core/kernels/*.cc
您将看到这些文件作为输出:
tensorflow/core/ops/random_grad.cc tensorflow/core/ops/random_ops.cc: tensorflow/core/kernels/random_op.cc
tensorflow/contrib/makefile/tf_op_files.txt
文件只有前两个文件,因此只需将最后一个tensorflow/core/kernels/random_op.cc
添加到 tf_op_files.txt
的末尾,然后再次运行tensorflow/contrib/makefile/build_all_ios.sh
。
最终,在加载模型时所有错误都消失了,我们可以通过实现应用逻辑来处理用户绘画,将点转换为模型期望的格式并返回分类结果,从而开始获得一些真正的乐趣。
开发 iOS 应用来使用模型
让我们使用 Objective-C 创建一个新的 Xcode 项目,然后从上一章中创建的Image2Text
iOS 项目中拖放tensorflow_util.h
和tensorflow_util.mm
文件。 另外,将两个模型文件quickdraw_frozen_long_blacklist_strip_transformed.pb
和quickdraw_frozen_long_blacklist_strip_transformed_memmapped.pb
以及training.tfrecord.classes
文件从 models/tutorials/rnn/quickdraw/rnn_tutorial_data
拖放到QuickDraw
项目,然后将training.tfrecord.classes
重命名为classes.txt
。
还将ViewController.m
重命名为ViewController.mm
,并在tensorflow_util.h
中注释GetTopN
函数定义,并在tensorflow_util.mm
中注释其实现,因为我们将在ViewController.mm
中实现修改后的版本。 您的项目现在应如图 7.7 所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySJd1grN-1681653119034)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-mobi-proj-tf/img/ee464972-a065-4400-aea9-72ce9306b901.png)]
图 7.7:显示带有ViewController
初始内容的QuickDraw
Xcode 项目。
我们现在准备单独处理ViewController.mm
,以完成我们的任务。
- 在按图 7.6 设置基本常量和变量以及两个函数原型之后,在
ViewController
的viewDidLoad
中实例化UIButton
,UILabel
和UIImageView
。 每个 UI 控件都设置有多个NSLayoutConstraint
(有关完整的代码列表,请参见源代码仓库)。UIImageView
的相关代码如下:
_iv = [[UIImageView alloc] init]; _iv.contentMode = UIViewContentModeScaleAspectFit; [_iv setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.view addSubview:_iv];
UIImageView
将用于显示通过UIBezierPath
实现的用户绘画。 同样,初始化两个用于保存每个连续点和用户绘制的所有点的数组:
_allPoints = [NSMutableArray array]; _consecutivePoints = [NSMutableArray array];
- 点击具有初始标题“开始”的按钮后,用户可以开始绘画; 按钮标题更改为“重新启动”,并进行了其他一些重置:
- (IBAction)btnTapped:(id)sender { _canDraw = YES; [_btn setTitle:@"Restart" forState:UIControlStateNormal]; [_lbl setText:@""]; _iv.image = [UIImage imageNamed:@""]; [_allPoints removeAllObjects]; }
- 为了处理用户绘画,我们首先实现
touchesBegan
方法:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (!_canDraw) return; [_consecutivePoints removeAllObjects]; UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self.view]; [_consecutivePoints addObject:[NSValue valueWithCGPoint:point]]; _iv.image = [self createDrawingImageInRect:_iv.frame]; }
然后是touchesMoved
方法:
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (!_canDraw) return; UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self.view]; [_consecutivePoints addObject:[NSValue valueWithCGPoint:point]]; _iv.image = [self createDrawingImageInRect:_iv.frame]; }
最后是touchesEnd
方法:
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (!_canDraw) return; UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self.view]; [_consecutivePoints addObject:[NSValue valueWithCGPoint:point]]; [_allPoints addObject:[NSArray arrayWithArray:_consecutivePoints]]; [_consecutivePoints removeAllObjects]; _iv.image = [self createDrawingImageInRect:_iv.frame]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ std::string classes = getDrawingClassification(_allPoints); dispatch_async(dispatch_get_main_queue(), ^{ NSString *c = [NSString stringWithCString:classes.c_str() encoding:[NSString defaultCStringEncoding]]; [_lbl setText:c]; }); }); }
这里的代码很容易解释,除了createDrawingImageInRect
和getDrawingClassification
这两种方法外,我们将在后面介绍。
- 方法
createDrawingImageInRect
使用UIBezierPath's
moveToPoint
和addLineToPoint
方法显示用户绘画。 它首先通过触摸事件准备所有完成的笔划,并将所有点存储在_allPoints
数组中:
- (UIImage *)createDrawingImageInRect:(CGRect)rect { UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), NO, 0.0); UIBezierPath *path = [UIBezierPath bezierPath]; for (NSArray *cp in _allPoints) { bool firstPoint = TRUE; for (NSValue *pointVal in cp) { CGPoint point = pointVal.CGPointValue; if (firstPoint) { [path moveToPoint:point]; firstPoint = FALSE; } else [path addLineToPoint:point]; } }
然后,它准备当前正在进行的笔划中的所有点,并存储在_consecutivePoints
中:
bool firstPoint = TRUE; for (NSValue *pointVal in _consecutivePoints) { CGPoint point = pointVal.CGPointValue; if (firstPoint) { [path moveToPoint:point]; firstPoint = FALSE; } else [path addLineToPoint:point]; }
最后,它执行实际绘画,并将绘画作为UIImage
返回,以显示在UIImageView
中:
path.lineWidth = 6.0; [[UIColor blackColor] setStroke]; [path stroke]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
getDrawingClassification
首先使用与上一章相同的代码来加载模型或其映射版本:
std::string getDrawingClassification(NSMutableArray *allPoints) { if (!_modelLoaded) { tensorflow::Status load_status; if (USEMEMMAPPED) { load_status = LoadMemoryMappedModel(MODEL_FILE_MEMMAPPED, MODEL_FILE_TYPE, &tf_session, &tf_memmapped_env); } else { load_status = LoadModel(MODEL_FILE, MODEL_FILE_TYPE, &tf_session); } if (!load_status.ok()) { LOG(FATAL) << "Couldn't load model: " << load_status; return ""; } _modelLoaded = YES; }
然后,它获得总点数并分配一个浮点数数组,然后调用另一个函数normalizeScreenCoordinates
(稍后将介绍)将点转换为模型期望的格式:
if ([allPoints count] == 0) return ""; int total_points = 0; for (NSArray *cp in allPoints) { total_points += cp.count; } float *normalized_points = new float[total_points * 3]; normalizeScreenCoordinates(allPoints, normalized_points);
ensorFlow 智能移动项目:6~10(3)https://developer.aliyun.com/article/1426910