機械学習周りのプログラミング中心。 イベント情報
ポケモンバトルAI本電子書籍通販中

iPadのNeural Engineで将棋AI part04 Core MLでdlshogiの評価関数を動かす

iPad上でCore MLを用いてバッチサイズ1でモデルを動作させ、計算結果があっているかどうかの確認までを行います。

検証用データの生成

DNNではモデルがエラーなく動作したように見えても、入力データの与え方が間違っていたり、変換ツールのバグを引いたりして結果が間違っているということがよくあります。 そのため、変換元のモデル(PyTorch)にあるテンソルXを入力したときの出力Yを保存しておき、変換後のモデル(Core ML)でも入力Xに対する出力がYと一致するかどうかを検証する必要があります。 検証用データはcolab notebook上で作成します。

コードは以下のようになります。ポイントは以下の通り。

  • 入力データは乱数より本来想定される入力を使うべきなので、棋譜データを使います。
  • 前回変換したモデルはバッチサイズ1しか対応していませんが、将来バッチサイズを増やすことを想定し、1024サンプル分(バッチサイズ32×32回反復)のデータを出力します。
  • Swiftでnumpyのファイルフォーマットを読み込めないので、数値を生のバイナリ形式で出力します。
from dlshogi.common import *
from dlshogi.network.policy_value_network import policy_value_network
from dlshogi import serializers
from dlshogi import cppshogi
from dlshogi.data_loader import DataLoader

# 棋譜データ
train_dataloader = DataLoader(DataLoader.load_files(["/content/drive/My Drive/ShogiAIBookData/dlshogi_with_gct-001.hcpe"]), 32, device, shuffle=False)

# モデルロード
model = policy_value_network("resnet10_swish", add_sigmoid=True)
model.set_swish(False)#swishをx*sigmoid(x)で計算するモード
serializers.load_npz("/content/drive/My Drive/ShogiAIBook/model/model_resnet10_swish-072", model, False)
model.eval()

# モデルに棋譜データを与えて結果を収集
x1s = []
x2s = []
moves = []
results = []
n_batch = 32
for x1, x2, move, result, value in train_dataloader:
  # x1 = [32, 62, 9, 9], x2 = [32, 57, 9, 9]
  pred_move, pred_result = model(x1, x2)
  # pred_mode: [32, 2187], pred_result: [32, 1]
  x1s.append(x1.detach().numpy())
  x2s.append(x2.detach().numpy())
  moves.append(pred_move.detach().numpy())
  results.append(pred_result.detach().numpy())
  if len(x1s) >= n_batch:
    break

# 入出力をファイルに保存
with open("/content/drive/My Drive/ShogiAIBook/model/SampleIO.bin", "wb") as f:
  for arys in [x1s, x2s, moves, results]:
    d = np.concatenate(arys, axis=0).tobytes()
    f.write(d)

iPadアプリの作成

ここからはMacXcode上での作業になります。iPhoneアプリ開発の入門書通りの箇所は説明しないので、適宜本を読んでください。私は「絶対に挫折しない iPhoneアプリ開発「超」入門 第8版 【Xcode 11 & iOS 13】 完全対応」(SBクリエイティブ)で入門しました。

  • アプリのテンプレートは、iOS→App
  • 言語はSwift

コードはこちらに上げてあるので、全文を見たい場合は参照してください。モデル・検証用データはおいていませんので、自前でcolab notebookを使って変換してください。Swift初心者なので、イケてないやり方が混じってるかもしれませんのでご了承ください。

github.com

プロジェクトへのCore MLモデルの追加

前回作成したDlShogiResnet10Swish.mlmodelファイルをダウンロードし、FinderからXcodeのプロジェクトファイル一覧のところにD&Dします。ダイアログが出たらそのままOKで閉じます。これだけでモデルの追加は完了です。

検証用データの読み込み

まず、先ほど生成したSampleIO.binをモデルと同様にXcodeプロジェクトに追加します。すると、アプリ内でこのファイルが使用可能になります(ファイルがコンパイルされるのかそのままアプリ内にコピーされるのかは拡張子で決まっている?)。以下のステップにより、ファイルの中身をFloat型の配列として読むことができます。

guard let url = Bundle.main.url(forResource: "SampleIO", withExtension: "bin") else {
    fatalError("SampleIO.bin not found")
}
guard let data = try? Data(contentsOf: url) else {
    fatalError("Failed to read SampleIO.bin")
}
let arr = data.withUnsafeBytes {
    Array(UnsafeBufferPointer(start: $0.baseAddress!.assumingMemoryBound(to: Float.self), count: $0.count / MemoryLayout<Float>.size))
}

Core MLのテンソルは、MLMultiArray型で表現されます(画像を扱う場合には、それに特化した型もあります)。これは以下のように生成します。生成時に、形状とデータ型を指定します。

guard let mmArray = try? MLMultiArray(shape: [1, 62, 9, 9], dataType: .float32) else {
    fatalError("Cannot allocate MLMultiArray")
}

MLMultiArrayの要素単位の読み書きはmmArray[0] = 1.2のように普通に配列のように扱って記述することも可能なのですが、NSNumber型を用いた変換が入るためか遅いです。そのため、以下のコードでより効率が良いポインタ型で扱います。

