格ゲーの動画を解析したい 〜機械学習をやってみる〜 8フレーム目

Kotlin+OpenCVで格ゲーの動画を解析したい連載の8回目です。
前回、輪郭にラベルを付けるGUIアプリケーションを作成して輪郭にラベルを付ける作業を行いました。
今回は輪郭から特徴量を抽出して機械学習をやってみます🤙

輪郭から特徴量を抽出する。

輪郭(座標のリスト)から特徴量を抽出していく。
画像に対する輪郭の大きさや、縦横比、輪郭の包含を縦横n分割した位置が輪郭内か外かのフラグ、などを特徴量とした。
ちなみに特徴量は約1万次元になった

class SamplePreProcessor {
    fun process(contour: MatOfPoint, screenWidth: Int, screenHeight: Int, resolution: Int): Sample
    {
        // 輪郭の正規化
        val normContourF = calcNormContour(contour)

        // 画面面積に対する領域面積の比率
        val areaRatio = calcAreaRatio(screenWidth, screenHeight, contour)

        // 領域の縦横比
        val aspectRatio = calcAspectRatio(contour)

        // 領域の包含をn分割して輪郭内か外かのフラグ
        val contourOneHot = ContourOneHotExpressionCalculator().calc(normContourF, resolution)

        return Sample(areaRatio, aspectRatio, contourOneHot)
    }
}

学習する

上記で算出した特徴量と前回付与したラベルを元に学習する
学習そのもののコードはこれだけ。
ここでは単純ベイズ分類を行っている。
sampleMatは特徴量で浮動小数点形式の行列、labelMatはラベルで符号付き整数の行列
学習結果のモデルはxml形式で保存できる

val ml = NormalBayesClassifier.create()
ml.train(sampleMat, Ml.ROW_SAMPLE, labelMat)
ml.save("/path/to/model.xml")

エラーが発生した

上記のコードを実行した所、エラーが発生した。
Javaが終了コード137で死んでる・・・🤔

java finished with non-zero exit value 137

Javaの終了コード 128以上は128 + シグナル番号を表していて、上記はsignal 9 (SIGKILL)を受信しているらしい。
メモリ使用率を確認すると、学習中にメモリが枯渇してOOM Killerが発動してプロセスが殺されたっぽい🧐

システムモニターでメモリ使用量を確認している図

スワップ領域を8GBに広げてみる
実メモリ領域と合わせて16GBぐらい使えるからいけるっしょ🤭

sudo swapoff -a
sudo dd if=/dev/zero of=/swapfile bs=1G count=8
sudo mkswap /swapfile
sudo swapon /swapfile

再実行してみる。

えぇ・・・
これでも全部食いつぶして死ぬんだけど・・・😱

システムモニターでメモリ使用量を確認している図

スワップを更に16GBに広げる。
流石に動くっしょ👀

システムモニターでメモリ使用量を確認している図

死ぬんだよなぁ・・・🤨

JVMがOOM Killerで殺されている様子

更にスワップを倍にしてみる。

システムモニターでメモリ使用量を確認している図

動いた!
けど3時間まっても学習がおわらなかった・・・。😱

処理時間は特徴量の次元数に比例?

特徴量が1万次元は多すぎるのかもしれない。
試しに、特徴量を100次元に減らして実行すると2600msで学習が完了した。
特徴量を150, 200, 250,,,と増やした時の処理時間をプロットすると以下の様になった。
処理時間が指数関数的に増えている

縦軸に処理時間、横軸に特徴量の次元数をプロットした図
プロットした点と回帰曲線の定義

特徴量は減らさないと厳しそうだな・・・
妥当そうな次元数まで特徴量を削減して、45分で学習が完了した。

複数のCPUを活用して学習速度を向上させたい

学習中のCPU使用率を見ていると、常に1つのCPUのみが100%に張り付いており、1CPUしか使われていない気がする。
これを複数CPUで回せる様になればもっと効率よく学習できるのではないか?

システムモニターでCPU利用率を確認している図

JVMが認識しているCPUの数を確認してみる
Runtime.getRuntime().availableProcessors()printlnすると8が表示されたので、CPU自体は認識されている

println("processors: ${Runtime.getRuntime().availableProcessors()}")

OpenCVのマルチスレッド機能が有効になっていない?¥
OpenCVのビルド情報を確認する
Parallel frameworkpthreadsになっているので有効化されている模様。

Core.getBuildInformation()
Parallel framework:            pthreads

getNumThreads()でOpenCVが並列領域に使用するスレッドの数を確認できる。
8が出力されたので、やはり有効化されていそう。
スレッド化がサポートされていない場合、1を返すらしい。

Core.getNumThreads()

調べていると以下のQAをみつけた
Enable Multithreading with TBB during cascade training
上記のQAによると、マルチスレッドを有効化しても分類器の学習はほとんど並列化されないらしい。
残念😢

学習したモデルから予測してみる。

前回作成した輪郭にラベルを付けるGUIアプリをコピペして、輪郭からラベルを予測するGUIアプリを作った。
画像上から輪郭を選択すると右ペインに予測結果のラベルが表示される。

輪郭からラベルを予測するGUIアプリケーション

予測部分のコードはこんな感じ。
loadメソッドで学習済みのモデルを読み込み、predictメソッドで予測できる。

val model = NormalBayesClassifier.load("/path/to/model.xml")
val labelData = model.predict(sampleMat)

今日はここまで!
次は交差検証などをやってみる

所感

前処理の実装と学習に時間が掛かるので、トライ&エラーの1回のトライに時間が掛かるのが難しいなぁと思った
「選んだ特徴量で精度が出るのか?」という一番不安な部分が最後まで答えがでないので、期限が決められた中でやるのは胃がキリキリなりそう
仕事で機械学習をやるなら、いきなりプロジェクトをドンと立ち上げるんじゃなくて、最初にローカル環境で少ないサンプルで実装してみて実現可能性を判断するんだろうな。
そのためにもローカルマシンはなるべくハイスペックな方がよさそう。特にメモリ。64GBぐらいほしいなぁ。

コメントする