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
@@ -49,29 +49,33 @@ class ConvolutionCalculation(MatrixQuestion):
49
49
 
50
50
  return output
51
51
 
52
- def refresh(self, *args, **kwargs):
53
- super().refresh(*args, **kwargs)
52
+ @classmethod
53
+ def _build_context(cls, *, rng_seed=None, **kwargs):
54
+ rng = cls.get_rng(rng_seed)
55
+ digits = cls.get_digits_to_round(digits_to_round=kwargs.get("digits_to_round"))
54
56
 
55
- # num_input_channels = 1
56
57
  input_size = kwargs.get("input_size", 4)
57
58
  num_filters = kwargs.get("num_filters", 1)
58
- self.stride = kwargs.get("stride", 1)
59
- self.padding = kwargs.get("padding", 0)
60
-
61
- # Small sizes for hand calculation
62
- self.image = self.get_rounded_matrix((input_size, input_size))
63
- self.kernel = self.get_rounded_matrix((3, 3, num_filters), -1, 1)
64
-
65
- self.result = self.conv2d_multi_channel(self.image, self.kernel, stride=self.stride, padding=self.padding)
66
-
67
- self.answers = {
68
- f"result_{i}" : ca.AnswerTypes.Matrix(self.result[:,:,i], label=f"Result of filter {i}")
69
- for i in range(self.result.shape[-1])
59
+ stride = kwargs.get("stride", 1)
60
+ padding = kwargs.get("padding", 0)
61
+
62
+ image = cls.get_rounded_matrix(rng, (input_size, input_size), digits_to_round=digits)
63
+ kernel = cls.get_rounded_matrix(rng, (3, 3, num_filters), -1, 1, digits)
64
+ result = cls.conv2d_multi_channel(image, kernel, stride=stride, padding=padding)
65
+
66
+ return {
67
+ "digits": digits,
68
+ "input_size": input_size,
69
+ "num_filters": num_filters,
70
+ "stride": stride,
71
+ "padding": padding,
72
+ "image": image,
73
+ "kernel": kernel,
74
+ "result": result,
70
75
  }
71
-
72
- return True
73
-
74
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
76
+
77
+ @classmethod
78
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
75
79
  """Build question body and collect answers."""
76
80
  body = ca.Section()
77
81
  answers = []
@@ -79,7 +83,7 @@ class ConvolutionCalculation(MatrixQuestion):
79
83
  body.add_elements(
80
84
  [
81
85
  ca.Text("Given image represented as matrix: "),
82
- ca.Matrix(self.image, name="image")
86
+ ca.Matrix(context["image"], name="image")
83
87
  ]
84
88
  )
85
89
 
@@ -87,38 +91,35 @@ class ConvolutionCalculation(MatrixQuestion):
87
91
  [
88
92
  ca.Text("And convolution filters: "),
89
93
  ] + [
90
- ca.Matrix(self.kernel[:, :, i], name=f"Filter {i}")
91
- for i in range(self.kernel.shape[-1])
94
+ ca.Matrix(context["kernel"][:, :, i], name=f"Filter {i}")
95
+ for i in range(context["kernel"].shape[-1])
92
96
  ]
93
97
  )
94
98
 
95
99
  body.add_element(
96
100
  ca.Paragraph(
97
101
  [
98
- f"Calculate the output of the convolution operation. Assume stride = {self.stride} and padding = {self.padding}."
102
+ f"Calculate the output of the convolution operation. Assume stride = {context['stride']} and padding = {context['padding']}."
99
103
  ]
100
104
  )
101
105
  )
102
106
 
103
107
  body.add_element(ca.LineBreak())
104
108
 
105
- for i in range(self.result.shape[-1]):
106
- answers.append(self.answers[f"result_{i}"])
109
+ for i in range(context["result"].shape[-1]):
110
+ answer = ca.AnswerTypes.Matrix(context["result"][:, :, i], label=f"Result of filter {i}")
111
+ answers.append(answer)
107
112
  body.add_elements([
108
113
  ca.Container([
109
- self.answers[f"result_{i}"],
114
+ answer,
110
115
  ca.LineBreak()
111
116
  ])
112
117
  ])
113
118
 
114
119
  return body, answers
115
120
 
