はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】Unityでもゼロから機械学習を作る【単回帰モデル】

はじめに

皆さん機械学習と聞いてどんな言語を使うと想像するでしょうか。

おそらくほとんどの人がPythonを思い浮かべると思います。よくYoutubeの広告でもPythonを学ぶ宣伝文句になっていたりしますよね。

ところがUnity好きな私はどうしてもUnityでも機械学習がしたかったので、実際にゼロからプログラムを作成してみようと思いました。

愛があればどんなことも乗り越えられるのです。



茶番はおいておいて、Unityで機械学習をするのも正直悪くない選択なような気がします。

実際にML-Agentという機外学習をするUnityのライブラリもあるわけですし(厳密にはPythonで学習させていますが)、最近はBrracudaなんて派生系のライブラリも登場してきています。

またUnityはComputeShaderというものを使えばGPUを用いた計算ができるので、速度もそれなりには出てくるはずです。

Barracudaも本当か知りませんが、内部的にはComputeShader祭りという噂も聞いたこともあります。

また機械学習の全部の流れがUnity内で完結するっていうもの夢のひとつですね。

また一応今回のプロジェクトのGitHubを公開しています。
github.com

では早速いきましょう。

その前に

ただ私も勉強したての初心者なので、間違っていたりするところも多々あると思います。

もし間違っていたら優しく教えていただけると嬉しいです。

また機械学習の入門にしていきたいので、ライブラリなどは一切使わない方針でいきたいと思います。

今回は教師あり学習の単回帰モデル*1というものをまず扱っていきます。

おそらく一番簡単な奴ですね。

また明日は重回帰モデル*2を投稿する予定です。

加えて細かい数学的な説明や証明をしていたら数万字を超えてしまうので凝縮していきますが、一応数学的に説明しながらいきたいと思います。

機械学習とは

機械学習を簡単に説明すると、このように表せます。

  1. 入力データに対して出力データをを返す関数のような働きを持ったモデル
  2. 機械学習モデルの振る舞いは、学習により規定される

この機械学習には3つの種類があります。今回は教師あり学習をします。

  1. 教師あり学習:学習データが、モデルに対する入力データとその時のあるべき出力である正解データのセットになっているもの
  2. 教師なし学習:正解データなしに、学習データのみが与えられたもの
  3. 強化学習:教師あり学習と教師なし学習の中間の学習法

さらに今回扱う単回帰モデルは、教師あり学習の中でも回帰モデルに含まれます。

  1. 回帰モデル:出力値が数値であるモデル
  2. 分類モデル:出力値が離散値(クラス・ラベル)であるモデル

この箇所はさらっと流し見して良いですが、いかに機械学習が広義の言葉であるかが伺えますよね。

人工知能なんて言葉はもはや手がつけられません。

学習データを取得する

今回はUC バークレー大学の UCI Machine Leaning Repository にて公開されている赤ワインのデータセットを利用させていただきます。

UCI Machine Learning Repository: Wine Quality Data Set

このデータの中のphからalcohol(アルコール度数)を予測するプログラムをこれから作成していきます。

f:id:hanaaaaaachiru:20200222213722p:plain

またこの二つの違いを覚えておくとよいでしょう。

yp : 予測値
yt : 正解値

データセットに含まれているのは正解値のほうで、予測値をプログラムにより作成します。

学習データをUnityに入れる

先程のデータを自分で編集して使っても良いですが、こんなところで時間を取られるのも嫌なのでこちらで使いやすいように編集したものを.unitypacageにして配布したいと思います。

再配布ってまずいような気もするので多少数値等の中身を変更させていただきましたが、それでもマズそうならこっそり教えてください。

https://drive.google.com/file/d/1SkkekzaSu9XBQnwdYHmCtSvh8M6bUkZ0/view?usp=sharing

ダウンロードができたら、Unityにドラッグ&ドロップしてください。

f:id:hanaaaaaachiru:20200222175145p:plain

こんな感じのものが入っていたらOKで、左にph,右にalcohol(アルコール度数)が1000個ずつ入っています。

f:id:hanaaaaaachiru:20200222222556p:plain

データを読み取る

csvファイルからデータを読み取りましょう。

ここは本質ではないのでこちらのコードを用意しました。

DatasetReader.cs


予測モデルの作成

まずは予測モデルを作る必要があります。

今回の線形単回帰モデルの場合には、2つのパラメータw0w1を用いた一次関数を表現して予測値ypを算出します

  yp = w_0 + w_1 x

もっと砕けて言えば、一次関数の傾きと切片を学習して先程の散布図に近そうなもの算出します。

ただしベクトルで扱った方が一般的なので、ダミー変数 x_0(=1)を導入して式を変形しましょう。

  \boldsymbol{x} =  (x_0, x_1)
  \boldsymbol{w} = (w_0, w_1)
  yp = \boldsymbol{w} ・\boldsymbol{x}

