hmb-helpers 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. hmb_helpers-0.1.0/HMB/AgentsHelper.py +1258 -0
  2. hmb_helpers-0.1.0/HMB/ArabicTextHelper.py +476 -0
  3. hmb_helpers-0.1.0/HMB/AttentionMapsHelper.py +686 -0
  4. hmb_helpers-0.1.0/HMB/AudioHelper.py +1304 -0
  5. hmb_helpers-0.1.0/HMB/CompressionsHelper.py +123 -0
  6. hmb_helpers-0.1.0/HMB/DataAugmentationHelper.py +1044 -0
  7. hmb_helpers-0.1.0/HMB/DatasetsHelper.py +2408 -0
  8. hmb_helpers-0.1.0/HMB/EmbeddingsToTextHelper.py +391 -0
  9. hmb_helpers-0.1.0/HMB/ExplainabilityHelper.py +5649 -0
  10. hmb_helpers-0.1.0/HMB/HandCraftedFeatures.py +2283 -0
  11. hmb_helpers-0.1.0/HMB/ImageSegmentationMetrics.py +1558 -0
  12. hmb_helpers-0.1.0/HMB/ImagesComparisonMetrics.py +1637 -0
  13. hmb_helpers-0.1.0/HMB/ImagesHelper.py +2212 -0
  14. hmb_helpers-0.1.0/HMB/ImagesNormalization.py +632 -0
  15. hmb_helpers-0.1.0/HMB/ImagesToEmbeddings.py +202 -0
  16. hmb_helpers-0.1.0/HMB/Initializations.py +570 -0
  17. hmb_helpers-0.1.0/HMB/MachineLearningHelper.py +4011 -0
  18. hmb_helpers-0.1.0/HMB/MetaheuristicsHelper.py +163 -0
  19. hmb_helpers-0.1.0/HMB/PDFHelper.py +585 -0
  20. hmb_helpers-0.1.0/HMB/PerformanceMetrics.py +4391 -0
  21. hmb_helpers-0.1.0/HMB/PlotsHelper.py +1507 -0
  22. hmb_helpers-0.1.0/HMB/PyTorchClassificationLosses.py +307 -0
  23. hmb_helpers-0.1.0/HMB/PyTorchHelper.py +2147 -0
  24. hmb_helpers-0.1.0/HMB/PyTorchModelMemoryProfiler.py +1180 -0
  25. hmb_helpers-0.1.0/HMB/PyTorchSegmentationLosses.py +343 -0
  26. hmb_helpers-0.1.0/HMB/PyTorchTabularModelsZoo.py +1659 -0
  27. hmb_helpers-0.1.0/HMB/PyTorchTrainingPipeline.py +3645 -0
  28. hmb_helpers-0.1.0/HMB/PyTorchUNetModelsZoo.py +3565 -0
  29. hmb_helpers-0.1.0/HMB/StatisticalAnalysisHelper.py +5329 -0
  30. hmb_helpers-0.1.0/HMB/StringsHelper.py +525 -0
  31. hmb_helpers-0.1.0/HMB/TFAttentionBlocks.py +1466 -0
  32. hmb_helpers-0.1.0/HMB/TFHelper.py +3011 -0
  33. hmb_helpers-0.1.0/HMB/TFSegmentationLosses.py +327 -0
  34. hmb_helpers-0.1.0/HMB/TFUNetHelper.py +3123 -0
  35. hmb_helpers-0.1.0/HMB/TextGenerationMetrics.py +720 -0
  36. hmb_helpers-0.1.0/HMB/TextHelper.py +1051 -0
  37. hmb_helpers-0.1.0/HMB/Utils.py +897 -0
  38. hmb_helpers-0.1.0/HMB/VectorsHelper.py +251 -0
  39. hmb_helpers-0.1.0/HMB/VideosHelper.py +368 -0
  40. hmb_helpers-0.1.0/HMB/VotingHelper.py +2611 -0
  41. hmb_helpers-0.1.0/HMB/WSIHelper.py +1891 -0
  42. hmb_helpers-0.1.0/HMB/YOLOHelper.py +939 -0
  43. hmb_helpers-0.1.0/HMB/__init__.py +0 -0
  44. hmb_helpers-0.1.0/LICENSE +23 -0
  45. hmb_helpers-0.1.0/PKG-INFO +537 -0
  46. hmb_helpers-0.1.0/README.md +316 -0
  47. hmb_helpers-0.1.0/hmb_helpers.egg-info/PKG-INFO +537 -0
  48. hmb_helpers-0.1.0/hmb_helpers.egg-info/SOURCES.txt +87 -0
  49. hmb_helpers-0.1.0/hmb_helpers.egg-info/dependency_links.txt +1 -0
  50. hmb_helpers-0.1.0/hmb_helpers.egg-info/not-zip-safe +1 -0
  51. hmb_helpers-0.1.0/hmb_helpers.egg-info/requires.txt +194 -0
  52. hmb_helpers-0.1.0/hmb_helpers.egg-info/top_level.txt +1 -0
  53. hmb_helpers-0.1.0/setup.cfg +4 -0
  54. hmb_helpers-0.1.0/setup.py +273 -0
  55. hmb_helpers-0.1.0/tests/Test_AgentsHelper.py +109 -0
  56. hmb_helpers-0.1.0/tests/Test_ArabicTextHelper.py +100 -0
  57. hmb_helpers-0.1.0/tests/Test_AttentionMapsHelper.py +39 -0
  58. hmb_helpers-0.1.0/tests/Test_AudioHelper.py +156 -0
  59. hmb_helpers-0.1.0/tests/Test_CompressionsHelper.py +161 -0
  60. hmb_helpers-0.1.0/tests/Test_DataAugmentationHelper.py +60 -0
  61. hmb_helpers-0.1.0/tests/Test_DatasetsHelper.py +207 -0
  62. hmb_helpers-0.1.0/tests/Test_EmbeddingsToTextHelper.py +138 -0
  63. hmb_helpers-0.1.0/tests/Test_ExplainabilityHelper.py +89 -0
  64. hmb_helpers-0.1.0/tests/Test_HandCraftedFeatures.py +59 -0
  65. hmb_helpers-0.1.0/tests/Test_ImageSegmentationMetrics.py +213 -0
  66. hmb_helpers-0.1.0/tests/Test_ImagesComparisonMetrics.py +181 -0
  67. hmb_helpers-0.1.0/tests/Test_ImagesHelper.py +306 -0
  68. hmb_helpers-0.1.0/tests/Test_ImagesNormalization.py +169 -0
  69. hmb_helpers-0.1.0/tests/Test_ImagesToEmbeddings.py +58 -0
  70. hmb_helpers-0.1.0/tests/Test_Initializations.py +313 -0
  71. hmb_helpers-0.1.0/tests/Test_MachineLearningHelper.py +132 -0
  72. hmb_helpers-0.1.0/tests/Test_MetaheuristicsHelper.py +33 -0
  73. hmb_helpers-0.1.0/tests/Test_PDFHelper.py +575 -0
  74. hmb_helpers-0.1.0/tests/Test_PerformanceMetrics.py +43 -0
  75. hmb_helpers-0.1.0/tests/Test_PyTorchClassificationLosses.py +64 -0
  76. hmb_helpers-0.1.0/tests/Test_PyTorchHelper.py +136 -0
  77. hmb_helpers-0.1.0/tests/Test_PyTorchSegmentationLosses.py +50 -0
  78. hmb_helpers-0.1.0/tests/Test_StatisticalAnalysisHelper.py +127 -0
  79. hmb_helpers-0.1.0/tests/Test_StringsHelper.py +289 -0
  80. hmb_helpers-0.1.0/tests/Test_TFAttentionBlocks.py +75 -0
  81. hmb_helpers-0.1.0/tests/Test_TFHelper.py +66 -0
  82. hmb_helpers-0.1.0/tests/Test_TextGenerationMetrics.py +103 -0
  83. hmb_helpers-0.1.0/tests/Test_TextHelper.py +90 -0
  84. hmb_helpers-0.1.0/tests/Test_Utils.py +624 -0
  85. hmb_helpers-0.1.0/tests/Test_VectorsHelper.py +363 -0
  86. hmb_helpers-0.1.0/tests/Test_VideosHelper.py +55 -0
  87. hmb_helpers-0.1.0/tests/Test_VotingHelper.py +533 -0
  88. hmb_helpers-0.1.0/tests/Test_WSIHelper.py +83 -0
  89. hmb_helpers-0.1.0/tests/Test_YOLOHelper.py +272 -0
