Skip to content

Commit 517a05a

Browse files
committed
search: implement the local-search variants that lack pseudocode (#1151)
#1151 tracks book algorithms with no written pseudocode. Added the local-search ones it names (AIMA Section 4.1), as simple, faithful implementations of the prose (each docstring notes there is no book pseudocode + the source section): - stochastic_hill_climbing (random uphill move) - first_choice_hill_climbing (random successors until one improves) - random_restart_hill_climbing (best of N random starts) - local_beam_search (keep the k best successors each step) Tests on a unimodal grid (single peak) where every variant reaches it.
1 parent 28b5cd4 commit 517a05a

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

‎aima/search.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,79 @@ def hill_climbing(problem):
954954
return current.state
955955

956956

957+
# The following hill-climbing variants and local beam search are described in
958+
# AIMA Section 4.1 (Local Search) but the book gives no pseudocode for them, so
959+
# these are simple, faithful implementations of the prose descriptions.
960+
961+
962+
def stochastic_hill_climbing(problem):
963+
"""Hill climbing that, instead of the steepest neighbor, picks *at random*
964+
among the uphill neighbors (AIMA 4e Section 4.1.1 -- no pseudocode in the book).
965+
"""
966+
current = Node(problem.initial)
967+
while True:
968+
uphill = [node for node in current.expand(problem)
969+
if problem.value(node.state) > problem.value(current.state)]
970+
if not uphill:
971+
break
972+
current = random.choice(uphill)
973+
return current.state
974+
975+
976+
def first_choice_hill_climbing(problem, tries=100):
977+
"""Hill climbing that generates successors at random until it finds one better
978+
than the current state -- useful when a state has very many successors (AIMA 4e
979+
Section 4.1.1 -- no pseudocode in the book).
980+
"""
981+
current = Node(problem.initial)
982+
while True:
983+
neighbors = current.expand(problem)
984+
if not neighbors:
985+
break
986+
better = None
987+
for _ in range(tries):
988+
candidate = random.choice(neighbors)
989+
if problem.value(candidate.state) > problem.value(current.state):
990+
better = candidate
991+
break
992+
if better is None:
993+
break
994+
current = better
995+
return current.state
996+
997+
998+
def random_restart_hill_climbing(problem, new_state, restarts=10):
999+
"""Run :func:`hill_climbing` from ``restarts`` random initial states (each from
1000+
the 0-argument ``new_state`` callable) and return the best state found (AIMA 4e
1001+
Section 4.1.1 -- no pseudocode in the book). Note: it sets ``problem.initial``.
1002+
"""
1003+
best = None
1004+
for _ in range(restarts):
1005+
problem.initial = new_state()
1006+
state = hill_climbing(problem)
1007+
if best is None or problem.value(state) > problem.value(best):
1008+
best = state
1009+
return best
1010+
1011+
1012+
def local_beam_search(problem, k=4, new_state=None, iterations=1000):
1013+
"""Keep ``k`` states; at each step expand all of them and keep the ``k`` best
1014+
successors, stopping when none improves on the current best (AIMA 4e Section
1015+
4.1.3 -- no pseudocode in the book). The initial ``k`` states come from
1016+
``new_state`` (a 0-argument callable) or default to the problem's initial state.
1017+
"""
1018+
current = [Node(new_state() if new_state else problem.initial) for _ in range(k)]
1019+
for _ in range(iterations):
1020+
neighbors = [nb for node in current for nb in node.expand(problem)]
1021+
if not neighbors:
1022+
break
1023+
best = sorted(neighbors, key=lambda node: problem.value(node.state), reverse=True)[:k]
1024+
if problem.value(best[0].state) <= max(problem.value(node.state) for node in current):
1025+
break
1026+
current = best
1027+
return max(current, key=lambda node: problem.value(node.state)).state
1028+
1029+
9571030
def exp_schedule(k=20, lam=0.005, limit=100):
9581031
"""One possible schedule function for simulated annealing"""
9591032
return lambda t: (k * np.exp(-lam * t) if t < limit else 0)

‎tests/test_search.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,21 @@ def test_grid_search_visualization():
512512
assert plot_grid_search(problem, expl_astar, path_astar) is not None
513513

514514

515+
def test_local_search_variants():
516+
# a unimodal grid (value = x + y + 1, single peak at (2, 2)): every local-search
517+
# variant climbs to it. These algorithms have no pseudocode in the book (#1151).
518+
random.seed(0)
519+
grid = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
520+
assert stochastic_hill_climbing(PeakFindingProblem((0, 0), grid)) == (2, 2)
521+
assert first_choice_hill_climbing(PeakFindingProblem((0, 0), grid)) == (2, 2)
522+
assert local_beam_search(PeakFindingProblem((0, 0), grid), k=3) == (2, 2)
523+
n, m = len(grid), len(grid[0])
524+
best = random_restart_hill_climbing(
525+
PeakFindingProblem((0, 0), grid),
526+
lambda: (random.randrange(n), random.randrange(m)), restarts=5)
527+
assert best == (2, 2)
528+
529+
515530
def test_node_path_states():
516531
# the full root->goal path is one call away from the returned Node (#1068)
517532
node = astar_search(GraphProblem('Arad', 'Bucharest', romania_map))

0 commit comments

Comments
 (0)