Examples¶
Game¶
Exercise (intermediate)
Implement and test the game problem from the
MDPFromMapping.Implement and test the value iteration algorithm. The output of this algorithm is a policy mapping and a value mapping (the optimal ones).
Hint 2.
To keep things more readable, a function qualities_from_state is also implemented.
1def value_iteration_empty[S: State](
2 mdp: MDPProblem[S], discount: float = 1.0, convergence_limit: float = 1e-3
3) -> tuple[PolicyMapping, ValuesMapping]:
4 """approximate the optimal values and the optimal policy for every state."""
5 ...
1def qualities_from_state_empty[S: State](
2 mdp: MDPProblem[S], state: S, values: ValuesMapping, discount: float
3) -> set[tuple[Reward, Action]]:
4 """compute the current approximation of the qualities from 'state'
5 using the current approximations of values
6
7 returns a set of tuples(reward, action) in order to be able to arg max on action
8 to define the optimal policy"""
9 ...
Correction
These tests are almost trivial. They are there to force you to clearly understand the defined types.
1mdp_mapping_game: MDPMapping = {
2 "in": {
3 "stay": {"in": (2.0 / 3.0, 4.0), "out": (1.0 / 3.0, 5.0)},
4 "quit": {"out": (1.0, 10.0)},
5 },
6 "out": {},
7}
1@pytest.fixture
2def game_mdp():
3 mdp_mapping_game: MDPMapping = {
4 "in": {
5 "stay": {"in": (2.0 / 3.0, 4.0), "out": (1.0 / 3.0, 5.0)},
6 "quit": {"out": (1.0, 10.0)},
7 },
8 "out": {},
9 }
10 return MDPFromMapping(mdp_mapping_game)
1def test_actions_from_state(game_mdp):
2 assert game_mdp.actions_from_state("in") == {"quit", "stay"}
3 assert game_mdp.actions_from_state("out") == set()
4
5
6def test_outcomes_from_state_and_action(game_mdp):
7 assert game_mdp.outcomes_from_state_and_action("in", "stay") == {
8 "in": (2.0 / 3.0, 4.0),
9 "out": (1.0 / 3.0, 5.0), # dubious == with floats
10 }
11 assert game_mdp.outcomes_from_state_and_action("in", "quit") == {"out": (1.0, 10.0)}
12
13
14def test_states(game_mdp):
15 assert game_mdp.states == {"in", "out"}
16
17
18def test_is_end_state(game_mdp):
19 assert game_mdp.is_end_state("out")
1def value_iteration[S: State](
2 mdp: MDPProblem[S], discount: float = 1.0, convergence_limit: float = 1e-3
3) -> tuple[PolicyMapping, ValuesMapping]:
4 """approximate the optimal values and the optimal policy for every state."""
5 states: set[S] = mdp.states
6 values: ValuesMapping = {}.fromkeys(states, 0.0)
7 policy: PolicyMapping = {}
8 is_converged = False
9 while not is_converged:
10 is_converged = True
11 for state in states:
12 if not mdp.is_end_state(state):
13 qualities: set[tuple[Reward, Action]] = qualities_from_state(
14 mdp, state, values, discount
15 )
16 quality_opt, action_opt = max(qualities)
17 if abs(values[state] - quality_opt) > convergence_limit:
18 # if any value for any state changes significantly,
19 # the process has not converged
20 is_converged = False
21 values[state] = quality_opt
22 policy[state] = action_opt
23 return policy, values
1def qualities_from_state[S: State](
2 mdp: MDPProblem[S], state: S, values: ValuesMapping, discount: float
3) -> set[tuple[Reward, Action]]:
4 """compute the current approximation of the qualities from 'state'
5 using the current approximations of values
6
7 returns a set of tuples(reward, action) in order to be able to arg max on action
8 to define the optimal policy"""
9 qualities = set()
10 for action in mdp.actions_from_state(state):
11 outcomes: Outcomes = mdp.outcomes_from_state_and_action(state, action)
12 # for a given state: outcomes = dict[Next_State, tuple[Transition, Reward]]
13 # from the definition of qualities (with the current approx of values):
14 quality: Reward = sum(
15 transition * (reward + discount * values[next_state])
16 for next_state, (transition, reward) in outcomes.items()
17 )
18 qualities.add((quality, action))
19 return qualities
1import pytest
2
3from ai.reinforcement.mdp_types import Reward, Action, State, ValuesMapping
4from ai.reinforcement.mdp_iteration import qualities_from_state, value_iteration
5
6
7@pytest.mark.parametrize(
8 "values, expected",
9 [
10 ({"in": 0.0, "out": 0.0}, {"stay": 13.0 / 3.0, "quit": 10.0}),
11 ({"in": 10.0, "out": 0.0}, {"stay": 11.0, "quit": 10.0}),
12 ],
13)
14def test_qualities_from_state_in(
15 game_mdp, values: ValuesMapping, expected: dict[Action, Reward]
16):
17 # stay: (4 + 1*0) * 2/3 + (5 + 1*0) * 1/3 = (8+5)/3 = 13/3
18 # quit: (10 + 1*0) * 1 = 10
19 # stay: (4 + 1*10) * 2/3 + (5 + 1*0) * 1/3 = (28+5)/3 = 11
20 # quit: (10 + 1*0) * 1 = 10
21 qualities_in: set[tuple[Reward, Action]] = qualities_from_state(
22 game_mdp, "in", values, 1.0
23 )
24 for value, action in qualities_in:
25 assert pytest.approx(value) == expected[action]
26
27
28def test_value_iteration(game_mdp):
29 policy, values = value_iteration(game_mdp, 1.0, 1e-12)
30 assert policy == {"in": "stay"}
31 assert pytest.approx(values["in"]) == 13.0
32 assert pytest.approx(values["out"]) == 0.0
Magic tramway¶
Extending the magic tram problem from the state problem section, a failure probability is introduced when taking the magic tram. Taking the tram has still a higher cost than walking, still can make you move (state x tram_factor) but now with a fail probability to leave you at your current state.
Magic tram fail¶
Exercise (intermediate)
Implement a
MDPMagicTramproblem from theMDPProblemand test it.Interpret the value iteration algorithm for different values of the failure probability.
Correction
- src¶
1@dataclass(frozen=True) 2class MDPMagicTram(MDPProblem[StateMagicTram]): 3 """already encountered magic tram example from CS221. 4 from starting point to destination only moving forward, walking has a cost and makes you move +1. 5 Taking the tram has a higher cost, but makes you move (state x tram_factor) with a fail probability to leave 6 you at your current state""" 7 8 starting_point: StateMagicTram 9 destination: StateMagicTram 10 fail_probability: float = 0.0 11 tram_factor: int = 2 12 tram_cost: float = 2.0 13 walk_cost: float = 1.0 14 15 @override 16 def actions_from_state(self, state: StateMagicTram) -> set[Action]: 17 if state == self.destination: 18 return set() 19 # must not risk to overshoot with tram 20 if state * self.tram_factor <= self.destination: 21 return {"walk", "tram"} 22 return {"walk"} 23 24 @override 25 def outcomes_from_state_and_action( 26 self, state: StateMagicTram, action: Action 27 ) -> Outcomes: 28 if action == "walk": 29 return {state + 1: (1.0, -self.walk_cost)} 30 31 outcomes: Outcomes = { 32 state * self.tram_factor: (1.0 - self.fail_probability, -self.tram_cost), 33 state: (self.fail_probability, -self.tram_cost), 34 } 35 return outcomes 36 37 @property 38 @override 39 def states(self) -> set[StateMagicTram]: 40 return set(range(self.starting_point, self.destination + 1)) 41 42 @override 43 def is_end_state(self, state: State) -> bool: 44 return state == self.destination 45 46 def to_string( 47 self, 48 policy: PolicyMapping, 49 values: ValuesMapping, 50 ) -> str: 51 table = [] 52 for state, action in policy.items(): 53 if action == "tram": 54 table.append( 55 [state, Fore.RED + action + Style.RESET_ALL, f"{values[state]:.1f}"] # type: ignore 56 ) 57 else: 58 table.append([state, action, f"{values[state]:.1f}"]) 59 table.append([self.destination, "", 0.0]) 60 return tabulate(table, headers=["state", "action", "value"], tablefmt="grid")
tests¶1import pytest 2 3 4from ai.reinforcement.mdp_example_tram import MDPMagicTram 5 6 7@pytest.fixture 8def tram(): 9 return MDPMagicTram(2, 5, 0.1) 10 11 12def test_actions_from_state(tram): 13 assert tram.actions_from_state(2) == {"walk", "tram"} 14 assert tram.actions_from_state(5) == set() 15 assert tram.actions_from_state(4) == {"walk"} 16 17 18def test_outcomes_from_state_and_action(tram): 19 assert tram.outcomes_from_state_and_action(2, "tram") == { 20 2: (tram.fail_probability, -tram.tram_cost), 21 4: (1.0 - tram.fail_probability, -tram.tram_cost), # dubious == with floats 22 } 23 assert tram.outcomes_from_state_and_action(2, "walk") == {3: (1.0, -1.0)} 24 25 26def test_is_end_state(tram): 27 assert tram.is_end_state(tram.destination)
For the case of 2 to 13:
For a failure probability of 0, the problem is identical to the state problem already solved during a previous session. The value of state 2 is supposed to be identical to the total cost of the state problem.
For a failure probability of 1, it is obvious that one should not take the tram.
For a failure probability of 0.2, the value of state 2 is logically larger than a failure probability of 0. However, it is still worth it to take the tram at state 3 and 6.
For a failure probability of 0.4, the value of state 2 is even higher than a failure probability of 0.2. However, now it is not worth it to take the tram at state 3, the risk of failure and the associated cost is to large regarding the length of the jump. The tram at state 6 is still worth it.
failure probability 0.0
state
action
value
2
walk
-6
3
tram
-5
4
walk
-5
5
walk
-4
6
tram
-3
7
walk
-6
8
walk
-5
9
walk
-4
10
walk
-3
11
walk
-2
12
walk
-1
13
0
failure probability 1.0
state
action
value
2
walk
-11
3
walk
-10
4
walk
-9
5
walk
-8
6
walk
-7
7
walk
-6
8
walk
-5
9
walk
-4
10
walk
-3
11
walk
-2
12
walk
-1
13
0
failure probability 0.2
state
action
value
2
walk
-7
3
tram
-6
4
walk
-5.5
5
walk
-4.5
6
tram
-3.5
7
walk
-6
8
walk
-5
9
walk
-4
10
walk
-3
11
walk
-2
12
walk
-1
13
0
failure probability 0.4
state
action
value
2
walk
-8.3
3
walk
-7.3
4
walk
-6.3
5
walk
-5.3
6
tram
-4.3
7
walk
-6
8
walk
-5
9
walk
-4
10
walk
-3
11
walk
-2
12
walk
-1
13
0
Swamp isle¶
The swamp isle problem is similar as the one presented in the state model section. The isle is rectangular, there are some swamps. The goal is to reach the end point limiting the cost. However, walking on dry land has a cost and walking in swamps has an even higher cost. One can walk either vertically or horizontally (not diagonally).
The novelty is that the problem is now a MDP because every action can lead to an equiprobable orientation failure in any of the other potential directions: the chance of no failure is 1-failure probability.
Exercise (intermediate)
Implement a
MDPSwampIsleproblem from theMDPProblemand test it.Interpret the value iteration algorithm for different values of the failure probability.
Correction
- src¶
1@dataclass(frozen=True) 2class MDPSwampIsle(MDPProblem[IsleState]): 3 """the isle is rectangular, there are some swamps. 4 The goal is to reach the end point limiting the cost. 5 However, walking on dry land has a cost and walking in swamps has an even higher cost. 6 One can walk either vertically or horizontally (not diagonally). 7 Every action can lead to an orientation failure in any of the other potential directions: the chance of no failure is 1-failure probability. 8 """ 9 10 nb_rows: int 11 nb_cols: int 12 end_point: Coordinates 13 swamp_spots: set[Coordinates] = field(default_factory=set) 14 fail_probability: float = 0.0 15 walking_cost: Reward = 1.0 16 swamp_cost: Reward = 20.0 17 18 @override 19 def actions_from_state(self, state: IsleState) -> set[Action]: 20 coordinates = state 21 22 if self.is_end_state(state): 23 return set() 24 25 LAST_COL = self.nb_cols - 1 26 LAST_ROW = self.nb_rows - 1 27 actions = {"N", "E", "S", "W"} 28 29 # can not cross the domain edge 30 if coordinates[0] == 0: 31 actions.remove("N") 32 if coordinates[0] == LAST_ROW: 33 actions.remove("S") 34 if coordinates[1] == 0: 35 actions.remove("W") 36 if coordinates[1] == LAST_COL: 37 actions.remove("E") 38 39 return actions 40 41 @override 42 def outcomes_from_state_and_action( 43 self, state: IsleState, action: Action 44 ) -> Outcomes: 45 coordinates = state 46 47 if coordinates in self.swamp_spots: 48 reward = -self.swamp_cost 49 else: 50 reward = -self.walking_cost 51 52 deltas: dict[Action, Coordinates] = { 53 "N": (-1, 0), 54 "E": (0, 1), 55 "S": (1, 0), 56 "W": (0, -1), 57 } 58 59 # target state 60 delta = deltas[action] 61 target: IsleState = (coordinates[0] + delta[0], coordinates[1] + delta[1]) 62 63 # keep potential fail directions 64 actions: set[Action] = self.actions_from_state(state) 65 actions_to_drop = set(deltas).difference(actions) 66 actions_to_drop.add(action) 67 for action_to_drop in actions_to_drop: 68 del deltas[action_to_drop] 69 70 # build outcomes 71 outcomes: Outcomes = {target: (1.0 - self.fail_probability, reward)} 72 nb_slip_directions = len(deltas) 73 # the failing probability is uniformly spread on every fail direction 74 fail_in_one_direction_probability = self.fail_probability / nb_slip_directions 75 for delta in deltas.values(): 76 next_state: IsleState = ( 77 coordinates[0] + delta[0], 78 coordinates[1] + delta[1], 79 ) 80 outcomes[next_state] = ( 81 fail_in_one_direction_probability, 82 reward, # type: ignore 83 ) 84 return outcomes 85 86 @property 87 @override 88 def states(self) -> set[IsleState]: 89 return { 90 (row, col) for row in range(self.nb_rows) for col in range(self.nb_cols) 91 } 92 93 @override 94 def is_end_state(self, state: IsleState) -> bool: 95 coordinates = state 96 return coordinates == self.end_point 97 98 def to_string( 99 self, 100 policy: PolicyMapping | None = None, 101 values: ValuesMapping | None = None, 102 ) -> str: 103 isle: list[list[str]] = [] 104 for row in range(self.nb_rows): 105 isle.append(self.nb_cols * [""]) 106 107 # indicate swamps 108 for row, col in self.swamp_spots: 109 isle[row][col] += Back.GREEN + " X " + Style.RESET_ALL + "\n" # type: ignore 110 111 # indicate potential actions 112 for row in range(self.nb_rows): 113 for col in range(self.nb_cols): 114 coordinates: Coordinates = (row, col) 115 state: IsleState = coordinates 116 actions = self.actions_from_state(state) 117 if self.is_end_state(state): 118 actions = Back.RED + "END" + Style.RESET_ALL + "\n" # type: ignore 119 120 isle[row][col] += f"{actions}\n" 121 122 # indicate best action and expected value 123 if policy is not None and values is not None: 124 for state_, action in policy.items(): 125 row, col = cast(IsleState, state_) 126 value: Reward = values[state_] 127 isle[row][col] += ( 128 Fore.RED + f"[{action}] {value:.1f}" + Style.RESET_ALL # type: ignore 129 ) 130 131 return tabulate(isle, tablefmt="grid")
tests¶1import pytest 2 3from ai.reinforcement.mdp_example_swamp import MDPSwampIsle 4 5 6@pytest.fixture 7def problem(): 8 return MDPSwampIsle(3, 4, (2, 3), {(0, 2), (0, 3), (1, 1), (1, 2), (2, 2)}, 0.1) 9 10 11def test_actions_from_state(problem): 12 assert problem.actions_from_state((0, 0)) == {"S", "E"} 13 assert problem.actions_from_state((0, 1)) == {"S", "E", "W"} 14 assert problem.actions_from_state((1, 0)) == {"S", "E", "N"} 15 assert problem.actions_from_state((1, 1)) == {"N", "S", "E", "W"} 16 assert problem.actions_from_state((problem.nb_rows - 1, 0)) == {"N", "E"} 17 assert problem.actions_from_state((problem.nb_rows - 1, 1)) == {"N", "E", "W"} 18 assert problem.actions_from_state((0, problem.nb_cols - 1)) == {"S", "W"} 19 assert problem.actions_from_state((1, problem.nb_cols - 1)) == {"S", "W", "N"} 20 assert problem.actions_from_state(problem.end_point) == set() 21 22 23def test_outcomes_from_state_and_action(problem): 24 assert problem.outcomes_from_state_and_action((0, 0), "S") == { 25 (1, 0): ( 26 1.0 - problem.fail_probability, 27 -problem.walking_cost, 28 ), 29 (0, 1): ( 30 problem.fail_probability, 31 -problem.walking_cost, 32 ), # dubious == with floats 33 } 34 assert problem.outcomes_from_state_and_action((1, 0), "E") == { 35 (0, 0): ( 36 problem.fail_probability / 2, 37 -problem.walking_cost, 38 ), 39 (2, 0): ( 40 problem.fail_probability / 2, 41 -problem.walking_cost, 42 ), 43 (1, 1): ( 44 (1.0 - problem.fail_probability), 45 -problem.walking_cost, 46 ), # dubious == with floats 47 } 48 49 50def test_is_end_state(problem): 51 assert problem.is_end_state(problem.end_point)
For a failure probability of 0, the problem is identical to the former state search problem. The difference here is that the optimal policy and value is obtained for every state, not only for a starting state. The X symbols denote the swamps. The letter between brackets represent the optimal policy. The value of 25 is from (0,1) is the same as the one obtained in the state model exercise.
For a failure probability of 1, you are certain of taking an other direction that the one you ask for. As a result, you can see that the optimal policy is going the opposite of the correct direction in order to hope to fail on the right direction. The cost is higher because when you have several failing direction you are not sure to fail on the correct one, and you have to pay the cost of the uncertainty.
The failure probability of 0.2 gives the same policy as the failure probability of 0, just at a higher cost.
failure probability 0.0
{‘E’, ‘S’} [S] -24.0
{‘E’, ‘W’, ‘S’} [W] -25.0
X {‘E’, ‘W’, ‘S’} [S] -41.0
X {‘W’, ‘S’} [S] -21.0
{‘E’, ‘N’, ‘S’} [S] -23.0
X {‘E’, ‘W’, ‘N’, ‘S’} [S] -41.0
X {‘E’, ‘W’, ‘N’, ‘S’} [E] -21.0
{‘W’, ‘N’, ‘S’} [S] -1.0
{‘E’, ‘N’} [E] -22.0
{‘E’, ‘W’, ‘N’} [E] -21.0
X {‘E’, ‘W’, ‘N’} [E] -20.0
END
failure probability 1.0
{‘E’, ‘S’} [E] -51.0
{‘E’, ‘W’, ‘S’} [S] -62.7
X {‘E’, ‘W’, ‘S’} [S] -72.3
X {‘W’, ‘S’} [W] -42.0
{‘E’, ‘N’, ‘S’} [E] -50.0
X {‘E’, ‘W’, ‘N’, ‘S’} [E] -72.9
X {‘E’, ‘W’, ‘N’, ‘S’} [W] -65.8
{‘W’, ‘N’, ‘S’} [W] -22.0
{‘E’, ‘N’} [N] -47.0
{‘E’, ‘W’, ‘N’} [N] -46.0
X {‘E’, ‘W’, ‘N’} [N] -43.0
END
failure probability 0.2
{‘E’, ‘S’} [S] -38.8
{‘E’, ‘W’, ‘S’} [W] -42.9
X {‘E’, ‘W’, ‘S’} [S] -56.7
X {‘W’, ‘S’} [S] -38.0
{‘E’, ‘N’, ‘S’} [S] -36.5
X {‘E’, ‘W’, ‘N’, ‘S’} [S] -52.4
X {‘E’, ‘W’, ‘N’, ‘S’} [E] -35.8
{‘W’, ‘N’, ‘S’} [S] -8.4
{‘E’, ‘N’} [E] -33.0
{‘E’, ‘W’, ‘N’} [E] -30.9
X {‘E’, ‘W’, ‘N’} [E] -26.7
END