Skip to content

Commit 08542a3

Browse files
Final changes
1 parent 3eeacdd commit 08542a3

9 files changed

Lines changed: 230 additions & 117 deletions

File tree

README.md

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
# TinyML Heart Health Monitoring Dashboard
1+
# Heart Health Monitoring Dashboard
22

3-
![Python Version](https://img.shields.io/badge/python-3.9%2B-blue)
4-
![FastAPI](https://img.shields.io/badge/FastAPI-0.100%2B-009688?logo=fastapi)
5-
![Streamlit](https://img.shields.io/badge/Streamlit-1.25%2B-FF4B4B?logo=streamlit)
6-
![License](https://img.shields.io/badge/license-MIT-green)
3+
A user-friendly web application for monitoring heart health metrics and predicting patient risks using Machine Learning. This project features a responsive Streamlit frontend and a robust FastAPI backend.
74

8-
A scalable, end-to-end Machine Learning ecosystem designed to provide heart health analytics, ensemble modeling predictions, and Edge AI deployment capabilities for resource-constrained microcontrollers.
5+
## 🌟 Key Engineering Achievements (Project Highlights)
6+
7+
From a systems engineering and machine learning perspective, this platform demonstrates several advanced concepts:
8+
9+
- **TinyML & Edge Computing Deployment**: Designed with hardware constraints in mind, this project transpiles complex Python ML models into highly optimized, dependency-free **C-code headers**. By utilizing **INT8 Quantization**, the system mathematically compresses 64-bit floating-point weights down to 8-bit integers, reducing the memory footprint by ~75%. This allows predictive models to be deployed directly onto severely resource-constrained microcontrollers (like the ESP32 or ARM Cortex-M) ensuring offline, ultra-low latency, and privacy-preserving inference right at the edge.
10+
- **Dynamic Hardware Profiling**: The deployment engine actively calculates predictive heuristics, such as expected inference latency (in microseconds) and flash memory payload size, mapped against specific embedded hardware profiles (e.g., Arduino Nano 33 BLE, Raspberry Pi Pico) before the code is even exported.
11+
- **Interpretable AI (SHAP)**: To solve the "black box" problem common in healthcare tech, the backend generates real-time SHAP (SHapley Additive exPlanations) values. This provides explicit, feature-level transparency into *why* the AI made a specific clinical decision, building essential trust with end-users.
12+
- **Robust Model Ensembling**: Rather than relying on a single algorithm, the system features a Soft-Voting Ensemble architecture that aggregates predictions across five distinct model architectures (KNN, SVM, Logistic Regression, Random Forest, and a Neural Network) to maximize predictive accuracy and minimize bias.
913

1014
---
1115

1216
## 🏗️ System Architecture
1317

14-
The system follows a separated frontend/backend architecture, enabling scaling flexibility and clean separation of concerns.
18+
We designed the system with strict decoupling between the client UI and the heavy-lifting ML pipeline. Doing this lets us scale instances efficiently while enabling simple API integration for other health services later on.
1519

1620
```mermaid
1721
flowchart TD
@@ -32,87 +36,87 @@ flowchart TD
3236
3337
%% ML Subgraph
3438
subgraph ML [🧠 Machine Learning & Inference Pipeline]
35-
ENS{Ensemble Engine<br/>Soft-Voting Aggregator}:::ml_node
39+
ENS{Ensemble Engine<br/>Soft-Voting}:::ml_node
3640
Models[Classifiers:<br/>RF, SVM, LogReg, NN, KNN]:::ml_node
37-
SHAP[🔍 SHAP Explainer<br/>Feature Impact Analysis]:::ml_node
41+
SHAP[🔍 SHAP Explainer]:::ml_node
3842
end
3943
4044
%% Edge Quantization Subgraph
41-
subgraph Quantization [⚡ Hardware & Edge AI Export]
45+
subgraph Quantization [⚡ Hardware Export]
4246
TRANS[C-Code Transpiler<br/>INT8 Quantization]:::edge_tech
43-
HEADER((tinyml_model.h<br/>Optimized Header File)):::edge_tech
44-
MCU>📟 ESP32 / ARM Cortex-M<br/>Microcontroller Node]:::edge_tech
47+
HEADER((tinyml_model.h)):::edge_tech
48+
MCU>ESP32 / Cortex-M Node]:::edge_tech
4549
end
4650
47-
%% Routing Connections
4851
API -->|Predict Request| ENS
4952
ENS --> Models
50-
5153
API -->|Explain Request| SHAP
52-
SHAP -.->|Analyzes Decision Trees| Models
53-
54+
SHAP -.-> Models
5455
API -->|Export Request| TRANS
55-
TRANS -->|Scales FP32 to INT8| HEADER
56+
TRANS --> HEADER
5657
HEADER -->|Flash Firmware| MCU
5758
```
5859

5960
---
6061

61-
## ✨ Key Technical Features
62-
63-
### 1. TinyML Edge Deployment & INT8 Quantization 📉
64-
The core value proposition is executing model inference offline on devices like the ESP32 and ARM Cortex-M. The backend transpiler natively parses trained models (Logistic Regression, Support Vector Machines, Neural Networks, K-Nearest Neighbors) into portable `stdint.h` C-code binaries.
65-
66-
Crucially, an automated **INT8 Quantization pipeline** is available. This procedure calculates appropriate linear scaling factors globally across Neural Network layers or SVM hyperplanes to convert 64-bit Floating Point (`double`) weights into constrained 8-bit Integer (`int8_t`) representations. This technique drastically reduces firmware flash size requirements (by approximately 75%) and limits execution to low-power integer arithmetic operations.
67-
68-
### 2. Clinical Model Interpretability (Explainable AI) 🧠
69-
Predictive medical systems cannot function as black boxes. By employing **SHAP (SHapley Additive exPlanations)**, the FastAPI backend interprets Random Forest decision trees, resolving the algebraic impact weights of individual clinical variables (e.g. SpO2 vs Heart Rate) over the final prediction. These explanations are visually charted on the frontend to explicitly map risk-increasing or protective physiological factors.
62+
## 🚀 Getting Started
7063

71-
### 3. Ensemble Prediction Algorithm 🤝
72-
The framework combines the predictive strengths of various fundamental Machine Learning techniques. Instead of relying on a singular hypothesis, an ensemble pipeline coordinates inferences from KNN, SVM, Logistic Regression, Random Forest, and a Multi-layer Perceptron. A soft-voting probability aggregator dictates the final classification, balancing variance and bias.
64+
Follow these instructions to get a copy of the project up and running on your local machine.
7365

74-
### 4. MLOps CI/CD and Drift Adjustment 🔄
75-
System performance over time heavily correlates with dataset drift. The integrated MLOps framework facilitates straightforward uploading of new Patient CSV cohorts payload schemas. The system will asynchronously restart the Scikit-Learn training pipelines across all active supervised learning models, store updated `.pkl` binaries, and seamlessly refresh caching layers to serve the updated ecosystem in realtime.
66+
### Prerequisites
67+
- Python 3.9 or higher
7668

77-
## Installation & Setup
69+
### Installation
7870

7971
1. **Clone the repository:**
8072
```bash
8173
git clone https://github.com/IDKHowToCodeFR/TinyML-Heart-Health-Monitoring-Dashboard.git
8274
cd TinyML-Heart-Health-Monitoring-Dashboard
8375
```
8476

85-
2. **Initialize Python environments and dependencies:**
86-
Ensure Python 3.9+ is installed.
77+
2. **Install Dependencies:**
78+
Ensure you have all required libraries installed.
8779
```bash
88-
python -m pip install -r requirements.txt
80+
pip install -r requirements.txt
8981
pip install -r backend/requirements.txt
9082
pip install -r frontend/requirements.txt
9183
```
9284

93-
3. **Train initial `.pkl` Models:**
94-
The dashboard requires foundational model states to establish the FastAPI ensemble arrays.
85+
3. **Train Initial Models:**
86+
Before starting the system, generate the initial machine learning models.
9587
```bash
9688
cd backend
9789
python models.py
9890
cd ..
9991
```
10092

101-
## Running the Application Locally
93+
## 💻 Usage
10294

103-
1. **Start the FastAPI Backend Service:**
95+
### Quick Start (Windows)
96+
If you are on Windows, simply run the included batch file to start both the frontend and backend automatically:
97+
```bash
98+
run.bat
99+
```
100+
101+
### Manual Start
102+
1. **Start the Backend:**
104103
```bash
105104
cd backend
106105
uvicorn main:app --host 0.0.0.0 --port 8000
107106
```
108-
109-
2. **Launch the Streamlit Frontend Client:** (In a separate terminal)
107+
2. **Start the Frontend:**
108+
Open a new terminal window and run:
110109
```bash
111110
cd frontend
112111
streamlit run Home.py
113112
```
114113

115-
The Streamlit UI will bind to `localhost:8501`. Navigate through the sidebar implementations to access prediction simulation, SHAP interpretation, or Edge specific transpilation outputs.
114+
### Docker
115+
If you prefer Docker, you can spin up the entire project with one command:
116+
```bash
117+
docker-compose up --build
118+
```
119+
Once running, open your browser and go to `http://localhost:8501`.
116120

117-
## License
118-
MIT License. See `LICENSE` for details.
121+
## 📜 License
122+
This project is licensed under the MIT License.

backend/database.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,44 @@
11
import sqlite3
22
import os
33
from datetime import datetime
4+
from huggingface_hub import HfApi, hf_hub_download
45

5-
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'patient_history.db')
6+
REPO_ID = "IDKHowToCodeFr/tinyml-logs"
7+
DB_NAME = 'patient_history.db'
8+
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_NAME)
9+
HF_TOKEN = os.getenv("HF_TOKEN")
10+
11+
api = HfApi()
12+
13+
def sync_from_hub():
14+
if not HF_TOKEN:
15+
print("No HF_TOKEN found. Skipping sync.")
16+
return
17+
try:
18+
print(f"Downloading {DB_NAME} from Hub...")
19+
path = hf_hub_download(repo_id=REPO_ID, filename=DB_NAME, repo_type="dataset", token=HF_TOKEN)
20+
import shutil
21+
shutil.copy(path, DB_PATH)
22+
print("Sync from Hub complete.")
23+
except Exception as e:
24+
print(f"Sync from Hub failed (maybe first run?): {e}")
25+
26+
def sync_to_hub():
27+
if not HF_TOKEN:
28+
return
29+
try:
30+
api.upload_file(
31+
path_or_fileobj=DB_PATH,
32+
path_in_repo=DB_NAME,
33+
repo_id=REPO_ID,
34+
repo_type="dataset",
35+
token=HF_TOKEN
36+
)
37+
except Exception as e:
38+
print(f"Sync to Hub failed: {e}")
639

740
def init_db():
41+
sync_from_hub()
842
conn = sqlite3.connect(DB_PATH)
943
cursor = conn.cursor()
1044
cursor.execute('''
@@ -43,23 +77,21 @@ def log_prediction(data, prediction_label, confidence):
4377
))
4478
conn.commit()
4579
conn.close()
80+
sync_to_hub()
4681

4782
def get_history():
83+
if not os.path.exists(DB_PATH):
84+
return []
4885
conn = sqlite3.connect(DB_PATH)
4986
cursor = conn.cursor()
5087
cursor.execute('SELECT * FROM predictions ORDER BY timestamp DESC LIMIT 100')
5188
rows = cursor.fetchall()
52-
53-
# Get column names
5489
col_names = [description[0] for description in cursor.description]
55-
5690
conn.close()
5791

58-
# Format as list of dicts
5992
history = []
6093
for row in rows:
6194
history.append(dict(zip(col_names, row)))
62-
6395
return history
6496

6597
# Initialize db when this module is loaded

backend/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ async def predict(data: PatientData):
9999
def history():
100100
return get_history()
101101

102+
@app.get("/dataset")
103+
async def get_dataset():
104+
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'
105+
if os.path.exists(data_path):
106+
df = pd.read_csv(data_path)
107+
# Return summary or partial data to avoid huge payloads
108+
return df.to_dict(orient="records")
109+
return {"error": "Dataset not found"}
110+
111+
@app.get("/sync")
112+
def force_sync():
113+
from database import sync_from_hub, sync_to_hub
114+
sync_from_hub()
115+
return {"status": "Sync attempted"}
116+
102117
@app.post("/explain")
103118
async def explain(data: PatientData):
104119
eng = get_ensemble()

backend/patient_history.db

0 Bytes
Binary file not shown.

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ shap
88
m2cgen
99
matplotlib
1010
python-multipart
11+
huggingface_hub

frontend/Home.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,15 @@
7979

8080
# ──────── System Status ────────
8181
try:
82-
resp = requests.get(f"{API_URL}/health", timeout=2)
83-
status = resp.json().get("status", "Unknown")
84-
is_online = "Healthy" in status
85-
except Exception:
86-
status = "Cloud Mode (Monolith Fallback)"
82+
resp = requests.get(f"{API_URL}/health", timeout=5)
83+
if resp.status_code == 200:
84+
status = resp.json().get("status", "Unknown")
85+
is_online = "Healthy" in status
86+
else:
87+
status = f"Backend Error ({resp.status_code})"
88+
is_online = False
89+
except Exception as e:
90+
status = f"Offline / Monolith Fallback ({str(e)[:50]})"
8791
is_online = False
8892

8993
badge_class = "status-online" if is_online else "status-fallback"
@@ -176,7 +180,7 @@ def live_metrics():
176180
ad_c = "#00cc96" if ad_good else "#ef553b"
177181
ad_d = "delta-good" if ad_good else "delta-bad"
178182
ad_i = "▼" if ad_good else "▲"
179-
ad_txt = f"{abs(alert_delta)} fewer" if ad_good else f"+{alert_delta} more"
183+
ad_txt = f"{abs(alert_delta)} fewer" if ad_good else f"{alert_delta} more"
180184

181185
hr_c = "#00cc96" if hr_ok else "#ef553b"
182186
hr_d = "delta-good" if hr_ok else "delta-bad"
@@ -212,45 +216,50 @@ def live_metrics():
212216
</div>
213217
""", unsafe_allow_html=True)
214218

215-
# ──── Candlestick chart: tracks alert level changes ────
216-
st.markdown("##### 📡 Live Alert Activity (Candlestick + Moving Average)")
219+
# ──── Smoothed Activity Area Chart: tracks average heart-rate & alerts ────
220+
st.markdown("##### 📡 Live Patient Emulation Trace")
217221

218222
cd = st.session_state.chart_candles
219223
ts = [x["time"] for x in cd]
220-
op = [x["open"] for x in cd]
221-
hi = [x["high"] for x in cd]
222-
lo = [x["low"] for x in cd]
224+
# Represent the primary metric as the close/current state
223225
cl = [x["close"] for x in cd]
224226

225227
fig = go.Figure()
226-
fig.add_trace(go.Candlestick(
227-
x=ts, open=op, high=hi, low=lo, close=cl,
228-
increasing=dict(line=dict(color="#ef553b"), fillcolor="rgba(239,85,59,0.4)"),
229-
decreasing=dict(line=dict(color="#00cc96"), fillcolor="rgba(0,204,150,0.4)"),
230-
name="Alerts"
228+
229+
# Main Area Waveform
230+
fig.add_trace(go.Scatter(
231+
x=ts, y=cl,
232+
mode='lines',
233+
line=dict(color='#00f2fe', width=3, shape='spline', smoothing=1.3),
234+
fill='tozeroy',
235+
fillcolor='rgba(0, 242, 254, 0.15)',
236+
name="Activity Index"
231237
))
232238

233-
w = min(7, len(cl))
239+
# Add a thin secondary baseline trend
240+
w = min(10, len(cl))
234241
ma = pd.Series(cl).rolling(window=w, min_periods=1).mean().tolist()
235242
fig.add_trace(go.Scatter(
236243
x=ts, y=ma, mode='lines',
237-
line=dict(color='rgba(79,172,254,0.9)', width=2.5, shape='spline'),
238-
name="MA-7"
244+
line=dict(color='rgba(255, 255, 255, 0.4)', width=1.5, dash='dot'),
245+
name="Moving Average"
239246
))
240247

241-
vol = [abs(cv - ov) * 2 + 1 for ov, cv in zip(op, cl)]
242-
vc = ["rgba(239,85,59,0.2)" if cv >= ov else "rgba(0,204,150,0.2)" for ov, cv in zip(op, cl)]
243-
fig.add_trace(go.Bar(x=ts, y=vol, marker=dict(color=vc), yaxis="y2", showlegend=False, hoverinfo='skip'))
244-
245248
fig.update_layout(
246249
template="plotly_dark", height=320,
247250
margin=dict(l=0, r=50, t=10, b=0),
248251
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
249-
xaxis=dict(gridcolor='rgba(255,255,255,0.03)', showgrid=False, rangeslider=dict(visible=False)),
250-
yaxis=dict(title="Alert Level", gridcolor='rgba(255,255,255,0.05)', title_font=dict(size=11), side="right", range=[0, ALERT_MAX + 10]),
251-
yaxis2=dict(overlaying="y", side="left", showgrid=False, showticklabels=False, range=[0, max(vol)*5] if vol else [0, 20]),
252+
xaxis=dict(gridcolor='rgba(255,255,255,0.03)', showgrid=True, rangeslider=dict(visible=False)),
253+
yaxis=dict(
254+
title="Relative Activity",
255+
gridcolor='rgba(255,255,255,0.05)',
256+
title_font=dict(size=11),
257+
side="right",
258+
range=[0, ALERT_MAX + 15]
259+
),
252260
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, font=dict(size=10)),
253-
showlegend=True
261+
showlegend=True,
262+
hovermode="x unified"
254263
)
255264
st.plotly_chart(fig, use_container_width=True)
256265

0 commit comments

Comments
 (0)