116
- def get_body(self, **kwargs) -> ca.Section:
117
- """Build question body (backward compatible interface)."""
118
- body, _ = self._get_body(**kwargs)
119
- return body
120
-
121
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
121
+ @classmethod
122
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
122
123
  """Build question explanation."""
123
124
  explanation = ca.Section()
124
125
  digits = ca.Answer.DEFAULT_ROUNDING_DIGITS
@@ -131,14 +132,14 @@ class ConvolutionCalculation(MatrixQuestion):
131
132
 
132
133
  explanation.add_element(
133
134
  ca.Paragraph([
134
- f"With stride={self.stride} and padding={self.padding}: ",
135
+ f"With stride={context['stride']} and padding={context['padding']}: ",
135
136
  f"stride controls how many pixels the filter moves each step, ",
136
- f"and padding adds zeros around the border {'(no border in this case)' if self.padding == 0 else f'({self.padding} pixels)'}."
137
+ f"and padding adds zeros around the border {'(no border in this case)' if context['padding'] == 0 else f'({context['padding']} pixels)'}."
137
138
  ])
138
139
  )
139
140
 
140
141
  # For each filter, show one detailed example computation
141
- for f_idx in range(self.kernel.shape[-1]):
142
+ for f_idx in range(context["kernel"].shape[-1]):
142
143
  explanation.add_element(
143
144
  ca.Paragraph([
144
145
  ca.Text(f"Filter {f_idx}:", emphasis=True)
@@ -147,7 +148,7 @@ class ConvolutionCalculation(MatrixQuestion):
147
148
 
148
149
  # Show the filter (rounded)
149
150
  explanation.add_element(
150
- ca.Matrix(np.round(self.kernel[:, :, f_idx], digits), name=f"Filter {f_idx}")
151
+ ca.Matrix(np.round(context["kernel"][:, :, f_idx], digits), name=f"Filter {f_idx}")
151
152
  )
152
153
 
153
154
  # Show ONE example computation (position 0,0)
@@ -158,23 +159,23 @@ class ConvolutionCalculation(MatrixQuestion):
158
159
  )
159
160
 
160
161
  # Account for padding when extracting receptive field
161
- if self.padding > 0:
162
- padded_image = np.pad(self.image, ((self.padding, self.padding), (self.padding, self.padding)), mode='constant')
162
+ if context["padding"] > 0:
163
+ padded_image = np.pad(context["image"], ((context["padding"], context["padding"]), (context["padding"], context["padding"])), mode='constant')
163
164
  receptive_field = padded_image[0:3, 0:3]
164
165
  else:
165
- receptive_field = self.image[0:3, 0:3]
166
+ receptive_field = context["image"][0:3, 0:3]
166
167
 
167
168
  computation_steps = []
168
169
  for r in range(3):
169
170
  row_terms = []
170
171
  for c in range(3):
171
172
  img_val = receptive_field[r, c]
172
- kernel_val = self.kernel[r, c, f_idx]
173
+ kernel_val = context["kernel"][r, c, f_idx]
173
174
  row_terms.append(f"({img_val:.2f} \\times {kernel_val:.2f})")
174
175
  computation_steps.append(" + ".join(row_terms))
175
176
 
176
177
  equation_str = " + ".join(computation_steps)
177
- result_val = self.result[0, 0, f_idx]
178
+ result_val = context["result"][0, 0, f_idx]
178
179
 
179
180
  explanation.add_element(
180
181
  ca.Equation(f"{equation_str} = {result_val:.2f}")
@@ -187,12 +188,7 @@ class ConvolutionCalculation(MatrixQuestion):
187
188
  ])
188
189
  )
189
190
  explanation.add_element(
190
- ca.Matrix(np.round(self.result[:, :, f_idx], digits))
191
+ ca.Matrix(np.round(context["result"][:, :, f_idx], digits))
191
192
  )
192
193
 
193
194
  return explanation, []
194
-
195
- def get_explanation(self, **kwargs) -> ca.Section:
196
- """Build question explanation (backward compatible interface)."""
197
- explanation, _ = self._get_explanation(**kwargs)
198
- return explanation
@@ -7,18 +7,68 @@ from QuizGenerator.question import Question
7
7
 
8
8
 
9
9
  class MatrixQuestion(Question, abc.ABC):
