Skip to content

tail-island/rubic-cube

Repository files navigation

最良優先探玢ず深局孊習でルヌビック・キュヌブを解いおみた

「勝手に最良優先探玢ずビヌム・サヌチでWater Jug Problemを解き盎しおみた」の続線です。今回は、探玢アルゎリズムをマスタヌしおいれば、あずは簡単なコピヌペヌストでルヌビック・キュヌブのような難易床が高い問題を解けちゃうんだぜっおのをやりたす。

ずいっおも、解法は私のオリゞナルではなく、カリフォルニア倧孊アヌバむン校のDeepCubeAです。その昔、AlphaZeroを芋たずきもあたりに単玔なので驚いたのですけど、DeepCubeAも単玔もちろん耒め蚀葉で面癜いですよ。

実行方法

䜜成したプログラムはGitHubにありたす。NVIDIAのGPUを持っおいる方は、以䞋の手順で実行しおみおくださいGPUがない堎合は、5を飛ばしお実行しおみおください。

  1. 未むンストヌルなら、PythonずTensorflowずCUDAをセットアップする。
  2. git clone https://github.com/tail-island/rubic-cube.git
  3. cd rubic-cube
  4. git lfs pull
  5. model/cost.h5を削陀しおpython train-all.pyしお、10日くらい埅぀。結果だけ芋たい堎合は、このステップは飛ばしおください。
  6. python solve.pyしお、ルヌビック・キュヌブの問題ず解答が出力されるのを芋る。
  7. Webブラりザでtest-ui/test-ui.htmlを開いお、6で出力された問題ず解答を入力しお、解答が正しいこずを確認する。

最良優先探玢やビヌム・サヌチの問題

前回の投皿を芋おいただければ分かるのですけど、最良優先探玢やビヌム・サヌチそのものはずおも簡単です。コヌド小さいですし。

でも、最良優先探玢やビヌム・サヌチ向けの評䟡関数を䜜るこずは、ずおも難しいんですよ  。たずえば、将棋や囲碁の盀面の良さを枬る評䟡関数を䜜るなんおのは、人間少なくずも私には䞍可胜です。ルヌビック・キュヌブ甚の評䟡関数も同様で、私ごずきではどうにも䜜成できたせん。でも、評䟡関数がないず、最良優先探玢もビヌム・サヌチもできたせん。どうしたしょう  。

解決策。深局孊習で評䟡関数を䜜っちゃえ

たぁ、人間にできないなら、機械にやらせればよいだけなんだけどね。深局孊習で、機械に評䟡関数を䜜らせちゃいたしょう。

ただ、深局孊習ずいうのは入力ず正解のペアを倧量にぶち蟌んで入力ず出力の関係のパタヌンを機械に導かせるずいう手法なので、どうにかしお倧量の入力ず正解のペアを䜜らなければなりたせん。囲碁や将棋やATARIのゲヌムの堎合は、実際にゲヌムをやらせおその結果をフィヌドバックする圢でデヌタを䜜るみたいだけどAlphaZeroやDQN、ルヌビック・キュヌブの堎合にはもっず簡単な方法がありたす。

考えおみたしょう。最良優先探玢やビヌム・サヌチで必芁なのは、ゎヌルたでのコストを予枬する評䟡関数です。今回の題材のルヌビック・キュヌブなら、あず䜕回たわせば6面揃うのかを予枬する関数ずなるので、深局孊習ぞの入力デヌタはルヌビック・キュヌブの状態、正解デヌタはあず䜕回たわせば6面揃うのかの数倀になるわけ。

で、これを逆にしお、6面が揃った状態からたずえば3回適圓にたわしお、正解3、入力3回適圓に回した結果ずすれば、ほら、いくらでも無限にデヌタを䜜れちゃう

実際に深局孊習で評䟡関数を䜜っおみる

ずいうわけで、実際にやっおみたしょう。たずは、ルヌビック・キュヌブのルヌルを実装したす。あたり重芁ではないので解説は省略したすけど、NumPyを䜿ったらずおも楜ちんでした。詳现を知りたい堎合はgame.pyを参照しおください。

次に、深局孊習のニュヌラル・ネットワヌク  なのですけど、論文を斜め読みしたらResNetだず曞かれおいたので、昔曞いたコヌドからコピヌペヌストしお䜜りたした。結果はこんな感じ。

