wyctorfogos's picture
Deploy Space
961cf0c
Raw
History Blame Contribute Delete
4.84 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Author: André Pacheco
E-mail: pacheco.comp@gmail.com
This file implements the Context Guided Cell (GCell)
and a full MetaNet + ResNet-50 model.
Paper:
Fusing Metadata and Dermoscopy Images for Skin Disease Diagnosis
IEEE Journal of Biomedical and Health Informatics, 2020
https://ieeexplore.ieee.org/document/9098645
"""
import timm
import torch
import torch.nn as nn
import torch.nn.functional as F
# =====================================================
# MetaNet block (Context Guided Cell)
# =====================================================
class MetaNet(nn.Module):
"""
Metadata-driven channel attention (MetaNet / GCell)
metadata (B, meta_dim) → (B, C, 1, 1)
feat_maps (B, C, H, W) → gated feature maps
"""
def __init__(self, in_channels: int, middle_channels: int, out_channels: int):
super().__init__()
self.metanet = nn.Sequential(
nn.Conv2d(in_channels, middle_channels, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(middle_channels, out_channels, kernel_size=1),
nn.Sigmoid()
)
def forward(self, feat_maps: torch.Tensor, metadata: torch.Tensor) -> torch.Tensor:
"""
feat_maps: (B, C, H, W)
metadata: (B, meta_dim)
"""
m = metadata.unsqueeze(-1).unsqueeze(-1) # (B, meta_dim, 1, 1)
attn = self.metanet(m) # (B, C, 1, 1)
return feat_maps * attn
# =====================================================
# MetaNet + ResNet-50 model
# =====================================================
class MetaNetModel(nn.Module):
"""
MetaNet + ResNet-50 (faithful to IEEE JBHI paper)
"""
def __init__(
self,
meta_dim: int,
num_classes: int = 6,
dropout_fraction: float = 0.3,
image_encoder: str = "resnet50",
pretrained: bool = True,
unfreeze_weights: bool = False
):
super().__init__()
self.meta_dim = meta_dim
self.num_classes = num_classes
self.dropout_fraction = dropout_fraction
self.image_encoder = image_encoder
self.pretrained = pretrained
self.unfreeze_weights = unfreeze_weights
# =====================================================
# 1) CNN backbone (conv features only)
# =====================================================
self.backbone = timm.create_model(
self.image_encoder,
pretrained=self.pretrained,
num_classes=0, # ❗ remove FC
global_pool="" # ❗ remove GAP → retorna (B,C,H,W)
)
self.feat_dim = self.backbone.num_features # 2048 for resnet50
if not self.unfreeze_weights:
for p in self.backbone.parameters():
p.requires_grad = False
# =====================================================
# 2) MetaNet attention (metadata → channel gates)
# =====================================================
self.metanet = MetaNet(
in_channels=self.meta_dim,
middle_channels=128,
out_channels=self.feat_dim
)
# =====================================================
# 3) Classifier (after GAP)
# =====================================================
self.classifier = self.fc_mlp_module(self.feat_dim)
# -----------------------------------------------------
# MLP classifier (stronger than single FC)
# -----------------------------------------------------
def fc_mlp_module(self, input_dim: int) -> nn.Module:
return nn.Sequential(
nn.Linear(input_dim, input_dim),
nn.LayerNorm(input_dim),
nn.ReLU(inplace=True),
nn.Dropout(self.dropout_fraction),
nn.Linear(input_dim, input_dim // 2),
nn.LayerNorm(input_dim // 2),
nn.ReLU(inplace=True),
nn.Dropout(self.dropout_fraction),
nn.Linear(input_dim // 2, self.num_classes)
)
# =====================================================
# Forward
# =====================================================
def forward(self, image: torch.Tensor, metadata: torch.Tensor) -> torch.Tensor:
"""
image: (B, 3, 224, 224)
metadata: (B, meta_dim)
"""
# 1) CNN feature maps
feat_maps = self.backbone(image) # (B, 2048, H, W)
# 2) Metadata-guided channel attention
feat_maps = self.metanet(feat_maps, metadata)
# 3) Global Average Pooling
pooled = F.adaptive_avg_pool2d(feat_maps, 1).flatten(1) # (B, 2048)
# 4) Classification
logits = self.classifier(pooled)
return logits