読者です 読者をやめる 読者になる 読者になる

daruma3940の日記

理解や文章に間違い等あればどんなことでもご指摘お願いします

Aperyの学習部分を読んでみようじぇ

コンピューター将棋

ゆっくりするのじぇ
f:id:daruma3940:20160520223745p:plain
今日はAperyの学習部分を読んでみようじぇ
f:id:daruma3940:20160521003616p:plain
それ絶対難しいわよ..
f:id:daruma3940:20160520223745p:plain
まあまりちゃも読んでもわかんないだろうけど
ありすと一緒に読んでいけばわかるかもしれないのじぇ
f:id:daruma3940:20160520223530p:plain
れいみゅもわすれないでほしいよ!

f:id:daruma3940:20160520223745p:plain
じぇえ....

f:id:daruma3940:20160520223745p:plain
今回は機械学習の細かいところはすっ飛ばして
何をしているのかの一番重要な本質をつかめたらいいなぐらいのものなのじぇ

いきなりソースコード読んで理解できるわけもないので
ちょっと文献とかも見て事前理解をして準備するのじぇ。
まりちゃはインターネットにあった
「将棋の棋譜を利用した大規模な評価関数の学習」
CiNii 論文 -  将棋の棋譜を利用した大規模な評価関数の学習
という論文を読んでちょっと事前知識を身に着けたのじぇ
その論文から得た事前知識としての学習とはこんな感じなのじぇ。

棋譜を読み込む!
棋譜で指された手を教師手とする!
教師手でコンピューターに探索させて帰ってきた評価値を保存 !
コンピューターにも自分でその局面における合法手で探索させてみて
その合法手とその手を指した時に探索して帰ってきた評価値を保存!
そして教師手の指し手の評価値が他の指し手の評価値を上回るように
パラメーターに微分量を足してパラメーターを調整!
これをパラメーターが収束するまで繰り返す!
f:id:daruma3940:20160520223745p:plain
これをAperyも行っているはずなのでどこがそれに相当するのか確認するのが今回の目的なのじぇ。

f:id:daruma3940:20160520223745p:plain
それではApery読んでいくのじぇ。

機械学習についてのコードの本質はlearner.hppの中に書かれていると思うのじぇ
そこからよんでいこうじぇ。
LEARNというプリプロセッサマクロが定義されていないと文字が白くなってしまって読みにくいので
無理やり#define LEARNというのをコードに書き加えたじぇ。

最初に出てきたのは struct RawEvaluaterとかいう構造体なのじぇ。
メンバ変数を見ようじぇ?

std::array<float, 2> kpp_raw[SquareNum][fe_end][fe_end];//サイズが2あるのはこちらから見た値とあいてから見た値?(これが手番付き学習ということ??)
std::array<float, 2> kkp_raw[SquareNum][SquareNum][fe_end];
std::array<float, 2> kk_raw[SquareNum][SquareNum];

f:id:daruma3940:20160521003616p:plain
これは学習中の評価関数ベクトルの値かしら?学習中はfloatで値を持っているのね...
というかこの配列はなんで2つ用意されているのかしら?
f:id:daruma3940:20160520223745p:plain
Aperyは手番付き学習をしているらしいので多分先手後手で使う評価関数ベクトルの値を分けているのだと予想しているのじぇ
....多分。

f:id:daruma3940:20160520223745p:plain
さあこれで RawEvaluater構造体を抜けて
次は lowerDimension関数に行くのじぇ。
Aperyのコメントにはこんなことが書かれていたのじぇ。

kpp_raw, kkp_raw, kk_raw の値を低次元の要素に与える。

f:id:daruma3940:20160520223530p:plain
...低次元の要素って何?
f:id:daruma3940:20160520223745p:plain

topcoder.g.hatena.ne.jp

このサイトのKPP次元下げの説明のところに有るように

Bonanzaメソッドの学習結果に汎化能力を持たせるための手法の一つ。
KPP以外に3駒の相対位置等も特徴量に加えて機械学習をし、これらを後からKPPに加える。
評価値の計算では絶対KPPの配列のみを使えば良い。
通常のKPPは絶対KPP、相対位置の特徴量は相対KPPと呼ばれている。
機械学習の次元削減とは違う意味だと思う。多分。

ということだと思うのじぇ。 しかしこの関数かなりムジカしいことをしているのじぇ。 まりちゃでは理解できないのじぇ... 混乱してきたし これは恐らく本質的な問題では無いので一旦この関数はパスするじぇ。

次は Learnerクラスに映るのじぇ。。
このクラスのメンバ関数の中で本質的だと思われるのは