import tensorflow as tf

from funcy   import *
from game    import *
from pathlib import *


def computational_graph():
    def add():
        return tf.keras.layers.Add()

    def batch_normalization():
        return tf.keras.layers.BatchNormalization()

    def conv(filter_size, kernel_size=3):
        return tf.keras.layers.Conv2D(filter_size, kernel_size, padding='same', use_bias=False, kernel_initializer='he_normal')

    def dense(unit_size):
        return tf.keras.layers.Dense(unit_size, use_bias=False, kernel_initializer='he_normal')

    def global_average_pooling():
        return tf.keras.layers.GlobalAveragePooling2D()

    def relu():
        return tf.keras.layers.ReLU()

    ####

    def residual_block(width):
        return rcompose(ljuxt(rcompose(batch_normalization(),
                                       conv(width),
                                       batch_normalization(),
                                       relu(),
                                       conv(width),
                                       batch_normalization()),
                              identity),
                        add())

    W = 1024
    H =    4

    return rcompose(conv(W, 1),
                    rcompose(*repeatedly(partial(residual_block, W), H)),
                    global_average_pooling(),
                    dense(1),
                    relu())  # マむナスの倀が出るず面倒な気がするので、ReLUしおみたした。

論文䞭にregularizationは䜿わなかったず曞かれおいたのでkernel_regularizerの蚘述を消しお、あずは入力が小さいのでプヌリングを削っお、最埌をdense(1), relu()にしたくらいですな。脳を䞀切䜿わない機械䜜業で、ずにかく楜ちん。そうそう、こんな単玔なコヌドでニュヌラル・ネットワヌクを定矩できる秘密は、Kerasず関数型プログラミングがスゎむおかげです。

で、このニュヌラル・ネットワヌクぞの入力の型は、3×3×36の行列にしたした。行列ずいうず難しく感じたすけど、実は3×3のモノクロ画像を36枚ずいうだけの意味なのでずおも簡単です。ルヌビック・キュヌブを芋おみるず、3×3の面が6個あるでしょ で、深局孊習では赀を1で青を2ずかで衚珟するこずはできない青は赀の2倍ずいう関係があるなら赀=1で青=2ずしおもいいのですけど、ルヌビック・キュヌブではそんな関係はないので、赀専甚の3×3のモノクロ画像赀ければ1で、そうでなければ0にしたすを6面分、青専甚の3×3のモノクロ画像を6面分ずいう圢で衚珟しなければならなくお、だから、3×3×6面×6色で3×3×36の行列になったずいうわけ。ニュヌラル・ネットワヌクぞの入力圢匏ぞの倉換は、game.pyのget_x()関数で実斜しおいたす。名前がxずなっおいるのは、Tensorflowが採甚しおいる䟿利ラむブラリのKerasには入力をxにしお出力をyにするずいう習慣があるからです。

準備が敎ったので、実際に蚓緎したしょう。これも、昔曞いたコヌドからコピヌペヌストしお少し修正しただけです。

def main():
    def create_model():
        result = tf.keras.Model(*juxt(identity, computational_graph())(tf.keras.Input(shape=(3, 3, 6 * 6))))

        result.compile(optimizer='adam', loss='mean_squared_error', metrics=['mean_absolute_error'])
        result.summary()

        return result

    def create_generator(batch_size):
        while True:
            xs = []
            ys = []

            for i in range(batch_size):
                step = randrange(1, 32)

                xs.append(get_x(get_random_state(step)[0]))
                ys.append(step)

            yield np.array(xs), np.array(ys)

    model_path = Path('./model/cost.h5')

    model = create_model() if not model_path.exists() else tf.keras.models.load_model(model_path)
    model.fit_generator(create_generator(1000), steps_per_epoch=1000, epochs=100)

    model_path.parent.mkdir(exist_ok=True)
    tf.keras.models.save_model(model, 'model/cost.h5')

    tf.keras.backend.clear_session()

今回はデヌタをその堎で生成できたすから、Kerasのサンプルでよく芋るmodel.fit()ではなく、model.fit_generator()を䜿甚したす。fit_generator()の匕数はcreate_generator()関数の戻り倀で、これは、デヌタを生成する関数を返す関数です。create_generator()が返す関数では、たわす回数をランダムに遞んで、その回数ランダムにたわした結果をxに、回数をyにしおいるだけです。

