shunliwang
update
8bc3305
Raw
History Blame Contribute Delete
12.4 kB
# detector_dna.py
import os
import logging
import numpy as np
from typing import Union
from collections import defaultdict
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
from scipy.fftpack import dct, idct
from metrics.base_metrics_class import calculate_acc_for_train # multi-class metrics
from .base_detector import AbstractDetector
from detectors import DETECTOR
from loss import LOSSFUNC
logger = logging.getLogger(__name__)
####################################
# Original network definitions: Simple_CNN and SupConNet
####################################
class vgg_layer(nn.Module):
def __init__(self, nin, nout):
super(vgg_layer, self).__init__()
self.main = nn.Sequential(
nn.Conv2d(nin, nout, 3, 1, 1),
nn.BatchNorm2d(nout),
nn.LeakyReLU(0.2)
)
def forward(self, input):
return self.main(input)
class dcgan_conv(nn.Module):
def __init__(self, nin, nout):
super(dcgan_conv, self).__init__()
self.main = nn.Sequential(
nn.Conv2d(nin, nout, 4, 2, 1),
nn.BatchNorm2d(nout),
nn.LeakyReLU(0.2),
)
def forward(self, input):
return self.main(input)
class Simple_CNN(nn.Module):
def __init__(self, class_num, pretrain=False):
super(Simple_CNN, self).__init__()
nc = 3
nf = 64
self.main = nn.Sequential(
dcgan_conv(nc, nf),
vgg_layer(nf, nf),
dcgan_conv(nf, nf * 2),
vgg_layer(nf * 2, nf * 2),
dcgan_conv(nf * 2, nf * 4),
vgg_layer(nf * 4, nf * 4),
dcgan_conv(nf * 4, nf * 8),
vgg_layer(nf * 8, nf * 8),
)
self.pool = nn.AdaptiveAvgPool2d(1)
self.classification_head = nn.Sequential(
nn.Dropout(p=0.2, inplace=True),
nn.Linear(nf * 8, class_num, bias=True)
)
self.pretrain = pretrain
def forward(self, input):
embedding = self.main(input) # [B, nf*8, H', W']
feature = self.pool(embedding) # [B, nf*8, 1, 1]
feature = feature.view(feature.shape[0], -1) # [B, nf*8]
cls_out = self.classification_head(feature) # [B, num_classes]
if not self.pretrain:
cls_out = F.softmax(cls_out, dim=1)
return cls_out, embedding
class SupConNet(nn.Module):
"""backbone + projection head"""
def __init__(self, backbone, head='mlp', dim_in=512, feat_dim=128):
super(SupConNet, self).__init__()
self.backbone = backbone
if head == 'linear':
self.head = nn.Linear(dim_in, feat_dim)
elif head == 'mlp':
self.head = nn.Sequential(
nn.Linear(dim_in, dim_in),
nn.ReLU(inplace=True),
nn.Linear(dim_in, feat_dim)
)
else:
raise ValueError(f'Unknown head type: {head}')
def forward(self, x):
# cls_out: softmax logits from Simple_CNN
cls_out, embedding = self.backbone(x) # embedding: [B, C, H, W]
feat = self.backbone.pool(embedding) # [B, C, 1, 1]
feat = feat.view(feat.shape[0], -1) # [B, C]
feat = F.normalize(self.head(feat), dim=1) # [B, feat_dim]
return cls_out, feat, embedding
##############################
# SupConLoss and AWL
##############################
class SupConLoss(nn.Module):
def __init__(self, temperature=0.07, contrast_mode='all',
base_temperature=0.07):
super(SupConLoss, self).__init__()
self.temperature = temperature
self.contrast_mode = contrast_mode
self.base_temperature = base_temperature
def forward(self, features, labels=None, mask=None):
device = (torch.device('cuda')
if features.is_cuda
else torch.device('cpu'))
if len(features.shape) < 3:
raise ValueError('`features` needs to be [bsz, n_views, ...],'
'at least 3 dimensions are required')
if len(features.shape) > 3:
features = features.view(features.shape[0], features.shape[1], -1)
batch_size = features.shape[0]
if labels is not None and mask is not None:
raise ValueError('Cannot define both `labels` and `mask`')
elif labels is None and mask is None:
mask = torch.eye(batch_size, dtype=torch.float32).to(device)
elif labels is not None:
labels = labels.contiguous().view(-1, 1)
if labels.shape[0] != batch_size:
raise ValueError('Num of labels does not match num of features')
mask = torch.eq(labels, labels.T).float().to(device)
else:
mask = mask.float().to(device)
contrast_count = features.shape[1]
contrast_feature = torch.cat(torch.unbind(features, dim=1), dim=0)
if self.contrast_mode == 'one':
anchor_feature = features[:, 0]
anchor_count = 1
elif self.contrast_mode == 'all':
anchor_feature = contrast_feature
anchor_count = contrast_count
else:
raise ValueError('Unknown mode: {}'.format(self.contrast_mode))
# compute logits
anchor_dot_contrast = torch.div(
torch.matmul(anchor_feature, contrast_feature.T),
self.temperature)
# for numerical stability
logits_max, _ = torch.max(anchor_dot_contrast, dim=1, keepdim=True)
logits = anchor_dot_contrast - logits_max.detach()
# tile mask
mask = mask.repeat(anchor_count, contrast_count)
# mask-out self-contrast cases
logits_mask = torch.scatter(
torch.ones_like(mask),
1,
torch.arange(batch_size * anchor_count).view(-1, 1).to(device),
0
)
mask = mask * logits_mask
# compute log_prob
exp_logits = torch.exp(logits) * logits_mask
log_prob = logits - torch.log(exp_logits.sum(1, keepdim=True))
pos_per_sample = mask.sum(1) # [bsz * anchor_count]
valid_mask = pos_per_sample > 0
mean_log_prob_pos = (mask * log_prob).sum(1)
mean_log_prob_pos = mean_log_prob_pos[valid_mask] / (pos_per_sample[valid_mask] + 1e-8)
loss = - (self.temperature / self.base_temperature) * mean_log_prob_pos
loss = loss.mean()
return loss
class AutomaticWeightedLoss(nn.Module):
def __init__(self, num=2):
super(AutomaticWeightedLoss, self).__init__()
params = torch.ones(num, requires_grad=True)
self.params = torch.nn.Parameter(params)
def forward(self, *x):
loss_sum = 0
for i, loss in enumerate(x):
loss_sum += 0.5 / (self.params[i] ** 2) * loss + torch.log(1 + self.params[i] ** 2)
return loss_sum
####################################
# DNADetector: inherits from AbstractDetector
####################################
@DETECTOR.register_module(module_name='dna_det')
class DNADetector(AbstractDetector):
"""
DNA_DET-style detector:
- backbone: Simple_CNN + SupConNet
- loss: automatically weighted CE + SupConLoss
"""
def __init__(self, config, load_param: Union[bool, str] = False):
super().__init__(config=config, load_param=load_param)
self.config = config
self.backbone_config = config['backbone_config'] # Read backbone_config
self.num_classes = self.backbone_config['num_classes'] # take the 36 classes from backbone_config
# Build the backbone (SupConNet(Simple_CNN))
self.backbone = self.build_backbone(config)
# Build the loss-related modules (CE, SupCon, AWL)
self.loss_modules = self.build_loss(config)
####################################
# Build backbone / loss
####################################
def build_backbone(self, config):
bb_cfg = config['backbone_config']
num_classes = bb_cfg.get('num_classes', 2)
pretrain = bb_cfg.get('pretrain', False)
head_type = bb_cfg.get('head', 'mlp')
dim_in = bb_cfg.get('dim_in', 512)
feat_dim = bb_cfg.get('feat_dim', 128)
base_cnn = Simple_CNN(num_classes, pretrain=pretrain)
backbone = SupConNet(
backbone=base_cnn,
head=head_type,
dim_in=dim_in,
feat_dim=feat_dim
)
return backbone
def build_loss(self, config):
temperature = config.get('temperature', 0.07)
criterion_ce = nn.CrossEntropyLoss()
criterion_con = SupConLoss(temperature=temperature)
awl = AutomaticWeightedLoss(num=2)
return {
'ce': criterion_ce,
'con': criterion_con,
'awl': awl
}
####################################
# Interfaces that AbstractDetector must implement
####################################
def features(self, data_dict: dict) -> torch.Tensor:
"""
Return the embedding features from the backbone network.
"""
x = data_dict['image'] # [B, 3, H, W]
cls_out, feat_vec, embedding = self.backbone(x)
# Return convolutional embeddings for visualization or post-processing.
return embedding
def classifier(self, features: torch.Tensor) -> torch.Tensor:
"""
Use the backbone classification head to output class logits or probabilities.
For consistency, classification is handled directly in `forward`.
If you want to avoid repeated computation, construct `pred_dict` directly in `forward`.
"""
# Assume the input is `data_dict['image']` rather than an embedding tensor,
# for simplicity, classification is handled directly in `forward`, and `classifier` is not called separately.
raise NotImplementedError(
"Classification in DNADetector is handled inside `forward`; `classifier(features)` is not called separately."
)
def forward(self, data_dict: dict, inference: bool = False) -> dict:
"""
Forward process:
- Input: `data_dict['image']` with shape [B, 3, H, W]
- Output: `pred_dict = {'cls', 'prob', 'feat', 'embedding'}`
"""
x = data_dict['image']
cls_out, feat_vec, embedding = self.backbone(x) # cls_out: [B, C], feat_vec: [B, feat_dim]
# `prob` is already softmax output. Remove softmax in `Simple_CNN` if logits are preferred here.
prob = cls_out
# SupConLoss expects features shaped as [B, n_views, feat_dim].
# Assume a single view here and unsqueeze on dimension 1.
contrast_feat = feat_vec.unsqueeze(1) # [B, 1, feat_dim]
pred_dict = {
'cls': cls_out, # [B, num_classes]
'prob': prob, # same as cls_out (softmax)
'feat': contrast_feat, # [B, 1, feat_dim] for SupConLoss
'embedding': embedding # [B, C, H', W'] optional convolutional features
}
return pred_dict
def get_losses(self, data_dict: dict, pred_dict: dict) -> dict:
"""
Compute the total loss and its components:
- CE loss: classification cross-entropy
- SupCon loss: contrastive learning loss
- AWL: automatically weighted overall loss
"""
label = data_dict['label'] # [B]
cls = pred_dict['cls'] # [B, num_classes]
feat = pred_dict['feat'] # [B, n_views, feat_dim]
ce = self.loss_modules['ce'](cls, label)
con = self.loss_modules['con'](feat, labels=label)
overall = self.loss_modules['awl'](ce, con)
loss_dict = {
'overall': overall,
'ce': ce,
'con': con
}
return loss_dict
def get_train_metrics(self, data_dict: dict, pred_dict: dict) -> dict:
"""Compute multi-class metrics (acc + mAP)"""
label = data_dict['label'].detach()
pred_logits = pred_dict['cls'].detach()
acc, mAP = calculate_acc_for_train(label, pred_logits, self.num_classes)
metric_batch_dict = {'acc': acc, 'mAP': mAP}
return metric_batch_dict