前回お話ししました通り、以下のCで書かれた「AIじゃんけんプログラム」を、アルゴリズムを理解して、そのままC++で書き直してみましょう。

 

【オリジナル】

#include<stdio.h>

#include<math.h>

 

#define N 5

 

/*1(グー),2(チョキ),3(パー)を入力すると,前もって決めていた

マシンの手を示します.そして「勝ち,負け,引き分け」を表示.

累積度数も示します.0以下の数を入力すると終了.*/

 

int perceptron(int m, int x[], int w[], int v[])

/* 過去のデータx[]にもとづいてw[]をかけた入力v[]の最大値をとる

予想ユニットの番号(1(グー),2(チョキ),3(パー))を返す.*/

{

        int i,j,k,kmax,vmax,prec[3];

        if(m <= 0) goto end;

 

/* 前回の相手プレイヤーの手 m=1,2,3 のバイナリー表現:

グー (m=1):prec={+1,-1,-1}

チョキ(m=2):prec={-1,+1,-1}

パー (m=3):prec={-1,-1,+1} */

        for(k=0;k<3;k++)prec[k] = -1;

        prec[m-1] = +1;

       

/* 各予測ユニットの入力と相手の新しい手のコードの符号が

一致していない場合に誤り訂正学習を行う */

        for(k=0;k<3;k++){

                if(prec[k]*v[k] <= 0){

                        for(j=0;j<3*N+1;j++)  w[(3*N+1)*k+j] += prec[k]*x[j];

                }

        }

       

/* x[0] から x[3*N-1] を3ビット分右に移動 */

                /*for(i=0;i<3*N-4;i++) x[3*N-1-i]=x[3*N-4-i];オリジナル*/

for(i=0;i<3*N-3;i++) x[3*N-1-i]=x[3*N-4-i];/*青木高明氏によりバグ修正2003/03/04 */

               

/* 前回の相手プレイヤーの手{prec[0], prec[1], prec[2]} を

入力スロット最前列{x[0],x[1],x[2]}に挿入 */

                for(i=0;i<3;i++)x[i]=prec[i];

               

/* 予測ユニットへの入力信号の算定 */

        for(k=0;k<3;k++)v[k]=0;

        for(k=0;k<3;k++){for(j=0;j<3*N+1;j++){

                v[k] += w[(3*N+1)*k+j]*x[j];}}

       

/* 最大入力を受けたユニットの番号(から1を引いたもの) */

        vmax=-1000000;

        for(k=0;k<3;k++){

                if(v[k] >= vmax){

                        vmax=v[k];

                        kmax=k;

                        }

        }

 

/* 最大入力を受けた予測ユニットの番号(1,2,3)を返す */

        end:   

        return(kmax+1);

}

 

int main()

{

        char line[100];

        int i,pred,m,v[3],x[3*N+1],w[9*N+3],fw[3];

       

/* 初期化 */

        for(i=0;i<3;i++){

                v[i] = 0;       /* 予測入力 */

                fw[i] = 0;      /* 勝敗累積 */

        }

        for(i=0;i<3*N;i++)x[i] = 0;             /* 内部状態 */

        x[3*N]=-1;                                              /* しきい */

        for(i=0;i<9*N+3;i++)w[i] = 0;   /* 重み */

       

/* ゲームの開始 */

       

        m=1;while(m>0){

 

/* パーセプトロン予測を前もって行う */

                pred=perceptron(m,x,w,v);/* pred は予測手(1,2,3) */

 

/* ここで相手プレイヤーの手をインプット */

                printf("{1(グー),2(チョキ),3(パー)}:");

                fgets(line, sizeof(line), stdin);

                sscanf(line, "%d", &m);

                if(m > 3) m = 3;

                printf("\n%1d",m); /* m はプレイヤーの手*/

 

/* パーセプトロンの「手」を示す */

                printf("<->%1d:   ",(pred+1)%3+1);

/* (pred+1)%3+1はパーセプトロンの予測手predに対して勝つ「手」

pred=1(グー)   :(pred+1)%3+1=3(パー)

pred=2(チョキ) :(pred+1)%3+1=1(グー)

pred=3(パー)   :(pred+1)%3+1=2(チョキ)*/

               

/* これ以降は勝ち負け表示 */

                if(pred==m){printf("[マシンの勝ち]"); fw[2]++;}

/* 相手プレイヤーがマシンの予測通りの手を打ったのでマシンの勝ち*/

                else if((pred%3) == (m-1)){printf("[あなたの勝ち]"); fw[0]++;}

/* 相手プレイヤーの勝ち:つまり

pred%3=0(予測がパー)だからチョキを出す:相手プレイヤーは m-1=0 グー

pred%3=1(予測がグー)だからパーを出す :相手プレイヤーは m-1=1 チョキ

pred%3=2(予測がチョキ)だからグーを出す:相手プレイヤーは m-1=2 パー 

戸田皓治氏によりコメント訂正2003/05/28 */

                else {printf("[ 引き分け ]"); fw[1]++;}

/* 成績の表示 */

                printf("    マシン%4d, 引き分け%4d, あなた%4d \n",fw[2],fw[1],fw[0]);

        }

        return(0);

}

 