@@ -0,0 +1,1258 @@
1
+ import numpy as np
2
+
3
+
4
+ class RandomAgent(object):
5
+ r'''
6
+ Random policy baseline that always samples from a provided sampler.
7
+ Useful as a non-learning baseline or to perform pure exploration.
8
+ '''
9
+
10
+ def __init__(self, ActionSpaceSampleFunc):
11
+ r'''
12
+ Initialize RandomAgent.
13
+
14
+ Parameters:
15
+ ActionSpaceSampleFunc (callable): Callable returning a random action index when called.
16
+ '''
17
+
18
+ # Store the action sampler for later use.
19
+ self.ActionSpaceSampleFunc = ActionSpaceSampleFunc
20
+
21
+ def ChooseAction(self, state=None):
22
+ r'''
23
+ Return a randomly sampled action.
24
+
25
+ Parameters:
26
+ state: Ignored by RandomAgent but kept for API compatibility.
27
+
28
+ Returns:
29
+ int: Random action index returned by ActionSpaceSampleFunc().
30
+ '''
31
+
32
+ # Delegate to the provided sampler and return the result.
33
+ return self.ActionSpaceSampleFunc()
34
+
35
+
36
+ class QAgent(object):
37
+ r'''
38
+ Base tabular Q-agent with epsilon-greedy action selection.
39
+
40
+ This base class stores a Q-table and implements a simple epsilon-greedy
41
+ action selection policy. Subclasses should implement UpdateParameters
42
+ to perform learning updates (Q-learning, SARSA, Expected SARSA, ...).
43
+
44
+ Parameters:
45
+ ActionSpaceSampleFunc (callable): Callable returning a sampled action index from the environment's action space; used for exploration.
46
+ alpha (float): Learning rate (0 < alpha <= 1).
47
+ gamma (float): Discount factor for future rewards (0 <= gamma <= 1).
48
+ noOfStates (int): Number of discrete states (size of first Q dimension).
49
+ noOfActions (int): Number of discrete actions (size of second Q dimension).
50
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy (default 0.1).
51
+
52
+ Attributes:
53
+ qTable (numpy.ndarray): Array of shape (noOfStates, noOfActions) storing Q-values.
54
+ alpha, gamma, epsilon (float): Learning and policy hyperparameters.
55
+ ActionSpaceSampleFunc (callable): Stored reference to the sampler.
56
+ '''
57
+
58
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=0.1):
59
+ r'''
60
+ Initialize the QAgent and allocate the Q-table.
61
+
62
+ Parameters:
63
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
64
+ alpha (float): Learning rate.
65
+ gamma (float): Discount factor.
66
+ noOfStates (int): Number of discrete states.
67
+ noOfActions (int): Number of discrete actions.
68
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy.
69
+ '''
70
+
71
+ # Store the action sampler function for exploration.
72
+ self.ActionSpaceSampleFunc = ActionSpaceSampleFunc
73
+ # Store the learning rate.
74
+ self.alpha = alpha
75
+ # Store the discount factor.
76
+ self.gamma = gamma
77
+ # Store the exploration probability.
78
+ self.epsilon = epsilon
79
+ # Store the number of available actions.
80
+ self.noOfActions = noOfActions
81
+ # Store the number of possible states.
82
+ self.noOfStates = noOfStates
83
+ # Allocate the Q-table initialized to zeros.
84
+ self.qTable = np.zeros([noOfStates, noOfActions])
85
+
86
+ def ChooseAction(self, state):
87
+ r'''
88
+ Select an action using an epsilon-greedy strategy.
89
+
90
+ Parameters:
91
+ state (int): Current discrete state index.
92
+
93
+ Returns:
94
+ int: Chosen action index. If exploring, result of ActionSpaceSampleFunc; otherwise the greedy action (argmax over Q-values).
95
+ '''
96
+
97
+ # Draw a uniform random number to decide exploration vs. exploitation.
98
+ if (np.random.uniform(0, 1) < self.epsilon):
99
+ # When exploring, sample a random action from the provided sampler.
100
+ action = self.ActionSpaceSampleFunc()
101
+ else:
102
+ # When exploiting, choose the greedy action from the Q-table.
103
+ action = self.GetAction(state)
104
+ # Return the selected action.
105
+ return action
106
+
107
+ def GetAction(self, state):
108
+ r'''
109
+ Return the greedy action for the given state using the Q-table.
110
+
111
+ Parameters:
112
+ state (int): State index to query.
113
+
114
+ Returns:
115
+ int: Index of the action with maximal Q-value for the state. Ties are resolved by numpy.argmax (first occurrence).
116
+ '''
117
+
118
+ # Clip out-of-range state index to last valid.
119
+ if ((state < 0) or (state >= self.qTable.shape[0])):
120
+ state = min(max(state, 0), self.qTable.shape[0] - 1)
121
+ return np.argmax(self.qTable[state, :])
122
+
123
+
124
+ class QLearningAgent(QAgent):
125
+ r'''
126
+ Tabular Q-learning agent implementing the standard off-policy update.
127
+
128
+ Implements the classical Q-learning update which bootstraps using the maximum action-value in the next state.
129
+ '''
130
+
131
+ def UpdateParameters(self, state, nextState, reward, action, nextAction):
132
+ r'''
133
+ Perform a Q-learning update for a single transition.
134
+
135
+ Parameters:
136
+ state (int): Previous state index.
137
+ nextState (int): Next state index after taking the action.
138
+ reward (float): Observed reward for the transition.
139
+ action (int): Action index taken in ``state``.
140
+ nextAction (int or None): Present for API parity with on-policy agents but unused by Q-learning.
141
+ '''
142
+
143
+ # Read the old Q-value for the state-action pair.
144
+ oldValue = self.qTable[state, action]
145
+ # Compute the target using the reward and the max Q in the next state.
146
+ target = reward + self.gamma * np.max(self.qTable[nextState, :])
147
+ # Apply the learning update rule.
148
+ newValue = oldValue + self.alpha * (target - oldValue)
149
+ # Write the updated Q-value back into the table.
150
+ self.qTable[state, action] = newValue
151
+
152
+
153
+ class SARSAAgent(QAgent):
154
+ r'''
155
+ On-policy SARSA agent that updates using the Q-value of the next taken action.
156
+
157
+ Uses the (state, action, reward, nextState, nextAction) tuple to form the TD target for on-policy updates.
158
+ '''
159
+
160
+ def UpdateParameters(self, prevState, nextState, reward, prevAction, nextAction):
161
+ r'''
162
+ Perform a SARSA update for the observed transition.
163
+
164
+ Parameters:
165
+ prevState (int): The previous state index.
166
+ nextState (int): The subsequent state index.
167
+ reward (float): Observed reward for the transition.
168
+ prevAction (int): Action taken in ``prevState``.
169
+ nextAction (int): Action taken in ``nextState`` (on-policy).
170
+ '''
171
+
172
+ # Read the old Q-value for the previous state-action pair.
173
+ oldValue = self.qTable[prevState, prevAction]
174
+ # Compute the SARSA target using the Q-value of the next (state, action) pair.
175
+ target = reward + self.gamma * self.qTable[nextState, nextAction]
176
+ # Apply the update step.
177
+ newValue = oldValue + self.alpha * (target - oldValue)
178
+ # Store the updated Q-value.
179
+ self.qTable[prevState, prevAction] = newValue
180
+
181
+
182
+ class ExpectedSARSAAgent(QAgent):
183
+ r'''
184
+ Expected SARSA agent that uses the expectation under the epsilon-greedy policy as the bootstrap target.
185
+
186
+ The expected value is computed exactly by accounting for the epsilon mass that is shared between greedy and non-greedy actions and handling ties.
187
+ '''
188
+
189
+ def UpdateParameters(self, prevState, nextState, reward, prevAction, nextAction):
190
+ r'''
191
+ Perform an Expected SARSA update using the expectation over the policy in the next state.
192
+
193
+ Parameters:
194
+ prevState (int): Previous state index.
195
+ nextState (int): Next state index.
196
+ reward (float): Observed reward.
197
+ prevAction (int): Action taken in previous state.
198
+ nextAction (int or None): Present for API parity but unused here.
199
+ '''
200
+
201
+ # Read the current Q-value for the previous state and action.
202
+ oldValue = self.qTable[prevState, prevAction]
203
+ # Initialize accumulator for expected Q-value in nextState.
204
+ expectedQ = 0
205
+ # Find the maximum Q-value in the next state.
206
+ qMax = np.max(self.qTable[nextState, :])
207
+ # Count how many actions achieve the maximal Q-value (tie handling).
208
+ greedyActions = 0
209
+ for i in range(self.noOfActions):
210
+ # Count greedy actions when their Q equals the maximum.
211
+ if (self.qTable[nextState, i] == qMax):
212
+ greedyActions += 1
213
+
214
+ # Probability assigned to any non-greedy action under epsilon-greedy policy.
215
+ nonGreedyActionProbability = self.epsilon / float(self.noOfActions)
216
+ # Probability for each greedy action is the remaining mass divided by number of greedy actions,
217
+ # plus the non-greedy probability that every action keeps.
218
+ greedyActionProbability = ((1.0 - self.epsilon) / greedyActions) + nonGreedyActionProbability
219
+
220
+ # Accumulate the expected value under the epsilon-greedy policy.
221
+ for i in range(self.noOfActions):
222
+ if (self.qTable[nextState, i] == qMax):
223
+ expectedQ += self.qTable[nextState, i] * greedyActionProbability
224
+ else:
225
+ expectedQ += self.qTable[nextState, i] * nonGreedyActionProbability
226
+
227
+ # Compute the expected SARSA target using expected Q of next state.
228
+ target = reward + self.gamma * expectedQ
229
+ # Apply the learning update.
230
+ newValue = oldValue + self.alpha * (target - oldValue)
231
+ # Write the updated value back to the Q-table.
232
+ self.qTable[prevState, prevAction] = newValue
233
+
234
+
235
+ class GreedyAgent(QAgent):
236
+ r'''
237
+ Deterministic greedy policy that always picks the current best action.
238
+ This convenience subclass of QAgent enforces epsilon=0 for deterministic action selection.
239
+ '''
240
+
241
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions):
242
+ r'''
243
+ Initialize GreedyAgent by calling QAgent with epsilon forced to 0.
244
+
245
+ Parameters:
246
+ See QAgent.__init__ for parameter descriptions; epsilon is forced to 0.
247
+ '''
248
+
249
+ # Initialize parent QAgent with epsilon disabled for pure exploitation.
250
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=0.0)
251
+
252
+ def ChooseAction(self, state):
253
+ r'''
254
+ Choose the greedy action deterministically.
255
+
256
+ Parameters:
257
+ state (int): Current state index.
258
+
259
+ Returns:
260
+ int: Index of the greedy action according to the stored Q-table.
261
+ '''
262
+
263
+ # Use QAgent's GetAction to return the greedy choice.
264
+ return self.GetAction(state)
265
+
266
+
267
+ class SoftmaxPolicyAgent(QAgent):
268
+ r'''
269
+ Softmax (Boltzmann) policy over Q-values for stochastic action selection.
270
+
271
+ The temperature parameter controls exploration: lower values concentrate
272
+ probability mass on higher-valued actions, higher values approach a
273
+ uniform distribution. An optional small epsilon mixes the softmax
274
+ probabilities with a uniform distribution.
275
+ '''
276
+
277
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, temperature=1.0, epsilon=0.0):
278
+ r'''
279
+ Initialize the SoftmaxPolicyAgent.
280
+
281
+ Parameters:
282
+ temperature (float): Softmax temperature (must be > 0). Lower favors greedy actions.
283
+ epsilon (float, optional): Small probability to mix the softmax distribution with a uniform distribution (default 0.0).
284
+ '''
285
+
286
+ # Initialize base QAgent, use epsilon as an optional uniform-mix parameter.
287
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
288
+ # Store the softmax temperature.
289
+ self.temperature = max(1e-8, float(temperature))
290
+
291
+ def _softmax(self, logits):
292
+ r'''
293
+ Numerically-stable softmax that returns a probability vector.
294
+
295
+ Parameters:
296
+ logits (array-like): Values to convert to probabilities.
297
+
298
+ Returns:
299
+ numpy.ndarray: Probability vector summing to 1.
300
+ '''
301
+
302
+ # Shift logits by their max for numerical stability.
303
+ shifted = logits - np.max(logits)
304
+ # Exponentiate scaled logits.
305
+ exp = np.exp(shifted / self.temperature)
306
+ # Normalize to get probabilities.
307
+ probs = exp / np.sum(exp)
308
+ return probs
309
+
310
+ def ChooseAction(self, state):
311
+ r'''
312
+ Sample an action according to the softmax policy (optionally mixed
313
+ with epsilon uniform).
314
+
315
+ Parameters:
316
+ state (int): Current state index.
317
+
318
+ Returns:
319
+ int: Sampled action index.
320
+ '''
321
+
322
+ # Read Q-values for the state.
323
+ qvals = self.qTable[state, :]
324
+ # Compute softmax probabilities.
325
+ probs = self._softmax(qvals)
326
+ # If small epsilon mixing is enabled, mix with uniform distribution.
327
+ if (self.epsilon > 0.0):
328
+ # Create uniform probabilities.
329
+ uniform = np.ones_like(probs) / float(len(probs))
330
+ # Mix the distributions.
331
+ probs = (1.0 - self.epsilon) * probs + self.epsilon * uniform
332
+ # Sample from the probability vector using numpy choice.
333
+ action = np.random.choice(len(probs), p=probs)
334
+ # Return the sampled action.
335
+ return int(action)
336
+
337
+
338
+ class DoubleQLearningAgent(QAgent):
339
+ r'''
340
+ Double Q-learning agent implementing two Q-tables to reduce overestimation.
341
+
342
+ On each update a random coin flip decides which table is updated; the
343
+ other table is used to evaluate the chosen action (the Double Q-learning
344
+ scheme described by Van Hasselt et al.).
345
+ '''
346
+
347
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=0.1):
348
+ r'''
349
+ Initialize double-Q tables and parameters.
350
+
351
+ Parameters:
352
+ See QAgent.__init__ for parameter descriptions; an additional second Q-table is allocated internally.
353
+ '''
354
+
355
+ # Initialize the base single QAgent with epsilon for action selection.
356
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
357
+ # Allocate the second Q-table initialized to zeros.
358
+ self.qTable2 = np.zeros_like(self.qTable)
359
+
360
+ def ChooseAction(self, state):
361
+ r'''
362
+ Choose action using the sum of both Q-tables for improved selection.
363
+
364
+ Parameters:
365
+ state (int): Current state index.
366
+
367
+ Returns:
368
+ int: Action index chosen either randomly (with probability epsilon) or as the argmax of qTable + qTable2.
369
+ '''
370
+
371
+ # Use the sum of both tables as an estimate for action values.
372
+ combined = self.qTable[state, :] + self.qTable2[state, :]
373
+ # With probability epsilon choose a random action.
374
+ if (np.random.uniform(0, 1) < self.epsilon):
375
+ # Exploration: use provided sampler.
376
+ return self.ActionSpaceSampleFunc()
377
+ # Exploitation: choose argmax of combined Q-values.
378
+ return int(np.argmax(combined))
379
+
380
+ def UpdateParameters(self, state, nextState, reward, action, nextAction=None):
381
+ r'''
382
+ Perform Double Q-learning update by randomly choosing which table to update.
383
+
384
+ If the chosen table is A then the argmax is computed on A and evaluated
385
+ using B (and vice-versa). This reduces the maximization bias present in
386
+ standard Q-learning.
387
+
388
+ Parameters:
389
+ state (int): Current state index.
390
+ nextState (int): Next state index.
391
+ reward (float): Observed reward.
392
+ action (int): Action taken in ``state``.
393
+ nextAction (int or None): Present for API parity; unused here.
394
+ '''
395
+
396
+ # Randomly choose which table to update this step.
397
+ if (np.random.rand() < 0.5):
398
+ # Update primary table qTable using qTable2 for evaluation.
399
+ old = self.qTable[state, action]
400
+ # Select greedy action according to qTable.
401
+ aStar = int(np.argmax(self.qTable[nextState, :]))
402
+ # Evaluate selected action using qTable2.
403
+ target = reward + self.gamma * self.qTable2[nextState, aStar]
404
+ # Apply learning rule.
405
+ self.qTable[state, action] = old + self.alpha * (target - old)
406
+ else:
407
+ # Update qTable2 using qTable for evaluation.
408
+ old = self.qTable2[state, action]
409
+ aStar = int(np.argmax(self.qTable2[nextState, :]))
410
+ target = reward + self.gamma * self.qTable[nextState, aStar]
411
+ self.qTable2[state, action] = old + self.alpha * (target - old)
412
+
413
+
414
+ class QLambdaAgent(QAgent):
415
+ r'''
416
+ Q(\u03BB) agent with accumulating eligibility traces (tabular).
417
+
418
+ Implements off-policy Q(\u03BB) with accumulating traces. The agent
419
+ maintains an eligibility matrix of the same shape as the Q-table and
420
+ updates all Q-values proportionally to their eligibility on each step.
421
+ '''
422
+
423
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, lambd=0.9, epsilon=0.1):
424
+ r'''
425
+ Initialize QLambdaAgent with eligibility traces.
426
+
427
+ Parameters:
428
+ lambd (float): Eligibility trace decay parameter (\u03BB), typically in [0, 1].
429
+ '''
430
+
431
+ # Initialize base QAgent.
432
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
433
+ # Store lambda parameter.
434
+ self.lambd = float(lambd)
435
+ # Allocate eligibility traces initialized to zeros.
436
+ self.eligibility = np.zeros_like(self.qTable)
437
+
438
+ def ResetTraces(self):
439
+ r'''
440
+ Reset eligibility traces to zero.
441
+
442
+ Call this at the start of each episode if episodic resets are used.
443
+ '''
444
+
445
+ # Zero the eligibility trace matrix.
446
+ self.eligibility.fill(0.0)
447
+
448
+ def UpdateParameters(self, state, nextState, reward, action, nextAction):
449
+ r'''
450
+ Perform Q(\u03BB) update with accumulating traces for a single transition.
451
+
452
+ This implementation uses the off-policy max over next-state actions
453
+ when forming the TD target (i.e., Q-based bootstrap).
454
+
455
+ Parameters:
456
+ state (int): Current state index.
457
+ nextState (int): Next state index after taking action.
458
+ reward (float): Observed reward.
459
+ action (int): Action taken in ``state``.
460
+ nextAction (int or None): Present for API parity but unused for the off-policy Q(\u03BB) update.
461
+ '''
462
+
463
+ # Read current Q-value for the (state, action) pair.
464
+ qSa = self.qTable[state, action]
465
+ # Compute TD target using max over next state's actions (off-policy Q(\u03BB)).
466
+ tdTarget = reward + self.gamma * np.max(self.qTable[nextState, :])
467
+ # Compute TD error.
468
+ delta = tdTarget - qSa
469
+
470
+ # Increment eligibility for the active state-action pair.
471
+ self.eligibility[state, action] += 1.0
472
+
473
+ # Update all Q-values proportionally to their eligibility.
474
+ self.qTable += self.alpha * delta * self.eligibility
475
+
476
+ # Decay eligibility traces by gamma * lambda.
477
+ self.eligibility *= (self.gamma * self.lambd)
478
+
479
+
480
+ class SARSALambdaAgent(QAgent):
481
+ r'''
482
+ On-policy SARSA(\u03BB) agent with eligibility traces.
483
+
484
+ This agent implements accumulating or replacing eligibility traces for
485
+ SARSA-style on-policy learning. Call ResetTraces() at episode start.
486
+
487
+ Parameters:
488
+ ActionSpaceSampleFunc (callable): Action sampler used for exploration.
489
+ alpha (float): Learning rate.
490
+ gamma (float): Discount factor.
491
+ noOfStates (int): Number of discrete states.
492
+ noOfActions (int): Number of discrete actions.
493
+ lambd (float, optional): Trace-decay parameter (default 0.9).
494
+ epsilon (float, optional): Epsilon for epsilon-greedy policy (default 0.1).
495
+ trace_type (str, optional): "accumulating" or "replacing" (default "accumulating").
496
+ '''
497
+
498
+ def __init__(
499
+ self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions,
500
+ lambd=0.9, epsilon=0.1, traceType="accumulating"
501
+ ):
502
+ r'''
503
+ Initialize the SARSALambdaAgent.
504
+
505
+ Parameters:
506
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
507
+ alpha (float): Learning rate.
508
+ gamma (float): Discount factor.
509
+ noOfStates (int): Number of discrete states.
510
+ noOfActions (int): Number of discrete actions.
511
+ lambd (float, optional): Trace-decay parameter for eligibility traces.
512
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy.
513
+ traceType (str, optional): Type of eligibility traces ("accumulating" or "replacing").
514
+ '''
515
+
516
+ # Initialize parent QAgent.
517
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
518
+ self.lambd = float(lambd)
519
+ self.traceType = traceType
520
+ self.eligibility = np.zeros_like(self.qTable)
521
+
522
+ def ResetTraces(self):
523
+ r'''
524
+ Zero the eligibility traces (call at episode start).
525
+ '''
526
+
527
+ # Zero the eligibility trace matrix.
528
+ self.eligibility.fill(0.0)
529
+
530
+ def UpdateParameters(self, prevState, nextState, reward, prevAction, nextAction):
531
+ r'''
532
+ Perform SARSA(\u03BB) update using the on-policy TD error.
533
+
534
+ Parameters follow the SARSA convention; nextAction is used for the
535
+ on-policy bootstrap.
536
+ '''
537
+
538
+ # TD error using Q(prevState, prevAction) and Q(nextState, nextAction)
539
+ old = self.qTable[prevState, prevAction]
540
+ target = reward + self.gamma * self.qTable[nextState, nextAction]
541
+ delta = target - old
542
+
543
+ # Update eligibility traces
544
+ if (self.traceType == "replacing"):
545
+ self.eligibility[prevState, prevAction] = 1.0
546
+ else:
547
+ self.eligibility[prevState, prevAction] += 1.0
548
+
549
+ # Update Q-values proportionally to eligibility
550
+ self.qTable += self.alpha * delta * self.eligibility
551
+
552
+ # Decay traces
553
+ self.eligibility *= (self.gamma * self.lambd)
554
+
555
+
556
+ class MonteCarloAgent(object):
557
+ r'''
558
+ First-visit Monte Carlo control (episodic) with incremental averaging.
559
+
560
+ Stores an episode buffer and updates Q at episode end using returns.
561
+
562
+ Parameters:
563
+ ActionSpaceSampleFunc (callable): Action sampler for exploration.
564
+ gamma (float): Discount factor.
565
+ noOfStates (int): Number of discrete states.
566
+ noOfActions (int): Number of discrete actions.
567
+ epsilon (float, optional): Epsilon for epsilon-greedy policy (default 0.1).
568
+ useFirstVisit (bool, optional): If True use first-visit MC, otherwise every-visit.
569
+ '''
570
+
571
+ def __init__(self, ActionSpaceSampleFunc, gamma, noOfStates, noOfActions, epsilon=0.1, useFirstVisit=True):
572
+ r'''
573
+ Initialize the MonteCarloAgent.
574
+
575
+ Parameters:
576
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
577
+ gamma (float): Discount factor.
578
+ noOfStates (int): Number of discrete states.
579
+ noOfActions (int): Number of discrete actions.
580
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy.
581
+ useFirstVisit (bool, optional): If True use first-visit MC, otherwise every-visit.
582
+ '''
583
+
584
+ # Store the action sampler function for exploration.
585
+ self.ActionSpaceSampleFunc = ActionSpaceSampleFunc
586
+ # Store the discount factor.
587
+ self.gamma = gamma
588
+ # Store the number of available actions.
589
+ self.noOfActions = noOfActions
590
+ # Store the number of possible states.
591
+ self.noOfStates = noOfStates
592
+ # Store the exploration probability.
593
+ self.epsilon = epsilon
594
+ # Store the first-visit flag.
595
+ self.useFirstVisit = useFirstVisit
596
+
597
+ # Q-table and counts for incremental averaging
598
+ self.qTable = np.zeros([noOfStates, noOfActions])
599
+ self.counts = np.zeros([noOfStates, noOfActions], dtype=np.int64)
600
+
601
+ # Episode memory
602
+ self.episode = [] # list of (state, action, reward)
603
+
604
+ def ChooseAction(self, state):
605
+ r'''
606
+ Epsilon-greedy action selection based on current Q-table.
607
+
608
+ Parameters:
609
+ state (int): Current discrete state index.
610
+
611
+ Returns:
612
+ int: Chosen action index. If exploring, result of ActionSpaceSampleFunc; otherwise the greedy action (argmax over Q-values).
613
+ '''
614
+
615
+ # Draw a uniform random number to decide exploration vs. exploitation.
616
+ if (np.random.uniform() < self.epsilon):
617
+ # When exploring, sample a random action from the provided sampler.
618
+ action = self.ActionSpaceSampleFunc()
619
+ else:
620
+ # When exploiting, choose the greedy action from the Q-table.
621
+ action = int(np.argmax(self.qTable[state, :]))
622
+ # Return the selected action.
623
+ return action
624
+
625
+ def StoreTransition(self, state, action, reward):
626
+ r'''
627
+ Append a transition to the current episode buffer.
628
+
629
+ Parameters:
630
+ state (int): State index.
631
+ action (int): Action index.
632
+ reward (float): Reward value.
633
+ '''
634
+
635
+ # Append the (state, action, reward) tuple to the episode list.
636
+ self.episode.append((state, action, reward))
637
+
638
+ def EndEpisodeAndUpdate(self):
639
+ r'''
640
+ Process stored episode and perform first-visit (or every-visit)
641
+ Monte Carlo updates, then clear the episode buffer.
642
+ '''
643
+
644
+ G = 0.0
645
+ visited = set()
646
+ # iterate backwards.
647
+ for t in reversed(range(len(self.episode))):
648
+ s, a, r = self.episode[t]
649
+ G = self.gamma * G + r
650
+ if (self.useFirstVisit):
651
+ if ((s, a) in visited):
652
+ continue
653
+ visited.add((s, a))
654
+ # incremental average.
655
+ self.counts[s, a] += 1
656
+ n = self.counts[s, a]
657
+ self.qTable[s, a] += (G - self.qTable[s, a]) / float(n)
658
+ # clear episode.
659
+ self.episode = []
660
+
661
+
662
+ class DynaQAgent(QAgent):
663
+ r'''
664
+ Dyna-Q agent: model-based planning with a simple tabular model.
665
+
666
+ Learns a one-step model (last observed next state and reward per (s,a))
667
+ and performs planning updates by sampling previously observed pairs.
668
+
669
+ Parameters:
670
+ ActionSpaceSampleFunc (callable): Action sampler for exploration.
671
+ alpha (float): Learning rate.
672
+ gamma (float): Discount factor.
673
+ noOfStates (int): Number of discrete states.
674
+ noOfActions (int): Number of discrete actions.
675
+ epsilon (float, optional): Epsilon for epsilon-greedy policy (default 0.1).
676
+ planningSteps (int, optional): Number of model-based planning updates per real step.
677
+ '''
678
+
679
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=0.1, planningSteps=5):
680
+ r'''
681
+ Initialize the DynaQAgent.
682
+
683
+ Parameters:
684
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
685
+ alpha (float): Learning rate.
686
+ gamma (float): Discount factor.
687
+ noOfStates (int): Number of discrete states.
688
+ noOfActions (int): Number of discrete actions.
689
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy.
690
+ planningSteps (int, optional): Number of planning steps to perform.
691
+ '''
692
+
693
+ # Initialize the base QAgent.
694
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
695
+ # Store the number of planning steps.
696
+ self.planningSteps = int(planningSteps)
697
+ # Simple model: store last seen reward and nextState for each (s,a)
698
+ self.modelReward = np.zeros_like(self.qTable)
699
+ self.modelNext = np.zeros_like(self.qTable, dtype=int)
700
+ self.observed = np.zeros_like(self.qTable, dtype=bool)
701
+
702
+ def UpdateParameters(self, state, nextState, reward, action, nextAction=None):
703
+ r'''
704
+ Perform a real experience Q-learning update, update the model, and
705
+ run planningSteps simulated updates sampled from observed (s,a) pairs.
706
+
707
+ Parameters:
708
+ state (int): Current state index.
709
+ nextState (int): Next state index.
710
+ reward (float): Observed reward.
711
+ action (int): Action taken in ``state``.
712
+ nextAction (int or None): Present for API parity; unused here.
713
+ '''
714
+
715
+ # Real experience Q-learning update
716
+ old = self.qTable[state, action]
717
+ target = reward + self.gamma * np.max(self.qTable[nextState, :])
718
+ self.qTable[state, action] = old + self.alpha * (target - old)
719
+
720
+ # Update model (simple last-observed model)
721
+ self.modelReward[state, action] = reward
722
+ self.modelNext[state, action] = int(nextState)
723
+ self.observed[state, action] = True
724
+
725
+ # Planning: sample previously observed (s,a) uniformly
726
+ seenIndices = np.argwhere(self.observed)
727
+ if (seenIndices.size == 0):
728
+ return
729
+ for _ in range(self.planningSteps):
730
+ idx = np.random.randint(len(seenIndices))
731
+ sP, aP = seenIndices[idx]
732
+ rP = float(self.modelReward[sP, aP])
733
+ sPNext = int(self.modelNext[sP, aP])
734
+ oldP = self.qTable[sP, aP]
735
+ targetP = rP + self.gamma * np.max(self.qTable[sPNext, :])
736
+ self.qTable[sP, aP] = oldP + self.alpha * (targetP - oldP)
737
+
738
+
739
+ class UCB1Agent(object):
740
+ r'''
741
+ UCB1 multi-armed bandit agent (no states).
742
+
743
+ Uses the UCB1 formula to select actions in pure bandit problems.
744
+ '''
745
+
746
+ def __init__(self, ActionSpaceSampleFunc, noOfActions, c=1.0):
747
+ r'''
748
+ Initialize the UCB1Agent.
749
+
750
+ Parameters:
751
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
752
+ noOfActions (int): Number of available actions (arms).
753
+ c (float, optional): Exploration parameter for UCB1 (default 1.0).
754
+ '''
755
+
756
+ # Store the action sampler function for exploration.
757
+ self.ActionSpaceSampleFunc = ActionSpaceSampleFunc
758
+ # Store the number of available actions.
759
+ self.noOfActions = noOfActions
760
+ # Store the exploration parameter.
761
+ self.c = float(c)
762
+ # Initialize counts and values for each action.
763
+ self.counts = np.zeros(noOfActions, dtype=np.int64)
764
+ self.values = np.zeros(noOfActions, dtype=float)
765
+
766
+ def ChooseAction(self, state=None):
767
+ r'''
768
+ Choose an action using the UCB1 rule.
769
+
770
+ Parameters:
771
+ state: Ignored by UCB1Agent but kept for API compatibility.
772
+
773
+ Returns:
774
+ int: Action index selected by the UCB1 algorithm.
775
+ '''
776
+
777
+ total = np.sum(self.counts)
778
+ # choose any untried action first
779
+ for a in range(self.noOfActions):
780
+ if (self.counts[a] == 0):
781
+ return a
782
+ # compute ucb values
783
+ ucb = self.values + self.c * np.sqrt(np.log(total) / (self.counts + 1e-12))
784
+ return int(np.argmax(ucb))
785
+
786
+ def UpdateParameters(self, action, reward):
787
+ r'''
788
+ Update running average for the chosen arm.
789
+
790
+ Parameters:
791
+ action (int): Chosen action index.
792
+ reward (float): Observed reward.
793
+ '''
794
+
795
+ # Increment the count for the selected action.
796
+ self.counts[action] += 1
797
+ n = self.counts[action]
798
+ # Update the value estimate for the selected action using incremental average.
799
+ self.values[action] += (reward - self.values[action]) / float(n)
800
+
801
+
802
+ class CountBonusQLAgent(QAgent):
803
+ r'''
804
+ Q-learning augmented with a count-based intrinsic bonus to encourage exploration.
805
+
806
+ The bonus can be computed per-state or per state-action pair.
807
+ '''
808
+
809
+ def __init__(
810
+ self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions,
811
+ epsilon=0.1, bonus_coef=1.0, countType="state"
812
+ ):
813
+ r'''
814
+ Initialize the CountBonusQLAgent.
815
+
816
+ Parameters:
817
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
818
+ alpha (float): Learning rate.
819
+ gamma (float): Discount factor.
820
+ noOfStates (int): Number of discrete states.
821
+ noOfActions (int): Number of discrete actions.
822
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy.
823
+ bonus_coef (float, optional): Coefficient for the exploration bonus.
824
+ countType (str, optional): Type of count-based bonus ("state" or "state_action").
825
+ '''
826
+
827
+ # Initialize the base QAgent.
828
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
829
+ # Store the bonus coefficient and count type.
830
+ self.bonus_coef = float(bonus_coef)
831
+ self.countType = countType
832
+ # Initialize visitation counts.
833
+ if (countType == "state"):
834
+ self.counts = np.zeros(noOfStates, dtype=np.int64)
835
+ else:
836
+ self.counts = np.zeros([noOfStates, noOfActions], dtype=np.int64)
837
+
838
+ def UpdateParameters(self, state, nextState, reward, action, nextAction=None):
839
+ r'''
840
+ Perform Q-learning update with an added exploration bonus computed
841
+ from visitation counts.
842
+
843
+ Parameters:
844
+ state (int): Current state index.
845
+ nextState (int): Next state index.
846
+ reward (float): Observed reward.
847
+ action (int): Action taken in ``state``.
848
+ nextAction (int or None): Present for API parity; unused here.
849
+ '''
850
+
851
+ # increment counts for the nextState or (state,action)
852
+ if (self.countType == "state"):
853
+ self.counts[nextState] += 1
854
+ cnt = self.counts[nextState]
855
+ else:
856
+ self.counts[state, action] += 1
857
+ cnt = self.counts[state, action]
858
+
859
+ # Compute the exploration bonus based on visitation count.
860
+ bonus = self.bonus_coef / np.sqrt(float(cnt))
861
+ # Augment the reward with the exploration bonus.
862
+ augReward = reward + bonus
863
+
864
+ # Perform the standard Q-learning update with the augmented reward.
865
+ old = self.qTable[state, action]
866
+ target = augReward + self.gamma * np.max(self.qTable[nextState, :])
867
+ self.qTable[state, action] = old + self.alpha * (target - old)
868
+
869
+
870
+ class NStepTDAgent(object):
871
+ r'''
872
+ n-step on-policy TD agent (n-step SARSA style).
873
+
874
+ Maintains a buffer of recent transitions and performs n-step updates.
875
+ '''
876
+
877
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, n=3, epsilon=0.1):
878
+ r'''
879
+ Initialize the NStepTDAgent.
880
+
881
+ Parameters:
882
+ ActionSpaceSampleFunc (callable): Function to sample random actions.
883
+ alpha (float): Learning rate.
884
+ gamma (float): Discount factor.
885
+ noOfStates (int): Number of discrete states.
886
+ noOfActions (int): Number of discrete actions.
887
+ n (int, optional): Number of steps for n-step TD updates (default 3).
888
+ epsilon (float, optional): Exploration probability for epsilon-greedy policy.
889
+ '''
890
+
891
+ # Store the action sampler function for exploration.
892
+ self.ActionSpaceSampleFunc = ActionSpaceSampleFunc
893
+ # Store the learning rate.
894
+ self.alpha = alpha
895
+ # Store the discount factor.
896
+ self.gamma = gamma
897
+ # Store the number of available actions.
898
+ self.noOfActions = noOfActions
899
+ # Store the number of possible states.
900
+ self.noOfStates = noOfStates
901
+ # Store the n-step value.
902
+ self.n = int(max(1, n))
903
+ # Store the exploration probability.
904
+ self.epsilon = epsilon
905
+
906
+ # Allocate the Q-table initialized to zeros.
907
+ self.qTable = np.zeros([noOfStates, noOfActions])
908
+ # buffers
909
+ self.states = []
910
+ self.actions = []
911
+ self.rewards = []
912
+
913
+ def ChooseAction(self, state):
914
+ r'''
915
+ Select an action using an epsilon-greedy strategy.
916
+
917
+ Parameters:
918
+ state (int): Current discrete state index.
919
+
920
+ Returns:
921
+ int: Chosen action index. If exploring, result of ActionSpaceSampleFunc; otherwise the greedy action (argmax over Q-values).
922
+ '''
923
+
924
+ # Draw a uniform random number to decide exploration vs. exploitation.
925
+ if (np.random.uniform() < self.epsilon):
926
+ # When exploring, sample a random action from the provided sampler.
927
+ action = self.ActionSpaceSampleFunc()
928
+ else:
929
+ # When exploiting, choose the greedy action from the Q-table.
930
+ action = int(np.argmax(self.qTable[state, :]))
931
+ # Return the selected action.
932
+ return action
933
+
934
+ def UpdateParameters(self, state, nextState, reward, action, nextAction, done=False):
935
+ r'''
936
+ Step the agent with a (state, action, reward) sample and perform any
937
+ ready n-step updates. If done==True, flush remaining updates.
938
+
939
+ Parameters:
940
+ state (int): Current state index.
941
+ nextState (int): Next state index.
942
+ reward (float): Observed reward.
943
+ action (int): Action taken in ``state``.
944
+ nextAction (int): Action taken in ``nextState`` (on-policy).
945
+ done (bool, optional): Flag indicating episode termination (default False).
946
+ '''
947
+
948
+ # Append transition.
949
+ self.states.append(state)
950
+ self.actions.append(action)
951
+ self.rewards.append(reward)
952
+
953
+ # Perform updates while we have at least n rewards or if episode ended.
954
+ while len(self.rewards) >= self.n or (done and len(self.rewards) > 0):
955
+ # Compute n-step return for the oldest stored transition.
956
+ G = 0.0
957
+ for i in range(self.n):
958
+ if (i < len(self.rewards)):
959
+ G += (self.gamma ** i) * self.rewards[i]
960
+ else:
961
+ break
962
+ # Bootstrap term.
963
+ if (len(self.rewards) >= self.n and not (done and len(self.rewards) == self.n and nextState is None)):
964
+ # if we have a nextState/action to bootstrap from.
965
+ G += (self.gamma ** self.n) * self.qTable[nextState, nextAction]
966
+
967
+ # Pop the oldest transition from the buffers.
968
+ s0 = self.states.pop(0)
969
+ a0 = self.actions.pop(0)
970
+ self.rewards.pop(0)
971
+
972
+ # Perform the Q-value update for the oldest transition.
973
+ old = self.qTable[s0, a0]
974
+ self.qTable[s0, a0] = old + self.alpha * (G - old)
975
+
976
+ # If episode ended, clear buffers.
977
+ if (done):
978
+ self.states = []
979
+ self.actions = []
980
+ self.rewards = []
981
+
982
+
983
+ class PrioritizedSweepingAgent(QAgent):
984
+ r'''
985
+ Prioritized Sweeping agent (model-based planning with prioritized updates).
986
+
987
+ This agent maintains a one-step model (reward and next-state for each
988
+ observed (s,a)) and a predecessor map for states. When a real transition
989
+ is observed it computes a priority for the state and uses a priority
990
+ queue to selectively perform planning updates on predecessor state-action
991
+ pairs with largest expected TD error first. This is useful for
992
+ sample-efficient planning in small tabular MDPs.
993
+
994
+ Parameters:
995
+ ActionSpaceSampleFunc (callable): Action sampler used for exploration.
996
+ alpha (float): Learning rate for Q-value updates.
997
+ gamma (float): Discount factor.
998
+ noOfStates (int): Number of discrete states.
999
+ noOfActions (int): Number of discrete actions.
1000
+ epsilon (float, optional): Epsilon for epsilon-greedy action selection (default 0.1).
1001
+ planningSteps (int, optional): Number of prioritized-planning iterations to perform per real update (default 5).
1002
+ theta (float, optional): Priority threshold; only priorities greater than theta are pushed (default 1e-4).
1003
+ '''
1004
+
1005
+ def __init__(self, ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=0.1, planningSteps=5,
1006
+ theta=1e-4):
1007
+ r'''
1008
+ Initialize PrioritizedSweepingAgent and allocate model/priorities.
1009
+ '''
1010
+
1011
+ super().__init__(ActionSpaceSampleFunc, alpha, gamma, noOfStates, noOfActions, epsilon=epsilon)
1012
+ import heapq
1013
+ self._heapq = heapq
1014
+ self.planningSteps = int(planningSteps)
1015
+ self.theta = float(theta)
1016
+ # Model tables.
1017
+ self.modelReward = np.zeros_like(self.qTable)
1018
+ self.modelNext = np.zeros_like(self.qTable, dtype=int)
1019
+ self.observed = np.zeros_like(self.qTable, dtype=bool)
1020
+ # Predecessors: mapping state -> set of (s_prev, a_prev).
1021
+ self.predecessors = {s: set() for s in range(self.noOfStates)}
1022
+ # Priority queue storing tuples (-priority, state) to simulate max-heap.
1023
+ self.pq = []
1024
+
1025
+ def _push_state(self, state, priority):
1026
+ # Only push if priority exceeds threshold.
1027
+ if (priority > self.theta):
1028
+ # Store negative priority for max-heap behavior.
1029
+ self._heapq.heappush(self.pq, (-float(priority), int(state)))
1030
+
1031
+ def _pop_state(self):
1032
+ if (not self.pq):
1033
+ return None
1034
+ pr, s = self._heapq.heappop(self.pq)
1035
+ return int(s), -float(pr)
1036
+
1037
+ def ResetModel(self):
1038
+ r'''
1039
+ Clear the learned model and predecessor information.
1040
+ '''
1041
+
1042
+ self.modelReward.fill(0.0)
1043
+ self.modelNext.fill(0)
1044
+ self.observed.fill(False)
1045
+ self.predecessors = {s: set() for s in range(self.noOfStates)}
1046
+ self.pq = []
1047
+
1048
+ def ChooseAction(self, state):
1049
+ r'''
1050
+ Choose an action using epsilon-greedy based on current Q-table.
1051
+ '''
1052
+
1053
+ return super().ChooseAction(state)
1054
+
1055
+ def UpdateParameters(self, state, nextState, reward, action, nextAction=None):
1056
+ r'''
1057
+ Process a real experience (state, action, reward, nextState):
1058
+ - Update Q with a standard Q-learning step (real experience).
1059
+ - Update internal one-step model and predecessors.
1060
+ - Compute priority for the experienced state and push it if large.
1061
+ - Run up to planningSteps prioritized planning updates.
1062
+
1063
+ Parameters:
1064
+ state (int): Current state index.
1065
+ nextState (int): Next state index observed.
1066
+ reward (float): Observed reward.
1067
+ action (int): Action taken in ``state``.
1068
+ '''
1069
+
1070
+ # Real Q-learning update.
1071
+ old = self.qTable[state, action]
1072
+ target = reward + self.gamma * np.max(self.qTable[nextState, :])
1073
+ self.qTable[state, action] = old + self.alpha * (target - old)
1074
+
1075
+ # Update model: store last seen reward/next for this (s,a).
1076
+ self.modelReward[state, action] = reward
1077
+ self.modelNext[state, action] = int(nextState)
1078
+ just_new = not self.observed[state, action]
1079
+ self.observed[state, action] = True
1080
+
1081
+ # Update predecessors map: predecessor of nextState includes (state, action).
1082
+ self.predecessors[int(nextState)].add((int(state), int(action)))
1083
+
1084
+ # Compute priority for the state-action pair and push state.
1085
+ priority = abs(target - old)
1086
+ self._push_state(state, priority)
1087
+
1088
+ # Planning loop: pop highest-priority state and update predecessors.
1089
+ for _ in range(self.planningSteps):
1090
+ popped = self._pop_state()
1091
+ if (popped is None):
1092
+ break
1093
+ sPopped, pVal = popped
1094
+ # For each predecessor (sPrev, aPrev) of s_popped, perform update.
1095
+ for (sPrev, aPrev) in list(self.predecessors.get(sPopped, [])):
1096
+ rP = float(self.modelReward[sPrev, aPrev])
1097
+ sPNext = int(self.modelNext[sPrev, aPrev])
1098
+ oldPrev = self.qTable[sPrev, aPrev]
1099
+ targetPrev = rP + self.gamma * np.max(self.qTable[sPNext, :])
1100
+ # Q update for predecessor
1101
+ self.qTable[sPrev, aPrev] = oldPrev + self.alpha * (targetPrev - oldPrev)
1102
+ # Compute priority for predecessor and push if large.
1103
+ pr = abs(targetPrev - oldPrev)
1104
+ self._push_state(sPrev, pr)
1105
+
1106
+
1107
+ # Self-checking test harness
1108
+ if __name__ == "__main__":
1109
+ '''
1110
+ Run deterministic tests for all agents and compare to expected values.
1111
+
1112
+ The tests use a tiny deterministic scenario (3 states x 2 actions)
1113
+ and perform the same sequence of calls used previously. Expected
1114
+ results are computed analytically and compared with a small numeric
1115
+ tolerance. The harness prints observed vs expected values and an
1116
+ overall PASS/FAIL summary.
1117
+ '''
1118
+
1119
+ import math
1120
+
1121
+ np.random.seed(0) # Make sampling deterministic for reproducibility.
1122
+
1123
+ print("Running deterministic agent tests...")
1124
+
1125
+ # Environment and sampler.
1126
+ noS = 3
1127
+ noA = 2
1128
+
1129
+
1130
+ def sampler():
1131
+ # Deterministic pseudo-random sampler (depends on np.random seed).
1132
+ return int(np.random.randint(0, noA))
1133
+
1134
+
1135
+ results = {}
1136
+
1137
+ # 1) SarsaLambdaAgent: single SARSA(lambda) update.
1138
+ sarsaL = SARSALambdaAgent(sampler, 0.5, 0.9, noS, noA, lambd=0.8, epsilon=0.2)
1139
+ results["SARSALambda_Q_sum"] = float(np.sum(sarsaL.qTable))
1140
+ sarsaL.UpdateParameters(prevState=0, nextState=1, reward=1.0, prevAction=0, nextAction=1)
1141
+ results["SARSALambda_Q_sum"] = float(np.sum(sarsaL.qTable))
1142
+
1143
+ # 2) MonteCarloAgent: two-step episode (0,0,1.0), (1,1,2.0).
1144
+ mc = MonteCarloAgent(sampler, 0.9, noS, noA, epsilon=0.2, useFirstVisit=True)
1145
+ results["MonteCarlo_Q_00"] = float(mc.qTable[0, 0])
1146
+ mc.StoreTransition(0, 0, 1.0)
1147
+ mc.StoreTransition(1, 1, 2.0)
1148
+ mc.EndEpisodeAndUpdate()
1149
+ results["MonteCarlo_Q_00"] = float(mc.qTable[0, 0])
1150
+
1151
+ # 3) DynaQAgent: one real update then 3 planning steps.
1152
+ dyna = DynaQAgent(sampler, 0.5, 0.9, noS, noA, epsilon=0.2, planningSteps=3)
1153
+ results["DynaQ_Q_sum"] = float(np.sum(dyna.qTable))
1154
+ dyna.UpdateParameters(state=0, nextState=1, reward=1.0, action=0)
1155
+ results["DynaQ_Q_sum"] = float(np.sum(dyna.qTable))
1156
+
1157
+ # 4) UCB1Agent: choose untried arm then update its count.
1158
+ ucb = UCB1Agent(sampler, noA, c=1.0)
1159
+ results["UCB1_counts"] = np.array(ucb.counts, copy=True)
1160
+ chosen = ucb.ChooseAction()
1161
+ ucb.UpdateParameters(chosen, 1.0)
1162
+ results["UCB1_counts"] = np.array(ucb.counts, copy=True)
1163
+ cb = CountBonusQLAgent(sampler, 0.5, 0.9, noS, noA, epsilon=0.1, bonus_coef=1.0, countType="state")
1164
+ # 5) CountBonusQLAgent: state-count bonus update.
1165
+ results["CountBonusQL_Q_sum"] = float(np.sum(cb.qTable))
1166
+ cb.UpdateParameters(state=0, nextState=1, reward=0.0, action=1)
1167
+ results["CountBonusQL_Q_sum"] = float(np.sum(cb.qTable))
1168
+
1169
+ # 6) NStepTDAgent: two-step episode flushed at end.
1170
+ ns = NStepTDAgent(sampler, 0.5, 0.9, noS, noA, n=2, epsilon=0.1)
1171
+ ns.UpdateParameters(state=0, nextState=1, reward=1.0, action=0, nextAction=1, done=False)
1172
+ ns.UpdateParameters(state=1, nextState=2, reward=2.0, action=1, nextAction=0, done=True)
1173
+ results["NStep_Q_sum"] = float(np.sum(ns.qTable))
1174
+
1175
+ # 7) PrioritizedSweepingAgent complex test: two sequential updates to build predecessors
1176
+ # First update: (1,0) -> nextState=2 with reward=1.0
1177
+ # Second update: (0,1) -> nextState=1 with reward=2.0
1178
+ # We expect real updates only (no additional predecessor-triggered changes) for this small scenario
1179
+ psa = PrioritizedSweepingAgent(sampler, 0.5, 0.9, noS, noA, epsilon=0.1, planningSteps=2, theta=0.0)
1180
+ psa.UpdateParameters(state=1, nextState=2, reward=1.0, action=0)
1181
+ psa.UpdateParameters(state=0, nextState=1, reward=2.0, action=1)
1182
+ # Analytical expectations:
1183
+ # Q[1,0] = 0.5 (first real update with alpha=0.5)
1184
+ # Q[0,1] = 0.5 * (2.0 + 0.9 * 0.5) = 0.5 * 2.45 = 1.225
1185
+ # Sum = 0.5 + 1.225 = 1.725
1186
+ results["PrioritizedSweeping_Q_sum"] = float(np.sum(psa.qTable))
1187
+
1188
+ # Analytically computed expected values for the above deterministic sequence.
1189
+ expected = {
1190
+ "SARSALambda_Q_sum" : 0.5, # 0.5 after single SARSA(lambda) accumulating update.
1191
+ "MonteCarlo_Q_00" : 2.8, # G for state 0: 1 + 0.9*2 = 2.8.
1192
+ "DynaQ_Q_sum" : 0.9375, # 0.5 -> 0.75 -> 0.875 -> 0.9375 after 3 planning steps.
1193
+ "UCB1_counts" : np.array([1, 0]), # first untried arm chosen then incremented.
1194
+ "CountBonusQL_Q_sum" : 0.5, # Bonus=1->aug_reward=1->Q update yields 0.5.
1195
+ "NStep_Q_sum" : 2.4, # 2-step returns lead to Q[0,0]=1.4 and Q[1,1]=1.0 -> sum=2.4.
1196
+ "PrioritizedSweeping_Q_sum": 1.725 # See analytical expectations above.
1197
+ }
1198
+
1199
+ print("\nTest results:")
1200
+ tol = 1e-8
1201
+ allPass = True
1202
+
1203
+ for k in expected.keys():
1204
+ expVal = expected[k]
1205
+ obsVal = results[k]
1206
+ if (isinstance(expVal, np.ndarray)):
1207
+ testPass = np.allclose(obsVal, expVal, atol=tol)
1208
+ else:
1209
+ testPass = math.isclose(obsVal, expVal, abs_tol=tol)
1210
+ allPass = allPass and testPass
1211
+ status = "PASS" if testPass else "FAIL"
1212
+ print(f" {k:25s}: observed={obsVal} expected={expVal} [{status}]")
1213
+
1214
+ print("\nOverall test result: " + ("PASS" if allPass else "FAIL"))
1215
+
1216
+ # Another advanced test.
1217
+ print("\nRunning advanced test...")
1218
+
1219
+ # Advanced deterministic scenario: two sequential Q-updates with gamma=0 simplify bootstrap.
1220
+ np.random.seed(123) # Reseed for reproducibility in advanced test.
1221
+
1222
+ advResults = {}
1223
+
1224
+ # QLearningAgent: two updates on (state=0, action=0) with gamma=0, alpha=0.5.
1225
+ ql = QLearningAgent(sampler, 0.5, 0.0, noS, noA, epsilon=0.0)
1226
+ ql.UpdateParameters(state=0, nextState=0, reward=1.0, action=0, nextAction=None)
1227
+ ql.UpdateParameters(state=0, nextState=0, reward=3.0, action=0, nextAction=None)
1228
+ advResults["QLearning_Q00"] = float(ql.qTable[0, 0])
1229
+
1230
+ # DynaQAgent with planningSteps=0 should behave like Q-learning here.
1231
+ dyna0 = DynaQAgent(sampler, 0.5, 0.0, noS, noA, epsilon=0.0, planningSteps=0)
1232
+ dyna0.UpdateParameters(state=0, nextState=0, reward=1.0, action=0)
1233
+ dyna0.UpdateParameters(state=0, nextState=0, reward=3.0, action=0)
1234
+ advResults["DynaQ_Q00"] = float(dyna0.qTable[0, 0])
1235
+
1236
+ # PrioritizedSweepingAgent with planningSteps=0 should also reduce to a Q-like update.
1237
+ psa0 = PrioritizedSweepingAgent(sampler, 0.5, 0.0, noS, noA, epsilon=0.0, planningSteps=0, theta=0.0)
1238
+ psa0.UpdateParameters(state=0, nextState=0, reward=1.0, action=0)
1239
+ psa0.UpdateParameters(state=0, nextState=0, reward=3.0, action=0)
1240
+ advResults["PSA_Q00"] = float(psa0.qTable[0, 0])
1241
+
1242
+ # Analytical expected Q after two sequential updates with alpha=0.5, gamma=0:
1243
+ # Q1 = 0 + 0.5*(1 - 0) = 0.5
1244
+ # Q2 = 0.5 + 0.5*(3 - 0.5) = 1.75
1245
+ advExpected = {"QLearning_Q00": 1.75, "DynaQ_Q00": 1.75, "PSA_Q00": 1.75}
1246
+
1247
+ print("\nAdvanced test results:")
1248
+ advAllPass = True
1249
+ for k, obs in advResults.items():
1250
+ exp = advExpected[k]
1251
+ ok = bool(np.isclose(obs, exp, rtol=1e-6, atol=1e-8))
1252
+ advAllPass = advAllPass and ok
1253
+ print(f" {k:15s}: observed={obs:.6f} expected={exp:.6f} [{'PASS' if ok else 'FAIL'}]")
1254
+
1255
+ print("\nAdvanced overall: " + ("PASS" if advAllPass else "FAIL"))
1256
+
1257
+ # End of advanced test.
1258
+ print("\nAll deterministic tests completed.")