譯者注:這篇文章從如何在Keras中建立自定義層,講到如何建立督弓、訓(xùn)練Keras模型愚隧,如何轉(zhuǎn)換為Core ML模型,以及如何在app中使用自定義層睹耐,如何使用Accelerate加速代碼硝训,如何使用GPU加速代碼窖梁。內(nèi)容非常全面,學(xué)習(xí)Core ML自定義層不可錯(cuò)過(guò)的優(yōu)秀文章假哎,譯者筆力有限舵抹,英文水平過(guò)得去的可以看英文原文扇救。
蘋(píng)果新的Core ML框架使得在iOS app中添加機(jī)器學(xué)習(xí)模型變得很容易迅腔。但有一個(gè)很大的局限是Core ML只支持有限的神經(jīng)網(wǎng)絡(luò)層類(lèi)型。更糟糕的是掺出,作為應(yīng)用程序開(kāi)發(fā)人員汤锨,不可能擴(kuò)展Core ML的功能。
好消息:從iOS 11.2開(kāi)始柬泽,Core ML現(xiàn)在支持定制層锨并!在我看來(lái),這使Coew ML更加有用包警。
在本文中害晦,我將展示如何將具有自定義層的Keras模型轉(zhuǎn)換為Core ML苟呐。
步驟如下:
- 創(chuàng)建具有自定義層的Keras模型
- 使用coremltools將Keras轉(zhuǎn)換為mlmodel
- 為自定義層實(shí)現(xiàn)Swift類(lèi)
- 將Core ML模型放到iOS應(yīng)用程序中并運(yùn)行它
- 利潤(rùn)!
像往常一樣,您可以在GitHub上找到源代碼粱挡。運(yùn)行環(huán)境為Python 2询筏、TensorFlow、Keras踱讨、coremltools和Xcode 9痹筛。
注意:我選擇Keras作為這個(gè)博客帖子,因?yàn)樗子谑褂煤徒忉專(zhuān)鞘苟ㄖ茖右韵嗤姆绞焦ぷ髯淘纾还苣褂檬裁垂ぞ邅?lái)訓(xùn)練模型。
Swish!
讓我們實(shí)現(xiàn)一個(gè)名為Swish的激活函數(shù)(activation function)夕土,演示如何創(chuàng)建自定義層馆衔。
“等等…”,你可能會(huì)說(shuō)怨绣,“我以為這篇文章是關(guān)于自定義層的角溃,而不是定制激活函數(shù)篮撑?”哦减细,這要看你怎么看待事物。
您可以認(rèn)為激活函數(shù)是非線性的應(yīng)用于層的輸出赢笨,但是您也可以將激活函數(shù)視為它們自己的層未蝌。在許多深度學(xué)習(xí)軟件包中驮吱,包括Keras,激活功能實(shí)際上被看作獨(dú)立的層萧吠。
Core ML只支持一組固定的激活函數(shù)左冬,比如標(biāo)準(zhǔn)的ReLU和sigmoid激活。(完整的列表在NeuralNetwork.proto中纸型,它是mlmodel規(guī)范的一部分拇砰。)
但時(shí)常有人發(fā)明一種奇特的新激活函數(shù),如果你想在Core ML模型中使用它狰腌,那你只能編寫(xiě)自己的自定義層除破。這就是我們要做的。
我們將實(shí)現(xiàn)Swish激活函數(shù)琼腔。公式是:
swish(x) = x * sigmoid(beta * x)
其中sigmoid是著名的logistic sigmoid函數(shù)1/(1+exp(-x))瑰枫。因此,Swish的完整定義是:
swish(x) = x / (1 + exp(-beta * x))
這里丹莲,x是輸入值光坝,beta可以是常數(shù)或可訓(xùn)練的參數(shù)。不同的beta會(huì)改變Swish函數(shù)的曲線圾笨。
用beta=1.0進(jìn)行刷新看起來(lái)是這樣的:
是不是很像無(wú)處不在的ReLU激活函數(shù)教馆,不同的是Swish在左手邊是平滑的,而不是在x=0處進(jìn)行突然改變(這給Swish提供了一個(gè)不錯(cuò)的擂达、干凈的導(dǎo)數(shù))土铺。
beta值越大,Swish看起來(lái)越像ReLU板鬓。beta越接近0悲敷,Swish看起來(lái)越像直線。(如果你好奇俭令,試試看后德。)
顯然,這種Swish激活使您的神經(jīng)網(wǎng)絡(luò)比ReLU更容易學(xué)習(xí)抄腔,并且也給出了更好的結(jié)果瓢湃。您可以在“Searching for Activation Functions”一文中閱讀更多關(guān)于Swish的信息。
為了簡(jiǎn)化示例赫蛇,最初我們將使用beta=1绵患,但是稍后我們將使用beta作為一個(gè)可學(xué)習(xí)的參數(shù)。
Keras模型
撰寫(xiě)本文時(shí)悟耘,Swish還不夠流行落蝙,沒(méi)有進(jìn)入Keras。所以我們還要編寫(xiě)一個(gè)定制的Keras層。它很容易實(shí)現(xiàn):
from keras import backend as K
def swish(x):
return K.sigmoid(x) * x
這里筏勒,x是一個(gè)張量移迫,我們簡(jiǎn)單地把它和K.sigmoid函數(shù)的結(jié)果相乘。K是對(duì)Keras后端的引用管行,后者通常是TensorFlow〕瘢現(xiàn)在我將beta排除在代碼之外(這與beta=1相同)。
為了在Keras模型中使用這個(gè)自定義激活函數(shù)病瞳,我們可以編寫(xiě)以下代碼:
import keras
from keras.models import *
from keras.layers import *
def create_model():
inp = Input(shape=(256, 256, 3))
x = Conv2D(6, (3, 3), padding="same")(inp)
x = Lambda(swish)(x) # look here!
x = GlobalAveragePooling2D()(x)
x = Dense(10, activation="softmax")(x)
return Model(inp, x)
這只是一個(gè)帶有一些基本層類(lèi)型的簡(jiǎn)單模型揽咕。重要的部分是x=Lambda(swish)(x)。這在前一層的輸出上調(diào)用新的swish函數(shù)套菜,該層在本例中是卷積層。
Lambda層是一個(gè)特殊的Keras類(lèi)设易,它非常適合于只使用函數(shù)或lambda表達(dá)式(類(lèi)似于Swift中的閉包)編寫(xiě)快速但不完善的層逗柴。Lambda對(duì)于沒(méi)有狀態(tài)的層很有用,在Keras模型中通常用于進(jìn)行基本計(jì)算顿肺。
注意:您還可以通過(guò)創(chuàng)建Layer子類(lèi)在Keras中創(chuàng)建更高級(jí)的自定義層戏溺,稍后我們將看到一個(gè)示例。
激活呢屠尊?
如果您是Keras用戶(hù)旷祸,那么您可能習(xí)慣于為這樣的層指定激活函數(shù):
x = Conv2D(..., activation="swish")(x)
或者像這樣
x = Conv2D(6, (3, 3), padding="same")(inp)
x = Activation(swish)(x)
在Keras中我們通常使用Activation層,而不是使用Lambda作為激活函數(shù)讼昆。
不幸的是托享,0.7版的coremltools不能轉(zhuǎn)換自定義激活,只能轉(zhuǎn)換自定義層浸赫。如果試圖轉(zhuǎn)換使用Activation(...)闰围,而它不是Keras內(nèi)置激活函數(shù)之一,coremltools將給出錯(cuò)誤消息:
RuntimeError: Unsupported option activation=swish in layer Activation
解決方法是使用Lambda層替代Activation既峡。
特別指出來(lái)羡榴,因?yàn)檫@是一個(gè)稍微令人討厭的限制。我們可以使用自定義層來(lái)實(shí)現(xiàn)不支持的激活函數(shù)运敢,但是模型編碼中不能使用Activation(func)或activation="func"校仑。在使用coremltools Keras轉(zhuǎn)換器之前,必須先用Lambda層替換它們传惠。
注意:或者迄沫,您可以使用coremltools的NeuralNetworkBuilder類(lèi)從頭創(chuàng)建模型。這樣涉枫,您不受Keras轉(zhuǎn)換器理解的限制邢滑,但是也不太方便。
在我們將這個(gè)模型轉(zhuǎn)換為Core ML之前,應(yīng)該先給它一些權(quán)重困后。
“訓(xùn)練”模型
在這篇文章的源代碼中乐纸,我創(chuàng)建了Keras模型,它寫(xiě)在轉(zhuǎn)換腳本_lambda.py之中摇予。在實(shí)踐中汽绢,您可能有不同的用于訓(xùn)練和轉(zhuǎn)換的腳本,但是對(duì)于這個(gè)示例侧戴,我們不會(huì)煩惱訓(xùn)練爽雄。(不管怎么說(shuō),這是個(gè)粗糙的模型塑猖。)
首先品姓,我們使用您剛才看到的create_model()函數(shù)創(chuàng)建模型的實(shí)例:
model = create_model()
model.compile(loss="categorical_crossentropy", optimizer="Adam",
metrics=["accuracy"])
model.summary()
我們不訓(xùn)練模型,而是給它隨機(jī)加權(quán):
import numpy as np
W = model.get_weights()
np.random.seed(12345)
for i in range(len(W)):
W[i] = np.random.randn(*(W[i].shape)) * 2 - 1
model.set_weights(W)
通常訓(xùn)練過(guò)程會(huì)填補(bǔ)這些權(quán)重蜕猫,但是為了這個(gè)博客的目的寂曹,我們只是假裝。
為了獲得一些輸出回右,我們?cè)谳斎雸D像上測(cè)試模型:
這是一個(gè)256×256像素的RGB圖像隆圆。你可以使用任何你想要的圖像,但是我的貓自愿做這份工作翔烁。以下是加載圖像渺氧、將其加入神經(jīng)網(wǎng)絡(luò)并輸出結(jié)果的代碼:
from keras.preprocessing.image import load_img, img_to_array
img = load_img("floortje.png", target_size=(256, 256))
img = np.expand_dims(img_to_array(img), 0)
pred = model.predict(img)
print("Predicted output:")
print(pred)
預(yù)測(cè)輸出是:
[[ 2.24579312e-02 6.99496120e-02 7.55519234e-03 1.38940173e-03
5.51432837e-03 8.00364137e-01 1.42883752e-02 3.57461395e-04
5.40433871e-03 7.27192238e-02]]
這些數(shù)字沒(méi)有任何意義……畢竟,這只是一個(gè)非车乓伲基本的模型侣背,我們沒(méi)有對(duì)其進(jìn)行訓(xùn)練。沒(méi)關(guān)系哩治,在這個(gè)階段秃踩,我們只是想得到一些有關(guān)輸入圖像的輸出。
在將模型轉(zhuǎn)換為Core ML之后业筏,我們希望iOS應(yīng)用程序?yàn)橄嗤妮斎雸D像提供完全相同的輸出憔杨。如果做到了,可以證明轉(zhuǎn)換是正確的蒜胖,我們的自定義層可以正常工作消别。
注:有可能你的電腦會(huì)有不同的輸出。不用擔(dān)心台谢,只要每次運(yùn)行腳本時(shí)得到相同的數(shù)字就好寻狂。
轉(zhuǎn)換模型
現(xiàn)在讓我們將這個(gè)非常基本的模型轉(zhuǎn)換為Core ML mlmodel文件朋沮。如果一切順利蛇券,生成的mlmodel文件將不僅包含標(biāo)準(zhǔn)Keras層,而且還包含我們的自定義lambda層。然后纠亚,我們將編寫(xiě)這個(gè)層的Swift實(shí)現(xiàn)塘慕,以便可以在iOS上運(yùn)行模型。
注意:我使用coremltools 0.7版本進(jìn)行轉(zhuǎn)換蒂胞。隨著軟件的不斷改進(jìn)图呢,在您閱讀本文時(shí),它的行為可能會(huì)稍有不同骗随。有關(guān)使用和安裝說(shuō)明蛤织,請(qǐng)查看文檔。
將Keras模型轉(zhuǎn)換為Core ML非常簡(jiǎn)單鸿染,只需調(diào)用coremltools.converters.keras..():
import coremltools
coreml_model = coremltools.converters.keras.convert(
model,
input_names="image",
image_input_names="image",
output_names="output",
add_custom_layers=True,
custom_conversion_functions={ "Lambda": convert_lambda })
這引用了我們剛剛創(chuàng)建的模型指蚜,以及模型的輸入和輸出的名稱(chēng)。
對(duì)于我們的目的來(lái)說(shuō)特別重要的是add_custom_layers=True牡昆,它告訴轉(zhuǎn)換器檢測(cè)自定義層姚炕。但是轉(zhuǎn)換器還需要知道一旦找到這樣的層該做什么——這就是custom_conversion_functions的用途。
custom_conversion_functions參數(shù)接受一個(gè)字典丢烘,該字典將層類(lèi)型的名稱(chēng)映射為所謂的“轉(zhuǎn)換函數(shù)”。我們還需要編寫(xiě)這個(gè)函數(shù):
from coremltools.proto import NeuralNetwork_pb2
def convert_lambda(layer):
# Only convert this Lambda layer if it is for our swish function.
if layer.function == swish:
params = NeuralNetwork_pb2.CustomLayerParams()
# The name of the Swift or Obj-C class that implements this layer.
params.className = "Swish"
# The desciption is shown in Xcode's mlmodel viewer.
params.description = "A fancy new activation function"
return params
else:
return None
此函數(shù)接收Keras層對(duì)象些椒,并應(yīng)返回CustomLayerParams對(duì)象播瞳。CustomLayerParams對(duì)象告訴Core ML如何處理這個(gè)層。
CustomLayerParams在NeuralNetwork.proto中定義免糕。它具有以下字段:
- className
- description
- parameters
- weights
至少你應(yīng)該填寫(xiě)className字段赢乓。這是在iOS上實(shí)現(xiàn)這一層的Swift或Objective-C類(lèi)的名稱(chēng)。我選擇簡(jiǎn)單地將這個(gè)類(lèi)命名為Swish石窑。
如果不填寫(xiě)className牌芋,Xcode將顯示以下錯(cuò)誤,并且不能使用模型:
其他字段是可選的松逊。description顯示在Xcode的mlmodel查看器中躺屁,parameters是一個(gè)帶有附加定制選項(xiàng)的字典,weights包含層的學(xué)習(xí)參數(shù)(如果有的話)经宏。
現(xiàn)在我們有了轉(zhuǎn)換函數(shù)犀暑,我們可以使用coremltools.converters.keras.convert() 運(yùn)行Keras轉(zhuǎn)換器,它將為模型中遇到的任何Lambda層調(diào)用convert_lambda()烁兰。
注意:convert_lambda()函數(shù)將針對(duì)網(wǎng)絡(luò)中的每個(gè)Lambda層調(diào)用耐亏,因此如果具有具有不同函數(shù)的多個(gè)Lambda層,則需要在它們之間消除歧義沪斟。這就是為什么我們首先執(zhí)行l(wèi)ayer.function == swish的原因广辰。
轉(zhuǎn)換過(guò)程中的最后一步是填充模型的元數(shù)據(jù)并保存mlmodel文件:
coreml_model.author = "AuthorMcAuthorName"
coreml_model.license = "Public Domain"
coreml_model.short_description = "Playing with custom Core ML layers"
coreml_model.input_description["image"] = "Input image"
coreml_model.output_description["output"] = "The predictions"
coreml_model.save("NeuralMcNeuralNet.mlmodel")
當(dāng)您運(yùn)行轉(zhuǎn)換腳本時(shí),coremltools將打印出它所找到的所有層并轉(zhuǎn)換:
0 : input_1, <keras.engine.topology.InputLayer object at 0x1169995d0>
1 : conv2d_1, <keras.layers.convolutional.Conv2D object at 0x10a50ae10>
2 : lambda_1, <keras.layers.core.Lambda object at 0x1169b0650>
3 : global_average_pooling2d_1, <keras.layers.pooling.GlobalAveragePooling2D object at 0x1169d7110>
4 : dense_1, <keras.layers.core.Dense object at 0x116657f50>
5 : dense_1__activation__, <keras.layers.core.Activation object at 0x116b56350>
名為lambda_1的層是具有swish激活功能的層。轉(zhuǎn)換沒(méi)有給出任何錯(cuò)誤择吊,這意味著我們已經(jīng)準(zhǔn)備好將.mlmodel文件放入應(yīng)用程序中李根!
注意:您不是必須使用轉(zhuǎn)換函數(shù)。另一種填寫(xiě)自定義層詳細(xì)信息的方法是傳遞custom_conversion_functions={}干发。(省略它就會(huì)出錯(cuò)朱巨,但是空字典也可以。)然后調(diào)用coremltools.converters.keras.convert()枉长。這將在模型中包括您的自定義層冀续,但不會(huì)給它任何屬性。然后必峰,執(zhí)行以下操作:
layer = coreml_model._spec.neuralNetwork.layers[1]
layer.custom.className = "Swish"
這將獲取層并直接更改其屬性洪唐。無(wú)論哪種方式都可以,只要在保存mlmodel文件時(shí)已經(jīng)填充了className吼蚁。
將模型放入app
在應(yīng)用程序中添加Core ML模型非常簡(jiǎn)單:只需將mlmodel文件拖放到Xcode項(xiàng)目中即可凭需。
Xcode mlmodel查看器展示轉(zhuǎn)換后的模型如下所示:
它像往常一樣顯示輸入和輸出,并且在新的Dependencies部分列出自定義層以及哪些類(lèi)實(shí)現(xiàn)它們肝匆。
我已經(jīng)創(chuàng)建了一個(gè)演示應(yīng)用程序粒蜈,它使用Vision框架運(yùn)行模型,并與Python腳本使用的相同圖片旗国。它將預(yù)測(cè)數(shù)字打印到Xcode輸出窗格枯怖。回想一下能曾,這個(gè)模型實(shí)際上沒(méi)有計(jì)算任何有意義的內(nèi)容——因?yàn)槲覀儧](méi)有訓(xùn)練它——但是它應(yīng)該給出與Python相同的結(jié)果度硝。
在將mlmodel文件添加到應(yīng)用程序之后,您需要提供一個(gè)實(shí)現(xiàn)自定義層的Swift或Objective-C類(lèi)寿冕。如果沒(méi)有蕊程,那么一旦嘗試實(shí)例化MLModel對(duì)象,您將得到以下錯(cuò)誤:
[coreml] A Core ML custom neural network layer requires an implementation
named 'Swish' which was not found in the global namespace.
[coreml] Error creating Core ML custom layer implementation from factory
for layer "Swish".
[coreml] Error in adding network -1.
[coreml] MLModelAsset: load failed with error Error Domain=com.apple.CoreML
Code=0 "Error in declaring network."
Core ML試圖實(shí)例化一個(gè)名為Swish的類(lèi)驼唱,因?yàn)槲覀兏嬖V轉(zhuǎn)換腳本類(lèi)名是這個(gè)藻茂,但是它找不到這個(gè)類(lèi)。所以我們需要在Swish.swift中實(shí)現(xiàn)它:
import Foundation
import CoreML
import Accelerate
@objc(Swish) class Swish: NSObject, MLCustomLayer {
required init(parameters: [String : Any]) throws {
print(#function, parameters)
super.init()
}
func setWeightData(_ weights: [Data]) throws {
print(#function, weights)
}
func outputShapes(forInputShapes inputShapes: [[NSNumber]]) throws
-> [[NSNumber]] {
print(#function, inputShapes)
return inputShapes
}
func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
print(#function, inputs.count, outputs.count)
}
}
這是你需要做的最低限度的工作曙蒸。該類(lèi)需要擴(kuò)展NSObject捌治,使用@objc()修飾符使其對(duì)Objective-C運(yùn)行時(shí)可見(jiàn),并實(shí)現(xiàn)MLCustomLayer協(xié)議纽窟。該協(xié)議由四個(gè)必需的方法和一個(gè)可選的方法組成:
- init(parameters) 構(gòu)造函數(shù)肖油。參數(shù)是一個(gè)字典,它為該層提供了附加的配置選項(xiàng)(稍后將詳細(xì)介紹)臂港。
- setWeightData() 為具有可訓(xùn)練權(quán)重的層賦值(稍后將詳細(xì)介紹)森枪。
- outputShapes(forInputShapes) 這決定了層如何修改輸入數(shù)據(jù)的大小视搏。我們的Swish激活函數(shù)不會(huì)改變層的大小,因此我們只是返回輸入形狀县袱。
- evaluate(inputs, outputs) 執(zhí)行實(shí)際的計(jì)算-這是魔術(shù)發(fā)生的地方浑娜!此方法是必需的,當(dāng)模型在CPU上運(yùn)行時(shí)將調(diào)用此方法式散。
- encode(commandBuffer, inputs, outputs) 此方法是可選的筋遭。它也實(shí)現(xiàn)了在GPU上的計(jì)算。
所以有兩種不同的函數(shù)提供層的實(shí)現(xiàn):一個(gè)用于CPU暴拄,一個(gè)用于GPU漓滔。CPU方法是必需的——您必須始終至少提供層的CPU版本。GPU方法是可選的乖篷,但是推薦使用响驴。
目前,Swish類(lèi)沒(méi)有做任何事情撕蔼,但它足以在設(shè)備上(或在模擬器中)實(shí)際運(yùn)行模型豁鲤。給定256×256像素輸入圖像,Swish.swift打印中的打印語(yǔ)句輸出如下:
init(parameters:) ["engineName": Swish]
setWeightData []
outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
outputShapes(forInputShapes:) [[1, 1, 6, 256, 256]]
evaluate(inputs:outputs:) 1 1
顯然鲸沮,首先調(diào)用init(parameters)琳骡,它的參數(shù)字典包含一個(gè)項(xiàng)目“engineName”,其值是Swish讼溺。很快我將向您展示如何將自己的參數(shù)添加到這個(gè)字典中日熬。
其次調(diào)用setWeightData(),這將得到一個(gè)空數(shù)組肾胯。那是因?yàn)槲覀儧](méi)有在這個(gè)層中加入任何可學(xué)習(xí)的權(quán)重(稍后我們將討論)。
然后一行多次調(diào)用outputShapes(forInputShapes:)耘纱。我不確定為什么它被如此頻繁地調(diào)用敬肚,但是沒(méi)什么大不了的,因?yàn)闊o(wú)論如何我們沒(méi)有用那種方法做很多工作艳馒。
注意员寇,這些形狀是以五個(gè)維度給出的。這使用了以下約定:
[ sequence, batch, channel, height, width ]
我們的Swish層接收一個(gè)6個(gè)通道的256×256像素的圖像陆爽。(為什么有6個(gè)頻道扳缕?回想一下模型定義别威,這個(gè)Swish層應(yīng)用于Conv2D層的輸出省古,而卷積層有6個(gè)濾波器丧失。)
最后,調(diào)用evaluate(inputs, outputs)來(lái)執(zhí)行該層的計(jì)算琳拭。它接受一個(gè)MLMultiArray對(duì)象數(shù)組作為輸入炒事,并生成一個(gè)新MLMultiArray對(duì)象數(shù)組作為輸出(這些輸出對(duì)象已經(jīng)被分配臀栈,所以很方便——我們只需要填充它們)。
它獲得MLMultiArray對(duì)象數(shù)組的原因是某些類(lèi)型的層可以接受多個(gè)輸入或產(chǎn)生多個(gè)輸出挠乳。在上面的調(diào)試輸出中可以看到权薯,我們只得到了其中的一個(gè),因?yàn)槲覀兊哪P头浅:?jiǎn)單睡扬。
好的盟蚣,讓我們真正實(shí)現(xiàn)這個(gè)Swish激活函數(shù):
func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
for i in 0..<inputs.count {
let input = inputs[I]
let output = outputs[I]
assert(input.dataType == .float32)
assert(output.dataType == .float32)
assert(input.shape == output.shape)
for j in 0..<input.count {
let x = input[j].floatValue
let y = x / (1 + exp(-x)) // look familiar?
output[j] = NSNumber(value: y)
}
}
}
與大多數(shù)激活函數(shù)一樣,Swish是按元素進(jìn)行的操作卖怜,因此它循環(huán)遍歷輸入數(shù)組中的所有值屎开,計(jì)算x/(1+exp(-x))并將結(jié)果寫(xiě)入輸出數(shù)組。
重點(diǎn):MLMultiArray支持不同的數(shù)據(jù)類(lèi)型马靠。在這種情況下奄抽,我們假設(shè)數(shù)據(jù)類(lèi)型是.float32,即單精度浮點(diǎn)數(shù)甩鳄,這對(duì)我們的模型是正確的逞度。但是,MLMultiArray也可以支持int32和double妙啃,因此需要確保層類(lèi)能夠處理Core ML拋出的任何數(shù)據(jù)類(lèi)型档泽。(這里我使用了一個(gè)簡(jiǎn)單的斷言來(lái)使應(yīng)用程序崩潰,但是最好拋出一個(gè)錯(cuò)誤并讓Core ML進(jìn)行適當(dāng)?shù)那謇硪靖啊#?/p>
如果我們現(xiàn)在運(yùn)行應(yīng)用程序馆匿,預(yù)測(cè)的輸出是:
[0.02245793305337429, 0.06994961202144623, 0.007555192802101374,
0.00138940173201263, 0.005514328368008137, 0.8003641366958618,
0.01428837608546019, 0.0003574613947421312, 0.005404338706284761,
0.07271922379732132]
這與Keras輸出完全匹配!
那么現(xiàn)在我們完成了嗎燥滑?是的渐北,如果你不介意代碼變慢的話突倍。我們可以加快一點(diǎn)(實(shí)際上很多)。
使用Accelerate加速代碼
evaluate(inputs, outputs)函數(shù)是在CPU上執(zhí)行的淡喜,我們使用一個(gè)簡(jiǎn)單的for循環(huán)。這對(duì)于實(shí)現(xiàn)和調(diào)試層算法的第一個(gè)版本很有用瘟芝,但是它運(yùn)行速度不快锌俱。
更糟糕的是,當(dāng)我們以這種方式使用MLMultiArray時(shí)吭练,我們?cè)L問(wèn)的每個(gè)值都會(huì)得到NSNumber對(duì)象。直接訪問(wèn)MLMultiArray內(nèi)存中的浮點(diǎn)值要快得多分尸。
我們將使用向量化的CPU函數(shù)代替for循環(huán)寓落。幸運(yùn)的是史飞,Accelerate框架使此操作變得簡(jiǎn)單——但是我們必須使用指針抽诉,這使得代碼的可讀性稍微降低迹淌。
func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
for i in 0..<inputs.count {
let input = inputs[i]
let output = outputs[i]
let count = input.count
let iptr = UnsafeMutablePointer<Float>(OpaquePointer(input.dataPointer))
let optr = UnsafeMutablePointer<Float>(OpaquePointer(output.dataPointer))
// output = -input
vDSP_vneg(iptr, 1, optr, 1, vDSP_Length(count))
// output = exp(-input)
var countAsInt32 = Int32(count)
vvexpf(optr, optr, &countAsInt32)
// output = 1 + exp(-input)
var one: Float = 1
vDSP_vsadd(optr, 1, &one, optr, 1, vDSP_Length(count))
// output = x / (1 + exp(-input))
vvdivf(optr, iptr, optr, &countAsInt32)
}
}
對(duì)于for循環(huán)耙饰,我們將公式output=input/(1+exp(-input))應(yīng)用于每個(gè)數(shù)組值。但是在這里件已,我們將這個(gè)公式分成單獨(dú)的步驟,并且同時(shí)將每個(gè)步驟應(yīng)用于所有數(shù)組值鉴未。
首先歼狼,我們使用vDSP_vneg()一次性計(jì)算輸入數(shù)組中所有值的-input。中間結(jié)果被寫(xiě)入輸出數(shù)組梅屉。然后坯汤,使用vvexpf()一次性對(duì)數(shù)組中的每個(gè)值進(jìn)行指數(shù)化。我們使用vDSP_vsadd()對(duì)每個(gè)值添加1搓幌,最后執(zhí)行vvdivf()給出最終結(jié)果的除法。
結(jié)果和前面完全一樣拐揭,但是它是通過(guò)利用CPU的SIMD指令集以更有效的方式完成的堂污。如果您要編寫(xiě)自己的自定義層息楔,我建議您盡可能多地使用Accelerate框架(這也是Core ML內(nèi)部為其自己的層使用的)值依。
即使啟用了優(yōu)化,for循環(huán)版本在iPhone 7上也花費(fèi)了0.18秒辆亏。加速版本花費(fèi)了0.0012秒〕勾牛快150倍衷蜓!
您可以在repo中的CPU only文件夾中找到此代碼。您可以在設(shè)備上或在模擬器中運(yùn)行此應(yīng)用程序置吓。試試看交洗!
更快的速度:在GPU上運(yùn)行
與其他機(jī)器學(xué)習(xí)框架相比咆爽,使用Core ML的優(yōu)勢(shì)在于符糊,Core ML可以在CPU上或是在GPU上運(yùn)行模型,而不需要您做任何額外的工作建瘫。對(duì)于大型神經(jīng)網(wǎng)絡(luò)啰脚,它通常嘗試使用GPU,但是在沒(méi)有非常強(qiáng)大的GPU的較老設(shè)備上亮航,它將回到使用CPU准给。
事實(shí)證明圆存,Core ML也可以混合匹配。如果您的自定義層只有一個(gè)CPU實(shí)現(xiàn)(就像我們剛剛做的那樣)油讯,那么它仍然會(huì)在GPU上運(yùn)行其他層陌兑,切換到用于自定義層的CPU,然后切換回GPU用于神經(jīng)網(wǎng)絡(luò)的其余部分软驰。
因此锭亏,在自定義層中只使用CPU實(shí)現(xiàn)不會(huì)降低模型的其余部分的性能戴已。然而糖儡,為什么不充分利用GPU呢?
對(duì)于Swish激活功能拴疤,GPU實(shí)現(xiàn)非常簡(jiǎn)單呐矾。這是Metal shader代碼:
#include <metal_stdlib>
using namespace metal;
kernel void swish(
texture2d_array<half, access::read> inTexture [[texture(0)]],
texture2d_array<half, access::write> outTexture [[texture(1)]],
ushort3 gid [[thread_position_in_grid]])
{
if (gid.x >= outTexture.get_width() ||
gid.y >= outTexture.get_height()) {
return;
}
const float4 x = float4(inTexture.read(gid.xy, gid.z));
const float4 y = x / (1.0f + exp(-x)); // recognize this?
outTexture.write(half4(y), gid.xy, gid.z);
}
我們將對(duì)輸入數(shù)組中的每個(gè)數(shù)據(jù)元素調(diào)用這個(gè)計(jì)算內(nèi)核一次荞膘。因?yàn)镾wish是按元素進(jìn)行的操作羽资,所以我們可以在這里簡(jiǎn)單地編寫(xiě)熟悉的公式x/(1.0f+exp(-x))潮改。
與以前使用MLMultiArray不同汇在,這里的數(shù)據(jù)放在Metal紋理對(duì)象中亩鬼。MLMultiArray的數(shù)據(jù)類(lèi)型是32位浮點(diǎn)數(shù)辛孵,但這里我們實(shí)際處理的是16位浮點(diǎn)數(shù)或者h(yuǎn)alf。請(qǐng)注意,即使紋理類(lèi)型是half咆瘟,我們也要使用浮點(diǎn)值進(jìn)行實(shí)際計(jì)算袒餐,否則會(huì)損失太多的精度,而答案將是完全錯(cuò)誤的焰宣。
回想一下,數(shù)據(jù)有6個(gè)通道深闪唆。這就是為什么計(jì)算內(nèi)核使用texture_array,它是由多個(gè)“片”組成的Metal紋理笼吟。在我們的演示應(yīng)用程序中,紋理數(shù)組只包含2個(gè)切片(總共8個(gè)通道撵枢,所以最后兩個(gè)通道被忽略)潜必,但是上面的計(jì)算內(nèi)核將處理任意數(shù)量的切片/通道。
要使用這個(gè)GPU計(jì)算內(nèi)核垂攘,我們必須向Swift類(lèi)添加一些代碼:
@objc(Swish) class Swish: NSObject, MLCustomLayer {
let swishPipeline: MTLComputePipelineState
required init(parameters: [String : Any]) throws {
// Create the Metal compute kernels.
let device = MTLCreateSystemDefaultDevice()!
let library = device.makeDefaultLibrary()!
let swishFunction = library.makeFunction(name: "swish")!
swishPipeline = try! device.makeComputePipelineState(
function: swishFunction)
super.init()
}
這是將Metal swish內(nèi)核函數(shù)加載到MTLComputePipelineState對(duì)象中的樣式化代碼晒他。我們還需要添加以下方法:
func encode(commandBuffer: MTLCommandBuffer,
inputs: [MTLTexture], outputs: [MTLTexture]) throws {
if let encoder = commandBuffer.makeComputeCommandEncoder() {
for i in 0..<inputs.count {
encoder.setTexture(inputs[i], index: 0)
encoder.setTexture(outputs[i], index: 1)
encoder.dispatch(pipeline: swishPipeline, texture: inputs[I])
encoder.endEncoding()
}
}
}
如果MLCustomLayer類(lèi)中存在此方法灼伤,那么該層將在GPU上運(yùn)行饺蔑。在這個(gè)方法中隆敢,您將“compute pipeline state”編碼為MTLCommandBuffer穴墅。多半又是樣式化代碼。encoder.dispatch()方法確保對(duì)輸入紋理中的每個(gè)通道中的每個(gè)像素調(diào)用一次計(jì)算內(nèi)核温自。有關(guān)詳細(xì)信息玄货,請(qǐng)參閱源代碼。
現(xiàn)在悼泌,當(dāng)您運(yùn)行應(yīng)用程序(在一個(gè)相當(dāng)新的設(shè)備上)時(shí)松捉,encode(commandBuffer, inputs, outputs) 函數(shù)被調(diào)用,而不是evaluate(inputs, outputs)馆里,GPU有幸計(jì)算swish激活函數(shù)隘世。
您應(yīng)該得到與以前相同的輸出械媒。這很有意義——您希望自定義層的CPU和GPU版本計(jì)算完全相同的答案!
注意:您不能在模擬器上運(yùn)行Metal應(yīng)用程序臀玄,所以這個(gè)版本的應(yīng)用程序只能在真機(jī)上運(yùn)行。一部iPhone 6或者更好的就行了示损。如果設(shè)備太舊,Core ML仍將使用CPU而不是GPU運(yùn)行模型。
進(jìn)一步:如果您以前使用過(guò)MPSCNN葫盼,那么請(qǐng)注意闺金,Core ML使用GPU有一些不同。對(duì)于MPSCNN抬驴,您處理的是MPSImage對(duì)象,但是Core ML為您提供了MTLTexture推捐。像素格式似乎是.rgba16Float,這與MPSImage的.float16通道格式相對(duì)應(yīng)舌狗。
使用MPSCNN,具有4個(gè)通道或更少通道的圖像使用type2D紋理筷屡,超過(guò)4個(gè)通道的圖像使用type2DArray紋理。這意味著诡曙,對(duì)于MPSCNN,您可能必須編寫(xiě)兩個(gè)版本的計(jì)算內(nèi)核:一個(gè)采用texture對(duì)象,另一個(gè)采用texture_array對(duì)象。據(jù)我所知资铡,對(duì)于Core ML珊擂,紋理總是type2DArray,即使有4個(gè)通道或更少槽华,因此只需要編寫(xiě)一個(gè)版本的計(jì)算內(nèi)核傀蚌。
參數(shù)和權(quán)重
現(xiàn)在我們有了一個(gè)帶有相應(yīng)的Swift實(shí)現(xiàn)的自定義層。不錯(cuò)聊记,但這只是一個(gè)非常簡(jiǎn)單的層湿弦。
我們還可以向該層添加參數(shù)和權(quán)重∶笊冢“參數(shù)”在此上下文中表示可配置設(shè)置,例如卷積層的內(nèi)核大小和在該層周?chē)砑拥奶畛淞俊?/p>
在我們的例子中虏缸,我們可以將beta設(shè)置為一個(gè)參數(shù)。還記得beta嗎嫩实?beta的值決定了Swish函數(shù)有多陡峭刽辙。到目前為止,我們已經(jīng)實(shí)現(xiàn)的Swish版本是:
swish(x) = x * sigmoid(x)
但是記住完整的定義是這樣
swish(x) = x * sigmoid(beta * x)
beta是一個(gè)數(shù)字甲献。到目前為止宰缤,我們假設(shè)beta總是1.0,但是我們可以把它配置為一個(gè)參數(shù)竟纳,或者甚至讓模型在訓(xùn)練時(shí)學(xué)習(xí)beta的值撵溃,在這種情況下疚鲤,我們將它看作一個(gè)權(quán)重锥累。
要向自定義層添加參數(shù)或權(quán)重,請(qǐng)按以下方式更改轉(zhuǎn)換函數(shù):
def convert_lambda(layer):
if layer.function == swish:
params = NeuralNetwork_pb2.CustomLayerParams()
. . .
# Set configuration parameters
params.parameters["someNumber"].intValue = 100
params.parameters["someString"].stringValue = "Hello, world!"
# Add some random weights
my_weights = params.weights.add()
my_weights.floatValue.extend(np.random.randn(10).astype(float))
return params
else:
return None
現(xiàn)在集歇,當(dāng)您運(yùn)行該應(yīng)用程序時(shí)桶略,Swish類(lèi)將在init(parameters)方法中接收帶有這些整數(shù)和字符串值的參數(shù)字典,通過(guò)setWeightData()中的Data對(duì)象接收權(quán)重。
讓我們添加beta作為參數(shù)际歼。為此惶翻,我們應(yīng)該遠(yuǎn)離Lambda層,并將Swish激活函數(shù)轉(zhuǎn)換為適當(dāng)?shù)腒eras層對(duì)象鹅心。Lambda層非常適合于簡(jiǎn)單的計(jì)算吕粗,但是現(xiàn)在我們希望給Swish層一些狀態(tài)(beta的值),創(chuàng)建一個(gè)Layer子類(lèi)是更好的方法旭愧。
在Python腳本 convert_subclass.py中颅筋,我們現(xiàn)在將Swish函數(shù)定義為L(zhǎng)ayer的子類(lèi):
from keras.engine.topology import Layer
class Swish(Layer):
def __init__(self, beta=1., **kwargs):
super(Swish, self).__init__(**kwargs)
self.beta = beta
def build(self, input_shape):
super(Swish, self).build(input_shape)
def call(self, x):
return K.sigmoid(self.beta * x) * x
def compute_output_shape(self, input_shape):
return input_shape
注意,這如何在構(gòu)造函數(shù)中采用beta值输枯。Keras中的call()函數(shù)等價(jià)于swift中的evaluate(inputs, outputs)议泵。在call()函數(shù)中,我們計(jì)算Swish公式——這次包含beta桃熄。
新的模型定義如下所示:
def create_model():
inp = Input(shape=(256, 256, 3))
x = Conv2D(6, (3, 3), padding="same")(inp)
x = Swish(beta=0.01)(x) # look here!
x = GlobalAveragePooling2D()(x)
x = Dense(10, activation="softmax")(x)
return Model(inp, x)
beta的值是一個(gè)超參數(shù)先口,它是在模型構(gòu)建時(shí)定義的。這里我選擇使用beta=0.01瞳收,這樣我們就會(huì)得到與以前不同的預(yù)測(cè)碉京。
順便說(shuō)一下,這里是Swish在beta 0.01中的樣子缎讼,它幾乎是一條直線:
為了將這個(gè)層轉(zhuǎn)換為Core ML收夸,我們需要為它建一個(gè)轉(zhuǎn)換函數(shù):
def convert_swish(layer):
params = NeuralNetwork_pb2.CustomLayerParams()
params.className = "Swish"
params.description = "A fancy new activation function"
# Add the hyperparameter to the dictionary
params.parameters["beta"].doubleValue = layer.beta
return params
這與以前非常相似,只是現(xiàn)在我們從層(這是我們剛剛創(chuàng)建的新Swish類(lèi)的實(shí)例)讀取beta屬性血崭,并將其粘貼到CustomLayerParams的參數(shù)字典中卧惜。注意,這個(gè)字典不支持32位浮點(diǎn)夹纫,只支持64位雙精度浮點(diǎn)(以及整數(shù)和布爾值)咽瓷,所以我們使用.doubleValue。
當(dāng)我們調(diào)用Keras轉(zhuǎn)換器時(shí)舰讹,我們必須告訴它這個(gè)新的轉(zhuǎn)換函數(shù):
coreml_model = coremltools.converters.keras.convert(
model,
input_names="image",
image_input_names="image",
output_names="output",
add_custom_layers=True,
custom_conversion_functions={ "Swish": convert_swish })
這一切都非常類(lèi)似于我們之前所做的茅姜,除了現(xiàn)在Swish不是包裝在Lambda對(duì)象中的基本Python函數(shù),而是從Keras Layer基類(lèi)派生的一個(gè)成熟的類(lèi)月匣。
在iOS方面钻洒,我們需要調(diào)整Swish.swift以從參數(shù)字典中讀出這個(gè)“beta”值并將其應(yīng)用于計(jì)算。
@objc(Swish) class Swish: NSObject, MLCustomLayer {
let beta: Float
required init(parameters: [String : Any]) throws {
if let beta = parameters["beta"] as? Float {
self.beta = beta
} else {
self.beta = 1
}
...
}
在evaluate(inputs, outputs) 時(shí)锄开,我們現(xiàn)在用self.beta乘以輸入素标。
同樣,對(duì)于Metal compute shader,在encode(commandBuffer, inputs, outputs)中萍悴,我們可以將self.beta傳遞到計(jì)算內(nèi)核中头遭,如下所示:
var beta = self.beta
encoder.setBytes(&beta, length: MemoryLayout<Float>.size, index: 0)
然后在Metak內(nèi)核中:
kernel void swish(
texture2d_array<half, access::read> inTexture [[texture(0)]],
texture2d_array<half, access::write> outTexture [[texture(1)]],
constant float& beta [[buffer(0)]],
ushort3 gid [[thread_position_in_grid]])
{
...
const float4 y = x / (1.0f + exp(-x * beta));
...
}
請(qǐng)參閱源代碼以獲得完整的更改寓免。我希望解釋的足夠清楚,使您能很容易的配置參數(shù)添加到定制層中计维。
注意:當(dāng)我運(yùn)行這個(gè)新版本的iOS應(yīng)用程序時(shí)袜香,預(yù)測(cè)結(jié)果與Keras的結(jié)果并不100%匹配。當(dāng)Core ML使用GPU時(shí)鲫惶,這種不匹配的情況很常見(jiàn)蜈首。卷積層在GPU上運(yùn)行,帶有16位浮點(diǎn)數(shù)欠母,這降低了精度疾就,而Keras對(duì)一切都使用32位浮點(diǎn)數(shù)。所以您一定會(huì)看到來(lái)自iOS模型和來(lái)自原始Keras模型的預(yù)測(cè)之間的差異艺蝴。只要差別很锈(大約1e-3或更小)就可以接受猜敢。
可學(xué)習(xí)權(quán)重
最后一件事我想告訴你姑荷。機(jī)器學(xué)習(xí)的全部意義在于學(xué)習(xí)東西,所以對(duì)于許多定制層缩擂,您希望能夠賦予它們可學(xué)習(xí)的權(quán)重鼠冕。因此,讓我們?cè)僖淮胃淖僑wish層的實(shí)現(xiàn)以使beta可以學(xué)習(xí)胯盯。這讓模型學(xué)習(xí)激活函數(shù)的最佳形狀是什么懈费。
Swish層仍然是Layer的一個(gè)子類(lèi),但是這次我們通過(guò)add_weight()函數(shù)來(lái)賦予它一個(gè)可學(xué)習(xí)權(quán)重:
class LearnableSwish(Layer):
def __init__(self, **kwargs):
super(LearnableSwish, self).__init__(**kwargs)
def build(self, input_shape):
self.beta = self.add_weight(
name="beta",
shape=(input_shape[3], ),
initializer=keras.initializers.Constant(value=1),
trainable=True)
super(LearnableSwish, self).build(input_shape)
def call(self, x):
return K.sigmoid(self.beta * x) * x
def compute_output_shape(self, input_shape):
return input_shape
我們將為輸入數(shù)據(jù)中的每個(gè)通道創(chuàng)建可學(xué)習(xí)的權(quán)重博脑,而不是單個(gè)beta值憎乙,這就是為什么我們使用shape=(input_shape[3], )。在該示例中叉趣,因?yàn)閬?lái)自前一個(gè)Conv2D層的輸出有6個(gè)通道泞边,所以該層將學(xué)習(xí)6個(gè)不同的beta值。beta的初始值為1疗杉,這似乎是一個(gè)合理的缺省值阵谚。
現(xiàn)在,當(dāng)您調(diào)用model.fit(...)來(lái)訓(xùn)練模型時(shí)烟具,它將學(xué)習(xí)每個(gè)通道的最佳beta值梢什。
在轉(zhuǎn)換函數(shù)中,我們必須執(zhí)行以下操作以將這些學(xué)習(xí)到的權(quán)重放入mlmodel文件中:
def convert_learnable_swish(layer):
params = NeuralNetwork_pb2.CustomLayerParams()
. . .
beta_weights = params.weights.add()
beta_weights.floatValue.extend(layer.get_weights()[0].astype(float))
return params
以上是Keras中所需要做的所有工作朝聋。
在運(yùn)行iOS應(yīng)用程序時(shí)嗡午,您將注意到setWeightData() 現(xiàn)在接收一個(gè)包含24字節(jié)的Data對(duì)象。就是6通道乘以每個(gè)浮點(diǎn)數(shù)的4字節(jié)玖翅。
使用Swish.swift層代碼從這個(gè)權(quán)重?cái)?shù)組讀取beta并在計(jì)算中使用它翼馆,這相當(dāng)簡(jiǎn)單。與以前的主要區(qū)別是金度,我們知道应媚,在數(shù)據(jù)中有許多不同的beta值。我將把這個(gè)留給讀者作為練習(xí)猜极。