Core ML框架詳細(xì)解析(十四) —— 使用Keras和Core ML開始機(jī)器學(xué)習(xí)(二)

版本記錄

版本號 時間
V1.0 2018.10.16 星期二

前言

目前世界上科技界的所有大佬一致認(rèn)為人工智能是下一代科技革命,蘋果作為科技界的巨頭,當(dāng)然也會緊跟新的科技革命的步伐,其中ios API 就新出了一個框架Core ML。ML是Machine Learning的縮寫,也就是機(jī)器學(xué)習(xí),這正是現(xiàn)在很火的一個技術(shù),它也是人工智能最核心的內(nèi)容。感興趣的可以看我寫的下面幾篇。
1. Core ML框架詳細(xì)解析(一) —— Core ML基本概覽
2. Core ML框架詳細(xì)解析(二) —— 獲取模型并集成到APP中
3. Core ML框架詳細(xì)解析(三) —— 利用Vision和Core ML對圖像進(jìn)行分類
4. Core ML框架詳細(xì)解析(四) —— 將訓(xùn)練模型轉(zhuǎn)化為Core ML
5. Core ML框架詳細(xì)解析(五) —— 一個Core ML簡單示例(一)
6. Core ML框架詳細(xì)解析(六) —— 一個Core ML簡單示例(二)
7. Core ML框架詳細(xì)解析(七) —— 減少Core ML應(yīng)用程序的大?。ㄒ唬?/a>
8. Core ML框架詳細(xì)解析(八) —— 在用戶設(shè)備上下載和編譯模型(一)
9. Core ML框架詳細(xì)解析(九) —— 用一系列輸入進(jìn)行預(yù)測(一)
10. Core ML框架詳細(xì)解析(十) —— 集成自定義圖層(一)
11. Core ML框架詳細(xì)解析(十一) —— 創(chuàng)建自定義圖層(一)
12. Core ML框架詳細(xì)解析(十二) —— 用scikit-learn開始機(jī)器學(xué)習(xí)(一)
13. Core ML框架詳細(xì)解析(十三) —— 使用Keras和Core ML開始機(jī)器學(xué)習(xí)(一)

Train the Model - 訓(xùn)練模型

1. Define Callbacks List - 定義回調(diào)列表

callbacksfit函數(shù)的可選參數(shù),因此首先定義callbacks_list。

輸入以下代碼,然后運(yùn)行它。

callbacks_list = [
    keras.callbacks.ModelCheckpoint(
        filepath='best_model.{epoch:02d}-{val_loss:.2f}.h5',
        monitor='val_loss', save_best_only=True),
    keras.callbacks.EarlyStopping(monitor='acc', patience=1)
]

一個epoch是完整傳遞數(shù)據(jù)集中的所有小批量。

ModelCheckpoint回調(diào)監(jiān)視驗證丟失值,使用文件編號和文件名中的驗證丟失將文件中的最低值保存。

EarlyStopping回調(diào)監(jiān)控訓(xùn)練準(zhǔn)確性:如果連續(xù)兩個epochs未能改善,則訓(xùn)練提前停止。在我的實(shí)驗中,這種情況從未發(fā)生過:如果acc在一個epoch內(nèi)逐漸消失,它總會在下一個時代恢復(fù)。

2. Compile & Fit Model - 編譯和擬合模型

除非您可以訪問GPU,否則我建議您使用Malireddimodel_m進(jìn)行此步驟,因為它的運(yùn)行速度比Chollet的model_c快得多:在我的MacBook Pro上,76-106s / epoch與246-309s / epoch相比,或者大約15分鐘vs 。 45分鐘。

注意:如果在第一個epoch完成后notebook中沒有出現(xiàn).h5文件,請單擊stop button以中斷內(nèi)核,單擊save button,然后注銷。在終端中,按Control-C停止服務(wù)器,然后重新運(yùn)行docker run命令。將URL或令牌粘貼到瀏覽器或登錄頁面,導(dǎo)航到notebook,然后單擊Not Trusted button按鈕。選擇此單元格,然后從菜單中選擇Cell \ Run All Above

