Examples¶
Environment¶
Before implementing the reinforcement learning algorithm, it is very useful to implement the real world from which episodes will be sampled. This environment will be based on a known MDP, and its job is to yield on demand random episodes which respect asymptotically the MDP (transitions and rewards). The goal of this entire section is to implement, test the reinforcement learning algorithms and show that they enable to learn from the interactions with the environment. Depending on the learning algorithm, either the MDP itself is estimated, or the qualities of a policy, or the optimum policy.
1"""Base types for a sampling environment"""
2
3from collections.abc import Generator
4from typing import Protocol
5from collections import namedtuple
6from dataclasses import dataclass, field
7
8from ai.reinforcement.mdp_types import (
9 State,
10 Action,
11 Transition,
12 Reward,
13 Policy,
14)
15
16
17type Outcome[S] = tuple[S, Reward] # careful, singular, single outcome
18
19Frame = namedtuple("Frame", ("state", "action", "reward", "next_state"))
20
21
22@dataclass
23class OutcomesForChoice[S: State]: # only reformatting mdp to ease random choice
24 outcomes: list[Outcome[S]] = field(default_factory=list)
25 transitions: list[Transition] = field(default_factory=list)
26
27
28class Environment[S: State](Protocol):
29 """an environnement is able to spit frames of episodes on demand.
30 A frame is a ("state", "action", "reward", "next_state") named tuple.
31 """
32
33 def actions_from_state(self, state: S) -> set[Action]: ...
34
35 def outcome_from_state_and_action(self, state: S, action: Action) -> Outcome[S]: ...
36
37 """careful, return a single random outcome following a given probability distribution"""
38
39 def is_end_state(self, state: S) -> bool: ...
40
41 def yield_frame_in_episode(self, state: S, policy: Policy) -> Generator[Frame]: ...
Exercise (intermediate)
Implement and test EnvironmentFromMDP.
Correction
1class EnvironmentFromMDP(Environment):
2 def __init__(self, mdp: MDPProblem):
3 self.mdp = mdp
4 self.outcomes_for_choice = self.format_outcomes_for_choice()
5
6 def format_outcomes_for_choice(
7 self,
8 ) -> dict[tuple[State, Action], OutcomesForChoice]:
9 transition_cases = {}
10 for state in self.mdp.states:
11 for action in self.mdp.actions_from_state(state):
12 outcomes: Outcomes = self.mdp.outcomes_from_state_and_action(
13 state, action
14 )
15 outcomes_for_choice = OutcomesForChoice()
16 for next_state, (transition, reward) in outcomes.items():
17 outcomes_for_choice.outcomes.append((next_state, reward))
18 outcomes_for_choice.transitions.append(transition)
19
20 transition_cases[(state, action)] = outcomes_for_choice
21 return transition_cases
22
23 @override
24 def actions_from_state(self, state: State) -> set[Action]:
25 return self.mdp.actions_from_state(state)
26
27 @override
28 def outcome_from_state_and_action(self, state: State, action: Action) -> Outcome:
29 return choices(
30 self.outcomes_for_choice[(state, action)].outcomes,
31 self.outcomes_for_choice[(state, action)].transitions,
32 )[0]
33
34 @override
35 def is_end_state(self, state: State) -> bool:
36 return not self.actions_from_state(state)
37
38 @override
39 def yield_frame_in_episode(self, state: State, policy: Policy) -> Generator[Frame]:
40 while not self.is_end_state(state):
41 actions = self.actions_from_state(state)
42 action = policy.action_from_state(state, actions)
43 next_state, reward = self.outcome_from_state_and_action(state, action)
44 yield Frame(state, action, reward, next_state)
45 state = next_state
1import pytest
2
3from ai.reinforcement.environment_types import OutcomesForChoice
4
5
6def test_format_outcomes_for_choice(game_environment):
7 expected = {
8 ("in", "quit"): OutcomesForChoice([("out", 10.0)], [1.0]),
9 ("in", "stay"): OutcomesForChoice(
10 [("in", 4.0), ("out", 5.0)], [2.0 / 3.0, 1.0 / 3.0]
11 ),
12 }
13 assert expected == game_environment.outcomes_for_choice # dubious float comparison
14
15
16def test_outcome_from_state_and_action(game_environment):
17 nb_trials = 1_000_000
18 nb_in = 0
19 for _ in range(nb_trials):
20 next_state, _ = game_environment.outcome_from_state_and_action("in", "stay")
21 if next_state == "in":
22 nb_in += 1
23 assert pytest.approx(nb_in / nb_trials, abs=0.01) == 2.0 / 3.0
24
25
26def test_yield_frame_in_episode(game_environment, policy_stay):
27 nb_episode = 100_000
28 nb_in = 0
29 nb_frame = 0
30 for _ in range(nb_episode):
31 for frame in game_environment.yield_frame_in_episode("in", policy_stay):
32 nb_frame += 1
33 if frame.next_state == "in":
34 nb_in += 1
35 assert pytest.approx(nb_in / nb_frame, abs=0.03) == 2.0 / 3.0
Game¶
Exercise (difficult)
For the four following questions, use a RandomPolicy.
Implement and test the model based Monte-Carlo algorithm. Add a
export_to_MDPMappingmethod.Implement and test the model free Monte-Carlo algorithm.
Implement and test the Sarsa algorithm.
Implement and test the Q-learning algorithm.
Hints
The four algorithms work iteratively: the estimation is incrementally improved. The easiest way is to loop a certain amount of times over all discovered states, each time generate an episode and use it with one of the algorithm.
Obviously, it would be even better have a convergence check: either for every estimated transition, or quality. This step is not implemented in the correction.
These algorithms are not easy to test because of the randomness from the environment sampling.
However, if the environment is an input parameter of the function, it is a prime example of a dependency injection.
It allows to feed a tweaked environment which behaves the same, but which is not random.
For example:
1class DummyGameEnvironment(EnvironmentFromMDP):
2 def __init__(self, mdp: MDPProblem):
3 super().__init__(mdp)
4 self.outcome_cycle = cycle((("in", 4.0), ("in", 4.0), ("out", 5.0)))
5
6 def outcome_from_state_and_action(self, state: State, action: Action) -> Outcome:
7 return next(self.outcome_cycle)
This environment will always produce the same episode.
Hints 1.
Maintain the number of times a (state, action, next_state) and a (state, action) has been encountered.
The ratio between the two is the estimation of a transition.
Use the class StateActionNextStateMetric in a defaultdict.
1@dataclass
2class StateActionNextStateMetric:
3 counter: int = 0
4 reward_sum: Reward = 0.0
Do not forget to update your knowledge of the existing states.
An approximation of the true transitions is expected.
Hints 2.
Maintain the number of times (state, action) has been encountered because
it will be the \(N\) in the convex combination.
Use reversed full episode and maintain the qualities for every (state, action).
Going in reverse, allows to easily calculate the discounted utility: \(u_t=r+\gamma u_{t+1}\).
If you update the quality (state, action) for every frame, it will automatically keep
the first encounter of every (state, action) of the episode.
It ease much easier to first obtain the utilities for every (state, action)
of the episode, and then perform the convex combination to update the qualities.
An approximation of the random policy qualities are expected.
Hints 3.
This the hardest of the four to implement because you need a pick view of the next action.
However, a frame is state, action, reward, next_state.
At a current frame, you do not have the next action.
A possibility is to loop by hand with a while loop,
calling next on the episode iterator.
This way you can manage to constantly have access to a frame and its successor.
However, pay attention if the next state is an end state or not.
An approximation of the random policy qualities are expected.
Hints 4.
Similar to the previous one, but easier. You no longer need the next action, just the next optimal action from the next state. Do not forget to update the optimal policy mapping right after the convex combination update.
An approximation of the optimal policy qualities are expected.
Correction
1def model_based_Monte_Carlo(
2 environment: Environment,
3 state_: State,
4 policy: Policy,
5 nb_episodes_per_state: int = 1_000,
6) -> MDPMapping:
7 """builds an estimated MDP and returns its mapping"""
8 states: set[State] = {state_}
9
10 S_A_NS_metric: dict[tuple[State, Action, NextState], StateActionNextStateMetric] = (
11 defaultdict(StateActionNextStateMetric)
12 ) # numerator transition fraction and denominator reward fraction
13 counter_state_action: dict[tuple[State, Action], int] = defaultdict(int)
14 # denominator transition fraction
15
16 # generate episode many times from all known states
17 for _ in range(nb_episodes_per_state):
18 for state in states.copy(): # must not iterate on a mutating set
19 episode: Generator[Frame] = environment.yield_frame_in_episode(
20 state, policy
21 )
22
23 if environment.is_end_state(state):
24 continue
25
26 # analyze episode
27 for state, action, reward, next_state in episode:
28 states.add(state)
29 states.add(next_state)
30 counter_state_action[(state, action)] += 1
31 metric = S_A_NS_metric[(state, action, next_state)]
32 metric.counter += 1
33 metric.reward_sum += reward
34 return export_to_MDPMapping(states, S_A_NS_metric, counter_state_action)
1def export_to_MDPMapping(
2 states: set[State],
3 S_A_NS_metric: dict[tuple[State, Action, NextState], StateActionNextStateMetric],
4 counter_state_action: dict[tuple[State, Action], int],
5) -> MDPMapping:
6 # [state][action][next_state] = (Transition, Reward)
7 mdp: MDPMapping = {}
8 for state in states:
9 mdp[state] = {}
10 for state, action in counter_state_action:
11 mdp[state][action] = {}
12
13 for (state, action, next_state), metric in S_A_NS_metric.items():
14 mdp[state][action][next_state] = (
15 metric.counter / counter_state_action[(state, action)],
16 metric.reward_sum / metric.counter,
17 )
18 return mdp
1def test_model_based_Monte_Carlo_dummy(dummy_game_environment, policy_stay):
2 mdp_mapping: MDPMapping = model_based_Monte_Carlo(
3 dummy_game_environment, "in", policy_stay, 2
4 )
5 assert pytest.approx(mdp_mapping["in"]["stay"]["in"][0]) == 2.0 / 3.0
6 assert pytest.approx(mdp_mapping["in"]["stay"]["in"][1]) == 4.0
1def model_free_Monte_Carlo(
2 environment: Environment,
3 state_: State,
4 policy: Policy,
5 discount: float = 1.0,
6 nb_episodes_per_state: int = 1_000,
7) -> QualitiesMapping:
8 states: set[State] = {state_}
9
10 qualities: QualitiesMapping[State] = defaultdict(float)
11 counter_state_action: dict[tuple[State, Action], int] = defaultdict(int)
12 # counter number of update for each Q(s,a)
13
14 # generate episode many times from all known states
15 for _ in range(nb_episodes_per_state):
16 for state in states.copy(): # must not iterate on a mutating set
17 episode: Generator[Frame] = environment.yield_frame_in_episode(
18 state, policy
19 )
20
21 if environment.is_end_state(state):
22 continue
23
24 # analyze episode
25 reversed_episode = reversed(tuple(episode))
26 utility: Reward = 0.0
27 updated_state_action: set[tuple[State, Action]] = set()
28 utilities: QualitiesMapping = {}
29 # obtain first occurrence utilities
30 for state, action, reward, next_state in reversed_episode:
31 states.add(state)
32 states.add(next_state)
33 utility = utility * discount + reward
34 utilities[(state, action)] = utility
35 updated_state_action.add((state, action))
36
37 # update qualities
38 for state, action in updated_state_action:
39 alpha: float = 1.0 / (1.0 + counter_state_action[(state, action)])
40 qualities[(state, action)] = (1.0 - alpha) * qualities[
41 (state, action)
42 ] + alpha * utilities[(state, action)]
43 counter_state_action[(state, action)] += 1
44
45 return qualities
1def test_model_free_Monte_Carlo_dummy(dummy_game_environment, policy_stay):
2 qualities_mapping: QualitiesMapping = model_free_Monte_Carlo(
3 dummy_game_environment, "in", policy_stay, nb_episodes_per_state=2
4 )
5 assert pytest.approx(qualities_mapping[("in", "stay")]) == 13.0
1def sarsa(
2 environment: Environment,
3 state_: State,
4 policy: Policy,
5 discount: float = 1.0,
6 nb_episodes_per_state: int = 1_000,
7) -> QualitiesMapping:
8 states: set[State] = {state_}
9
10 qualities: QualitiesMapping = defaultdict(float)
11 counter_state_action: dict[tuple[State, Action], int] = defaultdict(int)
12 # counter number of update for each Q(s,a)
13
14 # generate episode many times from all known states
15 for _ in range(nb_episodes_per_state):
16 for state in states.copy(): # must not iterate on a mutating set
17 episode: Generator[Frame] = environment.yield_frame_in_episode(
18 state, policy
19 )
20 if environment.is_end_state(state):
21 continue
22
23 # analyze episode
24 current_frame: Frame = next(episode)
25 last_frame: bool = False
26 while not last_frame:
27 state, action, reward, next_state = current_frame
28
29 # obtain quality next state
30 if environment.is_end_state(next_state):
31 next_quality: Reward = 0.0
32 last_frame = True
33 else:
34 next_frame: Frame = next(episode)
35 _, next_action, _, _ = next_frame
36 next_quality: Reward = qualities[(next_state, next_action)]
37 current_frame = next_frame
38
39 # update qualities
40 alpha: float = 1.0 / (1.0 + counter_state_action[(state, action)])
41 qualities[(state, action)] = (1.0 - alpha) * qualities[
42 (state, action)
43 ] + alpha * (reward + discount * next_quality)
44 counter_state_action[(state, action)] += 1
45
46 return qualities
1def test_sarsa_dummy(dummy_game_environment, policy_stay):
2 qualities_mapping: QualitiesMapping = sarsa(
3 dummy_game_environment, "in", policy_stay, nb_episodes_per_state=1
4 )
5 assert pytest.approx(qualities_mapping[("in", "stay")]) == 17.0 / 3.0
1def qlearning(
2 environment: Environment,
3 state_: State,
4 policy: Policy,
5 discount: float = 1.0,
6 nb_episodes_per_state: int = 1_000,
7) -> tuple[QualitiesMapping, PolicyMapping]:
8 states: set[State] = {state_}
9
10 opt_qualities: QualitiesMapping = defaultdict(float)
11 counter_state_action: dict[tuple[State, Action], int] = defaultdict(int)
12 # counter number of update for each Q(s,a)
13 opt_policy_mapping: PolicyMapping = {}
14
15 # generate episode many times from all known states
16 for _ in range(nb_episodes_per_state):
17 for state in states.copy(): # must not iterate on a mutating set
18 episode: Generator[Frame] = environment.yield_frame_in_episode(
19 state, policy
20 )
21 if environment.is_end_state(state):
22 continue
23
24 # analyze episode
25 for frame in episode:
26 state, action, reward, next_state = frame
27 states.add(state)
28 states.add(next_state)
29
30 # obtain quality next state
31 if environment.is_end_state(next_state):
32 opt_next_quality: Reward = 0.0
33 else:
34 next_actions = environment.actions_from_state(next_state)
35 opt_next_quality = max(
36 [
37 opt_qualities[(next_state, next_action)]
38 for next_action in next_actions
39 ]
40 )
41
42 # update optimum qualities
43 alpha: float = 1.0 / (1.0 + counter_state_action[(state, action)])
44 opt_qualities[(state, action)] = (1.0 - alpha) * opt_qualities[
45 (state, action)
46 ] + alpha * (reward + discount * opt_next_quality)
47 counter_state_action[(state, action)] += 1
48
49 # update optimum policy
50 actions = environment.actions_from_state(state)
51 _, opt_action = max(
52 [(opt_qualities[(state, action)], action) for action in actions]
53 )
54 opt_policy_mapping[state] = opt_action
55
56 return opt_qualities, opt_policy_mapping
1def test_qlearning_dummy(dummy_game_environment, policy_stay):
2 qualities_mapping, opt_policy_mapping = qlearning(
3 dummy_game_environment, "in", policy_stay, nb_episodes_per_state=1
4 )
5 assert pytest.approx(qualities_mapping[("in", "stay")]) == 17.0 / 3.0
6 assert opt_policy_mapping["in"] == "stay"
Magic tram¶
Exercise (easy)
Use the four previous algorithms on the magic tram problem.
Model based MC, comment on the result.
Model free MC and Sarsa, why don’t we find the same qualities as the correction of the magic tram MDP exercise?
Qlearning, do we retrieve the optimal policy of the magic tram MDP exercise? Use this policy mapping in Model free MC and Sarsa to confirm that you find a satisfying estimation of the qualities of the magic tram MDP exercise.
Correction
For nb_episodes_per_state of 100_000 and a fail probability of 0.2.
The MDP and the fail probability are correctly estimated, the variance of the estimations between runs decreases when the
nb_episodes_per_stateincreases.{2: {'tram': {2: (0.20043060556168696, -2.0), 4: (0.799569394438313, -2.0)}, 'walk': {3: (1.0, -1.0)}}, 3: {'tram': {3: (0.19742429996892588, -2.0), 6: (0.8025757000310741, -2.0)}, 'walk': {4: (1.0, -1.0)}}, 4: {'tram': {4: (0.20133871320884553, -2.0), 8: (0.7986612867911544, -2.0)}, 'walk': {5: (1.0, -1.0)}}, 5: {'tram': {5: (0.2012801210754909, -2.0), 10: (0.7987198789245091, -2.0)}, 'walk': {6: (1.0, -1.0)}}, 6: {'tram': {6: (0.20070165624923997, -2.0), 12: (0.79929834375076, -2.0)}, 'walk': {7: (1.0, -1.0)}}, 7: {'walk': {8: (1.0, -1.0)}}, 8: {'walk': {9: (1.0, -1.0)}}, 9: {'walk': {10: (1.0, -1.0)}}, 10: {'walk': {11: (1.0, -1.0)}}, 11: {'walk': {12: (1.0, -1.0)}}, 12: {'walk': {13: (1.0, -1.0)}}, 13: {}}
The magic tram MDP correction gave the qualities of the optimal policy (value iteration algorithm). Here, the random policy is used. The two matches when the two policies are equivalent: when the only choice is to walk.
{(2, 'tram'): -9.654715245560729, (2, 'walk'): -9.118302645579085, (3, 'tram'): -7.970203666250092, (3, 'walk'): -8.238426328292135, (4, 'tram'): -7.4482404985182455, (4, 'walk'): -7.0281073940240875, (5, 'tram'): -5.600584493424383, (5, 'walk'): -6.443788333294008, (6, 'tram'): -3.896297797594899, (6, 'walk'): -6.999999999999999, (7, 'walk'): -6.0, (8, 'walk'): -4.999999999999999, (9, 'walk'): -4.0, (10, 'walk'): -3.0, (11, 'walk'): -2.0, (12, 'walk'): -1.0} {(2, 'tram'): -9.481134252634465, (2, 'walk'): -8.877060965638796, (3, 'tram'): -7.858008074723888, (3, 'walk'): -8.09721124858775, (4, 'tram'): -7.409247009049843, (4, 'walk'): -6.930076577752609, (5, 'tram'): -5.580370812949426, (5, 'walk'): -6.380390930193599, (6, 'tram'): -3.8683076395406277, (6, 'walk'): -6.9511901596126995, (7, 'walk'): -5.98235136847001, (8, 'walk'): -4.994789408530305, (9, 'walk'): -3.998836675565334, (10, 'walk'): -2.9998236749092415, (11, 'walk'): -1.9999872676343076, (12, 'walk'): -1.0}
Yes. Using the optimum policy from Qlearning in Sarsa or Model free MC allows to retrieve the qualities from the magic tram MDP correction.
{2: 'walk',
3: 'tram',
4: 'walk',
5: 'walk',
6: 'tram',
7: 'walk',
8: 'walk',
9: 'walk',
10: 'walk',
11: 'walk',
12: 'walk'}
{(2, 'walk'): -7.0063599999999475,
(3, 'tram'): -6.000875004375021,
(6, 'tram'): -3.5002433349554973,
(12, 'walk'): -1.0}
{(2, 'walk'): -6.983584610287968,
(3, 'tram'): -5.997740461664008,
(6, 'tram'): -3.504182096707191,
(12, 'walk'): -1.0}
1def reinforcement_learning_magic_tram():
2 from icecream import ic
3
4 from ai.reinforcement.mdp_types import RandomPolicy
5 from ai.reinforcement.mdp_example_tram import MDPMagicTram
6 from ai.reinforcement.environment_from_mdp import EnvironmentFromMDP
7
8 policy = RandomPolicy()
9 mdp = MDPMagicTram(2, 13, fail_probability=0.2)
10 environment = EnvironmentFromMDP(mdp)
11 nb_episodes_per_state = 100_000
12 start: State = 2
13 ic(
14 model_based_Monte_Carlo(
15 environment, start, policy, nb_episodes_per_state=nb_episodes_per_state
16 )
17 )
18 ic(
19 model_free_Monte_Carlo(
20 environment, start, policy, nb_episodes_per_state=nb_episodes_per_state
21 )
22 )
23 ic(sarsa(environment, start, policy, nb_episodes_per_state=nb_episodes_per_state))
24 opt_qualities, opt_policy_mapping = qlearning(
25 environment, start, policy, nb_episodes_per_state=nb_episodes_per_state
26 )
27 ic(opt_qualities, opt_policy_mapping)
28
29 opt_policy = PolicyFromMapping(opt_policy_mapping)
30 ic(
31 model_free_Monte_Carlo(
32 environment, start, opt_policy, nb_episodes_per_state=nb_episodes_per_state
33 )
34 )
35 ic(
36 sarsa(
37 environment, start, opt_policy, nb_episodes_per_state=nb_episodes_per_state
38 )
39 )
Swamp isle¶
Rearranging the Q-learning equation, one gets:
This form is a classical form of iterative correction where the current value is updated proportionally to the difference between the better / target value and the current value. In this context, it is sometimes referred as temporal difference. In the original equation, a strict convex combination is equivalent to \(\alpha= \cfrac{1}{N+1}\). However, the next exercise will illustrate that this rapidly decreasing learning rate is not efficient.
Exercise (intermediate)
Implement and test the epsilon greedy algorithm (with defaulting to a random strategy when a state is not present in the strategy mapping).
Modify and test the qlearning algorithm in order to mutate continuously the exploration policy to the estimated optimum policy. Use the epsilon greedy policy for exploration, initialize its mapping with a non optimal policy. Comment on the results.
Improve the qlearning algorithm by using a fixed learning rate. Comment on the results.
Correction
The simplest experiment is to start with a failure probability of 0. This way the result should be identical to the state model and MDP exercises. Doin
Since the policy mapping is mutated along, there is no need to return a policy mapping at the end, the optimal policy is in the policy mapping of the policy. The counter (state, action) is no longer required because the learning rate is fixed.
src¶1class EpsilonGreedyPolicy(PolicyFromMapping): 2 def __init__(self, policy_mapping: PolicyMapping, epsilon: float = 0.1): 3 self.policy_mapping = dict(policy_mapping) 4 self.epsilon = epsilon 5 6 @override 7 def action_from_state(self, state: State, actions: set[Action]) -> Action: 8 actions_ = tuple(actions) 9 opt_action: State | None = self.policy_mapping.get(state) 10 if opt_action is None: 11 return choice(actions_) # random policy if no known optimal policy 12 13 if len(actions)>1: 14 non_opt_probability: float = self.epsilon / (len(actions) - 1) 15 else: 16 non_opt_probability: float = 0.0 17 18 weights: list[float] = [ 19 non_opt_probability if action != opt_action else 1.0 - self.epsilon 20 for action in actions_ 21 ] # for the case where len(actions)==1 => weights=[1-epsi] 22 # this not a problem because choices normalizes the weights 23 return choices(actions_, weights, k=1)[0]
tests¶1def test_epsilon_greedy(): 2 policy_mapping: PolicyMapping = {"in": "stay"} 3 epsilon: float = 0.4 4 nb_trials = 1000 5 policy_epsilon_greedy = EpsilonGreedyPolicy(policy_mapping, epsilon=epsilon) 6 are_actions_stay = [ 7 "stay" == policy_epsilon_greedy.action_from_state("in", {"stay", "quit"}) 8 for _ in range(nb_trials) 9 ] 10 assert pytest.approx(sum(are_actions_stay) / nb_trials, abs=0.05) == 1.0 - epsilon
Even using a number of episodes per state very large (100_000 or even more), leads to very disappointing results: the qualities close to the destination are pretty well estimated, however, the qualities close to the starting position do not seen to converge. The problem with the decreasing learning rate is that the states close to the starting position are updated multiple times using very poor target values. The learning rate drops for these states, and later when target improves (from the destination), the corrections are too small. Using a random order for state traversal does not help to propagate the knowledge faster. With the fixed learning rate, the convergence is dramatically improved.
{((0, 0), 'E'): -12.677577227122406, ((0, 0), 'S'): -12.667075392250679, ((0, 1), 'E'): -31.68226743698947, ((0, 1), 'S'): -31.66799828086022, ((0, 1), 'W'): -12.670359811413386, ((0, 2), 'E'): -40.9997934415334, ((0, 2), 'S'): -40.999834420809556, ((0, 2), 'W'): -31.67645420617531, ((0, 3), 'S'): -20.999992192948962, ((0, 3), 'W'): -50.681523379327565, ((1, 0), 'E'): -31.66673091925328, ((1, 0), 'N'): -12.66885326879473, ((1, 0), 'S'): -12.6661214455744, ((1, 1), 'E'): -40.9998103493078, ((1, 1), 'N'): -31.675785937100066, ((1, 1), 'S'): -31.66929228245872, ((1, 1), 'W'): -31.66414897696369, ((1, 2), 'E'): -21.000000000000004, ((1, 2), 'N'): -50.686172012334524, ((1, 2), 'S'): -39.999926341212834, ((1, 2), 'W'): -50.6678345317146, ((1, 3), 'N'): -21.999897582486756, ((1, 3), 'S'): -1.0, ((1, 3), 'W'): -21.999850806709116, ((2, 0), 'E'): -12.66813474888183, ((2, 0), 'N'): -12.667260215082385, ((2, 1), 'E'): -20.999777113079706, ((2, 1), 'N'): -31.66945779221507, ((2, 1), 'W'): -12.668045310032248, ((2, 2), 'E'): -19.999999999999993, ((2, 2), 'N'): -40.999794234146705, ((2, 2), 'W'): -31.667637590668363} {(0, 0): 'S', (0, 1): 'W', (0, 2): 'W', (0, 3): 'S', (1, 0): 'S', (1, 1): 'W', (1, 2): 'E', (1, 3): 'S', (2, 0): 'N', (2, 1): 'W', (2, 2): 'E'} {((0, 0), 'E'): -25.99999999999998, ((0, 0), 'S'): -23.999999999999982, ((0, 1), 'E'): -41.99999999999996, ((0, 1), 'S'): -41.99999999999996, ((0, 1), 'W'): -24.999999999999982, ((0, 2), 'E'): -40.99999999999997, ((0, 2), 'S'): -40.99999999999997, ((0, 2), 'W'): -44.99999999999999, ((0, 3), 'S'): -20.999999999999993, ((0, 3), 'W'): -60.99999999999998, ((1, 0), 'E'): -41.99999999999996, ((1, 0), 'N'): -24.999999999999982, ((1, 0), 'S'): -22.999999999999986, ((1, 1), 'E'): -40.99999999999997, ((1, 1), 'N'): -44.99999999999999, ((1, 1), 'S'): -40.99999999999997, ((1, 1), 'W'): -42.99999999999997, ((1, 2), 'E'): -20.999999999999993, ((1, 2), 'N'): -60.99999999999998, ((1, 2), 'S'): -39.999999999999986, ((1, 2), 'W'): -60.99999999999998, ((1, 3), 'N'): -21.99999999999999, ((1, 3), 'S'): -0.9999999999999998, ((1, 3), 'W'): -21.99999999999999, ((2, 0), 'E'): -21.99999999999999, ((2, 0), 'N'): -23.999999999999982, ((2, 1), 'E'): -20.999999999999993, ((2, 1), 'N'): -41.99999999999996, ((2, 1), 'W'): -22.999999999999986, ((2, 2), 'E'): -19.999999999999993, ((2, 2), 'N'): -40.99999999999997, ((2, 2), 'W'): -40.99999999999997} {(0, 0): 'S', (0, 1): 'W', (0, 2): 'S', (0, 3): 'S', (1, 0): 'S', (1, 1): 'S', (1, 2): 'E', (1, 3): 'S', (2, 0): 'E', (2, 1): 'E', (2, 2): 'E'}
- src¶
1def qlearning_with_update( 2 environment: Environment, 3 state_: State, 4 policy: PolicyFromMapping, # change 5 discount: float = 1.0, 6 nb_episodes_per_state: int = 1_000, 7 learning_rate: float = 0.2, # change 8) -> QualitiesMapping: # change 9 """contrary to the previous qlearning, this one receives the policy as input, 10 and update the policy mapping along the way. For example, if the epsilon greedy 11 polilcy is passed, obviously initially the optimal policy is not known (and defaulted to random). 12 The optimal policy is thus gradually built and updated inside the policy. 13 That is why the policy is not explictly returned.""" 14 states: set[State] = {state_} 15 alpha = learning_rate 16 17 opt_qualities: QualitiesMapping = defaultdict(float) 18 19 # synchronize the optimum policy and exploration policy mappings 20 opt_policy_mapping: PolicyMapping = policy.policy_mapping # change 21 states |= set(opt_policy_mapping) # change 22 23 # generate episode many times from all known states 24 for _ in range(nb_episodes_per_state): 25 for state in states.copy(): 26 episode: Generator[Frame] = environment.yield_frame_in_episode( 27 state, policy 28 ) 29 if environment.is_end_state(state): 30 continue 31 32 # analyze episode 33 for frame in episode: 34 state, action, reward, next_state = frame 35 states.add(state) 36 states.add(next_state) 37 38 # obtain quality next state 39 if environment.is_end_state(next_state): 40 opt_next_quality: Reward = 0.0 41 else: 42 next_actions = environment.actions_from_state(next_state) 43 opt_next_quality = max( 44 [ 45 opt_qualities[(next_state, next_action)] 46 for next_action in next_actions 47 ] 48 ) 49 50 # update optimum qualities 51 opt_qualities[(state, action)] = (1.0 - alpha) * opt_qualities[ 52 (state, action) 53 ] + alpha * (reward + discount * opt_next_quality) 54 55 # update optimum policy 56 actions = environment.actions_from_state(state) 57 _, opt_action = max( 58 [(opt_qualities[(state, action)], action) for action in actions] 59 ) 60 opt_policy_mapping[state] = opt_action 61 62 return opt_qualities