10
- def __init__(self, *args, **kwargs):
11
- super().__init__(*args, **kwargs)
12
- self.default_digits_to_round = kwargs.get("digits_to_round", 2)
13
-
14
- def refresh(self, rng_seed=None, *args, **kwargs):
15
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
16
- self.rng = np.random.RandomState(rng_seed)
17
-
18
- def get_rounded_matrix(self, shape, low=0, high=1, digits_to_round=None):
10
+ default_digits_to_round = 2
11
+
12
+ @staticmethod
13
+ def get_rng(rng_seed):
14
+ return np.random.RandomState(rng_seed)
15
+
16
+ @classmethod
17
+ def get_digits_to_round(cls, *, digits_to_round=None):
19
18
  if digits_to_round is None:
20
- digits_to_round = self.default_digits_to_round
19
+ digits_to_round = cls.default_digits_to_round
20
+ return digits_to_round
21
+
22
+ @staticmethod
23
+ def get_rounded_matrix(*args, **kwargs):
24
+ # Supports (rng, shape, ...) or (self, shape, ...)
25
+ if not args:
26
+ raise ValueError("get_rounded_matrix requires at least a rng or self plus shape.")
27
+
28
+ if isinstance(args[0], MatrixQuestion):
29
+ self = args[0]
30
+ shape = args[1]
31
+ remaining = list(args[2:])
32
+ rng = kwargs.pop("rng", getattr(self, "_np_rng", np.random.RandomState()))
33
+ low = kwargs.pop("low", None)
34
+ high = kwargs.pop("high", None)
35
+ digits_to_round = kwargs.pop("digits_to_round", None)
36
+
37
+ if remaining:
38
+ low = remaining.pop(0)
39
+ if remaining:
40
+ high = remaining.pop(0)
41
+ if remaining:
42
+ digits_to_round = remaining.pop(0)
43
+
44
+ if low is None:
45
+ low = 0
46
+ if high is None:
47
+ high = 1
48
+ digits_to_round = self.get_digits_to_round(digits_to_round=digits_to_round)
49
+ else:
50
+ rng = args[0]
51
+ shape = args[1]
52
+ remaining = list(args[2:])
53
+ low = kwargs.pop("low", None)
54
+ high = kwargs.pop("high", None)
55
+ digits_to_round = kwargs.pop("digits_to_round", None)
56
+
57
+ if remaining:
58
+ low = remaining.pop(0)
59
+ if remaining:
60
+ high = remaining.pop(0)
61
+ if remaining:
62
+ digits_to_round = remaining.pop(0)
63
+
64
+ if low is None:
65
+ low = 0
66
+ if high is None:
67
+ high = 1
68
+ if digits_to_round is None:
69
+ digits_to_round = 2
70
+
21
71
  return np.round(
22
- (high - low) * self.rng.rand(*shape) + low,
72
+ (high - low) * rng.rand(*shape) + low,
23
73
  digits_to_round
24
74
  )
@@ -44,31 +44,38 @@ class RNNForwardPass(MatrixQuestion, TableQuestionMixin):
44
44
 
45
45
  return h_states
46
46
 
47
- def refresh(self, *args, **kwargs):
48
- super().refresh(*args, **kwargs)
49
- self.rng = np.random.RandomState(kwargs.get("rng_seed", None))
50
-
47
+ @classmethod
48
+ def _build_context(cls, *, rng_seed=None, **kwargs):
49
+ rng = cls.get_rng(rng_seed)
50
+ digits = cls.get_digits_to_round(digits_to_round=kwargs.get("digits_to_round"))
51
+
51
52
  seq_len = kwargs.get("seq_len", 3)
52
- input_dim = kwargs.get("input_dim", 1)
53
+ input_dim = kwargs.get("input_dim", 1)
53
54
  hidden_dim = kwargs.get("hidden_dim", 1)
