Vaczination / detector.py
Zonda001's picture
Upload 16 files
00d514b verified
Raw
History Blame Contribute Delete
8.99 kB
"""
Detector — "імунна клітина" системи.
Окрема модель, що розрізняє чисті vs отруєні зразки.
Архітектура:
- Encoder (CNN) → embedding простір
- Poison head: бінарна класифікація clean/poisoned
- Attack-type head: класифікація типу атаки (для аналізу)
- Memory bank: зберігає embedding'и відомих атак ("лімфоцити пам'яті")
Тренування:
- Класифікаційний loss (cross-entropy)
- Contrastive loss (SupCon) — чисті embedding'и кластеризуються разом
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Dict, Tuple, Optional
class DetectorEncoder(nn.Module):
"""Малий CNN-енкодер. Вистачає для CIFAR-розмірів."""
def __init__(self, in_channels: int = 3, embed_dim: int = 128):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(in_channels, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 32 → 16
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 16 → 8
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 8 → 4
nn.Conv2d(128, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
)
self.projection = nn.Linear(128, embed_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
h = self.net(x)
return self.projection(h)
class Detector(nn.Module):
"""
Детектор з двома головами + memory bank.
Forward повертає:
embeddings: (B, embed_dim) — нормалізовані embedding'и
poison_logits: (B, 2) — clean vs poisoned
attack_logits: (B, num_attack_types) — який тип атаки
"""
def __init__(
self,
in_channels: int = 3,
embed_dim: int = 128,
num_attack_types: int = 5, # clean + 4 attack types
):
super().__init__()
self.encoder = DetectorEncoder(in_channels, embed_dim)
self.poison_head = nn.Linear(embed_dim, 2)
self.attack_head = nn.Linear(embed_dim, num_attack_types)
self.embed_dim = embed_dim
# Memory bank — зберігаємо embedding'и відомих атак
self.register_buffer("memory_embeds", torch.zeros(0, embed_dim))
self.register_buffer("memory_attack_ids", torch.zeros(0, dtype=torch.long))
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
embed = self.encoder(x)
embed_normalized = F.normalize(embed, dim=-1)
poison_logits = self.poison_head(embed)
attack_logits = self.attack_head(embed)
return embed_normalized, poison_logits, attack_logits
@torch.no_grad()
def poison_probability(self, x: torch.Tensor) -> torch.Tensor:
"""Повертає P(poisoned) для кожного зразка."""
self.eval()
_, poison_logits, _ = self.forward(x)
probs = F.softmax(poison_logits, dim=-1)
return probs[:, 1]
@torch.no_grad()
def trust_weights(self, x: torch.Tensor, soft: bool = True) -> torch.Tensor:
"""
Ваги довіри для loss-зважування.
soft=True: ваги в [0, 1] = 1 - P(poisoned)
soft=False: hard rejection — 1.0 якщо clean, 0.0 якщо poisoned
"""
p = self.poison_probability(x)
if soft:
return 1.0 - p
return (p < 0.5).float()
@torch.no_grad()
def update_memory(
self,
embeddings: torch.Tensor,
attack_ids: torch.Tensor,
max_size: int = 2000,
):
"""Додає нові сигнатури в memory bank ('лімфоцити пам'яті')."""
self.memory_embeds = torch.cat([self.memory_embeds, embeddings.detach()])
self.memory_attack_ids = torch.cat([self.memory_attack_ids, attack_ids])
if len(self.memory_embeds) > max_size:
# Залишаємо найсвіжіші
self.memory_embeds = self.memory_embeds[-max_size:]
self.memory_attack_ids = self.memory_attack_ids[-max_size:]
@torch.no_grad()
def memory_lookup(self, x: torch.Tensor, k: int = 5) -> Optional[torch.Tensor]:
"""
Шукає k найближчих зразків у memory bank.
Повертає вектор оцінок схожості з відомими атаками.
"""
if len(self.memory_embeds) == 0:
return None
embed, _, _ = self.forward(x)
# Косинусна схожість (embed вже нормалізований)
sim = torch.matmul(embed, self.memory_embeds.T) # (B, M)
top_k_sim, top_k_idx = sim.topk(min(k, sim.size(1)), dim=1)
# Чи нагадує зразок щось з memory (середня top-k схожість)
return top_k_sim.mean(dim=1)
def supervised_contrastive_loss(
embeddings: torch.Tensor, labels: torch.Tensor, temperature: float = 0.1
) -> torch.Tensor:
"""
SupCon loss (Khosla et al., 2020).
Тягне зразки однієї мітки ближче, штовхає інші геть.
Для нас: чисті embedding'и → один кластер, отруєні → окремо.
"""
device = embeddings.device
batch_size = embeddings.size(0)
# similarity matrix
sim = torch.matmul(embeddings, embeddings.T) / temperature
# числова стабільність
sim_max, _ = sim.max(dim=1, keepdim=True)
sim = sim - sim_max.detach()
# маска "позитивних пар" (та сама мітка, виключаючи self)
labels = labels.contiguous().view(-1, 1)
pos_mask = torch.eq(labels, labels.T).float().to(device)
self_mask = torch.eye(batch_size, device=device)
pos_mask = pos_mask - self_mask # виключити діагональ
# log_prob
exp_sim = torch.exp(sim) * (1 - self_mask) # виключити self
log_prob = sim - torch.log(exp_sim.sum(dim=1, keepdim=True) + 1e-12)
# середнє по позитивах
pos_count = pos_mask.sum(dim=1)
pos_count = torch.clamp(pos_count, min=1.0)
mean_log_prob_pos = (pos_mask * log_prob).sum(dim=1) / pos_count
return -mean_log_prob_pos.mean()
def detector_loss(
embeddings: torch.Tensor,
poison_logits: torch.Tensor,
attack_logits: torch.Tensor,
is_poisoned: torch.Tensor,
attack_ids: torch.Tensor,
use_contrastive: bool = True,
contrast_weight: float = 0.5,
attack_weight: float = 0.3,
) -> Tuple[torch.Tensor, Dict[str, float]]:
"""
Комбінований loss для тренування Detector'а.
Args:
embeddings: (B, D) нормалізовані
poison_logits: (B, 2)
attack_logits: (B, num_attack_types)
is_poisoned: (B,) bool
attack_ids: (B,) long — 0 для clean, 1..N для типів атак
"""
is_poisoned_long = is_poisoned.long()
# 1. Бінарна класифікація clean/poisoned
cls_loss = F.cross_entropy(poison_logits, is_poisoned_long)
# 2. Multi-class класифікація типу атаки
atk_loss = F.cross_entropy(attack_logits, attack_ids)
total = cls_loss + attack_weight * atk_loss
metrics = {"cls": cls_loss.item(), "attack_cls": atk_loss.item()}
# 3. SupCon — для розділення в embedding-просторі
if use_contrastive and embeddings.size(0) > 2:
contrast = supervised_contrastive_loss(embeddings, is_poisoned_long)
total = total + contrast_weight * contrast
metrics["contrast"] = contrast.item()
metrics["total"] = total.item()
return total, metrics
if __name__ == "__main__":
# Швидкий тест
detector = Detector(in_channels=3, embed_dim=128, num_attack_types=5)
x = torch.rand(8, 3, 32, 32)
embed, poison_logits, attack_logits = detector(x)
print(f"Embedding shape: {embed.shape}")
print(f"Poison logits shape: {poison_logits.shape}")
print(f"Attack logits shape: {attack_logits.shape}")
print(f"Trust weights: {detector.trust_weights(x).tolist()}")
# Тест loss'а
is_poisoned = torch.randint(0, 2, (8,)).bool()
attack_ids = torch.randint(0, 5, (8,))
loss, metrics = detector_loss(embed, poison_logits, attack_logits, is_poisoned, attack_ids)
print(f"Loss: {loss.item():.4f}, metrics: {metrics}")