輸入以下代碼,然后運(yùn)行它。這將花費(fèi)很長時間,所以在等待時閱讀Explanations部分。但是幾分鐘后檢查Finder,以確保notebook正在保存.h5文件。

注意:此單元格顯示多行函數(shù)調(diào)用的兩種縮進(jìn)類型,具體取決于您編寫第一個參數(shù)的位置。如果它甚至被一個空格輸出,那么這是一個語法錯誤。

model_m.compile(loss='categorical_crossentropy',
                optimizer='adam', metrics=['accuracy'])

# Hyper-parameters
batch_size = 200
epochs = 10

# Enable validation to use ModelCheckpoint and EarlyStopping callbacks.
model_m.fit(
    x_train, y_train, batch_size=batch_size, epochs=epochs,
    callbacks=callbacks_list, validation_data=(x_val, y_val), verbose=1)

Convolutional Neural Network: Explanations - 卷積神經(jīng)網(wǎng)絡(luò):解釋

您可以使用幾乎任何ML方法來創(chuàng)建MNIST分類器,但本教程使用卷積神經(jīng)網(wǎng)絡(luò)(CNN),因為這是TensorFlowKeras的關(guān)鍵優(yōu)勢。

卷積神經(jīng)網(wǎng)絡(luò)假設(shè)輸入是圖像,并在三個維度上排列神經(jīng)元:寬度,高度,深度。 CNN由卷積層組成,每個卷層檢測訓(xùn)練圖像的更高級特征:第一層可以訓(xùn)練濾波器以檢測各種角度的短線或弧線;第二層訓(xùn)練濾波器以檢測這些線的重要組合;最后一層的過濾器構(gòu)建在前面的圖層上以對圖像進(jìn)行分類。

每個卷積層在輸入上傳遞一個小方塊的kernel權(quán)重 - 1×1,3×35×5 ,計算內(nèi)核下輸入單元的加權(quán)和。 這是卷積過程。

每個神經(jīng)元僅連接到前一層中的1個,9個或25個神經(jīng)元,因此存在co-adapting的危險 - 過多地依賴于少數(shù)輸入 - 這可能導(dǎo)致過度擬合。 因此,CNN包括poolingdropout層,以抵消co-adapting和過度擬合。 我在下面解釋這些。

Sample Model - 樣本模型

這是Malireddi的模型:

model_m = Sequential()
model_m.add(Conv2D(32, (5, 5), input_shape=input_shape, activation='relu'))
model_m.add(MaxPooling2D(pool_size=(2, 2)))
model_m.add(Dropout(0.5))
model_m.add(Conv2D(64, (3, 3), activation='relu'))
model_m.add(MaxPooling2D(pool_size=(2, 2)))
model_m.add(Dropout(0.2))
model_m.add(Conv2D(128, (1, 1), activation='relu'))
model_m.add(MaxPooling2D(pool_size=(2, 2)))
model_m.add(Dropout(0.2))
model_m.add(Flatten())
model_m.add(Dense(128, activation='relu'))
model_m.add(Dense(num_classes, activation='softmax'))

1. Sequential

首先創(chuàng)建一個空的Sequential模型,然后添加一個線性的圖層堆棧:這些圖層按照它們添加到模型的順序運(yùn)行。 Keras文檔有幾個examples of Sequential models

注意:Keras還具有用于定義復(fù)雜模型的函數(shù)API,例如多輸出模型,有向非循環(huán)圖或具有共享層的模型。 Google的InceptionMicrosoft Research AsiaResidual Networks是具有非線性連接結(jié)構(gòu)的復(fù)雜模型的示例。

