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.
- QuizGenerator/contentast.py +48 -15
- QuizGenerator/generate.py +2 -1
- QuizGenerator/mixins.py +14 -100
- QuizGenerator/premade_questions/basic.py +24 -29
- QuizGenerator/premade_questions/cst334/languages.py +100 -99
- QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
- QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
- QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
- QuizGenerator/premade_questions/cst334/process.py +312 -328
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -521
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
- QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
- QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
- QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
- QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
- QuizGenerator/premade_questions/cst463/models/text.py +65 -67
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
- QuizGenerator/question.py +310 -202
- QuizGenerator/quiz.py +8 -5
- QuizGenerator/regenerate.py +14 -6
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/METADATA +30 -2
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/RECORD +30 -30
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/WHEEL +0 -0
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,6 +21,16 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
21
21
|
self.num_variables = kwargs.get("num_variables", 2)
|
|
22
22
|
self.max_degree = kwargs.get("max_degree", 2)
|
|
23
23
|
|
|
24
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
25
|
+
if "num_variables" in kwargs:
|
|
26
|
+
self.num_variables = kwargs.get("num_variables", self.num_variables)
|
|
27
|
+
if "max_degree" in kwargs:
|
|
28
|
+
self.max_degree = kwargs.get("max_degree", self.max_degree)
|
|
29
|
+
self.rng.seed(rng_seed)
|
|
30
|
+
context = dict(kwargs)
|
|
31
|
+
context["rng_seed"] = rng_seed
|
|
32
|
+
return context
|
|
33
|
+
|
|
24
34
|
def _generate_evaluation_point(self) -> List[float]:
|
|
25
35
|
"""Generate a random point for gradient evaluation."""
|
|
26
36
|
return [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
|
|
@@ -32,9 +42,9 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
32
42
|
else:
|
|
33
43
|
return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
|
|
34
44
|
|
|
35
|
-
def _create_derivative_answers(self, evaluation_point: List[float]) ->
|
|
45
|
+
def _create_derivative_answers(self, evaluation_point: List[float]) -> List[ca.Answer]:
|
|
36
46
|
"""Create answer fields for each partial derivative at the evaluation point."""
|
|
37
|
-
|
|
47
|
+
answers: List[ca.Answer] = []
|
|
38
48
|
|
|
39
49
|
# Evaluate gradient at the specified point
|
|
40
50
|
subs_map = dict(zip(self.variables, evaluation_point))
|
|
@@ -57,9 +67,11 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
57
67
|
# Use auto_float for Canvas compatibility with integers and decimals
|
|
58
68
|
# Label includes the partial derivative notation
|
|
59
69
|
label = f"∂f/∂x_{i} at ({eval_point_str})"
|
|
60
|
-
|
|
70
|
+
answers.append(ca.AnswerTypes.Float(gradient_value, label=label))
|
|
71
|
+
|
|
72
|
+
return answers
|
|
61
73
|
|
|
62
|
-
def _create_gradient_vector_answer(self) ->
|
|
74
|
+
def _create_gradient_vector_answer(self) -> ca.Answer:
|
|
63
75
|
"""Create a single gradient vector answer for PDF format."""
|
|
64
76
|
# Format gradient as vector notation
|
|
65
77
|
subs_map = dict(zip(self.variables, self.evaluation_point))
|
|
@@ -75,9 +87,9 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
75
87
|
|
|
76
88
|
# Format as vector for display using consistent formatting
|
|
77
89
|
vector_str = format_vector(gradient_values)
|
|
78
|
-
|
|
90
|
+
return ca.AnswerTypes.String(vector_str, pdf_only=True)
|
|
79
91
|
|
|
80
|
-
def
|
|
92
|
+
def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
81
93
|
"""Build question body and collect answers."""
|
|
82
94
|
body = ca.Section()
|
|
83
95
|
answers = []
|
|
@@ -109,8 +121,8 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
109
121
|
)
|
|
110
122
|
|
|
111
123
|
# For Canvas: Use OnlyHtml to show individual partial derivatives
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
derivative_answers = self._create_derivative_answers(self.evaluation_point)
|
|
125
|
+
for i, answer in enumerate(derivative_answers):
|
|
114
126
|
answers.append(answer)
|
|
115
127
|
body.add_element(
|
|
116
128
|
ca.OnlyHtml([
|
|
@@ -126,12 +138,7 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
126
138
|
|
|
127
139
|
return body, answers
|
|
128
140
|
|
|
129
|
-
def
|
|
130
|
-
"""Build question body (backward compatible interface)."""
|
|
131
|
-
body, _ = self._get_body(**kwargs)
|
|
132
|
-
return body
|
|
133
|
-
|
|
134
|
-
def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
141
|
+
def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
135
142
|
"""Build question explanation."""
|
|
136
143
|
explanation = ca.Section()
|
|
137
144
|
|
|
@@ -182,18 +189,13 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
182
189
|
|
|
183
190
|
return explanation, []
|
|
184
191
|
|
|
185
|
-
def get_explanation(self, **kwargs) -> ca.Section:
|
|
186
|
-
"""Build question explanation (backward compatible interface)."""
|
|
187
|
-
explanation, _ = self._get_explanation(**kwargs)
|
|
188
|
-
return explanation
|
|
189
|
-
|
|
190
192
|
|
|
191
193
|
@QuestionRegistry.register("DerivativeBasic")
|
|
192
194
|
class DerivativeBasic(DerivativeQuestion):
|
|
193
195
|
"""Basic derivative calculation using polynomial functions."""
|
|
194
196
|
|
|
195
|
-
def
|
|
196
|
-
super().
|
|
197
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
198
|
+
super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
197
199
|
|
|
198
200
|
# Generate a basic polynomial function
|
|
199
201
|
self.variables, self.function, self.gradient_function, self.equation = generate_function(
|
|
@@ -203,19 +205,20 @@ class DerivativeBasic(DerivativeQuestion):
|
|
|
203
205
|
# Generate evaluation point
|
|
204
206
|
self.evaluation_point = self._generate_evaluation_point()
|
|
205
207
|
|
|
206
|
-
# Create answers
|
|
208
|
+
# Create answers for evaluation point (used in _build_body)
|
|
207
209
|
self._create_derivative_answers(self.evaluation_point)
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
context = dict(kwargs)
|
|
212
|
+
context["rng_seed"] = rng_seed
|
|
213
|
+
return context
|
|
211
214
|
|
|
212
215
|
|
|
213
216
|
@QuestionRegistry.register("DerivativeChain")
|
|
214
217
|
class DerivativeChain(DerivativeQuestion):
|
|
215
218
|
"""Chain rule derivative calculation using function composition."""
|
|
216
219
|
|
|
217
|
-
def
|
|
218
|
-
super().
|
|
220
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
221
|
+
super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
219
222
|
|
|
220
223
|
# Try to generate a valid function/point combination, regenerating if we hit complex numbers
|
|
221
224
|
max_attempts = 10
|
|
@@ -230,9 +233,6 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
230
233
|
# Create answers - this will raise ValueError if we get complex numbers
|
|
231
234
|
self._create_derivative_answers(self.evaluation_point)
|
|
232
235
|
|
|
233
|
-
# For PDF: Create single gradient vector answer
|
|
234
|
-
self._create_gradient_vector_answer()
|
|
235
|
-
|
|
236
236
|
# If we get here, everything worked
|
|
237
237
|
break
|
|
238
238
|
|
|
@@ -245,6 +245,10 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
245
245
|
# If we've exhausted attempts or different error, re-raise
|
|
246
246
|
raise
|
|
247
247
|
|
|
248
|
+
context = dict(kwargs)
|
|
249
|
+
context["rng_seed"] = rng_seed
|
|
250
|
+
return context
|
|
251
|
+
|
|
248
252
|
def _generate_composed_function(self) -> None:
|
|
249
253
|
"""Generate a composed function f(g(x)) for chain rule practice."""
|
|
250
254
|
# Create variable symbols
|
|
@@ -284,7 +288,7 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
284
288
|
f = sp.Function('f')
|
|
285
289
|
self.equation = sp.Eq(f(*self.variables), self.function)
|
|
286
290
|
|
|
287
|
-
def
|
|
291
|
+
def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
288
292
|
"""Build question explanation."""
|
|
289
293
|
explanation = ca.Section()
|
|
290
294
|
|
|
@@ -388,8 +392,3 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
388
392
|
)
|
|
389
393
|
|
|
390
394
|
return explanation, []
|
|
391
|
-
|
|
392
|
-
def get_explanation(self, **kwargs) -> ca.Section:
|
|
393
|
-
"""Build question explanation (backward compatible interface)."""
|
|
394
|
-
explanation, _ = self._get_explanation(**kwargs)
|
|
395
|
-
return explanation
|
|
@@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
# Note: This file does not use ca.Answer wrappers - it uses TableQuestionMixin
|
|
19
19
|
# which handles answer display through create_answer_table(). The answers are created
|
|
20
|
-
# with labels embedded at creation time in
|
|
20
|
+
# with labels embedded at creation time in _build_context().
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class GradientDescentQuestion(Question, abc.ABC):
|
|
@@ -75,40 +75,55 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
75
75
|
|
|
76
76
|
return results
|
|
77
77
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
79
|
+
if "num_steps" in kwargs:
|
|
80
|
+
self.num_steps = kwargs.get("num_steps", self.num_steps)
|
|
81
|
+
if "num_variables" in kwargs:
|
|
82
|
+
self.num_variables = kwargs.get("num_variables", self.num_variables)
|
|
83
|
+
if "max_degree" in kwargs:
|
|
84
|
+
self.max_degree = kwargs.get("max_degree", self.max_degree)
|
|
85
|
+
if "single_variable" in kwargs:
|
|
86
|
+
self.single_variable = kwargs.get("single_variable", self.single_variable)
|
|
87
|
+
if self.single_variable:
|
|
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)
|
|
93
|
+
|
|
81
94
|
# Generate function and its properties
|
|
82
95
|
self.variables, self.function, self.gradient_function, self.equation = generate_function(self.rng, self.num_variables, self.max_degree)
|
|
83
|
-
|
|
96
|
+
|
|
84
97
|
# Generate learning rate (expanded range)
|
|
85
98
|
self.learning_rate = self.rng.choice([0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5])
|
|
86
|
-
|
|
99
|
+
|
|
87
100
|
self.starting_point = [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
|
|
88
|
-
|
|
101
|
+
|
|
89
102
|
# Perform gradient descent
|
|
90
103
|
self.gradient_descent_results = self._perform_gradient_descent()
|
|
91
|
-
|
|
92
|
-
# Set up answers
|
|
93
|
-
self.answers = {}
|
|
94
104
|
|
|
95
|
-
#
|
|
105
|
+
# Build answers for each step
|
|
106
|
+
self.step_answers = {}
|
|
96
107
|
for i, result in enumerate(self.gradient_descent_results):
|
|
97
108
|
step = result['step']
|
|
98
109
|
|
|
99
110
|
# Location answer
|
|
100
111
|
location_key = f"answer__location_{step}"
|
|
101
|
-
self.
|
|
112
|
+
self.step_answers[location_key] = ca.AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
|
|
102
113
|
|
|
103
114
|
# Gradient answer
|
|
104
115
|
gradient_key = f"answer__gradient_{step}"
|
|
105
|
-
self.
|
|
116
|
+
self.step_answers[gradient_key] = ca.AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
|
|
106
117
|
|
|
107
118
|
# Update answer
|
|
108
119
|
update_key = f"answer__update_{step}"
|
|
109
|
-
self.
|
|
120
|
+
self.step_answers[update_key] = ca.AnswerTypes.Vector(list(result['update']), label=f"Update at step {step}")
|
|
121
|
+
|
|
122
|
+
context = dict(kwargs)
|
|
123
|
+
context["rng_seed"] = rng_seed
|
|
124
|
+
return context
|
|
110
125
|
|
|
111
|
-
def
|
|
126
|
+
def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
112
127
|
"""Build question body and collect answers."""
|
|
113
128
|
body = ca.Section()
|
|
114
129
|
answers = []
|
|
@@ -147,20 +162,20 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
147
162
|
|
|
148
163
|
# Fill in starting location for first row with default formatting
|
|
149
164
|
row["location"] = f"{format_vector(self.starting_point)}"
|
|
150
|
-
row[headers[2]] = f"answer__gradient_{step}" # gradient column
|
|
151
|
-
row[headers[3]] = f"answer__update_{step}" # update column
|
|
165
|
+
row[headers[2]] = self.step_answers[f"answer__gradient_{step}"] # gradient column
|
|
166
|
+
row[headers[3]] = self.step_answers[f"answer__update_{step}"] # update column
|
|
152
167
|
# Collect answers for this step (no location answer for step 1)
|
|
153
|
-
answers.append(self.
|
|
154
|
-
answers.append(self.
|
|
168
|
+
answers.append(self.step_answers[f"answer__gradient_{step}"])
|
|
169
|
+
answers.append(self.step_answers[f"answer__update_{step}"])
|
|
155
170
|
else:
|
|
156
171
|
# Subsequent rows - all answer fields
|
|
157
|
-
row["location"] = f"answer__location_{step}"
|
|
158
|
-
row[headers[2]] = f"answer__gradient_{step}"
|
|
159
|
-
row[headers[3]] = f"answer__update_{step}"
|
|
172
|
+
row["location"] = self.step_answers[f"answer__location_{step}"]
|
|
173
|
+
row[headers[2]] = self.step_answers[f"answer__gradient_{step}"]
|
|
174
|
+
row[headers[3]] = self.step_answers[f"answer__update_{step}"]
|
|
160
175
|
# Collect all answers for this step
|
|
161
|
-
answers.append(self.
|
|
162
|
-
answers.append(self.
|
|
163
|
-
answers.append(self.
|
|
176
|
+
answers.append(self.step_answers[f"answer__location_{step}"])
|
|
177
|
+
answers.append(self.step_answers[f"answer__gradient_{step}"])
|
|
178
|
+
answers.append(self.step_answers[f"answer__update_{step}"])
|
|
164
179
|
table_rows.append(row)
|
|
165
180
|
|
|
166
181
|
# Create the table using mixin
|
|
@@ -174,12 +189,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
174
189
|
|
|
175
190
|
return body, answers
|
|
176
191
|
|
|
177
|
-
def
|
|
178
|
-
"""Build question body (backward compatible interface)."""
|
|
179
|
-
body, _ = self._get_body(**kwargs)
|
|
180
|
-
return body
|
|
181
|
-
|
|
182
|
-
def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
192
|
+
def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
183
193
|
"""Build question explanation."""
|
|
184
194
|
explanation = ca.Section()
|
|
185
195
|
|
|
@@ -323,8 +333,3 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
323
333
|
)
|
|
324
334
|
|
|
325
335
|
return explanation, []
|
|
326
|
-
|
|
327
|
-
def get_explanation(self, **kwargs) -> ca.Section:
|
|
328
|
-
"""Build question explanation (backward compatible interface)."""
|
|
329
|
-
explanation, _ = self._get_explanation(**kwargs)
|
|
330
|
-
return explanation
|
|
@@ -13,7 +13,7 @@ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
|
|
|
13
13
|
log = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
# Note: This file migrates to the
|
|
16
|
+
# Note: This file migrates to the _build_body()/_build_explanation() pattern
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
@@ -35,12 +35,24 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
35
35
|
self.individual_losses = []
|
|
36
36
|
self.overall_loss = 0.0
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
39
39
|
"""Generate new random data and calculate losses."""
|
|
40
|
-
|
|
40
|
+
# Update configurable parameters if provided
|
|
41
|
+
if "num_samples" in kwargs:
|
|
42
|
+
self.num_samples = max(3, min(10, kwargs.get("num_samples", self.num_samples)))
|
|
43
|
+
if "num_input_features" in kwargs:
|
|
44
|
+
self.num_input_features = max(1, min(5, kwargs.get("num_input_features", self.num_input_features)))
|
|
45
|
+
if "vector_inputs" in kwargs:
|
|
46
|
+
self.vector_inputs = kwargs.get("vector_inputs", self.vector_inputs)
|
|
47
|
+
|
|
48
|
+
# Seed RNG and generate data
|
|
49
|
+
self.rng.seed(rng_seed)
|
|
41
50
|
self._generate_data()
|
|
42
51
|
self._calculate_losses()
|
|
43
|
-
|
|
52
|
+
|
|
53
|
+
context = dict(kwargs)
|
|
54
|
+
context["rng_seed"] = rng_seed
|
|
55
|
+
return context
|
|
44
56
|
|
|
45
57
|
@abc.abstractmethod
|
|
46
58
|
def _generate_data(self):
|
|
@@ -67,18 +79,15 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
67
79
|
"""Return the short name of the loss function (used in question body)."""
|
|
68
80
|
pass
|
|
69
81
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# Overall loss answer
|
|
79
|
-
self.answers["overall_loss"] = ca.AnswerTypes.Float(self.overall_loss, label="Overall loss")
|
|
82
|
+
def _build_loss_answers(self) -> Tuple[List[ca.Answer], ca.Answer]:
|
|
83
|
+
answers = [
|
|
84
|
+
ca.AnswerTypes.Float(self.individual_losses[i], label=f"Sample {i + 1} loss")
|
|
85
|
+
for i in range(self.num_samples)
|
|
86
|
+
]
|
|
87
|
+
overall = ca.AnswerTypes.Float(self.overall_loss, label="Overall loss")
|
|
88
|
+
return answers, overall
|
|
80
89
|
|
|
81
|
-
def
|
|
90
|
+
def _build_body(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
82
91
|
"""Build question body and collect answers."""
|
|
83
92
|
body = ca.Section()
|
|
84
93
|
answers = []
|
|
@@ -90,32 +99,25 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
90
99
|
]))
|
|
91
100
|
|
|
92
101
|
# Data table (contains individual loss answers)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
for i in range(self.num_samples):
|
|
97
|
-
answers.append(self.answers[f"loss_{i}"])
|
|
102
|
+
loss_answers, overall_answer = self._build_loss_answers()
|
|
103
|
+
body.add_element(self._create_data_table(loss_answers))
|
|
104
|
+
answers.extend(loss_answers)
|
|
98
105
|
|
|
99
106
|
# Overall loss question
|
|
100
107
|
body.add_element(ca.Paragraph([
|
|
101
108
|
f"Overall {self._get_loss_function_short_name()}: "
|
|
102
109
|
]))
|
|
103
|
-
answers.append(
|
|
104
|
-
body.add_element(
|
|
110
|
+
answers.append(overall_answer)
|
|
111
|
+
body.add_element(overall_answer)
|
|
105
112
|
|
|
106
113
|
return body, answers
|
|
107
114
|
|
|
108
|
-
def get_body(self, **kwargs) -> ca.Element:
|
|
109
|
-
"""Build question body (backward compatible interface)."""
|
|
110
|
-
body, _ = self._get_body(**kwargs)
|
|
111
|
-
return body
|
|
112
|
-
|
|
113
115
|
@abc.abstractmethod
|
|
114
|
-
def _create_data_table(self) -> ca.Element:
|
|
116
|
+
def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
|
|
115
117
|
"""Create the data table with answer fields."""
|
|
116
118
|
pass
|
|
117
119
|
|
|
118
|
-
def
|
|
120
|
+
def _build_explanation(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
119
121
|
"""Build question explanation."""
|
|
120
122
|
explanation = ca.Section()
|
|
121
123
|
|
|
@@ -137,11 +139,6 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
137
139
|
|
|
138
140
|
return explanation, []
|
|
139
141
|
|
|
140
|
-
def get_explanation(self, **kwargs) -> ca.Element:
|
|
141
|
-
"""Build question explanation (backward compatible interface)."""
|
|
142
|
-
explanation, _ = self._get_explanation(**kwargs)
|
|
143
|
-
return explanation
|
|
144
|
-
|
|
145
142
|
@abc.abstractmethod
|
|
146
143
|
def _create_calculation_steps(self) -> ca.Element:
|
|
147
144
|
"""Create step-by-step calculation explanations."""
|
|
@@ -167,6 +164,11 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
167
164
|
self.num_output_vars = max(1, min(5, self.num_output_vars)) # Constrain to 1-5 range
|
|
168
165
|
super().__init__(*args, **kwargs)
|
|
169
166
|
|
|
167
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
168
|
+
if "num_output_vars" in kwargs:
|
|
169
|
+
self.num_output_vars = max(1, min(5, kwargs.get("num_output_vars", self.num_output_vars)))
|
|
170
|
+
return super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
171
|
+
|
|
170
172
|
def _generate_data(self):
|
|
171
173
|
"""Generate regression data with continuous target values."""
|
|
172
174
|
self.data = []
|
|
@@ -225,7 +227,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
225
227
|
else:
|
|
226
228
|
return r"L(\mathbf{y}, \mathbf{p}) = \sum_{i=1}^{k} (y_i - p_i)^2"
|
|
227
229
|
|
|
228
|
-
def _create_data_table(self) -> ca.Element:
|
|
230
|
+
def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
|
|
229
231
|
"""Create table with input features, true values, predictions, and loss fields."""
|
|
230
232
|
headers = ["x"]
|
|
231
233
|
|
|
@@ -262,7 +264,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
262
264
|
row[f"p_{j}"] = f"{sample['predictions'][j]:.2f}"
|
|
263
265
|
|
|
264
266
|
# Loss answer field
|
|
265
|
-
row["loss"] =
|
|
267
|
+
row["loss"] = loss_answers[i]
|
|
266
268
|
|
|
267
269
|
rows.append(row)
|
|
268
270
|
|
|
@@ -416,7 +418,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
416
418
|
def _get_loss_function_formula(self) -> str:
|
|
417
419
|
return r"L(y, p) = -[y \ln(p) + (1-y) \ln(1-p)]"
|
|
418
420
|
|
|
419
|
-
def _create_data_table(self) -> ca.Element:
|
|
421
|
+
def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
|
|
420
422
|
"""Create table with features, true labels, predicted probabilities, and loss fields."""
|
|
421
423
|
headers = ["x", "y", "p", "loss"]
|
|
422
424
|
|
|
@@ -435,7 +437,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
435
437
|
row["p"] = f"{sample['predictions']:.3f}"
|
|
436
438
|
|
|
437
439
|
# Loss answer field
|
|
438
|
-
row["loss"] =
|
|
440
|
+
row["loss"] = loss_answers[i]
|
|
439
441
|
|
|
440
442
|
rows.append(row)
|
|
441
443
|
|
|
@@ -511,6 +513,11 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
511
513
|
self.num_classes = max(3, min(5, self.num_classes)) # Constrain to 3-5 classes
|
|
512
514
|
super().__init__(*args, **kwargs)
|
|
513
515
|
|
|
516
|
+
def _build_context(self, *, rng_seed=None, **kwargs):
|
|
517
|
+
if "num_classes" in kwargs:
|
|
518
|
+
self.num_classes = max(3, min(5, kwargs.get("num_classes", self.num_classes)))
|
|
519
|
+
return super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
520
|
+
|
|
514
521
|
def _generate_data(self):
|
|
515
522
|
"""Generate multi-class classification data."""
|
|
516
523
|
self.data = []
|
|
@@ -560,7 +567,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
560
567
|
def _get_loss_function_formula(self) -> str:
|
|
561
568
|
return r"L(\mathbf{y}, \mathbf{p}) = -\sum_{i=1}^{K} y_i \ln(p_i)"
|
|
562
569
|
|
|
563
|
-
def _create_data_table(self) -> ca.Element:
|
|
570
|
+
def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
|
|
564
571
|
"""Create table with features, true class vectors, predicted probabilities, and loss fields."""
|
|
565
572
|
headers = ["x", "y", "p", "loss"]
|
|
566
573
|
|
|
@@ -581,7 +588,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
581
588
|
row["p"] = p_vector
|
|
582
589
|
|
|
583
590
|
# Loss answer field
|
|
584
|
-
row["loss"] =
|
|
591
|
+
row["loss"] = loss_answers[i]
|
|
585
592
|
|
|
586
593
|
rows.append(row)
|
|
587
594
|
|
|
@@ -666,4 +673,4 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
666
673
|
|
|
667
674
|
explanation.add_element(ca.Equation(calculation, inline=False))
|
|
668
675
|
|
|
669
|
-
return explanation
|
|
676
|
+
return explanation
|