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.
- hmb_helpers-0.1.0/HMB/AgentsHelper.py +1258 -0
- hmb_helpers-0.1.0/HMB/ArabicTextHelper.py +476 -0
- hmb_helpers-0.1.0/HMB/AttentionMapsHelper.py +686 -0
- hmb_helpers-0.1.0/HMB/AudioHelper.py +1304 -0
- hmb_helpers-0.1.0/HMB/CompressionsHelper.py +123 -0
- hmb_helpers-0.1.0/HMB/DataAugmentationHelper.py +1044 -0
- hmb_helpers-0.1.0/HMB/DatasetsHelper.py +2408 -0
- hmb_helpers-0.1.0/HMB/EmbeddingsToTextHelper.py +391 -0
- hmb_helpers-0.1.0/HMB/ExplainabilityHelper.py +5649 -0
- hmb_helpers-0.1.0/HMB/HandCraftedFeatures.py +2283 -0
- hmb_helpers-0.1.0/HMB/ImageSegmentationMetrics.py +1558 -0
- hmb_helpers-0.1.0/HMB/ImagesComparisonMetrics.py +1637 -0
- hmb_helpers-0.1.0/HMB/ImagesHelper.py +2212 -0
- hmb_helpers-0.1.0/HMB/ImagesNormalization.py +632 -0
- hmb_helpers-0.1.0/HMB/ImagesToEmbeddings.py +202 -0
- hmb_helpers-0.1.0/HMB/Initializations.py +570 -0
- hmb_helpers-0.1.0/HMB/MachineLearningHelper.py +4011 -0
- hmb_helpers-0.1.0/HMB/MetaheuristicsHelper.py +163 -0
- hmb_helpers-0.1.0/HMB/PDFHelper.py +585 -0
- hmb_helpers-0.1.0/HMB/PerformanceMetrics.py +4391 -0
- hmb_helpers-0.1.0/HMB/PlotsHelper.py +1507 -0
- hmb_helpers-0.1.0/HMB/PyTorchClassificationLosses.py +307 -0
- hmb_helpers-0.1.0/HMB/PyTorchHelper.py +2147 -0
- hmb_helpers-0.1.0/HMB/PyTorchModelMemoryProfiler.py +1180 -0
- hmb_helpers-0.1.0/HMB/PyTorchSegmentationLosses.py +343 -0
- hmb_helpers-0.1.0/HMB/PyTorchTabularModelsZoo.py +1659 -0
- hmb_helpers-0.1.0/HMB/PyTorchTrainingPipeline.py +3645 -0
- hmb_helpers-0.1.0/HMB/PyTorchUNetModelsZoo.py +3565 -0
- hmb_helpers-0.1.0/HMB/StatisticalAnalysisHelper.py +5329 -0
- hmb_helpers-0.1.0/HMB/StringsHelper.py +525 -0
- hmb_helpers-0.1.0/HMB/TFAttentionBlocks.py +1466 -0
- hmb_helpers-0.1.0/HMB/TFHelper.py +3011 -0
- hmb_helpers-0.1.0/HMB/TFSegmentationLosses.py +327 -0
- hmb_helpers-0.1.0/HMB/TFUNetHelper.py +3123 -0
- hmb_helpers-0.1.0/HMB/TextGenerationMetrics.py +720 -0
- hmb_helpers-0.1.0/HMB/TextHelper.py +1051 -0
- hmb_helpers-0.1.0/HMB/Utils.py +897 -0
- hmb_helpers-0.1.0/HMB/VectorsHelper.py +251 -0
- hmb_helpers-0.1.0/HMB/VideosHelper.py +368 -0
- hmb_helpers-0.1.0/HMB/VotingHelper.py +2611 -0
- hmb_helpers-0.1.0/HMB/WSIHelper.py +1891 -0
- hmb_helpers-0.1.0/HMB/YOLOHelper.py +939 -0
- hmb_helpers-0.1.0/HMB/__init__.py +0 -0
- hmb_helpers-0.1.0/LICENSE +23 -0
- hmb_helpers-0.1.0/PKG-INFO +537 -0
- hmb_helpers-0.1.0/README.md +316 -0
- hmb_helpers-0.1.0/hmb_helpers.egg-info/PKG-INFO +537 -0
- hmb_helpers-0.1.0/hmb_helpers.egg-info/SOURCES.txt +87 -0
- hmb_helpers-0.1.0/hmb_helpers.egg-info/dependency_links.txt +1 -0
- hmb_helpers-0.1.0/hmb_helpers.egg-info/not-zip-safe +1 -0
- hmb_helpers-0.1.0/hmb_helpers.egg-info/requires.txt +194 -0
- hmb_helpers-0.1.0/hmb_helpers.egg-info/top_level.txt +1 -0
- hmb_helpers-0.1.0/setup.cfg +4 -0
- hmb_helpers-0.1.0/setup.py +273 -0
- hmb_helpers-0.1.0/tests/Test_AgentsHelper.py +109 -0
- hmb_helpers-0.1.0/tests/Test_ArabicTextHelper.py +100 -0
- hmb_helpers-0.1.0/tests/Test_AttentionMapsHelper.py +39 -0
- hmb_helpers-0.1.0/tests/Test_AudioHelper.py +156 -0
- hmb_helpers-0.1.0/tests/Test_CompressionsHelper.py +161 -0
- hmb_helpers-0.1.0/tests/Test_DataAugmentationHelper.py +60 -0
- hmb_helpers-0.1.0/tests/Test_DatasetsHelper.py +207 -0
- hmb_helpers-0.1.0/tests/Test_EmbeddingsToTextHelper.py +138 -0
- hmb_helpers-0.1.0/tests/Test_ExplainabilityHelper.py +89 -0
- hmb_helpers-0.1.0/tests/Test_HandCraftedFeatures.py +59 -0
- hmb_helpers-0.1.0/tests/Test_ImageSegmentationMetrics.py +213 -0
- hmb_helpers-0.1.0/tests/Test_ImagesComparisonMetrics.py +181 -0
- hmb_helpers-0.1.0/tests/Test_ImagesHelper.py +306 -0
- hmb_helpers-0.1.0/tests/Test_ImagesNormalization.py +169 -0
- hmb_helpers-0.1.0/tests/Test_ImagesToEmbeddings.py +58 -0
- hmb_helpers-0.1.0/tests/Test_Initializations.py +313 -0
- hmb_helpers-0.1.0/tests/Test_MachineLearningHelper.py +132 -0
- hmb_helpers-0.1.0/tests/Test_MetaheuristicsHelper.py +33 -0
- hmb_helpers-0.1.0/tests/Test_PDFHelper.py +575 -0
- hmb_helpers-0.1.0/tests/Test_PerformanceMetrics.py +43 -0
- hmb_helpers-0.1.0/tests/Test_PyTorchClassificationLosses.py +64 -0
- hmb_helpers-0.1.0/tests/Test_PyTorchHelper.py +136 -0
- hmb_helpers-0.1.0/tests/Test_PyTorchSegmentationLosses.py +50 -0
- hmb_helpers-0.1.0/tests/Test_StatisticalAnalysisHelper.py +127 -0
- hmb_helpers-0.1.0/tests/Test_StringsHelper.py +289 -0
- hmb_helpers-0.1.0/tests/Test_TFAttentionBlocks.py +75 -0
- hmb_helpers-0.1.0/tests/Test_TFHelper.py +66 -0
- hmb_helpers-0.1.0/tests/Test_TextGenerationMetrics.py +103 -0
- hmb_helpers-0.1.0/tests/Test_TextHelper.py +90 -0
- hmb_helpers-0.1.0/tests/Test_Utils.py +624 -0
- hmb_helpers-0.1.0/tests/Test_VectorsHelper.py +363 -0
- hmb_helpers-0.1.0/tests/Test_VideosHelper.py +55 -0
- hmb_helpers-0.1.0/tests/Test_VotingHelper.py +533 -0
- hmb_helpers-0.1.0/tests/Test_WSIHelper.py +83 -0
- 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.")
|