1. 程式人生 > 其它 >【實踐操作】 在iOS11中使用Core ML 和TensorFlow對手勢進行智慧識別

【實踐操作】 在iOS11中使用Core ML 和TensorFlow對手勢進行智慧識別

在電腦科學中,手勢識別是通過數學演算法來識別人類手勢的一個議題。使用者可以使用簡單的手勢來控制或與裝置互動,讓計算機理解人類的行為。

這篇文章將帶領你實現在你自己的應用中使用深度學習來識別複雜的手勢,比如心形、複選標記或移動裝置上的笑臉。我還將介紹和使用蘋果的Core ML框架(iOS11中的新框架)。

在螢幕上隨便划動兩下,手機就會對複雜的手勢進行實時識別

這項技術使用機器學習來識別手勢。本文中的一些內容是特定於iOS系統的,但是Android開發者仍然可以找到一些有用的資訊。

完成專案的原始碼:https://github.com/mitochrome/complex-gestures-demo

我們將構建什麼?

在本教程結束時,我們將有一個設定,讓我們可以選擇完全自定義的手勢,並在iOS應用中非常準確地識別它們。

1.一個APP收集每個手勢的一些例子(畫一些複選標記或者心形,等等)。

2.一些Python指令碼用於訓練機器學習演算法(下面將會解釋),以識別手勢。我們將使用TensorFlow,稍後會講到。

3.這款APP可以使用自定義手勢。記錄使用者在螢幕上的動作,並使用機器學習演算法來找出它們所代表的手勢。

我們所畫的手勢將用於訓練機器學習演算法,我們將用Core ML來評估應用內(in-app)的演算法

什麼是機器學習演算法?

機器學習演算法從一組資料中學習,以便根據其他資料的不完整的資訊作出推斷。

在我們的例子中,資料是使用者及其相關的手勢類(“心形”、“複選標記”等)在螢幕上做出的划動。我們想要推斷的是,在我們不知道手勢類(不完整的資訊)的情況下,使用者所畫出的東西是什麼。

允許一種演算法從資料中學習,稱為“訓練”。對資料進行建模的推理機器被恰當地稱為“模型”。

什麼是Core ML?

機器學習模型可能是複雜的,(尤其是在移動裝置上)評估是非常緩慢的。在iOS 11中,蘋果引入了Core ML,這是一種新的框架,使其快速並易於實現。對於Core ML,實現一個模型主要是為了在Core ML模型格式(.mlmodel)中儲存它。

Core ML的詳細介紹,請參閱:https://developer.apple.com/documentation/coreml

使用官方的Python包coremltools,可以方便地儲存mlmodel檔案。它有針對Caffe、Keras、LIBSVM、scikit-learn和XCBoost模型的轉換器,以及當那些還沒有足夠能力(例如使用TensorFlow時)的低級別API。但要注意的是,coremltools目前需要Python的2.7版本。coremltools地址:https://pypi.python.org/pypi/coremltools

支援的格式可以通過使用coremltools自動轉換成Core ML模型。像TensorFlow這樣的不支援格式需要更多的手動操作來完成。

注意:Core ML只支援在裝置上評估模型,而不是訓練新模型。

1.生成資料集

首先,讓我們確保我們的機器學習演算法有一些資料(手勢)來學習。為了生成一個真實的資料集,我編寫了一個名為“GestureInput”的iOS應用,用於在裝置上輸入手勢。它允許你輸入大量的筆畫,然後預覽所生成的影象,並將其新增到資料集中。你還可以修改相關的類(稱為標籤)並且刪除示例。

當我想要改變它們顯示的頻率時(例如,當向現有的資料集新增一個新的類時),我將更改硬編碼的值並重新編譯。儘管看起來不是很漂亮,但很管用。

硬編碼的值:https://github.com/mitochrome/complex-gestures-demo/blob/ddaef7401cf3024c2df0a0af5883bbf2e7fac12a/apps/GestureInput/Source/InputViewController.swift#L8

為機器學習演算法生成資料

專案的自述檔案解釋瞭如何修改手勢類的集合,包括複選標記、x標記、“塗鴉”(在上下移動時快速的側向運動)、圓形、U形、心形、加號、問號、大寫A、大寫B、笑臉和悲傷的表情。還包括一個樣本資料集,你可以將它傳輸到你的裝置上。

樣本資料集:https://github.com/mitochrome/complex-gestures-demo/tree/ddaef7401cf3024c2df0a0af5883bbf2e7fac12a/sample_data

輸出訓練

GestureInput中的“Rasterize”按鈕將使用者畫的圖案轉換為影象,並將其儲存到一個名為data.trainingset的檔案中。這些影象就是我們要輸入的演算法。