let mmRawPtr = UnsafeMutablePointer<Float>(OpaquePointer(mmArray.dataPointer))

// オフセット付きコピーの例
for i in 0..<size {
    mmRawPtr[i] = arr[ofs + i]
}

SampleIO.binには複数のテンソルをつなげて書き込んであるため、適宜オフセットを指定して読み込んでいきます。コード全文はこちらを参照ください。

モデルの読み込み

アプリ内でのモデルのロード方法です。プロジェクトにモデルを追加すると、そのファイル名に対応したDlShogiResnet10Swishクラスが生成されています。ContentViewクラス内で、ボタンをクリックした際のイベントハンドラloadModelでモデルを読み込みます。ここで、config.computeUnitsに指定する値により、使用されるアクセラレータが変わります。allはNeural Engine/GPU/CPUすべて、cpuAndGPUGPUとCPU、cpuOnlyはCPUのみでの実行となります。すべての処理がNeural Engine上でできるとは限らず、実行計画はCore ML任せとなります。それから、動作確認用のサンプルデータもgetSampleIOで読み込んでおきます。

struct ContentView: View {
    @State var model: DlShogiResnet10Swish?
    @State var sampleIO: SampleIO?
    
    func loadModel() {
        let config = MLModelConfiguration()
        config.computeUnits = .all//デバイス指定(all/cpuAndGPU/cpuOnly)
        model = try? DlShogiResnet10Swish(configuration: config)
        sampleIO = getSampleIO(batchSize: 1)
    }

モデルの実行

モデルの実行は非常に簡単です。model.prediction()に入力テンソルMLMultiArray形式で与えるだけです。PyTorchだとGPU上にあるモデルにはGPU上にあるテンソルを入力する必要がありますが、Core MLではそのようなデータ移動処理は不要です。引数名はモデル変換時のct.TensorType(name='x1', shape=(1, 62, 9, 9))のように指定した値から来ています。モデルの出力テンソルが2個あり、pred.move, pred.resultとしてそれぞれMLMultiArrayを取り出すことができます。move, resultという名前は変換のところでct.utils.rename_feature(spec, move_name, "move")のように指定した値から来ています。最後に、独自に実装したテンソル比較用の関数isArrayCloseで計算結果があっているかサンプルデータと比較します。

guard let pred = try? model.prediction(x1: sampleIO.x1, x2: sampleIO.x2) else {
    msg = "Error on prediction"
    return
}
let moveDiff = isArrayClose(expected: sampleIO.move, actual: pred.move)
let resultDiff = isArrayClose(expected: sampleIO.result, actual: pred.result)

iPad実機での実行

作ったアプリをまずはMac内のシミュレータで実行して、動作を確認できました。MacにはNeural Engineは搭載されておらず、エミュレーションもされないのでNeural Engineでの動作を確認するにはiPad実機での実行が必要です。以下のような初期設定が必要ですが、Core MLを使うからといって特別な操作はなく入門書通りです。

  • まずMac-iPadを初めて繋いだら、「このコンピュータを信頼しますか」と聞かれるのでOK
  • Xcode内、左側のペインでプロジェクトを選択、中央のペインでSigning & Capabilitiesを選択、中央でTeamを設定。iPadに設定されているApple IDと関連づいていないといけない。
  • iPad設定アプリ内、一般→VPNとデバイス管理→デベロッパApp→Apple Development: <メールアドレス>→Apple Development: <メールアドレス>を信頼

実行結果

作成したアプリを実行し、結果があっているかを確認しました。浮動小数点演算が絡む以上、結果の完全一致はしないので誤差を許容して比較します。比較はnumpy.isclose相当の手段を実装しました。e: 期待する値(PyTorchのモデルの出力)、a: 実際の値(Core MLの出力)として、diff = abs(e-a), tol = atol + rtol * abs(e)を計算、diff > tolならエラーとなります。実験してみると、アクセラレータにより、誤差が大きく変わることがわかりました。

computeUnits = .cpuの場合、rtol=1e-3, atol=1e-5でエラーになりませんでしたが、.cpuAndGPU.allの場合はエラーとなりました。.allの場合、e=0.0390に対してa=0.0569というかなり大きな誤差が出る要素がありました。エラーが出ないようにするにはrtol=1e-1, atol=5e-2というかなり大きな許容範囲を設定(およそ10%の誤差を許容)する必要がありました。今回試した1サンプルでの結果なので、あらゆるサンプルでこの範囲の誤差に収まる保証はありません。大多数の結果はそこまでずれませんが、思いのほかずれる場合があるというのは留意が必要です。アクセラレータ指定を変えると結果が変わるので、確かにアクセラレータが使用されていると考えられます。

以上のように、Core ML形式に変換したdlshogiのモデルを実行して結果が正しいこと、またNeural Engineが使用されていることを確認しました。今後、バッチサイズ2以上の処理を実装し、どの程度の速度で評価関数の実行が可能かを調べていきます。現在の変換方法ではバッチサイズ1以外のMLMutiArrayを与えるとエラーになるため、変換時にバッチ処理を考慮する必要があります。