Skip to main content

Adaptive Entropy-Gated Contrastive Fusion for robust multimodal learning

Project description

AECF: Adaptive Entropy-Gated Contrastive Fusion

PyPI version Python 3.8+ License: MIT

Real-world multimodal systems routinely face missing-input scenarios. AECF is a lightweight fusion layer that maintains both robustness and calibration when modalities are missing at inference time.

📄 Paper: Adaptive Entropy-Gated Contrastive Fusion

🔥 Key Features

  • Adaptive Entropy Control: Dynamically adjusts entropy coefficients per instance for optimal fusion
  • Robust Missing Modality Handling: Maintains performance when modalities are missing at inference
  • Curriculum Learning: Progressive masking based on attention entropy for improved training
  • Drop-in Replacement: Compatible with any attention-based multimodal architecture
  • Calibrated Predictions: Ensures well-calibrated confidence scores across modality subsets
  • PyTorch Optimized: Efficient implementation with gradient checkpointing and numerical stability

🚀 Installation

pip install aecf

Or install from source:

git clone https://github.com/your-username/aecf.git
cd aecf
pip install .

📖 Quick Start

Basic Usage

import torch
from aecf import create_fusion_pool

# Simple factory function (recommended)
fusion_query, attention_pool = create_fusion_pool(
    embed_dim=512,
    num_modalities=3,
    mask_prob=0.15
)

# Forward pass
batch_size = 32
modalities = torch.randn(batch_size, 3, 512)  # [batch, modalities, features]
expanded_query = fusion_query.expand(batch_size, -1, -1)
fused_features = attention_pool(expanded_query, modalities)  # [batch, 1, 512]

Advanced Usage

from aecf import CurriculumMasking, MultimodalAttentionPool

# Custom configuration
curriculum_masking = CurriculumMasking(
    base_mask_prob=0.15,
    entropy_target=0.7,
    min_active=1
)

attention_pool = MultimodalAttentionPool(
    embed_dim=512,
    num_heads=8,
    curriculum_masking=curriculum_masking
)

# Get training info including entropy for loss computation
output, info = attention_pool(query, key, value, return_info=True)
entropy_loss = curriculum_masking.entropy_loss(info['entropy'])

🏗️ Components

CurriculumMasking

Applies entropy-driven adaptive masking to attention weights:

masking = CurriculumMasking(
    base_mask_prob=0.15,    # Base probability for masking attention weights
    entropy_target=0.7,     # Target entropy as fraction of maximum
    min_active=1           # Minimum number of active attention weights
)

MultimodalAttentionPool

Attention-based pooling with optional curriculum masking:

pool = MultimodalAttentionPool(
    embed_dim=512,
    num_heads=8,
    dropout=0.1,
    curriculum_masking=masking  # Optional
)

🔧 Requirements

  • Python 3.8+
  • PyTorch 2.0+
  • NumPy

📄 Citation

@article{aecf2024,
  title={Adaptive Entropy-Gated Contrastive Fusion for Robust Multimodal Learning},
  author={Your Name and Collaborators},
  journal={arXiv preprint arXiv:2505.15417},
  year={2024}
}

📜 License

This project is licensed under the MIT License - see the LICENSE file for details.

🐛 Issues and Support

With gradient checkpointing for memory efficiency

output = pool(query, key, value, use_checkpoint=True)

Get detailed information

output, info = pool(query, key, value, return_info=True)


### 3. Functional Interface
For simple cases without learnable parameters:

```python
from aecf import multimodal_attention_pool

# Fast path for simple attention
output = multimodal_attention_pool(query, modalities)

# With curriculum masking
output = multimodal_attention_pool(
    query, modalities,
    curriculum_masking=masking,
    training=True
)

📊 Integration Examples

Vision-Language Model

import torch
import torch.nn as nn
from aecf import create_fusion_pool

class VisionLanguageModel(nn.Module):
    def __init__(self, img_dim=2048, txt_dim=768, hidden_dim=512, num_classes=1000):
        super().__init__()
        
        # Modality projections
        self.img_proj = nn.Linear(img_dim, hidden_dim)
        self.txt_proj = nn.Linear(txt_dim, hidden_dim)
        
        # AECF fusion layer
        self.fusion_query, self.fusion_pool = create_fusion_pool(
            embed_dim=hidden_dim,
            num_modalities=2,
            mask_prob=0.15
        )
        
        # Classification head
        self.classifier = nn.Linear(hidden_dim, num_classes)
        
    def forward(self, image_feats, text_feats, return_info=False):
        # Project modalities to common space
        img_proj = self.img_proj(image_feats)  # [batch, hidden_dim]
        txt_proj = self.txt_proj(text_feats)   # [batch, hidden_dim]
        
        # Stack modalities
        modalities = torch.stack([img_proj, txt_proj], dim=1)  # [batch, 2, hidden_dim]
        
        # Expand fusion query for batch
        batch_size = modalities.size(0)
        query = self.fusion_query.expand(batch_size, -1, -1)
        
        # Apply AECF fusion
        if return_info:
            fused, info = self.fusion_pool(query, modalities, return_info=True)
            return self.classifier(fused.squeeze(1)), info
        else:
            fused = self.fusion_pool(query, modalities)
            return self.classifier(fused.squeeze(1))

# Usage
model = VisionLanguageModel()
img_feats = torch.randn(32, 2048)
txt_feats = torch.randn(32, 768)

# Training with entropy regularization
logits, info = model(img_feats, txt_feats, return_info=True)
entropy_loss = model.fusion_pool.curriculum_masking.entropy_loss(info['entropy'])
total_loss = F.cross_entropy(logits, labels) + 0.01 * entropy_loss

Multi-Modal Medical Diagnosis

class MedicalDiagnosisModel(nn.Module):
    def __init__(self):
        super().__init__()
        
        # Modality encoders
        self.image_encoder = nn.Sequential(
            nn.Linear(1024, 512),
            nn.ReLU(),
            nn.Dropout(0.1)
        )
        self.lab_encoder = nn.Sequential(
            nn.Linear(50, 512),
            nn.ReLU(),
            nn.Dropout(0.1)
        )
        self.clinical_encoder = nn.Sequential(
            nn.Linear(200, 512),
            nn.ReLU(),
            nn.Dropout(0.1)
        )
        
        # AECF fusion with higher masking for robustness
        self.fusion_query, self.fusion_pool = create_fusion_pool(
            embed_dim=512,
            num_modalities=3,
            mask_prob=0.25,  # Higher masking for medical robustness
            num_heads=8
        )
        
        self.classifier = nn.Linear(512, 10)  # 10 disease classes
        
    def forward(self, image=None, lab=None, clinical=None):
        modalities = []
        
        # Handle missing modalities gracefully
        if image is not None:
            modalities.append(self.image_encoder(image))
        if lab is not None:
            modalities.append(self.lab_encoder(lab))
        if clinical is not None:
            modalities.append(self.clinical_encoder(clinical))
            
        if not modalities:
            raise ValueError("At least one modality must be provided")
        
        # Stack available modalities
        modality_tensor = torch.stack(modalities, dim=1)
        batch_size = modality_tensor.size(0)
        
        query = self.fusion_query.expand(batch_size, -1, -1)
        fused = self.fusion_pool(query, modality_tensor)
        
        return self.classifier(fused.squeeze(1))

🧪 Testing and Validation

Running Tests

# Run comprehensive test suite
python -m pytest test_suite/ -v

# Run specific component tests
python -m pytest test_suite/test_aecf.py::TestCurriculumMasking -v

# Run benchmark tests
python -m pytest test_suite/aecf_benchmark_suite.py -v

Running COCO Experiments

# Download COCO features (if not present)
cd aecf/coco_tests/coco_features/
# Place your CLIP features: train_60k_clip_feats.pt, val_5k_clip_feats.pt, test_5k_clip_feats.pt

# Run comprehensive benchmark
python -m aecf.coco_tests.main_test

# Run organized experiments
python -m aecf.coco_tests.test_organized

Performance Validation

import torch
from aecf import CurriculumMasking

# Test entropy computation
masking = CurriculumMasking()
weights = torch.softmax(torch.randn(100, 10), dim=-1)
masked_weights, info = masking(weights)

print(f"Original entropy: {info['entropy'].mean():.3f}")
print(f"Mask rate: {info['mask_rate'].mean():.3f}")
print(f"Target entropy: {info['target_entropy'].mean():.3f}")

# Validate numerical stability
extreme_weights = torch.tensor([[1.0, 0.0, 0.0], [0.33, 0.33, 0.34]])
masked, _ = masking(extreme_weights)
assert torch.isfinite(masked).all(), "Should handle extreme distributions"

📈 Performance Characteristics

Memory Efficiency

  • Gradient Checkpointing: Reduces memory usage for large models
  • Vectorized Operations: Efficient batch processing
  • Minimal Parameters: Only learnable fusion query (optional)

Computational Complexity

  • Time: O(n²d) where n is sequence length, d is embedding dimension
  • Space: O(nd) with gradient checkpointing
  • Fast Paths: Optimized single-head attention without curriculum masking

Numerical Stability

  • Entropy Computation: Uses torch.xlogy for stable x*log(x) computation
  • NaN/Inf Handling: Robust handling of degenerate attention weights
  • Gradient Flow: Proper gradient preservation through masking operations

🔧 Advanced Configuration

Custom Curriculum Schedules

class CustomCurriculumMasking(CurriculumMasking):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.step_count = 0
        
    def forward(self, weights):
        # Reduce masking over training steps
        self.base_mask_prob = max(0.05, 0.2 * (0.99 ** self.step_count))
        self.step_count += 1
        return super().forward(weights)

Multi-Scale Fusion

class MultiScaleFusion(nn.Module):
    def __init__(self, dims=[256, 512, 1024]):
        super().__init__()
        self.fusion_layers = nn.ModuleList([
            create_fusion_pool(dim, num_modalities=2)[1] 
            for dim in dims
        ])
        
    def forward(self, multi_scale_features):
        fused_scales = []
        for features, fusion_layer in zip(multi_scale_features, self.fusion_layers):
            query = torch.randn(features.size(0), 1, features.size(-1), device=features.device)
            fused = fusion_layer(query, features)
            fused_scales.append(fused)
        return torch.cat(fused_scales, dim=-1)

📚 API Reference

CurriculumMasking

CurriculumMasking(
    base_mask_prob: float = 0.15,      # Base masking probability (0, 1]
    entropy_target: float = 0.7,       # Target entropy as fraction of max (0, 1]  
    min_active: int = 1                # Minimum active elements >= 1
)

Methods:

  • forward(weights)(masked_weights, info_dict)
  • entropy_loss(entropy)scalar_loss
  • compute_entropy(weights)entropy_tensor

MultimodalAttentionPool

MultimodalAttentionPool(
    embed_dim: int,                               # Embedding dimension
    num_heads: int = 1,                          # Number of attention heads
    dropout: float = 0.0,                        # Dropout probability [0, 1]
    bias: bool = True,                           # Add bias to projections
    curriculum_masking: CurriculumMasking = None, # Optional masking module
    batch_first: bool = True,                    # Batch-first tensor format
    device: torch.device = None,                 # Device for parameters
    dtype: torch.dtype = None                    # Parameter dtype
)

Methods:

  • forward(query, key, value=None, ...)output or (output, info)

Factory Functions

create_fusion_pool(
    embed_dim: int,          # Feature dimension
    num_modalities: int,     # Number of input modalities  
    mask_prob: float = 0.15, # Base masking probability
    **kwargs                 # Additional arguments to MultimodalAttentionPool
)  (fusion_query, attention_pool)

🤝 Contributing

We welcome contributions! Please see our Contributing Guidelines for details.

Development Setup

git clone https://github.com/your-username/aecf.git
cd aecf
pip install -r requirements.txt
pip install -e .  # Install in development mode

# Run tests
python -m pytest test_suite/ -v

# Run style checks
flake8 aecf/
black aecf/

📄 Citation

@article{aecf2024,
  title={Adaptive Entropy-Gated Contrastive Fusion for Robust Multimodal Learning},
  author={Your Name and Collaborators},
  journal={arXiv preprint arXiv:2505.15417},
  year={2024}
}

📜 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙋‍♀️ Support


🔍 Troubleshooting

Common Issues

Q: Getting NaN losses during training? A: Ensure your input features are properly normalized and not containing NaN/Inf values. AECF includes robust handling, but extreme input distributions can still cause issues.

# Normalize features before fusion
features = F.normalize(features, p=2, dim=-1)

Q: Memory issues with large sequences? A: Use gradient checkpointing and consider reducing batch size:

output = pool(query, key, value, use_checkpoint=True)

Q: Poor performance with missing modalities? A: Increase the mask_prob parameter to train with more aggressive masking:

masking = CurriculumMasking(base_mask_prob=0.3)  # Higher masking

Q: Want to disable curriculum learning? A: Set curriculum_masking=None or use the functional interface:

pool = MultimodalAttentionPool(embed_dim=512, curriculum_masking=None)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aecf-0.1.0.tar.gz (19.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aecf-0.1.0-py3-none-any.whl (15.2 kB view details)

Uploaded Python 3

File details

Details for the file aecf-0.1.0.tar.gz.

File metadata

  • Download URL: aecf-0.1.0.tar.gz
  • Upload date:
  • Size: 19.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.4

File hashes

Hashes for aecf-0.1.0.tar.gz
Algorithm Hash digest
SHA256 170b6b573320dbd1857dd879b154a43f98fba967e7a7c68f076cd7e8d473be7d
MD5 6c2e3fbbddbeb031af909854290d7b42
BLAKE2b-256 91f75a10429fb953175ae83ad0b8cb6994ac9a3ca2a9c2773354f64fbc61f866

See more details on using hashes here.

File details

Details for the file aecf-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: aecf-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.4

File hashes

Hashes for aecf-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8dc88d6b9796e2691720213ecf3fadbdf388b8b94d3aacc53cf129f7d0aa8d4e
MD5 a90130e04ef11ef1938895821de33fb4
BLAKE2b-256 a999427cd67c563a4a635dfe3dcb2960c176605e69632947201e976d468155d2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page