縮放並翻譯使用者的手勢(“繪畫”)來適應一個固定大小的方框,然後將其轉換為灰度影象。這有助於讓我們的手勢獨立地識別使用者的手勢位置和大小。它還最小化了代表空白空間的影象畫素的數量。參考:https://hackernoon.com/a-new-approach-to-touch-based-mobile-interaction-ba47b14400b0

將使用者畫出的圖案轉換成一個灰度影象來輸入我們的機器學習演算法

請注意,我仍然在另一個檔案中儲存每次筆畫的觸控位置的原始時間序列。這樣,我就可以改變手勢在未來轉換成影象的方式,甚至可以使用非基於影象的方法來識別,而不用再畫出所有的手勢。手勢輸入在它的container文件資料夾中儲存資料集。從你的裝置上獲取資料的最簡單方法是通過Xcode下載container。

下載地址:https://stackoverflow.com/questions/6121613/browse-the-files-created-on-a-device-by-the-ios-application-im-developing-on-w/28161494#28161494

2. 訓練一個神經網路

目前,最先進的影象分類機器學習演算法是卷積神經網路(CNNs)。我們將用TensorFlow訓練一個CNNs,並在我們的APP中使用它。

我的神經網路是基於“Deep MNIST for Experts”的TensorFlow教程所使用的。

教程地址:https://www.tensorflow.org/get_started/mnist/pros

我用來訓練和匯出模型的一組指令碼在一個叫做“gesturelearner”的資料夾中。資料夾地址:https://github.com/mitochrome/complex-gestures-demo/tree/master/gesturelearner。

我將討論典型的用例,但是它們有一些額外的以virtualenv開頭的命令列選項可能是有用的:

cd /path/to/gesturelearner 

# Until coremltools supports Python 3, use Python 2.7. 

virtualenv -p $(which python2.7) venv 

pip install -r requirements.txt

準備資料集

首先,我使用filter.py將資料集分成15%的“測試集”和85%的“訓練集”。

# Activate the virtualenv. 

source /path/to/gesturelearner/venv/bin/activate 

# Split the data set. 

python /path/to/gesturelearner/filter.py --test-fraction=0.15 

data.trainingset

訓練集當然是用來訓練神經網路的。測試集的目的是為了說明神經網路的學習是如何對新資料進行歸納的。

我選擇把15%的資料放在測試集中,如果你只有幾百個手勢例子,那麼15%的數字將是一個相當小的數字。這意味著測試集的準確性只會讓你對演算法的表現有一個大致的瞭解。

訓練

在把我的自定義.trainingset格式變為TensorFlow喜歡的TFRecords格式之後,我使用train.py來訓練一個模型。我們給神經網路提供了有力的分類,它在未來會遇到新的手勢。

train.py列印出它的程序,然後定期儲存一個TensorFlow Checkpoint檔案,並在測試集上測試它的準確性(如果指定的話)。

# Convert the generated files to the TensorFlow TFRecords format. 

python /path/to/gesturelearner/convert_to_tfrecords.py 

data_filtered.trainingset 

python /path/to/gesturelearner/convert_to_tfrecords.py 

data_filtered_test.trainingset 

# Train the neural network. 

python /path/to/gesturelearner/train.py --test-

file=data_filtered_test.tfrecords data_filtered.tfrecords

訓練應該很快,在一分鐘內達到98%的準確率,在大約10分鐘後完成。

訓練神經網路

如果你在訓練中退出了train.py,你可以稍後重新啟動,它將載入checkpoint檔案以獲取它所處的位置,它還可以選擇從哪裡載入模型以及儲存它的位置。

用不平衡資料訓練

如果你的手勢比其他手勢有更多的例子,那麼網路就會傾向於學會以犧牲其他手勢為代價來識別更好的手勢。有幾種不同的方法來應對這個問題:

  • 神經網路是通過最小化與製造錯誤相關的成本函式來訓練的。為了避免忽略某些類,你可以增加錯誤分類的成本。
  • 包含一些較少代表性(less-represented)的手勢的副本,這樣你的所有手勢的數量都是相等的。
  • 刪除一些更有代表性(more-represented)的手勢的例子。

我的程式碼並不是開箱即用的,但是它們應該相對容易實現。

輸出到Core ML

Core ML沒有一個用於將TensorFlow模型轉換為Core ML的ML模型的“轉換器”。這就給我們提供了兩種把我們的神經網路轉換成一個ML模型的方法:

  • 使用一個用於構建神經網路的API的coremltools.模型包。模型包地址:https://pypi.python.org/pypi/coremltools API地址:https://apple.github.io/coremltools/generated/coremltools.models.neural_network.html
  • 由於MLModel說明是基於Google的protocol buffers,所以你可以跳過coremltools,然後直接在任何程式語言中使用protobuf。Google的protocol buffers地址:https://developers.google.com/protocol-buffers/

