🎲

NNのパラメータを遺伝学習で調整するオセロAI

2023/03/27に公開

今更感ある内容ではありますが、オセロAIを作りました。きっかけは、遺伝的アルゴリズムについて調べていたことです。遺伝的アルゴリズムについて調べても、本当にこんな方法で学習が進むのかと半信半疑になったので、ニューラルネットワークの重みパラメータを遺伝的アルゴリズムにより調整するオセロAIを作ってみました。

コードは次のURLから見ることができます。

遺伝的アルゴリズム

強化学習のアルゴリズムの一つである遺伝的アルゴリズムは、親集団を交叉・突然変異させて子集団を作り、目的関数の値が理想に近い子個体を残し、それ以外を淘汰して次の世代に引き継ぐことで、徐々に目的関数に適応した子孫を生成していく手法です。生物の進化を模したアルゴリズムであるため、遺伝的アルゴリズムと呼ばれています。

ニューラルネットワーク

ニューラルネットワークは、入力ベクトルに対し、重みとなる行列を掛け合わせることで出力となるベクトルを得られます。入力に対して掛け合わせる重み行列のパラメータを上手に調整することができれば、入力に対して理想的な出力を得られるニューラルネットワークとなります。

ニューラルネットワークのパラメータ調整

ニューラルネットワークの重みの調整方法は様々ですが、今回はオセロAIということで強化学習の一種である遺伝的アルゴリズムによりパラメータを調整することにしました。

ニューラルネットワークによるオセロAIの学習

ニューラルネットワークによるオセロAIを実現するためには、以下の3つのステップが必要です。

  1. オセロルールに沿って判定するコードの作成:オセロゲームのルールに沿って、石の置き場所や裏返しの処理を判定するコードを作成する必要があります。
    例えば、オセロの石をセットする関数は次のように実装しました。
func Set(board *[]int, pos int, player int) bool {
	// 既に石がある場合はエラーを返す。
	if (*board)[pos] != 0 {
		return false
	}
	(*board)[pos] = player //指定された場所に石を置く。

	// 縦横斜めを自分の石の色に変更できるか判定していく。
	mi := []int{-1, 1, 0, 0, -1, 1, 1, -1} //8方向の移動量。
	mj := []int{0, 0, -1, 1, -1, 1, -1, 1}
	i := pos % N //石が置かれた場所のx座標。
	j := pos / N //石が置かれた場所のy座標。
	for k := 0; k < len(mi); k++ {
		if f := check(board, i, j, mi[k], mj[k], player); len(f) > 0 {
			// 変更できる場合は、その方向にある石を自分の色に変更する。
			for _, p := range f {
				(*board)[p] = player
			}
		}
	}
	return true
}
  1. ニューラルネットワークの設計:ニューラルネットワークのアーキテクチャを設計し、入力として現在の盤面の状態を受け取り、出力として最適な手を返すように学習させる必要があります。ニューラルネットワークの学習には、適切な損失関数や最適化アルゴリズムを選択する必要があります。
    ニューラルネットワークはオセロ以外にも汎用性が高いので、https://github.com/takoyaki-3/go-nnとしてモジュール化しました。ニューラルネットワークの構造体は次のようにしました。