第一層必須具有關(guān)于輸入形狀的信息,對于MNIST(28,28,1)。 其他層從前一層的輸出形狀推斷出它們的輸入形狀。 這是模型摘要的輸出形狀部分:

Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_6 (Conv2D)            (None, 24, 24, 32)        832       
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 12, 12, 32)        0         
_________________________________________________________________
dropout_6 (Dropout)          (None, 12, 12, 32)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 10, 10, 64)        18496     
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
dropout_7 (Dropout)          (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 5, 5, 128)         8320      
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 2, 2, 128)         0         
_________________________________________________________________
dropout_8 (Dropout)          (None, 2, 2, 128)         0         
_________________________________________________________________
flatten_3 (Flatten)          (None, 512)               0         
_________________________________________________________________
dense_5 (Dense)              (None, 128)               65664     
_________________________________________________________________
dense_6 (Dense)              (None, 10)                1290      

2. Conv2D

該模型有三個Conv2D層:

Conv2D(32, (5, 5), input_shape=input_shape, activation='relu')
Conv2D(64, (3, 3), activation='relu')
Conv2D(128, (1, 1), activation='relu')
  • 第一個參數(shù) - 32,64,128 - 是您要訓(xùn)練此圖層檢測的過濾器或要素的數(shù)量。 這也是輸出形狀的深度 - 最后一個維度。
  • 第二個參數(shù) - (5,5),(3,3),(1,1) - 是內(nèi)核大?。阂粋€元組,指定在輸入空間上滑動的卷積窗口的寬度和高度,計算加權(quán)和 - dot 內(nèi)核權(quán)重和輸入單位值的乘積。
  • 第三個參數(shù)activation ='relu'指定ReLU(Rectified Linear Unit)(整流線性單元)激活功能。 當(dāng)內(nèi)核以輸入單元為中心時,如果加權(quán)和大于閾值,則稱該單元激活或觸發(fā):weighted_sum> threshold。 偏差值為-threshold:如果weighted_sum + bias> 0,則單位觸發(fā)。訓(xùn)練模型計算每個濾波器的內(nèi)核權(quán)重和偏差值。 ReLU是深度神經(jīng)網(wǎng)絡(luò)中最受歡迎的激活函數(shù)。

3. MaxPooling2D

MaxPooling2D(pool_size=(2, 2))

pooling層在前一層上通過m列過濾器滑動n行,將n x m值替換為其最大值。pooling濾器通常是方形的:n = m。 如下所示,最常用的2 x 2 pooling濾器將前一層的寬度和高度減半,從而減少了參數(shù)的數(shù)量,從而有助于控制過度擬合。

Malireddi的模型在每個卷積層之后都有一個pooling層,這大大減少了最終的模型大小和訓(xùn)練時間。

Chollet的模型在pooling之前有兩個卷積層。這建議用于較大的網(wǎng)絡(luò),因為它允許卷積層在pooling之前開發(fā)更復(fù)雜的特征,丟棄75%的值。

Conv2DMaxPooling2D參數(shù)確定每個圖層的輸出形狀和可訓(xùn)練參數(shù)的數(shù)量:

Output Shape = (input width – kernel width + 1, input height – kernel height + 1, number of filters)

您不能將3×3內(nèi)核置于每行和每列的第一個和最后一個單元的中心,因此輸出寬度和高度比輸入小2個像素。 5×5內(nèi)核可將輸出寬度和高度減少4個像素。

  • Conv2D(32,(5,5),input_shape =(28,28,1)):( 28-4,28-4,32)=(24,24,32)
  • MaxPooling2D將輸入寬度和高度減半:(24 / 2,24 / 2,32)=(12,12,32)
  • Conv2D(64,(3,3)):( 12-2,12-2,64)=(10,10,64)
  • MaxPooling2D將輸入寬度和高度減半:(10 / 2,10 / 2,64)=(5,5,64)
  • Conv2D(128,(1,1)):( 5-0,5-0,128)=(5,5,128)

