在iOS11中使用Core ML 和TensorFlow對手勢進行智能識別
在計算機科學中,手勢識別是通過數學算法來識別人類手勢的一個議題。用戶可以使用簡單的手勢來控製或與設備交互,讓計算機理解人類的行為。
這篇文章將帶領你實現在你自己的應用中使用深度學習來識別複雜的手勢,比如心形、複選標記或移動設備上的笑臉。我還將介紹和使用蘋果的Core ML框架(iOS11中的新框架)。
在屏幕上隨便劃動兩下,手機就會對複雜的手勢進行實時識別
這項技術使用機器學習來識別手勢。本文中的一些內容是特定於iOS係統的,但是Android開發者仍然可以找到一些有用的信息。
完成項目的源代碼:https://github.com/mitochrome/complex-gestures-demo
我們將構建什麼?
在本教程結束時,我們將有一個設置,讓我們可以選擇完全自定義的手勢,並在iOS應用中非常準確地識別它們。
一個APP收集每個手勢的一些例子(畫一些複選標記或者心形,等等)。
一些Python腳本用於訓練機器學習算法(下麵將會解釋),以識別手勢。我們將使用TensorFlow,稍後會講到。
-
這款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應用,用於在設備上輸入手勢。它允許你輸入大量的筆畫,然後預覽所生成的圖像,並將其添加到數據集中。你還可以修改相關的類(稱為標簽)並且刪除示例。
當我想要改變它們顯示的頻率時(例如,當向現有的數據集添加一個新的類時),我將更改硬編碼的值並重新編譯。盡管看起來不是很漂亮,但很管用。
為機器學習算法生成數據
項目的自述文件解釋了如何修改手勢類的集合,包括複選標記、x標記、“塗鴉”(在上下移動時快速的側向運動)、圓形、U形、心形、加號、問號、大寫A、大寫B、笑臉和悲傷的表情。還包括一個樣本數據集,你可以將它傳輸到你的設備上。
輸出訓練
GestureInput中的“Rasterize”按鈕將用戶畫的圖案轉換為圖像,並將其保存到一個名為data.trainingset的文件中。這些圖像就是我們要輸入的算法。
縮放並翻譯用戶的手勢(“繪畫”)來適應一個固定大小的方框,然後將其轉換為灰度圖像。這有助於讓我們的手勢獨立地識別用戶的手勢位置和大小。它還最小化了代表空白空間的圖像像素的數量。參考:https://hackernoon.com/a-new-approach-to-touch-based-mobile-interaction-ba47b14400b0
將用戶畫出的圖案轉換成一個灰度圖像來輸入我們的機器學習算法
請注意,我仍然在另一個文件中存儲每次筆畫的觸摸位置的原始時間序列。這樣,我就可以改變手勢在未來轉換成圖像的方式,甚至可以使用非基於圖像的方法來識別,而不用再畫出所有的手勢。手勢輸入在它的container文檔文件夾中保存數據集。從你的設備上獲取數據的最簡單方法是通過Xcode下載container。
2.訓練一個神經網絡
目前,最先進的圖像分類機器學習算法是卷積神經網絡(CNNs)。我們將用TensorFlow訓練一個CNNs,並在我們的APP中使用它。
我的神經網絡是基於“Deep MNIST for Experts”的TensorFlow教程所使用的。
我用來訓練和導出模型的一組腳本在一個叫做“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
到目前為止,除了在現有的轉換器的內部代碼之外,在web上似乎沒有找到任何方法的例子。下麵是我使用coremltools的示例的精簡版:
01
1 from coremltools.models import MLModel
02
2 from coremltools.models.neural_network import NeuralNetworkBuilder
03
3 import coremltools.models.datatypes as datatypes
04
4
05
06
5 # ...
07
6
08
09
7 def make_mlmodel(variables):
10
8 # Specify the inputs and outputs (there can be multiple).
11
9 # Each name corresponds to the input_name/output_name of a layer in the network so
12
10 # that Core ML knows where to insert and extract data.
13
11 input_features = [('image', datatypes.Array(1, IMAGE_HEIGHT, IMAGE_WIDTH))]
14
12 output_features = [('labelValues', datatypes.Array(NUM_LABEL_INDEXES))]
15
13 builder = NeuralNetworkBuilder(input_features, output_features, mode=None)
16
14
17
18
15 # The "name" parameter has no effect on the function of the network. As far as I know
19
16 # it's only used when Xcode fails to load your mlmodel and gives you an error telling
20
17 # you what the problem is.
21
18 # The input_names and output_name are used to link layers to each other and to the
22
19 # inputs and outputs of the model. When adding or removing layers, or renaming their
23
20 # outputs, always make sure you correct the input and output names of the layers
24
21 # before and after them.
25
22 builder.add_elementwise(name='add_layer',
26
23 input_names=['image'], output_name='add_layer', mode='ADD',
27
24 alpha=-0.5)
28
25
29
30
26 # Although Core ML internally uses weight matrices of shape
31
27 # (outputChannels, inputChannels, height, width) (as can be found by looking at the
32
28 # protobuf specification comments), add_convolution takes the shape
33
29 # (height, width, inputChannels, outputChannels) (as can be found in the coremltools
34
30 # documentation). The latter shape matches what TensorFlow uses so we don't need to
35
31 # reorder the matrix axes ourselves.
36
32 builder.add_convolution(name='conv2d_1', kernel_channels=1,
37
33 output_channels=32, height=3, width=3, stride_height=1,
38
34 stride_width=1, border_mode='same', groups=0,
39
35 W=variables['W_conv1'].eval(), b=variables['b_conv1'].eval(),
40
36 has_bias=True, is_deconv=False, output_shape=None,
41
37 input_name='add_layer', output_name='conv2d_1')
42
38
43
44
39 builder.add_activation(name='relu_1', non_linearity='RELU', input_name='conv2d_1',
45
40 output_name='relu_1', params=None)
46
41
47
48
42 builder.add_pooling(name='maxpool_1', height=2, width=2, stride_height=2,
49
43 stride_width=2, layer_type='MAX', padding_type='SAME',
50
44 input_name='relu_1', output_name='maxpool_1')
51
45
52
53
46 # ...
54
47
55
56
48 builder.add_flatten(name='maxpool_3_flat', mode=1, input_name='maxpool_3',
57
49 output_name='maxpool_3_flat')
58
50
59
60
51 # We must swap the axes of the weight matrix because add_inner_product takes the shape
61
52 # (outputChannels, inputChannels) whereas TensorFlow uses
62
53 # (inputChannels, outputChannels). Unlike with add_convolution (see the comment
63
54 # above), the shape add_inner_product expects matches what the protobuf specification
64
55 # requires for inner products.
65
56 builder.add_inner_product(name='fc1',
66
57 W=tf_fc_weights_order_to_mlmodel(variables['W_fc1'].eval())
67
58 .flatten(),
68
59 b=variables['b_fc1'].eval().flatten(),
69
60 input_channels=6*6*64, output_channels=1024, has_bias=True,
70
61 input_name='maxpool_3_flat', output_name='fc1')
71
62
72
73
63 # ...
74
64
75
76
65 builder.add_softmax(name='softmax', input_name='fc2', output_name='labelValues')
77
66
78
79
67 model = MLModel(builder.spec)
80
68
81
82
69 model.short_description = 'Model for recognizing a variety of images drawn on screen with one\'s finger'
83
70
84
85
71 model.input_description['image'] = 'A gesture image to classify'
86
72 model.output_description['labelValues'] = 'The "probability" of each label, in a dense array'
87
73
88
89
74 return model
使用它:
# Save a Core ML .mlmodel file from the TensorFlow checkpoint
model.ckpt.
python /path/to/gesturelearner/save_mlmodel.py model.ckpt
必須編寫這種轉換代碼的一個副作用是,我們將整個網絡描述為兩個位置(TensorFlow代碼位置和轉換代碼位置)。每當我們更改TensorFlow圖時,我們就必須同步轉換代碼以確保我們的模型正確地導出。
希望將來蘋果能開發出一種更好的輸出TensorFlow模型的方法。而在Android上,你可以使用官方的Tensorflow API。
此外,穀歌還將發布一款名為TensorFlow Lite的移動優化版本的TensorFlow。
3.在應用內識別手勢
最後,讓我們把我們的模型放到一個麵向用戶的APP中,這個項目的一部分是手勢識別(GestureRecognizer。
一旦你有了一個mlmodel文件,就可以將它添加到Xcode中的一個目標。你將需要運行Xcode 9。
Xcode 9將編譯任何向目標添加的mlmodel文件,並為它們生成Swift類。我將我的模型命名為GestureModel,因此Xcode生成了GestureModel, GestureModelInput和GestureModelOutput這三個類。
我們需要將用戶的手勢轉換成GestureModel接受的格式。這意味著要將這個手勢轉換成灰度圖像,就像我們在步驟1中所做的那樣。然後,Core ML要求我們將灰度值數組轉換為多維數組類型,MLMultiArray。
MLMultiArray:https://developer.apple.com/documentation/coreml/mlmultiarray
01
1 /**
02
2 * Convert the `Drawing` into a binary image of format suitable for input to the
03
3 * GestureModel neural network.
04
4 *
05
5 * - returns: If successful, a valid input for GestureModel
06
6 */
07
7 func drawingToGestureModelFormat(_ drawing: Drawing) -> MLMultiArray? {
08
8 guard let image = drawing.rasterized(), let grays = imageToGrayscaleValues(image: image) else {
09
9 return nil
10
10 }
11
11
12
12 guard let array = try? MLMultiArray(
13
13 shape: [
14
14 1,
15
15 NSNumber(integerLiteral: Int(image.size.width)),
16
16 NSNumber(integerLiteral: Int(image.size.height))
17
17 ],
18
18 dataType: .double
19
19 ) else {
20
20 return nil
21
21 }
22
22
23
23 let doubleArray = array.dataPointer.bindMemory(to: Float64.self, capacity: array.count)
24
24
25
25 for i in 0 ..< array.count {
26
26 doubleArray.advanced(by: i).pointee = Float64(grays[i]) / 255.0
27
27 }
28
28
29
29 return array
30
30 }
MLMultiArray就像一個圍繞一個原始數組的包裝器(wrapper),它告訴了Core ML它包含什麼類型以及它的形狀(例如維度)是什麼。有了一個MLMultiArray,我們可以評估我們的神經網絡。
01
1 /**
02
2 * Convert the `Drawing` into a grayscale image and use a neural network to compute
03
3 * values ("probabilities") for each gesture label.
04
4 *
05
5 * - returns: An array that has at each index `i` the value for
06
6 * `Touches_Label.all[i]`.
07
7 */
08
8 func predictLabel(drawing: Drawing) -> [Double]? {
09
9 // Convert the user's gesture ("drawing") into a fixed-size grayscale image.
10
10 guard let array = drawingToGestureModelFormat(drawing) else {
11
11 return nil
12
12 }
13
13
14
14 let model = GestureModel.shared
15
15
16
16 // The GestureModel convenience method prediction(image:) wraps our image in
17
17 // a GestureModelInput instance before passing that to prediction(input:).
18
18 // Both methods return a GestureModelOutput with our output in the
19
19 // labelValues property. The names "image" and "labelValues" come from the
20
20 // names we gave to the inputs and outputs of the .mlmodel when we saved it.
21
21 guard let labelValues = try? model.prediction(image: array).labelValues else {
22
22 return nil
23
23 }
24
24
25
25 // Convert the MLMultiArray labelValues into a normal array.
26
26 let dataPointer = labelValues.dataPointer.bindMemory(to: Double.self, capacity: labelValues.count)
27
27 return Array(UnsafeBufferPointer(start: dataPointer, count: labelValues.count))
28
28 }
我使用了一個GestureModel的共享實例,因為每個實例似乎都要花費很長的時間來分配。事實上,即使在創建實例之後,這個模型第一次評估的速度也很慢。當應用程序啟動時,我用一個空白圖像對網絡進行評估,這樣用戶在開始做手勢時不會看到延遲。
避免手勢衝突
由於我使用的一些手勢類彼此包含(笑臉與U形嘴相包含,x標記與上升的對角相包含),所以當用戶想要繪製更複雜的圖形時,可能會貿然地識別出更簡單的手勢。
為了減少衝突,我使用了兩個簡單的規則:
如果一個手勢能構成更複雜的手勢的一部分,那麼就可以暫時延遲它的識別,看看用戶是否能做出更大的手勢。
考慮到用戶的筆畫數,一個還未被完全畫出的手勢(例如,一張笑臉需要至少畫三筆:一張嘴巴和兩隻眼睛)是不能被識別的。
結語
就是這樣!有了這個設置,你可以在大約20分鍾內給你的iOS應用添加一個全新的手勢(輸入100張圖片,訓練達到99.5+%的準確率,並且把模型導出)。
要查看這些片段是如何組合在一起的,或者在你自己的項目中使用它們的話,請參閱完整的源代碼:https://github.com/mitochrome/complex-gestures-demo
本文為編譯作品,轉載請注明出處。更多內容關注公眾號:atyun_com
最後更新:2017-10-23 19:03:41