備忘録 blog

Docker/Machine Learning/Linux

将棋の棋譜を自動解析して詰み逃しを見つける

将棋の棋譜から、詰みがあったのにも関わらず詰ませ損ねた局面を集め、詰将棋集を作って練習したいと思った。そこで、棋譜内に詰み逃しがあった時に Jupyter Notebook 上で表示されるようなコードを実装した。例として著作権の切れた古い棋譜を使って、どのように結果が得られるのか簡単に紹介する。

ここでの詰み逃しの定義は、相手玉に詰みが発生している場合でかつ、自分が最善手あるいは次善手(次善手も詰みになる場合)であるということにする。詰みが発生していることの基準は、エンジンが実行した結果で mate が得られていることであるため、厳密な詰将棋としての王手の連続ではなく、相手玉が必至の状態で相手が王手ラッシュをかけてくるという場合や、無駄合いも含まれている。

ここでは江戸時代の棋譜から、1808年の大橋柳雪(中村喜多次郎)と大橋宗英(九世名人)の対局を対象にしてみたいと思う*1。この棋譜を解析すると、以下のように出力された。

f:id:sharply:20220302102600p:plain

Best Eval_value:  mate 11
11手詰
Move No.:  101
Your Move:  5445UM 4五馬
Best Move:  0037GI 3七銀 ['S*3g', '4f4g', 'N*3i', '4g3g', 'R*3h', '3g4f', '4d4e', '4f5g', '3h5h', '5g6g', '8g7h']

上記のように出力されて、実際は何手詰であるか(これは探索時間によって余詰の方を出力することがあるので、必ずしも最短の手数とは限らない)と、最善手、また実際に指した手が表示される。この場合では、投了直前の101 手目の4五馬という手が詰みを逃しており、3七銀と指していれば11手詰めであったことを示している。また、表示される画像が詰将棋の出題図であるため、冷静な気持ちでこの局面を眺めてみて、再度指し手を考え直してみるのも良いだろう。

実際に解析に用いたコードは以下である。

from tqdm import tqdm
import Ayane.source.shogi.Ayane as ayane
from cshogi import CSA, KIF, Board, move_to_csa, move_to_usi
import glob
import pandas as pd
import requests
from IPython.display import Image,display_png

usi = ayane.UsiEngine()

usi.set_engine_options({"Hash":"128","Threads":"4","NetworkDelay":"0","NetworkDelay2":"0"})
usi.connect("/content/YaneuraOu-6.00/source/YaneuraOu-by-gcc")

for (_, kifu) in df.iterrows():
  board = Board(kifu.sfen)
  for (idx, move) in enumerate(tqdm(kifu.moves)):
    usi.usi_position("sfen {}".format(board.sfen()))
    if kifu.types == "kif":
      board.push_usi(move)
    else:
      board.push(move)

    url = "http://sfenreader.appspot.com/sfen?sfen={}".format(board.sfen()).replace("+", "%2B")
    r = requests.get(url, params={}, stream=all)
    display_png(Image(r.content))

    usi.send_command("multipv 2")
    usi.usi_go_and_wait_bestmove("btime 0 wtime 0 byoyomi 2000")

    if ayane.UsiEvalValue.is_mate_score(usi.think_result.pvs[0].eval) and (turn == (idx2 % 2 == 0)):
        bestmove_usi = usi.think_result.bestmove
       
        board.pop()
        prev_move = move_to_csa(board.peek())
        url = "http://sfenreader.appspot.com/sfen?sfen={}&lm={}".format(board.sfen(), prev_move[2:4]).replace("+", "%2B")
        r = requests.get(url, params={}, stream=all)

        board.push_usi(bestmove_usi)
        bestmove = move_to_csa(board.peek())
        board.pop()
        if kifu.types == "kif":
          board.push_usi(move)
        else:
          board.push(move)
        move = move_to_csa(board.peek())
        move_usi = move_to_usi(board.peek())
        next_hand_mate = ayane.UsiEvalValue.is_mate_score(usi.think_result.pvs[1].eval)

        if not (bestmove == move_usi or (next_hand_mate and move_usi == next_hand_mate)):
          display_png(Image(r.content))

          print("Best Eval_value: ", usi.think_result.pvs[0].eval.to_string())
          print("{}手詰".format(usi.think_result.pvs[0].eval.to_string().split(" ")[1]))
          print("Move No.: ", idx)
          print("Your Move: ", move, human_readable_move(move))
          print("Best Move: ", bestmove, human_readable_move(bestmove), usi.think_result.pvs[0].pv.split(" "))

CSA フォーマットと USI フォーマットについて

出力の指し手を人間が読みやすい形に整形することを考える。まず、将棋エンジンとの通信では USI フォーマットと呼ばれる記法を用いて指し手を通信している。これは、 3c3d, 7g7f, 4c4d, 4g4f, 4a3b, 3i4h, 3a4b のように座標のみで表現している。それに対して CSA フォーマットでは、7776FU のように移動前後の座標と、その駒の名前も載っている。この情報をもとに、上で用いているような human-readable な指し手を表示することを考えたい。

def human_readable_move(csa):
  NUMBER_JAPANESE_KANJI_SYMBOLS = [ None, "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" ]
  PIECE_JAPANESE_SYMBOLS = [
    '',
    '歩', '香', '桂', '銀', '角', '飛', '金', '玉', 'と', '成香', '成桂', '成銀', '馬', '龍'
  ]
  PIECE_SYMBOLS = [
    '',
    'FU', 'KY', 'KE', 'GI', 'KA', 'HI', 'KI', 'OU', 'TO', 'NY', 'NK', 'NG', 'UM', 'RY'
  ]
  piece = PIECE_SYMBOLS.index(csa[4:6])
  return csa[2] + NUMBER_JAPANESE_KANJI_SYMBOLS[int(csa[3])] + PIECE_JAPANESE_SYMBOLS[piece]

しかしこのコードでは、例えば銀が4八と6八にいるときに「5七銀」が「5七銀右」なのか、「5七銀左」なのかの区別をつけることができない。あるいは、「78成桂」があるときに、その駒が元々成駒であったか、今ちょうど成ったかを区別することができない。これは、1手前の盤面情報がないと再現できないためである。したがって、あくまで上記のコードはパッチワークであることに注意されたい。厳密な指し手を得るためには、盤面情報を利用することとなるが、それは今後の課題としたい。

*1:平手ではなく飛車落ちである

if 文と jump 命令の性質の違いを説明する

はじめに

Human Resource Machine というゲームがある。 Steam 等で入手することができる。

Human Resource Machine

Human Resource Machine

  • Tomorrow Corporation
Amazon

このゲームを雑に説明してしまうと、このゲーム固有の命令セットを持つようなプロセッサが処理できる、アセンブリプログラミングをするというようなゲームである。ここでは人間がプロセッサに代わって命令通りに行動し、状態を保持するレジスタは工場の区画として表現される。入出力は数字だが、数字はあたかもその「実体」を持つように扱われる。したがって、アルゴリズム上のいくつかの概念はまさにアセンブリプログラミングをする時とまさに同じような形で実装しなければならない*1。現実の CPU と同様の制約も存在する*2

ところで、条件付きの jump 命令(jump if negative, jump if zero)が登場した時に、その扱い方が高級言語の if 文と異なるので、混乱することがあるだろう。ここでは、その性質の違いを説明してみたいと思う。なお、文中で厳密さを欠いた説明をしていることをお許しいただきたい。(例えば一般に Windowsx86-64 の CPU 上で動作するというステートメントは、Windows 9x 系や ARM 版 Windows について無視していることになるが、そうした部分についてはご容赦いただきたい。)

CPU とアセンブリプログラミングにおける一般論

CPUは命令列によって動作する。パンチカードの時代から、プロセッサは常に一列の命令列を順に処理していく。つまり基本的には、CPU は与えられた命令を順序通り処理するように設計されている。しかしそれだけでは制御構造が実装できないので、CPU が読み取ることができる命令列は機械語である。あらゆるソフトウェアは最終的にはその機械語に翻訳されて実行される。ところで、CPU の種類によって実行できる命令列のパターンが異なっている。これはいわば、異なる言語が CPU ごとに存在していて、CPU ごとに異なる言い方をしないとその CPU が動作しない、というようなものだ。

