移植facenet pb模型到android

引子[编辑 | 编辑源代码]

  前面已经写了几篇wiki介绍facenet人脸分类,但是并没有写到将其移植到android上。这篇就是记录如何将facenet移植到android的。其中经历了约两个月的时间。并遇到问题停止不前。但还好些这篇wiki说明我们闯过了这个关。成功将facenet的tensorflow模型移植到了android上。

ckpt模型转pb模型[编辑 | 编辑源代码]

  当我们准备进行移植时,查阅资料了解到android只支持tensorflow的pb模型。而facenet官网公布的训练好的模型只有ckpt结尾的模型文件。于是我们就开始了自己转换ckpt模型到pb模型的尝试之路。并且坚信这是可以转换的。但其实在这里断断续续尝试,过了3周左右的时间都未能成功。最后发现facenet github上更新的20170512-110547模型,其中包含pb模型。
facenet释放的freeze_graph.py
并且facenet自己的代码中就已经有将ckpt模型转换成pb模型的文件freeze_graph.py,代码请参考https://github.com/davidsandberg/facenet/blob/master/src/freeze_graph.py ,而我之前并没有看到或者去研究过这个文件的意义。从而浪费了时间自己研究。可以直接运行转换下载的ckpt模型为pb模型。
或者直接下载20170512-110547模型,其中包含pb模型。

验证pb模型
修改代码将pb模型加载出来并在validate_on_lfw中使用。确认能正常运行。其实这一步也可以不做,大家看的时候可以略过。不知道为什么当时自己就是想确认pb模型跟ckpt模型一样能够加载进行预测测试
在validate_on_lfw.py文件main()函数中做如下的modify:

def main(args):
  
    with tf.Graph().as_default():
      
        with tf.Session() as sess:
            
            # Read the file containing the pairs used for testing
            pairs = lfw.read_pairs(os.path.expanduser(args.lfw_pairs)) 

            # Get the paths for the corresponding images
            paths, actual_issame = lfw.get_paths(os.path.expanduser(args.lfw_dir), pairs, args.lfw_file_ext)
- # Load the model
-            facenet.load_model(args.model)
-            
-            # Get input and output tensors
-            images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0")
-            embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0")
-            phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0")
 
+           # Load the model
+            graph_def = facenet.load_pb(args.model)
+            _ = tf.import_graph_def(graph_def, name=)   //将读出来的graph_def 图,import到当前这个session中。就是这句最关键,参考github issue才解决。
+
+            images_placeholder = sess.graph.get_tensor_by_name("input:0")           //再从这个session中取到张量的名字
+            embeddings = sess.graph.get_tensor_by_name("embeddings:0")
+            phase_train_placeholder = sess.graph.get_tensor_by_name("phase_train:0")
其中的load_pb()方法在facenet文件中添加,代码如下:
+
+ def load_pb(model):
+    # Check if the model is a model directory (containing a metagraph and a checkpoint file)
+    #  or if it is a protobuf file with a frozen graph
+    model_exp = os.path.expanduser(model)
+    if (os.path.isfile(model_exp)):
+       print('Model filename: %s' % model_exp)
+       with gfile.FastGFile(model_exp, 'rb') as f:
+          graph_def = tf.GraphDef()
+           graph_def.ParseFromString(f.read())
+           tf.import_graph_def(graph_def, name=)
+   else:
+       print('Model directory: %s' % model_exp)
+       pbfile = get_pb_filenames(model_exp)
+       pb_path = os.path.join(model_exp,pbfile)
+      print('pb path: %s' % pb_path)
+       with gfile.FastGFile(pb_path,'rb') as f:
+          graph_def = tf.GraphDef()
+          graph_def.ParseFromString(f.read())
+   return graph_def
+
+ def get_pb_filenames(model_dir):
+    files = os.listdir(model_dir)
+    pb_files = [s for s in files if s.endswith('.pb')]
+    if len(pb_files)==0:
+        raise ValueError('No pb file found in the model directory (%s)' % pb_files)
+    elif len(pb_files)>1:
+        raise ValueError('There should not be more than one meta file in the model directory (%s)' % pb_files)
+    pb_file = pb_files[0]
+    return pb_file
+

