381 lines
13 KiB
Plaintext
381 lines
13 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"```\n",
|
|
"Technische Hochschule Ostwestfalen-Lippe, inIT\n",
|
|
"Kurs Künstliche Intelligenz (KI), Wintersemester 2020\n",
|
|
"Malte Schmidt, Arbeitsgruppe Diskrete Systeme\n",
|
|
"```\n",
|
|
"\n",
|
|
"# A\\*-Suche\n",
|
|
"\n",
|
|
"In diesem Notebook wird A*-Suche behandelt implementiert. Literaturreferenzen finden Sie in den Lösungshinweisen.\n",
|
|
"\n",
|
|
"Führen Sie die folgende Codezelle aus. Sie importiert benötigte Bibliotheken und stellt Klassen und Funktionen für eine Problembeschreibung mit einem Zustandsgraphen bereit."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"%matplotlib inline\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import math\n",
|
|
"import random\n",
|
|
"import heapq\n",
|
|
"\n",
|
|
"class Problem(object):\n",
|
|
" \"\"\"The abstract class for a formal problem. A new domain subclasses this,\n",
|
|
" overriding `actions` and `results`, and perhaps other methods.\n",
|
|
" The default heuristic is 0 and the default action cost is 1 for all states.\n",
|
|
" When yiou create an instance of a subclass, specify `initial`, and `goal` states \n",
|
|
" (or give an `is_goal` method) and perhaps other keyword args for the subclass.\"\"\"\n",
|
|
"\n",
|
|
" def __init__(self, initial=None, goal=None, **kwds): \n",
|
|
" self.__dict__.update(initial=initial, goal=goal, **kwds) \n",
|
|
" \n",
|
|
" def actions(self, state): raise NotImplementedError\n",
|
|
" def result(self, state, action): raise NotImplementedError\n",
|
|
" def is_goal(self, state): return state == self.goal\n",
|
|
" def action_cost(self, s, a, s1): return 1\n",
|
|
" def h(self, node): return 0\n",
|
|
" \n",
|
|
" def __str__(self):\n",
|
|
" return '{}({!r}, {!r})'.format(\n",
|
|
" type(self).__name__, self.initial, self.goal)\n",
|
|
" \n",
|
|
"\n",
|
|
"class Node:\n",
|
|
" \"A Node in a search tree.\"\n",
|
|
" def __init__(self, state, parent=None, action=None, path_cost=0):\n",
|
|
" self.__dict__.update(state=state, parent=parent, action=action, path_cost=path_cost)\n",
|
|
"\n",
|
|
" def __repr__(self): return '<{}>'.format(self.state)\n",
|
|
" def __len__(self): return 0 if self.parent is None else (1 + len(self.parent))\n",
|
|
" def __lt__(self, other): return self.path_cost < other.path_cost\n",
|
|
" \n",
|
|
" \n",
|
|
"failure = Node('failure', path_cost=math.inf) # Indicates an algorithm couldn't find a solution.\n",
|
|
"cutoff = Node('cutoff', path_cost=math.inf) # Indicates iterative deepening search was cut off.\n",
|
|
" \n",
|
|
" \n",
|
|
"def expand(problem, node):\n",
|
|
" \"Expand a node, generating the children nodes.\"\n",
|
|
" s = node.state\n",
|
|
" for action in problem.actions(s):\n",
|
|
" s1 = problem.result(s, action)\n",
|
|
" cost = node.path_cost + problem.action_cost(s, action, s1)\n",
|
|
" yield Node(s1, node, action, cost)\n",
|
|
" \n",
|
|
"\n",
|
|
"def path_actions(node):\n",
|
|
" \"The sequence of actions to get to this node.\"\n",
|
|
" if node.parent is None:\n",
|
|
" return [] \n",
|
|
" return path_actions(node.parent) + [node.action]\n",
|
|
"\n",
|
|
"\n",
|
|
"def path_states(node):\n",
|
|
" \"The sequence of states to get to this node.\"\n",
|
|
" if node in (cutoff, failure, None): \n",
|
|
" return []\n",
|
|
" return path_states(node.parent) + [node.state]\n",
|
|
"\n",
|
|
"failure = Node('failure', path_cost=math.inf) # Indicates an algorithm couldn't find a solution."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Die A*-Suche benötigt eine Warteschlange mit Prioritäten für die Implementation. Diese Warteschlange ist in der folgenden Codezelle implementiert."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class PriorityQueue:\n",
|
|
" \"\"\"A queue in which the item with minimum f(item) is always popped first.\"\"\"\n",
|
|
"\n",
|
|
" def __init__(self, items=(), key=lambda x: x): \n",
|
|
" self.key = key\n",
|
|
" self.items = [] # a heap of (score, item) pairs\n",
|
|
" for item in items:\n",
|
|
" self.add(item)\n",
|
|
" \n",
|
|
" def add(self, item):\n",
|
|
" \"\"\"Add item to the queuez.\"\"\"\n",
|
|
" pair = (self.key(item), item)\n",
|
|
" heapq.heappush(self.items, pair)\n",
|
|
"\n",
|
|
" def pop(self):\n",
|
|
" \"\"\"Pop and return the item with min f(item) value.\"\"\"\n",
|
|
" return heapq.heappop(self.items)[1]\n",
|
|
" \n",
|
|
" def top(self): return self.items[0][1]\n",
|
|
"\n",
|
|
" def __len__(self): return len(self.items)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Gitterprobleme\n",
|
|
"Im folgenden werden Probleme betrachtet bei denen durch ein 2D Gitter navigiert werden muss. Einige Zellen sind unpassierbar und es muss ein Weg vom Start zum Ziel gefunden werden. Es kann zu jeder der acht benachbarten Zellen navigiert werden, also ist auch eine diagonale Bewegung möglich. Die eingesetzte Heuristik ist eine gerade Linie vom Start zum Ziel. Ein Zustand ist duch eine Zellposition `(x, y)` beschrieben und Aktionen werden durch ein Tupel `(dx, dy)` beschrieben, in dem die Differenz zur aktuellen Koordinate angegeben wird (entweder 0, 1 oder -1). `(0, -1)` bedeutet, dass die x-Koordinate gleich bleibt und die y-Koordinate um 1 verrringert wird. Es handelt sich also um eine gerade Bewegung nach unten. Die `GridProblem`-Klasse beschreibt ein solches Problem. Die hier verwendete Heurisitk ist die euklidische Distanz zwischen zwei Punkten."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def g(n): return n.path_cost\n",
|
|
"\n",
|
|
"def euclidean_distance(A, B):\n",
|
|
" \"Eucledian distance between two points.\"\n",
|
|
" return sum(abs(a - b)**2 for (a, b) in zip(A, B)) ** 0.5\n",
|
|
"\n",
|
|
"class GridProblem(Problem):\n",
|
|
" \"\"\"Finding a path on a 2D grid with obstacles. Obstacles are (x, y) cells.\"\"\"\n",
|
|
"\n",
|
|
" def __init__(self, initial=(15, 30), goal=(130, 30), obstacles=(), **kwds):\n",
|
|
" Problem.__init__(self, initial=initial, goal=goal, \n",
|
|
" obstacles=set(obstacles) - {initial, goal}, **kwds)\n",
|
|
"\n",
|
|
" directions = [(-1, -1), (0, -1), (1, -1),\n",
|
|
" (-1, 0), (1, 0),\n",
|
|
" (-1, +1), (0, +1), (1, +1)]\n",
|
|
" \n",
|
|
" def action_cost(self, s, action, s1): return euclidean_distance(s, s1)\n",
|
|
" \n",
|
|
" def h(self, node): return euclidean_distance(node.state, self.goal)\n",
|
|
" \n",
|
|
" def result(self, state, action): \n",
|
|
" \"Both states and actions are represented by (x, y) pairs.\"\n",
|
|
" return action if action not in self.obstacles else state\n",
|
|
" \n",
|
|
" def actions(self, state):\n",
|
|
" \"\"\"You can move one cell in any of `directions` to a non-obstacle cell.\"\"\"\n",
|
|
" x, y = state\n",
|
|
" return {(x + dx, y + dy) for (dx, dy) in self.directions} - self.obstacles"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"In der folgenden Codezelle werden einige Gitternetzprobleme mit Hindernissen erstellt. "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Some grid routing problems\n",
|
|
"\n",
|
|
"# The following can be used to create obstacles:\n",
|
|
" \n",
|
|
"def random_lines(X=range(15, 130), Y=range(60), N=150, lengths=range(6, 12)):\n",
|
|
" \"\"\"The set of cells in N random lines of the given lengths.\"\"\"\n",
|
|
" result = set()\n",
|
|
" for _ in range(N):\n",
|
|
" x, y = random.choice(X), random.choice(Y)\n",
|
|
" dx, dy = random.choice(((0, 1), (1, 0)))\n",
|
|
" result |= line(x, y, dx, dy, random.choice(lengths))\n",
|
|
" return result\n",
|
|
"\n",
|
|
"def line(x, y, dx, dy, length):\n",
|
|
" \"\"\"A line of `length` cells starting at (x, y) and going in (dx, dy) direction.\"\"\"\n",
|
|
" return {(x + i * dx, y + i * dy) for i in range(length)}\n",
|
|
"\n",
|
|
"random.seed(42) # To make this reproducible\n",
|
|
"\n",
|
|
"frame = line(-10, 20, 0, 1, 20) | line(150, 20, 0, 1, 20)\n",
|
|
"cup = line(102, 44, -1, 0, 15) | line(102, 20, -1, 0, 20) | line(102, 44, 0, -1, 24)\n",
|
|
"\n",
|
|
"d1 = GridProblem(obstacles=random_lines(N=100) | frame)\n",
|
|
"d2 = GridProblem(obstacles=random_lines(N=150) | frame)\n",
|
|
"d3 = GridProblem(obstacles=random_lines(N=200) | frame)\n",
|
|
"d4 = GridProblem(obstacles=random_lines(N=250) | frame)\n",
|
|
"d5 = GridProblem(obstacles=random_lines(N=300) | frame)\n",
|
|
"d6 = GridProblem(obstacles=cup | frame)\n",
|
|
"d7 = GridProblem(obstacles=cup | frame | line(50, 35, 0, -1, 10) | line(60, 37, 0, -1, 17) | line(70, 31, 0, -1, 19))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## A*-Suche\n",
|
|
"Es soll die A\\*-Suche für Gitternetzprobleme an Beispielen untersucht werden. Die Pfadkosten g(n) und die euklidische Distanz als Heurisitk wurden oben definiert."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def astar_search(problem):\n",
|
|
" \"\"\"Search nodes with minimum f(n) = g(n) + h(n).\"\"\"\n",
|
|
" h = problem.h # heuristic\n",
|
|
" f = lambda n: g(n) + h(n)\n",
|
|
" global reached\n",
|
|
" node = Node(problem.initial)\n",
|
|
" frontier = PriorityQueue([node], key=f)\n",
|
|
" reached = {problem.initial: node}\n",
|
|
" while frontier:\n",
|
|
" node = frontier.pop()\n",
|
|
" if problem.is_goal(node.state):\n",
|
|
" return node\n",
|
|
" for child in expand(problem, node):\n",
|
|
" s = child.state\n",
|
|
" if s not in reached or child.path_cost < reached[s].path_cost:\n",
|
|
" reached[s] = child\n",
|
|
" frontier.add(child)\n",
|
|
" return failure"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def plot_grid_problem(grid, solution, reached=(), title='Search', show=True):\n",
|
|
" \"Use matplotlib to plot the grid, obstacles, solution, and reached.\"\n",
|
|
" reached = list(reached)\n",
|
|
" plt.figure(figsize=(16, 10))\n",
|
|
" plt.axis('off'); plt.axis('equal')\n",
|
|
" plt.scatter(*transpose(grid.obstacles), marker='s', color='darkgrey')\n",
|
|
" plt.scatter(*transpose(reached), 1**2, marker='.', c='blue')\n",
|
|
" plt.scatter(*transpose(path_states(solution)), marker='s', c='blue')\n",
|
|
" plt.scatter(*transpose([grid.initial]), 9**2, marker='D', c='green')\n",
|
|
" plt.scatter(*transpose([grid.goal]), 9**2, marker='8', c='red')\n",
|
|
" if show: plt.show()\n",
|
|
" print('{} {} search: {:.1f} path cost, {:,d} states reached'\n",
|
|
" .format(' ' * 10, title, solution.path_cost, len(reached)))\n",
|
|
" \n",
|
|
"def plots(grid, weights=(1.4, 2)): \n",
|
|
" \"\"\"Plot the results of 4 heuristic search algorithms for this grid.\"\"\"\n",
|
|
" solution = astar_search(grid)\n",
|
|
" plot_grid_problem(grid, solution, reached, 'A* search')\n",
|
|
" \n",
|
|
"def transpose(matrix): return list(zip(*matrix))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d2)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d3)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d4)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d5)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d6)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plots(d7)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"- Entspricht die A\\*-Suche ihren Erwartungen oder finden Sie, dass zu viele Punkte analysiert werden müssen? \n",
|
|
"- Woran könnte das liegen?\n",
|
|
"- Haben Sie Verbesserungsvorschläge?"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": []
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.8.3"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|