54
-
55
- # Small integer weights for hand calculation
56
- self.x_seq = self.get_rounded_matrix((seq_len, input_dim)) # self.rng.randint(0, 3, size=(seq_len, input_dim))
57
- self.W_xh = self.get_rounded_matrix((input_dim, hidden_dim), -1, 2)
58
- self.W_hh = self.get_rounded_matrix((hidden_dim, hidden_dim), -1, 2)
59
- self.b_h = self.get_rounded_matrix((hidden_dim,), -1, 2)
60
- self.h_0 = np.zeros(hidden_dim)
61
-
62
- self.h_states = self.rnn_forward(self.x_seq, self.W_xh, self.W_hh, self.b_h, self.h_0) #.reshape((seq_len,-1))
63
-
64
- ## Answers:
65
- # x_seq, W_xh, W_hh, b_h, h_0, h_states
66
-
67
- self.answers["output_sequence"] = ca.AnswerTypes.Matrix(value=self.h_states, label="Hidden states")
68
-
69
- return True
70
-
71
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
55
+
56
+ x_seq = cls.get_rounded_matrix(rng, (seq_len, input_dim), digits_to_round=digits)
57
+ W_xh = cls.get_rounded_matrix(rng, (input_dim, hidden_dim), -1, 2, digits)
58
+ W_hh = cls.get_rounded_matrix(rng, (hidden_dim, hidden_dim), -1, 2, digits)
59
+ b_h = cls.get_rounded_matrix(rng, (hidden_dim,), -1, 2, digits)
60
+ h_0 = np.zeros(hidden_dim)
61
+
62
+ h_states = cls.rnn_forward(x_seq, W_xh, W_hh, b_h, h_0)
63
+
64
+ return {
65
+ "digits": digits,
66
+ "seq_len": seq_len,
67
+ "input_dim": input_dim,
68
+ "hidden_dim": hidden_dim,
69
+ "x_seq": x_seq,
70
+ "W_xh": W_xh,
71
+ "W_hh": W_hh,
72
+ "b_h": b_h,
73
+ "h_0": h_0,
74
+ "h_states": h_states,
75
+ }
76
+
77
+ @classmethod
78
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
72
79
  """Build question body and collect answers."""
73
80
  body = ca.Section()
74
81
  answers = []
@@ -80,30 +87,27 @@ class RNNForwardPass(MatrixQuestion, TableQuestionMixin):
80
87
  ])
81
88
  )
82
89
  body.add_element(
83
- self.create_info_table(
90
+ cls.create_info_table(
84
91
  {
85
- ca.Container(["Input sequence, ", ca.Equation("x_{seq}", inline=True)]) : ca.Matrix(self.x_seq),
86
- ca.Container(["Input weights, ", ca.Equation("W_{xh}", inline=True)]) : ca.Matrix(self.W_xh),
87
- ca.Container(["Hidden weights, ", ca.Equation("W_{hh}", inline=True)]) : ca.Matrix(self.W_hh),
88
- ca.Container(["Bias, ", ca.Equation("b_{h}", inline=True)]) : ca.Matrix(self.b_h),
89
- ca.Container(["Hidden states, ", ca.Equation("h_{0}", inline=True)]) : ca.Matrix(self.h_0),
92
+ ca.Container(["Input sequence, ", ca.Equation("x_{seq}", inline=True)]) : ca.Matrix(context["x_seq"]),
93
+ ca.Container(["Input weights, ", ca.Equation("W_{xh}", inline=True)]) : ca.Matrix(context["W_xh"]),
94
+ ca.Container(["Hidden weights, ", ca.Equation("W_{hh}", inline=True)]) : ca.Matrix(context["W_hh"]),
95
+ ca.Container(["Bias, ", ca.Equation("b_{h}", inline=True)]) : ca.Matrix(context["b_h"]),
96
+ ca.Container(["Hidden states, ", ca.Equation("h_{0}", inline=True)]) : ca.Matrix(context["h_0"]),
90
97
  }
91
98
  )
92
99
  )
93
100
 
94
101
  body.add_element(ca.LineBreak())
95
102
 
96
- answers.append(self.answers["output_sequence"])
97
- body.add_element(self.answers["output_sequence"])
103
+ output_answer = ca.AnswerTypes.Matrix(value=context["h_states"], label="Hidden states")
104
+ answers.append(output_answer)
105
+ body.add_element(output_answer)
98
106
 
99
107
  return body, answers
100
108
 
101
- def get_body(self, **kwargs) -> ca.Section:
102
- """Build question body (backward compatible interface)."""
103
- body, _ = self._get_body(**kwargs)
104
- return body
105
-
106
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
109
+ @classmethod
110
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
107
111
  """Build question explanation."""
108
112
  explanation = ca.Section()
109
113
  digits = ca.Answer.DEFAULT_ROUNDING_DIGITS
@@ -134,7 +138,7 @@ class RNNForwardPass(MatrixQuestion, TableQuestionMixin):
134
138
  return "[" + ", ".join([f"{fix_negative_zero(x):.{digits}f}" for x in arr.flatten()]) + "]"
135
139
 
136
140
  # Show detailed examples for first 2 timesteps (or just 1 if seq_len == 1)
137
- seq_len = len(self.x_seq)
141
+ seq_len = len(context["x_seq"])
138
142
  num_examples = min(2, seq_len)
139
143
 
140
144
  explanation.add_element(ca.Paragraph([""]))
@@ -147,18 +151,18 @@ class RNNForwardPass(MatrixQuestion, TableQuestionMixin):
147
151
  )
