QuizGenerator 0.4.3__py3-none-any.whl → 0.5.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/contentast.py +949 -80
- QuizGenerator/generate.py +44 -7
- QuizGenerator/misc.py +4 -554
- QuizGenerator/mixins.py +47 -25
- QuizGenerator/premade_questions/cst334/languages.py +139 -125
- QuizGenerator/premade_questions/cst334/math_questions.py +78 -66
- QuizGenerator/premade_questions/cst334/memory_questions.py +258 -144
- QuizGenerator/premade_questions/cst334/persistence_questions.py +71 -33
- QuizGenerator/premade_questions/cst334/process.py +51 -20
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +32 -6
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +59 -34
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +27 -8
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +53 -32
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +228 -88
- QuizGenerator/premade_questions/cst463/models/attention.py +26 -10
- QuizGenerator/premade_questions/cst463/models/cnns.py +32 -19
- QuizGenerator/premade_questions/cst463/models/rnns.py +25 -12
- QuizGenerator/premade_questions/cst463/models/text.py +26 -11
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +36 -22
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +89 -109
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +126 -53
- QuizGenerator/question.py +110 -15
- QuizGenerator/quiz.py +74 -23
- QuizGenerator/regenerate.py +98 -29
- {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/METADATA +1 -1
- {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/RECORD +29 -31
- QuizGenerator/README.md +0 -5
- QuizGenerator/logging.yaml +0 -55
- {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -85,16 +85,18 @@ class ParameterCountingQuestion(Question):
|
|
|
85
85
|
"""Create answer fields."""
|
|
86
86
|
self.answers = {}
|
|
87
87
|
|
|
88
|
-
self.answers["total_weights"] = Answer.integer("total_weights", self.total_weights)
|
|
88
|
+
self.answers["total_weights"] = Answer.integer("total_weights", self.total_weights, label="Total weights")
|
|
89
89
|
|
|
90
90
|
if self.include_biases:
|
|
91
|
-
self.answers["total_biases"] = Answer.integer("total_biases", self.total_biases)
|
|
92
|
-
self.answers["total_params"] = Answer.integer("total_params", self.total_params)
|
|
91
|
+
self.answers["total_biases"] = Answer.integer("total_biases", self.total_biases, label="Total biases")
|
|
92
|
+
self.answers["total_params"] = Answer.integer("total_params", self.total_params, label="Total trainable parameters")
|
|
93
93
|
else:
|
|
94
|
-
self.answers["total_params"] = Answer.integer("total_params", self.total_params)
|
|
94
|
+
self.answers["total_params"] = Answer.integer("total_params", self.total_params, label="Total trainable parameters")
|
|
95
95
|
|
|
96
|
-
def
|
|
96
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
97
|
+
"""Build question body and collect answers."""
|
|
97
98
|
body = ContentAST.Section()
|
|
99
|
+
answers = []
|
|
98
100
|
|
|
99
101
|
# Question description
|
|
100
102
|
body.add_element(ContentAST.Paragraph([
|
|
@@ -122,27 +124,36 @@ class ParameterCountingQuestion(Question):
|
|
|
122
124
|
table_data = []
|
|
123
125
|
table_data.append(["Parameter Type", "Count"])
|
|
124
126
|
|
|
127
|
+
answers.append(self.answers["total_weights"])
|
|
125
128
|
table_data.append([
|
|
126
129
|
"Total weights (connections between layers)",
|
|
127
|
-
|
|
130
|
+
self.answers["total_weights"]
|
|
128
131
|
])
|
|
129
132
|
|
|
130
133
|
if self.include_biases:
|
|
134
|
+
answers.append(self.answers["total_biases"])
|
|
131
135
|
table_data.append([
|
|
132
136
|
"Total biases",
|
|
133
|
-
|
|
137
|
+
self.answers["total_biases"]
|
|
134
138
|
])
|
|
135
139
|
|
|
140
|
+
answers.append(self.answers["total_params"])
|
|
136
141
|
table_data.append([
|
|
137
142
|
"Total trainable parameters",
|
|
138
|
-
|
|
143
|
+
self.answers["total_params"]
|
|
139
144
|
])
|
|
140
145
|
|
|
141
146
|
body.add_element(ContentAST.Table(data=table_data))
|
|
142
147
|
|
|
148
|
+
return body, answers
|
|
149
|
+
|
|
150
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
151
|
+
"""Build question body (backward compatible interface)."""
|
|
152
|
+
body, _ = self._get_body(**kwargs)
|
|
143
153
|
return body
|
|
144
154
|
|
|
145
|
-
def
|
|
155
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
156
|
+
"""Build question explanation."""
|
|
146
157
|
explanation = ContentAST.Section()
|
|
147
158
|
|
|
148
159
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -150,7 +161,7 @@ class ParameterCountingQuestion(Question):
|
|
|
150
161
|
]))
|
|
151
162
|
|
|
152
163
|
explanation.add_element(ContentAST.Paragraph([
|
|
153
|
-
"
|
|
164
|
+
ContentAST.Text("Weights calculation:", emphasis=True)
|
|
154
165
|
]))
|
|
155
166
|
|
|
156
167
|
for i in range(len(self.layer_sizes) - 1):
|
|
@@ -174,7 +185,7 @@ class ParameterCountingQuestion(Question):
|
|
|
174
185
|
|
|
175
186
|
if self.include_biases:
|
|
176
187
|
explanation.add_element(ContentAST.Paragraph([
|
|
177
|
-
"
|
|
188
|
+
ContentAST.Text("Biases calculation:", emphasis=True)
|
|
178
189
|
]))
|
|
179
190
|
|
|
180
191
|
for i in range(len(self.layer_sizes) - 1):
|
|
@@ -194,7 +205,7 @@ class ParameterCountingQuestion(Question):
|
|
|
194
205
|
]))
|
|
195
206
|
|
|
196
207
|
explanation.add_element(ContentAST.Paragraph([
|
|
197
|
-
"
|
|
208
|
+
ContentAST.Text("Total trainable parameters:", emphasis=True)
|
|
198
209
|
]))
|
|
199
210
|
|
|
200
211
|
if self.include_biases:
|
|
@@ -208,6 +219,11 @@ class ParameterCountingQuestion(Question):
|
|
|
208
219
|
inline=False
|
|
209
220
|
))
|
|
210
221
|
|
|
222
|
+
return explanation, []
|
|
223
|
+
|
|
224
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
225
|
+
"""Build question explanation (backward compatible interface)."""
|
|
226
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
211
227
|
return explanation
|
|
212
228
|
|
|
213
229
|
|
|
@@ -315,15 +331,17 @@ class ActivationFunctionComputationQuestion(Question):
|
|
|
315
331
|
|
|
316
332
|
if self.activation == self.ACTIVATION_SOFTMAX:
|
|
317
333
|
# Softmax: single vector answer
|
|
318
|
-
self.answers["output"] = Answer.vector_value("output", self.output_vector)
|
|
334
|
+
self.answers["output"] = Answer.vector_value("output", self.output_vector, label="Output vector")
|
|
319
335
|
else:
|
|
320
336
|
# Element-wise: individual answers
|
|
321
337
|
for i, output in enumerate(self.output_vector):
|
|
322
338
|
key = f"output_{i}"
|
|
323
|
-
self.answers[key] = Answer.float_value(key, float(output))
|
|
339
|
+
self.answers[key] = Answer.float_value(key, float(output), label=f"Output for input {self.input_vector[i]:.1f}")
|
|
324
340
|
|
|
325
|
-
def
|
|
341
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
342
|
+
"""Build question body and collect answers."""
|
|
326
343
|
body = ContentAST.Section()
|
|
344
|
+
answers = []
|
|
327
345
|
|
|
328
346
|
# Question description
|
|
329
347
|
body.add_element(ContentAST.Paragraph([
|
|
@@ -349,9 +367,10 @@ class ActivationFunctionComputationQuestion(Question):
|
|
|
349
367
|
"Compute the output vector:"
|
|
350
368
|
]))
|
|
351
369
|
|
|
370
|
+
answers.append(self.answers["output"])
|
|
352
371
|
table_data = []
|
|
353
372
|
table_data.append(["Output Vector"])
|
|
354
|
-
table_data.append([
|
|
373
|
+
table_data.append([self.answers["output"]])
|
|
355
374
|
|
|
356
375
|
body.add_element(ContentAST.Table(data=table_data))
|
|
357
376
|
|
|
@@ -364,16 +383,24 @@ class ActivationFunctionComputationQuestion(Question):
|
|
|
364
383
|
table_data.append(["Input", "Output"])
|
|
365
384
|
|
|
366
385
|
for i, x in enumerate(self.input_vector):
|
|
386
|
+
answer = self.answers[f"output_{i}"]
|
|
387
|
+
answers.append(answer)
|
|
367
388
|
table_data.append([
|
|
368
389
|
ContentAST.Equation(f"{x:.1f}", inline=True),
|
|
369
|
-
|
|
390
|
+
answer
|
|
370
391
|
])
|
|
371
392
|
|
|
372
393
|
body.add_element(ContentAST.Table(data=table_data))
|
|
373
394
|
|
|
395
|
+
return body, answers
|
|
396
|
+
|
|
397
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
398
|
+
"""Build question body (backward compatible interface)."""
|
|
399
|
+
body, _ = self._get_body(**kwargs)
|
|
374
400
|
return body
|
|
375
401
|
|
|
376
|
-
def
|
|
402
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
403
|
+
"""Build question explanation."""
|
|
377
404
|
explanation = ContentAST.Section()
|
|
378
405
|
|
|
379
406
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -382,7 +409,7 @@ class ActivationFunctionComputationQuestion(Question):
|
|
|
382
409
|
|
|
383
410
|
if self.activation == self.ACTIVATION_SOFTMAX:
|
|
384
411
|
explanation.add_element(ContentAST.Paragraph([
|
|
385
|
-
"
|
|
412
|
+
ContentAST.Text("Softmax computation:", emphasis=True)
|
|
386
413
|
]))
|
|
387
414
|
|
|
388
415
|
# Show exponentials
|
|
@@ -419,7 +446,7 @@ class ActivationFunctionComputationQuestion(Question):
|
|
|
419
446
|
|
|
420
447
|
else:
|
|
421
448
|
explanation.add_element(ContentAST.Paragraph([
|
|
422
|
-
"
|
|
449
|
+
ContentAST.Text("Element-wise computation:", emphasis=True)
|
|
423
450
|
]))
|
|
424
451
|
|
|
425
452
|
for i, (x, y) in enumerate(zip(self.input_vector, self.output_vector)):
|
|
@@ -441,6 +468,11 @@ class ActivationFunctionComputationQuestion(Question):
|
|
|
441
468
|
inline=False
|
|
442
469
|
))
|
|
443
470
|
|
|
471
|
+
return explanation, []
|
|
472
|
+
|
|
473
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
474
|
+
"""Build question explanation (backward compatible interface)."""
|
|
475
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
444
476
|
return explanation
|
|
445
477
|
|
|
446
478
|
|
|
@@ -513,14 +545,16 @@ class RegularizationCalculationQuestion(Question):
|
|
|
513
545
|
"""Create answer fields."""
|
|
514
546
|
self.answers = {}
|
|
515
547
|
|
|
516
|
-
self.answers["prediction"] = Answer.float_value("prediction", float(self.prediction))
|
|
517
|
-
self.answers["base_loss"] = Answer.float_value("base_loss", float(self.base_loss))
|
|
518
|
-
self.answers["l2_penalty"] = Answer.float_value("l2_penalty", float(self.l2_penalty))
|
|
519
|
-
self.answers["total_loss"] = Answer.float_value("total_loss", float(self.total_loss))
|
|
520
|
-
self.answers["grad_total_w0"] = Answer.auto_float("grad_total_w0", float(self.grad_total_w0))
|
|
548
|
+
self.answers["prediction"] = Answer.float_value("prediction", float(self.prediction), label="Prediction ŷ")
|
|
549
|
+
self.answers["base_loss"] = Answer.float_value("base_loss", float(self.base_loss), label="Base MSE loss")
|
|
550
|
+
self.answers["l2_penalty"] = Answer.float_value("l2_penalty", float(self.l2_penalty), label="L2 penalty")
|
|
551
|
+
self.answers["total_loss"] = Answer.float_value("total_loss", float(self.total_loss), label="Total loss")
|
|
552
|
+
self.answers["grad_total_w0"] = Answer.auto_float("grad_total_w0", float(self.grad_total_w0), label="Gradient ∂L/∂w₀")
|
|
521
553
|
|
|
522
|
-
def
|
|
554
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
555
|
+
"""Build question body and collect answers."""
|
|
523
556
|
body = ContentAST.Section()
|
|
557
|
+
answers = []
|
|
524
558
|
|
|
525
559
|
# Question description
|
|
526
560
|
body.add_element(ContentAST.Paragraph([
|
|
@@ -570,36 +604,47 @@ class RegularizationCalculationQuestion(Question):
|
|
|
570
604
|
table_data = []
|
|
571
605
|
table_data.append(["Calculation", "Value"])
|
|
572
606
|
|
|
607
|
+
answers.append(self.answers["prediction"])
|
|
573
608
|
table_data.append([
|
|
574
609
|
ContentAST.Paragraph(["Prediction ", ContentAST.Equation(r"\hat{y}", inline=True)]),
|
|
575
|
-
|
|
610
|
+
self.answers["prediction"]
|
|
576
611
|
])
|
|
577
612
|
|
|
613
|
+
answers.append(self.answers["base_loss"])
|
|
578
614
|
table_data.append([
|
|
579
615
|
ContentAST.Paragraph(["Base MSE loss: ", ContentAST.Equation(r"L_{base} = (1/2)(y - \hat{y})^2", inline=True)]),
|
|
580
|
-
|
|
616
|
+
self.answers["base_loss"]
|
|
581
617
|
])
|
|
582
618
|
|
|
619
|
+
answers.append(self.answers["l2_penalty"])
|
|
583
620
|
table_data.append([
|
|
584
621
|
ContentAST.Paragraph(["L2 penalty: ", ContentAST.Equation(r"L_{reg} = (\lambda/2)\sum w_i^2", inline=True)]),
|
|
585
|
-
|
|
622
|
+
self.answers["l2_penalty"]
|
|
586
623
|
])
|
|
587
624
|
|
|
625
|
+
answers.append(self.answers["total_loss"])
|
|
588
626
|
table_data.append([
|
|
589
627
|
ContentAST.Paragraph(["Total loss: ", ContentAST.Equation(r"L_{total} = L_{base} + L_{reg}", inline=True)]),
|
|
590
|
-
|
|
628
|
+
self.answers["total_loss"]
|
|
591
629
|
])
|
|
592
630
|
|
|
631
|
+
answers.append(self.answers["grad_total_w0"])
|
|
593
632
|
table_data.append([
|
|
594
633
|
ContentAST.Paragraph(["Gradient: ", ContentAST.Equation(r"\frac{\partial L_{total}}{\partial w_0}", inline=True)]),
|
|
595
|
-
|
|
634
|
+
self.answers["grad_total_w0"]
|
|
596
635
|
])
|
|
597
636
|
|
|
598
637
|
body.add_element(ContentAST.Table(data=table_data))
|
|
599
638
|
|
|
639
|
+
return body, answers
|
|
640
|
+
|
|
641
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
642
|
+
"""Build question body (backward compatible interface)."""
|
|
643
|
+
body, _ = self._get_body(**kwargs)
|
|
600
644
|
return body
|
|
601
645
|
|
|
602
|
-
def
|
|
646
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
647
|
+
"""Build question explanation."""
|
|
603
648
|
explanation = ContentAST.Section()
|
|
604
649
|
|
|
605
650
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -608,7 +653,7 @@ class RegularizationCalculationQuestion(Question):
|
|
|
608
653
|
|
|
609
654
|
# Step 1: Forward pass
|
|
610
655
|
explanation.add_element(ContentAST.Paragraph([
|
|
611
|
-
"
|
|
656
|
+
ContentAST.Text("Step 1: Compute prediction", emphasis=True)
|
|
612
657
|
]))
|
|
613
658
|
|
|
614
659
|
terms = []
|
|
@@ -626,7 +671,7 @@ class RegularizationCalculationQuestion(Question):
|
|
|
626
671
|
|
|
627
672
|
# Step 2: Base loss
|
|
628
673
|
explanation.add_element(ContentAST.Paragraph([
|
|
629
|
-
"
|
|
674
|
+
ContentAST.Text("Step 2: Compute base MSE loss", emphasis=True)
|
|
630
675
|
]))
|
|
631
676
|
|
|
632
677
|
explanation.add_element(ContentAST.Equation(
|
|
@@ -636,7 +681,7 @@ class RegularizationCalculationQuestion(Question):
|
|
|
636
681
|
|
|
637
682
|
# Step 3: L2 penalty
|
|
638
683
|
explanation.add_element(ContentAST.Paragraph([
|
|
639
|
-
"
|
|
684
|
+
ContentAST.Text("Step 3: Compute L2 penalty", emphasis=True)
|
|
640
685
|
]))
|
|
641
686
|
|
|
642
687
|
weight_squares = [f"{w:.1f}^2" for w in self.weights]
|
|
@@ -649,7 +694,7 @@ class RegularizationCalculationQuestion(Question):
|
|
|
649
694
|
|
|
650
695
|
# Step 4: Total loss
|
|
651
696
|
explanation.add_element(ContentAST.Paragraph([
|
|
652
|
-
"
|
|
697
|
+
ContentAST.Text("Step 4: Compute total loss", emphasis=True)
|
|
653
698
|
]))
|
|
654
699
|
|
|
655
700
|
explanation.add_element(ContentAST.Equation(
|
|
@@ -659,7 +704,7 @@ class RegularizationCalculationQuestion(Question):
|
|
|
659
704
|
|
|
660
705
|
# Step 5: Gradient with regularization
|
|
661
706
|
explanation.add_element(ContentAST.Paragraph([
|
|
662
|
-
"
|
|
707
|
+
ContentAST.Text("Step 5: Compute gradient with regularization", emphasis=True)
|
|
663
708
|
]))
|
|
664
709
|
|
|
665
710
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -688,6 +733,11 @@ class RegularizationCalculationQuestion(Question):
|
|
|
688
733
|
" to the gradient, pushing the weight toward zero."
|
|
689
734
|
]))
|
|
690
735
|
|
|
736
|
+
return explanation, []
|
|
737
|
+
|
|
738
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
739
|
+
"""Build question explanation (backward compatible interface)."""
|
|
740
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
691
741
|
return explanation
|
|
692
742
|
|
|
693
743
|
|
|
@@ -767,17 +817,19 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
767
817
|
self.answers = {}
|
|
768
818
|
|
|
769
819
|
# New velocity
|
|
770
|
-
self.answers["velocity"] = Answer.vector_value("velocity", self.new_velocity)
|
|
820
|
+
self.answers["velocity"] = Answer.vector_value("velocity", self.new_velocity, label="New velocity")
|
|
771
821
|
|
|
772
822
|
# New weights with momentum
|
|
773
|
-
self.answers["weights_momentum"] = Answer.vector_value("weights_momentum", self.new_weights)
|
|
823
|
+
self.answers["weights_momentum"] = Answer.vector_value("weights_momentum", self.new_weights, label="Weights (momentum)")
|
|
774
824
|
|
|
775
825
|
# Vanilla SGD weights for comparison
|
|
776
826
|
if self.show_vanilla_sgd:
|
|
777
|
-
self.answers["weights_sgd"] = Answer.vector_value("weights_sgd", self.sgd_weights)
|
|
827
|
+
self.answers["weights_sgd"] = Answer.vector_value("weights_sgd", self.sgd_weights, label="Weights (vanilla SGD)")
|
|
778
828
|
|
|
779
|
-
def
|
|
829
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
830
|
+
"""Build question body and collect answers."""
|
|
780
831
|
body = ContentAST.Section()
|
|
832
|
+
answers = []
|
|
781
833
|
|
|
782
834
|
# Question description
|
|
783
835
|
body.add_element(ContentAST.Paragraph([
|
|
@@ -800,14 +852,14 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
800
852
|
|
|
801
853
|
# Current state
|
|
802
854
|
body.add_element(ContentAST.Paragraph([
|
|
803
|
-
"
|
|
855
|
+
ContentAST.Text("Current optimization state:", emphasis=True)
|
|
804
856
|
]))
|
|
805
857
|
|
|
806
858
|
body.add_element(ContentAST.Paragraph([
|
|
807
859
|
"Current weights: ",
|
|
808
860
|
ContentAST.Equation(f"{format_vector(self.current_weights)}", inline=True)
|
|
809
861
|
]))
|
|
810
|
-
|
|
862
|
+
|
|
811
863
|
body.add_element(ContentAST.Paragraph([
|
|
812
864
|
"Previous velocity: ",
|
|
813
865
|
ContentAST.Equation(f"{format_vector(self.prev_velocity)}", inline=True)
|
|
@@ -815,7 +867,7 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
815
867
|
|
|
816
868
|
# Hyperparameters
|
|
817
869
|
body.add_element(ContentAST.Paragraph([
|
|
818
|
-
"
|
|
870
|
+
ContentAST.Text("Hyperparameters:", emphasis=True)
|
|
819
871
|
]))
|
|
820
872
|
|
|
821
873
|
body.add_element(ContentAST.Paragraph([
|
|
@@ -837,30 +889,39 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
837
889
|
table_data = []
|
|
838
890
|
table_data.append(["Update Type", "Formula", "Result"])
|
|
839
891
|
|
|
892
|
+
answers.append(self.answers["velocity"])
|
|
840
893
|
table_data.append([
|
|
841
894
|
"New velocity",
|
|
842
895
|
ContentAST.Equation(r"v' = \beta v + (1-\beta)\nabla f", inline=True),
|
|
843
|
-
|
|
896
|
+
self.answers["velocity"]
|
|
844
897
|
])
|
|
845
898
|
|
|
899
|
+
answers.append(self.answers["weights_momentum"])
|
|
846
900
|
table_data.append([
|
|
847
901
|
"Weights (momentum)",
|
|
848
902
|
ContentAST.Equation(r"w' = w - \alpha v'", inline=True),
|
|
849
|
-
|
|
903
|
+
self.answers["weights_momentum"]
|
|
850
904
|
])
|
|
851
905
|
|
|
852
906
|
if self.show_vanilla_sgd:
|
|
907
|
+
answers.append(self.answers["weights_sgd"])
|
|
853
908
|
table_data.append([
|
|
854
909
|
"Weights (vanilla SGD)",
|
|
855
910
|
ContentAST.Equation(r"w' = w - \alpha \nabla f", inline=True),
|
|
856
|
-
|
|
911
|
+
self.answers["weights_sgd"]
|
|
857
912
|
])
|
|
858
913
|
|
|
859
914
|
body.add_element(ContentAST.Table(data=table_data))
|
|
860
915
|
|
|
916
|
+
return body, answers
|
|
917
|
+
|
|
918
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
919
|
+
"""Build question body (backward compatible interface)."""
|
|
920
|
+
body, _ = self._get_body(**kwargs)
|
|
861
921
|
return body
|
|
862
922
|
|
|
863
|
-
def
|
|
923
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
|
|
924
|
+
"""Build question explanation."""
|
|
864
925
|
explanation = ContentAST.Section()
|
|
865
926
|
|
|
866
927
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -870,7 +931,7 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
870
931
|
|
|
871
932
|
# Step 1: Calculate new velocity
|
|
872
933
|
explanation.add_element(ContentAST.Paragraph([
|
|
873
|
-
"
|
|
934
|
+
ContentAST.Text("Step 1: Update velocity using momentum", emphasis=True)
|
|
874
935
|
]))
|
|
875
936
|
|
|
876
937
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -883,17 +944,24 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
883
944
|
))
|
|
884
945
|
|
|
885
946
|
# Show calculation for each component
|
|
947
|
+
digits = Answer.DEFAULT_ROUNDING_DIGITS
|
|
886
948
|
for i in range(self.num_variables):
|
|
887
949
|
var_name = f"x_{i}"
|
|
950
|
+
# Round all intermediate values to avoid floating point precision issues
|
|
951
|
+
beta_times_v = round(self.momentum_beta * self.prev_velocity[i], digits)
|
|
952
|
+
one_minus_beta = round(1 - self.momentum_beta, digits)
|
|
953
|
+
one_minus_beta_times_grad = round((1 - self.momentum_beta) * self.gradients[i], digits)
|
|
954
|
+
|
|
888
955
|
explanation.add_element(ContentAST.Equation(
|
|
889
|
-
f"v'[{i}] = {self.momentum_beta} \\times {self.prev_velocity[i]:.
|
|
890
|
-
f"{
|
|
956
|
+
f"v'[{i}] = {self.momentum_beta} \\times {self.prev_velocity[i]:.{digits}f} + "
|
|
957
|
+
f"{one_minus_beta:.{digits}f} \\times {self.gradients[i]:.{digits}f} = "
|
|
958
|
+
f"{beta_times_v:.{digits}f} + {one_minus_beta_times_grad:.{digits}f} = {self.new_velocity[i]:.{digits}f}",
|
|
891
959
|
inline=False
|
|
892
960
|
))
|
|
893
961
|
|
|
894
962
|
# Step 2: Update weights with momentum
|
|
895
963
|
explanation.add_element(ContentAST.Paragraph([
|
|
896
|
-
"
|
|
964
|
+
ContentAST.Text("Step 2: Update weights using new velocity", emphasis=True)
|
|
897
965
|
]))
|
|
898
966
|
|
|
899
967
|
explanation.add_element(ContentAST.Equation(
|
|
@@ -910,7 +978,7 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
910
978
|
# Comparison with vanilla SGD
|
|
911
979
|
if self.show_vanilla_sgd:
|
|
912
980
|
explanation.add_element(ContentAST.Paragraph([
|
|
913
|
-
"
|
|
981
|
+
ContentAST.Text("Comparison with vanilla SGD:", emphasis=True)
|
|
914
982
|
]))
|
|
915
983
|
|
|
916
984
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -933,4 +1001,9 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
|
|
|
933
1001
|
"which can help accelerate learning and smooth out noisy gradients."
|
|
934
1002
|
]))
|
|
935
1003
|
|
|
1004
|
+
return explanation, []
|
|
1005
|
+
|
|
1006
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
1007
|
+
"""Build question explanation (backward compatible interface)."""
|
|
1008
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
936
1009
|
return explanation
|
QuizGenerator/question.py
CHANGED
|
@@ -29,6 +29,14 @@ import logging
|
|
|
29
29
|
log = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
@dataclasses.dataclass
|
|
33
|
+
class QuestionComponents:
|
|
34
|
+
"""Bundle of question parts generated during construction."""
|
|
35
|
+
body: ContentAST.Element
|
|
36
|
+
answers: List[Answer]
|
|
37
|
+
explanation: ContentAST.Element
|
|
38
|
+
|
|
39
|
+
|
|
32
40
|
# Spacing presets for questions
|
|
33
41
|
SPACING_PRESETS = {
|
|
34
42
|
"NONE": 0,
|
|
@@ -344,25 +352,31 @@ class Question(abc.ABC):
|
|
|
344
352
|
ensures consistent rendering across PDF/LaTeX and Canvas/HTML formats.
|
|
345
353
|
|
|
346
354
|
Required Methods:
|
|
347
|
-
-
|
|
348
|
-
-
|
|
355
|
+
- _get_body(): Return Tuple[ContentAST.Section, List[Answer]] with body and answers
|
|
356
|
+
- _get_explanation(): Return Tuple[ContentAST.Section, List[Answer]] with explanation
|
|
357
|
+
|
|
358
|
+
Note: get_body() and get_explanation() are provided for backward compatibility
|
|
359
|
+
and call the _get_* methods, returning just the first element of the tuple.
|
|
349
360
|
|
|
350
361
|
Required Class Attributes:
|
|
351
362
|
- VERSION (str): Question version number (e.g., "1.0")
|
|
352
363
|
Increment when RNG logic changes to ensure reproducibility
|
|
353
364
|
|
|
354
365
|
ContentAST Usage Examples:
|
|
355
|
-
def
|
|
366
|
+
def _get_body(self):
|
|
356
367
|
body = ContentAST.Section()
|
|
368
|
+
answers = []
|
|
357
369
|
body.add_element(ContentAST.Paragraph(["Calculate the matrix:"]))
|
|
358
370
|
|
|
359
371
|
# Use ContentAST.Matrix for math, NOT manual LaTeX
|
|
360
372
|
matrix_data = [[1, 2], [3, 4]]
|
|
361
373
|
body.add_element(ContentAST.Matrix(data=matrix_data, bracket_type="b"))
|
|
362
374
|
|
|
363
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
375
|
+
# Answer extends ContentAST.Leaf - add directly to body
|
|
376
|
+
ans = Answer.integer("result", 42, label="Result")
|
|
377
|
+
answers.append(ans)
|
|
378
|
+
body.add_element(ans)
|
|
379
|
+
return body, answers
|
|
366
380
|
|
|
367
381
|
Common ContentAST Elements:
|
|
368
382
|
- ContentAST.Paragraph: Text blocks
|
|
@@ -472,6 +486,9 @@ class Question(abc.ABC):
|
|
|
472
486
|
|
|
473
487
|
self.rng_seed_offset = kwargs.get("rng_seed_offset", 0)
|
|
474
488
|
|
|
489
|
+
# Component caching for unified Answer architecture
|
|
490
|
+
self._components: QuestionComponents = None
|
|
491
|
+
|
|
475
492
|
# To be used throughout when generating random things
|
|
476
493
|
self.rng = random.Random()
|
|
477
494
|
|
|
@@ -569,28 +586,105 @@ class Question(abc.ABC):
|
|
|
569
586
|
|
|
570
587
|
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
571
588
|
"""
|
|
572
|
-
Gets the body of the question during generation
|
|
589
|
+
Gets the body of the question during generation (backward compatible wrapper).
|
|
590
|
+
Calls _get_explanation() and returns just the explanation.
|
|
573
591
|
:param kwargs:
|
|
574
592
|
:return: (ContentAST.Section) Containing question explanation or None
|
|
575
593
|
"""
|
|
594
|
+
# Try new pattern first
|
|
595
|
+
if hasattr(self, '_get_explanation') and callable(getattr(self, '_get_explanation')):
|
|
596
|
+
explanation, _ = self._get_explanation()
|
|
597
|
+
return explanation
|
|
598
|
+
# Fallback: default explanation
|
|
576
599
|
return ContentAST.Section(
|
|
577
600
|
[ContentAST.Text("[Please reach out to your professor for clarification]")]
|
|
578
601
|
)
|
|
579
|
-
|
|
602
|
+
|
|
603
|
+
def _get_body(self) -> Tuple[ContentAST.Element, List[Answer]]:
|
|
604
|
+
"""
|
|
605
|
+
Build question body and collect answers (new pattern).
|
|
606
|
+
Questions should override this to return (body, answers) tuple.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Tuple of (body_ast, answers_list)
|
|
610
|
+
"""
|
|
611
|
+
# Fallback: call old get_body() and return empty answers
|
|
612
|
+
body = self.get_body()
|
|
613
|
+
return body, []
|
|
614
|
+
|
|
615
|
+
def _get_explanation(self) -> Tuple[ContentAST.Element, List[Answer]]:
|
|
616
|
+
"""
|
|
617
|
+
Build question explanation and collect answers (new pattern).
|
|
618
|
+
Questions can override this to include answers in explanations.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Tuple of (explanation_ast, answers_list)
|
|
622
|
+
"""
|
|
623
|
+
return ContentAST.Section(
|
|
624
|
+
[ContentAST.Text("[Please reach out to your professor for clarification]")]
|
|
625
|
+
), []
|
|
626
|
+
|
|
627
|
+
def build_question_components(self, **kwargs) -> QuestionComponents:
|
|
628
|
+
"""
|
|
629
|
+
Build question components (body, answers, explanation) in single pass.
|
|
630
|
+
|
|
631
|
+
Calls _get_body() and _get_explanation() which return tuples of
|
|
632
|
+
(content, answers).
|
|
633
|
+
"""
|
|
634
|
+
# Build body with its answers
|
|
635
|
+
body, body_answers = self._get_body()
|
|
636
|
+
|
|
637
|
+
# Build explanation with its answers
|
|
638
|
+
explanation, explanation_answers = self._get_explanation()
|
|
639
|
+
|
|
640
|
+
# Combine all answers
|
|
641
|
+
all_answers = body_answers + explanation_answers
|
|
642
|
+
|
|
643
|
+
return QuestionComponents(
|
|
644
|
+
body=body,
|
|
645
|
+
answers=all_answers,
|
|
646
|
+
explanation=explanation
|
|
647
|
+
)
|
|
648
|
+
|
|
580
649
|
def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
|
|
581
|
-
|
|
650
|
+
"""
|
|
651
|
+
Return answers from cached components (new pattern) or self.answers dict (old pattern).
|
|
652
|
+
"""
|
|
653
|
+
# Try component-based approach first (new pattern)
|
|
654
|
+
if self._components is None:
|
|
655
|
+
try:
|
|
656
|
+
self._components = self.build_question_components()
|
|
657
|
+
except Exception as e:
|
|
658
|
+
# If component building fails, fall back to dict
|
|
659
|
+
log.debug(f"Failed to build question components: {e}, falling back to dict")
|
|
660
|
+
pass
|
|
661
|
+
|
|
662
|
+
# Use components if available and non-empty
|
|
663
|
+
if self._components is not None and len(self._components.answers) > 0:
|
|
664
|
+
answers = self._components.answers
|
|
665
|
+
if self.can_be_numerical():
|
|
666
|
+
return (
|
|
667
|
+
Answer.AnswerKind.NUMERICAL_QUESTION,
|
|
668
|
+
list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in answers]))
|
|
669
|
+
)
|
|
582
670
|
return (
|
|
583
|
-
|
|
584
|
-
list(itertools.chain(*[a.get_for_canvas(
|
|
671
|
+
self.answer_kind,
|
|
672
|
+
list(itertools.chain(*[a.get_for_canvas() for a in answers]))
|
|
585
673
|
)
|
|
586
|
-
|
|
674
|
+
|
|
675
|
+
# Fall back to dict pattern (old pattern)
|
|
676
|
+
if len(self.answers.values()) > 0:
|
|
677
|
+
if self.can_be_numerical():
|
|
678
|
+
return (
|
|
679
|
+
Answer.AnswerKind.NUMERICAL_QUESTION,
|
|
680
|
+
list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
|
|
681
|
+
)
|
|
587
682
|
return (
|
|
588
683
|
self.answer_kind,
|
|
589
684
|
list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
|
|
590
685
|
)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
)
|
|
686
|
+
|
|
687
|
+
return (Answer.AnswerKind.ESSAY, [])
|
|
594
688
|
|
|
595
689
|
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
596
690
|
"""If it is necessary to regenerate aspects between usages, this is the time to do it.
|
|
@@ -601,6 +695,7 @@ class Question(abc.ABC):
|
|
|
601
695
|
:return: bool - True if the generated question is interesting, False otherwise
|
|
602
696
|
"""
|
|
603
697
|
self.answers = {}
|
|
698
|
+
self._components = None # Clear component cache
|
|
604
699
|
# Seed the RNG directly with the provided seed (no offset)
|
|
605
700
|
self.rng.seed(rng_seed)
|
|
606
701
|
# Note: We don't call is_interesting() here because child classes need to
|