LoginSignup
3
3

More than 5 years have passed since last update.

コンピュータ将棋ソフトとの対局サーバーを立てよう<その5>

Last updated at Posted at 2017-03-08

前回の記事 : http://qiita.com/muzudho1/items/3ca7bade03f7c2fd8292

プログラム常駐、プロセス間通信を諦め、Expectでプロセスを叩くことにしたのだった。
ホームページで画像を見るのと同じように、コンピュータ将棋ソフトの指し手を取ってこようというのだ。富豪プログラミングもいいところだ。

速度を測定したところ、

  • (1)Webアクセス 0.000012 秒
  • (2)1+Expect使用 0.022 秒
  • (3)1+2+Apery稼働 3.61 秒

ぐらいだったので、Aperyのソースを眺め、3.587988 秒(3.61 - 0.022 - 0.000012)の内訳を調査しよう。

1手の指し手を 1秒未満 にしたいので、Aperyの初期設定時間から 2.6 秒削ればいいはずだ。

  • 何をやっていて
  • だから諦めろ、納得しろ、演出で体感時間を 2.6 秒縮めろ

などの説明ができればいいかと思う。

続く。

浮かむ瀬のソースを追いかけよう。

main.cpp 抜粋

// 将棋を指すソフト
int main(int argc, char* argv[]) {
    initTable();
    Position::initZobrist();
    HuffmanCodedPos::init();
    auto s = std::unique_ptr<Searcher>(new Searcher);
    s->init();
    s->doUSICommandLoop(argc, argv);
    s->threads.exit();
}
  • ビットボードでも初期化してるのだろうか
  • 局面ハッシュを作るためのゾブリスト・ハッシュでも捏ねているのだろう
  • ハフマン符号!? 推測すると多分、指し手を 256ビットに納めたいんじゃないか
  • サーチャーってのは 指し手のツリー探索だ。
    • まあ、サーチャーを初期化するだろ
    • ループするだろ
    • スレッドから抜けるわな

もう、どこを 2.6秒も縮める っていうね。とりあえず ざっと眺めてみよう。

続く。

initTable

init.cpp 抜粋

void initTable() {
    initAttacks(false);
    initAttacks(true);
    initKingAttacks();
    initGoldAttacks();
    initSilverAttacks();
    initPawnAttacks();
    initKnightAttacks();
    initLanceAttacks();
    initSquareRelation();
    initAttackToEdge();
    initBetweenBB();
    initCheckTable();
    initNeighbor5x5();
    initSquareDistance();

    Book::init();
    initSearchTable();
}
  • initAttacks()~initSquareDistance() というのは多分、ビットボードを初期化しているのだろう。駒の動かし方とか利きを 全パターン最初に作っておくわけだ
  • Book::init(); は 定跡ファイル を読込んでいるのだろう。使いたくない時は定跡ファイルを消しておけばいい。これ、コメントアウトしたらどうだろうか?
  • initSearchTable() は何だろうか

よーし、お父さん Book::init(); をコメントアウトしちゃうぞ!

    // Book::init();

そういえば、改造するほど弱くなるという説がある。確か名前を きふわらぷりー とか言ったな。
レーティング 3300 の大樹の枝を レーティング 2600 ぐらいまで負の方向へ引き上げたことがある。Aperyの改造は任せろ!

cp -R /home/★user/shogi/ukamuse_sdt4/ /home/★user/shogi/ukamuse_sdt4_child

ディレクトリーをコピーしとこ。チャイルドの方を改造していくことにする。

# cd ukamuse_sdt4_child/src
# nano init.cpp
# nano Makefile

profgen_sse でコンパイルするか……。 g++ は入れてある。

# make profgen_sse

はい、コンパイルおっけ。apery という実行ファイルがでけた。
あっ、名前変えておくか。

# mv apery ../bin/apery_child

いつか間違えそうだよな。apery を apery_child で上書くよな。

# cd ../bin

とりあえず比較用の環境を作っておこう。浮かむ瀬チャイルドを叩き起こすやつ。

tamesi24a3.php

<?php
$time=microtime(true);
exec('/usr/bin/expect /home/★user/shogi/expect_service/tamesi24a3.expect '.urldecode($_SERVER['QUERY_STRING']),$o);echo $o[count($o)-1];
echo '<br />' . (microtime(true) - $time);

tamesi24a3.expect

#!/usr/bin/expect --
cd /home/★user/shogi/ukamuse_sdt4_child/bin
set timeout -1
spawn ./apery_child
send "usi\n"
expect -ex "usiok"
send "isready\n"
expect -ex "readyok"
send "usinewgame\nposition ${argv}\ngo\n"
expect -re "bestmove*" {
  exit
} "checkmate nomate *" {
  send "quit\n"
  send_user "successful 0.0\n"
} "*info time 1*" {
  send "quit\n"
  send_user "error 3.1\n"
} "checkmate timeout *" {
  send "quit\n"
  send_user "error 3.2\n"
} "*bad_alloc*" {
  send "quit\n"
  send_user "error 3.3\n"
} "*Out of memory*" {
  send "quit\n"
  send_user "error 3.4\n"
} "*recursively*" {
  send_user "error 3.5\n"
} "checkmate *" {
  send "quit\n"
  send_user "successful 1.0\n"
}

2行目のディレクトリー名に「_child」と付けて、
4行目の 「spawn ./apery」を「spawn ./apery_child」に変えるだけのためにファイルを2枚もコピーして増産したが、これが コピペ・マン だ。
ためしさん の数字もどんどん増えていく。

なんか、フォルダー名で分けているから、ファイル名にまで _child を付けなくてもいい気がしてきた。変えとこ。

動かしてみた。

調べるまでもなく変わってない。3.2秒とか、3.7秒とか、そんな感じ。
関数呼び出し1個と 少々の論理演算を削っただけか。ミクロ、マイクロ、ナノの世界だ。

isready と、readyok の間の時間が大きいのでは?

そういえばそうだった。

