QuizGenerator 0.7.1__py3-none-any.whl → 0.8.1__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.
Files changed (30) hide show
  1. QuizGenerator/contentast.py +48 -15
  2. QuizGenerator/generate.py +2 -1
  3. QuizGenerator/mixins.py +14 -100
  4. QuizGenerator/premade_questions/basic.py +24 -29
  5. QuizGenerator/premade_questions/cst334/languages.py +100 -99
  6. QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
  9. QuizGenerator/premade_questions/cst334/process.py +312 -328
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -521
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
  15. QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
  17. QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
  18. QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
  19. QuizGenerator/premade_questions/cst463/models/text.py +65 -67
  20. QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
  21. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
  22. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
  23. QuizGenerator/question.py +310 -202
  24. QuizGenerator/quiz.py +8 -5
  25. QuizGenerator/regenerate.py +14 -6
  26. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/METADATA +30 -2
  27. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/RECORD +30 -30
  28. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/WHEEL +0 -0
  29. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/entry_points.txt +0 -0
  30. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,7 @@
1
1
  import abc
2
2
  import logging
3
3
  import math
4
+ import random
4
5
  import keras
5
6
  import numpy as np
6
7
  from typing import List, Tuple
@@ -13,8 +14,9 @@ log = logging.getLogger(__name__)
13
14
 
14
15
 
15
16
  class WeightCounting(Question, abc.ABC):
17
+ @staticmethod
16
18
  @abc.abstractmethod
17
- def get_model(self) -> keras.Model:
19
+ def get_model(rng: random.Random) -> keras.Model:
18
20
  pass
19
21
 
20
22
  @staticmethod
@@ -71,25 +73,30 @@ class WeightCounting(Question, abc.ABC):
71
73
  lines.append("])")
72
74
  return "\n".join(lines)
73
75
 
74
- def refresh(self, *args, **kwargs):
75
- super().refresh(*args, **kwargs)
76
-
76
+ @classmethod
77
+ def _build_context(cls, *, rng_seed=None, **kwargs):
78
+ rng = random.Random(rng_seed)
79
+
77
80
  refresh_success = False
78
81
  while not refresh_success:
79
82
  try:
80
- self.model, self.fields = self.get_model()
83
+ model, fields = cls.get_model(rng)
81
84
  refresh_success = True
82
85
  except ValueError as e:
83
86
  log.error(e)
84
- log.info(f"Regenerating {self.__class__.__name__} due to improper configuration")
87
+ log.info(f"Regenerating {cls.__name__} due to improper configuration")
85
88
  continue
86
-
87
- self.num_parameters = self.model.count_params()
88
- self.answers["num_parameters"] = ca.AnswerTypes.Int(self.num_parameters, label="Number of Parameters")
89
-
90
- return True
91
-
92
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
89
+
90
+ num_parameters = model.count_params()
91
+
92
+ return {
93
+ "model": model,
94
+ "fields": fields,
95
+ "num_parameters": num_parameters,
96
+ }
97
+
98
+ @classmethod
99
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
93
100
  """Build question body and collect answers."""
94
101
  body = ca.Section()
95
102
  answers = []
@@ -104,26 +111,23 @@ class WeightCounting(Question, abc.ABC):
104
111
 
