PyTorchで生成した姓名分類モデルをiOSアプリ(swift4)上で利用するまで

# 概要

DeepLearning＋スマホアプリ(iOSアプリ)でなにかするための導入としてやってみました。

本記事の目的は自然言語処理の学習モデルをcoreMLのモデルに変換し、iOSアプリで使用できるようにすることです。そのため、PyTorchでの学習法については**触れません**。

参考にさせていただいた記事 →
　　https://qiita.com/kamata1729/items/e54bcb7d0ed3d296daf8

### 手順

- PyTorchで学習モデル作成
- PyTorchのモデル(**.pth**)をcoreMLのモデル(**.mlmodel**)に変換
- iOSアプリ上で前処理 → 予測 → 結果を出力

### バージョン

- Python 3.7.3
- PyTorch 1.1.0
- swift4
- Xcode 10.3

# PyTorchで学習モデル作成

繰り返しになりますが、本記事の主眼は学習モデルの変換とcoreMLの利用法のため、学習モデルの生成については[公式のサンプル](https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html)を利用しております。

ソースコードは上記リンクのgithubから取得しました。また、そのままだと動かなかったので、エラーは取りました(エラーまとめるのは面倒なので略。すみません)。

色々調べてみると、モデルの保存時の宣言が推奨のやり方と異なっていたため、そこは変更。

```train.py
# 変更前
torch.save(rnn, 'char-rnn-classification.pth')

# 変更後
torch.save(rnn.state_dict(), 'char-rnn-classification.pth')
```
※GPUを使って学習させていた場合、変更前の保存方法で保存してしまうと学習時のGPUの情報が乗ってしまうとかなんとか。


学習モデルを変換するにあたって必要な情報だけまとめます。

- **Input** : 名前を１単語ずつ入力する。入力の形式はフォーカスしている単語のOne-Hotベクトル。小文字＋大文字＋" .,;'-"=58列
- **Hidden** : 学習結果で更新していく隠れパラメータ。128列
- **Output** : 18ヶ国語に分類するため、18列

### 学習モデル基本情報

**モデル名** : char-rnn-classification.pth

| パラメータ       |      　サイズ|
|:--------------|------------:|
| Input         |  [文字列][58]|
| hidden        |        [128]|
| Output        |         [18]|

# PyTorchのモデル(**.pth**)をcoreMLのモデル(**.mlmodel**)に変換

PyTorchモデルを直接coreMLモデルにする手段はないようです。
そのため一度onnxフォーマットに変換してからcoreMLモデルにします。
※ONNXもPyTorchからのexportはできるけど、importはできないらしいです。今後に期待

## PyTorch → ONNX

ONNXとは、様々なDeepLearning用フレームワーク間でモデルを交換することが可能なフォーマットです。まずはPyTorchのモデルをこいつに変換します。

### ONNXをインストール

```
$ pip install onnx
```

### ONNXモデルへ変換

inputとhiddenのダミーデータを用意してあげる必要があります。

- input(1, 1, 58)
- hidden(1, 128)

```convertPyTorchToONNX.py
from data import *
from model import *
import torch
import onnx
from torch.autograd import Variable
from onnx_coreml import convert

# input_size, hidden_size, output_size
rnn = RNN(58,128,18)
rnn.load_state_dict(torch.load('char-rnn-classification.pth'))

# dummy_input
dummy_input = Variable(lineToTensor('nomura'))[0]
# initialize as zero matrix
hidden = rnn.initHidden()
torch.onnx.export(rnn,(dummy_input,hidden),'classification.proto',verbose=False)
```

### coreMLモデルへ

convert関数のパラメータは最低限で問題ないようです。class_labelsには出力サイズを設定します。

```convertONNXTocoreML.py
from onnx_coreml import convert

model_onnx = onnx.load('classification.proto')

coreml_model = convert(
    model_onnx,
    'classifier',
    class_labels=[i for i in range(18)],
)

coreml_model.save('classification.mlmodel')
```

モデルの変換が完了しました！それではiOSアプリに組み込んでいきます。

# iOSアプリ上で前処理 → 予測 → 結果を出力

## モデルのインポート

前章で生成したcoreMLモデルをXcodeのナビゲータエリアにドラック＆ドロップすると、プロジェクトと紐付けるかどうか聞かれるので、OKします。
ナビゲータエリアに表示された**classification.mlmodel**を選択するとmlmodelのプロパティが表示されます。このプロパティ内の**Model Evaluation Parameters**の項目にinput,outputの番号が書いてあります。この番号で入力値と出力値の設定と取得ができます。

<img width="1258" alt="スクリーンショット 2019-09-23 10.04.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273336/f8498384-cfba-c0bd-5739-fa6ee86223c0.png">

私の環境では以下の様になっていました。