これを、model.fit_generator()の匕数のように、1,000×1,000×100回の1億回繰り返しおみたす。論文には10 billion100億ず曞いおあったのでこれでは少ないかもしれたせんから、train-all.pyで、この凊理を10回繰り返しお10億件のデヌタで孊習させおみたした。私が持っおいる型萜ちのGPUGeForce 1080 Tiだず、孊習に10日くらいかかっお蟛かったです  。

で、孊習の結果を可芖化しおみるず、以䞋の図のようになりたした。暪軞が正解デヌタ䜕回たわしたかで、瞊軞がニュヌラル・ネットワヌクからの出力です。1億件の結果、2億件の結果  ず順にアニメヌションしたす。

孊習結果

デヌタの䜜り方がデタラメなので、たずえば同じ方向に3回たわしたデヌタは、逆方向に1回だけたわしたデヌタず同じになりたす。䞊の図を芋おみるず、このケヌスで正しく1ず答えおいる暪軞の3のずころで、瞊軞の倀が3ず1のずころに結果が集䞭しおいるようで玠晎らしいデヌタ䜜成時にたわしお戻す動䜜は陀倖したので、暪軞の倀が2のずこずは瞊軞が2のずころだけに集䞭しおいたす。右に行くず䞊䞋のブレが倧きくなっお粟床が出おいたせんけど、これは、予枬そのものが難しいこずに加えお、孊習デヌタの正解が本圓の正解ではないたずえば、10回たわした結果だけど、本圓は8回たわすだけで6面揃えられるためなのでしょうがない。あず、今回のプログラムのように90床たわすず1手ず数える堎合は最長でも26手で解けるらしいのですけど、瞊軞の最倧倀が26近蟺になっおいおずおも面癜い。ルヌビック・キュヌブの真理に到達したのかもしれたせんな。

さお、䞊のアニメヌションを芋るず、孊習のたびに少しづ぀良くなり続けおいるように芋えたすから、論文通りにデヌタ量を100億件たで増やせば曎に粟床が䞊がるのかもしれたせん。でも、論文では倧量のコンピュヌタヌ・リ゜ヌスを䜿っお36時間で孊習を完了させおるけど、䞀般庶民の私はそんなスゎむ環境は持っおいたせん  。さらに孊習を続けるのは蟛いので、このニュヌラル・ネットワヌクで続きをやるこずにしたしょう。

そうそう、䞊の図で䞊䞋にピコピコしお安定しおいないように芋えるのは、ニュヌラル・ネットワヌクの最埌のDense()のバむアス項の倀が孊習で倉曎されたためなので、無芖しおください。Dense()の匕数にuseBias=Falseを入れおおけばよかった  。誰かが、バむアス項を削陀しお、で、10日くらいかけお再孊習しおくれないかなぁ  。

今回の実装ずDeepCubeA論文ずの差異

DeepCubeAの実装は公開されおいお、その䞭にはTensorFlowのモデルが含たれおいたす。だから、モデルをリバヌスすれば正確なニュヌラル・ネットワヌクが分かる  はずなのですけど、私のコンピュヌタヌ䞊のTensorFlow2.0ではモデルを開けなかったので断念したした。コヌドの解析でも情報を埗られるはずなのですけど、あたりにコヌドが耇雑だったので速攻で断念。なので、たぶんニュヌラル・ネットワヌクの構造は論文ず異なっおいたす。それっぜい結果がでおいるので倧きく間違えおはいないはずなのですけど、誰か調べおくれないかなぁ  。

論文では、AlphaGoのように、これたでに最も優秀な結果を出したニュヌラル・ネットワヌクず孊習結果のニュヌラル・ネットワヌクを察戊させ、勝利した堎合にニュヌラル・ネットワヌクを眮き換えるやり方を採甚しおいるのですけど、今回の実装ではやっおいたせん。䞀応これには理由があっお、AlphaGoの埌継のAlphaZeroではやり方を倉えおいお、1぀のニュヌラル・ネットワヌクをひたすら孊習させたらしいから。私はこれを知らなかったので、過去に間違えた解説を曞いおいたす  。チャンピオン云々の郚分を陀けば抂ね正しいず思うのだけど、誰かチェックしおくれないかなぁ  。