Param # = number of filters x (kernel width x kernel height x input depth + 1 bias)

  • Conv2D(32,(5,5),input_shape =(28,28,1)):32 x(5x5x1 + 1)= 832
  • Conv2D(64,(3,3)):64 x(3x3x32 + 1)= 18,496
  • Conv2D(128,(1,1)):128 x(1x1x64 + 1)= 8320

Challenge:計算Chollet架構(gòu)model_c的輸出形狀和參數(shù)編號。

Output Shape = (input width – kernel width + 1, input height – kernel height + 1, number of filters)

  • Conv2D(32, (3, 3), input_shape=(28, 28, 1)): (28-2, 28-2, 32) = (26, 26, 32)
  • Conv2D(64, (3, 3)): (26-2, 26-2, 64) = (24, 24, 64)
  • MaxPooling2D halves the input width and height: (24/2, 24/2, 64) = (12, 12, 64)

Param # = number of filters x (kernel width x kernel height x input depth + 1 bias)

  • Conv2D(32, (3, 3), input_shape=(28, 28, 1)): 32 x (3x3x1 + 1) = 320
  • Conv2D(64, (3, 3)): 64 x (3x3x32 + 1) = 18,496

4. Dropout

Dropout(0.5)
Dropout(0.2)

dropout層通常與pooling層配對。 它將輸入單位的一小部分隨機(jī)設(shè)置為0。這是控制過度擬合的另一種方法:神經(jīng)元不太可能受到相鄰神經(jīng)元的過多影響,因為它們中的任何一個都可能隨機(jī)掉出網(wǎng)絡(luò)。 這使得網(wǎng)絡(luò)對輸入中的微小變化不太敏感,因此更有可能推廣到新輸入。

Hands-on Machine Learning with Scikit-Learn & TensorFlowAurélienGéron將其與工作場所進(jìn)行比較,在任何一天,某些人可能無法上班:每個人都必須能夠完成關(guān)鍵任務(wù), 并且必須與更多的同事合作。 這將使公司更具彈性,減少對任何單個工人的依賴。

5. Flatten

在將卷積層傳遞到完全連接的密集層之前,必須使卷積層的權(quán)重為1。

model_m.add(Dropout(0.2))
model_m.add(Flatten())
model_m.add(Dense(128, activation='relu'))

前一層的輸出形狀為(2,2,128),因此Flatten()的輸出是一個包含512個元素的數(shù)組。

6. Dense

Dense(128, activation='relu')
Dense(num_classes, activation='softmax')

卷積層中的每個神經(jīng)元使用前一層中僅少數(shù)神經(jīng)元的值。 完全連接層中的每個神經(jīng)元使用前一層中所有神經(jīng)元的值。 此類圖層的Keras名稱為Dense

看看上面的模型摘要,Malireddi的第一個Dense層有512個神經(jīng)元,而Chollet有9216個。兩者都產(chǎn)生128個神經(jīng)元輸出層,但Chollet必須計算的參數(shù)比Malireddi的多18倍。 這是使用大部分額外訓(xùn)練時間的原因。

大多數(shù)CNN架構(gòu)以一個或多個Dense層結(jié)束,然后是輸出層。

第一個參數(shù)是圖層的輸出大小。 最終輸出層的輸出大小為10,對應(yīng)于10個數(shù)字類。

softmax激活函數(shù)在10個輸出類別上產(chǎn)生概率分布。 它是sigmoid函數(shù)的推廣,它將其輸入值縮放到[0,1]范圍內(nèi)。 對于您的MNIST分類器,softmax將10個值中的每一個都縮放為[0,1],這樣它們總計為1。

您可以將sigmoid函數(shù)用于單個輸出類:例如,這是一張好狗照片的概率是多少?

7. Compile

model_m.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

分類交叉熵(categorical crossentropy)損失函數(shù)測量由CNN計算的概率分布與標(biāo)簽的真實(shí)分布之間的距離。

