はじめに
つい先日、物体検知のモデルであるYOLOの最新バージョンv4が発表されましたね!
速度を保ったまま大幅に精度を上げることができたようです!
そんなYOLOv4を今回はiOS上で動かしてみました。
環境
python=3.7.2
tensorflow=1.13.1
keras=2.3.1
coremltools=3.3
YOLOv4 の学習済みモデルを取得
こちらからダウンロードできます。
学習済みモデルの変換 Darknet →(Keras →)CoreML
Darknetで学習した or 学習済みのモデルをiOS上で動かせるように変換します。
内部では一旦 Keras のモデルに変換されてから CoreML 用のモデルに変換されています。
コンバーターはYOLOv3のものをベースにしています。(keras-yolo3/convert.py)
そこで、YOLOv4 から採用された Mish Activation レイヤーに新しく対応させる必要があります。
まずはレイヤーの定義から
class Mish(Layer):
def __init__(self, **kwargs):
super(Mish, self).__init__(**kwargs)
self.supports_masking = True
def call(self, inputs):
return inputs * K.tanh(K.softplus(inputs))
def get_config(self):
config = super(Mish, self).get_config()
return config
def compute_output_shape(self, input_shape):
return input_shape
このレイヤーを activation == 'Mish'
の場合に適応してあげます。
if activation == 'linear':
all_layers.append(prev_layer)
elif activation == 'mish':
act_layer = Mish()(prev_layer)
prev_layer = act_layer
all_layers.append(act_layer)
elif activation == 'leaky':
act_layer = LeakyReLU(alpha=0.1)(prev_layer)
prev_layer = act_layer
all_layers.append(act_layer)
また、CoreML 側にも Mish レイヤーの情報を与える必要があります。
ちゃんと className
を与えないと、Xcode側から正しく読めないみたいです。
def convert_mish(layer):
params = NeuralNetwork_pb2.CustomLayerParams()
params.className = "Mish"
params.description = "Mish Activation Layer"
return params
coreml_model = coremltools.converters.keras.convert(
model, input_names='input1', image_input_names='input1',
output_names=['output3', 'output2', 'output1'], image_scale=1/255.,
add_custom_layers=True,custom_conversion_functions={ "Mish": convert_mish })
python3 convert.py yolov4.cfg yolov4.weights yolov4.mlmodel
実行すると、次のようなエラーが発生しますが問題ありません。yolov4.mlmodel
が作成されていると思います。
You will not be able to run predict() on this Core ML model. Underlying exception message was: Error compiling model: "compiler error: Error creating Core ML custom layer implementation from factory for layer "Mish".".
RuntimeWarning)
iOS上で実行
今回使用したデバイスは iPhone XS, iOS 13.3 です。
作成した .mlmodel
を使用するにあたって、今回こちらのリポジトリを使用させていただきました。
.mlmodel
をプロジェクトフォルダに入れます
モデルを今回作成したものに切り替えます。
// let model = YOLOv3()
let model = Yolov4()
Mish レイヤーの作成
Swift 側にも Mish レイヤーを定義していきます。
@objc(Mish) class Mish: NSObject, MLCustomLayer {
// (略)
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))
var countAsInt32 = Int32(count)
var one: Float = 1
let vdspLength = vDSP_Length(count)
vvexpf(optr, iptr, &countAsInt32)
vDSP_vsadd(optr, 1, &one, optr, 1,vdspLength)
vvlogf(optr, optr, &countAsInt32)
vvtanhf(optr, optr, &countAsInt32)
vDSP_vmul(optr, 1, iptr, 1, optr, 1, vdspLength)
}
}
}
後半に vvexpf
とか、vDSP_vsadd
とかのキモい関数が並んでいますが、数値計算などを高速に処理してくれる関数みたいで、
Accelerateと言うみたいです。Apple Document | Accelerate
ここの部分、見やすく書くと次みたいな感じになるのですが、YOLOv4の中で72回呼ばれてるということもあり、for文で1つ1つ処理してるとめっちゃ重くなっちゃうわけですね、なので配列として一気に計算させようという感じです。
for j in 0..<input.count {
let x = input[j].floatValue
let y = x / (1 + exp(-x))
output[j] = NSNumber(value: y)
}
他にも GPU を使うようにしたりしています。
Mish.metal
は Mish.swift
と同じ場所に置きます。
実行の様子
無事に実行することができましたが、全体的にモッサリしてます。あと、精度も YOLOv3 と比べてむしろ悪くなっているような印象を受けます。
YOLOv4
YOLOv3
調べてみると、Mish レイヤーを無効にしている場合のCPU使用率は160%程度(おそらく最大400%)、メモリ使用量は60MBほどなのに対して、有効にしている場合のCPU使用率は30%程度、使用メモリは250MBにまで増加します。
メモリをめっちゃ使用する上に、CPUをあまり使えてないという形ですね。この辺は Swift 側のコードを最適化するのが正しそうです。