105
112
  body.add_element(
106
113
  ca.Code(
107
- self.model_to_python(
108
- self.model,
109
- fields=self.fields
114
+ cls.model_to_python(
115
+ context["model"],
116
+ fields=context["fields"]
110
117
  )
111
118
  )
112
119
  )
113
120
 
114
121
  body.add_element(ca.LineBreak())
115
122
 
116
- answers.append(self.answers["num_parameters"])
117
- body.add_element(self.answers["num_parameters"])
123
+ num_parameters_answer = ca.AnswerTypes.Int(context["num_parameters"], label="Number of Parameters")
124
+ answers.append(num_parameters_answer)
125
+ body.add_element(num_parameters_answer)
118
126
 
119
127
  return body, answers
120
128
 
121
- def get_body(self, **kwargs) -> ca.Section:
122
- """Build question body (backward compatible interface)."""
123
- body, _ = self._get_body(**kwargs)
124
- return body
125
-
126
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
129
+ @classmethod
130
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
127
131
  """Build question explanation."""
128
132
  explanation = ca.Section()
129
133
 
@@ -159,31 +163,27 @@ class WeightCounting(Question, abc.ABC):
159
163
 
160
164
 
161
165
  summary_lines = []
162
- self.model.summary(print_fn=lambda x: summary_lines.append(x))
166
+ context["model"].summary(print_fn=lambda x: summary_lines.append(x))
163
167
  explanation.add_element(
164
168
  # ca.Text('\n'.join(summary_lines))
165
- markdown_summary(self.model)
169
+ markdown_summary(context["model"])
166
170
  )
167
171
 
168
172
  return explanation, []
169
173
 
170
- def get_explanation(self, **kwargs) -> ca.Section:
171
- """Build question explanation (backward compatible interface)."""
172
- explanation, _ = self._get_explanation(**kwargs)
173
- return explanation
174
-
175
174
 
176
175
  @QuestionRegistry.register("cst463.WeightCounting-CNN")
177
176
  class WeightCounting_CNN(WeightCounting):
178
177
 
179
- def get_model(self) -> tuple[keras.Model, list[str]]:
180
- input_size = self.rng.choice(np.arange(28, 32))
181
- cnn_num_filters = self.rng.choice(2 ** np.arange(8))
182
- cnn_kernel_size = self.rng.choice(1 + np.arange(10))
183
- cnn_strides = self.rng.choice(1 + np.arange(10))
184
- pool_size = self.rng.choice(1 + np.arange(10))
185
- pool_strides = self.rng.choice(1 + np.arange(10))
186
- num_output_size = self.rng.choice([1, 10, 32, 100])
178
+ @staticmethod
179
+ def get_model(rng: random.Random) -> tuple[keras.Model, list[str]]:
180
+ input_size = rng.choice(np.arange(28, 32))
181
+ cnn_num_filters = rng.choice(2 ** np.arange(8))
182
+ cnn_kernel_size = rng.choice(1 + np.arange(10))
183
+ cnn_strides = rng.choice(1 + np.arange(10))
184
+ pool_size = rng.choice(1 + np.arange(10))
185
+ pool_strides = rng.choice(1 + np.arange(10))
186
+ num_output_size = rng.choice([1, 10, 32, 100])
187
187
 
188
188
  # Let's just make a small model
189
189
  model = keras.models.Sequential(
@@ -209,15 +209,16 @@ class WeightCounting_CNN(WeightCounting):
209
209
 
210
210
  @QuestionRegistry.register("cst463.WeightCounting-RNN")
211
211
  class WeightCounting_RNN(WeightCounting):
212
- def get_model(self) -> tuple[keras.Model, list[str]]:
213
- timesteps = int(self.rng.choice(np.arange(20, 41)))
214
- feature_size = int(self.rng.choice(np.arange(8, 65)))
212
+ @staticmethod
213
+ def get_model(rng: random.Random) -> tuple[keras.Model, list[str]]:
214
+ timesteps = int(rng.choice(np.arange(20, 41)))
215
+ feature_size = int(rng.choice(np.arange(8, 65)))
215
216
 
216
- rnn_units = int(self.rng.choice(2 ** np.arange(4, 9)))
217
- rnn_type = self.rng.choice(["SimpleRNN"])
218
- return_sequences = bool(self.rng.choice([True, False]))
217
+ rnn_units = int(rng.choice(2 ** np.arange(4, 9)))
218
+ rnn_type = rng.choice(["SimpleRNN"])
219
+ return_sequences = bool(rng.choice([True, False]))
219
220
 
220
- num_output_size = int(self.rng.choice([1, 10, 32, 100]))
221
+ num_output_size = int(rng.choice([1, 10, 32, 100]))
221
222
 
222
223
  RNNLayer = getattr(keras.layers, rnn_type)
223
224
 
@@ -7,7 +7,7 @@ import math
7
7
  import numpy as np
8
8
  import uuid
9
9
  import os
10
- from typing import List, Tuple, Dict, Any
10
+ from typing import List, Tuple
11
11
 
12
12
  import matplotlib.pyplot as plt
13
13
  import matplotlib.patches as mpatches
@@ -73,37 +73,60 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
73
73
  self.da2_dz2 = None # Gradient of activation w.r.t. pre-activation
74
74
  self.dL_dz2 = None # Gradient of loss w.r.t. output pre-activation
75
75
 
76
+ def _build_context(self, *, rng_seed=None, **kwargs):
77
+ if "num_inputs" in kwargs:
78
+ self.num_inputs = kwargs.get("num_inputs", self.num_inputs)
79
+ if "num_hidden" in kwargs:
80
+ self.num_hidden = kwargs.get("num_hidden", self.num_hidden)
81
+ if "num_outputs" in kwargs:
82
+ self.num_outputs = kwargs.get("num_outputs", self.num_outputs)
83
+ if "use_bias" in kwargs:
84
+ self.use_bias = kwargs.get("use_bias", self.use_bias)
85
+ if "param_digits" in kwargs:
86
+ self.param_digits = kwargs.get("param_digits", self.param_digits)
87
+
88
+ self.rng.seed(rng_seed)
89
+ self._np_rng = np.random.RandomState(rng_seed)
90
+
91
+ context = dict(kwargs)
92
+ context["rng_seed"] = rng_seed
93
+ return context
94
+
76
95
  def _generate_network(self, weight_range=(-2, 2), input_range=(-3, 3)):
77
96
  """Generate random network parameters and input."""
78
97
  # Generate weights using MatrixQuestion's rounded matrix method
79
98
  # Use param_digits to match display precision in tables and explanations
80
99
  self.W1 = self.get_rounded_matrix(
100
+ self._np_rng,
81
101
  (self.num_hidden, self.num_inputs),
82
- low=weight_range[0],
83
- high=weight_range[1],
84
- digits_to_round=self.param_digits
102
+ weight_range[0],
103
+ weight_range[1],
104
+ self.param_digits
85
105
  )
86
106
 
87
107
  self.W2 = self.get_rounded_matrix(
108
+ self._np_rng,
88
109
  (self.num_outputs, self.num_hidden),
89
- low=weight_range[0],
90
- high=weight_range[1],
91
- digits_to_round=self.param_digits
110
+ weight_range[0],
111
+ weight_range[1],
112
+ self.param_digits
92
113
  )
93
114
 
94
115
  # Generate biases
95
116
  if self.use_bias:
96
117
  self.b1 = self.get_rounded_matrix(
118
+ self._np_rng,
97
119
  (self.num_hidden,),
98
- low=weight_range[0],
99
- high=weight_range[1],
100
- digits_to_round=self.param_digits
120
+ weight_range[0],
121
+ weight_range[1],
122
+ self.param_digits
101
123
  )
102
124
  self.b2 = self.get_rounded_matrix(
125
+ self._np_rng,
103
126
  (self.num_outputs,),
104
- low=weight_range[0],
105
- high=weight_range[1],
106
- digits_to_round=self.param_digits
127
+ weight_range[0],
128
+ weight_range[1],
129
+ self.param_digits
107
130
  )
108
131
  else:
109
132
  self.b1 = np.zeros(self.num_hidden)
@@ -111,10 +134,11 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
111
134
 
112
135
  # Generate input values (keep as integers for simplicity)
113
136
  self.X = self.get_rounded_matrix(
137
+ self._np_rng,
114
138
  (self.num_inputs,),
115
- low=input_range[0],
116
- high=input_range[1],
117
- digits_to_round=0 # Round to integers
139
+ input_range[0],
140
+ input_range[1],
141
+ 0 # Round to integers
118
142
  )
119
143
 
120
144
  def _select_activation_function(self):
@@ -546,8 +570,8 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
546
570
  - Final output (ŷ)
547
571
  """
548
572
 
549
- def refresh(self, rng_seed=None, *args, **kwargs):
550
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
573
+ def _build_context(self, *, rng_seed=None, **kwargs):
574
+ super()._build_context(rng_seed=rng_seed, **kwargs)
551
575
 
552
576
  # Generate network
553
577
  self._generate_network()
@@ -556,22 +580,11 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
556
580
  # Run forward pass to get correct answers
557
581
  self._forward_pass()
558
582
 
559
- # Create answer fields
560
- self._create_answers()
561
-
562
- def _create_answers(self):
563
- """Create answer fields for forward pass values."""
564
- self.answers = {}
565
-
566
- # Hidden layer activations
567
- for i in range(self.num_hidden):
568
- key = f"h{i+1}"
569
- self.answers[key] = ca.AnswerTypes.Float(float(self.a1[i]), label=f"h_{i + 1}")
570
-
571
- # Output
572
- self.answers["y_pred"] = ca.AnswerTypes.Float(float(self.a2[0]), label="ŷ")
583
+ context = dict(kwargs)
584
+ context["rng_seed"] = rng_seed
585
+ return context
573
586
 
574
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
587
+ def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
575
588
  """Build question body and collect answers."""
576
589
  body = ca.Section()
577
590
  answers = []
@@ -599,22 +612,16 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
599
612
  f"**Hidden layer activation:** {self._get_activation_name()}"
600
613
  ]))
601
614
 
602
- # Collect answers
603
615
  for i in range(self.num_hidden):
604
- answers.append(self.answers[f"h{i+1}"])
616
+ answers.append(ca.AnswerTypes.Float(float(self.a1[i]), label=f"h_{i + 1}"))
605
617
 
606
- answers.append(self.answers["y_pred"])
618
+ answers.append(ca.AnswerTypes.Float(float(self.a2[0]), label="ŷ"))
607
619
 
608
620
  body.add_element(ca.AnswerBlock(answers))
609
621
 
610
622
  return body, answers
611
623
 
612
- def get_body(self, **kwargs) -> ca.Section:
613
- """Build question body (backward compatible interface)."""
614
- body, _ = self._get_body(**kwargs)
615
- return body
616
-
617
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
624
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
618
625
  """Build question explanation."""
619
626
  explanation = ca.Section()
620
627
 
@@ -694,11 +701,6 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
694
701
 
695
702
  return explanation, []
696
703
 
697
- def get_explanation(self, **kwargs) -> ca.Section:
698
- """Build question explanation (backward compatible interface)."""
699
- explanation, _ = self._get_explanation(**kwargs)
700
- return explanation
701
-
702
704
 
703
705
  @QuestionRegistry.register()
704
706
  class BackpropGradientQuestion(SimpleNeuralNetworkBase):
@@ -709,8 +711,8 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
709
711
  - Gradients for multiple specific weights (∂L/∂w)
710
712
  """
711
713
 
712
- def refresh(self, rng_seed=None, *args, **kwargs):
713
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
714
+ def _build_context(self, *, rng_seed=None, **kwargs):
715
+ super()._build_context(rng_seed=rng_seed, **kwargs)
714
716
 
715
717
  # Generate network
716
718
  self._generate_network()
@@ -730,27 +732,11 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
730
732
  self.loss = round(self.loss, 4)
731
733
  self._compute_output_gradient()
732
734
 
733
- # Create answer fields for specific weight gradients
734
- self._create_answers()
735
-
736
- def _create_answers(self):
737
- """Create answer fields for weight gradients."""
738
- self.answers = {}
739
-
740
- # Ask for gradients of 2-3 weights
741
- # Include at least one from each layer
735
+ context = dict(kwargs)
736
+ context["rng_seed"] = rng_seed
737
+ return context
742
738
 
743
- # Gradient for W2 (hidden to output)
744
- for i in range(self.num_hidden):
745
- key = f"dL_dw2_{i}"
746
- self.answers[key] = ca.AnswerTypes.Float(self._compute_gradient_W2(i), label=f"∂L/∂w_{i + 3}")
747
-
748
- # Gradient for W1 (input to hidden) - pick first hidden neuron
749
- for j in range(self.num_inputs):
750
- key = f"dL_dw1_0{j}"
751
- self.answers[key] = ca.AnswerTypes.Float(self._compute_gradient_W1(0, j), label=f"∂L/∂w_1{j + 1}")
752
-
753
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
739
+ def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
754
740
  """Build question body and collect answers."""
755
741
  body = ca.Section()
756
742
  answers = []
@@ -783,24 +769,23 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
783
769
  "**Calculate the following gradients:**"
784
770
  ]))