|        |            Name|                        Type|
|:--------------|--------:|:---------------------------|
| inputs        |        0| MultiArray (Float32 58)    |
| inputs        |        1| MultiArray (Float32 128)   |
| outputs       |        9| Dictionary (Int64 → Double)|
| outputs       |        7| MultiArray (Float32 128)   |

インポートしたcoreMLモデルは以下のように利用できます。

```swift
import CoreML

// ml_hiddenを更新することで予測を進める
for i in 0 ... n_string - 1 {
    if let output = try? self.classification_model.prediction(_0: input[i], _1: ml_hidden) {
        ml_hidden = output._7;
    }
}
```

## 前処理

我流で泥臭くやってしまいましたが、良い方法はあるのでしょうか・・・？

```swift
    // convert input name to one hot vector
    func convertStringToOneHotVector( string : String! ) -> [MLMultiArray] {
        var one_hot_vectors: [MLMultiArray] = [];
        
        var name_counter = 0;
        var letter_counter = 0;

        let n_letter = all_letters.count;
        let ns_n_letter: NSNumber = n_letter as NSNumber;

        
        for string in string {
            // define ml multi array by one word
            let ml_input = try! MLMultiArray(shape: [ns_n_letter], dataType: MLMultiArrayDataType.float32);
            letter_counter = 0;
            for letter in all_letters {
                if ( string == letter ) {
                    ml_input[letter_counter] = 1;
                } else {
                    ml_input[letter_counter] = 0;
                }
                letter_counter+=1;
            }
            one_hot_vectors.append(ml_input);
            name_counter+=1;
        }
        
        return one_hot_vectors;
    }
    
    // initialize vector as empty vector
    func initEmptyVector( vector: MLMultiArray, size: Int ) {
        for i in 0 ... size - 1 {
            vector[i] = 0;
        }
    }
```

## 予測、出力

まずは出力時の対応です。予測結果は0~18のkey値にしか紐付かないため、理解できる結果と紐付けておく必要があります。PyTorchソースから以下の**all_categories**に格納された言語の順番をswift側でも宣言しておきます。

```data.py
all_categories = []
for filename in findFiles('data/names/*.txt'):
    category = filename.split('/')[-1].split('.')[0]
    all_categories.append(category)
    lines = readLines(filename)
    category_lines[category] = lines

n_categories = len(all_categories)
```

私の結果は以下のようになっていましたので、

```console
['Czech', 'German', 'Arabic', 'Japanese', 'Chinese', 'Vietnamese', 'Russian', 'French', 'Irish', 'English', 'Spanish', 'Greek', 'Italian', 'Portuguese', 'Scottish', 'Dutch', 'Korean', 'Polish']
```

swiftで以下のようにdictionary型を定義しました

```swift
    let classDic: [Int64 : String] = [
        0: "Czech",
        1: "German",
        2: "Arabic",
        3: "Japanese",
        4: "Chinese",
        5: "Vietnamese",
        6: "Russian",
        7: "French",
        8: "Irish",
        9: "English",
        10: "Spanish",
        11: "Greek",
        12: "Italian",
        13: "Portiguese",
        14: "Scottish",
        15: "Dutch",
        16: "Korean",
        17: "Polish"
    ]

```

出力結果はソートしておきます。
トップ3件の候補を表示します。

```swift
    // predict what language input name is
    func predictNameLanguage( string: String, input: [MLMultiArray], hidden: MLMultiArray) {
        var output_array: [Int64: Double] = [:];
        let n_string = string.count;
        var ml_hidden: MLMultiArray = hidden;
        
        for i in 0 ... n_string - 1 {
            if let output = try? self.classification_model.prediction(_0: input[i], _1: ml_hidden) {
                ml_hidden = output._7;
                output_array = output._9;
            }
        }
        
        // sort result
        let output_sorted_array = output_array.sorted{ $0.value > $1.value }
        dispResult(array: output_sorted_array);
    }
    
    // display result
    func dispResult( array: [(key: Int64, value: Double)] ) {
        result_1.text = classDic[array[0].key] as! String;
        result_2.text = classDic[array[1].key] as! String;
        result_3.text = classDic[array[2].key] as! String;
    }
```

# 実行
<img width="350" alt="IMG_0278.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273336/f73cad00-6dc0-1dec-680c-cd342e485933.png">

<img width="121" alt="スクリーンショット 2019-09-23 10.59.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273336/78f112d6-8bd7-3d30-4046-c86e8ee72599.png">

pythonで実行した場合と結果が一致していれば成功です！

# おわりに

**Nomura**を分類してみたところ・・・

<img width="350" alt="IMG_0279.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273336/25df4ab0-bb12-08cb-7d71-9e02b45b7bf8.png">

Arabicと予測されました・・・
次はモデルの精度向上を目指します。