ここで、Gobang aiのコア部分であるミニマックス検索とα-βプルーニングアルゴリズムについて説明します。この2つは非常に背が高いように聞こえましたが、実際に気付いた後、同じであることがわかりました。
1.ミニマックス検索
ミニマックス検索とは何ですか?まず、ゲームツリーの概念を紹介します。ゲームツリーは、自分と対戦相手が決定を下すときに形成されるツリー構造であり、各ノードのブランチは、現在のノードがとることができるさまざまな可能な位置を表し、各リーフノードは状況を表します。たとえば、空のチェス盤(ルートノード)から始めて、移動します。15x15= 255種類の移動オプションがあります。移動すると、255ノードのゲームツリーが形成されます。ゲームツリーのリーフノードは次のとおりです。状況.. この時点で対戦相手が動き、対戦相手に254の選択肢がある場合、新しく形成されたゲームツリーには255x254のリーフノードがあります。
それことがわかるゲームツリーは指数レベルである。平均分岐がBであり、ゲームツリーレベルの数はD(ルートノードが第0レベルである)であり、次いで、リーフノードの総数はBについての場合^ d。
ゲームツリーのコンセプトで、いくつかのステップが見える「スマート」なAIを実現できます。では、どのように「いくつかのステップを見る」のでしょうか。簡単に言えば、ゲームツリーのすべてのリーフノードをトラバースし、aiにとって最も好ましい状況を見つけてから、移動することです。ゲームツリーでは、aiがチェスを移動すると、移動するのに最も有利な位置ノードが選択され、プレーヤーがチェスを移動すると、aiは、プレーヤーがプレーヤーにとって最も有利な位置ノードを選択することをシミュレートします。評価関数Fはaiの状況をスコアリングするため、aiが移動すると、Fが最大のノードが選択され、プレーヤーの移動をシミュレートするときにFが最小のノードが選択されます(Fが小さいほど、aiにとって不利になります)。 、そしてそれはプレイヤーにとってより重要です。有利なことに、AIは、プレイヤーをシミュレートするときにプレイヤーが「スマート」であると見なします)。そのため、ミニマックス検索と呼ばれます。
2.ミニマックス検索の最適化
1.α-βプルーニングアルゴリズム
このアルゴリズムの名前は非常に大きく聞こえますが、実際の意味合いを理解するのは難しくありません。
非常に簡単な例を挙げてください。
最大層は、ノードのこの層の値がその子ノードの最大値を選択する必要があることを示し、最小層は、ノードのこの層の値がその子ノードの最小値を選択する必要があることを示します。
そのようなゲームツリーの場合、リーフノードd、f、g、およびhの値を知っているので、aの値を見つける方法は?まず、dとfから、bの値が-1であることがわかり、次にaがノードbを検索し、反対側を検索し、acgを検索したことがわかります。このとき、 aは最大値を選択する必要があるため、cは<=-2である必要があります。ノードの場合、bの値はすでにcより大きく、ノードcを検索する必要がないため、achのブランチは「カットオフ」されます。
α-βプルーニングアルゴリズムでは、各ノードはαとβに対応します。αは現在のノードの最良の下限を表し、βは現在のノードの最良の上限を表します。最初は、αは負の無限大であり、βは正の無限大です。次に、検索が実行されます。最大レイヤーのノードが子ノードの1つを検索するたびに、自身のα(下限)を更新する必要があります。最小レイヤーのノードが子ノードの1つを検索するたびに、 β(上界)を更新する必要があります。更新後にα> =βであることが判明した場合は、後続の子ノードを検索する必要がなくなり、それらが壊れて直接プルーニングされることを意味します。これが、α-βプルーニングアルゴリズムの完全な意味です。
私のコード:
struct POINTS{
//最佳落子位置,[0]分数最高,[9]分数最低
QPoint pos[10];
int score[10];//此处落子的局势分数
};
struct DECISION{
QPoint pos;//位置
int eval;//对分数的评估
};
DECISION decision;//analyse得到的最佳落子点
int chessAi::analyse(int (*board)[15], int depth, int alpha, int beta){
gameResult RESULT=evaluate(board).result;
if(depth==0||RESULT!=R_DRAW){
//如果模拟落子可以分出输赢,那么直接返回结果,不需要再搜索
if(depth==0)
POINTS P;
P=seekPoints(board);//生成最佳的可能落子位置
return P.score[0];//返回最佳位置对应的最高分
}else return evaluate(board).score;
}else if(depth%2==0){
//max层,我方(白)决策
int sameBoard[15][15];
copyBoard(board,sameBoard);
POINTS P=seekPoints(sameBoard);
for(int i=0;i<10;++i){
sameBoard[P.pos[i].x()][P.pos[i].y()]=C_WHITE;//模拟己方落子,不能用board,否则可能改变board的信息
int a=analyse(sameBoard,depth-1,alpha,beta);
sameBoard[P.pos[i].x()][P.pos[i].y()]=C_NONE;//还原落子
if(a>alpha){
alpha=a;
if(depth==4){
//4是自己设立的深度(可以改为6,8,但必须为偶数),用来找最佳落子
decision.pos.setX(P.pos[i].x());
decision.pos.setY(P.pos[i].y());
decision.eval=a;
}
}
if(beta<=alpha)break;//剪枝
}
return alpha;
}else{
//min层,敌方(黑)决策
int rBoard[15][15];
reverseBoard(board,rBoard);
POINTS P=seekPoints(rBoard);//找对于黑子的最佳位置,需要将棋盘不同颜色反转,因为seekPoint是求白色方的最佳位置
int sameBoard[15][15];
copyBoard(board,sameBoard);
for(int i=0;i<10;++i){
sameBoard[P.pos[i].x()][P.pos[i].y()]=C_BLACK;//模拟敌方落子
int a=analyse(sameBoard,depth-1,alpha,beta);
sameBoard[P.pos[i].x()][P.pos[i].y()]=C_NONE;//还原落子
if(a<beta)beta=a;
if(beta<=alpha)break;//剪枝
}
return beta;
}
}
seekPoints()は、現在の状況で最適な配置ポイントの位置と配置後のスコアを見つけるために使用されます。ここでは、ローカル検索と静的評価ヒューリスティックを使用して効率を向上させます。これについては後で説明します。
間違いを犯しやすい点がいくつかあり(バグを変更するのに半週間かかりました)、注意が必要です
。1。位置があるため、ゲームツリーの検索深度は偶数である必要があります。リーフノードの評価関数Fは、Bai Ziが一歩踏み出した状況で評価されます。深さが奇数の場合、リーフノードのF推定が正しくなくなります。
2. analysisを再帰的に呼び出すと、analyzeが移動をシミュレートするため、元のボードチェッカーボード配列を使用できません。単にボード上の移動をシミュレートすると、ボード情報が変更され、後続の再帰呼び出しは引き続き変更されます。ボード、それが正しい結果を取得しないように。新しいチェス盤配列をコピーし、移動をシミュレートして、それをパラメーターとして再帰分析に渡すことをお勧めします。
2.ローカル検索と静的評価のインスピレーション
ゲームツリーは指数関数的であるため、平均ブランチ数bを減らす方法を見つけることができれば、リーフノードの数を大幅に減らすことができます。
ローカル検索とは、チェスの駒と関係がある可能性のある空の位置のみが考慮され、すべての空の位置が考慮されないことを意味します。これにより、bの値が大幅に減少する可能性があります。私のローカル検索では、チェス盤の各ポイントの周りに8方向に3つの深さを拡張することを考慮しており、これらの場所のみが既存のチェスの駒に関連付けられます。
静的評価ヒューリスティックは、α-β剪定アルゴリズム用です。つまり、より良い動きが早く見つかるほど、剪定が早く発生します。歩行可能なノードの評価スコアを単純にソートすると、剪定速度を向上させることができます。ローカル検索からすべての歩行可能なノードのスコアを並べ替えて、それらをPOINTSクラスのオブジェクトに配置しました。[0]が最高のスコアで、[9]が最低のスコアでした。
seekPoints()は、これら2つの最適化手法を組み合わせて最適な配置位置を生成する関数です。一般的に、10の最適な動きを検索するだけで需要を満たすことができ、検索速度を大幅に向上させることができますが、小さすぎる場合は有利な枝を切断することが可能です。次に、検索深度が4の場合、最大で10 ^ 5 = 100000の葉ノードのみを検索する必要があり、静的評価ヒューリスティックおよびα-βプルーニングアルゴリズムがさらに削減されます。実行中の実際のプログラムは、5000リーフノードを検索するだけで済みます。 、それは減少しますそれはたくさんです!
struct POINTS{
//最佳落子位置,[0]分数最高,[9]分数最低
QPoint pos[10];
int score[10];//此处落子的局势分数
};
POINTS chessAi::seekPoints(int board[15][15]){
bool B[15][15];//局部搜索标记数组
int worth[15][15];
POINTS best_points;
memset(B,0,sizeof (B));
for(int i=0;i<15;++i){
//每个非空点附近8个方向延伸3个深度,若不越界则标记为可走
for(int j=0;j<15;++j){
if(board[i][j]!=C_NONE){
for(int k=-3;k<=3;++k){
if(i+k>=0&&i+k<15){
B[i+k][j]=true;
if(j+k>=0&&j+k<15)B[i+k][j+k]=true;
if(j-k>=0&&j-k<15)B[i+k][j-k]=true;
}
if(j+k>=0&&j+k<15)B[i][j+k]=true;
}
}
}
}
for(int i=0;i<15;++i){
for(int j=0;j<15;++j){
worth[i][j]=-INT_MAX;
if(board[i][j]==C_NONE&&B[i][j]==true){
board[i][j]=C_BLACK;
worth[i][j]=evaluate(board).score;
board[i][j]=C_NONE;
}
}
}
int w;
for(int k=0;k<10;++k){
w=-INT_MAX;
for(int i=0;i<15;++i){
for(int j=0;j<15;++j){
if(worth[i][j]>w){
w=worth[i][j];
QPoint tmp(i,j);
best_points.pos[k]=tmp;
}
}
}
best_points.score[k]=w;
worth[best_points.pos[k].x()][best_points.pos[k].y()]=-INT_MAX;//清除掉上一点,计算下一点的位置和分数
}
return best_points;
}
結びの言葉
これで、Gobangaiの実現は基本的に終わりました。深度が4の場合、パフォーマンスは非常に良好(0.5秒未満)であり、深度が6の場合、操作は遅くなります(前部で8秒、後部で高速、わずか2または3秒)。他の大物のプロジェクトを見ると、彼らは詳細な反復計算、ゾブリストキャッシュの最適化、マルチスレッドなどのより深いテクノロジーも実装していますが、現在オンラインクラスを開始しています。以前に失った団結プロジェクトはほとんど忘れられています。要するに、大きなことがあります。ヒープ、そして最適化を続けることは、後で時間があるときに話すことです。
私はこのプロジェクトに1週間以上携わっています。今振り返ってみると、多くの回り道をし、多くの経験と教訓を学びました。私が懸命に取り組んできた評価機能を共有するために、ここにブログを書いています。結局のところ、自分で情報を見つけるのは非常に難しいのです。