multisense_df / src /models /fusion.py
vijay-2132's picture
Clean deployment without large checkpoints
a94bd47
Raw
History Blame Contribute Delete
5.39 kB
"""
MultiSense-DF — Cross-Modal Attention Fusion Module
Fuses visual, audio, and lip-sync embeddings via Transformer self-attention
"""
import torch
import torch.nn as nn
class CrossModalFusion(nn.Module):
"""
Takes 3 modality embeddings (visual, audio, lip-sync), each 512-d,
stacks them as a 3-token sequence, and applies 2 layers of multi-head
self-attention so each modality can attend to the others.
Per-modality auxiliary heads + global classification head.
Input : vis_emb (B, 512)
aud_emb (B, 512)
sync_emb (B, 512)
Output:
global_logit (B, 1)
per_mod_logits: dict {'visual': (B,1), 'audio': (B,1), 'lipsync': (B,1)}
attn_weights list of attention weight tensors per layer
"""
def __init__(self, embed_dim=512, num_heads=8, num_fusion_layers=2,
num_modalities=3, dropout=0.1):
super().__init__()
self.num_modalities = num_modalities
# Learnable modality-type embeddings (like segment embeddings in BERT)
self.modality_embed = nn.Parameter(
torch.zeros(1, num_modalities, embed_dim)
)
nn.init.trunc_normal_(self.modality_embed, std=0.02)
# Fusion Transformer (2 layers, 8 heads)
encoder_layer = nn.TransformerEncoderLayer(
d_model=embed_dim, nhead=num_heads,
dim_feedforward=embed_dim * 4,
dropout=dropout, batch_first=True, norm_first=True
)
self.fusion_transformer = nn.TransformerEncoder(
encoder_layer, num_layers=num_fusion_layers,
enable_nested_tensor=False
)
self.norm = nn.LayerNorm(embed_dim)
# Per-modality classification heads (auxiliary)
self.vis_head = self._make_head(embed_dim, dropout)
self.aud_head = self._make_head(embed_dim, dropout)
self.sync_head = self._make_head(embed_dim, dropout)
# Global classification head (mean-pool over modality tokens)
self.global_head = self._make_head(embed_dim, dropout)
@staticmethod
def _make_head(embed_dim, dropout):
return nn.Sequential(
nn.Linear(embed_dim, 256),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(256, 1)
)
def forward(self, vis_emb, aud_emb, sync_emb):
# Stack: (B, 3, 512)
tokens = torch.stack([vis_emb, aud_emb, sync_emb], dim=1)
tokens = tokens + self.modality_embed # add modality type embedding
# Cross-modal self-attention
fused = self.fusion_transformer(tokens) # (B, 3, 512)
fused = self.norm(fused)
vis_out = fused[:, 0] # (B, 512)
aud_out = fused[:, 1]
sync_out = fused[:, 2]
global_ = fused.mean(1) # mean-pool
global_logit = self.global_head(global_)
per_mod_logits = {
'visual': self.vis_head(vis_out),
'audio': self.aud_head(aud_out),
'lipsync': self.sync_head(sync_out)
}
# Attention weights for explainability (hook-based — approximated here)
attn_weights = {
'visual_weight': vis_out.norm(dim=-1, keepdim=True),
'audio_weight': aud_out.norm(dim=-1, keepdim=True),
'lipsync_weight': sync_out.norm(dim=-1, keepdim=True),
}
return global_logit, per_mod_logits, attn_weights
class MultiSenseDF(nn.Module):
"""
Complete MultiSense-DF system.
Composes all three branches + cross-modal fusion.
"""
def __init__(self, visual_branch, audio_branch, lipsync_branch,
embed_dim=512, num_heads=8, dropout=0.1):
super().__init__()
self.visual = visual_branch
self.audio = audio_branch
self.lipsync = lipsync_branch
self.fusion = CrossModalFusion(
embed_dim=embed_dim, num_heads=num_heads,
dropout=dropout
)
def forward(self, frames, waveform, mouth_crops, mel_specs,
audio_mask=None):
vis_emb, vis_logit = self.visual(frames)
aud_emb, aud_logit = self.audio(waveform, audio_mask)
sync_emb, sync_logit, sync_profile = self.lipsync(mouth_crops, mel_specs)
global_logit, per_mod_logits, attn_weights = self.fusion(
vis_emb, aud_emb, sync_emb
)
return {
'global_logit': global_logit,
'per_mod_logits': per_mod_logits,
'branch_logits': {
'visual': vis_logit,
'audio': aud_logit,
'lipsync': sync_logit
},
'sync_profile': sync_profile,
'attn_weights': attn_weights
}
if __name__ == '__main__':
from visual_branch import VisualBranch
from audio_branch import AudioBranch
from lipsync_branch import LipSyncBranch
vis = VisualBranch(num_frames=125)
aud = AudioBranch()
sync = LipSyncBranch()
model = MultiSenseDF(vis, aud, sync)
frames = torch.randn(1, 125, 3, 224, 224)
waveform = torch.randn(1, 80000)
mouths = torch.randn(1, 125, 3, 96, 96)
mels = torch.randn(1, 125, 1, 80, 16)
out = model(frames, waveform, mouths, mels)
print('Global logit:', out['global_logit'].shape)
print('Per-mod logits:', {k: v.shape for k, v in out['per_mod_logits'].items()})