ちなみに \boldsymbol{x}は太文字になっていますが、高校で習う \vec{x}と同じ意味です。


これをもっとちゃんと書くと、単回帰モデルの大切な3つの式の一つになります。

  yp^{(k)(m)} = \boldsymbol{w}^{(k)}・\boldsymbol{x}^{(m)}   (1)

意味としては「m番目のデータ系列の繰り返し計算k回目の予測値 = 重みベクトル \boldsymbol{w}の繰り返し計算k回目の結果・m番目のデータ系列の入力ベクトル \boldsymbol{x}となります。

実際にコードにしてみるとこんな感じ。

/// <summary>
/// 予測関数(1,x)の値から予測値ypを計算する
/// </summary>
private float[] Pred(Vector2[] x, Vector2 w)
{
    float[] yp = new float[_dataSize];

    for(int m = 0; m < _dataSize; m++)
    {
        yp[m] = Vector2.Dot(x[m], w);
    }

    return yp;
}

Vector2.Dot(a,b)はaとbの内積をとります。

これをグラフにしてみると機械学習で良くみる奴になります。

f:id:hanaaaaaachiru:20200222225829p:plain

損失関数の作成

次に損失関数を作成します。

損失関数は予測値と正解値がどれくらい近いかを表現していて、小さければ小さいほど良いです。

  L(w_0, w_1) = \frac{1}{2M} \sum_{m=0}^{M-1} (yp^{(m)} - yt^{(m)})^2

これを学習の度に出力すればどれくらい学習がうまくいっているかの目安になることもできます。

また予測値ypと正解値ytの差、いわゆる誤差を求める式は3つの大切な式の2つ目になります。

  yd^{(k)(m)} = yp^{(k)(m)} - yt^{(m)}  (2)

意味は「m番目のデータ系列の繰り返し計算k回目の誤差 = m番目のデータ系列の繰り返し計算k回目の予測値 - m番目のデータ系列の正解値」です。

コードにしたらこんな感じ。

/// <summary>
/// 誤差を計算する
/// </summary>
private float[] CalsulateError(float[] yp, float[] yt)
{
    float[] yd = new float[_dataSize];

    for(int m = 0; m < _dataSize; m++)
    {
        yd[m] = yp[m] - yt[m];
    }

    return yd;
}

勾配降下法

勾配降下法は損失関数を最小とするような最適なモデルのパラメータを見つけるためのアルゴリズムです。

そのためには損失関数を変数 w_0, w_1で偏微分したものを用います。

  \frac{\partial L(w_0, w_1)}{\partial w_i} = \frac{1}{M} \sum_{m=0}^{M-1} yd^{(m)}・x_i^{(m)}  (i = 0, 1)

損失関数が小さくなる方向を知るためには、偏微分したものの逆向きに進めばよいというわけですね。

これを用いて、大切な3つの式の最後の式が導けます。

  w_i^{(k+1)}  = w_i^{(k)} - \frac{α}{M} \sum_{m=0}^{M-1} yd^{(m)}・x_i^{(m)}  (i = 0, 1)  (3)

この式のαは学習率と呼ばれるもので、ほど良い値にしないとうまく学習ができない重要なパラメーターになります。

これをコードにしてみるとこんな感じ。

/// <summary>
/// 勾配降下法の実装
/// </summary>
private Vector2 GradientDescentMethod(Vector2[] x, Vector2 w, float[] yd)
{
    for(int i = 0; i < 2; i++)
    {
        float sum = 0;
        for(int m = 0; m < _dataSize; m++)
        {
            sum += yd[m] * x[m][i];
        }

        w[i] = w[i] - (_alpha * sum / _dataSize);
    }

    return w;
}

コード

実際にこれらを組み合わせてコードを作ってみました。

SimpleRegressionModel.cs

このコードでは50000回の繰り返し,学習をしています。

この処理でなにか一番重いかというとおそらくDebug.Logです。これはUnityの仕様なのでどうしようもありませんので、なるべく使用控えましょう。

結果

これを実際に動かしてみたところ、こんな一次関数が得られました。

yp = 1.481764x + 5.592909

これを最初の方に貼った散布図と照らし合わせてみるとこんな結果になっていました。(ソフトに使い慣れていなくてメモリがほんの少しずれていますがご了承を)

f:id:hanaaaaaachiru:20200222234716p:plain

もとの散布図が結構バラバラだったので悪くない結果といえるでしょう。

さいごに

実際にやってわかったことはC#の行列は貧弱という点ですね。

行列のライブラリがあるらしいのでそちらを入れるのも一つの手ですが、自作してみるのも面白いかもしれません。

またComputeShaderを使って書き直したものがうまくいったらそちらの記事も書こうと思います。

あと繰り返しになりますが、入力変数をもっと増やした重回帰モデルも明日公開しようと計画しています。

良かった興味があるかたはそちらもどうぞ。

ではまた。

www.hanachiru-blog.com

*1:入力変数が1次元のもの

*2:入力変数が2次元以上のもの