USIループを見に行こう。

usi.cpp 抜粋

void Searcher::doUSICommandLoop(int argc, char* argv[]) {
    bool evalTableIsRead = false;
    Position pos(DefaultStartPositionSFEN, threads.main(), thisptr);

    std::string cmd;
    std::string token;

    for (int i = 1; i < argc; ++i)
        cmd += std::string(argv[i]) + " ";

    do {
        if (argc == 1 && !std::getline(std::cin, cmd))
            cmd = "quit";

        std::istringstream ssCmd(cmd);

        ssCmd >> std::skipws >> token;

        if (token == "quit" || token == "stop" || token == "ponderhit" || token == "gameover") {
            if (token != "ponderhit" || signals.stopOnPonderHit) {
                signals.stop = true;
                threads.main()->startSearching(true);
            }
            else
                limits.ponder = false;
            if (token == "ponderhit" && limits.moveTime != 0)
                limits.moveTime += timeManager.elapsed();
        }
        else if (token == "go"       ) go(pos, ssCmd);
        else if (token == "position" ) setPosition(pos, ssCmd);
        else if (token == "usinewgame"); // isready で準備は出来たので、対局開始時に特にする事はない。
        else if (token == "usi"      ) SYNCCOUT << "id name " << std::string(options["Engine_Name"])
                                                << "\nid author Hiraoka Takuya"
                                                << "\n" << options
                                                << "\nusiok" << SYNCENDL;
        else if (token == "isready"  ) { // 対局開始前の準備。
            tt.clear();
            threads.main()->previousScore = ScoreInfinite;
            if (!evalTableIsRead) {
                // 一時オブジェクトを生成して Evaluator::init() を呼んだ直後にオブジェクトを破棄する。
                // 評価関数の次元下げをしたデータを格納する分のメモリが無駄な為、
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }
            SYNCCOUT << "readyok" << SYNCENDL;
        }
        else if (token == "setoption") setOption(ssCmd);
        else if (token == "write_eval") { // 対局で使う為の評価関数バイナリをファイルに書き出す。
            if (!evalTableIsRead)
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
            Evaluator::writeSynthesized(Evaluator::evalDir);
        }
#if defined LEARN
        else if (token == "l"        ) {
            auto learner = std::unique_ptr<Learner>(new Learner);
            learner->learn(pos, ssCmd);
        }
        else if (token == "make_teacher") {
            if (!evalTableIsRead) {
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }
            make_teacher(ssCmd);
        }
        else if (token == "use_teacher") {
            if (!evalTableIsRead) {
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }
            use_teacher(pos, ssCmd);
        }
        else if (token == "check_teacher") {
            check_teacher(ssCmd);
        }
        else if (token == "print"    ) printEvalTable(SQ88, f_gold + SQ78, f_gold, false);
#endif
#if !defined MINIMUL
        // 以下、デバッグ用
        else if (token == "bench"    ) {
            if (!evalTableIsRead) {
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }
            benchmark(pos);
        }
        else if (token == "key"      ) SYNCCOUT << pos.getKey() << SYNCENDL;
        else if (token == "tosfen"   ) SYNCCOUT << pos.toSFEN() << SYNCENDL;
        else if (token == "eval"     ) std::cout << evaluateUnUseDiff(pos) / FVScale << std::endl;
        else if (token == "d"        ) pos.print();
        else if (token == "s"        ) measureGenerateMoves(pos);
        else if (token == "t"        ) std::cout << pos.mateMoveIn1Ply().toCSA() << std::endl;
        else if (token == "b"        ) makeBook(pos, ssCmd);
#endif
        else                           SYNCCOUT << "unknown command: " << cmd << SYNCENDL;
    } while (token != "quit" && argc == 1);

    threads.main()->waitForSearchFinished();
}

これが USIプロトコル で通信しているところだ。
中でも isready が関係しているのは、

        else if (token == "isready"  ) { // 対局開始前の準備。
            tt.clear();
            threads.main()->previousScore = ScoreInfinite;
            if (!evalTableIsRead) {
                // 一時オブジェクトを生成して Evaluator::init() を呼んだ直後にオブジェクトを破棄する。
                // 評価関数の次元下げをしたデータを格納する分のメモリが無駄な為、
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }
            SYNCCOUT << "readyok" << SYNCENDL;
        }

この部分。最後に "readyok" を出力しているのが分かるだろう。

  • tt : トランス・ポジション・テーブル。将棋では 指す順番が違うだけで 合流して同じ局面になることがある。そういう重複を省くために局面ハッシュを記憶しているもの。ハッシュ・サイズを小さくするのは手かもしれないが、瞬時だと思う。
  • previousScore : 多分、アルファベータ探索の初期値を入れているのではないか。最初は無限大からスタートしてどんどん下げていく。限りなく0秒だろ。
  • init(Evaluator::evalDir, true); 評価関数を読込んでいるんだろうか? 多分、これが時間かかってるんだろ

C++ でストップウォッチはどう書くんだろうか。

と、その前に。

evaluate.hpp

evaluate.hpp 抜粋

    void init(const std::string& dirName, const bool Synthesized, const bool readBase = true) {
        // 合成された評価関数バイナリがあればそちらを使う。
        if (Synthesized) {
            if (readSynthesized(dirName))
                return;
        }
        if (readBase)
            clear();
        readSomeSynthesized(dirName);
        if (readBase)
            read(dirName);
        setEvaluate();
    }

3駒関係とかで有名な 評価データを読込んでいる。
Synthesized というのは、学習したバラバラの評価データを、3駒関係の形にまとめたバカでかい3つのファイルだ。

aperyのbin ディレクトリに入っている。

ファイル名 内容 容量
KK_synthesized.bin 王×玉 52KB
KKP_synthesized.bin 王×玉×その他 77.4 MB
KPP_synthesized.bin (王か玉)×その他×その他 740 MB

簡単に言えば、およそ 817.4 メガ・バイト の画像を読込んでいるようなものだ。
多分、どう考えても この読込みは重い。