一般に低レイヤーに近い、つまり CPU やメモリといった資源の配分をするようなレイヤーのコードは、CPUの機能を直接利用することになるため、CPU に特化したコードを書くことになる。例えば OS のカーネルがそれに該当し、 最近の Windowsx86_64 という命令セットが動作する CPU 上で動作するし、 最近の MaciOS は ARM という命令セットが動作する CPU 上で動作するようになっている。さらには、同じ命令セットが動作する CPU であっても、そのOS上で実行できる実行ファイルのフォーマットが異なるので、たとえ同一のソフトウェアであっても WindowsMacLinux で実行するバイナリが異なることがある *3。OSごとにダウンロードするファイルが異なるというのはそのためである。フォーマットの違いは、共有ライブラリの呼び出しの方法や、ヘッダの違いなどに起因している。

命令セットの違いとは何か?

機械語は基本的には 0/1 の二進数で表現されるものである。CPU のアーキテクチャによってその命令セットは異なることは説明したが、それが具体的にはどのように記述するかを説明したい。よくCPU には 64bit のものと 32 bit のものがあることが知られているが、その違いは何だろうか。よくある間違いとしては、1つの命令の長さは 64 bit の CPU では 64bit で、 32 bit の CPU では 32bit である、というわけでは必ずしもない。

usm-takl.github.io

ここであるように、x86_64 は可変長だが、その他の固定長のアーキテクチャ(いわゆるRISC)では64 bit の CPU であっても 32 bit であることがわかる。その場合では、一般に1つの命令につき32個の0と1で表現されるということになる。 では 64 bit の CPU は何が 64 bit かというと、汎用レジスタのサイズが 64 bit であるということである。つまり、1つのレジスタに64 個の 0/1 の数字を格納することができる、というわけである。

これは単純に、例えば浮動小数点を倍精度で扱うときに1つのレジスタで済むので処理を高速化できるといったような利点もあるが、大きな利点は大きいメモリを利用できるということだろう。具体的には 32 bit のレジスタで指し示すことのできるメモリ空間は 232 ~ 4GB 程度だが、メモリが 4GB しか使えないとなると現代においてはかなり強い制約となる。仮にOSが 32 bit のメモリ空間しか利用できないならば、その上で動作するソフトウェアも同様の制約を受けることになってしまう。個々のソフトウェア単位でも、例えば動画編集や3Dアニメーションを利用しようとするとすぐに4GB のメモリを消費してしまうだろう。これが 64 bit のメモリアドレス空間を利用できるとなると、理想的には 264 のメモリ空間を利用できるため、一般的な用途として十分である。

よくバイナリをダウンロードするときに、32 bit 版と 64 bit 版を選ばせる。どちらをダウンロードすればよいか悩む場合があるが、それは 32 bit のマシンでは 64 bit のレジスタが存在することを前提とした命令セットを用いてビルドされたバイナリは実行できないが、逆は可能である。では 64 bitの環境でも 32 bit のバイナリを使っておけば問題がないような気もするが、先述の通り、32 bit 版のバイナリでは利用できるメモリサイズが限られてしまう以上、 64 bit 版のバイナリを利用した方が少なくともメモリに関する制約を回避することはできる。

固定命令長の命令セットにおいて、命令長サイズが 32 bit であっても、64 bit のメモリのアドレスの先を指し示すことができるのか?というのが次に疑問として湧いてくるかもしれないが、一般には 64 bit のレジスタにアクセスしたいメモリのアドレスを入れておいて、そのアドレスを読み出す、というように二段階でメモリ上の値を読み書きするため、 1つの命令がメモリアドレスを指し示す必要は必ずしもないのだ。大抵の場合でレジスタの本数は 8 bit に収まるため、 32 bit の命令長があれば、レジスタを指し示すことは十分にできる。そこで、基本的には計算は全てレジスタ上で行う。これをロードストアアーキテクチャなどと呼んだりする。

ともかく、0/1で表現された機械語の命令を CPU は読み込んで実行していくと仕組みになっており、基本的には命令の順番通りに実行される *4

機械語アセンブリ言語高級言語の関係

機械語を人間が読み書きして理解することは一般には極めて困難である。機械語の代わりに命令やレジスタを、ニーモニックと呼ばれる文字列で表現することがある。これがアセンブリ言語である。アセンブリ言語はその意味では機械語とほぼ同じであるといえるが、幾分リーダビリティが高いといえる。

今見てきたように、アセンブリ言語は CPU の命令セットに依存する。つまりあるプログラムを書くときに、アセンブリ言語で記述しようとすると、その CPU の仕様を熟知していなければならないし、異なる命令セットのマシンで同じプログラムを動かす(移植する)ことが極めて難しくなってしまう。低レイヤーのプログラムや速度を重視したプログラムでは CPU の機能をダイレクトに使うために、そのようなコードを書かなければならないのは仕方ないが、そうではない場合、言い換えると一般のプログラムを記述する場合では、CPU 上での具体的な動作(例えば変数をどのレジスタに置くか)を気にせずに書きたい。そこで、アセンブリ言語より抽象的な言語でプログラムを書いておいて、実際にプログラムを実行する前に、そのソースコードをそのマシンで実行できるように機械語に翻訳するという仕組みを設けることを考える。この仕組みをコンパイルと呼び、より抽象的な言語のことを高級言語と一括りに呼ぶことがある。別の方法として、ソースコードを解釈して実行するプログラムを用いることもある。この仕組みはインタプリタと呼ばれる。高級言語の中にはC言語とか Python とか Ruby とかが含まれる。

こうした高級言語コンパイル(あるいはインタプリタ)の仕組みがあることによって、普段我々はどの CPU 上で動作するマシンかということをそれほど気にしなくても、抽象的に命令を記述することができる。しかしながら、こうした高級言語のプログラミングのやり方と、アセンブリ言語でのプログラミングのやり方が異なるがゆえに、先入観によってアセンブリ言語ではどのように書けば良いのかということがわからなくなってしまうというのが、ここで取り上げたい話題である。

if 文による世界と jump 命令による世界がある

よく見る高級言語では、条件分岐があったときにこのように書きたいと思う。これが普通の人が想像しやすい if 文の書き方である。

if x == 0
    # do something if the condition is satisfied
else
    # do something if the condition is not satisfied
end
# do finally

if 文の下には、if 条件を満たしている場合の処理を記述する。これをフローチャートで記述すると、以下のようになるだろう。

f:id:sharply:20220304122209p:plain

ここで、 jump 命令でこの if 文相当の処理を書くとしたらどうなるだろうか。ここでは、 無条件で jump する命令と、条件付きで jump する命令を使って、例えば PowerPC 風の記述で書くとこのようになるだろう。条件付きの jump 命令とは、jump if negative であれば負の時にジャンプ、 jump if zero であれば0の時にジャンプ、といったものである。

(前略)
    be %cr7, .IfZero
    # do something if the condition is not satisfied
    b .Finally
.IfZero
    # do something if the condition is satisfied
.Finally
    # do finally

ここでの記法を説明すると、ドットで書き始めている行はラベルであり、b は、無条件でラベルの行にジャンプする命令、be は、条件付き(ここでは、0に等しい時)でラベルの行にジャンプする命令、 %cr7 は適当に指定したレジスタであるとみなしてほしい。ここで、先ほどの if 文を使ったプログラムに比べて、いくつか注意しなければならないことがあることがわかる。

  • be 命令の下に書くのは、条件が満たさない場合の処理であって、条件が満たされている場合の処理は jump した先に書かなければならない。
  • プログラムは上から下に処理されるので、jump 命令で .IfZero ラベルの中身をスキップしないと、.IfZero ラベルに下に書かれた行の処理も順に実行されてしまう。

つまり、if 文では暗黙のうちに「if 節の処理を行なった場合は else 節はスキップされる」ということが仮定されているが、それらの処理を全て明示的に jump 命令を使って記述しなければならない、ということである。これをフローチャートで表現すると以下である。

f:id:sharply:20220304122353p:plain

抽象的なフローチャートをトポロジカルソートしたい

