zeguru commited on
Commit
9cde5e3
·
verified ·
1 Parent(s): 6163a9f

Upload folder using huggingface_hub

Browse files
Files changed (48) hide show
  1. .gitignore +0 -0
  2. README.md +12 -16
  3. app.py +100 -52
  4. checkpoints/.gitkeep +0 -0
  5. checkpoints/tiny-sentence-transformer-no-position.pt +3 -0
  6. checkpoints/tiny-sentence-transformer.pt +3 -0
  7. checkpoints/ttt-autocomplete.pt +3 -0
  8. data/tiny_shakespear.txt +0 -0
  9. pyproject.toml +9 -0
  10. requirements.txt +2 -0
  11. src/__pycache__/generate.cpython-314.pyc +0 -0
  12. src/__pycache__/test_inference.cpython-314.pyc +0 -0
  13. src/__pycache__/train.cpython-314.pyc +0 -0
  14. src/generate.py +67 -0
  15. src/infra/__init__.py +0 -0
  16. src/infra/__pycache__/__init__.cpython-314.pyc +0 -0
  17. src/infra/__pycache__/dataset.cpython-314.pyc +0 -0
  18. src/infra/__pycache__/generation.cpython-314.pyc +0 -0
  19. src/infra/__pycache__/tokenizer.cpython-314.pyc +0 -0
  20. src/infra/dataset.py +204 -0
  21. src/infra/generation.py +101 -0
  22. src/infra/tokenizer.py +87 -0
  23. src/task/__init__.py +0 -0
  24. src/task/__pycache__/__init__.cpython-314.pyc +0 -0
  25. src/task/__pycache__/config.cpython-314.pyc +0 -0
  26. src/task/__pycache__/inference.cpython-314.pyc +0 -0
  27. src/task/__pycache__/model.cpython-314.pyc +0 -0
  28. src/task/__pycache__/training.cpython-314.pyc +0 -0
  29. src/task/config.py +22 -0
  30. src/task/inference.py +43 -0
  31. src/task/model.py +73 -0
  32. src/task/training.py +103 -0
  33. src/train.py +172 -0
  34. src/transformer/__pycache__/attention.cpython-314.pyc +0 -0
  35. src/transformer/__pycache__/embedding.cpython-314.pyc +0 -0
  36. src/transformer/__pycache__/feed_forward.cpython-314.pyc +0 -0
  37. src/transformer/__pycache__/input_representation.cpython-314.pyc +0 -0
  38. src/transformer/__pycache__/positional_embedding.cpython-314.pyc +0 -0
  39. src/transformer/__pycache__/transformer.cpython-314.pyc +0 -0
  40. src/transformer/__pycache__/transformer_block.cpython-314.pyc +0 -0
  41. src/transformer/attention.py +277 -0
  42. src/transformer/embedding.py +31 -0
  43. src/transformer/feed_forward.py +26 -0
  44. src/transformer/input_representation.py +66 -0
  45. src/transformer/positional_embedding.py +41 -0
  46. src/transformer/transformer.py +36 -0
  47. src/transformer/transformer_block.py +35 -0
  48. uv.lock +0 -0
.gitignore ADDED
File without changes
README.md CHANGED
@@ -1,17 +1,13 @@
1
- ---
2
- title: Tiny Transformers
3
- emoji: 💬
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.5.1
8
- app_file: app.py
9
- pinned: false
10
- hf_oauth: true
11
- hf_oauth_scopes:
12
- - inference-api
13
- license: mit
14
- short_description: 1M parameters and below. Specialized.
15
- ---
16
 
17
- An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
 
 
 
 
 
 
 
 
 
 
 
1
+ Train
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ ```
4
+ cd tiny-sentence-transformer
5
+ python -m src.train
6
+ ```
7
+
8
+ Inference
9
+
10
+ ```
11
+ cd tiny-sentence-transformer
12
+ python -m src.test_inference
13
+ ```
app.py CHANGED
@@ -1,69 +1,117 @@
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- def respond(
6
- message,
7
- history: list[dict[str, str]],
8
- system_message,
9
- max_tokens,
10
- temperature,
11
- top_p,
12
- hf_token: gr.OAuthToken,
13
- ):
14
- """
15
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
16
- """
17
- client = InferenceClient(token=hf_token.token, model="openai/gpt-oss-20b")
18
 
19
- messages = [{"role": "system", "content": system_message}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- messages.extend(history)
22
 
23
- messages.append({"role": "user", "content": message})
 
 
24
 
25
- response = ""
 
 
 
 
26
 
27
- for message in client.chat_completion(
28
- messages,
29
- max_tokens=max_tokens,
30
- stream=True,
31
  temperature=temperature,
32
- top_p=top_p,
33
- ):
34
- choices = message.choices
35
- token = ""
36
- if len(choices) and choices[0].delta.content:
37
- token = choices[0].delta.content
38
-
39
- response += token
40
- yield response
41
-
42
-
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- chatbot = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
 
52
  gr.Slider(
53
  minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
  ),
59
  ],
 
 
 
 
 
 
 
 
 
60
  )
61
 
62
- with gr.Blocks() as demo:
63
- with gr.Sidebar():
64
- gr.LoginButton()
65
- chatbot.render()
66
-
67
 
68
  if __name__ == "__main__":
69
- demo.launch()
 
1
  import gradio as gr
2
+ from pathlib import Path
3
 