785
771
 
786
- # Collect W2 gradient answers
787
772
  for i in range(self.num_hidden):
788
- answers.append(self.answers[f"dL_dw2_{i}"])
773
+ answers.append(ca.AnswerTypes.Float(
774
+ self._compute_gradient_W2(i),
775
+ label=f"∂L/∂w_{i + 3}"
776
+ ))
789
777
 
790
- # Collect W1 gradient answers (first hidden neuron)
791
778
  for j in range(self.num_inputs):
792
- answers.append(self.answers[f"dL_dw1_0{j}"])
779
+ answers.append(ca.AnswerTypes.Float(
780
+ self._compute_gradient_W1(0, j),
781
+ label=f"∂L/∂w_1{j + 1}"
782
+ ))
793
783
 
794
784
  body.add_element(ca.AnswerBlock(answers))
795
785
 
796
786
  return body, answers
797
787
 
798
- def get_body(self, **kwargs) -> ca.Section:
799
- """Build question body (backward compatible interface)."""
800
- body, _ = self._get_body(**kwargs)
801
- return body
802
-
803
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
788
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
804
789
  """Build question explanation."""
805
790
  explanation = ca.Section()
806
791
 
@@ -874,11 +859,6 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
874
859
 
875
860
  return explanation, []
876
861
 
