| """ |
| model.py |
| ======== |
| LSTM-Autoencoder for REST API anomaly detection. |
| |
| Supports two input modes: |
| - 'embedding' : token sequences (CSIC 2010) — integers → learned vectors |
| - 'numeric' : feature sequences (CIC-IDS2018, UNSW-NB15) — floats directly |
| |
| The model trains only on normal traffic. |
| At inference, high reconstruction error = anomaly. |
| |
| Author : K.A.D.S.D. Kandanaarachchi (2020/ICT/19) |
| Project: Detecting Anomalous REST API Traffic — IT4216 |
| """ |
|
|
| import torch |
| import torch.nn as nn |
|
|
|
|
| class Encoder(nn.Module): |
| """ |
| Reads a session sequence and compresses it into a fixed-size context vector (the bottleneck). |
| |
| Input : (batch, seq_len, input_size) |
| Output : (batch, hidden_size) — last hidden state only |
| """ |
|
|
| def __init__( |
| self, |
| input_size: int, |
| hidden_size: int, |
| num_layers: int = 2, |
| dropout: float = 0.2, |
| ): |
| super().__init__() |
| self.lstm = nn.LSTM( |
| input_size=input_size, |
| hidden_size=hidden_size, |
| num_layers=num_layers, |
| batch_first=True, |
| dropout=dropout if num_layers > 1 else 0.0, |
| ) |
|
|
| def forward(self, x): |
| _, (hidden, _) = self.lstm(x) |
| |
| |
| return hidden[-1] |
|
|
|
|
| class Decoder(nn.Module): |
| """ |
| Takes the bottleneck vector and reconstructs the original sequence step by step. |
| |
| Input : (batch, hidden_size) |
| Output : (batch, seq_len, input_size) |
| """ |
|
|
| def __init__( |
| self, |
| hidden_size: int, |
| output_size: int, |
| seq_len: int, |
| num_layers: int = 2, |
| dropout: float = 0.2, |
| ): |
| super().__init__() |
| self.seq_len = seq_len |
|
|
| self.lstm = nn.LSTM( |
| input_size=hidden_size, |
| hidden_size=hidden_size, |
| num_layers=num_layers, |
| batch_first=True, |
| dropout=dropout if num_layers > 1 else 0.0, |
| ) |
| |
| self.output_layer = nn.Linear(hidden_size, output_size) |
|
|
| def forward(self, context): |
| |
| |
| repeated = context.unsqueeze(1).repeat(1, self.seq_len, 1) |
| out, _ = self.lstm(repeated) |
| |
| return self.output_layer(out) |
|
|
|
|
| class LSTMAutoencoder(nn.Module): |
| """ |
| Full LSTM-Autoencoder for sequence anomaly detection. |
| |
| Parameters |
| ---------- |
| input_mode : 'embedding' for token sequences (CSIC 2010) |
| 'numeric' for float sequences (CIC-IDS2018, UNSW-NB15) |
| vocab_size : required when input_mode='embedding' |
| embed_dim : embedding dimension when input_mode='embedding' |
| input_size : number of features when input_mode='numeric' |
| hidden_size : LSTM hidden state size (bottleneck width) |
| seq_len : number of timesteps per session (window size) |
| num_layers : number of stacked LSTM layers |
| dropout : dropout between LSTM layers |
| """ |
|
|
| def __init__( |
| self, |
| input_mode: str = "numeric", |
| vocab_size: int = None, |
| embed_dim: int = 32, |
| input_size: int = 23, |
| hidden_size: int = 64, |
| seq_len: int = 5, |
| num_layers: int = 2, |
| dropout: float = 0.2, |
| ): |
| super().__init__() |
|
|
| self.input_mode = input_mode |
| self.seq_len = seq_len |
|
|
| |
| if input_mode == "embedding": |
| assert vocab_size is not None, "vocab_size required for embedding mode" |
| self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) |
| encoder_input_sz = embed_dim |
| decoder_output_sz = embed_dim |
| else: |
| self.embedding = None |
| encoder_input_sz = input_size |
| decoder_output_sz = input_size |
|
|
| self.encoder = Encoder(encoder_input_sz, hidden_size, num_layers, dropout) |
| self.decoder = Decoder( |
| hidden_size, decoder_output_sz, seq_len, num_layers, dropout |
| ) |
|
|
| def forward(self, x): |
| """ |
| x shape: |
| embedding mode : (batch, seq_len) int64 token ids |
| numeric mode : (batch, seq_len, features) float32 |
| """ |
| if self.input_mode == "embedding": |
| x = self.embedding(x) |
|
|
| context = self.encoder(x) |
| reconstruction = self.decoder(context) |
| return reconstruction |
|
|
| def reconstruction_error(self, x): |
| """ |
| Compute per-sample mean squared error between input and reconstruction. Used as anomaly score. |
| |
| Returns: (batch,) float tensor of error scores |
| """ |
| with torch.no_grad(): |
| if self.input_mode == "embedding": |
| x_float = self.embedding(x).detach() |
| else: |
| x_float = x |
|
|
| recon = self.forward(x) |
| |
| error = ((recon - x_float) ** 2).mean(dim=(1, 2)) |
| return error |
|
|
|
|
| def build_model_csic2010( |
| vocab_size: int, |
| embed_dim: int = 32, |
| hidden_size: int = 64, |
| num_layers: int = 2, |
| seq_len: int = 5, |
| ) -> LSTMAutoencoder: |
| """Instantiate model configured for CSIC 2010 token sequences.""" |
| return LSTMAutoencoder( |
| input_mode="embedding", |
| vocab_size=vocab_size, |
| embed_dim=embed_dim, |
| hidden_size=hidden_size, |
| seq_len=seq_len, |
| num_layers=num_layers, |
| ) |
|
|
|
|
| def build_model_cicids2018( |
| n_features: int = 23, |
| hidden_size: int = 64, |
| num_layers: int = 2, |
| seq_len: int = 5, |
| ) -> LSTMAutoencoder: |
| """Instantiate model configured for CIC-IDS2018 flow features.""" |
| return LSTMAutoencoder( |
| input_mode="numeric", |
| input_size=n_features, |
| hidden_size=hidden_size, |
| seq_len=seq_len, |
| num_layers=num_layers, |
| ) |
|
|
|
|
| def build_model_unsw( |
| n_features: int = 20, |
| hidden_size: int = 64, |
| num_layers: int = 2, |
| seq_len: int = 5, |
| ) -> LSTMAutoencoder: |
| """Instantiate model configured for UNSW-NB15 flow features.""" |
| return LSTMAutoencoder( |
| input_mode="numeric", |
| input_size=n_features, |
| hidden_size=hidden_size, |
| seq_len=seq_len, |
| num_layers=num_layers, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| print("=== Smoke test — all three model configs ===\n") |
|
|
| |
| m1 = build_model_csic2010(vocab_size=51) |
| x1 = torch.randint(0, 51, (8, 5)) |
| r1 = m1(x1) |
| e1 = m1.reconstruction_error(x1) |
| print( |
| f"CSIC 2010 input={tuple(x1.shape)} " |
| f"recon={tuple(r1.shape)} error={e1.mean():.4f}" |
| ) |
|
|
| |
| m2 = build_model_cicids2018(n_features=23) |
| x2 = torch.randn(8, 5, 23) |
| r2 = m2(x2) |
| e2 = m2.reconstruction_error(x2) |
| print( |
| f"CIC-IDS2018 input={tuple(x2.shape)} " |
| f"recon={tuple(r2.shape)} error={e2.mean():.4f}" |
| ) |
|
|
| |
| m3 = build_model_unsw(n_features=20) |
| x3 = torch.randn(8, 5, 20) |
| r3 = m3(x3) |
| e3 = m3.reconstruction_error(x3) |
| print( |
| f"UNSW-NB15 input={tuple(x3.shape)} " |
| f"recon={tuple(r3.shape)} error={e3.mean():.4f}" |
| ) |
|
|
| total = sum(p.numel() for p in m1.parameters()) |
| print(f"\nCSIC 2010 model parameters: {total:,}") |
| total = sum(p.numel() for p in m2.parameters()) |
| print(f"CIC-IDS2018 model parameters: {total:,}") |
|
|