つまり条件分岐がある時に、脳内で想起される最初のif文のフローチャートを、jump 型のフローチャートで表現されるように、全ての処理を一列に並び替えて記述する必要がある。そして、次に行いたい処理が1行以上飛んだ先になる場合は、jump 命令でその間をスキップする必要がある。なので、プログラムに分岐が発生する場合、路線図で言うところの、別の路線に分岐して、最終的に合流する、と言うような路線ではなく、路線の中で快速と特別快速で停車駅が異なって、千鳥停車になっている、というような路線であるというように想像する必要がある。肝心なのは、if 分岐をしたい時にこうした構造を想起して、コードに書き起こすということを毎回する必要がある、ということである。

trafficnews.jp

これをどう理解するかについて、 一つのアイディアとしては、 if 文を書きたい場合は、まずは jump 命令を用いて枠組みを作って、その中に条件が真の時の処理、偽の時の処理というように書く、とパターンで認識することもできるが、ただしこれも、 else 節の処理があるかないか、 finally の処理があるかないか、あるいは入れ子の if 文があるか、ということによって、実際にはさらに最適化できるコードにはなってしまう。やはり原理に立ち返って、あくまで記述の通りに処理を忠実に行なっていく、ということを理解してプログラムを作っていくのが良いのではないだろうか。

参考文献

www.mztn.org

*1:例えば掛け算は足し算の繰り返しとして表現できる、レジスタにカウンタを作ることができるなど

*2:例えば、処理の戻り値は1つだけであるとか、ADD 命令は自分の手元に持っている値に何かを加える、ということはなかなか初見で理解しづらい場合がある

*3:例えば Mac ではMach-O、 Linux では ELF などが使われる

*4:高速化のため、結果に影響を与えない範囲で投機的に実行されるアウトオブオーダの仕組みが実装されている場合もある。

先後差を考慮した将棋のプロ棋士の強さモデリング(2)

はじめに

本稿は前回に引き続き、先後差を考慮したプロ棋士の強さベイズモデリングをしていきたい。今回は棋士ごとの先手・後手の差をモデルに埋め込んで、先後の強さの差がどのぐらい出るかというのをみていきたい。

前回の記事はこちら。

sharply.hatenablog.com

ローカルな先手後手の差をモデルに埋め込んだモデル

このモデルでは、ローカルな先手後手の差、即ち各棋士ごとに先手後手のムラがあると仮定して、そのムラがどのぐらい強く出るか、というのを棋士ごとにみていきたい。モデルの中身は以下である。

  • 棋士の強さをμ、実力のムラをσとして、対局時の実力を normal(μ, σ) として定める。
    • 棋士の強さμの分散(すなわち正規分布の分散)の事前分布を一様分布として定める。
  • 実力が高いほうが対局に勝利する。具体的には、実力の差をシグモイド変換した確率値 p に対して、 その確率 p で 1 が出るような試行であったとモデリングする(ベルヌーイ分布)。
    • この時に、実力の差に対して、棋士ごとに先手勝ちの場合に δ、後手勝ちの場合に -δ の補正を加える。この補正項によって、棋士ごとの先手・後手の得意・不得意を扱って解析することができる。
      • この δ は平均0の正規分布に従う(その正規分布から各棋士ごと値が選ばれる)というようにここではモデリングする。この制約により、全体の平均が0に収束することを期待している。
with pm.Model() as model:
    # prior
    s_mu = pm.Uniform('s_mu',upper=300)
    mu = pm.Normal('mu', mu=0, sd=s_mu, shape=len_players)
    sigma = pm.Gamma('sigma', alpha=10, beta=10, shape=len_players)
    delta = pm.Normal('delta', mu=0, shape=len_players)

    loser_perf = pm.Normal('loser_perf', mu=mu[battles.Loser-1], sd=sigma[battles.Loser-1], shape=len(battles))
    winner_perf = pm.Normal('winner_perf', mu=mu[battles.Winner-1], sd=sigma[battles.Winner-1], shape=len(battles))
    sente_perf = pm.Normal('sente_perf', mu=delta[battles.Winner-1] * battles.FirstWin, shape=len(battles)) #sp * battles.FirstWin
    gote_perf = pm.Normal('gote_perf', mu=delta[battles.Loser-1] * battles.FirstWin, shape=len(battles)) 
     
    diff = winner_perf-loser_perf + sente_perf + gote_perf
    #p = pm.math.switch(diff, 1.0, 0.0)
    p = pm.math.sigmoid(diff)
    z = pm.Bernoulli('z', p=p, observed=np.ones(len(battles)))
pm.traceplot(trace, var_names=['mu','sigma','delta'])

f:id:sharply:20220225165940p:plain

棋士の先後ムラの平均(大きい順=先手の方が強い棋士

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
藤森哲也 1.303 0.613 0.187 2.504 0.038 0.027 256 521 1.01
村中秀史 1.244 0.624 0.116 2.396 0.051 0.036 150 367 1.02
青嶋未来 1.24 0.537 0.254 2.264 0.031 0.022 300 449 1.01
都成竜馬 1.207 0.551 0.162 2.178 0.034 0.024 258 746 1.01
上村亘 1.163 0.588 0.107 2.341 0.032 0.023 337 692 1.01

棋士の先後ムラの平均(絶対値が小さい順=先後ムラが少ない棋士

ここで示している棋士は、先後による勝率の差が少ない棋士であるといえるだろう。

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
橋本崇載 0 1.008 1.931 1.843 0.013 0.02 5706 2127 1
今泉健司 0 0.564 0.99 1.121 0.036 0.026 240 636 1
小林健 0.002 0.845 1.609 1.531 0.043 0.031 378 771 1
長岡裕也 0.004 0.509 0.928 0.987 0.032 0.022 262 719 1.02
阪口悟 0.005 0.619 1.176 1.106 0.032 0.023 364 758 1
小林宏 0.006 0.771 1.456 1.415 0.036 0.025 461 917 1
小林裕士 0.01 0.56 1.061 1.072 0.035 0.025 257 680 1.02
平藤真吾 0.016 0.607 1.05 1.212 0.036 0.026 280 745 1.01
中村太地 0.024 0.565 0.994 1.082 0.029 0.021 374 675 1.01
屋敷伸之 0.027 0.596 1.125 1.115 0.037 0.026 267 593 1.01

棋士の先後ムラの平均(小さい順=後手の方が強い棋士

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
山本博 -1.604 0.577 -2.657 -0.543 0.038 0.027 233 683 1
川上猛 -1.162 0.742 -2.67 0.166 0.037 0.026 402 725 1
冨田誠也 -1.011 0.585 -2.091 0.089 0.041 0.029 209 386 1.01
窪田義行 -0.93 0.584 -2.034 0.162 0.039 0.028 226 504 1
森悠 -0.81 0.581 -2.002 0.197 0.039 0.028 217 505 1
稲葉陽 -0.806 0.571 -1.849 0.23 0.036 0.026 249 468 1
飯島栄治 -0.717 0.542 -1.722 0.237 0.031 0.022 312 702 1
羽生善治 -0.711 0.487 -1.615 0.215 0.026 0.018 351 585 1
勝又清和 -0.706 0.661 -1.93 0.557 0.035 0.025 358 569 1
田中寅彦 -0.688 0.749 -2.077 0.702 0.069 0.049 118 371 1.03

考察

これらの結果は、2021 年における先手勝率・後手勝率と照らし合わせても概ね妥当である。

kenyu1234.php.xdomain.jp

だとすると単純に先後の勝率だけを比較すれば良いではないか、という意見もあるかもしれないが、このモデルでは対戦相手の強さについてもパラメーターに含めているため、対戦相手の強さも考慮しながら、先後の勝率を補正して計算していると考えられるので、こちらのモデルによる検証も意味があるといえるだろう。

この結果から興味深い点は、後手の方が強い棋士として挙げられている棋士では、特にTop5に振り飛車党が多いといえる。

https://shogidata.info/list/strategrylist_junni.html

この中から居飛車党・オールラウンダー・振り飛車党を括り出して、それぞれどのような特徴を持っているのか調べるということも、今後の課題となりそうだ。

また、統計解析については、独学で行なっているため、補正項の扱い方が適切ではない虞がある。もし良いアイディアがあれば、コメントをいただけると幸いである。

参考文献

gaiasky.hatenablog.com

先後差を考慮した将棋のプロ棋士の強さモデリング(1)

はじめに

将棋は先手有利であるという通説があるが、実際にその効果はどのぐらいなのだろうか。また、「振り飛車党は先後の不利を克服できる」という説があるが、その説はどのぐらい信憑性があるのか、という点に対して、PyMC3 を用いたベイズモデリングによって解析してみたいと思う。

本稿で解析する内容は以下である。

  • 2021 年のデータを用いて、棋士の強さモデリングの先行例を再現する (1)
  • 棋士共通の先手・後手の影響をモデルに組み込むことで、その差を除いた強さ・勝負ムラを見てみる (1)
  • 棋士ごとの先手・後手の影響をモデルに組み込むことで、先後の強さ差が大きい棋士、少ない棋士を見てみる (2)

MCMC の手法的な性質、またデータが少ないことに起因して、何度か実行した時に結果がばらつくことがある。そのため、ここでの結果はあくまで1回の試行によって得られたものであって、絶対的なものではないということを先に述べておきたい。

先行例の調査

こちらの記事では、 PyMC3 を用いて棋士の強さをモデリングしており、強さの平均と勝負ムラについて解析している。

gaiasky.hatenablog.com

また、次の記事では、 PyStan を用いて同様のモデルでモデリングしている。この例では、先手と後手を別々の棋士であるとみなしてモデリングすることで、先手後手の棋士の実力差を見ている。

qiita.com

ここでは、 これらの記事を参考にしながら、PyMC3 を用いて 先手後手の差を組み込んだモデル を作り、それを用いて最新の勝敗データを用いて以下のことを検討してみたい。

対象は2021年のデータを用いた。その中で、日本将棋連盟に所属するプロ棋士うしの対局であり、勝敗が判明しているもののみを対象とした。(対象となる対局は2330局、棋士数は174人)

f:id:sharply:20220225183656p:plain

のちの解析で利用するため、先手勝ちか後手勝ちかという情報も入れてある。

2021年のデータで同じ実験を再現してみる

まずは、2021年の勝敗データを用いた場合に、どのような結果になるか見てみよう。先行例2に倣ってそのモデルを表現すると、

  • 棋士の強さをμ、実力のムラをσとして、対局時の実力を normal(μ, σ) として定める。
    • 棋士の強さμの分散(すなわち正規分布の分散)の事前分布を一様分布として定める。
  • 実力が高いほうが対局に勝利する。具体的には、実力の差をシグモイド変換した確率値 p に対して、 その確率 p で 1 が出るような試行であったとモデリングする(ベルヌーイ分布)。

モデルコードは PyMC3 による先行例を参考にした。先行例では s_mu = pm.Normal('s_mu', mu=0, sd=100) としていたが、サンプリングの初期化の過程で、μ = -inf となるときに発散してしまうことがわかってきた。以下がエラーメッセージである。

SamplingError: Initial evaluation of model at starting point failed!
Starting values:
{'s_mu': array(0.), 'mu': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0.]), 'sigma_log__': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0.]), 'loser_perf': array([0., 0., 0., ..., 0., 0., 0.]), 'winner_perf': array([0., 0., 0., ..., 0., 0., 0.])}