移植pb模型到android[编辑 | 编辑源代码]

一、移植所需文件准备[编辑 | 编辑源代码]


1) 将.pb文件放入项目中app/src/main/assets 
打开 Project view ,app/src/main/assets。 
若不存在assets目录,右键main->new->folder->Assets Folder 

2) 将本地下载的tensorflow 编译生成的android_tensorflow_inference_java.jar拷贝到app->libs下 
编译方法:bazel build //tensorflow/contrib/android:android_tensorflow_inference_java 

3) 打开 Project view,将libtensorflow_inference.so文件拷贝到 app/src/main/jniLibs/armeabi-v7a下(我用的android手机cpu属于armeabi-v7a)
编译方法:bazel build -c opt //tensorflow/contrib/android:libtensorflow_inference.so \ 
--crosstool_top=//external:android/crosstool \ 
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \ 
--cpu=armeabi-v7a 

4)我在AS编译的时候有遇到如下报错:
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tzutalin.dlibtest-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tzutalin.dlibtest-1/lib/arm64, /vendor/lib64, /system/lib64]]] couldn't find "libtensorflow_inference.so" 
解决方法是:
在build.gradle中的defaultConfig中增加如下配置:

       ndk {
           abiFilters "armeabi-v7a"
       }

二、添加java代码调用tensorflow模型[编辑 | 编辑源代码]


官方最新的方法就如下四步:
1)new一个TensorFlowInferenceInterface,传入pb模型路径。
inferenceInterface = new TensorFlowInferenceInterface(context.getAssets(), MODEL_FILE);
2) inferenceInterface.feed(INPUT_NODE, inputArray, inputsize, inputsize, inputsize, channel);
3) inferenceInterface.run(new String[]{OUTPUT_NODE}, false);
4) inferenceInterface.fetch(OUTPUT_NODE, outputs);

有很多网上的博客还用的旧的方法或者要在官方的classify demo TensorFlowImageClassifier文件中修改,其实都用不上。直接在你的代码中按照步骤来就可以。
这里关键的是要注意构建你的输入数组及参数。

三、人脸分类输入数组及参数构建[编辑 | 编辑源代码]

其实这里也花了点时间研究,因为从python中使用misc.imread()读取图片数据后得到的矩阵数组与android中调用bitmap.getPixel()再通过Color.red()、Color.green()等转换的数组不一致。
但最后决定不再纠结,先将android得到的数据传入进去调用pb模型看下结果再说。最后事实证明android侧数据与python输入数据不一致没有关系。
实现的将图片转换成像素数组的函数如下:

 1     private static final String MODEL_FILE = "file:///android_asset/face.pb";
 2     private static final String INPUT_NODE = "input";
 3     private static final String OUTPUT_NODE = "embeddings";
 4     private static final String TAG = "facenet";
 5     private TensorFlowInferenceInterface inferenceInterface; 
 6 
 7     static {
 8         System.loadLibrary("tensorflow_inference");
 9 
10     }
11  
12     public float[] bitmapToFloatArray(Bitmap bitmap) {
13         int height = bitmap.getHeight();
14         int width = bitmap.getWidth();
15         float[] result = new float[height * width * 3];
16         for (int j = 0; j < height; j++) {
17             for (int i = 0; i < width; i++) {
18                 int argb = bitmap.getPixel(i, j);
19                 int r = Color.red(argb);
20                 int g = Color.green(argb);
21                 int b = Color.blue(argb);
22                 result[j * height* 3 + 3 * i + 0] = r / 1.0f;
23                 result[j * height* 3 + 3 * i + 1] = g / 1.0f;
24                 result[j * height* 3 + 3 * i + 2] = b / 1.0f;
25             }
26         }
27 
28         return result;
29     }
30   public void getImageData(ArrayList<String> imagePath, Context context) {
31         if (!imagePath.isEmpty()) {
32             float[] iamgesPixels = new float[imagePath.size() * 160 * 160 * 3]; //这里160是输入图片的长,宽,3表示RGB三通道。输入的图片都是经过预处理后的人脸大小为160x160的图片
33             Log.d("facenet", " getImageData in not empty iamgesPixels length = " + iamgesPixels.length);
34             int imagesCount = imagePath.size();
35             inferenceInterface = new TensorFlowInferenceInterface(context.getAssets(), MODEL_FILE);
36             ArrayList<float[]> featureMat = new ArrayList<>();
37             for (int i = 0; i < imagesCount; i++) {
38                 Log.d("facenet", "imagePath = " + imagePath.get(i) + " i = " + i);
39                 Bitmap bitmap = BitmapFactory.decodeFile(imagePath.get(i)); //读取图片转换成bitmap
40                 float[] imageArray = bitmapToFloatArray(bitmap);   //将bitmap转换成像素数组
41                 float[] outputs = new float[128];
42                 inferenceInterface.feed(INPUT_NODE, imageArray, 1, 160, 160, 3); //第三个参数1表示1张图片
43                 inferenceInterface.run(new String[]{OUTPUT_NODE}, false);
44                 inferenceInterface.fetch(OUTPUT_NODE, outputs);
45                 featureMat.add(outputs);
46             }
47             }
48         }

