Downloads · 30 days
0
malansi/gsr-hrv-stress-detection-cnn-lstm
gsr-hrv-stress-detection-cnn-lstm is a tabular classification model from malansi. Use it for the tabular classification task on the model card, and read the license before you ship it in a product. It is set up for pytorch. The card lists the license as cc-by-nc-4.0.
A subject-independent deep learning model that classifies a person's physiological state — Relaxed vs. Stressed — from two low-cost wearable signals: Galvanic Skin Response (GSR / skin conductance) and Heart Rate Vari…
Downloads · 30 days
0
Access
Public
Updated Jul 13, 2026
Repo size
357 KB
Likes
0
Public
Click a slice to open those files.
.pt240 KB · 55%
From the Hugging Face model README
A subject-independent deep learning model that classifies a person's physiological state — Relaxed vs. Stressed — from two low-cost wearable signals: Galvanic Skin Response (GSR / skin conductance) and Heart Rate Variability (RR intervals). Developed as an applied research project at Almutlaq United Company, targeting real-time fatigue/stress monitoring for field workers via wearable sensors reporting to a central server.
Full training pipeline, data-cleaning scripts, and experiment history: github.com/Alansi775/GSR-COSSINUS_VOLUNTEERS_DATA_FOR_ML
| Task | Binary classification: Relaxed (0) vs. Stressed (1) |
| Input | 30-second windows of GSR + RR-interval signal (1 Hz) + 5 engineered HRV/GSR features |
| Architecture | CNN (Conv1D ×2) + BiLSTM + attention pooling, concatenated with an MLP feature branch into a dense classifier |
| Cross-validated accuracy | 73.59% ± 3.90% (5-fold, subject-wise, N = 104 volunteers, 2,592 windows) |
| Shipped checkpoint accuracy | 76.13% on its own held-out split (507 windows, 21 volunteers) |
The model is a hybrid architecture with two branches that are combined before classification:
The two representations are concatenated and passed through a final classifier head (Dense → 2 classes).
Input Sequence (30×3) → Conv1D×2 + BatchNorm + MaxPool → BiLSTM → Attention Pool ─┐
├─→ Concatenate → Dense → Relaxed / Stressed
Engineered Features (5) → Dense + BatchNorm ──────────────────────────────────────────────┘
Why CNN+LSTM instead of a classical model (SVM / Logistic Regression)? Classical models operate on a fixed, hand-crafted feature vector per window and cannot learn how the signal evolves second-by-second. The CNN+LSTM branch adds that temporal modeling while remaining lightweight enough for fast CPU inference — importantly, in the target deployment, inference runs on a central server (not the wearable itself), so model complexity does not affect sensor battery life.
Intended for:
Not intended for:
Known limitations:
Custom dataset of 150+ volunteers following an identical 4-phase protocol (Calibration ≈30s → Normal ≈4min → Stress ≈3min "time-pressure task" → Relaxation ≈1min), recorded at 1 Hz. Two GSR/HRV channels were logged per second: Conductance_microS and RR_Interval_ms.
Data-quality issues found and corrected (full detail and code in the linked GitHub repo):
conductance = 1 / resistance.RR_Interval_ms populated for fewer than 10 rows out of 500+, found via a dedicated zero-window diagnostic rather than a simple NaN count.5-fold subject-wise cross-validation — 5 independently trained models, each with a different held-out group of volunteers:
| Fold | Test Accuracy |
|---|---|
| 1 | 76.57% |
| 2 | 77.18% |
| 3 | 66.28% |
| 4 | 74.41% |
| 5 | 73.53% |
| Mean | 73.59% ± 3.90% |
Fold 3's lower accuracy is a real signal of inter-subject variability (a harder held-out group of volunteers), not an error — this is exactly the kind of spread a single train/val/test split would hide, and part of why subject-wise cross-validation is reported here as the primary figure instead of one split's number.
Pooled classification report (all 5 folds, 2,592 windows):
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| Relaxed | 0.754 | 0.812 | 0.782 | 1,510 |
| Stressed | 0.706 | 0.630 | 0.666 | 1,082 |
best_model_stress.pt)The specific checkpoint included in this repository was trained once on a single train/val/test split and reached 76.13% accuracy on its own 507-window held-out test set:
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| Relaxed | 0.818 | 0.772 | 0.795 | 303 |
| Stressed | 0.688 | 0.745 | 0.715 | 204 |
Confusion matrix and training curves for this checkpoint are included as confusion_matrix.png and training_curves.png in this repository. Per-fold accuracy from cross-validation is plotted in kfold_accuracy.png.
import torch
import numpy as np
# model.py defines CNNLSTMHybrid — see the GitHub repo for the full class definition
from model import CNNLSTMHybrid
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = CNNLSTMHybrid(n_channels=3, n_scalar_features=5).to(device)
model.load_state_dict(torch.load("best_model_stress.pt", map_location=device))
model.eval()
# Load the saved normalization stats (fit on the training split only)
seq_mean = np.load("global_mean.npy")
seq_std = np.load("global_std.npy")
feat_mean = np.load("global_feat_mean.npy")
feat_std = np.load("global_feat_std.npy")
# seq: [1, 30, 3] -> (GSR_z, RR_z, dGSR_z) per second, baseline-normalized per subject first
# feat: [1, 5] -> (RMSSD, SDNN, GSR slope, GSR range, GSR peak count)
seq_norm = (seq - seq_mean) / seq_std
feat_norm = (feat - feat_mean) / feat_std
with torch.no_grad():
logits = model(torch.from_numpy(seq_norm).float().to(device),
torch.from_numpy(feat_norm).float().to(device))
pred = logits.argmax(dim=1).item() # 0 = Relaxed, 1 = Stressed
Full preprocessing (per-volunteer baseline normalization, windowing, feature extraction) and a ready-to-run inference script (predict.py) are in the linked GitHub repository.
| File | Description |
|---|---|
best_model_stress.pt | Trained PyTorch model weights |
global_mean.npy, global_std.npy | Global normalization stats for the 3 sequence channels (fit on train split) |
global_feat_mean.npy, global_feat_std.npy | Global normalization stats for the 5 engineered features (fit on train split) |
confusion_matrix.png | Confusion matrix for the shipped checkpoint's held-out test split |
training_curves.png | Train/validation loss and accuracy curves for the shipped checkpoint |
kfold_accuracy.png | Per-fold accuracy from 5-fold subject-wise cross-validation |
classification_report.txt, kfold_summary.txt | Full text metrics |
This model was trained on physiological data collected from consenting volunteers under a controlled research protocol. It is not a diagnostic tool and should not be used to make unsupervised decisions about an individual's health, employment status, or fitness for duty. Any field deployment should retain human review of model outputs, especially for borderline-confidence predictions, and should be paired with per-worker calibration (a short relaxed-baseline recording) rather than applied as a one-size-fits-all threshold.
Mohammed Saleh Almutlaq United Company
Code, data-cleaning scripts, and full experiment history: github.com/Alansi775/GSR-COSSINUS_VOLUNTEERS_DATA_FOR_ML