99- There are N arms, each with a different probability of giving a reward.
1010- The agent must learn to choose the best arm to pull in order to maximize its reward.
1111
12- Here there are 3 optimising strategies have been implemented:
12+ Here 3 optimising strategies have been implemented:
1313- Epsilon-Greedy
1414- Upper Confidence Bound (UCB)
1515- Thompson Sampling
@@ -41,6 +41,11 @@ def __init__(self, probabilities: list[float]) -> None:
4141
4242 Args:
4343 probabilities: List of probabilities for each arm.
44+
45+ Example:
46+ >>> bandit = Bandit([0.1, 0.5, 0.9])
47+ >>> bandit.num_arms
48+ 3
4449 """
4550 self .probabilities = probabilities
4651 self .num_arms = len (probabilities )
@@ -127,7 +132,7 @@ def select_arm(self) -> int:
127132 rng = np .random .default_rng ()
128133
129134 if rng .random () < self .epsilon :
130- return rng .integers (self .num_arms )
135+ return int ( rng .integers (self .num_arms ) )
131136 else :
132137 return int (np .argmax (self .values ))
133138
@@ -274,7 +279,7 @@ def update(self, arm_index: int, reward: int) -> None:
274279# Random strategy (full exploration)
275280class RandomStrategy (Strategy ):
276281 """
277- A class for choosing totally random at each round to give
282+ A class for choosing an arm uniformly at random at each round to give
278283 a better comparison with the other optimised strategies.
279284 """
280285
@@ -297,10 +302,10 @@ def select_arm(self) -> int:
297302 Example:
298303 >>> strategy = RandomStrategy(num_arms=3)
299304 >>> 0 <= strategy.select_arm() < 3
300- np.True_
305+ True
301306 """
302307 rng = np .random .default_rng ()
303- return rng .integers (self .num_arms )
308+ return int ( rng .integers (self .num_arms ) )
304309
305310 def update (self , arm_index : int , reward : int ) -> None :
306311 """
@@ -371,7 +376,62 @@ def update(self, arm_index: int, reward: int) -> None:
371376
372377def test_mab_strategies () -> None :
373378 """
374- Test the MAB strategies.
379+ Deterministic behavioural tests for the MAB strategies.
380+
381+ These checks feed each strategy a fixed sequence of rewards and assert
382+ on the resulting internal state and arm selection, so a regression in
383+ the update/select logic will fail the suite instead of only being
384+ visible in the (stochastic) plotted demo.
385+ """
386+ num_arms = 3
387+
388+ # After repeatedly rewarding arm 2, a purely greedy strategy must
389+ # settle on arm 2.
390+ greedy = GreedyStrategy (num_arms = num_arms )
391+ for _ in range (10 ):
392+ greedy .update (2 , 1 )
393+ greedy .update (0 , 0 )
394+ greedy .update (1 , 0 )
395+ assert greedy .select_arm () == 2
396+
397+ # Epsilon-Greedy with epsilon=0 behaves like the greedy strategy.
398+ epsilon_greedy = EpsilonGreedy (epsilon = 0.0 , num_arms = num_arms )
399+ for _ in range (10 ):
400+ epsilon_greedy .update (1 , 1 )
401+ epsilon_greedy .update (0 , 0 )
402+ epsilon_greedy .update (2 , 0 )
403+ assert epsilon_greedy .select_arm () == 1
404+
405+ # UCB must exhaustively try every arm once before repeating any of them.
406+ ucb = UCB (num_arms = num_arms )
407+ first_round_arms = set ()
408+ for _ in range (num_arms ):
409+ arm = ucb .select_arm ()
410+ first_round_arms .add (arm )
411+ ucb .update (arm , 1 )
412+ assert first_round_arms == set (range (num_arms ))
413+
414+ # Thompson Sampling should heavily favor an arm with only successes
415+ # over arms with only failures.
416+ thompson = ThompsonSampling (num_arms = num_arms )
417+ for _ in range (20 ):
418+ thompson .update (0 , 1 )
419+ thompson .update (1 , 0 )
420+ thompson .update (2 , 0 )
421+ selections = [thompson .select_arm () for _ in range (50 )]
422+ assert selections .count (0 ) > len (selections ) // 2
423+
424+ # RandomStrategy.update is a no-op and select_arm always returns a
425+ # valid arm index.
426+ random_strategy = RandomStrategy (num_arms = num_arms )
427+ random_strategy .update (0 , 1 )
428+ assert 0 <= random_strategy .select_arm () < num_arms
429+
430+
431+ def demo_mab_strategies () -> None :
432+ """
433+ Run a stochastic simulation of the MAB strategies and plot their
434+ cumulative reward over time for visual comparison.
375435 """
376436 # Simulation
377437 num_arms = 4
@@ -418,3 +478,4 @@ def test_mab_strategies() -> None:
418478
419479 doctest .testmod ()
420480 test_mab_strategies ()
481+ demo_mab_strategies ()
0 commit comments