で、C++ でストップウォッチはどう書くんだろうか。

と、その前に。

さっきから 浮かむ瀬じゃなくて Apery のソースを見ているが……。

多分 だいたい同じだろ。

評価関数テーブルを毎回読込まない方法

評価関数テーブルは、コンピュータ将棋の 強さ なので、これを削ることができたら わたしが将棋電王トーナメントの2位になるんで、「2.6秒縮めてください」と言われたら Webサーバーを止めて ゲーム・サーバーにしましょう というところなんだが、もう1つ解決方法がないわけでもない。

浮かむ瀬は停止することなく常駐させておいて(それがなぜかできないのだった)、間にサービス(名前を仮に 指し手ブローカー三河屋 としよう)を1つ置き、ブラウザでURLを叩くと 指し手ブローカー三河屋 を叩き起こし、指し手ブローカー三河屋 は浮かむ瀬に指し手を問い合わせ、Webページに出したあと停止するというものだ。

だから それができないというのが この記事の <その1>~<その3> なのだった。

とりあえず C++でストップウォッチを使う方法を調べよう。

ところで atコマンド というのがあるらしい

調べるか……。

「at」 (Linuxコマンド集)
http://itpro.nikkeibp.co.jp/article/COLUMN/20060227/230711/?rt=nocnt

「指定日時にプログラムを動かす」 (Linuxコマンド逆引き大全)
http://itpro.nikkeibp.co.jp/article/COLUMN/20060228/231173/

うーむ。

ところで KPP_synthesized.bin を読込まなかったらどれぐらいの強さなのかについて

PP がいなくなる。
KK、KKP、KPP があって、KPP がいなくなると、PP の組み合わせがいなくなるんで、2駒関係より弱くなる。

駒割りだけで どれぐらいの強さなのかについて

わからん。うーむ。
ためしに 駒割りだけ浮かむ瀬 にしてみるか。

やり方は 単純に考えると、

uci.cpp 抜粋

        else if (token == "isready"  ) { // 対局開始前の準備。
            tt.clear();
            threads.main()->previousScore = ScoreInfinite;
            if (!evalTableIsRead) {
                // 一時オブジェクトを生成して Evaluator::init() を呼んだ直後にオブジェクトを破棄する。
                // 評価関数の次元下げをしたデータを格納する分のメモリが無駄な為、
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }
            SYNCCOUT << "readyok" << SYNCENDL;
        }

この部分を……。

        else if (token == "isready"  ) { // 対局開始前の準備。
            tt.clear();
            threads.main()->previousScore = ScoreInfinite;
//            if (!evalTableIsRead) {
//                // 一時オブジェクトを生成して Evaluator::init() を呼んだ直後にオブジェクトを破棄する。
//                // 評価関数の次元下げをしたデータを格納する分のメモリが無駄な為、
//                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
//                evalTableIsRead = true;
//            }
            SYNCCOUT << "readyok" << SYNCENDL;
        }

こうすればいいんじゃないか。 何が起こるか分からないがやってみよう。

そういえば 平岡さんは言ってたぜ。

探索と評価関数は車の両輪。両方同時にいじると強くなったか分からないので、片方ずつ強くしていくんだぜ。

そして今 自転車の前輪を外すんだぜ。

駒割り浮かむ瀬 初手 0.7秒 1g1f ……、端歩だ!

ところで駒割りなんだが

pieceScore.hpp 抜粋

const Score PawnScore             = static_cast<Score>( 100 * 9 / 10);
const Score LanceScore            = static_cast<Score>( 350 * 9 / 10);
const Score KnightScore           = static_cast<Score>( 450 * 9 / 10);
const Score SilverScore           = static_cast<Score>( 550 * 9 / 10);
const Score GoldScore             = static_cast<Score>( 600 * 9 / 10);
const Score BishopScore           = static_cast<Score>( 950 * 9 / 10);
const Score RookScore             = static_cast<Score>(1100 * 9 / 10);
const Score ProPawnScore          = static_cast<Score>( 600 * 9 / 10);
const Score ProLanceScore         = static_cast<Score>( 600 * 9 / 10);
const Score ProKnightScore        = static_cast<Score>( 600 * 9 / 10);
const Score ProSilverScore        = static_cast<Score>( 600 * 9 / 10);
const Score HorseScore            = static_cast<Score>(1050 * 9 / 10);
const Score DragonScore           = static_cast<Score>(1550 * 9 / 10);

const Score KingScore             = static_cast<Score>(15000);

const Score CapturePawnScore      = PawnScore      * 2;
const Score CaptureLanceScore     = LanceScore     * 2;
const Score CaptureKnightScore    = KnightScore    * 2;
const Score CaptureSilverScore    = SilverScore    * 2;
const Score CaptureGoldScore      = GoldScore      * 2;
const Score CaptureBishopScore    = BishopScore    * 2;
const Score CaptureRookScore      = RookScore      * 2;
const Score CaptureProPawnScore   = ProPawnScore   + PawnScore;
const Score CaptureProLanceScore  = ProLanceScore  + LanceScore;
const Score CaptureProKnightScore = ProKnightScore + KnightScore;
const Score CaptureProSilverScore = ProSilverScore + SilverScore;
const Score CaptureHorseScore     = HorseScore     + BishopScore;
const Score CaptureDragonScore    = DragonScore    + RookScore;
const Score CaptureKingScore      = KingScore      * 2;

const Score PromotePawnScore      = ProPawnScore   - PawnScore;
const Score PromoteLanceScore     = ProLanceScore  - LanceScore;
const Score PromoteKnightScore    = ProKnightScore - KnightScore;
const Score PromoteSilverScore    = ProSilverScore - SilverScore;
const Score PromoteBishopScore    = HorseScore     - BishopScore;
const Score PromoteRookScore      = DragonScore    - RookScore;

const Score ScoreKnownWin = KingScore;