Initial evaluation results:
s_mu             -5.52
mu                -inf
sigma_log__      38.98
loser_perf    -2142.05
winner_perf   -2142.05
z             -1615.73
Name: Log-probability of test_point, dtype: float64

そこで、ある程度上限の大きい一様分布で表現することにして、上記のエラーを回避している。 このことの意味は、実力の分散の事前分布を一様分布で与えている、ということになる。

pm.traceplot(trace, var_names=['mu','sigma'])

f:id:sharply:20220225000854p:plain

2021 年の対局における計算結果は以下。

棋士の強さの平均(強い順)

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
藤井聡太 2.982 0.458 2.173 3.853 0.03 0.021 228 553 1.01
渡辺明 1.834 0.48 0.896 2.694 0.028 0.02 303 674 1.01
永瀬拓矢 1.76 0.415 1.005 2.565 0.026 0.018 262 627 1.01
伊藤匠 1.726 0.452 0.929 2.601 0.024 0.017 352 871 1
出口若武 1.719 0.461 0.866 2.569 0.023 0.016 419 810 1.01

棋士の勝負ムラの平均(大きい順)

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
石田直裕 1.108 0.34 0.519 1.752 0.019 0.013 318 484 1.02
豊島将之 1.081 0.318 0.52 1.673 0.02 0.014 230 268 1.01
高崎一生 1.073 0.328 0.461 1.654 0.018 0.013 311 551 1
渡辺和史 1.071 0.335 0.48 1.709 0.019 0.014 296 704 1
佐藤和俊 1.065 0.336 0.495 1.72 0.02 0.014 268 488 1

棋士の勝負ムラの平均(小さい順)

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
丸山忠久 0.935 0.308 0.414 1.514 0.018 0.012 278 701 1.01
豊川孝弘 0.934 0.292 0.4 1.459 0.013 0.009 478 582 1
塚田泰明 0.932 0.287 0.437 1.465 0.013 0.009 446 888 1
永瀬拓矢 0.93 0.276 0.456 1.45 0.018 0.013 219 552 1.02
藤井聡太 0.912 0.284 0.411 1.406 0.02 0.014 189 299 1

興味深いこととして、藤井聡太五冠(記事執筆当時)は強さが高く、勝負ムラが最も少ない。言い換えるとどの対局においても平均的に高いパフォーマンスを発揮しているということがいえるだろう。

グローバルな先手後手の差をモデルに埋め込んだモデル

次のモデルでは、先手・後手の差には全体的な傾向(具体的には、先手の方が若干有利)があるとして、その傾向をモデルに反映させるということをおこなってみたい。こうすることで、先手・後手のバイアスを除いて強さや勝負ムラを計算するということができるのではないかと考えている。

モデルは色々試行錯誤したが、以下のように定めた。

  • 棋士の強さをμ、実力のムラをσとして、対局時の実力を normal(μ, σ) として定める。
    • 棋士の強さμの分散(すなわち正規分布の分散)の事前分布を一様分布として定める。
  • 実力が高いほうが対局に勝利する。具体的には、実力の差をシグモイド変換した確率値 p に対して、 その確率 p で 1 が出るような試行であったとモデリングする(ベルヌーイ分布)。
    • この時に、実力の差に対して、先手勝ちの場合に δ、後手勝ちの場合に -δ の補正を加える。この補正項によって、先手の得を陽に扱って解析することができる。
      • この δ は-1〜1の一様分布に従うというようにここではモデリングする。
      • 例えばδを平均0の正規分布に従うとモデリングすることもできるが、そうするとδが0に近いほど確率が高くなるというバイアスが逆に乗ってしまうと考えられる。そうであると考えることもできるが、ここでは補正項の値は-1〜1の間でランダムに選ばれると仮定する。
with pm.Model() as model:
    # prior
    s_mu = pm.Uniform('s_mu',upper=300)
    mu = pm.Normal('mu', mu=0, sd=s_mu, shape=len_players)
    sigma = pm.Gamma('sigma', alpha=10, beta=10, shape=len_players)
    #delta = pm.Normal('delta', mu=0)
    delta = pm.Uniform('delta', lower=-1, upper=1)

    loser_perf = pm.Normal('loser_perf', mu=mu[battles.Loser-1], sd=sigma[battles.Loser-1], shape=len(battles))
    winner_perf = pm.Normal('winner_perf', mu=mu[battles.Winner-1], sd=sigma[battles.Winner-1], shape=len(battles))
    sente_perf = delta * battles.FirstWin
     
    diff = winner_perf-loser_perf+sente_perf
    p = pm.math.sigmoid(diff)
    z = pm.Bernoulli('z', p=p, observed=np.ones(len(battles)))
pm.traceplot(trace, var_names=['mu','sigma','delta'])

f:id:sharply:20220225184406p:plain

