群れで迷路を解くAIに憧れて作ってみたら、一番地味な方法に完敗した

  • python
  • 機械学習
  • 強化学習
  • アルゴリズム
  • 遺伝的アルゴリズム

少し前に、こんな動画を見ました。迷路の中をたくさんの点が走り回って、壁にぶつかっては何度も最初からやり直して、そのうち何匹かがスルッと出口にたどり着く。世代を重ねるごとに、群れ全体がだんだん上手になっていく。正直、「こんなの、どうやって作るんだ」と感心しっぱなしでした。

でも、しばらく眺めているうちに、「これ、自分でも作れるんじゃないか」と思えてきて、作ってみることにしました。ついでに、ボタンひとつで「ヨーイドン」と走らせて眺められる観戦画面も用意しました。

自作の観戦画面。赤い群れが迷路の中を探索している

結論から書くと、作っている途中で話が思わぬ方向に転がりました。私が憧れたあの群れに、別の2つの解き方を並べてみたら、一番派手な群れが、一番たくさん失敗して、一番回り道をしていたのです。しかもその「負けっぷり」をたどっていくと、最後は「AIの手法を公平に比べるって、思ったより難しい」という話に行き着きました。順番に書いていきます。

まず、あの群れを再現する

私が見たのは、たぶん遺伝的アルゴリズムと呼ばれるものでした。仕組みはこうです。

  • 1匹の点は、「上・右・下・左…」という動きの手順テープを1本持っているだけ。賢さは持っていません。
  • テープのとおりに歩いて、壁にぶつかった瞬間に力尽きます(そこで止まる)。
  • 1世代が終わったら、全員スタートに戻します。出口に近かった個体ほど、子孫を多く残す。
  • 残った手順を2匹ぶんつなぎ合わせ(交叉)、ときどき1手だけランダムに変えて(突然変異)、次の世代を作る。

ポイントは、点そのものは迷路の地図を知らないことです。盲目のまま、ただテープを再生して、壁にぶつかったら止まる。それを世代で繰り返すだけ。

実際に、15×15の小さな迷路(最短で44歩)に、400匹を放って走らせてみました。動かすと、こうなります。

赤の群れが世代を重ねながら、迷路の中へ経路を伸ばしていく

最初の数十世代は、誰ひとり出口にたどり着けません。毎回どこかの壁で全員が力尽きます。それでも、「出口までの残り距離」だけは、世代を追うごとにジリジリと縮んでいきます。盲目の群れが、地図も知らないのに、出口の“方向”を少しずつ手順に溜め込んでいく。

そして98世代目あたりで、たった1匹がスルッと出口を抜けました。次の数世代で、その勝ちルートが群れ中に広がって、何十匹も出口へ流れ込むようになります。最終的に群れが見つけた一番いいルートは46歩。最短の44歩には、あと一歩届きませんでした。

ここで、作りながら気づいたことがあります。この群れ、1匹1匹は学習していないんです。賢くなっているのは「群れ」のほうで、強かった個体の手順が次の世代に受け継がれているだけ。これは「学習」ではなく「進化」でした。見ていると同じ「ぶつかって賢くなる」に見えるのに、中身がだいぶ違う。

もうひとつ、書いておきたいことがあります。点は盲目ですが、“選別する側”は迷路の地図を見ています。どの個体を残すかを決めるとき、出口からの距離(最短距離)を物差しに使っているのです。つまり、賢さは点ではなく審判の側にありました。盲目の群れを、地図を持った審判が後ろから選り分けている。これは後で効いてきます。

ここに、別の2つを足してみた

群れ=進化だと分かると、「じゃあ、1匹が本当に自分で学習するやつは?」と気になりました。そこで2つ目に Q学習(強化学習の素朴なやつ)を足します。これは1匹だけで、自分が今どのマスにいるかは分かり、「このマスでこっちに行くと痛い」を報酬から覚えて、自分の行動を直していきます。歩くたびに小さな罰、壁にぶつかると大きな罰、出口に着いたらご褒美。まだ歩いていないマスのほうが(罰を受けていないぶん)魅力的に見えるので、放っておいても自分から奥へ探索しにいきます。

ついでに、3つ目として何も学ばないやつも置きました。右手法——右手をずっと壁に当てたまま歩く、昔ながらの方法です。学習も記憶もせず、周りの壁を感じて進むだけ。

