Node.jsでのターンベースゲームのAI開発(パート1)



みなさんこんにちは!

Habréに関する最初の記事を書いてから1年半が経ちました 。 それ以来、FOTMプロジェクトには多くの変更が加えられています。 最初に、すべてのアップグレードを簡単に説明し、その後、メイン機能であるAIの詳細な分析に進みます。



私の話の最初の部分では、プロジェクトの最新の変更と、ニューラルネットワークを使用したAIの実装の試みについて説明します。 そして、次の記事から、決定木と将来の計画について学びます。 それでは始めましょう!



それはいいことだろう...



最初の記事の後、私は必要なフィードバックを受け取り、最も合理的で複雑すぎないように思われるものの実装に取り​​掛かりました。





上記の改善では、約半年の遅延作業が必要でした。 ハブラ効果が終わるとすぐに、ゲームに参加する人には誰も遊んでいないことに気付きました。 彼らは空のチャットで書き込み、アリーナの待ち行列に入れて...ゲームを閉じます。 結局のところ、もしあなたがサーバー上に一人でいるなら、誰とでも戦うことはできません:-(



ニューロフォーム



率直に言って、私は一般的にプロジェクトを放棄したかったのです。既にその成果と経験をもたらしたからです。 しかし、私は常に機械学習に興味があったので、ニューラルネットワークの情報を探し始めました。 しばらくして、結論を出しました。ネットワークを学習するための環境が必要です。 このため、フィールドが非表示の私のゲームは、学習ボットの作成に最適です。



最初に、各ターンで利用可能なアクションのリストを作成する機能を実装しました。 合計で3つあります。





リソースは各アクションに費やされます-エネルギーは、ターンごとに制限されます。 プレイヤーが自由に1000のエネルギーを持っていると想像してください。「火の玉」能力を使用すると、移動に300のエネルギーがかかります。そのため、たとえば、次のアクションを実行できます:Movement-> Movement-> Fireball-> End the turn。 この後、キャラクターのエネルギーが最大限に補充され、次のプレイヤーが歩きます。



実験のために、2つの無謀なボットがすべての種類の愚かなことをどのように行うかを見るために、アクションの選択をランダムにしました:)



その後、AIがアクションをどのように選択すべきかという疑問が生じました。 さまざまなオプションを検討しましたが、最終的には次のアイデアを思いつきました。 ニューラルネットワークは、戦場の状況に関する正規化された情報を受け取り、次の動作モデルを提供します。



  1. 攻撃的な移動ケージに移動することは、攻撃の観点から有利です(最大数の敵が攻撃能力を使用するのに最適な距離にあるポイント)
  2. 防御的な移動ケージへの移動は、防衛の観点(同盟国の最大数が保護能力の使用に最適な距離にある点)からより有利です。
  3. 攻撃敵にダメージを与える能力を使用します。
  4. キャラクターまたは味方の生存を支援する能力の防御的使用:キャラクターを癒すか、負の効果を取り除きます。
  5. コントロール敵の行動を制限する能力の使用:見事な、減速、能力の使用不能など
  6. 獲得キャラクターまたは味方の特性を高める能力を使用します。
  7. 弱体化敵のステータスを減らす能力の使用。


ニューラルネットワークモデルでは、理解しやすく、自分のタスクに適したパーセプトロンを選択しました。





多層パーセプトロン



入力時-状況に関するデータの配列。 値のシーケンスの小さなスライスを考えてみましょう。



let input = [..., 1, 1, 0.8, 1, 0.5, 0.86, ...];
      
      





この例の6つの数字は、現在のヘルスリザーブと最大6人のキャラクター(アクティブキャラクター、2人の味方、3人の敵)の比率を示しています。 ニューラルネットワークの出力では、次のことがわかります。



  let output = [0.2, 0.1, 0.7, 0.45, 0, 0.01, 0.03];
      
      