877
- def get_explanation(self, **kwargs) -> ca.Section:
878
- """Build question explanation (backward compatible interface)."""
879
- explanation, _ = self._get_explanation(**kwargs)
880
- return explanation
881
-
882
862
 
883
863
  @QuestionRegistry.register()
884
864
  class EnsembleAveragingQuestion(Question):
@@ -897,8 +877,11 @@ class EnsembleAveragingQuestion(Question):
897
877
  self.num_models = kwargs.get("num_models", 5)
898
878
  self.predictions = None
899
879
 
900
- def refresh(self, rng_seed=None, *args, **kwargs):
901
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
880
+ def _build_context(self, *, rng_seed=None, **kwargs):
881
+ if "num_models" in kwargs:
882
+ self.num_models = kwargs.get("num_models", self.num_models)
883
+
884
+ self.rng.seed(rng_seed)
902
885
 
903
886
  # Generate predictions from multiple models
904
887
  # Use a range that makes sense for typical regression problems
@@ -911,22 +894,11 @@ class EnsembleAveragingQuestion(Question):
911
894
  # Round to make calculations easier
912
895
  self.predictions = [round(p, 1) for p in self.predictions]
913
896
 
914
- # Create answers
915
- self._create_answers()
897
+ context = dict(kwargs)
898
+ context["rng_seed"] = rng_seed
899
+ return context
916
900
 