learnParse1Body()とlearnParse2Body()だと思われるのでここを読み進めてみようじぇ。
この辺りは深夜1時ぐらいに読んだので合ってる保証はないのじぇ
f:id:daruma3940:20160520223745p:plain
learnParse1はまりちゃがざっと読んでみた感じこんな感じなのじぇ。

 void learnParse1Body(Position& pos, std::mt19937& mt) {
        /*uniform_int_distributionは、指定された範囲の値が等確率で発生するよう離散分布するクラスである*/
        std::uniform_int_distribution<Ply> dist(minDepth_, maxDepth_);//探索深さをばらけさせる?

        pos.searcher()->tt.clear();
        for (size_t i = lockingIndexIncrement<true>(); i < gameNumForIteration_; i = lockingIndexIncrement<true>()) {
            StateStackPtr setUpStates = StateStackPtr(new std::stack<StateInfo>());
            pos.set(DefaultStartPositionSFEN, pos.searcher()->threads.mainThread());
            auto& gameMoves = bookMovesDatum_[i];


            //教師データについて
            for (auto& bmd : gameMoves) {
                if (bmd.useLearning) {
                    //alpha betaの設定
                    pos.searcher()->alpha = -ScoreMaxEvaluate;
                    pos.searcher()->beta  =  ScoreMaxEvaluate;
                    go(pos, dist(mt), bmd.move);//指定された差し手(教師手)を用いて探索開始
                    const Score recordScore = pos.searcher()->rootMoves[0].score_;//教師手の評価値を記録
                    ++moveCount_;
                    bmd.otherPVExist = false;
                    bmd.pvBuffer.clear();//pvを0にする??

                    if (abs(recordScore) < ScoreMaxEvaluate) {
                        int recordIsNth = 0; // 正解の手が何番目に良い手か。0から数える。
                        auto& recordPv = pos.searcher()->rootMoves[0].pv_;//教師手
                        bmd.pvBuffer.insert(std::end(bmd.pvBuffer), std::begin(recordPv), std::end(recordPv));//vector2つの連結
                        const auto recordPVSize = bmd.pvBuffer.size();

                        //全合法手に対して
                        for (MoveList<LegalAll> ml(pos); !ml.end(); ++ml) {
                            if (ml.move() != bmd.move) {//教師手以外の差し手を調べる
                                pos.searcher()->alpha = recordScore - FVWindow;//FVWindow(256)の外にあるような評価値となる指し手は考えない方がよい
                                pos.searcher()->beta  = recordScore + FVWindow;
                                go(pos, dist(mt), ml.move());
                                const Score score = pos.searcher()->rootMoves[0].score_;//探索した指し手の評価値

                                //alpha beta内に収まる値が帰ってくれば
                                if (pos.searcher()->alpha < score && score < pos.searcher()->beta) {
                                    auto& pv = pos.searcher()->rootMoves[0].pv_;
                                    bmd.pvBuffer.insert(std::end(bmd.pvBuffer), std::begin(pv), std::end(pv));//pvBufferの連結 (指し手と評価値を記録)
                                }
                                if (recordScore < score)//教師手の価値を超えれば超えた差し手の数を記録?
                                    ++recordIsNth;
                            }
                        }


                        //教師手に近い値(棋譜の手の評価値±256以内)の指し手があったか?(これは全合法手に対してのループで一つでもpvBufferに要素が入れられていればtureになる、。)
                        bmd.otherPVExist = (recordPVSize != bmd.pvBuffer.size());

                        //predictionとは一体何のために用意されている?
                        for (int i = recordIsNth; i < PredSize; ++i)
                            ++predictions_[i];
                    }
                }
                setUpStates->push(StateInfo());
                pos.doMove(bmd.move, setUpStates->top());//教師手で差し手を進める
            }
        }
    }//end of phase1 body

    /*===============================
   教師手を用いて探索をする。その時に帰ってきた評価値を記録する。
    それ以外の指し手を用いて探索する。その時に帰ってきた評価値を記録する。
   ==============================*/
    void learnParse1(Position& pos) {
        Time t = Time::currentTime();
        // 棋譜をシャッフルすることで、先頭 gameNum_ 個の学習に使うデータをランダムに選ぶ。
        std::shuffle(std::begin(bookMovesDatum_), std::end(bookMovesDatum_), mt_);
        std::cout << "shuffle elapsed: " << t.elapsed() / 1000 << "[sec]" << std::endl;
        index_ = 0;
        moveCount_.store(0);//現在の合法手の数
        for (auto& pred : predictions_)
            pred.store(0);
        std::vector<std::thread> threads(positions_.size());
        for (size_t i = 0; i < positions_.size(); ++i)
            threads[i] = std::thread([this, i] { learnParse1Body(positions_[i], mts_[i]); });//thread並列処理
        learnParse1Body(pos, mt_);

        //スレッド終了処理
        for (auto& thread : threads)
            thread.join();

        std::cout << "\nGames = " << bookMovesDatum_.size()
                  << "\nTotal Moves = " << moveCount_
                  << "\nPrediction = ";

        //predictionとは一体何なのか何の確率をここで表示しているのか
        for (auto& pred : predictions_)
            std::cout << static_cast<double>(pred.load()*100) / moveCount_.load() << ", ";
        std::cout << std::endl;
        std::cout << "parse1 elapsed: " << t.elapsed() / 1000 << "[sec]" << std::endl;

    }//end of learn phase1