コンピューター将棋の強さは 3駒関係 の表の空いているところをどれだけ学習でカバーして埋めていくかによるところが大きいが、
3駒関係評価値とは別に 駒には点数が付いており、駒の点数 は将棋というゲームの形勢判断に大きな役割を果たしている。

勝ちにいくためには、何かと何かを比較して 勝ちに近づいている方を選びたいわけだが、将棋では 駒の強さ というのが その身近な、比較の目印になるだろう、というわけだ。

で、駒の点数は 単に 駒を数えることで付けられる。歩が1枚、2枚、3枚……。相手も数えるので どっちが歩は2枚多いとか、どっちが香車が2枚多いとか、そういう感じで点数が付く。どっちが駒何枚得しているかという話しで、駒割りと言う

だから、3駒関係の評価値を読込むところをコメントアウトしても、駒割りは働いている(はず)ぜ、ということを説明しようとした節だった。ここは。

KK と KKP は残そう

いくらなんでも 3駒関係全部外すと 指し手がおかしいだろう、ということで
KK と KKP ぐらいは残すという反抗ぐらいやってみる。

それが 弱い方に引っ張らなければいいんだが。無いよりはマシなのか、それとも 妙に主張して変な方へ意固地になる、ということになるかどうかは やってみないと分からない。

ためしさん25 を用意したい。また コピペ・マン だ。

tamesi25a3.php

<?php
$time=microtime(true);
exec('/usr/bin/expect /home/★user/shogi/expect_service/tamesi25a3.expect '.urldecode($_SERVER['QUERY_STRING']),$o);echo $o[count($o)-1];
echo '<br />' . (microtime(true) - $time);

tamesi25a3.expect

#!/usr/bin/expect --
cd /home/★user/shogi/ukamuse_sdt4_child2/bin
set timeout -1
spawn ./apery
send "usi\n"
expect -ex "usiok"
send "isready\n"
expect -ex "readyok"
send "usinewgame\nposition ${argv}\ngo\n"
expect -re "bestmove*" {
  exit
} "checkmate nomate *" {
  send "quit\n"
  send_user "successful 0.0\n"
} "*info time 1*" {
  send "quit\n"
  send_user "error 3.1\n"
} "checkmate timeout *" {
  send "quit\n"
  send_user "error 3.2\n"
} "*bad_alloc*" {
  send "quit\n"
  send_user "error 3.3\n"
} "*Out of memory*" {
  send "quit\n"
  send_user "error 3.4\n"
} "*recursively*" {
  send_user "error 3.5\n"
} "checkmate *" {
  send "quit\n"
  send_user "successful 1.0\n"
}

フォルダー名の ukamuse_sdt4_child を ukamuse_sdt4_child2 に変えるだけでファイル丸ごとコピーして増殖させてしまうという 富豪プログラミング の1つだ。メンテナンスをせず放置していく前提の必殺技なんだが、富豪プログラミングをメンテナンスしようという人間がいたら 富豪 だと思う。

富豪プログラミング

どんどん 増殖させよう。まるで単細胞生物のようだ。

'''
cp -R /home/★user/shogi/ukamuse_sdt4_child/ /home/★user/shogi/ukamuse_sdt4_child2
'''

ほら、ソフト丸ごと1本コピーして増殖だぜ☆ 少し変えるだけのために。

usi.cpp

            if (!evalTableIsRead) {
                // 一時オブジェクトを生成して Evaluator::init() を呼んだ直後にオブジェクトを破棄する。
                // 評価関数の次元下げをしたデータを格納する分のメモリが無駄な為、
                std::unique_ptr<Evaluator>(new Evaluator)->init(Evaluator::evalDir, true);
                evalTableIsRead = true;
            }

ここは戻す。

分かってなくても、だいたいで

分かってないんだが、evaluate.hpp を読んでいると いいところを見つけたぜ。

evaluate.hpp

    void setEvaluate() {
#if !defined LEARN
        SYNCCOUT << "info string start setting eval table" << SYNCENDL;
#endif
#define FOO(indices, oneArray, sum)                                     \
        for (auto indexAndWeight : indices) {                           \
            if (indexAndWeight.first == std::numeric_limits<ptrdiff_t>::max()) break; \
            if (0 <= indexAndWeight.first) {                            \
                sum[0] += static_cast<s64>((*oneArray( indexAndWeight.first))[0]) * indexAndWeight.second; \
                sum[1] += static_cast<s64>((*oneArray( indexAndWeight.first))[1]) * indexAndWeight.second; \
            }                                                           \
            else {                                                      \
                sum[0] -= static_cast<s64>((*oneArray(-indexAndWeight.first))[0]) * indexAndWeight.second; \
                sum[1] += static_cast<s64>((*oneArray(-indexAndWeight.first))[1]) * indexAndWeight.second; \
            }                                                           \
        }                                                               \
        sum[0] /= Base::MaxWeight();                                    \
        sum[1] /= Base::MaxWeight() * Base::TurnWeight();

#if defined _OPENMP
#pragma omp parallel
#endif
        // KPP
        {
#ifdef _OPENMP
#pragma omp for
#endif
            // OpenMP対応したら何故か ksq を Square 型にすると ++ksq が定義されていなくてコンパイルエラーになる。
            for (int ksq = SQ11; ksq < SquareNum; ++ksq) {
                // indices は更に for ループの外側に置きたいが、OpenMP 使っているとアクセス競合しそうなのでループの中に置く。
                std::pair<ptrdiff_t, int> indices[KPPIndicesMax];
                for (int i = 0; i < fe_end; ++i) {
                    for (int j = 0; j < fe_end; ++j) {
                        EvaluatorBase<KPPType, KKPType, KKType>::kppIndices(indices, static_cast<Square>(ksq), i, j);
                        std::array<s64, 2> sum = {{}};
                        FOO(indices, Base::oneArrayKPP, sum);
                        KPP[ksq][i][j] += sum;
                    }
                }
            }
        }
        // KKP
        {
#ifdef _OPENMP
#pragma omp for
#endif
            for (int ksq0 = SQ11; ksq0 < SquareNum; ++ksq0) {
                std::pair<ptrdiff_t, int> indices[KKPIndicesMax];
                for (Square ksq1 = SQ11; ksq1 < SquareNum; ++ksq1) {
                    for (int i = 0; i < fe_end; ++i) {
                        EvaluatorBase<KPPType, KKPType, KKType>::kkpIndices(indices, static_cast<Square>(ksq0), ksq1, i);
                        std::array<s64, 2> sum = {{}};
                        FOO(indices, Base::oneArrayKKP, sum);
                        KKP[ksq0][ksq1][i] += sum;
                    }
                }
            }
        }
        // KK
        {
#ifdef _OPENMP
#pragma omp for
#endif
            for (int ksq0 = SQ11; ksq0 < SquareNum; ++ksq0) {
                std::pair<ptrdiff_t, int> indices[KKIndicesMax];
                for (Square ksq1 = SQ11; ksq1 < SquareNum; ++ksq1) {
                    EvaluatorBase<KPPType, KKPType, KKType>::kkIndices(indices, static_cast<Square>(ksq0), ksq1);
                    std::array<s64, 2> sum = {{}};
                    FOO(indices, Base::oneArrayKK, sum);
                    KK[ksq0][ksq1][0] += sum[0] / 2;
                    KK[ksq0][ksq1][1] += sum[1] / 2;
                }
            }
        }
#undef FOO

#if !defined LEARN
        SYNCCOUT << "info string end setting eval table" << SYNCENDL;
#endif
    }