棋士の強さの平均(強い順)

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
藤井聡太 3.003 0.509 2.06 3.962 0.041 0.029 156 346 1.01
渡辺明 1.839 0.484 0.915 2.735 0.032 0.023 230 585 1.01
伊藤匠 1.814 0.468 0.963 2.741 0.023 0.016 430 727 1
永瀬拓矢 1.8 0.434 1.025 2.607 0.029 0.021 217 709 1.01
出口若武 1.662 0.477 0.78 2.577 0.028 0.02 289 755 1

棋士の勝負ムラの平均(大きい順)

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
渡辺和史 1.105 0.343 0.498 1.735 0.02 0.015 285 602 1.01
山本博 1.096 0.34 0.482 1.718 0.022 0.016 224 446 1.01
伊藤真吾 1.086 0.35 0.461 1.721 0.019 0.013 334 559 1
船江恒平 1.082 0.341 0.478 1.719 0.019 0.014 292 706 1
高崎一生 1.08 0.339 0.482 1.747 0.019 0.014 289 269 1.01

棋士の勝負ムラの平均(小さい順)

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
竹内雄悟 0.93 0.291 0.432 1.514 0.019 0.013 228 498 1.01
田中寅彦 0.929 0.297 0.44 1.511 0.017 0.012 290 659 1.01
藤井聡太 0.921 0.268 0.446 1.424 0.021 0.015 158 332 1.01
池永天志 0.912 0.294 0.411 1.467 0.02 0.014 203 376 1
永瀬拓矢 0.887 0.278 0.425 1.413 0.02 0.014 190 342 1

まとめ

まずは、各棋士の強さ及び勝負ムラについて調査した。次に、先後の差をモデルに組み入れることで、先後の差を除いた各棋士の強さ及び勝負ムラについて調査した。

次回は、モデリングに各棋士の先後ムラを組み込んでみたいと思う。

壊れた Mac のデータをシングルユーザーモードで NTFS の USB メモリに救出する

MacSSD が死んでしまった時の復旧方法をメモしておく。症状としては以下の記事と同じであった。

qiita.com

第一の手段としては、ディスク修復ツールを使うというものだが、 First Aid を使っても Error Code : 8 で失敗してしまった。

そのため第二の手段として、シングルユーザーモードで以下のようなコマンドを実行し、救出できるファイルをUSBメモリにコピーするということを考えたい。(Mac のバージョンによってコマンドの細部が異なることがあるので、シングルユーザーモードログイン時に表示されるメッセージを参考にしてほしい。)

mount -rw /
mkdir -p /tmp/mnt/usb
fstyp /dev/disk2s1
mount -t hfs /dev/disk2s1 /tmp/mnt/usb # hfsの場合
mkdir /tmp/mnt/usb/xxx

USB 接続のハードディスクが ntfs でマウントされていた場合は、普通にマウントしてしまうと read-only になってしまうので、新しいディレクトリを作る処理を実行できない。そのため、マウントし直す必要がある。

まず一旦今マウントしたドライブをアンマウントしなければならない。

umount /tmp/mnt/usb

マウント時にオプションを指定すると、読み書き可能な状態でマウントすることができる。

mount -t ntfs -o rw,auto,nobrowse /dev/disk2s1 /tmp/mnt/usb
mkdir /tmp/mnt/usb/xxx
rsync -a --partial --append-verify -v /Users/xxx /tmp/mnt/usb/xxx

これで rsync ができたら一件落着かというと実際そんなことはなく、 rsync 実行中にコピーがスタックしてしまって困っている。これは SSD のエラーに起因するものであると思われるが、そうした読み取りを妨害するような不良セクタのようなものをスキップすることはできないか?というのが次に期待したいところではある。

superuser.com

しかしもしそれをやろうとすると rsync ではなく、 ddrescue のように丸ごとディスクをコピーする方法を取るしかないという感じであった。

参考ページ

note.com

www.fixes.pub

blog.daich.org

宇野常寛「遅いインターネット」から想像する

ここでは、宇野常寛の「遅いインターネット」を読んで、政治の司法化について議論してみたい。

第1章においては期間限定でこちらで公開されている。

slowinternet.jp

宇野常寛氏については当ブログにおいても着目しており、「ゼロ年代の想像力」については以下の記事でも触れている。

sharply.hatenablog.com

提言について

本書では第1章や第3章の末尾において、これからの政治に対して3つの提言をしている。これを簡単に要約すると以下のようになるだろう。

  1. 民主主義に制限を加え、立憲主義にパワーバランスを傾けるべきである
  2. 普通選挙だけに決定権を集中させず、ボトムアップの意思決定のシステムによってリスクを分散せよ
  3. 自己幻想から「自立」せよ

2つ目の提案については、台湾などの civictech の例を援用して、政治意識の高い「市民」でも、扇動に踊らされる「大衆」でもなく、「人間」として政治に参加できるような、ボトムアップの仕組みを整えるべきだと提言している。また3つ目の提案である、自己幻想からの自立については、吉本隆明の議論を現代風にアレンジしながら説明しており、自己幻想は具体的には、SNSによって肥大化した自己意識、自己像のことを指しており、そうした自己意識とうまく付き合っていくための処方箋として、「遅いインターネット」という概念を提唱している。詳しい議論は本書に譲る。

ところで、1つ目の提案、すなわち「民主主義を半分あきらめる」ことについては、本書についてはそれほど触れられていない。立憲主義の側面を強める、これはすなわち司法の持つ権力を強めるバランス調整であると述べられており、抽象的違憲審査制の導入が例示されている。著者がこの本ではないどこか別のところで既により詳細に議論している可能性はあるが、ここでは本書で述べられている議論の上で、立憲主義を強めた際に何が起こるのか、について少し考えてみたいと思う。

政治の司法化

本書では立憲主義の側面を強める方法としては司法権の強化を例示している。実際に日本の司法権が弱いということは、 Björn Dressel (2012) The Judicialization of Politics in Asia によっても示されている。その中で日本は、「司法の独立性が高い」というのと、「司法による政治の介入度が低い」という2つの特徴を持つというように記述されている。アジアにおいては、「司法の独立性が高い」上に、「司法による政治の介入度が高い」国として韓国やインドネシア、フィリピンが、また「司法の独立性が低い」かつ「司法による政治の介入度が高い」国として、タイ等が挙げられている。

政治の司法化というワードは、政治問題の解決のために司法に頼る傾向のことを指す用語として提案されたものであるが、タイは政治の司法化というより、司法の政治化、すなわち司法と政治が一体化し、司法が政治に積極的に関わるというという状況にまで進んでいるとしている。これが、立憲主義を強めた時に起こりうる一つの現象であると解釈することができるだろう。ここでは具体的にそれが指し示す状況が何であるかについてみてみよう。

タイの政治制度

日本とタイの政治制度は、似ているところもあれば違うところもある。似ているところとしては、どちらも1990年代に選挙制度改革を行い、小選挙区比例代表を組み合わせた選挙制度を採用した。相違点としては、日本では非自民連立政権としての細川政権・羽田政権、また2009年の民主党政権のほかは、常に自民党が政権与党であり続けた。これに対してタイでは地方部からの支持を集めたタクシン派と反タクシン派の間で何度か政権が揺れ動くなど、政権交代が何度か発生していることがわかる。*1

タイにおいては小選挙区制が導入されたときに、選挙で第一党となったのは地方部を基盤としたタクシン派であり、それは従来のエスタブリッシュメントとは相容れない政党であった。その後、タイではエスタブリッシュメントによる反動が発生し、例えばタクシン派の勝利で終わった2006年の総選挙は、憲法裁判所が違憲の判決を下して政治が混乱し、その後軍部クーデターによってそもそも民主主義自体が損なわれてしまう、という事態になってしまった。その後も、タクシン派が政権を握るたびに、その反動としてクーデターが発生するという構造は変わっていない。クーデターに至る前に、司法が何らかの形で選挙に対して異議を唱え(例えば選挙が違憲であったり、政党の解党命令を出したりするなど)、それによって政治が混乱し、それがクーデターの引き金となっている。このことをさして、裁判所が「政治の司法化」をしている、すなわち司法権が政治を担うような状況になっていると言われることもある。

