From 6fc6332f9d5ab6f95bcf2d39fe6cf3423f4aabc2 Mon Sep 17 00:00:00 2001 From: Sander Land Date: Sat, 11 Apr 2020 21:05:04 +0200 Subject: [PATCH] extra analysis --- board.py | 33 ++++++++++++++++++++++++------ config.json | 4 +++- controller.py | 53 ++++++++++++++++++++++++++++++++++-------------- katrain.kv | 4 ++++ katrain.py | 34 ++++++++++++++++++++++++++++--- outdated_log.txt | 13 ++++++++++++ 6 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 outdated_log.txt diff --git a/board.py b/board.py index 84607c1..58e93d1 100644 --- a/board.py +++ b/board.py @@ -1,6 +1,7 @@ import os import random from datetime import datetime +import copy class IllegalMoveException(Exception): @@ -13,7 +14,7 @@ class Move: SGF_COORD = [chr(i) for i in range(97, 123)] _move_id_counter = -1 - def __init__(self, player, coords=None, gtpcoords=None, sgfcoords=None, robot=False): + def __init__(self, player=0, coords=None, gtpcoords=None, sgfcoords=None, robot=False): Move._move_id_counter += 1 self.id = Move._move_id_counter self.player = player @@ -108,9 +109,12 @@ class Move: elif not self.is_pass and self.parent.analysis[0]["move"] != self.gtp(): if sgf: # shown in stats anyway text += f"Evaluation: {self.evaluation:.1%} efficient\n" - outdated_evaluation = self.outdated_evaluation + outdated_evaluation, outdated_details = self.outdated_evaluation if outdated_evaluation and outdated_evaluation > self.evaluation and outdated_evaluation > self.evaluation + 0.05: text += f"(Was considered last move as {outdated_evaluation:.0%})\n" + if outdated_evaluation > self.evaluation + 0.15: + with open("outdated_log.txt", "a") as f: + f.write(f"logs.append({repr(outdated_details)})\n") points_lost = self.player_sign * (prev_best_score - score) if points_lost > 0.5: text += f"Estimated point loss: {points_lost:.1f}\n" @@ -158,24 +162,26 @@ class Move: prev_analysis_current_move = [d for d in self.parent.analysis if d["move"] == self.gtp()] if prev_analysis_current_move: - best_score, worst_score, prev_temp = self.parent.temperature_stats best_score = outdated_score(self.parent.analysis[0]) worst_score = self.parent.pass_analysis[0]["scoreLead"] prev_temp = max(self.player_sign * (best_score - worst_score), 0) score = outdated_score(prev_analysis_current_move[0]) - return self.player_sign * (score - worst_score) / prev_temp if prev_temp > 0 else None + return (self.player_sign * (score - worst_score) / prev_temp if prev_temp > 0 else None), prev_analysis_current_move + else: + return None, None @property def ai_moves(self): if not self.analysis_ready: return [] _, worst_score, temperature = self.temperature_stats - for d in self.analysis: + analysis = copy.copy(self.analysis) # not deep, so eval is saved, but avoids race conditions + for d in analysis: if temperature > 0.5: d["evaluation"] = -self.player_sign * (d["scoreLead"] - worst_score) / temperature else: d["evaluation"] = int(-self.player_sign * d["scoreLead"] >= -self.player_sign * self.analysis[0]["scoreLead"]) - return self.analysis + return analysis # various output and conversion functions @staticmethod @@ -335,6 +341,21 @@ class Board: return 1 - self.current_move.player def store_analysis(self, json): + if json["id"].startswith("AA:"): # board sweep analyze all + _, move_id, gtpcoords = json["id"].split(":") + move = self.all_moves.get(int(move_id)) + if not move.analysis: + return # should have been prevented, but better not to crash + cur_analysis = [d for d in move.analysis if d["move"] == gtpcoords] + move_analysis = {k: v for k, v in json["moveInfos"][0].items() if k not in {"move", "pv"}} + move_analysis["visits"] = sum(d["visits"] for d in json["moveInfos"]) # TODO: ?? + if cur_analysis: + if cur_analysis[0]["visits"] < move_analysis["visits"]: + cur_analysis[0].update(move_analysis) + else: + move.analysis.append({"move": gtpcoords, **move_analysis}) + return + if json["id"].startswith("PASS_"): move_id = int(json["id"].lstrip("PASS_")) is_pass = True diff --git a/config.json b/config.json index 4a295cc..f61a665 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,9 @@ "pass_visits": 100, "pass_visits_fast": 25, "visits": 2000, - "visits_fast": 500 + "visits_fast": 500, + "analyze_all_visits": 500, + "analyze_all_visits_fast": 100 }, "board": { "size": 19, diff --git a/controller.py b/controller.py index 505435f..a01f0c5 100644 --- a/controller.py +++ b/controller.py @@ -37,11 +37,15 @@ class EngineControls(GridLayout): self.command = shlex.split(self.command) analysis_settings = Config.get("analysis") - self.visits = [[analysis_settings["pass_visits"], analysis_settings["visits"]], [analysis_settings["pass_visits_fast"], analysis_settings["visits_fast"]]] + self.visits = [ + [analysis_settings["pass_visits"], analysis_settings["visits"], analysis_settings["analyze_all_visits"]], + [analysis_settings["pass_visits_fast"], analysis_settings["visits_fast"], analysis_settings["analyze_all_visits_fast"]], + ] self.train_settings = Config.get("trainer") self.debug = Config.get("debug")["level"] self.board_size = Config.get("board")["size"] self.ready = False + self.ai_thinking = False self.message_queue = None self.board = Board(self.board_size) self.komi = 6.5 # loaded from config in init @@ -162,7 +166,9 @@ class EngineControls(GridLayout): ts = self.train_settings while not self.board.current_move.analysis_ready: self.info.text = "Thinking..." + self.ai_thinking = True time.sleep(0.05) + self.ai_thinking = False # select move current_move = self.board.current_move pos_moves = [ @@ -233,6 +239,32 @@ class EngineControls(GridLayout): self.show_error(f"could not decode file contents of {file}") return "" + def _do_analyze_extra(self, sweep): + stones = {s.coords for s in self.board.stones} + current_move = self.board.current_move + if not current_move.analysis: + self.info.text = "Wait for initial analysis to complete before doing a board-sweep or refinement" + return + played_moves = self.board.moves + if sweep: + analyze_moves = [Move(coords=(x, y)).gtp() for x in range(self.board_size) for y in range(self.board_size) if (x, y) not in stones] + visits = self.visits[self.ai_fast.active][2] + self.info.text = f"Refining analysis of entire board to {visits} visits" + else: + analyze_moves = [a["move"] for a in current_move.analysis] + visits = current_move.analysis[0]["visits"] + self.visits[1][2] + self.info.text = f"Refining analysis of candidate moves to {visits} visits" + + for gtpcoords in analyze_moves: + self._send_analysis_query( + { + "id": f"AA:{current_move.id}:{gtpcoords}", + "moves": [[m.bw_player(), m.gtp()] for m in played_moves] + [[current_move.bw_player(True), gtpcoords]], + "includeOwnership": False, + "maxVisits": visits, + } + ) + def _do_analyze_sgf(self, sgf, faster=False, rewind=False): sgfprops = {k: v.strip("[]").split("][") if k in ["AB", "AW"] else v.strip("[]") for k, v in re.findall(r"\b(\w+)((?:\[.*?\])+)", sgf)} size = int(sgfprops.get("SZ", self.board_size)) @@ -292,14 +324,16 @@ class EngineControls(GridLayout): if self.debug: print(f"[{time.time()-self.query_time.get(analysis['id'],0):.1f}] kata analysis received:", line[:80], "...") if "error" in analysis: - print(analysis) - self.show_error(f"ERROR IN KATA ANALYSIS: {analysis['error']}") + if "AA" not in analysis["id"]: # silently drop illegal moves from analysis all + print(analysis) + self.show_error(f"ERROR IN KATA ANALYSIS: {analysis['error']}") else: self.board.store_analysis(analysis) self.update_evaluation() def _send_analysis_query(self, query): self.query_time[query["id"]] = time.time() + query = {"rules": "japanese", "komi": self.komi, "boardXSize": self.board_size, "boardYSize": self.board_size, "analyzeTurns": [len(query["moves"])], **query} if self.kata: self.kata.stdin.write((json.dumps(query) + "\n").encode()) self.kata.stdin.flush() @@ -311,23 +345,12 @@ class EngineControls(GridLayout): move_id = move.id moves = self.board.moves fast = self.ai_fast.active - query = { - "id": str(move_id), - "moves": [[m.bw_player(), m.gtp()] for m in moves], - "rules": "japanese", - "komi": self.komi, - "boardXSize": self.board_size, - "boardYSize": self.board_size, - "analyzeTurns": [len(moves)], - "includeOwnership": True, - "maxVisits": self.visits[fast][1] // faster_fac, - } + query = {"id": str(move_id), "moves": [[m.bw_player(), m.gtp()] for m in moves], "includeOwnership": True, "maxVisits": self.visits[fast][1] // faster_fac} if self.debug: print(f"sending query for move {move_id}: {str(query)[:80]}") self._send_analysis_query(query) query.update({"id": f"PASS_{move_id}", "maxVisits": self.visits[fast][0] // faster_fac, "includeOwnership": False}) query["moves"] += [[move.bw_player(next_move=True), "pass"]] - query["analyzeTurns"][0] += 1 self._send_analysis_query(query) def output_sgf(self): diff --git a/katrain.kv b/katrain.kv index 4e9d483..2c82d7b 100644 --- a/katrain.kv +++ b/katrain.kv @@ -38,6 +38,7 @@ orientation: 'vertical' checkbox: checkbox + label: label text: '' default_active: False position: (0,0) @@ -46,6 +47,7 @@ font_size: root.height * 0.2 if root.orientation=='vertical' else root.height text: root.text on_press: if not checkbox.disabled: checkbox._do_press() + id: label CheckBox: size_hint: 1,0.55 if root.orientation=='vertical' else (0.2,1) id: checkbox @@ -55,11 +57,13 @@ black: black white: white + label: label orientation: 'vertical' text: '' default_active: False CheckBoxHintLabel: size_hint: 1,0.2 + id: label text: root.text font_size: self.height on_press: if not white.disabled: white._do_press(); black._do_press() diff --git a/katrain.py b/katrain.py index fc27df1..e0524b8 100644 --- a/katrain.py +++ b/katrain.py @@ -169,11 +169,18 @@ class BadukPanWidget(Widget): # hints if self.engine.hints.active(current_player): - for i, d in enumerate(last_move.ai_moves): - move = Move(gtpcoords=d["move"], player=0) + hint_moves = last_move.ai_moves + for i, d in enumerate(hint_moves): + move = Move(gtpcoords=d["move"]) c = [*self._eval_spectrum(d["evaluation"]), 0.5] if move.coords[0] is not None and move.coords not in undo_coords: - self.draw_stone(move.coords[0], move.coords[1], c, scale=1.0 if i == 0 else 0.8) + if i == 0: + scale = 1.0 + elif d["visits"] < 0.1 * hint_moves[0]["visits"]: # TODO: config? + scale = 0.6 # TODO: config? + else: + scale = 0.85 + self.draw_stone(move.coords[0], move.coords[1], c, scale=scale) # hover next move ghost stone if self.ghost_stone: @@ -208,6 +215,27 @@ class KaTrainGui(BoxLayout): self.controls.action("redo-branch", 1) elif keycode[1] == "left": self.controls.action("redo-branch", -1) + elif keycode[1] == "s": + self.controls.action("analyze-extra", True) + elif keycode[1] == "r": + self.controls.action("analyze-extra", False) + elif keycode[1] == "a": + if not self.controls.ai_thinking: + self.controls.ai_move.trigger_action(duration=0) + elif keycode[1] == "p":# TODO: clean repetitive shortcuts + self.controls.play.trigger_action(duration=0) + elif keycode[1] == "f": + self.controls.ai_fast.label.trigger_action(duration=0) + elif keycode[1] == "h": + self.controls.hints.label.trigger_action(duration=0) + elif keycode[1] == "e": + self.controls.eval.label.trigger_action(duration=0) + elif keycode[1] == "u": + self.controls.auto_undo.label.trigger_action(duration=0) + elif keycode[1] == "b": + self.controls.ai_balance.label.trigger_action(duration=0) + elif keycode[1] == "o": + self.controls.ownership.label.trigger_action(duration=0) return True diff --git a/outdated_log.txt b/outdated_log.txt new file mode 100644 index 0000000..61f0492 --- /dev/null +++ b/outdated_log.txt @@ -0,0 +1,13 @@ +logs.append([{'lcb': 0.9866929576961049, 'move': 'Q13', 'order': 2, 'prior': 0.11910770833492279, 'pv': ['Q13', 'R13', 'S12', 'J1', 'M1', 'G16'], 'scoreLead': 124.47271696460673, 'scoreMean': 126.27482461531041, 'scoreSelfplay': 122.06720914951835, 'scoreStdev': 27.614507025114275, 'utility': 1.1346133945628056, 'utilityLcb': 1.098224634752818, 'visits': 68, 'winrate': 0.9996889433425291, 'outdatedScoreLead': 127.91250052175701, 'evaluation': 0.6566815927216013}]) +logs.append([{'lcb': 0.23000714901044894, 'move': 'A7', 'order': 2, 'prior': 0.0939372107386589, 'pv': ['A7', 'B4', 'B3', 'B7', 'G12', 'G10', 'H10', 'G9'], 'scoreLead': -9.121776796888566, 'scoreMean': -4.895108362277802, 'scoreSelfplay': -5.2100667634473705, 'scoreStdev': 21.56650224292897, 'utility': -0.4353121358045132, 'utilityLcb': -0.6303358573690578, 'visits': 97, 'winrate': 0.2996584781406435, 'evaluation': 0.9730087446544682, 'outdatedScoreLead': -4.895108362277802}]) +logs.append([{'lcb': 0.48324174996112224, 'move': 'F10', 'order': 2, 'prior': 0.12889033555984497, 'pv': ['F10', 'B9', 'A10', 'A9', 'B9', 'B7', 'H17'], 'scoreLead': 0.7020370412502724, 'scoreMean': 3.1918735507436278, 'scoreSelfplay': 3.8505667580252867, 'scoreStdev': 22.209495140940156, 'utility': 0.18945409233268506, 'utilityLcb': -0.12664315194471099, 'visits': 33, 'winrate': 0.5961336229173351, 'evaluation': 0.9857626433435042, 'outdatedScoreLead': 3.1918735507436278}]) +logs.append([{'lcb': 1.2961580404924993, 'move': 'B8', 'order': 4, 'prior': 0.18784967064857483, 'pv': ['B8', 'D10', 'D11', 'G12', 'C14', 'B12'], 'scoreLead': 39.980447187304534, 'scoreMean': 16.300339300165696, 'scoreSelfplay': 16.75147660635949, 'scoreStdev': 27.816062826637406, 'utility': 0.6706339164931426, 'utilityLcb': 2.134771010104406, 'visits': 11, 'winrate': 0.7732519356313338, 'evaluation': 0.465537391117901, 'outdatedScoreLead': 16.300339300165696}]) +logs.append([{'lcb': -0.9128409039094549, 'move': 'F6', 'order': 7, 'prior': 0.006048424169421196, 'pv': ['F6', 'E6'], 'scoreLead': -16.519556878825906, 'scoreMean': -14.270362377166748, 'scoreSelfplay': -14.119140625, 'scoreStdev': 19.22452345556336, 'utility': -0.8780453309766154, 'utilityLcb': -2.8000000000000003, 'visits': 2, 'winrate': 0.08715909609054506, 'evaluation': 1.0506796226282822, 'outdatedScoreLead': -14.270362377166748}]) +logs.append([{'lcb': -0.11179767394897855, 'move': 'C8', 'order': 4, 'prior': 0.029647205024957657, 'pv': ['C8', 'A9', 'A10', 'A8', 'A7', 'B7', 'B9'], 'scoreLead': -27.91700043151085, 'scoreMean': -21.906844603223075, 'scoreSelfplay': -21.44171085687105, 'scoreStdev': 22.73403649360621, 'utility': -0.9781789774390067, 'utilityLcb': -1.4388028521708551, 'visits': 10, 'winrate': 0.05271085274096721, 'evaluation': 0.925279362535498, 'outdatedScoreLead': -21.906844603223075}]) +logs.append([{'lcb': 0.004716111156360503, 'move': 'A8', 'order': 1, 'prior': 0.26070424914360046, 'pv': ['A8', 'A10', 'A6', 'B9', 'A9', 'C8', 'B9', 'C14'], 'scoreLead': -29.377870307329598, 'scoreMean': -25.069371462858335, 'scoreSelfplay': -24.13833752712488, 'scoreStdev': 19.46618456676441, 'utility': -1.0407429104348054, 'utilityLcb': -1.140719563219286, 'visits': 112, 'winrate': 0.0404220585793893, 'evaluation': 0.6556051615755801, 'outdatedScoreLead': -25.069371462858335}]) +logs.append([{'lcb': 0.14960351964179275, 'move': 'C8', 'order': 2, 'prior': 0.29037389159202576, 'pv': ['C8', 'B9', 'A10', 'A3', 'A6', 'A4', 'A11', 'A9'], 'scoreLead': -13.234832497594427, 'scoreMean': -19.963710264718976, 'scoreSelfplay': -18.960616476623688, 'scoreStdev': 20.125619040929173, 'utility': -0.8869855532624162, 'utilityLcb': -0.710554899867088, 'visits': 64, 'winrate': 0.0865925720006041, 'evaluation': 0.7800065883824968, 'outdatedScoreLead': -19.963710264718976}]) +logs.append([{'lcb': 0.2780396932369631, 'move': 'C14', 'order': 2, 'prior': 0.11985918134450912, 'pv': ['C14', 'A3', 'A6', 'A4', 'A10', 'G8', 'B12', 'F5', 'A11'], 'scoreLead': -6.718795563515677, 'scoreMean': -15.893474225710637, 'scoreSelfplay': -15.203650210809267, 'scoreStdev': 22.044457012561093, 'utility': -0.6851691288815772, 'utilityLcb': -0.422485576723899, 'visits': 122, 'winrate': 0.18422413889493516, 'evaluation': 1.0768651808168639, 'outdatedScoreLead': -15.893474225710637}]) +logs.append([{'lcb': -0.04419652907096561, 'move': 'A6', 'order': 1, 'prior': 0.25112441182136536, 'pv': ['A6', 'B2', 'C2', 'A3', 'B1', 'A2', 'C1', 'D10'], 'scoreLead': -30.647512138007034, 'scoreMean': -22.862077390041346, 'scoreSelfplay': -21.523760459148246, 'scoreStdev': 22.10661200254798, 'utility': -0.8707647010387976, 'utilityLcb': -1.2786073140053726, 'visits': 35, 'winrate': 0.10146154698852561, 'evaluation': 0.5179365909169477, 'outdatedScoreLead': -22.862077390041346}]) +logs.append([{'lcb': -0.04419652907096561, 'move': 'A6', 'order': 1, 'prior': 0.25112441182136536, 'pv': ['A6', 'B2', 'C2', 'A3', 'B1', 'A2', 'C1', 'D10'], 'scoreLead': -30.647512138007034, 'scoreMean': -22.862077390041346, 'scoreSelfplay': -21.523760459148246, 'scoreStdev': 22.10661200254798, 'utility': -0.8707647010387976, 'utilityLcb': -1.2786073140053726, 'visits': 35, 'winrate': 0.10146154698852561, 'evaluation': 0.2362162847661815, 'outdatedScoreLead': -22.862077390041346}]) +logs.append([{'lcb': 1.054309982706412, 'move': 'A11', 'order': 7, 'prior': 0.2366078794002533, 'pv': ['A11', 'A4', 'R9', 'D5', 'Q10'], 'scoreLead': 6.910397638811527, 'scoreMean': 4.639930709907449, 'scoreSelfplay': 5.535607733520525, 'scoreStdev': 15.874905556161806, 'utility': 0.6591456525404961, 'utilityLcb': 1.379769560744805, 'visits': 12, 'winrate': 0.7969443012048731, 'evaluation': 0.28741264032782, 'outdatedScoreLead': 4.639930709907449}]) +logs.append([{'lcb': 1.006487464714684, 'move': 'H11', 'order': 2, 'prior': 0.2041485607624054, 'pv': ['H11', 'G10', 'H7', 'H1', 'G2', 'K2'], 'scoreLead': 52.47195084132946, 'scoreMean': 46.81754482750254, 'scoreSelfplay': 46.92620595873263, 'scoreStdev': 27.67551678483527, 'utility': 1.067884055424014, 'utilityLcb': 1.1097375772381621, 'visits': 24, 'winrate': 0.9915397783524882, 'outdatedScoreLead': 45.208157412185365}])