よく見ると //KPP //KKP //KK とコメントを振っている。じゃあ、//KPP のところを丸ごとコメントアウトしよう。

#if defined _OPENMP
#pragma omp parallel
#endif
//        // KPP
//        {
#ifdef _OPENMP
#pragma omp for
#endif
//            // OpenMP対応したら何故か ksq を Square 型にすると ++ksq が定義されていなくてコンパイルエラーになる。
//            for (int ksq = SQ11; ksq < SquareNum; ++ksq) {
//                // indices は更に for ループの外側に置きたいが、OpenMP 使っているとアクセス競合しそうなのでループの中に置く。
//                std::pair<ptrdiff_t, int> indices[KPPIndicesMax];
//                for (int i = 0; i < fe_end; ++i) {
//                    for (int j = 0; j < fe_end; ++j) {
//                        EvaluatorBase<KPPType, KKPType, KKType>::kppIndices(indices, static_cast<Square>(ksq), i, j);
//                        std::array<s64, 2> sum = {{}};
//                        FOO(indices, Base::oneArrayKPP, sum);
//                        KPP[ksq][i][j] += sum;
//                    }
//                }
//            }
//        }
        // KKP
        {

こう?

evaluate.hpp: In member function ‘void Evaluator::setEvaluate()’:
evaluate.hpp:980:9: error: for statement expected before ‘{’ token
         {
         ^

なんだろう? コメントアウトをもう少し狭くするか。

#if defined _OPENMP
#pragma omp parallel
#endif
        // KPP
        {
#ifdef _OPENMP
#pragma omp for
#endif
            // OpenMP対応したら何故か ksq を Square 型にすると ++ksq が定義されていなくてコンパイルエラーになる。
            for (int ksq = SQ11; ksq < SquareNum; ++ksq) {
                // indices は更に for ループの外側に置きたいが、OpenMP 使っているとアクセス競合しそうなのでループの中に置く。
                std::pair<ptrdiff_t, int> indices[KPPIndicesMax];
                for (int i = 0; i < fe_end; ++i) {
                    for (int j = 0; j < fe_end; ++j) {
//                        EvaluatorBase<KPPType, KKPType, KKType>::kppIndices(indices, static_cast<Square>(ksq), i, j);
//                        std::array<s64, 2> sum = {{}};
//                        FOO(indices, Base::oneArrayKPP, sum);
//                        KPP[ksq][i][j] += sum;
                    }
                }
            }
        }
        // KKP
        {

じゃあ、空ループにしよう。 富豪プログラミング ではよく見かける。何もしていない多重ループとか。

3.8秒……。このループが重いんじゃないか? このループを消そうぜ。

#if defined _OPENMP
#pragma omp parallel
#endif
        // KPP
        {
#ifdef _OPENMP
#pragma omp for
#endif
            // OpenMP対応したら何故か ksq を Square 型にすると ++ksq が定義されていなくてコンパイルエラーになる。
            for (int ksq = SQ11; ksq < SquareNum; ++ksq) {
                // indices は更に for ループの外側に置きたいが、OpenMP 使っているとアクセス競合しそうなのでループの中に置く。
//                std::pair<ptrdiff_t, int> indices[KPPIndicesMax];
//                for (int i = 0; i < fe_end; ++i) {
//                    for (int j = 0; j < fe_end; ++j) {
//                        EvaluatorBase<KPPType, KKPType, KKType>::kppIndices(indices, static_cast<Square>(ksq), i, j);
//                        std::array<s64, 2> sum = {{}};
//                        FOO(indices, Base::oneArrayKPP, sum);
//                        KPP[ksq][i][j] += sum;
//                    }
//                }
            }
        }
        // KKP
        {

このループは処理時間的に関係なさそう……。

init関数の中

#define ALL_SYNTHESIZED_EVAL {                  \
        FOO(KPP);                               \
        FOO(KKP);                               \
        FOO(KK);                                \
    }

ここかな。

//#define ALL_SYNTHESIZED_EVAL {                  \
//        FOO(KPP);                               \
//        FOO(KKP);                               \
//        FOO(KK);                                \
//    }
#define ALL_SYNTHESIZED_EVAL {                  \
        FOO(KKP);                               \
        FOO(KK);                                \
    }

こうすればいいんだろーか?

1秒未満の指し手に変わったが、眠くて精査できない(/_\) また今度続きを書く。

飛車先を伸ばしても守らないとのこと

7手読んでれば 飛車先の角頭は守ると思うのだが、思考時間が 0.02 秒なので 7手の手順の中で1手でも ポカ すれば飛車先の角頭は守れない。

思考時間を 0.25 秒ぐらい足せば少しはマシかと思うが、次の指示が来た。

なんかツイッターが動かないぜ

仕事主と連絡が取れないが、ツイッターはそのうち動くだろ。現状はこう。

  • (1)却下 : ゲームサーバ化(それをやるぐらいなら もっと予算をつけて他の人に頼む → なるほど)
  • (2)スルー : 指し手ブローカー三河屋
  • (3)現状 : Webサーバ
  • (4)向こうからの指示 : 定跡ファイルを使用
  • (5)こっちからの提案 : さくらVPSの調査が必要
  • (6)こっちからの提案 : 2駒関係の技巧、駒割りのやねうら王を使用

そういえば、予算がたくさんあると たくさんお金が使えていいじゃないか、と思うかもしれないが「なんだ この予算の使い方は!ぼったくられてるんじゃないか!?」とか事務方から怒られたりするかと思うんだが、そこで クリエイトに高いお金を払う意味を 説明すると さらに火に油を注ぐのな。だから最初に「使い道」「予算」を付けておいて「使わなければ損」という風な融資に形を作っておくんだぜ。まあいいか。

じゃあ次は 定跡+駒割+KK+KKP 浮かむ瀬で。

ためしさん26 で 富豪プログラミング

やっとくぜ。

tamesi26a3.php

<?php
$time=microtime(true);
exec('/usr/bin/expect /home/★user/shogi/expect_service/tamesi26a3.expect '.urldecode($_SERVER['QUERY_STRING']),$o);echo $o[count($o)-1];
echo '<br />' . (microtime(true) - $time);

tamesi26a3.expect

#!/usr/bin/expect --
cd /home/★user/shogi/ukamuse_sdt4_child3/bin
set timeout -1
spawn ./apery
send "usi\n"
expect -ex "usiok"
send "isready\n"
expect -ex "readyok"
send "usinewgame\nposition ${argv}\ngo\n"
expect -re "bestmove*" {
  exit
} "checkmate nomate *" {
  send "quit\n"
  send_user "successful 0.0\n"
} "*info time 1*" {
  send "quit\n"
  send_user "error 3.1\n"
} "checkmate timeout *" {
  send "quit\n"
  send_user "error 3.2\n"
} "*bad_alloc*" {
  send "quit\n"
  send_user "error 3.3\n"
} "*Out of memory*" {
  send "quit\n"
  send_user "error 3.4\n"
} "*recursively*" {
  send_user "error 3.5\n"
} "checkmate *" {
  send "quit\n"
  send_user "successful 1.0\n"
}

コピー。

cp -R ukamuse_sdt4_child2 ukamuse_sdt4_child3

よし。

なんというか、さくらVPS のいい点 は 容量を どんぶり勘定的に どんっ! とプランにして提供してくれるんで、空き容量がたっぷりあるんで、富豪プログラミング が使えるのな。

これも、「使い道」「予算」「使わなきゃ損」の3段論法だよな。

これが従量制だったら「何やってんだ!」となるわけだ。こういう お金の流れもあるということを知らないと クレーマーがさも コストを考えて忠告しています、みたいに 言ってくるわけだ。契約内容まで把握してプランを把握して忠告してくれるんだったら、クレーマーじゃなくてコンサルタントだよな。

次。

init.cpp

void initTable() {
    initAttacks(false);
    initAttacks(true);
    initKingAttacks();
    initGoldAttacks();
    initSilverAttacks();
    initPawnAttacks();
    initKnightAttacks();
    initLanceAttacks();
    initSquareRelation();
    initAttackToEdge();
    initBetweenBB();
    initCheckTable();
    initNeighbor5x5();
    initSquareDistance();

    // Book::init();
    initSearchTable();
}

これの、「// Book::init();」のコメントアウトを外す。

# make profgen_sse
# mv apery ../bin/apery

よし。

大きなヒューマン・エラーが

ためしさん 内容
24a3 駒割
25a3 駒割+KK+KKP
26a3 定跡+駒割+KK+KKP

なんだが、これが 駒割+KK+KKP です、とか言って 24a3 をテストに回してしまった。
テスト部隊の労力が丸損なんだが、どうやったら このミスは回避できるのだろうか。

おそらく、25a3 を渡したつもりで 24a3 をコピペして渡したんだぜ。

  • 寝ろ
  • コピペせず、1文字ずつ指差し確認

うーむ。

動かないツイッターを見ていても仕方がないので

技巧でも調べるか。

「将棋ソフト「技巧」」 (gikou-official/Gikou)
https://github.com/gikou-official/Gikou

商用利用が可なのかどうか ぱっと見 書かれてないが やってみればいいんじゃないか(^q^)
出村さんに訴えられたら 勝てる一般人とか 居なさそうなのが また魅力だよな。

じゃあ ソースをダウンロードするか。

FileZilla で移動

Gikou-master というフォルダー名のまんまでコピーしよう。分かりやすいだろ。

# cd /home/★user/shogi/Gikou-master
# nano Makefile

make release と打てばいいのか?

# make release

よし、コンパイルが終わった。

# cd bin
# ls
release

release という名前の実行ファイルか。
じゃあ、これを 富豪プログラミング でやってみよう。名前は ためしさん27 で。

tamesi27a3.php

<?php
$time=microtime(true);
exec('/usr/bin/expect /home/★user/shogi/expect_service/tamesi27a3.expect '.urldecode($_SERVER['QUERY_STRING']),$o);echo $o[count($o)-1];
echo '<br />' . (microtime(true) - $time);

なんか このファイル名の部分、引数にしたいよな。

tamesi27a3.expect

#!/usr/bin/expect --
cd /home/★user/shogi/Gikou-master/bin
set timeout -1
spawn ./release
send "usi\n"
expect -ex "usiok"
send "isready\n"
expect -ex "readyok"
send "usinewgame\nposition ${argv}\ngo\n"
expect -re "bestmove*" {
  exit
} "checkmate nomate *" {
  send "quit\n"
  send_user "successful 0.0\n"
} "*info time 1*" {
  send "quit\n"
  send_user "error 3.1\n"
} "checkmate timeout *" {
  send "quit\n"
  send_user "error 3.2\n"
} "*bad_alloc*" {
  send "quit\n"
  send_user "error 3.3\n"
} "*Out of memory*" {
  send "quit\n"
  send_user "error 3.4\n"
} "*recursively*" {
  send_user "error 3.5\n"
} "checkmate *" {
  send "quit\n"
  send_user "successful 1.0\n"
}

ディレクトリとファイル名が違うだけ。

しかし あんまり便利にすると 外部から やりたい放題になってしまうからな。

http://★.★.★.★/tamesi27a3.php?sfen lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1 moves

外部から これでアクセスすればいいわけだ。

おや? 固まっている……。

技巧を動かしてみよう

# ./release
info string Failed to open progress.bin.
info string Failed to open params.bin.
info string Failed to open probability.bin.

ファイルが無いと言っているな。Git Hubの説明を読んでみよう。

将棋の評価データの学習には1日かかると書いてるな。楽できないものか。
Windows版に 入ってないだろうか。

なんか ダウンロードまで残り4時間とか出ている。

棋譜データとか無いけど、

# ./release --learn-progress

動いてるけど、 0 ばっかだな。

# ls
progress.bin  release

お、1分とかからず progress.bin ファイルがでけたぜ。
これ、将棋の進行度が入ってるんだろ、全部 0 だけど。

# ./release --learn &
[1] 15974
root@★:/home/★user/shogi/Gikou-master/bin# info string Failed to open params.bin.
info string Failed to open probability.bin.
Set num_threads = 2
Extract the games from the database.

先に probability.bin がいるのか。

# ./release --learn-probability
# ls
probability.bin

これも 1分とかからず でけた。

# ./release --learn &
[1] 16140
root@★:/home/★user/shogi/Gikou-master/bin# info string Failed to open params.bin.
Set num_threads = 2
Extract the games from the database.

腹減ったな。

# ./release --create-book
info string Failed to open params.bin.
total games=0
total moves=0, registered=0
startpos
startpos moves 7g7f

これも 瞬時に終わった。

# ls
book.bin  probability.bin  progress.bin  release

book.bin がでけた。

あとは params.bin だが。

# ./release
info string Failed to open params.bin.

腹が減ったぜ。

「floodgateの棋譜を技巧風DBに変換するツールなのじぇ。」 (daruma3940の日記)
http://daruma3940.hatenablog.com/entry/2016/08/29/191808

1行目: <棋譜番号> <対局開始日> <先手名> <後手名> <勝敗(0:引き分け,1:先手勝ち,2:後手勝ち)> <手数> <棋戦> <戦型>
2行目: <CSA形式の指し手(1手6文字)を一行に並べたもの>

こういう形式なのらしい。

「CSA標準棋譜ファイル形式」 (CSA)
http://www.computer-shogi.org/protocol/record_v22.html

だったら

kifu.txt

1 2017/03/10 Taro Jiro 1 1 SaburoTournament Igyoku
2726FU %TORYO

これで 棋譜データベースになるだろうか?

# ./release --learn &
[1] 16500
root@★:/home/★user/shogi/Gikou-master/bin# info string Failed to open params.bin.
Set num_threads = 2
Extract the games from the database.

ダメか。

そういえば ごはんが全然 炊き上がらないな、と思って 炊飯器を見に行ったら
洗った釜が置いてあった。

技巧の棋譜データベースを用意できないんで、先に やねうら王も見ておこう。

やねうら王をダウンロードしよう

「やねうら王オープンソースプロジェクト」 (やねうら王)
http://yaneuraou.yaneu.com/yaneuraou_mini/

でも よくよく考えると やねうら王は Windows だったような気がするな。
通信回線もなんかよく止まるし、技巧のダウンロードが終わってからにしよう。

技巧のソースを読んでみよう

info string Failed to open params.bin.

という文字列を grep する。

3個所あるが 多分これだろうか。

evaluation.css

void Evaluation::ReadParametersFromFile(const char* file_name) {
  // Read parameters from file.
  std::FILE* fp = std::fopen(file_name, "rb");
  if (fp == nullptr) {
    std::printf("info string Failed to open %s.\n", file_name);
    return;
  }
  if (std::fread(g_eval_params.get(), sizeof(EvalParameters), 1, fp) != 1) {
    std::printf("info string Failed to read %s.\n", file_name);
    std::fclose(fp);
    return;
  }
  std::fclose(fp);
}

だから そのまんま params.bin ファイルを読込もうとして、ファイルを開けられないと言ってるわけだ。

このファイルは多分 EvalParameters サイズのバイナリだろう。

g_eval_params.get()

は何だろうか。

std::unique_ptr<EvalParameters> g_eval_params(new EvalParameters);

腹減ったなあ。使い終わったあとにメモリが開放される使いやすいポインタで、EvalParameters をメモリ上に展開して それをずっと指していてくれるわけだ。

evaluation.h 抜粋

/**
 * 評価値の計算に用いるパラメータ(重み)です.
 */
struct EvalParameters {
  /**
   * 評価パラメータをゼロクリアします.
   */
  void Clear() {
    std::memset(this, 0, sizeof(*this));
  }

  //
  // 1. 駒の価値
  //
  /** 駒の価値 [駒の種類] */
  ArrayMap<Score, PieceType> material;

  //
  // 2. 2駒の位置関係
  //
  /** 玉と玉以外の駒の位置関係 [玉の位置][玉以外の駒の種類及び位置] */
  ArrayMap<PackedScore, Square, PsqIndex> king_piece;
  /** 玉を除く2駒の位置関係 [駒1の種類及び位置][駒2の種類及び位置] */
  ArrayMap<PackedScore, PsqIndex, PsqIndex> two_pieces;

  //
  // 3. 各マスごとの利き
  //
  /** 各マスの利き [先手玉か後手玉か][玉の位置][マスの位置、駒の種類、先手の利き数、後手の利き数] */
  ArrayMap<PackedScore, Color, Square, PsqControlIndex> controls;

  //
  // 4. 玉の安全度
  //
  /** 玉の安全度 [相手の持ち駒][玉から見た方向][そのマスにある駒][攻め方の利き数][受け方の利き数] */
  ArrayMap<Array<PackedScore, 4, 4>, HandSet, Direction, Piece> king_safety;

  //
  // 5. 飛車・角・香車の利き
  //
  /** 飛車の利き [先手玉か後手玉か][玉の位置][飛車の位置][飛車の利きが届いている位置] */
  ArrayMap<PackedScore, Color, Square, Square, Square> rook_control;
  /** 角の利き [先手玉か後手玉か][玉の位置][角の位置][角の利きが届いている位置] */
  ArrayMap<PackedScore, Color, Square, Square, Square> bishop_control;
  /** 香車の利き [先手玉か後手玉か][玉の位置][香車の位置][香車の利きが届いている位置] */
  ArrayMap<PackedScore, Color, Square, Square, Square> lance_control;
  /** 飛車の利きが付いている駒 [相手玉の位置][飛車が利きをつけている駒の位置][飛車が利きをつけている駒] */
  ArrayMap<PackedScore, Square, Square, Piece> rook_threat;
  /** 角の利きが付いている駒 [相手玉の位置][角が利きをつけている駒の位置][角が利きをつけている駒] */
  ArrayMap<PackedScore, Square, Square, Piece> bishop_threat;
  /** 香車の利きが付いている駒 [相手玉の位置][香車が利きをつけている駒の位置][香車が利きをつけている駒] */
  ArrayMap<PackedScore, Square, Square, Piece> lance_threat;

  //
  // 6. 手番
  //
  /** 手番 */
  PackedScore tempo;
};

ボナンザみたいに評価値が並んでるだけのテーブルじゃないのか。
こりゃ大変だ。

技巧1.0.1のWindows版のダウンロードが終わった。
中には

  • book.bin
  • params.bin
  • probability.bin
  • progress.bin

が入っていた。同じソースで読み取るバイナリなので、OSが違うから動かないということも あんま無いだろう。
FileZilla で Ubuntu の中へコピー。

chmod 777 bin
# ./release --learn &
[1] 17725
root@★:/home/★user/shogi/Gikou-master/bin# Set num_threads = 2
Extract the games from the database.

さて、どうなったのか?

なんか 標準入力が戻ってこないぜ。[Ctrl]+[Z] も効かないし。

あっ、そうか。

学習しなくていいんだ。

# ./release
usi
id name Gikou 20160606
id author Yosuke Demura
option name BookMaxPly type spin default 50 min 0 max 50
option name ByoyomiMargin type spin default 100 min 0 max 10000
option name DrawScore type spin default 0 min -200 max 200
option name FischerMargin type spin default 12000 min 0 max 60000
option name MinBookScoreForBlack type spin default 0 min -500 max 500
option name MinBookScoreForWhite type spin default -180 min -500 max 500
option name MinThinkingTime type spin default 1000 min 10 max 60000
option name MultiPV type spin default 1 min 1 max 700
option name NarrowBook type check default false
option name OwnBook type check default true
option name SuddenDeathMargin type spin default 60 min 0 max 600
option name Threads type spin default 2 min 1 max 64
option name TinyBook type check default false
usiok

よし。

usi を返して終わるらしい

技巧とは別に ためしさん なんだが、

usi 
0.017974853515625

と返してきて 処理が止まるらしい。調べるか。

確かに usi と返してきて止まっている。
1手指すごとに コンピューター将棋ソフトを 再起動 しているんで、将棋エンジンを起こすために usi を送っているんだが、将棋エンジンが起きないということだ。

Linux はマルチユーザーだろう。

技巧の評価ファイルは 247MB あるんで、これも一手指すための時間が1秒未満にならないのが見えてきたな。

そにしても usi と返してくるのを なんとかしないと。何とかするにしても、やること無いんだが。

定跡+駒割+KK+KKP 浮かむ瀬も 指し手を返してくるのに 1.2秒 ぐらいかかってるんで、1秒未満という要件を満たさない。

nozomi を調べてみよう

「nozomi」 (saihyou/nozomi)
https://github.com/saihyou/nozomi

ポナンザや技巧にもワンパンチ入れてる面白ソフトだぜ☆(^~^

じゃあ そのまんま FileZilla でコピーしよう☆(^~^

# nano Makefile

make nozomi でいいんだろうか?

# make nozomi

g++ でコンパイルが進んでいるぜ。

# ./nozomi
nozomi 100317 by Yuhei Ohmori
Illegal instruction

なんのこっちゃ。

ところで

PuTTY で2重ログインして

# ./apery
usi
id name ukamuse_SDT4
id author Hiraoka Takuya

option name Best_Book_Move type check default false
option name Book_File type string default book/20150503/book.bin
option name Byoyomi_Margin type spin default 500 min 0 max 2147483647
~以下略~

したんだが、もう1つの端末で Apery を起動しようとすると

# ./apery
Killed

となる。
なんでだろう。Ubuntuには 別のユーザーが使ってると ロック する仕組みでもあるのだろうか?

なんというか Linuxの基礎知識がないので分からない。

nozomi の様子を調べよう

# make clean
# make
~略~
# ./nozomi clean
nozomi 100317 by Yuhei Ohmori
Illegal instruction

うーむ、これでもダメか。 さくらVPSに無いCPU命令でも使ってるのか、なんなのか。

次の記事では 常駐プログラムについて調べてみる

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3