【物体検知】YOLOv4をiOS上で動かす

# はじめに

つい先日、物体検知のモデルであるYOLOの最新バージョンv4が発表されましたね！
速度を保ったまま大幅に精度を上げることができたようです！

そんなYOLOv4を今回はiOS上で動かしてみました。

<img width="330" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588399/0fa6f2c9-92b1-b148-b3dd-94d446202d2a.jpeg">

# 環境

python=3.7.2
tensorflow=1.13.1
keras=2.3.1
coremltools=3.3

# YOLOv4 の学習済みモデルを取得

こちらからダウンロードできます。

<a href="https://github.com/AlexeyAB/darknet"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/AlexeyAB/darknet.png" width="460px"></a>

# 学習済みモデルの変換 Darknet →（Keras →）CoreML 

Darknetで学習した or 学習済みのモデルをiOS上で動かせるように変換します。
内部では一旦 Keras のモデルに変換されてから CoreML 用のモデルに変換されています。

コンバーターはYOLOv3のものをベースにしています。（[keras-yolo3/convert.py](https://github.com/qqwweee/keras-yolo3/blob/master/convert.py)）
そこで、YOLOv4 から採用された Mish Activation レイヤーに新しく対応させる必要があります。

まずはレイヤーの定義から

```py:convert.py
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'` の場合に適応してあげます。

```py:convert.py
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側から正しく読めないみたいです。

```py:convert.py
def convert_mish(layer):
    params = NeuralNetwork_pb2.CustomLayerParams()
    params.className = "Mish"
    params.description = "Mish Activation Layer"
    return params
```

```py:convert.py
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 })
```

[convert.pyのソースコード](https://gist.github.com/TakaoNarikawa/aef13571eec97d78603688eef05b5389)

```shell:zsh
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` を使用するにあたって、今回こちらのリポジトリを使用させていただきました。

<a href="https://github.com/Ma-Dan/YOLOv3-CoreML"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/Ma-Dan/YOLOv3-CoreML.png" width="460px"></a>

`.mlmodel` をプロジェクトフォルダに入れます

<img width="304" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588399/395071e4-bbfc-9c2f-4c71-55df32134e4d.png">

モデルを今回作成したものに切り替えます。

```swift:YOLO.swift
// let model = YOLOv3()
let model = Yolov4()
```

## Mish レイヤーの作成

Swift 側にも Mish レイヤーを定義していきます。

```swift:Mish.swift
@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](https://developer.apple.com/documentation/accelerate?language=objc)

ここの部分、見やすく書くと次みたいな感じになるのですが、YOLOv4の中で72回呼ばれてるということもあり、for文で１つ１つ処理してるとめっちゃ重くなっちゃうわけですね、なので配列として一気に計算させようという感じです。

```swift
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` と同じ場所に置きます。

[Mish.swift](https://gist.github.com/TakaoNarikawa/cde26544babaab52ecb70b0f39d29979)
[Mish.metal](https://gist.github.com/TakaoNarikawa/b21f7a9e2e5c6a2b114515ee61ac85d2)

# 実行の様子

無事に実行することができましたが、全体的にモッサリしてます。あと、精度も YOLOv3 と比べてむしろ悪くなっているような印象を受けます。

### YOLOv4
![v4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588399/58f1425b-4c5e-60d9-74de-0abde8acc5da.gif)


### YOLOv3
![v3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588399/e638cb18-6674-8106-4a6c-edaa76ae61ad.gif)


調べてみると、Mish レイヤーを無効にしている場合のCPU使用率は160%程度（おそらく最大400%）、メモリ使用量は60MBほどなのに対して、有効にしている場合のCPU使用率は30%程度、使用メモリは250MBにまで増加します。

メモリをめっちゃ使用する上に、CPUをあまり使えてないという形ですね。この辺は Swift 側のコードを最適化するのが正しそうです。