AylinMaylinn commited on
Commit
00be985
Β·
verified Β·
1 Parent(s): 68c3e66

Add cross_attention_collapse_proof.md (4,316 bytes)

Browse files
Files changed (1) hide show
  1. cross_attention_collapse_proof.md +110 -0
cross_attention_collapse_proof.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cross-Attention Collapse β€” Mathematical Proof
2
+
3
+ ## The buggy code (pipeline.py:1119-1124)
4
+
5
+ ```python
6
+ def forward(self, input_ids, attention_mask, chart_window):
7
+ t_out = self.text_model(input_ids=input_ids, attention_mask=attention_mask)
8
+ t_pool = t_out.last_hidden_state[:,0] # (B, D) - text CLS pool
9
+ c_pool = self.chart_enc(chart_window) # (B, D) - chart pool
10
+ fused, _ = self.fuse(t_pool.unsqueeze(1), # query (B, 1, D)
11
+ c_pool.unsqueeze(1), # key (B, 1, D)
12
+ c_pool.unsqueeze(1)) # value (B, 1, D)
13
+ f = fused.squeeze(1) # (B, D)
14
+ ```
15
+
16
+ `self.fuse` is `nn.MultiheadAttention(text_dim, num_heads=8, batch_first=True)`.
17
+
18
+ ## Why this collapses
19
+
20
+ Standard MHA forward, with Q, K, V each shaped `(B, T, D)`:
21
+
22
+ 1. Linear projection split into H heads:
23
+ - `Q' = Q @ W_q`, `K' = K @ W_k`, `V' = V @ W_v` β†’ reshape to `(B, H, T, d_head)`
24
+ 2. Scaled-dot scores: `S = Q' @ K'^T / sqrt(d_head)` β†’ `(B, H, T_q, T_k)`
25
+ 3. Attention weights: `A = softmax(S, dim=-1)` β†’ `(B, H, T_q, T_k)`
26
+ 4. Weighted values: `O = A @ V'` β†’ `(B, H, T_q, d_head)` β†’ concat to `(B, T_q, D)`
27
+ 5. Output: `Y = O @ W_o + b_o`
28
+
29
+ In our call, `T_q = T_k = T_v = 1`. Hence:
30
+
31
+ - `S` has shape `(B, H, 1, 1)` β€” a single scalar per head, per batch.
32
+ - `softmax` over a 1-element vector is identically `1.0`. So `A` is identically 1.
33
+ - `O = A @ V' = V'` exactly (per head).
34
+ - `Y = V' @ W_o + b_o`.
35
+
36
+ Plug in: `V' = V @ W_v = (c_pool.unsqueeze(1)) @ W_v`. Squeeze:
37
+
38
+ > `f = c_pool @ W_v @ W_o + b_o`
39
+
40
+ **The text query `t_pool` and key `t_pool` never enter `f`.** `W_q`, `W_k`, and the
41
+ text encoder's entire 336M parameters are mathematically irrelevant to the
42
+ forward pass output.
43
+
44
+ ## Independent sanity check
45
+
46
+ You can verify by toggling the text input β€” any input that produces the same
47
+ `c_pool` will produce the same `f`. Concretely:
48
+
49
+ ```python
50
+ m.train(False) # disable dropout/batchnorm side-effects
51
+ text_a = tokenizer("Apple beats Q3 earnings", return_tensors='pt', padding='max_length', max_length=128)
52
+ text_b = tokenizer("Bitcoin crashes 40%", return_tensors='pt', padding='max_length', max_length=128)
53
+ chart = torch.randn(1, 128, 5) # same chart for both
54
+
55
+ out_a = m(text_a.input_ids, text_a.attention_mask, chart)
56
+ out_b = m(text_b.input_ids, text_b.attention_mask, chart)
57
+ # Predicted: out_a['sig_logits'] == out_b['sig_logits'] (bit-exact)
58
+ ```
59
+
60
+ If this is bit-exact, the bug is confirmed.
61
+
62
+ ## Why it matters
63
+
64
+ All four heads (`head_sig`, `head_dir`, `head_mag`, `head_reason`) consume only
65
+ `f`. So:
66
+
67
+ - The model cannot use news semantics for any prediction.
68
+ - Iteration scoreboard's plateau at 2.495 is the **chart-only loss floor**, not a
69
+ data/label noise floor.
70
+ - The "encoder choice doesn't matter" observation is trivially true because no
71
+ encoder choice is ever exercised.
72
+ - `sig_acc = 0.557 = majority class` is what a chart-only model achieves when it
73
+ cannot read the news that triggers the move.
74
+ - The v6 "winner" claim (delta=0.0004 over v4) is comparing two run-to-run noise
75
+ draws of effectively the same chart-only model.
76
+
77
+ ## The fix
78
+
79
+ Replace the cross-attention with a real fusion. Examples:
80
+
81
+ ```python
82
+ # Option A β€” concatenate then project (no attention, deterministic)
83
+ self.fuse = nn.Sequential(nn.Linear(2*text_dim, text_dim), nn.GELU())
84
+ def forward(...):
85
+ f = self.fuse(torch.cat([t_pool, c_pool], dim=-1))
86
+ ```
87
+
88
+ ```python
89
+ # Option B β€” proper cross-attention with chart used as a SEQUENCE
90
+ # Don't pool c_pool yet; let chart_enc return per-bar features (B, T, D),
91
+ # and use t_pool as query against all T chart bars:
92
+ class ChartEncoder(nn.Module):
93
+ def forward(self, x):
94
+ ...
95
+ return self.proj_out(h) # (B, T, D) β€” DO NOT mean(dim=1)
96
+
97
+ def forward(self, ...):
98
+ c_seq = self.chart_enc(chart_window) # (B, T, D)
99
+ fused, _ = self.fuse(t_pool.unsqueeze(1), # (B, 1, D) query
100
+ c_seq, c_seq) # (B, T, D) key/value
101
+ f = fused.squeeze(1)
102
+ ```
103
+
104
+ Option B is the architecturally honest version of the original intent.
105
+
106
+ ## Severity
107
+
108
+ **CRITICAL.** Until this is fixed, the project is, at best, a chart-only price
109
+ movement classifier conditioned on _which symbol_ the news happened to mention.
110
+ No news semantics are used.