| |
|
|
| 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 |
| from .base_detector import AbstractDetector |
| from detectors import DETECTOR |
| from loss import LOSSFUNC |
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
|
|
| 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) |
| feature = self.pool(embedding) |
| feature = feature.view(feature.shape[0], -1) |
| cls_out = self.classification_head(feature) |
| 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, embedding = self.backbone(x) |
| feat = self.backbone.pool(embedding) |
| feat = feat.view(feat.shape[0], -1) |
| feat = F.normalize(self.head(feat), dim=1) |
| return cls_out, feat, embedding |
|
|
|
|
| |
| |
| |
|
|
| 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)) |
|
|
| |
| anchor_dot_contrast = torch.div( |
| torch.matmul(anchor_feature, contrast_feature.T), |
| self.temperature) |
|
|
| |
| logits_max, _ = torch.max(anchor_dot_contrast, dim=1, keepdim=True) |
| logits = anchor_dot_contrast - logits_max.detach() |
|
|
| |
| mask = mask.repeat(anchor_count, contrast_count) |
|
|
| |
| 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 |
|
|
| |
| exp_logits = torch.exp(logits) * logits_mask |
| log_prob = logits - torch.log(exp_logits.sum(1, keepdim=True)) |
| pos_per_sample = mask.sum(1) |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| @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'] |
| self.num_classes = self.backbone_config['num_classes'] |
| |
| self.backbone = self.build_backbone(config) |
|
|
| |
| self.loss_modules = self.build_loss(config) |
|
|
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| def features(self, data_dict: dict) -> torch.Tensor: |
| """ |
| Return the embedding features from the backbone network. |
| """ |
| x = data_dict['image'] |
| cls_out, feat_vec, embedding = self.backbone(x) |
| |
| 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`. |
| """ |
| |
| |
| 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) |
|
|
| |
| prob = cls_out |
|
|
| |
| |
| contrast_feat = feat_vec.unsqueeze(1) |
|
|
| pred_dict = { |
| 'cls': cls_out, |
| 'prob': prob, |
| 'feat': contrast_feat, |
| 'embedding': embedding |
| } |
| 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'] |
| cls = pred_dict['cls'] |
| feat = pred_dict['feat'] |
|
|
| 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 |
| |
|
|