これは、上で説明した非常に7つの動作の配列です。 すなわち この場合のニューラルネットワークは状況を評価し、敵を攻撃する(0.7)、自分自身または味方を守る(0.45)、または単に敵に近いセルに移動する(0.2)のが最善であるという結論に達しました。



ゲームの各機能には、それを分類する個別のuseClassプロパティがあります。





プラウラーの能力はダメージを与え、7ターン敵をスタンさせる



「プラウラー」機能の場合、このプロパティは次のとおりです。



  useClass : { "offensiveMove" : 0, "defensiveMove" : 0, "offensive" : 1, "defensive" : 0, "control" : 1, "gain" : 0, "weakening" : 0, }
      
      





そして、配列の形で:



  let abilityUseClassArray = [0, 0, 1, 0, 1, 0, 0];
      
      





「プラウラー」能力がこのモデルにどのように適合するかを判断するために、ニューラルネットワーク( 出力 )によって取得されたソリューションを使用し、2つの線形配列を比較します。



  let difference = 0; for( let i = 0; i < output.length; i++ ) { difference += Math.abs(output[i] - abilityUseClassArray[i]); }
      
      





の値が低いほど、この機能を使用する可能性が高くなります。 配列が完全に同一である場合( useClass機能の動作とプロパティが100%一致する場合)、 は0になります。その後、 が最小になるアクションを選択するだけです。



すべてが美しくはっきりしているように見えますが、いくつかの問題があります。



問題1:正規化



入力データの配列を形成するには、0〜1の範囲で正規化する必要がありました。上記のヘルスバランスの値では、すべてが非常に簡単であることがわかりました。 キャラクターに重ねられた一時的な効果(バフとデバフ)など、一貫性のない値ではより困難です。 それらはそれぞれ、残り時間や効果の乗数(スタック)など、いくつかの重要なフィールドを持つオブジェクトです。 ニューラルネットワークに対して、ある効果が別の効果とどのように異なるかを明確にするには、能力と同じuseClassフィールドを入力する必要がありました。 したがって、私は効果を説明することができましたが、その量の問題は残っていました。 これを行うには、キャラクターに課されたバフとデバフの数を取り、次の形式で正規化しました。



  buffs.length / 42
      
      





このような解決策は、実際には、buffs配列内のオブジェクトのプロパティについてニューラルネットワークに通知しません。 平均して、2〜3個の効果がキャラクターにかかっています。 42バーを越えることは不可能です。戦闘では、それぞれに6つのキャラクターと7つの能力しかありません。 その結果、ゲーム状況の正規化された記述は約500個の値の配列になります。

42個の値のシーケンスを作成して、効果を説明できます(存在しない場合はゼロで埋めます)。 ただし、たとえばそれぞれに10個のプロパティがある場合でも、420個の値が出力されます(これはバフ専用です)。 したがって、私はこの質問をしばらく延期しました:)



問題2:学習サンプル



トレーニングサンプルを作成するには、いくつかの状況の出力値を手動で入力する必要がありました。 このターンで利用可能なすべてのアクションを示すUIを実装しました。 選択したアクションは、特定の入力値セット(入力)のソリューション(出力)として個別のJSONファイルに書き込まれました。 1つのバッチについて、約500の入出力の一致を生成することができました。これはトレーニングサンプルです。 しかし、主な疑問は空中に留まり続けていました。 サンプルはどれくらいの大きさにすべきですか?



さらに、何らかの理由で状況の説明を変更することを決定した場合(およびそれが起こった場合)、最初からやり直す必要があります。 たとえば、入力データ配列が520ではなく800の値で構成されている場合、古い選択全体をネットワーク構成とともにゴミ箱に捨てることができます。



問題3:ネットワークのアーキテクチャと構成