四、移植报错[编辑 | 编辑源代码]

错误1:No Op'Switch'

No OpKernel was registered to support Op 'Switch' with these attrs. Registered devices: [CPU], Registered kernels:
device='GPU'; T in [DT_STRING]
device='GPU'; T in [DT_BOOL]
device='GPU'; T in [DT_INT32]
device='GPU'; T in [DT_FLOAT]
device='CPU'; T in [DT_FLOAT]
device='CPU'; T in [DT_INT32]
   [[Node: InceptionResnetV1/Conv2d_1a_3x3/BatchNorm/cond/Switch = Switch[T=DT_BOOL](phase_train, phase_train)]]
                                                                     at org.tensorflow.Session.run(Native Method)
                                                                     at org.tensorflow.Session.access$100(Session.java:48)
                                                                     at org.tensorflow.Session$Runner.runHelper(Session.java:295)
                                                                     at org.tensorflow.Session$Runner.run(Session.java:245)
                                                                     at org.tensorflow.contrib.android.TensorFlowInferenceInterface.run(TensorFlowInferenceInterface.java:142)

如第三节增加代码后发现移植报错,这里对于报错的解决也花费了很多时间,关键是没有多在网络查询,只是查了facenet github issue里没有人提这个问题,其实在别的pb移植案例也遇到了这个问题。
解决方法:
参考https://github.com/tensorflow/models/issues/1740介绍的方法

1)修改tensorflow/core/framework/register_types.h文件, 把所有的

#define TF_CALL_bool(m)

修改为

#define TF_CALL_bool(m) m(bool)

然后重新编译libtensorflow_inference.so文件:
bazel build -c opt //tensorflow/contrib/android:libtensorflow_inference.so \ 
--crosstool_top=//external:android/crosstool \ 
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \ 
--cpu=armeabi-v7a 
最后将新生产的so替换到AS项目中。至于为什么这么改,也没有搞明白,这可能是个workaround的方法

错误2: You must feed a value for placeholder tensor 'phase_train'
解决错误1后重新运行apk报下面的错误:

java.lang.IllegalArgumentException: You must feed a value for placeholder tensor 'phase_train' with dtype bool

[[Node: phase_train = Placeholder[dtype=DT_BOOL, shape=[], _device="/job:localhost/replica:0/task:0/cpu:0"]()]]

解决办法:
1) 在inferenceInterface.feed(INPUT_NODE, imageArray, 1, 160, 160, 3);后面增加一行

               inferenceInterface.feed("phase_train", new boolean[]{false});

2)TensorFlowInferenceInterface.java 增加支持feed bool的方法 
因为添加了上面的代码,android编译不过,因为feed函数没有参数为bool类型的, 所以还需要修改tensorflow/contrib/android/java/org/tensorflow/contrib/android/TensorFlowInferenceInterface.java文件,增加支持feed bool的方法:

1  
2   public void feed(String inputName, boolean[] src, long... dims) {
3      byte[] b = new byte[src.length];
4      for (int i = 0; i < src.length; ++i) {
5        b[i] = src[i] ? (byte)1 : (byte)0;
6      }
7      addFeed(inputName, Tensor.create(DataType.BOOL, dims, ByteBuffer.wrap(b)));
8    }

然后编译libandroid_tensorflow_inference_java.jar:
bazel build //tensorflow/contrib/android:android_tensorflow_inference_java
最后将新生产的jar替换到AS项目中。
错误3 No OpKernel was registered to support Op 'FIFOQueueV2'
解决上面两个问题运行仍然有如下报错:

No OpKernel was registered to support Op 'FIFOQueueV2' with these attrs.  Registered devices: [CPU], Registered kernels:
  
  [[Node: batch_join/fifo_queue = FIFOQueueV2[capacity=1440, component_types=[DT_FLOAT, DT_INT64], container="", shapes=[[160,160,3], []], shared_name=""]()]]

解决方法:
修改facenet/src/freeze_graph.py,
增加 将FIFOQueueV2 op修改为PaddingFIFOQueueV2的代码

def freeze_graph_def(sess, input_graph_def, output_node_names):
+        elif node.op == 'FIFOQueueV2':
+            node.op = 'PaddingFIFOQueueV2'

然后运行freeze_graph.py脚本生成新的pb文件:
python src\freeze_graph.py models\freeze_model face.pb
将新的pb模型替换到AS工程中。再次运行即能正常运行并得到输出为128长度的数组。
为什么要将FIFOQueueV2改为PaddingFIFOQueueV2,也是根据如下的tf_op_files.txt中有PaddingFIFOQueueV2类似的cc文件存在,所以这样尝试改的。
在contrib/makefile/tf_op_files.txt中有
tensorflow/core/kernels/padding_fifo_queue_op.cc 
tensorflow/core/kernels/padding_fifo_queue.cc

压缩pb模型[编辑 | 编辑源代码]

参考这篇文章讲了几种压缩方式TensorFlow Graph Transform Tool(GTT) 。我们就是尝试的其中的 quantize_weights,具体执行命令如下:
1,准备步骤:

./configure (其中很多不熟悉的如CUDA,openCL可以选择N)
bazel build tensorflow/tools/graph_transforms:transform_graph

2,具体执行quantize_weights:

ckt@ubuntu:/home/workdir/tensorflow/tensorflow$ bazel-bin/tensorflow/tools/graph_transforms/transform_graph  --in_graph=../facenet2/facenet/models/freezepb/face.pb --out_graph=../facenet2/facenet/models/freezepb/face_compress.pb --inputs='input' --
outputs='embeddings' --transforms='quantize_weights'

完成后pb模型从压缩前的93M减小到了25M。
压缩后的模型放入到apk中后,确实减少了apk的size,但是其准确率也受到了影响。同样适用阈值为0.28时,准确率约为75.1% 

07-27 15:04:10.251 10947-10981/com.tzutalin.dlibtest D/facenet: p_error ratio of error: 7.282767
07-27 15:04:10.251 10947-10981/com.tzutalin.dlibtest D/facenet: n_error ratio of error: 17.599535
07-27 15:04:10.251 10947-10981/com.tzutalin.dlibtest D/facenet: ratio of error: 24.882301

小结[编辑 | 编辑源代码]

本篇主要介绍了移植facenetpb模型到android的过程,以及记录在移植过程中遇到问题的解决方法。以供后续遇到类似问题提供思路。
代码没有提交到gerit,这里是对应代码的patch,文件:0001-Feature-port-the-facenet-db-feature.rar可以在智能相册的基础上,打上这个patch测试。
对于输入数据的传入,inputnode,outputnode的名称还需多读python代码。理解其训练及预测代码才能传入正确参数。
移植到android后,发现facent的准确率,在用threshhold为0.28时,其错误率为:

postive_error 10.20
negative_error 1.28
total_error 11.49

准确率即为:88.5% 
而参考另一篇wiki在PC上测试得到的准确率约为:91.3%。有一点差别,但可以确认facenet的pb移植在android是成功的。

你可能感兴趣的:(ML)