到目前為止,除了在現有的轉換器的內部程式碼之外,在web上似乎沒有找到任何方法的例子。下面是我使用coremltools的示例的精簡版:

1 from coremltools.modelsimport MLModel
2 from coremltools.models.neural_networkimport NeuralNetworkBuilder
3 import coremltools.models.datatypes as datatypes
4

5 # ...
6

7 def make_mlmodel(variables):
8     # Specify the inputs and outputs (there can be multiple).
9     # Each name corresponds to the input_name/output_name of a layer in the network so
10     # that Core ML knows where to insert and extract data.
11     input_features= [('image', datatypes.Array(1, IMAGE_HEIGHT, IMAGE_WIDTH))]
12     output_features= [('labelValues', datatypes.Array(NUM_LABEL_INDEXES))]
13     builder= NeuralNetworkBuilder(input_features, output_features, mode=None)
14

15     # The "name" parameter has no effect on the function of the network. As far as I know
16     # it's only used when Xcode fails to load your mlmodel and gives you an error telling
17     # you what the problem is.
18     # The input_names and output_name are used to link layers to each other and to the
19     # inputs and outputs of the model. When adding or removing layers, or renaming their
20     # outputs, always make sure you correct the input and output names of the layers
21     # before and after them.
22     builder.add_elementwise(name='add_layer',
23                             input_names=['image'], output_name='add_layer', mode='ADD',
24                             alpha=-0.5)
25

26     # Although Core ML internally uses weight matrices of shape
27     # (outputChannels, inputChannels, height, width) (as can be found by looking at the
28     # protobuf specification comments), add_convolution takes the shape
29     # (height, width, inputChannels, outputChannels) (as can be found in the coremltools
30     # documentation). The latter shape matches what TensorFlow uses so we don't need to
31     # reorder the matrix axes ourselves.
32     builder.add_convolution(name='conv2d_1', kernel_channels=1,
33                             output_channels=32, height=3, width=3, stride_height=1,
34                             stride_width=1, border_mode='same', groups=0,
35                             W=variables['W_conv1'].eval(), b=variables['b_conv1'].eval(),
36                             has_bias=True, is_deconv=False, output_shape=None,
37                             input_name='add_layer', output_name='conv2d_1')
38

39     builder.add_activation(name='relu_1', non_linearity='RELU', input_name='conv2d_1',
40                            output_name='relu_1', params=None)
41

42     builder.add_pooling(name='maxpool_1', height=2, width=2, stride_height=2,
43                         stride_width=2, layer_type='MAX', padding_type='SAME',
44                         input_name='relu_1', output_name='maxpool_1')
45

46     # ...
47

48     builder.add_flatten(name='maxpool_3_flat', mode=1, input_name='maxpool_3',
49                         output_name='maxpool_3_flat')
50

51     # We must swap the axes of the weight matrix because add_inner_product takes the shape
52     # (outputChannels, inputChannels) whereas TensorFlow uses
53     # (inputChannels, outputChannels). Unlike with add_convolution (see the comment
54     # above), the shape add_inner_product expects matches what the protobuf specification
55     # requires for inner products.
56     builder.add_inner_product(name='fc1',
57                               W=tf_fc_weights_order_to_mlmodel(variables['W_fc1'].eval())
58                                 .flatten(),
59                               b=variables['b_fc1'].eval().flatten(),
60                               input_channels=6*6*64, output_channels=1024, has_bias=True,
61                               input_name='maxpool_3_flat', output_name='fc1')
62

63     # ...
64

65     builder.add_softmax(name='softmax', input_name='fc2', output_name='labelValues')
66

67     model= MLModel(builder.spec)
68

69     model.short_description= 'Model for recognizing a variety of images drawn on screen with one's finger'
70

71     model.input_description['image']= 'A gesture image to classify'
72     model.output_description['labelValues']= 'The "probability" of each label, in a dense array'
73

74     return model

使用它:

# Save a Core ML .mlmodel file from the TensorFlow checkpoint 

model.ckpt. 

python /path/to/gesturelearner/save_mlmodel.py model.ckpt

完整的程式碼:https://github.com/mitochrome/complex-gestures-demo/blob/ddaef7401cf3024c2df0a0af5883bbf2e7fac12a/gesturelearner/gesturelearner/graph.py#L113

必須編寫這種轉換程式碼的一個副作用是,我們將整個網路描述為兩個位置(TensorFlow程式碼位置和轉換程式碼位置)。每當我們更改TensorFlow圖時,我們就必須同步轉換程式碼以確保我們的模型正確地匯出。

希望將來蘋果能開發出一種更好的輸出TensorFlow模型的方法。而在Android上,你可以使用官方的Tensorflow API。

此外,谷歌還將釋出一款名為TensorFlow Lite的移動優化版本的TensorFlow。

3.在應用內識別手勢

