ushah's picture
Initial commit: GraPHFormer codebase
cf84204
Raw
History Blame Contribute Delete
11.2 kB
"""
Fusion Mechanisms for Multimodal Learning
This module contains various fusion strategies to combine tree and image features:
- CrossAttentionFusion: Cross-modal attention
- CMF: Cross-Modal Fusion
- BiDirectionalCrossAttention: Bidirectional cross-attention
- GatedFusion: Gated fusion with learnable gates
- MultiHeadCrossModalAttention: Multi-head cross-modal attention
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
class CrossAttentionFusion(nn.Module):
"""Cross-attention fusion for tree and image features"""
def __init__(self, dim, num_heads=4, dropout=0.1):
super(CrossAttentionFusion, self).__init__()
self.num_heads = num_heads
self.dim = dim
self.head_dim = dim // num_heads
assert dim % num_heads == 0, "dim must be divisible by num_heads"
# Query, Key, Value projections
self.q_proj = nn.Linear(dim, dim)
self.k_proj = nn.Linear(dim, dim)
self.v_proj = nn.Linear(dim, dim)
self.out_proj = nn.Sequential(
nn.Linear(dim, dim * 4),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(dim * 4, dim),
nn.Dropout(dropout)
)
self.dropout = nn.Dropout(dropout)
self.scale = self.head_dim ** -0.5
def forward(self, tree_feat, image_feat):
"""
Args:
tree_feat: (B, dim) tree features
image_feat: (B, dim) image features
Returns:
fused: (B, dim) fused features
"""
B = tree_feat.shape[0]
# Add sequence dimension: (B, 1, dim)
tree_feat = tree_feat.unsqueeze(1)
image_feat = image_feat.unsqueeze(1)
# Tree attends to image (tree as query, image as key/value)
Q = self.q_proj(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
K = self.k_proj(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
V = self.v_proj(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
# Attention scores
attn = (Q @ K.transpose(-2, -1)) * self.scale
attn = torch.softmax(attn, dim=-1)
attn = self.dropout(attn)
# Apply attention to values
out = (attn @ V).transpose(1, 2).contiguous().view(B, 1, self.dim)
# Project and add residual
out = self.out_proj(out.squeeze(1))
fused = out + tree_feat.squeeze(1)
return fused
class CMF(nn.Module):
"""Cross-Modal Fusion with attention mechanism"""
def __init__(self, dim, dropout=0.1):
super(CMF, self).__init__()
self.dim = dim
# Feature-level attention
self.tree_attn = nn.Sequential(
nn.Linear(dim, dim),
nn.Tanh(),
nn.Linear(dim, 1)
)
self.image_attn = nn.Sequential(
nn.Linear(dim, dim),
nn.Tanh(),
nn.Linear(dim, 1)
)
# Cross-modal interaction
self.cross_proj = nn.Sequential(
nn.Linear(dim * 2, dim),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(dim, dim)
)
def forward(self, tree_feat, image_feat):
"""
Args:
tree_feat: (B, dim)
image_feat: (B, dim)
Returns:
fused: (B, dim)
"""
# Compute attention weights
tree_weight = torch.sigmoid(self.tree_attn(tree_feat))
image_weight = torch.sigmoid(self.image_attn(image_feat))
# Normalize weights
total_weight = tree_weight + image_weight + 1e-8
tree_weight = tree_weight / total_weight
image_weight = image_weight / total_weight
# Weighted combination
weighted_tree = tree_feat * tree_weight
weighted_image = image_feat * image_weight
# Cross-modal projection
combined = torch.cat([weighted_tree, weighted_image], dim=1)
fused = self.cross_proj(combined)
return fused
class BiDirectionalCrossAttention(nn.Module):
"""Bidirectional cross-attention: tree→image and image→tree"""
def __init__(self, dim, num_heads=4, dropout=0.1):
super(BiDirectionalCrossAttention, self).__init__()
self.num_heads = num_heads
self.dim = dim
self.head_dim = dim // num_heads
assert dim % num_heads == 0, "dim must be divisible by num_heads"
# Tree → Image attention
self.tree2img_q = nn.Linear(dim, dim)
self.tree2img_k = nn.Linear(dim, dim)
self.tree2img_v = nn.Linear(dim, dim)
self.tree2img_out = nn.Linear(dim, dim)
# Image → Tree attention
self.img2tree_q = nn.Linear(dim, dim)
self.img2tree_k = nn.Linear(dim, dim)
self.img2tree_v = nn.Linear(dim, dim)
self.img2tree_out = nn.Linear(dim, dim)
self.dropout = nn.Dropout(dropout)
self.scale = self.head_dim ** -0.5
# Layer norm
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
def _compute_attention(self, q_proj, k_proj, v_proj, query, key_value):
"""Helper function to compute cross-attention"""
B = query.shape[0]
# Add sequence dimension
query = query.unsqueeze(1) # (B, 1, dim)
key_value = key_value.unsqueeze(1) # (B, 1, dim)
# Project
Q = q_proj(query).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
K = k_proj(key_value).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
V = v_proj(key_value).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
# Attention
attn = (Q @ K.transpose(-2, -1)) * self.scale
attn = torch.softmax(attn, dim=-1)
attn = self.dropout(attn)
# Apply to values
out = (attn @ V).transpose(1, 2).contiguous().view(B, 1, self.dim)
return out.squeeze(1)
def forward(self, tree_feat, image_feat):
"""
Args:
tree_feat: (B, dim)
image_feat: (B, dim)
Returns:
tree_enhanced: (B, dim)
image_enhanced: (B, dim)
"""
# Tree attends to image
tree_enhanced = self._compute_attention(
self.tree2img_q, self.tree2img_k, self.tree2img_v,
tree_feat, image_feat
)
tree_enhanced = self.tree2img_out(tree_enhanced)
tree_enhanced = self.norm1(tree_feat + tree_enhanced)
# Image attends to tree
image_enhanced = self._compute_attention(
self.img2tree_q, self.img2tree_k, self.img2tree_v,
image_feat, tree_feat
)
image_enhanced = self.img2tree_out(image_enhanced)
image_enhanced = self.norm2(image_feat + image_enhanced)
# Concatenate both enhanced features
fused = torch.cat([tree_enhanced, image_enhanced], dim=1)
return fused
class GatedFusion(nn.Module):
"""Gated fusion with learnable gates for tree and image modalities"""
def __init__(self, dim, dropout=0.1):
super(GatedFusion, self).__init__()
self.dim = dim
# Gating mechanism
self.gate_tree = nn.Sequential(
nn.Linear(dim * 2, dim),
nn.Sigmoid()
)
self.gate_image = nn.Sequential(
nn.Linear(dim * 2, dim),
nn.Sigmoid()
)
# Feature transformation
self.tree_transform = nn.Sequential(
nn.Linear(dim, dim),
nn.ReLU(),
nn.Dropout(dropout)
)
self.image_transform = nn.Sequential(
nn.Linear(dim, dim),
nn.ReLU(),
nn.Dropout(dropout)
)
# Output projection
self.output = nn.Linear(dim, dim)
def forward(self, tree_feat, image_feat):
"""
Args:
tree_feat: (B, dim)
image_feat: (B, dim)
Returns:
fused: (B, dim)
"""
# Concatenate features for gating
combined = torch.cat([tree_feat, image_feat], dim=1)
# Compute gates
gate_t = self.gate_tree(combined)
gate_i = self.gate_image(combined)
# Transform features
tree_transformed = self.tree_transform(tree_feat)
image_transformed = self.image_transform(image_feat)
# Apply gates
gated_tree = gate_t * tree_transformed
gated_image = gate_i * image_transformed
# Combine
fused = gated_tree + gated_image
fused = self.output(fused)
return fused
class MultiHeadCrossModalAttention(nn.Module):
"""Multi-head cross-modal attention for flexible fusion"""
def __init__(self, dim, num_heads=8, dropout=0.1):
super(MultiHeadCrossModalAttention, self).__init__()
self.num_heads = num_heads
self.dim = dim
self.head_dim = dim // num_heads
assert dim % num_heads == 0, "dim must be divisible by num_heads"
# Projections
self.q_tree = nn.Linear(dim, dim)
self.k_tree = nn.Linear(dim, dim)
self.v_tree = nn.Linear(dim, dim)
self.q_image = nn.Linear(dim, dim)
self.k_image = nn.Linear(dim, dim)
self.v_image = nn.Linear(dim, dim)
self.out_proj = nn.Linear(dim * 2, dim)
self.dropout = nn.Dropout(dropout)
self.scale = self.head_dim ** -0.5
self.norm = nn.LayerNorm(dim)
def forward(self, tree_feat, image_feat):
"""
Args:
tree_feat: (B, dim)
image_feat: (B, dim)
Returns:
fused: (B, dim)
"""
B = tree_feat.shape[0]
# Add sequence dimension
tree_feat = tree_feat.unsqueeze(1) # (B, 1, dim)
image_feat = image_feat.unsqueeze(1) # (B, 1, dim)
# Project tree features
Q_t = self.q_tree(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
K_t = self.k_tree(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
V_t = self.v_tree(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
# Project image features
Q_i = self.q_image(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
K_i = self.k_image(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
V_i = self.v_image(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
# Tree self-attention with image context
attn_t = (Q_t @ K_i.transpose(-2, -1)) * self.scale
attn_t = torch.softmax(attn_t, dim=-1)
attn_t = self.dropout(attn_t)
out_t = (attn_t @ V_i).transpose(1, 2).contiguous().view(B, 1, self.dim)
# Image self-attention with tree context
attn_i = (Q_i @ K_t.transpose(-2, -1)) * self.scale
attn_i = torch.softmax(attn_i, dim=-1)
attn_i = self.dropout(attn_i)
out_i = (attn_i @ V_t).transpose(1, 2).contiguous().view(B, 1, self.dim)
# Concatenate and project
combined = torch.cat([out_t.squeeze(1), out_i.squeeze(1)], dim=1)
fused = self.out_proj(combined)
fused = self.norm(fused)
return fused