QuizGenerator 0.1.3__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.
- QuizGenerator/canvas/__init__.py +2 -2
- QuizGenerator/canvas/canvas_interface.py +6 -1
- QuizGenerator/contentast.py +425 -279
- QuizGenerator/generate.py +18 -1
- QuizGenerator/misc.py +122 -23
- QuizGenerator/mixins.py +10 -1
- QuizGenerator/premade_questions/cst334/memory_questions.py +6 -4
- QuizGenerator/premade_questions/cst334/process.py +1 -2
- QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
- QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
- QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
- QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
- QuizGenerator/premade_questions/cst463/models/text.py +201 -0
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +138 -94
- QuizGenerator/question.py +25 -11
- QuizGenerator/quiz.py +0 -1
- {quizgenerator-0.1.3.dist-info → quizgenerator-0.3.0.dist-info}/METADATA +3 -1
- {quizgenerator-0.1.3.dist-info → quizgenerator-0.3.0.dist-info}/RECORD +23 -16
- {quizgenerator-0.1.3.dist-info → quizgenerator-0.3.0.dist-info}/WHEEL +1 -1
- {quizgenerator-0.1.3.dist-info → quizgenerator-0.3.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.1.3.dist-info → quizgenerator-0.3.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/generate.py
CHANGED
|
@@ -25,6 +25,8 @@ def parse_args():
|
|
|
25
25
|
default=os.path.join(Path.home(), '.env'),
|
|
26
26
|
help="Path to .env file specifying canvas details"
|
|
27
27
|
)
|
|
28
|
+
|
|
29
|
+
parser.add_argument("--debug", action="store_true", help="Set logging level to debug")
|
|
28
30
|
|
|
29
31
|
parser.add_argument("--quiz_yaml", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_files/exam_generation.yaml"))
|
|
30
32
|
parser.add_argument("--seed", type=int, default=None,
|
|
@@ -211,6 +213,21 @@ def main():
|
|
|
211
213
|
|
|
212
214
|
# Load environment variables
|
|
213
215
|
load_dotenv(args.env)
|
|
216
|
+
|
|
217
|
+
if args.debug:
|
|
218
|
+
# Set root logger to DEBUG
|
|
219
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
220
|
+
|
|
221
|
+
# Set all handlers to DEBUG level
|
|
222
|
+
for handler in logging.getLogger().handlers:
|
|
223
|
+
handler.setLevel(logging.DEBUG)
|
|
224
|
+
|
|
225
|
+
# Set named loggers to DEBUG
|
|
226
|
+
for logger_name in ['QuizGenerator', 'lms_interface', '__main__']:
|
|
227
|
+
logger = logging.getLogger(logger_name)
|
|
228
|
+
logger.setLevel(logging.DEBUG)
|
|
229
|
+
for handler in logger.handlers:
|
|
230
|
+
handler.setLevel(logging.DEBUG)
|
|
214
231
|
|
|
215
232
|
if args.command == "TEST":
|
|
216
233
|
test()
|
|
@@ -233,4 +250,4 @@ def main():
|
|
|
233
250
|
|
|
234
251
|
|
|
235
252
|
if __name__ == "__main__":
|
|
236
|
-
main()
|
|
253
|
+
main()
|
QuizGenerator/misc.py
CHANGED
|
@@ -6,16 +6,21 @@ import enum
|
|
|
6
6
|
import itertools
|
|
7
7
|
import logging
|
|
8
8
|
import math
|
|
9
|
-
|
|
9
|
+
import numpy as np
|
|
10
|
+
from typing import List, Dict, Tuple, Any
|
|
10
11
|
|
|
11
12
|
import fractions
|
|
12
13
|
|
|
14
|
+
from QuizGenerator.contentast import ContentAST
|
|
15
|
+
|
|
13
16
|
log = logging.getLogger(__name__)
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
def fix_negative_zero(value):
|
|
20
|
+
"""Convert -0.0 to 0.0 to avoid confusing display."""
|
|
21
|
+
if isinstance(value, (int, float)):
|
|
22
|
+
return 0.0 if value == 0 else value
|
|
23
|
+
return value
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
class Answer:
|
|
@@ -23,11 +28,12 @@ class Answer:
|
|
|
23
28
|
|
|
24
29
|
class AnswerKind(enum.Enum):
|
|
25
30
|
BLANK = "fill_in_multiple_blanks_question"
|
|
26
|
-
MULTIPLE_ANSWER = "multiple_answers_question"
|
|
31
|
+
MULTIPLE_ANSWER = "multiple_answers_question"
|
|
27
32
|
ESSAY = "essay_question"
|
|
28
33
|
MULTIPLE_DROPDOWN = "multiple_dropdowns_question"
|
|
34
|
+
NUMERICAL_QUESTION = "numerical_question" # note: these can only be single answers as far as I can tell
|
|
29
35
|
|
|
30
|
-
class VariableKind(enum.Enum):
|
|
36
|
+
class VariableKind(enum.Enum):
|
|
31
37
|
STR = enum.auto()
|
|
32
38
|
INT = enum.auto()
|
|
33
39
|
FLOAT = enum.auto()
|
|
@@ -37,6 +43,7 @@ class Answer:
|
|
|
37
43
|
AUTOFLOAT = enum.auto()
|
|
38
44
|
LIST = enum.auto()
|
|
39
45
|
VECTOR = enum.auto()
|
|
46
|
+
MATRIX = enum.auto()
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
def __init__(
|
|
@@ -66,7 +73,7 @@ class Answer:
|
|
|
66
73
|
self.baffles = baffles
|
|
67
74
|
self.pdf_only = pdf_only
|
|
68
75
|
|
|
69
|
-
def get_for_canvas(self) -> List[Dict]:
|
|
76
|
+
def get_for_canvas(self, single_answer=False) -> List[Dict]:
|
|
70
77
|
# If this answer is marked as PDF-only, don't send it to Canvas
|
|
71
78
|
if self.pdf_only:
|
|
72
79
|
return []
|
|
@@ -131,18 +138,29 @@ class Answer:
|
|
|
131
138
|
Answer.VariableKind.FLOAT,
|
|
132
139
|
Answer.VariableKind.INT
|
|
133
140
|
]:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
if single_answer:
|
|
142
|
+
canvas_answers = [
|
|
143
|
+
{
|
|
144
|
+
"numerical_answer_type": "exact_answer",
|
|
145
|
+
"answer_text": round(self.value, self.DEFAULT_ROUNDING_DIGITS),
|
|
146
|
+
"answer_exact": round(self.value, self.DEFAULT_ROUNDING_DIGITS),
|
|
147
|
+
"answer_error_margin": 0.1,
|
|
148
|
+
"answer_weight": 100 if self.correct else 0,
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
else:
|
|
152
|
+
# Use the accepted_strings helper with settings that match the original AUTOFLOAT behavior
|
|
153
|
+
answer_strings = self.__class__.accepted_strings(
|
|
154
|
+
self.value,
|
|
155
|
+
allow_integer=True,
|
|
156
|
+
allow_simple_fraction=True,
|
|
157
|
+
max_denominator=3*4*5, # For process questions, these are the numbers of jobs we'd have
|
|
158
|
+
allow_mixed=True,
|
|
159
|
+
include_spaces=False,
|
|
160
|
+
include_fixed_even_if_integer=True
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
canvas_answers = [
|
|
146
164
|
{
|
|
147
165
|
"blank_id": self.key,
|
|
148
166
|
"answer_text": answer_string,
|
|
@@ -188,6 +206,7 @@ class Answer:
|
|
|
188
206
|
}
|
|
189
207
|
for possible_state in [self.value] #itertools.permutations(self.value)
|
|
190
208
|
]
|
|
209
|
+
|
|
191
210
|
else:
|
|
192
211
|
# For string answers, check if value is a list of acceptable alternatives
|
|
193
212
|
if isinstance(self.value, list):
|
|
@@ -244,13 +263,16 @@ class Answer:
|
|
|
244
263
|
|
|
245
264
|
elif self.variable_kind == Answer.VariableKind.AUTOFLOAT:
|
|
246
265
|
# Round to default precision for readability
|
|
247
|
-
|
|
266
|
+
rounded = round(self.value, self.DEFAULT_ROUNDING_DIGITS)
|
|
267
|
+
return f"{fix_negative_zero(rounded)}"
|
|
248
268
|
|
|
249
269
|
elif self.variable_kind == Answer.VariableKind.FLOAT:
|
|
250
270
|
# Round to default precision
|
|
251
271
|
if isinstance(self.value, (list, tuple)):
|
|
252
|
-
|
|
253
|
-
|
|
272
|
+
rounded = round(self.value[0], self.DEFAULT_ROUNDING_DIGITS)
|
|
273
|
+
return f"{fix_negative_zero(rounded)}"
|
|
274
|
+
rounded = round(self.value, self.DEFAULT_ROUNDING_DIGITS)
|
|
275
|
+
return f"{fix_negative_zero(rounded)}"
|
|
254
276
|
|
|
255
277
|
elif self.variable_kind == Answer.VariableKind.INT:
|
|
256
278
|
return str(int(self.value))
|
|
@@ -260,12 +282,17 @@ class Answer:
|
|
|
260
282
|
|
|
261
283
|
elif self.variable_kind == Answer.VariableKind.VECTOR:
|
|
262
284
|
# Format as comma-separated rounded values
|
|
263
|
-
return ", ".join(str(round(v, self.DEFAULT_ROUNDING_DIGITS)) for v in self.value)
|
|
285
|
+
return ", ".join(str(fix_negative_zero(round(v, self.DEFAULT_ROUNDING_DIGITS))) for v in self.value)
|
|
264
286
|
|
|
265
287
|
else:
|
|
266
288
|
# Default: use display or value
|
|
267
289
|
return str(self.display if hasattr(self, 'display') else self.value)
|
|
268
290
|
|
|
291
|
+
def get_ast_element(self, label=None):
|
|
292
|
+
from QuizGenerator.contentast import ContentAST
|
|
293
|
+
|
|
294
|
+
return ContentAST.Answer(answer=self, label=label) # todo fix label
|
|
295
|
+
|
|
269
296
|
# Factory methods for common answer types
|
|
270
297
|
@classmethod
|
|
271
298
|
def binary_hex(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
@@ -392,9 +419,18 @@ class Answer:
|
|
|
392
419
|
**kwargs
|
|
393
420
|
)
|
|
394
421
|
|
|
422
|
+
@classmethod
|
|
423
|
+
def matrix(cls, key: str, value: np.array|List, **kwargs ):
|
|
424
|
+
return MatrixAnswer(
|
|
425
|
+
key=key,
|
|
426
|
+
value=value,
|
|
427
|
+
variable_kind=cls.VariableKind.MATRIX
|
|
428
|
+
)
|
|
429
|
+
|
|
395
430
|
@staticmethod
|
|
396
431
|
def _to_fraction(x):
|
|
397
432
|
"""Convert int/float/decimal.Decimal/fractions.Fraction/str('a/b' or decimal) to fractions.Fraction exactly."""
|
|
433
|
+
log.debug(f"x: {x} {x.__class__}")
|
|
398
434
|
if isinstance(x, fractions.Fraction):
|
|
399
435
|
return x
|
|
400
436
|
if isinstance(x, int):
|
|
@@ -477,4 +513,67 @@ class Answer:
|
|
|
477
513
|
outs.add(f"{sign}{whole} {rem}/{b}")
|
|
478
514
|
|
|
479
515
|
return sorted(outs, key=lambda s: (len(s), s))
|
|
516
|
+
|
|
480
517
|
|
|
518
|
+
class MatrixAnswer(Answer):
|
|
519
|
+
def get_for_canvas(self, single_answer=False) -> List[Dict]:
|
|
520
|
+
canvas_answers = []
|
|
521
|
+
|
|
522
|
+
"""
|
|
523
|
+
The core idea is that we will walk through and generate each one for X_i,j
|
|
524
|
+
|
|
525
|
+
The big remaining question is how we will get all these names to the outside world.
|
|
526
|
+
It might have to be a pretty big challenge, or rather re-write.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
# The core idea is that we will be walking through and generating a per-index set of answers.
|
|
530
|
+
# Boy will this get messy. Poor canvas.
|
|
531
|
+
for i, j in np.ndindex(self.value.shape):
|
|
532
|
+
entry_strings = self.__class__.accepted_strings(
|
|
533
|
+
self.value[i,j],
|
|
534
|
+
allow_integer=True,
|
|
535
|
+
allow_simple_fraction=True,
|
|
536
|
+
max_denominator=3 * 4 * 5,
|
|
537
|
+
allow_mixed=True,
|
|
538
|
+
include_spaces=False,
|
|
539
|
+
include_fixed_even_if_integer=True
|
|
540
|
+
)
|
|
541
|
+
canvas_answers.extend(
|
|
542
|
+
[
|
|
543
|
+
{
|
|
544
|
+
"blank_id": f"{self.key}_{i}_{j}", # Give each an index associated with it so we can track it
|
|
545
|
+
"answer_text": answer_string,
|
|
546
|
+
"answer_weight": 100 if self.correct else 0,
|
|
547
|
+
}
|
|
548
|
+
for answer_string in entry_strings
|
|
549
|
+
]
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return canvas_answers
|
|
553
|
+
|
|
554
|
+
def get_ast_element(self, label=None):
|
|
555
|
+
from QuizGenerator.contentast import ContentAST
|
|
556
|
+
|
|
557
|
+
log.debug(f"self.value: {self.value}")
|
|
558
|
+
|
|
559
|
+
data = [
|
|
560
|
+
[
|
|
561
|
+
ContentAST.Answer(
|
|
562
|
+
Answer.float_value(
|
|
563
|
+
key=f"{self.key}_{i}_{j}",
|
|
564
|
+
value=self.value[i,j]
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
for i in range(self.value.shape[0])
|
|
568
|
+
]
|
|
569
|
+
for j in range(self.value.shape[1])
|
|
570
|
+
]
|
|
571
|
+
table = ContentAST.Table(data)
|
|
572
|
+
|
|
573
|
+
if label is not None:
|
|
574
|
+
return ContentAST.Container([
|
|
575
|
+
ContentAST.Text(f"{label} = "),
|
|
576
|
+
table
|
|
577
|
+
])
|
|
578
|
+
else:
|
|
579
|
+
return table
|
QuizGenerator/mixins.py
CHANGED
|
@@ -32,8 +32,17 @@ class TableQuestionMixin:
|
|
|
32
32
|
Returns:
|
|
33
33
|
ContentAST.Table with the information formatted
|
|
34
34
|
"""
|
|
35
|
+
# Don't convert ContentAST elements to strings - let them render properly
|
|
36
|
+
table_data = []
|
|
37
|
+
for key, value in info_dict.items():
|
|
38
|
+
# Keep ContentAST elements as-is, convert others to strings
|
|
39
|
+
if isinstance(value, ContentAST.Element):
|
|
40
|
+
table_data.append([key, value])
|
|
41
|
+
else:
|
|
42
|
+
table_data.append([key, str(value)])
|
|
43
|
+
|
|
35
44
|
return ContentAST.Table(
|
|
36
|
-
data=
|
|
45
|
+
data=table_data,
|
|
37
46
|
transpose=transpose
|
|
38
47
|
)
|
|
39
48
|
|
|
@@ -190,6 +190,8 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
|
|
|
190
190
|
self.num_elements = self.config_params.get("num_elements", 5)
|
|
191
191
|
self.cache_size = self.config_params.get("cache_size", 3)
|
|
192
192
|
self.num_requests = self.config_params.get("num_requests", 10)
|
|
193
|
+
|
|
194
|
+
self.hit_rate = 0. # placeholder
|
|
193
195
|
|
|
194
196
|
def refresh(self, previous: Optional[CachingQuestion] = None, *args, hard_refresh: bool = False, **kwargs):
|
|
195
197
|
# Call parent refresh which seeds RNG and calls is_interesting()
|
|
@@ -200,7 +202,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
|
|
|
200
202
|
self.cache_policy = self.get_choice('policy', self.Kind)
|
|
201
203
|
|
|
202
204
|
self.requests = (
|
|
203
|
-
list(range(self.cache_size)) # Prime the cache with the
|
|
205
|
+
list(range(self.cache_size)) # Prime the cache with the capacity misses
|
|
204
206
|
+ self.rng.choices(
|
|
205
207
|
population=list(range(self.cache_size - 1)), k=1
|
|
206
208
|
) # Add in one request to an earlier that will differentiate clearly between FIFO and LRU
|
|
@@ -274,7 +276,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
|
|
|
274
276
|
hit_rate_block = ContentAST.AnswerBlock(
|
|
275
277
|
ContentAST.Answer(
|
|
276
278
|
answer=self.answers["answer__hit_rate"],
|
|
277
|
-
label=f"Hit rate, excluding
|
|
279
|
+
label=f"Hit rate, excluding capacity misses. If appropriate, round to {Answer.DEFAULT_ROUNDING_DIGITS} decimal digits.",
|
|
278
280
|
unit="%"
|
|
279
281
|
)
|
|
280
282
|
)
|
|
@@ -324,7 +326,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
|
|
|
324
326
|
"To calculate the hit rate we calculate the percentage of requests "
|
|
325
327
|
"that were cache hits out of the total number of requests. "
|
|
326
328
|
f"In this case we are counting only all but {self.cache_size} requests, "
|
|
327
|
-
f"since we are excluding
|
|
329
|
+
f"since we are excluding capacity misses."
|
|
328
330
|
]
|
|
329
331
|
)
|
|
330
332
|
)
|
|
@@ -636,7 +638,7 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
|
|
|
636
638
|
f"Since we are in the {self.segment} segment, "
|
|
637
639
|
f"we see from our table that our bounds are {self.bounds[self.segment]}. "
|
|
638
640
|
f"Remember that our check for our {self.segment} segment is: ",
|
|
639
|
-
f"`if (offset
|
|
641
|
+
f"`if (offset >= bounds({self.segment})) : INVALID`",
|
|
640
642
|
"which becomes"
|
|
641
643
|
f"`if ({self.offset:0b} > {self.bounds[self.segment]:0b}) : INVALID`"
|
|
642
644
|
]
|
|
@@ -15,7 +15,6 @@ from typing import List
|
|
|
15
15
|
|
|
16
16
|
import matplotlib.pyplot as plt
|
|
17
17
|
|
|
18
|
-
from QuizGenerator.misc import OutputFormat
|
|
19
18
|
from QuizGenerator.contentast import ContentAST
|
|
20
19
|
from QuizGenerator.question import Question, Answer, QuestionRegistry, RegenerableChoiceMixin
|
|
21
20
|
from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
|
|
@@ -380,7 +379,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
380
379
|
# Return whether this workload is interesting
|
|
381
380
|
return self.is_interesting()
|
|
382
381
|
|
|
383
|
-
def get_body(self,
|
|
382
|
+
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
384
383
|
# Create table data for scheduling results
|
|
385
384
|
table_rows = []
|
|
386
385
|
for job_id in sorted(self.job_stats.keys()):
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
import keras
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from QuizGenerator.misc import MatrixAnswer
|
|
8
|
+
from QuizGenerator.question import Question, QuestionRegistry, Answer
|
|
9
|
+
from QuizGenerator.contentast import ContentAST
|
|
10
|
+
from QuizGenerator.constants import MathRanges
|
|
11
|
+
from QuizGenerator.mixins import TableQuestionMixin
|
|
12
|
+
|
|
13
|
+
from .matrices import MatrixQuestion
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@QuestionRegistry.register("cst463.attention.forward-pass")
|
|
19
|
+
class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def simple_attention(Q, K, V):
|
|
23
|
+
"""
|
|
24
|
+
Q: (seq_len, d_k) - queries
|
|
25
|
+
K: (seq_len, d_k) - keys
|
|
26
|
+
V: (seq_len, d_v) - values
|
|
27
|
+
|
|
28
|
+
Returns: (seq_len, d_v) - attended output
|
|
29
|
+
"""
|
|
30
|
+
d_k = Q.shape[1]
|
|
31
|
+
|
|
32
|
+
# Compute attention scores
|
|
33
|
+
scores = Q @ K.T / np.sqrt(d_k)
|
|
34
|
+
|
|
35
|
+
# Softmax to get weights
|
|
36
|
+
attention_weights = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True)
|
|
37
|
+
|
|
38
|
+
# Weighted sum of values
|
|
39
|
+
output = attention_weights @ V
|
|
40
|
+
|
|
41
|
+
return output, attention_weights
|
|
42
|
+
|
|
43
|
+
def refresh(self, *args, **kwargs):
|
|
44
|
+
super().refresh(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
seq_len = kwargs.get("seq_len", 3)
|
|
47
|
+
d_k = kwargs.get("key_dimension", 1) # key/query dimension
|
|
48
|
+
d_v = kwargs.get("value_dimension", 1) # value dimension
|
|
49
|
+
|
|
50
|
+
# Small integer matrices
|
|
51
|
+
self.Q = self.rng.randint(0, 3, size=(seq_len, d_k))
|
|
52
|
+
self.K = self.rng.randint(0, 3, size=(seq_len, d_k))
|
|
53
|
+
self.V = self.rng.randint(0, 3, size=(seq_len, d_v))
|
|
54
|
+
|
|
55
|
+
self.Q = self.get_rounded_matrix((seq_len, d_k), 0, 3)
|
|
56
|
+
self.K = self.get_rounded_matrix((seq_len, d_k), 0, 3)
|
|
57
|
+
self.V = self.get_rounded_matrix((seq_len, d_v), 0, 3)
|
|
58
|
+
|
|
59
|
+
self.output, self.weights = self.simple_attention(self.Q, self.K, self.V)
|
|
60
|
+
|
|
61
|
+
## Answers:
|
|
62
|
+
# Q, K, V, output, weights
|
|
63
|
+
|
|
64
|
+
self.answers["weights"] = MatrixAnswer("weights", self.output)
|
|
65
|
+
self.answers["output"] = MatrixAnswer("output", self.output)
|
|
66
|
+
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
70
|
+
body = ContentAST.Section()
|
|
71
|
+
|
|
72
|
+
body.add_element(
|
|
73
|
+
ContentAST.Text("Given the below information about a self attention layer, please calculate the output sequence.")
|
|
74
|
+
)
|
|
75
|
+
body.add_element(
|
|
76
|
+
self.create_info_table(
|
|
77
|
+
{
|
|
78
|
+
"Q": ContentAST.Matrix(self.Q),
|
|
79
|
+
"K": ContentAST.Matrix(self.K),
|
|
80
|
+
"V": ContentAST.Matrix(self.V),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
body.add_elements([
|
|
86
|
+
ContentAST.LineBreak(),
|
|
87
|
+
self.answers["weights"].get_ast_element(label=f"Weights"),
|
|
88
|
+
ContentAST.LineBreak(),
|
|
89
|
+
self.answers["output"].get_ast_element(label=f"Output"),
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
return body
|
|
93
|
+
|
|
94
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
95
|
+
explanation = ContentAST.Section()
|
|
96
|
+
digits = Answer.DEFAULT_ROUNDING_DIGITS
|
|
97
|
+
|
|
98
|
+
explanation.add_element(
|
|
99
|
+
ContentAST.Paragraph([
|
|
100
|
+
"Self-attention uses scaled dot-product attention to compute a weighted combination of values based on query-key similarity."
|
|
101
|
+
])
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Step 1: Compute attention scores
|
|
105
|
+
explanation.add_element(
|
|
106
|
+
ContentAST.Paragraph([
|
|
107
|
+
ContentAST.Text("Step 1: Compute attention scores", emphasis=True)
|
|
108
|
+
])
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
d_k = self.Q.shape[1]
|
|
112
|
+
explanation.add_element(
|
|
113
|
+
ContentAST.Equation(f"\\text{{scores}} = \\frac{{Q K^T}}{{\\sqrt{{d_k}}}} = \\frac{{Q K^T}}{{\\sqrt{{{d_k}}}}}")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
scores = self.Q @ self.K.T / np.sqrt(d_k)
|
|
117
|
+
|
|
118
|
+
explanation.add_element(
|
|
119
|
+
ContentAST.Paragraph([
|
|
120
|
+
"Raw scores (scaling by ",
|
|
121
|
+
ContentAST.Equation(f'\\sqrt{{{d_k}}}', inline=True),
|
|
122
|
+
" prevents extremely large values):"
|
|
123
|
+
])
|
|
124
|
+
)
|
|
125
|
+
explanation.add_element(ContentAST.Matrix(np.round(scores, digits)))
|
|
126
|
+
|
|
127
|
+
# Step 2: Apply softmax
|
|
128
|
+
explanation.add_element(
|
|
129
|
+
ContentAST.Paragraph([
|
|
130
|
+
ContentAST.Text("Step 2: Apply softmax to get attention weights", emphasis=True)
|
|
131
|
+
])
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
explanation.add_element(
|
|
135
|
+
ContentAST.Equation(r"\alpha_{ij} = \frac{\exp(\text{score}_{ij})}{\sum_k \exp(\text{score}_{ik})}")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Show ONE example row
|
|
139
|
+
explanation.add_element(
|
|
140
|
+
ContentAST.Paragraph([
|
|
141
|
+
"Example: Row 0 softmax computation"
|
|
142
|
+
])
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
row_scores = scores[0]
|
|
146
|
+
exp_scores = np.exp(row_scores)
|
|
147
|
+
sum_exp = exp_scores.sum()
|
|
148
|
+
weights_row = exp_scores / sum_exp
|
|
149
|
+
|
|
150
|
+
exp_terms = " + ".join([f"e^{{{s:.{digits}f}}}" for s in row_scores])
|
|
151
|
+
|
|
152
|
+
explanation.add_element(
|
|
153
|
+
ContentAST.Paragraph([
|
|
154
|
+
f"Denominator = {exp_terms} = {sum_exp:.{digits}f}"
|
|
155
|
+
])
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Format array with proper rounding
|
|
159
|
+
weights_str = "[" + ", ".join([f"{w:.{digits}f}" for w in weights_row]) + "]"
|
|
160
|
+
explanation.add_element(
|
|
161
|
+
ContentAST.Paragraph([
|
|
162
|
+
f"Resulting weights: {weights_str}"
|
|
163
|
+
])
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
explanation.add_element(
|
|
167
|
+
ContentAST.Paragraph([
|
|
168
|
+
"Complete attention weight matrix:"
|
|
169
|
+
])
|
|
170
|
+
)
|
|
171
|
+
explanation.add_element(ContentAST.Matrix(np.round(self.weights, digits)))
|
|
172
|
+
|
|
173
|
+
# Step 3: Weighted sum of values
|
|
174
|
+
explanation.add_element(
|
|
175
|
+
ContentAST.Paragraph([
|
|
176
|
+
ContentAST.Text("Step 3: Compute weighted sum of values", emphasis=True)
|
|
177
|
+
])
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
explanation.add_element(
|
|
181
|
+
ContentAST.Equation(r"\text{output} = \text{weights} \times V")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
explanation.add_element(
|
|
185
|
+
ContentAST.Paragraph([
|
|
186
|
+
"Final output:"
|
|
187
|
+
])
|
|
188
|
+
)
|
|
189
|
+
explanation.add_element(ContentAST.Matrix(np.round(self.output, digits)))
|
|
190
|
+
|
|
191
|
+
return explanation
|
|
192
|
+
|