したがって、入力パラメーターの配列には約520の値があり、出力には7つの値があります。 ニューラルネットワークを実装するには、 Synaptic.jsライブラリを選択し、次のようにネットワークを実装しました。



  var network = new Architect.Perceptron(520, 300, 7); // input: 520, hidden: 300, output: 7 var trainer = new Trainer(myNetwork); var trainingSet = [ { input: [0, ... , 1], // input.length: 520 output: [0, 0.2, 0.4, 0.5, 0.1, 0, 0] //output.length: 7 }, ... ]; trainer.train(trainingSet, { rate: .1, iterations: 10000, error: .005, shuffle: true, log: 1000, cost: Trainer.cost.CROSS_ENTROPY });
      
      





これは、最初のネットワーク構成のようです。 私はそれを始めました...ニューロンの10,000回の反復では、2時間を費やしながら、設定されたエラー値0.005に近づくことさえできませんでした。 与えられた価値を達成するために何を変えることができるかを考えていました。 そして、私はすべてが悪いことに気付きました:(



使用可能な構成オプションを検討してください。



  1. サンプルサイズ
  2. 隠れ層の数とそれぞれのサイズ
  3. 学習速度係数
  4. 反復回数
  5. エラー値
  6. 評価機能(3つのオプション、または独自に作成できます)


特に2週間ニューラルネットワークを使用している場合、それぞれが作業の結果にどのように影響するかを理解することは非常に困難です。 10個のニューロンのみからなる隠れ層を1つ作成すると、0.01のエラーが非常に迅速に(約100回の繰り返し)達成されますが、そのようなネットワークの柔軟性に関する疑いは忍び込みます。 ほとんどの場合、あなたが彼女に非標準的なゲーム状況を「フィード」すると、彼女は完全に受け入れられない決定を下します。



問題4:トレーニング速度



上記の構成では、ネットワークトレーニングは約2時間続きました(1秒あたり約1.38回の反復)。 結果を得るには数値を試す必要があるため、これはかなり長い時間です。 問題は、ビデオカードではなく、CPU(Intel Core i5-4570)で計算が実行されることでした。 この時点で、CUDAを使用してGPUにコンピューティングを移植することを考えました。 私は多くの資料を掘り下げ、WindowsでNode.jsのCUDAを構成する可能性は実質的に0であるという結論に達しました。はい、ネットワーク計算のみを処理する別のサーバーをLinuxにデプロイできます。 このサーバーはNode.jsではなく、Pythonおよび他の多くのオプションで記述してください。 しかし、ニューラルネットワーク上に構築されたAIバリアントが私の問題を解決するために単に受け入れられない場合はどうでしょうか?



問題5:ゲームの仕組みの特徴



ネットワーク開発の段階で、AIの実装に対する選択されたアプローチの2つの問題に出会いました。





説明してみましょう



  1. すべての能力が行動の1つのモデルに起因するとは限りません。 最も顕著な例は、オラクルの「能力を奪おう」です。 彼女は敵からランダムなポジティブな効果を「盗み」、それを使用した敵に適用します。 問題は明らかです-いくつかの正の効果があります:癒すもの、仲間を保護するもの、戦闘特性を強化するもの、キャラクターの動きを制限するものなどがあります。 補強効果を盗んだ場合、どのような動作になりますか? 自分自身を強化する( 獲得 )か、敵を弱める弱める )か? 実際、両方。 しかし、その効果は治癒することもあります。したがって、これはすでに防御的な行動です。 敵の治療が奪われた場合、 攻撃的です。 したがって、「それを受け入れさせます」という能力は、すべての行動に該当します。 もちろん、これは非常に奇妙です。 この能力は、ランダムな要素を持つ唯一のものではありません。
  2. 動作は特定の状況に対してのみ決定されます。 現時点で何が最善であるかについての決定は、アクティブプレイヤーと対戦相手のプレイヤーの両方の以下のアクションを考慮していません。 状況のモデリングや結果の結果の誤算はありません。


上記の問題はすべて、AIの開発に対する選択されたアプローチの正しさを疑いました。 機械学習に精通しているある同僚は、ニューラルネットワークの代わりに決定木を使用することを提案しました。 これについては、記事の次の部分で説明します...



それまでの間、ご清聴ありがとうございました!



All Articles