IDKHowToCodeFr commited on
Commit
f7b331f
·
verified ·
1 Parent(s): 03fade0

Upload folder using huggingface_hub

Browse files
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