917
- def _create_answers(self):
918
- """Create answer fields for ensemble statistics."""
919
- self.answers = {}
920
-
921
- # Mean prediction
922
- mean_pred = np.mean(self.predictions)
923
- self.answers["mean"] = ca.AnswerTypes.Float(float(mean_pred), label="Mean (average)")
924
-
925
- # Median (optional, but useful)
926
- median_pred = np.median(self.predictions)
927
- self.answers["median"] = ca.AnswerTypes.Float(float(median_pred), label="Median")
928
-
929
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
901
+ def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
930
902
  """Build question body and collect answers."""
931
903
  body = ca.Section()
932
904
  answers = []
@@ -948,20 +920,16 @@ class EnsembleAveragingQuestion(Question):
948
920
  "To create an ensemble, calculate the combined prediction using the following methods:"
949
921
  ]))
950
922
 
951
- # Collect answers
952
- answers.append(self.answers["mean"])
953
- answers.append(self.answers["median"])
923
+ mean_pred = np.mean(self.predictions)
924
+ median_pred = np.median(self.predictions)
925
+ answers.append(ca.AnswerTypes.Float(float(mean_pred), label="Mean (average)"))
926
+ answers.append(ca.AnswerTypes.Float(float(median_pred), label="Median"))
954
927
 
955
928
  body.add_element(ca.AnswerBlock(answers))
956
929
 
957
930
  return body, answers
958
931
 
959
- def get_body(self, **kwargs) -> ca.Section:
960
- """Build question body (backward compatible interface)."""
961
- body, _ = self._get_body(**kwargs)
962
- return body
963
-
964
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
932
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
965
933
  """Build question explanation."""
966
934
  explanation = ca.Section()
967
935
 
@@ -1010,11 +978,6 @@ class EnsembleAveragingQuestion(Question):
1010
978
 
1011
979
  return explanation, []
1012
980
 
1013
- def get_explanation(self, **kwargs) -> ca.Section:
1014
- """Build question explanation (backward compatible interface)."""
1015
- explanation, _ = self._get_explanation(**kwargs)
1016
- return explanation
1017
-
1018
981
 
1019
982
  @QuestionRegistry.register()
1020
983
  class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
@@ -1034,8 +997,8 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1034
997
  self.new_W1 = None
1035
998
  self.new_W2 = None
1036
999
 
1037
- def refresh(self, rng_seed=None, *args, **kwargs):
1038
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
1000
+ def _build_context(self, *, rng_seed=None, **kwargs):
1001
+ super()._build_context(rng_seed=rng_seed, **kwargs)
1039
1002
 
1040
1003
  # Generate network