優(yōu)化器(optimizer)是隨機(jī)梯度下降算法,它試圖通過以恰當(dāng)?shù)乃俣雀S梯度來最小化損失函數(shù)。

準(zhǔn)確度(Accuracy) - 正確分類的圖像的分?jǐn)?shù) - 是在訓(xùn)練和測試期間監(jiān)控的最常見度量。

8. Fit

batch_size = 256
epochs = 10
model_m.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, callbacks=callbacks_list,
            validation_data=(x_val, y_val), verbose=1)

批量大小(Batch size)是用于小批量隨機(jī)梯度擬合的數(shù)據(jù)項的數(shù)量。選擇批量大小是一個試驗和錯誤的問題,一骰子。較小的值使得epoch需要更長的時間;較大的值可以更好地利用GPU并行性,并減少數(shù)據(jù)傳輸時間,但過大可能會導(dǎo)致內(nèi)存不足。

epoch的數(shù)量也是擲骰子。每個epoch都應(yīng)該改善損失和準(zhǔn)確度測量。更多epoch應(yīng)該產(chǎn)生更準(zhǔn)確的模型,但訓(xùn)練需要更長時間。太多的epoch可能導(dǎo)致過度擬合。如果模型在完成所有epoch之前停止改進(jìn),則設(shè)置回調(diào)以提前停止。在notebook中,您可以重新運(yùn)行fit的單元格以繼續(xù)改進(jìn)模型。

加載數(shù)據(jù)時,將10000個項目設(shè)置為驗證數(shù)據(jù)。通過此參數(shù)可以在訓(xùn)練時進(jìn)行驗證,因此您可以監(jiān)控驗證損失和準(zhǔn)確性。如果這些值比訓(xùn)練損失和準(zhǔn)確度差,則表明該模型過度擬合。

9. Verbose

0 = silent, 1 = progress bar, 2 = one line per epoch.

Results - 結(jié)果

以下是我的一次訓(xùn)練結(jié)果:

Epoch 1/10
60000/60000 [==============================] - 106s - loss: 0.0284 - acc: 0.9909 - val_loss: 0.0216 - val_acc: 0.9940
Epoch 2/10
60000/60000 [==============================] - 100s - loss: 0.0271 - acc: 0.9911 - val_loss: 0.0199 - val_acc: 0.9942
Epoch 3/10
60000/60000 [==============================] - 102s - loss: 0.0260 - acc: 0.9914 - val_loss: 0.0228 - val_acc: 0.9931
Epoch 4/10
60000/60000 [==============================] - 101s - loss: 0.0257 - acc: 0.9913 - val_loss: 0.0211 - val_acc: 0.9935
Epoch 5/10
60000/60000 [==============================] - 101s - loss: 0.0256 - acc: 0.9916 - val_loss: 0.0222 - val_acc: 0.9928
Epoch 6/10
60000/60000 [==============================] - 100s - loss: 0.0263 - acc: 0.9913 - val_loss: 0.0178 - val_acc: 0.9950
Epoch 7/10
60000/60000 [==============================] - 87s - loss: 0.0231 - acc: 0.9920 - val_loss: 0.0212 - val_acc: 0.9932
Epoch 8/10
60000/60000 [==============================] - 76s - loss: 0.0240 - acc: 0.9922 - val_loss: 0.0212 - val_acc: 0.9935
Epoch 9/10
60000/60000 [==============================] - 76s - loss: 0.0261 - acc: 0.9916 - val_loss: 0.0220 - val_acc: 0.9934
Epoch 10/10
60000/60000 [==============================] - 76s - loss: 0.0231 - acc: 0.9925 - val_loss: 0.0203 - val_acc: 0.9935

在每個epoch,損失值應(yīng)該減少,準(zhǔn)確度值應(yīng)該增加。 ModelCheckpoint回調(diào)保存了epoch1,2和6,因為epoch3,4和5中的驗證損失值高于epoch2,并且在epoch6之后驗證損失沒有改善。訓(xùn)練不會提前停止,因為訓(xùn)練準(zhǔn)確性從未在連續(xù)兩個epoch內(nèi)減少。

