| """ |
| Protected Model — основна модель, яку ми захищаємо від отруєння. |
| |
| Це невеликий ResNet для CIFAR-розмірів. |
| Її задача — звичайна класифікація. |
| Захист реалізований у train.py через зважування loss за trust_weights від Detector'а. |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| class BasicBlock(nn.Module): |
| """Стандартний ResNet block.""" |
|
|
| expansion = 1 |
|
|
| def __init__(self, in_channels: int, out_channels: int, stride: int = 1): |
| super().__init__() |
| self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False) |
| self.bn1 = nn.BatchNorm2d(out_channels) |
| self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False) |
| self.bn2 = nn.BatchNorm2d(out_channels) |
|
|
| self.shortcut = nn.Sequential() |
| if stride != 1 or in_channels != out_channels: |
| self.shortcut = nn.Sequential( |
| nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), |
| nn.BatchNorm2d(out_channels), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| out = F.relu(self.bn1(self.conv1(x)), inplace=True) |
| out = self.bn2(self.conv2(out)) |
| out = out + self.shortcut(x) |
| return F.relu(out, inplace=True) |
|
|
|
|
| class ProtectedModel(nn.Module): |
| """ |
| Малий ResNet (~ResNet-14) для CIFAR. |
| """ |
|
|
| def __init__(self, num_classes: int = 10, in_channels: int = 3): |
| super().__init__() |
|
|
| self.stem = nn.Sequential( |
| nn.Conv2d(in_channels, 64, 3, 1, 1, bias=False), |
| nn.BatchNorm2d(64), |
| nn.ReLU(inplace=True), |
| ) |
|
|
| self.layer1 = self._make_layer(64, 64, num_blocks=2, stride=1) |
| self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2) |
| self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2) |
|
|
| self.pool = nn.AdaptiveAvgPool2d(1) |
| self.fc = nn.Linear(256, num_classes) |
|
|
| def _make_layer(self, in_ch: int, out_ch: int, num_blocks: int, stride: int) -> nn.Sequential: |
| layers = [BasicBlock(in_ch, out_ch, stride)] |
| for _ in range(num_blocks - 1): |
| layers.append(BasicBlock(out_ch, out_ch, 1)) |
| return nn.Sequential(*layers) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| x = self.stem(x) |
| x = self.layer1(x) |
| x = self.layer2(x) |
| x = self.layer3(x) |
| x = self.pool(x).flatten(1) |
| return self.fc(x) |
|
|
|
|
| def weighted_cross_entropy( |
| logits: torch.Tensor, targets: torch.Tensor, weights: torch.Tensor |
| ) -> torch.Tensor: |
| """ |
| Cross-entropy з вагою на кожен зразок. |
| Це ключове місце "імунного захисту": |
| weights близько 1 → нормальне навчання |
| weights близько 0 → зразок майже не впливає |
| """ |
| per_sample_loss = F.cross_entropy(logits, targets, reduction="none") |
| |
| weight_sum = weights.sum().clamp(min=1e-6) |
| return (per_sample_loss * weights).sum() / weight_sum |
|
|
|
|
| if __name__ == "__main__": |
| model = ProtectedModel(num_classes=10) |
| x = torch.rand(4, 3, 32, 32) |
| out = model(x) |
| print(f"Output shape: {out.shape}") |
| print(f"Params: {sum(p.numel() for p in model.parameters()) / 1e6:.2f}M") |
|
|
| |
| targets = torch.randint(0, 10, (4,)) |
| weights = torch.tensor([1.0, 0.1, 1.0, 0.0]) |
| loss = weighted_cross_entropy(out, targets, weights) |
| print(f"Weighted loss: {loss.item():.4f}") |
|
|