QuizGenerator 0.8.1__py3-none-any.whl → 0.10.0__py3-none-any.whl
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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/canvas/canvas_interface.py +6 -2
- QuizGenerator/contentast.py +33 -11
- QuizGenerator/generate.py +51 -10
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/mixins.py +6 -2
- QuizGenerator/premade_questions/basic.py +49 -7
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +92 -82
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +68 -45
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +238 -162
- QuizGenerator/premade_questions/cst463/models/attention.py +0 -1
- QuizGenerator/premade_questions/cst463/models/cnns.py +0 -1
- QuizGenerator/premade_questions/cst463/models/rnns.py +0 -1
- QuizGenerator/premade_questions/cst463/models/text.py +0 -1
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +20 -1
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +51 -45
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +212 -215
- QuizGenerator/qrcode_generator.py +116 -54
- QuizGenerator/question.py +168 -23
- QuizGenerator/regenerate.py +23 -9
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/METADATA +34 -22
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/RECORD +25 -23
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,48 +15,52 @@ log = logging.getLogger(__name__)
|
|
|
15
15
|
class DerivativeQuestion(Question, abc.ABC):
|
|
16
16
|
"""Base class for derivative calculation questions."""
|
|
17
17
|
|
|
18
|
+
DEFAULT_NUM_VARIABLES = 2
|
|
19
|
+
DEFAULT_MAX_DEGREE = 2
|
|
20
|
+
|
|
18
21
|
def __init__(self, *args, **kwargs):
|
|
19
22
|
kwargs["topic"] = kwargs.get("topic", Question.Topic.ML_OPTIMIZATION)
|
|
20
23
|
super().__init__(*args, **kwargs)
|
|
21
|
-
self.num_variables = kwargs.get("num_variables",
|
|
22
|
-
self.max_degree = kwargs.get("max_degree",
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
self.rng.seed(rng_seed)
|
|
30
|
-
context = dict(kwargs)
|
|
31
|
-
context["rng_seed"] = rng_seed
|
|
24
|
+
self.num_variables = kwargs.get("num_variables", self.DEFAULT_NUM_VARIABLES)
|
|
25
|
+
self.max_degree = kwargs.get("max_degree", self.DEFAULT_MAX_DEGREE)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
29
|
+
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
30
|
+
context["num_variables"] = kwargs.get("num_variables", cls.DEFAULT_NUM_VARIABLES)
|
|
31
|
+
context["max_degree"] = kwargs.get("max_degree", cls.DEFAULT_MAX_DEGREE)
|
|
32
32
|
return context
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
@classmethod
|
|
35
|
+
def _generate_evaluation_point(cls, context) -> List[float]:
|
|
35
36
|
"""Generate a random point for gradient evaluation."""
|
|
36
|
-
return [
|
|
37
|
+
return [context.rng.randint(-3, 3) for _ in range(context.num_variables)]
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _format_partial_derivative(var_index: int, num_variables: int) -> str:
|
|
39
41
|
"""Format partial derivative symbol for display."""
|
|
40
|
-
if
|
|
42
|
+
if num_variables == 1:
|
|
41
43
|
return "\\frac{df}{dx_0}"
|
|
42
|
-
|
|
43
|
-
return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
|
|
44
|
+
return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _create_derivative_answers(context) -> List[ca.Answer]:
|
|
46
48
|
"""Create answer fields for each partial derivative at the evaluation point."""
|
|
47
49
|
answers: List[ca.Answer] = []
|
|
48
50
|
|
|
49
51
|
# Evaluate gradient at the specified point
|
|
50
|
-
subs_map = dict(zip(
|
|
52
|
+
subs_map = dict(zip(context.variables, context.evaluation_point))
|
|
51
53
|
|
|
52
54
|
# Format evaluation point for label
|
|
53
|
-
eval_point_str = ", ".join(
|
|
55
|
+
eval_point_str = ", ".join(
|
|
56
|
+
[f"x_{i} = {context.evaluation_point[i]}" for i in range(context.num_variables)]
|
|
57
|
+
)
|
|
54
58
|
|
|
55
59
|
# Create answer for each partial derivative
|
|
56
|
-
for i in range(
|
|
60
|
+
for i in range(context.num_variables):
|
|
57
61
|
answer_key = f"partial_derivative_{i}"
|
|
58
62
|
# Evaluate the partial derivative and convert to float
|
|
59
|
-
partial_value =
|
|
63
|
+
partial_value = context.gradient_function[i].subs(subs_map)
|
|
60
64
|
try:
|
|
61
65
|
gradient_value = float(partial_value)
|
|
62
66
|
except (TypeError, ValueError):
|
|
@@ -71,14 +75,15 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
71
75
|
|
|
72
76
|
return answers
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _create_gradient_vector_answer(context) -> ca.Answer:
|
|
75
80
|
"""Create a single gradient vector answer for PDF format."""
|
|
76
81
|
# Format gradient as vector notation
|
|
77
|
-
subs_map = dict(zip(
|
|
82
|
+
subs_map = dict(zip(context.variables, context.evaluation_point))
|
|
78
83
|
gradient_values = []
|
|
79
84
|
|
|
80
|
-
for i in range(
|
|
81
|
-
partial_value =
|
|
85
|
+
for i in range(context.num_variables):
|
|
86
|
+
partial_value = context.gradient_function[i].subs(subs_map)
|
|
82
87
|
try:
|
|
83
88
|
gradient_value = float(partial_value)
|
|
84
89
|
except TypeError:
|
|
@@ -89,7 +94,8 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
89
94
|
vector_str = format_vector(gradient_values)
|
|
90
95
|
return ca.AnswerTypes.String(vector_str, pdf_only=True)
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
@classmethod
|
|
98
|
+
def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
93
99
|
"""Build question body and collect answers."""
|
|
94
100
|
body = ca.Section()
|
|
95
101
|
answers = []
|
|
@@ -98,15 +104,17 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
98
104
|
body.add_element(
|
|
99
105
|
ca.Paragraph([
|
|
100
106
|
"Given the function ",
|
|
101
|
-
ca.Equation(sp.latex(
|
|
107
|
+
ca.Equation(sp.latex(context.equation), inline=True),
|
|
102
108
|
", calculate the gradient at the point ",
|
|
103
|
-
ca.Equation(format_vector(
|
|
109
|
+
ca.Equation(format_vector(context.evaluation_point), inline=True),
|
|
104
110
|
"."
|
|
105
111
|
])
|
|
106
112
|
)
|
|
107
113
|
|
|
108
114
|
# Format evaluation point for LaTeX
|
|
109
|
-
eval_point_str = ", ".join(
|
|
115
|
+
eval_point_str = ", ".join(
|
|
116
|
+
[f"x_{i} = {context.evaluation_point[i]}" for i in range(context.num_variables)]
|
|
117
|
+
)
|
|
110
118
|
|
|
111
119
|
# For PDF: Use OnlyLatex to show gradient vector format (no answer blank)
|
|
112
120
|
body.add_element(
|
|
@@ -121,14 +129,14 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
121
129
|
)
|
|
122
130
|
|
|
123
131
|
# For Canvas: Use OnlyHtml to show individual partial derivatives
|
|
124
|
-
derivative_answers =
|
|
132
|
+
derivative_answers = cls._create_derivative_answers(context)
|
|
125
133
|
for i, answer in enumerate(derivative_answers):
|
|
126
134
|
answers.append(answer)
|
|
127
135
|
body.add_element(
|
|
128
136
|
ca.OnlyHtml([
|
|
129
137
|
ca.Paragraph([
|
|
130
138
|
ca.Equation(
|
|
131
|
-
f"\\left. {
|
|
139
|
+
f"\\left. {cls._format_partial_derivative(i, context.num_variables)} \\right|_{{{eval_point_str}}} = ",
|
|
132
140
|
inline=True
|
|
133
141
|
),
|
|
134
142
|
answer
|
|
@@ -138,7 +146,8 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
138
146
|
|
|
139
147
|
return body, answers
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
@classmethod
|
|
150
|
+
def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
142
151
|
"""Build question explanation."""
|
|
143
152
|
explanation = ca.Section()
|
|
144
153
|
|
|
@@ -146,27 +155,27 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
146
155
|
explanation.add_element(
|
|
147
156
|
ca.Paragraph([
|
|
148
157
|
"To find the gradient, we calculate the partial derivatives of ",
|
|
149
|
-
ca.Equation(sp.latex(
|
|
158
|
+
ca.Equation(sp.latex(context.equation), inline=True),
|
|
150
159
|
":"
|
|
151
160
|
])
|
|
152
161
|
)
|
|
153
162
|
|
|
154
163
|
# Show analytical gradient
|
|
155
164
|
explanation.add_element(
|
|
156
|
-
ca.Equation(f"\\nabla f = {sp.latex(
|
|
165
|
+
ca.Equation(f"\\nabla f = {sp.latex(context.gradient_function)}", inline=False)
|
|
157
166
|
)
|
|
158
167
|
|
|
159
168
|
# Show evaluation at the specific point
|
|
160
169
|
explanation.add_element(
|
|
161
170
|
ca.Paragraph([
|
|
162
|
-
f"Evaluating at the point {format_vector(
|
|
171
|
+
f"Evaluating at the point {format_vector(context.evaluation_point)}:"
|
|
163
172
|
])
|
|
164
173
|
)
|
|
165
174
|
|
|
166
175
|
# Show each partial derivative calculation
|
|
167
|
-
subs_map = dict(zip(
|
|
168
|
-
for i in range(
|
|
169
|
-
partial_expr =
|
|
176
|
+
subs_map = dict(zip(context.variables, context.evaluation_point))
|
|
177
|
+
for i in range(context.num_variables):
|
|
178
|
+
partial_expr = context.gradient_function[i]
|
|
170
179
|
partial_value = partial_expr.subs(subs_map)
|
|
171
180
|
|
|
172
181
|
# Use ca.Answer.accepted_strings for clean numerical formatting
|
|
@@ -181,7 +190,7 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
181
190
|
explanation.add_element(
|
|
182
191
|
ca.Paragraph([
|
|
183
192
|
ca.Equation(
|
|
184
|
-
f"{
|
|
193
|
+
f"{cls._format_partial_derivative(i, context.num_variables)} = {sp.latex(partial_expr)} = {clean_value}",
|
|
185
194
|
inline=False
|
|
186
195
|
)
|
|
187
196
|
])
|
|
@@ -194,22 +203,21 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
194
203
|
class DerivativeBasic(DerivativeQuestion):
|
|
195
204
|
"""Basic derivative calculation using polynomial functions."""
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
@classmethod
|
|
207
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
208
|
+
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
199
209
|
|
|
200
210
|
# Generate a basic polynomial function
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
context.variables, context.function, context.gradient_function, context.equation = generate_function(
|
|
212
|
+
context.rng, context.num_variables, context.max_degree
|
|
203
213
|
)
|
|
204
214
|
|
|
205
215
|
# Generate evaluation point
|
|
206
|
-
|
|
216
|
+
context.evaluation_point = cls._generate_evaluation_point(context)
|
|
207
217
|
|
|
208
218
|
# Create answers for evaluation point (used in _build_body)
|
|
209
|
-
|
|
219
|
+
cls._create_derivative_answers(context)
|
|
210
220
|
|
|
211
|
-
context = dict(kwargs)
|
|
212
|
-
context["rng_seed"] = rng_seed
|
|
213
221
|
return context
|
|
214
222
|
|
|
215
223
|
|
|
@@ -217,21 +225,22 @@ class DerivativeBasic(DerivativeQuestion):
|
|
|
217
225
|
class DerivativeChain(DerivativeQuestion):
|
|
218
226
|
"""Chain rule derivative calculation using function composition."""
|
|
219
227
|
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
@classmethod
|
|
229
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
230
|
+
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
222
231
|
|
|
223
232
|
# Try to generate a valid function/point combination, regenerating if we hit complex numbers
|
|
224
233
|
max_attempts = 10
|
|
225
234
|
for attempt in range(max_attempts):
|
|
226
235
|
try:
|
|
227
236
|
# Generate inner and outer functions for composition
|
|
228
|
-
|
|
237
|
+
cls._generate_composed_function(context)
|
|
229
238
|
|
|
230
239
|
# Generate evaluation point
|
|
231
|
-
|
|
240
|
+
context.evaluation_point = cls._generate_evaluation_point(context)
|
|
232
241
|
|
|
233
242
|
# Create answers - this will raise ValueError if we get complex numbers
|
|
234
|
-
|
|
243
|
+
cls._create_derivative_answers(context)
|
|
235
244
|
|
|
236
245
|
# If we get here, everything worked
|
|
237
246
|
break
|
|
@@ -239,30 +248,28 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
239
248
|
except ValueError as e:
|
|
240
249
|
if "Complex number encountered" in str(e) and attempt < max_attempts - 1:
|
|
241
250
|
# Advance RNG state by making a dummy call
|
|
242
|
-
_ =
|
|
251
|
+
_ = context.rng.random()
|
|
243
252
|
continue
|
|
244
253
|
else:
|
|
245
254
|
# If we've exhausted attempts or different error, re-raise
|
|
246
255
|
raise
|
|
247
|
-
|
|
248
|
-
context = dict(kwargs)
|
|
249
|
-
context["rng_seed"] = rng_seed
|
|
250
256
|
return context
|
|
251
257
|
|
|
252
|
-
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _generate_composed_function(context) -> None:
|
|
253
260
|
"""Generate a composed function f(g(x)) for chain rule practice."""
|
|
254
261
|
# Create variable symbols
|
|
255
|
-
var_names = [f'x_{i}' for i in range(
|
|
256
|
-
|
|
262
|
+
var_names = [f'x_{i}' for i in range(context.num_variables)]
|
|
263
|
+
context.variables = sp.symbols(var_names)
|
|
257
264
|
|
|
258
265
|
# Generate inner function g(x) - simpler polynomial
|
|
259
|
-
inner_terms = [m for m in sp.polys.itermonomials(
|
|
266
|
+
inner_terms = [m for m in sp.polys.itermonomials(context.variables, max(1, context.max_degree-1)) if m != 1]
|
|
260
267
|
coeff_pool = [*range(-5, 0), *range(1, 6)] # Smaller coefficients for inner function
|
|
261
268
|
|
|
262
269
|
if inner_terms:
|
|
263
|
-
inner_poly = sp.Add(*(
|
|
270
|
+
inner_poly = sp.Add(*(context.rng.choice(coeff_pool) * t for t in inner_terms))
|
|
264
271
|
else:
|
|
265
|
-
inner_poly = sp.Add(*[
|
|
272
|
+
inner_poly = sp.Add(*[context.rng.choice(coeff_pool) * v for v in context.variables])
|
|
266
273
|
|
|
267
274
|
# Generate outer function - use polynomials, exp, and ln for reliable evaluation
|
|
268
275
|
u = sp.Symbol('u') # Intermediate variable
|
|
@@ -274,21 +281,24 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
274
281
|
sp.log(u + 2) # Add 2 to ensure positive argument for evaluation points
|
|
275
282
|
]
|
|
276
283
|
|
|
277
|
-
outer_func =
|
|
284
|
+
outer_func = context.rng.choice(outer_functions)
|
|
278
285
|
|
|
279
286
|
# Compose the functions: f(g(x))
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
287
|
+
context.inner_function = inner_poly
|
|
288
|
+
context.outer_function = outer_func
|
|
289
|
+
context.function = outer_func.subs(u, inner_poly)
|
|
283
290
|
|
|
284
291
|
# Calculate gradient using chain rule
|
|
285
|
-
|
|
292
|
+
context.gradient_function = sp.Matrix([context.function.diff(v) for v in context.variables])
|
|
286
293
|
|
|
287
294
|
# Create equation for display
|
|
288
295
|
f = sp.Function('f')
|
|
289
|
-
|
|
296
|
+
context.equation = sp.Eq(f(*context.variables), context.function)
|
|
297
|
+
|
|
298
|
+
return context
|
|
290
299
|
|
|
291
|
-
|
|
300
|
+
@classmethod
|
|
301
|
+
def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
292
302
|
"""Build question explanation."""
|
|
293
303
|
explanation = ca.Section()
|
|
294
304
|
|
|
@@ -296,9 +306,9 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
296
306
|
explanation.add_element(
|
|
297
307
|
ca.Paragraph([
|
|
298
308
|
"This is a composition of functions requiring the chain rule. The function ",
|
|
299
|
-
ca.Equation(sp.latex(
|
|
309
|
+
ca.Equation(sp.latex(context.equation), inline=True),
|
|
300
310
|
" can be written as ",
|
|
301
|
-
ca.Equation(f"f(g(x)) \\text{{ where }} g(x) = {sp.latex(
|
|
311
|
+
ca.Equation(f"f(g(x)) \\text{{ where }} g(x) = {sp.latex(context.inner_function)}", inline=True),
|
|
302
312
|
"."
|
|
303
313
|
])
|
|
304
314
|
)
|
|
@@ -313,7 +323,7 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
313
323
|
)
|
|
314
324
|
|
|
315
325
|
# Show chain rule formula for each variable
|
|
316
|
-
for i in range(
|
|
326
|
+
for i in range(context.num_variables):
|
|
317
327
|
var_name = f"x_{i}"
|
|
318
328
|
explanation.add_element(
|
|
319
329
|
ca.Equation(
|
|
@@ -329,12 +339,12 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
329
339
|
)
|
|
330
340
|
|
|
331
341
|
# Show the specific derivatives step by step
|
|
332
|
-
for i in range(
|
|
342
|
+
for i in range(context.num_variables):
|
|
333
343
|
var_name = f"x_{i}"
|
|
334
344
|
|
|
335
345
|
# Get outer function derivative with respect to inner function
|
|
336
|
-
outer_deriv =
|
|
337
|
-
inner_deriv =
|
|
346
|
+
outer_deriv = context.outer_function.diff(sp.Symbol('u'))
|
|
347
|
+
inner_deriv = context.inner_function.diff(context.variables[i])
|
|
338
348
|
|
|
339
349
|
explanation.add_element(
|
|
340
350
|
ca.Paragraph([
|
|
@@ -357,20 +367,20 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
357
367
|
)
|
|
358
368
|
|
|
359
369
|
explanation.add_element(
|
|
360
|
-
ca.Equation(f"\\nabla f = {sp.latex(
|
|
370
|
+
ca.Equation(f"\\nabla f = {sp.latex(context.gradient_function)}", inline=False)
|
|
361
371
|
)
|
|
362
372
|
|
|
363
373
|
# Show evaluation at the specific point
|
|
364
374
|
explanation.add_element(
|
|
365
375
|
ca.Paragraph([
|
|
366
|
-
f"Evaluating at the point {format_vector(
|
|
376
|
+
f"Evaluating at the point {format_vector(context.evaluation_point)}:"
|
|
367
377
|
])
|
|
368
378
|
)
|
|
369
379
|
|
|
370
380
|
# Show each partial derivative calculation
|
|
371
|
-
subs_map = dict(zip(
|
|
372
|
-
for i in range(
|
|
373
|
-
partial_expr =
|
|
381
|
+
subs_map = dict(zip(context.variables, context.evaluation_point))
|
|
382
|
+
for i in range(context.num_variables):
|
|
383
|
+
partial_expr = context.gradient_function[i]
|
|
374
384
|
partial_value = partial_expr.subs(subs_map)
|
|
375
385
|
|
|
376
386
|
# Use ca.Answer.accepted_strings for clean numerical formatting
|
|
@@ -385,7 +395,7 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
385
395
|
explanation.add_element(
|
|
386
396
|
ca.Paragraph([
|
|
387
397
|
ca.Equation(
|
|
388
|
-
f"{
|
|
398
|
+
f"{cls._format_partial_derivative(i, context.num_variables)} = {sp.latex(partial_expr)} = {clean_value}",
|
|
389
399
|
inline=False
|
|
390
400
|
)
|
|
391
401
|
])
|
|
@@ -4,6 +4,8 @@ import abc
|
|
|
4
4
|
import logging
|
|
5
5
|
import math
|
|
6
6
|
from typing import List, Tuple, Callable, Union, Any
|
|
7
|
+
|
|
8
|
+
import sympy
|
|
7
9
|
import sympy as sp
|
|
8
10
|
|
|
9
11
|
import QuizGenerator.contentast as ca
|
|
@@ -28,37 +30,52 @@ class GradientDescentQuestion(Question, abc.ABC):
|
|
|
28
30
|
|
|
29
31
|
@QuestionRegistry.register("GradientDescentWalkthrough")
|
|
30
32
|
class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
31
|
-
|
|
33
|
+
DEFAULT_NUM_STEPS = 4
|
|
34
|
+
DEFAULT_NUM_VARIABLES = 2
|
|
35
|
+
DEFAULT_MAX_DEGREE = 2
|
|
36
|
+
DEFAULT_SINGLE_VARIABLE = False
|
|
37
|
+
DEFAULT_MINIMIZE = True
|
|
38
|
+
|
|
32
39
|
def __init__(self, *args, **kwargs):
|
|
33
40
|
super().__init__(*args, **kwargs)
|
|
34
|
-
self.num_steps = kwargs.get("num_steps",
|
|
35
|
-
self.num_variables = kwargs.get("num_variables",
|
|
36
|
-
self.max_degree = kwargs.get("max_degree",
|
|
37
|
-
self.single_variable = kwargs.get("single_variable",
|
|
38
|
-
self.minimize = kwargs.get("minimize",
|
|
41
|
+
self.num_steps = kwargs.get("num_steps", self.DEFAULT_NUM_STEPS)
|
|
42
|
+
self.num_variables = kwargs.get("num_variables", self.DEFAULT_NUM_VARIABLES)
|
|
43
|
+
self.max_degree = kwargs.get("max_degree", self.DEFAULT_MAX_DEGREE)
|
|
44
|
+
self.single_variable = kwargs.get("single_variable", self.DEFAULT_SINGLE_VARIABLE)
|
|
45
|
+
self.minimize = kwargs.get("minimize", self.DEFAULT_MINIMIZE) # Default to minimization
|
|
39
46
|
|
|
40
47
|
if self.single_variable:
|
|
41
48
|
self.num_variables = 1
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _perform_gradient_descent(
|
|
52
|
+
cls,
|
|
53
|
+
function: sympy.Function,
|
|
54
|
+
gradient_function,
|
|
55
|
+
starting_point,
|
|
56
|
+
num_steps,
|
|
57
|
+
variables,
|
|
58
|
+
learning_rate,
|
|
59
|
+
minimize=True,
|
|
60
|
+
) -> List[dict]:
|
|
44
61
|
"""
|
|
45
62
|
Perform gradient descent and return step-by-step results.
|
|
46
63
|
"""
|
|
47
64
|
results = []
|
|
48
65
|
|
|
49
|
-
x = list(map(float,
|
|
66
|
+
x = list(map(float, starting_point)) # current location as floats
|
|
50
67
|
|
|
51
|
-
for step in range(
|
|
52
|
-
subs_map = dict(zip(
|
|
68
|
+
for step in range(num_steps):
|
|
69
|
+
subs_map = dict(zip(variables, x))
|
|
53
70
|
|
|
54
71
|
# gradient as floats
|
|
55
|
-
g_syms =
|
|
72
|
+
g_syms = gradient_function.subs(subs_map)
|
|
56
73
|
g = [float(val) for val in g_syms]
|
|
57
74
|
|
|
58
75
|
# function value
|
|
59
|
-
f_val = float(
|
|
76
|
+
f_val = float(function.subs(subs_map))
|
|
60
77
|
|
|
61
|
-
update = [
|
|
78
|
+
update = [learning_rate * gi for gi in g]
|
|
62
79
|
|
|
63
80
|
results.append(
|
|
64
81
|
{
|
|
@@ -70,61 +87,65 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
70
87
|
}
|
|
71
88
|
)
|
|
72
89
|
|
|
73
|
-
x = [xi - ui for xi, ui in zip(x, update)] if
|
|
90
|
+
x = [xi - ui for xi, ui in zip(x, update)] if minimize else \
|
|
74
91
|
[xi + ui for xi, ui in zip(x, update)]
|
|
75
92
|
|
|
76
93
|
return results
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
self.num_variables = 1
|
|
89
|
-
if "minimize" in kwargs:
|
|
90
|
-
self.minimize = kwargs.get("minimize", self.minimize)
|
|
91
|
-
|
|
92
|
-
self.rng.seed(rng_seed)
|
|
95
|
+
@classmethod
|
|
96
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
97
|
+
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
98
|
+
context.num_steps = kwargs.get("num_steps", cls.DEFAULT_NUM_STEPS)
|
|
99
|
+
context.num_variables = kwargs.get("num_variables", cls.DEFAULT_NUM_VARIABLES)
|
|
100
|
+
context.max_degree = kwargs.get("max_degree", cls.DEFAULT_MAX_DEGREE)
|
|
101
|
+
context.single_variable = kwargs.get("single_variable", cls.DEFAULT_SINGLE_VARIABLE)
|
|
102
|
+
if context.single_variable:
|
|
103
|
+
context.num_variables = 1
|
|
104
|
+
context.minimize = kwargs.get("minimize", cls.DEFAULT_MINIMIZE)
|
|
93
105
|
|
|
94
106
|
# Generate function and its properties
|
|
95
|
-
|
|
107
|
+
context.variables, context.function, context.gradient_function, context.equation = generate_function(
|
|
108
|
+
context.rng, context.num_variables, context.max_degree
|
|
109
|
+
)
|
|
96
110
|
|
|
97
111
|
# Generate learning rate (expanded range)
|
|
98
|
-
|
|
112
|
+
context.learning_rate = context.rng.choice([0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5])
|
|
99
113
|
|
|
100
|
-
|
|
114
|
+
context.starting_point = [context.rng.randint(-3, 3) for _ in range(context.num_variables)]
|
|
101
115
|
|
|
102
116
|
# Perform gradient descent
|
|
103
|
-
|
|
117
|
+
context.gradient_descent_results = cls._perform_gradient_descent(
|
|
118
|
+
context.function,
|
|
119
|
+
context.gradient_function,
|
|
120
|
+
context.starting_point,
|
|
121
|
+
context.num_steps,
|
|
122
|
+
context.variables,
|
|
123
|
+
context.learning_rate,
|
|
124
|
+
minimize=context.minimize,
|
|
125
|
+
)
|
|
104
126
|
|
|
105
127
|
# Build answers for each step
|
|
106
|
-
|
|
107
|
-
for i, result in enumerate(
|
|
128
|
+
context.step_answers = {}
|
|
129
|
+
for i, result in enumerate(context.gradient_descent_results):
|
|
108
130
|
step = result['step']
|
|
109
131
|
|
|
110
132
|
# Location answer
|
|
111
133
|
location_key = f"answer__location_{step}"
|
|
112
|
-
|
|
134
|
+
context.step_answers[location_key] = ca.AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
|
|
113
135
|
|
|
114
136
|
# Gradient answer
|
|
115
137
|
gradient_key = f"answer__gradient_{step}"
|
|
116
|
-
|
|
138
|
+
context.step_answers[gradient_key] = ca.AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
|
|
117
139
|
|
|
118
140
|
# Update answer
|
|
119
141
|
update_key = f"answer__update_{step}"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
context = dict(kwargs)
|
|
123
|
-
context["rng_seed"] = rng_seed
|
|
142
|
+
context.step_answers[update_key] = ca.AnswerTypes.Vector(list(result['update']), label=f"Update at step {step}")
|
|
124
143
|
return context
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
@classmethod
|
|
146
|
+
def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
127
147
|
"""Build question body and collect answers."""
|
|
148
|
+
self = context
|
|
128
149
|
body = ca.Section()
|
|
129
150
|
answers = []
|
|
130
151
|
|
|
@@ -179,7 +200,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
179
200
|
table_rows.append(row)
|
|
180
201
|
|
|
181
202
|
# Create the table using mixin
|
|
182
|
-
gradient_table =
|
|
203
|
+
gradient_table = cls.create_answer_table(
|
|
183
204
|
headers=headers,
|
|
184
205
|
data_rows=table_rows,
|
|
185
206
|
answer_columns=["location", headers[2], headers[3]] # Use actual header objects
|
|
@@ -189,8 +210,10 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
189
210
|
|
|
190
211
|
return body, answers
|
|
191
212
|
|
|
192
|
-
|
|
213
|
+
@classmethod
|
|
214
|
+
def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
193
215
|
"""Build question explanation."""
|
|
216
|
+
self = context
|
|
194
217
|
explanation = ca.Section()
|
|
195
218
|
|
|
196
219
|
explanation.add_element(
|