๐ง BERT on EEG
The autoregressive Transformer walkthrough built a model that predicts the next token, and had to hide the future behind a causal mask to do it.
BERT throws that mask away.
Its training task is not "what comes next" but "what is missing": some inputs are blanked out, and the model reconstructs them using context from both sides. That single change — bidirectional instead of causal — is what makes BERT an encoder of meaning rather than a generator of sequences.
Here the inputs are not words but EEG. A raw multi-channel recording is cut into short time patches, each patch is projected into a vector, some patches are masked, and the encoder reconstructs them. This is exactly how EEG foundation models are pre-trained, and it maps onto BERT with one substitution:
| BERT on text | BERT on EEG |
|---|---|
| token → embedding lookup | patch → linear projection |
| predict token ID, cross-entropy | predict the waveform, MSE |
| vocabulary softmax | reconstruction head |
Everything below is computed in NumPy with every matrix printed, and each hand computation is checked against the array it came from.
Display helpers ready: banner, step, check, expand, show_matrix, show_matmul, show_elementwise.
1. The Signal
Two channels of synthetic EEG, one second at 64 Hz. Both carry a background alpha rhythm (~10 Hz); a spike-wave burst is injected in the middle, which is the interesting event a model would need to represent.
EEG array shape: (2, 64) (2 channels x 64 samples)
sampling rate 64 Hz, duration 1.0s, spike burst at samples 24-40
sample time(s) Fp1 Fp2
----------------------------------------
0 0.0000 0.0248 0.4272
1 0.0156 0.6583 0.6641
2 0.0312 0.7715 0.2725
3 0.0469 0.2322 -0.2394
4 0.0625 -0.5774 -0.5797
5 0.0781 -0.7963 -0.4069
...
28 0.4375 1.7602 0.9957
29 0.4531 1.2208 0.6281
30 0.4688 0.4369 0.3733
31 0.4844 0.1165 0.3113
32 0.5000 -0.0007 0.4013
33 0.5156 -0.0179 0.1052
----------------------------------------
mean +0.0234, std 0.5996
The dotted lines cut the recording into 8 patches of 8 samples each. Those patches are this model's 'tokens'.
2. Patching — Turning a Signal into Tokens
A Transformer needs a sequence of vectors. Feeding one time sample per position would give 64 positions carrying almost no information each, and attention cost grows with the square of that number.
So the signal is cut into non-overlapping patches, and each patch is flattened across channels:
$$\text{patch}_p \in \mathbb{R}^{C \times L} \;\longrightarrow\; \mathbb{R}^{C \cdot L}$$
with $C = 2$ channels and $L = 8$ samples, giving vectors of length 16.
8 patches x 16 values -> patches.shape = (8, 16)
patch p3 covers samples 24..31 (t = 0.375s to 0.484s)
Fp1 values: [-0.8272 -0.3474 0.638 1.6286 1.7602 1.2208 0.4369 0.1165]
Fp2 values: [-0.4854 0.1653 0.8491 1.2341 0.9957 0.6281 0.3733 0.3113]
flattened : [-0.8272 -0.3474 0.638 1.6286 1.7602 1.2208 0.4369 0.1165 -0.4854 0.1653 0.8491 1.2341 0.9957 0.6281 0.3733 0.3113]
MATCH: True
patches (8, 16) =
p0 | +0.025 +0.658 +0.771 +0.232 -0.577 -0.796 -0.227 +0.483 +0.427 +0.664 +0.272 -0.239 -0.580 -0.407 +0.200 +0.653
p1 | +0.777 +0.472 -0.329 -0.808 -0.554 +0.060 +0.653 +0.637 +0.457 +0.012 -0.664 -0.484 -0.047 +0.454 +0.576 +0.067
p2 | -0.051 -0.649 -0.785 -0.227 +0.639 +0.773 +0.310 -0.516 -0.398 -0.578 -0.202 +0.264 +0.557 +0.350 -0.136 -0.560
p3 | -0.827 -0.347 +0.638 +1.629 +1.760 +1.221 +0.437 +0.117 -0.485 +0.165 +0.849 +1.234 +0.996 +0.628 +0.373 +0.311
p4 | -0.001 -0.018 -0.220 -0.930 -1.321 -1.278 -0.474 +0.454 +0.401 +0.105 -0.524 -1.121 -1.281 -0.712 +0.083 +0.536
p5 | +0.837 +0.453 -0.312 -0.800 -0.640 +0.120 +0.716 +0.718 +0.451 -0.046 -0.438 -0.517 -0.038 +0.465 +0.476 +0.165
p6 | +0.017 -0.753 -0.723 -0.175 +0.532 +0.815 +0.358 -0.398 -0.384 -0.473 -0.286 +0.305 +0.596 +0.316 -0.124 -0.539
p7 | -0.842 -0.460 +0.323 +0.833 +0.542 -0.165 -0.794 -0.725 -0.419 +0.021 +0.603 +0.455 +0.081 -0.359 -0.621 -0.195
row energy (how active each patch is):
p0: 2.0146 ########
p1: 2.0391 ########
p2: 1.9563 #######
p3: 3.5758 ############## <- spike burst
p4: 2.9780 ########### <- spike burst
p5: 2.0568 ########
p6: 1.9134 #######
p7: 2.1221 ########
3. Patch Embedding
Text BERT looks a token ID up in an embedding table. There is no table here — the input is continuous — so the patch is projected instead:
$$E_p = \text{patch}_p W_e + b_e, \qquad W_e \in \mathbb{R}^{16 \times 8}$$
This is the only structural difference at the input, and it is why the same architecture transfers to signals, images (ViT), and audio.
==============================================================================
patches (8, 16) @ W_e (16, 8) = E_patch (8, 8)
==============================================================================
shape rule: (8 x 16) @ (16 x 8) -> (8 x 8) inner dim 16 cancels
each 16-value patch becomes an 8-dim vector
==============================================================================
patches (8, 16) [first 14 of 16 columns] =
p0 | +0.025 +0.658 +0.771 +0.232 -0.577 -0.796 -0.227 +0.483 +0.427 +0.664 +0.272 -0.239 -0.580 -0.407
p1 | +0.777 +0.472 -0.329 -0.808 -0.554 +0.060 +0.653 +0.637 +0.457 +0.012 -0.664 -0.484 -0.047 +0.454
p2 | -0.051 -0.649 -0.785 -0.227 +0.639 +0.773 +0.310 -0.516 -0.398 -0.578 -0.202 +0.264 +0.557 +0.350
p3 | -0.827 -0.347 +0.638 +1.629 +1.760 +1.221 +0.437 +0.117 -0.485 +0.165 +0.849 +1.234 +0.996 +0.628
p4 | -0.001 -0.018 -0.220 -0.930 -1.321 -1.278 -0.474 +0.454 +0.401 +0.105 -0.524 -1.121 -1.281 -0.712
p5 | +0.837 +0.453 -0.312 -0.800 -0.640 +0.120 +0.716 +0.718 +0.451 -0.046 -0.438 -0.517 -0.038 +0.465
p6 | +0.017 -0.753 -0.723 -0.175 +0.532 +0.815 +0.358 -0.398 -0.384 -0.473 -0.286 +0.305 +0.596 +0.316
p7 | -0.842 -0.460 +0.323 +0.833 +0.542 -0.165 -0.794 -0.725 -0.419 +0.021 +0.603 +0.455 +0.081 -0.359
W_e (16, 8) =
0 | -0.459 +0.091 +0.178 -0.483 +0.012 -0.274 +0.145 -0.326
1 | +0.191 -0.113 +0.437 -0.362 -0.159 -0.387 +0.425 +0.377
2 | -0.242 +0.160 +0.317 +0.055 +0.030 -0.258 -0.407 +0.397
3 | +0.400 +0.133 -0.161 -0.151 +0.226 +0.397 +0.387 +0.280
4 | +0.142 -0.416 -0.338 +0.399 +0.106 -0.491 -0.399 +0.164
5 | -0.495 -0.339 +0.049 +0.192 +0.152 -0.276 +0.212 -0.263
6 | -0.175 +0.246 +0.150 +0.349 +0.158 +0.068 -0.406 -0.132
7 | -0.235 -0.256 +0.473 -0.107 +0.392 +0.131 +0.295 +0.003
8 | +0.077 -0.007 -0.305 +0.222 -0.219 -0.476 +0.145 -0.323
9 | +0.440 +0.454 +0.415 -0.130 -0.485 +0.428 -0.072 +0.467
10 | +0.464 +0.353 -0.206 -0.115 +0.351 -0.183 -0.331 +0.057
11 | +0.436 +0.196 +0.070 -0.403 +0.115 +0.490 -0.360 +0.018
12 | +0.377 +0.241 +0.197 +0.202 -0.141 -0.206 +0.309 +0.310
13 | +0.367 +0.413 +0.011 +0.002 +0.298 +0.150 +0.202 +0.296
14 | +0.390 -0.162 -0.124 -0.406 +0.078 -0.464 -0.034 +0.043
15 | -0.213 +0.091 -0.469 -0.463 +0.323 -0.140 -0.373 +0.022
E_patch (8, 8) =
p0 | +0.176 +0.479 +0.472 -1.184 -0.219 -0.029 -0.147 +0.664
p1 | -1.002 -0.174 +0.819 -0.394 -0.141 -0.775 +0.782 -0.666
p2 | -0.085 -0.436 -0.546 +1.148 +0.222 -0.010 -0.008 -0.635
p3 | +2.006 +0.302 -0.635 +0.574 +1.563 +0.110 -0.984 +1.579
p4 | -1.383 -0.335 +0.029 -0.806 -0.750 +0.168 +0.188 -0.894
p5 | -1.094 -0.073 +0.803 -0.436 +0.038 -0.791 +0.736 -0.730
p6 | -0.174 -0.367 -0.412 +1.094 +0.215 +0.129 +0.024 -0.648
p7 | +1.175 +0.227 -0.810 +0.460 -0.000 +0.853 -0.647 +0.773
worked element E_patch[3,0] = row 3 of patches . column 0 of W_e
row 3 of patches = [-0.8272 -0.3474 0.638 1.6286 1.7602 1.2208 0.4369 0.1165 -0.4854 0.1653 0.8491 1.2341 0.9957 0.6281 0.3733 0.3113]
col 0 of W_e = [-0.4592 0.1909 -0.2421 0.4004 0.142 -0.4949 -0.1746 -0.2348 0.0769 0.4405 0.4636 0.4362 0.3774 0.3671 0.39 -0.2135]
(-0.8272)*(-0.4592) + (-0.3474)*(+0.1909) + (+0.6380)*(-0.2421) + (+1.6286)*(+0.4004) +
(+1.7602)*(+0.1420) + (+1.2208)*(-0.4949) + (+0.4369)*(-0.1746) + (+0.1165)*(-0.2348) +
(-0.4854)*(+0.0769) + (+0.1653)*(+0.4405) + (+0.8491)*(+0.4636) + (+1.2341)*(+0.4362) +
(+0.9957)*(+0.3774) + (+0.6281)*(+0.3671) + (+0.3733)*(+0.3900) + (+0.3113)*(-0.2135)
= +2.006270 | from array: +2.006270 | MATCH: True
The [CLS] token and positional encoding
Two additions, both borrowed unchanged from text BERT:
[CLS] is a learned vector prepended to the sequence. It belongs to no patch, so it
is free to accumulate a summary of the whole recording through attention — which is
what a downstream classifier reads.
Positional encoding restores order. Attention is permutation-invariant: without it, a spike at 0.4s and the same spike at 0.9s would be indistinguishable.
$$Z = \big[\,\texttt{[CLS]};\, E_0;\, \dots;\, E_{7}\,\big] + PE$$
sequence length = 1 ([CLS]) + 8 patches = 9
[CLS] vector = [-0.2529 0.2403 -0.2582 0.0765 0.0161 -0.2304 0.758 0.2241]
==============================================================================
Z0 = embeddings + PE all three are (9, 8) - element-wise
==============================================================================
[CLS] |
embeddings: -0.253 +0.240 -0.258 +0.077 +0.016 -0.230 +0.758 +0.224
PE: +0.000 +1.000 +0.000 +1.000 +0.000 +1.000 +0.000 +1.000
Z0: -0.253 +1.240 -0.258 +1.077 +0.016 +0.770 +0.758 +1.224
p0 |
embeddings: +0.176 +0.479 +0.472 -1.184 -0.219 -0.029 -0.147 +0.664
PE: +0.841 +0.540 +0.100 +0.995 +0.010 +1.000 +0.001 +1.000
Z0: +1.018 +1.020 +0.572 -0.189 -0.209 +0.971 -0.146 +1.664
p1 |
embeddings: -1.002 -0.174 +0.819 -0.394 -0.141 -0.775 +0.782 -0.666
PE: +0.909 -0.416 +0.199 +0.980 +0.020 +1.000 +0.002 +1.000
Z0: -0.092 -0.590 +1.018 +0.586 -0.121 +0.225 +0.784 +0.334
p2 |
embeddings: -0.085 -0.436 -0.546 +1.148 +0.222 -0.010 -0.008 -0.635
PE: +0.141 -0.990 +0.296 +0.955 +0.030 +1.000 +0.003 +1.000
Z0: +0.056 -1.426 -0.251 +2.104 +0.252 +0.990 -0.005 +0.365
p3 |
embeddings: +2.006 +0.302 -0.635 +0.574 +1.563 +0.110 -0.984 +1.579
PE: -0.757 -0.654 +0.389 +0.921 +0.040 +0.999 +0.004 +1.000
Z0: +1.249 -0.351 -0.245 +1.495 +1.603 +1.109 -0.980 +2.579
p4 |
embeddings: -1.383 -0.335 +0.029 -0.806 -0.750 +0.168 +0.188 -0.894
PE: -0.959 +0.284 +0.479 +0.878 +0.050 +0.999 +0.005 +1.000
Z0: -2.342 -0.051 +0.508 +0.071 -0.700 +1.166 +0.193 +0.106
p5 |
embeddings: -1.094 -0.073 +0.803 -0.436 +0.038 -0.791 +0.736 -0.730
PE: -0.279 +0.960 +0.565 +0.825 +0.060 +0.998 +0.006 +1.000
Z0: -1.374 +0.887 +1.368 +0.389 +0.098 +0.207 +0.742 +0.270
p6 |
embeddings: -0.174 -0.367 -0.412 +1.094 +0.215 +0.129 +0.024 -0.648
PE: +0.657 +0.754 +0.644 +0.765 +0.070 +0.998 +0.007 +1.000
Z0: +0.483 +0.387 +0.232 +1.859 +0.285 +1.126 +0.031 +0.352
p7 |
embeddings: +1.175 +0.227 -0.810 +0.460 -0.000 +0.853 -0.647 +0.773
PE: +0.989 -0.146 +0.717 +0.697 +0.080 +0.997 +0.008 +1.000
Z0: +2.164 +0.082 -0.093 +1.156 +0.080 +1.849 -0.639 +1.773
check [0,0]: -0.252898 + +0.000000 = -0.252898 MATCH: True
4. Masking
BERT's training signal comes from hiding part of the input. A fraction of positions is
selected, and the chosen ones are replaced by a single learned [MASK] vector.
Text BERT applies the well-known 80/10/10 rule to each selected position:
- 80% replaced by
[MASK], - 10% replaced by a random token,
- 10% left unchanged.
The reason is a train/inference mismatch: [MASK] never appears when the model is
actually used, so if every selected position were masked the model would only ever learn
to handle that symbol. The 10% "unchanged" slice forces it to keep a good representation
of every position, because it cannot tell which ones it will be scored on.
masking 25% of 8 patches -> 2 positions
[MASK] vector = [-0.716 0.0659 -0.234 0.3014 -0.2802 -0.0406 0.1785 0.3061]
pos name masked input row (after masking)
--------------------------------------------------------------
0 [CLS] - [-0.253 1.24 -0.258 1.077 0.016 0.77 0.758 1.224]
1 p0 - [ 1.018 1.02 0.572 -0.189 -0.209 0.971 -0.146 1.664]
2 p1 - [-0.092 -0.59 1.018 0.586 -0.121 0.225 0.784 0.334]
3 p2 - [ 0.056 -1.426 -0.251 2.104 0.252 0.99 -0.005 0.365]
4 p3 YES [-1.473 -0.588 0.155 1.222 -0.24 0.959 0.183 1.306]
5 p4 - [-2.342 -0.051 0.508 0.071 -0.7 1.166 0.193 0.106]
6 p5 - [-1.374 0.887 1.368 0.389 0.098 0.207 0.742 0.27 ]
7 p6 - [0.483 0.387 0.232 1.859 0.285 1.126 0.031 0.352]
8 p7 YES [ 0.273 -0.08 0.483 0.998 -0.2 0.956 0.187 1.306]
--------------------------------------------------------------
masked positions: [4, 8] = ['p3', 'p7']
The true content of those patches is kept aside as the reconstruction
target - the encoder never sees it again until the loss is computed.
targets.shape = (2, 16) (2 patches x 16 values)
5. Bidirectional Self-Attention
This is the difference that defines BERT.
The autoregressive decoder added a causal mask $M$ before the softmax so position $t$ could not see beyond itself. BERT adds nothing:
$$A = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) \qquad\text{(no } +M \text{ term)}$$
Every position attends to every other position, in both directions. A masked patch is reconstructed from the signal before and after it — which is why BERT is useless for generation and excellent for representation.
STEP 1 : project Z to Q, K, V
==============================================================================
==============================================================================
Z (9, 8) @ Wq (8, 8) = Q_full (9, 8)
==============================================================================
shape rule: (9 x 8) @ (8 x 8) -> (9 x 8) inner dim 8 cancels
K uses Wk, V uses Wv - identical arithmetic
==============================================================================
Z (9, 8) =
[CLS] | -0.253 +1.240 -0.258 +1.077 +0.016 +0.770 +0.758 +1.224
p0 | +1.018 +1.020 +0.572 -0.189 -0.209 +0.971 -0.146 +1.664
p1 | -0.092 -0.590 +1.018 +0.586 -0.121 +0.225 +0.784 +0.334
p2 | +0.056 -1.426 -0.251 +2.104 +0.252 +0.990 -0.005 +0.365
p3 | -1.473 -0.588 +0.155 +1.222 -0.240 +0.959 +0.183 +1.306
p4 | -2.342 -0.051 +0.508 +0.071 -0.700 +1.166 +0.193 +0.106
p5 | -1.374 +0.887 +1.368 +0.389 +0.098 +0.207 +0.742 +0.270
p6 | +0.483 +0.387 +0.232 +1.859 +0.285 +1.126 +0.031 +0.352
p7 | +0.273 -0.080 +0.483 +0.998 -0.200 +0.956 +0.187 +1.306
Wq (8, 8) =
0 | -0.319 -0.497 -0.388 +0.532 +0.169 +0.020 +0.192 -0.079
1 | +0.282 -0.554 +0.081 -0.418 -0.465 -0.194 -0.500 -0.497
2 | -0.231 +0.587 -0.398 -0.591 +0.323 +0.376 -0.188 -0.043
3 | +0.183 -0.554 +0.550 +0.474 -0.293 -0.594 +0.531 +0.001
4 | +0.048 +0.225 +0.142 +0.544 +0.544 +0.450 +0.167 +0.369
5 | +0.217 +0.090 -0.455 +0.381 +0.393 +0.154 +0.392 +0.186
6 | -0.359 -0.277 -0.350 -0.150 -0.565 +0.145 -0.200 +0.191
7 | -0.140 +0.222 -0.195 -0.293 -0.005 +0.236 -0.186 +0.535
Q_full (9, 8) =
[CLS] | +0.411 -1.174 +0.042 -0.161 -1.141 -0.457 -0.123 +0.364
p0 | -0.185 -0.180 -1.390 -0.522 +0.280 +0.579 -0.456 +0.353
p1 | -0.550 +0.497 -0.553 -0.323 -0.006 +0.320 +0.246 +0.583
p2 | +0.202 -0.321 +0.636 +2.178 +0.502 -0.714 +2.251 +1.189
p3 | +0.440 +0.745 +0.346 -0.227 -0.147 -0.209 +0.687 +1.226
p4 | +0.763 +1.369 +0.024 -1.488 -0.262 +0.029 -0.200 +0.240
p5 | +0.188 +0.675 -0.117 -1.785 -0.604 +0.330 -0.860 -0.031
p6 | +0.440 -1.112 +0.222 +1.315 +0.011 -0.692 +1.261 +0.270
p7 | -0.091 -0.081 -0.539 +0.210 +0.102 +0.003 +0.593 +0.836
worked element Q_full[0,0] = row 0 of Z . column 0 of Wq
row 0 of Z = [-0.2529 1.2403 -0.2582 1.0765 0.0161 0.7696 0.758 1.2241]
col 0 of Wq = [-0.3187 0.2817 -0.231 0.1834 0.0482 0.217 -0.3592 -0.1404]
(-0.2529)*(-0.3187) + (+1.2403)*(+0.2817) + (-0.2582)*(-0.2310) + (+1.0765)*(+0.1834) +
(+0.0161)*(+0.0482) + (+0.7696)*(+0.2170) + (+0.7580)*(-0.3592) + (+1.2241)*(-0.1404)
= +0.410802 | from array: +0.410802 | MATCH: True
K_full (9, 8), V_full (9, 8)
STEP 2 : split into 2 heads of 4 -> (2, 9, 4)
STEP 3 : scores = Q K^T / sqrt(d_k) (head 0)
==============================================================================
==============================================================================
Q_head0 (9, 4) @ K_head0^T (4, 9) = raw_scores (9, 9)
==============================================================================
shape rule: (9 x 4) @ (4 x 9) -> (9 x 9) inner dim 4 cancels
(9,4) @ (4,9) -> (9,9)
==============================================================================
Q_head0 (9, 4) =
[CLS] | +0.411 -1.174 +0.042 -0.161
p0 | -0.185 -0.180 -1.390 -0.522
p1 | -0.550 +0.497 -0.553 -0.323
p2 | +0.202 -0.321 +0.636 +2.178
p3 | +0.440 +0.745 +0.346 -0.227
p4 | +0.763 +1.369 +0.024 -1.488
p5 | +0.188 +0.675 -0.117 -1.785
p6 | +0.440 -1.112 +0.222 +1.315
p7 | -0.091 -0.081 -0.539 +0.210
K_head0^T (4, 9) =
0 | +1.036 -0.694 +0.844 +2.334 +2.509 +2.331 +0.533 +1.051 +1.041
1 | +0.552 -0.626 +0.651 +1.874 +1.032 +0.878 +0.211 +1.207 +0.562
2 | -0.206 +0.305 +0.055 -0.981 -1.444 -1.927 -0.483 -0.552 -0.334
3 | +1.840 +1.519 +0.238 +0.902 +1.260 +0.492 +0.609 +1.622 +1.449
raw_scores (9, 9) =
[CLS] | -0.527 +0.217 -0.453 -1.427 -0.443 -0.232 -0.146 -1.270 -0.479
p0 | -0.964 -0.975 -0.474 +0.125 +0.701 +1.834 +0.217 -0.490 -0.584
p1 | -0.776 -0.588 -0.248 -0.101 -0.476 +0.061 -0.118 -0.196 -0.577
p2 | +3.908 +3.563 +0.515 +1.209 +2.001 +0.035 +1.059 +3.006 +2.973
p3 | +0.378 -1.010 +0.821 +1.878 +1.087 +0.900 +0.086 +0.803 +0.432
p4 | -1.196 -3.638 +1.182 +2.980 +1.417 +2.201 -0.222 +0.028 -0.600
p5 | -2.692 -3.300 +0.167 +0.210 -0.910 +0.379 -0.788 -1.817 -1.972
p6 | +2.216 +2.456 -0.027 -0.089 +1.292 +0.268 +0.694 +1.130 +1.664
p7 | +0.360 +0.269 -0.109 +0.356 +0.733 +0.861 +0.323 +0.446 +0.345
worked element raw_scores[0,0] = row 0 of Q_head0 . column 0 of K_head0^T
row 0 of Q_head0 = [ 0.4108 -1.1735 0.0416 -0.1612]
col 0 of K_head0^T = [ 1.0363 0.5522 -0.2061 1.8397]
(+0.4108)*(+1.0363) + (-1.1735)*(+0.5522) + (+0.0416)*(-0.2061) + (-0.1612)*(+1.8397)
= -0.527471 | from array: -0.527471 | MATCH: True
then divide every entry by sqrt(d_k) = 2:
scaled_scores (9, 9) =
[CLS] | -0.264 +0.109 -0.227 -0.714 -0.222 -0.116 -0.073 -0.635 -0.239
p0 | -0.482 -0.487 -0.237 +0.063 +0.350 +0.917 +0.109 -0.245 -0.292
p1 | -0.388 -0.294 -0.124 -0.050 -0.238 +0.031 -0.059 -0.098 -0.288
p2 | +1.954 +1.781 +0.258 +0.605 +1.000 +0.017 +0.530 +1.503 +1.486
p3 | +0.189 -0.505 +0.411 +0.939 +0.543 +0.450 +0.043 +0.401 +0.216
p4 | -0.598 -1.819 +0.591 +1.490 +0.709 +1.100 -0.111 +0.014 -0.300
p5 | -1.346 -1.650 +0.083 +0.105 -0.455 +0.190 -0.394 -0.909 -0.986
p6 | +1.108 +1.228 -0.013 -0.045 +0.646 +0.134 +0.347 +0.565 +0.832
p7 | +0.180 +0.134 -0.054 +0.178 +0.366 +0.430 +0.162 +0.223 +0.173
STEP 4 : softmax - and note what is NOT here
==============================================================================
A = softmax(scores) (9, 9) =
[CLS] | 0.1081 0.1568 0.1121 0.0689 0.1127 0.1253 0.1307 0.0746 0.1107
p0 | 0.0640 0.0636 0.0818 0.1103 0.1471 0.2592 0.1155 0.0811 0.0774
p1 | 0.0884 0.0971 0.1151 0.1239 0.1027 0.1343 0.1228 0.1181 0.0976
p2 | 0.2316 0.1949 0.0425 0.0601 0.0892 0.0334 0.0557 0.1475 0.1451
p3 | 0.0933 0.0466 0.1165 0.1975 0.1330 0.1212 0.0806 0.1154 0.0959
p4 | 0.0376 0.0111 0.1233 0.3031 0.1387 0.2053 0.0611 0.0693 0.0506
p5 | 0.0438 0.0323 0.1829 0.1869 0.1067 0.2034 0.1135 0.0678 0.0628
p6 | 0.1791 0.2019 0.0584 0.0566 0.1129 0.0676 0.0837 0.1040 0.1359
p7 | 0.1081 0.1033 0.0855 0.1079 0.1302 0.1388 0.1061 0.1128 0.1073
row sums (must all be 1): [1. 1. 1. 1. 1. 1. 1. 1. 1.]
entries ABOVE the diagonal - the 'future' - are all non-zero: True
smallest such weight = 0.033393
In the autoregressive decoder every one of those was exactly 0.
That is the whole difference between BERT and GPT, visible as a matrix.
Bidirectional vs causal, side by side
The same scores, softmaxed twice: once as BERT does it, once with the causal mask the decoder-style model would apply.
query BERT: non-zero weights causal: non-zero weights
--------------------------------------------------------------
[CLS] 9 1
p0 9 2
p1 9 3
p2 9 4
p3 9 5
p4 9 6
p5 9 7
p6 9 8
p7 9 9
--------------------------------------------------------------
Every BERT row uses all 9 positions. The causal rows grow 1, 2, 3, ...
A masked patch in the middle of the recording therefore gets context from
BOTH sides under BERT, and only from the left under a causal model.
One attention weight, by hand
$$A_{ij} = \frac{\exp(s_{ij} - m)}{\sum_{j'} \exp(s_{ij'} - m)}, \qquad s_{ij} = \frac{q_i \cdot k_j}{\sqrt{d_k}}, \qquad m = \max_{j} s_{ij}$$
query = position 4 ('p3', MASKED), key = position 5 ('p4')
q = [ 0.4397 0.7448 0.3462 -0.2267]
k = [ 2.331 0.878 -1.9271 0.492 ]
q . k = (+0.4397)*(+2.3310) + (+0.7448)*(+0.8780) + (+0.3462)*(-1.9271) + (-0.2267)*(+0.4920)
= +0.900163
/ sqrt(d_k) = +0.900163 / 2 = +0.450082
from the scores matrix: +0.450082 MATCH: True
softmax over row 4, m = max = 0.938971
j name s_ij s-m exp /Z array
----------------------------------------------------------------------
0 [CLS] 0.189236 -0.749735 0.472492 0.093338 0.093338
1 p0 -0.504971 -1.443941 0.235996 0.046619 0.046619
2 p1 0.410514 -0.528456 0.589514 0.116455 0.116455
3 p2 0.938971 0.000000 1.000000 0.197544 0.197544
4 p3 0.543256 -0.395714 0.673199 0.132986 0.132986
5 p4 0.450082 -0.488889 0.613307 0.121155 0.121155
6 p5 0.043121 -0.895849 0.408261 0.080649 0.080649
7 p6 0.401259 -0.537711 0.584083 0.115382 0.115382
8 p7 0.216020 -0.722951 0.485318 0.095872 0.095872
----------------------------------------------------------------------
Z = 5.062170, sum of weights = 1.0000000000
MATCH with the array row: True
6. The Encoder Block
Identical in form to the encoder half of the autoregressive Transformer: attention, residual + LayerNorm, feed-forward, residual + LayerNorm. Nothing here is BERT-specific — the only BERT-specific choice was the absent mask.
STEPS 5-7 : weights @ V, concat heads, project with W_O
==============================================================================
==============================================================================
A_head0 (9, 9) @ V_head0 (9, 4) = head0_out (9, 4)
==============================================================================
shape rule: (9 x 9) @ (9 x 4) -> (9 x 4) inner dim 9 cancels
(9,9) @ (9,4) -> (9,4)
==============================================================================
A_head0 (9, 9) =
[CLS] | +0.108 +0.157 +0.112 +0.069 +0.113 +0.125 +0.131 +0.075 +0.111
p0 | +0.064 +0.064 +0.082 +0.110 +0.147 +0.259 +0.115 +0.081 +0.077
p1 | +0.088 +0.097 +0.115 +0.124 +0.103 +0.134 +0.123 +0.118 +0.098
p2 | +0.232 +0.195 +0.042 +0.060 +0.089 +0.033 +0.056 +0.148 +0.145
p3 | +0.093 +0.047 +0.116 +0.198 +0.133 +0.121 +0.081 +0.115 +0.096
p4 | +0.038 +0.011 +0.123 +0.303 +0.139 +0.205 +0.061 +0.069 +0.051
p5 | +0.044 +0.032 +0.183 +0.187 +0.107 +0.203 +0.113 +0.068 +0.063
p6 | +0.179 +0.202 +0.058 +0.057 +0.113 +0.068 +0.084 +0.104 +0.136
p7 | +0.108 +0.103 +0.085 +0.108 +0.130 +0.139 +0.106 +0.113 +0.107
V_head0 (9, 4) =
0 | +0.158 -1.394 -1.264 -0.123
1 | +0.197 -0.766 -0.639 -1.657
2 | -0.134 -0.605 -0.688 -0.961
3 | +1.146 -0.971 -1.650 +0.281
4 | +1.222 -1.446 -1.388 -0.736
5 | +1.177 -1.143 -0.061 -0.986
6 | -0.564 -1.140 -0.446 -0.428
7 | +0.398 -1.170 -1.403 +0.329
8 | +0.609 -1.065 -1.276 -1.107
head0_out (9, 4) =
[CLS] | +0.420 -1.066 -0.896 -0.722
p0 | +0.637 -1.112 -0.844 -0.633
p1 | +0.480 -1.072 -0.953 -0.578
p2 | +0.402 -1.114 -1.088 -0.610
p3 | +0.599 -1.088 -1.066 -0.465
p4 | +0.774 -1.069 -1.034 -0.420
p5 | +0.574 -1.040 -0.897 -0.557
p6 | +0.420 -1.097 -1.006 -0.699
p7 | +0.522 -1.100 -0.971 -0.596
worked element head0_out[0,0] = row 0 of A_head0 . column 0 of V_head0
row 0 of A_head0 = [0.1081 0.1568 0.1121 0.0689 0.1127 0.1253 0.1307 0.0746 0.1107]
col 0 of V_head0 = [ 0.1577 0.1974 -0.134 1.1456 1.222 1.1771 -0.564 0.398 0.6086]
(+0.1081)*(+0.1577) + (+0.1568)*(+0.1974) + (+0.1121)*(-0.1340) + (+0.0689)*(+1.1456) +
(+0.1127)*(+1.2220) + (+0.1253)*(+1.1771) + (+0.1307)*(-0.5640) + (+0.0746)*(+0.3980) +
(+0.1107)*(+0.6086)
= +0.420451 | from array: +0.420451 | MATCH: True
==============================================================================
concat (9, 8) @ W_O (8, 8) = attn_out (9, 8)
==============================================================================
shape rule: (9 x 8) @ (8 x 8) -> (9 x 8) inner dim 8 cancels
W_O mixes the two heads
==============================================================================
concat (9, 8) =
[CLS] | +0.420 -1.066 -0.896 -0.722 +0.531 -0.615 +0.610 +0.172
p0 | +0.637 -1.112 -0.844 -0.633 +0.649 -0.793 +0.586 -0.061
p1 | +0.480 -1.072 -0.953 -0.578 +0.727 -0.733 +0.598 +0.057
p2 | +0.402 -1.114 -1.088 -0.610 +1.059 -0.599 +0.614 +0.396
p3 | +0.599 -1.088 -1.066 -0.465 +0.874 -0.649 +0.610 +0.267
p4 | +0.774 -1.069 -1.034 -0.420 +0.602 -0.704 +0.601 +0.058
p5 | +0.574 -1.040 -0.897 -0.557 +0.464 -0.712 +0.586 +0.010
p6 | +0.420 -1.097 -1.006 -0.699 +0.816 -0.647 +0.616 +0.202
p7 | +0.522 -1.100 -0.971 -0.596 +0.820 -0.692 +0.607 +0.161
W_O (8, 8) =
0 | -0.511 +0.461 +0.063 -0.410 -0.109 +0.340 -0.024 +0.594
1 | -0.151 +0.306 -0.131 +0.403 +0.085 -0.535 -0.567 -0.448
2 | -0.596 -0.520 +0.235 +0.042 +0.306 +0.506 +0.104 +0.277
3 | +0.315 -0.150 -0.317 -0.361 -0.304 -0.276 -0.359 +0.463
4 | +0.315 -0.555 -0.283 -0.585 -0.002 -0.029 +0.406 -0.235
5 | +0.387 +0.573 -0.504 +0.357 +0.110 -0.024 -0.097 +0.349
6 | +0.171 +0.374 +0.494 +0.144 +0.588 +0.132 +0.167 +0.067
7 | -0.501 +0.277 +0.058 -0.060 +0.503 -0.247 +0.029 +0.242
attn_out (9, 8) =
[CLS] | +0.199 +0.071 +0.656 -0.832 +0.186 +0.496 +1.142 -0.111
p0 | +0.173 -0.126 +0.691 -1.092 -0.004 +0.652 +1.192 -0.054
p1 | +0.321 -0.108 +0.592 -1.066 +0.040 +0.475 +1.172 -0.138
p2 | +0.426 -0.090 +0.438 -1.209 +0.207 +0.317 +1.330 -0.167
p3 | +0.340 +0.012 +0.474 -1.233 +0.078 +0.379 +1.186 +0.006
p4 | +0.240 +0.134 +0.564 -1.161 -0.059 +0.493 +1.046 +0.126
p5 | +0.187 +0.052 +0.656 -0.934 +0.015 +0.530 +1.038 -0.015
p6 | +0.341 -0.051 +0.566 -1.036 +0.157 +0.436 +1.261 -0.188
p7 | +0.303 -0.081 +0.564 -1.133 +0.094 +0.473 +1.230 -0.096
worked element attn_out[0,0] = row 0 of concat . column 0 of W_O
row 0 of concat = [ 0.4205 -1.0659 -0.896 -0.7223 0.5309 -0.6145 0.61 0.1724]
col 0 of W_O = [-0.511 -0.151 -0.5956 0.3149 0.3148 0.3875 0.1707 -0.5009]
(+0.4205)*(-0.5110) + (-1.0659)*(-0.1510) + (-0.8960)*(-0.5956) + (-0.7223)*(+0.3149) +
(+0.5309)*(+0.3148) + (-0.6145)*(+0.3875) + (+0.6100)*(+0.1707) + (+0.1724)*(-0.5009)
= +0.199056 | from array: +0.199056 | MATCH: True
Add & Norm 1
==============================================================================
==============================================================================
residual = Z + attn_out all three are (9, 8) - element-wise
==============================================================================
[CLS] |
Z: -0.253 +1.240 -0.258 +1.077 +0.016 +0.770 +0.758 +1.224
attn_out: +0.199 +0.071 +0.656 -0.832 +0.186 +0.496 +1.142 -0.111
residual: -0.054 +1.311 +0.397 +0.244 +0.203 +1.266 +1.900 +1.113
p0 |
Z: +1.018 +1.020 +0.572 -0.189 -0.209 +0.971 -0.146 +1.664
attn_out: +0.173 -0.126 +0.691 -1.092 -0.004 +0.652 +1.192 -0.054
residual: +1.191 +0.894 +1.263 -1.282 -0.213 +1.624 +1.046 +1.609
p1 |
Z: -0.092 -0.590 +1.018 +0.586 -0.121 +0.225 +0.784 +0.334
attn_out: +0.321 -0.108 +0.592 -1.066 +0.040 +0.475 +1.172 -0.138
residual: +0.228 -0.698 +1.610 -0.480 -0.081 +0.700 +1.956 +0.196
p2 |
Z: +0.056 -1.426 -0.251 +2.104 +0.252 +0.990 -0.005 +0.365
attn_out: +0.426 -0.090 +0.438 -1.209 +0.207 +0.317 +1.330 -0.167
residual: +0.482 -1.515 +0.187 +0.895 +0.458 +1.307 +1.325 +0.199
p3 |
Z: -1.473 -0.588 +0.155 +1.222 -0.240 +0.959 +0.183 +1.306
attn_out: +0.340 +0.012 +0.474 -1.233 +0.078 +0.379 +1.186 +0.006
residual: -1.132 -0.575 +0.630 -0.011 -0.162 +1.338 +1.369 +1.312
p4 |
Z: -2.342 -0.051 +0.508 +0.071 -0.700 +1.166 +0.193 +0.106
attn_out: +0.240 +0.134 +0.564 -1.161 -0.059 +0.493 +1.046 +0.126
residual: -2.102 +0.083 +1.072 -1.090 -0.759 +1.659 +1.239 +0.232
p5 |
Z: -1.374 +0.887 +1.368 +0.389 +0.098 +0.207 +0.742 +0.270
attn_out: +0.187 +0.052 +0.656 -0.934 +0.015 +0.530 +1.038 -0.015
residual: -1.187 +0.939 +2.024 -0.545 +0.113 +0.737 +1.781 +0.255
p6 |
Z: +0.483 +0.387 +0.232 +1.859 +0.285 +1.126 +0.031 +0.352
attn_out: +0.341 -0.051 +0.566 -1.036 +0.157 +0.436 +1.261 -0.188
residual: +0.824 +0.335 +0.799 +0.823 +0.442 +1.562 +1.291 +0.163
p7 |
Z: +0.273 -0.080 +0.483 +0.998 -0.200 +0.956 +0.187 +1.306
attn_out: +0.303 -0.081 +0.564 -1.133 +0.094 +0.473 +1.230 -0.096
residual: +0.576 -0.161 +1.048 -0.135 -0.106 +1.429 +1.417 +1.210
check [0,0]: -0.252898 + +0.199056 = -0.053842 MATCH: True
Z1 = LayerNorm(residual) (9, 8) =
[CLS] | -1.317 +0.795 -0.619 -0.856 -0.920 +0.724 +1.706 +0.487
p0 | +0.450 +0.135 +0.526 -2.171 -1.038 +0.908 +0.296 +0.893
p1 | -0.227 -1.275 +1.336 -1.028 -0.577 +0.307 +1.728 -0.263
p2 | +0.077 -2.292 -0.273 +0.567 +0.049 +1.055 +1.077 -0.259
p3 | -1.645 -1.025 +0.316 -0.397 -0.565 +1.103 +1.138 +1.075
p4 | -1.775 +0.034 +0.853 -0.937 -0.663 +1.339 +0.992 +0.158
p5 | -1.665 +0.415 +1.477 -1.036 -0.393 +0.218 +1.238 -0.254
p6 | +0.099 -1.003 +0.042 +0.096 -0.763 +1.766 +1.154 -1.392
p7 | -0.126 -1.238 +0.585 -1.199 -1.155 +1.160 +1.142 +0.830
per-row mean ~ 0: [ 0. -0. -0. 0. -0. -0. 0. -0. 0.]
per-row std ~ 1: [1. 1. 1. 1. 1. 1. 1. 1. 1.]
Feed-forward network: 8 -> 16 -> 8
==============================================================================
==============================================================================
Z1 (9, 8) @ W1 (8, 16) = pre-ReLU (9, 16)
==============================================================================
shape rule: (9 x 8) @ (8 x 16) -> (9 x 16) inner dim 8 cancels
==============================================================================
Z1 (9, 8) =
[CLS] | -1.317 +0.795 -0.619 -0.856 -0.920 +0.724 +1.706 +0.487
p0 | +0.450 +0.135 +0.526 -2.171 -1.038 +0.908 +0.296 +0.893
p1 | -0.227 -1.275 +1.336 -1.028 -0.577 +0.307 +1.728 -0.263
p2 | +0.077 -2.292 -0.273 +0.567 +0.049 +1.055 +1.077 -0.259
p3 | -1.645 -1.025 +0.316 -0.397 -0.565 +1.103 +1.138 +1.075
p4 | -1.775 +0.034 +0.853 -0.937 -0.663 +1.339 +0.992 +0.158
p5 | -1.665 +0.415 +1.477 -1.036 -0.393 +0.218 +1.238 -0.254
p6 | +0.099 -1.003 +0.042 +0.096 -0.763 +1.766 +1.154 -1.392
p7 | -0.126 -1.238 +0.585 -1.199 -1.155 +1.160 +1.142 +0.830
W1 (8, 16) [first 14 of 16 columns] =
0 | +0.296 -0.041 +0.342 +0.269 -0.434 -0.454 +0.121 -0.153 -0.291 +0.080 -0.158 +0.037 -0.040 +0.085
1 | -0.320 +0.197 -0.088 +0.374 +0.015 +0.473 +0.102 -0.276 +0.322 -0.155 -0.152 -0.468 +0.049 +0.034
2 | -0.371 -0.170 -0.178 -0.408 -0.019 +0.188 +0.012 -0.343 -0.123 -0.497 +0.368 -0.415 +0.097 +0.486
3 | -0.264 +0.260 +0.031 +0.221 -0.438 -0.352 -0.367 +0.187 +0.344 +0.250 -0.470 +0.367 -0.146 -0.103
4 | -0.318 +0.064 +0.341 -0.411 +0.035 -0.267 -0.157 -0.026 -0.145 +0.149 -0.020 +0.084 +0.237 +0.058
5 | -0.121 -0.163 +0.400 +0.108 -0.256 -0.002 -0.170 +0.434 -0.492 -0.275 -0.135 -0.012 +0.351 -0.412
6 | +0.342 -0.448 -0.482 +0.197 +0.497 +0.397 +0.076 +0.417 -0.495 +0.475 -0.009 +0.223 +0.321 +0.218
7 | +0.339 -0.295 +0.468 +0.211 -0.300 +0.236 +0.030 +0.207 +0.268 -0.413 +0.006 +0.432 -0.179 +0.094
pre-ReLU (9, 16) [first 14 of 16 columns] =
[CLS] | +0.765 -0.993 -1.055 +0.902 +1.454 +2.195 +0.394 +1.185 -0.517 +0.140 +0.170 +0.026 +0.652 -0.231
p0 | +1.091 -1.256 +0.265 +0.249 +0.357 +1.327 +0.929 +0.037 -1.104 -1.421 +1.023 -0.709 +0.364 +0.236
p1 | +0.764 -1.519 -1.265 -0.755 +1.343 +0.890 +0.397 +0.550 -1.855 +0.017 +1.158 -0.125 +0.799 +0.885
p2 | +0.845 -0.835 +0.094 -0.350 +0.034 -1.019 -0.548 +1.673 -1.660 +0.975 -0.184 +1.517 +0.550 -0.485
p3 | +0.627 -1.334 -0.337 -0.240 +0.806 +1.315 -0.134 +1.542 -0.763 -0.518 +0.578 +0.799 +0.530 -0.118
p4 | -0.165 -1.061 -0.886 -0.374 +1.244 +1.918 +0.100 +0.838 -0.912 -0.865 +0.855 -0.564 +0.895 +0.003
p5 | -0.465 -0.912 -1.665 -0.747 +1.777 +2.131 +0.350 +0.008 -0.651 -0.616 +1.197 -1.120 +0.808 +0.832
p6 | +0.262 -0.626 -0.643 +0.092 +0.412 -0.216 -0.259 +1.245 -2.027 +0.691 -0.134 +0.062 +0.996 -0.666
p7 | +1.356 -1.668 -0.167 -0.001 +0.531 +0.959 +0.401 +1.118 -1.593 -0.699 +0.849 +0.393 +0.528 +0.137
worked element pre-ReLU[0,0] = row 0 of Z1 . column 0 of W1
row 0 of Z1 = [-1.3169 0.7946 -0.6191 -0.8557 -0.9203 0.7243 1.7057 0.4874]
col 0 of W1 = [ 0.2965 -0.3199 -0.3713 -0.2639 -0.3177 -0.1212 0.3423 0.3386]
(-1.3169)*(+0.2965) + (+0.7946)*(-0.3199) + (-0.6191)*(-0.3713) + (-0.8557)*(-0.2639) +
(-0.9203)*(-0.3177) + (+0.7243)*(-0.1212) + (+1.7057)*(+0.3423) + (+0.4874)*(+0.3386)
= +0.764521 | from array: +0.764521 | MATCH: True
ReLU zeroes 61 of 144 hidden activations (42.4%)
==============================================================================
post-ReLU (9, 16) @ W2 (16, 8) = ffn_out (9, 8)
==============================================================================
shape rule: (9 x 16) @ (16 x 8) -> (9 x 8) inner dim 16 cancels
==============================================================================
post-ReLU (9, 16) [first 14 of 16 columns] =
[CLS] | +0.765 +0.000 +0.000 +0.902 +1.454 +2.195 +0.394 +1.185 +0.000 +0.140 +0.170 +0.026 +0.652 +0.000
p0 | +1.091 +0.000 +0.265 +0.249 +0.357 +1.327 +0.929 +0.037 +0.000 +0.000 +1.023 +0.000 +0.364 +0.236
p1 | +0.764 +0.000 +0.000 +0.000 +1.343 +0.890 +0.397 +0.550 +0.000 +0.017 +1.158 +0.000 +0.799 +0.885
p2 | +0.845 +0.000 +0.094 +0.000 +0.034 +0.000 +0.000 +1.673 +0.000 +0.975 +0.000 +1.517 +0.550 +0.000
p3 | +0.627 +0.000 +0.000 +0.000 +0.806 +1.315 +0.000 +1.542 +0.000 +0.000 +0.578 +0.799 +0.530 +0.000
p4 | +0.000 +0.000 +0.000 +0.000 +1.244 +1.918 +0.100 +0.838 +0.000 +0.000 +0.855 +0.000 +0.895 +0.003
p5 | +0.000 +0.000 +0.000 +0.000 +1.777 +2.131 +0.350 +0.008 +0.000 +0.000 +1.197 +0.000 +0.808 +0.832
p6 | +0.262 +0.000 +0.000 +0.092 +0.412 +0.000 +0.000 +1.245 +0.000 +0.691 +0.000 +0.062 +0.996 +0.000
p7 | +1.356 +0.000 +0.000 +0.000 +0.531 +0.959 +0.401 +1.118 +0.000 +0.000 +0.849 +0.393 +0.528 +0.137
W2 (16, 8) =
0 | +0.049 +0.049 -0.298 +0.185 -0.412 -0.361 -0.497 -0.383
1 | -0.027 +0.106 +0.294 -0.393 +0.351 +0.246 -0.091 +0.433
2 | +0.491 -0.295 -0.121 +0.426 +0.222 -0.452 +0.282 +0.328
3 | +0.251 +0.300 +0.325 -0.314 -0.264 +0.134 +0.408 -0.184
4 | +0.088 +0.183 -0.048 +0.214 +0.400 +0.124 +0.040 -0.061
5 | +0.077 -0.145 -0.109 +0.032 -0.433 -0.271 +0.043 -0.068
6 | -0.167 +0.231 +0.194 -0.333 +0.379 -0.005 +0.241 +0.073
7 | +0.498 +0.252 +0.207 +0.279 -0.357 -0.295 +0.214 -0.006
8 | +0.255 -0.397 +0.036 -0.121 -0.043 +0.104 +0.002 +0.040
9 | -0.014 -0.091 +0.272 -0.488 +0.098 +0.066 +0.216 +0.099
10 | +0.327 +0.459 -0.157 -0.273 -0.076 -0.212 +0.115 +0.412
11 | -0.361 -0.399 -0.244 +0.226 +0.093 -0.398 +0.419 +0.290
12 | -0.477 +0.151 +0.271 -0.126 -0.431 -0.423 -0.396 +0.340
13 | +0.411 -0.377 -0.264 -0.334 -0.314 +0.337 -0.168 -0.189
14 | -0.273 +0.108 -0.121 +0.244 -0.294 +0.288 +0.104 -0.386
15 | -0.085 +0.364 +0.423 -0.034 -0.019 +0.418 +0.087 -0.467
ffn_out (9, 8) =
[CLS] | +0.690 +0.850 +0.204 +0.362 -1.613 -1.099 +0.340 -0.544
p0 | +0.257 +0.679 -0.483 -0.130 -1.123 -0.845 -0.037 -0.267
p1 | +0.575 +0.789 -0.489 -0.048 -1.154 -0.568 -0.320 -0.135
p2 | -0.033 -0.116 +0.066 +0.587 -1.055 -1.470 +0.644 +0.230
p3 | +0.437 +0.476 -0.272 +0.879 -1.452 -1.411 +0.367 +0.004
p4 | +0.257 +0.813 -0.081 +0.410 -1.324 -0.901 +0.176 +0.088
p5 | +0.423 +0.558 -0.486 -0.210 -0.985 -0.448 +0.003 +0.085
p6 | -0.032 +0.579 +0.537 +0.200 -1.001 -0.570 +0.054 -0.031
p7 | +0.379 +0.754 -0.453 +0.530 -1.565 -1.278 -0.155 -0.314
worked element ffn_out[0,0] = row 0 of post-ReLU . column 0 of W2
row 0 of post-ReLU = [0.7645 0. 0. 0.9017 1.4536 2.195 0.3945 1.1848 0. 0.1397 0.1698 0.0264 0.6519 0. 0.4703 0. ]
col 0 of W2 = [ 0.0486 -0.0268 0.4909 0.2505 0.0883 0.0775 -0.1672 0.4977 0.2546 -0.0136 0.3268 -0.3609 -0.477 0.4107 -0.2726 -0.0855]
(+0.7645)*(+0.0486) + (+0.0000)*(-0.0268) + (+0.0000)*(+0.4909) + (+0.9017)*(+0.2505) +
(+1.4536)*(+0.0883) + (+2.1950)*(+0.0775) + (+0.3945)*(-0.1672) + (+1.1848)*(+0.4977) +
(+0.0000)*(+0.2546) + (+0.1397)*(-0.0136) + (+0.1698)*(+0.3268) + (+0.0264)*(-0.3609) +
(+0.6519)*(-0.4770) + (+0.0000)*(+0.4107) + (+0.4703)*(-0.2726) + (+0.0000)*(-0.0855)
= +0.690151 | from array: +0.690151 | MATCH: True
Add & Norm 2 -> ENCODER OUTPUT H
==============================================================================
H (9, 8) =
[CLS] | -0.395 +1.311 -0.236 -0.295 -1.827 -0.205 +1.613 +0.033
p0 | +0.806 +0.897 +0.243 -1.745 -1.627 +0.260 +0.427 +0.738
p1 | +0.544 -0.334 +1.070 -0.956 -1.646 -0.097 +1.661 -0.242
p2 | +0.158 -1.918 -0.054 +1.098 -0.730 -0.230 +1.579 +0.097
p3 | -0.998 -0.393 +0.151 +0.555 -1.742 -0.171 +1.495 +1.103
p4 | -1.335 +0.845 +0.776 -0.420 -1.766 +0.468 +1.141 +0.292
p5 | -1.082 +1.079 +1.095 -1.086 -1.216 -0.095 +1.341 -0.036
p6 | +0.097 -0.377 +0.589 +0.317 -1.667 +1.184 +1.195 -1.339
p7 | +0.489 -0.210 +0.375 -0.385 -2.335 +0.137 +1.188 +0.740
H.shape = (9, 8) one contextual vector per position.
Row 0 is the [CLS] summary; rows 1..8 are the contextualised patches.
7. The Reconstruction Head and the Loss
Text BERT ends with a softmax over the vocabulary and a cross-entropy loss. There is no vocabulary for a continuous signal, so the head projects each encoder output back to the patch dimension and the loss is mean squared error:
$$\hat{p}_i = H_i W_r + b_r \in \mathbb{R}^{16}, \qquad \mathcal{L} = \frac{1}{|\mathcal{M}|}\sum_{i \in \mathcal{M}} \big\lVert \hat{p}_i - p_i \big\rVert^2$$
Crucially the sum runs only over the masked positions $\mathcal{M}$. Unmasked positions were handed to the model as input, so reconstructing them would be copying, not learning.
reconstruction head: (8) -> (16) for each of 8 patches
recon.shape = (8, 16)
==============================================================================
H (patches only) (8, 8) @ W_r (8, 16) = reconstruction (8, 16)
==============================================================================
shape rule: (8 x 8) @ (8 x 16) -> (8 x 16) inner dim 8 cancels
==============================================================================
H (patches only) (8, 8) =
p0 | +0.806 +0.897 +0.243 -1.745 -1.627 +0.260 +0.427 +0.738
p1 | +0.544 -0.334 +1.070 -0.956 -1.646 -0.097 +1.661 -0.242
p2 | +0.158 -1.918 -0.054 +1.098 -0.730 -0.230 +1.579 +0.097
p3 | -0.998 -0.393 +0.151 +0.555 -1.742 -0.171 +1.495 +1.103
p4 | -1.335 +0.845 +0.776 -0.420 -1.766 +0.468 +1.141 +0.292
p5 | -1.082 +1.079 +1.095 -1.086 -1.216 -0.095 +1.341 -0.036
p6 | +0.097 -0.377 +0.589 +0.317 -1.667 +1.184 +1.195 -1.339
p7 | +0.489 -0.210 +0.375 -0.385 -2.335 +0.137 +1.188 +0.740
W_r (8, 16) =
0 | +0.413 -0.252 +0.078 -0.334 -0.466 -0.189 +0.280 -0.222 -0.280 -0.287 +0.015 +0.476 -0.041 +0.057 +0.361 +0.035
1 | -0.316 -0.200 -0.190 -0.103 -0.073 +0.300 -0.151 -0.032 +0.125 -0.122 +0.337 +0.087 -0.206 +0.214 +0.028 +0.034
2 | -0.019 -0.003 +0.265 -0.397 -0.166 -0.424 +0.253 -0.228 +0.397 +0.027 +0.301 +0.479 +0.340 +0.367 -0.092 +0.052
3 | -0.246 -0.304 +0.006 +0.095 -0.161 +0.069 +0.387 +0.057 +0.221 +0.305 +0.489 +0.103 +0.307 +0.463 +0.444 -0.359
4 | -0.094 -0.176 -0.413 +0.133 +0.236 +0.348 -0.377 +0.376 +0.143 +0.204 +0.411 +0.125 -0.164 +0.325 -0.137 -0.466
5 | +0.331 -0.155 +0.274 -0.137 +0.361 -0.280 +0.475 +0.280 -0.386 +0.066 +0.485 -0.029 -0.318 -0.015 +0.012 +0.242
6 | +0.199 -0.097 -0.282 +0.145 -0.078 -0.368 +0.401 +0.191 +0.183 +0.323 +0.030 +0.315 -0.002 -0.433 -0.095 -0.003
7 | +0.220 -0.393 -0.360 -0.236 -0.234 +0.232 -0.249 +0.133 -0.006 +0.073 +0.338 -0.096 +0.271 -0.078 -0.146 +0.459
reconstruction (8, 16) =
p0 | +0.960 +0.061 +0.304 -0.989 -0.697 -0.732 +0.201 -0.722 -0.661 -0.990 -0.745 +0.252 -0.287 -1.255 -0.405 +1.856
p1 | +0.944 +0.455 +0.656 -0.571 -0.750 -1.936 +1.404 -0.770 +0.128 -0.203 -1.005 +0.987 +0.349 -1.323 -0.234 +1.034
p2 | +0.729 -0.017 +0.127 +0.410 -0.502 -1.254 +1.520 +0.076 +0.210 +0.875 -0.454 +0.399 +0.923 -0.838 +0.430 -0.132
p3 | +0.219 -0.086 -0.105 +0.114 -0.468 -0.808 +0.933 -0.039 +0.498 +0.705 -0.212 -0.226 +0.980 -1.126 -0.205 +1.032
p4 | -0.118 +0.305 +0.370 -0.191 +0.094 -0.951 +0.805 -0.207 +0.469 +0.232 -0.074 -0.135 +0.233 -0.903 -0.619 +1.240
p5 | -0.199 +0.495 +0.107 -0.233 +0.000 -0.911 +0.350 -0.338 +0.742 +0.052 -0.373 +0.269 +0.079 -0.903 -0.900 +0.969
p6 | +0.560 +0.473 +1.395 -0.092 +0.087 -2.022 +2.359 -0.372 -0.238 +0.156 -0.320 +0.590 -0.096 -0.685 +0.437 +0.353
p7 | +1.019 +0.018 +0.576 -0.660 -0.980 -1.457 +1.352 -0.724 -0.273 -0.252 -0.745 +0.363 +0.570 -1.390 +0.066 +1.625
worked element reconstruction[3,0] = row 3 of H (patches only) . column 0 of W_r
row 3 of H (patches only) = [-0.9984 -0.393 0.1514 0.555 -1.7423 -0.1711 1.4953 1.1031]
col 0 of W_r = [ 0.4127 -0.3155 -0.0189 -0.2461 -0.0936 0.3307 0.1988 0.2202]
(-0.9984)*(+0.4127) + (-0.3930)*(-0.3155) + (+0.1514)*(-0.0189) + (+0.5550)*(-0.2461) +
(-1.7423)*(-0.0936) + (-0.1711)*(+0.3307) + (+1.4953)*(+0.1988) + (+1.1031)*(+0.2202)
= +0.219234 | from array: +0.219234 | MATCH: True
LOSS - computed on masked patches only
patch masked MSE counts toward loss?
--------------------------------------------------
p0 - 0.775568 no (input, not a target)
p1 - 0.935136 no (input, not a target)
p2 - 0.880113 no (input, not a target)
p3 YES 1.362692 YES
p4 - 0.606813 no (input, not a target)
p5 - 0.589623 no (input, not a target)
p6 - 1.352232 no (input, not a target)
p7 YES 1.349081 YES
--------------------------------------------------
masked patches: ['p3', 'p7']
their MSEs : [1.3627 1.3491]
loss = mean = (1.362692 + 1.349081) / 2
= 2.711772 / 2 = 1.355886
worked example, patch p3:
predicted[:4] = [ 0.2192 -0.0855 -0.1055 0.1145]
true[:4] = [-0.8272 -0.3474 0.638 1.6286]
diff[:4] = [ 1.0465 0.2619 -0.7435 -1.5141]
sum of squares over 16 values = 21.803068
/ 16 = 1.362692
from array = 1.362692 MATCH: True
baseline (predict the mean of the masked patches): 0.492816
model loss with RANDOM, untrained weights : 1.355886
The weights here were never trained, so there is nothing to celebrate in
that number. What has been shown is the mechanism, not a result.
With random weights the reconstruction is unrelated to the target - as it must be. Training is what closes that gap, and the loss above is the signal that would drive it.
8. What [CLS] Is For
The reconstruction head is thrown away after pre-training. What survives is the encoder,
and the usual way to attach a classifier is through the [CLS] position:
$$\hat{y} = \sigma\big(H_{\texttt{[CLS]}} \, w_c + b_c\big)$$
[CLS] carries no signal of its own, so everything in it arrived through attention from
the patches. That is what makes it a whole-recording summary — useful for "does this
epoch contain a seizure?".
H[CLS] = [-0.3946 1.3115 -0.2359 -0.2945 -1.8269 -0.2055 1.6127 0.0333]
logit = H_CLS . w_c + b_c
= (-0.3946)*(-0.0796) + (+1.3115)*(+0.4269) + (-0.2359)*(+0.0815) + (-0.2945)*(+0.1528) +
(-1.8269)*(+0.1913) + (-0.2055)*(+0.0470) + (+1.6127)*(+0.1838) + (+0.0333)*(-0.0418)
= +0.463005
P(seizure) = sigmoid(+0.463005) = 1 / (1 + exp(-0.463005)) = 0.613727
where [CLS] got its content - its attention row (head 0):
attends to weight
------------------------
[CLS] 0.1081 ######
p0 0.1568 #########
p1 0.1121 ######
p2 0.0689 ####
p3 0.1127 ######
p4 0.1253 #######
p5 0.1307 #######
p6 0.0746 ####
p7 0.1107 ######
------------------------
sum = 1.000000
[CLS] began as a fixed learned vector identical for every recording.
Everything that distinguishes one recording's [CLS] from another's came
through these attention weights.
9. Summary
| Stage | Operation | Shape |
|---|---|---|
| Raw EEG | 2 channels x 64 samples | $(2, 64)$ |
| Patching | 8 patches of 8 samples, channels flattened | $(8, 16)$ |
| Patch embedding | $\text{patch}\,W_e + b_e$ | $(8, 8)$ |
Add [CLS] + PE |
prepend and add | $(9, 8)$ |
| Masking | 2 positions → [MASK] vector |
$(9, 8)$ |
| Self-attention | $\mathrm{softmax}(QK^\top/\sqrt{d_k})V$, no mask | $(9, 9)$ weights |
| Encoder output $H$ | attention + FFN, each with Add & Norm | $(9, 8)$ |
| Reconstruction | $H_{1:}W_r + b_r$ | $(8, 16)$ |
| Loss | MSE on masked positions only | scalar |
The one idea: remove the causal mask and the model stops being a generator and becomes an encoder. Prediction of the next item is replaced by reconstruction of a missing one, which is a task that requires understanding context from both directions.
Why this transfers to EEG so directly: BERT never actually required words. It required a sequence of vectors, a way to corrupt some of them, and a loss comparing the reconstruction to the original. Patching supplies the vectors; MSE supplies the loss. The attention mathematics is untouched.
What is deliberately left out: multiple stacked encoder layers (this is one block), learned rather than sinusoidal positional embeddings, the 80/10/10 corruption applied stochastically, GELU instead of ReLU, dropout, next-sentence prediction, and any actual training — every weight here is random.
Compared with the autoregressive Transformer walkthrough:
| Autoregressive (GPT-style) | BERT (this notebook) | |
|---|---|---|
| Mask | causal, upper triangle $= -\infty$ | none |
| Context | left only | both directions |
| Task | predict the next token | reconstruct masked inputs |
| Loss | cross-entropy over vocabulary | MSE over the masked patches |
| Stacks | encoder + decoder | encoder only |
| Good at | generating sequences | representing them |