Files
traffico/TrafficoDataset/train_gemma3_traffico.py

242 lines
8.2 KiB
Python
Raw Normal View History

# ============================================================
# Gemma-3-270M Analisi Traffico di Rete TCP/IP
# Unsloth + LoRA + Dataset JSONL (CIC-IDS2017 + UNSW-NB15)
# ============================================================
# Struttura basata sul tuo script, adattata per il dominio
# di analisi del traffico di rete con mappatura MITRE ATT&CK.
#
# PREREQUISITI:
# Google Colab con runtime GPU (T4 basta)
# !pip install --no-deps unsloth
# !pip install transformers datasets trl peft accelerate sentencepiece
#
# FILE NECESSARI (nella stessa cartella dello script):
# dataset.jsonl ← generato dalla script apposita
# ============================================================
# ---------- INSTALL (Colab) ----------
# !pip install --no-deps unsloth
# !pip install transformers datasets trl peft accelerate sentencepiece
# ---------- IMPORT ----------
from unsloth import FastModel
from unsloth.chat_templates import get_chat_template, train_on_responses_only
import torch
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig
# ---------- CONFIG ----------
MODEL_NAME = "unsloth/gemma-3-270m-it"
DATASET_PATH = "dataset_traffico.jsonl" # <== il JSONL che abbiamo generato
OUTPUT_DIR = "outputs"
MAX_SEQ_LENGTH = 512 # 512 basta per questi prompt, risparmia memoria
# ---------- LOAD MODEL ----------
model, tokenizer = FastModel.from_pretrained(
model_name = MODEL_NAME,
max_seq_length = MAX_SEQ_LENGTH,
load_in_4bit = False,
load_in_8bit = False,
full_finetuning = False,
)
# ---------- LoRA ----------
# Configurazione più aggressiva sul rank (r=64) per un dominio specifico come questo.
# Target modules: tutti i proiettori del transformer.
model = FastModel.get_peft_model(
model,
r = 64,
target_modules = [
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_alpha = 64,
lora_dropout = 0,
bias = "none",
use_gradient_checkpointing = "unsloth",
random_state = 3407,
)
# ---------- CHAT TEMPLATE (Gemma-3) ----------
tokenizer = get_chat_template(
tokenizer,
chat_template = "gemma3",
)
# ---------- LOAD DATASET ----------
dataset = load_dataset(
"json",
data_files = DATASET_PATH,
split = "train",
)
print(f"Dataset caricato: {len(dataset)} righe")
print(f"Campi presenti: {dataset.column_names}")
print(f"\nEsempio riga 0:")
print(dataset[0])
# ---------- CONVERT TO CHATML ----------
# Il JSONL ha campi: instruction, input, output
# Li convertiamo nel formato conversations [system, user, assistant]
# che Gemma-3 si aspetta.
def convert_to_chatml(example):
system_prompt = example["instruction"]
# Se c'è un campo 'context' lo aggiungiamo al system prompt
if "context" in example and example["context"]:
system_prompt += f"\nContesto: {example['context']}."
return {
"conversations": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": example["input"]},
{"role": "assistant", "content": example["output"]},
]
}
dataset = dataset.map(convert_to_chatml)
# ---------- APPLY GEMMA-3 TEMPLATE ----------
# Applica il template di chat di Gemma-3 a ogni esempio.
# Questo produce la stringa finale che il modello vedrà durante il training.
def formatting_prompts_func(examples):
convos = examples["conversations"]
texts = [
tokenizer.apply_chat_template(
convo,
tokenize = False,
add_generation_prompt = False,
).removeprefix("<bos>")
for convo in convos
]
return {"text": texts}
dataset = dataset.map(formatting_prompts_func, batched=True)
# Verifica come appare un prompt formattato
print("\n" + "=" * 60)
print(" PROMPT FORMATTATO (esempio)")
print("=" * 60)
print(dataset[0]["text"])
print("=" * 60)
# ---------- TRAINER ----------
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset,
eval_dataset = None,
args = SFTConfig(
dataset_text_field = "text",
per_device_train_batch_size = 4,
gradient_accumulation_steps = 4, # batch effettivo = 4 * 4 = 16
warmup_steps = 10,
max_steps = 500, # ~500 step su 10k righe con batch 16
learning_rate = 2e-5,
logging_steps = 25,
optim = "adamw_8bit",
weight_decay = 0.001,
lr_scheduler_type = "linear",
seed = 3407,
output_dir = OUTPUT_DIR,
report_to = "none",
),
)
# ---------- TRAIN ONLY ON ASSISTANT ----------
# Fondamentale: il modello calcola il loss SOLO sulla risposta dell'assistant,
# non sul prompt. Così non "impara" a ripetere la domanda.
trainer = train_on_responses_only(
trainer,
instruction_part = "<start_of_turn>user\n",
response_part = "<start_of_turn>model\n",
)
# ---------- TRAIN ----------
trainer.train()
# ---------- SAVE LoRA ----------
model.save_pretrained("gemma3-traffico-rete-lora")
tokenizer.save_pretrained("gemma3-traffico-rete-lora")
print("\n✓ Modello LoRA salvato in: gemma3-traffico-rete-lora/")
model.save_pretrained_merged(
"gemma3-traffico-rete-lora", # cartella output
tokenizer,
save_method="merged_16bit" # Float16 per GGUF
)
model.save_pretrained_gguf(
"gemma3-traffico-rete-lora",
tokenizer,
quantization_method = "BF16", # For now only Q8_0, BF16, F16 supported
)
# ---------- INFERENCE: TEST ----------
# Dopo il training, prova il modello con alcuni flussi di esempio.
from transformers import TextStreamer
test_cases = [
# Caso 1: profilo tipico DoS (masse enormi di byte src, pochissimi dst, durata minima)
"Protocollo: tcp | Porta dst: 80 | Byte src: 480000 | Byte dst: 40 | Pacchetti: 5200 | Durata: 0.015s",
# Caso 2: traffico normale HTTPS
"Protocollo: tcp | Porta dst: 443 | Byte src: 1500 | Byte dst: 6200 | Pacchetti: 9 | Durata: 3.200s",
# Caso 3: profilo PortScan (tanti dst diversi, pochi byte, durata quasi zero)
"Protocollo: tcp | Porta dst: 22 | Byte src: 60 | Byte dst: 0 | Pacchetti: 1 | Durata: 0.002s",
# Caso 4: profilo Brute Force su SSH
"Protocollo: tcp | Porta dst: 22 | Byte src: 3200 | Byte dst: 8500 | Pacchetti: 45 | Durata: 1.800s",
# Caso 5: profilo Infiltration / esfiltrazioni dati
"Protocollo: tcp | Porta dst: 443 | Byte src: 8000 | Byte dst: 120000 | Pacchetti: 200 | Durata: 25.500s",
]
streamer = TextStreamer(tokenizer, skip_prompt=True)
for i, test_input in enumerate(test_cases, 1):
messages = [
{
"role": "system",
"content": (
"Analizza il seguente flusso di traffico di rete TCP/IP. "
"Classifica se è traffico normale o un attacco. "
"Se è un attacco, indica la categoria e la tecnica MITRE ATT&CK corrispondente."
),
},
{"role": "user", "content": test_input},
]
text = tokenizer.apply_chat_template(
messages,
tokenize = False,
add_generation_prompt = True,
).removeprefix("<bos>")
print(f"\n{'' * 60}")
print(f" TEST {i}: {test_input[:80]}...")
print(f"{'' * 60}")
print(" Risposta: ", end="")
_ = model.generate(
**tokenizer(text, return_tensors="pt").to("cuda"),
max_new_tokens = 128,
temperature = 0.3, # bassa temperatura = risposte più deterministe
top_p = 0.9,
top_k = 40,
streamer = streamer,
)
# ---------- SAVE MERGED (opzionale) ----------
# Unisce i pesi LoRA al modello base e salva come modello completo.
# Utile per deployare senza dipendenza da PEFT.
#
model.save_pretrained_merged(
"gemma3-traffico-rete-merged",
tokenizer,
save_method = "merged_16bit",
)
#
# ---------- SAVE GGUF (opzionale) ----------
# Formato GGUF per inferenza locale con llama.cpp / Ollama.
#
model.save_pretrained_gguf(
"gemma3-traffico-rete-gguf",
tokenizer,
quantization_method = "Q8_0", # Q8_0 = buon equilibrio qualità/dimensione
)