Java
遺伝的アルゴリズム
機械学習
人工知能
Java入門

【Java】超簡単機械学習~ルーレット選択をやってみた~

こんな人向けに書いてみました

  • 機械学習とかAIに興味はある
  • だけどそれを作れる気はしない
  • Python?プログラミングの一種だっけ??
  • Javaの勉強を始めてみた
  • 数学は割り算までならいける
  • とにかく機械学習っぽいものを書いてみたい

はい、誇張しましたがこんな感じで。
ただし、厳密には機械学習ではないです。機械学習っぽいものですので悪しからず。

機械学習とかAI、興味はあるけど…

昨今、よく耳にするようになりました。興味を持っている方も多いでしょう。
ですが、プログラミング初学者の方にとってはハードルが高いと思います。
なので、難しいことは抜きにして、Java言語でべた書きで、それっぽいものを書いてみます。
Python先生は登場しません。

ルーレット選択とは

機械学習の1つ遺伝的アルゴリズム(GA)で使われる技術要素です。
※厳密には遺伝的アルゴリズムは機械学習に区分されないそうですが…。

乱暴な言い方をすれば、エリートだったり人気者は選ばれやすく、落ちこぼれで人気がない者は選ばれにくくする選択法です。

こんなときに使えるかも

例えばECサイトとか検索サイトとかを開発していたとして、

ユーザーが沢山アクセスしたサイトとか、何回も検索したキーワードをオススメとして表示したい。

でも、「アクセス数No.1」だけがオススメとして毎回表示されても面白くない…。

ある程度ランダム性も持たせつつ、人気のものを高頻度でオススメとして選びたい。

そこでルーレット選択を使ってみましょう。

考え方

至ってシンプルです。

  • アクセス数が多い:エリート、人気者
  • アクセス数が少ない:落ちこぼれ(これ何か語弊があるな…)、人気がない者

そして、エリートが選ばれる確率を高くし、落ちこぼれは低くします。

書いてみた

早速ですが書いてみます。

例として、ユーザーがアクセスした回数をサイト毎に数えていたとします。

それらの合計を計算します。

その合計で各サイトの全アクセス数を割ってあげます。

この操作を正規化と言うことにします。

正規化されたアクセス数を合計すると1になります。

要は、全アクセス数に対してそれぞれの要素が何パーセント占めてるのかを計算してあげる感じです。

あとは0以上1未満[0,1)の一様乱数を振ってあげましょう。

ではでは、アクセス数の入った配列があったと考えて、
その要素をルーレット選択で選んでみます。

public class RouletteWheelSelection {

    public static void main(String[] args) {

        // アクセス数の配列accessCountを用意します。
        // 各要素が何かしらのサイトにアクセスした回数だとイメージしてください。
        // 3つ目のサイト(15回アクセス)のアクセス数がこの中では1番多いということです。
        int[] accessCount = { 3, 5, 15, 2, 7, 8, 3, 5, 11, 14, 3 };

        // アクセス数の合計を格納する変数sumを用意
        double sum = 0;

        // アクセス数の合計を計算
        for (int count : accessCount) {
            sum += count;
        }

        // アクセス数の合計が1になるように正規化を行います。
        // 単純に配列の各要素(アクセス数)を合計値で割るだけです。

        // 正規化したアクセス数を格納する配列(normalizedAccessCount)を用意
        double[] normalizedAccessCount = new double[accessCount.length];

        for (int i = 0; i < accessCount.length; i++) {
            normalizedAccessCount[i] = accessCount[i] / sum; // int同士で割り算すると結果もintになってしまうので注意
        }

        // normalizedAccessCountの合計が1に正規化されたので、
        // [0,1)の一様乱数にあてはめられます。
        double randomNum = Math.random(); // 一様乱数の生成はこの一行だけ。Mathクラスのrondomメソッドを使います。

        // 生成したrandomNumの値に応じた、アクセス数の配列accessCountの要素を
        // 取得してみます。
        // ちょっとややこしいのはここ
        int count = 0;  // 何番目の要素なのかを格納する変数です。
        double dSum = 0;    // 正規化したアクセス数を0番目の要素から足していき、その値を格納する変数です。
        for (double nAccessCount : normalizedAccessCount) {
            dSum += nAccessCount;
            if (dSum >= randomNum) {
                // 正規化したアクセス数を配列の頭から足していき、生成した正規乱数を超えたらbreak
                break;
            }
            count++;
        }
        // コンソール出力
        System.out.println("乱数値は" + randomNum + "です。");
        System.out.println("選択されたのは" + count + "要素目です。");
        System.out.println("値は" + accessCount[count] + "です。");
    }
}

こんな感じです。

わかりやすくするために、accessCountを単純にintの配列としていますが、
どんな型であれ、基本的にロジックは変わりません。

ちなみに100000毎回試行してみた結果は下記

0番目の要素が選ばれた回数は3883回です。
1番目の要素が選ばれた回数は6443回です。
2番目の要素が選ばれた回数は19829回です。
3番目の要素が選ばれた回数は2707回です。
4番目の要素が選ばれた回数は9102回です。
5番目の要素が選ばれた回数は10342回です。
6番目の要素が選ばれた回数は3916回です。
7番目の要素が選ばれた回数は6601回です。
8番目の要素が選ばれた回数は14543回です。
9番目の要素が選ばれた回数は18725回です。
10番目の要素が選ばれた回数は3909回です。

2番目の要素が多く選ばれていることがわかるかと思います。
また、それ以外の要素もある程度選択されています。

accessCountが更新されるたびに、各要素の選ばれる確率が変動します。
これで、ランダム性もあり、且つ選ばれる要素に偏りを持たせることができました。

長くなりましたが、これが「機械学習っぽいもの」です。
飽くまで「ぽい」だけですので、もっとちゃんとしたものを期待してた人はごめんなさい。

ちなみに

遺伝的アルゴリズムについてはこちらの記事がわかりやすかったです。
ルーレット選択の説明もされています。
https://qiita.com/simanezumi1989/items/10cfe1e8a23cd9d4c7b1