最初は「3匹を同じ迷路で同時に競走させよう」と思ったのですが、やめました。よく考えると不公平なんです。赤(進化)は「群れ × 世代」で進み、青(学習)は「1匹 × 試行回数」で進み、黄(右手法)は「一発勝負」。進み方の時計がバラバラなものを、横一線でヨーイドンさせても意味がありません。

そこで、3者に共通の物差しを1つ決めました。経験量=「迷路に何回問い合わせたか」です。あるマスが壁か通路かを確かめた回数、と言ってもいい。移動を試した回数も、壁を感じた回数も、1回は1回として同じ単位で数えます。世代でも秒でもなく、「出口を見つけるまでに、環境に何回ぶつかったり問い合わせたりしたか」。これなら、進み方の違う3者を同じ土俵に乗せられます。

同じ迷路で、経験量で測ってみた

結果がこれです(さっきと同じ、最短44歩の迷路)。観戦画面で3者を走らせて、結果を並べてもらいました。

3者の結果。赤は726,430回かけて46歩、黄は88回で44歩

解き方 初めて脱出するまでの経験 最終的な経路
🔴 赤(遺伝的アルゴリズム) 726,430 回 46 歩
🔵 青(Q学習) 1,530 回 44 歩(最短)
🟡 黄(右手法) 88 回 44 歩(最短)

最短は44歩。並べてみると、私が憧れた赤の群れだけが、桁違いに経験を食って、しかも最短に届かず46歩で止まっている。一方、一番単純そうな黄が、たった88回のやり取りで最短どおりに出口へ。青も最短を当てました。

(赤の経験量だけ「世代単位」でまとめている点だけ補足します。赤は群れで並列に探すので、初めて1匹が抜けた“その世代”までに、群れ全体が問い合わせた回数を数えています。青と黄が「1匹が出口に着いた瞬間まで」なのと、集計の粒度は少し違います。とはいえ桁が違うので、結論は変わりません。)

経験量を時計だと思って3者を眺めると、性格の違いがはっきりします。時計が進むにつれて、まず🟡黄がほんの一瞬で脱出し、少し遅れて🔵青が脱出する。🔴赤は最初の1世代を評価し終わる前に、もう他の2人が解き終わっている。赤がようやく初めて出口を抜けるのは、ずっとずっと後です。

ただ、ここで止めなくてよかった、と思う出来事がありました。この迷路1枚だけを見て、私は一度、間違った結論を書きかけたのです。「黄(右手法)が最短に並んだ!」と。慌てて別の迷路でも確かめたら、景色が変わりました。

迷路(最短) 🟡 黄 🔵 青 🔴 赤
24 24 ✓ 24 ✓ 30
44(さっきの盤) 44 ✓ 44 ✓ 46
44(別の盤) 84 44 ✓ 56
56 88 56 ✓ 60
64 104 64 ✓ 期間内に脱出できず
68 92 68 ✓ 期間内に脱出できず

複数枚で見ると、3者の性格がくっきり出ました。

  • 🔵 青(Q学習)は、試した迷路すべてで最短を当てました。地味ですが、どの迷路でも安定して最短に届く優等生。経験のコストも中くらい。
  • 🟡 黄(右手法)は、いつも試行錯誤ゼロで一番安く脱出します。ただし経路の良し悪しは迷路次第。素直な迷路なら最短、入り組むと最短の2倍近く(最短56に対して88歩、最短64に対して104歩)まで回り道します。最短44の盤が2枚あって、片方は44歩でぴたりなのに、もう片方は84歩。最初に「黄が最短に並んだ」と見えたのは、たまたま右手側に行き止まりが少ない“当たりの盤”だっただけでした。
  • 🔴 赤(遺伝的アルゴリズム)は、どの迷路でも最短に届かず、さらに難しい2枚(最短64・68)では決めた世代数の中で一度も脱出できませんでした。経験の量も飛び抜けて多い。

1枚だけ見ていたら、私は「単純な右手法が最短に並んだ」と書いていたはずです。複数枚まわして、本当に助かりました。

結局、何が分かったのか

「派手な赤が一番ダメだった」——これは半分正しくて、半分ズルい言い方です。振り返ってみると、私は気づかないうちに、3通りのやり方で“レースを仕組んで”いました。

  • 🟡 黄には、壁をタダで見抜くセンサーを与えていた(ぶつからずに避けられる)。
  • 🔵 青には、わざわざ壁にぶつからせてから学ばせていた。
  • 🔴 赤には、点を盲目にしておきながら、選別する審判に迷路の地図(最短距離)という全知の知識を持たせていた。

