| import os |
| import datetime |
| import logging |
| import numpy as np |
| from sklearn import metrics |
| from typing import Union |
| from collections import defaultdict |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import torch.optim as optim |
| from torch.nn import DataParallel |
| from torch.utils.tensorboard import SummaryWriter |
|
|
| from metrics.base_metrics_class import calculate_metrics_for_train, calculate_acc_for_train |
|
|
| from .base_detector import AbstractDetector |
| from detectors import DETECTOR |
| from networks import BACKBONE |
| from loss import LOSSFUNC |
| from transformers import AutoProcessor, CLIPModel, ViTModel, ViTConfig |
| import loralib as lora |
| import copy |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| class LSDAAugmentor(nn.Module): |
| def __init__(self, feature_dim=1024, beta_base=0.5, affine_theta_range=(-np.pi/6, np.pi/6), noise_ratio=0.01): |
| super().__init__() |
| self.feature_dim = feature_dim |
| self.beta_base = beta_base |
| self.affine_theta_range = affine_theta_range |
| self.noise_ratio = noise_ratio |
|
|
| def centrifugal_trans(self, z: torch.Tensor) -> torch.Tensor: |
| """Original version: adaptive centrifugal transform (the closer a sample is to the domain center, the farther it is pushed)""" |
| batch_size = z.shape[0] |
| mu = z.mean(dim=0, keepdim=True) |
| dist = torch.norm(z - mu, dim=1, keepdim=True) |
| dist_max = dist.max() + 1e-6 |
| beta = self.beta_base * (1 - dist / dist_max) |
| z_aug = z + beta * (z - mu) |
| return z_aug |
|
|
| def affine_trans(self, z: torch.Tensor) -> torch.Tensor: |
| """Affine transform that truly performs high-dimensional rotation (block-wise 2D rotations)""" |
| batch_size, feat_dim = z.shape |
| device = z.device |
|
|
| |
| assert feat_dim % 2 == 0, f"feature dimension{feat_dim}must be even in order to perform block-wise 2D rotations" |
| num_blocks = feat_dim // 2 |
|
|
| |
| theta = torch.rand(batch_size, num_blocks, device=device) * (self.affine_theta_range[1] - self.affine_theta_range[0]) + self.affine_theta_range[0] |
| |
| scale = torch.rand(batch_size, 1, device=device) * 0.1 + 0.95 |
|
|
| |
| cos_theta = torch.cos(theta) |
| sin_theta = torch.sin(theta) |
|
|
| |
| z_reshaped = z.reshape(batch_size, num_blocks, 2) |
|
|
| |
| z_rotated = torch.stack([ |
| z_reshaped[..., 0] * cos_theta - z_reshaped[..., 1] * sin_theta, |
| z_reshaped[..., 0] * sin_theta + z_reshaped[..., 1] * cos_theta |
| ], dim=-1) |
|
|
| |
| z_aug = z_rotated.reshape(batch_size, feat_dim) * scale |
|
|
| return z_aug |
|
|
| def additive_trans(self, z: torch.Tensor) -> torch.Tensor: |
| """ |
| Apply adaptive additive noise augmentation to the input tensor based on the feature standard deviation |
| |
| Args: |
| z (torch.Tensor): input feature tensor with shape [batch_size, feature_dim] |
| |
| Returns: |
| torch.Tensor: augmented feature tensor with the same shape as the input |
| |
| Note: |
| 1. noise strength is jointly determined by the standard deviation of the feature dimension and the preset noise_ratio |
| 2. keep statistics computed independently for each batch dimension |
| 3. the generated noise follows a standard normal distribution N(0,1) and is then scaled |
| """ |
| """Original version: adaptive additive noise (based on the feature standard deviation)""" |
| |
| z_std = z.std(dim=0, keepdim=True) |
| noise = torch.randn_like(z) * z_std * self.noise_ratio |
| z_aug = z + noise |
| return z_aug |
|
|
| def forward(self, z: torch.Tensor) -> torch.Tensor: |
| """Original core logic: sequentially stack all intra-domain augmentations during training, while validation/inference returns the original features""" |
| if not self.training: |
| return z |
|
|
| |
| z = self.centrifugal_trans(z) |
| |
| z = self.affine_trans(z) |
| |
| z = self.additive_trans(z) |
|
|
| return z |
|
|
|
|
| @DETECTOR.register_module(module_name='clip_large_fft_lsda') |
| class CLIP_Large_FFT_LSDA_Detector(AbstractDetector): |
| def __init__(self, config): |
| super().__init__() |
| self.config = config |
| self.num_classes = config['backbone_config']['num_classes'] |
|
|
| |
| self.backbone = self.build_backbone(config) |
| self.head = nn.Linear(1024, self.num_classes) |
| self.loss_func = self.build_loss(config) |
|
|
| |
| self.augmentor = LSDAAugmentor(feature_dim=1024) |
|
|
| def build_backbone(self, config): |
| _, backbone = get_clip_visual(model_name=config['pretrained']) |
| return backbone |
|
|
| def build_loss(self, config): |
| |
| loss_class = LOSSFUNC[config['loss_func']] |
| loss_func = loss_class() |
| return loss_func |
|
|
| def features(self, data_dict: dict) -> torch.tensor: |
| """Core behavior: use augmented features for training and original features for validation/inference""" |
| |
| feat = self.backbone(data_dict['image'])['pooler_output'] |
|
|
| |
| feat = self.augmentor(feat) |
|
|
| return feat |
|
|
| def classifier(self, features: torch.tensor) -> torch.tensor: |
| |
| return self.head(features) |
|
|
| def get_losses(self, data_dict: dict, pred_dict: dict) -> dict: |
| |
| label = data_dict['label'] |
| pred = pred_dict['cls'] |
| loss = self.loss_func(pred, label) |
| loss_dict = {'overall': loss} |
| return loss_dict |
|
|
| def get_train_metrics(self, data_dict: dict, pred_dict: dict) -> dict: |
| |
| label = data_dict['label'] |
| pred = pred_dict['cls'] |
| acc, mAP = calculate_acc_for_train(label.detach(), pred.detach(), self.num_classes) |
| metric_batch_dict = {'acc': acc, 'mAP': mAP} |
| return metric_batch_dict |
|
|
| def forward(self, data_dict: dict, inference=False) -> dict: |
| """ |
| - When inference=True (validation/inference), the augmentor automatically returns the original features |
| - When inference=False (training), the augmentor returns augmented features |
| """ |
| |
| if inference: |
| self.augmentor.eval() |
| else: |
| self.augmentor.train() |
|
|
| |
| features = self.features(data_dict) |
| |
| pred = self.classifier(features) |
| prob = torch.softmax(pred, dim=1) |
| pred_dict = {'cls': pred, 'prob': prob, 'feat': features} |
| return pred_dict |
|
|
|
|
| def get_clip_visual(model_name="openai/clip-vit-base-patch16"): |
| processor = AutoProcessor.from_pretrained(model_name) |
| model = CLIPModel.from_pretrained(model_name) |
| return processor, model.vision_model |
|
|