QuizGenerator 0.1.4__py3-none-any.whl → 0.3.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.
@@ -15,11 +15,12 @@ import matplotlib.patches as mpatches
15
15
  from QuizGenerator.contentast import ContentAST
16
16
  from QuizGenerator.question import Question, Answer, QuestionRegistry
17
17
  from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
18
+ from ..models.matrices import MatrixQuestion
18
19
 
19
20
  log = logging.getLogger(__name__)
20
21
 
21
22
 
22
- class SimpleNeuralNetworkBase(Question, abc.ABC):
23
+ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
23
24
  """
24
25
  Base class for simple neural network questions.
25
26
 
@@ -73,45 +74,42 @@ class SimpleNeuralNetworkBase(Question, abc.ABC):
73
74
 
74
75
  def _generate_network(self, weight_range=(-2, 2), input_range=(-3, 3)):
75
76
  """Generate random network parameters and input."""
76
- # Generate weights (using small values for numerical stability)
77
- self.W1 = np.array([
78
- [self.rng.uniform(weight_range[0], weight_range[1])
79
- for _ in range(self.num_inputs)]
80
- for _ in range(self.num_hidden)
81
- ])
82
-
83
- self.W2 = np.array([
84
- [self.rng.uniform(weight_range[0], weight_range[1])
85
- for _ in range(self.num_hidden)]
86
- for _ in range(self.num_outputs)
87
- ])
77
+ # Generate weights using MatrixQuestion's rounded matrix method
78
+ self.W1 = self.get_rounded_matrix(
79
+ (self.num_hidden, self.num_inputs),
80
+ low=weight_range[0],
81
+ high=weight_range[1]
82
+ )
83
+
84
+ self.W2 = self.get_rounded_matrix(
85
+ (self.num_outputs, self.num_hidden),
86
+ low=weight_range[0],
87
+ high=weight_range[1]
88
+ )
88
89
 
89
90
  # Generate biases
90
91
  if self.use_bias:
91
- self.b1 = np.array([
92
- self.rng.uniform(weight_range[0], weight_range[1])
93
- for _ in range(self.num_hidden)
94
- ])
95
- self.b2 = np.array([
96
- self.rng.uniform(weight_range[0], weight_range[1])
97
- for _ in range(self.num_outputs)
98
- ])
92
+ self.b1 = self.get_rounded_matrix(
93
+ (self.num_hidden,),
94
+ low=weight_range[0],
95
+ high=weight_range[1]
96
+ )
97
+ self.b2 = self.get_rounded_matrix(
98
+ (self.num_outputs,),
99
+ low=weight_range[0],
100
+ high=weight_range[1]
101
+ )
99
102
  else:
100
103
  self.b1 = np.zeros(self.num_hidden)
101
104
  self.b2 = np.zeros(self.num_outputs)
102
105
 
103
- # Round weights to make calculations cleaner
104
- self.W1 = np.round(self.W1 * 2) / 2 # Round to nearest 0.5
105
- self.W2 = np.round(self.W2 * 2) / 2
106
- self.b1 = np.round(self.b1 * 2) / 2
107
- self.b2 = np.round(self.b2 * 2) / 2
108
-
109
- # Generate input values
110
- self.X = np.array([
111
- self.rng.uniform(input_range[0], input_range[1])
112
- for _ in range(self.num_inputs)
113
- ])
114
- self.X = np.round(self.X) # Use integer inputs for simplicity
106
+ # Generate input values (keep as integers for simplicity)
107
+ self.X = self.get_rounded_matrix(
108
+ (self.num_inputs,),
109
+ low=input_range[0],
110
+ high=input_range[1],
111
+ digits_to_round=0 # Round to integers
112
+ )
115
113
 
116
114
  def _select_activation_function(self):
117
115
  """Randomly select an activation function."""
@@ -158,7 +156,7 @@ class SimpleNeuralNetworkBase(Question, abc.ABC):
158
156
 
159
157
  # Output layer
160
158
  self.z2 = self.W2 @ self.a1 + self.b2
161
- self.a2 = self._apply_activation(self.z2, self.ACTIVATION_LINEAR) # Linear output
159
+ self.a2 = self._apply_activation(self.z2, self.ACTIVATION_SIGMOID) # Sigmoid output for binary classification
162
160
 
163
161
  # Round all computed values to display precision to ensure students can reproduce calculations
164
162
  # We display z and a values with 4 decimal places
@@ -170,21 +168,34 @@ class SimpleNeuralNetworkBase(Question, abc.ABC):
170
168
  return self.a2
171
169
 
172
170
  def _compute_loss(self, y_target):
173
- """Compute MSE loss."""
171
+ """Compute binary cross-entropy loss."""
174
172
  self.y_target = y_target
175
- self.loss = 0.5 * (y_target - self.a2[0]) ** 2
173
+ # BCE: L = -[y log(ŷ) + (1-y) log(1-ŷ)]
174
+ # Add small epsilon to prevent log(0)
175
+ epsilon = 1e-15
176
+ y_pred = np.clip(self.a2[0], epsilon, 1 - epsilon)
177
+ self.loss = -(y_target * np.log(y_pred) + (1 - y_target) * np.log(1 - y_pred))
176
178
  return self.loss
177
179
 
178
180
  def _compute_output_gradient(self):
179
181
  """Compute gradient of loss w.r.t. output."""
180
- # For MSE loss: dL/da2 = -(y - a2)
181
- self.dL_da2 = -(self.y_target - self.a2[0])
182
-
183
- # For linear output activation: da2/dz2 = 1
184
- self.da2_dz2 = 1.0
185
-
186
- # Chain rule: dL/dz2 = dL/da2 * da2/dz2
187
- self.dL_dz2 = self.dL_da2 * self.da2_dz2
182
+ # For BCE loss with sigmoid activation, the gradient simplifies beautifully:
183
+ # dL/dz2 = ŷ - y (this is the combined derivative of BCE loss and sigmoid activation)
184
+ #
185
+ # Derivation:
186
+ # BCE: L = -[y log(ŷ) + (1-y) log(1-ŷ)]
187
+ # dL/dŷ = -[y/ŷ - (1-y)/(1-ŷ)]
188
+ # Sigmoid: ŷ = σ(z), dŷ/dz = ŷ(1-ŷ)
189
+ # Chain rule: dL/dz = dL/dŷ * dŷ/dz = ŷ - y
190
+
191
+ self.dL_dz2 = self.a2[0] - self.y_target
192
+
193
+ # Store intermediate values for explanation purposes
194
+ # Clip to prevent division by zero (same epsilon as in loss calculation)
195
+ epsilon = 1e-15
196
+ y_pred_clipped = np.clip(self.a2[0], epsilon, 1 - epsilon)
197
+ self.dL_da2 = -(self.y_target / y_pred_clipped - (1 - self.y_target) / (1 - y_pred_clipped))
198
+ self.da2_dz2 = self.a2[0] * (1 - self.a2[0])
188
199
 
189
200
  return self.dL_dz2
190
201
 
@@ -302,7 +313,7 @@ class SimpleNeuralNetworkBase(Question, abc.ABC):
302
313
  if self.y_target is not None:
303
314
  right_data.append([
304
315
  ContentAST.Equation("y", inline=True),
305
- f"{self.y_target:.2f}"
316
+ f"{int(self.y_target)}" # Binary target (0 or 1)
306
317
  ])
307
318
 
308
319
  if self.loss is not None:
@@ -560,7 +571,7 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
560
571
  # Question description
561
572
  body.add_element(ContentAST.Paragraph([
562
573
  f"Given the neural network below with {self._get_activation_name()} activation "
563
- f"in the hidden layer and linear activation (f(z) = z) in the output layer, "
574
+ f"in the hidden layer and sigmoid activation in the output layer (for binary classification), "
564
575
  f"calculate the forward pass for the given input values."
565
576
  ]))
566
577
 
@@ -577,7 +588,7 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
577
588
 
578
589
  # Activation function
579
590
  body.add_element(ContentAST.Paragraph([
580
- f"**Activation function:** {self._get_activation_name()}"
591
+ f"**Hidden layer activation:** {self._get_activation_name()}"
581
592
  ]))
582
593
 
583
594
  # Create answer block
@@ -586,14 +597,14 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
586
597
  answers.append(
587
598
  ContentAST.Answer(
588
599
  answer=self.answers[f"h{i+1}"],
589
- label=f"h_{i+1} (hidden neuron {i+1} output)"
600
+ label=f"h_{i+1}"
590
601
  )
591
602
  )
592
603
 
593
604
  answers.append(
594
605
  ContentAST.Answer(
595
606
  answer=self.answers["y_pred"],
596
- label="ŷ (network output)"
607
+ label="ŷ"
597
608
  )
598
609
  )
599
610
 
@@ -652,7 +663,7 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
652
663
 
653
664
  # Output layer
654
665
  explanation.add_element(ContentAST.Paragraph([
655
- "**Step 3: Calculate output (with linear activation)**"
666
+ "**Step 3: Calculate output (with sigmoid activation)**"
656
667
  ]))
657
668
 
658
669
  terms = []
@@ -669,12 +680,13 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
669
680
  ))
670
681
 
671
682
  explanation.add_element(ContentAST.Equation(
672
- f"\\hat{{y}} = f(z_{{out}}) = z_{{out}} = {self.a2[0]:.4f}",
683
+ f"\\hat{{y}} = \\sigma(z_{{out}}) = \\frac{{1}}{{1 + e^{{-{self.z2[0]:.4f}}}}} = {self.a2[0]:.4f}",
673
684
  inline=False
674
685
  ))
675
686
 
676
687
  explanation.add_element(ContentAST.Paragraph([
677
- "(Note: The output layer uses linear activation, so the output can be any real number)"
688
+ "(Note: The output layer uses sigmoid activation for binary classification, "
689
+ "so the output is between 0 and 1, representing the probability of class 1)"
678
690
  ]))
679
691
 
680
692
  return explanation
@@ -699,11 +711,12 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
699
711
  # Run forward pass
700
712
  self._forward_pass()
701
713
 
702
- # Generate target and compute loss
703
- # Target should be different from output to create meaningful gradients
704
- self.y_target = float(self.a2[0] + self.rng.uniform(1, 3) * self.rng.choice([-1, 1]))
705
- # Round target to display precision (2 decimal places)
706
- self.y_target = round(self.y_target, 2)
714
+ # Generate binary target (0 or 1)
715
+ # Choose the opposite of what the network predicts to create meaningful gradients
716
+ if self.a2[0] > 0.5:
717
+ self.y_target = 0
718
+ else:
719
+ self.y_target = 1
707
720
  self._compute_loss(self.y_target)
708
721
  # Round loss to display precision (4 decimal places)
709
722
  self.loss = round(self.loss, 4)
@@ -735,7 +748,8 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
735
748
  # Question description
736
749
  body.add_element(ContentAST.Paragraph([
737
750
  f"Given the neural network below with {self._get_activation_name()} activation "
738
- f"in the hidden layer, a forward pass has been completed with the values shown. "
751
+ f"in the hidden layer and sigmoid activation in the output layer (for binary classification), "
752
+ f"a forward pass has been completed with the values shown. "
739
753
  f"Calculate the gradients (∂L/∂w) for the specified weights using backpropagation."
740
754
  ]))
741
755
 
@@ -752,7 +766,7 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
752
766
 
753
767
  # Activation function
754
768
  body.add_element(ContentAST.Paragraph([
755
- f"**Activation function:** {self._get_activation_name()}"
769
+ f"**Hidden layer activation:** {self._get_activation_name()}"
756
770
  ]))
757
771
 
758
772
  body.add_element(ContentAST.Paragraph([
@@ -767,7 +781,7 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
767
781
  answers.append(
768
782
  ContentAST.Answer(
769
783
  answer=self.answers[f"dL_dw2_{i}"],
770
- label=f"∂L/∂w_{i+3} (weight from h_{i+1} to output)"
784
+ label=f"∂L/∂w_{i+3}"
771
785
  )
772
786
  )
773
787
 
@@ -776,7 +790,7 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
776
790
  answers.append(
777
791
  ContentAST.Answer(
778
792
  answer=self.answers[f"dL_dw1_0{j}"],
779
- label=f"∂L/∂w_1{j+1} (weight from x_{j+1} to h_1)"
793
+ label=f"∂L/∂w_1{j+1}"
780
794
  )
781
795
  )
782
796
 
@@ -797,14 +811,19 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
797
811
  ]))
798
812
 
799
813
  explanation.add_element(ContentAST.Paragraph([
800
- "For MSE loss with linear output activation:"
814
+ "For binary cross-entropy loss with sigmoid output activation, "
815
+ "the gradient with respect to the pre-activation simplifies beautifully:"
801
816
  ]))
802
817
 
803
818
  explanation.add_element(ContentAST.Equation(
804
- f"\\frac{{\\partial L}}{{\\partial \\hat{{y}}}} = -(y - \\hat{{y}}) = -({self.y_target:.2f} - {self.a2[0]:.4f}) = {self.dL_da2:.4f}",
819
+ f"\\frac{{\\partial L}}{{\\partial z_{{out}}}} = \\hat{{y}} - y = {self.a2[0]:.4f} - {int(self.y_target)} = {self.dL_dz2:.4f}",
805
820
  inline=False
806
821
  ))
807
822
 
823
+ explanation.add_element(ContentAST.Paragraph([
824
+ "(This elegant result comes from combining the BCE loss derivative and sigmoid activation derivative)"
825
+ ]))
826
+
808
827
  # W2 gradients
809
828
  explanation.add_element(ContentAST.Paragraph([
810
829
  "**Step 2: Gradients for hidden-to-output weights**"
@@ -817,7 +836,7 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
817
836
  for i in range(self.num_hidden):
818
837
  grad = self._compute_gradient_W2(i)
819
838
  explanation.add_element(ContentAST.Equation(
820
- f"\\frac{{\\partial L}}{{\\partial w_{i+3}}} = \\frac{{\\partial L}}{{\\partial \\hat{{y}}}} \\cdot \\frac{{\\partial \\hat{{y}}}}{{\\partial w_{i+3}}} = {self.dL_da2:.4f} \\cdot {self.a1[i]:.4f} = {grad:.4f}",
839
+ f"\\frac{{\\partial L}}{{\\partial w_{i+3}}} = \\frac{{\\partial L}}{{\\partial z_{{out}}}} \\cdot \\frac{{\\partial z_{{out}}}}{{\\partial w_{i+3}}} = {self.dL_dz2:.4f} \\cdot {self.a1[i]:.4f} = {grad:.4f}",
821
840
  inline=False
822
841
  ))
823
842
 
@@ -839,14 +858,14 @@ class BackpropGradientQuestion(SimpleNeuralNetworkBase):
839
858
  grad = self._compute_gradient_W1(0, j)
840
859
 
841
860
  if self.activation_function == self.ACTIVATION_SIGMOID:
842
- act_deriv_str = f"\\sigma(z_1)(1-\\sigma(z_1)) = {self.a1[0]:.4f}(1-{self.a1[0]:.4f}) = {da1_dz1:.4f}"
861
+ act_deriv_str = f"\\sigma'(z_1) = h_1(1-h_1) = {self.a1[0]:.4f}(1-{self.a1[0]:.4f}) = {da1_dz1:.4f}"
843
862
  elif self.activation_function == self.ACTIVATION_RELU:
844
- act_deriv_str = f"\\mathbb{{1}}(z_1 > 0) = {da1_dz1:.4f}"
863
+ act_deriv_str = f"\\text{{ReLU}}'(z_1) = \\mathbb{{1}}(z_1 > 0) = {da1_dz1:.4f}"
845
864
  else:
846
865
  act_deriv_str = f"1"
847
866
 
848
867
  explanation.add_element(ContentAST.Equation(
849
- f"\\frac{{\\partial L}}{{\\partial w_{{1{j+1}}}}} = \\frac{{\\partial L}}{{\\partial \\hat{{y}}}} \\cdot w_{3} \\cdot {act_deriv_str} \\cdot x_{j+1} = {self.dL_da2:.4f} \\cdot {dz2_da1:.4f} \\cdot {da1_dz1:.4f} \\cdot {self.X[j]:.1f} = {grad:.4f}",
868
+ f"\\frac{{\\partial L}}{{\\partial w_{{1{j+1}}}}} = \\frac{{\\partial L}}{{\\partial z_{{out}}}} \\cdot w_{3} \\cdot {act_deriv_str} \\cdot x_{j+1} = {self.dL_dz2:.4f} \\cdot {dz2_da1:.4f} \\cdot {da1_dz1:.4f} \\cdot {self.X[j]:.1f} = {grad:.4f}",
850
869
  inline=False
851
870
  ))
852
871
 
@@ -1015,10 +1034,12 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1015
1034
  # Run forward pass
1016
1035
  self._forward_pass()
1017
1036
 
1018
- # Generate target and compute loss
1019
- self.y_target = float(self.a2[0] + self.rng.uniform(1, 3) * self.rng.choice([-1, 1]))
1020
- # Round target to display precision (2 decimal places)
1021
- self.y_target = round(self.y_target, 2)
1037
+ # Generate binary target (0 or 1)
1038
+ # Choose the opposite of what the network predicts to create meaningful gradients
1039
+ if self.a2[0] > 0.5:
1040
+ self.y_target = 0
1041
+ else:
1042
+ self.y_target = 1
1022
1043
  self._compute_loss(self.y_target)
1023
1044
  # Round loss to display precision (4 decimal places)
1024
1045
  self.loss = round(self.loss, 4)
@@ -1070,15 +1091,16 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1070
1091
 
1071
1092
  # Question description
1072
1093
  body.add_element(ContentAST.Paragraph([
1073
- f"Given the neural network below, perform one complete training step (forward pass, "
1074
- f"loss calculation, backpropagation, and weight update) for the given input and target."
1094
+ f"Given the neural network below with {self._get_activation_name()} activation "
1095
+ f"in the hidden layer and sigmoid activation in the output layer (for binary classification), "
1096
+ f"perform one complete training step (forward pass, loss calculation, "
1097
+ f"backpropagation, and weight update) for the given input and target."
1075
1098
  ]))
1076
1099
 
1077
1100
  # Network diagram
1078
1101
  body.add_element(
1079
1102
  ContentAST.Picture(
1080
- img_data=self._generate_network_diagram(show_weights=True, show_activations=False),
1081
- caption=f"Neural network (before training)"
1103
+ img_data=self._generate_network_diagram(show_weights=True, show_activations=False)
1082
1104
  )
1083
1105
  )
1084
1106
 
@@ -1096,7 +1118,7 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1096
1118
 
1097
1119
  body.add_element(ContentAST.Paragraph([
1098
1120
  "Target: ",
1099
- ContentAST.Equation(f"y = {self.y_target:.2f}", inline=True)
1121
+ ContentAST.Equation(f"y = {int(self.y_target)}", inline=True)
1100
1122
  ]))
1101
1123
 
1102
1124
  body.add_element(ContentAST.Paragraph([
@@ -1105,13 +1127,9 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1105
1127
  ]))
1106
1128
 
1107
1129
  body.add_element(ContentAST.Paragraph([
1108
- f"**Activation function:** {self._get_activation_name()}"
1130
+ f"**Hidden layer activation:** {self._get_activation_name()}"
1109
1131
  ]))
1110
1132
 
1111
- body.add_element(ContentAST.Paragraph([
1112
- "**Complete the following training steps:**"
1113
- ]))
1114
-
1115
1133
  # Network parameters table
1116
1134
  body.add_element(self._generate_parameter_table(include_activations=False))
1117
1135
 
@@ -1128,35 +1146,35 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1128
1146
  answers.append(
1129
1147
  ContentAST.Answer(
1130
1148
  answer=self.answers["loss"],
1131
- label="2. Loss - MSE: L = (1/2)(y - ŷ)²"
1149
+ label="2. Loss"
1132
1150
  )
1133
1151
  )
1134
1152
 
1135
1153
  answers.append(
1136
1154
  ContentAST.Answer(
1137
1155
  answer=self.answers["grad_w3"],
1138
- label="3. Gradient ∂L/∂w₃ (weight h₁ → ŷ)"
1156
+ label="3. Gradient ∂L/∂w₃"
1139
1157
  )
1140
1158
  )
1141
1159
 
1142
1160
  answers.append(
1143
1161
  ContentAST.Answer(
1144
1162
  answer=self.answers["grad_w11"],
1145
- label="4. Gradient ∂L/∂w₁₁ (weight x₁ → h₁)"
1163
+ label="4. Gradient ∂L/∂w₁₁"
1146
1164
  )
1147
1165
  )
1148
1166
 
1149
1167
  answers.append(
1150
1168
  ContentAST.Answer(
1151
1169
  answer=self.answers["new_w3"],
1152
- label="5. Updated w₃: w₃' = w₃ - α(∂L/∂w₃)"
1170
+ label="5. Updated w₃:"
1153
1171
  )
1154
1172
  )
1155
1173
 
1156
1174
  answers.append(
1157
1175
  ContentAST.Answer(
1158
1176
  answer=self.answers["new_w11"],
1159
- label="6. Updated w₁₁: w₁₁' = w₁₁ - α(∂L/∂w₁₁)"
1177
+ label="6. Updated w₁₁:"
1160
1178
  )
1161
1179
  )
1162
1180
 
@@ -1194,40 +1212,59 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1194
1212
  inline=False
1195
1213
  ))
1196
1214
 
1197
- # Output
1215
+ # Output (pre-activation)
1198
1216
  z2 = self.W2[0, 0] * self.a1[0] + self.W2[0, 1] * self.a1[1] + self.b2[0]
1199
1217
  explanation.add_element(ContentAST.Equation(
1200
- f"\\hat{{y}} = w_3 h_1 + w_4 h_2 + b_2 = {self.W2[0,0]:.1f} \\cdot {self.a1[0]:.4f} + {self.W2[0,1]:.1f} \\cdot {self.a1[1]:.4f} + {self.b2[0]:.1f} = {self.a2[0]:.4f}",
1218
+ f"z_{{out}} = w_3 h_1 + w_4 h_2 + b_2 = {self.W2[0,0]:.1f} \\cdot {self.a1[0]:.4f} + {self.W2[0,1]:.1f} \\cdot {self.a1[1]:.4f} + {self.b2[0]:.1f} = {self.z2[0]:.4f}",
1219
+ inline=False
1220
+ ))
1221
+
1222
+ # Output (sigmoid activation)
1223
+ explanation.add_element(ContentAST.Equation(
1224
+ f"\\hat{{y}} = \\sigma(z_{{out}}) = \\frac{{1}}{{1 + e^{{-{self.z2[0]:.4f}}}}} = {self.a2[0]:.4f}",
1201
1225
  inline=False
1202
1226
  ))
1203
1227
 
1204
1228
  # Step 2: Loss
1205
1229
  explanation.add_element(ContentAST.Paragraph([
1206
- "**Step 2: Calculate Loss**"
1230
+ "**Step 2: Calculate Loss (Binary Cross-Entropy)**"
1207
1231
  ]))
1208
1232
 
1233
+ # Show the full BCE formula first
1209
1234
  explanation.add_element(ContentAST.Equation(
1210
- f"L = \\frac{{1}}{{2}}(y - \\hat{{y}})^2 = \\frac{{1}}{{2}}({self.y_target:.2f} - {self.a2[0]:.4f})^2 = {self.loss:.4f}",
1235
+ f"L = -[y \\log(\\hat{{y}}) + (1-y) \\log(1-\\hat{{y}})]",
1211
1236
  inline=False
1212
1237
  ))
1213
1238
 
1239
+ # Then evaluate it
1240
+ if self.y_target == 1:
1241
+ explanation.add_element(ContentAST.Equation(
1242
+ f"L = -[1 \\cdot \\log({self.a2[0]:.4f}) + 0 \\cdot \\log(1-{self.a2[0]:.4f})] = -\\log({self.a2[0]:.4f}) = {self.loss:.4f}",
1243
+ inline=False
1244
+ ))
1245
+ else:
1246
+ explanation.add_element(ContentAST.Equation(
1247
+ f"L = -[0 \\cdot \\log({self.a2[0]:.4f}) + 1 \\cdot \\log(1-{self.a2[0]:.4f})] = -\\log({1-self.a2[0]:.4f}) = {self.loss:.4f}",
1248
+ inline=False
1249
+ ))
1250
+
1214
1251
  # Step 3: Gradients
1215
1252
  explanation.add_element(ContentAST.Paragraph([
1216
1253
  "**Step 3: Compute Gradients**"
1217
1254
  ]))
1218
1255
 
1219
1256
  explanation.add_element(ContentAST.Paragraph([
1220
- "Loss gradient:"
1257
+ "For BCE with sigmoid, the output layer gradient simplifies to:"
1221
1258
  ]))
1222
1259
 
1223
1260
  explanation.add_element(ContentAST.Equation(
1224
- f"\\frac{{\\partial L}}{{\\partial \\hat{{y}}}} = -(y - \\hat{{y}}) = {self.dL_da2:.4f}",
1261
+ f"\\frac{{\\partial L}}{{\\partial z_{{out}}}} = \\hat{{y}} - y = {self.a2[0]:.4f} - {int(self.y_target)} = {self.dL_dz2:.4f}",
1225
1262
  inline=False
1226
1263
  ))
1227
1264
 
1228
1265
  grad_w3 = self._compute_gradient_W2(0)
1229
1266
  explanation.add_element(ContentAST.Equation(
1230
- f"\\frac{{\\partial L}}{{\\partial w_3}} = \\frac{{\\partial L}}{{\\partial \\hat{{y}}}} \\cdot h_1 = {self.dL_da2:.4f} \\cdot {self.a1[0]:.4f} = {grad_w3:.4f}",
1267
+ f"\\frac{{\\partial L}}{{\\partial w_3}} = \\frac{{\\partial L}}{{\\partial z_{{out}}}} \\cdot h_1 = {self.dL_dz2:.4f} \\cdot {self.a1[0]:.4f} = {grad_w3:.4f}",
1231
1268
  inline=False
1232
1269
  ))
1233
1270
 
@@ -1235,8 +1272,15 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1235
1272
  dz2_da1 = self.W2[0, 0]
1236
1273
  da1_dz1 = self._activation_derivative(self.z1[0])
1237
1274
 
1275
+ if self.activation_function == self.ACTIVATION_SIGMOID:
1276
+ act_deriv_str = f"h_1(1-h_1)"
1277
+ elif self.activation_function == self.ACTIVATION_RELU:
1278
+ act_deriv_str = f"\\text{{ReLU}}'(z_1)"
1279
+ else:
1280
+ act_deriv_str = f"1"
1281
+
1238
1282
  explanation.add_element(ContentAST.Equation(
1239
- f"\\frac{{\\partial L}}{{\\partial w_{{11}}}} = \\frac{{\\partial L}}{{\\partial \\hat{{y}}}} \\cdot w_3 \\cdot \\sigma'(z_1) \\cdot x_1 = {self.dL_da2:.4f} \\cdot {dz2_da1:.4f} \\cdot {da1_dz1:.4f} \\cdot {self.X[0]:.1f} = {grad_w11:.4f}",
1283
+ f"\\frac{{\\partial L}}{{\\partial w_{{11}}}} = \\frac{{\\partial L}}{{\\partial z_{{out}}}} \\cdot w_3 \\cdot {act_deriv_str} \\cdot x_1 = {self.dL_dz2:.4f} \\cdot {dz2_da1:.4f} \\cdot {da1_dz1:.4f} \\cdot {self.X[0]:.1f} = {grad_w11:.4f}",
1240
1284
  inline=False
1241
1285
  ))
1242
1286
 
QuizGenerator/question.py CHANGED
@@ -21,7 +21,7 @@ import yaml
21
21
  from typing import List, Dict, Any, Tuple, Optional
22
22
  import canvasapi.course, canvasapi.quiz
23
23
 
24
- from QuizGenerator.misc import OutputFormat, Answer
24
+ from QuizGenerator.misc import Answer
25
25
  from QuizGenerator.contentast import ContentAST
26
26
  from QuizGenerator.performance import timer, PerformanceTracker
27
27
 
@@ -500,7 +500,8 @@ class Question(abc.ABC):
500
500
  while not is_interesting:
501
501
  # Increment seed for each backoff attempt to maintain deterministic behavior
502
502
  current_seed = None if base_seed is None else base_seed + backoff_counter
503
- self.refresh(rng_seed=current_seed, hard_refresh=(backoff_counter > 0))
503
+ # Pass config_params to refresh so custom kwargs from YAML are available
504
+ self.refresh(rng_seed=current_seed, hard_refresh=(backoff_counter > 0), **self.config_params)
504
505
  is_interesting = self.is_interesting()
505
506
  backoff_counter += 1
506
507
 
QuizGenerator/quiz.py CHANGED
@@ -14,7 +14,6 @@ from typing import List, Dict, Optional
14
14
 
15
15
  import yaml
16
16
 
17
- from QuizGenerator.misc import OutputFormat
18
17
  from QuizGenerator.contentast import ContentAST
19
18
  from QuizGenerator.question import Question, QuestionRegistry, QuestionGroup
20
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.1.4
3
+ Version: 0.3.0
4
4
  Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
5
5
  Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
6
6
  Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
@@ -21,6 +21,7 @@ Requires-Dist: canvasapi==3.2.0
21
21
  Requires-Dist: cryptography>=41.0.0
22
22
  Requires-Dist: graphviz>=0.21
23
23
  Requires-Dist: jinja2==3.1.3
24
+ Requires-Dist: keras>=3.12.0
24
25
  Requires-Dist: markdown>=3.9
25
26
  Requires-Dist: matplotlib
26
27
  Requires-Dist: pylatex>=1.4.2
@@ -31,6 +32,7 @@ Requires-Dist: pyyaml==6.0.1
31
32
  Requires-Dist: requests==2.32.2
32
33
  Requires-Dist: segno>=1.6.0
33
34
  Requires-Dist: sympy>=1.14.0
35
+ Requires-Dist: tensorflow>=2.20.0
34
36
  Provides-Extra: grading
35
37
  Requires-Dist: pillow>=10.0.0; extra == 'grading'
36
38
  Requires-Dist: pyzbar>=0.1.9; extra == 'grading'
@@ -2,15 +2,15 @@ QuizGenerator/README.md,sha256=4n16gKyhIAKRBX4VKlpfcK0pyUYJ6Ht08MUsnwgxrZo,145
2
2
  QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
3
3
  QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
4
4
  QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
5
- QuizGenerator/contentast.py,sha256=3yvgtpw4ZSN9Cxfiq7QncT0JMoLoX4XuQ57GeIkfRj8,64171
5
+ QuizGenerator/contentast.py,sha256=Em4cnA64Y8_07VruJk_MXwiWcJwqT4-YVf-Lw7uIvYY,68327
6
6
  QuizGenerator/generate.py,sha256=o2XezoSE0u-qjxYu1_Ofm9Lpkza7M2Tg47C-ClMcPsE,7197
7
7
  QuizGenerator/logging.yaml,sha256=VJCdh26D8e_PNUs4McvvP1ojz9EVjQNifJzfhEk1Mbo,1114
8
- QuizGenerator/misc.py,sha256=HE6B49IQ4E2EdAIO2yprlluVRS5dtWtFsiIkk68zY2E,16125
9
- QuizGenerator/mixins.py,sha256=RjV76C1tkTLSvhSoMys67W7UmR6y6wAAcBM2Msxdpd0,18186
8
+ QuizGenerator/misc.py,sha256=JYv-SUn3y33O6grpgvRWRBkJi4RhTuXcMgrhmBjRbZg,18710
9
+ QuizGenerator/mixins.py,sha256=zUKTkswq7aoDZ_nGPUdRuvnza8iH8ZCi6IH2Uw-kCvs,18492
10
10
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
11
11
  QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
12
- QuizGenerator/question.py,sha256=c72zJMoUGH-Io7XVaR1kLBzlW_s-0W0_XeMy9gbIuIg,26004
13
- QuizGenerator/quiz.py,sha256=wOuj_mDfdnUNHlAEAcLUZoH-JX57RovLoYW4mA0GgPc,18762
12
+ QuizGenerator/question.py,sha256=uxDYyrq17JFXQ11S03Px5cyRuPYn4qKT3z7TZn9XSjg,26093
13
+ QuizGenerator/quiz.py,sha256=toPodXea2UYGgAf4jyor3Gz-gtXYN1YUJFJFQ5u70v4,18718
14
14
  QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
15
15
  QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
16
16
  QuizGenerator/canvas/canvas_interface.py,sha256=wsEWh2lonUMgmbtXF-Zj59CAM_0NInoaERqsujlYMfc,24501
@@ -20,10 +20,10 @@ QuizGenerator/premade_questions/basic.py,sha256=wAvVZED6a7VToIvSCdAx6SrExmR0xVRo
20
20
  QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
21
21
  QuizGenerator/premade_questions/cst334/languages.py,sha256=N5vcmZi-AFM_BZypnvNogHD7s--28-j-8tykYg6CBzs,14388
22
22
  QuizGenerator/premade_questions/cst334/math_questions.py,sha256=za8lNqhM0RB8qefmPP-Ww0WB_SQn0iRcBKOrZgyHCQQ,9290
23
- QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=QhCALX7nzq_moLnkok4JVNO4crtmJnosg7_xOZL_5-c,51597
23
+ QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=B4hpnMliJY-x65hNbjwbf22m-jiTi3WEXmauKv_YA84,51598
24
24
  QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
25
25
  QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=em-HzFRnaroDmHl5uA771HyFMI7dMvG-gxTgsB3ecaY,14458
26
- QuizGenerator/premade_questions/cst334/process.py,sha256=C7-FzOGW7EzQHXZNKden7eHo1NoG0UQbci0ZrUtybzs,23732
26
+ QuizGenerator/premade_questions/cst334/process.py,sha256=EB0iuT9Q8FfOnmlQoXL7gkfsPyVJP55cRFOe2mWfamc,23647
27
27
  QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
29
29
  QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=ssj6Xkpw6vDiL4qwVOiHUhly3TX50oX4KJtouj7qN6g,12809
@@ -33,12 +33,19 @@ QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=iB3obG6-u
33
33
  QuizGenerator/premade_questions/cst463/math_and_data/__init__.py,sha256=EbIaUrx7_aK9j3Gd8Mk08h9GocTq_0OoNu2trfNwaU8,202
34
34
  QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=sq27xv7X9aa0axFxomusZRwM-ICj9grbhD_Bv3n3gJg,28947
35
35
  QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=tNxfR6J1cZHsHG9GfwVyl6lxxN_TEnhKDmMq4aVLwow,20793
36
+ QuizGenerator/premade_questions/cst463/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ QuizGenerator/premade_questions/cst463/models/attention.py,sha256=i8h6DihzJTc_QFrdm1eaYhnuhlXKRUv_vIDg3jk_LZ8,5502
38
+ QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=iNuQmHgd8kwAXaofTwidal6pZum31r21UP3XERspE0M,5774
39
+ QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
40
+ QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=-tXeGgqPkctBBUy4RvEPqhv2kfPqoyO2wk-lNJLNWmY,6697
41
+ QuizGenerator/premade_questions/cst463/models/text.py,sha256=T1cmaoxNcWK1UbytFQmweL_Nmkm5ONq1m6F4AIJdmdc,6284
42
+ QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=A2ZVyodfibAWQg9-KraT1cCPTLXRWy5vNSqL0GWexXg,6784
36
43
  QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
37
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=7ZtCQ2fXhIPSd99TstQfOKCN13GJE_56UfBQKdmzmMI,42398
44
+ QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=pyTSvibCaLuT0LnYAZfjUoZlzywaPWWAaSZ5VepH04E,44148
38
45
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
39
46
  QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=dPn8Sj0yk4m02np62esMKZ7CvcljhYq3Tq51nY9aJnA,29781
40
- quizgenerator-0.1.4.dist-info/METADATA,sha256=ebekoIHR35xPw-W6sVVHxD1xQulJioslbCYtT8uCfiE,7149
41
- quizgenerator-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
- quizgenerator-0.1.4.dist-info/entry_points.txt,sha256=iViWMzswXGe88WKoue_Ib-ODUSiT_j_6f1us28w9pkc,56
43
- quizgenerator-0.1.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
44
- quizgenerator-0.1.4.dist-info/RECORD,,
47
+ quizgenerator-0.3.0.dist-info/METADATA,sha256=qLE6mR5zXwWdFv5HYskA4KLgnya7VVRh85a7AY8lDRg,7212
48
+ quizgenerator-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
49
+ quizgenerator-0.3.0.dist-info/entry_points.txt,sha256=iViWMzswXGe88WKoue_Ib-ODUSiT_j_6f1us28w9pkc,56
50
+ quizgenerator-0.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
51
+ quizgenerator-0.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any