├── message.tex ├── main.pdf ├── cover └── cover.ai ├── figures ├── sokoban.pdf ├── grid-gbfs.png ├── blocks-image.png ├── grid-astar.png ├── grid-wastar-5.png ├── manhattan-distance.png ├── msa.tex ├── reexpansion.tex ├── slidingtile.tex ├── fvalue.tex ├── astar-dijkstra.tex ├── bidirectional.tex ├── gridpathfinding.tex ├── tokenpuzzle.tex ├── tsp.tex ├── grid-brfs.tex ├── statespace.tex ├── pathtree.tex ├── mst.tex ├── bucket.tex ├── bdd.tex ├── pattern-database.tex ├── tikz.tex ├── nodetypes.tex └── external-brfs.tex ├── python ├── state_space_graph.py ├── dijkstra.py ├── wastar_search.py ├── greedy_best_first_search.py ├── depth_first_search.py ├── breadth_first_search.py ├── state_space_problem.py ├── iterative_deepening_astar.py ├── astar_search.py ├── astar_search_tiebreaking.py ├── search_node.py ├── beam_search.py ├── bucket_openlist.py ├── util.py ├── tree_search.py ├── recursive_dfs.py ├── hash_closedlist.py ├── branch_and_bound.py ├── graph_search.py ├── depth_first_iterative_deepening.py ├── tsp.py ├── grid_pathfinding.py ├── optimized_graph_search.py ├── sliding_tile.py └── parallel_graph_search.py ├── make.sh ├── README.md ├── lstlang0.sty ├── LICENSE ├── working.tex ├── main.tex ├── pythonhighlight.sty ├── intro.tex ├── data.tex ├── planning.tex ├── problem.tex ├── blind.tex ├── heuristic.tex └── spphys.bst /message.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /main.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/main.pdf -------------------------------------------------------------------------------- /cover/cover.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/cover/cover.ai -------------------------------------------------------------------------------- /figures/sokoban.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/figures/sokoban.pdf -------------------------------------------------------------------------------- /figures/grid-gbfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/figures/grid-gbfs.png -------------------------------------------------------------------------------- /figures/blocks-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/figures/blocks-image.png -------------------------------------------------------------------------------- /figures/grid-astar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/figures/grid-astar.png -------------------------------------------------------------------------------- /figures/grid-wastar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/figures/grid-wastar-5.png -------------------------------------------------------------------------------- /figures/manhattan-distance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinnaiyuu/search-ja/HEAD/figures/manhattan-distance.png -------------------------------------------------------------------------------- /figures/msa.tex: -------------------------------------------------------------------------------- 1 | Sequence1 -TCA----\\ 2 | Sequence2 ATCACG--\\ 3 | Sequence3 ATGAGG--\\ 4 | Sequence4 ---AGGCA 5 | 6 | -------------------------------------------------------------------------------- /python/state_space_graph.py: -------------------------------------------------------------------------------- 1 | class StateSpaceGraph: 2 | def __init__(self, V, E, u_0, T): 3 | self.V = V 4 | self.E = E 5 | self.u_0 = u_0 6 | self.T = T 7 | 8 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | # mkdir -p textbook 2 | platex main 3 | bibtex main 4 | mendex main 5 | platex main 6 | platex main 7 | dvipdfmx main.dvi 8 | # latex2html -init_file latex2html-init -dir textbook textbook 9 | 10 | # platex figures/tikz.tex 11 | # dvipdfmx figures/tikz.tex 12 | -------------------------------------------------------------------------------- /figures/reexpansion.tex: -------------------------------------------------------------------------------- 1 | % Reexpansion 2 | 3 | \node [draw,circle] (a) at ( 0, 4) {$a$}; 4 | \node [draw,circle] (b) at (-2, 2) {$b$}; 5 | \node [draw,circle] (c) at ( 2, 2) {$c$}; 6 | \node [draw,circle] (d) at ( 0, 0) {$d$}; 7 | 8 | \draw[->] (a) -- (b); 9 | \draw[->] (b) -- (d); 10 | 11 | \draw[->] (a) -- (c); 12 | \draw[->] (c) -- (d); 13 | -------------------------------------------------------------------------------- /python/dijkstra.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def DijkstraSearch(problem): 4 | return GraphSearch(problem, lambda node: node.g) 5 | 6 | 7 | if __name__ == "__main__": 8 | from grid_pathfinding import GridPathfinding 9 | 10 | problem = GridPathfinding() 11 | path = DijkstraSearch(problem) 12 | 13 | for s in reversed(path): 14 | print(s) -------------------------------------------------------------------------------- /figures/slidingtile.tex: -------------------------------------------------------------------------------- 1 | % Sliding tile puzzle 2 | 3 | \draw (0, 0) grid (4, 4); 4 | 5 | \foreach \x in {0,...,3} 6 | \foreach \y in {0,...,3} 7 | { 8 | \pgfmathsetmacro{\val}{int(\x+4*\y+1)} 9 | \ifthenelse{\x=3 \AND \y=3}{;}{ 10 | \node at (\x + 0.5, 3 - \y + 0.5) {\val}; 11 | } 12 | } 13 | 14 | \draw[fill=gray] (3,0) rectangle (4, 1); 15 | -------------------------------------------------------------------------------- /python/wastar_search.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def WAstarSearch(problem, w=1.0): 4 | f = lambda node: problem.heuristic(node.state) * w + node.g 5 | return GraphSearch(problem, f) 6 | 7 | 8 | if __name__ == "__main__": 9 | from grid_pathfinding import GridPathfinding 10 | 11 | problem = GridPathfinding() 12 | path = WAstarSearch(problem, w=3.0) 13 | 14 | for s in reversed(path): 15 | print(s) -------------------------------------------------------------------------------- /python/greedy_best_first_search.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def GreedyBestFirstSearch(problem): 4 | h = lambda node: problem.heuristic(node.state) 5 | return GraphSearch(problem, h) 6 | 7 | 8 | if __name__ == "__main__": 9 | from grid_pathfinding import GridPathfinding 10 | 11 | problem = GridPathfinding() 12 | path = GreedyBestFirstSearch(problem) 13 | 14 | for s in reversed(path): 15 | print(s) -------------------------------------------------------------------------------- /python/depth_first_search.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def DepthFristSearch(problem): 4 | return GraphSearch(problem, lambda node: -node.d) 5 | 6 | 7 | if __name__ == "__main__": 8 | from grid_pathfinding import GridPathfinding 9 | 10 | problem = GridPathfinding() 11 | path = DepthFristSearch(problem) 12 | 13 | print(problem.init_state.x, problem.init_state.y) 14 | for s in reversed(path): 15 | print(s.state.x, s.state.y) -------------------------------------------------------------------------------- /python/breadth_first_search.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def BreadthFristSearch(problem): 4 | return GraphSearch(problem, lambda node: node.d) 5 | 6 | 7 | if __name__ == "__main__": 8 | from grid_pathfinding import GridPathfinding 9 | 10 | problem = GridPathfinding() 11 | path = BreadthFristSearch(problem) 12 | 13 | print(problem.init_state.x, problem.init_state.y) 14 | for s in reversed(path): 15 | print(s.state.x, s.state.y) -------------------------------------------------------------------------------- /figures/fvalue.tex: -------------------------------------------------------------------------------- 1 | % BrFS region 2 | 3 | \pgfmathsetmacro{\x}{3} 4 | 5 | \node[draw,circle,fill=black] (s0) at (0,0) {}; 6 | \node (ss) at (0, 0 - 0.6) {$u_0$}; 7 | 8 | \node[draw,circle,fill=black] (t) at (2*\x,0) {}; 9 | \node (gg) at (2*\x,0 - 0.6) {$t$}; 10 | 11 | 12 | \node[draw,circle,fill=black] (c) at (\x * 0.8, 0.5 * \x) {}; 13 | \node (cc) at (\x * 0.8, 0.5 * \x - 0.6) {$u$}; 14 | 15 | 16 | \draw[->] (s0) -- (c) node[above,pos=0.5] {$g(u)$}; 17 | \draw[->,dashed] (c) -- (t) node[above,pos=0.5] {$h(u)$}; 18 | 19 | -------------------------------------------------------------------------------- /figures/astar-dijkstra.tex: -------------------------------------------------------------------------------- 1 | % BrFS region 2 | 3 | \pgfmathsetmacro{\x}{2} 4 | 5 | \draw[fill=red!30, fill opacity=0.4] (0, 0) circle (2*\x); 6 | \draw[fill=blue!30, fill opacity=0.4] ({0.9*\x}, 0) ellipse ({1.1*\x} and {0.8*\x}); 7 | 8 | \node[draw,circle,fill=black] (a) at (0,0) {}; 9 | \node (aa) at (0+0.6, 0) {$s_0$}; 10 | 11 | \node[draw,circle,fill=black] (b) at (2*\x-0.25,0) {}; 12 | \node (bb) at (2*\x+0.5,0) {$t$}; 13 | 14 | 15 | \node (d) at (0, 2*\x+0.3) {Dijkstra}; 16 | \node (a) at ({0.9*\x}, {0.8*\x+0.3}) {A*}; 17 | -------------------------------------------------------------------------------- /python/state_space_problem.py: -------------------------------------------------------------------------------- 1 | class StateSpaceProblem: 2 | def __init__(self): 3 | assert False 4 | 5 | def get_init_state(self): 6 | assert False 7 | 8 | def is_goal(self, state) -> bool: 9 | assert False 10 | 11 | def get_available_actions(self, state): 12 | assert False 13 | 14 | def get_next_state(self, state, action): 15 | assert False 16 | 17 | def get_action_cost(self, state, action): 18 | assert False 19 | 20 | def heuristic(self, state): 21 | assert False 22 | -------------------------------------------------------------------------------- /python/iterative_deepening_astar.py: -------------------------------------------------------------------------------- 1 | from depth_first_iterative_deepening import IterativeDeepening 2 | 3 | def IterativeDeepeningAstar(problem): 4 | return IterativeDeepening(problem, lambda node: node.g + problem.heuristic(node.state)) 5 | 6 | if __name__ == "__main__": 7 | from tsp import Tsp 8 | 9 | cities = [ 10 | (0, 0), 11 | (1, 0), 12 | (0, 1), 13 | (1, 1) 14 | ] 15 | 16 | problem = Tsp(cities) 17 | path = IterativeDeepeningAstar(problem) 18 | 19 | for p in reversed(path): 20 | print(p) -------------------------------------------------------------------------------- /figures/bidirectional.tex: -------------------------------------------------------------------------------- 1 | \pgfmathsetmacro{\x}{2} 2 | % forward search 3 | \draw[fill=red!30, fill opacity=0.4] (0, 0) circle (2*\x); 4 | 5 | % bidirectional search 6 | \draw[fill=blue!30, fill opacity=0.4] (0, 0) circle (1*\x); 7 | \draw[fill=blue!30, fill opacity=0.4] (2*\x, 0) circle (1*\x); 8 | 9 | 10 | \node[draw,circle,fill=black] (a) at (0,0) {}; 11 | \node (aa) at (0+0.6, 0) {$s_0$}; 12 | 13 | \node[draw,circle,fill=black] (b) at (2*\x,0) {}; 14 | \node (bb) at (2*\x+0.5,0) {$g$}; 15 | 16 | \node (d) at (0, 2*\x+0.3) {Dijkstra}; 17 | \node (a) at (\x, 1*\x+0.3) {Bidirectional}; 18 | -------------------------------------------------------------------------------- /figures/gridpathfinding.tex: -------------------------------------------------------------------------------- 1 | % Grid pathfinding 2 | 3 | \foreach \x in {0,...,3} 4 | \foreach \y in {0,...,3} 5 | {\node [draw, circle, fill=black] (\x\y) at (1.5*\x, 1.5*\y) {};} 6 | 7 | \foreach \x in {0,...,3} 8 | \foreach \y in {0,...,2} 9 | {\draw (1.5 * \x, 1.5 * \y) -- (1.5 * \x, 1.5 * \y + 1.5);} 10 | 11 | \foreach \y in {0,...,3} 12 | \foreach \x in {0,...,2} 13 | {\draw (1.5 * \x, 1.5 * \y) -- (1.5 * \x + 1.5, 1.5 * \y);} 14 | 15 | \foreach \x in {0,...,3} 16 | {\node at (1.5 * \x, -1) {\x}; 17 | \node at (-1, 1.5 * \x) {\x}; 18 | } 19 | -------------------------------------------------------------------------------- /figures/tokenpuzzle.tex: -------------------------------------------------------------------------------- 1 | 2 | \pgfmathsetmacro{\phi}{0.15} 3 | 4 | \foreach \base in {0, 6} { 5 | \draw (\base+0,0) grid (\base+4, 1); 6 | 7 | \foreach \x in {0, 1, 2, 3} { 8 | \node at (\base+\x+0.5, 1+0.5) {\x}; 9 | } 10 | 11 | \node at (\base+0+0.5, -1+0.5) {00}; 12 | \node at (\base+1+0.5, -1+0.5) {01}; 13 | \node at (\base+2+0.5, -1+0.5) {10}; 14 | \node at (\base+3+0.5, -1+0.5) {11}; 15 | 16 | } 17 | 18 | \draw[fill=black] (0+\phi,0+\phi) rectangle (1-\phi,1-\phi); 19 | 20 | \draw[fill=black] (6+3+\phi,0+\phi) rectangle (6+4-\phi,1-\phi); 21 | 22 | \draw[->] (4+\phi, 0.5) -- (6-\phi, 0.5); 23 | -------------------------------------------------------------------------------- /python/astar_search.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def AstarSearch(problem): 4 | f = lambda node: problem.heuristic(node.state) + node.g 5 | return GraphSearch(problem, f) 6 | 7 | 8 | if __name__ == "__main__": 9 | from grid_pathfinding import GridPathfinding 10 | from sliding_tile import SlidingTile 11 | problem = GridPathfinding(100, 100, goal_position=(99, 99)) 12 | path = AstarSearch(problem) 13 | 14 | for s in reversed(path): 15 | print(s) 16 | 17 | tiles = SlidingTile(3, 3, init_position=[7, 4, 5, 1, 8, 3, 2, 0, 6]) 18 | path = AstarSearch(tiles) 19 | 20 | for s in reversed(path): 21 | print(s) 22 | -------------------------------------------------------------------------------- /figures/tsp.tex: -------------------------------------------------------------------------------- 1 | \foreach \x in {0, 8} 2 | { 3 | \pgfmathsetmacro{\noise}{0.15} 4 | \node[draw,circle,fill=black] (1\x) at (\x+0, 0) {}; 5 | \node[draw,circle,fill=black] (2\x) at (\x+2, 1-\noise) {}; 6 | \node[draw,circle,fill=black] (3\x) at (\x+3+2*\noise, 2) {}; 7 | \node[draw,circle,fill=black] (4\x) at (\x+2-\noise, 3-\noise) {}; 8 | \node[draw,circle,fill=black] (5\x) at (\x+1, 2-2*\noise) {}; 9 | \node[draw,circle,fill=black] (6\x) at (\x+0, 3+\noise) {}; 10 | \node[draw,circle,fill=black] (7\x) at (\x-1+\noise, 1) {}; 11 | } 12 | 13 | \foreach \a in {1,...,6} 14 | { 15 | \pgfmathsetmacro{\n}{int(\a+1)} 16 | \draw[-] (\a8) -- (\n8); 17 | } 18 | 19 | \draw[-] (78) -- (18); 20 | -------------------------------------------------------------------------------- /python/astar_search_tiebreaking.py: -------------------------------------------------------------------------------- 1 | from graph_search import GraphSearch 2 | 3 | def TiebreakingAstarSearch(problem): 4 | f = lambda node: (problem.heuristic(node.state) + node.g, problem.heuristic(node.state)) 5 | return GraphSearch(problem, f) 6 | 7 | 8 | if __name__ == "__main__": 9 | from grid_pathfinding import GridPathfinding 10 | from sliding_tile import SlidingTile 11 | 12 | problem = GridPathfinding() 13 | path = TiebreakingAstarSearch(problem) 14 | 15 | for s in reversed(path): 16 | print(s) 17 | 18 | 19 | tiles = SlidingTile(3, 3, init_position=[7, 4, 5, 1, 8, 3, 2, 0, 6]) 20 | path = TiebreakingAstarSearch(tiles) 21 | 22 | for s in reversed(path): 23 | print(s) -------------------------------------------------------------------------------- /python/search_node.py: -------------------------------------------------------------------------------- 1 | class SearchNode: 2 | def __init__(self, state): 3 | self.state = state 4 | 5 | def set_g(self, g): 6 | self.g = g 7 | 8 | def set_d(self, d): 9 | self.d = d 10 | 11 | def set_prev_n(self, prev_n): 12 | self.prev_n = prev_n 13 | 14 | def __str__(self): 15 | return self.state.__str__() + \ 16 | ": g=" + str(self.g) + ", d=" + str(self.d) 17 | 18 | def get_path(self): 19 | cur_node = self 20 | path = [] 21 | 22 | while (cur_node is not None \ 23 | and hasattr(cur_node, 'prev_n')): 24 | path.append(cur_node) 25 | cur_node = cur_node.prev_n 26 | 27 | return path -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ヒューリスティック探索入門 2 | 3 | このテキストの目的は、この分野の資料を日本語で残し、日本の研究者・エンジニアの方々がヒューリスティック探索の基礎知識に簡単にアクセスできるようにすることです。 4 | 5 | ヒューリスティック探索は強化学習や自動運転など最近ホットな話題の基礎となる研究分野であり、かつエンジニアリングのさまざまな場面で使われる手法でもあります。 6 | この分野は世界的には非常に重要な分野であると認識されていますが、残念ながら日本では研究者が少ない分野です。 7 | そのため、日本語で読めるヒューリスティック探索の文章が少ない状況になっています。 8 | 9 | このテキストを読んでヒューリスティック探索の面白さを知る方が増えてくれたら幸いです。 10 | 11 | 12 | # ビルド 13 | 14 | make.shでビルドが出来ます。以下の環境で実行しています。 15 | 16 | - Ubuntu 18.04およびWindows WSL内のUbuntu 18.04で確認 17 | - platex e-pTeX, Version 3.14159265-p3.7.1-161114-2.6 (utf8.euc) (TeX Live 2017/Debian) (preloaded format=platex) 18 | - mendex version 2.6f [14-Aug-2009] (utf8.euc) (TeX Live 2017). 19 | 20 | # 誤字脱字 21 | 22 | 誤字脱字がありましたらIssuesを建てるかあるいは陣内 にご連絡いただければ幸いです。 -------------------------------------------------------------------------------- /figures/grid-brfs.tex: -------------------------------------------------------------------------------- 1 | % BrFS region 2 | 3 | \draw[rounded corners, fill=gray!20, fill opacity=0.3] (-0.4, -0.4) -- (3*1.5+0.5, -0.4) -- (3*1.5+0.5, 0.2) -- (0.2, 3*1.5+0.5) -- (-0.4, 3*1.5+0.5) -- cycle; 4 | 5 | 6 | % Grid pathfinding 7 | 8 | \foreach \x in {0,...,3} 9 | \foreach \y in {0,...,3} 10 | {\node [draw, circle, fill=black] (\x\y) at (1.5*\x, 1.5*\y) {};} 11 | 12 | \foreach \x in {0,...,3} 13 | \foreach \y in {0,...,2} 14 | {\draw (1.5 * \x, 1.5 * \y) -- (1.5 * \x, 1.5 * \y + 1.5);} 15 | 16 | \foreach \y in {0,...,3} 17 | \foreach \x in {0,...,2} 18 | {\draw (1.5 * \x, 1.5 * \y) -- (1.5 * \x + 1.5, 1.5 * \y);} 19 | 20 | \foreach \x in {0,...,3} 21 | {\node at (1.5 * \x, -1) {\x}; 22 | \node at (-1, 1.5 * \x) {\x}; 23 | } 24 | -------------------------------------------------------------------------------- /figures/statespace.tex: -------------------------------------------------------------------------------- 1 | % MDP i 2 | \node [draw, circle, minimum size=0.7cm] (a) at (2, 2) {$a$}; 3 | \node [draw, circle, minimum size=0.7cm] (b) at (4, 2) {$b$}; 4 | \node [draw, circle, minimum size=0.7cm] (c) at (0, 4) {$c$}; 5 | \node [draw, circle, minimum size=0.7cm] (d) at (0, 2) {$d$}; 6 | \node [draw, circle, minimum size=0.7cm] (e) at (2, 0) {$e$}; 7 | \node [draw, circle, minimum size=0.7cm] (f) at (4, 0) {$f$}; 8 | \node [draw, circle, minimum size=0.7cm] (g) at (0, 0) {$g$}; 9 | \node [draw, circle, minimum size=0.5cm] at (0, 0) {}; 10 | 11 | \coordinate[above of=a] (init); 12 | 13 | \draw[->] (init) -- (a); 14 | \draw[-] (a) -- (b); 15 | \draw[-] (a) -- (c); 16 | \draw[-] (a) -- (d); 17 | \draw[-] (b) -- (e); 18 | \draw[-] (b) -- (f); 19 | \draw[-] (c) -- (d); 20 | \draw[-] (d) -- (g); 21 | \draw[-] (e) -- (f); 22 | \draw[-] (d) -- (g); 23 | -------------------------------------------------------------------------------- /python/beam_search.py: -------------------------------------------------------------------------------- 1 | from optimized_graph_search import OptimizedGraphSearch 2 | from bucket_openlist import BucketOpenList 3 | from hash_closedlist import HashClosedList 4 | 5 | def BeamSearch(problem, priority_f, beam_width): 6 | init_h_value = problem.heuristic(problem.get_init_state()) 7 | open_list = BucketOpenList(C_min=init_h_value, C_max=init_h_value*8, beam_width=beam_width) 8 | closed_list = HashClosedList() 9 | path = OptimizedGraphSearch(problem, priority_f, open_list, closed_list) 10 | return path 11 | 12 | if __name__ == "__main__": 13 | from grid_pathfinding import GridPathfinding 14 | 15 | problem = GridPathfinding(10, 10, goal_position=(9, 9)) 16 | priority_f = lambda node: node.g 17 | 18 | path = BeamSearch(problem, priority_f, beam_width=5) 19 | 20 | for s in reversed(path): 21 | print(s) 22 | -------------------------------------------------------------------------------- /figures/pathtree.tex: -------------------------------------------------------------------------------- 1 | % MDP i 2 | \node (a0) at (4, 6) {$a$}; 3 | 4 | \node (b1) at (2, 4) {$b$}; 5 | \node (c1) at (4, 4) {$c$}; 6 | \node (d1) at (6, 4) {$d$}; 7 | 8 | \node (e2) at (0, 2) {$e$}; 9 | \node (f2) at (2, 2) {$f$}; 10 | \node (d2) at (4, 2) {$d$}; 11 | \node (c2) at (6, 2) {$c$}; 12 | \node (g2) at (8, 2) {$g$}; 13 | 14 | \node (f3) at (0, 0) {$f$}; 15 | \node (e3) at (2, 0) {$e$}; 16 | \node (a31) at (3, 0) {$a$}; 17 | \node (c3) at (4, 0) {$c$}; 18 | \node (g3) at (5, 0) {$g$}; 19 | \node (a32) at (6, 0) {$a$}; 20 | 21 | \draw[-] (a0) -- (b1); 22 | \draw[-] (a0) -- (c1); 23 | \draw[-] (a0) -- (d1); 24 | \draw[-] (b1) -- (e2); 25 | \draw[-] (b1) -- (f2); 26 | \draw[-] (c1) -- (d2); 27 | \draw[-] (d1) -- (g2); 28 | \draw[-] (d1) -- (c2); 29 | \draw[-] (e2) -- (f3); 30 | \draw[-] (f2) -- (e3); 31 | \draw[-] (d2) -- (a31); 32 | \draw[-] (d2) -- (c3); 33 | \draw[-] (d2) -- (g3); 34 | \draw[-] (c2) -- (a32); 35 | -------------------------------------------------------------------------------- /lstlang0.sty: -------------------------------------------------------------------------------- 1 | \lst@definelanguage{pddl} 2 | { 3 | keywords={ 4 | define, 5 | }, 6 | morekeywords={[2] 7 | domain, 8 | problem, 9 | requirements, 10 | predicates, 11 | types, 12 | objects, 13 | action, 14 | init, 15 | goal, 16 | }, 17 | morekeywords={[3] 18 | parameters, 19 | vars, 20 | precondition, 21 | effect, 22 | }, 23 | morekeywords={[4] 24 | forall, 25 | or, 26 | and, 27 | not, 28 | when, 29 | =, 30 | exists, 31 | imply, 32 | }, 33 | comment=[l]{\;}, 34 | sensitive=true 35 | }[keywords, comments] 36 | 37 | \lst@definelanguage[mapl]{pddl}[]{pddl} 38 | { 39 | morekeywords={[2] 40 | sensor, 41 | }, 42 | morekeywords={[3] 43 | sense, 44 | replan, 45 | }, 46 | morekeywords={[4] 47 | KIF, 48 | }, 49 | } -------------------------------------------------------------------------------- /figures/mst.tex: -------------------------------------------------------------------------------- 1 | \foreach \x in {0, 8} 2 | { 3 | \pgfmathsetmacro{\noise}{0.15} 4 | \node[draw,circle,fill=black] (1\x) at (\x+0, 0) {}; 5 | \node[draw,circle,fill=black] (2\x) at (\x+2, 1-\noise) {}; 6 | \node[draw,circle,fill=black] (3\x) at (\x+3+2*\noise, 2) {}; 7 | \node[draw,circle,fill=black] (4\x) at (\x+2-\noise, 3-\noise) {}; 8 | \node[draw,circle,fill=black] (5\x) at (\x+1, 2-2*\noise) {}; 9 | \node[draw,circle,fill=black] (6\x) at (\x+0, 3+\noise) {}; 10 | \node[draw,circle,fill=black] (7\x) at (\x-1+\noise, 1) {}; 11 | } 12 | 13 | % TSP Solution 14 | \foreach \a in {1,...,6} 15 | { 16 | \pgfmathsetmacro{\n}{int(\a+1)} 17 | \draw[-] (\a0) -- (\n0); 18 | } 19 | 20 | \draw[-] (70) -- (10); 21 | 22 | % Minimum spanning tree 23 | 24 | \draw[-] (18) -- (78); 25 | \draw[-] (28) -- (58); 26 | \draw[-] (38) -- (28); 27 | \draw[-] (48) -- (58); 28 | \draw[-] (58) -- (68); 29 | \draw[-] (78) -- (58); 30 | -------------------------------------------------------------------------------- /figures/bucket.tex: -------------------------------------------------------------------------------- 1 | % Array 2 | \draw (0, 0) grid (6, 1); 3 | 4 | % f-value and index 5 | \foreach \x in {0, 1, ..., 5} { 6 | \pgfmathsetmacro{\fval}{int(\x + 6)} 7 | \node (f\x) at (\x + 0.5, 0.5) {\fval}; 8 | \node (i\x) at (\x + 0.5, 1.5) {\x}; 9 | } 10 | 11 | % label 12 | \node (f) at (-1, 0.5) {$f$ value}; 13 | \node (i) at (-1, 1.5) {index}; 14 | 15 | 16 | % items 17 | \node at (0 + 0.5, -1) [circle, draw] (n01) {$s$}; 18 | \draw[->] (0 + 0.5, 0) -- (n01); 19 | 20 | \node at (2 + 0.5, -1) [circle, draw] (n21) {$s$}; 21 | \node at (2 + 0.5, -2.5) [circle, draw] (n22) {$s$}; 22 | \node at (2 + 0.5, -4) [circle, draw] (n23) {$s$}; 23 | \draw[->] (2 + 0.5, 0) -- (n21); 24 | \draw[->] (n21) -- (n22); 25 | \draw[->] (n22) -- (n23); 26 | 27 | \node at (3 + 0.5, -1) [circle, draw] (n31) {$s$}; 28 | \draw[->] (3 + 0.5, 0) -- (n31); 29 | 30 | \node at (4 + 0.5, -1) [circle, draw] (n41) {$s$}; 31 | \node at (4 + 0.5, -2.5) [circle, draw] (n42) {$s$}; 32 | \draw[->] (4 + 0.5, 0) -- (n41); 33 | \draw[->] (n41) -- (n42); 34 | 35 | -------------------------------------------------------------------------------- /figures/bdd.tex: -------------------------------------------------------------------------------- 1 | % Unreduced 2 | \node[circle,draw] (00) at (0, 4) {$x_0$}; 3 | 4 | \node[circle,draw] (10) at (-1, 2) {$x_1$}; 5 | \node[circle,draw] (11) at (1, 2) {$x_1$}; 6 | 7 | \node[rectangle,draw] (20) at (-1-0.5, 0) {$0$}; 8 | \node[rectangle,draw] (21) at (-1+0.5, 0) {$0$}; 9 | \node[rectangle,draw] (22) at ( 1-0.5, 0) {$0$}; 10 | \node[rectangle,draw] (23) at ( 1+0.5, 0) {$1$}; 11 | 12 | \draw[->] (00) -- (10); 13 | \draw[->] (00) -- (11); 14 | 15 | \draw[->] (10) -- (20); 16 | \draw[->] (10) -- (21); 17 | \draw[->] (11) -- (22); 18 | \draw[->] (11) -- (23); 19 | 20 | \node at (0, -1) {Unreduced}; 21 | 22 | % Reduced 23 | \pgfmathsetmacro{\base}{5} 24 | \node[circle,draw] (00) at (\base+0, 4) {$x_0$}; 25 | 26 | \node[circle,draw] (11) at (\base+1, 2) {$x_1$}; 27 | 28 | \node[rectangle,draw] (22) at (\base-0.5, 0) {$0$}; 29 | \node[rectangle,draw] (23) at (\base+ 0.5, 0) {$1$}; 30 | 31 | \draw[->] (00) -- (22); 32 | \draw[->] (00) -- (11); 33 | 34 | \draw[->] (11) -- (22); 35 | \draw[->] (11) -- (23); 36 | 37 | \node at (\base+0, -1) {Reduced}; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yuu David Jinnai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /figures/pattern-database.tex: -------------------------------------------------------------------------------- 1 | 2 | %%% Initial state 3 | 4 | \draw (0, 0) grid (3, 3); 5 | 6 | 7 | \node at (0 + 0.5, 0 + 0.5) {1}; 8 | \draw[fill=gray] (1,0) rectangle (2, 1); 9 | \node at (2 + 0.5, 0 + 0.5) {3}; 10 | 11 | \node at (0 + 0.5, 1 + 0.5) {*}; 12 | \node at (1 + 0.5, 1 + 0.5) {*}; 13 | \node at (2 + 0.5, 1 + 0.5) {2}; 14 | 15 | \node at (0 + 0.5, 2 + 0.5) {*}; 16 | \node at (1 + 0.5, 2 + 0.5) {4}; 17 | \node at (2 + 0.5, 2 + 0.5) {*}; 18 | 19 | \node at (1.5, -0.5) {Initial State}; 20 | 21 | %%% Goal state 22 | \pgfmathsetmacro{\base}{4} 23 | 24 | \draw (\base+0, 0) grid (\base+3, 3); 25 | 26 | \foreach \x in {0,...,2} 27 | { 28 | \foreach \y in {0,...,2} 29 | { 30 | \pgfmathsetmacro{\val}{int(\x+3*\y+1)} 31 | \ifthenelse{\x=2 \AND \y=2}{;}{ 32 | \ifthenelse{\val>4}{ 33 | \node at (\base+\x + 0.5, 2 - \y + 0.5) {*}; 34 | }{ 35 | \node at (\base+\x + 0.5, 2 - \y + 0.5) {\val}; 36 | } 37 | } 38 | } 39 | } 40 | \draw[fill=gray] (\base+2,0) rectangle (\base+3, 1); 41 | 42 | \node at (\base+1.5, -0.5) {Goal State}; 43 | -------------------------------------------------------------------------------- /python/bucket_openlist.py: -------------------------------------------------------------------------------- 1 | class BucketOpenList: 2 | def __init__(self, C_min, C_max, beam_width=None): 3 | self.C_min = C_min 4 | self.C_max = C_max 5 | self.bucket = [[] for _ in range(C_max - C_min + 1)] 6 | self.size = 0 7 | self.beam_width = beam_width 8 | 9 | def __len__(self): 10 | return self.size 11 | 12 | def pop(self): 13 | assert self.size > 0 14 | for i in range(len(self.bucket)): 15 | if len(self.bucket[i]) > 0: 16 | self.size -= 1 17 | return self.bucket[i].pop() 18 | assert False 19 | 20 | def push(self, item, priority): 21 | self.bucket[priority - self.C_min].append(item) 22 | self.size += 1 23 | 24 | if self.beam_width is not None: 25 | self.shrink() 26 | 27 | def shrink(self, beam_width=None): 28 | # For limited memory search like beam search 29 | if beam_width is None: 30 | beam_width = self.beam_width 31 | if beam_width is not None: 32 | cur_C = self.C_max - self.C_min 33 | while self.size > beam_width: 34 | if len(self.bucket[cur_C]) > 0: 35 | self.bucket[cur_C].pop() 36 | self.size -= 1 37 | else: 38 | cur_C -= 1 39 | -------------------------------------------------------------------------------- /figures/tikz.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | \errorcontextlines=200 3 | \documentclass[11pt,dvipdfmx,b5paper]{book} 4 | 5 | \usepackage{times} 6 | \usepackage{courier} 7 | \usepackage{comment} 8 | \usepackage{subfig} 9 | \usepackage{graphicx} 10 | 11 | \usepackage{enumitem} 12 | \usepackage{url} 13 | \usepackage{listings} 14 | \usepackage{tikz} % Drawing sliding-tile 15 | \usepackage{xcolor} 16 | 17 | %%%%%%%%%%%%%%%%%%%%% 18 | % Math 19 | \usepackage{amsmath} 20 | \usepackage{amsthm} 21 | \usepackage{amssymb} 22 | \usepackage{mathtools} 23 | \DeclarePairedDelimiter\ceil{\lceil}{\rceil} 24 | \DeclarePairedDelimiter\floor{\lfloor}{\rfloor} 25 | 26 | %%%%%%%%%%%%%%%%%%%%% 27 | % Format 28 | \usepackage[Bjornstrup]{fncychap} 29 | \usepackage{titlesec} 30 | \usepackage[framemethod=TikZ]{mdframed} 31 | \usepackage{booktabs} 32 | 33 | \setcounter{secnumdepth}{3} 34 | \renewcommand{\baselinestretch}{1.15} 35 | 36 | \usepackage{appendix} 37 | \usepackage{hyperref} 38 | \usepackage{makeidx} 39 | \makeindex 40 | 41 | 42 | %%%%%%%%%%%%%%%%%%%% 43 | % Algorithm 44 | \usepackage[boxruled,linesnumbered,noend]{algorithm2e} 45 | \SetKwInOut{Input}{Input} 46 | \SetKwInOut{Output}{Output} 47 | \SetKwInOut{Side}{Side effect} 48 | \SetKwComment{Comment}{$\triangleright$}{} 49 | 50 | \begin{document} 51 | 52 | \begin{figure} 53 | \begin{tikzpicture}[scale=0.6] 54 | \input{manhattan-distance.tex} 55 | \end{tikzpicture} 56 | \end{figure} 57 | 58 | \end{document} 59 | -------------------------------------------------------------------------------- /python/util.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class SearchLogger: 4 | def __init__(self) -> None: 5 | self.expanded = 0 6 | self.generated = 0 7 | self.pruned = 0 8 | 9 | def start(self): 10 | self.start_perf_time = time.perf_counter() 11 | self.start_time = time.time() 12 | 13 | def end(self): 14 | self.end_perf_time = time.perf_counter() 15 | self.end_time = time.time() 16 | 17 | def branching_factor(self): 18 | return self.generated / self.expanded 19 | 20 | def pruned_rate(self): 21 | return self.pruned / (self.generated + self.expanded) 22 | 23 | def time(self): 24 | return self.end_time - self.start_time 25 | 26 | def perf_time(self): 27 | return self.end_perf_time - self.start_perf_time 28 | 29 | def expansion_rate(self): 30 | return self.expanded / self.time() 31 | 32 | def generation_rate(self): 33 | return self.generated / self.time() 34 | 35 | def print(self): 36 | print("Time: ", self.time()) 37 | print("Perf Time: ", self.perf_time()) 38 | print("Expanded: ", self.expanded) 39 | print("Generated: ", self.generated) 40 | print("Pruned: ", self.pruned) 41 | print("Expansion rate: ", self.expansion_rate()) 42 | print("Generation rate: ", self.generation_rate()) 43 | print("Branching factor: ", self.branching_factor()) 44 | print("Pruned rate: ", self.pruned_rate()) 45 | -------------------------------------------------------------------------------- /python/tree_search.py: -------------------------------------------------------------------------------- 1 | from search_node import SearchNode 2 | from util import SearchLogger 3 | 4 | def TreeSearch(problem, priority_f=None): 5 | open = [] 6 | 7 | init_state = problem.get_init_state() 8 | init_node = SearchNode(init_state) 9 | init_node.set_g(0) 10 | init_node.set_d(0) 11 | 12 | logger = SearchLogger() 13 | logger.start() 14 | 15 | open.append(init_node) 16 | 17 | while (len(open) > 0): 18 | open.sort(key=lambda node: priority_f(node), reverse=True) 19 | 20 | node = open.pop() 21 | logger.expanded += 1 22 | 23 | if problem.is_goal(node.state): 24 | logger.end() 25 | logger.print() 26 | return node.get_path() 27 | else: 28 | actions = problem.get_available_actions(node.state) 29 | 30 | for a in actions: 31 | next_state = problem.get_next_state(node.state, a) 32 | 33 | next_node = SearchNode(next_state) 34 | next_node.set_g(node.g + problem.get_action_cost(node.state, a)) 35 | next_node.set_d(node.d + 1) 36 | next_node.set_prev_n(node) 37 | open.append(next_node) 38 | logger.generated += 1 39 | 40 | logger.end() 41 | logger.print() 42 | return None 43 | 44 | 45 | if __name__ == "__main__": 46 | from grid_pathfinding import GridPathfinding 47 | 48 | problem = GridPathfinding() 49 | priority_f = lambda node: node.g 50 | path = TreeSearch(problem, priority_f) 51 | 52 | for s in reversed(path): 53 | print(s) -------------------------------------------------------------------------------- /figures/nodetypes.tex: -------------------------------------------------------------------------------- 1 | % Reexpansion 2 | 3 | \node [draw,circle] (a) at ( 0, 4) {a}; 4 | 5 | \node [draw,circle] (b) at (-1, 2) {b}; 6 | \node [draw,circle] (c) at ( 1, 2) {c}; 7 | 8 | \node [draw,circle] (d) at (-2, 0) {d}; 9 | \node [draw,circle] (e) at ( 0, 0) {e}; 10 | \node [draw,circle] (f) at ( 2, 0) {f}; 11 | 12 | \node [draw,circle] (g) at (-3,-2) {g}; 13 | \node [draw,circle] (h) at (-1,-2) {h}; 14 | \node [draw,circle] (i) at ( 1,-2) {i}; 15 | \node [draw,circle] (j) at ( 3,-2) {j}; 16 | 17 | \draw[->] (a) -- (b); 18 | \draw[->] (a) -- (c); 19 | 20 | \draw[->] (b) -- (d); 21 | \draw[->] (b) -- (e); 22 | \draw[->] (c) -- (e); 23 | \draw[->] (c) -- (f); 24 | 25 | \draw[->] (d) -- (g); 26 | \draw[->] (d) -- (h); 27 | \draw[->] (e) -- (h); 28 | \draw[->] (e) -- (i); 29 | \draw[->] (f) -- (i); 30 | \draw[->] (f) -- (j); 31 | 32 | 33 | 34 | % expanded nodes 35 | \draw[rounded corners, fill=gray!20, fill opacity=0.3] (0, 4 + 1) -- (-1 - 1.1, 2 - 0.6) -- (1 + 1.1, 2 - 0.6) -- cycle; 36 | 37 | \node (label3) at (4.2, {((4+1) + (2 - 0.6)) / 2}) {(3) Expanded nodes}; 38 | 39 | \coordinate (draw3) at ({(0 + (1 + 1.1)) / 2}, {((4+1) + (2 - 0.6)) / 2}); 40 | \draw[-] (draw3) -- (label3); 41 | 42 | % generated nodes 43 | \draw[rounded corners, fill=gray!10, fill opacity=0.3] (0, 4 + 1 + 0.4) -- (-2 - 1.4, 0 - 0.6) -- (2 + 1.4, 0 - 0.6) -- cycle; 44 | 45 | \node (label2) at (5.2, {((4+1+0.4) + (0 - 0.6)) / 2}) {(2) Generated nodes}; 46 | 47 | \coordinate (draw2) at ({(0 + (2 + 1.4)) / 2}, {((4+1+0.4) + (0 - 0.6)) / 2}); 48 | \draw[-] (draw2) -- (label2); 49 | 50 | 51 | \node at (7, -2) {(1) Undiscovered nodes}; 52 | 53 | -------------------------------------------------------------------------------- /python/recursive_dfs.py: -------------------------------------------------------------------------------- 1 | from search_node import SearchNode 2 | from util import SearchLogger 3 | 4 | logger = SearchLogger() 5 | 6 | def RecursiveSearchEngine(problem, cur_node): 7 | logger.expanded += 1 8 | 9 | if problem.is_goal(cur_node.state): 10 | return [cur_node] 11 | else: 12 | actions = problem.get_available_actions(cur_node.state) 13 | 14 | for a in actions: 15 | next_state = problem.get_next_state(cur_node.state, a) 16 | 17 | next_node = SearchNode(next_state) 18 | next_node.set_g(cur_node.g + problem.get_action_cost(cur_node.state, a)) 19 | next_node.set_d(cur_node.d + 1) 20 | next_node.set_prev_n(cur_node) 21 | 22 | logger.generated += 1 23 | path = RecursiveSearchEngine(problem, next_node) 24 | if len(path) > 0: 25 | path.append(cur_node) 26 | return path 27 | return [] 28 | 29 | def RecursiveDepthFirstSearch(problem): 30 | init_state = problem.get_init_state() 31 | init_node = SearchNode(init_state) 32 | init_node.set_g(0) 33 | init_node.set_d(0) 34 | 35 | logger.start() 36 | 37 | path = RecursiveSearchEngine(problem, init_node) 38 | 39 | logger.end() 40 | logger.print() 41 | 42 | return path 43 | 44 | 45 | if __name__ == "__main__": 46 | from tsp import Tsp 47 | 48 | cities = [ 49 | (0, 0), 50 | (1, 0), 51 | (0, 1), 52 | (1, 1) 53 | ] 54 | 55 | problem = Tsp(cities) 56 | path = RecursiveDepthFirstSearch(problem) 57 | 58 | for p in reversed(path): 59 | print(p) -------------------------------------------------------------------------------- /python/hash_closedlist.py: -------------------------------------------------------------------------------- 1 | class HashClosedList: 2 | def __init__(self, max_size=10000): 3 | self.table = [[]] * max_size 4 | self.table_size = max_size 5 | self.size = 0 6 | 7 | def __len__(self): 8 | return self.size 9 | 10 | def push(self, item): 11 | hash_value = hash(item.state) % self.table_size 12 | 13 | states = [n.state for n in self.table[hash_value]] 14 | if item.state in states: 15 | idx = states.index(item.state) 16 | 17 | if item.g < self.table[hash_value][idx].g: 18 | self.table[hash_value][idx].set_g(item.g) 19 | self.table[hash_value][idx].set_d(item.d) 20 | self.table[hash_value][idx].set_prev_n(item.prev_n) 21 | else: 22 | self.table[hash_value].append(item) 23 | self.size += 1 24 | 25 | def is_explored(self, item): 26 | hash_value = hash(item.state) % self.table_size 27 | 28 | for n in self.table[hash_value]: 29 | if (n.state == item.state) and (n.g <= item.g): 30 | return True 31 | return False 32 | 33 | def find(self, item): 34 | hash_value = hash(item.state) % self.table_size 35 | if self.table[hash_value] == None: 36 | return None 37 | else: 38 | states = [n.state for n in self.table[hash_value]] 39 | if item.state in states: 40 | idx = states.index(item.state) 41 | return (hash_value, idx) 42 | 43 | return None 44 | 45 | def get_by_index(self, hash_value, idx): 46 | return self.table[hash_value][idx] 47 | 48 | -------------------------------------------------------------------------------- /python/branch_and_bound.py: -------------------------------------------------------------------------------- 1 | from search_node import SearchNode 2 | from util import SearchLogger 3 | 4 | logger = SearchLogger() 5 | 6 | def BnBEngine(problem, cur_node, best_solution, path=None): 7 | logger.expanded += 1 8 | if problem.is_goal(cur_node.state): 9 | if cur_node.g < best_solution: 10 | best_solution = cur_node.g 11 | path = cur_node.get_path() 12 | else: 13 | actions = problem.get_available_actions(cur_node.state) 14 | 15 | for a in actions: 16 | next_state = problem.get_next_state(cur_node.state, a) 17 | 18 | next_node = SearchNode(next_state) 19 | next_node.set_g(cur_node.g + problem.get_action_cost(cur_node.state, a)) 20 | next_node.set_d(cur_node.d + 1) 21 | next_node.set_prev_n(cur_node) 22 | 23 | if next_node.g + problem.heuristic(next_state) < best_solution: 24 | logger.generated += 1 25 | best_solution, path = BnBEngine(problem, next_node, best_solution, path) 26 | else: 27 | logger.pruned += 1 28 | 29 | return (best_solution, path) 30 | 31 | def Branch_and_Bound(problem, best_solution=None): 32 | init_state = problem.get_init_state() 33 | init_node = SearchNode(init_state) 34 | init_node.set_g(0) 35 | init_node.set_d(0) 36 | 37 | if best_solution is None: 38 | best_solution = float('inf') 39 | 40 | logger.start() 41 | solution_cost, path = BnBEngine(problem, init_node, best_solution) 42 | 43 | logger.end() 44 | logger.print() 45 | return (solution_cost, path) 46 | 47 | 48 | if __name__ == "__main__": 49 | from tsp import Tsp 50 | 51 | cities = [ 52 | (0, 0), 53 | (1, 0), 54 | (0, 1), 55 | (1, 1) 56 | ] 57 | 58 | problem = Tsp(cities) 59 | solution_cost, path = Branch_and_Bound(problem) 60 | 61 | print('solution_cost=', solution_cost) 62 | for p in reversed(path): 63 | print(p) -------------------------------------------------------------------------------- /python/graph_search.py: -------------------------------------------------------------------------------- 1 | from search_node import SearchNode 2 | from util import SearchLogger 3 | 4 | def is_explored(node, closed_list): 5 | for n in closed_list: 6 | if (n.state == node.state) and (n.g <= node.g): 7 | return True 8 | return False 9 | 10 | def GraphSearch(problem, priority_f=None): 11 | open = [] 12 | closed = [] 13 | 14 | init_state = problem.get_init_state() 15 | init_node = SearchNode(init_state) 16 | init_node.set_g(0) 17 | init_node.set_d(0) 18 | 19 | logger = SearchLogger() 20 | logger.start() 21 | 22 | open.append(init_node) 23 | closed.append(init_node) 24 | 25 | while (len(open) > 0): 26 | open.sort(key=lambda node: priority_f(node), reverse=True) 27 | 28 | node = open.pop() 29 | logger.expanded += 1 30 | 31 | if problem.is_goal(node.state): 32 | logger.end() 33 | logger.print() 34 | return node.get_path() 35 | else: 36 | actions = problem.get_available_actions(node.state) 37 | 38 | for a in actions: 39 | next_state = problem.get_next_state(node.state, a) 40 | 41 | next_node = SearchNode(next_state) 42 | next_node.set_g(node.g + problem.get_action_cost(node.state, a)) 43 | next_node.set_d(node.d + 1) 44 | if not is_explored(next_node, closed): 45 | next_node.set_prev_n(node) 46 | open.append(next_node) 47 | closed.append(next_node) 48 | logger.generated += 1 49 | else: 50 | logger.pruned += 1 51 | logger.end() 52 | logger.print() 53 | return None 54 | 55 | 56 | if __name__ == "__main__": 57 | from grid_pathfinding import GridPathfinding 58 | 59 | problem = GridPathfinding() 60 | priority_f = lambda node: node.g 61 | path = GraphSearch(problem, priority_f) 62 | 63 | print(problem.init_state.x, problem.init_state.y) 64 | for s in reversed(path): 65 | print(s) -------------------------------------------------------------------------------- /python/depth_first_iterative_deepening.py: -------------------------------------------------------------------------------- 1 | from search_node import SearchNode 2 | from util import SearchLogger 3 | 4 | logger = SearchLogger() 5 | 6 | def CLDFS_DFID(problem, cur_node, max_priorty, priority_f): 7 | logger.expanded += 1 8 | if problem.is_goal(cur_node.state): 9 | return [cur_node] 10 | else: 11 | actions = problem.get_available_actions(cur_node.state) 12 | 13 | for a in actions: 14 | next_state = problem.get_next_state(cur_node.state, a) 15 | next_node = SearchNode(next_state) 16 | next_node.set_g(cur_node.g + problem.get_action_cost(cur_node.state, a)) 17 | next_node.set_d(cur_node.d + 1) 18 | next_node.set_prev_n(cur_node) 19 | 20 | if priority_f(next_node) <= max_priorty: 21 | logger.generated += 1 22 | path = CLDFS_DFID(problem, next_node, max_priorty, priority_f) 23 | if len(path) > 0: 24 | path.append(cur_node) 25 | return path 26 | else: 27 | logger.pruned += 1 28 | return [] 29 | 30 | def IterativeDeepening(problem, priority_f): 31 | logger.start() 32 | max_priorty = 1 33 | path = [] 34 | 35 | while len(path) == 0: 36 | init_state = problem.get_init_state() 37 | init_node = SearchNode(init_state) 38 | init_node.set_g(0) 39 | init_node.set_d(0) 40 | init_node.set_prev_n = 0 41 | 42 | path = CLDFS_DFID(problem, init_node, max_priorty, priority_f) 43 | 44 | max_priorty += 1 45 | 46 | logger.end() 47 | logger.print() 48 | return path 49 | 50 | 51 | def DepthFirstIterativeDeepening(problem): 52 | return IterativeDeepening(problem, lambda node: node.g) 53 | 54 | if __name__ == "__main__": 55 | from tsp import Tsp 56 | 57 | cities = [ 58 | (0, 0), 59 | (1, 0), 60 | (0, 1), 61 | (1, 1) 62 | ] 63 | 64 | problem = Tsp(cities) 65 | path = DepthFirstIterativeDeepening(problem) 66 | 67 | for p in reversed(path): 68 | print(p) -------------------------------------------------------------------------------- /python/tsp.py: -------------------------------------------------------------------------------- 1 | import math 2 | from state_space_problem import StateSpaceProblem 3 | 4 | class TspState: 5 | def __init__(self, visited, cur_city): 6 | self.visited = visited 7 | self.cur_city = cur_city 8 | 9 | def __eq__(self, other): 10 | return (self.visited == other.visited) and (self.cur_city == other.cur_city) 11 | 12 | def __str__(self): 13 | return self.visited.__str__() + " " + self.cur_city.__str__() 14 | 15 | class Tsp(StateSpaceProblem): 16 | def __init__(self, cities): 17 | self.cities = cities 18 | self.distances = [[self.compute_distance(self.cities[i], self.cities[j]) for i in range(len(cities))] for j in range(len(cities))] 19 | 20 | def get_init_state(self): 21 | return TspState([False for i in range(len(self.cities))], 0) 22 | 23 | def is_goal(self, state): 24 | return all(state.visited) 25 | 26 | def get_available_actions(self, state): 27 | actions = [] 28 | for city_id in range(len(self.cities)): 29 | if not state.visited[city_id]: 30 | actions.append(city_id) 31 | 32 | return actions 33 | 34 | def get_next_state(self, state, action): 35 | next_state_visited = state.visited.copy() 36 | next_state_visited[action] = True 37 | next_state_cur_city = action 38 | return TspState(next_state_visited, next_state_cur_city) 39 | 40 | def get_action_cost(self, state, action): 41 | return self.distances[state.cur_city][action] 42 | 43 | def heuristic(self, state): 44 | # Minimum spanning tree? 45 | # TODO: Implement this 46 | return 1 47 | 48 | def compute_distance(self, city1, city2): 49 | return math.sqrt((city1[0] - city2[0])**2 + (city1[1] - city2[1])**2) 50 | 51 | 52 | 53 | if __name__ == "__main__": 54 | from tree_search import TreeSearch 55 | 56 | cities = [ 57 | (0, 0), 58 | (1, 0), 59 | (0, 1), 60 | (1, 1) 61 | ] 62 | 63 | problem = Tsp(cities) 64 | priority_f = lambda node: node.g 65 | path = TreeSearch(problem, priority_f) 66 | 67 | for p in reversed(path): 68 | print(p) -------------------------------------------------------------------------------- /python/grid_pathfinding.py: -------------------------------------------------------------------------------- 1 | from state_space_problem import StateSpaceProblem 2 | 3 | class GridState: 4 | def __init__(self, xy): 5 | self.x = xy[0] 6 | self.y = xy[1] 7 | 8 | def __hash__(self): 9 | return hash((self.x, self.y)) 10 | 11 | def __eq__(self, other): 12 | return (self.x == other.x) and (self.y == other.y) 13 | 14 | def __str__(self): 15 | return "(" + self.x.__str__() + ", " + self.y.__str__() + ")" 16 | 17 | class GridPathfinding(StateSpaceProblem): 18 | state_type = GridState 19 | 20 | def __init__(self, 21 | width=5, 22 | height=5, 23 | init_position=(0, 0), 24 | goal_position=(4, 4)): 25 | 26 | self.width = width 27 | self.height = height 28 | self.init_position = init_position 29 | self.goal_position = goal_position 30 | 31 | self.init_state = GridState(self.init_position) 32 | 33 | def get_init_state(self): 34 | return self.init_state 35 | 36 | def is_goal(self, state): 37 | return (state.x == self.goal_position[0]) and (state.y == self.goal_position[1]) 38 | 39 | def get_available_actions(self, state): 40 | actions = [] 41 | if state.x > 0: 42 | actions.append('l') 43 | if state.x < self.width - 1: 44 | actions.append('r') 45 | if state.y > 0: 46 | actions.append('u') 47 | if state.y < self.height - 1: 48 | actions.append('d') 49 | return actions 50 | 51 | def get_next_state(self, state, action): 52 | if action == 'l': 53 | return GridState((state.x - 1, state.y)) 54 | elif action == 'r': 55 | return GridState((state.x + 1, state.y)) 56 | elif action == 'u': 57 | return GridState((state.x, state.y - 1)) 58 | elif action == 'd': 59 | return GridState((state.x, state.y + 1)) 60 | else: 61 | raise Exception("Invalid action: " + action) 62 | 63 | def get_action_cost(self, state, action): 64 | return 1 65 | 66 | def heuristic(self, state): 67 | # Manhattan distance heuristic 68 | return abs(state.x - self.goal_position[0]) + abs(state.y - self.goal_position[1]) -------------------------------------------------------------------------------- /python/optimized_graph_search.py: -------------------------------------------------------------------------------- 1 | from search_node import SearchNode 2 | from util import SearchLogger 3 | 4 | def OptimizedGraphSearch(problem, priority_f, open_list, closed_list): 5 | 6 | init_state = problem.get_init_state() 7 | 8 | init_node = SearchNode(init_state) 9 | init_node.set_g(0) 10 | init_node.set_d(0) 11 | 12 | logger = SearchLogger() 13 | logger.start() 14 | 15 | open_list.push(init_node, priority_f(init_node)) 16 | closed_list.push(init_node) 17 | 18 | while (len(open_list) > 0): 19 | node = open_list.pop() 20 | logger.expanded += 1 21 | 22 | if problem.is_goal(node.state): 23 | logger.end() 24 | logger.print() 25 | return node.get_path() 26 | else: 27 | actions = problem.get_available_actions(node.state) 28 | 29 | for a in actions: 30 | next_state = problem.get_next_state(node.state, a) 31 | 32 | next_node = SearchNode(next_state) 33 | next_node.set_g(node.g + problem.get_action_cost(node.state, a)) 34 | next_node.set_d(node.d + 1) 35 | 36 | if not closed_list.is_explored(next_node): 37 | next_node.set_prev_n(node) 38 | open_list.push(next_node, priority_f(next_node)) 39 | closed_list.push(next_node) 40 | logger.generated += 1 41 | else: 42 | logger.pruned += 1 43 | 44 | logger.end() 45 | logger.print() 46 | return None 47 | 48 | 49 | if __name__ == "__main__": 50 | from grid_pathfinding import GridPathfinding 51 | from sliding_tile import SlidingTile 52 | from bucket_openlist import BucketOpenList 53 | from hash_closedlist import HashClosedList 54 | 55 | # problem = GridPathfinding(100, 100, goal_position=(99, 99)) 56 | # priority_f = lambda node: node.g + problem.heuristic(node.state) 57 | 58 | # init_h_value = problem.heuristic(problem.get_init_state()) 59 | # open_list = BucketOpenList(C_min=init_h_value, C_max=init_h_value*8) 60 | # closed_list = HashClosedList() 61 | # path = OptimizedGraphSearch(problem, priority_f, open_list, closed_list) 62 | 63 | # for s in reversed(path): 64 | # print(s) 65 | 66 | tiles = SlidingTile(3, 3, init_position=[7, 4, 5, 1, 8, 3, 2, 0, 6]) 67 | priority_f = lambda node: node.g + tiles.heuristic(node.state) 68 | 69 | init_h_value = tiles.heuristic(tiles.get_init_state()) 70 | open_list = BucketOpenList(C_min=init_h_value, C_max=init_h_value*8) 71 | closed_list = HashClosedList() 72 | path = OptimizedGraphSearch(tiles, priority_f, open_list, closed_list) 73 | 74 | for s in reversed(path): 75 | print(s) -------------------------------------------------------------------------------- /python/sliding_tile.py: -------------------------------------------------------------------------------- 1 | from state_space_problem import StateSpaceProblem 2 | 3 | class TileState: 4 | def __init__(self, tile): 5 | self.tile = tile 6 | 7 | def __eq__(self, other): 8 | return (self.tile == other.tile) 9 | 10 | def __hash__(self): 11 | return hash(tuple(self.tile)) 12 | 13 | def __str__(self): 14 | return self.tile.__str__() 15 | 16 | class SlidingTile(StateSpaceProblem): 17 | def __init__(self, 18 | width=3, 19 | height=3, 20 | init_position=[1, 0, 2, 3, 4, 5, 6, 7, 8]): 21 | 22 | self.width = width 23 | self.height = height 24 | self.init_position = init_position 25 | self.goal_position = [i for i in range(0, width*height)] 26 | 27 | assert(len(self.init_position) == self.width * self.height) 28 | 29 | # TODO: make state class? 30 | self.init_state = TileState(self.init_position) 31 | 32 | def get_init_state(self): 33 | return self.init_state 34 | 35 | def is_goal(self, state): 36 | return state.tile == self.goal_position 37 | 38 | def get_available_actions(self, state): 39 | blank_position = state.tile.index(0) 40 | 41 | b_pos_x = blank_position % self.width 42 | b_pos_y = blank_position // self.width 43 | 44 | actions = [] 45 | if b_pos_x > 0: 46 | actions.append('l') 47 | if b_pos_x < self.width - 1: 48 | actions.append('r') 49 | if b_pos_y > 0: 50 | actions.append('d') 51 | if b_pos_y < self.height - 1: 52 | actions.append('u') 53 | 54 | return actions 55 | 56 | def get_next_state(self, state, action): 57 | blank_position = state.tile.index(0) 58 | 59 | next_state_tile = state.tile.copy() 60 | if action == 'l': 61 | next_blank_position = blank_position - 1 62 | elif action == 'r': 63 | next_blank_position = blank_position + 1 64 | elif action == 'd': 65 | next_blank_position = blank_position - self.width 66 | elif action == 'u': 67 | next_blank_position = blank_position + self.width 68 | 69 | sliding_tile = state.tile[next_blank_position] 70 | next_state_tile[blank_position] = sliding_tile 71 | next_state_tile[next_blank_position] = 0 72 | 73 | return TileState(next_state_tile) 74 | 75 | def get_action_cost(self, state, action): 76 | return 1 77 | 78 | def heuristic(self, state): 79 | return self.manhattan_distance(state) 80 | 81 | def manhattan_distance(self, state): 82 | dist = 0 83 | for tile_id in range(1, len(state.tile)): 84 | position = state.tile.index(tile_id) 85 | 86 | pos_x = position % self.width 87 | pos_y = position // self.width 88 | 89 | goal_x = tile_id % self.width 90 | goal_y = tile_id // self.width 91 | 92 | dist += abs(goal_x - pos_x) + abs(goal_y - pos_y) 93 | return dist 94 | 95 | 96 | 97 | 98 | if __name__ == "__main__": 99 | from tree_search import TreeSearch 100 | 101 | problem = SlidingTile(3, 3, init_position=[1, 2, 0, 3, 4, 5, 6, 7, 8]) 102 | priority_f = lambda node: node.g 103 | path = TreeSearch(problem, priority_f) 104 | 105 | print(problem.get_init_state()) 106 | for p in reversed(path): 107 | print(p) 108 | 109 | -------------------------------------------------------------------------------- /working.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | 3 | \begin{comment} 4 | 5 | \chapter{探索問題の派生} 6 | \label{ch:search-problem-variants} 7 | 8 | この章では\ref{ch:state-space-problem}章で定式化した状態空間問題と少し定式化の異なる問題を扱う。 9 | ブラックボックスプランニングは状態空間問題の一つであるが、今まで扱った問題と異なり、ドメインモデルがブラックボックスで与えられるという違いがある(\ref{sec:black-box-planning}章)。よってヒューリスティック関数を生成することが出来ず、情報なし探索が必要となる。 10 | 11 | 12 | 13 | \section{ブラックボックスプランニング (Blackbox Planning)} 14 | \label{sec:black-box-planning} 15 | 16 | プランナーはPDDLを用いることでドメインの知識を吸い出し、それを利用して探索を効率化する。しかしながら、完全なモデルを得るのが難しい問題の場合、PDDLのような記述を得ることが出来ない。 17 | 例えばビデオゲームのような環境では、ゲームをクラックしない限り、完全なモデルを得ることは出来ない。 18 | このような中身を見ることの出来ない環境でのプランニング問題をブラックボックスプランニング問題と呼ぶ。 19 | 20 | ブラックボックスプランニングはAtari 2600や\cite{lipovetzky2015a}や、General Video Game Playing \cite{geffner2015}などのビデオゲームなどの環境に応用されている。 21 | 22 | ブラックボックスプランニング問題は状態空間問題である。状態$s$は有限長の配列$V$で表せられ、$v \in V$の値域は$D(v)$とする。ただし、$V$の各変数がどのような意味を持つのかは未知である。 23 | Expand関数、Goal関数はブラックボックスとして与えられる。また、ある状態に対して$A$のうち実行可能なアクションの集合が既知とは限らない\footnote{厳密にブラックボックスである場合は既知とするべきではないが、多くの研究ではオラクルによって実行可能なアクションが知らされるというモデルを用いている。}。 24 | 25 | このようなドメインではドメインの知識を得ることが出来ないので、\ref{ch:heuristic-search}章で解説したようなヒューリスティック関数を用いることは出来ない。 26 | 27 | 幅優先探索などによってBrute-forceに探索しつくす方法を取ることも出来るが問題のサイズが大きい場合に解くことが出来ない\cite{Bellemare2013}。 28 | Iterative Width探索 (IW search)\cite{lipovetzky2015a}は幅優先探索に新奇性による枝刈りを加えた手法である\footnote{Iterative Width探索はドメインモデルのある場合でも有用であることが知られている\cite{lipovetzkyg12}。}。IW(1)は新しく生成された状態は新しいatomを真にしない場合、枝刈りされる。 29 | 30 | \subsection{新奇性に基づく枝刈り} 31 | \label{sec:novelty-based-pruning} 32 | inadmissible pruning 33 | Novelty-based pruning 34 | Iterative Width search 35 | 36 | \subsection{ビデオゲームAI: Atari 2600} 37 | \label{sec:video-game} 38 | 39 | \section{パターンマイニング} 40 | 41 | 42 | \section{二人プレイヤーゲーム} 43 | \label{sec:two-player-game} 44 | and or tree 45 | Alpha beta pruning 46 | 47 | \subsection{αβ木} 48 | \label{sec:alpha-beta-tree} 49 | 50 | \subsection{Monte Carlo Tree Search} 51 | \label{sec:monte-carlo-tree-search} 52 | 53 | 54 | \section{オンラインプランニング} 55 | \label{sec:online-planning} 56 | 57 | 58 | 59 | % TODO: こういうチャプターがあるとキャッチーだね 60 | \chapter{機械学習と探索・プランニング (Machine Learning, Search, and Planning)} 61 | \label{ch:machine-learning} 62 | A review of ML for AP \cite{jimenez2012review} 63 | 64 | 65 | \section{機械学習による探索の効率化} 66 | \label{sec:ml-for-search} 67 | YJ DASP 68 | 69 | \section{ドメインモデルの生成} 70 | \label{sec:domain-acquisition} 71 | 72 | 本書を通して扱ってきた状態空間問題の大きな問題点は、問題モデルをどのように獲得するか、である。 73 | \ref{sec:coverage}章で述べたように、本書はこれまで正しいモデルが与えられていることが前提として話を進めてきた。 74 | 75 | %では、正しいモデルはどのようにして得るのか? 76 | 多くのアプリケーションではドメインモデル(e.g. PDDL)は人間のエキスパートが手でコーディングする。 77 | しかしながら、この方法だと人間のエキスパートがあらかじめ想定した環境にしか適用できない。ダイナミックな環境で活躍できるようなエージェントを実装するためには、エージェントが何らかの方法でドメインモデルを生成する方法が必要である。 78 | 79 | LOCMはプランからアクションスキームを生成する。 80 | LOCM staticなconstraintも見つける。 81 | 82 | 階層的プランニング 83 | オプション 84 | Kaelbling 85 | Konidaris et al. 86 | 87 | \section{探索と機械学習} 88 | \label{sec:search-and-ml} 89 | R. Sutton 90 | David Silver RL and simulation-based search 91 | 92 | \subsection{Alpha Go} 93 | \label{sec:alpha-go} 94 | 95 | \section{参考文献} 96 | Predictron 97 | DL for Reward design in MCTS 98 | Juhn, Satinder, et al. 99 | \end{comment} 100 | 101 | \begin{comment} 102 | \begin{appendices} 103 | \chapter{関連和書} 104 | 105 | 本書で扱った内容に関連する和書はいくつかある。 106 | 107 | まず、ヒューリスティック探索は人工知能の一分野であり、人工知能の一部を実装する手法として研究されている。人工知能の金字塔であるRussel\& Norvigによる教科書は和訳も存在する\cite{russell2016artificial}。人工知能研究について興味がある読者はこれをお勧めする。 108 | 109 | グラフ探索アルゴリズムの数学的背景は組合せ問題である。 110 | 組合せ問題についてはDonald KnuthによるThe Art of Computer Programming Volume 4A Combinatorial Algorithmsが網羅的である\cite{knuth2011art}。 111 | 112 | 113 | 114 | \end{appendices} 115 | \end{comment} 116 | 117 | -------------------------------------------------------------------------------- /main.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | \errorcontextlines=200 3 | \documentclass[11pt,dvipdfmx,b5paper]{book} 4 | 5 | \usepackage{times} 6 | \usepackage{courier} 7 | \usepackage{comment} 8 | \usepackage{subfig} 9 | \usepackage{graphicx} 10 | 11 | \usepackage{enumitem} 12 | \usepackage{url} 13 | \usepackage{listings} 14 | \usepackage{lstlang0} 15 | \usepackage{tikz} 16 | \usepackage{xcolor} 17 | 18 | %%%%%%%%%%%%%%%%%%%%% 19 | % Math 20 | \usepackage{amsmath} 21 | \usepackage{amsthm} 22 | \usepackage{amssymb} 23 | \usepackage{mathtools} 24 | \DeclarePairedDelimiter\ceil{\lceil}{\rceil} 25 | \DeclarePairedDelimiter\floor{\lfloor}{\rfloor} 26 | 27 | %%%%%%%%%%%%%%%%%%%%% 28 | % Format 29 | \usepackage[Bjornstrup]{fncychap} 30 | \usepackage{titlesec} 31 | \usepackage[framemethod=TikZ]{mdframed} 32 | \usepackage{booktabs} 33 | 34 | \setcounter{secnumdepth}{3} 35 | \renewcommand{\baselinestretch}{1.15} 36 | 37 | \usepackage{appendix} 38 | \usepackage[dvipdfmx]{hyperref} 39 | \usepackage{pxjahyper} 40 | \usepackage{makeidx} 41 | \makeindex 42 | 43 | %%%%%%%%%%%%%%%%%%%% 44 | % Python, bash 45 | \usepackage{pythonhighlight} 46 | \newcommand{\code}[1]{\texttt{#1}} 47 | 48 | 49 | %%%%%%%%%%%%%%%%%%%% 50 | % Algorithm 51 | \usepackage[boxruled,linesnumbered,noend]{algorithm2e} 52 | \SetKwInOut{Input}{Input} 53 | \SetKwInOut{Output}{Output} 54 | \SetKwInOut{Side}{Side effect} 55 | \SetKwComment{Comment}{$\triangleright$}{} 56 | 57 | \DeclareMathOperator*{\argmax}{arg\,max} 58 | \DeclareMathOperator*{\argmin}{arg\,min} 59 | 60 | 61 | \titleformat{\section} 62 | {\bfseries\Large}{\thesection}{1em}{}[\vspace{-1em}\rule{\textwidth}{0.5pt}] 63 | 64 | \newcounter{DefCounter} 65 | \setcounter{DefCounter}{1} 66 | \newcommand{\ddef}[2] 67 | { 68 | \begin{mdframed}[roundcorner=5pt, 69 | linecolor=red, 70 | linewidth=2pt, 71 | backgroundcolor=red!6] 72 | \vspace{1mm} 73 | {\bf 定義 \theDefCounter} (#1): {\it #2} 74 | \stepcounter{DefCounter} 75 | \end{mdframed} 76 | } 77 | 78 | \newcounter{TheoremCounter} 79 | \setcounter{TheoremCounter}{1} 80 | \newcommand{\dtheorem}[1] 81 | { 82 | \begin{mdframed}[roundcorner=5pt, 83 | linecolor=blue, 84 | linewidth=2pt, 85 | backgroundcolor=blue!6] 86 | {\bf 定理 \theTheoremCounter}: 87 | #1 88 | \stepcounter{TheoremCounter} 89 | \end{mdframed} 90 | } 91 | 92 | \newcommand{\dproof}[1] 93 | { 94 | \begin{mdframed}[topline=false, 95 | bottomline=false, 96 | rightline=false, 97 | linewidth=12pt, 98 | linecolor=blue!6] 99 | {\bf 証明}: 100 | #1 101 | \end{mdframed} 102 | } 103 | 104 | \newcommand{\pnt}[2] 105 | { 106 | \begin{mdframed}[roundcorner=1pt, backgroundcolor=white] 107 | \vspace{1mm} 108 | {\bf ポイント} (#1): {#2} 109 | \end{mdframed} 110 | } 111 | 112 | % TODO: Example? 113 | 114 | 115 | 116 | %%%%%%%%%%%%%%%%%%% 117 | % Macros 118 | \newcommand{\define}[3]{{\bf #1} (#2) \index{#2} \index{#3@#1}} 119 | \newcommand{\TODO}[1]{{\bf TODO:} #1} 120 | 121 | 122 | % Acronyms 123 | \newcommand{\ZHDA}{ZHDA*} 124 | 125 | 126 | % Environments 127 | \newenvironment{abst}[0] 128 | { 129 | \begin{quote} 130 | } 131 | { 132 | \end{quote} 133 | } 134 | 135 | 136 | %%%%%%%%%%%%%%%% 137 | % Style names 138 | %%%%%%%%%%%%%%%% 139 | \renewcommand{\contentsname}{目次} 140 | \renewcommand{\chaptername}{チャプター} 141 | \renewcommand{\figurename}{図} 142 | \renewcommand{\tablename}{表} 143 | \renewcommand{\algorithmcfname}{アルゴリズム} 144 | \renewcommand{\bibname}{参考文献} 145 | \renewcommand{\indexname}{索引} 146 | 147 | \title{ヒューリスティック探索入門} 148 | \author{陣内 佑} 149 | 150 | 151 | \begin{document} 152 | 153 | \maketitle 154 | 155 | \tableofcontents 156 | 157 | \input{message.tex} 158 | 159 | \input{intro.tex} 160 | 161 | \input{problem.tex} 162 | 163 | \input{blind.tex} 164 | 165 | \input{heuristic.tex} 166 | 167 | \input{data.tex} 168 | 169 | \input{heuristic2.tex} 170 | 171 | \input{planning.tex} 172 | 173 | \input{working.tex} 174 | 175 | \bibliographystyle{spmpsci} 176 | 177 | \bibliography{ref-jf17} 178 | 179 | \printindex 180 | 181 | \end{document} 182 | 183 | -------------------------------------------------------------------------------- /figures/external-brfs.tex: -------------------------------------------------------------------------------- 1 | \pgfmathsetmacro{\phi}{0.1} 2 | \pgfmathsetmacro{\sp}{4.5} 3 | 4 | 5 | %%%%%%%%%%%%%%%%%%%%%%%% 6 | % State 1 7 | %%%%%%%%%%%%%%%%%%%%%%%% 8 | 9 | \pgfmathsetmacro{\bs}{0} 10 | \pgfmathsetmacro{\bsy}{0} 11 | 12 | % Memory 13 | \draw[rounded corners] (\bs+0,0) rectangle (\bs+4, 8); 14 | 15 | % External disk 16 | \draw[rounded corners] (\bs+\sp,0) rectangle (\bs+\sp+4, 8); 17 | 18 | \foreach \x in {0, 1, 2} { 19 | \draw[rounded corners] (\bs+\sp+\phi,\x*2+\phi) rectangle (\bs+\sp+4-\phi, \x*2+2-\phi); 20 | \node (\x) at (\bs+\sp+2,\x*2+1) {$Open(\x)$}; 21 | } 22 | 23 | \draw[rounded corners] (\bs+\phi,2*2+\phi) rectangle (\bs+4-\phi, 2*2+2-\phi); 24 | \node (mem2) at (\bs+2,2*2+1) {$Open(2)$}; 25 | 26 | \draw[rounded corners] (\bs+\phi,2*2+2+\phi) rectangle (\bs+4-\phi, 2*2+2+2-\phi); 27 | \node (a2) at (\bs+2,2*2+2+1) {$A(3)$}; 28 | \draw[->] (mem2) -- (a2); 29 | 30 | \node at ({\bs + (\sp + 4) / 2}, -1) {1. Expand $Open(i-1)$}; 31 | 32 | \node at ({\bs + 2}, \bsy+8.5) {Main Memory}; 33 | \node at ({\bs + \sp + 2}, \bsy+8.5) {External Memory}; 34 | 35 | %%%%%%%%%%%%%%%%%%%%%%%% 36 | % State 2 37 | %%%%%%%%%%%%%%%%%%%%%%%% 38 | 39 | \pgfmathsetmacro{\bs}{10} 40 | \pgfmathsetmacro{\bsy}{0} 41 | 42 | % Memory 43 | \draw[rounded corners] (\bs+0,0) rectangle (\bs+4, 8); 44 | 45 | % External disk 46 | \draw[rounded corners] (\bs+\sp,0) rectangle (\bs+\sp+4, 8); 47 | 48 | \foreach \x in {0, 1, 2} { 49 | \draw[rounded corners] (\bs+\sp+\phi,\x*2+\phi) rectangle (\bs+\sp+4-\phi, \x*2+2-\phi); 50 | \node (\x) at (\bs+\sp+2,\x*2+1) {$Open(\x)$}; 51 | } 52 | 53 | \draw[rounded corners] (\bs+\phi,2*2+\phi) rectangle (\bs+4-\phi, 2*2+2-\phi); 54 | \node (mem2) at (\bs+2,2*2+1) {$Open(2)$}; 55 | 56 | \draw[rounded corners] (\bs+\phi,2*2+2+\phi) rectangle (\bs+4-\phi, 2*2+2+2-\phi); 57 | \node[align=center,font=\footnotesize] (a2) at (\bs+2,2*2+2+1) {$A(3)$\\$\setminus Open(2)$}; 58 | \draw[->] (mem2) -- (a2); 59 | 60 | \node at ({\bs + (\sp + 4) / 2}, -1) {2. Duplicate Detection with $Open(i-1)$}; 61 | 62 | \node at ({\bs + 2}, \bsy+8.5) {Main Memory}; 63 | \node at ({\bs + \sp + 2}, \bsy+8.5) {External Memory}; 64 | 65 | 66 | %%%%%%%%%%%%%%%%%%%%%%%% 67 | % State 3 68 | %%%%%%%%%%%%%%%%%%%%%%%% 69 | 70 | \pgfmathsetmacro{\bs}{0} 71 | \pgfmathsetmacro{\bsy}{-11} 72 | 73 | % Memory 74 | \draw[rounded corners] (\bs+0,\bsy+0) rectangle (\bs+4, \bsy+8); 75 | 76 | % External disk 77 | \draw[rounded corners] (\bs+\sp,\bsy+0) rectangle (\bs+\sp+4, \bsy+8); 78 | 79 | \foreach \x in {0, 1, 2} { 80 | \draw[rounded corners] (\bs+\sp+\phi,\bsy+\x*2+\phi) rectangle (\bs+\sp+4-\phi, \bsy+\x*2+2-\phi); 81 | \node (\x) at (\bs+\sp+2,\bsy+\x*2+1) {$Open(\x)$}; 82 | } 83 | 84 | \draw[rounded corners] (\bs+\phi,\bsy+2*1+\phi) rectangle (\bs+4-\phi, \bsy+2*1+2-\phi); 85 | \node (mem1) at (\bs+2,\bsy+2*1+1) {$Open(1)$}; 86 | 87 | \draw[rounded corners] (\bs+\phi,\bsy+2*2+2+\phi) rectangle (\bs+4-\phi, \bsy+2*2+2+2-\phi); 88 | \node[align=center,font=\footnotesize] (a2) at (\bs+2,\bsy+2*2+2+1) {$A(3)$\\$\setminus Open(2)$\\$\setminus Open(1)$}; 89 | \draw[->] (mem1) -- (a2); 90 | 91 | \node at ({\bs + (\sp + 4) / 2}, \bsy-1) {3. Duplicate Detection with $Open(i-2)$}; 92 | 93 | \node at ({\bs + 2}, \bsy+8.5) {Main Memory}; 94 | \node at ({\bs + \sp + 2}, \bsy+8.5) {External Memory}; 95 | 96 | 97 | 98 | %%%%%%%%%%%%%%%%%%%%%%%% 99 | % State 4 100 | %%%%%%%%%%%%%%%%%%%%%%%% 101 | 102 | \pgfmathsetmacro{\bs}{10} 103 | \pgfmathsetmacro{\bsy}{-11} 104 | 105 | % Memory 106 | \draw[rounded corners] (\bs+0,\bsy+0) rectangle (\bs+4, \bsy+8); 107 | 108 | % External disk 109 | \draw[rounded corners] (\bs+\sp,\bsy+0) rectangle (\bs+\sp+4, \bsy+8); 110 | 111 | \foreach \x in {0, 1, 2, 3} { 112 | \draw[rounded corners] (\bs+\sp+\phi,\bsy+\x*2+\phi) rectangle (\bs+\sp+4-\phi, \bsy+\x*2+2-\phi); 113 | \node (\x) at (\bs+\sp+2,\bsy+\x*2+1) {$Open(\x)$}; 114 | } 115 | 116 | \draw[rounded corners] (\bs+\phi,\bsy+2*2+2+\phi) rectangle (\bs+4-\phi, \bsy+2*2+2+2-\phi); 117 | \node[align=center,font=\footnotesize] (a2) at (\bs+2,\bsy+2*2+2+1) {$A(3)$\\$\setminus Open(2)$\\$\setminus Open(1)$}; 118 | 119 | \draw[->] (a2) -- (3); 120 | 121 | \node at ({\bs + (\sp + 4) / 2}, \bsy-1) {4. Store $Open(i)$}; 122 | 123 | \node at ({\bs + 2}, \bsy+8.5) {Main Memory}; 124 | \node at ({\bs + \sp + 2}, \bsy+8.5) {External Memory}; 125 | -------------------------------------------------------------------------------- /pythonhighlight.sty: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesPackage{pythonhighlight}[2011/09/19 python code highlighting; provided by Olivier Verdier ] 3 | 4 | 5 | \RequirePackage{listings} 6 | \RequirePackage{xcolor} 7 | 8 | \renewcommand*{\lstlistlistingname}{Code Listings} 9 | \renewcommand*{\lstlistingname}{Code Listing} 10 | \definecolor{gray}{gray}{0.5} 11 | \colorlet{commentcolour}{green!50!black} 12 | 13 | \colorlet{stringcolour}{red!60!black} 14 | \colorlet{keywordcolour}{magenta!90!black} 15 | \colorlet{exceptioncolour}{yellow!50!red} 16 | \colorlet{commandcolour}{blue!60!black} 17 | \colorlet{numpycolour}{blue!60!green} 18 | \colorlet{literatecolour}{magenta!90!black} 19 | \colorlet{promptcolour}{green!50!black} 20 | \colorlet{specmethodcolour}{violet} 21 | 22 | \newcommand*{\framemargin}{3ex} 23 | 24 | \newcommand*{\literatecolour}{\textcolor{literatecolour}} 25 | 26 | \newcommand*{\pythonprompt}{\textcolor{promptcolour}{{>}{>}{>}}} 27 | 28 | \lstdefinestyle{mypython}{ 29 | %\lstset{ 30 | %keepspaces=true, 31 | language=python, 32 | showtabs=true, 33 | tab=, 34 | tabsize=2, 35 | basicstyle=\ttfamily\footnotesize,%\setstretch{.5}, 36 | stringstyle=\color{stringcolour}, 37 | showstringspaces=false, 38 | alsoletter={1234567890}, 39 | otherkeywords={\%, \}, \{, \&, \|}, 40 | keywordstyle=\color{keywordcolour}\bfseries, 41 | emph={and,break,class,continue,def,yield,del,elif ,else,% 42 | except,exec,finally,for,from,global,if,import,in,% 43 | lambda,not,or,pass,print,raise,return,try,while,assert,with}, 44 | emphstyle=\color{blue}\bfseries, 45 | emph={[2]True, False, None}, 46 | emphstyle=[2]\color{keywordcolour}, 47 | emph={[3]object,type,isinstance,copy,deepcopy,zip,enumerate,reversed,list,set,len,dict,tuple,xrange,append,execfile,real,imag,reduce,str,repr}, 48 | emphstyle=[3]\color{commandcolour}, 49 | emph={Exception,NameError,IndexError,SyntaxError,TypeError,ValueError,OverflowError,ZeroDivisionError}, 50 | emphstyle=\color{exceptioncolour}\bfseries, 51 | %upquote=true, 52 | morecomment=[s]{"""}{"""}, 53 | commentstyle=\color{commentcolour}\slshape, 54 | %emph={[4]1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 55 | emph={[4]ode, fsolve, sqrt, exp, sin, cos,arctan, arctan2, arccos, pi, array, norm, solve, dot, arange, isscalar, max, sum, flatten, shape, reshape, find, any, all, abs, plot, linspace, legend, quad, polyval,polyfit, hstack, concatenate,vstack,column_stack,empty,zeros,ones,rand,vander,grid,pcolor,eig,eigs,eigvals,svd,qr,tan,det,logspace,roll,min,mean,cumsum,cumprod,diff,vectorize,lstsq,cla,eye,xlabel,ylabel,squeeze}, 56 | emphstyle=[4]\color{numpycolour}, 57 | emph={[5]__init__,__add__,__mul__,__div__,__sub__,__call__,__getitem__,__setitem__,__eq__,__ne__,__nonzero__,__rmul__,__radd__,__repr__,__str__,__get__,__truediv__,__pow__,__name__,__future__,__all__}, 58 | emphstyle=[5]\color{specmethodcolour}, 59 | emph={[6]assert,yield}, 60 | emphstyle=[6]\color{keywordcolour}\bfseries, 61 | emph={[7]range}, 62 | emphstyle={[7]\color{keywordcolour}\bfseries}, 63 | % emph={[7]self}, 64 | % emphstyle=[7]\bfseries, 65 | literate=*% 66 | {:}{{\literatecolour:}}{1}% 67 | {=}{{\literatecolour=}}{1}% 68 | {-}{{\literatecolour-}}{1}% 69 | {+}{{\literatecolour+}}{1}% 70 | {*}{{\literatecolour*}}{1}% 71 | {**}{{\literatecolour{**}}}2% 72 | {/}{{\literatecolour/}}{1}% 73 | {//}{{\literatecolour{//}}}2% 74 | {!}{{\literatecolour!}}{1}% 75 | %{(}{{\literatecolour(}}{1}% 76 | %{)}{{\literatecolour)}}{1}% 77 | {[}{{\literatecolour[}}{1}% 78 | {]}{{\literatecolour]}}{1}% 79 | {<}{{\literatecolour<}}{1}% 80 | {>}{{\literatecolour>}}{1}% 81 | {>>>}{\pythonprompt}{3}% 82 | ,% 83 | %aboveskip=.5ex, 84 | frame=trbl, 85 | %frameround=tttt, 86 | %framesep=.3ex, 87 | rulecolor=\color{black!40}, 88 | %framexleftmargin=\framemargin, 89 | %framextopmargin=.1ex, 90 | %framexbottommargin=.1ex, 91 | %framexrightmargin=\framemargin, 92 | %framexleftmargin=1mm, framextopmargin=1mm, frame=shadowbox, rulesepcolor=\color{blue},#1 93 | %frame=tb, 94 | backgroundcolor=\color{white}, 95 | breakindent=.5\textwidth,frame=single,breaklines=true% 96 | %} 97 | } 98 | 99 | \newcommand*{\inputpython}[3]{\lstinputlisting[firstline=#2,lastline=#3,firstnumber=#2,frame=single,breakindent=.5\textwidth,frame=single,breaklines=true,style=mypython]{#1}} 100 | 101 | \lstnewenvironment{python}[1][]{\lstset{style=mypython}}{} 102 | 103 | \lstdefinestyle{mypythoninline}{ 104 | style=mypython,% 105 | basicstyle=\ttfamily,% 106 | keywordstyle=\color{keywordcolour},% 107 | emphstyle={[7]\color{keywordcolour}},% 108 | emphstyle=\color{exceptioncolour},% 109 | literate=*% 110 | {:}{{\literatecolour:}}{2}% 111 | {=}{{\literatecolour=}}{2}% 112 | {-}{{\literatecolour-}}{2}% 113 | {+}{{\literatecolour+}}{2}% 114 | {*}{{\literatecolour*}}2% 115 | {**}{{\literatecolour{**}}}3% 116 | {/}{{\literatecolour/}}{2}% 117 | {//}{{\literatecolour{//}}}{2}% 118 | {!}{{\literatecolour!}}{2}% 119 | %{(}{{\literatecolour(}}{2}% 120 | %{)}{{\literatecolour)}}{2}% 121 | {[}{{\literatecolour[}}{2}% 122 | {]}{{\literatecolour]}}{2}% 123 | {<}{{\literatecolour<}}{2}% 124 | {<=}{{\literatecolour{<=}}}3% 125 | {>}{{\literatecolour>}}{2}% 126 | {>=}{{\literatecolour{>=}}}3% 127 | {==}{{\literatecolour{==}}}3% 128 | {!=}{{\literatecolour{!=}}}3% 129 | {+=}{{\literatecolour{+=}}}3% 130 | {-=}{{\literatecolour{-=}}}3% 131 | {*=}{{\literatecolour{*=}}}3% 132 | {/=}{{\literatecolour{/=}}}3% 133 | %% emphstyle=\color{blue},% 134 | } 135 | 136 | \newcommand*{\pyth}{\lstinline[style=mypythoninline]} 137 | 138 | -------------------------------------------------------------------------------- /python/parallel_graph_search.py: -------------------------------------------------------------------------------- 1 | from threading import Thread, Lock 2 | from multiprocessing import Value, Array 3 | import queue 4 | 5 | from search_node import SearchNode 6 | from util import SearchLogger 7 | 8 | from bucket_openlist import BucketOpenList 9 | from hash_closedlist import HashClosedList 10 | 11 | def terminate_detection(has_job, terminating, confirm_terminating, n_threads, thread_id): 12 | if not has_job: 13 | terminating[thread_id] = True 14 | 15 | for i in range(n_threads): 16 | if not terminating[i]: 17 | return False 18 | confirm_terminating[thread_id] = True 19 | 20 | for i in range(n_threads): 21 | if not confirm_terminating[i]: 22 | return False 23 | return True 24 | else: 25 | terminating[thread_id] = False 26 | confirm_terminating[thread_id] = False 27 | return False 28 | 29 | def search_thread(problem, 30 | priority_f, 31 | open, 32 | closed, 33 | buffers, 34 | n_threads, 35 | thread_id, 36 | incumbent_cost, 37 | incumbent_goal_node_pos, 38 | incumbent_lock, 39 | terminating, 40 | confirm_terminating, 41 | logging): 42 | 43 | terminating[thread_id] = False 44 | confirm_terminating[thread_id] = False 45 | 46 | while (True): 47 | while not buffers[thread_id].empty(): 48 | recv_node = buffers[thread_id].get() 49 | f = priority_f(recv_node) 50 | if (not closed.is_explored(recv_node)) and (f < incumbent_cost.value): 51 | open.push(recv_node, f) 52 | closed.push(recv_node) 53 | logging.generated += 1 54 | else: 55 | logging.pruned += 1 56 | 57 | if terminate_detection(len(open) > 0, terminating, confirm_terminating, n_threads, thread_id): 58 | break 59 | 60 | if (len(open) == 0): 61 | continue 62 | 63 | node = open.pop() 64 | logging.expanded += 1 65 | 66 | if problem.is_goal(node.state) and (node.g < incumbent_cost.value): 67 | incumbent_lock.acquire() 68 | incumbent_cost.value = node.g 69 | idx = closed.find(node) 70 | incumbent_goal_node_pos[0] = thread_id 71 | incumbent_goal_node_pos[1] = idx[0] 72 | incumbent_goal_node_pos[2] = idx[1] 73 | print("incumbent solution updated: ", incumbent_cost.value) 74 | incumbent_lock.release() 75 | else: 76 | # Expand the node 77 | actions = problem.get_available_actions(node.state) 78 | 79 | for a in actions: 80 | next_state = problem.get_next_state(node.state, a) 81 | 82 | next_node = SearchNode(next_state) 83 | next_node.set_g(node.g + problem.get_action_cost(node.state, a)) 84 | next_node.set_d(node.d + 1) 85 | next_node.set_prev_n(node) 86 | 87 | f = priority_f(next_node) 88 | if (f < incumbent_cost.value): 89 | dst = hash(next_state) % n_threads 90 | buffers[dst].put(next_node) 91 | 92 | return 93 | 94 | def HashDistributedGraphSearch(problem, priority_f=None, n_threads=2): 95 | 96 | init_state = problem.get_init_state() 97 | 98 | init_node = SearchNode(init_state) 99 | init_node.set_g(0) 100 | init_node.set_d(0) 101 | init_node.set_prev_n = 0 102 | 103 | init_dst = hash(init_state) % n_threads 104 | 105 | init_h_value = priority_f(init_node) 106 | opens = [BucketOpenList(C_min=init_h_value, C_max=init_h_value*8)] * n_threads 107 | closeds = [HashClosedList()] * n_threads 108 | 109 | opens[init_dst].push(init_node, priority_f(init_node)) 110 | closeds[init_dst].push(init_node) 111 | 112 | # Data structure to keep the incumbent solution 113 | incumbent_lock = Lock() 114 | incumbent_cost = Value('f', 1000000000000000.0) 115 | incumbent_goal_node_pos = Array('i', [0, 0, 0]) 116 | 117 | # Data structure to keep the termination status 118 | terminating = Array('b', [False] * n_threads) 119 | confirm_terminating = Array('b', [False] * n_threads) 120 | 121 | # Buffer for asynchronous message passing 122 | waiting_buffers = [queue.Queue() for i in range(n_threads)] 123 | 124 | loggings = [SearchLogger() for i in range(n_threads)] 125 | threads = [] 126 | for i in range(n_threads): 127 | 128 | t = Thread(target=search_thread, 129 | args=(problem, 130 | priority_f, 131 | opens[i], 132 | closeds[i], 133 | waiting_buffers, 134 | n_threads, 135 | i, 136 | incumbent_cost, 137 | incumbent_goal_node_pos, 138 | incumbent_lock, 139 | terminating, 140 | confirm_terminating, 141 | loggings[i])) 142 | threads.append(t) 143 | 144 | global_logger = SearchLogger() 145 | 146 | global_logger.start() 147 | 148 | for i in range(n_threads): 149 | threads[i].start() 150 | 151 | for i in range(n_threads): 152 | threads[i].join() 153 | 154 | 155 | pos = incumbent_goal_node_pos.get_obj() 156 | incumbent_goal_node = closeds[pos[0]].get_by_index(pos[1], pos[2]) 157 | path = incumbent_goal_node.get_path() 158 | 159 | global_logger.end() 160 | global_logger.expanded = sum([l.expanded for l in loggings]) 161 | global_logger.generated = sum([l.generated for l in loggings]) 162 | global_logger.pruned = sum([l.pruned for l in loggings]) 163 | global_logger.print() 164 | 165 | return path 166 | 167 | if __name__ == "__main__": 168 | from grid_pathfinding import GridPathfinding 169 | from sliding_tile import SlidingTile 170 | 171 | grid = GridPathfinding() 172 | priority_f = lambda node: node.g + grid.heuristic(node.state) 173 | path = HashDistributedGraphSearch(grid, priority_f, n_threads=4) 174 | 175 | for s in reversed(path): 176 | print(s) 177 | 178 | 179 | tiles = SlidingTile(3, 3, init_position=[7, 4, 5, 1, 8, 3, 2, 0, 6]) 180 | priority_f_tiles = lambda node: node.g + tiles.heuristic(node.state) 181 | path = HashDistributedGraphSearch(tiles, priority_f_tiles, n_threads=4) 182 | 183 | for s in reversed(path): 184 | print(s) 185 | -------------------------------------------------------------------------------- /intro.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | \chapter{イントロダクション} 3 | \label{ch:introduction} 4 | 5 | 朝起きて、ごはんをよそい、味噌汁を作る。 6 | ご飯を食べて、最寄駅まで歩き、職場へ向かう電車に乗る。 7 | 8 | ごはんをよそうためには、しゃもじを右手にとり、茶碗を左手に持つ。 9 | 炊飯器を空けて、ごはんをかき混ぜる。 10 | かき混ぜたらごはんをしゃもじの上に乗せて、茶碗の上に持っていく。 11 | しゃもじを回転させると、ごはんは茶碗に落ちる。 12 | 13 | とても、とても難しいことをやっていると思わないだろうか? 14 | 不思議なことに、我々は「ごはんをよそう」と頭にあるだけなのに、そのために必要な行動を列挙し、一つずつ実行していけるのである。 15 | 16 | 我々が自覚的にはほとんど頭を使わずにこのような計画を立てることが出来るのはなぜだろうか?ごはんをよそうためにお湯を沸かしたり、最寄り駅まで歩いたりする必要はないと分かるのは何故だろうか? 17 | それは我々が無数の選択肢から{\bf 直感} (ヒューリスティック)的に必要そうな行動を絞り込めるからである。 18 | 19 | 本書で扱う{\bf ヒューリスティック探索}は、先に述べたような直感を駆使し、未来を先読みし、知的に行動を計画する能力をコンピュータに実装しようとする、人工知能研究の一分野である。 20 | 21 | %人は様々な問題を探索によって解決している。 22 | %例えば飛行機で成田からロンドンに行く安い/速い方法などを計画するのは探索の一つである。 23 | %一昔前は探索こそが人類の知であるという価値観が広くあり、囲碁、将棋、チェスなどのゲームはそれを競う競技であるとして。 24 | %あるいは囲碁、将棋、チェスなどのゲームも、ある手を選んだ時にどのような局面につながるのかを先読みし、選ぶべき次の一手を探索する。 25 | %このような様々な問題はグラフ探索問題として統合してモデルすることが出来る。 26 | %もちろん、それぞれの問題はそれぞれの特徴があり、それぞれで効率的な解法が異なる。 27 | 28 | %\captionlistentry[todo]{Introduction: なんかいい感じの絵} 29 | %{\TODO いい感じの絵} 30 | 31 | 32 | \section{何故人工知能に探索が必要なのか} 33 | \label{sec:why-search} 34 | 35 | % XXX: 「グラフ探索アルゴリズム」が突然出てくる 36 | グラフ探索アルゴリズムは、人工知能分野に限らず情報科学のいろいろな分野で使われる。 37 | 本書では、特に人工知能の要素技術としてのグラフ探索アルゴリズムを解説する。 38 | 39 | 人工知能とは何か、と考えることは本書の主眼ではない。 40 | 人工知能の教科書として有名なArtificial Intelligence: Modern Approach \cite{russelln03}では、人工知能研究の目標として以下の4つを掲げている。 41 | 42 | \begin{mdframed}[backgroundcolor=gray!10, roundcorner=10pt] 43 | \begin{enumerate} 44 | \item Think Rationally (合理的に考える) 45 | \item Think Humanly (人間的に考える) 46 | \item Act Rationally (合理的に行動する) 47 | \item Act Humanly (人間的に行動する) 48 | \end{enumerate} 49 | \end{mdframed} 50 | 51 | グラフ探索アルゴリズムは主にThink Rationallyを実現するための技術である\footnote{探索は4つ全てに強く関係しているが本書は主にThink Rationallyに注視する}。 52 | 探索による\define{先読み}{lookahead}{さきよみ}で、最も合理的な手を選ぶことがここでの目的である。 53 | 54 | 機械学習によるThink Rationallyとの違いは先読みをするという点である。 55 | 機械学習は過去の経験を元に合理的な行動を選ぶための技術である。 56 | それに対して、探索では未来の経験を先読みし、合理的な行動を選ぶ。 57 | 58 | 探索技術の大きな課題・欠点はモデルを必要とする点である。 59 | モデルがないと未来の経験を先読みできない。 60 | 例えば将棋で先を読むには、各コマの動き方や、敵の王を詰ますと勝ちであることを知っていなければならない。 61 | 加えて、ある局面でどちらがどのくらい有利なのかを評価できないと強いエージェントを作れない。 % エージェントはテクニカルターム 62 | モデルは完璧である必要はないが、ある程度は有用なものでないと先読みがうまく行かなくなる。 63 | 64 | %探索は特に学習データを得ることが難しい・不可能なシステムに用いられてきた。 65 | 以上のような理由で、探索はモデルが容易に得られる問題において使われてきた。 66 | 例えば経路探索問題では地図から距離を推定し、モデルを作ることが出来る。 67 | 今後のNASAなどによる宇宙開発でも探索技術が重要であり続けると考えられている。NASAのウェブページを見ると過去と現在の探索技術を用いたプロジェクトがたくさん紹介されている \footnote{\url{https://ti.arc.nasa.gov/tech/asr/planning-and-scheduling/}}。 68 | 69 | より賢い行動をするために、探索と機械学習を組み合わせようとする研究もある。 70 | モンテカルロ木探索とディープラーニングを組み合わせてプロ棋士に勝利したAlphaGoなどは、探索と機械学習の組み合わせの強力さを体現している \cite{silver2016mastering}。ここではディープラーニングを使って局面の評価値を学習し、それと探索を組み合わせて評価値が良い局面に繋がる手を選んでいる。 71 | 機械学習によってモデルを学習し、それを使って探索をするアプローチもある。先に述べたように探索にはモデルが必要である。例えばAtariでディープラーニングを使って探索のためのモデルを学習する研究がある \cite{oh2015action,silver2016predictron,chiappa2017recurrent}。 72 | 73 | このように、探索アルゴリズムは人工知能技術を理解する上で欠かせない分野の一つである。 74 | 特に最近大きなブレイクスルーのあった機械学習・深層学習とも強いシナジーを持っているため、これから大きな進展があると期待される分野の一つである。 75 | 76 | %Think RationallyとAct Rationallyの間には大きなギャップが存在する。 77 | 78 | \begin{comment} 79 | \section{アルゴリズムの概略図} 80 | \label{sec:cheatsheet} 81 | 82 | ここでは大まかに問題の特徴に対してどのようなアルゴリズムを最初に試すべきかの概略を示す。 83 | ここで紹介する手法が常に優れているわけではない。が、最初に考慮する価値はあるだろう。 84 | つまり、ここではその問題に当てはまらないとされた手法だからといって有用ではないということはない。 85 | 各章を読んでみて、適用可能かを読んでみたい。 86 | 87 | \begin{enumerate} 88 | \item 同じ状態に至る経路がたくさんある 89 | \begin{enumerate} 90 | \item YES: グラフ探索アルゴリズム 91 | \item NO: 木探索アルゴリズム (e.g. IDA*) 92 | \item SOSO: IDA* with transposition table 93 | \end{enumerate} 94 | 95 | \item 問題の特徴がある程度分かっておりヒューリスティック関数が作れる 96 | \begin{enumerate} 97 | \item YES: ヒューリスティック探索 98 | \item NO: ブラインド探索 99 | \end{enumerate} 100 | 101 | \item 最適解ではなくある程度良い解なら十分である 102 | \begin{enumerate} 103 | \item YES: 局所探索、weighted A* 104 | \item NO: A* 105 | \end{enumerate} 106 | 107 | \item 最適解と比べてどのくらい良い解かの保証が必要である 108 | \begin{enumerate} 109 | \item YES: weighted A* 110 | \item NO: Greedy best first search 111 | \end{enumerate} 112 | 113 | \item 実行時間が足りない 114 | \begin{enumerate} 115 | \item YES: 局所探索、IDA*、weighted A*、並列探索 116 | \end{enumerate} 117 | 118 | \item メモリが足りない 119 | \begin{enumerate} 120 | \item YES: 深さ優先探索、IDA*、並列探索、外部メモリ探索、 121 | \end{enumerate} 122 | 123 | \item 状態空間に似たような状態がたくさんある 124 | \begin{enumerate} 125 | \item YES: Novelty-based Pruning 126 | \end{enumerate} 127 | 128 | \item 最適解のコストの上界が求められる 129 | \begin{enumerate} 130 | \item YES: branch-and-bound 131 | \end{enumerate} 132 | 133 | \item コスト(ノード間距離)は連続値である 134 | \begin{enumerate} 135 | \item YES: 二分木によるプライオリティキュー 136 | \item NO: (離散値) nested bucketによるプライオリティキュー 137 | \end{enumerate} 138 | 139 | \item 分枝数が大きい 140 | \begin{enumerate} 141 | \item YES: 遅延重複検出 142 | \end{enumerate} 143 | 144 | \item いろいろな種類の問題を解かなければならない 145 | \begin{enumerate} 146 | \item YES: PDDL、自動行動計画問題 147 | \end{enumerate} 148 | 149 | %解を一つではなく全列挙したい 150 | %YES: Symbolic Search? 151 | 152 | %あるアクションの後に実行すると有益なアクションが分かっている 153 | %YES: マクロアクション 154 | \end{enumerate} 155 | \end{comment} 156 | 157 | 158 | \section{本書で扱う内容} 159 | \label{sec:coverage} 160 | 161 | % XXX: 状態空間問題って一般的な訳語? 162 | 本書で主に扱う問題は\define{状態空間問題}{state-space problem}{じょうたいくうかんもんだい}である。 163 | グラフ探索アルゴリズムは様々な場面で使われるが、この本では特に状態空間問題への応用に注目する。 164 | 状態空間問題はゴールに到達するための行動の列、\define{プラン}{plan}{プラン}を発見する問題である。 165 | 166 | 本書では特に、状態空間問題の中でも 167 | \define{完全情報}{perfect information}{かんぜんじょうほう}かつ\define{決定論的}{deterministic}{けっていろんてき}モデルを取り扱う(\ref{ch:state-space-problem}~\ref{ch:heuristic-search-variants}章 168 | )。 169 | 170 | 現実世界をそっくりそのままプログラム上で扱うのは困難である。 171 | 現実の問題を扱いやすい形式で{\bf モデル化}し、その問題を解くことで現実の問題を解決するのがエンジニアリングである。 172 | 173 | 世界をどうモデルするべきか判断するのは非常に難しい。 174 | モデルが単純であるほどモデル上の問題は解き易くなるが、単純だが正しいモデルをデザインする・自動生成することは難しい。 175 | その他にも、モデルの理解しやすさや汎用性の観点からモデルの良し悪しは決まる。 176 | 177 | 完全情報モデルとは、エージェントが世界の状態を全て観察できるモデルである。これは神の目線に立った意思決定のモデルである。 178 | これに対して\define{不完全情報}{partial information}{ふかんぜんじょうほう}モデルでは、エージェントは世界の状態の一部だけを\define{観察}{observation}{かんさつ}することで知ることが出来る。 179 | 実世界で動くロボットなどを考えると、不完全情報モデルの方が現実に沿っているが、多くの問題を完全情報モデルで表現できる。 180 | 181 | 決定論的なモデルではエージェントの行動による世界の状態遷移が一意に(決定論的)に定まる。 182 | 一方、非決定論的モデルでは、同じ状態から同じアクションを取ったとしても、世界がどう変化するかは一意には定まらない。 183 | 非決定論的モデルにおける探索問題は本書では扱わない。興味があれば強化学習の教科書を読むと良い\cite{sutton1998introduction}。 184 | 185 | %モデルを自動生成する方法については\ref{sec:domain-acquisition}章でも簡単に触れるが、本書の主眼ではない。 186 | 187 | 本書が扱う完全情報決定論的モデルは、上に挙げた中でもシンプルなモデルである。 188 | 本書ではこのモデルを対象にしたグラフ探索アルゴリズムを解説する。 189 | %より複雑なモデルに対してはより複雑なアルゴリズムを考える必要がある 190 | %これを不完全情報、非決定論的モデルとすることでより元の問題も表現しやすくなることがあるが、一方でモデルのシンプルさを失うことになる。 191 | 192 | % TODO: 関連書籍 193 | \subsection{関連書籍} 194 | 195 | ヒューリスティック探索に関連した書籍をいくつか紹介する。 196 | Judea PearlのHeuristic \cite{pearl84}は1984年に出版されたこの分野の古典的名著であり、長く教科書として使われている本である。A*探索の基本的な性質の解析が丁寧に書かれているのでとても読みやすい。また、二人ゲームのための探索に多くの紙面を割いている。 197 | Stefan Edelkamp and Stefan SchrodlのHeuristic Search Theory and Application \cite{edelkamp:2010:hst:1875144}はヒューリスティック探索について辞書的に調べられる本である。2010年出版なのでPearlよりも新しい内容が書かれている。 198 | Stuart Russell and Peter NorvigのArtificial Intelligence \cite{russelln03}は人工知能の定番の教科書である。人工知能に興味がある方はこの本を読むべきである。この本は探索・プランニングだけでなく制約充足問題、機械学習、自然言語処理、画像処理など人工知能のさまざまなテーマを扱っている。 199 | Malik Ghallab, Dana Nau, and Paolo TraversoのAutomated Planning and Acting \cite{ghallab:04}はヒューリスティック探索ではなくプランニングの本である。探索は主にThink Rationallyのための技術だが、探索をロボット制御等に応用するためには考えるだけでなく実際に行動をしなければならない(Act Rationally)。この本では探索をロボットの意思決定に使うための技術的な課題とその解決方法を紹介している。 200 | 201 | 202 | % TODO: グラフの用語を定義する? 203 | 204 | \begin{table} 205 | \centering 206 | \begin{tabular}{c | c} 207 | ユニットコスト状態空間問題 & $P_{u} = (S, A, s_0, T)$ \\ 208 | 状態空間問題 & $P = (S, A, s_0, T, w)$ \\ 209 | 状態空間グラフ & $G_{u} = (V, E, u_0, T)$ \\ 210 | 重み付き状態空間グラフ & $G = (V, E, u_0, T, w)$ \\ 211 | 非明示的状態空間グラフ & $G_{i} = (\mathcal{E}, u_0, \mathcal{G}, w)$ \\ 212 | 状態 & $s, s'$ \\ 213 | 初期状態 & $s_0$ \\ 214 | 状態集合 & $S$ \\ 215 | アクション (行動) & $a$ \\ 216 | アクション (行動)集合 & $A$ \\ 217 | ゴール集合 & $T$ \\ 218 | 問題グラフ & $G$ \\ 219 | ノード & $u, v, n$ \\ 220 | 初期ノード & $u_0$ \\ 221 | ノード集合 & $V$ \\ 222 | エッジ & $e$ \\ 223 | エッジ集合 & $E$ \\ 224 | 解 & $\pi$ \\ 225 | コスト関数 & $w$ \\ 226 | 実数集合 & $\mathbb{R}$ \\ 227 | ブーリアン集合 & $\mathbb{B}$ \\ 228 | 分枝度 & $b$ \\ 229 | 深さ & $d$ \\ 230 | 最小経路コスト関数 & $g$ \\ 231 | ヒューリスティック関数 & $h$ \\ 232 | プライオリティ関数 & $f$ \\ 233 | 最適解のコスト & $c^*$ \\ 234 | オープンリスト & $Open$ \\ 235 | クローズドリスト & $Closed$ \\ 236 | 状態変数 & $x$ \\ 237 | 命題変数の集合 & $AP$ \\ 238 | 適用条件 & pre \\ 239 | 追加効果 & add \\ 240 | 削除効果 & del \\ 241 | % Expand & Expand \\ 242 | % Goal & Goal \\ 243 | % parent & parent \\ 244 | \end{tabular} 245 | \caption{表記表} 246 | \label{notation} 247 | \end{table} 248 | 249 | \section{Python実装について} 250 | 251 | 本書では読者の理解の助けとなるべくPython 3のコードを掲載している。 252 | 疑似コードと合わせて読むことでアルゴリズムレベルでの処理と実装レベルでの処理の対応関係を理解できるだろう。 253 | 254 | 言語としてPythonを選んだ理由は読みやすく、人工知能技術に興味を持つ人にとってよく使われる言語であるためである。 255 | コードの目的は理解のためであり、高速な実装やソフトウェア工学的に優れた実装を目的としていない。 256 | 257 | 一方、探索アルゴリズムはC, C++のような高速な言語による恩恵を強く受けるアルゴリズムである。 258 | もし高速な実装が必要な場合はC, C++で実装することをお勧めする。 259 | とにかく高速な実装をしたい読者はアルゴリズムレベルの理解だけでなく、実装・コードレベルの理解も必要である。 260 | そのような読者はBurns et al. 2012 \cite{burns2012implementing}を参照されたい。 261 | -------------------------------------------------------------------------------- /data.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | \chapter{グラフ探索のためのデータ構造} 3 | \label{ch:search-performance} 4 | 5 | ヒューリスティック探索の効率は探索効率、つまり展開したノードの数によって測られる場合が多い。本書の多くの節は探索効率を上げるためのアルゴリズムについて解説している。 6 | しかしヒューリスティック探索の実行時間とメモリ量はデータ構造の実装にも大きく左右される。 7 | 8 | 9 | 探索にかかる実行時間をシンプルに見積もるとすると、(展開したノードの数)と(1展開あたりにかかる時間の平均値)の積である。 10 | 1展開あたりにかかる時間とは、大きく分けて以下の4つの部分に分けられる。 11 | 12 | \begin{enumerate} 13 | \item オープンリストからノードを取り出す時間 14 | \item 次状態の生成にかかる時間 15 | \item ヒューリスティック関数の計算にかかる時間 16 | \item 重複検出を行う時間 17 | \item オープンリスト・クローズドリストにノードを入れる時間 18 | \end{enumerate} 19 | 20 | ほとんどの問題では次状態の生成とヒューリスティック関数の計算にかかる時間はノードの数によらず定数である。 21 | 一方、オープンリスト・クローズドリストへのアクセスにかかる時間はノードの数が大きくなるほど増加する。 22 | 例えばシンプルなヒープでオープンリストを実装した場合、ノードを入れるためにかかる時間は$O(log (n))$である ($n$はノード数)。 23 | 実際、グリッド経路探索問題やスライディングタイル問題などのシンプルな問題においてはデータ構造へのアクセスにかかる時間が探索のほとんどの時間を占めている。 24 | よって、探索を効率化するためにはデータ構造の選択が重要である。 25 | 26 | ヒューリスティック探索ではオープンリストとクローズドリストの2つのデータ構造を保持する。 27 | オープンリストに必要なインターフェイスは必要な操作はエンキュー (enqueue, push)とデキュー (dequeue, pop)である。 28 | クローズドリストに必要なインターフェイスは挿入 (insert)と重複検知のための検索 (find, lookup)である。 29 | 30 | これらのデータ構造をどのように実装するかは探索の効率に大きな影響を与える。 31 | 歴史的な経緯からオープン・クローズドリストと呼ばれているが、リストとして実装するのは非効率的である。 32 | 33 | これらを実装するための効率的なデータ構造はアルゴリズムと問題ドメインに依存する。 34 | この章ではどのようなシチュエーションでどのようなデータ構造を使われるかを説明する。 35 | この章は実践的に非常に重要な章である。 36 | 残念ながらヒューリスティック探索の研究論文のほとんどはこの章で扱われる内容について自明のものとして扱わないことが多い。あるいはこれらの内容を「コードの最適化」として論文中には明示しない。が、その実自明ではないので初学者の多くはここで苦労することになる。 37 | データ構造について議論を行っている論文としては\cite{burns2012implementing}がある。 38 | 39 | %まず、$f$値の定義域が連続値か離散値かは重要である。 40 | %また、$f$値が単調増加するかどうか。これはヒューリスティックが無矛盾かどうかにも依存する。 41 | 42 | 43 | 44 | \section{オープンリスト (Open List)} 45 | \label{sec:open-list} 46 | %\captionlistentry[todo]{openlist} 47 | オープンリストのプライオリティキューの実装方法は様々ある。 48 | $f$値が連続値 (e.g. 実数)である場合はヒープを使うことが多い。 49 | $f$値の取りうる値が有限個であり、$f$値が同じノードが沢山ある場合はバケットで実装することが出来る。 50 | また、そのような場合は$f$値が同じノードのうちどのノードを選ぶかの\define{タイブレーキング}{tiebreaking}{タイブレーキング}も重要になってくる \cite{asai2016tiebreaking}。 51 | 52 | 53 | \subsection{データ構造の選択} 54 | \label{sec:priority-queue} 55 | 56 | オープンリストは探索の中で沢山エンキューとデキューを行うため、これらの操作の計算時間が速いものを選択したい。 57 | オープンリストのシンプルな実装方法としては二分ヒープがあげられる。二分ヒープはほとんどの言語の標準ライブラリにあるため実装が簡単である。 58 | 探索アルゴリズムはノードの数が膨大になることが多い。 59 | 計算量のオーダーで見ると単純な二分ヒープよりも\define{フィボナッチヒープ}{Fibonacci heap}{フィボナッチヒープ} \cite{fredman1987fibonacci}が効率が良いが、定数項が大きいため多くの場合二分ヒープが採用される。 60 | 二分ヒープよりも効率が良いデータ構造として\define{基数ヒープ}{radix heap}{きすうヒープ}がある \cite{ahuja1990faster}。基数ヒープはエンキューできる数値は最後に取り出した数値以上のものであるという制約がある。A*探索はヒューリスティックが単調である場合この制約を満たす。 61 | これらのヒープの実装や性質についてはIntroduction to Algorithmsを参照されたい \cite{cormen01}。 62 | 63 | プライオリティ値が取る値が有限個である場合はバケットプライオリティキュー \cite{dial1969algorithm} で実装をすることが出来る \cite{burns2012implementing}。プライオリティ値が取る値が$C_{min} \leq i \leq C_{max}$の整数値であるとする。 64 | バケットは長さ$C_{max} - C_{min} + 1$の配列からなる。プライオリティ値が$i$の要素は配列の$i - C_{min}$番目の位置にあるリストに追加される。バケットはエンキューもデキューも$O(C_{max} - C_{min})$で計算でき、保持しているノードの数に依存しない。そのため二分ヒープよりも高速であることが多い。 65 | 後述するタイブレーキングを行う場合はバケットの中に更に二段階目のバケットを用意することもできる \cite{burns2012implementing}。 66 | 67 | \begin{figure} 68 | \centering 69 | \begin{tikzpicture}[scale=0.6] 70 | \input{figures/bucket.tex} 71 | \end{tikzpicture} 72 | \caption{バケットによるオープンリストの実装 ($C_{min} = 6$, $C_{max} = 11$)} 73 | \label{fig:bucket} 74 | \end{figure} 75 | 76 | 77 | \subsection{タイブレーキング (Tiebreaking)} 78 | \label{sec:tiebreaking} 79 | 80 | \ref{ch:blind-search}章、\ref{ch:heuristic-search}章ではオープンリストでどのノードを最初に展開するかによってアルゴリズムの性能が大きく変わることを示してきた。 81 | 例えばA*探索では$f$値が最小のノードを優先して展開する (アルゴリズム\ref{alg:astar-search})。 82 | だが、$f$値が最小のノードは複数ある場合がある。コスト関数が離散値である場合は$f$値が同じノードが大量にあることが多い。 83 | このような場合、同じ$f$値のノードの中からどのノードを選ぶかを決定することを\define{タイブレーキング}{tie-breaking}{タイブレーキング}と呼ぶ。 84 | 85 | A*探索で広く使われるタイブレーキング戦略は$h$値が小さいノードを優先させる方法である。$\arg \min_{u' \in Open} f(u')$を選択する代わりに以下の条件を満たす$u$を選択する。 86 | 87 | \begin{align*} 88 | u =& \arg \min_{v \in B} h(v) \\ 89 | s.t. \; \; & B = \{v | v \in Open, f(v) = \min_{v' \in Open} f(v')\} 90 | \end{align*} 91 | 92 | $h$値が小さいノードを優先させる理由としては、$h$値がゴールへの距離の推定だからである。なのでゴールに近いノードから展開したほうがゴールにたどり着くのが早いはずだ、というヒューリスティックである。 93 | 94 | もう一つはLast-in-first-out (LIFO)タイブレーキングがよく使われる。 95 | LIFOは最後にオープンリストに入ったノードから優先して展開する。 96 | LIFOを使うメリットはオープンリストをバケット実装している場合に配列の一番後ろのノードがLIFOで得られるノードであることである。 97 | \ref{sec:priority-queue}節にあるようにバケットは配列で実装されることが多いが、配列の末尾のノードを取り出すには定数時間しかかからない。なので自然にバケットを実装するとLIFOになる。 98 | 99 | %逆にFirst-in-first-out (FIFO)にすると 100 | 101 | タイブレーキングは長い間ヒューリスティック探索研究の中であまり重要視されておらず、よいヒューリスティック関数をデザインすることと比較してアルゴリズムの効率に対してあまり影響を及ぼさないと考えられてきた。 102 | 近年の研究でタイブレーキングが重要なファクターになる場合があるということが実験的に示された \cite{asai2016tiebreaking}。 103 | 104 | 105 | \section{クローズドリスト (Closed List)} 106 | \label{sec:closed-list} 107 | 108 | 重複検出は無駄な展開を防ぐために不可欠な操作である。 109 | 非明示的状態空間グラフではあらかじめすべての状態を知ることが出来ないので、探索中に発見された状態の集合を保持しておくための動的な辞書が必要になる。 110 | クローズドリストは生成済み・展開済みノードを保持し、新しくノードが生成されたときにすでにその中にあるかどうかを確かめる。 111 | 一般的に辞書のデータ構造は挿入 (insert)、検索 (lookup)、削除 (delete)をサポートするが、探索アルゴリズムに必要なのは挿入と検索である。 112 | 113 | クローズドリストの実装には\define{ハッシュテーブル}{hash table}{ハッシュテーブル}が用いられることが多い。 114 | ハッシュテーブルは探索で一番使われる検索操作が上手くいけばほぼ定数時間で出来るため効率的であることが多い。 115 | 116 | 117 | \subsection{ハッシュテーブル (Hash Table)} 118 | \label{sec:hash-table} 119 | 120 | 集合$U = \{0, 1, ..., N - 1\}$をキーの候補とする\define{辞書}{dictionary}{じしょ}は、保持されたキーの集合$R \subseteq U$から連想情報の集合$I$への部分写像である。 121 | グラフ探索において状態$s \in S$を一意に表すためのキーを$k(s)$とする ($k: S \rightarrow U$)。 122 | キーは更にハッシュテーブルと呼ばれる配列$T[0, ..., m-1]$に写される。この写像$h: U \rightarrow \{0, ..., m-1\}$を\define{ハッシュ関数}{hash function}{ハッシュかんすう}と呼ぶ。簡単のためここでは状態から配列のインデックスまでの関数$S \rightarrow \{0, ..., m-1\}$を$h$を使って表す。 123 | 124 | 多くの場合$N > m$なので、このハッシュ関数は単射ではない。よって異なるキーがハッシュテーブル上の同じ位置に配置されることになる。これをハッシュの\define{衝突}{hash collision}{しょうとつ}と呼ぶ。 125 | ハッシュテーブルへのアクセスにかかる時間は1. ハッシュ値の計算、2. 衝突時の処理方法、3. 保持されたキーの数とハッシュテーブルの大きさの比に依存する。 126 | 127 | ハッシュ関数の選択はハッシュテーブルの性能に大きく影響を与える。最悪の場合、すべてのキーがテーブルの同じ位置に移される。最良の場合、一度も衝突が起こらず、アクセス時間は定数になる。最悪の場合の計算時間は理論的には興味のある話題だが、多くの場合正しくハッシュを計算することで避けられる。 128 | 129 | 衝突したハッシュの処理方法としては\define{連鎖法}{chaining}{れんさほう}と\define{開番地法}{open addressing}{かいばんちほう}がある。連鎖法はハッシュテーブルの各位置にリストが置かれ、その位置に挿入された連想情報はそのリストに順次追加されていく。同じ位置に$n$個の状態が挿入された場合リストの長さは$n$になる。このとき、ここの位置への検索の速度は$O(n)$となる。開番地法は衝突した場合テーブル中の空いている別の位置を探し、そちらに入れる方法である。探索アルゴリズムにおいては連鎖法が使われることが多い。 130 | 131 | 132 | \subsection{ハッシュ関数 (Hash Function)} 133 | \label{sec:hash-function} 134 | 135 | {\bf 良い}ハッシュ関数に求められる要件は主に2つである。 136 | 一つはハッシュ値が値域内でなるべく均等に分散してほしい。 137 | もう一つはハッシュの計算に時間がかからない方が良い。 138 | 139 | グラフ探索のためのハッシュ関数をデザインするために考えなければならないのは、探索中に現れる状態集合、探索空間は状態空間全体のほんの一部であり、かつ偏りのある集合であるということである。 140 | ハッシュ関数はその探索空間内でハッシュ値が十分に均等であってほしい。 141 | 142 | 143 | \subsubsection{剰余法 (Remainder Method)} 144 | 145 | $k(s)$を整数と考えることが出来るならば、それを$m$で割った余りをハッシュ値として使える。 146 | 147 | \begin{equation} 148 | h(s) := k(s) \; \text{mod} \; m 149 | \end{equation} 150 | 151 | このハッシュ関数は$S$から$T$への写像になる ($h: S \rightarrow \{0, ..., m-1\}$)。$T$へ均等に分配するためには法$m$の選択が重要になる。 152 | 例えば$m$に2の倍数を選んでしまうと、$h(s)$が偶数であるのは$k(s)$が偶数であるときであり、そのときのみである (iff)。$m$が任意の数の倍数であると同様の不均一が起きてしまうので、$m$は素数を選ぶと良い。 153 | 154 | 155 | \subsubsection{積算ハッシュ法 (Multiplicative Hashing)} 156 | 積算ハッシュ法はキー$k(s)$と無理数$\phi$の積の小数部分を使ったハッシュである。 157 | 158 | \begin{equation} 159 | h(s) = \floor{m (k(s) \phi - \floor{k(s) \phi})} 160 | \end{equation} 161 | 162 | $\phi$はパラメータであり、$(\sqrt{5} - 1) / 2$の黄金比を使うと良いとされている。 163 | 164 | \subsubsection{ハッシュの性質} 165 | 166 | 関連して、クローズドリストにとって良い性質のハッシュがいくつかある。 167 | 168 | グラフ探索では状態遷移は状態の一部分のみに作用し、状態変数のほとんどは変わらないということが多い。このとき、新しい状態のハッシュ値を一から計算するのではなく、元の状態のハッシュ値と状態遷移による差分を利用して新しい状態のハッシュ値を求めることが出来る場合がある。この方法を\define{差分ハッシュ}{incremental hashing}{さぶんハッシュ}と呼ぶ。差分ハッシュはハッシュ値の計算時間を短くできることがある。 169 | 170 | どのような操作があっても衝突が起こらないようなハッシュを\define{完全ハッシュ}{perfect hash function}{かんぜんハッシュ}と呼ぶ。もし$n = m$であれば最小完全ハッシュと呼ぶ。完全ハッシュがあればどのような操作も$O(1)$で行うことができる。 171 | 問題によって完全ハッシュは簡単に計算することが出来る。例えばスライディングタイルパズルであれば状態$s = (v_0, v_1,...,v_8)$に対して$h(s) = \sum_{i = 0..8} v_i \cdot 9^i$と単純に状態変数の列を使って計算することが出来る。ただし長さ$9^9$の配列は大きすぎるので工夫が必要になる。スライディングタイルパズルでは\define{辞書順}{lexicographical order}{じしょじゅん}によって最小完全ハッシュを作ることが出来る \cite{korf2005large}。しかしながら完全ハッシュを作るのは簡単であっても、小さく実用的な完全ハッシュを作るのは難しい問題も多い。 172 | 173 | 174 | \subsection{トランスポジションテーブル (Transposition Table)} 175 | 176 | 木探索の問題点は重複検出を行わないため、同じ状態を複数回展開し、時間がかかってしまうことである。一方グラフ探索の問題点は重複検出を行うため、生成したノードの数だけメモリを消費することにある。 177 | グラフが大きい場合はやがてメモリを使い果たし、探索が続けられなくなってしまう。 178 | しかしながら、実はグラフ探索において重複検出は必ずしも正しくなければならないものではない。重複してノードを生成しても問題なくアルゴリズムは動作する。仮にクローズドリストの一部を捨ててもアルゴリズムの正しさを損なうものではない。 179 | 180 | \define{トランスポジションテーブル}{transposition table}{トランスポジションテーブル}はこのアイディアに基づき、展開したノードの一部のみを保存することで重複検出をしつつメモリの消費を抑える手法である。 181 | トランスポジションテーブルはグラフ探索におけるクローズドリストと同様、辞書である。 182 | クローズドリストがすべての生成済みノードを保持していることを保証するのに対して、トランスポジションテーブルはそのような保証をしない。 183 | トランスポジションテーブルはどのノードを保持し、どのノードを捨てるかを賢く選択しなければならない。どのノードを保持するべきかという戦略はいくつか提案されている \cite{breuker1994replacement,akagi2010transposition}。 184 | 185 | 単純なものとしては、テーブルが一杯になるまではすべてのノードをテーブルに追加し、テーブルが一杯になったらすべてのノードを捨てる戦略がある。 186 | Stochastic node caching \cite{miura1999stochastic}は、新しく追加されたノードを確率$1-p$で捨てるというものである。この戦略だと$n$回訪れたノードは確率$1- (1-p)^n$でテーブルに追加されるため、たくさん訪れたノードほど重複検出されやすいことになるという点で合理的である。 187 | 2人ゲームでよく使われる戦略は、2つのノードのハッシュ値が衝突したらどちらか一方のみを残すという戦略である。より$d$値が大きい(深い)ノードを残す戦略が一般的である。この戦略はハッシュテーブルの衝突を減らせるという点で一挙両得になる。 188 | 189 | トランスポジションテーブルはA*だけでなく後述するIDA* (節\ref{sec:depth-first-iterative-deepening})でも使われる \cite{reinefeld1994enhanced}。IDA*の空間量は最適解の深さに対して線形なので消費メモリが非常に少ない。あまったメモリを効率よく使うためにIDA*にトランスポジションテーブルを用いるという研究が多い \cite{reinefeld1994enhanced,akagi2010transposition,kishimoto:02}。 190 | 191 | トランスポジションテーブルは元々2人ゲームの分野から発展した技術である。同じ手を異なる順番で行うことで同じ状態に到達することを検出するために使われていたのでtranspositionという名前になっている。 192 | 193 | 194 | \subsection{遅延重複検出 (Delayed Duplicate Detection)} 195 | \label{sec:delayed-duplicate-detection} 196 | 197 | \ref{sec:graph-search-algorithm}節ではノードを生成したタイミングで重複検出を行うアルゴリズムを説明した。この方法だとノードが生成されたその瞬間に検出を行うという意味で\define{即時重複検出}{immediate duplicate detection}{そくじちょうふくけんしゅつ}と呼ぶことがある。 198 | それに対して、ノードを展開するタイミングで検出を行う\define{遅延重複検出}{delayed duplicate detection}{ちえんちょうふくけんしゅつ}という方法もある \cite{korf2003delayed}。 199 | ノードが生成された瞬間に重複検出を行わない場合、オープンリスト内に同じ状態を持ったノードが重複して存在する場合がある。 200 | しかしノードを展開するときに重複検出を行うので、クローズドリストにノードの重複はない。 201 | アルゴリズム\ref{alg:ddd}は遅延重複検出を用いる場合のグラフ探索アルゴリズムの疑似コードである。 202 | 203 | 遅延重複検出は展開ノード毎に重複検出の回数を減らすことができるというメリットがある。一方、オープンリスト内に重複が存在する可能性があるため、プライオリティの選択次第ではノードの再展開が増えてしまうデメリットがある。 204 | そのため遅延重複検出はノードの重複が少ない問題や、重複検出に時間がかかる場合に使われる。特にクローズドリストをハードディスクなどの外部メモリに置く外部メモリ探索に相性が良い。 205 | 206 | 207 | \begin{algorithm} 208 | \caption{遅延重複検出を用いたグラフ探索} 209 | \label{alg:ddd} 210 | \Input{非明示的状態空間グラフ $(\mathcal{E}, u_0, \mathcal{G}, w)$, プライオリティ関数 $f$} 211 | \Output{$u_0$からゴール状態への経路、経路が存在しなければ$\emptyset$} 212 | $Closed \leftarrow \emptyset$, $Open \leftarrow \{u_0\}$, $d(u_0) \leftarrow 0$, $g(u_0) \leftarrow 0$\; 213 | \While{$Open \neq \emptyset$} { 214 | $u \leftarrow \argmin_{u' \in Open} f(u')$ \; 215 | $Open \leftarrow Open \setminus \{u\} $\; 216 | 217 | \If {$\mathcal{G}(u)$} { 218 | \Return $Path(u)$\; 219 | } 220 | 221 | $s \leftarrow parent(u)$\; 222 | \If{$u \notin Closed$ {\bf or} $g(s) + w(s, u) < g(u)$} { 223 | $d(u) \leftarrow d(s) + 1$\; 224 | $d(u) \leftarrow g(s) + w(s, u)$\; 225 | $parent(u) \leftarrow s$\; 226 | \For {each $v \in \mathcal{E}(u)$} { 227 | $Open \leftarrow Open \cup \{v\}$\; 228 | $parent(v) \leftarrow u$\; 229 | } 230 | } 231 | 232 | \If{$u \notin Closed$} { 233 | $Closed \leftarrow Closed \cup \{u\}$\; 234 | } 235 | } 236 | \Return $\emptyset$\; 237 | \end{algorithm} 238 | 239 | \section{Python実装} 240 | 241 | タイブレーキング戦略の実装はシンプルである。 242 | プライオリティ関数に複数のプライオリティ値を出力する関数にすればよい。 243 | Pythonの場合タプルをsortで渡せば先頭の値からタイブレーキングをしてくれる。 244 | 245 | \inputpython{python/astar_search_tiebreaking.py}{1}{5} 246 | 247 | バケットプライオリティキューの実装のためにはオープンリスト・クローズドリストを選べるようにグラフ探索のコードを書き換える必要がある。 248 | 以下のコードは\code{GraphSearch}の引数にオープンリストとクローズドリストを渡せるようにしたものである。 249 | 250 | \inputpython{python/optimized_graph_search.py}{1}{46} 251 | 252 | バケットオープンリストおよびハッシュテーブルによるクローズドリストは以下のように実装できる。 253 | 254 | \inputpython{python/bucket_openlist.py}{1}{37} 255 | 256 | \inputpython{python/hash_closedlist.py}{1}{33} 257 | 258 | バケットオープンリストにある\code{beam width}は後述するビームサーチのために用いる。 259 | ビームサーチを利用する予定がなければ無視してよい。 260 | 261 | \section{まとめ} 262 | 263 | 探索にかかる時間はおよそ(展開したノードの数)と(1展開あたりにかかる時間の平均値)の積である。 264 | 1展開あたりにかかる時間の多くがデータ構造へのアクセスにかかっているのであれば、データ構造を適切に選択することでアルゴリズムの実行時間を向上させることができる可能性がある。 265 | 266 | プライオリティ値が取る値が有限個である場合はバケットプライオリティキューが強力である \cite{burns2012implementing}。 267 | タイブレーキング戦略はシンプルで強力な戦略であるので、これも積極的に使うことをお勧めする。 268 | 269 | \section{練習問題} 270 | \begin{enumerate} 271 | \item 3x3のスライディングタイル問題のオープンリストにタイブレーキングを実装してみよう。$f$値が最小のノードが複数ある場合に$h$値が最小のノードを優先して展開するタイブレーキング戦略と、逆に$h$値が最大のノードを優先する戦略を実装し、性能を比較してみよう。どちらの方が効率的だったか?それは何故か? 272 | 273 | \item グリッド経路探索問題において完全ハッシュを作ることはできるか? 274 | 275 | \item 3x3のスライディングタイル問題で遅延重複検出を実装してみよう。展開時に重複検出をする場合と比較し、展開ノード数はどちらが多かったか?生成ノード数はどちらが多かったか? 276 | \end{enumerate} 277 | 278 | \section{関連文献} 279 | 280 | ヒューリスティック探索の性能がデータ構造の選択によって大きく左右されることは古くから知られていたようであるが、それを学術論文としてまとめたものは多くはない。 281 | Burns et al. (2012) \cite{burns2012implementing}は高速なヒューリスティック探索のコードを実装するための方法をまとめた論文である。データ構造の選択によって性能が大きく変わるということを指摘した論文である。 282 | オープンリストのタイブレーキングに関してはAsai\&Fukunaga (2016) \cite{asai2016tiebreaking}を参照されたい。 283 | 284 | % データ構造に関する一般的な紹介は必要か? 285 | %バケットプライオリティキューは Dial (1969) \cite{dial1969algorithm}で提案された。 286 | 287 | 288 | % \TODO{Rabin and Karp hashing} 289 | % 290 | % \TODO{Universal hash function} 291 | -------------------------------------------------------------------------------- /planning.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | 3 | \chapter{自動行動計画問題 (Automated Planning Problem)} 4 | %\chapter{古典的プランニング問題 (Classical Planning Problem)} 5 | \label{ch:classical-planning} 6 | 7 | 本書の冒頭でグラフ探索アルゴリズムが人工知能のための技術として研究されていると説明した。 8 | 人工知能とはさまざまな定義で使われる言葉であるが、グラフ探索は自動推論や自動行動計画のために使うことができるために研究されている。 9 | この章では人工知能の一分野である\define{自動行動計画問題}{automated planning problem}{じどうこうどうけいかくもんだい}について説明する \cite{ghallab:04}。 10 | 自動行動計画は初期状態から命題集合で表される目的を達成するための{\bf プラン}を発見する問題である。 11 | 自動行動計画のためのモデルは様々提案されている。 12 | 最も基本となるモデルは古典的プランニング問題である \cite{fikes:71}。 13 | 古典的プランニングは完全情報\footnote{正確には完全情報ではなく、アクションの決定のために必要な情報がすべて得られると仮定される。例えば問題に全く関係ない冗長な変数が含まれ、その情報がエージェントに与えられない場合は完全情報ではないが、アクションの決定のためには不要な情報であれば古典的プランニング問題で扱うことができる。}、決定的状態遷移を仮定とするモデルであり、状態空間問題に含まれる \cite{fikes:71}。 14 | 古典的プランニングによって様々な実問題を表すことができる。例えばロジスティック\cite{helmert2010scanalyzer,sousa2013toward}、セルアセンブリ\cite{asai2014fully}、遺伝子距離計算\cite{erdem2005genome}、ビデオゲーム\cite{lipovetzky2015a}など、様々な応用問題を含むモデルである。 15 | 16 | \section{定義} 17 | \label{sec:planning-definition} 18 | 19 | 古典的プランニング問題の最も基本となるSTRIPSモデル\cite{fikes:71}に従って定義する。 20 | 21 | \ddef{ 22 | STRIPS問題$P = (AP, A, s_0, Goal)$は命題変数の集合$AP$、アクションの集合$A$、初期状態$s_0 \subseteq AP$、ゴール条件$Goal \subseteq AP$からなる。 23 | アクション$a \in A$には適用条件$pre: A \rightarrow 2^{AP}$、追加効果$add: A \rightarrow 2^{AP}$、削除効果$del: A \rightarrow 2^{AP}$からなる。アクション$a$は適用条件$pre(a)$の命題を全て含む状態にのみ適用可能である。追加効果$add(a)$はアクション$a$を適用すると状態に追加される命題の集合であり、削除効果$del(a)$はアクション$a$を適用すると削除される命題の集合である。 24 | 古典的プランニングの目的は与えられた初期状態$s_0$からはじめ、ゴール条件に含まれる命題がすべて含まれる状態に到達するまでのアクションの列を求めることである。 25 | } 26 | 27 | STRIPS問題は状態空間問題の一種である。状態空間は$2^{AP}$であり、アクション$a$を状態$s$に適用後の状態$s' = a(s)$は 28 | \begin{equation} 29 | a(s) = (s \cup add(a)) \setminus del(a) 30 | \end{equation} 31 | である。 32 | 状態空間問題であるので、状態空間グラフを考えることができ、ヒューリスティック探索によって解くことができる問題である。 33 | 34 | 上の定義では集合の言葉で定義したが、人工知能の言葉、命題論理の言葉で説明することもできる。 35 | STRIPSモデルは\define{命題論理}{propositional logic}{めいだいろんり}で書かれたモデルである。 36 | \define{閉世界仮説}{closed-world assumption}{へいせかいかせつ}を仮定し、真であると示されていない命題は全て偽であるとする。 37 | 状態$s_0 = p_1 \land p_2 \land ...$は命題論理で書かれた世界の状態である。 38 | アクションの適用条件はアクションを適用するために真であるべき命題である。 39 | 追加効果はアクションを適用した後に真になる命題、削除効果は偽になる命題である。 40 | ゴール条件はゴール状態で真であるべき命題である。 41 | 42 | このように古典的プランニングは論理エージェントと非常に密接な関係がある。 43 | 古典的プランニングに興味がある方は\cite{russelln03}を参照されたい。 44 | 45 | 46 | % TODO: PDDLの文字フォントを\textttに 47 | \section{プランニングドメイン記述言語: Planning Domain Definition Language} 48 | \label{sec:pddl} 49 | 50 | Planning Domain Definition Language (PDDL) \cite{aeronautiques1998pddl}はプランニング問題を記述されるために用いられる言語の一つである。PDDLはプランニング問題を\define{一階述語論理}{first-order predicate logic}{いっかいじゅつごろんり}で表現する。 51 | PDDLはドメイン記述とインスタンス記述の2つに分けられ、Lispのような文法で書かれる。 52 | 図\ref{fig:pddl-domain}と図\ref{fig:pddl-instance}はブロックスワールド問題のドメイン記述とインスタンス記述である。 53 | ここでドメインとインスタンスは計算理論などで定義される\define{問題}{problem}{もんだい}と\define{インスタンス}{instance}{インスタンス}に対応する。 54 | ドメイン記述は問いの集合を定義し、インスタンスはその個例に対応する。 55 | 例えば「グリッド経路探索問題」はドメインであり、そのうちの特定の一つのマップがインスタンスに対応する。 56 | ドメイン記述には\define{述語}{predicate}{じゅつご} (\texttt{predicates})と\define{アクションスキーム}{action scheme}{アクションスキーム} (\texttt{action})がある。 57 | インスタンス記述には\define{オブジェクト}{object}{オブジェクト} (\texttt{objects})と初期状態(\texttt{init})、ゴール条件(\texttt{goal})がある。 58 | これら以外にも例えばオブジェクトの型などを定義することができるが、簡単のためここではこれら基本の文法のみを紹介する。 59 | 60 | PDDLに記述された一階述語論理は命題論理に変換できる。 61 | まず、命題集合$AP$は述語に含まれる変数にオブジェクトを割り当てることによって得られる。 62 | 図の例だと例えば\texttt{(on A B), (on A C), (ontable D),...}などの命題が$AP$に含まれる。 63 | アクション集合$A$はアクションスキームに含まれる変数にオブジェクトを割り当てることによって得られ、アクションの変数は\texttt{parameters}に定義される。 64 | アクション$a$の適用条件$pre(a)$はアクションスキームの\texttt{precondition}にオブジェクトを割り当てることで得られる。\texttt{effect}のうち\texttt{not}のついていない命題は$add(a)$に対応し、\texttt{not}のついた命題は$del(a)$に対応する。 65 | 例えばアクション\texttt{(pickup A)}の適用条件は{\texttt{(clear A), (ontable A), (handempty)}}、追加効果は{\texttt{(holding A)}}、削除効果{\texttt{(ontable A), (clear A), (handempty)}}である。 66 | 初期状態$s_0$は\texttt{init}の命題集合である。この例では{\texttt{(CLEAR C) (CLEAR A) (CLEAR B) (CLEAR D) (ONTABLE C) (ONTABLE A)}}である。 67 | ゴール条件$Goal$は\texttt{goal}の命題集合である。つまり、ゴール状態集合$T$は$Goal$を含む状態の集合である。 68 | 69 | PDDLのミソは一階述語論理によってプランニング問題を記述する点である。 70 | 状態空間に含まれる命題を一つ一つ記述するのではなく、述語とオブジェクトの組み合わせによって複数の命題をコンパクトに記述することができる。 71 | また、インスタンス記述を変えることで同じドメインの異なるインスタンスをモデルすることができる。 72 | 例えばブロックスワールドのドメイン記述はそのままに、インスタンス記述のオブジェクトや\texttt{init}などを変えることで違う問いにすることができる。 73 | 74 | PDDLは状態空間問題だけでなくより広く様々な問題を扱うことができる \cite{aeronautiques1998pddl,fox2003pddl2}。 75 | Fast DownwardはPDDLの文法の多くをサポートしているので試してみるには便利である。 76 | 77 | \begin{figure} 78 | \includegraphics[width=0.8\linewidth]{./figures/blocks-image.png} 79 | \caption{Blocks worldドメイン} 80 | \label{fig:blocks-world} 81 | \end{figure} 82 | 83 | \begin{figure} 84 | %\begin{adjustbox}{width=\textwidth,keepaspectratio} 85 | \lstset{language=pddl,basicstyle=\ttfamily\footnotesize,breaklines=true} 86 | \begin{lstlisting} 87 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 88 | ;;; 4 Op-blocks world 89 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 90 | (define (domain BLOCKS) 91 | (:requirements :strips) 92 | (:predicates (on ?x ?y) 93 | (ontable ?x) 94 | (clear ?x) 95 | (handempty) 96 | (holding ?x) 97 | ) 98 | 99 | (:action pick-up 100 | :parameters (?x) 101 | :precondition (and (clear ?x) (ontable ?x) (handempty)) 102 | :effect 103 | (and (not (ontable ?x)) 104 | (not (clear ?x)) 105 | (not (handempty)) 106 | (holding ?x))) 107 | 108 | (:action put-down 109 | :parameters (?x) 110 | :precondition (holding ?x) 111 | :effect 112 | (and (not (holding ?x)) 113 | (clear ?x) 114 | (handempty) 115 | (ontable ?x))) 116 | (:action stack 117 | :parameters (?x ?y) 118 | :precondition (and (holding ?x) (clear ?y)) 119 | :effect 120 | (and (not (holding ?x)) 121 | (not (clear ?y)) 122 | (clear ?x) 123 | (handempty) 124 | (on ?x ?y))) 125 | (:action unstack 126 | :parameters (?x ?y) 127 | :precondition (and (on ?x ?y) (clear ?x) (handempty)) 128 | :effect 129 | (and (holding ?x) 130 | (clear ?y) 131 | (not (clear ?x)) 132 | (not (handempty)) 133 | (not (on ?x ?y))))) 134 | \end{lstlisting} 135 | %\end{adjustbox} 136 | \caption{blocks-worldのdomainファイル} 137 | \label{fig:pddl-domain} 138 | \end{figure} 139 | 140 | \begin{figure} 141 | %\begin{adjustbox}{width=\textwidth,keepaspectratio} 142 | \lstset{language=pddl,basicstyle=\ttfamily\footnotesize,breaklines=true} 143 | \begin{lstlisting} 144 | (define (problem BLOCKS-3) 145 | (:domain BLOCKS) 146 | (:objects A B C) 147 | (:INIT (CLEAR C) (CLEAR B) (ONTABLE C) (ONTABLE B) 148 | (ON C A) (HANDEMPTY)) 149 | (:goal (AND (ON A B) (ON B C))) 150 | ) 151 | 152 | \end{lstlisting} 153 | %\end{adjustbox} 154 | \caption{blocks-worldのinstanceファイル} 155 | \label{fig:pddl-instance} 156 | \end{figure} 157 | 158 | \section{古典的プランニング問題の例} 159 | \label{sec:classical-planning-example} 160 | 161 | プランニングは様々な問題解決に役立てることができる。 162 | ここでは簡単にプランニングによってどのような問題がモデルできるかを紹介する。 163 | 164 | \begin{enumerate} 165 | \item エアポート (airport) 166 | 空港の地上の交通管理を行う問題である。 167 | 飛行機の離着陸の時間が与えられるのに対し、安全かつ飛行機の飛行時間を最小化する交通を求める問題である。 168 | 169 | \item サテライト (sattellite) 170 | 人工衛星は与えられた現象を撮影し、地球にデータを送らなければならない。 171 | このドメインはNASAの人工衛星の実際の応用から考案されたものである。 172 | 173 | \item ローバー (rovers) 174 | ローバーとは惑星探査ロボットのことである。 175 | この問題は惑星探査ロボットのグループを使って惑星を探索する計画を作る問題である。 176 | ロボットらは興味深い地形などの写真を取るなどの作業を実行する必要がある。 177 | このドメインもNASAの応用をもとにしたものである。 178 | 179 | \item パイプスワールド (pipesworld) 180 | 複数の原油の油田から複数の目的地にパイプを通して送りたい。 181 | 各目的地に定められた量を送るように調整することが目的である。 182 | パイプのネットワークはグラフとしてモデルされており、また同時に原油を通せないパイプの組が存在する。 183 | 184 | \item セルアセンブリ (cell assembly) 185 | セルアセンブリは狭いセルの中で働き手が複雑な製品を作成する工場システムである。 186 | 大規模な製造ラインと比較して、 187 | セルアセンブリは主に中程度の数(100個程度など)の製品を作るために使われる。 188 | 製品の開発や受注生産などに対応して、今生産しなければならない製品を手早く作成するためのセルの行動プランを考えることが問題の目的である。 189 | \cite{asai2014fully} 190 | 191 | \item ゲノムリアレンジメント (genome rearrangement) 192 | ゲノムリアレンジメントは多重整列問題の一つである。 193 | ゲノム間の編集距離とは類似性を測るための指標として使われ、生物の進化の歴史をたどるために使われる。編集距離はあるゲノムから操作を繰り返してもう一方のゲノムに変換するためのコストの総和として定義される。 194 | プランニングモデルを用いることでより様々な操作を定義することができる。例えば遺伝子の位置に入れ替えなど、\ref{sec:msa}節で説明したように単純にグリッド経路探索に落とし込むことのできない複雑な操作を考えることができる。 195 | \cite{erdem2005genome} 196 | 197 | \item トラック (trucks) 198 | ロジスティクスと呼ばれる問題の一種である。 199 | トラックを運転してすべての荷物をそれぞれ定められた運び先に届ける問題である。 200 | ただしトラックが運べる荷物の総量は有限であるため、それを考慮して経路を考えなければならない。加えて、届けるまでの期限が存在する荷物が存在する。 201 | 202 | \end{enumerate} 203 | 204 | これらの問題はすべて問題に特化した特別なアルゴリズムをそれぞれの問題に対して開発することもできる。多くの場合、特化アルゴリズムの方が汎用プランナーよりも高速である。プランナーの利点は問題に特化した実装をしなくてもPDDLを書くだけで問題を解くことが出来るという点にある。 205 | 206 | \section{ヒューリスティック関数の自動生成} 207 | \label{sec:automated-heuristic} 208 | 209 | PDDLにはヒューリスティック関数は何を使えばよいかなどの情報は書かれていない。 210 | よって、PDDLを入力とする状態空間問題を解く場合、エージェントはヒューリスティック関数を自動生成しなければならない。 211 | PDDLからヒューリスティック関数を自動生成する手法はプランニング研究の最も重要な研究課題の一つである。 212 | 213 | ヒューリスティックの生成方法の一つの指針としては\ref{sec:heuristic-example}節で言及した緩和問題によるヒューリスティックが分かりやすい。 214 | すなわち、元の問題$P$よりも簡単な問題$P'$を生成し、$P$の状態$s$から$P'$の状態$s'$へのふさわしい関数を定義する。そして$h(s)$の値を$P'$における$s'$を初期状態とするゴールへの最適解にする。 215 | このようにしてヒューリスティック関数は自動生成することができる。 216 | 217 | 各アルゴリズムの実装はfast-downward \footnote{\url{http://www.fast-downward.org}}を参照されたい。 218 | 219 | \subsection{ゴールカウントヒューリスティック (Goal Count Heuristic)} 220 | 221 | 多くの問題ではゴールはいくつかの条件を満たした状態の集合として与えられる。 222 | ゴールカウントヒューリスティックは満たしていないゴール条件の数をヒューリスティック値とする関数である。 223 | 例えばスライディングタイルのゴール条件は全てのタイルが所定の位置にあることである。 224 | なので所定の位置にないタイルの数を$h$値とすることが出来る。 225 | 226 | ゴールカウントヒューリスティックは許容的であるとは限らない。コスト1のアクションが2つのゴールを同時に満たすかもしれないからだ。1つのアクションで同時に満たせるゴールが1つ以下である場合、そしてその時のみ、許容的である。 227 | 228 | \subsection{適用条件緩和 (Precondition Relaxation)} 229 | 230 | \define{適用条件緩和}{precondition relaxation}{てきようじょうけんかんわ}はアクションの適用条件を無視し、すべてのアクションをすべての状態で適用できる緩和問題を解くヒューリスティックである。すべてのアクションが適用できるようになるので、グラフのエッジの数が増えることになる。このとき、すべてのゴール条件の命題は1ステップで満たすことができる。適用条件緩和はゴールカウントヒューリスティックに近いが、少しだけ適用条件緩和の方が正確である。なぜなら適用条件緩和の場合、1. 複数のゴールを同時に満たすアクションがある場合、そのアクションを1ステップで実行することができ、2. アクションを適用することによって一旦満たされた命題が削除されることがあるからである。適用条件緩和された問題は元の問題と比べて非常に簡単になっているが、まだまだ難しい。なので更に緩和し、一度満たされた命題が削除されないとすることが多い。 231 | こうすると緩和問題は、追加効果$add(a)$の和集合がゴール条件を全て満たす$Goal \subseteq \bigcup_{a \in A'} add(s)$ような最小のアクション集合$A'$を求める問題になる。これはまさしく\define{集合被覆問題}{set cover}{しゅうごうひふくもんだい}である \cite{karp1972reducibility}。 232 | 集合被覆問題でもまだNP困難問題であるがシンプルな貪欲で$\log n$の近似アルゴリズムになる\cite{chvatal1979greedy}。 233 | ただし近似アルゴリズムを使う場合、許容的なヒューリスティックではなくなってしまう。 234 | 235 | 236 | \subsection{削除緩和 (Delete Relaxation)} 237 | 238 | \define{削除緩和}{delete relaxation}{さくじょかんわ}はアクションの削除効果を無視する緩和問題を用いたヒューリスティックである \cite{hoffmann:01a}。この緩和問題では各アクション$a \in A$の代わりに$a^+$を用いる。$a^+$は$a^+$と同じ適用条件、追加効果を持っているが削除効果が空集合である ($del(a^+) = \emptyset$)。この緩和問題における最適解のコストをヒューリスティック関数$h^+$として用いる。 239 | 緩和問題では削除効果がないので状態に含まれる命題変数は単調増加していく。そのため元の問題よりも簡単になるが、これでもまだNP困難である \cite{bylander:94}。 240 | そのため$h^+$を非許容的に見積もるヒューリスティック、例えばadditive heuristic \cite{bonet:01a}、FF heuristic \cite{hoffmann:01a}、pairwise max heuristic \cite{mirkis2007cost}、set-additive heuristic \cite{keyder2009trees}などがつかわれる。これらを用いた場合得られるヒューリスティックは非許容的になる。 241 | 242 | $h^+$を許容的に見積もるヒューリスティックとしてはmax heuristic $h^{max}$がある \cite{bonet:01a}。$h^{max}$はゴール条件の各命題$t \in Goal$に対してその命題一つのみをゴール条件と更に緩和した問題 ($del(a^{max}) = \emptyset, Goal^{max} = t$)を解く。このコストを$c(t)$として、$h^{max}$はその最大値である ($h^{max} = \max_{t \in Goal} c(t)$)。 243 | $h^{max}$は許容的であるが、非許容的なヒューリスティックよりも探索の効率が悪いこと多いことが実験的に示されている。 244 | ちなみにadditive heuristicは最大値の代わりに$c(t)$の総和を取るものである。非許容的である代わりに$h^{max}$よりも性能が良いことが多い。 245 | 246 | \subsection{最長経路 (Critical Path)} 247 | 248 | \define{最長経路ヒューリスティック}{critical path heuristic} \cite{haslum:00}は命題の集合を全て満たすための最小コスト$c(X)$を$X$の大きさ$m$以下の部分集合$K \subseteq X$の最大値で近似する ($c^m(X) = \max_{X' \subseteq X, |X'| \leq m} c^m(X')$)というアイディアに基づいたヒューリスティックである。この近似は下界になるので許容的なヒューリスティックが得られる。max heuristicは最長経路ヒューリスティックのうち$m=1$の場合である ($h^{max} = h^1$)。$h^m \geq h^{m-1}$なので$m$が大きいほど正確な見積もりが出来るが、同時に計算コストが$m$に対して指数的に伸びるトレードオフがある。 249 | 250 | 元々GraphPlanと呼ばれる並行プランナーで使われたアイディアに基づいている \cite{blum:97}。 251 | 252 | \subsection{抽象化 (Abstraction)} 253 | 254 | \define{抽象化}{abstraction}{ちゅうしょうか}ヒューリスティックは状態$s$を抽象状態$\alpha(s)$への写像を用いたヒューリスティックである。 255 | ヒューリスティック値$h^\alpha(s)$は抽象化された状態空間$S^\alpha = \{\alpha(s) | s \in S\}$でのゴールへの距離である。 256 | 抽象化は元の問題よりも簡単になるので許容的なヒューリスティックが得られる。 257 | ヒューリスティックの性能は$\alpha$の選択による。 258 | $\alpha$の選択方法としてはパターンデータベースヒューリスティック \cite{culberson1998pattern,edelkamp2001planning,holte:04,katz2008structural}、merge-and-shrink \cite{helmert2007flexible,helmert2014merge}などがある。 259 | 260 | \subsection{ランドマーク (Landmark)} 261 | 262 | プランニング問題の\define{ランドマーク}{landmark}{ランドマーク}とは、全ての解の中で一度でも真になることがある命題である \cite{porteous2001extraction}。 263 | 初期状態とゴール条件に含まれる命題はランドマークである。 264 | ランドマークの直感的な理解としては、問題を解くために通過する必要がある中間目標地点である。 265 | 266 | \define{ランドマークカウント}{landmark count}{ランドマークカウント}ヒューリスティックはこれから通過しなければならないランドマークの数をヒューリスティック値とする。 267 | ランドマークを全て正しく発見する問題はPSPACE困難なので近似をする必要がある。 268 | 近似の方法によって非許容的なヒューリスティック (e.g. LAMA) \cite{richter2008landmarks}を得る手法と許容的なヒューリスティックを得る手法がある \cite{karpas2009cost}。 269 | 270 | \define{ランドマークカット}{Landmark cut}{ランドマークカット}はランドマークカウントを一般化したヒューリスティックである \cite{helmert:09}。「全ての可能な解で真になる命題」はいくつかは発見できるが、十分な数を発見するのはかなり難しい。なのでランドマークカットはその代わりに「全ての可能な解で少なくとも一つが真になる命題の集合」を使う。このような命題の集合はjustification graphと呼ばれるランドマーク発見のためのグラフのカットに相当するため、ランドマークカットと呼ばれる。 271 | 272 | % \section{プラン認識、ゴール認識} 273 | 274 | 275 | \section{Python実装} 276 | 277 | PDDLプランナーの多くはC, C++で実装されているが、Pythonで書かれたプランナーもある。 278 | \define{pyperplan}{pyperplan}{パイパープラン}は著名なプランニングの研究者らがGPLv3で公開しているPythonで実装されたSTRIPSのプランナーである。 279 | 基本的な探索アルゴリズムや、ランドマーク、ランドマークカットなどのヒューリスティック関数、また本書では扱っていないが充足可能性問題 (SAT)のソルバーを利用したプランナーも実装されている。 280 | pyperplanは\code{pip}からインストールできる。 281 | 282 | \begin{python} 283 | pip install pyperplan 284 | \end{python} 285 | 286 | 例えばランドマークカットヒューリスティックでA*探索を行うプランナーを実行するには以下のようにする。 287 | 288 | \begin{python} 289 | pyperplan -H=lmcut --domain=domain.pddl --problem=problem.pddl 290 | \end{python} 291 | 292 | \section{まとめ} 293 | 294 | ヒューリスティック探索はPDDLで記述された幅広い問題を自動行動計画問題として解くことができる。 295 | ヒューリスティック関数は人間がデザインする必要はなく、PDDLから自動抽出した関数を利用して探索を効率化することができる。 296 | 297 | 298 | \section{練習問題} 299 | \begin{enumerate} 300 | \item グリッド経路探索問題をPDDLで記述し、それをpyperplanに入力して解いてみよう。 301 | 302 | \item いくつかのヒューリスティック関数を用いてグリッド経路探索問題を解いてみて、どのヒューリスティック関数が最も効果的かを調べてみよう。例えばランドマークカットとFFヒューリスティックではどちらが効率的だったか? 303 | 304 | \item (難問) ヒューリスティック探索は幅広い自動行動計画問題を解くことができる。では、それぞれ個別の問題を解くためのアルゴリズムを考える必要はあるのか?例えば巡回セールスパーソン問題はヒューリスティック探索以外だとどのような手法が使われているか?ヒューリスティック探索で解こうとすると、どのような問題があるのか? 305 | \end{enumerate} 306 | 307 | \section{関連文献} 308 | 309 | 自動行動計画を状態空間探索アルゴリズムで解くための手法はStefan Edelkamp and Stefan SchrodlのHeuristic Search Theory and Application \cite{edelkamp:2010:hst:1875144}にまとめられている。状態空間探索アルゴリズム以外にもSATやCSPなどの制約充足問題に変換して解く方法もある \cite{ernst1997automatic,do2001planning,sharon2015conflict}。 310 | 311 | 状態遷移が確率的である問題を\define{確率的プランニング}{probabilistic planning}{かくりつてきプランニング}と呼ぶ。確率的プランニングはマルコフ決定過程でモデルされることが多い。更に不完全情報である場合は\define{信念空間プランニング}{belief space planning}{しんねんくうかんプランニング}問題である。これらの問題は本文の範囲外とする。詳細は教科書を参照されたい\cite{russelln03}。 312 | 313 | プランニングの問題定義やアルゴリズムの実装を見たい方はFast Downward \cite{helmert2006}をオススメする。 314 | Fast DownwardはPDDLで表現された古典的プランニング問題を解くstate-of-the-artのプランナーである。 315 | 本書で紹介するアルゴリズムの多くがFast Downwardに組み込まれている。 316 | -------------------------------------------------------------------------------- /problem.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | 3 | \chapter{状態空間問題 (State-Space Problem)} 4 | \label{ch:state-space-problem} 5 | 6 | % This chapter is ... 7 | この章ではまず、\ref{sec:state-space-problem}節ではグラフ探索手法が用いられる問題として状態空間問題を定義する。 8 | 次に\ref{sec:search-problem}節で状態空間問題の例をいくつか紹介する。 9 | 経路探索問題や倉庫番問題など、応用がありつつ、かつ分かりやすい問題を選んだ。これらの問題はすべてヒューリスティック探索研究でベンチマークとして広く使われているものである。 10 | 11 | \ref{sec:state-space-problem}節における定式化は\cite{russelln03}、\cite{pearl84}、\cite{edelkamp:2010:hst:1875144}などを参考にしている。本文は入門の内容であるので、研究の詳細が知りたい方はこれらの教科書を読むべきである。 12 | 13 | %\captionlistentry[todo]{状態空間問題の例示} 14 | 15 | \section{状態空間問題 (State-Space Problem)} 16 | \label{sec:state-space-problem} 17 | 18 | この本では主に初期状態とゴール条件が与えられたとき、ゴール条件を満たすための経路を返す問題を探索する手法を考える。 19 | 特に本書の主眼は\ref{ch:state-space-problem}章から\ref{ch:heuristic-search-variants}章までで扱う\define{状態空間問題}{state-space problem}{じょうたいくうかんもんだい}である。 20 | 21 | \ddef{ユニットコスト状態空間問題、state-space problem}{ 22 | ユニットコスト状態空間問題$P = (S, A, s, T)$は状態の集合$S$、初期状態$s \in S$、ゴール集合$T \in S$、アクション集合$A = {a_1, ....,a_n}$、$a_i : S \rightarrow S$が与えられ、初期状態$s$からゴール状態へ遷移させるアクションの列を返す問題である。 23 | } 24 | 25 | % \TODO{状態空間問題の例。それをグラフ問題に帰着させる例。} 26 | 27 | \begin{figure}[htb] 28 | \centering 29 | \begin{tikzpicture}[scale=0.5] 30 | \input{figures/statespace.tex} 31 | \end{tikzpicture} 32 | \caption{状態空間問題の例。エージェントはスタート地点$a$からゴール地点$g$を目指す。 33 | } 34 | \label{fig:ssp-graph} 35 | \end{figure} 36 | 37 | 38 | ユニットコスト状態空間問題はグラフにモデルすることで考えやすくなる。 39 | ユニットコスト状態空間問題を表す\define{状態空間グラフ}{state-space graph}{じょうたいくうかんぐらふ}は以下のように定義される。 40 | 41 | \ddef{状態空間グラフ、State-space graph}{ 42 | 問題グラフ$G = (V, E, s, T)$は状態空間問題$P = (S, A, s, T)$に対して以下のように定義される。ノード集合 $V = S$、初期ノード$s \in S$、ゴールノード集合$T$、エッジ集合$E\subseteq V \times V$。エッジ$u,v\in E$は$a(u) = v$となる$a\in A$が存在する場合に存在し、そしてその場合にのみ存在する(iff)。 43 | } 44 | 45 | 状態空間問題の\define{解}{solution}{かい}は以下の定義である。 46 | 47 | \ddef{解、Solution}{ 48 | 解$\pi = (a_1,a_2...,a_k)$はアクション$a_i \in A$の(順序付)配列であり、初期状態$s$からゴール状態$t \in T$へ遷移させる。すなわち、$u_i \in S$,$i \in \{0,1,...,k\}$, $u_0 = s, u_k = t$が存在し、$u_i = a_i(u_{i-1})$となる。 49 | } 50 | 51 | どのような解を見つけたいかは問題に依存する。 52 | 多くの問題では\define{経路コスト}{path cost}{けいろこすと}の合計を小さくすることを目的とする。 53 | 54 | %すなわち、アクションに対してコストが定義されており、経路 55 | 56 | \ddef{状態空間問題、Weighted state-space problem}{ 57 | 状態空間問題$P = (S, A, s, T, w)$はユニットコスト状態空間問題の定義に加え、コスト関数$w: A \rightarrow \mathbb{R}$がある。経路$(a_1,...,a_k)$のコストは$\sum^k_{i=1}w(a_i)$と定義される。 58 | } 59 | 60 | %ある解が可能なすべての解の中でコストが最小である場合、その解を最適解(optimal cost solution)であると言う。 61 | 本書ではこの状態空間問題を主に扱う。 62 | 状態空間問題のうちコストが定数関数である場合がユニットコスト状態空間問題である。 63 | 状態空間問題は重み付き(コスト付き)グラフとしてモデルすることが出来る。すなわち、$G = (V, E, s, T, w)$は状態空間グラフの定義に加え、エッジの重み$w: E \rightarrow \mathbb{R}$を持つ。 64 | 65 | \ref{ch:blind-search}章で詳解するが、探索アルゴリズムは状態空間グラフのノード・エッジ全てを保持する必要はない。 66 | 全てのノード・エッジを保持した状態空間グラフを特に\define{明示的状態空間グラフ}{explicit state-space graph}{めいじてきじょうたいくうかんぐらふ}と呼ぶとする。このようなグラフは、例えば隣接行列を用いて表すことが出来る。隣接行列$M$は行と列の大きさが$|V|$である正方行列であり、エッジ$(v_i, v_j)$が存在するならば$M_{i,j}=1$、なければ$M_{i,j}=0$とする行列である。 67 | このような表現方法の問題点は行列の大きさが$|V|^2$であるため、大きな状態空間を保持することが出来ないことである。 68 | 例えば、\ref{sec:search-problem}節で紹介する15-puzzleは状態の数が$|V|=15!/2$であるため、隣接行列を保持することは現在のコンピュータでは非常に困難である。 69 | 70 | そこで、探索アルゴリズムは多くの場合初期ノードとノード展開関数による\define{非明示的状態空間グラフ}{implicit state-space graph}{ひめいじてきじょうたいくうかんぐらふ}で表せられる。 71 | 72 | \ddef{非明示的状態空間グラフ、Implicit state-space graph}{ 73 | 非明示的状態空間グラフ $(s, Goal, Expand, w)$は初期状態$s \in V$、ゴール条件Goal: $V \rightarrow B = \{false, true\}$、ノード展開関数Expand: $V \rightarrow 2^V$、コスト関数$w: V \times V \rightarrow \mathbb{R}$によって与えられる\footnote{$2^V$はノード集合$V$のべき集合である。}。 74 | } 75 | 76 | 非明示的状態空間グラフも状態空間問題に対して定義できる。明示的状態空間グラフとの違いは次状態の情報が隣接行列ではなくノード展開関数Expandの形に表現されている点である。Expandはある状態からの可能な次の状態の集合を返す関数である。Expand関数は明示的に与えられるのではなく、ルールによって与えられることが多い。例えば将棋であれば、将棋のルールによって定められる合法手によって得られる次の状態の集合がExpand関数によって得られる。多くの場合このノード展開関数は隣接行列よりも小さな情報で表現できる。 77 | 78 | 本書ではすべてのノードの分枝数が有限であると仮定する (局所有限グラフ、locally finite graph)。% また、コスト関数$w$は非負値であるとする。 79 | また、特に断りがない場合簡単のため$w \geq 0$を仮定する。 80 | 81 | % 本書で紹介するアルゴリズムは無限グラフでも完全なものがあるが、局所有限ではないグラフでも完全なものはない。 82 | 83 | 84 | 85 | \section{状態空間問題の例} 86 | \label{sec:search-problem} 87 | 88 | 状態空間問題の例をいくつか紹介する。 89 | これらの問題はヒューリスティック探索研究でベンチマークとして広く使われているものである。 90 | 91 | %グリッド経路探索問題など、応用がありつつ、かつ分かりやすい問題を選んだ。 92 | %グラフ探索アルゴリズムによって効率的に解くことが出来ると知られているドメインをいくつか紹介する。 93 | %ここで詳解する問題はグラフ探索以外の手法でも解くことが出来る。 94 | 95 | 96 | \subsection{グリッド経路探索 (Grid Path-Finding)} 97 | %\captionlistentry[todo]{Grid Pathfinding: なんかいい感じの絵} 98 | %{\TODO Grid Pathfinding: なんかいい感じの絵} 99 | 100 | \define{グリッド経路探索問題}{grid path-finding problem}{グリッドけいろたんさくもんだい}は2次元 (あるいはもっと高次元でもよい) のグリッド上で初期配置からゴール位置までの経路を求める問題である\cite{yap2002grid}。グリッドには障害物がおかれ、通れない箇所がある。エージェントが移動できる方向は4方向($A= \{up, down, left, right\}$)か8方向(4方向に加えて斜め移動)とする場合が多い。自由方向(Any Angle)の問題を扱う研究も存在する\cite{nash2007theta}。 101 | 102 | \begin{figure} 103 | \centering 104 | % \includegraphics[width=0.5\textwidth]{figures/grid-astar.png} 105 | \begin{tikzpicture}[scale=0.5] 106 | \input{figures/gridpathfinding.tex} 107 | \end{tikzpicture} 108 | \caption{グリッド経路探索問題} 109 | \label{fig:grid-pathfinding} 110 | \end{figure} 111 | 112 | 113 | Web上に簡単に試せるデモがあるので、参照されたい\footnote{\url{http://qiao.github.io/PathFinding.js/visual/}}。この本で説明する様々なグラフ探索手法をグリッド経路探索に試すことが出来る。% この本の画像の一部はこのデモをもとに作成している。 114 | 115 | グリッド経路探索はロボットのモーションプランニングやゲームAIなどで応用される\cite{algfoor2015comprehensive}。ストラテジーゲームなどでユニット(エージェント)を動かすために使われる \cite{cui2011based,sturtevant2012benchmarks}。% よく使われるベンチマーク問題集にもStarcraftのゲームのマップが含まれている\cite{sturtevant2012benchmarks}. 116 | またグリッドは様々な問題を経路探索に帰着して解くことができるという意味でも重要である。例えば後述する多重整列問題 (Multiple Sequence Alignment)はグリッド経路探索に帰着して解くことが出来る(節 \ref{sec:msa})。 117 | ロボットのモーションプランニングも経路探索問題に帰着することが出来ることがある \cite{barraquand91}。この問題では複数個の関節の角度を変えながら、現在状態から目的の状態 (Configuration)にたどり着けることが目的となる。各関節の角度をグリッドの各次元で表し、ロボットの物理的な構造のために不可能な角度の組み合わせを障害物の置かれたグリッドとすることでグリッド経路探索問題に帰着することができる。このようにモデルを作ると、グリッド上で障害物を避けた経路を計算することで現在状態から目的状態へ関節をうまく動かすモーションプランが発見できる。 118 | 119 | 120 | \subsection{スライディングタイル (Sliding-tile Puzzle)} 121 | 122 | 多くの一人ゲームはグラフ探索問題に帰着することが出来る。スライディングタイルはその例であり、ヒューリスティック探索研究においてメジャーなベンチマーク問題でもある (図\ref{fig:15-puzzle}) \cite{johnson1879notes}。 123 | $1$から$(n^2)-1$までの数字が振られたタイルが$n\times n$の正方形に並べられている。正方形には一つだけ{\it ブランク}と呼ばれるタイルのない位置があり、四方に隣り合うタイルのいずれかをその位置に移動する(スライドする)ことが出来る。スライディングタイル問題は、与えられた初期状態からスライドを繰り返し、ゴール状態にたどり着く経路を求める問題である。 124 | 125 | \begin{figure} 126 | \centering 127 | % \includegraphics[bb=0 0 372 373,width=0.5\textwidth]{figures/15-puzzle.png} 128 | \begin{tikzpicture}[scale=0.8] 129 | \input{figures/slidingtile.tex} 130 | \end{tikzpicture} 131 | \caption{15パズルのゴール状態の例} 132 | \label{fig:15-puzzle} 133 | \end{figure} 134 | 135 | 136 | スライディングタイルの到達可能な状態の数は$|V| = (n^2)!/2$\footnote{スライディングタイルは偶奇性があり、到達不可能な状態がある\cite{johnson1879notes}。}であり、$n$に対して指数的に増加する。 137 | 可能なアクションは$A= \{up, down, left, right\}$の4つであり、アクションにかかるコストはすべて同じとする。 138 | 139 | 後述するが、ヒューリスティック探索のためには状態からゴール状態までの距離(コスト)の下界(lower bound)が計算できると有用である。 140 | スライディングタイルにおける下界の求め方として最もシンプルなものは{\it マンハッタン距離ヒューリスティック}である。マンハッタン距離ヒューリスティックは各タイルの現在状態の位置とゴール状態の位置のマンハッタン距離の総和を取る。可能なアクションはすべて一つしかタイルを動かさないので、一回のアクションでマンハッタン距離は最大で1しか縮まらない。よって、マンハッタン距離はゴールまでの距離の下界である。 141 | 142 | %ちなみに、スライディングタイルはpermutation problemの一つである。 143 | 144 | \subsection{多重整列問題 (Multiple Sequence Alignment)} 145 | \label{sec:msa} 146 | 生物学・進化学では遺伝子配列・アミノ酸配列の編集距離(edit distance)を比較することでニ個体がどれだけ親しいかを推定することが広く研究されている。 147 | \define{多重整列問題}{Multiple Sequence Alignment}{たじゅうせいれつもんだい} (MSA)は複数の遺伝子・アミノ酸配列が与えられた時、それらの配列間の編集距離とその時出来上がった配列を求める問題である。 148 | 2つの配列に対してそれぞれコストの定義された編集操作を繰り返し、同一の配列に並べ替える手続きをアライメントと呼ぶ。 149 | 2つの配列の\define{編集距離}{edit distance}{へんしゅうきょり}は編集操作の合計コストの最小値である。 150 | 3つ以上の配列における距離の定義は様々考えられるが、例えば全ての配列のペアの編集距離の総和を用いられる。 151 | 152 | MSAにおける可能な編集操作は置換と挿入である。置換は配列のある要素(DNAかアミノ酸)を別の要素に入れ替える操作であり、挿入は配列のある位置に要素を挿入する操作である。例えば(ATG, TGC, AGC)の3つの配列のアライメントを考える。表\ref{tbl:msa-cost}は置換と編集に対するコストの例である。-は欠損を示し、対応する要素が存在しないことを表す。% アミノ酸配列における有名なコスト表としてPAM250\cite{pearson1990}があるが、ここでは簡単のため仮のコスト表を用いる。 153 | 表\ref{tbl:msa}はこのコスト表を用いたアライメントの例である。 154 | このとき、例えば配列ATG-と-TGCの編集距離は(A,-)、 (T,T)、 (G,G)、 (-,C)のコストの総和であるので、表\ref{tbl:msa-cost}を参照し、$3+0+0+3=6$である。同様に(ATG-, A-GC)の距離は$9$, (-TGC, A-GC)の距離は$6$であるので、3配列の編集距離は$6+9+6=21$である。 155 | 156 | $n$配列のMSAは$n$次元のグリッドの経路探索問題に帰着することが出来る\cite{korf:2000}。 157 | 図\ref{tbl:msa-to-grid}は(ATG)と(TGC)の2つの配列によるMSAをグリッド経路探索問題に帰着した例である。 158 | 状態$s = (x_1, x_2,...,x_n)$の各変数$x_i$は配列$i$のどの位置までアライメントを完了したかを表す変数であり、配列$i$の長さを$l_i$とすると定義域は$0 \leq x_0 \leq l_0$である。 159 | 全てのアライメントが完了した状態$s=(l_1, l_2,...,l_n)$がゴール状態である。 160 | 可能なアクション$a=(b_1, b_2, ..., b_n), (b_i=0, 1)$は配列$i$に対してそれぞれ欠損を挿入するか否かであり、配列$i$に対して欠損を挿入する場合に$b_i=0$、挿入しない場合は$b_i=1$となる。 161 | 状態$s$に対してアクション$a$を適用した後の状態$s'$は$s'=(x_1+b_1, x_2+b_2,..., x_n+b_n)$となる。図\ref{tbl:msa-to-grid}は初期状態$s=(0,0)$に対して$a=(1,0)$を適用している。これは(A), (-)までアライメントを進めた状態に対応する。次に$a=(1,1)$が適用され、アライメントは(A,T), (-,T)という状態に遷移する。このようにして$(0, 0)$から$(l_1, l_2)$までたどり着くまでの最小コストを求める。 162 | 163 | MSAは可能なアクションの数が配列の数$n$に対して指数的に増える ($2^n-1$)点が難しい。アミノ酸配列が対象である場合はコストの値が幅広い点も難しい \cite{pearson1990}。 164 | MSAは生物学研究に役立つというモチベーションから非常に熱心に研究されており、様々な定式化による解法が知られている。 165 | 詳しくは\cite{waterman1995introduction, 166 | gusfield1997algorithms,edgar2006multiple}を参照されたい。 167 | 168 | \begin{table} 169 | \centering 170 | \caption{多重配列問題 (MSA)} 171 | \begin{tabular}{c|cccc} 172 | \toprule 173 | Sequence1 & A & T & G & - \\ 174 | Sequence2 & - & T & G & C \\ 175 | Sequence3 & A & - & G & C \\ 176 | \bottomrule 177 | \end{tabular} 178 | \label{tbl:msa} 179 | \end{table} 180 | 181 | \begin{table} 182 | \centering 183 | \caption{MSAの塩基配列のコスト表} 184 | \begin{tabular}{c|ccccc} 185 | \toprule 186 | & A & T & G & C & - \\ \midrule 187 | A & 0 & 3 & 3 & 3 & 3 \\ 188 | T & 3 & 0 & 3 & 3 & 3 \\ 189 | G & 3 & 3 & 0 & 3 & 3 \\ 190 | C & 3 & 3 & 3 & 0 & 3 \\ 191 | - & 3 & 3 & 3 & 3 & 0 \\ 192 | \bottomrule 193 | \end{tabular} 194 | \label{tbl:msa-cost} 195 | \end{table} 196 | 197 | \begin{table} 198 | \centering 199 | \caption{MSAのグリッド経路探索問題への帰着} 200 | \begin{tabular}{c|cccc} 201 | \toprule 202 | & A & T & G & - \\ \midrule 203 | T & $\rightarrow$ & $\searrow$ & & \\ 204 | G & & & $\searrow$ & \\ 205 | C & & & & $\downarrow$ \\ 206 | \bottomrule 207 | \end{tabular} 208 | \label{tbl:msa-to-grid} 209 | \end{table} 210 | 211 | 212 | \subsection{倉庫番 (Sokoban)} 213 | 倉庫番(Sokoban)は倉庫の荷物を押していくことで指定された位置に置くというパズルゲームである。現在でも様々なゲームの中で親しまれている \cite{junghanns1997sokoban,culberson:97}。 214 | プレイヤーは「荷物の後ろに回って押す」ことしか出来ず、引っ張ったり、横から動かしたりすることが出来ない。また、荷物の上を通ることも出来ない。 215 | PSPACE-completeであることが知られている\cite{culberson:97}。 216 | 217 | % #### 218 | % ##### # 219 | % # $ # 220 | % # .# # 221 | % ## ## ## 222 | % # # 223 | % # @# # 224 | % # ##### 225 | % #### 226 | 227 | \begin{figure} 228 | \centering 229 | \includegraphics[bb=0 0 213 238,width=0.4\textwidth]{figures/sokoban.pdf} 230 | \caption{倉庫番} 231 | \label{fig:sokoban} 232 | \end{figure} 233 | 234 | 状態の表現方法は2通りあり、一つはグリッドの各位置に何が置いてあるかを変数とする方法である。もうひとつはプレイヤー、各荷物の位置に対してそれぞれ変数を割り当てる方法である。 235 | 可能なアクションは{\tt move-up}, {\tt move-left}, {\tt move-down}, {\tt move-right}, {\tt push-up}, {\tt push-left}, {\tt push-down}, {\tt push-right} の8通りである。{\tt move-*}はプレイヤーが動くアクションに対応し、コストは0である。{\tt push-*}は荷物を押すアクションであり、正のアクションコストが割当てられている。よって、倉庫番はなるべく荷物を押す回数を少なくして荷物を目的の位置に動かすことが目的となる。 236 | 237 | グラフ探索問題として倉庫番を考えるときに重要であるのは、倉庫番は\define{不可逆なアクション}{irreversible action}{ふかぎゃくなアクション}が存在することである。 238 | 全てのアクション$a \in A$に対して$a^{-1} \in A$が存在し、$a(a^{-1}(s)) = s$かつ$a^{-1}(a(s)) = s$となる場合、問題は\define{可逆}{reversible}{かぎゃく}であると呼ぶ。 239 | 例えばグリッド経路探索やスライディングタイルは可逆である。 240 | 可逆な問題は対応するアクションのコストが同じであれば無向グラフとしてモデルすることもでき、初期状態から到達できる状態は、すべて初期状態に戻ることが出来る。 241 | 一方、不可逆な問題ではこれが保証されず、デッドエンドにはまる可能性がある (\ref{sec:difficulity}節)。 242 | 243 | 倉庫番では荷物を押すことは出来ても引っ張ることが出来ないため、不可逆な問題である。例えば、荷物を部屋の隅に置いてしまうと戻すことが出来ないため、詰み状態に陥る可能性がある問題である。 244 | このような性質を持つ問題では特にグラフ探索による先読みが効果的である。 245 | 246 | 倉庫番のもうひとつ重要な性質は\define{ゼロコストアクション}{zero-cost action}{ゼロコストアクション}の存在である。ゼロコストアクションはコストが0のアクションである。%TODO 247 | 倉庫番のアクションのうち{\tt move-up}, {\tt move-left}, {\tt move-down}, {\tt move-right}はコストゼロ($w(e)=0$)のアクションである。ヘタなアルゴリズムを実行すると無限に無駄なアクションを繰り返し続けるということもありうるだろう。 248 | 249 | 250 | \subsection{巡回セールスパーソン問題 (Traveling Salesperson Problem, TSP)} 251 | 252 | \begin{figure} 253 | \centering 254 | % \includegraphics[bb=0 0 328 352,width=0.5\textwidth]{figures/tsp.eps}%TODO 255 | \begin{tikzpicture}[scale=0.7] 256 | \input{figures/tsp.tex} 257 | \end{tikzpicture} 258 | \caption{巡回セールスパーソン問題} 259 | \label{fig:tsp} 260 | \end{figure} 261 | 262 | 263 | 巡回セールスパーソン問題は、地図上にある都市を全て回り最初の都市に戻るときの最短距離の経路を求める問題である \cite{applegate2006traveling}。 264 | $n$個の都市があるとすると(非最適解含む)解の数は$(n-1)!/2$個である。 265 | 可能なアクションは「都市$i \in \{1..n\}$を訪れる」であり、一度訪れた都市には行けない。 266 | TSPのゴール条件はすべての都市を訪れることである。よって、何も考えずに$n$回アクションを実行すれば、とりあえず解を得ることが出来る。しかし最適解を得ることは難しく、NP完全問題であることが知られている。 267 | TSPはヒューリスティック探索に限らず、様々なアプローチで研究されている実用的に非常に重要なドメインである\cite{applegate2006traveling}。TSPについて特に詳しく知りたい方はそちらの教科書を参照されたい。 268 | 269 | 270 | \section{問題の性質・難しさ} 271 | \label{sec:difficulity} 272 | 273 | 本書で定義した状態空間問題は小さなモデルである。 274 | 完全情報であり、状態遷移は決定論的である。 275 | % 最小コスト経路探索は 276 | それでもNP困難問題であり、難しい問題は難しい。 277 | この節は問題の難しさがどのような要素に左右されるかを列挙する。 278 | 279 | \begin{enumerate} 280 | \item {\bf 状態空間の大きさ} 281 | 282 | 状態空間の大きさ$|S|$は大きい程概して問題は難しくなる。 283 | 特に状態空間が無限である場合深さ優先探索などのアルゴリズムは停止しない場合がある。 284 | 例えば状態変数に実数が含まれる場合、状態空間の大きさは無限になる。 285 | % 状態空間の大きさがそのまま問題の難しさに直結するわけではない。 286 | 287 | \item {\bf 分枝度} 288 | 289 | ある状態$s$の\define{分枝度}{branching factor}{ぶんしど}はそのノードの子ノードの数を指す。 290 | 特に状態空間問題の分枝度は、すべての状態の分枝度の平均を指す。ただし多くの場合平均を厳密に求めることはなく、おおよその平均を指して分枝度をすることが多い。 291 | 分枝度が大きいほど問題は難しいとは限らない。 292 | 分枝度が多いほどグラフが密である、つまりエッジの数が多いことに対応する。 293 | %エッジが多い程解の候補となる経路が多くなる。 294 | 分枝度を$b$とすると、あるノード$s$の子ノードの数は$b$個であり、孫ノードの数は$b^2$である。$s$からの深さ$d$のノードは$b^d$個である。 295 | 296 | \item {\bf デッドエンド} 297 | 298 | 問題によってはある状態に到達するともう問題を解くことは出来ないというシチュエーションがある。例えば倉庫番は荷物を角においてしまうともう動かすことができない。これによってもう問題がクリアできなくなるということがある。このような問題では状態空間を広く探索し、デッドエンド状態のみを探索し続けるということをうまく避ける必要がある。例えば\ref{sec:greedy-best-first-search}節の貪欲最良優先探索はデッドエンドに入ってしまうとなかなか抜け出せないかもしれない。 299 | % あるいはデッドエンドを検知する手法も考えられる。 300 | 概してデッドエンドがある問題では状態空間を広く探索する手法、探索済みの状態を記録する手法が有利であり、局所探索手法はうまくいかないことがある。 301 | %このような問題では局所探索アルゴリズムではうまくいかないことがある。局所探索アルゴリズムは 302 | 303 | \item {\bf 解の存在} 304 | 305 | 当然解が存在しない問題もありうる。 306 | 本書で紹介するアルゴリズムは解が存在しない場合非常に時間がかかるものが多い\footnote{一般にどのようなアルゴリズムを使っても解が存在しない状態空間問題は難しい。}。 307 | 一部のアルゴリズムは解が存在しない場合永遠に停止しない場合がある。 308 | そのような場合、アルゴリズムは解が存在しないと示せれば理想的である。 309 | そのため解が存在しないことを検出するアルゴリズムの研究もされている \cite{backstrom2013fast,hoffmann2014distance}。 310 | 311 | \end{enumerate} 312 | 313 | \section{Python実装} 314 | 315 | 状態空間問題$P = (S, A, s, T, w)$のインターフェイスをPythonで実装すると以下のように書ける。 316 | 317 | \inputpython{python/state_space_problem.py}{1}{21} 318 | 319 | 状態空間問題の構成要素とPythonの実装は以下のように対応している。 320 | 321 | \begin{itemize} 322 | \item 状態集合$S$ \\ 323 | 状態集合は明示的にコード中には現れない。 324 | \item 行動集合$A$: \code{get\_available\_actions} \\ 325 | 状態に対してそれに適用できる行動の集合を返す関数$S \rightarrow A$として表現される。 326 | \item 初期状態$s$: \code{get\_init\_state} \\ 327 | 初期状態を返す関数。 328 | \item 遷移関数$T$: \code{get\_next\_state} \\ 329 | 状態と行動を受け取り、次の状態を返す関数$S, A \rightarrow S$として表現される。 330 | \item コスト関数$w$: \code{get\_action\_cost} \\ 331 | 行動を受け取り、その行動のコストを返す関数$A \rightarrow \mathbb{R}$として表現される。 332 | \end{itemize} 333 | 334 | このクラスを親としてグリッド経路探索問題を実装すると、以下のようなコードになる。 335 | 336 | \inputpython{python/grid_pathfinding.py}{1}{63} 337 | 338 | 本書で扱う探索アルゴリズム含めほとんどの状態空間問題の解法は以上の構成要素を含んでいればこのインターフェイスに対応して実装が出来る。 339 | 340 | 341 | \section{まとめ} 342 | 343 | {\it 状態空間問題}は初期状態とゴール条件が与えられたとき、ゴール条件を満たすための{\it プラン} (行動の列) を見つける問題である。状態空間問題はグラフ上の経路探索問題に帰着できる。このときのグラフを{\it 状態空間グラフ}と呼ぶ。 344 | 状態空間グラフを初期ノードとそのノードと隣接するノードを返す{\it ノード展開関数}によって表現することができる。これを{\it 非明示的状態空間グラフ}と呼ぶ。 345 | 346 | 状態空間問題ではしばしば行動に{\it コスト}が定義されていて、プラン全体の行動コストの総和を最小化することが求められる。 347 | 特に全ての行動のコストが同じである場合は{\it ユニットコスト状態空間問題}と呼ぶ。 348 | 349 | \section{練習問題} 350 | 351 | \begin{enumerate} 352 | \item ルービックキューブを状態空間問題で表現するとする。このとき、何を状態とするべきか?何を行動とするべきか? 353 | 354 | \item スライディングタイル問題でタイルの大きさが$3 \times 3$の時の状態の数はいくつか?$4 \times 4$の時は? 355 | (ヒント: スライディングタイルには偶奇性があるため、ゴール状態に到達できない盤面がある。) 356 | 357 | \item 障害物のない5x5の2次元空間上で上下左右に移動が出来るグリッド経路探索問題を考える。この状態空間における分枝度はいくつか? 358 | 359 | \item (難問) 3x3のスライディングタイル問題における分枝度はいくつかを正確に計算することはできるか? 360 | 361 | \item (難問) 状態空間問題として表現することが難しい問題の例を2, 3挙げてみよう。それらの問題を表現するにはどのように状態空間問題を拡張するべきか? 362 | \end{enumerate} 363 | 364 | 365 | \section{関連文献} 366 | 367 | 本章で扱うヒューリスティック探索は主にここで定義した状態空間問題への適用を前提として書かれている。 368 | そのため状態空間問題よりも広い問題を解くためには少し工夫が必要になることが多い。 369 | 370 | 状態空間問題は完全情報であり状態遷移が決定論的であることを仮定した。 371 | 状態遷移が決定論的ではなく確率的であると仮定したモデルは\define{マルコフ過程問題}{Markov Decision Process Problem}{マルコフかていもんだい} (MDP)と呼ばれている \cite{puterman2014markov}。 372 | MDP$(S, A, P, R)$は状態集合$S$、アクション集合$A$、状態遷移確率$P$、報酬$R$からなる。 373 | 状態空間問題は状態遷移が決定論的であるのに対してMDPのそれは確率的である。 374 | また、MDPはコストを最小化するのではなく報酬の総和の期待値を最大化する問題として定義されることが多い。 375 | 状態空間問題はMDPのうち状態遷移が常に決定的であるものである。 376 | MDPは\define{強化学習}{reinforcement learning}{きょうかがくしゅう}における問題モデルとしても広く使われている \cite{sutton:99}。 377 | 状態遷移が決定論的であれば、状態遷移は一つの状態から一つの状態へのエッジによって表すことができる。状態遷移が確率的である場合は可能な次状態を列挙するAND-OR木 \cite{luger1997artificial}でモデルすることが出来る。 378 | MDPを解くには動的計画法 \cite{puterman2014markov,barto1995learning}やモンテカルロ木探索 \cite{browne2012survey,kocsis2006bandit}が使われることが多い。 379 | 380 | MDPからさらに不完全情報問題に拡張したものを\define{部分観測マルコフ過程問題}{partially observable Markov decision process problem}{ぶぶんかんそくマルコフかていもんだい} (POMDP)と呼ぶ \cite{kaelbling1998planning}。 381 | POMDPにおけるプランニング問題の厳密解はBelief space \cite{kaelbling1998planning}上を探索することで求められるが、多くの場合厳密計算は困難なのでモンテカルロ木探索などの近似手法が用いられる。 382 | POMDPは非常に計算が難しいモデルであるので、仮にPOMDPが与えられた問題をもっとも正確に表せられるモデルであったとしても近似的にMDPを使った方がうまくいくかもしれない。 383 | 384 | 囲碁やチェスなどの敵対するエージェントがいる問題も探索が活躍するドメインであり、特に二人零和ゲームでの研究が盛んである。 385 | このような問題では敵プレイヤーの取るアクションが事前に分からないのでAND-OR木でモデルすることが多い。 386 | 敵対ゲームを解くための手法としてはMiniMax木、$\alpha$-$\beta$木\cite{pearl84}やモンテカルロ木探索などがある。近年では強化学習によってヒューリスティック関数を学習するアプローチが盛んに研究されている \cite{sutton:99}。 387 | 388 | ヒューリスティック探索はゲームやロボティクスの応用の中で現れる経路探索問題を解くために特によく使われる。 389 | ゲームに探索を使う場合問題になるのは計算にかかる時間である。 390 | たとえば次の一手を計算するために一日以上かかるチェスAIとプレイしたい人は少ないだろう。 391 | このような応用では解のコストだけでなく、解を得るために使う時間も評価しなければならない。 392 | このような問題をリアルタイム探索と呼ぶ \cite{korf90,barto1995learning}。 393 | リアルタイム探索にはLearning real-time A* (LRTA*) \cite{korf90,koenig2001minimax}やReal-time adaptive A* (RTAA*) \cite{koenig2006real}などが使われる。 394 | 395 | 本書ではノードの次数が有限 (局所有限グラフ)である問題を対象としたアルゴリズムを紹介している。 396 | 一見自然な仮定だが、この仮定ではアクションや状態空間の大きさが無限である連続空間問題を解くことができない。そのためロボティクスで現れる連続空間問題を解くためには工夫が必要になる。 397 | 例えばグリッド経路探索問題で任意の角度を取ることが出来るAny-angle path planning問題は本書では扱わないが、ヒューリスティック探索の派生アルゴリズムで解くことができる\cite{lavalle2006planning,lavalle2001rapidly,daniel2010theta,nash2013any}。 398 | 399 | 状態空間問題は制約充足問題の一種であり、Boolean satisfiability problem (SAT)として書くことができる \cite{garey1979computers}。それを利用してSATによって状態空間問題を解くアプローチも広く研究されている \cite{kautz1992planning,kautz2006satplan,rintanen2012planning}。状態空間問題では表現することが難しい制約や目的関数がある場合は制約充足問題や離散最適化問題としてモデルすることもできる。 400 | -------------------------------------------------------------------------------- /blind.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3 | %%% CHAPTER: Blind Search 4 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5 | \chapter{情報なし探索 (Blind Search)} 6 | \label{ch:blind-search} 7 | 8 | \ref{ch:introduction}章では様々な状態空間問題を紹介したが、それぞれの問題の解法はどれも沢山研究されている。 9 | 一つの指針としては、ある問題に特化した解法を研究することでその問題をより高速に解くというモチベーションがある。 10 | これは例えばMSAのように重要なアプリケーションがある問題の場合に特に熱心に研究されることが多い。 11 | 一方、なるべく広い範囲の問題に対して適用可能な手法を研究するというモチベーションもある。 12 | {\bf この章で紹介する手法は問題特化のアルゴリズムよりもパフォーマンスに劣るが、問題の知識をあまり必要とせず、さまざまな問題に適用できる}。 13 | %ただしこのような手法はドメインに特化したプログラムと比べてパフォーマンスに劣ることが多い。 14 | 15 | \ref{ch:introduction}章で紹介した状態空間問題を広く扱うことの出来る手法としてグラフ探索アルゴリズムがある。 16 | 本章では最もシンプルな問題(ドメイン)の知識を利用しない探索を紹介する。 17 | 情報なし探索 (Blind Search)は状態空間グラフのみに注目し、背景にある問題に関する知識を一切使わないアルゴリズムである。 18 | このような探索では{\bf 1. 重複検知を行うか 2. ノードの展開順序}が重要になる。 19 | 重複検出は訪問済みの状態を保存しておくことで同じ状態を繰り返し探索することを防ぐ手法である。対価としては、メモリの消費量が非常に大きくなることにある。 20 | ノードの展開順序とは、例えば幅優先探索・深さ優先探索などのバリエーションを指す。 21 | 効率的な展開順序は問題によって大きく異なり、問題を選べばこれらの手法によって十分に効率的な探索を行うことが出来る。 22 | これらの探索手法は競技プログラミングでもよく解法として使われる\cite{skiena2006programming}。また、いわゆるコーディング面接でもグラフ探索アルゴリズムは頻出である\cite{mcdowell2011cracking}。 23 | %\ref{ch:search-performance}章はグラフ探索の高速化の紹介をするので、特に競技プログラミングに興味がある場合はそちらも参照されたい。% TODO 24 | 情報なし探索は\cite{cormen01}の22章Elementary Graph Algorithmsにも詳しく説明されている。 25 | 26 | 27 | \section{木探索アルゴリズム (Tree Search Algorithm)} 28 | \label{sec:tree-search-algorithm} 29 | 木探索アルゴリズムはグラフ探索アルゴリズムの基礎となるフレームワークであり、本文で紹介する手法のほとんどがこのフレームワークを基礎としているといえる。 30 | アルゴリズム\ref{alg:implicit-tree-search}は木探索の疑似コードである。 31 | 32 | \begin{algorithm}[h] 33 | \caption{木探索 (Implicit Tree Search)} 34 | \label{alg:implicit-tree-search} 35 | \Input{非明示的状態空間グラフ $(s, Goal, Expand, w)$, プライオリティ関数 $f$} 36 | \Output{$s$からゴール状態への経路、経路が存在しなければ$\emptyset$} 37 | $Open \leftarrow \{s\}$, $d(s) \leftarrow 0$, $g(s) \leftarrow 0$\; 38 | \While{$Open \neq \emptyset$} { 39 | $u \leftarrow \argmin_{u' \in Open} f(u')$ \; 40 | $Open \leftarrow Open \setminus \{u\} $\; 41 | \If {$Goal(u)$} { 42 | \Return $Path(u)$\; 43 | } 44 | \For {each $v \in Expand(u)$} { 45 | $Open \leftarrow Open \cup \{u\}$\; 46 | $d(v) \leftarrow d(u) + 1$\; 47 | $g(v) \leftarrow g(u) + w(u, v)$\; 48 | $parent(v) \leftarrow u$\; 49 | } 50 | } 51 | \Return $\emptyset$\; 52 | \end{algorithm} 53 | 54 | 木探索はオープンリスト\footnote{歴史的な経緯でリストと呼ばれているが、データ構造がリストで実装されるという意味ではない。効率的なデータ構造は\ref{ch:search-performance}章で紹介する。}と呼ばれるノードの集合をPriority queueに保持する。探索の開始時には、初期状態のみがオープンリストに入っている。 55 | 木探索は、このオープンリストから一つノード$u$を選び、ゴール条件を満たしているかを確認する。満たしていれば初期状態から$u$への経路を返す。満たしていなければ、そのノードを展開する。展開とは、そのノードの子ノードを列挙し、オープンリストに入れることを指す。 56 | 57 | \begin{figure} 58 | \centering 59 | \begin{tikzpicture}[scale=0.6] 60 | \input{figures/nodetypes.tex} 61 | \end{tikzpicture} 62 | \caption{未生成・生成済み・展開済みノードの例。木探索は生成済みノードのうち展開済みでないものをひとつ取り出し、そのノードを展開し子ノードを得る。新しく得られた子ノードは未生成ノードから生成済みノードとなり、展開したノードは展開済みノードになる。} 63 | \label{fig:node-life} 64 | \end{figure} 65 | 66 | 各ノードの注目すると、ノードは1. 未生成、2. 生成済み、3. 展開済みと状態が遷移していく。 67 | 68 | \begin{enumerate} 69 | \item 未生成ノード (Undiscovered nodes): 状態空間内のまだ生成されていないノード。非明示的グラフでは情報は保持されていない。 70 | \item 生成済みノード (Generated nodes): オープンリストに一度でも入れられたノード。後述するグラフ探索ではクローズドリストに入れられる。 71 | \item 展開済みノード (Expanded nodes): $Expand$を実行し終えたノード。子ノードがすべて生成済みノードになる。オープンリストからは取り除かれる。 72 | \end{enumerate} 73 | 74 | 図\ref{fig:node-life}は探索アルゴリズムが$\{a, b, c\}$を展開し終えた時点でのノードの分類の例である。 75 | $\{a, b, c\}$は展開済みノードであり、同時に生成済みノードである。これらのノードは後述するグラフ探索ではクローズドリストと呼ばれるデータ構造に入れられ、保持される。これらのノードの子ノードがすべて生成される。$\{d, e, f\}$は生成済みノードであるが展開済みではない。探索アルゴリズムのオープンリストにはこれらのノードが入っている。$\{g, h, i, j\}$は未生成のノードであり、アルゴリズムからは見えない未発見のノードである。 76 | 例えば次に$d$を展開すると、展開済みノードは$\{a, b, c, d\}$となり、新たに$\{g, h\}$が未生成ノードから生成済みノードに遷移する。このように探索が進行していくことによってノードは順次状態遷移していく。 77 | 78 | 79 | 初期状態からノード$n$への最小ステップ数を深さ$d$と呼び、最小経路コストを$g$値と呼ぶ。すべてのアクションのコストが1のドメインであれば任意の$n$に対して$d(n) = g(n)$が成り立つ。 80 | 状態を更新すると同時に$g$値を更新する。これによって解を発見した時に解ノードの$g$値が解のコストとなる。 81 | なお、状態$s$に対して適用可能なアクションの集合$A(s)$は与えられていると仮定する。 82 | 83 | $Path(u)$はノードに対して初期状態からそこへ到達するまでの経路を返す関数である。$(u, parent(u), parent(parent(u)),...,s)$のように再帰的に$parent$を初期状態まで辿る。 84 | 85 | {\bf 紛らわしいが、木探索アルゴリズムは木だけでなくグラフ一般を探索するアルゴリズムである。} 86 | 木探索の強みは生成済みノードのうち展開済みではないもののみをオープンリストに保持すればよいことにある。未生成ノード、展開済みノードはメモリ上に保持する必要がない。 87 | 一方これの問題は、一度展開したノードが再びExpandによって現れた場合\define{再展開}{reexpansion}{さいてんかい}をすることになる。図\ref{fig:reexpansion}の例ではノード$d$に二通りの経路で到達できるが、木探索では二回目に$d$に到達したとき、すでに到達済みであることを把握できない。そのため同じノードを再び生成・展開することになる。 88 | このように木探索は複数の経路で到達可能なノードがあるほど(グラフがより木から遠いほど)同じノードを何度も再展開することになり、効率が悪くなってしまう。 89 | 90 | また、木探索アルゴリズムは状態数が有限であっても停止しない場合がある。 91 | これらが問題になるような問題ドメインである場合は後述する重複検出を使うグラフ探索 (\ref{sec:graph-search-algorithm}節)を使うと良いだろう。 92 | 93 | 94 | \begin{figure} 95 | \centering 96 | \begin{tikzpicture}[scale=0.5] 97 | \input{figures/reexpansion.tex} 98 | \end{tikzpicture} 99 | \caption{ノードの再展開の例} 100 | \label{fig:reexpansion} 101 | \end{figure} 102 | 103 | 104 | オープンリストはプライオリティキューであり、どの順番でノードを取り出すかを決めなければならない。プライオリティ$f$の選択は探索アルゴリズムの性能に強く関係する。 105 | 本章の\ref{sec:breadth-first-search}節以降、及び\ref{ch:heuristic-search}章はこのプライオリティをどうデザインするかについて議論をする。 106 | 107 | %木探索ベースのアルゴリズムの問題は、解が存在しない場合に停止性を満たさないことである。よって、この手法は解が間違いなく存在することが分かっている問題に対して適用される。あるいは、解が存在することを判定してから用いる。 108 | 109 | 110 | \section{グラフ探索アルゴリズム (Graph Search Algorithm)} 111 | \label{sec:graph-search-algorithm} 112 | 113 | 114 | 明示的グラフのあるノードが初期状態から複数の経路でたどり着ける場合、同じ状態を表すノードが木探索による非明示的グラフに複数現れるということが生じる。このようなノードを\define{重複}{duplicate}{ちょうふく}と呼ぶ。ノードの重複は計算資源を消費してしまうので、効率的な\define{重複検出}{duplicate detection}{ちょうふくけんしゅつ}の方法は重要な研究分野である。 115 | {\bf 本書ではノードの重複検出を行う探索アルゴリズムを狭義にグラフ探索アルゴリズムと呼び、重複検出を行わない探索を狭義に木探索アルゴリズムと呼ぶ。} 116 | 117 | 118 | \begin{algorithm}[tbh] 119 | \caption{グラフ探索 (Implicit Graph Search)} 120 | \label{alg:implicit-graph-search} 121 | \Input{非明示的状態空間グラフ $(s, Goal, Expand, w)$、プライオリティ関数 $f$} 122 | \Output{$s$からゴール状態への経路、経路が存在しなければ$\emptyset$} 123 | $Open \leftarrow \{s\}$, $Closed \leftarrow \{s\}$, $d(s) \leftarrow 0$, $g(s) \leftarrow 0$\; 124 | \While{$Open \neq \emptyset$} { 125 | $u \leftarrow \argmin_{u' \in Open} f(u')$ \; 126 | $Open \leftarrow Open \setminus \{u\} $\; 127 | \If {$Goal(u)$} { 128 | \Return $Path(u)$\; 129 | } 130 | \For {each $v \in Expand(u)$} { 131 | \If{$v \notin Closed$ {\bf or} $g(u) + w(u, v) < g(v)$} { 132 | $Open \leftarrow Open \cup \{v\}$\; 133 | $d(v) \leftarrow d(u) + 1$\; 134 | $g(v) \leftarrow g(u) + w(u, v)$\; 135 | $parent(v) \leftarrow u$\; 136 | } 137 | \If{$v \notin Closed$} { 138 | $Closed \leftarrow Closed \cup \{v\}$\; 139 | } 140 | } 141 | } 142 | \Return $\emptyset$\; 143 | \end{algorithm} 144 | 145 | 146 | 重複検出のためには生成されたノードを\define{クローズドリスト}{closed list}{クローズドリスト}に保存する。一度クローズドリストに入れられたノードはずっとクローズドリストに保持される。 147 | ノード展開関数から子ノードが生成されたら、その子ノードと同じ状態を保持するノードがクローズドリストに存在するかを確認する。 148 | もし存在しなければ、そのノードは重複ではない。なのでそのノードをオープンリストに加える。 149 | 存在した場合の処理は少しややこしい。 150 | 新たに生成されたノード$n$の$g$値のほうが先に生成されクローズドリストにあるノード$n'$の$g$値よりも小さい場合が存在する。このとき、$n$をそのまま捨ててしまうと、そのノードの$g$値が本来の値よりも大きく評価されてしまう。 151 | 152 | $g$値をそのノードに到達できる既知の最小コストにするためには、まずクローズドリストに保存されているノードの$g$値を$g(n')$から$g(n)$に更新しなければならない。 153 | 加えて、ノード$n$を\define{再展開}{reexpansion}{さいてんかい}しなければならない。 154 | ノード$n$の子ノード$c$は$n'$の子ノードとして展開されていたわけであるが、そのとき$g(c) = g(n') + w(n', c)$として計算された。この値は$g(c) = g(n) + w(n, c)$に更新しなければならない。$w(n', c) = w(n, c)$なので、$g(n') - g(n)$だけ$g$値が小さくなる。なので、$c$の子ノードも再展開をする必要がある。そしてそのまた子ノードも。。。というように、再展開が生じるとそこから先のノードをすべて再展開する必要がある。これはかなり大きなコストになることが多いので、可能な限り避けたい処理である。 155 | 156 | %なので常に$g$値をそのノードに到達できる既知の最小コストに更新する。 157 | 158 | 重複が存在した場合に必ずノードを捨てることができる場合も存在する。 159 | まず、解の最適性が必要でない場合$g$値を更新する必要はない。$g$値が過大に評価されても解経路は解経路のままであり、ただ解経路のコストが大きくなるだけである。 160 | また、例えば幅優先探索では探索の過程で生成されるノードの$d$値は単調増加する。もしユニットコストドメインならば$g$値も単調増加である。つまりノード$n$と重複したノード$n'$がクローズドリストにあったとすると、$g(n) \geq g(n')$が成立する。この場合、解最適性を保ったまま$n$を安全に捨てることができる。 161 | また、状態空間グラフが木である場合は重複が発生しない。 162 | なお、後述するA*探索\ref{sec:astar-search}ではある条件を満たせば再展開は行わずに解の最適性が満たせることが知られている。これがA*探索がstate-of-the-artとして重要視されている理由である。 163 | 164 | ここで「ノード」と「状態」の言葉の使い分けに注意したい。 165 | 状態とは状態空間問題における状態$s$である。ノードは状態$s$を含み、$f$値、$g$値の情報を含む。 166 | 重複検出を行わない木探索の場合、同じ状態を保持するノードが2つ以上存在しうる。 167 | 重複検知は同じ状態を保持するノードをマージする処理に相当する。この処理を行うと同じノードに複数の経路で到達するようになり、グラフは木ではなくなる。 168 | 169 | % 重複検出を行ってもノードの再展開が必要になる場合は存在するが、ほとんどの場合重複検出を行わない場合よりもはるかに再展開の回数は少なくなる。 170 | % 実行時間で見ると重複検出を行ったほうがほぼ確実に効率的である。 171 | 重複検出の問題はメモリの使用量である。重複検出を行うためには生成済みノードをすべてクローズドリストに保存しなければならない。なので展開済みノードの数に比例した空間が必要になる。 172 | クローズドリストの効率的な実装については\ref{sec:closed-list}節で議論をする。 173 | 174 | なお、重複検出はノードが生成されたときではなく、ノードが展開されるときに遅らせることができる。 175 | オープンリストには重複したノードが含まれることになるが、ノードの展開時には重複をチェックするので重複したノードの展開は防げる、ということである。これは\define{遅延重複検出}{delayed duplicate detection}{ちえんちょうふくけんしゅつ}と呼ばれ、\ref{sec:delayed-duplicate-detection}節で議論をする。 176 | 177 | 178 | 179 | \section{幅優先探索 (Breadth-First Search)} 180 | \label{sec:breadth-first-search} 181 | 182 | 探索のパフォーマンスにおいて重要になるのは{\bf どのようにして次に展開するノードを選択するか}にある。 183 | ヒューリスティック探索の研究の非常に大きな部分はここに費やされているといえる。 184 | シンプルかつ強力なノード選択方法はFirst-in-first-out (FIFO)である。あるいは幅優先探索と呼ぶ。 185 | 186 | 幅優先探索の手順は非常に単純であり、FIFOの順に$Open$から取り出せばいいだけである。 187 | これをもう少し大きな視点で、{\it どのようなノードを優先して探索しているのか}を考えてみたい。 188 | 初期状態から現在状態にたどり着くまでの経路の長さをノードの$d$値と定義する。 189 | すると、幅優先探索のプライオリティ関数$f$は$d$値と一致する。 190 | 191 | \begin{equation} 192 | f_{\text{brfs}}(s) = d(s) 193 | \label{alg:brfs-open} 194 | \end{equation} 195 | 196 | ユニットコスト問題である場合、更に$g$値とも一致する ($f_{\text{brfs}}(s) = d(s) = g(s)$)。 197 | 198 | 幅優先探索のメリットは最初に発見した解が最短経路長の解であることである。 199 | 問題がユニットコストドメインであれば、最短経路が最小コスト経路であるので、最適解が得られる。 200 | なお、後述するBest First Searchと区別するため、Breadth-First Searchの略称はBrFSを用いることがある (Best First SearchはBFSとなる)。 201 | 202 | 重複検出を用いた幅優先探索で図\ref{fig:ssp-graph}の問題を解こうとすると、オープンリスト、クローズドリストの中身は表\ref{tbl:brfs-traj}のように遷移する。 203 | 図\ref{fig:ssp-tree}の探索木を見比べながら確認してみてほしい。 204 | 205 | \begin{figure} 206 | \centering 207 | \begin{tikzpicture}[scale=0.5] 208 | \input{figures/statespace.tex} 209 | \end{tikzpicture} \hspace{20pt} 210 | \begin{tikzpicture}[scale=0.5] 211 | \input{figures/pathtree.tex} 212 | \end{tikzpicture} 213 | \caption{探索木} 214 | \label{fig:ssp-tree} 215 | \end{figure} 216 | 217 | 218 | \begin{table}[tbh] 219 | \centering 220 | \caption{重複検出を用いた幅優先グラフ探索のオープンリスト・クローズドリスト (\cite{edelkamp:2010:hst:1875144}より)} 221 | \begin{tabular}{c|c|l|l|l} 222 | \toprule 223 | ステップ & ノードの選択 & オープンリスト & クローズドリスト & コメント \\ \midrule 224 | 1 & \{\} & \{a\} & \{\} \\ 225 | 2 & a & \{b,c,d\} & \{a\} \\ 226 | 3 & b & \{c,d,e,f\} & \{a,b\} \\ 227 | 4 & c & \{d,e,f\} & \{a,b,c\} \\ 228 | 5 & d & \{e,f,g\} & \{a,b,c,d\} \\ 229 | 6 & e & \{f,g\} & \{a,b,c,d,e\} \\ 230 | 7 & f & \{g\} & \{a,b,c,d,e,f\} \\ 231 | 8 & g & \{\} & \{a,b,c,d,e,f,g\} & ゴール \\ 232 | \bottomrule 233 | \end{tabular} 234 | \label{tbl:brfs-traj} 235 | \end{table} 236 | 237 | \section{深さ優先探索 (Depth-First Search)} 238 | \label{sec:depth-first-search} 239 | 240 | 幅優先探索が幅を優先するのに対して深さ優先探索はもっとも深いノードを優先して探索する。 241 | 242 | \begin{equation} 243 | f_{\text{dfs}}(s) = -d(s) 244 | \label{alg:dfs-open} 245 | \end{equation} 246 | 247 | 深さ優先探索は解がある一定の深さにあることが既知である場合に有効である。 248 | 例えばTSPは全ての街を回ったときのみが解であるので、街の数が$n$であれば全ての解の経路長が$n$である。 249 | このような問題を幅優先探索で解こうとすると、解は最も深いところにしかないので、最後の最後まで解が一つも得られないということになる。一方、深さ優先探索なら$n$回目の展開で一つ目の解を見つけることが出来る。 250 | 表\ref{tbl:dfs-traj}は図\ref{fig:ssp-graph}の問題で重複検出ありの深さ優先探索を行った場合のオープンリスト・クローズドリストの遷移を示した。図\ref{fig:ssp-tree}と合わせてノードが展開される順序を確認すると良い。 251 | 252 | 深さ優先探索は無限グラフにおいて、解が存在しても永遠に停止しない場合がある。幅優先探索であれば解がある場合、いずれそれを発見する (解の深さを$d^*$とすると、$d(s) \leq d^*$であるノードの数は有限であるので)。しかし深さ優先探索は停止しない場合がある。 253 | 254 | 良い解、最適解を見つけたい場合でも深さ優先探索が有用である場合がある。 255 | 早めに一つ解が見つけられると、その解よりも質が悪い解にしかつながらないノードを\define{枝刈り}{pruning}{えだがり}することが出来る。ノード$n$を枝刈りするとは、ノード$n$をオープンリストに加えずそのまま捨てることを指す。つまりアルゴリズム\ref{alg:implicit-tree-search}における$Open \leftarrow Open \cup \{v\}$をスキップする。このような枝刈りを用いた探索アルゴリズムを\define{分枝限定法}{Branch-and-Bound}{ぶんしげんていほう}と呼ぶ。 256 | 257 | \begin{table}[tbh] 258 | \centering 259 | \caption{重複検出を用いた深さ優先グラフ探索のオープンリスト・クローズドリスト (\cite{edelkamp:2010:hst:1875144}より)} 260 | \begin{tabular}{c|c|l|l|l} 261 | \toprule 262 | ステップ & ノードの選択 & オープンリスト & クローズドリスト & コメント \\ \midrule 263 | 1 & \{\} & \{a\} & \{\} \\ 264 | 2 & a & \{b,c,d\} & \{a\} \\ 265 | 3 & b & \{e,f,c,d\} & \{a,b\} \\ 266 | 4 & e & \{f,c,d\} & \{a,b,e\} \\ 267 | 5 & f & \{c,d\} & \{a,b,e,f\} \\ 268 | 6 & c & \{d\} & \{a,b,e,f,c\} \\ 269 | 7 & d & \{g\} & \{a,b,e,f,c,d\} \\ 270 | 8 & g & \{\} & \{a,b,e,f,c,d,g\} & ゴール \\ 271 | \bottomrule 272 | \end{tabular} 273 | \label{tbl:dfs-traj} 274 | \end{table} 275 | 276 | \subsection{再帰による深さ優先探索} 277 | \label{sec:recursive-depth-first-search} 278 | 279 | 上述の実装はオープンリストを利用した深さ優先探索である。 280 | アルゴリズム \ref{alg:implicit-tree-search}を元にした深さ優先探索の実装は効率的ではないことが多い。 281 | 深さ優先探索は再帰によって効率的に実装することができる (アルゴリズム \ref{alg:recursive-dfs})。 282 | 再帰実装ではオープンリストがないことに注目したい。 283 | 再帰を利用することでデータ構造を明に保存する必要がなくなり、キャッシュ効率が良くなることがある。 284 | 幅優先探索も同様に再帰によって実装することが出来るが、効率的であるケースはあまりない。 285 | 286 | \begin{algorithm}[tbh] 287 | \caption{DFS: 再帰による深さ優先探索 (Depth-First Search)} 288 | \label{alg:recursive-dfs} 289 | \Input{非明示的状態空間グラフ $(s, Goal, Expand, w)$、前状態 $s'$} 290 | \Output{$s$からゴール状態への経路、経路が存在しなければ$\emptyset$} 291 | \If {$Goal(s)$} { 292 | \Return $s$\; 293 | } 294 | \For {each $u \in Expand(s) \setminus \{s'\}$} { 295 | $v \leftarrow DFS(u, Goal, Expand, w, s)$\; 296 | \If {$v \neq \emptyset$} { 297 | \Return $(s, DFS(v, Goal, Expand, w, u))$ 298 | } 299 | } 300 | \Return $\emptyset$\; 301 | \end{algorithm} 302 | 303 | 304 | \section{ダイクストラ法 (Dijkstra Algorithm)} 305 | \label{sec:dijkstra} 306 | 307 | \define{ダイクストラ法}{Dijkstra's Algorithm}{ダイクストラほう}はグラフ探索アルゴリズムの一種であり、グラフ理論の教科書な 308 | どでも登場する情報科学全体に多岐に渡り重要とされるアルゴリズムである \cite{dijkstra1959note}。 309 | 例えばネットワークルーティングにおけるlink state algorithmなどにDijkstraが使われる\cite{mcquillan1980new}。 310 | %初期状態からノード$n$への既知の最小経路コストを$g$値と呼び、$g(n)$と書く。 311 | ダイクストラ法はグラフ探索において$g$値が最も小さいノードを優先して展開するアルゴリズムと説明することができる。 312 | 313 | \begin{equation} 314 | f_{\text{bfs}}(s) = g(n) 315 | \end{equation} 316 | 317 | {\bf ダイクストラ法は重複検出を行うグラフ探索アルゴリズムである。} 318 | ダイクストラ法は非負コストグラフにおいて最短経路を返す。 319 | ユニットコストドメインでは$\forall n (g(n) = d(n))$であるため、幅優先探索と同じ動作をする。 320 | フィボナッチヒープを用いてオープンリストを実装したダイクストラ法は$O(|E| + |V|log|V|)$時間でであることが知られている\cite{fredman1987fibonacci}。 321 | そのため、後述するヒューリスティック関数が得られない問題においてはとりあえずダイクストラ法を試してみることは有効である。 322 | 323 | 324 | \section{情報なし探索の比較} 325 | \label{sec:blind-comparison} 326 | 327 | 探索アルゴリズムの評価指標としては以下の四点が重要である。 328 | 329 | \begin{enumerate} 330 | \item 完全性: 解が存在するとき、有限時間内に解を返すか。 331 | \item 最適性: 最初に発見された解が最適解か。 332 | \item 時間: アルゴリズムの実行にかかる時間 333 | \item 空間: アルゴリズムの実行にかかる空間 334 | \end{enumerate} 335 | 336 | 完全なアルゴリズムであることは有用だが、時間・空間とのトレードオフにあることが多い。 337 | 完全であっても実現可能な時間・空間で実行することが出来なければ意味がないので、完全でない高速なアルゴリズムを選択する方が良い場合もある。 338 | 最適性も同様に時間・空間とのトレードオフにあることが多いので、解きたい問題に最適な解が必要かどうかを考える必要がある。 339 | また、後述するwA* (節\ref{sec:weighted-astar-search})は発見された解が最適解のコスト$c^*$の定数倍以下であることを保証する近似アルゴリズムである。 340 | 341 | 342 | 表\ref{tbl:comparison}は情報なし探索をこれら四点で比較している。 343 | 反復深化深さ優先は節\ref{sec:depth-first-iterative-deepening}、両方向探索は節\ref{sec:bidirectional-search}で紹介する。 344 | $b$は分枝数、$d$は解の深さ、$m$は可能な探索木の深さの最大値である。 345 | 重複検出を行うグラフ探索の場合、深さ優先探索は有限グラフならば完全であり、時間・空間は状態空間の大きさ$|S|$でバウンドされる。 346 | この表にある時間・空間量は理論的な最悪の場合の比較である。解きたい問題にかかる平均的な性能は必ずしもこれに相関しない。最終的には実験的なベンチマークを行い良いアルゴリズムを選択する必要がある。 347 | 348 | \begin{table}[tbh] 349 | \centering 350 | \caption{木探索アルゴリズムの比較 (\cite{russelln03}のFigure 3.21より)} 351 | \resizebox{\textwidth}{!}{ 352 | \begin{tabular}{c|cccc} 353 | \toprule 354 | 性能 & 幅優先探索 & 深さ優先探索 & 反復深化深さ優先 & 両方向幅優先探索 \\ \midrule 355 | 完全性 & 局所有限グラフならYes & No & 局所有限グラフならYes & 局所有限グラフならYes \\ 356 | 最適性 & ユニットコストならYes & No & ユニットコストならYes & ユニットコストならYes \\ 357 | 時間 & $O(b^d)$ & $O(b^m)$ & $O(b^d)$ & $O(b^{d/2})$ \\ 358 | 空間 & $O(b^d)$ & $O(bm)$ & $O(bd)$ & $O(b^{d/2})$ \\ 359 | \bottomrule 360 | \end{tabular} 361 | } 362 | \label{tbl:comparison} 363 | \end{table} 364 | 365 | 366 | \section{Python実装} 367 | 368 | 情報なし探索は様々な言語でライブラリが開発されているため、参考に出来る実装例が多い。 369 | 本書では理解の助けとなるために紹介することが目的であるため、既存のライブラリは用いない実装を示す。 370 | 371 | Pythonによる木探索は例えば以下のように実装できる。 372 | 373 | \inputpython{python/search_node.py}{1}{25} 374 | 375 | \inputpython{python/tree_search.py}{1}{43} 376 | 377 | \code{TreeSearch}は状態空間問題とプライオリティ関数 \code{priority\_f} を引数に取る。 378 | 379 | このコードに重複検出を加えたグラフ探索は以下のように実装できる。 380 | 381 | \inputpython{python/graph_search.py}{1}{56} 382 | 383 | \code{priority\_f}を適当に渡すことで本章で紹介した幅優先探索、深さ優先探索、ダイクストラ法を実行することができる。 384 | 385 | \inputpython{python/breadth_first_search.py}{1}{4} 386 | \inputpython{python/depth_first_search.py}{1}{4} 387 | \inputpython{python/dijkstra.py}{1}{4} 388 | 389 | そして次章で紹介するヒューリスティック探索の手法も実はこのプライオリティ関数を適切に渡すことで実装ができる。 390 | 391 | 392 | 上記これらのアルゴリズムはイテレーション (反復) によって実装されている。 393 | 394 | 再帰による探索アルゴリズムはコードの構成が異なるため、別途実装を示す。 395 | 再帰による深さ優先探索は以下のように実装できる。 396 | 397 | \inputpython{python/recursive_dfs.py}{1}{46} 398 | 399 | 探索アルゴリズムの性能評価は様々な指標が考えられる。 400 | 多くの場合モニターするべきと考えられるのは以下の指標である。 401 | 402 | \begin{itemize} 403 | \item 探索アルゴリズムの実行時間 (walltime, cputime) 404 | \item 展開されたノード数 405 | \item 生成されたノード数 406 | \item 重複検出・枝刈りされたノード数 407 | \item ノードの展開速度 (生成速度) 408 | \item 重複検出・枝刈りされたノードの割合 409 | \end{itemize} 410 | 411 | これらをモニターするためのユーティリティとして\code{SearchLogger}を実装した。 412 | 413 | \inputpython{python/util.py}{1}{45} 414 | 415 | 探索アルゴリズムの性能が思わしくない場合、その原因がどこにあるかを突き止める必要がある。 416 | まずこれらの指標を確認することはその特定に役立つだろう。 417 | 418 | \section{まとめ} 419 | 420 | 状態空間問題を解くための手法としてグラフ探索アルゴリズムがある。グラフ探索アルゴリズムは1. 展開するノードの順序と2. 展開したノードの処理の2点で特徴づけることが出来る。 421 | {\it 幅優先探索}は{\it オープンリスト}の中から深さが浅いノードから優先して探索し、{\it 深さ優先探索}は逆に深いノードから優先して探索する。{\it ダイクストラ法}はコストが小さいノードから優先して探索するため、最短経路を最初に見つけることができる。 422 | 木探索は展開したノードを保持せず同じ状態を複数回重複して展開してしまうリスクがある。グラフ探索は展開済みノードを{\it クローズドリスト}に保存しておくことで重複展開を防ぐ。一方メモリ消費量が大きくなるというデメリットがある。 423 | 424 | 探索アルゴリズムの選択において考慮するべきことは主に以下の4点である。解が存在するとき、有限時間内に解を返すか ({\it 完全性})、最初に発見された解が最適解か (最適性)、アルゴリズムの実行にかかる時間と空間。 425 | 426 | 427 | \section{練習問題} 428 | 429 | \begin{enumerate} 430 | \item 幅優先木探索を実装し、10x10程度のグリッド経路探索問題を解いてみよう。 431 | 432 | \item 重複検出を使った幅優先探索を実装し、10x10程度のグリッド経路探索問題を解いてみよう。重複検出を使わない幅優先木探索と比べて展開したノードの数は?重複検出によって枝刈りされたノードの数は? 433 | 434 | \item 重複検出を使った深さ優先探索を実装し、10x10程度のグリッド経路探索問題を解いてみよう。見つけた解は1, 2で見つけた解と比べて長いか? 435 | 436 | \item 重複検出を使った幅優先探索で巡回セールスパーソン問題を解いてみよう。この時、見つかった解は最短経路だったか? 437 | 438 | \item ダイクストラ法を実装し、巡回セールスパーソン問題を解いてみよう。この時、見つかった解は最短経路だったか? 439 | 440 | \item 幅優先探索も再帰によって実装することができる。再帰による幅優先探索を実装してみよう。再帰による深さ優先探索と比べてどのような違いがあるか? 441 | \end{enumerate} 442 | 443 | 444 | \section{関連文献} 445 | 446 | 時間・空間制約が厳しい場合は反復深化探索、両方向探索や並列探索を使えば解決できることがある。 447 | これらのアルゴリズムについては\ref{ch:heuristic-search-variants}章で紹介する。 448 | 449 | ダイクストラ法はコストが負のエッジを持つ場合にうまくいかない。 450 | 負のエッジを含む問題を解くための手法としてはベルマン-フォード法が有名である \cite{bellman1958routing,ford1956network}。 451 | 452 | No Free Lunch定理\cite{wolpert1997no}はコンピュータサイエンスの多くの最適化問題で言及される定理である。 453 | 状態空間問題におけるNo Free Lunch定理の主張はざっくりと説明すると以下である。 454 | \dtheorem{ 455 | すべての可能なコスト関数による状態空間問題のインスタンスの集合を考える。 456 | この問題集合に対する平均性能はすべての探索アルゴリズムで同じである。 457 | } 458 | つまり、問題の知識が何もなければ「効率的なアルゴリズム」というものは存在しない。 459 | 問題に対して知っている知識を利用することによってはじめて探索を効率的にすることができる。 460 | すなわち、状態空間問題のインスタンスの一部分で性能を犠牲にすることで、他のインスタンス集合への性能を向上させることができる。例えば解の長さが20以下であるという知識があるとすれば、探索を深さ20までで打ち切ると良いと考えられる。この探索アルゴリズムは解の長さが20以下であるインスタンスに対しての性能は良くなるが、解の長さが20より大きいインスタンスの性能は著しく悪くなる。このように知識を利用して探索の方向性を変えることがグラフ探索では重要になる。 461 | それが次章で扱うヒューリスティック探索の肝である。 462 | 463 | 464 | -------------------------------------------------------------------------------- /heuristic.tex: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5 | \chapter{ヒューリスティック探索 (Heuristic Search)} 6 | \label{ch:heuristic-search} 7 | 8 | \ref{ch:blind-search}章では問題の知識を利用しないグラフ探索手法について解説した。 9 | 本章では問題の知識を利用することでより効率的なグラフ探索を行う手法、特にヒューリスティック探索について解説する。 10 | 11 | \section{ヒューリスティックとは?} 12 | \label{sec:heursitic} 13 | 14 | \begin{figure} 15 | \centering 16 | \begin{tikzpicture}[scale=0.7] 17 | \input{figures/grid-brfs.tex} 18 | \end{tikzpicture} 19 | \caption{グリッド経路探索におけるダイクストラ法} 20 | \label{fig:grid-brfs} 21 | \end{figure} 22 | 23 | 経路探索問題をダイクストラ法で解くことを考えよう。 24 | 図\ref{fig:grid-brfs}のグリッド経路探索問題で(0, 0)の位置から(3, 0)の位置まで移動するため経路を求める問題を考えよう。 25 | このときダイクストラ法が探索していく範囲は図\ref{fig:grid-brfs}の灰色のエリアにあるノードになる。 26 | しかし人間が経路探索を行うときにこんなに広い領域を探索しないだろう。グリッドの右側、ゴールのある方向に向かってのみ探索するだろう。なぜか。 27 | それは人間が問題の特徴を利用して、このノードを展開したほうがよいだろう、このノードは展開しなくてよいだろう、という直感を働かせているからである。 28 | 問題の特徴を利用してノードの{\bf 有望さ}を\define{ヒューリスティック関数}{heuristic function}{ヒューリスティックかんすう}として定量化し、それを探索に利用したアルゴリズムを\define{ヒューリスティック探索}{heuristic search}{ヒューリスティックたんさく}と呼ぶ。 29 | % ヒューリスティック関数は人間が自分の知識を利用してコーディングする場合と、自動的に生成する場合もある。 30 | 31 | % \begin{figure} 32 | % \centering 33 | % \subfloat[幅優先探索]{ 34 | % \includegraphics[width=0.45\textwidth]{figures/grid-brfs.png} 35 | % \label{fig:grid-dijkstra} 36 | % } \hspace{4pt} 37 | % \subfloat[ヒューリスティック探索]{ 38 | % \includegraphics[width=0.45\textwidth]{figures/grid-astar-mdheuristic.png} 39 | % \label{fig:grid-astar-mdheuristic} 40 | % } 41 | % \end{figure} 42 | 43 | 44 | \section{ヒューリスティック関数 (Heuristic Function)} 45 | \label{sec:heuristic-function} 46 | ヒューリスティック関数は状態の有望さの評価値であり、多くの場合その状態からゴールまでの最短距離の見積もりであることが多い \cite{hart68formal}。 47 | 48 | \ddef{ヒューリスティック関数、heuristic function}{ 49 | ヒューリスティック関数$h$はノードの評価関数である。$h: V \rightarrow \mathbb{R}_{\geq 0}$ 50 | } 51 | ヒューリスティックの値が小さいノードほどゴールに近いと推測できるので、探索ではヒューリスティック値が小さいノードを優先して展開する。 52 | ヒューリスティック関数の値をそのノードの$h$値と呼ぶ。 53 | 54 | ヒューリスティック関数の望ましい性質として、まず正確である方が望ましい。すなわち、$h$値が実際のゴールまでの最短距離に近いほど、有用な情報であると言える。 55 | ノード$n$からゴールまでの正しい最短コストを$h^*$とする。 56 | ヒューリスティック関数$h$が任意の$n$に対して$h(n) = h^*(n)$である場合、\define{完璧なヒューリスティック}{Perfect Heuristic}{かんぺきなヒューリスティック}と呼ぶ。完璧なヒューリスティックがある場合、ほとんどの場合その問題を一瞬で解くことができる。現実には完璧なヒューリスティックはなかなか得られないが、ヒューリスティック関数がこれに近いほど必要な展開ノード数が小さいことが多い\cite{helmert:08}。 57 | %完璧なヒューリスティック関数がある場合、A*探索は 58 | 反対に役に立たないヒューリスティック関数は$h(n) = 0$などの定数関数である。これはどのノードに対してもゴールまでの距離が同じだと推測しているということであり、つまり何も主張をしていない。定数関数をヒューリスティックに使った探索を\define{ブラインド探索}{blind search}{ブラインドたんさく}と呼ぶ。\ref{ch:blind-search}章で扱った情報なし探索はヒューリスティック探索の特別な場合と考えることができる。 59 | 60 | 61 | もう一つ望ましい性質は$h$値が最適解コストの下界である場合である。 62 | \ref{sec:astar-search}章で解説するが、$h$値が最短距離の下界である場合、それを用いた効率的な探索アルゴリズム(A*探索、重み付きA*探索)において解コストに理論的保証が得られることが広く知られている。 63 | $h$値が常に最適解コストの下界であるヒューリスティック関数を\define{許容的なヒューリスティック}{admissible heuristic}{きょようてきなヒューリスティック}と呼ぶ。 64 | 65 | \ddef{許容的なヒューリスティック、admissible heuristic}{ 66 | ヒューリスティック関数$h$は最適解のコストの下界である場合、許容的である。すなわち、全てのノード$u \in V$に対して$h(u) \leq h^*(u)$が成り立つ。 67 | } 68 | 69 | ただし、$h^*(u)$はノード$u$からゴールノード集合$T$のいずれかへたどり着くための最短経路である。%$h^*$はパーフェクトヒューリスティックと呼ぶ。 70 | 71 | 一般に、許容的なヒューリスティックを得る方法としては、元問題の\define{緩和問題}{relaxed problem}{かんわもんだい}を解き、その最適解コストをヒューリスティック値とすることである。ある問題の緩和問題とは、解集合に元の問題の解を含む問題を指す。要するに元の問題より簡単な問題である\footnote{解が多いほど簡単であるとは一概には言えないが}。 72 | 73 | %許容的よりも強い性質としてとして無矛盾性がある。 74 | 75 | もう一つ有用なヒューリスティックは\define{無矛盾なヒューリスティック}{consistent heuristic}{むむじゅんなヒューリスティック}である。 76 | 77 | \ddef{無矛盾なヒューリスティック、consistent heuristic}{ 78 | ヒューリスティック関数$h$は全てのノードのペア$(u, v)$に対して 79 | \begin{equation} 80 | h(u) \leq h(v) + k(u,v) 81 | \end{equation} 82 | が成り立つ場合、無矛盾である。$k(u, v)$は$u$から$v$への最小コストである (経路がない場合$k = \infty$とする)。 83 | } 84 | 85 | 無矛盾性は特に\ref{sec:astar-search}章で後述するA*探索において探索の効率性に重要な性質である。 86 | また、無矛盾なヒューリスティックのうちゴールノードの$h$値が0となるヒューリスティックは許容的である。 87 | 88 | \dtheorem{ 89 | ゴールノード$n \in T$に対して$h(n) = 0$となる無矛盾なヒューリスティックは許容的なヒューリスティックである。 90 | } 91 | 92 | \begin{proof} 93 | すべての状態$u \in S$に対して以下が成り立つ。 94 | \begin{align} 95 | h(u) &\leq h(n) + k(u, n) \\ 96 | &= k(u, n) \\ 97 | &= h^*(u) 98 | \end{align} 99 | よって$h(s) \leq h^*(n)$より許容的である。 100 | \end{proof} 101 | 102 | ゴールノードに対してヒューリスティック値が$0$になるヒューリスティック関数を作ることは簡単である。単純に$Goal(s)$ならば$h(s)=0$とすればよい。 103 | ヒューリスティックの無矛盾性は証明することは難しそうであるが、実はシンプルな導き方がある。そのためにまず\define{単調なヒューリスティック}{monotone heuristic}{たんちょうなヒューリスティック}を定義する。 104 | 105 | \ddef{単調なヒューリスティック、monotone heuristic}{ 106 | ヒューリスティック関数$h$は全てのエッジ$e = (u, v) \in E$に対して$h(u) \leq h(v) + w(u,v)$が成り立つ場合、無矛盾である。 107 | } 108 | 109 | 単調なヒューリスティックは無矛盾性の定義と比較して、ノードのペア$(u, v)$が直接繋がっているという制約がある分、弱い性質に思えるかもしれない。しかし実は単調性と無矛盾性は同値である。 110 | 111 | 112 | \dtheorem{ 113 | 単調性と無矛盾性は同値である。 114 | } 115 | \dproof{ 116 | 無矛盾性であるならば単調性であることは自明である。 117 | 単調性が成り立つ場合に無矛盾であることを示す。 118 | $(u, v)$に経路がない場合$k=\infty$なので 119 | \begin{align} 120 | h(n_0) &\leq h(n_1) + w(n_0, n_1) \\ 121 | &\leq h(n_2) + w(n_0, n_1) + w(n_1, n_2) \\ 122 | &... \\ 123 | &\leq h(n_k) + \sum_{i=0..k-1}(w(n_i,n_{i+1})) \\ 124 | \end{align} 125 | $u = n_0, v = n_k$とすると無矛盾性が成り立つ。 126 | 127 | } 128 | 129 | よって、無矛盾性を示すために必要なのは直接つながっているノードのペアに対して$h(u) \leq h(v) + w(u,v)$が成り立つことを示せばよい。これを示すことは比較的にシンプルである。 130 | 131 | これらのアルゴリズムの性質は後述するA*探索において非常に有用である。 132 | %無矛盾性の主張は、ヒューリスティックの値が小さいほどゴールに近いということを意味する。つまり、ノード$n_0, n_1$に対して$h(n_0) < h(n_1)$ならば、$n_0$からゴールへの最小コストは$n_1$からゴールへの最小コストよりも小さい。 133 | %これのうれしい点は、次にどのアクションを選択するべきかを考える際、よりゴールに近いノードに向かったほうが良いだろう、と判断をすることができる点にある。 134 | %例えばグリッド経路探索問題におけるマンハッタン距離ヒューリスティックは無矛盾なヒューリスティックである。マンハッタン距離ヒューリスティックは現在位置とゴール位置のマンハッタン距離を$h$値とする。 135 | 136 | 137 | \section{A*探索 (A* Search)} 138 | \label{sec:astar-search} 139 | 140 | \begin{figure} 141 | \centering 142 | \begin{tikzpicture}[scale=0.6] 143 | \input{figures/astar-dijkstra.tex} 144 | \end{tikzpicture} 145 | \caption{A* (ヒューリスティック探索)とダイクストラ法 (情報なし探索)の探索範囲の比較} 146 | \label{fig:dijkstra-astar} 147 | \end{figure} 148 | 149 | ダイクストラ法は初期状態からそのノードまでのコストである$g$値が最小のノードを展開していく。これは間違った方針ではないだろうが、理想的にはゴール状態に向かっていくノードを展開していきたい。図\ref{fig:dijkstra-astar}の大きい方の円はダイクストラ法 (情報なし探索)による状態空間の探索を図示したものである。ダイクストラ法はゴールがどこにあるかということを無視して探索を進めているため、初期位置からどの方向へも均等に探索を行う。 150 | 人間の目で見れば一目で右に探索していけばよいというのは分かる。そのような人間の持っている知識を利用して探索を効率化出来ないだろうか? 151 | \define{A*探索}{A* search}{エースターたんさく}はゴールまでの距離を見積もる\define{ヒューリスティック関数}{heuristic function}{ヒューリスティックかんすう}を用いることでゴールに向かって探索していくことを目指した手法である。 152 | 153 | A*探索はヒューリスティック探索の代名詞である、最も広く知られている手法である \cite{fikes:71}。 154 | A*探索は以下の$f$値が最小となるノードを優先したグラフ探索アルゴリズムである。 155 | 156 | \begin{equation} 157 | f_{\text{A*}}(s) = g(s) + h(s) 158 | \label{alg:astar-search} 159 | \end{equation} 160 | 161 | \begin{figure} 162 | \centering 163 | \begin{tikzpicture}[scale=0.6] 164 | \input{figures/fvalue.tex} 165 | \end{tikzpicture} 166 | \caption{$f_{\text{A*}}$値の意味} 167 | \label{fig:fvalue} 168 | \end{figure} 169 | 170 | 171 | ノード$s$の$f$値は、初期状態$s_0$から$s$を通過してゴール状態$g$に辿り着くためのコストの見積もりである (図 \ref{fig:fvalue})。$g$値は初期状態からノード$s$までの既知の最短経路コストである。一方$h$値はヒューリスティック関数による$s$からゴール状態までの最短経路の見積もりである。 172 | A*探索は非明示的グラフ探索アルゴリズム(アルゴリズム\ref{alg:implicit-graph-search})の一つであり、$f$値が最小ノードから優先して探索を行う \ref{alg:astar-search}。 173 | 174 | % $g(n)$のみでノードを選択するダイクストラ法(\ref{sec:dijkstra}章)と比較すると、A*探索はゴール状態までのコストの見積もりを考慮して次に展開するノードを決めている。 175 | 176 | % 図\ref{fig:grid-astar-mdheuristic}は\define{マンハッタン距離ヒューリスティック}{Manhattan distance heuristic}{マンハッタンきょりヒューリスティック}によるA*探索である。 177 | % 青いノードは展開済みノード、緑のノードはオープンリストに入れられた未展開ノードである。幅優先探索による図\ref{fig:grid-dijkstra}と比較すると、展開済み・未展開ノードの数が少なく済んでいることがわかるだろう。 178 | 179 | 180 | \dtheorem{ 181 | グラフに経路コストが0以下のサイクルが存在しない場合、A*は完全である。 182 | } 183 | \dproof{ 184 | グラフが有限である場合は、やがてすべてのノードを生成・展開し解を発見するので、完全である。 185 | グラフが無限である場合もやがて解を発見できる。最適解のコストを$c^*$とすると、A*が展開するノードは$f(s) \leq c^*$のノードのみである。本書ではグラフは局所有限であると仮定している (すべてのノードの分枝数は有限である)ので、$f(s) \leq c^*$を満たすノードは有限個である。A*探索はやがてこれらのノードをすべて生成・展開し解を発見するので、完全である。 186 | } 187 | なお、解が存在しない場合、グラフが無限である場合にA*は停止しないので停止性を満たさない。 188 | 189 | 190 | A*に用いるヒューリスティック関数は正確であるほど良いが、それに加えて許容的、無矛盾であるという性質も有用である。 191 | 192 | \dtheorem{ 193 | ヒューリスティックが許容的である時、A*は最適解を返す。 194 | } 195 | \dproof{ 196 | 197 | %全てのノードnは展開時にg(n)がnに辿り着くための最短経路コストの値である。%これは無矛盾性か 198 | 許容的なヒューリスティック$h(n)$は$n$からゴールへの経路の下界である。よって、ゴール状態の$h$値は$0$である。つまりゴール状態の$f$値は$g$値と同じである。この解の$g(n')$値を$f*$と置く(解のコストに相当)。 199 | A*のノードの展開順に従うと、$f*$のノードを展開する前に全ての$f h_1(s) 225 | \end{equation} 226 | であるとき、$h_2$は$h_1$よりも\define{情報がある}{more informed}{ヒューリスティックのじょうほう}と呼ぶ。 227 | } 228 | 229 | より情報があるヒューリスティックを使う方がA*は効率的である。 230 | 231 | \dtheorem{ 232 | (ノード展開の必要条件): A*探索で展開されるノードの$f$値はすべて最適解のコスト以下である ($f(s) \leq c^*$)。 233 | } 234 | 235 | \dtheorem{ 236 | (ノード展開の十分条件): $f$値が最適解のコスト未満であるノードは必ずすべてA*探索で展開される ($f(s) < c^*$)。 237 | } 238 | 239 | A*探索は$f$値が一番小さいノードから展開していくので、これらの定理が満たされる。 240 | 241 | \dtheorem{ 242 | $h_2$が$h_1$よりも情報がある場合、$h_2$を使ったA*探索によって展開されるノードはすべて$h_1$を使ったA*探索でも展開される。 243 | } 244 | \dproof{ 245 | $h_2$によるA*で展開されるノードの集合はすべて$c^* \geq g(s) + h_2(s)$である。 246 | ゴールを除き$g(s) + h_2(s) > g(s) + h_1(s)$である。 247 | よって、$c^* > g(s) + h_1(s)$なのでこれらのノードはすべて$h_1$を使ったA*探索でも展開される。 248 | } 249 | 250 | よって、A*に使うヒューリスティックは情報があるほど良い。 251 | 252 | 253 | 一方のヒューリスティック関数が他方より情報がある場合はより情報のある方を使えばよい。 254 | しかし、ある場所では$h_1$よりも$h_2$の方が正確であり、他のある場所では$h_2$よりも$h_1$の方が正確である、となる場合がある。この場合どちらのヒューリスティックを使えば良いだろうか?実は両方を使うことによってより良い正確なヒューリスティックを得ることができる。 255 | 256 | \dtheorem{ 257 | %\begin{itemize} 258 | ヒューリスティック$h_1,..., h_m$が許容的であるとき、$h(s) = \max(h_1(s),..., h_m(s))$は許容的である。 259 | ヒューリスティック$h_1,..., h_m$が許容的で無矛盾であるとき、$h(s) = \max(h_1(s),..., h_m(s))$は許容的で無矛盾である。 260 | %\end{itemize} 261 | } 262 | 263 | このように複数のヒューリスティックを組み合わせることでより良いヒューリスティックを得ることができる。 264 | このアイディアはさまざまなヒューリスティック関数の自動生成に応用されている。 265 | 266 | 267 | \section{ヒューリスティック関数の例} 268 | \label{sec:heuristic-example} 269 | 270 | \ref{sec:heuristic-function}章にあるように、なるべく正確であり、許容的、無矛盾なヒューリスティックが望ましい。 271 | 一般に、許容的なヒューリスティックを得る方法としては、元問題の{\bf 緩和問題}を解き、その最適解コストをヒューリスティック値とすることである。ある問題の緩和問題とは、解集合に元の問題の解を含む問題を指す。要するに元の問題より簡単な問題である\footnote{解が多いほど簡単であるとは一概には言えないが}。 272 | グラフ探索アルゴリズムにおいて緩和問題を作る方法は様々あるが、一つはグラフのエッジを増やすことで緩和が出来る。グラフのエッジを増やすには、問題の可能なアクションを増やすなどの方法がある。 273 | 274 | \subsection{グリッド経路探索:マンハッタン距離} 275 | 276 | 4方向グリッド経路探索問題の元問題は障害物のあるグリッドに移動することは出来ない。グリッド経路探索で有効なヒューリスティックの一つはマンハッタン距離ヒューリスティックである。これは現在位置とゴール位置のマンハッタン距離を$h$値とする。マンハッタン距離は障害物を無視した最短経路の距離であるので、元の問題グラフに対してグラフのエッジを増やした緩和問題での解のコストに対応する。 277 | このように、問題の性質を理解していれば許容的なヒューリスティック関数を設計することが出来る。 278 | 279 | 280 | \subsection{スライディングタイル:マンハッタン距離} 281 | スライディングタイルにおけるマンハッタン距離ヒューリスティックは各タイルの現在の位置とゴール状態の位置のマンハッタン距離の総和を$h$値とする。 282 | スライディングタイル問題において一度に動かせるタイルは1つであり、その距離は1つである。 283 | そのため、マンハッタン距離ヒューリスティックは許容的なヒューリスティックである。 284 | 285 | \subsection{スライディングタイル:パターンデータベース} 286 | 287 | \define{パターンデータベースヒューリスティック}{Pattern database heuristic}{パターンデータベースヒューリスティック}は部分問題を解き、その解コストをヒューリスティック値とするアルゴリズムである\cite{edelkamp2001planning}。 288 | 探索を始める前に部分問題を解き、部分問題のすべての状態からゴール状態への最適解のコストをテーブルに保存する。 289 | 図\ref{fig:pattern-database}はスライディングタイルにおけるパターンデータベースの例である。 290 | 図の例は「1, 2, 3, 4のタイルを正しい位置に動かす」という部分問題を解いている。 291 | この部分問題では5, 6, 7, 8のタイルの位置はどこでもよい。1, 2, 3, 4さえゴール位置にあればよい。 292 | ゴール条件以外は元の問題と同様である。 293 | この部分問題は元の問題の緩和問題になっているので許容的なヒューリスティックが得られる。 294 | 295 | 図の例では1, 2, 3, 4のタイルに絞った部分問題だったが、他のタイルの組み合わせでも良い。 296 | 例えば1, 2, 3, 4のタイルによる部分問題と5, 6, 7, 8のタイルによる部分問題からは異なるヒューリスティック値を得ることができる。 297 | これらのヒューリスティック値の最大値を取ることでより正確なヒューリスティックにすることができる。 298 | このように複数のパターンデータベースを使うヒューリスティックを\define{複数パターンデータベース}{multiple pattern databse}{ふくすうパターンデータベース}と呼ぶ。複数パターンデータベースは一つの大きなパターンデータベースを使うよりも正確さに欠けるが、計算時間・空間の面で大きなアドバンテージがある。 299 | 300 | 1, 2, 3, 4のタイルによる部分問題と5, 6, 7, 8のタイルによる部分問題は一見完全に分割された部分問題なので、これらのヒューリスティック値の和を取ってもよさそうだが、そうすると許容的なヒューリスティックにはならない。なぜなら例えばタイル5を動かすアクションによるコストは両方の部分問題のコストに数えられているからである。 301 | そこで部分問題のコストを「1, 2, 3, 4 (あるいは5, 6, 7, 8)のタイルを動かすときのみコストがかかり、他のタイルはコスト0で動かせる」とすると、コストを重複して数えることはなくなる。そうするとこれらの部分問題のコストの和は許容的なヒューリスティックになる。 302 | このように複数のパターンデータベースで重複してコストが数えられないようにしたものを\define{素集合パターンデータベース}{disjoint pattern databse}{そしゅうごうパターンデータベース}と呼ぶ \cite{korf2002}。 % TODO: good term? 303 | 304 | マンハッタン距離ヒューリスティックは、各タイルごとの部分問題8つに分けた場合の素集合パターンデータベースである。 305 | 306 | \begin{figure} 307 | \centering 308 | \begin{tikzpicture}[scale=0.6] 309 | \input{figures/pattern-database.tex} 310 | \end{tikzpicture} 311 | \caption{パターンデータベースの初期状態 (左)とゴール条件 (右)の例。1, 2, 3, 4のタイルだけゴール位置にあればよく、他のタイルの位置は関係ない。} 312 | \label{fig:pattern-database} 313 | \end{figure} 314 | 315 | 316 | \subsection{巡回セールスパーソン問題:最小全域木} 317 | TSPの解の下界としては\define{最小全域木}{minimum spanning tree}{さいしょうぜんいきき}のコストがよく用いられる (図\ref{fig:tsp-mst})。 318 | グラフの\define{全域木}{spanning tree}{ぜんいきき}は全てのノードを含むループを含まない部分グラフである。 319 | 最小全域木は全域木のうち最もエッジコストの総和が小さいものである。 320 | 未訪問の都市によるグラフの最小全域木はTSPの下界となることが知られている。 321 | ヒューリスティック探索ではまだ訪問していない都市をカバーする最小全域木のコストをヒューリスティック関数に用いる。 322 | 最小全域木は素早く計算ができるので探索には使いやすい。 323 | 324 | \begin{figure}[tbh] 325 | \centering 326 | \begin{tikzpicture}[scale=0.6] 327 | \input{figures/mst.tex} 328 | \end{tikzpicture} 329 | \caption{巡回セールスパーソンにおけるTSP解 (左)と最小全域木 (右)} 330 | \label{fig:tsp-mst} 331 | \end{figure} 332 | 333 | 334 | \section{非最適解の探索} 335 | \label{sec:inadmissible} 336 | 337 | 許容的なヒューリスティックを用いたA*探索は最適解が得られるが、必ずしも最適解がほしいわけではない場合もある。解のクオリティよりもとにかく解が何か欲しい、という場合もある。 338 | 最適解ではない解を\define{非最適解}{suboptimal solution}{ひさいてきかい}と呼び、最適解に限らず解を発見するアルゴリズムを\define{非最適探索}{suboptimal search}{ひさいてきたんさく}と呼ぶ。% あるいは\define{局所探索}{local search}と呼ぶ。 % 局所探索はまた違う文脈か 339 | 340 | \subsection{重み付きA*探索 (Weighted A*)} 341 | \label{sec:weighted-astar-search} 342 | 343 | \define{重み付きA*探索}{weighted A*}{おもみつきエースターたんさく} (wA*)は解のクオリティが落ちる代わりにより素早く解にたどり着くための手法である \cite{wilt2010comparison}。 344 | wA*は重み付き$f$値、$f_w$が最小のノードを優先して探索する。 345 | 346 | \begin{equation} 347 | f_w(n) = g(n) + w h(n) 348 | \end{equation} 349 | 350 | \dtheorem{ 351 | 許容的なヒューリスティックを用いた重み付きA*探索によって発見される解は最適解のコスト$f^*$の$w$倍以下である。 352 | } 353 | 354 | 355 | wA*の利点はA*よりもはるかに高速であることである。 356 | 多くの場合、$w$の大きさに対して指数的な高速化が期待できる。これは深さ$d$のノードの個数は$d$に対して指数的(分枝度を$b$とすると$b^d$個)であることに対応する。 357 | 358 | wA*などの非最適探索を使う場合はいずれにせよ最適解が得られないので、許容的でないヒューリスティックと組み合わせて使われることが多い。許容的でないヒューリスティックは許容的なヒューリスティックよりも高速であることが多い。% TODO lama? 359 | 360 | wA*の解は最適解のコストの上界になるので、A*探索の枝刈りに用いることが出来る。 361 | A*探索を実行する前にwA*を走らせ、解の上界$c^*$を得、A*探索実行時にノード$n$に対して$f$値が$f(n) \geq c^*$である場合、そのノードを枝刈りすることができる。このテクニックは多重配列アライメントなどに使われる\cite{ikeda1999enhanced}。 362 | 363 | 図\ref{fig:grid-astar}、\ref{fig:grid-wastar}はグリッド経路探索問題でのA*とwA* ($w=5$)の比較である。 364 | 緑のグリッドが初期位置、赤のグリッドがゴール、黒のグリッドは進行不可能の壁である。 365 | 青のグリッドが探索終了時点に各アルゴリズムによって展開されたノード、薄緑のグリッドが各アルゴリズムに生成され、まだ展開されていないノード (オープンリストにあるノード)である。 366 | 367 | wA*の展開・生成ノード数がA*よりも少ない。 368 | ただしwA*で発見された解はA*で発見された解よりも遠回りなことがわかる。 369 | A*探索は最適解を発見するが、wA*では最初に発見した解が最適とは限らない。 370 | 371 | \begin{figure} 372 | \centering 373 | \subfloat[A*] { 374 | \includegraphics[width=0.3\linewidth]{./figures/grid-astar.png} 375 | \label{fig:grid-astar} 376 | } \hspace{4pt} 377 | \subfloat[wA*] { 378 | \includegraphics[width=0.3\linewidth]{./figures/grid-wastar-5.png} 379 | \label{fig:grid-wastar} 380 | } \hspace{4pt} 381 | \subfloat[GBFS] { 382 | \includegraphics[width=0.3\linewidth]{./figures/grid-gbfs.png} 383 | \label{fig:grid-gbfs} 384 | } 385 | \caption{グリッド経路探索問題におけるA*、wA*、GBFSの比較} 386 | \label{fig:grid-comparison} 387 | \end{figure} 388 | 389 | \subsection{貪欲最良優先探索 (Greedy Best-First Search)} 390 | \label{sec:greedy-best-first-search} 391 | 392 | wA*の例で見たように、$g$値に対して$h$値に重きを置くことによって解のクオリティを犠牲により高速に解を得ることができる。 393 | 問題によっては解のクオリティはあまり重要でなかったり、そもそもクオリティという概念がないことがある。 394 | このようにとにかく解があればよいという場合は\define{貪欲最良優先探索}{Greedy Best-First Search}{どんよくさいりょうゆうせんたんさく}が使われることが多い \cite{wilt2010comparison}。 395 | 396 | \begin{equation} 397 | f_{\text{gbfs}}(s) = h(s) 398 | \end{equation} 399 | 400 | 貪欲最良優先探索は$g$値を無視し、$h$値のみで展開順を決定する。つまりwA*の$w$を無限大にしたものである。 401 | 402 | 貪欲最良優先探索は解のクオリティに保証がない。 403 | しかし多くの問題で高速に解を発見できるとても強力な手法である。 404 | 図\ref{fig:grid-gbfs}は貪欲最良優先探索である。wA*よりもさらに生成ノード数が少ないことが分かるだろう。 405 | 406 | \subsection{山登り法 (Hill Climbing)} 407 | \label{sec:hill-climbing} 408 | 409 | A*探索はクローズドリストに探索済みのノードを記憶しておくことでグラフ全体を見渡して探索を進める。しかし探索済みのノードをメモリにおいておくことは時間がかかるし、メモリも消費する。\define{局所探索}{local search}{きょくしょたんさく}は現在見つかった最良のノードのみを記憶しておくことで効率的に(最適とは限らない)解を発見する方法である。 410 | 411 | \define{山登り法}{hill climbing}{やまのぼりほう}は局所探索アルゴリズムであり、特に組み合わせ最適化問題のためのアルゴリズムとして使われる。 412 | 山登り法のアルゴリズムは非常に単純である。子ノードのうち最も$h$値が小さいノードを選ぶことを繰り返す。 413 | 414 | \begin{algorithm} 415 | \caption{山登り法 (Hill Climbing)} 416 | \Input{非明示的状態空間グラフ $(s, Goal, Expand, w)$, 評価関数 $h$} 417 | \Output{$s$からゴール状態$t \in T$への経路、経路が存在しなければ$\emptyset$} 418 | \While {{\bf not} $Goal(s)$} { 419 | $u \leftarrow \arg \min_{s' \in Expand(s)} h(s')$\; 420 | $parent{u} \leftarrow s$\; 421 | $s \leftarrow u$\; 422 | } 423 | \Return $Path(s)$ 424 | \label{alg:hill-climbing} 425 | \end{algorithm} 426 | 427 | この手法は組合せ最適化・連続最適化のためによく使われる。 428 | グラフ探索アルゴリズムでこのまま使おうとすると評価関数の極小ではまってしまったり、デッドエンドに落ちてしまう可能性がある。 429 | 430 | 431 | \subsection{強制山登り法 (Enforced Hill Climbing)} 432 | 433 | ヒューリスティック探索でよく使われる局所探索は\define{強制山登り法}{enforced hill-climbing}{きょうせいやまのぼりほう} (EHC)である \cite{hoffmann2005ignoring}。 434 | EHCは幅優先探索を繰り返し、現在のノードよりも$h$値が小さいノードを発見する。発見できればそのノードから再び幅優先を行い、ゴールを発見するまで続ける。もし$h$値が小さいノードが見つからなければ極小に陥ってしまったということであるので、探索を打ち切り、失敗 (Fail)を返す。 435 | 436 | \begin{algorithm} 437 | \caption{強制山登り法 (Enforced Hill Climbing)} 438 | \Input{非明示的状態空間グラフ $(s, Goal, Expand, w)$, 評価関数 $h$} 439 | \Output{$s$からゴール状態$t \in T$への経路、経路が存在しなければ$\emptyset$} 440 | \While {{\bf not} $Goal(s)$} { 441 | $T \leftarrow \{v \in Expand(s)\}$\; 442 | $T' \leftarrow \{v \in T | h(v) < h(s) \}$\; 443 | \While {$T' = \emptyset$} { 444 | $T \leftarrow \{v \in Expand(u) | u \in T\}$\; 445 | $T' \leftarrow \{v \in T | h(v) < h(s) \}$\; 446 | } 447 | $u \leftarrow \arg \min_{s' \in Expand(s)} h(s')$\; 448 | $parent(u) \leftarrow s$\; 449 | $s \leftarrow u$\; 450 | } 451 | \Return $Path(s)$ 452 | \label{alg:enforced-hill-climbing} 453 | \end{algorithm} 454 | 455 | 456 | ほとんどの局所探索アルゴリズムは完全性を満たさない。 457 | なので解が発見できなければ(失敗を返す場合)続けてA*探索などを走らせないと解が発見できない場合がある。 458 | 山登り法などの局所探索の利点はなんといってもアルゴリズムはシンプルであり、実装が簡単であることである。 459 | また、オープンリストなどのデータ構造を保持する必要がないので消費メモリ量が非常に少ない。 460 | 461 | 山登り法が記憶しなければならないノードは現在のノードとその子ノードらだけである。 462 | 毎回次のノードを選択したら、そのノード以外のノードは捨ててしまう。 463 | 貪欲最良優先探索(\ref{sec:greedy-best-first-search}節)も同様に$h$値のみしか見ていないが、すべての生成済みノードをオープンリストに保存して評価値を確認していることと比較すると、山登り法がいかにアグレッシブな手法であるかがうかがえるだろう。 464 | 465 | \section{上手く行かない場合} 466 | 467 | ヒューリスティック探索が思ったより遅い場合考えるべきことはいくつかある。 468 | 469 | まず考えるべきは、そもそもヒューリスティック探索によって解くべき問題なのかという点である。 470 | 制約プログラミングや他の組合せ最適化手法なら効率的な問題かもしれない。 471 | 似たような問題が他の手法によって解かれていないかを調べてみると良い。 472 | 473 | ヒューリスティック探索が良さそうだと思ったら、試しにwA*にしてみて、weightを大きく取ってみよう。 474 | おおまかに言うとwA*の実行速度はwの大きさに対して指数的に速くなる。$w=1$だと全く解けそうにない問題でも$w=2$にすると一瞬で解けることがある。 475 | もし$w$を10くらいにしてもすぐに解が発見できなければ、おそらくその問題で最適解を発見するのは非常に難しい。このような問題に対しては最適解を見つけることは諦め、非最適解探索 (節 \ref{sec:inadmissible})を使ったほうが良いだろう。 476 | wA*でも解が見つけられない場合はそもそも解がない問題かもしれない。解がない場合は制約プログラミングのソルバーを使うと素早く検出できることが多い。 477 | 478 | wA*で非最適解を見つけられる場合なら、工夫すればA*で最適解を見つけることができるかもしれない。 479 | その場合まず、おおまかな探索空間の大きさを見積もり、そして1秒間に何ノードが展開されているかを確認すると良い。 480 | ヒューリスティック探索の実行時間はおおまかには(展開するノード数)と(1ノードの展開にかかる時間)の積である。 481 | 482 | %例えばA*探索はC++でスライディングタイル問題だと秒間XXXノードが展開できる。 483 | もし秒間に展開されているノードの数が少ない場合、プロファイラを使ってどのオペレーションに時間がかかっているかを確認するべきだろう。多くの場合実行時間の大半をオープンリスト、クローズドリスト、ノードの展開関数(現在の状態とアクションから状態を計算する関数)のいずれかが占めている、ということがわかるだろう。 484 | 485 | オープンリスト、クローズドリストの処理に時間がかかっている場合はデータ構造の改良によって問題が解決するかもしれない。効率的なデータ構造の設計については\ref{ch:search-performance}章を参照されたい。 486 | 特に探索が進みデータ構造が大きくなるほどアクセスするためにかかる時間がどんどん増えていく場合はデータ構造の工夫によって解決できる可能性がある。 487 | あとはプロセスのメモリ消費量を確認してみよう。A*はメモリ消費も激しい。 488 | 実行時間が遅いのは、メモリを使いすぎていてメモリスワップが起きているからかもしれない。 489 | より大きなメモリを使うか、IDA*などのメモリの消費量の少ない手法を試してみると良いかもしれない。 490 | 491 | ノードの展開関数に時間がかかる場合はその実装を工夫するか、ノードの展開回数を最小限に抑えるしかない。 492 | ノードの展開回数を減らす方法はなかなか難しいが、例えばヒューリスティック関数の改良によって可能である。 493 | ほんの少しのヒューリスティック関数の改善によって探索の効率は劇的に上がりうる。 494 | % 例えばスライディングタイル問題ではTODO。 495 | 496 | これらの方法でもまだ解けない場合、扱っている問題は純粋なA*で解くには結構難しい問題かもしれない。 497 | 1968年にA*が初めて提案されてから、様々な発展手法が考案されてきた。 498 | 本書の後半で紹介する最新の手法を試してみれば解けるかもしれない。 499 | 500 | 501 | \section{Python実装} 502 | 503 | A*探索の実装は非常にシンプルである。グラフ探索のコードに渡すプライオリティ関数を$f = g + h$にすればよいだけである。 504 | 505 | \inputpython{python/astar_search.py}{1}{5} 506 | 507 | 重み付きA*や貪欲最良優先探索もほぼ同様である。 508 | 509 | \inputpython{python/wastar_search.py}{1}{5} 510 | \inputpython{python/greedy_best_first_search.py}{1}{5} 511 | 512 | ヒューリスティック関数の実装は問題によっては難しい場合もある。 513 | ここではグリッド経路探索のためのマンハッタン距離ヒューリスティック関数を紹介する。 514 | 515 | \inputpython{python/grid_pathfinding.py}{66}{68} 516 | 517 | この関数を\code{GridPathfinding}クラスに追加すればよい。 518 | スライディングタイル問題のためのマンハッタン距離ヒューリスティック関数は以下のようになる。 519 | 520 | \inputpython{python/sliding_tile.py}{78}{93} 521 | 522 | 各問題に対してそれぞれのヒューリスティック関数を実装することは面倒に感じられるかもしれない。 523 | ある特定の問題に特化して高速化させたい場合は、ヒューリスティック関数はその問題の前提知識を利用した関数であるので、それぞれの問題毎に適切なものを実装する必要があるだろう。 524 | 一方で汎用的に様々な問題に対して使えるヒューリスティック関数もあるので、\ref{ch:classical-planning}章で紹介する。 525 | 526 | 527 | \section{まとめ} 528 | 529 | 状態空間問題に対して何らかの前提知識がある場合、その知識を利用して探索を効率化させる方法の一つとしてヒューリスティック探索がある。 530 | ヒューリスティック探索は状態の「有望さ」を推定できる場合、それをヒューリスティック関数として表すことが出来る。 531 | 532 | ヒューリスティック探索で基本となる手法はA*探索である。 533 | A*は各ノードの有望さをそのノードに到達するまでのコストとそのノードからゴールに到達するまでのコストの推定値の和として推定し、最も有望なノードから優先して探索する手法である。 534 | 535 | ヒューリスティック関数のうち重要な性質は2つある。 536 | 許容的なヒューリスティック関数はゴールまでのコスト未満の推定をしないヒューリスティックであり、これを用いたA*は最適解が見つけられることが保証されている。 537 | 無矛盾なヒューリスティックを用いたA*探索はノードの再展開が生じない。 538 | 539 | A*で解を見つけるのに時間がかかりすぎる場合は、最適解を見つけることを諦めてwA*や局所探索を用いることができる。 540 | 541 | \section{練習問題} 542 | 543 | \begin{enumerate} 544 | \item グリッド経路探索問題においてマンハッタン距離ヒューリスティックを実装し、A*探索を実装せよ。幅優先探索と比較して展開するノードの数は変わったか。 545 | 546 | \item グリッド経路探索問題において、グリッドが斜めを含めた隣り合う8マスに動ける場合を考える。このとき、現在位置とゴール位置とのマンハッタン距離は許容的である。では、現在位置とゴール位置とのユークリッド距離 (直線距離) は許容的なヒューリスティックになるか? 547 | 548 | \item 斜め移動ありのグリッド経路探索問題においてマンハッタン距離よりも推定値が正確でありかつ許容的なヒューリスティック関数はあるだろうか? 549 | (ヒント:ある。) 550 | 551 | \item 上述のヒューリスティック関数を用いたA*探索で斜め移動ありのグリッド経路探索問題を解いてみよう。マンハッタン距離を用いた場合と比較して展開するノードの数は変わったか。 552 | 553 | \item ヒューリスティック$h$が許容的であるとする。このとき、$h'(s) = h(s) + c$は許容的であるか?$c$が何を満たせば許容的であるか?($c$は定数とする) 554 | \item ヒューリスティック$h$が無矛盾であるとする。このとき、$h'(s) = h(s) + c$は無矛盾であるか?$c$が何を満たせば無矛盾であるか?($c$は定数とする) 555 | 556 | \item 「許容的なヒューリスティックを用いた重み付きA*探索によって発見される解は最適解のコスト$f^*$の$w$倍以下である」ことを証明せよ。 557 | (ヒント: 「ヒューリスティックが許容的である時、A*は最適解を返す」定理の証明が参考になる。) 558 | 559 | \item 山登り法が解を見つけることが出来ないような状態空間問題の例を一つ考えてみよう。 560 | \end{enumerate} 561 | 562 | 563 | \section{関連文献} 564 | 565 | \define{最良優先探索}{best-first search}{さいりょうゆうせんたんさく}は2種類の定義があることに注意したい。 566 | 本書では最良優先探索をA*を含む、何らかの有望さの指標に従ってグラフを探索するアルゴリズムを指す。 567 | 一方、ゴールに最も近いと考えられるノードを常に最優先して展開するアルゴリズムを特に最良優先探索と呼ぶことも多い。本書ではこの方法は貪欲最良優先探索と呼んでいる。 568 | 569 | 制限時間内でできるだけ良いクオリティの解が欲しい場合はAnytime Reparing A* \cite{likhachev2004ara}が使える。Anytime Reparing A*は問題に対して重み付きA*を繰り返し$w$値をだんだんと小さくしていくことでだんだんと解のクオリティを向上させる。一般にいつ停止しても現時点で見つかった中で一番良い (最適とは限らない)解を返し、時間をかけるほど解のクオリティを改善していくアルゴリズムをanytimeアルゴリズムと呼ぶ。Anytimeアルゴリズムとしては他にもモンテカルロ木探索などがある \cite{browne2012survey}。 570 | 571 | 山登り法は評価値が最も良い状態を決定論的に選ぶので解・最適解を発見する保証はない。 572 | 一方、すべての次状態を均等な確率でランダムに選ぶランダムウォークはいずれ解を発見するが、時間がかかりすぎる。 573 | \define{焼きなまし法}{simulated annealing}はこの二つの手法の中間をとったような手法であり、最初は評価値を無視して均等なランダムウォークからはじめ、探索が進行していくにつれて評価値に応じて次状態を選択するようにするという手法である \cite{vcerny1985thermodynamical,van1987simulated}。焼きなまし法は最適化問題で非常に重要なアルゴリズムである \cite{kirkpatrick1983optimization}。 574 | 575 | \define{遺伝的アルゴリズム}{genetic algorithm}もさまざまな最適化問題に適用できる重要なアルゴリズムである \cite{goldberg1989}。 576 | グラフ探索問題で使われることは少ないが、グラフ探索問題ではなかなか解けない難しい問題が遺伝的アルゴリズムによって簡単に解けることがある。例えばN-Queen問題はチェスの盤面上にクイーンを互いに移動可能範囲がぶつからないように配置する問題であるが、これはグラフ探索の手法だとなかなか解くのが難しいが遺伝的アルゴリズムだと非常に効率的に解けることが知られている。 577 | -------------------------------------------------------------------------------- /spphys.bst: -------------------------------------------------------------------------------- 1 | %% 2 | %% This is file `spphys.bst', 3 | %% generated with the docstrip utility. 4 | %% 5 | %% The original source files were: 6 | %% 7 | %% merlin.mbs (with options: `seq-no,vonx,nm-init,ed-au,yr-par,xmth,jtit-x,jttl-rm,thtit-a,vol-bf,volp-com,jpg-1,pgsep-c,num-xser,ser-vol,ser-ed,jnm-x,pub-date,pre-pub,doi,edpar,edby,fin-bare,pp,ed,abr,ord,jabr,xand,url,url-blk,nfss,') 8 | %% ---------------------------------------- 9 | %%********************************************************************************%% 10 | %% %% 11 | %% For Springer physics publications. Based on the APS reference style. %% 12 | %% Report bugs and improvements to: Joylene Vette-Guillaume or Frank Holzwarth %% 13 | %% Springer-Verlag 2004/10/15 %% 14 | %% %% 15 | %%********************************************************************************%% 16 | %% 17 | %% Copyright 1994-2004 Patrick W Daly 18 | % =============================================================== 19 | % IMPORTANT NOTICE: 20 | % This bibliographic style (bst) file has been generated from one or 21 | % more master bibliographic style (mbs) files, listed above. 22 | % 23 | % This generated file can be redistributed and/or modified under the terms 24 | % of the LaTeX Project Public License Distributed from CTAN 25 | % archives in directory macros/latex/base/lppl.txt; either 26 | % version 1 of the License, or any later version. 27 | % =============================================================== 28 | % Name and version information of the main mbs file: 29 | % \ProvidesFile{merlin.mbs}[2004/02/09 4.13 (PWD, AO, DPC)] 30 | % For use with BibTeX version 0.99a or later 31 | %------------------------------------------------------------------- 32 | % This bibliography style file is intended for texts in ENGLISH 33 | % This is a numerical citation style, and as such is standard LaTeX. 34 | % It requires no extra package to interface to the main text. 35 | % The form of the \bibitem entries is 36 | % \bibitem{key}... 37 | % Usage of \cite is as follows: 38 | % \cite{key} ==>> [#] 39 | % \cite[chap. 2]{key} ==>> [#, chap. 2] 40 | % where # is a number determined by the ordering in the reference list. 41 | % The order in the reference list is that by which the works were originally 42 | % cited in the text, or that in the database. 43 | %--------------------------------------------------------------------- 44 | 45 | ENTRY 46 | { address 47 | author 48 | booktitle 49 | chapter 50 | doi 51 | edition 52 | editor 53 | eid 54 | howpublished 55 | institution 56 | journal 57 | key 58 | month 59 | note 60 | number 61 | organization 62 | pages 63 | publisher 64 | school 65 | series 66 | title 67 | type 68 | url 69 | volume 70 | year 71 | } 72 | {} 73 | { label } 74 | INTEGERS { output.state before.all mid.sentence after.sentence after.block } 75 | FUNCTION {init.state.consts} 76 | { #0 'before.all := 77 | #1 'mid.sentence := 78 | #2 'after.sentence := 79 | #3 'after.block := 80 | } 81 | STRINGS { s t} 82 | FUNCTION {output.nonnull} 83 | { 's := 84 | output.state mid.sentence = 85 | { ", " * write$ } 86 | { output.state after.block = 87 | { add.period$ write$ 88 | newline$ 89 | "\newblock " write$ 90 | } 91 | { output.state before.all = 92 | 'write$ 93 | { add.period$ " " * write$ } 94 | if$ 95 | } 96 | if$ 97 | mid.sentence 'output.state := 98 | } 99 | if$ 100 | s 101 | } 102 | FUNCTION {output} 103 | { duplicate$ empty$ 104 | 'pop$ 105 | 'output.nonnull 106 | if$ 107 | } 108 | FUNCTION {output.check} 109 | { 't := 110 | duplicate$ empty$ 111 | { pop$ "empty " t * " in " * cite$ * warning$ } 112 | 'output.nonnull 113 | if$ 114 | } 115 | FUNCTION {fin.entry} 116 | { duplicate$ empty$ 117 | 'pop$ 118 | 'write$ 119 | if$ 120 | newline$ 121 | } 122 | 123 | FUNCTION {new.block} 124 | { output.state before.all = 125 | 'skip$ 126 | { after.block 'output.state := } 127 | if$ 128 | } 129 | FUNCTION {new.sentence} 130 | { output.state after.block = 131 | 'skip$ 132 | { output.state before.all = 133 | 'skip$ 134 | { after.sentence 'output.state := } 135 | if$ 136 | } 137 | if$ 138 | } 139 | FUNCTION {add.blank} 140 | { " " * before.all 'output.state := 141 | } 142 | 143 | FUNCTION {add.comma} 144 | { duplicate$ empty$ 145 | 'skip$ 146 | { "," * add.blank } 147 | if$ 148 | } 149 | 150 | FUNCTION {date.block} 151 | { 152 | new.block 153 | } 154 | 155 | FUNCTION {not} 156 | { { #0 } 157 | { #1 } 158 | if$ 159 | } 160 | FUNCTION {and} 161 | { 'skip$ 162 | { pop$ #0 } 163 | if$ 164 | } 165 | FUNCTION {or} 166 | { { pop$ #1 } 167 | 'skip$ 168 | if$ 169 | } 170 | FUNCTION {new.block.checka} 171 | { empty$ 172 | 'skip$ 173 | 'new.block 174 | if$ 175 | } 176 | FUNCTION {new.block.checkb} 177 | { empty$ 178 | swap$ empty$ 179 | and 180 | 'skip$ 181 | 'new.block 182 | if$ 183 | } 184 | FUNCTION {new.sentence.checka} 185 | { empty$ 186 | 'skip$ 187 | 'new.sentence 188 | if$ 189 | } 190 | FUNCTION {new.sentence.checkb} 191 | { empty$ 192 | swap$ empty$ 193 | and 194 | 'skip$ 195 | 'new.sentence 196 | if$ 197 | } 198 | FUNCTION {field.or.null} 199 | { duplicate$ empty$ 200 | { pop$ "" } 201 | 'skip$ 202 | if$ 203 | } 204 | FUNCTION {emphasize} 205 | { duplicate$ empty$ 206 | { pop$ "" } 207 | { "\emph{" swap$ * "}" * } 208 | if$ 209 | } 210 | FUNCTION {bolden} 211 | { duplicate$ empty$ 212 | { pop$ "" } 213 | { "\textbf{" swap$ * "}" * } 214 | if$ 215 | } 216 | FUNCTION {tie.or.space.prefix} 217 | { duplicate$ text.length$ #3 < 218 | { "~" } 219 | { " " } 220 | if$ 221 | swap$ 222 | } 223 | 224 | FUNCTION {capitalize} 225 | { "u" change.case$ "t" change.case$ } 226 | 227 | FUNCTION {space.word} 228 | { " " swap$ * " " * } 229 | % Here are the language-specific definitions for explicit words. 230 | % Each function has a name bbl.xxx where xxx is the English word. 231 | % The language selected here is ENGLISH 232 | FUNCTION {bbl.and} 233 | { "and"} 234 | 235 | FUNCTION {bbl.etal} 236 | { "et~al." } 237 | 238 | FUNCTION {bbl.editors} 239 | { "eds." } 240 | 241 | FUNCTION {bbl.editor} 242 | { "ed." } 243 | 244 | FUNCTION {bbl.edby} 245 | { "ed. by" } 246 | 247 | FUNCTION {bbl.edition} 248 | { "edn." } 249 | 250 | FUNCTION {bbl.volume} 251 | { "vol." } 252 | 253 | FUNCTION {bbl.of} 254 | { "of" } 255 | 256 | FUNCTION {bbl.number} 257 | { "no." } 258 | 259 | FUNCTION {bbl.nr} 260 | { "no." } 261 | 262 | FUNCTION {bbl.in} 263 | { "in" } 264 | 265 | FUNCTION {bbl.pages} 266 | { "pp." } 267 | 268 | FUNCTION {bbl.page} 269 | { "p." } 270 | 271 | FUNCTION {bbl.chapter} 272 | { "chap." } 273 | 274 | FUNCTION {bbl.techrep} 275 | { "Tech. Rep." } 276 | 277 | FUNCTION {bbl.mthesis} 278 | { "Master's thesis" } 279 | 280 | FUNCTION {bbl.phdthesis} 281 | { "Ph.D. thesis" } 282 | 283 | FUNCTION {bbl.first} 284 | { "1st" } 285 | 286 | FUNCTION {bbl.second} 287 | { "2nd" } 288 | 289 | FUNCTION {bbl.third} 290 | { "3rd" } 291 | 292 | FUNCTION {bbl.fourth} 293 | { "4th" } 294 | 295 | FUNCTION {bbl.fifth} 296 | { "5th" } 297 | 298 | FUNCTION {bbl.st} 299 | { "st" } 300 | 301 | FUNCTION {bbl.nd} 302 | { "nd" } 303 | 304 | FUNCTION {bbl.rd} 305 | { "rd" } 306 | 307 | FUNCTION {bbl.th} 308 | { "th" } 309 | 310 | MACRO {jan} {"Jan."} 311 | 312 | MACRO {feb} {"Feb."} 313 | 314 | MACRO {mar} {"Mar."} 315 | 316 | MACRO {apr} {"Apr."} 317 | 318 | MACRO {may} {"May"} 319 | 320 | MACRO {jun} {"Jun."} 321 | 322 | MACRO {jul} {"Jul."} 323 | 324 | MACRO {aug} {"Aug."} 325 | 326 | MACRO {sep} {"Sep."} 327 | 328 | MACRO {oct} {"Oct."} 329 | 330 | MACRO {nov} {"Nov."} 331 | 332 | MACRO {dec} {"Dec."} 333 | 334 | FUNCTION {eng.ord} 335 | { duplicate$ "1" swap$ * 336 | #-2 #1 substring$ "1" = 337 | { bbl.th * } 338 | { duplicate$ #-1 #1 substring$ 339 | duplicate$ "1" = 340 | { pop$ bbl.st * } 341 | { duplicate$ "2" = 342 | { pop$ bbl.nd * } 343 | { "3" = 344 | { bbl.rd * } 345 | { bbl.th * } 346 | if$ 347 | } 348 | if$ 349 | } 350 | if$ 351 | } 352 | if$ 353 | } 354 | 355 | MACRO {acmcs} {"ACM Comput. Surv."} 356 | 357 | MACRO {acta} {"Acta Inf."} 358 | 359 | MACRO {cacm} {"Commun. ACM"} 360 | 361 | MACRO {ibmjrd} {"IBM J. Res. Dev."} 362 | 363 | MACRO {ibmsj} {"IBM Syst.~J."} 364 | 365 | MACRO {ieeese} {"IEEE Trans. Software Eng."} 366 | 367 | MACRO {ieeetc} {"IEEE Trans. Comput."} 368 | 369 | MACRO {ieeetcad} 370 | {"IEEE Trans. Comput. Aid. Des."} 371 | 372 | MACRO {ipl} {"Inf. Process. Lett."} 373 | 374 | MACRO {jacm} {"J.~ACM"} 375 | 376 | MACRO {jcss} {"J.~Comput. Syst. Sci."} 377 | 378 | MACRO {scp} {"Sci. Comput. Program."} 379 | 380 | MACRO {sicomp} {"SIAM J. Comput."} 381 | 382 | MACRO {tocs} {"ACM Trans. Comput. Syst."} 383 | 384 | MACRO {tods} {"ACM Trans. Database Syst."} 385 | 386 | MACRO {tog} {"ACM Trans. Graphic."} 387 | 388 | MACRO {toms} {"ACM Trans. Math. Software"} 389 | 390 | MACRO {toois} {"ACM Trans. Office Inf. Syst."} 391 | 392 | MACRO {toplas} {"ACM Trans. Progr. Lang. Syst."} 393 | 394 | MACRO {tcs} {"Theor. Comput. Sci."} 395 | 396 | FUNCTION {bibinfo.check} 397 | { swap$ 398 | duplicate$ missing$ 399 | { 400 | pop$ pop$ 401 | "" 402 | } 403 | { duplicate$ empty$ 404 | { 405 | swap$ pop$ 406 | } 407 | { swap$ 408 | pop$ 409 | } 410 | if$ 411 | } 412 | if$ 413 | } 414 | FUNCTION {bibinfo.warn} 415 | { swap$ 416 | duplicate$ missing$ 417 | { 418 | swap$ "missing " swap$ * " in " * cite$ * warning$ pop$ 419 | "" 420 | } 421 | { duplicate$ empty$ 422 | { 423 | swap$ "empty " swap$ * " in " * cite$ * warning$ 424 | } 425 | { swap$ 426 | pop$ 427 | } 428 | if$ 429 | } 430 | if$ 431 | } 432 | FUNCTION {format.url} 433 | { url empty$ 434 | { "" } 435 | { "\urlprefix\url{" url * "}" * } 436 | if$ 437 | } 438 | 439 | STRINGS { bibinfo} 440 | INTEGERS { nameptr namesleft numnames } 441 | 442 | FUNCTION {format.names} 443 | { 'bibinfo := 444 | duplicate$ empty$ 'skip$ { 445 | 's := 446 | "" 't := 447 | #1 'nameptr := 448 | s num.names$ 'numnames := 449 | numnames 'namesleft := 450 | { namesleft #0 > } 451 | { s nameptr 452 | "{f{.}.~}{vv~}{ll}{, jj}" 453 | format.name$ 454 | bibinfo bibinfo.check 455 | 't := 456 | nameptr #1 > 457 | { 458 | namesleft #1 > 459 | { ", " * t * } 460 | { 461 | "," * 462 | s nameptr "{ll}" format.name$ duplicate$ "others" = 463 | { 't := } 464 | { pop$ } 465 | if$ 466 | t "others" = 467 | { 468 | " " * bbl.etal * 469 | } 470 | { " " * t * } 471 | if$ 472 | } 473 | if$ 474 | } 475 | 't 476 | if$ 477 | nameptr #1 + 'nameptr := 478 | namesleft #1 - 'namesleft := 479 | } 480 | while$ 481 | } if$ 482 | } 483 | FUNCTION {format.names.ed} 484 | { 485 | format.names 486 | } 487 | FUNCTION {format.authors} 488 | { author "author" format.names 489 | } 490 | FUNCTION {get.bbl.editor} 491 | { editor num.names$ #1 > 'bbl.editors 'bbl.editor if$ } 492 | 493 | FUNCTION {format.editors} 494 | { editor "editor" format.names duplicate$ empty$ 'skip$ 495 | { 496 | " " * 497 | get.bbl.editor 498 | "(" swap$ * ")" * 499 | * 500 | } 501 | if$ 502 | } 503 | FUNCTION {format.doi} 504 | { doi "doi" bibinfo.check 505 | duplicate$ empty$ 'skip$ 506 | { 507 | new.block 508 | "\doi{" swap$ * "}" * 509 | } 510 | if$ 511 | } 512 | FUNCTION {format.note} 513 | { 514 | note empty$ 515 | { "" } 516 | { note #1 #1 substring$ 517 | duplicate$ "{" = 518 | 'skip$ 519 | { output.state mid.sentence = 520 | { "l" } 521 | { "u" } 522 | if$ 523 | change.case$ 524 | } 525 | if$ 526 | note #2 global.max$ substring$ * "note" bibinfo.check 527 | } 528 | if$ 529 | } 530 | 531 | FUNCTION {format.title} 532 | { title 533 | duplicate$ empty$ 'skip$ 534 | { "t" change.case$ } 535 | if$ 536 | "title" bibinfo.check 537 | } 538 | FUNCTION {output.bibitem} 539 | { newline$ 540 | "\bibitem{" write$ 541 | cite$ write$ 542 | "}" write$ 543 | newline$ 544 | "" 545 | before.all 'output.state := 546 | } 547 | 548 | FUNCTION {if.digit} 549 | { duplicate$ "0" = 550 | swap$ duplicate$ "1" = 551 | swap$ duplicate$ "2" = 552 | swap$ duplicate$ "3" = 553 | swap$ duplicate$ "4" = 554 | swap$ duplicate$ "5" = 555 | swap$ duplicate$ "6" = 556 | swap$ duplicate$ "7" = 557 | swap$ duplicate$ "8" = 558 | swap$ "9" = or or or or or or or or or 559 | } 560 | FUNCTION {n.separate} 561 | { 't := 562 | "" 563 | #0 'numnames := 564 | { t empty$ not } 565 | { t #-1 #1 substring$ if.digit 566 | { numnames #1 + 'numnames := } 567 | { #0 'numnames := } 568 | if$ 569 | t #-1 #1 substring$ swap$ * 570 | t #-2 global.max$ substring$ 't := 571 | numnames #5 = 572 | { duplicate$ #1 #2 substring$ swap$ 573 | #3 global.max$ substring$ 574 | "," swap$ * * 575 | } 576 | 'skip$ 577 | if$ 578 | } 579 | while$ 580 | } 581 | FUNCTION {n.dashify} 582 | { 583 | n.separate 584 | 't := 585 | "" 586 | { t empty$ not } 587 | { t #1 #1 substring$ "-" = 588 | { t #1 #2 substring$ "--" = not 589 | { "--" * 590 | t #2 global.max$ substring$ 't := 591 | } 592 | { { t #1 #1 substring$ "-" = } 593 | { "-" * 594 | t #2 global.max$ substring$ 't := 595 | } 596 | while$ 597 | } 598 | if$ 599 | } 600 | { t #1 #1 substring$ * 601 | t #2 global.max$ substring$ 't := 602 | } 603 | if$ 604 | } 605 | while$ 606 | } 607 | 608 | FUNCTION {word.in} 609 | { bbl.in 610 | " " * } 611 | 612 | FUNCTION {format.date} 613 | { 614 | "" 615 | duplicate$ empty$ 616 | year "year" bibinfo.check duplicate$ empty$ 617 | { swap$ 'skip$ 618 | { "there's a month but no year in " cite$ * warning$ } 619 | if$ 620 | * 621 | } 622 | { swap$ 'skip$ 623 | { 624 | swap$ 625 | " " * swap$ 626 | } 627 | if$ 628 | * 629 | } 630 | if$ 631 | duplicate$ empty$ 632 | 'skip$ 633 | { 634 | before.all 'output.state := 635 | " (" swap$ * ")" * 636 | } 637 | if$ 638 | } 639 | FUNCTION {format.btitle} 640 | { title "title" bibinfo.check 641 | duplicate$ empty$ 'skip$ 642 | { 643 | emphasize 644 | } 645 | if$ 646 | } 647 | FUNCTION {either.or.check} 648 | { empty$ 649 | 'pop$ 650 | { "can't use both " swap$ * " fields in " * cite$ * warning$ } 651 | if$ 652 | } 653 | FUNCTION {format.bvolume} 654 | { volume empty$ 655 | { "" } 656 | { bbl.volume volume tie.or.space.prefix 657 | "volume" bibinfo.check * * 658 | series "series" bibinfo.check 659 | duplicate$ empty$ 'pop$ 660 | { emphasize ", " * swap$ * } 661 | if$ 662 | "volume and number" number either.or.check 663 | } 664 | if$ 665 | } 666 | FUNCTION {format.number.series} 667 | { volume empty$ 668 | { number empty$ 669 | { series field.or.null } 670 | { series empty$ 671 | { number "number" bibinfo.check } 672 | { output.state mid.sentence = 673 | { bbl.number } 674 | { bbl.number capitalize } 675 | if$ 676 | number tie.or.space.prefix "number" bibinfo.check * * 677 | bbl.in space.word * 678 | series "series" bibinfo.check * 679 | } 680 | if$ 681 | } 682 | if$ 683 | } 684 | { "" } 685 | if$ 686 | } 687 | FUNCTION {is.num} 688 | { chr.to.int$ 689 | duplicate$ "0" chr.to.int$ < not 690 | swap$ "9" chr.to.int$ > not and 691 | } 692 | 693 | FUNCTION {extract.num} 694 | { duplicate$ 't := 695 | "" 's := 696 | { t empty$ not } 697 | { t #1 #1 substring$ 698 | t #2 global.max$ substring$ 't := 699 | duplicate$ is.num 700 | { s swap$ * 's := } 701 | { pop$ "" 't := } 702 | if$ 703 | } 704 | while$ 705 | s empty$ 706 | 'skip$ 707 | { pop$ s } 708 | if$ 709 | } 710 | 711 | FUNCTION {convert.edition} 712 | { extract.num "l" change.case$ 's := 713 | s "first" = s "1" = or 714 | { bbl.first 't := } 715 | { s "second" = s "2" = or 716 | { bbl.second 't := } 717 | { s "third" = s "3" = or 718 | { bbl.third 't := } 719 | { s "fourth" = s "4" = or 720 | { bbl.fourth 't := } 721 | { s "fifth" = s "5" = or 722 | { bbl.fifth 't := } 723 | { s #1 #1 substring$ is.num 724 | { s eng.ord 't := } 725 | { edition 't := } 726 | if$ 727 | } 728 | if$ 729 | } 730 | if$ 731 | } 732 | if$ 733 | } 734 | if$ 735 | } 736 | if$ 737 | t 738 | } 739 | 740 | FUNCTION {format.edition} 741 | { edition duplicate$ empty$ 'skip$ 742 | { 743 | convert.edition 744 | output.state mid.sentence = 745 | { "l" } 746 | { "t" } 747 | if$ change.case$ 748 | "edition" bibinfo.check 749 | " " * bbl.edition * 750 | } 751 | if$ 752 | } 753 | INTEGERS { multiresult } 754 | FUNCTION {multi.page.check} 755 | { 't := 756 | #0 'multiresult := 757 | { multiresult not 758 | t empty$ not 759 | and 760 | } 761 | { t #1 #1 substring$ 762 | duplicate$ "-" = 763 | swap$ duplicate$ "," = 764 | swap$ "+" = 765 | or or 766 | { #1 'multiresult := } 767 | { t #2 global.max$ substring$ 't := } 768 | if$ 769 | } 770 | while$ 771 | multiresult 772 | } 773 | FUNCTION {format.pages} 774 | { pages duplicate$ empty$ 'skip$ 775 | { duplicate$ multi.page.check 776 | { 777 | bbl.pages swap$ 778 | n.dashify 779 | } 780 | { 781 | bbl.page swap$ 782 | } 783 | if$ 784 | tie.or.space.prefix 785 | "pages" bibinfo.check 786 | * * 787 | } 788 | if$ 789 | } 790 | FUNCTION {first.page} 791 | { 't := 792 | "" 793 | { t empty$ not t #1 #1 substring$ "-" = not and } 794 | { t #1 #1 substring$ * 795 | t #2 global.max$ substring$ 't := 796 | } 797 | while$ 798 | } 799 | 800 | FUNCTION {format.journal.pages} 801 | { pages duplicate$ empty$ 'pop$ 802 | { swap$ duplicate$ empty$ 803 | { pop$ pop$ format.pages } 804 | { 805 | ", " * 806 | swap$ 807 | first.page 808 | "pages" bibinfo.check 809 | * 810 | } 811 | if$ 812 | } 813 | if$ 814 | } 815 | FUNCTION {format.journal.eid} 816 | { eid "eid" bibinfo.check 817 | duplicate$ empty$ 'pop$ 818 | { swap$ duplicate$ empty$ 'skip$ 819 | { 820 | ", " * 821 | } 822 | if$ 823 | swap$ * 824 | } 825 | if$ 826 | } 827 | FUNCTION {format.vol.num.pages} 828 | { volume field.or.null 829 | duplicate$ empty$ 'skip$ 830 | { 831 | "volume" bibinfo.check 832 | } 833 | if$ 834 | bolden 835 | number "number" bibinfo.check duplicate$ empty$ 'skip$ 836 | { 837 | swap$ duplicate$ empty$ 838 | { "there's a number but no volume in " cite$ * warning$ } 839 | 'skip$ 840 | if$ 841 | swap$ 842 | "(" swap$ * ")" * 843 | } 844 | if$ * 845 | eid empty$ 846 | { format.journal.pages } 847 | { format.journal.eid } 848 | if$ 849 | } 850 | 851 | FUNCTION {format.chapter.pages} 852 | { chapter empty$ 853 | 'format.pages 854 | { type empty$ 855 | { bbl.chapter } 856 | { type "l" change.case$ 857 | "type" bibinfo.check 858 | } 859 | if$ 860 | chapter tie.or.space.prefix 861 | "chapter" bibinfo.check 862 | * * 863 | pages empty$ 864 | 'skip$ 865 | { ", " * format.pages * } 866 | if$ 867 | } 868 | if$ 869 | } 870 | 871 | FUNCTION {format.booktitle} 872 | { 873 | booktitle "booktitle" bibinfo.check 874 | emphasize 875 | } 876 | FUNCTION {format.in.ed.booktitle} 877 | { format.booktitle duplicate$ empty$ 'skip$ 878 | { 879 | format.bvolume duplicate$ empty$ 'pop$ 880 | { ", " swap$ * * } 881 | if$ 882 | editor "editor" format.names.ed duplicate$ empty$ 'pop$ 883 | { 884 | bbl.edby 885 | " " * swap$ * 886 | swap$ 887 | "," * 888 | " " * swap$ 889 | * } 890 | if$ 891 | word.in swap$ * 892 | } 893 | if$ 894 | } 895 | FUNCTION {empty.misc.check} 896 | { author empty$ title empty$ howpublished empty$ 897 | month empty$ year empty$ note empty$ 898 | and and and and and 899 | { "all relevant fields are empty in " cite$ * warning$ } 900 | 'skip$ 901 | if$ 902 | } 903 | FUNCTION {format.thesis.type} 904 | { type duplicate$ empty$ 905 | 'pop$ 906 | { swap$ pop$ 907 | "t" change.case$ "type" bibinfo.check 908 | } 909 | if$ 910 | } 911 | FUNCTION {format.tr.number} 912 | { number "number" bibinfo.check 913 | type duplicate$ empty$ 914 | { pop$ bbl.techrep } 915 | 'skip$ 916 | if$ 917 | "type" bibinfo.check 918 | swap$ duplicate$ empty$ 919 | { pop$ "t" change.case$ } 920 | { tie.or.space.prefix * * } 921 | if$ 922 | } 923 | FUNCTION {format.article.crossref} 924 | { 925 | key duplicate$ empty$ 926 | { pop$ 927 | journal duplicate$ empty$ 928 | { "need key or journal for " cite$ * " to crossref " * crossref * warning$ } 929 | { "journal" bibinfo.check emphasize word.in swap$ * } 930 | if$ 931 | } 932 | { word.in swap$ * " " *} 933 | if$ 934 | " \cite{" * crossref * "}" * 935 | } 936 | FUNCTION {format.crossref.editor} 937 | { editor #1 "{vv~}{ll}" format.name$ 938 | "editor" bibinfo.check 939 | editor num.names$ duplicate$ 940 | #2 > 941 | { pop$ 942 | "editor" bibinfo.check 943 | " " * bbl.etal 944 | * 945 | } 946 | { #2 < 947 | 'skip$ 948 | { editor #2 "{ff }{vv }{ll}{ jj}" format.name$ "others" = 949 | { 950 | "editor" bibinfo.check 951 | " " * bbl.etal 952 | * 953 | } 954 | { 955 | bbl.and space.word 956 | * editor #2 "{vv~}{ll}" format.name$ 957 | "editor" bibinfo.check 958 | * 959 | } 960 | if$ 961 | } 962 | if$ 963 | } 964 | if$ 965 | } 966 | FUNCTION {format.book.crossref} 967 | { volume duplicate$ empty$ 968 | { "empty volume in " cite$ * "'s crossref of " * crossref * warning$ 969 | pop$ word.in 970 | } 971 | { bbl.volume 972 | capitalize 973 | swap$ tie.or.space.prefix "volume" bibinfo.check * * bbl.of space.word * 974 | } 975 | if$ 976 | editor empty$ 977 | editor field.or.null author field.or.null = 978 | or 979 | { key empty$ 980 | { series empty$ 981 | { "need editor, key, or series for " cite$ * " to crossref " * 982 | crossref * warning$ 983 | "" * 984 | } 985 | { series emphasize * } 986 | if$ 987 | } 988 | { key * } 989 | if$ 990 | } 991 | { format.crossref.editor * } 992 | if$ 993 | " \cite{" * crossref * "}" * 994 | } 995 | FUNCTION {format.incoll.inproc.crossref} 996 | { 997 | editor empty$ 998 | editor field.or.null author field.or.null = 999 | or 1000 | { key empty$ 1001 | { format.booktitle duplicate$ empty$ 1002 | { "need editor, key, or booktitle for " cite$ * " to crossref " * 1003 | crossref * warning$ 1004 | } 1005 | { word.in swap$ * } 1006 | if$ 1007 | } 1008 | { word.in key * " " *} 1009 | if$ 1010 | } 1011 | { word.in format.crossref.editor * " " *} 1012 | if$ 1013 | " \cite{" * crossref * "}" * 1014 | } 1015 | FUNCTION {format.org.or.pub} 1016 | { 't := 1017 | "" 1018 | year empty$ 1019 | { "empty year in " cite$ * warning$ } 1020 | 'skip$ 1021 | if$ 1022 | address empty$ t empty$ and 1023 | year empty$ and 1024 | 'skip$ 1025 | { 1026 | add.blank "(" * 1027 | t empty$ 1028 | { address "address" bibinfo.check * 1029 | } 1030 | { t * 1031 | address empty$ 1032 | 'skip$ 1033 | { ", " * address "address" bibinfo.check * } 1034 | if$ 1035 | } 1036 | if$ 1037 | year empty$ 1038 | 'skip$ 1039 | { t empty$ address empty$ and 1040 | 'skip$ 1041 | { ", " * } 1042 | if$ 1043 | year "year" bibinfo.check 1044 | * 1045 | } 1046 | if$ 1047 | ")" * 1048 | } 1049 | if$ 1050 | } 1051 | FUNCTION {format.publisher.address} 1052 | { publisher "publisher" bibinfo.warn format.org.or.pub 1053 | } 1054 | 1055 | FUNCTION {format.organization.address} 1056 | { organization "organization" bibinfo.check format.org.or.pub 1057 | } 1058 | 1059 | FUNCTION {article} 1060 | { output.bibitem 1061 | format.authors "author" output.check 1062 | add.comma 1063 | crossref missing$ 1064 | { 1065 | journal 1066 | "journal" bibinfo.check 1067 | "journal" output.check 1068 | add.blank 1069 | format.vol.num.pages output 1070 | format.date "year" output.check 1071 | } 1072 | { format.article.crossref output.nonnull 1073 | format.pages output 1074 | } 1075 | if$ 1076 | format.doi output 1077 | new.block 1078 | format.url output 1079 | new.block 1080 | format.note output 1081 | fin.entry 1082 | } 1083 | FUNCTION {book} 1084 | { output.bibitem 1085 | author empty$ 1086 | { format.editors "author and editor" output.check 1087 | } 1088 | { format.authors output.nonnull 1089 | crossref missing$ 1090 | { "author and editor" editor either.or.check } 1091 | 'skip$ 1092 | if$ 1093 | } 1094 | if$ 1095 | add.comma 1096 | format.btitle "title" output.check 1097 | crossref missing$ 1098 | { format.bvolume output 1099 | format.edition output 1100 | new.block 1101 | format.number.series output 1102 | new.sentence 1103 | format.publisher.address output 1104 | } 1105 | { 1106 | new.block 1107 | format.book.crossref output.nonnull 1108 | format.date "year" output.check 1109 | } 1110 | if$ 1111 | format.doi output 1112 | new.block 1113 | format.url output 1114 | new.block 1115 | format.note output 1116 | fin.entry 1117 | } 1118 | FUNCTION {booklet} 1119 | { output.bibitem 1120 | format.authors output 1121 | add.comma 1122 | format.title "title" output.check 1123 | new.block 1124 | howpublished "howpublished" bibinfo.check output 1125 | address "address" bibinfo.check output 1126 | format.date output 1127 | format.doi output 1128 | new.block 1129 | format.url output 1130 | new.block 1131 | format.note output 1132 | fin.entry 1133 | } 1134 | 1135 | FUNCTION {inbook} 1136 | { output.bibitem 1137 | author empty$ 1138 | { format.editors "author and editor" output.check 1139 | } 1140 | { format.authors output.nonnull 1141 | crossref missing$ 1142 | { "author and editor" editor either.or.check } 1143 | 'skip$ 1144 | if$ 1145 | } 1146 | if$ 1147 | add.comma 1148 | format.btitle "title" output.check 1149 | crossref missing$ 1150 | { 1151 | format.publisher.address output 1152 | format.bvolume output 1153 | format.edition output 1154 | format.chapter.pages "chapter and pages" output.check 1155 | new.block 1156 | format.number.series output 1157 | new.sentence 1158 | } 1159 | { 1160 | format.chapter.pages "chapter and pages" output.check 1161 | new.block 1162 | format.book.crossref output.nonnull 1163 | format.date "year" output.check 1164 | } 1165 | if$ 1166 | format.doi output 1167 | new.block 1168 | format.url output 1169 | new.block 1170 | format.note output 1171 | fin.entry 1172 | } 1173 | 1174 | FUNCTION {incollection} 1175 | { output.bibitem 1176 | format.authors "author" output.check 1177 | add.comma 1178 | crossref missing$ 1179 | { format.in.ed.booktitle "booktitle" output.check 1180 | format.edition output 1181 | format.number.series output 1182 | format.publisher.address output 1183 | format.chapter.pages output 1184 | new.sentence 1185 | } 1186 | { format.incoll.inproc.crossref output.nonnull 1187 | format.chapter.pages output 1188 | } 1189 | if$ 1190 | format.doi output 1191 | new.block 1192 | format.url output 1193 | new.block 1194 | format.note output 1195 | fin.entry 1196 | } 1197 | FUNCTION {inproceedings} 1198 | { output.bibitem 1199 | format.authors "author" output.check 1200 | add.comma 1201 | crossref missing$ 1202 | { format.in.ed.booktitle "booktitle" output.check 1203 | new.sentence 1204 | publisher empty$ 1205 | { format.organization.address output } 1206 | { organization "organization" bibinfo.check output 1207 | format.publisher.address output 1208 | } 1209 | if$ 1210 | format.bvolume output 1211 | format.number.series output 1212 | format.pages output 1213 | } 1214 | { format.incoll.inproc.crossref output.nonnull 1215 | format.pages output 1216 | } 1217 | if$ 1218 | format.doi output 1219 | new.block 1220 | format.url output 1221 | new.block 1222 | format.note output 1223 | fin.entry 1224 | } 1225 | FUNCTION {conference} { inproceedings } 1226 | FUNCTION {manual} 1227 | { output.bibitem 1228 | author empty$ 1229 | { organization "organization" bibinfo.check 1230 | duplicate$ empty$ 'pop$ 1231 | { output 1232 | address "address" bibinfo.check output 1233 | } 1234 | if$ 1235 | } 1236 | { format.authors output.nonnull } 1237 | if$ 1238 | add.comma 1239 | format.btitle "title" output.check 1240 | author empty$ 1241 | { organization empty$ 1242 | { 1243 | address new.block.checka 1244 | address "address" bibinfo.check output 1245 | } 1246 | 'skip$ 1247 | if$ 1248 | } 1249 | { 1250 | organization address new.block.checkb 1251 | organization "organization" bibinfo.check output 1252 | address "address" bibinfo.check output 1253 | } 1254 | if$ 1255 | format.edition output 1256 | format.date output 1257 | format.doi output 1258 | new.block 1259 | format.url output 1260 | new.block 1261 | format.note output 1262 | fin.entry 1263 | } 1264 | 1265 | FUNCTION {mastersthesis} 1266 | { output.bibitem 1267 | format.authors "author" output.check 1268 | add.comma 1269 | format.title 1270 | "title" output.check 1271 | new.block 1272 | bbl.mthesis format.thesis.type output.nonnull 1273 | school "school" bibinfo.warn output 1274 | address "address" bibinfo.check output 1275 | format.date "year" output.check 1276 | format.doi output 1277 | new.block 1278 | format.url output 1279 | new.block 1280 | format.note output 1281 | fin.entry 1282 | } 1283 | 1284 | FUNCTION {misc} 1285 | { output.bibitem 1286 | format.authors output 1287 | title howpublished new.block.checkb 1288 | format.title output 1289 | howpublished new.block.checka 1290 | howpublished "howpublished" bibinfo.check output 1291 | format.date output 1292 | format.doi output 1293 | new.block 1294 | format.url output 1295 | new.block 1296 | format.note output 1297 | fin.entry 1298 | empty.misc.check 1299 | } 1300 | FUNCTION {phdthesis} 1301 | { output.bibitem 1302 | format.authors "author" output.check 1303 | add.comma 1304 | format.title 1305 | "title" output.check 1306 | new.block 1307 | bbl.phdthesis format.thesis.type output.nonnull 1308 | school "school" bibinfo.warn output 1309 | address "address" bibinfo.check output 1310 | format.date "year" output.check 1311 | format.doi output 1312 | new.block 1313 | format.url output 1314 | new.block 1315 | format.note output 1316 | fin.entry 1317 | } 1318 | 1319 | FUNCTION {proceedings} 1320 | { output.bibitem 1321 | editor empty$ 1322 | { organization "organization" bibinfo.check output 1323 | } 1324 | { format.editors output.nonnull } 1325 | if$ 1326 | new.block 1327 | format.btitle "title" output.check 1328 | format.bvolume output 1329 | format.number.series output 1330 | editor empty$ 1331 | { publisher empty$ 1332 | 'skip$ 1333 | { 1334 | new.sentence 1335 | format.publisher.address output 1336 | } 1337 | if$ 1338 | } 1339 | { publisher empty$ 1340 | { 1341 | new.sentence 1342 | format.organization.address output } 1343 | { 1344 | new.sentence 1345 | organization "organization" bibinfo.check output 1346 | format.publisher.address output 1347 | } 1348 | if$ 1349 | } 1350 | if$ 1351 | format.doi output 1352 | new.block 1353 | format.url output 1354 | new.block 1355 | format.note output 1356 | fin.entry 1357 | } 1358 | 1359 | FUNCTION {techreport} 1360 | { output.bibitem 1361 | format.authors "author" output.check 1362 | add.comma 1363 | format.title 1364 | "title" output.check 1365 | new.block 1366 | format.tr.number output.nonnull 1367 | institution "institution" bibinfo.warn output 1368 | address "address" bibinfo.check output 1369 | format.date "year" output.check 1370 | format.doi output 1371 | new.block 1372 | format.url output 1373 | new.block 1374 | format.note output 1375 | fin.entry 1376 | } 1377 | 1378 | FUNCTION {unpublished} 1379 | { output.bibitem 1380 | format.authors "author" output.check 1381 | add.comma 1382 | format.title "title" output.check 1383 | format.date output 1384 | format.doi output 1385 | new.block 1386 | format.url output 1387 | new.block 1388 | format.note "note" output.check 1389 | fin.entry 1390 | } 1391 | 1392 | FUNCTION {default.type} { misc } 1393 | READ 1394 | STRINGS { longest.label } 1395 | INTEGERS { number.label longest.label.width } 1396 | FUNCTION {initialize.longest.label} 1397 | { "" 'longest.label := 1398 | #1 'number.label := 1399 | #0 'longest.label.width := 1400 | } 1401 | FUNCTION {longest.label.pass} 1402 | { number.label int.to.str$ 'label := 1403 | number.label #1 + 'number.label := 1404 | label width$ longest.label.width > 1405 | { label 'longest.label := 1406 | label width$ 'longest.label.width := 1407 | } 1408 | 'skip$ 1409 | if$ 1410 | } 1411 | EXECUTE {initialize.longest.label} 1412 | ITERATE {longest.label.pass} 1413 | FUNCTION {begin.bib} 1414 | { preamble$ empty$ 1415 | 'skip$ 1416 | { preamble$ write$ newline$ } 1417 | if$ 1418 | "\begin{thebibliography}{" longest.label * "}" * 1419 | write$ newline$ 1420 | "\providecommand{\url}[1]{{#1}}" 1421 | write$ newline$ 1422 | "\providecommand{\urlprefix}{URL }" 1423 | write$ newline$ 1424 | "\expandafter\ifx\csname urlstyle\endcsname\relax" 1425 | write$ newline$ 1426 | " \providecommand{\doi}[1]{DOI \discretionary{}{}{}#1}\else" 1427 | write$ newline$ 1428 | " \providecommand{\doi}{DOI \discretionary{}{}{}\begingroup \urlstyle{rm}\Url}\fi" 1429 | write$ newline$ 1430 | } 1431 | EXECUTE {begin.bib} 1432 | EXECUTE {init.state.consts} 1433 | ITERATE {call.type$} 1434 | FUNCTION {end.bib} 1435 | { newline$ 1436 | "\end{thebibliography}" write$ newline$ 1437 | } 1438 | EXECUTE {end.bib} 1439 | %% End of customized bst file 1440 | %% 1441 | %% End of file `spphys.bst'. 1442 | 1443 | --------------------------------------------------------------------------------