148
152
 
149
153
  # Compute step t
150
- x_contribution = self.x_seq[t] @ self.W_xh
154
+ x_contribution = context["x_seq"][t] @ context["W_xh"]
151
155
  if t == 0:
152
- h_prev = self.h_0
156
+ h_prev = context["h_0"]
153
157
  h_prev_label = 'h_{-1}'
154
158
  h_prev_desc = " (initial state)"
155
159
  else:
156
- h_prev = self.h_states[t-1]
160
+ h_prev = context["h_states"][t-1]
157
161
  h_prev_label = f'h_{{{t-1}}}'
158
162
  h_prev_desc = ""
159
163
 
160
- h_contribution = h_prev @ self.W_hh
161
- pre_activation = x_contribution + h_contribution + self.b_h
164
+ h_contribution = h_prev @ context["W_hh"]
165
+ pre_activation = x_contribution + h_contribution + context["b_h"]
162
166
  h_result = np.tanh(pre_activation)
163
167
 
164
168
  explanation.add_element(
@@ -203,13 +207,7 @@ class RNNForwardPass(MatrixQuestion, TableQuestionMixin):
203
207
  )
204
208
 
205
209
  explanation.add_element(
206
- ca.Matrix(np.round(self.h_states, digits))
210
+ ca.Matrix(np.round(context["h_states"], digits))
207
211
  )
208
212
 
209
213
  return explanation, []
210
-
211
- def get_explanation(self, **kwargs) -> ca.Section:
212
- """Build question explanation (backward compatible interface)."""
213
- explanation, _ = self._get_explanation(**kwargs)
214
- return explanation
215
-
@@ -34,73 +34,77 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
34
34
 
35
35
  return logits, probs
36
36
 
37
- def refresh(self, *args, **kwargs):
38
- super().refresh(*args, **kwargs)
39
- self.rng = np.random.RandomState(kwargs.get("rng_seed", None))
40
-
37
+ @classmethod
38
+ def _build_context(cls, *, rng_seed=None, **kwargs):
39
+ rng = cls.get_rng(rng_seed)
40
+ digits = cls.get_digits_to_round(digits_to_round=kwargs.get("digits_to_round"))
41
+
41
42
  embed_dim = kwargs.get("embed_dim", 3)
42
43
  num_contexts = kwargs.get("num_contexts", 3)
43
-
44
- # Vocabulary pool
45
- vocab = ['cat', 'dog', 'run', 'jump', 'happy', 'sad', 'tree', 'house',
46
- 'walk', 'sleep', 'fast', 'slow', 'big', 'small']
47
-
48
- # Sample words
49
- self.selected_words = self.rng.choice(vocab, size=num_contexts + 1, replace=False)
50
- self.center_word = self.selected_words[0]
51
- self.context_words = self.selected_words[1:]
52
-
53
- # Small integer embeddings
54
44
 
