Dennislee:) commited on
Commit
7cde09f
·
1 Parent(s): 1e548db

Deploy autoencoder-unsw (auto)

Browse files
Files changed (4) hide show
  1. Dockerfile +10 -0
  2. README.md +25 -8
  3. app.py +206 -0
  4. requirements.txt +8 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ RUN useradd -m -u 1000 user
3
+ WORKDIR /app
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+ COPY --chown=user app.py .
7
+ USER user
8
+ ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH
9
+ EXPOSE 7860
10
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,29 @@
1
  ---
2
- title: Axiom Autoencoder Unsw
3
- emoji: 🌖
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: docker
7
- pinned: false
8
- license: cc-by-nc-nd-3.0
9
- short_description: axiom-autoencoder-unsw
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Axiom Autoencoder UNSW
 
 
 
3
  sdk: docker
4
+ app_port: 7860
 
 
5
  ---
6
 
7
+ # Axiom Autoencoder UNSW-NB15 - 高維 log、行為壓縮與偵測
8
+
9
+ 49 維特徵,使用 Autoencoder 壓縮與異常偵測。資料集 UNSW-NB15(HF datasets)。
10
+
11
+ ## API
12
+
13
+ | 方法 | 路徑 | 說明 |
14
+ |------|------|------|
15
+ | GET | `/` | 根路徑 |
16
+ | GET | `/health` | 健康檢查 |
17
+ | POST | `/score` | 異常分數 |
18
+ | POST | `/reload` | 重新下載模型 |
19
+ | POST | `/train` | 觸發訓練 |
20
+ | GET | `/train/status` | 訓練狀態 |
21
+
22
+ ## curl
23
+
24
+ ```bash
25
+ BASE=https://dennislee928tw-axiom-autoencoder-unsw.hf.space
26
+ curl $BASE/health
27
+ curl -X POST $BASE/score -H "Content-Type: application/json" -d '{"tenant_id":"t1","device_id":"d1","features":[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]}'
28
+ curl -X POST $BASE/train
29
+ ```
app.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Axiom Autoencoder UNSW-NB15 - 高維 log、行為壓縮與偵測(49 維)
3
+ 使用 Autoencoder 分析 UNSW-NB15 高維特徵。
4
+ """
5
+ import os
6
+ import pickle
7
+ import threading
8
+ import time
9
+ from typing import List, Optional
10
+
11
+ import numpy as np
12
+ from fastapi import FastAPI
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel
15
+
16
+ app = FastAPI(title="Axiom Autoencoder UNSW", version="0.1.0")
17
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
18
+
19
+ ANOMALY_THRESHOLD = float(os.environ.get("ANOMALY_THRESHOLD", "0.7"))
20
+ HF_REPO = os.environ.get("HF_REPO", "")
21
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
22
+ SKLEARN_N_SAMPLES = int(os.environ.get("SKLEARN_N_SAMPLES", "2000"))
23
+ FEATURE_DIM = 49
24
+
25
+ _model = None
26
+ _scaler = None
27
+ _threshold = 0.1
28
+ _training = False
29
+ _training_lock = threading.Lock()
30
+
31
+
32
+ class ScoreRequest(BaseModel):
33
+ tenant_id: str
34
+ device_id: str
35
+ features: List[float]
36
+ sequence: Optional[List[List[float]]] = None
37
+
38
+
39
+ class ScoreResponse(BaseModel):
40
+ anomaly_score: float
41
+ is_anomaly: bool
42
+ details: Optional[dict] = None
43
+
44
+
45
+ def _load_model():
46
+ global _model, _scaler, _threshold
47
+ if not HF_REPO or not HF_TOKEN:
48
+ return
49
+ try:
50
+ from huggingface_hub import hf_hub_download
51
+ path = hf_hub_download(repo_id=HF_REPO, filename="model.pt", token=HF_TOKEN)
52
+ with open(path, "rb") as f:
53
+ b = pickle.load(f)
54
+ _model = b.get("model")
55
+ _scaler = b.get("scaler")
56
+ _threshold = b.get("threshold", 0.1)
57
+ print(f"Loaded Autoencoder from {HF_REPO}")
58
+ except Exception as e:
59
+ print(f"Load failed: {e}")
60
+ _model = _scaler = None
61
+
62
+
63
+ def _heuristic_score(features: List[float]) -> tuple[float, dict]:
64
+ if not features:
65
+ return 0.0, {"reason": "empty_features"}
66
+ arr = np.array(features, dtype=np.float64)
67
+ std = float(np.std(arr))
68
+ score = min(1.0, std / 2.0) if std > 0 else 0.0
69
+ return score, {"feature_count": len(arr)}
70
+
71
+
72
+ def compute_score(features: List[float]) -> tuple[float, dict]:
73
+ if _model is not None and _scaler is not None:
74
+ try:
75
+ import torch
76
+ x = np.array([features], dtype=np.float64)
77
+ x_scaled = _scaler.transform(x)
78
+ x_t = torch.tensor(x_scaled, dtype=torch.float32)
79
+ with torch.no_grad():
80
+ recon = _model(x_t)
81
+ mse = float(((x_t - recon) ** 2).mean().item())
82
+ score = min(1.0, mse / (_threshold + 1e-8))
83
+ return min(1.0, max(0.0, score)), {"source": "autoencoder", "mse": mse, "feature_count": len(features)}
84
+ except Exception:
85
+ return _heuristic_score(features)
86
+ return _heuristic_score(features)
87
+
88
+
89
+ def _fetch_unsw(n_samples: int, rs: int) -> np.ndarray:
90
+ try:
91
+ from datasets import load_dataset
92
+ try:
93
+ ds = load_dataset("Mouwiya/UNSW-NB15", split="train", trust_remote_code=True)
94
+ except Exception:
95
+ ds = load_dataset("wwydmanski/UNSW-NB15", split="train", trust_remote_code=True)
96
+ df = ds.to_pandas()
97
+ if "label" in df.columns:
98
+ df = df.drop(columns=["label"], errors="ignore")
99
+ X = df.select_dtypes(include=[np.number]).values.astype(float)
100
+ if X.shape[1] > FEATURE_DIM:
101
+ X = X[:, :FEATURE_DIM]
102
+ elif X.shape[1] < FEATURE_DIM:
103
+ pad = np.zeros((X.shape[0], FEATURE_DIM - X.shape[1]))
104
+ X = np.hstack([X, pad])
105
+ except Exception:
106
+ X = np.random.uniform(0, 1, (n_samples, FEATURE_DIM))
107
+ rng = np.random.default_rng(rs)
108
+ n = min(n_samples, len(X))
109
+ idx = rng.choice(len(X), size=n, replace=False)
110
+ return X[idx]
111
+
112
+
113
+ def _run_training():
114
+ global _model, _training
115
+ with _training_lock:
116
+ if _training:
117
+ return
118
+ _training = True
119
+ try:
120
+ import torch
121
+ import torch.nn as nn
122
+ from sklearn.preprocessing import StandardScaler
123
+ from huggingface_hub import HfApi, login
124
+ rs = int(time.time()) % 10000
125
+ X = _fetch_unsw(SKLEARN_N_SAMPLES, rs)
126
+ scaler = StandardScaler()
127
+ X_scaled = scaler.fit_transform(X)
128
+
129
+ class Autoencoder(nn.Module):
130
+ def __init__(self):
131
+ super().__init__()
132
+ self.enc = nn.Sequential(nn.Linear(FEATURE_DIM, 16), nn.ReLU(), nn.Linear(16, 8))
133
+ self.dec = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, FEATURE_DIM))
134
+ def forward(self, x):
135
+ return self.dec(self.enc(x))
136
+
137
+ model = Autoencoder()
138
+ opt = torch.optim.Adam(model.parameters(), lr=0.01)
139
+ x_t = torch.tensor(X_scaled, dtype=torch.float32)
140
+ for _ in range(50):
141
+ recon = model(x_t)
142
+ loss = nn.functional.mse_loss(recon, x_t)
143
+ opt.zero_grad()
144
+ loss.backward()
145
+ opt.step()
146
+ model.eval()
147
+ with torch.no_grad():
148
+ recon = model(x_t)
149
+ threshold = float(((x_t - recon) ** 2).mean().item()) * 2.0
150
+
151
+ bundle = {"model": model, "scaler": scaler, "threshold": threshold, "feature_dim": FEATURE_DIM, "dataset": "unsw_nb15"}
152
+ os.makedirs("/tmp/models", exist_ok=True)
153
+ path = "/tmp/models/model.pt"
154
+ with open(path, "wb") as f:
155
+ pickle.dump(bundle, f)
156
+ if HF_TOKEN and HF_REPO:
157
+ login(token=HF_TOKEN)
158
+ api = HfApi()
159
+ api.create_repo(repo_id=HF_REPO, repo_type="model", exist_ok=True, token=HF_TOKEN)
160
+ api.upload_file(path_or_fileobj=path, path_in_repo="model.pt", repo_id=HF_REPO, repo_type="model", token=HF_TOKEN)
161
+ print(f"Uploaded to {HF_REPO}")
162
+ _load_model()
163
+ except Exception as e:
164
+ print(f"Training failed: {e}")
165
+ finally:
166
+ with _training_lock:
167
+ _training = False
168
+
169
+
170
+ @app.on_event("startup")
171
+ def startup():
172
+ _load_model()
173
+
174
+
175
+ @app.get("/")
176
+ def root():
177
+ return {"service": "Axiom Autoencoder UNSW", "version": "0.1.0", "endpoints": ["/health", "/score", "/reload", "/train", "/train/status"]}
178
+
179
+
180
+ @app.get("/health")
181
+ def health():
182
+ return {"status": "ok", "model_loaded": _model is not None}
183
+
184
+
185
+ @app.post("/reload")
186
+ def reload():
187
+ _load_model()
188
+ return {"status": "ok", "model_loaded": _model is not None}
189
+
190
+
191
+ @app.post("/train")
192
+ def train():
193
+ t = threading.Thread(target=_run_training, daemon=True)
194
+ t.start()
195
+ return {"status": "training_started", "dataset": "UNSW-NB15"}
196
+
197
+
198
+ @app.get("/train/status")
199
+ def train_status():
200
+ return {"training": _training}
201
+
202
+
203
+ @app.post("/score", response_model=ScoreResponse)
204
+ def score(req: ScoreRequest):
205
+ anomaly_score, details = compute_score(req.features)
206
+ return ScoreResponse(anomaly_score=round(anomaly_score, 4), is_anomaly=anomaly_score >= ANOMALY_THRESHOLD, details=details)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.109.0
2
+ uvicorn[standard]>=0.27.0
3
+ pydantic>=2.5.0
4
+ numpy>=1.26.0
5
+ torch>=2.0.0
6
+ scikit-learn>=1.3.0
7
+ huggingface_hub>=0.20.0
8
+ datasets>=2.14.0