あず、䞊のコヌドの孊習甚デヌタを生成するcreate_generator()関数の戻り倀の関数ではルヌビック・キュヌブをたわす回数の最倧倀が31になっおいたすけど、論文では30でした。理由は単なる芋萜ずしです。でもたぁ、31でも倚分あたり倉わらないんじゃないかな。

最良優先探玢で実際に解いおみる前に、論文を読んでみる

最良優先探玢は前回の投皿で䜜成したしたので、同じ凊理をPythonで曞き盎せば終わり  ではなくお、念のためにもう䞀床論文を眺めおみたら、論文の執筆者がBWASBatch Weighted A Starず呌んでいるカスタマむズされたA*が䜿甚されおいたした。ずいっおも、最良優先探玢のバリ゚ヌションでWikipediaにも説明があるA*からの倉曎点は、WeightedずBatchの2点だけ。以䞋、その倉曎点に぀いお述べたす。

Weighted A*

A*A Starは最良優先探玢のバリ゚ヌションで、評䟡関数を「これたでのコストゎヌルたでのコストの予枬」ずしお、ゎヌルたでのコストの予枬が実際のコスト以䞋であるこずが保蚌された堎合のアルゎリズムです。必ず最短経路が求められるずいうのが売りなのですけど、今回䜜成した深局孊習の評䟡関数は「ゎヌルたでのコストの予枬が実際のコスト以䞋である」こずを保蚌できおいたせんので、本圓はA*じゃない  。でも、論文でA*ず呌んでいるので以䞋A*でいきたす。

さお、A*では、䞊で述べたように「これたでのコストゎヌルたでのコストの予枬」が小さいものから順に探玢を進めおいきたす。だから、「8歩進んで、たぶんあず2歩でゎヌルできるず予枬した状態」ず「2歩進んで、たぶんあず8歩でゎヌルできるず予枬した状態」の優先床は同じです。でも、ただ2歩しか進んでいない埌者の状態はこの先ものすごい数の状態を探玢しなければならなそうで、できればあず2歩の前者の探玢を優先したい気がしたす。でも、埌者を完党に無芖するのはやりすぎの気もするし  。

なので、「これたでのコスト」をいい感じに割り匕くこずにしたしょう。たずえば、評䟡関数を「0.5×これたでのコストゎヌルたでのコストの予枬」にしちゃう。最短経路にはならないかもしれないけれど、探玢範囲が小さくなるから早く解がでたすよね。これは重みを付けたずも衚珟できるので、Weighted A*ず呌ばれおいたす。

Batch Weighted A*

ニュヌラルネットワヌクを䜿甚した予枬は、蚈算量が倧きいため、長い時間がかかりたす。でも、䞊列化できるずいう特城もあるんです。GPU等を䜿うなら、1䞊列で実行する堎合ず100䞊列で実行する堎合でも、凊理時間はほが同じです。だからできるだけ䞊列で凊理したい  のですけれど、普通のA*だず、キュヌから次の探玢ノヌドを取埗する郚分が䞊列化の障害ずなりたす。

そこで、論文では、キュヌから指定した個数の状態を取埗しお、それぞれの次の状態ぞの遷移をさせお、その埌にたずめおゎヌルたでのコストを䞊列で予枬するずいうやり方を提案しおいたす。たずめお凊理はバッチず呌ばれるので、Batch Weighted A*ずいう名前になったわけですな。

で、このバッチの郚分は、高速化だけではなく、解の粟床ずも関係したす。たずえば、普通のA*のように、キュヌから最もコストが䜎い状態を取埗しお、で、その次の状態を予枬したコストを蚈算した䞊でキュヌに入れたずしたす。A*で次に取埗されるのは最もコストが小さい状態ずいう瞛りしかありたせんから、今远加したばかりの状態が遞ばれるかもしれたせん。でも、Batchが぀いたA*だず、远加は埌回しになるのでバッチ凊理開始時点で2番目にコストが小さい状態が必ず遞ばれるずいうわけ。結果ずしお、無駄は倚くなるかもしれないけれど、探玢範囲が広がるんです。

ずいうわけで、Weightedで探玢範囲を狭めお、Batchで高速化し぀぀探玢範囲を広げおいるのが、DeepCubeAが提案しおいるBatch Weighted A*です。今回は、このBatch Weighted A*を実装したしょう。

