PowerMachine commited on
Commit
678de14
·
verified ·
1 Parent(s): f3b2bf2

Delete models/multimodal_attention.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. models/multimodal_attention.py +0 -470
models/multimodal_attention.py DELETED
@@ -1,470 +0,0 @@
1
- """multimodal_attention.py — Multi-head Attention Multimodal para CNN-BiGRU.
2
-
3
- Implementa atenção multi-cabeça cross-modal que combina informações de
4
- múltiplas modalidades (texto A, texto B, imagem, áudio) via mecanismo
5
- de atenção cruzada hierárquica.
6
-
7
- ==============================================================================
8
- ANÁLISE MATEMÁTICA E LÓGICA
9
- ==============================================================================
10
-
11
- Dadas as representações de N modalidades:
12
- H_1, H_2, ..., H_N onde H_i ∈ R^{B×T_i×d_i}
13
-
14
- A multimodal multi-head attention computa:
15
-
16
- Para cada par (i, j) com i != j:
17
- Q_i = H_i W_Q^i [B, T_i, d]
18
- K_j = H_j W_K^j [B, T_j, d]
19
- V_j = H_j W_V^j [B, T_j, d]
20
-
21
- A_{i->j} = softmax(Q_i K_j^T / sqrt(d_h)) V_j [B, T_i, d]
22
-
23
- Output para modalidade i:
24
- H_i' = H_i + sum_{j != i} A_{i->j}
25
-
26
- Em seguida, uma camada de fusão final agrega todas as H_i':
27
- H_fused = sum_i alpha_i * Pool(H_i') onde alpha_i é um gate de modalidade
28
-
29
- VANTAGENS:
30
- 1. Cada modalidade "olha" para todas as outras — descoberta de correlações
31
- 2. Atenção multi-cabeça permite capturar diferentes tipos de relações
32
- 3. Gate de modalidade permite PESAR dinamicamente cada modalidade
33
- (e.g., imagem é mais importante para pergunta visual)
34
-
35
- ==============================================================================
36
- INTEGRAÇÃO
37
- ==============================================================================
38
-
39
- - Recebe as saídas do CooperativeCNNBiGRU (seq_A, seq_B) e dos encoders
40
- multimodais (img_seq, audio_seq)
41
- - Substitui a MultimodalFusion simples (gated) por uma versão com atenção
42
- - Mantém compatibilidade com a interface existente
43
-
44
- Autor: CNN-BiGRU Project
45
- """
46
- from __future__ import annotations
47
-
48
- import logging
49
- import math
50
- from dataclasses import dataclass, field
51
- from typing import Dict, List, Optional, Tuple
52
-
53
- import torch
54
- import torch.nn as nn
55
- import torch.nn.functional as F
56
-
57
- logger = logging.getLogger(__name__)
58
-
59
-
60
- # ============================================================================
61
- # Configuração
62
- # ============================================================================
63
-
64
- @dataclass
65
- class MultimodalAttentionConfig:
66
- """Configuração da atenção multimodal multi-cabeça."""
67
- # Dimensões por modalidade (input)
68
- d_text_a: int = 128
69
- d_text_b: int = 128
70
- d_image: int = 32
71
- d_audio: int = 32
72
- # Dimensão interna (todas modalidades são projetadas para d_model)
73
- d_model: int = 128
74
- # Número de cabeças
75
- n_heads: int = 4
76
- # Dropout
77
- dropout: float = 0.1
78
- # Modos de fusão final
79
- fusion_mode: str = "attention" # "attention" | "gated" | "concat"
80
- # Usar gate de modalidade (alpha_i)
81
- use_modality_gate: bool = True
82
- # Device
83
- device: str = "cpu"
84
-
85
-
86
- # ============================================================================
87
- # Cross-Modal Attention Block
88
- # ============================================================================
89
-
90
- class CrossModalAttention(nn.Module):
91
- """Atenção cruzada entre duas modalidades.
92
-
93
- A modalidade "query" atende sobre a modalidade "key_value".
94
- """
95
-
96
- def __init__(
97
- self,
98
- d_query: int,
99
- d_kv: int,
100
- d_model: int,
101
- n_heads: int = 4,
102
- dropout: float = 0.1,
103
- ):
104
- super().__init__()
105
- assert d_model % n_heads == 0, \
106
- f"d_model ({d_model}) deve ser divisível por n_heads ({n_heads})"
107
- self.d_model = d_model
108
- self.n_heads = n_heads
109
- self.head_dim = d_model // n_heads
110
-
111
- # Projeções para Query (da modalidade query)
112
- self.q_proj = nn.Linear(d_query, d_model)
113
- # Projeções para Key, Value (da modalidade kv)
114
- self.k_proj = nn.Linear(d_kv, d_model)
115
- self.v_proj = nn.Linear(d_kv, d_model)
116
- # Output projection
117
- self.out_proj = nn.Linear(d_model, d_query)
118
-
119
- self.dropout = nn.Dropout(dropout)
120
- self.scale = 1.0 / math.sqrt(self.head_dim)
121
-
122
- # Init
123
- for layer in (self.q_proj, self.k_proj, self.v_proj, self.out_proj):
124
- nn.init.xavier_uniform_(layer.weight)
125
- nn.init.zeros_(layer.bias)
126
-
127
- def forward(
128
- self,
129
- query: torch.Tensor,
130
- kv: torch.Tensor,
131
- kv_mask: Optional[torch.Tensor] = None,
132
- ) -> torch.Tensor:
133
- """
134
- Args:
135
- query: [B, T_q, d_query]
136
- kv: [B, T_kv, d_kv]
137
- kv_mask: [B, T_kv] (1=valid, 0=pad)
138
-
139
- Returns:
140
- output: [B, T_q, d_query] — query enriquecida com info de kv
141
- """
142
- B, Tq, _ = query.shape
143
- Tkv = kv.size(1)
144
-
145
- # Projeções
146
- q = self.q_proj(query) # [B, T_q, d_model]
147
- k = self.k_proj(kv) # [B, T_kv, d_model]
148
- v = self.v_proj(kv) # [B, T_kv, d_model]
149
-
150
- # Reshape para multi-head: [B, n_heads, T, head_dim]
151
- q = q.view(B, Tq, self.n_heads, self.head_dim).transpose(1, 2)
152
- k = k.view(B, Tkv, self.n_heads, self.head_dim).transpose(1, 2)
153
- v = v.view(B, Tkv, self.n_heads, self.head_dim).transpose(1, 2)
154
-
155
- # Scores: [B, n_heads, T_q, T_kv]
156
- scores = torch.matmul(q, k.transpose(-2, -1)) * self.scale
157
-
158
- # Máscara (padding em kv)
159
- if kv_mask is not None:
160
- scores = scores.masked_fill(
161
- kv_mask.unsqueeze(1).unsqueeze(1) == 0, -1e9
162
- )
163
-
164
- # Softmax
165
- attn = F.softmax(scores, dim=-1)
166
- attn = self.dropout(attn)
167
-
168
- # Contexto: [B, n_heads, T_q, head_dim]
169
- ctx = torch.matmul(attn, v)
170
- # Reshape: [B, T_q, d_model]
171
- ctx = ctx.transpose(1, 2).contiguous().view(B, Tq, self.d_model)
172
-
173
- # Output projection de volta para d_query
174
- return self.out_proj(ctx)
175
-
176
-
177
- # ============================================================================
178
- # Modality Gate (peso dinâmico por modalidade)
179
- # ============================================================================
180
-
181
- class ModalityGate(nn.Module):
182
- """Computa pesos alpha_i ∈ [0, 1] para cada modalidade.
183
-
184
- Usa a representação pooled de cada modalidade para decidir o peso.
185
- """
186
-
187
- def __init__(
188
- self,
189
- d_text_a: int,
190
- d_text_b: int,
191
- d_image: int,
192
- d_audio: int,
193
- d_hidden: int = 64,
194
- ):
195
- super().__init__()
196
- # Projeta cada modalidade para d_hidden
197
- self.proj_a = nn.Linear(d_text_a, d_hidden)
198
- self.proj_b = nn.Linear(d_text_b, d_hidden)
199
- self.proj_img = nn.Linear(d_image, d_hidden)
200
- self.proj_aud = nn.Linear(d_audio, d_hidden)
201
-
202
- # Score: d_hidden -> 1
203
- self.scorer = nn.Linear(d_hidden, 1)
204
- nn.init.zeros_(self.scorer.bias)
205
-
206
- def forward(
207
- self,
208
- h_a: torch.Tensor,
209
- h_b: torch.Tensor,
210
- h_img: torch.Tensor,
211
- h_aud: torch.Tensor,
212
- ) -> Dict[str, torch.Tensor]:
213
- """
214
- Args:
215
- h_a, h_b, h_img, h_aud: [B, d_i] representações pooled
216
-
217
- Returns:
218
- dict com alpha_i ∈ [B, 1] para cada modalidade
219
- """
220
- # Projeta e computa score
221
- s_a = self.scorer(torch.tanh(self.proj_a(h_a)))
222
- s_b = self.scorer(torch.tanh(self.proj_b(h_b)))
223
- s_img = self.scorer(torch.tanh(self.proj_img(h_img)))
224
- s_aud = self.scorer(torch.tanh(self.proj_aud(h_aud)))
225
-
226
- # Stack e softmax sobre as 4 modalidades
227
- scores = torch.cat([s_a, s_b, s_img, s_aud], dim=-1) # [B, 4]
228
- weights = F.softmax(scores, dim=-1) # [B, 4]
229
-
230
- return {
231
- "alpha_a": weights[:, 0:1],
232
- "alpha_b": weights[:, 1:2],
233
- "alpha_img": weights[:, 2:3],
234
- "alpha_aud": weights[:, 3:4],
235
- "weights": weights,
236
- }
237
-
238
-
239
- # ============================================================================
240
- # Multimodal Multi-Head Attention
241
- # ============================================================================
242
-
243
- class MultimodalMultiHeadAttention(nn.Module):
244
- """Atenção multi-cabeça cross-modal completa.
245
-
246
- Combina 4 modalidades (texto A, texto B, imagem, áudio) via atenção
247
- cruzada par a par + gate de modalidade.
248
- """
249
-
250
- def __init__(self, config: MultimodalAttentionConfig):
251
- super().__init__()
252
- self.config = config
253
-
254
- # Cross-modal attention: cada par (i, j) com i != j
255
- # Total: 4*3 = 12 pares direcionais
256
- # Simplificação: projetar todas para d_model, usar 4 blocos de cross-attn
257
- # Texto A atende B, img, aud
258
- self.attn_a_to_b = CrossModalAttention(
259
- config.d_text_a, config.d_text_b, config.d_model, config.n_heads, config.dropout
260
- )
261
- self.attn_a_to_img = CrossModalAttention(
262
- config.d_text_a, config.d_image, config.d_model, config.n_heads, config.dropout
263
- )
264
- self.attn_a_to_aud = CrossModalAttention(
265
- config.d_text_a, config.d_audio, config.d_model, config.n_heads, config.dropout
266
- )
267
- # Texto B atende A, img, aud
268
- self.attn_b_to_a = CrossModalAttention(
269
- config.d_text_b, config.d_text_a, config.d_model, config.n_heads, config.dropout
270
- )
271
- self.attn_b_to_img = CrossModalAttention(
272
- config.d_text_b, config.d_image, config.d_model, config.n_heads, config.dropout
273
- )
274
- self.attn_b_to_aud = CrossModalAttention(
275
- config.d_text_b, config.d_audio, config.d_model, config.n_heads, config.dropout
276
- )
277
- # Imagem atende A, B, aud (opcional — pode ser pesado)
278
- self.attn_img_to_a = CrossModalAttention(
279
- config.d_image, config.d_text_a, config.d_model, config.n_heads, config.dropout
280
- )
281
- self.attn_img_to_b = CrossModalAttention(
282
- config.d_image, config.d_text_b, config.d_model, config.n_heads, config.dropout
283
- )
284
- # Áudio atende A, B
285
- self.attn_aud_to_a = CrossModalAttention(
286
- config.d_audio, config.d_text_a, config.d_model, config.n_heads, config.dropout
287
- )
288
- self.attn_aud_to_b = CrossModalAttention(
289
- config.d_audio, config.d_text_b, config.d_model, config.n_heads, config.dropout
290
- )
291
-
292
- # LayerNorm para cada modalidade (estabilidade)
293
- self.ln_a = nn.LayerNorm(config.d_text_a)
294
- self.ln_b = nn.LayerNorm(config.d_text_b)
295
- self.ln_img = nn.LayerNorm(config.d_image)
296
- self.ln_aud = nn.LayerNorm(config.d_audio)
297
-
298
- # Modality gate
299
- self.gate = ModalityGate(
300
- config.d_text_a, config.d_text_b, config.d_image, config.d_audio
301
- ) if config.use_modality_gate else None
302
-
303
- # Pooling projections (para fusão final)
304
- d_total = config.d_text_a + config.d_text_b + config.d_image + config.d_audio
305
- if config.fusion_mode == "attention":
306
- self.fusion_proj = nn.Linear(d_total, config.d_model)
307
- elif config.fusion_mode == "gated":
308
- self.fusion_proj = nn.Linear(d_total, config.d_model)
309
- else: # concat
310
- self.fusion_proj = nn.Linear(d_total, config.d_model)
311
-
312
- def forward(
313
- self,
314
- seq_a: torch.Tensor,
315
- seq_b: torch.Tensor,
316
- seq_img: Optional[torch.Tensor] = None,
317
- seq_aud: Optional[torch.Tensor] = None,
318
- mask_a: Optional[torch.Tensor] = None,
319
- mask_b: Optional[torch.Tensor] = None,
320
- mask_img: Optional[torch.Tensor] = None,
321
- mask_aud: Optional[torch.Tensor] = None,
322
- ) -> Dict[str, torch.Tensor]:
323
- """Forward pass completo.
324
-
325
- Args:
326
- seq_a: [B, T_a, d_a]
327
- seq_b: [B, T_b, d_b]
328
- seq_img: [B, T_img, d_img] ou None
329
- seq_aud: [B, T_aud, d_aud] ou None
330
- masks: máscaras de padding correspondentes
331
-
332
- Returns:
333
- dict com:
334
- fused: [B, d_model] representação fundida
335
- seq_a_out, seq_b_out, seq_img_out, seq_aud_out: saídas enriquecidas
336
- modality_weights: pesos do gate (se ativo)
337
- """
338
- B = seq_a.size(0)
339
- device = seq_a.device
340
-
341
- # Default para modalidades ausentes: zeros
342
- if seq_img is None:
343
- seq_img = torch.zeros(B, 1, self.config.d_image, device=device)
344
- mask_img = torch.ones(B, 1, device=device)
345
- if seq_aud is None:
346
- seq_aud = torch.zeros(B, 1, self.config.d_audio, device=device)
347
- mask_aud = torch.ones(B, 1, device=device)
348
-
349
- # Cross-modal attention (com residual + LayerNorm)
350
- # Texto A atende B, img, aud
351
- a_to_b = self.attn_a_to_b(seq_a, seq_b, mask_b)
352
- a_to_img = self.attn_a_to_img(seq_a, seq_img, mask_img)
353
- a_to_aud = self.attn_a_to_aud(seq_a, seq_aud, mask_aud)
354
- seq_a_out = self.ln_a(seq_a + a_to_b + a_to_img + a_to_aud)
355
-
356
- # Texto B atende A, img, aud
357
- b_to_a = self.attn_b_to_a(seq_b, seq_a, mask_a)
358
- b_to_img = self.attn_b_to_img(seq_b, seq_img, mask_img)
359
- b_to_aud = self.attn_b_to_aud(seq_b, seq_aud, mask_aud)
360
- seq_b_out = self.ln_b(seq_b + b_to_a + b_to_img + b_to_aud)
361
-
362
- # Imagem atende A, B
363
- img_to_a = self.attn_img_to_a(seq_img, seq_a, mask_a)
364
- img_to_b = self.attn_img_to_b(seq_img, seq_b, mask_b)
365
- seq_img_out = self.ln_img(seq_img + img_to_a + img_to_b)
366
-
367
- # Áudio atende A, B
368
- aud_to_a = self.attn_aud_to_a(seq_aud, seq_a, mask_a)
369
- aud_to_b = self.attn_aud_to_b(seq_aud, seq_b, mask_b)
370
- seq_aud_out = self.ln_aud(seq_aud + aud_to_a + aud_to_b)
371
-
372
- # Pooling (mean sobre a sequência, com máscara)
373
- def pool(seq, mask):
374
- if mask is None:
375
- return seq.mean(dim=1)
376
- m = mask.unsqueeze(-1).float()
377
- return (seq * m).sum(dim=1) / m.sum(dim=1).clamp(min=1.0)
378
-
379
- pooled_a = pool(seq_a_out, mask_a) # [B, d_a]
380
- pooled_b = pool(seq_b_out, mask_b) # [B, d_b]
381
- pooled_img = pool(seq_img_out, mask_img) # [B, d_img]
382
- pooled_aud = pool(seq_aud_out, mask_aud) # [B, d_aud]
383
-
384
- # Modality gate (opcional)
385
- if self.gate is not None:
386
- gate_out = self.gate(pooled_a, pooled_b, pooled_img, pooled_aud)
387
- alpha_a = gate_out["alpha_a"]
388
- alpha_b = gate_out["alpha_b"]
389
- alpha_img = gate_out["alpha_img"]
390
- alpha_aud = gate_out["alpha_aud"]
391
- modality_weights = gate_out["weights"]
392
- else:
393
- alpha_a = alpha_b = alpha_img = alpha_aud = 1.0
394
- modality_weights = None
395
-
396
- # Fusão final
397
- # Concat + projeção (com gate aplicado)
398
- if self.gate is not None:
399
- pooled_a_g = pooled_a * alpha_a
400
- pooled_b_g = pooled_b * alpha_b
401
- pooled_img_g = pooled_img * alpha_img
402
- pooled_aud_g = pooled_aud * alpha_aud
403
- else:
404
- pooled_a_g = pooled_a
405
- pooled_b_g = pooled_b
406
- pooled_img_g = pooled_img
407
- pooled_aud_g = pooled_aud
408
-
409
- concat = torch.cat([pooled_a_g, pooled_b_g, pooled_img_g, pooled_aud_g], dim=-1)
410
- fused = self.fusion_proj(concat)
411
-
412
- return {
413
- "fused": fused,
414
- "seq_a_out": seq_a_out,
415
- "seq_b_out": seq_b_out,
416
- "seq_img_out": seq_img_out,
417
- "seq_aud_out": seq_aud_out,
418
- "pooled_a": pooled_a,
419
- "pooled_b": pooled_b,
420
- "pooled_img": pooled_img,
421
- "pooled_aud": pooled_aud,
422
- "modality_weights": modality_weights,
423
- }
424
-
425
-
426
- # ============================================================================
427
- # Self-test
428
- # ============================================================================
429
-
430
- def _self_test():
431
- """Teste rápido do módulo multimodal attention."""
432
- torch.manual_seed(42)
433
- config = MultimodalAttentionConfig(
434
- d_text_a=32, d_text_b=32, d_image=16, d_audio=16,
435
- d_model=64, n_heads=4, dropout=0.1,
436
- use_modality_gate=True,
437
- )
438
- mha = MultimodalMultiHeadAttention(config)
439
- print(f"Params: {sum(p.numel() for p in mha.parameters())}")
440
-
441
- B = 2
442
- seq_a = torch.randn(B, 8, 32)
443
- seq_b = torch.randn(B, 6, 32)
444
- seq_img = torch.randn(B, 4, 16)
445
- seq_aud = torch.randn(B, 5, 16)
446
- mask_a = torch.ones(B, 8)
447
- mask_b = torch.ones(B, 6)
448
- mask_img = torch.ones(B, 4)
449
- mask_aud = torch.ones(B, 5)
450
-
451
- out = mha(seq_a, seq_b, seq_img, seq_aud, mask_a, mask_b, mask_img, mask_aud)
452
- print(f"Fused: {out['fused'].shape}")
453
- print(f"seq_a_out: {out['seq_a_out'].shape}")
454
- print(f"Modality weights: {out['modality_weights']}")
455
-
456
- # Test sem modalidades opcionais
457
- out2 = mha(seq_a, seq_b, None, None, mask_a, mask_b)
458
- print(f"Fused (text only): {out2['fused'].shape}")
459
-
460
-
461
- if __name__ == "__main__":
462
- _self_test()
463
-
464
-
465
- __all__ = [
466
- "MultimodalAttentionConfig",
467
- "CrossModalAttention",
468
- "ModalityGate",
469
- "MultimodalMultiHeadAttention",
470
- ]