4
+ from src.infra.tokenizer import CharacterTokenizer
5
+ from src.task.config import ModelConfig
6
+ from src.task.model import load_model
7
+ from src.task.inference import TextGenerator
8
+
9
+
10
+ # --------------------------------------------------
11
+ # Paths
12
+ # --------------------------------------------------
13
+
14
+ DATA_PATH = Path("data/tiny_shakespear.txt")
15
+
16
+ MODEL_PATH = Path(
17
+ "checkpoints/tiny-sentence-transformer.pt"
18
+ )
19
+
20
+
21
+ # --------------------------------------------------
22
+ # Configuration
23
+ # --------------------------------------------------
24
+
25
+ config = ModelConfig()
26
+
27
+
28
+ # --------------------------------------------------
29
+ # Tokenizer
30
+ # --------------------------------------------------
31
+
32
+ text = DATA_PATH.read_text(
33
+ encoding="utf-8",
34
+ )
35
+
36
+ tokenizer = CharacterTokenizer(text)
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ # --------------------------------------------------
40
+ # Model
41
+ # --------------------------------------------------
42
+
43
+ model = load_model(
44
+ checkpoint_path=MODEL_PATH,
45
+ config=config,
46
+ vocab_size=tokenizer.vocab_size,
47
+ )
48
+
49
+
50
+ # --------------------------------------------------
51
+ # Generator
52
+ # --------------------------------------------------
53
+
54
+ text_generator = TextGenerator(
55
+ model=model,
56
+ tokenizer=tokenizer,
57
+ context_length=config.context_length,
58
+ )
59
 
 
60
 
61
+ # --------------------------------------------------
62
+ # Gradio function
63
+ # --------------------------------------------------
64
 
65
+ def generate_text(
66
+ prompt,
67
+ max_new_tokens,
68
+ temperature,
69
+ ):
70
 
71
+ return text_generator.generate(
72
+ prompt=prompt,
73
+ max_new_tokens=max_new_tokens,
 
74
  temperature=temperature,
75
+ )
76
+
77
+
78
+ # --------------------------------------------------
79
+ # Interface
80
+ # --------------------------------------------------
81
+
82
+ demo = gr.Interface(
83
+ fn=generate_text,
84
+ inputs=[
85
+ gr.Textbox(
86
+ label="Prompt",
87
+ value="ROMEO:",
88
+ ),
89
+ gr.Slider(
90
+ minimum=10,
91
+ maximum=500,
92
+ value=300,
93
+ step=10,
94
+ label="Max new tokens",
95
+ ),
96
  gr.Slider(
97
  minimum=0.1,
98
+ maximum=1.5,
99
+ value=0.7,
100
+ step=0.1,
101
+ label="Temperature",
102
  ),
103
  ],
104
+ outputs=gr.Textbox(
105
+ label="Generated text",
106
+ lines=15,
107
+ ),
108
+ title="Tiny Sentence Transformer",
109
+ description=(
110
+ "A tiny character-level transformer "
111
+ "trained on a Shakespeare corpus."
112
+ ),
113
  )
114
 
 
 
 
 
 
115
 
116
  if __name__ == "__main__":
117
+ demo.launch()
checkpoints/.gitkeep ADDED
File without changes
checkpoints/tiny-sentence-transformer-no-position.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:deb399d39d5a76192f92f9047fd3d11fccd35e9b59bcdf199338e6c79bff5db0
3
+ size 419108
checkpoints/tiny-sentence-transformer.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e84f8908304173d528131887bd3b5df6713fcfc5fb43a6f9095b13723c93622
3
+ size 453634
checkpoints/ttt-autocomplete.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31f1923be1874c46f8e1bb0979d54bfac2b915bb9ce8645d17a0d7182abda7ac
3
+ size 418747
data/tiny_shakespear.txt ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "tiny-sentence-transformer"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.14"
5
+ dependencies = [
6
+ "gradio>=6.26.0",
7
+ "numpy>=2.5.2",
8
+ "torch>=2.13.0",
9
+ ]
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ torch
2
+ gradio
src/__pycache__/generate.cpython-314.pyc ADDED
Binary file (1.1 kB). View file
 
src/__pycache__/test_inference.cpython-314.pyc ADDED
Binary file (1.11 kB). View file
 
src/__pycache__/train.cpython-314.pyc ADDED
Binary file (3.77 kB). View file
 
src/generate.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from src.task.config import ModelConfig
4
+ from src.task.model import load_model
5
+ from src.infra.tokenizer import CharacterTokenizer
6
+ from src.task.inference import TextGenerator
7
+
8
+
9
+ # ------------------------------------------
10
+ # Load corpus
11
+ # ------------------------------------------
12
+
13
+ text = Path(
14
+ "data/tiny_shakespear.txt"
15
+ ).read_text(
16
+ encoding="utf-8"
17
+ )
18
+
19
+
20
+ # ------------------------------------------
21
+ # Tokenizer
22
+ # ------------------------------------------
23
+
24
+ tokenizer = CharacterTokenizer(text)
25
+
26
+
27
+ # ------------------------------------------
28
+ # Configuration
29
+ # ------------------------------------------
30
+
31
+ config = ModelConfig()
32
+
33
+
34
+ # ------------------------------------------
35
+ # Load trained model
36
+ # ------------------------------------------
37
+
38
+ model = load_model(
39
+ checkpoint_path="checkpoints/tiny-sentence-transformer.pt",
40
+ config=config,
41
+ vocab_size=tokenizer.vocab_size,
42
+ )
43
+
44
+
45
+ # ------------------------------------------
46
+ # Generator
47
+ # ------------------------------------------
48
+
49
+ text_generator = TextGenerator(
50
+ model=model,
51
+ tokenizer=tokenizer,
52
+ context_length=config.context_length,
53
+ )
54
+
55
+
56
+ # ------------------------------------------
57
+ # Generate
58
+ # ------------------------------------------
59
+
60
+ output = text_generator.generate(
61
+ prompt="ROMEO:",
62
+ max_new_tokens=300,
63
+ temperature=0.7,
64
+ )
65
+
66
+
67
+ print(output)
src/infra/__init__.py ADDED
File without changes
src/infra/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (172 Bytes). View file
 
src/infra/__pycache__/dataset.cpython-314.pyc ADDED
Binary file (7.15 kB). View file
 
src/infra/__pycache__/generation.cpython-314.pyc ADDED
Binary file (2.01 kB). View file
 
src/infra/__pycache__/tokenizer.cpython-314.pyc ADDED
Binary file (3.71 kB). View file
 
