Examples

Game

Exercise (intermediate)

  1. Implement and test the game problem from the MDPFromMapping.

  2. 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.

src
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    ...
src
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
  1. 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}
tests
 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)
tests
 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")
src
 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
src
 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
tests
 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

Magic tram fail

Exercise (intermediate)

  1. Implement a MDPMagicTram problem from the MDPProblem and test it.

  2. Interpret the value iteration algorithm for different values of the failure probability.

Correction
  1. 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)
    
  2. 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)

  1. Implement a MDPSwampIsle problem from the MDPProblem and test it.

  2. Interpret the value iteration algorithm for different values of the failure probability.

Correction
  1. 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