55
- self.center_emb = self.get_rounded_matrix((embed_dim,), -2, 3)
56
- self.context_embs = self.get_rounded_matrix((num_contexts, embed_dim), -2, 3)
57
-
58
- self.logits, self.probs = self.skipgram_predict(self.center_emb, self.context_embs)
59
-
60
- ## Answers:
61
- # center_word, center_emb, context_words, context_embs, logits, probs
62
- self.answers["logits"] = ca.AnswerTypes.Vector(self.logits, label="Logits")
63
- most_likely_idx = np.argmax(self.probs)
64
- most_likely_word = self.context_words[most_likely_idx]
65
- self.answers["center_word"] = ca.AnswerTypes.String(most_likely_word, label="Most likely context word")
66
-
67
-
68
- return True
69
-
70
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
45
+ vocab = [
46
+ 'cat', 'dog', 'run', 'jump', 'happy', 'sad', 'tree', 'house',
47
+ 'walk', 'sleep', 'fast', 'slow', 'big', 'small'
48
+ ]
49
+
50
+ selected_words = rng.choice(vocab, size=num_contexts + 1, replace=False)
51
+ center_word = selected_words[0]
52
+ context_words = selected_words[1:]
53
+
54
+ center_emb = cls.get_rounded_matrix(rng, (embed_dim,), -2, 3, digits)
55
+ context_embs = cls.get_rounded_matrix(rng, (num_contexts, embed_dim), -2, 3, digits)
56
+
57
+ logits, probs = cls.skipgram_predict(center_emb, context_embs)
58
+
59
+ most_likely_idx = np.argmax(probs)
60
+ most_likely_word = context_words[most_likely_idx]
61
+
62
+ return {
63
+ "digits": digits,
64
+ "embed_dim": embed_dim,
65
+ "num_contexts": num_contexts,
66
+ "center_word": center_word,
67
+ "context_words": context_words,
68
+ "center_emb": center_emb,
69
+ "context_embs": context_embs,
70
+ "logits": logits,
71
+ "probs": probs,
72
+ "most_likely_word": most_likely_word,
73
+ }
74
+
75
+ @classmethod
76
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
71
77
  """Build question body and collect answers."""
72
78
  body = ca.Section()
73
79
  answers = []
74
80
 
75
81
  body.add_element(
76
82
  ca.Paragraph([
77
- f"Given center word: `{self.center_word}` with embedding {self.center_emb}, compute the skip-gram probabilities for each context word and identify the most likely one."
83
+ f"Given center word: `{context['center_word']}` with embedding {context['center_emb']}, compute the skip-gram probabilities for each context word and identify the most likely one."
78
84
  ])
79
85
  )
80
86
  body.add_elements([
81
- ca.Paragraph([ca.Text(f"`{w}` : "), str(e)]) for w, e in zip(self.context_words, self.context_embs)
87
+ ca.Paragraph([ca.Text(f"`{w}` : "), str(e)]) for w, e in zip(context["context_words"], context["context_embs"])
82
88
  ])
83
89
 
84
- answers.append(self.answers["logits"])
85
- answers.append(self.answers["center_word"])
90
+ logits_answer = ca.AnswerTypes.Vector(context["logits"], label="Logits")
91
+ center_word_answer = ca.AnswerTypes.String(context["most_likely_word"], label="Most likely context word")
92
+ answers.append(logits_answer)
93
+ answers.append(center_word_answer)
86
94
  body.add_elements([
87
95
  ca.LineBreak(),
88
- self.answers["logits"],
96
+ logits_answer,
89
97
  ca.LineBreak(),
90
- self.answers["center_word"]
98
+ center_word_answer
91
99
  ])
92
100
 
93
- log.debug(f"output: {self.logits}")
94
- log.debug(f"weights: {self.probs}")
101
+ log.debug(f"output: {context['logits']}")
102
+ log.debug(f"weights: {context['probs']}")
95
103
 
96
104
  return body, answers
97
105
 
98
- def get_body(self, **kwargs) -> ca.Section:
99
- """Build question body (backward compatible interface)."""
100
- body, _ = self._get_body(**kwargs)
101
- return body
102
-
103
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
106
+ @classmethod
107
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
104
108
  """Build question explanation."""
105
109
  explanation = ca.Section()
106
110
  digits = ca.Answer.DEFAULT_ROUNDING_DIGITS
@@ -119,10 +123,10 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
119
123
  )
120
124
 
121
125
  # Format center embedding
122
- center_emb_str = "[" + ", ".join([f"{x:.{digits}f}" for x in self.center_emb]) + "]"
126
+ center_emb_str = "[" + ", ".join([f"{x:.{digits}f}" for x in context["center_emb"]]) + "]"
123
127
  explanation.add_element(
124
128
  ca.Paragraph([
125
- f"Center word `{self.center_word}`: {center_emb_str}"
129
+ f"Center word `{context['center_word']}`: {center_emb_str}"
126
130
  ])
127
131
  )
128
132
 
