Skip to content

Commit ad99d5e

Browse files
Add Q-Learning algorithm implementation with epsilon-greedy policy an… (#13402)
* Add Q-Learning algorithm implementation with epsilon-greedy policy and grid world demo * Add Q-Learning algorithm implementation with epsilon-greedy policy and grid world demo code * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * bug fixes and linting * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * cls * bug fix and hints --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a23264c commit ad99d5e

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

machine_learning/q_learning.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""
2+
Q-Learning is a widely-used model-free algorithm in reinforcement learning that
3+
learns the optimal action-value function Q(s, a), which tells an agent the expected
4+
utility of taking action a in state s and then following the optimal policy after.
5+
It is able to find the best policy for any given finite Markov decision process (MDP)
6+
without requiring a model of the environment.
7+
8+
See: [https://en.wikipedia.org/wiki/Q-learning](https://en.wikipedia.org/wiki/Q-learning)
9+
"""
10+
11+
import random
12+
from collections import defaultdict
13+
14+
# Type alias for state
15+
type State = tuple[int, int]
16+
17+
# Hyperparameters for Q-Learning
18+
LEARNING_RATE = 0.1
19+
DISCOUNT_FACTOR = 0.97
20+
EPSILON = 0.2
21+
EPSILON_DECAY = 0.995
22+
EPSILON_MIN = 0.01
23+
24+
# Global Q-table to store state-action values
25+
q_table: dict[State, dict[int, float]] = defaultdict(lambda: defaultdict(float))
26+
27+
# Environment variables for simple grid world
28+
SIZE = 4
29+
GOAL = (SIZE - 1, SIZE - 1)
30+
current_state = (0, 0)
31+
32+
33+
def get_q_value(state: State, action: int) -> float:
34+
"""
35+
Get Q-value for a given state-action pair.
36+
37+
>>> q_table.clear()
38+
>>> get_q_value((0, 0), 2)
39+
0.0
40+
"""
41+
return q_table[state][action]
42+
43+
44+
def get_best_action(state: State, available_actions: list[int]) -> int:
45+
"""
46+
Get the action with maximum Q-value in the given state.
47+
48+
>>> q_table.clear()
49+
>>> q_table[(0, 0)][1] = 0.7
50+
>>> q_table[(0, 0)][2] = 0.7
51+
>>> q_table[(0, 0)][3] = 0.5
52+
>>> get_best_action((0, 0), [1, 2, 3]) in [1, 2]
53+
True
54+
"""
55+
if not available_actions:
56+
raise ValueError("No available actions provided")
57+
max_q = max(q_table[state][a] for a in available_actions)
58+
best = [a for a in available_actions if q_table[state][a] == max_q]
59+
return random.choice(best)
60+
61+
62+
def choose_action(state: State, available_actions: list[int]) -> int:
63+
"""
64+
Choose action using epsilon-greedy policy.
65+
66+
>>> q_table.clear()
67+
>>> old_epsilon = EPSILON
68+
>>> EPSILON = 0.0
69+
>>> q_table[(0, 0)][1] = 1.0
70+
>>> q_table[(0, 0)][2] = 0.5
71+
>>> result = choose_action((0, 0), [1, 2])
72+
>>> EPSILON = old_epsilon # Restore
73+
>>> result
74+
1
75+
"""
76+
global EPSILON
77+
if not available_actions:
78+
raise ValueError("No available actions provided")
79+
if random.random() < EPSILON:
80+
return random.choice(available_actions)
81+
return get_best_action(state, available_actions)
82+
83+
84+
def update(
85+
state: State,
86+
action: int,
87+
reward: float,
88+
next_state: State,
89+
next_available_actions: list[int],
90+
done: bool = False,
91+
alpha: float | None = None,
92+
gamma: float | None = None,
93+
) -> None:
94+
"""
95+
Perform Q-value update for a transition using the Q-learning rule.
96+
97+
Q(s,a) <- Q(s,a) + alpha * (r + gamma * max_a' Q(s',a') - Q(s,a))
98+
99+
>>> q_table.clear()
100+
>>> update((0, 0), 1, 1.0, (0, 1), [1, 2], done=True, alpha=0.5, gamma=0.9)
101+
>>> get_q_value((0, 0), 1)
102+
0.5
103+
"""
104+
global LEARNING_RATE, DISCOUNT_FACTOR
105+
alpha = alpha if alpha is not None else LEARNING_RATE
106+
gamma = gamma if gamma is not None else DISCOUNT_FACTOR
107+
max_q_next = (
108+
0.0
109+
if done or not next_available_actions
110+
else max(get_q_value(next_state, a) for a in next_available_actions)
111+
)
112+
old_q = get_q_value(state, action)
113+
new_q = (1 - alpha) * old_q + alpha * (reward + gamma * max_q_next)
114+
q_table[state][action] = new_q
115+
116+
117+
def get_policy() -> dict[State, int]:
118+
"""
119+
Extract a deterministic policy from the Q-table.
120+
121+
122+
>>> q_table.clear()
123+
>>> q_table[(1, 2)][1] = 2.0
124+
>>> q_table[(1, 2)][2] = 1.0
125+
>>> get_policy()[(1, 2)]
126+
1
127+
"""
128+
policy: dict[State, int] = {}
129+
for s, a_dict in q_table.items():
130+
if a_dict:
131+
policy[s] = max(a_dict, key=lambda a: a_dict[a])
132+
return policy
133+
134+
135+
def reset_env() -> State:
136+
"""
137+
Reset the environment to initial state.
138+
139+
>>> old_state = current_state
140+
>>> current_state = (1, 1) # Simulate non-initial state
141+
>>> result = reset_env()
142+
>>> current_state = old_state # Restore for other tests
143+
>>> result
144+
(0, 0)
145+
"""
146+
global current_state
147+
current_state = (0, 0)
148+
return current_state
149+
150+
151+
def get_available_actions_env() -> list[int]:
152+
"""
153+
Get available actions in the current environment state.
154+
"""
155+
return [0, 1, 2, 3] # 0: up, 1: right, 2: down, 3: left
156+
157+
158+
def step_env(action: int) -> tuple[State, float, bool]:
159+
"""
160+
Take a step in the environment with the given action.
161+
"""
162+
global current_state
163+
x, y = current_state
164+
if action == 0: # up
165+
x = max(0, x - 1)
166+
elif action == 1: # right
167+
y = min(SIZE - 1, y + 1)
168+
elif action == 2: # down
169+
x = min(SIZE - 1, x + 1)
170+
elif action == 3: # left
171+
y = max(0, y - 1)
172+
next_state = (x, y)
173+
reward = 10.0 if next_state == GOAL else -1.0
174+
done = next_state == GOAL
175+
current_state = next_state
176+
return next_state, reward, done
177+
178+
179+
def run_q_learning() -> None:
180+
"""
181+
Run Q-Learning on the simple grid world environment.
182+
"""
183+
global EPSILON
184+
episodes = 200
185+
for _ in range(episodes):
186+
state = reset_env()
187+
done = False
188+
while not done:
189+
actions = get_available_actions_env()
190+
action = choose_action(state, actions)
191+
next_state, reward, done = step_env(action)
192+
next_actions = get_available_actions_env()
193+
update(state, action, reward, next_state, next_actions, done)
194+
state = next_state
195+
EPSILON = max(EPSILON * EPSILON_DECAY, EPSILON_MIN)
196+
policy = get_policy()
197+
print("Learned Policy (state: action):")
198+
for s, a in sorted(policy.items()):
199+
print(f"{s}: {a}")
200+
201+
202+
if __name__ == "__main__":
203+
import doctest
204+
205+
doctest.testmod()
206+
run_q_learning()

0 commit comments

Comments
 (0)