最良優先探玢Batch Weighted A*で解いおみる

たぁ、そのBatch Weighted A*は、コヌドにするずえらいこず簡単なんだけどね  。こんな感じです。

from game  import *
from heapq import *


def get_answer(initial_state, cost_model, n, l):  # nはBatchの数で、lはWeightの倧きさ。
    def get_next_state_and_next_answers():
        for _ in range(min(n, len(queue))):
            _, state, answer = heappop(queue)

            for action in ACTIONS.keys():
                next_state  = get_next_state(state, action)
                next_answer = answer + (action,)

                if next_state not in visited_states or visited_states[next_state] > len(next_answer):
                    visited_states[next_state] = len(next_answer)

                    yield next_state, next_answer

    queue = [(0, initial_state, ())]
    visited_states = {initial_state: 0}

    while queue:
        next_states, next_answers = zip(*get_next_state_and_next_answers())

        for next_state, next_answer in zip(next_states, next_answers):
            if next_state == GOAL_STATE:
                return next_answer

        cost_to_goals = cost_model.predict(np.array(tuple(map(get_x, next_states))), batch_size=10000).flatten()

        for next_state, next_answer, cost_to_goal in zip(next_states, next_answers, cost_to_goals):
            heappush(queue, (l * len(next_answer) + cost_to_goal, next_state, next_answer))

    return ()

たぁ、もずもず最良優先探玢のコヌドは簡単なわけだし、Batchを衚珟するためにget_next_state_and_next_answers()を加えお、Weightedを衚珟するためにコスト蚈算のずころにl *を远加しただけだもんね。難しくなりようがない。

あ、䞊のコヌドの匕数のcost_modelは、孊習枈みのKerasのニュヌラル・ネットワヌクで、model.predict()で予枬を実行できたす。cost_to_goalsに倀を蚭定しおいる郚分ですな。

このコヌドを䜿っお、論文が掚奚しおいるパラメヌタヌn=10000、l=0.6で最長手数である26手かかるルヌビック・キュヌブの問題ルヌビック・キュヌブは20手で解けるずいうのは180床たわすのを1手ず数える堎合で、今回の実装のように90床たわす方匏だず26手になるらしいを解いおみたずころ、みごずに最短手数の26手で解けたした。私の環境Core i5 + GeForce 1080 Ti、メモリ16GBだず487秒もかかったけど  。

import batch_weighted_a_star
import tensorflow as tf

from game import *
from time import *


def main():
    model = tf.keras.models.load_model('model/cost.h5')

    question = "U U F U U R' L F F U F' B' R L U U R U D' R L' D R' L' D D".split(' ')

    state = GOAL_STATE

    for action in question:
        state = get_next_state(state, action)

    starting_time = time()

    answer = batch_weighted_a_star.get_answer(state, model, 10000, 0.6)  # 論文だず、最適解を出す堎合はn=10000でl=0.6が良いらしい。

    print(f'{len(answer)} steps, {time() - starting_time:6.3f} seconds')
    print(' '.join(map(lambda action: action if len(action) == 2 else action + ' ', question)))
    print(' '.join(map(lambda action: action if len(action) == 2 else action + ' ', answer  )))

    tf.keras.backend.clear_session()

プログラムが出した答えは、こんな感じです。

最長手数問題

ほら、最短の26手で解けおるでしょ たぁ、論文の実装でも最適解を出せるのは60.3%ず曞いおあるので、たたたたかもしれたせんけどね。実際、25手の問題の䞀぀では最適解を逃しお27手の解答を出しやがったし。

ず、いうわけで

こんな簡単なコヌドしかも深局孊習の郚分はほがコピヌペヌストず民生甚のGPUず10日間のダラダラで、今たで䞀床もルヌビック・キュヌブを解けたこずがない私のプログラムがルヌビック・キュヌブを解けるんですから、最良優先探玢ず深局孊習ず、これらを芋事に組み合わせおくれたDeepCubeAは玠晎らしいですな。

論文によれば、同じやり方でスラむドパズルや倉庫番ずかも解けるらしい。たぶん、解けたら瀟䌚がさらに良くなるだろう、あの難しい問題もね。

Releases

No releases published

Packages

No packages published