注意:實(shí)際上,這些結(jié)果來自20或30個epoch:我在不重置模型的情況下不止一次地運(yùn)行fit單元格,因此即使在第1epoch中,損失和準(zhǔn)確度值也已經(jīng)非常好。但是您在測量中看到一些波動。例如,在epoch4,6和9中精度降低。

到目前為止,您的模型已經(jīng)完成訓(xùn)練,所以回到編碼!


Convert to Core ML Model - 轉(zhuǎn)換為Core ML模型

訓(xùn)練步驟完成后,您應(yīng)該在notebook中保存一些模型。 具有最高epoch數(shù)(和最低驗證損失)的那個是最佳模型,因此在convert函數(shù)中使用該文件名。

輸入以下代碼,然后運(yùn)行它。

output_labels = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
# For the first argument, use the filename of the newest .h5 file in the notebook folder.
coreml_mnist = coremltools.converters.keras.convert(
    'best_model.09-0.03.h5', input_names=['image'], output_names=['output'], 
    class_labels=output_labels, image_input_names='image')

在這里,您在數(shù)組中設(shè)置10個輸出標(biāo)簽,并將其作為class_labels參數(shù)傳遞。 如果訓(xùn)練具有大量輸出類的模型,請將標(biāo)簽放在文本文件中,每行一個標(biāo)簽,并將class_labels參數(shù)設(shè)置為文件名。

在參數(shù)列表中,您提供輸入和輸出名稱,并設(shè)置image_input_names ='image',以便Core ML模型接受圖像作為輸入,而不是多數(shù)組。

1. Inspect Core ML model - 檢查Core ML模型

輸入此行,然后運(yùn)行它以查看打印輸出。

print(coreml_mnist)

只需檢查輸入類型是imageType,而不是多數(shù)組:

input {
  name: "image"
  shortDescription: "Digit image"
  type {
    imageType {
      width: 28
      height: 28
      colorSpace: GRAYSCALE
    }
  }
}

2. Add Metadata for Xcode - 為Xcode添加元數(shù)據(jù)

現(xiàn)在添加以下內(nèi)容,替換前兩個項目的自己的名稱和許可證信息,然后運(yùn)行它。

coreml_mnist.author = 'raywenderlich.com'
coreml_mnist.license = 'Razeware'
coreml_mnist.short_description = 'Image based digit recognition (MNIST)'
coreml_mnist.input_description['image'] = 'Digit image'
coreml_mnist.output_description['output'] = 'Probability of each digit'
coreml_mnist.output_description['classLabel'] = 'Labels of digits'

在Xcode的項目導(dǎo)航器中選擇模型時會出現(xiàn)此信息。

3. Save the Core ML Model - 保存Core ML模型

最后,添加以下內(nèi)容并運(yùn)行它。

coreml_mnist.save('MNISTClassifier.mlmodel')

這會將mlmodel文件保存在notebook文件夾中。

恭喜,您現(xiàn)在擁有一個Core ML模型,可以對手寫數(shù)字進(jìn)行分類! 是時候在iOS應(yīng)用程序中使用它了。


Use Model in iOS App - 在iOS App中使用Model

1. Step 1. Drag the model into the app - 步驟1.將模型拖到應(yīng)用程序中:

在Xcode中打開入門應(yīng)用程序,并將Finders中的MNISTClassifier.mlmodel拖到項目的Project導(dǎo)航器中。 選擇它以查看您添加的元數(shù)據(jù):

如果不是Automatically generated Swift model class,而是建立項目來生成模型類,請繼續(xù)執(zhí)行此操作。

2. Step 2. Import the CoreML and Vision frameworks: - 步驟2.導(dǎo)入CoreML和Vision框架:

打開ViewController.swift,導(dǎo)入兩個框架,就在導(dǎo)入UIKit下面:

import CoreML
import Vision

3. Step 3. Create VNCoreMLModel and VNCoreMLRequest objects: - 步驟3.創(chuàng)建VNCoreMLModel和VNCoreMLRequest對象:

outlets下面添加以下代碼:

lazy var classificationRequest: VNCoreMLRequest = {
  // Load the ML model through its generated class and create a Vision request for it.
  do {
    let model = try VNCoreMLModel(for: MNISTClassifier().model)
    return VNCoreMLRequest(model: model, completionHandler: handleClassification)
  } catch {
    fatalError("Can't load Vision ML model: \(error).")
  }
}()

func handleClassification(request: VNRequest, error: Error?) {
  guard let observations = request.results as? [VNClassificationObservation]
    else { fatalError("Unexpected result type from VNCoreMLRequest.") }
  guard let best = observations.first
    else { fatalError("Can't get best result.") }

  DispatchQueue.main.async {
    self.predictLabel.text = best.identifier
    self.predictLabel.isHidden = false
  }
}

請求對象適用于步驟4中的處理程序傳遞給它的任何圖像,因此您只需將其定義一次,作為一個lazy var。

請求對象的完成處理程序接收requesterror對象。 您檢查request.results是一個VNClassificationObservation對象的數(shù)組,這是當(dāng)Core ML模型是分類器而不是預(yù)測器或圖像處理器時Vision框架返回的對象。

VNClassificationObservation對象有兩個屬性:identifier - 一個String - 和confidence - 一個介于0和1之間的數(shù)字 - 分類正確的概率。 您獲取第一個結(jié)果,該結(jié)果具有最高置信度值,并調(diào)度回主隊列以更新predictLabel。 分類工作發(fā)生在主隊列之外,因為它可能很慢。

4. Step 4. Create and run a VNImageRequestHandler: - 步驟4.創(chuàng)建并運(yùn)行VNImageRequestHandler:

找到predictTapped(),并使用以下代碼替換print語句:

let ciImage = CIImage(cgImage: inputImage)
let handler = VNImageRequestHandler(ciImage: ciImage)
do {
  try handler.perform([classificationRequest])
} catch {
  print(error)
}

您可以從inputImage創(chuàng)建CIImage,然后為此ciImage創(chuàng)建VNImageRequestHandler對象,并在VNCoreMLRequest對象數(shù)組上運(yùn)行處理程序 - 在本例中,只是您在步驟3中創(chuàng)建的一個請求對象。

建立并運(yùn)行。 在繪圖區(qū)域的中心繪制一個數(shù)字,然后點(diǎn)擊Predict。 點(diǎn)按Clear再試一次。

較大的繪制往往效果更好,但模型常常遇到'7'和'4'的問題。 毫不奇怪,因為MNIST數(shù)據(jù)的PCA visualization顯示7s和4s聚集在9s:

注意:Malireddi表示Vision框架使用了20%的CPU,因此his app包含一個將UIImage對象轉(zhuǎn)換為CVPixelBuffer格式的擴(kuò)展。

如果您不使用Vision,請在將Keras模型轉(zhuǎn)換為Core ML時將image_scale = 1 / 255.0作為參數(shù):Keras模型訓(xùn)練灰度值在[0,1]范圍內(nèi)的圖像,CVPixelBuffer值為 在[0,255]范圍內(nèi)。

感謝 Sri Raghu M, Matthijs HollemansHon Weng Chong的有益討論!

資源

進(jìn)一步閱讀


源碼

1. Swift

看下工程文檔結(jié)構(gòu)

接著,看一下sb內(nèi)容

1. ViewController.swift
import UIKit
import CoreML
import Vision

class ViewController: UIViewController {

  @IBOutlet weak var drawView: DrawView!
  @IBOutlet weak var predictLabel: UILabel!

