Answers You Derive Rather Than Learn
States, goals, and the one line that separates two search strategies
Week 2 was about learning from examples. This week is about the other half of artificial intelligence, the half that came first and is still running a great deal of software: getting an answer by searching through possibilities rather than by learning from data.
It matters for two reasons. Plenty of real problems have this shape and are solved badly with machine learning. And the vocabulary of this tradition, states, goals, heuristics and constraints, is how a lot of AI work is still described.
The problem shape
'...#........',
'...#........',
'.#......#..#',
'....#..#..#.',
'#...#...#...',
'..........##',
'...#........',
'.#..........',
'.#....#.##..',
'.##.#....#..',
'....##.###..',
'#.###..#.##.',
]
ROWS, COLS = len(GRID), len(GRID[0])
START, GOAL = (0, 0), (ROWS - 1, COLS - 1)
def neighbours(cell):
r, c = cell
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < ROWS and 0 <= nc < COLS and GRID[nr][nc] != '#':
yield (nr, nc)
def show(path):
marked = set(path or [])
for r in range(ROWS):
print(''.join('o' if (r, c) in marked else GRID[r][c]
for c in range(COLS)))
show([])
print()
print('start %s, goal %s' % (START, GOAL))
...#........
.#......#..#
....#..#..#.
#...#...#...
..........##
...#........
.#..........
.#....#.##..
.##.#....#..
....##.###..
#.###..#.##.
start (0, 0), goal (11, 11)
Breadth first: correct, and wasteful
Explore everything one step away, then everything two steps away, and so on. The first time you reach the goal you are guaranteed to have done it in the fewest possible moves, because you have already ruled out every shorter route.
'...#........',
'...#........',
'.#......#..#',
'....#..#..#.',
'#...#...#...',
'..........##',
'...#........',
'.#..........',
'.#....#.##..',
'.##.#....#..',
'....##.###..',
'#.###..#.##.',
]
ROWS, COLS = len(GRID), len(GRID[0])
START, GOAL = (0, 0), (ROWS - 1, COLS - 1)
def neighbours(cell):
r, c = cell
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < ROWS and 0 <= nc < COLS and GRID[nr][nc] != '#':
yield (nr, nc)
def show(path):
marked = set(path or [])
for r in range(ROWS):
print(''.join('o' if (r, c) in marked else GRID[r][c]
for c in range(COLS)))
from collections import deque
def breadth_first(start, goal):
queue = deque([[start]])
seen = {start}
expanded = 0
while queue:
path = queue.popleft()
expanded += 1
if path[-1] == goal:
return path, expanded
for nxt in neighbours(path[-1]):
if nxt not in seen:
seen.add(nxt)
queue.append(path + [nxt])
return None, expanded
path, expanded = breadth_first(START, GOAL)
show(path)
print()
print('%d steps, %d states examined' % (len(path) - 1, expanded))
o..#........
o#......#..#
oo..#..#..#.
#o..#...#...
.o........##
.oo#........
.#ooooooooo.
.#....#.##o.
.##.#....#o.
....##.###oo
#.###..#.##o
22 steps, 108 states examined
Depth first: cheap, and not shortest
'...#........',
'...#........',
'.#......#..#',
'....#..#..#.',
'#...#...#...',
'..........##',
'...#........',
'.#..........',
'.#....#.##..',
'.##.#....#..',
'....##.###..',
'#.###..#.##.',
]
ROWS, COLS = len(GRID), len(GRID[0])
START, GOAL = (0, 0), (ROWS - 1, COLS - 1)
def neighbours(cell):
r, c = cell
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < ROWS and 0 <= nc < COLS and GRID[nr][nc] != '#':
yield (nr, nc)
def show(path):
marked = set(path or [])
for r in range(ROWS):
print(''.join('o' if (r, c) in marked else GRID[r][c]
for c in range(COLS)))
def depth_first(start, goal):
stack = [[start]]
seen = {start}
expanded = 0
while stack:
path = stack.pop()
expanded += 1
if path[-1] == goal:
return path, expanded
for nxt in neighbours(path[-1]):
if nxt not in seen:
seen.add(nxt)
stack.append(path + [nxt])
return None, expanded
path, expanded = depth_first(START, GOAL)
print('%d steps, %d states examined' % (len(path) - 1, expanded))
print('breadth first found a shorter route, having looked at more')
breadth first found a shorter route, having looked at more
One line differs between those two functions: whether you take work from the front of the queue or the back. That single choice decides whether the answer is guaranteed shortest and how much memory the search needs, which is a good illustration of how much structure sits in very small decisions here.