senses(何を感じられるか)も、前提知識も、経験の数え方も、3者でバラバラだったわけです。だから「経験を共通の物差しで測る」と決めたのは正解で、たぶんそこがこの遊びで一番大事なところでした。手法どうしの比較は、感じられるものと持っている知識を揃えて、経験を同じ単位で数えて、はじめて成り立つ。揃え方を変えれば、同じ手法が賢くも不器用にも見えます。

念のため書いておくと、赤(遺伝的アルゴリズム)が悪い手法というわけではありません。距離を測ってくれる審判がいない問題や、探索空間が広すぎて素直に最短を求められない問題では、進化的なやり方がうまく効く場面があります。今回はたまたま「地図から最短距離がきれいに測れる迷路」だったので、その審判を素直に使えるQ学習や、迷路の構造をそのまま突く右手法のほうに分があった——それだけのことだと思っています。

最後に、まだ分かっていないことも書いておきます。本物の——私が動画で見たような——群れは、たぶん私の素朴な手順テープではなく、ニューラルネットと視線センサーみたいな、もっと賢い作りになっている気がします。それに、前提知識がバラバラな手法を“完全に公平に”比べる正しい物差しを、私はまだ自信を持って言えません。このあたり、詳しい方がいたら教えてください。

おまけ:カクカクを直した話(言語のせいじゃなかった)

この観戦画面、最初は赤の群れがカクカクしていました。眺めていて気になったので、「Pythonが遅いのかな、C++あたりに書き直せば直る?」と一瞬思いました。

でも、直す前にまず測ってみることにしました(この記事、最初からずっとそれが教訓なので)。

アニメは60fpsで描いています。まず、そのコマの間隔を測ってみると、群れが動いている最中でも安定して62fps。時間的には、ぜんぜん詰まっていない。じゃあ何が「カクカク」なのか。今度は、点が1コマでどれだけ動くかを測りました。すると、位置の更新の43.5%が、滑らかに動くのではなく“ワープ”していたのです。

原因は、自分のコードでした。群れの再生を3マス飛ばしで描いていて、それが自分で決めた「これ以上離れていたら瞬間移動でいい」という閾値を超えていた。だから移動のたびにワープ判定になっていたんです。1マスずつ描くように直し、点が目標へ滑らかに追いつく設定を整え直したら、ワープは43.5% → 0.7%まで減りました。残った0.7%は「世代ごとに全員スタートへ戻る」演出なので、これは意図どおり。

結局、言語は1mmも関係ありませんでした。「カクつく → 速い言語に書き直す」ではなく、「カクつく → まず測って、時間の問題なのか位置の問題なのかを切り分ける」。この記事の迷路と、まったく同じ話でした。

試したコードと、迷路の作り

中身は素朴です。Pythonで、

  • 迷路生成(穴掘り法)と、出口からの最短距離マップ(幅優先探索)
  • 🔴 遺伝的アルゴリズム(手順テープ・トーナメント選択・交叉・突然変異)
  • 🔵 Q学習(マスごとの行動価値テーブル、ε-greedy)
  • 🟡 右手法(壁を感じて右・直進・左・後ろの順に進む)
  • 3者を同じ迷路に乗せ、経験量で揃えて測るしくみ
  • そして、上の画面のようなライブ観戦GUI(PySide6・▶Startと🏁Race allで生で見られる)

合わせて数百行ほどの素朴な作りです。コードは GitHub に置いてあります → logicia32/maze-ai-race。クローンして python maze_gui.py を実行すると、上の観戦画面がそのまま立ち上がります。--play=race を付ければ、起動と同時に3者レースが始まります。

さいごに

私はまだ、知らないことばかりです。今回も、面白そうだと思ったものを手探りで作って、測って、比べてみて、ようやく「ああ、こういうことか」と腑に落ちる——その繰り返しでした。便利な道具も増えてきたので、その力も借りながら、これからも少しずつ知識を増やして、学んだことをここ Zenn に残していくつもりです。

もっと良いやり方や、私が見落としているところがあれば、ぜひ教えてください。これからも、少しずつ。

この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る