src/infra/dataset.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ from src.infra.tokenizer import CharacterTokenizer
6
+
7
+
8
+ class TextClassificationDataset:
9
+
10
+ def __init__(
11
+ self,
12
+ texts,
13
+ labels,
14
+ tokenizer: CharacterTokenizer,
15
+ ):
16
+ if len(texts) != len(labels):
17
+ raise ValueError(
18
+ "texts and labels must have the same length"
19
+ )
20
+
21
+ self.texts = texts
22
+
23
+ self.labels = torch.tensor(
24
+ labels,
25
+ dtype=torch.long,
26
+ )
27
+
28
+ encoded = [
29
+ tokenizer.encode(text)
30
+ for text in texts
31
+ ]
32
+
33
+ self.tokens, self.padding_mask = tokenizer.pad(
34
+ encoded
35
+ )
36
+
37
+ self.tokens = torch.tensor(
38
+ self.tokens,
39
+ dtype=torch.long,
40
+ )
41
+
42
+ self.padding_mask = torch.tensor(
43
+ self.padding_mask,
44
+ dtype=torch.bool,
45
+ )
46
+
47
+ def __len__(self):
48
+ return len(self.texts)
49
+
50
+ def __getitem__(self, index):
51
+
52
+ return (
53
+ self.tokens[index],
54
+ self.padding_mask[index],
55
+ self.labels[index],
56
+ )
57
+
58
+
59
+ class AutoRegressiveDataset:
60
+ """Dataset for autoregressive language modeling.
61
+
62
+ A token sequence is converted into overlapping
63
+ input/target pairs.
64
+
65
+ For a context length of 4:
66
+
67
+ tokens: A B C D E
68
+
69
+ input: A B C D
70
+ target: B C D E
71
+
72
+ Each target sequence is shifted one token
73
+ relative to its corresponding input sequence.
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ tokens: torch.Tensor,
79
+ context_length: int,
80
+ ) -> None:
81
+
82
+ if tokens.ndim != 1:
83
+ raise ValueError(
84
+ "tokens must be a 1-dimensional tensor"
85
+ )
86
+
87
+ if context_length <= 0:
88
+ raise ValueError(
89
+ "context_length must be greater than 0"
90
+ )
91
+
92
+ if len(tokens) <= context_length:
93
+ raise ValueError(
94
+ "tokens must contain more elements than "
95
+ "context_length"
96
+ )
97
+
98
+ self.tokens = tokens
99
+ self.context_length = context_length
100
+
101
+ def __len__(self) -> int:
102
+ return len(self.tokens) - self.context_length
103
+
104
+ def __getitem__(
105
+ self,
106
+ index: int,
107
+ ) -> tuple[torch.Tensor, torch.Tensor]:
108
+
109
+ if index < 0 or index >= len(self):
110
+ raise IndexError(
111
+ "dataset index out of range"
112
+ )
113
+
114
+ start = index
115
+ end = start + self.context_length
116
+
117
+ input_ids = self.tokens[start:end]
118
+ target_ids = self.tokens[start + 1:end + 1]
119
+
120
+ return input_ids, target_ids
121
+
122
+
123
+ def split_tokens(
124
+ tokens: torch.Tensor,
125
+ train_ratio: float = 0.90,
126
+ val_ratio: float = 0.05,
127
+ ) -> tuple[
128
+ torch.Tensor,
129
+ torch.Tensor,
130
+ torch.Tensor,
131
+ ]:
132
+ """Split a token sequence into train, validation and test sets.
133
+
134
+ The sequence is split chronologically rather than randomly.
135
+ This prevents future tokens from leaking into the training set.
136
+ """
137
+
138
+ if tokens.ndim != 1:
139
+ raise ValueError(
140
+ "tokens must be a 1-dimensional tensor"
141
+ )
142
+
143
+ if not 0 < train_ratio < 1:
144
+ raise ValueError(
145
+ "train_ratio must be between 0 and 1"
146
+ )
147
+
148
+ if not 0 < val_ratio < 1:
149
+ raise ValueError(
150
+ "val_ratio must be between 0 and 1"
151
+ )
152
+
153
+ if train_ratio + val_ratio >= 1:
154
+ raise ValueError(
155
+ "train_ratio + val_ratio must be less than 1"
156
+ )
157
+
158
+ n = len(tokens)
159
+
160
+ train_end = int(train_ratio * n)
161
+ val_end = int((train_ratio + val_ratio) * n)
162
+
163
+ train_tokens = tokens[:train_end]
164
+ val_tokens = tokens[train_end:val_end]
165
+ test_tokens = tokens[val_end:]
166
+
167
+ return (
168
+ train_tokens,
169
+ val_tokens,
170
+ test_tokens,
171
+ )
172
+
173
+
174
+ def get_batch(
175
+ dataset: AutoRegressiveDataset,
176
+ batch_size: int,
177
+ ) -> tuple[torch.Tensor, torch.Tensor]:
178
+ """Sample a random batch from an autoregressive dataset."""
179
+
180
+ if batch_size <= 0:
181
+ raise ValueError(
182
+ "batch_size must be greater than 0"
183
+ )
184
+
185
+ indices = torch.randint(
186
+ low=0,
187
+ high=len(dataset),
188
+ size=(batch_size,),
189
+ )
190
+
191
+ inputs = []
192
+ targets = []
193
+
194
+ for index in indices:
195
+
196
+ input_ids, target_ids = dataset[index.item()]
197
+
198
+ inputs.append(input_ids)
199
+ targets.append(target_ids)
200
+
201
+ return (
202
+ torch.stack(inputs),
203
+ torch.stack(targets),
204
+ )
src/infra/generation.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ @torch.no_grad()
5
+ def generate(
6
+ tokens,
7
+ embedding,
8
+ model,
9
+ output_projection,
10
+ context_length,
11
+ max_new_tokens,
12
+ temperature=1.0,
13
+ ):
14
+
15
+ if temperature <= 0:
16
+ raise ValueError("temperature must be greater than 0")
17
+
18
+ model.eval()
19
+
20
+ for _ in range(max_new_tokens):
21
+
22
+ context = tokens[:, -context_length:]
23
+
24
+ # ------------------------------------------
25
+ # Forward pass
26
+ # ------------------------------------------
27
+ x = embedding(context)
28
+ hidden = model(x)
29
+ logits = output_projection(hidden)
30
+
31
+ next_token_logits = logits[:, -1, :]
32
+
33
+ next_token_logits = (
34
+ next_token_logits / temperature
35
+ )
36
+
37
+ probabilities = torch.softmax(
38
+ next_token_logits,
39
+ dim=-1,
40
+ )
41
+
42
+ next_token = torch.multinomial(
43
+ probabilities,
44
+ num_samples=1,
45
+ )
46
+
47
+ tokens = torch.cat(
48
+ [tokens, next_token],
49
+ dim=1,
50
+ )
51
+
52
+ return tokens
53
+
54
+
55
+
56
+ @torch.no_grad()
57
+ def generator(
58
+ tokens,
59
+ generator,
60
+ context_length,
61
+ max_new_tokens,
62
+ temperature=1.0,
63
+ ):
64
+
65
+ if temperature <= 0:
66
+ raise ValueError("temperature must be greater than 0")
67
+
68
+ generator.eval()
69
+
70
+ for _ in range(max_new_tokens):
71
+
72
+ context = tokens[:, -context_length:]
73
+
74
+ # ------------------------------------------
75
+ # Forward pass
76
+ # ------------------------------------------
77
+
78
+ logits = generator(context)
79
+
80
+ next_token_logits = logits[:, -1, :]
81
+
82
+ next_token_logits = (
83
+ next_token_logits / temperature
84
+ )
85
+
86
+ probabilities = torch.softmax(
87
+ next_token_logits,
88
+ dim=-1,
89
+ )
90
+
91
+ next_token = torch.multinomial(
92
+ probabilities,
93
+ num_samples=1,
94
+ )
95
+
96
+ tokens = torch.cat(
97
+ [tokens, next_token],
98
+ dim=1,
99
+ )
100
+
101
+ return tokens
src/infra/tokenizer.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class CharacterTokenizer:
2
+
3
+ PAD_TOKEN = "<PAD>"
4
+ UNK_TOKEN = "<UNK>"
5
+
6
+
7
+ def __init__(self, text):
8
+ characters = sorted(set(text))
9
+
10
+ self.stoi = {
11
+ self.PAD_TOKEN: 0,
12
+ self.UNK_TOKEN: 1,
13
+ }
14
+
15
+ for character in characters:
16
+ self.stoi[character] = len(self.stoi)
17
+
18
+ self.itos = {
19
+ index: token
20
+ for token, index in self.stoi.items()
21
+ }
22
+
23
+ @property
24
+ def vocab_size(self):
25
+ return len(self.stoi)
26
+
27
+ @property
28
+ def pad_token_id(self):
29
+ return self.stoi[self.PAD_TOKEN]
30
+
31
+ @property
32
+ def unk_token_id(self):
33
+ return self.stoi[self.UNK_TOKEN]
34
+
35
+ def encode(self, text):
36
+ return [
37
+ self.stoi.get(
38
+ character,
39
+ self.unk_token_id,
40
+ )
41
+ for character in text
42
+ ]
43
+
44
+ def decode(self, token_ids):
45
+ return "".join(
46
+ self.itos[token_id]
47
+ for token_id in token_ids
48
+ if token_id != self.pad_token_id
49
+ )
50
+
51
+ def pad(
52
+ self,
53
+ sequences,
54
+ max_length=None,
55
+ ):
56
+
57
+ if max_length is None:
58
+ max_length = max(
59
+ len(sequence)
60
+ for sequence in sequences
61
+ )
62
+
63
+ padded = []
64
+ masks = []
65
+
66
+ for sequence in sequences:
67
+
68
+ sequence = sequence[:max_length]
69
+
70
+ padding_length = (
71
+ max_length - len(sequence)
72
+ )
73
+
74
+ padded_sequence = (
75
+ sequence
76
+ + [self.pad_token_id] * padding_length
77
+ )
78
+
79
+ mask = (
80
+ [1] * len(sequence)
81
+ + [0] * padding_length
82
+ )
83
+
84
+ padded.append(padded_sequence)
85
+ masks.append(mask)
86
+
87
+ return padded, masks
src/task/__init__.py ADDED
File without changes
src/task/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (178 Bytes). View file
 
src/task/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.64 kB). View file
 
src/task/__pycache__/inference.cpython-314.pyc ADDED
Binary file (1.65 kB). View file
 
src/task/__pycache__/model.cpython-314.pyc ADDED
Binary file (2.67 kB). View file
 
src/task/__pycache__/training.cpython-314.pyc ADDED
Binary file (3.1 kB). View file
 
src/task/config.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class ModelConfig:
5
+
6
+ context_length: int = 64
7
+
8
+ d_model: int = 64
9
+ d_ff: int = 256
10
+ num_layers: int = 2
11
+
12
+ dropout: float = 0.1
13
+
14
+
15
+ @dataclass
16
+ class TrainingConfig:
17
+
18
+ batch_size: int = 32
19
+ learning_rate: float = 0.001
20
+ num_steps: int = 1000
21
+ eval_interval: int = 100
22
+ eval_batches: int = 20
src/task/inference.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from src.infra.generation import generator
4
+
5
+
6
+ class TextGenerator:
7
+
8
+ def __init__(
9
+ self,
10
+ model,
11
+ tokenizer,
12
+ context_length,
13
+ ):
14
+ self.model = model
15
+ self.tokenizer = tokenizer
16
+ self.context_length = context_length
17
+
18
+ self.model.eval()
19
+
20
+ @torch.no_grad()
21
+ def generate(
22
+ self,
23
+ prompt,
24
+ max_new_tokens=300,
25
+ temperature=0.7,
26
+ ):
27
+
28
+ prompt_tokens = torch.tensor(
29
+ [self.tokenizer.encode(prompt)],
30
+ dtype=torch.long,
31
+ )
32
+
33
+ generated_tokens = generator(
34
+ tokens=prompt_tokens,
35
+ generator=self.model,
36
+ context_length=self.context_length,
37
+ max_new_tokens=max_new_tokens,
38
+ temperature=temperature,
39
+ )
40
+
41
+ return self.tokenizer.decode(
42
+ generated_tokens[0].tolist()
43
+ )
src/task/model.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from src.transformer.transformer import Transformer
5
+ from src.transformer.input_representation import InputRepresentation
6
+
7
+ class TinyTransformerLM(nn.Module):
8
+
9
+ def __init__(
10
+ self,
11
+ config,
12
+ vocab_size,
13
+ ):
14
+ super().__init__()
15
+
16
+ self.embedding = nn.Embedding(
17
+ vocab_size,
18
+ config.d_model,
19
+ )
20
+
21
+ self.input_representation = InputRepresentation(
22
+ vocab_size=vocab_size,
23
+ context_length=config.context_length,
24
+ d_model=config.d_model,
25
+ )
26
+
27
+ self.transformer = Transformer(
28
+ d_model=config.d_model,
29
+ d_ff=config.d_ff,
30
+ context_length=config.context_length,
31
+ num_layers=config.num_layers,
32
+ dropout=config.dropout,
33
+ )
34
+
35
+ self.head = nn.Linear(
36
+ config.d_model,
37
+ vocab_size,
38
+ )
39
+
40
+ def forward(
41
+ self,
42
+ tokens: torch.Tensor,
43
+ ) -> torch.Tensor:
44
+
45
+ x = self.input_representation(tokens)
46
+
47
+ x = self.transformer(x)
48
+
49
+ logits = self.head(x)
50
+
51
+ return logits
52
+
53
+
54
+ def load_model(
55
+ checkpoint_path,
56
+ config,
57
+ vocab_size,
58
+ ):
59
+ model = TinyTransformerLM(
60
+ config=config,
61
+ vocab_size=vocab_size,
62
+ )
63
+
64
+ checkpoint = torch.load(
65
+ checkpoint_path,
66
+ map_location="cpu",
67
+ )
68
+
69
+ model.load_state_dict(checkpoint)
70
+
71
+ model.eval()
72
+
73
+ return model
src/task/training.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from src.infra.dataset import (AutoRegressiveDataset, get_batch)
5
+
6
+
7
+ @torch.no_grad()
8
+ def evaluate(
9
+ model,
10
+ dataset: AutoRegressiveDataset,
11
+ model_config,
12
+ training_config,
13
+ ):
14
+ model.eval()
15
+
16
+ loss_function = nn.CrossEntropyLoss()
17
+
18
+ losses = []
19
+
20
+ for _ in range(training_config.eval_batches):
21
+
22
+ x, y = get_batch(
23
+ dataset=dataset,
24
+ batch_size=training_config.batch_size,
25
+ )
26
+
27
+ logits = model(x)
28
+
29
+ loss = loss_function(
30
+ logits.view(-1, logits.size(-1)),
31
+ y.view(-1),
32
+ )
33
+
34
+ losses.append(loss.item())
35
+
36
+ model.train()
37
+
38
+ return sum(losses) / len(losses)
39
+
40
+
41
+
42
+
43
+
44
+
45
+ def train(
46
+ model,
47
+ train_dataset: AutoRegressiveDataset,
48
+ val_dataset: AutoRegressiveDataset,
49
+ model_config,
50
+ training_config,
51
+ ):
52
+ loss_function = nn.CrossEntropyLoss()
53
+
54
+ optimizer = torch.optim.AdamW(
55
+ model.parameters(),
56
+ lr=training_config.learning_rate,
57
+ )
58
+
59
+ model.train()
60
+
61
+ for step in range(training_config.num_steps):
62
+
63
+ if step % training_config.eval_interval == 0:
64
+
65
+ train_loss = evaluate(
66
+ model=model,
67
+ dataset=train_dataset,
68
+ model_config=model_config,
69
+ training_config=training_config,
70
+ )
71
+
72
+ val_loss = evaluate(
73
+ model=model,
74
+ dataset=val_dataset,
75
+ model_config=model_config,
76
+ training_config=training_config,
77
+ )
78
+
79
+ print(
80
+ f"step {step:4d} | "
81
+ f"train {train_loss:.4f} | "
82
+ f"val {val_loss:.4f}"
83
+ )
84
+
85
+ x, y = get_batch(
86
+ dataset=train_dataset,
87
+ batch_size=training_config.batch_size,
88
+ )
89
+
90
+ logits = model(x)
91
+
92
+ loss = loss_function(
93
+ logits.view(-1, logits.size(-1)),
94
+ y.view(-1),
95
+ )
96
+
97
+ optimizer.zero_grad()
98
+
99
+ loss.backward()
100
+
101
+ optimizer.step()
102
+
103
+ return model
src/train.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import torch
4
+
5
+ from src.infra.tokenizer import CharacterTokenizer
6
+ from src.infra.dataset import AutoRegressiveDataset, split_tokens
7
+
8
+ from src.task.config import ModelConfig,TrainingConfig
9
+
10
+ from src.task.model import TinyTransformerLM
11
+ from src.task.training import train, evaluate
12
+
13
+
14
+
15
+ def separator(title: str) -> None:
16
+ print("\n" + "=" * 60)
17
+ print(title)
18
+ print("=" * 60)
19
+
20
+
21
+ # --------------------------------------------------
22
+ # Configuration
23
+ # --------------------------------------------------
24
+
25
+ separator("Configuration")
26
+
27
+ torch.manual_seed(42)
28
+
29
+ model_config = ModelConfig()
30
+ training_config = TrainingConfig()
31
+
32
+ print(model_config)
33
+ print(training_config)
34
+
35
+
36
+ # --------------------------------------------------
37
+ # Load corpus
38
+ # --------------------------------------------------
39
+
40
+ separator("Corpus")
41
+
42
+ data_path = Path("data/tiny_shakespear.txt")
43
+
44
+ text = data_path.read_text(
45
+ encoding="utf-8",
46
+ )
47
+
48
+ print(f"Corpus length: {len(text):,} characters")
49
+
50
+
51
+ # --------------------------------------------------
52
+ # Tokenization
53
+ # --------------------------------------------------
54
+
55
+ separator("Tokenization")
56
+
57
+ tokenizer = CharacterTokenizer(text)
58
+
59
+ tokens = torch.tensor(
60
+ tokenizer.encode(text),
61
+ dtype=torch.long,
62
+ )
63
+
64
+ print(f"Vocabulary size: {tokenizer.vocab_size}")
65
+ print(f"Token count: {len(tokens):,}")
66
+
67
+
68
+ # --------------------------------------------------
69
+ # Train / validation / test split
70
+ # --------------------------------------------------
71
+
72
+ separator("Data Split")
73
+
74
+ train_tokens, val_tokens, test_tokens = split_tokens(
75
+ tokens,
76
+ )
77
+
78
+ print(f"Train tokens: {len(train_tokens):,}")
79
+ print(f"Val tokens: {len(val_tokens):,}")
80
+ print(f"Test tokens: {len(test_tokens):,}")
81
+
82
+
83
+ # --------------------------------------------------
84
+ # Datasets
85
+ # --------------------------------------------------
86
+
87
+ separator("Datasets")
88
+
89
+ train_dataset = AutoRegressiveDataset(
90
+ tokens=train_tokens,
91
+ context_length=model_config.context_length,
92
+ )
93
+
94
+ val_dataset = AutoRegressiveDataset(
95
+ tokens=val_tokens,
96
+ context_length=model_config.context_length,
97
+ )
98
+
99
+ test_dataset = AutoRegressiveDataset(
100
+ tokens=test_tokens,
101
+ context_length=model_config.context_length,
102
+ )
103
+
104
+ print(f"Train examples: {len(train_dataset):,}")
105
+ print(f"Val examples: {len(val_dataset):,}")
106
+ print(f"Test examples: {len(test_dataset):,}")
107
+
108
+
109
+ # --------------------------------------------------
110
+ # Model
111
+ # --------------------------------------------------
112
+
113
+ separator("Model")
114
+
115
+ model = TinyTransformerLM(
116
+ config=model_config,
117
+ vocab_size=tokenizer.vocab_size,
118
+ )
119
+
120
+ print(model)
121
+
122
+
123
+ # --------------------------------------------------
124
+ # Training
125
+ # --------------------------------------------------
126
+
127
+ separator("Training")
128
+
129
+ train(
130
+ model=model,
131
+ train_dataset=train_dataset,
132
+ val_dataset=val_dataset,
133
+ model_config=model_config,
134
+ training_config=training_config,
135
+ )
136
+
137
+
138
+ # --------------------------------------------------
139
+ # Final test evaluation
140
+ # --------------------------------------------------
141
+
142
+ separator("Test Evaluation")
143
+
144
+ test_loss = evaluate(
145
+ model=model,
146
+ dataset=test_dataset,
147
+ model_config=model_config,
148
+ training_config=training_config,
149
+ )
150
+
151
+ print(f"Test loss: {test_loss:.4f}")
152
+
153
+
154
+ # --------------------------------------------------
155
+ # Save checkpoint
156
+ # --------------------------------------------------
157
+
158
+ separator("Saving")
159
+
160
+ checkpoint_dir = Path("checkpoints")
161
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
162
+
163
+ checkpoint_path = (
164
+ checkpoint_dir / "tiny-sentence-transformer.pt"
165
+ )
166
+
167
+ torch.save(
168
+ model.state_dict(),
169
+ checkpoint_path,
170
+ )
171
+
172
+ print(f"Checkpoint saved to: {checkpoint_path}")
src/transformer/__pycache__/attention.cpython-314.pyc ADDED
Binary file (3.83 kB). View file
 
src/transformer/__pycache__/embedding.cpython-314.pyc ADDED
Binary file (1.76 kB). View file
 
src/transformer/__pycache__/feed_forward.cpython-314.pyc ADDED
Binary file (1.83 kB). View file
 
src/transformer/__pycache__/input_representation.cpython-314.pyc ADDED
Binary file (2.32 kB). View file
 
src/transformer/__pycache__/positional_embedding.cpython-314.pyc ADDED
Binary file (2.17 kB). View file
 
src/transformer/__pycache__/transformer.cpython-314.pyc ADDED
Binary file (2.01 kB). View file
 
src/transformer/__pycache__/transformer_block.cpython-314.pyc ADDED
Binary file (2.41 kB). View file
 
src/transformer/attention.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Single-head causal self-attention.
3
+
4
+ Learns relationships between tokens in the input sequence.
5
+
6
+ Input Representation (X)
7
+ │
8
+ ▼
9
+ Q = X @ Wq
10
+ K = X @ Wk
11
+ V = X @ Wv
12
+ │
13
+ ▼
14
+ Scores = Q @ Kᵀ
15
+ │
16
+ ▼
17
+ Scale by √d
18
+ │
19
+ ▼
20
+ Apply Causal Mask
21
+ │
22
+ ▼
23
+ Softmax
24
+ │
25
+ ▼
26
+ Attention Weights
27
+ │
28
+ ▼
29
+ Output = Attention Weights @ V
30
+
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import math
36
+
37
+ import torch
38
+ import torch.nn as nn
39
+
40
+
41
+ class Attention(nn.Module):
42
+ """
43
+ Single-head causal self-attention.
44
+
45
+ Input:
46
+ (batch_size, context_length, d_model)
47
+
48
+ Output:
49
+ (batch_size, context_length, d_model)
50
+ """
51
+
52
+ def __init__(self, d_model: int, context_length: int,):
53
+ super().__init__()
54
+
55
+ # --------------------------------------------------
56
+ # Learnable projection layers.
57
+ #
58
+ # Each nn.Linear layer contains a learnable weight
59
+ # matrix that projects the input representation into
60
+ # a different vector space.
61
+ #
62
+ # Transformer paper notation:
63
+ #
64
+ # Query Projection -> Wq
65
+ # Key Projection -> Wk
66
+ # Value Projection -> Wv
67
+ #
68
+ # During the forward pass:
69
+ #
70
+ # Q = X @ Wq
71
+ # K = X @ Wk
72
+ # V = X @ Wv
73
+ #
74
+ # In PyTorch, the multiplication is performed by
75
+ # calling the corresponding nn.Linear layer.
76
+ # --------------------------------------------------
77
+
78
+ self.query = nn.Linear(
79
+ in_features=d_model,
80
+ out_features=d_model,
81
+ bias=False,
82
+ )
83
+
84
+ self.key = nn.Linear(
85
+ in_features=d_model,
86
+ out_features=d_model,
87
+ bias=False,
88
+ )
89
+
90
+ self.value = nn.Linear(
91
+ in_features=d_model,
92
+ out_features=d_model,
93
+ bias=False,
94
+ )
95
+
96
+ # --------------------------------------------------
97
+ # Causal Mask
98
+ #
99
+ # Prevents tokens from attending to future tokens.
100
+ #
101
+ # The mask is created once during initialization
102
+ # and reused during every forward pass.
103
+ # --------------------------------------------------
104
+
105
+ self.register_buffer(
106
+ "mask",
107
+ torch.triu(
108
+ torch.ones(
109
+ context_length,
110
+ context_length,
111
+ dtype=torch.bool,
112
+ ),
113
+ diagonal=1,
114
+ ),
115
+ )
116
+
117
+
118
+
119
+ def forward(self,
120
+ x: torch.Tensor,
121
+ padding_mask: torch.Tensor | None = None,
122
+ ) -> torch.Tensor:
123
+ """
124
+ Parameters
125
+ ----------
126
+ x
127
+ Input representation.
128
+
129
+ Shape:
130
+ (batch_size, context_length, d_model)
131
+
132
+ padding_mask
133
+ Optional mask indicating which positions contain
134
+ real tokens.
135
+
136
+ True -> real token
137
+ False -> padding
138
+
139
+ Shape:
140
+ (batch_size, context_length)
141
+ """
142
+
143
+ # --------------------------------------------------
144
+ # Step 1
145
+ # Create Query (Q), Key (K) and Value (V)
146
+ #
147
+ # Mathematically:
148
+ #
149
+ # Q = X @ Wq
150
+ # K = X @ Wk
151
+ # V = X @ Wv
152
+ #
153
+ # where X is the input representation and Wq, Wk and
154
+ # Wv are the learnable weight matrices contained in
155
+ # the projection layers above.
156
+ # --------------------------------------------------
157
+
158
+ Q = self.query(x)
159
+ K = self.key(x)
160
+ V = self.value(x)
161
+
162
+ # --------------------------------------------------
163
+ # Step 2
164
+ # Compute Attention Scores
165
+ #
166
+ # Mathematically:
167
+ #
168
+ # Scores = Q @ Kᵀ
169
+ #
170
+ # Every Query vector is compared with every Key vector
171
+ # using the dot product, producing one attention score
172
+ # for every Query–Key pair.
173
+ #
174
+ # scores[i, j] answers:
175
+ #
176
+ # "How much should token i attend to token j?"
177
+ # --------------------------------------------------
178
+
179
+ scores = Q @ K.transpose(-2, -1)
180
+
181
+ # --------------------------------------------------
182
+ # Step 3
183
+ # Scale Attention Scores
184
+ #
185
+ # Mathematically:
186
+ #
187
+ # Q @ Kᵀ
188
+ # Scores = --------
189
+ # √d
190
+ #
191
+ # where d is the embedding dimension (d_model).
192
+ #
193
+ # Scaling keeps the magnitude of the attention
194
+ # scores relatively stable regardless of the
195
+ # embedding dimension.
196
+ # --------------------------------------------------
197
+
198
+ scores = scores / math.sqrt(Q.size(-1))
199
+
200
+ # --------------------------------------------------
201
+ # Step 4
202
+ # Apply Causal Mask
203
+ #
204
+ # Prevent each token from attending to future tokens.
205
+ #
206
+ # The masked positions are assigned negative infinity.
207
+ # After applying softmax, they will receive a
208
+ # probability of zero.
209
+ # --------------------------------------------------
210
+
211
+ context_length = scores.size(-1)
212
+
213
+ # --------------------------------------------------
214
+ # Step 4
215
+ # Apply Causal Mask
216
+ # "Don't look into the future."
217
+ # --------------------------------------------------
218
+
219
+ scores = scores.masked_fill(
220
+ self.mask[:context_length, :context_length],
221
+ float("-inf"),
222
+ )
223
+
224
+ # --------------------------------------------------
225
+ # Apply Padding Mask
226
+ #
227
+ # True -> real token
228
+ # False -> PAD
229
+ #
230
+ # We mask the KEY positions, meaning:
231
+ #
232
+ # "Do not allow any token to attend
233
+ # to a PAD token."
234
+ # --------------------------------------------------
235
+
236
+ if padding_mask is not None:
237
+ scores = scores.masked_fill(
238
+ ~padding_mask[:, None, :],
239
+ float("-inf"),
240
+ )
241
+ # --------------------------------------------------
242
+ # Step 5
243
+ # Apply Softmax
244
+ #
245
+ # Convert the attention scores into probabilities.
246
+ #
247
+ # Softmax is applied independently to each row of
248
+ # the attention score matrix so that every row sums
249
+ # to one.
250
+ #
251
+ # Each probability represents how much attention a
252
+ # Query token pays to each Key token.
253
+ # --------------------------------------------------
254
+
255
+ attention = torch.softmax(
256
+ scores,
257
+ dim=-1,
258
+ )
259
+
260
+ # --------------------------------------------------
261
+ # Step 6
262
+ # Compute the Weighted Sum
263
+ #
264
+ # Mathematically:
265
+ #
266
+ # Output = Attention Weights @ V
267
+ #
268
+ # The attention weights determine how much
269
+ # information to retrieve from every Value vector.
270
+ #
271
+ # Each output vector is a weighted combination of
272
+ # all Value vectors visible to the current Query.
273
+ # --------------------------------------------------
274
+
275
+ output = attention @ V
276
+
277
+ return output
src/transformer/embedding.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Token embedding layer.
3
+
4
+ Transforms token IDs into dense, learnable vectors.
5
+ """
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+
10
+
11
+ class Embedding(nn.Module):
12
+ """
13
+ Token Embedding
14
+
15
+ Input:
16
+ (batch_size, context_length)
17
+
18
+ Output:
19
+ (batch_size, context_length, d_model)
20
+ """
21
+
22
+ def __init__(self, vocab_size: int, d_model: int):
23
+ super().__init__()
24
+
25
+ self.embedding = nn.Embedding(
26
+ num_embeddings=vocab_size,
27
+ embedding_dim=d_model
28
+ )
29
+
30
+ def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
31
+ return self.embedding(token_ids)
src/transformer/feed_forward.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Attention
3
+ → tokens exchange information
4
+
5
+ Feed Forward
6
+ → each token independently transforms
7
+ its context-aware representation
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+
13
+
14
+ class FeedForward(nn.Module):
15
+
16
+ def __init__(self, d_model: int, d_ff: int):
17
+ super().__init__()
18
+
19
+ self.net = nn.Sequential(
20
+ nn.Linear(d_model, d_ff), #d_ff is the hidden dimension of the feed forward network, usually larger than d_model
21
+ nn.GELU(), #Activation function.. Gaussian Error Linear Unit (GELU) is a smooth approximation of the ReLU function, which allows for better gradient flow during training.
22
+ nn.Linear(d_ff, d_model), #collapses the hidden dimension back to the original model dimension, ensuring that the output has the same shape as the input
23
+ )
24
+
25
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
26
+ return self.net(x)
src/transformer/input_representation.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Input representation layer.
3
+
4
+ Combines token embeddings and positional embeddings to produce
5
+ the representation that enters the transformer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+
13
+ from .embedding import Embedding
14
+ from .positional_embedding import PositionalEmbedding
15
+
16
+
17
+ class InputRepresentation(nn.Module):
18
+ """
19
+ Input Representation
20
+
21
+ Input:
22
+ token_ids
23
+ Shape:
24
+ (batch_size, context_length)
25
+
26
+ Output:
27
+ input_representation
28
+ Shape:
29
+ (batch_size, context_length, d_model)
30
+
31
+ Formula:
32
+ Input Representation =
33
+ Token Embedding +
34
+ Positional Embedding
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ vocab_size: int,
40
+ context_length: int,
41
+ d_model: int,
42
+ ) -> None:
43
+ super().__init__()
44
+
45
+ self.token_embedding = Embedding(
46
+ vocab_size=vocab_size,
47
+ d_model=d_model,
48
+ )
49
+
50
+ self.positional_embedding = PositionalEmbedding(
51
+ context_length=context_length,
52
+ d_model=d_model,
53
+ )
54
+
55
+ def forward(
56
+ self,
57
+ token_ids: torch.Tensor,
58
+ ) -> torch.Tensor:
59
+
60
+ token_vectors = self.token_embedding(token_ids)
61
+
62
+ position_vectors = self.positional_embedding(token_ids)
63
+
64
+ return token_vectors + position_vectors
65
+
66
+
src/transformer/positional_embedding.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Positional embedding layer.
3
+
4
+ Converts token positions into learnable embedding vectors.
5
+ """
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+
10
+
11
+ class PositionalEmbedding(nn.Module):
12
+ """
13
+ Learned positional embeddings.
14
+
15
+ Input:
16
+ token_ids: (batch_size, context_length)
17
+
18
+ Output:
19
+ (batch_size, context_length, d_model)
20
+ """
21
+
22
+ def __init__(self, context_length: int, d_model: int):
23
+ super().__init__()
24
+
25
+ self.embedding = nn.Embedding(
26
+ num_embeddings=context_length,
27
+ embedding_dim=d_model
28
+ )
29
+
30
+ def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
31
+
32
+ batch_size, context_length = token_ids.shape
33
+
34
+ positions = torch.arange(
35
+ context_length,
36
+ device=token_ids.device
37
+ )
38
+
39
+ positions = positions.unsqueeze(0).expand(batch_size, context_length)
40
+
41
+ return self.embedding(positions)
src/transformer/transformer.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from src.transformer.transformer_block import TransformerBlock
5
+
6
+
7
+ class Transformer(nn.Module):
8
+
9
+ def __init__(
10
+ self,
11
+ d_model: int,
12
+ d_ff: int,
13
+ context_length: int,
14
+ num_layers: int,
15
+ dropout = 0.1,
16
+ ):
17
+ super().__init__()
18
+
19
+ self.blocks = nn.ModuleList([
20
+ TransformerBlock(
21
+ d_model=d_model,
22
+ d_ff=d_ff,
23
+ context_length=context_length,
24
+ dropout=dropout,
25
+ )
26
+ for _ in range(num_layers)
27
+ ])
28
+
29
+ def forward(self,
30
+ x: torch.Tensor,
31
+ padding_mask: torch.Tensor | None = None,) -> torch.Tensor:
32
+
33
+ for block in self.blocks:
34
+ x = block(x,padding_mask=padding_mask,)
35
+
36
+ return x
src/transformer/transformer_block.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from src.transformer.attention import Attention
5
+ from src.transformer.feed_forward import FeedForward
6
+
7
+
8
+ class TransformerBlock(nn.Module):
9
+
10
+ def __init__(self, d_model: int, d_ff: int, context_length: int, dropout=0.1,):
11
+ super().__init__()
12
+
13
+ self.attention = Attention(d_model, context_length)
14
+ self.feed_forward = FeedForward(d_model, d_ff)
15
+
16
+ self.layer_norm_1 = nn.LayerNorm(d_model)
17
+ self.layer_norm_2 = nn.LayerNorm(d_model)
18
+
19
+ self.dropout = nn.Dropout(dropout)
20
+
21
+ def forward(self, x, padding_mask: torch.Tensor | None = None,) -> torch.Tensor:
22
+
23
+ attention_output = self.attention(x, padding_mask=padding_mask)
24
+
25
+ x = self.layer_norm_1(
26
+ x + self.dropout(attention_output)
27
+ )
28
+
29
+ ffn_output = self.feed_forward(x)
30
+
31
+ x = self.layer_norm_2(
32
+ x + self.dropout(ffn_output)
33
+ )
34
+
35
+ return x
uv.lock ADDED
The diff for this file is too large to render. See raw diff