目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
Mbtree_Anim のバグの修正と改良
前回の記事では αβ 法での枝狩りが行われたノードの数などの視覚化を行うために、Mbtree_Anim で表示されているアニメーションのフレームまでの、下記の A、P、M、R の値を表示するという改良を行いました。
記号 | 意味 | 見出し |
---|---|---|
A | αβ 法で計算を行ったノードの数 | 計算済 |
P | αβ 法で枝狩りを行ったノードの数 | 枝狩り |
M | ミニマックス法で計算を行ったノードの数。M = A + P で計算できる | 合計 |
R | M に対する A の比率。A / M で計算できる | 割合 |
しかし、現状では 枝狩りが行われた際 に、いくつのノードの枝狩りが行われたか が わかりづらい という問題があります。言葉の説明ではわかりづらいので具体例を挙げます。
下図は下記のプログラムを実行し、4485 フレーム目を表示した際の図で、図の「β 狩り」の文字が赤く表示されていることから、次のフレームで β 狩りが行われる ことがわかります。
from tree import Mbtree, Mbtree_Anim
mbtree = Mbtree(algo="df")
mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)
実行結果
実際に、次の 4486 フレーム目では下図のように枝狩りが行われたノードがさらに暗く表示されます。その際に、枝狩りが行われたにも関わらず「枝狩り」の 右の数字が 9179 のまま変化しない という問題が発生していることがわかります。
ただし、その次 の評価値が確定した 4487 フレーム目では下図のように 「枝狩り」の右の数字 が 9179 から 14694 に 大きく増え、赤枠のノードの 評価値が確定 したので 「計算済」の右の数字が 1 増えています。
上記から、以下の 2 種類の問題がある事がわかります。
- 枝狩りが行われた際 に、「枝狩り」の右の数値が増えるフレーム が 1 つ遅れる
- 枝狩り によって処理が省略された ノードの数 は 14694 - 9179 = 5515 という 計算を行うことで知ることができる が、そのような計算を行うのは面倒 である
前者についてはバグなので修正し、後者については枝狩りが行われた際に 処理が省略されたノードの数を表示 するという改良を行うことにします。
バグの修正
最初に、枝狩りの数値の表示が 1 フレーム遅れて増えるバグを修正します。このバグの原因について少し考えてみて下さい。
バグの原因の検証と修正
下記は、calc_score_by_ab
の中で、max ノードの 子ノードの評価値が計算された次のアニメーションのフレームの処理を記録 するプログラムで、以下のような処理を行っています。この処理にバグの原因があるので、何が原因であるかについて少し考えてみて下さい。
-
1、2 行目:このフレームで行われた処理に関するデータを
nodelist_by_score
属性とablist_by_score
属性の要素に追加する -
4 ~ 8 行目:β 狩りが行われた場合に残りの子ノードに対して
assign_pruned_index
を呼び出すことで、枝狩りが行われたノードに対する処理を行う。枝狩りが行われたノードの数 を表すnum_pruned
属性の値を増やす処理 は、assign_pruned_index
で行われる
1 self.nodelist_by_score.append(node)
2 self.ablist_by_score.append((alpha, beta, None, "update",
3 self.num_calculated, self.num_pruned))
4 if score >= beta:
5 index = node.children.index(childnode)
6 for prunednode in node.children[index + 1:]:
7 assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
8 break
問題の原因は以下の通りです。
-
β 狩りが行われた場合 は 4 ~ 8 行目 の処理によって枝狩りが行われた数を計算して
num_pruned
属性の値が更新 される -
このフレームに関するデータ は、その前の 2 行目で
ablist_by_score
に追加される ので、追加されたデータの中のself.num_pruned
は、β 狩りが行われる前 の枝狩りが行われたノードの 数を表している
従って、このバグを修正するためには ablist_by_score
にフレームのデータを追加する処理を、下記のプログラムのように β 狩りに関する処理の後で行う 必要があります。
-
9、10 行目:4 行目の後に記述していた
ablist_by_score
にフレームのデータを追加する処理を、β 狩りに関する処理を行った後に移動する - 5 ~ 8、11、12 行目:β 狩りに関する処理を行った後で上記の 9 行目の処理が行われるようにするために、8 行目の後にあった break 文を削除し、β 狩りに関する処理を行った後の 11、12 行目にその処理を移動する
- 13 ~ 21 行目:min ノードに対する処理でも、上記と同様の修正を行う
1 from marubatsu import Marubatsu
2
3 def calc_score_by_ab(self, abroot, debug=False):
元と同じなので省略
4 self.nodelist_by_score.append(node)
5 if score >= beta:
6 index = node.children.index(childnode)
7 for prunednode in node.children[index + 1:]:
8 assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
9 self.ablist_by_score.append((alpha, beta, None, "update",
10 self.num_calculated, self.num_pruned))
11 if score >= beta:
12 break
元と同じなので省略
13 self.nodelist_by_score.append(node)
14 if score <= alpha:
15 index = node.children.index(childnode)
16 for prunednode in node.children[index + 1:]:
17 assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
18 self.ablist_by_score.append((alpha, beta, None, "update",
19 self.num_calculated, self.num_pruned))
20 if score <= alpha:
21 break
元と同じなので省略
22
23 Mbtree.calc_score_by_ab = calc_score_by_ab
行番号のないプログラム
from marubatsu import Marubatsu
def calc_score_by_ab(self, abroot, debug=False):
def assign_pruned_index(node, index):
node.pruned_index = index
self.num_pruned += 1
for childnode in node.children:
assign_pruned_index(childnode, index)
def calc_ab_score(node, alpha=float("-inf"), beta=float("inf")):
self.nodelist_by_score.append(node)
self.ablist_by_score.append((alpha, beta, None, "start",
self.num_calculated, self.num_pruned))
if node.mb.status != Marubatsu.PLAYING:
self.calc_score_of_node(node)
if node.mb.turn == Marubatsu.CIRCLE:
alpha = node.score
else:
beta = node.score
else:
if node.mb.turn == Marubatsu.CIRCLE:
for childnode in node.children:
score = calc_ab_score(childnode, alpha, beta)
self.nodelist_by_score.append(node)
self.ablist_by_score.append((alpha, beta, score, "score",
self.num_calculated, self.num_pruned))
if score > alpha:
alpha = score
self.nodelist_by_score.append(node)
if score >= beta:
index = node.children.index(childnode)
for prunednode in node.children[index + 1:]:
assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
self.ablist_by_score.append((alpha, beta, None, "update",
self.num_calculated, self.num_pruned))
if score >= beta:
break
node.score = alpha
else:
for childnode in node.children:
score = calc_ab_score(childnode, alpha, beta)
self.nodelist_by_score.append(node)
self.ablist_by_score.append((alpha, beta, score, "score",
self.num_calculated, self.num_pruned))
if score < beta:
beta = score
self.nodelist_by_score.append(node)
if score <= alpha:
index = node.children.index(childnode)
for prunednode in node.children[index + 1:]:
assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
self.ablist_by_score.append((alpha, beta, None, "update",
self.num_calculated, self.num_pruned))
if score <= alpha:
break
node.score = beta
self.nodelist_by_score.append(node)
self.num_calculated += 1
self.ablist_by_score.append((alpha, beta, None, "end",
self.num_calculated, self.num_pruned))
node.score_index = len(self.nodelist_by_score) - 1
return node.score
from ai import dprint
for node in self.nodelist:
node.score_index = float("inf")
node.pruned_index = float("inf")
self.nodelist_by_score = []
self.ablist_by_score = []
self.num_calculated = 0
self.num_pruned = 0
calc_ab_score(abroot)
total_nodenum = self.num_pruned + self.num_calculated
ratio = self.num_calculated / total_nodenum * 100
dprint(debug, "計算したノードの数", self.num_calculated)
dprint(debug, "枝狩りしたノードの数", self.num_pruned)
dprint(debug, "合計", total_nodenum)
dprint(debug, f"割合 {ratio:.1f}%")
Mbtree.calc_score_by_ab = calc_score_by_ab
修正箇所
from marubatsu import Marubatsu
def calc_score_by_ab(self, abroot, debug=False):
元と同じなので省略
self.nodelist_by_score.append(node)
- self.ablist_by_score.append((alpha, beta, score, "score",
- self.num_calculated, self.num_pruned))
if score >= beta:
index = node.children.index(childnode)
for prunednode in node.children[index + 1:]:
assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
- break
+ self.ablist_by_score.append((alpha, beta, None, "update",
+ self.num_calculated, self.num_pruned))
+ if score >= beta:
+ break
元と同じなので省略
self.nodelist_by_score.append(node)
- self.ablist_by_score.append((alpha, beta, None, "update",
- self.num_calculated, self.num_pruned))
if score <= alpha:
index = node.children.index(childnode)
for prunednode in node.children[index + 1:]:
assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
- break
+ self.ablist_by_score.append((alpha, beta, None, "update",
+ self.num_calculated, self.num_pruned))
+ if score <= alpha:
+ break
元と同じなので省略
Mbtree.calc_score_by_ab = calc_score_by_ab
上記の修正後に下記のプログラムを実行して正しい処理が行われるようになったかどうかを確認します。
mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)
下図は β 狩りの処理が行われた 4486 フレーム目の図で、先程と異なり 枝狩りの数が正しく 14694 に増えている ことが確認できます。また、このフレームでは ノードの評価値が確定しない ので 計算済の数は 1121 のまま増えていない ことも確認できます。
下図はこのノードの評価値が確定したその次 4487 フレーム目の図で、計算済の数が正しく 1121 から 1122 に 1 だけ増えている ことが確認できます。
フレーム間の差分データの表示
枝狩りの処理が行われた際に、枝狩りによって処理が省略されたノードの数を表示する方法として、本記事では 直前のフレーム に表示されていた「枝狩り」の数と 現在のフレーム で表示されている数の 差分を表示 するという方法を取ることにします。また、その際にせっかくなので「計算済」、「合計」などの差分も表示することにします。もっと良い表示方法を思いついた方は実装してみて下さい。
直前のフレームとの差分を計算するためには、直前のフレームの番号を記録しておく必要がある ので、その値を prev_frame
という属性に記録する ことにします。
Mbtree_Anim クラスの __init__
メソッドの修正
prev_frame
属性を導入する場合は、その 初期化処理を行う必要 があります。Mbtree_Anim で フレームが最初に表示 された際には 直前のフレームは存在しない ので、prev_frame
を None
で初期化するという方法が考えられますが、None
で初期化を行う と差分の計算処理を prev_frame
が None
の場合とそうでない場合で分けて行う必要がある点が面倒 です。
このような場合は、prev_frame
属性の初期値 として、最初に表示されるフレームである 0
を設定 するのが一般的だと思います。そのように初期化することで、最初に表示を行った際の差分がすべて 0 で計算される ことになります。
prev_frame
属性の初期化処理 は __init__
メソッドで行えば良い ので、下記のプログラムの 2 行目のように __init__
メソッドを修正します。
1 def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
元と同じなので省略
2 self.prev_frame = 0
3 super(Mbtree_Anim, self).__init__()
4
5 Mbtree_Anim.__init__ = __init__
行番号のないプログラム
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
self.mbtree = mbtree
self.isscore = isscore
self.size = size
self.width = 50
self.height = 65
self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist
self.nodenum = len(self.nodelist)
self.prev_frame = 0
super(Mbtree_Anim, self).__init__()
Mbtree_Anim.__init__ = __init__
修正箇所
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
元と同じなので省略
+ self.prev_frame = 0
super(Mbtree_Anim, self).__init__()
Mbtree_Anim.__init__ = __init__
Mbtree_Anim クラスの update_ab
メソッドの修正
次に、update_ab
メソッドを下記のプログラムのように、直前のフレームとの差分を表示 するように修正します。なお、直前のフレームを表す prev_frame
属性には __init__
メソッドで 0
が代入 されており、その値を変更する処理をまだ記述していない ので、現時点では 常に最初の 0
フレーム目との差分が表示 されます。prev_frame
属性の更新の処理はこの後で記述します。
-
6 行目:直前のフレームのデータから、A と P の値を取り出して変数に代入する。変数の名前は現在のフレームの A と P を代入する変数の前に
prev_
をつけた名前とした。また差分を計算する際に必要のないデータは_
という変数に代入した - 7 行目:直前のフレームの M を計算する
- 8 ~ 11 行目:現在のフレームと直前のフレームの A、P、M の差分を計算して変数に代入する。また、R としては M の差分に対する A の差分の比率 を計算することにした
-
15 行目:上から表示する順で、差分を要素とする list を
diff_datalist
に代入する - 19 行目:18 行目と同様の方法で差分のデータを右揃えで描画する
1 import matplotlib.patches as patches
2
3 def update_ab(self):
元と同じなので省略
4 num_total = num_calculated + num_pruned
5 num_ratio = num_calculated / num_total if num_total != 0 else 0
6 _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
7 prev_num_total = prev_num_calculated + prev_num_pruned
8 diff_num_calculated = num_calculated - prev_num_calculated
9 diff_num_pruned = num_pruned - prev_num_pruned
10 diff_num_total = num_total - prev_num_total
11 diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
12
13 textlist = [ "計算済", "枝狩り", "合計", "割合" ]
14 datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
15 diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
16 for i in range(4):
17 self.abax.text(15, 1 - i * 0.7, textlist[i])
18 self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
19 self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
20
21 Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
import matplotlib.patches as patches
def update_ab(self):
alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
acolor = "red" if maxnode else "black"
bcolor = "black" if maxnode else "red"
self.abax.clear()
self.abax.set_xlim(-4, 23)
self.abax.set_ylim(-1.5, 1.5)
self.abax.axis("off")
minus_inf = -3
plus_inf = 4
alphacoord = max(minus_inf, alpha)
betacoord = min(plus_inf, beta)
color = "lightgray" if maxnode else "aqua"
rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
height=1, fc=color)
self.abax.add_patch(rect)
rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
height=1, fc="yellow")
self.abax.add_patch(rect)
color = "aqua" if maxnode else "lightgray"
rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
height=1, fc=color)
self.abax.add_patch(rect)
self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
for num in range(minus_inf, plus_inf + 1):
if num == minus_inf:
numtext = "-∞"
elif num == plus_inf:
numtext = "∞"
else:
numtext = num
self.abax.text(num, -1, numtext, ha="center")
arrowprops = { "arrowstyle": "->"}
self.abax.plot(alphacoord, 0, "or")
self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
arrowprops=arrowprops, ha="center", c=acolor)
self.abax.plot(betacoord, 0, "ob")
self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
arrowprops=arrowprops, ha="center", c=bcolor)
if score is not None:
self.abax.plot(score, 0, "og")
self.abax.annotate(f"score = {score}", xy=(score, 0),
xytext=((minus_inf + plus_inf) / 2, 1),
arrowprops=arrowprops, ha="center")
facecolorlist = ["aqua", "yellow", "lightgray"]
textcolorlist = ["black", "black", "black"]
if maxnode:
nodetype = f"深さ {self.selectednode.mb.move_count} max node"
textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
if score is not None :
if beta <= score:
textcolorlist[0] = "red"
elif alpha < score:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
else:
nodetype = f"深さ {self.selectednode.mb.move_count} min node"
textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
if score is not None :
if score <= alpha:
textcolorlist[0] = "red"
elif score < beta:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
if status == "start":
facecolor = "white"
nodetype += " 処理の開始"
elif status == "score":
facecolor = "lightyellow"
nodetype += " 子ノードの評価値"
elif status == "update":
facecolor = "lightcyan"
if maxnode:
nodetype += " α 値の処理"
else:
nodetype += " β 値の処理"
else:
facecolor = "lavenderblush"
nodetype += " 評価値の確定"
self.abfig.set_facecolor(facecolor)
self.abax.text(6, 1, nodetype)
for i in range(3):
rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
self.abax.add_patch(rect)
self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])
num_total = num_calculated + num_pruned
num_ratio = num_calculated / num_total if num_total != 0 else 0
_, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
prev_num_total = prev_num_calculated + prev_num_pruned
diff_num_calculated = num_calculated - prev_num_calculated
diff_num_pruned = num_pruned - prev_num_pruned
diff_num_total = num_total - prev_num_total
diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
textlist = [ "計算済", "枝狩り", "合計", "割合" ]
datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
Mbtree_Anim.update_ab = update_ab
修正箇所
import matplotlib.patches as patches
def update_ab(self):
元と同じなので省略
num_total = num_calculated + num_pruned
num_ratio = num_calculated / num_total if num_total != 0 else 0
+ _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
+ prev_num_total = prev_num_calculated + prev_num_pruned
+ diff_num_calculated = num_calculated - prev_num_calculated
+ diff_num_pruned = num_pruned - prev_num_pruned
+ diff_num_total = num_total - prev_num_total
+ diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
textlist = [ "計算済", "枝狩り", "合計", "割合" ]
datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
+ diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
+ self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
Mbtree_Anim.update_ab = update_ab
上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックして 9351 フレーム目を表示すると、実行結果のように一番右の列に 0 フレーム目との差分の A、P、M と差分の割合のデータが表示されるようになります。なお、最初の 0 フレーム目の A、P、M はいずれも 0 なので、差分として表示されるデータ はいずれも 現在のフレームのデータと同じ になります。他のフレームの表示も確認してみて下さい。
Mbtree_Anim(mbtree, isscore=True)
実行結果
差分のデータであることの明確化
上図では差分として「2338」のような正の数値が表示されていますが、差分が正の値であった場合でも「+2338」 のように 符号を表示 することで、2338 だけ 増加した差分のデータであることが明確になります。従って、差分のデータを表示する場合 は 常に符号を表示したほうがわかりやすい でしょう。
数値に常に符号をつけて表示するには、以前の記事で説明した f 文字列 の 書式指定 に + を記述します。具体的には、下記のプログラムのように update_ab
メソッドを修正します。
-
2 行目:
diff_datalist
の A、P、M の要素を f 文字列の書式指定 に+d
を記述することで、符号がついた整数の文字列に変換 する。書式指定の + が符合をつける、d が整数の文字列に変換することを表す
1 def update_ab(self):
元と同じなので省略
2 diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}",
3 f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
4 for i in range(4):
5 self.abax.text(15, 1 - i * 0.7, textlist[i])
6 self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
7 self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
8
9 Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
acolor = "red" if maxnode else "black"
bcolor = "black" if maxnode else "red"
self.abax.clear()
self.abax.set_xlim(-4, 23)
self.abax.set_ylim(-1.5, 1.5)
self.abax.axis("off")
minus_inf = -3
plus_inf = 4
alphacoord = max(minus_inf, alpha)
betacoord = min(plus_inf, beta)
color = "lightgray" if maxnode else "aqua"
rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
height=1, fc=color)
self.abax.add_patch(rect)
rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
height=1, fc="yellow")
self.abax.add_patch(rect)
color = "aqua" if maxnode else "lightgray"
rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
height=1, fc=color)
self.abax.add_patch(rect)
self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
for num in range(minus_inf, plus_inf + 1):
if num == minus_inf:
numtext = "-∞"
elif num == plus_inf:
numtext = "∞"
else:
numtext = num
self.abax.text(num, -1, numtext, ha="center")
arrowprops = { "arrowstyle": "->"}
self.abax.plot(alphacoord, 0, "or")
self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
arrowprops=arrowprops, ha="center", c=acolor)
self.abax.plot(betacoord, 0, "ob")
self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
arrowprops=arrowprops, ha="center", c=bcolor)
if score is not None:
self.abax.plot(score, 0, "og")
self.abax.annotate(f"score = {score}", xy=(score, 0),
xytext=((minus_inf + plus_inf) / 2, 1),
arrowprops=arrowprops, ha="center")
facecolorlist = ["aqua", "yellow", "lightgray"]
textcolorlist = ["black", "black", "black"]
if maxnode:
nodetype = f"深さ {self.selectednode.mb.move_count} max node"
textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
if score is not None :
if beta <= score:
textcolorlist[0] = "red"
elif alpha < score:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
else:
nodetype = f"深さ {self.selectednode.mb.move_count} min node"
textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
if score is not None :
if score <= alpha:
textcolorlist[0] = "red"
elif score < beta:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
if status == "start":
facecolor = "white"
nodetype += " 処理の開始"
elif status == "score":
facecolor = "lightyellow"
nodetype += " 子ノードの評価値"
elif status == "update":
facecolor = "lightcyan"
if maxnode:
nodetype += " α 値の処理"
else:
nodetype += " β 値の処理"
else:
facecolor = "lavenderblush"
nodetype += " 評価値の確定"
self.abfig.set_facecolor(facecolor)
self.abax.text(6, 1, nodetype)
for i in range(3):
rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
self.abax.add_patch(rect)
self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])
num_total = num_calculated + num_pruned
num_ratio = num_calculated / num_total if num_total != 0 else 0
_, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
prev_num_total = prev_num_calculated + prev_num_pruned
diff_num_calculated = num_calculated - prev_num_calculated
diff_num_pruned = num_pruned - prev_num_pruned
diff_num_total = num_total - prev_num_total
diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
textlist = [ "計算済", "枝狩り", "合計", "割合" ]
datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}",
f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
- diff_datalist = [ diff_num_calculated, diff_num_pruned,
- diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
+ diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}",
+ f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
Mbtree_Anim.update_ab = update_ab
上記の修正後に下記のプログラムを実行すると、実行結果のように 差分の数値に符号が表示 されるようになったこと確認できます。
Mbtree_Anim(mbtree, isscore=True)
実行結果
直前のフレームの情報の更新方法
次に、直前のフレームを表す prev_frame
属性を 更新する処理を記述 する必要がありますが、どのように記述すれば良いかについて少し考えてみて下さい。
間違った更新方法
ぱっと思いつく方法として、update_ab
メソッド内の 描画の更新を行う処理を行った直後 に prev_frame
属性を現在のアニメーションのフレームを表す self.play.value
で更新する という方法があるでしょう。
この方法の考え方は以下の通りです。
-
フレームが変更 されると
update_ab
が呼び出されて描画の更新を行われる -
update_ab
内で 描画の更新を行う処理を行った後 でprev_frame
を現在のフレームに更新 すると、次にアニメーションのフレームが変更 されてupdate_ab
が呼び出された際に、prev_frame
には直前のフレームの値が代入 されていることになる
一見するとこの方法で問題がないように見えるかもしれませんが、この方法には問題があります。筆者も最初はこの方法を思いついて実際に実装を行ってしまいました ので、この方法の問題について説明することにします。
下記は、そのように update_ab
を修正したプログラムです。
6 行目:描画を更新した後で prev_frame
に現在のアニメーションのフレームを表す self.play.value
を代入して更新する
1 def update_ab(self):
元と同じなので省略
2 for i in range(4):
3 self.abax.text(15, 1 - i * 0.7, textlist[i])
4 self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
5 self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
6 self.prev_frame = self.play.value
7
8 Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
acolor = "red" if maxnode else "black"
bcolor = "black" if maxnode else "red"
self.abax.clear()
self.abax.set_xlim(-4, 23)
self.abax.set_ylim(-1.5, 1.5)
self.abax.axis("off")
minus_inf = -3
plus_inf = 4
alphacoord = max(minus_inf, alpha)
betacoord = min(plus_inf, beta)
color = "lightgray" if maxnode else "aqua"
rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
height=1, fc=color)
self.abax.add_patch(rect)
rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
height=1, fc="yellow")
self.abax.add_patch(rect)
color = "aqua" if maxnode else "lightgray"
rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
height=1, fc=color)
self.abax.add_patch(rect)
self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
for num in range(minus_inf, plus_inf + 1):
if num == minus_inf:
numtext = "-∞"
elif num == plus_inf:
numtext = "∞"
else:
numtext = num
self.abax.text(num, -1, numtext, ha="center")
arrowprops = { "arrowstyle": "->"}
self.abax.plot(alphacoord, 0, "or")
self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
arrowprops=arrowprops, ha="center", c=acolor)
self.abax.plot(betacoord, 0, "ob")
self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
arrowprops=arrowprops, ha="center", c=bcolor)
if score is not None:
self.abax.plot(score, 0, "og")
self.abax.annotate(f"score = {score}", xy=(score, 0),
xytext=((minus_inf + plus_inf) / 2, 1),
arrowprops=arrowprops, ha="center")
facecolorlist = ["aqua", "yellow", "lightgray"]
textcolorlist = ["black", "black", "black"]
if maxnode:
nodetype = f"深さ {self.selectednode.mb.move_count} max node"
textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
if score is not None :
if beta <= score:
textcolorlist[0] = "red"
elif alpha < score:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
else:
nodetype = f"深さ {self.selectednode.mb.move_count} min node"
textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
if score is not None :
if score <= alpha:
textcolorlist[0] = "red"
elif score < beta:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
if status == "start":
facecolor = "white"
nodetype += " 処理の開始"
elif status == "score":
facecolor = "lightyellow"
nodetype += " 子ノードの評価値"
elif status == "update":
facecolor = "lightcyan"
if maxnode:
nodetype += " α 値の処理"
else:
nodetype += " β 値の処理"
else:
facecolor = "lavenderblush"
nodetype += " 評価値の確定"
self.abfig.set_facecolor(facecolor)
self.abax.text(6, 1, nodetype)
for i in range(3):
rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
self.abax.add_patch(rect)
self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])
num_total = num_calculated + num_pruned
num_ratio = num_calculated / num_total if num_total != 0 else 0
_, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
prev_num_total = prev_num_calculated + prev_num_pruned
diff_num_calculated = num_calculated - prev_num_calculated
diff_num_pruned = num_pruned - prev_num_pruned
diff_num_total = num_total - prev_num_total
diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
textlist = [ "計算済", "枝狩り", "合計", "割合" ]
datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}",
f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
self.prev_frame = self.play.value
Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
+ self.prev_frame = self.play.value
Mbtree_Anim.update_ab = update_ab
上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックして 9351 フレーム目を表示すると、実行結果のように 差分のデータにすべて +0 が表示される という問題が発生することがわかります。また、他のボタンをクリックして 別のフレームを表示 しても 同様に差分のデータにすべて +0 が表示される ことを実際に確認してみて下さい。また、この問題の原因について少し考えてみて下さい。
Mbtree_Anim(mbtree, isscore=True)
実行結果
バグの原因
この問題の原因は直前のフレームを表す prev_frame
属性の値の更新 を、描画の更新を行う update_ab
メソッドの中で行っている 点にあります。そのため update_ab
メソッドが、アニメーションのフレームが 変更されていない場合に呼び出される と、prev_frame
が直前のフレームではなくなってしまいます。
わかりづらいと思いますので、具体例として 0 フレーム目から 100 フレーム目、200 フレームに表示が変更 された場合の処理を説明します。
下記の表は、アニメーションの フレームが変更された際 に、update_ab
が 1 回だけ 呼び出される場合の処理を表します。表の 太字の行 のように update_ab
で表示を更新する際に prev_frame
には直前のフレームの値が代入されている ので、差分が正しく描画されます。
現在のフレーム |
prev_frame の値 |
|
---|---|---|
100 フレームが変更される前 | 0 | 0 |
100 フレーム目に表示が移動 | 100 | 0 |
update_ab での表示の更新中 |
100 | 0 |
update_ab の処理の終了後 |
100 | 100 |
200 フレーム目に表示が移動 | 200 | 100 |
update_ab での表示の更新中 |
200 | 100 |
update_ab の処理の終了後 |
200 | 200 |
一方、下記の表はアニメーションの フレームが変更された際 に、update_ab
が 2 回 呼び出された場合の処理を表します。1 回目の update_ab
の処理によって prev_frame
の値が 現在のアニメーションのフレームに更新 されたしまうため、太字の行 のように 2 回目の update_ab
で表示を更新する際 には prev_frame
には現在のフレームが代入 されています。そのため、差分には必ず 0 が表示されます。
現在のフレーム |
prev_frame の値 |
|
---|---|---|
100 フレームが変更される前 | 0 | 0 |
100 フレーム目に表示が移動 | 100 | 0 |
1 回目の update_ab での表示の更新中 |
100 | 0 |
1 回目の update_ab の処理の終了後 |
100 | 100 |
2 回目の update_ab での表示の更新中 |
100 | 100 |
2 回目の update_ab の処理の終了後 |
100 | 100 |
200 フレーム目に表示が移動 | 200 | 100 |
1 回目の update_ab での表示の更新中 |
200 | 100 |
1 回目の update_ab の処理の終了後 |
200 | 200 |
2 回目の update_ab での表示の更新中 |
200 | 200 |
2 回目の update_ab の処理の終了後 |
200 | 200 |
update_ab
が呼び出された回数の確認
上記では、フレームが変更された際に update_ab
が 2 回以上呼び出されると差分に 0 が表示されることを示しましたが、フレームが変更された際に update_ab
が何回呼び出されているの確認は行っていません。
そこで、フレームが変更された際に 本当に update_ab
が 2 回以上呼び出されているかを確認 するために、下記のプログラムの 3 行目のように update_ab
の中で prev_frame
属性の値を更新 した際に update_ab というメッセージを表示 するように修正します。
1 def update_ab(self):
元と同じなので省略
2 self.prev_frame = self.play.value
3 print("update_ab")
4
5 Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
acolor = "red" if maxnode else "black"
bcolor = "black" if maxnode else "red"
self.abax.clear()
self.abax.set_xlim(-4, 23)
self.abax.set_ylim(-1.5, 1.5)
self.abax.axis("off")
minus_inf = -3
plus_inf = 4
alphacoord = max(minus_inf, alpha)
betacoord = min(plus_inf, beta)
color = "lightgray" if maxnode else "aqua"
rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
height=1, fc=color)
self.abax.add_patch(rect)
rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
height=1, fc="yellow")
self.abax.add_patch(rect)
color = "aqua" if maxnode else "lightgray"
rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
height=1, fc=color)
self.abax.add_patch(rect)
self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
for num in range(minus_inf, plus_inf + 1):
if num == minus_inf:
numtext = "-∞"
elif num == plus_inf:
numtext = "∞"
else:
numtext = num
self.abax.text(num, -1, numtext, ha="center")
arrowprops = { "arrowstyle": "->"}
self.abax.plot(alphacoord, 0, "or")
self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
arrowprops=arrowprops, ha="center", c=acolor)
self.abax.plot(betacoord, 0, "ob")
self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
arrowprops=arrowprops, ha="center", c=bcolor)
if score is not None:
self.abax.plot(score, 0, "og")
self.abax.annotate(f"score = {score}", xy=(score, 0),
xytext=((minus_inf + plus_inf) / 2, 1),
arrowprops=arrowprops, ha="center")
facecolorlist = ["aqua", "yellow", "lightgray"]
textcolorlist = ["black", "black", "black"]
if maxnode:
nodetype = f"深さ {self.selectednode.mb.move_count} max node"
textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
if score is not None :
if beta <= score:
textcolorlist[0] = "red"
elif alpha < score:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
else:
nodetype = f"深さ {self.selectednode.mb.move_count} min node"
textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
if score is not None :
if score <= alpha:
textcolorlist[0] = "red"
elif score < beta:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
if status == "start":
facecolor = "white"
nodetype += " 処理の開始"
elif status == "score":
facecolor = "lightyellow"
nodetype += " 子ノードの評価値"
elif status == "update":
facecolor = "lightcyan"
if maxnode:
nodetype += " α 値の処理"
else:
nodetype += " β 値の処理"
else:
facecolor = "lavenderblush"
nodetype += " 評価値の確定"
self.abfig.set_facecolor(facecolor)
self.abax.text(6, 1, nodetype)
for i in range(3):
rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
self.abax.add_patch(rect)
self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])
num_total = num_calculated + num_pruned
num_ratio = num_calculated / num_total if num_total != 0 else 0
_, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
prev_num_total = prev_num_calculated + prev_num_pruned
diff_num_calculated = num_calculated - prev_num_calculated
diff_num_pruned = num_pruned - prev_num_pruned
diff_num_total = num_total - prev_num_total
diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
textlist = [ "計算済", "枝狩り", "合計", "割合" ]
datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}",
f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
self.prev_frame = self.play.value
print("update_ab")
Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
self.prev_frame = self.play.value
+ print("update_ab")
Mbtree_Anim.update_ab = update_ab
上記の修正後に下記のプログラムを実行すると、画像の下に実行結果の 1 行目のように update_ab が 1 回だけ表示 されるので、最初の描画を行うために update_ab
が 1 回だけ呼び出された ことが確認できます。なお、実行結果の 3 行目は Mbtree_Anim の呼び出しによって 返り値として返された Mbtree_Anim クラスのインスタンス を表します。
Mbtree_Anim(mbtree, isscore=True)
実行結果
update_ab
<tree.Mbtree_Anim at 0x15b4feb9930>
その後で選択中のノード内の移動の右の > ボタンをクリックして フレームを変更 すると下記のように update_ab が 2 回表示される ので、確かに > ボタンをクリックして フレームを変更 したことで update_ab
メソッドが 2 回呼び出されている ことが確認できました。
update_ab
update_ab
他のフレームを移動するボタンをクリック した際に、同様に update_ab が 2 回表示される ことを確認して下さい。
上記から、差分に常に 0 が表示されるという 問題の原因 が、フレームが変更された際 に、update_ab
が 2 回 呼び出されたことであることが確認できました。
間違った考え方をした原因
先程、この方法の考え方を以下のように説明しました。この考え方はフレームが変更された際に update_ab
が 1 回だけしか呼び出されない場合は正しい ですが、update_ab
が 2 回以上 呼び出される場合は 正しくありません。
- アニメーションのフレームが変更されると
update_ab
を呼び出して描画の更新を行う -
update_ab
内で描画の更新を行う処理を行った後でprev_frame
を現在のフレームに更新すると、次にアニメーションのフレームが変更されてupdate_ab
が呼び出された際に、prev_frame
には直前のフレームの値が代入されていることになる
筆者が最初に上記の方法を考えたのは、フレームが変更された際に update_ab
を 1 回だけ 呼び出されるように プログラムを記述したつもり だったからです。また、そのように 勘違いした理由 は以下の通りです。
- Mbtree_Anim では
update_ab
はupdate_gui
メソッドの中 の処理で 1 度だけ 呼び出されるので、update_ab
が呼び出される回数 はupdate_gui
が呼び出される回数と等しい - 例えば、フレームを 1 つ先に移動する > ボタンをクリック した際の イベントハンドラ は下記のプログラムのように定義されている。このプログラムから > ボタンをクリックすると、
update_gui
メソッドが 1 回だけ呼び出される と考えた - 従って > ボタンをクリックすると
update_ab
メソッドが 1 回だけ呼び出される と考えた - 他のフレームを移動するボタンに関しても同様だと考えた
def on_next_button_clicked(b=None):
self.play.value += 1
self.update_gui()
上記の筆者の考え方は間違っていますが、どこに間違いがあるかがわかる人は、それほど多くないと思います。
このような勘違いは、他人が作ったモジュールを利用する際に良く起きます。今回の場合は ipywidgets モジュールを利用する際に 行われると筆者が思っていた処理 が、実際に行われている処理と異なっていた点 が問題の原因です。
update_ab
が複数回呼び出されてしまう原因の検証
ipywidgets のドキュメントをしっかりと読んで、ipywidgets モジュールが行う処理を理解すれば何を勘違いしているかを理解することは可能ですが、ドキュメントはかなり長く、すべてを正しく理解するのはかなり困難 です1。そこで、フレームが変更された際に update_ab
が 2 回呼び出されてしまう原因 を自力で検証することにします。
先ほど説明したように、Mbtree_Anim の処理の中で update_ab
メソッドを呼び出す処理が記述 されているのは update_gui
メソッドの中だけ です。従って update_gui
メソッドが呼び出された場所を確認 することで update_ab
メソッドが 2 回呼び出された原因を確認 することができます。
update_gui
メソッドを呼び出す処理が記述されているのは create_event_handler
メソッドの中だけ なので、update_gui
メソッドを 呼び出す処理を行う直前 に、下記のプログラムのように print
で update_gui
を呼び出した ローカル関数の名前 を表示するように create_event_handler
メソッドを修正します。
-
3、8、13、17 行目:
update_gui
を呼び出す直前に処理を行っているローカル関数の名前を表示する処理を追加する
1 def create_event_handler(self):
2 def on_play_changed(changed):
3 print("on_play_changed")
4 self.update_gui()
5
6 def on_prev_button_clicked(b=None):
7 self.play.value -= 1
8 print("on_prev_button_clicked")
9 self.update_gui()
10
11 def on_next_button_clicked(b=None):
12 self.play.value += 1
13 print("on_next_button_clicked")
14 self.update_gui()
元と同じなので省略
15 def change_frame(edge_status, diff, status_list):
元と同じなので省略
16 self.play.value = frame
17 print("change_frame")
18 self.update_gui()
元と同じなので省略
19
20 Mbtree_Anim.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
def on_play_changed(changed):
print("on_play_changed")
self.update_gui()
def on_prev_button_clicked(b=None):
self.play.value -= 1
print("on_prev_button_clicked")
self.update_gui()
def on_next_button_clicked(b=None):
self.play.value += 1
print("on_next_button_clicked")
self.update_gui()
self.prev_button.on_click(on_prev_button_clicked)
self.next_button.on_click(on_next_button_clicked)
self.play.observe(on_play_changed, names="value")
def change_frame(edge_status, diff, status_list):
frame = self.play.value
selectednode = self.mbtree.nodelist_by_score[frame]
selectedstatus = self.mbtree.ablist_by_score[frame][3]
if selectedstatus == edge_status:
return
while True:
frame += diff
node = self.mbtree.nodelist_by_score[frame]
status = self.mbtree.ablist_by_score[frame][3]
if node == selectednode and status in status_list:
break
self.play.value = frame
print("change_frame")
self.update_gui()
def on_node_first_button_clicked(b=None):
change_frame("start", -1, ["start"])
def on_node_prev_button_clicked(b=None):
change_frame("start", -1, ["start", "score"])
def on_node_next_button_clicked(b=None):
change_frame("end", 1, ["end", "score"])
def on_node_last_button_clicked(b=None):
change_frame("end", 1, ["end"])
if self.abfig is not None:
self.node_first_button.on_click(on_node_first_button_clicked)
self.node_prev_button.on_click(on_node_prev_button_clicked)
self.node_next_button.on_click(on_node_next_button_clicked)
self.node_last_button.on_click(on_node_last_button_clicked)
Mbtree_Anim.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
def on_play_changed(changed):
+ print("on_play_changed")
self.update_gui()
def on_prev_button_clicked(b=None):
self.play.value -= 1
+ print("on_prev_button_clicked")
self.update_gui()
def on_next_button_clicked(b=None):
self.play.value += 1
+ print("on_next_button_clicked")
self.update_gui()
元と同じなので省略
def change_frame(edge_status, diff, status_list):
元と同じなので省略
self.play.value = frame
+ print("change_frame")
self.update_gui()
元と同じなので省略
Mbtree_Anim.create_event_handler = create_event_handler
上記の修正後に下記のプログラムを実行した後で選択中のノード内の移動の右の > ボタンをクリックしてフレームを変更すると、実行結果のような表示が行われます。
Mbtree_Anim(mbtree, isscore=True)
実行結果
on_play_changed
update_ab
change_frame
update_ab
実行結果から、ローカル関数 on_play_changed
と change_frame
の処理によって update_ab
が 2 回呼び出されている ことが確認できました。
下記は 他のフレームを移動するボタンを クリックした際に表示されるメッセージから、呼び出されるローカル関数をまとめた表 です。
1 回目に呼び出される関数 | 2 回目に呼び出される関数 | |
---|---|---|
上部の < | on_play_changed |
on_prev_button_clicked |
上部の > | on_play_changed |
on_next_button_clicked |
下部の << | on_play_changed |
change_frame |
下部の < | on_play_changed |
change_frame |
下部の > | on_play_changed |
change_frame |
下部の >> | on_play_changed |
change_frame |
1 回目の update_ab
は すべて on_play_changed
から呼び出される ことがわかります。下部の 4 つのボタン は下記のプログラムのように、いずれもそれらのボタンをクリックした際の イベントハンドラ内 で change_frame
を呼び出す ことでフレームを移動する処理を行っているので、2 回目の update_ab
は 上部のボタンも含めて いずれも ボタンをクリックした際の イベントハンドラから呼び出されている ことが確認できます。
def on_node_first_button_clicked(b=None):
change_frame("start", -1, ["start"])
def on_node_prev_button_clicked(b=None):
change_frame("start", -1, ["start", "score"])
def on_node_next_button_clicked(b=None):
change_frame("end", 1, ["end", "score"])
def on_node_last_button_clicked(b=None):
change_frame("end", 1, ["end"])
フレームを 1 つ前に移動する < ボタンをクリック した場合の処理を詳しく検証することにします。上記の表から、on_play_changed
と on_prev_button_clicked
の順番で update_gui
が呼び出された ことが確認できます。
下記はその 2 つの関数の定義で、on_play_changed
はフレームを表す self.play.value
の値が変更された場合 に呼び出される関数です。on_prev_button_clicked
フレームを 1 つ前に移動する < ボタンをクリックした場合 に呼び出される関数です。
1 def on_play_changed(changed):
2 print("on_play_changed")
3 self.update_gui()
4
5 def on_prev_button_clicked(b=None):
6 self.play.value -= 1
7 print("on_prev_button_clicked")
8 self.update_gui()
このことから、< ボタンをクリックすると以下の理由で update_gui
が 2 回呼び出されることが確認できました。
-
on_prev_button_clicked
の 8 行目 からupdate_gui
が呼び出される -
on_prev_button_clicked
の 6 行目でself.play.value
の値が変更された結果、on_play_changed
が呼び出され、その 3 行目からupdate_gui
が呼び出される
上記から、勘違いの原因が self.play.value -= 1
という 一見すると値を代入する処理にしか見えない処理 によって on_play_changed
が呼び出される ことに気が付かなかった点であることがわかります。
また、on_prev_button_clicked
または on_play_changed
の どちらかのみから update_gui
を呼び出す ようにプログラムを修正することでバグを修正することができますが、そのような修正方法は行わないほうが良い でしょう。その理由について少し考えてみて下さい。
イベントハンドラのように、何らかの処理に応じて後から別の関数を呼び出すような処理 は、このような 思わぬ処理が行われる可能性がある 点に注意が必要です。
< ボタンをクリックした際に、on_prev_button_clicked
が最初に呼び出されるので、on_play_changed、on_prev_button_clicked の順で表示が行われる点がおかしいと思う人がいるかもしれないので補足します。
on_play_changed
のように、ウィジェットの値が変更された際のイベントハンドラは、ウィジェットの observe
メソッドによって結び付けます。observe
メソッドによってウィジェットに結びつけられたイベントハンドラは、ウィジェットの値が変更された際にすぐに呼びだされるという性質があります。例えば、self.play.value -= 1
のようにウィジェットの値を代入処理によって変更した場合は、その次の行の処理が行われる前に on_play_changed
が呼び出されます。
従って、< ボタンをクリックした際には下記の手順で処理が行われます。
- 6 行目が実行されて
self.play.value
の値が更新される - 次の 7 行目の処理が行われる前に
on_play_changed
が呼び出され、その中の 2 行目で on_play_changed が表示される - 7 行目が呼び出されて on_prev_button_clicked が表示される
update_ab
内で prev_frame
を更新する方法の問題点
勘違いしている人が多いかもしれませんが、この バグの原因は フレームが変更された際に 複数回 update_ab
が呼ばれることではありません。このバグの本当の原因は、prev_frame
を更新する処理 を、フレームを変更する処理ではない場所で行っている 点にあります。
update_ab
が行う処理は 描画の更新処理 であり、その処理は現状の Mbtree_Anim が行っているように、フレームが変更されない場合でも行われる可能性 があります。
従って、フレームを変更するボタンをクリックした際に update_ab
が 1 回だけ呼び出されるように修正 した場合は、今後 Mbtree_Anim を修正した結果、別の場所から update_ab
を呼び出す処理を記述 してしまうと、このバグが再び発生してしまう ことになります。
従って、このバグを修正する正しい方法 は、prev_frame
を更新する処理 を、フレームが更新された際に行う ようにすることです。どのように修正すれば良いかについて少し考えてみて下さい。
IntSlider の操作によって行われる処理
正しい修正方法を説明する前に、フレームの移動を上部の IntSlider で行うこともできることに気が付きましたので、IntSlider でフレームを移動した場合のメッセージを確認することにします。この部分はおまけのようなものなので読み飛ばしても問題はありません。
IntSlider をドラッグしてフレームを移動した場合は、下記のように on_play_changed
が 2 回呼ばれるようです。
on_play_changed
update_ab
on_play_changed
update_ab
2 回呼ばれる原因は IntSlider のドラッグ操作がリアルタイムに self.play.value
の値に反映され、そのたびに on_play_changed
が呼び出されることです。そのため、ドラッグ操作をゆっくり行った場合は、3 回以上 on_play_changed
が呼ばれることになります。
また、様々な速度でドラッグ操作を行ってみた結果、どれだけ素早くドラッグ操作を行っても最低でも 2 回は on_play_changed
が呼ばれることが判明しました。そのため、0 フレーム目からドラッグ操作を行った場合は、以下のような理由で下図のように差分の左の数値と差分の数値が一致しなくなるようです。
- ドラッグ操作を終えるまでの間に何度か
self.play.value
の値が更新される -
self.play.value
の値が更新されるたびにupdate_ab
が呼び出されて描画が更新される - ドラッグ操作が完了した時点では、ドラッグ操作の中で最後に描画が行われたアニメーションのフレームとの差分 のデータが表示される
なお、IntSlider では、下記の方法でも値を変更することができ、その場合は on_play_changed
は 1 回しか呼び出されないようです。
- IntSlider のボタンが表示されていない場所でクリックする
- IntSlider の右の数値をクリックし、キーボードから値を変更する
IntSlider の値を変更した際に行われる処理に時間がかかる場合は、ドラッグ操作の途中で self.play.value
の値の変更が行われるたびにその処理が行われてしまうとプログラムの動作が非常に重くなってしまうという問題が発生します。そのような問題に対処するために、IntSlider などのウィジェットは、作成する際にキーワード属性 continuous_update=False
2を記述することで、ドラッグなどの操作が完了するまでの間に値を更新しないようにすることができます。
例えば、Mbtee_Anim の create_widgets
の中で、アニメーションのフレームを変更する IntSlider のウィジェットを作成する処理を、下記のプログラムのように修正することで、IntSlider のドラッグ操作が完了するまで on_play_changed
が呼び出されなくなります。
self.frame_slider = widgets.IntSlider(max=self.nodenum - 1, description="frame",
continuous_update=False)
なお、そのようにしてしまうとドラッグをゆっくり行うことで、ドラッグ中のフレームの画像をリアルタイムに見ることができなくなってしまうという欠点があるので、本記事では上記のように修正は行いません。興味がある方は実際に上記のように修正し、IntSlider のドラッグ中に表示が変わらなくなることを確認してみて下さい。
prev_frame
を更新する場所
先程説明したように、prev_frame
属性は アニメーションのフレームが変更された際に更新する必要 があります。ただし、そのことがわかっていても以下の理由から先程の間違った方法を最初に思いつく人が多いのではないかと思います。
- プログラムの中でアニメーションのフレームを表す
self.play.value
を変更する場所が複数ある - その全ての場所で
prev_frame
属性を更新する処理を記述するのは面倒 - アニメーションのフレームが変更された場合は、必ず
update_ab
で表示を更新するので、そちらにprev_frame
属性を更新する処理を記述すれば良いのではなかと勘違いする
先程の表からわかるように、アニメーションのフレームを表す self.play.value
の値が変更された場合 は必ず on_play_chaned
が 1 回だけ呼び出される ので、その中で prev_frame
属性を更新する処理を記述 することができます。
また、以前の記事で説明したように on_play_changed
の仮引数 に代入されたデータの "old"
属性に変更前の値が記録されている ので、それを利用して下記のプログラムのように修正を行うことができます。
-
2 行目:IntSlider の変更前の値は
changed.old
に代入されているので、それをprev_frame
属性に代入するようにする - 4、7、11、14 行目の前にあったデバッグ用の
print
の表示はもう必要がないのですべて削除した
1 def create_event_handler(self):
2 def on_play_changed(changed):
3 self.prev_frame = changed.old
4 self.update_gui()
元と同じなので省略
5 def on_prev_button_clicked(b=None):
6 self.play.value -= 1
7 self.update_gui()
8
9 def on_next_button_clicked(b=None):
10 self.play.value += 1
11 self.update_gui()
元と同じなので省略
12 def change_frame(edge_status, diff, status_list):
元と同じなので省略
13 self.play.value = frame
14 self.update_gui()
元と同じなので省略
15
16 Mbtree_Anim.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
def on_play_changed(changed):
self.prev_frame = changed.old
self.update_gui()
def on_prev_button_clicked(b=None):
self.play.value -= 1
self.update_gui()
def on_next_button_clicked(b=None):
self.play.value += 1
self.update_gui()
self.prev_button.on_click(on_prev_button_clicked)
self.next_button.on_click(on_next_button_clicked)
self.play.observe(on_play_changed, names="value")
def change_frame(edge_status, diff, status_list):
frame = self.play.value
selectednode = self.mbtree.nodelist_by_score[frame]
selectedstatus = self.mbtree.ablist_by_score[frame][3]
if selectedstatus == edge_status:
return
while True:
frame += diff
node = self.mbtree.nodelist_by_score[frame]
status = self.mbtree.ablist_by_score[frame][3]
if node == selectednode and status in status_list:
break
self.play.value = frame
self.update_gui()
def on_node_first_button_clicked(b=None):
change_frame("start", -1, ["start"])
def on_node_prev_button_clicked(b=None):
change_frame("start", -1, ["start", "score"])
def on_node_next_button_clicked(b=None):
change_frame("end", 1, ["end", "score"])
def on_node_last_button_clicked(b=None):
change_frame("end", 1, ["end"])
if self.abfig is not None:
self.node_first_button.on_click(on_node_first_button_clicked)
self.node_prev_button.on_click(on_node_prev_button_clicked)
self.node_next_button.on_click(on_node_next_button_clicked)
self.node_last_button.on_click(on_node_last_button_clicked)
Mbtree_Anim.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
def on_play_changed(changed):
+ self.prev_frame = changed.old
- print("on_play_changed")
self.update_gui()
元と同じなので省略
def on_prev_button_clicked(b=None):
self.play.value -= 1
- print("on_prev_button_clicked")
self.update_gui()
def on_next_button_clicked(b=None):
self.play.value += 1
- print("on_next_button_clicked")
self.update_gui()
元と同じなので省略
def change_frame(edge_status, diff, status_list):
元と同じなので省略
self.play.value = frame
- print("change_frame")
self.update_gui()
元と同じなので省略
Mbtree_Anim.create_event_handler = create_event_handler
次に下記のプログラムのように update_ab
内で prev_frame
属性の値を更新する処理を削除 する必要があります。
- 5 行目の後にあった
prev_frame
属性を更新する処理と、print
によるデバッグ表示を削除する
1 def update_ab(self):
元と同じなので省略
2 for i in range(4):
3 self.abax.text(15, 1 - i * 0.7, textlist[i])
4 self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
5 self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
6
7 Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
acolor = "red" if maxnode else "black"
bcolor = "black" if maxnode else "red"
self.abax.clear()
self.abax.set_xlim(-4, 23)
self.abax.set_ylim(-1.5, 1.5)
self.abax.axis("off")
minus_inf = -3
plus_inf = 4
alphacoord = max(minus_inf, alpha)
betacoord = min(plus_inf, beta)
color = "lightgray" if maxnode else "aqua"
rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
height=1, fc=color)
self.abax.add_patch(rect)
rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
height=1, fc="yellow")
self.abax.add_patch(rect)
color = "aqua" if maxnode else "lightgray"
rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
height=1, fc=color)
self.abax.add_patch(rect)
self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
for num in range(minus_inf, plus_inf + 1):
if num == minus_inf:
numtext = "-∞"
elif num == plus_inf:
numtext = "∞"
else:
numtext = num
self.abax.text(num, -1, numtext, ha="center")
arrowprops = { "arrowstyle": "->"}
self.abax.plot(alphacoord, 0, "or")
self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
arrowprops=arrowprops, ha="center", c=acolor)
self.abax.plot(betacoord, 0, "ob")
self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
arrowprops=arrowprops, ha="center", c=bcolor)
if score is not None:
self.abax.plot(score, 0, "og")
self.abax.annotate(f"score = {score}", xy=(score, 0),
xytext=((minus_inf + plus_inf) / 2, 1),
arrowprops=arrowprops, ha="center")
facecolorlist = ["aqua", "yellow", "lightgray"]
textcolorlist = ["black", "black", "black"]
if maxnode:
nodetype = f"深さ {self.selectednode.mb.move_count} max node"
textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
if score is not None :
if beta <= score:
textcolorlist[0] = "red"
elif alpha < score:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
else:
nodetype = f"深さ {self.selectednode.mb.move_count} min node"
textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
if score is not None :
if score <= alpha:
textcolorlist[0] = "red"
elif score < beta:
textcolorlist[1] = "red"
else:
textcolorlist[2] = "red"
if status == "start":
facecolor = "white"
nodetype += " 処理の開始"
elif status == "score":
facecolor = "lightyellow"
nodetype += " 子ノードの評価値"
elif status == "update":
facecolor = "lightcyan"
if maxnode:
nodetype += " α 値の処理"
else:
nodetype += " β 値の処理"
else:
facecolor = "lavenderblush"
nodetype += " 評価値の確定"
self.abfig.set_facecolor(facecolor)
self.abax.text(6, 1, nodetype)
for i in range(3):
rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
self.abax.add_patch(rect)
self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])
num_total = num_calculated + num_pruned
num_ratio = num_calculated / num_total if num_total != 0 else 0
_, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
prev_num_total = prev_num_calculated + prev_num_pruned
diff_num_calculated = num_calculated - prev_num_calculated
diff_num_pruned = num_pruned - prev_num_pruned
diff_num_total = num_total - prev_num_total
diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
textlist = [ "計算済", "枝狩り", "合計", "割合" ]
datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}",
f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
for i in range(4):
self.abax.text(15, 1 - i * 0.7, textlist[i])
self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
- self.prev_frame = self.play.value
- print("update_ab")
Mbtree_Anim.update_ab = update_ab
上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックすると実行結果のような図が表示されます。0 フレーム目からの差分として、その左の数値と同じ値が正しく表示されることが確認できます。
Mbtree_Anim(mbtree, isscore=True)
実行結果
また、今回の記事の最初で行ったように、4485 フレーム目から 4486 フレーム目に移動すると、下図のように右にフレーム間で行われた枝狩り数の 差分である 14694 - 9179 = 5515 が右の列に正しく表示 されるようになったことが確認できます。
今回の記事のまとめ
今回の記事では、Mbtre_Anim のバグの修正と、フレームを移動した際の A、P、M の 差分のデータを表示するという改良 を行いました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
tree.py | 本記事で更新した tree_new.py |
次回の記事