前回お話ししました通り、以下の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++に書き換えてみようと思います。