タイの例では、民主主義に対して、さまざまな形で司法が圧力をかけることとなり、その司法が実質的にはエスタブリッシュメントに支持されていたという状況から、立憲主義エスタブリッシュメントを利する方向に働いたと言えるだろう。

アメリカの政治制度

アメリカ合衆国の場合を見てみると、民主主義の側面は四年毎の大統領選挙、および上院・下院の選挙によって担保されている。それに対して、9人の最高裁判事は終身制であり、空席ができた際の大統領が指名することができるという制度になっている。最高裁判事の構成では、リベラル派と保守派の均衡が保たれるのが今までの平時であった。しかし、2022年現在において、保守派が6人に対してリベラル派が3人と、偏りが生じている。この構造は、再び最高裁判事に空席が生じるまで変わらない。そのため、当分の間は保守派に有利な判決が下される傾向が続くというように考えらえる。

このことから、司法権を担っている最高裁における判事の構成に、実際に行われる政治的決断が強く依存してしまうということが示唆される。

www3.nhk.or.jp

まとめ

宇野の指摘通り、民主主義の負の側面を克服するために、立憲主義を強める、言い換えると司法権を強化するという施策自体は、日本以外の国々でもみられる現象ではある。しかしながら政治の司法化、さらには司法の政治化と呼ばれる現象が発生している国も見られ、その運用には注意が必要となるだろう。

参考文献

Björn Dressel "The Judicialization of Politics in Asia"

外山文子「タイ民主化憲法改革: 立憲主義は民主主義を救ったか」

重冨真一「タイの政治混乱――その歴史的位置――」

https://www.ide.go.jp/Japanese/IDEsquare/Eyes/2010/RCT201002_001.html

重冨真一「続くタイの政治混乱――あぶり出された真の対立軸」

https://www.ide.go.jp/Japanese/IDEsquare/Analysis/2020/ISQ202010_001.html

*1:赤シャツ派、黄シャツ派に別れたデモのことを覚えている方も少なくないだろう

Java 軽量 Web フレームワーク調査

tl;dr

Java で利用できる軽量 Web フレームワークを調査した。特に、 Helidon について最後に説明する。

なぜ Web フレームワークが必要となるか

まずは冗長かもしれないが、 Web フレームワークがなぜ必要となってきたか、について1つの説明を考えてみたい。

1つのウェブサービスを実現するために、複数のパーツを組み合わせてそれぞれをうまく動かすことが必要となる。パーツというのは、例えばTwitter のようなサービスを作ると考えた時に、ユーザ情報やツイート情報などを管理しておくためのデータベース、データベースの内容をクライアントに伝えるサーバ、サーバから受け取った内容を人間が見やすい形にして表示する HTML、CSSJavaScript などの実装が必要になってくる。

これらは、大別してDBサーバ+アプリサーバ(バックエンド)+クライアント(フロントエンド)の3つの要素に分けることができるだろう。その中で、具体的にはバックエンドとフロントエンドにおいてはある程度の量の実装が必要である。これらの実装のためには、データベースの知識、 web の知識、プログラミング言語の知識、フロントエンドの知識などが必要となり、幅広い知識を薄く広く知っていないといけないというのが web 開発の全体的な特徴である。

そのため、バックエンド開発では一般にフレームワークと呼ばれる、webアプリケーションを実装する上で必要な機能を最初から実装してあって、利用者はその関数を呼ぶだけでwebアプリケーションを実装できるようになっているライブラリ群を用いることがある。これによって、同じ目的に利用するコードを別々の人が何度も書かなければならなくなってしまうことを防ぐうえ、最初からセキュリティに配慮されたシステムを構築することがある程度可能になる。

こうしたフレームワークの上で、バックエンドはMVCアーキテクチャによって実装することが、一つのプラクティスとして確立している。MはModel、VはView、CはControllerである。Modelはしばしばデータベースと密に連携し、データベースに入っているデータをオブジェクトとして扱えるような機能が実装されている。ViewはModelの内容をhtmlとしてレンダリングする。ControllerはModelのデータをViewに受け渡す媒介の役割を果たす。実際にはバックエンドの役割は多岐に渡り、ユーザ認証、アクセス認可、セッション管理、テンプレートエンジン、ルーティング、ORM(データベースとの連携)などが含まれる。

このとき一般には、車輪の再発明を防ぐためにフレームワークを用いて実装することになる。こうしたフレームワークは、「フルスタック型」と「軽量型」の2通りに分けられると考えられる。

フルスタック型と軽量型フレームワーク

フルスタック型

例: Ruby on Rails/HANAMI, Django(Python), Yesod(Haskell), Phoenix(Elixir), Spring boot(Java), Sails.js(Node.js), Revel(Go), ...

フルスタック型は、そのライブラリとそのプラグインを用いるだけで、基本的な web サービスの提供ができるように構成されたライブラリ群で、しばしば開発者が細かい設定をせずとも、デフォルトの設定項目をそのまま利用するだけでデータベースへの連携やログイン機能の実装など、Web サービスに必要な機能が提供される、といった特徴を持つ。

バックエンドとフロントエンドを密に実装することになる。例えば、1つのソースコードルートの中でバックエンドのコードとフロントエンドのhtml, jsが共存するような形になる。Webアプリはこれらのフレームワークの上でバックエンドからフロントエンドまで一括して作ることができる。このことでシステムは複雑になりがちだが、コードがモノリシックなので、開発の初期にはデータ構造の変更に対して反映が容易という利点がある。

軽量型

例: Sinatra(Ruby), Flask(Python), ...

基本的には、バックエンドのDBと接続して、jsonを返す簡易サーバーを書くことになる。データに対する処理のロジックを記述し、スマホ用のAPIとwebアプリ用のAPIだけをそれぞれ実装するというシンプルな構成になる。

バックエンドとフロントエンドをわけて実装することになり、これとは別にフロントエンドはAPIから受け取った情報をもとに、ビューを提供する。フロントエンドはバックエンドから独立して実装出来るので、かなり自由に実装出来る反面、認証などの連携が面倒であったり、バックエンドでAPIの変更した場合、フロントエンドに反映することが必要となる。

サービスの肥大化を防ぐことができるという利点がある一方で、こうした軽量型のライブラリでは先述の要件に対して必要最小限の機能しか提供されていないことも多く、コード量が増えてしまうという点もある。

Java における Web フレームワーク

Java でウェブサーバーを実装する場合、重厚なものとしては Spring があるが、さまざまな理由から Spring よりも軽量な(あるいは開発者の実装量が少ない)フレームワークを選択したくなる、ということがある。

Spring | Home

例えば、 Spring, Vert.x, Helidon, Quarkus の4つの web フレームワーク を比較したベンチマークによれば、起動時間や平均レスポンスタイムがSpring に比べてその他3つのフレームワークの方が早い、という結果が報告されている。

blogs.itemis.com

どうしてこのような差が生じているかということについては、さまざまな要因があるが、1つには利用しているサーブレットコンテナ( Web Engine )の違いという要因があると考えられる。サーブレットコンテナは、リクエストやレスポンス・セッション管理など、Web サーバーのコアになる機能を備えており、Web フレームワークは何らかのサーブレットコンテナ上に、さまざまなラッパー関数を提供していると考えることができる。

Spring framework 上で実装されている web フレームワークである Spring boot では、 Tomcat, Jetty, Undertow をサーブレットコンテナとして利用することができる。ベンチマークとしては以下である。

www.baeldung.com

Jetty は Tomcat の軽量版、のような捉え方をすることができる。実際に軽量フレームワークでは、 jetty と netty が多く使われている。この2つの違いは何だろうか。

stackoverflow.com

gist.github.com

これらを要約すると、 Jetty は軽量であること、Netty は non-blocking であることがそれぞれと特性であり、使い分けるとするならば、ネットワークプロトコルを頻繁に扱い、それを非ブロッキングにしたい場合は、Netty、軽量のHTTPサーブレットコンテナが必要な場合は、Jetty、というように使い分けることはできるだろう。とはいえ、開発の初期にはこれらの違いよりも、フレームワークとしての完成度であったり成熟度の方が問題になるケースが多いだろう。