  // DONE: Define lazy var classificationRequest
  lazy var classificationRequest: VNCoreMLRequest = {
    // Load the ML model through its generated class and create a Vision request for it.
    do {
      let model = try VNCoreMLModel(for: MNISTClassifier().model)
      return VNCoreMLRequest(model: model, completionHandler: self.handleClassification)
    } catch {
      fatalError("Can't load Vision ML model: \(error).")
    }
  }()

  func handleClassification(request: VNRequest, error: Error?) {
    guard let observations = request.results as? [VNClassificationObservation]
      else { fatalError("Unexpected result type from VNCoreMLRequest.") }
    guard let best = observations.first
      else { fatalError("Can't get best result.") }

    DispatchQueue.main.async {
      self.predictLabel.text = best.identifier
      self.predictLabel.isHidden = false
    }
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    predictLabel.isHidden = true
  }

  @IBAction func clearTapped() {
    drawView.lines = []
    drawView.setNeedsDisplay()
    predictLabel.isHidden = true
  }

  @IBAction func predictTapped() {
    guard let context = drawView.getViewContext(),
      let inputImage = context.makeImage()
      else { fatalError("Get context or make image failed.") }
    // DONE: Perform request on model
    let ciImage = CIImage(cgImage: inputImage)
    let handler = VNImageRequestHandler(ciImage: ciImage)
    do {
      try handler.perform([classificationRequest])
    } catch {
      print(error)
    }
  }

}
2. DrawView.swift
// Code taken with inspiration from Apple's Metal-2 sample MPSCNNHelloWorld
import UIKit

/**
 This class is used to handle the drawing in the DigitView so we can get user input digit,
 This class doesn't really have an MPS or Metal going in it, it is just used to get user input
 */
class DrawView: UIView {
    
    // some parameters of how thick a line to draw 15 seems to work
    // and we have white drawings on black background just like MNIST needs its input
    var linewidth = CGFloat(15) { didSet { setNeedsDisplay() } }
    var color = UIColor.white { didSet { setNeedsDisplay() } }
    
    // we will keep touches made by user in view in these as a record so we can draw them.
    var lines: [Line] = []
    var lastPoint: CGPoint!
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        lastPoint = touches.first!.location(in: self)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let newPoint = touches.first!.location(in: self)
        // keep all lines drawn by user as touch in record so we can draw them in view
        lines.append(Line(start: lastPoint, end: newPoint))
        lastPoint = newPoint
        // make a draw call
        setNeedsDisplay()
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        let drawPath = UIBezierPath()
        drawPath.lineCapStyle = .round
        
        for line in lines{
            drawPath.move(to: line.start)
            drawPath.addLine(to: line.end)
        }
        
        drawPath.lineWidth = linewidth
        color.set()
        drawPath.stroke()
    }
    
    
    /**
     This function gets the pixel data of the view so we can put it in MTLTexture
     
     - Returns:
     Void
     */
    func getViewContext() -> CGContext? {
        // our network takes in only grayscale images as input
        let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceGray()
        
        // we have 3 channels no alpha value put in the network
        let bitmapInfo = CGImageAlphaInfo.none.rawValue
        
        // this is where our view pixel data will go in once we make the render call
        let context = CGContext(data: nil, width: 28, height: 28, bitsPerComponent: 8, bytesPerRow: 28, space: colorSpace, bitmapInfo: bitmapInfo)
        
        // scale and translate so we have the full digit and in MNIST standard size 28x28
        context!.translateBy(x: 0 , y: 28)
        context!.scaleBy(x: 28/self.frame.size.width, y: -28/self.frame.size.height)
        
        // put view pixel data in context
        self.layer.render(in: context!)
        
        return context
    }
}

/**
 2 points can give a line and this class is just for that purpose, it keeps a record of a line
 */
class Line{
    var start, end: CGPoint
    
    init(start: CGPoint, end: CGPoint) {
        self.start = start
        self.end   = end
    }
}

后記

本篇主要講述了使用Keras和Core ML開始機(jī)器學(xué)習(xí),感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容