Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- Dockerfile +8 -0
- data/patient_dataset.csv +0 -0
- database.py +66 -0
- ensemble.py +48 -0
- main.py +486 -0
- model/classes.pkl +3 -0
- model/knn.pkl +3 -0
- model/label_encoder.pkl +3 -0
- model/logreg.pkl +3 -0
- model/rf.pkl +3 -0
- model/scaler.pkl +3 -0
- model/small_nn.pkl +3 -0
- model/svm.pkl +3 -0
- models.py +39 -0
- preprocessing.py +62 -0
- requirements.txt +10 -0
Dockerfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY requirements.txt .
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
COPY . .
|
| 6 |
+
EXPOSE 7860
|
| 7 |
+
ENV PORT=7860
|
| 8 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
data/patient_dataset.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
database.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'patient_history.db')
|
| 6 |
+
|
| 7 |
+
def init_db():
|
| 8 |
+
conn = sqlite3.connect(DB_PATH)
|
| 9 |
+
cursor = conn.cursor()
|
| 10 |
+
cursor.execute('''
|
| 11 |
+
CREATE TABLE IF NOT EXISTS predictions (
|
| 12 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 13 |
+
timestamp DATETIME,
|
| 14 |
+
heart_rate REAL,
|
| 15 |
+
spo2 REAL,
|
| 16 |
+
sys_bp REAL,
|
| 17 |
+
dia_bp REAL,
|
| 18 |
+
temp REAL,
|
| 19 |
+
fall_detection TEXT,
|
| 20 |
+
prediction_label TEXT,
|
| 21 |
+
confidence REAL
|
| 22 |
+
)
|
| 23 |
+
''')
|
| 24 |
+
conn.commit()
|
| 25 |
+
conn.close()
|
| 26 |
+
|
| 27 |
+
def log_prediction(data, prediction_label, confidence):
|
| 28 |
+
conn = sqlite3.connect(DB_PATH)
|
| 29 |
+
cursor = conn.cursor()
|
| 30 |
+
cursor.execute('''
|
| 31 |
+
INSERT INTO predictions (timestamp, heart_rate, spo2, sys_bp, dia_bp, temp, fall_detection, prediction_label, confidence)
|
| 32 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 33 |
+
''', (
|
| 34 |
+
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 35 |
+
data.Heart_Rate,
|
| 36 |
+
data.SpO2_Level,
|
| 37 |
+
data.Systolic_BP,
|
| 38 |
+
data.Diastolic_BP,
|
| 39 |
+
data.Body_Temp,
|
| 40 |
+
data.Fall_Detection,
|
| 41 |
+
prediction_label,
|
| 42 |
+
confidence
|
| 43 |
+
))
|
| 44 |
+
conn.commit()
|
| 45 |
+
conn.close()
|
| 46 |
+
|
| 47 |
+
def get_history():
|
| 48 |
+
conn = sqlite3.connect(DB_PATH)
|
| 49 |
+
cursor = conn.cursor()
|
| 50 |
+
cursor.execute('SELECT * FROM predictions ORDER BY timestamp DESC LIMIT 100')
|
| 51 |
+
rows = cursor.fetchall()
|
| 52 |
+
|
| 53 |
+
# Get column names
|
| 54 |
+
col_names = [description[0] for description in cursor.description]
|
| 55 |
+
|
| 56 |
+
conn.close()
|
| 57 |
+
|
| 58 |
+
# Format as list of dicts
|
| 59 |
+
history = []
|
| 60 |
+
for row in rows:
|
| 61 |
+
history.append(dict(zip(col_names, row)))
|
| 62 |
+
|
| 63 |
+
return history
|
| 64 |
+
|
| 65 |
+
# Initialize db when this module is loaded
|
| 66 |
+
init_db()
|
ensemble.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
import numpy as np
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
from preprocessing import resolve_model_dir
|
| 8 |
+
|
| 9 |
+
class EnsembleModel:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.model_dir = resolve_model_dir()
|
| 12 |
+
self.model_names = ['knn', 'svm', 'logreg', 'rf', 'small_nn']
|
| 13 |
+
self.models = {}
|
| 14 |
+
self.load_models()
|
| 15 |
+
self.label_encoder = joblib.load(f'{self.model_dir}/label_encoder.pkl')
|
| 16 |
+
self.weights = {'knn': 0.15, 'svm': 0.15, 'logreg': 0.20, 'rf': 0.25, 'small_nn': 0.25}
|
| 17 |
+
|
| 18 |
+
def load_models(self):
|
| 19 |
+
for name in self.model_names:
|
| 20 |
+
path = f'{self.model_dir}/{name}.pkl'
|
| 21 |
+
if os.path.exists(path):
|
| 22 |
+
self.models[name] = joblib.load(path)
|
| 23 |
+
|
| 24 |
+
def predict(self, X):
|
| 25 |
+
individual_preds = {}
|
| 26 |
+
individual_probs = {}
|
| 27 |
+
|
| 28 |
+
for name, model in self.models.items():
|
| 29 |
+
probs = model.predict_proba(X)
|
| 30 |
+
individual_probs[name] = probs[0]
|
| 31 |
+
pred_indices = np.argmax(probs, axis=1)
|
| 32 |
+
individual_preds[name] = str(self.label_encoder.inverse_transform(pred_indices)[0])
|
| 33 |
+
|
| 34 |
+
num_classes = list(individual_probs.values())[0].shape[0]
|
| 35 |
+
weighted_probs = np.zeros(num_classes)
|
| 36 |
+
|
| 37 |
+
total_weight = 0
|
| 38 |
+
for name in self.models.keys():
|
| 39 |
+
w = self.weights.get(name, 1.0)
|
| 40 |
+
weighted_probs += individual_probs[name] * w
|
| 41 |
+
total_weight += w
|
| 42 |
+
|
| 43 |
+
weighted_probs /= total_weight
|
| 44 |
+
final_pred_idx = np.argmax(weighted_probs)
|
| 45 |
+
final_pred = str(self.label_encoder.inverse_transform([final_pred_idx])[0])
|
| 46 |
+
confidence = float(np.max(weighted_probs))
|
| 47 |
+
|
| 48 |
+
return final_pred, confidence, individual_preds, individual_probs, self.weights
|
main.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, File
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import asyncio
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import m2cgen as m2c
|
| 9 |
+
import shap
|
| 10 |
+
from database import log_prediction, get_history
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 14 |
+
from preprocessing import preprocess_data
|
| 15 |
+
|
| 16 |
+
app = FastAPI(title="TinyML Healthcare API")
|
| 17 |
+
|
| 18 |
+
app.add_middleware(
|
| 19 |
+
CORSMiddleware,
|
| 20 |
+
allow_origins=["*"],
|
| 21 |
+
allow_credentials=True,
|
| 22 |
+
allow_methods=["*"],
|
| 23 |
+
allow_headers=["*"],
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
ensemble_system = None
|
| 27 |
+
def get_ensemble():
|
| 28 |
+
global ensemble_system
|
| 29 |
+
if ensemble_system is None:
|
| 30 |
+
try:
|
| 31 |
+
from ensemble import EnsembleModel
|
| 32 |
+
ensemble_system = EnsembleModel()
|
| 33 |
+
except Exception:
|
| 34 |
+
return None
|
| 35 |
+
return ensemble_system
|
| 36 |
+
|
| 37 |
+
class PatientData(BaseModel):
|
| 38 |
+
Heart_Rate: float
|
| 39 |
+
SpO2_Level: float
|
| 40 |
+
Systolic_BP: float
|
| 41 |
+
Diastolic_BP: float
|
| 42 |
+
Body_Temp: float
|
| 43 |
+
Fall_Detection: str
|
| 44 |
+
|
| 45 |
+
@app.get("/health")
|
| 46 |
+
def health_check():
|
| 47 |
+
return {"status": "Healthy" if get_ensemble() else "Warning - Models Offline"}
|
| 48 |
+
|
| 49 |
+
@app.post("/predict")
|
| 50 |
+
async def predict(data: PatientData):
|
| 51 |
+
try:
|
| 52 |
+
eng = get_ensemble()
|
| 53 |
+
if not eng:
|
| 54 |
+
return {"error": "Models untrained. Ensure python backend/models.py executes."}
|
| 55 |
+
|
| 56 |
+
df = pd.DataFrame([{
|
| 57 |
+
'Heart Rate (bpm)': data.Heart_Rate,
|
| 58 |
+
'SpO2 Level (%)': data.SpO2_Level,
|
| 59 |
+
'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
|
| 60 |
+
'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
|
| 61 |
+
'Body Temperature (°C)': data.Body_Temp,
|
| 62 |
+
'Fall Detection': data.Fall_Detection
|
| 63 |
+
}])
|
| 64 |
+
|
| 65 |
+
X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
|
| 66 |
+
final_pred, conf, ind_preds, ind_probs, weights = await asyncio.to_thread(eng.predict, X_proc)
|
| 67 |
+
|
| 68 |
+
is_at_risk = 0 if final_pred == "Healthy" else 1
|
| 69 |
+
|
| 70 |
+
# Critical Alert System — fault-tolerant
|
| 71 |
+
if is_at_risk == 1 and float(conf) > 0.80:
|
| 72 |
+
try:
|
| 73 |
+
with open("alerts.log", "a") as f:
|
| 74 |
+
f.write(f"ALERT: Patient at risk! HR: {data.Heart_Rate}, SpO2: {data.SpO2_Level}, Confidence: {conf:.2f}\n")
|
| 75 |
+
except Exception:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
# Log to SQLite History — fault-tolerant
|
| 79 |
+
try:
|
| 80 |
+
await asyncio.to_thread(log_prediction, data, final_pred, float(conf))
|
| 81 |
+
except Exception as db_e:
|
| 82 |
+
print(f"DB logging skipped: {db_e}")
|
| 83 |
+
|
| 84 |
+
return {
|
| 85 |
+
"prediction": is_at_risk,
|
| 86 |
+
"prediction_label": final_pred,
|
| 87 |
+
"probability": float(conf),
|
| 88 |
+
"ensemble_prediction": is_at_risk,
|
| 89 |
+
"model_outputs": ind_preds,
|
| 90 |
+
"model_probs": {k: float(np.max(v)) for k, v in ind_probs.items()},
|
| 91 |
+
"weights": weights
|
| 92 |
+
}
|
| 93 |
+
except Exception as e:
|
| 94 |
+
import traceback
|
| 95 |
+
from fastapi import HTTPException
|
| 96 |
+
raise HTTPException(status_code=500, detail=f"Backend Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
| 97 |
+
|
| 98 |
+
@app.get("/history")
|
| 99 |
+
def history():
|
| 100 |
+
return get_history()
|
| 101 |
+
|
| 102 |
+
@app.post("/explain")
|
| 103 |
+
async def explain(data: PatientData):
|
| 104 |
+
eng = get_ensemble()
|
| 105 |
+
if not eng or 'rf' not in eng.models:
|
| 106 |
+
return {"error": "RF Model unavailable for explanation."}
|
| 107 |
+
|
| 108 |
+
df = pd.DataFrame([{
|
| 109 |
+
'Heart Rate (bpm)': data.Heart_Rate,
|
| 110 |
+
'SpO2 Level (%)': data.SpO2_Level,
|
| 111 |
+
'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
|
| 112 |
+
'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
|
| 113 |
+
'Body Temperature (°C)': data.Body_Temp,
|
| 114 |
+
'Fall Detection': data.Fall_Detection
|
| 115 |
+
}])
|
| 116 |
+
|
| 117 |
+
X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
|
| 118 |
+
|
| 119 |
+
def compute_shap():
|
| 120 |
+
import numpy as np
|
| 121 |
+
rf_model = eng.models['rf']
|
| 122 |
+
explainer = shap.TreeExplainer(rf_model)
|
| 123 |
+
shap_values = explainer.shap_values(X_proc)
|
| 124 |
+
pred_idx = int(rf_model.predict(X_proc)[0])
|
| 125 |
+
if isinstance(shap_values, list):
|
| 126 |
+
vals = shap_values[pred_idx][0]
|
| 127 |
+
elif isinstance(shap_values, np.ndarray) and len(shap_values.shape) == 3:
|
| 128 |
+
vals = shap_values[0, :, pred_idx]
|
| 129 |
+
else:
|
| 130 |
+
vals = shap_values[0]
|
| 131 |
+
return vals.tolist(), X_proc.columns.tolist()
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
shap_vals, features = await asyncio.to_thread(compute_shap)
|
| 135 |
+
return {"shap_values": shap_vals, "feature_names": features}
|
| 136 |
+
except Exception as e:
|
| 137 |
+
return {"error": str(e)}
|
| 138 |
+
|
| 139 |
+
@app.get("/export_tinyml")
|
| 140 |
+
def export_tinyml(model_name: str = "rf", quantize: bool = False):
|
| 141 |
+
import numpy as np
|
| 142 |
+
eng = get_ensemble()
|
| 143 |
+
if not eng or model_name not in eng.models:
|
| 144 |
+
return {"error": f"Model {model_name} not found"}
|
| 145 |
+
|
| 146 |
+
model = eng.models[model_name]
|
| 147 |
+
|
| 148 |
+
# m2cgen handles Random Forest beautifully, but outputs FP32/double rules
|
| 149 |
+
if model_name == "rf":
|
| 150 |
+
try:
|
| 151 |
+
code = m2c.export_to_c(model)
|
| 152 |
+
if quantize:
|
| 153 |
+
code = "/* WARNING: M2CGen generated FP32 output. INT8 Quantization is not supported directly for Random Forest trees. */\n" + code
|
| 154 |
+
return {"code": code}
|
| 155 |
+
except Exception:
|
| 156 |
+
pass
|
| 157 |
+
|
| 158 |
+
# For LogReg, use m2cgen if FP32, otherwise manual generation for INT8
|
| 159 |
+
if not quantize and model_name == "logreg":
|
| 160 |
+
try:
|
| 161 |
+
code = m2c.export_to_c(model)
|
| 162 |
+
return {"code": code}
|
| 163 |
+
except Exception:
|
| 164 |
+
pass
|
| 165 |
+
|
| 166 |
+
# Manual C-code generation for all model types
|
| 167 |
+
try:
|
| 168 |
+
L = []
|
| 169 |
+
L.append("/* ====================================================== */")
|
| 170 |
+
L.append(f"/* TinyML C Export: {model_name} */")
|
| 171 |
+
q_text = "INT8 Quantized" if quantize else "FP32 Double"
|
| 172 |
+
L.append(f"/* Auto-generated for ARM Cortex-M / ESP32 ({q_text}) */")
|
| 173 |
+
L.append("/* ====================================================== */")
|
| 174 |
+
L.append("")
|
| 175 |
+
L.append("#include <math.h>")
|
| 176 |
+
L.append("#include <stdint.h>")
|
| 177 |
+
L.append("#include <string.h>")
|
| 178 |
+
L.append("")
|
| 179 |
+
|
| 180 |
+
if model_name == "svm" and hasattr(model, 'coef_'):
|
| 181 |
+
coefs = model.coef_
|
| 182 |
+
intercepts = model.intercept_
|
| 183 |
+
n_classes = len(model.classes_)
|
| 184 |
+
n_features = coefs.shape[1]
|
| 185 |
+
L.append(f"/* Linear SVM with {n_classes} classes, {n_features} features */")
|
| 186 |
+
L.append(f"#define N_FEATURES {n_features}")
|
| 187 |
+
L.append(f"#define N_CLASSES {n_classes}")
|
| 188 |
+
L.append(f"#define N_HYPERPLANES {coefs.shape[0]}")
|
| 189 |
+
L.append("")
|
| 190 |
+
|
| 191 |
+
if quantize:
|
| 192 |
+
scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
|
| 193 |
+
L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
|
| 194 |
+
L.append("static const int8_t SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
|
| 195 |
+
for row in coefs:
|
| 196 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 197 |
+
L.append(f" {{{vals}}},")
|
| 198 |
+
L.append("};")
|
| 199 |
+
L.append("")
|
| 200 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
|
| 201 |
+
L.append(f"static const int8_t SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
|
| 202 |
+
L.append("")
|
| 203 |
+
L.append("int predict(int8_t *features) {")
|
| 204 |
+
L.append(" int32_t scores[N_CLASSES] = {0};")
|
| 205 |
+
L.append(" int h = 0;")
|
| 206 |
+
L.append(" for (int i = 0; i < N_CLASSES; i++) {")
|
| 207 |
+
L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
|
| 208 |
+
L.append(" int32_t decision = SVM_INTERCEPT[h];")
|
| 209 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 210 |
+
L.append(" decision += (int32_t)SVM_COEF[h][f] * features[f];")
|
| 211 |
+
L.append(" }")
|
| 212 |
+
L.append(" if (decision > 0) scores[i] += 1;")
|
| 213 |
+
L.append(" else scores[j] += 1;")
|
| 214 |
+
L.append(" h++;")
|
| 215 |
+
L.append(" }")
|
| 216 |
+
L.append(" }")
|
| 217 |
+
L.append(" int best = 0;")
|
| 218 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 219 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 220 |
+
L.append(" }")
|
| 221 |
+
L.append(" return best;")
|
| 222 |
+
L.append("}")
|
| 223 |
+
else:
|
| 224 |
+
L.append("static const double SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
|
| 225 |
+
for row in coefs:
|
| 226 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 227 |
+
L.append(f" {{{vals}}},")
|
| 228 |
+
L.append("};")
|
| 229 |
+
L.append("")
|
| 230 |
+
vals = ", ".join([f"{v:.6f}" for v in intercepts])
|
| 231 |
+
L.append(f"static const double SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
|
| 232 |
+
L.append("")
|
| 233 |
+
L.append("int predict(double *features) {")
|
| 234 |
+
L.append(" double scores[N_CLASSES] = {0};")
|
| 235 |
+
L.append(" int h = 0;")
|
| 236 |
+
L.append(" for (int i = 0; i < N_CLASSES; i++) {")
|
| 237 |
+
L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
|
| 238 |
+
L.append(" double decision = SVM_INTERCEPT[h];")
|
| 239 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 240 |
+
L.append(" decision += SVM_COEF[h][f] * features[f];")
|
| 241 |
+
L.append(" }")
|
| 242 |
+
L.append(" if (decision > 0) scores[i] += 1.0;")
|
| 243 |
+
L.append(" else scores[j] += 1.0;")
|
| 244 |
+
L.append(" h++;")
|
| 245 |
+
L.append(" }")
|
| 246 |
+
L.append(" }")
|
| 247 |
+
L.append(" int best = 0;")
|
| 248 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 249 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 250 |
+
L.append(" }")
|
| 251 |
+
L.append(" return best;")
|
| 252 |
+
L.append("}")
|
| 253 |
+
|
| 254 |
+
elif model_name == "logreg" and hasattr(model, 'coef_'):
|
| 255 |
+
coefs = model.coef_
|
| 256 |
+
intercepts = model.intercept_
|
| 257 |
+
n_classes = coefs.shape[0] if len(model.classes_) > 2 else 2
|
| 258 |
+
n_features = coefs.shape[1]
|
| 259 |
+
L.append(f"/* Logistic Regression with {n_classes} classes, {n_features} features */")
|
| 260 |
+
L.append(f"#define N_FEATURES {n_features}")
|
| 261 |
+
L.append(f"#define N_CLASSES {coefs.shape[0]}")
|
| 262 |
+
L.append("")
|
| 263 |
+
|
| 264 |
+
if quantize:
|
| 265 |
+
scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
|
| 266 |
+
L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
|
| 267 |
+
L.append("static const int8_t LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
|
| 268 |
+
for row in coefs:
|
| 269 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 270 |
+
L.append(f" {{{vals}}},")
|
| 271 |
+
L.append("};")
|
| 272 |
+
L.append("")
|
| 273 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
|
| 274 |
+
L.append(f"static const int8_t LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
|
| 275 |
+
L.append("")
|
| 276 |
+
L.append("int predict(int8_t *features) {")
|
| 277 |
+
L.append(" int32_t scores[N_CLASSES];")
|
| 278 |
+
L.append(" for (int c = 0; c < N_CLASSES; c++) {")
|
| 279 |
+
L.append(f" scores[c] = LOGREG_INTERCEPT[c] * {int(scale_factor)};")
|
| 280 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 281 |
+
L.append(" scores[c] += (int32_t)LOGREG_COEF[c][f] * features[f];")
|
| 282 |
+
L.append(" }")
|
| 283 |
+
L.append(" }")
|
| 284 |
+
L.append(" int best = 0;")
|
| 285 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 286 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 287 |
+
L.append(" }")
|
| 288 |
+
L.append(" return best;")
|
| 289 |
+
L.append("}")
|
| 290 |
+
else:
|
| 291 |
+
L.append("static const double LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
|
| 292 |
+
for row in coefs:
|
| 293 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 294 |
+
L.append(f" {{{vals}}},")
|
| 295 |
+
L.append("};")
|
| 296 |
+
L.append("")
|
| 297 |
+
vals = ", ".join([f"{v:.6f}" for v in intercepts])
|
| 298 |
+
L.append(f"static const double LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
|
| 299 |
+
L.append("")
|
| 300 |
+
L.append("int predict(double *features) {")
|
| 301 |
+
L.append(" double scores[N_CLASSES];")
|
| 302 |
+
L.append(" for (int c = 0; c < N_CLASSES; c++) {")
|
| 303 |
+
L.append(" scores[c] = LOGREG_INTERCEPT[c];")
|
| 304 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 305 |
+
L.append(" scores[c] += LOGREG_COEF[c][f] * features[f];")
|
| 306 |
+
L.append(" }")
|
| 307 |
+
L.append(" }")
|
| 308 |
+
L.append(" int best = 0;")
|
| 309 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 310 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 311 |
+
L.append(" }")
|
| 312 |
+
L.append(" return best;")
|
| 313 |
+
L.append("}")
|
| 314 |
+
|
| 315 |
+
elif model_name == "small_nn" and hasattr(model, 'coefs_'):
|
| 316 |
+
layers = model.coefs_
|
| 317 |
+
biases = model.intercepts_
|
| 318 |
+
arch = " -> ".join([str(l.shape[0]) for l in layers] + [str(layers[-1].shape[1])])
|
| 319 |
+
L.append(f"/* MLP Neural Network: {len(layers)} layers */")
|
| 320 |
+
L.append(f"/* Architecture: {arch} */")
|
| 321 |
+
L.append("")
|
| 322 |
+
|
| 323 |
+
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 324 |
+
n_in, n_out = W.shape
|
| 325 |
+
L.append(f"#define L{idx}_IN {n_in}")
|
| 326 |
+
L.append(f"#define L{idx}_OUT {n_out}")
|
| 327 |
+
L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
|
| 328 |
+
for row in W:
|
| 329 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 330 |
+
L.append(f" {{{vals}}},")
|
| 331 |
+
L.append("};")
|
| 332 |
+
bvals = ", ".join([f"{v:.6f}" for v in b])
|
| 333 |
+
L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
|
| 334 |
+
L.append("")
|
| 335 |
+
|
| 336 |
+
L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
|
| 337 |
+
L.append("")
|
| 338 |
+
|
| 339 |
+
if quantize:
|
| 340 |
+
# Calculate global max for int8 scaling
|
| 341 |
+
max_val = max([np.max(np.abs(w)) for w in layers] + [np.max(np.abs(b)) for b in biases] + [1e-6])
|
| 342 |
+
scale_factor = 127.0 / max_val
|
| 343 |
+
L.append(f"/* INT8 Quantization Scale Factor: {scale_factor:.4f} */")
|
| 344 |
+
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 345 |
+
n_in, n_out = W.shape
|
| 346 |
+
L.append(f"static const int8_t W{idx}[{n_in}][{n_out}] = {{")
|
| 347 |
+
for row in W:
|
| 348 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 349 |
+
L.append(f" {{{vals}}},")
|
| 350 |
+
L.append("};")
|
| 351 |
+
bvals = ", ".join([str(int(round(v * scale_factor))) for v in b])
|
| 352 |
+
L.append(f"static const int8_t B{idx}[{n_out}] = {{{bvals}}};")
|
| 353 |
+
L.append("")
|
| 354 |
+
L.append("static inline int32_t relu_int(int32_t x) { return x > 0 ? x : 0; }")
|
| 355 |
+
L.append("")
|
| 356 |
+
L.append("int predict(int8_t *input) {")
|
| 357 |
+
for idx in range(len(layers)):
|
| 358 |
+
n_in = layers[idx].shape[0]
|
| 359 |
+
n_out = layers[idx].shape[1]
|
| 360 |
+
is_last = (idx == len(layers) - 1)
|
| 361 |
+
src = "input" if idx == 0 else f"a{idx-1}"
|
| 362 |
+
L.append(f" int32_t a{idx}[{n_out}];")
|
| 363 |
+
L.append(f" for (int j = 0; j < {n_out}; j++) {{")
|
| 364 |
+
L.append(f" a{idx}[j] = B{idx}[j] * {int(scale_factor)}; /* scale bias */")
|
| 365 |
+
L.append(f" for (int i = 0; i < {n_in}; i++) {{")
|
| 366 |
+
L.append(f" a{idx}[j] += (int32_t){src}[i] * W{idx}[i][j];")
|
| 367 |
+
L.append(f" }}")
|
| 368 |
+
if not is_last:
|
| 369 |
+
L.append(f" a{idx}[j] = relu_int(a{idx}[j]) / {int(scale_factor)}; /* rescale */")
|
| 370 |
+
L.append(f" }}")
|
| 371 |
+
last_idx = len(layers) - 1
|
| 372 |
+
last_out = layers[-1].shape[1]
|
| 373 |
+
L.append(f" int best = 0;")
|
| 374 |
+
L.append(f" for (int c = 1; c < {last_out}; c++) {{")
|
| 375 |
+
L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
|
| 376 |
+
L.append(f" }}")
|
| 377 |
+
L.append(f" return best;")
|
| 378 |
+
L.append("}")
|
| 379 |
+
else:
|
| 380 |
+
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 381 |
+
n_in, n_out = W.shape
|
| 382 |
+
L.append(f"#define L{idx}_IN {n_in}")
|
| 383 |
+
L.append(f"#define L{idx}_OUT {n_out}")
|
| 384 |
+
L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
|
| 385 |
+
for row in W:
|
| 386 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 387 |
+
L.append(f" {{{vals}}},")
|
| 388 |
+
L.append("};")
|
| 389 |
+
bvals = ", ".join([f"{v:.6f}" for v in b])
|
| 390 |
+
L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
|
| 391 |
+
L.append("")
|
| 392 |
+
|
| 393 |
+
L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
|
| 394 |
+
L.append("")
|
| 395 |
+
L.append("int predict(double *input) {")
|
| 396 |
+
for idx in range(len(layers)):
|
| 397 |
+
n_in = layers[idx].shape[0]
|
| 398 |
+
n_out = layers[idx].shape[1]
|
| 399 |
+
is_last = (idx == len(layers) - 1)
|
| 400 |
+
src = "input" if idx == 0 else f"a{idx-1}"
|
| 401 |
+
L.append(f" double a{idx}[{n_out}];")
|
| 402 |
+
L.append(f" for (int j = 0; j < {n_out}; j++) {{")
|
| 403 |
+
L.append(f" a{idx}[j] = B{idx}[j];")
|
| 404 |
+
L.append(f" for (int i = 0; i < {n_in}; i++) {{")
|
| 405 |
+
L.append(f" a{idx}[j] += {src}[i] * W{idx}[i][j];")
|
| 406 |
+
L.append(f" }}")
|
| 407 |
+
if not is_last:
|
| 408 |
+
L.append(f" a{idx}[j] = relu(a{idx}[j]);")
|
| 409 |
+
L.append(f" }}")
|
| 410 |
+
|
| 411 |
+
last_idx = len(layers) - 1
|
| 412 |
+
last_out = layers[-1].shape[1]
|
| 413 |
+
L.append(f" int best = 0;")
|
| 414 |
+
L.append(f" for (int c = 1; c < {last_out}; c++) {{")
|
| 415 |
+
L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
|
| 416 |
+
L.append(f" }}")
|
| 417 |
+
L.append(f" return best;")
|
| 418 |
+
L.append("}")
|
| 419 |
+
|
| 420 |
+
elif model_name == "knn" and hasattr(model, '_fit_X'):
|
| 421 |
+
n_samples = min(model._fit_X.shape[0], 100)
|
| 422 |
+
n_feats = model._fit_X.shape[1]
|
| 423 |
+
L.append(f"/* KNN Lookup Table: {n_samples} reference samples */")
|
| 424 |
+
L.append(f"#define N_NEIGHBORS {model.n_neighbors}")
|
| 425 |
+
L.append(f"#define N_SAMPLES {n_samples}")
|
| 426 |
+
L.append(f"#define N_FEATURES {n_feats}")
|
| 427 |
+
L.append("")
|
| 428 |
+
L.append("static const double REF[N_SAMPLES][N_FEATURES] = {")
|
| 429 |
+
for row in model._fit_X[:n_samples]:
|
| 430 |
+
vals = ", ".join([f"{v:.4f}" for v in row])
|
| 431 |
+
L.append(f" {{{vals}}},")
|
| 432 |
+
L.append("};")
|
| 433 |
+
L.append("")
|
| 434 |
+
labels_str = ", ".join([str(int(l)) for l in model._y[:n_samples]])
|
| 435 |
+
L.append(f"static const int LABELS[N_SAMPLES] = {{{labels_str}}};")
|
| 436 |
+
L.append("")
|
| 437 |
+
L.append("int predict(double *features) {")
|
| 438 |
+
L.append(" double dists[N_SAMPLES];")
|
| 439 |
+
L.append(" for (int i = 0; i < N_SAMPLES; i++) {")
|
| 440 |
+
L.append(" dists[i] = 0.0;")
|
| 441 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 442 |
+
L.append(" double d = features[f] - REF[i][f];")
|
| 443 |
+
L.append(" dists[i] += d * d;")
|
| 444 |
+
L.append(" }")
|
| 445 |
+
L.append(" }")
|
| 446 |
+
L.append(" int votes[10] = {0};")
|
| 447 |
+
L.append(" for (int k = 0; k < N_NEIGHBORS; k++) {")
|
| 448 |
+
L.append(" int mi = 0;")
|
| 449 |
+
L.append(" for (int i = 1; i < N_SAMPLES; i++) {")
|
| 450 |
+
L.append(" if (dists[i] < dists[mi]) mi = i;")
|
| 451 |
+
L.append(" }")
|
| 452 |
+
L.append(" votes[LABELS[mi]]++;")
|
| 453 |
+
L.append(" dists[mi] = 1e18;")
|
| 454 |
+
L.append(" }")
|
| 455 |
+
L.append(" int best = 0;")
|
| 456 |
+
L.append(" for (int i = 1; i < 10; i++) {")
|
| 457 |
+
L.append(" if (votes[i] > votes[best]) best = i;")
|
| 458 |
+
L.append(" }")
|
| 459 |
+
L.append(" return best;")
|
| 460 |
+
L.append("}")
|
| 461 |
+
else:
|
| 462 |
+
return {"error": f"Model {model_name} cannot be exported to C."}
|
| 463 |
+
|
| 464 |
+
return {"code": "\n".join(L)}
|
| 465 |
+
except Exception as e:
|
| 466 |
+
return {"error": f"Export failed: {str(e)}"}
|
| 467 |
+
|
| 468 |
+
@app.post("/retrain")
|
| 469 |
+
async def retrain(file: UploadFile = File(...)):
|
| 470 |
+
data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
|
| 471 |
+
os.makedirs(os.path.dirname(data_path), exist_ok=True)
|
| 472 |
+
|
| 473 |
+
content = await file.read()
|
| 474 |
+
with open(data_path, "wb") as f:
|
| 475 |
+
f.write(content)
|
| 476 |
+
|
| 477 |
+
try:
|
| 478 |
+
from models import train_models
|
| 479 |
+
await asyncio.to_thread(train_models)
|
| 480 |
+
|
| 481 |
+
global ensemble_system
|
| 482 |
+
ensemble_system = None
|
| 483 |
+
|
| 484 |
+
return {"status": "success", "message": "Ensemble retrained successfully!"}
|
| 485 |
+
except Exception as e:
|
| 486 |
+
return {"error": str(e)}
|
model/classes.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a72e398999fbcd2495f31ec5f28f07e8325e903d836d2f2fbef872d6da2785bb
|
| 3 |
+
size 440
|
model/knn.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0fd25eec3d294ae23ef7fcce62dad8c695f585a149935845bdcd4771b3271e61
|
| 3 |
+
size 6440614
|
model/label_encoder.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:da67513db74567d59333258b4d1982743cfdc80e54a59feb67922e7ea1bb92a9
|
| 3 |
+
size 543
|
model/logreg.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f065ca55b8b3017459446e339341e947635c15b9919b7e033707e261d451662f
|
| 3 |
+
size 1615
|
model/rf.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1e9d9d36ed1ae8016273c4c72d77043ea059db8e359059f29b05f40885441daa
|
| 3 |
+
size 171569
|
model/scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d084c7de7a9421241c7eda590f17b42175ca4b042162377056f45351d0bc82f4
|
| 3 |
+
size 1127
|
model/small_nn.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4aa30676f4f08388fa0bcb22823e4dfc0ef001ef834b56d0caafe6ced207c0f9
|
| 3 |
+
size 22664
|
model/svm.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bb1d7c34921c17009437b565526ed302366625bcf74a8068f8e6e2e71e2d9ce6
|
| 3 |
+
size 899439
|
models.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import joblib
|
| 3 |
+
from sklearn.neighbors import KNeighborsClassifier
|
| 4 |
+
from sklearn.svm import SVC
|
| 5 |
+
from sklearn.linear_model import LogisticRegression
|
| 6 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 7 |
+
from sklearn.neural_network import MLPClassifier
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
from preprocessing import get_train_test_split, resolve_model_dir
|
| 13 |
+
|
| 14 |
+
def train_models():
|
| 15 |
+
data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
|
| 16 |
+
model_dir = resolve_model_dir()
|
| 17 |
+
os.makedirs(model_dir, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
print("Loading data...")
|
| 20 |
+
df = pd.read_csv(data_path)
|
| 21 |
+
X_train, X_test, y_train, y_test = get_train_test_split(df)
|
| 22 |
+
|
| 23 |
+
models = {
|
| 24 |
+
'knn': KNeighborsClassifier(n_neighbors=5),
|
| 25 |
+
'svm': SVC(kernel='linear', probability=True, max_iter=2000),
|
| 26 |
+
'logreg': LogisticRegression(max_iter=1000),
|
| 27 |
+
'rf': RandomForestClassifier(n_estimators=30, max_depth=5, random_state=42),
|
| 28 |
+
'small_nn': MLPClassifier(hidden_layer_sizes=(16, 8), max_iter=500, random_state=42)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
for name, model in models.items():
|
| 32 |
+
print(f"Training {name}...")
|
| 33 |
+
model.fit(X_train, y_train)
|
| 34 |
+
joblib.dump(model, f'{model_dir}/{name}.pkl')
|
| 35 |
+
|
| 36 |
+
print("Training Complete.")
|
| 37 |
+
|
| 38 |
+
if __name__ == '__main__':
|
| 39 |
+
train_models()
|
preprocessing.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import os
|
| 4 |
+
import joblib
|
| 5 |
+
from sklearn.model_selection import train_test_split
|
| 6 |
+
from sklearn.preprocessing import StandardScaler, LabelEncoder
|
| 7 |
+
|
| 8 |
+
def resolve_model_dir():
|
| 9 |
+
# Resolve relative paths universally whether Docker or local execution
|
| 10 |
+
return '/app/model' if os.path.exists('/app/model') else '../model' if os.path.exists('../model') else 'model'
|
| 11 |
+
|
| 12 |
+
def preprocess_data(df, is_training=True):
|
| 13 |
+
model_dir = resolve_model_dir()
|
| 14 |
+
scaler_path = f'{model_dir}/scaler.pkl'
|
| 15 |
+
label_encoder_path = f'{model_dir}/label_encoder.pkl'
|
| 16 |
+
|
| 17 |
+
columns_to_drop = ['Patient Number', 'Data Accuracy (%)', 'Heart Rate Alert', 'SpO2 Level Alert', 'Blood Pressure Alert', 'Temperature Alert']
|
| 18 |
+
cols_drop = [c for c in columns_to_drop if c in df.columns]
|
| 19 |
+
X_raw = df.drop(columns=cols_drop)
|
| 20 |
+
|
| 21 |
+
y = None
|
| 22 |
+
if 'Predicted Disease' in X_raw.columns:
|
| 23 |
+
y_raw = X_raw['Predicted Disease']
|
| 24 |
+
X_raw = X_raw.drop(columns=['Predicted Disease'])
|
| 25 |
+
|
| 26 |
+
if is_training:
|
| 27 |
+
label_encoder = LabelEncoder()
|
| 28 |
+
y = label_encoder.fit_transform(y_raw)
|
| 29 |
+
os.makedirs(os.path.dirname(label_encoder_path), exist_ok=True)
|
| 30 |
+
joblib.dump(label_encoder, label_encoder_path)
|
| 31 |
+
joblib.dump(label_encoder.classes_, f'{model_dir}/classes.pkl')
|
| 32 |
+
else:
|
| 33 |
+
if os.path.exists(label_encoder_path):
|
| 34 |
+
label_encoder = joblib.load(label_encoder_path)
|
| 35 |
+
y = label_encoder.transform(y_raw)
|
| 36 |
+
else:
|
| 37 |
+
y = y_raw
|
| 38 |
+
|
| 39 |
+
if 'Fall Detection' in X_raw.columns:
|
| 40 |
+
X_raw['Fall Detection'] = X_raw['Fall Detection'].map({'Yes': 1, 'No': 0}).fillna(0)
|
| 41 |
+
|
| 42 |
+
X_raw.fillna(X_raw.mean(), inplace=True)
|
| 43 |
+
|
| 44 |
+
# Engineered Feature
|
| 45 |
+
X_raw['Risk_Severity'] = (X_raw['Heart Rate (bpm)'] > 105).astype(int) + (X_raw['SpO2 Level (%)'] < 94).astype(int)
|
| 46 |
+
|
| 47 |
+
continuous_features = ['Heart Rate (bpm)', 'SpO2 Level (%)', 'Systolic Blood Pressure (mmHg)', 'Diastolic Blood Pressure (mmHg)', 'Body Temperature (°C)', 'Risk_Severity']
|
| 48 |
+
|
| 49 |
+
if is_training:
|
| 50 |
+
scaler = StandardScaler()
|
| 51 |
+
X_raw[continuous_features] = scaler.fit_transform(X_raw[continuous_features])
|
| 52 |
+
joblib.dump(scaler, scaler_path)
|
| 53 |
+
else:
|
| 54 |
+
if os.path.exists(scaler_path):
|
| 55 |
+
scaler = joblib.load(scaler_path)
|
| 56 |
+
X_raw[continuous_features] = scaler.transform(X_raw[continuous_features])
|
| 57 |
+
|
| 58 |
+
return X_raw, y
|
| 59 |
+
|
| 60 |
+
def get_train_test_split(df):
|
| 61 |
+
X, y = preprocess_data(df, is_training=True)
|
| 62 |
+
return train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
scikit-learn==1.8.0
|
| 4 |
+
pandas
|
| 5 |
+
numpy
|
| 6 |
+
joblib
|
| 7 |
+
shap
|
| 8 |
+
m2cgen
|
| 9 |
+
matplotlib
|
| 10 |
+
python-multipart
|