軽量web フレームワークを比較したスライドとして、以下のものが詳しい。

https://www.jfokus.se/jfokus20-preso/The-ultimate-microframework-smackdown.pdf 

Helidon

Oracle 謹製フレームワーク Helidon についてここで少し紹介する。

https://helidon.io/#/

https://www.infoq.com/jp/articles/helidon-tutorial/

Helidon 2.0 が既にリリースされているが、ウェブ上の情報では Helidon 1.0 の情報も交じっていることが多いので注意が必要である。

Helidon MP と Helidon SE の違い

www.infoq.com

Helidon SE は最もプリミティブな機能を搭載したフレームワークであり、 javalin といったフレームワークと同じレベルのフレームワークであると infoq の記事では整理されている。

Helidon SE は上記のようにコア機能に絞っているのに対して、 Helidon MP では、 CDI (Context and Dependency Injection) extension と呼ばれる、主にデータベース接続のための一種のプラグインが提供されている。上記のページでは、下記の5つが紹介されている。

ベイズ統計を用いた投票モデリングを目指して

はじめに

投票をある程度解釈可能なモデルによってモデリングすることによって、そのモデルのパラメータを変更した際に、選挙結果がどのぐらい変化するかというのを見積もることができる可能性がある。まず選挙に対するモデリング手法について、先行例として調査したものを紹介する。

また、実際はまだモデリングまで至っていないが、投票結果を解析するために、人間の解釈が可能なモデルを今後作っていきたい。その上で検討しているアイディアについても、あわせて記す。

先行例の調査

ベイズの考え方を用いると、投票行動は、ディリクレー多項分布モデルによって表現することができる。それぞれの分布について簡単に説明する。

類似した問題として、サイコロを複数回振った時に、どの出目が何回出たのか、というのをモデリングしたいと考える。この時、サイコロの出目は複数存在し、その中で1つを選ぶ、ということを表現する分布としてカテゴリカル分布というものがある。この試行を複数回繰り返すような場合では、多項分布を用いて表現することができる。

ただしサイコロの場合は等確率だが、候補者が選ばれる確率は等確率ではないこともある。この時、値の選ばれ方の性質(例えばサイコロであれば全ての出目が出る確率は同じ、投票であれば、1回の投票につき、候補者が選ばれる確率というのは候補者によって異なる)は、何らかの事前分布によって表現したい。そのためここでは、ディリクレ分布というものを用いることにする。

ディリクレ分布におけるパラメータ  {\alpha} によって、i 個の事象がそれぞれ  { \alpha_{i}-1} 回発生したときに、各事象の起こる確率が  {x_{i}}  である確率分布が与えられる。この分布は、あらゆる i に対して  { \alpha_{i}=1} の時は一様分布になり、 { \alpha_{i}} が大きくなればなるほど、確率分布の分散が小さくなっていく。

www.behind-the-enemy-lines.com

例えば3回、  { \alpha_{i}=1} のディリクレ分布から値を取る(サンプリングする)という試行を行うと、以下のようにランダムに3人の候補者に対する投票確率を得ることができる。(和は1になっている。)

f:id:sharply:20220204001549p:plain

ディリクレ分布は多項分布の共役事前分布になっている。このディリクレ-多項分布を用いて、投票をモデリングすることができる。

blog.byronjsmith.com

例えば、1988年のブッシュvsデュカキスのアメリカ大統領選挙モデリングする例がいくつか実装されている。1988年9月25日、大統領選挙の討論の前後で、ブッシュとデュカキスのどちらを支持するか?という世論調査を行ったとき、その結果がどのぐらい変化したか?というのをここではモデリングしている。

notebook.community

f:id:sharply:20220204000554p:plain

f:id:sharply:20220207180724p:plain

もっと複雑なものとして、パリにおけるEU・大統領・地域議員の選挙において、それぞれの選挙区における6つの主要政党(極左、左派、緑の党、中道、右派、極右)の得票数をモデルに組み込んだ Jupyter Notebook が公開されており、まだ全貌を読み解くことはできていないが、モデリング手法の検討に向けた大きな手がかりになると考えられる。

https://nbviewer.org/github/AlexAndorra/pollsposition_models/blob/master/district-level/munic_model_prod.ipynb

これについては、 YouTube に紹介のプレゼンテーションも上がっている。

www.youtube.com

これとは別の方法として、ベイズ回帰を用いたモデリングをするという方法もある。具体的には、個人の得票数に対して、グループによるバイアスがどの程度あるかを調べている。

gaiasky.hatenablog.com

モデリング

ここでは投票について、どのようなモデルが作れるかについて、いくつかの検討を記載する。

  • パターンA: ディリクレ分布によって表現される投票先のパターンについて、その重み付き和として表現される、各選挙区ごとの有権者の好みを多項分布によって表現する、ディリクレー多項分布モデルによって扱う。
    • 人間がとりうる投票パターンは限られている(選挙区ごとに完全にバラバラではなく、ある程度一致している)という仮定の元で、少ない投票パターンの重ね合わせで各選挙区の得票分布を説明することで、どんな投票パターンがあると、現実のそれぞれの選挙区での得票分布が説明できるかを探す。
    • Pros: 実装しやすい。
    • Cons: 人間がとりうる投票パターンは、政党の好みをディリクレ分布で表現するよりも、類似した政治的主張をする政党に投票する確率に相関がある、とみなすほうがより現実に近いのではないか?と考えることができる。
n_patterns = 4
n_candidates = 3
n_regions = 2

with pm.Model() as polling_model:
    
    # initializes the Dirichlet distribution with a uniform prior:
    shape = (n_patterns, n_candidates)
    a = np.ones(shape)
    shape2= (n_patterns, n_regions)
    b = np.ones(shape2)
    shape3 = (n_candidates, n_regions)
    np.dot(a.T,b)

    # This creates a separate Dirichlet distribution for each debate
    # where sum of probabilities across candidates = 100% for each debate
    theta = pm.Dirichlet("theta", a=a, shape=shape)

    coeff = pm.Dirichlet("coeff", a=b, shape=shape2)
    
    vote_dist = pm.math.dot(theta.T, coeff)
    
    responses = pm.Multinomial("responses", n=n, p=vote_dist, observed=y, shape=shape3)

f:id:sharply:20220207235219p:plain

このモデルに対して、有権者の政党に対する好みにおける仮定を反映させたいと考える。この仮定は、政治傾向が類似した候補者にはある程度の確率で投票するが、政治傾向が遠い候補者に投票する確率が低いという想定である*1

この想定の中で、候補者(あるいは政党)を、その政治的な立ち位置によって一次元の軸上に表現できるのではないかという、いささか大胆な仮定を置いている。つまり、どの候補者を選択するかというのを単純なカテゴリとして分類するのであれば、ディリクレ分布のようなモデルを使うのは妥当かもしれないが、どの候補者を選ぶかというところに何らかの指向性があるとすると、別の分布を利用できることが考えられる。

ここでは、順序予測 (ordinary prediction) と同じような考え方を用いてモデリングすることを想定している。順序予測というのは、例えば「あなたは今幸福かどうかを1(幸福ではない)~10(幸福である)の値で表現した時に、 どのぐらいの値ですか(整数)?」というアンケートにおける分類の値であったり、「競馬による着順は何位か?」というように、カテゴリではあるが、隣り合ったカテゴリは便宜上隔てられているだけであって、その間には何らかの連続性があるものに対する予測のことを指している。

これはどのように実現できるかというと、積分布関数上の閾値 x_1, x_2, ... の差 x_2-x_1, x_3-x_2, ... を確率とするようにモデルを作ることで、その閾値の分布を、不確かさを含めてモデリングできるようになる。このようなアイディアに基づいて、パターンBとパターンCを考えている。

  • パターンB: 政党の好みを一次元のそれぞれの選挙区において、有権者が平均μ、分散σの正規分布に従うとする。正規分布か何かでえられるような閾値x_1, x_2, ... によって、その有権者分布を候補c_1, c_2,... に分割し、累積分布関数の差によって得られた確率p_1, p_2, ... によって表現される各選挙区ごとの有権者の好みを、多項分布によって表現する。
    • パターンBでは、政党の好みが、集団ごと異なる正規分布で表現されるという仮定を置いている。
    • Pros:
      • 選挙区毎の有権者の政治傾向分布を表現することができる。
      • 候補の政治傾向を一次元で表現できる。
    • Cons:
      • 政党をポジションによって一軸にソートできない場合、政党のポジションは何によって定義すべきか?
      • 全ての選挙区で同じ切り口で切ってしまうと、単に選挙区毎の有権者の思想分布が違うという結論になってしまって、政党ごとの特徴を見いだすのが難しくなってしまう可能性はある。