最後,讓我們把我們的模型放到一個面向使用者的APP中,這個專案的一部分是手勢識別(GestureRecognizer。專案地址:https://github.com/mitochrome/complex-gestures-demo/tree/ddaef7401cf3024c2df0a0af5883bbf2e7fac12a/apps

一旦你有了一個mlmodel檔案,就可以將它新增到Xcode中的一個目標。你將需要執行Xcode 9。

Xcode 9將編譯任何向目標新增的mlmodel檔案,併為它們生成Swift類。我將我的模型命名為GestureModel,因此Xcode生成了GestureModel, GestureModelInputGestureModelOutput這三個類

我們需要將使用者的手勢轉換成GestureModel接受的格式。這意味著要將這個手勢轉換成灰度影象,就像我們在步驟1中所做的那樣。然後,Core ML要求我們將灰度值陣列轉換為多維陣列型別,MLMultiArray。

MLMultiArray:https://developer.apple.com/documentation/coreml/mlmultiarray

1 /**
2  * Convert the `Drawing` into a binary image offormat suitablefor input to the
3  * GestureModel neural network.
4  *
5  * - returns: If successful, a validinput for GestureModel
6  */
7 func drawingToGestureModelFormat(_ drawing: Drawing)-> MLMultiArray? {
8     guard let image= drawing.rasterized(), let grays= imageToGrayscaleValues(image: image)else {
9         return nil
10     }
11     
12     guard let array= try? MLMultiArray(
13         shape: [
14             1,
15             NSNumber(integerLiteral:Int(image.size.width)),
16             NSNumber(integerLiteral:Int(image.size.height))
17         ],
18         dataType: .double
19         )else {
20             return nil
21     }
22     
23     let doubleArray= array.dataPointer.bindMemory(to: Float64.self, capacity: array.count)
24     
25     for iin 0 ..< array.count {
26         doubleArray.advanced(by: i).pointee= Float64(grays[i])/ 255.0
27     }
28     
29     return array
30 }

MLMultiArray就像一個圍繞一個原始陣列的包裝器(wrapper),它告訴了Core ML它包含什麼型別以及它的形狀(例如維度)是什麼。有了一個MLMultiArray,我們可以評估我們的神經網路。

1 /**
2  * Convert the `Drawing` into a grayscale imageand use a neural network to compute
3  * values ("probabilities")for each gesture label.
4  *
5  * - returns: An array that has at each index `i` the valuefor
6  * `Touches_Label.all[i]`.
7  */
8 func predictLabel(drawing: Drawing)-> [Double]? {
9     // Convert the user's gesture ("drawing") into a fixed-size grayscale image.
10     guard let array= drawingToGestureModelFormat(drawing)else {
11         return nil
12     }
13     
14     let model= GestureModel.shared
15     
16     // The GestureModel convenience method prediction(image:) wraps our imagein
17     // a GestureModelInput instance before passing that to prediction(input:).
18     // Both methodsreturn a GestureModelOutput with our outputin the
19     // labelValuesproperty. The names"image" and "labelValues" comefrom the
20     // names we gave to the inputsand outputs of the .mlmodel when we saved it.
21     guard let labelValues= try? model.prediction(image: array).labelValueselse {
22         return nil
23     }
24     
25     // Convert the MLMultiArray labelValues into a normal array.
26     let dataPointer= labelValues.dataPointer.bindMemory(to: Double.self, capacity: labelValues.count)
27     return Array(UnsafeBufferPointer(start: dataPointer, count: labelValues.count))
28 }

我使用了一個GestureModel的共享例項,因為每個例項似乎都要花費很長的時間來分配。事實上,即使在建立例項之後,這個模型第一次評估的速度也很慢。當應用程式啟動時,我用一個空白影象對網路進行評估,這樣使用者在開始做手勢時不會看到延遲。

避免手勢衝突

由於我使用的一些手勢類彼此包含(笑臉與U形嘴相包含,x標記與上升的對角相包含),所以當用戶想要繪製更復雜的圖形時,可能會貿然地識別出更簡單的手勢。

為了減少衝突,我使用了兩個簡單的規則:

  • 如果一個手勢能構成更復雜的手勢的一部分,那麼就可以暫時延遲它的識別,看看使用者是否能做出更大的手勢。
  • 考慮到使用者的筆畫數,一個還未被完全畫出的手勢(例如,一張笑臉需要至少畫三筆:一張嘴巴和兩隻眼睛)是不能被識別的。

結語

就是這樣!有了這個設定,你可以在大約20分鐘內給你的iOS應用新增一個全新的手勢(輸入100張圖片,訓練達到99.5+%的準確率,並且把模型匯出)。

要檢視這些片段是如何組合在一起的,或者在你自己的專案中使用它們的話,請參閱完整的原始碼:https://github.com/mitochrome/complex-gestures-demo