type NeuralNetwork struct {
	inputSize  int
	hiddenSize int
	outputSize int
	weights1   [][]float64
	weights2   [][]float64
	bias1      []float64
	bias2      []float64
	activationFunction1 func(float64)float64
	activationFunction1Derivative func(float64)float64
	activationFunction2 func(float64)float64
	activationFunction2Derivative func(float64)float64
	Score			 float64
}
  1. 学習と戦闘:学習させたニューラルネットワークを使って、オセロAI同士を戦わせることで、AIの強さを確認する必要があります。このとき、AI同士が戦うことで、より高度な戦略を学習し、より強力なAIを作り出すことができます。
	// 学習ループ
	for {
		e++
		// 試合を繰り広げる
		type Case struct {
			IScore float64
			JScore float64
			I      int
			J      int
		}
		cases := []Case{}
		for i := 0; i < len(nns); i++ {
			for jj := 0; jj < NumVS; jj++ {
				j := rand.Intn(len(nns))
				// i vs j を試合パターンに追加
				cases = append(cases, Case{
					I: i,
					J: j,
				})
			}
		}

		// 実際に試合を行う処理
		Parallel(NumCore, len(cases), func(index, rank int) {
			i := cases[index].I
			j := cases[index].J
			if i != j {
				// i vs j
				a := Game(nns[i], nns[j], PrintBoard, RandMode, VS_Human)
				cpu1score := 0
				cpu2score := 0
				if v, ok := a[1]; ok {
					cpu1score = v
				}
				if v, ok := a[-1]; ok {
					cpu2score = v
				}
				if cpu1score >= cpu2score {
					cases[index].IScore += float64(cpu1score)
					cases[index].JScore -= float64(cpu1score)
				}
				if cpu1score <= cpu2score {
					cases[index].IScore -= float64(cpu2score)
					cases[index].JScore += float64(cpu2score)
				}
			}
		})

		// 試合結果を基に各ニューラルネットワークの成績を加点又は減点する
		for _, c := range cases {
			nns[c.I].Score += c.IScore
			nns[c.J].Score += c.JScore
		}

		// ニューラルネットワークを成績順に並び替える
		sort.Slice(nns, func(i, j int) bool {
			return nns[i].Score > nns[j].Score
		})

		// ニューラルネットワークの重みをファイルに保存
		nns[0].SaveWeights("trained_data.json")

		// 結果を標準出力
		fmt.Print("e:", e, ":")
		for _, n := range nns {
			fmt.Print(n.Score, " ")
		}
		fmt.Println("")

		// 突然変異を起こしつつ、子世代を生成する
		er := 0.002
		cs := gonn.Crossover(nns[:NextGen], NumParent, er)
		nns = cs
	}

以上のステップを実行することで、ニューラルネットワークを用いたオセロAIを実現することができます。

ニューラルネットワークの設定概要

ニューラルネットワークは入力層、隠れ層、出力層の3層からなるニューラルネットワークを設計しました。

次元数 なぜその次元数か
入力層 65 オセロのマス数8×8=64に次の手番が先攻プレイヤーと後攻プレイヤーのどちらかという情報を加えた65次元の情報
隠れ層 200 表現の幅と演算時間のバランスから適当に200次元と設定
出力層 64 8×8=64マスの内、次に置くことが可能なマスの内、最も値の大きい値を出力したマスに次の手を置く

遺伝的アルゴリズムの設定概要

上記ニューラルネットワークのパラメータを調整するため、次のような設定において遺伝的アルゴリズムによる自然淘汰を行いました。

パラメータ 設定した値
世代数 64206
親の1世代あたりの数 10
生成する子供1世代あたりの数 100
突然変異率 0.002

ニューラルネットワークの学習と結果

ニューラルネットワークの学習は、上記の設定で行いました。学習には、ランダムに生成された盤面を入力し、遺伝的アルゴリズムによってニューラルネットワークの重みパラメータを調整しました。結果を客観的に評価することは難しいので、次のURLから対戦して頂ければと思います。

https://osero.app.takoyaki3.com/

まとめ

ニューラルネットワークと遺伝的アルゴリズムを用いたオセロAIを実装しました。遺伝アルゴリズムに対して私は半信半疑でしたが、遺伝的アルゴリズムによって、ニューラルネットワークの重みパラメータを効率的に調整することができ、強力なオセロAIを実現することができました。

この遺伝的アルゴリズムによって学習した過程の重みを可視化した記事を記述しましたので、併せてご覧ください。
https://zenn.dev/takoyaki3/articles/c8af7b6e5ba3cb

Discussion