f:id:daruma3940:20160520223745p:plain
学習phase1 は何個の差し手が教師手の価値を超えたかを調べるphaseだと思うのじぇ。

ここではまず棋譜による教師手でで局面を勧めた時の評価値を算出し
その後それ以外の指し手で探索を行い
その指し手の評価値とその指し手を記録するところだと思われるのじぇ!
ただpredictionというのが何をやっているのかわからないのじぇ
教師手を超えた評価値を持つ指し手の数を格納していると思われるけどなににつかうんだじぇ?

 for (auto& pred : predictions_)
            std::cout << static_cast<double>(pred.load()*100) / moveCount_.load() << ", ";

よくわからないのじぇ。

f:id:daruma3940:20160520223745p:plain
learnParse2はこんな感じなのじぇ。

void learnParse2Body(Position& pos, Parse2Data& parse2Data) {
        parse2Data.clear();//phase2データの初期化
        SearchStack ss[2];

        for (size_t i = lockingIndexIncrement<false>(); i < gameNumForIteration_; i = lockingIndexIncrement<false>()) {
            StateStackPtr setUpStates = StateStackPtr(new std::stack<StateInfo>());

            pos.set(DefaultStartPositionSFEN, pos.searcher()->threads.mainThread());//局面の初期化

            auto& gameMoves = bookMovesDatum_[i];
            for (auto& bmd : gameMoves) {
                PRINT_PV(pos.print());
                if (bmd.useLearning && bmd.otherPVExist) {//教師手の価値と近い手が存在し、教師手は学習に使えるものである
                    const Color rootColor = pos.turn();
                    int recordPVIndex = 0;
                    PRINT_PV(std::cout << "recordpv: ");

                    //pvBufferの頭に格納されているのは正解の手である。 教師手に対して
                    for (; !bmd.pvBuffer[recordPVIndex].isNone(); ++recordPVIndex) {
                        PRINT_PV(std::cout << bmd.pvBuffer[recordPVIndex].toCSA());
                        setUpStates->push(StateInfo());
                        pos.doMove(bmd.pvBuffer[recordPVIndex], setUpStates->top());//phase1で調べた差し手でpvBufferがなくなるまで局面を進めていく?
                    }
                    // evaluate() の差分計算を無効化する。
                    ss[0].staticEvalRaw.p[0][0] = ss[1].staticEvalRaw.p[0][0] = ScoreNotEvaluated;//まだ評価されたことはないフラグ つまり差分計算はなされない

                    const Score recordScore = (rootColor == pos.turn() ? evaluate(pos, ss+1) : -evaluate(pos, ss+1));//正解の手の評価値の計算(phase1で探索させて評価値を記録しているはずでは?)
                    PRINT_PV(std::cout << ", score: " << recordScore << std::endl);

                    for (int jj = recordPVIndex - 1; 0 <= jj; --jj) {//評価値を計算し終われば局面を戻す
                        pos.undoMove(bmd.pvBuffer[jj]);
                    }

                    //sum_DTは目的関数の微分の足し上げ
                    std::array<double, 2> sum_dT = {{0.0, 0.0}};

                    //教師手以外の差し手に対して?
                    for (int otherPVIndex = recordPVIndex + 1; otherPVIndex < static_cast<int>(bmd.pvBuffer.size()); ++otherPVIndex) {
                        PRINT_PV(std::cout << "otherpv : ");
                        for (; !bmd.pvBuffer[otherPVIndex].isNone(); ++otherPVIndex) {
                            PRINT_PV(std::cout << bmd.pvBuffer[otherPVIndex].toCSA());
                            setUpStates->push(StateInfo());
                            pos.doMove(bmd.pvBuffer[otherPVIndex], setUpStates->top());//指し手で局面をすすめる
                        }
                        ss[0].staticEvalRaw.p[0][0] = ss[1].staticEvalRaw.p[0][0] = ScoreNotEvaluated;//差分計算はなされない
                        const Score score = (rootColor == pos.turn() ? evaluate(pos, ss+1) : -evaluate(pos, ss+1));//正解の手以外で進んだ局面で評価値計算(phase1で探索させて評価値を記録しているはずでは?)



                        const auto diff = score - recordScore;//教師手の評価値との差をとる
                        const double dsig = dsigmoid(diff);//シグモイド関数の微分

                        //dTは手番を考慮しているということ??(diff 手番?)
                        //TODO 最終的にどうなるように符号を変えてるんだ??
                        std::array<double, 2> dT = {{(rootColor == Black ? dsig : -dsig), dsig}};
                        PRINT_PV(std::cout << ", score: " << score << ", dT: " << dT[0] << std::endl);
                        sum_dT += dT;//目的関数の微分の足し上げ?(全局面とこの局面の合法手に対しての足し上げ?)
                        dT[0] = -dT[0];
                        dT[1] = (pos.turn() == rootColor ? -dT[1] : dT[1]);
                        parse2Data.params.incParam(pos, dT);//include parameterの更新??これは一体何?次元下げに関係してくる?



                        //局面を戻す
                        for (int jj = otherPVIndex - 1; !bmd.pvBuffer[jj].isNone(); --jj) {
                            pos.undoMove(bmd.pvBuffer[jj]);
                        }
                    }

                    //ここは何をしている?
                    for (int jj = 0; jj < recordPVIndex; ++jj) {
                        setUpStates->push(StateInfo());
                        pos.doMove(bmd.pvBuffer[jj], setUpStates->top());//どんな指し手で局面を進めている?
                    }
                    sum_dT[1] = (pos.turn() == rootColor ? sum_dT[1] : -sum_dT[1]);//手番を考慮して符号を変える。
                    parse2Data.params.incParam(pos, sum_dT);//parse2Data.params.incParam(pos, dT);をしたはずなのになぜまた足している?
                    for (int jj = recordPVIndex - 1; 0 <= jj; --jj) {
                        pos.undoMove(bmd.pvBuffer[jj]);
                    }
                }
                setUpStates->push(StateInfo());
                pos.doMove(bmd.move, setUpStates->top());//教師手で指し手をすすめる?
            }
        }
    }

    /*==================================================================
   phase2
   目的関数の微分の値の正負によってパラメーターを更新
   include parameterとはなに?
   lowerDimension()とはなに?
   incParam()とはなに?
   =====================================================================*/
    void learnParse2(Position& pos) {
        Time t;
        for (int step = 1; step <= stepNum_; ++step) {
            t.restart();//step2にかかる時間の計測
            std::cout << "step " << step << "/" << stepNum_ << " " << std::flush;
            index_ = 0;
            std::vector<std::thread> threads(positions_.size());

            //処理の並列化
            for (size_t i = 0; i < positions_.size(); ++i)
                threads[i] = std::thread([this, i] { learnParse2Body(positions_[i], parse2Datum_[i]); });
            learnParse2Body(pos, parse2Data_);
            for (auto& thread : threads)
                thread.join();


            for (auto& parse2 : parse2Datum_) {
                parse2Data_.params += parse2.params;//parse2Data.params.incParamされたデータの足し上げ?(この足し上げはスレッドに関しての足し上げ)
            }

            parse2EvalBase_.clear();

            //ここが一番何をしているのかわからない また頑張って理解する。
            lowerDimension(parse2EvalBase_, parse2Data_.params);//phase2で得られたparameterを用いて次元下げした値にも足してやる?

            //特徴ベクトルはstep分だけ更新される
            setUpdateMask(step);

            std::cout << "update eval ... " << std::flush;
            //======================================================
            //ここで計算結果を用いて評価値ベクトルを更新しファイルに書き出している!
            //=======================================================
            if (usePenalty_) updateEval<true >(pos.searcher()->options["Eval_Dir"]);
            else             updateEval<false>(pos.searcher()->options["Eval_Dir"]);
            std::cout << "done" << std::endl;
            std::cout << "parse2 1 step elapsed: " << t.elapsed() / 1000 << "[sec]" << std::endl;
            print();
        }
    }//end of learnphase2

f:id:daruma3940:20160520223745p:plain

dTの中の値の符号の変え方は最終的にどうなるように符号を変えているのかわからないのじぇ...
めちゃくちゃ難しいことをしているのじぇApery 平岡さんは天才的だじぇ.... 

これまで見てきた内容によるとAperyの探索部は
棋譜を読み込む!
棋譜で指された手を教師手とする!   phase1
教師手でコンピューターに探索させて帰ってきた評価値を保存!   phase1
コンピューターにも自分でその局面における合法手で探索させてみて  phase1
その合法手とその手を指した時に探索して帰ってきた評価値を保存!  phase1
そして教師手の指し手の評価値が他の指し手の評価値を上回るように  phase2
パラメーターに微分量を足してパラメーターを調整! phase2
これをパラメーターが収束するまで繰り返す!
f:id:daruma3940:20160520223745p:plain
って感じになっていると予想されるのじぇ!
細かいところはかなり高度な内容なのでまた今度読んでいくのじぇ~~

今回の内容は間違ってる可能性大なのであんまり信用しないでほしいのじぇ~

続く

機械学習部を読んでいこうじぇ2 - daruma3940の日記