1041
1004
  self._generate_network()
@@ -1061,8 +1024,9 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1061
1024
  # Compute updated weights
1062
1025
  self._compute_weight_updates()
1063
1026
 
1064
- # Create answers
1065
- self._create_answers()
1027
+ context = dict(kwargs)
1028
+ context["rng_seed"] = rng_seed
1029
+ return context
1066
1030
 
1067
1031
  def _compute_weight_updates(self):
1068
1032
  """Compute new weights after gradient descent step."""
@@ -1078,25 +1042,7 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1078
1042
  grad = self._compute_gradient_W1(0, j)
1079
1043
  self.new_W1[0, j] = self.W1[0, j] - self.learning_rate * grad
1080
1044
 
1081
- def _create_answers(self):
1082
- """Create answer fields for all steps."""
1083
- self.answers = {}
1084
-
1085
- # Forward pass answers
1086
- self.answers["y_pred"] = ca.AnswerTypes.Float(float(self.a2[0]), label="1. Forward Pass - Network output ŷ")
1087
-
1088
- # Loss answer
1089
- self.answers["loss"] = ca.AnswerTypes.Float(float(self.loss), label="2. Loss")
1090
-
1091
- # Gradient answers (for key weights)
1092
- self.answers["grad_w3"] = ca.AnswerTypes.Float(self._compute_gradient_W2(0), label="3. Gradient ∂L/∂w₃")
1093
- self.answers["grad_w11"] = ca.AnswerTypes.Float(self._compute_gradient_W1(0, 0), label="4. Gradient ∂L/∂w₁₁")
1094
-
1095
- # Updated weight answers
1096
- self.answers["new_w3"] = ca.AnswerTypes.Float(float(self.new_W2[0, 0]), label="5. Updated w₃:")
1097
- self.answers["new_w11"] = ca.AnswerTypes.Float(float(self.new_W1[0, 0]), label="6. Updated w₁₁:")
1098
-
1099
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
1045
+ def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
1100
1046
  """Build question body and collect answers."""
1101
1047
  body = ca.Section()
1102
1048
  answers = []
@@ -1145,24 +1091,27 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1145
1091
  # Network parameters table
1146
1092
  body.add_element(self._generate_parameter_table(include_activations=False))
1147
1093
 
1148
- # Collect answers
1149
- answers.append(self.answers["y_pred"])
1150
- answers.append(self.answers["loss"])
1151
- answers.append(self.answers["grad_w3"])
1152
- answers.append(self.answers["grad_w11"])
1153
- answers.append(self.answers["new_w3"])
1154
- answers.append(self.answers["new_w11"])
1094
+ answers.append(ca.AnswerTypes.Float(
1095
+ float(self.a2[0]),
1096
+ label="1. Forward Pass - Network output ŷ"
1097
+ ))
1098
+ answers.append(ca.AnswerTypes.Float(float(self.loss), label="2. Loss"))
1099
+ answers.append(ca.AnswerTypes.Float(
1100
+ self._compute_gradient_W2(0),
1101
+ label="3. Gradient ∂L/∂w₃"
1102
+ ))
1103
+ answers.append(ca.AnswerTypes.Float(
1104
+ self._compute_gradient_W1(0, 0),
1105
+ label="4. Gradient ∂L/∂w₁₁"
1106
+ ))
1107
+ answers.append(ca.AnswerTypes.Float(float(self.new_W2[0, 0]), label="5. Updated w₃:"))
1108
+ answers.append(ca.AnswerTypes.Float(float(self.new_W1[0, 0]), label="6. Updated w₁₁:"))
1155
1109
 
1156
1110
  body.add_element(ca.AnswerBlock(answers))
1157
1111
 
1158
1112
  return body, answers
1159
1113
 
1160
- def get_body(self, **kwargs) -> ca.Section:
1161
- """Build question body (backward compatible interface)."""
1162
- body, _ = self._get_body(**kwargs)
1163
- return body
1164
-
1165
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
1114
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
1166
1115
  """Build question explanation."""
1167
1116
  explanation = ca.Section()
1168
1117
 
@@ -1287,8 +1236,3 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1287
1236
  ]))
1288
1237
 
1289
1238
  return explanation, []
1290
-
1291
- def get_explanation(self, **kwargs) -> ca.Section:
1292
- """Build question explanation (backward compatible interface)."""
1293
- explanation, _ = self._get_explanation(**kwargs)
1294
- return explanation