@@ -132,7 +136,7 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
132
136
  ])
133
137
  )
134
138
 
135
- for i, (word, emb) in enumerate(zip(self.context_words, self.context_embs)):
139
+ for i, (word, emb) in enumerate(zip(context["context_words"], context["context_embs"])):
136
140
  emb_str = "[" + ", ".join([f"{x:.2f}" for x in emb]) + "]"
137
141
  explanation.add_element(
138
142
  ca.Paragraph([
@@ -150,20 +154,20 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
150
154
  # Show ONE example
151
155
  explanation.add_element(
152
156
  ca.Paragraph([
153
- f"Example: Logit for `{self.context_words[0]}`"
157
+ f"Example: Logit for `{context['context_words'][0]}`"
154
158
  ])
155
159
  )
156
160
 
157
- context_emb = self.context_embs[0]
158
- dot_product_terms = " + ".join([f"({self.center_emb[j]:.2f} \\times {context_emb[j]:.2f})"
159
- for j in range(len(self.center_emb))])
160
- logit_val = self.logits[0]
161
+ context_emb = context["context_embs"][0]
162
+ dot_product_terms = " + ".join([f"({context['center_emb'][j]:.2f} \\times {context_emb[j]:.2f})"
163
+ for j in range(len(context["center_emb"]))])
164
+ logit_val = context["logits"][0]
161
165
 
162
166
  explanation.add_element(
163
167
  ca.Equation(f"{dot_product_terms} = {logit_val:.2f}")
164
168
  )
165
169
 
166
- logits_str = "[" + ", ".join([f"{x:.2f}" for x in self.logits]) + "]"
170
+ logits_str = "[" + ", ".join([f"{x:.2f}" for x in context["logits"]]) + "]"
167
171
  explanation.add_element(
168
172
  ca.Paragraph([
169
173
  f"All logits: {logits_str}"
@@ -177,10 +181,10 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
177
181
  ])
178
182
  )
179
183
 
180
- exp_logits = np.exp(self.logits)
184
+ exp_logits = np.exp(context["logits"])
181
185
  sum_exp = exp_logits.sum()
182
186
 
183
- exp_terms = " + ".join([f"e^{{{l:.{digits}f}}}" for l in self.logits])
187
+ exp_terms = " + ".join([f"e^{{{l:.{digits}f}}}" for l in context["logits"]])
184
188
 
185
189
  explanation.add_element(
186
190
  ca.Equation(f"\\text{{denominator}} = {exp_terms} = {sum_exp:.{digits}f}")
@@ -192,26 +196,20 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
192
196
  ])
193
197
  )
194
198
 
195
- for i, (word, prob) in enumerate(zip(self.context_words, self.probs)):
199
+ for i, (word, prob) in enumerate(zip(context["context_words"], context["probs"])):
196
200
  explanation.add_element(
197
- ca.Equation(f"P(\\text{{{word}}}) = \\frac{{e^{{{self.logits[i]:.{digits}f}}}}}{{{sum_exp:.{digits}f}}} = {prob:.{digits}f}")
201
+ ca.Equation(f"P(\\text{{{word}}}) = \\frac{{e^{{{context['logits'][i]:.{digits}f}}}}}{{{sum_exp:.{digits}f}}} = {prob:.{digits}f}")
198
202
  )
199
203
 
200
204
  # Step 4: Identify most likely
201
- most_likely_idx = np.argmax(self.probs)
202
- most_likely_word = self.context_words[most_likely_idx]
205
+ most_likely_idx = np.argmax(context["probs"])
206
+ most_likely_word = context["context_words"][most_likely_idx]
203
207
 
204
208
  explanation.add_element(
205
209
  ca.Paragraph([
206
210
  ca.Text("Conclusion:", emphasis=True),
207
- f" The most likely context word is `{most_likely_word}` with probability {self.probs[most_likely_idx]:.{digits}f}"
211
+ f" The most likely context word is `{most_likely_word}` with probability {context['probs'][most_likely_idx]:.{digits}f}"
208
212
  ])
209
213
  )
210
214
 
211
215
  return explanation, []
212
-
213
- def get_explanation(self, **kwargs) -> ca.Section:
214
- """Build question explanation (backward compatible interface)."""
215
- explanation, _ = self._get_explanation(**kwargs)
216
- return explanation
217
-