【考察】

(1)エントリーポイントの関数であるmain()では、

   ①変数の初期化を行い、

   ②ゲームループに入り、

    ③プレーヤーの手の予測値をperceptron関数で取得し、

    ④プレーヤーに手を入力させ、表示

    ⑤PCの③で取得した予測値に基づく勝利手を表示

    ⑥勝敗判定

   ⑦(プレーヤーが0以下を入力すると)ループを抜け終了

   という流れになっています。詳細はコメントを参照して下さい。

(2)本プログラムの肝である「手の予測」はperceptron関数にあり、

   ①引数mを使って、ローカル変数precに値を代入(初期化)

   -内容はコメント通り。但し「バイナリー表現」というのは「

    通常のコンピューターでいう、0と1のバイナリー」ではなく、

    「+1と-1による疑似バイナリー表現(実際はweightで1以上

    になることもある)」と理解されます。

   ②作成したprec配列と引数のv配列の要素を比較し、同じでな

    い要素があれば、その要素(グー、チョキ、パー)に相応

    するw配列に、prec配列要素とv配列要素の乗算値を加算し

    てゆきます。

   ーこれだけだと何をやっているのか分かりませんね。コメン

    トは「各予測ユニットの入力と相手の新しい手のコードの

    符号が一致していない場合に誤り訂正学習を行う」とあり

    ます。ここで気づくのは整数配列precとv配列は整数3つの

    長さ、整数配列wは整数配列v(3 x N + 1の長さ)の3倍の

    長さであり、+1と-1を使い乗算を行うと

    (例)

     +1 AND +1 = +1

     +1 AND -1 = -1

    結果は+1、-1になることから、この乗算はANDゲートで

    使っているのかもしれない、ということです。

    ③次にx配列(0 ~ 3 x N)は上位に3要素シフトし、空い

     た下位(0~2)にprecの値を追加しています。

    -x配列が、手(m)を疑似バイナリー表現で展開したprec

     配列をN回分記録する為のキュー(que、FIFO型データ

     コレクション)であることが判ります。

    ④次にv配列(これは↓で分かりますが、前回の予測手を

     意味し、実際の手がprecになります)を0で初期化し、

     ②で加工したw配列のグー、チョキ、パー(配列添字0

     ~2)に相当する手の履歴に実際の手の履歴であるx

     の履歴値を乗算(AND)したものを加算してゆきます。

    -↑の②の処理(予測と実際のANDを加算)とこの処理

     修正履歴と履歴のANDの加算の意味を考えてみます。

     wはグー(0)、チョキ(1)、パー(2)の順で、予想手と実

     際の手の相違した手のデータについて、実際の手のAN

     D値が加算されて収容されます。例えば予想手がグー(

     {+1, -1, -1})で、実際の手がチョキ({-1, +1, -1})

     の場合だと、wの第1x配列(グー(0)のデータ)につい

     て、現実の手(チョキ)の第1配列要素の-1と乗算し、

     xの値が0の場合:0 + 0 x -1 = 0

     xの値が+1の場合:1 + 1 x -1 = 0 

     xの値が-1の場合:-1 + 0 x -1 = -2

     と要素の値が修正されます。(誤ったグーのネガティブ

     評価となるのでしょう。)

     wの第2x配列(チョキ(1)のデータ)については、現実

     の手(チョキ)の第2配列要素の1と乗算し、

     xの値が0の場合:0 + 0 x 1 = 0

     xの値が+1の場合:1 + 1 x 1 = 2 

     xの値が-1の場合:-1 + -1 x 1 = -2 

     と要素の値が修正されます。(なお予想手が当たった

     場合はwへの修正(これが「学習」となる)は行われ

     ません。)

     予想手は実際の手の履歴(x)にこの修正履歴を乗算し、

     加算するので、合計値は誤った手の場合低く、当たっ

     た手の場合高くなります。

    ⑤最後に予想手v[3]の各要素の合計値を比較し、最大の

     もの(グー(0)、チョキ(1)、パー(2))を予想手とし

     て返すことになります。

(3)perceptron関数の理解とANDゲートを使った過去履歴と重

   み評価の有効性は完全に自身があるわけではありませんが、

   このように「過去の手の履歴」と「過去の正誤歴」から評

   価関数を用いて予測すること自体は合理的です。

 

では、次回はこの考え方でこのCのプログラムをC++に書き換えてみようと思います。