@as_op(itypes=[tt.dmatrix, tt.dscalar, tt.dscalar], otypes=[tt.dmatrix]) 
def outcome_probabilities_mat(theta, mu, sigma):
    out = np.empty((theta.shape[0]+1, theta.shape[1]), dtype=np.float64)
    n = norm(loc=mu, scale=sigma)       
    cdf = np.vectorize(lambda x: n.cdf(x))
    theta_cdf = cdf(theta).T

    out[0, :] = theta_cdf[0, :]      
    out[1, :] = np.max([np.repeat(0, theta.shape[1]), theta_cdf[1, :] - theta_cdf[0, :]], axis=0)
    out[2, :] = 1 - theta_cdf[1, :]

    return out

thresh_tmp = np.repeat([(0.35, 0.65)], n_regions, axis=0)

with pm.Model() as polling_model_complex:
    
    shape = (n_patterns, n_candidates)
    shape3 = (n_regions, n_candidates)
    ps = pm.Normal('theta', mu=thresh_tmp, #tau=np.repeat(1/2**2, len(thresh4)),
                      shape=(n_candidates - 1, n_regions))#, observed=thresh_obs4)
    p1 = pm.Uniform('p', -1, 1)
    sds = pm.Uniform("sds", 0.1, 10)

    vote_dist = outcome_probabilities_mat(ps, p1, sds)

    responses = pm.Multinomial("responses", n=n, p=vote_dist.T, observed=y, shape=shape3)

f:id:sharply:20220204000612p:plain

  • パターンC: 日本全体で正規分布に従う1つの指向モデル(いわゆる選挙の風)を持っていて、その上でそれぞれの選挙区において、正規分布か何かでえられるような閾値 x_1, x_2, , ... によって、その有権者分布を候補 c_1, c_2,... に分割し、累積分布関数の差によって得られた確率 p_1, p_2, ... によって表現される各選挙区ごとの有権者の好みを、多項分布によって表現する。
    • Pros:
      • 政党間の連合が存在している場合と存在していない場合との間で、どのぐらい支持を得ている層が違うかが分かる可能性がある。
    • Cons:
      • 政党をポジションによって一軸にソートできない場合、政党のポジションは何によって定義すべきか?
      • 単純に政党別の得票率の帯グラフとは何が違うのだろうか?

これらについて実際のモデルの適用については、具体的に議論しておらず、今後の課題となっている。

*1:具体的には右派の有権者は極右〜中道の候補者に投票しがち、左派の有権者極左〜中道の候補者に投票しがち、という傾向。

D3 で Sankey Plot を書く

tl;dr

JavaScript のライブラリである D3 を使って、 Sankey プロットを描画する方法について、ウェブ上のリンクを収集した。

Sankey diagram

d3 のデモサイト

www.d3-graph-gallery.com

Sankey diagram with d3v4

bl.ocks.org

d3 で Sankey diagram を描画するときのデータの説明

www.d3noob.org

循環参照があるような Sankey plot を描画する例

codesandbox.io

bl.ocks.org

bl.ocks.org

水平・垂直にノードを移動できる Sankey diagram

bl.ocks.org

JavaScript に動きをつける方法

散布図の例

Responding to D3 events in TypeScripthstefanski.wordpress.com

React のD3 サーバーサイドレンダリング

swizec.com

vtk.js のデモ

https://kitware.github.io/vtk-js/examples/

英文要約が欲しい

はじめに

要約の取得方法として、自然言語処理的な手法を用いて行いたいと考えた時、そのやり方は抽出型と生成型に分類できる。抽出型はそのものずばり、文集合のなかから要約に相当する文を抽出する手法である。たとえば、LexRank等の手法を使って、文のなかでグラフ構造を構築して代表となる文を抽出する、というやり方が考えられる。これに対して生成型は、要約となるような文を生成するモデルである。

多くのユースケースでは抽出型があれば十分だろう。例えばしかし1文自体が長かったり、格調高かったりすると、もう少し要約した文が欲しくなる場合がある。OPENAI は そうしたAPIのβ版を公開しており、例えばtl;dr 要約であったり、小学2年生でもわかるような文に要約するといったことを対話的に行うことが可能である。

実際に試してみると、tl;dr 要約だと基本的には1行要約になってしまって短すぎる、小学2年生でもわかるような文は語彙が簡単になりすぎるので、嬉しくない場合がある。では例えば小学6年生でわかるような文レベルで要約してくれるかというと、同じ文が連続して出力されたりするなど、ディープラーニング特有の解釈が難しい挙動のために実際はうまくかないこともある。

openai.com

そこでここでは、三行要約ぐらいの量の要約を得ることを目指して、無料で試せる範囲で、英文要約を試みてみたいと思う。

Transformers を試す

Python で使える Transformers というライブラリは、様々な自然言語処理のモデルが統一的なインターフェース上で利用可能な形で提供している。GoogleのT5(Text-to-text Transfer Transformer) モデルには、要約する機能が付いているので、それを利用してみたい。 ここで、適当に英語の論文を突っ込んでみたいと思う。今回要約のために利用した論文は以下のものである。

arxiv.org

!pip install transformers -U
from transformers import pipeline

summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base", framework="tf")

text = '''
<ここにアブストラクトを入れる>
'''
summarizer(text, min_length=25, max_length=150)
summarizer(text, min_length=200, max_length=300)

max_length パラメータを変えることで、出力される要約の量を調節することができる。500 words を越えるような文を要約しようとすると warning メッセージが表示されるので、それより短い単語量の文を入れることが推奨される。

1行目の場合の出力は以下であった。

attention mechanisms play a central role in recurrent neural network (RNN) models . a recent paper claims that attention is not explanation' . we propose four alternative tests to determine when/whether attention can be used as explanation .

大文字小文字のルールがおかしくなっているというのが気になるところであるが、文が選択され、それが一部縮約された形で表現されている。しかしこれはたまに問題があって、縮約前と縮約後で文意が代わってしまうことがある。たとえば、主語に対する限定修飾が抜けてしまって主語がクソデカになってしまう、といった場合が観測される(ここではそうした問題は発生していないが)。

しかし、2行目の場合(min_length=200にした場合)の出力は以下のようになっている。

attention mechanisms play a central role in NLP systems, especially within recurrent neural network (RNN) models . a recent paper claims that Attention is not Explanation\' . we propose four alternative tests to determine when/whether attention can be used as explanation . the results show that even when reliable adversarial distributions can be found, they don\'t perform well on the simple diagnostic - indicating that prior work does not disprove the usefulness of attention mechanisms for explainability, we show versiune . n s - \xad - n s " -- . .- s. ss s- .. n. .s ns -s . "

途中まではまともそうな文章にみえるが、途中から出力が意味を成さない文字列になっており、残念な感じになっている。存在しない単語までも出力されるようになってしまっている。

まとめと課題

Transformer ライブラリを用いて英文要約を試みた。出力が短いような場合では、うまくいく例もあるが、たまに挙動がおかしくなることがあるので注意も必要である。

論文であれば、アブストラクトを要約する必要はあまりない。本文は要約したいこともあるが、あまり長いとこのモデルに突っ込むことが難しくなる。そもそも、良い論文であれば各パラグラフの先頭の文を飛ばし読みすれば、それがだいたいの要約になっているということを期待しても良いはずであるが、それよりも”良い”要約がそもそも何であるか、というのは、まだ残された解決すべき問題である。個人的には、何らかの構造(起承転結など)があるのであれば、それを抜き出すという要約は期待したい気がする。あるいは、日本語話者として欲しいのは単なる全文翻訳かもしれない。